///|
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=@core.ViewDirtyHint::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,
  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=caret_rect.origin.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)
    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(_) => {
        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,
    )
    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 line_height = rich_text_block_line_height(block, base_font, text_system)
  let block_top = y
  let frame_top = y + block.inset.top
  let line_center_y = if visual_line >= 0 &&
    visual_line < rich_text_block_visual_line_count(block) {
    frame_top + (visual_line.to_double() + 0.5) * step
  } else {
    caret_rect.origin.y + caret_rect.size.height * 0.5
  }
  let target_y = if up {
    if visual_line == 0 {
      block_top - 1.0
    } else {
      line_center_y - step
    }
  } else {
    let line_count = rich_text_block_visual_line_count(block)
    if visual_line >= line_count - 1 {
      block_top + line_height + block.inset.top + block.inset.bottom + 1.0
    } else {
      line_center_y + step
    }
  }
  let target_point = @core.Point::new(
    x=caret_rect.origin.x + caret_rect.size.width * 0.5,
    y=target_y,
  )
  match
    rich_text_document_source_offset_at_point(
      document, target_point, content, base_font, text_system,
    ) {
    Some(offset) =>
      if offset != source_caret {
        offset
      } else {
        rich_text_vertical_block_boundary_offset(
          document,
          block_index,
          source_caret,
          up~,
        )
      }
    None =>
      rich_text_vertical_block_boundary_offset(
        document,
        block_index,
        source_caret,
        up~,
      )
  }
}

///|
fn rich_text_vertical_block_boundary_offset(
  document : RichTextDocument,
  block_index : Int,
  source_caret : Int,
  up~ : Bool,
) -> Int {
  if up {
    if block_index <= 0 {
      return 0
    }
    let prev_block = document.blocks[block_index - 1]
    match rich_text_block_end_source_offset(prev_block) {
      Some(end) => end
      None => source_caret
    }
  } else {
    if block_index >= document.blocks.length() - 1 {
      return source_caret
    }
    let next_block = document.blocks[block_index + 1]
    match next_block.content_range {
      Some(range) => range.start
      None =>
        match next_block.source_range {
          Some(range) => range.start
          None => 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,
  )
  let target_offset = rich_text_vertical_target_offset(
    document,
    visual_caret,
    caret_rect,
    content,
    font,
    ctx.text_system,
    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,
    }
  } else {
    {
      ..ctx.text_control,
      caret: target_offset,
      selection: None,
      text_selection_anchor: None,
      composition: None,
      composition_cursor: None,
    }
  }
  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=@core.ViewDirtyHint::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)
    if record_history && previous != edit.text {
      undo_stack.push({
        text: previous,
        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,
    }
  } else {
    { text: previous, control, changed: false }
  }
}

///|
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,
        )
        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 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
  }
}

///|
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 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,
  )
  if end_rect.origin.x > start_rect.origin.x {
    commands.push(
      @core.DrawCommand::FillRect(
        @core.Rect::new(
          x=start_rect.origin.x,
          y=frame.origin.y + 4.0,
          width=end_rect.origin.x - start_rect.origin.x,
          height=max_double(10.0, frame.size.height - 8.0),
        ),
        @core.Color::blue().with_alpha(0.18),
      ),
    )
  }
}

///|
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,
  )
}

///|
fn rich_text_document_caret_rect(
  document : RichTextDocument,
  source_caret : Int,
  content : @core.Rect,
  base_font : @core.FontSpec,
  text_system : @core.TextSystem,
) -> @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)
    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) =>
        return Some(
          rich_text_block_table_caret_rect(
            block, table, source_caret, frame, base_font, text_system,
          ),
        )
      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 {
    let line_height = rich_text_block_line_height(block, base_font, text_system)
    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 point.y < y + block_height {
      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)
    }
    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)
    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 = max_double(
    rich_text_width(block.prefix, font, text_system),
    font.size * 1.8,
  )
  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
  }
}

