flux-tui 0.5.0

Fast and lightweight Terminal UI drawing library
Documentation
use arrayvec::ArrayVec;
use as_any::Downcast;
use vector2d::Vector2D;

use crate::{canvas::*, common::*, window::*};
use std::rc::Rc;

use super::*;

pub type FocusChangedCallback = dyn Fn(&mut DockPanel);
pub type DockPanelKeyHandler = fn(&mut DockPanel, event: &mut WindowEvent);

#[derive(Debug, Clone, Copy, PartialEq)]
#[repr(usize)]
pub enum DockAlignment {
	Top,
	Bottom,
	Left,
	Right,
	Center,
}

impl DockAlignment {
	pub const VARIANTS: &'static [Self] = &[
		DockAlignment::Top,
		DockAlignment::Bottom,
		DockAlignment::Left,
		DockAlignment::Right,
		DockAlignment::Center,
	];
}

/// The center element takes most of the space, but at least 50% in width and height, depending how big the other elements are.
/// The outer elements are technically computed within the layout first and the center is the last one. Each outer element
/// should occupy at most 25% of the width/height - their relative size is relative to the maximum 25%. In case their size
/// is fixed, the heuristic will choose MIN(\<fixed window size\>, \<25% of the dockpanels size\>).
/// This control by default has no keybindings.
/// ```text
/// ┌───────────────────────┐
/// │         Top           │
/// ├──────┬────────┬───────┤
/// │ Left │ Center │ Right │
/// ├──────┴────────┴───────┤
/// │        Bottom         │
/// └───────────────────────┘
/// ```
pub struct DockPanel {
	base: WidgetBase,
	inner_border: bool,
	children: [Option<WindowRef>; 5],
	inner_borders: ArrayVec<Line2D<TSize>, 4>,
	focus: DockAlignment,
	key_handler: DockPanelKeyHandler,
	callback: Rc<FocusChangedCallback>,
}

impl Default for DockPanel {
	fn default() -> Self {
		Self::new(false)
	}
}

impl DockPanel {
	/// The maximum size of an outer (non-center) child element
	pub const MAX_CHILD_SIZE: Size = Size::Relative(Percent::from_int(25));

	pub fn new(inner_border: bool) -> Self {
		Self {
			base: WidgetBase::default(),
			inner_border,
			children: [const { None }; 5],
			inner_borders: ArrayVec::new(),
			focus: DockAlignment::Center,
			key_handler: Self::default_key_handler,
			callback: Rc::new(Self::default_callback),
		}
	}

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

	pub fn default_key_handler(&mut self, _: &mut WindowEvent) {}

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

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

	pub fn set_inner_border(&mut self, inner_border: bool) {
		self.inner_border = inner_border;
		self.provoke_changed_property(WindowProperty::Children);
	}

	pub fn set_child(&mut self, dock: DockAlignment, child: Option<WindowRef>) {
		if self.focus == dock && child.is_none() {
			self.focus = DockAlignment::Center;
		}
		self.children[dock as usize] = child;
		self.provoke_changed_property(WindowProperty::Children);
	}

	pub fn set_focus(&mut self, dock: DockAlignment) {
		if self.focus != dock && self.children[dock as usize].is_some() {
			self.focus = dock;
			self.provoke_changed_property(WindowProperty::Focus);
			let cb = self.callback.clone();
			cb(self);
		}
	}

	fn draw_border(window: &dyn Window, mut canvas: BorderCanvas) {
		let slf: &Self = window.downcast_ref().unwrap();
		if let BorderStyle::Preset { kind, bg, fg } = slf.base.border {
			canvas.draw_preset_border(kind, fg, bg, "").unwrap();
			let cts = kind.connector_style();
			let size = canvas.size();
			for l in slf.inner_borders.iter().filter(|x| x.is_horizontal()) {
				canvas
					.get_glyph(0, l.a.y + 1)
					.unwrap()
					.set_grapheme(cts[0])
					.unwrap();
				canvas
					.get_glyph(size.x - 1, l.a.y + 1)
					.unwrap()
					.set_grapheme(cts[1])
					.unwrap();
			}
		}
	}
}

