// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0

// cSpell: ignore textitem
/*!
This module contains the builtin text related items.

When adding an item or a property, it needs to be kept in sync with different place.
Lookup the [`crate::items`] module documentation.
*/
use super::{
    EventResult, FontMetrics, InputMethodHints, InputType, Item, ItemConsts, ItemRc, ItemRef,
    KeyEventArg, KeyEventResult, KeyEventType, PointArg, PointerEventButton, RenderingResult,
    StringArg, TextHorizontalAlignment, TextOverflow, TextStrokeStyle, TextVerticalAlignment,
    TextWrap, VoidArg,
};
use crate::graphics::{Brush, Color, FontRequest};
use crate::input::{
    FocusEvent, FocusEventResult, FocusReason, InputEventFilterResult, InputEventResult,
    InternalKeyEvent, KeyboardModifiers, MouseEvent, StandardShortcut, TextShortcut, key_codes,
};
use crate::item_rendering::{
    CachedRenderingData, HasFont, ItemRenderer, PlainOrStyledText, RenderString, RenderText,
};
use crate::layout::{LayoutInfo, Orientation};
use crate::lengths::{LogicalLength, LogicalPoint, LogicalRect, LogicalSize};
use crate::platform::Clipboard;
#[cfg(feature = "rtti")]
use crate::rtti::*;
use crate::string::string_to_float;
use crate::window::{InputMethodProperties, InputMethodRequest, WindowAdapter, WindowInner};
use crate::{Callback, Coord, Property, SharedString, SharedVector};
use alloc::{rc::Rc, string::String};
use const_field_offset::FieldOffsets;
use core::cell::Cell;
use core::pin::Pin;
#[allow(unused)]
use euclid::num::Ceil;
use i_slint_core_macros::*;
use unicode_segmentation::UnicodeSegmentation;

/// The implementation of the `Text` element
#[repr(C)]
#[derive(FieldOffsets, Default, SlintElement)]
#[pin]
pub struct ComplexText {
    pub width: Property<LogicalLength>,
    pub height: Property<LogicalLength>,
    pub text: Property<SharedString>,
    pub font_size: Property<LogicalLength>,
    pub font_weight: Property<i32>,
    pub color: Property<Brush>,
    pub horizontal_alignment: Property<TextHorizontalAlignment>,
    pub vertical_alignment: Property<TextVerticalAlignment>,
    pub max_lines: Property<i32>,

    pub font_family: Property<SharedString>,
    pub font_italic: Property<bool>,
    pub wrap: Property<TextWrap>,
    pub overflow: Property<TextOverflow>,
    pub letter_spacing: Property<LogicalLength>,
    pub stroke: Property<Brush>,
    pub stroke_width: Property<LogicalLength>,
    pub stroke_style: Property<TextStrokeStyle>,
    pub cached_rendering_data: CachedRenderingData,
}

impl Item for ComplexText {
    fn init(self: Pin<&Self>, _self_rc: &ItemRc) {}

    fn deinit(self: Pin<&Self>, _window_adapter: &Rc<dyn WindowAdapter>) {}

    fn layout_info(
        self: Pin<&Self>,
        orientation: Orientation,
        cross_axis_constraint: Coord,
        window_adapter: &Rc<dyn WindowAdapter>,
        self_rc: &ItemRc,
    ) -> LayoutInfo {
        text_layout_info(
            self,
            self_rc,
            window_adapter,
            orientation,
            Self::FIELD_OFFSETS.width().apply_pin(self),
            cross_axis_constraint,
        )
    }

    fn input_event_filter_before_children(
        self: Pin<&Self>,
        _: &MouseEvent,
        _window_adapter: &Rc<dyn WindowAdapter>,
        _self_rc: &ItemRc,
        _: &mut super::MouseCursorInner,
    ) -> InputEventFilterResult {
        InputEventFilterResult::ForwardAndIgnore
    }

    fn input_event(
        self: Pin<&Self>,
        _: &MouseEvent,
        _window_adapter: &Rc<dyn WindowAdapter>,
        _self_rc: &ItemRc,
        _: &mut super::MouseCursorInner,
    ) -> InputEventResult {
        InputEventResult::EventIgnored
    }

    fn capture_key_event(
        self: Pin<&Self>,
        _: &InternalKeyEvent,
        _window_adapter: &Rc<dyn WindowAdapter>,
        _self_rc: &ItemRc,
    ) -> KeyEventResult {
        KeyEventResult::EventIgnored
    }

    fn key_event(
        self: Pin<&Self>,
        _: &InternalKeyEvent,
        _window_adapter: &Rc<dyn WindowAdapter>,
        _self_rc: &ItemRc,
    ) -> KeyEventResult {
        KeyEventResult::EventIgnored
    }

    fn focus_event(
        self: Pin<&Self>,
        _: &FocusEvent,
        _window_adapter: &Rc<dyn WindowAdapter>,
        _self_rc: &ItemRc,
    ) -> FocusEventResult {
        FocusEventResult::FocusIgnored
    }

    fn render(
        self: Pin<&Self>,
        backend: &mut &mut dyn ItemRenderer,
        self_rc: &ItemRc,
        size: LogicalSize,
    ) -> RenderingResult {
        (*backend).draw_text(self, self_rc, size, &self.cached_rendering_data);
        RenderingResult::ContinueRenderingChildren
    }

    fn bounding_rect(
        self: core::pin::Pin<&Self>,
        _window_adapter: &Rc<dyn WindowAdapter>,
        _self_rc: &ItemRc,
        geometry: LogicalRect,
    ) -> LogicalRect {
        geometry
    }

    fn clips_children(self: core::pin::Pin<&Self>) -> bool {
        false
    }
}

impl ItemConsts for ComplexText {
    const cached_rendering_data_offset: const_field_offset::FieldOffset<
        ComplexText,
        CachedRenderingData,
    > = ComplexText::FIELD_OFFSETS.cached_rendering_data().as_unpinned_projection();
}

impl HasFont for ComplexText {
    fn font_request(self: Pin<&Self>, self_rc: &crate::items::ItemRc) -> FontRequest {
        crate::items::WindowItem::resolved_font_request(
            self_rc,
            self.font_family(),
            self.font_weight(),
            self.font_size(),
            self.letter_spacing(),
            self.font_italic(),
        )
    }
}

impl RenderString for ComplexText {
    fn text(self: Pin<&Self>) -> PlainOrStyledText {
        PlainOrStyledText::Plain(self.text())
    }

    fn max_lines(self: Pin<&Self>) -> i32 {
        Self::FIELD_OFFSETS.max_lines().apply_pin(self).get()
    }

    fn stroke(self: Pin<&Self>) -> (Brush, LogicalLength, TextStrokeStyle) {
        (self.stroke(), self.stroke_width(), self.stroke_style())
    }
}

impl RenderText for ComplexText {
    fn target_size(self: Pin<&Self>) -> LogicalSize {
        LogicalSize::from_lengths(self.width(), self.height())
    }

    fn color(self: Pin<&Self>) -> Brush {
        self.color()
    }

    fn alignment(
        self: Pin<&Self>,
    ) -> (super::TextHorizontalAlignment, super::TextVerticalAlignment) {
        (self.horizontal_alignment(), self.vertical_alignment())
    }

    fn wrap(self: Pin<&Self>) -> TextWrap {
        self.wrap()
    }

    fn overflow(self: Pin<&Self>) -> TextOverflow {
        self.overflow()
    }

    fn is_markdown(self: Pin<&Self>) -> bool {
        false
    }
}

impl ComplexText {
    pub fn font_metrics(
        self: Pin<&Self>,
        window_adapter: &Rc<dyn WindowAdapter>,
        self_rc: &ItemRc,
    ) -> FontMetrics {
        let font_request = self.font_request(self_rc);
        window_adapter.renderer().font_metrics(font_request)
    }
}

/// The implementation of the `Text` element
#[repr(C)]
#[derive(FieldOffsets, Default, SlintElement)]
#[pin]
pub struct StyledTextItem {
    pub width: Property<LogicalLength>,
    pub height: Property<LogicalLength>,
    pub text: Property<crate::styled_text::StyledText>,
    pub default_color: Property<Brush>,
    pub default_font_size: Property<LogicalLength>,
    pub default_font_family: Property<SharedString>,
    pub horizontal_alignment: Property<TextHorizontalAlignment>,
    pub vertical_alignment: Property<TextVerticalAlignment>,
    pub max_lines: Property<i32>,
    pub link_clicked: Callback<StringArg>,
    pub link_color: Property<Color>,
    pub cached_rendering_data: CachedRenderingData,
}

impl Item for StyledTextItem {
    fn init(self: Pin<&Self>, _self_rc: &ItemRc) {}

    fn deinit(self: Pin<&Self>, _window_adapter: &Rc<dyn WindowAdapter>) {}

    fn layout_info(
        self: Pin<&Self>,
        orientation: Orientation,
        cross_axis_constraint: Coord,
        window_adapter: &Rc<dyn WindowAdapter>,
        self_rc: &ItemRc,
    ) -> LayoutInfo {
        text_layout_info(
            self,
            self_rc,
            window_adapter,
            orientation,
            Self::FIELD_OFFSETS.width().apply_pin(self),
            cross_axis_constraint,
        )
    }

    fn input_event_filter_before_children(
        self: Pin<&Self>,
        _: &MouseEvent,
        _window_adapter: &Rc<dyn WindowAdapter>,
        _self_rc: &ItemRc,
        _: &mut super::MouseCursorInner,
    ) -> InputEventFilterResult {
        InputEventFilterResult::ForwardEvent
    }

    #[cfg_attr(not(feature = "shared-parley"), allow(unused))]
    fn input_event(
        self: Pin<&Self>,
        event: &MouseEvent,
        window_adapter: &Rc<dyn WindowAdapter>,
        self_rc: &ItemRc,
        cursor: &mut super::MouseCursorInner,
    ) -> InputEventResult {
        #[cfg(feature = "shared-parley")]
        let find_link = |position: &LogicalPoint| {
            let window_inner = WindowInner::from_pub(window_adapter.window());
            let scale_factor = crate::lengths::ScaleFactor::new(window_inner.scale_factor());
            crate::textlayout::sharedparley::link_under_cursor(
                scale_factor,
                self,
                self_rc,
                LogicalSize::from_lengths(self.width(), self.height()),
                *position * scale_factor,
                window_adapter.window(),
                None,
            )
        };
        match event {
            #[cfg(feature = "shared-parley")]
            MouseEvent::Released {
                position,
                button: PointerEventButton::Left,
                click_count: _,
                touch_finger_id: _,
            } => {
                if let Some(link) = find_link(position) {
                    *cursor = super::MouseCursorInner::BuiltIn(super::BuiltInMouseCursor::Pointer);
                    Self::FIELD_OFFSETS.link_clicked().apply_pin(self).call(&(link.into(),));
                }
                InputEventResult::EventAccepted
            }
            #[cfg(feature = "shared-parley")]
            MouseEvent::Moved { position, .. }
            | MouseEvent::Pressed { position, .. }
            | MouseEvent::Released { position, .. } => {
                if find_link(position).is_some() {
                    *cursor = super::MouseCursorInner::BuiltIn(super::BuiltInMouseCursor::Pointer);
                }
                InputEventResult::EventAccepted
            }
            _ => InputEventResult::EventIgnored,
        }
    }

