///|
fn[Msg] rich_text_editor_event(
  ctx : @core.ViewEventContext,
  value~ : String,
  format~ : RichTextFormatter,
  font~ : @core.FontSpec,
  on_input~ : ((String) -> Msg)?,
  on_input_edit~ : ((String, Int, @core.TextRange?, RichTextInputEdit?) -> Msg)?,
  on_selection_change~ : ((Int, @core.TextRange?) -> Msg)?,
  on_prefix_click~ : ((Int) -> Msg)?,
  on_command_click~ : ((Int) -> Msg)?,
  input_transform~ : ((String, Int, @core.TextRange?, String) -> RichTextInputTransform?)?,
  accepts_tab_input~ : Bool,
  visible_scroll_y~ : Double,
  on_scroll~ : ((Double) -> Msg)?,
  visible_viewport_height~ : Double,
  visible_overscan~ : Double,
) -> @core.ViewEventResult[Msg] {
  match ctx.event {
    Pointer(event) =>
      rich_text_pointer_event(
        ctx,
        value~,
        format~,
        font~,
        on_selection_change~,
        on_prefix_click~,
        on_command_click~,
        event~,
        visible_scroll_y~,
        on_scroll~,
        visible_viewport_height~,
        visible_overscan~,
      )
    Keyboard(event) =>
      rich_text_keyboard_event(
        ctx,
        value~,
        format~,
        font~,
        on_input~,
        on_input_edit~,
        on_selection_change~,
        input_transform~,
        accepts_tab_input~,
        event~,
      )
    TextInput(event) =>
      rich_text_input_event(
        ctx,
        value~,
        on_input~,
        on_input_edit~,
        on_selection_change~,
        input_transform~,
        event~,
        record_history=true,
      )
    _ => @core.ViewEventResult::ignored()
  }
}

