Skip to main content

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