Evan Almloff 3 лет назад
Родитель
Сommit
7a1d8f0532
10 измененных файлов с 821 добавлено и 277 удалено
  1. 0 3
      examples/border.rs
  2. 48 0
      examples/color_test.rs
  3. 30 4
      examples/text.rs
  4. 12 120
      src/attributes.rs
  5. 20 0
      src/config.rs
  6. 4 3
      src/layout.rs
  7. 15 4
      src/lib.rs
  8. 143 143
      src/render.rs
  9. 453 0
      src/style.rs
  10. 96 0
      src/widget.rs

+ 0 - 3
examples/border.rs

@@ -16,9 +16,6 @@ fn app(cx: Scope) -> Element {
             background_color: "hsl(248, 53%, 58%)",
             onwheel: move |w| set_radius((radius + w.delta_y as i8).abs()),
 
-            // the border can either be solid, double, thick, OR rounded
-            // if multable are set only the last style is appiled
-            // to skip a side set the style to none
             border_style: "solid none solid double",
             border_width: "thick",
             border_radius: "{radius}px",

+ 48 - 0
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}%)",
+                                        }
+                                    })
+                                }
+                            )
+                        }
+                    })
+                }
+            )
+        }
+    })
+}

+ 30 - 4
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"
@@ -73,7 +84,7 @@ fn app(cx: Scope) -> Element {
             div {
                 font_weight: "bold",
                 color: "#666666",
-                p{
+                p {
                     "bold"
                 }
                 p {
@@ -88,14 +99,29 @@ fn app(cx: Scope) -> Element {
             }
             p {
                 text_decoration: "underline",
-                color: "rgb(50, 100, 255)",
+                color: "rgba(255, 255, 255)",
                 "underline"
             }
             p {
                 text_decoration: "line-through",
-                color: "hsl(10, 100%, 70%)",
+                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%)"
+                }
+            }
         }
     })
 }

+ 12 - 120
src/attributes.rs

@@ -32,15 +32,17 @@
 use stretch2::{prelude::*, style::PositionType, style::Style};
 use tui::style::{Color, 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,
 }
 
 pub struct TuiModifier {
     // border arrays start at the top and proceed clockwise
-    pub border_colors: [Option<Color>; 4],
+    pub border_colors: [Option<RinkColor>; 4],
     pub border_types: [BorderType; 4],
     pub border_widths: [UnitSystem; 4],
     pub border_radi: [UnitSystem; 4],
@@ -71,116 +73,6 @@ impl Default for TuiModifier {
     }
 }
 
