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 current_blend: sequoia_core::BlendMode,
20}
21
22impl<'a, 'b> Painter<'a, 'b> {
23 pub fn new(ctx: &'a mut PaintCtx<'b>) -> Self {
24 debug_assert!(
25 ctx.draw_offset.x == 0.0 && ctx.draw_offset.y == 0.0,
26 "Painter not using draw_offset - remove translate/set_draw_offset"
27 );
28 let origin = ctx.rect.origin;
29 let size = ctx.rect.size;
30 Self {
31 ctx,
32 origin,
33 size,
34 transform: Transform::IDENTITY,
35 current_blend: sequoia_core::BlendMode::Normal,
36 }
37 }
38
39 pub fn size(&self) -> Size {
40 self.size
41 }
42
43 pub fn rect(&self) -> Rect {
44 Rect::new(Point::ZERO, self.size)
45 }
46
47 pub fn origin_global(&self) -> Point {
48 self.origin
49 }
50
51 pub fn ctx(&mut self) -> &mut PaintCtx<'b> {
52 self.ctx
53 }
54
55 pub fn push_list_transform(&mut self, t: Transform) {
56 self.ctx.list.push_transform(t);
57 }
58
59 pub fn pop_list_transform(&mut self) {
60 self.ctx.list.pop_transform();
61 }
62
63 pub fn with_transform<R>(&mut self, t: Transform, f: impl FnOnce(&mut Self) -> R) -> R {
64 let prev = self.transform;
65 self.transform = t.then(prev);
66 let r = f(self);
67 self.transform = prev;
68 r
69 }
70
71 pub fn unproject(&self, p: Point) -> Option<Point> {
72 let inv = self.transform.inverse()?;
73 Some(inv.apply(Point::new(p.x - self.origin.x, p.y - self.origin.y)))
74 }
75
76 fn unit_scale(&self) -> f32 {
77 self.transform.apply_vec(Vec2::new(1.0, 0.0)).length()
78 }
79
80 fn stroke_scale(&self, stroke: BrushStroke) -> f32 {
81 match stroke.space {
82 StrokeSpace::Local => self.unit_scale(),
83 StrokeSpace::Screen => 1.0,
84 _ => self.unit_scale(),
85 }
86 }
87
88 fn stroke_style(&self, stroke: BrushStroke) -> sequoia_draw::StrokeStyle {
89 use sequoia_draw::StrokeStyle;
90 let w = (stroke.width * self.stroke_scale(stroke)).max(0.0);
91 if stroke.rounded {
92 StrokeStyle::round(w)
93 } else {
94 StrokeStyle::new(w)
95 }
96 }
97
98 pub fn to_global_rect(&self, r: Rect) -> Rect {
99 if r.is_empty() {
100 return r;
101 }
102 let r = self.transform.apply_rect_aabb(r);
103 Rect::from_xywh(
104 r.left() + self.origin.x,
105 r.top() + self.origin.y,
106 r.size.width,
107 r.size.height,
108 )
109 }
110
111 pub fn to_global_point(&self, p: Point) -> Point {
112 let p = self.transform.apply(p);
113 Point::new(p.x + self.origin.x, p.y + self.origin.y)
114 }
115
116 pub fn to_local_point(&self, p: Point) -> Point {
117 Point::new(p.x - self.origin.x, p.y - self.origin.y)
118 }
119
120 pub fn fill(&mut self, rect: Rect, fill: Fill) -> Rect {
121 self.rounded(rect, 0.0, fill)
122 }
123
124 pub fn rounded(&mut self, rect: Rect, radius: f32, fill: Fill) -> Rect {
125 if rect.is_empty() || !fill.is_visible() {
126 return Rect::ZERO;
127 }
128 let global = self.to_global_rect(rect);
129
130 let radius = radius * self.unit_scale();
131 let radius = if radius.is_finite() {
132 radius
133 .clamp(0.0, global.size.width.min(global.size.height) * 0.5)
134 .max(0.0)
135 } else {
136 0.0
137 };
138 match fill {
139 Fill::Solid(color) => {
140 if color.a <= 0.0 {
141 return Rect::ZERO;
142 }
143 self.ctx.list.push_quad(global, color, radius);
144 rect
145 }
146 Fill::Gradient { kind, stops } => self.push_gradient(rect, radius, kind, stops),
147 }
148 }
149
150 pub fn circle(&mut self, center: Point, radius: f32, fill: Fill) -> Rect {
151 if !(radius > 0.0) || !radius.is_finite() || !fill.is_visible() {
152 return Rect::ZERO;
153 }
154 let rect = Rect::from_xywh(
155 center.x - radius,
156 center.y - radius,
157 radius * 2.0,
158 radius * 2.0,
159 );
160
161 let gr = radius * self.unit_scale();
162 if !(gr > 0.0) {
163 return Rect::ZERO;
164 }
165 match fill {
166 Fill::Solid(color) => {
167 if color.a <= 0.0 {
168 return Rect::ZERO;
169 }
170 let global = self.to_global_rect(rect);
171 self.ctx.list.push_quad(global, color, gr);
172 rect
173 }
174 Fill::Gradient { kind, stops } => {
175 let kind = match kind {
176 GradientKind::Radial { radius: r } if r <= 0.0 => {
177 GradientKind::Radial { radius: gr }
178 }
179 other => other,
180 };
181 self.push_gradient(rect, gr, kind, stops)
182 }
183 }
184 }
185
186 pub fn ring(&mut self, center: Point, radius: f32, width: f32, color: Color) -> Rect {
187 if !(radius > 0.0) || !(width > 0.0) || !width.is_finite() || color.a <= 0.0 {
188 return Rect::ZERO;
189 }
190 let k = self.unit_scale();
191 let mut path = sequoia_draw::Path::new();
192 path.circle(self.to_global_point(center), radius * k);
193 let bounds = self
194 .ctx
195 .stroke(&path, &self.stroke_style(BrushStroke::new(width)), color);
196 self.to_local_bounds(bounds)
197 }
198
199 pub fn line(&mut self, from: Point, to: Point, stroke: BrushStroke, color: Color) -> Rect {
200 if !stroke.is_visible() || color.a <= 0.0 {
201 return Rect::ZERO;
202 }
203 let style = self.stroke_style(stroke);
204 let from = self.to_global_point(from);
205 let to = self.to_global_point(to);
206 let (from, to) = crisp_axis_line(
207 from,
208 to,
209 stroke.width * self.stroke_scale(stroke),
210 self.ctx.scale_factor(),
211 );
212 let mut path = sequoia_draw::Path::new();
213 path.move_to(from).line_to(to);
214 let bounds = self.ctx.stroke(&path, &style, color);
215 self.to_local_bounds(bounds)
216 }
217
218 pub fn polyline(&mut self, points: &[Point], stroke: BrushStroke, color: Color) -> Rect {
219 if points.len() < 2 || !stroke.is_visible() || color.a <= 0.0 {
220 return Rect::ZERO;
221 }
222 use sequoia_draw::Path;
223 let style = self.stroke_style(stroke);
224 let mut path = Path::new();
225 let mut it = points.iter();
226
227 path.move_to(self.to_global_point(*it.next().unwrap_or(&Point::ZERO)));
228 for p in it {
229 path.line_to(self.to_global_point(*p));
230 }
231 let bounds = self.ctx.stroke(&path, &style, color);
232 self.to_local_bounds(bounds)
233 }
234
235 pub fn curve(
236 &mut self,
237 p0: Point,
238 c0: Point,
239 c1: Point,
240 p1: Point,
241 stroke: BrushStroke,
242 color: Color,
243 ) -> Rect {
244 let span = (p1.x - p0.x).abs() + (p1.y - p0.y).abs();
245 let n = ((span / 12.0) as usize).clamp(4, 32);
246 let mut pts = Vec::with_capacity(n + 1);
247 for i in 0..=n {
248 let t = i as f32 / n as f32;
249 let u = 1.0 - t;
250 pts.push(Point::new(
251 u * u * u * p0.x
252 + 3.0 * u * u * t * c0.x
253 + 3.0 * u * t * t * c1.x
254 + t * t * t * p1.x,
255 u * u * u * p0.y
256 + 3.0 * u * u * t * c0.y
257 + 3.0 * u * t * t * c1.y
258 + t * t * t * p1.y,
259 ));
260 }
261 self.polyline(&pts, stroke, color)
262 }
263
264 pub fn rect_outline(
265 &mut self,
266 rect: Rect,
267 radius: f32,
268 stroke: BrushStroke,
269 color: Color,
270 ) -> Rect {
271 if rect.is_empty() || !stroke.is_visible() || color.a <= 0.0 {
272 return Rect::ZERO;
273 }
274 use sequoia_draw::Path;
275 let style = self.stroke_style(stroke);
276 let mut path = Path::new();
277 let global = self.to_global_rect(rect);
278 let radius = radius * self.unit_scale();
279 if radius > 0.0 {
280 path.rounded_rect(global, radius);
281 } else {
282 path.rect(global);
283 }
284 let bounds = self.ctx.stroke(&path, &style, color);
285 self.to_local_bounds(bounds)
286 }
287
288 pub fn polygon(&mut self, points: &[Point], color: Color) -> Rect {
289 if points.len() < 3 || color.a <= 0.0 {
290 return Rect::ZERO;
291 }
292 let global: Vec<Point> = points.iter().map(|p| self.to_global_point(*p)).collect();
293 let bounds = self.ctx.polygon(&global, color);
294 self.to_local_bounds(bounds)
295 }
296
297 pub fn ellipse(&mut self, center: Point, rx: f32, ry: f32, color: Color) -> Rect {
298 if !(rx > 0.0) || !(ry > 0.0) || color.a <= 0.0 {
299 return Rect::ZERO;
300 }
301 let kx = self.transform.apply_vec(Vec2::new(1.0, 0.0)).length();
302 let ky = self.transform.apply_vec(Vec2::new(0.0, 1.0)).length();
303 let bounds = self
304 .ctx
305 .ellipse(self.to_global_point(center), rx * kx, ry * ky, color);
306 self.to_local_bounds(bounds)
307 }
308
309 pub fn text(&mut self, text: &str, baseline: Point, size: f32, color: Color) {
314 if text.is_empty() || !(size > 0.0) || color.a <= 0.0 {
315 return;
316 }
317 let g = self.to_global_point(baseline);
318 self.ctx
319 .draw_text(text, (g.x, g.y), size * self.unit_scale(), color);
320 }
321
322 pub fn text_tl(&mut self, text: &str, top_left: Point, size: f32, color: Color) {
326 let k = self.unit_scale();
327 if k <= 0.0 {
328 return;
329 }
330 let ascent = self.ctx.text_metrics(size * k).ascent / k;
331 let baseline = Point::new(top_left.x, top_left.y + ascent);
332 self.text(text, baseline, size, color);
333 }
334
335 pub fn text_metrics(&mut self, size: f32) -> crate::TextMetrics {
337 self.ctx.text_metrics(size)
338 }
339
340 pub fn text_tl_rotated(
341 &mut self,
342 text: &str,
343 top_left: Point,
344 size: f32,
345 color: Color,
346 degrees: f32,
347 pivot: Point,
348 ) {
349 let pivot = self.to_global_point(pivot);
350 let t = Transform::rotate_around_degrees(degrees, pivot);
351 self.push_list_transform(t);
352 self.text_tl(text, top_left, size, color);
353 self.pop_list_transform();
354 }
355
356 pub fn text_styled(
357 &mut self,
358 text: &str,
359 baseline: Point,
360 size: f32,
361 weight: FontWeight,
362 color: Color,
363 ) {
364 if text.is_empty() || !(size > 0.0) || color.a <= 0.0 {
365 return;
366 }
367 let g = self.to_global_point(baseline);
368 self.ctx
369 .draw_text_weighted(text, (g.x, g.y), size * self.unit_scale(), weight, color);
370 }
371
372 pub fn text_width(&mut self, text: &str, size: f32) -> f32 {
374 if text.is_empty() || !(size > 0.0) {
375 return 0.0;
376 }
377 let k = self.unit_scale();
378 if k <= 0.0 {
379 return 0.0;
380 }
381 self.ctx.text_width(text, size * k) / k
382 }
383
384 pub fn text_width_weighted(&mut self, text: &str, size: f32, weight: FontWeight) -> f32 {
385 if text.is_empty() || !(size > 0.0) {
386 return 0.0;
387 }
388 let k = self.unit_scale();
389 if k <= 0.0 {
390 return 0.0;
391 }
392 self.ctx.text_width_weighted(text, size * k, weight) / k
393 }
394
395 pub fn text_center_x(&mut self, text: &str, center_x: f32, top: f32, size: f32, color: Color) {
396 let width = self.text_width(text, size);
397 self.text_tl(text, Point::new(center_x - width * 0.5, top), size, color);
398 }
399
400 pub fn text_center(&mut self, rect: Rect, text: &str, size: f32, color: Color) {
401 let k = self.unit_scale();
402 if k <= 0.0 {
403 return;
404 }
405 let metrics = self.ctx.measure_text(text, size * k);
406 let width = metrics.width / k;
407 let height = metrics.height / k;
408 let top = rect.top() + (rect.size.height - height) * 0.5;
409 self.text_tl(
410 text,
411 Point::new(rect.center().x - width * 0.5, top),
412 size,
413 color,
414 );
415 }
416
417 pub fn image(&mut self, rect: Rect, uv: [f32; 4], source: ImageSource) -> Rect {
418 if rect.is_empty() {
419 return Rect::ZERO;
420 }
421 let global = self.to_global_rect(rect);
422 self.ctx.list.push_image(global, uv, source);
423 rect
424 }
425
426 pub fn pixels(&mut self, rect: Rect, pixels: &Pixels) -> Rect {
427 let full = Rect::from_xywh(0.0, 0.0, pixels.width() as f32, pixels.height() as f32);
428 self.pixels_region_sampled(rect, pixels, full, ImageSampling::Linear)
429 }
430
431 pub fn pixels_nearest(&mut self, rect: Rect, pixels: &Pixels) -> Rect {
432 let full = Rect::from_xywh(0.0, 0.0, pixels.width() as f32, pixels.height() as f32);
433 self.pixels_region_sampled(rect, pixels, full, ImageSampling::Nearest)
434 }
435
436 pub fn pixels_region(&mut self, dst: Rect, pixels: &Pixels, src: Rect) -> Rect {
437 self.pixels_region_sampled(dst, pixels, src, ImageSampling::Linear)
438 }
439
440 fn pixels_region_sampled(
441 &mut self,
442 dst: Rect,
443 pixels: &Pixels,
444 src: Rect,
445 sampling: ImageSampling,
446 ) -> Rect {
447 if dst.is_empty() {
448 return Rect::ZERO;
449 }
450 let Some(uv) = Self::pixel_uv(pixels.width(), pixels.height(), src) else {
451 return Rect::ZERO;
452 };
453 let global = self.to_global_rect(dst);
454 self.ctx
455 .list
456 .push_image_sampled(global, uv, pixels.to_source(), sampling);
457 dst
458 }
459
460 pub fn pixel_uv(width: u32, height: u32, src: Rect) -> Option<[f32; 4]> {
461 if width == 0 || height == 0 {
462 return None;
463 }
464 let (w, h) = (width as f32, height as f32);
465 let x0 = src.left().clamp(0.0, w);
466 let y0 = src.top().clamp(0.0, h);
467 let x1 = src.right().clamp(0.0, w);
468 let y1 = src.bottom().clamp(0.0, h);
469 if x1 <= x0 || y1 <= y0 {
470 return None;
471 }
472 Some([x0 / w, y0 / h, x1 / w, y1 / h])
473 }
474
475 #[must_use]
482 pub fn target_uv(group: Size, rect: Rect) -> [f32; 4] {
483 if group.width <= 0.0 || group.height <= 0.0 || rect.is_empty() {
484 return [0.0, 0.0, 1.0, 1.0];
485 }
486 [
487 (rect.left() / group.width).clamp(0.0, 1.0),
488 (rect.top() / group.height).clamp(0.0, 1.0),
489 (rect.right() / group.width).clamp(0.0, 1.0),
490 (rect.bottom() / group.height).clamp(0.0, 1.0),
491 ]
492 }
493
494 pub fn pixel_at(rect: Rect, width: u32, height: u32, p: Point) -> Option<(i32, i32)> {
495 if width == 0 || height == 0 || rect.is_empty() {
496 return None;
497 }
498 let fx = (p.x - rect.left()) / rect.size.width * width as f32;
499 let fy = (p.y - rect.top()) / rect.size.height * height as f32;
500 if !fx.is_finite() || !fy.is_finite() || fx < 0.0 || fy < 0.0 {
501 return None;
502 }
503 if fx >= width as f32 || fy >= height as f32 {
504 return None;
505 }
506 Some((fx.floor() as i32, fy.floor() as i32))
507 }
508
509 pub fn knob(&mut self, center: Point, radius: f32, fill: Color) -> Rect {
510 if !(radius > 0.0) || !radius.is_finite() {
511 return Rect::ZERO;
512 }
513 let outer = self.circle(center, radius, Fill::solid(Color::WHITE));
514 let inner_r = radius - 2.0;
515 if inner_r > 0.0 {
516 let inner = self.circle(center, inner_r, Fill::solid(fill));
517 outer.union(inner)
518 } else {
519 outer
520 }
521 }
522
523 pub fn sv_field(&mut self, rect: Rect, hue01: f32) -> Rect {
524 if rect.is_empty() {
525 return Rect::ZERO;
526 }
527 let h = hue01.clamp(0.0, 1.0);
528 self.fill(rect, Fill::solid(Color::from_hsva(h, 1.0, 1.0, 1.0)));
529 self.fill(
530 rect,
531 Fill::horizontal(GradientStops::two(
532 Color::WHITE,
533 Color::rgba(1.0, 1.0, 1.0, 0.0),
534 )),
535 );
536 self.fill(
537 rect,
538 Fill::vertical(GradientStops::two(
539 Color::rgba(0.0, 0.0, 0.0, 0.0),
540 Color::BLACK,
541 )),
542 );
543 rect
544 }
545
546 #[must_use]
548 pub fn current_blend(&self) -> sequoia_core::BlendMode {
549 self.current_blend
550 }
551
552 pub fn with_blend<R>(
554 &mut self,
555 mode: sequoia_core::BlendMode,
556 f: impl FnOnce(&mut Self) -> R,
557 ) -> R {
558 let previous = self.current_blend;
559 self.current_blend = mode;
560 let result = f(self);
561 self.current_blend = previous;
562 result
563 }
564
565 pub fn group<R>(
571 &mut self,
572 size: Size,
573 blend: sequoia_core::BlendMode,
574 f: impl FnOnce(&mut Self) -> R,
575 ) -> (sequoia_core::RenderTargetId, R) {
576 let target = self.ctx.list.begin_target(size, blend);
577 let result = f(self);
578 self.ctx.list.end_target(target);
579 (target, result)
580 }
581
582 pub fn blit(&mut self, target: sequoia_core::RenderTargetId, rect: Rect, uv: [f32; 4]) -> Rect {
587 if rect.is_empty() {
588 return Rect::ZERO;
589 }
590 let global = self.to_global_rect(rect);
591 self.ctx
592 .list
593 .push_blit(global, uv, target, self.current_blend);
594 rect
595 }
596
597 pub fn shader<S: sequoia_shader::SequoiShader>(
604 &mut self,
605 shader: S,
606 rect: Rect,
607 inputs: &[(&str, sequoia_core::TexInput)],
608 ) -> Rect {
609 self.shader_at(shader, rect, [0.0, 0.0, 1.0, 1.0], inputs)
610 }
611
612 pub fn shader_at<S: sequoia_shader::SequoiShader>(
617 &mut self,
618 shader: S,
619 rect: Rect,
620 uv: [f32; 4],
621 inputs: &[(&str, sequoia_core::TexInput)],
622 ) -> Rect {
623 if self.push_shader_draw(shader, rect, uv, inputs, None) {
624 rect
625 } else {
626 Rect::ZERO
627 }
628 }
629
630 pub fn try_shader<S: sequoia_shader::SequoiShader>(
635 &mut self,
636 shader: S,
637 rect: Rect,
638 inputs: &[(&str, sequoia_core::TexInput)],
639 ) -> bool {
640 self.push_shader_draw(shader, rect, [0.0, 0.0, 1.0, 1.0], inputs, None)
641 }
642
643 pub fn try_shader_at<S: sequoia_shader::SequoiShader>(
645 &mut self,
646 shader: S,
647 rect: Rect,
648 uv: [f32; 4],
649 inputs: &[(&str, sequoia_core::TexInput)],
650 ) -> bool {
651 self.push_shader_draw(shader, rect, uv, inputs, None)
652 }
653
654 pub fn try_shader_with_blend<S: sequoia_shader::SequoiShader>(
656 &mut self,
657 shader: S,
658 rect: Rect,
659 textures: &[(&str, sequoia_core::TexInput)],
660 blend: sequoia_core::BlendMode,
661 ) -> bool {
662 self.push_shader_draw(shader, rect, [0.0, 0.0, 1.0, 1.0], textures, Some(blend))
663 }
664
665 pub fn try_shader_at_with_blend<S: sequoia_shader::SequoiShader>(
667 &mut self,
668 shader: S,
669 rect: Rect,
670 uv: [f32; 4],
671 textures: &[(&str, sequoia_core::TexInput)],
672 blend: sequoia_core::BlendMode,
673 ) -> bool {
674 self.push_shader_draw(shader, rect, uv, textures, Some(blend))
675 }
676
677 pub fn shader_with_blend<S: sequoia_shader::SequoiShader>(
679 &mut self,
680 shader: S,
681 rect: Rect,
682 textures: &[(&str, sequoia_core::TexInput)],
683 blend: sequoia_core::BlendMode,
684 ) -> Rect {
685 if self.push_shader_draw(shader, rect, [0.0, 0.0, 1.0, 1.0], textures, Some(blend)) {
686 rect
687 } else {
688 Rect::ZERO
689 }
690 }
691
692 fn push_shader_draw<S: sequoia_shader::SequoiShader>(
693 &mut self,
694 shader: S,
695 rect: Rect,
696 uv: [f32; 4],
697 inputs: &[(&str, sequoia_core::TexInput)],
698 blend: Option<sequoia_core::BlendMode>,
699 ) -> bool {
700 use sequoia_core::ShaderDraw;
701 use std::sync::Arc;
702
703 if rect.is_empty() {
704 return false;
705 }
706 let Some(shader_id) = self.ctx.runtime().shader_id(S::NAME) else {
707 log::warn!("Painter::shader: shader `{}` is not registered", S::NAME);
708 return false;
709 };
710 let mut uniforms = Vec::with_capacity(S::UNIFORM_SIZE);
711 shader.encode_uniforms(&mut uniforms);
712 let mut bindings = Vec::new();
713 for (slot, name) in S::TEXTURES
714 .iter()
715 .enumerate()
716 .take(sequoia_shader::MAX_TEXTURES)
717 {
718 let Some((_, input)) = inputs.iter().find(|(key, _)| key == name) else {
719 continue;
720 };
721 bindings.push((slot as u32, input.clone()));
722 }
723 for (name, _) in inputs {
724 if !S::TEXTURES.contains(name) {
725 log::warn!(
726 "Painter::shader: shader `{}` has no texture input named `{name}`",
727 S::NAME
728 );
729 }
730 }
731 let global = self.to_global_rect(rect);
732 let draw = ShaderDraw {
733 rect: global,
734 uv,
735 shader: shader_id,
736 uniforms: Arc::from(uniforms),
737 textures: Arc::from(bindings),
738 blend: blend.unwrap_or(self.current_blend),
739 opacity: 1.0,
740 };
741 self.ctx.list.push_shader(draw);
742 true
743 }
744
745 pub fn grid(&mut self, step: f32, color: Color) -> u32 {
746 self.grid_offset(step, Vec2::ZERO, color)
747 }
748
749 pub fn grid_offset(&mut self, step: f32, offset: Vec2, color: Color) -> u32 {
750 if step <= 0.0 || color.a <= 0.0 || self.size.width <= 0.0 || self.size.height <= 0.0 {
751 return 0;
752 }
753 let mut drawn = 0u32;
754 let ox = offset.x.rem_euclid(step);
755 let oy = offset.y.rem_euclid(step);
756 let w = self.size.width;
757 let h = self.size.height;
758 let mut x = ox;
759 while x < w {
760 self.fill(Rect::from_xywh(x, 0.0, 1.0, h), Fill::solid(color));
761 drawn += 1;
762 if drawn > 4096 {
763 break;
764 }
765 x += step;
766 }
767 let mut y = oy;
768 while y < h {
769 self.fill(Rect::from_xywh(0.0, y, w, 1.0), Fill::solid(color));
770 drawn += 1;
771 if drawn > 8192 {
772 break;
773 }
774 y += step;
775 }
776 drawn
777 }
778
779 pub fn axes(&mut self, origin: Point, color: Color, width: f32) -> Rect {
780 if !(width > 0.0) || color.a <= 0.0 {
781 return Rect::ZERO;
782 }
783 let stroke = BrushStroke::new(width);
784 let w = self.size.width;
785 let h = self.size.height;
786 let a = self.line(
787 Point::new(0.0, origin.y),
788 Point::new(w, origin.y),
789 stroke,
790 color,
791 );
792 let b = self.line(
793 Point::new(origin.x, 0.0),
794 Point::new(origin.x, h),
795 stroke,
796 color,
797 );
798 a.union(b)
799 }
800
801 pub fn frame(&mut self, rect: Rect, color: Color, width: f32) -> Rect {
802 self.rect_outline(rect, 0.0, BrushStroke::new(width), color)
803 }
804
805 pub fn cross(&mut self, center: Point, half: f32, width: f32, color: Color) -> Rect {
806 if !(half > 0.0) || color.a <= 0.0 {
807 return Rect::ZERO;
808 }
809 let s = BrushStroke::round(width);
810 let a = self.line(
811 Point::new(center.x - half, center.y),
812 Point::new(center.x + half, center.y),
813 s,
814 color,
815 );
816 let b = self.line(
817 Point::new(center.x, center.y - half),
818 Point::new(center.x, center.y + half),
819 s,
820 color,
821 );
822 a.union(b)
823 }
824
825 pub fn marquee(&mut self, a: Point, b: Point, fill: Fill, outline: Color) -> Rect {
826 let rect = Rect::from_xywh(
827 a.x.min(b.x),
828 a.y.min(b.y),
829 (a.x - b.x).abs(),
830 (a.y - b.y).abs(),
831 );
832 if rect.is_empty() {
833 return Rect::ZERO;
834 }
835 self.fill(rect, fill);
836 self.rect_outline(rect, 0.0, BrushStroke::new(1.0), outline);
837 rect
838 }
839
840 pub fn ellipse_outline(
841 &mut self,
842 center: Point,
843 rx: f32,
844 ry: f32,
845 stroke: BrushStroke,
846 color: Color,
847 ) -> Rect {
848 if !(rx > 0.0) || !(ry > 0.0) || !stroke.is_visible() || color.a <= 0.0 {
849 return Rect::ZERO;
850 }
851 use sequoia_draw::Path;
852 let style = self.stroke_style(stroke);
853 let kx = self.transform.apply_vec(Vec2::new(1.0, 0.0)).length();
854 let ky = self.transform.apply_vec(Vec2::new(0.0, 1.0)).length();
855 let mut path = Path::new();
856 path.ellipse(self.to_global_point(center), rx * kx, ry * ky);
857 let bounds = self.ctx.stroke(&path, &style, color);
858 self.to_local_bounds(bounds)
859 }
860
861 pub fn capsule(&mut self, from: Point, to: Point, radius: f32, color: Color) -> Rect {
862 if !(radius > 0.0) || color.a <= 0.0 {
863 return Rect::ZERO;
864 }
865 let bounds = self.ctx.capsule(
866 self.to_global_point(from),
867 self.to_global_point(to),
868 radius,
869 color,
870 );
871 self.to_local_bounds(bounds)
872 }
873
874 pub fn arrow(
875 &mut self,
876 from: Point,
877 to: Point,
878 width: f32,
879 head_len: f32,
880 head_width: f32,
881 color: Color,
882 ) -> Rect {
883 if color.a <= 0.0 {
884 return Rect::ZERO;
885 }
886 let bounds = self.ctx.arrow(
887 self.to_global_point(from),
888 self.to_global_point(to),
889 width,
890 head_len,
891 head_width,
892 color,
893 );
894 self.to_local_bounds(bounds)
895 }
896
897 pub fn x_marker(&mut self, center: Point, half: f32, width: f32, color: Color) -> Rect {
898 if !(half > 0.0) || color.a <= 0.0 {
899 return Rect::ZERO;
900 }
901 let bounds = self
902 .ctx
903 .x_marker(self.to_global_point(center), half, width, color);
904 self.to_local_bounds(bounds)
905 }
906
907 pub fn regular_polygon(
908 &mut self,
909 center: Point,
910 radius: f32,
911 sides: u32,
912 rotation_deg: f32,
913 color: Color,
914 ) -> Rect {
915 if !(radius > 0.0) || sides < 3 || color.a <= 0.0 {
916 return Rect::ZERO;
917 }
918 let bounds = self.ctx.regular_polygon(
919 self.to_global_point(center),
920 radius,
921 sides,
922 rotation_deg,
923 color,
924 );
925 self.to_local_bounds(bounds)
926 }
927
928 pub fn star(
929 &mut self,
930 center: Point,
931 outer_r: f32,
932 inner_r: f32,
933 points: u32,
934 rotation_deg: f32,
935 color: Color,
936 ) -> Rect {
937 if !(outer_r > 0.0) || !(inner_r > 0.0) || points < 2 || color.a <= 0.0 {
938 return Rect::ZERO;
939 }
940 let bounds = self.ctx.star(
941 self.to_global_point(center),
942 outer_r,
943 inner_r,
944 points,
945 rotation_deg,
946 color,
947 );
948 self.to_local_bounds(bounds)
949 }
950
951 pub fn dots(&mut self, step: f32, offset: Vec2, radius: f32, color: Color) -> u32 {
952 if step <= 0.0 || !(radius > 0.0) || color.a <= 0.0 {
953 return 0;
954 }
955 if self.size.width <= 0.0 || self.size.height <= 0.0 {
956 return 0;
957 }
958 let mut n = 0u32;
959 let ox = offset.x.rem_euclid(step);
960 let oy = offset.y.rem_euclid(step);
961 let mut y = oy;
962 while y < self.size.height {
963 let mut x = ox;
964 while x < self.size.width {
965 self.circle(Point::new(x, y), radius, Fill::solid(color));
966 n += 1;
967 if n > 16384 {
968 return n;
969 }
970 x += step;
971 }
972 y += step;
973 }
974 n
975 }
976
977 pub fn grid_split(
978 &mut self,
979 step: f32,
980 every: u32,
981 offset: Vec2,
982 minor: Color,
983 major: Color,
984 ) -> u32 {
985 if step <= 0.0 || every == 0 {
986 return 0;
987 }
988 if self.size.width <= 0.0 || self.size.height <= 0.0 {
989 return 0;
990 }
991 let every = every.max(1);
992 let mut drawn = 0u32;
993 let ox = offset.x.rem_euclid(step);
994 let oy = offset.y.rem_euclid(step);
995 let w = self.size.width;
996 let h = self.size.height;
997 let mut i = 0u32;
998 let mut x = ox;
999 while x < w {
1000 let c = if i % every == every - 1 { major } else { minor };
1001 self.fill(Rect::from_xywh(x, 0.0, 1.0, h), Fill::solid(c));
1002 drawn += 1;
1003 if drawn > 4096 {
1004 break;
1005 }
1006 x += step;
1007 i += 1;
1008 }
1009 let mut j = 0u32;
1010 let mut y = oy;
1011 while y < h {
1012 let c = if j % every == every - 1 { major } else { minor };
1013 self.fill(Rect::from_xywh(0.0, y, w, 1.0), Fill::solid(c));
1014 drawn += 1;
1015 if drawn > 8192 {
1016 break;
1017 }
1018 y += step;
1019 j += 1;
1020 }
1021 drawn
1022 }
1023
1024 pub fn clipped<R>(&mut self, rect: Rect, f: impl FnOnce(&mut Self) -> R) -> R {
1025 let global = self.to_global_rect(rect);
1026 self.ctx.list.push_clip(global);
1027 let r = f(self);
1028 self.ctx.list.pop_clip();
1029 r
1030 }
1031
1032 pub fn clipped_rounded<R>(
1033 &mut self,
1034 rect: Rect,
1035 radius: f32,
1036 f: impl FnOnce(&mut Self) -> R,
1037 ) -> R {
1038 let global = self.to_global_rect(rect);
1039 self.ctx.push_rounded_clip(global, radius);
1040 let r = f(self);
1041 self.ctx.pop_clip();
1042 r
1043 }
1044
1045 fn push_gradient(
1046 &mut self,
1047 rect: Rect,
1048 radius: f32,
1049 kind: GradientKind,
1050 stops: GradientStops,
1051 ) -> Rect {
1052 if stops.len() <= sequoia_core::MAX_GRADIENT_STOPS {
1053 let global = self.to_global_rect(rect);
1054 self.ctx.list.push_gradient(global, radius, kind, stops);
1055 return rect;
1056 }
1057
1058 self.push_gradient_bands(rect, radius, kind, stops)
1059 }
1060
1061 fn push_gradient_bands(
1062 &mut self,
1063 rect: Rect,
1064 shape_radius: f32,
1065 kind: GradientKind,
1066 stops: GradientStops,
1067 ) -> Rect {
1068 const RADIAL_BANDS: usize = 64;
1069 let vertical = match kind {
1070 GradientKind::Linear { angle_deg } => {
1071 let (dx, dy) = GradientKind::Linear { angle_deg }.direction();
1072 dy.abs() >= dx.abs()
1073 }
1074 GradientKind::Radial { .. } => true,
1075 };
1076 let global = self.to_global_rect(rect);
1077 let mut bounds = Rect::ZERO;
1078 match kind {
1079 GradientKind::Radial { radius } => {
1080 let center = Point::new(
1081 global.left() + global.size.width * 0.5,
1082 global.top() + global.size.height * 0.5,
1083 );
1084 let extent = if radius > 0.0 {
1085 radius
1086 } else {
1087 global.size.width.hypot(global.size.height) * 0.5
1088 };
1089 if extent <= 0.0 {
1090 return Rect::ZERO;
1091 }
1092 if shape_radius > 0.0 {
1093 self.ctx.push_rounded_clip(global, shape_radius);
1094 } else {
1095 self.ctx.list.push_clip(global);
1096 }
1097 for i in (0..RADIAL_BANDS).rev() {
1098 let t0 = i as f32 / RADIAL_BANDS as f32;
1099 let t1 = (i + 1) as f32 / RADIAL_BANDS as f32;
1100 let [r, g, b, a] = stops.sample_linear((t0 + t1) * 0.5);
1101 let color = Color { r, g, b, a }.from_linear();
1102 let r1 = extent * t1;
1103 let band = Rect::from_xywh(center.x - r1, center.y - r1, r1 * 2.0, r1 * 2.0);
1104 self.ctx.list.push_quad(band, color, r1);
1105 }
1106 self.ctx.pop_clip();
1107 bounds = global;
1108 }
1109 GradientKind::Linear { .. } => {
1110 let extent = if vertical {
1111 global.size.height
1112 } else {
1113 global.size.width
1114 };
1115 let bands = (extent.ceil() as usize).clamp(1, 256);
1116 let scale = {
1117 let s = self.ctx.scale_factor();
1118 if s.is_finite() && s > 0.0 { s } else { 1.0 }
1119 };
1120 let origin = if vertical {
1121 global.top()
1122 } else {
1123 global.left()
1124 };
1125 let edges =
1126 crate::paint_draw::band_edges(origin, extent, bands, 1.0 / scale, 0.5 / scale);
1127 for (i, (lo, hi)) in edges.into_iter().enumerate() {
1128 let t = (i as f32 + 0.5) / bands as f32;
1129 let [r, g, b, a] = stops.sample_linear(t);
1130 let color = Color { r, g, b, a }.from_linear();
1131 let band = if vertical {
1132 Rect::from_xywh(global.left(), lo, global.size.width, hi - lo)
1133 } else {
1134 Rect::from_xywh(lo, global.top(), hi - lo, global.size.height)
1135 };
1136 self.ctx.list.push_quad(band, color, 0.0);
1137 bounds = bounds.union(band);
1138 }
1139 }
1140 }
1141 if bounds.is_empty() {
1142 return Rect::ZERO;
1143 }
1144
1145 Rect::from_xywh(
1146 bounds.left() - self.origin.x,
1147 bounds.top() - self.origin.y,
1148 bounds.size.width,
1149 bounds.size.height,
1150 )
1151 }
1152
1153 fn to_local_bounds(&self, bounds: Rect) -> Rect {
1154 if bounds.is_empty() {
1155 return Rect::ZERO;
1156 }
1157 let r = Rect::from_xywh(
1158 bounds.left() - self.origin.x,
1159 bounds.top() - self.origin.y,
1160 bounds.size.width,
1161 bounds.size.height,
1162 );
1163
1164 match self.transform.inverse() {
1165 Some(inv) => inv.apply_rect_aabb(r),
1166 None => Rect::ZERO,
1167 }
1168 }
1169}