///|
fn rich_text_with_composition(
  text : String,
  control : @core.TextControlStateContext,
  focused~ : Bool,
) -> String {
  if !focused {
    text
  } else {
    match control.composition {
      Some(composition) =>
        replace_current_selection(
          text,
          control.caret,
          control.selection,
          composition,
        ).text
      None => text
    }
  }
}

///|
fn rich_text_visual_caret(
  text : String,
  control : @core.TextControlStateContext,
  focused~ : Bool,
) -> Int {
  let caret = @core.clamp_int(control.caret, 0, text_length(text))
  if !focused {
    caret
  } else {
    match control.composition {
      Some(composition) =>
        @core.composition_start_index(text, control.caret, control.selection) +
        @core.composition_cursor_offset(control, composition)
      None => caret
    }
  }
}

///|
fn rich_text_document_caret_rect_or_plain(
  document : RichTextDocument,
  source_caret : Int,
  content : @core.Rect,
  font : @core.FontSpec,
  text_system : @core.TextSystem,
  display_text : String,
) -> @core.Rect {
  match
    rich_text_document_caret_rect_at_source(
      document,
      source_caret,
      content,
      font,
      text_system~,
    ) {
    Some(rect) => rect
    None =>
      plain_rich_text_caret_rect(
        display_text, source_caret, content, font, text_system,
      )
  }
}

///|
fn plain_rich_text_caret_rect(
  text : String,
  caret : Int,
  content : @core.Rect,
  font : @core.FontSpec,
  text_system : @core.TextSystem,
) -> @core.Rect {
  let prefix = string_prefix(text, caret)
  let width = text_system.measure_text(
      @core.TextLayoutInput::new(
        text=prefix,
        font~,
        max_width=content.size.width,
      ),
    ).size.width
  @core.Rect::new(
    x=content.origin.x + width,
    y=content.origin.y + 4.0,
    width=1.5,
    height=max_double(10.0, font.size * 1.55 - 8.0),
  )
}

///|
fn[Msg] rich_text_line_edge_move_result(
  ctx : @core.ViewEventContext,
  text : String,
  home~ : Bool,
  on_selection_change : ((Int, @core.TextRange?) -> Msg)?,
) -> @core.ViewEventResult[Msg] {
  let caret = if home {
    multiline_line_start(text, ctx.text_control.caret)
  } else {
    multiline_line_end(text, ctx.text_control.caret)
  }
  let next_control = {
    ..ctx.text_control,
    caret,
    selection: None,
    composition: None,
    composition_cursor: None,
  }
  let changed = next_control.caret != ctx.text_control.caret ||
    ctx.text_control.selection is Some(_) ||
    ctx.text_control.composition is Some(_) ||
    ctx.text_control.composition_cursor is Some(_)
  view_event_result(
    changed~,
    captured=true,
    text_control=Some(next_control),
    dirty=ViewPaintDirty,
    messages=if changed {
      selection_messages(
        on_selection_change,
        next_control.caret,
        next_control.selection,
      )
    } else {
      []
    },
  )
}

///|
fn rich_text_vertical_target_offset(
  document : RichTextDocument,
  source_caret : Int,
  caret_rect : @core.Rect,
  content : @core.Rect,
  base_font : @core.FontSpec,
  text_system : @core.TextSystem,
  goal_x~ : Double,
  up~ : Bool,
) -> Int {
  if document.blocks.is_empty() {
    let line_height = if caret_rect.size.height > 0.0 {
      caret_rect.size.height
    } else {
      max_double(10.0, base_font.size * 1.55 - 8.0)
    }
    let target_y = if up {
      caret_rect.origin.y - line_height * 0.5
    } else {
      caret_rect.origin.y + caret_rect.size.height + line_height * 0.5
    }
    let target_point = @core.Point::new(x=goal_x, y=target_y)
    return rich_text_document_source_offset_at_point(
      document, target_point, content, base_font, text_system,
    ).unwrap_or(source_caret)
  }
  let mut block_index = -1
  let mut visual_line = 0
  let mut y = content.origin.y
  for index, block in document.blocks {
    let line_height = rich_text_block_line_height(
      block,
      base_font,
      text_system,
      block_width=content.size.width,
    )
    let block_height = line_height + block.inset.top + block.inset.bottom
    if rich_text_caret_before_block(source_caret, block) {
      block_index = index
      visual_line = 0
      break
    }
    match block.table {
      Some(_) => {
        // Stop on the table only when the caret maps into it; a caret below
        // the table must keep walking to its own block, exactly like the
        // caret rect walk.
        let caret_in_table = match block.source_range {
          Some(range) => source_caret <= range.end
          None => false
        }
        if caret_in_table {
          block_index = index
          visual_line = 0
          break
        }
      }
      None => ()
    }
    match rich_text_block_visual_offset(block, source_caret) {
      Some(vo) => {
        block_index = index
        visual_line = rich_text_visual_offset_to_line(block, vo)
        break
      }
      None => ()
    }
    y = y + block_height
  }
  if block_index < 0 {
    block_index = document.blocks.length() - 1
    let last_block = document.blocks[block_index]
    visual_line = rich_text_block_visual_line_count(last_block) - 1
    let last_line_height = rich_text_block_line_height(
      last_block,
      base_font,
      text_system,
      block_width=content.size.width,
    )
    let last_block_height = last_line_height +
      last_block.inset.top +
      last_block.inset.bottom
    y = y - last_block_height
  }
  let block = document.blocks[block_index]
  let step = rich_text_block_line_step(block, base_font, text_system)
  let frame_top = y + block.inset.top
  let line_count = rich_text_block_visual_line_count(block)
  // Leaving the block: step straight to the neighbouring line, skipping the
  // paragraph separators (blocks with no line box) in between. The pointer hit
  // test is deliberately not used here — its half-band rule answers "which
  // line did I click", not "which way am I travelling".
  if (up && visual_line == 0) || (!up && visual_line >= line_count - 1) {
    return rich_text_vertical_block_boundary_offset(
      document,
      block_index,
      source_caret,
      base_font,
      text_system,
      up~,
    )
  }
  let line_center_y = if visual_line >= 0 && visual_line < line_count {
    frame_top + (visual_line.to_double() + 0.5) * step
  } else {
    caret_rect.origin.y + caret_rect.size.height * 0.5
  }
  // Within the block: keep the caret column and move one visual line.
  let target_point = @core.Point::new(
    x=goal_x,
    y=if up { line_center_y - step } else { line_center_y + step },
  )
  match
    rich_text_document_source_offset_at_point(
      document, target_point, content, base_font, text_system,
    ) {
    Some(offset) => offset
    None =>
      rich_text_vertical_block_boundary_offset(
        document,
        block_index,
        source_caret,
        base_font,
        text_system,
        up~,
      )
  }
}

