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}