-fn parse_color(color: &str) -> Option<tui::style::Color> {
-    match color {
-        "red" => Some(Color::Red),
-        "green" => Some(Color::Green),
-        "blue" => Some(Color::Blue),
-        "yellow" => Some(Color::Yellow),
-        "cyan" => Some(Color::Cyan),
-        "magenta" => Some(Color::Magenta),
-        "white" => Some(Color::White),
-        "black" => Some(Color::Black),
-        _ => {
-            if color.len() == 7 && color.starts_with('#') {
-                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 {
-                    Some(Color::Rgb(values[0], values[1], values[2]))
-                } else {
-                    None
-                }
-            } else if color.starts_with("rgb(") {
-                let mut values = [0, 0, 0];
-                let mut color_ok = true;
-                for (v, i) in color[4..]
-                    .trim_end_matches(')')
-                    .split(',')
-                    .zip(0..values.len())
-                {
-                    if let Ok(v) = v.trim().parse() {
-                        values[i] = v;
-                    } else {
-                        color_ok = false;
-                    }
-                }
-                if color_ok {
-                    Some(Color::Rgb(values[0], values[1], values[2]))
-                } else {
-                    None
-                }
-            } else if color.starts_with("hsl(") {
-                let mut values = [0, 0, 0];
-                let mut color_ok = true;
-                for (v, i) in color[4..]
-                    .trim_end_matches(')')
-                    .split(',')
-                    .zip(0..values.len())
-                {
-                    if let Ok(v) = v.trim_end_matches('%').trim().parse() {
-                        values[i] = v;
-                    } else {
-                        color_ok = false;
-                    }
-                }
-                if color_ok {
-                    let [h, s, l] = [
-                        values[0] as f32 / 360.0,
-                        values[1] as f32 / 100.0,
-                        values[2] as f32 / 100.0,
-                    ];
-                    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,
-                        ]
-                    };
-
-                    Some(Color::Rgb(rgb[0], rgb[1], rgb[2]))
-                } else {
-                    None
-                }
-            } else {
-                None
-            }
-        }
-    }
-}
-
 /// applies the entire html namespace defined in dioxus-html
 pub fn apply_attributes(
     //
@@ -257,7 +149,7 @@ pub fn apply_attributes(
         "clip" => {}
 
         "color" => {
-            if let Some(c) = parse_color(value) {
+            if let Ok(c) = value.parse() {
                 style.tui_style.fg.replace(c);
             }
         }
@@ -512,7 +404,7 @@ fn apply_display(_name: &str, value: &str, style: &mut StyleModifer) {
 fn apply_background(name: &str, value: &str, style: &mut StyleModifer) {
     match name {
         "background-color" => {
-            if let Some(c) = parse_color(value) {
+            if let Ok(c) = value.parse() {
                 style.tui_style.bg.replace(c);
             }
         }
@@ -548,7 +440,7 @@ fn apply_border(name: &str, value: &str, style: &mut StyleModifer) {
         "border" => {}
         "border-bottom" => {}
         "border-bottom-color" => {
-            if let Some(c) = parse_color(value) {
+            if let Ok(c) = value.parse() {
                 style.tui_modifier.border_colors[2] = Some(c);
             }
         }
@@ -572,14 +464,14 @@ fn apply_border(name: &str, value: &str, style: &mut StyleModifer) {
         "border-color" => {
             let values: Vec<_> = value.split(' ').collect();
             if values.len() == 1 {
-                if let Some(c) = parse_color(values[0]) {
+                if let Ok(c) = values[0].parse() {
                     for i in 0..4 {
                         style.tui_modifier.border_colors[i] = Some(c);
                     }
                 }
             } else {
                 for (i, v) in values.into_iter().enumerate() {
-                    if let Some(c) = parse_color(v) {
+                    if let Ok(c) = v.parse() {
                         style.tui_modifier.border_colors[i] = Some(c);
                     }
                 }
@@ -593,7 +485,7 @@ fn apply_border(name: &str, value: &str, style: &mut StyleModifer) {
         "border-image-width" => {}
         "border-left" => {}
         "border-left-color" => {
-            if let Some(c) = parse_color(value) {
+            if let Ok(c) = value.parse() {
                 style.tui_modifier.border_colors[3] = Some(c);
             }
         }
@@ -621,7 +513,7 @@ fn apply_border(name: &str, value: &str, style: &mut StyleModifer) {
         }
         "border-right" => {}
         "border-right-color" => {
-            if let Some(c) = parse_color(value) {
+            if let Ok(c) = value.parse() {
                 style.tui_modifier.border_colors[1] = Some(c);
             }
         }
@@ -647,7 +539,7 @@ fn apply_border(name: &str, value: &str, style: &mut StyleModifer) {
         }
         "border-top" => {}
         "border-top-color" => {
-            if let Some(c) = parse_color(value) {
+            if let Ok(c) = value.parse() {
                 style.tui_modifier.border_colors[0] = Some(c);
             }
         }

+ 20 - 0
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
+    }
+}

+ 4 - 3
src/layout.rs

@@ -4,6 +4,7 @@ use tui::style::Style as TuiStyle;
 
 use crate::{
     attributes::{apply_attributes, StyleModifer},
+    style::RinkStyle,
     TuiModifier, TuiNode,
 };
 
@@ -41,7 +42,7 @@ 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(),
                 },
@@ -51,7 +52,7 @@ 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(),
             };
 
@@ -87,7 +88,7 @@ pub fn collect_layout<'a>(
                 node.mounted_id(),
                 TuiNode {
                     node,
-                    block_style: modifier.tui_style,
+                    block_style: modifier.tui_style.into(),
                     tui_modifier: modifier.tui_modifier,
                     layout: layout.new_node(modifier.style, &child_layout).unwrap(),
                 },

+ 15 - 4
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,12 @@ 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>,
 }
@@ -51,6 +60,7 @@ pub fn render_vdom(
     vdom: &mut VirtualDom,
     ctx: UnboundedSender<TermEvent>,
     handler: RinkInputHandler,
+    cfg: Config,
 ) -> Result<()> {
     // Setup input handling
     let (tx, mut rx) = unbounded();
@@ -139,7 +149,8 @@ pub fn render_vdom(
                         &mut nodes,
                         vdom,
                         root_node,
-                        &TuiStyle::default(),
+                        &RinkStyle::default(),
+                        cfg,
                     );
                     assert!(nodes.is_empty());
                 })?;

+ 143 - 143
src/render.rs

@@ -5,20 +5,111 @@ use stretch2::{
     prelude::{Layout, Size},
     Stretch,
 };
-use tui::{
-    backend::CrosstermBackend,
-    buffer::Buffer,
-    layout::Rect,
-    style::{Color, Style as TuiStyle},
-    widgets::Widget,
-};
+use tui::{backend::CrosstermBackend, layout::Rect};
 
-use crate::{BorderType, TuiNode, UnitSystem};
+use crate::{
+    style::{RinkColor, RinkStyle},
+    widget::{RinkBuffer, RinkCell, RinkWidget, WidgetWithContext},
+    BorderType, Config, TuiNode, UnitSystem,
+};
 
 const RADIUS_MULTIPLIER: [f32; 2] = [1.0, 0.5];
 
-impl<'a> Widget for TuiNode<'a> {
-    fn render(self, area: Rect, buf: &mut Buffer) {
+pub fn render_vnode<'a>(
+    frame: &mut tui::Frame<CrosstermBackend<Stdout>>,
+    layout: &Stretch,
+    layouts: &mut HashMap<ElementId, TuiNode<'a>>,
+    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, style, cfg);
+            }
+            return;
+        }
+
+        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, style, cfg);
+            return;
+        }
+
+        VNode::Placeholder(_) => return,
+
+        VNode::Element(_) | VNode::Text(_) => {}
+    }
+
+    let id = node.try_mounted_id().unwrap();
+    let mut node = layouts.remove(&id).unwrap();
+
+    let Layout { location, size, .. } = layout.layout(node.layout).unwrap();
+
+    let Point { x, y } = location;
+    let Size { width, height } = size;
+
+    match node.node {
+        VNode::Text(t) => {
+            #[derive(Default)]
+            struct Label<'a> {
+                text: &'a str,
+                style: RinkStyle,
+            }
+
+            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,
+                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(WidgetWithContext::new(label, cfg), area);
+            }
+        }
+        VNode::Element(el) => {
+            let area = Rect::new(*x as u16, *y as u16, *width as u16, *height as u16);
+
+            let 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(WidgetWithContext::new(node, cfg), area);
+            }
+
+            for el in el.children {
+                render_vnode(frame, layout, layouts, vdom, el, &new_style, cfg);
+            }
+        }
+        VNode::Fragment(_) => todo!(),
+        VNode::Component(_) => todo!(),
+        VNode::Placeholder(_) => todo!(),
+    }
+}
+
+impl<'a> RinkWidget for TuiNode<'a> {
+    fn render(self, area: Rect, mut buf: RinkBuffer<'_>) {
         use tui::symbols::line::*;
 
         enum Direction {
@@ -29,11 +120,11 @@ impl<'a> Widget for TuiNode<'a> {
         }
 
         fn draw(
-            buf: &mut Buffer,
+            buf: &mut RinkBuffer,
             points_history: [[i32; 2]; 3],
             symbols: &Set,
             pos: [u16; 2],
-            color: &Option<Color>,
+            color: &Option<RinkColor>,
         ) {
             let [before, current, after] = points_history;
             let start_dir = match [before[0] - current[0], before[1] - current[1]] {
@@ -61,14 +152,11 @@ impl<'a> Widget for TuiNode<'a> {
                 }
             };
 
-            let cell = buf.get_mut(
-                (current[0] + pos[0] as i32) as u16,
-                (current[1] + pos[1] as i32) as u16,
-            );
+            let mut new_cell = RinkCell::default();
             if let Some(c) = color {
-                cell.fg = *c;
+                new_cell.fg = *c;
             }
-            cell.symbol = match [start_dir, end_dir] {
+            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,
@@ -87,6 +175,11 @@ impl<'a> Widget for TuiNode<'a> {
                 ),
             }
             .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(
@@ -95,8 +188,8 @@ impl<'a> Widget for TuiNode<'a> {
             arc_angle: f32,
             radius: f32,
             symbols: &Set,
-            buf: &mut Buffer,
-            color: &Option<Color>,
+            mut buf: &mut RinkBuffer,
+            color: &Option<RinkColor>,
         ) {
             if radius < 0.0 {
                 return;
@@ -143,7 +236,7 @@ impl<'a> Widget for TuiNode<'a> {
                             _ => todo!(),
                         };
                         draw(
-                            buf,
+                            &mut buf,
                             [points_history[0], points_history[1], connecting_point],
                             &symbols,
                             pos,
@@ -152,7 +245,7 @@ impl<'a> Widget for TuiNode<'a> {
                         points_history = [points_history[1], connecting_point, points_history[2]];
                     }
 
-                    draw(buf, points_history, &symbols, pos, color);
+                    draw(&mut buf, points_history, &symbols, pos, color);
                 }
             }
 
@@ -173,13 +266,24 @@ impl<'a> Widget for TuiNode<'a> {
                 }
             }];
 
-            draw(buf, points_history, &symbols, pos, color);
+            draw(&mut buf, points_history, &symbols, pos, color);
         }
 
         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);
+            }
+        }
+
         for i in 0..4 {
             // the radius for the curve between this line and the next
             let r = match self.tui_modifier.border_types[(i + 1) % 4] {
@@ -231,41 +335,33 @@ impl<'a> Widget for TuiNode<'a> {
 
             let color = self.tui_modifier.border_colors[i].or(self.block_style.fg);
 
+            let mut new_cell = RinkCell::default();
+            if let Some(c) = color {
+                new_cell.fg = c;
+            }
             match i {
                 0 => {
                     for x in (area.left() + last_radius[0] + 1)..(area.right() - radius[0]) {
-                        let cell = buf.get_mut(x, area.top());
-                        if let Some(c) = color {
-                            cell.fg = c;
-                        }
-                        cell.symbol = symbols.horizontal.to_string();
+                        new_cell.symbol = symbols.horizontal.to_string();
+                        buf.set(x, area.top(), &new_cell);
                     }
                 }
                 1 => {
                     for y in (area.top() + last_radius[1] + 1)..(area.bottom() - radius[1]) {
-                        let cell = buf.get_mut(area.right() - 1, y);
-                        if let Some(c) = color {
-                            cell.fg = c;
-                        }
-                        cell.symbol = symbols.vertical.to_string();
+                        new_cell.symbol = symbols.vertical.to_string();
+                        buf.set(area.right() - 1, y, &new_cell);
                     }
                 }
                 2 => {
                     for x in (area.left() + radius[0])..(area.right() - last_radius[0] - 1) {
-                        let cell = buf.get_mut(x, area.bottom() - 1);
-                        if let Some(c) = color {
-                            cell.fg = c;
-                        }
-                        cell.symbol = symbols.horizontal.to_string();
+                        new_cell.symbol = symbols.horizontal.to_string();
+                        buf.set(x, area.bottom() - 1, &new_cell);
                     }
                 }
                 3 => {
                     for y in (area.top() + radius[1])..(area.bottom() - last_radius[1] - 1) {
-                        let cell = buf.get_mut(area.left(), y);
-                        if let Some(c) = color {
-                            cell.fg = c;
-                        }
-                        cell.symbol = symbols.vertical.to_string();
+                        new_cell.symbol = symbols.vertical.to_string();
+                        buf.set(area.left(), y, &new_cell);
                     }
                 }
                 _ => (),
@@ -278,7 +374,7 @@ impl<'a> Widget for TuiNode<'a> {
                     std::f32::consts::FRAC_PI_2,
                     r,
                     &symbols,
-                    buf,
+                    &mut buf,
                     &color,
                 ),
                 1 => draw_arc(
@@ -287,7 +383,7 @@ impl<'a> Widget for TuiNode<'a> {
                     std::f32::consts::FRAC_PI_2,
                     r,
                     &symbols,
-                    buf,
+                    &mut buf,
                     &color,
                 ),
                 2 => draw_arc(
@@ -296,7 +392,7 @@ impl<'a> Widget for TuiNode<'a> {
                     std::f32::consts::FRAC_PI_2,
                     r,
                     &symbols,
-                    buf,
+                    &mut buf,
                     &color,
                 ),
                 3 => draw_arc(
@@ -305,107 +401,11 @@ impl<'a> Widget for TuiNode<'a> {
                     std::f32::consts::FRAC_PI_2,
                     r,
                     &symbols,
-                    buf,
+                    &mut buf,
                     &color,
                 ),
                 _ => panic!("more than 4 sides?"),
             }
         }
-
-        // todo: only render inside borders
-        for x in area.left()..area.right() {
-            for y in area.top()..area.bottom() {
-                let cell = buf.get_mut(x, y);
-                if let Some(c) = self.block_style.bg {
-                    cell.bg = c;
-                }
-            }
-        }
-    }
-}
-
-pub fn render_vnode<'a>(
-    frame: &mut tui::Frame<CrosstermBackend<Stdout>>,
-    layout: &Stretch,
-    layouts: &mut HashMap<ElementId, TuiNode<'a>>,
-    vdom: &'a VirtualDom,
-    node: &'a VNode<'a>,
-    // this holds the parents syle state for styled text rendering and potentially transparentcy
-    style: &TuiStyle,
-) {
-    match node {
-        VNode::Fragment(f) => {
-            for child in f.children {
-                render_vnode(frame, layout, layouts, vdom, child, style);
-            }
-            return;
-        }
-
-        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, style);
-            return;
-        }
-
-        VNode::Placeholder(_) => return,
-
-        VNode::Element(_) | VNode::Text(_) => {}
-    }
-
-    let id = node.try_mounted_id().unwrap();
-    let node = layouts.remove(&id).unwrap();
-
-    let Layout { location, size, .. } = layout.layout(node.layout).unwrap();
-
-    let Point { x, y } = location;
-    let Size { width, height } = size;
-
-    match node.node {
-        VNode::Text(t) => {
-            #[derive(Default)]
-            struct Label<'a> {
-                text: &'a str,
-                style: TuiStyle,
-            }
-
-            impl<'a> Widget for Label<'a> {
-                fn render(self, area: Rect, buf: &mut Buffer) {
-                    buf.set_string(area.left(), area.top(), self.text, self.style);
-                }
-            }
-
-            // let s = Span::raw(t.text);
-
-            // Block::default().
-
-            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);
-            }
-        }
-        VNode::Element(el) => {
-            let area = Rect::new(*x as u16, *y as u16, *width as u16, *height as u16);
-
-            let new_style = style.patch(node.block_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(node, area);
-            }
-
-            for el in el.children {
-                render_vnode(frame, layout, layouts, vdom, el, &new_style);
-            }
-        }
-        VNode::Fragment(_) => todo!(),
-        VNode::Component(_) => todo!(),
-        VNode::Placeholder(_) => todo!(),
     }
 }

+ 453 - 0
src/style.rs

@@ -0,0 +1,453 @@
+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 if other == Color::Reset {
+                self.color
+            } 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);
+                let c = 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,
+                );
+                c
+            }
+        }
+    }
+}
+
+fn parse_value(
+    v: &str,
+    current_max_output: f32,
+    required_max_output: f32,
+) -> Result<f32, ParseFloatError> {
+    if v.ends_with('%') {
+        Ok((v[..v.len() - 1].trim().parse::<f32>()? / 100.0) * required_max_output)
+    } else {
+        Ok((v.trim().parse::<f32>()? / current_max_output) * required_max_output)
+    }
+}
+
+pub struct ParseColorError;
+
+fn parse_hex(color: &str) -> Result<Color, ParseColorError> {
+    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<Color, ParseColorError> {
+    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<Color, ParseColorError> {
+    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<Self, Self::Err> {
+        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 color.starts_with("rgb(") {
+                    let color_values = color[4..].trim_end_matches(')');
+                    if color.matches(',').count() == 4 {
+                        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 color.starts_with("rgba(") {
+                    let color_values = color[5..].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 color.starts_with("hsl(") {
+                    let color_values = color[4..].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 color.starts_with("hsla(") {
+                    let color_values = color[5..].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) / 6;
+                let g = (((v as u16 % 36) / 6) * 255 + 3) / 6;
+                let b = ((v as u16 % 6) * 255 + 3) / 6;
+                let vals = [v / 36, (v % 36) / 6, v % 6];
+                [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],
+        },
+        _ => todo!("{c:?}"),
+    }
+}
+
+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: 24 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 * 6) / 255;
+                        let g = (rgb[1] as u16 * 6) / 255;
+                        let b = (rgb[2] as u16 * 6) / 255;
+                        let idx = 16 + r * 36 + g * 6 + b;
+                        Color::Indexed(idx as u8)
+                    }
+                }
+            },
+        }
+    }
+}
+
+#[test]
+fn rgb_to_ansi() {
+    for idx in 16..=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 && i <= 255);
+            }
+        } 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<RinkColor>,
+    pub bg: Option<RinkColor>,
+    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<Style> for RinkStyle {
+    fn into(self) -> Style {
+        Style {
+            fg: self.fg.map(|c| c.color),
+            bg: self.bg.map(|c| c.color),
+            add_modifier: self.add_modifier,
+            sub_modifier: self.sub_modifier,
+        }
+    }
+}