///|
/// A blank line renders as an empty paragraph with no runs. Vertical caret
/// movement stops on it like on any other line: the caret walks into the
/// blank line and it expands to the writing line while the caret is there.
fn rich_text_block_is_blank_gap(block : RichTextBlock) -> Bool {
  block.runs.is_empty() &&
  block.prefix == "" &&
  block.table is None &&
  !block.rule
}

///|
fn rich_text_vertical_block_boundary_offset(
  document : RichTextDocument,
  block_index : Int,
  source_caret : Int,
  base_font : @core.FontSpec,
  text_system : @core.TextSystem,
  up~ : Bool,
) -> Int {
  // Blocks with no line box are pure spacing — the paragraph separator a
  // blank source line renders as — so the caret steps over them to the
  // neighbouring line, exactly like the pointer hit test does.
  if up {
    let mut index = block_index - 1
    while index >= 0 {
      let prev_block = document.blocks[index]
      if rich_text_block_line_height(prev_block, base_font, text_system) <= 0.0 {
        index = index - 1
        continue
      }
      return match rich_text_block_end_source_offset(prev_block) {
        Some(end) => end
        None => source_caret
      }
    }
    0
  } else {
    let mut index = block_index + 1
    while index < document.blocks.length() {
      let next_block = document.blocks[index]
      if rich_text_block_line_height(next_block, base_font, text_system) <= 0.0 {
        index = index + 1
        continue
      }
      return match next_block.content_range {
        Some(range) => range.start
        None =>
          match next_block.source_range {
            Some(range) => range.start
            None => source_caret
          }
      }
    }
    source_caret
  }
}

///|
fn rich_text_visual_offset_to_line(
  block : RichTextBlock,
  visual_offset : Int,
) -> Int {
  let prefix_len = text_length(block.prefix)
  let mut consumed = prefix_len
  let mut line = 0
  if visual_offset <= prefix_len {
    return 0
  }
  for run in block.runs {
    let segments = rich_text_split_lines(rich_text_run_visual_text(run))
    for index, segment in segments {
      let seg_len = text_length(segment)
      if consumed + seg_len >= visual_offset {
        return line
      }
      consumed = consumed + seg_len
      if index + 1 < segments.length() {
        if consumed == visual_offset {
          return line
        }
        consumed = consumed + 1
        line = line + 1
      }
    }
  }
  line
}

///|
fn[Msg] rich_text_vertical_caret_move_result(
  ctx : @core.ViewEventContext,
  text : String,
  document : RichTextDocument,
  content : @core.Rect,
  font : @core.FontSpec,
  up~ : Bool,
  extend_selection~ : Bool,
  on_selection_change : ((Int, @core.TextRange?) -> Msg)?,
) -> @core.ViewEventResult[Msg] {
  let display_text = rich_text_with_composition(
    text,
    ctx.text_control,
    focused=true,
  )
  let visual_caret = @core.clamp_int(
    rich_text_visual_caret(text, ctx.text_control, focused=true),
    0,
    text_length(display_text),
  )
  let caret_rect = rich_text_document_caret_rect_or_plain(
    document,
    visual_caret,
    content,
    font,
    ctx.text_system,
    display_text,
  )
  // M6② goal-column:垂直移动播种(首移)并保持目标 x;水平移动/点击/
  // 输入把 goal_column 清回 None(重置点在各自的 control 构造处)
  let goal_x = match ctx.text_control.goal_column {
    Some(x) => x
    None => caret_rect.origin.x
  }
  let target_offset = rich_text_vertical_target_offset(
    document,
    visual_caret,
    caret_rect,
    content,
    font,
    ctx.text_system,
    goal_x~,
    up~,
  )
  if target_offset == ctx.text_control.caret && !extend_selection {
    return @core.ViewEventResult::ignored()
  }
  let next_control = if extend_selection {
    let anchor = match ctx.text_control.text_selection_anchor {
      Some(a) => a
      None => ctx.text_control.caret
    }
    let start = @core.min_int(anchor, target_offset)
    let end = @core.max_int(anchor, target_offset)
    {
      ..ctx.text_control,
      caret: target_offset,
      selection: Some(@core.TextRange::new(start~, end~)),
      text_selection_anchor: Some(anchor),
      composition: None,
      composition_cursor: None,
      goal_column: Some(goal_x),
    }
  } else {
    {
      ..ctx.text_control,
      caret: target_offset,
      selection: None,
      text_selection_anchor: None,
      composition: None,
      composition_cursor: None,
      goal_column: Some(goal_x),
    }
  }
  let changed = next_control.caret != ctx.text_control.caret ||
    next_control.selection != ctx.text_control.selection ||
    ctx.text_control.composition is Some(_) ||
    ctx.text_control.composition_cursor is Some(_)
  view_event_result(
    changed~,
    captured=true,
    text_control=Some(next_control),
    dirty=ViewPaintDirty,
    messages=if changed {
      selection_messages(
        on_selection_change,
        next_control.caret,
        next_control.selection,
      )
    } else {
      []
    },
  )
}

///|
fn multiline_line_start(text : String, caret : Int) -> Int {
  let chars = text.to_array()
  let mut index = @core.clamp_int(caret, 0, chars.length())
  while index > 0 && chars[index - 1] != '\n' {
    index = index - 1
  }
  index
}

///|
fn multiline_line_end(text : String, caret : Int) -> Int {
  let chars = text.to_array()
  let mut index = @core.clamp_int(caret, 0, chars.length())
  while index < chars.length() && chars[index] != '\n' {
    index = index + 1
  }
  index
}

///|
fn rich_text_transform_insert(
  input_transform : ((String, Int, @core.TextRange?, String) -> RichTextInputTransform?)?,
  text : String,
  control : @core.TextControlStateContext,
  inserted : String,
) -> RichTextInputTransform? {
  match input_transform {
    Some(transform) =>
      transform(text, control.caret, control.selection, inserted)
    None => None
  }
}