    fn capture_key_event(
        self: Pin<&Self>,
        _: &InternalKeyEvent,
        _window_adapter: &Rc<dyn WindowAdapter>,
        _self_rc: &ItemRc,
    ) -> KeyEventResult {
        KeyEventResult::EventIgnored
    }

    fn key_event(
        self: Pin<&Self>,
        _: &InternalKeyEvent,
        _window_adapter: &Rc<dyn WindowAdapter>,
        _self_rc: &ItemRc,
    ) -> KeyEventResult {
        KeyEventResult::EventIgnored
    }

    fn focus_event(
        self: Pin<&Self>,
        _: &FocusEvent,
        _window_adapter: &Rc<dyn WindowAdapter>,
        _self_rc: &ItemRc,
    ) -> FocusEventResult {
        FocusEventResult::FocusIgnored
    }

    fn render(
        self: Pin<&Self>,
        backend: &mut &mut dyn ItemRenderer,
        self_rc: &ItemRc,
        size: LogicalSize,
    ) -> RenderingResult {
        (*backend).draw_text(self, self_rc, size, &self.cached_rendering_data);
        RenderingResult::ContinueRenderingChildren
    }

    fn bounding_rect(
        self: core::pin::Pin<&Self>,
        _window_adapter: &Rc<dyn WindowAdapter>,
        _self_rc: &ItemRc,
        geometry: LogicalRect,
    ) -> LogicalRect {
        geometry
    }

    fn clips_children(self: Pin<&Self>) -> bool {
        false
    }
}

impl ItemConsts for StyledTextItem {
    const cached_rendering_data_offset: const_field_offset::FieldOffset<
        StyledTextItem,
        CachedRenderingData,
    > = StyledTextItem::FIELD_OFFSETS.cached_rendering_data().as_unpinned_projection();
}

impl HasFont for StyledTextItem {
    fn font_request(self: Pin<&Self>, self_rc: &crate::items::ItemRc) -> FontRequest {
        crate::items::WindowItem::resolved_font_request(
            self_rc,
            self.default_font_family(),
            Default::default(),
            self.default_font_size(),
            Default::default(),
            Default::default(),
        )
    }
}

impl RenderString for StyledTextItem {
    fn text(self: Pin<&Self>) -> PlainOrStyledText {
        PlainOrStyledText::Styled(self.text())
    }

    fn max_lines(self: Pin<&Self>) -> i32 {
        Self::FIELD_OFFSETS.max_lines().apply_pin(self).get()
    }

    fn link_color(self: Pin<&Self>) -> Color {
        self.link_color()
    }
}

impl RenderText for StyledTextItem {
    fn target_size(self: Pin<&Self>) -> LogicalSize {
        LogicalSize::from_lengths(self.width(), self.height())
    }

    fn color(self: Pin<&Self>) -> Brush {
        self.default_color()
    }

    fn alignment(
        self: Pin<&Self>,
    ) -> (super::TextHorizontalAlignment, super::TextVerticalAlignment) {
        (self.horizontal_alignment(), self.vertical_alignment())
    }

    fn wrap(self: Pin<&Self>) -> TextWrap {
        TextWrap::WordWrap
    }

    fn overflow(self: Pin<&Self>) -> TextOverflow {
        TextOverflow::Clip
    }

    fn is_markdown(self: Pin<&Self>) -> bool {
        true
    }
}

impl StyledTextItem {
    pub fn font_metrics(
        self: Pin<&Self>,
        window_adapter: &Rc<dyn WindowAdapter>,
        self_rc: &ItemRc,
    ) -> FontMetrics {
        let font_request = self.font_request(self_rc);
        window_adapter.renderer().font_metrics(font_request)
    }
}

/// The implementation of the `Text` element
#[repr(C)]
#[derive(FieldOffsets, Default, SlintElement)]
#[pin]
pub struct SimpleText {
    pub width: Property<LogicalLength>,
    pub height: Property<LogicalLength>,
    pub text: Property<SharedString>,
    pub font_size: Property<LogicalLength>,
    pub font_weight: Property<i32>,
    pub color: Property<Brush>,
    pub horizontal_alignment: Property<TextHorizontalAlignment>,
    pub vertical_alignment: Property<TextVerticalAlignment>,
    pub max_lines: Property<i32>,

    pub cached_rendering_data: CachedRenderingData,
}

impl Item for SimpleText {
    fn init(self: Pin<&Self>, _self_rc: &ItemRc) {}

    fn deinit(self: Pin<&Self>, _window_adapter: &Rc<dyn WindowAdapter>) {}

    fn layout_info(
        self: Pin<&Self>,
        orientation: Orientation,
        cross_axis_constraint: Coord,
        window_adapter: &Rc<dyn WindowAdapter>,
        self_rc: &ItemRc,
    ) -> LayoutInfo {
        text_layout_info(
            self,
            self_rc,
            window_adapter,
            orientation,
            Self::FIELD_OFFSETS.width().apply_pin(self),
            cross_axis_constraint,
        )
    }

    fn input_event_filter_before_children(
        self: Pin<&Self>,
        _: &MouseEvent,
        _window_adapter: &Rc<dyn WindowAdapter>,
        _self_rc: &ItemRc,
        _: &mut super::MouseCursorInner,
    ) -> InputEventFilterResult {
        InputEventFilterResult::ForwardAndIgnore
    }

    fn input_event(
        self: Pin<&Self>,
        _: &MouseEvent,
        _window_adapter: &Rc<dyn WindowAdapter>,
        _self_rc: &ItemRc,
        _: &mut super::MouseCursorInner,
    ) -> InputEventResult {
        InputEventResult::EventIgnored
    }

    fn capture_key_event(
        self: Pin<&Self>,
        _: &InternalKeyEvent,
        _window_adapter: &Rc<dyn WindowAdapter>,
        _self_rc: &ItemRc,
    ) -> KeyEventResult {
        KeyEventResult::EventIgnored
    }

    fn key_event(
        self: Pin<&Self>,
        _: &InternalKeyEvent,
        _window_adapter: &Rc<dyn WindowAdapter>,
        _self_rc: &ItemRc,
    ) -> KeyEventResult {
        KeyEventResult::EventIgnored
    }

    fn focus_event(
        self: Pin<&Self>,
        _: &FocusEvent,
        _window_adapter: &Rc<dyn WindowAdapter>,
        _self_rc: &ItemRc,
    ) -> FocusEventResult {
        FocusEventResult::FocusIgnored
    }

    fn render(
        self: Pin<&Self>,
        backend: &mut &mut dyn ItemRenderer,
        self_rc: &ItemRc,
        size: LogicalSize,
    ) -> RenderingResult {
        (*backend).draw_text(self, self_rc, size, &self.cached_rendering_data);
        RenderingResult::ContinueRenderingChildren
    }

    fn bounding_rect(
        self: core::pin::Pin<&Self>,
        _window_adapter: &Rc<dyn WindowAdapter>,
        _self_rc: &ItemRc,
        geometry: LogicalRect,
    ) -> LogicalRect {
        geometry
    }

    fn clips_children(self: core::pin::Pin<&Self>) -> bool {
        false
    }
}

impl ItemConsts for SimpleText {
    const cached_rendering_data_offset: const_field_offset::FieldOffset<
        SimpleText,
        CachedRenderingData,
    > = SimpleText::FIELD_OFFSETS.cached_rendering_data().as_unpinned_projection();
}

impl HasFont for SimpleText {
    fn font_request(self: Pin<&Self>, self_rc: &crate::items::ItemRc) -> FontRequest {
        crate::items::WindowItem::resolved_font_request(
            self_rc,
            SharedString::default(),
            self.font_weight(),
            self.font_size(),
            LogicalLength::default(),
            false,
        )
    }
}

impl RenderString for SimpleText {
    fn text(self: Pin<&Self>) -> PlainOrStyledText {
        PlainOrStyledText::Plain(self.text())
    }

    fn max_lines(self: Pin<&Self>) -> i32 {
        Self::FIELD_OFFSETS.max_lines().apply_pin(self).get()
    }
}

impl RenderText for SimpleText {
    fn target_size(self: Pin<&Self>) -> LogicalSize {
        LogicalSize::from_lengths(self.width(), self.height())
    }

    fn color(self: Pin<&Self>) -> Brush {
        self.color()
    }

    fn alignment(
        self: Pin<&Self>,
    ) -> (super::TextHorizontalAlignment, super::TextVerticalAlignment) {
        (self.horizontal_alignment(), self.vertical_alignment())
    }

    fn wrap(self: Pin<&Self>) -> TextWrap {
        TextWrap::default()
    }

    fn overflow(self: Pin<&Self>) -> TextOverflow {
        TextOverflow::default()
    }

    fn is_markdown(self: Pin<&Self>) -> bool {
        false
    }
}

impl SimpleText {
    pub fn font_metrics(
        self: Pin<&Self>,
        window_adapter: &Rc<dyn WindowAdapter>,
        self_rc: &ItemRc,
    ) -> FontMetrics {
        window_adapter.renderer().font_metrics(self.font_request(self_rc))
    }
}

// The compiler's single-cell box layout lowering relies on text and image
// items keeping the default stretch of 0 in their layout info.
fn text_layout_info(
    text: Pin<&dyn RenderText>,
    self_rc: &ItemRc,
    window_adapter: &Rc<dyn WindowAdapter>,
    orientation: Orientation,
    width: Pin<&Property<LogicalLength>>,
    cross_axis_constraint: Coord,
) -> LayoutInfo {
    let implicit_size = |max_width, text_wrap| {
        window_adapter.renderer().text_size(text, self_rc, max_width, text_wrap)
    };

    // Stretch uses `round_layout` to explicitly align the top left and bottom right of layout nodes
    // to pixel boundaries. To avoid rounding down causing the minimum width to become so little that
    // letters will be cut off, apply the ceiling here.
    match orientation {
        Orientation::Horizontal => {
            // A word-wrapping text mustn't be squeezed below its longest word. One
            // content-widths measurement gives both that minimum and the single-line
            // preferred width, so this replaces the plain measurement below.
            let word_wrap_widths =
                matches!((text.overflow(), text.wrap()), (TextOverflow::Clip, TextWrap::WordWrap))
                    .then(|| window_adapter.renderer().text_content_widths(text, self_rc))
                    .flatten();

            let (min, preferred) = match word_wrap_widths {
                Some(widths) => (widths.min.get(), widths.max.get()),
                None => {
                    let unwrapped_width = implicit_size(None, TextWrap::NoWrap).width;
                    let min = match text.overflow() {
                        TextOverflow::Elide => unwrapped_width
                            .min(window_adapter.renderer().char_size(text, self_rc, '…').width),
                        TextOverflow::Clip => match text.wrap() {
                            TextWrap::NoWrap => unwrapped_width,
                            // char-wrap can break anywhere, so it keeps no lower bound.
                            TextWrap::WordWrap | TextWrap::CharWrap => 0 as Coord,
                        },
                    };
                    (min, unwrapped_width)
                }
            };
            LayoutInfo { min: min.ceil(), preferred: preferred.ceil(), ..LayoutInfo::default() }
        }
        Orientation::Vertical => {
            let h = match text.wrap() {
                TextWrap::NoWrap => implicit_size(None, TextWrap::NoWrap).height,
                wrap @ (TextWrap::WordWrap | TextWrap::CharWrap) => {
                    let w = if cross_axis_constraint >= 0 as Coord {
                        LogicalLength::new(cross_axis_constraint)
                    } else {
                        width.get()
                    };
                    implicit_size(Some(w), wrap).height
                }
            }
            .ceil();
            LayoutInfo { min: h, preferred: h, ..LayoutInfo::default() }
        }
    }
}

