Skip to main content

presentar_widgets/
waveform.rs

1//! Waveform widget: a realtime audio waveform drawn as one polyline.
2//!
3//! This is the pure half of the audio display. [`waveform_points`] maps a
4//! buffer of samples to canvas points and is total: any input (NaN, infinity,
5//! out-of-range values, empty buffers, degenerate bounds) yields points that
6//! lie inside the target rectangle. [`Waveform`] owns a sample buffer and
7//! paints that path. Capturing samples (microphone, Web Audio) is a separate
8//! concern and not part of this module.
9//!
10//! Contract: `contracts/present-waveform-path-v1.yaml`.
11
12use presentar_core::{
13    widget::{AccessibleRole, LayoutResult},
14    Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas, Color, Constraints, Event,
15    Point, Rect, Size, TypeId, Widget,
16};
17use std::any::Any;
18use std::time::Duration;
19
20/// Default upper bound on the number of points a [`Waveform`] draws.
21pub const DEFAULT_MAX_POINTS: usize = 1024;
22
23/// Treat NaN and infinities as silence; clamp everything else to [-1, 1].
24fn sanitize(sample: f32) -> f32 {
25    if sample.is_finite() {
26        sample.clamp(-1.0, 1.0)
27    } else {
28        0.0
29    }
30}
31
32/// Largest magnitude accepted for any coordinate or extent. Larger finite
33/// values are clamped so that `x + width` can never overflow to infinity.
34const COORD_LIMIT: f32 = 1.0e15;
35
36/// A finite coordinate: NaN and infinities become 0, huge values are clamped.
37fn sane(value: f32) -> f32 {
38    if value.is_finite() {
39        value.clamp(-COORD_LIMIT, COORD_LIMIT)
40    } else {
41        0.0
42    }
43}
44
45/// `bounds` made safe to compute with: every component finite and within
46/// `COORD_LIMIT`, and the size not negative. Valid bounds in range pass through
47/// unchanged.
48fn sane_bounds(bounds: Rect) -> Rect {
49    Rect::new(
50        sane(bounds.x),
51        sane(bounds.y),
52        sane(bounds.width).max(0.0),
53        sane(bounds.height).max(0.0),
54    )
55}
56
57/// Whether `bounds` is finite with a size that is not negative.
58fn valid_bounds(bounds: Rect) -> bool {
59    [bounds.x, bounds.y, bounds.width, bounds.height]
60        .iter()
61        .all(|v| v.is_finite())
62        && bounds.width >= 0.0
63        && bounds.height >= 0.0
64}
65
66/// A usable preferred extent: finite values are clamped into
67/// `[0, COORD_LIMIT]`; NaN and infinities count as "not set".
68fn sane_extent(value: f32) -> Option<f32> {
69    value.is_finite().then(|| value.clamp(0.0, COORD_LIMIT))
70}
71
72/// Fit `value` between `min` and `max` and keep it finite. Unlike
73/// `f32::clamp` this never panics, whatever the constraints (NaN, infinite or
74/// `min > max`); a lower bound that exceeds the upper one loses.
75fn fit(value: f32, min: f32, max: f32) -> f32 {
76    sane(value.max(min).min(max)).max(0.0)
77}
78
79/// Keep whichever of the two samples has the larger magnitude (first wins ties).
80fn larger_magnitude(best: f32, candidate: f32) -> f32 {
81    if candidate.abs() > best.abs() {
82        candidate
83    } else {
84        best
85    }
86}
87
88/// Start index of `bucket` out of `target`, computed in `u128` so that
89/// 32-bit `usize` (wasm32) cannot saturate the product.
90fn bucket_edge(bucket: usize, len: usize, target: usize) -> usize {
91    let edge = bucket_edge_wide(bucket as u64, len as u64, target as u64);
92    usize::try_from(edge).map_or(len, |e| e.min(len))
93}
94
95/// Width-independent core of [`bucket_edge`]: exact in `u128`, clamped to `len`.
96fn bucket_edge_wide(bucket: u64, len: u64, target: u64) -> u64 {
97    let edge = (u128::from(bucket) * u128::from(len)) / u128::from(target);
98    u64::try_from(edge).map_or(len, |e| e.min(len))
99}
100
101/// Reduce `samples` to between 2 and `max(max_points, 2)` sanitized levels.
102///
103/// With more samples than the target, each bucket keeps its sample of largest
104/// magnitude so that peaks survive. Fewer than two levels (silence, or a lone
105/// sample) become a flat two-level line.
106fn reduce_levels(samples: &[f32], max_points: usize) -> Vec<f32> {
107    let target = max_points.max(2);
108    let len = samples.len();
109    let levels: Vec<f32> = if len <= target {
110        samples.iter().copied().map(sanitize).collect()
111    } else {
112        (0..target)
113            .map(|bucket| {
114                let start = bucket_edge(bucket, len, target);
115                let end = bucket_edge(bucket + 1, len, target);
116                samples
117                    .get(start..end)
118                    .unwrap_or_default()
119                    .iter()
120                    .copied()
121                    .map(sanitize)
122                    .fold(0.0, larger_magnitude)
123            })
124            .collect()
125    };
126    if levels.len() < 2 {
127        let level = levels.first().copied().unwrap_or(0.0);
128        return vec![level, level];
129    }
130    levels
131}
132
133/// Map audio samples to a polyline inside `bounds`.
134///
135/// * Samples are amplitudes nominally in [-1, 1]; larger magnitudes are
136///   clamped, NaN and infinities count as 0.
137/// * x runs from `bounds.x` (first point) to `bounds.x + bounds.width` (last
138///   point); `y = centre - sample * (height / 2)`, so +1 is the top edge, -1
139///   the bottom edge and 0 the centre line.
140/// * More samples than `max_points` are reduced by buckets, keeping the
141///   largest-magnitude sample per bucket. The result never has more than
142///   `max(max_points, 2)` points.
143/// * No samples (silence) gives the centre line: two points, left-centre and
144///   right-centre. A lone sample gives a flat line at its level.
145/// * Every point is finite and lies inside `bounds` (edges inclusive), for any
146///   input. Negative width/height count as 0, non-finite components as 0, and
147///   components beyond +-1e15 are clamped to it so that no sum overflows; the
148///   "inside" promise is then to those sanitized bounds, which equal `bounds`
149///   whenever it is finite, not negative and within +-1e15.
150#[must_use]
151pub fn waveform_points(samples: &[f32], bounds: Rect, max_points: usize) -> Vec<Point> {
152    let levels = reduce_levels(samples, max_points);
153    let safe = sane_bounds(bounds);
154    let (x0, y0, width, height) = (safe.x, safe.y, safe.width, safe.height);
155    let y1 = y0 + height;
156    let half = height / 2.0;
157    let centre = y0 + half;
158    let span = levels.len().saturating_sub(1).max(1) as f32;
159
160    levels
161        .iter()
162        .enumerate()
163        .map(|(index, &level)| {
164            let t = index as f32 / span;
165            // t is in [0, 1] and width >= 0, so x stays in [x0, x0 + width].
166            let x = t.mul_add(width, x0);
167            let y = (-level).mul_add(half, centre).clamp(y0, y1);
168            Point::new(x, y)
169        })
170        .collect()
171}
172
173/// Escape text for an HTML attribute value.
174fn escape_attr(text: &str) -> String {
175    text.replace('&', "&amp;")
176        .replace('"', "&quot;")
177        .replace('<', "&lt;")
178        .replace('>', "&gt;")
179}
180
181/// Realtime audio waveform widget.
182#[derive(Debug, Clone)]
183pub struct Waveform {
184    /// Raw sample buffer (amplitudes, nominally in [-1, 1])
185    samples: Vec<f32>,
186    /// Line colour
187    color: Color,
188    /// Line width in pixels
189    line_width: f32,
190    /// Upper bound on drawn points
191    max_points: usize,
192    /// Preferred width
193    width: Option<f32>,
194    /// Preferred height
195    height: Option<f32>,
196    /// Accessible name
197    accessible_name_value: Option<String>,
198    /// Test ID
199    test_id_value: Option<String>,
200    /// Cached bounds
201    bounds: Rect,
202}
203
204impl Default for Waveform {
205    fn default() -> Self {
206        Self {
207            samples: Vec::new(),
208            color: Color::new(0.1, 0.6, 0.9, 1.0),
209            line_width: 1.5,
210            max_points: DEFAULT_MAX_POINTS,
211            width: None,
212            height: None,
213            accessible_name_value: None,
214            test_id_value: None,
215            bounds: Rect::default(),
216        }
217    }
218}
219
220impl Waveform {
221    /// Create an empty waveform.
222    #[must_use]
223    pub fn new() -> Self {
224        Self::default()
225    }
226
227    /// Replace the sample buffer (builder form).
228    #[must_use]
229    pub fn samples(mut self, samples: Vec<f32>) -> Self {
230        self.samples = samples;
231        self
232    }
233
234    /// Replace the sample buffer in place; call this once per frame.
235    pub fn set_samples(&mut self, samples: &[f32]) {
236        self.samples.clear();
237        self.samples.extend_from_slice(samples);
238    }
239
240    /// Set the line colour.
241    #[must_use]
242    pub fn color(mut self, color: Color) -> Self {
243        self.color = color;
244        self
245    }
246
247    /// Set the line width (negative values become 0; NaN and infinities become
248    /// 0 and huge values are clamped to +-1e15, so the stroke is always finite).
249    #[must_use]
250    pub fn line_width(mut self, width: f32) -> Self {
251        self.line_width = sane(width).max(0.0);
252        self
253    }
254
255    /// Set the upper bound on drawn points.
256    #[must_use]
257    pub fn max_points(mut self, max_points: usize) -> Self {
258        self.max_points = max_points;
259        self
260    }
261
262    /// Set the preferred width.
263    ///
264    /// Negative values become 0 and values above 1e15 are clamped to it; NaN
265    /// and infinities count as "not set", so `measure` falls back to the
266    /// default width instead of reporting a non-finite size.
267    #[must_use]
268    pub fn width(mut self, width: f32) -> Self {
269        self.width = sane_extent(width);
270        self
271    }
272
273    /// Set the preferred height (same rules as [`Waveform::width`]).
274    #[must_use]
275    pub fn height(mut self, height: f32) -> Self {
276        self.height = sane_extent(height);
277        self
278    }
279
280    /// Set both preferred width and height.
281    #[must_use]
282    pub fn size(self, width: f32, height: f32) -> Self {
283        self.width(width).height(height)
284    }
285
286    /// Set the accessible name.
287    #[must_use]
288    pub fn accessible_name(mut self, name: impl Into<String>) -> Self {
289        self.accessible_name_value = Some(name.into());
290        self
291    }
292
293    /// Set the test ID.
294    #[must_use]
295    pub fn test_id(mut self, id: impl Into<String>) -> Self {
296        self.test_id_value = Some(id.into());
297        self
298    }
299
300    /// The raw sample buffer.
301    #[must_use]
302    pub fn get_samples(&self) -> &[f32] {
303        &self.samples
304    }
305
306    /// The line colour.
307    #[must_use]
308    pub const fn get_color(&self) -> Color {
309        self.color
310    }
311
312    /// The line width.
313    #[must_use]
314    pub fn get_line_width(&self) -> f32 {
315        sane(self.line_width).max(0.0)
316    }
317
318    /// The upper bound on drawn points.
319    #[must_use]
320    pub const fn get_max_points(&self) -> usize {
321        self.max_points
322    }
323
324    /// Largest magnitude among the samples after clamping; 0.0 when empty.
325    #[must_use]
326    pub fn peak(&self) -> f32 {
327        self.samples
328            .iter()
329            .copied()
330            .map(sanitize)
331            .fold(0.0, |best, v| best.max(v.abs()))
332    }
333
334    /// Root mean square of the samples after clamping; 0.0 when empty.
335    #[must_use]
336    pub fn rms(&self) -> f32 {
337        if self.samples.is_empty() {
338            return 0.0;
339        }
340        let sum: f64 = self
341            .samples
342            .iter()
343            .copied()
344            .map(sanitize)
345            .map(|v| f64::from(v) * f64::from(v))
346            .sum();
347        (sum / self.samples.len() as f64).sqrt() as f32
348    }
349
350    /// Whether a non-blank accessible name has been set.
351    fn has_accessible_name(&self) -> bool {
352        self.accessible_name_value
353            .as_deref()
354            .is_some_and(|name| !name.trim().is_empty())
355    }
356}
357
358impl Widget for Waveform {
359    fn type_id(&self) -> TypeId {
360        TypeId::of::<Self>()
361    }
362
363    fn measure(&self, constraints: Constraints) -> Size {
364        // Not `Constraints::constrain`: its `f32::clamp` panics on NaN or
365        // inverted constraints and passes an infinite minimum through.
366        Size::new(
367            fit(
368                self.width.unwrap_or(200.0),
369                constraints.min_width,
370                constraints.max_width,
371            ),
372            fit(
373                self.height.unwrap_or(80.0),
374                constraints.min_height,
375                constraints.max_height,
376            ),
377        )
378    }
379
380    fn layout(&mut self, bounds: Rect) -> LayoutResult {
381        self.bounds = bounds;
382        LayoutResult {
383            size: sane_bounds(bounds).size(),
384        }
385    }
386
387    fn paint(&self, canvas: &mut dyn Canvas) {
388        let points = waveform_points(&self.samples, self.bounds, self.max_points);
389        canvas.draw_path(&points, self.color, self.get_line_width());
390    }
391
392    fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
393        None
394    }
395
396    fn children(&self) -> &[Box<dyn Widget>] {
397        &[]
398    }
399
400    fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
401        &mut []
402    }
403
404    fn accessible_name(&self) -> Option<&str> {
405        self.accessible_name_value.as_deref()
406    }
407
408    fn accessible_role(&self) -> AccessibleRole {
409        AccessibleRole::Image
410    }
411
412    fn test_id(&self) -> Option<&str> {
413        self.test_id_value.as_deref()
414    }
415
416    fn bounds(&self) -> Rect {
417        self.bounds
418    }
419}
420
421// PROBAR-SPEC-009: Brick Architecture - Tests define interface
422impl Brick for Waveform {
423    fn brick_name(&self) -> &'static str {
424        "Waveform"
425    }
426
427    fn assertions(&self) -> &[BrickAssertion] {
428        &[BrickAssertion::MaxLatencyMs(16)]
429    }
430
431    fn budget(&self) -> BrickBudget {
432        BrickBudget::uniform(16)
433    }
434
435    fn verify(&self) -> BrickVerification {
436        let mut passed = self.assertions().to_vec();
437        let mut failed = Vec::new();
438
439        // A waveform has no text of its own: screen readers need a name.
440        let named = BrickAssertion::ElementPresent("accessible-name".to_string());
441        if self.has_accessible_name() {
442            passed.push(named);
443        } else {
444            failed.push((named, "Waveform has no accessible name".to_string()));
445        }
446
447        // The laid-out bounds must be finite and not negative.
448        let bounds_ok = BrickAssertion::ElementPresent("valid-bounds".to_string());
449        if valid_bounds(self.bounds) {
450            passed.push(bounds_ok);
451        } else {
452            failed.push((
453                bounds_ok,
454                "Waveform bounds are not finite and >= 0".to_string(),
455            ));
456        }
457
458        BrickVerification {
459            passed,
460            failed,
461            verification_time: Duration::from_micros(10),
462        }
463    }
464
465    fn to_html(&self) -> String {
466        let label = escape_attr(self.accessible_name_value.as_deref().unwrap_or(""));
467        format!(r#"<canvas class="brick-waveform" role="img" aria-label="{label}"></canvas>"#)
468    }
469
470    fn to_css(&self) -> String {
471        ".brick-waveform { display: block; }".to_string()
472    }
473
474    fn test_id(&self) -> Option<&str> {
475        self.test_id_value.as_deref()
476    }
477}
478
479#[cfg(test)]
480#[allow(clippy::unwrap_used, clippy::disallowed_methods)]
481mod tests {
482    use super::*;
483    use presentar_core::draw::DrawCommand;
484    use presentar_core::RecordingCanvas;
485    use proptest::prelude::*;
486
487    const B: Rect = Rect {
488        x: 10.0,
489        y: 20.0,
490        width: 100.0,
491        height: 40.0,
492    };
493
494    fn centre_y(b: Rect) -> f32 {
495        b.y + b.height / 2.0
496    }
497
498    fn inside(p: &Point, b: Rect) -> bool {
499        p.x >= b.x && p.x <= b.x + b.width && p.y >= b.y && p.y <= b.y + b.height
500    }
501
502    // ===== waveform_points =====
503
504    #[test]
505    fn points_empty_is_centre_line() {
506        let pts = waveform_points(&[], B, 64);
507        assert_eq!(pts.len(), 2);
508        assert_eq!(pts[0], Point::new(10.0, 40.0));
509        assert_eq!(pts[1], Point::new(110.0, 40.0));
510    }
511
512    #[test]
513    fn points_all_zero_on_centre() {
514        let pts = waveform_points(&[0.0; 50], B, 64);
515        assert_eq!(pts.len(), 50);
516        assert!(pts.iter().all(|p| p.y == centre_y(B)));
517    }
518
519    #[test]
520    fn points_map_extremes_to_edges() {
521        let pts = waveform_points(&[1.0, 0.0, -1.0], B, 64);
522        assert_eq!(pts[0].y, 20.0); // +1 = top edge
523        assert_eq!(pts[1].y, 40.0); // 0 = centre
524        assert_eq!(pts[2].y, 60.0); // -1 = bottom edge
525    }
526
527    #[test]
528    fn points_x_spans_bounds() {
529        let pts = waveform_points(&[0.1, 0.2, 0.3, 0.4, 0.5], B, 64);
530        assert_eq!(pts[0].x, 10.0);
531        assert_eq!(pts[4].x, 110.0);
532        assert_eq!(pts[2].x, 60.0);
533    }
534
535    #[test]
536    fn points_clamp_out_of_range() {
537        let pts = waveform_points(&[5.0, -5.0, f32::MAX, f32::MIN], B, 64);
538        assert_eq!(pts[0].y, 20.0);
539        assert_eq!(pts[1].y, 60.0);
540        assert_eq!(pts[2].y, 20.0);
541        assert_eq!(pts[3].y, 60.0);
542    }
543
544    #[test]
545    fn points_nonfinite_is_silence() {
546        let pts = waveform_points(&[f32::NAN, f32::INFINITY, f32::NEG_INFINITY], B, 64);
547        assert!(pts.iter().all(|p| p.y == centre_y(B)));
548    }
549
550    #[test]
551    fn points_downsample_keeps_peaks() {
552        let mut samples = vec![0.1_f32; 100];
553        samples[37] = -0.9;
554        let bounds = Rect::new(0.0, 0.0, 100.0, 20.0);
555        let pts = waveform_points(&samples, bounds, 10);
556        assert_eq!(pts.len(), 10);
557        // -0.9 maps to y = 10 + 0.9 * 10 = 19
558        assert!(pts.iter().any(|p| (p.y - 19.0).abs() < 1e-4));
559        // the bucket holding the spike is not the spike's neighbour bucket
560        assert!((pts[3].y - 19.0).abs() < 1e-4);
561        assert!((pts[2].y - 9.0).abs() < 1e-4);
562    }
563
564    #[test]
565    fn points_downsample_largest_magnitude_ignores_sign() {
566        // bucket of 2: -0.5 then 0.8 -> keeps 0.8 (larger magnitude), then
567        // 0.8 then -0.5 -> keeps 0.8 (first wins)
568        let pts = waveform_points(&[-0.5, 0.8, 0.8, -0.5, 0.0, 0.0], B, 3);
569        assert_eq!(pts.len(), 3);
570        let top = centre_y(B) - 0.8 * 20.0;
571        assert!((pts[0].y - top).abs() < 1e-4);
572        assert!((pts[1].y - top).abs() < 1e-4);
573        assert_eq!(pts[2].y, centre_y(B));
574    }
575
576    #[test]
577    fn points_max_points_zero_and_one_do_not_panic() {
578        for max in [0_usize, 1] {
579            let pts = waveform_points(&[0.5, -0.5, 0.25, 0.0], B, max);
580            assert_eq!(pts.len(), 2);
581            assert!(pts.iter().all(|p| inside(p, B)));
582            let none = waveform_points(&[], B, max);
583            assert_eq!(none.len(), 2);
584        }
585    }
586
587    #[test]
588    fn points_huge_max_points_is_all_samples() {
589        let pts = waveform_points(&[0.1, 0.2, 0.3], B, usize::MAX);
590        assert_eq!(pts.len(), 3);
591    }
592
593    #[test]
594    fn points_single_sample_is_flat_line() {
595        let pts = waveform_points(&[0.5], B, 64);
596        assert_eq!(pts.len(), 2);
597        assert_eq!(pts[0].y, pts[1].y);
598        assert_eq!(pts[0].x, 10.0);
599        assert_eq!(pts[1].x, 110.0);
600    }
601
602    #[test]
603    fn points_degenerate_bounds_stay_inside() {
604        for b in [
605            Rect::new(5.0, 7.0, 0.0, 0.0),
606            Rect::new(5.0, 7.0, 0.0, 30.0),
607            Rect::new(5.0, 7.0, 30.0, 0.0),
608            Rect::new(5.0, 7.0, -3.0, -3.0),
609            Rect::new(f32::NAN, f32::INFINITY, f32::NAN, f32::INFINITY),
610        ] {
611            let pts = waveform_points(&[1.0, -1.0, 0.3], b, 8);
612            assert!(finite_and_inside(&pts, b), "{b:?}");
613        }
614    }
615
616    #[test]
617    fn points_table_of_generated_inputs_stay_inside() {
618        // deterministic pseudo-random sweep, including non-finite values
619        let mut state = 0x2545_F491_4F6C_DD1D_u64;
620        let mut next = move || {
621            state ^= state << 13;
622            state ^= state >> 7;
623            state ^= state << 17;
624            state
625        };
626        let specials = [
627            f32::NAN,
628            f32::INFINITY,
629            f32::NEG_INFINITY,
630            f32::MAX,
631            f32::MIN,
632        ];
633        for _ in 0..200 {
634            let n = (next() % 300) as usize;
635            let samples: Vec<f32> = (0..n)
636                .map(|_| {
637                    let r = next();
638                    if r % 11 == 0 {
639                        specials[(r >> 8) as usize % specials.len()]
640                    } else {
641                        ((r >> 16) % 4001) as f32 / 1000.0 - 2.0
642                    }
643                })
644                .collect();
645            let b = Rect::new(
646                (next() % 2000) as f32 - 1000.0,
647                (next() % 2000) as f32 - 1000.0,
648                (next() % 500) as f32,
649                (next() % 500) as f32,
650            );
651            let max = (next() % 70) as usize;
652            let pts = waveform_points(&samples, b, max);
653            assert!(pts.len() >= 2 && pts.len() <= max.max(2));
654            assert!(pts.iter().all(|p| inside(p, b)));
655            assert!(pts.windows(2).all(|w| w[0].x <= w[1].x));
656        }
657    }
658
659    /// Ordinary coordinates mixed with every f32, NaN and infinities included.
660    fn bound_coord() -> impl Strategy<Value = f32> {
661        prop_oneof![-1.0e6_f32..1.0e6, proptest::num::f32::ANY]
662    }
663
664    proptest! {
665        #[test]
666        fn prop_points_inside_bounds(
667            samples in proptest::collection::vec(proptest::num::f32::ANY, 0..200),
668            x in bound_coord(),
669            y in bound_coord(),
670            w in bound_coord(),
671            h in bound_coord(),
672            max in 0_usize..300,
673        ) {
674            let b = Rect::new(x, y, w, h);
675            let pts = waveform_points(&samples, b, max);
676            prop_assert!(finite_and_inside(&pts, b), "{:?} -> {:?}", b, pts);
677        }
678
679        #[test]
680        fn prop_points_len_bounded(
681            samples in proptest::collection::vec(proptest::num::f32::ANY, 0..200),
682            max in 0_usize..300,
683        ) {
684            let pts = waveform_points(&samples, B, max);
685            prop_assert!(pts.len() >= 2);
686            prop_assert!(pts.len() <= max.max(2));
687        }
688
689        #[test]
690        fn prop_points_x_non_decreasing(
691            samples in proptest::collection::vec(proptest::num::f32::ANY, 0..200),
692            x in -1.0e6_f32..1.0e6,
693            w in 0.0_f32..1.0e6,
694            max in 0_usize..300,
695        ) {
696            let pts = waveform_points(&samples, Rect::new(x, 0.0, w, 10.0), max);
697            prop_assert!(pts.windows(2).all(|p| p[0].x <= p[1].x));
698        }
699
700        #[test]
701        fn prop_silence_is_centre_line(
702            n in 0_usize..200,
703            y in -1.0e6_f32..1.0e6,
704            h in 0.0_f32..1.0e6,
705            max in 0_usize..300,
706        ) {
707            let b = Rect::new(0.0, y, 50.0, h);
708            let pts = waveform_points(&vec![0.0; n], b, max);
709            let c = y + h / 2.0;
710            prop_assert!(pts.iter().all(|p| p.y == c));
711        }
712    }
713
714    // ===== Overflow-proof bounds (APR-6897 review, defect 2) =====
715
716    /// Documented coordinate limit: independent of the implementation's constant.
717    const LIMIT: f32 = 1.0e15;
718
719    fn oracle_coord(value: f32) -> f32 {
720        if value.is_finite() {
721            value.clamp(-LIMIT, LIMIT)
722        } else {
723            0.0
724        }
725    }
726
727    /// The rectangle the contract promises the points stay in.
728    fn oracle_bounds(b: Rect) -> Rect {
729        Rect::new(
730            oracle_coord(b.x),
731            oracle_coord(b.y),
732            oracle_coord(b.width).max(0.0),
733            oracle_coord(b.height).max(0.0),
734        )
735    }
736
737    fn finite_and_inside(pts: &[Point], b: Rect) -> bool {
738        let eff = oracle_bounds(b);
739        pts.iter()
740            .all(|p| p.x.is_finite() && p.y.is_finite() && inside(p, eff))
741    }
742
743    #[test]
744    fn points_reviewer_repro_f32_max_bounds_are_finite_and_inside() {
745        let b = Rect::new(f32::MAX, f32::MAX, f32::MAX, f32::MAX);
746        let pts = waveform_points(&[0.0, 1.0, -1.0], b, 4);
747        assert_eq!(pts.len(), 3);
748        assert!(finite_and_inside(&pts, b), "{pts:?}");
749        // The clamped rectangle is [1e15, 2e15] on both axes (f32 ulp there is
750        // about 1e8, so compare with a tolerance).
751        let near = |actual: f32, want: f32| (actual - want).abs() <= 4.0e8;
752        assert!(near(pts[0].x, LIMIT) && near(pts[0].y, 1.5 * LIMIT));
753        assert!(near(pts[1].x, 1.5 * LIMIT) && near(pts[1].y, LIMIT));
754        assert!(near(pts[2].x, 2.0 * LIMIT) && near(pts[2].y, 2.0 * LIMIT));
755    }
756
757    #[test]
758    fn points_extreme_bounds_table_stays_finite_and_inside() {
759        let extremes = [
760            f32::MAX,
761            f32::MIN,
762            f32::INFINITY,
763            f32::NEG_INFINITY,
764            f32::NAN,
765            f32::MIN_POSITIVE,
766            -0.0,
767            LIMIT,
768            -LIMIT,
769            3.0e15,
770        ];
771        for &x in &extremes {
772            for &w in &extremes {
773                let b = Rect::new(x, f32::MAX, w, w);
774                let pts = waveform_points(&[1.0, -1.0, 0.25, f32::NAN], b, 8);
775                assert!(finite_and_inside(&pts, b), "{b:?} -> {pts:?}");
776                let b = Rect::new(f32::MIN, x, w, f32::MAX);
777                let pts = waveform_points(&[1.0, -1.0, 0.25, f32::NAN], b, 8);
778                assert!(finite_and_inside(&pts, b), "{b:?} -> {pts:?}");
779            }
780        }
781    }
782
783    #[test]
784    fn points_negative_extents_collapse_to_the_origin_point() {
785        let b = Rect::new(10.0, 20.0, -50.0, -40.0);
786        let pts = waveform_points(&[1.0, -1.0, 0.5], b, 8);
787        assert!(pts.iter().all(|p| *p == Point::new(10.0, 20.0)), "{pts:?}");
788    }
789
790    proptest! {
791        #[test]
792        fn prop_points_finite_and_inside_any_f32_bounds(
793            samples in proptest::collection::vec(proptest::num::f32::ANY, 0..100),
794            x in proptest::num::f32::ANY,
795            y in proptest::num::f32::ANY,
796            w in proptest::num::f32::ANY,
797            h in proptest::num::f32::ANY,
798            max in 0_usize..100,
799        ) {
800            let b = Rect::new(x, y, w, h);
801            let pts = waveform_points(&samples, b, max);
802            prop_assert!(finite_and_inside(&pts, b), "{:?} -> {:?}", b, pts);
803        }
804    }
805
806    // ===== Non-finite preferred sizes and constraints =====
807
808    #[test]
809    fn non_finite_preferred_size_counts_as_absent() {
810        let unbounded = Constraints::unbounded();
811        for bad in [f32::INFINITY, f32::NEG_INFINITY, f32::NAN] {
812            let w = Waveform::new().size(bad, bad);
813            assert_eq!(w.measure(unbounded), Size::new(200.0, 80.0), "{bad}");
814        }
815    }
816
817    #[test]
818    fn huge_preferred_size_is_clamped_to_the_coordinate_limit() {
819        let w = Waveform::new().size(f32::MAX, f32::MAX);
820        assert_eq!(w.measure(Constraints::unbounded()), Size::new(LIMIT, LIMIT));
821    }
822
823    #[test]
824    fn measure_is_finite_for_degenerate_constraints() {
825        let w = Waveform::new().size(f32::MAX, 10.0);
826        for c in [
827            Constraints::unbounded(),
828            Constraints::new(f32::INFINITY, f32::INFINITY, 0.0, 5.0),
829            Constraints::new(0.0, f32::NAN, f32::NAN, f32::INFINITY),
830            Constraints::new(9.0, 3.0, 9.0, 3.0),
831        ] {
832            let s = w.measure(c);
833            assert!(
834                s.width.is_finite() && s.height.is_finite(),
835                "{c:?} -> {s:?}"
836            );
837        }
838    }
839
840    proptest! {
841        #[test]
842        fn prop_measure_finite_for_any_constraints(
843            a in proptest::num::f32::ANY,
844            b in proptest::num::f32::ANY,
845            c in proptest::num::f32::ANY,
846            d in proptest::num::f32::ANY,
847        ) {
848            let s = Waveform::new().measure(Constraints::new(a, b, c, d));
849            prop_assert!(s.width.is_finite() && s.height.is_finite());
850        }
851    }
852
853    // ===== verify: valid bounds =====
854
855    #[test]
856    fn brick_verify_flags_invalid_bounds() {
857        for b in [
858            Rect::new(f32::NAN, 0.0, 10.0, 10.0),
859            Rect::new(0.0, f32::INFINITY, 10.0, 10.0),
860            Rect::new(0.0, 0.0, -1.0, 10.0),
861            Rect::new(0.0, 0.0, 10.0, -1.0),
862            Rect::new(0.0, 0.0, f32::NAN, 10.0),
863            Rect::new(0.0, 0.0, 10.0, f32::NEG_INFINITY),
864        ] {
865            let mut w = Waveform::new().accessible_name("Mic level");
866            w.layout(b);
867            let v = w.verify();
868            assert!(!v.is_valid(), "{b:?}");
869            assert_eq!(v.failed.len(), 1, "{b:?}");
870            assert_eq!(
871                v.failed[0].0,
872                BrickAssertion::ElementPresent("valid-bounds".to_string())
873            );
874            assert!(v.failed[0].1.contains("bounds"));
875        }
876    }
877
878    #[test]
879    fn brick_verify_accepts_valid_laid_out_bounds() {
880        let mut w = Waveform::new().accessible_name("Mic level");
881        w.layout(B);
882        let v = w.verify();
883        assert!(v.is_valid());
884        assert!(v
885            .passed
886            .contains(&BrickAssertion::ElementPresent("valid-bounds".to_string())));
887    }
888
889    // ===== peak / rms =====
890
891    #[test]
892    fn peak_is_max_clamped_magnitude() {
893        assert_eq!(Waveform::new().peak(), 0.0);
894        assert_eq!(Waveform::new().samples(vec![0.2, -0.7, 0.5]).peak(), 0.7);
895        assert_eq!(Waveform::new().samples(vec![9.0, -3.0]).peak(), 1.0);
896        assert_eq!(
897            Waveform::new()
898                .samples(vec![f32::NAN, f32::INFINITY])
899                .peak(),
900            0.0
901        );
902    }
903
904    #[test]
905    fn rms_of_known_signals() {
906        assert_eq!(Waveform::new().rms(), 0.0);
907        assert_eq!(
908            Waveform::new().samples(vec![1.0, -1.0, 1.0, -1.0]).rms(),
909            1.0
910        );
911        let half = Waveform::new().samples(vec![0.5; 4]).rms();
912        assert!((half - 0.5).abs() < 1e-6);
913        // 3.0 clamps to 1.0, NaN counts as 0 -> sqrt(1/2)
914        let clamped = Waveform::new().samples(vec![3.0, f32::NAN]).rms();
915        assert!((clamped - 0.5_f32.sqrt()).abs() < 1e-6);
916    }
917
918    // ===== Waveform builder =====
919
920    #[test]
921    fn widget_defaults_and_builders() {
922        let w = Waveform::new();
923        assert!(w.get_samples().is_empty());
924        assert_eq!(w.get_max_points(), DEFAULT_MAX_POINTS);
925        assert_eq!(w.get_line_width(), 1.5);
926
927        let red = Color::new(1.0, 0.0, 0.0, 1.0);
928        let w = Waveform::new()
929            .samples(vec![0.1, 0.2])
930            .color(red)
931            .line_width(3.0)
932            .max_points(64)
933            .size(300.0, 90.0)
934            .accessible_name("Microphone level")
935            .test_id("wave");
936        assert_eq!(w.get_samples(), &[0.1, 0.2]);
937        assert_eq!(w.get_color(), red);
938        assert_eq!(w.get_line_width(), 3.0);
939        assert_eq!(w.get_max_points(), 64);
940        assert_eq!(Widget::accessible_name(&w), Some("Microphone level"));
941        assert_eq!(Widget::test_id(&w), Some("wave"));
942        assert_eq!(Brick::test_id(&w), Some("wave"));
943    }
944
945    #[test]
946    fn line_width_and_size_never_negative() {
947        let w = Waveform::new().line_width(-2.0).width(-1.0).height(-1.0);
948        assert_eq!(w.get_line_width(), 0.0);
949        assert_eq!(
950            w.measure(Constraints::loose(Size::new(500.0, 500.0))),
951            Size::new(0.0, 0.0)
952        );
953    }
954
955    #[test]
956    fn set_samples_replaces_buffer() {
957        let mut w = Waveform::new().samples(vec![0.9; 10]);
958        w.set_samples(&[0.1, 0.2, 0.3]);
959        assert_eq!(w.get_samples(), &[0.1, 0.2, 0.3]);
960        w.set_samples(&[]);
961        assert!(w.get_samples().is_empty());
962    }
963
964    // ===== Widget =====
965
966    #[test]
967    fn measure_defaults_and_preferred() {
968        let loose = Constraints::loose(Size::new(500.0, 500.0));
969        assert_eq!(Waveform::new().measure(loose), Size::new(200.0, 80.0));
970        assert_eq!(
971            Waveform::new().size(320.0, 60.0).measure(loose),
972            Size::new(320.0, 60.0)
973        );
974    }
975
976    #[test]
977    fn layout_caches_bounds() {
978        let mut w = Waveform::new();
979        let result = w.layout(B);
980        assert_eq!(result.size, Size::new(100.0, 40.0));
981        assert_eq!(Widget::bounds(&w), B);
982    }
983
984    #[test]
985    fn widget_trait_surface() {
986        let mut w = Waveform::new();
987        assert_eq!(Widget::type_id(&w), TypeId::of::<Waveform>());
988        assert!(w
989            .event(&Event::MouseMove {
990                position: Point::ORIGIN
991            })
992            .is_none());
993        assert!(w.children().is_empty());
994        assert!(w.children_mut().is_empty());
995        assert!(!w.is_interactive());
996        assert!(!w.is_focusable());
997        assert_eq!(w.accessible_role(), AccessibleRole::Image);
998        assert_eq!(Widget::accessible_name(&w), None);
999        assert_eq!(Widget::test_id(&w), None);
1000    }
1001
1002    #[test]
1003    fn paint_draws_one_path_from_waveform_points() {
1004        let red = Color::new(1.0, 0.0, 0.0, 1.0);
1005        let mut w = Waveform::new()
1006            .samples(vec![0.0, 1.0, -1.0])
1007            .color(red)
1008            .line_width(2.0);
1009        w.layout(B);
1010        let mut canvas = RecordingCanvas::new();
1011        w.paint(&mut canvas);
1012        let cmds = canvas.take_commands();
1013        assert_eq!(cmds.len(), 1);
1014        let expected = waveform_points(&[0.0, 1.0, -1.0], B, DEFAULT_MAX_POINTS);
1015        assert!(matches!(
1016            &cmds[0],
1017            DrawCommand::Path { points, closed, style }
1018                if *points == expected && !closed && style.color == red && style.width == 2.0
1019        ));
1020    }
1021
1022    #[test]
1023    fn paint_honours_max_points() {
1024        let samples: Vec<f32> = (0..100).map(|i| (i as f32 / 50.0) - 1.0).collect();
1025        let mut w = Waveform::new().samples(samples.clone()).max_points(4);
1026        w.layout(B);
1027        let mut canvas = RecordingCanvas::new();
1028        w.paint(&mut canvas);
1029        let expected = waveform_points(&samples, B, 4);
1030        assert_eq!(expected.len(), 4);
1031        assert!(matches!(
1032            &canvas.take_commands()[0],
1033            DrawCommand::Path { points, .. } if *points == expected
1034        ));
1035    }
1036
1037    #[test]
1038    fn paint_without_samples_draws_centre_line() {
1039        let mut w = Waveform::new();
1040        w.layout(B);
1041        let mut canvas = RecordingCanvas::new();
1042        w.paint(&mut canvas);
1043        let expected = vec![Point::new(10.0, 40.0), Point::new(110.0, 40.0)];
1044        assert!(matches!(
1045            &canvas.take_commands()[0],
1046            DrawCommand::Path { points, .. } if *points == expected
1047        ));
1048    }
1049
1050    // ===== Brick =====
1051
1052    #[test]
1053    fn brick_verify_passes_with_accessible_name() {
1054        let w = Waveform::new().accessible_name("Mic level");
1055        let v = w.verify();
1056        assert!(v.is_valid());
1057        assert!(v.failed.is_empty());
1058        assert!(w.can_render());
1059        assert_eq!(w.brick_name(), "Waveform");
1060        assert_eq!(w.assertions(), &[BrickAssertion::MaxLatencyMs(16)]);
1061        assert_eq!(w.budget(), BrickBudget::uniform(16));
1062    }
1063
1064    #[test]
1065    fn brick_verify_fails_without_accessible_name() {
1066        for w in [
1067            Waveform::new(),
1068            Waveform::new().accessible_name(""),
1069            Waveform::new().accessible_name("   "),
1070        ] {
1071            let v = w.verify();
1072            assert!(!v.is_valid());
1073            assert!(!w.can_render());
1074            assert_eq!(v.failed.len(), 1);
1075            assert!(matches!(v.failed[0].0, BrickAssertion::ElementPresent(_)));
1076            assert!(v.failed[0].1.contains("accessible name"));
1077        }
1078    }
1079
1080    #[test]
1081    fn brick_html_and_css() {
1082        let w = Waveform::new().accessible_name(r#"Mic <"A"> & B"#);
1083        let html = w.to_html();
1084        assert!(html.contains("brick-waveform"));
1085        assert!(html.contains("Mic &lt;&quot;A&quot;&gt; &amp; B"));
1086        assert!(Waveform::new().to_html().contains(r#"aria-label="""#));
1087        assert!(w.to_css().contains(".brick-waveform"));
1088    }
1089
1090    // ===== Class property: every public entry point, any f32 (PR #6897, round 3) =====
1091    //
1092    // Entry point                       covered by
1093    // waveform_points                   class_waveform_points_are_finite_inside_and_bounded
1094    // new / samples / set_samples / color / max_points / test_id / accessible_name
1095    //                                   class_waveform_builders_and_getters_are_sane
1096    // line_width / get_line_width       class_waveform_builders_and_getters_are_sane,
1097    //                                   class_waveform_paint_is_sane_for_any_input
1098    // width / height / size             class_waveform_measure_is_finite_and_not_negative
1099    // peak / rms                        class_waveform_builders_and_getters_are_sane
1100    // measure                           class_waveform_measure_is_finite_and_not_negative
1101    // layout / bounds / verify          class_waveform_layout_size_is_sane_and_verify_judges_raw_bounds
1102    // paint                             class_waveform_paint_is_sane_for_any_input
1103    // event                             class_waveform_event_is_total
1104
1105    /// Any f32, with the awkward ones over-represented.
1106    fn wild() -> impl Strategy<Value = f32> {
1107        prop_oneof![
1108            proptest::num::f32::ANY,
1109            Just(f32::NAN),
1110            Just(f32::INFINITY),
1111            Just(f32::NEG_INFINITY),
1112            Just(f32::MAX),
1113            Just(f32::MIN),
1114            Just(f32::MIN_POSITIVE),
1115            Just(1.0e-40_f32),
1116            Just(-0.0_f32),
1117            Just(-10.0_f32),
1118            -1000.0_f32..1000.0,
1119        ]
1120    }
1121
1122    fn any_max_points() -> impl Strategy<Value = usize> {
1123        prop_oneof![0_usize..300, Just(usize::MAX), any::<usize>()]
1124    }
1125
1126    fn finite_non_negative(v: f32) -> bool {
1127        v.is_finite() && v >= 0.0
1128    }
1129
1130    fn size_sane(s: Size) -> bool {
1131        finite_non_negative(s.width) && finite_non_negative(s.height)
1132    }
1133
1134    /// Every coordinate finite; every width, radius and blur finite and not negative.
1135    fn command_widths_sane(cmd: &DrawCommand) -> bool {
1136        match cmd {
1137            DrawCommand::Path { points, style, .. } => {
1138                finite_non_negative(style.width)
1139                    && style.dash.iter().copied().all(finite_non_negative)
1140                    && points.iter().all(|p| p.x.is_finite() && p.y.is_finite())
1141            }
1142            _ => false,
1143        }
1144    }
1145
1146    fn raw_bounds_valid(b: Rect) -> bool {
1147        [b.x, b.y, b.width, b.height].iter().all(|v| v.is_finite())
1148            && b.width >= 0.0
1149            && b.height >= 0.0
1150    }
1151
1152    fn bits(r: Rect) -> [u32; 4] {
1153        [
1154            r.x.to_bits(),
1155            r.y.to_bits(),
1156            r.width.to_bits(),
1157            r.height.to_bits(),
1158        ]
1159    }
1160
1161    fn painted(w: &Waveform) -> Vec<DrawCommand> {
1162        let mut canvas = RecordingCanvas::new();
1163        w.paint(&mut canvas);
1164        canvas.take_commands()
1165    }
1166
1167    #[test]
1168    fn reviewer_repro_waveform_measure_negative_upper_limits_is_not_negative() {
1169        let size = Waveform::new().measure(Constraints::new(0.0, -10.0, 0.0, -10.0));
1170        assert!(size_sane(size), "{size:?}");
1171        assert_eq!(size, Size::new(0.0, 0.0));
1172    }
1173
1174    #[test]
1175    fn reviewer_repro_waveform_infinite_line_width_paints_a_finite_stroke() {
1176        for width in [f32::INFINITY, f32::MAX, 3.0e15, f32::NAN, f32::NEG_INFINITY] {
1177            let mut wf = Waveform::new().samples(vec![0.5, -0.5]).line_width(width);
1178            assert!(finite_non_negative(wf.get_line_width()), "{width}");
1179            wf.layout(Rect::new(0.0, 0.0, 100.0, 50.0));
1180            let cmds = painted(&wf);
1181            assert_eq!(cmds.len(), 1);
1182            assert!(cmds.iter().all(command_widths_sane), "{width}: {cmds:?}");
1183        }
1184        // An ordinary width is kept exactly.
1185        assert_eq!(Waveform::new().line_width(2.5).get_line_width(), 2.5);
1186    }
1187
1188    #[test]
1189    fn class_waveform_helpers_reject_what_they_should() {
1190        let wide = DrawCommand::Path {
1191            points: vec![Point::new(0.0, 0.0)],
1192            closed: false,
1193            style: presentar_core::draw::StrokeStyle {
1194                width: f32::INFINITY,
1195                ..presentar_core::draw::StrokeStyle::default()
1196            },
1197        };
1198        assert!(!command_widths_sane(&wide));
1199        let rect = DrawCommand::filled_rect(B, Color::WHITE);
1200        assert!(!command_widths_sane(&rect));
1201        assert!(!raw_bounds_valid(Rect::new(0.0, 0.0, -1.0, 1.0)));
1202        assert!(raw_bounds_valid(B));
1203    }
1204
1205    proptest! {
1206        #[test]
1207        fn class_waveform_points_are_finite_inside_and_bounded(
1208            samples in proptest::collection::vec(wild(), 0..120),
1209            x in wild(),
1210            y in wild(),
1211            w in wild(),
1212            h in wild(),
1213            max in any_max_points(),
1214        ) {
1215            let b = Rect::new(x, y, w, h);
1216            let pts = waveform_points(&samples, b, max);
1217            prop_assert!(finite_and_inside(&pts, b), "{:?} -> {:?}", b, pts);
1218            prop_assert!(pts.len() >= 2 && pts.len() <= max.max(2));
1219            prop_assert!(pts.windows(2).all(|p| p[0].x <= p[1].x));
1220        }
1221
1222        #[test]
1223        fn class_waveform_builders_and_getters_are_sane(
1224            samples in proptest::collection::vec(wild(), 0..80),
1225            line in wild(),
1226            w in wild(),
1227            h in wild(),
1228            max in any_max_points(),
1229        ) {
1230            let wf = Waveform::new()
1231                .samples(samples.clone())
1232                .color(Color::new(0.1, 0.2, 0.3, 1.0))
1233                .line_width(line)
1234                .max_points(max)
1235                .width(w)
1236                .height(h)
1237                .accessible_name("Mic")
1238                .test_id("wave");
1239            prop_assert!(finite_non_negative(wf.get_line_width()), "{}", wf.get_line_width());
1240            prop_assert_eq!(wf.get_max_points(), max);
1241            prop_assert_eq!(wf.get_samples().len(), samples.len());
1242            prop_assert!(finite_non_negative(wf.peak()) && wf.peak() <= 1.0);
1243            prop_assert!(finite_non_negative(wf.rms()) && wf.rms() <= 1.0 + 1.0e-3);
1244            let mut in_place = Waveform::new();
1245            in_place.set_samples(&samples);
1246            prop_assert_eq!(in_place.peak().to_bits(), wf.peak().to_bits());
1247            prop_assert_eq!(in_place.rms().to_bits(), wf.rms().to_bits());
1248            prop_assert!(!wf.to_html().is_empty());
1249        }
1250
1251        #[test]
1252        fn class_waveform_measure_is_finite_and_not_negative(
1253            w in wild(),
1254            h in wild(),
1255            a in wild(),
1256            b in wild(),
1257            c in wild(),
1258            d in wild(),
1259        ) {
1260            let constraints = Constraints::new(a, b, c, d);
1261            for wf in [
1262                Waveform::new(),
1263                Waveform::new().size(w, h),
1264                Waveform::new().width(w),
1265                Waveform::new().height(h),
1266            ] {
1267                let s = wf.measure(constraints);
1268                prop_assert!(size_sane(s), "{constraints:?} -> {s:?}");
1269            }
1270        }
1271
1272        #[test]
1273        fn class_waveform_layout_size_is_sane_and_verify_judges_raw_bounds(
1274            x in wild(),
1275            y in wild(),
1276            w in wild(),
1277            h in wild(),
1278        ) {
1279            let bounds = Rect::new(x, y, w, h);
1280            let mut wf = Waveform::new().accessible_name("Mic level");
1281            let result = wf.layout(bounds);
1282            prop_assert!(size_sane(result.size), "{:?}", result.size);
1283            prop_assert_eq!(result.size, oracle_bounds(bounds).size());
1284            prop_assert_eq!(bits(Widget::bounds(&wf)), bits(bounds));
1285            prop_assert_eq!(wf.verify().is_valid(), raw_bounds_valid(bounds));
1286        }
1287
1288        #[test]
1289        fn class_waveform_paint_is_sane_for_any_input(
1290            samples in proptest::collection::vec(wild(), 0..120),
1291            line in wild(),
1292            x in wild(),
1293            y in wild(),
1294            w in wild(),
1295            h in wild(),
1296            max in any_max_points(),
1297        ) {
1298            let bounds = Rect::new(x, y, w, h);
1299            let mut wf = Waveform::new().samples(samples).line_width(line).max_points(max);
1300            wf.layout(bounds);
1301            let cmds = painted(&wf);
1302            prop_assert_eq!(cmds.len(), 1);
1303            prop_assert!(cmds.iter().all(command_widths_sane), "{cmds:?}");
1304            let expected = oracle_bounds(bounds);
1305            for cmd in &cmds {
1306                let DrawCommand::Path { points, .. } = cmd else {
1307                    return Err(TestCaseError::fail("not a path"));
1308                };
1309                prop_assert!(points.len() >= 2 && points.len() <= max.max(2));
1310                prop_assert!(points.iter().all(|p| inside(p, expected)));
1311            }
1312        }
1313
1314        #[test]
1315        fn class_waveform_event_is_total(px in wild(), py in wild()) {
1316            let mut wf = Waveform::new();
1317            wf.layout(B);
1318            let at = Point::new(px, py);
1319            let reply = wf.event(&Event::MouseMove { position: at });
1320            prop_assert!(reply.is_none());
1321        }
1322    }
1323
1324    #[test]
1325    fn bucket_edge_does_not_saturate_at_32_bit_limits() {
1326        // 1023 * 5_000_000 exceeds u32::MAX: a 32-bit saturating product
1327        // (the wasm32 `usize` behaviour) would give u32::MAX / 1024 instead.
1328        let (bucket, len, target) = (1023u64, 5_000_000u64, 1024u64);
1329        let saturated32 = u64::from((bucket as u32).saturating_mul(len as u32)) / target;
1330        let exact = bucket_edge_wide(bucket, len, target);
1331        assert_ne!(exact, saturated32);
1332        assert_eq!(exact, 4_995_117);
1333        assert_eq!(bucket_edge_wide(1024, len, target), len);
1334        assert_eq!(bucket_edge(usize::MAX, 4, 2), 4);
1335    }
1336
1337    #[test]
1338    fn empty_waveform_rms_is_positive_zero() {
1339        let w = Waveform::new();
1340        assert!(w.rms() == 0.0 && w.rms().is_sign_positive());
1341    }
1342}