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

            
47
use alloc::collections::BTreeMap;
48
use alloc::vec::Vec;
49
use core::sync::atomic::{AtomicU64, Ordering};
50

            
51
use crate::dom::{DomId, DomNodeId, NodeId};
52
use 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)]
59
pub 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)]
72
pub 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)]
83
pub 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)]
93
pub 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

            
100
impl_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)]
110
pub struct SelectionRange {
111
    pub start: TextCursor,
112
    pub end: TextCursor,
113
}
114

            
115
impl_option!(
116
    SelectionRange,
117
    OptionSelectionRange,
118
    [Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd]
119
);
120

            
121
impl_vec!(SelectionRange, SelectionRangeVec, SelectionRangeVecDestructor, SelectionRangeVecDestructorType, SelectionRangeVecSlice, OptionSelectionRange);
122
impl_vec_debug!(SelectionRange, SelectionRangeVec);
123
impl_vec_clone!(
124
    SelectionRange,
125
    SelectionRangeVec,
126
    SelectionRangeVecDestructor
127
);
128
impl_vec_partialeq!(SelectionRange, SelectionRangeVec);
129
impl_vec_partialord!(SelectionRange, SelectionRangeVec);
130

            
131
/// A single selection, which can be either a blinking cursor or a highlighted range.
132
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
133
#[repr(C, u8)]
134
pub enum Selection {
135
    Cursor(TextCursor),
136
    Range(SelectionRange),
137
}
138

            
139
impl_option!(
140
    Selection,
141
    OptionSelection,
142
    [Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord]
143
);
144

            
145
impl_vec!(Selection, SelectionVec, SelectionVecDestructor, SelectionVecDestructorType, SelectionVecSlice, OptionSelection);
146
impl_vec_debug!(Selection, SelectionVec);
147
impl_vec_clone!(Selection, SelectionVec, SelectionVecDestructor);
148
impl_vec_partialeq!(Selection, SelectionVec);
149
impl_vec_partialord!(Selection, SelectionVec);
150

            
151
/// The complete selection state for a single text block, supporting multiple cursors/ranges.
152
#[derive(Debug, Clone, PartialEq)]
153
#[repr(C)]
154
pub struct SelectionState {
155
    /// A list of all active selections. This list is kept sorted and non-overlapping.
156
    pub selections: SelectionVec,
157
    /// The DOM node this selection state applies to.
158
    pub node_id: DomNodeId,
159
}
160

            
161
impl SelectionState {
162
    /// Adds a new selection, merging it with any existing selections it overlaps with.
163
    pub fn add(&mut self, new_selection: Selection) {
164
        // A full implementation would handle merging overlapping ranges.
165
        // For now, we simply add and sort for simplicity.
166
        let mut selections: Vec<Selection> = self.selections.as_ref().to_vec();
167
        selections.push(new_selection);
168
        selections.sort_unstable();
169
        selections.dedup(); // Removes duplicate cursors
170
        self.selections = selections.into();
171
    }
172

            
173
}
174

            
175
impl_option!(
176
    SelectionState,
177
    OptionSelectionState,
178
    copy = false,
179
    clone = false,
180
    [Debug, Clone, PartialEq]
181
);
182

            
183
// ============================================================================
184
// MULTI-CURSOR SUPPORT (Sublime Text style)
185
// ============================================================================
186

            
187
/// Stable identifier for a cursor/selection within a MultiCursorState.
188
///
189
/// Uses a monotonic u64 counter (not UUID) so it is `Copy` and C-API friendly.
190
/// Each SelectionId is unique within the lifetime of the process.
191
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
192
#[repr(C)]
193
pub struct SelectionId {
194
    pub inner: u64,
195
}
196

            
197
impl SelectionId {
198
    /// Generate a new unique SelectionId.
199
294
    pub fn new() -> Self {
200
        static COUNTER: AtomicU64 = AtomicU64::new(1);
201
294
        SelectionId { inner: COUNTER.fetch_add(1, Ordering::Relaxed) }
202
294
    }
203
}
204

            
205
/// Note: `Default` generates a new unique ID (increments global counter),
206
/// rather than returning a zero/sentinel value.
207
impl Default for SelectionId {
208
    fn default() -> Self {
209
        Self::new()
210
    }
211
}
212

            
213
impl_option!(
214
    SelectionId,
215
    OptionSelectionId,
216
    [Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord]
217
);
218

            
219
impl_vec!(SelectionId, SelectionIdVec, SelectionIdVecDestructor, SelectionIdVecDestructorType, SelectionIdVecSlice, OptionSelectionId);
220
impl_vec_debug!(SelectionId, SelectionIdVec);
221
impl_vec_clone!(SelectionId, SelectionIdVec, SelectionIdVecDestructor);
222
impl_vec_partialeq!(SelectionId, SelectionIdVec);
223
impl_vec_partialord!(SelectionId, SelectionIdVec);
224

            
225
/// A selection (cursor or range) paired with a stable identity.
226
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
227
#[repr(C)]
228
pub struct IdentifiedSelection {
229
    pub id: SelectionId,
230
    pub selection: Selection,
231
}
232

            
233
impl_option!(
234
    IdentifiedSelection,
235
    OptionIdentifiedSelection,
236
    [Debug, Clone, Copy, PartialEq, Eq, Hash]
237
);
238

            
239
impl_vec!(IdentifiedSelection, IdentifiedSelectionVec, IdentifiedSelectionVecDestructor, IdentifiedSelectionVecDestructorType, IdentifiedSelectionVecSlice, OptionIdentifiedSelection);
240
impl_vec_debug!(IdentifiedSelection, IdentifiedSelectionVec);
241
impl_vec_clone!(IdentifiedSelection, IdentifiedSelectionVec, IdentifiedSelectionVecDestructor);
242
impl_vec_partialeq!(IdentifiedSelection, IdentifiedSelectionVec);
243

            
244
/// Multi-cursor state for a contenteditable element (Sublime Text style).
245
///
246
/// Replaces the split CursorManager + SelectionManager pattern for text editing.
247
/// Supports multiple simultaneous cursors/selections, each with a stable ID.
248
///
249
/// ## Invariants
250
///
251
/// - `selections` is sorted by position and non-overlapping.
252
/// - The **primary** selection is the last one added (highest index).
253
/// - After any mutation, `merge_overlapping()` is called to maintain invariants.
254
#[derive(Debug, Clone, PartialEq)]
255
pub struct MultiCursorState {
256
    /// Sorted by position, non-overlapping. Primary = last added (highest index).
257
    pub selections: Vec<IdentifiedSelection>,
258
    /// The DOM node this multi-cursor state applies to.
259
    pub node_id: DomNodeId,
260
    /// Stable key that survives DOM rebuilds (from `calculate_contenteditable_key`).
261
    pub contenteditable_key: u64,
262
}
263

            
264
impl MultiCursorState {
265
    /// Create a new MultiCursorState with a single cursor.
266
294
    pub fn new_with_cursor(cursor: TextCursor, node_id: DomNodeId, contenteditable_key: u64) -> Self {
267
294
        let id = SelectionId::new();
268
294
        Self {
269
294
            selections: vec![IdentifiedSelection {
270
294
                id,
271
294
                selection: Selection::Cursor(cursor),
272
294
            }],
273
294
            node_id,
274
294
            contenteditable_key,
275
294
        }
276
294
    }
277

            
278
    /// Add a cursor, merging if it overlaps with existing selections.
279
    /// Returns the SelectionId of the new (or merged) cursor.
280
    #[must_use]
281
    pub fn add_cursor(&mut self, cursor: TextCursor) -> SelectionId {
282
        let id = SelectionId::new();
283
        self.selections.push(IdentifiedSelection {
284
            id,
285
            selection: Selection::Cursor(cursor),
286
        });
287
        self.merge_overlapping();
288
        id
289
    }
290

            
291
    /// Add a selection range, merging if it overlaps.
292
    /// Returns the SelectionId of the new (or merged) selection.
293
    #[must_use]
294
    pub fn add_selection(&mut self, range: SelectionRange) -> SelectionId {
295
        let id = SelectionId::new();
296
        self.selections.push(IdentifiedSelection {
297
            id,
298
            selection: Selection::Range(range),
299
        });
300
        self.merge_overlapping();
301
        id
302
    }
303

            
304
    /// Remove a selection by its stable ID. Returns true if found and removed.
305
    #[must_use]
306
    pub fn remove_selection(&mut self, id: SelectionId) -> bool {
307
        let len_before = self.selections.len();
308
        self.selections.retain(|s| s.id != id);
309
        self.selections.len() < len_before
310
    }
311

            
312
    /// Get the primary selection (last added = highest index).
313
1855
    pub fn get_primary(&self) -> Option<&IdentifiedSelection> {
314
1855
        self.selections.last()
315
1855
    }
316

            
317
    /// Get a mutable reference to the primary selection.
318
    pub fn get_primary_mut(&mut self) -> Option<&mut IdentifiedSelection> {
319
        self.selections.last_mut()
320
    }
321

            
322
    /// Get the primary cursor position (for scroll-into-view, IME, etc.)
323
1400
    pub fn get_primary_cursor(&self) -> Option<TextCursor> {
324
1400
        self.get_primary().map(|s| match &s.selection {
325
1366
            Selection::Cursor(c) => *c,
326
            Selection::Range(r) => r.end,
327
1366
        })
328
1400
    }
329

            
330
    /// Convert to a Vec<Selection> for passing to `edit_text()`.
331
546
    pub fn to_selections(&self) -> Vec<Selection> {
332
546
        self.selections.iter().map(|s| s.selection).collect()
333
546
    }
334

            
335
    /// Update selections from the result of `edit_text()`.
336
    ///
337
    /// Preserves existing IDs where possible (by index), assigns new IDs for extras.
338
546
    pub fn update_from_edit_result(&mut self, new_selections: &[Selection]) {
339
546
        let old_ids: Vec<SelectionId> = self.selections.iter().map(|s| s.id).collect();
340
546
        self.selections.clear();
341
546
        for (i, sel) in new_selections.iter().enumerate() {
342
546
            let id = old_ids.get(i).copied().unwrap_or_else(SelectionId::new);
343
546
            self.selections.push(IdentifiedSelection {
344
546
                id,
345
546
                selection: *sel,
346
546
            });
347
546
        }
348
        // Don't merge here — edit_text already returns correct positions
349
546
    }
350

            
351
    /// Set all selections to a single cursor (e.g., on plain click without Ctrl).
352
    pub fn set_single_cursor(&mut self, cursor: TextCursor) {
353
        let id = if let Some(primary) = self.selections.last() {
354
            primary.id
355
        } else {
356
            SelectionId::new()
357
        };
358
        self.selections.clear();
359
        self.selections.push(IdentifiedSelection {
360
            id,
361
            selection: Selection::Cursor(cursor),
362
        });
363
    }
364

            
365
    /// Set all selections to a single range.
366
    pub fn set_single_range(&mut self, range: SelectionRange) {
367
        let id = if let Some(primary) = self.selections.last() {
368
            primary.id
369
        } else {
370
            SelectionId::new()
371
        };
372
        self.selections.clear();
373
        self.selections.push(IdentifiedSelection {
374
            id,
375
            selection: Selection::Range(range),
376
        });
377
    }
378

            
379
    /// Number of active cursors/selections.
380
    pub fn len(&self) -> usize {
381
        self.selections.len()
382
    }
383

            
384
    /// Whether there are no selections (should not normally happen).
385
    pub fn is_empty(&self) -> bool {
386
        self.selections.is_empty()
387
    }
388

            
389
    /// Sort selections by position and merge any that overlap.
390
    pub fn merge_overlapping(&mut self) {
391
        if self.selections.len() <= 1 {
392
            return;
393
        }
394

            
395
        // Sort by the start position of each selection
396
        self.selections.sort_by(|a, b| {
397
            let pos_a = selection_start_pos(&a.selection);
398
            let pos_b = selection_start_pos(&b.selection);
399
            pos_a.cmp(&pos_b)
400
        });
401

            
402
        // Merge overlapping: if selection[i+1] starts at or before selection[i] ends,
403
        // merge them into one range (keeping the later ID as it's more recent).
404
        let mut merged: Vec<IdentifiedSelection> = Vec::with_capacity(self.selections.len());
405
        for sel in self.selections.drain(..) {
406
            if let Some(last) = merged.last_mut() {
407
                let last_end = selection_end_pos(&last.selection);
408
                let cur_start = selection_start_pos(&sel.selection);
409
                if cur_start <= last_end {
410
                    // Overlap — merge into one range covering both
411
                    let new_start = selection_start_pos(&last.selection);
412
                    let cur_end = selection_end_pos(&sel.selection);
413
                    let new_end = if cur_end > last_end { cur_end } else { last_end };
414
                    if new_start == new_end {
415
                        last.selection = Selection::Cursor(new_start);
416
                    } else {
417
                        last.selection = Selection::Range(SelectionRange {
418
                            start: new_start,
419
                            end: new_end,
420
                        });
421
                    }
422
                    // Keep the newer ID (the one being merged in)
423
                    last.id = sel.id;
424
                    continue;
425
                }
426
            }
427
            merged.push(sel);
428
        }
429
        self.selections = merged;
430
    }
431

            
432
    /// Move all cursors using a movement function. Merges collisions afterward.
433
    ///
434
    /// `move_fn` takes a TextCursor and returns the new TextCursor after movement.
435
    /// If `extend_selection` is true, the anchor stays and only the focus moves,
436
    /// creating or extending a range.
437
    pub fn move_all_cursors(
438
        &mut self,
439
        extend_selection: bool,
440
        move_fn: impl Fn(&TextCursor) -> TextCursor,
441
    ) {
442
        for sel in self.selections.iter_mut() {
443
            match &sel.selection {
444
                Selection::Cursor(c) => {
445
                    let new_cursor = move_fn(c);
446
                    if extend_selection {
447
                        if *c != new_cursor {
448
                            sel.selection = Selection::Range(SelectionRange {
449
                                start: *c,
450
                                end: new_cursor,
451
                            });
452
                        }
453
                    } else {
454
                        sel.selection = Selection::Cursor(new_cursor);
455
                    }
456
                }
457
                Selection::Range(r) => {
458
                    if extend_selection {
459
                        let new_end = move_fn(&r.end);
460
                        if r.start == new_end {
461
                            sel.selection = Selection::Cursor(r.start);
462
                        } else {
463
                            sel.selection = Selection::Range(SelectionRange {
464
                                start: r.start,
465
                                end: new_end,
466
                            });
467
                        }
468
                    } else {
469
                        // Collapse to the moved end
470
                        let new_cursor = move_fn(&r.end);
471
                        sel.selection = Selection::Cursor(new_cursor);
472
                    }
473
                }
474
            }
475
        }
476
        self.merge_overlapping();
477
    }
478

            
479
    /// Remap the NodeId in `node_id` after DOM reconciliation.
480
    ///
481
    /// If the node was removed (not in the map), the multi-cursor state is cleared.
482
    pub fn remap_node_ids(
483
        &mut self,
484
        dom_id: DomId,
485
        node_id_map: &alloc::collections::BTreeMap<crate::dom::NodeId, crate::dom::NodeId>,
486
    ) {
487
        if self.node_id.dom != dom_id {
488
            return;
489
        }
490
        if let Some(old_node_id) = self.node_id.node.into_crate_internal() {
491
            if let Some(&new_node_id) = node_id_map.get(&old_node_id) {
492
                self.node_id.node = crate::styled_dom::NodeHierarchyItemId::from_crate_internal(Some(new_node_id));
493
            } else {
494
                // Node removed — clear selections
495
                self.selections.clear();
496
            }
497
        }
498
    }
499
}
500

            
501
/// Helper: get the start position of a Selection for sorting.
502
fn selection_start_pos(sel: &Selection) -> TextCursor {
503
    match sel {
504
        Selection::Cursor(c) => *c,
505
        Selection::Range(r) => {
506
            if r.start <= r.end { r.start } else { r.end }
507
        }
508
    }
509
}
510

            
511
/// Helper: get the end position of a Selection for merging.
512
fn selection_end_pos(sel: &Selection) -> TextCursor {
513
    match sel {
514
        Selection::Cursor(c) => *c,
515
        Selection::Range(r) => {
516
            if r.end >= r.start { r.end } else { r.start }
517
        }
518
    }
519
}
520

            
521
// ============================================================================
522
// MULTI-NODE SELECTION (Browser-style Anchor/Focus model)
523
// ============================================================================
524

            
525
/// The anchor point of a text selection - where the user started selecting.
526
///
527
/// This is the fixed point during a drag operation. It records:
528
/// - The IFC root node (where the `UnifiedLayout` lives)
529
/// - The exact cursor position within that layout
530
/// - The visual bounds of the anchor character (for logical rectangle calculations)
531
///
532
/// The anchor remains constant during a drag; only the focus moves.
533
#[derive(Debug, Clone, PartialEq)]
534
pub struct SelectionAnchor {
535
    /// The IFC root node ID where selection started.
536
    /// This is the node that has `inline_layout_result` (e.g., `<p>`, `<div>`).
537
    pub ifc_root_node_id: NodeId,
538
    
539
    /// The exact cursor position within the IFC's `UnifiedLayout`.
540
    pub cursor: TextCursor,
541
    
542
    /// Visual bounds of the anchor character in viewport coordinates.
543
    /// Used for computing the logical selection rectangle during multi-line/multi-node selection.
544
    pub char_bounds: LogicalRect,
545
    
546
    /// The mouse position when the selection started (viewport coordinates).
547
    pub mouse_position: LogicalPosition,
548
}
549

            
550
/// The focus point of a text selection - where the selection currently ends.
551
///
552
/// This is the movable point during a drag operation. It updates on every mouse move.
553
#[derive(Debug, Clone, PartialEq)]
554
pub struct SelectionFocus {
555
    /// The IFC root node ID where selection currently ends.
556
    /// May differ from anchor's IFC root during cross-node selection.
557
    pub ifc_root_node_id: NodeId,
558
    
559
    /// The exact cursor position within the IFC's `UnifiedLayout`.
560
    pub cursor: TextCursor,
561
    
562
    /// Current mouse position in viewport coordinates.
563
    pub mouse_position: LogicalPosition,
564
}
565

            
566
/// Complete selection state spanning potentially multiple DOM nodes.
567
///
568
/// This implements the W3C Selection API model with anchor/focus endpoints.
569
/// The selection can span multiple IFC roots (e.g., multiple `<p>` elements).
570
///
571
/// ## Storage Model
572
///
573
/// Uses `BTreeMap<NodeId, SelectionRange>` for O(log N) lookup during rendering.
574
/// The key is the **IFC root NodeId**, and the value is the `SelectionRange` for that IFC.
575
///
576
/// ## Example
577
///
578
/// ```text
579
/// <p id="1">Hello [World</p>     <- Anchor in IFC 1, partial selection
580
/// <p id="2">Complete line</p>    <- InBetween, fully selected
581
/// <p id="3">Partial] end</p>     <- Focus in IFC 3, partial selection
582
/// ```
583
#[derive(Debug, Clone, PartialEq)]
584
pub struct TextSelection {
585
    /// The DOM this selection belongs to.
586
    pub dom_id: DomId,
587
    
588
    /// The anchor point - where the selection started (fixed during drag).
589
    pub anchor: SelectionAnchor,
590
    
591
    /// The focus point - where the selection currently ends (moves during drag).
592
    pub focus: SelectionFocus,
593
    
594
    /// Map from IFC root NodeId to the SelectionRange for that IFC.
595
    /// This allows O(log N) lookup during rendering.
596
    ///
597
    /// The `SelectionRange` contains the actual `TextCursor` positions for that IFC,
598
    /// ready to be passed to `UnifiedLayout::get_selection_rects()`.
599
    pub affected_nodes: BTreeMap<NodeId, SelectionRange>,
600
    
601
    /// Indicates whether anchor comes before focus in document order.
602
    /// True = forward selection (left-to-right), False = backward selection.
603
    pub is_forward: bool,
604
}
605

            
606
impl TextSelection {
607
    /// Create a new collapsed selection (cursor) at the given position.
608
    pub fn new_collapsed(
609
        dom_id: DomId,
610
        ifc_root_node_id: NodeId,
611
        cursor: TextCursor,
612
        char_bounds: LogicalRect,
613
        mouse_position: LogicalPosition,
614
    ) -> Self {
615
        let anchor = SelectionAnchor {
616
            ifc_root_node_id,
617
            cursor,
618
            char_bounds,
619
            mouse_position,
620
        };
621
        
622
        let focus = SelectionFocus {
623
            ifc_root_node_id,
624
            cursor,
625
            mouse_position,
626
        };
627
        
628
        // For a collapsed selection, the anchor node has a zero-width range
629
        let mut affected_nodes = BTreeMap::new();
630
        affected_nodes.insert(ifc_root_node_id, SelectionRange {
631
            start: cursor,
632
            end: cursor,
633
        });
634
        
635
        TextSelection {
636
            dom_id,
637
            anchor,
638
            focus,
639
            affected_nodes,
640
            is_forward: true, // Direction doesn't matter for collapsed selection
641
        }
642
    }
643
    
644
    /// Check if this is a collapsed selection (cursor with no range).
645
    pub fn is_collapsed(&self) -> bool {
646
        self.anchor.ifc_root_node_id == self.focus.ifc_root_node_id
647
            && self.anchor.cursor == self.focus.cursor
648
    }
649
    
650
    /// Get the selection range for a specific IFC root node.
651
    /// Returns `None` if this node is not part of the selection.
652
    pub fn get_range_for_node(&self, ifc_root_node_id: &NodeId) -> Option<&SelectionRange> {
653
        self.affected_nodes.get(ifc_root_node_id)
654
    }
655

            
656
}
657

            
658
impl_option!(
659
    TextSelection,
660
    OptionTextSelection,
661
    copy = false,
662
    clone = false,
663
    [Debug, Clone, PartialEq]
664
);