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