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