1use std::rc::Rc;
2
3use sequoia_core::{Color, ImageSource, Point, Rect, Shadow, Vec2};
4use sequoia_draw::{
5 Dash, FillRule, Gradient, GradientStop, MinMax, Path, Peaks, Radius, RasterOptions, Shape,
6 ShapeCache, StrokeStyle, Transform, WaveformOpts, bake_strip, layout_bars,
7 waveform_line_points,
8};
9
10use crate::track::Orientation;
11use crate::view::PaintCtx;
12
13fn line_style(width: f32) -> StrokeStyle {
14 StrokeStyle::new(width)
15}
16
17pub const GRADIENT_BAND_PX: f32 = 1.0;
18
19pub(crate) const GRADIENT_BANDS_MAX: usize = 256;
20
21pub(crate) fn gradient_band_count(extent: f32, band: f32) -> usize {
22 let band = if band.is_finite() && band > 0.0 {
23 band
24 } else {
25 GRADIENT_BAND_PX
26 };
27 ((extent / band).ceil().max(1.0) as usize).clamp(1, GRADIENT_BANDS_MAX)
28}
29
30pub(crate) fn band_edges(
31 origin: f32,
32 extent: f32,
33 count: usize,
34 snap: f32,
35 over: f32,
36) -> Vec<(f32, f32)> {
37 let count = count.max(1);
38 let snap = if snap.is_finite() && snap > 0.0 {
39 snap
40 } else {
41 1.0
42 };
43 let over = if over.is_finite() && over > 0.0 {
44 over
45 } else {
46 0.0
47 };
48 (0..count)
49 .map(|i| {
50 let last = i + 1 == count;
51 let lo = if i == 0 {
52 origin
53 } else {
54 ((origin + extent * i as f32 / count as f32) / snap).round() * snap
55 };
56 let mut hi = if last {
57 origin + extent
58 } else {
59 ((origin + extent * (i + 1) as f32 / count as f32) / snap).round() * snap + over
60 };
61 if hi < lo + 0.01 {
62 hi = lo + 0.01;
63 }
64 (lo, hi)
65 })
66 .collect()
67}
68
69impl PaintCtx<'_> {
70 pub fn shapes(&self) -> &ShapeCache {
71 self.shapes
72 }
73
74 pub fn raster_options(&self) -> RasterOptions {
75 RasterOptions::default().for_scale(self.scale_factor())
76 }
77
78 pub fn translate(&mut self, dx: f32, dy: f32) {
79 self.draw_offset.x += dx;
80 self.draw_offset.y += dy;
81 }
82
83 pub fn set_draw_offset(&mut self, offset: impl Into<Point>) {
84 let p = offset.into();
85 self.draw_offset = Vec2::new(p.x, p.y);
86 }
87
88 pub fn draw_offset(&self) -> Vec2 {
89 self.draw_offset
90 }
91
92 pub fn reset_draw_offset(&mut self) {
93 self.draw_offset = Vec2::ZERO;
94 }
95
96 pub fn save(&mut self) {
97 self.offset_stack.push(self.draw_offset);
98 }
99
100 pub fn restore(&mut self) {
101 if let Some(prev) = self.offset_stack.pop() {
102 self.draw_offset = prev;
103 }
104 }
105
106 pub fn path_from_svg(&self, data: &str) -> Option<Path> {
107 match Path::from_svg(data) {
108 Ok(path) => Some(path),
109 Err(err) => {
110 log::warn!("invalid SVG path: {err}");
111 None
112 }
113 }
114 }
115
116 pub fn rect(&mut self, rect: Rect, color: Color) {
117 self.quad(rect, color, 0.0);
118 }
119
120 pub fn rounded(&mut self, rect: Rect, radius: f32, color: Color) {
121 self.quad(rect, color, radius.max(0.0));
122 }
123
124 pub fn circle(&mut self, center: Point, radius: f32, color: Color) -> Rect {
125 self.disk(center, radius, color)
126 }
127
128 pub fn fill_gradient_rect(
129 &mut self,
130 rect: Rect,
131 stops: &[GradientStop],
132 orientation: Orientation,
133 ) -> Rect {
134 if rect.is_empty() || stops.is_empty() {
135 return Rect::ZERO;
136 }
137 let rect = self.shifted_rect(rect);
138 if rect.is_empty() {
139 return Rect::ZERO;
140 }
141 let gradient = match orientation {
142 Orientation::Horizontal => {
143 Gradient::linear((rect.left(), 0.0), (rect.right(), 0.0), stops.to_vec())
144 }
145 Orientation::Vertical => {
146 Gradient::linear((0.0, rect.top()), (0.0, rect.bottom()), stops.to_vec())
147 }
148 };
149 let mut bounds = Rect::ZERO;
150
151 let over = self.band_px(0.5);
152 match orientation {
153 Orientation::Horizontal => {
154 let mut x = rect.left();
155 while x < rect.right() {
156 let nx = (x + 1.0).min(rect.right());
157 let extra = if nx < rect.right() { over } else { 0.0 };
158 let c = gradient.color_at(Point::new((x + nx) * 0.5, rect.top()));
159 let band = Rect::from_xywh(x, rect.top(), nx - x + extra, rect.size.height);
160 self.list.push_quad(band, c, 0.0);
161 bounds = bounds.union(band);
162 x = nx;
163 }
164 }
165 Orientation::Vertical => {
166 let mut y = rect.top();
167 while y < rect.bottom() {
168 let ny = (y + 1.0).min(rect.bottom());
169 let extra = if ny < rect.bottom() { over } else { 0.0 };
170 let c = gradient.color_at(Point::new(rect.left(), (y + ny) * 0.5));
171 let band = Rect::from_xywh(rect.left(), y, rect.size.width, ny - y + extra);
172 self.list.push_quad(band, c, 0.0);
173 bounds = bounds.union(band);
174 y = ny;
175 }
176 }
177 }
178 bounds
179 }
180
181 pub fn knob(&mut self, center: Point, radius: f32, fill: Color) -> Rect {
182 if !(radius > 0.0) || !radius.is_finite() {
183 return Rect::ZERO;
184 }
185 let outer = self.disk(center, radius, Color::WHITE);
186 let inner_r = radius - 2.0;
187 if inner_r > 0.0 {
188 let inner = self.disk(center, inner_r, fill);
189 outer.union(inner)
190 } else {
191 outer
192 }
193 }
194
195 pub fn sv_field(&mut self, rect: Rect, hue01: f32) -> Rect {
196 if rect.is_empty() {
197 return Rect::ZERO;
198 }
199 let h = hue01.clamp(0.0, 1.0);
200 self.fill_rect(rect, Color::from_hsva(h, 1.0, 1.0, 1.0));
201 self.fill_gradient_rect(
202 rect,
203 &[
204 GradientStop::new(0.0, Color::WHITE),
205 GradientStop::new(1.0, Color::rgba(1.0, 1.0, 1.0, 0.0)),
206 ],
207 Orientation::Horizontal,
208 );
209 self.fill_gradient_rect(
210 rect,
211 &[
212 GradientStop::new(0.0, Color::rgba(0.0, 0.0, 0.0, 0.0)),
213 GradientStop::new(1.0, Color::BLACK),
214 ],
215 Orientation::Vertical,
216 );
217 self.shifted_rect(rect)
218 }
219
220 pub fn path(&mut self, path: &Path, width: f32, color: Color) -> Rect {
221 self.stroke(path, &line_style(width.max(0.0)), color)
222 }
223
224 pub fn text(&mut self, text: &str, size: f32, color: Color) {
225 let origin = (self.rect.origin.x, self.rect.origin.y);
226 self.draw_text(text, origin, size.max(0.0), color);
227 }
228
229 pub fn fill(&mut self, path: &Path, color: Color) -> Rect {
230 self.fill_rule(path, FillRule::NonZero, color)
231 }
232
233 pub fn fill_rule(&mut self, path: &Path, rule: FillRule, color: Color) -> Rect {
234 let opts = self.raster_options();
235 let shape = self.shapes.fill(path, rule, opts);
236 self.draw_shape(&shape, Point::ZERO, color)
237 }
238
239 pub fn stroke(&mut self, path: &Path, style: &StrokeStyle, color: Color) -> Rect {
240 let opts = self.raster_options();
241 let shape = self.shapes.stroke(path, style, opts);
242 self.draw_shape(&shape, Point::ZERO, color)
243 }
244
245 pub fn shape(&mut self, shape: &Shape, color: Color) -> Rect {
246 self.draw_shape(shape, Point::ZERO, color)
247 }
248
249 pub fn draw_shape(&mut self, shape: &Shape, at: Point, color: Color) -> Rect {
250 let offset = Vec2::new(self.draw_offset.x + at.x, self.draw_offset.y + at.y);
251 let mut bounds = Rect::ZERO;
252 for span in shape.iter() {
253 let rect = if offset.x == 0.0 && offset.y == 0.0 {
254 span.rect
255 } else {
256 Rect::from_xywh(
257 span.rect.origin.x + offset.x,
258 span.rect.origin.y + offset.y,
259 span.rect.size.width,
260 span.rect.size.height,
261 )
262 };
263 let color = Color {
264 a: color.a * span.alpha,
265 ..color
266 };
267 self.list.push_quad(rect, color, span.radius);
268 bounds = bounds.union(rect);
269 }
270 bounds
271 }
272
273 pub fn fill_gradient(&mut self, path: &Path, gradient: &Gradient) -> Rect {
274 self.fill_gradient_rule(path, FillRule::NonZero, gradient)
275 }
276
277 pub fn fill_gradient_rule(&mut self, path: &Path, rule: FillRule, gradient: &Gradient) -> Rect {
278 let opts = self.raster_options().analytic(false);
279 let shape = self.shapes.fill(path, rule, opts);
280 let band = self.band_px(GRADIENT_BAND_PX);
281 self.draw_shape_gradient(&shape, Point::ZERO, gradient, band)
282 }
283
284 pub fn rounded_rect_gradient(&mut self, rect: Rect, radius: f32, gradient: &Gradient) -> Rect {
285 if rect.is_empty() {
286 return Rect::ZERO;
287 }
288 let mut path = Path::new();
289 path.rounded_rect(rect, radius);
290 self.fill_gradient(&path, gradient)
291 }
292
293 pub fn stroke_gradient(
294 &mut self,
295 path: &Path,
296 style: &StrokeStyle,
297 gradient: &Gradient,
298 ) -> Rect {
299 let opts = self.raster_options().analytic(false);
300 let shape = self.shapes.stroke(path, style, opts);
301 let band = self.band_px(GRADIENT_BAND_PX);
302 self.draw_shape_gradient(&shape, Point::ZERO, gradient, band)
303 }
304
305 pub fn shadow(
306 &mut self,
307 rect: Rect,
308 radius: f32,
309 blur: f32,
310 offset: Point,
311 color: Color,
312 ) -> Rect {
313 let base = shift(rect, self.draw_offset);
314 if base.is_empty() || color.a <= 0.0 || !(blur > 0.0) {
315 return Rect::ZERO;
316 }
317 let shadow = Shadow {
318 rect: base,
319 color,
320 radius: shadow_radius(radius, base),
321 blur,
322 spread: 0.0,
323 offset: Vec2::new(offset.x, offset.y),
324 inset: false,
325 };
326 let bounds = shadow.bounds();
327 self.list.push_shadow(shadow);
328 bounds
329 }
330
331 pub fn inner_shadow(
332 &mut self,
333 rect: Rect,
334 radius: f32,
335 blur: f32,
336 offset: Point,
337 color: Color,
338 ) -> Rect {
339 let base = shift(rect, self.draw_offset);
340 if base.is_empty() || color.a <= 0.0 || !(blur > 0.0) {
341 return Rect::ZERO;
342 }
343 self.list.push_shadow(Shadow {
344 rect: base,
345 color,
346 radius: shadow_radius(radius, base),
347 blur,
348 spread: 0.0,
349 offset: Vec2::new(offset.x, offset.y),
350 inset: true,
351 });
352 base
353 }
354
355 pub fn shape_gradient_bands(
356 &self,
357 shape: &Shape,
358 at: Point,
359 gradient: &Gradient,
360 band: f32,
361 ) -> Vec<(Rect, Color, f32)> {
362 let offset = Vec2::new(self.draw_offset.x + at.x, self.draw_offset.y + at.y);
363 let band = if band > 0.0 {
364 band
365 } else {
366 self.band_px(GRADIENT_BAND_PX)
367 };
368 let band = band.max(0.05);
369 let snap = self.band_px(1.0);
370 let vertical = gradient.is_vertical();
371 let mut out = Vec::new();
372 for span in shape.iter() {
373 let rect = shift(span.rect, offset);
374 if rect.is_empty() || span.alpha <= 0.0 {
375 continue;
376 }
377 if span.radius > 0.0 {
378 let center = Point::new(
379 rect.left() + rect.size.width * 0.5,
380 rect.top() + rect.size.height * 0.5,
381 );
382 let color = gradient.color_at(center);
383 out.push((rect, tint(color, span.alpha), span.radius));
384 continue;
385 }
386 let origin = if vertical { rect.top() } else { rect.left() };
387 let extent = if vertical {
388 rect.size.height
389 } else {
390 rect.size.width
391 };
392 let count = gradient_band_count(extent, band);
393
394 for (lo, hi) in band_edges(origin, extent, count, snap, snap * 0.5) {
395 if hi <= lo {
396 continue;
397 }
398 let sub = if vertical {
399 Rect::from_xywh(rect.left(), lo, rect.size.width, hi - lo)
400 } else {
401 Rect::from_xywh(lo, rect.top(), hi - lo, rect.size.height)
402 };
403 let color = gradient.color_at(Point::new(
404 sub.left() + sub.size.width * 0.5,
405 sub.top() + sub.size.height * 0.5,
406 ));
407 out.push((sub, tint(color, span.alpha), 0.0));
408 }
409 }
410 out
411 }
412
413 pub fn push_gradient_bands(&mut self, bands: &[(Rect, Color, f32)]) -> Rect {
414 let mut bounds = Rect::ZERO;
415 for (rect, color, radius) in bands {
416 if rect.is_empty() {
417 continue;
418 }
419 self.list.push_quad(*rect, *color, *radius);
420 bounds = bounds.union(*rect);
421 }
422 bounds
423 }
424
425 pub fn draw_shape_gradient(
426 &mut self,
427 shape: &Shape,
428 at: Point,
429 gradient: &Gradient,
430 band: f32,
431 ) -> Rect {
432 let bands = self.shape_gradient_bands(shape, at, gradient, band);
433 self.push_gradient_bands(&bands)
434 }
435
436 pub fn shape_gradient(&mut self, shape: &Shape, gradient: &Gradient) -> Rect {
437 let band = self.band_px(GRADIENT_BAND_PX);
438 self.draw_shape_gradient(shape, Point::ZERO, gradient, band)
439 }
440
441 pub fn fill_rect(&mut self, rect: Rect, color: Color) -> Rect {
442 let rect = self.shifted_rect(rect);
443 self.list.push_quad(rect, color, 0.0);
444 rect
445 }
446
447 pub fn bars(&mut self, bars: &[Rect], color: Color) -> Rect {
448 if bars.is_empty() || color.a <= 0.0 {
449 return Rect::ZERO;
450 }
451 let off = self.draw_offset;
452 let mut bounds = Rect::ZERO;
453 if off.x == 0.0 && off.y == 0.0 {
454 self.list.push_waveform(bars, color);
455 for r in bars {
456 bounds = bounds.union(*r);
457 }
458 } else {
459 let shifted: Vec<Rect> = bars
460 .iter()
461 .map(|r| {
462 Rect::from_xywh(
463 r.left() + off.x,
464 r.top() + off.y,
465 r.size.width,
466 r.size.height,
467 )
468 })
469 .collect();
470 self.list.push_waveform(&shifted, color);
471 for r in &shifted {
472 bounds = bounds.union(*r);
473 }
474 }
475 bounds
476 }
477
478 pub fn waveform(
479 &mut self,
480 area: Rect,
481 peaks: &[MinMax],
482 opts: WaveformOpts,
483 color: Color,
484 ) -> Rect {
485 if area.is_empty() || peaks.is_empty() || color.a <= 0.0 {
486 return Rect::ZERO;
487 }
488 let area = self.shifted_rect(area);
489 let bars = layout_bars(area, peaks, opts);
490 if bars.is_empty() {
491 return Rect::ZERO;
492 }
493 self.list.push_waveform(&bars, color);
494 area
495 }
496
497 pub fn waveform_window(
498 &mut self,
499 area: Rect,
500 peaks: &Peaks,
501 start_peak: usize,
502 count: usize,
503 opts: WaveformOpts,
504 color: Color,
505 ) -> (Rect, usize) {
506 if area.is_empty() || peaks.is_empty() || color.a <= 0.0 {
507 return (Rect::ZERO, 0);
508 }
509 let width = (area.size.width.ceil() as usize).clamp(1, 8192);
510 let (level, cols) = peaks.query_for_width(start_peak, count, width);
511 if cols.is_empty() {
512 return (Rect::ZERO, level);
513 }
514 let bounds = self.waveform(area, &cols, opts, color);
515 (bounds, level)
516 }
517
518 pub fn waveform_line(
519 &mut self,
520 area: Rect,
521 peaks: &[MinMax],
522 width: f32,
523 color: Color,
524 ) -> Rect {
525 if area.is_empty() || peaks.is_empty() || color.a <= 0.0 {
526 return Rect::ZERO;
527 }
528 let pts = waveform_line_points(area, peaks);
529 if pts.len() < 2 {
530 return Rect::ZERO;
531 }
532 let mut path = Path::new();
533 path.polyline(&pts);
534 let opts = self.raster_options();
535 let shape = self
536 .shapes
537 .stroke(&path, &StrokeStyle::round(width.clamp(0.5, 8.0)), opts);
538 let off = self.draw_offset;
539 let mut rects = Vec::with_capacity(shape.len().min(16384));
540 let mut bounds = Rect::ZERO;
541 for span in shape.iter() {
542 let r = span.rect;
543 let r = if off.x == 0.0 && off.y == 0.0 {
544 r
545 } else {
546 Rect::from_xywh(
547 r.left() + off.x,
548 r.top() + off.y,
549 r.size.width,
550 r.size.height,
551 )
552 };
553 if r.is_empty() {
554 continue;
555 }
556 bounds = bounds.union(r);
557 rects.push(r);
558 if rects.len() >= 16384 {
559 break;
560 }
561 }
562 if rects.is_empty() {
563 return Rect::ZERO;
564 }
565 self.list.push_waveform(&rects, color);
566 bounds
567 }
568
569 pub fn waveform_image(
570 &mut self,
571 area: Rect,
572 peaks: &[MinMax],
573 fg: Color,
574 bg: Color,
575 id: u64,
576 version: u64,
577 ) -> Rect {
578 if area.is_empty() || peaks.is_empty() {
579 return Rect::ZERO;
580 }
581 let w = (area.size.width.ceil() as u32).clamp(8, 4096);
582 let h = (area.size.height.ceil() as u32).clamp(8, 1024);
583 let cols: Vec<MinMax> = if peaks.len() == w as usize {
584 peaks.to_vec()
585 } else {
586 let n = peaks.len();
587 let mut out = Vec::with_capacity(w as usize);
588 for i in 0..(w as usize) {
589 let a = (i * n) / (w as usize);
590 let mut b = ((i + 1) * n) / (w as usize);
591 if b <= a {
592 b = (a + 1).min(n);
593 }
594 let mut m = peaks[a];
595 for &v in &peaks[a + 1..b] {
596 m = m.merge(v);
597 }
598 out.push(m);
599 }
600 out
601 };
602 let data = bake_strip(&cols, w, h, fg, bg);
603 if data.is_empty() {
604 return Rect::ZERO;
605 }
606 let area = self.shifted_rect(area);
607 self.list.push_image(
608 area,
609 [0.0, 0.0, 1.0, 1.0],
610 ImageSource::Dynamic {
611 id,
612 width: w,
613 height: h,
614 data: Rc::from(data.into_boxed_slice()),
615 version,
616 },
617 );
618 area
619 }
620
621 pub fn disk(&mut self, center: Point, radius: f32, color: Color) -> Rect {
622 if !(radius > 0.0) {
623 return Rect::ZERO;
624 }
625 let shape = self.shapes.disk(center, radius);
626 self.draw_shape(&shape, Point::ZERO, color)
627 }
628
629 pub fn rounded_rect(&mut self, rect: Rect, radius: f32, color: Color) -> Rect {
630 let shape = self.shapes.rounded_rect(rect, radius);
631 self.draw_shape(&shape, Point::ZERO, color)
632 }
633
634 pub fn ellipse(&mut self, center: Point, rx: f32, ry: f32, color: Color) -> Rect {
635 if !(rx > 0.0) || !(ry > 0.0) {
636 return Rect::ZERO;
637 }
638 let mut path = Path::new();
639 path.ellipse(center, rx, ry);
640 self.fill(&path, color)
641 }
642
643 pub fn ellipse_outline(
644 &mut self,
645 center: Point,
646 rx: f32,
647 ry: f32,
648 style: &StrokeStyle,
649 color: Color,
650 ) -> Rect {
651 if !(rx > 0.0) || !(ry > 0.0) {
652 return Rect::ZERO;
653 }
654 let mut path = Path::new();
655 path.ellipse(center, rx, ry);
656 self.stroke(&path, style, color)
657 }
658
659 pub fn rect_outline(&mut self, rect: Rect, style: &StrokeStyle, color: Color) -> Rect {
660 if rect.is_empty() {
661 return Rect::ZERO;
662 }
663 let mut path = Path::new();
664 path.rect(rect);
665 self.stroke(&path, style, color)
666 }
667
668 pub fn rounded_rect_outline(
669 &mut self,
670 rect: Rect,
671 radius: f32,
672 style: &StrokeStyle,
673 color: Color,
674 ) -> Rect {
675 if rect.is_empty() {
676 return Rect::ZERO;
677 }
678 let mut path = Path::new();
679 path.rounded_rect(rect, radius);
680 self.stroke(&path, style, color)
681 }
682
683 pub fn rounded_rect_corners(
684 &mut self,
685 rect: Rect,
686 radii: impl Into<Radius>,
687 color: Color,
688 ) -> Rect {
689 if rect.is_empty() {
690 return Rect::ZERO;
691 }
692 let mut path = Path::new();
693 path.rounded_rect(rect, radii);
694 self.fill(&path, color)
695 }
696
697 pub fn rounded_rect_corners_outline(
698 &mut self,
699 rect: Rect,
700 radii: impl Into<Radius>,
701 style: &StrokeStyle,
702 color: Color,
703 ) -> Rect {
704 if rect.is_empty() {
705 return Rect::ZERO;
706 }
707 let mut path = Path::new();
708 path.rounded_rect(rect, radii);
709 self.stroke(&path, style, color)
710 }
711
712 pub fn capsule(&mut self, from: Point, to: Point, radius: f32, color: Color) -> Rect {
713 if !(radius > 0.0) {
714 return Rect::ZERO;
715 }
716 if from == to {
717 return self.disk(from, radius, color);
718 }
719 let style = StrokeStyle::round(radius * 2.0);
720 self.line_styled(from, to, &style, color)
721 }
722
723 pub fn arrow(
724 &mut self,
725 from: Point,
726 to: Point,
727 width: f32,
728 head_len: f32,
729 head_width: f32,
730 color: Color,
731 ) -> Rect {
732 self.arrow_styled(from, to, &line_style(width), head_len, head_width, color)
733 }
734
735 pub fn arrow_styled(
736 &mut self,
737 from: Point,
738 to: Point,
739 style: &StrokeStyle,
740 head_len: f32,
741 head_width: f32,
742 color: Color,
743 ) -> Rect {
744 let dx = to.x - from.x;
745 let dy = to.y - from.y;
746 let len = (dx * dx + dy * dy).sqrt();
747 if !(len > 1e-6) {
748 return Rect::ZERO;
749 }
750 let (ux, uy) = (dx / len, dy / len);
751 let hl = head_len.clamp(0.0, len);
752 let hw = head_width.max(0.0);
753 let mut bounds = Rect::ZERO;
754 if hl <= 0.0 || hw <= 0.0 {
755 return self.line_styled(from, to, style, color);
756 }
757 let base = Point::new(to.x - ux * hl, to.y - uy * hl);
758 if len - hl > 1e-4 {
759 bounds = bounds.union(self.line_styled(from, base, style, color));
760 }
761 let (px, py) = (-uy, ux);
762 let mut head = Path::new();
763 head.polygon(&[
764 to,
765 Point::new(base.x + px * hw * 0.5, base.y + py * hw * 0.5),
766 Point::new(base.x - px * hw * 0.5, base.y - py * hw * 0.5),
767 ]);
768 bounds.union(self.fill(&head, color))
769 }
770
771 pub fn cross(&mut self, center: Point, half: f32, width: f32, color: Color) -> Rect {
772 if !(half > 0.0) {
773 return Rect::ZERO;
774 }
775 let style = StrokeStyle::round(width);
776 let h = self.line_styled(
777 Point::new(center.x - half, center.y),
778 Point::new(center.x + half, center.y),
779 &style,
780 color,
781 );
782 let v = self.line_styled(
783 Point::new(center.x, center.y - half),
784 Point::new(center.x, center.y + half),
785 &style,
786 color,
787 );
788 h.union(v)
789 }
790
791 pub fn x_marker(&mut self, center: Point, half: f32, width: f32, color: Color) -> Rect {
792 if !(half > 0.0) {
793 return Rect::ZERO;
794 }
795 let style = StrokeStyle::round(width);
796 let a = self.line_styled(
797 Point::new(center.x - half, center.y - half),
798 Point::new(center.x + half, center.y + half),
799 &style,
800 color,
801 );
802 let b = self.line_styled(
803 Point::new(center.x - half, center.y + half),
804 Point::new(center.x + half, center.y - half),
805 &style,
806 color,
807 );
808 a.union(b)
809 }
810
811 pub fn regular_polygon(
812 &mut self,
813 center: Point,
814 radius: f32,
815 sides: u32,
816 rotation_deg: f32,
817 color: Color,
818 ) -> Rect {
819 let pts = regular_corners(center, radius, sides, rotation_deg);
820 if pts.is_empty() {
821 return Rect::ZERO;
822 }
823 self.polygon(&pts, color)
824 }
825
826 pub fn regular_polygon_outline(
827 &mut self,
828 center: Point,
829 radius: f32,
830 sides: u32,
831 rotation_deg: f32,
832 style: &StrokeStyle,
833 color: Color,
834 ) -> Rect {
835 let pts = regular_corners(center, radius, sides, rotation_deg);
836 if pts.is_empty() {
837 return Rect::ZERO;
838 }
839 let mut path = Path::new();
840 path.polygon(&pts);
841 self.stroke(&path, style, color)
842 }
843
844 pub fn star(
845 &mut self,
846 center: Point,
847 outer_r: f32,
848 inner_r: f32,
849 points: u32,
850 rotation_deg: f32,
851 color: Color,
852 ) -> Rect {
853 let pts = star_corners(center, outer_r, inner_r, points, rotation_deg);
854 if pts.is_empty() {
855 return Rect::ZERO;
856 }
857 self.polygon(&pts, color)
858 }
859
860 pub fn star_outline(
861 &mut self,
862 center: Point,
863 outer_r: f32,
864 inner_r: f32,
865 points: u32,
866 rotation_deg: f32,
867 style: &StrokeStyle,
868 color: Color,
869 ) -> Rect {
870 let pts = star_corners(center, outer_r, inner_r, points, rotation_deg);
871 if pts.is_empty() {
872 return Rect::ZERO;
873 }
874 let mut path = Path::new();
875 path.polygon(&pts);
876 self.stroke(&path, style, color)
877 }
878
879 pub fn ring(&mut self, center: Point, radius: f32, width: f32, color: Color) -> Rect {
880 let mut path = Path::new();
881 path.circle(center, radius);
882 self.stroke(&path, &StrokeStyle::new(width), color)
883 }
884
885 pub fn arc(
886 &mut self,
887 center: Point,
888 radius: f32,
889 from_deg: f32,
890 to_deg: f32,
891 width: f32,
892 color: Color,
893 ) -> Rect {
894 let mut path = Path::new();
895 path.arc(center, radius, from_deg, to_deg);
896 self.stroke(&path, &line_style(width), color)
897 }
898
899 pub fn sector(
900 &mut self,
901 center: Point,
902 radius: f32,
903 from_deg: f32,
904 to_deg: f32,
905 color: Color,
906 ) -> Rect {
907 if !(radius > 0.0) || !from_deg.is_finite() || !to_deg.is_finite() {
908 return Rect::ZERO;
909 }
910 if (to_deg - from_deg).abs() < 1e-6 {
911 return Rect::ZERO;
912 }
913 let mut path = Path::new();
914 path.move_to(center);
915 path.arc(center, radius, from_deg, to_deg);
916 path.close();
917 self.fill(&path, color)
918 }
919
920 pub fn donut_sector(
921 &mut self,
922 center: Point,
923 inner_r: f32,
924 outer_r: f32,
925 from_deg: f32,
926 to_deg: f32,
927 color: Color,
928 ) -> Rect {
929 if !(outer_r > 0.0) || !from_deg.is_finite() || !to_deg.is_finite() {
930 return Rect::ZERO;
931 }
932 if (to_deg - from_deg).abs() < 1e-6 {
933 return Rect::ZERO;
934 }
935 let inner = if inner_r.is_finite() {
936 inner_r.clamp(0.0, outer_r)
937 } else {
938 0.0
939 };
940 if inner <= 0.0 {
941 return self.sector(center, outer_r, from_deg, to_deg, color);
942 }
943 let (a, b) = (from_deg.to_radians(), to_deg.to_radians());
944 let outer_from = Point::new(center.x + a.cos() * outer_r, center.y + a.sin() * outer_r);
945 let inner_to = Point::new(center.x + b.cos() * inner, center.y + b.sin() * inner);
946 let mut path = Path::new();
947 path.move_to(outer_from);
948 path.arc(center, outer_r, from_deg, to_deg);
949 path.line_to(inner_to);
950 path.arc(center, inner, to_deg, from_deg);
951 path.close();
952 self.fill(&path, color)
953 }
954
955 pub fn line(&mut self, from: Point, to: Point, width: f32, color: Color) -> Rect {
956 self.line_styled(from, to, &line_style(width), color)
957 }
958
959 pub fn line_crisp(&mut self, from: Point, to: Point, width: f32, color: Color) -> Rect {
960 let (from, to) = crisp_axis_line(from, to, width, self.scale_factor());
961 self.line(from, to, width, color)
962 }
963
964 pub fn dotted_line(
965 &mut self,
966 from: Point,
967 to: Point,
968 width: f32,
969 spacing: f32,
970 color: Color,
971 ) -> Rect {
972 let radius = width * 0.5;
973 if !(radius > 0.0) || !(spacing > 0.0) || !spacing.is_finite() {
974 return Rect::ZERO;
975 }
976 let dx = to.x - from.x;
977 let dy = to.y - from.y;
978 let len = (dx * dx + dy * dy).sqrt();
979 if !(len > 1e-6) {
980 return self.disk(from, radius, color);
981 }
982
983 const MAX_DOTS: usize = 4096;
984 let count = ((len / spacing).floor() as usize).min(MAX_DOTS);
985 let mut bounds = Rect::ZERO;
986 for i in 0..=count {
987 let t = (i as f32 * spacing).min(len);
988 let k = t / len;
989 let p = Point::new(from.x + dx * k, from.y + dy * k);
990 bounds = bounds.union(self.disk(p, radius, color));
991 }
992
993 if (count as f32 * spacing - len).abs() > 1e-3 {
994 bounds = bounds.union(self.disk(to, radius, color));
995 }
996 bounds
997 }
998
999 pub fn line_styled(
1000 &mut self,
1001 from: Point,
1002 to: Point,
1003 style: &StrokeStyle,
1004 color: Color,
1005 ) -> Rect {
1006 let mut path = Path::new();
1007 path.move_to(from).line_to(to);
1008 self.stroke(&path, style, color)
1009 }
1010
1011 pub fn dashed_line(
1012 &mut self,
1013 from: Point,
1014 to: Point,
1015 width: f32,
1016 dash: impl Into<Dash>,
1017 color: Color,
1018 ) -> Rect {
1019 let style = line_style(width).dash(dash);
1020 self.line_styled(from, to, &style, color)
1021 }
1022
1023 pub fn outline(&mut self, points: &[Point], style: &StrokeStyle, color: Color) -> Rect {
1024 let mut path = Path::new();
1025 path.polyline(points);
1026 self.stroke(&path, style, color)
1027 }
1028
1029 pub fn polygon(&mut self, points: &[Point], color: Color) -> Rect {
1030 let mut path = Path::new();
1031 path.polygon(points);
1032 self.fill(&path, color)
1033 }
1034
1035 pub fn spline(
1036 &mut self,
1037 points: &[Point],
1038 tension: f32,
1039 style: &StrokeStyle,
1040 color: Color,
1041 ) -> Rect {
1042 let mut path = Path::new();
1043 path.spline(points, tension);
1044 self.stroke(&path, style, color)
1045 }
1046
1047 pub fn curve(
1048 &mut self,
1049 from: Point,
1050 c1: Point,
1051 c2: Point,
1052 to: Point,
1053 style: &StrokeStyle,
1054 color: Color,
1055 ) -> Rect {
1056 let mut path = Path::new();
1057 path.move_to(from).cubic_to(c1, c2, to);
1058 self.stroke(&path, style, color)
1059 }
1060
1061 pub fn curve_quad(
1062 &mut self,
1063 from: Point,
1064 control: Point,
1065 to: Point,
1066 style: &StrokeStyle,
1067 color: Color,
1068 ) -> Rect {
1069 let mut path = Path::new();
1070 path.move_to(from).quad_to(control, to);
1071 self.stroke(&path, style, color)
1072 }
1073
1074 pub fn draw_svg(&mut self, data: &str, style: Option<&StrokeStyle>, color: Color) -> Rect {
1075 let Some(path) = self.path_from_svg(data) else {
1076 return Rect::ZERO;
1077 };
1078 match style {
1079 Some(style) => self.stroke(&path, style, color),
1080 None => self.fill(&path, color),
1081 }
1082 }
1083
1084 pub fn draw_transformed(
1085 &mut self,
1086 path: &Path,
1087 transform: Transform,
1088 style: Option<&StrokeStyle>,
1089 color: Color,
1090 ) -> Rect {
1091 let path = path.transformed(transform);
1092 match style {
1093 Some(style) => self.stroke(&path, style, color),
1094 None => self.fill(&path, color),
1095 }
1096 }
1097
1098 fn shifted_rect(&self, rect: Rect) -> Rect {
1099 if self.draw_offset.x == 0.0 && self.draw_offset.y == 0.0 {
1100 rect
1101 } else {
1102 Rect::from_xywh(
1103 rect.origin.x + self.draw_offset.x,
1104 rect.origin.y + self.draw_offset.y,
1105 rect.size.width,
1106 rect.size.height,
1107 )
1108 }
1109 }
1110}
1111
1112impl PaintCtx<'_> {
1113 fn band_px(&self, px: f32) -> f32 {
1114 let scale = self.scale_factor();
1115 let scale = if scale.is_finite() && scale > 0.0 {
1116 scale
1117 } else {
1118 1.0
1119 };
1120 px.max(0.05) / scale
1121 }
1122}
1123
1124fn shadow_radius(radius: f32, rect: Rect) -> f32 {
1125 radius
1126 .clamp(0.0, rect.size.width.min(rect.size.height) * 0.5)
1127 .max(0.0)
1128}
1129
1130fn crisp_axis_line(from: Point, to: Point, width: f32, scale: f32) -> (Point, Point) {
1131 let s = if scale.is_finite() && scale > 0.0 {
1132 scale
1133 } else {
1134 1.0
1135 };
1136 let px = 1.0 / s;
1137 let width_px = width * s;
1138 if !width.is_finite() || width <= 0.0 || (width_px - width_px.round()).abs() > 0.01 {
1139 return (from, to);
1140 }
1141 let half = if (width_px.round() as i64) % 2 != 0 {
1142 px * 0.5
1143 } else {
1144 0.0
1145 };
1146 let dx = (to.x - from.x).abs();
1147 let dy = (to.y - from.y).abs();
1148 if dy <= 1e-4 && dx > 1e-4 {
1149 let y = (from.y / px).round() * px + half;
1150 (Point::new(from.x, y), Point::new(to.x, y))
1151 } else if dx <= 1e-4 && dy > 1e-4 {
1152 let x = (from.x / px).round() * px + half;
1153 (Point::new(x, from.y), Point::new(x, to.y))
1154 } else {
1155 (from, to)
1156 }
1157}
1158
1159fn shift(rect: Rect, offset: Vec2) -> Rect {
1160 if offset.x == 0.0 && offset.y == 0.0 {
1161 rect
1162 } else {
1163 Rect::from_xywh(
1164 rect.origin.x + offset.x,
1165 rect.origin.y + offset.y,
1166 rect.size.width,
1167 rect.size.height,
1168 )
1169 }
1170}
1171
1172fn tint(color: Color, alpha: f32) -> Color {
1173 Color {
1174 a: color.a * alpha,
1175 ..color
1176 }
1177}
1178
1179fn regular_corners(center: Point, radius: f32, sides: u32, rotation_deg: f32) -> Vec<Point> {
1180 if !(radius > 0.0) {
1181 return Vec::new();
1182 }
1183 let n = sides.clamp(3, 64);
1184 let base = rotation_deg.to_radians();
1185 (0..n)
1186 .map(|i| {
1187 let a = base + i as f32 * std::f32::consts::TAU / n as f32;
1188 Point::new(center.x + a.cos() * radius, center.y + a.sin() * radius)
1189 })
1190 .collect()
1191}
1192
1193fn star_corners(
1194 center: Point,
1195 outer_r: f32,
1196 inner_r: f32,
1197 points: u32,
1198 rotation_deg: f32,
1199) -> Vec<Point> {
1200 if !(outer_r > 0.0) || !(inner_r > 0.0) {
1201 return Vec::new();
1202 }
1203
1204 let n = points.clamp(2, 64);
1205 let base = rotation_deg.to_radians();
1206 (0..2 * n)
1207 .map(|i| {
1208 let r = if i % 2 == 0 { outer_r } else { inner_r };
1209 let a = base + i as f32 * std::f32::consts::PI / n as f32;
1210 Point::new(center.x + a.cos() * r, center.y + a.sin() * r)
1211 })
1212 .collect()
1213}
1214
1215#[cfg(test)]
1216mod tests {
1217 use super::*;
1218
1219 #[test]
1220 fn gradient_bands_capped_per_span() {
1221 assert_eq!(gradient_band_count(10.0, 1.0), 10);
1222 assert_eq!(
1223 gradient_band_count(800.0, 1.0),
1224 GRADIENT_BANDS_MAX,
1225 "800px cover is max 256 quads per span, not 800"
1226 );
1227 assert_eq!(gradient_band_count(0.0, 1.0), 1, "zawsze co najmniej 1");
1228 assert_eq!(gradient_band_count(-5.0, 1.0), 1);
1229 assert_eq!(
1230 gradient_band_count(100.0, 0.0),
1231 gradient_band_count(100.0, GRADIENT_BAND_PX),
1232 "bad band returns to default"
1233 );
1234 }
1235
1236 #[test]
1237 fn gradient_bands_cover_span_on_fractional_origin() {
1238 let e = band_edges(100.5, 198.0, 198, 1.0, 0.5);
1239 assert_eq!(e.len(), 198);
1240 assert!((e[0].0 - 100.5).abs() < 1e-6, "pierwszy pas od originu");
1241 assert!(
1242 (e.last().unwrap().1 - 298.5).abs() < 1e-6,
1243 "last to the end"
1244 );
1245 for (i, w) in e.windows(2).enumerate() {
1246 let phi = w[0].1;
1247 let (lo, hi) = (w[1].0, w[1].1);
1248 assert!(lo <= phi + 1e-4, "dziura przed pasem {i}");
1249 assert!((lo - lo.round()).abs() < 1e-4, "edge {i} outside mesh");
1250
1251 assert!(hi - lo > 0.0 && hi - lo <= 2.6, "wrong belt size {i}");
1252 }
1253
1254 let e2 = band_edges(100.25, 50.0, 50, 0.5, 0.25);
1255 assert_eq!(e2.len(), 50);
1256 for w in e2.windows(2) {
1257 assert!(w[1].0 <= w[0].1 + 1e-4);
1258 assert!(((w[1].0 * 2.0).round() - w[1].0 * 2.0).abs() < 1e-3);
1259 }
1260 }
1261
1262 #[test]
1263 fn star_corner_count_is_clamped() {
1264 let c = Point::new(0.0, 0.0);
1265 assert_eq!(
1266 star_corners(c, 10.0, 5.0, u32::MAX, 0.0).len(),
1267 128,
1268 "top clamp 64 arms"
1269 );
1270 assert_eq!(star_corners(c, 10.0, 5.0, 65, 0.0).len(), 128);
1271 assert_eq!(star_corners(c, 10.0, 5.0, 5, 0.0).len(), 10);
1272 assert_eq!(star_corners(c, 10.0, 5.0, 1, 0.0).len(), 4, "dolny clamp 2");
1273 assert_eq!(star_corners(c, 10.0, 5.0, 0, 0.0).len(), 4);
1274 }
1275}