Skip to main content

sequoia_view/
paint_draw.rs

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}