///|
fn[Msg] rich_text_editor_event(
  ctx : @core.ViewEventContext,
  value~ : @core.Binding[String],
  format~ : RichTextFormatter,
  font~ : @core.FontSpec,
  on_input~ : ((String) -> Msg)?,
  on_input_edit~ : ((String, Int, @core.TextRange?) -> 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,
  visible_viewport_height~ : Double,
  visible_overscan~ : Double,
  visible_scroll_state~ : @core.ScrollState?,
) -> @core.ViewEventResult[Msg] {
  match ctx.event {
    @core.AppEvent::Pointer(event) =>
      rich_text_pointer_event(
        ctx,
        value~,
        format~,
        font~,
        on_selection_change~,
        on_prefix_click~,
        on_command_click~,
        event~,
        visible_scroll_y~,
        visible_viewport_height~,
        visible_overscan~,
        visible_scroll_state~,
      )
    @core.AppEvent::Keyboard(event) =>
      rich_text_keyboard_event(
        ctx,
        value~,
        format~,
        font~,
        on_input~,
        on_input_edit~,
        on_selection_change~,
        input_transform~,
        accepts_tab_input~,
        event~,
      )
    @core.AppEvent::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~ : @core.Binding[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,
  visible_viewport_height~ : Double,
  visible_overscan~ : Double,
  visible_scroll_state~ : @core.ScrollState?,
) -> @core.ViewEventResult[Msg] {
  ignore(visible_overscan)
  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_viewport_height~,
      visible_scroll_state~,
    )
  }
  match event.phase {
    @core.PointerPhase::Down =>
      if inside {
        let text = value.get()
        let document = format(text)
        let base_content = rich_text_editor_content_rect(ctx.frame)
        let scroll_y = markdown_session_current_scroll_y(
          visible_scroll_y, visible_scroll_state,
        )
        let content = rich_text_scrolled_content_rect(
          base_content,
          scroll_y~,
          visible_viewport_height~,
        )
        // 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=@core.ViewDirtyHint::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),
              composition: None,
              composition_cursor: None,
            }
            view_event_result(
              changed=true,
              focused=true,
              captured=true,
              text_control=Some(next_control),
              dirty=@core.ViewDirtyHint::ViewPaintDirty,
              messages=prefix_messages +
                selection_messages(on_selection_change, caret, None),
            )
          }
        }
      } else {
        @core.ViewEventResult::ignored()
      }
    @core.PointerPhase::Move =>
      match ctx.text_control.text_selection_anchor {
        Some(anchor) => {
          let text = value.get()
          let document = format(text)
          let base_content = rich_text_editor_content_rect(ctx.frame)
          let scroll_y = markdown_session_current_scroll_y(
            visible_scroll_y, visible_scroll_state,
          )
          let content = rich_text_scrolled_content_rect(
            base_content,
            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=@core.ViewDirtyHint::ViewPaintDirty,
            messages=if changed {
              selection_messages(on_selection_change, caret, next_selection)
            } else {
              []
            },
          )
        }
        None => @core.ViewEventResult::ignored()
      }
    @core.PointerPhase::Up
    | @core.PointerPhase::Cancel
    | @core.PointerPhase::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=@core.ViewDirtyHint::ViewPaintDirty,
      )
    }
  }
}

///|
fn[Msg] rich_text_scroll_event(
  value : @core.Binding[String],
  format : RichTextFormatter,
  font : @core.FontSpec,
  text_system : @core.TextSystem,
  event~ : @core.PointerEvent,
  visible_viewport_height~ : Double,
  visible_scroll_state~ : @core.ScrollState?,
) -> @core.ViewEventResult[Msg] {
  match visible_scroll_state {
    Some(state) => {
      let viewport_height = if visible_viewport_height > 0.0 {
        visible_viewport_height
      } else {
        0.0
      }
      let content_height = rich_text_document_height(
          format(value.get()),
          font,
          text_system~,
        ) +
        48.0
      let max_scroll = max_double(0.0, content_height - viewport_height)
      let current = state.offset()
      let next_y = min_double(
        max_double(0.0, current.y - event.wheel_delta.y),
        max_scroll,
      )
      state.scroll_to(x=0.0, y=next_y)
      view_event_result(
        changed=next_y != current.y,
        captured=true,
        dirty=@core.ViewDirtyHint::ViewPaintDirty,
      )
    }
    None => @core.ViewEventResult::ignored()
  }
}

