1use crate::{
2 render::MeasureFunc, segmented_string::SegmentedString, CanvasTextStyle, Color, Component,
3 ComponentDrawer, ComponentUpdater, Hooks, Props, Weight,
4};
5use taffy::{AvailableSpace, Size};
6use unicode_width::UnicodeWidthStr;
7
8#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
10pub enum TextWrap {
11 #[default]
13 Wrap,
14 NoWrap,
16}
17
18#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
20pub enum TextAlign {
21 #[default]
23 Left,
24 Right,
26 Center,
28}
29
30#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
32pub enum TextDecoration {
33 #[default]
35 None,
36 Underline,
38}
39
40#[non_exhaustive]
42#[derive(Default, Props)]
43pub struct TextProps {
44 pub color: Option<Color>,
46
47 pub content: String,
49
50 pub weight: Weight,
52
53 pub wrap: TextWrap,
55
56 pub align: TextAlign,
58
59 pub decoration: TextDecoration,
61
62 pub italic: bool,
64}
65
66#[derive(Default)]
79pub struct Text {
80 style: CanvasTextStyle,
81 content: String,
82 wrap: TextWrap,
83 align: TextAlign,
84}
85
86impl Text {
87 pub(crate) fn measure_func(content: String, text_wrap: TextWrap) -> MeasureFunc {
88 Box::new(move |known_size, available_space, _| {
89 let content = Text::wrap(&content, text_wrap, known_size.width, available_space.width);
90 let mut max_width = 0;
91 let mut num_lines = 0;
92 for line in content.lines() {
93 max_width = max_width.max(line.width());
94 num_lines += 1;
95 }
96 Size {
97 width: max_width as _,
98 height: num_lines.max(1) as _,
99 }
100 })
101 }
102
103 fn do_wrap(s: &str, width: usize) -> String {
104 let s: SegmentedString = s.into();
105 let mut ret = String::new();
106 for line in s.wrap(width) {
107 if !ret.is_empty() {
108 ret.push('\n');
109 }
110 ret.push_str(line.to_string().trim_end());
111 }
112 ret
113 }
114
115 fn wrap(
116 content: &str,
117 text_wrap: TextWrap,
118 known_width: Option<f32>,
119 available_width: AvailableSpace,
120 ) -> String {
121 match text_wrap {
122 TextWrap::Wrap => match known_width {
123 Some(w) => Self::do_wrap(content, w as usize),
124 None => match available_width {
125 AvailableSpace::Definite(w) => Self::do_wrap(content, w as usize),
126 AvailableSpace::MaxContent => content.to_string(),
127 AvailableSpace::MinContent => Self::do_wrap(content, 1),
128 },
129 },
130 TextWrap::NoWrap => content.to_string(),
131 }
132 }
133
134 pub(crate) fn alignment_padding(line_width: usize, align: TextAlign, width: usize) -> usize {
135 match align {
136 TextAlign::Left => 0,
137 TextAlign::Right => width - line_width,
138 TextAlign::Center => width / 2 - line_width / 2,
139 }
140 }
141
142 fn align(content: String, align: TextAlign, width: usize) -> String {
143 match align {
144 TextAlign::Left => content,
145 _ => content
146 .lines()
147 .map(|line| {
148 format!(
149 "{:width$}{}",
150 "",
151 line,
152 width = Self::alignment_padding(line.width(), align, width)
153 )
154 })
155 .collect::<Vec<_>>()
156 .join("\n"),
157 }
158 }
159}
160
161pub(crate) struct TextDrawer<'a, 'b> {
162 x: isize,
163 y: isize,
164 drawer: &'a mut ComponentDrawer<'b>,
165 line_encountered_non_whitespace: bool,
166 skip_leading_whitespace: bool,
167}
168
169impl<'a, 'b> TextDrawer<'a, 'b> {
170 pub fn new(drawer: &'a mut ComponentDrawer<'b>, skip_leading_whitespace: bool) -> Self {
171 TextDrawer {
172 x: 0,
173 y: 0,
174 drawer,
175 line_encountered_non_whitespace: false,
176 skip_leading_whitespace,
177 }
178 }
179
180 pub fn append_lines<'c>(
181 &mut self,
182 lines: impl IntoIterator<Item = &'c str>,
183 style: CanvasTextStyle,
184 ) {
185 let mut lines = lines.into_iter().peekable();
186 while let Some(mut line) = lines.next() {
187 if self.skip_leading_whitespace && !self.line_encountered_non_whitespace {
188 let to_skip = line
189 .chars()
190 .position(|c| !c.is_whitespace())
191 .unwrap_or(line.len());
192 let (whitespace, remaining) = line.split_at(to_skip);
193 self.x += whitespace.width() as isize;
194 line = remaining;
195 if !line.is_empty() {
196 self.line_encountered_non_whitespace = true;
197 }
198 }
199 self.drawer.canvas().set_text(self.x, self.y, line, style);
200 if lines.peek().is_some() {
201 self.y += 1;
202 self.x = 0;
203 self.line_encountered_non_whitespace = false;
204 } else {
205 self.x += line.width() as isize;
206 }
207 }
208 }
209}
210
211impl Component for Text {
212 type Props<'a> = TextProps;
213
214 fn new(_props: &Self::Props<'_>) -> Self {
215 Self::default()
216 }
217
218 fn update(
219 &mut self,
220 props: &mut Self::Props<'_>,
221 _hooks: Hooks,
222 updater: &mut ComponentUpdater,
223 ) {
224 self.style = CanvasTextStyle {
225 color: props.color,
226 weight: props.weight,
227 underline: props.decoration == TextDecoration::Underline,
228 italic: props.italic,
229 };
230 self.content = props.content.clone();
231 self.wrap = props.wrap;
232 self.align = props.align;
233 updater.set_measure_func(Self::measure_func(self.content.clone(), props.wrap));
234 }
235
236 fn draw(&mut self, drawer: &mut ComponentDrawer<'_>) {
237 let width = drawer.layout().size.width;
238 let content = Self::wrap(
239 &self.content,
240 self.wrap,
241 None,
242 AvailableSpace::Definite(width),
243 );
244 let content = Self::align(content, self.align, width as _);
245 let mut drawer = TextDrawer::new(drawer, self.align != TextAlign::Left);
246 drawer.append_lines(content.lines(), self.style);
247 }
248}
249
250#[cfg(test)]
251mod tests {
252 use crate::prelude::*;
253 use crossterm::{csi, style::Attribute};
254 use std::io::Write;
255
256 #[test]
257 fn test_text() {
258 assert_eq!(element!(Text).to_string(), "\n");
259
260 assert_eq!(element!(Text(content: "foo")).to_string(), "foo\n");
261
262 assert_eq!(
263 element!(Text(content: "foo\nbar")).to_string(),
264 "foo\nbar\n"
265 );
266
267 assert_eq!(element!(Text(content: "😀")).to_string(), "😀\n");
268
269 assert_eq!(
270 element! {
271 View(width: 14) {
272 Text(content: "this is a wrapping test")
273 }
274 }
275 .to_string(),
276 "this is a\nwrapping test\n"
277 );
278
279 assert_eq!(
280 element! {
281 View(width: 15) {
282 Text(content: "this is an alignment test", align: TextAlign::Right)
283 }
284 }
285 .to_string(),
286 " this is an\n alignment test\n"
287 );
288
289 assert_eq!(
290 element! {
291 View(width: 15) {
292 Text(content: "this is an alignment test", align: TextAlign::Center)
293 }
294 }
295 .to_string(),
296 " this is an\nalignment test\n"
297 );
298
299 {
301 let canvas = element! {
302 View(width: 16) {
303 Text(content: "this is an alignment test", align: TextAlign::Center, decoration: TextDecoration::Underline)
304 }
305 }
306 .render(None);
307 let mut actual = Vec::new();
308 canvas.write_ansi(&mut actual).unwrap();
309
310 let mut expected = Vec::new();
311 write!(expected, csi!("0m")).unwrap();
312 write!(expected, " ").unwrap();
313 write!(expected, csi!("{}m"), Attribute::Underlined.sgr()).unwrap();
314 write!(expected, "this is an").unwrap();
315 write!(expected, csi!("K")).unwrap();
316 write!(expected, "\r\n").unwrap();
317 write!(expected, csi!("0m")).unwrap();
318 write!(expected, " ").unwrap();
319 write!(expected, csi!("{}m"), Attribute::Underlined.sgr()).unwrap();
320 write!(expected, "alignment test").unwrap();
321 write!(expected, csi!("K")).unwrap();
322 write!(expected, csi!("0m")).unwrap();
323 write!(expected, "\r\n").unwrap();
324
325 assert_eq!(actual, expected);
326 }
327 }
328}