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