Skip to main content

kas_widgets/
text.rs

1// Licensed under the Apache License, Version 2.0 (the "License");
2// you may not use this file except in compliance with the License.
3// You may obtain a copy of the License in the LICENSE-APACHE file or at:
4//     https://www.apache.org/licenses/LICENSE-2.0
5
6//! Text widgets
7
8use kas::prelude::*;
9use kas::text::format::FormattableText;
10use kas::theme::{self, TextClass};
11
12// NOTE: Text maintains a copy of the (formatted) string internally. Most
13// importantly this allows change detection.
14
15#[impl_self]
16mod Text {
17    /// A text label (derived from data)
18    ///
19    /// `Text` derives its contents from input data. Use [`Label`](crate::Label)
20    /// instead for fixed contents.
21    ///
22    /// By default, this uses [`TextClass::Standard`]; see [`Self::set_class`]
23    /// and [`Self::with_class`].
24    ///
25    /// See also macros [`format_text`](super::format_text) and
26    /// [`format_label`](super::format_label) which construct a
27    /// `Text` widget.
28    ///
29    /// Vertical alignment defaults to centred, horizontal alignment depends on
30    /// the script direction if not specified. Line-wrapping is enabled by
31    /// default.
32    #[widget]
33    #[layout(self.text)]
34    pub struct Text<A, T: Default + FormattableText + 'static = String> {
35        core: widget_core!(),
36        text: theme::Text<T>,
37        text_fn: Box<dyn Fn(&ConfigCx, &A, &mut T) -> bool>,
38    }
39
40    impl Default for Self
41    where
42        for<'a> &'a A: Into<T>,
43    {
44        fn default() -> Self {
45            Text {
46                core: Default::default(),
47                text: theme::Text::new(T::default(), TextClass::Standard, true),
48                text_fn: Box::new(|_, data, text| {
49                    let new_text = data.into();
50                    let changed = new_text != *text;
51                    if changed {
52                        *text = new_text;
53                    }
54                    changed
55                }),
56            }
57        }
58    }
59
60    impl<A> Text<A, String> {
61        /// Construct with an `str` accessor
62        pub fn new_str(as_str: impl Fn(&A) -> &str + 'static) -> Self {
63            Text {
64                core: Default::default(),
65                text: theme::Text::new(String::new(), TextClass::Standard, true),
66                text_fn: Box::new(move |_, data, text| {
67                    let s = as_str(data);
68                    let changed = *text != *s;
69                    if changed {
70                        *text = s.into();
71                    }
72                    changed
73                }),
74            }
75        }
76    }
77
78    impl Self {
79        /// Construct with a generator function
80        ///
81        /// `gen_text` is called on each widget update to generate text from
82        /// input data.
83        pub fn new_gen(gen_text: impl Fn(&ConfigCx, &A) -> T + 'static) -> Self {
84            Text {
85                core: Default::default(),
86                text: theme::Text::new(T::default(), TextClass::Standard, true),
87                text_fn: Box::new(move |cx, data, text| {
88                    let new_text = gen_text(cx, data);
89                    let changed = new_text != *text;
90                    if changed {
91                        *text = new_text;
92                    }
93                    changed
94                }),
95            }
96        }
97
98        /// Construct with an update function
99        ///
100        /// `update_text` is called on each widget update to generate text from
101        /// input data. It must return `true` when the input text is changed (or
102        /// updated text will not be displayed) and should return `false`
103        /// otherwise (or the text will be re-prepared needlessly, which can be
104        /// expensive).
105        pub fn new_update(update_text: impl Fn(&ConfigCx, &A, &mut T) -> bool + 'static) -> Self {
106            Text {
107                core: Default::default(),
108                text: theme::Text::new(T::default(), TextClass::Standard, true),
109                text_fn: Box::new(update_text),
110            }
111        }
112
113        /// Get text class
114        #[inline]
115        pub fn class(&self) -> TextClass {
116            self.text.class()
117        }
118
119        /// Set text class
120        ///
121        /// Default: [`TextClass::Standard`]
122        #[inline]
123        pub fn set_class(&mut self, class: TextClass) {
124            self.text.set_class(class);
125        }
126
127        /// Set text class (inline)
128        ///
129        /// Default: [`TextClass::Standard`]
130        #[inline]
131        pub fn with_class(mut self, class: TextClass) -> Self {
132            self.text.set_class(class);
133            self
134        }
135
136        /// Get whether line-wrapping is enabled
137        #[inline]
138        pub fn wrap(&self) -> bool {
139            self.text.wrap()
140        }
141
142        /// Enable/disable line wrapping
143        ///
144        /// By default this is enabled.
145        #[inline]
146        pub fn set_wrap(&mut self, wrap: bool) {
147            self.text.set_wrap(wrap);
148        }
149
150        /// Enable/disable line wrapping (inline)
151        #[inline]
152        pub fn with_wrap(mut self, wrap: bool) -> Self {
153            self.text.set_wrap(wrap);
154            self
155        }
156
157        /// Get read access to the text object
158        #[inline]
159        pub fn text(&self) -> &theme::Text<T> {
160            &self.text
161        }
162
163        /// Read the text contents as an `str`
164        #[inline]
165        pub fn as_str(&self) -> &str {
166            self.text.as_str()
167        }
168    }
169
170    impl Layout for Self {
171        fn set_rect(&mut self, cx: &mut SizeCx, rect: Rect, hints: AlignHints) {
172            self.text
173                .set_rect(cx, rect, hints.combine(AlignHints::VERT_CENTER));
174        }
175    }
176
177    impl Tile for Self {
178        fn role(&self, _: &mut dyn RoleCx) -> Role<'_> {
179            Role::Label(self.text.as_str())
180        }
181    }
182
183    impl Events for Self {
184        type Data = A;
185
186        fn configure(&mut self, cx: &mut ConfigCx) {
187            self.text.configure(&mut cx.size_cx());
188        }
189
190        fn update(&mut self, cx: &mut ConfigCx, data: &A) {
191            if (self.text_fn)(cx, data, self.text.text_mut()) {
192                self.text.require_reprepare();
193                self.text.reprepare_action(cx);
194            }
195        }
196    }
197}
198
199/// Construct a [`Text`] widget which updates text using the [`format!`] macro
200///
201/// This uses [`TextClass::Standard`]. See also [`format_label`](crate::format_label).
202///
203/// Examples:
204/// ```
205/// use kas_widgets::Text;
206/// let _ = kas_widgets::format_text!(data: &i32, "Data value: {data}");
207/// let _: Text<i32, _> = kas_widgets::format_text!(data, "Data value: {data}");
208/// let _: Text<i32, String> = kas_widgets::format_text!("Data value: {}");
209/// ```
210#[macro_export]
211macro_rules! format_text {
212    ($data:ident, $($arg:tt)*) => {
213        $crate::Text::new_gen(move |_, $data| format!($($arg)*))
214    };
215    ($data:ident : $data_ty:ty , $($arg:tt)*) => {
216        $crate::Text::new_gen(move |_, $data : $data_ty| format!($($arg)*))
217    };
218    ($lit:literal $(, $arg:tt)*) => {
219        $crate::Text::new_gen(move |_, data| format!($lit $(, $arg)*, data))
220    };
221}
222
223/// Construct a [`Text`] widget using [`TextClass::Label`] which updates text
224/// using the [`format!`] macro
225///
226/// This is identical to [`format_text`](crate::format_text) aside from the
227/// [`TextClass`].
228#[macro_export]
229macro_rules! format_label {
230    ($data:ident, $($arg:tt)*) => {
231        $crate::Text::new_gen(move |_, $data| format!($($arg)*))
232            .with_class(::kas::theme::TextClass::Label)
233    };
234    ($data:ident : $data_ty:ty , $($arg:tt)*) => {
235        $crate::Text::new_gen(move |_, $data : $data_ty| format!($($arg)*))
236            .with_class(::kas::theme::TextClass::Label)
237    };
238    ($lit:literal $(, $arg:tt)*) => {
239        $crate::Text::new_gen(move |_, data| format!($lit $(, $arg)*, data))
240            .with_class(::kas::theme::TextClass::Label)
241    };
242}