///|
fn apply_rich_text_input_transform(
  control : @core.TextControlStateContext,
  previous : String,
  edit : RichTextInputTransform,
  record_history~ : Bool,
) -> TextInputApplyResult {
  let changed = edit.text != previous ||
    edit.caret != control.caret ||
    edit.selection != control.selection ||
    control.composition is Some(_) ||
    control.composition_cursor is Some(_)
  if changed {
    let undo_stack : Array[@core.TextHistoryEntryContext] = []
    undo_stack.append(control.undo_stack)
    // The transform API only exposes before/after documents, so the edit
    // record and history entry come from a one-shot prefix/suffix diff.
    let (start, before, after) = @core.text_history_delta(previous, edit.text)
    if record_history && previous != edit.text {
      rich_text_history_extend_stack(
        undo_stack,
        rich_text_history_record_edit(
          control,
          previous,
          edit.text,
          control.caret,
          shape=Some(
            rich_text_history_edit_shape_at(
              start,
              before,
              after,
              control.caret,
              selection_active=@core.active_text_selection(control.selection)
                is Some(_),
            ),
          ),
        ),
        {
          start,
          before,
          after,
          caret: control.caret,
          selection: control.selection,
        },
      )
    }
    {
      text: edit.text,
      control: {
        ..control,
        caret: edit.caret,
        selection: edit.selection,
        composition: None,
        composition_cursor: None,
        undo_stack,
        redo_stack: [],
      },
      changed: true,
      edit: Some({ start, deleted: before, replacement: after, }),
    }
  } else {
    { text: previous, control, changed: false, edit: None, }
  }
}

///|
fn append_rich_text_selection_commands(
  commands : Array[@core.DrawCommand],
  document : RichTextDocument,
  content : @core.Rect,
  base_font : @core.FontSpec,
  text_system : @core.TextSystem,
  selection : @core.TextRange?,
) -> Unit {
  match @core.active_text_selection(selection) {
    Some(selection) => {
      let mut y = content.origin.y
      for block in document.blocks {
        let line_height = rich_text_block_line_height(
          block,
          base_font,
          text_system,
          block_width=content.size.width,
        )
        let block_height = line_height + block.inset.top + block.inset.bottom
        let frame = @core.Rect::new(
          x=content.origin.x + block.inset.left,
          y=y + block.inset.top,
          width=max_double(
            0.0,
            content.size.width - block.inset.left - block.inset.right,
          ),
          height=line_height,
        )
        match block.table {
          // 表格按单元格几何绘制高亮;文本块仍走行盒路径。
          Some(table) =>
            append_rich_text_table_selection_rects(
              commands, block, table, frame, selection, base_font, text_system,
            )
          None =>
            match rich_text_block_selection_visual_range(block, selection) {
              Some(range) =>
                append_rich_text_block_selection_rect(
                  commands, block, range, frame, base_font, text_system,
                )
              None => ()
            }
        }
        y = y + block_height
      }
    }
    None => ()
  }
}

///|
fn rich_text_block_selection_visual_range(
  block : RichTextBlock,
  selection : @core.TextRange,
) -> @core.TextRange? {
  match (block.source_range, block.content_range) {
    (Some(source), Some(content)) =>
      if selection.end <= source.start || selection.start >= source.end {
        None
      } else {
        let start = rich_text_block_source_offset_to_visual(
          block,
          @core.clamp_int(selection.start, source.start, source.end),
          content,
        )
        let end = rich_text_block_source_offset_to_visual(
          block,
          @core.clamp_int(selection.end, source.start, source.end),
          content,
        )
        if end > start {
          Some(@core.TextRange::new(start~, end~))
        } else {
          None
        }
      }
    (_, Some(content)) =>
      if selection.end <= content.start || selection.start >= content.end {
        None
      } else {
        let start = rich_text_block_source_offset_to_visual(
          block,
          @core.clamp_int(selection.start, content.start, content.end),
          content,
        )
        let end = rich_text_block_source_offset_to_visual(
          block,
          @core.clamp_int(selection.end, content.start, content.end),
          content,
        )
        if end > start {
          Some(@core.TextRange::new(start~, end~))
        } else {
          None
        }
      }
    _ => None
  }
}

///|

///|
/// Table selection highlight: per cell, intersect the selection with the
/// cell's source range and draw one band per covered visual line, matching
/// the table painter's per-line geometry. Cells without source ranges (the
/// read-only viewer) contribute nothing.
fn append_rich_text_table_selection_rects(
  commands : Array[@core.DrawCommand],
  block : RichTextBlock,
  table : RichTextTable,
  frame : @core.Rect,
  selection : @core.TextRange,
  base_font : @core.FontSpec,
  text_system : @core.TextSystem,
) -> Unit {
  let layout = rich_text_table_layout(
    table,
    block,
    base_font,
    frame_width=frame.size.width,
    text_system~,
  )
  for row_index, row_layout in layout.rows {
    if row_index >= table.rows.length() {
      continue
    }
    let table_row = table.rows[row_index]
    let row_y = frame.origin.y + layout.row_top(row_index)
    for column_index, cell_layout in row_layout.cells {
      if column_index >= table_row.length() {
        continue
      }
      let cell = table_row[column_index]
      let range = match cell.source_range {
        Some(range) => range
        None => continue
      }
      if selection.end <= range.start || selection.start >= range.end {
        continue
      }
      let chars = cell.text.to_array()
      let cs = @core.clamp_int(selection.start - range.start, 0, chars.length())
      let ce = @core.clamp_int(selection.end - range.start, 0, chars.length())
      if ce <= cs {
        continue
      }
      let cell_x = frame.origin.x +
        layout.column_width * column_index.to_double()
      let inner_x = cell_x + table.cell_padding.left
      // Styled cell: highlight each segment's selected slice at its
      // laid-out x, skipping marker-only overlaps (no width to show).
      if cell_layout.segments.length() > 0 {
        for segment in cell_layout.segments {
          let s = @core.max_int(cs, segment.start)
          let e = @core.min_int(ce, segment.end)
          if e <= s {
            continue
          }
          let chars = segment.text.to_array()
          let visible_s = @core.clamp_int(s - segment.start, 0, chars.length())
          let visible_e = @core.clamp_int(e - segment.start, 0, chars.length())
          if visible_e <= visible_s {
            continue
          }
          let font = segment.run.font.unwrap_or(cell_layout.font)
          let before = String::from_array(chars[:visible_s])
          let covered = String::from_array(chars[:visible_e])
          let x0 = inner_x +
            segment.x +
            rich_text_width(before, font, text_system)
          let x1 = inner_x +
            segment.x +
            rich_text_width(covered, font, text_system)
          if x1 > x0 {
            commands.push(
              FillRect(
                @core.Rect::new(
                  x=x0,
                  y=row_y +
                    table.cell_padding.top +
                    layout.line_step * segment.line.to_double() +
                    3.0,
                  width=x1 - x0,
                  height=max_double(10.0, layout.line_step - 6.0),
                ),
                @core.Color::blue().with_alpha(0.18),
              ),
            )
          }
        }
        continue
      }
      for line_index, line in cell_layout.lines {
        let s = @core.max_int(cs, line.start)
        let e = @core.min_int(ce, line.end)
        if e <= s {
          continue
        }
        let line_chars = line.text.to_array()
        let before = String::from_array(line_chars[0:s - line.start])
        let covered = String::from_array(line_chars[0:e - line.start])
        let text_x = match cell.align.unwrap_or(TextStart) {
          TextStart => inner_x
          TextCenter =>
            inner_x +
            max_double(0.0, (layout.cell_inner_width - line.width) / 2.0)
          TextEnd =>
            inner_x + max_double(0.0, layout.cell_inner_width - line.width)
        }
        let x0 = text_x + rich_text_width(before, cell_layout.font, text_system)
        let x1 = text_x +
          rich_text_width(covered, cell_layout.font, text_system)
        if x1 > x0 {
          commands.push(
            FillRect(
              @core.Rect::new(
                x=x0,
                y=row_y +
                  table.cell_padding.top +
                  layout.line_step * line_index.to_double() +
                  3.0,
                width=x1 - x0,
                height=max_double(10.0, layout.line_step - 6.0),
              ),
              @core.Color::blue().with_alpha(0.18),
            ),
          )
        }
      }
    }
  }
}

