Skip to main content

azul_layout/widgets/
progressbar.rs

1//! Native progress bar widget with customizable backgrounds, height, and
2//! gradient styling. The main type is [`ProgressBar`], which is rendered
3//! into a DOM via [`ProgressBar::dom()`].
4
5use azul_core::dom::{Dom, IdOrClass, IdOrClass::Class, IdOrClassVec};
6#[allow(clippy::wildcard_imports)] // widget/render module pulls in the css property/value types it builds with
7use azul_css::{
8    dynamic_selector::{CssPropertyWithConditions, CssPropertyWithConditionsVec},
9    props::{
10        basic::*,
11        layout::*,
12        property::{CssProperty, *},
13        style::*,
14    },
15    *,
16};
17use azul_css::css::BoxOrStatic;
18
19const STYLE_BACKGROUND_CONTENT_2688422633177340412_ITEMS: &[StyleBackgroundContent] =
20    &[StyleBackgroundContent::LinearGradient(LinearGradient {
21        direction: Direction::FromTo(DirectionCorners {
22            dir_from: DirectionCorner::Top,
23            dir_to: DirectionCorner::Bottom,
24        }),
25        extend_mode: ExtendMode::Clamp,
26        stops: NormalizedLinearColorStopVec::from_const_slice(
27            LINEAR_COLOR_STOP_12009347504665939_ITEMS,
28        ),
29    })];
30const STYLE_BACKGROUND_CONTENT_14586281004485141058_ITEMS: &[StyleBackgroundContent] =
31    &[StyleBackgroundContent::LinearGradient(LinearGradient {
32        direction: Direction::FromTo(DirectionCorners {
33            dir_from: DirectionCorner::Top,
34            dir_to: DirectionCorner::Bottom,
35        }),
36        extend_mode: ExtendMode::Clamp,
37        stops: NormalizedLinearColorStopVec::from_const_slice(
38            LINEAR_COLOR_STOP_3104396762583413726_ITEMS,
39        ),
40    })];
41const LINEAR_COLOR_STOP_12009347504665939_ITEMS: &[NormalizedLinearColorStop] = &[
42    NormalizedLinearColorStop {
43        offset: PercentageValue::const_new(0),
44        color: ColorOrSystem::color(ColorU {
45            r: 193,
46            g: 255,
47            b: 187,
48            a: 255,
49        }),
50    },
51    NormalizedLinearColorStop {
52        offset: PercentageValue::const_new(10),
53        color: ColorOrSystem::color(ColorU {
54            r: 205,
55            g: 255,
56            b: 205,
57            a: 255,
58        }),
59    },
60    NormalizedLinearColorStop {
61        offset: PercentageValue::const_new(15),
62        color: ColorOrSystem::color(ColorU {
63            r: 156,
64            g: 238,
65            b: 172,
66            a: 255,
67        }),
68    },
69    NormalizedLinearColorStop {
70        offset: PercentageValue::const_new(20),
71        color: ColorOrSystem::color(ColorU {
72            r: 0,
73            g: 211,
74            b: 40,
75            a: 255,
76        }),
77    },
78    NormalizedLinearColorStop {
79        offset: PercentageValue::const_new(30),
80        color: ColorOrSystem::color(ColorU {
81            r: 0,
82            g: 211,
83            b: 40,
84            a: 255,
85        }),
86    },
87    NormalizedLinearColorStop {
88        offset: PercentageValue::const_new(70),
89        color: ColorOrSystem::color(ColorU {
90            r: 32,
91            g: 219,
92            b: 65,
93            a: 255,
94        }),
95    },
96    NormalizedLinearColorStop {
97        offset: PercentageValue::const_new(100),
98        color: ColorOrSystem::color(ColorU {
99            r: 32,
100            g: 219,
101            b: 65,
102            a: 255,
103        }),
104    },
105];
106const LINEAR_COLOR_STOP_3104396762583413726_ITEMS: &[NormalizedLinearColorStop] = &[
107    NormalizedLinearColorStop {
108        offset: PercentageValue::const_new(0),
109        color: ColorOrSystem::color(ColorU {
110            r: 243,
111            g: 243,
112            b: 243,
113            a: 255,
114        }),
115    },
116    NormalizedLinearColorStop {
117        offset: PercentageValue::const_new(10),
118        color: ColorOrSystem::color(ColorU {
119            r: 252,
120            g: 252,
121            b: 252,
122            a: 255,
123        }),
124    },
125    NormalizedLinearColorStop {
126        offset: PercentageValue::const_new(15),
127        color: ColorOrSystem::color(ColorU {
128            r: 218,
129            g: 218,
130            b: 218,
131            a: 255,
132        }),
133    },
134    NormalizedLinearColorStop {
135        offset: PercentageValue::const_new(20),
136        color: ColorOrSystem::color(ColorU {
137            r: 201,
138            g: 201,
139            b: 201,
140            a: 255,
141        }),
142    },
143    NormalizedLinearColorStop {
144        offset: PercentageValue::const_new(30),
145        color: ColorOrSystem::color(ColorU {
146            r: 218,
147            g: 218,
148            b: 218,
149            a: 255,
150        }),
151    },
152    NormalizedLinearColorStop {
153        offset: PercentageValue::const_new(70),
154        color: ColorOrSystem::color(ColorU {
155            r: 203,
156            g: 203,
157            b: 203,
158            a: 255,
159        }),
160    },
161    NormalizedLinearColorStop {
162        offset: PercentageValue::const_new(100),
163        color: ColorOrSystem::color(ColorU {
164            r: 203,
165            g: 203,
166            b: 203,
167            a: 255,
168        }),
169    },
170];
171
172/// A native progress bar widget with customizable bar/container backgrounds and height.
173#[derive(Debug, Clone)]
174#[repr(C)]
175pub struct ProgressBar {
176    pub progressbar_state: ProgressBarState,
177    pub height: PixelValue,
178    pub bar_background: StyleBackgroundContentVec,
179    pub container_background: StyleBackgroundContentVec,
180}
181
182/// Internal state for a [`ProgressBar`], tracking completion percentage.
183#[derive(Copy, Debug, Clone)]
184#[repr(C)]
185pub struct ProgressBarState {
186    pub percent_done: f32,
187    pub display_percentage: bool,
188}
189
190impl ProgressBar {
191    /// Creates a new progress bar with the given completion percentage (0.0 to 100.0).
192    #[inline]
193    #[must_use] pub const fn create(percent_done: f32) -> Self {
194        Self {
195            progressbar_state: ProgressBarState {
196                percent_done,
197                display_percentage: false,
198            },
199            height: PixelValue::const_px(15),
200            bar_background: StyleBackgroundContentVec::from_const_slice(
201                STYLE_BACKGROUND_CONTENT_2688422633177340412_ITEMS,
202            ),
203            container_background: StyleBackgroundContentVec::from_const_slice(
204                STYLE_BACKGROUND_CONTENT_14586281004485141058_ITEMS,
205            ),
206        }
207    }
208
209    /// Replaces `self` with a default (0%) progress bar, returning the previous value.
210    #[inline]
211    #[must_use]
212    pub const fn swap_with_default(&mut self) -> Self {
213        let mut s = Self::create(0.0);
214        core::mem::swap(&mut s, self);
215        s
216    }
217
218    pub fn set_container_background(&mut self, background: StyleBackgroundContentVec) {
219        self.container_background = background;
220    }
221
222    #[must_use] pub fn with_container_background(mut self, background: StyleBackgroundContentVec) -> Self {
223        self.set_container_background(background);
224        self
225    }
226
227    pub fn set_bar_background(&mut self, background: StyleBackgroundContentVec) {
228        self.bar_background = background;
229    }
230
231    #[must_use] pub fn with_bar_background(mut self, background: StyleBackgroundContentVec) -> Self {
232        self.set_bar_background(background);
233        self
234    }
235
236    pub const fn set_height(&mut self, height: PixelValue) {
237        self.height = height;
238    }
239
240    #[must_use] pub const fn with_height(mut self, height: PixelValue) -> Self {
241        self.set_height(height);
242        self
243    }
244
245    /// Renders this progress bar into a [`Dom`] tree consisting of a container div
246    /// with two children: the filled bar and the remaining empty space.
247    #[allow(clippy::too_many_lines)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
248    #[must_use] pub fn dom(self) -> Dom {
249        use azul_core::dom::DomVec;
250
251        // Use percentage widths for the progress bar and remaining space.
252        // The container uses flex-direction: row, and we set explicit widths
253        // on the children using CSS percentages.
254        let percent_done = self.progressbar_state.percent_done.clamp(0.0, 100.0);
255
256        Dom::create_div()
257            .with_css_props(CssPropertyWithConditionsVec::from_vec(vec![
258                // .__azul-native-progress-bar-container
259                CssPropertyWithConditions::simple(CssProperty::Height(LayoutHeightValue::Exact(
260                    LayoutHeight::Px(self.height),
261                ))),
262                CssPropertyWithConditions::simple(CssProperty::FlexDirection(
263                    LayoutFlexDirectionValue::Exact(LayoutFlexDirection::Row),
264                )),
265                CssPropertyWithConditions::simple(CssProperty::BoxShadowBottom(
266                    StyleBoxShadowValue::Exact(BoxOrStatic::heap(StyleBoxShadow {
267                        offset_x: PixelValueNoPercent {
268                            inner: PixelValue::const_px(0),
269                        },
270                        offset_y: PixelValueNoPercent {
271                            inner: PixelValue::const_px(0),
272                        },
273                        color: ColorU {
274                            r: 0,
275                            g: 0,
276                            b: 0,
277                            a: 9,
278                        },
279                        blur_radius: PixelValueNoPercent {
280                            inner: PixelValue::const_px(15),
281                        },
282                        spread_radius: PixelValueNoPercent {
283                            inner: PixelValue::const_px(2),
284                        },
285                        clip_mode: BoxShadowClipMode::Inset,
286                    })),
287                )),
288                CssPropertyWithConditions::simple(CssProperty::BoxShadowTop(
289                    StyleBoxShadowValue::Exact(BoxOrStatic::heap(StyleBoxShadow {
290                        offset_x: PixelValueNoPercent {
291                            inner: PixelValue::const_px(0),
292                        },
293                        offset_y: PixelValueNoPercent {
294                            inner: PixelValue::const_px(0),
295                        },
296                        color: ColorU {
297                            r: 0,
298                            g: 0,
299                            b: 0,
300                            a: 9,
301                        },
302                        blur_radius: PixelValueNoPercent {
303                            inner: PixelValue::const_px(15),
304                        },
305                        spread_radius: PixelValueNoPercent {
306                            inner: PixelValue::const_px(2),
307                        },
308                        clip_mode: BoxShadowClipMode::Inset,
309                    })),
310                )),
311                CssPropertyWithConditions::simple(CssProperty::BoxShadowRight(
312                    StyleBoxShadowValue::Exact(BoxOrStatic::heap(StyleBoxShadow {
313                        offset_x: PixelValueNoPercent {
314                            inner: PixelValue::const_px(0),
315                        },
316                        offset_y: PixelValueNoPercent {
317                            inner: PixelValue::const_px(0),
318                        },
319                        color: ColorU {
320                            r: 0,
321                            g: 0,
322                            b: 0,
323                            a: 9,
324                        },
325                        blur_radius: PixelValueNoPercent {
326                            inner: PixelValue::const_px(15),
327                        },
328                        spread_radius: PixelValueNoPercent {
329                            inner: PixelValue::const_px(2),
330                        },
331                        clip_mode: BoxShadowClipMode::Inset,
332                    })),
333                )),
334                CssPropertyWithConditions::simple(CssProperty::BoxShadowLeft(
335                    StyleBoxShadowValue::Exact(BoxOrStatic::heap(StyleBoxShadow {
336                        offset_x: PixelValueNoPercent {
337                            inner: PixelValue::const_px(0),
338                        },
339                        offset_y: PixelValueNoPercent {
340                            inner: PixelValue::const_px(0),
341                        },
342                        color: ColorU {
343                            r: 0,
344                            g: 0,
345                            b: 0,
346                            a: 9,
347                        },
348                        blur_radius: PixelValueNoPercent {
349                            inner: PixelValue::const_px(15),
350                        },
351                        spread_radius: PixelValueNoPercent {
352                            inner: PixelValue::const_px(2),
353                        },
354                        clip_mode: BoxShadowClipMode::Inset,
355                    })),
356                )),
357                CssPropertyWithConditions::simple(CssProperty::BorderBottomRightRadius(
358                    StyleBorderBottomRightRadiusValue::Exact(StyleBorderBottomRightRadius {
359                        inner: PixelValue::const_px(3),
360                    }),
361                )),
362                CssPropertyWithConditions::simple(CssProperty::BorderBottomLeftRadius(
363                    StyleBorderBottomLeftRadiusValue::Exact(StyleBorderBottomLeftRadius {
364                        inner: PixelValue::const_px(3),
365                    }),
366                )),
367                CssPropertyWithConditions::simple(CssProperty::BorderTopRightRadius(
368                    StyleBorderTopRightRadiusValue::Exact(StyleBorderTopRightRadius {
369                        inner: PixelValue::const_px(3),
370                    }),
371                )),
372                CssPropertyWithConditions::simple(CssProperty::BorderTopLeftRadius(
373                    StyleBorderTopLeftRadiusValue::Exact(StyleBorderTopLeftRadius {
374                        inner: PixelValue::const_px(3),
375                    }),
376                )),
377                CssPropertyWithConditions::simple(CssProperty::BorderBottomWidth(
378                    LayoutBorderBottomWidthValue::Exact(LayoutBorderBottomWidth {
379                        inner: PixelValue::const_px(1),
380                    }),
381                )),
382                CssPropertyWithConditions::simple(CssProperty::BorderLeftWidth(
383                    LayoutBorderLeftWidthValue::Exact(LayoutBorderLeftWidth {
384                        inner: PixelValue::const_px(1),
385                    }),
386                )),
387                CssPropertyWithConditions::simple(CssProperty::BorderRightWidth(
388                    LayoutBorderRightWidthValue::Exact(LayoutBorderRightWidth {
389                        inner: PixelValue::const_px(1),
390                    }),
391                )),
392                CssPropertyWithConditions::simple(CssProperty::BorderTopWidth(
393                    LayoutBorderTopWidthValue::Exact(LayoutBorderTopWidth {
394                        inner: PixelValue::const_px(1),
395                    }),
396                )),
397                CssPropertyWithConditions::simple(CssProperty::BorderBottomStyle(
398                    StyleBorderBottomStyleValue::Exact(StyleBorderBottomStyle {
399                        inner: BorderStyle::Solid,
400                    }),
401                )),
402                CssPropertyWithConditions::simple(CssProperty::BorderLeftStyle(
403                    StyleBorderLeftStyleValue::Exact(StyleBorderLeftStyle {
404                        inner: BorderStyle::Solid,
405                    }),
406                )),
407                CssPropertyWithConditions::simple(CssProperty::BorderRightStyle(
408                    StyleBorderRightStyleValue::Exact(StyleBorderRightStyle {
409                        inner: BorderStyle::Solid,
410                    }),
411                )),
412                CssPropertyWithConditions::simple(CssProperty::BorderTopStyle(
413                    StyleBorderTopStyleValue::Exact(StyleBorderTopStyle {
414                        inner: BorderStyle::Solid,
415                    }),
416                )),
417                CssPropertyWithConditions::simple(CssProperty::BorderBottomColor(
418                    StyleBorderBottomColorValue::Exact(StyleBorderBottomColor {
419                        inner: ColorU {
420                            r: 178,
421                            g: 178,
422                            b: 178,
423                            a: 255,
424                        },
425                    }),
426                )),
427                CssPropertyWithConditions::simple(CssProperty::BorderLeftColor(
428                    StyleBorderLeftColorValue::Exact(StyleBorderLeftColor {
429                        inner: ColorU {
430                            r: 178,
431                            g: 178,
432                            b: 178,
433                            a: 255,
434                        },
435                    }),
436                )),
437                CssPropertyWithConditions::simple(CssProperty::BorderRightColor(
438                    StyleBorderRightColorValue::Exact(StyleBorderRightColor {
439                        inner: ColorU {
440                            r: 178,
441                            g: 178,
442                            b: 178,
443                            a: 255,
444                        },
445                    }),
446                )),
447                CssPropertyWithConditions::simple(CssProperty::BorderTopColor(
448                    StyleBorderTopColorValue::Exact(StyleBorderTopColor {
449                        inner: ColorU {
450                            r: 178,
451                            g: 178,
452                            b: 178,
453                            a: 255,
454                        },
455                    }),
456                )),
457                CssPropertyWithConditions::simple(CssProperty::BackgroundContent(
458                    StyleBackgroundContentVecValue::Exact(self.container_background.clone()),
459                )),
460            ]))
461            .with_ids_and_classes({
462                const IDS_AND_CLASSES_10874511710181900075: &[IdOrClass] = &[Class(
463                    AzString::from_const_str("__azul-native-progress-bar-container"),
464                )];
465                IdOrClassVec::from_const_slice(IDS_AND_CLASSES_10874511710181900075)
466            })
467            .with_children(DomVec::from_vec(vec![
468                Dom::create_div()
469                    .with_css_props(CssPropertyWithConditionsVec::from_vec(vec![
470                        // .__azul-native-progress-bar-bar
471                        // Use percentage width instead of flex-grow hack
472                        CssPropertyWithConditions::simple(CssProperty::Width(
473                            LayoutWidthValue::Exact(LayoutWidth::Px(
474                                PixelValue::percent(percent_done),
475                            )),
476                        )),
477                        CssPropertyWithConditions::simple(CssProperty::BoxShadowBottom(
478                            StyleBoxShadowValue::Exact(BoxOrStatic::heap(StyleBoxShadow {
479                                offset_x: PixelValueNoPercent {
480                                    inner: PixelValue::const_px(0),
481                                },
482                                offset_y: PixelValueNoPercent {
483                                    inner: PixelValue::const_px(0),
484                                },
485                                color: ColorU {
486                                    r: 0,
487                                    g: 51,
488                                    b: 0,
489                                    a: 51,
490                                },
491                                blur_radius: PixelValueNoPercent {
492                                    inner: PixelValue::const_px(15),
493                                },
494                                spread_radius: PixelValueNoPercent {
495                                    inner: PixelValue::const_px(12),
496                                },
497                                clip_mode: BoxShadowClipMode::Inset,
498                            })),
499                        )),
500                        CssPropertyWithConditions::simple(CssProperty::BoxShadowTop(
501                            StyleBoxShadowValue::Exact(BoxOrStatic::heap(StyleBoxShadow {
502                                offset_x: PixelValueNoPercent {
503                                    inner: PixelValue::const_px(0),
504                                },
505                                offset_y: PixelValueNoPercent {
506                                    inner: PixelValue::const_px(0),
507                                },
508                                color: ColorU {
509                                    r: 0,
510                                    g: 51,
511                                    b: 0,
512                                    a: 51,
513                                },
514                                blur_radius: PixelValueNoPercent {
515                                    inner: PixelValue::const_px(15),
516                                },
517                                spread_radius: PixelValueNoPercent {
518                                    inner: PixelValue::const_px(12),
519                                },
520                                clip_mode: BoxShadowClipMode::Inset,
521                            })),
522                        )),
523                        CssPropertyWithConditions::simple(CssProperty::BoxShadowRight(
524                            StyleBoxShadowValue::Exact(BoxOrStatic::heap(StyleBoxShadow {
525                                offset_x: PixelValueNoPercent {
526                                    inner: PixelValue::const_px(0),
527                                },
528                                offset_y: PixelValueNoPercent {
529                                    inner: PixelValue::const_px(0),
530                                },
531                                color: ColorU {
532                                    r: 0,
533                                    g: 51,
534                                    b: 0,
535                                    a: 51,
536                                },
537                                blur_radius: PixelValueNoPercent {
538                                    inner: PixelValue::const_px(15),
539                                },
540                                spread_radius: PixelValueNoPercent {
541                                    inner: PixelValue::const_px(12),
542                                },
543                                clip_mode: BoxShadowClipMode::Inset,
544                            })),
545                        )),
546                        CssPropertyWithConditions::simple(CssProperty::BoxShadowLeft(
547                            StyleBoxShadowValue::Exact(BoxOrStatic::heap(StyleBoxShadow {
548                                offset_x: PixelValueNoPercent {
549                                    inner: PixelValue::const_px(0),
550                                },
551                                offset_y: PixelValueNoPercent {
552                                    inner: PixelValue::const_px(0),
553                                },
554                                color: ColorU {
555                                    r: 0,
556                                    g: 51,
557                                    b: 0,
558                                    a: 51,
559                                },
560                                blur_radius: PixelValueNoPercent {
561                                    inner: PixelValue::const_px(15),
562                                },
563                                spread_radius: PixelValueNoPercent {
564                                    inner: PixelValue::const_px(12),
565                                },
566                                clip_mode: BoxShadowClipMode::Inset,
567                            })),
568                        )),
569                        CssPropertyWithConditions::simple(CssProperty::BorderBottomRightRadius(
570                            StyleBorderBottomRightRadiusValue::Exact(
571                                StyleBorderBottomRightRadius {
572                                    inner: PixelValue::const_px(1),
573                                },
574                            ),
575                        )),
576                        CssPropertyWithConditions::simple(CssProperty::BorderBottomLeftRadius(
577                            StyleBorderBottomLeftRadiusValue::Exact(StyleBorderBottomLeftRadius {
578                                inner: PixelValue::const_px(1),
579                            }),
580                        )),
581                        CssPropertyWithConditions::simple(CssProperty::BorderTopRightRadius(
582                            StyleBorderTopRightRadiusValue::Exact(StyleBorderTopRightRadius {
583                                inner: PixelValue::const_px(1),
584                            }),
585                        )),
586                        CssPropertyWithConditions::simple(CssProperty::BorderTopLeftRadius(
587                            StyleBorderTopLeftRadiusValue::Exact(StyleBorderTopLeftRadius {
588                                inner: PixelValue::const_px(1),
589                            }),
590                        )),
591                        CssPropertyWithConditions::simple(CssProperty::BackgroundContent(
592                            StyleBackgroundContentVecValue::Exact(self.bar_background),
593                        )),
594                    ]))
595                    .with_ids_and_classes({
596                        const IDS_AND_CLASSES_16512648314570682783: &[IdOrClass] = &[Class(
597                            AzString::from_const_str("__azul-native-progress-bar-bar"),
598                        )];
599                        IdOrClassVec::from_const_slice(IDS_AND_CLASSES_16512648314570682783)
600                    }),
601                Dom::create_div()
602                    .with_css_props(CssPropertyWithConditionsVec::from_vec(vec![
603                        // .__azul-native-progress-bar-remaining
604                        // Use percentage width for the remaining space
605                        CssPropertyWithConditions::simple(CssProperty::Width(
606                            LayoutWidthValue::Exact(LayoutWidth::Px(
607                                PixelValue::percent(100.0 - percent_done),
608                            )),
609                        )),
610                    ]))
611                    .with_ids_and_classes({
612                        const IDS_AND_CLASSES_2492405364126620395: &[IdOrClass] = &[Class(
613                            AzString::from_const_str("__azul-native-progress-bar-remaining"),
614                        )];
615                        IdOrClassVec::from_const_slice(IDS_AND_CLASSES_2492405364126620395)
616                    }),
617            ]))
618    }
619}
620
621#[cfg(test)]
622#[allow(
623    clippy::float_cmp,
624    clippy::cast_precision_loss,
625    clippy::cast_possible_truncation,
626    clippy::unreadable_literal,
627    clippy::too_many_lines
628)]
629mod autotest_generated {
630    use std::collections::HashSet;
631
632    use azul_core::dom::NodeType;
633
634    use super::*;
635
636    // ------------------------------------------------------------------
637    // Helpers
638    // ------------------------------------------------------------------
639
640    /// Every `f32` a caller can realistically hand to `ProgressBar::create`.
641    /// The percentage is stored raw and only clamped inside `dom()`, where it is
642    /// pushed through `PixelValue::percent` — which multiplies by 1000 and casts
643    /// to `isize`. That cast saturates (NaN → 0, out of range → `isize::MIN/MAX`),
644    /// so none of these may panic or wrap.
645    ///
646    /// `NAN` is deliberately absent: it is the one input that is unordered against
647    /// the clamp bounds, so it gets its own test.
648    const ADVERSARIAL_PERCENTS: [f32; 16] = [
649        0.0,
650        -0.0,
651        1.0,
652        50.0,
653        100.0,
654        -1.0,
655        101.0,
656        0.001,
657        -0.001,
658        f32::EPSILON,
659        f32::MIN_POSITIVE,
660        -f32::MIN_POSITIVE,
661        f32::MAX,
662        f32::MIN,
663        f32::INFINITY,
664        f32::NEG_INFINITY,
665    ];
666
667    /// Heights that stress the `f32 → isize` fixed-point encoding behind
668    /// `PixelValue`: zero, both signed zeroes, the saturating extremes, NaN, and
669    /// the relative metrics the widget is not supposed to reject.
670    fn adversarial_heights() -> Vec<PixelValue> {
671        vec![
672            PixelValue::zero(),
673            PixelValue::const_px(0),
674            PixelValue::px(-0.0),
675            PixelValue::px(-1.0),
676            PixelValue::px(0.001),
677            PixelValue::px(f32::MAX),
678            PixelValue::px(f32::MIN),
679            PixelValue::px(f32::INFINITY),
680            PixelValue::px(f32::NEG_INFINITY),
681            PixelValue::px(f32::NAN),
682            PixelValue::percent(100.0),
683            PixelValue::em(0.001),
684            // The largest whole-pixel value `const_px` can scale by 1000 without
685            // overflowing `isize` (one more would be a debug-build panic *inside
686            // the argument*, not inside `set_height`).
687            PixelValue::const_px(isize::MAX / 1000),
688        ]
689    }
690
691    /// The raw fixed-point encoding of a length: `FloatValue` stores `value * 1000`
692    /// as an `isize`, so this is what actually survives — comparing it avoids a
693    /// second lossy float round-trip through `get()`.
694    fn raw(pv: PixelValue) -> isize {
695        pv.number.number()
696    }
697
698    /// The addresses `ProgressBar::create` hands out for the two static
699    /// gradients — the reference every "is this still borrowed?" assertion below
700    /// compares against.
701    ///
702    /// Deliberately NOT `STYLE_BACKGROUND_CONTENT_*_ITEMS.as_ptr()`. Those are
703    /// `const` items, and every *use site* of a `const &[T]` gets its own
704    /// promoted read-only allocation; two use sites share an address only if the
705    /// optimizer merges them, which it does in an optimized build and does not
706    /// in an unoptimized one. Comparing a `create()` pointer against the const
707    /// was therefore an accidental green that held only because the suite had
708    /// never been run on the dev profile. `create()` contains ONE use site of
709    /// each const, so the address it returns is stable across calls — and that
710    /// is exactly the property under test: a `create()` that copied the slice
711    /// into a heap vec would hand out a fresh address every time.
712    fn create_gradient_ptrs() -> (
713        *const StyleBackgroundContent,
714        *const StyleBackgroundContent,
715    ) {
716        let pb = ProgressBar::create(0.0);
717        (pb.bar_background.as_ptr(), pb.container_background.as_ptr())
718    }
719
720    /// A heap-allocated background of `n` distinct solid colours. Heap-backed on
721    /// purpose: it is the only case where the vec owns memory that can be
722    /// double-freed or leaked.
723    fn solid(n: usize) -> StyleBackgroundContentVec {
724        StyleBackgroundContentVec::from_vec(
725            (0..n)
726                .map(|i| {
727                    StyleBackgroundContent::Color(ColorU {
728                        r: (i % 256) as u8,
729                        g: 1,
730                        b: 2,
731                        a: 255,
732                    })
733                })
734                .collect(),
735        )
736    }
737
738    fn kids(dom: &Dom) -> &[Dom] {
739        dom.children.as_ref()
740    }
741
742    /// The filled part (`.__azul-native-progress-bar-bar`).
743    fn bar(dom: &Dom) -> &Dom {
744        &kids(dom)[0]
745    }
746
747    /// The empty part (`.__azul-native-progress-bar-remaining`).
748    fn remaining(dom: &Dom) -> &Dom {
749        &kids(dom)[1]
750    }
751
752    /// The declared properties of a node's inline style, in declaration order.
753    fn inline_props(dom: &Dom) -> Vec<CssProperty> {
754        dom.root
755            .style
756            .iter_inline_properties()
757            .map(|(p, _)| p.clone())
758            .collect()
759    }
760
761    /// The CSS classes of a node, in declaration order.
762    fn classes(dom: &Dom) -> Vec<String> {
763        dom.root
764            .get_ids_and_classes()
765            .as_ref()
766            .iter()
767            .filter_map(|c| match c {
768                IdOrClass::Class(s) => Some(s.as_str().to_string()),
769                IdOrClass::Id(_) => None,
770            })
771            .collect()
772    }
773
774    fn width_of(dom: &Dom) -> Option<PixelValue> {
775        dom.root
776            .style
777            .iter_inline_properties()
778            .find_map(|(p, _)| match p {
779                CssProperty::Width(v) => match v.get_property() {
780                    Some(LayoutWidth::Px(pv)) => Some(*pv),
781                    Some(other) => panic!("the progress bar must size in lengths, got {other:?}"),
782                    None => None,
783                },
784                _ => None,
785            })
786    }
787
788    fn height_of(dom: &Dom) -> Option<PixelValue> {
789        dom.root
790            .style
791            .iter_inline_properties()
792            .find_map(|(p, _)| match p {
793                CssProperty::Height(v) => match v.get_property() {
794                    Some(LayoutHeight::Px(pv)) => Some(*pv),
795                    Some(other) => panic!("the progress bar must size in lengths, got {other:?}"),
796                    None => None,
797                },
798                _ => None,
799            })
800    }
801
802    /// The background layers a node declares, cloned out of the DOM.
803    fn background_of(dom: &Dom) -> Option<Vec<StyleBackgroundContent>> {
804        dom.root
805            .style
806            .iter_inline_properties()
807            .find_map(|(p, _)| match p {
808                CssProperty::BackgroundContent(v) => {
809                    v.get_property().map(|b| b.as_ref().to_vec())
810                }
811                _ => None,
812            })
813    }
814
815    /// The *address* of a node's background buffer — the only way to tell a move
816    /// from a copy, and a copy from a use-after-free.
817    fn background_ptr(dom: &Dom) -> Option<*const StyleBackgroundContent> {
818        dom.root
819            .style
820            .iter_inline_properties()
821            .find_map(|(p, _)| match p {
822                CssProperty::BackgroundContent(v) => {
823                    v.get_property().map(StyleBackgroundContentVec::as_ptr)
824                }
825                _ => None,
826            })
827    }
828
829    /// Every absolute length a chrome property declares (box shadows excluded —
830    /// they carry `PixelValueNoPercent`, which cannot express a relative unit).
831    fn lengths_of(p: &CssProperty) -> Vec<PixelValue> {
832        let one = |pv: Option<PixelValue>| pv.into_iter().collect::<Vec<_>>();
833        match p {
834            CssProperty::BorderBottomWidth(v) => one(v.get_property().map(|x| x.inner)),
835            CssProperty::BorderLeftWidth(v) => one(v.get_property().map(|x| x.inner)),
836            CssProperty::BorderRightWidth(v) => one(v.get_property().map(|x| x.inner)),
837            CssProperty::BorderTopWidth(v) => one(v.get_property().map(|x| x.inner)),
838            CssProperty::BorderBottomRightRadius(v) => one(v.get_property().map(|x| x.inner)),
839            CssProperty::BorderBottomLeftRadius(v) => one(v.get_property().map(|x| x.inner)),
840            CssProperty::BorderTopRightRadius(v) => one(v.get_property().map(|x| x.inner)),
841            CssProperty::BorderTopLeftRadius(v) => one(v.get_property().map(|x| x.inner)),
842            _ => Vec::new(),
843        }
844    }
845
846    // ------------------------------------------------------------------
847    // ProgressBar::create
848    // ------------------------------------------------------------------
849
850    #[test]
851    fn create_stores_the_percentage_bit_for_bit_and_never_normalises_it() {
852        // `create` is documented as taking 0.0..=100.0 but performs no validation:
853        // whatever comes in has to come back out untouched, sign of zero included.
854        for p in ADVERSARIAL_PERCENTS {
855            let pb = ProgressBar::create(p);
856            assert_eq!(
857                pb.progressbar_state.percent_done.to_bits(),
858                p.to_bits(),
859                "create() rewrote the percentage {p}",
860            );
861            assert!(
862                !pb.progressbar_state.display_percentage,
863                "a fresh progress bar must not opt into the percentage label",
864            );
865        }
866
867        // NaN cannot be compared, only inspected.
868        let nan = ProgressBar::create(f32::NAN);
869        assert!(
870            nan.progressbar_state.percent_done.is_nan(),
871            "create() silently replaced a NaN percentage",
872        );
873    }
874
875    #[test]
876    fn create_defaults_to_a_15px_height() {
877        let pb = ProgressBar::create(50.0);
878        assert_eq!(pb.height.metric, SizeMetric::Px, "the default height must be absolute");
879        assert_eq!(raw(pb.height), 15_000, "the default height is 15px in 1/1000 units");
880    }
881
882    #[test]
883    fn create_borrows_the_static_gradients_instead_of_allocating_them() {
884        let pb = ProgressBar::create(50.0);
885        let (bar_ptr, container_ptr) = create_gradient_ptrs();
886
887        // Pointer identity, not just content equality: a `create()` that copied the
888        // static slice into a heap vec would allocate on every frame, and one that
889        // kept the static pointer but claimed ownership of it would free `&'static`
890        // memory on drop.
891        assert_eq!(
892            pb.bar_background.as_ptr(),
893            bar_ptr,
894            "the bar gradient stopped being shared with the static slice",
895        );
896        assert_eq!(
897            pb.container_background.as_ptr(),
898            container_ptr,
899            "the container gradient stopped being shared with the static slice",
900        );
901        // Content still pinned to the declared constants, so "shared" cannot
902        // degrade into "shared with something else".
903        assert_eq!(
904            pb.bar_background.as_ref(),
905            STYLE_BACKGROUND_CONTENT_2688422633177340412_ITEMS,
906        );
907        assert_eq!(
908            pb.container_background.as_ref(),
909            STYLE_BACKGROUND_CONTENT_14586281004485141058_ITEMS,
910        );
911        assert_eq!(pb.bar_background.len(), 1);
912        assert_eq!(pb.container_background.len(), 1);
913        assert_eq!(
914            pb.bar_background.capacity(),
915            pb.bar_background.len(),
916            "a borrowed buffer must report capacity == len, or the free path over-reads",
917        );
918
919        // 10_000 bars built and dropped: if the destructor of a static-backed vec
920        // were ever flipped to the owning one, this frees the same `&'static`
921        // allocation 10_000 times.
922        for i in 0..10_000 {
923            let pb = ProgressBar::create(i as f32);
924            assert_eq!(pb.bar_background.len(), 1);
925            assert_eq!(pb.bar_background.as_ptr(), bar_ptr);
926        }
927    }
928
929    #[test]
930    fn create_backgrounds_are_the_declared_gradients_with_sorted_stops() {
931        let pb = ProgressBar::create(0.0);
932        assert_eq!(
933            pb.bar_background.as_ref(),
934            STYLE_BACKGROUND_CONTENT_2688422633177340412_ITEMS,
935        );
936        assert_eq!(
937            pb.container_background.as_ref(),
938            STYLE_BACKGROUND_CONTENT_14586281004485141058_ITEMS,
939        );
940
941        for bg in [&pb.bar_background, &pb.container_background] {
942            match &bg.as_ref()[0] {
943                StyleBackgroundContent::LinearGradient(g) => {
944                    let stops = g.stops.as_ref();
945                    assert_eq!(stops.len(), 7, "a gradient lost or gained a colour stop");
946
947                    // Unsorted or out-of-range stops make the gradient renderer's
948                    // interpolation run backwards over a segment.
949                    let mut prev = f32::NEG_INFINITY;
950                    for s in stops {
951                        let offset = s.offset.normalized() * 100.0;
952                        assert!(
953                            (0.0..=100.0).contains(&offset),
954                            "gradient stop outside 0%..100%: {offset}",
955                        );
956                        assert!(
957                            offset >= prev,
958                            "gradient stops are not sorted: {offset} follows {prev}",
959                        );
960                        prev = offset;
961                    }
962                }
963                other => panic!("the progress bar gradients degraded to {other:?}"),
964            }
965        }
966    }
967
968    #[test]
969    fn create_is_usable_in_const_context() {
970        // `create` is `const fn`; a caller may therefore build a bar as a `const`
971        // item. That only const-evaluates while the backgrounds stay
972        // `from_const_slice` (a heap allocation would not be const-evaluable).
973        const CONST_BAR: ProgressBar = ProgressBar::create(12.5);
974
975        assert_eq!(CONST_BAR.progressbar_state.percent_done, 12.5);
976        assert_eq!(raw(CONST_BAR.height), 15_000);
977        assert_eq!(CONST_BAR.bar_background.len(), 1);
978    }
979
980    // ------------------------------------------------------------------
981    // ProgressBar::swap_with_default
982    // ------------------------------------------------------------------
983
984    #[test]
985    fn swap_with_default_returns_the_previous_bar_and_installs_a_pristine_one() {
986        for p in ADVERSARIAL_PERCENTS {
987            let mut pb = ProgressBar::create(p).with_height(PixelValue::const_px(99));
988            let prev = pb.swap_with_default();
989
990            assert_eq!(
991                prev.progressbar_state.percent_done.to_bits(),
992                p.to_bits(),
993                "the returned bar is not the one that was there ({p})",
994            );
995            assert_eq!(raw(prev.height), 99_000, "the returned bar lost its height");
996
997            assert_eq!(
998                pb.progressbar_state.percent_done.to_bits(),
999                0_u32,
1000                "the replacement must be +0.0 — a -0.0 would encode with the sign bit set",
1001            );
1002            assert_eq!(raw(pb.height), 15_000, "the replacement must use the default height");
1003            assert_eq!(
1004                pb.bar_background.as_ptr(),
1005                create_gradient_ptrs().0,
1006                "the replacement must borrow the static gradient again",
1007            );
1008        }
1009    }
1010
1011    #[test]
1012    fn swap_with_default_keeps_a_nan_percentage_and_moves_owned_memory_out() {
1013        let owned = solid(4);
1014        let ptr = owned.as_ptr();
1015        let mut pb = ProgressBar::create(f32::NAN).with_bar_background(owned);
1016
1017        let prev = pb.swap_with_default();
1018
1019        assert!(
1020            prev.progressbar_state.percent_done.is_nan(),
1021            "a NaN percentage did not survive the swap",
1022        );
1023        assert_eq!(
1024            prev.bar_background.as_ptr(),
1025            ptr,
1026            "the heap buffer was copied instead of moved out",
1027        );
1028        assert_eq!(prev.bar_background.len(), 4);
1029
1030        // Dropping the previous value frees that heap buffer. If `swap_with_default`
1031        // had left `self` pointing at it too, everything below would be a
1032        // use-after-free.
1033        drop(prev);
1034        assert_eq!(
1035            pb.bar_background.as_ref(),
1036            STYLE_BACKGROUND_CONTENT_2688422633177340412_ITEMS,
1037            "the swapped-in bar aliased the memory that was just freed",
1038        );
1039        assert_eq!(pb.progressbar_state.percent_done, 0.0);
1040    }
1041
1042    #[test]
1043    fn repeated_swaps_never_alias_or_leak_the_backgrounds() {
1044        let mut pb = ProgressBar::create(1.0);
1045        let (bar_ptr, _) = create_gradient_ptrs();
1046        for i in 0..1_000_usize {
1047            let want = i % 8 + 1;
1048            pb.set_bar_background(solid(want));
1049            let prev = pb.swap_with_default();
1050
1051            assert_eq!(prev.bar_background.len(), want, "round {i} handed back the wrong buffer");
1052            assert_eq!(pb.progressbar_state.percent_done, 0.0);
1053            assert_eq!(pb.bar_background.as_ptr(), bar_ptr);
1054        }
1055    }
1056
1057    // ------------------------------------------------------------------
1058    // set_/with_ background
1059    // ------------------------------------------------------------------
1060
1061    #[test]
1062    fn each_background_setter_touches_exactly_one_field() {
1063        let mut pb = ProgressBar::create(50.0);
1064        let bar_ptr = pb.bar_background.as_ptr();
1065        pb.set_container_background(solid(3));
1066        assert_eq!(pb.container_background.len(), 3);
1067        assert_eq!(
1068            pb.bar_background.as_ptr(),
1069            bar_ptr,
1070            "set_container_background clobbered the bar background",
1071        );
1072
1073        let mut pb = ProgressBar::create(50.0);
1074        let container_ptr = pb.container_background.as_ptr();
1075        pb.set_bar_background(solid(5));
1076        assert_eq!(pb.bar_background.len(), 5);
1077        assert_eq!(
1078            pb.container_background.as_ptr(),
1079            container_ptr,
1080            "set_bar_background clobbered the container background",
1081        );
1082        assert_eq!(pb.progressbar_state.percent_done, 50.0);
1083        assert_eq!(raw(pb.height), 15_000);
1084    }
1085
1086    #[test]
1087    fn the_builder_forms_are_exactly_their_setters() {
1088        let a = ProgressBar::create(7.5)
1089            .with_bar_background(solid(3))
1090            .with_container_background(solid(2))
1091            .with_height(PixelValue::px(-4.5));
1092
1093        let mut b = ProgressBar::create(7.5);
1094        b.set_bar_background(solid(3));
1095        b.set_container_background(solid(2));
1096        b.set_height(PixelValue::px(-4.5));
1097
1098        assert_eq!(a.bar_background.as_ref(), b.bar_background.as_ref());
1099        assert_eq!(a.container_background.as_ref(), b.container_background.as_ref());
1100        assert_eq!(a.height, b.height);
1101        assert_eq!(
1102            a.progressbar_state.percent_done,
1103            b.progressbar_state.percent_done,
1104        );
1105        assert_eq!(
1106            a.progressbar_state.display_percentage,
1107            b.progressbar_state.display_percentage,
1108        );
1109    }
1110
1111    #[test]
1112    fn an_empty_background_stays_an_empty_declaration() {
1113        let pb = ProgressBar::create(0.0)
1114            .with_bar_background(StyleBackgroundContentVec::from_vec(Vec::new()))
1115            .with_container_background(StyleBackgroundContentVec::new());
1116
1117        assert!(pb.bar_background.is_empty());
1118        assert_eq!(pb.bar_background.len(), 0);
1119        assert!(pb.container_background.is_empty());
1120
1121        let dom = pb.dom();
1122        assert_eq!(
1123            background_of(bar(&dom)),
1124            Some(Vec::new()),
1125            "an empty background must reach the DOM as an empty layer list, not vanish",
1126        );
1127        assert_eq!(background_of(&dom), Some(Vec::new()));
1128    }
1129
1130    #[test]
1131    fn a_background_with_spare_capacity_keeps_its_allocation_intact() {
1132        // The free path rebuilds a `Vec` from (ptr, len, cap); a `cap` that drifted
1133        // to `len` frees the wrong layout.
1134        let mut v = Vec::with_capacity(64);
1135        v.push(StyleBackgroundContent::Color(ColorU {
1136            r: 1,
1137            g: 2,
1138            b: 3,
1139            a: 4,
1140        }));
1141        let bg = StyleBackgroundContentVec::from_vec(v);
1142        assert_eq!(bg.len(), 1);
1143        assert!(bg.capacity() >= 64, "from_vec lost the spare capacity: {}", bg.capacity());
1144
1145        let pb = ProgressBar::create(0.0).with_container_background(bg);
1146        assert_eq!(pb.container_background.len(), 1);
1147        assert!(
1148            pb.container_background.capacity() >= 64,
1149            "the setter rewrote the buffer's capacity",
1150        );
1151    }
1152
1153    #[test]
1154    fn a_very_large_background_is_neither_truncated_nor_copied() {
1155        let big = solid(10_000);
1156        let ptr = big.as_ptr();
1157        let pb = ProgressBar::create(50.0).with_bar_background(big);
1158        assert_eq!(pb.bar_background.len(), 10_000);
1159        assert_eq!(pb.bar_background.as_ptr(), ptr, "the setter deep-copied a 10k-layer background");
1160
1161        let dom = pb.dom();
1162        assert_eq!(
1163            background_of(bar(&dom)).map(|v| v.len()),
1164            Some(10_000),
1165            "the background was truncated on the way into the DOM",
1166        );
1167    }
1168
1169    #[test]
1170    fn overwriting_a_background_releases_the_previous_one() {
1171        // 500 replacements of an owned buffer: a setter that forgot to drop the old
1172        // value leaks, and one that dropped it twice aborts.
1173        let n = 500_usize;
1174        let mut pb = ProgressBar::create(0.0);
1175        for i in 1..=n {
1176            pb.set_bar_background(solid(i % 16 + 1));
1177            pb.set_container_background(solid(i % 4 + 1));
1178        }
1179        // The surviving background is whatever the LAST iteration installed, so the
1180        // expected length is derived from `n` rather than hard-coded.
1181        assert_eq!(pb.bar_background.len(), n % 16 + 1);
1182        assert_eq!(pb.container_background.len(), n % 4 + 1);
1183    }
1184
1185    #[test]
1186    fn cloning_deep_copies_owned_backgrounds_but_shares_static_ones() {
1187        let pb = ProgressBar::create(3.0).with_bar_background(solid(4));
1188        let copy = pb.clone();
1189
1190        assert_ne!(
1191            copy.bar_background.as_ptr(),
1192            pb.bar_background.as_ptr(),
1193            "Clone shared an owned heap buffer — dropping both would double-free it",
1194        );
1195        assert_eq!(
1196            copy.container_background.as_ptr(),
1197            pb.container_background.as_ptr(),
1198            "the static gradient is never freed, so the clone should keep sharing it",
1199        );
1200
1201        drop(pb);
1202        assert_eq!(copy.bar_background.len(), 4);
1203        assert_eq!(
1204            copy.bar_background.as_ref()[3],
1205            StyleBackgroundContent::Color(ColorU {
1206                r: 3,
1207                g: 1,
1208                b: 2,
1209                a: 255,
1210            }),
1211            "the clone read back garbage after the original was dropped",
1212        );
1213    }
1214
1215    // ------------------------------------------------------------------
1216    // set_height / with_height
1217    // ------------------------------------------------------------------
1218
1219    #[test]
1220    fn set_height_stores_every_pixel_value_verbatim() {
1221        for h in adversarial_heights() {
1222            let mut pb = ProgressBar::create(0.0);
1223            pb.set_height(h);
1224            assert_eq!(pb.height.metric, h.metric, "set_height changed the unit of {h:?}");
1225            assert_eq!(raw(pb.height), raw(h), "set_height re-encoded {h:?}");
1226            // and nothing else moved
1227            assert_eq!(pb.progressbar_state.percent_done, 0.0);
1228            assert_eq!(pb.bar_background.len(), 1);
1229        }
1230    }
1231
1232    #[test]
1233    fn an_out_of_range_height_saturates_instead_of_wrapping() {
1234        let mut pb = ProgressBar::create(0.0);
1235
1236        // `FloatValue::new` computes `value * 1000.0` in `f32` (which overflows to
1237        // an infinity) and then casts to `isize` — a saturating cast, so the result
1238        // is a bound, never a wrapped negative.
1239        pb.set_height(PixelValue::px(f32::MAX));
1240        assert_eq!(raw(pb.height), isize::MAX, "an overflowing height wrapped instead of saturating");
1241        assert!(pb.height.number.get().is_finite(), "the saturated height decoded to a non-finite f32");
1242
1243        pb.set_height(PixelValue::px(f32::INFINITY));
1244        assert_eq!(raw(pb.height), isize::MAX);
1245
1246        pb.set_height(PixelValue::px(f32::MIN));
1247        assert_eq!(raw(pb.height), isize::MIN);
1248
1249        pb.set_height(PixelValue::px(f32::NEG_INFINITY));
1250        assert_eq!(raw(pb.height), isize::MIN);
1251
1252        pb.set_height(PixelValue::px(f32::NAN));
1253        assert_eq!(raw(pb.height), 0, "a NaN height must land on 0, not on an arbitrary integer");
1254
1255        pb.set_height(PixelValue::px(-0.0));
1256        assert_eq!(raw(pb.height), 0, "-0.0 must encode to the same 0 as +0.0");
1257
1258        // Below the 1/1000 resolution everything truncates to zero, deterministically.
1259        pb.set_height(PixelValue::px(0.0004));
1260        assert_eq!(raw(pb.height), 0);
1261        pb.set_height(PixelValue::px(f32::MIN_POSITIVE));
1262        assert_eq!(raw(pb.height), 0);
1263    }
1264
1265    #[test]
1266    fn with_height_is_set_height_and_leaves_the_rest_alone() {
1267        for h in adversarial_heights() {
1268            let a = ProgressBar::create(42.0).with_height(h);
1269            let mut b = ProgressBar::create(42.0);
1270            b.set_height(h);
1271
1272            assert_eq!(a.height, b.height, "with_height disagreed with set_height for {h:?}");
1273            assert_eq!(raw(a.height), raw(h));
1274            assert_eq!(a.progressbar_state.percent_done, 42.0);
1275            assert_eq!(
1276                a.bar_background.as_ptr(),
1277                b.bar_background.as_ptr(),
1278                "with_height reallocated the background",
1279            );
1280        }
1281    }
1282
1283    // ------------------------------------------------------------------
1284    // ProgressBar::dom
1285    // ------------------------------------------------------------------
1286
1287    #[test]
1288    fn dom_is_a_container_div_with_exactly_two_leaf_children() {
1289        let dom = ProgressBar::create(50.0).dom();
1290
1291        assert!(matches!(dom.root.get_node_type(), NodeType::Div));
1292        assert_eq!(kids(&dom).len(), 2, "the progress bar must render bar + remaining");
1293        assert!(kids(bar(&dom)).is_empty(), "the bar must stay a leaf");
1294        assert!(kids(remaining(&dom)).is_empty(), "the remaining space must stay a leaf");
1295
1296        // A cached child count that is too small makes `convert_dom_into_compact_dom`
1297        // under-allocate its arenas and panic on out-of-bounds writes.
1298        assert_eq!(dom.estimated_total_children, 2);
1299
1300        assert_eq!(classes(&dom), vec!["__azul-native-progress-bar-container".to_string()]);
1301        assert_eq!(classes(bar(&dom)), vec!["__azul-native-progress-bar-bar".to_string()]);
1302        assert_eq!(
1303            classes(remaining(&dom)),
1304            vec!["__azul-native-progress-bar-remaining".to_string()],
1305        );
1306    }
1307
1308    #[test]
1309    fn dom_clamps_every_out_of_range_percentage_into_zero_to_one_hundred() {
1310        // (input, bar width, remaining width) — widths in 1/1000 of a percent.
1311        const CASES: [(f32, isize, isize); 12] = [
1312            (0.0, 0, 100_000),
1313            (-0.0, 0, 100_000),
1314            (50.0, 50_000, 50_000),
1315            (100.0, 100_000, 0),
1316            (-1.0, 0, 100_000),
1317            (101.0, 100_000, 0),
1318            (-1e30, 0, 100_000),
1319            (1e30, 100_000, 0),
1320            (f32::MAX, 100_000, 0),
1321            (f32::MIN, 0, 100_000),
1322            (f32::INFINITY, 100_000, 0),
1323            (f32::NEG_INFINITY, 0, 100_000),
1324        ];
1325
1326        for (input, bar_width, remaining_width) in CASES {
1327            let dom = ProgressBar::create(input).dom();
1328            let b = width_of(bar(&dom)).expect("the bar must declare a width");
1329            let r = width_of(remaining(&dom)).expect("the remaining space must declare a width");
1330
1331            assert_eq!(b.metric, SizeMetric::Percent, "the bar must size in %, not {:?}", b.metric);
1332            assert_eq!(r.metric, SizeMetric::Percent, "the gap must size in %, not {:?}", r.metric);
1333            assert_eq!(raw(b), bar_width, "bar width for input {input}");
1334            assert_eq!(raw(r), remaining_width, "remaining width for input {input}");
1335            assert!(raw(b) >= 0 && raw(r) >= 0, "a negative width escaped for input {input}");
1336        }
1337    }
1338
1339    #[test]
1340    fn dom_collapses_a_nan_percentage_to_two_empty_children() {
1341        // `f32::clamp` propagates NaN rather than clamping it, and the `f32 -> isize`
1342        // cast inside `FloatValue::new` then turns it into 0. The documented result:
1343        // BOTH children get 0% — the bar renders as an empty container instead of
1344        // falling back to 0%/100%. It does not panic, and it is deterministic.
1345        let dom = ProgressBar::create(f32::NAN).dom();
1346        let b = width_of(bar(&dom)).expect("the bar must declare a width");
1347        let r = width_of(remaining(&dom)).expect("the remaining space must declare a width");
1348
1349        assert_eq!(raw(b), 0);
1350        assert_eq!(raw(r), 0);
1351        assert_eq!(b.metric, SizeMetric::Percent);
1352        assert_eq!(r.metric, SizeMetric::Percent);
1353        assert_eq!(kids(&dom).len(), 2, "a NaN percentage must not change the tree shape");
1354    }
1355
1356    #[test]
1357    fn dom_splits_the_container_exactly_for_whole_percentages() {
1358        for i in 0..=100_isize {
1359            let dom = ProgressBar::create(i as f32).dom();
1360            let b = raw(width_of(bar(&dom)).unwrap());
1361            let r = raw(width_of(remaining(&dom)).unwrap());
1362
1363            assert_eq!(b, i * 1000, "the bar is not {i}% wide");
1364            assert_eq!(
1365                b + r,
1366                100_000,
1367                "the two halves do not add up to the container at {i}%",
1368            );
1369        }
1370    }
1371
1372    #[test]
1373    fn dom_loses_at_most_the_encoding_truncation_for_fractional_percentages() {
1374        // Each side is truncated to 1/1000 of a percent independently, so the pair
1375        // may under-fill by two ticks — but never overflow the container, and never
1376        // go negative.
1377        for p in [
1378            0.0005_f32,
1379            0.5,
1380            1.0 / 3.0,
1381            33.333,
1382            66.667,
1383            99.999,
1384            99.9999,
1385            f32::EPSILON,
1386            f32::MIN_POSITIVE,
1387        ] {
1388            let dom = ProgressBar::create(p).dom();
1389            let b = raw(width_of(bar(&dom)).unwrap());
1390            let r = raw(width_of(remaining(&dom)).unwrap());
1391
1392            assert!(
1393                (0..=100_000).contains(&b) && (0..=100_000).contains(&r),
1394                "a width left 0%..100% for {p}: {b} / {r}",
1395            );
1396            assert!(
1397                (b + r - 100_000).abs() <= 10,
1398                "the two halves drifted apart for {p}: {b} + {r}",
1399            );
1400        }
1401    }
1402
1403    #[test]
1404    fn dom_routes_each_background_to_its_own_node() {
1405        let bar_bg = solid(3);
1406        let container_bg = solid(5);
1407        let bar_ptr = bar_bg.as_ptr();
1408        let container_ptr = container_bg.as_ptr();
1409
1410        let dom = ProgressBar::create(25.0)
1411            .with_bar_background(bar_bg)
1412            .with_container_background(container_bg)
1413            .dom();
1414
1415        assert_eq!(
1416            background_of(&dom).map(|v| v.len()),
1417            Some(5),
1418            "the container lost (or swapped) its background",
1419        );
1420        assert_eq!(
1421            background_of(bar(&dom)).map(|v| v.len()),
1422            Some(3),
1423            "the bar lost (or swapped) its background",
1424        );
1425        assert_eq!(
1426            background_of(remaining(&dom)),
1427            None,
1428            "the remaining space must not paint anything",
1429        );
1430
1431        // The bar background is *moved* into the DOM — same allocation, no copy.
1432        assert_eq!(
1433            background_ptr(bar(&dom)),
1434            Some(bar_ptr),
1435            "the bar background was copied instead of moved",
1436        );
1437        // The container background is cloned, because `self` — and with it the
1438        // original buffer — is dropped at the end of `dom()`. Handing the DOM the
1439        // same pointer would be a use-after-free.
1440        assert_ne!(
1441            background_ptr(&dom),
1442            Some(container_ptr),
1443            "the DOM kept a pointer into a buffer that `dom()` then freed",
1444        );
1445    }
1446
1447    #[test]
1448    fn dom_forwards_any_height_to_the_container_and_to_nobody_else() {
1449        for h in adversarial_heights() {
1450            let dom = ProgressBar::create(50.0).with_height(h).dom();
1451            let got = height_of(&dom).expect("the container must declare a height");
1452
1453            assert_eq!(got.metric, h.metric, "the height unit changed on the way into the DOM");
1454            assert_eq!(raw(got), raw(h), "the height was re-encoded on the way into the DOM");
1455            assert_eq!(height_of(bar(&dom)), None, "the bar must not declare its own height");
1456            assert_eq!(
1457                height_of(remaining(&dom)),
1458                None,
1459                "the remaining space must not declare its own height",
1460            );
1461            assert_eq!(width_of(&dom), None, "the container must not declare a width");
1462        }
1463    }
1464
1465    #[test]
1466    fn dom_declares_the_expected_style_blocks_and_no_property_twice() {
1467        let dom = ProgressBar::create(50.0).with_bar_background(solid(1)).dom();
1468
1469        assert_eq!(inline_props(&dom).len(), 23, "the container style block drifted");
1470        assert_eq!(inline_props(bar(&dom)).len(), 10, "the bar style block drifted");
1471
1472        let props = inline_props(remaining(&dom));
1473        assert_eq!(props.len(), 1, "the remaining space grew a style block: {props:?}");
1474        assert!(
1475            matches!(&props[0], CssProperty::Width(_)),
1476            "the remaining space must only declare its width",
1477        );
1478
1479        // A property declared twice means one of the two is silently dead, and which
1480        // one wins depends on cascade order.
1481        for node in [&dom, bar(&dom), remaining(&dom)] {
1482            let mut seen = HashSet::new();
1483            for p in inline_props(node) {
1484                assert!(
1485                    seen.insert(core::mem::discriminant(&p)),
1486                    "duplicate declaration of {p:?}",
1487                );
1488            }
1489        }
1490    }
1491
1492    #[test]
1493    fn dom_chrome_lengths_are_all_absolute_pixels() {
1494        // Only the two child widths are relative. A border or radius that slipped
1495        // into `em`/`%` would resolve against the parent font or box and either
1496        // vanish or blow up.
1497        let dom = ProgressBar::create(50.0).dom();
1498        for node in [&dom, bar(&dom), remaining(&dom)] {
1499            for p in inline_props(node) {
1500                for length in lengths_of(&p) {
1501                    assert_eq!(
1502                        length.metric,
1503                        SizeMetric::Px,
1504                        "{p:?} declares a relative length: {length:?}",
1505                    );
1506                }
1507            }
1508        }
1509    }
1510
1511    #[test]
1512    fn dom_ignores_display_percentage() {
1513        // The field is public and settable, but nothing in `dom()` reads it: the
1514        // rendered tree has to be byte-identical either way.
1515        let mut with_label = ProgressBar::create(40.0);
1516        with_label.progressbar_state.display_percentage = true;
1517        let without_label = ProgressBar::create(40.0);
1518
1519        assert_eq!(
1520            with_label.dom(),
1521            without_label.dom(),
1522            "display_percentage started changing the tree",
1523        );
1524    }
1525
1526    #[test]
1527    fn dom_is_deterministic_for_equal_inputs() {
1528        let a = ProgressBar::create(37.5)
1529            .with_bar_background(solid(2))
1530            .with_height(PixelValue::px(7.25))
1531            .dom();
1532        let b = ProgressBar::create(37.5)
1533            .with_bar_background(solid(2))
1534            .with_height(PixelValue::px(7.25))
1535            .dom();
1536
1537        assert_eq!(a, b, "two identically-built progress bars rendered differently");
1538    }
1539
1540    #[test]
1541    fn dom_survives_every_extreme_percentage_and_background_size() {
1542        for p in ADVERSARIAL_PERCENTS.into_iter().chain([f32::NAN]) {
1543            for layers in [0_usize, 1, 64] {
1544                let dom = ProgressBar::create(p)
1545                    .with_bar_background(solid(layers))
1546                    .with_container_background(solid(layers))
1547                    .with_height(PixelValue::px(f32::MAX))
1548                    .dom();
1549
1550                assert_eq!(kids(&dom).len(), 2, "shape changed for {p} / {layers} layers");
1551                assert_eq!(dom.estimated_total_children, 2);
1552                assert_eq!(background_of(bar(&dom)).map(|v| v.len()), Some(layers));
1553                assert_eq!(background_of(&dom).map(|v| v.len()), Some(layers));
1554
1555                let b = width_of(bar(&dom)).expect("the bar must declare a width");
1556                assert_eq!(b.metric, SizeMetric::Percent);
1557                assert!((0..=100_000).contains(&raw(b)), "width out of range for {p}");
1558            }
1559        }
1560    }
1561}