///|
fn[Msg] rich_text_pointer_event(
  ctx : @core.ViewEventContext,
  value~ : String,
  format~ : RichTextFormatter,
  font~ : @core.FontSpec,
  on_selection_change~ : ((Int, @core.TextRange?) -> Msg)?,
  on_prefix_click~ : ((Int) -> Msg)?,
  on_command_click~ : ((Int) -> Msg)?,
  event~ : @core.PointerEvent,
  visible_scroll_y~ : Double,
  on_scroll~ : ((Double) -> Msg)?,
  visible_viewport_height~ : Double,
  visible_overscan~ : Double,
) -> @core.ViewEventResult[Msg] {
  ignore(visible_overscan)
  // Secondary/middle presses reach the view tree now that host backends route
  // right-button downs for per-target context menus. A right press only places
  // the caret (see the Down branch), so the menu the host builds next describes
  // what was pressed; every other button and phase is ignored, which keeps a
  // right-drag from selecting and leaves the wheel path (button 0) untouched.
  if event.button != 0 && !(event.button == 1 && event.phase == Down) {
    return @core.ViewEventResult::ignored()
  }
  let inside = ctx.frame.contains(event.position)
  if inside && (event.wheel_delta.x != 0.0 || event.wheel_delta.y != 0.0) {
    return rich_text_scroll_event(
      value,
      format,
      font,
      ctx.text_system,
      event~,
      visible_scroll_y~,
      on_scroll~,
      visible_viewport_height~,
      content_width=rich_text_editor_content_rect(ctx.frame).size.width,
    )
  }
  match event.phase {
    Down =>
      if inside {
        let text = value
        let document = format(text)
        let base_content = rich_text_editor_content_rect(ctx.frame)
        let content = rich_text_scrolled_content_rect(
          base_content,
          scroll_y=visible_scroll_y,
          visible_viewport_height~,
        )
        // A right press moves the caret the way mature editors do, so the
        // context menu the host builds next describes the text that was
        // pressed. A press inside the current selection keeps it: that is what
        // makes "Bold" in the menu apply to the selected run. The result
        // reports `activated=false` on purpose — the enclosing
        // `on_secondary_tap` observer must still run, so the app records its
        // menu target from the same press — and leaves no selection anchor
        // behind, so a right-drag never grows a selection.
        if event.button == 1 {
          let caret = rich_text_document_source_offset_at_point(
            document,
            event.position,
            content,
            font,
            ctx.text_system,
          ).unwrap_or(text_length(text))
          if rich_text_selection_covers(ctx.text_control.selection, caret) {
            return view_event_result(focused=true)
          }
          return view_event_result(
            changed=true,
            focused=true,
            text_control=Some({
              ..ctx.text_control,
              caret,
              selection: None,
              text_selection_anchor: None,
              goal_column: None,
              text_selection_origin: None,
              composition: None,
              composition_cursor: None,
            }),
            dirty=ViewPaintDirty,
            messages=selection_messages(on_selection_change, caret, None),
          )
        }
        // A Cmd/Ctrl-held click opens links/images instead of moving the caret:
        // dispatch the command-click hook with the source offset and swallow the
        // caret placement so the selection is not disturbed.
        let command_modifiers = event.modifiers.meta || event.modifiers.control
        match (command_modifiers, on_command_click) {
          (true, Some(handler)) => {
            let offset = rich_text_document_source_offset_at_point(
              document,
              event.position,
              content,
              font,
              ctx.text_system,
            ).unwrap_or(text_length(text))
            view_event_result(
              changed=true,
              focused=true,
              captured=true,
              text_control=Some(ctx.text_control),
              dirty=ViewPaintDirty,
              messages=[handler(offset)],
            )
          }
          _ => {
            let prefix_messages = match on_prefix_click {
              Some(handler) =>
                match
                  rich_text_document_prefix_source_offset_at_point(
                    document,
                    event.position,
                    content,
                    font,
                    ctx.text_system,
                  ) {
                  Some(offset) => [handler(offset)]
                  None => []
                }
              None => []
            }
            let caret = rich_text_document_source_offset_at_point(
              document,
              event.position,
              content,
              font,
              ctx.text_system,
            ).unwrap_or(text_length(text))
            let next_control = {
              ..ctx.text_control,
              caret,
              selection: None,
              text_selection_anchor: Some(caret),
              goal_column: None,
              composition: None,
              composition_cursor: None,
            }
            view_event_result(
              changed=true,
              focused=true,
              captured=true,
              text_control=Some(next_control),
              dirty=ViewPaintDirty,
              messages=prefix_messages +
                selection_messages(on_selection_change, caret, None),
            )
          }
        }
      } else {
        @core.ViewEventResult::ignored()
      }
    Move =>
      match ctx.text_control.text_selection_anchor {
        Some(anchor) => {
          let text = value
          let document = format(text)
          let base_content = rich_text_editor_content_rect(ctx.frame)
          let content = rich_text_scrolled_content_rect(
            base_content,
            scroll_y=visible_scroll_y,
            visible_viewport_height~,
          )
          let caret = rich_text_document_source_offset_at_point(
            document,
            event.position,
            content,
            font,
            ctx.text_system,
          ).unwrap_or(text_length(text))
          let next_selection = if caret == anchor {
            None
          } else {
            Some(@core.TextRange::new(start=anchor, end=caret))
          }
          let changed = ctx.text_control.caret != caret ||
            ctx.text_control.selection != next_selection
          let next_control = {
            ..ctx.text_control,
            caret,
            selection: next_selection,
            composition: None,
            composition_cursor: None,
          }
          view_event_result(
            changed~,
            captured=true,
            text_control=Some(next_control),
            dirty=ViewPaintDirty,
            messages=if changed {
              selection_messages(on_selection_change, caret, next_selection)
            } else {
              []
            },
          )
        }
        None => @core.ViewEventResult::ignored()
      }
    Up | Cancel | Exit => {
      let changed = ctx.text_control.text_selection_anchor is Some(_)
      view_event_result(
        changed~,
        captured=changed,
        text_control=Some({ ..ctx.text_control, text_selection_anchor: None, }),
        dirty=ViewPaintDirty,
      )
    }
  }
}

