flux-tui 0.5.0

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

use crossterm::event::{Event, KeyCode, KeyModifiers};
use std::rc::Rc;

pub type NumericValueChangedCallback = dyn Fn(&mut Numeric);
pub type NumericKeyHandler = fn(&mut Numeric, event: &mut WindowEvent);


///
/// Numerical Up/Down component, where a number can be changed by stepping up or down with a given
/// stepcount
/// Design:
/// ```text
/// -128▼▲
/// ```
///
pub struct Numeric {
	base: WidgetBase,
	has_focus: bool,
	value: i32,
	stepcount: u8,
	radix: Radix,
	callback: Rc<NumericValueChangedCallback>,
	key_handler: NumericKeyHandler,
	focus_color: Option<Color>,
	min_range: i32,
	max_range: i32,
}

impl Default for Numeric {
	fn default() -> Self {
		let mut slf = Self {
			base: WidgetBase::default(),
			has_focus: false,
			value: 0,
			stepcount: 1,
			radix: Radix::Decimal,
			callback: Rc::new(Self::default_callback),
			key_handler: Self::default_key_handler,
			focus_color: None,
			min_range: i32::MIN,
			max_range: i32::MAX,
		};
		slf.base.constraints.x.min = NonZero::new(3).unwrap();
		slf.value_changed_hook();
		slf
	}
}

impl Numeric {
	pub const ARROW_UP: Grapheme = ScrollViewer::ARROW_UP;
	pub const ARROW_DOWN: Grapheme = ScrollViewer::ARROW_DOWN;
	pub const ARROW_UP_DISABLED: Grapheme = ScrollViewer::ARROW_UP_DISABLED;
	pub const ARROW_DOWN_DISABLED: Grapheme = ScrollViewer::ARROW_DOWN_DISABLED;
	pub const NUMERIC_PLACEHOLDER: Grapheme = Grapheme::new_unchecked("x", GlyphWidth::Half);

	pub fn default_key_handler(&mut self, event: &mut WindowEvent) {
		if let Event::Key(k) = event.raw() {
			if k.modifiers.contains(KeyModifiers::ALT) {
				if k.code == KeyCode::Up {
					self.step_up();
					event.handled = true;
				}
				else if k.code == KeyCode::Down {
					self.step_down();
					event.handled = true;
				}
			}
		}
	}

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

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

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

	pub fn set_min_range(&mut self, min_range: i32) {
		if self.min_range < self.max_range {
			self.min_range = min_range;
			if !(self.min_range..self.max_range).contains(&self.value) {
				self.value = self.min_range;
				self.value_changed_hook();
			}
		}
	}

	pub fn set_max_range(&mut self, max_range: i32) {
		if self.max_range > self.min_range {
			self.max_range = max_range;
			if !(self.min_range..self.max_range).contains(&self.value) {
				self.value = self.min_range;
				self.value_changed_hook();
			}
		}
	}

	pub fn set_value(&mut self, value: i32) {
		self.value = value;
		self.value_changed_hook();
	}

	pub fn get_value(&self) -> i32 {
		self.value
	}

	pub fn set_base(&mut self, base: Radix) {
		if base != self.radix {
			self.radix = base;
			self.value_changed_hook();
		}
	}

	pub fn get_base(&self) -> Radix {
		self.radix
	}

	pub fn step_up(&mut self) {
		let cb = self.callback.clone();
		if self.value <= self.max_range - self.stepcount as i32 {
			self.value += self.stepcount as i32;
		}
		else {
			self.value = self.max_range;
		}
		self.value_changed_hook();
		cb(self);
	}

	pub fn step_down(&mut self) {
		let cb = self.callback.clone();
		if self.value > self.min_range + self.stepcount as i32 {
			self.value -= self.stepcount as i32;
		}
		else {
			self.value = self.min_range;
		}
		self.value_changed_hook();
		cb(self);
	}

	pub fn get_stepcount(&self) -> u8 {
		self.stepcount
	}

	pub fn set_stepcount(&mut self, stepcount: u8) {
		self.stepcount = stepcount;
	}

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

	fn value_changed_hook(&mut self) {
		let old = self.base.constraints.x.max;
		self.base.constraints.x.max = Size::Fixed(
			NonZero::new(
				self.radix.prefix_glyphs().len() as TSize
					+ self
						.value
						.abs()
						.checked_ilog(self.radix as i32)
						.unwrap_or(0) as TSize
					+ self.base.constraints.x.min.get()
					+ self.value.is_negative() as TSize,
			)
			.unwrap(),
		);
		if old != self.base.constraints.x.max {
			self.provoke_changed_property(WindowProperty::Size);
		}
	}
}

impl Window for Numeric {
	fn render(&self, canvas: &mut Canvas) {
		let len = self
			.value
			.abs()
			.checked_ilog(self.radix as i32)
			.unwrap_or(0) as TSize
			+ 1;
		let updown = 2;

		let mut row = canvas.get_row(0, GlyphWidth::Half).unwrap();

		row.get(row.length() - 1)
			.unwrap()
			.set_grapheme(Self::ARROW_UP)
			.unwrap();
		row.get(row.length() - 2)
			.unwrap()
			.set_grapheme(Self::ARROW_DOWN)
			.unwrap();
		if self.value == self.max_range {
			row.get(row.length() - 1)
				.unwrap()
				.set_grapheme(Self::ARROW_UP_DISABLED)
				.unwrap();
		}
		else if self.value == self.min_range {
			row.get(row.length() - 2)
				.unwrap()
				.set_grapheme(Self::ARROW_DOWN_DISABLED)
				.unwrap();
		}

		if row.length()
			>= len
				+ updown + self.value.is_negative() as TSize
				+ self.radix.prefix_glyphs().len() as TSize
		{
			let focus_color = match self.has_focus {
				true => self.focus_color,
				false => None,
			};
			let mut start = 0;
			if self.value < 0 {
				let mut item = row.get(0).unwrap();
				item.set_grapheme(Radix::NEGATIVE_INDICATOR).unwrap();
				start += 1;
				if let Some(color) = focus_color {
					item.set_fg(color);
				}
			}
			let end = row.length() - updown;

			for g in self.radix.prefix_glyphs() {
				let mut item = row.get(start).unwrap();
				item.set_grapheme(*g).unwrap();
				start += 1;
			}

			let mut rem = self.value.abs();
			for idx in (start..end).rev() {
				let mut item = row.get(idx).unwrap();
				item.set_grapheme(self.radix.first_literal(rem as usize))
					.unwrap();
				rem /= self.radix as i32;
			}
		}
		else {
			let mut item = row.get(row.length() - 3).unwrap();
			item.set_grapheme(Self::NUMERIC_PLACEHOLDER).unwrap();
		}

		if let Some(color) = self.focus_color
			&& self.has_focus
		{
			row.fill_foreground(color);
		}
		else if let Some(color) = self.base.colors.base_fg {
			row.fill_foreground(color);
		}

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

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

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

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

impl WindowLayout for Numeric {
	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 Numeric {
	fn uid(&self) -> WindowUID {
		self.base.uid
	}
}

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

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

	/// Unsupported
	fn set_height(&mut self, _: 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;
	}

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

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

impl WidgetColors for Numeric {
	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;
	}
}