1use azul_core::{
4 callbacks::Update,
5 dom::Dom,
6 refany::RefAny,
7};
8use azul_css::dynamic_selector::{CssPropertyWithConditions, CssPropertyWithConditionsVec};
9#[allow(clippy::wildcard_imports)] use azul_css::{
11 props::{
12 basic::*,
13 layout::*,
14 property::{CssProperty, *},
15 style::*,
16 },
17 *,
18};
19
20use crate::callbacks::{Callback, CallbackInfo};
21
22#[derive(Debug, Default, Clone, PartialEq, Eq)]
24#[repr(C)]
25pub struct ColorInput {
26 pub color_input_state: ColorInputStateWrapper,
27 pub style: CssPropertyWithConditionsVec,
28}
29
30pub type ColorInputOnValueChangeCallbackType =
32 extern "C" fn(RefAny, CallbackInfo, ColorInputState) -> Update;
33impl_widget_callback!(
34 ColorInputOnValueChange,
35 OptionColorInputOnValueChange,
36 ColorInputOnValueChangeCallback,
37 ColorInputOnValueChangeCallbackType
38);
39
40azul_core::impl_managed_callback! {
41 wrapper: ColorInputOnValueChangeCallback,
42 info_ty: CallbackInfo,
43 return_ty: Update,
44 default_ret: Update::DoNothing,
45 invoker_static: COLOR_INPUT_ON_VALUE_CHANGE_INVOKER,
46 invoker_ty: AzColorInputOnValueChangeCallbackInvoker,
47 thunk_fn: az_color_input_on_value_change_callback_thunk,
48 setter_fn: AzApp_setColorInputOnValueChangeCallbackInvoker,
49 from_handle_fn: AzColorInputOnValueChangeCallback_createFromHostHandle,
50 extra_args: [ state: ColorInputState ],
51}
52
53#[derive(Debug, Clone, PartialEq, Eq, PartialOrd)]
55#[repr(C)]
56pub struct ColorInputStateWrapper {
57 pub inner: ColorInputState,
58 pub title: AzString,
59 pub on_value_change: OptionColorInputOnValueChange,
60}
61
62impl Default for ColorInputStateWrapper {
63 fn default() -> Self {
64 Self {
65 inner: ColorInputState::default(),
66 title: AzString::from_const_str("Pick color"),
67 on_value_change: None.into(),
68 }
69 }
70}
71
72#[derive(Copy, Debug, Clone, PartialEq, PartialOrd, Ord, Eq, Hash)]
74#[repr(C)]
75pub struct ColorInputState {
76 pub color: ColorU,
77}
78
79impl Default for ColorInputState {
80 fn default() -> Self {
81 Self {
82 color: ColorU {
83 r: 255,
84 g: 255,
85 b: 255,
86 a: 255,
87 },
88 }
89 }
90}
91
92static DEFAULT_COLOR_INPUT_STYLE: &[CssPropertyWithConditions] = &[
93 CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Block)),
94 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
95 CssPropertyWithConditions::simple(CssProperty::const_width(LayoutWidth::const_px(14))),
96 CssPropertyWithConditions::simple(CssProperty::const_height(LayoutHeight::const_px(14))),
97 CssPropertyWithConditions::simple(CssProperty::const_cursor(StyleCursor::Pointer)),
98];
99
100impl ColorInput {
101 #[inline]
103 #[must_use]
104 pub fn create(color: ColorU) -> Self {
105 Self {
106 color_input_state: ColorInputStateWrapper {
107 inner: ColorInputState { color },
108 ..Default::default()
109 },
110 style: CssPropertyWithConditionsVec::from_const_slice(DEFAULT_COLOR_INPUT_STYLE),
111 }
112 }
113
114 #[inline]
116 pub fn set_on_value_change<I: Into<ColorInputOnValueChangeCallback>>(
117 &mut self,
118 data: RefAny,
119 callback: I,
120 ) {
121 self.color_input_state.on_value_change = Some(ColorInputOnValueChange {
122 callback: callback.into(),
123 refany: data,
124 })
125 .into();
126 }
127
128 #[inline]
130 #[must_use]
131 pub fn with_on_value_change<C: Into<ColorInputOnValueChangeCallback>>(
132 mut self,
133 data: RefAny,
134 callback: C,
135 ) -> Self {
136 self.set_on_value_change(data, callback);
137 self
138 }
139
140 #[inline]
142 #[must_use]
143 pub fn swap_with_default(&mut self) -> Self {
144 let mut s = Self::default();
145 core::mem::swap(&mut s, self);
146 s
147 }
148
149 #[inline]
151 #[must_use]
152 pub fn dom(self) -> Dom {
153 use azul_core::{
154 callbacks::{CoreCallback, CoreCallbackData},
155 dom::{EventFilter, HoverEventFilter, IdOrClass::Class},
156 };
157
158 let mut style = self.style.into_library_owned_vec();
159 style.push(CssPropertyWithConditions::simple(
160 CssProperty::const_background_content(
161 vec![StyleBackgroundContent::Color(
162 self.color_input_state.inner.color,
163 )]
164 .into(),
165 ),
166 ));
167
168 Dom::create_div()
169 .with_ids_and_classes(vec![Class("__azul_native_color_input".into())].into())
170 .with_css_props(style.into())
171 .with_callbacks(
172 vec![CoreCallbackData {
173 event: EventFilter::Hover(HoverEventFilter::MouseUp),
174 refany: RefAny::new(self.color_input_state),
175 callback: CoreCallback {
176 cb: on_color_input_clicked as usize,
177 ctx: azul_core::refany::OptionRefAny::None,
178 },
179 }]
180 .into(),
181 )
182 }
183}
184
185extern "C" fn on_color_input_clicked(mut data: RefAny, mut info: CallbackInfo) -> Update {
186 let Some(mut color_input) = data.downcast_mut::<ColorInputStateWrapper>() else {
187 return Update::DoNothing;
188 };
189
190 let color_input = &mut *color_input;
193 let onvaluechange = &mut color_input.on_value_change;
194 let inner = color_input.inner;
195
196 match onvaluechange.as_mut() {
197 Some(ColorInputOnValueChange {
198 callback,
199 refany: data,
200 }) => (callback.cb)(data.clone(), info, inner),
201 None => Update::DoNothing,
202 }
203}
204
205#[cfg(all(test, feature = "std"))]
206#[allow(clippy::float_cmp, clippy::too_many_lines)]
207mod autotest_generated {
208 use std::{
209 collections::{hash_map::DefaultHasher, BTreeMap, HashMap},
210 hash::{Hash, Hasher},
211 mem::discriminant,
212 sync::{Arc, Mutex},
213 };
214
215 use azul_core::{
216 dom::{DomId, DomNodeId, EventFilter, HoverEventFilter, IdOrClass, NodeId, NodeType},
217 geom::{LogicalRect, OptionLogicalPosition},
218 gl::OptionGlContextPtr,
219 hit_test::ScrollPosition,
220 refany::OptionRefAny,
221 resources::RendererResources,
222 styled_dom::{NodeHierarchyItemId, StyledDom},
223 window::{MonitorVec, RawWindowHandle},
224 };
225 use azul_css::props::basic::{length::SizeMetric, pixel::PixelValue};
226 use rust_fontconfig::FcFontCache;
227
228 use super::*;
229 #[cfg(feature = "icu")]
230 use crate::icu::IcuLocalizerHandle;
231 use crate::{
232 callbacks::{CallbackChange, CallbackInfoRefData, ExternalSystemCallbacks},
233 solver3::{display_list::DisplayList, layout_tree::LayoutTree},
234 window::{DomLayoutResult, LayoutWindow},
235 window_state::FullWindowState,
236 };
237
238 const SIDE: f32 = 14.0;
244
245 const DEFAULT_TITLE: &str = "Pick color";
247
248 const DEFAULT_COLOR: ColorU = ColorU {
252 r: 255,
253 g: 255,
254 b: 255,
255 a: 255,
256 };
257
258 const SAMPLE_COLORS: [ColorU; 8] = [
263 ColorU { r: 0, g: 0, b: 0, a: 0 },
264 ColorU { r: 0, g: 0, b: 0, a: 255 },
265 ColorU { r: 255, g: 255, b: 255, a: 255 },
266 ColorU { r: 255, g: 255, b: 255, a: 0 },
267 ColorU { r: 255, g: 0, b: 0, a: 1 },
268 ColorU { r: 0, g: 255, b: 0, a: 254 },
269 ColorU { r: 1, g: 2, b: 3, a: 4 },
270 ColorU { r: 128, g: 64, b: 32, a: 16 },
271 ];
272
273 fn properties(v: &CssPropertyWithConditionsVec) -> Vec<CssProperty> {
278 v.as_ref().iter().map(|p| p.property.clone()).collect()
279 }
280
281 fn find<T>(v: &CssPropertyWithConditionsVec, f: impl Fn(&CssProperty) -> Option<T>) -> Option<T> {
282 v.as_ref().iter().find_map(|p| f(&p.property))
283 }
284
285 fn px(pv: &PixelValue) -> f32 {
289 assert_eq!(
290 pv.metric,
291 SizeMetric::Px,
292 "color-input geometry must be absolute px, got {:?}",
293 pv.metric,
294 );
295 pv.number.get()
296 }
297
298 fn width_px(v: &CssPropertyWithConditionsVec) -> Option<f32> {
299 find(v, |p| match p {
300 CssProperty::Width(w) => match w.get_property() {
301 Some(LayoutWidth::Px(pv)) => Some(px(pv)),
302 _ => None,
303 },
304 _ => None,
305 })
306 }
307
308 fn height_px(v: &CssPropertyWithConditionsVec) -> Option<f32> {
309 find(v, |p| match p {
310 CssProperty::Height(h) => match h.get_property() {
311 Some(LayoutHeight::Px(pv)) => Some(px(pv)),
312 _ => None,
313 },
314 _ => None,
315 })
316 }
317
318 fn background_color(v: &CssPropertyWithConditionsVec) -> Option<ColorU> {
320 v.as_ref().iter().find_map(|p| match &p.property {
321 CssProperty::BackgroundContent(b) => match b.get_property()?.as_ref().first()? {
322 StyleBackgroundContent::Color(c) => Some(*c),
323 _ => None,
324 },
325 _ => None,
326 })
327 }
328
329 fn classes(dom: &Dom) -> Vec<String> {
330 dom.root
331 .get_ids_and_classes()
332 .as_ref()
333 .iter()
334 .filter_map(|c| match c {
335 IdOrClass::Class(s) => Some(s.as_str().to_string()),
336 IdOrClass::Id(_) => None,
337 })
338 .collect()
339 }
340
341 fn inline_properties(dom: &Dom) -> Vec<CssProperty> {
343 dom.root
344 .style
345 .iter_inline_properties()
346 .map(|(p, _)| p.clone())
347 .collect()
348 }
349
350 fn dom_background(dom: &Dom) -> Option<ColorU> {
352 inline_properties(dom).into_iter().find_map(|p| match p {
353 CssProperty::BackgroundContent(b) => match b.get_property()?.as_ref().first()? {
354 StyleBackgroundContent::Color(c) => Some(*c),
355 _ => None,
356 },
357 _ => None,
358 })
359 }
360
361 fn expected_background(c: ColorU) -> CssProperty {
363 CssProperty::const_background_content(StyleBackgroundContentVec::from_vec(vec![
364 StyleBackgroundContent::Color(c),
365 ]))
366 }
367
368 fn hash_of<T: Hash>(t: &T) -> u64 {
369 let mut h = DefaultHasher::new();
370 t.hash(&mut h);
371 h.finish()
372 }
373
374 fn node(idx: usize) -> DomNodeId {
380 DomNodeId {
381 dom: DomId::ROOT_ID,
382 node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(idx))),
383 }
384 }
385
386 fn node_none() -> DomNodeId {
388 DomNodeId {
389 dom: DomId::ROOT_ID,
390 node: NodeHierarchyItemId::NONE,
391 }
392 }
393
394 fn layout_result(styled_dom: StyledDom) -> DomLayoutResult {
397 DomLayoutResult {
398 styled_dom,
399 layout_tree: LayoutTree {
400 nodes: Vec::new(),
401 warm: Vec::new(),
402 cold: Vec::new(),
403 root: 0,
404 dom_to_layout: BTreeMap::new(),
405 children_arena: Vec::new(),
406 children_offsets: Vec::new(),
407 subtree_needs_intrinsic: Vec::new(),
408 },
409 calculated_positions: Vec::new(),
410 viewport: LogicalRect::zero(),
411 display_list: DisplayList::default(),
412 scroll_ids: HashMap::new(),
413 scroll_id_to_node_id: HashMap::new(),
414 }
415 }
416
417 fn with_info<R>(
421 styled_dom: StyledDom,
422 hit: DomNodeId,
423 f: impl FnOnce(&mut CallbackInfo) -> R,
424 ) -> (R, Vec<CallbackChange>) {
425 let mut layout_window =
426 LayoutWindow::new(FcFontCache::default()).expect("LayoutWindow::new failed");
427 layout_window
428 .layout_results
429 .insert(DomId::ROOT_ID, layout_result(styled_dom));
430
431 let renderer_resources = RendererResources::default();
432 let previous_window_state: Option<FullWindowState> = None;
433 let current_window_state = FullWindowState::default();
434 let gl_context = OptionGlContextPtr::None;
435 let scroll_states: BTreeMap<DomId, BTreeMap<NodeHierarchyItemId, ScrollPosition>> =
436 BTreeMap::new();
437 let window_handle = RawWindowHandle::Unsupported;
438 let system_callbacks = ExternalSystemCallbacks::rust_internal();
439
440 let ref_data = CallbackInfoRefData {
441 layout_window: &layout_window,
442 renderer_resources: &renderer_resources,
443 previous_window_state: &previous_window_state,
444 current_window_state: ¤t_window_state,
445 gl_context: &gl_context,
446 current_scroll_manager: &scroll_states,
447 current_window_handle: &window_handle,
448 system_callbacks: &system_callbacks,
449 system_style: Arc::new(azul_css::system::SystemStyle::default()),
450 monitors: Arc::new(Mutex::new(MonitorVec::from_const_slice(&[]))),
451 #[cfg(feature = "icu")]
452 icu_localizer: IcuLocalizerHandle::default(),
453 ctx: OptionRefAny::None,
454 };
455
456 let changes: Arc<Mutex<Vec<CallbackChange>>> = Arc::new(Mutex::new(Vec::new()));
457
458 let mut info = CallbackInfo::new(
459 &ref_data,
460 &changes,
461 hit,
462 OptionLogicalPosition::None,
463 OptionLogicalPosition::None,
464 );
465
466 let r = f(&mut info);
467 let pushed = info.take_changes();
468 (r, pushed)
469 }
470
471 fn laid_out(color_input: ColorInput) -> (StyledDom, RefAny) {
476 let dom = color_input.dom();
477 let state = dom.root.callbacks.as_ref()[0].refany.clone();
478 (StyledDom::create_from_dom(dom), state)
479 }
480
481 fn click(styled_dom: StyledDom, state: &RefAny, hit: DomNodeId) -> (Update, Vec<CallbackChange>) {
483 with_info(styled_dom, hit, |info| {
484 on_color_input_clicked(state.clone(), *info)
485 })
486 }
487
488 fn state_color(state: &RefAny) -> ColorU {
489 let mut state = state.clone();
490 let wrapper = state
491 .downcast_ref::<ColorInputStateWrapper>()
492 .expect("the widget state changed type");
493 wrapper.inner.color
494 }
495
496 #[derive(Debug, Clone, PartialEq, Eq)]
500 struct ColorLog {
501 seen: Vec<ColorU>,
502 payload: u32,
503 }
504
505 extern "C" fn record_value(
506 mut data: RefAny,
507 _info: CallbackInfo,
508 state: ColorInputState,
509 ) -> Update {
510 if let Some(mut log) = data.downcast_mut::<ColorLog>() {
511 log.seen.push(state.color);
512 }
513 Update::RefreshDom
514 }
515
516 extern "C" fn value_do_nothing(
517 _data: RefAny,
518 _info: CallbackInfo,
519 _state: ColorInputState,
520 ) -> Update {
521 Update::DoNothing
522 }
523
524 extern "C" fn value_refresh_all(
525 _data: RefAny,
526 _info: CallbackInfo,
527 _state: ColorInputState,
528 ) -> Update {
529 Update::RefreshDomAllWindows
530 }
531
532 extern "C" fn generic_shaped(_data: RefAny, _info: CallbackInfo) -> Update {
535 Update::DoNothing
536 }
537
538 fn log_refany() -> RefAny {
539 RefAny::new(ColorLog {
540 seen: Vec::new(),
541 payload: 0xDEAD_BEEF,
542 })
543 }
544
545 fn read_log(probe: &RefAny) -> ColorLog {
546 let mut probe = probe.clone();
547 let log = probe
548 .downcast_ref::<ColorLog>()
549 .expect("the user payload changed type");
550 log.clone()
551 }
552
553 #[test]
558 fn create_stores_every_channel_verbatim() {
559 for c in SAMPLE_COLORS {
562 let w = ColorInput::create(c);
563 assert_eq!(
564 w.color_input_state.inner.color, c,
565 "create({c:?}) did not store the color it was given",
566 );
567 }
568 }
569
570 #[test]
571 fn create_installs_no_callback_and_the_default_title() {
572 for c in SAMPLE_COLORS {
573 let w = ColorInput::create(c);
574 assert!(
575 w.color_input_state.on_value_change.as_ref().is_none(),
576 "create({c:?}) invented a value-change callback out of nowhere",
577 );
578 assert_eq!(
579 w.color_input_state.title.as_str(),
580 DEFAULT_TITLE,
581 "create({c:?}) did not keep the default title",
582 );
583 }
584 }
585
586 #[test]
587 fn create_is_pure_and_distinguishes_every_sample_color() {
588 for c in SAMPLE_COLORS {
589 assert_eq!(
590 ColorInput::create(c),
591 ColorInput::create(c),
592 "create({c:?}) is not deterministic",
593 );
594 }
595 for (i, a) in SAMPLE_COLORS.iter().enumerate() {
596 for b in &SAMPLE_COLORS[i + 1..] {
597 assert_ne!(
598 ColorInput::create(*a),
599 ColorInput::create(*b),
600 "the widgets for {a:?} and {b:?} are indistinguishable",
601 );
602 }
603 }
604 }
605
606 #[test]
607 fn create_treats_alpha_as_significant() {
608 let opaque = ColorU { r: 255, g: 0, b: 0, a: 255 };
611 let clear = ColorU { r: 255, g: 0, b: 0, a: 0 };
612 assert_ne!(
613 ColorInput::create(opaque),
614 ColorInput::create(clear),
615 "a transparent swatch compares equal to an opaque one",
616 );
617 }
618
619 #[test]
620 fn create_geometry_is_absolute_14px_for_every_color() {
621 for c in SAMPLE_COLORS {
623 let w = ColorInput::create(c);
624 assert_eq!(width_px(&w.style), Some(SIDE), "{c:?}: wrong swatch width");
625 assert_eq!(height_px(&w.style), Some(SIDE), "{c:?}: wrong swatch height");
626 }
627 }
628
629 #[test]
630 fn create_marks_the_swatch_as_clickable() {
631 let props = properties(&ColorInput::create(DEFAULT_COLOR).style);
634 assert!(
635 props.contains(&CssProperty::const_cursor(StyleCursor::Pointer)),
636 "the color input does not present as clickable: {props:?}",
637 );
638 }
639
640 #[test]
641 fn create_is_a_non_growing_block() {
642 let props = properties(&ColorInput::create(DEFAULT_COLOR).style);
645 assert!(
646 props.contains(&CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
647 "the swatch is allowed to flex-grow: {props:?}",
648 );
649 assert!(
650 props.contains(&CssProperty::const_display(LayoutDisplay::Block)),
651 "the swatch is not a block box: {props:?}",
652 );
653 }
654
655 #[test]
656 fn create_declares_no_property_twice() {
657 let props = properties(&ColorInput::create(DEFAULT_COLOR).style);
660 let mut seen = Vec::new();
661 for p in &props {
662 let d = discriminant(p);
663 assert!(!seen.contains(&d), "the base style declares {p:?} twice");
664 seen.push(d);
665 }
666 }
667
668 #[test]
669 fn create_keeps_the_color_out_of_the_base_style() {
670 for c in SAMPLE_COLORS {
674 assert_eq!(
675 background_color(&ColorInput::create(c).style),
676 None,
677 "create({c:?}) leaked the color into the base style",
678 );
679 }
680 }
681
682 #[test]
683 fn create_style_does_not_depend_on_the_color() {
684 let reference = properties(&ColorInput::create(SAMPLE_COLORS[0]).style);
685 for c in SAMPLE_COLORS {
686 assert_eq!(
687 properties(&ColorInput::create(c).style),
688 reference,
689 "create({c:?}) produced a different style than create({:?})",
690 SAMPLE_COLORS[0],
691 );
692 }
693 }
694
695 #[test]
700 fn the_default_color_is_opaque_white_not_colorus_own_default() {
701 assert_eq!(ColorInputState::default().color, DEFAULT_COLOR);
705 assert_ne!(
706 ColorInputState::default().color,
707 ColorU::default(),
708 "the color input's default silently became ColorU::default()",
709 );
710 assert_eq!(ColorInputStateWrapper::default().inner.color, DEFAULT_COLOR);
711 assert_eq!(
712 ColorInputStateWrapper::default().title.as_str(),
713 DEFAULT_TITLE,
714 );
715 assert!(ColorInputStateWrapper::default()
716 .on_value_change
717 .as_ref()
718 .is_none());
719 }
720
721 #[test]
722 fn color_input_state_ord_and_partial_ord_agree() {
723 for a in SAMPLE_COLORS {
726 for b in SAMPLE_COLORS {
727 let (x, y) = (ColorInputState { color: a }, ColorInputState { color: b });
728 assert_eq!(
729 x.partial_cmp(&y),
730 Some(x.cmp(&y)),
731 "PartialOrd and Ord disagree for {a:?} vs {b:?}",
732 );
733 assert_eq!(
734 x == y,
735 x.cmp(&y) == core::cmp::Ordering::Equal,
736 "Eq and Ord disagree for {a:?} vs {b:?}",
737 );
738 }
739 }
740 }
741
742 #[test]
743 fn equal_color_input_states_hash_equal() {
744 for c in SAMPLE_COLORS {
747 let a = ColorInputState { color: c };
748 let b = ColorInputState { color: c };
749 assert_eq!(a, b);
750 assert_eq!(hash_of(&a), hash_of(&b), "equal states hash differently ({c:?})");
751 }
752 }
753
754 #[test]
755 fn color_input_state_equality_is_channel_exact() {
756 let base = ColorU { r: 10, g: 20, b: 30, a: 40 };
758 let variants = [
759 ColorU { r: 11, ..base },
760 ColorU { g: 21, ..base },
761 ColorU { b: 31, ..base },
762 ColorU { a: 41, ..base },
763 ];
764 for v in variants {
765 assert_ne!(
766 ColorInputState { color: base },
767 ColorInputState { color: v },
768 "a one-channel difference ({base:?} vs {v:?}) was swallowed",
769 );
770 }
771 }
772
773 #[test]
778 fn set_on_value_change_stores_the_function_pointer_and_the_payload_verbatim() {
779 let mut w = ColorInput::create(DEFAULT_COLOR);
780 w.set_on_value_change(
781 RefAny::new(0xDEAD_BEEF_u32),
782 value_do_nothing as ColorInputOnValueChangeCallbackType,
783 );
784
785 let t = w
786 .color_input_state
787 .on_value_change
788 .as_ref()
789 .expect("set_on_value_change did not store anything");
790 assert_eq!(
791 t.callback.cb as *const () as usize,
792 value_do_nothing as ColorInputOnValueChangeCallbackType as *const () as usize,
793 "the fn pointer was corrupted on the way in",
794 );
795
796 let mut data = t.refany.clone();
797 assert_eq!(
798 *data.downcast_ref::<u32>().expect("the payload changed type"),
799 0xDEAD_BEEF,
800 "the payload was corrupted",
801 );
802 assert!(
803 data.downcast_ref::<u64>().is_none(),
804 "downcasting to the wrong type must fail, not reinterpret the bytes",
805 );
806 }
807
808 #[test]
809 fn set_on_value_change_replaces_rather_than_accumulates() {
810 let first = log_refany();
813 let mut w = ColorInput::create(DEFAULT_COLOR);
814 w.set_on_value_change(
815 first.clone(),
816 value_do_nothing as ColorInputOnValueChangeCallbackType,
817 );
818 w.set_on_value_change(
819 RefAny::new(1u8),
820 value_refresh_all as ColorInputOnValueChangeCallbackType,
821 );
822
823 let t = w
824 .color_input_state
825 .on_value_change
826 .as_ref()
827 .expect("the callback vanished");
828 assert_eq!(
829 t.callback.cb as *const () as usize,
830 value_refresh_all as ColorInputOnValueChangeCallbackType as *const () as usize,
831 "the second set_on_value_change did not win",
832 );
833 assert_eq!(read_log(&first).payload, 0xDEAD_BEEF);
835 }
836
837 #[test]
838 fn set_on_value_change_does_not_disturb_the_color_or_the_style() {
839 for c in SAMPLE_COLORS {
840 let pristine = ColorInput::create(c);
841 let mut w = ColorInput::create(c);
842 w.set_on_value_change(
843 RefAny::new(0u8),
844 value_do_nothing as ColorInputOnValueChangeCallbackType,
845 );
846
847 assert_eq!(
848 w.color_input_state.inner.color, c,
849 "installing a callback rewrote the color",
850 );
851 assert_eq!(
852 properties(&w.style),
853 properties(&pristine.style),
854 "installing a callback rewrote the style",
855 );
856 assert_eq!(
857 w.color_input_state.title.as_str(),
858 pristine.color_input_state.title.as_str(),
859 "installing a callback rewrote the title",
860 );
861 }
862 }
863
864 #[test]
865 fn with_on_value_change_is_exactly_set_on_value_change_in_builder_form() {
866 let by_builder = ColorInput::create(SAMPLE_COLORS[6]).with_on_value_change(
867 RefAny::new(7u32),
868 value_do_nothing as ColorInputOnValueChangeCallbackType,
869 );
870
871 let mut by_setter = ColorInput::create(SAMPLE_COLORS[6]);
872 by_setter.set_on_value_change(
873 RefAny::new(7u32),
874 value_do_nothing as ColorInputOnValueChangeCallbackType,
875 );
876
877 assert_eq!(by_builder.color_input_state.inner, by_setter.color_input_state.inner);
878 assert_eq!(properties(&by_builder.style), properties(&by_setter.style));
879
880 let a = by_builder
881 .color_input_state
882 .on_value_change
883 .as_ref()
884 .expect("builder lost the callback");
885 let b = by_setter
886 .color_input_state
887 .on_value_change
888 .as_ref()
889 .expect("setter lost the callback");
890 assert_eq!(
891 a.callback.cb as *const () as usize,
892 b.callback.cb as *const () as usize,
893 );
894
895 let (mut a, mut b) = (a.refany.clone(), b.refany.clone());
896 assert_eq!(
897 *a.downcast_ref::<u32>().expect("builder payload changed type"),
898 *b.downcast_ref::<u32>().expect("setter payload changed type"),
899 );
900 }
901
902 #[test]
903 fn with_on_value_change_accepts_a_generic_callback_without_mangling_the_pointer() {
904 let generic = Callback {
908 cb: generic_shaped,
909 ctx: azul_core::refany::OptionRefAny::None,
910 };
911 let expected = generic_shaped as *const () as usize;
912
913 let w = ColorInput::create(DEFAULT_COLOR).with_on_value_change(RefAny::new(0u8), generic);
914 let t = w
915 .color_input_state
916 .on_value_change
917 .as_ref()
918 .expect("the generic callback was dropped");
919 assert_eq!(
920 t.callback.cb as *const () as usize,
921 expected,
922 "the Callback -> ColorInputOnValueChangeCallback transmute mangled the pointer",
923 );
924 }
925
926 #[test]
931 fn swap_with_default_returns_the_old_widget_and_leaves_a_default_behind() {
932 for c in SAMPLE_COLORS {
933 let mut w = ColorInput::create(c);
934 let old = w.swap_with_default();
935
936 assert_eq!(old, ColorInput::create(c), "{c:?}: the old widget was not returned intact");
937 assert_eq!(w, ColorInput::default(), "{c:?}: what was left behind is not a default widget");
938 }
939 }
940
941 #[test]
942 fn swap_with_default_leaves_an_unstyled_widget_behind() {
943 assert_eq!(
948 ColorInput::default().color_input_state,
949 ColorInput::create(DEFAULT_COLOR).color_input_state,
950 "default() and create(white) no longer agree on the state",
951 );
952 assert!(
953 ColorInput::default().style.as_ref().is_empty(),
954 "ColorInput::default() gained a style",
955 );
956 assert_ne!(
957 ColorInput::default(),
958 ColorInput::create(DEFAULT_COLOR),
959 "default() and create(white) became interchangeable",
960 );
961
962 let mut w = ColorInput::create(SAMPLE_COLORS[6]);
963 let _ = w.swap_with_default();
964 assert_eq!(width_px(&w.style), None, "the swapped-in widget unexpectedly has a width");
965 assert_eq!(height_px(&w.style), None, "the swapped-in widget unexpectedly has a height");
966 }
967
968 #[test]
969 fn swap_with_default_moves_the_callback_out_rather_than_copying_or_dropping_it() {
970 let probe = log_refany();
971 let mut w = ColorInput::create(SAMPLE_COLORS[4]).with_on_value_change(
972 probe.clone(),
973 record_value as ColorInputOnValueChangeCallbackType,
974 );
975
976 let old = w.swap_with_default();
977
978 let moved = old
980 .color_input_state
981 .on_value_change
982 .as_ref()
983 .expect("the value-change callback vanished during the swap");
984 assert_eq!(
985 moved.callback.cb as *const () as usize,
986 record_value as ColorInputOnValueChangeCallbackType as *const () as usize,
987 "the fn pointer was mangled by the swap",
988 );
989
990 assert!(
993 w.color_input_state.on_value_change.as_ref().is_none(),
994 "the callback was copied instead of moved",
995 );
996
997 assert_eq!(read_log(&probe).payload, 0xDEAD_BEEF);
999 }
1000
1001 #[test]
1002 fn swapping_twice_round_trips_the_original_widget() {
1003 let mut a = ColorInput::create(SAMPLE_COLORS[6]);
1004 let mut b = a.swap_with_default(); let c = b.swap_with_default(); assert_eq!(c, ColorInput::create(SAMPLE_COLORS[6]));
1008 assert_eq!(a, ColorInput::default());
1009 assert_eq!(b, ColorInput::default());
1010 }
1011
1012 #[test]
1017 fn dom_is_a_single_childless_div_with_the_native_class() {
1018 for c in SAMPLE_COLORS {
1019 let dom = ColorInput::create(c).dom();
1020 assert!(
1021 matches!(dom.root.get_node_type(), NodeType::Div),
1022 "{c:?}: the color input is not a div",
1023 );
1024 assert_eq!(
1025 classes(&dom),
1026 vec!["__azul_native_color_input".to_string()],
1027 "{c:?}: wrong class list",
1028 );
1029 assert!(dom.children.as_ref().is_empty(), "{c:?}: the swatch grew children");
1030 }
1031 }
1032
1033 #[test]
1034 fn dom_appends_the_color_as_the_last_background_and_keeps_the_base_style() {
1035 for c in SAMPLE_COLORS {
1039 let base = properties(&ColorInput::create(c).style);
1040 let rendered = inline_properties(&ColorInput::create(c).dom());
1041
1042 assert_eq!(
1043 rendered.len(),
1044 base.len() + 1,
1045 "{c:?}: dom() added {} properties instead of exactly one",
1046 rendered.len() as i64 - base.len() as i64,
1047 );
1048 assert_eq!(&rendered[..base.len()], &base[..], "{c:?}: dom() rewrote the base style");
1049 assert_eq!(
1050 rendered[base.len()],
1051 expected_background(c),
1052 "{c:?}: the appended background is not this widget's color",
1053 );
1054 }
1055 }
1056
1057 #[test]
1058 fn dom_round_trips_every_channel_of_every_sample_color() {
1059 for c in SAMPLE_COLORS {
1060 assert_eq!(
1061 dom_background(&ColorInput::create(c).dom()),
1062 Some(c),
1063 "create({c:?}).dom() does not paint {c:?}",
1064 );
1065 }
1066 }
1067
1068 #[test]
1069 fn dom_declares_exactly_one_background_and_no_property_twice() {
1070 for c in SAMPLE_COLORS {
1071 let props = inline_properties(&ColorInput::create(c).dom());
1072 let backgrounds = props
1073 .iter()
1074 .filter(|p| matches!(p, CssProperty::BackgroundContent(_)))
1075 .count();
1076 assert_eq!(backgrounds, 1, "{c:?}: expected exactly one background declaration");
1077
1078 let mut seen = Vec::new();
1079 for p in &props {
1080 let d = discriminant(p);
1081 assert!(!seen.contains(&d), "{c:?}: the rendered node declares {p:?} twice");
1082 seen.push(d);
1083 }
1084 }
1085 }
1086
1087 #[test]
1088 fn dom_preserves_the_swatch_geometry() {
1089 for c in SAMPLE_COLORS {
1092 let rendered: CssPropertyWithConditionsVec = inline_properties(&ColorInput::create(c).dom())
1093 .into_iter()
1094 .map(CssPropertyWithConditions::simple)
1095 .collect();
1096 assert_eq!(width_px(&rendered), Some(SIDE), "{c:?}: the rendered swatch lost its width");
1097 assert_eq!(height_px(&rendered), Some(SIDE), "{c:?}: the rendered swatch lost its height");
1098 }
1099 }
1100
1101 #[test]
1102 fn dom_registers_exactly_one_mouse_up_handler_and_it_is_the_widgets_own() {
1103 for c in SAMPLE_COLORS {
1104 let dom = ColorInput::create(c).dom();
1105 let callbacks = dom.root.callbacks.as_ref();
1106
1107 assert_eq!(callbacks.len(), 1, "{c:?}: expected exactly one callback");
1108 assert_eq!(
1109 callbacks[0].event,
1110 EventFilter::Hover(HoverEventFilter::MouseUp),
1111 "{c:?}: the color input must fire on mouse-up",
1112 );
1113 assert_eq!(
1114 callbacks[0].callback.cb,
1115 on_color_input_clicked as usize,
1116 "{c:?}: the registered handler is not on_color_input_clicked",
1117 );
1118 assert_eq!(
1119 callbacks[0].callback.ctx,
1120 OptionRefAny::None,
1121 "{c:?}: a native handler must not carry an FFI context",
1122 );
1123 }
1124 }
1125
1126 #[test]
1127 fn dom_hands_the_widget_state_to_the_handler_not_the_user_payload() {
1128 for c in SAMPLE_COLORS {
1133 let dom = ColorInput::create(c)
1134 .with_on_value_change(
1135 RefAny::new(9u32),
1136 value_do_nothing as ColorInputOnValueChangeCallbackType,
1137 )
1138 .dom();
1139
1140 let mut state = dom.root.callbacks.as_ref()[0].refany.clone();
1141 let wrapper = state
1142 .downcast_ref::<ColorInputStateWrapper>()
1143 .expect("the handler's RefAny is not a ColorInputStateWrapper");
1144
1145 assert_eq!(wrapper.inner.color, c, "the color was lost on the way into the DOM");
1146 assert_eq!(wrapper.title.as_str(), DEFAULT_TITLE, "the title was lost");
1147 assert!(
1148 wrapper.on_value_change.as_ref().is_some(),
1149 "the user's value-change callback was lost on the way into the DOM",
1150 );
1151 }
1152 }
1153
1154 #[test]
1155 fn dom_of_a_callback_less_color_input_still_registers_the_click_handler() {
1156 let dom = ColorInput::create(DEFAULT_COLOR).dom();
1159 assert_eq!(dom.root.callbacks.as_ref().len(), 1);
1160
1161 let mut state = dom.root.callbacks.as_ref()[0].refany.clone();
1162 let wrapper = state
1163 .downcast_ref::<ColorInputStateWrapper>()
1164 .expect("wrong RefAny type");
1165 assert!(wrapper.on_value_change.as_ref().is_none());
1166 }
1167
1168 #[test]
1169 fn dom_of_an_unstyled_default_widget_still_carries_its_background() {
1170 let dom = ColorInput::default().dom();
1173 assert_eq!(
1174 inline_properties(&dom),
1175 vec![expected_background(DEFAULT_COLOR)],
1176 "a default color input did not render its background alone",
1177 );
1178 }
1179
1180 #[test]
1181 fn the_rendered_dom_flattens_to_exactly_one_node() {
1182 let styled = StyledDom::create_from_dom(ColorInput::create(SAMPLE_COLORS[6]).dom());
1185 assert_eq!(
1186 styled.node_data.as_ref().len(),
1187 1,
1188 "the color input no longer flattens to a single node",
1189 );
1190 }
1191
1192 #[test]
1197 fn clicking_without_a_callback_is_a_no_op() {
1198 for c in SAMPLE_COLORS {
1199 let (styled, state) = laid_out(ColorInput::create(c));
1200 let (update, changes) = click(styled, &state, node(0));
1201
1202 assert_eq!(update, Update::DoNothing, "{c:?}: a callback-less click asked for a redraw");
1203 assert!(changes.is_empty(), "{c:?}: a callback-less click wrote to the DOM");
1204 assert_eq!(state_color(&state), c, "{c:?}: the click changed the stored color");
1205 }
1206 }
1207
1208 #[test]
1209 fn clicking_with_a_refany_of_the_wrong_type_is_a_silent_no_op() {
1210 let (styled, _) = laid_out(ColorInput::create(DEFAULT_COLOR));
1213 let foreign = RefAny::new(0xDEAD_BEEF_u32);
1214
1215 let (update, changes) = click(styled, &foreign, node(0));
1216
1217 assert_eq!(update, Update::DoNothing);
1218 assert!(changes.is_empty(), "the handler wrote to the DOM through a foreign RefAny");
1219
1220 let mut foreign = foreign;
1221 assert_eq!(
1222 *foreign
1223 .downcast_ref::<u32>()
1224 .expect("the foreign payload was reinterpreted"),
1225 0xDEAD_BEEF,
1226 "the handler corrupted a RefAny it did not understand",
1227 );
1228 }
1229
1230 #[test]
1231 fn clicking_forwards_the_user_callbacks_verdict_verbatim() {
1232 let cases: [(ColorInputOnValueChangeCallbackType, Update); 3] = [
1235 (value_do_nothing, Update::DoNothing),
1236 (record_value, Update::RefreshDom),
1237 (value_refresh_all, Update::RefreshDomAllWindows),
1238 ];
1239 for (cb, expected) in cases {
1240 let (styled, state) = laid_out(
1241 ColorInput::create(SAMPLE_COLORS[6]).with_on_value_change(log_refany(), cb),
1242 );
1243 let (update, _) = click(styled, &state, node(0));
1244 assert_eq!(update, expected, "the handler did not forward {expected:?}");
1245 }
1246 }
1247
1248 #[test]
1249 fn the_callback_sees_this_widgets_color_not_the_default() {
1250 for c in SAMPLE_COLORS {
1254 let probe = log_refany();
1255 let (styled, state) = laid_out(
1256 ColorInput::create(c).with_on_value_change(
1257 probe.clone(),
1258 record_value as ColorInputOnValueChangeCallbackType,
1259 ),
1260 );
1261
1262 let (update, _) = click(styled, &state, node(0));
1263 assert_eq!(update, Update::RefreshDom);
1264 assert_eq!(
1265 read_log(&probe).seen,
1266 vec![c],
1267 "the callback was told the wrong color for {c:?}",
1268 );
1269 }
1270 }
1271
1272 #[test]
1273 fn the_callback_receives_the_user_payload_not_the_widget_state() {
1274 let probe = log_refany();
1275 let (styled, state) = laid_out(
1276 ColorInput::create(SAMPLE_COLORS[6]).with_on_value_change(
1277 probe.clone(),
1278 record_value as ColorInputOnValueChangeCallbackType,
1279 ),
1280 );
1281 click(styled, &state, node(0));
1282
1283 assert_eq!(read_log(&probe).seen.len(), 1);
1285 assert_eq!(read_log(&probe).payload, 0xDEAD_BEEF);
1286
1287 let mut probe = probe;
1289 assert!(
1290 probe.downcast_ref::<ColorInputStateWrapper>().is_none(),
1291 "the user payload and the widget state got confused",
1292 );
1293 }
1294
1295 #[test]
1296 fn clicking_never_mutates_the_stored_color() {
1297 let probe = log_refany();
1300 let c = SAMPLE_COLORS[6];
1301 let (_, state) = laid_out(
1302 ColorInput::create(c).with_on_value_change(
1303 probe.clone(),
1304 record_value as ColorInputOnValueChangeCallbackType,
1305 ),
1306 );
1307
1308 for i in 0..8 {
1309 let (styled, _) = laid_out(ColorInput::create(c));
1310 let (_, changes) = click(styled, &state, node(0));
1311 assert!(changes.is_empty(), "click {i} pushed a DOM change");
1312 assert_eq!(state_color(&state), c, "click {i} altered the stored color");
1313 }
1314 assert_eq!(
1315 read_log(&probe).seen,
1316 vec![c; 8],
1317 "the callback did not see the same color on every click",
1318 );
1319 }
1320
1321 #[test]
1322 fn clicking_a_stale_or_missing_hit_node_does_not_panic() {
1323 let c = SAMPLE_COLORS[4];
1329 for hit in [node(0), node(99), node(usize::MAX - 1), node_none()] {
1330 let probe = log_refany();
1331 let (styled, state) = laid_out(
1332 ColorInput::create(c).with_on_value_change(
1333 probe.clone(),
1334 record_value as ColorInputOnValueChangeCallbackType,
1335 ),
1336 );
1337 let (update, changes) = click(styled, &state, hit);
1338
1339 assert_eq!(update, Update::RefreshDom, "{hit:?}: wrong verdict");
1340 assert!(changes.is_empty(), "{hit:?}: a DOM change was pushed");
1341 assert_eq!(read_log(&probe).seen, vec![c], "{hit:?}: wrong color reported");
1342 }
1343 }
1344
1345 #[test]
1346 fn two_widgets_built_from_the_same_color_do_not_share_state() {
1347 let a_probe = log_refany();
1350 let b_probe = log_refany();
1351 let (a_styled, a_state) = laid_out(ColorInput::create(SAMPLE_COLORS[1]).with_on_value_change(
1352 a_probe.clone(),
1353 record_value as ColorInputOnValueChangeCallbackType,
1354 ));
1355 let (_b_styled, _b_state) = laid_out(ColorInput::create(SAMPLE_COLORS[1]).with_on_value_change(
1356 b_probe.clone(),
1357 record_value as ColorInputOnValueChangeCallbackType,
1358 ));
1359
1360 click(a_styled, &a_state, node(0));
1361
1362 assert_eq!(read_log(&a_probe).seen.len(), 1, "the clicked widget did not report");
1363 assert!(
1364 read_log(&b_probe).seen.is_empty(),
1365 "clicking one color input fired another one's callback",
1366 );
1367 }
1368}