///|
fn[Msg] rich_text_keyboard_event(
  ctx : @core.ViewEventContext,
  value~ : @core.Binding[String],
  format~ : RichTextFormatter,
  font~ : @core.FontSpec,
  on_input~ : ((String) -> Msg)?,
  on_input_edit~ : ((String, Int, @core.TextRange?) -> 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=@core.TextInputEvent::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=@core.TextInputEvent::InsertText("\n"),
      record_history=true,
    )
  }
  if @core.is_select_all_shortcut(event) {
    let next_control = select_rich_text_control(ctx.text_control, value.get())
    return view_event_result(
      changed=true,
      captured=true,
      text_control=Some(next_control),
      dirty=@core.ViewDirtyHint::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.get(), undo=true)
    value.set(result.text)
    return view_event_result(
      changed=result.changed,
      captured=true,
      text_control=Some(result.control),
      dirty=@core.ViewDirtyHint::ViewPaintDirty,
      messages=text_input_messages(on_input, result.text) +
        text_input_edit_messages(on_input_edit, result.text, result.control) +
        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.get(), undo=false)
    value.set(result.text)
    return view_event_result(
      changed=result.changed,
      captured=true,
      text_control=Some(result.control),
      dirty=@core.ViewDirtyHint::ViewPaintDirty,
      messages=text_input_messages(on_input, result.text) +
        text_input_edit_messages(on_input_edit, result.text, result.control) +
        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=@core.TextInputEvent::DeleteRange(selection),
          record_history=true,
        )
      None =>
        return rich_text_input_event(
          ctx,
          value~,
          on_input~,
          on_input_edit~,
          on_selection_change~,
          input_transform~,
          event=@core.TextInputEvent::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=@core.TextInputEvent::DeleteRange(selection),
          record_history=true,
        )
      None =>
        return rich_text_input_event(
          ctx,
          value~,
          on_input~,
          on_input_edit~,
          on_selection_change~,
          input_transform~,
          event=@core.TextInputEvent::DeleteSurrounding(0, 1),
          record_history=true,
        )
    }
  }
  if event.key == "ArrowUp" {
    let text = value.get()
    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.get()
    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.get(),
      @core.TextCaretDirection::MoveLeft,
      event.modifiers.shift,
      on_selection_change,
    )
  }
  if event.key == "ArrowRight" {
    return rich_text_caret_move_result(
      ctx,
      value.get(),
      @core.TextCaretDirection::MoveRight,
      event.modifiers.shift,
      on_selection_change,
    )
  }
  if event.key == "Home" {
    return rich_text_line_edge_move_result(
      ctx,
      value.get(),
      home=true,
      on_selection_change,
    )
  }
  if event.key == "End" {
    return rich_text_line_edge_move_result(
      ctx,
      value.get(),
      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=@core.TextInputEvent::InsertText(text),
          record_history=true,
        )
      } else {
        @core.ViewEventResult::ignored()
      }
    None => @core.ViewEventResult::ignored()
  }
}

///|
fn[Msg] rich_text_input_event(
  ctx : @core.ViewEventContext,
  value~ : @core.Binding[String],
  on_input~ : ((String) -> Msg)?,
  on_input_edit~ : ((String, Int, @core.TextRange?) -> 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.get()
  let result = match event {
    @core.TextInputEvent::InsertText(inserted) =>
      if inserted == "" {
        (
          { text, control: ctx.text_control, changed: false } :
          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~)
        }
      }
    @core.TextInputEvent::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~)
      }
    @core.TextInputEvent::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~)
  }
  if result.changed {
    value.set(result.text)
  }
  view_event_result(
    changed=result.changed,
    captured=result.changed,
    text_control=Some(result.control),
    dirty=@core.ViewDirtyHint::ViewPaintDirty,
    messages=if result.changed {
      text_input_messages(on_input, result.text) +
      text_input_edit_messages(on_input_edit, result.text, result.control) +
      selection_messages(
        on_selection_change,
        result.control.caret,
        result.control.selection,
      )
    } else {
      []
    },
  )
}

///|
fn[Msg] rich_text_editor_command(
  ctx : @core.ViewEventContext,
  value~ : @core.Binding[String],
  format~ : RichTextFormatter,
  on_input~ : ((String) -> Msg)?,
  on_input_edit~ : ((String, Int, @core.TextRange?) -> 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.get()
  let document = format(text)
  match intent {
    @core.CommandIntent::Copy =>
      @core.ViewTextCommandResult::new(
        result=@core.TextCommandResult::handled(
          clipboard_text=Some(
            selected_rich_text(text, document, ctx.text_control.selection),
          ),
        ),
      )
    @core.CommandIntent::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=@core.TextInputEvent::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("")),
          )
      }
    }
    @core.CommandIntent::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=@core.TextInputEvent::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(),
          )
      }
    @core.CommandIntent::Undo => {
      let result = restore_text_history(ctx.text_control, text, undo=true)
      if result.changed {
        value.set(result.text)
      }
      @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) +
          selection_messages(
            on_selection_change,
            result.control.caret,
            result.control.selection,
          )
        } else {
          []
        },
      )
    }
    @core.CommandIntent::Redo => {
      let result = restore_text_history(ctx.text_control, text, undo=false)
      if result.changed {
        value.set(result.text)
      }
      @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) +
          selection_messages(
            on_selection_change,
            result.control.caret,
            result.control.selection,
          )
        } else {
          []
        },
      )
    }
    @core.CommandIntent::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,
  visible_scroll_state~ : @core.ScrollState?,
  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 scroll_y = markdown_session_current_scroll_y(
    visible_scroll_y, visible_scroll_state,
  )
  let content = rich_text_scrolled_content_rect(
    base_content,
    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,
    element_id: None,
    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,
  }
  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 {
      []
    },
  )
}