Skip to main content

ravel_web/
text.rs

1//! Text nodes and formatting.
2
3use std::{
4    borrow::Cow,
5    fmt::{Arguments, Write},
6};
7
8use ravel::{Builder, State};
9use web_sys::wasm_bindgen::UnwrapThrowExt;
10
11use crate::{BuildCx, RebuildCx, ViewMarker, Web};
12
13/// A text node.
14pub struct Text<Value: ToString + AsRef<str>> {
15    value: Value,
16}
17
18impl<Value: ToString + AsRef<str>> Builder<Web> for Text<Value> {
19    type State = TextState<String>;
20
21    fn build(self, cx: BuildCx) -> Self::State {
22        let node =
23            web_sys::Text::new_with_data(self.value.as_ref()).unwrap_throw();
24
25        cx.position.insert(&node);
26
27        TextState {
28            node,
29            value: self.value.to_string(),
30        }
31    }
32
33    fn rebuild(self, _: RebuildCx, state: &mut Self::State) {
34        if state.value != self.value.as_ref() {
35            state.node.set_data(self.value.as_ref());
36            state.value = self.value.to_string();
37        }
38    }
39}
40
41/// The state of a [`Text`].
42pub struct TextState<Value> {
43    node: web_sys::Text,
44    value: Value,
45}
46
47impl<Output, Value: 'static> State<Output> for TextState<Value> {
48    fn run(&mut self, _: &mut Output) {}
49}
50
51impl<Value> ViewMarker for TextState<Value> {}
52
53/// A text node.
54pub fn text<V: ToString + AsRef<str>>(value: V) -> Text<V> {
55    Text { value }
56}
57
58impl Builder<Web> for &'static str {
59    type State = TextState<Self>;
60
61    fn build(self, cx: BuildCx) -> Self::State {
62        let node = web_sys::Text::new_with_data(self).unwrap_throw();
63
64        cx.position.insert(&node);
65
66        TextState { node, value: self }
67    }
68
69    fn rebuild(self, _: RebuildCx, state: &mut Self::State) {
70        if !std::ptr::eq(self, state.value) {
71            state.node.set_data(self);
72            state.value = self;
73        }
74    }
75}
76
77macro_rules! make_builder_web_to_string {
78    ($t:ty) => {
79        impl Builder<Web> for $t {
80            type State = TextState<Self>;
81
82            fn build(self, cx: BuildCx) -> Self::State {
83                let data = self.to_string();
84
85                let node = web_sys::Text::new_with_data(&data).unwrap_throw();
86                cx.position.insert(&node);
87
88                TextState { node, value: self }
89            }
90
91            fn rebuild(self, _: RebuildCx, state: &mut Self::State) {
92                if self == state.value {
93                    return;
94                }
95
96                state.node.set_data(&self.to_string());
97                state.value = self.clone();
98            }
99        }
100    };
101}
102
103make_builder_web_to_string!(char);
104make_builder_web_to_string!(f32);
105make_builder_web_to_string!(f64);
106make_builder_web_to_string!(i128);
107make_builder_web_to_string!(i16);
108make_builder_web_to_string!(i32);
109make_builder_web_to_string!(i64);
110make_builder_web_to_string!(i8);
111make_builder_web_to_string!(isize);
112make_builder_web_to_string!(u128);
113make_builder_web_to_string!(u16);
114make_builder_web_to_string!(u32);
115make_builder_web_to_string!(u64);
116make_builder_web_to_string!(u8);
117make_builder_web_to_string!(usize);
118
119/// Displays a value, updating when not equal to the previous value.
120pub struct Display<T: ToString + PartialEq + Clone> {
121    value: T,
122}
123
124impl<T: 'static + ToString + PartialEq + Clone> Builder<Web> for Display<T> {
125    type State = DisplayState<T>;
126
127    fn build(self, cx: BuildCx<'_>) -> Self::State {
128        let data = self.value.to_string();
129
130        let node = web_sys::Text::new_with_data(&data).unwrap_throw();
131        cx.position.insert(&node);
132
133        DisplayState {
134            node,
135            value: self.value.clone(),
136        }
137    }
138
139    fn rebuild(self, _: RebuildCx<'_>, state: &mut Self::State) {
140        if self.value == state.value {
141            return;
142        }
143
144        state.node.set_data(&self.value.to_string());
145        state.value = self.value.clone();
146    }
147}
148
149/// Displays a borrowed value, updating when not equal to the previous value.
150pub struct DisplayRef<'a, T: ToString + PartialEq + Clone> {
151    value: &'a T,
152}
153
154impl<'a, T: 'static + ToString + PartialEq + Clone> Builder<Web>
155    for DisplayRef<'a, T>
156{
157    type State = DisplayState<T>;
158
159    fn build(self, cx: BuildCx<'_>) -> Self::State {
160        let data = self.value.to_string();
161
162        let node = web_sys::Text::new_with_data(&data).unwrap_throw();
163        cx.position.insert(&node);
164
165        DisplayState {
166            node,
167            value: self.value.clone(),
168        }
169    }
170
171    fn rebuild(self, _: RebuildCx<'_>, state: &mut Self::State) {
172        if *self.value == state.value {
173            return;
174        }
175
176        state.node.set_data(&self.value.to_string());
177        state.value = self.value.clone();
178    }
179}
180
181/// The state for a [`Display`].
182pub struct DisplayState<T: ToString + PartialEq> {
183    node: web_sys::Text,
184    value: T,
185}
186
187impl<T: 'static + ToString + PartialEq, Output> State<Output>
188    for DisplayState<T>
189{
190    fn run(&mut self, _: &mut Output) {}
191}
192
193impl<T: ToString + PartialEq> ViewMarker for DisplayState<T> {}
194
195/// Displays a value, updating when not equal to the previous value.
196pub fn display<T: ToString + PartialEq + Clone>(value: T) -> Display<T> {
197    Display { value }
198}
199
200/// Displays a borrowed value, updating when not equal to the previous value.
201pub fn display_ref<T: ToString + PartialEq + Clone>(
202    value: &T,
203) -> DisplayRef<'_, T> {
204    DisplayRef { value }
205}
206
207impl<'a> Builder<Web> for Arguments<'a> {
208    type State = TextState<Cow<'static, str>>;
209
210    fn build(self, cx: BuildCx) -> Self::State {
211        let value = match self.as_str() {
212            Some(s) => Cow::Borrowed(s),
213            None => Cow::Owned(self.to_string()),
214        };
215
216        let node = web_sys::Text::new_with_data(&value).unwrap_throw();
217
218        cx.position.insert(&node);
219
220        TextState { node, value }
221    }
222
223    fn rebuild(self, _: RebuildCx, state: &mut Self::State) {
224        match self.as_str() {
225            Some(new) => {
226                if !match &state.value {
227                    Cow::Borrowed(old) => std::ptr::eq(new, *old),
228                    Cow::Owned(old) => new == old,
229                } {
230                    state.node.set_data(new);
231                    state.value = Cow::Borrowed(new);
232                }
233            }
234            None => match &mut state.value {
235                Cow::Borrowed(_) => {
236                    let new = self.to_string();
237                    state.node.set_data(&new);
238                    state.value = Cow::Owned(new);
239                }
240                Cow::Owned(value) => {
241                    let mut w = UpdateString {
242                        value,
243                        index: 0,
244                        changed: false,
245                    };
246
247                    std::fmt::write(&mut w, self).unwrap_throw();
248
249                    if w.changed {
250                        state.node.set_data(value);
251                    }
252                }
253            },
254        }
255    }
256}
257
258struct UpdateString<'a> {
259    value: &'a mut String,
260    index: usize,
261    changed: bool,
262}
263
264impl<'a> Write for UpdateString<'a> {
265    fn write_str(&mut self, s: &str) -> std::fmt::Result {
266        let remaining = &self.value[self.index..];
267
268        if remaining.strip_prefix(s).is_none() {
269            self.value.truncate(self.index);
270            self.value.push_str(s);
271            self.changed = true;
272        }
273        self.index += s.len();
274
275        Ok(())
276    }
277}
278
279#[doc(hidden)]
280pub mod reexport {
281    pub use ravel::with;
282}
283
284/// Displays text with a format string.
285///
286/// Once [rust-lang/rust#92698](https://github.com/rust-lang/rust/issues/92698)
287/// is fixed, it will be possible to use [`format_args`] directly.
288#[macro_export]
289macro_rules! format_text {
290    ($fmt:expr) => {
291        $crate::text::reexport::with(move |cx| cx.build(::std::format_args!($fmt)))
292    };
293    ($fmt:expr, $($args:tt)*) => {
294        $crate::text::reexport::with(move |cx| cx.build(::std::format_args!($fmt, $($args)*)))
295    };
296}