1use 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#[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#[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#[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
641fn 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
655fn 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 match orientation {
673 Orientation::Horizontal => {
674 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 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)]
721struct 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#[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 preedit_selection: Property<PreEditSelection>,
799 pub cached_rendering_data: CachedRenderingData,
800 preferred_x_pos: Cell<Coord>,
803 pressed: Cell<u8>,
805 undo_items: Cell<SharedVector<UndoItem>>,
806 redo_items: Cell<SharedVector<UndoItem>>,
807 internal_text: Cell<SharedString>,
811 text_change_tracker: crate::properties::ChangeTracker,
813}
814
815impl Item for TextInput {
816 fn init(self: Pin<&Self>, self_rc: &ItemRc) {
817 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 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 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 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 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 if event.key_event.text.is_empty()
1079 || event.key_event.text.as_str().chars().any(|ch| {
1080 ('\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 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 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 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 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 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 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 #[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 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 PreviousCharacter,
1360 StartOfLine,
1361 EndOfLine,
1362 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 #[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#[repr(C)]
1395#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
1396pub enum TextCursorAffinity {
1397 #[default]
1399 NextCharacter,
1400 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#[derive(Copy, Clone, PartialEq, Eq)]
1420pub enum TextChangeNotify {
1421 TriggerCallbacks,
1423 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
1434fn 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#[derive(Debug)]
1452pub struct TextInputVisualRepresentation {
1453 pub text: SharedString,
1455 pub preedit_range: core::ops::Range<usize>,
1459 pub selection_range: core::ops::Range<usize>,
1461 pub cursor_position: Option<usize>,
1463 pub cursor_affinity: TextCursorAffinity,
1465 pub text_color: Brush,
1467 pub cursor_color: Color,
1469 text_without_password: Option<SharedString>,
1470}
1471
1472pub(crate) const PASSWORD_CHARACTER: char = '\u{25cf}';
1475
1476pub(crate) fn mask_password(text: &str) -> SharedString {
1478 core::iter::repeat_n(PASSWORD_CHARACTER, text.chars().count()).collect()
1479}
1480
1481impl TextInputVisualRepresentation {
1482 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 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 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
1528fn 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 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 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 self.as_ref().show_cursor(window_adapter);
1673
1674 moved
1675 }
1676
1677 fn set_text_internal(self: Pin<&Self>, text: SharedString) {
1680 self.internal_text.set(text.clone());
1681 self.text.set(text);
1682 }
1683
1684 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 }
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 = [¤t[..a], text_to_insert, ¤t[c..]].concat();
2406
2407 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}