From f6ef9981dd9204cab0eae364c3609ad821807331 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sat, 12 Feb 2022 14:03:08 -0600 Subject: [PATCH 1/6] added border and more attributes --- Cargo.toml | 2 +- examples/border.rs | 30 +++ examples/text.rs | 33 +++- src/attributes.rs | 444 +++++++++++++++++++++++++++++++++++---------- src/layout.rs | 5 +- src/lib.rs | 10 +- src/render.rs | 336 +++++++++++++++++++++++++++++++++- 7 files changed, 745 insertions(+), 115 deletions(-) create mode 100644 examples/border.rs 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..2e267b271 --- /dev/null +++ b/examples/border.rs @@ -0,0 +1,30 @@ +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()), + + // 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", + border_color: "#0000FF #FF00FF #FF0000 #00FF00", + + "{radius}" + } + }) +} diff --git a/examples/text.rs b/examples/text.rs index db49cd733..21a7654ad 100644 --- a/examples/text.rs +++ b/examples/text.rs @@ -53,13 +53,6 @@ fn app(cx: Scope) -> Element { "zib" "zib" "zib" - "zib" - "zib" - "zib" - "zib" - "zib" - "zib" - "zib" } p { background_color: "yellow", @@ -77,6 +70,32 @@ 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: "rgb(50, 100, 255)", + "underline" + } + p { + text_decoration: "line-through", + color: "hsl(10, 100%, 70%)", + "line-through" + } } }) } diff --git a/src/attributes.rs b/src/attributes.rs index d84779406..b4a5691f3 100644 --- a/src/attributes.rs +++ b/src/attributes.rs @@ -30,15 +30,155 @@ */ use stretch2::{prelude::*, style::PositionType, style::Style}; -use tui::style::Style as TuiStyle; +use tui::style::{Color, Style as TuiStyle}; pub struct StyleModifer { pub style: Style, pub tui_style: TuiStyle, + pub tui_modifier: TuiModifier, } -enum TuiModifier { - Text, +pub struct TuiModifier { + // border arrays start at the top and proceed clockwise + pub border_colors: [Option; 4], + pub border_types: [BorderType; 4], + pub border_widths: [UnitSystem; 4], + pub border_radi: [UnitSystem; 4], +} + +#[derive(Clone, Copy)] +pub enum BorderType { + DOTTED, + DASHED, + SOLID, + DOUBLE, + GROOVE, + RIDGE, + INSET, + OUTSET, + HIDDEN, + NONE, +} + +impl Default for TuiModifier { + fn default() -> Self { + Self { + border_colors: [None; 4], + border_types: [BorderType::NONE; 4], + border_widths: [UnitSystem::Point(0.0); 4], + border_radi: [UnitSystem::Point(0.0); 4], + } + } +} + +fn parse_color(color: &str) -> Option { + 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 @@ -117,7 +257,9 @@ pub fn apply_attributes( "clip" => {} "color" => { - // text color + if let Some(c) = parse_color(value) { + style.tui_style.fg.replace(c); + } } "column-count" @@ -170,14 +312,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 +425,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 +440,8 @@ pub fn apply_attributes( } } -enum UnitSystem { +#[derive(Clone, Copy)] +pub enum UnitSystem { Percent(f32), Point(f32), } @@ -344,7 +481,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 +512,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 Some(c) = parse_color(value) { + style.tui_style.bg.replace(c); + } } "background" => {} "background-attachment" => {} @@ -423,17 +528,63 @@ 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_type(v: &str) -> BorderType { + match v { + "dotted" => BorderType::DOTTED, + "dashed" => BorderType::DASHED, + "solid" => BorderType::SOLID, + "double" => BorderType::DOUBLE, + "groove" => BorderType::GROOVE, + "ridge" => BorderType::RIDGE, + "inset" => BorderType::INSET, + "outset" => BorderType::OUTSET, + "none" => BorderType::NONE, + "hidden" => BorderType::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 Some(c) = parse_color(value) { + style.tui_modifier.border_colors[2] = Some(c); + } + } + "border-bottom-left-radius" => { + if let Some(v) = parse_value(value) { + style.tui_modifier.border_radi[2] = v; + } + } + "border-bottom-right-radius" => { + if let Some(v) = parse_value(value) { + style.tui_modifier.border_radi[1] = v; + } + } + "border-bottom-style" => style.tui_modifier.border_types[2] = parse_border_type(value), + "border-bottom-width" => { + if let Some(v) = parse_value(value) { + style.tui_modifier.border_widths[2] = v; + } + } "border-collapse" => {} - "border-color" => {} + "border-color" => { + let values: Vec<_> = value.split(' ').collect(); + if values.len() == 1 { + if let Some(c) = parse_color(values[0]) { + 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) { + style.tui_modifier.border_colors[i] = Some(c); + } + } + } + } "border-image" => {} "border-image-outset" => {} "border-image-repeat" => {} @@ -441,28 +592,89 @@ 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 Some(c) = parse_color(value) { + style.tui_modifier.border_colors[3] = Some(c); } } - _ => {} + "border-left-style" => style.tui_modifier.border_types[3] = parse_border_type(value), + "border-left-width" => { + if let Some(v) = parse_value(value) { + style.tui_modifier.border_widths[3] = v; + } + } + "border-radius" => { + let values: Vec<_> = value.split(' ').collect(); + if values.len() == 1 { + if let Some(r) = parse_value(values[0]) { + for i in 0..4 { + style.tui_modifier.border_radi[i] = r; + } + } + } else { + for (i, v) in values.into_iter().enumerate() { + if let Some(r) = parse_value(v) { + style.tui_modifier.border_radi[i] = r; + } + } + } + } + "border-right" => {} + "border-right-color" => { + if let Some(c) = parse_color(value) { + style.tui_modifier.border_colors[1] = Some(c); + } + } + "border-right-style" => style.tui_modifier.border_types[1] = parse_border_type(value), + "border-right-width" => { + if let Some(v) = parse_value(value) { + style.tui_modifier.border_widths[1] = v; + } + } + "border-spacing" => {} + "border-style" => { + let values: Vec<_> = value.split(' ').collect(); + if values.len() == 1 { + let border = parse_border_type(values[0]); + for i in 0..4 { + style.tui_modifier.border_types[i] = border; + } + } else { + for (i, v) in values.into_iter().enumerate() { + style.tui_modifier.border_types[i] = parse_border_type(v); + } + } + } + "border-top" => {} + "border-top-color" => { + if let Some(c) = parse_color(value) { + style.tui_modifier.border_colors[0] = Some(c); + } + } + "border-top-left-radius" => { + if let Some(v) = parse_value(value) { + style.tui_modifier.border_radi[3] = v; + } + } + "border-top-right-radius" => { + if let Some(v) = parse_value(value) { + style.tui_modifier.border_radi[0] = v; + } + } + "border-top-style" => style.tui_modifier.border_types[0] = parse_border_type(value), + "border-top-width" => { + if let Some(v) = parse_value(value) { + style.tui_modifier.border_widths[0] = v; + } + } + "border-width" => { + if let Some(v) = parse_value(value) { + for i in 0..4 { + style.tui_modifier.border_widths[i] = v; + } + } + } + _ => (), } } @@ -516,14 +728,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 +759,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" => todo!(), + "font-family" => todo!(), + "font-size" => todo!(), + "font-size-adjust" => todo!(), + "font-stretch" => todo!(), + "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 +815,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/layout.rs b/src/layout.rs index 94f750990..d9cf6ad21 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -4,7 +4,7 @@ use tui::style::Style as TuiStyle; use crate::{ attributes::{apply_attributes, StyleModifer}, - TuiNode, + TuiModifier, TuiNode, }; /* @@ -42,6 +42,7 @@ pub fn collect_layout<'a>( TuiNode { node, block_style: tui::style::Style::default(), + tui_modifier: TuiModifier::default(), layout: layout.new_node(style, &[]).unwrap(), }, ); @@ -51,6 +52,7 @@ pub fn collect_layout<'a>( let mut modifier = StyleModifer { style: Style::default(), tui_style: TuiStyle::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..c3cba6970 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -43,6 +43,7 @@ pub fn launch(app: Component<()>) { pub struct TuiNode<'a> { pub layout: stretch2::node::Node, pub block_style: TuiStyle, + pub tui_modifier: TuiModifier, pub node: &'a VNode<'a>, } @@ -132,7 +133,14 @@ 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, + &TuiStyle::default(), + ); assert!(nodes.is_empty()); })?; diff --git a/src/render.rs b/src/render.rs index c997d5302..83b30046c 100644 --- a/src/render.rs +++ b/src/render.rs @@ -9,11 +9,320 @@ use tui::{ backend::CrosstermBackend, buffer::Buffer, layout::Rect, - style::Style as TuiStyle, - widgets::{Block, Widget}, + style::{Color, Style as TuiStyle}, + widgets::Widget, }; -use crate::TuiNode; +use crate::{BorderType, 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) { + use tui::symbols::line::*; + + enum Direction { + Left, + Right, + Up, + Down, + } + + fn draw( + buf: &mut Buffer, + 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 cell = buf.get_mut( + (current[0] + pos[0] as i32) as u16, + (current[1] + pos[1] as i32) as u16, + ); + if let Some(c) = color { + cell.fg = *c; + } + 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(); + } + + fn draw_arc( + pos: [u16; 2], + starting_angle: f32, + arc_angle: f32, + radius: f32, + symbols: &Set, + buf: &mut Buffer, + 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, + ]; + 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); + } + + if area.area() == 0 { + return; + } + + 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] { + BorderType::HIDDEN => 0.0, + BorderType::NONE => 0.0, + _ => match self.tui_modifier.border_radi[i] { + 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), + }; + 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_idx = if i == 0 { 3 } else { i - 1 }; + let last_r = match self.tui_modifier.border_types[last_idx] { + BorderType::HIDDEN => 0.0, + BorderType::NONE => 0.0, + _ => match self.tui_modifier.border_radi[last_idx] { + 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), + }; + let last_radius = [ + (last_r * RADIUS_MULTIPLIER[0]) as u16, + (last_r * RADIUS_MULTIPLIER[1]) as u16, + ]; + + let symbols = match self.tui_modifier.border_types[i] { + BorderType::DOTTED => NORMAL, + BorderType::DASHED => NORMAL, + BorderType::SOLID => NORMAL, + BorderType::DOUBLE => DOUBLE, + BorderType::GROOVE => NORMAL, + BorderType::RIDGE => NORMAL, + BorderType::INSET => NORMAL, + BorderType::OUTSET => NORMAL, + BorderType::HIDDEN => continue, + BorderType::NONE => continue, + }; + + let color = self.tui_modifier.border_colors[i].or(self.block_style.fg); + + 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(); + } + } + 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(); + } + } + 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(); + } + } + 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(); + } + } + _ => (), + } + + match i { + 0 => 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, + buf, + &color, + ), + 1 => draw_arc( + [area.right() - radius[0] - 1, area.bottom() - radius[1] - 1], + 0.0, + std::f32::consts::FRAC_PI_2, + r, + &symbols, + buf, + &color, + ), + 2 => 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, + buf, + &color, + ), + 3 => draw_arc( + [area.left() + radius[0], area.top() + radius[1]], + std::f32::consts::PI, + std::f32::consts::FRAC_PI_2, + r, + &symbols, + 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>, @@ -21,11 +330,13 @@ pub fn render_vnode<'a>( layouts: &mut HashMap>, 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); + render_vnode(frame, layout, layouts, vdom, child, style); } return; } @@ -33,7 +344,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); return; } @@ -55,11 +366,12 @@ pub fn render_vnode<'a>( #[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, TuiStyle::default()); + buf.set_string(area.left(), area.top(), self.text, self.style); } } @@ -67,7 +379,10 @@ pub fn render_vnode<'a>( // 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 @@ -76,16 +391,17 @@ pub fn render_vnode<'a>( } } 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 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(block, area); + frame.render_widget(node, area); } for el in el.children { - render_vnode(frame, layout, layouts, vdom, el); + render_vnode(frame, layout, layouts, vdom, el, &new_style); } } VNode::Fragment(_) => todo!(), From 7a1d8f05320dbcaef410e7df99634b7b0d228107 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 17 Feb 2022 16:06:28 -0600 Subject: [PATCH 2/6] added alpha channel --- examples/border.rs | 3 - examples/color_test.rs | 48 +++++ examples/text.rs | 34 +++- src/attributes.rs | 132 ++---------- src/config.rs | 20 ++ src/layout.rs | 7 +- src/lib.rs | 19 +- src/render.rs | 286 +++++++++++++------------- src/style.rs | 453 +++++++++++++++++++++++++++++++++++++++++ src/widget.rs | 96 +++++++++ 10 files changed, 821 insertions(+), 277 deletions(-) create mode 100644 examples/color_test.rs create mode 100644 src/config.rs create mode 100644 src/style.rs create mode 100644 src/widget.rs diff --git a/examples/border.rs b/examples/border.rs index 2e267b271..1d9f681b7 100644 --- a/examples/border.rs +++ b/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", 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 21a7654ad..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" @@ -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%)" + } + } } }) } diff --git a/src/attributes.rs b/src/attributes.rs index b4a5691f3..8ef9af46a 100644 --- a/src/attributes.rs +++ b/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; 4], + pub border_colors: [Option; 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 { - 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); } } 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 d9cf6ad21..21b75db42 100644 --- a/src/layout.rs +++ b/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(), }, diff --git a/src/lib.rs b/src/lib.rs index c3cba6970..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,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, 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()); })?; diff --git a/src/render.rs b/src/render.rs index 83b30046c..1dac33f86 100644 --- a/src/render.rs +++ b/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>, + layout: &Stretch, + 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, 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: &Option, ) { 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, + mut buf: &mut RinkBuffer, + color: &Option, ) { 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>, - layout: &Stretch, - layouts: &mut HashMap>, - 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!(), } } diff --git a/src/style.rs b/src/style.rs new file mode 100644 index 000000000..ffe3aa568 --- /dev/null +++ b/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 { + if v.ends_with('%') { + Ok((v[..v.len() - 1].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 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, + 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