Skip to main content

flux_tui/components/
textblock.rs

1use unicode_width::UnicodeWidthStr;
2
3use super::*;
4use crate::canvas::*;
5
6/// Multi-line text component
7/// Breaks at each '\n'
8/// Support text alignment
9#[derive(Default)]
10pub struct TextBlock {
11	base: WidgetBase,
12	text: String,
13	text_alignment: HorizontalAlignment,
14	has_focus: bool,
15	line_wrap: bool,
16	focus_color: Option<Color>,
17}
18
19impl TextBlock {
20	pub const LINE_BREAK: char = '\n';
21	pub const WORD_BREAK: char = ' ';
22
23	pub fn new(text: String) -> Self {
24		Self {
25			text,
26			..Default::default()
27		}
28	}
29
30	/// Gets the inner text
31	pub fn get_text(&self) -> &str {
32		&self.text
33	}
34
35	/// Replaces the inner text
36	pub fn set_text(&mut self, text: &str) {
37		self.text.clear();
38		self.text.push_str(text);
39	}
40
41	/// Sets the alignment of the inner text
42	pub fn set_text_alignment(&mut self, alignment: HorizontalAlignment) {
43		self.text_alignment = alignment;
44	}
45
46	/// Sets the line wrapping of the text
47	pub fn set_line_wrapping(&mut self, wrapping: bool) {
48		self.line_wrap = wrapping;
49	}
50
51	pub fn set_focus_color(&mut self, color: Option<Color>) {
52		self.focus_color = color;
53	}
54}
55
56impl Window for TextBlock {
57	fn render(&self, canvas: &mut Canvas) {
58		let fg = match self.base.enabled {
59			true => match self.has_focus {
60				true => self.focus_color,
61				false => None,
62			},
63			false => self.base.colors.disabled,
64		};
65
66		let mut row_index = 0;
67		let mut row = canvas.get_row_variable_width(row_index).unwrap();
68
69		let space = Grapheme::from(&Self::WORD_BREAK.to_string()).unwrap();
70		let dot3 = VariableWidthGlyphRow::DOT3_REPLACEMENT;
71
72		match self.line_wrap {
73			true => {
74				for ln in self.text.split(Self::LINE_BREAK) {
75					let words = ln.split(Self::WORD_BREAK).collect::<Vec<_>>();
76					let mut counter = 0;
77					while counter < words.len() {
78						let mut word_count = 0;
79						let mut text_w = 0;
80						for word in words[counter..].iter() {
81							text_w += (word_count > 0) as TSize;
82							let width = word.width() as TSize;
83							if (text_w + width) > row.width() {
84								text_w = text_w.checked_sub(1).unwrap_or_default();
85								break;
86							}
87							text_w += width;
88							word_count += 1;
89						}
90
91						if word_count == 0 {
92							row.add_string(words[counter], fg, None, Style::None, Some(dot3))
93								.unwrap();
94							counter += 1;
95						}
96						else {
97							match self.text_alignment {
98								HorizontalAlignment::Left => {}
99								HorizontalAlignment::Center => {
100									row.skip(
101										row.width().checked_sub(text_w).unwrap_or_default() / 2,
102									);
103								}
104								HorizontalAlignment::Right => {
105									row.skip(row.width().checked_sub(text_w).unwrap_or_default());
106								}
107							}
108							for n in 0..word_count {
109								row.add_string(words[counter + n], fg, None, Style::None, None)
110									.unwrap();
111								if n != word_count - 1 {
112									row.add_grapheme(space, fg, None, Style::None).unwrap();
113								}
114							}
115						}
116
117						counter += word_count;
118						row_index += 1;
119						row = match canvas.get_row_variable_width(row_index) {
120							Some(l) => l,
121							None => break,
122						};
123					}
124
125					row_index += 1;
126					row = match canvas.get_row_variable_width(row_index) {
127						Some(l) => l,
128						None => break,
129					};
130				}
131			}
132			false => {
133				for ln in self.text.split(Self::LINE_BREAK) {
134					let text_w = ln.width() as TSize;
135					match self.text_alignment {
136						HorizontalAlignment::Left => {}
137						HorizontalAlignment::Center => {
138							row.skip(row.width().checked_sub(text_w).unwrap_or_default() / 2);
139						}
140						HorizontalAlignment::Right => {
141							row.skip(row.width().checked_sub(text_w).unwrap_or_default());
142						}
143					}
144					row.add_string(ln, fg, None, Style::None, Some(dot3))
145						.unwrap();
146					row_index += 1;
147					row = match canvas.get_row_variable_width(row_index) {
148						Some(l) => l,
149						None => break,
150					};
151				}
152			}
153		}
154	}
155
156	fn handle_event(&mut self, event: &mut WindowEvent) {
157		match event.raw() {
158			Event::FocusGained => self.has_focus = true,
159			Event::FocusLost => self.has_focus = false,
160			_ => {}
161		}
162	}
163
164	fn is_enabled(&self) -> bool {
165		self.base.enabled
166	}
167}
168
169impl WindowLayout for TextBlock {
170	fn desired_size(&self, available_size: TPoint) -> TPoint {
171		self.base.desired_size(available_size)
172	}
173
174	fn border(&self) -> BorderStyle {
175		self.base.border
176	}
177
178	fn alignment(&self) -> (HorizontalAlignment, VerticalAlignment) {
179		self.base.alignment
180	}
181
182	fn is_visible(&self) -> bool {
183		self.base.visibility
184	}
185
186	fn margin(&self) -> Thickness {
187		self.base.margin
188	}
189}
190
191impl HasWindowUID for TextBlock {
192	fn uid(&self) -> WindowUID {
193		self.base.uid
194	}
195}
196
197impl Widget for TextBlock {
198	fn set_visibility(&mut self, visibility: bool) {
199		self.base.visibility = visibility;
200		self.provoke_changed_property(WindowProperty::IsVisible);
201	}
202
203	fn set_width(&mut self, width: Size) {
204		self.base.size.x = width;
205		self.provoke_changed_property(WindowProperty::Size);
206	}
207
208	fn set_height(&mut self, height: Size) {
209		self.base.size.y = height;
210		self.provoke_changed_property(WindowProperty::Size);
211	}
212
213	fn set_margin(&mut self, margin: Thickness) {
214		self.base.margin = margin;
215		self.provoke_changed_property(WindowProperty::Margin);
216	}
217
218	fn set_border(&mut self, border: BorderStyle) {
219		self.base.border = border;
220		self.provoke_changed_property(WindowProperty::Border);
221	}
222
223	fn set_alignment(&mut self, horizontal: HorizontalAlignment, vertical: VerticalAlignment) {
224		self.base.alignment = (horizontal, vertical);
225		self.provoke_changed_property(WindowProperty::Alignment);
226	}
227
228	fn set_enabled_state(&mut self, is_enabled: bool) {
229		self.base.enabled = is_enabled;
230	}
231
232	fn set_width_constraint(&mut self, width: SizeConstraint) {
233		self.base.constraints.x = width;
234		self.provoke_changed_property(WindowProperty::Size);
235	}
236
237	fn set_height_constraint(&mut self, height: SizeConstraint) {
238		self.base.constraints.y = height;
239		self.provoke_changed_property(WindowProperty::Size);
240	}
241}
242
243impl WidgetColors for TextBlock {
244	fn set_disabled_color(&mut self, color: Option<Color>) {
245		self.base.colors.disabled = color;
246	}
247
248	fn set_base_fg_color(&mut self, color: Option<Color>) {
249		self.base.colors.base_fg = color;
250	}
251
252	fn set_base_bg_color(&mut self, color: Option<Color>) {
253		self.base.colors.base_bg = color;
254	}
255}