///|
/// One highlight band per visual line the range touches. With soft-wrap
/// the selection usually spans several wrapped lines, and a single
/// first-line band would hide most of the selection (and disappear
/// entirely when the end x lands left of the start x on a later line).
fn append_rich_text_block_selection_rect(
  commands : Array[@core.DrawCommand],
  block : RichTextBlock,
  range : @core.TextRange,
  frame : @core.Rect,
  base_font : @core.FontSpec,
  text_system : @core.TextSystem,
) -> Unit {
  let line_heights = rich_text_block_line_heights(
    block,
    base_font,
    text_system,
    frame.size.width,
  )
  let line_widths = rich_text_block_line_widths(
    block,
    base_font,
    text_system,
    frame.size.width,
  )
  let start_line = rich_text_visual_offset_to_line(block, range.start)
  let end_line = rich_text_visual_offset_to_line(block, range.end)
  let start_rect = rich_text_block_caret_rect(
    block,
    range.start,
    frame,
    base_font,
    text_system,
  )
  let end_rect = rich_text_block_caret_rect(
    block,
    range.end,
    frame,
    base_font,
    text_system,
  )
  let last_line = @core.clamp_int(
    end_line,
    0,
    @core.max_int(line_heights.length() - 1, 0),
  )
  let line_step = rich_text_block_line_step(block, base_font, text_system)
  let mut y = frame.origin.y
  for line in 0..<=last_line {
    let height = line_heights.get(line).unwrap_or(line_step)
    if line >= start_line && line <= last_line {
      let left = if line == start_line {
        start_rect.origin.x
      } else {
        frame.origin.x
      }
      let right = if line == end_line {
        end_rect.origin.x
      } else {
        frame.origin.x + line_widths.get(line).unwrap_or(frame.size.width)
      }
      if right > left {
        commands.push(
          FillRect(
            @core.Rect::new(
              x=left,
              y=y + 4.0,
              width=right - left,
              height=max_double(10.0, height - 8.0),
            ),
            @core.Color::blue().with_alpha(0.18),
          ),
        )
      }
    }
    y = y + height
  }
}

///|
pub fn rich_text_document_caret_rect_at_source(
  document : RichTextDocument,
  source_caret : Int,
  content : @core.Rect,
  base_font : @core.FontSpec,
  text_system? : @core.TextSystem = @core.TextSystem::fallback(),
) -> @core.Rect? {
  rich_text_document_caret_rect(
    document,
    source_caret,
    content,
    base_font,
    text_system,
    block_width=content.size.width,
  )
}

///|
fn rich_text_document_caret_rect(
  document : RichTextDocument,
  source_caret : Int,
  content : @core.Rect,
  base_font : @core.FontSpec,
  text_system : @core.TextSystem,
  block_width~ : Double,
) -> @core.Rect? {
  if !rich_text_document_has_source_ranges(document) {
    return None
  }
  let mut y = content.origin.y
  let mut last_rect = @core.Rect::new(
    x=content.origin.x,
    y=content.origin.y + 4.0,
    width=1.5,
    height=max_double(10.0, base_font.size * 1.55 - 8.0),
  )
  for block in document.blocks {
    let line_height = rich_text_block_line_height(
      block,
      base_font,
      text_system,
      block_width~,
    )
    let block_height = line_height + block.inset.top + block.inset.bottom
    let frame = @core.Rect::new(
      x=content.origin.x + block.inset.left,
      y=y + block.inset.top,
      width=max_double(
        0.0,
        content.size.width - block.inset.left - block.inset.right,
      ),
      height=line_height,
    )
    if rich_text_caret_before_block(source_caret, block) {
      match block.table {
        Some(table) =>
          return Some(
            rich_text_block_table_caret_rect(
              block, table, source_caret, frame, base_font, text_system,
            ),
          )
        None => ()
      }
      return Some(
        rich_text_block_caret_rect(block, 0, frame, base_font, text_system),
      )
    }
    match block.table {
      Some(table) => {
        // Only short-circuit while the caret maps into this table; a caret
        // parked in a block AFTER the table must keep walking, otherwise the
        // caret is drawn at the table's last cell and never shows up where
        // the user clicked.
        let caret_in_table = match block.source_range {
          Some(range) => source_caret <= range.end
          None => false
        }
        if caret_in_table {
          return Some(
            rich_text_block_table_caret_rect(
              block, table, source_caret, frame, base_font, text_system,
            ),
          )
        }
        last_rect = rich_text_block_table_caret_rect(
          block,
          table,
          match block.source_range {
            Some(range) => range.end
            None => source_caret
          },
          frame,
          base_font,
          text_system,
        )
        y = y + block_height
        continue
      }
      None => ()
    }
    match rich_text_block_visual_offset(block, source_caret) {
      Some(visual_offset) =>
        return Some(
          rich_text_block_caret_rect(
            block, visual_offset, frame, base_font, text_system,
          ),
        )
      None => ()
    }
    last_rect = rich_text_block_caret_rect(
      block,
      rich_text_block_visual_length(block),
      frame,
      base_font,
      text_system,
    )
    y = y + block_height
  }
  Some(last_rect)
}

