pub struct MultiCursorState {
pub selections: Vec<IdentifiedSelection>,
pub primary_id: SelectionId,
pub node_id: DomNodeId,
pub contenteditable_key: u64,
}Expand description
Multi-cursor state for a contenteditable element (Sublime Text style).
Replaces the split CursorManager + SelectionManager pattern for text editing.
Supports multiple simultaneous cursors/selections, each with a stable ID.
§Invariants
selectionsis sorted by owner, then by position, and non-overlapping within one owner.- The primary selection is identified by the stable
primary_id, NOT by vector position:merge_overlapping()re-sortsselectionsby position, so “last index” is not the most-recently-added cursor. - After any mutation,
merge_overlapping()is called to maintain invariants.
§Who a selection belongs to (U3)
A selection is identified by an OWNER-SCOPED id: (owner, id). The engine
acts on the SelectionOwner::LOCAL set and on nothing else:
- the PRIMARY is always local -
get_primarynever answers with a peer’s selection, andprimary_idis re-pointed only at a local one; - the EDIT SET (
to_selections, what typing / Backspace / paste apply to) is the local set, andupdate_from_edit_resultwrites back to it alone, leaving every peer’s entry untouched; - a plain click (
set_single_cursor/set_single_range) collapses the LOCAL set to one and keeps the peers in view; - cursor movement (
move_all_cursors*) moves local carets only; - the platform’s idea of “the selection” (
selectedTextRange, the IME’s marked range, the Android selection bridge) is the local primary.
Peers’ selections are DISPLAY-ONLY SNAPSHOTS: they enter through
set_owner_selections, leave through remove_owner, and are painted in
their owner’s colour. They are not shifted by a local edit either - the
sync layer that carried the snapshot here is the one that knows how the
edit moved the peer’s caret, and it replaces the snapshot. Anything that
walks selections directly and means “what is the user doing” must go
through Self::local_selections; walking the whole list is right only
for painting.
Fields§
§selections: Vec<IdentifiedSelection>Sorted by position, non-overlapping. Primary is tracked via primary_id.
primary_id: SelectionIdStable ID of the primary selection (most recently added/set). Survives the
position sort in merge_overlapping, which would otherwise make the
vector’s last element (position-last) masquerade as the primary.
node_id: DomNodeIdThe DOM node this multi-cursor state applies to.
contenteditable_key: u64Stable key that survives DOM rebuilds (from calculate_contenteditable_key).
Implementations§
Source§impl MultiCursorState
impl MultiCursorState
Sourcepub fn new_with_cursor(
cursor: TextCursor,
node_id: DomNodeId,
contenteditable_key: u64,
) -> Self
pub fn new_with_cursor( cursor: TextCursor, node_id: DomNodeId, contenteditable_key: u64, ) -> Self
Create a new MultiCursorState with a single cursor.
Sourcepub fn add_cursor(&mut self, cursor: TextCursor) -> SelectionId
pub fn add_cursor(&mut self, cursor: TextCursor) -> SelectionId
Add a cursor, merging if it overlaps with existing selections.
Returns the SelectionId of the new (or merged) cursor.
Sourcepub fn add_selection(&mut self, range: SelectionRange) -> SelectionId
pub fn add_selection(&mut self, range: SelectionRange) -> SelectionId
Add a selection range, merging if it overlaps.
Returns the SelectionId of the new (or merged) selection.
Sourcepub fn remove_selection(&mut self, id: SelectionId) -> bool
pub fn remove_selection(&mut self, id: SelectionId) -> bool
Remove a selection by its stable ID. Returns true if found and removed.
Sourcepub fn local_selections(&self) -> impl Iterator<Item = &IdentifiedSelection>
pub fn local_selections(&self) -> impl Iterator<Item = &IdentifiedSelection>
The LOCAL participant’s selections - the ones the engine edits, moves and reports (U3). Peers’ snapshots are excluded.
Sourcepub fn local_selections_mut(
&mut self,
) -> impl Iterator<Item = &mut IdentifiedSelection>
pub fn local_selections_mut( &mut self, ) -> impl Iterator<Item = &mut IdentifiedSelection>
Mutable Self::local_selections.
Sourcepub fn local_len(&self) -> usize
pub fn local_len(&self) -> usize
How many carets the LOCAL user is typing into. This - not Self::len
- is the count a “one line per cursor” paste or a “multi-cursor mode” decision wants; a peer’s caret is not somewhere the local user types.
Sourcepub fn get_primary(&self) -> Option<&IdentifiedSelection>
pub fn get_primary(&self) -> Option<&IdentifiedSelection>
Get the primary selection (the most recently added/set, tracked by
primary_id — NOT the vector’s last element, which position-sorting
reorders). Falls back to the last LOCAL selection if primary_id was
somehow lost - never to a peer’s: the list is owner-sorted, so a plain
last() was a peer’s entry whenever one existed, and everything that
reads “the selection” (IME, the platform selection, copy) would have
been answering with someone else’s.
Sourcepub fn get_primary_mut(&mut self) -> Option<&mut IdentifiedSelection>
pub fn get_primary_mut(&mut self) -> Option<&mut IdentifiedSelection>
Get a mutable reference to the primary selection (see get_primary).
Sourcepub fn get_primary_cursor(&self) -> Option<TextCursor>
pub fn get_primary_cursor(&self) -> Option<TextCursor>
Get the primary cursor position (for scroll-into-view, IME, etc.)
Sourcepub fn to_selections(&self) -> Vec<Selection>
pub fn to_selections(&self) -> Vec<Selection>
The EDIT SET: the LOCAL selections, as a Vec<Selection> for
edit_text(). Peers’ carets are excluded (U3) - with them included, a
local keystroke was applied at every peer’s caret as well, because
multi-cursor editing inserts at every selection it is handed.
Sourcepub fn update_from_edit_result(&mut self, new_selections: &[Selection])
pub fn update_from_edit_result(&mut self, new_selections: &[Selection])
Update the LOCAL selections from the result of edit_text().
Preserves existing local IDs where possible (by index among the local
entries), assigns new IDs for extras. Peers’ entries are carried over
UNTOUCHED, owner and id included: rebuilding the whole list as local
used to absorb every peer into the local user after the first
keystroke. Their POSITIONS are then moved across the edit by
Self::shift_peers_across (U3-a), by the caller that knows the
text delta.
Sourcepub fn shift_peers_across(&mut self, changes: &[RunTextChange])
pub fn shift_peers_across(&mut self, changes: &[RunTextChange])
Move the PEERS’ selections across a change to the text (U3-a).
update_from_edit_result carries peers over verbatim because the edit
result knows nothing about them; the caller that knows what the edit
did to the text - one RunTextChange per changed run, all relative to
the OLD text - applies it here, and a peer’s caret moves with the text
it was anchored in, per the user’s ruling (see
RunTextChange::transform). Local selections are not touched: the
edit result already placed them. A peer RANGE has both ends moved and
may collapse when the change spans it.
The sync layer still owns the semantics of CONCURRENT edits; this is only the local user’s own change, which the peer will also receive.
Sourcepub fn shift_all_across(&mut self, changes: &[RunTextChange])
pub fn shift_all_across(&mut self, changes: &[RunTextChange])
Move EVERY selection, the local ones included, across a change to the text that nobody’s edit placed them for (U3-b): the app’s new generation carried different text for the node - a remote participant’s edit applied through the app’s model, an app-side rewrite - and each caret keeps its logical anchor in it.
Sourcepub fn shift_peers_across_diff(&mut self, diff: &RunTextDiff)
pub fn shift_peers_across_diff(&mut self, diff: &RunTextDiff)
shift_peers_across for a diff that may also have changed the run
count (U3-a-i).
Sourcepub fn shift_all_across_diff(&mut self, diff: &RunTextDiff)
pub fn shift_all_across_diff(&mut self, diff: &RunTextDiff)
shift_all_across for a diff that may also have changed the run
count (U3-a-i).
Sourcepub fn set_single_cursor(&mut self, cursor: TextCursor)
pub fn set_single_cursor(&mut self, cursor: TextCursor)
Collapse the LOCAL selections to a single cursor (a plain click without Ctrl). Peers’ selections stay: a click is not a message to them.
Sourcepub fn set_single_range(&mut self, range: SelectionRange)
pub fn set_single_range(&mut self, range: SelectionRange)
Collapse the LOCAL selections to a single range. Peers stay, as above.
Sourcepub const fn len(&self) -> usize
pub const fn len(&self) -> usize
Number of selections of EVERY owner - what is painted. For how many
carets the local user types into, see Self::local_len.
Sourcepub const fn is_empty(&self) -> bool
pub const fn is_empty(&self) -> bool
Whether there are no selections (should not normally happen).
Sourcepub fn merge_overlapping(&mut self)
pub fn merge_overlapping(&mut self)
Sort selections by position and merge any that overlap.
Sourcepub fn set_owner_selections(
&mut self,
owner: SelectionOwner,
selections: &[Selection],
) -> bool
pub fn set_owner_selections( &mut self, owner: SelectionOwner, selections: &[Selection], ) -> bool
Replace everything ONE participant owns (U1).
The injection point for a shared editing session: a peer’s cursor arrives over the network, and this makes it the whole of what that peer has selected. Replacing rather than merging is deliberate - a remote participant’s state is a SNAPSHOT, and adding to it would leave stale carets behind whenever a message was missed.
Refuses to touch SelectionOwner::LOCAL: the local caret is the
engine’s, and letting an app overwrite it through this door would make
every text-editing invariant the engine maintains someone else’s
problem. Returns false in that case.
Sourcepub fn remove_owner(&mut self, owner: SelectionOwner) -> usize
pub fn remove_owner(&mut self, owner: SelectionOwner) -> usize
Forget a participant - they left, or their connection dropped.
Returns how many selections went. LOCAL is refused for the same
reason as above; removing it would leave the document with no caret.
Sourcepub fn owners(&self) -> Vec<SelectionOwner>
pub fn owners(&self) -> Vec<SelectionOwner>
Every participant with a selection right now, LOCAL included.
Sourcepub fn move_all_cursors(
&mut self,
extend_selection: bool,
move_fn: impl Fn(&TextCursor) -> TextCursor,
)
pub fn move_all_cursors( &mut self, extend_selection: bool, move_fn: impl Fn(&TextCursor) -> TextCursor, )
Move all cursors using a movement function. Merges collisions afterward.
move_fn takes a TextCursor and returns the new TextCursor after movement.
If extend_selection is true, the anchor stays and only the focus moves,
creating or extending a range.
A bare (non-extending) move over an active range COLLAPSES to the range
boundary — the arrow-key rule. Use Self::move_all_cursors_with for
steps where that is wrong (Home/End, document jumps).
Sourcepub fn move_all_cursors_with(
&mut self,
extend_selection: bool,
collapse_range_to_boundary: bool,
move_fn: impl Fn(&TextCursor) -> TextCursor,
)
pub fn move_all_cursors_with( &mut self, extend_selection: bool, collapse_range_to_boundary: bool, move_fn: impl Fn(&TextCursor) -> TextCursor, )
Self::move_all_cursors, with control over what a bare move does to
an active range.
collapse_range_to_boundary is the arrow-key rule: Left/Right with a
selection put the caret on the selection’s edge and go no further.
Every OTHER step — Home/End, Ctrl+Home/End, a visual line, a word — is a
MOVEMENT and must be performed: collapsing them to the nearest edge is
how pressing End with text selected used to leave the caret sitting at
the end of the selection instead of the end of the line.
Trait Implementations§
Source§impl Clone for MultiCursorState
impl Clone for MultiCursorState
Source§fn clone(&self) -> MultiCursorState
fn clone(&self) -> MultiCursorState
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read more