Implement composable, type-safe renderer fallback

This commit is contained in:
Héctor Ramón Jiménez 2024-03-21 22:27:17 +01:00
parent 7e4ae8450e
commit 3645d34d6a
No known key found for this signature in database
GPG key ID: 7CC46565708259A7
35 changed files with 1474 additions and 1210 deletions

View file

@ -397,3 +397,12 @@ impl backend::Svg for Backend {
self.image_pipeline.viewport_dimensions(handle)
}
}
#[cfg(feature = "geometry")]
impl crate::graphics::geometry::Backend for Backend {
type Frame = crate::geometry::Frame;
fn new_frame(&self, size: Size) -> Self::Frame {
crate::geometry::Frame::new(size)
}
}

View file

@ -6,7 +6,7 @@ use crate::core::{
use crate::graphics::color;
use crate::graphics::geometry::fill::{self, Fill};
use crate::graphics::geometry::{
LineCap, LineDash, LineJoin, Path, Stroke, Style, Text,
self, LineCap, LineDash, LineJoin, Path, Stroke, Style, Text,
};
use crate::graphics::gradient::{self, Gradient};
use crate::graphics::mesh::{self, Mesh};
@ -14,6 +14,7 @@ use crate::primitive::{self, Primitive};
use lyon::geom::euclid;
use lyon::tessellation;
use std::borrow::Cow;
/// A frame for drawing some geometry.
@ -27,6 +28,326 @@ pub struct Frame {
stroke_tessellator: tessellation::StrokeTessellator,
}
impl Frame {
/// Creates a new [`Frame`] with the given [`Size`].
pub fn new(size: Size) -> Frame {
Frame {
size,
buffers: BufferStack::new(),
primitives: Vec::new(),
transforms: Transforms {
previous: Vec::new(),
current: Transform(lyon::math::Transform::identity()),
},
fill_tessellator: tessellation::FillTessellator::new(),
stroke_tessellator: tessellation::StrokeTessellator::new(),
}
}
fn into_primitives(mut self) -> Vec<Primitive> {
for buffer in self.buffers.stack {
match buffer {
Buffer::Solid(buffer) => {
if !buffer.indices.is_empty() {
self.primitives.push(Primitive::Custom(
primitive::Custom::Mesh(Mesh::Solid {
buffers: mesh::Indexed {
vertices: buffer.vertices,
indices: buffer.indices,
},
size: self.size,
}),
));
}
}
Buffer::Gradient(buffer) => {
if !buffer.indices.is_empty() {
self.primitives.push(Primitive::Custom(
primitive::Custom::Mesh(Mesh::Gradient {
buffers: mesh::Indexed {
vertices: buffer.vertices,
indices: buffer.indices,
},
size: self.size,
}),
));
}
}
}
}
self.primitives
}
}
impl geometry::Frame for Frame {
type Geometry = Primitive;
/// Creates a new empty [`Frame`] with the given dimensions.
///
/// The default coordinate system of a [`Frame`] has its origin at the
/// top-left corner of its bounds.
#[inline]
fn width(&self) -> f32 {
self.size.width
}
#[inline]
fn height(&self) -> f32 {
self.size.height
}
#[inline]
fn size(&self) -> Size {
self.size
}
#[inline]
fn center(&self) -> Point {
Point::new(self.size.width / 2.0, self.size.height / 2.0)
}
fn fill(&mut self, path: &Path, fill: impl Into<Fill>) {
let Fill { style, rule } = fill.into();
let mut buffer = self
.buffers
.get_fill(&self.transforms.current.transform_style(style));
let options = tessellation::FillOptions::default()
.with_fill_rule(into_fill_rule(rule));
if self.transforms.current.is_identity() {
self.fill_tessellator.tessellate_path(
path.raw(),
&options,
buffer.as_mut(),
)
} else {
let path = path.transform(&self.transforms.current.0);
self.fill_tessellator.tessellate_path(
path.raw(),
&options,
buffer.as_mut(),
)
}
.expect("Tessellate path.");
}
fn fill_rectangle(
&mut self,
top_left: Point,
size: Size,
fill: impl Into<Fill>,
) {
let Fill { style, rule } = fill.into();
let mut buffer = self
.buffers
.get_fill(&self.transforms.current.transform_style(style));
let top_left = self
.transforms
.current
.0
.transform_point(lyon::math::Point::new(top_left.x, top_left.y));
let size =
self.transforms.current.0.transform_vector(
lyon::math::Vector::new(size.width, size.height),
);
let options = tessellation::FillOptions::default()
.with_fill_rule(into_fill_rule(rule));
self.fill_tessellator
.tessellate_rectangle(
&lyon::math::Box2D::new(top_left, top_left + size),
&options,
buffer.as_mut(),
)
.expect("Fill rectangle");
}
fn stroke<'a>(&mut self, path: &Path, stroke: impl Into<Stroke<'a>>) {
let stroke = stroke.into();
let mut buffer = self
.buffers
.get_stroke(&self.transforms.current.transform_style(stroke.style));
let mut options = tessellation::StrokeOptions::default();
options.line_width = stroke.width;
options.start_cap = into_line_cap(stroke.line_cap);
options.end_cap = into_line_cap(stroke.line_cap);
options.line_join = into_line_join(stroke.line_join);
let path = if stroke.line_dash.segments.is_empty() {
Cow::Borrowed(path)
} else {
Cow::Owned(dashed(path, stroke.line_dash))
};
if self.transforms.current.is_identity() {
self.stroke_tessellator.tessellate_path(
path.raw(),
&options,
buffer.as_mut(),
)
} else {
let path = path.transform(&self.transforms.current.0);
self.stroke_tessellator.tessellate_path(
path.raw(),
&options,
buffer.as_mut(),
)
}
.expect("Stroke path");
}
fn fill_text(&mut self, text: impl Into<Text>) {
let text = text.into();
let (scale_x, scale_y) = self.transforms.current.scale();
if self.transforms.current.is_scale_translation()
&& scale_x == scale_y
&& scale_x > 0.0
&& scale_y > 0.0
{
let (position, size, line_height) =
if self.transforms.current.is_identity() {
(text.position, text.size, text.line_height)
} else {
let position =
self.transforms.current.transform_point(text.position);
let size = Pixels(text.size.0 * scale_y);
let line_height = match text.line_height {
LineHeight::Absolute(size) => {
LineHeight::Absolute(Pixels(size.0 * scale_y))
}
LineHeight::Relative(factor) => {
LineHeight::Relative(factor)
}
};
(position, size, line_height)
};
let bounds = Rectangle {
x: position.x,
y: position.y,
width: f32::INFINITY,
height: f32::INFINITY,
};
// TODO: Honor layering!
self.primitives.push(Primitive::Text {
content: text.content,
bounds,
color: text.color,
size,
line_height,
font: text.font,
horizontal_alignment: text.horizontal_alignment,
vertical_alignment: text.vertical_alignment,
shaping: text.shaping,
clip_bounds: Rectangle::with_size(Size::INFINITY),
});
} else {
text.draw_with(|path, color| self.fill(&path, color));
}
}
#[inline]
fn translate(&mut self, translation: Vector) {
self.transforms.current.0 =
self.transforms
.current
.0
.pre_translate(lyon::math::Vector::new(
translation.x,
translation.y,
));
}
#[inline]
fn rotate(&mut self, angle: impl Into<Radians>) {
self.transforms.current.0 = self
.transforms
.current
.0
.pre_rotate(lyon::math::Angle::radians(angle.into().0));
}
#[inline]
fn scale(&mut self, scale: impl Into<f32>) {
let scale = scale.into();
self.scale_nonuniform(Vector { x: scale, y: scale });
}
#[inline]
fn scale_nonuniform(&mut self, scale: impl Into<Vector>) {
let scale = scale.into();
self.transforms.current.0 =
self.transforms.current.0.pre_scale(scale.x, scale.y);
}
fn push_transform(&mut self) {
self.transforms.previous.push(self.transforms.current);
}
fn pop_transform(&mut self) {
self.transforms.current = self.transforms.previous.pop().unwrap();
}
fn draft(&mut self, size: Size) -> Frame {
Frame::new(size)
}
fn paste(&mut self, frame: Frame, at: Point) {
let size = frame.size();
let primitives = frame.into_primitives();
let transformation = Transformation::translate(at.x, at.y);
let (text, meshes) = primitives
.into_iter()
.partition(|primitive| matches!(primitive, Primitive::Text { .. }));
self.primitives.push(Primitive::Group {
primitives: vec![
Primitive::Transform {
transformation,
content: Box::new(Primitive::Group { primitives: meshes }),
},
Primitive::Transform {
transformation,
content: Box::new(Primitive::Clip {
bounds: Rectangle::with_size(size),
content: Box::new(Primitive::Group {
primitives: text,
}),
}),
},
],
});
}
}
impl From<Frame> for Primitive {
fn from(frame: Frame) -> Self {
Self::Group {
primitives: frame.into_primitives(),
}
}
}
enum Buffer {
Solid(tessellation::VertexBuffers<mesh::SolidVertex2D, u32>),
Gradient(tessellation::VertexBuffers<mesh::GradientVertex2D, u32>),
@ -165,386 +486,6 @@ impl Transform {
gradient
}
}
impl Frame {
/// Creates a new empty [`Frame`] with the given dimensions.
///
/// The default coordinate system of a [`Frame`] has its origin at the
/// top-left corner of its bounds.
pub fn new(size: Size) -> Frame {
Frame {
size,
buffers: BufferStack::new(),
primitives: Vec::new(),
transforms: Transforms {
previous: Vec::new(),
current: Transform(lyon::math::Transform::identity()),
},
fill_tessellator: tessellation::FillTessellator::new(),
stroke_tessellator: tessellation::StrokeTessellator::new(),
}
}
/// Returns the width of the [`Frame`].
#[inline]
pub fn width(&self) -> f32 {
self.size.width
}
/// Returns the height of the [`Frame`].
#[inline]
pub fn height(&self) -> f32 {
self.size.height
}
/// Returns the dimensions of the [`Frame`].
#[inline]
pub fn size(&self) -> Size {
self.size
}
/// Returns the coordinate of the center of the [`Frame`].
#[inline]
pub fn center(&self) -> Point {
Point::new(self.size.width / 2.0, self.size.height / 2.0)
}
/// Draws the given [`Path`] on the [`Frame`] by filling it with the
/// provided style.
pub fn fill(&mut self, path: &Path, fill: impl Into<Fill>) {
let Fill { style, rule } = fill.into();
let mut buffer = self
.buffers
.get_fill(&self.transforms.current.transform_style(style));
let options = tessellation::FillOptions::default()
.with_fill_rule(into_fill_rule(rule));
if self.transforms.current.is_identity() {
self.fill_tessellator.tessellate_path(
path.raw(),
&options,
buffer.as_mut(),
)
} else {
let path = path.transform(&self.transforms.current.0);
self.fill_tessellator.tessellate_path(
path.raw(),
&options,
buffer.as_mut(),
)
}
.expect("Tessellate path.");
}
/// Draws an axis-aligned rectangle given its top-left corner coordinate and
/// its `Size` on the [`Frame`] by filling it with the provided style.
pub fn fill_rectangle(
&mut self,
top_left: Point,
size: Size,
fill: impl Into<Fill>,
) {
let Fill { style, rule } = fill.into();
let mut buffer = self
.buffers
.get_fill(&self.transforms.current.transform_style(style));
let top_left = self
.transforms
.current
.0
.transform_point(lyon::math::Point::new(top_left.x, top_left.y));
let size =
self.transforms.current.0.transform_vector(
lyon::math::Vector::new(size.width, size.height),
);
let options = tessellation::FillOptions::default()
.with_fill_rule(into_fill_rule(rule));
self.fill_tessellator
.tessellate_rectangle(
&lyon::math::Box2D::new(top_left, top_left + size),
&options,
buffer.as_mut(),
)
.expect("Fill rectangle");
}
/// Draws the stroke of the given [`Path`] on the [`Frame`] with the
/// provided style.
pub fn stroke<'a>(&mut self, path: &Path, stroke: impl Into<Stroke<'a>>) {
let stroke = stroke.into();
let mut buffer = self
.buffers
.get_stroke(&self.transforms.current.transform_style(stroke.style));
let mut options = tessellation::StrokeOptions::default();
options.line_width = stroke.width;
options.start_cap = into_line_cap(stroke.line_cap);
options.end_cap = into_line_cap(stroke.line_cap);
options.line_join = into_line_join(stroke.line_join);
let path = if stroke.line_dash.segments.is_empty() {
Cow::Borrowed(path)
} else {
Cow::Owned(dashed(path, stroke.line_dash))
};
if self.transforms.current.is_identity() {
self.stroke_tessellator.tessellate_path(
path.raw(),
&options,
buffer.as_mut(),
)
} else {
let path = path.transform(&self.transforms.current.0);
self.stroke_tessellator.tessellate_path(
path.raw(),
&options,
buffer.as_mut(),
)
}
.expect("Stroke path");
}
/// Draws the characters of the given [`Text`] on the [`Frame`], filling
/// them with the given color.
///
/// __Warning:__ Text currently does not work well with rotations and scale
/// transforms! The position will be correctly transformed, but the
/// resulting glyphs will not be rotated or scaled properly.
///
/// Additionally, all text will be rendered on top of all the layers of
/// a `Canvas`. Therefore, it is currently only meant to be used for
/// overlays, which is the most common use case.
///
/// Support for vectorial text is planned, and should address all these
/// limitations.
pub fn fill_text(&mut self, text: impl Into<Text>) {
let text = text.into();
let (scale_x, scale_y) = self.transforms.current.scale();
if self.transforms.current.is_scale_translation()
&& scale_x == scale_y
&& scale_x > 0.0
&& scale_y > 0.0
{
let (position, size, line_height) =
if self.transforms.current.is_identity() {
(text.position, text.size, text.line_height)
} else {
let position =
self.transforms.current.transform_point(text.position);
let size = Pixels(text.size.0 * scale_y);
let line_height = match text.line_height {
LineHeight::Absolute(size) => {
LineHeight::Absolute(Pixels(size.0 * scale_y))
}
LineHeight::Relative(factor) => {
LineHeight::Relative(factor)
}
};
(position, size, line_height)
};
let bounds = Rectangle {
x: position.x,
y: position.y,
width: f32::INFINITY,
height: f32::INFINITY,
};
// TODO: Honor layering!
self.primitives.push(Primitive::Text {
content: text.content,
bounds,
color: text.color,
size,
line_height,
font: text.font,
horizontal_alignment: text.horizontal_alignment,
vertical_alignment: text.vertical_alignment,
shaping: text.shaping,
clip_bounds: Rectangle::with_size(Size::INFINITY),
});
} else {
text.draw_with(|path, color| self.fill(&path, color));
}
}
/// Stores the current transform of the [`Frame`] and executes the given
/// drawing operations, restoring the transform afterwards.
///
/// This method is useful to compose transforms and perform drawing
/// operations in different coordinate systems.
#[inline]
pub fn with_save<R>(&mut self, f: impl FnOnce(&mut Frame) -> R) -> R {
self.push_transform();
let result = f(self);
self.pop_transform();
result
}
/// Pushes the current transform in the transform stack.
pub fn push_transform(&mut self) {
self.transforms.previous.push(self.transforms.current);
}
/// Pops a transform from the transform stack and sets it as the current transform.
pub fn pop_transform(&mut self) {
self.transforms.current = self.transforms.previous.pop().unwrap();
}
/// Executes the given drawing operations within a [`Rectangle`] region,
/// clipping any geometry that overflows its bounds. Any transformations
/// performed are local to the provided closure.
///
/// This method is useful to perform drawing operations that need to be
/// clipped.
#[inline]
pub fn with_clip<R>(
&mut self,
region: Rectangle,
f: impl FnOnce(&mut Frame) -> R,
) -> R {
let mut frame = Frame::new(region.size());
let result = f(&mut frame);
let origin = Point::new(region.x, region.y);
self.clip(frame, origin);
result
}
/// Draws the clipped contents of the given [`Frame`] with origin at the given [`Point`].
pub fn clip(&mut self, frame: Frame, at: Point) {
let size = frame.size();
let primitives = frame.into_primitives();
let transformation = Transformation::translate(at.x, at.y);
let (text, meshes) = primitives
.into_iter()
.partition(|primitive| matches!(primitive, Primitive::Text { .. }));
self.primitives.push(Primitive::Group {
primitives: vec![
Primitive::Transform {
transformation,
content: Box::new(Primitive::Group { primitives: meshes }),
},
Primitive::Transform {
transformation,
content: Box::new(Primitive::Clip {
bounds: Rectangle::with_size(size),
content: Box::new(Primitive::Group {
primitives: text,
}),
}),
},
],
});
}
/// Applies a translation to the current transform of the [`Frame`].
#[inline]
pub fn translate(&mut self, translation: Vector) {
self.transforms.current.0 =
self.transforms
.current
.0
.pre_translate(lyon::math::Vector::new(
translation.x,
translation.y,
));
}
/// Applies a rotation in radians to the current transform of the [`Frame`].
#[inline]
pub fn rotate(&mut self, angle: impl Into<Radians>) {
self.transforms.current.0 = self
.transforms
.current
.0
.pre_rotate(lyon::math::Angle::radians(angle.into().0));
}
/// Applies a uniform scaling to the current transform of the [`Frame`].
#[inline]
pub fn scale(&mut self, scale: impl Into<f32>) {
let scale = scale.into();
self.scale_nonuniform(Vector { x: scale, y: scale });
}
/// Applies a non-uniform scaling to the current transform of the [`Frame`].
#[inline]
pub fn scale_nonuniform(&mut self, scale: impl Into<Vector>) {
let scale = scale.into();
self.transforms.current.0 =
self.transforms.current.0.pre_scale(scale.x, scale.y);
}
/// Produces the [`Primitive`] representing everything drawn on the [`Frame`].
pub fn into_primitive(self) -> Primitive {
Primitive::Group {
primitives: self.into_primitives(),
}
}
fn into_primitives(mut self) -> Vec<Primitive> {
for buffer in self.buffers.stack {
match buffer {
Buffer::Solid(buffer) => {
if !buffer.indices.is_empty() {
self.primitives.push(Primitive::Custom(
primitive::Custom::Mesh(Mesh::Solid {
buffers: mesh::Indexed {
vertices: buffer.vertices,
indices: buffer.indices,
},
size: self.size,
}),
));
}
}
Buffer::Gradient(buffer) => {
if !buffer.indices.is_empty() {
self.primitives.push(Primitive::Custom(
primitive::Custom::Mesh(Mesh::Gradient {
buffers: mesh::Indexed {
vertices: buffer.vertices,
indices: buffer.indices,
},
size: self.size,
}),
));
}
}
}
}
self.primitives
}
}
struct GradientVertex2DBuilder {
gradient: gradient::Packed,
}

View file

@ -28,3 +28,11 @@ impl Damage for Custom {
}
}
}
impl TryFrom<Mesh> for Custom {
type Error = &'static str;
fn try_from(mesh: Mesh) -> Result<Self, Self::Error> {
Ok(Custom::Mesh(mesh))
}
}