+ 96 - 0
src/widget.rs

@@ -0,0 +1,96 @@
+use tui::{
+    buffer::Buffer,
+    layout::Rect,
+    style::{Color, Modifier},
+    widgets::Widget,
+};
+
+use crate::{
+    style::{convert, RinkColor, RinkStyle},
+    Config,
+};
+
+pub struct RinkBuffer<'a> {
+    buf: &'a mut Buffer,
+    cfg: Config,
+}
+
+impl<'a> RinkBuffer<'a> {
+    fn new(buf: &'a mut Buffer, cfg: Config) -> RinkBuffer<'a> {
+        Self { buf, cfg }
+    }
+
+    pub fn set(&mut self, x: u16, y: u16, new: &RinkCell) {
+        let mut cell = self.buf.get_mut(x, y);
+        cell.bg = convert(self.cfg.rendering_mode, new.bg.blend(cell.bg));
+        if &new.symbol == "" {
+            if &cell.symbol != "" {
+                // allows text to "shine through" transparent backgrounds
+                cell.fg = convert(self.cfg.rendering_mode, new.bg.blend(cell.fg));
+            }
+        } else {
+            cell.modifier = new.modifier;
+            cell.symbol = new.symbol.clone();
+            cell.fg = convert(self.cfg.rendering_mode, new.fg.blend(cell.bg));
+        }
+    }
+}
+
+pub trait RinkWidget {
+    fn render(self, area: Rect, buf: RinkBuffer);
+}
+
+pub struct WidgetWithContext<T: RinkWidget> {
+    widget: T,
+    config: Config,
+}
+
+impl<T: RinkWidget> WidgetWithContext<T> {
+    pub fn new(widget: T, config: Config) -> WidgetWithContext<T> {
+        WidgetWithContext { widget, config }
+    }
+}
+
+impl<T: RinkWidget> Widget for WidgetWithContext<T> {
+    fn render(self, area: Rect, buf: &mut Buffer) {
+        self.widget.render(area, RinkBuffer::new(buf, self.config))
+    }
+}
+
+#[derive(Clone)]
+pub struct RinkCell {
+    pub symbol: String,
+    pub bg: RinkColor,
+    pub fg: RinkColor,
+    pub modifier: Modifier,
+}
+
+impl Default for RinkCell {
+    fn default() -> Self {
+        Self {
+            symbol: "".to_string(),
+            fg: RinkColor {
+                color: Color::Rgb(0, 0, 0),
+                alpha: 0.0,
+            },
+            bg: RinkColor {
+                color: Color::Rgb(0, 0, 0),
+                alpha: 0.0,
+            },
+            modifier: Modifier::empty(),
+        }
+    }
+}
+
+impl RinkCell {
+    pub fn set_style(&mut self, style: RinkStyle) {
+        if let Some(c) = style.fg {
+            self.fg = c;
+        }
+        if let Some(c) = style.bg {
+            self.bg = c;
+        }
+        self.modifier = style.add_modifier;
+        self.modifier.remove(style.sub_modifier);
+    }
+}