#[repr(C)]
#[derive(Default, Clone, Copy, PartialEq)]
/// Similar as `Option<core::ops::Range<i32>>` but `repr(C)`
///
/// This is the selection within a preedit
struct PreEditSelection {
    valid: bool,
    start: i32,
    end: i32,
}

impl From<Option<core::ops::Range<i32>>> for PreEditSelection {
    fn from(value: Option<core::ops::Range<i32>>) -> Self {
        value.map_or_else(Default::default, |r| Self { valid: true, start: r.start, end: r.end })
    }
}

impl PreEditSelection {
    fn as_option(self) -> Option<core::ops::Range<i32>> {
        self.valid.then_some(self.start..self.end)
    }
}

#[repr(C)]
#[derive(Clone)]
enum UndoItemKind {
    TextInsert,
    TextRemove,
}

#[repr(C)]
#[derive(Clone)]
struct UndoItem {
    pos: usize,
    text: SharedString,
    cursor: usize,
    anchor: usize,
    kind: UndoItemKind,
}

/// The implementation of the `TextInput` element
#[repr(C)]
#[derive(FieldOffsets, Default, SlintElement)]
#[pin]
pub struct TextInput {
    pub text: Property<SharedString>,
    pub font_family: Property<SharedString>,
    pub font_size: Property<LogicalLength>,
    pub font_weight: Property<i32>,
    pub font_italic: Property<bool>,
    pub color: Property<Brush>,
    pub selection_foreground_color: Property<Color>,
    pub selection_background_color: Property<Color>,
    pub horizontal_alignment: Property<TextHorizontalAlignment>,
    pub vertical_alignment: Property<TextVerticalAlignment>,
    pub wrap: Property<TextWrap>,
    pub input_type: Property<InputType>,
    pub input_method_hints: Property<InputMethodHints>,
    pub letter_spacing: Property<LogicalLength>,
    pub width: Property<LogicalLength>,
    pub height: Property<LogicalLength>,
    pub cursor_position_byte_offset: Property<i32>,
    pub anchor_position_byte_offset: Property<i32>,
    pub text_cursor_width: Property<LogicalLength>,
    pub page_height: Property<LogicalLength>,
    pub cursor_visible: Property<bool>,
    pub has_focus: Property<bool>,
    pub enabled: Property<bool>,
    pub accepted: Callback<VoidArg>,
    pub cursor_position_changed: Callback<PointArg>,
    pub edited: Callback<VoidArg>,
    pub key_pressed: Callback<KeyEventArg, EventResult>,
    pub key_released: Callback<KeyEventArg, EventResult>,
    pub single_line: Property<bool>,
    pub read_only: Property<bool>,
    pub preedit_text: Property<SharedString>,
    /// A selection within the preedit (cursor and anchor)
    preedit_selection: Property<PreEditSelection>,
    pub cached_rendering_data: CachedRenderingData,
    // The x position where the cursor wants to be.
    // It is not updated when moving up and down even when the line is shorter.
    preferred_x_pos: Cell<Coord>,
    /// 0 = not pressed, 1 = single press, 2 = double clicked+press , ...
    pressed: Cell<u8>,
    undo_items: Cell<SharedVector<UndoItem>>,
    redo_items: Cell<SharedVector<UndoItem>>,
    /// Mirror of `text` as of the last internal edit. Used by the change handler installed
    /// in `init` to tell internal edits apart from external assignments to the public `text`
    /// property, so that only the latter realign the cursor/anchor offsets and undo stack.
    internal_text: Cell<SharedString>,
    /// Runs `align_to_text` whenever `text` is assigned externally (see issues #331 and #9024).
    text_change_tracker: crate::properties::ChangeTracker,
}

impl Item for TextInput {
    fn init(self: Pin<&Self>, self_rc: &ItemRc) {
        // Seed the mirror so the change handler doesn't treat the initial text as external.
        self.internal_text.set(self.text());
        self.text_change_tracker.init_delayed(
            self_rc.downgrade(),
            |self_weak| {
                self_weak
                    .upgrade()
                    .and_then(|rc| rc.downcast::<TextInput>())
                    .map(|text_input| text_input.as_pin_ref().text())
                    .unwrap_or_default()
            },
            |self_weak, new_text| {
                let Some(self_rc) = self_weak.upgrade() else { return };
                let Some(text_input) = self_rc.downcast::<TextInput>() else { return };
                text_input.as_pin_ref().align_to_text(new_text, &self_rc);
            },
        );
    }

    fn deinit(self: Pin<&Self>, window_adapter: &Rc<dyn WindowAdapter>) {
        if self.has_focus() {
            let window_inner = crate::window::WindowInner::from_pub(window_adapter.window());
            window_inner.set_text_input_focused(false);
        }
    }

    fn layout_info(
        self: Pin<&Self>,
        orientation: Orientation,
        cross_axis_constraint: Coord,
        window_adapter: &Rc<dyn WindowAdapter>,
        self_rc: &ItemRc,
    ) -> LayoutInfo {
        let implicit_size = |max_width, text_wrap| {
            window_adapter.renderer().text_size(self, self_rc, max_width, text_wrap)
        };

        // Stretch uses `round_layout` to explicitly align the top left and bottom right of layout nodes
        // to pixel boundaries. To avoid rounding down causing the minimum width to become so little that
        // letters will be cut off, apply the ceiling here.
        match orientation {
            Orientation::Horizontal => {
                let implicit_size = implicit_size(None, TextWrap::NoWrap);
                let min = match self.wrap() {
                    TextWrap::NoWrap => implicit_size.width,
                    TextWrap::WordWrap | TextWrap::CharWrap => 0 as Coord,
                };
                LayoutInfo {
                    min: min.ceil(),
                    preferred: implicit_size.width.ceil(),
                    ..LayoutInfo::default()
                }
            }
            Orientation::Vertical => {
                let h = match self.wrap() {
                    TextWrap::NoWrap => implicit_size(None, TextWrap::NoWrap).height,
                    wrap @ (TextWrap::WordWrap | TextWrap::CharWrap) => {
                        let w = if cross_axis_constraint >= 0 as Coord {
                            LogicalLength::new(cross_axis_constraint)
                        } else {
                            self.width()
                        };
                        implicit_size(Some(w), wrap).height
                    }
                }
                .ceil();
                LayoutInfo { min: h, preferred: h, ..LayoutInfo::default() }
            }
        }
    }

    fn input_event_filter_before_children(
        self: Pin<&Self>,
        _: &MouseEvent,
        _window_adapter: &Rc<dyn WindowAdapter>,
        _self_rc: &ItemRc,
        _: &mut super::MouseCursorInner,
    ) -> InputEventFilterResult {
        InputEventFilterResult::ForwardEvent
    }

    fn input_event(
        self: Pin<&Self>,
        event: &MouseEvent,
        window_adapter: &Rc<dyn WindowAdapter>,
        self_rc: &ItemRc,
        cursor: &mut super::MouseCursorInner,
    ) -> InputEventResult {
        if !self.enabled() {
            return InputEventResult::EventIgnored;
        }

        *cursor = super::MouseCursorInner::BuiltIn(super::BuiltInMouseCursor::Text);

        match event {
            MouseEvent::Pressed {
                position, button: PointerEventButton::Left, click_count, ..
            } => {
                let clicked_offset =
                    self.byte_offset_for_position(*position, window_adapter, self_rc) as i32;
                self.as_ref().pressed.set((click_count % 3) + 1);

                if !window_adapter.window().0.context().0.modifiers.get().shift() {
                    self.as_ref().anchor_position_byte_offset.set(clicked_offset);
                }

                #[cfg(not(any(target_os = "android", target_os = "ios")))]
                self.ensure_focus_and_ime(window_adapter, self_rc);

                match click_count % 3 {
                    0 => self.set_cursor_position(
                        clicked_offset,
                        true,
                        TextChangeNotify::TriggerCallbacks,
                        window_adapter,
                        self_rc,
                    ),
                    1 => self.select_word(window_adapter, self_rc),
                    2 => self.select_paragraph(window_adapter, self_rc),
                    _ => unreachable!(),
                };

                return InputEventResult::GrabMouse;
            }
            MouseEvent::Pressed { button: PointerEventButton::Middle, .. } => {
                #[cfg(not(any(target_os = "android", target_os = "ios")))]
                self.ensure_focus_and_ime(window_adapter, self_rc);
            }
            MouseEvent::Released { button: PointerEventButton::Left, .. } => {
                self.as_ref().pressed.set(0);
                self.copy_clipboard(window_adapter, Clipboard::SelectionClipboard);
                #[cfg(any(target_os = "android", target_os = "ios"))]
                self.ensure_focus_and_ime(window_adapter, self_rc);
            }
            MouseEvent::Released { position, button: PointerEventButton::Middle, .. } => {
                let clicked_offset =
                    self.byte_offset_for_position(*position, window_adapter, self_rc) as i32;
                self.as_ref().anchor_position_byte_offset.set(clicked_offset);
                self.set_cursor_position(
                    clicked_offset,
                    true,
                    // We trigger the callbacks because paste_clipboard might not if there is no clipboard
                    TextChangeNotify::TriggerCallbacks,
                    window_adapter,
                    self_rc,
                );
                self.paste_clipboard(window_adapter, self_rc, Clipboard::SelectionClipboard);
            }
            MouseEvent::Exit => self.as_ref().pressed.set(0),
            MouseEvent::Moved { position, .. } => {
                let pressed = self.as_ref().pressed.get();
                if pressed > 0 {
                    let clicked_offset =
                        self.byte_offset_for_position(*position, window_adapter, self_rc) as i32;
                    self.set_cursor_position(
                        clicked_offset,
                        true,
                        if (pressed - 1).is_multiple_of(3) {
                            TextChangeNotify::TriggerCallbacks
                        } else {
                            TextChangeNotify::SkipCallbacks
                        },
                        window_adapter,
                        self_rc,
                    );
                    match (pressed - 1) % 3 {
                        0 => (),
                        1 => self.select_word(window_adapter, self_rc),
                        2 => self.select_paragraph(window_adapter, self_rc),
                        _ => unreachable!(),
                    }
                    return InputEventResult::GrabMouse;
                }
            }
            _ => return InputEventResult::EventIgnored,
        }
        InputEventResult::EventAccepted
    }

    fn capture_key_event(
        self: Pin<&Self>,
        _: &InternalKeyEvent,
        _window_adapter: &Rc<dyn WindowAdapter>,
        _self_rc: &ItemRc,
    ) -> KeyEventResult {
        KeyEventResult::EventIgnored
    }