///|
fn[Msg] rich_text_scroll_event(
  value : String,
  format : RichTextFormatter,
  font : @core.FontSpec,
  text_system : @core.TextSystem,
  event~ : @core.PointerEvent,
  visible_scroll_y~ : Double,
  on_scroll~ : ((Double) -> Msg)?,
  visible_viewport_height~ : Double,
  content_width? : Double = 0.0,
) -> @core.ViewEventResult[Msg] {
  match on_scroll {
    Some(handler) => {
      let viewport_height = if visible_viewport_height > 0.0 {
        visible_viewport_height
      } else {
        0.0
      }
      let content_height = rich_text_document_height(
          format(value),
          font,
          text_system~,
          block_width=content_width,
        ) +
        48.0
      let max_scroll = max_double(0.0, content_height - viewport_height)
      let next_y = min_double(
        max_double(0.0, visible_scroll_y - event.wheel_delta.y),
        max_scroll,
      )
      view_event_result(
        changed=next_y != visible_scroll_y,
        activated=next_y != visible_scroll_y,
        captured=true,
        dirty=ViewPaintDirty,
        messages=if next_y != visible_scroll_y { [handler(next_y)] } else { [] },
      )
    }
    None => @core.ViewEventResult::ignored()
  }
}

///|
fn[Msg] rich_text_keyboard_event(
  ctx : @core.ViewEventContext,
  value~ : String,
  format~ : RichTextFormatter,
  font~ : @core.FontSpec,
  on_input~ : ((String) -> Msg)?,
  on_input_edit~ : ((String, Int, @core.TextRange?, RichTextInputEdit?) -> Msg)?,
  on_selection_change~ : ((Int, @core.TextRange?) -> Msg)?,
  input_transform~ : ((String, Int, @core.TextRange?, String) -> RichTextInputTransform?)?,
  accepts_tab_input~ : Bool,
  event~ : @core.KeyboardEvent,
) -> @core.ViewEventResult[Msg] {
  if !event.pressed {
    return @core.ViewEventResult::ignored()
  }
  if event.key == "Tab" && accepts_tab_input {
    let text = if event.modifiers.shift { "\u{000b}" } else { "\t" }
    return rich_text_input_event(
      ctx,
      value~,
      on_input~,
      on_input_edit~,
      on_selection_change~,
      input_transform~,
      event=InsertText(text),
      record_history=true,
    )
  }
  if event.key == "Enter" {
    return rich_text_input_event(
      ctx,
      value~,
      on_input~,
      on_input_edit~,
      on_selection_change~,
      input_transform~,
      event=InsertText("\n"),
      record_history=true,
    )
  }
  if @core.is_select_all_shortcut(event) {
    let next_control = select_rich_text_control(ctx.text_control, value)
    return view_event_result(
      changed=true,
      captured=true,
      text_control=Some(next_control),
      dirty=ViewPaintDirty,
      messages=selection_messages(
        on_selection_change,
        next_control.caret,
        next_control.selection,
      ),
    )
  }
  if @core.is_undo_shortcut(event) {
    let result = restore_text_history(ctx.text_control, value, undo=true)
    return view_event_result(
      changed=result.changed,
      captured=true,
      text_control=Some(result.control),
      dirty=ViewPaintDirty,
      messages=text_input_messages(on_input, result.text) +
        text_input_edit_messages(
          on_input_edit,
          result.text,
          result.control,
          record=result.edit,
        ) +
        selection_messages(
          on_selection_change,
          result.control.caret,
          result.control.selection,
        ),
    )
  }
  if @core.is_redo_shortcut(event) {
    let result = restore_text_history(ctx.text_control, value, undo=false)
    return view_event_result(
      changed=result.changed,
      captured=true,
      text_control=Some(result.control),
      dirty=ViewPaintDirty,
      messages=text_input_messages(on_input, result.text) +
        text_input_edit_messages(
          on_input_edit,
          result.text,
          result.control,
          record=result.edit,
        ) +
        selection_messages(
          on_selection_change,
          result.control.caret,
          result.control.selection,
        ),
    )
  }
  if event.key == "Backspace" {
    match @core.active_text_selection(ctx.text_control.selection) {
      Some(selection) =>
        return rich_text_input_event(
          ctx,
          value~,
          on_input~,
          on_input_edit~,
          on_selection_change~,
          input_transform~,
          event=DeleteRange(selection),
          record_history=true,
        )
      None =>
        return rich_text_input_event(
          ctx,
          value~,
          on_input~,
          on_input_edit~,
          on_selection_change~,
          input_transform~,
          event=DeleteSurrounding(1, 0),
          record_history=true,
        )
    }
  }
  if event.key == "Delete" {
    match @core.active_text_selection(ctx.text_control.selection) {
      Some(selection) =>
        return rich_text_input_event(
          ctx,
          value~,
          on_input~,
          on_input_edit~,
          on_selection_change~,
          input_transform~,
          event=DeleteRange(selection),
          record_history=true,
        )
      None =>
        return rich_text_input_event(
          ctx,
          value~,
          on_input~,
          on_input_edit~,
          on_selection_change~,
          input_transform~,
          event=DeleteSurrounding(0, 1),
          record_history=true,
        )
    }
  }
  if event.key == "ArrowUp" {
    let text = value
    let document = format(text)
    let content = rich_text_editor_content_rect(ctx.frame)
    return rich_text_vertical_caret_move_result(
      ctx,
      text,
      document,
      content,
      font,
      up=true,
      extend_selection=event.modifiers.shift,
      on_selection_change,
    )
  }
  if event.key == "ArrowDown" {
    let text = value
    let document = format(text)
    let content = rich_text_editor_content_rect(ctx.frame)
    return rich_text_vertical_caret_move_result(
      ctx,
      text,
      document,
      content,
      font,
      up=false,
      extend_selection=event.modifiers.shift,
      on_selection_change,
    )
  }
  if event.key == "ArrowLeft" {
    return rich_text_caret_move_result(
      ctx,
      value,
      MoveLeft,
      event.modifiers.shift,
      on_selection_change,
    )
  }
  if event.key == "ArrowRight" {
    return rich_text_caret_move_result(
      ctx,
      value,
      MoveRight,
      event.modifiers.shift,
      on_selection_change,
    )
  }
  if event.key == "Home" {
    return rich_text_line_edge_move_result(
      ctx,
      value,
      home=true,
      on_selection_change,
    )
  }
  if event.key == "End" {
    return rich_text_line_edge_move_result(
      ctx,
      value,
      home=false,
      on_selection_change,
    )
  }
  match event.text {
    Some(text) =>
      if text != "" {
        rich_text_input_event(
          ctx,
          value~,
          on_input~,
          on_input_edit~,
          on_selection_change~,
          input_transform~,
          event=InsertText(text),
          record_history=true,
        )
      } else {
        @core.ViewEventResult::ignored()
      }
    None => @core.ViewEventResult::ignored()
  }
}