///|
fn rich_text_document_source_offset_at_point(
  document : RichTextDocument,
  point : @core.Point,
  content : @core.Rect,
  base_font : @core.FontSpec,
  text_system : @core.TextSystem,
) -> Int? {
  if !rich_text_document_has_source_ranges(document) {
    return None
  }
  let mut y = content.origin.y
  let mut last_offset : Int? = None
  let mut last_block : RichTextBlock? = None
  let mut last_frame = @core.Rect::new(x=0.0, y=0.0, width=0.0, height=0.0)
  for block in document.blocks {
    // Block heights must match the painted layout: percentage-width image
    // runs only reach their drawn height once the content column width is
    // known. A width-blind walk drifts out of sync with the screen and
    // resolves clicks below an image gallery into the wrong block.
    let line_height = rich_text_block_line_height(
      block,
      base_font,
      text_system,
      block_width=content.size.width,
    )
    let block_height = line_height + block.inset.top + block.inset.bottom
    let frame = @core.Rect::new(
      x=content.origin.x + block.inset.left,
      y=y + block.inset.top,
      width=max_double(
        0.0,
        content.size.width - block.inset.left - block.inset.right,
      ),
      height=line_height,
    )
    // A block with no line box is pure spacing: the paragraph separator a
    // blank source line renders as (see
    // `markdown_editor_collapse_inactive_blank_gaps`). Nothing can park a
    // caret on it, so the pointer resolves to the content on the far side —
    // the paragraph above for the upper half of the band, the next line for
    // the lower half.
    let spacing = line_height <= 0.0
    if point.y < y + block_height {
      if spacing {
        if point.y < y + block_height * 0.5 {
          match last_offset {
            Some(offset) => return Some(offset)
            None => ()
          }
        }
      } else {
        match
          rich_text_block_table_source_offset_at_point(
            block, point, frame, base_font, text_system,
          ) {
          Some(offset) => return Some(offset)
          None => ()
        }
        let offset = rich_text_block_source_offset_at_point(
          block, point, frame, base_font, text_system,
        )
        return Some(offset)
      }
    }
    if !spacing {
      last_block = Some(block)
      last_frame = frame
      last_offset = rich_text_block_end_source_offset(block)
    }
    y = y + block_height
  }
  match last_block {
    Some(block) =>
      Some(
        rich_text_block_source_offset_at_point(
          block, point, last_frame, base_font, text_system,
        ),
      )
    None => last_offset
  }
}

///|
fn rich_text_block_end_source_offset(block : RichTextBlock) -> Int? {
  match block.source_range {
    Some(range) => Some(range.end)
    None =>
      Some(
        rich_text_block_source_offset_from_visual(
          block,
          rich_text_block_visual_length(block),
        ),
      )
  }
}

///|
fn rich_text_caret_before_block(
  source_caret : Int,
  block : RichTextBlock,
) -> Bool {
  match block.source_range {
    Some(range) => source_caret < range.start
    None => false
  }
}

///|
fn rich_text_block_visual_offset(
  block : RichTextBlock,
  source_caret : Int,
) -> Int? {
  match (block.source_range, block.content_range) {
    (Some(source), Some(content)) =>
      if source_caret < source.start || source_caret > source.end {
        None
      } else if source_caret < content.start {
        Some(0)
      } else if source_caret > content.end {
        Some(rich_text_block_visual_length(block))
      } else {
        Some(
          rich_text_block_source_offset_to_visual(block, source_caret, content),
        )
      }
    (_, Some(content)) =>
      if source_caret < content.start || source_caret > content.end {
        None
      } else {
        Some(
          rich_text_block_source_offset_to_visual(block, source_caret, content),
        )
      }
    _ => None
  }
}

///|
fn rich_text_document_prefix_source_offset_at_point(
  document : RichTextDocument,
  point : @core.Point,
  content : @core.Rect,
  base_font : @core.FontSpec,
  text_system : @core.TextSystem,
) -> Int? {
  if !rich_text_document_has_source_ranges(document) {
    return None
  }
  let mut y = content.origin.y
  let mut fallback : Int? = None
  for block in document.blocks {
    let line_height = rich_text_block_line_height(
      block,
      base_font,
      text_system,
      block_width=content.size.width,
    )
    let block_height = line_height + block.inset.top + block.inset.bottom
    let frame = @core.Rect::new(
      x=content.origin.x + block.inset.left,
      y=y + block.inset.top,
      width=max_double(
        0.0,
        content.size.width - block.inset.left - block.inset.right,
      ),
      height=line_height,
    )
    if fallback is None &&
      rich_text_point_in_prefix_lane(
        block, point, frame, base_font, text_system,
      ) {
      fallback = match block.content_range {
        Some(range) => Some(range.start)
        None => None
      }
    }
    if point.y < y + block_height {
      return rich_text_block_prefix_source_offset_at_point(
        block, point, frame, base_font, text_system,
      )
    }
    y = y + block_height
  }
  fallback
}

///|
fn rich_text_block_prefix_source_offset_at_point(
  block : RichTextBlock,
  point : @core.Point,
  frame : @core.Rect,
  base_font : @core.FontSpec,
  text_system : @core.TextSystem,
) -> Int? {
  if block.prefix == "" {
    return None
  }
  let font = rich_text_block_font(block, base_font)
  let width = rich_text_block_prefix_hit_width(
    block, font, base_font, text_system,
  )
  if point.x >= frame.origin.x && point.x <= frame.origin.x + width {
    match block.content_range {
      Some(content) => Some(content.start)
      None => None
    }
  } else {
    None
  }
}

