diff --git a/Cargo.lock b/Cargo.lock index 552b5b4001362f..45230fa420268d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4698,6 +4698,12 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ce81f49ae8a0482e4c55ea62ebbd7e5a686af544c00b9d090bba3ff9be97b3d" +[[package]] +name = "float_next_after" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8" + [[package]] name = "flume" version = "0.11.1" @@ -5426,6 +5432,7 @@ dependencies = [ "itertools 0.14.0", "linkme", "log", + "lyon", "media", "metal", "num_cpus", @@ -7429,6 +7436,69 @@ dependencies = [ "url", ] +[[package]] +name = "lyon" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7f9cda98b5430809e63ca5197b06c7d191bf7e26dfc467d5a3f0290e2a74f" +dependencies = [ + "lyon_algorithms", + "lyon_extra", + "lyon_tessellation", +] + +[[package]] +name = "lyon_algorithms" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f13c9be19d257c7d37e70608ed858e8eab4b2afcea2e3c9a622e892acbf43c08" +dependencies = [ + "lyon_path", + "num-traits", +] + +[[package]] +name = "lyon_extra" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ca94c7bf1e2557c2798989c43416822c12fc5dcc5e17cc3307ef0e71894a955" +dependencies = [ + "lyon_path", + "thiserror 1.0.69", +] + +[[package]] +name = "lyon_geom" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8af69edc087272df438b3ee436c4bb6d7c04aa8af665cfd398feae627dbd8570" +dependencies = [ + "arrayvec", + "euclid", + "num-traits", +] + +[[package]] +name = "lyon_path" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e0b8aec2f58586f6eef237985b9a9b7cb3a3aff4417c575075cf95bf925252e" +dependencies = [ + "lyon_geom", + "num-traits", +] + +[[package]] +name = "lyon_tessellation" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579d42360a4b09846eff2feef28f538696c7d6c7439bfa65874ff3cbe0951b2c" +dependencies = [ + "float_next_after", + "lyon_path", + "num-traits", +] + [[package]] name = "mac" version = "0.1.1" diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 328aa44120bf4d..cf09a9d475dc37 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -7837,8 +7837,8 @@ impl HighlightedRange { }; let top_curve_width = curve_width(first_line.start_x, first_line.end_x); - let mut path = gpui::Path::new(first_top_right - top_curve_width); - path.curve_to(first_top_right + curve_height, first_top_right); + let mut builder = gpui::PathBuilder::fill(); + builder.curve_to(first_top_right + curve_height, first_top_right); let mut iter = lines.iter().enumerate().peekable(); while let Some((ix, line)) = iter.next() { @@ -7849,42 +7849,42 @@ impl HighlightedRange { match next_top_right.x.partial_cmp(&bottom_right.x).unwrap() { Ordering::Equal => { - path.line_to(bottom_right); + builder.line_to(bottom_right); } Ordering::Less => { let curve_width = curve_width(next_top_right.x, bottom_right.x); - path.line_to(bottom_right - curve_height); + builder.line_to(bottom_right - curve_height); if self.corner_radius > Pixels::ZERO { - path.curve_to(bottom_right - curve_width, bottom_right); + builder.curve_to(bottom_right - curve_width, bottom_right); } - path.line_to(next_top_right + curve_width); + builder.line_to(next_top_right + curve_width); if self.corner_radius > Pixels::ZERO { - path.curve_to(next_top_right + curve_height, next_top_right); + builder.curve_to(next_top_right + curve_height, next_top_right); } } Ordering::Greater => { let curve_width = curve_width(bottom_right.x, next_top_right.x); - path.line_to(bottom_right - curve_height); + builder.line_to(bottom_right - curve_height); if self.corner_radius > Pixels::ZERO { - path.curve_to(bottom_right + curve_width, bottom_right); + builder.curve_to(bottom_right + curve_width, bottom_right); } - path.line_to(next_top_right - curve_width); + builder.line_to(next_top_right - curve_width); if self.corner_radius > Pixels::ZERO { - path.curve_to(next_top_right + curve_height, next_top_right); + builder.curve_to(next_top_right + curve_height, next_top_right); } } } } else { let curve_width = curve_width(line.start_x, line.end_x); - path.line_to(bottom_right - curve_height); + builder.line_to(bottom_right - curve_height); if self.corner_radius > Pixels::ZERO { - path.curve_to(bottom_right - curve_width, bottom_right); + builder.curve_to(bottom_right - curve_width, bottom_right); } let bottom_left = point(line.start_x, bottom_right.y); - path.line_to(bottom_left + curve_width); + builder.line_to(bottom_left + curve_width); if self.corner_radius > Pixels::ZERO { - path.curve_to(bottom_left - curve_height, bottom_left); + builder.curve_to(bottom_left - curve_height, bottom_left); } } } @@ -7892,24 +7892,26 @@ impl HighlightedRange { if first_line.start_x > last_line.start_x { let curve_width = curve_width(last_line.start_x, first_line.start_x); let second_top_left = point(last_line.start_x, start_y + self.line_height); - path.line_to(second_top_left + curve_height); + builder.line_to(second_top_left + curve_height); if self.corner_radius > Pixels::ZERO { - path.curve_to(second_top_left + curve_width, second_top_left); + builder.curve_to(second_top_left + curve_width, second_top_left); } let first_bottom_left = point(first_line.start_x, second_top_left.y); - path.line_to(first_bottom_left - curve_width); + builder.line_to(first_bottom_left - curve_width); if self.corner_radius > Pixels::ZERO { - path.curve_to(first_bottom_left - curve_height, first_bottom_left); + builder.curve_to(first_bottom_left - curve_height, first_bottom_left); } } - path.line_to(first_top_left + curve_height); + builder.line_to(first_top_left + curve_height); if self.corner_radius > Pixels::ZERO { - path.curve_to(first_top_left + top_curve_width, first_top_left); + builder.curve_to(first_top_left + top_curve_width, first_top_left); } - path.line_to(first_top_right - top_curve_width); + builder.line_to(first_top_right - top_curve_width); - window.paint_path(path, self.color); + if let Ok(path) = builder.build() { + window.paint_path(path, self.color); + } } } diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index 760958b0e9deb6..3f44b843ea601a 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -108,6 +108,7 @@ thiserror.workspace = true util.workspace = true uuid.workspace = true waker-fn = "1.2.0" +lyon = "1.0" [dev-dependencies] backtrace = "0.3" @@ -117,6 +118,7 @@ rand.workspace = true util = { workspace = true, features = ["test-support"] } http_client = { workspace = true, features = ["test-support"] } unicode-segmentation.workspace = true +lyon = { version = "1.0", features = ["extra"] } [target.'cfg(target_os = "windows")'.build-dependencies] embed-resource = "3.0" diff --git a/crates/gpui/examples/gradient.rs b/crates/gpui/examples/gradient.rs index 45de8cdd0afa3d..ec4cdf9bfcdf97 100644 --- a/crates/gpui/examples/gradient.rs +++ b/crates/gpui/examples/gradient.rs @@ -218,13 +218,17 @@ impl Render for GradientViewer { let height = square_bounds.size.height; let horizontal_offset = height; let vertical_offset = px(30.); - let mut path = gpui::Path::new(square_bounds.bottom_left()); - path.line_to(square_bounds.origin + point(horizontal_offset, vertical_offset)); - path.line_to( + let mut builder = gpui::PathBuilder::fill(); + builder.move_to(square_bounds.bottom_left()); + builder + .line_to(square_bounds.origin + point(horizontal_offset, vertical_offset)); + builder.line_to( square_bounds.top_right() + point(-horizontal_offset, vertical_offset), ); - path.line_to(square_bounds.bottom_right()); - path.line_to(square_bounds.bottom_left()); + + builder.line_to(square_bounds.bottom_right()); + builder.line_to(square_bounds.bottom_left()); + let path = builder.build().unwrap(); window.paint_path( path, linear_gradient( diff --git a/crates/gpui/examples/painting.rs b/crates/gpui/examples/painting.rs index 9a8ab790650130..7c1a6a367d1335 100644 --- a/crates/gpui/examples/painting.rs +++ b/crates/gpui/examples/painting.rs @@ -1,46 +1,62 @@ use gpui::{ - canvas, div, point, prelude::*, px, size, App, Application, Bounds, Context, MouseDownEvent, - Path, Pixels, Point, Render, Window, WindowOptions, + canvas, div, linear_color_stop, linear_gradient, point, prelude::*, px, rgb, size, Application, + Background, Bounds, ColorSpace, Context, MouseDownEvent, Path, PathBuilder, PathStyle, Pixels, + Point, Render, StrokeOptions, Window, WindowOptions, }; + struct PaintingViewer { - default_lines: Vec>, + default_lines: Vec<(Path, Background)>, lines: Vec>>, start: Point, _painting: bool, } impl PaintingViewer { - fn new() -> Self { + fn new(_window: &mut Window, _cx: &mut Context) -> Self { let mut lines = vec![]; - // draw a line - let mut path = Path::new(point(px(50.), px(180.))); - path.line_to(point(px(100.), px(120.))); - // go back to close the path - path.line_to(point(px(100.), px(121.))); - path.line_to(point(px(50.), px(181.))); - lines.push(path); + // draw a Rust logo + let mut builder = lyon::path::Path::svg_builder(); + lyon::extra::rust_logo::build_logo_path(&mut builder); + // move down the Path + let mut builder: PathBuilder = builder.into(); + builder.translate(point(px(10.), px(100.))); + builder.scale(0.9); + let path = builder.build().unwrap(); + lines.push((path, gpui::black().into())); // draw a lightening bolt ⚡ - let mut path = Path::new(point(px(150.), px(200.))); - path.line_to(point(px(200.), px(125.))); - path.line_to(point(px(200.), px(175.))); - path.line_to(point(px(250.), px(100.))); - lines.push(path); + let mut builder = PathBuilder::fill(); + builder.move_to(point(px(150.), px(200.))); + builder.line_to(point(px(200.), px(125.))); + builder.line_to(point(px(200.), px(175.))); + builder.line_to(point(px(250.), px(100.))); + let path = builder.build().unwrap(); + lines.push((path, rgb(0x1d4ed8).into())); // draw a ⭐ - let mut path = Path::new(point(px(350.), px(100.))); - path.line_to(point(px(370.), px(160.))); - path.line_to(point(px(430.), px(160.))); - path.line_to(point(px(380.), px(200.))); - path.line_to(point(px(400.), px(260.))); - path.line_to(point(px(350.), px(220.))); - path.line_to(point(px(300.), px(260.))); - path.line_to(point(px(320.), px(200.))); - path.line_to(point(px(270.), px(160.))); - path.line_to(point(px(330.), px(160.))); - path.line_to(point(px(350.), px(100.))); - lines.push(path); + let mut builder = PathBuilder::fill(); + builder.move_to(point(px(350.), px(100.))); + builder.line_to(point(px(370.), px(160.))); + builder.line_to(point(px(430.), px(160.))); + builder.line_to(point(px(380.), px(200.))); + builder.line_to(point(px(400.), px(260.))); + builder.line_to(point(px(350.), px(220.))); + builder.line_to(point(px(300.), px(260.))); + builder.line_to(point(px(320.), px(200.))); + builder.line_to(point(px(270.), px(160.))); + builder.line_to(point(px(330.), px(160.))); + builder.line_to(point(px(350.), px(100.))); + let path = builder.build().unwrap(); + lines.push(( + path, + linear_gradient( + 180., + linear_color_stop(rgb(0xFACC15), 0.7), + linear_color_stop(rgb(0xD56D0C), 1.), + ) + .color_space(ColorSpace::Oklab), + )); let square_bounds = Bounds { origin: point(px(450.), px(100.)), @@ -49,18 +65,42 @@ impl PaintingViewer { let height = square_bounds.size.height; let horizontal_offset = height; let vertical_offset = px(30.); - let mut path = Path::new(square_bounds.bottom_left()); - path.curve_to( + let mut builder = PathBuilder::fill(); + builder.move_to(square_bounds.bottom_left()); + builder.curve_to( square_bounds.origin + point(horizontal_offset, vertical_offset), square_bounds.origin + point(px(0.0), vertical_offset), ); - path.line_to(square_bounds.top_right() + point(-horizontal_offset, vertical_offset)); - path.curve_to( + builder.line_to(square_bounds.top_right() + point(-horizontal_offset, vertical_offset)); + builder.curve_to( square_bounds.bottom_right(), square_bounds.top_right() + point(px(0.0), vertical_offset), ); - path.line_to(square_bounds.bottom_left()); - lines.push(path); + builder.line_to(square_bounds.bottom_left()); + let path = builder.build().unwrap(); + lines.push(( + path, + linear_gradient( + 180., + linear_color_stop(gpui::blue(), 0.4), + linear_color_stop(gpui::red(), 1.), + ), + )); + + // draw a wave + let options = StrokeOptions::default() + .with_line_width(1.) + .with_line_join(lyon::path::LineJoin::Bevel); + let mut builder = PathBuilder::stroke(px(1.)).with_style(PathStyle::Stroke(options)); + builder.move_to(point(px(40.), px(320.))); + for i in 0..50 { + builder.line_to(point( + px(40.0 + i as f32 * 10.0), + px(320.0 + (i as f32 * 10.0).sin() * 40.0), + )); + } + let path = builder.build().unwrap(); + lines.push((path, gpui::green().into())); Self { default_lines: lines.clone(), @@ -115,27 +155,28 @@ impl Render for PaintingViewer { canvas( move |_, _, _| {}, move |_, _, window, _| { - const STROKE_WIDTH: Pixels = px(2.0); - for path in default_lines { - window.paint_path(path, gpui::black()); + + for (path, color) in default_lines { + window.paint_path(path, color); } + for points in lines { - let mut path = Path::new(points[0]); - for p in points.iter().skip(1) { - path.line_to(*p); + if points.len() < 2 { + continue; } - let mut last = points.last().unwrap(); - for p in points.iter().rev() { - let mut offset_x = px(0.); - if last.x == p.x { - offset_x = STROKE_WIDTH; + let mut builder = PathBuilder::stroke(px(1.)); + for (i, p) in points.into_iter().enumerate() { + if i == 0 { + builder.move_to(p); + } else { + builder.line_to(p); } - path.line_to(point(p.x + offset_x, p.y + STROKE_WIDTH)); - last = p; } - window.paint_path(path, gpui::black()); + if let Ok(path) = builder.build() { + window.paint_path(path, gpui::black()); + } } }, ) @@ -185,13 +226,13 @@ impl Render for PaintingViewer { } fn main() { - Application::new().run(|cx: &mut App| { + Application::new().run(|cx| { cx.open_window( WindowOptions { focus: true, ..Default::default() }, - |_, cx| cx.new(|_| PaintingViewer::new()), + |window, cx| cx.new(|cx| PaintingViewer::new(window, cx)), ) .unwrap(); cx.activate(true); diff --git a/crates/gpui/src/gpui.rs b/crates/gpui/src/gpui.rs index 590aa4cd87ee40..54c9bcf49fbb9d 100644 --- a/crates/gpui/src/gpui.rs +++ b/crates/gpui/src/gpui.rs @@ -82,6 +82,7 @@ mod input; mod interactive; mod key_dispatch; mod keymap; +mod path_builder; mod platform; pub mod prelude; mod scene; @@ -135,6 +136,7 @@ pub use input::*; pub use interactive::*; use key_dispatch::*; pub use keymap::*; +pub use path_builder::*; pub use platform::*; pub use refineable::*; pub use scene::*; diff --git a/crates/gpui/src/path_builder.rs b/crates/gpui/src/path_builder.rs new file mode 100644 index 00000000000000..0fd8eb6fa54583 --- /dev/null +++ b/crates/gpui/src/path_builder.rs @@ -0,0 +1,241 @@ +use anyhow::Error; +use etagere::euclid::Vector2D; +use lyon::geom::Angle; +use lyon::tessellation::{ + BuffersBuilder, FillTessellator, FillVertex, StrokeTessellator, StrokeVertex, VertexBuffers, +}; + +pub use lyon::math::Transform; +pub use lyon::tessellation::{FillOptions, FillRule, StrokeOptions}; + +use crate::{point, px, Path, Pixels, Point}; + +/// Style of the PathBuilder +pub enum PathStyle { + /// Stroke style + Stroke(StrokeOptions), + /// Fill style + Fill(FillOptions), +} + +/// A [`Path`] builder. +pub struct PathBuilder { + raw: lyon::path::builder::WithSvg, + transform: Option, + /// PathStyle of the PathBuilder + pub style: PathStyle, +} + +impl From for PathBuilder { + fn from(builder: lyon::path::Builder) -> Self { + Self { + raw: builder.with_svg(), + ..Default::default() + } + } +} + +impl From> for PathBuilder { + fn from(raw: lyon::path::builder::WithSvg) -> Self { + Self { + raw, + ..Default::default() + } + } +} + +impl From for Point { + fn from(p: lyon::math::Point) -> Self { + point(px(p.x), px(p.y)) + } +} + +impl From> for lyon::math::Point { + fn from(p: Point) -> Self { + lyon::math::point(p.x.0, p.y.0) + } +} + +impl Default for PathBuilder { + fn default() -> Self { + Self { + raw: lyon::path::Path::builder().with_svg(), + style: PathStyle::Fill(FillOptions::default()), + transform: None, + } + } +} + +impl PathBuilder { + /// Creates a new [`PathBuilder`] to build a Stroke path. + pub fn stroke(width: Pixels) -> Self { + Self { + style: PathStyle::Stroke(StrokeOptions::default().with_line_width(width.0)), + ..Self::default() + } + } + + /// Creates a new [`PathBuilder`] to build a Fill path. + pub fn fill() -> Self { + Self::default() + } + + /// Sets the style of the [`PathBuilder`]. + pub fn with_style(self, style: PathStyle) -> Self { + Self { style, ..self } + } + + /// Move the current point to the given point. + #[inline] + pub fn move_to(&mut self, to: Point) { + self.raw.move_to(to.into()); + } + + /// Draw a straight line from the current point to the given point. + #[inline] + pub fn line_to(&mut self, to: Point) { + self.raw.line_to(to.into()); + } + + /// Draw a curve from the current point to the given point, using the given control point. + #[inline] + pub fn curve_to(&mut self, to: Point, ctrl: Point) { + self.raw.quadratic_bezier_to(ctrl.into(), to.into()); + } + + /// Adds a cubic Bézier to the [`Path`] given its two control points + /// and its end point. + #[inline] + pub fn cubic_bezier_to( + &mut self, + to: Point, + control_a: Point, + control_b: Point, + ) { + self.raw + .cubic_bezier_to(control_a.into(), control_b.into(), to.into()); + } + + /// Close the current sub-path. + #[inline] + pub fn close(&mut self) { + self.raw.close(); + } + + /// Applies a transform to the path. + #[inline] + pub fn transform(&mut self, transform: Transform) { + self.transform = Some(transform); + } + + /// Applies a translation to the path. + #[inline] + pub fn translate(&mut self, to: Point) { + if let Some(transform) = self.transform { + self.transform = Some(transform.then_translate(Vector2D::new(to.x.0, to.y.0))); + } else { + self.transform = Some(Transform::translation(to.x.0, to.y.0)) + } + } + + /// Applies a scale to the path. + #[inline] + pub fn scale(&mut self, scale: f32) { + if let Some(transform) = self.transform { + self.transform = Some(transform.then_scale(scale, scale)); + } else { + self.transform = Some(Transform::scale(scale, scale)); + } + } + + /// Applies a rotation to the path. + /// + /// The `angle` is in degrees value in the range 0.0 to 360.0. + #[inline] + pub fn rotate(&mut self, angle: f32) { + let radians = angle.to_radians(); + if let Some(transform) = self.transform { + self.transform = Some(transform.then_rotate(Angle::radians(radians))); + } else { + self.transform = Some(Transform::rotation(Angle::radians(radians))); + } + } + + /// Builds into a [`Path`]. + #[inline] + pub fn build(self) -> Result, Error> { + let path = if let Some(transform) = self.transform { + self.raw.build().transformed(&transform) + } else { + self.raw.build() + }; + + match self.style { + PathStyle::Stroke(options) => Self::tessellate_stroke(&path, &options), + PathStyle::Fill(options) => Self::tessellate_fill(&path, &options), + } + } + + fn tessellate_fill( + path: &lyon::path::Path, + options: &FillOptions, + ) -> Result, Error> { + // Will contain the result of the tessellation. + let mut buf: VertexBuffers = VertexBuffers::new(); + let mut tessellator = FillTessellator::new(); + + // Compute the tessellation. + tessellator.tessellate_path( + path, + options, + &mut BuffersBuilder::new(&mut buf, |vertex: FillVertex| vertex.position()), + )?; + + Ok(Self::build_path(buf)) + } + + fn tessellate_stroke( + path: &lyon::path::Path, + options: &StrokeOptions, + ) -> Result, Error> { + // Will contain the result of the tessellation. + let mut buf: VertexBuffers = VertexBuffers::new(); + let mut tessellator = StrokeTessellator::new(); + + // Compute the tessellation. + tessellator.tessellate_path( + path, + options, + &mut BuffersBuilder::new(&mut buf, |vertex: StrokeVertex| vertex.position()), + )?; + + Ok(Self::build_path(buf)) + } + + /// Builds a [`Path`] from a [`lyon::VertexBuffers`]. + pub fn build_path(buf: VertexBuffers) -> Path { + if buf.vertices.is_empty() { + return Path::new(Point::default()); + } + + let first_point = buf.vertices[0]; + + let mut path = Path::new(first_point.into()); + for i in 0..buf.indices.len() / 3 { + let i0 = buf.indices[i * 3] as usize; + let i1 = buf.indices[i * 3 + 1] as usize; + let i2 = buf.indices[i * 3 + 2] as usize; + + let v0 = buf.vertices[i0]; + let v1 = buf.vertices[i1]; + let v2 = buf.vertices[i2]; + + path.push_triangle( + (v0.into(), v1.into(), v2.into()), + (point(0., 1.), point(0., 1.), point(0., 1.)), + ); + } + + path + } +} diff --git a/crates/gpui/src/scene.rs b/crates/gpui/src/scene.rs index 778a5d1f273418..b837f2ad9131b6 100644 --- a/crates/gpui/src/scene.rs +++ b/crates/gpui/src/scene.rs @@ -715,6 +715,13 @@ impl Path { } } + /// Move the start, current point to the given point. + pub fn move_to(&mut self, to: Point) { + self.contour_count += 1; + self.start = to; + self.current = to; + } + /// Draw a straight line from the current point to the given point. pub fn line_to(&mut self, to: Point) { self.contour_count += 1; @@ -744,7 +751,8 @@ impl Path { self.current = to; } - fn push_triangle( + /// Push a triangle to the Path. + pub fn push_triangle( &mut self, xy: (Point, Point, Point), st: (Point, Point, Point),