From 4b9dced354406d90ccf9d5635460f9c77f22a35e Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Tue, 26 Nov 2024 19:17:57 +0800 Subject: [PATCH 1/3] gpui: Draw Path with 2x to anti-aliasing and add move_to, translate method. Follow-up to #21025 Release Notes: - N/A --- crates/gpui/examples/painting.rs | 82 +++++++++++-------- crates/gpui/src/geometry.rs | 1 + .../gpui/src/platform/blade/blade_renderer.rs | 19 +++-- .../gpui/src/platform/mac/metal_renderer.rs | 17 ++-- crates/gpui/src/scene.rs | 23 +++++- crates/gpui/src/window.rs | 4 +- 6 files changed, 97 insertions(+), 49 deletions(-) diff --git a/crates/gpui/examples/painting.rs b/crates/gpui/examples/painting.rs index 6e5fe25dfd2b45..dc16f3eec02cd1 100644 --- a/crates/gpui/examples/painting.rs +++ b/crates/gpui/examples/painting.rs @@ -1,9 +1,9 @@ use gpui::{ - canvas, div, point, prelude::*, px, size, App, AppContext, Bounds, MouseDownEvent, Path, + canvas, div, point, prelude::*, px, size, App, AppContext, Bounds, Hsla, MouseDownEvent, Path, Pixels, Point, Render, ViewContext, WindowOptions, }; struct PaintingViewer { - default_lines: Vec>, + default_lines: Vec<(Path, Hsla)>, lines: Vec>>, start: Point, _painting: bool, @@ -14,33 +14,45 @@ impl PaintingViewer { 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); + let mut path = Path::new(point(px(20.), px(100.))); + path.line_to(point(px(50.), px(160.))); + path.line_to(point(px(60.), px(100.))); + path.line_to(point(px(60.5), px(101.))); + path.line_to(point(px(51.), px(160.))); + path.line_to(point(px(21.), px(100.))); + lines.push((path, gpui::black())); + + // draw a triangle + let mut path = Path::new(point(px(25.), px(0.))); + path.line_to(point(px(50.), px(50.))); + path.line_to(point(px(0.), px(50.))); + path.translate(point(px(100.), px(100.))); + lines.push((path, gpui::red())); // 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 path = Path::new(point(px(-50.), px(50.))); + path.line_to(point(px(0.), px(-25.))); + path.line_to(point(px(0.), px(0.))); + path.move_to(point(px(0.), px(0.))); + path.line_to(point(px(50.), px(-50.))); + path.line_to(point(px(0.), px(25.))); + path.line_to(point(px(0.), px(5.))); + path.translate(point(px(220.), px(150.))); + lines.push((path, gpui::blue())); // 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 path = Path::new(point(px(76.8), px(116.864))); + path.line_to(point(px(31.6608), px(142.1312))); + path.line_to(point(px(41.7408), px(91.392))); + path.line_to(point(px(3.7568), px(56.2688))); + path.line_to(point(px(55.1296), px(50.176))); + path.line_to(point(px(76.8), px(3.2))); + path.line_to(point(px(98.4704), px(50.176))); + path.line_to(point(px(149.8432), px(56.2688))); + path.line_to(point(px(111.8592), px(91.392))); + path.line_to(point(px(121.9392), px(142.1312))); + path.translate(point(px(270.), px(80.))); + lines.push((path, gpui::yellow())); let square_bounds = Bounds { origin: point(px(450.), px(100.)), @@ -59,8 +71,7 @@ impl PaintingViewer { square_bounds.lower_right(), square_bounds.upper_right() + point(px(0.0), vertical_offset), ); - path.line_to(square_bounds.lower_left()); - lines.push(path); + lines.push((path, gpui::green())); Self { default_lines: lines.clone(), @@ -115,9 +126,9 @@ impl Render for PaintingViewer { canvas( move |_, _| {}, move |_, _, cx| { - const STROKE_WIDTH: Pixels = px(2.0); - for path in default_lines { - cx.paint_path(path, gpui::black()); + const STROKE_WIDTH: Pixels = px(1.0); + for (path, color) in default_lines { + cx.paint_path(path, color); } for points in lines { let mut path = Path::new(points[0]); @@ -127,11 +138,12 @@ impl Render for PaintingViewer { 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; - } - path.line_to(point(p.x + offset_x, p.y + STROKE_WIDTH)); + let dx = p.x - last.x; + let dy = p.y - last.y; + let distance = (dx * dx + dy * dy).0.sqrt(); + let offset_x = (STROKE_WIDTH * dy / distance).clamp(px(0.0), STROKE_WIDTH); + let offset_y = (STROKE_WIDTH * dx / distance).clamp(px(0.0), STROKE_WIDTH); + path.line_to(point(p.x + offset_x, p.y - offset_y)); last = p; } diff --git a/crates/gpui/src/geometry.rs b/crates/gpui/src/geometry.rs index 9e0b9b90140391..2972fff4176d10 100644 --- a/crates/gpui/src/geometry.rs +++ b/crates/gpui/src/geometry.rs @@ -2456,6 +2456,7 @@ impl From for Pixels { Copy, Default, Div, + Mul, Eq, Hash, Ord, diff --git a/crates/gpui/src/platform/blade/blade_renderer.rs b/crates/gpui/src/platform/blade/blade_renderer.rs index 5c37caf2cbab02..576e975882a318 100644 --- a/crates/gpui/src/platform/blade/blade_renderer.rs +++ b/crates/gpui/src/platform/blade/blade_renderer.rs @@ -5,7 +5,7 @@ use super::{BladeAtlas, PATH_TEXTURE_FORMAT}; use crate::{ AtlasTextureKind, AtlasTile, Bounds, ContentMask, DevicePixels, GPUSpecs, Hsla, MonochromeSprite, Path, PathId, PathVertex, PolychromeSprite, PrimitiveBatch, Quad, - ScaledPixels, Scene, Shadow, Size, Underline, + ScaledPixels, Scene, Shadow, Size, Underline, PATH_SUBPIXEL_VARIANTS, }; use bytemuck::{Pod, Zeroable}; use collections::HashMap; @@ -622,12 +622,19 @@ impl BladeRenderer { for path in paths { let tile = &self.path_tiles[&path.id]; let tex_info = self.atlas.get_texture_info(tile.texture_id); - let origin = path.bounds.intersect(&path.content_mask.bounds).origin; + let origin = path + .bounds + .intersect(&path.content_mask.bounds) + .origin + .map(|p| (p / PATH_SUBPIXEL_VARIANTS).floor()); + let size = tile + .bounds + .size + .map(|s| s / PATH_SUBPIXEL_VARIANTS as i32) + .map(Into::into); + let sprites = [PathSprite { - bounds: Bounds { - origin: origin.map(|p| p.floor()), - size: tile.bounds.size.map(Into::into), - }, + bounds: Bounds { origin, size }, color: path.color, tile: (*tile).clone(), }]; diff --git a/crates/gpui/src/platform/mac/metal_renderer.rs b/crates/gpui/src/platform/mac/metal_renderer.rs index f42a2e2df7b943..1186af00160d86 100644 --- a/crates/gpui/src/platform/mac/metal_renderer.rs +++ b/crates/gpui/src/platform/mac/metal_renderer.rs @@ -3,6 +3,7 @@ use crate::{ point, size, AtlasTextureId, AtlasTextureKind, AtlasTile, Bounds, ContentMask, DevicePixels, Hsla, MonochromeSprite, PaintSurface, Path, PathId, PathVertex, PolychromeSprite, PrimitiveBatch, Quad, ScaledPixels, Scene, Shadow, Size, Surface, Underline, + PATH_SUBPIXEL_VARIANTS, }; use anyhow::{anyhow, Result}; use block::ConcreteBlock; @@ -733,12 +734,18 @@ impl MetalRenderer { if let Some((path, tile)) = paths_and_tiles.peek() { if prev_texture_id.map_or(true, |texture_id| texture_id == tile.texture_id) { prev_texture_id = Some(tile.texture_id); - let origin = path.bounds.intersect(&path.content_mask.bounds).origin; + let origin = path + .bounds + .intersect(&path.content_mask.bounds) + .origin + .map(|p| (p / PATH_SUBPIXEL_VARIANTS).floor()); + let size = tile + .bounds + .size + .map(|s| s / PATH_SUBPIXEL_VARIANTS as i32) + .map(Into::into); sprites.push(PathSprite { - bounds: Bounds { - origin: origin.map(|p| p.floor()), - size: tile.bounds.size.map(Into::into), - }, + bounds: Bounds { origin, size }, color: path.color, tile: (*tile).clone(), }); diff --git a/crates/gpui/src/scene.rs b/crates/gpui/src/scene.rs index 9787ec5d87f137..826e7c5c83ca0b 100644 --- a/crates/gpui/src/scene.rs +++ b/crates/gpui/src/scene.rs @@ -7,6 +7,9 @@ use crate::{ }; use std::{fmt::Debug, iter::Peekable, ops::Range, slice}; +/// Subpixel variants for antialiasing for Path render. +pub(crate) const PATH_SUBPIXEL_VARIANTS: f32 = 2.0; + #[allow(non_camel_case_types, unused)] pub(crate) type PathVertex_ScaledPixels = PathVertex; @@ -785,13 +788,20 @@ impl Path { .iter() .map(|vertex| vertex.scale(factor)) .collect(), - start: self.start.map(|start| start.scale(factor)), + start: self.start.scale(factor), current: self.current.scale(factor), contour_count: self.contour_count, color: self.color, } } + /// Move the current point of the path to the given point. + pub fn move_to(&mut self, to: Point) { + self.start = to; + self.current = to; + self.contour_count = 0; + } + /// Draw a straight line from the current point to the given point. pub fn line_to(&mut self, to: Point) { self.contour_count += 1; @@ -821,6 +831,17 @@ impl Path { self.current = to; } + /// Translate path by the given offset. + pub fn translate(&mut self, offset: Point) { + self.start = self.start + offset; + self.current = self.current + offset; + self.bounds.origin = self.bounds.origin + offset; + self.content_mask.bounds.origin = self.content_mask.bounds.origin + offset; + for vertex in &mut self.vertices { + vertex.xy_position = vertex.xy_position + offset; + } + } + fn push_triangle( &mut self, xy: (Point, Point, Point), diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index e4fa74f981ed5f..a2866ed48157a9 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -14,7 +14,7 @@ use crate::{ TaffyLayoutEngine, Task, TextStyle, TextStyleRefinement, TransformationMatrix, Underline, UnderlineStyle, View, VisualContext, WeakView, WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowControls, WindowDecorations, WindowOptions, WindowParams, WindowTextSystem, - SUBPIXEL_VARIANTS, + PATH_SUBPIXEL_VARIANTS, SUBPIXEL_VARIANTS, }; use anyhow::{anyhow, Context as _, Result}; use collections::{FxHashMap, FxHashSet}; @@ -2330,7 +2330,7 @@ impl<'a> WindowContext<'a> { self.window .next_frame .scene - .insert_primitive(path.scale(scale_factor)); + .insert_primitive(path.scale(scale_factor * PATH_SUBPIXEL_VARIANTS)); } /// Paint an underline into the scene for the next frame at the current z-index. From 60ae55e1dc911d665fb4bcb90ae0e2627881f45a Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Wed, 27 Nov 2024 00:31:54 +0800 Subject: [PATCH 2/3] Add more example. --- crates/gpui/examples/painting.rs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/crates/gpui/examples/painting.rs b/crates/gpui/examples/painting.rs index dc16f3eec02cd1..4b8b19ebf598a2 100644 --- a/crates/gpui/examples/painting.rs +++ b/crates/gpui/examples/painting.rs @@ -16,8 +16,8 @@ impl PaintingViewer { // draw a line let mut path = Path::new(point(px(20.), px(100.))); path.line_to(point(px(50.), px(160.))); - path.line_to(point(px(60.), px(100.))); - path.line_to(point(px(60.5), px(101.))); + path.line_to(point(px(80.), px(100.))); + path.line_to(point(px(80.5), px(100.5))); path.line_to(point(px(51.), px(160.))); path.line_to(point(px(21.), px(100.))); lines.push((path, gpui::black())); @@ -54,6 +54,21 @@ impl PaintingViewer { path.translate(point(px(270.), px(80.))); lines.push((path, gpui::yellow())); + // draw double square + // https://yqnn.github.io/svg-path-editor/#P=M_2_1_L_2_3_L_4_3_L_4_4_L_6_4_L_6_2_L_4_2_L_4_1_L_2_1 + let mut path = Path::new(point(px(0.), px(50.))); + path.line_to(point(px(0.), px(150.))); + path.line_to(point(px(100.), px(150.))); + path.line_to(point(px(100.), px(200.))); + path.line_to(point(px(200.), px(200.))); + path.line_to(point(px(200.), px(100.))); + path.line_to(point(px(100.), px(100.))); + path.line_to(point(px(100.), px(50.))); + path.line_to(point(px(0.), px(50.))); + path.translate(point(px(20.), px(200.))); + lines.push((path, gpui::black())); + + // draw a square with rounded corners let square_bounds = Bounds { origin: point(px(450.), px(100.)), size: size(px(200.), px(80.)), From 787ef97b15157245de6c601a560590b5be2ce6d0 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Wed, 27 Nov 2024 19:09:21 +0800 Subject: [PATCH 3/3] add build_line_path method for draw line. --- crates/gpui/examples/painting.rs | 119 ++++++++++++++++++++++++++----- 1 file changed, 102 insertions(+), 17 deletions(-) diff --git a/crates/gpui/examples/painting.rs b/crates/gpui/examples/painting.rs index 4b8b19ebf598a2..def043fdf83175 100644 --- a/crates/gpui/examples/painting.rs +++ b/crates/gpui/examples/painting.rs @@ -101,6 +101,107 @@ impl PaintingViewer { cx.notify(); } } + +fn build_line_path(points: Vec>, width: f32) -> Path { + let mut path = Path::new(point(points[0].x, points[0].y)); + let half_width = width / 2.0; + let angle_threshold: f32 = 15.; + // 4~6 for performance, 8~12 for medium, 16~24 for high quality + const SEGMENT: usize = 0; + let angle_threshold_cos = angle_threshold.to_radians().cos(); + + for i in 0..points.len() - 1 { + let p0 = points[i]; + let p1 = points[i + 1]; + + // Calculate direction vector and normal + let dx = p1.x - p0.x; + let dy = p1.y - p0.y; + let length = (dx * dx + dy * dy).0.sqrt(); + let dir = [dx / length, dy / length]; + let normal = [-dir[1] * half_width, dir[0] * half_width]; + + // Current segment boundary vertices + let left0 = [p0.x - normal[0], p0.y - normal[1]]; + let right0 = [p0.x + normal[0], p0.y + normal[1]]; + let left1 = [p1.x - normal[0], p1.y - normal[1]]; + let right1 = [p1.x + normal[0], p1.y + normal[1]]; + + // Add main triangles of the current segment + path.move_to(point(left0[0], left0[1])); + path.line_to(point(right0[0], right0[1])); + path.line_to(point(left1[0], left1[1])); + + path.move_to(point(right0[0], right0[1])); + path.line_to(point(right1[0], right1[1])); + path.line_to(point(left1[0], left1[1])); + + // Corner handling + if i < points.len() - 2 { + let p2 = points[i + 2]; + + // Previous and next direction vectors + let next_length = ((p2.x - p1.x).0.powi(2) + (p2.y - p1.y).0.powi(2)).sqrt(); + let prev_dir = [dir[0], dir[1]]; + let next_dir = [(p2.x - p1.x) / next_length, (p2.y - p1.y) / next_length]; + + // Calculate angle + let cos_angle = prev_dir[0] * next_dir[0] + prev_dir[1] * next_dir[1]; + + if cos_angle.0 < -0.99 { + // 180 degree turn: fill intersection area + path.line_to(point(p1.x - normal[0], p1.y - normal[1])); + path.line_to(point(p1.x + normal[0], p1.y + normal[1])); + continue; + } else if cos_angle.0 > angle_threshold_cos { + // Sharp angle: fill intersection area, generate polygon cover + let mut intersection_points = vec![ + [p1.x + normal[0], p1.y + normal[1]], + [p1.x - normal[0], p1.y - normal[1]], + ]; + let step = (1.0 - cos_angle.0) * (std::f32::consts::PI / 2.0) / SEGMENT as f32; + for j in 0..=SEGMENT { + let theta = j as f32 * step; + let rotated = [ + prev_dir[0] * theta.cos() - prev_dir[1] * theta.sin(), + prev_dir[0] * theta.sin() + prev_dir[1] * theta.cos(), + ]; + let rounded_vertex = [ + p1.x + rotated[0] * half_width, + p1.y + rotated[1] * half_width, + ]; + intersection_points.push(rounded_vertex); + } + for k in 1..intersection_points.len() - 1 { + path.move_to(point(intersection_points[0][0], intersection_points[0][1])); + path.line_to(point(intersection_points[k][0], intersection_points[k][1])); + path.line_to(point( + intersection_points[k + 1][0], + intersection_points[k + 1][1], + )); + } + } else { + // Regular corner handling + let step = (std::f32::consts::PI - cos_angle.0.acos()) / SEGMENT as f32; + for j in 0..=SEGMENT { + let theta = j as f32 * step; + let rotated = [ + prev_dir[0] * theta.cos() - prev_dir[1] * theta.sin(), + prev_dir[0] * theta.sin() + prev_dir[1] * theta.cos(), + ]; + let rounded_vertex = [ + p1.x + rotated[0] * half_width, + p1.y + rotated[1] * half_width, + ]; + path.line_to(point(rounded_vertex[0], rounded_vertex[1])); + } + } + } + } + + path +} + impl Render for PaintingViewer { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { let default_lines = self.default_lines.clone(); @@ -141,27 +242,11 @@ impl Render for PaintingViewer { canvas( move |_, _| {}, move |_, _, cx| { - const STROKE_WIDTH: Pixels = px(1.0); for (path, color) in default_lines { cx.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); - } - - let mut last = points.last().unwrap(); - for p in points.iter().rev() { - let dx = p.x - last.x; - let dy = p.y - last.y; - let distance = (dx * dx + dy * dy).0.sqrt(); - let offset_x = (STROKE_WIDTH * dy / distance).clamp(px(0.0), STROKE_WIDTH); - let offset_y = (STROKE_WIDTH * dx / distance).clamp(px(0.0), STROKE_WIDTH); - path.line_to(point(p.x + offset_x, p.y - offset_y)); - last = p; - } - + let path = build_line_path(points, 1.5); cx.paint_path(path, gpui::black()); } },