use compact_str::CompactString;
use serde::{Deserialize, Serialize};
use smallvec::SmallVec;
use crate::layout::{FlexStyle, Rect};
use crate::terminal::{Color, Style};
pub type NodeId = u64;
#[derive(Debug, Clone)]
pub struct RenderNode {
pub id: NodeId,
pub kind: NodeKind,
pub style: FlexStyle,
pub appearance: Appearance,
pub children: SmallVec<[NodeId; 4]>,
pub layout: Option<Rect>,
pub dirty: bool,
}
impl RenderNode {
pub fn new(id: NodeId, kind: NodeKind) -> Self {
Self {
id,
kind,
style: FlexStyle::default(),
appearance: Appearance::default(),
children: SmallVec::new(),
layout: None,
dirty: true,
}
}
pub fn box_node(id: NodeId) -> Self {
Self::new(id, NodeKind::Box)
}
pub fn text_node(id: NodeId, content: impl Into<CompactString>) -> Self {
Self::new(id, NodeKind::Text(TextContent::new(content)))
}
pub fn with_style(mut self, style: FlexStyle) -> Self {
self.style = style;
self
}
pub fn with_appearance(mut self, appearance: Appearance) -> Self {
self.appearance = appearance;
self
}
pub fn add_child(&mut self, child_id: NodeId) {
self.children.push(child_id);
}
pub fn remove_child(&mut self, child_id: NodeId) {
if let Some(pos) = self.children.iter().position(|&id| id == child_id) {
self.children.remove(pos);
}
}
pub fn mark_dirty(&mut self) {
self.dirty = true;
}
pub fn mark_clean(&mut self) {
self.dirty = false;
}
}
#[derive(Debug, Clone)]
pub enum NodeKind {
Box,
Text(TextContent),
Input(InputContent),
Raw(RawContent),
}
#[derive(Debug, Clone, Default)]
pub struct TextContent {
pub text: CompactString,
pub wrap: bool,
}
impl TextContent {
pub fn new(text: impl Into<CompactString>) -> Self {
Self {
text: text.into(),
wrap: false,
}
}
pub fn with_wrap(mut self) -> Self {
self.wrap = true;
self
}
}
#[derive(Debug, Clone, Default)]
pub struct InputContent {
pub value: CompactString,
pub placeholder: CompactString,
pub cursor: usize,
pub focused: bool,
pub mask: bool,
pub mask_char: char,
}
impl InputContent {
pub fn new() -> Self {
Self {
mask_char: '*',
..Default::default()
}
}
pub fn with_value(mut self, value: impl Into<CompactString>) -> Self {
self.value = value.into();
self
}
pub fn with_placeholder(mut self, placeholder: impl Into<CompactString>) -> Self {
self.placeholder = placeholder.into();
self
}
pub fn with_mask(mut self, mask_char: char) -> Self {
self.mask = true;
self.mask_char = mask_char;
self
}
}
#[derive(Debug, Clone)]
pub struct RawContent {
pub lines: SmallVec<[CompactString; 4]>,
}
impl RawContent {
pub fn new(lines: impl IntoIterator<Item = impl Into<CompactString>>) -> Self {
Self {
lines: lines.into_iter().map(Into::into).collect(),
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Appearance {
pub fg: Option<Color>,
pub bg: Option<Color>,
pub bold: bool,
pub dim: bool,
pub italic: bool,
pub underline: bool,
pub strikethrough: bool,
pub border: Option<BorderStyle>,
}
impl Appearance {
pub fn new() -> Self {
Self::default()
}
pub fn fg(mut self, color: Color) -> Self {
self.fg = Some(color);
self
}
pub fn bg(mut self, color: Color) -> Self {
self.bg = Some(color);
self
}
pub fn bold(mut self) -> Self {
self.bold = true;
self
}
pub fn italic(mut self) -> Self {
self.italic = true;
self
}
pub fn underline(mut self) -> Self {
self.underline = true;
self
}
pub fn border(mut self, style: BorderStyle) -> Self {
self.border = Some(style);
self
}
pub fn to_style(&self) -> Style {
Style {
fg: self.fg,
bg: self.bg,
bold: self.bold,
dim: self.dim,
italic: self.italic,
underline: self.underline,
strikethrough: self.strikethrough,
..Default::default()
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum BorderStyle {
None,
Single,
Double,
Rounded,
Heavy,
Dashed,
}
impl BorderStyle {
pub fn chars(
&self,
) -> (
&'static str,
&'static str,
&'static str,
&'static str,
&'static str,
&'static str,
) {
match self {
BorderStyle::None => (" ", " ", " ", " ", " ", " "),
BorderStyle::Single => ("─", "│", "┌", "┐", "└", "┘"),
BorderStyle::Double => ("═", "║", "╔", "╗", "╚", "╝"),
BorderStyle::Rounded => ("─", "│", "╭", "╮", "╰", "╯"),
BorderStyle::Heavy => ("━", "┃", "┏", "┓", "┗", "┛"),
BorderStyle::Dashed => ("╌", "╎", "┌", "┐", "└", "┘"),
}
}
}
#[cfg(test)]
mod tests {
use super::{Appearance, BorderStyle, NodeKind, RenderNode};
use crate::terminal::Color;
#[test]
fn test_render_node_new() {
let node = RenderNode::new(1, NodeKind::Box);
assert_eq!(node.id, 1);
assert!(matches!(node.kind, NodeKind::Box));
assert!(node.dirty);
}
#[test]
fn test_text_node() {
let node = RenderNode::text_node(1, "Hello");
assert!(matches!(node.kind, NodeKind::Text(_)));
if let NodeKind::Text(content) = &node.kind {
assert_eq!(content.text.as_str(), "Hello");
}
}
#[test]
fn test_appearance() {
let app = Appearance::new()
.fg(Color::Red)
.bold()
.border(BorderStyle::Single);
assert_eq!(app.fg, Some(Color::Red));
assert!(app.bold);
assert_eq!(app.border, Some(BorderStyle::Single));
}
#[test]
fn test_border_chars() {
let (h, v, tl, _tr, _bl, _br) = BorderStyle::Single.chars();
assert_eq!(h, "─");
assert_eq!(v, "│");
assert_eq!(tl, "┌");
}
}