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;