use crate::geometry::{Point, Rect, Size};
use crate::UiError;
use crate::{Color, FontSpec};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
pub enum FillRule {
EvenOdd,
#[default]
NonZero,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
pub enum LineJoin {
#[default]
Miter,
Bevel,
Round,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
pub enum LineCap {
#[default]
Butt,
Round,
Square,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct StrokeStyle {
pub width: f32,
pub join: LineJoin,
pub cap: LineCap,
pub miter_limit: f32,
}
impl Default for StrokeStyle {
fn default() -> Self {
Self {
width: 1.0,
join: LineJoin::Miter,
cap: LineCap::Butt,
miter_limit: 4.0,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct GradientStop {
pub offset: f32,
pub color: Color,
}
impl GradientStop {
pub fn new(offset: f32, color: Color) -> Self {
Self {
offset: offset.clamp(0.0, 1.0),
color,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
pub enum ImageFilter {
#[default]
Nearest,
Bilinear,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ImageData {
pub rgba: Vec<u8>,
pub width: u32,
pub height: u32,
}
impl ImageData {
pub fn new(rgba: Vec<u8>, width: u32, height: u32) -> Self {
Self {
rgba,
width,
height,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum PathVerb {
MoveTo(Point),
LineTo(Point),
QuadTo {
ctrl: Point,
end: Point,
},
CubicTo {
c1: Point,
c2: Point,
end: Point,
},
Close,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct PathData {
pub verbs: Vec<PathVerb>,
pub fill_rule: FillRule,
}
impl PathData {
pub fn new() -> Self {
Self::default()
}
pub fn with_fill_rule(mut self, rule: FillRule) -> Self {
self.fill_rule = rule;
self
}
pub fn move_to(&mut self, p: Point) -> &mut Self {
self.verbs.push(PathVerb::MoveTo(p));
self
}
pub fn line_to(&mut self, p: Point) -> &mut Self {
self.verbs.push(PathVerb::LineTo(p));
self
}
pub fn quad_to(&mut self, ctrl: Point, end: Point) -> &mut Self {
self.verbs.push(PathVerb::QuadTo { ctrl, end });
self
}
pub fn cubic_to(&mut self, c1: Point, c2: Point, end: Point) -> &mut Self {
self.verbs.push(PathVerb::CubicTo { c1, c2, end });
self
}
pub fn close(&mut self) -> &mut Self {
self.verbs.push(PathVerb::Close);
self
}
pub fn is_empty(&self) -> bool {
self.verbs.is_empty()
}
pub fn bounds(&self) -> Option<Rect> {
let mut min_x = f32::MAX;
let mut min_y = f32::MAX;
let mut max_x = f32::MIN;
let mut max_y = f32::MIN;
let mut found = false;
let mut update = |p: Point| {
found = true;
if p.x < min_x {
min_x = p.x;
}
if p.y < min_y {
min_y = p.y;
}
if p.x > max_x {
max_x = p.x;
}
if p.y > max_y {
max_y = p.y;
}
};
for verb in &self.verbs {
match verb {
PathVerb::MoveTo(p) | PathVerb::LineTo(p) => update(*p),
PathVerb::QuadTo { ctrl, end } => {
update(*ctrl);
update(*end);
}
PathVerb::CubicTo { c1, c2, end } => {
update(*c1);
update(*c2);
update(*end);
}
PathVerb::Close => {}
}
}
if found {
Some(Rect::new(min_x, min_y, max_x - min_x, max_y - min_y))
} else {
None
}
}
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub enum DrawCommand {
PushClip {
rect: Rect,
},
PopClip,
FillRect {
rect: Rect,
color: Color,
},
StrokeRect {
rect: Rect,
thickness: f32,
color: Color,
},
FillRoundedRect {
rect: Rect,
radius: f32,
color: Color,
},
FillRoundedRectPerCorner {
rect: Rect,
radii: [f32; 4],
color: Color,
},
FillCircle {
center: Point,
radius: f32,
color: Color,
},
FillEllipse {
center: Point,
rx: f32,
ry: f32,
color: Color,
},
Line {
from: Point,
to: Point,
color: Color,
},
LineAa {
from: Point,
to: Point,
color: Color,
},
LineThick {
from: Point,
to: Point,
width: f32,
color: Color,
},
LineDashed {
from: Point,
to: Point,
dash_len: f32,
gap_len: f32,
color: Color,
},
FillPath {
path: PathData,
color: Color,
},
StrokePath {
path: PathData,
style: StrokeStyle,
color: Color,
},
LinearGradient {
rect: Rect,
start: Point,
end: Point,
stops: Vec<GradientStop>,
},
RadialGradient {
rect: Rect,
center: Point,
radius: f32,
stops: Vec<GradientStop>,
},
Image {
image: ImageData,
dest: Rect,
filter: ImageFilter,
},
NineSlice {
image: ImageData,
dest: Rect,
insets: [u32; 4],
},
BoxShadow {
rect: Rect,
offset: Point,
blur_radius: f32,
color: Color,
},
DrawText {
rect: Rect,
text: String,
font: FontSpec,
color: Color,
},
}
#[derive(Clone, Debug, Default)]
pub struct DrawList {
cmds: Vec<DrawCommand>,
clip_depth: usize,
bounds: Option<Rect>,
}
impl DrawList {
pub fn new() -> Self {
Self::default()
}
pub fn push(&mut self, cmd: DrawCommand) {
match &cmd {
DrawCommand::PushClip { .. } => {
self.clip_depth = self.clip_depth.saturating_add(1);
}
DrawCommand::PopClip => {
self.clip_depth = self.clip_depth.saturating_sub(1);
}
_ => {
if let Some(b) = Self::cmd_bounds(&cmd) {
self.bounds = Some(match self.bounds {
None => b,
Some(existing) => existing.union(&b),
});
}
}
}
self.cmds.push(cmd);
}
pub fn len(&self) -> usize {
self.cmds.len()
}
pub fn is_empty(&self) -> bool {
self.cmds.is_empty()
}
pub fn iter(&self) -> std::slice::Iter<'_, DrawCommand> {
self.cmds.iter()
}
pub fn clear(&mut self) {
self.cmds.clear();
self.clip_depth = 0;
self.bounds = None;
}
pub fn bounds(&self) -> Option<Rect> {
self.bounds
}
pub fn clip_depth(&self) -> usize {
self.clip_depth
}
pub fn is_clip_balanced(&self) -> bool {
self.clip_depth == 0
}
pub fn push_rect(&mut self, rect: Rect, color: Color) {
self.push(DrawCommand::FillRect { rect, color });
}
pub fn push_stroke_rect(&mut self, rect: Rect, thickness: f32, color: Color) {
self.push(DrawCommand::StrokeRect {
rect,
thickness,
color,
});
}
pub fn push_rounded_rect(&mut self, rect: Rect, radius: f32, color: Color) {
self.push(DrawCommand::FillRoundedRect {
rect,
radius,
color,
});
}
pub fn push_rounded_rect_per_corner(&mut self, rect: Rect, radii: [f32; 4], color: Color) {
self.push(DrawCommand::FillRoundedRectPerCorner { rect, radii, color });
}
pub fn push_circle(&mut self, center: Point, radius: f32, color: Color) {
self.push(DrawCommand::FillCircle {
center,
radius,
color,
});
}
pub fn push_ellipse(&mut self, center: Point, rx: f32, ry: f32, color: Color) {
self.push(DrawCommand::FillEllipse {
center,
rx,
ry,
color,
});
}
pub fn push_line(&mut self, from: Point, to: Point, color: Color) {
self.push(DrawCommand::Line { from, to, color });
}
pub fn push_line_aa(&mut self, from: Point, to: Point, color: Color) {
self.push(DrawCommand::LineAa { from, to, color });
}
pub fn push_line_thick(&mut self, from: Point, to: Point, width: f32, color: Color) {
self.push(DrawCommand::LineThick {
from,
to,
width,
color,
});
}
pub fn push_line_dashed(
&mut self,
from: Point,
to: Point,
dash_len: f32,
gap_len: f32,
color: Color,
) {
self.push(DrawCommand::LineDashed {
from,
to,
dash_len,
gap_len,
color,
});
}
pub fn push_clip(&mut self, rect: Rect) {
self.push(DrawCommand::PushClip { rect });
}
pub fn pop_clip(&mut self) {
self.push(DrawCommand::PopClip);
}
pub fn push_path(&mut self, path: PathData, color: Color) {
self.push(DrawCommand::FillPath { path, color });
}
pub fn push_stroke_path(&mut self, path: PathData, style: StrokeStyle, color: Color) {
self.push(DrawCommand::StrokePath { path, style, color });
}
pub fn push_gradient_linear(
&mut self,
rect: Rect,
start: Point,
end: Point,
stops: Vec<GradientStop>,
) {
self.push(DrawCommand::LinearGradient {
rect,
start,
end,
stops,
});
}
pub fn push_gradient_radial(
&mut self,
rect: Rect,
center: Point,
radius: f32,
stops: Vec<GradientStop>,
) {
self.push(DrawCommand::RadialGradient {
rect,
center,
radius,
stops,
});
}
pub fn push_image(&mut self, image: ImageData, dest: Rect, filter: ImageFilter) {
self.push(DrawCommand::Image {
image,
dest,
filter,
});
}
pub fn push_nine_slice(&mut self, image: ImageData, dest: Rect, insets: [u32; 4]) {
self.push(DrawCommand::NineSlice {
image,
dest,
insets,
});
}
pub fn push_shadow(&mut self, rect: Rect, offset: Point, blur_radius: f32, color: Color) {
self.push(DrawCommand::BoxShadow {
rect,
offset,
blur_radius,
color,
});
}
pub fn push_text(&mut self, rect: Rect, text: impl Into<String>, font: FontSpec, color: Color) {
self.push(DrawCommand::DrawText {
rect,
text: text.into(),
font,
color,
});
}
fn cmd_bounds(cmd: &DrawCommand) -> Option<Rect> {
match cmd {
DrawCommand::FillRect { rect, .. }
| DrawCommand::StrokeRect { rect, .. }
| DrawCommand::FillRoundedRect { rect, .. }
| DrawCommand::FillRoundedRectPerCorner { rect, .. }
| DrawCommand::LinearGradient { rect, .. }
| DrawCommand::RadialGradient { rect, .. }
| DrawCommand::Image { dest: rect, .. }
| DrawCommand::NineSlice { dest: rect, .. }
| DrawCommand::DrawText { rect, .. } => Some(*rect),
DrawCommand::BoxShadow {
rect,
offset,
blur_radius,
..
} => {
let pad = *blur_radius;
Some(Rect::new(
rect.left() + offset.x - pad,
rect.top() + offset.y - pad,
rect.width() + 2.0 * pad,
rect.height() + 2.0 * pad,
))
}
DrawCommand::FillCircle { center, radius, .. } => Some(Rect::new(
center.x - radius,
center.y - radius,
radius * 2.0,
radius * 2.0,
)),
DrawCommand::FillEllipse { center, rx, ry, .. } => {
Some(Rect::new(center.x - rx, center.y - ry, rx * 2.0, ry * 2.0))
}
DrawCommand::Line { from, to, .. } | DrawCommand::LineAa { from, to, .. } => {
let x = from.x.min(to.x);
let y = from.y.min(to.y);
Some(Rect::new(
x,
y,
(from.x - to.x).abs(),
(from.y - to.y).abs(),
))
}
DrawCommand::LineThick {
from, to, width, ..
} => {
let pad = width / 2.0;
let x = from.x.min(to.x) - pad;
let y = from.y.min(to.y) - pad;
let w = (from.x - to.x).abs() + *width;
let h = (from.y - to.y).abs() + *width;
Some(Rect::new(x, y, w, h))
}
DrawCommand::LineDashed { from, to, .. } => {
let x = from.x.min(to.x);
let y = from.y.min(to.y);
Some(Rect::new(
x,
y,
(from.x - to.x).abs(),
(from.y - to.y).abs(),
))
}
DrawCommand::FillPath { path, .. } => path.bounds(),
DrawCommand::StrokePath { path, style, .. } => path.bounds().map(|b| {
let pad = style.width / 2.0;
Rect::new(
b.left() - pad,
b.top() - pad,
b.width() + style.width,
b.height() + style.width,
)
}),
DrawCommand::PushClip { .. } | DrawCommand::PopClip => None,
}
}
}
pub trait RenderBackend {
fn execute(&mut self, list: &DrawList) -> Result<(), UiError>;
fn surface_size(&self) -> Size;
fn supports_blur(&self) -> bool {
false
}
fn supports_gradients(&self) -> bool {
false
}
fn supports_paths(&self) -> bool {
false
}
fn supports_images(&self) -> bool {
false
}
fn supports_text(&self) -> bool {
false
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::geometry::{Point, Rect};
use crate::Color;
fn red() -> Color {
Color(255, 0, 0, 255)
}
fn blue() -> Color {
Color(0, 0, 255, 255)
}
#[test]
fn draw_list_builder_records_command_sequence() {
let mut dl = DrawList::new();
dl.push_rect(Rect::new(0.0, 0.0, 10.0, 10.0), red());
dl.push_clip(Rect::new(0.0, 0.0, 5.0, 5.0));
dl.push_rect(Rect::new(1.0, 1.0, 3.0, 3.0), blue());
dl.pop_clip();
assert_eq!(dl.len(), 4);
let cmds: Vec<_> = dl.iter().collect();
assert!(matches!(cmds[0], DrawCommand::FillRect { .. }));
assert!(matches!(cmds[1], DrawCommand::PushClip { .. }));
assert!(matches!(cmds[2], DrawCommand::FillRect { .. }));
assert!(matches!(cmds[3], DrawCommand::PopClip));
}
#[test]
fn draw_list_len_and_is_empty() {
let mut dl = DrawList::new();
assert!(dl.is_empty());
assert_eq!(dl.len(), 0);
dl.push_rect(Rect::new(0.0, 0.0, 1.0, 1.0), red());
assert!(!dl.is_empty());
assert_eq!(dl.len(), 1);
}
#[test]
fn clip_push_pop_balance() {
let mut dl = DrawList::new();
assert!(dl.is_clip_balanced());
dl.push_clip(Rect::new(0.0, 0.0, 10.0, 10.0));
assert_eq!(dl.clip_depth(), 1);
assert!(!dl.is_clip_balanced());
dl.pop_clip();
assert_eq!(dl.clip_depth(), 0);
assert!(dl.is_clip_balanced());
dl.pop_clip();
assert_eq!(dl.clip_depth(), 0);
}
#[test]
fn bounds_union_of_draw_commands() {
let mut dl = DrawList::new();
dl.push_rect(Rect::new(0.0, 0.0, 10.0, 10.0), red());
dl.push_rect(Rect::new(20.0, 20.0, 5.0, 5.0), blue());
let b = dl.bounds().expect("bounds should be Some");
assert!((b.left() - 0.0).abs() < 0.001);
assert!((b.top() - 0.0).abs() < 0.001);
assert!((b.width() - 25.0).abs() < 0.001);
assert!((b.height() - 25.0).abs() < 0.001);
}
#[test]
fn bounds_excludes_clip_commands() {
let mut dl = DrawList::new();
dl.push_clip(Rect::new(0.0, 0.0, 100.0, 100.0));
dl.pop_clip();
assert!(
dl.bounds().is_none(),
"clip commands must not contribute to bounds"
);
}
#[test]
fn clear_resets_bounds_and_depth() {
let mut dl = DrawList::new();
dl.push_clip(Rect::new(0.0, 0.0, 10.0, 10.0));
dl.push_rect(Rect::new(0.0, 0.0, 10.0, 10.0), red());
dl.clear();
assert!(dl.is_empty());
assert!(dl.bounds().is_none());
assert_eq!(dl.clip_depth(), 0);
}
#[test]
fn path_data_builder_and_bounds() {
let mut p = PathData::new();
p.move_to(Point::new(0.0, 0.0));
p.line_to(Point::new(10.0, 0.0));
p.line_to(Point::new(5.0, 8.0));
p.close();
let b = p.bounds().expect("triangle has bounds");
assert!((b.left() - 0.0).abs() < 0.001);
assert!((b.top() - 0.0).abs() < 0.001);
assert!((b.width() - 10.0).abs() < 0.001);
assert!((b.height() - 8.0).abs() < 0.001);
assert_eq!(p.fill_rule, FillRule::NonZero);
let p2 = PathData::new().with_fill_rule(FillRule::EvenOdd);
assert_eq!(p2.fill_rule, FillRule::EvenOdd);
}
#[test]
fn empty_list_iter_is_empty() {
let dl = DrawList::new();
assert!(dl.iter().next().is_none());
}
#[test]
fn gradient_stop_clamps_offset() {
let s = GradientStop::new(-0.5, red());
assert!((s.offset - 0.0).abs() < 0.001);
let s2 = GradientStop::new(1.5, blue());
assert!((s2.offset - 1.0).abs() < 0.001);
}
#[test]
fn stroke_style_defaults() {
let s = StrokeStyle::default();
assert!((s.width - 1.0).abs() < 0.001);
assert!(matches!(s.join, LineJoin::Miter));
assert!(matches!(s.cap, LineCap::Butt));
}
}