    fn key_event(
        self: Pin<&Self>,
        event: &InternalKeyEvent,
        window_adapter: &Rc<dyn WindowAdapter>,
        self_rc: &ItemRc,
    ) -> KeyEventResult {
        if !self.enabled() {
            return KeyEventResult::EventIgnored;
        }
        match event.event_type {
            KeyEventType::KeyPressed => {
                // invoke first key_pressed callback to give the developer/designer the possibility to implement a custom behavior
                if Self::FIELD_OFFSETS
                    .key_pressed()
                    .apply_pin(self)
                    .call(&(event.key_event.clone(),))
                    == EventResult::Accept
                {
                    return KeyEventResult::EventAccepted;
                }

                let delete_direction = match event.text_shortcut() {
                    Some(TextShortcut::Move(direction)) => {
                        TextInput::move_cursor(
                            self,
                            direction,
                            event.key_event.modifiers.into(),
                            TextChangeNotify::TriggerCallbacks,
                            window_adapter,
                            self_rc,
                        );
                        return KeyEventResult::EventAccepted;
                    }
                    // Special case: backspace breaks the grapheme and selects the previous character
                    Some(TextShortcut::DeleteBackward) => {
                        Some(TextCursorDirection::PreviousCharacter)
                    }
                    Some(TextShortcut::DeleteForward) => Some(TextCursorDirection::Forward),
                    Some(TextShortcut::DeleteWordForward) => {
                        Some(TextCursorDirection::ForwardByWord)
                    }
                    Some(TextShortcut::DeleteWordBackward) => {
                        Some(TextCursorDirection::BackwardByWord)
                    }
                    Some(TextShortcut::DeleteToStartOfLine) => {
                        Some(TextCursorDirection::StartOfLine)
                    }
                    None => None,
                };
                if let Some(direction) = delete_direction {
                    if self.read_only() {
                        return KeyEventResult::EventIgnored;
                    }
                    TextInput::select_and_delete(self, direction, window_adapter, self_rc);
                    return KeyEventResult::EventAccepted;
                }

                if let Some(keycode) = event.key_event.text.chars().next()
                    && keycode == key_codes::Return
                    && !self.read_only()
                    && self.single_line()
                {
                    Self::FIELD_OFFSETS.accepted().apply_pin(self).call(&());
                    return KeyEventResult::EventAccepted;
                }

                // Only insert/interpreter non-control character strings
                if event.key_event.text.is_empty()
                    || event.key_event.text.as_str().chars().any(|ch| {
                        // exclude the private use area as we encode special keys into it
                        ('\u{f700}'..='\u{f7ff}').contains(&ch) || (ch.is_control() && ch != '\n')
                    })
                {
                    return KeyEventResult::EventIgnored;
                }

                if let Some(shortcut) = event.shortcut() {
                    match shortcut {
                        StandardShortcut::SelectAll => {
                            self.select_all(window_adapter, self_rc);
                            return KeyEventResult::EventAccepted;
                        }
                        StandardShortcut::Copy => {
                            self.copy(window_adapter, self_rc);
                            return KeyEventResult::EventAccepted;
                        }
                        StandardShortcut::Paste if !self.read_only() => {
                            self.paste(window_adapter, self_rc);
                            return KeyEventResult::EventAccepted;
                        }
                        StandardShortcut::Cut if !self.read_only() => {
                            self.cut(window_adapter, self_rc);
                            return KeyEventResult::EventAccepted;
                        }
                        StandardShortcut::Paste | StandardShortcut::Cut => {
                            return KeyEventResult::EventIgnored;
                        }
                        StandardShortcut::Undo if !self.read_only() => {
                            self.undo(window_adapter, self_rc);
                            return KeyEventResult::EventAccepted;
                        }
                        StandardShortcut::Redo if !self.read_only() => {
                            self.redo(window_adapter, self_rc);
                            return KeyEventResult::EventAccepted;
                        }
                        _ => (),
                    }
                }

                if self.read_only() || event.key_event.modifiers.control {
                    return KeyEventResult::EventIgnored;
                }

                // save real anchor/cursor for undo/redo
                let (real_cursor, real_anchor) = {
                    let text = self.text();
                    (self.cursor_position(&text), self.anchor_position(&text))
                };

                if !self.accept_text_input(event.key_event.text.as_str()) {
                    return KeyEventResult::EventIgnored;
                }

                self.delete_selection(window_adapter, self_rc, TextChangeNotify::SkipCallbacks);

                let mut text: String = self.text().into();

                // FIXME: respect grapheme boundaries
                let insert_pos = self.selection_anchor_and_cursor().1;
                text.insert_str(insert_pos, &event.key_event.text);

                self.add_undo_item(UndoItem {
                    pos: insert_pos,
                    text: event.key_event.text.clone(),
                    cursor: real_cursor,
                    anchor: real_anchor,
                    kind: UndoItemKind::TextInsert,
                });

                self.as_ref().set_text_internal(text.into());
                let new_cursor_pos = (insert_pos + event.key_event.text.len()) as i32;
                self.as_ref().anchor_position_byte_offset.set(new_cursor_pos);
                self.set_cursor_position(
                    new_cursor_pos,
                    true,
                    TextChangeNotify::TriggerCallbacks,
                    window_adapter,
                    self_rc,
                );

                // Keep the cursor visible when inserting text. Blinking should only occur when
                // nothing is entered or the cursor isn't moved.
                self.as_ref().show_cursor(window_adapter);

                Self::FIELD_OFFSETS.edited().apply_pin(self).call(&());

                KeyEventResult::EventAccepted
            }
            KeyEventType::KeyReleased => {
                match Self::FIELD_OFFSETS
                    .key_released()
                    .apply_pin(self)
                    .call(&(event.key_event.clone(),))
                {
                    EventResult::Accept => KeyEventResult::EventAccepted,
                    EventResult::Reject => KeyEventResult::EventIgnored,
                }
            }
            KeyEventType::UpdateComposition | KeyEventType::CommitComposition => {
                if !self.accept_text_input(&event.key_event.text) {
                    return KeyEventResult::EventIgnored;
                }

                let cursor = self.cursor_position(&self.text()) as i32;
                self.preedit_text.set(event.preedit_text.clone());
                self.preedit_selection.set(event.preedit_selection.clone().into());

                if let Some(r) = &event.replacement_range {
                    // Set the selection so the call to insert erases it
                    self.anchor_position_byte_offset.set(cursor.saturating_add(r.start));
                    self.cursor_position_byte_offset.set(cursor.saturating_add(r.end));
                    if event.key_event.text.is_empty() {
                        self.delete_selection(
                            window_adapter,
                            self_rc,
                            if event.cursor_position.is_none() {
                                TextChangeNotify::TriggerCallbacks
                            } else {
                                // will be updated by the set_cursor_position later
                                TextChangeNotify::SkipCallbacks
                            },
                        );
                    }
                }
                self.insert(&event.key_event.text, window_adapter, self_rc);
                if let Some(cursor) = event.cursor_position {
                    self.anchor_position_byte_offset.set(event.anchor_position.unwrap_or(cursor));
                    self.set_cursor_position(
                        cursor,
                        true,
                        TextChangeNotify::TriggerCallbacks,
                        window_adapter,
                        self_rc,
                    );
                }
                KeyEventResult::EventAccepted
            }
        }
    }

    fn focus_event(
        self: Pin<&Self>,
        event: &FocusEvent,
        window_adapter: &Rc<dyn WindowAdapter>,
        self_rc: &ItemRc,
    ) -> FocusEventResult {
        match event {
            FocusEvent::FocusIn(_reason) => {
                if !self.enabled() {
                    return FocusEventResult::FocusIgnored;
                }
                self.has_focus.set(true);
                self.show_cursor(window_adapter);
                WindowInner::from_pub(window_adapter.window()).set_text_input_focused(true);
                // FIXME: This should be tracked by a PropertyTracker in window and toggled when read_only() toggles.
                if !self.read_only() {
                    if let Some(w) = window_adapter.internal(crate::InternalToken) {
                        w.input_method_request(InputMethodRequest::Enable(
                            self.ime_properties(window_adapter, self_rc),
                        ));
                    }

                    if cfg!(not(target_vendor = "apple")) && *_reason == FocusReason::TabNavigation
                    {
                        self.select_all(window_adapter, self_rc);
                    }
                }
            }
            FocusEvent::FocusOut(reason) => {
                self.has_focus.set(false);
                self.hide_cursor();
                if !matches!(reason, FocusReason::WindowActivation | FocusReason::PopupActivation) {
                    self.as_ref()
                        .anchor_position_byte_offset
                        .set(self.as_ref().cursor_position_byte_offset());
                }
                WindowInner::from_pub(window_adapter.window()).set_text_input_focused(false);
                if !self.read_only() {
                    // commit the preedit text on android
                    #[cfg(target_os = "android")]
                    {
                        let preedit_text = self.preedit_text();
                        if !preedit_text.is_empty() {
                            let mut text = String::from(self.text());
                            let cursor_position = self.cursor_position(&text);
                            text.insert_str(cursor_position, &preedit_text);
                            self.set_text_internal(text.into());
                            let new_pos = (cursor_position + preedit_text.len()) as i32;
                            self.anchor_position_byte_offset.set(new_pos);
                            self.set_cursor_position(
                                new_pos,
                                false,
                                TextChangeNotify::TriggerCallbacks,
                                window_adapter,
                                self_rc,
                            );
                            Self::FIELD_OFFSETS.edited().apply_pin(self).call(&());
                        }
                    }
                    self.preedit_text.set(Default::default());
                }
            }
        }
        FocusEventResult::FocusAccepted
    }

    fn render(
        self: Pin<&Self>,
        backend: &mut &mut dyn ItemRenderer,
        self_rc: &ItemRc,
        size: LogicalSize,
    ) -> RenderingResult {
        crate::properties::evaluate_no_tracking(|| {
            if self.has_focus() && self.text() != *backend.window().last_ime_text.borrow() {
                let window_adapter = &backend.window().window_adapter();
                if let Some(w) = window_adapter.internal(crate::InternalToken) {
                    w.input_method_request(InputMethodRequest::Update(
                        self.ime_properties(window_adapter, self_rc),
                    ));
                }
            }
        });
        (*backend).draw_text_input(self, self_rc, size);
        RenderingResult::ContinueRenderingChildren
    }

    fn bounding_rect(
        self: core::pin::Pin<&Self>,
        _window_adapter: &Rc<dyn WindowAdapter>,
        _self_rc: &ItemRc,
        geometry: LogicalRect,
    ) -> LogicalRect {
        geometry
    }

    fn clips_children(self: core::pin::Pin<&Self>) -> bool {
        false
    }
}

impl ItemConsts for TextInput {
    const cached_rendering_data_offset: const_field_offset::FieldOffset<
        TextInput,
        CachedRenderingData,
    > = TextInput::FIELD_OFFSETS.cached_rendering_data().as_unpinned_projection();
}