///|
fn[Msg] rich_text_input_event(
  ctx : @core.ViewEventContext,
  value~ : String,
  on_input~ : ((String) -> Msg)?,
  on_input_edit~ : ((String, Int, @core.TextRange?, RichTextInputEdit?) -> Msg)?,
  on_selection_change~ : ((Int, @core.TextRange?) -> Msg)?,
  input_transform~ : ((String, Int, @core.TextRange?, String) -> RichTextInputTransform?)?,
  event~ : @core.TextInputEvent,
  record_history~ : Bool,
) -> @core.ViewEventResult[Msg] {
  let text = value
  let result = match event {
    InsertText(inserted) =>
      if inserted == "" {
        (
          { text, control: ctx.text_control, changed: false, edit: None, } :
          TextInputApplyResult)
      } else {
        match
          rich_text_transform_insert(
            input_transform,
            text,
            ctx.text_control,
            inserted,
          ) {
          Some(transformed) =>
            apply_rich_text_input_transform(
              ctx.text_control,
              text,
              transformed,
              record_history~,
            )
          None =>
            apply_text_input(ctx.text_control, text, event, record_history~)
        }
      }
    DeleteRange(_) =>
      match
        rich_text_transform_insert(
          input_transform,
          text,
          ctx.text_control,
          "\u{000e}",
        ) {
        Some(transformed) =>
          apply_rich_text_input_transform(
            ctx.text_control,
            text,
            transformed,
            record_history~,
          )
        None => apply_text_input(ctx.text_control, text, event, record_history~)
      }
    DeleteSurrounding(before, after) => {
      let signal = if before > 0 && after == 0 {
        "\u{0008}"
      } else if before == 0 && after > 0 {
        "\u{007f}"
      } else {
        ""
      }
      if signal != "" {
        match
          rich_text_transform_insert(
            input_transform,
            text,
            ctx.text_control,
            signal,
          ) {
          Some(transformed) =>
            apply_rich_text_input_transform(
              ctx.text_control,
              text,
              transformed,
              record_history~,
            )
          None =>
            apply_text_input(ctx.text_control, text, event, record_history~)
        }
      } else {
        apply_text_input(ctx.text_control, text, event, record_history~)
      }
    }
    _ => apply_text_input(ctx.text_control, text, event, record_history~)
  }
  view_event_result(
    changed=result.changed,
    captured=result.changed,
    text_control=Some(result.control),
    dirty=ViewPaintDirty,
    messages=if result.changed {
      text_input_messages(on_input, result.text) +
      text_input_edit_messages(
        on_input_edit,
        result.text,
        result.control,
        record=result.edit,
      ) +
      selection_messages(
        on_selection_change,
        result.control.caret,
        result.control.selection,
      )
    } else {
      []
    },
  )
}

