gpui: Add PathBuilder
based on lyon to build Path
(#22808)
Release Notes: - N/A --- Continue https://github.com/zed-industries/zed/pull/20499 We to draw more complex Path. Before this change, we only have `line_to`, but it is not enough. Add a new `PathBuilder` to use [lyon](https://github.com/nical/lyon) to build more complex path. And then with PR #22812 to enable anti-aliasing, all thing will be perfect. ## Show case ```bash cargo run -p gpui --example painting ``` Before: <img width="1136" alt="image" src="https://github.com/user-attachments/assets/0c15833a-ec95-404c-a469-24cf172cfd86" /> After: <img width="1136" alt="image" src="https://github.com/user-attachments/assets/42cfa35e-7e8f-4ef3-bb2d-b98defc62ad6" />
This commit is contained in:
parent
706f7be5e7
commit
31fa414422
8 changed files with 449 additions and 79 deletions
241
crates/gpui/src/path_builder.rs
Normal file
241
crates/gpui/src/path_builder.rs
Normal file
|
@ -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
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue