Skip to main content

sequoia_view/
painter.rs

1#![forbid(unsafe_code)]
2
3use sequoia_core::{
4    BrushStroke, Color, Fill, GradientKind, GradientStops, ImageSampling, ImageSource, Pixels,
5    Point, Rect, Size, StrokeSpace, Transform, Vec2,
6};
7use sequoia_text::FontWeight;
8
9use crate::paint_draw::crisp_axis_line;
10use crate::view::PaintCtx;
11
12pub struct Painter<'a, 'b> {
13    ctx: &'a mut PaintCtx<'b>,
14    origin: Point,
15    size: Size,
16
17    transform: Transform,
18
19    current_blend: sequoia_core::BlendMode,
20}
21
22impl<'a, 'b> Painter<'a, 'b> {
23    pub fn new(ctx: &'a mut PaintCtx<'b>) -> Self {
24        debug_assert!(
25            ctx.draw_offset.x == 0.0 && ctx.draw_offset.y == 0.0,
26            "Painter not using draw_offset - remove translate/set_draw_offset"
27        );
28        let origin = ctx.rect.origin;
29        let size = ctx.rect.size;
30        Self {
31            ctx,
32            origin,
33            size,
34            transform: Transform::IDENTITY,
35            current_blend: sequoia_core::BlendMode::Normal,
36        }
37    }
38
39    pub fn size(&self) -> Size {
40        self.size
41    }
42
43    pub fn rect(&self) -> Rect {
44        Rect::new(Point::ZERO, self.size)
45    }
46
47    pub fn origin_global(&self) -> Point {
48        self.origin
49    }
50
51    pub fn ctx(&mut self) -> &mut PaintCtx<'b> {
52        self.ctx
53    }
54
55    pub fn push_list_transform(&mut self, t: Transform) {
56        self.ctx.list.push_transform(t);
57    }
58
59    pub fn pop_list_transform(&mut self) {
60        self.ctx.list.pop_transform();
61    }
62
63    pub fn with_transform<R>(&mut self, t: Transform, f: impl FnOnce(&mut Self) -> R) -> R {
64        let prev = self.transform;
65        self.transform = t.then(prev);
66        let r = f(self);
67        self.transform = prev;
68        r
69    }
70
71    pub fn unproject(&self, p: Point) -> Option<Point> {
72        let inv = self.transform.inverse()?;
73        Some(inv.apply(Point::new(p.x - self.origin.x, p.y - self.origin.y)))
74    }
75
76    fn unit_scale(&self) -> f32 {
77        self.transform.apply_vec(Vec2::new(1.0, 0.0)).length()
78    }
79
80    fn stroke_scale(&self, stroke: BrushStroke) -> f32 {
81        match stroke.space {
82            StrokeSpace::Local => self.unit_scale(),
83            StrokeSpace::Screen => 1.0,
84            _ => self.unit_scale(),
85        }
86    }
87
88    fn stroke_style(&self, stroke: BrushStroke) -> sequoia_draw::StrokeStyle {
89        use sequoia_draw::StrokeStyle;
90        let w = (stroke.width * self.stroke_scale(stroke)).max(0.0);
91        if stroke.rounded {
92            StrokeStyle::round(w)
93        } else {
94            StrokeStyle::new(w)
95        }
96    }
97
98    pub fn to_global_rect(&self, r: Rect) -> Rect {
99        if r.is_empty() {
100            return r;
101        }
102        let r = self.transform.apply_rect_aabb(r);
103        Rect::from_xywh(
104            r.left() + self.origin.x,
105            r.top() + self.origin.y,
106            r.size.width,
107            r.size.height,
108        )
109    }
110
111    pub fn to_global_point(&self, p: Point) -> Point {
112        let p = self.transform.apply(p);
113        Point::new(p.x + self.origin.x, p.y + self.origin.y)
114    }
115
116    pub fn to_local_point(&self, p: Point) -> Point {
117        Point::new(p.x - self.origin.x, p.y - self.origin.y)
118    }
119
120    pub fn fill(&mut self, rect: Rect, fill: Fill) -> Rect {
121        self.rounded(rect, 0.0, fill)
122    }
123
124    pub fn rounded(&mut self, rect: Rect, radius: f32, fill: Fill) -> Rect {
125        if rect.is_empty() || !fill.is_visible() {
126            return Rect::ZERO;
127        }
128        let global = self.to_global_rect(rect);
129
130        let radius = radius * self.unit_scale();
131        let radius = if radius.is_finite() {
132            radius
133                .clamp(0.0, global.size.width.min(global.size.height) * 0.5)
134                .max(0.0)
135        } else {
136            0.0
137        };
138        match fill {
139            Fill::Solid(color) => {
140                if color.a <= 0.0 {
141                    return Rect::ZERO;
142                }
143                self.ctx.list.push_quad(global, color, radius);
144                rect
145            }
146            Fill::Gradient { kind, stops } => self.push_gradient(rect, radius, kind, stops),
147        }
148    }
149
150    pub fn circle(&mut self, center: Point, radius: f32, fill: Fill) -> Rect {
151        if !(radius > 0.0) || !radius.is_finite() || !fill.is_visible() {
152            return Rect::ZERO;
153        }
154        let rect = Rect::from_xywh(
155            center.x - radius,
156            center.y - radius,
157            radius * 2.0,
158            radius * 2.0,
159        );
160
161        let gr = radius * self.unit_scale();
162        if !(gr > 0.0) {
163            return Rect::ZERO;
164        }
165        match fill {
166            Fill::Solid(color) => {
167                if color.a <= 0.0 {
168                    return Rect::ZERO;
169                }
170                let global = self.to_global_rect(rect);
171                self.ctx.list.push_quad(global, color, gr);
172                rect
173            }
174            Fill::Gradient { kind, stops } => {
175                let kind = match kind {
176                    GradientKind::Radial { radius: r } if r <= 0.0 => {
177                        GradientKind::Radial { radius: gr }
178                    }
179                    other => other,
180                };
181                self.push_gradient(rect, gr, kind, stops)
182            }
183        }
184    }
185
186    pub fn ring(&mut self, center: Point, radius: f32, width: f32, color: Color) -> Rect {
187        if !(radius > 0.0) || !(width > 0.0) || !width.is_finite() || color.a <= 0.0 {
188            return Rect::ZERO;
189        }
190        let k = self.unit_scale();
191        let mut path = sequoia_draw::Path::new();
192        path.circle(self.to_global_point(center), radius * k);
193        let bounds = self
194            .ctx
195            .stroke(&path, &self.stroke_style(BrushStroke::new(width)), color);
196        self.to_local_bounds(bounds)
197    }
198
199    pub fn line(&mut self, from: Point, to: Point, stroke: BrushStroke, color: Color) -> Rect {
200        if !stroke.is_visible() || color.a <= 0.0 {
201            return Rect::ZERO;
202        }
203        let style = self.stroke_style(stroke);
204        let from = self.to_global_point(from);
205        let to = self.to_global_point(to);
206        let (from, to) = crisp_axis_line(
207            from,
208            to,
209            stroke.width * self.stroke_scale(stroke),
210            self.ctx.scale_factor(),
211        );
212        let mut path = sequoia_draw::Path::new();
213        path.move_to(from).line_to(to);
214        let bounds = self.ctx.stroke(&path, &style, color);
215        self.to_local_bounds(bounds)
216    }
217
218    pub fn polyline(&mut self, points: &[Point], stroke: BrushStroke, color: Color) -> Rect {
219        if points.len() < 2 || !stroke.is_visible() || color.a <= 0.0 {
220            return Rect::ZERO;
221        }
222        use sequoia_draw::Path;
223        let style = self.stroke_style(stroke);
224        let mut path = Path::new();
225        let mut it = points.iter();
226
227        path.move_to(self.to_global_point(*it.next().unwrap_or(&Point::ZERO)));
228        for p in it {
229            path.line_to(self.to_global_point(*p));
230        }
231        let bounds = self.ctx.stroke(&path, &style, color);
232        self.to_local_bounds(bounds)
233    }
234
235    pub fn curve(
236        &mut self,
237        p0: Point,
238        c0: Point,
239        c1: Point,
240        p1: Point,
241        stroke: BrushStroke,
242        color: Color,
243    ) -> Rect {
244        let span = (p1.x - p0.x).abs() + (p1.y - p0.y).abs();
245        let n = ((span / 12.0) as usize).clamp(4, 32);
246        let mut pts = Vec::with_capacity(n + 1);
247        for i in 0..=n {
248            let t = i as f32 / n as f32;
249            let u = 1.0 - t;
250            pts.push(Point::new(
251                u * u * u * p0.x
252                    + 3.0 * u * u * t * c0.x
253                    + 3.0 * u * t * t * c1.x
254                    + t * t * t * p1.x,
255                u * u * u * p0.y
256                    + 3.0 * u * u * t * c0.y
257                    + 3.0 * u * t * t * c1.y
258                    + t * t * t * p1.y,
259            ));
260        }
261        self.polyline(&pts, stroke, color)
262    }
263
264    pub fn rect_outline(
265        &mut self,
266        rect: Rect,
267        radius: f32,
268        stroke: BrushStroke,
269        color: Color,
270    ) -> Rect {
271        if rect.is_empty() || !stroke.is_visible() || color.a <= 0.0 {
272            return Rect::ZERO;
273        }
274        use sequoia_draw::Path;
275        let style = self.stroke_style(stroke);
276        let mut path = Path::new();
277        let global = self.to_global_rect(rect);
278        let radius = radius * self.unit_scale();
279        if radius > 0.0 {
280            path.rounded_rect(global, radius);
281        } else {
282            path.rect(global);
283        }
284        let bounds = self.ctx.stroke(&path, &style, color);
285        self.to_local_bounds(bounds)
286    }
287
288    pub fn polygon(&mut self, points: &[Point], color: Color) -> Rect {
289        if points.len() < 3 || color.a <= 0.0 {
290            return Rect::ZERO;
291        }
292        let global: Vec<Point> = points.iter().map(|p| self.to_global_point(*p)).collect();
293        let bounds = self.ctx.polygon(&global, color);
294        self.to_local_bounds(bounds)
295    }
296
297    pub fn ellipse(&mut self, center: Point, rx: f32, ry: f32, color: Color) -> Rect {
298        if !(rx > 0.0) || !(ry > 0.0) || color.a <= 0.0 {
299            return Rect::ZERO;
300        }
301        let kx = self.transform.apply_vec(Vec2::new(1.0, 0.0)).length();
302        let ky = self.transform.apply_vec(Vec2::new(0.0, 1.0)).length();
303        let bounds = self
304            .ctx
305            .ellipse(self.to_global_point(center), rx * kx, ry * ky, color);
306        self.to_local_bounds(bounds)
307    }
308
309    /// Draws a single line with `baseline` as the text baseline.
310    ///
311    /// The font size scales with the active `with_transform` scale so text
312    /// zooms with the canvas instead of staying at a fixed pixel size.
313    pub fn text(&mut self, text: &str, baseline: Point, size: f32, color: Color) {
314        if text.is_empty() || !(size > 0.0) || color.a <= 0.0 {
315            return;
316        }
317        let g = self.to_global_point(baseline);
318        self.ctx
319            .draw_text(text, (g.x, g.y), size * self.unit_scale(), color);
320    }
321
322    /// Draws a single line with `top_left` at the top-left of its line box.
323    ///
324    /// The font size scales with the active `with_transform` scale.
325    pub fn text_tl(&mut self, text: &str, top_left: Point, size: f32, color: Color) {
326        let k = self.unit_scale();
327        if k <= 0.0 {
328            return;
329        }
330        let ascent = self.ctx.text_metrics(size * k).ascent / k;
331        let baseline = Point::new(top_left.x, top_left.y + ascent);
332        self.text(text, baseline, size, color);
333    }
334
335    /// Vertical metrics for the default font at `size`.
336    pub fn text_metrics(&mut self, size: f32) -> crate::TextMetrics {
337        self.ctx.text_metrics(size)
338    }
339
340    pub fn text_tl_rotated(
341        &mut self,
342        text: &str,
343        top_left: Point,
344        size: f32,
345        color: Color,
346        degrees: f32,
347        pivot: Point,
348    ) {
349        let pivot = self.to_global_point(pivot);
350        let t = Transform::rotate_around_degrees(degrees, pivot);
351        self.push_list_transform(t);
352        self.text_tl(text, top_left, size, color);
353        self.pop_list_transform();
354    }
355
356    pub fn text_styled(
357        &mut self,
358        text: &str,
359        baseline: Point,
360        size: f32,
361        weight: FontWeight,
362        color: Color,
363    ) {
364        if text.is_empty() || !(size > 0.0) || color.a <= 0.0 {
365            return;
366        }
367        let g = self.to_global_point(baseline);
368        self.ctx
369            .draw_text_weighted(text, (g.x, g.y), size * self.unit_scale(), weight, color);
370    }
371
372    /// Measures `text` in local coordinates, including the transform scale.
373    pub fn text_width(&mut self, text: &str, size: f32) -> f32 {
374        if text.is_empty() || !(size > 0.0) {
375            return 0.0;
376        }
377        let k = self.unit_scale();
378        if k <= 0.0 {
379            return 0.0;
380        }
381        self.ctx.text_width(text, size * k) / k
382    }
383
384    pub fn text_width_weighted(&mut self, text: &str, size: f32, weight: FontWeight) -> f32 {
385        if text.is_empty() || !(size > 0.0) {
386            return 0.0;
387        }
388        let k = self.unit_scale();
389        if k <= 0.0 {
390            return 0.0;
391        }
392        self.ctx.text_width_weighted(text, size * k, weight) / k
393    }
394
395    pub fn text_center_x(&mut self, text: &str, center_x: f32, top: f32, size: f32, color: Color) {
396        let width = self.text_width(text, size);
397        self.text_tl(text, Point::new(center_x - width * 0.5, top), size, color);
398    }
399
400    pub fn text_center(&mut self, rect: Rect, text: &str, size: f32, color: Color) {
401        let k = self.unit_scale();
402        if k <= 0.0 {
403            return;
404        }
405        let metrics = self.ctx.measure_text(text, size * k);
406        let width = metrics.width / k;
407        let height = metrics.height / k;
408        let top = rect.top() + (rect.size.height - height) * 0.5;
409        self.text_tl(
410            text,
411            Point::new(rect.center().x - width * 0.5, top),
412            size,
413            color,
414        );
415    }
416
417    pub fn image(&mut self, rect: Rect, uv: [f32; 4], source: ImageSource) -> Rect {
418        if rect.is_empty() {
419            return Rect::ZERO;
420        }
421        let global = self.to_global_rect(rect);
422        self.ctx.list.push_image(global, uv, source);
423        rect
424    }
425
426    pub fn pixels(&mut self, rect: Rect, pixels: &Pixels) -> Rect {
427        let full = Rect::from_xywh(0.0, 0.0, pixels.width() as f32, pixels.height() as f32);
428        self.pixels_region_sampled(rect, pixels, full, ImageSampling::Linear)
429    }
430
431    pub fn pixels_nearest(&mut self, rect: Rect, pixels: &Pixels) -> Rect {
432        let full = Rect::from_xywh(0.0, 0.0, pixels.width() as f32, pixels.height() as f32);
433        self.pixels_region_sampled(rect, pixels, full, ImageSampling::Nearest)
434    }
435
436    pub fn pixels_region(&mut self, dst: Rect, pixels: &Pixels, src: Rect) -> Rect {
437        self.pixels_region_sampled(dst, pixels, src, ImageSampling::Linear)
438    }
439
440    fn pixels_region_sampled(
441        &mut self,
442        dst: Rect,
443        pixels: &Pixels,
444        src: Rect,
445        sampling: ImageSampling,
446    ) -> Rect {
447        if dst.is_empty() {
448            return Rect::ZERO;
449        }
450        let Some(uv) = Self::pixel_uv(pixels.width(), pixels.height(), src) else {
451            return Rect::ZERO;
452        };
453        let global = self.to_global_rect(dst);
454        self.ctx
455            .list
456            .push_image_sampled(global, uv, pixels.to_source(), sampling);
457        dst
458    }
459
460    pub fn pixel_uv(width: u32, height: u32, src: Rect) -> Option<[f32; 4]> {
461        if width == 0 || height == 0 {
462            return None;
463        }
464        let (w, h) = (width as f32, height as f32);
465        let x0 = src.left().clamp(0.0, w);
466        let y0 = src.top().clamp(0.0, h);
467        let x1 = src.right().clamp(0.0, w);
468        let y1 = src.bottom().clamp(0.0, h);
469        if x1 <= x0 || y1 <= y0 {
470            return None;
471        }
472        Some([x0 / w, y0 / h, x1 / w, y1 / h])
473    }
474
475    /// Maps `rect` onto an offscreen target that was rendered at `group`.
476    ///
477    /// A target captures the whole group area, so drawing it into a smaller
478    /// rect with `[0.0, 0.0, 1.0, 1.0]` squeezes the entire target into that
479    /// rect. These coordinates sample only the part of the target under
480    /// `rect`, which is what [`Painter::group`] users need.
481    #[must_use]
482    pub fn target_uv(group: Size, rect: Rect) -> [f32; 4] {
483        if group.width <= 0.0 || group.height <= 0.0 || rect.is_empty() {
484            return [0.0, 0.0, 1.0, 1.0];
485        }
486        [
487            (rect.left() / group.width).clamp(0.0, 1.0),
488            (rect.top() / group.height).clamp(0.0, 1.0),
489            (rect.right() / group.width).clamp(0.0, 1.0),
490            (rect.bottom() / group.height).clamp(0.0, 1.0),
491        ]
492    }
493
494    pub fn pixel_at(rect: Rect, width: u32, height: u32, p: Point) -> Option<(i32, i32)> {
495        if width == 0 || height == 0 || rect.is_empty() {
496            return None;
497        }
498        let fx = (p.x - rect.left()) / rect.size.width * width as f32;
499        let fy = (p.y - rect.top()) / rect.size.height * height as f32;
500        if !fx.is_finite() || !fy.is_finite() || fx < 0.0 || fy < 0.0 {
501            return None;
502        }
503        if fx >= width as f32 || fy >= height as f32 {
504            return None;
505        }
506        Some((fx.floor() as i32, fy.floor() as i32))
507    }
508
509    pub fn knob(&mut self, center: Point, radius: f32, fill: Color) -> Rect {
510        if !(radius > 0.0) || !radius.is_finite() {
511            return Rect::ZERO;
512        }
513        let outer = self.circle(center, radius, Fill::solid(Color::WHITE));
514        let inner_r = radius - 2.0;
515        if inner_r > 0.0 {
516            let inner = self.circle(center, inner_r, Fill::solid(fill));
517            outer.union(inner)
518        } else {
519            outer
520        }
521    }
522
523    pub fn sv_field(&mut self, rect: Rect, hue01: f32) -> Rect {
524        if rect.is_empty() {
525            return Rect::ZERO;
526        }
527        let h = hue01.clamp(0.0, 1.0);
528        self.fill(rect, Fill::solid(Color::from_hsva(h, 1.0, 1.0, 1.0)));
529        self.fill(
530            rect,
531            Fill::horizontal(GradientStops::two(
532                Color::WHITE,
533                Color::rgba(1.0, 1.0, 1.0, 0.0),
534            )),
535        );
536        self.fill(
537            rect,
538            Fill::vertical(GradientStops::two(
539                Color::rgba(0.0, 0.0, 0.0, 0.0),
540                Color::BLACK,
541            )),
542        );
543        rect
544    }
545
546    /// Returns the blend mode used by subsequent shader draws and blits.
547    #[must_use]
548    pub fn current_blend(&self) -> sequoia_core::BlendMode {
549        self.current_blend
550    }
551
552    /// Runs `f` with `mode` as the blend mode for shader draws and blits.
553    pub fn with_blend<R>(
554        &mut self,
555        mode: sequoia_core::BlendMode,
556        f: impl FnOnce(&mut Self) -> R,
557    ) -> R {
558        let previous = self.current_blend;
559        self.current_blend = mode;
560        let result = f(self);
561        self.current_blend = previous;
562        result
563    }
564
565    /// Renders `f` into a new offscreen target.
566    ///
567    /// The target is transparent when `f` starts painting into it. Use the
568    /// returned handle as a shader input ([`sequoia_core::TexInput::Target`]) or
569    /// composite it with [`Painter::blit`].
570    pub fn group<R>(
571        &mut self,
572        size: Size,
573        blend: sequoia_core::BlendMode,
574        f: impl FnOnce(&mut Self) -> R,
575    ) -> (sequoia_core::RenderTargetId, R) {
576        let target = self.ctx.list.begin_target(size, blend);
577        let result = f(self);
578        self.ctx.list.end_target(target);
579        (target, result)
580    }
581
582    /// Copies a finished offscreen target into the current pass.
583    ///
584    /// `uv` is `[u0, v0, u1, v1]`; `[0.0, 0.0, 1.0, 1.0]` copies the whole
585    /// target. The current blend mode from [`Painter::with_blend`] applies.
586    pub fn blit(&mut self, target: sequoia_core::RenderTargetId, rect: Rect, uv: [f32; 4]) -> Rect {
587        if rect.is_empty() {
588            return Rect::ZERO;
589        }
590        let global = self.to_global_rect(rect);
591        self.ctx
592            .list
593            .push_blit(global, uv, target, self.current_blend);
594        rect
595    }
596
597    /// Pushes a typed shader draw generated by [`sequoia_shader::shader`].
598    ///
599    /// Texture names must match the `Sampler2D` parameters of the shader.
600    /// Inputs may be images or offscreen targets. Missing inputs sample a
601    /// transparent white texture. The shader must have been registered under
602    /// `S::NAME` (see `App::shader_type`).
603    pub fn shader<S: sequoia_shader::SequoiShader>(
604        &mut self,
605        shader: S,
606        rect: Rect,
607        inputs: &[(&str, sequoia_core::TexInput)],
608    ) -> Rect {
609        self.shader_at(shader, rect, [0.0, 0.0, 1.0, 1.0], inputs)
610    }
611
612    /// Pushes a typed shader draw with explicit texture coordinates.
613    ///
614    /// `uv` is `[u0, v0, u1, v1]`. Use [`Painter::group`] to produce target
615    /// inputs. Returns [`Rect::ZERO`] when the shader is not registered.
616    pub fn shader_at<S: sequoia_shader::SequoiShader>(
617        &mut self,
618        shader: S,
619        rect: Rect,
620        uv: [f32; 4],
621        inputs: &[(&str, sequoia_core::TexInput)],
622    ) -> Rect {
623        if self.push_shader_draw(shader, rect, uv, inputs, None) {
624            rect
625        } else {
626            Rect::ZERO
627        }
628    }
629
630    /// Pushes a typed shader draw and reports whether it was recorded.
631    ///
632    /// Unlike [`Painter::shader`], a missing registration is visible to the
633    /// caller, which makes optional effect chains and tests explicit.
634    pub fn try_shader<S: sequoia_shader::SequoiShader>(
635        &mut self,
636        shader: S,
637        rect: Rect,
638        inputs: &[(&str, sequoia_core::TexInput)],
639    ) -> bool {
640        self.push_shader_draw(shader, rect, [0.0, 0.0, 1.0, 1.0], inputs, None)
641    }
642
643    /// Records a shader draw with explicit texture coordinates.
644    pub fn try_shader_at<S: sequoia_shader::SequoiShader>(
645        &mut self,
646        shader: S,
647        rect: Rect,
648        uv: [f32; 4],
649        inputs: &[(&str, sequoia_core::TexInput)],
650    ) -> bool {
651        self.push_shader_draw(shader, rect, uv, inputs, None)
652    }
653
654    /// Records a shader draw with an explicit blend mode.
655    pub fn try_shader_with_blend<S: sequoia_shader::SequoiShader>(
656        &mut self,
657        shader: S,
658        rect: Rect,
659        textures: &[(&str, sequoia_core::TexInput)],
660        blend: sequoia_core::BlendMode,
661    ) -> bool {
662        self.push_shader_draw(shader, rect, [0.0, 0.0, 1.0, 1.0], textures, Some(blend))
663    }
664
665    /// Records a shader draw with explicit texture coordinates and blend mode.
666    pub fn try_shader_at_with_blend<S: sequoia_shader::SequoiShader>(
667        &mut self,
668        shader: S,
669        rect: Rect,
670        uv: [f32; 4],
671        textures: &[(&str, sequoia_core::TexInput)],
672        blend: sequoia_core::BlendMode,
673    ) -> bool {
674        self.push_shader_draw(shader, rect, uv, textures, Some(blend))
675    }
676
677    /// Pushes a typed shader draw with an explicit blend mode.
678    pub fn shader_with_blend<S: sequoia_shader::SequoiShader>(
679        &mut self,
680        shader: S,
681        rect: Rect,
682        textures: &[(&str, sequoia_core::TexInput)],
683        blend: sequoia_core::BlendMode,
684    ) -> Rect {
685        if self.push_shader_draw(shader, rect, [0.0, 0.0, 1.0, 1.0], textures, Some(blend)) {
686            rect
687        } else {
688            Rect::ZERO
689        }
690    }
691
692    fn push_shader_draw<S: sequoia_shader::SequoiShader>(
693        &mut self,
694        shader: S,
695        rect: Rect,
696        uv: [f32; 4],
697        inputs: &[(&str, sequoia_core::TexInput)],
698        blend: Option<sequoia_core::BlendMode>,
699    ) -> bool {
700        use sequoia_core::ShaderDraw;
701        use std::sync::Arc;
702
703        if rect.is_empty() {
704            return false;
705        }
706        let Some(shader_id) = self.ctx.runtime().shader_id(S::NAME) else {
707            log::warn!("Painter::shader: shader `{}` is not registered", S::NAME);
708            return false;
709        };
710        let mut uniforms = Vec::with_capacity(S::UNIFORM_SIZE);
711        shader.encode_uniforms(&mut uniforms);
712        let mut bindings = Vec::new();
713        for (slot, name) in S::TEXTURES
714            .iter()
715            .enumerate()
716            .take(sequoia_shader::MAX_TEXTURES)
717        {
718            let Some((_, input)) = inputs.iter().find(|(key, _)| key == name) else {
719                continue;
720            };
721            bindings.push((slot as u32, input.clone()));
722        }
723        for (name, _) in inputs {
724            if !S::TEXTURES.contains(name) {
725                log::warn!(
726                    "Painter::shader: shader `{}` has no texture input named `{name}`",
727                    S::NAME
728                );
729            }
730        }
731        let global = self.to_global_rect(rect);
732        let draw = ShaderDraw {
733            rect: global,
734            uv,
735            shader: shader_id,
736            uniforms: Arc::from(uniforms),
737            textures: Arc::from(bindings),
738            blend: blend.unwrap_or(self.current_blend),
739            opacity: 1.0,
740        };
741        self.ctx.list.push_shader(draw);
742        true
743    }
744
745    pub fn grid(&mut self, step: f32, color: Color) -> u32 {
746        self.grid_offset(step, Vec2::ZERO, color)
747    }
748
749    pub fn grid_offset(&mut self, step: f32, offset: Vec2, color: Color) -> u32 {
750        if step <= 0.0 || color.a <= 0.0 || self.size.width <= 0.0 || self.size.height <= 0.0 {
751            return 0;
752        }
753        let mut drawn = 0u32;
754        let ox = offset.x.rem_euclid(step);
755        let oy = offset.y.rem_euclid(step);
756        let w = self.size.width;
757        let h = self.size.height;
758        let mut x = ox;
759        while x < w {
760            self.fill(Rect::from_xywh(x, 0.0, 1.0, h), Fill::solid(color));
761            drawn += 1;
762            if drawn > 4096 {
763                break;
764            }
765            x += step;
766        }
767        let mut y = oy;
768        while y < h {
769            self.fill(Rect::from_xywh(0.0, y, w, 1.0), Fill::solid(color));
770            drawn += 1;
771            if drawn > 8192 {
772                break;
773            }
774            y += step;
775        }
776        drawn
777    }
778
779    pub fn axes(&mut self, origin: Point, color: Color, width: f32) -> Rect {
780        if !(width > 0.0) || color.a <= 0.0 {
781            return Rect::ZERO;
782        }
783        let stroke = BrushStroke::new(width);
784        let w = self.size.width;
785        let h = self.size.height;
786        let a = self.line(
787            Point::new(0.0, origin.y),
788            Point::new(w, origin.y),
789            stroke,
790            color,
791        );
792        let b = self.line(
793            Point::new(origin.x, 0.0),
794            Point::new(origin.x, h),
795            stroke,
796            color,
797        );
798        a.union(b)
799    }
800
801    pub fn frame(&mut self, rect: Rect, color: Color, width: f32) -> Rect {
802        self.rect_outline(rect, 0.0, BrushStroke::new(width), color)
803    }
804
805    pub fn cross(&mut self, center: Point, half: f32, width: f32, color: Color) -> Rect {
806        if !(half > 0.0) || color.a <= 0.0 {
807            return Rect::ZERO;
808        }
809        let s = BrushStroke::round(width);
810        let a = self.line(
811            Point::new(center.x - half, center.y),
812            Point::new(center.x + half, center.y),
813            s,
814            color,
815        );
816        let b = self.line(
817            Point::new(center.x, center.y - half),
818            Point::new(center.x, center.y + half),
819            s,
820            color,
821        );
822        a.union(b)
823    }
824
825    pub fn marquee(&mut self, a: Point, b: Point, fill: Fill, outline: Color) -> Rect {
826        let rect = Rect::from_xywh(
827            a.x.min(b.x),
828            a.y.min(b.y),
829            (a.x - b.x).abs(),
830            (a.y - b.y).abs(),
831        );
832        if rect.is_empty() {
833            return Rect::ZERO;
834        }
835        self.fill(rect, fill);
836        self.rect_outline(rect, 0.0, BrushStroke::new(1.0), outline);
837        rect
838    }
839
840    pub fn ellipse_outline(
841        &mut self,
842        center: Point,
843        rx: f32,
844        ry: f32,
845        stroke: BrushStroke,
846        color: Color,
847    ) -> Rect {
848        if !(rx > 0.0) || !(ry > 0.0) || !stroke.is_visible() || color.a <= 0.0 {
849            return Rect::ZERO;
850        }
851        use sequoia_draw::Path;
852        let style = self.stroke_style(stroke);
853        let kx = self.transform.apply_vec(Vec2::new(1.0, 0.0)).length();
854        let ky = self.transform.apply_vec(Vec2::new(0.0, 1.0)).length();
855        let mut path = Path::new();
856        path.ellipse(self.to_global_point(center), rx * kx, ry * ky);
857        let bounds = self.ctx.stroke(&path, &style, color);
858        self.to_local_bounds(bounds)
859    }
860
861    pub fn capsule(&mut self, from: Point, to: Point, radius: f32, color: Color) -> Rect {
862        if !(radius > 0.0) || color.a <= 0.0 {
863            return Rect::ZERO;
864        }
865        let bounds = self.ctx.capsule(
866            self.to_global_point(from),
867            self.to_global_point(to),
868            radius,
869            color,
870        );
871        self.to_local_bounds(bounds)
872    }
873
874    pub fn arrow(
875        &mut self,
876        from: Point,
877        to: Point,
878        width: f32,
879        head_len: f32,
880        head_width: f32,
881        color: Color,
882    ) -> Rect {
883        if color.a <= 0.0 {
884            return Rect::ZERO;
885        }
886        let bounds = self.ctx.arrow(
887            self.to_global_point(from),
888            self.to_global_point(to),
889            width,
890            head_len,
891            head_width,
892            color,
893        );
894        self.to_local_bounds(bounds)
895    }
896
897    pub fn x_marker(&mut self, center: Point, half: f32, width: f32, color: Color) -> Rect {
898        if !(half > 0.0) || color.a <= 0.0 {
899            return Rect::ZERO;
900        }
901        let bounds = self
902            .ctx
903            .x_marker(self.to_global_point(center), half, width, color);
904        self.to_local_bounds(bounds)
905    }
906
907    pub fn regular_polygon(
908        &mut self,
909        center: Point,
910        radius: f32,
911        sides: u32,
912        rotation_deg: f32,
913        color: Color,
914    ) -> Rect {
915        if !(radius > 0.0) || sides < 3 || color.a <= 0.0 {
916            return Rect::ZERO;
917        }
918        let bounds = self.ctx.regular_polygon(
919            self.to_global_point(center),
920            radius,
921            sides,
922            rotation_deg,
923            color,
924        );
925        self.to_local_bounds(bounds)
926    }
927
928    pub fn star(
929        &mut self,
930        center: Point,
931        outer_r: f32,
932        inner_r: f32,
933        points: u32,
934        rotation_deg: f32,
935        color: Color,
936    ) -> Rect {
937        if !(outer_r > 0.0) || !(inner_r > 0.0) || points < 2 || color.a <= 0.0 {
938            return Rect::ZERO;
939        }
940        let bounds = self.ctx.star(
941            self.to_global_point(center),
942            outer_r,
943            inner_r,
944            points,
945            rotation_deg,
946            color,
947        );
948        self.to_local_bounds(bounds)
949    }
950
951    pub fn dots(&mut self, step: f32, offset: Vec2, radius: f32, color: Color) -> u32 {
952        if step <= 0.0 || !(radius > 0.0) || color.a <= 0.0 {
953            return 0;
954        }
955        if self.size.width <= 0.0 || self.size.height <= 0.0 {
956            return 0;
957        }
958        let mut n = 0u32;
959        let ox = offset.x.rem_euclid(step);
960        let oy = offset.y.rem_euclid(step);
961        let mut y = oy;
962        while y < self.size.height {
963            let mut x = ox;
964            while x < self.size.width {
965                self.circle(Point::new(x, y), radius, Fill::solid(color));
966                n += 1;
967                if n > 16384 {
968                    return n;
969                }
970                x += step;
971            }
972            y += step;
973        }
974        n
975    }
976
977    pub fn grid_split(
978        &mut self,
979        step: f32,
980        every: u32,
981        offset: Vec2,
982        minor: Color,
983        major: Color,
984    ) -> u32 {
985        if step <= 0.0 || every == 0 {
986            return 0;
987        }
988        if self.size.width <= 0.0 || self.size.height <= 0.0 {
989            return 0;
990        }
991        let every = every.max(1);
992        let mut drawn = 0u32;
993        let ox = offset.x.rem_euclid(step);
994        let oy = offset.y.rem_euclid(step);
995        let w = self.size.width;
996        let h = self.size.height;
997        let mut i = 0u32;
998        let mut x = ox;
999        while x < w {
1000            let c = if i % every == every - 1 { major } else { minor };
1001            self.fill(Rect::from_xywh(x, 0.0, 1.0, h), Fill::solid(c));
1002            drawn += 1;
1003            if drawn > 4096 {
1004                break;
1005            }
1006            x += step;
1007            i += 1;
1008        }
1009        let mut j = 0u32;
1010        let mut y = oy;
1011        while y < h {
1012            let c = if j % every == every - 1 { major } else { minor };
1013            self.fill(Rect::from_xywh(0.0, y, w, 1.0), Fill::solid(c));
1014            drawn += 1;
1015            if drawn > 8192 {
1016                break;
1017            }
1018            y += step;
1019            j += 1;
1020        }
1021        drawn
1022    }
1023
1024    pub fn clipped<R>(&mut self, rect: Rect, f: impl FnOnce(&mut Self) -> R) -> R {
1025        let global = self.to_global_rect(rect);
1026        self.ctx.list.push_clip(global);
1027        let r = f(self);
1028        self.ctx.list.pop_clip();
1029        r
1030    }
1031
1032    pub fn clipped_rounded<R>(
1033        &mut self,
1034        rect: Rect,
1035        radius: f32,
1036        f: impl FnOnce(&mut Self) -> R,
1037    ) -> R {
1038        let global = self.to_global_rect(rect);
1039        self.ctx.push_rounded_clip(global, radius);
1040        let r = f(self);
1041        self.ctx.pop_clip();
1042        r
1043    }
1044
1045    fn push_gradient(
1046        &mut self,
1047        rect: Rect,
1048        radius: f32,
1049        kind: GradientKind,
1050        stops: GradientStops,
1051    ) -> Rect {
1052        if stops.len() <= sequoia_core::MAX_GRADIENT_STOPS {
1053            let global = self.to_global_rect(rect);
1054            self.ctx.list.push_gradient(global, radius, kind, stops);
1055            return rect;
1056        }
1057
1058        self.push_gradient_bands(rect, radius, kind, stops)
1059    }
1060
1061    fn push_gradient_bands(
1062        &mut self,
1063        rect: Rect,
1064        shape_radius: f32,
1065        kind: GradientKind,
1066        stops: GradientStops,
1067    ) -> Rect {
1068        const RADIAL_BANDS: usize = 64;
1069        let vertical = match kind {
1070            GradientKind::Linear { angle_deg } => {
1071                let (dx, dy) = GradientKind::Linear { angle_deg }.direction();
1072                dy.abs() >= dx.abs()
1073            }
1074            GradientKind::Radial { .. } => true,
1075        };
1076        let global = self.to_global_rect(rect);
1077        let mut bounds = Rect::ZERO;
1078        match kind {
1079            GradientKind::Radial { radius } => {
1080                let center = Point::new(
1081                    global.left() + global.size.width * 0.5,
1082                    global.top() + global.size.height * 0.5,
1083                );
1084                let extent = if radius > 0.0 {
1085                    radius
1086                } else {
1087                    global.size.width.hypot(global.size.height) * 0.5
1088                };
1089                if extent <= 0.0 {
1090                    return Rect::ZERO;
1091                }
1092                if shape_radius > 0.0 {
1093                    self.ctx.push_rounded_clip(global, shape_radius);
1094                } else {
1095                    self.ctx.list.push_clip(global);
1096                }
1097                for i in (0..RADIAL_BANDS).rev() {
1098                    let t0 = i as f32 / RADIAL_BANDS as f32;
1099                    let t1 = (i + 1) as f32 / RADIAL_BANDS as f32;
1100                    let [r, g, b, a] = stops.sample_linear((t0 + t1) * 0.5);
1101                    let color = Color { r, g, b, a }.from_linear();
1102                    let r1 = extent * t1;
1103                    let band = Rect::from_xywh(center.x - r1, center.y - r1, r1 * 2.0, r1 * 2.0);
1104                    self.ctx.list.push_quad(band, color, r1);
1105                }
1106                self.ctx.pop_clip();
1107                bounds = global;
1108            }
1109            GradientKind::Linear { .. } => {
1110                let extent = if vertical {
1111                    global.size.height
1112                } else {
1113                    global.size.width
1114                };
1115                let bands = (extent.ceil() as usize).clamp(1, 256);
1116                let scale = {
1117                    let s = self.ctx.scale_factor();
1118                    if s.is_finite() && s > 0.0 { s } else { 1.0 }
1119                };
1120                let origin = if vertical {
1121                    global.top()
1122                } else {
1123                    global.left()
1124                };
1125                let edges =
1126                    crate::paint_draw::band_edges(origin, extent, bands, 1.0 / scale, 0.5 / scale);
1127                for (i, (lo, hi)) in edges.into_iter().enumerate() {
1128                    let t = (i as f32 + 0.5) / bands as f32;
1129                    let [r, g, b, a] = stops.sample_linear(t);
1130                    let color = Color { r, g, b, a }.from_linear();
1131                    let band = if vertical {
1132                        Rect::from_xywh(global.left(), lo, global.size.width, hi - lo)
1133                    } else {
1134                        Rect::from_xywh(lo, global.top(), hi - lo, global.size.height)
1135                    };
1136                    self.ctx.list.push_quad(band, color, 0.0);
1137                    bounds = bounds.union(band);
1138                }
1139            }
1140        }
1141        if bounds.is_empty() {
1142            return Rect::ZERO;
1143        }
1144
1145        Rect::from_xywh(
1146            bounds.left() - self.origin.x,
1147            bounds.top() - self.origin.y,
1148            bounds.size.width,
1149            bounds.size.height,
1150        )
1151    }
1152
1153    fn to_local_bounds(&self, bounds: Rect) -> Rect {
1154        if bounds.is_empty() {
1155            return Rect::ZERO;
1156        }
1157        let r = Rect::from_xywh(
1158            bounds.left() - self.origin.x,
1159            bounds.top() - self.origin.y,
1160            bounds.size.width,
1161            bounds.size.height,
1162        );
1163
1164        match self.transform.inverse() {
1165            Some(inv) => inv.apply_rect_aabb(r),
1166            None => Rect::ZERO,
1167        }
1168    }
1169}