Skip to main content

i_slint_core/items/
text.rs

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