impl HasFont for TextInput {
    fn font_request(self: Pin<&Self>, self_rc: &crate::items::ItemRc) -> FontRequest {
        crate::items::WindowItem::resolved_font_request(
            self_rc,
            self.font_family(),
            self.font_weight(),
            self.font_size(),
            self.letter_spacing(),
            self.font_italic(),
        )
    }
}

impl RenderString for TextInput {
    fn text(self: Pin<&Self>) -> PlainOrStyledText {
        // Deliberately not `visual_representation`, which would size the item off the cursor and
        // the selection too -- see `text_with_preedit`.
        let text = self.text_with_preedit().0;
        PlainOrStyledText::Plain(if self.is_password() { mask_password(&text) } else { text })
    }
}

pub enum TextCursorDirection {
    Forward,
    Backward,
    ForwardByWord,
    BackwardByWord,
    NextLine,
    PreviousLine,
    /// breaks grapheme boundaries, so only used by delete-previous-char
    PreviousCharacter,
    StartOfLine,
    EndOfLine,
    /// These don't care about wrapping
    StartOfParagraph,
    EndOfParagraph,
    StartOfText,
    EndOfText,
    PageUp,
    PageDown,
}

impl core::convert::TryFrom<char> for TextCursorDirection {
    type Error = ();

    fn try_from(value: char) -> Result<Self, Self::Error> {
        Ok(match value {
            key_codes::LeftArrow => Self::Backward,
            key_codes::RightArrow => Self::Forward,
            key_codes::UpArrow => Self::PreviousLine,
            key_codes::DownArrow => Self::NextLine,
            key_codes::PageUp => Self::PageUp,
            key_codes::PageDown => Self::PageDown,
            // On macOS and iOS this scrolls to the top or the bottom of the page
            #[cfg(not(target_vendor = "apple"))]
            key_codes::Home => Self::StartOfLine,
            #[cfg(not(target_vendor = "apple"))]
            key_codes::End => Self::EndOfLine,
            _ => return Err(()),
        })
    }
}

#[derive(PartialEq)]
enum AnchorMode {
    KeepAnchor,
    MoveAnchor,
}

impl From<KeyboardModifiers> for AnchorMode {
    fn from(modifiers: KeyboardModifiers) -> Self {
        if modifiers.shift { Self::KeepAnchor } else { Self::MoveAnchor }
    }
}

/// Argument to [`TextInput::delete_selection`] that determines whether to trigger the
/// `edited` and cursor position callbacks and issue an input method request update.
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum TextChangeNotify {
    /// Trigger the callbacks.
    TriggerCallbacks,
    /// Skip triggering the callbacks, as a subsequent operation will trigger them.
    SkipCallbacks,
}

fn safe_byte_offset(unsafe_byte_offset: i32, text: &str) -> usize {
    if unsafe_byte_offset <= 0 {
        return 0;
    }
    text.ceil_char_boundary(unsafe_byte_offset as usize)
}

/// Like [`safe_byte_offset`], but additionally snaps the result up to a grapheme cluster
/// boundary. Used when realigning the cursor/anchor to text that was replaced externally, so
/// they never land inside a multi-codepoint grapheme (e.g. an emoji with a skin-tone modifier
/// or a base character followed by a combining mark), matching how cursor movement treats a
/// grapheme cluster as indivisible.
fn safe_grapheme_boundary_offset(unsafe_byte_offset: i32, text: &str) -> usize {
    let offset = safe_byte_offset(unsafe_byte_offset, text);
    let mut grapheme_cursor = unicode_segmentation::GraphemeCursor::new(offset, text.len(), true);
    match grapheme_cursor.is_boundary(text, 0) {
        Ok(true) => offset,
        _ => grapheme_cursor.next_boundary(text, 0).ok().flatten().unwrap_or(text.len()),
    }
}

/// This struct holds the fields needed for rendering a TextInput item after applying any
/// on-going composition. This way the renderer's don't have to duplicate the code for extracting
/// and applying the pre-edit text, cursor placement within, etc.
#[derive(Debug)]
pub struct TextInputVisualRepresentation {
    /// The text to be rendered including any pre-edit string
    pub text: SharedString,
    /// If set, this field specifies the range as byte offsets within the text field where the composition
    /// is in progress. Renderers typically provide visual feedback for the currently composed text, such as
    /// by using underlines.
    pub preedit_range: core::ops::Range<usize>,
    /// If set, specifies the range as byte offsets within the text where to draw the selection.
    pub selection_range: core::ops::Range<usize>,
    /// The position where to draw the cursor, as byte offset within the text.
    pub cursor_position: Option<usize>,
    /// The color of the (unselected) text
    pub text_color: Brush,
    /// The color of the blinking cursor
    pub cursor_color: Color,
    text_without_password: Option<SharedString>,
}

/// What the characters of a password field are displayed as. The same everywhere, so that
/// measuring, hit-testing and drawing agree on the shaped text and can share it.
pub(crate) const PASSWORD_CHARACTER: char = '\u{25cf}';

/// Replaces every character of `text` with [`PASSWORD_CHARACTER`].
pub(crate) fn mask_password(text: &str) -> SharedString {
    core::iter::repeat_n(PASSWORD_CHARACTER, text.chars().count()).collect()
}

impl TextInputVisualRepresentation {
    /// If the given `TextInput` renders a password, then all characters in this `TextInputVisualRepresentation` are replaced
    /// with [`PASSWORD_CHARACTER`] and the selection/preedit-ranges/cursor position are adjusted.
    fn apply_password_character_substitution(&mut self, text_input: Pin<&TextInput>) {
        if !text_input.is_password() {
            return;
        }

        let text = &mut self.text;
        let fixup_range = |r: &mut core::ops::Range<usize>| {
            if !core::ops::Range::is_empty(r) {
                r.start = text[..r.start].chars().count() * PASSWORD_CHARACTER.len_utf8();
                r.end = text[..r.end].chars().count() * PASSWORD_CHARACTER.len_utf8();
            }
        };
        fixup_range(&mut self.preedit_range);
        fixup_range(&mut self.selection_range);
        if let Some(cursor_pos) = self.cursor_position.as_mut() {
            *cursor_pos = text[..*cursor_pos].chars().count() * PASSWORD_CHARACTER.len_utf8();
        }
        self.text_without_password = Some(core::mem::replace(text, mask_password(text)));
    }

    /// Use this function to make a byte offset in the visual text (used for rendering) back to a byte offset in the
    /// TextInput's text. The offsets might differ for example for password text input fields.
    pub fn map_byte_offset_from_visual_text_to_actual_text(&self, byte_offset: usize) -> usize {
        if let Some(text_without_password) = self.text_without_password.as_ref() {
            text_without_password
                .char_indices()
                .nth(byte_offset / PASSWORD_CHARACTER.len_utf8())
                .map_or(text_without_password.len(), |(r, _)| r)
        } else {
            byte_offset
        }
    }

    /// Map the byte_offset inside the TextInput's text to the byte offset in the visual text.
    /// This is the opposite of `map_byte_offset_from_byte_offset_in_visual_text`.
    pub fn map_byte_offset_from_actual_to_visual_text(&self, byte_offset: usize) -> usize {
        if let Some(text_without_password) = self.text_without_password.as_ref() {
            text_without_password[..byte_offset].chars().count() * PASSWORD_CHARACTER.len_utf8()
        } else {
            byte_offset
        }
    }
}

/// Whether the text cursor is drawn in the selection color (Apple and Android platforms) rather
/// than in the text color.
fn cursor_uses_selection_color() -> bool {
    matches!(
        crate::detect_operating_system(),
        crate::items::OperatingSystemType::Android
            | crate::items::OperatingSystemType::Ios
            | crate::items::OperatingSystemType::Macos
    )
}

impl TextInput {
    fn show_cursor(&self, window_adapter: &Rc<dyn WindowAdapter>) {
        WindowInner::from_pub(window_adapter.window())
            .set_cursor_blink_binding(&self.cursor_visible);
    }

    fn hide_cursor(&self) {
        self.cursor_visible.set(false);
    }

    /// Moves the cursor (and/or anchor) and returns true if the cursor position changed; false otherwise.
    fn move_cursor(
        self: Pin<&Self>,
        direction: TextCursorDirection,
        anchor_mode: AnchorMode,
        trigger_callbacks: TextChangeNotify,
        window_adapter: &Rc<dyn WindowAdapter>,
        self_rc: &ItemRc,
    ) -> bool {
        let text = self.text();
        if text.is_empty() {
            return false;
        }

        let (anchor, cursor) = self.selection_anchor_and_cursor();
        let last_cursor_pos = self.cursor_position(&text);

        let mut grapheme_cursor =
            unicode_segmentation::GraphemeCursor::new(last_cursor_pos, text.len(), true);

        let font_height = window_adapter.renderer().char_size(self, self_rc, ' ').height;

        let mut reset_preferred_x_pos = true;

        let new_cursor_pos = match direction {
            TextCursorDirection::Forward => {
                if anchor == cursor || anchor_mode == AnchorMode::KeepAnchor {
                    grapheme_cursor
                        .next_boundary(&text, 0)
                        .ok()
                        .flatten()
                        .unwrap_or_else(|| text.len())
                } else {
                    cursor
                }
            }
            TextCursorDirection::Backward => {
                if anchor == cursor || anchor_mode == AnchorMode::KeepAnchor {
                    grapheme_cursor.prev_boundary(&text, 0).ok().flatten().unwrap_or(0)
                } else {
                    anchor
                }
            }
            TextCursorDirection::NextLine => {
                reset_preferred_x_pos = false;

                let cursor_rect =
                    self.cursor_rect_for_byte_offset(last_cursor_pos, window_adapter, self_rc);
                let mut cursor_xy_pos = cursor_rect.center();

                cursor_xy_pos.y += font_height;
                cursor_xy_pos.x = self.preferred_x_pos.get();
                self.byte_offset_for_position(cursor_xy_pos, window_adapter, self_rc)
            }
            TextCursorDirection::PreviousLine => {
                reset_preferred_x_pos = false;

                let cursor_rect =
                    self.cursor_rect_for_byte_offset(last_cursor_pos, window_adapter, self_rc);
                let mut cursor_xy_pos = cursor_rect.center();

                cursor_xy_pos.y -= font_height;
                cursor_xy_pos.x = self.preferred_x_pos.get();
                self.byte_offset_for_position(cursor_xy_pos, window_adapter, self_rc)
            }
            TextCursorDirection::PreviousCharacter => {
                let mut i = last_cursor_pos;
                loop {
                    i = i.saturating_sub(1);
                    if text.is_char_boundary(i) {
                        break i;
                    }
                }
            }
            // Currently moving by word behaves like macos: next end of word(forward) or previous beginning of word(backward)
            TextCursorDirection::ForwardByWord => next_word_boundary(&text, last_cursor_pos + 1),
            TextCursorDirection::BackwardByWord => {
                prev_word_boundary(&text, last_cursor_pos.saturating_sub(1))
            }
            TextCursorDirection::StartOfLine => {
                let cursor_rect =
                    self.cursor_rect_for_byte_offset(last_cursor_pos, window_adapter, self_rc);
                let mut cursor_xy_pos = cursor_rect.center();

                cursor_xy_pos.x = 0 as Coord;
                self.byte_offset_for_position(cursor_xy_pos, window_adapter, self_rc)
            }
            TextCursorDirection::EndOfLine => {
                let cursor_rect =
                    self.cursor_rect_for_byte_offset(last_cursor_pos, window_adapter, self_rc);
                let mut cursor_xy_pos = cursor_rect.center();

                cursor_xy_pos.x = Coord::MAX;
                self.byte_offset_for_position(cursor_xy_pos, window_adapter, self_rc)
            }
            TextCursorDirection::StartOfParagraph => {
                prev_paragraph_boundary(&text, last_cursor_pos.saturating_sub(1))
            }
            TextCursorDirection::EndOfParagraph => {
                next_paragraph_boundary(&text, last_cursor_pos + 1)
            }
            TextCursorDirection::StartOfText => 0,
            TextCursorDirection::EndOfText => text.len(),
            TextCursorDirection::PageUp => {
                let offset = self.page_height().get() - font_height;
                if offset <= 0 as Coord {
                    return false;
                }
                reset_preferred_x_pos = false;
                let cursor_rect =
                    self.cursor_rect_for_byte_offset(last_cursor_pos, window_adapter, self_rc);
                let mut cursor_xy_pos = cursor_rect.center();
                cursor_xy_pos.y -= offset;
                cursor_xy_pos.x = self.preferred_x_pos.get();
                self.byte_offset_for_position(cursor_xy_pos, window_adapter, self_rc)
            }
            TextCursorDirection::PageDown => {
                let offset = self.page_height().get() - font_height;
                if offset <= 0 as Coord {
                    return false;
                }
                reset_preferred_x_pos = false;
                let cursor_rect =
                    self.cursor_rect_for_byte_offset(last_cursor_pos, window_adapter, self_rc);
                let mut cursor_xy_pos = cursor_rect.center();
                cursor_xy_pos.y += offset;
                cursor_xy_pos.x = self.preferred_x_pos.get();
                self.byte_offset_for_position(cursor_xy_pos, window_adapter, self_rc)
            }
        };

        match anchor_mode {
            AnchorMode::KeepAnchor => {}
            AnchorMode::MoveAnchor => {
                self.as_ref().anchor_position_byte_offset.set(new_cursor_pos as i32);
            }
        }
        self.set_cursor_position(
            new_cursor_pos as i32,
            reset_preferred_x_pos,
            trigger_callbacks,
            window_adapter,
            self_rc,
        );

        // Keep the cursor visible when moving. Blinking should only occur when
        // nothing is entered or the cursor isn't moved.
        self.as_ref().show_cursor(window_adapter);

        new_cursor_pos != last_cursor_pos
    }