///|
fn[Msg] rich_text_editor_command(
  ctx : @core.ViewEventContext,
  value~ : String,
  format~ : RichTextFormatter,
  on_input~ : ((String) -> Msg)?,
  on_input_edit~ : ((String, Int, @core.TextRange?, RichTextInputEdit?) -> Msg)?,
  on_selection_change~ : ((Int, @core.TextRange?) -> Msg)?,
  input_transform~ : ((String, Int, @core.TextRange?, String) -> RichTextInputTransform?)?,
  intent : @core.CommandIntent,
  paste_text~ : String?,
) -> @core.ViewTextCommandResult[Msg] {
  let text = value
  let document = format(text)
  match intent {
    Copy =>
      @core.ViewTextCommandResult::new(
        result=@core.TextCommandResult::handled(
          clipboard_text=Some(
            selected_rich_text(text, document, ctx.text_control.selection),
          ),
        ),
      )
    Cut => {
      let copied = selected_rich_text(
        text,
        document,
        ctx.text_control.selection,
      )
      match @core.active_text_selection(ctx.text_control.selection) {
        Some(selection) => {
          let event_result = rich_text_input_event(
            ctx,
            value~,
            on_input~,
            on_input_edit~,
            on_selection_change~,
            input_transform~,
            event=DeleteRange(selection),
            record_history=true,
          )
          @core.ViewTextCommandResult::new(
            result=@core.TextCommandResult::handled(
              changed=event_result.changed,
              clipboard_text=Some(copied),
            ),
            text_control=event_result.text_control,
            messages=event_result.messages,
          )
        }
        None =>
          @core.ViewTextCommandResult::new(
            result=@core.TextCommandResult::handled(clipboard_text=Some("")),
          )
      }
    }
    Paste =>
      match paste_text {
        Some(value_to_paste) => {
          let event_result = rich_text_input_event(
            ctx,
            value~,
            on_input~,
            on_input_edit~,
            on_selection_change~,
            input_transform~,
            event=InsertText(value_to_paste),
            record_history=true,
          )
          @core.ViewTextCommandResult::new(
            result=@core.TextCommandResult::handled(
              changed=event_result.changed,
            ),
            text_control=event_result.text_control,
            messages=event_result.messages,
          )
        }
        None =>
          @core.ViewTextCommandResult::new(
            result=@core.TextCommandResult::handled(),
          )
      }
    Undo => {
      let result = restore_text_history(ctx.text_control, text, undo=true)
      @core.ViewTextCommandResult::new(
        result=@core.TextCommandResult::handled(changed=result.changed),
        text_control=Some(result.control),
        messages=if result.changed {
          text_input_messages(on_input, result.text) +
          text_input_edit_messages(
            on_input_edit,
            result.text,
            result.control,
            record=result.edit,
          ) +
          selection_messages(
            on_selection_change,
            result.control.caret,
            result.control.selection,
          )
        } else {
          []
        },
      )
    }
    Redo => {
      let result = restore_text_history(ctx.text_control, text, undo=false)
      @core.ViewTextCommandResult::new(
        result=@core.TextCommandResult::handled(changed=result.changed),
        text_control=Some(result.control),
        messages=if result.changed {
          text_input_messages(on_input, result.text) +
          text_input_edit_messages(
            on_input_edit,
            result.text,
            result.control,
            record=result.edit,
          ) +
          selection_messages(
            on_selection_change,
            result.control.caret,
            result.control.selection,
          )
        } else {
          []
        },
      )
    }
    SelectAll => {
      let next_control = select_rich_text_control(ctx.text_control, text)
      @core.ViewTextCommandResult::new(
        result=@core.TextCommandResult::handled(changed=true),
        text_control=Some(next_control),
        messages=selection_messages(
          on_selection_change,
          next_control.caret,
          next_control.selection,
        ),
      )
    }
    _ => @core.ViewTextCommandResult::ignored()
  }
}

