use crate::element::{Component, Element};
use crate::layout::{
AlignContent, AlignItems, AlignSelf, Display, FlexDirection, FlexWrap, GridAutoFlow,
GridPlacement, JustifyContent, LayoutStyle, Overflow, Position, TrackSize,
};
use crate::style::Color;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BorderChars {
pub top_left: char,
pub top_right: char,
pub bottom_left: char,
pub bottom_right: char,
pub horizontal: char,
pub vertical: char,
}
impl Default for BorderChars {
fn default() -> Self {
BorderStyle::Single.chars()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum BorderStyle {
#[default]
None,
Single,
Double,
Round,
Bold,
Classic,
Custom(BorderChars),
}
impl BorderStyle {
pub fn chars(self) -> BorderChars {
match self {
BorderStyle::None => BorderChars {
top_left: ' ',
top_right: ' ',
bottom_left: ' ',
bottom_right: ' ',
horizontal: ' ',
vertical: ' ',
},
BorderStyle::Single => BorderChars {
top_left: '┌',
top_right: '┐',
bottom_left: '└',
bottom_right: '┘',
horizontal: '─',
vertical: '│',
},
BorderStyle::Double => BorderChars {
top_left: '╔',
top_right: '╗',
bottom_left: '╚',
bottom_right: '╝',
horizontal: '═',
vertical: '║',
},
BorderStyle::Round => BorderChars {
top_left: '╭',
top_right: '╮',
bottom_left: '╰',
bottom_right: '╯',
horizontal: '─',
vertical: '│',
},
BorderStyle::Bold => BorderChars {
top_left: '┏',
top_right: '┓',
bottom_left: '┗',
bottom_right: '┛',
horizontal: '━',
vertical: '┃',
},
BorderStyle::Classic => BorderChars {
top_left: '+',
top_right: '+',
bottom_left: '+',
bottom_right: '+',
horizontal: '-',
vertical: '|',
},
BorderStyle::Custom(chars) => chars,
}
}
pub fn has_border(self) -> bool {
!matches!(self, BorderStyle::None)
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct BorderSides {
pub top: bool,
pub bottom: bool,
pub left: bool,
pub right: bool,
}
impl BorderSides {
pub fn all() -> Self {
Self {
top: true,
bottom: true,
left: true,
right: true,
}
}
pub fn none() -> Self {
Self {
top: false,
bottom: false,
left: false,
right: false,
}
}
pub fn horizontal() -> Self {
Self {
top: true,
bottom: true,
left: false,
right: false,
}
}
pub fn vertical() -> Self {
Self {
top: false,
bottom: false,
left: true,
right: true,
}
}
pub fn top_only() -> Self {
Self {
top: true,
bottom: false,
left: false,
right: false,
}
}
pub fn bottom_only() -> Self {
Self {
top: false,
bottom: true,
left: false,
right: false,
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct BorderColors {
pub top: Option<Color>,
pub bottom: Option<Color>,
pub left: Option<Color>,
pub right: Option<Color>,
}
impl BorderColors {
pub fn all(color: Color) -> Self {
Self {
top: Some(color),
bottom: Some(color),
left: Some(color),
right: Some(color),
}
}
pub fn top_or(&self, default: Option<Color>) -> Option<Color> {
self.top.or(default)
}
pub fn bottom_or(&self, default: Option<Color>) -> Option<Color> {
self.bottom.or(default)
}
pub fn left_or(&self, default: Option<Color>) -> Option<Color> {
self.left.or(default)
}
pub fn right_or(&self, default: Option<Color>) -> Option<Color> {
self.right.or(default)
}
}
#[derive(Debug, Clone)]
pub struct BoxProps {
pub width: Option<f32>,
pub height: Option<f32>,
pub min_width: Option<f32>,
pub min_height: Option<f32>,
pub max_width: Option<f32>,
pub max_height: Option<f32>,
pub flex_direction: FlexDirection,
pub flex_grow: f32,
pub flex_shrink: f32,
pub padding: f32,
pub padding_left: Option<f32>,
pub padding_right: Option<f32>,
pub padding_top: Option<f32>,
pub padding_bottom: Option<f32>,
pub margin: f32,
pub margin_left: Option<f32>,
pub margin_right: Option<f32>,
pub margin_top: Option<f32>,
pub margin_bottom: Option<f32>,
pub gap: f32,
pub align_items: Option<AlignItems>,
pub align_self: Option<AlignSelf>,
pub align_content: Option<AlignContent>,
pub justify_content: Option<JustifyContent>,
pub display: Display,
pub position: Position,
pub flex_wrap: FlexWrap,
pub flex_basis: Option<f32>,
pub aspect_ratio: Option<f32>,
pub overflow_x: Overflow,
pub overflow_y: Overflow,
pub inset_top: Option<f32>,
pub inset_bottom: Option<f32>,
pub inset_left: Option<f32>,
pub inset_right: Option<f32>,
pub grid_template_columns: Vec<TrackSize>,
pub grid_template_rows: Vec<TrackSize>,
pub grid_auto_flow: GridAutoFlow,
pub grid_column: GridPlacement,
pub grid_row: GridPlacement,
pub border_style: BorderStyle,
pub border_color: Option<Color>,
pub border_colors: BorderColors,
pub border_sides: Option<BorderSides>,
pub border_dim: bool,
pub background_color: Option<Color>,
pub visible: bool,
}
impl Default for BoxProps {
fn default() -> Self {
Self {
width: None,
height: None,
min_width: None,
min_height: None,
max_width: None,
max_height: None,
flex_direction: FlexDirection::default(),
flex_grow: 0.0,
flex_shrink: 0.0,
padding: 0.0,
padding_left: None,
padding_right: None,
padding_top: None,
padding_bottom: None,
margin: 0.0,
margin_left: None,
margin_right: None,
margin_top: None,
margin_bottom: None,
gap: 0.0,
align_items: None,
align_self: None,
align_content: None,
justify_content: None,
display: Display::Flex,
position: Position::Relative,
flex_wrap: FlexWrap::NoWrap,
flex_basis: None,
aspect_ratio: None,
overflow_x: Overflow::Visible,
overflow_y: Overflow::Visible,
inset_top: None,
inset_bottom: None,
inset_left: None,
inset_right: None,
grid_template_columns: Vec::new(),
grid_template_rows: Vec::new(),
grid_auto_flow: GridAutoFlow::Row,
grid_column: GridPlacement::auto(),
grid_row: GridPlacement::auto(),
border_style: BorderStyle::default(),
border_color: None,
border_colors: BorderColors::default(),
border_sides: None,
border_dim: false,
background_color: None,
visible: true, }
}
}
impl BoxProps {
pub fn new() -> Self {
Self::default()
}
pub fn column() -> Self {
Self {
flex_direction: FlexDirection::Column,
..Default::default()
}
}
pub fn row() -> Self {
Self {
flex_direction: FlexDirection::Row,
..Default::default()
}
}
pub fn with_gap(mut self, gap: f32) -> Self {
self.gap = gap;
self
}
pub fn with_padding(mut self, padding: f32) -> Self {
self.padding = padding;
self
}
pub fn with_border(mut self, style: BorderStyle) -> Self {
self.border_style = style;
self
}
pub fn with_border_color(mut self, style: BorderStyle, color: Color) -> Self {
self.border_style = style;
self.border_color = Some(color);
self
}
pub fn with_width(mut self, width: f32) -> Self {
self.width = Some(width);
self
}
pub fn with_height(mut self, height: f32) -> Self {
self.height = Some(height);
self
}
pub fn with_visible(mut self, visible: bool) -> Self {
self.visible = visible;
self
}
pub fn hidden(mut self) -> Self {
self.visible = false;
self
}
pub fn effective_border_sides(&self) -> BorderSides {
if !self.border_style.has_border() {
return BorderSides::none();
}
self.border_sides.unwrap_or_else(BorderSides::all)
}
pub fn top_border_color(&self) -> Option<Color> {
self.border_colors.top_or(self.border_color)
}
pub fn bottom_border_color(&self) -> Option<Color> {
self.border_colors.bottom_or(self.border_color)
}
pub fn left_border_color(&self) -> Option<Color> {
self.border_colors.left_or(self.border_color)
}
pub fn right_border_color(&self) -> Option<Color> {
self.border_colors.right_or(self.border_color)
}
pub fn to_layout_style(&self) -> LayoutStyle {
let sides = self.effective_border_sides();
let border_top: f32 = if sides.top { 1.0 } else { 0.0 };
let border_bottom: f32 = if sides.bottom { 1.0 } else { 0.0 };
let border_left: f32 = if sides.left { 1.0 } else { 0.0 };
let border_right: f32 = if sides.right { 1.0 } else { 0.0 };
let max_border = border_top
.max(border_bottom)
.max(border_left)
.max(border_right);
LayoutStyle {
width: self.width,
height: self.height,
min_width: self.min_width,
min_height: self.min_height,
max_width: self.max_width,
max_height: self.max_height,
flex_direction: self.flex_direction,
flex_grow: self.flex_grow,
flex_shrink: self.flex_shrink,
padding: self.padding + max_border,
padding_left: Some(self.padding_left.unwrap_or(self.padding) + border_left),
padding_right: Some(self.padding_right.unwrap_or(self.padding) + border_right),
padding_top: Some(self.padding_top.unwrap_or(self.padding) + border_top),
padding_bottom: Some(self.padding_bottom.unwrap_or(self.padding) + border_bottom),
margin: self.margin,
margin_left: self.margin_left,
margin_right: self.margin_right,
margin_top: self.margin_top,
margin_bottom: self.margin_bottom,
gap: self.gap,
align_items: self.align_items,
align_self: self.align_self,
align_content: self.align_content,
justify_content: self.justify_content,
display: self.display,
position: self.position,
flex_wrap: self.flex_wrap,
flex_basis: self.flex_basis,
aspect_ratio: self.aspect_ratio,
overflow_x: self.overflow_x,
overflow_y: self.overflow_y,
inset_top: self.inset_top,
inset_bottom: self.inset_bottom,
inset_left: self.inset_left,
inset_right: self.inset_right,
grid_template_columns: self.grid_template_columns.clone(),
grid_template_rows: self.grid_template_rows.clone(),
grid_auto_columns: Vec::new(),
grid_auto_rows: Vec::new(),
grid_auto_flow: self.grid_auto_flow,
grid_column: self.grid_column.clone(),
grid_row: self.grid_row.clone(),
..Default::default()
}
}
}
pub struct Box;
impl Component for Box {
type Props = BoxProps;
fn render(_props: &Self::Props) -> Element {
Element::empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_border_chars_default() {
let chars = BorderChars::default();
assert_eq!(chars.top_left, '┌');
}
#[test]
fn test_border_style_has_border() {
assert!(!BorderStyle::None.has_border());
assert!(BorderStyle::Single.has_border());
assert!(BorderStyle::Double.has_border());
assert!(BorderStyle::Round.has_border());
assert!(BorderStyle::Bold.has_border());
assert!(BorderStyle::Classic.has_border());
}
#[test]
fn test_box_props_to_layout_style() {
let props = BoxProps {
width: Some(80.0),
height: Some(24.0),
flex_direction: FlexDirection::Row,
padding: 2.0,
..Default::default()
};
let layout = props.to_layout_style();
assert_eq!(layout.width, Some(80.0));
assert_eq!(layout.height, Some(24.0));
assert_eq!(layout.flex_direction, FlexDirection::Row);
assert_eq!(layout.padding, 2.0);
}
#[test]
fn test_box_props_with_border_adds_to_padding() {
let props = BoxProps {
border_style: BorderStyle::Single,
padding: 1.0,
..Default::default()
};
let layout = props.to_layout_style();
assert_eq!(layout.padding, 2.0);
}
#[test]
fn test_box_props_without_border() {
let props = BoxProps {
border_style: BorderStyle::None,
padding: 1.0,
..Default::default()
};
let layout = props.to_layout_style();
assert_eq!(layout.padding, 1.0);
}
#[test]
fn test_border_sides_all() {
let sides = BorderSides::all();
assert!(sides.top);
assert!(sides.bottom);
assert!(sides.left);
assert!(sides.right);
}
#[test]
fn test_border_sides_none() {
let sides = BorderSides::none();
assert!(!sides.top);
assert!(!sides.bottom);
assert!(!sides.left);
assert!(!sides.right);
}
#[test]
fn test_border_sides_horizontal() {
let sides = BorderSides::horizontal();
assert!(sides.top);
assert!(sides.bottom);
assert!(!sides.left);
assert!(!sides.right);
}
#[test]
fn test_border_sides_vertical() {
let sides = BorderSides::vertical();
assert!(!sides.top);
assert!(!sides.bottom);
assert!(sides.left);
assert!(sides.right);
}
#[test]
fn test_border_sides_top_only() {
let sides = BorderSides::top_only();
assert!(sides.top);
assert!(!sides.bottom);
assert!(!sides.left);
assert!(!sides.right);
}
#[test]
fn test_border_sides_bottom_only() {
let sides = BorderSides::bottom_only();
assert!(!sides.top);
assert!(sides.bottom);
assert!(!sides.left);
assert!(!sides.right);
}
#[test]
fn test_border_colors_all() {
let colors = BorderColors::all(Color::Red);
assert_eq!(colors.top, Some(Color::Red));
assert_eq!(colors.bottom, Some(Color::Red));
assert_eq!(colors.left, Some(Color::Red));
assert_eq!(colors.right, Some(Color::Red));
}
#[test]
fn test_border_colors_default() {
let colors = BorderColors::default();
assert!(colors.top.is_none());
assert!(colors.bottom.is_none());
assert!(colors.left.is_none());
assert!(colors.right.is_none());
}
#[test]
fn test_border_colors_fallback() {
let colors = BorderColors {
top: Some(Color::Red),
..Default::default()
};
assert_eq!(colors.top_or(Some(Color::Blue)), Some(Color::Red));
assert_eq!(colors.bottom_or(Some(Color::Blue)), Some(Color::Blue));
}
#[test]
fn test_box_props_effective_border_sides_default() {
let props = BoxProps {
border_style: BorderStyle::Single,
..Default::default()
};
let sides = props.effective_border_sides();
assert!(sides.top);
assert!(sides.bottom);
assert!(sides.left);
assert!(sides.right);
}
#[test]
fn test_box_props_effective_border_sides_custom() {
let props = BoxProps {
border_style: BorderStyle::Single,
border_sides: Some(BorderSides::horizontal()),
..Default::default()
};
let sides = props.effective_border_sides();
assert!(sides.top);
assert!(sides.bottom);
assert!(!sides.left);
assert!(!sides.right);
}
#[test]
fn test_box_props_effective_border_sides_no_border() {
let props = BoxProps {
border_style: BorderStyle::None,
border_sides: Some(BorderSides::all()),
..Default::default()
};
let sides = props.effective_border_sides();
assert!(!sides.top);
assert!(!sides.bottom);
assert!(!sides.left);
assert!(!sides.right);
}
#[test]
fn test_box_props_per_side_colors() {
let props = BoxProps {
border_color: Some(Color::White),
border_colors: BorderColors {
top: Some(Color::Red),
bottom: Some(Color::Blue),
..Default::default()
},
..Default::default()
};
assert_eq!(props.top_border_color(), Some(Color::Red));
assert_eq!(props.bottom_border_color(), Some(Color::Blue));
assert_eq!(props.left_border_color(), Some(Color::White));
assert_eq!(props.right_border_color(), Some(Color::White));
}
#[test]
fn test_box_props_partial_border_padding() {
let props = BoxProps {
border_style: BorderStyle::Single,
border_sides: Some(BorderSides::horizontal()),
padding: 1.0,
..Default::default()
};
let layout = props.to_layout_style();
assert_eq!(layout.padding_top, Some(2.0)); assert_eq!(layout.padding_bottom, Some(2.0)); assert_eq!(layout.padding_left, Some(1.0)); assert_eq!(layout.padding_right, Some(1.0)); }
}