1use std::ops::Range;
8
9use glyphon::{
10 Cursor, Edit,
11 cosmic_text::{self, Selection},
12};
13use tessera_platform::clipboard;
14use tessera_ui::{
15 CallbackWith, Color, ComputedData, Dp, FocusRequester, LayoutResult, MeasurementError, Px,
16 PxPosition, State, current_frame_nanos,
17 layout::{MeasureScope, layout},
18 receive_frame_nanos, remember, tessera,
19 time::Instant,
20 winit,
21};
22use winit::keyboard::NamedKey;
23
24use crate::{
25 pipelines::{
26 shape::command::ShapeCommand,
27 text::{
28 command::{TextCommand, TextConstraint},
29 pipeline::{TextData, color_to_glyphon, shaping_fonts},
30 },
31 },
32 theme::TextStyle,
33};
34
35const CURSOR_WIDTH: Dp = Dp(2.5);
37
38fn push_rect(fragment: &mut tessera_ui::RenderFragment, rect: &RectDef, color: Color) {
39 if rect.width <= Px::ZERO || rect.height <= Px::ZERO {
40 return;
41 }
42 let index = fragment.push_draw_command(ShapeCommand::Rect {
43 color,
44 corner_radii: glam::Vec4::ZERO.into(),
45 corner_g2: [3.0; 4],
46 });
47 if let Some(op) = fragment.ops_mut().get_mut(index as usize) {
48 op.position_override = Some(PxPosition::new(rect.x, rect.y));
49 op.size_override = Some(tessera_ui::PxSize::new(rect.width, rect.height));
50 }
51}
52
53fn push_highlight_rects(
54 fragment: &mut tessera_ui::RenderFragment,
55 rects: &[RectDef],
56 color: Color,
57) {
58 for rect in rects {
59 push_rect(fragment, rect, color);
60 }
61}
62
63#[derive(Clone, PartialEq)]
66pub struct TransformedText {
67 text: String,
68 raw_boundaries: Vec<usize>,
69 transformed_boundaries: Vec<usize>,
70 raw_to_transformed: Vec<usize>,
71 transformed_to_raw: Vec<usize>,
72}
73
74impl TransformedText {
75 pub fn from_strings(raw: &str, text: String) -> Self {
78 let raw_boundaries = char_boundary_offsets(raw);
79 let transformed_boundaries = char_boundary_offsets(&text);
80
81 let raw_to_transformed = if raw == text {
82 raw_boundaries.clone()
83 } else if raw_boundaries.len() == transformed_boundaries.len() {
84 transformed_boundaries.clone()
85 } else if let Some(mapping) = subsequence_boundary_mapping(raw, &text) {
86 mapping
87 } else {
88 raw_boundaries
89 .iter()
90 .enumerate()
91 .map(|(index, _)| {
92 let transformed_index =
93 index.min(transformed_boundaries.len().saturating_sub(1));
94 transformed_boundaries[transformed_index]
95 })
96 .collect()
97 };
98
99 let transformed_to_raw = transformed_boundaries
100 .iter()
101 .map(|&transformed_offset| {
102 let raw_index = raw_to_transformed
103 .partition_point(|&mapped_offset| mapped_offset <= transformed_offset)
104 .saturating_sub(1);
105 raw_boundaries[raw_index]
106 })
107 .collect();
108
109 Self {
110 text,
111 raw_boundaries,
112 transformed_boundaries,
113 raw_to_transformed,
114 transformed_to_raw,
115 }
116 }
117
118 pub fn identity(text: String) -> Self {
120 let boundaries = char_boundary_offsets(&text);
121 Self {
122 text,
123 raw_boundaries: boundaries.clone(),
124 transformed_boundaries: boundaries.clone(),
125 raw_to_transformed: boundaries.clone(),
126 transformed_to_raw: boundaries,
127 }
128 }
129
130 fn text(&self) -> &str {
131 &self.text
132 }
133
134 pub(crate) fn map_from_raw(&self, raw_offset: usize) -> usize {
135 let index = boundary_index_for_offset(&self.raw_boundaries, raw_offset);
136 self.raw_to_transformed[index]
137 }
138
139 #[allow(dead_code)]
140 fn map_to_raw(&self, transformed_offset: usize) -> usize {
141 let index = boundary_index_for_offset(&self.transformed_boundaries, transformed_offset);
142 self.transformed_to_raw[index]
143 }
144
145 fn map_to_raw_forward(&self, transformed_offset: usize) -> usize {
146 let index = self
147 .raw_to_transformed
148 .partition_point(|&mapped_offset| mapped_offset < transformed_offset)
149 .min(self.raw_boundaries.len().saturating_sub(1));
150 self.raw_boundaries[index]
151 }
152}
153
154pub type DisplayTransform = CallbackWith<String, TransformedText>;
157
158type CachedLayout = (Vec<RectDef>, Vec<RectDef>, Option<RectDef>, ComputedData);
159
160struct DerivedGeometryInput {
161 scroll_horizontal: Px,
162 cursor_offset: usize,
163 selection: Option<TextSelection>,
164 composition_range: Option<Range<usize>>,
165 raw_composition: Option<(Cursor, Cursor)>,
166}
167
168struct PointerActionContext<'a> {
169 raw_text: &'a str,
170 cursor_offset: usize,
171 selection: Option<TextSelection>,
172 drag_selection_mode: DragSelectionMode,
173 drag_origin_selection: Option<Range<usize>>,
174 action: glyphon::Action,
175}
176
177fn char_boundary_offsets(text: &str) -> Vec<usize> {
178 let mut boundaries = Vec::with_capacity(text.chars().count() + 1);
179 boundaries.push(0);
180 for (index, character) in text.char_indices() {
181 boundaries.push(index + character.len_utf8());
182 }
183 boundaries
184}
185
186fn subsequence_boundary_mapping(raw: &str, transformed: &str) -> Option<Vec<usize>> {
187 let raw_chars: Vec<char> = raw.chars().collect();
188 let transformed_chars: Vec<char> = transformed.chars().collect();
189 let transformed_boundaries = char_boundary_offsets(transformed);
190 if raw_chars.is_empty() {
191 return Some(vec![transformed.len()]);
192 }
193 let mut matched_indices = Vec::with_capacity(raw_chars.len());
194
195 let mut transformed_index = 0usize;
196 for raw_char in raw_chars {
197 while transformed_index < transformed_chars.len()
198 && transformed_chars[transformed_index] != raw_char
199 {
200 transformed_index += 1;
201 }
202 if transformed_index >= transformed_chars.len() {
203 return None;
204 }
205 matched_indices.push(transformed_index);
206 transformed_index += 1;
207 }
208
209 let mut mapping = Vec::with_capacity(matched_indices.len() + 1);
210 mapping.push(transformed_boundaries[matched_indices[0]]);
211 for matched_index in matched_indices.iter().skip(1) {
212 mapping.push(transformed_boundaries[*matched_index]);
213 }
214 mapping.push(transformed.len());
215
216 Some(mapping)
217}
218
219fn boundary_index_for_offset(boundaries: &[usize], offset: usize) -> usize {
220 boundaries
221 .partition_point(|&boundary| boundary <= offset)
222 .saturating_sub(1)
223}
224
225fn is_word_character(character: char) -> bool {
226 character.is_alphanumeric() || character == '_'
227}
228
229fn next_word_boundary(text: &str, offset: usize) -> usize {
230 let boundaries = char_boundary_offsets(text);
231 let characters: Vec<char> = text.chars().collect();
232 let mut index = boundary_index_for_offset(&boundaries, offset.min(text.len()));
233
234 if index >= characters.len() {
235 return text.len();
236 }
237
238 if !is_word_character(characters[index]) {
239 while index < characters.len() && !is_word_character(characters[index]) {
240 index += 1;
241 }
242 }
243 while index < characters.len() && is_word_character(characters[index]) {
244 index += 1;
245 }
246
247 boundaries[index.min(boundaries.len().saturating_sub(1))]
248}
249
250fn previous_word_boundary(text: &str, offset: usize) -> usize {
251 let boundaries = char_boundary_offsets(text);
252 let characters: Vec<char> = text.chars().collect();
253 let mut index = boundary_index_for_offset(&boundaries, offset.min(text.len()));
254
255 while index > 0 && !is_word_character(characters[index - 1]) {
256 index -= 1;
257 }
258 while index > 0 && is_word_character(characters[index - 1]) {
259 index -= 1;
260 }
261
262 boundaries[index]
263}
264
265fn word_range_at_offset(text: &str, offset: usize) -> Option<Range<usize>> {
266 let boundaries = char_boundary_offsets(text);
267 let characters: Vec<char> = text.chars().collect();
268 if characters.is_empty() {
269 return None;
270 }
271
272 let mut index = boundary_index_for_offset(&boundaries, offset.min(text.len()));
273 if index >= characters.len() {
274 index = characters.len().saturating_sub(1);
275 }
276
277 if !is_word_character(characters[index]) {
278 let next_index = (index..characters.len()).find(|&i| is_word_character(characters[i]));
279 let previous_index = (0..index).rev().find(|&i| is_word_character(characters[i]));
280 index = next_index.or(previous_index)?;
281 }
282
283 let mut start = index;
284 while start > 0 && is_word_character(characters[start - 1]) {
285 start -= 1;
286 }
287
288 let mut end = index + 1;
289 while end < characters.len() && is_word_character(characters[end]) {
290 end += 1;
291 }
292
293 Some(boundaries[start]..boundaries[end])
294}
295
296fn line_range_at_offset(text: &str, offset: usize) -> Range<usize> {
297 let clamped_offset = offset.min(text.len());
298 let start = text[..clamped_offset]
299 .rfind('\n')
300 .map_or(0, |index| index + 1);
301 let end = text[clamped_offset..]
302 .find('\n')
303 .map_or(text.len(), |index| clamped_offset + index);
304 start..end
305}
306
307fn paragraph_start_offsets(text: &str) -> Vec<usize> {
308 if text.is_empty() {
309 return vec![0];
310 }
311
312 let mut starts = Vec::new();
313 let mut offset = 0usize;
314 let mut previous_blank = true;
315 for segment in text.split_inclusive('\n') {
316 let line = segment.strip_suffix('\n').unwrap_or(segment);
317 let blank = line.trim().is_empty();
318 if !blank && previous_blank {
319 starts.push(offset);
320 }
321 offset += segment.len();
322 previous_blank = blank;
323 }
324
325 if starts.is_empty() {
326 starts.push(0);
327 }
328
329 starts
330}
331
332fn previous_paragraph_boundary(text: &str, offset: usize) -> usize {
333 let starts = paragraph_start_offsets(text);
334 let clamped_offset = offset.min(text.len());
335 let index = starts.partition_point(|&start| start < clamped_offset);
336 if index == 0 { 0 } else { starts[index - 1] }
337}
338
339fn next_paragraph_boundary(text: &str, offset: usize) -> usize {
340 let starts = paragraph_start_offsets(text);
341 let clamped_offset = offset.min(text.len());
342 let index = starts.partition_point(|&start| start <= clamped_offset);
343 starts.get(index).copied().unwrap_or(text.len())
344}
345
346fn extend_selection_range(origin: &Range<usize>, target: &Range<usize>) -> TextSelection {
347 if target.start >= origin.end {
348 TextSelection {
349 start: origin.start,
350 end: target.end,
351 }
352 } else if target.end <= origin.start {
353 TextSelection {
354 start: origin.end,
355 end: target.start,
356 }
357 } else {
358 TextSelection {
359 start: origin.start,
360 end: origin.end,
361 }
362 }
363}
364
365#[derive(Clone, Debug, PartialEq)]
367pub struct RectDef {
372 pub x: Px,
374 pub y: Px,
376 pub width: Px,
378 pub height: Px,
380}
381
382#[derive(Clone, Copy, PartialEq, Eq)]
384pub enum ClickType {
388 Single,
390 Double,
392 Triple,
394}
395
396#[derive(Clone, Copy, PartialEq, Eq)]
397enum DragSelectionMode {
398 Character,
399 Word,
400 Line,
401}
402
403#[derive(Clone, Copy, PartialEq)]
404pub(crate) struct TextScrollState {
405 scroll: glyphon::cosmic_text::Scroll,
406}
407
408impl TextScrollState {
409 fn from_scroll(scroll: glyphon::cosmic_text::Scroll) -> Self {
410 Self { scroll }
411 }
412
413 fn as_scroll(self) -> glyphon::cosmic_text::Scroll {
414 self.scroll
415 }
416
417 pub(crate) fn horizontal(&self) -> f32 {
418 self.scroll.horizontal
419 }
420
421 pub(crate) fn vertical(&self) -> f32 {
422 self.scroll.vertical
423 }
424
425 fn reset_vertical(self) -> Self {
426 let mut scroll = self.scroll;
427 scroll.line = 0;
428 scroll.vertical = 0.0;
429 Self { scroll }
430 }
431}
432
433#[derive(Clone, Default)]
434struct TextLayoutSnapshot {
435 text_cache_key: Option<TextLayoutCacheKey>,
436 geometry_cache_key: Option<TextLayoutCacheKey>,
437 text_data: Option<TextData>,
438 buffer: Option<glyphon::Buffer>,
439 transformed_text: Option<TransformedText>,
440 selection_rects: Vec<RectDef>,
441 composition_rects: Vec<RectDef>,
442 cursor_rect: Option<RectDef>,
443 ime_rect: Option<RectDef>,
444 computed_data: Option<ComputedData>,
445}
446
447struct DerivedLayoutGeometry {
448 selection_rects: Vec<RectDef>,
449 composition_rects: Vec<RectDef>,
450 cursor_position: Option<(i32, i32)>,
451 scroll_horizontal: Px,
452}
453
454struct PreviewEditResult {
455 text: String,
456 selection: TextSelection,
457}
458
459struct PreviewReplaceResult {
460 text: String,
461 replaced_range: Range<usize>,
462}
463
464#[derive(Clone, Copy, Debug, PartialEq, Eq)]
465struct TextLayoutCacheKey {
466 layout_version: u64,
467 max_width_bits: Option<u32>,
468 max_height_bits: Option<u32>,
469}
470
471impl TextLayoutCacheKey {
472 fn new(layout_version: u64, constraint: &TextConstraint) -> Self {
473 Self {
474 layout_version,
475 max_width_bits: constraint.max_width.map(f32::to_bits),
476 max_height_bits: constraint.max_height.map(f32::to_bits),
477 }
478 }
479}
480
481struct TextEditState {
482 editor: glyphon::Editor<'static>,
483 display_transform: Option<DisplayTransform>,
484 text_color: Color,
485 cursor_color: Color,
486 style: TextStyle,
487 single_line: bool,
488 pending_preview: Option<PendingPreview>,
489}
490
491struct PendingPreview {
492 action: glyphon::Action,
493 editor: glyphon::Editor<'static>,
494 text: String,
495 selection: TextSelection,
496}
497
498impl TextEditState {
499 fn editor(&self) -> &glyphon::Editor<'static> {
500 &self.editor
501 }
502
503 fn with_editor_mut<F, R>(&mut self, f: F) -> R
504 where
505 F: FnOnce(&mut glyphon::Editor<'static>) -> R,
506 {
507 f(&mut self.editor)
508 }
509
510 fn text(&self) -> String {
511 editor_content(&self.editor)
512 }
513
514 fn text_color(&self) -> Color {
515 self.text_color
516 }
517
518 fn set_text_color(&mut self, color: Color) -> bool {
519 if self.text_color == color {
520 return false;
521 }
522 self.text_color = color;
523 true
524 }
525
526 fn cursor_color(&self) -> Color {
527 self.cursor_color
528 }
529
530 fn set_cursor_color(&mut self, color: Color) {
531 self.cursor_color = color;
532 }
533
534 fn display_transform(&self) -> Option<DisplayTransform> {
535 self.display_transform
536 }
537
538 fn display_transform_ref(&self) -> Option<&DisplayTransform> {
539 self.display_transform.as_ref()
540 }
541
542 fn set_display_transform(&mut self, transform: Option<DisplayTransform>) -> bool {
543 let should_update = match (&self.display_transform, &transform) {
544 (None, None) => false,
545 (Some(current), Some(next)) => current != next,
546 _ => true,
547 };
548 if should_update {
549 self.display_transform = transform;
550 }
551 should_update
552 }
553
554 fn single_line(&self) -> bool {
555 self.single_line
556 }
557
558 fn set_single_line(&mut self, single_line: bool) -> bool {
559 if self.single_line == single_line {
560 return false;
561 }
562 self.single_line = single_line;
563 true
564 }
565
566 fn cursor(&self) -> Cursor {
567 self.editor.cursor()
568 }
569
570 fn cursor_offset(&self) -> usize {
571 self.cursor_to_text_offset(self.cursor())
572 }
573
574 fn selection(&self) -> TextSelection {
575 match self.editor.selection() {
576 Selection::None => TextSelection::collapsed(self.cursor_offset()),
577 Selection::Normal(anchor) | Selection::Line(anchor) | Selection::Word(anchor) => {
578 TextSelection {
579 start: self
580 .editor
581 .with_buffer(|buffer| cursor_to_text_offset_in_buffer(buffer, anchor)),
582 end: self.cursor_offset(),
583 }
584 }
585 }
586 }
587
588 fn selected_text(&self) -> Option<String> {
589 let selection = self.selection().ordered_range();
590 if selection.is_empty() {
591 return None;
592 }
593 let text = self.text();
594 Some(text[selection].to_string())
595 }
596
597 fn clone_owned_editor(&self) -> glyphon::Editor<'static> {
598 let mut new_editor = self.editor.clone();
599 let mut new_buffer = None;
600 match new_editor.buffer_ref_mut() {
601 glyphon::cosmic_text::BufferRef::Owned(_) => {}
602 glyphon::cosmic_text::BufferRef::Borrowed(buffer) => {
603 new_buffer = Some(buffer.clone());
604 }
605 glyphon::cosmic_text::BufferRef::Arc(buffer) => {
606 new_buffer = Some((**buffer).clone());
607 }
608 }
609 if let Some(buffer) = new_buffer {
610 *new_editor.buffer_ref_mut() = glyphon::cosmic_text::BufferRef::Owned(buffer);
611 }
612 new_editor
613 }
614
615 fn preview_action_result(&mut self, action: glyphon::Action) -> PreviewEditResult {
616 if let Some(preview) = self.pending_preview.as_ref()
617 && preview.action == action
618 {
619 return PreviewEditResult {
620 text: preview.text.clone(),
621 selection: preview.selection.clone(),
622 };
623 }
624
625 let mut editor = self.clone_owned_editor();
626 editor.action(&mut shaping_fonts(), action);
627 let preview = PreviewEditResult {
628 text: editor_content(&editor),
629 selection: editor_selection(&editor),
630 };
631 self.pending_preview = Some(PendingPreview {
632 action,
633 editor,
634 text: preview.text.clone(),
635 selection: preview.selection.clone(),
636 });
637 preview
638 }
639
640 fn preview_replace_result(
641 &self,
642 range: Range<usize>,
643 replacement: &str,
644 ) -> PreviewReplaceResult {
645 let current_text = self.text();
646 let start = range.start.min(current_text.len());
647 let end = range.end.min(current_text.len()).max(start);
648 let mut text = current_text;
649 text.replace_range(start..end, replacement);
650 PreviewReplaceResult {
651 text,
652 replaced_range: start..(start + replacement.len()),
653 }
654 }
655
656 fn commit_previewed_action(&mut self) {
657 if let Some(preview) = self.pending_preview.take() {
658 self.editor = preview.editor;
659 }
660 }
661
662 fn apply_action(&mut self, action: glyphon::Action) {
663 self.pending_preview = None;
664 self.editor.action(&mut shaping_fonts(), action);
665 }
666
667 fn set_cursor_and_selection(&mut self, cursor: Cursor, selection: Option<Selection>) {
668 self.editor.set_cursor(cursor);
669 self.editor
670 .set_selection(selection.unwrap_or(Selection::None));
671 }
672
673 fn set_cursor_and_selection_offsets(
674 &mut self,
675 cursor_offset: usize,
676 selection: Option<TextSelection>,
677 ) {
678 let cursor = self.text_offset_to_cursor(cursor_offset);
679 let editor_selection = if let Some(selection) = selection
680 && !selection.is_collapsed()
681 {
682 Some(Selection::Normal(
683 self.text_offset_to_cursor(selection.start),
684 ))
685 } else {
686 None
687 };
688 self.set_cursor_and_selection(cursor, editor_selection);
689 }
690
691 fn cursor_line(&self) -> usize {
692 self.cursor().line
693 }
694
695 fn scroll(&self) -> glyphon::cosmic_text::Scroll {
696 self.editor.with_buffer(|buffer| buffer.scroll())
697 }
698
699 fn metrics_and_scroll(&self) -> (glyphon::Metrics, glyphon::cosmic_text::Scroll) {
700 self.editor
701 .with_buffer(|buffer| (buffer.metrics(), buffer.scroll()))
702 }
703 fn with_buffer<R>(&self, f: impl FnOnce(&glyphon::Buffer) -> R) -> R {
704 self.editor.with_buffer(f)
705 }
706 fn last_line_index(&self) -> usize {
707 self.editor
708 .with_buffer(|buffer| buffer.lines.len().saturating_sub(1))
709 }
710
711 fn line_len(&self, line_index: usize) -> usize {
712 self.editor
713 .with_buffer(|buffer| buffer.lines[line_index].text().len())
714 }
715
716 fn first_line_start_cursor(&self) -> Cursor {
717 Cursor::new(0, 0)
718 }
719
720 fn last_line_end_cursor(&self) -> Cursor {
721 let last_line_index = self.last_line_index();
722 Cursor::new(last_line_index, self.line_len(last_line_index))
723 }
724
725 fn cursor_to_text_offset(&self, cursor: Cursor) -> usize {
726 self.editor
727 .with_buffer(|buffer| cursor_to_text_offset_in_buffer(buffer, cursor))
728 }
729
730 fn text_offset_to_cursor(&self, offset: usize) -> Cursor {
731 self.editor
732 .with_buffer(|buffer| text_offset_to_cursor_in_buffer(buffer, offset))
733 }
734
735 fn set_wrap(&mut self, wrap: glyphon::Wrap) {
736 self.editor.with_buffer_mut(|buffer| {
737 buffer.set_wrap(wrap);
738 });
739 }
740
741 fn set_scroll(&mut self, scroll: glyphon::cosmic_text::Scroll) {
742 self.editor.with_buffer_mut(|buffer| {
743 buffer.set_scroll(scroll);
744 });
745 }
746
747 fn sync_size_and_shape_until_scroll(&mut self, constraint: &TextConstraint) {
748 self.editor.with_buffer_mut(|buffer| {
749 buffer.set_size(constraint.max_width, constraint.max_height);
750 buffer.shape_until_scroll(&mut shaping_fonts(), false);
751 });
752 }
753
754 fn scroll_horizontal_by(&mut self, delta: f32) -> bool {
755 self.editor.with_buffer_mut(|buffer| {
756 let viewport_width = buffer.size().0.unwrap_or(0.0);
757 let content_width = buffer
758 .layout_runs()
759 .fold(0.0f32, |width, run| width.max(run.line_w));
760 let max_horizontal = (content_width - viewport_width).max(0.0);
761 let mut scroll = buffer.scroll();
762 let next_horizontal = (scroll.horizontal + delta).clamp(0.0, max_horizontal);
763 if (next_horizontal - scroll.horizontal).abs() <= f32::EPSILON {
764 return false;
765 }
766 scroll.horizontal = next_horizontal;
767 buffer.set_scroll(scroll);
768 true
769 })
770 }
771
772 fn has_horizontal_overflow(&mut self) -> bool {
773 self.editor.with_buffer_mut(|buffer| {
774 let viewport_width = buffer.size().0.unwrap_or(0.0);
775 if viewport_width <= f32::EPSILON {
776 return false;
777 }
778 let content_width = buffer
779 .layout_runs()
780 .fold(0.0f32, |width, run| width.max(run.line_w));
781 content_width - viewport_width > f32::EPSILON
782 })
783 }
784
785 fn has_vertical_overflow(&mut self) -> bool {
786 self.editor.with_buffer_mut(|buffer| {
787 let viewport_height = buffer.size().1.unwrap_or(0.0);
788 if viewport_height <= f32::EPSILON {
789 return false;
790 }
791 let content_height = buffer.layout_runs().fold(0.0f32, |height, run| {
792 height.max(run.line_top + run.line_height)
793 });
794 content_height - viewport_height > f32::EPSILON
795 })
796 }
797
798 fn scroll_vertical_by(&mut self, delta: f32) {
799 self.editor.action(
800 &mut shaping_fonts(),
801 glyphon::Action::Scroll { pixels: delta },
802 );
803 }
804
805 fn shape_until_cursor(&mut self) -> glyphon::cosmic_text::Scroll {
806 let cursor = self.cursor();
807 self.editor.with_buffer_mut(|buffer| {
808 buffer.shape_until_cursor(&mut shaping_fonts(), cursor, false);
809 });
810 self.scroll()
811 }
812
813 fn motion_target_offset(&mut self, motion: cosmic_text::Motion) -> Option<usize> {
814 let cursor = self.cursor();
815 let next_cursor = self.editor.with_buffer_mut(|buffer| {
816 buffer
817 .cursor_motion(&mut shaping_fonts(), cursor, None, motion)
818 .map(|(next_cursor, _)| next_cursor)
819 })?;
820 Some(self.cursor_to_text_offset(next_cursor))
821 }
822
823 fn set_text_and_selection(&mut self, text: &str, selection: TextSelection) {
824 self.editor.with_buffer_mut(|buffer| {
825 let color = color_to_glyphon(self.text_color);
826 let alignment = buffer.lines.first().and_then(|line| line.align());
827 buffer.set_text(
828 text,
829 &editor_font_attrs(self.style).color(color),
830 glyphon::Shaping::Advanced,
831 alignment,
832 );
833 buffer.set_redraw(true);
834 });
835
836 let start = self.editor.with_buffer(|buffer| {
837 text_offset_to_cursor_in_buffer(buffer, selection.start.min(text.len()))
838 });
839 let end = self.editor.with_buffer(|buffer| {
840 text_offset_to_cursor_in_buffer(buffer, selection.end.min(text.len()))
841 });
842 let editor_selection = if selection.is_collapsed() {
843 None
844 } else {
845 Some(Selection::Normal(start))
846 };
847 self.set_cursor_and_selection(end, editor_selection);
848 }
849
850 fn select_all(&mut self) {
851 let end = self.text().len();
852 let start_cursor = self.text_offset_to_cursor(0);
853 let end_cursor = self.text_offset_to_cursor(end);
854 self.set_cursor_and_selection(end_cursor, Some(Selection::Normal(start_cursor)));
855 }
856}
857
858struct TextSelectionState {
859 selection_color: Color,
860 last_click_time: Option<Instant>,
861 last_click_position: Option<PxPosition>,
862 last_click_window_position: Option<PxPosition>,
863 click_count: u32,
864 is_dragging: bool,
865 drag_selection_mode: DragSelectionMode,
866 drag_origin_selection: Option<Range<usize>>,
867}
868
869impl TextSelectionState {
870 fn selection_color(&self) -> Color {
871 self.selection_color
872 }
873
874 fn set_selection_color(&mut self, color: Color) {
875 self.selection_color = color;
876 }
877
878 fn handle_click(
879 &mut self,
880 position: PxPosition,
881 window_position: PxPosition,
882 timestamp: Instant,
883 ) -> ClickType {
884 const DOUBLE_CLICK_TIME_MS: u128 = 500;
885 const CLICK_DISTANCE_THRESHOLD: Px = Px(5);
886
887 let click_type = if let (Some(last_time), Some(last_pos)) =
888 (self.last_click_time, self.last_click_window_position)
889 {
890 let time_diff = timestamp.duration_since(last_time).as_millis();
891 let distance =
892 (window_position.x - last_pos.x).abs() + (window_position.y - last_pos.y).abs();
893
894 if time_diff <= DOUBLE_CLICK_TIME_MS
895 && distance <= CLICK_DISTANCE_THRESHOLD.abs() as f32
896 {
897 self.click_count += 1;
898 match self.click_count {
899 2 => ClickType::Double,
900 3 => {
901 self.click_count = 0;
902 ClickType::Triple
903 }
904 _ => ClickType::Single,
905 }
906 } else {
907 self.click_count = 1;
908 ClickType::Single
909 }
910 } else {
911 self.click_count = 1;
912 ClickType::Single
913 };
914
915 self.last_click_time = Some(timestamp);
916 self.last_click_position = Some(position);
917 self.last_click_window_position = Some(window_position);
918 self.is_dragging = false;
919 self.drag_selection_mode = DragSelectionMode::Character;
920 self.drag_origin_selection = None;
921
922 click_type
923 }
924
925 fn start_drag(&mut self, click_type: ClickType, origin_selection: Option<Range<usize>>) {
926 self.is_dragging = true;
927 match click_type {
928 ClickType::Single => {
929 self.drag_selection_mode = DragSelectionMode::Character;
930 self.drag_origin_selection = None;
931 }
932 ClickType::Double => {
933 self.drag_selection_mode = DragSelectionMode::Word;
934 self.drag_origin_selection = origin_selection;
935 }
936 ClickType::Triple => {
937 self.drag_selection_mode = DragSelectionMode::Line;
938 self.drag_origin_selection = origin_selection;
939 }
940 }
941 }
942
943 fn stop_drag(&mut self) {
944 self.is_dragging = false;
945 self.drag_selection_mode = DragSelectionMode::Character;
946 self.drag_origin_selection = None;
947 }
948
949 fn is_dragging(&self) -> bool {
950 self.is_dragging
951 }
952
953 fn drag_selection_mode(&self) -> DragSelectionMode {
954 self.drag_selection_mode
955 }
956
957 fn drag_origin_selection(&self) -> Option<Range<usize>> {
958 self.drag_origin_selection.clone()
959 }
960
961 fn last_click_position(&self) -> Option<PxPosition> {
962 self.last_click_position
963 }
964
965 fn last_click_time(&self) -> Option<Instant> {
966 self.last_click_time
967 }
968
969 fn update_last_click_position(&mut self, position: PxPosition) {
970 self.last_click_position = Some(position);
971 }
972
973 fn anchor_offset(&self, selection: TextSelection, cursor_offset: usize) -> usize {
974 if selection.is_collapsed() {
975 cursor_offset
976 } else {
977 selection.start
978 }
979 }
980
981 fn drag_origin_for_click(
982 &self,
983 click_type: ClickType,
984 selection: TextSelection,
985 ) -> Option<Range<usize>> {
986 match click_type {
987 ClickType::Single => Some(selection.ordered_range()),
988 ClickType::Double | ClickType::Triple => Some(selection.ordered_range()),
989 }
990 }
991
992 fn collapse_target(&self, selection: TextSelection, collapse_to_end: bool) -> Option<usize> {
993 if selection.is_collapsed() {
994 return None;
995 }
996 let ordered = selection.ordered_range();
997 Some(if collapse_to_end {
998 ordered.end
999 } else {
1000 ordered.start
1001 })
1002 }
1003
1004 fn extended_selection(
1005 &self,
1006 selection: TextSelection,
1007 cursor_offset: usize,
1008 next_offset: usize,
1009 ) -> Option<TextSelection> {
1010 let anchor_offset = self.anchor_offset(selection, cursor_offset);
1011 (next_offset != anchor_offset).then_some(TextSelection {
1012 start: anchor_offset,
1013 end: next_offset,
1014 })
1015 }
1016
1017 fn deletion_range(
1018 &self,
1019 selection: TextSelection,
1020 cursor_offset: usize,
1021 target_offset: Option<usize>,
1022 ) -> Option<Range<usize>> {
1023 let selection = selection.ordered_range();
1024 if !selection.is_empty() {
1025 return Some(selection);
1026 }
1027
1028 let target_offset = target_offset?;
1029 (target_offset != cursor_offset)
1030 .then_some(cursor_offset.min(target_offset)..cursor_offset.max(target_offset))
1031 }
1032}
1033
1034struct TextScrollControllerState {
1035 state: TextScrollState,
1036}
1037
1038impl TextScrollControllerState {
1039 fn new(scroll: glyphon::cosmic_text::Scroll) -> Self {
1040 Self {
1041 state: TextScrollState::from_scroll(scroll),
1042 }
1043 }
1044
1045 fn as_scroll(&self) -> glyphon::cosmic_text::Scroll {
1046 self.state.as_scroll()
1047 }
1048
1049 fn state(&self) -> TextScrollState {
1050 self.state
1051 }
1052
1053 fn horizontal(&self) -> f32 {
1054 self.state.horizontal()
1055 }
1056
1057 fn sync_from_scroll(&mut self, scroll: glyphon::cosmic_text::Scroll) -> bool {
1058 if self.state.as_scroll() == scroll {
1059 return false;
1060 }
1061 self.state = TextScrollState::from_scroll(scroll);
1062 true
1063 }
1064
1065 fn sync_from_edit_state(&mut self, edit_state: &TextEditState) -> bool {
1066 self.sync_from_scroll(edit_state.scroll())
1067 }
1068
1069 fn reset_vertical(&mut self) {
1070 self.state = self.state.reset_vertical();
1071 }
1072}
1073
1074struct TextImeState {
1075 composition: Option<ImeComposition>,
1076}
1077
1078#[derive(Clone, Debug, PartialEq, Eq)]
1079pub(crate) struct PlannedImeEdit {
1080 pub(crate) replacement_range: Range<usize>,
1081 pub(crate) replacement_text: String,
1082 pub(crate) selection: TextSelection,
1083 pub(crate) composition_range: Option<Range<usize>>,
1084}
1085
1086#[derive(Clone, Debug, PartialEq, Eq)]
1087pub(crate) enum PlannedImeEvent {
1088 Submit,
1089 Edit(PlannedImeEdit),
1090}
1091
1092impl TextImeState {
1093 fn composition(&self) -> Option<&ImeComposition> {
1094 self.composition.as_ref()
1095 }
1096
1097 #[cfg(test)]
1098 fn set_composition(&mut self, composition: Option<ImeComposition>) {
1099 self.composition = composition;
1100 }
1101
1102 fn clear(&mut self) -> bool {
1103 self.composition.take().is_some()
1104 }
1105
1106 fn composition_range(&self) -> Option<Range<usize>> {
1107 self.composition
1108 .as_ref()
1109 .map(|composition| composition.range.clone())
1110 }
1111
1112 fn composition_cursor_range(&self, edit_state: &TextEditState) -> Option<(Cursor, Cursor)> {
1113 self.composition_range().map(|range| {
1114 (
1115 edit_state.text_offset_to_cursor(range.start),
1116 edit_state.text_offset_to_cursor(range.end),
1117 )
1118 })
1119 }
1120
1121 fn plan_event(
1122 &self,
1123 selection: TextSelection,
1124 single_line: bool,
1125 text: &str,
1126 event: &winit::event::Ime,
1127 ) -> Option<PlannedImeEvent> {
1128 if single_line
1129 && matches!(
1130 event,
1131 winit::event::Ime::Commit(text) if matches!(text.as_str(), "\n" | "\r" | "\r\n")
1132 )
1133 {
1134 return Some(PlannedImeEvent::Submit);
1135 }
1136
1137 let replacement_range = self
1138 .composition
1139 .as_ref()
1140 .map(|composition| composition.range.clone())
1141 .unwrap_or_else(|| selection.ordered_range());
1142
1143 match event {
1144 winit::event::Ime::DeleteSurroundingText { before, after } => {
1145 let range = selection.ordered_range();
1146 let start = range.start.checked_sub(*before)?;
1147 let end = range.end.checked_add(*after)?;
1148 text.get(start..end)?;
1149 Some(PlannedImeEvent::Edit(PlannedImeEdit {
1150 replacement_range: start..end,
1151 replacement_text: text.get(range.clone())?.into(),
1152 selection: TextSelection {
1153 start: start + selection.start - range.start,
1154 end: start + selection.end - range.start,
1155 },
1156 composition_range: None,
1157 }))
1158 }
1159 winit::event::Ime::TextState(state) => Some(PlannedImeEvent::Edit(PlannedImeEdit {
1160 replacement_range: 0..text.len(),
1161 replacement_text: state.text.clone(),
1162 selection: TextSelection {
1163 start: state.selection.start,
1164 end: state.selection.end,
1165 },
1166 composition_range: state.composition.clone(),
1167 })),
1168 winit::event::Ime::Action(winit::event::ImeAction::Submit) => {
1169 Some(PlannedImeEvent::Submit)
1170 }
1171 winit::event::Ime::Action(winit::event::ImeAction::Newline) => self.plan_event(
1172 selection,
1173 single_line,
1174 text,
1175 &winit::event::Ime::Commit("\n".into()),
1176 ),
1177 winit::event::Ime::Commit(text) => Some(PlannedImeEvent::Edit(PlannedImeEdit {
1178 replacement_range: replacement_range.clone(),
1179 replacement_text: text.clone(),
1180 selection: TextSelection::collapsed(replacement_range.start + text.len()),
1181 composition_range: None,
1182 })),
1183 winit::event::Ime::Preedit(text, cursor_offset) => {
1184 if text.is_empty() {
1185 self.composition.as_ref()?;
1186 Some(PlannedImeEvent::Edit(PlannedImeEdit {
1187 replacement_range: replacement_range.clone(),
1188 replacement_text: String::new(),
1189 selection: TextSelection::collapsed(replacement_range.start),
1190 composition_range: None,
1191 }))
1192 } else {
1193 let selection = composition_selection(
1194 replacement_range.start,
1195 text,
1196 *cursor_offset,
1197 self.composition
1198 .as_ref()
1199 .map(|composition| composition.selection.clone()),
1200 );
1201 Some(PlannedImeEvent::Edit(PlannedImeEdit {
1202 replacement_range: replacement_range.clone(),
1203 replacement_text: text.clone(),
1204 selection: selection.clone(),
1205 composition_range: Some(
1206 replacement_range.start..replacement_range.start + text.len(),
1207 ),
1208 }))
1209 }
1210 }
1211 _ => None,
1212 }
1213 }
1214}
1215
1216struct TextLayoutState {
1217 snapshot: TextLayoutSnapshot,
1218 layout_version: u64,
1219 text_layout_version: u64,
1220}
1221
1222enum LayoutInvalidation {
1223 Geometry,
1224 Text,
1225}
1226
1227enum PointerActionOutcome {
1228 Selection(TextSelection),
1229 Cursor {
1230 cursor_offset: usize,
1231 selection: Option<TextSelection>,
1232 },
1233 ApplyAction(glyphon::Action),
1234 Ignored,
1235}
1236
1237impl TextLayoutState {
1238 fn new() -> Self {
1239 Self {
1240 snapshot: TextLayoutSnapshot::default(),
1241 layout_version: 0,
1242 text_layout_version: 0,
1243 }
1244 }
1245
1246 fn update_geometry(
1247 &mut self,
1248 cache_key: TextLayoutCacheKey,
1249 selection_rects: Vec<RectDef>,
1250 composition_rects: Vec<RectDef>,
1251 cursor_rect: Option<RectDef>,
1252 ime_rect: Option<RectDef>,
1253 computed_data: ComputedData,
1254 ) {
1255 self.snapshot.geometry_cache_key = Some(cache_key);
1256 self.snapshot.selection_rects = selection_rects;
1257 self.snapshot.composition_rects = composition_rects;
1258 self.snapshot.cursor_rect = cursor_rect;
1259 self.snapshot.ime_rect = ime_rect;
1260 self.snapshot.computed_data = Some(computed_data);
1261 }
1262
1263 fn update_geometry_for_constraint(
1264 &mut self,
1265 constraint: &TextConstraint,
1266 selection_rects: Vec<RectDef>,
1267 composition_rects: Vec<RectDef>,
1268 cursor_rect: Option<RectDef>,
1269 ime_rect: Option<RectDef>,
1270 computed_data: ComputedData,
1271 ) {
1272 self.update_geometry(
1273 self.geometry_cache_key(constraint),
1274 selection_rects,
1275 composition_rects,
1276 cursor_rect,
1277 ime_rect,
1278 computed_data,
1279 );
1280 }
1281
1282 fn selection_rects(&self) -> &[RectDef] {
1283 &self.snapshot.selection_rects
1284 }
1285
1286 fn text_data(&self) -> Option<TextData> {
1287 self.snapshot.text_data.clone()
1288 }
1289
1290 #[cfg(test)]
1291 fn cache_key(&self) -> Option<TextLayoutCacheKey> {
1292 self.snapshot.text_cache_key
1293 }
1294
1295 fn composition_rects(&self) -> &[RectDef] {
1296 &self.snapshot.composition_rects
1297 }
1298
1299 fn cursor_rect(&self) -> Option<RectDef> {
1300 self.snapshot.cursor_rect.clone()
1301 }
1302
1303 fn ime_rect(&self) -> Option<RectDef> {
1304 self.snapshot.ime_rect.clone()
1305 }
1306
1307 fn cached_geometry(&self, cache_key: TextLayoutCacheKey) -> Option<CachedLayout> {
1308 if self.snapshot.geometry_cache_key != Some(cache_key) {
1309 return None;
1310 }
1311 Some((
1312 self.snapshot.selection_rects.clone(),
1313 self.snapshot.composition_rects.clone(),
1314 self.snapshot.cursor_rect.clone(),
1315 self.snapshot.computed_data?,
1316 ))
1317 }
1318
1319 fn cached_geometry_for_constraint(&self, constraint: &TextConstraint) -> Option<CachedLayout> {
1320 self.cached_geometry(self.geometry_cache_key(constraint))
1321 }
1322
1323 fn reset_geometry_cache(&mut self) {
1324 self.snapshot.geometry_cache_key = None;
1325 self.snapshot.selection_rects.clear();
1326 self.snapshot.composition_rects.clear();
1327 self.snapshot.cursor_rect = None;
1328 self.snapshot.ime_rect = None;
1329 self.snapshot.computed_data = None;
1330 }
1331
1332 fn sync_snapshot_scroll(&mut self, scroll: glyphon::cosmic_text::Scroll) {
1333 if let Some(data) = self.snapshot.text_data.as_mut() {
1334 data.set_scroll(scroll);
1335 }
1336 if let Some(buffer) = self.snapshot.buffer.as_mut()
1337 && buffer.scroll() != scroll
1338 {
1339 buffer.set_scroll(scroll);
1340 buffer.shape_until_scroll(&mut shaping_fonts(), false);
1341 }
1342 }
1343
1344 fn geometry_cache_key(&self, constraint: &TextConstraint) -> TextLayoutCacheKey {
1345 TextLayoutCacheKey::new(self.layout_version, constraint)
1346 }
1347
1348 fn text_cache_key(&self, constraint: &TextConstraint) -> TextLayoutCacheKey {
1349 TextLayoutCacheKey::new(self.text_layout_version, constraint)
1350 }
1351
1352 fn cached_text_data(&self, cache_key: TextLayoutCacheKey) -> Option<TextData> {
1353 (self.snapshot.text_cache_key == Some(cache_key))
1354 .then(|| self.snapshot.text_data.clone())
1355 .flatten()
1356 }
1357
1358 fn cached_text_data_for_constraint(
1359 &mut self,
1360 constraint: &TextConstraint,
1361 scroll: glyphon::cosmic_text::Scroll,
1362 ) -> Option<TextData> {
1363 let cache_key = self.text_cache_key(constraint);
1364 if self.snapshot.text_cache_key != Some(cache_key) {
1365 return None;
1366 }
1367 self.sync_snapshot_scroll(scroll);
1368 self.cached_text_data(cache_key)
1369 }
1370
1371 fn set_text_snapshot(
1372 &mut self,
1373 cache_key: TextLayoutCacheKey,
1374 buffer: Option<glyphon::Buffer>,
1375 transformed_text: Option<TransformedText>,
1376 text_data: TextData,
1377 ) {
1378 self.snapshot.text_cache_key = Some(cache_key);
1379 self.snapshot.buffer = buffer;
1380 self.snapshot.transformed_text = transformed_text;
1381 self.snapshot.text_data = Some(text_data);
1382 self.reset_geometry_cache();
1383 }
1384
1385 fn set_text_snapshot_for_constraint(
1386 &mut self,
1387 constraint: &TextConstraint,
1388 buffer: Option<glyphon::Buffer>,
1389 transformed_text: Option<TransformedText>,
1390 text_data: TextData,
1391 ) {
1392 self.set_text_snapshot(
1393 self.text_cache_key(constraint),
1394 buffer,
1395 transformed_text,
1396 text_data,
1397 );
1398 }
1399
1400 fn transformed_text(&self) -> Option<&TransformedText> {
1401 self.snapshot.transformed_text.as_ref()
1402 }
1403
1404 fn cloned_transformed_text(&self) -> Option<TransformedText> {
1405 self.snapshot.transformed_text.clone()
1406 }
1407
1408 fn layout_version(&self) -> u64 {
1409 self.layout_version
1410 }
1411
1412 fn text_layout_version(&self) -> u64 {
1413 self.text_layout_version
1414 }
1415
1416 fn bump_layout_version(&mut self) {
1417 self.layout_version = self.layout_version.wrapping_add(1);
1418 }
1419
1420 fn bump_text_layout_version(&mut self) {
1421 self.text_layout_version = self.text_layout_version.wrapping_add(1);
1422 self.bump_layout_version();
1423 }
1424
1425 fn invalidate(&mut self, invalidation: LayoutInvalidation) {
1426 match invalidation {
1427 LayoutInvalidation::Geometry => self.bump_layout_version(),
1428 LayoutInvalidation::Text => self.bump_text_layout_version(),
1429 }
1430 }
1431
1432 fn display_editor(
1433 &self,
1434 cursor_offset: usize,
1435 selection: Option<TextSelection>,
1436 ) -> Option<(glyphon::Editor<'static>, TransformedText)> {
1437 let buffer = self.snapshot.buffer.clone()?;
1438 let transformed_text = self.cloned_transformed_text()?;
1439 let cursor =
1440 text_offset_to_cursor_in_buffer(&buffer, transformed_text.map_from_raw(cursor_offset));
1441 let selection = selection.map(|selection| {
1442 let ordered = selection.ordered_range();
1443 let start = text_offset_to_cursor_in_buffer(
1444 &buffer,
1445 transformed_text.map_from_raw(ordered.start),
1446 );
1447 let end = text_offset_to_cursor_in_buffer(
1448 &buffer,
1449 transformed_text.map_from_raw(ordered.end),
1450 );
1451 if selection.start <= selection.end {
1452 (start, end)
1453 } else {
1454 (end, start)
1455 }
1456 });
1457 Some((
1458 build_display_editor(buffer, cursor, selection),
1459 transformed_text,
1460 ))
1461 }
1462
1463 fn derived_geometry(
1464 &self,
1465 input: DerivedGeometryInput,
1466 raw_editor: &glyphon::Editor<'_>,
1467 ) -> Option<DerivedLayoutGeometry> {
1468 let DerivedGeometryInput {
1469 scroll_horizontal,
1470 cursor_offset,
1471 selection,
1472 composition_range,
1473 raw_composition,
1474 ..
1475 } = input;
1476
1477 if let Some((display_editor, transformed_text)) =
1478 self.display_editor(cursor_offset, selection)
1479 {
1480 let composition_rects = display_editor.with_buffer(|buffer| {
1481 composition_range
1482 .clone()
1483 .map(|composition| {
1484 compute_transformed_composition_rects(
1485 buffer,
1486 &transformed_text,
1487 composition,
1488 )
1489 })
1490 .unwrap_or_default()
1491 });
1492 return Some(DerivedLayoutGeometry {
1493 selection_rects: compute_selection_rects(&display_editor),
1494 composition_rects,
1495 cursor_position: display_editor.cursor_position(),
1496 scroll_horizontal,
1497 });
1498 }
1499
1500 let composition_rects = raw_editor
1501 .with_buffer(|buffer| compute_composition_rects_for_range(buffer, raw_composition));
1502 Some(DerivedLayoutGeometry {
1503 selection_rects: compute_selection_rects(raw_editor),
1504 composition_rects,
1505 cursor_position: raw_editor.cursor_position(),
1506 scroll_horizontal,
1507 })
1508 }
1509
1510 fn display_hit_offset(
1511 &self,
1512 cursor_offset: usize,
1513 selection: Option<TextSelection>,
1514 x: i32,
1515 y: i32,
1516 ) -> Option<(TransformedText, usize)> {
1517 let (editor, transformed_text) = self.display_editor(cursor_offset, selection)?;
1518 let hit_offset = editor_hit_offset(&editor, x, y)?;
1519 Some((transformed_text, hit_offset))
1520 }
1521
1522 fn raw_hit_offset(editor: &glyphon::Editor<'_>, x: i32, y: i32) -> Option<usize> {
1523 editor_hit_offset(editor, x, y)
1524 }
1525
1526 fn display_motion_offsets(
1527 &self,
1528 cursor_offset: usize,
1529 selection: Option<TextSelection>,
1530 motion: cosmic_text::Motion,
1531 ) -> Option<(TransformedText, usize, usize)> {
1532 let (mut editor, transformed_text) = self.display_editor(cursor_offset, selection)?;
1533 let current_offset = editor_cursor_offset(&editor);
1534 let next_offset = editor_motion_offset(&mut editor, motion)?;
1535 Some((transformed_text, current_offset, next_offset))
1536 }
1537
1538 fn paragraph_motion_offset(&self, text: &str, cursor_offset: usize, forward: bool) -> usize {
1539 if let Some(transformed_text) = self.transformed_text() {
1540 let display_offset = transformed_text.map_from_raw(cursor_offset);
1541 let next_display_offset = if forward {
1542 next_paragraph_boundary(transformed_text.text(), display_offset)
1543 } else {
1544 previous_paragraph_boundary(transformed_text.text(), display_offset)
1545 };
1546 if next_display_offset > display_offset {
1547 transformed_text.map_to_raw_forward(next_display_offset)
1548 } else {
1549 transformed_text.map_to_raw(next_display_offset)
1550 }
1551 } else if forward {
1552 next_paragraph_boundary(text, cursor_offset)
1553 } else {
1554 previous_paragraph_boundary(text, cursor_offset)
1555 }
1556 }
1557
1558 fn transformed_motion_target_offset(
1559 &self,
1560 cursor_offset: usize,
1561 selection: Option<TextSelection>,
1562 motion: cosmic_text::Motion,
1563 ) -> Option<usize> {
1564 if let Some(transformed_text) = self.transformed_text() {
1565 let display_offset = transformed_text.map_from_raw(cursor_offset);
1566 let next_display_offset = match motion {
1567 cosmic_text::Motion::LeftWord => {
1568 previous_word_boundary(transformed_text.text(), display_offset)
1569 }
1570 cosmic_text::Motion::RightWord => {
1571 next_word_boundary(transformed_text.text(), display_offset)
1572 }
1573 _ => {
1574 let (transformed_text, display_offset, next_display_offset) =
1575 self.display_motion_offsets(cursor_offset, selection, motion)?;
1576 let moving_forward = next_display_offset > display_offset;
1577 return Some(if moving_forward {
1578 transformed_text.map_to_raw_forward(next_display_offset)
1579 } else {
1580 transformed_text.map_to_raw(next_display_offset)
1581 });
1582 }
1583 };
1584 let moving_forward = next_display_offset > display_offset;
1585 return Some(if moving_forward {
1586 transformed_text.map_to_raw_forward(next_display_offset)
1587 } else {
1588 transformed_text.map_to_raw(next_display_offset)
1589 });
1590 }
1591
1592 let (transformed_text, _, display_offset) =
1593 self.display_motion_offsets(cursor_offset, selection, motion)?;
1594 Some(transformed_text.map_to_raw(display_offset))
1595 }
1596
1597 fn display_drag_selection(
1598 &self,
1599 origin: Range<usize>,
1600 cursor_offset: usize,
1601 selection: Option<TextSelection>,
1602 mode: DragSelectionMode,
1603 x: i32,
1604 y: i32,
1605 ) -> Option<TextSelection> {
1606 let (transformed_text, hit_offset) =
1607 self.display_hit_offset(cursor_offset, selection, x, y)?;
1608 if matches!(mode, DragSelectionMode::Character) {
1609 let anchor = origin.start;
1610 return (hit_offset != transformed_text.map_from_raw(anchor)).then(|| TextSelection {
1611 start: anchor,
1612 end: transformed_text.map_to_raw(hit_offset),
1613 });
1614 }
1615 let display_range = match mode {
1616 DragSelectionMode::Word => word_range_at_offset(transformed_text.text(), hit_offset)?,
1617 DragSelectionMode::Line => line_range_at_offset(transformed_text.text(), hit_offset),
1618 DragSelectionMode::Character => unreachable!(),
1619 };
1620 let target = transformed_text.map_to_raw(display_range.start)
1621 ..transformed_text.map_to_raw_forward(display_range.end);
1622 Some(extend_selection_range(&origin, &target))
1623 }
1624
1625 fn display_pointer_selection(
1626 &self,
1627 cursor_offset: usize,
1628 selection: Option<TextSelection>,
1629 action: glyphon::Action,
1630 ) -> Option<TextSelection> {
1631 let (x, y) = match action {
1632 glyphon::Action::DoubleClick { x, y } | glyphon::Action::TripleClick { x, y } => (x, y),
1633 _ => return None,
1634 };
1635 let (transformed_text, hit_offset) =
1636 self.display_hit_offset(cursor_offset, selection, x, y)?;
1637 let display_range = match action {
1638 glyphon::Action::DoubleClick { .. } => {
1639 word_range_at_offset(transformed_text.text(), hit_offset)?
1640 }
1641 glyphon::Action::TripleClick { .. } => {
1642 line_range_at_offset(transformed_text.text(), hit_offset)
1643 }
1644 _ => return None,
1645 };
1646 Some(TextSelection {
1647 start: transformed_text.map_to_raw(display_range.start),
1648 end: transformed_text.map_to_raw_forward(display_range.end),
1649 })
1650 }
1651
1652 fn raw_drag_selection(
1653 editor: &glyphon::Editor<'_>,
1654 origin: Range<usize>,
1655 text: &str,
1656 mode: DragSelectionMode,
1657 x: i32,
1658 y: i32,
1659 ) -> Option<TextSelection> {
1660 let hit_offset = Self::raw_hit_offset(editor, x, y)?;
1661 if matches!(mode, DragSelectionMode::Character) {
1662 return (hit_offset != origin.start).then_some(TextSelection {
1663 start: origin.start,
1664 end: hit_offset,
1665 });
1666 }
1667 let target = match mode {
1668 DragSelectionMode::Word => word_range_at_offset(text, hit_offset)?,
1669 DragSelectionMode::Line => line_range_at_offset(text, hit_offset),
1670 DragSelectionMode::Character => unreachable!(),
1671 };
1672 Some(extend_selection_range(&origin, &target))
1673 }
1674
1675 fn raw_pointer_selection(
1676 editor: &glyphon::Editor<'_>,
1677 text: &str,
1678 action: glyphon::Action,
1679 ) -> Option<TextSelection> {
1680 let (x, y) = match action {
1681 glyphon::Action::DoubleClick { x, y } | glyphon::Action::TripleClick { x, y } => (x, y),
1682 _ => return None,
1683 };
1684 let hit_offset = Self::raw_hit_offset(editor, x, y)?;
1685 let target = match action {
1686 glyphon::Action::DoubleClick { .. } => word_range_at_offset(text, hit_offset)?,
1687 glyphon::Action::TripleClick { .. } => line_range_at_offset(text, hit_offset),
1688 _ => return None,
1689 };
1690 Some(TextSelection {
1691 start: target.start,
1692 end: target.end,
1693 })
1694 }
1695
1696 fn display_action_result(
1697 &self,
1698 cursor_offset: usize,
1699 selection: Option<TextSelection>,
1700 action: glyphon::Action,
1701 ) -> Option<(usize, Option<TextSelection>)> {
1702 let (mut editor, transformed_text) = self.display_editor(cursor_offset, selection)?;
1703 editor.action(&mut shaping_fonts(), action);
1704 let cursor_offset = transformed_text.map_to_raw(editor_cursor_offset(&editor));
1705 let selection = Self::display_selection_to_raw(&editor, &transformed_text);
1706 Some((cursor_offset, selection))
1707 }
1708
1709 fn display_selection_to_raw(
1710 display_editor: &glyphon::Editor<'_>,
1711 transformed_text: &TransformedText,
1712 ) -> Option<TextSelection> {
1713 let anchor = match display_editor.selection() {
1714 Selection::None => return None,
1715 Selection::Normal(anchor) | Selection::Line(anchor) | Selection::Word(anchor) => anchor,
1716 };
1717 let anchor_offset =
1718 display_editor.with_buffer(|buffer| cursor_to_text_offset_in_buffer(buffer, anchor));
1719 let cursor_offset = editor_cursor_offset(display_editor);
1720 Some(TextSelection {
1721 start: transformed_text.map_to_raw(anchor_offset),
1722 end: if cursor_offset >= anchor_offset {
1723 transformed_text.map_to_raw_forward(cursor_offset)
1724 } else {
1725 transformed_text.map_to_raw(cursor_offset)
1726 },
1727 })
1728 }
1729
1730 fn pointer_action_outcome(
1731 &self,
1732 editor: &glyphon::Editor<'_>,
1733 context: PointerActionContext<'_>,
1734 ) -> PointerActionOutcome {
1735 let PointerActionContext {
1736 raw_text,
1737 cursor_offset,
1738 selection,
1739 drag_selection_mode,
1740 drag_origin_selection,
1741 action,
1742 ..
1743 } = context;
1744
1745 if self.cloned_transformed_text().is_some() {
1746 if let glyphon::Action::Drag { x, y } = action
1747 && let Some(origin) = drag_origin_selection
1748 && let Some(selection) = self.display_drag_selection(
1749 origin,
1750 cursor_offset,
1751 selection.clone(),
1752 drag_selection_mode,
1753 x,
1754 y,
1755 )
1756 {
1757 return PointerActionOutcome::Selection(selection);
1758 }
1759
1760 if let Some(selection) =
1761 self.display_pointer_selection(cursor_offset, selection.clone(), action)
1762 {
1763 return PointerActionOutcome::Selection(selection);
1764 }
1765
1766 if matches!(
1767 action,
1768 glyphon::Action::DoubleClick { .. } | glyphon::Action::TripleClick { .. }
1769 ) {
1770 return PointerActionOutcome::Ignored;
1771 }
1772
1773 return self
1774 .display_action_result(cursor_offset, selection, action)
1775 .map(|(cursor_offset, selection)| PointerActionOutcome::Cursor {
1776 cursor_offset,
1777 selection,
1778 })
1779 .unwrap_or(PointerActionOutcome::Ignored);
1780 }
1781
1782 if let glyphon::Action::Drag { x, y } = action
1783 && let Some(origin) = drag_origin_selection
1784 && let Some(selection) =
1785 Self::raw_drag_selection(editor, origin, raw_text, drag_selection_mode, x, y)
1786 {
1787 return PointerActionOutcome::Selection(selection);
1788 }
1789
1790 if let Some(selection) = Self::raw_pointer_selection(editor, raw_text, action) {
1791 return PointerActionOutcome::Selection(selection);
1792 }
1793
1794 PointerActionOutcome::ApplyAction(action)
1795 }
1796
1797 #[cfg(test)]
1798 fn set_snapshot_buffer(
1799 &mut self,
1800 buffer: glyphon::Buffer,
1801 transformed_text: Option<TransformedText>,
1802 ) {
1803 self.snapshot.buffer = Some(buffer);
1804 self.snapshot.transformed_text = transformed_text;
1805 }
1806}
1807
1808pub struct TextEditorController {
1816 fonts: Option<u64>,
1817 line_height: Px,
1818 blink_start_frame_nanos: u64,
1819 current_frame_nanos: u64,
1820 focus_handler: FocusRequester,
1821 edit_state: TextEditState,
1822 selection_state: TextSelectionState,
1823 scroll_state: TextScrollControllerState,
1824 ime_state: TextImeState,
1825 layout_state: TextLayoutState,
1826 intrinsic_size_cache: Option<(TextLayoutCacheKey, ComputedData)>,
1827}
1828
1829#[derive(Clone, Debug, PartialEq, Eq)]
1830pub(crate) struct TextSelection {
1831 pub(crate) start: usize,
1832 pub(crate) end: usize,
1833}
1834
1835impl TextSelection {
1836 pub(crate) fn collapsed(offset: usize) -> Self {
1837 Self {
1838 start: offset,
1839 end: offset,
1840 }
1841 }
1842
1843 pub(crate) fn ordered_range(&self) -> Range<usize> {
1844 self.start.min(self.end)..self.start.max(self.end)
1845 }
1846
1847 pub(crate) fn is_collapsed(&self) -> bool {
1848 self.start == self.end
1849 }
1850}
1851
1852#[derive(Clone, Debug, PartialEq, Eq)]
1853pub(crate) struct ImeComposition {
1854 pub(crate) range: Range<usize>,
1855 pub(crate) selection: TextSelection,
1856}
1857
1858#[derive(Clone, Debug, PartialEq, Eq)]
1859pub(crate) struct ImeEditResult {
1860 pub(crate) selection: TextSelection,
1861 pub(crate) replaced_range: Range<usize>,
1862}
1863
1864fn composition_selection(
1865 replacement_start: usize,
1866 replacement_text: &str,
1867 cursor_offset: Option<(usize, usize)>,
1868 previous_selection: Option<TextSelection>,
1869) -> TextSelection {
1870 let replacement_end = replacement_start + replacement_text.len();
1871 if let Some((start, end)) = cursor_offset {
1872 TextSelection {
1873 start: (replacement_start + start).min(replacement_end),
1874 end: (replacement_start + end).min(replacement_end),
1875 }
1876 } else if let Some(previous_selection) = previous_selection {
1877 TextSelection {
1878 start: previous_selection
1879 .start
1880 .clamp(replacement_start, replacement_end),
1881 end: previous_selection
1882 .end
1883 .clamp(replacement_start, replacement_end),
1884 }
1885 } else {
1886 TextSelection::collapsed(replacement_end)
1887 }
1888}
1889
1890impl TextEditorController {
1891 fn update_layout_geometry(
1892 &mut self,
1893 constraint: &TextConstraint,
1894 selection_rects: Vec<RectDef>,
1895 composition_rects: Vec<RectDef>,
1896 cursor_rect: Option<RectDef>,
1897 ime_rect: Option<RectDef>,
1898 computed_data: ComputedData,
1899 ) {
1900 self.layout_state.update_geometry_for_constraint(
1901 constraint,
1902 selection_rects,
1903 composition_rects,
1904 cursor_rect,
1905 ime_rect,
1906 computed_data,
1907 );
1908 }
1909
1910 pub(crate) fn current_selection_rects(&self) -> &[RectDef] {
1911 self.layout_state.selection_rects()
1912 }
1913
1914 pub(crate) fn current_composition_rects(&self) -> &[RectDef] {
1915 self.layout_state.composition_rects()
1916 }
1917
1918 pub(crate) fn current_ime_rect(&self) -> Option<RectDef> {
1919 self.layout_state.ime_rect()
1920 }
1921
1922 pub(crate) fn current_text_data(&self) -> Option<TextData> {
1923 self.layout_state.text_data()
1924 }
1925
1926 fn current_cursor_rect(&self) -> Option<RectDef> {
1927 self.layout_state.cursor_rect()
1928 }
1929
1930 fn cached_layout(&self, constraint: &TextConstraint) -> Option<CachedLayout> {
1931 self.layout_state.cached_geometry_for_constraint(constraint)
1932 }
1933
1934 fn cached_intrinsic_size(&self, constraint: &TextConstraint) -> Option<ComputedData> {
1935 let key = TextLayoutCacheKey::new(self.text_layout_version(), constraint);
1936 self.intrinsic_size_cache
1937 .filter(|(cached, _)| *cached == key)
1938 .map(|(_, size)| size)
1939 }
1940
1941 fn measure_intrinsic_size(
1942 &mut self,
1943 constraint: &TextConstraint,
1944 display: Option<TransformedText>,
1945 ) -> ComputedData {
1946 let raw = self.edit_state.text();
1947 let content = display
1948 .as_ref()
1949 .map_or(raw.as_str(), |display| display.text());
1950 let (metrics, _) = self.edit_state.metrics_and_scroll();
1951 let buffer = build_display_buffer(
1952 content,
1953 self.edit_state.text_color(),
1954 metrics.font_size,
1955 metrics.line_height,
1956 constraint,
1957 self.wrap_mode(),
1958 glyphon::cosmic_text::Scroll::default(),
1959 Some(self.edit_state.style),
1960 );
1961 let text_data = TextData::from_buffer(buffer);
1962 let size = measured_editor_size(&text_data, constraint, self.single_line());
1963 self.intrinsic_size_cache = Some((
1964 TextLayoutCacheKey::new(self.text_layout_version(), constraint),
1965 size,
1966 ));
1967 size
1968 }
1969
1970 fn sync_layout_snapshot_scroll(&mut self) {
1971 self.layout_state
1972 .sync_snapshot_scroll(self.scroll_state.as_scroll());
1973 }
1974
1975 fn sync_scroll_state_from_editor(&mut self) {
1976 self.scroll_state.sync_from_edit_state(&self.edit_state);
1977 }
1978
1979 #[cfg(test)]
1980 fn set_layout_snapshot_buffer(
1981 &mut self,
1982 buffer: glyphon::Buffer,
1983 transformed_text: Option<TransformedText>,
1984 ) {
1985 self.layout_state
1986 .set_snapshot_buffer(buffer, transformed_text);
1987 }
1988
1989 pub fn new(size: Dp, line_height: Option<Dp>) -> Self {
1998 Self::with_selection_color(size, line_height, Color::new(0.5, 0.7, 1.0, 0.4))
1999 }
2000
2001 pub fn with_selection_color(size: Dp, line_height: Option<Dp>, selection_color: Color) -> Self {
2010 let final_line_height = line_height.unwrap_or(Dp(size.0 * 1.2));
2011 let line_height_px: Px = final_line_height.into();
2012 let frame_nanos = current_frame_nanos();
2013 let mut buffer = glyphon::Buffer::new(
2014 &mut shaping_fonts(),
2015 glyphon::Metrics::new(size.to_pixels_f32(), line_height_px.to_f32()),
2016 );
2017 buffer.set_wrap(glyphon::Wrap::Glyph);
2018 let scroll_state = TextScrollControllerState::new(buffer.scroll());
2019 let editor = glyphon::Editor::new(buffer);
2020 let text_color = Color::BLACK;
2021 let cursor_color = Color::BLACK;
2022 Self {
2023 line_height: line_height_px,
2024 fonts: None,
2025 blink_start_frame_nanos: frame_nanos,
2026 current_frame_nanos: frame_nanos,
2027 focus_handler: FocusRequester::new(),
2028 edit_state: TextEditState {
2029 editor,
2030 display_transform: None,
2031 text_color,
2032 cursor_color,
2033 style: TextStyle {
2034 font_size: size,
2035 line_height: Some(final_line_height),
2036 ..Default::default()
2037 },
2038 single_line: false,
2039 pending_preview: None,
2040 },
2041 selection_state: TextSelectionState {
2042 selection_color,
2043 last_click_time: None,
2044 last_click_position: None,
2045 last_click_window_position: None,
2046 click_count: 0,
2047 is_dragging: false,
2048 drag_selection_mode: DragSelectionMode::Character,
2049 drag_origin_selection: None,
2050 },
2051 scroll_state,
2052 ime_state: TextImeState { composition: None },
2053 layout_state: TextLayoutState::new(),
2054 intrinsic_size_cache: None,
2055 }
2056 }
2057
2058 pub fn line_height(&self) -> Px {
2060 self.line_height
2061 }
2062
2063 fn prepare_display(&self) -> Option<TransformedText> {
2064 let raw = self.edit_state.text();
2065 self.edit_state
2066 .display_transform_ref()
2067 .map(|transform| transform.call(raw.clone()))
2068 .filter(|display| display.text() != raw)
2069 }
2070
2071 fn fonts_for(&self, display: Option<&TransformedText>) -> Option<u64> {
2072 let raw = self.edit_state.text();
2073 crate::fonts::prepare(
2074 display.map_or(raw.as_str(), |display| display.text()),
2075 self.edit_state.style,
2076 )
2077 }
2078
2079 fn set_fonts(&mut self, fonts: Option<u64>) {
2080 self.edit_state.editor.with_buffer_mut(|buffer| {
2081 for line in &mut buffer.lines {
2082 line.reset_shaping();
2083 }
2084 });
2085 self.invalidate_text_layout();
2086 self.fonts = fonts;
2087 }
2088
2089 fn text_data(
2090 &mut self,
2091 constraint: TextConstraint,
2092 display: Option<TransformedText>,
2093 ) -> TextData {
2094 if let Some(text_data) = self
2095 .layout_state
2096 .cached_text_data_for_constraint(&constraint, self.scroll_state.as_scroll())
2097 {
2098 return text_data;
2099 }
2100
2101 self.edit_state
2102 .sync_size_and_shape_until_scroll(&constraint);
2103
2104 let (display_buffer, transformed_text) = if let Some(transformed) = display {
2105 let (metrics, scroll) = self.edit_state.metrics_and_scroll();
2106 let text_buffer = build_display_buffer(
2107 transformed.text(),
2108 self.edit_state.text_color(),
2109 metrics.font_size,
2110 metrics.line_height,
2111 &constraint,
2112 self.wrap_mode(),
2113 scroll,
2114 Some(self.edit_state.style),
2115 );
2116 (Some(text_buffer), Some(transformed))
2117 } else {
2118 (None, None)
2119 };
2120
2121 let text_data = if let Some(buffer) = display_buffer.as_ref() {
2122 TextData::from_buffer(buffer.clone())
2123 } else {
2124 self.edit_state
2125 .with_buffer(|buffer| TextData::from_buffer(buffer.clone()))
2126 };
2127 self.layout_state.set_text_snapshot_for_constraint(
2128 &constraint,
2129 display_buffer,
2130 transformed_text,
2131 text_data.clone(),
2132 );
2133 text_data
2134 }
2135
2136 pub fn request_focus(&self) {
2142 self.focus_handler.request_focus();
2143 }
2144
2145 pub(crate) fn focus_handler(&self) -> &FocusRequester {
2147 &self.focus_handler
2148 }
2149
2150 pub fn editor(&self) -> &glyphon::Editor<'static> {
2152 self.edit_state.editor()
2153 }
2154
2155 pub fn with_editor_mut<F, R>(&mut self, f: F) -> R
2157 where
2158 F: FnOnce(&mut glyphon::Editor<'static>) -> R,
2159 {
2160 let result = self.edit_state.with_editor_mut(f);
2161 self.reset_cursor_blink();
2162 self.ensure_cursor_visible();
2163 self.invalidate_text_layout();
2164 result
2165 }
2166
2167 fn blink_start_frame_nanos(&self) -> u64 {
2169 self.blink_start_frame_nanos
2170 }
2171
2172 #[cfg(test)]
2174 fn current_frame_nanos(&self) -> u64 {
2175 self.current_frame_nanos
2176 }
2177
2178 #[cfg(test)]
2180 fn update_frame_nanos(&mut self, frame_nanos: u64) {
2181 self.current_frame_nanos = frame_nanos;
2182 }
2183
2184 fn reset_cursor_blink(&mut self) {
2185 let frame_nanos = self.current_frame_nanos.max(current_frame_nanos());
2186 self.blink_start_frame_nanos = frame_nanos;
2187 self.current_frame_nanos = frame_nanos;
2188 }
2189
2190 pub fn selection_color(&self) -> Color {
2192 self.selection_state.selection_color()
2193 }
2194
2195 pub fn set_selection_color(&mut self, color: Color) {
2201 self.selection_state.set_selection_color(color);
2202 }
2203
2204 pub fn text_color(&self) -> Color {
2206 self.edit_state.text_color()
2207 }
2208
2209 pub fn set_text_color(&mut self, color: Color) {
2211 if !self.edit_state.set_text_color(color) {
2212 return;
2213 }
2214 let current_text = self.text();
2215 let selection = self.selection();
2216 self.set_text_and_selection(¤t_text, selection);
2217 }
2218
2219 pub fn text_style(&self) -> TextStyle {
2221 self.edit_state.style
2222 }
2223
2224 pub fn set_text_style(&mut self, style: TextStyle) {
2226 if self.edit_state.style == style {
2227 return;
2228 }
2229 self.edit_state.style = style;
2230 self.line_height = style
2231 .line_height
2232 .unwrap_or(Dp(style.font_size.0 * 1.2))
2233 .to_px();
2234 self.edit_state.editor.with_buffer_mut(|buffer| {
2235 buffer.set_metrics(glyphon::Metrics::new(
2236 style.font_size.to_pixels_f32(),
2237 self.line_height.to_f32(),
2238 ));
2239 });
2240 let text = self.text();
2241 let selection = self.selection();
2242 self.set_text_and_selection(&text, selection);
2243 }
2244
2245 pub fn cursor_color(&self) -> Color {
2247 self.edit_state.cursor_color()
2248 }
2249
2250 pub fn set_cursor_color(&mut self, color: Color) {
2252 self.edit_state.set_cursor_color(color);
2253 }
2254
2255 pub fn set_display_transform(&mut self, transform: Option<DisplayTransform>) {
2257 if self.edit_state.set_display_transform(transform) {
2258 self.invalidate_text_layout();
2259 }
2260 }
2261
2262 pub(crate) fn single_line(&self) -> bool {
2263 self.edit_state.single_line()
2264 }
2265
2266 pub(crate) fn set_single_line(&mut self, single_line: bool) {
2267 if !self.edit_state.set_single_line(single_line) {
2268 return;
2269 }
2270 let wrap = self.wrap_mode();
2271 self.edit_state.set_wrap(wrap);
2272 if single_line {
2273 self.scroll_state.reset_vertical();
2274 self.edit_state.set_scroll(self.scroll_state.as_scroll());
2275 }
2276 if !single_line {
2277 self.sync_scroll_state_from_editor();
2278 }
2279 self.ensure_cursor_visible();
2280 self.invalidate_text_layout();
2281 }
2282
2283 #[cfg(test)]
2284 pub(crate) fn scroll(&self) -> glyphon::cosmic_text::Scroll {
2285 self.scroll_state.as_scroll()
2286 }
2287
2288 pub(crate) fn scroll_state(&self) -> TextScrollState {
2289 self.scroll_state.state()
2290 }
2291
2292 pub(crate) fn scroll_horizontal_by(&mut self, delta: f32) {
2293 let changed = self.edit_state.scroll_horizontal_by(delta);
2294 if changed {
2295 self.sync_scroll_state_from_editor();
2296 self.sync_layout_snapshot_scroll();
2297 self.invalidate_layout_geometry();
2298 }
2299 }
2300
2301 pub(crate) fn scroll_vertical_by(&mut self, delta: f32) {
2302 let scroll_before = self.scroll_state.as_scroll();
2303 self.edit_state.scroll_vertical_by(delta);
2304 self.sync_scroll_state_from_editor();
2305 if self.scroll_state.as_scroll() != scroll_before {
2306 self.sync_layout_snapshot_scroll();
2307 self.invalidate_layout_geometry();
2308 }
2309 }
2310
2311 pub(crate) fn scroll_horizontal_consumed(&mut self, delta: f32) -> f32 {
2316 let before = self.scroll_state().horizontal();
2317 self.scroll_horizontal_by(delta);
2318 self.scroll_state().horizontal() - before
2319 }
2320
2321 pub(crate) fn scroll_vertical_consumed(&mut self, delta: f32) -> f32 {
2326 let before = self.scroll_state().vertical();
2327 self.scroll_vertical_by(delta);
2328 self.scroll_state().vertical() - before
2329 }
2330
2331 pub(crate) fn has_horizontal_overflow(&mut self) -> bool {
2333 self.edit_state.has_horizontal_overflow()
2334 }
2335
2336 pub(crate) fn has_vertical_overflow(&mut self) -> bool {
2338 self.edit_state.has_vertical_overflow()
2339 }
2340
2341 pub fn display_transform_active(&self) -> bool {
2343 self.edit_state.display_transform().is_some()
2344 }
2345
2346 pub(crate) fn display_transform(&self) -> Option<DisplayTransform> {
2349 self.edit_state.display_transform()
2350 }
2351
2352 pub(crate) fn handle_click(
2355 &mut self,
2356 position: PxPosition,
2357 window_position: PxPosition,
2358 timestamp: Instant,
2359 ) -> ClickType {
2360 self.selection_state
2361 .handle_click(position, window_position, timestamp)
2362 }
2363
2364 pub(crate) fn start_drag(&mut self, click_type: ClickType) {
2366 let origin_selection = self
2367 .selection_state
2368 .drag_origin_for_click(click_type, self.selection());
2369 self.selection_state
2370 .start_drag(click_type, origin_selection);
2371 }
2372
2373 pub fn is_dragging(&self) -> bool {
2375 self.selection_state.is_dragging()
2376 }
2377
2378 pub(crate) fn stop_drag(&mut self) {
2380 self.selection_state.stop_drag();
2381 }
2382
2383 pub(crate) fn last_click_position(&self) -> Option<PxPosition> {
2385 self.selection_state.last_click_position()
2386 }
2387
2388 pub(crate) fn last_click_time(&self) -> Option<Instant> {
2389 self.selection_state.last_click_time()
2390 }
2391
2392 pub(crate) fn update_last_click_position(&mut self, position: PxPosition) {
2394 self.selection_state.update_last_click_position(position);
2395 }
2396
2397 fn ensure_cursor_visible(&mut self) {
2398 let mut scroll_after = self.edit_state.shape_until_cursor();
2399 if self.single_line() && (scroll_after.line != 0 || scroll_after.vertical != 0.0) {
2400 scroll_after.line = 0;
2404 scroll_after.vertical = 0.0;
2405 self.edit_state.set_scroll(scroll_after);
2406 }
2407 if self.scroll_state.sync_from_scroll(scroll_after) {
2408 self.sync_layout_snapshot_scroll();
2409 }
2410 }
2411
2412 #[cfg(test)]
2413 fn current_raw_layout_editor(&self) -> &glyphon::Editor<'static> {
2414 self.edit_state.editor()
2415 }
2416
2417 fn current_layout_geometry(&self) -> Option<DerivedLayoutGeometry> {
2418 self.layout_state.derived_geometry(
2419 DerivedGeometryInput {
2420 scroll_horizontal: Px(self.scroll_state.horizontal().round() as i32),
2421 cursor_offset: self.cursor_offset(),
2422 selection: self.has_selection().then(|| self.selection()),
2423 composition_range: self.ime_state.composition_range(),
2424 raw_composition: self.ime_state.composition_cursor_range(&self.edit_state),
2425 },
2426 self.edit_state.editor(),
2427 )
2428 }
2429
2430 fn apply_cursor_and_selection_offsets(
2431 &mut self,
2432 cursor_offset: usize,
2433 selection: Option<TextSelection>,
2434 ) {
2435 self.edit_state
2436 .set_cursor_and_selection_offsets(cursor_offset, selection);
2437 self.reset_cursor_blink();
2438 self.ensure_cursor_visible();
2439 self.invalidate_layout_geometry();
2440 }
2441
2442 pub(crate) fn apply_pointer_action(&mut self, action: glyphon::Action) {
2443 let text = self.text();
2444 match self.layout_state.pointer_action_outcome(
2445 self.edit_state.editor(),
2446 PointerActionContext {
2447 raw_text: &text,
2448 cursor_offset: self.cursor_offset(),
2449 selection: self.has_selection().then(|| self.selection()),
2450 drag_selection_mode: self.selection_state.drag_selection_mode(),
2451 drag_origin_selection: self.selection_state.drag_origin_selection(),
2452 action,
2453 },
2454 ) {
2455 PointerActionOutcome::Selection(selection) => {
2456 self.apply_cursor_and_selection_offsets(selection.end, Some(selection));
2457 }
2458 PointerActionOutcome::Cursor {
2459 cursor_offset,
2460 selection,
2461 } => {
2462 self.apply_cursor_and_selection_offsets(cursor_offset, selection);
2463 }
2464 PointerActionOutcome::ApplyAction(action) => {
2465 self.edit_state.apply_action(action);
2466 self.reset_cursor_blink();
2467 self.ensure_cursor_visible();
2468 self.invalidate_text_layout();
2469 }
2470 PointerActionOutcome::Ignored => {}
2471 }
2472 }
2473
2474 fn motion_target_offset(&mut self, motion: cosmic_text::Motion) -> Option<usize> {
2475 if let Some(next_offset) = self.layout_state.transformed_motion_target_offset(
2476 self.cursor_offset(),
2477 self.has_selection().then(|| self.selection()),
2478 motion,
2479 ) {
2480 return Some(next_offset);
2481 }
2482
2483 self.edit_state.motion_target_offset(motion)
2484 }
2485
2486 fn paragraph_motion_offset(&self, forward: bool) -> usize {
2487 self.layout_state
2488 .paragraph_motion_offset(&self.text(), self.cursor_offset(), forward)
2489 }
2490
2491 fn move_cursor_with_motion(&mut self, motion: cosmic_text::Motion) -> bool {
2492 let Some(next_offset) = self.motion_target_offset(motion) else {
2493 return false;
2494 };
2495 self.apply_cursor_and_selection_offsets(next_offset, None);
2496 true
2497 }
2498
2499 fn move_cursor_with_paragraph_motion(&mut self, forward: bool) -> bool {
2500 let next_offset = self.paragraph_motion_offset(forward);
2501 self.apply_cursor_and_selection_offsets(next_offset, None);
2502 true
2503 }
2504
2505 fn collapse_selection(&mut self, collapse_to_end: bool) -> bool {
2506 let selection = self.selection();
2507 let Some(target) = self
2508 .selection_state
2509 .collapse_target(selection, collapse_to_end)
2510 else {
2511 return false;
2512 };
2513
2514 self.edit_state
2515 .set_cursor_and_selection(self.text_offset_to_cursor(target), None);
2516 self.reset_cursor_blink();
2517 self.ensure_cursor_visible();
2518 self.invalidate_layout_geometry();
2519 true
2520 }
2521
2522 fn extend_selection_with_motion(&mut self, motion: cosmic_text::Motion) -> bool {
2523 if let Some(next_offset) = self.motion_target_offset(motion) {
2524 let selection = self.selection_state.extended_selection(
2525 self.selection(),
2526 self.cursor_offset(),
2527 next_offset,
2528 );
2529 self.apply_cursor_and_selection_offsets(next_offset, selection);
2530 return true;
2531 }
2532 false
2533 }
2534
2535 fn extend_selection_with_paragraph_motion(&mut self, forward: bool) -> bool {
2536 let next_offset = self.paragraph_motion_offset(forward);
2537 let selection = self.selection_state.extended_selection(
2538 self.selection(),
2539 self.cursor_offset(),
2540 next_offset,
2541 );
2542 self.apply_cursor_and_selection_offsets(next_offset, selection);
2543 true
2544 }
2545
2546 pub(crate) fn deletion_range_for_motion(
2547 &mut self,
2548 motion: cosmic_text::Motion,
2549 ) -> Option<std::ops::Range<usize>> {
2550 let target_offset = self.motion_target_offset(motion);
2551 self.selection_state
2552 .deletion_range(self.selection(), self.cursor_offset(), target_offset)
2553 }
2554
2555 pub(crate) fn extend_selection_to_point(&mut self, position: PxPosition) -> bool {
2556 let selection = self.selection();
2557 let cursor_offset = self.cursor_offset();
2558 if let Some((transformed_text, next_offset)) = self.layout_state.display_hit_offset(
2559 cursor_offset,
2560 self.has_selection().then(|| selection.clone()),
2561 position.x_px().0,
2562 position.y_px().0,
2563 ) {
2564 let next_offset = transformed_text.map_to_raw(next_offset);
2565 let next_selection = self.selection_state.extended_selection(
2566 selection.clone(),
2567 cursor_offset,
2568 next_offset,
2569 );
2570 self.apply_cursor_and_selection_offsets(next_offset, next_selection);
2571 return true;
2572 }
2573
2574 let Some(next_offset) = TextLayoutState::raw_hit_offset(
2575 self.edit_state.editor(),
2576 position.x_px().0,
2577 position.y_px().0,
2578 ) else {
2579 return false;
2580 };
2581 let next_selection =
2582 self.selection_state
2583 .extended_selection(selection, cursor_offset, next_offset);
2584 self.apply_cursor_and_selection_offsets(next_offset, next_selection);
2585 true
2586 }
2587
2588 pub fn map_key_event_to_action(
2606 &mut self,
2607 key_event: tessera_ui::KeyboardEvent,
2608 key_modifiers: winit::keyboard::ModifiersState,
2609 ) -> Option<Vec<glyphon::Action>> {
2610 let is_ctrl = key_modifiers.control_key() || key_modifiers.super_key();
2611 let is_shift = key_modifiers.shift_key();
2612
2613 match key_event.state {
2614 winit::event::ElementState::Pressed => {}
2615 winit::event::ElementState::Released => return None,
2616 }
2617
2618 match key_event.logical_key {
2619 winit::keyboard::Key::Named(named_key) => {
2620 self.handle_named_key(named_key, is_ctrl, is_shift)
2621 }
2622
2623 winit::keyboard::Key::Character(s) => {
2624 if is_ctrl {
2625 return None;
2626 }
2627 Some(s.chars().map(glyphon::Action::Insert).collect::<Vec<_>>())
2628 }
2629 _ => None,
2630 }
2631 }
2632
2633 fn handle_named_key(
2634 &mut self,
2635 named_key: NamedKey,
2636 is_ctrl: bool,
2637 is_shift: bool,
2638 ) -> Option<Vec<glyphon::Action>> {
2639 match named_key {
2640 NamedKey::Backspace => Some(vec![glyphon::Action::Backspace]),
2641 NamedKey::Delete => Some(vec![glyphon::Action::Delete]),
2642 NamedKey::Enter => Some(vec![glyphon::Action::Enter]),
2643 NamedKey::Escape => Some(vec![glyphon::Action::Escape]),
2644 NamedKey::Tab => Some(vec![glyphon::Action::Insert('\t')]),
2645 NamedKey::ArrowLeft => {
2646 let motion = if is_ctrl {
2647 cosmic_text::Motion::LeftWord
2648 } else {
2649 cosmic_text::Motion::Left
2650 };
2651 if is_shift {
2652 self.extend_selection_with_motion(motion);
2653 None
2654 } else {
2655 (!self.collapse_selection(false) && !self.move_cursor_with_motion(motion))
2656 .then(|| vec![glyphon::Action::Motion(motion)])
2657 }
2658 }
2659 NamedKey::ArrowRight => {
2660 let motion = if is_ctrl {
2661 cosmic_text::Motion::RightWord
2662 } else {
2663 cosmic_text::Motion::Right
2664 };
2665 if is_shift {
2666 self.extend_selection_with_motion(motion);
2667 None
2668 } else {
2669 (!self.collapse_selection(true) && !self.move_cursor_with_motion(motion))
2670 .then(|| vec![glyphon::Action::Motion(motion)])
2671 }
2672 }
2673 NamedKey::ArrowUp => {
2674 if is_ctrl {
2675 if is_shift {
2676 self.extend_selection_with_paragraph_motion(false);
2677 } else {
2678 let _ = self.collapse_selection(false)
2679 || self.move_cursor_with_paragraph_motion(false);
2680 }
2681 return None;
2682 }
2683 if is_shift {
2684 self.extend_selection_with_motion(cosmic_text::Motion::Up);
2685 return None;
2686 }
2687 if self.collapse_selection(false) {
2688 return None;
2689 }
2690 if self.move_cursor_with_motion(cosmic_text::Motion::Up) {
2691 return None;
2692 }
2693 if self.edit_state.cursor_line() == 0 {
2696 self.edit_state
2697 .set_cursor_and_selection(self.edit_state.first_line_start_cursor(), None);
2698 self.reset_cursor_blink();
2699 self.ensure_cursor_visible();
2700
2701 return None;
2702 }
2703
2704 Some(vec![glyphon::Action::Motion(cosmic_text::Motion::Up)])
2705 }
2706 NamedKey::ArrowDown => {
2707 if is_ctrl {
2708 if is_shift {
2709 self.extend_selection_with_paragraph_motion(true);
2710 } else {
2711 let _ = self.collapse_selection(true)
2712 || self.move_cursor_with_paragraph_motion(true);
2713 }
2714 return None;
2715 }
2716 if is_shift {
2717 self.extend_selection_with_motion(cosmic_text::Motion::Down);
2718 return None;
2719 }
2720 if self.collapse_selection(true) {
2721 return None;
2722 }
2723 if self.move_cursor_with_motion(cosmic_text::Motion::Down) {
2724 return None;
2725 }
2726 let last_line_index = self.edit_state.last_line_index();
2727
2728 if self.edit_state.cursor_line() >= last_line_index {
2731 self.edit_state
2732 .set_cursor_and_selection(self.edit_state.last_line_end_cursor(), None);
2733 self.reset_cursor_blink();
2734 self.ensure_cursor_visible();
2735 return None;
2736 }
2737
2738 Some(vec![glyphon::Action::Motion(cosmic_text::Motion::Down)])
2739 }
2740 NamedKey::PageUp => {
2741 if is_shift {
2742 self.extend_selection_with_motion(cosmic_text::Motion::PageUp);
2743 None
2744 } else {
2745 (!self.collapse_selection(false)
2746 && !self.move_cursor_with_motion(cosmic_text::Motion::PageUp))
2747 .then(|| vec![glyphon::Action::Motion(cosmic_text::Motion::PageUp)])
2748 }
2749 }
2750 NamedKey::PageDown => {
2751 if is_shift {
2752 self.extend_selection_with_motion(cosmic_text::Motion::PageDown);
2753 None
2754 } else {
2755 (!self.collapse_selection(true)
2756 && !self.move_cursor_with_motion(cosmic_text::Motion::PageDown))
2757 .then(|| vec![glyphon::Action::Motion(cosmic_text::Motion::PageDown)])
2758 }
2759 }
2760 NamedKey::Home => {
2761 let motion = if is_ctrl {
2762 cosmic_text::Motion::BufferStart
2763 } else {
2764 cosmic_text::Motion::Home
2765 };
2766 if is_shift {
2767 self.extend_selection_with_motion(motion);
2768 None
2769 } else if self.move_cursor_with_motion(motion) {
2770 None
2771 } else {
2772 Some(vec![glyphon::Action::Motion(motion)])
2773 }
2774 }
2775 NamedKey::End => {
2776 let motion = if is_ctrl {
2777 cosmic_text::Motion::BufferEnd
2778 } else {
2779 cosmic_text::Motion::End
2780 };
2781 if is_shift {
2782 self.extend_selection_with_motion(motion);
2783 None
2784 } else if self.move_cursor_with_motion(motion) {
2785 None
2786 } else {
2787 Some(vec![glyphon::Action::Motion(motion)])
2788 }
2789 }
2790 NamedKey::Space => Some(vec![glyphon::Action::Insert(' ')]),
2791 _ => None,
2792 }
2793 }
2794
2795 pub fn set_text(&mut self, text: &str) {
2802 let cursor = self.cursor_offset();
2803 self.set_text_and_selection(text, TextSelection::collapsed(cursor));
2804 }
2805
2806 pub fn text(&self) -> String {
2808 self.edit_state.text()
2809 }
2810
2811 pub(crate) fn cursor_offset(&self) -> usize {
2812 self.edit_state.cursor_offset()
2813 }
2814
2815 pub(crate) fn selection(&self) -> TextSelection {
2816 self.edit_state.selection()
2817 }
2818
2819 pub(crate) fn composition(&self) -> Option<&ImeComposition> {
2820 self.ime_state.composition()
2821 }
2822
2823 #[cfg(test)]
2824 pub(crate) fn set_composition(&mut self, composition: Option<ImeComposition>) {
2825 self.ime_state.set_composition(composition);
2826 }
2827
2828 pub(crate) fn clear_composition(&mut self) {
2829 if self.ime_state.clear() {
2830 self.invalidate_layout_geometry();
2831 }
2832 }
2833
2834 pub(crate) fn plan_ime_event(
2835 &self,
2836 single_line: bool,
2837 event: &winit::event::Ime,
2838 ) -> Option<PlannedImeEvent> {
2839 self.ime_state
2840 .plan_event(self.selection(), single_line, &self.text(), event)
2841 }
2842
2843 pub(crate) fn commit_ime_edit(&mut self, plan: &PlannedImeEdit, result: &ImeEditResult) {
2844 self.ime_state.composition = plan.composition_range.as_ref().and_then(|range| {
2845 let text = self.text();
2846 let replacement = &text[result.replaced_range.clone()];
2847 let relative = range.start.saturating_sub(plan.replacement_range.start)
2848 ..range.end.saturating_sub(plan.replacement_range.start);
2849 let range = rebase_range(&plan.replacement_text, replacement, relative);
2850 (!range.is_empty()).then(|| ImeComposition {
2851 range: (result.replaced_range.start + range.start)
2852 ..(result.replaced_range.start + range.end),
2853 selection: result.selection.clone(),
2854 })
2855 });
2856 }
2857
2858 pub(crate) fn selected_text(&self) -> Option<String> {
2859 self.edit_state.selected_text()
2860 }
2861
2862 pub(crate) fn has_selection(&self) -> bool {
2863 !self.selection().is_collapsed()
2864 }
2865
2866 pub(crate) fn select_all(&mut self) {
2867 self.edit_state.select_all();
2868 self.reset_cursor_blink();
2869 self.ensure_cursor_visible();
2870 self.invalidate_layout_geometry();
2871 }
2872
2873 pub(crate) fn apply_action_with_pipeline(
2874 &mut self,
2875 action: glyphon::Action,
2876 on_change: CallbackWith<String, String>,
2877 input_transform: Option<CallbackWith<String, String>>,
2878 ) {
2879 let can_apply_directly = input_transform.is_none() && on_change == CallbackWith::identity();
2880 if can_apply_directly {
2881 self.clear_composition();
2882 self.edit_state.apply_action(action);
2883 self.reset_cursor_blink();
2884 self.ensure_cursor_visible();
2885 self.invalidate_text_layout();
2886 return;
2887 }
2888
2889 let preview = self.edit_state.preview_action_result(action);
2890 let raw_content_after_action = preview.text;
2891 let selection_after_action = preview.selection;
2892 let transformed_content = if let Some(transform) = input_transform.as_ref() {
2893 transform.call(raw_content_after_action.clone())
2894 } else {
2895 raw_content_after_action.clone()
2896 };
2897 let transformed_selection = rebase_selection(
2898 &raw_content_after_action,
2899 &transformed_content,
2900 selection_after_action.clone(),
2901 );
2902
2903 let new_content = on_change.call(transformed_content.clone());
2904 let (final_content, final_selection) = if new_content != transformed_content {
2905 (
2906 new_content.clone(),
2907 rebase_selection(&transformed_content, &new_content, transformed_selection),
2908 )
2909 } else {
2910 (transformed_content, transformed_selection)
2911 };
2912
2913 self.clear_composition();
2914 if final_content == raw_content_after_action && final_selection == selection_after_action {
2915 self.edit_state.commit_previewed_action();
2916 self.reset_cursor_blink();
2917 self.ensure_cursor_visible();
2918 self.invalidate_text_layout();
2919 } else {
2920 self.set_text_and_selection(&final_content, final_selection);
2921 }
2922 }
2923
2924 pub(crate) fn replace_text_range_with_pipeline(
2925 &mut self,
2926 range: Range<usize>,
2927 replacement: &str,
2928 selection: TextSelection,
2929 on_change: CallbackWith<String, String>,
2930 input_transform: Option<CallbackWith<String, String>>,
2931 ) -> ImeEditResult {
2932 let preview = self.edit_state.preview_replace_result(range, replacement);
2933 let raw_content_after_replace = preview.text;
2934 let raw_replaced_range = preview.replaced_range;
2935
2936 if raw_content_after_replace == self.text() {
2937 self.set_text_and_selection(&raw_content_after_replace, selection);
2938 return ImeEditResult {
2939 selection: self.selection(),
2940 replaced_range: raw_replaced_range,
2941 };
2942 }
2943
2944 let transformed_content = if let Some(transform) = input_transform.as_ref() {
2945 transform.call(raw_content_after_replace.clone())
2946 } else {
2947 raw_content_after_replace.clone()
2948 };
2949 let transformed_selection =
2950 rebase_selection(&raw_content_after_replace, &transformed_content, selection);
2951 let transformed_replaced_range = rebase_range(
2952 &raw_content_after_replace,
2953 &transformed_content,
2954 raw_replaced_range,
2955 );
2956 let new_content = on_change.call(transformed_content.clone());
2957 let (final_content, final_selection, final_replaced_range) =
2958 if new_content != transformed_content {
2959 (
2960 new_content.clone(),
2961 rebase_selection(&transformed_content, &new_content, transformed_selection),
2962 rebase_range(
2963 &transformed_content,
2964 &new_content,
2965 transformed_replaced_range,
2966 ),
2967 )
2968 } else {
2969 (
2970 transformed_content,
2971 transformed_selection,
2972 transformed_replaced_range,
2973 )
2974 };
2975
2976 self.set_text_and_selection(&final_content, final_selection.clone());
2977 ImeEditResult {
2978 selection: final_selection,
2979 replaced_range: final_replaced_range,
2980 }
2981 }
2982
2983 pub(crate) fn replace_selected_text_with_pipeline(
2984 &mut self,
2985 replacement: &str,
2986 on_change: CallbackWith<String, String>,
2987 input_transform: Option<CallbackWith<String, String>>,
2988 ) -> TextSelection {
2989 let selection = self.selection().ordered_range();
2990 self.replace_text_range_with_pipeline(
2991 selection.clone(),
2992 replacement,
2993 TextSelection::collapsed(selection.start + replacement.len()),
2994 on_change,
2995 input_transform,
2996 )
2997 .selection
2998 }
2999
3000 pub(crate) fn copy_selection_to_clipboard(&self) -> bool {
3001 if let Some(text) = self.selected_text() {
3002 clipboard::set_text(&text);
3003 true
3004 } else {
3005 false
3006 }
3007 }
3008
3009 pub(crate) fn cut_selection_with_pipeline(
3010 &mut self,
3011 on_change: CallbackWith<String, String>,
3012 input_transform: Option<CallbackWith<String, String>>,
3013 ) -> bool {
3014 if !self.copy_selection_to_clipboard() {
3015 return false;
3016 }
3017 self.replace_selected_text_with_pipeline("", on_change, input_transform);
3018 true
3019 }
3020
3021 pub(crate) fn paste_from_clipboard_with_pipeline(
3022 &mut self,
3023 on_change: CallbackWith<String, String>,
3024 input_transform: Option<CallbackWith<String, String>>,
3025 ) -> bool {
3026 let Some(text) = clipboard::get_text() else {
3027 return false;
3028 };
3029 self.replace_selected_text_with_pipeline(&text, on_change, input_transform);
3030 true
3031 }
3032
3033 pub(crate) fn delete_motion_with_pipeline(
3034 &mut self,
3035 motion: cosmic_text::Motion,
3036 on_change: CallbackWith<String, String>,
3037 input_transform: Option<CallbackWith<String, String>>,
3038 ) -> bool {
3039 let Some(range) = self.deletion_range_for_motion(motion) else {
3040 return false;
3041 };
3042 self.replace_text_range_with_pipeline(
3043 range.clone(),
3044 "",
3045 TextSelection::collapsed(range.start),
3046 on_change,
3047 input_transform,
3048 );
3049 true
3050 }
3051
3052 pub(crate) fn set_text_and_selection(&mut self, text: &str, selection: TextSelection) {
3053 self.edit_state.set_text_and_selection(text, selection);
3054 self.reset_cursor_blink();
3055 self.ensure_cursor_visible();
3056 self.ime_state.clear();
3057 self.invalidate_text_layout();
3058 }
3059
3060 fn text_offset_to_cursor(&self, offset: usize) -> Cursor {
3061 self.edit_state.text_offset_to_cursor(offset)
3062 }
3063
3064 fn wrap_mode(&self) -> glyphon::Wrap {
3065 if self.edit_state.single_line() {
3066 glyphon::Wrap::None
3067 } else {
3068 glyphon::Wrap::Glyph
3069 }
3070 }
3071
3072 pub(crate) fn layout_version(&self) -> u64 {
3073 self.layout_state.layout_version()
3074 }
3075
3076 pub(crate) fn text_layout_version(&self) -> u64 {
3077 self.layout_state.text_layout_version()
3078 }
3079
3080 #[cfg(test)]
3081 fn text_cache_key(&self) -> Option<TextLayoutCacheKey> {
3082 self.layout_state.cache_key()
3083 }
3084
3085 #[cfg(test)]
3086 fn snapshot_buffer(&self) -> Option<glyphon::Buffer> {
3087 self.layout_state.snapshot.buffer.clone()
3088 }
3089
3090 pub(crate) fn invalidate_layout_geometry(&mut self) {
3091 self.layout_state.invalidate(LayoutInvalidation::Geometry);
3092 }
3093
3094 pub(crate) fn invalidate_text_layout(&mut self) {
3095 self.layout_state.invalidate(LayoutInvalidation::Text);
3096 }
3097}
3098
3099fn compute_selection_rects_for_range(
3100 buffer: &glyphon::Buffer,
3101 selection: Option<(Cursor, Cursor)>,
3102) -> Vec<RectDef> {
3103 let Some((selection_start, selection_end)) = selection else {
3104 return Vec::new();
3105 };
3106 compute_range_rects(buffer, selection_start, selection_end)
3107}
3108
3109fn compute_selection_rects(editor: &glyphon::Editor) -> Vec<RectDef> {
3110 let selection = match editor.selection() {
3111 Selection::None => None,
3112 Selection::Normal(anchor) | Selection::Line(anchor) | Selection::Word(anchor) => {
3113 Some((anchor, editor.cursor()))
3114 }
3115 };
3116
3117 editor.with_buffer(|buffer| compute_selection_rects_for_range(buffer, selection))
3118}
3119
3120fn editor_cursor_offset(editor: &glyphon::Editor<'_>) -> usize {
3121 editor.with_buffer(|buffer| cursor_to_text_offset_in_buffer(buffer, editor.cursor()))
3122}
3123
3124fn fallback_buffer_hit(buffer: &glyphon::Buffer, x: f32, y: f32) -> Option<Cursor> {
3125 let mut closest_line_y = None::<(f32, f32)>;
3126
3127 for run in buffer.layout_runs() {
3128 let line_top = run.line_top;
3129 let line_bottom = line_top + run.line_height;
3130 let distance = if y < line_top {
3131 line_top - y
3132 } else if y >= line_bottom {
3133 y - line_bottom
3134 } else {
3135 0.0
3136 };
3137 let line_mid_y = line_top + (run.line_height * 0.5);
3138
3139 match closest_line_y {
3140 Some((best_distance, _)) if best_distance <= distance => {}
3141 _ => {
3142 closest_line_y = Some((distance, line_mid_y));
3143 }
3144 }
3145 }
3146
3147 let (_, line_mid_y) = closest_line_y?;
3148 buffer.hit(x, line_mid_y)
3149}
3150
3151fn editor_hit_offset(editor: &glyphon::Editor<'_>, x: i32, y: i32) -> Option<usize> {
3152 let cursor = editor.with_buffer(|buffer| {
3153 buffer
3154 .hit(x as f32, y as f32)
3155 .or_else(|| fallback_buffer_hit(buffer, x as f32, y as f32))
3156 })?;
3157 Some(editor.with_buffer(|buffer| cursor_to_text_offset_in_buffer(buffer, cursor)))
3158}
3159
3160fn editor_motion_offset(
3161 editor: &mut glyphon::Editor<'_>,
3162 motion: cosmic_text::Motion,
3163) -> Option<usize> {
3164 let cursor = editor.cursor();
3165 let next_cursor = editor.with_buffer_mut(|buffer| {
3166 buffer
3167 .cursor_motion(&mut shaping_fonts(), cursor, None, motion)
3168 .map(|(next_cursor, _)| next_cursor)
3169 })?;
3170 Some(editor.with_buffer(|buffer| cursor_to_text_offset_in_buffer(buffer, next_cursor)))
3171}
3172
3173fn cursor_to_text_offset_in_buffer(buffer: &glyphon::Buffer, cursor: Cursor) -> usize {
3174 let mut offset = 0usize;
3175 for (line_index, line) in buffer.lines.iter().enumerate() {
3176 let line_len = line.text().len();
3177 if line_index == cursor.line {
3178 return offset + cursor.index.min(line_len);
3179 }
3180 offset += line_len + line.ending().as_str().len();
3181 }
3182 offset
3183}
3184
3185fn text_offset_to_cursor_in_buffer(buffer: &glyphon::Buffer, offset: usize) -> Cursor {
3186 if buffer.lines.is_empty() {
3187 return Cursor::new(0, 0);
3188 }
3189
3190 let mut remaining = offset;
3191 for (line_index, line) in buffer.lines.iter().enumerate() {
3192 let line_len = line.text().len();
3193 if remaining <= line_len {
3194 return Cursor::new(line_index, remaining);
3195 }
3196
3197 let line_total_len = line_len + line.ending().as_str().len();
3198 if remaining < line_total_len {
3199 return Cursor::new(line_index, line_len);
3200 }
3201 remaining = remaining.saturating_sub(line_total_len);
3202 }
3203
3204 let last_line_index = buffer.lines.len().saturating_sub(1);
3205 let last_line_len = buffer.lines[last_line_index].text().len();
3206 Cursor::new(last_line_index, last_line_len)
3207}
3208
3209fn compute_range_rects(
3210 buffer: &glyphon::Buffer,
3211 range_start: Cursor,
3212 range_end: Cursor,
3213) -> Vec<RectDef> {
3214 let mut rects = Vec::new();
3215 let (range_start, range_end) = if cursor_to_text_offset_in_buffer(buffer, range_start)
3216 <= cursor_to_text_offset_in_buffer(buffer, range_end)
3217 {
3218 (range_start, range_end)
3219 } else {
3220 (range_end, range_start)
3221 };
3222
3223 for run in buffer.layout_runs() {
3224 let line_top = Px(run.line_top as i32);
3225 let line_height = Px(run.line_height as i32);
3226
3227 for (x, w) in run.highlight(range_start, range_end) {
3228 rects.push(RectDef {
3229 x: Px(x as i32),
3230 y: line_top,
3231 width: Px(w as i32),
3232 height: line_height,
3233 });
3234 }
3235 }
3236
3237 rects
3238}
3239
3240fn composition_underline_rects(rects: Vec<RectDef>) -> Vec<RectDef> {
3241 const UNDERLINE_HEIGHT: Px = Px(2);
3242
3243 rects
3244 .into_iter()
3245 .map(|mut rect| {
3246 let height = UNDERLINE_HEIGHT.min(rect.height.max(Px(1)));
3247 rect.y += rect.height - height;
3248 rect.height = height;
3249 rect
3250 })
3251 .collect()
3252}
3253
3254fn compute_transformed_composition_rects(
3255 buffer: &glyphon::Buffer,
3256 transformed_text: &TransformedText,
3257 raw_range: Range<usize>,
3258) -> Vec<RectDef> {
3259 let start =
3260 text_offset_to_cursor_in_buffer(buffer, transformed_text.map_from_raw(raw_range.start));
3261 let end = text_offset_to_cursor_in_buffer(buffer, transformed_text.map_from_raw(raw_range.end));
3262 composition_underline_rects(compute_range_rects(buffer, start, end))
3263}
3264
3265fn compute_composition_rects_for_range(
3266 buffer: &glyphon::Buffer,
3267 composition: Option<(Cursor, Cursor)>,
3268) -> Vec<RectDef> {
3269 let Some((start, end)) = composition else {
3270 return Vec::new();
3271 };
3272 composition_underline_rects(compute_range_rects(buffer, start, end))
3273}
3274
3275fn rect_union(rects: &[RectDef]) -> Option<RectDef> {
3276 let mut iter = rects.iter();
3277 let first = iter.next()?.clone();
3278 Some(iter.fold(first, |acc, rect| {
3279 let left = acc.x.min(rect.x);
3280 let top = acc.y.min(rect.y);
3281 let right = (acc.x + acc.width).max(rect.x + rect.width);
3282 let bottom = (acc.y + acc.height).max(rect.y + rect.height);
3283 RectDef {
3284 x: left,
3285 y: top,
3286 width: right - left,
3287 height: bottom - top,
3288 }
3289 }))
3290}
3291
3292fn active_ime_rect(
3293 selection_rects: &[RectDef],
3294 composition_rects: &[RectDef],
3295 cursor_rect: Option<RectDef>,
3296) -> Option<RectDef> {
3297 rect_union(composition_rects)
3298 .or_else(|| rect_union(selection_rects))
3299 .or(cursor_rect)
3300}
3301
3302fn apply_horizontal_scroll_offset(rects: &mut [RectDef], scroll_horizontal: Px) {
3303 if scroll_horizontal == Px::ZERO {
3304 return;
3305 }
3306
3307 for rect in rects {
3308 rect.x -= scroll_horizontal;
3309 }
3310}
3311
3312fn editor_content(editor: &glyphon::Editor) -> String {
3313 editor.with_buffer(|buffer| {
3314 buffer
3315 .lines
3316 .iter()
3317 .map(|line| line.text().to_string() + line.ending().as_str())
3318 .collect::<String>()
3319 })
3320}
3321
3322fn editor_selection(editor: &glyphon::Editor<'_>) -> TextSelection {
3323 match editor.selection() {
3324 glyphon::cosmic_text::Selection::None => {
3325 let cursor = editor.cursor();
3326 let offset =
3327 editor.with_buffer(|buffer| cursor_to_text_offset_in_buffer(buffer, cursor));
3328 TextSelection::collapsed(offset)
3329 }
3330 glyphon::cosmic_text::Selection::Normal(anchor)
3331 | glyphon::cosmic_text::Selection::Line(anchor)
3332 | glyphon::cosmic_text::Selection::Word(anchor) => TextSelection {
3333 start: editor.with_buffer(|buffer| cursor_to_text_offset_in_buffer(buffer, anchor)),
3334 end: editor
3335 .with_buffer(|buffer| cursor_to_text_offset_in_buffer(buffer, editor.cursor())),
3336 },
3337 }
3338}
3339
3340fn rebase_selection(before: &str, after: &str, selection: TextSelection) -> TextSelection {
3341 TextSelection {
3342 start: rebase_offset(before, after, selection.start),
3343 end: rebase_offset(before, after, selection.end),
3344 }
3345}
3346
3347fn rebase_range(before: &str, after: &str, range: Range<usize>) -> Range<usize> {
3348 let start = rebase_offset(before, after, range.start);
3349 let end = rebase_offset(before, after, range.end);
3350 start.min(end)..start.max(end)
3351}
3352
3353fn rebase_offset(before: &str, after: &str, offset: usize) -> usize {
3354 TransformedText::from_strings(before, after.to_string()).map_from_raw(offset.min(before.len()))
3355}
3356
3357fn glyphon_color(color: Color) -> glyphon::Color {
3358 color_to_glyphon(color)
3359}
3360
3361fn editor_font_attrs(style: TextStyle) -> glyphon::Attrs<'static> {
3362 glyphon::Attrs::new()
3363 .family(style.font_family.map_or(
3364 glyphon::fontdb::Family::SansSerif,
3365 glyphon::fontdb::Family::Name,
3366 ))
3367 .weight(glyphon::Weight(style.font_weight.clamp(1, 1000)))
3368 .letter_spacing(if style.font_size.0 > 0.0 {
3369 (style.letter_spacing.0 / style.font_size.0) as f32
3370 } else {
3371 0.0
3372 })
3373}
3374
3375fn build_display_buffer(
3376 text: &str,
3377 color: Color,
3378 font_size: f32,
3379 line_height: f32,
3380 constraint: &TextConstraint,
3381 wrap: glyphon::Wrap,
3382 scroll: glyphon::cosmic_text::Scroll,
3383 style: Option<TextStyle>,
3384) -> glyphon::Buffer {
3385 let mut buffer = glyphon::Buffer::new(
3386 &mut shaping_fonts(),
3387 glyphon::Metrics::new(font_size, line_height),
3388 );
3389 buffer.set_wrap(wrap);
3390 buffer.set_size(constraint.max_width, constraint.max_height);
3391 buffer.set_text(
3392 text,
3393 &editor_font_attrs(style.unwrap_or_default()).color(glyphon_color(color)),
3394 glyphon::Shaping::Advanced,
3395 None,
3396 );
3397 buffer.set_scroll(scroll);
3398 buffer.shape_until_scroll(&mut shaping_fonts(), false);
3399 buffer
3400}
3401
3402fn build_display_editor(
3403 buffer: glyphon::Buffer,
3404 cursor: Cursor,
3405 selection: Option<(Cursor, Cursor)>,
3406) -> glyphon::Editor<'static> {
3407 let mut editor = glyphon::Editor::new(buffer);
3408 if let Some((start, end)) = selection {
3409 editor.set_selection(Selection::Normal(start));
3410 editor.set_cursor(end);
3411 } else {
3412 editor.set_cursor(cursor);
3413 editor.set_selection(Selection::None);
3414 }
3415 editor
3416}
3417
3418fn clip_and_take_visible(rects: Vec<RectDef>, visible_x1: Px, visible_y1: Px) -> Vec<RectDef> {
3419 let visible_x0 = Px(0);
3420 let visible_y0 = Px(0);
3421
3422 rects
3423 .into_iter()
3424 .filter_map(|mut rect| {
3425 let rect_x1 = rect.x + rect.width;
3426 let rect_y1 = rect.y + rect.height;
3427 if rect_x1 <= visible_x0
3428 || rect.y >= visible_y1
3429 || rect.x >= visible_x1
3430 || rect_y1 <= visible_y0
3431 {
3432 None
3433 } else {
3434 let new_x = rect.x.max(visible_x0);
3435 let new_y = rect.y.max(visible_y0);
3436 let new_x1 = rect_x1.min(visible_x1);
3437 let new_y1 = rect_y1.min(visible_y1);
3438 rect.x = new_x;
3439 rect.y = new_y;
3440 rect.width = (new_x1 - new_x).max(Px(0));
3441 rect.height = (new_y1 - new_y).max(Px(0));
3442 Some(rect)
3443 }
3444 })
3445 .collect()
3446}
3447
3448fn measured_editor_size(
3449 text: &TextData,
3450 constraint: &TextConstraint,
3451 single_line: bool,
3452) -> ComputedData {
3453 let [width, height] = text.measure_info().size;
3454 let height = constraint
3455 .max_height
3456 .map_or(height, |max| height.min(Px::from_f32(max).abs()));
3457 let width = Px::from(width) + CURSOR_WIDTH.to_px();
3458 let width = if single_line {
3459 constraint
3460 .max_width
3461 .map_or(width, |max| width.min(Px::from(Px::from_f32(max).abs())))
3462 } else {
3463 width
3464 };
3465 ComputedData {
3466 width,
3467 height: height.into(),
3468 }
3469}
3470
3471#[derive(Clone)]
3472struct TextEditLayout {
3473 controller: State<TextEditorController>,
3474 layout_version: u64,
3475}
3476
3477impl PartialEq for TextEditLayout {
3478 fn eq(&self, other: &Self) -> bool {
3479 self.layout_version == other.layout_version
3480 }
3481}
3482
3483impl tessera_ui::LayoutModifierNode for TextEditLayout {
3484 fn measure(&self, input: &mut MeasureScope<'_>) -> Result<LayoutResult, MeasurementError> {
3485 let display = self.controller.with(TextEditorController::prepare_display);
3486 let fonts = self
3487 .controller
3488 .with(|controller| controller.fonts_for(display.as_ref()));
3489 if self.controller.with(|controller| controller.fonts) != fonts {
3490 self.controller
3491 .with_mut(|controller| controller.set_fonts(fonts));
3492 }
3493 let result = LayoutResult::default();
3494 let max_width_pixels: Option<Px> = input.parent_constraint().width().resolve_max();
3495 let max_height_pixels: Option<Px> = input.parent_constraint().height().resolve_max();
3496
3497 let text_constraint = TextConstraint {
3498 max_width: max_width_pixels.map(|px: Px| px.to_f32()),
3499 max_height: max_height_pixels.map(|px: Px| px.to_f32()),
3500 ..Default::default()
3501 };
3502 if input.is_intrinsic() {
3503 let cached = self
3504 .controller
3505 .with(|controller| controller.cached_intrinsic_size(&text_constraint));
3506 let size = cached.unwrap_or_else(|| {
3507 self.controller.with_mut(|controller| {
3508 controller.measure_intrinsic_size(&text_constraint, display)
3509 })
3510 });
3511 return Ok(result.with_size(size));
3512 }
3513 if let Some((_, _, _, computed_data)) =
3514 self.controller.with(|c| c.cached_layout(&text_constraint))
3515 {
3516 return Ok(result.with_size(computed_data));
3517 }
3518
3519 let text_data = self
3520 .controller
3521 .with_mut(|c| c.text_data(text_constraint.clone(), display));
3522
3523 let line_height = self.controller.with(|c| c.line_height());
3524 let DerivedLayoutGeometry {
3525 mut selection_rects,
3526 mut composition_rects,
3527 cursor_position: cursor_pos_raw,
3528 scroll_horizontal,
3529 } = self.controller.with(|c| {
3530 c.current_layout_geometry()
3531 .expect("layout geometry should be available")
3532 });
3533
3534 apply_horizontal_scroll_offset(&mut selection_rects, scroll_horizontal);
3535 apply_horizontal_scroll_offset(&mut composition_rects, scroll_horizontal);
3536
3537 let visible_x1 = max_width_pixels.unwrap_or(Px(i32::MAX));
3538 let visible_y1 = max_height_pixels.unwrap_or(Px(i32::MAX));
3539 selection_rects = clip_and_take_visible(selection_rects, visible_x1, visible_y1);
3540 composition_rects = clip_and_take_visible(composition_rects, visible_x1, visible_y1);
3541 let cursor_rect = cursor_pos_raw.map(|cursor_pos_raw| RectDef {
3542 x: Px(cursor_pos_raw.0) - scroll_horizontal,
3543 y: Px(cursor_pos_raw.1),
3544 width: CURSOR_WIDTH.to_px(),
3545 height: line_height,
3546 });
3547 let ime_rect = active_ime_rect(&selection_rects, &composition_rects, cursor_rect.clone());
3548 let computed_data = self.controller.with(|controller| {
3549 measured_editor_size(&text_data, &text_constraint, controller.single_line())
3550 });
3551 self.controller.with_mut(|c| {
3552 c.update_layout_geometry(
3553 &text_constraint,
3554 selection_rects.clone(),
3555 composition_rects.clone(),
3556 cursor_rect.clone(),
3557 ime_rect,
3558 computed_data,
3559 );
3560 });
3561
3562 Ok(result.with_size(computed_data))
3563 }
3564}
3565
3566#[derive(Clone, PartialEq)]
3567struct TextEditDraw {
3568 controller: State<TextEditorController>,
3569 presentation_focused: bool,
3570 blink: State<(u64, bool)>,
3571}
3572
3573impl tessera_ui::DrawModifierNode for TextEditDraw {
3574 fn draw(
3575 &self,
3576 ctx: &mut tessera_ui::DrawModifierContext<'_, '_>,
3577 content: &mut dyn tessera_ui::DrawModifierContent,
3578 ) {
3579 let input = &mut *ctx.render_input;
3580 let mut metadata = input.metadata_mut();
3581 metadata.set_clips_children(true);
3582 let focus = self.controller.with(|c| c.focus_handler().clone());
3583 let focused = focus.is_focused();
3584 let (
3585 selection_rects,
3586 composition_rects,
3587 cursor_rect,
3588 selection_color,
3589 composition_color,
3590 cursor_color,
3591 cursor_visible,
3592 text_data,
3593 ) = self.controller.with(|c| {
3594 let (blink_start, visible) = self.blink.get();
3595 let cursor_visible = if focused {
3596 blink_start != c.blink_start_frame_nanos() || visible
3597 } else {
3598 self.presentation_focused
3599 };
3600 (
3601 c.current_selection_rects().to_vec(),
3602 c.current_composition_rects().to_vec(),
3603 c.current_cursor_rect(),
3604 c.selection_color(),
3605 c.cursor_color(),
3606 c.cursor_color(),
3607 cursor_visible,
3608 c.current_text_data(),
3609 )
3610 });
3611 let fragment = metadata.fragment_mut();
3612 push_highlight_rects(fragment, &selection_rects, selection_color);
3613 push_highlight_rects(fragment, &composition_rects, composition_color);
3614 if cursor_visible && let Some(cursor_rect) = cursor_rect {
3615 push_rect(fragment, &cursor_rect, cursor_color);
3616 }
3617 if let Some(text_data) = text_data {
3618 fragment.push_draw_command(TextCommand {
3619 data: text_data,
3620 offset: PxPosition::ZERO,
3621 });
3622 }
3623 content.draw(input);
3624 }
3625}
3626
3627#[tessera]
3642pub fn text_edit_core(
3643 controller: Option<State<TextEditorController>>,
3644 presentation_focused: Option<bool>,
3645) {
3646 let controller = controller.expect("text_edit_core requires a controller");
3647 let layout_version = controller.with(|c| c.layout_version());
3648 let policy = TextEditLayout {
3649 controller,
3650 layout_version,
3651 };
3652 let initial_blink = (controller.with(|c| c.blink_start_frame_nanos()), true);
3653 let blink = remember(|| initial_blink);
3654 let draw = TextEditDraw {
3655 controller,
3656 presentation_focused: presentation_focused.unwrap_or(false),
3657 blink,
3658 };
3659 layout().policy(policy).draw(draw).child(move || {
3660 let focused = controller.with(|c| c.focus_handler().is_focused());
3661 if focused {
3662 receive_frame_nanos(move |frame_nanos| {
3663 let (is_focused, blink_start) = controller.with(|controller| {
3664 (
3665 controller.focus_handler().is_focused(),
3666 controller.blink_start_frame_nanos(),
3667 )
3668 });
3669 if !is_focused {
3670 return tessera_ui::FrameNanosControl::Stop;
3671 }
3672 let visible = frame_nanos.saturating_sub(blink_start) % 1_000_000_000 < 500_000_000;
3673 let next = (blink_start, visible);
3674 if blink.get() != next {
3675 blink.set(next);
3676 }
3677 tessera_ui::FrameNanosControl::Continue
3678 });
3679 }
3680 });
3681}
3682
3683#[cfg(test)]
3684mod tests {
3685 use super::{
3686 ClickType, RectDef, TextEditorController, TextLayoutCacheKey, TextSelection,
3687 TransformedText, active_ime_rect, build_display_buffer, build_display_editor,
3688 compute_transformed_composition_rects, text_offset_to_cursor_in_buffer,
3689 };
3690 use crate::pipelines::text::command::TextConstraint;
3691 use glyphon::{Action as GlyphonAction, Edit as _, cosmic_text::Motion};
3692 use tessera_ui::winit::keyboard::NamedKey;
3693 use tessera_ui::{CallbackWith, ComputedData, Dp, Px, winit};
3694
3695 mod layout_reuse {
3696
3697 use tessera_ui::{
3698 Dp, State,
3699 layout::layout,
3700 remember, tessera,
3701 testing::{layout_test, pump_layout_test_session, start_layout_test_session},
3702 };
3703
3704 use super::super::{
3705 DisplayTransform, TextEditorController, TransformedText, text_edit_core,
3706 };
3707
3708 #[derive(Clone, Default, PartialEq)]
3709 struct Probe {
3710 controller: tessera_ui::testing::TestProbe<Option<State<TextEditorController>>>,
3711 transform: tessera_ui::testing::TestProbe<Option<DisplayTransform>>,
3712 enabled: tessera_ui::testing::TestProbe<Option<State<bool>>>,
3713 width: tessera_ui::testing::TestProbe<Option<State<i32>>>,
3714 size: tessera_ui::testing::TestProbe<Option<tessera_ui::ComputedData>>,
3715 }
3716
3717 #[tessera]
3718 fn editor_cache_contents(controller: State<TextEditorController>) {
3719 let _ = controller.with(|controller| controller.layout_version());
3720 text_edit_core().controller(controller);
3721 }
3722
3723 #[tessera]
3724 fn editor_cache_probe(probe: Probe) {
3725 let controller = remember(|| {
3726 let mut controller = TextEditorController::new(Dp(14.0), None);
3727 controller.set_text("a");
3728 controller
3729 });
3730 probe.controller.set(Some(controller));
3731 layout().child(move || {
3732 editor_cache_contents(controller);
3733 });
3734 }
3735
3736 #[derive(Clone, PartialEq)]
3737 struct ProbeAfterFinal {
3738 enabled: State<bool>,
3739 width: State<i32>,
3740 output: tessera_ui::testing::TestProbe<Option<tessera_ui::ComputedData>>,
3741 }
3742
3743 impl tessera_ui::LayoutModifierNode for ProbeAfterFinal {
3744 fn measure(
3745 &self,
3746 input: &mut tessera_ui::layout::MeasureScope<'_>,
3747 ) -> Result<tessera_ui::layout::LayoutResult, tessera_ui::MeasurementError>
3748 {
3749 let child = input.children()[0];
3750 let normal = child.measure(
3751 input,
3752 &tessera_ui::Constraint::exact(tessera_ui::Px(200), tessera_ui::Px(80)),
3753 )?;
3754 if self.enabled.get() {
3755 let natural = child.probe_intrinsic(
3756 input,
3757 &tessera_ui::Constraint::new(
3758 tessera_ui::AxisConstraint::exact(tessera_ui::Px(self.width.get())),
3759 tessera_ui::AxisConstraint::NONE,
3760 ),
3761 )?;
3762 self.output.set(Some(natural.size()));
3763 }
3764 let mut result = tessera_ui::layout::LayoutResult::new(normal.size());
3765 result.place_child(child, tessera_ui::PxPosition::ZERO);
3766 Ok(result)
3767 }
3768 }
3769
3770 #[tessera]
3771 fn intrinsic_editor_probe(probe: Probe) {
3772 let display_transform = DisplayTransform::new(|raw: String| {
3773 TransformedText::from_strings(&raw, String::new())
3774 });
3775 probe.transform.set(Some(display_transform));
3776 let controller = remember(|| {
3777 let mut controller = TextEditorController::new(Dp(14.0), None);
3778 controller.set_text("one two three four five six seven eight nine");
3779 controller
3780 });
3781 probe.controller.set(Some(controller));
3782 let enabled = remember(|| false);
3783 let width = remember(|| 40_i32);
3784 probe.enabled.set(Some(enabled));
3785 probe.width.set(Some(width));
3786 layout()
3787 .policy(ProbeAfterFinal {
3788 enabled,
3789 width,
3790 output: probe.size.clone(),
3791 })
3792 .child(move || {
3793 editor_cache_contents(controller);
3794 });
3795 }
3796
3797 #[test]
3798 fn intrinsic_editor_probe_preserves_final_layout_and_invalidates_only_its_size_cache() {
3799 use super::super::{CURSOR_WIDTH, TextLayoutCacheKey};
3800 use crate::{pipelines::text::command::TextConstraint, theme::TextStyle};
3801 use tessera_ui::Px;
3802
3803 let probe = Probe::default();
3804 let output = probe.clone();
3805 let mut session = start_layout_test_session(
3806 layout_test(move || {
3807 intrinsic_editor_probe(output.clone());
3808 })
3809 .viewport_px(400, 200),
3810 );
3811 for frame in 0..4 {
3812 pump_layout_test_session(&mut session, frame * 16_666_667);
3813 }
3814 let controller = probe.controller.get().unwrap();
3815 let normal = TextConstraint {
3816 max_width: Some(200.0),
3817 max_height: Some(80.0),
3818 ..Default::default()
3819 };
3820 let before = controller.with(|controller| {
3821 (
3822 controller.text_cache_key(),
3823 controller.current_text_data().unwrap().measure_info().size,
3824 controller.current_cursor_rect(),
3825 controller.layout_state.ime_rect(),
3826 controller
3827 .edit_state
3828 .with_buffer(|buffer| (buffer.size(), buffer.scroll())),
3829 )
3830 });
3831 assert!(before.2.is_some());
3832 probe.enabled.get().unwrap().set(true);
3833 for frame in 4..8 {
3834 pump_layout_test_session(&mut session, frame * 16_666_667);
3835 }
3836 let after = controller.with(|controller| {
3837 (
3838 controller.text_cache_key(),
3839 controller.current_text_data().unwrap().measure_info().size,
3840 controller.current_cursor_rect(),
3841 controller.layout_state.ime_rect(),
3842 controller
3843 .edit_state
3844 .with_buffer(|buffer| (buffer.size(), buffer.scroll())),
3845 )
3846 });
3847 assert_eq!(
3848 after, before,
3849 "intrinsic probing must not publish editor viewport, draw snapshot, cursor, IME or scroll changes"
3850 );
3851 let narrow = probe.size.get().unwrap();
3852 assert!(narrow.height > Px(before.1[1] as i32));
3853 probe.width.get().unwrap().set(80);
3854 for frame in 8..11 {
3855 pump_layout_test_session(&mut session, frame * 16_666_667);
3856 }
3857 assert!(
3858 probe.size.get().unwrap().height < narrow.height,
3859 "a wider intrinsic constraint must reshape wrapped content"
3860 );
3861 controller.with_mut(|controller| controller.set_text("hi"));
3862 for frame in 11..14 {
3863 pump_layout_test_session(&mut session, frame * 16_666_667);
3864 }
3865 let short = probe.size.get().unwrap();
3866 assert!(short.height < narrow.height);
3867 controller.with_mut(|controller| {
3868 controller.set_text_style(TextStyle {
3869 font_size: Dp(24.0),
3870 line_height: Some(Dp(44.0)),
3871 ..Default::default()
3872 })
3873 });
3874 for frame in 14..17 {
3875 pump_layout_test_session(&mut session, frame * 16_666_667);
3876 }
3877 assert!(probe.size.get().unwrap().height >= Px(44));
3878 assert!(probe.size.get().unwrap().height > short.height);
3879 controller
3880 .with_mut(|controller| controller.set_display_transform(probe.transform.get()));
3881 for frame in 17..20 {
3882 pump_layout_test_session(&mut session, frame * 16_666_667);
3883 }
3884 assert_eq!(
3885 probe.size.get().unwrap().width,
3886 CURSOR_WIDTH.to_px(),
3887 "intrinsic sizing must use transformed display text"
3888 );
3889 controller.with(|controller| {
3890 assert!(controller.cached_layout(&normal).is_some());
3891 assert_eq!(
3892 controller.text_cache_key(),
3893 Some(TextLayoutCacheKey::new(
3894 controller.text_layout_version(),
3895 &normal
3896 ))
3897 );
3898 assert_eq!(
3899 controller.edit_state.with_buffer(|buffer| buffer.size()),
3900 (Some(200.0), Some(80.0))
3901 );
3902 });
3903 let settled = session
3904 .work_totals()
3905 .count("editor_cache_contents", tessera_ui::WorkPhase::Build);
3906 for frame in 20..28 {
3907 pump_layout_test_session(&mut session, frame * 16_666_667);
3908 }
3909 assert_eq!(
3910 session
3911 .work_totals()
3912 .count("editor_cache_contents", tessera_ui::WorkPhase::Build),
3913 settled,
3914 "a cached intrinsic probe must not perpetually invalidate editor readers"
3915 );
3916 }
3917
3918 #[test]
3919 fn cached_text_measurement_stops_rebuilding_without_edits() {
3920 let probe = Probe::default();
3921 let output = probe.clone();
3922 let mut session = start_layout_test_session(
3923 layout_test(move || {
3924 editor_cache_probe(output.clone());
3925 })
3926 .viewport_px(400, 100),
3927 );
3928 for frame in 0..4 {
3929 let _ = pump_layout_test_session(&mut session, frame * 16_666_667);
3930 }
3931 let settled_builds = session
3932 .work_totals()
3933 .count("editor_cache_contents", tessera_ui::WorkPhase::Build);
3934 for frame in 4..12 {
3935 let _ = pump_layout_test_session(&mut session, frame * 16_666_667);
3936 }
3937 assert_eq!(
3938 session
3939 .work_totals()
3940 .count("editor_cache_contents", tessera_ui::WorkPhase::Build),
3941 settled_builds
3942 );
3943
3944 let controller = probe.controller.get().expect("editor controller");
3945 let initial_width =
3946 controller.with(|c| c.current_text_data().unwrap().measure_info().size[0]);
3947 controller.with_mut(|c| c.set_text("a much longer value"));
3948 for frame in 12..16 {
3949 let _ = pump_layout_test_session(&mut session, frame * 16_666_667);
3950 }
3951 assert!(
3952 session
3953 .work_totals()
3954 .count("editor_cache_contents", tessera_ui::WorkPhase::Build)
3955 > settled_builds
3956 );
3957 assert!(
3958 controller.with(|c| c.current_text_data().unwrap().measure_info().size[0])
3959 > initial_width
3960 );
3961 }
3962 }
3963
3964 #[test]
3965 fn native_ime_edits_preserve_composition_and_selection() {
3966 #[tessera_ui::tessera]
3967 fn snapshot_contract() {
3968 let mut controller = controller_with_text("old");
3969 for (selection, composition) in [
3970 (8..1, Some(1..8)),
3971 (5..8, Some(5..8)),
3972 (9..9, None),
3973 (8..1, None),
3974 ] {
3975 let event = winit::event::Ime::TextState(winit::event::ImeTextState {
3976 text: "a😃中z".into(),
3977 selection: selection.clone(),
3978 composition: composition.clone(),
3979 });
3980 let Some(super::PlannedImeEvent::Edit(plan)) =
3981 controller.plan_ime_event(false, &event)
3982 else {
3983 panic!("text snapshot must produce an edit");
3984 };
3985 let on_change = if controller.text() == "a😃中z" {
3986 CallbackWith::new(|_| {
3987 panic!("selection and composition must not rerun on_change")
3988 })
3989 } else {
3990 CallbackWith::identity()
3991 };
3992 let result = controller.replace_text_range_with_pipeline(
3993 plan.replacement_range.clone(),
3994 &plan.replacement_text,
3995 plan.selection.clone(),
3996 on_change,
3997 None,
3998 );
3999 controller.commit_ime_edit(&plan, &result);
4000 assert_eq!(controller.text(), "a😃中z");
4001 assert_eq!(
4002 controller.selection(),
4003 TextSelection {
4004 start: selection.start,
4005 end: selection.end
4006 }
4007 );
4008 assert_eq!(
4009 controller
4010 .composition()
4011 .map(|composition| composition.range.clone()),
4012 composition
4013 );
4014 }
4015 assert!(
4016 controller
4017 .plan_ime_event(false, &winit::event::Ime::Preedit(String::new(), None))
4018 .is_none()
4019 );
4020 let Some(super::PlannedImeEvent::Edit(plan)) = controller.plan_ime_event(
4021 false,
4022 &winit::event::Ime::DeleteSurroundingText {
4023 before: 1,
4024 after: 1,
4025 },
4026 ) else {
4027 panic!("surrounding deletion must produce an edit");
4028 };
4029 let result = controller.replace_text_range_with_pipeline(
4030 plan.replacement_range.clone(),
4031 &plan.replacement_text,
4032 plan.selection.clone(),
4033 CallbackWith::identity(),
4034 None,
4035 );
4036 controller.commit_ime_edit(&plan, &result);
4037 assert_eq!(controller.text(), "😃中");
4038 assert_eq!(controller.selection(), TextSelection { start: 7, end: 0 });
4039 assert!(
4040 controller
4041 .plan_ime_event(
4042 false,
4043 &winit::event::Ime::DeleteSurroundingText {
4044 before: 1,
4045 after: 0
4046 }
4047 )
4048 .is_none()
4049 );
4050 }
4051 tessera_ui::testing::layout_test(snapshot_contract).run();
4052 }
4053
4054 fn controller_with_text(text: &str) -> TextEditorController {
4055 let mut controller = TextEditorController::new(Dp(14.0), None);
4056 controller.set_text(text);
4057 controller
4058 }
4059
4060 fn cursor_position_at_raw(controller: &TextEditorController, raw: usize) -> (i32, i32) {
4061 let buffer = controller.snapshot_buffer().expect("transformed layout");
4062 let offset = controller
4063 .layout_state
4064 .transformed_text()
4065 .expect("display mapping")
4066 .map_from_raw(raw);
4067 build_display_editor(
4068 buffer.clone(),
4069 text_offset_to_cursor_in_buffer(&buffer, offset),
4070 None,
4071 )
4072 .cursor_position()
4073 .expect("display cursor")
4074 }
4075
4076 fn prepare_transformed_layout(
4077 controller: &mut TextEditorController,
4078 transformed: TransformedText,
4079 ) -> TransformedText {
4080 prepare_transformed_layout_with_height(controller, transformed, 24.0)
4081 }
4082
4083 fn prepare_transformed_layout_with_height(
4084 controller: &mut TextEditorController,
4085 transformed: TransformedText,
4086 max_height: f32,
4087 ) -> TransformedText {
4088 let constraint = TextConstraint {
4089 max_width: Some(240.0),
4090 max_height: Some(max_height),
4091 ..Default::default()
4092 };
4093 let (metrics, scroll) = controller
4094 .editor()
4095 .with_buffer(|buffer| (buffer.metrics(), buffer.scroll()));
4096 let buffer = build_display_buffer(
4097 transformed.text(),
4098 controller.text_color(),
4099 metrics.font_size,
4100 metrics.line_height,
4101 &constraint,
4102 controller.wrap_mode(),
4103 scroll,
4104 Some(controller.text_style()),
4105 );
4106 controller.set_layout_snapshot_buffer(buffer, Some(transformed.clone()));
4107 transformed
4108 }
4109
4110 #[test]
4111 fn single_line_typography_resize_keeps_fitting_text_at_the_top_of_its_viewport() {
4112 let runtime = tessera_ui::RuntimeSession::new();
4113 let _scope = runtime.enter();
4114 let mut controller = TextEditorController::new(Dp(16.0), Some(Dp(24.0)));
4115 controller.set_single_line(true);
4116 controller.set_text("Carmen");
4117 controller.text_data(
4118 TextConstraint {
4119 max_width: Some(200.0),
4120 max_height: Some(24.0),
4121 ..Default::default()
4122 },
4123 controller.prepare_display(),
4124 );
4125 controller.set_text_style(crate::theme::TextStyle {
4126 font_size: Dp(28.0),
4127 line_height: Some(Dp(42.0)),
4128 ..Default::default()
4129 });
4130 controller.text_data(
4131 TextConstraint {
4132 max_width: Some(350.0),
4133 max_height: Some(42.0),
4134 ..Default::default()
4135 },
4136 controller.prepare_display(),
4137 );
4138 let scroll = controller.editor().with_buffer(|buffer| buffer.scroll());
4139 assert_eq!(scroll.line, 0);
4140 assert_eq!(
4141 scroll.vertical, 0.0,
4142 "a complete single line must remain visible after its font and viewport grow together"
4143 );
4144 assert_eq!(controller.scroll_state().vertical(), 0.0);
4145 }
4146
4147 #[test]
4148 fn text_data_reuses_cached_snapshot_for_same_layout_key() {
4149 let runtime = tessera_ui::RuntimeSession::new();
4150 let _scope = runtime.enter();
4151 let mut controller = controller_with_text("hello");
4152 let constraint = TextConstraint {
4153 max_width: Some(240.0),
4154 max_height: Some(80.0),
4155 ..Default::default()
4156 };
4157 controller.text_data(constraint.clone(), controller.prepare_display());
4158 let text_cache_key = controller.text_cache_key().expect("initial text layout");
4159 controller.update_layout_geometry(
4160 &constraint,
4161 vec![RectDef {
4162 x: Px(1),
4163 y: Px(2),
4164 width: Px(3),
4165 height: Px(4),
4166 }],
4167 Vec::new(),
4168 Some(RectDef {
4169 x: Px(5),
4170 y: Px(6),
4171 width: Px(1),
4172 height: Px(7),
4173 }),
4174 None,
4175 ComputedData {
4176 width: Px(11),
4177 height: Px(22),
4178 },
4179 );
4180
4181 controller.text_data(constraint.clone(), controller.prepare_display());
4182 assert_eq!(
4183 controller
4184 .cached_layout(&constraint)
4185 .map(|(_, _, _, size)| size),
4186 Some(ComputedData {
4187 width: Px(11),
4188 height: Px(22),
4189 })
4190 );
4191 assert!(matches!(
4192 controller.text_cache_key(),
4193 Some(key) if key == text_cache_key
4194 ));
4195
4196 controller.invalidate_layout_geometry();
4197 controller.text_data(constraint.clone(), controller.prepare_display());
4198 assert_eq!(controller.cached_layout(&constraint), None);
4199 assert!(matches!(
4200 controller.text_cache_key(),
4201 Some(key) if key == text_cache_key
4202 ));
4203 }
4204
4205 #[test]
4206 fn scroll_horizontal_by_reuses_text_layout_cache_and_syncs_editor_scroll() {
4207 let runtime = tessera_ui::RuntimeSession::new();
4208 let _scope = runtime.enter();
4209 let mut controller =
4210 controller_with_text("hello world hello world hello world hello world");
4211 controller.set_single_line(true);
4212 let constraint = TextConstraint {
4213 max_width: Some(60.0),
4214 max_height: Some(40.0),
4215 ..Default::default()
4216 };
4217
4218 controller.text_data(constraint.clone(), controller.prepare_display());
4219 let text_layout_version = controller.text_layout_version();
4220 let text_cache_key = TextLayoutCacheKey::new(text_layout_version, &constraint);
4221
4222 controller.scroll_horizontal_by(40.0);
4223
4224 assert!(controller.scroll().horizontal > 0.0);
4225 assert_eq!(controller.text_layout_version(), text_layout_version);
4226 assert!(matches!(
4227 controller.text_cache_key(),
4228 Some(key) if key == text_cache_key
4229 ));
4230 assert_eq!(
4231 controller.editor().with_buffer(|buffer| buffer.scroll()),
4232 controller.scroll()
4233 );
4234
4235 controller.text_data(constraint, controller.prepare_display());
4236 assert!(matches!(
4237 controller.text_cache_key(),
4238 Some(key) if key == text_cache_key
4239 ));
4240 }
4241
4242 #[test]
4243 fn horizontal_overflow_reports_and_clamps_consumed_scroll() {
4244 let runtime = tessera_ui::RuntimeSession::new();
4245 let _scope = runtime.enter();
4246 let mut controller =
4247 controller_with_text("hello world hello world hello world hello world");
4248 controller.set_single_line(true);
4249 controller.text_data(
4250 TextConstraint {
4251 max_width: Some(60.0),
4252 max_height: Some(40.0),
4253 ..Default::default()
4254 },
4255 controller.prepare_display(),
4256 );
4257
4258 assert!(controller.has_horizontal_overflow());
4259 let consumed = controller.scroll_horizontal_consumed(10_000.0);
4260 assert!(consumed > 0.0);
4261 assert_eq!(controller.scroll_horizontal_consumed(10_000.0), 0.0);
4264 }
4265
4266 #[test]
4267 fn vertical_overflow_is_detected_for_wrapped_content() {
4268 let runtime = tessera_ui::RuntimeSession::new();
4269 let _scope = runtime.enter();
4270 let mut controller = controller_with_text("111\n222\n333\n444\n555\n666\n777\n888");
4271 controller.text_data(
4272 TextConstraint {
4273 max_width: Some(120.0),
4274 max_height: Some(20.0),
4275 ..Default::default()
4276 },
4277 controller.prepare_display(),
4278 );
4279 assert!(controller.has_vertical_overflow());
4280 }
4281
4282 #[test]
4283 fn vertical_consumed_scroll_stops_at_the_content_boundary() {
4284 let runtime = tessera_ui::RuntimeSession::new();
4285 let _scope = runtime.enter();
4286 let mut controller = controller_with_text("111\n222\n333\n444\n555\n666\n777\n888");
4287 controller.text_data(
4288 TextConstraint {
4289 max_width: Some(120.0),
4290 max_height: Some(20.0),
4291 ..Default::default()
4292 },
4293 controller.prepare_display(),
4294 );
4295 assert!(controller.scroll_vertical_consumed(1_000.0) > 0.0);
4296 assert_eq!(controller.scroll_vertical_consumed(1_000.0), 0.0);
4299 }
4300
4301 #[test]
4302 fn ensure_cursor_visible_keeps_scroll_state_in_sync_with_editor_scroll() {
4303 let runtime = tessera_ui::RuntimeSession::new();
4304 let _scope = runtime.enter();
4305 let text = "hello world hello world hello world hello world";
4306 let mut controller = controller_with_text(text);
4307 controller.set_single_line(true);
4308 controller.text_data(
4309 TextConstraint {
4310 max_width: Some(60.0),
4311 max_height: Some(40.0),
4312 ..Default::default()
4313 },
4314 controller.prepare_display(),
4315 );
4316 controller.set_text_and_selection(text, TextSelection::collapsed(text.len()));
4317
4318 assert!(controller.scroll_state().horizontal() > 0.0);
4319 assert_eq!(
4320 controller.scroll_state().as_scroll(),
4321 controller.editor().with_buffer(|buffer| buffer.scroll())
4322 );
4323 }
4324
4325 #[test]
4326 fn scroll_vertical_by_reuses_text_layout_cache_and_syncs_scroll_state() {
4327 let runtime = tessera_ui::RuntimeSession::new();
4328 let _scope = runtime.enter();
4329 let mut controller = controller_with_text("111\n222\n333\n444\n555\n666\n777\n888");
4330 let constraint = TextConstraint {
4331 max_width: Some(120.0),
4332 max_height: Some(20.0),
4333 ..Default::default()
4334 };
4335
4336 controller.text_data(constraint.clone(), controller.prepare_display());
4337 let text_layout_version = controller.text_layout_version();
4338 let text_cache_key = TextLayoutCacheKey::new(text_layout_version, &constraint);
4339
4340 controller.scroll_vertical_by(20.0);
4341
4342 assert!(controller.scroll_state().vertical() > 0.0);
4343 assert_eq!(controller.text_layout_version(), text_layout_version);
4344 assert!(matches!(
4345 controller.text_cache_key(),
4346 Some(key) if key == text_cache_key
4347 ));
4348 assert_eq!(
4349 controller.scroll_state().as_scroll(),
4350 controller.editor().with_buffer(|buffer| buffer.scroll())
4351 );
4352 }
4353
4354 #[test]
4355 fn current_raw_layout_editor_preserves_backward_selection_direction() {
4356 let runtime = tessera_ui::RuntimeSession::new();
4357 let _scope = runtime.enter();
4358 let mut controller = controller_with_text("hello");
4359 controller.set_text_and_selection("hello", TextSelection { start: 4, end: 1 });
4360 controller.text_data(
4361 TextConstraint {
4362 max_width: Some(120.0),
4363 max_height: Some(40.0),
4364 ..Default::default()
4365 },
4366 controller.prepare_display(),
4367 );
4368
4369 let raw_editor = controller.current_raw_layout_editor();
4370 let anchor = match raw_editor.selection() {
4371 glyphon::cosmic_text::Selection::Normal(anchor)
4372 | glyphon::cosmic_text::Selection::Line(anchor)
4373 | glyphon::cosmic_text::Selection::Word(anchor) => anchor,
4374 glyphon::cosmic_text::Selection::None => {
4375 panic!("raw layout editor should preserve selection")
4376 }
4377 };
4378 let cursor = raw_editor.cursor();
4379
4380 raw_editor.with_buffer(|buffer| {
4381 assert_eq!(super::cursor_to_text_offset_in_buffer(buffer, anchor), 4);
4382 assert_eq!(super::cursor_to_text_offset_in_buffer(buffer, cursor), 1);
4383 });
4384 }
4385
4386 #[test]
4387 fn current_layout_geometry_renders_backward_selection_rects() {
4388 let runtime = tessera_ui::RuntimeSession::new();
4389 let _scope = runtime.enter();
4390 let mut controller = controller_with_text("hello");
4391 controller.set_text_and_selection("hello", TextSelection { start: 4, end: 1 });
4392
4393 let geometry = controller
4394 .current_layout_geometry()
4395 .expect("layout geometry should be available");
4396
4397 assert!(!geometry.selection_rects.is_empty());
4398 }
4399
4400 #[test]
4401 fn collapse_selection_left_moves_cursor_to_selection_start() {
4402 let runtime = tessera_ui::RuntimeSession::new();
4403 let _scope = runtime.enter();
4404 let mut controller = controller_with_text("hello");
4405 controller.set_text_and_selection("hello", TextSelection { start: 1, end: 4 });
4406
4407 assert!(controller.collapse_selection(false));
4408 assert_eq!(controller.cursor_offset(), 1);
4409 assert!(controller.editor().selection_bounds().is_none());
4410 }
4411
4412 #[test]
4413 fn collapse_selection_right_moves_cursor_to_selection_end() {
4414 let runtime = tessera_ui::RuntimeSession::new();
4415 let _scope = runtime.enter();
4416 let mut controller = controller_with_text("hello");
4417 controller.set_text_and_selection("hello", TextSelection { start: 1, end: 4 });
4418
4419 assert!(controller.collapse_selection(true));
4420 assert_eq!(controller.cursor_offset(), 4);
4421 assert!(controller.editor().selection_bounds().is_none());
4422 }
4423
4424 fn assert_named_key_sequence(
4425 text: &str,
4426 display: Option<&str>,
4427 initial: [usize; 2],
4428 key: NamedKey,
4429 ctrl: bool,
4430 shift: bool,
4431 expected: &[[usize; 2]],
4432 ) {
4433 let mut controller = controller_with_text(text);
4434 if let Some(display) = display {
4435 prepare_transformed_layout(
4436 &mut controller,
4437 TransformedText::from_strings(text, display.to_string()),
4438 );
4439 }
4440 controller.set_text_and_selection(
4441 text,
4442 TextSelection {
4443 start: initial[0],
4444 end: initial[1],
4445 },
4446 );
4447 for &[start, end] in expected {
4448 assert_eq!(controller.handle_named_key(key, ctrl, shift), None);
4449 assert_eq!(
4450 controller.selection(),
4451 TextSelection { start, end },
4452 "{text:?} {key:?} ctrl={ctrl} shift={shift}"
4453 );
4454 assert_eq!(controller.cursor_offset(), end);
4455 }
4456 }
4457
4458 #[test]
4459 fn named_keys_collapse_selections_and_traverse_or_extend_paragraphs() {
4460 let runtime = tessera_ui::RuntimeSession::new();
4461 let _scope = runtime.enter();
4462 for (text, initial, key, ctrl, shift, expected) in [
4463 (
4464 "111\n222\n333",
4465 [8, 4],
4466 NamedKey::ArrowUp,
4467 false,
4468 false,
4469 &[[4, 4]][..],
4470 ),
4471 (
4472 "111\n222\n333",
4473 [8, 4],
4474 NamedKey::ArrowDown,
4475 false,
4476 false,
4477 &[[8, 8]][..],
4478 ),
4479 (
4480 "111\n222\n333\n444",
4481 [10, 4],
4482 NamedKey::PageUp,
4483 false,
4484 false,
4485 &[[4, 4]][..],
4486 ),
4487 (
4488 "111\n222\n333\n444",
4489 [10, 4],
4490 NamedKey::PageDown,
4491 false,
4492 false,
4493 &[[10, 10]][..],
4494 ),
4495 (
4496 "111\n\n222\n\n333",
4497 [1, 1],
4498 NamedKey::ArrowDown,
4499 true,
4500 false,
4501 &[[5, 5], [10, 10]][..],
4502 ),
4503 (
4504 "111\n\n222\n\n333",
4505 [10, 10],
4506 NamedKey::ArrowUp,
4507 true,
4508 false,
4509 &[[5, 5], [0, 0]][..],
4510 ),
4511 (
4512 "111\n\n222\n\n333",
4513 [9, 5],
4514 NamedKey::ArrowUp,
4515 true,
4516 false,
4517 &[[5, 5]][..],
4518 ),
4519 (
4520 "111\n\n222\n\n333",
4521 [9, 5],
4522 NamedKey::ArrowDown,
4523 true,
4524 false,
4525 &[[9, 9]][..],
4526 ),
4527 (
4528 "111\n\n222\n\n333",
4529 [1, 1],
4530 NamedKey::ArrowDown,
4531 true,
4532 true,
4533 &[[1, 5]][..],
4534 ),
4535 (
4536 "111\n\n222\n\n333",
4537 [10, 10],
4538 NamedKey::ArrowUp,
4539 true,
4540 true,
4541 &[[10, 5]][..],
4542 ),
4543 ] {
4544 assert_named_key_sequence(text, None, initial, key, ctrl, shift, expected);
4545 }
4546 }
4547
4548 #[test]
4549 fn named_keys_map_transformed_paragraphs_and_document_boundaries_to_raw_offsets() {
4550 let runtime = tessera_ui::RuntimeSession::new();
4551 let _scope = runtime.enter();
4552 for (text, display, initial, key, ctrl, shift, expected) in [
4553 (
4554 "123456\n\nabcdef\n\nghijkl",
4555 "123 456\n\nabc def\n\nghi jkl",
4556 [4, 4],
4557 NamedKey::ArrowDown,
4558 true,
4559 false,
4560 &[[8, 8], [16, 16]][..],
4561 ),
4562 (
4563 "123456\n\nabcdef\n\nghijkl",
4564 "123 456\n\nabc def\n\nghi jkl",
4565 [16, 16],
4566 NamedKey::ArrowUp,
4567 true,
4568 false,
4569 &[[8, 8], [0, 0]][..],
4570 ),
4571 (
4572 "123456\n\nabcdef\n\nghijkl",
4573 "123 456\n\nabc def\n\nghi jkl",
4574 [16, 16],
4575 NamedKey::ArrowUp,
4576 true,
4577 true,
4578 &[[16, 8]][..],
4579 ),
4580 (
4581 "123456\nabcdef",
4582 "123 456\nabc def",
4583 [11, 11],
4584 NamedKey::Home,
4585 true,
4586 false,
4587 &[[0, 0]][..],
4588 ),
4589 (
4590 "123456\nabcdef",
4591 "123 456\nabc def",
4592 [1, 1],
4593 NamedKey::End,
4594 true,
4595 false,
4596 &[[13, 13]][..],
4597 ),
4598 (
4599 "123456\nabcdef",
4600 "123 456\nabc def",
4601 [11, 11],
4602 NamedKey::Home,
4603 true,
4604 true,
4605 &[[11, 0]][..],
4606 ),
4607 (
4608 "123456\nabcdef",
4609 "123 456\nabc def",
4610 [1, 1],
4611 NamedKey::End,
4612 true,
4613 true,
4614 &[[1, 13]][..],
4615 ),
4616 ] {
4617 assert_named_key_sequence(text, Some(display), initial, key, ctrl, shift, expected);
4618 }
4619 }
4620
4621 #[test]
4622 fn extend_selection_with_motion_creates_selection_from_cursor() {
4623 let runtime = tessera_ui::RuntimeSession::new();
4624 let _scope = runtime.enter();
4625 let mut controller = controller_with_text("hello");
4626 controller.set_text_and_selection("hello", TextSelection::collapsed(1));
4627
4628 assert!(controller.extend_selection_with_motion(Motion::Right));
4629 assert_eq!(controller.selection(), TextSelection { start: 1, end: 2 });
4630 }
4631
4632 #[test]
4633 fn extend_selection_with_motion_clears_when_returning_to_anchor() {
4634 let runtime = tessera_ui::RuntimeSession::new();
4635 let _scope = runtime.enter();
4636 let mut controller = controller_with_text("hello");
4637 controller.set_text_and_selection("hello", TextSelection::collapsed(1));
4638
4639 assert!(controller.extend_selection_with_motion(Motion::Right));
4640 assert!(controller.extend_selection_with_motion(Motion::Left));
4641 assert_eq!(controller.selection(), TextSelection::collapsed(1));
4642 assert!(controller.editor().selection_bounds().is_none());
4643 }
4644
4645 #[test]
4646 fn extend_selection_with_word_motion_reaches_next_word_boundary() {
4647 let runtime = tessera_ui::RuntimeSession::new();
4648 let _scope = runtime.enter();
4649 let mut controller = controller_with_text("hello world");
4650 controller.set_text_and_selection("hello world", TextSelection::collapsed(0));
4651
4652 assert!(controller.extend_selection_with_motion(Motion::RightWord));
4653 assert_eq!(controller.selection(), TextSelection { start: 0, end: 5 });
4654 }
4655
4656 #[test]
4657 fn extend_selection_to_point_selects_from_existing_cursor_anchor() {
4658 let runtime = tessera_ui::RuntimeSession::new();
4659 let _scope = runtime.enter();
4660 let mut controller = controller_with_text("hello");
4661 controller.set_text_and_selection("hello", TextSelection::collapsed(1));
4662
4663 assert!(
4664 controller.extend_selection_to_point(tessera_ui::PxPosition::new(
4665 tessera_ui::Px(40),
4666 tessera_ui::Px(0),
4667 ))
4668 );
4669 assert!(controller.selection().end > controller.selection().start);
4670 assert_eq!(controller.selection().start, 1);
4671 }
4672
4673 #[test]
4674 fn extend_selection_to_point_clears_when_pointing_back_to_anchor() {
4675 let runtime = tessera_ui::RuntimeSession::new();
4676 let _scope = runtime.enter();
4677 let mut controller = controller_with_text("hello");
4678 controller.set_text_and_selection("hello", TextSelection::collapsed(1));
4679 let anchor_pos = controller
4680 .editor()
4681 .cursor_position()
4682 .expect("cursor position should be available for shaped text");
4683
4684 assert!(
4685 controller.extend_selection_to_point(tessera_ui::PxPosition::new(
4686 tessera_ui::Px(40),
4687 tessera_ui::Px(0),
4688 ))
4689 );
4690 assert!(
4691 controller.extend_selection_to_point(tessera_ui::PxPosition::new(
4692 tessera_ui::Px(anchor_pos.0),
4693 tessera_ui::Px(anchor_pos.1),
4694 ))
4695 );
4696 assert!(controller.editor().selection_bounds().is_none());
4697 }
4698
4699 #[test]
4700 fn set_text_and_selection_keeps_single_line_caret_in_view() {
4701 let runtime = tessera_ui::RuntimeSession::new();
4702 let _scope = runtime.enter();
4703 let text = "hello world hello world";
4704 let mut controller = controller_with_text(text);
4705 controller.with_editor_mut(|editor| {
4706 editor.with_buffer_mut(|buffer| buffer.set_size(Some(40.0), Some(20.0)))
4707 });
4708 controller.set_single_line(true);
4709 controller.set_text_and_selection(text, TextSelection::collapsed(text.len()));
4710
4711 assert!(controller.scroll().horizontal > 0.0);
4712 }
4713
4714 #[test]
4715 fn set_text_and_selection_resets_cursor_blink_phase() {
4716 let runtime = tessera_ui::RuntimeSession::new();
4717 let _scope = runtime.enter();
4718 let mut controller = controller_with_text("hello");
4719 controller.update_frame_nanos(123);
4720 controller.blink_start_frame_nanos = 0;
4721
4722 controller.set_text_and_selection("hello!", TextSelection::collapsed(6));
4723
4724 assert_eq!(controller.blink_start_frame_nanos(), 123);
4725 assert_eq!(controller.current_frame_nanos(), 123);
4726 }
4727
4728 #[test]
4729 fn select_all_marks_full_text_selection() {
4730 let runtime = tessera_ui::RuntimeSession::new();
4731 let _scope = runtime.enter();
4732 let mut controller = controller_with_text("hello");
4733
4734 controller.select_all();
4735
4736 assert!(controller.has_selection());
4737 assert_eq!(controller.selection(), TextSelection { start: 0, end: 5 });
4738 }
4739
4740 #[test]
4741 fn apply_pointer_action_maps_raw_double_click_to_word_selection() {
4742 let runtime = tessera_ui::RuntimeSession::new();
4743 let _scope = runtime.enter();
4744 let mut controller = controller_with_text("foo bar");
4745 controller.set_text_and_selection("foo bar", TextSelection::collapsed(5));
4746 let (x, y) = controller
4747 .editor()
4748 .cursor_position()
4749 .expect("raw cursor position should be available");
4750
4751 controller.apply_pointer_action(GlyphonAction::DoubleClick { x, y });
4752
4753 assert_eq!(controller.selection(), TextSelection { start: 4, end: 7 });
4754 assert_eq!(controller.cursor_offset(), 7);
4755 }
4756
4757 #[test]
4758 fn apply_pointer_action_maps_raw_double_click_drag_to_word_selection() {
4759 let runtime = tessera_ui::RuntimeSession::new();
4760 let _scope = runtime.enter();
4761 let mut controller = controller_with_text("foo bar baz");
4762 controller.set_text_and_selection("foo bar baz", TextSelection::collapsed(5));
4763 let (start_x, start_y) = controller
4764 .editor()
4765 .cursor_position()
4766 .expect("raw cursor position should be available");
4767 controller.set_text_and_selection("foo bar baz", TextSelection::collapsed(9));
4768 let (end_x, end_y) = controller
4769 .editor()
4770 .cursor_position()
4771 .expect("raw cursor position should be available");
4772
4773 controller.apply_pointer_action(GlyphonAction::DoubleClick {
4774 x: start_x,
4775 y: start_y,
4776 });
4777 controller.start_drag(ClickType::Double);
4778 controller.apply_pointer_action(GlyphonAction::Drag { x: end_x, y: end_y });
4779
4780 assert_eq!(controller.selection(), TextSelection { start: 4, end: 11 });
4781 assert_eq!(controller.cursor_offset(), 11);
4782 }
4783
4784 #[test]
4785 fn apply_pointer_action_maps_raw_double_click_drag_to_backward_word_selection() {
4786 let runtime = tessera_ui::RuntimeSession::new();
4787 let _scope = runtime.enter();
4788 let mut controller = controller_with_text("foo bar baz");
4789 controller.set_text_and_selection("foo bar baz", TextSelection::collapsed(5));
4790 let (start_x, start_y) = controller
4791 .editor()
4792 .cursor_position()
4793 .expect("raw cursor position should be available");
4794 controller.set_text_and_selection("foo bar baz", TextSelection::collapsed(1));
4795 let (end_x, end_y) = controller
4796 .editor()
4797 .cursor_position()
4798 .expect("raw cursor position should be available");
4799
4800 controller.apply_pointer_action(GlyphonAction::DoubleClick {
4801 x: start_x,
4802 y: start_y,
4803 });
4804 controller.start_drag(ClickType::Double);
4805 controller.apply_pointer_action(GlyphonAction::Drag { x: end_x, y: end_y });
4806
4807 assert_eq!(controller.selection(), TextSelection { start: 7, end: 0 });
4808 assert_eq!(controller.cursor_offset(), 0);
4809 }
4810
4811 #[test]
4812 fn apply_pointer_action_maps_raw_triple_click_to_line_selection() {
4813 let runtime = tessera_ui::RuntimeSession::new();
4814 let _scope = runtime.enter();
4815 let mut controller = controller_with_text("111\n222\n333");
4816 controller.set_text_and_selection("111\n222\n333", TextSelection::collapsed(5));
4817 let (x, y) = controller
4818 .editor()
4819 .cursor_position()
4820 .expect("raw cursor position should be available");
4821
4822 controller.apply_pointer_action(GlyphonAction::TripleClick { x, y });
4823
4824 assert_eq!(controller.selection(), TextSelection { start: 4, end: 7 });
4825 assert_eq!(controller.cursor_offset(), 7);
4826 }
4827
4828 #[test]
4829 fn apply_pointer_action_maps_raw_triple_click_drag_to_line_selection() {
4830 let runtime = tessera_ui::RuntimeSession::new();
4831 let _scope = runtime.enter();
4832 let mut controller = controller_with_text("111\n222\n333");
4833 controller.set_text_and_selection("111\n222\n333", TextSelection::collapsed(5));
4834 let (start_x, start_y) = controller
4835 .editor()
4836 .cursor_position()
4837 .expect("raw cursor position should be available");
4838 controller.set_text_and_selection("111\n222\n333", TextSelection::collapsed(9));
4839 let (end_x, end_y) = controller
4840 .editor()
4841 .cursor_position()
4842 .expect("raw cursor position should be available");
4843
4844 controller.apply_pointer_action(GlyphonAction::TripleClick {
4845 x: start_x,
4846 y: start_y,
4847 });
4848 controller.start_drag(ClickType::Triple);
4849 controller.apply_pointer_action(GlyphonAction::Drag { x: end_x, y: end_y });
4850
4851 assert_eq!(controller.selection(), TextSelection { start: 4, end: 11 });
4852 assert_eq!(controller.cursor_offset(), 11);
4853 }
4854
4855 #[test]
4856 fn apply_pointer_action_maps_raw_triple_click_drag_to_backward_line_selection() {
4857 let runtime = tessera_ui::RuntimeSession::new();
4858 let _scope = runtime.enter();
4859 let mut controller = controller_with_text("111\n222\n333");
4860 controller.set_text_and_selection("111\n222\n333", TextSelection::collapsed(5));
4861 let (start_x, start_y) = controller
4862 .editor()
4863 .cursor_position()
4864 .expect("raw cursor position should be available");
4865 controller.set_text_and_selection("111\n222\n333", TextSelection::collapsed(1));
4866 let (end_x, end_y) = controller
4867 .editor()
4868 .cursor_position()
4869 .expect("raw cursor position should be available");
4870
4871 controller.apply_pointer_action(GlyphonAction::TripleClick {
4872 x: start_x,
4873 y: start_y,
4874 });
4875 controller.start_drag(ClickType::Triple);
4876 controller.apply_pointer_action(GlyphonAction::Drag { x: end_x, y: end_y });
4877
4878 assert_eq!(controller.selection(), TextSelection { start: 7, end: 0 });
4879 assert_eq!(controller.cursor_offset(), 0);
4880 }
4881
4882 #[test]
4883 fn transformed_text_maps_inserted_formatting_offsets() {
4884 let transformed = TransformedText::from_strings("1234567890", "123 456 7890".to_string());
4885
4886 assert_eq!(transformed.map_from_raw(3), 4);
4887 assert_eq!(transformed.map_from_raw(6), 8);
4888 assert_eq!(transformed.map_to_raw(4), 3);
4889 assert_eq!(transformed.map_to_raw(8), 6);
4890 }
4891
4892 #[test]
4893 fn transformed_text_maps_same_char_count_with_multibyte_mask() {
4894 let transformed = TransformedText::from_strings("abcd", "••••".to_string());
4895
4896 assert_eq!(transformed.map_from_raw(1), 3);
4897 assert_eq!(transformed.map_from_raw(4), 12);
4898 assert_eq!(transformed.map_to_raw(3), 1);
4899 assert_eq!(transformed.map_to_raw(12), 4);
4900 }
4901
4902 #[test]
4903 fn apply_pointer_action_maps_transformed_click_to_raw_cursor_offset() {
4904 let runtime = tessera_ui::RuntimeSession::new();
4905 let _scope = runtime.enter();
4906 let mut controller = controller_with_text("1234567890");
4907 let transformed = prepare_transformed_layout(
4908 &mut controller,
4909 TransformedText::from_strings("1234567890", "123 456 7890".to_string()),
4910 );
4911 let buffer = controller
4912 .snapshot_buffer()
4913 .expect("transformed layout buffer should exist");
4914 let display_offset = 4;
4915 let display_editor = build_display_editor(
4916 buffer.clone(),
4917 text_offset_to_cursor_in_buffer(&buffer, display_offset),
4918 None,
4919 );
4920 let (x, y) = display_editor
4921 .cursor_position()
4922 .expect("transformed cursor position should be available");
4923
4924 controller.set_layout_snapshot_buffer(buffer, Some(transformed));
4925 controller.apply_pointer_action(GlyphonAction::Click { x, y });
4926
4927 assert_eq!(controller.cursor_offset(), 3);
4928 }
4929
4930 #[test]
4931 fn apply_pointer_action_click_below_single_line_moves_cursor_to_nearest_offset() {
4932 let runtime = tessera_ui::RuntimeSession::new();
4933 let _scope = runtime.enter();
4934 let mut controller = controller_with_text("hello");
4935 let constraint = TextConstraint {
4936 max_width: Some(240.0),
4937 max_height: Some(80.0),
4938 ..Default::default()
4939 };
4940 controller.text_data(constraint, controller.prepare_display());
4941 let buffer = controller.editor().with_buffer(|buffer| buffer.clone());
4942 let editor = build_display_editor(
4943 buffer.clone(),
4944 text_offset_to_cursor_in_buffer(&buffer, "hello".len()),
4945 None,
4946 );
4947 let (x, y) = editor
4948 .cursor_position()
4949 .expect("cursor position should be available");
4950
4951 controller.apply_pointer_action(GlyphonAction::Click { x, y: y + 24 });
4952
4953 assert_eq!(controller.cursor_offset(), "hello".len());
4954 }
4955
4956 #[test]
4957 fn apply_pointer_action_maps_transformed_double_click_to_raw_word_selection() {
4958 let runtime = tessera_ui::RuntimeSession::new();
4959 let _scope = runtime.enter();
4960 let mut controller = controller_with_text("1234567890");
4961 prepare_transformed_layout(
4962 &mut controller,
4963 TransformedText::from_strings("1234567890", "123 456 7890".to_string()),
4964 );
4965 let (x, y) = cursor_position_at_raw(&controller, 4);
4966
4967 controller.apply_pointer_action(GlyphonAction::DoubleClick { x, y });
4968
4969 assert_eq!(controller.selection(), TextSelection { start: 3, end: 6 });
4970 assert_eq!(controller.cursor_offset(), 6);
4971 }
4972
4973 #[test]
4974 fn apply_pointer_action_maps_transformed_double_click_drag_to_raw_word_selection() {
4975 let runtime = tessera_ui::RuntimeSession::new();
4976 let _scope = runtime.enter();
4977 let mut controller = controller_with_text("1234567890");
4978 prepare_transformed_layout(
4979 &mut controller,
4980 TransformedText::from_strings("1234567890", "123 456 7890".to_string()),
4981 );
4982 let (start_x, start_y) = cursor_position_at_raw(&controller, 4);
4983 let (end_x, end_y) = cursor_position_at_raw(&controller, 8);
4984
4985 controller.apply_pointer_action(GlyphonAction::DoubleClick {
4986 x: start_x,
4987 y: start_y,
4988 });
4989 controller.start_drag(ClickType::Double);
4990 controller.apply_pointer_action(GlyphonAction::Drag { x: end_x, y: end_y });
4991
4992 assert_eq!(controller.selection(), TextSelection { start: 3, end: 10 });
4993 assert_eq!(controller.cursor_offset(), 10);
4994 }
4995
4996 #[test]
4997 fn apply_pointer_action_maps_transformed_double_click_drag_to_backward_word_selection() {
4998 let runtime = tessera_ui::RuntimeSession::new();
4999 let _scope = runtime.enter();
5000 let mut controller = controller_with_text("1234567890");
5001 prepare_transformed_layout(
5002 &mut controller,
5003 TransformedText::from_strings("1234567890", "123 456 7890".to_string()),
5004 );
5005 let (start_x, start_y) = cursor_position_at_raw(&controller, 4);
5006 let (end_x, end_y) = cursor_position_at_raw(&controller, 1);
5007
5008 controller.apply_pointer_action(GlyphonAction::DoubleClick {
5009 x: start_x,
5010 y: start_y,
5011 });
5012 controller.start_drag(ClickType::Double);
5013 controller.apply_pointer_action(GlyphonAction::Drag { x: end_x, y: end_y });
5014
5015 assert_eq!(controller.selection(), TextSelection { start: 6, end: 0 });
5016 assert_eq!(controller.cursor_offset(), 0);
5017 }
5018
5019 #[test]
5020 fn apply_pointer_action_preserves_backward_transformed_drag_selection_direction() {
5021 let runtime = tessera_ui::RuntimeSession::new();
5022 let _scope = runtime.enter();
5023 let mut controller = controller_with_text("1234567890");
5024 prepare_transformed_layout(
5025 &mut controller,
5026 TransformedText::from_strings("1234567890", "123 456 7890".to_string()),
5027 );
5028 let (start_x, start_y) = cursor_position_at_raw(&controller, 6);
5029 let (end_x, end_y) = cursor_position_at_raw(&controller, 3);
5030
5031 controller.apply_pointer_action(GlyphonAction::Click {
5032 x: start_x,
5033 y: start_y,
5034 });
5035 controller.apply_pointer_action(GlyphonAction::Drag { x: end_x, y: end_y });
5036
5037 assert_eq!(controller.selection(), TextSelection { start: 6, end: 3 });
5038 assert_eq!(controller.cursor_offset(), 3);
5039 }
5040
5041 #[test]
5042 fn apply_pointer_action_maps_transformed_triple_click_to_raw_line_selection() {
5043 let runtime = tessera_ui::RuntimeSession::new();
5044 let _scope = runtime.enter();
5045 let mut controller = controller_with_text("123456\nabcdef");
5046 let transformed =
5047 TransformedText::from_strings("123456\nabcdef", "123 456\nabc def".to_string());
5048 let constraint = TextConstraint {
5049 max_width: Some(240.0),
5050 max_height: Some(80.0),
5051 ..Default::default()
5052 };
5053 let (metrics, scroll) = controller
5054 .editor()
5055 .with_buffer(|buffer| (buffer.metrics(), buffer.scroll()));
5056 let buffer = build_display_buffer(
5057 transformed.text(),
5058 controller.text_color(),
5059 metrics.font_size,
5060 metrics.line_height,
5061 &constraint,
5062 controller.wrap_mode(),
5063 scroll,
5064 Some(controller.text_style()),
5065 );
5066 controller.set_layout_snapshot_buffer(buffer.clone(), Some(transformed.clone()));
5067
5068 let display_editor = build_display_editor(
5069 buffer,
5070 text_offset_to_cursor_in_buffer(
5071 &controller
5072 .snapshot_buffer()
5073 .expect("transformed layout buffer should exist"),
5074 transformed.map_from_raw(9),
5075 ),
5076 None,
5077 );
5078 let (x, y) = display_editor
5079 .cursor_position()
5080 .expect("transformed cursor position should be available");
5081
5082 controller.apply_pointer_action(GlyphonAction::TripleClick { x, y });
5083
5084 assert_eq!(controller.selection(), TextSelection { start: 7, end: 13 });
5085 assert_eq!(controller.cursor_offset(), 13);
5086 }
5087
5088 #[test]
5089 fn apply_pointer_action_maps_transformed_triple_click_drag_to_raw_line_selection() {
5090 let runtime = tessera_ui::RuntimeSession::new();
5091 let _scope = runtime.enter();
5092 let mut controller = controller_with_text("111\n222\n333");
5093 prepare_transformed_layout_with_height(
5094 &mut controller,
5095 TransformedText::from_strings("111\n222\n333", "1 11\n2 22\n3 33".to_string()),
5096 80.0,
5097 );
5098 let (start_x, start_y) = cursor_position_at_raw(&controller, 5);
5099 let (end_x, end_y) = cursor_position_at_raw(&controller, 9);
5100
5101 controller.apply_pointer_action(GlyphonAction::TripleClick {
5102 x: start_x,
5103 y: start_y,
5104 });
5105 controller.start_drag(ClickType::Triple);
5106 controller.apply_pointer_action(GlyphonAction::Drag { x: end_x, y: end_y });
5107
5108 assert_eq!(controller.selection(), TextSelection { start: 4, end: 11 });
5109 assert_eq!(controller.cursor_offset(), 11);
5110 }
5111
5112 #[test]
5113 fn apply_pointer_action_maps_transformed_triple_click_drag_to_backward_line_selection() {
5114 let runtime = tessera_ui::RuntimeSession::new();
5115 let _scope = runtime.enter();
5116 let mut controller = controller_with_text("111\n222\n333");
5117 prepare_transformed_layout_with_height(
5118 &mut controller,
5119 TransformedText::from_strings("111\n222\n333", "1 11\n2 22\n3 33".to_string()),
5120 80.0,
5121 );
5122 let (start_x, start_y) = cursor_position_at_raw(&controller, 5);
5123 let (end_x, end_y) = cursor_position_at_raw(&controller, 1);
5124
5125 controller.apply_pointer_action(GlyphonAction::TripleClick {
5126 x: start_x,
5127 y: start_y,
5128 });
5129 controller.start_drag(ClickType::Triple);
5130 controller.apply_pointer_action(GlyphonAction::Drag { x: end_x, y: end_y });
5131
5132 assert_eq!(controller.selection(), TextSelection { start: 7, end: 0 });
5133 assert_eq!(controller.cursor_offset(), 0);
5134 }
5135
5136 #[test]
5137 fn move_cursor_with_motion_uses_transformed_vertical_offsets() {
5138 let runtime = tessera_ui::RuntimeSession::new();
5139 let _scope = runtime.enter();
5140 let mut controller = controller_with_text("123456\nabcdef");
5141 let transformed =
5142 TransformedText::from_strings("123456\nabcdef", "123 456\nabc def".to_string());
5143 let constraint = TextConstraint {
5144 max_width: Some(240.0),
5145 max_height: Some(80.0),
5146 ..Default::default()
5147 };
5148 let (metrics, scroll) = controller
5149 .editor()
5150 .with_buffer(|buffer| (buffer.metrics(), buffer.scroll()));
5151 let buffer = build_display_buffer(
5152 transformed.text(),
5153 controller.text_color(),
5154 metrics.font_size,
5155 metrics.line_height,
5156 &constraint,
5157 controller.wrap_mode(),
5158 scroll,
5159 Some(controller.text_style()),
5160 );
5161 controller.set_layout_snapshot_buffer(buffer, Some(transformed));
5162 controller.set_text_and_selection("123456\nabcdef", TextSelection::collapsed(4));
5163
5164 assert!(controller.move_cursor_with_motion(Motion::Down));
5165 assert_eq!(controller.cursor_offset(), 11);
5166
5167 assert!(controller.move_cursor_with_motion(Motion::Up));
5168 assert_eq!(controller.cursor_offset(), 4);
5169 }
5170
5171 #[test]
5172 fn move_cursor_with_motion_uses_transformed_line_boundaries() {
5173 let runtime = tessera_ui::RuntimeSession::new();
5174 let _scope = runtime.enter();
5175 let mut controller = controller_with_text("123456\nabcdef");
5176 prepare_transformed_layout(
5177 &mut controller,
5178 TransformedText::from_strings("123456\nabcdef", "123 456\nabc def".to_string()),
5179 );
5180 controller.set_text_and_selection("123456\nabcdef", TextSelection::collapsed(4));
5181
5182 assert!(controller.move_cursor_with_motion(Motion::Home));
5183 assert_eq!(controller.cursor_offset(), 0);
5184
5185 assert!(controller.move_cursor_with_motion(Motion::End));
5186 assert_eq!(controller.cursor_offset(), 6);
5187 }
5188
5189 #[test]
5190 fn extend_selection_with_motion_uses_transformed_line_boundaries() {
5191 let runtime = tessera_ui::RuntimeSession::new();
5192 let _scope = runtime.enter();
5193 let mut controller = controller_with_text("123456\nabcdef");
5194 prepare_transformed_layout(
5195 &mut controller,
5196 TransformedText::from_strings("123456\nabcdef", "123 456\nabc def".to_string()),
5197 );
5198 controller.set_text_and_selection("123456\nabcdef", TextSelection::collapsed(4));
5199
5200 assert!(controller.extend_selection_with_motion(Motion::Home));
5201 assert_eq!(controller.selection(), TextSelection { start: 4, end: 0 });
5202
5203 controller.set_text_and_selection("123456\nabcdef", TextSelection::collapsed(4));
5204 assert!(controller.extend_selection_with_motion(Motion::End));
5205 assert_eq!(controller.selection(), TextSelection { start: 4, end: 6 });
5206 }
5207
5208 #[test]
5209 fn move_cursor_with_motion_uses_transformed_page_vertical_offsets() {
5210 let runtime = tessera_ui::RuntimeSession::new();
5211 let _scope = runtime.enter();
5212 let mut controller = controller_with_text("111\n222\n333\n444");
5213 prepare_transformed_layout_with_height(
5214 &mut controller,
5215 TransformedText::from_strings(
5216 "111\n222\n333\n444",
5217 "1 11\n2 22\n3 33\n4 44".to_string(),
5218 ),
5219 40.0,
5220 );
5221 controller.set_text_and_selection("111\n222\n333\n444", TextSelection::collapsed(1));
5222
5223 assert!(controller.move_cursor_with_motion(Motion::PageDown));
5224 assert_eq!(controller.cursor_offset(), 9);
5225
5226 assert!(controller.move_cursor_with_motion(Motion::PageUp));
5227 assert_eq!(controller.cursor_offset(), 1);
5228 }
5229
5230 #[test]
5231 fn extend_selection_with_motion_uses_transformed_page_vertical_offsets() {
5232 let runtime = tessera_ui::RuntimeSession::new();
5233 let _scope = runtime.enter();
5234 let mut controller = controller_with_text("111\n222\n333\n444");
5235 prepare_transformed_layout_with_height(
5236 &mut controller,
5237 TransformedText::from_strings(
5238 "111\n222\n333\n444",
5239 "1 11\n2 22\n3 33\n4 44".to_string(),
5240 ),
5241 40.0,
5242 );
5243 controller.set_text_and_selection("111\n222\n333\n444", TextSelection::collapsed(1));
5244
5245 assert!(controller.extend_selection_with_motion(Motion::PageDown));
5246 assert_eq!(controller.selection(), TextSelection { start: 1, end: 9 });
5247 }
5248
5249 #[test]
5250 fn move_cursor_with_motion_uses_transformed_word_boundaries() {
5251 let runtime = tessera_ui::RuntimeSession::new();
5252 let _scope = runtime.enter();
5253 let mut controller = controller_with_text("1234567890");
5254 prepare_transformed_layout(
5255 &mut controller,
5256 TransformedText::from_strings("1234567890", "123 456 7890".to_string()),
5257 );
5258 controller.set_text_and_selection("1234567890", TextSelection::collapsed(0));
5259
5260 assert!(controller.move_cursor_with_motion(Motion::RightWord));
5261 assert_eq!(controller.cursor_offset(), 3);
5262
5263 assert!(controller.move_cursor_with_motion(Motion::RightWord));
5264 assert_eq!(controller.cursor_offset(), 6);
5265 }
5266
5267 #[test]
5268 fn extend_selection_with_motion_uses_transformed_word_boundaries() {
5269 let runtime = tessera_ui::RuntimeSession::new();
5270 let _scope = runtime.enter();
5271 let mut controller = controller_with_text("1234567890");
5272 prepare_transformed_layout(
5273 &mut controller,
5274 TransformedText::from_strings("1234567890", "123 456 7890".to_string()),
5275 );
5276 controller.set_text_and_selection("1234567890", TextSelection::collapsed(0));
5277
5278 assert!(controller.extend_selection_with_motion(Motion::RightWord));
5279 assert_eq!(controller.selection(), TextSelection { start: 0, end: 3 });
5280 }
5281
5282 #[test]
5283 fn deletion_range_for_motion_uses_transformed_word_boundaries() {
5284 let runtime = tessera_ui::RuntimeSession::new();
5285 let _scope = runtime.enter();
5286 let mut controller = controller_with_text("1234567890");
5287 prepare_transformed_layout(
5288 &mut controller,
5289 TransformedText::from_strings("1234567890", "123 456 7890".to_string()),
5290 );
5291 controller.set_text_and_selection("1234567890", TextSelection::collapsed(6));
5292
5293 assert_eq!(
5294 controller.deletion_range_for_motion(Motion::LeftWord),
5295 Some(3..6)
5296 );
5297 assert_eq!(
5298 controller.deletion_range_for_motion(Motion::RightWord),
5299 Some(6..10)
5300 );
5301 }
5302
5303 #[test]
5304 fn deletion_range_for_motion_prefers_existing_selection() {
5305 let runtime = tessera_ui::RuntimeSession::new();
5306 let _scope = runtime.enter();
5307 let mut controller = controller_with_text("hello world");
5308 controller.set_text_and_selection("hello world", TextSelection { start: 8, end: 2 });
5309
5310 assert_eq!(
5311 controller.deletion_range_for_motion(Motion::LeftWord),
5312 Some(2..8)
5313 );
5314 }
5315
5316 #[test]
5317 fn transformed_composition_rects_follow_mapped_display_offsets() {
5318 let runtime = tessera_ui::RuntimeSession::new();
5319 let _scope = runtime.enter();
5320 let transformed = TransformedText::from_strings("1234567890", "123 456 7890".to_string());
5321 let constraint = TextConstraint {
5322 max_width: Some(240.0),
5323 max_height: Some(24.0),
5324 ..Default::default()
5325 };
5326 let buffer = build_display_buffer(
5327 transformed.text(),
5328 tessera_ui::Color::BLACK,
5329 14.0,
5330 18.0,
5331 &constraint,
5332 glyphon::Wrap::None,
5333 glyphon::cosmic_text::Scroll::default(),
5334 None,
5335 );
5336 let expected_start = text_offset_to_cursor_in_buffer(&buffer, transformed.map_from_raw(3));
5337 let expected_x = build_display_editor(buffer.clone(), expected_start, None)
5338 .cursor_position()
5339 .expect("cursor position should be available")
5340 .0;
5341
5342 let rects = compute_transformed_composition_rects(&buffer, &transformed, 3..6);
5343
5344 assert!(!rects.is_empty());
5345 assert_eq!(rects[0].x, tessera_ui::Px(expected_x));
5346 }
5347
5348 #[test]
5349 fn active_ime_rect_prefers_composition_over_selection_and_cursor() {
5350 let selection_rects = vec![super::RectDef {
5351 x: tessera_ui::Px(4),
5352 y: tessera_ui::Px(8),
5353 width: tessera_ui::Px(20),
5354 height: tessera_ui::Px(18),
5355 }];
5356 let composition_rects = vec![super::RectDef {
5357 x: tessera_ui::Px(12),
5358 y: tessera_ui::Px(10),
5359 width: tessera_ui::Px(30),
5360 height: tessera_ui::Px(2),
5361 }];
5362 let cursor_rect = Some(super::RectDef {
5363 x: tessera_ui::Px(2),
5364 y: tessera_ui::Px(8),
5365 width: tessera_ui::Px(1),
5366 height: tessera_ui::Px(18),
5367 });
5368
5369 assert_eq!(
5370 active_ime_rect(&selection_rects, &composition_rects, cursor_rect),
5371 composition_rects.into_iter().next()
5372 );
5373 }
5374
5375 #[test]
5376 fn active_ime_rect_unions_selection_rects_before_cursor_fallback() {
5377 let selection_rects = vec![
5378 super::RectDef {
5379 x: tessera_ui::Px(4),
5380 y: tessera_ui::Px(8),
5381 width: tessera_ui::Px(20),
5382 height: tessera_ui::Px(18),
5383 },
5384 super::RectDef {
5385 x: tessera_ui::Px(30),
5386 y: tessera_ui::Px(8),
5387 width: tessera_ui::Px(10),
5388 height: tessera_ui::Px(18),
5389 },
5390 ];
5391
5392 assert_eq!(
5393 active_ime_rect(&selection_rects, &[], None),
5394 Some(super::RectDef {
5395 x: tessera_ui::Px(4),
5396 y: tessera_ui::Px(8),
5397 width: tessera_ui::Px(36),
5398 height: tessera_ui::Px(18),
5399 })
5400 );
5401 }
5402 #[test]
5403 fn typography_changes_preserve_selection_and_apply_to_subsequent_edits() {
5404 let runtime = tessera_ui::RuntimeSession::new();
5405 let _scope = runtime.enter();
5406 let mut controller = TextEditorController::new(Dp(16.0), Some(Dp(24.0)));
5407 let selection = TextSelection { start: 2, end: 8 };
5408 controller.set_text_and_selection("Material typography", selection.clone());
5409 let constraint = TextConstraint {
5410 max_width: Some(1000.0),
5411 max_height: None,
5412 ..Default::default()
5413 };
5414 let before = controller.text_data(constraint.clone(), controller.prepare_display());
5415 let style = crate::theme::TextStyle {
5416 font_weight: 700,
5417 letter_spacing: Dp(1.0),
5418 ..crate::theme::MaterialTypography::default().body_large
5419 };
5420 controller.set_text_style(style);
5421 assert_eq!(controller.selection(), selection);
5422 assert_eq!(controller.text(), "Material typography");
5423 assert!(
5424 controller
5425 .text_data(constraint.clone(), controller.prepare_display())
5426 .measure_info()
5427 .size[0]
5428 > before.measure_info().size[0]
5429 );
5430 controller.set_text("Roboto");
5431 let _ = controller.text_data(constraint, controller.prepare_display());
5432 let buffer = controller.editor().with_buffer(Clone::clone);
5433 let glyph = &buffer.layout_runs().next().unwrap().glyphs[0];
5434 assert_eq!(glyph.font_weight, glyphon::Weight::BOLD);
5435 }
5436}