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}