///|
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 = max_double(
    rich_text_width(block.prefix, font, text_system),
    font.size * 1.8,
  )
  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)
}

///|
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 line_step = rich_text_block_line_step(block, base_font, text_system)
  let target_line = @core.clamp_int(
    ((point.y - frame.origin.y) / line_step).floor().to_int(),
    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
  if block.prefix != "" {
    let prefix_length = text_length(block.prefix)
    let hit = rich_text_run_visual_offset_at_x(
      block.prefix,
      font,
      point.x,
      origin,
      text_system,
    )
    let width = rich_text_width(block.prefix, font, 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,
      )
      if line == target_line {
        line_end = visual_offset + segment_length
        if point.x <= origin + segment_width || segment_length == 0 {
          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 {
          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)
  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)
    if remaining <= prefix_length {
      let prefix = rich_text_prefix(block.prefix, remaining)
      x = x + rich_text_width(prefix, font, text_system)
      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 + rich_text_width(block.prefix, font, text_system)
  }
  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_step
        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 row_index = @core.clamp_int(
        ((point.y - frame.origin.y) / table.row_height).floor().to_int(),
        0,
        row_count - 1,
      )
      let column_width = frame.size.width / column_count.to_double()
      let column_index = @core.clamp_int(
        ((point.x - frame.origin.x) / column_width).floor().to_int(),
        0,
        column_count - 1,
      )
      let row = table.rows[row_index]
      if column_index >= 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 + column_width * column_index.to_double()
          let inner_x = cell_x + table.cell_padding.left
          let inner_width = max_double(
            0.0,
            column_width - table.cell_padding.left - table.cell_padding.right,
          )
          // 空 cell 直接钳到 range.start,避免后续除零/无效测量。
          if range.end <= range.start || cell.text == "" {
            return Some(range.start)
          }
          let base_block_font = rich_text_block_font(block, base_font)
          // 渲染端对 header cell 加重 weight=700,body 用 400。
          let cell_font = if cell.header {
            { ..base_block_font, weight: 700 }
          } else {
            { ..base_block_font, weight: 400 }
          }
          let text_width = rich_text_width(cell.text, cell_font, text_system)
          if text_width <= 0.0 {
            return Some(range.start)
          }
          let text_x = match cell.align.unwrap_or(@core.TextAlign::TextStart) {
            TextStart => inner_x
            TextCenter => inner_x + (inner_width - text_width) / 2.0
            TextEnd => inner_x + inner_width - text_width
          }
          let text_x_end = text_x + text_width
          // 点击落在文本左右两侧空白时,clamp 到 range 边界。
          if point.x < text_x {
            return Some(range.start)
          }
          if point.x > text_x_end {
            return Some(range.end)
          }
          // 点击落在文本渲染范围内:按逐字符中点命中,再钳到图形素边界。
          // 参考 rich_text_run_visual_offset_at_x 的 for index in 0.. None
      }
    }
    None => None
  }
}