///|
/// Hit width of a block's prefix lane. Text prefixes keep the historical
/// `1.8em` floor so a long indented lane stays clickable; a marker lane is
/// already wider than the drawn box and must not swallow the label's caret
/// positions.
fn rich_text_block_prefix_hit_width(
  block : RichTextBlock,
  font : @core.FontSpec,
  base_font : @core.FontSpec,
  text_system : @core.TextSystem,
) -> Double {
  match block.marker {
    Some(_) => rich_text_block_marker_lane(block, font, text_system)
    None =>
      max_double(
        rich_text_width(block.prefix, font, text_system),
        base_font.size * 1.8,
      )
  }
}

///|
fn rich_text_point_in_prefix_lane(
  block : RichTextBlock,
  point : @core.Point,
  frame : @core.Rect,
  base_font : @core.FontSpec,
  text_system : @core.TextSystem,
) -> Bool {
  if block.prefix == "" {
    return false
  }
  let font = rich_text_block_font(block, base_font)
  let width = rich_text_block_prefix_hit_width(
    block, font, base_font, text_system,
  )
  point.x >= frame.origin.x && point.x <= frame.origin.x + width
}

///|
fn rich_text_block_source_offset_at_point(
  block : RichTextBlock,
  point : @core.Point,
  frame : @core.Rect,
  base_font : @core.FontSpec,
  text_system : @core.TextSystem,
) -> Int {
  let visual = rich_text_block_visual_offset_at_point(
    block, point, frame, base_font, text_system,
  )
  rich_text_block_source_offset_from_visual(block, visual)
}

///|
/// Index of the visual line whose band contains `offset_y` within a block.
/// Lines are not uniform when image runs scale with the content column, so
/// this walks the same per-line heights painting uses instead of assuming a
/// fixed `line_step`.
fn rich_text_block_line_index_at_y(
  block : RichTextBlock,
  base_font : @core.FontSpec,
  text_system : @core.TextSystem,
  available_width~ : Double,
  offset_y~ : Double,
) -> Int {
  let mut cursor = 0.0
  for
    index, height in rich_text_block_line_heights(
      block, base_font, text_system, available_width,
    ) {
    if offset_y < cursor + height {
      return index
    }
    cursor = cursor + height
  }
  @core.max_int(rich_text_block_visual_line_count(block) - 1, 0)
}

///|
fn rich_text_block_visual_offset_at_point(
  block : RichTextBlock,
  point : @core.Point,
  frame : @core.Rect,
  base_font : @core.FontSpec,
  text_system : @core.TextSystem,
) -> Int {
  let font = rich_text_block_font(block, base_font)
  let target_line = @core.clamp_int(
    rich_text_block_line_index_at_y(
      block,
      base_font,
      text_system,
      available_width=frame.size.width,
      offset_y=point.y - frame.origin.y,
    ),
    0,
    rich_text_block_visual_line_count(block) - 1,
  )
  let mut origin = frame.origin.x
  let mut visual_offset = 0
  let mut line = 0
  let mut line_end = 0
  // Whether the target line's own content has been reached. A newline run
  // (a run whose text is just "\n") splits into two empty segments, and
  // the second one sits at the START of the next line before any content;
  // returning there pinned every click on the line to its first offset,
  // ignoring x.
  let mut target_line_has_content = false
  if block.prefix != "" {
    let prefix_length = text_length(block.prefix)
    let width = rich_text_block_marker_lane(block, font, text_system)
    // A marker lane is one atomic slot: it has no interior caret position, so
    // the whole lane maps to the offset in front of the label.
    if block.marker is Some(_) {
      if target_line == 0 && point.x <= origin + width {
        return 0
      }
      visual_offset = visual_offset + prefix_length
      line_end = visual_offset
      origin = origin + width
    } else {
      let hit = rich_text_run_visual_offset_at_x(
        block.prefix,
        font,
        point.x,
        origin,
        text_system,
      )
      if target_line == 0 && (hit < prefix_length || point.x <= origin + width) {
        return hit
      }
      visual_offset = visual_offset + prefix_length
      line_end = visual_offset
      origin = origin + width
    }
  }
  for run in block.runs {
    let run_font = run.font.unwrap_or(font)
    let segments = rich_text_split_lines(rich_text_run_visual_text(run))
    for index, segment in segments {
      let segment_length = text_length(segment)
      let segment_width = rich_text_run_segment_width(
        run,
        segment,
        run_font,
        text_system,
        available_width=frame.size.width,
      )
      if line == target_line {
        if segment_length == 0 {
          // An empty segment at the target line: the split artifact around
          // a newline run (its second half is the new line's start, before
          // any content) or a genuinely empty visual line. Remember the
          // line start as the fallback and keep walking — the line's
          // content is the next non-empty segment at the same line.
          if !target_line_has_content {
            line_end = visual_offset
          }
        } else {
          target_line_has_content = true
          line_end = visual_offset + segment_length
          if point.x <= origin + segment_width {
            return visual_offset +
              rich_text_run_visual_offset_at_x(
                segment,
                run_font,
                point.x,
                origin,
                text_system,
                width=rich_text_run_hit_width(run, segment_width),
              )
          }
        }
      }
      visual_offset = visual_offset + segment_length
      origin = origin + segment_width
      if index + 1 < segments.length() {
        if line == target_line && target_line_has_content {
          return line_end
        }
        visual_offset = visual_offset + 1
        line = line + 1
        origin = frame.origin.x
      }
    }
  }
  line_end
}

