Skip to main content

jag_surface/
shapes.rs

1use jag_draw::{Brush, FillRule, Path, PathCmd, Rect, RoundedRect};
2
3use crate::canvas::Canvas;
4
5#[derive(Clone, Copy, Debug, Default)]
6pub struct BorderWidths {
7    pub top: f32,
8    pub right: f32,
9    pub bottom: f32,
10    pub left: f32,
11}
12
13#[derive(Clone, Debug)]
14pub struct BorderStyle {
15    pub widths: BorderWidths,
16    pub brush: Brush,
17}
18
19#[derive(Clone, Debug, Default)]
20pub struct RectStyle {
21    pub fill: Option<Brush>,
22    pub border: Option<BorderStyle>,
23}
24
25fn snapped_rect(canvas: &Canvas, x: f32, y: f32, w: f32, h: f32) -> (f32, f32, f32, f32) {
26    let snapped = canvas.snap_rect_logical_to_device(Rect { x, y, w, h });
27    (snapped.x, snapped.y, snapped.w, snapped.h)
28}
29
30/// Public helper for snapping a rect to device pixels using the canvas DPI.
31pub fn snap_rect_to_device(canvas: &Canvas, rect: Rect) -> Rect {
32    canvas.snap_rect_logical_to_device(rect)
33}
34
35/// Draw a rectangle with optional fill and per-side border widths,
36/// snapping edges to device pixels for crisp 1px borders.
37pub fn draw_snapped_rectangle(
38    canvas: &mut Canvas,
39    x: f32,
40    y: f32,
41    w: f32,
42    h: f32,
43    style: &RectStyle,
44    z: i32,
45) {
46    let (x, y, w, h) = snapped_rect(canvas, x, y, w, h);
47    draw_rectangle(canvas, x, y, w, h, style, z);
48}
49
50/// Draw a rectangle with optional fill and per-side border widths.
51pub fn draw_rectangle(
52    canvas: &mut Canvas,
53    x: f32,
54    y: f32,
55    w: f32,
56    h: f32,
57    style: &RectStyle,
58    z: i32,
59) {
60    if let Some(fill) = &style.fill {
61        canvas.fill_rect(x, y, w, h, fill.clone(), z);
62    }
63    if let Some(border) = &style.border {
64        let b = &border.widths;
65        let brush = border.brush.clone();
66        if b.top > 0.0 {
67            canvas.fill_rect(x, y, w, b.top, brush.clone(), z + 1);
68        }
69        if b.right > 0.0 {
70            canvas.fill_rect(x + w - b.right, y, b.right, h, brush.clone(), z + 1);
71        }
72        if b.bottom > 0.0 {
73            canvas.fill_rect(x, y + h - b.bottom, w, b.bottom, brush.clone(), z + 1);
74        }
75        if b.left > 0.0 {
76            canvas.fill_rect(x, y, b.left, h, brush, z + 1);
77        }
78    }
79}
80
81/// Draw a rounded rectangle with optional fill and uniform stroke/border.
82pub fn draw_rounded_rectangle(
83    canvas: &mut Canvas,
84    rrect: RoundedRect,
85    fill: Option<Brush>,
86    stroke_width: Option<f32>,
87    stroke_brush: Option<Brush>,
88    z: i32,
89) {
90    if let Some(f) = fill {
91        canvas.rounded_rect(rrect, f, z);
92    }
93    if let (Some(w), Some(b)) = (stroke_width, stroke_brush) {
94        canvas.stroke_rounded_rect(rrect, w, b, z + 1);
95    }
96}
97
98/// Draw a rounded rectangle with edges snapped to device pixels.
99///
100/// The rounded-rect radii are preserved; only the outer rect is snapped so
101/// borders land cleanly on integral physical pixels (especially important
102/// on Windows with fractional DPI scaling).
103pub fn draw_snapped_rounded_rectangle(
104    canvas: &mut Canvas,
105    mut rrect: RoundedRect,
106    fill: Option<Brush>,
107    stroke_width: Option<f32>,
108    stroke_brush: Option<Brush>,
109    z: i32,
110) {
111    let (x, y, w, h) = snapped_rect(
112        canvas,
113        rrect.rect.x,
114        rrect.rect.y,
115        rrect.rect.w,
116        rrect.rect.h,
117    );
118    rrect.rect.x = x;
119    rrect.rect.y = y;
120    rrect.rect.w = w;
121    rrect.rect.h = h;
122    draw_rounded_rectangle(canvas, rrect, fill, stroke_width, stroke_brush, z);
123}
124
125/// Draw a circle with optional fill and stroke. Stroke supports solid color only.
126pub fn draw_circle(
127    canvas: &mut Canvas,
128    center: [f32; 2],
129    radius: f32,
130    fill: Option<Brush>,
131    stroke_width: Option<f32>,
132    stroke_brush: Option<Brush>,
133    z: i32,
134) {
135    if let Some(f) = fill {
136        canvas.circle(center, radius, f, z);
137    }
138    if let (Some(w), Some(sb)) = (stroke_width, stroke_brush) {
139        // Only solid strokes are supported for path-based circle stroke
140        if let Brush::Solid(col) = sb {
141            let segs = 48u32;
142            let mut path = Path {
143                cmds: Vec::new(),
144                fill_rule: FillRule::NonZero,
145            };
146            let mut first = true;
147            for i in 0..=segs {
148                let t = (i as f32) / (segs as f32);
149                let ang = std::f32::consts::TAU * t;
150                let x = center[0] + radius * ang.cos();
151                let y = center[1] + radius * ang.sin();
152                if first {
153                    path.cmds.push(PathCmd::MoveTo([x, y]));
154                    first = false;
155                } else {
156                    path.cmds.push(PathCmd::LineTo([x, y]));
157                }
158            }
159            path.cmds.push(PathCmd::Close);
160            canvas.stroke_path(path, w, col, z + 1);
161        }
162    }
163}
164
165/// Draw an ellipse with optional fill and stroke. Stroke supports solid color only.
166pub fn draw_ellipse(
167    canvas: &mut Canvas,
168    center: [f32; 2],
169    radii: [f32; 2],
170    fill: Option<Brush>,
171    stroke_width: Option<f32>,
172    stroke_brush: Option<Brush>,
173    z: i32,
174) {
175    if let Some(f) = fill {
176        canvas.ellipse(center, radii, f, z);
177    }
178    if let (Some(w), Some(sb)) = (stroke_width, stroke_brush) {
179        // Only solid strokes are supported for path-based ellipse stroke
180        if let Brush::Solid(col) = sb {
181            let segs = 64u32;
182            let mut path = Path {
183                cmds: Vec::new(),
184                fill_rule: FillRule::NonZero,
185            };
186            let mut first = true;
187            for i in 0..=segs {
188                let t = (i as f32) / (segs as f32);
189                let ang = std::f32::consts::TAU * t;
190                let x = center[0] + radii[0] * ang.cos();
191                let y = center[1] + radii[1] * ang.sin();
192                if first {
193                    path.cmds.push(PathCmd::MoveTo([x, y]));
194                    first = false;
195                } else {
196                    path.cmds.push(PathCmd::LineTo([x, y]));
197                }
198            }
199            path.cmds.push(PathCmd::Close);
200            canvas.stroke_path(path, w, col, z + 1);
201        }
202    }
203}