flux-tui 0.5.0

Fast and lightweight Terminal UI drawing library
Documentation
use unicode_width::UnicodeWidthStr;

use super::*;
use crate::canvas::*;

/// Multi-line text component
/// Breaks at each '\n'
/// Support text alignment
#[derive(Default)]
pub struct TextBlock {
	base: WidgetBase,
	text: String,
	text_alignment: HorizontalAlignment,
	has_focus: bool,
	line_wrap: bool,
	focus_color: Option<Color>,
}

impl TextBlock {
	pub const LINE_BREAK: char = '\n';
	pub const WORD_BREAK: char = ' ';

	pub fn new(text: String) -> Self {
		Self {
			text,
			..Default::default()
		}
	}

	/// Gets the inner text
	pub fn get_text(&self) -> &str {
		&self.text
	}

	/// Replaces the inner text
	pub fn set_text(&mut self, text: &str) {
		self.text.clear();
		self.text.push_str(text);
	}

	/// Sets the alignment of the inner text
	pub fn set_text_alignment(&mut self, alignment: HorizontalAlignment) {
		self.text_alignment = alignment;
	}

	/// Sets the line wrapping of the text
	pub fn set_line_wrapping(&mut self, wrapping: bool) {
		self.line_wrap = wrapping;
	}

	pub fn set_focus_color(&mut self, color: Option<Color>) {
		self.focus_color = color;
	}
}

impl Window for TextBlock {
	fn render(&self, canvas: &mut Canvas) {
		let fg = match self.base.enabled {
			true => match self.has_focus {
				true => self.focus_color,
				false => None,
			},
			false => self.base.colors.disabled,
		};

		let mut row_index = 0;
		let mut row = canvas.get_row_variable_width(row_index).unwrap();

		let space = Grapheme::from(&Self::WORD_BREAK.to_string()).unwrap();
		let dot3 = VariableWidthGlyphRow::DOT3_REPLACEMENT;

		match self.line_wrap {
			true => {
				for ln in self.text.split(Self::LINE_BREAK) {
					let words = ln.split(Self::WORD_BREAK).collect::<Vec<_>>();
					let mut counter = 0;
					while counter < words.len() {
						let mut word_count = 0;
						let mut text_w = 0;
						for word in words[counter..].iter() {
							text_w += (word_count > 0) as TSize;
							let width = word.width() as TSize;
							if (text_w + width) > row.width() {
								text_w = text_w.checked_sub(1).unwrap_or_default();
								break;
							}
							text_w += width;
							word_count += 1;
						}

						if word_count == 0 {
							row.add_string(words[counter], fg, None, Style::None, Some(dot3))
								.unwrap();
							counter += 1;
						}
						else {
							match self.text_alignment {
								HorizontalAlignment::Left => {}
								HorizontalAlignment::Center => {
									row.skip(
										row.width().checked_sub(text_w).unwrap_or_default() / 2,
									);
								}
								HorizontalAlignment::Right => {
									row.skip(row.width().checked_sub(text_w).unwrap_or_default());
								}
							}
							for n in 0..word_count {
								row.add_string(words[counter + n], fg, None, Style::None, None)
									.unwrap();
								if n != word_count - 1 {
									row.add_grapheme(space, fg, None, Style::None).unwrap();
								}
							}
						}

						counter += word_count;
						row_index += 1;
						row = match canvas.get_row_variable_width(row_index) {
							Some(l) => l,
							None => break,
						};
					}

					row_index += 1;
					row = match canvas.get_row_variable_width(row_index) {
						Some(l) => l,
						None => break,
					};
				}
			}
			false => {
				for ln in self.text.split(Self::LINE_BREAK) {
					let text_w = ln.width() as TSize;
					match self.text_alignment {
						HorizontalAlignment::Left => {}
						HorizontalAlignment::Center => {
							row.skip(row.width().checked_sub(text_w).unwrap_or_default() / 2);
						}
						HorizontalAlignment::Right => {
							row.skip(row.width().checked_sub(text_w).unwrap_or_default());
						}
					}
					row.add_string(ln, fg, None, Style::None, Some(dot3))
						.unwrap();
					row_index += 1;
					row = match canvas.get_row_variable_width(row_index) {
						Some(l) => l,
						None => break,
					};
				}
			}
		}
	}

	fn handle_event(&mut self, event: &mut WindowEvent) {
		match event.raw() {
			Event::FocusGained => self.has_focus = true,
			Event::FocusLost => self.has_focus = false,
			_ => {}
		}
	}

	fn is_enabled(&self) -> bool {
		self.base.enabled
	}
}

impl WindowLayout for TextBlock {
	fn desired_size(&self, available_size: TPoint) -> TPoint {
		self.base.desired_size(available_size)
	}

	fn border(&self) -> BorderStyle {
		self.base.border
	}

	fn alignment(&self) -> (HorizontalAlignment, VerticalAlignment) {
		self.base.alignment
	}

	fn is_visible(&self) -> bool {
		self.base.visibility
	}

	fn margin(&self) -> Thickness {
		self.base.margin
	}
}

impl HasWindowUID for TextBlock {
	fn uid(&self) -> WindowUID {
		self.base.uid
	}
}

impl Widget for TextBlock {
	fn set_visibility(&mut self, visibility: bool) {
		self.base.visibility = visibility;
		self.provoke_changed_property(WindowProperty::IsVisible);
	}

	fn set_width(&mut self, width: Size) {
		self.base.size.x = width;
		self.provoke_changed_property(WindowProperty::Size);
	}

	fn set_height(&mut self, height: Size) {
		self.base.size.y = height;
		self.provoke_changed_property(WindowProperty::Size);
	}

	fn set_margin(&mut self, margin: Thickness) {
		self.base.margin = margin;
		self.provoke_changed_property(WindowProperty::Margin);
	}

	fn set_border(&mut self, border: BorderStyle) {
		self.base.border = border;
		self.provoke_changed_property(WindowProperty::Border);
	}

	fn set_alignment(&mut self, horizontal: HorizontalAlignment, vertical: VerticalAlignment) {
		self.base.alignment = (horizontal, vertical);
		self.provoke_changed_property(WindowProperty::Alignment);
	}

	fn set_enabled_state(&mut self, is_enabled: bool) {
		self.base.enabled = is_enabled;
	}

	fn set_width_constraint(&mut self, width: SizeConstraint) {
		self.base.constraints.x = width;
		self.provoke_changed_property(WindowProperty::Size);
	}

	fn set_height_constraint(&mut self, height: SizeConstraint) {
		self.base.constraints.y = height;
		self.provoke_changed_property(WindowProperty::Size);
	}
}

impl WidgetColors for TextBlock {
	fn set_disabled_color(&mut self, color: Option<Color>) {
		self.base.colors.disabled = color;
	}

	fn set_base_fg_color(&mut self, color: Option<Color>) {
		self.base.colors.base_fg = color;
	}

	fn set_base_bg_color(&mut self, color: Option<Color>) {
		self.base.colors.base_bg = color;
	}
}