///|
fn rich_text_run_visual_offset_at_x(
  text : String,
  font : @core.FontSpec,
  x : Double,
  origin_x : Double,
  text_system : @core.TextSystem,
  width? : Double? = None,
) -> Int {
  let chars = text.to_array()
  let boundaries = @core.TextGraphemeBoundaries::new(text~)
  if x <= origin_x {
    return 0
  }
  match width {
    Some(width) =>
      if chars.length() <= 1 {
        if x < origin_x + width / 2.0 {
          return 0
        } else {
          return chars.length()
        }
      } else {
        let relative = clamp_double((x - origin_x) / width, 0.0, 1.0)
        return boundaries.nearest_boundary(
          (relative * chars.length().to_double()).round().to_int(),
        )
      }
    None => ()
  }
  for index in 0.. Int {
  match block.content_range {
    Some(content) => {
      let prefix_length = text_length(block.prefix)
      if visual_offset <= prefix_length {
        content.start
      } else {
        match
          rich_text_block_run_source_offset_from_visual(
            block,
            visual_offset - prefix_length,
          ) {
          Some(offset) => offset
          None =>
            @core.clamp_int(
              content.start + visual_offset - prefix_length,
              content.start,
              content.end,
            )
        }
      }
    }
    None => visual_offset
  }
}

///|
fn rich_text_block_run_source_offset_from_visual(
  block : RichTextBlock,
  visual_offset : Int,
) -> Int? {
  let mut remaining = visual_offset
  let mut fallback = match block.content_range {
    Some(content) => Some(content.start)
    None => None
  }
  for run in block.runs {
    let visual_text = rich_text_run_visual_text(run)
    let visual_length = text_length(visual_text)
    if remaining <= visual_length {
      match run.source_range {
        Some(range) =>
          return Some(
            @core.clamp_int(
              range.start +
              rich_text_run_source_offset_from_visual_offset(
                remaining,
                visual_text,
                run.text,
              ),
              range.start,
              range.end,
            ),
          )
        None => return fallback
      }
    }
    remaining = remaining - visual_length
    match run.source_range {
      Some(range) => fallback = Some(range.end)
      None => ()
    }
  }
  match block.runs.last() {
    Some(run) =>
      match run.source_range {
        Some(range) => Some(range.end)
        None => fallback
      }
    None => None
  }
}

///|
fn rich_text_block_source_offset_to_visual(
  block : RichTextBlock,
  source_offset : Int,
  content : RichTextSourceRange,
) -> Int {
  if source_offset < content.start {
    0
  } else if source_offset > content.end {
    rich_text_block_visual_length(block)
  } else {
    let prefix_length = text_length(block.prefix)
    match rich_text_block_run_visual_offset_from_source(block, source_offset) {
      Some(offset) => prefix_length + offset
      None =>
        @core.clamp_int(
          prefix_length + source_offset - content.start,
          0,
          rich_text_block_visual_length(block),
        )
    }
  }
}

///|
fn rich_text_block_run_visual_offset_from_source(
  block : RichTextBlock,
  source_offset : Int,
) -> Int? {
  let mut visual = 0
  for run in block.runs {
    let visual_text = rich_text_run_visual_text(run)
    let visual_length = text_length(visual_text)
    match run.source_range {
      Some(range) =>
        if source_offset <= range.end {
          return Some(
            visual +
            rich_text_run_visual_offset_from_source_offset(
              source_offset - range.start,
              run.text,
              visual_text,
            ),
          )
        }
      None => ()
    }
    visual = visual + visual_length
  }
  Some(visual)
}

///|
fn rich_text_block_caret_rect(
  block : RichTextBlock,
  visual_offset : Int,
  frame : @core.Rect,
  base_font : @core.FontSpec,
  text_system : @core.TextSystem,
) -> @core.Rect {
  let font = rich_text_block_font(block, base_font)
  let line_step = rich_text_block_line_step(block, base_font, text_system)
  // Wrapped-line y positions follow the per-line heights painting uses, so a
  // caret below a column-scaled image line lands on the image line's band.
  let line_heights = rich_text_block_line_heights(
    block,
    base_font,
    text_system,
    frame.size.width,
  )
  let mut line_slot = rich_text_text_newline_count(block.prefix)
  let mut remaining = @core.clamp_int(
    visual_offset,
    0,
    rich_text_block_visual_length(block),
  )
  let mut x = frame.origin.x
  let mut y = frame.origin.y
  if block.prefix != "" {
    let prefix_length = text_length(block.prefix)
    let lane = rich_text_block_marker_lane(block, font, text_system)
    if remaining <= prefix_length {
      // The marker lane is atomic, so any offset inside it sits at the lane
      // start; only a text prefix has interior caret stops.
      let advance = match block.marker {
        Some(_) => 0.0
        None =>
          rich_text_width(
            rich_text_prefix(block.prefix, remaining),
            font,
            text_system,
          )
      }
      x = x + advance
      return rich_text_cursor_rect_at(
        x,
        @core.Rect::new(
          x=frame.origin.x,
          y~,
          width=frame.size.width,
          height=frame.size.height,
        ),
        font,
      )
    }
    remaining = remaining - prefix_length
    x = x + lane
  }
  for run in block.runs {
    let run_font = run.font.unwrap_or(font)
    let segments = rich_text_split_lines(rich_text_run_visual_text(run))
    for index, segment in segments {
      let segment_length = text_length(segment)
      if remaining <= segment_length {
        x = x +
          rich_text_run_segment_prefix_width(
            run, segment, remaining, run_font, text_system,
          )
        return rich_text_cursor_rect_at(
          x,
          @core.Rect::new(
            x=frame.origin.x,
            y~,
            width=frame.size.width,
            height=line_step,
          ),
          run_font,
        )
      }
      remaining = remaining - segment_length
      x = x + rich_text_run_segment_width(run, segment, run_font, text_system)
      if index + 1 < segments.length() {
        if remaining == 0 {
          return rich_text_cursor_rect_at(
            x,
            @core.Rect::new(
              x=frame.origin.x,
              y~,
              width=frame.size.width,
              height=line_step,
            ),
            run_font,
          )
        }
        remaining = remaining - 1
        y = y + line_heights.get(line_slot).unwrap_or(line_step)
        line_slot = line_slot + 1
        x = frame.origin.x
      }
    }
  }
  rich_text_cursor_rect_at(
    x,
    @core.Rect::new(
      x=frame.origin.x,
      y~,
      width=frame.size.width,
      height=line_step,
    ),
    font,
  )
}

///|
fn rich_text_run_segment_prefix_width(
  run : RichTextRun,
  text : String,
  visual_offset : Int,
  font : @core.FontSpec,
  text_system : @core.TextSystem,
) -> Double {
  let length = text_length(text)
  if length <= 0 || visual_offset <= 0 {
    0.0
  } else if visual_offset >= length {
    rich_text_run_segment_width(run, text, font, text_system)
  } else {
    match run.image_source {
      Some(_) =>
        rich_text_run_segment_width(run, text, font, text_system) *
        visual_offset.to_double() /
        length.to_double()
      None =>
        rich_text_width(
          rich_text_prefix(text, visual_offset),
          font,
          text_system,
        )
    }
  }
}

///|
fn rich_text_prefix(text : String, length : Int) -> String {
  let chars = text.to_array()
  let end = @core.TextGraphemeBoundaries::new(text~).nearest_boundary(length)
  String::from_array(chars[0:end])
}

///|
fn rich_text_cursor_rect_at(
  x : Double,
  frame : @core.Rect,
  font : @core.FontSpec,
) -> @core.Rect {
  @core.Rect::new(
    x~,
    y=frame.origin.y + 4.0,
    width=1.5,
    height=max_double(10.0, font.size * 1.55 - 8.0),
  )
}

///|
fn rich_text_block_visual_length(block : RichTextBlock) -> Int {
  let mut length = text_length(block.prefix)
  for run in block.runs {
    length = length + text_length(rich_text_run_visual_text(run))
  }
  length
}

///|
fn rich_text_block_table_source_offset_at_point(
  block : RichTextBlock,
  point : @core.Point,
  frame : @core.Rect,
  base_font : @core.FontSpec,
  text_system : @core.TextSystem,
) -> Int? {
  match block.table {
    Some(table) => {
      let row_count = table.rows.length()
      let column_count = rich_text_table_column_count(table)
      if row_count == 0 || column_count == 0 || table.row_height <= 0.0 {
        return None
      }
      let layout = rich_text_table_layout(
        table,
        block,
        base_font,
        frame_width=frame.size.width,
        text_system~,
      )
      // Walk the dynamic row heights; a click past the last row clamps into
      // it so dragging below the table keeps editing its final row.
      let mut row_index = row_count - 1
      for index in 0..= row.length() {
        return None
      }
      let cell = row[column_index]
      match cell.source_range {
        Some(range) => {
          // 与渲染端 (append_rich_text_block_table) 保持一致的几何:
          // cell 文本按行绘制,行 frame = cell_frame.inset(cell_padding),
          // 按 cell.align 决定每行 x 起点。
          let cell_x = frame.origin.x +
            layout.column_width * column_index.to_double()
          let inner_x = cell_x + table.cell_padding.left
          let inner_width = layout.cell_inner_width
          // 空 cell 直接钳到 range.start,避免后续除零/无效测量。
          if range.end <= range.start || cell.text == "" {
            return Some(range.start)
          }
          let row_layout = layout.rows[row_index]
          let cell_layout = row_layout.cells[column_index]
          let cell_font = cell_layout.font
          // Styled cell: map x through the run segments (markers occupy no
          // width; a click over a marker snaps to the adjacent visible
          // character), falling back to the plain path when segments are
          // empty.
          if cell_layout.segments.length() > 0 {
            return rich_text_table_segment_offset_at_point(
              cell,
              cell_layout,
              point,
              range~,
              row_top=frame.origin.y + layout.row_top(row_index),
              inner_x~,
              table,
              layout,
              text_system~,
            )
          }
          // 行按 y 选择;拖到单元格底部时钳在最后一行。
          let cell_top = frame.origin.y + layout.row_top(row_index)
          let line_index = @core.clamp_int(
            ((point.y - cell_top - table.cell_padding.top) / layout.line_step)
            .floor()
            .to_int(),
            0,
            cell_layout.lines.length() - 1,
          )
          let line = cell_layout.lines[line_index]
          if line.end <= line.start {
            return Some(
              @core.clamp_int(range.start + line.start, range.start, range.end),
            )
          }
          let text_x = match cell.align.unwrap_or(TextStart) {
            TextStart => inner_x
            TextCenter =>
              inner_x + max_double(0.0, (inner_width - line.width) / 2.0)
            TextEnd => inner_x + max_double(0.0, inner_width - line.width)
          }
          let text_x_end = text_x + line.width
          // 点击落在文本左右两侧空白时,clamp 到该行文本边界。
          if point.x < text_x {
            return Some(
              @core.clamp_int(range.start + line.start, range.start, range.end),
            )
          }
          if point.x > text_x_end {
            return Some(
              @core.clamp_int(range.start + line.end, range.start, range.end),
            )
          }
          // 点击落在文本渲染范围内:按逐字符中点命中,再钳到图形素边界。
          // 参考 rich_text_run_visual_offset_at_x 的 for index in 0.. None
      }
    }
    None => None
  }
}

///|
/// Hit-test a styled cell through its run segments. Each segment covers its
/// visible characters (markers excluded from width); the char midpoints
/// inside a segment decide the offset, and clicks left/right of the whole
/// styled content clamp to its first/last visible char. Marker source chars
/// are never returned directly: they collapse onto the nearest visible
/// boundary so the caret always lands where text was drawn.
fn rich_text_table_segment_offset_at_point(
  _cell : RichTextTableCell,
  cell_layout : RichTextTableCellLayout,
  point : @core.Point,
  range~ : RichTextSourceRange,
  row_top~ : Double,
  inner_x~ : Double,
  table : RichTextTable,
  layout : RichTextTableLayout,
  text_system~ : @core.TextSystem,
) -> Int? {
  ignore(table)
  let first = cell_layout.segments[0]
  let last = cell_layout.segments[cell_layout.segments.length() - 1]
  // y band selects the line; below the last line clamps into it.
  let line_index = @core.clamp_int(
    ((point.y - row_top - table.cell_padding.top) / layout.line_step)
    .floor()
    .to_int(),
    0,
    cell_layout.lines.length() - 1,
  )
  // Segments on the picked line, in x order (the layout walks runs in
  // order and segments carry their x offsets).
  let line_segments : Array[RichTextTableCellSegment] = []
  for segment in cell_layout.segments {
    if segment.line == line_index {
      line_segments.push(segment)
    }
  }
  if line_segments.is_empty() {
    return Some(
      @core.clamp_int(range.start + first.start, range.start, range.end),
    )
  }
  let head = line_segments[0]
  let tail = line_segments[line_segments.length() - 1]
  let text_x = inner_x + head.x
  let text_end = inner_x + tail.x + tail.width
  if point.x <= text_x {
    return Some(
      @core.clamp_int(range.start + head.start, range.start, range.end),
    )
  }
  if point.x >= text_end {
    return Some(@core.clamp_int(range.start + tail.end, range.start, range.end))
  }
  // Inside the styled content: find the segment under the point (a click
  // over a marker gap between segments snaps to the nearer side).
  for index, segment in line_segments {
    let segment_x = inner_x + segment.x
    let segment_end = segment_x + segment.width
    if point.x < segment_end || index == line_segments.length() - 1 {
      let font = segment.run.font.unwrap_or(cell_layout.font)
      let chars = segment.text.to_array()
      let mut chars_local = chars.length()
      for char_index in 0..