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
20impl<'a, 'b> Painter<'a, 'b> {
21    pub fn new(ctx: &'a mut PaintCtx<'b>) -> Self {
22        debug_assert!(
23            ctx.draw_offset.x == 0.0 && ctx.draw_offset.y == 0.0,
24            "Painter not using draw_offset - remove translate/set_draw_offset"
25        );
26        let origin = ctx.rect.origin;
27        let size = ctx.rect.size;
28        Self {
29            ctx,
30            origin,
31            size,
32            transform: Transform::IDENTITY,
33        }
34    }
35
36    pub fn size(&self) -> Size {
37        self.size
38    }
39
40    pub fn rect(&self) -> Rect {
41        Rect::new(Point::ZERO, self.size)
42    }
43
44    pub fn origin_global(&self) -> Point {
45        self.origin
46    }
47
48    pub fn ctx(&mut self) -> &mut PaintCtx<'b> {
49        self.ctx
50    }
51
52    pub fn push_list_transform(&mut self, t: Transform) {
53        self.ctx.list.push_transform(t);
54    }
55
56    pub fn pop_list_transform(&mut self) {
57        self.ctx.list.pop_transform();
58    }
59
60    pub fn with_transform<R>(&mut self, t: Transform, f: impl FnOnce(&mut Self) -> R) -> R {
61        let prev = self.transform;
62        self.transform = t.then(prev);
63        let r = f(self);
64        self.transform = prev;
65        r
66    }
67
68    pub fn unproject(&self, p: Point) -> Option<Point> {
69        let inv = self.transform.inverse()?;
70        Some(inv.apply(Point::new(p.x - self.origin.x, p.y - self.origin.y)))
71    }
72
73    fn unit_scale(&self) -> f32 {
74        self.transform.apply_vec(Vec2::new(1.0, 0.0)).length()
75    }
76
77    fn stroke_scale(&self, stroke: BrushStroke) -> f32 {
78        match stroke.space {
79            StrokeSpace::Local => self.unit_scale(),
80            StrokeSpace::Screen => 1.0,
81            _ => self.unit_scale(),
82        }
83    }
84
85    fn stroke_style(&self, stroke: BrushStroke) -> sequoia_draw::StrokeStyle {
86        use sequoia_draw::StrokeStyle;
87        let w = (stroke.width * self.stroke_scale(stroke)).max(0.0);
88        if stroke.rounded {
89            StrokeStyle::round(w)
90        } else {
91            StrokeStyle::new(w)
92        }
93    }
94
95    pub fn to_global_rect(&self, r: Rect) -> Rect {
96        if r.is_empty() {
97            return r;
98        }
99        let r = self.transform.apply_rect_aabb(r);
100        Rect::from_xywh(
101            r.left() + self.origin.x,
102            r.top() + self.origin.y,
103            r.size.width,
104            r.size.height,
105        )
106    }
107
108    pub fn to_global_point(&self, p: Point) -> Point {
109        let p = self.transform.apply(p);
110        Point::new(p.x + self.origin.x, p.y + self.origin.y)
111    }
112
113    pub fn to_local_point(&self, p: Point) -> Point {
114        Point::new(p.x - self.origin.x, p.y - self.origin.y)
115    }
116
117    pub fn fill(&mut self, rect: Rect, fill: Fill) -> Rect {
118        self.rounded(rect, 0.0, fill)
119    }
120
121    pub fn rounded(&mut self, rect: Rect, radius: f32, fill: Fill) -> Rect {
122        if rect.is_empty() || !fill.is_visible() {
123            return Rect::ZERO;
124        }
125        let global = self.to_global_rect(rect);
126
127        let radius = radius * self.unit_scale();
128        let radius = if radius.is_finite() {
129            radius
130                .clamp(0.0, global.size.width.min(global.size.height) * 0.5)
131                .max(0.0)
132        } else {
133            0.0
134        };
135        match fill {
136            Fill::Solid(color) => {
137                if color.a <= 0.0 {
138                    return Rect::ZERO;
139                }
140                self.ctx.list.push_quad(global, color, radius);
141                rect
142            }
143            Fill::Gradient { kind, stops } => self.push_gradient(rect, radius, kind, stops),
144        }
145    }
146
147    pub fn circle(&mut self, center: Point, radius: f32, fill: Fill) -> Rect {
148        if !(radius > 0.0) || !radius.is_finite() || !fill.is_visible() {
149            return Rect::ZERO;
150        }
151        let rect = Rect::from_xywh(
152            center.x - radius,
153            center.y - radius,
154            radius * 2.0,
155            radius * 2.0,
156        );
157
158        let gr = radius * self.unit_scale();
159        if !(gr > 0.0) {
160            return Rect::ZERO;
161        }
162        match fill {
163            Fill::Solid(color) => {
164                if color.a <= 0.0 {
165                    return Rect::ZERO;
166                }
167                let global = self.to_global_rect(rect);
168                self.ctx.list.push_quad(global, color, gr);
169                rect
170            }
171            Fill::Gradient { kind, stops } => {
172                let kind = match kind {
173                    GradientKind::Radial { radius: r } if r <= 0.0 => {
174                        GradientKind::Radial { radius: gr }
175                    }
176                    other => other,
177                };
178                self.push_gradient(rect, gr, kind, stops)
179            }
180        }
181    }
182
183    pub fn ring(&mut self, center: Point, radius: f32, width: f32, color: Color) -> Rect {
184        if !(radius > 0.0) || !(width > 0.0) || !width.is_finite() || color.a <= 0.0 {
185            return Rect::ZERO;
186        }
187        let k = self.unit_scale();
188        let mut path = sequoia_draw::Path::new();
189        path.circle(self.to_global_point(center), radius * k);
190        let bounds = self
191            .ctx
192            .stroke(&path, &self.stroke_style(BrushStroke::new(width)), color);
193        self.to_local_bounds(bounds)
194    }
195
196    pub fn line(&mut self, from: Point, to: Point, stroke: BrushStroke, color: Color) -> Rect {
197        if !stroke.is_visible() || color.a <= 0.0 {
198            return Rect::ZERO;
199        }
200        let style = self.stroke_style(stroke);
201        let from = self.to_global_point(from);
202        let to = self.to_global_point(to);
203        let (from, to) = crisp_axis_line(
204            from,
205            to,
206            stroke.width * self.stroke_scale(stroke),
207            self.ctx.scale_factor(),
208        );
209        let mut path = sequoia_draw::Path::new();
210        path.move_to(from).line_to(to);
211        let bounds = self.ctx.stroke(&path, &style, color);
212        self.to_local_bounds(bounds)
213    }
214
215    pub fn polyline(&mut self, points: &[Point], stroke: BrushStroke, color: Color) -> Rect {
216        if points.len() < 2 || !stroke.is_visible() || color.a <= 0.0 {
217            return Rect::ZERO;
218        }
219        use sequoia_draw::Path;
220        let style = self.stroke_style(stroke);
221        let mut path = Path::new();
222        let mut it = points.iter();
223
224        path.move_to(self.to_global_point(*it.next().unwrap_or(&Point::ZERO)));
225        for p in it {
226            path.line_to(self.to_global_point(*p));
227        }
228        let bounds = self.ctx.stroke(&path, &style, color);
229        self.to_local_bounds(bounds)
230    }
231
232    pub fn curve(
233        &mut self,
234        p0: Point,
235        c0: Point,
236        c1: Point,
237        p1: Point,
238        stroke: BrushStroke,
239        color: Color,
240    ) -> Rect {
241        let span = (p1.x - p0.x).abs() + (p1.y - p0.y).abs();
242        let n = ((span / 12.0) as usize).clamp(4, 32);
243        let mut pts = Vec::with_capacity(n + 1);
244        for i in 0..=n {
245            let t = i as f32 / n as f32;
246            let u = 1.0 - t;
247            pts.push(Point::new(
248                u * u * u * p0.x
249                    + 3.0 * u * u * t * c0.x
250                    + 3.0 * u * t * t * c1.x
251                    + t * t * t * p1.x,
252                u * u * u * p0.y
253                    + 3.0 * u * u * t * c0.y
254                    + 3.0 * u * t * t * c1.y
255                    + t * t * t * p1.y,
256            ));
257        }
258        self.polyline(&pts, stroke, color)
259    }
260
261    pub fn rect_outline(
262        &mut self,
263        rect: Rect,
264        radius: f32,
265        stroke: BrushStroke,
266        color: Color,
267    ) -> Rect {
268        if rect.is_empty() || !stroke.is_visible() || color.a <= 0.0 {
269            return Rect::ZERO;
270        }
271        use sequoia_draw::Path;
272        let style = self.stroke_style(stroke);
273        let mut path = Path::new();
274        let global = self.to_global_rect(rect);
275        let radius = radius * self.unit_scale();
276        if radius > 0.0 {
277            path.rounded_rect(global, radius);
278        } else {
279            path.rect(global);
280        }
281        let bounds = self.ctx.stroke(&path, &style, color);
282        self.to_local_bounds(bounds)
283    }
284
285    pub fn polygon(&mut self, points: &[Point], color: Color) -> Rect {
286        if points.len() < 3 || color.a <= 0.0 {
287            return Rect::ZERO;
288        }
289        let global: Vec<Point> = points.iter().map(|p| self.to_global_point(*p)).collect();
290        let bounds = self.ctx.polygon(&global, color);
291        self.to_local_bounds(bounds)
292    }
293
294    pub fn ellipse(&mut self, center: Point, rx: f32, ry: f32, color: Color) -> Rect {
295        if !(rx > 0.0) || !(ry > 0.0) || color.a <= 0.0 {
296            return Rect::ZERO;
297        }
298        let kx = self.transform.apply_vec(Vec2::new(1.0, 0.0)).length();
299        let ky = self.transform.apply_vec(Vec2::new(0.0, 1.0)).length();
300        let bounds = self
301            .ctx
302            .ellipse(self.to_global_point(center), rx * kx, ry * ky, color);
303        self.to_local_bounds(bounds)
304    }
305
306    /// Draws a single line with `baseline` as the text baseline.
307    ///
308    /// The font size scales with the active `with_transform` scale so text
309    /// zooms with the canvas instead of staying at a fixed pixel size.
310    pub fn text(&mut self, text: &str, baseline: Point, size: f32, color: Color) {
311        if text.is_empty() || !(size > 0.0) || color.a <= 0.0 {
312            return;
313        }
314        let g = self.to_global_point(baseline);
315        self.ctx
316            .draw_text(text, (g.x, g.y), size * self.unit_scale(), color);
317    }
318
319    /// Draws a single line with `top_left` at the top-left of its line box.
320    ///
321    /// The font size scales with the active `with_transform` scale.
322    pub fn text_tl(&mut self, text: &str, top_left: Point, size: f32, color: Color) {
323        let k = self.unit_scale();
324        if k <= 0.0 {
325            return;
326        }
327        let ascent = self.ctx.text_metrics(size * k).ascent / k;
328        let baseline = Point::new(top_left.x, top_left.y + ascent);
329        self.text(text, baseline, size, color);
330    }
331
332    /// Vertical metrics for the default font at `size`.
333    pub fn text_metrics(&mut self, size: f32) -> crate::TextMetrics {
334        self.ctx.text_metrics(size)
335    }
336
337    pub fn text_tl_rotated(
338        &mut self,
339        text: &str,
340        top_left: Point,
341        size: f32,
342        color: Color,
343        degrees: f32,
344        pivot: Point,
345    ) {
346        let pivot = self.to_global_point(pivot);
347        let t = Transform::rotate_around_degrees(degrees, pivot);
348        self.push_list_transform(t);
349        self.text_tl(text, top_left, size, color);
350        self.pop_list_transform();
351    }
352
353    pub fn text_styled(
354        &mut self,
355        text: &str,
356        baseline: Point,
357        size: f32,
358        weight: FontWeight,
359        color: Color,
360    ) {
361        if text.is_empty() || !(size > 0.0) || color.a <= 0.0 {
362            return;
363        }
364        let g = self.to_global_point(baseline);
365        self.ctx
366            .draw_text_weighted(text, (g.x, g.y), size * self.unit_scale(), weight, color);
367    }
368
369    /// Measures `text` in local coordinates, including the transform scale.
370    pub fn text_width(&mut self, text: &str, size: f32) -> f32 {
371        if text.is_empty() || !(size > 0.0) {
372            return 0.0;
373        }
374        let k = self.unit_scale();
375        if k <= 0.0 {
376            return 0.0;
377        }
378        self.ctx.text_width(text, size * k) / k
379    }
380
381    pub fn text_width_weighted(&mut self, text: &str, size: f32, weight: FontWeight) -> f32 {
382        if text.is_empty() || !(size > 0.0) {
383            return 0.0;
384        }
385        let k = self.unit_scale();
386        if k <= 0.0 {
387            return 0.0;
388        }
389        self.ctx.text_width_weighted(text, size * k, weight) / k
390    }
391
392    pub fn text_center_x(&mut self, text: &str, center_x: f32, top: f32, size: f32, color: Color) {
393        let width = self.text_width(text, size);
394        self.text_tl(text, Point::new(center_x - width * 0.5, top), size, color);
395    }
396
397    pub fn text_center(&mut self, rect: Rect, text: &str, size: f32, color: Color) {
398        let k = self.unit_scale();
399        if k <= 0.0 {
400            return;
401        }
402        let metrics = self.ctx.measure_text(text, size * k);
403        let width = metrics.width / k;
404        let height = metrics.height / k;
405        let top = rect.top() + (rect.size.height - height) * 0.5;
406        self.text_tl(
407            text,
408            Point::new(rect.center().x - width * 0.5, top),
409            size,
410            color,
411        );
412    }
413
414    pub fn image(&mut self, rect: Rect, uv: [f32; 4], source: ImageSource) -> Rect {
415        if rect.is_empty() {
416            return Rect::ZERO;
417        }
418        let global = self.to_global_rect(rect);
419        self.ctx.list.push_image(global, uv, source);
420        rect
421    }
422
423    pub fn pixels(&mut self, rect: Rect, pixels: &Pixels) -> Rect {
424        let full = Rect::from_xywh(0.0, 0.0, pixels.width() as f32, pixels.height() as f32);
425        self.pixels_region_sampled(rect, pixels, full, ImageSampling::Linear)
426    }
427
428    pub fn pixels_nearest(&mut self, rect: Rect, pixels: &Pixels) -> Rect {
429        let full = Rect::from_xywh(0.0, 0.0, pixels.width() as f32, pixels.height() as f32);
430        self.pixels_region_sampled(rect, pixels, full, ImageSampling::Nearest)
431    }
432
433    pub fn pixels_region(&mut self, dst: Rect, pixels: &Pixels, src: Rect) -> Rect {
434        self.pixels_region_sampled(dst, pixels, src, ImageSampling::Linear)
435    }
436
437    fn pixels_region_sampled(
438        &mut self,
439        dst: Rect,
440        pixels: &Pixels,
441        src: Rect,
442        sampling: ImageSampling,
443    ) -> Rect {
444        if dst.is_empty() {
445            return Rect::ZERO;
446        }
447        let Some(uv) = Self::pixel_uv(pixels.width(), pixels.height(), src) else {
448            return Rect::ZERO;
449        };
450        let global = self.to_global_rect(dst);
451        self.ctx
452            .list
453            .push_image_sampled(global, uv, pixels.to_source(), sampling);
454        dst
455    }
456
457    pub fn pixel_uv(width: u32, height: u32, src: Rect) -> Option<[f32; 4]> {
458        if width == 0 || height == 0 {
459            return None;
460        }
461        let (w, h) = (width as f32, height as f32);
462        let x0 = src.left().clamp(0.0, w);
463        let y0 = src.top().clamp(0.0, h);
464        let x1 = src.right().clamp(0.0, w);
465        let y1 = src.bottom().clamp(0.0, h);
466        if x1 <= x0 || y1 <= y0 {
467            return None;
468        }
469        Some([x0 / w, y0 / h, x1 / w, y1 / h])
470    }
471
472    pub fn pixel_at(rect: Rect, width: u32, height: u32, p: Point) -> Option<(i32, i32)> {
473        if width == 0 || height == 0 || rect.is_empty() {
474            return None;
475        }
476        let fx = (p.x - rect.left()) / rect.size.width * width as f32;
477        let fy = (p.y - rect.top()) / rect.size.height * height as f32;
478        if !fx.is_finite() || !fy.is_finite() || fx < 0.0 || fy < 0.0 {
479            return None;
480        }
481        if fx >= width as f32 || fy >= height as f32 {
482            return None;
483        }
484        Some((fx.floor() as i32, fy.floor() as i32))
485    }
486
487    pub fn knob(&mut self, center: Point, radius: f32, fill: Color) -> Rect {
488        if !(radius > 0.0) || !radius.is_finite() {
489            return Rect::ZERO;
490        }
491        let outer = self.circle(center, radius, Fill::solid(Color::WHITE));
492        let inner_r = radius - 2.0;
493        if inner_r > 0.0 {
494            let inner = self.circle(center, inner_r, Fill::solid(fill));
495            outer.union(inner)
496        } else {
497            outer
498        }
499    }
500
501    pub fn sv_field(&mut self, rect: Rect, hue01: f32) -> Rect {
502        if rect.is_empty() {
503            return Rect::ZERO;
504        }
505        let h = hue01.clamp(0.0, 1.0);
506        self.fill(rect, Fill::solid(Color::from_hsva(h, 1.0, 1.0, 1.0)));
507        self.fill(
508            rect,
509            Fill::horizontal(GradientStops::two(
510                Color::WHITE,
511                Color::rgba(1.0, 1.0, 1.0, 0.0),
512            )),
513        );
514        self.fill(
515            rect,
516            Fill::vertical(GradientStops::two(
517                Color::rgba(0.0, 0.0, 0.0, 0.0),
518                Color::BLACK,
519            )),
520        );
521        rect
522    }
523
524    pub fn grid(&mut self, step: f32, color: Color) -> u32 {
525        self.grid_offset(step, Vec2::ZERO, color)
526    }
527
528    pub fn grid_offset(&mut self, step: f32, offset: Vec2, color: Color) -> u32 {
529        if step <= 0.0 || color.a <= 0.0 || self.size.width <= 0.0 || self.size.height <= 0.0 {
530            return 0;
531        }
532        let mut drawn = 0u32;
533        let ox = offset.x.rem_euclid(step);
534        let oy = offset.y.rem_euclid(step);
535        let w = self.size.width;
536        let h = self.size.height;
537        let mut x = ox;
538        while x < w {
539            self.fill(Rect::from_xywh(x, 0.0, 1.0, h), Fill::solid(color));
540            drawn += 1;
541            if drawn > 4096 {
542                break;
543            }
544            x += step;
545        }
546        let mut y = oy;
547        while y < h {
548            self.fill(Rect::from_xywh(0.0, y, w, 1.0), Fill::solid(color));
549            drawn += 1;
550            if drawn > 8192 {
551                break;
552            }
553            y += step;
554        }
555        drawn
556    }
557
558    pub fn axes(&mut self, origin: Point, color: Color, width: f32) -> Rect {
559        if !(width > 0.0) || color.a <= 0.0 {
560            return Rect::ZERO;
561        }
562        let stroke = BrushStroke::new(width);
563        let w = self.size.width;
564        let h = self.size.height;
565        let a = self.line(
566            Point::new(0.0, origin.y),
567            Point::new(w, origin.y),
568            stroke,
569            color,
570        );
571        let b = self.line(
572            Point::new(origin.x, 0.0),
573            Point::new(origin.x, h),
574            stroke,
575            color,
576        );
577        a.union(b)
578    }
579
580    pub fn frame(&mut self, rect: Rect, color: Color, width: f32) -> Rect {
581        self.rect_outline(rect, 0.0, BrushStroke::new(width), color)
582    }
583
584    pub fn cross(&mut self, center: Point, half: f32, width: f32, color: Color) -> Rect {
585        if !(half > 0.0) || color.a <= 0.0 {
586            return Rect::ZERO;
587        }
588        let s = BrushStroke::round(width);
589        let a = self.line(
590            Point::new(center.x - half, center.y),
591            Point::new(center.x + half, center.y),
592            s,
593            color,
594        );
595        let b = self.line(
596            Point::new(center.x, center.y - half),
597            Point::new(center.x, center.y + half),
598            s,
599            color,
600        );
601        a.union(b)
602    }
603
604    pub fn marquee(&mut self, a: Point, b: Point, fill: Fill, outline: Color) -> Rect {
605        let rect = Rect::from_xywh(
606            a.x.min(b.x),
607            a.y.min(b.y),
608            (a.x - b.x).abs(),
609            (a.y - b.y).abs(),
610        );
611        if rect.is_empty() {
612            return Rect::ZERO;
613        }
614        self.fill(rect, fill);
615        self.rect_outline(rect, 0.0, BrushStroke::new(1.0), outline);
616        rect
617    }
618
619    pub fn ellipse_outline(
620        &mut self,
621        center: Point,
622        rx: f32,
623        ry: f32,
624        stroke: BrushStroke,
625        color: Color,
626    ) -> Rect {
627        if !(rx > 0.0) || !(ry > 0.0) || !stroke.is_visible() || color.a <= 0.0 {
628            return Rect::ZERO;
629        }
630        use sequoia_draw::Path;
631        let style = self.stroke_style(stroke);
632        let kx = self.transform.apply_vec(Vec2::new(1.0, 0.0)).length();
633        let ky = self.transform.apply_vec(Vec2::new(0.0, 1.0)).length();
634        let mut path = Path::new();
635        path.ellipse(self.to_global_point(center), rx * kx, ry * ky);
636        let bounds = self.ctx.stroke(&path, &style, color);
637        self.to_local_bounds(bounds)
638    }
639
640    pub fn capsule(&mut self, from: Point, to: Point, radius: f32, color: Color) -> Rect {
641        if !(radius > 0.0) || color.a <= 0.0 {
642            return Rect::ZERO;
643        }
644        let bounds = self.ctx.capsule(
645            self.to_global_point(from),
646            self.to_global_point(to),
647            radius,
648            color,
649        );
650        self.to_local_bounds(bounds)
651    }
652
653    pub fn arrow(
654        &mut self,
655        from: Point,
656        to: Point,
657        width: f32,
658        head_len: f32,
659        head_width: f32,
660        color: Color,
661    ) -> Rect {
662        if color.a <= 0.0 {
663            return Rect::ZERO;
664        }
665        let bounds = self.ctx.arrow(
666            self.to_global_point(from),
667            self.to_global_point(to),
668            width,
669            head_len,
670            head_width,
671            color,
672        );
673        self.to_local_bounds(bounds)
674    }
675
676    pub fn x_marker(&mut self, center: Point, half: f32, width: f32, color: Color) -> Rect {
677        if !(half > 0.0) || color.a <= 0.0 {
678            return Rect::ZERO;
679        }
680        let bounds = self
681            .ctx
682            .x_marker(self.to_global_point(center), half, width, color);
683        self.to_local_bounds(bounds)
684    }
685
686    pub fn regular_polygon(
687        &mut self,
688        center: Point,
689        radius: f32,
690        sides: u32,
691        rotation_deg: f32,
692        color: Color,
693    ) -> Rect {
694        if !(radius > 0.0) || sides < 3 || color.a <= 0.0 {
695            return Rect::ZERO;
696        }
697        let bounds = self.ctx.regular_polygon(
698            self.to_global_point(center),
699            radius,
700            sides,
701            rotation_deg,
702            color,
703        );
704        self.to_local_bounds(bounds)
705    }
706
707    pub fn star(
708        &mut self,
709        center: Point,
710        outer_r: f32,
711        inner_r: f32,
712        points: u32,
713        rotation_deg: f32,
714        color: Color,
715    ) -> Rect {
716        if !(outer_r > 0.0) || !(inner_r > 0.0) || points < 2 || color.a <= 0.0 {
717            return Rect::ZERO;
718        }
719        let bounds = self.ctx.star(
720            self.to_global_point(center),
721            outer_r,
722            inner_r,
723            points,
724            rotation_deg,
725            color,
726        );
727        self.to_local_bounds(bounds)
728    }
729
730    pub fn dots(&mut self, step: f32, offset: Vec2, radius: f32, color: Color) -> u32 {
731        if step <= 0.0 || !(radius > 0.0) || color.a <= 0.0 {
732            return 0;
733        }
734        if self.size.width <= 0.0 || self.size.height <= 0.0 {
735            return 0;
736        }
737        let mut n = 0u32;
738        let ox = offset.x.rem_euclid(step);
739        let oy = offset.y.rem_euclid(step);
740        let mut y = oy;
741        while y < self.size.height {
742            let mut x = ox;
743            while x < self.size.width {
744                self.circle(Point::new(x, y), radius, Fill::solid(color));
745                n += 1;
746                if n > 16384 {
747                    return n;
748                }
749                x += step;
750            }
751            y += step;
752        }
753        n
754    }
755
756    pub fn grid_split(
757        &mut self,
758        step: f32,
759        every: u32,
760        offset: Vec2,
761        minor: Color,
762        major: Color,
763    ) -> u32 {
764        if step <= 0.0 || every == 0 {
765            return 0;
766        }
767        if self.size.width <= 0.0 || self.size.height <= 0.0 {
768            return 0;
769        }
770        let every = every.max(1);
771        let mut drawn = 0u32;
772        let ox = offset.x.rem_euclid(step);
773        let oy = offset.y.rem_euclid(step);
774        let w = self.size.width;
775        let h = self.size.height;
776        let mut i = 0u32;
777        let mut x = ox;
778        while x < w {
779            let c = if i % every == every - 1 { major } else { minor };
780            self.fill(Rect::from_xywh(x, 0.0, 1.0, h), Fill::solid(c));
781            drawn += 1;
782            if drawn > 4096 {
783                break;
784            }
785            x += step;
786            i += 1;
787        }
788        let mut j = 0u32;
789        let mut y = oy;
790        while y < h {
791            let c = if j % every == every - 1 { major } else { minor };
792            self.fill(Rect::from_xywh(0.0, y, w, 1.0), Fill::solid(c));
793            drawn += 1;
794            if drawn > 8192 {
795                break;
796            }
797            y += step;
798            j += 1;
799        }
800        drawn
801    }
802
803    pub fn clipped<R>(&mut self, rect: Rect, f: impl FnOnce(&mut Self) -> R) -> R {
804        let global = self.to_global_rect(rect);
805        self.ctx.list.push_clip(global);
806        let r = f(self);
807        self.ctx.list.pop_clip();
808        r
809    }
810
811    pub fn clipped_rounded<R>(
812        &mut self,
813        rect: Rect,
814        radius: f32,
815        f: impl FnOnce(&mut Self) -> R,
816    ) -> R {
817        let global = self.to_global_rect(rect);
818        self.ctx.push_rounded_clip(global, radius);
819        let r = f(self);
820        self.ctx.pop_clip();
821        r
822    }
823
824    fn push_gradient(
825        &mut self,
826        rect: Rect,
827        radius: f32,
828        kind: GradientKind,
829        stops: GradientStops,
830    ) -> Rect {
831        if stops.len() <= sequoia_core::MAX_GRADIENT_STOPS {
832            let global = self.to_global_rect(rect);
833            self.ctx.list.push_gradient(global, radius, kind, stops);
834            return rect;
835        }
836
837        self.push_gradient_bands(rect, radius, kind, stops)
838    }
839
840    fn push_gradient_bands(
841        &mut self,
842        rect: Rect,
843        shape_radius: f32,
844        kind: GradientKind,
845        stops: GradientStops,
846    ) -> Rect {
847        const RADIAL_BANDS: usize = 64;
848        let vertical = match kind {
849            GradientKind::Linear { angle_deg } => {
850                let (dx, dy) = GradientKind::Linear { angle_deg }.direction();
851                dy.abs() >= dx.abs()
852            }
853            GradientKind::Radial { .. } => true,
854        };
855        let global = self.to_global_rect(rect);
856        let mut bounds = Rect::ZERO;
857        match kind {
858            GradientKind::Radial { radius } => {
859                let center = Point::new(
860                    global.left() + global.size.width * 0.5,
861                    global.top() + global.size.height * 0.5,
862                );
863                let extent = if radius > 0.0 {
864                    radius
865                } else {
866                    global.size.width.hypot(global.size.height) * 0.5
867                };
868                if extent <= 0.0 {
869                    return Rect::ZERO;
870                }
871                if shape_radius > 0.0 {
872                    self.ctx.push_rounded_clip(global, shape_radius);
873                } else {
874                    self.ctx.list.push_clip(global);
875                }
876                for i in (0..RADIAL_BANDS).rev() {
877                    let t0 = i as f32 / RADIAL_BANDS as f32;
878                    let t1 = (i + 1) as f32 / RADIAL_BANDS as f32;
879                    let [r, g, b, a] = stops.sample_linear((t0 + t1) * 0.5);
880                    let color = Color { r, g, b, a }.from_linear();
881                    let r1 = extent * t1;
882                    let band = Rect::from_xywh(center.x - r1, center.y - r1, r1 * 2.0, r1 * 2.0);
883                    self.ctx.list.push_quad(band, color, r1);
884                }
885                self.ctx.pop_clip();
886                bounds = global;
887            }
888            GradientKind::Linear { .. } => {
889                let extent = if vertical {
890                    global.size.height
891                } else {
892                    global.size.width
893                };
894                let bands = (extent.ceil() as usize).clamp(1, 256);
895                let scale = {
896                    let s = self.ctx.scale_factor();
897                    if s.is_finite() && s > 0.0 { s } else { 1.0 }
898                };
899                let origin = if vertical {
900                    global.top()
901                } else {
902                    global.left()
903                };
904                let edges =
905                    crate::paint_draw::band_edges(origin, extent, bands, 1.0 / scale, 0.5 / scale);
906                for (i, (lo, hi)) in edges.into_iter().enumerate() {
907                    let t = (i as f32 + 0.5) / bands as f32;
908                    let [r, g, b, a] = stops.sample_linear(t);
909                    let color = Color { r, g, b, a }.from_linear();
910                    let band = if vertical {
911                        Rect::from_xywh(global.left(), lo, global.size.width, hi - lo)
912                    } else {
913                        Rect::from_xywh(lo, global.top(), hi - lo, global.size.height)
914                    };
915                    self.ctx.list.push_quad(band, color, 0.0);
916                    bounds = bounds.union(band);
917                }
918            }
919        }
920        if bounds.is_empty() {
921            return Rect::ZERO;
922        }
923
924        Rect::from_xywh(
925            bounds.left() - self.origin.x,
926            bounds.top() - self.origin.y,
927            bounds.size.width,
928            bounds.size.height,
929        )
930    }
931
932    fn to_local_bounds(&self, bounds: Rect) -> Rect {
933        if bounds.is_empty() {
934            return Rect::ZERO;
935        }
936        let r = Rect::from_xywh(
937            bounds.left() - self.origin.x,
938            bounds.top() - self.origin.y,
939            bounds.size.width,
940            bounds.size.height,
941        );
942
943        match self.transform.inverse() {
944            Some(inv) => inv.apply_rect_aabb(r),
945            None => Rect::ZERO,
946        }
947    }
948}