use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error};
use crate::node::{ActionId, Common, IntoNode, Node};
use crate::text::{Alignment, Line};
use crate::{style::Color, style::Style};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum BorderType {
#[default]
Plain,
Rounded,
Double,
Thick,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum BorderSide {
TOP,
RIGHT,
BOTTOM,
LEFT,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
pub enum Borders {
All,
#[default]
None_,
Sides(Vec<BorderSide>),
Other(String),
}
pub mod borders {
pub const ALL: &str = "ALL";
pub const NONE: &str = "NONE";
}
impl Serialize for Borders {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self {
Self::All => serializer.serialize_str(borders::ALL),
Self::None_ => serializer.serialize_str(borders::NONE),
Self::Sides(sides) => sides.serialize(serializer),
Self::Other(name) => serializer.serialize_str(name),
}
}
}
impl<'de> Deserialize<'de> for Borders {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let raw = serde_json::Value::deserialize(deserializer)?;
match raw {
serde_json::Value::String(name) => match name.as_str() {
borders::ALL => Ok(Self::All),
borders::NONE => Ok(Self::None_),
_ => Ok(Self::Other(name)),
},
serde_json::Value::Array(_) => {
let sides = Vec::<BorderSide>::deserialize(raw).map_err(D::Error::custom)?;
Ok(Self::Sides(sides))
}
_ => Err(D::Error::custom(
"borders must be \"ALL\", \"NONE\" or an array of sides",
)),
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum TitlePosition {
#[default]
Top,
Bottom,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Title {
pub content: Line,
#[serde(skip_serializing_if = "Option::is_none")]
pub alignment: Option<Alignment>,
#[serde(skip_serializing_if = "Option::is_none")]
pub position: Option<TitlePosition>,
}
impl Title {
#[must_use]
pub fn top(content: impl Into<Line>) -> Self {
Self {
content: content.into(),
alignment: None,
position: Some(TitlePosition::Top),
}
}
#[must_use]
pub fn bottom(content: impl Into<Line>) -> Self {
Self {
content: content.into(),
alignment: None,
position: Some(TitlePosition::Bottom),
}
}
#[must_use]
pub fn alignment(mut self, alignment: Alignment) -> Self {
self.alignment = Some(alignment);
self
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum BlockTitle {
Line(Line),
Titles(Vec<Title>),
}
impl From<&str> for BlockTitle {
fn from(s: &str) -> Self {
Self::Line(Line::from(s))
}
}
impl From<String> for BlockTitle {
fn from(s: String) -> Self {
Self::Line(Line::from(s))
}
}
impl From<Line> for BlockTitle {
fn from(line: Line) -> Self {
Self::Line(line)
}
}
impl From<Vec<Title>> for BlockTitle {
fn from(titles: Vec<Title>) -> Self {
Self::Titles(titles)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Padding {
Uniform(u16),
Rect {
#[serde(default, skip_serializing_if = "Option::is_none")]
left: Option<u16>,
#[serde(default, skip_serializing_if = "Option::is_none")]
right: Option<u16>,
#[serde(default, skip_serializing_if = "Option::is_none")]
top: Option<u16>,
#[serde(default, skip_serializing_if = "Option::is_none")]
bottom: Option<u16>,
},
}
impl Padding {
#[must_use]
pub fn uniform(n: u16) -> Self {
Self::Uniform(n)
}
#[must_use]
pub fn new(left: u16, right: u16, top: u16, bottom: u16) -> Self {
Self::Rect {
left: Some(left),
right: Some(right),
top: Some(top),
bottom: Some(bottom),
}
}
#[must_use]
pub fn horizontal(n: u16) -> Self {
Self::Rect {
left: Some(n),
right: Some(n),
top: None,
bottom: None,
}
}
#[must_use]
pub fn vertical(n: u16) -> Self {
Self::Rect {
left: None,
right: None,
top: Some(n),
bottom: Some(n),
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BlockProps {
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<BlockTitle>,
#[serde(skip_serializing_if = "Option::is_none")]
pub title_alignment: Option<Alignment>,
#[serde(skip_serializing_if = "Option::is_none")]
pub borders: Option<Borders>,
#[serde(skip_serializing_if = "Option::is_none")]
pub border_type: Option<BorderType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub border_style: Option<Style>,
#[serde(skip_serializing_if = "Option::is_none")]
pub title_style: Option<Style>,
#[serde(skip_serializing_if = "Option::is_none")]
pub padding: Option<Padding>,
#[serde(skip_serializing_if = "Option::is_none")]
pub collapsible: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub collapsed: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rail: Option<Color>,
#[serde(skip_serializing_if = "Option::is_none")]
pub style: Option<Style>,
}
impl BlockProps {
#[must_use]
pub fn bordered() -> Self {
Self {
borders: Some(Borders::All),
..Self::default()
}
}
#[must_use]
pub fn title(mut self, title: impl Into<BlockTitle>) -> Self {
self.title = Some(title.into());
self
}
#[must_use]
pub fn title_alignment(mut self, alignment: Alignment) -> Self {
self.title_alignment = Some(alignment);
self
}
#[must_use]
pub fn borders(mut self, borders: Borders) -> Self {
self.borders = Some(borders);
self
}
#[must_use]
pub fn border_type(mut self, border_type: BorderType) -> Self {
self.border_type = Some(border_type);
self
}
#[must_use]
pub fn border_style(mut self, style: Style) -> Self {
self.border_style = Some(style);
self
}
#[must_use]
pub fn title_style(mut self, style: Style) -> Self {
self.title_style = Some(style);
self
}
#[must_use]
pub fn padding(mut self, padding: Padding) -> Self {
self.padding = Some(padding);
self
}
#[must_use]
pub fn collapsible(mut self, collapsible: bool) -> Self {
self.collapsible = Some(collapsible);
self
}
#[must_use]
pub fn collapsed(mut self, collapsed: bool) -> Self {
self.collapsed = Some(collapsed);
self
}
#[must_use]
pub fn rail(mut self, color: Color) -> Self {
self.rail = Some(color);
self
}
#[must_use]
pub fn style(mut self, style: Style) -> Self {
self.style = Some(style);
self
}
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Block {
#[serde(flatten)]
pub common: Common,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<BlockTitle>,
#[serde(skip_serializing_if = "Option::is_none")]
pub title_alignment: Option<Alignment>,
#[serde(skip_serializing_if = "Option::is_none")]
pub borders: Option<Borders>,
#[serde(skip_serializing_if = "Option::is_none")]
pub border_type: Option<BorderType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub border_style: Option<Style>,
#[serde(skip_serializing_if = "Option::is_none")]
pub title_style: Option<Style>,
#[serde(skip_serializing_if = "Option::is_none")]
pub padding: Option<Padding>,
#[serde(skip_serializing_if = "Option::is_none")]
pub collapsible: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub collapsed: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rail: Option<Color>,
#[serde(skip_serializing_if = "Option::is_none")]
pub child: Option<Box<Node>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub on_toggle: Option<ActionId>,
}
impl Block {
#[must_use]
pub fn bordered() -> Self {
Self {
borders: Some(Borders::All),
..Self::default()
}
}
#[must_use]
pub fn title(mut self, title: impl Into<BlockTitle>) -> Self {
self.title = Some(title.into());
self
}
#[must_use]
pub fn title_alignment(mut self, alignment: Alignment) -> Self {
self.title_alignment = Some(alignment);
self
}
#[must_use]
pub fn borders(mut self, borders: Borders) -> Self {
self.borders = Some(borders);
self
}
#[must_use]
pub fn border_type(mut self, border_type: BorderType) -> Self {
self.border_type = Some(border_type);
self
}
#[must_use]
pub fn border_style(mut self, style: Style) -> Self {
self.border_style = Some(style);
self
}
#[must_use]
pub fn title_style(mut self, style: Style) -> Self {
self.title_style = Some(style);
self
}
#[must_use]
pub fn padding(mut self, padding: Padding) -> Self {
self.padding = Some(padding);
self
}
#[must_use]
pub fn collapsible(mut self, collapsible: bool) -> Self {
self.collapsible = Some(collapsible);
self
}
#[must_use]
pub fn collapsed(mut self, collapsed: bool) -> Self {
self.collapsed = Some(collapsed);
self
}
#[must_use]
pub fn rail(mut self, color: Color) -> Self {
self.rail = Some(color);
self
}
#[must_use]
pub fn on_toggle(mut self, action: impl Into<ActionId>) -> Self {
self.on_toggle = Some(action.into());
self
}
#[must_use]
pub fn child(mut self, child: impl IntoNode) -> Self {
self.child = Some(Box::new(child.into_node()));
self
}
}
impl From<Block> for BlockProps {
fn from(block: Block) -> Self {
Self {
title: block.title,
title_alignment: block.title_alignment,
borders: block.borders,
border_type: block.border_type,
border_style: block.border_style,
title_style: block.title_style,
padding: block.padding,
collapsible: block.collapsible,
collapsed: block.collapsed,
rail: block.rail,
style: block.common.style,
}
}
}
impl IntoNode for Block {}
impl From<Block> for Node {
fn from(b: Block) -> Self {
Node::Block(b)
}
}
#[cfg(test)]
mod tests {
#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
reason = "tests assert by unwrapping"
)]
use super::*;
#[test]
fn borders_shorthand() {
let b: Borders = serde_json::from_str("\"ALL\"").unwrap_or_default();
assert_eq!(b, Borders::All);
let b: Borders = serde_json::from_str("[\"TOP\",\"LEFT\"]").unwrap_or_default();
assert_eq!(b, Borders::Sides(vec![BorderSide::TOP, BorderSide::LEFT]));
}
}