Skip to main content

frus_widgets/
editing.rs

1//! [`TextEditingController`]: a text field's text, held outside the field.
2//!
3//! A field draws the text it is given; where that text lives is up to whoever built it.
4//! A controller is that place, made once and handed to the field: what the person types
5//! lands in it, and what the program puts in it appears in the field. Reading it is
6//! reading what the field says, and writing it is writing to the field.
7//!
8//! ```
9//! use frus_widgets::{TextEditingController, TextField};
10//!
11//! let name = TextEditingController::with_text("Ada");
12//! let field: TextField = TextField::new("").label("Name").controller(&name);
13//! # let _ = field;
14//!
15//! assert_eq!(name.text(), "Ada");
16//! name.set_text("Ada Lovelace");
17//! assert_eq!(name.text(), "Ada Lovelace");
18//! name.clear();
19//! assert!(name.is_empty());
20//! ```
21//!
22//! It is made once and kept: in a [`State`](crate::State), or with
23//! [`BuildContext::use_text_controller`].
24//!
25//! ```
26//! use frus_widgets::{button, column, BuildContext, TextField, Widget};
27//!
28//! fn form(cx: &BuildContext) -> Box<dyn Widget> {
29//!     let email = cx.use_text_controller("");
30//!     let submit = email.clone();
31//!     Box::new(column![
32//!         TextField::new("").label("Email").controller(&email),
33//!         button("Send", move || println!("sending to {}", submit.text())),
34//!     ])
35//! }
36//! ```
37//!
38//! A controller is a [`Listenable`]: `add_listener` tells you whenever the text changes,
39//! whether the person typed or the program did.
40
41use crate::component::BuildContext;
42use crate::notifier::{Listenable, Subscription, ValueNotifier};
43
44/// A text field's text, and the changes to it.
45///
46/// Cloning gives another handle to the same text.
47#[derive(Clone, Default)]
48pub struct TextEditingController {
49    text: ValueNotifier<String>,
50}
51
52impl TextEditingController {
53    /// A controller holding no text.
54    pub fn new() -> Self {
55        Self::default()
56    }
57
58    /// A controller that starts out holding `text`.
59    pub fn with_text(text: impl Into<String>) -> Self {
60        Self {
61            text: ValueNotifier::new(text.into()),
62        }
63    }
64
65    /// The text.
66    pub fn text(&self) -> String {
67        self.text.get()
68    }
69
70    /// Reads the text without copying it.
71    pub fn with_text_ref<R>(&self, read: impl FnOnce(&str) -> R) -> R {
72        self.text.with(|text| read(text))
73    }
74
75    /// Replaces the text; a field driven by this controller shows it at once. Changing it to
76    /// what it already is tells nobody.
77    pub fn set_text(&self, text: impl Into<String>) {
78        self.text.set(text.into());
79    }
80
81    /// Empties the text.
82    pub fn clear(&self) {
83        self.set_text("");
84    }
85
86    /// Whether the text is empty.
87    pub fn is_empty(&self) -> bool {
88        self.text.with(|text| text.is_empty())
89    }
90
91    /// How many characters the text has.
92    pub fn len(&self) -> usize {
93        self.text.with(|text| text.chars().count())
94    }
95}
96
97impl Listenable for TextEditingController {
98    fn add_listener(&self, listener: impl Fn() + 'static) -> Subscription {
99        self.text.add_listener(listener)
100    }
101}
102
103impl std::fmt::Debug for TextEditingController {
104    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105        f.debug_tuple("TextEditingController")
106            .field(&self.text())
107            .finish()
108    }
109}
110
111impl BuildContext<'_> {
112    /// A [`TextEditingController`] that lives as long as this component does, holding
113    /// `initial` to begin with. The same controller comes back on every rebuild.
114    pub fn use_text_controller(&self, initial: &str) -> TextEditingController {
115        self.use_ref(|| TextEditingController::with_text(initial))
116            .get()
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123    use crate::{build_deferred, Callback, Runtime, TextField, Theme, Widget};
124    use std::cell::RefCell;
125    use std::rc::Rc;
126
127    #[test]
128    fn a_controller_holds_and_changes_its_text() {
129        let c = TextEditingController::with_text("ab");
130        assert_eq!(
131            (c.text(), c.len(), c.is_empty()),
132            ("ab".to_string(), 2, false)
133        );
134        c.set_text("héllo");
135        assert_eq!(c.len(), 5, "characters, not bytes");
136        c.clear();
137        assert!(c.is_empty());
138        assert!(c.with_text_ref(|t| t.is_empty()));
139    }
140
141    #[test]
142    fn a_clone_is_the_same_text() {
143        let a = TextEditingController::new();
144        let b = a.clone();
145        a.set_text("x");
146        assert_eq!(b.text(), "x");
147    }
148
149    #[test]
150    fn a_listener_hears_every_change_and_only_changes() {
151        let c = TextEditingController::new();
152        let heard = Rc::new(RefCell::new(Vec::new()));
153        let log = heard.clone();
154        let probe = c.clone();
155        let _l = c.add_listener(move || log.borrow_mut().push(probe.text()));
156        c.set_text("a");
157        c.set_text("a");
158        c.set_text("ab");
159        assert_eq!(*heard.borrow(), ["a", "ab"]);
160    }
161
162    /// A field is driven by its controller: it shows the controller's text, and an edit
163    /// writes to it — when the message is delivered, once.
164    #[test]
165    fn a_field_shows_its_controller_and_writes_back_to_it() {
166        let c = TextEditingController::with_text("start");
167        let field: TextField = TextField::new("ignored").controller(&c);
168        assert_eq!(Widget::text_value(&field), Some("start"));
169
170        let message: Callback = Widget::replace_value(&field, "typed".into()).expect("editable");
171        assert_eq!(c.text(), "start", "asking for the message changes nothing");
172        message.call();
173        assert_eq!(c.text(), "typed");
174
175        c.set_text("set from code");
176        let field: TextField = TextField::new("").controller(&c);
177        assert_eq!(
178            Widget::text_value(&field),
179            Some("set from code"),
180            "and what the program writes is what the next build shows"
181        );
182    }
183
184    #[test]
185    fn a_hook_gives_the_same_controller_on_every_build() {
186        let runtime = Runtime::default();
187        let seen: Rc<RefCell<Vec<TextEditingController>>> = Rc::default();
188        type Builder = Rc<dyn Fn(&BuildContext) -> Box<dyn Widget>>;
189        let build: Builder = {
190            let seen = seen.clone();
191            Rc::new(move |cx| {
192                let c = cx.use_text_controller("hello");
193                seen.borrow_mut().push(c.clone());
194                Box::new(TextField::new("").controller(&c))
195            })
196        };
197        let make = || {
198            let build = build.clone();
199            crate::Component::stateless(move |cx: &BuildContext| build(cx))
200        };
201        for _ in 0..2 {
202            runtime.states.begin_build();
203            build_deferred(&make(), &Theme::default(), &runtime);
204            runtime.states.end_frame();
205            seen.borrow()[0].set_text("changed");
206        }
207        let seen = seen.borrow();
208        assert_eq!(seen.len(), 2);
209        assert_eq!(
210            seen[1].text(),
211            "changed",
212            "the same text, kept between builds"
213        );
214    }
215}