Skip to main content

i_slint_core/items/
text.rs

1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4// cSpell: ignore textitem
5/*!
6This module contains the builtin text related items.
7
8When adding an item or a property, it needs to be kept in sync with different place.
9Lookup the [`crate::items`] module documentation.
10*/
11use super::{
12    EventResult, FontMetrics, InputType, Item, ItemConsts, ItemRc, ItemRef, KeyEventArg,
13    KeyEventResult, KeyEventType, PointArg, PointerEventButton, RenderingResult, StringArg,
14    TextHorizontalAlignment, TextOverflow, TextStrokeStyle, TextVerticalAlignment, TextWrap,
15    VoidArg,
16};
17use crate::graphics::{Brush, Color, FontRequest};
18use crate::input::{
19    FocusEvent, FocusEventResult, FocusReason, InputEventFilterResult, InputEventResult,
20    InternalKeyEvent, KeyboardModifiers, MouseEvent, StandardShortcut, TextShortcut, key_codes,
21};
22use crate::item_rendering::{
23    CachedRenderingData, HasFont, ItemRenderer, PlainOrStyledText, RenderString, RenderText,
24};
25use crate::layout::{LayoutInfo, Orientation};
26use crate::lengths::{LogicalLength, LogicalPoint, LogicalRect, LogicalSize};
27use crate::platform::Clipboard;
28#[cfg(feature = "rtti")]
29use crate::rtti::*;
30use crate::string::string_to_float;
31use crate::window::{InputMethodProperties, InputMethodRequest, WindowAdapter, WindowInner};
32use crate::{Callback, Coord, Property, SharedString, SharedVector};
33use alloc::{rc::Rc, string::String};
34use const_field_offset::FieldOffsets;
35use core::cell::Cell;
36use core::pin::Pin;
37#[allow(unused)]
38use euclid::num::Ceil;
39use i_slint_core_macros::*;
40use unicode_segmentation::UnicodeSegmentation;
41
42/// The implementation of the `Text` element
43#[repr(C)]
44#[derive(FieldOffsets, Default, SlintElement)]
45#[pin]
46pub struct ComplexText {
47    pub width: Property<LogicalLength>,
48    pub height: Property<LogicalLength>,
49    pub text: Property<SharedString>,
50    pub font_size: Property<LogicalLength>,
51    pub font_weight: Property<i32>,
52    pub color: Property<Brush>,
53    pub horizontal_alignment: Property<TextHorizontalAlignment>,
54    pub vertical_alignment: Property<TextVerticalAlignment>,
55
56    pub font_family: Property<SharedString>,
57    pub font_italic: Property<bool>,
58    pub wrap: Property<TextWrap>,
59    pub overflow: Property<TextOverflow>,
60    pub letter_spacing: Property<LogicalLength>,
61    pub stroke: Property<Brush>,
62    pub stroke_width: Property<LogicalLength>,
63    pub stroke_style: Property<TextStrokeStyle>,
64    pub cached_rendering_data: CachedRenderingData,
65}
66
67impl Item for ComplexText {
68    fn init(self: Pin<&Self>, _self_rc: &ItemRc) {}
69
70    fn deinit(self: Pin<&Self>, _window_adapter: &Rc<dyn WindowAdapter>) {}
71
72    fn layout_info(
73        self: Pin<&Self>,
74        orientation: Orientation,
75        cross_axis_constraint: Coord,
76        window_adapter: &Rc<dyn WindowAdapter>,
77        self_rc: &ItemRc,
78    ) -> LayoutInfo {
79        text_layout_info(
80            self,
81            self_rc,
82            window_adapter,
83            orientation,
84            Self::FIELD_OFFSETS.width().apply_pin(self),
85            cross_axis_constraint,
86        )
87    }
88
89    fn input_event_filter_before_children(
90        self: Pin<&Self>,
91        _: &MouseEvent,
92        _window_adapter: &Rc<dyn WindowAdapter>,
93        _self_rc: &ItemRc,
94        _: &mut super::MouseCursor,
95    ) -> InputEventFilterResult {
96        InputEventFilterResult::ForwardAndIgnore
97    }
98
99    fn input_event(
100        self: Pin<&Self>,
101        _: &MouseEvent,
102        _window_adapter: &Rc<dyn WindowAdapter>,
103        _self_rc: &ItemRc,
104        _: &mut super::MouseCursor,
105    ) -> InputEventResult {
106        InputEventResult::EventIgnored
107    }
108
109    fn capture_key_event(
110        self: Pin<&Self>,
111        _: &InternalKeyEvent,
112        _window_adapter: &Rc<dyn WindowAdapter>,
113        _self_rc: &ItemRc,
114    ) -> KeyEventResult {
115        KeyEventResult::EventIgnored
116    }
117
118    fn key_event(
119        self: Pin<&Self>,
120        _: &InternalKeyEvent,
121        _window_adapter: &Rc<dyn WindowAdapter>,
122        _self_rc: &ItemRc,
123    ) -> KeyEventResult {
124        KeyEventResult::EventIgnored
125    }
126
127    fn focus_event(
128        self: Pin<&Self>,
129        _: &FocusEvent,
130        _window_adapter: &Rc<dyn WindowAdapter>,
131        _self_rc: &ItemRc,
132    ) -> FocusEventResult {
133        FocusEventResult::FocusIgnored
134    }
135
136    fn render(
137        self: Pin<&Self>,
138        backend: &mut &mut dyn ItemRenderer,
139        self_rc: &ItemRc,
140        size: LogicalSize,
141    ) -> RenderingResult {
142        (*backend).draw_text(self, self_rc, size, &self.cached_rendering_data);
143        RenderingResult::ContinueRenderingChildren
144    }
145
146    fn bounding_rect(
147        self: core::pin::Pin<&Self>,
148        _window_adapter: &Rc<dyn WindowAdapter>,
149        _self_rc: &ItemRc,
150        geometry: LogicalRect,
151    ) -> LogicalRect {
152        geometry
153    }
154
155    fn clips_children(self: core::pin::Pin<&Self>) -> bool {
156        false
157    }
158}
159
160impl ItemConsts for ComplexText {
161    const cached_rendering_data_offset: const_field_offset::FieldOffset<
162        ComplexText,
163        CachedRenderingData,
164    > = ComplexText::FIELD_OFFSETS.cached_rendering_data().as_unpinned_projection();
165}
166
167impl HasFont for ComplexText {
168    fn font_request(self: Pin<&Self>, self_rc: &crate::items::ItemRc) -> FontRequest {
169        crate::items::WindowItem::resolved_font_request(
170            self_rc,
171            self.font_family(),
172            self.font_weight(),
173            self.font_size(),
174            self.letter_spacing(),
175            self.font_italic(),
176        )
177    }
178}
179
180impl RenderString for ComplexText {
181    fn text(self: Pin<&Self>) -> PlainOrStyledText {
182        PlainOrStyledText::Plain(self.text())
183    }
184}
185
186impl RenderText for ComplexText {
187    fn target_size(self: Pin<&Self>) -> LogicalSize {
188        LogicalSize::from_lengths(self.width(), self.height())
189    }
190
191    fn color(self: Pin<&Self>) -> Brush {
192        self.color()
193    }
194
195    fn link_color(self: Pin<&Self>) -> Color {
196        Default::default()
197    }
198
199    fn alignment(
200        self: Pin<&Self>,
201    ) -> (super::TextHorizontalAlignment, super::TextVerticalAlignment) {
202        (self.horizontal_alignment(), self.vertical_alignment())
203    }
204
205    fn wrap(self: Pin<&Self>) -> TextWrap {
206        self.wrap()
207    }
208
209    fn overflow(self: Pin<&Self>) -> TextOverflow {
210        self.overflow()
211    }
212
213    fn stroke(self: Pin<&Self>) -> (Brush, LogicalLength, TextStrokeStyle) {
214        (self.stroke(), self.stroke_width(), self.stroke_style())
215    }
216
217    fn is_markdown(self: Pin<&Self>) -> bool {
218        false
219    }
220}
221
222impl ComplexText {
223    pub fn font_metrics(
224        self: Pin<&Self>,
225        window_adapter: &Rc<dyn WindowAdapter>,
226        self_rc: &ItemRc,
227    ) -> FontMetrics {
228        let font_request = self.font_request(self_rc);
229        window_adapter.renderer().font_metrics(font_request)
230    }
231}
232
233/// The implementation of the `Text` element
234#[repr(C)]
235#[derive(FieldOffsets, Default, SlintElement)]
236#[pin]
237pub struct StyledTextItem {
238    pub width: Property<LogicalLength>,
239    pub height: Property<LogicalLength>,
240    pub text: Property<crate::styled_text::StyledText>,
241    pub default_color: Property<Brush>,
242    pub default_font_size: Property<LogicalLength>,
243    pub default_font_family: Property<SharedString>,
244    pub horizontal_alignment: Property<TextHorizontalAlignment>,
245    pub vertical_alignment: Property<TextVerticalAlignment>,
246    pub link_clicked: Callback<StringArg>,
247    pub link_color: Property<Color>,
248    pub cached_rendering_data: CachedRenderingData,
249}
250
251impl Item for StyledTextItem {
252    fn init(self: Pin<&Self>, _self_rc: &ItemRc) {}
253
254    fn deinit(self: Pin<&Self>, _window_adapter: &Rc<dyn WindowAdapter>) {}
255
256    fn layout_info(
257        self: Pin<&Self>,
258        orientation: Orientation,
259        cross_axis_constraint: Coord,
260        window_adapter: &Rc<dyn WindowAdapter>,
261        self_rc: &ItemRc,
262    ) -> LayoutInfo {
263        text_layout_info(
264            self,
265            self_rc,
266            window_adapter,
267            orientation,
268            Self::FIELD_OFFSETS.width().apply_pin(self),
269            cross_axis_constraint,
270        )
271    }
272
273    fn input_event_filter_before_children(
274        self: Pin<&Self>,
275        _: &MouseEvent,
276        _window_adapter: &Rc<dyn WindowAdapter>,
277        _self_rc: &ItemRc,
278        _: &mut super::MouseCursor,
279    ) -> InputEventFilterResult {
280        InputEventFilterResult::ForwardEvent
281    }
282
283    #[cfg_attr(not(feature = "shared-parley"), allow(unused))]
284    fn input_event(
285        self: Pin<&Self>,
286        event: &MouseEvent,
287        window_adapter: &Rc<dyn WindowAdapter>,
288        self_rc: &ItemRc,
289        cursor: &mut super::MouseCursor,
290    ) -> InputEventResult {
291        #[cfg(feature = "shared-parley")]
292        let find_link = |position: &LogicalPoint| {
293            let window_inner = WindowInner::from_pub(window_adapter.window());
294            let scale_factor = crate::lengths::ScaleFactor::new(window_inner.scale_factor());
295            crate::textlayout::sharedparley::link_under_cursor(
296                &mut window_inner.context().font_context().borrow_mut(),
297                scale_factor,
298                self,
299                self_rc,
300                LogicalSize::from_lengths(self.width(), self.height()),
301                *position * scale_factor,
302                None,
303            )
304        };
305        match event {
306            #[cfg(feature = "shared-parley")]
307            MouseEvent::Released {
308                position,
309                button: PointerEventButton::Left,
310                click_count: _,
311                touch_finger_id: _,
312            } => {
313                if let Some(link) = find_link(position) {
314                    *cursor = super::MouseCursor::Pointer;
315                    Self::FIELD_OFFSETS.link_clicked().apply_pin(self).call(&(link.into(),));
316                }
317                InputEventResult::EventAccepted
318            }
319            #[cfg(feature = "shared-parley")]
320            MouseEvent::Moved { position, .. }
321            | MouseEvent::Pressed { position, .. }
322            | MouseEvent::Released { position, .. } => {
323                if find_link(position).is_some() {
324                    *cursor = super::MouseCursor::Pointer;
325                }
326                InputEventResult::EventAccepted
327            }
328            _ => InputEventResult::EventIgnored,
329        }
330    }
331
332    fn capture_key_event(
333        self: Pin<&Self>,
334        _: &InternalKeyEvent,
335        _window_adapter: &Rc<dyn WindowAdapter>,
336        _self_rc: &ItemRc,
337    ) -> KeyEventResult {
338        KeyEventResult::EventIgnored
339    }
340
341    fn key_event(
342        self: Pin<&Self>,
343        _: &InternalKeyEvent,
344        _window_adapter: &Rc<dyn WindowAdapter>,
345        _self_rc: &ItemRc,
346    ) -> KeyEventResult {
347        KeyEventResult::EventIgnored
348    }
349
350    fn focus_event(
351        self: Pin<&Self>,
352        _: &FocusEvent,
353        _window_adapter: &Rc<dyn WindowAdapter>,
354        _self_rc: &ItemRc,
355    ) -> FocusEventResult {
356        FocusEventResult::FocusIgnored
357    }
358
359    fn render(
360        self: Pin<&Self>,
361        backend: &mut &mut dyn ItemRenderer,
362        self_rc: &ItemRc,
363        size: LogicalSize,
364    ) -> RenderingResult {
365        (*backend).draw_text(self, self_rc, size, &self.cached_rendering_data);
366        RenderingResult::ContinueRenderingChildren
367    }
368
369    fn bounding_rect(
370        self: core::pin::Pin<&Self>,
371        _window_adapter: &Rc<dyn WindowAdapter>,
372        _self_rc: &ItemRc,
373        geometry: LogicalRect,
374    ) -> LogicalRect {
375        geometry
376    }
377
378    fn clips_children(self: Pin<&Self>) -> bool {
379        false
380    }
381}
382
383impl ItemConsts for StyledTextItem {
384    const cached_rendering_data_offset: const_field_offset::FieldOffset<
385        StyledTextItem,
386        CachedRenderingData,
387    > = StyledTextItem::FIELD_OFFSETS.cached_rendering_data().as_unpinned_projection();
388}
389
390impl HasFont for StyledTextItem {
391    fn font_request(self: Pin<&Self>, self_rc: &crate::items::ItemRc) -> FontRequest {
392        crate::items::WindowItem::resolved_font_request(
393            self_rc,
394            self.default_font_family(),
395            Default::default(),
396            self.default_font_size(),
397            Default::default(),
398            Default::default(),
399        )
400    }
401}
402
403impl RenderString for StyledTextItem {
404    fn text(self: Pin<&Self>) -> PlainOrStyledText {
405        PlainOrStyledText::Styled(self.text())
406    }
407}
408
409impl RenderText for StyledTextItem {
410    fn target_size(self: Pin<&Self>) -> LogicalSize {
411        LogicalSize::from_lengths(self.width(), self.height())
412    }
413
414    fn color(self: Pin<&Self>) -> Brush {
415        self.default_color()
416    }
417
418    fn link_color(self: Pin<&Self>) -> Color {
419        self.link_color()
420    }
421
422    fn alignment(
423        self: Pin<&Self>,
424    ) -> (super::TextHorizontalAlignment, super::TextVerticalAlignment) {
425        (self.horizontal_alignment(), self.vertical_alignment())
426    }
427
428    fn wrap(self: Pin<&Self>) -> TextWrap {
429        TextWrap::WordWrap
430    }
431
432    fn overflow(self: Pin<&Self>) -> TextOverflow {
433        TextOverflow::Clip
434    }
435
436    fn stroke(self: Pin<&Self>) -> (Brush, LogicalLength, TextStrokeStyle) {
437        Default::default()
438    }
439
440    fn is_markdown(self: Pin<&Self>) -> bool {
441        true
442    }
443}
444
445impl StyledTextItem {
446    pub fn font_metrics(
447        self: Pin<&Self>,
448        window_adapter: &Rc<dyn WindowAdapter>,
449        self_rc: &ItemRc,
450    ) -> FontMetrics {
451        let font_request = self.font_request(self_rc);
452        window_adapter.renderer().font_metrics(font_request)
453    }
454}
455
456/// The implementation of the `Text` element
457#[repr(C)]
458#[derive(FieldOffsets, Default, SlintElement)]
459#[pin]
460pub struct SimpleText {
461    pub width: Property<LogicalLength>,
462    pub height: Property<LogicalLength>,
463    pub text: Property<SharedString>,
464    pub font_size: Property<LogicalLength>,
465    pub font_weight: Property<i32>,
466    pub color: Property<Brush>,
467    pub horizontal_alignment: Property<TextHorizontalAlignment>,
468    pub vertical_alignment: Property<TextVerticalAlignment>,
469
470    pub cached_rendering_data: CachedRenderingData,
471}
472
473impl Item for SimpleText {
474    fn init(self: Pin<&Self>, _self_rc: &ItemRc) {}
475
476    fn deinit(self: Pin<&Self>, _window_adapter: &Rc<dyn WindowAdapter>) {}
477
478    fn layout_info(
479        self: Pin<&Self>,
480        orientation: Orientation,
481        cross_axis_constraint: Coord,
482        window_adapter: &Rc<dyn WindowAdapter>,
483        self_rc: &ItemRc,
484    ) -> LayoutInfo {
485        text_layout_info(
486            self,
487            self_rc,
488            window_adapter,
489            orientation,
490            Self::FIELD_OFFSETS.width().apply_pin(self),
491            cross_axis_constraint,
492        )
493    }
494
495    fn input_event_filter_before_children(
496        self: Pin<&Self>,
497        _: &MouseEvent,
498        _window_adapter: &Rc<dyn WindowAdapter>,
499        _self_rc: &ItemRc,
500        _: &mut super::MouseCursor,
501    ) -> InputEventFilterResult {
502        InputEventFilterResult::ForwardAndIgnore
503    }
504
505    fn input_event(
506        self: Pin<&Self>,
507        _: &MouseEvent,
508        _window_adapter: &Rc<dyn WindowAdapter>,
509        _self_rc: &ItemRc,
510        _: &mut super::MouseCursor,
511    ) -> InputEventResult {
512        InputEventResult::EventIgnored
513    }
514
515    fn capture_key_event(
516        self: Pin<&Self>,
517        _: &InternalKeyEvent,
518        _window_adapter: &Rc<dyn WindowAdapter>,
519        _self_rc: &ItemRc,
520    ) -> KeyEventResult {
521        KeyEventResult::EventIgnored
522    }
523
524    fn key_event(
525        self: Pin<&Self>,
526        _: &InternalKeyEvent,
527        _window_adapter: &Rc<dyn WindowAdapter>,
528        _self_rc: &ItemRc,
529    ) -> KeyEventResult {
530        KeyEventResult::EventIgnored
531    }
532
533    fn focus_event(
534        self: Pin<&Self>,
535        _: &FocusEvent,
536        _window_adapter: &Rc<dyn WindowAdapter>,
537        _self_rc: &ItemRc,
538    ) -> FocusEventResult {
539        FocusEventResult::FocusIgnored
540    }
541
542    fn render(
543        self: Pin<&Self>,
544        backend: &mut &mut dyn ItemRenderer,
545        self_rc: &ItemRc,
546        size: LogicalSize,
547    ) -> RenderingResult {
548        (*backend).draw_text(self, self_rc, size, &self.cached_rendering_data);
549        RenderingResult::ContinueRenderingChildren
550    }
551
552    fn bounding_rect(
553        self: core::pin::Pin<&Self>,
554        _window_adapter: &Rc<dyn WindowAdapter>,
555        _self_rc: &ItemRc,
556        geometry: LogicalRect,
557    ) -> LogicalRect {
558        geometry
559    }
560
561    fn clips_children(self: core::pin::Pin<&Self>) -> bool {
562        false
563    }
564}
565
566impl ItemConsts for SimpleText {
567    const cached_rendering_data_offset: const_field_offset::FieldOffset<
568        SimpleText,
569        CachedRenderingData,
570    > = SimpleText::FIELD_OFFSETS.cached_rendering_data().as_unpinned_projection();
571}
572
573impl HasFont for SimpleText {
574    fn font_request(self: Pin<&Self>, self_rc: &crate::items::ItemRc) -> FontRequest {
575        crate::items::WindowItem::resolved_font_request(
576            self_rc,
577            SharedString::default(),
578            self.font_weight(),
579            self.font_size(),
580            LogicalLength::default(),
581            false,
582        )
583    }
584}
585
586impl RenderString for SimpleText {
587    fn text(self: Pin<&Self>) -> PlainOrStyledText {
588        PlainOrStyledText::Plain(self.text())
589    }
590}
591
592impl RenderText for SimpleText {
593    fn target_size(self: Pin<&Self>) -> LogicalSize {
594        LogicalSize::from_lengths(self.width(), self.height())
595    }
596
597    fn color(self: Pin<&Self>) -> Brush {
598        self.color()
599    }
600
601    fn link_color(self: Pin<&Self>) -> Color {
602        Default::default()
603    }
604
605    fn alignment(
606        self: Pin<&Self>,
607    ) -> (super::TextHorizontalAlignment, super::TextVerticalAlignment) {
608        (self.horizontal_alignment(), self.vertical_alignment())
609    }
610
611    fn wrap(self: Pin<&Self>) -> TextWrap {
612        TextWrap::default()
613    }
614
615    fn overflow(self: Pin<&Self>) -> TextOverflow {
616        TextOverflow::default()
617    }
618
619    fn stroke(self: Pin<&Self>) -> (Brush, LogicalLength, TextStrokeStyle) {
620        Default::default()
621    }
622
623    fn is_markdown(self: Pin<&Self>) -> bool {
624        false
625    }
626}
627
628impl SimpleText {
629    pub fn font_metrics(
630        self: Pin<&Self>,
631        window_adapter: &Rc<dyn WindowAdapter>,
632        self_rc: &ItemRc,
633    ) -> FontMetrics {
634        window_adapter.renderer().font_metrics(self.font_request(self_rc))
635    }
636}
637
638fn text_layout_info(
639    text: Pin<&dyn RenderText>,
640    self_rc: &ItemRc,
641    window_adapter: &Rc<dyn WindowAdapter>,
642    orientation: Orientation,
643    width: Pin<&Property<LogicalLength>>,
644    cross_axis_constraint: Coord,
645) -> LayoutInfo {
646    let implicit_size = |max_width, text_wrap| {
647        window_adapter.renderer().text_size(text, self_rc, max_width, text_wrap)
648    };
649
650    // Stretch uses `round_layout` to explicitly align the top left and bottom right of layout nodes
651    // to pixel boundaries. To avoid rounding down causing the minimum width to become so little that
652    // letters will be cut off, apply the ceiling here.
653    match orientation {
654        Orientation::Horizontal => {
655            let implicit_size = implicit_size(None, TextWrap::NoWrap);
656            let min = match text.overflow() {
657                TextOverflow::Elide => implicit_size
658                    .width
659                    .min(window_adapter.renderer().char_size(text, self_rc, '…').width),
660                TextOverflow::Clip => match text.wrap() {
661                    TextWrap::NoWrap => implicit_size.width,
662                    TextWrap::WordWrap | TextWrap::CharWrap => 0 as Coord,
663                },
664            };
665            LayoutInfo {
666                min: min.ceil(),
667                preferred: implicit_size.width.ceil(),
668                ..LayoutInfo::default()
669            }
670        }
671        Orientation::Vertical => {
672            let h = match text.wrap() {
673                TextWrap::NoWrap => implicit_size(None, TextWrap::NoWrap).height,
674                wrap @ (TextWrap::WordWrap | TextWrap::CharWrap) => {
675                    let w = if cross_axis_constraint >= 0 as Coord {
676                        LogicalLength::new(cross_axis_constraint)
677                    } else {
678                        width.get()
679                    };
680                    implicit_size(Some(w), wrap).height
681                }
682            }
683            .ceil();
684            LayoutInfo { min: h, preferred: h, ..LayoutInfo::default() }
685        }
686    }
687}
688
689#[repr(C)]
690#[derive(Default, Clone, Copy, PartialEq)]
691/// Similar as `Option<core::ops::Range<i32>>` but `repr(C)`
692///
693/// This is the selection within a preedit
694struct PreEditSelection {
695    valid: bool,
696    start: i32,
697    end: i32,
698}
699
700impl From<Option<core::ops::Range<i32>>> for PreEditSelection {
701    fn from(value: Option<core::ops::Range<i32>>) -> Self {
702        value.map_or_else(Default::default, |r| Self { valid: true, start: r.start, end: r.end })
703    }
704}
705
706impl PreEditSelection {
707    fn as_option(self) -> Option<core::ops::Range<i32>> {
708        self.valid.then_some(self.start..self.end)
709    }
710}
711
712#[repr(C)]
713#[derive(Clone)]
714enum UndoItemKind {
715    TextInsert,
716    TextRemove,
717}
718
719#[repr(C)]
720#[derive(Clone)]
721struct UndoItem {
722    pos: usize,
723    text: SharedString,
724    cursor: usize,
725    anchor: usize,
726    kind: UndoItemKind,
727}
728
729/// The implementation of the `TextInput` element
730#[repr(C)]
731#[derive(FieldOffsets, Default, SlintElement)]
732#[pin]
733pub struct TextInput {
734    pub text: Property<SharedString>,
735    pub font_family: Property<SharedString>,
736    pub font_size: Property<LogicalLength>,
737    pub font_weight: Property<i32>,
738    pub font_italic: Property<bool>,
739    pub color: Property<Brush>,
740    pub selection_foreground_color: Property<Color>,
741    pub selection_background_color: Property<Color>,
742    pub horizontal_alignment: Property<TextHorizontalAlignment>,
743    pub vertical_alignment: Property<TextVerticalAlignment>,
744    pub wrap: Property<TextWrap>,
745    pub input_type: Property<InputType>,
746    pub letter_spacing: Property<LogicalLength>,
747    pub width: Property<LogicalLength>,
748    pub height: Property<LogicalLength>,
749    pub cursor_position_byte_offset: Property<i32>,
750    pub anchor_position_byte_offset: Property<i32>,
751    pub text_cursor_width: Property<LogicalLength>,
752    pub page_height: Property<LogicalLength>,
753    pub cursor_visible: Property<bool>,
754    pub has_focus: Property<bool>,
755    pub enabled: Property<bool>,
756    pub accepted: Callback<VoidArg>,
757    pub cursor_position_changed: Callback<PointArg>,
758    pub edited: Callback<VoidArg>,
759    pub key_pressed: Callback<KeyEventArg, EventResult>,
760    pub key_released: Callback<KeyEventArg, EventResult>,
761    pub single_line: Property<bool>,
762    pub read_only: Property<bool>,
763    pub preedit_text: Property<SharedString>,
764    /// A selection within the preedit (cursor and anchor)
765    preedit_selection: Property<PreEditSelection>,
766    pub cached_rendering_data: CachedRenderingData,
767    // The x position where the cursor wants to be.
768    // It is not updated when moving up and down even when the line is shorter.
769    preferred_x_pos: Cell<Coord>,
770    /// 0 = not pressed, 1 = single press, 2 = double clicked+press , ...
771    pressed: Cell<u8>,
772    undo_items: Cell<SharedVector<UndoItem>>,
773    redo_items: Cell<SharedVector<UndoItem>>,
774}
775
776impl Item for TextInput {
777    fn init(self: Pin<&Self>, _self_rc: &ItemRc) {}
778
779    fn deinit(self: Pin<&Self>, window_adapter: &Rc<dyn WindowAdapter>) {
780        if self.has_focus() {
781            let window_inner = crate::window::WindowInner::from_pub(window_adapter.window());
782            window_inner.set_text_input_focused(false);
783        }
784    }
785
786    fn layout_info(
787        self: Pin<&Self>,
788        orientation: Orientation,
789        cross_axis_constraint: Coord,
790        window_adapter: &Rc<dyn WindowAdapter>,
791        self_rc: &ItemRc,
792    ) -> LayoutInfo {
793        let implicit_size = |max_width, text_wrap| {
794            window_adapter.renderer().text_size(self, self_rc, max_width, text_wrap)
795        };
796
797        // Stretch uses `round_layout` to explicitly align the top left and bottom right of layout nodes
798        // to pixel boundaries. To avoid rounding down causing the minimum width to become so little that
799        // letters will be cut off, apply the ceiling here.
800        match orientation {
801            Orientation::Horizontal => {
802                let implicit_size = implicit_size(None, TextWrap::NoWrap);
803                let min = match self.wrap() {
804                    TextWrap::NoWrap => implicit_size.width,
805                    TextWrap::WordWrap | TextWrap::CharWrap => 0 as Coord,
806                };
807                LayoutInfo {
808                    min: min.ceil(),
809                    preferred: implicit_size.width.ceil(),
810                    ..LayoutInfo::default()
811                }
812            }
813            Orientation::Vertical => {
814                let h = match self.wrap() {
815                    TextWrap::NoWrap => implicit_size(None, TextWrap::NoWrap).height,
816                    wrap @ (TextWrap::WordWrap | TextWrap::CharWrap) => {
817                        let w = if cross_axis_constraint >= 0 as Coord {
818                            LogicalLength::new(cross_axis_constraint)
819                        } else {
820                            self.width()
821                        };
822                        implicit_size(Some(w), wrap).height
823                    }
824                }
825                .ceil();
826                LayoutInfo { min: h, preferred: h, ..LayoutInfo::default() }
827            }
828        }
829    }
830
831    fn input_event_filter_before_children(
832        self: Pin<&Self>,
833        _: &MouseEvent,
834        _window_adapter: &Rc<dyn WindowAdapter>,
835        _self_rc: &ItemRc,
836        _: &mut super::MouseCursor,
837    ) -> InputEventFilterResult {
838        InputEventFilterResult::ForwardEvent
839    }
840
841    fn input_event(
842        self: Pin<&Self>,
843        event: &MouseEvent,
844        window_adapter: &Rc<dyn WindowAdapter>,
845        self_rc: &ItemRc,
846        cursor: &mut super::MouseCursor,
847    ) -> InputEventResult {
848        if !self.enabled() {
849            return InputEventResult::EventIgnored;
850        }
851
852        *cursor = super::MouseCursor::Text;
853
854        match event {
855            MouseEvent::Pressed {
856                position, button: PointerEventButton::Left, click_count, ..
857            } => {
858                let clicked_offset =
859                    self.byte_offset_for_position(*position, window_adapter, self_rc) as i32;
860                self.as_ref().pressed.set((click_count % 3) + 1);
861
862                if !window_adapter.window().0.context().0.modifiers.get().shift() {
863                    self.as_ref().anchor_position_byte_offset.set(clicked_offset);
864                }
865
866                #[cfg(not(target_os = "android"))]
867                self.ensure_focus_and_ime(window_adapter, self_rc);
868
869                match click_count % 3 {
870                    0 => self.set_cursor_position(
871                        clicked_offset,
872                        true,
873                        TextChangeNotify::TriggerCallbacks,
874                        window_adapter,
875                        self_rc,
876                    ),
877                    1 => self.select_word(window_adapter, self_rc),
878                    2 => self.select_paragraph(window_adapter, self_rc),
879                    _ => unreachable!(),
880                };
881
882                return InputEventResult::GrabMouse;
883            }
884            MouseEvent::Pressed { button: PointerEventButton::Middle, .. } => {
885                #[cfg(not(target_os = "android"))]
886                self.ensure_focus_and_ime(window_adapter, self_rc);
887            }
888            MouseEvent::Released { button: PointerEventButton::Left, .. } => {
889                self.as_ref().pressed.set(0);
890                self.copy_clipboard(window_adapter, Clipboard::SelectionClipboard);
891                #[cfg(target_os = "android")]
892                self.ensure_focus_and_ime(window_adapter, self_rc);
893            }
894            MouseEvent::Released { position, button: PointerEventButton::Middle, .. } => {
895                let clicked_offset =
896                    self.byte_offset_for_position(*position, window_adapter, self_rc) as i32;
897                self.as_ref().anchor_position_byte_offset.set(clicked_offset);
898                self.set_cursor_position(
899                    clicked_offset,
900                    true,
901                    // We trigger the callbacks because paste_clipboard might not if there is no clipboard
902                    TextChangeNotify::TriggerCallbacks,
903                    window_adapter,
904                    self_rc,
905                );
906                self.paste_clipboard(window_adapter, self_rc, Clipboard::SelectionClipboard);
907            }
908            MouseEvent::Exit => self.as_ref().pressed.set(0),
909            MouseEvent::Moved { position, .. } => {
910                let pressed = self.as_ref().pressed.get();
911                if pressed > 0 {
912                    let clicked_offset =
913                        self.byte_offset_for_position(*position, window_adapter, self_rc) as i32;
914                    self.set_cursor_position(
915                        clicked_offset,
916                        true,
917                        if (pressed - 1).is_multiple_of(3) {
918                            TextChangeNotify::TriggerCallbacks
919                        } else {
920                            TextChangeNotify::SkipCallbacks
921                        },
922                        window_adapter,
923                        self_rc,
924                    );
925                    match (pressed - 1) % 3 {
926                        0 => (),
927                        1 => self.select_word(window_adapter, self_rc),
928                        2 => self.select_paragraph(window_adapter, self_rc),
929                        _ => unreachable!(),
930                    }
931                    return InputEventResult::GrabMouse;
932                }
933            }
934            _ => return InputEventResult::EventIgnored,
935        }
936        InputEventResult::EventAccepted
937    }
938
939    fn capture_key_event(
940        self: Pin<&Self>,
941        _: &InternalKeyEvent,
942        _window_adapter: &Rc<dyn WindowAdapter>,
943        _self_rc: &ItemRc,
944    ) -> KeyEventResult {
945        KeyEventResult::EventIgnored
946    }
947
948    fn key_event(
949        self: Pin<&Self>,
950        event: &InternalKeyEvent,
951        window_adapter: &Rc<dyn WindowAdapter>,
952        self_rc: &ItemRc,
953    ) -> KeyEventResult {
954        if !self.enabled() {
955            return KeyEventResult::EventIgnored;
956        }
957        match event.event_type {
958            KeyEventType::KeyPressed => {
959                // invoke first key_pressed callback to give the developer/designer the possibility to implement a custom behavior
960                if Self::FIELD_OFFSETS
961                    .key_pressed()
962                    .apply_pin(self)
963                    .call(&(event.key_event.clone(),))
964                    == EventResult::Accept
965                {
966                    return KeyEventResult::EventAccepted;
967                }
968
969                let delete_direction = match event.text_shortcut() {
970                    Some(TextShortcut::Move(direction)) => {
971                        TextInput::move_cursor(
972                            self,
973                            direction,
974                            event.key_event.modifiers.into(),
975                            TextChangeNotify::TriggerCallbacks,
976                            window_adapter,
977                            self_rc,
978                        );
979                        return KeyEventResult::EventAccepted;
980                    }
981                    // Special case: backspace breaks the grapheme and selects the previous character
982                    Some(TextShortcut::DeleteBackward) => {
983                        Some(TextCursorDirection::PreviousCharacter)
984                    }
985                    Some(TextShortcut::DeleteForward) => Some(TextCursorDirection::Forward),
986                    Some(TextShortcut::DeleteWordForward) => {
987                        Some(TextCursorDirection::ForwardByWord)
988                    }
989                    Some(TextShortcut::DeleteWordBackward) => {
990                        Some(TextCursorDirection::BackwardByWord)
991                    }
992                    Some(TextShortcut::DeleteToStartOfLine) => {
993                        Some(TextCursorDirection::StartOfLine)
994                    }
995                    None => None,
996                };
997                if let Some(direction) = delete_direction {
998                    if self.read_only() {
999                        return KeyEventResult::EventIgnored;
1000                    }
1001                    TextInput::select_and_delete(self, direction, window_adapter, self_rc);
1002                    return KeyEventResult::EventAccepted;
1003                }
1004
1005                if let Some(keycode) = event.key_event.text.chars().next()
1006                    && keycode == key_codes::Return
1007                    && !self.read_only()
1008                    && self.single_line()
1009                {
1010                    Self::FIELD_OFFSETS.accepted().apply_pin(self).call(&());
1011                    return KeyEventResult::EventAccepted;
1012                }
1013
1014                // Only insert/interpreter non-control character strings
1015                if event.key_event.text.is_empty()
1016                    || event.key_event.text.as_str().chars().any(|ch| {
1017                        // exclude the private use area as we encode special keys into it
1018                        ('\u{f700}'..='\u{f7ff}').contains(&ch) || (ch.is_control() && ch != '\n')
1019                    })
1020                {
1021                    return KeyEventResult::EventIgnored;
1022                }
1023
1024                if let Some(shortcut) = event.shortcut() {
1025                    match shortcut {
1026                        StandardShortcut::SelectAll => {
1027                            self.select_all(window_adapter, self_rc);
1028                            return KeyEventResult::EventAccepted;
1029                        }
1030                        StandardShortcut::Copy => {
1031                            self.copy(window_adapter, self_rc);
1032                            return KeyEventResult::EventAccepted;
1033                        }
1034                        StandardShortcut::Paste if !self.read_only() => {
1035                            self.paste(window_adapter, self_rc);
1036                            return KeyEventResult::EventAccepted;
1037                        }
1038                        StandardShortcut::Cut if !self.read_only() => {
1039                            self.cut(window_adapter, self_rc);
1040                            return KeyEventResult::EventAccepted;
1041                        }
1042                        StandardShortcut::Paste | StandardShortcut::Cut => {
1043                            return KeyEventResult::EventIgnored;
1044                        }
1045                        StandardShortcut::Undo if !self.read_only() => {
1046                            self.undo(window_adapter, self_rc);
1047                            return KeyEventResult::EventAccepted;
1048                        }
1049                        StandardShortcut::Redo if !self.read_only() => {
1050                            self.redo(window_adapter, self_rc);
1051                            return KeyEventResult::EventAccepted;
1052                        }
1053                        _ => (),
1054                    }
1055                }
1056
1057                if self.read_only() || event.key_event.modifiers.control {
1058                    return KeyEventResult::EventIgnored;
1059                }
1060
1061                // save real anchor/cursor for undo/redo
1062                let (real_cursor, real_anchor) = {
1063                    let text = self.text();
1064                    (self.cursor_position(&text), self.anchor_position(&text))
1065                };
1066
1067                if !self.accept_text_input(event.key_event.text.as_str()) {
1068                    return KeyEventResult::EventIgnored;
1069                }
1070
1071                self.delete_selection(window_adapter, self_rc, TextChangeNotify::SkipCallbacks);
1072
1073                let mut text: String = self.text().into();
1074
1075                // FIXME: respect grapheme boundaries
1076                let insert_pos = self.selection_anchor_and_cursor().1;
1077                text.insert_str(insert_pos, &event.key_event.text);
1078
1079                self.add_undo_item(UndoItem {
1080                    pos: insert_pos,
1081                    text: event.key_event.text.clone(),
1082                    cursor: real_cursor,
1083                    anchor: real_anchor,
1084                    kind: UndoItemKind::TextInsert,
1085                });
1086
1087                self.as_ref().text.set(text.into());
1088                let new_cursor_pos = (insert_pos + event.key_event.text.len()) as i32;
1089                self.as_ref().anchor_position_byte_offset.set(new_cursor_pos);
1090                self.set_cursor_position(
1091                    new_cursor_pos,
1092                    true,
1093                    TextChangeNotify::TriggerCallbacks,
1094                    window_adapter,
1095                    self_rc,
1096                );
1097
1098                // Keep the cursor visible when inserting text. Blinking should only occur when
1099                // nothing is entered or the cursor isn't moved.
1100                self.as_ref().show_cursor(window_adapter);
1101
1102                Self::FIELD_OFFSETS.edited().apply_pin(self).call(&());
1103
1104                KeyEventResult::EventAccepted
1105            }
1106            KeyEventType::KeyReleased => {
1107                match Self::FIELD_OFFSETS
1108                    .key_released()
1109                    .apply_pin(self)
1110                    .call(&(event.key_event.clone(),))
1111                {
1112                    EventResult::Accept => KeyEventResult::EventAccepted,
1113                    EventResult::Reject => KeyEventResult::EventIgnored,
1114                }
1115            }
1116            KeyEventType::UpdateComposition | KeyEventType::CommitComposition => {
1117                if !self.accept_text_input(&event.key_event.text) {
1118                    return KeyEventResult::EventIgnored;
1119                }
1120
1121                let cursor = self.cursor_position(&self.text()) as i32;
1122                self.preedit_text.set(event.preedit_text.clone());
1123                self.preedit_selection.set(event.preedit_selection.clone().into());
1124
1125                if let Some(r) = &event.replacement_range {
1126                    // Set the selection so the call to insert erases it
1127                    self.anchor_position_byte_offset.set(cursor.saturating_add(r.start));
1128                    self.cursor_position_byte_offset.set(cursor.saturating_add(r.end));
1129                    if event.key_event.text.is_empty() {
1130                        self.delete_selection(
1131                            window_adapter,
1132                            self_rc,
1133                            if event.cursor_position.is_none() {
1134                                TextChangeNotify::TriggerCallbacks
1135                            } else {
1136                                // will be updated by the set_cursor_position later
1137                                TextChangeNotify::SkipCallbacks
1138                            },
1139                        );
1140                    }
1141                }
1142                self.insert(&event.key_event.text, window_adapter, self_rc);
1143                if let Some(cursor) = event.cursor_position {
1144                    self.anchor_position_byte_offset.set(event.anchor_position.unwrap_or(cursor));
1145                    self.set_cursor_position(
1146                        cursor,
1147                        true,
1148                        TextChangeNotify::TriggerCallbacks,
1149                        window_adapter,
1150                        self_rc,
1151                    );
1152                }
1153                KeyEventResult::EventAccepted
1154            }
1155        }
1156    }
1157
1158    fn focus_event(
1159        self: Pin<&Self>,
1160        event: &FocusEvent,
1161        window_adapter: &Rc<dyn WindowAdapter>,
1162        self_rc: &ItemRc,
1163    ) -> FocusEventResult {
1164        match event {
1165            FocusEvent::FocusIn(_reason) => {
1166                if !self.enabled() {
1167                    return FocusEventResult::FocusIgnored;
1168                }
1169                self.has_focus.set(true);
1170                self.show_cursor(window_adapter);
1171                WindowInner::from_pub(window_adapter.window()).set_text_input_focused(true);
1172                // FIXME: This should be tracked by a PropertyTracker in window and toggled when read_only() toggles.
1173                if !self.read_only() {
1174                    if let Some(w) = window_adapter.internal(crate::InternalToken) {
1175                        w.input_method_request(InputMethodRequest::Enable(
1176                            self.ime_properties(window_adapter, self_rc),
1177                        ));
1178                    }
1179
1180                    if cfg!(not(target_vendor = "apple")) && *_reason == FocusReason::TabNavigation
1181                    {
1182                        self.select_all(window_adapter, self_rc);
1183                    }
1184                }
1185            }
1186            FocusEvent::FocusOut(reason) => {
1187                self.has_focus.set(false);
1188                self.hide_cursor();
1189                if !matches!(reason, FocusReason::WindowActivation | FocusReason::PopupActivation) {
1190                    self.as_ref()
1191                        .anchor_position_byte_offset
1192                        .set(self.as_ref().cursor_position_byte_offset());
1193                }
1194                WindowInner::from_pub(window_adapter.window()).set_text_input_focused(false);
1195                if !self.read_only() {
1196                    // commit the preedit text on android
1197                    #[cfg(target_os = "android")]
1198                    {
1199                        let preedit_text = self.preedit_text();
1200                        if !preedit_text.is_empty() {
1201                            let mut text = String::from(self.text());
1202                            let cursor_position = self.cursor_position(&text);
1203                            text.insert_str(cursor_position, &preedit_text);
1204                            self.text.set(text.into());
1205                            let new_pos = (cursor_position + preedit_text.len()) as i32;
1206                            self.anchor_position_byte_offset.set(new_pos);
1207                            self.set_cursor_position(
1208                                new_pos,
1209                                false,
1210                                TextChangeNotify::TriggerCallbacks,
1211                                window_adapter,
1212                                self_rc,
1213                            );
1214                            Self::FIELD_OFFSETS.edited().apply_pin(self).call(&());
1215                        }
1216                    }
1217                    self.preedit_text.set(Default::default());
1218                }
1219            }
1220        }
1221        FocusEventResult::FocusAccepted
1222    }
1223
1224    fn render(
1225        self: Pin<&Self>,
1226        backend: &mut &mut dyn ItemRenderer,
1227        self_rc: &ItemRc,
1228        size: LogicalSize,
1229    ) -> RenderingResult {
1230        crate::properties::evaluate_no_tracking(|| {
1231            if self.has_focus() && self.text() != *backend.window().last_ime_text.borrow() {
1232                let window_adapter = &backend.window().window_adapter();
1233                if let Some(w) = window_adapter.internal(crate::InternalToken) {
1234                    w.input_method_request(InputMethodRequest::Update(
1235                        self.ime_properties(window_adapter, self_rc),
1236                    ));
1237                }
1238            }
1239        });
1240        (*backend).draw_text_input(self, self_rc, size);
1241        RenderingResult::ContinueRenderingChildren
1242    }
1243
1244    fn bounding_rect(
1245        self: core::pin::Pin<&Self>,
1246        _window_adapter: &Rc<dyn WindowAdapter>,
1247        _self_rc: &ItemRc,
1248        geometry: LogicalRect,
1249    ) -> LogicalRect {
1250        geometry
1251    }
1252
1253    fn clips_children(self: core::pin::Pin<&Self>) -> bool {
1254        false
1255    }
1256}
1257
1258impl ItemConsts for TextInput {
1259    const cached_rendering_data_offset: const_field_offset::FieldOffset<
1260        TextInput,
1261        CachedRenderingData,
1262    > = TextInput::FIELD_OFFSETS.cached_rendering_data().as_unpinned_projection();
1263}
1264
1265impl HasFont for TextInput {
1266    fn font_request(self: Pin<&Self>, self_rc: &crate::items::ItemRc) -> FontRequest {
1267        crate::items::WindowItem::resolved_font_request(
1268            self_rc,
1269            self.font_family(),
1270            self.font_weight(),
1271            self.font_size(),
1272            self.letter_spacing(),
1273            self.font_italic(),
1274        )
1275    }
1276}
1277
1278impl RenderString for TextInput {
1279    fn text(self: Pin<&Self>) -> PlainOrStyledText {
1280        PlainOrStyledText::Plain(self.as_ref().visual_representation(None).text.clone())
1281    }
1282}
1283
1284pub enum TextCursorDirection {
1285    Forward,
1286    Backward,
1287    ForwardByWord,
1288    BackwardByWord,
1289    NextLine,
1290    PreviousLine,
1291    /// breaks grapheme boundaries, so only used by delete-previous-char
1292    PreviousCharacter,
1293    StartOfLine,
1294    EndOfLine,
1295    /// These don't care about wrapping
1296    StartOfParagraph,
1297    EndOfParagraph,
1298    StartOfText,
1299    EndOfText,
1300    PageUp,
1301    PageDown,
1302}
1303
1304impl core::convert::TryFrom<char> for TextCursorDirection {
1305    type Error = ();
1306
1307    fn try_from(value: char) -> Result<Self, Self::Error> {
1308        Ok(match value {
1309            key_codes::LeftArrow => Self::Backward,
1310            key_codes::RightArrow => Self::Forward,
1311            key_codes::UpArrow => Self::PreviousLine,
1312            key_codes::DownArrow => Self::NextLine,
1313            key_codes::PageUp => Self::PageUp,
1314            key_codes::PageDown => Self::PageDown,
1315            // On macos this scrolls to the top or the bottom of the page
1316            #[cfg(not(target_os = "macos"))]
1317            key_codes::Home => Self::StartOfLine,
1318            #[cfg(not(target_os = "macos"))]
1319            key_codes::End => Self::EndOfLine,
1320            _ => return Err(()),
1321        })
1322    }
1323}
1324
1325#[derive(PartialEq)]
1326enum AnchorMode {
1327    KeepAnchor,
1328    MoveAnchor,
1329}
1330
1331impl From<KeyboardModifiers> for AnchorMode {
1332    fn from(modifiers: KeyboardModifiers) -> Self {
1333        if modifiers.shift { Self::KeepAnchor } else { Self::MoveAnchor }
1334    }
1335}
1336
1337/// Argument to [`TextInput::delete_selection`] that determines whether to trigger the
1338/// `edited` and cursor position callbacks and issue an input method request update.
1339#[derive(Copy, Clone, PartialEq, Eq)]
1340pub enum TextChangeNotify {
1341    /// Trigger the callbacks.
1342    TriggerCallbacks,
1343    /// Skip triggering the callbacks, as a subsequent operation will trigger them.
1344    SkipCallbacks,
1345}
1346
1347fn safe_byte_offset(unsafe_byte_offset: i32, text: &str) -> usize {
1348    if unsafe_byte_offset <= 0 {
1349        return 0;
1350    }
1351    text.ceil_char_boundary(unsafe_byte_offset as usize)
1352}
1353
1354/// This struct holds the fields needed for rendering a TextInput item after applying any
1355/// on-going composition. This way the renderer's don't have to duplicate the code for extracting
1356/// and applying the pre-edit text, cursor placement within, etc.
1357#[derive(Debug)]
1358pub struct TextInputVisualRepresentation {
1359    /// The text to be rendered including any pre-edit string
1360    pub text: SharedString,
1361    /// If set, this field specifies the range as byte offsets within the text field where the composition
1362    /// is in progress. Renderers typically provide visual feedback for the currently composed text, such as
1363    /// by using underlines.
1364    pub preedit_range: core::ops::Range<usize>,
1365    /// If set, specifies the range as byte offsets within the text where to draw the selection.
1366    pub selection_range: core::ops::Range<usize>,
1367    /// The position where to draw the cursor, as byte offset within the text.
1368    pub cursor_position: Option<usize>,
1369    /// The color of the (unselected) text
1370    pub text_color: Brush,
1371    /// The color of the blinking cursor
1372    pub cursor_color: Color,
1373    text_without_password: Option<SharedString>,
1374    password_character: char,
1375}
1376
1377impl TextInputVisualRepresentation {
1378    /// If the given `TextInput` renders a password, then all characters in this `TextInputVisualRepresentation` are replaced
1379    /// with the password character and the selection/preedit-ranges/cursor position are adjusted.
1380    /// If `password_character_fn` is Some, it is called lazily to query the password character, otherwise a default is used.
1381    fn apply_password_character_substitution(
1382        &mut self,
1383        text_input: Pin<&TextInput>,
1384        password_character_fn: Option<fn() -> char>,
1385    ) {
1386        if !matches!(text_input.input_type(), InputType::Password) {
1387            return;
1388        }
1389
1390        let password_character = password_character_fn.map_or('●', |f| f());
1391
1392        let text = &mut self.text;
1393        let fixup_range = |r: &mut core::ops::Range<usize>| {
1394            if !core::ops::Range::is_empty(r) {
1395                r.start = text[..r.start].chars().count() * password_character.len_utf8();
1396                r.end = text[..r.end].chars().count() * password_character.len_utf8();
1397            }
1398        };
1399        fixup_range(&mut self.preedit_range);
1400        fixup_range(&mut self.selection_range);
1401        if let Some(cursor_pos) = self.cursor_position.as_mut() {
1402            *cursor_pos = text[..*cursor_pos].chars().count() * password_character.len_utf8();
1403        }
1404        self.text_without_password = Some(core::mem::replace(
1405            text,
1406            core::iter::repeat_n(password_character, text.chars().count()).collect(),
1407        ));
1408        self.password_character = password_character;
1409    }
1410
1411    /// Use this function to make a byte offset in the visual text (used for rendering) back to a byte offset in the
1412    /// TextInput's text. The offsets might differ for example for password text input fields.
1413    pub fn map_byte_offset_from_visual_text_to_actual_text(&self, byte_offset: usize) -> usize {
1414        if let Some(text_without_password) = self.text_without_password.as_ref() {
1415            text_without_password
1416                .char_indices()
1417                .nth(byte_offset / self.password_character.len_utf8())
1418                .map_or(text_without_password.len(), |(r, _)| r)
1419        } else {
1420            byte_offset
1421        }
1422    }
1423
1424    /// Map the byte_offset inside the TextInput's text to the byte offset in the visual text.
1425    /// This is the opposite of `map_byte_offset_from_byte_offset_in_visual_text`.
1426    pub fn map_byte_offset_from_actual_to_visual_text(&self, byte_offset: usize) -> usize {
1427        if let Some(text_without_password) = self.text_without_password.as_ref() {
1428            text_without_password[..byte_offset].chars().count()
1429                * self.password_character.len_utf8()
1430        } else {
1431            byte_offset
1432        }
1433    }
1434}
1435
1436/// Whether the running process is the screenshot test driver. The text cursor color is
1437/// platform-dependent (see [`TextInput::visual_representation`]); in the screenshot tests we pick
1438/// the cross-platform color so the references match regardless of the host OS.
1439fn running_in_screenshot_test() -> bool {
1440    #[cfg(feature = "std")]
1441    {
1442        // Read the environment once: this is queried while drawing the cursor every frame.
1443        static IS_SCREENSHOT_TEST: std::sync::LazyLock<bool> = std::sync::LazyLock::new(|| {
1444            std::env::var_os("CARGO_PKG_NAME").is_some_and(|v| v == "test-driver-screenshots")
1445        });
1446        *IS_SCREENSHOT_TEST
1447    }
1448    #[cfg(not(feature = "std"))]
1449    {
1450        false
1451    }
1452}
1453
1454impl TextInput {
1455    fn show_cursor(&self, window_adapter: &Rc<dyn WindowAdapter>) {
1456        WindowInner::from_pub(window_adapter.window())
1457            .set_cursor_blink_binding(&self.cursor_visible);
1458    }
1459
1460    fn hide_cursor(&self) {
1461        self.cursor_visible.set(false);
1462    }
1463
1464    /// Moves the cursor (and/or anchor) and returns true if the cursor position changed; false otherwise.
1465    fn move_cursor(
1466        self: Pin<&Self>,
1467        direction: TextCursorDirection,
1468        anchor_mode: AnchorMode,
1469        trigger_callbacks: TextChangeNotify,
1470        window_adapter: &Rc<dyn WindowAdapter>,
1471        self_rc: &ItemRc,
1472    ) -> bool {
1473        let text = self.text();
1474        if text.is_empty() {
1475            return false;
1476        }
1477
1478        let (anchor, cursor) = self.selection_anchor_and_cursor();
1479        let last_cursor_pos = self.cursor_position(&text);
1480
1481        let mut grapheme_cursor =
1482            unicode_segmentation::GraphemeCursor::new(last_cursor_pos, text.len(), true);
1483
1484        let font_height = window_adapter.renderer().char_size(self, self_rc, ' ').height;
1485
1486        let mut reset_preferred_x_pos = true;
1487
1488        let new_cursor_pos = match direction {
1489            TextCursorDirection::Forward => {
1490                if anchor == cursor || anchor_mode == AnchorMode::KeepAnchor {
1491                    grapheme_cursor
1492                        .next_boundary(&text, 0)
1493                        .ok()
1494                        .flatten()
1495                        .unwrap_or_else(|| text.len())
1496                } else {
1497                    cursor
1498                }
1499            }
1500            TextCursorDirection::Backward => {
1501                if anchor == cursor || anchor_mode == AnchorMode::KeepAnchor {
1502                    grapheme_cursor.prev_boundary(&text, 0).ok().flatten().unwrap_or(0)
1503                } else {
1504                    anchor
1505                }
1506            }
1507            TextCursorDirection::NextLine => {
1508                reset_preferred_x_pos = false;
1509
1510                let cursor_rect =
1511                    self.cursor_rect_for_byte_offset(last_cursor_pos, window_adapter, self_rc);
1512                let mut cursor_xy_pos = cursor_rect.center();
1513
1514                cursor_xy_pos.y += font_height;
1515                cursor_xy_pos.x = self.preferred_x_pos.get();
1516                self.byte_offset_for_position(cursor_xy_pos, window_adapter, self_rc)
1517            }
1518            TextCursorDirection::PreviousLine => {
1519                reset_preferred_x_pos = false;
1520
1521                let cursor_rect =
1522                    self.cursor_rect_for_byte_offset(last_cursor_pos, window_adapter, self_rc);
1523                let mut cursor_xy_pos = cursor_rect.center();
1524
1525                cursor_xy_pos.y -= font_height;
1526                cursor_xy_pos.x = self.preferred_x_pos.get();
1527                self.byte_offset_for_position(cursor_xy_pos, window_adapter, self_rc)
1528            }
1529            TextCursorDirection::PreviousCharacter => {
1530                let mut i = last_cursor_pos;
1531                loop {
1532                    i = i.saturating_sub(1);
1533                    if text.is_char_boundary(i) {
1534                        break i;
1535                    }
1536                }
1537            }
1538            // Currently moving by word behaves like macos: next end of word(forward) or previous beginning of word(backward)
1539            TextCursorDirection::ForwardByWord => next_word_boundary(&text, last_cursor_pos + 1),
1540            TextCursorDirection::BackwardByWord => {
1541                prev_word_boundary(&text, last_cursor_pos.saturating_sub(1))
1542            }
1543            TextCursorDirection::StartOfLine => {
1544                let cursor_rect =
1545                    self.cursor_rect_for_byte_offset(last_cursor_pos, window_adapter, self_rc);
1546                let mut cursor_xy_pos = cursor_rect.center();
1547
1548                cursor_xy_pos.x = 0 as Coord;
1549                self.byte_offset_for_position(cursor_xy_pos, window_adapter, self_rc)
1550            }
1551            TextCursorDirection::EndOfLine => {
1552                let cursor_rect =
1553                    self.cursor_rect_for_byte_offset(last_cursor_pos, window_adapter, self_rc);
1554                let mut cursor_xy_pos = cursor_rect.center();
1555
1556                cursor_xy_pos.x = Coord::MAX;
1557                self.byte_offset_for_position(cursor_xy_pos, window_adapter, self_rc)
1558            }
1559            TextCursorDirection::StartOfParagraph => {
1560                prev_paragraph_boundary(&text, last_cursor_pos.saturating_sub(1))
1561            }
1562            TextCursorDirection::EndOfParagraph => {
1563                next_paragraph_boundary(&text, last_cursor_pos + 1)
1564            }
1565            TextCursorDirection::StartOfText => 0,
1566            TextCursorDirection::EndOfText => text.len(),
1567            TextCursorDirection::PageUp => {
1568                let offset = self.page_height().get() - font_height;
1569                if offset <= 0 as Coord {
1570                    return false;
1571                }
1572                reset_preferred_x_pos = false;
1573                let cursor_rect =
1574                    self.cursor_rect_for_byte_offset(last_cursor_pos, window_adapter, self_rc);
1575                let mut cursor_xy_pos = cursor_rect.center();
1576                cursor_xy_pos.y -= offset;
1577                cursor_xy_pos.x = self.preferred_x_pos.get();
1578                self.byte_offset_for_position(cursor_xy_pos, window_adapter, self_rc)
1579            }
1580            TextCursorDirection::PageDown => {
1581                let offset = self.page_height().get() - font_height;
1582                if offset <= 0 as Coord {
1583                    return false;
1584                }
1585                reset_preferred_x_pos = false;
1586                let cursor_rect =
1587                    self.cursor_rect_for_byte_offset(last_cursor_pos, window_adapter, self_rc);
1588                let mut cursor_xy_pos = cursor_rect.center();
1589                cursor_xy_pos.y += offset;
1590                cursor_xy_pos.x = self.preferred_x_pos.get();
1591                self.byte_offset_for_position(cursor_xy_pos, window_adapter, self_rc)
1592            }
1593        };
1594
1595        match anchor_mode {
1596            AnchorMode::KeepAnchor => {}
1597            AnchorMode::MoveAnchor => {
1598                self.as_ref().anchor_position_byte_offset.set(new_cursor_pos as i32);
1599            }
1600        }
1601        self.set_cursor_position(
1602            new_cursor_pos as i32,
1603            reset_preferred_x_pos,
1604            trigger_callbacks,
1605            window_adapter,
1606            self_rc,
1607        );
1608
1609        // Keep the cursor visible when moving. Blinking should only occur when
1610        // nothing is entered or the cursor isn't moved.
1611        self.as_ref().show_cursor(window_adapter);
1612
1613        new_cursor_pos != last_cursor_pos
1614    }
1615
1616    pub fn set_cursor_position(
1617        self: Pin<&Self>,
1618        new_position: i32,
1619        reset_preferred_x_pos: bool,
1620        trigger_callbacks: TextChangeNotify,
1621        window_adapter: &Rc<dyn WindowAdapter>,
1622        self_rc: &ItemRc,
1623    ) {
1624        self.cursor_position_byte_offset.set(new_position);
1625        if new_position >= 0 {
1626            let pos = self
1627                .cursor_rect_for_byte_offset(new_position as usize, window_adapter, self_rc)
1628                .origin;
1629            if reset_preferred_x_pos {
1630                self.preferred_x_pos.set(pos.x);
1631            }
1632            if trigger_callbacks == TextChangeNotify::TriggerCallbacks {
1633                Self::FIELD_OFFSETS
1634                    .cursor_position_changed()
1635                    .apply_pin(self)
1636                    .call(&(crate::api::LogicalPosition::from_euclid(pos),));
1637                self.update_ime(window_adapter, self_rc);
1638            }
1639        }
1640    }
1641
1642    fn update_ime(self: Pin<&Self>, window_adapter: &Rc<dyn WindowAdapter>, self_rc: &ItemRc) {
1643        if self.read_only() || !self.has_focus() {
1644            return;
1645        }
1646        if let Some(w) = window_adapter.internal(crate::InternalToken) {
1647            w.input_method_request(InputMethodRequest::Update(
1648                self.ime_properties(window_adapter, self_rc),
1649            ));
1650        }
1651    }
1652
1653    fn select_and_delete(
1654        self: Pin<&Self>,
1655        step: TextCursorDirection,
1656        window_adapter: &Rc<dyn WindowAdapter>,
1657        self_rc: &ItemRc,
1658    ) {
1659        if !self.has_selection() {
1660            self.move_cursor(
1661                step,
1662                AnchorMode::KeepAnchor,
1663                TextChangeNotify::SkipCallbacks,
1664                window_adapter,
1665                self_rc,
1666            );
1667        }
1668        self.delete_selection(window_adapter, self_rc, TextChangeNotify::TriggerCallbacks);
1669    }
1670
1671    pub fn delete_selection(
1672        self: Pin<&Self>,
1673        window_adapter: &Rc<dyn WindowAdapter>,
1674        self_rc: &ItemRc,
1675        trigger_callbacks: TextChangeNotify,
1676    ) {
1677        let text: String = self.text().into();
1678        if text.is_empty() {
1679            return;
1680        }
1681
1682        let (anchor, cursor) = self.selection_anchor_and_cursor();
1683        if anchor == cursor {
1684            return;
1685        }
1686
1687        let removed_text: SharedString = text[anchor..cursor].into();
1688        // save real anchor/cursor for undo/redo
1689        let (real_cursor, real_anchor) = {
1690            let text = self.text();
1691            (self.cursor_position(&text), self.anchor_position(&text))
1692        };
1693
1694        let text = [text.split_at(anchor).0, text.split_at(cursor).1].concat();
1695        self.text.set(text.into());
1696        self.anchor_position_byte_offset.set(anchor as i32);
1697
1698        self.add_undo_item(UndoItem {
1699            pos: anchor,
1700            text: removed_text,
1701            cursor: real_cursor,
1702            anchor: real_anchor,
1703            kind: UndoItemKind::TextRemove,
1704        });
1705
1706        if trigger_callbacks == TextChangeNotify::TriggerCallbacks {
1707            self.set_cursor_position(
1708                anchor as i32,
1709                true,
1710                trigger_callbacks,
1711                window_adapter,
1712                self_rc,
1713            );
1714            Self::FIELD_OFFSETS.edited().apply_pin(self).call(&());
1715        } else {
1716            self.cursor_position_byte_offset.set(anchor as i32);
1717        }
1718    }
1719
1720    pub fn anchor_position(self: Pin<&Self>, text: &str) -> usize {
1721        safe_byte_offset(self.anchor_position_byte_offset(), text)
1722    }
1723
1724    pub fn cursor_position(self: Pin<&Self>, text: &str) -> usize {
1725        safe_byte_offset(self.cursor_position_byte_offset(), text)
1726    }
1727
1728    fn ime_properties(
1729        self: Pin<&Self>,
1730        window_adapter: &Rc<dyn WindowAdapter>,
1731        self_rc: &ItemRc,
1732    ) -> InputMethodProperties {
1733        let text = self.text();
1734        WindowInner::from_pub(window_adapter.window()).last_ime_text.replace(text.clone());
1735        let cursor_position = self.cursor_position(&text);
1736        let anchor_position = self.anchor_position(&text);
1737        let cursor_relative =
1738            self.cursor_rect_for_byte_offset(cursor_position, window_adapter, self_rc);
1739        let geometry = self_rc.geometry();
1740        let origin = self_rc.map_to_native_window(geometry.origin);
1741        let origin_vector = origin.to_vector();
1742        let cursor_rect_origin =
1743            crate::api::LogicalPosition::from_euclid(cursor_relative.origin + origin_vector);
1744        let cursor_rect_size = crate::api::LogicalSize::from_euclid(cursor_relative.size);
1745        let anchor_point = crate::api::LogicalPosition::from_euclid(
1746            self.cursor_rect_for_byte_offset(anchor_position, window_adapter, self_rc).origin
1747                + origin_vector
1748                + cursor_relative.size,
1749        );
1750        let maybe_parent =
1751            self_rc.parent_item(crate::item_tree::ParentItemTraversalMode::StopAtPopups);
1752        let clip_rect = maybe_parent.map(|parent| {
1753            let geom = parent.geometry();
1754            LogicalRect::new(parent.map_to_native_window(geom.origin), geom.size)
1755        });
1756
1757        InputMethodProperties {
1758            text,
1759            cursor_position,
1760            anchor_position: (cursor_position != anchor_position).then_some(anchor_position),
1761            preedit_text: self.preedit_text(),
1762            preedit_offset: cursor_position,
1763            cursor_rect_origin,
1764            cursor_rect_size,
1765            anchor_point,
1766            input_type: self.input_type(),
1767            clip_rect,
1768        }
1769    }
1770
1771    // Avoid accessing self.cursor_position()/self.anchor_position() directly, always
1772    // use this bounds-checking function.
1773    pub fn selection_anchor_and_cursor(self: Pin<&Self>) -> (usize, usize) {
1774        let text = self.text();
1775        let cursor_pos = self.cursor_position(&text);
1776        let anchor_pos = self.anchor_position(&text);
1777
1778        if anchor_pos > cursor_pos {
1779            (cursor_pos as _, anchor_pos as _)
1780        } else {
1781            (anchor_pos as _, cursor_pos as _)
1782        }
1783    }
1784
1785    pub fn has_selection(self: Pin<&Self>) -> bool {
1786        let (anchor_pos, cursor_pos) = self.selection_anchor_and_cursor();
1787        anchor_pos != cursor_pos
1788    }
1789
1790    fn insert(
1791        self: Pin<&Self>,
1792        text_to_insert: &str,
1793        window_adapter: &Rc<dyn WindowAdapter>,
1794        self_rc: &ItemRc,
1795    ) {
1796        if text_to_insert.is_empty() {
1797            return;
1798        }
1799
1800        let (real_cursor, real_anchor) = {
1801            let text = self.text();
1802            (self.cursor_position(&text), self.anchor_position(&text))
1803        };
1804
1805        self.delete_selection(window_adapter, self_rc, TextChangeNotify::SkipCallbacks);
1806        let mut text: String = self.text().into();
1807        let cursor_pos = self.selection_anchor_and_cursor().1;
1808        let mut inserted_text: SharedString = text_to_insert.into();
1809        if text_to_insert.contains('\n') && self.single_line() {
1810            inserted_text = text_to_insert.replace('\n', " ").into();
1811            text.insert_str(cursor_pos, &inserted_text);
1812        } else {
1813            text.insert_str(cursor_pos, text_to_insert);
1814        }
1815
1816        self.add_undo_item(UndoItem {
1817            pos: cursor_pos,
1818            text: inserted_text,
1819            cursor: real_cursor,
1820            anchor: real_anchor,
1821            kind: UndoItemKind::TextInsert,
1822        });
1823
1824        let cursor_pos = cursor_pos + text_to_insert.len();
1825        self.text.set(text.into());
1826        self.anchor_position_byte_offset.set(cursor_pos as i32);
1827        self.set_cursor_position(
1828            cursor_pos as i32,
1829            true,
1830            TextChangeNotify::TriggerCallbacks,
1831            window_adapter,
1832            self_rc,
1833        );
1834        Self::FIELD_OFFSETS.edited().apply_pin(self).call(&());
1835    }
1836
1837    pub fn cut(self: Pin<&Self>, window_adapter: &Rc<dyn WindowAdapter>, self_rc: &ItemRc) {
1838        self.copy(window_adapter, self_rc);
1839        self.delete_selection(window_adapter, self_rc, TextChangeNotify::TriggerCallbacks);
1840    }
1841
1842    pub fn set_selection_offsets(
1843        self: Pin<&Self>,
1844        window_adapter: &Rc<dyn WindowAdapter>,
1845        self_rc: &ItemRc,
1846        start: i32,
1847        end: i32,
1848    ) {
1849        let text = self.text();
1850        let safe_start = safe_byte_offset(start, &text);
1851        let safe_end = safe_byte_offset(end, &text);
1852
1853        self.as_ref().anchor_position_byte_offset.set(safe_start as i32);
1854        self.set_cursor_position(
1855            safe_end as i32,
1856            true,
1857            TextChangeNotify::TriggerCallbacks,
1858            window_adapter,
1859            self_rc,
1860        );
1861    }
1862
1863    pub fn select_all(self: Pin<&Self>, window_adapter: &Rc<dyn WindowAdapter>, self_rc: &ItemRc) {
1864        self.move_cursor(
1865            TextCursorDirection::StartOfText,
1866            AnchorMode::MoveAnchor,
1867            TextChangeNotify::SkipCallbacks,
1868            window_adapter,
1869            self_rc,
1870        );
1871        self.move_cursor(
1872            TextCursorDirection::EndOfText,
1873            AnchorMode::KeepAnchor,
1874            TextChangeNotify::TriggerCallbacks,
1875            window_adapter,
1876            self_rc,
1877        );
1878    }
1879
1880    pub fn clear_selection(self: Pin<&Self>, _: &Rc<dyn WindowAdapter>, _: &ItemRc) {
1881        self.as_ref().anchor_position_byte_offset.set(self.as_ref().cursor_position_byte_offset());
1882    }
1883
1884    pub fn select_word(self: Pin<&Self>, window_adapter: &Rc<dyn WindowAdapter>, self_rc: &ItemRc) {
1885        let text = self.text();
1886        let anchor = self.anchor_position(&text);
1887        let cursor = self.cursor_position(&text);
1888        let (new_a, new_c) = if anchor <= cursor {
1889            (prev_word_boundary(&text, anchor), next_word_boundary(&text, cursor))
1890        } else {
1891            (next_word_boundary(&text, anchor), prev_word_boundary(&text, cursor))
1892        };
1893        self.as_ref().anchor_position_byte_offset.set(new_a as i32);
1894        self.set_cursor_position(
1895            new_c as i32,
1896            true,
1897            TextChangeNotify::TriggerCallbacks,
1898            window_adapter,
1899            self_rc,
1900        );
1901    }
1902
1903    fn select_paragraph(
1904        self: Pin<&Self>,
1905        window_adapter: &Rc<dyn WindowAdapter>,
1906        self_rc: &ItemRc,
1907    ) {
1908        let text = self.text();
1909        let anchor = self.anchor_position(&text);
1910        let cursor = self.cursor_position(&text);
1911        let (new_a, new_c) = if anchor <= cursor {
1912            (prev_paragraph_boundary(&text, anchor), next_paragraph_boundary(&text, cursor))
1913        } else {
1914            (next_paragraph_boundary(&text, anchor), prev_paragraph_boundary(&text, cursor))
1915        };
1916        self.as_ref().anchor_position_byte_offset.set(new_a as i32);
1917        self.set_cursor_position(
1918            new_c as i32,
1919            true,
1920            TextChangeNotify::TriggerCallbacks,
1921            window_adapter,
1922            self_rc,
1923        );
1924    }
1925
1926    pub fn copy(self: Pin<&Self>, w: &Rc<dyn WindowAdapter>, _: &ItemRc) {
1927        self.copy_clipboard(w, Clipboard::DefaultClipboard);
1928    }
1929
1930    fn copy_clipboard(
1931        self: Pin<&Self>,
1932        window_adapter: &Rc<dyn WindowAdapter>,
1933        clipboard: Clipboard,
1934    ) {
1935        let (anchor, cursor) = self.selection_anchor_and_cursor();
1936        if anchor == cursor {
1937            return;
1938        }
1939        let text = self.text();
1940
1941        WindowInner::from_pub(window_adapter.window())
1942            .context()
1943            .platform()
1944            .set_clipboard_text(&text[anchor..cursor], clipboard);
1945    }
1946
1947    pub fn paste(self: Pin<&Self>, window_adapter: &Rc<dyn WindowAdapter>, self_rc: &ItemRc) {
1948        self.paste_clipboard(window_adapter, self_rc, Clipboard::DefaultClipboard);
1949    }
1950
1951    fn paste_clipboard(
1952        self: Pin<&Self>,
1953        window_adapter: &Rc<dyn WindowAdapter>,
1954        self_rc: &ItemRc,
1955        clipboard: Clipboard,
1956    ) {
1957        if let Some(text) = WindowInner::from_pub(window_adapter.window())
1958            .context()
1959            .platform()
1960            .clipboard_text(clipboard)
1961        {
1962            self.preedit_text.set(Default::default());
1963            self.insert(&text, window_adapter, self_rc);
1964        }
1965    }
1966
1967    /// Returns a [`TextInputVisualRepresentation`] struct that contains all the fields necessary for rendering the text input,
1968    /// after making adjustments such as applying a substitution of characters for password input fields, or making sure
1969    /// that the selection start is always less or equal than the selection end.
1970    pub fn visual_representation(
1971        self: Pin<&Self>,
1972        password_character_fn: Option<fn() -> char>,
1973    ) -> TextInputVisualRepresentation {
1974        let mut text = self.text();
1975
1976        let preedit_text = self.preedit_text();
1977        let (preedit_range, selection_range, cursor_position) = if !preedit_text.is_empty() {
1978            let cursor_position = self.cursor_position(&text);
1979
1980            text =
1981                [&text[..cursor_position], &preedit_text, &text[cursor_position..]].concat().into();
1982            let preedit_range = cursor_position..cursor_position + preedit_text.len();
1983
1984            if let Some(preedit_sel) = self.preedit_selection().as_option() {
1985                let preedit_selection = cursor_position + preedit_sel.start as usize
1986                    ..cursor_position + preedit_sel.end as usize;
1987                (preedit_range, preedit_selection, Some(cursor_position + preedit_sel.end as usize))
1988            } else {
1989                let cur = preedit_range.end;
1990                (preedit_range, cur..cur, None)
1991            }
1992        } else {
1993            let preedit_range = Default::default();
1994            let (selection_anchor_pos, selection_cursor_pos) = self.selection_anchor_and_cursor();
1995            let selection_range = selection_anchor_pos..selection_cursor_pos;
1996            let cursor_position = self.cursor_position(&text);
1997            let cursor_visible = self.cursor_visible() && self.enabled();
1998            let cursor_position = if cursor_visible && selection_range.is_empty() {
1999                Some(cursor_position)
2000            } else {
2001                None
2002            };
2003            (preedit_range, selection_range, cursor_position)
2004        };
2005
2006        let text_color = self.color();
2007
2008        let cursor_color = if cfg!(any(target_os = "android", target_vendor = "apple"))
2009            && !running_in_screenshot_test()
2010        {
2011            if cursor_position.is_some() {
2012                self.selection_background_color().with_alpha(1.)
2013            } else {
2014                Default::default()
2015            }
2016        } else {
2017            // Other platforms (and the screenshot tests, so references match regardless of host OS)
2018            // draw the cursor in the text color.
2019            text_color.color()
2020        };
2021
2022        let mut repr = TextInputVisualRepresentation {
2023            text,
2024            preedit_range,
2025            selection_range,
2026            cursor_position,
2027            text_without_password: None,
2028            password_character: Default::default(),
2029            text_color,
2030            cursor_color,
2031        };
2032        repr.apply_password_character_substitution(self, password_character_fn);
2033        repr
2034    }
2035
2036    fn cursor_rect_for_byte_offset(
2037        self: Pin<&Self>,
2038        byte_offset: usize,
2039        window_adapter: &Rc<dyn WindowAdapter>,
2040        self_rc: &ItemRc,
2041    ) -> LogicalRect {
2042        window_adapter.renderer().text_input_cursor_rect_for_byte_offset(self, self_rc, byte_offset)
2043    }
2044
2045    pub fn byte_offset_for_position(
2046        self: Pin<&Self>,
2047        pos: LogicalPoint,
2048        window_adapter: &Rc<dyn WindowAdapter>,
2049        self_rc: &ItemRc,
2050    ) -> usize {
2051        window_adapter.renderer().text_input_byte_offset_for_position(self, self_rc, pos)
2052    }
2053
2054    /// When pressing the mouse (or releasing the finger, on android) we should take the focus if we don't have it already.
2055    /// 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
2056    fn ensure_focus_and_ime(
2057        self: Pin<&Self>,
2058        window_adapter: &Rc<dyn WindowAdapter>,
2059        self_rc: &ItemRc,
2060    ) {
2061        if !self.has_focus() {
2062            WindowInner::from_pub(window_adapter.window()).set_focus_item(
2063                self_rc,
2064                true,
2065                FocusReason::PointerClick,
2066            );
2067        } else if !self.read_only()
2068            && let Some(w) = window_adapter.internal(crate::InternalToken)
2069        {
2070            w.input_method_request(InputMethodRequest::Enable(
2071                self.ime_properties(window_adapter, self_rc),
2072            ));
2073        }
2074    }
2075
2076    fn add_undo_item(self: Pin<&Self>, item: UndoItem) {
2077        let mut items = self.undo_items.take();
2078        // try to merge with the last item
2079        if let Some(last) = items.make_mut_slice().last_mut() {
2080            match (&item.kind, &last.kind) {
2081                (UndoItemKind::TextInsert, UndoItemKind::TextInsert) => {
2082                    let is_new_line = item.text == "\n";
2083                    let last_is_new_line = last.text == "\n";
2084                    // if the last item or current item is a new_line
2085                    // we insert it as a standalone item, no merging
2086                    if item.pos == last.pos + last.text.len() && !is_new_line && !last_is_new_line {
2087                        last.text += &item.text;
2088                    } else {
2089                        items.push(item);
2090                    }
2091                }
2092                (UndoItemKind::TextRemove, UndoItemKind::TextRemove)
2093                    if item.pos + item.text.len() == last.pos =>
2094                {
2095                    last.pos = item.pos;
2096                    let old_text = last.text.clone();
2097                    last.text = item.text;
2098                    last.text += &old_text;
2099                    // prepend
2100                }
2101                _ => {
2102                    items.push(item);
2103                }
2104            }
2105        } else {
2106            items.push(item);
2107        }
2108
2109        self.undo_items.set(items);
2110    }
2111
2112    pub fn undo(self: Pin<&Self>, window_adapter: &Rc<dyn WindowAdapter>, self_rc: &ItemRc) {
2113        let mut items = self.undo_items.take();
2114        let Some(last) = items.pop() else {
2115            return;
2116        };
2117
2118        match last.kind {
2119            UndoItemKind::TextInsert => {
2120                let text: String = self.text().into();
2121                let text = [text.split_at(last.pos).0, text.split_at(last.pos + last.text.len()).1]
2122                    .concat();
2123                self.text.set(text.into());
2124
2125                self.anchor_position_byte_offset.set(last.anchor as i32);
2126                self.set_cursor_position(
2127                    last.cursor as i32,
2128                    true,
2129                    TextChangeNotify::TriggerCallbacks,
2130                    window_adapter,
2131                    self_rc,
2132                );
2133            }
2134            UndoItemKind::TextRemove => {
2135                let mut text: String = self.text().into();
2136                text.insert_str(last.pos, &last.text);
2137                self.text.set(text.into());
2138
2139                self.anchor_position_byte_offset.set(last.anchor as i32);
2140                self.set_cursor_position(
2141                    last.cursor as i32,
2142                    true,
2143                    TextChangeNotify::TriggerCallbacks,
2144                    window_adapter,
2145                    self_rc,
2146                );
2147            }
2148        }
2149        self.undo_items.set(items);
2150
2151        let mut redo = self.redo_items.take();
2152        redo.push(last);
2153        self.redo_items.set(redo);
2154        Self::FIELD_OFFSETS.edited().apply_pin(self).call(&());
2155    }
2156
2157    pub fn redo(self: Pin<&Self>, window_adapter: &Rc<dyn WindowAdapter>, self_rc: &ItemRc) {
2158        let mut items = self.redo_items.take();
2159        let Some(last) = items.pop() else {
2160            return;
2161        };
2162
2163        match last.kind {
2164            UndoItemKind::TextInsert => {
2165                let mut text: String = self.text().into();
2166                text.insert_str(last.pos, &last.text);
2167                self.text.set(text.into());
2168
2169                self.anchor_position_byte_offset.set(last.anchor as i32);
2170                self.set_cursor_position(
2171                    last.cursor as i32,
2172                    true,
2173                    TextChangeNotify::TriggerCallbacks,
2174                    window_adapter,
2175                    self_rc,
2176                );
2177            }
2178            UndoItemKind::TextRemove => {
2179                let text: String = self.text().into();
2180                let text = [text.split_at(last.pos).0, text.split_at(last.pos + last.text.len()).1]
2181                    .concat();
2182                self.text.set(text.into());
2183
2184                self.anchor_position_byte_offset.set(last.anchor as i32);
2185                self.set_cursor_position(
2186                    last.cursor as i32,
2187                    true,
2188                    TextChangeNotify::TriggerCallbacks,
2189                    window_adapter,
2190                    self_rc,
2191                );
2192            }
2193        }
2194
2195        self.redo_items.set(items);
2196
2197        let mut undo_items = self.undo_items.take();
2198        undo_items.push(last);
2199        self.undo_items.set(undo_items);
2200        Self::FIELD_OFFSETS.edited().apply_pin(self).call(&());
2201    }
2202
2203    pub fn font_metrics(
2204        self: Pin<&Self>,
2205        window_adapter: &Rc<dyn WindowAdapter>,
2206        self_rc: &ItemRc,
2207    ) -> FontMetrics {
2208        let font_request = self.font_request(self_rc);
2209        window_adapter.renderer().font_metrics(font_request)
2210    }
2211
2212    fn accept_text_input(self: Pin<&Self>, text_to_insert: &str) -> bool {
2213        let input_type = self.input_type();
2214
2215        match input_type {
2216            InputType::Number => return text_to_insert.chars().all(|ch| ch.is_ascii_digit()),
2217            InputType::Decimal => {
2218                let (a, c) = self.selection_anchor_and_cursor();
2219                let current = self.text();
2220                let candidate = [&current[..a], text_to_insert, &current[c..]].concat();
2221
2222                // Allow localized ".", "-", "-." because otherwise the cannot start entering
2223                if candidate.len() <= 2
2224                    && crate::context::GLOBAL_CONTEXT.with(|ctx| {
2225                        let sep =
2226                            ctx.get().map(|ctx| ctx.locale_decimal_separator()).unwrap_or('.');
2227                        let mut it = candidate.chars();
2228                        match (it.next(), it.next()) {
2229                            (Some('-'), None) => true,
2230                            (Some('-'), Some(c2)) => c2 == sep,
2231                            (Some(c1), None) => c1 == sep,
2232                            _ => false,
2233                        }
2234                    })
2235                {
2236                    return true;
2237                }
2238                return string_to_float(&candidate).is_some();
2239            }
2240            InputType::Password | InputType::Text | InputType::Search => (),
2241        }
2242
2243        true
2244    }
2245}
2246
2247fn next_paragraph_boundary(text: &str, last_cursor_pos: usize) -> usize {
2248    text.as_bytes()
2249        .iter()
2250        .enumerate()
2251        .skip(last_cursor_pos)
2252        .find(|(_, c)| **c == b'\n')
2253        .map(|(new_pos, _)| new_pos)
2254        .unwrap_or(text.len())
2255}
2256
2257fn prev_paragraph_boundary(text: &str, last_cursor_pos: usize) -> usize {
2258    text.as_bytes()
2259        .iter()
2260        .enumerate()
2261        .rev()
2262        .skip(text.len() - last_cursor_pos)
2263        .find(|(_, c)| **c == b'\n')
2264        .map(|(new_pos, _)| new_pos + 1)
2265        .unwrap_or(0)
2266}
2267
2268fn prev_word_boundary(text: &str, last_cursor_pos: usize) -> usize {
2269    let mut word_offset = 0;
2270
2271    for (current_word_offset, _) in text.unicode_word_indices() {
2272        if current_word_offset <= last_cursor_pos {
2273            word_offset = current_word_offset;
2274        } else {
2275            break;
2276        }
2277    }
2278
2279    word_offset
2280}
2281
2282fn next_word_boundary(text: &str, last_cursor_pos: usize) -> usize {
2283    text.unicode_word_indices()
2284        .find(|(offset, slice)| *offset + slice.len() >= last_cursor_pos)
2285        .map_or(text.len(), |(offset, slice)| offset + slice.len())
2286}
2287
2288#[cfg(feature = "ffi")]
2289#[unsafe(no_mangle)]
2290pub unsafe extern "C" fn slint_textinput_set_selection_offsets(
2291    text_input: Pin<&TextInput>,
2292    window_adapter: *const crate::window::ffi::WindowAdapterRcOpaque,
2293    self_component: &vtable::VRc<crate::item_tree::ItemTreeVTable>,
2294    self_index: u32,
2295    start: i32,
2296    end: i32,
2297) {
2298    unsafe {
2299        let window_adapter = &*(window_adapter as *const Rc<dyn WindowAdapter>);
2300        let self_rc = ItemRc::new(self_component.clone(), self_index);
2301        text_input.set_selection_offsets(window_adapter, &self_rc, start, end);
2302    }
2303}
2304
2305#[cfg(feature = "ffi")]
2306#[unsafe(no_mangle)]
2307pub unsafe extern "C" fn slint_textinput_select_all(
2308    text_input: Pin<&TextInput>,
2309    window_adapter: *const crate::window::ffi::WindowAdapterRcOpaque,
2310    self_component: &vtable::VRc<crate::item_tree::ItemTreeVTable>,
2311    self_index: u32,
2312) {
2313    unsafe {
2314        let window_adapter = &*(window_adapter as *const Rc<dyn WindowAdapter>);
2315        let self_rc = ItemRc::new(self_component.clone(), self_index);
2316        text_input.select_all(window_adapter, &self_rc);
2317    }
2318}
2319
2320#[cfg(feature = "ffi")]
2321#[unsafe(no_mangle)]
2322pub unsafe extern "C" fn slint_textinput_clear_selection(
2323    text_input: Pin<&TextInput>,
2324    window_adapter: *const crate::window::ffi::WindowAdapterRcOpaque,
2325    self_component: &vtable::VRc<crate::item_tree::ItemTreeVTable>,
2326    self_index: u32,
2327) {
2328    unsafe {
2329        let window_adapter = &*(window_adapter as *const Rc<dyn WindowAdapter>);
2330        let self_rc = ItemRc::new(self_component.clone(), self_index);
2331        text_input.clear_selection(window_adapter, &self_rc);
2332    }
2333}
2334
2335#[cfg(feature = "ffi")]
2336#[unsafe(no_mangle)]
2337pub unsafe extern "C" fn slint_textinput_cut(
2338    text_input: Pin<&TextInput>,
2339    window_adapter: *const crate::window::ffi::WindowAdapterRcOpaque,
2340    self_component: &vtable::VRc<crate::item_tree::ItemTreeVTable>,
2341    self_index: u32,
2342) {
2343    unsafe {
2344        let window_adapter = &*(window_adapter as *const Rc<dyn WindowAdapter>);
2345        let self_rc = ItemRc::new(self_component.clone(), self_index);
2346        text_input.cut(window_adapter, &self_rc);
2347    }
2348}
2349
2350#[cfg(feature = "ffi")]
2351#[unsafe(no_mangle)]
2352pub unsafe extern "C" fn slint_textinput_copy(
2353    text_input: Pin<&TextInput>,
2354    window_adapter: *const crate::window::ffi::WindowAdapterRcOpaque,
2355    self_component: &vtable::VRc<crate::item_tree::ItemTreeVTable>,
2356    self_index: u32,
2357) {
2358    unsafe {
2359        let window_adapter = &*(window_adapter as *const Rc<dyn WindowAdapter>);
2360        let self_rc = ItemRc::new(self_component.clone(), self_index);
2361        text_input.copy(window_adapter, &self_rc);
2362    }
2363}
2364
2365#[cfg(feature = "ffi")]
2366#[unsafe(no_mangle)]
2367pub unsafe extern "C" fn slint_textinput_paste(
2368    text_input: Pin<&TextInput>,
2369    window_adapter: *const crate::window::ffi::WindowAdapterRcOpaque,
2370    self_component: &vtable::VRc<crate::item_tree::ItemTreeVTable>,
2371    self_index: u32,
2372) {
2373    unsafe {
2374        let window_adapter = &*(window_adapter as *const Rc<dyn WindowAdapter>);
2375        let self_rc = ItemRc::new(self_component.clone(), self_index);
2376        text_input.paste(window_adapter, &self_rc);
2377    }
2378}
2379
2380#[cfg(feature = "ffi")]
2381#[unsafe(no_mangle)]
2382pub unsafe extern "C" fn slint_textinput_undo(
2383    text_input: Pin<&TextInput>,
2384    window_adapter: *const crate::window::ffi::WindowAdapterRcOpaque,
2385    self_component: &vtable::VRc<crate::item_tree::ItemTreeVTable>,
2386    self_index: u32,
2387) {
2388    unsafe {
2389        let window_adapter = &*(window_adapter as *const Rc<dyn WindowAdapter>);
2390        let self_rc = ItemRc::new(self_component.clone(), self_index);
2391        text_input.undo(window_adapter, &self_rc);
2392    }
2393}
2394
2395#[cfg(feature = "ffi")]
2396#[unsafe(no_mangle)]
2397pub unsafe extern "C" fn slint_textinput_redo(
2398    text_input: Pin<&TextInput>,
2399    window_adapter: *const crate::window::ffi::WindowAdapterRcOpaque,
2400    self_component: &vtable::VRc<crate::item_tree::ItemTreeVTable>,
2401    self_index: u32,
2402) {
2403    unsafe {
2404        let window_adapter = &*(window_adapter as *const Rc<dyn WindowAdapter>);
2405        let self_rc = ItemRc::new(self_component.clone(), self_index);
2406        text_input.redo(window_adapter, &self_rc);
2407    }
2408}
2409
2410pub fn slint_text_item_fontmetrics(
2411    window_adapter: &Rc<dyn WindowAdapter>,
2412    item_ref: Pin<ItemRef<'_>>,
2413    self_rc: &ItemRc,
2414) -> FontMetrics {
2415    if let Some(simple_text) = ItemRef::downcast_pin::<SimpleText>(item_ref) {
2416        simple_text.font_metrics(window_adapter, self_rc)
2417    } else if let Some(complex_text) = ItemRef::downcast_pin::<ComplexText>(item_ref) {
2418        complex_text.font_metrics(window_adapter, self_rc)
2419    } else if let Some(text_input) = ItemRef::downcast_pin::<TextInput>(item_ref) {
2420        text_input.font_metrics(window_adapter, self_rc)
2421    } else {
2422        Default::default()
2423    }
2424}
2425
2426#[cfg(feature = "ffi")]
2427#[unsafe(no_mangle)]
2428pub unsafe extern "C" fn slint_cpp_text_item_fontmetrics(
2429    window_adapter: *const crate::window::ffi::WindowAdapterRcOpaque,
2430    self_component: &vtable::VRc<crate::item_tree::ItemTreeVTable>,
2431    self_index: u32,
2432) -> FontMetrics {
2433    unsafe {
2434        let window_adapter = &*(window_adapter as *const Rc<dyn WindowAdapter>);
2435        let self_rc = ItemRc::new(self_component.clone(), self_index);
2436        let self_ref = self_rc.borrow();
2437        slint_text_item_fontmetrics(window_adapter, self_ref, &self_rc)
2438    }
2439}