From f1a011cf52f3bec64cac097fb9e47f5ccaf3812a Mon Sep 17 00:00:00 2001 From: Shark Date: Sat, 27 Apr 2024 14:41:37 +0200 Subject: [PATCH] render borders --- Cargo.lock | 5 +- crates/gosub_renderer/Cargo.toml | 2 + crates/gosub_renderer/src/draw.rs | 466 +++++++++++++++++++++--- crates/gosub_rendering/src/layout.rs | 6 + crates/gosub_styling/src/render_tree.rs | 13 +- src/bin/resources/gosub.html | 69 +++- 6 files changed, 506 insertions(+), 55 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 960b7af34..ff118026a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1377,6 +1377,7 @@ dependencies = [ "gosub_shared", "gosub_styling", "slotmap", + "smallvec", "taffy", "vello", "wasm-bindgen-futures", @@ -2911,9 +2912,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "smithay-client-toolkit" diff --git a/crates/gosub_renderer/Cargo.toml b/crates/gosub_renderer/Cargo.toml index f400380c2..915ef746c 100644 --- a/crates/gosub_renderer/Cargo.toml +++ b/crates/gosub_renderer/Cargo.toml @@ -19,6 +19,8 @@ anyhow = "1.0.81" wgpu = "0.19.3" futures = "0.3.30" slotmap = "1.0.7" +smallvec = "1.13.2" + [target.'cfg(target_arch = "wasm32")'.dependencies] diff --git a/crates/gosub_renderer/src/draw.rs b/crates/gosub_renderer/src/draw.rs index 19d4d7d4d..f02e91f3b 100644 --- a/crates/gosub_renderer/src/draw.rs +++ b/crates/gosub_renderer/src/draw.rs @@ -1,13 +1,16 @@ use anyhow::anyhow; -use taffy::{AvailableSpace, PrintTree, Size}; -use vello::kurbo::{Affine, Rect, RoundedRect}; +use smallvec::SmallVec; +use std::ops::Div; +use taffy::{AvailableSpace, Layout, NodeId, PrintTree, Size, TaffyTree, TraversePartialTree}; +use vello::kurbo::{Affine, Arc, BezPath, Cap, Join, Rect, RoundedRect, Stroke}; use vello::peniko::{Color, Fill}; use vello::Scene; use winit::dpi::PhysicalSize; +use gosub_html5::node::NodeId as GosubId; use gosub_styling::css_colors::RgbColor; use gosub_styling::css_values::CssValue; -use gosub_styling::render_tree::RenderNodeData; +use gosub_styling::render_tree::{RenderNodeData, RenderTree, RenderTreeNode}; use crate::render_tree::{NodeID, TreeDrawer}; @@ -42,6 +45,8 @@ impl TreeDrawer { return; } + print_tree(&self.taffy, self.root, &self.style); + let bg = Rect::new(0.0, 0.0, size.width as f64, size.height as f64); scene.fill(Fill::NonZero, Affine::IDENTITY, Color::BLACK, None, &bg); @@ -88,32 +93,21 @@ impl TreeDrawer { pos.0 += layout.location.x as f64; pos.1 += layout.location.y as f64; - if let RenderNodeData::Text(text) = &node.data { - let color = node - .properties - .get("color") - .and_then(|prop| { - prop.compute_value(); - - match &prop.actual { - CssValue::Color(color) => Some(*color), - CssValue::String(color) => Some(RgbColor::from(color.as_str())), - _ => None, - } - }) - .map(|color| { - Color::rgba8(color.r as u8, color.g as u8, color.b as u8, color.a as u8) - }) - .unwrap_or(Color::BLACK); - - let affine = Affine::translate((pos.0, pos.1)); - - text.show(scene, color, affine, Fill::NonZero, None); - } + render_text(node, scene, pos, layout); + + let border_radius = render_bg(node, scene, layout, pos); + + render_border(node, scene, layout, pos, border_radius); + + Ok(()) + } +} - let bg_color = node +fn render_text(node: &mut RenderTreeNode, scene: &mut Scene, pos: &(f64, f64), layout: &Layout) { + if let RenderNodeData::Text(text) = &node.data { + let color = node .properties - .get("background-color") + .get("color") .and_then(|prop| { prop.compute_value(); @@ -123,26 +117,410 @@ impl TreeDrawer { _ => None, } }) - .map(|color| Color::rgba8(color.r as u8, color.g as u8, color.b as u8, color.a as u8)); + .map(|color| Color::rgba8(color.r as u8, color.g as u8, color.b as u8, color.a as u8)) + .unwrap_or(Color::BLACK); + + let affine = Affine::translate((pos.0, pos.1 + layout.size.height as f64)); + + text.show(scene, color, affine, Fill::NonZero, None); + } +} + +fn render_bg( + node: &mut RenderTreeNode, + scene: &mut Scene, + layout: &Layout, + pos: &(f64, f64), +) -> f64 { + let bg_color = node + .properties + .get("background-color") + .and_then(|prop| { + prop.compute_value(); + + match &prop.actual { + CssValue::Color(color) => Some(*color), + CssValue::String(color) => Some(RgbColor::from(color.as_str())), + _ => None, + } + }) + .map(|color| Color::rgba8(color.r as u8, color.g as u8, color.b as u8, color.a as u8)); + + let border_radius = node + .properties + .get("border-radius") + .map(|prop| { + prop.compute_value(); + prop.actual.unit_to_px() as f64 + }) + .unwrap_or(0.0); + + if let Some(bg_color) = bg_color { + let rect = RoundedRect::new( + pos.0, + pos.1, + pos.0 + layout.size.width as f64, + pos.1 + layout.size.height as f64, + border_radius, + ); + scene.fill(Fill::NonZero, Affine::IDENTITY, bg_color, None, &rect); + } + + border_radius +} + +enum Side { + Top, + Right, + Bottom, + Left, +} + +impl Side { + fn all() -> [Side; 4] { + [Side::Top, Side::Right, Side::Bottom, Side::Left] + } + + fn to_str(&self) -> &'static str { + match self { + Side::Top => "top", + Side::Right => "right", + Side::Bottom => "bottom", + Side::Left => "left", + } + } +} + +fn render_border( + node: &mut RenderTreeNode, + scene: &mut Scene, + layout: &Layout, + pos: &(f64, f64), + border_radius: f64, +) { + for side in Side::all() { + render_border_side(node, scene, layout, pos, border_radius, side); + } +} + +fn render_border_side( + node: &mut RenderTreeNode, + scene: &mut Scene, + layout: &Layout, + pos: &(f64, f64), + border_radius: f64, + side: Side, +) { + let border_width = match side { + Side::Top => layout.border.top, + Side::Right => layout.border.right, + Side::Bottom => layout.border.bottom, + Side::Left => layout.border.left, + } as f64; + + let border_color = node + .properties + .get(&format!("border-{}-color", side.to_str())) + .and_then(|prop| { + prop.compute_value(); + + match &prop.actual { + CssValue::Color(color) => Some(*color), + CssValue::String(color) => Some(RgbColor::from(color.as_str())), + _ => None, + } + }) + .map(|color| Color::rgba8(color.r as u8, color.g as u8, color.b as u8, color.a as u8)); + + // let border_radius = 16f64; + + let width = layout.size.width as f64; + let height = layout.size.height as f64; + + if let Some(border_color) = border_color { + let mut path = BezPath::new(); + + //draw the border segment with rounded corners + + match side { + Side::Top => { + let offset = border_radius.powi(2).div(2.0).sqrt() - border_radius; + + path.move_to((pos.0 - offset, pos.1 - offset)); + + let arc = Arc::new( + (pos.0 + border_radius, pos.1 + border_radius), + (border_radius, border_radius), + -std::f64::consts::PI * 3.0 / 4.0, + std::f64::consts::PI / 4.0, + 0.0, + ); + + arc.to_cubic_beziers(0.1, |p1, p2, p3| { + path.curve_to(p1, p2, p3); + }); + + path.line_to((pos.0 + width - border_radius, pos.1)); + + let arc = Arc::new( + (pos.0 + width - border_radius, pos.1 + border_radius), + (border_radius, border_radius), + -std::f64::consts::PI / 2.0, + std::f64::consts::PI / 4.0, + 0.0, + ); + + arc.to_cubic_beziers(0.1, |p1, p2, p3| { + path.curve_to(p1, p2, p3); + }); + } + Side::Right => { + let offset = border_radius.powi(2).div(2.0).sqrt() - border_radius; + path.move_to((pos.0 + width + offset, pos.1 - offset)); + + let arc = Arc::new( + (pos.0 + width - border_radius, pos.1 + border_radius), + (border_radius, border_radius), + -std::f64::consts::PI / 4.0, + std::f64::consts::PI / 4.0, + 0.0, + ); + + arc.to_cubic_beziers(0.1, |p1, p2, p3| { + path.curve_to(p1, p2, p3); + }); + + path.line_to((pos.0 + width, pos.1 + height - border_radius)); + + let arc = Arc::new( + ( + pos.0 + width - border_radius, + pos.1 + height - border_radius, + ), + (border_radius, border_radius), + 0.0, + std::f64::consts::PI / 4.0, + 0.0, + ); + + arc.to_cubic_beziers(0.1, |p1, p2, p3| { + path.curve_to(p1, p2, p3); + }); + } + Side::Bottom => { + let offset = border_radius.powi(2).div(2.0).sqrt() - border_radius; - let border_radius = node + path.move_to((pos.0 + width + offset, pos.1 + height + offset)); + + let arc = Arc::new( + ( + pos.0 + width - border_radius, + pos.1 + height - border_radius, + ), + (border_radius, border_radius), + -std::f64::consts::PI * 7.0 / 4.0, + std::f64::consts::PI / 4.0, + 0.0, + ); + + arc.to_cubic_beziers(0.1, |p1, p2, p3| { + path.curve_to(p1, p2, p3); + }); + + path.line_to((pos.0 + border_radius, pos.1 + height)); + + let arc = Arc::new( + (pos.0 + border_radius, pos.1 + height - border_radius), + (border_radius, border_radius), + -std::f64::consts::PI * 3.0 / 2.0, + std::f64::consts::PI / 4.0, + 0.0, + ); + + arc.to_cubic_beziers(0.1, |p1, p2, p3| { + path.curve_to(p1, p2, p3); + }); + } + Side::Left => { + let offset = border_radius.powi(2).div(2.0).sqrt() - border_radius; + + path.move_to((pos.0 - offset, pos.1 + height + offset)); + + let arc = Arc::new( + (pos.0 + border_radius, pos.1 + height - border_radius), + (border_radius, border_radius), + -std::f64::consts::PI * 5.0 / 4.0, + std::f64::consts::PI / 4.0, + 0.0, + ); + + arc.to_cubic_beziers(0.1, |p1, p2, p3| { + path.curve_to(p1, p2, p3); + }); + + path.line_to((pos.0, pos.1 + border_radius)); + + let arc = Arc::new( + (pos.0 + border_radius, pos.1 + border_radius), + (border_radius, border_radius), + -std::f64::consts::PI, + std::f64::consts::PI / 4.0, + 0.0, + ); + + arc.to_cubic_beziers(0.1, |p1, p2, p3| { + path.curve_to(p1, p2, p3); + }); + } + } + + let Some(border_style) = node .properties - .get("border-radius") - .map(|prop| prop.actual.unit_to_px() as f64) - .unwrap_or(0.0); - - if let Some(bg_color) = bg_color { - println!("Rendering background color: {:?}", bg_color); - let rect = RoundedRect::new( - pos.0, - pos.1, - layout.size.width as f64, - layout.size.height as f64, - border_radius, - ); - scene.fill(Fill::NonZero, Affine::IDENTITY, bg_color, None, &rect); + .get(&format!("border-{}-style", side.to_str())) + .and_then(|prop| { + prop.compute_value(); + + match &prop.actual { + CssValue::String(style) => Some(style.as_str()), + _ => None, + } + }) + else { + return; + }; + + let border_style = BorderStyle::from_str(border_style); + + let cap = match border_style { + BorderStyle::Dashed => Cap::Square, + BorderStyle::Dotted => Cap::Round, + _ => Cap::Butt, + }; + + let dash_pattern = match border_style { + BorderStyle::Dashed => SmallVec::from([ + border_width * 3.0, + border_width * 3.0, + border_width * 3.0, + border_width * 3.0, + ]), + BorderStyle::Dotted => { + SmallVec::from([border_width, border_width, border_width, border_width]) + //TODO: somehow this doesn't result in circles. It is more like a rounded rectangle + } + _ => SmallVec::default(), + }; + + let stroke = Stroke { + width: border_width, + join: Join::Bevel, + miter_limit: 0.0, + start_cap: cap, + end_cap: cap, + dash_pattern, + dash_offset: 0.0, + }; + + scene.stroke(&stroke, Affine::IDENTITY, border_color, None, &path); + } +} + +#[derive(Debug)] +enum BorderStyle { + None, + Hidden, + Dotted, + Dashed, + Solid, + Double, + Groove, + Ridge, + Inset, + Outset, + //DotDash, //TODO: should we support these? + //DotDotDash, +} + +impl BorderStyle { + fn from_str(style: &str) -> Self { + match style { + "none" => Self::None, + "hidden" => Self::Hidden, + "dotted" => Self::Dotted, + "dashed" => Self::Dashed, + "solid" => Self::Solid, + "double" => Self::Double, + "groove" => Self::Groove, + "ridge" => Self::Ridge, + "inset" => Self::Inset, + "outset" => Self::Outset, + _ => Self::None, + } + } +} + +//just for debugging +pub fn print_tree(tree: &TaffyTree, root: NodeId, gosub_tree: &RenderTree) { + println!("TREE"); + print_node(tree, root, false, String::new(), gosub_tree); + + /// Recursive function that prints each node in the tree + fn print_node( + tree: &TaffyTree, + node_id: NodeId, + has_sibling: bool, + lines_string: String, + gosub_tree: &RenderTree, + ) { + let layout = &tree.get_final_layout(node_id); + let display = tree.get_debug_label(node_id); + let num_children = tree.child_count(node_id); + let gosub_id = tree.get_node_context(node_id).unwrap(); + let width_style = tree.style(node_id).unwrap().size; + + let fork_string = if has_sibling { + "├── " + } else { + "└── " + }; + let node = gosub_tree.get_node(*gosub_id).unwrap(); + let mut node_render = String::new(); + + match &node.data { + RenderNodeData::Element(element) => { + node_render.push('<'); + node_render.push_str(&element.name); + for (key, value) in element.attributes.iter() { + node_render.push_str(&format!(" {}=\"{}\"", key, value)); + } + node_render.push('>'); + } + RenderNodeData::Text(text) => { + let text = text.text.replace('\n', " "); + node_render.push_str(text.trim()); + } + + _ => {} } - Ok(()) + println!( + "{lines}{fork} {display} [x: {x:<4} y: {y:<4} width: {width:<4} height: {height:<4}] ({key:?}) |{node_render}|{width_style:?}|", + lines = lines_string, + fork = fork_string, + display = display, + x = layout.location.x, + y = layout.location.y, + width = layout.size.width, + height = layout.size.height, + key = node_id, + ); + let bar = if has_sibling { "│ " } else { " " }; + let new_string = lines_string + bar; + + // Recurse into children + for (index, child) in tree.child_ids(node_id).enumerate() { + let has_sibling = index < num_children - 1; + print_node(tree, child, has_sibling, new_string.clone(), gosub_tree); + } } } diff --git a/crates/gosub_rendering/src/layout.rs b/crates/gosub_rendering/src/layout.rs index c2a8c21b4..de5ca9a4f 100644 --- a/crates/gosub_rendering/src/layout.rs +++ b/crates/gosub_rendering/src/layout.rs @@ -41,15 +41,21 @@ fn add_children_to_tree( let style = get_style_from_node(node); let node = rt.get_node(node_id).unwrap(); + let mut is_text = false; if let RenderNodeData::Text(text) = &node.data { println!("Text: {:?}", text.text); println!("Style: {:?}", style.size); + is_text = true; } let node = tree .new_with_children(style, &children) .map_err(|e| anyhow::anyhow!(e.to_string()))?; + if is_text { + println!("Node: {:?}", node); + } + tree.set_node_context(node, Some(node_id))?; Ok(node) diff --git a/crates/gosub_styling/src/render_tree.rs b/crates/gosub_styling/src/render_tree.rs index fd68ce9d7..42980d04d 100644 --- a/crates/gosub_styling/src/render_tree.rs +++ b/crates/gosub_styling/src/render_tree.rs @@ -206,15 +206,14 @@ impl RenderTree { return RenderNodeData::from_node_data( current_node.data.clone(), parent_props, - ) - .ok(); + ); }; }; - RenderNodeData::from_node_data(current_node.data.clone(), None).ok() + RenderNodeData::from_node_data(current_node.data.clone(), None) }; - let Some(data) = data() else { + let Ok(data) = data() else { eprintln!("Failed to create node data for node: {:?}", current_node_id); continue; }; @@ -281,6 +280,10 @@ impl RenderNodeData { NodeData::Document(data) => RenderNodeData::Document(data), NodeData::Element(data) => RenderNodeData::Element(data), NodeData::Text(data) => { + let text = data.value.trim(); + let text = text.replace('\n', ""); + let text = text.replace('\r', ""); + let props = props.ok_or(anyhow::anyhow!("No properties found"))?; let font_cache = &mut *FONT_RENDERER_CACHE @@ -321,7 +324,7 @@ impl RenderNodeData { }) .unwrap_or(DEFAULT_FS); - let text = PrerenderText::with_renderer(data.value.clone(), fs, font)?; + let text = PrerenderText::with_renderer(text, fs, font)?; RenderNodeData::Text(text) } NodeData::Comment(data) => RenderNodeData::Comment(data), diff --git a/src/bin/resources/gosub.html b/src/bin/resources/gosub.html index 055773a21..368b732b2 100644 --- a/src/bin/resources/gosub.html +++ b/src/bin/resources/gosub.html @@ -39,6 +39,11 @@ margin-bottom: 0; margin-left: 0; margin-right: 0; + border-width: 8px; + border-top-width: 8px; + border-color: green; + border-style: solid; + background-color: #0c3ebb44; } blockquote { @@ -70,9 +75,41 @@ a { color: cyan; text-decoration: none; - border-bottom: 2px solid #6e40c9; + + border-bottom-width: 2px; + border-top-width: 2px; + border-left-width: 2px; + border-right-width: 2px; + + + border-top-style: dashed; + border-right-style: solid; + border-left-style: solid; + border-bottom-style: solid; font-family: "Verdana", sans-serif; + + border-radius: 1rem; + transition: border-bottom 0.3s ease-in-out; + /*width: min-content;*/ + } + + #link1 { + /*border-top-color: red;*/ + /*border-left-color: green;*/ + /*border-right-color: orange;*/ + border-bottom-color: yellow; + width: 100px; + border-radius: 8px; + background-color: #ff8000aa; + } + + #link2 { + border-color: #7289da; + } + + #link3 { + border-color: #401e98; } a:link { @@ -95,6 +132,28 @@ width: 400px; height: 500px; /*aspect-ratio: 1/2;*/ + background-color: #ff8000; + border-radius: 64px; + border-top-color: #ffff00; + border-top-width: 2px; + border-top-style: dashed; + border-right-color: #22FF00; + border-right-width: 2px; + border-right-style: solid; + border-left-color: #00FFFF; + border-left-width: 16px; + border-left-style: solid; + border-bottom-color: #FF00FF; + border-bottom-width: 2px; + border-bottom-style: double; + } + + textarea { + border: 2px solid; + border-color: red green yellow orange; + margin-bottom: 2rem; + border-left: none; + border-radius: .5rem / 3px; } h1 { @@ -121,9 +180,10 @@ Gosub - The gateway to optimized searching and browsing -

Gosub

-

The gateway to optimized searching and browsing

+
+

Gosub

+

The gateway to optimized searching and browsing

+
Join us on the journey to a new web browser
+
\ No newline at end of file