Skip to main content

tessera_components/
text_edit_core.rs

1//! Editable text buffers, selection, and caret rendering.
2//!
3//! ## Usage
4//!
5//! Build custom editors for short-form input, documents, and text selection.
6
7use 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
35/// Width of the caret drawn by the text editor in device-independent pixels.
36const 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/// Display-only text transform output with offset mapping between raw and
64/// transformed content.
65#[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    /// Creates a transformed text model using heuristics to derive offset
76    /// mapping from the raw and transformed strings.
77    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    /// Creates a transformed text model without changing visible content.
119    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
154/// Display-only text transform applied to the text content before rendering
155/// (e.g., masking or formatting without changing the underlying buffer).
156pub 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/// Definition of a rectangular selection highlight
366#[derive(Clone, Debug, PartialEq)]
367/// Defines a rectangular region for text selection highlighting.
368///
369/// Used internally to represent the geometry of a selection highlight in pixel
370/// coordinates.
371pub struct RectDef {
372    /// The x-coordinate (in pixels) of the rectangle's top-left corner.
373    pub x: Px,
374    /// The y-coordinate (in pixels) of the rectangle's top-left corner.
375    pub y: Px,
376    /// The width (in pixels) of the rectangle.
377    pub width: Px,
378    /// The height (in pixels) of the rectangle.
379    pub height: Px,
380}
381
382/// Types of mouse clicks
383#[derive(Clone, Copy, PartialEq, Eq)]
384/// Represents the type of mouse click detected in the editor.
385///
386/// Used for distinguishing between single, double, and triple click actions.
387pub enum ClickType {
388    /// A single mouse click.
389    Single,
390    /// A double mouse click.
391    Double,
392    /// A triple mouse click.
393    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
1808/// Core text editing state, shared between components
1809/// Core state for text editing, including content, selection, cursor, and
1810/// interaction state.
1811///
1812/// This struct manages the text buffer, selection, cursor position, focus, and
1813/// user interaction state. It is designed to be shared between UI components
1814/// via a `TextEditorController`.
1815pub 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    /// Creates a new `TextEditorController` with the given font size and
1990    /// optional line height.
1991    ///
1992    /// # Arguments
1993    ///
1994    /// * `size` - Font size in Dp.
1995    /// * `line_height` - Optional line height in Dp. If `None`, uses 1.2x the
1996    ///   font size.
1997    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    /// Creates a new `TextEditorController` with a custom selection highlight
2002    /// color.
2003    ///
2004    /// # Arguments
2005    ///
2006    /// * `size` - Font size in Dp.
2007    /// * `line_height` - Optional line height in Dp.
2008    /// * `selection_color` - Color used for selection highlight.
2009    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    /// Returns the line height in pixels.
2059    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    /// Requests keyboard focus for this editor while it is attached and
2137    /// available.
2138    ///
2139    /// Hidden or disabled editors reject the request. A scrollable ancestor may
2140    /// reveal an attached editor that is outside its viewport.
2141    pub fn request_focus(&self) {
2142        self.focus_handler.request_focus();
2143    }
2144
2145    // Returns a reference to the internal focus handler.
2146    pub(crate) fn focus_handler(&self) -> &FocusRequester {
2147        &self.focus_handler
2148    }
2149
2150    /// Returns a reference to the underlying `glyphon::Editor`.
2151    pub fn editor(&self) -> &glyphon::Editor<'static> {
2152        self.edit_state.editor()
2153    }
2154
2155    /// Mutates the underlying `glyphon::Editor` and refreshes layout state.
2156    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    // Returns the cursor blink start timestamp.
2168    fn blink_start_frame_nanos(&self) -> u64 {
2169        self.blink_start_frame_nanos
2170    }
2171
2172    // Returns the latest frame timestamp observed by this controller.
2173    #[cfg(test)]
2174    fn current_frame_nanos(&self) -> u64 {
2175        self.current_frame_nanos
2176    }
2177
2178    // Updates the latest frame timestamp used by cursor blink rendering.
2179    #[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    /// Returns the current selection highlight color.
2191    pub fn selection_color(&self) -> Color {
2192        self.selection_state.selection_color()
2193    }
2194
2195    /// Sets the selection highlight color.
2196    ///
2197    /// # Arguments
2198    ///
2199    /// * `color` - The new selection color.
2200    pub fn set_selection_color(&mut self, color: Color) {
2201        self.selection_state.set_selection_color(color);
2202    }
2203
2204    /// Returns the current text color.
2205    pub fn text_color(&self) -> Color {
2206        self.edit_state.text_color()
2207    }
2208
2209    /// Sets the text color used by the editor.
2210    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(&current_text, selection);
2217    }
2218
2219    /// Returns the typography used by editable text.
2220    pub fn text_style(&self) -> TextStyle {
2221        self.edit_state.style
2222    }
2223
2224    /// Sets typography while preserving text and selection.
2225    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    /// Returns the cursor color.
2246    pub fn cursor_color(&self) -> Color {
2247        self.edit_state.cursor_color()
2248    }
2249
2250    /// Sets the cursor color used by the editor.
2251    pub fn set_cursor_color(&mut self, color: Color) {
2252        self.edit_state.set_cursor_color(color);
2253    }
2254
2255    /// Sets a display transform applied when rendering text.
2256    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    /// Scrolls horizontally by `delta` and returns the consumed amount.
2312    ///
2313    /// The returned value is the actual change applied in scroll space, so
2314    /// callers can hand the remainder to an ancestor scroll container.
2315    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    /// Scrolls vertically by `delta` and returns the consumed amount.
2322    ///
2323    /// The returned value is the actual change applied in scroll space; the
2324    /// editor engine clamps vertical scrolling to the content extent.
2325    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    /// Returns whether the editor content overflows horizontally.
2332    pub(crate) fn has_horizontal_overflow(&mut self) -> bool {
2333        self.edit_state.has_horizontal_overflow()
2334    }
2335
2336    /// Returns whether the editor content overflows vertically.
2337    pub(crate) fn has_vertical_overflow(&mut self) -> bool {
2338        self.edit_state.has_vertical_overflow()
2339    }
2340
2341    /// Returns whether a display transform is active.
2342    pub fn display_transform_active(&self) -> bool {
2343        self.edit_state.display_transform().is_some()
2344    }
2345
2346    // Returns the current display transform handle for equality checks before
2347    // mutating state.
2348    pub(crate) fn display_transform(&self) -> Option<DisplayTransform> {
2349        self.edit_state.display_transform()
2350    }
2351
2352    // Handles a mouse click event and determines the click type (single,
2353    // double, triple).
2354    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    // Starts a drag operation (for text selection).
2365    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    /// Returns `true` if a drag operation is in progress.
2374    pub fn is_dragging(&self) -> bool {
2375        self.selection_state.is_dragging()
2376    }
2377
2378    // Stops the current drag operation.
2379    pub(crate) fn stop_drag(&mut self) {
2380        self.selection_state.stop_drag();
2381    }
2382
2383    // Returns the last click position, if any.
2384    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    // Updates the last click position (used for drag tracking).
2393    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            // Typography can change before layout publishes the larger
2401            // viewport. A single-line editor must not retain
2402            // vertical caret reveal from that temporary constraint.
2403            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    /// Map keyboard events to text editing actions
2589    /// Maps a keyboard event to a list of text editing actions for the editor.
2590    ///
2591    /// This function translates keyboard input (including modifiers) into
2592    /// editing actions such as character insertion, deletion, and navigation.
2593    ///
2594    /// Clipboard shortcuts are handled at a higher layer so they stay on the
2595    /// unified edit pipeline instead of bypassing controller state.
2596    ///
2597    /// # Arguments
2598    ///
2599    /// * `key_event` - The keyboard event to map.
2600    /// * `key_modifiers` - The current keyboard modifier state.
2601    ///
2602    /// # Returns
2603    ///
2604    /// An optional vector of `glyphon::Action` to be applied to the editor.
2605    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 we are on the first line, we move the cursor to the
2694                // beginning of the line
2695                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 we are on the last line, we move the cursor to the end of
2729                // the line
2730                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    /// Sets the entire text content of the editor, preserving cursor position
2796    /// as much as possible.
2797    ///
2798    /// # Arguments
2799    ///
2800    /// - `text` - The new text content to set in the editor.
2801    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    /// Returns the current text content.
2807    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/// # text_edit_core
3628///
3629/// Render editable text, selection, and a caret for custom input controls.
3630///
3631/// ## Usage
3632///
3633/// Build custom text editors with shared content and selection state.
3634///
3635/// ## Parameters
3636///
3637/// - `controller` — shared text, selection, and keyboard focus state.
3638/// - `presentation_focused` — show a static caret without requesting focus;
3639///   actual keyboard focus continues to use its normal blinking caret.
3640
3641#[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        // At the right edge nothing more is consumed, so an ancestor can take
4262        // over.
4263        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        // The editor engine clamps vertical scroll, so at the bottom nothing
4297        // more is consumed and the remainder can go to an ancestor.
4298        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}