///|
fn rich_text_input_state(
  text~ : String,
  frame~ : @core.Rect,
  format~ : RichTextFormatter,
  font~ : @core.FontSpec,
  control~ : @core.TextControlStateContext,
  selection_caret~ : Int,
  selection_range~ : @core.TextRange?,
  visible_scroll_y~ : Double,
  visible_viewport_height~ : Double,
  visible_overscan~ : Double,
  text_system~ : @core.TextSystem,
) -> @core.TextInputState {
  ignore(visible_overscan)
  let boundaries = @core.TextGraphemeBoundaries::new(text~)
  let control = if selection_caret >= 0 {
    { ..control, caret: selection_caret, selection: selection_range, }
  } else {
    control
  }
  let display_text = rich_text_with_composition(text, control, focused=true)
  let base_content = rich_text_editor_content_rect(frame)
  let content = rich_text_scrolled_content_rect(
    base_content,
    scroll_y=visible_scroll_y,
    visible_viewport_height~,
  )
  let caret = boundaries.nearest_boundary(control.caret)
  let visual_caret = @core.clamp_int(
    rich_text_visual_caret(text, control, focused=true),
    0,
    text_length(display_text),
  )
  let document = format(display_text)
  let caret_rect = rich_text_document_caret_rect_or_plain(
    document, visual_caret, content, font, text_system, display_text,
  )
  {
    focused: true,
    text,
    caret,
    selection: rich_text_input_grapheme_selection(boundaries, control.selection),
    composition: control.composition,
    composition_cursor: control.composition_cursor,
    frame,
    caret_rect,
  }
}

///|
fn[Msg] rich_text_caret_move_result(
  ctx : @core.ViewEventContext,
  text : String,
  direction : @core.TextCaretDirection,
  extend_selection : Bool,
  on_selection_change : ((Int, @core.TextRange?) -> Msg)?,
) -> @core.ViewEventResult[Msg] {
  let caret_move = @core.move_text_caret(
    text,
    ctx.text_control.caret,
    ctx.text_control.selection,
    direction,
    extend_selection~,
  )
  let next_control = {
    ..ctx.text_control,
    caret: caret_move.caret,
    selection: caret_move.selection,
    composition: None,
    composition_cursor: None,
    // 水平移动重置目标列(M6②):只有垂直移动才播种/保持 goal
    goal_column: 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=ViewPaintDirty,
    messages=if changed {
      selection_messages(
        on_selection_change,
        next_control.caret,
        next_control.selection,
      )
    } else {
      []
    },
  )
}