Skip to main content

azul_core/
selection.rs

1//! Text selection and cursor positioning for inline content.
2//!
3//! This module provides data structures for managing text cursors and selection ranges
4//! in a bidirectional (Bidi) and line-breaking aware manner. It handles:
5//!
6//! - **Grapheme cluster identification**: Unicode-aware character boundaries
7//! - **Bidi support**: Cursor movement in mixed LTR/RTL text
8//! - **Stable positions**: Selection anchors survive layout changes
9//! - **Affinity tracking**: Cursor position at leading/trailing edges
10//! - **Multi-node selection**: Browser-style selection spanning multiple DOM nodes
11//!
12//! # Architecture
13//!
14//! Text positions are represented as:
15//! - `ContentIndex`: Logical position in the original inline content array
16//! - `GraphemeClusterId`: Stable identifier for a grapheme cluster (survives reordering)
17//! - `TextCursor`: Precise cursor location with leading/trailing affinity
18//! - `SelectionRange`: Start and end cursors defining a selection
19//!
20//! Multi-node selection uses an Anchor/Focus model (W3C Selection API):
21//! - `SelectionAnchor`: Fixed point where user started selection (mousedown)
22//! - `SelectionFocus`: Movable point where selection currently ends (drag position)
23//! - `TextSelection`: Complete selection state spanning potentially multiple IFC roots
24//!
25//! # Use Cases
26//!
27//! - Text editing: Insert/delete at cursor position
28//! - Selection rendering: Highlight selected text across multiple nodes
29//! - Keyboard navigation: Move cursor by grapheme/word/line
30//! - Mouse selection: Convert pixel coordinates to text positions
31//! - Drag selection: Extend selection across multiple DOM nodes
32//!
33//! # Examples
34//!
35//! ```rust,no_run
36//! use azul_core::selection::{CursorAffinity, GraphemeClusterId, TextCursor};
37//!
38//! let cursor = TextCursor {
39//!     cluster_id: GraphemeClusterId {
40//!         source_run: 0,
41//!         start_byte_in_run: 0,
42//!     },
43//!     affinity: CursorAffinity::Leading,
44//! };
45//! ```
46
47use alloc::collections::BTreeMap;
48use alloc::vec::Vec;
49use core::sync::atomic::{AtomicU64, Ordering};
50
51use crate::dom::{DomId, DomNodeId, NodeId};
52use crate::geom::{LogicalPosition, LogicalRect};
53
54/// A stable, logical pointer to an item within the original `InlineContent` array.
55///
56/// This structure eliminates the need for string concatenation and byte-offset math
57/// by tracking both the run index and the item index within that run.
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
59pub struct ContentIndex {
60    /// The index of the `InlineContent` run in the original input array.
61    pub run_index: u32,
62    /// The byte index of the character or item *within* that run's string.
63    pub item_index: u32,
64}
65
66/// A stable, logical identifier for a grapheme cluster.
67///
68/// This survives Bidi reordering and line breaking, making it ideal for tracking
69/// text positions for selection and cursor logic.
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
71#[repr(C)]
72pub struct GraphemeClusterId {
73    /// The `run_index` from the source `ContentIndex`.
74    pub source_run: u32,
75    /// The byte index of the start of the cluster in its original `StyledRun`.
76    pub start_byte_in_run: u32,
77}
78
79/// Represents the logical position of the cursor *between* two grapheme clusters
80/// or at the start/end of the text.
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
82#[repr(C)]
83pub enum CursorAffinity {
84    /// The cursor is at the leading edge of the character (left in LTR, right in RTL).
85    Leading,
86    /// The cursor is at the trailing edge of the character (right in LTR, left in RTL).
87    Trailing,
88}
89
90/// Represents a precise cursor location in the logical text.
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
92#[repr(C)]
93pub struct TextCursor {
94    /// The grapheme cluster the cursor is associated with.
95    pub cluster_id: GraphemeClusterId,
96    /// The edge of the cluster the cursor is on.
97    pub affinity: CursorAffinity,
98}
99
100impl_option!(
101    TextCursor,
102    OptionTextCursor,
103    [Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd]
104);
105
106/// Represents a range of selected text. The direction is implicit (start can be
107/// logically after end if selecting backwards).
108#[derive(Debug, PartialOrd, Ord, Clone, Copy, PartialEq, Eq, Hash)]
109#[repr(C)]
110pub struct SelectionRange {
111    pub start: TextCursor,
112    pub end: TextCursor,
113}
114
115impl_option!(
116    SelectionRange,
117    OptionSelectionRange,
118    [Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd]
119);
120
121impl_vec!(
122    SelectionRange,
123    SelectionRangeVec,
124    SelectionRangeVecDestructor,
125    SelectionRangeVecDestructorType,
126    SelectionRangeVecSlice,
127    OptionSelectionRange
128);
129impl_vec_debug!(SelectionRange, SelectionRangeVec);
130impl_vec_clone!(
131    SelectionRange,
132    SelectionRangeVec,
133    SelectionRangeVecDestructor
134);
135impl_vec_partialeq!(SelectionRange, SelectionRangeVec);
136impl_vec_partialord!(SelectionRange, SelectionRangeVec);
137
138/// A single selection, which can be either a blinking cursor or a highlighted range.
139#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
140#[repr(C, u8)]
141pub enum Selection {
142    Cursor(TextCursor),
143    Range(SelectionRange),
144}
145
146impl_option!(
147    Selection,
148    OptionSelection,
149    [Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord]
150);
151
152impl_vec!(
153    Selection,
154    SelectionVec,
155    SelectionVecDestructor,
156    SelectionVecDestructorType,
157    SelectionVecSlice,
158    OptionSelection
159);
160impl_vec_debug!(Selection, SelectionVec);
161impl_vec_clone!(Selection, SelectionVec, SelectionVecDestructor);
162impl_vec_partialeq!(Selection, SelectionVec);
163impl_vec_partialord!(Selection, SelectionVec);
164
165/// The complete selection state for a single text block, supporting multiple cursors/ranges.
166#[derive(Debug, Clone, PartialEq)]
167#[repr(C)]
168pub struct SelectionState {
169    /// A list of all active selections. This list is kept sorted and non-overlapping.
170    pub selections: SelectionVec,
171    /// The DOM node this selection state applies to.
172    pub node_id: DomNodeId,
173}
174
175impl SelectionState {
176    /// Adds a new selection, merging it with any existing selections it overlaps with.
177    pub fn add(&mut self, new_selection: Selection) {
178        // A full implementation would handle merging overlapping ranges.
179        // For now, we simply add and sort for simplicity.
180        let mut selections: Vec<Selection> = self.selections.as_ref().to_vec();
181        selections.push(new_selection);
182        selections.sort_unstable();
183        selections.dedup(); // Removes duplicate cursors
184        self.selections = selections.into();
185    }
186}
187
188impl_option!(
189    SelectionState,
190    OptionSelectionState,
191    copy = false,
192    clone = false,
193    [Debug, Clone, PartialEq]
194);
195
196// ============================================================================
197// MULTI-CURSOR SUPPORT (Sublime Text style)
198// ============================================================================
199
200/// Stable identifier for a cursor/selection within a `MultiCursorState`.
201///
202/// Uses a monotonic u64 counter (not UUID) so it is `Copy` and C-API friendly.
203/// Each `SelectionId` is unique within the lifetime of the process.
204#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
205#[repr(C)]
206pub struct SelectionId {
207    pub inner: u64,
208}
209
210impl SelectionId {
211    /// Generate a new unique `SelectionId`.
212    pub fn new() -> Self {
213        static COUNTER: AtomicU64 = AtomicU64::new(1);
214        Self {
215            inner: COUNTER.fetch_add(1, Ordering::Relaxed),
216        }
217    }
218}
219
220/// Note: `Default` generates a new unique ID (increments global counter),
221/// rather than returning a zero/sentinel value.
222impl Default for SelectionId {
223    fn default() -> Self {
224        Self::new()
225    }
226}
227
228impl_option!(
229    SelectionId,
230    OptionSelectionId,
231    [Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord]
232);
233
234impl_vec!(
235    SelectionId,
236    SelectionIdVec,
237    SelectionIdVecDestructor,
238    SelectionIdVecDestructorType,
239    SelectionIdVecSlice,
240    OptionSelectionId
241);
242impl_vec_debug!(SelectionId, SelectionIdVec);
243impl_vec_clone!(SelectionId, SelectionIdVec, SelectionIdVecDestructor);
244impl_vec_partialeq!(SelectionId, SelectionIdVec);
245impl_vec_partialord!(SelectionId, SelectionIdVec);
246
247/// A selection (cursor or range) paired with a stable identity.
248#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
249#[repr(C)]
250pub struct IdentifiedSelection {
251    pub id: SelectionId,
252    pub selection: Selection,
253    /// WHOSE selection this is (U1).
254    ///
255    /// [`SelectionOwner::LOCAL`] for the person at this machine, which is
256    /// every selection the engine creates itself. An app running a shared
257    /// editing session injects the other participants' cursors with their own
258    /// owners, and the paint path colours by this rather than by the node's
259    /// `caret-color` - one colour per participant is the whole point.
260    pub owner: SelectionOwner,
261}
262
263impl_option!(
264    IdentifiedSelection,
265    OptionIdentifiedSelection,
266    [Debug, Clone, Copy, PartialEq, Eq, Hash]
267);
268
269impl_vec!(
270    IdentifiedSelection,
271    IdentifiedSelectionVec,
272    IdentifiedSelectionVecDestructor,
273    IdentifiedSelectionVecDestructorType,
274    IdentifiedSelectionVecSlice,
275    OptionIdentifiedSelection
276);
277impl_vec_debug!(IdentifiedSelection, IdentifiedSelectionVec);
278impl_vec_clone!(
279    IdentifiedSelection,
280    IdentifiedSelectionVec,
281    IdentifiedSelectionVecDestructor
282);
283impl_vec_partialeq!(IdentifiedSelection, IdentifiedSelectionVec);
284
285/// WHO a cursor or selection belongs to (U1).
286///
287/// A 128-bit id rather than an index, because it has to survive a NETWORK: in a
288/// shared editing session the participants are decided elsewhere - a server, a
289/// CRDT peer id, a user account - and an engine-allocated number could not be
290/// agreed on by two machines. [`SelectionId`] is that engine-allocated number
291/// and stays local; this is the app's, and the two are separate fields for
292/// exactly that reason.
293///
294/// Split into two `u64`s rather than a `u128` because this crosses the C ABI,
295/// where `u128` had no stable layout until recently and still surprises
296/// bindings.
297#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
298#[repr(C)]
299pub struct SelectionOwner {
300    pub high: u64,
301    pub low: u64,
302}
303
304impl SelectionOwner {
305    /// The person at this machine.
306    ///
307    /// All-zero, so a `Default` selection is a local one and every existing
308    /// construction site keeps meaning what it meant.
309    pub const LOCAL: Self = Self { high: 0, low: 0 };
310
311    #[must_use]
312    pub const fn new(high: u64, low: u64) -> Self {
313        Self { high, low }
314    }
315
316    /// Is this the local participant?
317    #[must_use]
318    pub const fn is_local(self) -> bool {
319        self.high == 0 && self.low == 0
320    }
321
322    /// The `high` half every SEAT owner carries (9b-ii-a-i-d-ii-a). A random
323    /// (v4) peer UUID never has this exact high half, and it is non-zero so a
324    /// seat owner is never "local".
325    pub const SEAT_HIGH: u64 = 0x5EA7_0000_0000_0001;
326
327    /// The owner that stands for pointer / keyboard seat `seat_id`'s caret
328    /// (9b-ii-a-i-d-ii-a): a second person at this machine, drawn like a
329    /// peer caret in that seat's colour. Seat 0, the primary, is `LOCAL`.
330    #[must_use]
331    pub const fn seat(seat_id: u64) -> Self {
332        if seat_id == 0 {
333            return Self::LOCAL;
334        }
335        Self {
336            high: Self::SEAT_HIGH,
337            low: seat_id,
338        }
339    }
340
341    /// Whether this owner is a (non-primary) seat at this machine.
342    #[must_use]
343    pub const fn is_seat(self) -> bool {
344        self.high == Self::SEAT_HIGH
345    }
346
347    /// The seat this owner stands for, if it is one.
348    #[must_use]
349    pub const fn seat_id(self) -> Option<u64> {
350        if self.is_seat() {
351            Some(self.low)
352        } else {
353            None
354        }
355    }
356}
357
358/// Multi-cursor state for a contenteditable element (Sublime Text style).
359///
360/// Replaces the split `CursorManager` + `SelectionManager` pattern for text editing.
361/// Supports multiple simultaneous cursors/selections, each with a stable ID.
362///
363/// ## Invariants
364///
365/// - `selections` is sorted by owner, then by position, and non-overlapping
366///   within one owner.
367/// - The **primary** selection is identified by the stable `primary_id`, NOT by
368///   vector position: `merge_overlapping()` re-sorts `selections` by position,
369///   so "last index" is not the most-recently-added cursor.
370/// - After any mutation, `merge_overlapping()` is called to maintain invariants.
371///
372/// ## Who a selection belongs to (U3)
373///
374/// A selection is identified by an OWNER-SCOPED id: `(owner, id)`. The engine
375/// acts on the [`SelectionOwner::LOCAL`] set and on nothing else:
376///
377/// - the PRIMARY is always local - `get_primary` never answers with a peer's
378///   selection, and `primary_id` is re-pointed only at a local one;
379/// - the EDIT SET (`to_selections`, what typing / Backspace / paste apply
380///   to) is the local set, and `update_from_edit_result` writes back to it
381///   alone, leaving every peer's entry untouched;
382/// - a plain click (`set_single_cursor` / `set_single_range`) collapses the
383///   LOCAL set to one and keeps the peers in view;
384/// - cursor movement (`move_all_cursors*`) moves local carets only;
385/// - the platform's idea of "the selection" (`selectedTextRange`, the IME's
386///   marked range, the Android selection bridge) is the local primary.
387///
388/// Peers' selections are DISPLAY-ONLY SNAPSHOTS: they enter through
389/// `set_owner_selections`, leave through `remove_owner`, and are painted in
390/// their owner's colour. They are not shifted by a local edit either - the
391/// sync layer that carried the snapshot here is the one that knows how the
392/// edit moved the peer's caret, and it replaces the snapshot. Anything that
393/// walks `selections` directly and means "what is the user doing" must go
394/// through [`Self::local_selections`]; walking the whole list is right only
395/// for painting.
396#[derive(Debug, Clone, PartialEq, Eq)]
397pub struct MultiCursorState {
398    /// Sorted by position, non-overlapping. Primary is tracked via `primary_id`.
399    pub selections: Vec<IdentifiedSelection>,
400    /// Stable ID of the primary selection (most recently added/set). Survives the
401    /// position sort in `merge_overlapping`, which would otherwise make the
402    /// vector's last element (position-last) masquerade as the primary.
403    pub primary_id: SelectionId,
404    /// The DOM node this multi-cursor state applies to.
405    pub node_id: DomNodeId,
406    /// Stable key that survives DOM rebuilds (from `calculate_contenteditable_key`).
407    pub contenteditable_key: u64,
408}
409
410impl MultiCursorState {
411    /// Create a new `MultiCursorState` with a single cursor.
412    #[must_use]
413    pub fn new_with_cursor(
414        cursor: TextCursor,
415        node_id: DomNodeId,
416        contenteditable_key: u64,
417    ) -> Self {
418        let id = SelectionId::new();
419        Self {
420            selections: vec![IdentifiedSelection {
421                id,
422                selection: Selection::Cursor(cursor),
423                owner: SelectionOwner::LOCAL,
424            }],
425            primary_id: id,
426            node_id,
427            contenteditable_key,
428        }
429    }
430
431    /// Add a cursor, merging if it overlaps with existing selections.
432    /// Returns the `SelectionId` of the new (or merged) cursor.
433    #[must_use]
434    pub fn add_cursor(&mut self, cursor: TextCursor) -> SelectionId {
435        let id = SelectionId::new();
436        self.selections.push(IdentifiedSelection {
437            id,
438            selection: Selection::Cursor(cursor),
439            owner: SelectionOwner::LOCAL,
440        });
441        self.primary_id = id;
442        self.merge_overlapping();
443        id
444    }
445
446    /// Add a selection range, merging if it overlaps.
447    /// Returns the `SelectionId` of the new (or merged) selection.
448    #[must_use]
449    pub fn add_selection(&mut self, range: SelectionRange) -> SelectionId {
450        let id = SelectionId::new();
451        self.selections.push(IdentifiedSelection {
452            id,
453            selection: Selection::Range(range),
454            owner: SelectionOwner::LOCAL,
455        });
456        self.primary_id = id;
457        self.merge_overlapping();
458        id
459    }
460
461    /// Remove a selection by its stable ID. Returns true if found and removed.
462    #[must_use]
463    pub fn remove_selection(&mut self, id: SelectionId) -> bool {
464        let len_before = self.selections.len();
465        self.selections.retain(|s| s.id != id);
466        let removed = self.selections.len() < len_before;
467        if removed {
468            // If we just removed the primary, re-point it at a surviving one.
469            self.ensure_primary_valid();
470        }
471        removed
472    }
473
474    /// The LOCAL participant's selections - the ones the engine edits, moves
475    /// and reports (U3). Peers' snapshots are excluded.
476    pub fn local_selections(&self) -> impl Iterator<Item = &IdentifiedSelection> {
477        self.selections.iter().filter(|s| s.owner.is_local())
478    }
479
480    /// Mutable [`Self::local_selections`].
481    pub fn local_selections_mut(&mut self) -> impl Iterator<Item = &mut IdentifiedSelection> {
482        self.selections.iter_mut().filter(|s| s.owner.is_local())
483    }
484
485    /// How many carets the LOCAL user is typing into. This - not [`Self::len`]
486    /// - is the count a "one line per cursor" paste or a "multi-cursor mode"
487    /// decision wants; a peer's caret is not somewhere the local user types.
488    #[must_use]
489    pub fn local_len(&self) -> usize {
490        self.local_selections().count()
491    }
492
493    /// Get the primary selection (the most recently added/set, tracked by
494    /// `primary_id` — NOT the vector's last element, which position-sorting
495    /// reorders). Falls back to the last LOCAL selection if `primary_id` was
496    /// somehow lost - never to a peer's: the list is owner-sorted, so a plain
497    /// `last()` was a peer's entry whenever one existed, and everything that
498    /// reads "the selection" (IME, the platform selection, copy) would have
499    /// been answering with someone else's.
500    #[must_use]
501    pub fn get_primary(&self) -> Option<&IdentifiedSelection> {
502        let pid = self.primary_id;
503        self.selections
504            .iter()
505            .find(|s| s.id == pid && s.owner.is_local())
506            .or_else(|| self.local_selections().last())
507    }
508
509    /// Get a mutable reference to the primary selection (see `get_primary`).
510    pub fn get_primary_mut(&mut self) -> Option<&mut IdentifiedSelection> {
511        let pid = self.primary_id;
512        if let Some(pos) = self
513            .selections
514            .iter()
515            .position(|s| s.id == pid && s.owner.is_local())
516        {
517            return self.selections.get_mut(pos);
518        }
519        self.local_selections_mut().last()
520    }
521
522    /// Ensure `primary_id` names a LOCAL selection that still exists; if not,
523    /// adopt the last local one (best effort) so `get_primary` stays
524    /// meaningful. With no local selection left, `primary_id` is left dangling
525    /// on purpose and `get_primary` answers `None` - a peer's caret must not
526    /// become "the selection" because the local one went away.
527    fn ensure_primary_valid(&mut self) {
528        let pid = self.primary_id;
529        if !self.selections.iter().any(|s| s.id == pid && s.owner.is_local()) {
530            if let Some(last) = self.local_selections().last() {
531                self.primary_id = last.id;
532            }
533        }
534    }
535
536    /// Get the primary cursor position (for scroll-into-view, IME, etc.)
537    #[must_use]
538    pub fn get_primary_cursor(&self) -> Option<TextCursor> {
539        self.get_primary().map(|s| match &s.selection {
540            Selection::Cursor(c) => *c,
541            Selection::Range(r) => r.end,
542        })
543    }
544
545    /// The EDIT SET: the LOCAL selections, as a `Vec<Selection>` for
546    /// `edit_text()`. Peers' carets are excluded (U3) - with them included, a
547    /// local keystroke was applied at every peer's caret as well, because
548    /// multi-cursor editing inserts at every selection it is handed.
549    #[must_use]
550    pub fn to_selections(&self) -> Vec<Selection> {
551        self.local_selections().map(|s| s.selection).collect()
552    }
553
554    /// Update the LOCAL selections from the result of `edit_text()`.
555    ///
556    /// Preserves existing local IDs where possible (by index among the local
557    /// entries), assigns new IDs for extras. Peers' entries are carried over
558    /// UNTOUCHED, owner and id included: rebuilding the whole list as local
559    /// used to absorb every peer into the local user after the first
560    /// keystroke. Their POSITIONS are then moved across the edit by
561    /// [`Self::shift_peers_across`] (U3-a), by the caller that knows the
562    /// text delta.
563    pub fn update_from_edit_result(&mut self, new_selections: &[Selection]) {
564        let old_ids: Vec<SelectionId> = self.local_selections().map(|s| s.id).collect();
565        let peers: Vec<IdentifiedSelection> = self
566            .selections
567            .iter()
568            .filter(|s| !s.owner.is_local())
569            .copied()
570            .collect();
571        self.selections.clear();
572        for (i, sel) in new_selections.iter().enumerate() {
573            let id = old_ids.get(i).copied().unwrap_or_else(SelectionId::new);
574            self.selections.push(IdentifiedSelection {
575                id,
576                selection: *sel,
577                owner: SelectionOwner::LOCAL,
578            });
579        }
580        // Owner-sorted order: LOCAL is all-zero and sorts first, so the peers
581        // go back behind the rebuilt local set.
582        self.selections.extend(peers);
583        // IDs are reassigned by index; make sure primary_id still resolves.
584        self.ensure_primary_valid();
585        // Don't merge here — edit_text already returns correct positions
586    }
587
588    /// Move the PEERS' selections across a change to the text (U3-a).
589    ///
590    /// `update_from_edit_result` carries peers over verbatim because the edit
591    /// result knows nothing about them; the caller that knows what the edit
592    /// did to the text - one `RunTextChange` per changed run, all relative to
593    /// the OLD text - applies it here, and a peer's caret moves with the text
594    /// it was anchored in, per the user's ruling (see
595    /// [`RunTextChange::transform`]). Local selections are not touched: the
596    /// edit result already placed them. A peer RANGE has both ends moved and
597    /// may collapse when the change spans it.
598    ///
599    /// The sync layer still owns the semantics of CONCURRENT edits; this is
600    /// only the local user's own change, which the peer will also receive.
601    pub fn shift_peers_across(&mut self, changes: &[RunTextChange]) {
602        self.shift_across(changes, false);
603    }
604
605    /// Move EVERY selection, the local ones included, across a change to the
606    /// text that nobody's edit placed them for (U3-b): the app's new
607    /// generation carried different text for the node - a remote
608    /// participant's edit applied through the app's model, an app-side
609    /// rewrite - and each caret keeps its logical anchor in it.
610    pub fn shift_all_across(&mut self, changes: &[RunTextChange]) {
611        self.shift_across(changes, true);
612    }
613
614    /// `shift_peers_across` for a diff that may also have changed the run
615    /// count (U3-a-i).
616    pub fn shift_peers_across_diff(&mut self, diff: &RunTextDiff) {
617        self.shift_across_diff(diff, false);
618    }
619
620    /// `shift_all_across` for a diff that may also have changed the run
621    /// count (U3-a-i).
622    pub fn shift_all_across_diff(&mut self, diff: &RunTextDiff) {
623        self.shift_across_diff(diff, true);
624    }
625
626    fn shift_across_diff(&mut self, diff: &RunTextDiff, include_local: bool) {
627        if diff.is_empty() {
628            return;
629        }
630        for sel in self
631            .selections
632            .iter_mut()
633            .filter(|s| include_local || !s.owner.is_local())
634        {
635            sel.selection = match sel.selection {
636                Selection::Cursor(c) => Selection::Cursor(diff.map_cursor(c)),
637                Selection::Range(r) => {
638                    let start = diff.map_cursor(r.start);
639                    let end = diff.map_cursor(r.end);
640                    if start == end {
641                        Selection::Cursor(start)
642                    } else {
643                        Selection::Range(SelectionRange { start, end })
644                    }
645                }
646            };
647        }
648    }
649
650    fn shift_across(&mut self, changes: &[RunTextChange], include_local: bool) {
651        if changes.is_empty() {
652            return;
653        }
654        let shift = |mut c: TextCursor| -> TextCursor {
655            for change in changes {
656                if change.run == c.cluster_id.source_run {
657                    c.cluster_id.start_byte_in_run =
658                        change.transform(c.cluster_id.start_byte_in_run);
659                }
660            }
661            c
662        };
663        for sel in self
664            .selections
665            .iter_mut()
666            .filter(|s| include_local || !s.owner.is_local())
667        {
668            sel.selection = match sel.selection {
669                Selection::Cursor(c) => Selection::Cursor(shift(c)),
670                Selection::Range(r) => Selection::Range(SelectionRange {
671                    start: shift(r.start),
672                    end: shift(r.end),
673                }),
674            };
675        }
676    }
677
678    /// The id a collapsed local set keeps: the local primary's when there is
679    /// one, so a caller tracking it sees the same selection continue.
680    fn surviving_local_id(&self) -> SelectionId {
681        self.get_primary().map_or_else(SelectionId::new, |primary| primary.id)
682    }
683
684    /// Collapse the LOCAL selections to a single cursor (a plain click without
685    /// Ctrl). Peers' selections stay: a click is not a message to them.
686    pub fn set_single_cursor(&mut self, cursor: TextCursor) {
687        let id = self.surviving_local_id();
688        self.selections.retain(|s| !s.owner.is_local());
689        self.selections.insert(
690            0,
691            IdentifiedSelection {
692                id,
693                selection: Selection::Cursor(cursor),
694                owner: SelectionOwner::LOCAL,
695            },
696        );
697        self.primary_id = id;
698    }
699
700    /// Collapse the LOCAL selections to a single range. Peers stay, as above.
701    pub fn set_single_range(&mut self, range: SelectionRange) {
702        let id = self.surviving_local_id();
703        self.selections.retain(|s| !s.owner.is_local());
704        self.selections.insert(
705            0,
706            IdentifiedSelection {
707                id,
708                selection: Selection::Range(range),
709                owner: SelectionOwner::LOCAL,
710            },
711        );
712        self.primary_id = id;
713    }
714
715    /// Number of selections of EVERY owner - what is painted. For how many
716    /// carets the local user types into, see [`Self::local_len`].
717    #[must_use]
718    pub const fn len(&self) -> usize {
719        self.selections.len()
720    }
721
722    /// Whether there are no selections (should not normally happen).
723    #[must_use]
724    pub const fn is_empty(&self) -> bool {
725        self.selections.is_empty()
726    }
727
728    /// Sort selections by position and merge any that overlap.
729    pub fn merge_overlapping(&mut self) {
730        if self.selections.len() <= 1 {
731            return;
732        }
733
734        // Capture the primary before sorting/merging reorders and rewrites IDs.
735        let primary = self.primary_id;
736        let mut new_primary = primary;
737
738        // BY OWNER FIRST, then by position (U1). Two participants' cursors
739        // must never merge: in a shared editing session that would silently
740        // delete someone from the document - their caret absorbed into another
741        // person's and repainted in that person's colour. Sorting by owner
742        // groups each participant's selections so the adjacency check below
743        // only ever compares two of the same owner's.
744        self.selections.sort_by(|a, b| {
745            a.owner.cmp(&b.owner).then_with(|| {
746                let pos_a = selection_start_pos(&a.selection);
747                let pos_b = selection_start_pos(&b.selection);
748                pos_a.cmp(&pos_b)
749            })
750        });
751
752        // Merge overlapping: if selection[i+1] starts at or before selection[i] ends,
753        // merge them into one range (keeping the later ID as it's more recent).
754        let mut merged: Vec<IdentifiedSelection> = Vec::with_capacity(self.selections.len());
755        for sel in self.selections.drain(..) {
756            if let Some(last) = merged.last_mut() {
757                let last_end = selection_end_pos(&last.selection);
758                let cur_start = selection_start_pos(&sel.selection);
759                // SAME OWNER ONLY - see the sort above.
760                if last.owner == sel.owner && cur_start <= last_end {
761                    // Overlap — merge into one range covering both
762                    let new_start = selection_start_pos(&last.selection);
763                    let cur_end = selection_end_pos(&sel.selection);
764                    let new_end = if cur_end > last_end {
765                        cur_end
766                    } else {
767                        last_end
768                    };
769                    if new_start == new_end {
770                        last.selection = Selection::Cursor(new_start);
771                    } else {
772                        last.selection = Selection::Range(SelectionRange {
773                            start: new_start,
774                            end: new_end,
775                        });
776                    }
777                    // If either side of the merge — or the accumulator that has
778                    // already absorbed the primary earlier in the chain — was the
779                    // primary, the merged selection inherits primary status.
780                    // `last.id == new_primary` carries the primary across a 3+-link
781                    // chain: without it, `new_primary` would keep pointing at an
782                    // intermediate id that the next merge overwrites, and
783                    // `ensure_primary_valid` would then adopt an unrelated tail.
784                    let inherits_primary =
785                        last.id == primary || sel.id == primary || last.id == new_primary;
786                    // Keep the newer ID (the one being merged in)
787                    last.id = sel.id;
788                    if inherits_primary {
789                        new_primary = sel.id;
790                    }
791                    continue;
792                }
793            }
794            merged.push(sel);
795        }
796        self.selections = merged;
797
798        // Point primary at a surviving selection (fallback: last element).
799        self.primary_id = new_primary;
800        self.ensure_primary_valid();
801    }
802
803    /// Replace everything ONE participant owns (U1).
804    ///
805    /// The injection point for a shared editing session: a peer's cursor
806    /// arrives over the network, and this makes it the whole of what that peer
807    /// has selected. Replacing rather than merging is deliberate - a remote
808    /// participant's state is a SNAPSHOT, and adding to it would leave stale
809    /// carets behind whenever a message was missed.
810    ///
811    /// Refuses to touch [`SelectionOwner::LOCAL`]: the local caret is the
812    /// engine's, and letting an app overwrite it through this door would make
813    /// every text-editing invariant the engine maintains someone else's
814    /// problem. Returns `false` in that case.
815    pub fn set_owner_selections(
816        &mut self,
817        owner: SelectionOwner,
818        selections: &[Selection],
819    ) -> bool {
820        if owner.is_local() {
821            return false;
822        }
823        self.selections.retain(|s| s.owner != owner);
824        for selection in selections {
825            self.selections.push(IdentifiedSelection {
826                id: SelectionId::new(),
827                selection: *selection,
828                owner,
829            });
830        }
831        // NOT `merge_overlapping` here: that call is about keeping the LOCAL
832        // caret set sane after a movement, and a remote snapshot is already
833        // whatever the peer says it is. Merging it would also renumber ids the
834        // caller may be tracking.
835        self.ensure_primary_valid();
836        true
837    }
838
839    /// Forget a participant - they left, or their connection dropped.
840    ///
841    /// Returns how many selections went. `LOCAL` is refused for the same
842    /// reason as above; removing it would leave the document with no caret.
843    pub fn remove_owner(&mut self, owner: SelectionOwner) -> usize {
844        if owner.is_local() {
845            return 0;
846        }
847        let before = self.selections.len();
848        self.selections.retain(|s| s.owner != owner);
849        self.ensure_primary_valid();
850        before - self.selections.len()
851    }
852
853    /// Every participant with a selection right now, `LOCAL` included.
854    #[must_use]
855    pub fn owners(&self) -> Vec<SelectionOwner> {
856        let mut out: Vec<SelectionOwner> = self.selections.iter().map(|s| s.owner).collect();
857        out.sort_unstable();
858        out.dedup();
859        out
860    }
861
862    /// Move all cursors using a movement function. Merges collisions afterward.
863    ///
864    /// `move_fn` takes a `TextCursor` and returns the new `TextCursor` after movement.
865    /// If `extend_selection` is true, the anchor stays and only the focus moves,
866    /// creating or extending a range.
867    ///
868    /// A bare (non-extending) move over an active range COLLAPSES to the range
869    /// boundary — the arrow-key rule. Use [`Self::move_all_cursors_with`] for
870    /// steps where that is wrong (Home/End, document jumps).
871    pub fn move_all_cursors(
872        &mut self,
873        extend_selection: bool,
874        move_fn: impl Fn(&TextCursor) -> TextCursor,
875    ) {
876        self.move_all_cursors_with(extend_selection, true, move_fn);
877    }
878
879    /// [`Self::move_all_cursors`], with control over what a bare move does to
880    /// an active range.
881    ///
882    /// `collapse_range_to_boundary` is the arrow-key rule: Left/Right with a
883    /// selection put the caret on the selection's edge and go no further.
884    /// Every OTHER step — Home/End, Ctrl+Home/End, a visual line, a word — is a
885    /// MOVEMENT and must be performed: collapsing them to the nearest edge is
886    /// how pressing End with text selected used to leave the caret sitting at
887    /// the end of the selection instead of the end of the line.
888    pub fn move_all_cursors_with(
889        &mut self,
890        extend_selection: bool,
891        collapse_range_to_boundary: bool,
892        move_fn: impl Fn(&TextCursor) -> TextCursor,
893    ) {
894        // LOCAL carets only (U3): an arrow key is the local user's, and moving
895        // a peer's caret with it would show the peer somewhere they are not.
896        for sel in self.selections.iter_mut().filter(|s| s.owner.is_local()) {
897            match &sel.selection {
898                Selection::Cursor(c) => {
899                    let new_cursor = move_fn(c);
900                    if extend_selection {
901                        if *c != new_cursor {
902                            sel.selection = Selection::Range(SelectionRange {
903                                start: *c,
904                                end: new_cursor,
905                            });
906                        }
907                    } else {
908                        sel.selection = Selection::Cursor(new_cursor);
909                    }
910                }
911                Selection::Range(r) => {
912                    if extend_selection {
913                        let new_end = move_fn(&r.end);
914                        if r.start == new_end {
915                            sel.selection = Selection::Cursor(r.start);
916                        } else {
917                            sel.selection = Selection::Range(SelectionRange {
918                                start: r.start,
919                                end: new_end,
920                            });
921                        }
922                    } else if collapse_range_to_boundary {
923                        // Bare arrow with an active selection collapses the caret
924                        // to the selection boundary in the arrow's direction WITHOUT
925                        // advancing a character (standard editor behavior). Running
926                        // move_fn on the focus and using that as the caret would step
927                        // one unit past the edge. We don't get the arrow direction
928                        // here, so probe it: apply move_fn to the focus and compare —
929                        // a forward move collapses to the max boundary, a backward
930                        // move to the min boundary.
931                        let (lo, hi) = if r.start <= r.end {
932                            (r.start, r.end)
933                        } else {
934                            (r.end, r.start)
935                        };
936                        let probe = move_fn(&r.end);
937                        let collapsed = if probe >= r.end { hi } else { lo };
938                        sel.selection = Selection::Cursor(collapsed);
939                    } else {
940                        // Home / End / Ctrl+Home / Ctrl+End / a visual line step:
941                        // the caret goes where the step points, measured from the
942                        // focus. The boundary collapse above would strand it on
943                        // the selection's edge instead.
944                        sel.selection = Selection::Cursor(move_fn(&r.end));
945                    }
946                }
947            }
948        }
949        self.merge_overlapping();
950    }
951
952    /// Remap the `NodeId` in `node_id` after DOM reconciliation.
953    ///
954    /// If the node was removed (not in the map), the multi-cursor state is cleared.
955    pub fn remap_node_ids(&mut self, dom_id: DomId, node_id_map: &BTreeMap<NodeId, NodeId>) {
956        if self.node_id.dom != dom_id {
957            return;
958        }
959        if let Some(old_node_id) = self.node_id.node.into_crate_internal() {
960            if let Some(&new_node_id) = node_id_map.get(&old_node_id) {
961                self.node_id.node =
962                    crate::styled_dom::NodeHierarchyItemId::from_crate_internal(Some(new_node_id));
963            } else {
964                // Node removed — clear selections
965                self.selections.clear();
966            }
967        }
968    }
969}
970
971/// Helper: get the start position of a Selection for sorting.
972fn selection_start_pos(sel: &Selection) -> TextCursor {
973    match sel {
974        Selection::Cursor(c) => *c,
975        Selection::Range(r) => {
976            if r.start <= r.end {
977                r.start
978            } else {
979                r.end
980            }
981        }
982    }
983}
984
985/// Helper: get the end position of a Selection for merging.
986fn selection_end_pos(sel: &Selection) -> TextCursor {
987    match sel {
988        Selection::Cursor(c) => *c,
989        Selection::Range(r) => {
990            if r.end >= r.start {
991                r.end
992            } else {
993                r.start
994            }
995        }
996    }
997}
998
999// ============================================================================
1000// MULTI-NODE SELECTION (Browser-style Anchor/Focus model)
1001// ============================================================================
1002
1003/// The anchor point of a text selection - where the user started selecting.
1004///
1005/// This is the fixed point during a drag operation. It records:
1006/// - The IFC root node (where the `UnifiedLayout` lives)
1007/// - The exact cursor position within that layout
1008/// - The visual bounds of the anchor character (for logical rectangle calculations)
1009///
1010/// The anchor remains constant during a drag; only the focus moves.
1011#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1012pub struct SelectionAnchor {
1013    /// The IFC root node ID where selection started.
1014    /// This is the node that has `inline_layout_result` (e.g., `<p>`, `<div>`).
1015    pub ifc_root_node_id: NodeId,
1016
1017    /// The exact cursor position within the IFC's `UnifiedLayout`.
1018    pub cursor: TextCursor,
1019
1020    /// Visual bounds of the anchor character in viewport coordinates.
1021    /// Used for computing the logical selection rectangle during multi-line/multi-node selection.
1022    pub char_bounds: LogicalRect,
1023
1024    /// The mouse position when the selection started (viewport coordinates).
1025    pub mouse_position: LogicalPosition,
1026}
1027
1028/// The focus point of a text selection - where the selection currently ends.
1029///
1030/// This is the movable point during a drag operation. It updates on every mouse move.
1031#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1032pub struct SelectionFocus {
1033    /// The IFC root node ID where selection currently ends.
1034    /// May differ from anchor's IFC root during cross-node selection.
1035    pub ifc_root_node_id: NodeId,
1036
1037    /// The exact cursor position within the IFC's `UnifiedLayout`.
1038    pub cursor: TextCursor,
1039
1040    /// Current mouse position in viewport coordinates.
1041    pub mouse_position: LogicalPosition,
1042}
1043
1044/// Complete selection state spanning potentially multiple DOM nodes.
1045///
1046/// This implements the W3C Selection API model with anchor/focus endpoints.
1047/// The selection can span multiple IFC roots (e.g., multiple `<p>` elements).
1048///
1049/// ## Storage Model
1050///
1051/// Uses `BTreeMap<NodeId, Vec<SelectionRange>>` for O(log N) lookup during rendering.
1052/// The key is the **IFC root `NodeId`**, and the value is every `SelectionRange`
1053/// that IFC contributes.
1054///
1055/// ## Example
1056///
1057/// ```text
1058/// <p id="1">Hello [World</p>     <- Anchor in IFC 1, partial selection
1059/// <p id="2">Complete line</p>    <- InBetween, fully selected
1060/// <p id="3">Partial] end</p>     <- Focus in IFC 3, partial selection
1061/// ```
1062#[derive(Debug, Clone, PartialEq, Eq)]
1063pub struct TextSelection {
1064    /// The DOM this selection belongs to.
1065    pub dom_id: DomId,
1066
1067    /// The anchor point - where the selection started (fixed during drag).
1068    pub anchor: SelectionAnchor,
1069
1070    /// The focus point - where the selection currently ends (moves during drag).
1071    pub focus: SelectionFocus,
1072
1073    /// Map from IFC root `NodeId` to the `SelectionRange`s for that IFC.
1074    /// This allows O(log N) lookup during rendering.
1075    ///
1076    /// Each `SelectionRange` contains the actual `TextCursor` positions for that IFC,
1077    /// ready to be passed to `UnifiedLayout::get_selection_rects()`.
1078    ///
1079    /// A node carries SEVERAL ranges when a multi-cursor session selects several
1080    /// occurrences in it (Ctrl+D); the ranges are disjoint and in document order.
1081    pub affected_nodes: BTreeMap<NodeId, Vec<SelectionRange>>,
1082
1083    /// OTHER PARTICIPANTS' ranges on the same nodes, with whose they are
1084    /// (U1-a).
1085    ///
1086    /// Separate from `affected_nodes` rather than mixed into it, because the
1087    /// two are painted differently and mean different things: that one is the
1088    /// LOCAL user's selection and takes the node's `::selection` colour, while
1089    /// these take their owner's. Mixing them made a remote participant's range
1090    /// look like the local user's own, which is worse than not showing it.
1091    ///
1092    /// Empty for a single-user app, which is every app until one injects a
1093    /// remote owner.
1094    pub remote_ranges: BTreeMap<NodeId, Vec<(SelectionOwner, SelectionRange)>>,
1095
1096    /// Indicates whether anchor comes before focus in document order.
1097    /// True = forward selection (left-to-right), False = backward selection.
1098    pub is_forward: bool,
1099}
1100
1101/// One contiguous change to a run's text (U3-a): bytes `start..end` of the
1102/// OLD text were replaced by `inserted` bytes. The shape every caret shift is
1103/// computed from, whoever made the change.
1104/// An edit that changed the RUN COUNT of a node's inline content (U3-a-i):
1105/// a delete spanning two styled runs merged them, a styled paste split one.
1106/// The runs before `first` and the runs after the changed middle are the
1107/// same in both generations (aligned by common prefix and suffix); a caret
1108/// in the middle is mapped through the concatenated middle text, a caret
1109/// after it keeps its byte and moves its run index by the count delta.
1110#[derive(Debug, Clone, PartialEq, Eq)]
1111pub struct RunRemap {
1112    /// The first old run index that differs.
1113    pub first: u32,
1114    /// Text lengths of the old middle runs `first ..`.
1115    pub old_lens: Vec<u32>,
1116    /// Text lengths of the new middle runs `first ..`.
1117    pub new_lens: Vec<u32>,
1118    /// The byte change between the concatenated old and new middle texts,
1119    /// if they differ (`run` is meaningless here).
1120    pub middle: Option<RunTextChange>,
1121    /// Length of the unchanged run just before the middle, when there is
1122    /// one: where a caret lands when the whole middle vanished.
1123    pub prev_len: Option<u32>,
1124}
1125
1126impl RunRemap {
1127    /// Where a caret of the old generation sits in the new one.
1128    #[must_use]
1129    pub fn map_cursor(&self, c: TextCursor) -> TextCursor {
1130        let run = c.cluster_id.source_run;
1131        let first = self.first;
1132        let old_end = first + self.old_lens.len() as u32;
1133        let new_end = first + self.new_lens.len() as u32;
1134        if run < first {
1135            return c;
1136        }
1137        if run >= old_end {
1138            return TextCursor {
1139                cluster_id: GraphemeClusterId {
1140                    source_run: run - old_end + new_end,
1141                    start_byte_in_run: c.cluster_id.start_byte_in_run,
1142                },
1143                affinity: c.affinity,
1144            };
1145        }
1146        // In the middle: through the concatenated text.
1147        let mut global: u32 = self.old_lens[..(run - first) as usize].iter().sum();
1148        global = global.saturating_add(c.cluster_id.start_byte_in_run);
1149        if let Some(m) = &self.middle {
1150            global = m.transform(global);
1151        }
1152        let total_new: u32 = self.new_lens.iter().sum();
1153        global = global.min(total_new);
1154        if self.new_lens.is_empty() {
1155            // The middle vanished: the end of the run before it, or the
1156            // start of what follows.
1157            return match (first.checked_sub(1), self.prev_len) {
1158                (Some(prev), Some(len)) => TextCursor {
1159                    cluster_id: GraphemeClusterId {
1160                        source_run: prev,
1161                        start_byte_in_run: len,
1162                    },
1163                    affinity: c.affinity,
1164                },
1165                _ => TextCursor {
1166                    cluster_id: GraphemeClusterId {
1167                        source_run: first,
1168                        start_byte_in_run: 0,
1169                    },
1170                    affinity: c.affinity,
1171                },
1172            };
1173        }
1174        let mut offset = global;
1175        let mut target = first;
1176        for (i, len) in self.new_lens.iter().enumerate() {
1177            let last = i + 1 == self.new_lens.len();
1178            if offset <= *len || last {
1179                target = first + i as u32;
1180                break;
1181            }
1182            offset -= len;
1183        }
1184        TextCursor {
1185            cluster_id: GraphemeClusterId {
1186                source_run: target,
1187                start_byte_in_run: offset,
1188            },
1189            affinity: c.affinity,
1190        }
1191    }
1192}
1193
1194/// What an edit did to a node's text, for moving carets across it (U3-a,
1195/// U3-a-i): a run remap when the run count changed, then byte changes
1196/// within runs.
1197#[derive(Debug, Clone, PartialEq, Eq, Default)]
1198pub struct RunTextDiff {
1199    pub remap: Option<RunRemap>,
1200    pub changes: Vec<RunTextChange>,
1201}
1202
1203impl RunTextDiff {
1204    #[must_use]
1205    pub fn is_empty(&self) -> bool {
1206        self.remap.is_none() && self.changes.is_empty()
1207    }
1208
1209    /// A caret of the old generation in the new one: the remap first, then
1210    /// the byte changes of its (new) run.
1211    #[must_use]
1212    pub fn map_cursor(&self, c: TextCursor) -> TextCursor {
1213        let mut c = match &self.remap {
1214            Some(remap) => remap.map_cursor(c),
1215            None => c,
1216        };
1217        for change in &self.changes {
1218            if change.run == c.cluster_id.source_run {
1219                c.cluster_id.start_byte_in_run = change.transform(c.cluster_id.start_byte_in_run);
1220            }
1221        }
1222        c
1223    }
1224}
1225
1226#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1227pub struct RunTextChange {
1228    /// The run (`GraphemeClusterId::source_run`) whose text changed.
1229    pub run: u32,
1230    /// First changed byte of the old text.
1231    pub start: u32,
1232    /// One past the last changed byte of the old text (`== start` for a pure
1233    /// insert).
1234    pub end: u32,
1235    /// How many bytes replaced `start..end`.
1236    pub inserted: u32,
1237}
1238
1239impl RunTextChange {
1240    /// The smallest change between two versions of one run's text: the common
1241    /// prefix and the common suffix are unchanged, what lies between was
1242    /// replaced. `None` when the texts are identical.
1243    ///
1244    /// Byte-level, then backed off to char boundaries on BOTH sides so a
1245    /// change never starts or ends inside a multi-byte character. When the
1246    /// inserted text repeats its neighbour (`aa` -> `aaa`) the position is
1247    /// ambiguous by nature; the prefix wins, which puts the change AFTER the
1248    /// existing copy - the only carets that can tell the difference sit
1249    /// between identical characters.
1250    #[must_use]
1251    #[allow(clippy::cast_possible_truncation)] // run text is bounded far below u32
1252    pub fn between(run: u32, old: &str, new: &str) -> Option<Self> {
1253        if old == new {
1254            return None;
1255        }
1256        let ob = old.as_bytes();
1257        let nb = new.as_bytes();
1258        let shorter = ob.len().min(nb.len());
1259        let mut prefix = 0;
1260        while prefix < shorter && ob[prefix] == nb[prefix] {
1261            prefix += 1;
1262        }
1263        while prefix > 0 && !(old.is_char_boundary(prefix) && new.is_char_boundary(prefix)) {
1264            prefix -= 1;
1265        }
1266        let mut suffix = 0;
1267        while suffix < shorter - prefix && ob[ob.len() - 1 - suffix] == nb[nb.len() - 1 - suffix] {
1268            suffix += 1;
1269        }
1270        while suffix > 0
1271            && !(old.is_char_boundary(ob.len() - suffix) && new.is_char_boundary(nb.len() - suffix))
1272        {
1273            suffix -= 1;
1274        }
1275        Some(Self {
1276            run,
1277            start: prefix as u32,
1278            end: (ob.len() - suffix) as u32,
1279            inserted: (nb.len() - prefix - suffix) as u32,
1280        })
1281    }
1282
1283    /// Where a caret that sat at `byte` of the OLD text sits in the new one.
1284    ///
1285    /// The user's rule (2026-09-03): a caret is anchored at a LOGICAL
1286    /// position, so a change entirely before it shifts it by the delta, a
1287    /// change entirely after it leaves it alone, and a change that spans it
1288    /// collapses it to the change's start - the only position that still
1289    /// exists. A caret AT a pure insert's position counts as after it: it is
1290    /// attached to the character that follows, and that character moved.
1291    #[must_use]
1292    pub fn transform(&self, byte: u32) -> u32 {
1293        if byte < self.start {
1294            byte
1295        } else if byte >= self.end {
1296            byte - (self.end - self.start) + self.inserted
1297        } else {
1298            self.start
1299        }
1300    }
1301}
1302
1303impl TextSelection {
1304    /// Create a new collapsed selection (cursor) at the given position.
1305    #[must_use]
1306    pub fn new_collapsed(
1307        dom_id: DomId,
1308        ifc_root_node_id: NodeId,
1309        cursor: TextCursor,
1310        char_bounds: LogicalRect,
1311        mouse_position: LogicalPosition,
1312    ) -> Self {
1313        let anchor = SelectionAnchor {
1314            ifc_root_node_id,
1315            cursor,
1316            char_bounds,
1317            mouse_position,
1318        };
1319
1320        let focus = SelectionFocus {
1321            ifc_root_node_id,
1322            cursor,
1323            mouse_position,
1324        };
1325
1326        // For a collapsed selection, the anchor node has a zero-width range
1327        let mut affected_nodes = BTreeMap::new();
1328        affected_nodes.insert(
1329            ifc_root_node_id,
1330            vec![SelectionRange {
1331                start: cursor,
1332                end: cursor,
1333            }],
1334        );
1335
1336        Self {
1337            remote_ranges: BTreeMap::new(),
1338            dom_id,
1339            anchor,
1340            focus,
1341            affected_nodes,
1342            is_forward: true, // Direction doesn't matter for collapsed selection
1343        }
1344    }
1345
1346    /// Check if this is a collapsed selection (cursor with no range).
1347    #[must_use]
1348    pub fn is_collapsed(&self) -> bool {
1349        self.anchor.ifc_root_node_id == self.focus.ifc_root_node_id
1350            && self.anchor.cursor == self.focus.cursor
1351    }
1352
1353    /// Get the FIRST selection range for a specific IFC root node.
1354    /// Returns `None` if this node is not part of the selection.
1355    ///
1356    /// A multi-range node has more; [`Self::ranges_for_node`] returns all of them.
1357    #[must_use]
1358    pub fn get_range_for_node(&self, ifc_root_node_id: &NodeId) -> Option<&SelectionRange> {
1359        self.affected_nodes
1360            .get(ifc_root_node_id)
1361            .and_then(|r| r.first())
1362    }
1363
1364    /// Every range this IFC root contributes (empty slice when unaffected).
1365    #[must_use]
1366    pub fn ranges_for_node(&self, ifc_root_node_id: &NodeId) -> &[SelectionRange] {
1367        self.affected_nodes
1368            .get(ifc_root_node_id)
1369            .map_or(&[], Vec::as_slice)
1370    }
1371}
1372
1373impl_option!(
1374    TextSelection,
1375    OptionTextSelection,
1376    copy = false,
1377    clone = false,
1378    [Debug, Clone, PartialEq, Eq]
1379);
1380
1381// ============================================================================
1382// App-facing document coordinates (the CallbackInfo selection/sync API)
1383// ============================================================================
1384
1385/// A position in a node's TEXT CONTENT, app-facing: `text_byte` indexes the
1386/// flattened text of `node` — the exact string
1387/// `CallbackInfo::get_node_text_content(node)` returns (overlay-first, so it
1388/// sees uncommitted typing). The engine resolves cluster ids and affinity
1389/// BEFORE handing this out: the byte always lies on a grapheme-cluster
1390/// boundary of that string (a ZWJ emoji family or a decomposed `é` is never
1391/// split), and is in LOGICAL order — bidi visual reordering does not affect
1392/// it.
1393#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
1394#[repr(C)]
1395pub struct DocumentPosition {
1396    pub node: DomNodeId,
1397    pub text_byte: u32,
1398}
1399
1400impl_option!(
1401    DocumentPosition,
1402    OptionDocumentPosition,
1403    [Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord]
1404);
1405
1406/// A selected byte span `[start_byte, end_byte)` of `node`'s text content,
1407/// in the coordinates of [`DocumentPosition`]. Always LOGICAL and
1408/// NORMALIZED: `start_byte <= end_byte` regardless of drag direction or
1409/// script direction (an RTL selection is still a forward byte span). A
1410/// cross-block selection yields one span per affected node, in document
1411/// order.
1412#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
1413#[repr(C)]
1414pub struct DocumentSelectionSpan {
1415    pub node: DomNodeId,
1416    pub start_byte: u32,
1417    pub end_byte: u32,
1418}
1419
1420impl_option!(
1421    DocumentSelectionSpan,
1422    OptionDocumentSelectionSpan,
1423    [Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord]
1424);
1425
1426impl_vec!(
1427    DocumentSelectionSpan,
1428    DocumentSelectionSpanVec,
1429    DocumentSelectionSpanVecDestructor,
1430    DocumentSelectionSpanVecDestructorType,
1431    DocumentSelectionSpanVecSlice,
1432    OptionDocumentSelectionSpan
1433);
1434impl_vec_debug!(DocumentSelectionSpan, DocumentSelectionSpanVec);
1435impl_vec_clone!(
1436    DocumentSelectionSpan,
1437    DocumentSelectionSpanVec,
1438    DocumentSelectionSpanVecDestructor
1439);
1440impl_vec_partialeq!(DocumentSelectionSpan, DocumentSelectionSpanVec);
1441impl_vec_partialord!(DocumentSelectionSpan, DocumentSelectionSpanVec);
1442
1443/// One un-synced character-level edit: `node`'s effective text is now
1444/// `text` (revision-stamped). The app folds it into its model and acks the
1445/// highest revision it saw via `CallbackInfo::mark_text_revision_synced` —
1446/// the character-path counterpart of the structural DocumentEdit loop.
1447#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
1448#[repr(C)]
1449pub struct DocumentTextEdit {
1450    pub node: DomNodeId,
1451    pub text: azul_css::corety::AzString,
1452    pub revision: u64,
1453}
1454
1455impl_option!(
1456    DocumentTextEdit,
1457    OptionDocumentTextEdit,
1458    copy = false,
1459    [Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord]
1460);
1461
1462impl_vec!(
1463    DocumentTextEdit,
1464    DocumentTextEditVec,
1465    DocumentTextEditVecDestructor,
1466    DocumentTextEditVecDestructorType,
1467    DocumentTextEditVecSlice,
1468    OptionDocumentTextEdit
1469);
1470impl_vec_debug!(DocumentTextEdit, DocumentTextEditVec);
1471impl_vec_clone!(DocumentTextEdit, DocumentTextEditVec, DocumentTextEditVecDestructor);
1472impl_vec_partialeq!(DocumentTextEdit, DocumentTextEditVec);
1473impl_vec_partialord!(DocumentTextEdit, DocumentTextEditVec);
1474
1475#[cfg(test)]
1476#[path = "selection_test.rs"]
1477mod selection_test;