Skip to main content

sequoia_view/
painter.rs

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