    /// Set `text` from an internal edit, keeping the `internal_text` mirror in sync so the
    /// change handler doesn't mistake this edit for an external assignment.
    fn set_text_internal(self: Pin<&Self>, text: SharedString) {
        self.internal_text.set(text.clone());
        self.text.set(text);
    }

    /// Called by the change handler when the public `text` property changes. If the new text
    /// differs from our `internal_text` mirror, the change came from outside (the application
    /// assigned `text` directly), so realign all derived state to the new text: clamp the
    /// cursor and anchor offsets to valid boundaries (issue #331) and clear the undo/redo
    /// stacks, whose positions refer to the now-replaced text (issue #9024).
    fn align_to_text(self: Pin<&Self>, new_text: &SharedString, self_rc: &ItemRc) {
        let previous_text = self.internal_text.replace(new_text.clone());
        if previous_text == *new_text {
            // Produced by an internal edit: `set_text_internal` already kept the mirror in sync,
            // so the offsets and undo stack are consistent with `new_text`. Nothing to realign.
            return;
        }

        self.undo_items.set(Default::default());
        self.redo_items.set(Default::default());

        let old_cursor = self.cursor_position_byte_offset();
        let clamped_cursor = safe_grapheme_boundary_offset(old_cursor, new_text) as i32;
        let clamped_anchor =
            safe_grapheme_boundary_offset(self.anchor_position_byte_offset(), new_text) as i32;
        self.anchor_position_byte_offset.set(clamped_anchor);

        if let Some(window_adapter) = self_rc.window_adapter() {
            self.set_cursor_position(
                clamped_cursor,
                true,
                TextChangeNotify::TriggerCallbacks,
                &window_adapter,
                self_rc,
            );
        } else {
            self.cursor_position_byte_offset.set(clamped_cursor);
        }
    }

    pub fn set_cursor_position(
        self: Pin<&Self>,
        new_position: i32,
        reset_preferred_x_pos: bool,
        trigger_callbacks: TextChangeNotify,
        window_adapter: &Rc<dyn WindowAdapter>,
        self_rc: &ItemRc,
    ) {
        self.cursor_position_byte_offset.set(new_position);
        if new_position >= 0 {
            let pos = self
                .cursor_rect_for_byte_offset(new_position as usize, window_adapter, self_rc)
                .origin;
            if reset_preferred_x_pos {
                self.preferred_x_pos.set(pos.x);
            }
            if trigger_callbacks == TextChangeNotify::TriggerCallbacks {
                Self::FIELD_OFFSETS
                    .cursor_position_changed()
                    .apply_pin(self)
                    .call(&(crate::api::LogicalPosition::from_euclid(pos),));
                self.update_ime(window_adapter, self_rc);
            }
        }
    }

    fn update_ime(self: Pin<&Self>, window_adapter: &Rc<dyn WindowAdapter>, self_rc: &ItemRc) {
        if self.read_only() || !self.has_focus() {
            return;
        }
        if let Some(w) = window_adapter.internal(crate::InternalToken) {
            w.input_method_request(InputMethodRequest::Update(
                self.ime_properties(window_adapter, self_rc),
            ));
        }
    }

    fn select_and_delete(
        self: Pin<&Self>,
        step: TextCursorDirection,
        window_adapter: &Rc<dyn WindowAdapter>,
        self_rc: &ItemRc,
    ) {
        if !self.has_selection() {
            self.move_cursor(
                step,
                AnchorMode::KeepAnchor,
                TextChangeNotify::SkipCallbacks,
                window_adapter,
                self_rc,
            );
        }
        self.delete_selection(window_adapter, self_rc, TextChangeNotify::TriggerCallbacks);
    }

    pub fn delete_selection(
        self: Pin<&Self>,
        window_adapter: &Rc<dyn WindowAdapter>,
        self_rc: &ItemRc,
        trigger_callbacks: TextChangeNotify,
    ) {
        let text: String = self.text().into();
        if text.is_empty() {
            return;
        }

        let (anchor, cursor) = self.selection_anchor_and_cursor();
        if anchor == cursor {
            return;
        }

        let removed_text: SharedString = text[anchor..cursor].into();
        // save real anchor/cursor for undo/redo
        let (real_cursor, real_anchor) = {
            let text = self.text();
            (self.cursor_position(&text), self.anchor_position(&text))
        };

        let text = [text.split_at(anchor).0, text.split_at(cursor).1].concat();
        self.set_text_internal(text.into());
        self.anchor_position_byte_offset.set(anchor as i32);

        self.add_undo_item(UndoItem {
            pos: anchor,
            text: removed_text,
            cursor: real_cursor,
            anchor: real_anchor,
            kind: UndoItemKind::TextRemove,
        });

        if trigger_callbacks == TextChangeNotify::TriggerCallbacks {
            self.set_cursor_position(
                anchor as i32,
                true,
                trigger_callbacks,
                window_adapter,
                self_rc,
            );
            Self::FIELD_OFFSETS.edited().apply_pin(self).call(&());
        } else {
            self.cursor_position_byte_offset.set(anchor as i32);
        }
    }

    pub fn anchor_position(self: Pin<&Self>, text: &str) -> usize {
        safe_byte_offset(self.anchor_position_byte_offset(), text)
    }

    /// Whether this input masks what is typed into it.
    pub fn is_password(self: Pin<&Self>) -> bool {
        matches!(self.input_type(), InputType::Password)
    }

    pub fn cursor_position(self: Pin<&Self>, text: &str) -> usize {
        safe_byte_offset(self.cursor_position_byte_offset(), text)
    }

    fn ime_properties(
        self: Pin<&Self>,
        window_adapter: &Rc<dyn WindowAdapter>,
        self_rc: &ItemRc,
    ) -> InputMethodProperties {
        let text = self.text();
        WindowInner::from_pub(window_adapter.window()).last_ime_text.replace(text.clone());
        let cursor_position = self.cursor_position(&text);
        let anchor_position = self.anchor_position(&text);
        let cursor_relative =
            self.cursor_rect_for_byte_offset(cursor_position, window_adapter, self_rc);
        let geometry = self_rc.geometry();
        let origin = self_rc.map_to_native_window(geometry.origin);
        let origin_vector = origin.to_vector();
        let cursor_rect_origin =
            crate::api::LogicalPosition::from_euclid(cursor_relative.origin + origin_vector);
        let cursor_rect_size = crate::api::LogicalSize::from_euclid(cursor_relative.size);
        let anchor_point = crate::api::LogicalPosition::from_euclid(
            self.cursor_rect_for_byte_offset(anchor_position, window_adapter, self_rc).origin
                + origin_vector
                + cursor_relative.size,
        );
        let maybe_parent =
            self_rc.parent_item(crate::item_tree::ParentItemTraversalMode::StopAtPopups);
        let clip_rect = maybe_parent.map(|parent| {
            let geom = parent.geometry();
            LogicalRect::new(parent.map_to_native_window(geom.origin), geom.size)
        });

        InputMethodProperties {
            text,
            cursor_position,
            anchor_position: (cursor_position != anchor_position).then_some(anchor_position),
            preedit_text: self.preedit_text(),
            preedit_offset: cursor_position,
            cursor_rect_origin,
            cursor_rect_size,
            anchor_point,
            input_type: self.input_type(),
            input_method_hints: self.input_method_hints(),
            clip_rect,
        }
    }

    // Avoid accessing self.cursor_position()/self.anchor_position() directly, always
    // use this bounds-checking function.
    pub fn selection_anchor_and_cursor(self: Pin<&Self>) -> (usize, usize) {
        let text = self.text();
        let cursor_pos = self.cursor_position(&text);
        let anchor_pos = self.anchor_position(&text);

        if anchor_pos > cursor_pos {
            (cursor_pos as _, anchor_pos as _)
        } else {
            (anchor_pos as _, cursor_pos as _)
        }
    }

    pub fn has_selection(self: Pin<&Self>) -> bool {
        let (anchor_pos, cursor_pos) = self.selection_anchor_and_cursor();
        anchor_pos != cursor_pos
    }