///|
fn rich_text_block_table_caret_rect(
  block : RichTextBlock,
  table : RichTextTable,
  source_caret : Int,
  frame : @core.Rect,
  base_font : @core.FontSpec,
  text_system : @core.TextSystem,
) -> @core.Rect {
  let column_count = rich_text_table_column_count(table)
  let row_count = table.rows.length()
  let column_width = if column_count > 0 {
    frame.size.width / column_count.to_double()
  } else {
    0.0
  }
  // 默认 caret 矩形:表格起点。
  let base_font_resolved = rich_text_block_font(block, base_font)
  let line_step = rich_text_block_line_step(block, base_font, text_system)
  let caret_rect = rich_text_cursor_rect_at(
    frame.origin.x,
    @core.Rect::new(
      x=frame.origin.x,
      y=frame.origin.y,
      width=frame.size.width,
      height=line_step,
    ),
    base_font_resolved,
  )
  if column_count == 0 || row_count == 0 {
    return caret_rect
  }
  // 查找 source_caret 所在的 cell。
  let mut found_row = -1
  let mut found_col = -1
  let mut found_cell : RichTextTableCell? = None
  for row_index in 0..= row.length() {
        continue
      }
      let cell = row[col_index]
      match cell.source_range {
        Some(range) =>
          if source_caret >= range.start && source_caret <= range.end {
            found_row = row_index
            found_col = col_index
            found_cell = Some(cell)
            break
          }
        None => ()
      }
    }
    if found_row >= 0 {
      break
    }
  }
  // 若 source_caret 超出所有 cell 范围:当 source_caret 在第一个 cell 之前
  // (例如 position 0 位于表格 block 起点、但首个 cell 的 source_range 从
  // "| " 之后才开始),定位到首个 cell;否则定位到末尾 cell。
  if found_row < 0 {
    let first_row = table.rows[0]
    let first_col_index = @core.clamp_int(
      first_row.length() - 1,
      0,
      column_count - 1,
    )
    let caret_before_first_cell = match
      first_row[first_col_index].source_range {
      Some(range) => source_caret < range.start
      None => false
    }
    if caret_before_first_cell {
      found_row = 0
      found_col = first_col_index
      found_cell = Some(first_row[first_col_index])
    } else {
      let last_row_index = row_count - 1
      let last_row = table.rows[last_row_index]
      let last_col_index = @core.clamp_int(
        last_row.length() - 1,
        0,
        column_count - 1,
      )
      if last_col_index >= 0 && last_col_index < last_row.length() {
        found_row = last_row_index
        found_col = last_col_index
        found_cell = Some(last_row[last_col_index])
      }
    }
  }
  if found_row < 0 {
    return caret_rect
  }
  let cell = match found_cell {
    Some(c) => c
    None => return caret_rect
  }
  // 与渲染端一致的 cell 几何。
  let cell_x = frame.origin.x + column_width * found_col.to_double()
  let row_y = frame.origin.y + table.row_height * found_row.to_double()
  let text_frame = @core.Rect::new(
    x=cell_x + table.cell_padding.left,
    y=row_y + table.cell_padding.top,
    width=max_double(
      0.0,
      column_width - table.cell_padding.left - table.cell_padding.right,
    ),
    height=table.row_height - table.cell_padding.top - table.cell_padding.bottom,
  )
  // cell 字体(header 加重,与渲染端一致)。
  let cell_font = if cell.header {
    { ..base_font_resolved, weight: 700 }
  } else {
    { ..base_font_resolved, weight: 400 }
  }
  // 空 cell 直接返回 cell 文本起点。
  if cell.text == "" {
    return rich_text_cursor_rect_at(text_frame.origin.x, text_frame, cell_font)
  }
  match cell.source_range {
    Some(range) => {
      // local_offset = source_caret 相对 cell.source_range.start 的偏移,钳到 [0, text.length]。
      let local_offset = @core.clamp_int(
        source_caret - range.start,
        0,
        cell.text.to_array().length(),
      )
      // prefix 宽度。
      let chars = cell.text.to_array()
      let prefix = String::from_array(
        chars[0:@core.clamp_int(local_offset, 0, chars.length())],
      )
      let prefix_width = rich_text_width(prefix, cell_font, text_system)
      // 按 align 计算 text_x(与点击映射对称)。
      let text_width = rich_text_width(cell.text, cell_font, text_system)
      let inner_width = text_frame.size.width
      let text_x = match cell.align.unwrap_or(@core.TextAlign::TextStart) {
        TextStart => text_frame.origin.x
        TextCenter => text_frame.origin.x + (inner_width - text_width) / 2.0
        TextEnd => text_frame.origin.x + inner_width - text_width
      }
      rich_text_cursor_rect_at(text_x + prefix_width, text_frame, cell_font)
    }
    None => rich_text_cursor_rect_at(text_frame.origin.x, text_frame, cell_font)
  }
}