flux-tui 0.5.0

Fast and lightweight Terminal UI drawing library
Documentation
use super::*;
use crate::canvas::*;

use std::num::NonZero;
use std::rc::Rc;

use crossterm::event::{Event, KeyCode};
use unicode_width::UnicodeWidthStr;

pub type StateChangedCallback = dyn Fn(&mut TextCheckbox);
pub type CheckboxKeyHandler = fn(&mut Checkbox, event: &mut WindowEvent);

/// Checkbox that shows a boolean state and swaps state when pressing Spacebar (while focused)
/// Focus highlights the whole Checkbox
///
/// Design:
///
/// checked:    unchecked:
/// ```text
/// \[x\] Text  \[ \] Text
/// ```
pub struct TextCheckbox {
	checkbox: Checkbox,
	text: String,
	callback: Rc<StateChangedCallback>,
}

impl TextCheckbox {
	pub fn default_callback(_: &mut Self) {}

	pub fn set_state_changed_callback<F: Fn(&mut Self) + 'static>(&mut self, callback: F) {
		self.callback = Rc::new(callback);
	}

	pub fn set_text(&mut self, text: &str) {
		self.text.clear();
		self.text.push_str(text);
		let min = 3 + !self.text.is_empty() as TSize * 2;
		let max = 3 + self.text.width() as TSize + !self.text.is_empty() as TSize;
		self.checkbox.base.constraints.x.max = Size::Fixed(NonZero::new(max).unwrap());
		self.checkbox.base.constraints.x.min = NonZero::new(min).unwrap();
	}

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

	// Returns the internal boolean state
	pub fn get_state(&self) -> bool {
		self.checkbox.state
	}

	// Sets the internal boolean state
	pub fn set_state(&mut self, state: bool) {
		self.checkbox.state = state;
	}

	pub fn set_key_handler(&mut self, f: CheckboxKeyHandler) {
		self.checkbox.set_key_handler(f);
	}
}

impl Default for TextCheckbox {
	fn default() -> Self {
		let mut s = Self {
			checkbox: Checkbox::default(),
			text: String::new(),
			callback: Rc::new(Self::default_callback),
		};
		s.checkbox.base.constraints.x.min = NonZero::new(3).unwrap();
		s.checkbox.base.constraints.x.max = Size::Relative(Percent::from_int(100));
		s
	}
}

impl Window for TextCheckbox {
	fn render(&self, canvas: &mut Canvas) {
		self.checkbox.render(canvas);
		let mut row = canvas.get_row_variable_width(0).unwrap();
		row.skip(4);
		row.add_string(
			&self.text,
			None,
			None,
			Style::None,
			Some(VariableWidthGlyphRow::DOT3_REPLACEMENT),
		)
		.unwrap();
	}

	fn handle_event(&mut self, event: &mut WindowEvent) {
		if self.checkbox.handle_event(event) {
			let cb = self.callback.clone();
			cb(self);
		}
	}

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

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

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

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

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

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

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

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

	fn set_width(&mut self, _: Size) {}

	fn set_height(&mut self, _: Size) {}

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

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

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

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

	fn set_width_constraint(&mut self, _: SizeConstraint) {}

	fn set_height_constraint(&mut self, _: SizeConstraint) {}
}

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

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

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

/// Base struct which can be used to make custom checkbox components
pub struct Checkbox {
	has_focus: bool,
	base: WidgetBase,
	state: bool,
	key_handler: CheckboxKeyHandler,
}

impl Default for Checkbox {
	fn default() -> Self {
		let mut s = Self {
			state: false,
			has_focus: false,
			base: WidgetBase::default(),
			key_handler: Self::default_key_handler,
		};
		s.base.constraints.x.min = NonZero::new(3).unwrap();
		s.base.constraints.x.max = Size::Fixed(NonZero::new(3).unwrap());
		s.base.constraints.y.min = NonZero::<TSize>::MIN;
		s.base.constraints.y.max = Size::Fixed(NonZero::<TSize>::MIN);
		s
	}
}

impl Checkbox {
	pub const CROSS_MARK: Grapheme = Grapheme::new_unchecked("x", GlyphWidth::Half);
	pub const OPEN_BOX: Grapheme = Grapheme::new_unchecked("[", GlyphWidth::Half);
	pub const CLOSE_BOX: Grapheme = Grapheme::new_unchecked("]", GlyphWidth::Half);

	pub fn default_key_handler(&mut self, event: &mut WindowEvent) {
		if let Event::Key(k) = event.raw() {
			if k.code == KeyCode::Char(' ') {
				self.state = !self.state;
				event.handled = true;
			}
		}
	}

	pub fn set_key_handler(&mut self, f: CheckboxKeyHandler) {
		self.key_handler = f;
	}

	// Returns the internal boolean state
	pub fn get_state(&self) -> bool {
		self.state
	}

	// Sets the internal boolean state
	pub fn set_state(&mut self, state: bool) {
		self.state = state;
	}

	pub fn render(&self, canvas: &mut Canvas) {
		let mut row = canvas.get_row(0, GlyphWidth::Half).unwrap();
		row.get(0).unwrap().set_grapheme(Self::OPEN_BOX).unwrap();
		if self.state {
			row.get(1).unwrap().set_grapheme(Self::CROSS_MARK).unwrap();
		}
		row.get(2).unwrap().set_grapheme(Self::CLOSE_BOX).unwrap();

		if self.has_focus {
			row.get(0).unwrap().set_style(Style::Reverse);
			row.get(2).unwrap().set_style(Style::ResetAfter);
		}

		if let Some(color) = self.base.colors.base_fg {
			row.fill_foreground(color);
		}
		if let Some(color) = self.base.colors.base_bg {
			row.fill_background(color);
		}

		if let Some(color) = self.base.colors.disabled
			&& !self.base.enabled
		{
			row.fill_foreground(color);
		}
	}

	pub fn handle_event(&mut self, event: &mut WindowEvent) -> bool {
		let old = self.state;
		match event.raw() {
			Event::FocusGained => self.has_focus = true,
			Event::FocusLost => self.has_focus = false,
			_ => (self.key_handler)(self, event),
		}

		self.state != old
	}
}