    fn insert(
        self: Pin<&Self>,
        text_to_insert: &str,
        window_adapter: &Rc<dyn WindowAdapter>,
        self_rc: &ItemRc,
    ) {
        if text_to_insert.is_empty() {
            return;
        }

        let (real_cursor, real_anchor) = {
            let text = self.text();
            (self.cursor_position(&text), self.anchor_position(&text))
        };

        self.delete_selection(window_adapter, self_rc, TextChangeNotify::SkipCallbacks);
        let mut text: String = self.text().into();
        let cursor_pos = self.selection_anchor_and_cursor().1;
        let mut inserted_text: SharedString = text_to_insert.into();
        if text_to_insert.contains('\n') && self.single_line() {
            inserted_text = text_to_insert.replace('\n', " ").into();
            text.insert_str(cursor_pos, &inserted_text);
        } else {
            text.insert_str(cursor_pos, text_to_insert);
        }

        self.add_undo_item(UndoItem {
            pos: cursor_pos,
            text: inserted_text,
            cursor: real_cursor,
            anchor: real_anchor,
            kind: UndoItemKind::TextInsert,
        });

        let cursor_pos = cursor_pos + text_to_insert.len();
        self.set_text_internal(text.into());
        self.anchor_position_byte_offset.set(cursor_pos as i32);
        self.set_cursor_position(
            cursor_pos as i32,
            true,
            TextChangeNotify::TriggerCallbacks,
            window_adapter,
            self_rc,
        );
        Self::FIELD_OFFSETS.edited().apply_pin(self).call(&());
    }

    pub fn cut(self: Pin<&Self>, window_adapter: &Rc<dyn WindowAdapter>, self_rc: &ItemRc) {
        self.copy(window_adapter, self_rc);
        self.delete_selection(window_adapter, self_rc, TextChangeNotify::TriggerCallbacks);
    }

    pub fn set_selection_offsets(
        self: Pin<&Self>,
        window_adapter: &Rc<dyn WindowAdapter>,
        self_rc: &ItemRc,
        start: i32,
        end: i32,
    ) {
        let text = self.text();
        let safe_start = safe_byte_offset(start, &text);
        let safe_end = safe_byte_offset(end, &text);

        self.as_ref().anchor_position_byte_offset.set(safe_start as i32);
        self.set_cursor_position(
            safe_end as i32,
            true,
            TextChangeNotify::TriggerCallbacks,
            window_adapter,
            self_rc,
        );
    }

    pub fn select_all(self: Pin<&Self>, window_adapter: &Rc<dyn WindowAdapter>, self_rc: &ItemRc) {
        self.move_cursor(
            TextCursorDirection::StartOfText,
            AnchorMode::MoveAnchor,
            TextChangeNotify::SkipCallbacks,
            window_adapter,
            self_rc,
        );
        self.move_cursor(
            TextCursorDirection::EndOfText,
            AnchorMode::KeepAnchor,
            TextChangeNotify::TriggerCallbacks,
            window_adapter,
            self_rc,
        );
    }

    pub fn clear_selection(self: Pin<&Self>, _: &Rc<dyn WindowAdapter>, _: &ItemRc) {
        self.as_ref().anchor_position_byte_offset.set(self.as_ref().cursor_position_byte_offset());
    }

    pub fn select_word(self: Pin<&Self>, window_adapter: &Rc<dyn WindowAdapter>, self_rc: &ItemRc) {
        let text = self.text();
        let anchor = self.anchor_position(&text);
        let cursor = self.cursor_position(&text);
        let (new_a, new_c) = if anchor <= cursor {
            (prev_word_boundary(&text, anchor), next_word_boundary(&text, cursor))
        } else {
            (next_word_boundary(&text, anchor), prev_word_boundary(&text, cursor))
        };
        self.as_ref().anchor_position_byte_offset.set(new_a as i32);
        self.set_cursor_position(
            new_c as i32,
            true,
            TextChangeNotify::TriggerCallbacks,
            window_adapter,
            self_rc,
        );
    }

    fn select_paragraph(
        self: Pin<&Self>,
        window_adapter: &Rc<dyn WindowAdapter>,
        self_rc: &ItemRc,
    ) {
        let text = self.text();
        let anchor = self.anchor_position(&text);
        let cursor = self.cursor_position(&text);
        let (new_a, new_c) = if anchor <= cursor {
            (prev_paragraph_boundary(&text, anchor), next_paragraph_boundary(&text, cursor))
        } else {
            (next_paragraph_boundary(&text, anchor), prev_paragraph_boundary(&text, cursor))
        };
        self.as_ref().anchor_position_byte_offset.set(new_a as i32);
        self.set_cursor_position(
            new_c as i32,
            true,
            TextChangeNotify::TriggerCallbacks,
            window_adapter,
            self_rc,
        );
    }

    pub fn copy(self: Pin<&Self>, w: &Rc<dyn WindowAdapter>, _: &ItemRc) {
        self.copy_clipboard(w, Clipboard::DefaultClipboard);
    }

    fn copy_clipboard(
        self: Pin<&Self>,
        window_adapter: &Rc<dyn WindowAdapter>,
        clipboard: Clipboard,
    ) {
        let (anchor, cursor) = self.selection_anchor_and_cursor();
        if anchor == cursor {
            return;
        }
        let text = self.text();

        WindowInner::from_pub(window_adapter.window())
            .context()
            .platform()
            .set_clipboard_text(&text[anchor..cursor], clipboard);
    }

    pub fn paste(self: Pin<&Self>, window_adapter: &Rc<dyn WindowAdapter>, self_rc: &ItemRc) {
        self.paste_clipboard(window_adapter, self_rc, Clipboard::DefaultClipboard);
    }

    fn paste_clipboard(
        self: Pin<&Self>,
        window_adapter: &Rc<dyn WindowAdapter>,
        self_rc: &ItemRc,
        clipboard: Clipboard,
    ) {
        if let Some(text) = WindowInner::from_pub(window_adapter.window())
            .context()
            .platform()
            .clipboard_text(clipboard)
        {
            self.preedit_text.set(Default::default());
            self.insert(&text, window_adapter, self_rc);
        }
    }

    /// Returns the `text` property with the IME composition (preedit) inserted at the cursor, and
    /// the byte range that composition occupies within the returned string. The range is empty
    /// when no composition is in progress, in which case the text is returned unchanged.
    ///
    /// Password fields are *not* masked here; callers that render or measure the text apply the
    /// substitution themselves, because the masking character is renderer-specific.
    ///
    /// Deliberately reads less than [`Self::visual_representation`]: neither the cursor visibility
    /// nor the selection nor the colors, so that callers which only need the string -- sizing above
    /// all -- don't end up making the layout depend on the blinking cursor.
    pub(crate) fn text_with_preedit(self: Pin<&Self>) -> (SharedString, core::ops::Range<usize>) {
        let text = self.text();
        let preedit_text = self.preedit_text();
        if preedit_text.is_empty() {
            return (text, Default::default());
        }
        let cursor_position = self.cursor_position(&text);
        (
            [&text[..cursor_position], &preedit_text, &text[cursor_position..]].concat().into(),
            cursor_position..cursor_position + preedit_text.len(),
        )
    }

    /// Returns a [`TextInputVisualRepresentation`] struct that contains all the fields necessary for rendering the text input,
    /// after making adjustments such as applying a substitution of characters for password input fields, or making sure
    /// that the selection start is always less or equal than the selection end.
    pub fn visual_representation(self: Pin<&Self>) -> TextInputVisualRepresentation {
        let (text, composition) = self.text_with_preedit();

        let (preedit_range, selection_range, cursor_position) = if !composition.is_empty() {
            // Where the composition was inserted, i.e. the cursor within the pre-composition text.
            let cursor_position = composition.start;

            if let Some(preedit_sel) = self.preedit_selection().as_option() {
                let preedit_selection = cursor_position + preedit_sel.start as usize
                    ..cursor_position + preedit_sel.end as usize;
                (composition, preedit_selection, Some(cursor_position + preedit_sel.end as usize))
            } else {
                let cur = composition.end;
                (composition, cur..cur, None)
            }
        } else {
            let (selection_anchor_pos, selection_cursor_pos) = self.selection_anchor_and_cursor();
            let selection_range = selection_anchor_pos..selection_cursor_pos;
            let cursor_position = self.cursor_position(&text);
            let cursor_visible = self.cursor_visible() && self.enabled();
            let cursor_position = if cursor_visible && selection_range.is_empty() {
                Some(cursor_position)
            } else {
                None
            };
            (composition, selection_range, cursor_position)
        };

        let text_color = self.color();

        let cursor_color = if cursor_uses_selection_color() {
            if cursor_position.is_some() {
                self.selection_background_color().with_alpha(1.)
            } else {
                Default::default()
            }
        } else {
            // Other platforms draw the cursor in the text color.
            text_color.color()
        };

        let mut repr = TextInputVisualRepresentation {
            text,
            preedit_range,
            selection_range,
            cursor_position,
            text_without_password: None,
            text_color,
            cursor_color,
        };
        repr.apply_password_character_substitution(self);
        repr
    }

    fn cursor_rect_for_byte_offset(
        self: Pin<&Self>,
        byte_offset: usize,
        window_adapter: &Rc<dyn WindowAdapter>,
        self_rc: &ItemRc,
    ) -> LogicalRect {
        window_adapter.renderer().text_input_cursor_rect_for_byte_offset(self, self_rc, byte_offset)
    }

    pub fn byte_offset_for_position(
        self: Pin<&Self>,
        pos: LogicalPoint,
        window_adapter: &Rc<dyn WindowAdapter>,
        self_rc: &ItemRc,
    ) -> usize {
        window_adapter.renderer().text_input_byte_offset_for_position(self, self_rc, pos)
    }

    /// When pressing the mouse (or releasing the finger, on android) we should take the focus if we don't have it already.
    /// Setting the focus will show the virtual keyboard, otherwise we should make sure that the keyboard is shown if it was hidden by the user
    fn ensure_focus_and_ime(
        self: Pin<&Self>,
        window_adapter: &Rc<dyn WindowAdapter>,
        self_rc: &ItemRc,
    ) {
        if !self.has_focus() {
            WindowInner::from_pub(window_adapter.window()).set_focus_item(
                self_rc,
                true,
                FocusReason::PointerClick,
            );
        } else if !self.read_only()
            && let Some(w) = window_adapter.internal(crate::InternalToken)
        {
            w.input_method_request(InputMethodRequest::Enable(
                self.ime_properties(window_adapter, self_rc),
            ));
        }
    }

    fn add_undo_item(self: Pin<&Self>, item: UndoItem) {
        let mut items = self.undo_items.take();
        // try to merge with the last item
        if let Some(last) = items.make_mut_slice().last_mut() {
            match (&item.kind, &last.kind) {
                (UndoItemKind::TextInsert, UndoItemKind::TextInsert) => {
                    let is_new_line = item.text == "\n";
                    let last_is_new_line = last.text == "\n";
                    // if the last item or current item is a new_line
                    // we insert it as a standalone item, no merging
                    if item.pos == last.pos + last.text.len() && !is_new_line && !last_is_new_line {
                        last.text += &item.text;
                    } else {
                        items.push(item);
                    }
                }
                (UndoItemKind::TextRemove, UndoItemKind::TextRemove)
                    if item.pos + item.text.len() == last.pos =>
                {
                    last.pos = item.pos;
                    let old_text = last.text.clone();
                    last.text = item.text;
                    last.text += &old_text;
                    // prepend
                }
                _ => {
                    items.push(item);
                }
            }
        } else {
            items.push(item);
        }

        self.undo_items.set(items);
    }

