Skip to main content

lumen_core/
text_events.rs

1//! W3.2: text-editing message bus.
2//!
3//! Every text mutation flows through [`TextEditRequest`]. The single
4//! [`lumen_text_edit::text_apply_edits`] system drains the bus and is the
5//! only system that mutates [`crate::text_model::TextBuffer`].
6//!
7//! Producers (`route_ime_events`, `type_into_focused`, pointer drag,
8//! script `set_text(id, ...)`, paste) emit `TextEditRequest`; consumers
9//! react to the post-mutation [`TextEditApplied`] event.
10
11use bevy_ecs::message::Message;
12use bevy_ecs::prelude::{Entity, SystemSet};
13use std::ops::Range;
14use std::sync::Arc;
15
16use crate::text_model::TextPos;
17
18/// Cross-crate [`SystemSet`] labels for the text-editing pipeline.
19///
20/// `lumen-input` tags its request producers (`type_into_focused`,
21/// `route_ime_events`, `text_pointer_to_caret`, `text_pointer_drag_select`,
22/// `cycle_focus_on_tab`) with [`Self::Producers`];
23/// `lumen_text_edit::TextEditPlugin` schedules the single mutator
24/// [`Self::Apply`] after that set and the content mirror [`Self::Mirror`]
25/// after the mutator. Anchoring the edges on shared set labels (rather
26/// than function references) keeps the two crates decoupled: either
27/// plugin can be installed alone and the `.after(set)` edges are inert
28/// against an empty set.
29#[derive(SystemSet, Clone, Copy, Debug, Hash, PartialEq, Eq)]
30pub enum TextEditSet {
31    /// Systems that emit [`TextEditRequest`] or mutate the legacy
32    /// `TextContent` / `TextInput` pair directly.
33    Producers,
34    /// The single mutator (`text_apply_edits`).
35    Apply,
36    /// Post-mutation mirroring back into `TextContent` / `TextInput`.
37    Mirror,
38}
39
40/// Symbolic anchor inside an edit request. Resolved against the live
41/// [`crate::text_model::TextCursor`] / [`crate::text_model::TextBuffer`]
42/// inside the mutator.
43#[derive(Clone, Copy, Debug)]
44pub enum Anchor {
45    /// Resolve to the cursor head.
46    Cursor,
47    /// Explicit position.
48    Position(TextPos),
49    /// Resolve to the lower end of the selection (== cursor head when
50    /// no selection).
51    SelectionStart,
52    /// Resolve to the upper end of the selection.
53    SelectionEnd,
54    /// Buffer start.
55    DocumentStart,
56    /// Buffer end.
57    DocumentEnd,
58}
59
60/// Single-axis cursor motion (mirrors `QTextCursor::movePosition` + GTK
61/// `GtkMovementStep`).
62#[derive(Clone, Copy, Debug)]
63pub enum CursorMotion {
64    /// One extended grapheme cluster left.
65    CharLeft,
66    /// One extended grapheme cluster right.
67    CharRight,
68    /// Previous word boundary.
69    WordLeft,
70    /// Next word boundary.
71    WordRight,
72    /// Start of current line.
73    LineStart,
74    /// End of current line.
75    LineEnd,
76    /// Up one visual line (multi-line buffers).
77    LineUp,
78    /// Down one visual line.
79    LineDown,
80    /// Start of document.
81    DocStart,
82    /// End of document.
83    DocEnd,
84}
85
86/// Selection modifier for [`CursorMotion`] requests.
87#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
88pub enum MoveMode {
89    /// Anchor follows head (collapses selection).
90    #[default]
91    MoveAnchor,
92    /// Anchor stays put (extends selection).
93    KeepAnchor,
94}
95
96/// One text-editing request addressed to `entity`'s [`crate::text_model::TextBuffer`].
97///
98/// Producers write into a [`bevy_ecs::message::Messages`] queue; the
99/// single [`lumen_text_edit::text_apply_edits`] system drains them.
100#[derive(Message, Clone, Debug)]
101pub enum TextEditRequest {
102    /// Insert `text` at `at`.
103    Insert {
104        /// Target editable.
105        entity: Entity,
106        /// Resolved insertion position.
107        at: TextPos,
108        /// Inserted bytes (Arc'd so producers don't always allocate).
109        text: Arc<str>,
110    },
111    /// Delete the byte range.
112    Delete {
113        /// Target editable.
114        entity: Entity,
115        /// Range to delete, in bytes.
116        range: Range<usize>,
117    },
118    /// Replace `range` with `text` (IME's `replacementStart/Length`).
119    Replace {
120        /// Target editable.
121        entity: Entity,
122        /// Range to replace, in bytes.
123        range: Range<usize>,
124        /// Replacement bytes.
125        text: Arc<str>,
126    },
127    /// Move the cursor.
128    MoveCursor {
129        /// Target editable.
130        entity: Entity,
131        /// Motion axis.
132        motion: CursorMotion,
133        /// Selection modifier.
134        mode: MoveMode,
135    },
136    /// Set selection range (in bytes).
137    Select {
138        /// Target editable.
139        entity: Entity,
140        /// Range of bytes to select.
141        range: Range<usize>,
142    },
143    /// Set cursor to an explicit position (collapses selection).
144    SetCursor {
145        /// Target editable.
146        entity: Entity,
147        /// New cursor position.
148        pos: TextPos,
149    },
150    /// Move the selection head to `pos` while keeping the current anchor
151    /// (Shift+click, pointer drag). Unlike [`Self::Select`] the anchor
152    /// side is preserved, so repeated extends pivot around the same
153    /// fixed end regardless of direction.
154    ExtendSelection {
155        /// Target editable.
156        entity: Entity,
157        /// New selection head.
158        pos: TextPos,
159    },
160    /// Select all.
161    SelectAll {
162        /// Target editable.
163        entity: Entity,
164    },
165    /// Pop one entry off the undo stack.
166    Undo {
167        /// Target editable.
168        entity: Entity,
169    },
170    /// Re-apply the next redo entry.
171    Redo {
172        /// Target editable.
173        entity: Entity,
174    },
175    /// Begin an IME composition (no-op if already active).
176    ImeBegin {
177        /// Target editable.
178        entity: Entity,
179    },
180    /// Update the IME preedit.
181    ImeUpdate {
182        /// Target editable.
183        entity: Entity,
184        /// New preedit string.
185        text: Arc<str>,
186        /// Caret byte offset inside `text`.
187        caret_in_preedit: usize,
188    },
189    /// Commit the IME preedit; replaces `replace_range` with `text` if
190    /// `replace_range` is `Some`, otherwise inserts `text` at the cursor.
191    ImeCommit {
192        /// Target editable.
193        entity: Entity,
194        /// Final text.
195        text: Arc<str>,
196        /// Optional IME-requested replacement range (W3.5).
197        replace_range: Option<Range<usize>>,
198    },
199    /// Cancel any active IME preedit without committing.
200    ImeCancel {
201        /// Target editable.
202        entity: Entity,
203    },
204}
205
206impl TextEditRequest {
207    /// The target entity of this request.
208    pub fn entity(&self) -> Entity {
209        match self {
210            TextEditRequest::Insert { entity, .. }
211            | TextEditRequest::Delete { entity, .. }
212            | TextEditRequest::Replace { entity, .. }
213            | TextEditRequest::MoveCursor { entity, .. }
214            | TextEditRequest::Select { entity, .. }
215            | TextEditRequest::SetCursor { entity, .. }
216            | TextEditRequest::ExtendSelection { entity, .. }
217            | TextEditRequest::SelectAll { entity }
218            | TextEditRequest::Undo { entity }
219            | TextEditRequest::Redo { entity }
220            | TextEditRequest::ImeBegin { entity }
221            | TextEditRequest::ImeUpdate { entity, .. }
222            | TextEditRequest::ImeCommit { entity, .. }
223            | TextEditRequest::ImeCancel { entity } => *entity,
224        }
225    }
226}
227
228/// Classification of an applied edit for downstream observers (binding
229/// push, validators, undo coalescing).
230#[derive(Clone, Copy, Debug, PartialEq, Eq)]
231pub enum AppliedKind {
232    /// User-driven insertion (a `Key::Character` arm, IME commit, paste).
233    Insert,
234    /// User-driven deletion (Backspace, Delete, selection-replace).
235    Delete,
236    /// Replacement (IME commit with replace_range, paste-over-selection).
237    Replace,
238    /// Pure cursor / selection move; no text mutation.
239    CursorMove,
240    /// Undo / Redo replay.
241    UndoRedo,
242}
243
244/// Emitted by [`lumen_text_edit::text_apply_edits`] after every successful
245/// mutation. Replaces ad-hoc `Changed<TextContent>` snooping so signal
246/// binding / validators / undo coalescing react only to real edits.
247#[derive(Message, Clone, Debug)]
248pub struct TextEditApplied {
249    /// Mutated editable.
250    pub entity: Entity,
251    /// Buffer version after the edit.
252    pub version: u64,
253    /// What kind of edit.
254    pub kind: AppliedKind,
255    /// Cursor byte position before the edit (for undo).
256    pub before_byte: usize,
257    /// Cursor byte position after the edit.
258    pub after_byte: usize,
259}
260
261/// Reasons an edit might be rejected by validators / single-line guards.
262#[derive(Clone, Copy, Debug, PartialEq, Eq)]
263pub enum RejectReason {
264    /// Single-line buffer received `\n`.
265    NewlineInSingleLine,
266    /// Validator rejected.
267    Validator,
268    /// Undo stack empty.
269    NothingToUndo,
270    /// Redo stack empty.
271    NothingToRedo,
272    /// Target entity missing required components.
273    BadTarget,
274}
275
276/// Emitted when [`lumen_text_edit::text_apply_edits`] drops a request.
277#[derive(Message, Clone, Debug)]
278pub struct TextEditRejected {
279    /// Target.
280    pub entity: Entity,
281    /// Why.
282    pub reason: RejectReason,
283}
284
285/// Backend -> core: the OS IME asked for surrounding text (W3.5).
286/// `text_update_surrounding_response` replies with [`ImeSurroundingResponse`].
287#[derive(Message, Clone, Copy, Debug)]
288pub struct ImeSurroundingRequested {
289    /// Target editable (typically the focused entity).
290    pub entity: Entity,
291}
292
293/// Core -> backend: surrounding-text reply. Backend forwards to the OS
294/// IME (Wayland text-input-v3 / IBus).
295#[derive(Message, Clone, Debug)]
296pub struct ImeSurroundingResponse {
297    /// Target editable.
298    pub entity: Entity,
299    /// Snapshot of the buffer text.
300    pub text: Arc<str>,
301    /// Selection anchor byte offset (== cursor when no selection).
302    pub anchor_byte: usize,
303    /// Cursor byte offset.
304    pub cursor_byte: usize,
305}