teksilo_core/text_surface.rs
1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! `TextSurface` — "the widget the caret is in", as an application sees it.
5//!
6//! # Why the framework has to answer this
7//!
8//! An application that wants **one** Undo command — one chord, one menu row,
9//! routed to whatever the writer is actually editing — handles the
10//! [`edit.undo`](crate::shortcut::StandardEdit::Undo) intent. From the chord,
11//! the walk starts at the focused widget, so a text widget's own action
12//! answers before the application's. Two routes do not pass through the
13//! widget at all, and there the application owes it an answer: a menu row or
14//! a toolbar button, whose intent starts in the chrome, and a widget that
15//! refused its history (a forward-only writing mode), which passes the intent
16//! on rather than letting it reach the application's own history.
17//!
18//! It cannot produce one by itself. It can recognise the surfaces it built and
19//! kept a handle on, and it is blind to the rest: a rename box in a table cell,
20//! a search field, a text input inside a dialog it did not write. An application
21//! that guesses gets it exactly backwards — Ctrl+Z in a rename box undoes
22//! something else entirely, which is worse than not having the feature.
23//!
24//! Maintaining a list of every text widget in the application is not a fix: it
25//! is correct on the day it is written and wrong the first time someone adds a
26//! field. The framework already knows which widget has focus and which widgets
27//! edit text, so it is the only place the question can be answered *completely*.
28//!
29//! # What implementors get
30//!
31//! Every text widget registers itself with [`BuildContext::register_text_surface`],
32//! and the registration is torn down with the widget exactly as a global action
33//! is. A host then asks [`WidgetTree::focused_text_surface`] and either drives it
34//! or — knowing one exists — steps aside and lets the widget keep its own keys.
35//!
36//! [`BuildContext::register_text_surface`]: crate::build_context::BuildContext::register_text_surface
37//! [`WidgetTree::focused_text_surface`]: crate::widget_tree::WidgetTree::focused_text_surface
38
39use std::cell::RefCell;
40use std::rc::Rc;
41
42use crate::signal::Signal;
43use crate::widget::EventContext;
44use crate::widget_id::WidgetId;
45
46/// A widget that edits text, seen through the commands a host may need to
47/// invoke on it from outside — a menu row, a routed shortcut, an assistive
48/// technology.
49///
50/// Object-safe on purpose: a host holds `Rc<dyn TextSurface>` for whichever
51/// widget has focus, without knowing which kind it is.
52pub trait TextSurface {
53 /// Is there anything in this surface's own history to step back through?
54 fn can_undo(&self) -> bool;
55 /// Is there anything to step forward into?
56 fn can_redo(&self) -> bool;
57 fn undo(&self);
58 fn redo(&self);
59
60 /// Is this surface refusing to step through its history at all?
61 ///
62 /// Distinct from having nothing to undo, and a host must treat them
63 /// differently: an empty history may be a reason to look elsewhere, a
64 /// refusal is not. Applications impose modes — a "forbid erasing" writing
65 /// game, a read-only review pass — and a routed Undo that quietly went
66 /// somewhere else would defeat them.
67 fn history_frozen(&self) -> bool {
68 false
69 }
70
71 /// Is any text selected right now?
72 fn has_selection(&self) -> bool;
73 /// Does this surface refuse edits? Cut and Paste are meaningless when it does.
74 fn is_read_only(&self) -> bool;
75 /// May its contents be copied at all? A password field says no.
76 fn allows_copy(&self) -> bool;
77
78 fn cut(&self, ctx: &EventContext<'_>);
79 fn copy(&self, ctx: &EventContext<'_>);
80 fn paste(&self, ctx: &EventContext<'_>);
81 /// Paste stripped of formatting. A surface with no formatting to strip
82 /// should do a plain paste rather than nothing.
83 fn paste_plain(&self, ctx: &EventContext<'_>);
84 fn select_all(&self);
85}
86
87/// A cloneable view of one tree's registered text surfaces.
88///
89/// Taken once, with [`BuildContext::text_surfaces`], and held by whatever needs
90/// to ask the question later — a view-model refreshed from a frame tick has no
91/// `&WidgetTree` to consult, which is precisely when it needs the answer.
92///
93/// [`BuildContext::text_surfaces`]: crate::build_context::BuildContext::text_surfaces
94#[derive(Clone)]
95pub struct TextSurfaces {
96 entries: Rc<RefCell<Vec<(WidgetId, Rc<dyn TextSurface>)>>>,
97 focused: Signal<Option<WidgetId>>,
98}
99
100impl std::fmt::Debug for TextSurfaces {
101 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102 f.debug_struct("TextSurfaces")
103 .field("registered", &self.entries.borrow().len())
104 .field("focused_is_text", &self.focused_is_text_surface())
105 .finish()
106 }
107}
108
109impl TextSurfaces {
110 pub(crate) fn new(focused: Signal<Option<WidgetId>>) -> Self {
111 Self {
112 entries: Rc::new(RefCell::new(Vec::new())),
113 focused,
114 }
115 }
116
117 /// Record that `owner` edits text, replacing any previous registration from
118 /// the same widget so a rebuild re-points rather than accumulating.
119 pub(crate) fn insert(&self, owner: WidgetId, surface: Rc<dyn TextSurface>) {
120 let mut entries = self.entries.borrow_mut();
121 entries.retain(|(id, _)| *id != owner);
122 entries.push((owner, surface));
123 }
124
125 /// Forget `owner`'s registration — on its rebuild or destroy.
126 pub(crate) fn remove(&self, owner: WidgetId) {
127 self.entries.borrow_mut().retain(|(id, _)| *id != owner);
128 }
129
130 /// The text-editing widget that currently holds the keyboard focus.
131 ///
132 /// `None` when focus is elsewhere — or nowhere — which is exactly what a
133 /// host needs in order to know that a text chord is safe to route itself.
134 pub fn focused(&self) -> Option<Rc<dyn TextSurface>> {
135 let focused = self.focused.get()?;
136 self.entries
137 .borrow()
138 .iter()
139 .find(|(id, _)| *id == focused)
140 .map(|(_, s)| Rc::clone(s))
141 }
142
143 /// Is the keyboard focus inside a widget that edits text? The cheap half of
144 /// [`focused`](Self::focused), for a host that only needs to decide whether
145 /// to step aside.
146 pub fn focused_is_text_surface(&self) -> bool {
147 self.focused().is_some()
148 }
149
150 /// Does the widget registered under `owner` refuse edits?
151 ///
152 /// `false` for a widget that edits nothing — the question is only asked of
153 /// text surfaces, and a node that is not one is not read-only, it is simply
154 /// not text. Read by the hit-test slop pass, which must never re-attribute
155 /// a press to a surface that would ignore it.
156 pub fn is_read_only(&self, owner: WidgetId) -> bool {
157 self.entries
158 .borrow()
159 .iter()
160 .find(|(id, _)| *id == owner)
161 .map(|(_, s)| s.is_read_only())
162 .unwrap_or(false)
163 }
164
165 /// The signal to react to. A host mirroring "can undo" into a menu row
166 /// re-reads when focus moves.
167 pub fn focus_signal(&self) -> Signal<Option<WidgetId>> {
168 self.focused.clone()
169 }
170}