    pub fn undo(self: Pin<&Self>, window_adapter: &Rc<dyn WindowAdapter>, self_rc: &ItemRc) {
        let mut items = self.undo_items.take();
        let Some(last) = items.pop() else {
            return;
        };

        match last.kind {
            UndoItemKind::TextInsert => {
                let text: String = self.text().into();
                let text = [text.split_at(last.pos).0, text.split_at(last.pos + last.text.len()).1]
                    .concat();
                self.set_text_internal(text.into());

                self.anchor_position_byte_offset.set(last.anchor as i32);
                self.set_cursor_position(
                    last.cursor as i32,
                    true,
                    TextChangeNotify::TriggerCallbacks,
                    window_adapter,
                    self_rc,
                );
            }
            UndoItemKind::TextRemove => {
                let mut text: String = self.text().into();
                text.insert_str(last.pos, &last.text);
                self.set_text_internal(text.into());

                self.anchor_position_byte_offset.set(last.anchor as i32);
                self.set_cursor_position(
                    last.cursor as i32,
                    true,
                    TextChangeNotify::TriggerCallbacks,
                    window_adapter,
                    self_rc,
                );
            }
        }
        self.undo_items.set(items);

        let mut redo = self.redo_items.take();
        redo.push(last);
        self.redo_items.set(redo);
        Self::FIELD_OFFSETS.edited().apply_pin(self).call(&());
    }

    pub fn redo(self: Pin<&Self>, window_adapter: &Rc<dyn WindowAdapter>, self_rc: &ItemRc) {
        let mut items = self.redo_items.take();
        let Some(last) = items.pop() else {
            return;
        };

        match last.kind {
            UndoItemKind::TextInsert => {
                let mut text: String = self.text().into();
                text.insert_str(last.pos, &last.text);
                self.set_text_internal(text.into());

                self.anchor_position_byte_offset.set(last.anchor as i32);
                self.set_cursor_position(
                    last.cursor as i32,
                    true,
                    TextChangeNotify::TriggerCallbacks,
                    window_adapter,
                    self_rc,
                );
            }
            UndoItemKind::TextRemove => {
                let text: String = self.text().into();
                let text = [text.split_at(last.pos).0, text.split_at(last.pos + last.text.len()).1]
                    .concat();
                self.set_text_internal(text.into());

                self.anchor_position_byte_offset.set(last.anchor as i32);
                self.set_cursor_position(
                    last.cursor as i32,
                    true,
                    TextChangeNotify::TriggerCallbacks,
                    window_adapter,
                    self_rc,
                );
            }
        }

        self.redo_items.set(items);

        let mut undo_items = self.undo_items.take();
        undo_items.push(last);
        self.undo_items.set(undo_items);
        Self::FIELD_OFFSETS.edited().apply_pin(self).call(&());
    }

    pub fn font_metrics(
        self: Pin<&Self>,
        window_adapter: &Rc<dyn WindowAdapter>,
        self_rc: &ItemRc,
    ) -> FontMetrics {
        let font_request = self.font_request(self_rc);
        window_adapter.renderer().font_metrics(font_request)
    }

    fn accept_text_input(self: Pin<&Self>, text_to_insert: &str) -> bool {
        let input_type = self.input_type();

        match input_type {
            InputType::Number => return text_to_insert.chars().all(|ch| ch.is_ascii_digit()),
            InputType::Decimal => {
                let (a, c) = self.selection_anchor_and_cursor();
                let current = self.text();
                let candidate = [&current[..a], text_to_insert, &current[c..]].concat();

                // Allow localized ".", "-", "-." because otherwise the cannot start entering
                if candidate.len() <= 2
                    && crate::context::GLOBAL_CONTEXT.with(|ctx| {
                        let sep =
                            ctx.get().map(|ctx| ctx.locale_decimal_separator()).unwrap_or('.');
                        let mut it = candidate.chars();
                        match (it.next(), it.next()) {
                            (Some('-'), None) => true,
                            (Some('-'), Some(c2)) => c2 == sep,
                            (Some(c1), None) => c1 == sep,
                            _ => false,
                        }
                    })
                {
                    return true;
                }
                return string_to_float(&candidate).is_some();
            }
            InputType::Password | InputType::Text | InputType::Search => (),
        }

        true
    }
}

fn next_paragraph_boundary(text: &str, last_cursor_pos: usize) -> usize {
    text.as_bytes()
        .iter()
        .enumerate()
        .skip(last_cursor_pos)
        .find(|(_, c)| **c == b'\n')
        .map(|(new_pos, _)| new_pos)
        .unwrap_or(text.len())
}

fn prev_paragraph_boundary(text: &str, last_cursor_pos: usize) -> usize {
    text.as_bytes()
        .iter()
        .enumerate()
        .rev()
        .skip(text.len() - last_cursor_pos)
        .find(|(_, c)| **c == b'\n')
        .map(|(new_pos, _)| new_pos + 1)
        .unwrap_or(0)
}

fn prev_word_boundary(text: &str, last_cursor_pos: usize) -> usize {
    let mut word_offset = 0;

    for (current_word_offset, _) in text.unicode_word_indices() {
        if current_word_offset <= last_cursor_pos {
            word_offset = current_word_offset;
        } else {
            break;
        }
    }

    word_offset
}

fn next_word_boundary(text: &str, last_cursor_pos: usize) -> usize {
    text.unicode_word_indices()
        .find(|(offset, slice)| *offset + slice.len() >= last_cursor_pos)
        .map_or(text.len(), |(offset, slice)| offset + slice.len())
}

#[cfg(feature = "ffi")]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn slint_textinput_set_selection_offsets(
    text_input: Pin<&TextInput>,
    window_adapter: *const crate::window::ffi::WindowAdapterRcOpaque,
    self_component: &vtable::VRc<crate::item_tree::ItemTreeVTable>,
    self_index: u32,
    start: i32,
    end: i32,
) {
    unsafe {
        let window_adapter = &*(window_adapter as *const Rc<dyn WindowAdapter>);
        let self_rc = ItemRc::new(self_component.clone(), self_index);
        text_input.set_selection_offsets(window_adapter, &self_rc, start, end);
    }
}

#[cfg(feature = "ffi")]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn slint_textinput_select_all(
    text_input: Pin<&TextInput>,
    window_adapter: *const crate::window::ffi::WindowAdapterRcOpaque,
    self_component: &vtable::VRc<crate::item_tree::ItemTreeVTable>,
    self_index: u32,
) {
    unsafe {
        let window_adapter = &*(window_adapter as *const Rc<dyn WindowAdapter>);
        let self_rc = ItemRc::new(self_component.clone(), self_index);
        text_input.select_all(window_adapter, &self_rc);
    }
}

#[cfg(feature = "ffi")]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn slint_textinput_clear_selection(
    text_input: Pin<&TextInput>,
    window_adapter: *const crate::window::ffi::WindowAdapterRcOpaque,
    self_component: &vtable::VRc<crate::item_tree::ItemTreeVTable>,
    self_index: u32,
) {
    unsafe {
        let window_adapter = &*(window_adapter as *const Rc<dyn WindowAdapter>);
        let self_rc = ItemRc::new(self_component.clone(), self_index);
        text_input.clear_selection(window_adapter, &self_rc);
    }
}

#[cfg(feature = "ffi")]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn slint_textinput_cut(
    text_input: Pin<&TextInput>,
    window_adapter: *const crate::window::ffi::WindowAdapterRcOpaque,
    self_component: &vtable::VRc<crate::item_tree::ItemTreeVTable>,
    self_index: u32,
) {
    unsafe {
        let window_adapter = &*(window_adapter as *const Rc<dyn WindowAdapter>);
        let self_rc = ItemRc::new(self_component.clone(), self_index);
        text_input.cut(window_adapter, &self_rc);
    }
}

#[cfg(feature = "ffi")]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn slint_textinput_copy(
    text_input: Pin<&TextInput>,
    window_adapter: *const crate::window::ffi::WindowAdapterRcOpaque,
    self_component: &vtable::VRc<crate::item_tree::ItemTreeVTable>,
    self_index: u32,
) {
    unsafe {
        let window_adapter = &*(window_adapter as *const Rc<dyn WindowAdapter>);
        let self_rc = ItemRc::new(self_component.clone(), self_index);
        text_input.copy(window_adapter, &self_rc);
    }
}

#[cfg(feature = "ffi")]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn slint_textinput_paste(
    text_input: Pin<&TextInput>,
    window_adapter: *const crate::window::ffi::WindowAdapterRcOpaque,
    self_component: &vtable::VRc<crate::item_tree::ItemTreeVTable>,
    self_index: u32,
) {
    unsafe {
        let window_adapter = &*(window_adapter as *const Rc<dyn WindowAdapter>);
        let self_rc = ItemRc::new(self_component.clone(), self_index);
        text_input.paste(window_adapter, &self_rc);
    }
}

#[cfg(feature = "ffi")]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn slint_textinput_undo(
    text_input: Pin<&TextInput>,
    window_adapter: *const crate::window::ffi::WindowAdapterRcOpaque,
    self_component: &vtable::VRc<crate::item_tree::ItemTreeVTable>,
    self_index: u32,
) {
    unsafe {
        let window_adapter = &*(window_adapter as *const Rc<dyn WindowAdapter>);
        let self_rc = ItemRc::new(self_component.clone(), self_index);
        text_input.undo(window_adapter, &self_rc);
    }
}

#[cfg(feature = "ffi")]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn slint_textinput_redo(
    text_input: Pin<&TextInput>,
    window_adapter: *const crate::window::ffi::WindowAdapterRcOpaque,
    self_component: &vtable::VRc<crate::item_tree::ItemTreeVTable>,
    self_index: u32,
) {
    unsafe {
        let window_adapter = &*(window_adapter as *const Rc<dyn WindowAdapter>);
        let self_rc = ItemRc::new(self_component.clone(), self_index);
        text_input.redo(window_adapter, &self_rc);
    }
}

pub fn slint_text_item_fontmetrics(
    window_adapter: &Rc<dyn WindowAdapter>,
    item_ref: Pin<ItemRef<'_>>,
    self_rc: &ItemRc,
) -> FontMetrics {
    if let Some(simple_text) = ItemRef::downcast_pin::<SimpleText>(item_ref) {
        simple_text.font_metrics(window_adapter, self_rc)
    } else if let Some(complex_text) = ItemRef::downcast_pin::<ComplexText>(item_ref) {
        complex_text.font_metrics(window_adapter, self_rc)
    } else if let Some(text_input) = ItemRef::downcast_pin::<TextInput>(item_ref) {
        text_input.font_metrics(window_adapter, self_rc)
    } else {
        Default::default()
    }
}

#[cfg(feature = "ffi")]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn slint_cpp_text_item_fontmetrics(
    window_adapter: *const crate::window::ffi::WindowAdapterRcOpaque,
    self_component: &vtable::VRc<crate::item_tree::ItemTreeVTable>,
    self_index: u32,
    out: *mut FontMetrics,
) {
    unsafe {
        let window_adapter = &*(window_adapter as *const Rc<dyn WindowAdapter>);
        let self_rc = ItemRc::new(self_component.clone(), self_index);
        let self_ref = self_rc.borrow();
        core::ptr::write(out, slint_text_item_fontmetrics(window_adapter, self_ref, &self_rc));
    }
}
