diff --git a/Cargo.lock b/Cargo.lock index 851c2b50ee..3065f2f68d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1868,7 +1868,7 @@ dependencies = [ [[package]] name = "blade-graphics" version = "0.6.0" -source = "git+https://github.com/kvark/blade?rev=091a8401033847bb9b6ace3fcf70448d069621c5#091a8401033847bb9b6ace3fcf70448d069621c5" +source = "git+https://github.com/kvark/blade?rev=b16f5c7bd873c7126f48c82c39e7ae64602ae74f#b16f5c7bd873c7126f48c82c39e7ae64602ae74f" dependencies = [ "ash", "ash-window", @@ -1900,7 +1900,7 @@ dependencies = [ [[package]] name = "blade-macros" version = "0.3.0" -source = "git+https://github.com/kvark/blade?rev=091a8401033847bb9b6ace3fcf70448d069621c5#091a8401033847bb9b6ace3fcf70448d069621c5" +source = "git+https://github.com/kvark/blade?rev=b16f5c7bd873c7126f48c82c39e7ae64602ae74f#b16f5c7bd873c7126f48c82c39e7ae64602ae74f" dependencies = [ "proc-macro2", "quote", @@ -1910,7 +1910,7 @@ dependencies = [ [[package]] name = "blade-util" version = "0.2.0" -source = "git+https://github.com/kvark/blade?rev=091a8401033847bb9b6ace3fcf70448d069621c5#091a8401033847bb9b6ace3fcf70448d069621c5" +source = "git+https://github.com/kvark/blade?rev=b16f5c7bd873c7126f48c82c39e7ae64602ae74f#b16f5c7bd873c7126f48c82c39e7ae64602ae74f" dependencies = [ "blade-graphics", "bytemuck", @@ -4725,6 +4725,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" @@ -5476,6 +5482,7 @@ dependencies = [ "inventory", "itertools 0.14.0", "log", + "lyon", "media", "metal", "naga", @@ -7498,6 +7505,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/Cargo.toml b/Cargo.toml index 9f77103e62..73160b0cd1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -376,9 +376,9 @@ async-watch = "0.3.1" async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] } base64 = "0.22" bitflags = "2.6.0" -blade-graphics = { git = "https://github.com/kvark/blade", rev = "091a8401033847bb9b6ace3fcf70448d069621c5" } -blade-macros = { git = "https://github.com/kvark/blade", rev = "091a8401033847bb9b6ace3fcf70448d069621c5" } -blade-util = { git = "https://github.com/kvark/blade", rev = "091a8401033847bb9b6ace3fcf70448d069621c5" } +blade-graphics = { git = "https://github.com/kvark/blade", rev = "b16f5c7bd873c7126f48c82c39e7ae64602ae74f" } +blade-macros = { git = "https://github.com/kvark/blade", rev = "b16f5c7bd873c7126f48c82c39e7ae64602ae74f" } +blade-util = { git = "https://github.com/kvark/blade", rev = "b16f5c7bd873c7126f48c82c39e7ae64602ae74f" } naga = { version = "23.1.0", features = ["wgsl-in"] } blake3 = "1.5.3" bytes = "1.0" diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 99c1039348..32c2d4a5de 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -8149,8 +8149,9 @@ 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.move_to(first_top_right - top_curve_width); + 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() { @@ -8161,42 +8162,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); } } } @@ -8204,24 +8205,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 a0220cd572..05a5b28e76 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" [target.'cfg(target_os = "macos")'.dependencies] block = "0.1" @@ -205,6 +206,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 45de8cdd0a..ec4cdf9bfc 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 9a8ab79065..7c1a6a367d 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 db33bfca2e..1ebfc643ee 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 0000000000..0fd8eb6fa5 --- /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/platform/blade/blade_atlas.rs b/crates/gpui/src/platform/blade/blade_atlas.rs index fb703f2a41..2783d57127 100644 --- a/crates/gpui/src/platform/blade/blade_atlas.rs +++ b/crates/gpui/src/platform/blade/blade_atlas.rs @@ -27,6 +27,7 @@ struct BladeAtlasState { tiles_by_key: FxHashMap, initializations: Vec, uploads: Vec, + path_sample_count: u32, } #[cfg(gles)] @@ -42,10 +43,11 @@ impl BladeAtlasState { pub struct BladeTextureInfo { pub size: gpu::Extent, pub raw_view: gpu::TextureView, + pub msaa_view: Option, } impl BladeAtlas { - pub(crate) fn new(gpu: &Arc) -> Self { + pub(crate) fn new(gpu: &Arc, path_sample_count: u32) -> Self { BladeAtlas(Mutex::new(BladeAtlasState { gpu: Arc::clone(gpu), upload_belt: BufferBelt::new(BufferBeltDescriptor { @@ -57,6 +59,7 @@ impl BladeAtlas { tiles_by_key: Default::default(), initializations: Vec::new(), uploads: Vec::new(), + path_sample_count, })) } @@ -106,6 +109,7 @@ impl BladeAtlas { depth: 1, }, raw_view: texture.raw_view, + msaa_view: texture.msaa_view, } } } @@ -204,6 +208,39 @@ impl BladeAtlasState { } } + // We currently only enable MSAA for path textures. + let (msaa, msaa_view) = if self.path_sample_count > 1 && kind == AtlasTextureKind::Path { + let msaa = self.gpu.create_texture(gpu::TextureDesc { + name: "msaa path texture", + format, + size: gpu::Extent { + width: size.width.into(), + height: size.height.into(), + depth: 1, + }, + array_layer_count: 1, + mip_level_count: 1, + sample_count: self.path_sample_count, + dimension: gpu::TextureDimension::D2, + usage: gpu::TextureUsage::TARGET, + }); + + ( + Some(msaa), + Some(self.gpu.create_texture_view( + msaa, + gpu::TextureViewDesc { + name: "msaa texture view", + format, + dimension: gpu::ViewDimension::D2, + subresources: &Default::default(), + }, + )), + ) + } else { + (None, None) + }; + let raw = self.gpu.create_texture(gpu::TextureDesc { name: "atlas", format, @@ -240,6 +277,8 @@ impl BladeAtlasState { format, raw, raw_view, + msaa, + msaa_view, live_atlas_keys: 0, }; @@ -354,6 +393,8 @@ struct BladeAtlasTexture { allocator: BucketedAtlasAllocator, raw: gpu::Texture, raw_view: gpu::TextureView, + msaa: Option, + msaa_view: Option, format: gpu::TextureFormat, live_atlas_keys: u32, } @@ -381,6 +422,12 @@ impl BladeAtlasTexture { fn destroy(&mut self, gpu: &gpu::Context) { gpu.destroy_texture(self.raw); gpu.destroy_texture_view(self.raw_view); + if let Some(msaa) = self.msaa { + gpu.destroy_texture(msaa); + } + if let Some(msaa_view) = self.msaa_view { + gpu.destroy_texture_view(msaa_view); + } } fn bytes_per_pixel(&self) -> u8 { diff --git a/crates/gpui/src/platform/blade/blade_renderer.rs b/crates/gpui/src/platform/blade/blade_renderer.rs index ee8ffdfda7..200ebaaf07 100644 --- a/crates/gpui/src/platform/blade/blade_renderer.rs +++ b/crates/gpui/src/platform/blade/blade_renderer.rs @@ -7,16 +7,18 @@ use crate::{ MonochromeSprite, Path, PathId, PathVertex, PolychromeSprite, PrimitiveBatch, Quad, ScaledPixels, Scene, Shadow, Size, Underline, }; +use blade_graphics as gpu; +use blade_util::{BufferBelt, BufferBeltDescriptor}; use bytemuck::{Pod, Zeroable}; use collections::HashMap; #[cfg(target_os = "macos")] use media::core_video::CVMetalTextureCache; - -use blade_graphics as gpu; -use blade_util::{BufferBelt, BufferBeltDescriptor}; use std::{mem, sync::Arc}; const MAX_FRAME_TIME_MS: u32 = 10000; +// Use 4x MSAA, all devices support it. +// https://developer.apple.com/documentation/metal/mtldevice/1433355-supportstexturesamplecount +const PATH_SAMPLE_COUNT: u32 = 4; #[repr(C)] #[derive(Clone, Copy, Pod, Zeroable)] @@ -208,7 +210,10 @@ impl BladePipelines { blend: Some(gpu::BlendState::ADDITIVE), write_mask: gpu::ColorWrites::default(), }], - multisample_state: gpu::MultisampleState::default(), + multisample_state: gpu::MultisampleState { + sample_count: PATH_SAMPLE_COUNT, + ..Default::default() + }, }), paths: gpu.create_render_pipeline(gpu::RenderPipelineDesc { name: "paths", @@ -348,7 +353,7 @@ impl BladeRenderer { min_chunk_size: 0x1000, alignment: 0x40, // Vulkan `minStorageBufferOffsetAlignment` on Intel Xe }); - let atlas = Arc::new(BladeAtlas::new(&context.gpu)); + let atlas = Arc::new(BladeAtlas::new(&context.gpu, PATH_SAMPLE_COUNT)); let atlas_sampler = context.gpu.create_sampler(gpu::SamplerDesc { name: "atlas", mag_filter: gpu::FilterMode::Linear, @@ -497,27 +502,38 @@ impl BladeRenderer { }; let vertex_buf = unsafe { self.instance_belt.alloc_typed(&vertices, &self.gpu) }; - let mut pass = self.command_encoder.render( + let frame_view = tex_info.raw_view; + let color_target = if let Some(msaa_view) = tex_info.msaa_view { + gpu::RenderTarget { + view: msaa_view, + init_op: gpu::InitOp::Clear(gpu::TextureColor::OpaqueBlack), + finish_op: gpu::FinishOp::ResolveTo(frame_view), + } + } else { + gpu::RenderTarget { + view: frame_view, + init_op: gpu::InitOp::Clear(gpu::TextureColor::OpaqueBlack), + finish_op: gpu::FinishOp::Store, + } + }; + + if let mut pass = self.command_encoder.render( "paths", gpu::RenderTargetSet { - colors: &[gpu::RenderTarget { - view: tex_info.raw_view, - init_op: gpu::InitOp::Clear(gpu::TextureColor::OpaqueBlack), - finish_op: gpu::FinishOp::Store, - }], + colors: &[color_target], depth_stencil: None, }, - ); - - let mut encoder = pass.with(&self.pipelines.path_rasterization); - encoder.bind( - 0, - &ShaderPathRasterizationData { - globals, - b_path_vertices: vertex_buf, - }, - ); - encoder.draw(0, vertices.len() as u32, 0, 1); + ) { + let mut encoder = pass.with(&self.pipelines.path_rasterization); + encoder.bind( + 0, + &ShaderPathRasterizationData { + globals, + b_path_vertices: vertex_buf, + }, + ); + encoder.draw(0, vertices.len() as u32, 0, 1); + } } } diff --git a/crates/gpui/src/platform/mac/metal_atlas.rs b/crates/gpui/src/platform/mac/metal_atlas.rs index ca595c5ce3..4662761a7d 100644 --- a/crates/gpui/src/platform/mac/metal_atlas.rs +++ b/crates/gpui/src/platform/mac/metal_atlas.rs @@ -13,13 +13,14 @@ use std::borrow::Cow; pub(crate) struct MetalAtlas(Mutex); impl MetalAtlas { - pub(crate) fn new(device: Device) -> Self { + pub(crate) fn new(device: Device, path_sample_count: u32) -> Self { MetalAtlas(Mutex::new(MetalAtlasState { device: AssertSend(device), monochrome_textures: Default::default(), polychrome_textures: Default::default(), path_textures: Default::default(), tiles_by_key: Default::default(), + path_sample_count, })) } @@ -27,6 +28,10 @@ impl MetalAtlas { self.0.lock().texture(id).metal_texture.clone() } + pub(crate) fn msaa_texture(&self, id: AtlasTextureId) -> Option { + self.0.lock().texture(id).msaa_texture.clone() + } + pub(crate) fn allocate( &self, size: Size, @@ -54,6 +59,7 @@ struct MetalAtlasState { polychrome_textures: AtlasTextureList, path_textures: AtlasTextureList, tiles_by_key: FxHashMap, + path_sample_count: u32, } impl PlatformAtlas for MetalAtlas { @@ -176,6 +182,18 @@ impl MetalAtlasState { texture_descriptor.set_usage(usage); let metal_texture = self.device.new_texture(&texture_descriptor); + // We currently only enable MSAA for path textures. + let msaa_texture = if self.path_sample_count > 1 && kind == AtlasTextureKind::Path { + let mut descriptor = texture_descriptor.clone(); + descriptor.set_texture_type(metal::MTLTextureType::D2Multisample); + descriptor.set_storage_mode(metal::MTLStorageMode::Private); + descriptor.set_sample_count(self.path_sample_count as _); + let msaa_texture = self.device.new_texture(&descriptor); + Some(msaa_texture) + } else { + None + }; + let texture_list = match kind { AtlasTextureKind::Monochrome => &mut self.monochrome_textures, AtlasTextureKind::Polychrome => &mut self.polychrome_textures, @@ -191,6 +209,7 @@ impl MetalAtlasState { }, allocator: etagere::BucketedAtlasAllocator::new(size.into()), metal_texture: AssertSend(metal_texture), + msaa_texture: AssertSend(msaa_texture), live_atlas_keys: 0, }; @@ -217,6 +236,7 @@ struct MetalAtlasTexture { id: AtlasTextureId, allocator: BucketedAtlasAllocator, metal_texture: AssertSend, + msaa_texture: AssertSend>, live_atlas_keys: u32, } diff --git a/crates/gpui/src/platform/mac/metal_renderer.rs b/crates/gpui/src/platform/mac/metal_renderer.rs index c290d12f7e..56109d2ff6 100644 --- a/crates/gpui/src/platform/mac/metal_renderer.rs +++ b/crates/gpui/src/platform/mac/metal_renderer.rs @@ -28,6 +28,9 @@ pub(crate) type PointF = crate::Point; const SHADERS_METALLIB: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/shaders.metallib")); #[cfg(feature = "runtime_shaders")] const SHADERS_SOURCE_FILE: &str = include_str!(concat!(env!("OUT_DIR"), "/stitched_shaders.metal")); +// Use 4x MSAA, all devices support it. +// https://developer.apple.com/documentation/metal/mtldevice/1433355-supportstexturesamplecount +const PATH_SAMPLE_COUNT: u32 = 4; pub type Context = Arc>; pub type Renderer = MetalRenderer; @@ -170,6 +173,7 @@ impl MetalRenderer { "path_rasterization_vertex", "path_rasterization_fragment", MTLPixelFormat::R16Float, + PATH_SAMPLE_COUNT, ); let path_sprites_pipeline_state = build_pipeline_state( &device, @@ -229,7 +233,7 @@ impl MetalRenderer { ); let command_queue = device.new_command_queue(); - let sprite_atlas = Arc::new(MetalAtlas::new(device.clone())); + let sprite_atlas = Arc::new(MetalAtlas::new(device.clone(), PATH_SAMPLE_COUNT)); let core_video_texture_cache = unsafe { CVMetalTextureCache::new(device.as_ptr()).unwrap() }; @@ -531,10 +535,20 @@ impl MetalRenderer { .unwrap(); let texture = self.sprite_atlas.metal_texture(texture_id); - color_attachment.set_texture(Some(&texture)); - color_attachment.set_load_action(metal::MTLLoadAction::Clear); - color_attachment.set_store_action(metal::MTLStoreAction::Store); + let msaa_texture = self.sprite_atlas.msaa_texture(texture_id); + + if let Some(msaa_texture) = msaa_texture { + color_attachment.set_texture(Some(&msaa_texture)); + color_attachment.set_resolve_texture(Some(&texture)); + color_attachment.set_load_action(metal::MTLLoadAction::Clear); + color_attachment.set_store_action(metal::MTLStoreAction::MultisampleResolve); + } else { + color_attachment.set_texture(Some(&texture)); + color_attachment.set_load_action(metal::MTLLoadAction::Clear); + color_attachment.set_store_action(metal::MTLStoreAction::Store); + } color_attachment.set_clear_color(metal::MTLClearColor::new(0., 0., 0., 1.)); + let command_encoder = command_buffer.new_render_command_encoder(render_pass_descriptor); command_encoder.set_render_pipeline_state(&self.paths_rasterization_pipeline_state); command_encoder.set_vertex_buffer( @@ -1160,6 +1174,7 @@ fn build_path_rasterization_pipeline_state( vertex_fn_name: &str, fragment_fn_name: &str, pixel_format: metal::MTLPixelFormat, + path_sample_count: u32, ) -> metal::RenderPipelineState { let vertex_fn = library .get_function(vertex_fn_name, None) @@ -1172,6 +1187,10 @@ fn build_path_rasterization_pipeline_state( descriptor.set_label(label); descriptor.set_vertex_function(Some(vertex_fn.as_ref())); descriptor.set_fragment_function(Some(fragment_fn.as_ref())); + if path_sample_count > 1 { + descriptor.set_raster_sample_count(path_sample_count as _); + descriptor.set_alpha_to_coverage_enabled(true); + } let color_attachment = descriptor.color_attachments().object_at(0).unwrap(); color_attachment.set_pixel_format(pixel_format); color_attachment.set_blending_enabled(true); diff --git a/crates/gpui/src/scene.rs b/crates/gpui/src/scene.rs index 778a5d1f27..b837f2ad91 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),