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