1use azul_core::{
10 callbacks::{CoreCallbackData, Update},
11 dom::{Dom, IdOrClass, IdOrClass::Class, IdOrClassVec, TabIndex},
12 refany::RefAny,
13};
14use azul_css::dynamic_selector::{CssPropertyWithConditions, CssPropertyWithConditionsVec};
15use azul_css::{
16 props::{
17 basic::{color::ColorU, *},
18 layout::{LayoutDisplay, LayoutFlexDirection, LayoutAlignItems, LayoutAlignSelf, LayoutFlexGrow, LayoutWidth, LayoutHeight, LayoutPaddingLeft, LayoutPaddingRight, LayoutPaddingTop, LayoutPaddingBottom, LayoutMarginLeft},
19 property::{CssProperty, *},
20 style::{StyleBackgroundContent, StyleBackgroundContentVec, StyleBorderTopLeftRadius, StyleBorderTopRightRadius, StyleBorderBottomLeftRadius, StyleBorderBottomRightRadius, StyleCursor},
21 },
22 impl_option_inner, AzString,
23};
24
25use crate::callbacks::{Callback, CallbackInfo};
26
27static SWITCH_TRACK_CLASS: &[IdOrClass] =
28 &[Class(AzString::from_const_str("__azul-native-switch"))];
29static SWITCH_KNOB_CLASS: &[IdOrClass] =
30 &[Class(AzString::from_const_str("__azul-native-switch-knob"))];
31
32pub type SwitchOnToggleCallbackType = extern "C" fn(RefAny, CallbackInfo, SwitchState) -> Update;
34impl_widget_callback!(
35 SwitchOnToggle,
36 OptionSwitchOnToggle,
37 SwitchOnToggleCallback,
38 SwitchOnToggleCallbackType
39);
40
41azul_core::impl_managed_callback! {
42 wrapper: SwitchOnToggleCallback,
43 info_ty: CallbackInfo,
44 return_ty: Update,
45 default_ret: Update::DoNothing,
46 invoker_static: SWITCH_ON_TOGGLE_INVOKER,
47 invoker_ty: AzSwitchOnToggleCallbackInvoker,
48 thunk_fn: az_switch_on_toggle_callback_thunk,
49 setter_fn: AzApp_setSwitchOnToggleCallbackInvoker,
50 from_handle_fn: AzSwitchOnToggleCallback_createFromHostHandle,
51 extra_args: [ state: SwitchState ],
52}
53
54#[derive(Debug, Clone, PartialEq, Eq)]
56#[repr(C)]
57pub struct Switch {
58 pub switch_state: SwitchStateWrapper,
59 pub track_style: CssPropertyWithConditionsVec,
61 pub knob_style: CssPropertyWithConditionsVec,
63}
64
65#[derive(Debug, Default, Clone, PartialEq, Eq)]
66#[repr(C)]
67pub struct SwitchStateWrapper {
68 pub inner: SwitchState,
70 pub on_toggle: OptionSwitchOnToggle,
72}
73
74#[derive(Copy, Debug, Default, Clone, PartialEq, Eq)]
76#[repr(C)]
77pub struct SwitchState {
78 pub checked: bool,
80}
81
82const TRACK_WIDTH: isize = 36;
84const TRACK_HEIGHT: isize = 20;
85const TRACK_PADDING: isize = 2;
86const TRACK_RADIUS: isize = 10;
87const KNOB_SIZE: isize = 16;
88const KNOB_RADIUS: isize = 8;
89const KNOB_TRAVEL: isize = TRACK_WIDTH - (2 * TRACK_PADDING) - KNOB_SIZE;
91
92const TRACK_OFF_COLOR: ColorU = ColorU {
94 r: 204,
95 g: 204,
96 b: 204,
97 a: 255,
98}; const TRACK_ON_COLOR: ColorU = ColorU {
100 r: 76,
101 g: 217,
102 b: 100,
103 a: 255,
104}; const KNOB_COLOR: ColorU = ColorU {
106 r: 255,
107 g: 255,
108 b: 255,
109 a: 255,
110}; const TRACK_OFF_BG_ITEMS: &[StyleBackgroundContent] =
113 &[StyleBackgroundContent::Color(TRACK_OFF_COLOR)];
114const TRACK_OFF_BG: StyleBackgroundContentVec =
115 StyleBackgroundContentVec::from_const_slice(TRACK_OFF_BG_ITEMS);
116const TRACK_ON_BG_ITEMS: &[StyleBackgroundContent] =
117 &[StyleBackgroundContent::Color(TRACK_ON_COLOR)];
118const TRACK_ON_BG: StyleBackgroundContentVec =
119 StyleBackgroundContentVec::from_const_slice(TRACK_ON_BG_ITEMS);
120const KNOB_BG_ITEMS: &[StyleBackgroundContent] = &[StyleBackgroundContent::Color(KNOB_COLOR)];
121const KNOB_BG: StyleBackgroundContentVec =
122 StyleBackgroundContentVec::from_const_slice(KNOB_BG_ITEMS);
123
124fn build_track_style(checked: bool) -> CssPropertyWithConditionsVec {
128 let bg = if checked { TRACK_ON_BG } else { TRACK_OFF_BG };
129 CssPropertyWithConditionsVec::from_vec(alloc::vec![
130 CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Flex)),
131 CssPropertyWithConditions::simple(CssProperty::const_flex_direction(
132 LayoutFlexDirection::Row,
133 )),
134 CssPropertyWithConditions::simple(CssProperty::const_align_items(LayoutAlignItems::Center)),
135 CssPropertyWithConditions::simple(CssProperty::align_self(LayoutAlignSelf::Center)),
136 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(
137 0,
138 ))),
139 CssPropertyWithConditions::simple(CssProperty::const_width(LayoutWidth::const_px(
140 TRACK_WIDTH,
141 ))),
142 CssPropertyWithConditions::simple(CssProperty::const_height(LayoutHeight::const_px(
143 TRACK_HEIGHT,
144 ))),
145 CssPropertyWithConditions::simple(CssProperty::const_padding_left(
146 LayoutPaddingLeft::const_px(TRACK_PADDING),
147 )),
148 CssPropertyWithConditions::simple(CssProperty::const_padding_right(
149 LayoutPaddingRight::const_px(TRACK_PADDING),
150 )),
151 CssPropertyWithConditions::simple(CssProperty::const_padding_top(
152 LayoutPaddingTop::const_px(TRACK_PADDING),
153 )),
154 CssPropertyWithConditions::simple(CssProperty::const_padding_bottom(
155 LayoutPaddingBottom::const_px(TRACK_PADDING),
156 )),
157 CssPropertyWithConditions::simple(CssProperty::const_border_top_left_radius(
158 StyleBorderTopLeftRadius::const_px(TRACK_RADIUS),
159 )),
160 CssPropertyWithConditions::simple(CssProperty::const_border_top_right_radius(
161 StyleBorderTopRightRadius::const_px(TRACK_RADIUS),
162 )),
163 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_left_radius(
164 StyleBorderBottomLeftRadius::const_px(TRACK_RADIUS),
165 )),
166 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_right_radius(
167 StyleBorderBottomRightRadius::const_px(TRACK_RADIUS),
168 )),
169 CssPropertyWithConditions::simple(CssProperty::const_cursor(StyleCursor::Pointer)),
170 CssPropertyWithConditions::simple(CssProperty::const_background_content(bg)),
171 ])
172}
173
174fn build_knob_style(checked: bool) -> CssPropertyWithConditionsVec {
177 let margin = if checked { KNOB_TRAVEL } else { 0 };
178 CssPropertyWithConditionsVec::from_vec(alloc::vec![
179 CssPropertyWithConditions::simple(CssProperty::const_width(LayoutWidth::const_px(
180 KNOB_SIZE,
181 ))),
182 CssPropertyWithConditions::simple(CssProperty::const_height(LayoutHeight::const_px(
183 KNOB_SIZE,
184 ))),
185 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(
186 0,
187 ))),
188 CssPropertyWithConditions::simple(CssProperty::const_border_top_left_radius(
189 StyleBorderTopLeftRadius::const_px(KNOB_RADIUS),
190 )),
191 CssPropertyWithConditions::simple(CssProperty::const_border_top_right_radius(
192 StyleBorderTopRightRadius::const_px(KNOB_RADIUS),
193 )),
194 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_left_radius(
195 StyleBorderBottomLeftRadius::const_px(KNOB_RADIUS),
196 )),
197 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_right_radius(
198 StyleBorderBottomRightRadius::const_px(KNOB_RADIUS),
199 )),
200 CssPropertyWithConditions::simple(CssProperty::const_background_content(KNOB_BG)),
201 CssPropertyWithConditions::simple(CssProperty::const_margin_left(
202 LayoutMarginLeft::const_px(margin),
203 )),
204 ])
205}
206
207impl Switch {
208 #[must_use] pub fn create(checked: bool) -> Self {
210 Self {
211 switch_state: SwitchStateWrapper {
212 inner: SwitchState { checked },
213 ..Default::default()
214 },
215 track_style: build_track_style(checked),
216 knob_style: build_knob_style(checked),
217 }
218 }
219
220 #[inline]
221 #[must_use] pub fn swap_with_default(&mut self) -> Self {
222 let mut s = Self::create(false);
223 core::mem::swap(&mut s, self);
224 s
225 }
226
227 #[inline]
228 pub fn set_on_toggle<C: Into<SwitchOnToggleCallback>>(&mut self, data: RefAny, on_toggle: C) {
229 self.switch_state.on_toggle = Some(SwitchOnToggle {
230 callback: on_toggle.into(),
231 refany: data,
232 })
233 .into();
234 }
235
236 #[inline]
237 #[must_use] pub fn with_on_toggle<C: Into<SwitchOnToggleCallback>>(
238 mut self,
239 data: RefAny,
240 on_toggle: C,
241 ) -> Self {
242 self.set_on_toggle(data, on_toggle);
243 self
244 }
245
246 #[inline]
247 #[must_use] pub fn dom(self) -> Dom {
248 use azul_core::{
249 callbacks::{CoreCallback, CoreCallbackData},
250 dom::{Dom, EventFilter, HoverEventFilter},
251 };
252
253 Dom::create_div()
254 .with_ids_and_classes(IdOrClassVec::from(SWITCH_TRACK_CLASS))
255 .with_css_props(self.track_style)
256 .with_callbacks(
257 vec![CoreCallbackData {
258 event: EventFilter::Hover(HoverEventFilter::MouseUp),
259 callback: CoreCallback {
260 cb: input::default_on_switch_clicked as usize,
261 ctx: azul_core::refany::OptionRefAny::None,
262 },
263 refany: RefAny::new(self.switch_state),
264 }]
265 .into(),
266 )
267 .with_tab_index(TabIndex::Auto)
268 .with_children(
269 vec![Dom::create_div()
270 .with_ids_and_classes(IdOrClassVec::from(SWITCH_KNOB_CLASS))
271 .with_css_props(self.knob_style)]
272 .into(),
273 )
274 }
275}
276
277impl Default for Switch {
278 fn default() -> Self {
279 Self::create(false)
280 }
281}
282
283mod input {
285
286 use azul_core::{callbacks::Update, refany::RefAny};
287 use azul_css::props::{layout::LayoutMarginLeft, property::CssProperty};
288
289 use super::{SwitchOnToggle, SwitchStateWrapper, KNOB_TRAVEL, TRACK_OFF_BG, TRACK_ON_BG};
290 use crate::callbacks::CallbackInfo;
291
292 pub(super) extern "C" fn default_on_switch_clicked(
293 mut switch: RefAny,
294 mut info: CallbackInfo,
295 ) -> Update {
296 let Some(mut switch) = switch.downcast_mut::<SwitchStateWrapper>() else {
297 return Update::DoNothing;
298 };
299
300 let track_id = info.get_hit_node();
301 let Some(knob_id) = info.get_first_child(track_id) else {
302 return Update::DoNothing;
303 };
304
305 switch.inner.checked = !switch.inner.checked;
306
307 let result = {
308 let switch = &mut *switch;
310 let on_toggle = &mut switch.on_toggle;
311 let inner = switch.inner;
312
313 match on_toggle.as_mut() {
314 Some(SwitchOnToggle {
315 callback,
316 refany: data,
317 }) => (callback.cb)(data.clone(), info, inner),
318 None => Update::DoNothing,
319 }
320 };
321
322 if switch.inner.checked {
324 info.set_css_property(track_id, CssProperty::const_background_content(TRACK_ON_BG));
325 info.set_css_property(
326 knob_id,
327 CssProperty::const_margin_left(LayoutMarginLeft::const_px(KNOB_TRAVEL)),
328 );
329 } else {
330 info.set_css_property(track_id, CssProperty::const_background_content(TRACK_OFF_BG));
331 info.set_css_property(
332 knob_id,
333 CssProperty::const_margin_left(LayoutMarginLeft::const_px(0)),
334 );
335 }
336
337 result
338 }
339}
340
341impl From<Switch> for Dom {
342 fn from(s: Switch) -> Self {
343 s.dom()
344 }
345}
346
347#[cfg(test)]
348#[allow(clippy::float_cmp)] #[allow(clippy::assertions_on_constants)]
354mod autotest_generated {
355 use std::{
356 collections::{BTreeMap, HashMap},
357 mem::discriminant,
358 sync::{Arc, Mutex},
359 };
360
361 use azul_core::{
362 dom::{DomId, DomNodeId, EventFilter, HoverEventFilter, NodeId, NodeType},
363 geom::{LogicalRect, OptionLogicalPosition},
364 gl::OptionGlContextPtr,
365 hit_test::ScrollPosition,
366 refany::OptionRefAny,
367 resources::RendererResources,
368 styled_dom::{NodeHierarchyItemId, StyledDom},
369 window::{MonitorVec, RawWindowHandle},
370 };
371 use azul_css::props::basic::{length::SizeMetric, pixel::PixelValue};
372 use rust_fontconfig::FcFontCache;
373
374 use super::*;
375 #[cfg(feature = "icu")]
376 use crate::icu::IcuLocalizerHandle;
377 use crate::{
378 callbacks::{CallbackChange, CallbackInfoRefData, ExternalSystemCallbacks},
379 solver3::{display_list::DisplayList, layout_tree::LayoutTree},
380 window::{DomLayoutResult, LayoutWindow},
381 window_state::FullWindowState,
382 };
383
384 const TRACK_W: f32 = 36.0;
393 const TRACK_H: f32 = 20.0;
394 const PAD: f32 = 2.0;
395 const TRACK_R: f32 = 10.0;
396 const KNOB: f32 = 16.0;
397 const KNOB_R: f32 = 8.0;
398 const TRAVEL: f32 = 16.0;
400
401 const TRACK: usize = 0;
403 const KNOB_NODE: usize = 1;
404
405 fn node(idx: usize) -> DomNodeId {
411 DomNodeId {
412 dom: DomId::ROOT_ID,
413 node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(idx))),
414 }
415 }
416
417 fn node_none() -> DomNodeId {
421 DomNodeId {
422 dom: DomId::ROOT_ID,
423 node: NodeHierarchyItemId::NONE,
424 }
425 }
426
427 fn layout_result(styled_dom: StyledDom) -> DomLayoutResult {
431 DomLayoutResult {
432 styled_dom,
433 layout_tree: LayoutTree {
434 nodes: Vec::new(),
435 warm: Vec::new(),
436 cold: Vec::new(),
437 root: 0,
438 dom_to_layout: BTreeMap::new(),
439 children_arena: Vec::new(),
440 children_offsets: Vec::new(),
441 subtree_needs_intrinsic: Vec::new(),
442 },
443 calculated_positions: Vec::new(),
444 viewport: LogicalRect::zero(),
445 display_list: DisplayList::default(),
446 scroll_ids: HashMap::new(),
447 scroll_id_to_node_id: HashMap::new(),
448 }
449 }
450
451 fn with_info<R>(
455 styled_dom: StyledDom,
456 hit: DomNodeId,
457 f: impl FnOnce(&mut CallbackInfo) -> R,
458 ) -> (R, Vec<CallbackChange>) {
459 let mut layout_window =
460 LayoutWindow::new(FcFontCache::default()).expect("LayoutWindow::new failed");
461 layout_window
462 .layout_results
463 .insert(DomId::ROOT_ID, layout_result(styled_dom));
464
465 let renderer_resources = RendererResources::default();
466 let previous_window_state: Option<FullWindowState> = None;
467 let current_window_state = FullWindowState::default();
468 let gl_context = OptionGlContextPtr::None;
469 let scroll_states: BTreeMap<DomId, BTreeMap<NodeHierarchyItemId, ScrollPosition>> =
470 BTreeMap::new();
471 let window_handle = RawWindowHandle::Unsupported;
472 let system_callbacks = ExternalSystemCallbacks::rust_internal();
473
474 let ref_data = CallbackInfoRefData {
475 layout_window: &layout_window,
476 renderer_resources: &renderer_resources,
477 previous_window_state: &previous_window_state,
478 current_window_state: ¤t_window_state,
479 gl_context: &gl_context,
480 current_scroll_manager: &scroll_states,
481 current_window_handle: &window_handle,
482 system_callbacks: &system_callbacks,
483 system_style: Arc::new(azul_css::system::SystemStyle::default()),
484 monitors: Arc::new(Mutex::new(MonitorVec::from_const_slice(&[]))),
485 #[cfg(feature = "icu")]
486 icu_localizer: IcuLocalizerHandle::default(),
487 ctx: OptionRefAny::None,
488 };
489
490 let changes: Arc<Mutex<Vec<CallbackChange>>> = Arc::new(Mutex::new(Vec::new()));
491
492 let mut info = CallbackInfo::new(
493 &ref_data,
494 &changes,
495 hit,
496 OptionLogicalPosition::None,
497 OptionLogicalPosition::None,
498 );
499
500 let r = f(&mut info);
501 let pushed = info.take_changes();
502 (r, pushed)
503 }
504
505 fn laid_out(switch: Switch) -> (StyledDom, RefAny) {
511 let dom = switch.dom();
512 let state = dom.root.callbacks.as_ref()[0].refany.clone();
513 (StyledDom::create_from_dom(dom), state)
514 }
515
516 fn click(
518 styled_dom: StyledDom,
519 state: &RefAny,
520 hit: DomNodeId,
521 ) -> (Update, Vec<CallbackChange>) {
522 with_info(styled_dom, hit, |info| {
523 input::default_on_switch_clicked(state.clone(), *info)
524 })
525 }
526
527 fn is_checked(state: &RefAny) -> bool {
528 let mut state = state.clone();
529 let wrapper = state
530 .downcast_ref::<SwitchStateWrapper>()
531 .expect("the widget state changed type");
532 wrapper.inner.checked
533 }
534
535 fn pushed_pairs(changes: &[CallbackChange]) -> Vec<(NodeId, CssProperty)> {
537 changes
538 .iter()
539 .filter_map(|c| match c {
540 CallbackChange::ChangeNodeCssProperties {
541 node_id, properties, ..
542 } => Some((*node_id, properties.as_ref().to_vec())),
543 _ => None,
544 })
545 .flat_map(|(n, ps)| ps.into_iter().map(move |p| (n, p)))
546 .collect()
547 }
548
549 fn pushed_backgrounds(changes: &[CallbackChange]) -> Vec<(NodeId, StyleBackgroundContentVec)> {
550 pushed_pairs(changes)
551 .into_iter()
552 .filter_map(|(n, p)| match p {
553 CssProperty::BackgroundContent(b) => b.get_property().cloned().map(|b| (n, b)),
554 _ => None,
555 })
556 .collect()
557 }
558
559 fn pushed_margins(changes: &[CallbackChange]) -> Vec<(NodeId, f32)> {
560 pushed_pairs(changes)
561 .into_iter()
562 .filter_map(|(n, p)| match p {
563 CssProperty::MarginLeft(m) => m.get_property().map(|m| (n, px(&m.inner))),
564 _ => None,
565 })
566 .collect()
567 }
568
569 fn properties(v: &CssPropertyWithConditionsVec) -> Vec<CssProperty> {
574 v.as_ref().iter().map(|p| p.property.clone()).collect()
575 }
576
577 fn find<T>(v: &CssPropertyWithConditionsVec, f: impl Fn(&CssProperty) -> Option<T>) -> Option<T> {
578 v.as_ref().iter().find_map(|p| f(&p.property))
579 }
580
581 fn px(pv: &PixelValue) -> f32 {
586 assert_eq!(
587 pv.metric,
588 SizeMetric::Px,
589 "switch geometry must be absolute px, got {:?}",
590 pv.metric,
591 );
592 pv.number.get()
593 }
594
595 fn width_px(v: &CssPropertyWithConditionsVec) -> Option<f32> {
596 find(v, |p| match p {
597 CssProperty::Width(w) => match w.get_property() {
598 Some(LayoutWidth::Px(pv)) => Some(px(pv)),
599 _ => None,
600 },
601 _ => None,
602 })
603 }
604
605 fn height_px(v: &CssPropertyWithConditionsVec) -> Option<f32> {
606 find(v, |p| match p {
607 CssProperty::Height(h) => match h.get_property() {
608 Some(LayoutHeight::Px(pv)) => Some(px(pv)),
609 _ => None,
610 },
611 _ => None,
612 })
613 }
614
615 fn margin_left_px(v: &CssPropertyWithConditionsVec) -> Option<f32> {
618 find(v, |p| match p {
619 CssProperty::MarginLeft(m) => m.get_property().map(|m| px(&m.inner)),
620 _ => None,
621 })
622 }
623
624 fn paddings_px(v: &CssPropertyWithConditionsVec) -> (Option<f32>, Option<f32>, Option<f32>, Option<f32>) {
626 let get = |f: fn(&CssProperty) -> Option<f32>| find(v, f);
627 (
628 get(|p| match p {
629 CssProperty::PaddingTop(x) => x.get_property().map(|x| px(&x.inner)),
630 _ => None,
631 }),
632 get(|p| match p {
633 CssProperty::PaddingRight(x) => x.get_property().map(|x| px(&x.inner)),
634 _ => None,
635 }),
636 get(|p| match p {
637 CssProperty::PaddingBottom(x) => x.get_property().map(|x| px(&x.inner)),
638 _ => None,
639 }),
640 get(|p| match p {
641 CssProperty::PaddingLeft(x) => x.get_property().map(|x| px(&x.inner)),
642 _ => None,
643 }),
644 )
645 }
646
647 fn radii_px(v: &CssPropertyWithConditionsVec) -> Vec<f32> {
650 v.as_ref()
651 .iter()
652 .filter_map(|p| match &p.property {
653 CssProperty::BorderTopLeftRadius(r) => r.get_property().map(|r| px(&r.inner)),
654 CssProperty::BorderTopRightRadius(r) => r.get_property().map(|r| px(&r.inner)),
655 CssProperty::BorderBottomLeftRadius(r) => r.get_property().map(|r| px(&r.inner)),
656 CssProperty::BorderBottomRightRadius(r) => r.get_property().map(|r| px(&r.inner)),
657 _ => None,
658 })
659 .collect()
660 }
661
662 fn flex_grow(v: &CssPropertyWithConditionsVec) -> Option<f32> {
663 find(v, |p| match p {
664 CssProperty::FlexGrow(g) => g.get_property().map(|g| g.inner.get()),
665 _ => None,
666 })
667 }
668
669 fn background(v: &CssPropertyWithConditionsVec) -> Option<StyleBackgroundContentVec> {
670 find(v, |p| match p {
671 CssProperty::BackgroundContent(b) => b.get_property().cloned(),
672 _ => None,
673 })
674 }
675
676 fn solid_colors(bg: &StyleBackgroundContentVec) -> Vec<ColorU> {
679 bg.as_ref()
680 .iter()
681 .filter_map(|c| match c {
682 StyleBackgroundContent::Color(c) => Some(*c),
683 _ => None,
684 })
685 .collect()
686 }
687
688 fn px_values(v: &CssPropertyWithConditionsVec) -> Vec<f32> {
690 v.as_ref()
691 .iter()
692 .filter_map(|p| match &p.property {
693 CssProperty::Width(x) => match x.get_property() {
694 Some(LayoutWidth::Px(pv)) => Some(px(pv)),
695 _ => None,
696 },
697 CssProperty::Height(x) => match x.get_property() {
698 Some(LayoutHeight::Px(pv)) => Some(px(pv)),
699 _ => None,
700 },
701 CssProperty::PaddingTop(x) => x.get_property().map(|x| px(&x.inner)),
702 CssProperty::PaddingRight(x) => x.get_property().map(|x| px(&x.inner)),
703 CssProperty::PaddingBottom(x) => x.get_property().map(|x| px(&x.inner)),
704 CssProperty::PaddingLeft(x) => x.get_property().map(|x| px(&x.inner)),
705 CssProperty::MarginLeft(x) => x.get_property().map(|x| px(&x.inner)),
706 CssProperty::BorderTopLeftRadius(x) => x.get_property().map(|x| px(&x.inner)),
707 CssProperty::BorderTopRightRadius(x) => x.get_property().map(|x| px(&x.inner)),
708 CssProperty::BorderBottomLeftRadius(x) => x.get_property().map(|x| px(&x.inner)),
709 CssProperty::BorderBottomRightRadius(x) => x.get_property().map(|x| px(&x.inner)),
710 _ => None,
711 })
712 .collect()
713 }
714
715 fn classes(dom: &Dom) -> Vec<String> {
716 dom.root
717 .get_ids_and_classes()
718 .as_ref()
719 .iter()
720 .filter_map(|c| match c {
721 IdOrClass::Class(s) => Some(s.as_str().to_string()),
722 IdOrClass::Id(_) => None,
723 })
724 .collect()
725 }
726
727 fn inline_properties(dom: &Dom) -> Vec<CssProperty> {
729 dom.root
730 .style
731 .iter_inline_properties()
732 .map(|(p, _)| p.clone())
733 .collect()
734 }
735
736 #[derive(Debug, Clone, PartialEq, Eq)]
744 struct ToggleLog {
745 seen: Vec<bool>,
746 payload: u32,
747 }
748
749 extern "C" fn record_toggle(mut data: RefAny, _info: CallbackInfo, state: SwitchState) -> Update {
750 if let Some(mut log) = data.downcast_mut::<ToggleLog>() {
751 log.seen.push(state.checked);
752 }
753 Update::RefreshDom
754 }
755
756 extern "C" fn toggle_do_nothing(
757 _data: RefAny,
758 _info: CallbackInfo,
759 _state: SwitchState,
760 ) -> Update {
761 Update::DoNothing
762 }
763
764 extern "C" fn toggle_refresh_all(
765 _data: RefAny,
766 _info: CallbackInfo,
767 _state: SwitchState,
768 ) -> Update {
769 Update::RefreshDomAllWindows
770 }
771
772 extern "C" fn generic_shaped(_data: RefAny, _info: CallbackInfo) -> Update {
775 Update::DoNothing
776 }
777
778 fn log_refany() -> RefAny {
779 RefAny::new(ToggleLog {
780 seen: Vec::new(),
781 payload: 0xDEAD_BEEF,
782 })
783 }
784
785 fn read_log(probe: &RefAny) -> ToggleLog {
786 let mut probe = probe.clone();
787 let log = probe
788 .downcast_ref::<ToggleLog>()
789 .expect("the user payload changed type");
790 log.clone()
791 }
792
793 #[test]
798 fn knob_travel_matches_its_documented_formula_and_is_positive() {
799 assert_eq!(
802 KNOB_TRAVEL,
803 TRACK_WIDTH - (2 * TRACK_PADDING) - KNOB_SIZE,
804 "KNOB_TRAVEL no longer matches its own documented formula",
805 );
806 assert!(
807 KNOB_TRAVEL > 0,
808 "the knob has no room to travel: KNOB_TRAVEL = {KNOB_TRAVEL}",
809 );
810 assert_eq!(KNOB_TRAVEL as f32, TRAVEL);
811 }
812
813 #[test]
814 fn the_knob_exactly_fills_the_track_from_edge_to_edge_when_on() {
815 assert_eq!(
818 TRACK_PADDING + KNOB_TRAVEL + KNOB_SIZE + TRACK_PADDING,
819 TRACK_WIDTH,
820 "the switched-on knob does not sit flush against the track's right padding edge",
821 );
822 }
823
824 #[test]
825 fn the_knob_exactly_fills_the_tracks_vertical_padding_box() {
826 assert_eq!(
829 KNOB_SIZE,
830 TRACK_HEIGHT - (2 * TRACK_PADDING),
831 "the knob no longer fits the track's vertical padding box",
832 );
833 }
834
835 #[test]
836 fn both_shapes_are_fully_round_not_merely_rounded() {
837 assert_eq!(TRACK_RADIUS * 2, TRACK_HEIGHT, "the track is not a pill");
840 assert_eq!(KNOB_RADIUS * 2, KNOB_SIZE, "the knob is not a circle");
841 }
842
843 #[test]
844 fn every_geometry_constant_survives_floatvalues_fixed_point_encoding() {
845 const MULT: isize = 1000;
850 for (name, v) in [
851 ("TRACK_WIDTH", TRACK_WIDTH),
852 ("TRACK_HEIGHT", TRACK_HEIGHT),
853 ("TRACK_PADDING", TRACK_PADDING),
854 ("TRACK_RADIUS", TRACK_RADIUS),
855 ("KNOB_SIZE", KNOB_SIZE),
856 ("KNOB_RADIUS", KNOB_RADIUS),
857 ("KNOB_TRAVEL", KNOB_TRAVEL),
858 ] {
859 assert!(
860 v.checked_mul(MULT).is_some(),
861 "{name} = {v} overflows FloatValue's fixed-point encoding",
862 );
863 assert!(v >= 0, "{name} = {v} is negative");
864
865 let encoded = LayoutMarginLeft::const_px(v);
867 assert_eq!(
868 px(&encoded.inner),
869 v as f32,
870 "{name} = {v} does not round-trip through PixelValue",
871 );
872 }
873 }
874
875 #[test]
876 fn no_declared_px_value_is_nan_infinite_or_negative() {
877 for checked in [false, true] {
878 for (name, v) in [
879 ("track", build_track_style(checked)),
880 ("knob", build_knob_style(checked)),
881 ] {
882 let values = px_values(&v);
883 assert!(
884 !values.is_empty(),
885 "checked={checked}: the {name} style declares no px lengths at all",
886 );
887 for x in values {
888 assert!(
889 x.is_finite(),
890 "checked={checked}: the {name} style declares a non-finite length {x}",
891 );
892 assert!(
893 x >= 0.0,
894 "checked={checked}: the {name} style declares a negative length {x}",
895 );
896 }
897 }
898 }
899 }
900
901 #[test]
906 fn build_track_style_is_pure() {
907 for checked in [false, true] {
908 assert_eq!(
909 properties(&build_track_style(checked)),
910 properties(&build_track_style(checked)),
911 "build_track_style({checked}) is not deterministic",
912 );
913 }
914 }
915
916 #[test]
917 fn build_track_style_differs_between_the_two_states_only_in_the_background() {
918 let on = properties(&build_track_style(true));
921 let off = properties(&build_track_style(false));
922 assert_eq!(
923 on.len(),
924 off.len(),
925 "the two track styles declare a different number of properties",
926 );
927
928 let differing: Vec<_> = on
929 .iter()
930 .zip(off.iter())
931 .filter(|(a, b)| a != b)
932 .map(|(a, _)| discriminant(a))
933 .collect();
934 assert_eq!(
935 differing,
936 vec![discriminant(&CssProperty::const_background_content(TRACK_ON_BG))],
937 "the on/off track styles differ in something other than the background",
938 );
939 }
940
941 #[test]
942 fn build_track_style_maps_on_to_green_and_off_to_grey() {
943 let on = background(&build_track_style(true)).expect("the on track has no background");
946 let off = background(&build_track_style(false)).expect("the off track has no background");
947
948 assert_eq!(solid_colors(&on), vec![TRACK_ON_COLOR]);
949 assert_eq!(solid_colors(&off), vec![TRACK_OFF_COLOR]);
950 assert_ne!(
951 solid_colors(&on),
952 solid_colors(&off),
953 "the on and off tracks are the same colour — the switch has no visible state",
954 );
955 }
956
957 #[test]
958 fn the_track_background_is_exactly_one_fully_opaque_solid_layer() {
959 for checked in [false, true] {
962 let bg = background(&build_track_style(checked)).expect("no background");
963 assert_eq!(
964 bg.as_ref().len(),
965 1,
966 "checked={checked}: the track stacks {} background layers",
967 bg.as_ref().len(),
968 );
969 let colors = solid_colors(&bg);
970 assert_eq!(
971 colors.len(),
972 1,
973 "checked={checked}: the track background is not a plain solid colour",
974 );
975 assert_eq!(
976 colors[0].a, 255,
977 "checked={checked}: the track background is translucent (a = {})",
978 colors[0].a,
979 );
980 }
981 }
982
983 #[test]
984 fn build_track_style_geometry_is_absolute_px_in_both_states() {
985 for checked in [false, true] {
986 let v = build_track_style(checked);
987 assert_eq!(width_px(&v), Some(TRACK_W), "checked={checked}: track width");
990 assert_eq!(height_px(&v), Some(TRACK_H), "checked={checked}: track height");
991 assert_eq!(
992 paddings_px(&v),
993 (Some(PAD), Some(PAD), Some(PAD), Some(PAD)),
994 "checked={checked}: the track padding is not uniform",
995 );
996 assert_eq!(
997 radii_px(&v),
998 vec![TRACK_R; 4],
999 "checked={checked}: the track's four corners are not all {TRACK_R}px",
1000 );
1001 }
1002 }
1003
1004 #[test]
1005 fn build_track_style_declares_no_property_twice() {
1006 for checked in [false, true] {
1009 let props = properties(&build_track_style(checked));
1010 let mut seen = Vec::new();
1011 for p in &props {
1012 let d = discriminant(p);
1013 assert!(
1014 !seen.contains(&d),
1015 "checked={checked}: the track style declares {p:?} twice",
1016 );
1017 seen.push(d);
1018 }
1019 }
1020 }
1021
1022 #[test]
1023 fn build_track_style_marks_the_track_as_clickable_and_non_growing() {
1024 for checked in [false, true] {
1025 let v = build_track_style(checked);
1026 let cursor = find(&v, |p| match p {
1027 CssProperty::Cursor(c) => c.get_property().copied(),
1028 _ => None,
1029 });
1030 assert_eq!(
1031 cursor,
1032 Some(StyleCursor::Pointer),
1033 "checked={checked}: the switch does not present as clickable",
1034 );
1035 assert_eq!(
1038 flex_grow(&v),
1039 Some(0.0),
1040 "checked={checked}: the track is allowed to grow",
1041 );
1042 }
1043 }
1044
1045 #[test]
1046 fn build_track_style_lays_the_knob_out_as_a_centred_row() {
1047 for checked in [false, true] {
1050 let v = build_track_style(checked);
1051 assert_eq!(
1052 find(&v, |p| match p {
1053 CssProperty::Display(d) => d.get_property().copied(),
1054 _ => None,
1055 }),
1056 Some(LayoutDisplay::Flex),
1057 );
1058 assert_eq!(
1059 find(&v, |p| match p {
1060 CssProperty::FlexDirection(d) => d.get_property().copied(),
1061 _ => None,
1062 }),
1063 Some(LayoutFlexDirection::Row),
1064 "checked={checked}: margin-left only slides the knob in a row container",
1065 );
1066 assert_eq!(
1067 find(&v, |p| match p {
1068 CssProperty::AlignItems(a) => a.get_property().copied(),
1069 _ => None,
1070 }),
1071 Some(LayoutAlignItems::Center),
1072 );
1073 }
1074 }
1075
1076 #[test]
1081 fn build_knob_style_is_pure() {
1082 for checked in [false, true] {
1083 assert_eq!(
1084 properties(&build_knob_style(checked)),
1085 properties(&build_knob_style(checked)),
1086 "build_knob_style({checked}) is not deterministic",
1087 );
1088 }
1089 }
1090
1091 #[test]
1092 fn build_knob_style_differs_between_the_two_states_only_in_margin_left() {
1093 let on = properties(&build_knob_style(true));
1094 let off = properties(&build_knob_style(false));
1095 assert_eq!(
1096 on.len(),
1097 off.len(),
1098 "the two knob styles declare a different number of properties",
1099 );
1100
1101 let differing: Vec<_> = on
1102 .iter()
1103 .zip(off.iter())
1104 .filter(|(a, b)| a != b)
1105 .map(|(a, _)| discriminant(a))
1106 .collect();
1107 assert_eq!(
1108 differing,
1109 vec![discriminant(&CssProperty::const_margin_left(
1110 LayoutMarginLeft::const_px(0)
1111 ))],
1112 "the on/off knob styles differ in something other than margin-left",
1113 );
1114 }
1115
1116 #[test]
1117 fn build_knob_style_parks_the_knob_left_when_off_and_right_when_on() {
1118 assert_eq!(
1119 margin_left_px(&build_knob_style(false)),
1120 Some(0.0),
1121 "the off knob is not flush against the track's left padding edge",
1122 );
1123 assert_eq!(
1124 margin_left_px(&build_knob_style(true)),
1125 Some(TRAVEL),
1126 "the on knob does not travel the full width of the track",
1127 );
1128 }
1129
1130 #[test]
1131 fn build_knob_style_geometry_is_a_circle_in_absolute_px() {
1132 for checked in [false, true] {
1133 let v = build_knob_style(checked);
1134 assert_eq!(width_px(&v), Some(KNOB), "checked={checked}: knob width");
1135 assert_eq!(height_px(&v), Some(KNOB), "checked={checked}: knob height");
1136 assert_eq!(
1137 width_px(&v),
1138 height_px(&v),
1139 "checked={checked}: the knob is not square, so it cannot be a circle",
1140 );
1141 assert_eq!(
1142 radii_px(&v),
1143 vec![KNOB_R; 4],
1144 "checked={checked}: the knob's four corners are not all {KNOB_R}px",
1145 );
1146 assert_eq!(
1147 flex_grow(&v),
1148 Some(0.0),
1149 "checked={checked}: the knob is allowed to grow and would fill the track",
1150 );
1151 }
1152 }
1153
1154 #[test]
1155 fn build_knob_style_declares_no_property_twice() {
1156 for checked in [false, true] {
1159 let props = properties(&build_knob_style(checked));
1160 let mut seen = Vec::new();
1161 for p in &props {
1162 let d = discriminant(p);
1163 assert!(
1164 !seen.contains(&d),
1165 "checked={checked}: the knob style declares {p:?} twice",
1166 );
1167 seen.push(d);
1168 }
1169 }
1170 }
1171
1172 #[test]
1173 fn the_knob_is_opaque_white_in_both_states() {
1174 for checked in [false, true] {
1177 let bg = background(&build_knob_style(checked)).expect("the knob has no background");
1178 assert_eq!(bg.as_ref().len(), 1, "checked={checked}: the knob stacks layers");
1179 assert_eq!(solid_colors(&bg), vec![KNOB_COLOR]);
1180 assert_eq!(
1181 KNOB_COLOR.a, 255,
1182 "the knob is translucent and would tint with the track",
1183 );
1184 assert_ne!(
1185 solid_colors(&bg),
1186 vec![TRACK_OFF_COLOR],
1187 "checked={checked}: the knob is the same colour as the off track",
1188 );
1189 assert_ne!(
1190 solid_colors(&bg),
1191 vec![TRACK_ON_COLOR],
1192 "checked={checked}: the knob is the same colour as the on track",
1193 );
1194 }
1195 }
1196
1197 #[test]
1198 fn the_knob_never_leaves_the_track_in_either_state() {
1199 let content_w = TRACK_W - 2.0 * PAD;
1202 for checked in [false, true] {
1203 let margin = margin_left_px(&build_knob_style(checked)).expect("no margin-left");
1204 let size = width_px(&build_knob_style(checked)).expect("no width");
1205
1206 assert!(margin >= 0.0, "checked={checked}: the knob is pushed off the left edge");
1207 assert!(
1208 margin + size <= content_w,
1209 "checked={checked}: the knob overhangs the track ({margin} + {size} > {content_w})",
1210 );
1211 assert!(
1212 height_px(&build_knob_style(checked)).expect("no height") <= TRACK_H - 2.0 * PAD,
1213 "checked={checked}: the knob overflows the track vertically",
1214 );
1215 }
1216 }
1217
1218 #[test]
1223 fn create_stores_the_flag_and_installs_no_callback() {
1224 for checked in [false, true] {
1225 let s = Switch::create(checked);
1226 assert_eq!(
1227 s.switch_state.inner.checked, checked,
1228 "create({checked}) did not store the flag it was given",
1229 );
1230 assert!(
1231 s.switch_state.on_toggle.as_ref().is_none(),
1232 "create({checked}) invented a toggle callback out of nowhere",
1233 );
1234 }
1235 }
1236
1237 #[test]
1238 fn create_is_pure_and_its_two_states_are_distinguishable() {
1239 assert_eq!(Switch::create(true), Switch::create(true));
1240 assert_eq!(Switch::create(false), Switch::create(false));
1241 assert_ne!(
1242 Switch::create(true),
1243 Switch::create(false),
1244 "an on and an off switch are indistinguishable",
1245 );
1246 }
1247
1248 #[test]
1249 fn create_wires_the_flag_through_to_both_style_builders() {
1250 for checked in [false, true] {
1254 let s = Switch::create(checked);
1255 assert_eq!(
1256 properties(&s.track_style),
1257 properties(&build_track_style(checked)),
1258 "create({checked}) did not build the track for state {checked}",
1259 );
1260 assert_eq!(
1261 properties(&s.knob_style),
1262 properties(&build_knob_style(checked)),
1263 "create({checked}) did not build the knob for state {checked}",
1264 );
1265 }
1266 }
1267
1268 #[test]
1269 fn the_rendered_colour_and_the_knob_position_always_agree_with_the_stored_flag() {
1270 for checked in [false, true] {
1271 let s = Switch::create(checked);
1272 let bg = background(&s.track_style).expect("no track background");
1273 let margin = margin_left_px(&s.knob_style).expect("no knob margin");
1274
1275 let (expected_color, expected_margin) = if s.switch_state.inner.checked {
1276 (TRACK_ON_COLOR, TRAVEL)
1277 } else {
1278 (TRACK_OFF_COLOR, 0.0)
1279 };
1280 assert_eq!(solid_colors(&bg), vec![expected_color]);
1281 assert_eq!(
1282 margin, expected_margin,
1283 "checked={checked}: the knob position contradicts the track colour",
1284 );
1285 }
1286 }
1287
1288 #[test]
1289 fn default_is_an_off_switch() {
1290 assert_eq!(Switch::default(), Switch::create(false));
1291 assert!(!Switch::default().switch_state.inner.checked);
1292 assert!(!SwitchState::default().checked, "the default SwitchState is not off");
1293 assert!(!SwitchStateWrapper::default().inner.checked);
1294 assert!(SwitchStateWrapper::default().on_toggle.as_ref().is_none());
1295 }
1296
1297 #[test]
1302 fn swap_with_default_returns_the_old_widget_and_leaves_an_off_switch_behind() {
1303 let mut s = Switch::create(true);
1304 let old = s.swap_with_default();
1305
1306 assert_eq!(old, Switch::create(true), "the old widget was not returned intact");
1307 assert_eq!(s, Switch::create(false), "what was left behind is not a fresh off switch");
1308 }
1309
1310 #[test]
1311 fn swap_with_default_on_an_already_default_widget_is_a_no_op() {
1312 let mut s = Switch::create(false);
1313 let old = s.swap_with_default();
1314 assert_eq!(old, s, "swapping a default with a default produced two different widgets");
1315 assert_eq!(old, Switch::create(false));
1316 }
1317
1318 #[test]
1319 fn swapping_twice_round_trips_the_original_widget() {
1320 let mut a = Switch::create(true);
1321 let mut b = a.swap_with_default(); let c = b.swap_with_default(); assert_eq!(c, Switch::create(true));
1325 assert_eq!(a, Switch::create(false));
1326 assert_eq!(b, Switch::create(false));
1327 }
1328
1329 #[test]
1330 fn swap_with_default_moves_the_toggle_callback_out_rather_than_copying_or_dropping_it() {
1331 let probe = log_refany();
1332 let mut s = Switch::create(true)
1333 .with_on_toggle(probe.clone(), record_toggle as SwitchOnToggleCallbackType);
1334
1335 let old = s.swap_with_default();
1336
1337 let moved = old
1339 .switch_state
1340 .on_toggle
1341 .as_ref()
1342 .expect("the toggle callback vanished during the swap");
1343 assert_eq!(
1344 moved.callback.cb as *const () as usize,
1345 record_toggle as SwitchOnToggleCallbackType as *const () as usize,
1346 "the fn pointer was mangled by the swap",
1347 );
1348
1349 assert!(
1352 s.switch_state.on_toggle.as_ref().is_none(),
1353 "the toggle callback was copied instead of moved",
1354 );
1355
1356 assert_eq!(read_log(&probe).payload, 0xDEAD_BEEF);
1358 }
1359
1360 #[test]
1365 fn set_on_toggle_stores_the_function_pointer_and_the_payload_verbatim() {
1366 let mut s = Switch::create(false);
1367 s.set_on_toggle(
1368 RefAny::new(0xDEAD_BEEF_u32),
1369 toggle_do_nothing as SwitchOnToggleCallbackType,
1370 );
1371
1372 let t = s
1373 .switch_state
1374 .on_toggle
1375 .as_ref()
1376 .expect("set_on_toggle did not store anything");
1377 assert_eq!(
1378 t.callback.cb as *const () as usize,
1379 toggle_do_nothing as SwitchOnToggleCallbackType as *const () as usize,
1380 "the fn pointer was corrupted on the way in",
1381 );
1382
1383 let mut data = t.refany.clone();
1384 assert_eq!(
1385 *data.downcast_ref::<u32>().expect("the payload changed type"),
1386 0xDEAD_BEEF,
1387 "the payload was corrupted",
1388 );
1389 assert!(
1390 data.downcast_ref::<u64>().is_none(),
1391 "downcasting to the wrong type must fail, not reinterpret the bytes",
1392 );
1393 }
1394
1395 #[test]
1396 fn set_on_toggle_replaces_rather_than_accumulates() {
1397 let first = log_refany();
1400 let mut s = Switch::create(false);
1401 s.set_on_toggle(first.clone(), toggle_do_nothing as SwitchOnToggleCallbackType);
1402 s.set_on_toggle(RefAny::new(1u8), toggle_refresh_all as SwitchOnToggleCallbackType);
1403
1404 let t = s.switch_state.on_toggle.as_ref().expect("the callback vanished");
1405 assert_eq!(
1406 t.callback.cb as *const () as usize,
1407 toggle_refresh_all as SwitchOnToggleCallbackType as *const () as usize,
1408 "the second set_on_toggle did not win",
1409 );
1410 assert_eq!(read_log(&first).payload, 0xDEAD_BEEF);
1412 }
1413
1414 #[test]
1415 fn set_on_toggle_does_not_disturb_the_state_or_the_styles() {
1416 for checked in [false, true] {
1417 let pristine = Switch::create(checked);
1418 let mut s = Switch::create(checked);
1419 s.set_on_toggle(RefAny::new(0u8), toggle_do_nothing as SwitchOnToggleCallbackType);
1420
1421 assert_eq!(
1422 s.switch_state.inner.checked, checked,
1423 "installing a callback flipped the switch",
1424 );
1425 assert_eq!(
1426 properties(&s.track_style),
1427 properties(&pristine.track_style),
1428 "installing a callback rewrote the track style",
1429 );
1430 assert_eq!(
1431 properties(&s.knob_style),
1432 properties(&pristine.knob_style),
1433 "installing a callback rewrote the knob style",
1434 );
1435 }
1436 }
1437
1438 #[test]
1439 fn with_on_toggle_is_exactly_set_on_toggle_in_builder_form() {
1440 let by_builder = Switch::create(true)
1441 .with_on_toggle(RefAny::new(7u32), toggle_do_nothing as SwitchOnToggleCallbackType);
1442
1443 let mut by_setter = Switch::create(true);
1444 by_setter.set_on_toggle(RefAny::new(7u32), toggle_do_nothing as SwitchOnToggleCallbackType);
1445
1446 assert_eq!(by_builder.switch_state.inner, by_setter.switch_state.inner);
1447 assert_eq!(
1448 properties(&by_builder.track_style),
1449 properties(&by_setter.track_style),
1450 );
1451 assert_eq!(
1452 properties(&by_builder.knob_style),
1453 properties(&by_setter.knob_style),
1454 );
1455
1456 let a = by_builder.switch_state.on_toggle.as_ref().expect("builder lost the callback");
1457 let b = by_setter.switch_state.on_toggle.as_ref().expect("setter lost the callback");
1458 assert_eq!(a.callback.cb as *const () as usize, b.callback.cb as *const () as usize);
1459
1460 let (mut a, mut b) = (a.refany.clone(), b.refany.clone());
1461 assert_eq!(
1462 *a.downcast_ref::<u32>().expect("builder payload changed type"),
1463 *b.downcast_ref::<u32>().expect("setter payload changed type"),
1464 );
1465 }
1466
1467 #[test]
1468 fn with_on_toggle_accepts_a_generic_callback_without_mangling_the_pointer() {
1469 let generic = Callback {
1473 cb: generic_shaped,
1474 ctx: azul_core::refany::OptionRefAny::None,
1475 };
1476 let expected = generic_shaped as *const () as usize;
1477
1478 let s = Switch::create(false).with_on_toggle(RefAny::new(0u8), generic);
1479 let t = s.switch_state.on_toggle.as_ref().expect("the generic callback was dropped");
1480 assert_eq!(
1481 t.callback.cb as *const () as usize,
1482 expected,
1483 "the Callback -> SwitchOnToggleCallback transmute mangled the pointer",
1484 );
1485 }
1486
1487 #[test]
1492 fn dom_builds_a_focusable_track_with_exactly_one_knob_child() {
1493 for checked in [false, true] {
1494 let dom = Switch::create(checked).dom();
1495
1496 assert!(matches!(dom.root.get_node_type(), NodeType::Div));
1497 assert_eq!(
1498 dom.root.flags.get_tab_index(),
1499 Some(TabIndex::Auto),
1500 "checked={checked}: the switch is not keyboard-focusable",
1501 );
1502 assert_eq!(classes(&dom), vec!["__azul-native-switch".to_string()]);
1503
1504 let children = dom.children.as_ref();
1505 assert_eq!(children.len(), 1, "checked={checked}: the switch must have exactly one knob");
1506 assert_eq!(
1507 classes(&children[0]),
1508 vec!["__azul-native-switch-knob".to_string()],
1509 "checked={checked}: the knob carries the wrong class (external CSS would miss it)",
1510 );
1511 assert!(
1512 children[0].children.as_ref().is_empty(),
1513 "checked={checked}: the knob grew children",
1514 );
1515 assert_eq!(
1516 children[0].root.flags.get_tab_index(),
1517 None,
1518 "checked={checked}: the knob is separately focusable, so Tab stops twice",
1519 );
1520 }
1521 }
1522
1523 #[test]
1524 fn dom_puts_the_track_style_on_the_track_and_the_knob_style_on_the_knob() {
1525 for checked in [false, true] {
1528 let s = Switch::create(checked);
1529 let track = properties(&s.track_style);
1530 let knob = properties(&s.knob_style);
1531
1532 let dom = s.dom();
1533 assert_eq!(
1534 inline_properties(&dom),
1535 track,
1536 "checked={checked}: the track style did not land on the track",
1537 );
1538 assert_eq!(
1539 inline_properties(&dom.children.as_ref()[0]),
1540 knob,
1541 "checked={checked}: the knob style did not land on the knob",
1542 );
1543 }
1544 }
1545
1546 #[test]
1547 fn dom_registers_exactly_one_mouse_up_handler_and_it_is_the_widgets_own() {
1548 for checked in [false, true] {
1549 let dom = Switch::create(checked).dom();
1550 let callbacks = dom.root.callbacks.as_ref();
1551
1552 assert_eq!(callbacks.len(), 1, "checked={checked}: expected exactly one callback");
1553 assert_eq!(
1554 callbacks[0].event,
1555 EventFilter::Hover(HoverEventFilter::MouseUp),
1556 "checked={checked}: the switch must toggle on mouse-up",
1557 );
1558 assert_eq!(
1559 callbacks[0].callback.cb,
1560 input::default_on_switch_clicked as usize,
1561 "checked={checked}: the registered handler is not default_on_switch_clicked",
1562 );
1563
1564 assert!(
1567 dom.children.as_ref()[0].root.callbacks.as_ref().is_empty(),
1568 "checked={checked}: the knob registered a handler of its own",
1569 );
1570 }
1571 }
1572
1573 #[test]
1574 fn dom_hands_the_widget_state_to_the_handler_not_the_user_payload() {
1575 for checked in [false, true] {
1580 let dom = Switch::create(checked)
1581 .with_on_toggle(RefAny::new(9u32), toggle_do_nothing as SwitchOnToggleCallbackType)
1582 .dom();
1583
1584 let mut state = dom.root.callbacks.as_ref()[0].refany.clone();
1585 let wrapper = state
1586 .downcast_ref::<SwitchStateWrapper>()
1587 .expect("the handler's RefAny is not a SwitchStateWrapper");
1588
1589 assert_eq!(
1590 wrapper.inner.checked, checked,
1591 "the on/off flag was lost on the way into the DOM",
1592 );
1593 assert!(
1594 wrapper.on_toggle.as_ref().is_some(),
1595 "the user's toggle callback was lost on the way into the DOM",
1596 );
1597 }
1598 }
1599
1600 #[test]
1601 fn dom_of_a_callback_less_switch_still_registers_the_toggle_handler() {
1602 let dom = Switch::create(false).dom();
1605 assert_eq!(dom.root.callbacks.as_ref().len(), 1);
1606
1607 let mut state = dom.root.callbacks.as_ref()[0].refany.clone();
1608 let wrapper = state.downcast_ref::<SwitchStateWrapper>().expect("wrong RefAny type");
1609 assert!(wrapper.on_toggle.as_ref().is_none());
1610 }
1611
1612 #[test]
1613 fn from_switch_for_dom_is_the_same_as_calling_dom() {
1614 for checked in [false, true] {
1615 let via_from = Dom::from(Switch::create(checked));
1616 let via_dom = Switch::create(checked).dom();
1617
1618 assert_eq!(classes(&via_from), classes(&via_dom));
1619 assert_eq!(inline_properties(&via_from), inline_properties(&via_dom));
1620 assert_eq!(via_from.children.as_ref().len(), via_dom.children.as_ref().len());
1621 assert_eq!(
1622 inline_properties(&via_from.children.as_ref()[0]),
1623 inline_properties(&via_dom.children.as_ref()[0]),
1624 );
1625 assert_eq!(
1626 via_from.root.callbacks.as_ref().len(),
1627 via_dom.root.callbacks.as_ref().len(),
1628 );
1629 assert_eq!(via_from.root.flags.get_tab_index(), via_dom.root.flags.get_tab_index());
1630 }
1631 }
1632
1633 #[test]
1634 fn the_rendered_dom_flattens_to_exactly_two_nodes() {
1635 for checked in [false, true] {
1638 let styled = StyledDom::create_from_dom(Switch::create(checked).dom());
1639 assert_eq!(
1640 styled.node_data.as_ref().len(),
1641 2,
1642 "checked={checked}: the switch no longer flattens to a track + a knob",
1643 );
1644 }
1645 }
1646
1647 #[test]
1652 fn clicking_an_off_switch_turns_it_on_and_pushes_the_on_visuals() {
1653 let (styled, state) = laid_out(Switch::create(false));
1654 assert!(!is_checked(&state));
1655
1656 let (update, changes) = click(styled, &state, node(TRACK));
1657
1658 assert!(is_checked(&state), "the click did not turn the switch on");
1659 assert!(
1660 matches!(update, Update::DoNothing),
1661 "with no user callback installed, the handler must report DoNothing",
1662 );
1663 assert_eq!(
1664 pushed_backgrounds(&changes)
1665 .iter()
1666 .map(|(n, b)| (*n, solid_colors(b)))
1667 .collect::<Vec<_>>(),
1668 vec![(NodeId::new(TRACK), vec![TRACK_ON_COLOR])],
1669 "turning the switch on did not repaint the *track* green",
1670 );
1671 assert_eq!(
1672 pushed_margins(&changes),
1673 vec![(NodeId::new(KNOB_NODE), TRAVEL)],
1674 "turning the switch on did not slide the *knob* right",
1675 );
1676 }
1677
1678 #[test]
1679 fn clicking_an_on_switch_turns_it_off_and_pushes_the_off_visuals() {
1680 let (styled, state) = laid_out(Switch::create(true));
1681 let (_, changes) = click(styled, &state, node(TRACK));
1682
1683 assert!(!is_checked(&state), "the click did not turn the switch off");
1684 assert_eq!(
1685 pushed_backgrounds(&changes)
1686 .iter()
1687 .map(|(n, b)| (*n, solid_colors(b)))
1688 .collect::<Vec<_>>(),
1689 vec![(NodeId::new(TRACK), vec![TRACK_OFF_COLOR])],
1690 );
1691 assert_eq!(pushed_margins(&changes), vec![(NodeId::new(KNOB_NODE), 0.0)]);
1692 }
1693
1694 #[test]
1695 fn a_click_pushes_exactly_what_a_freshly_created_switch_would_render() {
1696 for start in [false, true] {
1701 let (styled, state) = laid_out(Switch::create(start));
1702 let (_, changes) = click(styled, &state, node(TRACK));
1703
1704 let expected = Switch::create(!start);
1705 assert_eq!(
1706 pushed_backgrounds(&changes).into_iter().map(|(_, b)| b).collect::<Vec<_>>(),
1707 vec![background(&expected.track_style).expect("no background")],
1708 "start={start}: the clicked track colour differs from a freshly built one",
1709 );
1710 assert_eq!(
1711 pushed_margins(&changes).into_iter().map(|(_, m)| m).collect::<Vec<_>>(),
1712 vec![margin_left_px(&expected.knob_style).expect("no margin")],
1713 "start={start}: the clicked knob offset differs from a freshly built one",
1714 );
1715 }
1716 }
1717
1718 #[test]
1719 fn clicking_twice_returns_to_the_original_state() {
1720 let (styled, state) = laid_out(Switch::create(false));
1721
1722 let (styled2, _) = laid_out(Switch::create(false));
1726 let (_, first) = click(styled, &state, node(TRACK));
1727 let (_, second) = click(styled2, &state, node(TRACK));
1728
1729 assert!(!is_checked(&state), "two clicks did not return the switch to its original state");
1730 assert_eq!(pushed_margins(&first), vec![(NodeId::new(KNOB_NODE), TRAVEL)]);
1731 assert_eq!(pushed_margins(&second), vec![(NodeId::new(KNOB_NODE), 0.0)]);
1732 }
1733
1734 #[test]
1735 fn clicking_with_a_refany_of_the_wrong_type_is_a_silent_no_op() {
1736 let (styled, _) = laid_out(Switch::create(false));
1739 let foreign = RefAny::new(0xDEAD_BEEF_u32);
1740
1741 let (update, changes) = click(styled, &foreign, node(TRACK));
1742
1743 assert!(matches!(update, Update::DoNothing));
1744 assert!(changes.is_empty(), "the handler wrote to the DOM through a foreign RefAny");
1745
1746 let mut foreign = foreign;
1747 assert_eq!(
1748 *foreign.downcast_ref::<u32>().expect("the foreign payload was reinterpreted"),
1749 0xDEAD_BEEF,
1750 "the handler corrupted a RefAny it did not understand",
1751 );
1752 }
1753
1754 #[test]
1755 fn clicking_the_knob_itself_does_not_half_apply_the_toggle() {
1756 let (styled, state) = laid_out(Switch::create(false));
1761
1762 let (update, changes) = click(styled, &state, node(KNOB_NODE));
1763
1764 assert!(matches!(update, Update::DoNothing));
1765 assert!(changes.is_empty(), "a change was pushed for a node the handler could not resolve");
1766 assert!(
1767 !is_checked(&state),
1768 "the flag was flipped even though the knob could not be moved",
1769 );
1770 }
1771
1772 #[test]
1773 fn stale_or_missing_hit_ids_do_not_panic_or_toggle() {
1774 for hit in [node(2), node(99), node(usize::MAX - 1), node_none()] {
1780 let (styled, state) = laid_out(Switch::create(true));
1781 let (update, changes) = click(styled, &state, hit);
1782
1783 assert!(matches!(update, Update::DoNothing), "{hit:?}: a stale hit was acted on");
1784 assert!(changes.is_empty(), "{hit:?}: a stale hit pushed a DOM change");
1785 assert!(is_checked(&state), "{hit:?}: a stale hit toggled the switch");
1786 }
1787 }
1788
1789 #[test]
1790 fn the_toggle_callback_sees_the_new_state_and_its_verdict_is_forwarded() {
1791 let probe = log_refany();
1794 let (styled, state) = laid_out(
1795 Switch::create(false)
1796 .with_on_toggle(probe.clone(), record_toggle as SwitchOnToggleCallbackType),
1797 );
1798
1799 let (update, changes) = click(styled, &state, node(TRACK));
1800
1801 assert_eq!(
1802 read_log(&probe).seen,
1803 vec![true],
1804 "the toggle callback was not called exactly once with the NEW state",
1805 );
1806 assert!(
1807 matches!(update, Update::RefreshDom),
1808 "the user callback's Update was swallowed instead of forwarded",
1809 );
1810 assert_eq!(pushed_margins(&changes), vec![(NodeId::new(KNOB_NODE), TRAVEL)]);
1812 }
1813
1814 #[test]
1815 fn the_toggle_callback_receives_the_user_payload_not_the_widget_state() {
1816 let probe = log_refany();
1817 let (styled, state) = laid_out(
1818 Switch::create(true)
1819 .with_on_toggle(probe.clone(), record_toggle as SwitchOnToggleCallbackType),
1820 );
1821
1822 click(styled, &state, node(TRACK));
1823
1824 let log = read_log(&probe);
1825 assert_eq!(
1826 log.payload, 0xDEAD_BEEF,
1827 "the callback was handed something other than the user's own RefAny",
1828 );
1829 assert_eq!(log.seen, vec![false]);
1831 }
1832
1833 #[test]
1834 fn a_toggle_callback_that_declines_the_update_still_gets_the_visuals_synced() {
1835 let (styled, state) = laid_out(
1838 Switch::create(false)
1839 .with_on_toggle(RefAny::new(0u8), toggle_do_nothing as SwitchOnToggleCallbackType),
1840 );
1841
1842 let (update, changes) = click(styled, &state, node(TRACK));
1843
1844 assert!(matches!(update, Update::DoNothing));
1845 assert!(is_checked(&state));
1846 assert_eq!(
1847 pushed_margins(&changes),
1848 vec![(NodeId::new(KNOB_NODE), TRAVEL)],
1849 "a DoNothing user callback suppressed the knob slide",
1850 );
1851 }
1852
1853 #[test]
1854 fn a_toggle_callback_on_an_unresolvable_node_is_never_called_at_all() {
1855 let probe = log_refany();
1858 let (styled, state) = laid_out(
1859 Switch::create(false)
1860 .with_on_toggle(probe.clone(), record_toggle as SwitchOnToggleCallbackType),
1861 );
1862
1863 let (update, changes) = click(styled, &state, node(KNOB_NODE));
1864
1865 assert!(matches!(update, Update::DoNothing));
1866 assert!(changes.is_empty());
1867 assert!(!is_checked(&state));
1868 assert!(
1869 read_log(&probe).seen.is_empty(),
1870 "the user was notified of a toggle that never happened",
1871 );
1872 }
1873
1874 #[test]
1875 fn many_clicks_leave_the_flag_the_colour_and_the_knob_in_agreement() {
1876 let mut expected_on = false;
1880 let (_, state) = laid_out(Switch::create(false));
1881
1882 for i in 0..51u32 {
1883 let (styled, _) = laid_out(Switch::create(false));
1884 let (_, changes) = click(styled, &state, node(TRACK));
1885 expected_on = !expected_on;
1886
1887 let (color, margin) = if expected_on {
1888 (TRACK_ON_COLOR, TRAVEL)
1889 } else {
1890 (TRACK_OFF_COLOR, 0.0)
1891 };
1892 assert_eq!(
1893 pushed_backgrounds(&changes)
1894 .iter()
1895 .map(|(n, b)| (*n, solid_colors(b)))
1896 .collect::<Vec<_>>(),
1897 vec![(NodeId::new(TRACK), vec![color])],
1898 "click #{i}: the pushed track colour disagrees with the flag",
1899 );
1900 assert_eq!(
1901 pushed_margins(&changes),
1902 vec![(NodeId::new(KNOB_NODE), margin)],
1903 "click #{i}: the pushed knob offset disagrees with the flag",
1904 );
1905 assert_eq!(is_checked(&state), expected_on, "click #{i}: the flag drifted");
1906 }
1907
1908 assert!(is_checked(&state), "an odd number of clicks left the switch off");
1909 }
1910
1911 #[test]
1912 fn a_click_writes_to_two_distinct_nodes_and_never_to_the_wrong_one() {
1913 let (styled, state) = laid_out(Switch::create(false));
1916 let (_, changes) = click(styled, &state, node(TRACK));
1917
1918 let bg_nodes: Vec<_> = pushed_backgrounds(&changes).into_iter().map(|(n, _)| n).collect();
1919 let margin_nodes: Vec<_> = pushed_margins(&changes).into_iter().map(|(n, _)| n).collect();
1920
1921 assert_eq!(bg_nodes, vec![NodeId::new(TRACK)]);
1922 assert_eq!(margin_nodes, vec![NodeId::new(KNOB_NODE)]);
1923 assert_ne!(
1924 bg_nodes, margin_nodes,
1925 "the colour and the knob offset landed on the same node",
1926 );
1927 }
1928}