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 | /*! |
5 | This module contains the builtin text related items. |
6 | |
7 | When adding an item or a property, it needs to be kept in sync with different place. |
8 | Lookup the [`crate::items`] module documentation. |
9 | */ |
10 | use 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 | }; |
16 | use crate::graphics::{Brush, Color, FontRequest}; |
17 | use crate::input::{ |
18 | key_codes, FocusEvent, FocusEventResult, InputEventFilterResult, InputEventResult, KeyEvent, |
19 | KeyboardModifiers, MouseEvent, StandardShortcut, TextShortcut, |
20 | }; |
21 | use crate::item_rendering::{CachedRenderingData, ItemRenderer, RenderText}; |
22 | use crate::layout::{LayoutInfo, Orientation}; |
23 | use crate::lengths::{ |
24 | LogicalLength, LogicalPoint, LogicalRect, LogicalSize, ScaleFactor, SizeLengths, |
25 | }; |
26 | use crate::platform::Clipboard; |
27 | #[cfg (feature = "rtti" )] |
28 | use crate::rtti::*; |
29 | use crate::window::{InputMethodProperties, InputMethodRequest, WindowAdapter, WindowInner}; |
30 | use crate::{Callback, Coord, Property, SharedString, SharedVector}; |
31 | use alloc::rc::Rc; |
32 | use alloc::string::String; |
33 | use const_field_offset::FieldOffsets; |
34 | use core::cell::Cell; |
35 | use core::pin::Pin; |
36 | #[allow (unused)] |
37 | use euclid::num::Ceil; |
38 | use i_slint_core_macros::*; |
39 | use unicode_segmentation::UnicodeSegmentation; |
40 | |
41 | /// The implementation of the `Text` element |
42 | #[repr (C)] |
43 | #[derive (FieldOffsets, Default, SlintElement)] |
44 | #[pin] |
45 | pub 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 | |
66 | impl 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 | |
142 | impl 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 | |
149 | impl 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 | |
218 | impl 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] |
231 | pub 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 | |
244 | impl 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 | |
320 | impl 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 | |
327 | impl 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 | |
389 | impl 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 | |
398 | fn 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 |
459 | struct PreEditSelection { |
460 | valid: bool, |
461 | start: i32, |
462 | end: i32, |
463 | } |
464 | |
465 | impl 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 | |
471 | impl 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)] |
479 | enum UndoItemKind { |
480 | TextInsert, |
481 | TextRemove, |
482 | } |
483 | |
484 | #[repr (C)] |
485 | #[derive (Clone)] |
486 | struct 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] |
498 | pub 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 | |
541 | impl 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 | |
1013 | impl 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 | |
1020 | pub 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 | |
1040 | impl 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)] |
1062 | enum AnchorMode { |
1063 | KeepAnchor, |
1064 | MoveAnchor, |
1065 | } |
1066 | |
1067 | impl 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)] |
1080 | pub enum TextChangeNotify { |
1081 | /// Trigger the callbacks. |
1082 | TriggerCallbacks, |
1083 | /// Skip triggering the callbacks, as a subsequent operation will trigger them. |
1084 | SkipCallbacks, |
1085 | } |
1086 | |
1087 | fn 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)] |
1111 | pub 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 | |
1130 | impl 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 | |
1178 | impl 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 | |
1975 | fn 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 | |
1985 | fn 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 | |
1996 | fn 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 | |
2010 | fn 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 ] |
2018 | pub 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 ] |
2033 | pub 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 ] |
2046 | pub 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 ] |
2059 | pub 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 ] |
2072 | pub 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 ] |
2085 | pub 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 | |
2096 | pub 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 ] |
2113 | pub 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 | |