Detailed changes
@@ -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"
@@ -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"
@@ -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);
+ }
}
}
@@ -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"
@@ -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(
@@ -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<Path<Pixels>>,
+ default_lines: Vec<(Path<Pixels>, Background)>,
lines: Vec<Vec<Point<Pixels>>>,
start: Point<Pixels>,
_painting: bool,
}
impl PaintingViewer {
- fn new() -> Self {
+ fn new(_window: &mut Window, _cx: &mut Context<Self>) -> 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);
@@ -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::*;
@@ -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<lyon::path::BuilderImpl>,
+ transform: Option<lyon::math::Transform>,
+ /// PathStyle of the PathBuilder
+ pub style: PathStyle,
+}
+
+impl From<lyon::path::Builder> for PathBuilder {
+ fn from(builder: lyon::path::Builder) -> Self {
+ Self {
+ raw: builder.with_svg(),
+ ..Default::default()
+ }
+ }
+}
+
+impl From<lyon::path::builder::WithSvg<lyon::path::BuilderImpl>> for PathBuilder {
+ fn from(raw: lyon::path::builder::WithSvg<lyon::path::BuilderImpl>) -> Self {
+ Self {
+ raw,
+ ..Default::default()
+ }
+ }
+}
+
+impl From<lyon::math::Point> for Point<Pixels> {
+ fn from(p: lyon::math::Point) -> Self {
+ point(px(p.x), px(p.y))
+ }
+}
+
+impl From<Point<Pixels>> for lyon::math::Point {
+ fn from(p: Point<Pixels>) -> 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<Pixels>) {
+ 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<Pixels>) {
+ 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<Pixels>, ctrl: Point<Pixels>) {
+ 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<Pixels>,
+ control_a: Point<Pixels>,
+ control_b: Point<Pixels>,
+ ) {
+ 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<Pixels>) {
+ 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<Path<Pixels>, 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<Path<Pixels>, Error> {
+ // Will contain the result of the tessellation.
+ let mut buf: VertexBuffers<lyon::math::Point, u16> = 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<Path<Pixels>, Error> {
+ // Will contain the result of the tessellation.
+ let mut buf: VertexBuffers<lyon::math::Point, u16> = 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<lyon::math::Point, u16>) -> Path<Pixels> {
+ 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
+ }
+}
@@ -27,6 +27,7 @@ struct BladeAtlasState {
tiles_by_key: FxHashMap<AtlasKey, AtlasTile>,
initializations: Vec<AtlasTextureId>,
uploads: Vec<PendingUpload>,
+ 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<gpu::TextureView>,
}
impl BladeAtlas {
- pub(crate) fn new(gpu: &Arc<gpu::Context>) -> Self {
+ pub(crate) fn new(gpu: &Arc<gpu::Context>, 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<gpu::Texture>,
+ msaa_view: Option<gpu::TextureView>,
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 {
@@ -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);
+ }
}
}
@@ -13,13 +13,14 @@ use std::borrow::Cow;
pub(crate) struct MetalAtlas(Mutex<MetalAtlasState>);
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<metal::Texture> {
+ self.0.lock().texture(id).msaa_texture.clone()
+ }
+
pub(crate) fn allocate(
&self,
size: Size<DevicePixels>,
@@ -54,6 +59,7 @@ struct MetalAtlasState {
polychrome_textures: AtlasTextureList<MetalAtlasTexture>,
path_textures: AtlasTextureList<MetalAtlasTexture>,
tiles_by_key: FxHashMap<AtlasKey, AtlasTile>,
+ 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<metal::Texture>,
+ msaa_texture: AssertSend<Option<metal::Texture>>,
live_atlas_keys: u32,
}
@@ -28,6 +28,9 @@ pub(crate) type PointF = crate::Point<f32>;
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<Mutex<InstanceBufferPool>>;
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);
@@ -715,6 +715,13 @@ impl Path<Pixels> {
}
}
+ /// Move the start, current point to the given point.
+ pub fn move_to(&mut self, to: Point<Pixels>) {
+ 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<Pixels>) {
self.contour_count += 1;
@@ -744,7 +751,8 @@ impl Path<Pixels> {
self.current = to;
}
- fn push_triangle(
+ /// Push a triangle to the Path.
+ pub fn push_triangle(
&mut self,
xy: (Point<Pixels>, Point<Pixels>, Point<Pixels>),
st: (Point<f32>, Point<f32>, Point<f32>),