impl Window for DockPanel {
	fn render(&self, canvas: &mut crate::canvas::Canvas) {
		//Draw inner borders
		if self.inner_border {
			let kind = self.base.border.get_borderkind().unwrap_or_default();
			let cts = kind.connector_style();
			let lns = kind.line_style();
			for line in self.inner_borders.iter() {
				if line.is_vertical() {
					if let Some(mut column) = canvas.get_column(line.a.x, GlyphWidth::Half) {
						for pos in line.iter_points() {
							column.get(pos.y).unwrap().set_grapheme(lns[1]).unwrap();
						}
					}
				}
				else if let Some(mut row) = canvas.get_row(line.a.y, GlyphWidth::Half) {
					for pos in line.iter_points() {
						row.get(pos.x).unwrap().set_grapheme(lns[0]).unwrap();
					}
				}
			}

			for vert in self.inner_borders.iter().filter(|x| x.is_vertical()) {
				for hori in self.inner_borders.iter().filter(|x| x.is_horizontal()) {
					if hori.a.y + 1 == vert.a.y {
						if let Some(mut gl) =
							canvas.get_glyph(vert.a.x, vert.a.y - 1, GlyphWidth::Half)
						{
							gl.set_grapheme(cts[2]).unwrap();
						}
					}
					else if hori.a.y == vert.b.y {
						if let Some(mut gl) = canvas.get_glyph(vert.b.x, vert.b.y, GlyphWidth::Half)
						{
							gl.set_grapheme(cts[3]).unwrap();
						}
					}
				}
			}
		}
	}

	fn handle_event(&mut self, event: &mut WindowEvent) {
		(self.key_handler)(self, event)
	}

	fn children(&mut self, mut builder: SubWindowBuilder) -> SubWindows {
		builder.disable_overlapping();
		let mut base_rect = builder.base_rect();

		let max_x = Self::MAX_CHILD_SIZE.get_size(base_rect.size.x);
		let max_y = Self::MAX_CHILD_SIZE.get_size(base_rect.size.y);

		let draw_border = self.inner_border && base_rect.size.x > 4 && base_rect.size.y > 4;

		self.inner_borders.clear();
		for dock in DockAlignment::VARIANTS.iter() {
			if let Some(child) = self.children[*dock as usize].clone() {
				let rect;
				let size = child.borrow().desired_size(Vector2D::new(max_x, max_y));
				//Retrieves the best possible size for the outer (non-center) children
				let border = draw_border as TSize;
				if size.x * size.y == 0 {
					continue;
				}
				match dock {
					DockAlignment::Top => {
						rect = base_rect.subrect(TSize::MIN, TSize::MIN, base_rect.size.x, size.y);
						if draw_border {
							self.inner_borders.push(Line2D::new(
								TSize::MIN,
								size.y,
								base_rect.end().x,
								size.y,
							));
						}
						base_rect = base_rect
							.subrect(
								TSize::MIN,
								size.y + border,
								base_rect.size.x,
								base_rect.size.y - (size.y + border),
							)
							.unwrap();
					}
					DockAlignment::Bottom => {
						rect = base_rect.subrect(
							TSize::MIN,
							base_rect.size.y - size.y,
							base_rect.size.x,
							size.y,
						);
						if draw_border {
							self.inner_borders.push(Line2D::new(
								TSize::MIN,
								base_rect.end().y - (size.y + border),
								base_rect.end().x,
								base_rect.end().y - (size.y + border),
							));
						}

						base_rect = base_rect
							.subrect(
								TSize::MIN,
								TSize::MIN,
								base_rect.size.x,
								base_rect.size.y - (size.y + border),
							)
							.unwrap();
					}
					DockAlignment::Left => {
						rect = base_rect.subrect(TSize::MIN, TSize::MIN, size.x, base_rect.size.y);
						if draw_border {
							self.inner_borders.push(Line2D::new(
								size.x,
								base_rect.start.y,
								size.x,
								base_rect.end().y,
							));
						}
						base_rect = base_rect
							.subrect(
								size.x + border,
								TSize::MIN,
								base_rect.size.x - (size.x + border),
								base_rect.size.y,
							)
							.unwrap();
					}
					DockAlignment::Right => {
						rect = base_rect.subrect(
							base_rect.size.x - size.x,
							TSize::MIN,
							size.x,
							base_rect.size.y,
						);
						if draw_border {
							self.inner_borders.push(Line2D::new(
								base_rect.end().x - (size.x + border),
								base_rect.start.y,
								base_rect.end().x - (size.x + border),
								base_rect.end().y,
							));
						}
						base_rect = base_rect
							.subrect(
								TSize::MIN,
								TSize::MIN,
								base_rect.size.x - (size.x + border),
								base_rect.size.y,
							)
							.unwrap();
					}
					DockAlignment::Center => {
						rect = Ok(base_rect);
					}
				}
				builder.add_rect(rect.unwrap()).unwrap();
				builder.add_child(child);
			}
		}

		builder.build().unwrap()
	}

	fn focus(&self) -> Option<WindowRef> {
		self.children[self.focus as usize].clone()
	}
}

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

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

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

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

	fn border(&self) -> BorderStyle {
		BorderStyle::custom(Self::draw_border, Thickness::from(1))
	}

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

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

	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_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 DockPanel {
	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;
	}
}