1use azul_core::dom::{Dom, IdOrClass, IdOrClass::Class, IdOrClassVec};
26use azul_css::dynamic_selector::{CssPropertyWithConditions, CssPropertyWithConditionsVec};
27use azul_css::{
28 props::{
29 basic::{color::ColorU, *},
30 layout::{LayoutAlignSelf, LayoutFlexGrow, LayoutWidth, LayoutHeight},
31 property::{CssProperty, *},
32 style::{LayoutBorderTopWidth, LayoutBorderBottomWidth, LayoutBorderLeftWidth, LayoutBorderRightWidth, StyleBorderTopStyle, BorderStyle, StyleBorderBottomStyle, StyleBorderLeftStyle, StyleBorderRightStyle, StyleBorderTopColor, StyleBorderBottomColor, StyleBorderLeftColor, StyleBorderRightColor, StyleBorderTopLeftRadius, StyleBorderTopRightRadius, StyleBorderBottomLeftRadius, StyleBorderBottomRightRadius},
33 },
34 AzString,
35};
36
37static SPINNER_CLASS: &[IdOrClass] = &[Class(AzString::from_const_str("__azul-native-spinner"))];
38
39const DEFAULT_SIZE: isize = 24;
41const DEFAULT_TRACK_COLOR: ColorU = ColorU { r: 208, g: 212, b: 217, a: 255 };
43const DEFAULT_ACCENT_COLOR: ColorU = ColorU { r: 13, g: 110, b: 253, a: 255 };
45
46#[derive(Debug, Clone, PartialEq, Eq)]
50#[repr(C)]
51pub struct Spinner {
52 pub size: isize,
54 pub color: ColorU,
56 pub track_color: ColorU,
58 pub spinner_style: CssPropertyWithConditionsVec,
60}
61
62fn build_spinner_style(size: isize, color: ColorU, track_color: ColorU) -> CssPropertyWithConditionsVec {
66 let border_width = (size / 8).max(2);
68 let radius = size / 2;
69 CssPropertyWithConditionsVec::from_vec(alloc::vec![
70 CssPropertyWithConditions::simple(CssProperty::align_self(LayoutAlignSelf::Start)),
72 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(
73 0,
74 ))),
75 CssPropertyWithConditions::simple(CssProperty::const_width(LayoutWidth::const_px(size))),
76 CssPropertyWithConditions::simple(CssProperty::const_height(LayoutHeight::const_px(size))),
77 CssPropertyWithConditions::simple(CssProperty::const_border_top_width(
79 LayoutBorderTopWidth::const_px(border_width),
80 )),
81 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_width(
82 LayoutBorderBottomWidth::const_px(border_width),
83 )),
84 CssPropertyWithConditions::simple(CssProperty::const_border_left_width(
85 LayoutBorderLeftWidth::const_px(border_width),
86 )),
87 CssPropertyWithConditions::simple(CssProperty::const_border_right_width(
88 LayoutBorderRightWidth::const_px(border_width),
89 )),
90 CssPropertyWithConditions::simple(CssProperty::const_border_top_style(StyleBorderTopStyle {
91 inner: BorderStyle::Solid,
92 })),
93 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_style(
94 StyleBorderBottomStyle {
95 inner: BorderStyle::Solid,
96 },
97 )),
98 CssPropertyWithConditions::simple(CssProperty::const_border_left_style(StyleBorderLeftStyle {
99 inner: BorderStyle::Solid,
100 })),
101 CssPropertyWithConditions::simple(CssProperty::const_border_right_style(
102 StyleBorderRightStyle {
103 inner: BorderStyle::Solid,
104 },
105 )),
106 CssPropertyWithConditions::simple(CssProperty::const_border_top_color(StyleBorderTopColor {
108 inner: color,
109 })),
110 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_color(
111 StyleBorderBottomColor { inner: track_color },
112 )),
113 CssPropertyWithConditions::simple(CssProperty::const_border_left_color(StyleBorderLeftColor {
114 inner: track_color,
115 })),
116 CssPropertyWithConditions::simple(CssProperty::const_border_right_color(
117 StyleBorderRightColor { inner: track_color },
118 )),
119 CssPropertyWithConditions::simple(CssProperty::const_border_top_left_radius(
121 StyleBorderTopLeftRadius::const_px(radius),
122 )),
123 CssPropertyWithConditions::simple(CssProperty::const_border_top_right_radius(
124 StyleBorderTopRightRadius::const_px(radius),
125 )),
126 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_left_radius(
127 StyleBorderBottomLeftRadius::const_px(radius),
128 )),
129 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_right_radius(
130 StyleBorderBottomRightRadius::const_px(radius),
131 )),
132 ])
133}
134
135impl Spinner {
136 #[inline]
138 #[must_use] pub fn create() -> Self {
139 Self::with_size(DEFAULT_SIZE)
140 }
141
142 #[inline]
145 #[must_use] pub fn with_size(size: isize) -> Self {
146 Self {
147 size,
148 color: DEFAULT_ACCENT_COLOR,
149 track_color: DEFAULT_TRACK_COLOR,
150 spinner_style: build_spinner_style(size, DEFAULT_ACCENT_COLOR, DEFAULT_TRACK_COLOR),
151 }
152 }
153
154 #[inline]
156 pub fn set_size(&mut self, size: isize) {
157 self.size = size;
158 self.spinner_style = build_spinner_style(size, self.color, self.track_color);
159 }
160
161 #[inline]
163 #[must_use] pub fn with_spinner_size(mut self, size: isize) -> Self {
164 self.set_size(size);
165 self
166 }
167
168 #[inline]
170 pub fn set_color(&mut self, color: ColorU) {
171 self.color = color;
172 self.spinner_style = build_spinner_style(self.size, color, self.track_color);
173 }
174
175 #[inline]
177 #[must_use] pub fn with_color(mut self, color: ColorU) -> Self {
178 self.set_color(color);
179 self
180 }
181
182 #[inline]
184 pub fn set_track_color(&mut self, track_color: ColorU) {
185 self.track_color = track_color;
186 self.spinner_style = build_spinner_style(self.size, self.color, track_color);
187 }
188
189 #[inline]
191 #[must_use] pub fn with_track_color(mut self, track_color: ColorU) -> Self {
192 self.set_track_color(track_color);
193 self
194 }
195
196 #[inline]
198 #[must_use] pub fn swap_with_default(&mut self) -> Self {
199 let mut s = Self::create();
200 core::mem::swap(&mut s, self);
201 s
202 }
203
204 #[inline]
207 #[must_use] pub fn dom(self) -> Dom {
208 Dom::create_div()
209 .with_ids_and_classes(IdOrClassVec::from_const_slice(SPINNER_CLASS))
210 .with_css_props(self.spinner_style)
211 }
212}
213
214impl Default for Spinner {
215 fn default() -> Self {
216 Self::create()
217 }
218}
219
220impl From<Spinner> for Dom {
221 fn from(s: Spinner) -> Self {
222 s.dom()
223 }
224}
225
226#[cfg(test)]
227#[allow(
228 clippy::too_many_lines,
229 clippy::unreadable_literal,
230 clippy::cast_possible_truncation,
231 clippy::cast_precision_loss,
232 clippy::float_cmp
233)]
234mod autotest_generated {
235 use azul_core::dom::NodeType;
236
237 use super::*;
238
239 const DECLARATIONS: usize = 20;
245
246 const EXPECTED_ORDER: [CssPropertyType; DECLARATIONS] = [
249 CssPropertyType::AlignSelf,
250 CssPropertyType::FlexGrow,
251 CssPropertyType::Width,
252 CssPropertyType::Height,
253 CssPropertyType::BorderTopWidth,
254 CssPropertyType::BorderBottomWidth,
255 CssPropertyType::BorderLeftWidth,
256 CssPropertyType::BorderRightWidth,
257 CssPropertyType::BorderTopStyle,
258 CssPropertyType::BorderBottomStyle,
259 CssPropertyType::BorderLeftStyle,
260 CssPropertyType::BorderRightStyle,
261 CssPropertyType::BorderTopColor,
262 CssPropertyType::BorderBottomColor,
263 CssPropertyType::BorderLeftColor,
264 CssPropertyType::BorderRightColor,
265 CssPropertyType::BorderTopLeftRadius,
266 CssPropertyType::BorderTopRightRadius,
267 CssPropertyType::BorderBottomLeftRadius,
268 CssPropertyType::BorderBottomRightRadius,
269 ];
270
271 const FP_SCALE: isize = 1000;
274
275 const MAX_ENCODABLE_SIZE: isize = isize::MAX / FP_SCALE;
279 const MIN_ENCODABLE_SIZE: isize = isize::MIN / FP_SCALE;
280
281 const SAFE_SIZES: [isize; 16] = [
286 0,
287 1,
288 2,
289 3,
290 4,
291 7,
292 8,
293 15,
294 16,
295 24,
296 1_000,
297 -1,
298 -3,
299 -24,
300 MAX_ENCODABLE_SIZE,
301 MIN_ENCODABLE_SIZE,
302 ];
303
304 const RED: ColorU = ColorU { r: 255, g: 0, b: 0, a: 255 };
307 const GREEN: ColorU = ColorU { r: 0, g: 255, b: 0, a: 255 };
308 const GHOST: ColorU = ColorU { r: 0, g: 0, b: 0, a: 0 };
309
310 fn props(v: &CssPropertyWithConditionsVec) -> Vec<CssProperty> {
312 v.as_slice().iter().map(|p| p.property.clone()).collect()
313 }
314
315 fn find<T>(v: &CssPropertyWithConditionsVec, f: impl Fn(&CssProperty) -> Option<T>) -> Option<T> {
317 v.as_slice().iter().find_map(|p| f(&p.property))
318 }
319
320 fn raw(pv: PixelValue) -> isize {
323 pv.number.number()
324 }
325
326 fn width(v: &CssPropertyWithConditionsVec) -> PixelValue {
327 find(v, |p| match p {
328 CssProperty::Width(x) => match x.get_property() {
329 Some(LayoutWidth::Px(pv)) => Some(*pv),
330 other => panic!("the ring must size in absolute lengths, got {other:?}"),
331 },
332 _ => None,
333 })
334 .expect("the ring must declare a width")
335 }
336
337 fn height(v: &CssPropertyWithConditionsVec) -> PixelValue {
338 find(v, |p| match p {
339 CssProperty::Height(x) => match x.get_property() {
340 Some(LayoutHeight::Px(pv)) => Some(*pv),
341 other => panic!("the ring must size in absolute lengths, got {other:?}"),
342 },
343 _ => None,
344 })
345 .expect("the ring must declare a height")
346 }
347
348 fn border_widths(v: &CssPropertyWithConditionsVec) -> [PixelValue; 4] {
350 [
351 find(v, |p| match p {
352 CssProperty::BorderTopWidth(x) => x.get_property().map(|x| x.inner),
353 _ => None,
354 }),
355 find(v, |p| match p {
356 CssProperty::BorderRightWidth(x) => x.get_property().map(|x| x.inner),
357 _ => None,
358 }),
359 find(v, |p| match p {
360 CssProperty::BorderBottomWidth(x) => x.get_property().map(|x| x.inner),
361 _ => None,
362 }),
363 find(v, |p| match p {
364 CssProperty::BorderLeftWidth(x) => x.get_property().map(|x| x.inner),
365 _ => None,
366 }),
367 ]
368 .map(|o| o.expect("the ring must declare all four border widths"))
369 }
370
371 fn border_colors(v: &CssPropertyWithConditionsVec) -> [ColorU; 4] {
373 [
374 find(v, |p| match p {
375 CssProperty::BorderTopColor(x) => x.get_property().map(|x| x.inner),
376 _ => None,
377 }),
378 find(v, |p| match p {
379 CssProperty::BorderRightColor(x) => x.get_property().map(|x| x.inner),
380 _ => None,
381 }),
382 find(v, |p| match p {
383 CssProperty::BorderBottomColor(x) => x.get_property().map(|x| x.inner),
384 _ => None,
385 }),
386 find(v, |p| match p {
387 CssProperty::BorderLeftColor(x) => x.get_property().map(|x| x.inner),
388 _ => None,
389 }),
390 ]
391 .map(|o| o.expect("the ring must declare all four border colours"))
392 }
393
394 fn border_styles(v: &CssPropertyWithConditionsVec) -> [BorderStyle; 4] {
396 [
397 find(v, |p| match p {
398 CssProperty::BorderTopStyle(x) => x.get_property().map(|x| x.inner),
399 _ => None,
400 }),
401 find(v, |p| match p {
402 CssProperty::BorderRightStyle(x) => x.get_property().map(|x| x.inner),
403 _ => None,
404 }),
405 find(v, |p| match p {
406 CssProperty::BorderBottomStyle(x) => x.get_property().map(|x| x.inner),
407 _ => None,
408 }),
409 find(v, |p| match p {
410 CssProperty::BorderLeftStyle(x) => x.get_property().map(|x| x.inner),
411 _ => None,
412 }),
413 ]
414 .map(|o| o.expect("the ring must declare all four border styles"))
415 }
416
417 fn radii(v: &CssPropertyWithConditionsVec) -> [PixelValue; 4] {
419 [
420 find(v, |p| match p {
421 CssProperty::BorderTopLeftRadius(x) => x.get_property().map(|x| x.inner),
422 _ => None,
423 }),
424 find(v, |p| match p {
425 CssProperty::BorderTopRightRadius(x) => x.get_property().map(|x| x.inner),
426 _ => None,
427 }),
428 find(v, |p| match p {
429 CssProperty::BorderBottomLeftRadius(x) => x.get_property().map(|x| x.inner),
430 _ => None,
431 }),
432 find(v, |p| match p {
433 CssProperty::BorderBottomRightRadius(x) => x.get_property().map(|x| x.inner),
434 _ => None,
435 }),
436 ]
437 .map(|o| o.expect("the ring must declare all four corner radii"))
438 }
439
440 fn lengths(v: &CssPropertyWithConditionsVec) -> Vec<PixelValue> {
443 let mut out = vec![width(v), height(v)];
444 out.extend(border_widths(v));
445 out.extend(radii(v));
446 out
447 }
448
449 fn dom_props(dom: &Dom) -> Vec<CssProperty> {
451 dom.root
452 .style
453 .iter_inline_properties()
454 .map(|(p, _)| p.clone())
455 .collect()
456 }
457
458 fn dom_classes(dom: &Dom) -> Vec<String> {
459 dom.root
460 .get_ids_and_classes()
461 .as_ref()
462 .iter()
463 .filter_map(|c| match c {
464 IdOrClass::Class(s) => Some(s.as_str().to_string()),
465 IdOrClass::Id(_) => None,
466 })
467 .collect()
468 }
469
470 #[test]
475 fn build_spinner_style_declares_every_side_exactly_once() {
476 for size in SAFE_SIZES {
479 let style = build_spinner_style(size, RED, GREEN);
480 let types: Vec<CssPropertyType> =
481 props(&style).iter().map(CssProperty::get_type).collect();
482
483 assert_eq!(style.len(), DECLARATIONS, "declaration count changed for {size}");
484 assert_eq!(types, EXPECTED_ORDER.to_vec(), "declaration order changed for {size}");
485
486 let mut sorted = types.clone();
487 sorted.sort_unstable();
488 sorted.dedup();
489 assert_eq!(sorted.len(), DECLARATIONS, "a property is declared twice for {size}");
490 }
491 }
492
493 #[test]
494 fn build_spinner_style_declarations_are_all_unconditional() {
495 let style = build_spinner_style(24, RED, GREEN);
498 for p in style.as_slice() {
499 assert!(
500 p.apply_if.is_empty(),
501 "{:?} became conditional: {:?}",
502 p.property.get_type(),
503 p.apply_if,
504 );
505 }
506 }
507
508 #[test]
509 fn build_spinner_style_is_deterministic() {
510 for size in SAFE_SIZES {
511 assert_eq!(
512 build_spinner_style(size, RED, GREEN),
513 build_spinner_style(size, RED, GREEN),
514 "two identical calls produced different styles for {size}",
515 );
516 }
517 }
518
519 #[test]
524 fn build_spinner_style_at_zero_size() {
525 let style = build_spinner_style(0, RED, GREEN);
528
529 assert_eq!(raw(width(&style)), 0);
530 assert_eq!(raw(height(&style)), 0);
531 for bw in border_widths(&style) {
532 assert_eq!(raw(bw), 2 * FP_SCALE, "the 2px border floor stopped applying at size 0");
533 }
534 for r in radii(&style) {
535 assert_eq!(raw(r), 0, "a zero-diameter ring must have a zero radius");
536 }
537 }
538
539 #[test]
540 fn border_width_is_an_eighth_of_the_size_with_a_two_px_floor() {
541 for size in (-256_isize..=256).chain(SAFE_SIZES) {
544 let style = build_spinner_style(size, RED, GREEN);
545 let expected = (size / 8).max(2);
546
547 for bw in border_widths(&style) {
548 assert_eq!(
549 raw(bw),
550 expected * FP_SCALE,
551 "border width wrong for size {size}",
552 );
553 assert!(raw(bw) >= 2 * FP_SCALE, "border width dropped below 2px for {size}");
554 }
555 }
556 }
557
558 #[test]
559 fn radius_is_half_the_size_truncated_toward_zero() {
560 for size in (-256_isize..=256).chain(SAFE_SIZES) {
563 let style = build_spinner_style(size, RED, GREEN);
564 let corners = radii(&style);
565
566 for r in corners {
567 assert_eq!(raw(r), (size / 2) * FP_SCALE, "radius wrong for size {size}");
568 }
569 assert!(
570 corners.iter().all(|r| *r == corners[0]),
571 "the four corners disagree for size {size}: {corners:?}",
572 );
573 }
574 }
575
576 #[test]
577 fn negative_sizes_pass_straight_through_unclamped() {
578 let style = build_spinner_style(-24, RED, GREEN);
583
584 assert_eq!(raw(width(&style)), -24 * FP_SCALE);
585 assert_eq!(raw(height(&style)), -24 * FP_SCALE);
586 assert_eq!(raw(border_widths(&style)[0]), 2 * FP_SCALE);
587 assert_eq!(raw(radii(&style)[0]), -12 * FP_SCALE);
588 }
589
590 #[test]
591 fn the_ring_is_thicker_than_its_box_only_below_four_px() {
592 for size in 0_isize..=64 {
596 let style = build_spinner_style(size, RED, GREEN);
597 let total = 2 * (raw(border_widths(&style)[0]) / FP_SCALE);
598
599 if size >= 4 {
600 assert!(total <= size, "size {size}: borders ({total}px) overflow the box");
601 } else {
602 assert!(total > size, "size {size}: expected the degenerate 2px-floor ring");
603 }
604 }
605 }
606
607 #[test]
608 fn every_length_is_an_absolute_pixel() {
609 for size in SAFE_SIZES {
612 let style = build_spinner_style(size, RED, GREEN);
613 for length in lengths(&style) {
614 assert_eq!(
615 length.metric,
616 SizeMetric::Px,
617 "size {size} produced a relative length: {length:?}",
618 );
619 }
620 }
621 }
622
623 #[test]
624 fn size_round_trips_through_the_fixed_point_encoding() {
625 for size in SAFE_SIZES {
628 let style = build_spinner_style(size, RED, GREEN);
629
630 assert_eq!(raw(width(&style)), size * FP_SCALE, "width encoding lost {size}");
631 assert_eq!(raw(width(&style)) / FP_SCALE, size, "width did not round-trip for {size}");
632 assert_eq!(raw(height(&style)) / FP_SCALE, size, "height did not round-trip for {size}");
633
634 if size.abs() <= 1_000 {
636 assert_eq!(
637 width(&style).number.get(),
638 size as f32,
639 "float view wrong for {size}",
640 );
641 }
642 }
643 }
644
645 #[test]
646 fn the_edges_of_the_encodable_range_do_not_overflow() {
647 for size in [MAX_ENCODABLE_SIZE, MIN_ENCODABLE_SIZE] {
650 let style = build_spinner_style(size, RED, GREEN);
651
652 assert_eq!(style.len(), DECLARATIONS);
653 assert_eq!(raw(width(&style)), size * FP_SCALE);
654 assert_eq!(raw(radii(&style)[0]), (size / 2) * FP_SCALE);
655 assert_eq!(raw(border_widths(&style)[0]), (size / 8).max(2) * FP_SCALE);
656 }
657 }
658
659 #[cfg(panic = "unwind")]
660 #[test]
661 fn sizes_beyond_the_encodable_range_are_not_saturated() {
662 use std::{
663 hint::black_box,
664 panic::{catch_unwind, AssertUnwindSafe},
665 };
666
667 let profile_traps_overflow = catch_unwind(AssertUnwindSafe(|| {
674 let big = black_box(isize::MAX);
675 let _ = black_box(big * FP_SCALE);
676 }))
677 .is_err();
678
679 for size in [isize::MAX, isize::MIN, MAX_ENCODABLE_SIZE + 1, MIN_ENCODABLE_SIZE - 1] {
680 let widget_panicked =
681 catch_unwind(AssertUnwindSafe(|| drop(build_spinner_style(size, RED, GREEN))))
682 .is_err();
683
684 assert_eq!(
685 widget_panicked, profile_traps_overflow,
686 "size {size}: the fixed-point encoding no longer behaves like a raw \
687 multiply (expected panic == {profile_traps_overflow})",
688 );
689 }
690 }
691
692 #[test]
697 fn only_the_top_side_gets_the_accent_colour() {
698 for (color, track) in [
701 (RED, GREEN),
702 (GHOST, RED),
703 (RED, GHOST),
704 (RED, RED),
705 (ColorU { r: 0, g: 0, b: 0, a: 0 }, ColorU { r: 255, g: 255, b: 255, a: 255 }),
706 ] {
707 let style = build_spinner_style(24, color, track);
708 let [top, right, bottom, left] = border_colors(&style);
709
710 assert_eq!(top, color, "the top side lost the accent colour");
711 assert_eq!([right, bottom, left], [track; 3], "a track side lost its colour");
712 }
713 }
714
715 #[test]
716 fn colour_channels_survive_untouched() {
717 for a in [0_u8, 1, 127, 255] {
720 let color = ColorU { r: 1, g: 2, b: 3, a };
721 let track = ColorU { r: 253, g: 254, b: 255, a: 255 - a };
722 let style = build_spinner_style(24, color, track);
723 let [top, right, bottom, left] = border_colors(&style);
724
725 assert_eq!((top.r, top.g, top.b, top.a), (1, 2, 3, a));
726 for side in [right, bottom, left] {
727 assert_eq!((side.r, side.g, side.b, side.a), (253, 254, 255, 255 - a));
728 }
729 }
730 }
731
732 #[test]
733 fn all_four_sides_are_solid() {
734 for size in SAFE_SIZES {
736 assert_eq!(
737 border_styles(&build_spinner_style(size, RED, GREEN)),
738 [BorderStyle::Solid; 4],
739 "a side stopped being solid at size {size}",
740 );
741 }
742 }
743
744 #[test]
745 fn the_ring_neither_grows_nor_stretches() {
746 let style = build_spinner_style(24, RED, GREEN);
749
750 let align = find(&style, |p| match p {
751 CssProperty::AlignSelf(x) => x.get_property().copied(),
752 _ => None,
753 });
754 let grow = find(&style, |p| match p {
755 CssProperty::FlexGrow(x) => x.get_property().map(|x| x.inner),
756 _ => None,
757 });
758
759 assert_eq!(align, Some(LayoutAlignSelf::Start));
760 assert_eq!(grow.map(|g| g.number()), Some(0));
761 }
762
763 #[test]
768 fn create_matches_the_documented_defaults() {
769 let s = Spinner::create();
770
771 assert_eq!(s.size, DEFAULT_SIZE);
772 assert_eq!(s.size, 24, "the documented default diameter changed");
773 assert_eq!(s.color, ColorU { r: 13, g: 110, b: 253, a: 255 });
774 assert_eq!(s.track_color, ColorU { r: 208, g: 212, b: 217, a: 255 });
775 assert_eq!(
776 s.spinner_style,
777 build_spinner_style(DEFAULT_SIZE, DEFAULT_ACCENT_COLOR, DEFAULT_TRACK_COLOR),
778 );
779 assert_eq!(raw(border_widths(&s.spinner_style)[0]), 3 * FP_SCALE);
781 assert_eq!(raw(radii(&s.spinner_style)[0]), 12 * FP_SCALE);
782 }
783
784 #[test]
785 fn default_is_create() {
786 assert_eq!(Spinner::default(), Spinner::create());
787 }
788
789 #[test]
790 fn with_size_records_the_size_and_rebuilds_the_style() {
791 for size in SAFE_SIZES {
792 let s = Spinner::with_size(size);
793
794 assert_eq!(s.size, size, "the size field does not match the argument");
795 assert_eq!(s.color, DEFAULT_ACCENT_COLOR);
796 assert_eq!(s.track_color, DEFAULT_TRACK_COLOR);
797 assert_eq!(s.spinner_style.len(), DECLARATIONS);
798 assert_eq!(
799 s.spinner_style,
800 build_spinner_style(size, DEFAULT_ACCENT_COLOR, DEFAULT_TRACK_COLOR),
801 );
802 assert_eq!(raw(width(&s.spinner_style)) / FP_SCALE, size);
803 }
804 }
805
806 #[test]
811 fn set_size_keeps_the_custom_colours() {
812 let mut s = Spinner::create().with_color(RED).with_track_color(GREEN);
815 s.set_size(64);
816
817 assert_eq!(s.size, 64);
818 assert_eq!(s.color, RED);
819 assert_eq!(s.track_color, GREEN);
820 assert_eq!(border_colors(&s.spinner_style), [RED, GREEN, GREEN, GREEN]);
821 assert_eq!(raw(width(&s.spinner_style)), 64 * FP_SCALE);
822 }
823
824 #[test]
825 fn set_color_touches_only_the_accent() {
826 let mut s = Spinner::with_size(48).with_track_color(GREEN);
827 s.set_color(RED);
828
829 assert_eq!(s.size, 48, "set_color moved the diameter");
830 assert_eq!(s.track_color, GREEN, "set_color clobbered the track colour");
831 assert_eq!(border_colors(&s.spinner_style), [RED, GREEN, GREEN, GREEN]);
832 assert_eq!(raw(width(&s.spinner_style)), 48 * FP_SCALE);
833 }
834
835 #[test]
836 fn set_track_color_touches_only_the_track() {
837 let mut s = Spinner::with_size(48).with_color(RED);
838 s.set_track_color(GREEN);
839
840 assert_eq!(s.size, 48, "set_track_color moved the diameter");
841 assert_eq!(s.color, RED, "set_track_color clobbered the accent colour");
842 assert_eq!(border_colors(&s.spinner_style), [RED, GREEN, GREEN, GREEN]);
843 }
844
845 #[test]
846 fn setters_are_idempotent_and_never_grow_the_style() {
847 let mut s = Spinner::create();
850 for _ in 0..100 {
851 s.set_size(24);
852 s.set_color(RED);
853 s.set_track_color(GREEN);
854 }
855
856 let once = Spinner::with_size(24).with_color(RED).with_track_color(GREEN);
857 assert_eq!(s.spinner_style.len(), DECLARATIONS, "the style vec grew");
858 assert_eq!(s, once, "repeated setters diverged from a single application");
859 }
860
861 #[test]
862 fn set_size_survives_every_encodable_diameter() {
863 let mut s = Spinner::create().with_color(RED).with_track_color(GREEN);
866 for size in SAFE_SIZES {
867 s.set_size(size);
868
869 assert_eq!(s.size, size);
870 assert_eq!(s.spinner_style.len(), DECLARATIONS);
871 assert_eq!(s, Spinner::with_size(size).with_color(RED).with_track_color(GREEN));
872 }
873 }
874
875 #[test]
880 fn builder_setters_match_the_mutating_setters() {
881 for size in SAFE_SIZES {
882 let mut mutated = Spinner::create();
883 mutated.set_size(size);
884 assert_eq!(Spinner::create().with_spinner_size(size), mutated, "size {size}");
885 }
886
887 let mut mutated = Spinner::create();
888 mutated.set_color(RED);
889 assert_eq!(Spinner::create().with_color(RED), mutated);
890
891 let mut mutated = Spinner::create();
892 mutated.set_track_color(GHOST);
893 assert_eq!(Spinner::create().with_track_color(GHOST), mutated);
894 }
895
896 #[test]
897 fn the_builder_chain_is_order_independent() {
898 let a = Spinner::create()
901 .with_spinner_size(40)
902 .with_color(RED)
903 .with_track_color(GREEN);
904 let b = Spinner::create()
905 .with_track_color(GREEN)
906 .with_color(RED)
907 .with_spinner_size(40);
908 let c = Spinner::create()
909 .with_color(RED)
910 .with_spinner_size(40)
911 .with_track_color(GREEN);
912
913 assert_eq!(a, b, "setting the size last changed the result");
914 assert_eq!(a, c, "interleaving the setters changed the result");
915 }
916
917 #[test]
918 fn equality_distinguishes_every_field() {
919 let base = Spinner::create();
920
921 assert_ne!(base, Spinner::create().with_spinner_size(25));
922 assert_ne!(base, Spinner::create().with_color(RED));
923 assert_ne!(base, Spinner::create().with_track_color(RED));
924 assert_eq!(base, Spinner::create().with_spinner_size(DEFAULT_SIZE));
925 }
926
927 #[test]
932 fn swap_with_default_returns_the_original_and_installs_a_default() {
933 let mut s = Spinner::with_size(96).with_color(RED).with_track_color(GREEN);
934 let expected = s.clone();
935
936 let taken = s.swap_with_default();
937
938 assert_eq!(taken, expected, "the returned spinner is not the original");
939 assert_eq!(s, Spinner::create(), "the receiver is not a fresh default");
940 assert_eq!(taken.spinner_style.len(), DECLARATIONS);
942 assert_eq!(border_colors(&taken.spinner_style), [RED, GREEN, GREEN, GREEN]);
943 }
944
945 #[test]
946 fn swapping_twice_leaves_a_default_both_times() {
947 let mut s = Spinner::with_size(MAX_ENCODABLE_SIZE);
948
949 let first = s.swap_with_default();
950 let second = s.swap_with_default();
951
952 assert_eq!(first.size, MAX_ENCODABLE_SIZE);
953 assert_eq!(second, Spinner::create(), "the second swap did not return the default");
954 assert_eq!(s, Spinner::create());
955 }
956
957 #[test]
958 fn swap_on_a_default_is_observationally_a_no_op() {
959 let mut s = Spinner::create();
960 let taken = s.swap_with_default();
961
962 assert_eq!(taken, Spinner::create());
963 assert_eq!(s, Spinner::create());
964 }
965
966 #[test]
971 fn clone_deep_copies_the_style_buffer() {
972 let original = Spinner::with_size(32).with_color(RED);
975 let copy = original.clone();
976
977 assert_eq!(copy, original);
978 assert_ne!(
979 original.spinner_style.as_ptr(),
980 copy.spinner_style.as_ptr(),
981 "the clone shares the original's style buffer",
982 );
983 }
984
985 #[test]
986 fn mutating_a_clone_leaves_the_original_alone() {
987 let original = Spinner::with_size(32).with_color(RED);
988 let mut copy = original.clone();
989
990 copy.set_size(8);
991 copy.set_track_color(GHOST);
992
993 assert_eq!(original.size, 32, "mutating the clone moved the original's size");
994 assert_eq!(original.track_color, DEFAULT_TRACK_COLOR);
995 assert_eq!(raw(width(&original.spinner_style)), 32 * FP_SCALE);
996 assert_eq!(border_colors(&original.spinner_style)[0], RED);
997 }
998
999 #[test]
1004 fn dom_is_a_single_classed_div() {
1005 let dom = Spinner::create().dom();
1006
1007 assert_eq!(*dom.root.get_node_type(), NodeType::Div);
1008 assert!(dom.children.as_ref().is_empty(), "the spinner must be a leaf node");
1009 assert_eq!(dom.estimated_total_children, 0);
1010 assert_eq!(dom_classes(&dom), vec!["__azul-native-spinner".to_string()]);
1011 }
1012
1013 #[test]
1014 fn dom_carries_exactly_the_spinner_style() {
1015 for size in SAFE_SIZES {
1018 let s = Spinner::with_size(size).with_color(RED).with_track_color(GREEN);
1019 let expected = props(&s.spinner_style);
1020 let dom = s.dom();
1021
1022 assert_eq!(dom_props(&dom), expected, "the DOM lost declarations for size {size}");
1023 assert!(
1024 dom.root.style.iter_inline_properties().all(|(_, c)| c.is_empty()),
1025 "size {size}: an inline declaration became conditional",
1026 );
1027 }
1028 }
1029
1030 #[test]
1031 fn from_impl_matches_the_dom_method() {
1032 let s = Spinner::with_size(17).with_color(GHOST);
1033 assert_eq!(Dom::from(s.clone()), s.dom());
1034 }
1035
1036 #[test]
1037 fn dom_is_deterministic_for_equal_inputs() {
1038 let a = Spinner::with_size(13).with_color(RED).with_track_color(GHOST).dom();
1039 let b = Spinner::with_size(13).with_color(RED).with_track_color(GHOST).dom();
1040
1041 assert_eq!(a, b, "two identically-built spinners rendered differently");
1042 }
1043
1044 #[test]
1045 fn dom_survives_every_encodable_size_and_colour() {
1046 for size in SAFE_SIZES {
1047 for (color, track) in [(RED, GREEN), (GHOST, GHOST)] {
1048 let dom = Spinner::with_size(size)
1049 .with_color(color)
1050 .with_track_color(track)
1051 .dom();
1052
1053 assert_eq!(dom_props(&dom).len(), DECLARATIONS, "shape changed for {size}");
1054 assert!(dom.children.as_ref().is_empty());
1055 assert_eq!(dom_classes(&dom).len(), 1);
1056 }
1057 }
1058 }
1059}