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_styled(
304        &mut self,
305        text: &str,
306        baseline: Point,
307        size: f32,
308        weight: FontWeight,
309        color: Color,
310    ) {
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_weighted(text, (g.x, g.y), size, weight, color);
317    }
318
319    pub fn text_width(&mut self, text: &str, size: f32) -> f32 {
320        if text.is_empty() || !(size > 0.0) {
321            return 0.0;
322        }
323        self.ctx.text_width(text, size)
324    }
325
326    pub fn text_width_weighted(&mut self, text: &str, size: f32, weight: FontWeight) -> f32 {
327        if text.is_empty() || !(size > 0.0) {
328            return 0.0;
329        }
330        self.ctx.text_width_weighted(text, size, weight)
331    }
332
333    pub fn image(&mut self, rect: Rect, uv: [f32; 4], source: ImageSource) -> Rect {
334        if rect.is_empty() {
335            return Rect::ZERO;
336        }
337        let global = self.to_global_rect(rect);
338        self.ctx.list.push_image(global, uv, source);
339        rect
340    }
341
342    pub fn pixels(&mut self, rect: Rect, pixels: &Pixels, id: u64, version: u64) -> Rect {
343        let full = Rect::from_xywh(0.0, 0.0, pixels.width() as f32, pixels.height() as f32);
344        self.pixels_region(rect, pixels, id, version, full)
345    }
346
347    pub fn pixels_region(
348        &mut self,
349        dst: Rect,
350        pixels: &Pixels,
351        id: u64,
352        version: u64,
353        src: Rect,
354    ) -> Rect {
355        if dst.is_empty() {
356            return Rect::ZERO;
357        }
358        let Some(uv) = Self::pixel_uv(pixels.width(), pixels.height(), src) else {
359            return Rect::ZERO;
360        };
361        let global = self.to_global_rect(dst);
362        self.ctx
363            .list
364            .push_image(global, uv, pixels.to_source(id, version));
365        dst
366    }
367
368    pub fn pixel_uv(width: u32, height: u32, src: Rect) -> Option<[f32; 4]> {
369        if width == 0 || height == 0 {
370            return None;
371        }
372        let (w, h) = (width as f32, height as f32);
373        let x0 = src.left().clamp(0.0, w);
374        let y0 = src.top().clamp(0.0, h);
375        let x1 = src.right().clamp(0.0, w);
376        let y1 = src.bottom().clamp(0.0, h);
377        if x1 <= x0 || y1 <= y0 {
378            return None;
379        }
380        Some([x0 / w, y0 / h, x1 / w, y1 / h])
381    }
382
383    pub fn pixel_at(rect: Rect, width: u32, height: u32, p: Point) -> Option<(i32, i32)> {
384        if width == 0 || height == 0 || rect.is_empty() {
385            return None;
386        }
387        let fx = (p.x - rect.left()) / rect.size.width * width as f32;
388        let fy = (p.y - rect.top()) / rect.size.height * height as f32;
389        if !fx.is_finite() || !fy.is_finite() || fx < 0.0 || fy < 0.0 {
390            return None;
391        }
392        if fx >= width as f32 || fy >= height as f32 {
393            return None;
394        }
395        Some((fx.floor() as i32, fy.floor() as i32))
396    }
397
398    pub fn knob(&mut self, center: Point, radius: f32, fill: Color) -> Rect {
399        if !(radius > 0.0) || !radius.is_finite() {
400            return Rect::ZERO;
401        }
402        let outer = self.circle(center, radius, Fill::solid(Color::WHITE));
403        let inner_r = radius - 2.0;
404        if inner_r > 0.0 {
405            let inner = self.circle(center, inner_r, Fill::solid(fill));
406            outer.union(inner)
407        } else {
408            outer
409        }
410    }
411
412    pub fn sv_field(&mut self, rect: Rect, hue01: f32) -> Rect {
413        if rect.is_empty() {
414            return Rect::ZERO;
415        }
416        let h = hue01.clamp(0.0, 1.0);
417        self.fill(rect, Fill::solid(Color::from_hsva(h, 1.0, 1.0, 1.0)));
418        self.fill(
419            rect,
420            Fill::horizontal(GradientStops::two(
421                Color::WHITE,
422                Color::rgba(1.0, 1.0, 1.0, 0.0),
423            )),
424        );
425        self.fill(
426            rect,
427            Fill::vertical(GradientStops::two(
428                Color::rgba(0.0, 0.0, 0.0, 0.0),
429                Color::BLACK,
430            )),
431        );
432        rect
433    }
434
435    pub fn grid(&mut self, step: f32, color: Color) -> u32 {
436        self.grid_offset(step, Vec2::ZERO, color)
437    }
438
439    pub fn grid_offset(&mut self, step: f32, offset: Vec2, color: Color) -> u32 {
440        if step <= 0.0 || color.a <= 0.0 || self.size.width <= 0.0 || self.size.height <= 0.0 {
441            return 0;
442        }
443        let mut drawn = 0u32;
444        let ox = offset.x.rem_euclid(step);
445        let oy = offset.y.rem_euclid(step);
446        let w = self.size.width;
447        let h = self.size.height;
448        let mut x = ox;
449        while x < w {
450            self.fill(Rect::from_xywh(x, 0.0, 1.0, h), Fill::solid(color));
451            drawn += 1;
452            if drawn > 4096 {
453                break;
454            }
455            x += step;
456        }
457        let mut y = oy;
458        while y < h {
459            self.fill(Rect::from_xywh(0.0, y, w, 1.0), Fill::solid(color));
460            drawn += 1;
461            if drawn > 8192 {
462                break;
463            }
464            y += step;
465        }
466        drawn
467    }
468
469    pub fn axes(&mut self, origin: Point, color: Color, width: f32) -> Rect {
470        if !(width > 0.0) || color.a <= 0.0 {
471            return Rect::ZERO;
472        }
473        let stroke = BrushStroke::new(width);
474        let w = self.size.width;
475        let h = self.size.height;
476        let a = self.line(
477            Point::new(0.0, origin.y),
478            Point::new(w, origin.y),
479            stroke,
480            color,
481        );
482        let b = self.line(
483            Point::new(origin.x, 0.0),
484            Point::new(origin.x, h),
485            stroke,
486            color,
487        );
488        a.union(b)
489    }
490
491    pub fn frame(&mut self, rect: Rect, color: Color, width: f32) -> Rect {
492        self.rect_outline(rect, 0.0, BrushStroke::new(width), color)
493    }
494
495    pub fn cross(&mut self, center: Point, half: f32, width: f32, color: Color) -> Rect {
496        if !(half > 0.0) || color.a <= 0.0 {
497            return Rect::ZERO;
498        }
499        let s = BrushStroke::round(width);
500        let a = self.line(
501            Point::new(center.x - half, center.y),
502            Point::new(center.x + half, center.y),
503            s,
504            color,
505        );
506        let b = self.line(
507            Point::new(center.x, center.y - half),
508            Point::new(center.x, center.y + half),
509            s,
510            color,
511        );
512        a.union(b)
513    }
514
515    pub fn marquee(&mut self, a: Point, b: Point, fill: Fill, outline: Color) -> Rect {
516        let rect = Rect::from_xywh(
517            a.x.min(b.x),
518            a.y.min(b.y),
519            (a.x - b.x).abs(),
520            (a.y - b.y).abs(),
521        );
522        if rect.is_empty() {
523            return Rect::ZERO;
524        }
525        self.fill(rect, fill);
526        self.rect_outline(rect, 0.0, BrushStroke::new(1.0), outline);
527        rect
528    }
529
530    pub fn ellipse_outline(
531        &mut self,
532        center: Point,
533        rx: f32,
534        ry: f32,
535        stroke: BrushStroke,
536        color: Color,
537    ) -> Rect {
538        if !(rx > 0.0) || !(ry > 0.0) || !stroke.is_visible() || color.a <= 0.0 {
539            return Rect::ZERO;
540        }
541        use sequoia_draw::Path;
542        let style = self.stroke_style(stroke);
543        let kx = self.transform.apply_vec(Vec2::new(1.0, 0.0)).length();
544        let ky = self.transform.apply_vec(Vec2::new(0.0, 1.0)).length();
545        let mut path = Path::new();
546        path.ellipse(self.to_global_point(center), rx * kx, ry * ky);
547        let bounds = self.ctx.stroke(&path, &style, color);
548        self.to_local_bounds(bounds)
549    }
550
551    pub fn capsule(&mut self, from: Point, to: Point, radius: f32, color: Color) -> Rect {
552        if !(radius > 0.0) || color.a <= 0.0 {
553            return Rect::ZERO;
554        }
555        let bounds = self.ctx.capsule(
556            self.to_global_point(from),
557            self.to_global_point(to),
558            radius,
559            color,
560        );
561        self.to_local_bounds(bounds)
562    }
563
564    pub fn arrow(
565        &mut self,
566        from: Point,
567        to: Point,
568        width: f32,
569        head_len: f32,
570        head_width: f32,
571        color: Color,
572    ) -> Rect {
573        if color.a <= 0.0 {
574            return Rect::ZERO;
575        }
576        let bounds = self.ctx.arrow(
577            self.to_global_point(from),
578            self.to_global_point(to),
579            width,
580            head_len,
581            head_width,
582            color,
583        );
584        self.to_local_bounds(bounds)
585    }
586
587    pub fn x_marker(&mut self, center: Point, half: f32, width: f32, color: Color) -> Rect {
588        if !(half > 0.0) || color.a <= 0.0 {
589            return Rect::ZERO;
590        }
591        let bounds = self
592            .ctx
593            .x_marker(self.to_global_point(center), half, width, color);
594        self.to_local_bounds(bounds)
595    }
596
597    pub fn regular_polygon(
598        &mut self,
599        center: Point,
600        radius: f32,
601        sides: u32,
602        rotation_deg: f32,
603        color: Color,
604    ) -> Rect {
605        if !(radius > 0.0) || sides < 3 || color.a <= 0.0 {
606            return Rect::ZERO;
607        }
608        let bounds = self.ctx.regular_polygon(
609            self.to_global_point(center),
610            radius,
611            sides,
612            rotation_deg,
613            color,
614        );
615        self.to_local_bounds(bounds)
616    }
617
618    pub fn star(
619        &mut self,
620        center: Point,
621        outer_r: f32,
622        inner_r: f32,
623        points: u32,
624        rotation_deg: f32,
625        color: Color,
626    ) -> Rect {
627        if !(outer_r > 0.0) || !(inner_r > 0.0) || points < 2 || color.a <= 0.0 {
628            return Rect::ZERO;
629        }
630        let bounds = self.ctx.star(
631            self.to_global_point(center),
632            outer_r,
633            inner_r,
634            points,
635            rotation_deg,
636            color,
637        );
638        self.to_local_bounds(bounds)
639    }
640
641    pub fn dots(&mut self, step: f32, offset: Vec2, radius: f32, color: Color) -> u32 {
642        if step <= 0.0 || !(radius > 0.0) || color.a <= 0.0 {
643            return 0;
644        }
645        if self.size.width <= 0.0 || self.size.height <= 0.0 {
646            return 0;
647        }
648        let mut n = 0u32;
649        let ox = offset.x.rem_euclid(step);
650        let oy = offset.y.rem_euclid(step);
651        let mut y = oy;
652        while y < self.size.height {
653            let mut x = ox;
654            while x < self.size.width {
655                self.circle(Point::new(x, y), radius, Fill::solid(color));
656                n += 1;
657                if n > 16384 {
658                    return n;
659                }
660                x += step;
661            }
662            y += step;
663        }
664        n
665    }
666
667    pub fn grid_split(
668        &mut self,
669        step: f32,
670        every: u32,
671        offset: Vec2,
672        minor: Color,
673        major: Color,
674    ) -> u32 {
675        if step <= 0.0 || every == 0 {
676            return 0;
677        }
678        if self.size.width <= 0.0 || self.size.height <= 0.0 {
679            return 0;
680        }
681        let every = every.max(1);
682        let mut drawn = 0u32;
683        let ox = offset.x.rem_euclid(step);
684        let oy = offset.y.rem_euclid(step);
685        let w = self.size.width;
686        let h = self.size.height;
687        let mut i = 0u32;
688        let mut x = ox;
689        while x < w {
690            let c = if i % every == every - 1 { major } else { minor };
691            self.fill(Rect::from_xywh(x, 0.0, 1.0, h), Fill::solid(c));
692            drawn += 1;
693            if drawn > 4096 {
694                break;
695            }
696            x += step;
697            i += 1;
698        }
699        let mut j = 0u32;
700        let mut y = oy;
701        while y < h {
702            let c = if j % every == every - 1 { major } else { minor };
703            self.fill(Rect::from_xywh(0.0, y, w, 1.0), Fill::solid(c));
704            drawn += 1;
705            if drawn > 8192 {
706                break;
707            }
708            y += step;
709            j += 1;
710        }
711        drawn
712    }
713
714    pub fn clipped<R>(&mut self, rect: Rect, f: impl FnOnce(&mut Self) -> R) -> R {
715        let global = self.to_global_rect(rect);
716        self.ctx.list.push_clip(global);
717        let r = f(self);
718        self.ctx.list.pop_clip();
719        r
720    }
721
722    pub fn clipped_rounded<R>(
723        &mut self,
724        rect: Rect,
725        radius: f32,
726        f: impl FnOnce(&mut Self) -> R,
727    ) -> R {
728        let global = self.to_global_rect(rect);
729        self.ctx.push_rounded_clip(global, radius);
730        let r = f(self);
731        self.ctx.pop_clip();
732        r
733    }
734
735    fn push_gradient(
736        &mut self,
737        rect: Rect,
738        radius: f32,
739        kind: GradientKind,
740        stops: GradientStops,
741    ) -> Rect {
742        if stops.len() <= sequoia_core::MAX_GRADIENT_STOPS {
743            let global = self.to_global_rect(rect);
744            self.ctx.list.push_gradient(global, radius, kind, stops);
745            return rect;
746        }
747
748        self.push_gradient_bands(rect, kind, stops)
749    }
750
751    fn push_gradient_bands(
752        &mut self,
753        rect: Rect,
754        kind: GradientKind,
755        stops: GradientStops,
756    ) -> Rect {
757        const BANDS: usize = 48;
758        let vertical = match kind {
759            GradientKind::Linear { angle_deg } => {
760                let (dx, dy) = GradientKind::Linear { angle_deg }.direction();
761                dy.abs() >= dx.abs()
762            }
763            GradientKind::Radial { .. } => true,
764        };
765        let global = self.to_global_rect(rect);
766        let mut bounds = Rect::ZERO;
767        match kind {
768            GradientKind::Radial { .. } => {
769                for i in (0..BANDS).rev() {
770                    let t0 = i as f32 / BANDS as f32;
771                    let t1 = (i + 1) as f32 / BANDS as f32;
772                    let [r, g, b, a] = stops.sample_linear((t0 + t1) * 0.5);
773                    let color = Color { r, g, b, a }.from_linear();
774                    let w = global.size.width * (t1 - t0);
775                    let cx = global.left() + global.size.width * t1 - w * 0.5;
776                    let cy = global.top() + global.size.height * 0.5;
777                    let h = global.size.height * (t1 - t0);
778                    let band = Rect::from_xywh(cx - w * 0.5, cy - h * 0.5, w, h);
779                    self.ctx.list.push_quad(band, color, 0.0);
780                    bounds = bounds.union(band);
781                }
782            }
783            GradientKind::Linear { .. } => {
784                let extent = if vertical {
785                    global.size.height
786                } else {
787                    global.size.width
788                };
789                let bands = (extent.ceil() as usize).clamp(1, 256);
790                let scale = {
791                    let s = self.ctx.scale_factor();
792                    if s.is_finite() && s > 0.0 { s } else { 1.0 }
793                };
794                let origin = if vertical {
795                    global.top()
796                } else {
797                    global.left()
798                };
799                let edges =
800                    crate::paint_draw::band_edges(origin, extent, bands, 1.0 / scale, 0.5 / scale);
801                for (i, (lo, hi)) in edges.into_iter().enumerate() {
802                    let t = (i as f32 + 0.5) / bands as f32;
803                    let [r, g, b, a] = stops.sample_linear(t);
804                    let color = Color { r, g, b, a }.from_linear();
805                    let band = if vertical {
806                        Rect::from_xywh(global.left(), lo, global.size.width, hi - lo)
807                    } else {
808                        Rect::from_xywh(lo, global.top(), hi - lo, global.size.height)
809                    };
810                    self.ctx.list.push_quad(band, color, 0.0);
811                    bounds = bounds.union(band);
812                }
813            }
814        }
815        if bounds.is_empty() {
816            return Rect::ZERO;
817        }
818
819        Rect::from_xywh(
820            bounds.left() - self.origin.x,
821            bounds.top() - self.origin.y,
822            bounds.size.width,
823            bounds.size.height,
824        )
825    }
826
827    fn to_local_bounds(&self, bounds: Rect) -> Rect {
828        if bounds.is_empty() {
829            return Rect::ZERO;
830        }
831        let r = Rect::from_xywh(
832            bounds.left() - self.origin.x,
833            bounds.top() - self.origin.y,
834            bounds.size.width,
835            bounds.size.height,
836        );
837
838        match self.transform.inverse() {
839            Some(inv) => inv.apply_rect_aabb(r),
840            None => Rect::ZERO,
841        }
842    }
843}