diff --git a/Cargo.toml b/Cargo.toml index 8b8b96616..1837ad5ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -tui = { version = "0.16.0", features = ["crossterm"], default-features = false } +tui = "0.17.0" crossterm = "0.22.1" anyhow = "1.0.42" thiserror = "1.0.24" diff --git a/examples/border.rs b/examples/border.rs new file mode 100644 index 000000000..1d9f681b7 --- /dev/null +++ b/examples/border.rs @@ -0,0 +1,27 @@ +use dioxus::prelude::*; + +fn main() { + rink::launch(app); +} + +fn app(cx: Scope) -> Element { + let (radius, set_radius) = use_state(&cx, || 0); + + cx.render(rsx! { + div { + width: "100%", + height: "100%", + justify_content: "center", + align_items: "center", + background_color: "hsl(248, 53%, 58%)", + onwheel: move |w| set_radius((radius + w.delta_y as i8).abs()), + + border_style: "solid none solid double", + border_width: "thick", + border_radius: "{radius}px", + border_color: "#0000FF #FF00FF #FF0000 #00FF00", + + "{radius}" + } + }) +} diff --git a/examples/color_test.rs b/examples/color_test.rs new file mode 100644 index 000000000..4aeefee8c --- /dev/null +++ b/examples/color_test.rs @@ -0,0 +1,48 @@ +use dioxus::prelude::*; + +fn main() { + // rink::launch(app); + rink::launch_cfg( + app, + rink::Config { + rendering_mode: rink::RenderingMode::Ansi, + }, + ); +} + +fn app(cx: Scope) -> Element { + let steps = 50; + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + flex_direction: "column", + (0..=steps).map(|x| + { + let hue = x as f32*360.0/steps as f32; + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + flex_direction: "row", + (0..=steps).map(|y| + { + let alpha = y as f32*100.0/steps as f32; + cx.render(rsx! { + div { + left: "{x}px", + top: "{y}px", + width: "10%", + height: "100%", + background_color: "hsl({hue}, 100%, 50%, {alpha}%)", + } + }) + } + ) + } + }) + } + ) + } + }) +} diff --git a/examples/text.rs b/examples/text.rs index db49cd733..a38dc0b8d 100644 --- a/examples/text.rs +++ b/examples/text.rs @@ -2,9 +2,17 @@ use dioxus::prelude::*; fn main() { rink::launch(app); + // rink::launch_cfg( + // app, + // rink::Config { + // rendering_mode: rink::RenderingMode::Ansi, + // }, + // ) } fn app(cx: Scope) -> Element { + let (alpha, set_alpha) = use_state(&cx, || 100); + cx.render(rsx! { div { width: "100%", @@ -13,7 +21,9 @@ fn app(cx: Scope) -> Element { // justify_content: "center", // align_items: "center", // flex_direction: "row", - // background_color: "red", + onwheel: move |evt| { + set_alpha((alpha + evt.data.delta_y as i64).min(100).max(0)); + }, p { background_color: "black", @@ -21,6 +31,7 @@ fn app(cx: Scope) -> Element { justify_content: "center", align_items: "center", // height: "10%", + color: "green", "hi" "hi" "hi" @@ -53,13 +64,6 @@ fn app(cx: Scope) -> Element { "zib" "zib" "zib" - "zib" - "zib" - "zib" - "zib" - "zib" - "zib" - "zib" } p { background_color: "yellow", @@ -77,6 +81,47 @@ fn app(cx: Scope) -> Element { background_color: "cyan", "asd" } + div { + font_weight: "bold", + color: "#666666", + p { + "bold" + } + p { + font_weight: "normal", + " normal" + } + } + p { + font_style: "italic", + color: "red", + "italic" + } + p { + text_decoration: "underline", + color: "rgba(255, 255, 255)", + "underline" + } + p { + text_decoration: "line-through", + color: "hsla(10, 100%, 70%)", + "line-through" + } + div{ + position: "absolute", + top: "1px", + background_color: "rgba(255, 0, 0, 50%)", + width: "100%", + p { + color: "rgba(255, 255, 255, {alpha}%)", + background_color: "rgba(100, 100, 100, {alpha}%)", + "rgba(255, 255, 255, {alpha}%)" + } + p { + color: "rgba(255, 255, 255, 100%)", + "rgba(255, 255, 255, 100%)" + } + } } }) } diff --git a/src/attributes.rs b/src/attributes.rs index d84779406..a1ffcb5ea 100644 --- a/src/attributes.rs +++ b/src/attributes.rs @@ -30,15 +30,97 @@ */ use stretch2::{prelude::*, style::PositionType, style::Style}; -use tui::style::Style as TuiStyle; + +use crate::style::{RinkColor, RinkStyle}; pub struct StyleModifer { pub style: Style, - pub tui_style: TuiStyle, + pub tui_style: RinkStyle, + pub tui_modifier: TuiModifier, } -enum TuiModifier { - Text, +#[derive(Default)] +pub struct TuiModifier { + pub borders: Borders, +} + +#[derive(Default)] +pub struct Borders { + pub top: BorderEdge, + pub right: BorderEdge, + pub bottom: BorderEdge, + pub left: BorderEdge, +} + +impl Borders { + fn slice(&mut self) -> [&mut BorderEdge; 4] { + [ + &mut self.top, + &mut self.right, + &mut self.bottom, + &mut self.left, + ] + } +} + +pub struct BorderEdge { + pub color: Option, + pub style: BorderStyle, + pub width: UnitSystem, + pub radius: UnitSystem, +} + +impl Default for BorderEdge { + fn default() -> Self { + Self { + color: None, + style: BorderStyle::NONE, + width: UnitSystem::Point(0.0), + radius: UnitSystem::Point(0.0), + } + } +} + +#[derive(Clone, Copy)] +pub enum BorderStyle { + DOTTED, + DASHED, + SOLID, + DOUBLE, + GROOVE, + RIDGE, + INSET, + OUTSET, + HIDDEN, + NONE, +} + +impl BorderStyle { + pub fn symbol_set(&self) -> Option { + use tui::symbols::line::*; + const DASHED: Set = Set { + horizontal: "╌", + vertical: "╎", + ..NORMAL + }; + const DOTTED: Set = Set { + horizontal: "┈", + vertical: "┊", + ..NORMAL + }; + match self { + BorderStyle::DOTTED => Some(DOTTED), + BorderStyle::DASHED => Some(DASHED), + BorderStyle::SOLID => Some(NORMAL), + BorderStyle::DOUBLE => Some(DOUBLE), + BorderStyle::GROOVE => Some(NORMAL), + BorderStyle::RIDGE => Some(NORMAL), + BorderStyle::INSET => Some(NORMAL), + BorderStyle::OUTSET => Some(NORMAL), + BorderStyle::HIDDEN => None, + BorderStyle::NONE => None, + } + } } /// applies the entire html namespace defined in dioxus-html @@ -117,7 +199,9 @@ pub fn apply_attributes( "clip" => {} "color" => { - // text color + if let Ok(c) = value.parse() { + style.tui_style.fg.replace(c); + } } "column-count" @@ -170,14 +254,11 @@ pub fn apply_attributes( | "font-weight" => apply_font(name, value, style), "height" => { - if value.ends_with("%") { - if let Ok(pct) = value.trim_end_matches("%").parse::() { - style.style.size.height = Dimension::Percent(pct / 100.0); - } - } else if value.ends_with("px") { - if let Ok(px) = value.trim_end_matches("px").parse::() { - style.style.size.height = Dimension::Points(px); - } + if let Some(v) = parse_value(value){ + style.style.size.height = match v { + UnitSystem::Percent(v)=> Dimension::Percent(v/100.0), + UnitSystem::Point(v)=> Dimension::Points(v), + }; } } "justify-content" => { @@ -286,14 +367,11 @@ pub fn apply_attributes( "visibility" => {} "white-space" => {} "width" => { - if value.ends_with("%") { - if let Ok(pct) = value.trim_end_matches("%").parse::() { - style.style.size.width = Dimension::Percent(pct / 100.0); - } - } else if value.ends_with("px") { - if let Ok(px) = value.trim_end_matches("px").parse::() { - style.style.size.width = Dimension::Points(px); - } + if let Some(v) = parse_value(value){ + style.style.size.width = match v { + UnitSystem::Percent(v)=> Dimension::Percent(v/100.0), + UnitSystem::Point(v)=> Dimension::Points(v), + }; } } "word-break" => {} @@ -304,7 +382,8 @@ pub fn apply_attributes( } } -enum UnitSystem { +#[derive(Clone, Copy)] +pub enum UnitSystem { Percent(f32), Point(f32), } @@ -316,8 +395,8 @@ fn parse_value(value: &str) -> Option { } else { None } - } else if value.ends_with("%") { - if let Ok(pct) = value.trim_end_matches("%").parse::() { + } else if value.ends_with('%') { + if let Ok(pct) = value.trim_end_matches('%').parse::() { Some(UnitSystem::Percent(pct)) } else { None @@ -344,7 +423,6 @@ fn apply_overflow(name: &str, value: &str, style: &mut StyleModifer) { } fn apply_display(_name: &str, value: &str, style: &mut StyleModifer) { - use stretch2::style::Display; style.style.display = match value { "flex" => Display::Flex, "block" => Display::None, @@ -376,40 +454,9 @@ fn apply_display(_name: &str, value: &str, style: &mut StyleModifer) { fn apply_background(name: &str, value: &str, style: &mut StyleModifer) { match name { "background-color" => { - use tui::style::Color; - match value { - "red" => style.tui_style.bg.replace(Color::Red), - "green" => style.tui_style.bg.replace(Color::Green), - "blue" => style.tui_style.bg.replace(Color::Blue), - "yellow" => style.tui_style.bg.replace(Color::Yellow), - "cyan" => style.tui_style.bg.replace(Color::Cyan), - "magenta" => style.tui_style.bg.replace(Color::Magenta), - "white" => style.tui_style.bg.replace(Color::White), - "black" => style.tui_style.bg.replace(Color::Black), - _ => { - if value.len() == 7 { - let mut values = [0, 0, 0]; - let mut color_ok = true; - for i in 0..values.len() { - if let Ok(v) = - u8::from_str_radix(&value[(1 + 2 * i)..(1 + 2 * (i + 1))], 16) - { - values[i] = v; - } else { - color_ok = false; - } - } - if color_ok { - let color = Color::Rgb(values[0], values[1], values[2]); - style.tui_style.bg.replace(color) - } else { - None - } - } else { - None - } - } - }; + if let Ok(c) = value.parse() { + style.tui_style.bg.replace(c); + } } "background" => {} "background-attachment" => {} @@ -423,17 +470,71 @@ fn apply_background(name: &str, value: &str, style: &mut StyleModifer) { } } -fn apply_border(name: &str, value: &str, _style: &mut StyleModifer) { +fn apply_border(name: &str, value: &str, style: &mut StyleModifer) { + fn parse_border_style(v: &str) -> BorderStyle { + match v { + "dotted" => BorderStyle::DOTTED, + "dashed" => BorderStyle::DASHED, + "solid" => BorderStyle::SOLID, + "double" => BorderStyle::DOUBLE, + "groove" => BorderStyle::GROOVE, + "ridge" => BorderStyle::RIDGE, + "inset" => BorderStyle::INSET, + "outset" => BorderStyle::OUTSET, + "none" => BorderStyle::NONE, + "hidden" => BorderStyle::HIDDEN, + _ => todo!(), + } + } match name { "border" => {} "border-bottom" => {} - "border-bottom-color" => {} - "border-bottom-left-radius" => {} - "border-bottom-right-radius" => {} - "border-bottom-style" => {} - "border-bottom-width" => {} + "border-bottom-color" => { + if let Ok(c) = value.parse() { + style.tui_modifier.borders.bottom.color = Some(c); + } + } + "border-bottom-left-radius" => { + if let Some(v) = parse_value(value) { + style.tui_modifier.borders.left.radius = v; + } + } + "border-bottom-right-radius" => { + if let Some(v) = parse_value(value) { + style.tui_modifier.borders.right.radius = v; + } + } + "border-bottom-style" => { + style.tui_modifier.borders.bottom.style = parse_border_style(value) + } + "border-bottom-width" => { + if let Some(v) = parse_value(value) { + style.tui_modifier.borders.bottom.width = v; + } + } "border-collapse" => {} - "border-color" => {} + "border-color" => { + let values: Vec<_> = value.split(' ').collect(); + if values.len() == 1 { + if let Ok(c) = values[0].parse() { + style + .tui_modifier + .borders + .slice() + .iter_mut() + .for_each(|b| b.color = Some(c)); + } + } else { + for (v, b) in values + .into_iter() + .zip(style.tui_modifier.borders.slice().iter_mut()) + { + if let Ok(c) = v.parse() { + b.color = Some(c); + } + } + } + } "border-image" => {} "border-image-outset" => {} "border-image-repeat" => {} @@ -441,28 +542,116 @@ fn apply_border(name: &str, value: &str, _style: &mut StyleModifer) { "border-image-source" => {} "border-image-width" => {} "border-left" => {} - "border-left-color" => {} - "border-left-style" => {} - "border-left-width" => {} - "border-radius" => {} - "border-right" => {} - "border-right-color" => {} - "border-right-style" => {} - "border-right-width" => {} - "border-spacing" => {} - "border-style" => {} - "border-top" => {} - "border-top-color" => {} - "border-top-left-radius" => {} - "border-top-right-radius" => {} - "border-top-style" => {} - "border-top-width" => {} - "border-width" => { - if let Ok(_px) = value.trim_end_matches("px").parse::() { - // tuistyle = px; + "border-left-color" => { + if let Ok(c) = value.parse() { + style.tui_modifier.borders.left.color = Some(c); } } - _ => {} + "border-left-style" => style.tui_modifier.borders.left.style = parse_border_style(value), + "border-left-width" => { + if let Some(v) = parse_value(value) { + style.tui_modifier.borders.left.width = v; + } + } + "border-radius" => { + let values: Vec<_> = value.split(' ').collect(); + if values.len() == 1 { + if let Some(r) = parse_value(values[0]) { + style + .tui_modifier + .borders + .slice() + .iter_mut() + .for_each(|b| b.radius = r); + } + } else { + for (v, b) in values + .into_iter() + .zip(style.tui_modifier.borders.slice().iter_mut()) + { + if let Some(r) = parse_value(v) { + b.radius = r; + } + } + } + } + "border-right" => {} + "border-right-color" => { + if let Ok(c) = value.parse() { + style.tui_modifier.borders.right.color = Some(c); + } + } + "border-right-style" => style.tui_modifier.borders.right.style = parse_border_style(value), + "border-right-width" => { + if let Some(v) = parse_value(value) { + style.tui_modifier.borders.right.width = v; + } + } + "border-spacing" => {} + "border-style" => { + let values: Vec<_> = value.split(' ').collect(); + if values.len() == 1 { + let border_style = parse_border_style(values[0]); + style + .tui_modifier + .borders + .slice() + .iter_mut() + .for_each(|b| b.style = border_style); + } else { + for (v, b) in values + .into_iter() + .zip(style.tui_modifier.borders.slice().iter_mut()) + { + b.style = parse_border_style(v); + } + } + } + "border-top" => {} + "border-top-color" => { + if let Ok(c) = value.parse() { + style.tui_modifier.borders.top.color = Some(c); + } + } + "border-top-left-radius" => { + if let Some(v) = parse_value(value) { + style.tui_modifier.borders.left.radius = v; + } + } + "border-top-right-radius" => { + if let Some(v) = parse_value(value) { + style.tui_modifier.borders.right.radius = v; + } + } + "border-top-style" => style.tui_modifier.borders.top.style = parse_border_style(value), + "border-top-width" => { + if let Some(v) = parse_value(value) { + style.tui_modifier.borders.top.width = v; + } + } + "border-width" => { + let values: Vec<_> = value.split(' ').collect(); + if values.len() == 1 { + if let Some(w) = parse_value(values[0]) { + style + .tui_modifier + .borders + .slice() + .iter_mut() + .for_each(|b| b.width = w); + } + } else { + for (v, b) in values + .into_iter() + .zip(style.tui_modifier.borders.slice().iter_mut()) + { + if let Some(w) = parse_value(v) { + b.width = w; + } + } + } + } + _ => (), } } @@ -516,14 +705,11 @@ fn apply_flex(name: &str, value: &str, style: &mut StyleModifer) { }; } "flex-basis" => { - if value.ends_with("%") { - if let Ok(pct) = value.trim_end_matches("%").parse::() { - style.style.flex_basis = Dimension::Percent(pct / 100.0); - } - } else if value.ends_with("px") { - if let Ok(px) = value.trim_end_matches("px").parse::() { - style.style.flex_basis = Dimension::Points(px); - } + if let Some(v) = parse_value(value) { + style.style.flex_basis = match v { + UnitSystem::Percent(v) => Dimension::Percent(v / 100.0), + UnitSystem::Point(v) => Dimension::Points(v), + }; } } "flex-flow" => {} @@ -550,8 +736,27 @@ fn apply_flex(name: &str, value: &str, style: &mut StyleModifer) { } } -fn apply_font(_name: &str, _value: &str, _style: &mut StyleModifer) { - todo!() +fn apply_font(name: &str, value: &str, style: &mut StyleModifer) { + use tui::style::Modifier; + match name { + "font" => (), + "font-family" => (), + "font-size" => (), + "font-size-adjust" => (), + "font-stretch" => (), + "font-style" => match value { + "italic" => style.tui_style = style.tui_style.add_modifier(Modifier::ITALIC), + "oblique" => style.tui_style = style.tui_style.add_modifier(Modifier::ITALIC), + _ => (), + }, + "font-variant" => todo!(), + "font-weight" => match value { + "bold" => style.tui_style = style.tui_style.add_modifier(Modifier::BOLD), + "normal" => style.tui_style = style.tui_style.remove_modifier(Modifier::BOLD), + _ => (), + }, + _ => (), + } } fn apply_padding(name: &str, value: &str, style: &mut StyleModifer) { @@ -587,8 +792,34 @@ fn apply_padding(name: &str, value: &str, style: &mut StyleModifer) { } } -fn apply_text(_name: &str, _value: &str, _style: &mut StyleModifer) { - todo!() +fn apply_text(name: &str, value: &str, style: &mut StyleModifer) { + use tui::style::Modifier; + + match name { + "text-align" => todo!(), + "text-align-last" => todo!(), + "text-decoration" | "text-decoration-line" => { + for v in value.split(' ') { + match v { + "line-through" => { + style.tui_style = style.tui_style.add_modifier(Modifier::CROSSED_OUT) + } + "underline" => { + style.tui_style = style.tui_style.add_modifier(Modifier::UNDERLINED) + } + _ => (), + } + } + } + "text-decoration-color" => todo!(), + "text-decoration-style" => todo!(), + "text-indent" => todo!(), + "text-justify" => todo!(), + "text-overflow" => todo!(), + "text-shadow" => todo!(), + "text-transform" => todo!(), + _ => todo!(), + } } fn apply_transform(_name: &str, _value: &str, _style: &mut StyleModifer) { diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 000000000..46a0926b8 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,20 @@ +#[derive(Default, Clone, Copy)] +pub struct Config { + pub rendering_mode: RenderingMode, +} + +#[derive(Clone, Copy)] +pub enum RenderingMode { + /// only 16 colors by accessed by name, no alpha support + BaseColors, + /// 8 bit colors, will be downsampled from rgb colors + Ansi, + /// 24 bit colors, most terminals support this + Rgb, +} + +impl Default for RenderingMode { + fn default() -> Self { + RenderingMode::Rgb + } +} diff --git a/src/layout.rs b/src/layout.rs index 94f750990..53b406797 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -1,10 +1,10 @@ use dioxus::core::*; use std::collections::HashMap; -use tui::style::Style as TuiStyle; use crate::{ attributes::{apply_attributes, StyleModifer}, - TuiNode, + style::RinkStyle, + TuiModifier, TuiNode, }; /* @@ -41,7 +41,8 @@ pub fn collect_layout<'a>( id, TuiNode { node, - block_style: tui::style::Style::default(), + block_style: RinkStyle::default(), + tui_modifier: TuiModifier::default(), layout: layout.new_node(style, &[]).unwrap(), }, ); @@ -50,7 +51,8 @@ pub fn collect_layout<'a>( // gather up all the styles from the attribute list let mut modifier = StyleModifer { style: Style::default(), - tui_style: TuiStyle::default(), + tui_style: RinkStyle::default(), + tui_modifier: TuiModifier::default(), }; for &Attribute { name, value, .. } in el.attributes { @@ -86,6 +88,7 @@ pub fn collect_layout<'a>( TuiNode { node, block_style: modifier.tui_style, + tui_modifier: modifier.tui_modifier, layout: layout.new_node(modifier.style, &child_layout).unwrap(), }, ); diff --git a/src/lib.rs b/src/lib.rs index 145e59d1b..16d861435 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,19 +13,28 @@ use std::{ time::{Duration, Instant}, }; use stretch2::{prelude::Size, Stretch}; -use tui::{backend::CrosstermBackend, style::Style as TuiStyle, Terminal}; +use style::RinkStyle; +use tui::{backend::CrosstermBackend, Terminal}; mod attributes; +mod config; mod hooks; mod layout; mod render; +mod style; +mod widget; pub use attributes::*; +pub use config::*; pub use hooks::*; pub use layout::*; pub use render::*; pub fn launch(app: Component<()>) { + launch_cfg(app, Config::default()) +} + +pub fn launch_cfg(app: Component<()>, cfg: Config) { let mut dom = VirtualDom::new(app); let (tx, rx) = unbounded(); @@ -37,12 +46,13 @@ pub fn launch(app: Component<()>) { dom.rebuild(); - render_vdom(&mut dom, tx, handler).unwrap(); + render_vdom(&mut dom, tx, handler, cfg).unwrap(); } pub struct TuiNode<'a> { pub layout: stretch2::node::Node, - pub block_style: TuiStyle, + pub block_style: RinkStyle, + pub tui_modifier: TuiModifier, pub node: &'a VNode<'a>, } @@ -50,6 +60,7 @@ pub fn render_vdom( vdom: &mut VirtualDom, ctx: UnboundedSender, handler: RinkInputHandler, + cfg: Config, ) -> Result<()> { // Setup input handling let (tx, mut rx) = unbounded(); @@ -132,7 +143,15 @@ pub fn render_vdom( // resolve events before rendering events = handler.get_events(vdom, &layout, &mut nodes, root_node); - render::render_vnode(frame, &layout, &mut nodes, vdom, root_node); + render::render_vnode( + frame, + &layout, + &mut nodes, + vdom, + root_node, + &RinkStyle::default(), + cfg, + ); assert!(nodes.is_empty()); })?; diff --git a/src/render.rs b/src/render.rs index c997d5302..59de80460 100644 --- a/src/render.rs +++ b/src/render.rs @@ -5,15 +5,15 @@ use stretch2::{ prelude::{Layout, Size}, Stretch, }; -use tui::{ - backend::CrosstermBackend, - buffer::Buffer, - layout::Rect, - style::Style as TuiStyle, - widgets::{Block, Widget}, +use tui::{backend::CrosstermBackend, layout::Rect}; + +use crate::{ + style::{RinkColor, RinkStyle}, + widget::{RinkBuffer, RinkCell, RinkWidget, WidgetWithContext}, + BorderEdge, BorderStyle, Config, TuiNode, UnitSystem, }; -use crate::TuiNode; +const RADIUS_MULTIPLIER: [f32; 2] = [1.0, 0.5]; pub fn render_vnode<'a>( frame: &mut tui::Frame>, @@ -21,11 +21,14 @@ pub fn render_vnode<'a>( layouts: &mut HashMap>, vdom: &'a VirtualDom, node: &'a VNode<'a>, + // this holds the accumulated syle state for styled text rendering + style: &RinkStyle, + cfg: Config, ) { match node { VNode::Fragment(f) => { for child in f.children { - render_vnode(frame, layout, layouts, vdom, child); + render_vnode(frame, layout, layouts, vdom, child, style, cfg); } return; } @@ -33,7 +36,7 @@ pub fn render_vnode<'a>( VNode::Component(vcomp) => { let idx = vcomp.scope.get().unwrap(); let new_node = vdom.get_scope(idx).unwrap().root_node(); - render_vnode(frame, layout, layouts, vdom, new_node); + render_vnode(frame, layout, layouts, vdom, new_node, style, cfg); return; } @@ -43,7 +46,7 @@ pub fn render_vnode<'a>( } let id = node.try_mounted_id().unwrap(); - let node = layouts.remove(&id).unwrap(); + let mut node = layouts.remove(&id).unwrap(); let Layout { location, size, .. } = layout.layout(node.layout).unwrap(); @@ -55,37 +58,46 @@ pub fn render_vnode<'a>( #[derive(Default)] struct Label<'a> { text: &'a str, + style: RinkStyle, } - impl<'a> Widget for Label<'a> { - fn render(self, area: Rect, buf: &mut Buffer) { - buf.set_string(area.left(), area.top(), self.text, TuiStyle::default()); + impl<'a> RinkWidget for Label<'a> { + fn render(self, area: Rect, mut buf: RinkBuffer) { + for (i, c) in self.text.char_indices() { + let mut new_cell = RinkCell::default(); + new_cell.set_style(self.style); + new_cell.symbol = c.to_string(); + buf.set(area.left() + i as u16, area.top(), &new_cell); + } } } - // let s = Span::raw(t.text); - - // Block::default(). - - let label = Label { text: t.text }; + let label = Label { + text: t.text, + style: *style, + }; let area = Rect::new(*x as u16, *y as u16, *width as u16, *height as u16); // the renderer will panic if a node is rendered out of range even if the size is zero if area.width > 0 && area.height > 0 { - frame.render_widget(label, area); + frame.render_widget(WidgetWithContext::new(label, cfg), area); } } VNode::Element(el) => { - let block = Block::default().style(node.block_style); let area = Rect::new(*x as u16, *y as u16, *width as u16, *height as u16); + let mut new_style = node.block_style.merge(*style); + node.block_style = new_style; + // the renderer will panic if a node is rendered out of range even if the size is zero if area.width > 0 && area.height > 0 { - frame.render_widget(block, area); + frame.render_widget(WidgetWithContext::new(node, cfg), area); } + // do not pass background color to children + new_style.bg = None; for el in el.children { - render_vnode(frame, layout, layouts, vdom, el); + render_vnode(frame, layout, layouts, vdom, el, &new_style, cfg); } } VNode::Fragment(_) => todo!(), @@ -93,3 +105,338 @@ pub fn render_vnode<'a>( VNode::Placeholder(_) => todo!(), } } + +impl<'a> RinkWidget for TuiNode<'a> { + fn render(self, area: Rect, mut buf: RinkBuffer<'_>) { + use tui::symbols::line::*; + + enum Direction { + Left, + Right, + Up, + Down, + } + + fn draw( + buf: &mut RinkBuffer, + points_history: [[i32; 2]; 3], + symbols: &Set, + pos: [u16; 2], + color: &Option, + ) { + let [before, current, after] = points_history; + let start_dir = match [before[0] - current[0], before[1] - current[1]] { + [1, 0] => Direction::Right, + [-1, 0] => Direction::Left, + [0, 1] => Direction::Down, + [0, -1] => Direction::Up, + [a, b] => { + panic!( + "draw({:?} {:?} {:?}) {}, {} no cell adjacent", + before, current, after, a, b + ) + } + }; + let end_dir = match [after[0] - current[0], after[1] - current[1]] { + [1, 0] => Direction::Right, + [-1, 0] => Direction::Left, + [0, 1] => Direction::Down, + [0, -1] => Direction::Up, + [a, b] => { + panic!( + "draw({:?} {:?} {:?}) {}, {} no cell adjacent", + before, current, after, a, b + ) + } + }; + + let mut new_cell = RinkCell::default(); + if let Some(c) = color { + new_cell.fg = *c; + } + new_cell.symbol = match [start_dir, end_dir] { + [Direction::Down, Direction::Up] => symbols.vertical, + [Direction::Down, Direction::Right] => symbols.top_left, + [Direction::Down, Direction::Left] => symbols.top_right, + [Direction::Up, Direction::Down] => symbols.vertical, + [Direction::Up, Direction::Right] => symbols.bottom_left, + [Direction::Up, Direction::Left] => symbols.bottom_right, + [Direction::Right, Direction::Left] => symbols.horizontal, + [Direction::Right, Direction::Up] => symbols.bottom_left, + [Direction::Right, Direction::Down] => symbols.top_left, + [Direction::Left, Direction::Up] => symbols.bottom_right, + [Direction::Left, Direction::Right] => symbols.horizontal, + [Direction::Left, Direction::Down] => symbols.top_right, + _ => panic!( + "{:?} {:?} {:?} cannont connect cell to itself", + before, current, after + ), + } + .to_string(); + buf.set( + (current[0] + pos[0] as i32) as u16, + (current[1] + pos[1] as i32) as u16, + &new_cell, + ); + } + + fn draw_arc( + pos: [u16; 2], + starting_angle: f32, + arc_angle: f32, + radius: f32, + symbols: &Set, + buf: &mut RinkBuffer, + color: &Option, + ) { + if radius < 0.0 { + return; + } + + let num_points = (radius * arc_angle) as i32; + let starting_point = [ + (starting_angle.cos() * (radius * RADIUS_MULTIPLIER[0])) as i32, + (starting_angle.sin() * (radius * RADIUS_MULTIPLIER[1])) as i32, + ]; + // keep track of the last 3 point to allow filling diagonals + let mut points_history = [ + [0, 0], + { + // change the x or y value based on which one is changing quicker + let ddx = -starting_angle.sin(); + let ddy = starting_angle.cos(); + if ddx.abs() > ddy.abs() { + [starting_point[0] - ddx.signum() as i32, starting_point[1]] + } else { + [starting_point[0], starting_point[1] - ddy.signum() as i32] + } + }, + starting_point, + ]; + + for i in 1..=num_points { + let angle = (i as f32 / num_points as f32) * arc_angle + starting_angle; + let x = angle.cos() * radius * RADIUS_MULTIPLIER[0]; + let y = angle.sin() * radius * RADIUS_MULTIPLIER[1]; + let new = [x as i32, y as i32]; + + if new != points_history[2] { + points_history = [points_history[1], points_history[2], new]; + + let dx = points_history[2][0] - points_history[1][0]; + let dy = points_history[2][1] - points_history[1][1]; + // fill diagonals + if dx != 0 && dy != 0 { + let connecting_point = match [dx, dy] { + [1, 1] => [points_history[1][0] + 1, points_history[1][1]], + [1, -1] => [points_history[1][0], points_history[1][1] - 1], + [-1, 1] => [points_history[1][0], points_history[1][1] + 1], + [-1, -1] => [points_history[1][0] - 1, points_history[1][1]], + _ => todo!(), + }; + draw( + buf, + [points_history[0], points_history[1], connecting_point], + symbols, + pos, + color, + ); + points_history = [points_history[1], connecting_point, points_history[2]]; + } + + draw(buf, points_history, symbols, pos, color); + } + } + + points_history = [points_history[1], points_history[2], { + // change the x or y value based on which one is changing quicker + let ddx = -(starting_angle + arc_angle).sin(); + let ddy = (starting_angle + arc_angle).cos(); + if ddx.abs() > ddy.abs() { + [ + points_history[2][0] + ddx.signum() as i32, + points_history[2][1], + ] + } else { + [ + points_history[2][0], + points_history[2][1] + ddy.signum() as i32, + ] + } + }]; + + draw(buf, points_history, symbols, pos, color); + } + + fn get_radius(border: &BorderEdge, area: Rect) -> f32 { + match border.style { + BorderStyle::HIDDEN => 0.0, + BorderStyle::NONE => 0.0, + _ => match border.radius { + UnitSystem::Percent(p) => p * area.width as f32 / 100.0, + UnitSystem::Point(p) => p, + } + .abs() + .min((area.width as f32 / RADIUS_MULTIPLIER[0]) / 2.0) + .min((area.height as f32 / RADIUS_MULTIPLIER[1]) / 2.0), + } + } + + if area.area() == 0 { + return; + } + + // todo: only render inside borders + for x in area.left()..area.right() { + for y in area.top()..area.bottom() { + let mut new_cell = RinkCell::default(); + if let Some(c) = self.block_style.bg { + new_cell.bg = c; + } + buf.set(x, y, &new_cell); + } + } + + let borders = self.tui_modifier.borders; + + let last_edge = &borders.left; + let current_edge = &borders.top; + if let Some(symbols) = current_edge.style.symbol_set() { + // the radius for the curve between this line and the next + let r = get_radius(current_edge, area); + let radius = [ + (r * RADIUS_MULTIPLIER[0]) as u16, + (r * RADIUS_MULTIPLIER[1]) as u16, + ]; + // the radius for the curve between this line and the last + let last_r = get_radius(last_edge, area); + let last_radius = [ + (last_r * RADIUS_MULTIPLIER[0]) as u16, + (last_r * RADIUS_MULTIPLIER[1]) as u16, + ]; + let color = current_edge.color.or(self.block_style.fg); + let mut new_cell = RinkCell::default(); + if let Some(c) = color { + new_cell.fg = c; + } + for x in (area.left() + last_radius[0] + 1)..(area.right() - radius[0]) { + new_cell.symbol = symbols.horizontal.to_string(); + buf.set(x, area.top(), &new_cell); + } + draw_arc( + [area.right() - radius[0] - 1, area.top() + radius[1]], + std::f32::consts::FRAC_PI_2 * 3.0, + std::f32::consts::FRAC_PI_2, + r, + &symbols, + &mut buf, + &color, + ); + } + + let last_edge = &borders.top; + let current_edge = &borders.right; + if let Some(symbols) = current_edge.style.symbol_set() { + // the radius for the curve between this line and the next + let r = get_radius(current_edge, area); + let radius = [ + (r * RADIUS_MULTIPLIER[0]) as u16, + (r * RADIUS_MULTIPLIER[1]) as u16, + ]; + // the radius for the curve between this line and the last + let last_r = get_radius(last_edge, area); + let last_radius = [ + (last_r * RADIUS_MULTIPLIER[0]) as u16, + (last_r * RADIUS_MULTIPLIER[1]) as u16, + ]; + let color = current_edge.color.or(self.block_style.fg); + let mut new_cell = RinkCell::default(); + if let Some(c) = color { + new_cell.fg = c; + } + for y in (area.top() + last_radius[1] + 1)..(area.bottom() - radius[1]) { + new_cell.symbol = symbols.vertical.to_string(); + buf.set(area.right() - 1, y, &new_cell); + } + draw_arc( + [area.right() - radius[0] - 1, area.bottom() - radius[1] - 1], + 0.0, + std::f32::consts::FRAC_PI_2, + r, + &symbols, + &mut buf, + &color, + ); + } + + let last_edge = &borders.right; + let current_edge = &borders.bottom; + if let Some(symbols) = current_edge.style.symbol_set() { + // the radius for the curve between this line and the next + let r = get_radius(current_edge, area); + let radius = [ + (r * RADIUS_MULTIPLIER[0]) as u16, + (r * RADIUS_MULTIPLIER[1]) as u16, + ]; + // the radius for the curve between this line and the last + let last_r = get_radius(last_edge, area); + let last_radius = [ + (last_r * RADIUS_MULTIPLIER[0]) as u16, + (last_r * RADIUS_MULTIPLIER[1]) as u16, + ]; + let color = current_edge.color.or(self.block_style.fg); + let mut new_cell = RinkCell::default(); + if let Some(c) = color { + new_cell.fg = c; + } + for x in (area.left() + radius[0])..(area.right() - last_radius[0] - 1) { + new_cell.symbol = symbols.horizontal.to_string(); + buf.set(x, area.bottom() - 1, &new_cell); + } + draw_arc( + [area.left() + radius[0], area.bottom() - radius[1] - 1], + std::f32::consts::FRAC_PI_2, + std::f32::consts::FRAC_PI_2, + r, + &symbols, + &mut buf, + &color, + ); + } + + let last_edge = &borders.bottom; + let current_edge = &borders.left; + if let Some(symbols) = current_edge.style.symbol_set() { + // the radius for the curve between this line and the next + let r = get_radius(current_edge, area); + let radius = [ + (r * RADIUS_MULTIPLIER[0]) as u16, + (r * RADIUS_MULTIPLIER[1]) as u16, + ]; + // the radius for the curve between this line and the last + let last_r = get_radius(last_edge, area); + let last_radius = [ + (last_r * RADIUS_MULTIPLIER[0]) as u16, + (last_r * RADIUS_MULTIPLIER[1]) as u16, + ]; + let color = current_edge.color.or(self.block_style.fg); + let mut new_cell = RinkCell::default(); + if let Some(c) = color { + new_cell.fg = c; + } + for y in (area.top() + radius[1])..(area.bottom() - last_radius[1] - 1) { + new_cell.symbol = symbols.vertical.to_string(); + buf.set(area.left(), y, &new_cell); + } + draw_arc( + [area.left() + radius[0], area.top() + radius[1]], + std::f32::consts::PI, + std::f32::consts::FRAC_PI_2, + r, + &symbols, + &mut buf, + &color, + ); + } + } +} diff --git a/src/style.rs b/src/style.rs new file mode 100644 index 000000000..943c88aed --- /dev/null +++ b/src/style.rs @@ -0,0 +1,447 @@ +use std::{num::ParseFloatError, str::FromStr}; + +use tui::style::{Color, Modifier, Style}; + +use crate::RenderingMode; + +#[derive(Clone, Copy, Debug)] +pub struct RinkColor { + pub color: Color, + pub alpha: f32, +} + +impl Default for RinkColor { + fn default() -> Self { + Self { + color: Color::Black, + alpha: 0.0, + } + } +} + +impl RinkColor { + pub fn blend(self, other: Color) -> Color { + if self.color == Color::Reset { + Color::Reset + } else if self.alpha == 0.0 { + other + } else { + let [sr, sg, sb] = to_rgb(self.color); + let [or, og, ob] = to_rgb(other); + let (sr, sg, sb, sa) = ( + sr as f32 / 255.0, + sg as f32 / 255.0, + sb as f32 / 255.0, + self.alpha, + ); + let (or, og, ob) = (or as f32 / 255.0, og as f32 / 255.0, ob as f32 / 255.0); + Color::Rgb( + (255.0 * (sr * sa + or * (1.0 - sa))) as u8, + (255.0 * (sg * sa + og * (1.0 - sa))) as u8, + (255.0 * (sb * sa + ob * (1.0 - sa))) as u8, + ) + } + } +} + +fn parse_value( + v: &str, + current_max_output: f32, + required_max_output: f32, +) -> Result { + if let Some(stripped) = v.strip_suffix('%') { + Ok((stripped.trim().parse::()? / 100.0) * required_max_output) + } else { + Ok((v.trim().parse::()? / current_max_output) * required_max_output) + } +} + +pub struct ParseColorError; + +fn parse_hex(color: &str) -> Result { + let mut values = [0, 0, 0]; + let mut color_ok = true; + for i in 0..values.len() { + if let Ok(v) = u8::from_str_radix(&color[(1 + 2 * i)..(1 + 2 * (i + 1))], 16) { + values[i] = v; + } else { + color_ok = false; + } + } + if color_ok { + Ok(Color::Rgb(values[0], values[1], values[2])) + } else { + Err(ParseColorError) + } +} + +fn parse_rgb(color: &str) -> Result { + let mut values = [0, 0, 0]; + let mut color_ok = true; + for (v, i) in color.split(',').zip(0..values.len()) { + if let Ok(v) = parse_value(v.trim(), 255.0, 255.0) { + values[i] = v as u8; + } else { + color_ok = false; + } + } + if color_ok { + Ok(Color::Rgb(values[0], values[1], values[2])) + } else { + Err(ParseColorError) + } +} + +fn parse_hsl(color: &str) -> Result { + let mut values = [0.0, 0.0, 0.0]; + let mut color_ok = true; + for (v, i) in color.split(',').zip(0..values.len()) { + if let Ok(v) = parse_value(v.trim(), if i == 0 { 360.0 } else { 100.0 }, 1.0) { + values[i] = v; + } else { + color_ok = false; + } + } + if color_ok { + let [h, s, l] = values; + let rgb = if s == 0.0 { + [l as u8; 3] + } else { + fn hue_to_rgb(p: f32, q: f32, mut t: f32) -> f32 { + if t < 0.0 { + t += 1.0; + } + if t > 1.0 { + t -= 1.0; + } + if t < 1.0 / 6.0 { + p + (q - p) * 6.0 * t + } else if t < 1.0 / 2.0 { + q + } else if t < 2.0 / 3.0 { + p + (q - p) * (2.0 / 3.0 - t) * 6.0 + } else { + p + } + } + + let q = if l < 0.5 { + l * (1.0 + s) + } else { + l + s - l * s + }; + let p = 2.0 * l - q; + [ + (hue_to_rgb(p, q, h + 1.0 / 3.0) * 255.0) as u8, + (hue_to_rgb(p, q, h) * 255.0) as u8, + (hue_to_rgb(p, q, h - 1.0 / 3.0) * 255.0) as u8, + ] + }; + + Ok(Color::Rgb(rgb[0], rgb[1], rgb[2])) + } else { + Err(ParseColorError) + } +} + +impl FromStr for RinkColor { + type Err = ParseColorError; + + fn from_str(color: &str) -> Result { + match color { + "red" => Ok(RinkColor { + color: Color::Red, + alpha: 1.0, + }), + "black" => Ok(RinkColor { + color: Color::Black, + alpha: 1.0, + }), + "green" => Ok(RinkColor { + color: Color::Green, + alpha: 1.0, + }), + "yellow" => Ok(RinkColor { + color: Color::Yellow, + alpha: 1.0, + }), + "blue" => Ok(RinkColor { + color: Color::Blue, + alpha: 1.0, + }), + "magenta" => Ok(RinkColor { + color: Color::Magenta, + alpha: 1.0, + }), + "cyan" => Ok(RinkColor { + color: Color::Cyan, + alpha: 1.0, + }), + "gray" => Ok(RinkColor { + color: Color::Gray, + alpha: 1.0, + }), + "darkgray" => Ok(RinkColor { + color: Color::DarkGray, + alpha: 1.0, + }), + // light red does not exist + "orangered" => Ok(RinkColor { + color: Color::LightRed, + alpha: 1.0, + }), + "lightgreen" => Ok(RinkColor { + color: Color::LightGreen, + alpha: 1.0, + }), + "lightyellow" => Ok(RinkColor { + color: Color::LightYellow, + alpha: 1.0, + }), + "lightblue" => Ok(RinkColor { + color: Color::LightBlue, + alpha: 1.0, + }), + // light magenta does not exist + "orchid" => Ok(RinkColor { + color: Color::LightMagenta, + alpha: 1.0, + }), + "lightcyan" => Ok(RinkColor { + color: Color::LightCyan, + alpha: 1.0, + }), + "white" => Ok(RinkColor { + color: Color::White, + alpha: 1.0, + }), + _ => { + if color.len() == 7 && color.starts_with('#') { + parse_hex(color).map(|c| RinkColor { + color: c, + alpha: 1.0, + }) + } else if let Some(stripped) = color.strip_prefix("rgb(") { + let color_values = stripped.trim_end_matches(')'); + if color.matches(',').count() == 3 { + let (alpha, rgb_values) = + color_values.rsplit_once(',').ok_or(ParseColorError)?; + if let Ok(a) = alpha.parse() { + parse_rgb(rgb_values).map(|c| RinkColor { color: c, alpha: a }) + } else { + Err(ParseColorError) + } + } else { + parse_rgb(color_values).map(|c| RinkColor { + color: c, + alpha: 1.0, + }) + } + } else if let Some(stripped) = color.strip_prefix("rgba(") { + let color_values = stripped.trim_end_matches(')'); + if color.matches(',').count() == 3 { + let (rgb_values, alpha) = + color_values.rsplit_once(',').ok_or(ParseColorError)?; + if let Ok(a) = parse_value(alpha, 1.0, 1.0) { + parse_rgb(rgb_values).map(|c| RinkColor { color: c, alpha: a }) + } else { + Err(ParseColorError) + } + } else { + parse_rgb(color_values).map(|c| RinkColor { + color: c, + alpha: 1.0, + }) + } + } else if let Some(stripped) = color.strip_prefix("hsl(") { + let color_values = stripped.trim_end_matches(')'); + if color.matches(',').count() == 3 { + let (rgb_values, alpha) = + color_values.rsplit_once(',').ok_or(ParseColorError)?; + if let Ok(a) = parse_value(alpha, 1.0, 1.0) { + parse_hsl(rgb_values).map(|c| RinkColor { color: c, alpha: a }) + } else { + Err(ParseColorError) + } + } else { + parse_hsl(color_values).map(|c| RinkColor { + color: c, + alpha: 1.0, + }) + } + } else if let Some(stripped) = color.strip_prefix("hsla(") { + let color_values = stripped.trim_end_matches(')'); + if color.matches(',').count() == 3 { + let (rgb_values, alpha) = + color_values.rsplit_once(',').ok_or(ParseColorError)?; + if let Ok(a) = parse_value(alpha, 1.0, 1.0) { + parse_hsl(rgb_values).map(|c| RinkColor { color: c, alpha: a }) + } else { + Err(ParseColorError) + } + } else { + parse_hsl(color_values).map(|c| RinkColor { + color: c, + alpha: 1.0, + }) + } + } else { + Err(ParseColorError) + } + } + } + } +} + +fn to_rgb(c: Color) -> [u8; 3] { + match c { + Color::Black => [0, 0, 0], + Color::Red => [255, 0, 0], + Color::Green => [0, 128, 0], + Color::Yellow => [255, 255, 0], + Color::Blue => [0, 0, 255], + Color::Magenta => [255, 0, 255], + Color::Cyan => [0, 255, 255], + Color::Gray => [128, 128, 128], + Color::DarkGray => [169, 169, 169], + Color::LightRed => [255, 69, 0], + Color::LightGreen => [144, 238, 144], + Color::LightYellow => [255, 255, 224], + Color::LightBlue => [173, 216, 230], + Color::LightMagenta => [218, 112, 214], + Color::LightCyan => [224, 255, 255], + Color::White => [255, 255, 255], + Color::Rgb(r, g, b) => [r, g, b], + Color::Indexed(idx) => match idx { + 16..=231 => { + let v = idx - 16; + // add 3 to round up + let r = ((v as u16 / 36) * 255 + 3) / 5; + let g = (((v as u16 % 36) / 6) * 255 + 3) / 5; + let b = ((v as u16 % 6) * 255 + 3) / 5; + [r as u8, g as u8, b as u8] + } + 232..=255 => { + let l = (idx - 232) / 24; + [l; 3] + } + // rink will never generate these colors, but they might be on the screen from another program + _ => [0, 0, 0], + }, + Color::Reset => [0, 0, 0], + } +} + +pub fn convert(mode: RenderingMode, c: Color) -> Color { + if let Color::Reset = c { + c + } else { + match mode { + crate::RenderingMode::BaseColors => match c { + Color::Rgb(_, _, _) => panic!("cannot convert rgb color to base color"), + Color::Indexed(_) => panic!("cannot convert Ansi color to base color"), + _ => c, + }, + crate::RenderingMode::Rgb => { + let rgb = to_rgb(c); + Color::Rgb(rgb[0], rgb[1], rgb[2]) + } + crate::RenderingMode::Ansi => match c { + Color::Indexed(_) => c, + _ => { + let rgb = to_rgb(c); + // 16-231: 6 × 6 × 6 color cube + // 232-255: 23 step grayscale + if rgb[0] == rgb[1] && rgb[1] == rgb[2] { + let idx = 232 + (rgb[0] as u16 * 23 / 255) as u8; + Color::Indexed(idx) + } else { + let r = (rgb[0] as u16 * 5) / 255; + let g = (rgb[1] as u16 * 5) / 255; + let b = (rgb[2] as u16 * 5) / 255; + let idx = 16 + r * 36 + g * 6 + b; + Color::Indexed(idx as u8) + } + } + }, + } + } +} + +#[test] +fn rgb_to_ansi() { + for idx in 17..=231 { + let idxed = Color::Indexed(idx); + let rgb = to_rgb(idxed); + // gray scale colors have two equivelent repersentations + let color = Color::Rgb(rgb[0], rgb[1], rgb[2]); + let converted = convert(RenderingMode::Ansi, color); + if let Color::Indexed(i) = converted { + if rgb[0] != rgb[1] || rgb[1] != rgb[2] { + assert_eq!(idxed, converted); + } else { + assert!(i >= 232); + } + } else { + panic!("color is not indexed") + } + } + for idx in 232..=255 { + let idxed = Color::Indexed(idx); + let rgb = to_rgb(idxed); + assert!(rgb[0] == rgb[1] && rgb[1] == rgb[2]); + } +} + +#[derive(Clone, Copy)] +pub struct RinkStyle { + pub fg: Option, + pub bg: Option, + pub add_modifier: Modifier, + pub sub_modifier: Modifier, +} + +impl Default for RinkStyle { + fn default() -> Self { + Self { + fg: Some(RinkColor { + color: Color::White, + alpha: 1.0, + }), + bg: None, + add_modifier: Modifier::empty(), + sub_modifier: Modifier::empty(), + } + } +} + +impl RinkStyle { + pub fn add_modifier(mut self, m: Modifier) -> Self { + self.sub_modifier.remove(m); + self.add_modifier.insert(m); + self + } + + pub fn remove_modifier(mut self, m: Modifier) -> Self { + self.add_modifier.remove(m); + self.sub_modifier.insert(m); + self + } + + pub fn merge(mut self, other: RinkStyle) -> Self { + self.fg = self.fg.or(other.fg); + self.add_modifier(other.add_modifier) + .remove_modifier(other.sub_modifier) + } +} + +impl Into