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