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}