diff --git a/examples/graphicstest/src/main.rs b/examples/graphicstest/src/main.rs index 4209b1733..e820edbb1 100644 --- a/examples/graphicstest/src/main.rs +++ b/examples/graphicstest/src/main.rs @@ -75,6 +75,8 @@ fn main() { y: 300., elements: PathElements::StaticElements(TRIANGLE_PATH.into()), fill_color: Color::from_rgb(0, 128, 255), + stroke_color: Color::BLACK, + stroke_width: 2.0, }); render_cache.allocate_entry(path_primitive) }; diff --git a/sixtyfps_compiler/typeregister.rs b/sixtyfps_compiler/typeregister.rs index cb7d51088..cd2fee908 100644 --- a/sixtyfps_compiler/typeregister.rs +++ b/sixtyfps_compiler/typeregister.rs @@ -279,6 +279,8 @@ impl TypeRegister { path.properties.insert("x".to_owned(), Type::Float32); path.properties.insert("y".to_owned(), Type::Float32); path.properties.insert("fill_color".to_owned(), Type::Color); + path.properties.insert("stroke_color".to_owned(), Type::Color); + path.properties.insert("stroke_width".to_owned(), Type::Float32); path.disallow_global_types_as_child_elements = true; let path_elements = { diff --git a/sixtyfps_runtime/corelib/abi/datastructures.rs b/sixtyfps_runtime/corelib/abi/datastructures.rs index 69f6925a6..8cf2088e4 100644 --- a/sixtyfps_runtime/corelib/abi/datastructures.rs +++ b/sixtyfps_runtime/corelib/abi/datastructures.rs @@ -207,6 +207,8 @@ impl Color { pub const BLACK: Color = Color::from_rgb(0, 0, 0); /// A constant for the white color pub const WHITE: Color = Color::from_rgb(255, 255, 255); + /// A constant for the transparent color + pub const TRANSPARENT: Color = Color::from_rgba(0, 0, 0, 0); } impl From for Color { @@ -428,6 +430,8 @@ pub enum RenderingPrimitive { y: f32, elements: crate::PathElements, fill_color: Color, + stroke_color: Color, + stroke_width: f32, }, } diff --git a/sixtyfps_runtime/corelib/abi/primitives.rs b/sixtyfps_runtime/corelib/abi/primitives.rs index 69f0d65d0..bdd8f2521 100644 --- a/sixtyfps_runtime/corelib/abi/primitives.rs +++ b/sixtyfps_runtime/corelib/abi/primitives.rs @@ -270,6 +270,8 @@ pub struct Path { pub y: Property, pub elements: Property, pub fill_color: Property, + pub stroke_color: Property, + pub stroke_width: Property, pub cached_rendering_data: CachedRenderingData, } @@ -291,6 +293,8 @@ impl Item for Path { y: Self::field_offsets().y.apply_pin(self).get(context), elements: Self::field_offsets().elements.apply_pin(self).get(context), fill_color: Self::field_offsets().fill_color.apply_pin(self).get(context), + stroke_color: Self::field_offsets().stroke_color.apply_pin(self).get(context), + stroke_width: Self::field_offsets().stroke_width.apply_pin(self).get(context), } } diff --git a/sixtyfps_runtime/rendering_backends/gl/lib.rs b/sixtyfps_runtime/rendering_backends/gl/lib.rs index 45bf40f2c..f44b1e1ca 100644 --- a/sixtyfps_runtime/rendering_backends/gl/lib.rs +++ b/sixtyfps_runtime/rendering_backends/gl/lib.rs @@ -3,7 +3,10 @@ use glow::{Context as GLContext, HasContext}; #[cfg(not(target_arch = "wasm32"))] use itertools::Itertools; use lyon::tessellation::geometry_builder::{BuffersBuilder, VertexBuffers}; -use lyon::tessellation::{FillAttributes, FillOptions, FillTessellator}; +use lyon::tessellation::{ + FillAttributes, FillOptions, FillTessellator, StrokeAttributes, StrokeOptions, + StrokeTessellator, +}; use sixtyfps_corelib::abi::datastructures::{ Color, ComponentWindow, ComponentWindowOpaque, Point, Rect, RenderingPrimitive, Resource, Size, }; @@ -111,6 +114,7 @@ pub struct GLRenderer { pub struct GLRenderingPrimitivesBuilder { context: Rc, fill_tesselator: FillTessellator, + stroke_tesselator: StrokeTessellator, texture_atlas: Rc>, #[cfg(not(target_arch = "wasm32"))] platform_data: Rc>, @@ -226,6 +230,7 @@ impl GraphicsBackend for GLRenderer { GLRenderingPrimitivesBuilder { context: self.context.clone(), fill_tesselator: FillTessellator::new(), + stroke_tesselator: StrokeTessellator::new(), texture_atlas: self.texture_atlas.clone(), #[cfg(not(target_arch = "wasm32"))] platform_data: self.platform_data.clone(), @@ -306,7 +311,9 @@ impl RenderingPrimitivesBuilder for GLRenderingPrimitivesBuilder { rect_path.line_to(Point::new(*width, *height)); rect_path.line_to(Point::new(0.0, *height)); rect_path.close(); - self.create_path(&rect_path.build(), FillStyle::SolidColor(*color)) + self.fill_path(&rect_path.build(), FillStyle::SolidColor(*color)) + .into_iter() + .collect() } RenderingPrimitive::Image { x: _, y: _, source } => { match source { @@ -337,8 +344,39 @@ impl RenderingPrimitivesBuilder for GLRenderingPrimitivesBuilder { if *font_pixel_size != 0. { *font_pixel_size } else { 48.0 * 72. / 96. }; smallvec![self.create_glyph_runs(text, font_family, pixel_size, *color)] } - RenderingPrimitive::Path { x: _, y: _, elements, fill_color } => self - .create_path(elements.build_path().iter(), FillStyle::SolidColor(*fill_color)), + RenderingPrimitive::Path { + x: _, + y: _, + elements, + fill_color, + stroke_color, + stroke_width, + } => { + let mut primitives = SmallVec::new(); + + if *fill_color != Color::TRANSPARENT { + primitives.extend( + self.fill_path( + elements.build_path().iter(), + FillStyle::SolidColor(*fill_color), + ) + .into_iter(), + ); + } + + if *stroke_color != Color::TRANSPARENT { + primitives.extend( + self.stroke_path( + elements.build_path().iter(), + *stroke_color, + *stroke_width, + ) + .into_iter(), + ); + } + + primitives + } }, rendering_primitive: primitive, } @@ -346,11 +384,11 @@ impl RenderingPrimitivesBuilder for GLRenderingPrimitivesBuilder { } impl GLRenderingPrimitivesBuilder { - fn create_path( + fn fill_path( &mut self, path: impl IntoIterator, style: FillStyle, - ) -> GLRenderingPrimitives { + ) -> Option { let mut geometry: VertexBuffers = VertexBuffers::new(); let fill_opts = FillOptions::default(); @@ -368,13 +406,53 @@ impl GLRenderingPrimitivesBuilder { .unwrap(); if geometry.vertices.len() == 0 || geometry.indices.len() == 0 { - return SmallVec::new(); + return None; } let vertices = GLArrayBuffer::new(&self.context, &geometry.vertices); let indices = GLIndexBuffer::new(&self.context, &geometry.indices); - smallvec![GLRenderingPrimitive::FillPath { vertices, indices, style }.into()] + Some(GLRenderingPrimitive::FillPath { vertices, indices, style }.into()) + } + + fn stroke_path( + &mut self, + path: impl IntoIterator, + stroke_color: Color, + stroke_width: f32, + ) -> Option { + let mut geometry: VertexBuffers = VertexBuffers::new(); + + let stroke_opts = StrokeOptions::DEFAULT.with_line_width(stroke_width); + + self.stroke_tesselator + .tessellate( + path, + &stroke_opts, + &mut BuffersBuilder::new( + &mut geometry, + |pos: lyon::math::Point, _: StrokeAttributes| Vertex { + _pos: [pos.x as f32, pos.y as f32], + }, + ), + ) + .unwrap(); + + if geometry.vertices.len() == 0 || geometry.indices.len() == 0 { + return None; + } + + let vertices = GLArrayBuffer::new(&self.context, &geometry.vertices); + let indices = GLIndexBuffer::new(&self.context, &geometry.indices); + + Some( + GLRenderingPrimitive::FillPath { + vertices, + indices, + style: FillStyle::SolidColor(stroke_color), + } + .into(), + ) } fn create_image( diff --git a/tests/cases/hello.60 b/tests/cases/hello.60 index 42da70773..54d00fb59 100644 --- a/tests/cases/hello.60 +++ b/tests/cases/hello.60 @@ -106,6 +106,9 @@ Hello := Rectangle { x: 100; y: 300; fill_color: green; + stroke_color: black; + stroke_width: 2.0; + LineTo { x: 100; y: 50;