1use azul_core::{
20 callbacks::{CoreCallbackData, Update},
21 dom::{Dom, IdOrClass, IdOrClass::Class, IdOrClassVec, TabIndex},
22 refany::RefAny,
23};
24use azul_css::dynamic_selector::{CssPropertyWithConditions, CssPropertyWithConditionsVec};
25use azul_css::{
26 props::{
27 basic::{color::ColorU, font::{StyleFontFamily, StyleFontFamilyVec}, StyleFontSize},
28 layout::{LayoutDisplay, LayoutFlexDirection, LayoutAlignItems, LayoutAlignSelf, LayoutFlexGrow, LayoutPaddingTop, LayoutPaddingBottom, LayoutPaddingLeft, LayoutPaddingRight, LayoutMarginLeft},
29 property::{CssProperty, *},
30 style::{StyleBackgroundContentVec, StyleBackgroundContent, StyleBorderTopLeftRadius, StyleBorderTopRightRadius, StyleBorderBottomLeftRadius, StyleBorderBottomRightRadius, StyleTextColor, StyleTextAlign, StyleUserSelect, StyleCursor},
31 },
32 impl_option_inner, AzString,
33};
34
35use crate::callbacks::{Callback, CallbackInfo};
36
37static CHIP_CONTAINER_CLASS: &[IdOrClass] = &[Class(AzString::from_const_str("__azul-native-chip"))];
38static CHIP_LABEL_CLASS: &[IdOrClass] =
39 &[Class(AzString::from_const_str("__azul-native-chip-label"))];
40static CHIP_REMOVE_CLASS: &[IdOrClass] =
41 &[Class(AzString::from_const_str("__azul-native-chip-remove"))];
42
43const SYSTEM_UI_STR: AzString = AzString::from_const_str("system:ui");
44const SYSTEM_UI_FAMILIES: &[StyleFontFamily] = &[StyleFontFamily::System(SYSTEM_UI_STR)];
45const SYSTEM_UI_FAMILY: StyleFontFamilyVec =
46 StyleFontFamilyVec::from_const_slice(SYSTEM_UI_FAMILIES);
47
48pub type ChipOnRemoveCallbackType = extern "C" fn(RefAny, CallbackInfo, ChipState) -> Update;
50impl_widget_callback!(
51 ChipOnRemove,
52 OptionChipOnRemove,
53 ChipOnRemoveCallback,
54 ChipOnRemoveCallbackType
55);
56
57azul_core::impl_managed_callback! {
58 wrapper: ChipOnRemoveCallback,
59 info_ty: CallbackInfo,
60 return_ty: Update,
61 default_ret: Update::DoNothing,
62 invoker_static: CHIP_ON_REMOVE_INVOKER,
63 invoker_ty: AzChipOnRemoveCallbackInvoker,
64 thunk_fn: az_chip_on_remove_callback_thunk,
65 setter_fn: AzApp_setChipOnRemoveCallbackInvoker,
66 from_handle_fn: AzChipOnRemoveCallback_createFromHostHandle,
67 extra_args: [ state: ChipState ],
68}
69
70pub type ChipOnClickCallbackType = extern "C" fn(RefAny, CallbackInfo, ChipState) -> Update;
72impl_widget_callback!(
73 ChipOnClick,
74 OptionChipOnClick,
75 ChipOnClickCallback,
76 ChipOnClickCallbackType
77);
78
79azul_core::impl_managed_callback! {
80 wrapper: ChipOnClickCallback,
81 info_ty: CallbackInfo,
82 return_ty: Update,
83 default_ret: Update::DoNothing,
84 invoker_static: CHIP_ON_CLICK_INVOKER,
85 invoker_ty: AzChipOnClickCallbackInvoker,
86 thunk_fn: az_chip_on_click_callback_thunk,
87 setter_fn: AzApp_setChipOnClickCallbackInvoker,
88 from_handle_fn: AzChipOnClickCallback_createFromHostHandle,
89 extra_args: [ state: ChipState ],
90}
91
92#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
94#[repr(C)]
95pub enum ChipKind {
96 #[default]
98 Default,
99 Primary,
101 Success,
103 Danger,
105 Warning,
107 Info,
109}
110
111impl ChipKind {
112 #[allow(clippy::trivially_copy_pass_by_ref)] const fn colors(&self) -> (ColorU, ColorU) {
115 const WHITE: ColorU = ColorU { r: 255, g: 255, b: 255, a: 255 };
116 const DARK: ColorU = ColorU { r: 33, g: 37, b: 41, a: 255 };
117 match self {
118 Self::Default => (ColorU { r: 233, g: 236, b: 239, a: 255 }, DARK),
121 Self::Primary => (ColorU { r: 13, g: 110, b: 253, a: 255 }, WHITE),
122 Self::Success => (ColorU { r: 25, g: 135, b: 84, a: 255 }, WHITE),
123 Self::Danger => (ColorU { r: 220, g: 53, b: 69, a: 255 }, WHITE),
124 Self::Warning => (ColorU { r: 255, g: 193, b: 7, a: 255 }, DARK),
125 Self::Info => (ColorU { r: 13, g: 202, b: 240, a: 255 }, DARK),
126 }
127 }
128
129 #[must_use] pub const fn class_name(&self) -> &'static str {
131 match self {
132 Self::Default => "__azul-chip-default",
133 Self::Primary => "__azul-chip-primary",
134 Self::Success => "__azul-chip-success",
135 Self::Danger => "__azul-chip-danger",
136 Self::Warning => "__azul-chip-warning",
137 Self::Info => "__azul-chip-info",
138 }
139 }
140}
141
142#[derive(Debug, Clone, PartialEq, Eq)]
144#[repr(C)]
145pub struct Chip {
146 pub chip_state: ChipStateWrapper,
148 pub label: AzString,
150 pub kind: ChipKind,
152 pub removable: bool,
154 pub container_style: CssPropertyWithConditionsVec,
156}
157
158#[derive(Debug, Default, Clone, PartialEq, Eq)]
159#[repr(C)]
160pub struct ChipStateWrapper {
161 pub inner: ChipState,
163 pub on_remove: OptionChipOnRemove,
165 pub on_click: OptionChipOnClick,
167}
168
169#[derive(Debug, Copy, Clone, PartialEq, Eq)]
171#[repr(C)]
172pub struct ChipState {
173 pub visible: bool,
175}
176
177impl Default for ChipState {
178 fn default() -> Self {
179 Self { visible: true }
180 }
181}
182
183fn build_chip_style(kind: ChipKind) -> CssPropertyWithConditionsVec {
187 let (bg, text) = kind.colors();
188 let bg_vec =
189 StyleBackgroundContentVec::from_vec(alloc::vec![StyleBackgroundContent::Color(bg)]);
190 CssPropertyWithConditionsVec::from_vec(alloc::vec![
191 CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Flex)),
192 CssPropertyWithConditions::simple(CssProperty::const_flex_direction(
193 LayoutFlexDirection::Row,
194 )),
195 CssPropertyWithConditions::simple(CssProperty::const_align_items(LayoutAlignItems::Center)),
196 CssPropertyWithConditions::simple(CssProperty::align_self(LayoutAlignSelf::Start)),
198 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(
199 0,
200 ))),
201 CssPropertyWithConditions::simple(CssProperty::const_padding_top(LayoutPaddingTop::const_px(
203 4,
204 ))),
205 CssPropertyWithConditions::simple(CssProperty::const_padding_bottom(
206 LayoutPaddingBottom::const_px(4),
207 )),
208 CssPropertyWithConditions::simple(CssProperty::const_padding_left(
209 LayoutPaddingLeft::const_px(10),
210 )),
211 CssPropertyWithConditions::simple(CssProperty::const_padding_right(
212 LayoutPaddingRight::const_px(10),
213 )),
214 CssPropertyWithConditions::simple(CssProperty::const_border_top_left_radius(
216 StyleBorderTopLeftRadius::const_px(12),
217 )),
218 CssPropertyWithConditions::simple(CssProperty::const_border_top_right_radius(
219 StyleBorderTopRightRadius::const_px(12),
220 )),
221 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_left_radius(
222 StyleBorderBottomLeftRadius::const_px(12),
223 )),
224 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_right_radius(
225 StyleBorderBottomRightRadius::const_px(12),
226 )),
227 CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(13))),
228 CssPropertyWithConditions::simple(CssProperty::const_font_family(SYSTEM_UI_FAMILY)),
229 CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor {
231 inner: text,
232 })),
233 CssPropertyWithConditions::simple(CssProperty::const_background_content(bg_vec)),
234 ])
235}
236
237static CHIP_LABEL_STYLE: &[CssPropertyWithConditions] = &[
239 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
240 CssPropertyWithConditions::simple(CssProperty::const_text_align(StyleTextAlign::Left)),
241 CssPropertyWithConditions::simple(CssProperty::user_select(StyleUserSelect::None)),
242];
243
244static CHIP_REMOVE_STYLE: &[CssPropertyWithConditions] = &[
246 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
247 CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(14))),
248 CssPropertyWithConditions::simple(CssProperty::const_cursor(StyleCursor::Pointer)),
249 CssPropertyWithConditions::simple(CssProperty::user_select(StyleUserSelect::None)),
250 CssPropertyWithConditions::simple(CssProperty::const_margin_left(LayoutMarginLeft::const_px(
251 6,
252 ))),
253];
254
255impl Chip {
256 #[inline]
258 #[must_use] pub fn create(label: AzString) -> Self {
259 Self::with_kind(label, ChipKind::Default)
260 }
261
262 #[inline]
264 #[must_use] pub fn with_kind(label: AzString, kind: ChipKind) -> Self {
265 Self {
266 chip_state: ChipStateWrapper::default(),
267 label,
268 kind,
269 removable: false,
270 container_style: build_chip_style(kind),
271 }
272 }
273
274 #[inline]
276 pub fn set_kind(&mut self, kind: ChipKind) {
277 self.kind = kind;
278 self.container_style = build_chip_style(kind);
279 }
280
281 #[inline]
283 #[must_use] pub fn with_chip_kind(mut self, kind: ChipKind) -> Self {
284 self.set_kind(kind);
285 self
286 }
287
288 #[inline]
290 pub const fn set_removable(&mut self, removable: bool) {
291 self.removable = removable;
292 }
293
294 #[inline]
296 #[must_use] pub const fn with_removable(mut self, removable: bool) -> Self {
297 self.set_removable(removable);
298 self
299 }
300
301 #[inline]
303 pub fn set_on_remove<C: Into<ChipOnRemoveCallback>>(&mut self, data: RefAny, on_remove: C) {
304 self.removable = true;
305 self.chip_state.on_remove = Some(ChipOnRemove {
306 callback: on_remove.into(),
307 refany: data,
308 })
309 .into();
310 }
311
312 #[inline]
314 #[must_use] pub fn with_on_remove<C: Into<ChipOnRemoveCallback>>(
315 mut self,
316 data: RefAny,
317 on_remove: C,
318 ) -> Self {
319 self.set_on_remove(data, on_remove);
320 self
321 }
322
323 #[inline]
325 pub fn set_on_click<C: Into<ChipOnClickCallback>>(&mut self, data: RefAny, on_click: C) {
326 self.chip_state.on_click = Some(ChipOnClick {
327 callback: on_click.into(),
328 refany: data,
329 })
330 .into();
331 }
332
333 #[inline]
335 #[must_use] pub fn with_on_click<C: Into<ChipOnClickCallback>>(
336 mut self,
337 data: RefAny,
338 on_click: C,
339 ) -> Self {
340 self.set_on_click(data, on_click);
341 self
342 }
343
344 #[inline]
346 #[must_use] pub fn swap_with_default(&mut self) -> Self {
347 let mut s = Self::create(AzString::from_const_str(""));
348 core::mem::swap(&mut s, self);
349 s
350 }
351
352 #[inline]
354 #[must_use] pub fn dom(self) -> Dom {
355 use azul_core::{
356 callbacks::CoreCallback,
357 dom::{EventFilter, HoverEventFilter},
358 refany::OptionRefAny,
359 };
360
361 let has_on_click = matches!(self.chip_state.on_click, OptionChipOnClick::Some(_));
362
363 let state_ref = RefAny::new(self.chip_state);
366
367 let mut label = Dom::create_text(self.label)
368 .with_ids_and_classes(IdOrClassVec::from_const_slice(CHIP_LABEL_CLASS))
369 .with_css_props(CssPropertyWithConditionsVec::from_const_slice(CHIP_LABEL_STYLE));
370
371 if has_on_click {
378 label = label.with_tab_index(TabIndex::Auto).with_callbacks(
379 alloc::vec![CoreCallbackData {
380 event: EventFilter::Hover(HoverEventFilter::MouseUp),
381 callback: CoreCallback {
382 cb: default_on_chip_click as usize,
383 ctx: OptionRefAny::None,
384 },
385 refany: state_ref.clone(),
386 }]
387 .into(),
388 );
389 }
390
391 let mut children = alloc::vec![label];
392
393 if self.removable {
394 let remove = Dom::create_text(AzString::from_const_str("\u{00D7}"))
395 .with_ids_and_classes(IdOrClassVec::from_const_slice(CHIP_REMOVE_CLASS))
396 .with_css_props(CssPropertyWithConditionsVec::from_const_slice(CHIP_REMOVE_STYLE))
397 .with_tab_index(TabIndex::Auto)
398 .with_callbacks(
399 alloc::vec![CoreCallbackData {
400 event: EventFilter::Hover(HoverEventFilter::MouseUp),
401 callback: CoreCallback {
402 cb: default_on_chip_remove as usize,
403 ctx: OptionRefAny::None,
404 },
405 refany: state_ref,
406 }]
407 .into(),
408 );
409 children.push(remove);
410 }
411
412 Dom::create_div()
413 .with_ids_and_classes(IdOrClassVec::from_const_slice(CHIP_CONTAINER_CLASS))
414 .with_css_props(self.container_style)
415 .with_children(children.into())
416 }
417}
418
419impl Default for Chip {
420 fn default() -> Self {
421 Self::create(AzString::from_const_str(""))
422 }
423}
424
425extern "C" fn default_on_chip_remove(mut data: RefAny, mut info: CallbackInfo) -> Update {
430 let remove_node = info.get_hit_node();
431 let Some(container) = info.get_parent(remove_node) else {
432 return Update::DoNothing;
433 };
434
435 let result = {
436 let Some(mut chip) = data.downcast_mut::<ChipStateWrapper>() else {
437 return Update::DoNothing;
438 };
439 chip.inner.visible = false;
440 let inner = chip.inner;
441 let chip = &mut *chip;
442 match chip.on_remove.as_mut() {
443 Some(ChipOnRemove { callback, refany }) => (callback.cb)(refany.clone(), info, inner),
444 None => Update::DoNothing,
445 }
446 };
447
448 info.set_css_property(container, CssProperty::const_display(LayoutDisplay::None));
453
454 result
455}
456
457extern "C" fn default_on_chip_click(mut data: RefAny, info: CallbackInfo) -> Update {
460 let Some(mut chip) = data.downcast_mut::<ChipStateWrapper>() else {
461 return Update::DoNothing;
462 };
463 let inner = chip.inner;
464 let chip = &mut *chip;
465 match chip.on_click.as_mut() {
466 Some(ChipOnClick { callback, refany }) => (callback.cb)(refany.clone(), info, inner),
467 None => Update::DoNothing,
468 }
469}
470
471impl From<Chip> for Dom {
472 fn from(c: Chip) -> Self {
473 c.dom()
474 }
475}
476
477#[cfg(test)]
478mod autotest_generated {
479 use std::{
480 collections::{BTreeMap, HashMap, HashSet},
481 sync::{Arc, Mutex},
482 };
483
484 use azul_core::{
485 dom::{DomId, DomNodeId, EventFilter, HoverEventFilter, NodeId, NodeType},
486 geom::{LogicalRect, OptionLogicalPosition},
487 gl::OptionGlContextPtr,
488 hit_test::ScrollPosition,
489 refany::OptionRefAny,
490 resources::RendererResources,
491 styled_dom::{NodeHierarchyItemId, StyledDom},
492 window::{MonitorVec, RawWindowHandle},
493 };
494 use azul_css::{
495 props::basic::{length::SizeMetric, pixel::PixelValue},
496 system::SystemStyle,
497 };
498 use rust_fontconfig::FcFontCache;
499
500 use super::*;
501 #[cfg(feature = "icu")]
502 use crate::icu::IcuLocalizerHandle;
503 use crate::{
504 callbacks::{CallbackChange, CallbackInfoRefData, ExternalSystemCallbacks},
505 solver3::{display_list::DisplayList, layout_tree::LayoutTree},
506 window::{DomLayoutResult, LayoutWindow},
507 window_state::FullWindowState,
508 };
509
510 const ALL_KINDS: [ChipKind; 6] = [
517 ChipKind::Default,
518 ChipKind::Primary,
519 ChipKind::Success,
520 ChipKind::Danger,
521 ChipKind::Warning,
522 ChipKind::Info,
523 ];
524
525 const WHITE: ColorU = ColorU { r: 255, g: 255, b: 255, a: 255 };
526 const DARK: ColorU = ColorU { r: 33, g: 37, b: 41, a: 255 };
527
528 fn properties(v: &CssPropertyWithConditionsVec) -> Vec<CssProperty> {
530 v.as_ref().iter().map(|p| p.property.clone()).collect()
531 }
532
533 fn property_types(
535 v: &CssPropertyWithConditionsVec,
536 ) -> Vec<core::mem::Discriminant<CssProperty>> {
537 v.as_ref().iter().map(|p| core::mem::discriminant(&p.property)).collect()
538 }
539
540 fn px(pv: &PixelValue) -> f32 {
544 assert_eq!(pv.metric, SizeMetric::Px, "chip geometry must be absolute px, got {:?}", pv.metric);
545 pv.number.get()
546 }
547
548 fn padding_px(
550 v: &CssPropertyWithConditionsVec,
551 ) -> (Option<f32>, Option<f32>, Option<f32>, Option<f32>) {
552 let find = |f: &dyn Fn(&CssProperty) -> Option<f32>| v.as_ref().iter().find_map(|p| f(&p.property));
553 (
554 find(&|p| match p {
555 CssProperty::PaddingTop(x) => x.get_property().map(|x| px(&x.inner)),
556 _ => None,
557 }),
558 find(&|p| match p {
559 CssProperty::PaddingBottom(x) => x.get_property().map(|x| px(&x.inner)),
560 _ => None,
561 }),
562 find(&|p| match p {
563 CssProperty::PaddingLeft(x) => x.get_property().map(|x| px(&x.inner)),
564 _ => None,
565 }),
566 find(&|p| match p {
567 CssProperty::PaddingRight(x) => x.get_property().map(|x| px(&x.inner)),
568 _ => None,
569 }),
570 )
571 }
572
573 fn radii_px(v: &CssPropertyWithConditionsVec) -> Vec<f32> {
575 v.as_ref()
576 .iter()
577 .filter_map(|p| match &p.property {
578 CssProperty::BorderTopLeftRadius(r) => r.get_property().map(|r| px(&r.inner)),
579 CssProperty::BorderTopRightRadius(r) => r.get_property().map(|r| px(&r.inner)),
580 CssProperty::BorderBottomLeftRadius(r) => r.get_property().map(|r| px(&r.inner)),
581 CssProperty::BorderBottomRightRadius(r) => r.get_property().map(|r| px(&r.inner)),
582 _ => None,
583 })
584 .collect()
585 }
586
587 fn font_size_px(v: &CssPropertyWithConditionsVec) -> Option<f32> {
588 v.as_ref().iter().find_map(|p| match &p.property {
589 CssProperty::FontSize(f) => f.get_property().map(|f| px(&f.inner)),
590 _ => None,
591 })
592 }
593
594 fn text_color(v: &CssPropertyWithConditionsVec) -> Option<ColorU> {
595 v.as_ref().iter().find_map(|p| match &p.property {
596 CssProperty::TextColor(c) => c.get_property().map(|c| c.inner),
597 _ => None,
598 })
599 }
600
601 fn background_color(v: &CssPropertyWithConditionsVec) -> Option<ColorU> {
604 let bg = v.as_ref().iter().find_map(|p| match &p.property {
605 CssProperty::BackgroundContent(b) => b.get_property(),
606 _ => None,
607 })?;
608 assert_eq!(bg.as_ref().len(), 1, "a chip must declare exactly one background layer");
609 match &bg.as_ref()[0] {
610 StyleBackgroundContent::Color(c) => Some(*c),
611 other => panic!("chip background is not a flat colour: {other:?}"),
612 }
613 }
614
615 fn all_pixel_values(v: &CssPropertyWithConditionsVec) -> Vec<PixelValue> {
617 v.as_ref()
618 .iter()
619 .filter_map(|p| match &p.property {
620 CssProperty::PaddingTop(x) => x.get_property().map(|x| x.inner),
621 CssProperty::PaddingBottom(x) => x.get_property().map(|x| x.inner),
622 CssProperty::PaddingLeft(x) => x.get_property().map(|x| x.inner),
623 CssProperty::PaddingRight(x) => x.get_property().map(|x| x.inner),
624 CssProperty::BorderTopLeftRadius(r) => r.get_property().map(|r| r.inner),
625 CssProperty::BorderTopRightRadius(r) => r.get_property().map(|r| r.inner),
626 CssProperty::BorderBottomLeftRadius(r) => r.get_property().map(|r| r.inner),
627 CssProperty::BorderBottomRightRadius(r) => r.get_property().map(|r| r.inner),
628 CssProperty::FontSize(f) => f.get_property().map(|f| f.inner),
629 _ => None,
630 })
631 .collect()
632 }
633
634 fn luma(c: ColorU) -> f32 {
638 0.2126 * f32::from(c.r) + 0.7152 * f32::from(c.g) + 0.0722 * f32::from(c.b)
639 }
640
641 fn text_of(node: &Dom) -> Option<&str> {
643 match node.root.get_node_type() {
644 NodeType::Text(s) => Some(s.as_ref().as_str()),
645 _ => None,
646 }
647 }
648
649 fn inline_properties(node: &Dom) -> Vec<CssProperty> {
651 node.root.style.iter_inline_properties().map(|(p, _)| p.clone()).collect()
652 }
653
654 fn adversarial_strings() -> Vec<String> {
659 let mut v: Vec<String> = [
660 "",
661 "tag",
662 " ",
663 "e\u{0301}", "\u{1F469}\u{200D}\u{1F469}\u{200D}\u{1F467}", "\u{5E9}\u{5DC}\u{5D5}\u{5DD}", "\0", "a\0b", "\u{FFFD}\u{202E}\u{200B}", "\u{00D7}", "line\nbreak\ttab", "-9223372036854775808", ]
673 .iter()
674 .map(|s| (*s).to_string())
675 .collect();
676 v.push("x".repeat(100_000));
677 v
678 }
679
680 fn remove_cb(f: ChipOnRemoveCallbackType) -> ChipOnRemoveCallback {
681 f.into()
682 }
683
684 fn click_cb(f: ChipOnClickCallbackType) -> ChipOnClickCallback {
685 f.into()
686 }
687
688 struct StateLog {
690 calls: Vec<bool>,
691 }
692
693 extern "C" fn record_remove(mut data: RefAny, _: CallbackInfo, state: ChipState) -> Update {
694 if let Some(mut log) = data.downcast_mut::<StateLog>() {
695 log.calls.push(state.visible);
696 }
697 Update::RefreshDom
698 }
699
700 extern "C" fn record_click(mut data: RefAny, _: CallbackInfo, state: ChipState) -> Update {
701 if let Some(mut log) = data.downcast_mut::<StateLog>() {
702 log.calls.push(state.visible);
703 log.calls.push(state.visible); }
705 Update::RefreshDom
706 }
707
708 extern "C" fn remove_do_nothing(_: RefAny, _: CallbackInfo, _: ChipState) -> Update {
709 Update::DoNothing
710 }
711
712 extern "C" fn click_do_nothing(_: RefAny, _: CallbackInfo, state: ChipState) -> Update {
713 let _ = state.visible;
717 Update::DoNothing
718 }
719
720 struct ReentrantProbe {
723 state: RefAny,
725 saw_state: Option<bool>,
728 calls: usize,
729 }
730
731 extern "C" fn probe_state_reentrantly(
732 mut data: RefAny,
733 _: CallbackInfo,
734 _: ChipState,
735 ) -> Update {
736 if let Some(mut probe) = data.downcast_mut::<ReentrantProbe>() {
737 probe.calls += 1;
738 let mut state = probe.state.clone();
739 probe.saw_state = state.downcast_ref::<ChipStateWrapper>().map(|w| w.inner.visible);
740 }
741 Update::DoNothing
742 }
743
744 fn wrapper_visible(data: &mut RefAny) -> bool {
746 data.downcast_ref::<ChipStateWrapper>()
747 .expect("payload must still be a ChipStateWrapper")
748 .inner
749 .visible
750 }
751
752 fn log_calls(data: &mut RefAny) -> Vec<bool> {
754 data.downcast_ref::<StateLog>()
755 .expect("payload must still be a StateLog")
756 .calls
757 .clone()
758 }
759
760 fn layout_result(styled_dom: StyledDom) -> DomLayoutResult {
763 DomLayoutResult {
764 styled_dom,
765 layout_tree: LayoutTree {
766 nodes: Vec::new(),
767 warm: Vec::new(),
768 cold: Vec::new(),
769 root: 0,
770 dom_to_layout: BTreeMap::new(),
771 children_arena: Vec::new(),
772 children_offsets: Vec::new(),
773 subtree_needs_intrinsic: Vec::new(),
774 },
775 calculated_positions: Vec::new(),
776 viewport: LogicalRect::zero(),
777 display_list: DisplayList::default(),
778 scroll_ids: HashMap::new(),
779 scroll_id_to_node_id: HashMap::new(),
780 }
781 }
782
783 fn removable_styled_dom() -> StyledDom {
787 let chip = Chip::create(AzString::from("tag")).with_removable(true);
788 let styled = StyledDom::create_from_dom(chip.dom());
789 assert_eq!(
790 styled.node_hierarchy.as_ref().len(),
791 3,
792 "fixture must flatten to exactly container/label/remove"
793 );
794 styled
795 }
796
797 fn with_callback_info<R>(
801 styled: Option<StyledDom>,
802 hit: usize,
803 f: impl FnOnce(CallbackInfo) -> R,
804 ) -> (R, Vec<CallbackChange>) {
805 let mut layout_window =
806 LayoutWindow::new(FcFontCache::default()).expect("LayoutWindow::new failed");
807 if let Some(sd) = styled {
808 layout_window.layout_results.insert(DomId::ROOT_ID, layout_result(sd));
809 }
810
811 let renderer_resources = RendererResources::default();
812 let previous_window_state: Option<FullWindowState> = None;
813 let current_window_state = FullWindowState::default();
814 let gl_context = OptionGlContextPtr::None;
815 let scroll_states: BTreeMap<DomId, BTreeMap<NodeHierarchyItemId, ScrollPosition>> =
816 BTreeMap::new();
817 let window_handle = RawWindowHandle::Unsupported;
818 let system_callbacks = ExternalSystemCallbacks::rust_internal();
819
820 let ref_data = CallbackInfoRefData {
821 layout_window: &layout_window,
822 renderer_resources: &renderer_resources,
823 previous_window_state: &previous_window_state,
824 current_window_state: ¤t_window_state,
825 gl_context: &gl_context,
826 current_scroll_manager: &scroll_states,
827 current_window_handle: &window_handle,
828 system_callbacks: &system_callbacks,
829 system_style: Arc::new(SystemStyle::default()),
830 monitors: Arc::new(Mutex::new(MonitorVec::from_const_slice(&[]))),
831 #[cfg(feature = "icu")]
832 icu_localizer: IcuLocalizerHandle::default(),
833 ctx: OptionRefAny::None,
834 };
835
836 let changes: Arc<Mutex<Vec<CallbackChange>>> = Arc::new(Mutex::new(Vec::new()));
837
838 let info = CallbackInfo::new(
839 &ref_data,
840 &changes,
841 DomNodeId {
842 dom: DomId::ROOT_ID,
843 node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(hit))),
844 },
845 OptionLogicalPosition::None,
846 OptionLogicalPosition::None,
847 );
848
849 let out = f(info);
850 let recorded = core::mem::take(&mut *changes.lock().expect("change log poisoned"));
851 (out, recorded)
852 }
853
854 fn run_remove(
855 styled: Option<StyledDom>,
856 hit: usize,
857 data: RefAny,
858 ) -> (Update, Vec<CallbackChange>) {
859 with_callback_info(styled, hit, move |info| default_on_chip_remove(data, info))
860 }
861
862 fn run_click(
863 styled: Option<StyledDom>,
864 hit: usize,
865 data: RefAny,
866 ) -> (Update, Vec<CallbackChange>) {
867 with_callback_info(styled, hit, move |info| default_on_chip_click(data, info))
868 }
869
870 fn display_writes(changes: &[CallbackChange]) -> Vec<(usize, LayoutDisplay)> {
872 let mut out = Vec::new();
873 for change in changes {
874 if let CallbackChange::ChangeNodeCssProperties { node_id, properties, .. } = change {
875 for p in properties.as_ref() {
876 if let CssProperty::Display(v) = p {
877 if let Some(d) = v.get_property() {
878 out.push((node_id.index(), *d));
879 }
880 }
881 }
882 }
883 }
884 out
885 }
886
887 fn state_with_remove_log() -> (RefAny, RefAny) {
889 let log = RefAny::new(StateLog { calls: Vec::new() });
890 let state = RefAny::new(ChipStateWrapper {
891 inner: ChipState { visible: true },
892 on_remove: Some(ChipOnRemove {
893 callback: remove_cb(record_remove),
894 refany: log.clone(),
895 })
896 .into(),
897 on_click: OptionChipOnClick::None,
898 });
899 (state, log)
900 }
901
902 fn state_with_click_log(visible: bool) -> (RefAny, RefAny) {
904 let log = RefAny::new(StateLog { calls: Vec::new() });
905 let state = RefAny::new(ChipStateWrapper {
906 inner: ChipState { visible },
907 on_remove: OptionChipOnRemove::None,
908 on_click: Some(ChipOnClick { callback: click_cb(record_click), refany: log.clone() })
909 .into(),
910 });
911 (state, log)
912 }
913
914 #[test]
919 fn colors_returns_the_documented_constants_for_every_kind() {
920 let expected = [
921 (ChipKind::Default, ColorU { r: 233, g: 236, b: 239, a: 255 }, DARK),
924 (ChipKind::Primary, ColorU { r: 13, g: 110, b: 253, a: 255 }, WHITE),
925 (ChipKind::Success, ColorU { r: 25, g: 135, b: 84, a: 255 }, WHITE),
926 (ChipKind::Danger, ColorU { r: 220, g: 53, b: 69, a: 255 }, WHITE),
927 (ChipKind::Warning, ColorU { r: 255, g: 193, b: 7, a: 255 }, DARK),
928 (ChipKind::Info, ColorU { r: 13, g: 202, b: 240, a: 255 }, DARK),
929 ];
930 for (kind, bg, text) in expected {
931 assert_eq!(kind.colors(), (bg, text), "{kind:?}: wrong (background, text) pair");
932 }
933 for kind in ALL_KINDS {
936 let (_, text) = kind.colors();
937 let dark_text = matches!(kind, ChipKind::Default | ChipKind::Warning | ChipKind::Info);
938 assert_eq!(
939 text == DARK,
940 dark_text,
941 "{kind:?}: text colour contradicts the documented variant"
942 );
943 }
944 }
945
946 #[test]
947 fn colors_only_ever_returns_one_of_the_two_documented_text_colours() {
948 for kind in ALL_KINDS {
949 let (_, text) = kind.colors();
950 assert!(
951 text == WHITE || text == DARK,
952 "{kind:?}: text colour {text:?} is neither the documented WHITE nor DARK"
953 );
954 }
955 }
956
957 #[test]
958 fn colors_are_fully_opaque_on_every_kind() {
959 for kind in ALL_KINDS {
962 let (bg, text) = kind.colors();
963 assert_eq!(bg.a, 255, "{kind:?}: translucent background {bg:?}");
964 assert_eq!(text.a, 255, "{kind:?}: translucent text colour {text:?}");
965 assert_ne!(bg, text, "{kind:?}: an invisible label is not a chip");
966 }
967 }
968
969 #[test]
970 fn colors_give_every_kind_a_distinguishable_background() {
971 let mut seen = HashSet::new();
973 for kind in ALL_KINDS {
974 let (bg, _) = kind.colors();
975 assert!(
976 seen.insert((bg.r, bg.g, bg.b, bg.a)),
977 "{kind:?}: duplicate background colour {bg:?}"
978 );
979 }
980 assert_eq!(seen.len(), ALL_KINDS.len());
981 }
982
983 #[test]
984 fn colors_pick_the_more_readable_of_the_two_text_colours() {
985 for kind in ALL_KINDS {
990 let (bg, text) = kind.colors();
991 let other = if text == WHITE { DARK } else { WHITE };
992
993 let chosen = (luma(bg) - luma(text)).abs();
994 let rejected = (luma(bg) - luma(other)).abs();
995 assert!(
996 chosen > rejected,
997 "{kind:?}: text {text:?} (delta luma {chosen:.1}) is less readable on {bg:?} than \
998 {other:?} (delta luma {rejected:.1})"
999 );
1000 assert!(chosen >= 60.0, "{kind:?}: text/background brightness gap {chosen:.1} is too low to read");
1001
1002 let light_bg = luma(bg) >= 128.0;
1004 assert_eq!(text == DARK, light_bg, "{kind:?}: bg luma {:.1} but text is {text:?}", luma(bg));
1005 }
1006 }
1007
1008 #[test]
1009 fn colors_is_pure_and_the_default_kind_is_the_neutral_tag() {
1010 assert_eq!(ChipKind::default(), ChipKind::Default);
1011 assert_eq!(ChipKind::default().colors(), ChipKind::Default.colors());
1012 for kind in ALL_KINDS {
1015 let copy = kind;
1016 assert_eq!(kind.colors(), kind.colors(), "{kind:?}: colors() is not pure");
1017 assert_eq!(kind.colors(), copy.colors(), "{kind:?}: a copy disagrees with the original");
1018 }
1019 }
1020
1021 #[test]
1022 fn colors_is_const_evaluable() {
1023 const DEFAULT: (ColorU, ColorU) = ChipKind::Default.colors();
1024 assert_eq!(DEFAULT.0, ColorU { r: 233, g: 236, b: 239, a: 255 });
1025 assert_eq!(DEFAULT.1, DARK);
1026 }
1027
1028 #[test]
1033 fn class_name_returns_the_documented_string_for_every_kind() {
1034 assert_eq!(ChipKind::Default.class_name(), "__azul-chip-default");
1035 assert_eq!(ChipKind::Primary.class_name(), "__azul-chip-primary");
1036 assert_eq!(ChipKind::Success.class_name(), "__azul-chip-success");
1037 assert_eq!(ChipKind::Danger.class_name(), "__azul-chip-danger");
1038 assert_eq!(ChipKind::Warning.class_name(), "__azul-chip-warning");
1039 assert_eq!(ChipKind::Info.class_name(), "__azul-chip-info");
1040 assert_eq!(ChipKind::default().class_name(), "__azul-chip-default");
1041 }
1042
1043 #[test]
1044 fn class_name_is_unique_per_kind_and_a_usable_css_identifier() {
1045 let mut seen = HashSet::new();
1046 for kind in ALL_KINDS {
1047 let name = kind.class_name();
1048 assert!(seen.insert(name), "{kind:?}: class name {name:?} collides with another kind");
1049 assert!(!name.is_empty(), "{kind:?}: empty class name");
1050 assert!(name.starts_with("__azul-chip-"), "{kind:?}: unnamespaced class {name:?}");
1051 assert!(name.is_ascii(), "{kind:?}: non-ASCII class name {name:?}");
1052 assert!(
1054 name.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_'),
1055 "{kind:?}: class name {name:?} contains a CSS-significant character"
1056 );
1057 assert_eq!(
1059 name.as_ptr(),
1060 kind.class_name().as_ptr(),
1061 "{kind:?}: class_name() is not a stable constant"
1062 );
1063 }
1064 assert_eq!(seen.len(), ALL_KINDS.len());
1065 }
1066
1067 #[test]
1068 fn class_name_does_not_collide_with_the_widget_element_classes() {
1069 let element_classes =
1073 ["__azul-native-chip", "__azul-native-chip-label", "__azul-native-chip-remove"];
1074 for kind in ALL_KINDS {
1075 let name = kind.class_name();
1076 for element in element_classes {
1077 assert_ne!(name, element, "{kind:?}: kind class shadows the element class {element:?}");
1078 }
1079 }
1080 }
1081
1082 #[test]
1083 fn class_name_is_const_evaluable() {
1084 const DANGER: &str = ChipKind::Danger.class_name();
1085 assert_eq!(DANGER, "__azul-chip-danger");
1086 }
1087
1088 #[test]
1093 fn build_chip_style_emits_the_documented_pill_geometry() {
1094 for kind in ALL_KINDS {
1095 let style = build_chip_style(kind);
1096 assert_eq!(
1097 padding_px(&style),
1098 (Some(4.0), Some(4.0), Some(10.0), Some(10.0)),
1099 "{kind:?}: padding is not 4px 10px"
1100 );
1101 assert_eq!(
1102 radii_px(&style),
1103 vec![12.0, 12.0, 12.0, 12.0],
1104 "{kind:?}: all four corners must carry a 12px radius"
1105 );
1106 assert_eq!(font_size_px(&style), Some(13.0), "{kind:?}: wrong font size");
1107 }
1108 }
1109
1110 #[test]
1111 fn build_chip_style_radius_actually_rounds_the_chip_to_a_pill() {
1112 for kind in ALL_KINDS {
1116 let style = build_chip_style(kind);
1117 let (top, bottom, ..) = padding_px(&style);
1118 let height = font_size_px(&style).expect("a font size must be declared")
1119 + top.expect("padding-top")
1120 + bottom.expect("padding-bottom");
1121 for r in radii_px(&style) {
1122 assert!(
1123 r * 2.0 >= height,
1124 "{kind:?}: radius {r} does not reach half of the {height}px pill height"
1125 );
1126 }
1127 }
1128 }
1129
1130 #[test]
1131 fn build_chip_style_is_a_row_flexbox_that_hugs_its_content() {
1132 for kind in ALL_KINDS {
1133 let props = properties(&build_chip_style(kind));
1134 let has = |p: &CssProperty| props.contains(p);
1135
1136 assert!(has(&CssProperty::const_display(LayoutDisplay::Flex)), "{kind:?}: not a flex box");
1137 assert!(
1138 has(&CssProperty::const_flex_direction(LayoutFlexDirection::Row)),
1139 "{kind:?}: the label and the x must sit side by side"
1140 );
1141 assert!(
1142 has(&CssProperty::const_align_items(LayoutAlignItems::Center)),
1143 "{kind:?}: label and x not vertically centred"
1144 );
1145 assert!(
1148 has(&CssProperty::align_self(LayoutAlignSelf::Start)),
1149 "{kind:?}: chip stretches on the cross axis"
1150 );
1151 assert!(
1152 has(&CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
1153 "{kind:?}: chip grows on the main axis"
1154 );
1155 }
1156 }
1157
1158 #[test]
1159 fn build_chip_style_leaves_text_alignment_to_the_label() {
1160 for kind in ALL_KINDS {
1164 for p in build_chip_style(kind).as_ref() {
1165 assert!(
1166 !matches!(p.property, CssProperty::TextAlign(_)),
1167 "{kind:?}: the container must not declare text-align"
1168 );
1169 assert!(
1170 !matches!(p.property, CssProperty::JustifyContent(_)),
1171 "{kind:?}: the container must not declare justify-content"
1172 );
1173 }
1174 }
1175 }
1176
1177 #[test]
1178 fn build_chip_style_declares_the_inheritable_text_style_once() {
1179 for kind in ALL_KINDS {
1182 let style = build_chip_style(kind);
1183 let families: Vec<_> = style
1184 .as_ref()
1185 .iter()
1186 .filter_map(|p| match &p.property {
1187 CssProperty::FontFamily(f) => f.get_property(),
1188 _ => None,
1189 })
1190 .collect();
1191 assert_eq!(families.len(), 1, "{kind:?}: exactly one font-family declaration");
1192 let fams = families[0].as_ref();
1193 assert_eq!(fams.len(), 1, "{kind:?}: expected a single system-ui family");
1194 match &fams[0] {
1195 StyleFontFamily::System(name) => {
1196 assert_eq!(name.as_str(), "system:ui", "{kind:?}: wrong system font family")
1197 }
1198 other => panic!("{kind:?}: chip must use the system UI font, got {other:?}"),
1199 }
1200 assert!(text_color(&style).is_some(), "{kind:?}: no inheritable text colour declared");
1201 }
1202 }
1203
1204 #[test]
1205 fn build_chip_style_colours_track_the_kind() {
1206 for kind in ALL_KINDS {
1207 let style = build_chip_style(kind);
1208 let (bg, text) = kind.colors();
1209 assert_eq!(background_color(&style), Some(bg), "{kind:?}: emitted background != colors().0");
1210 assert_eq!(text_color(&style), Some(text), "{kind:?}: emitted text colour != colors().1");
1211 }
1212 }
1213
1214 #[test]
1215 fn build_chip_style_declares_every_property_at_most_once() {
1216 for kind in ALL_KINDS {
1219 let types = property_types(&build_chip_style(kind));
1220 let mut seen = HashSet::new();
1221 for t in &types {
1222 assert!(seen.insert(*t), "{kind:?}: the container style declares the same property twice");
1223 }
1224 assert_eq!(seen.len(), types.len());
1225 }
1226 }
1227
1228 #[test]
1229 fn build_chip_style_properties_are_all_unconditional() {
1230 for kind in ALL_KINDS {
1233 for p in build_chip_style(kind).as_ref() {
1234 assert!(
1235 p.apply_if.as_ref().is_empty(),
1236 "{kind:?}: {:?} is conditional on a stateless container",
1237 p.property
1238 );
1239 }
1240 }
1241 }
1242
1243 #[test]
1244 fn build_chip_style_is_deterministic_and_kind_dependent() {
1245 let baseline = property_types(&build_chip_style(ChipKind::Default));
1246 assert!(!baseline.is_empty(), "the container style must not be empty");
1247
1248 for kind in ALL_KINDS {
1249 assert_eq!(
1250 properties(&build_chip_style(kind)),
1251 properties(&build_chip_style(kind)),
1252 "{kind:?}: two builds of the same kind disagree"
1253 );
1254 assert_eq!(
1255 property_types(&build_chip_style(kind)),
1256 baseline,
1257 "{kind:?}: declares different properties, or in a different order, than Default"
1258 );
1259 }
1260 for (i, a) in ALL_KINDS.iter().enumerate() {
1262 for b in &ALL_KINDS[i + 1..] {
1263 assert_ne!(
1264 properties(&build_chip_style(*a)),
1265 properties(&build_chip_style(*b)),
1266 "{a:?} and {b:?} produce an identical style"
1267 );
1268 }
1269 }
1270 }
1271
1272 #[test]
1273 fn build_chip_style_differs_only_in_the_colours() {
1274 for (i, a) in ALL_KINDS.iter().enumerate() {
1279 for b in &ALL_KINDS[i + 1..] {
1280 let (style_a, style_b) = (build_chip_style(*a), build_chip_style(*b));
1281 let differing: Vec<_> = style_a
1282 .as_ref()
1283 .iter()
1284 .zip(style_b.as_ref().iter())
1285 .filter(|(x, y)| x != y)
1286 .map(|(x, _)| core::mem::discriminant(&x.property))
1287 .collect();
1288
1289 let same_text = a.colors().1 == b.colors().1;
1290 let expected = if same_text { 1 } else { 2 };
1291 assert_eq!(
1292 differing.len(),
1293 expected,
1294 "{a:?} vs {b:?}: only the background{} may depend on the kind",
1295 if same_text { "" } else { " and the text colour" }
1296 );
1297 let bg_differs = style_a
1298 .as_ref()
1299 .iter()
1300 .zip(style_b.as_ref().iter())
1301 .any(|(x, y)| x != y && matches!(x.property, CssProperty::BackgroundContent(_)));
1302 assert!(
1303 bg_differs,
1304 "{a:?} vs {b:?}: the background must be one of the differing declarations"
1305 );
1306 }
1307 }
1308 }
1309
1310 #[test]
1311 fn build_chip_style_emits_only_finite_non_negative_px_lengths() {
1312 for kind in ALL_KINDS {
1315 let values = all_pixel_values(&build_chip_style(kind));
1316 assert_eq!(values.len(), 9, "{kind:?}: expected 4 paddings + 4 radii + 1 font size");
1317 for pv in values {
1318 let n = px(&pv); assert!(n.is_finite(), "{kind:?}: non-finite length {n}");
1320 assert!(n >= 0.0, "{kind:?}: negative length {n}");
1321 assert!(n <= 128.0, "{kind:?}: implausibly large length {n} for a chip");
1322 }
1323 }
1324 }
1325
1326 #[test]
1327 fn label_and_remove_static_styles_are_finite_unconditional_and_non_growing() {
1328 for (name, style) in [("label", CHIP_LABEL_STYLE), ("remove", CHIP_REMOVE_STYLE)] {
1329 let vec = CssPropertyWithConditionsVec::from_const_slice(style);
1330 for p in vec.as_ref() {
1331 assert!(p.apply_if.as_ref().is_empty(), "{name}: {:?} must be unconditional", p.property);
1332 }
1333 assert!(
1335 properties(&vec).contains(&CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
1336 "{name}: child must not grow inside the pill"
1337 );
1338 for pv in all_pixel_values(&vec) {
1339 let n = px(&pv);
1340 assert!(n.is_finite(), "{name}: non-finite length {n}");
1341 assert!((0.0..=64.0).contains(&n), "{name}: implausible length {n}");
1342 }
1343 let types = property_types(&vec);
1344 let mut seen = HashSet::new();
1345 for t in &types {
1346 assert!(seen.insert(*t), "{name}: declares the same property twice");
1347 }
1348 }
1349 }
1350
1351 #[test]
1352 fn the_remove_affordance_is_styled_as_a_clickable_target() {
1353 let remove = CssPropertyWithConditionsVec::from_const_slice(CHIP_REMOVE_STYLE);
1354 let props = properties(&remove);
1355 assert!(
1356 props.contains(&CssProperty::const_cursor(StyleCursor::Pointer)),
1357 "the x must advertise itself as clickable"
1358 );
1359 assert!(
1360 props.contains(&CssProperty::user_select(StyleUserSelect::None)),
1361 "dragging across the x must not select the glyph"
1362 );
1363 assert!(
1364 props.contains(&CssProperty::const_margin_left(LayoutMarginLeft::const_px(6))),
1365 "the x needs breathing room from the label"
1366 );
1367
1368 let label = CssPropertyWithConditionsVec::from_const_slice(CHIP_LABEL_STYLE);
1369 let label_props = properties(&label);
1370 assert!(
1371 label_props.contains(&CssProperty::const_text_align(StyleTextAlign::Left)),
1372 "the label owns text-align, not the container"
1373 );
1374 assert!(
1375 label_props.contains(&CssProperty::user_select(StyleUserSelect::None)),
1376 "the label must not be selectable (it is a click target)"
1377 );
1378 assert!(
1379 !label_props.iter().any(|p| matches!(p, CssProperty::Cursor(_))),
1380 "only the x declares a pointer cursor"
1381 );
1382 }
1383
1384 #[test]
1389 fn create_defaults_to_the_neutral_tag_and_keeps_the_label_verbatim() {
1390 for s in adversarial_strings() {
1391 let c = Chip::create(AzString::from(s.clone()));
1392 assert_eq!(c.label.as_str(), s.as_str(), "the label was not preserved verbatim");
1393 assert_eq!(c.label.len(), s.len(), "byte length changed (NUL truncation?)");
1394 assert_eq!(c.kind, ChipKind::Default, "create() must use the neutral default kind");
1395 assert!(!c.removable, "a fresh chip renders no x");
1396 assert!(c.chip_state.inner.visible, "a fresh chip is visible");
1397 assert!(c.chip_state.on_remove.is_none(), "a fresh chip carries no remove callback");
1398 assert!(c.chip_state.on_click.is_none(), "a fresh chip carries no click callback");
1399 assert_eq!(properties(&c.container_style), properties(&build_chip_style(ChipKind::Default)));
1400 }
1401 }
1402
1403 #[test]
1404 fn with_kind_stores_both_arguments_and_the_matching_style() {
1405 for kind in ALL_KINDS {
1406 for s in adversarial_strings() {
1407 let c = Chip::with_kind(AzString::from(s.clone()), kind);
1408 assert_eq!(c.label.as_str(), s.as_str(), "{kind:?}: label not preserved");
1409 assert_eq!(c.label.len(), s.len(), "{kind:?}: byte length changed");
1410 assert_eq!(c.kind, kind, "{kind:?}: kind field does not match the argument");
1411 assert!(!c.removable, "{kind:?}: with_kind must not turn on the x");
1412 assert_eq!(properties(&c.container_style), properties(&build_chip_style(kind)));
1414 assert_eq!(background_color(&c.container_style), Some(kind.colors().0));
1415 }
1416 }
1417 }
1418
1419 #[test]
1420 fn create_is_with_kind_default() {
1421 for s in ["", "tag", "\u{1F600}", "\u{00D7}"] {
1422 assert_eq!(
1423 Chip::create(AzString::from_const_str(s)),
1424 Chip::with_kind(AzString::from_const_str(s), ChipKind::Default)
1425 );
1426 }
1427 }
1428
1429 #[test]
1430 fn default_chip_is_an_empty_neutral_chip_and_equality_sees_every_field() {
1431 let d = Chip::default();
1432 assert_eq!(d, Chip::create(AzString::from_const_str("")));
1433 assert_eq!(d.label.as_str(), "");
1434 assert_eq!(d.kind, ChipKind::Default);
1435 assert!(!d.removable);
1436 assert_eq!(d.clone(), d, "Clone must preserve equality");
1437
1438 assert_ne!(d, Chip::create(AzString::from_const_str("tag")), "the label must affect equality");
1439 assert_ne!(
1440 Chip::with_kind(AzString::from_const_str("t"), ChipKind::Danger),
1441 Chip::with_kind(AzString::from_const_str("t"), ChipKind::Success),
1442 "chips of different kinds must not compare equal"
1443 );
1444 assert_ne!(
1445 Chip::default(),
1446 Chip::default().with_removable(true),
1447 "the removable flag must affect equality"
1448 );
1449 }
1450
1451 #[test]
1452 fn chips_wired_to_separate_payloads_are_not_equal() {
1453 let shared = RefAny::new(0u8);
1457 let a = Chip::create(AzString::from("t")).with_on_click(shared.clone(), click_cb(click_do_nothing));
1458 let b = Chip::create(AzString::from("t")).with_on_click(shared.clone(), click_cb(click_do_nothing));
1459 assert_eq!(a, b, "chips sharing one payload and one callback must be equal");
1460
1461 let c = Chip::create(AzString::from("t"))
1462 .with_on_click(RefAny::new(0u8), click_cb(click_do_nothing));
1463 assert_ne!(a, c, "a separately allocated payload is a different chip");
1464 }
1465
1466 #[test]
1471 fn set_kind_recomputes_the_style_without_growing_it() {
1472 let mut c = Chip::create(AzString::from_const_str("tag"));
1476 let expected_len = build_chip_style(ChipKind::Default).as_ref().len();
1477
1478 for round in 0..50 {
1479 let kind = ALL_KINDS[round % ALL_KINDS.len()];
1480 c.set_kind(kind);
1481
1482 assert_eq!(c.kind, kind, "round {round}: kind field not updated");
1483 assert_eq!(
1484 c.container_style.as_ref().len(),
1485 expected_len,
1486 "round {round}: style vec changed length — stale declarations?"
1487 );
1488 assert_eq!(
1489 properties(&c.container_style),
1490 properties(&build_chip_style(kind)),
1491 "round {round}: style does not match a freshly built one"
1492 );
1493 assert_eq!(
1494 background_color(&c.container_style),
1495 Some(kind.colors().0),
1496 "round {round}: stale background"
1497 );
1498 assert_eq!(c.label.as_str(), "tag", "round {round}: set_kind ate the label");
1499 }
1500 }
1501
1502 #[test]
1503 fn set_kind_leaves_label_removable_and_callbacks_alone() {
1504 let mut c = Chip::create(AzString::from("keep me"));
1505 c.set_on_remove(RefAny::new(StateLog { calls: Vec::new() }), remove_cb(remove_do_nothing));
1506 c.set_on_click(RefAny::new(0u8), click_cb(click_do_nothing));
1507 c.chip_state.inner.visible = false;
1508
1509 c.set_kind(ChipKind::Warning);
1510
1511 assert_eq!(c.label.as_str(), "keep me");
1512 assert!(c.removable, "set_kind must not clear the x");
1513 assert!(c.chip_state.on_remove.is_some(), "set_kind must not drop the remove callback");
1514 assert!(c.chip_state.on_click.is_some(), "set_kind must not drop the click callback");
1515 assert!(!c.chip_state.inner.visible, "set_kind must not resurrect a removed chip");
1516 }
1517
1518 #[test]
1519 fn with_chip_kind_agrees_with_set_kind_and_is_last_call_wins() {
1520 let chained = Chip::create(AzString::from_const_str("t"))
1521 .with_chip_kind(ChipKind::Danger)
1522 .with_chip_kind(ChipKind::Warning)
1523 .with_chip_kind(ChipKind::Info);
1524
1525 let mut mutated = Chip::create(AzString::from_const_str("t"));
1526 mutated.set_kind(ChipKind::Danger);
1527 mutated.set_kind(ChipKind::Warning);
1528 mutated.set_kind(ChipKind::Info);
1529
1530 assert_eq!(chained, mutated, "the builder and the mutator must agree");
1531 assert_eq!(chained.kind, ChipKind::Info);
1532 assert_eq!(chained.label.as_str(), "t");
1533 assert_eq!(properties(&chained.container_style), properties(&build_chip_style(ChipKind::Info)));
1534 assert_eq!(background_color(&chained.container_style), Some(ChipKind::Info.colors().0));
1536 }
1537
1538 #[test]
1539 fn setting_the_same_kind_twice_is_idempotent() {
1540 for kind in ALL_KINDS {
1541 let once = Chip::with_kind(AzString::from_const_str("x"), kind);
1542 let twice = once.clone().with_chip_kind(kind);
1543 assert_eq!(once, twice, "{kind:?}: re-setting the same kind changed the chip");
1544 }
1545 let original = Chip::create(AzString::from("t"));
1547 let mut cycled = original.clone();
1548 for kind in ALL_KINDS {
1549 cycled.set_kind(kind);
1550 }
1551 cycled.set_kind(ChipKind::Default);
1552 assert_eq!(cycled, original, "kind cycling must not accumulate state");
1553 }
1554
1555 #[test]
1560 fn set_removable_last_write_wins_and_touches_nothing_else() {
1561 let mut c = Chip::with_kind(AzString::from("t"), ChipKind::Warning);
1562 let style_before = c.container_style.clone();
1563
1564 for flag in [true, true, false, true, false, false] {
1565 c.set_removable(flag);
1566 assert_eq!(c.removable, flag);
1567 }
1568
1569 assert_eq!(c.kind, ChipKind::Warning);
1570 assert_eq!(c.label.as_str(), "t");
1571 assert_eq!(c.container_style, style_before, "toggling must not restyle the pill");
1572 assert!(c.chip_state.on_remove.is_none(), "toggling must not invent a callback");
1573 assert!(c.chip_state.inner.visible, "toggling must not hide the chip");
1574 }
1575
1576 #[test]
1577 fn with_removable_toggle_sequence_ends_on_the_last_value() {
1578 assert!(Chip::default().with_removable(true).removable);
1579 assert!(!Chip::default().with_removable(false).removable);
1580 assert!(!Chip::default().with_removable(true).with_removable(false).removable);
1581 assert!(Chip::default().with_removable(false).with_removable(true).removable);
1582
1583 let mut mutated = Chip::default();
1585 mutated.set_removable(true);
1586 assert_eq!(Chip::default().with_removable(true), mutated);
1587 }
1588
1589 #[test]
1590 fn removable_only_changes_the_child_count_not_the_pill() {
1591 let plain = Chip::create(AzString::from("t"));
1592 let style = plain.container_style.clone();
1593 let removable = plain.clone().with_removable(true);
1594
1595 assert_eq!(removable.container_style, style, "the x must not restyle the container");
1596 assert_eq!(plain.dom().children.as_ref().len(), 1);
1597 assert_eq!(removable.dom().children.as_ref().len(), 2);
1598 }
1599
1600 #[test]
1605 fn set_on_remove_implies_removable() {
1606 let mut c = Chip::create(AzString::from("t"));
1607 assert!(!c.removable);
1608
1609 c.set_on_remove(RefAny::new(1u8), remove_cb(remove_do_nothing));
1610
1611 assert!(c.removable, "a remove callback must render an x");
1612 assert!(c.chip_state.on_remove.is_some());
1613 assert!(c.chip_state.on_click.is_none(), "wiring on_remove must not invent an on_click");
1614 assert!(c.chip_state.inner.visible, "wiring a callback must not hide the chip");
1615 assert_eq!(c.dom().children.as_ref().len(), 2, "the x must actually be rendered");
1616 }
1617
1618 #[test]
1619 fn set_on_remove_replaces_rather_than_appends() {
1620 let mut c = Chip::create(AzString::from("t"));
1621
1622 c.set_on_remove(RefAny::new(1u8), remove_cb(remove_do_nothing));
1623 let first = c.chip_state.on_remove.as_ref().expect("first callback").refany.get_type_id();
1624 assert_eq!(first, RefAny::new(1u8).get_type_id());
1625
1626 c.set_on_remove(RefAny::new(9i64), remove_cb(record_remove));
1628 let second = c.chip_state.on_remove.as_ref().expect("second callback");
1629 assert_eq!(second.refany.get_type_id(), RefAny::new(9i64).get_type_id());
1630 assert_eq!(second.callback, remove_cb(record_remove));
1631 assert_ne!(second.callback, remove_cb(remove_do_nothing));
1632
1633 assert_eq!(c.dom().children.as_ref().len(), 2);
1635 }
1636
1637 #[test]
1638 fn with_on_remove_keeps_the_label_kind_and_style() {
1639 let c = Chip::with_kind(AzString::from("boom"), ChipKind::Danger)
1640 .with_on_remove(RefAny::new(0u8), remove_cb(remove_do_nothing));
1641
1642 assert_eq!(c.label.as_str(), "boom");
1643 assert_eq!(c.kind, ChipKind::Danger);
1644 assert_eq!(c.container_style, build_chip_style(ChipKind::Danger));
1645 assert!(c.removable);
1646 assert!(c.chip_state.on_remove.is_some());
1647 }
1648
1649 #[test]
1650 fn set_removable_false_after_set_on_remove_silently_drops_the_x() {
1651 let mut c = Chip::create(AzString::from("t"));
1655 c.set_on_remove(RefAny::new(0u8), remove_cb(record_remove));
1656 c.set_removable(false);
1657
1658 assert!(c.chip_state.on_remove.is_some(), "the callback is still stored");
1659 let dom = c.dom();
1660 assert_eq!(
1661 dom.children.as_ref().len(),
1662 1,
1663 "no x is rendered, so the remove callback can never fire"
1664 );
1665 }
1666
1667 #[test]
1672 fn set_on_click_does_not_imply_removable() {
1673 let mut c = Chip::create(AzString::from("t"));
1676 c.set_on_click(RefAny::new(1u8), click_cb(click_do_nothing));
1677
1678 assert!(!c.removable, "on_click must not turn on the x");
1679 assert!(c.chip_state.on_click.is_some());
1680 assert!(c.chip_state.on_remove.is_none());
1681 assert_eq!(c.dom().children.as_ref().len(), 1, "still just the label");
1682 }
1683
1684 #[test]
1685 fn set_on_click_replaces_rather_than_appends() {
1686 let mut c = Chip::create(AzString::from("t"));
1687
1688 c.set_on_click(RefAny::new(1u8), click_cb(click_do_nothing));
1689 c.set_on_click(RefAny::new(9i64), click_cb(record_click));
1690
1691 let second = c.chip_state.on_click.as_ref().expect("second callback");
1692 assert_eq!(second.refany.get_type_id(), RefAny::new(9i64).get_type_id());
1693 assert_eq!(second.callback, click_cb(record_click));
1694 assert_ne!(second.callback, click_cb(click_do_nothing));
1695
1696 let dom = c.dom();
1698 assert_eq!(dom.children.as_ref()[0].root.get_callbacks().as_ref().len(), 1);
1699 }
1700
1701 #[test]
1702 fn with_on_click_keeps_the_label_kind_and_style() {
1703 let c = Chip::with_kind(AzString::from("click me"), ChipKind::Primary)
1704 .with_on_click(RefAny::new(0u8), click_cb(click_do_nothing));
1705
1706 assert_eq!(c.label.as_str(), "click me");
1707 assert_eq!(c.kind, ChipKind::Primary);
1708 assert_eq!(c.container_style, build_chip_style(ChipKind::Primary));
1709 assert!(c.chip_state.on_click.is_some());
1710 }
1711
1712 #[test]
1713 fn both_callbacks_can_be_wired_at_once() {
1714 let c = Chip::create(AzString::from("t"))
1715 .with_on_click(RefAny::new(1u8), click_cb(record_click))
1716 .with_on_remove(RefAny::new(2u8), remove_cb(record_remove));
1717
1718 assert!(c.removable, "on_remove still implies removable when on_click is set");
1719 assert!(c.chip_state.on_click.is_some(), "on_remove must not clobber on_click");
1720 assert!(c.chip_state.on_remove.is_some());
1721 assert_eq!(c.chip_state.on_click.as_ref().expect("on_click").callback, click_cb(record_click));
1722 assert_eq!(c.chip_state.on_remove.as_ref().expect("on_remove").callback, remove_cb(record_remove));
1723
1724 let reversed = Chip::create(AzString::from("t"))
1726 .with_on_remove(RefAny::new(2u8), remove_cb(record_remove))
1727 .with_on_click(RefAny::new(1u8), click_cb(record_click));
1728 assert!(reversed.removable);
1729 assert!(reversed.chip_state.on_click.is_some());
1730 assert!(reversed.chip_state.on_remove.is_some());
1731 }
1732
1733 #[test]
1738 fn swap_with_default_returns_the_original_and_leaves_a_default_behind() {
1739 let mut c = Chip::with_kind(AzString::from_const_str("tag"), ChipKind::Danger)
1740 .with_removable(true);
1741 let taken = c.swap_with_default();
1742
1743 assert_eq!(taken.label.as_str(), "tag");
1745 assert_eq!(taken.kind, ChipKind::Danger);
1746 assert!(taken.removable);
1747 assert_eq!(properties(&taken.container_style), properties(&build_chip_style(ChipKind::Danger)));
1748
1749 assert_eq!(c, Chip::default());
1752 assert_eq!(c.label.as_str(), "");
1753 assert_eq!(c.kind, ChipKind::Default);
1754 assert!(!c.removable, "the x must not survive the swap");
1755 assert_eq!(
1756 background_color(&c.container_style),
1757 Some(ChipKind::Default.colors().0),
1758 "the red survived the swap"
1759 );
1760 }
1761
1762 #[test]
1763 fn swap_with_default_is_idempotent_on_an_already_default_chip() {
1764 let mut c = Chip::default();
1765 for _ in 0..3 {
1766 let taken = c.swap_with_default();
1767 assert_eq!(taken, Chip::default());
1768 assert_eq!(c, Chip::default());
1769 }
1770 }
1771
1772 #[test]
1773 fn swap_with_default_moves_the_callbacks_out_of_self() {
1774 let mut c = Chip::create(AzString::from("t"))
1775 .with_on_remove(RefAny::new(7u32), remove_cb(record_remove))
1776 .with_on_click(RefAny::new(8u32), click_cb(record_click));
1777
1778 let taken = c.swap_with_default();
1779
1780 assert!(taken.chip_state.on_remove.is_some(), "the remove callback moves out");
1781 assert!(taken.chip_state.on_click.is_some(), "the click callback moves out");
1782 assert!(
1783 c.chip_state.on_remove.is_none(),
1784 "the reset chip must not keep a reference to the old callback"
1785 );
1786 assert!(c.chip_state.on_click.is_none());
1787 assert!(!c.removable);
1788 }
1789
1790 #[test]
1791 fn swap_with_default_survives_a_huge_label_and_repeated_swaps() {
1792 let long = "x".repeat(100_000);
1793 let mut c = Chip::with_kind(AzString::from(long.clone()), ChipKind::Success);
1794 for round in 0..10 {
1795 let taken = c.swap_with_default();
1796 if round == 0 {
1797 assert_eq!(taken.label.len(), long.len(), "the long label was truncated");
1798 assert_eq!(taken.kind, ChipKind::Success);
1799 } else {
1800 assert_eq!(taken, Chip::default(), "round {round}: the emptied chip is not a default");
1801 }
1802 assert_eq!(c, Chip::default(), "round {round}: what was left behind is not a default");
1803 }
1804 }
1805
1806 #[test]
1807 fn swap_with_default_preserves_a_hidden_state_on_the_returned_chip() {
1808 let mut c = Chip::create(AzString::from("t")).with_removable(true);
1809 c.chip_state.inner.visible = false;
1810
1811 let taken = c.swap_with_default();
1812 assert!(!taken.chip_state.inner.visible, "the removed state travels with the original");
1813 assert!(c.chip_state.inner.visible, "the fresh chip left behind must be visible");
1814 }
1815
1816 #[test]
1821 fn dom_of_a_plain_chip_is_a_container_with_one_inert_label() {
1822 for kind in ALL_KINDS {
1823 let chip = Chip::with_kind(AzString::from("tag"), kind);
1824 let expected = properties(&chip.container_style);
1825 let dom = chip.dom();
1826
1827 assert!(dom.root.has_class("__azul-native-chip"), "{kind:?}: missing the widget class");
1828 assert!(
1829 dom.root.get_callbacks().as_ref().is_empty(),
1830 "{kind:?}: a stateless chip must carry no container callback"
1831 );
1832 assert_eq!(inline_properties(&dom), expected, "{kind:?}: the pill lost its computed style");
1833
1834 let children = dom.children.as_ref();
1835 assert_eq!(children.len(), 1, "{kind:?}: no x without `removable`");
1836 let label = &children[0];
1837 assert!(label.root.has_class("__azul-native-chip-label"));
1838 assert_eq!(text_of(label), Some("tag"), "{kind:?}: the label was mangled");
1839 assert!(
1840 label.root.get_callbacks().as_ref().is_empty(),
1841 "{kind:?}: no on_click means no handler on the label"
1842 );
1843 assert!(
1844 label.root.get_tab_index().is_none(),
1845 "{kind:?}: an inert label must not be keyboard-focusable"
1846 );
1847 assert!(label.children.as_ref().is_empty(), "{kind:?}: the label is a leaf text node");
1848 }
1849 }
1850
1851 #[test]
1852 fn dom_of_a_removable_chip_appends_a_focusable_x() {
1853 let dom = Chip::create(AzString::from("tag")).with_removable(true).dom();
1854
1855 let children = dom.children.as_ref();
1856 assert_eq!(children.len(), 2, "[label, remove]");
1857
1858 let remove = &children[1];
1859 assert!(remove.root.has_class("__azul-native-chip-remove"));
1860 assert_eq!(
1861 text_of(remove),
1862 Some("\u{00D7}"),
1863 "the remove glyph is U+00D7 MULTIPLICATION SIGN, not an ASCII 'x'"
1864 );
1865 assert!(
1866 matches!(remove.root.get_tab_index(), Some(TabIndex::Auto)),
1867 "the x must be keyboard-reachable"
1868 );
1869
1870 let callbacks = remove.root.get_callbacks();
1871 assert_eq!(callbacks.as_ref().len(), 1, "exactly one remove handler");
1872 let cb = &callbacks.as_ref()[0];
1873 assert!(matches!(&cb.event, EventFilter::Hover(HoverEventFilter::MouseUp)));
1874 assert_eq!(cb.callback.cb, default_on_chip_remove as usize);
1875 assert!(matches!(&cb.callback.ctx, OptionRefAny::None));
1876
1877 assert!(dom.root.get_callbacks().as_ref().is_empty());
1880 }
1881
1882 #[test]
1883 fn dom_attaches_the_click_handler_to_the_label_not_the_container() {
1884 let dom = Chip::create(AzString::from("tag"))
1887 .with_on_click(RefAny::new(0u8), click_cb(record_click))
1888 .dom();
1889
1890 assert!(
1891 dom.root.get_callbacks().as_ref().is_empty(),
1892 "the pill container must never carry the click handler"
1893 );
1894
1895 let label = &dom.children.as_ref()[0];
1896 assert!(
1897 matches!(label.root.get_tab_index(), Some(TabIndex::Auto)),
1898 "a clickable label must be keyboard-reachable"
1899 );
1900 let callbacks = label.root.get_callbacks();
1901 assert_eq!(callbacks.as_ref().len(), 1, "exactly one click handler");
1902 let cb = &callbacks.as_ref()[0];
1903 assert!(matches!(&cb.event, EventFilter::Hover(HoverEventFilter::MouseUp)));
1904 assert_eq!(cb.callback.cb, default_on_chip_click as usize);
1905 assert!(matches!(&cb.callback.ctx, OptionRefAny::None));
1906 }
1907
1908 #[test]
1909 fn dom_gives_the_label_and_the_x_one_shared_state() {
1910 let dom = Chip::create(AzString::from("tag"))
1912 .with_removable(true)
1913 .with_on_click(RefAny::new(0u8), click_cb(record_click))
1914 .dom();
1915
1916 let children = dom.children.as_ref();
1917 assert_eq!(children.len(), 2);
1918 let mut label_state = children[0].root.get_callbacks().as_ref()[0].refany.clone();
1919 let mut remove_state = children[1].root.get_callbacks().as_ref()[0].refany.clone();
1920
1921 assert_eq!(label_state, remove_state, "the two handlers must share one state RefAny");
1922
1923 {
1925 let mut w = label_state
1926 .downcast_mut::<ChipStateWrapper>()
1927 .expect("the label payload must be a ChipStateWrapper");
1928 w.inner.visible = false;
1929 }
1930 assert!(
1931 !wrapper_visible(&mut remove_state),
1932 "a write through the label's handle must be visible through the x's handle"
1933 );
1934 }
1935
1936 #[test]
1937 fn dom_of_a_removable_chip_without_on_click_leaves_the_label_inert() {
1938 let dom = Chip::create(AzString::from("tag")).with_removable(true).dom();
1939 let label = &dom.children.as_ref()[0];
1940 assert!(
1941 label.root.get_callbacks().as_ref().is_empty(),
1942 "removable alone must not make the label clickable"
1943 );
1944 assert!(label.root.get_tab_index().is_none());
1945 }
1946
1947 #[test]
1948 fn dom_preserves_adversarial_labels_verbatim() {
1949 for s in adversarial_strings() {
1950 let dom = Chip::create(AzString::from(s.clone())).with_removable(true).dom();
1951 let label = &dom.children.as_ref()[0];
1952 let t = text_of(label).expect("the label must be a text node");
1953 assert_eq!(t, s.as_str(), "the label changed on its way into the DOM");
1954 assert_eq!(t.len(), s.len(), "byte length changed (NUL truncation?)");
1955 assert!(dom.root.has_class("__azul-native-chip"));
1956
1957 let remove = &dom.children.as_ref()[1];
1960 assert!(label.root.has_class("__azul-native-chip-label"));
1961 assert!(remove.root.has_class("__azul-native-chip-remove"));
1962 assert!(!label.root.has_class("__azul-native-chip-remove"));
1963 }
1964 }
1965
1966 #[test]
1967 fn dom_does_not_emit_the_kind_class() {
1968 for kind in ALL_KINDS {
1972 let dom = Chip::with_kind(AzString::from("t"), kind).with_removable(true).dom();
1973 assert!(dom.root.has_class("__azul-native-chip"));
1974 assert!(
1975 !dom.root.has_class(kind.class_name()),
1976 "{kind:?}: the kind class is not emitted (kind is carried inline)"
1977 );
1978 }
1979 }
1980
1981 #[test]
1982 fn dom_renders_the_kind_the_chip_was_last_set_to() {
1983 for kind in ALL_KINDS {
1986 let mut chip = Chip::create(AzString::from_const_str("t"));
1987 chip.set_kind(ChipKind::Danger);
1988 chip.set_kind(kind);
1989 let expected = properties(&build_chip_style(kind));
1990 assert_eq!(
1991 inline_properties(&chip.dom()),
1992 expected,
1993 "{kind:?}: the DOM does not show the current kind"
1994 );
1995 }
1996 }
1997
1998 #[test]
1999 fn from_chip_for_dom_is_exactly_dom() {
2000 for kind in ALL_KINDS {
2001 let chip = Chip::with_kind(AzString::from_const_str("ok"), kind).with_removable(true);
2002 let via_into: Dom = chip.clone().into();
2003 let via_dom = chip.dom();
2004 assert_eq!(
2005 inline_properties(&via_into),
2006 inline_properties(&via_dom),
2007 "{kind:?}: `From` diverges from `dom()`"
2008 );
2009 assert_eq!(
2010 via_into.root.get_node_type(),
2011 via_dom.root.get_node_type(),
2012 "{kind:?}: `From` built a different node"
2013 );
2014 assert_eq!(via_into.children.as_ref().len(), via_dom.children.as_ref().len());
2015 }
2016 }
2017
2018 #[test]
2019 fn dom_flattens_to_the_hierarchy_the_remove_handler_expects() {
2020 let styled = removable_styled_dom();
2023 let hierarchy = styled.node_hierarchy.as_ref();
2024 assert_eq!(hierarchy.len(), 3, "container(0), label(1), remove(2)");
2025 assert_eq!(
2026 hierarchy[2].parent_id(),
2027 Some(NodeId::new(0)),
2028 "the x's parent must be the pill container"
2029 );
2030 assert_eq!(hierarchy[0].parent_id(), None, "the container is the root");
2031 }
2032
2033 #[test]
2038 fn remove_hides_the_container_and_flips_visible() {
2039 let mut data = RefAny::new(ChipStateWrapper::default());
2040
2041 let (update, changes) = run_remove(Some(removable_styled_dom()), 2, data.clone());
2043
2044 assert_eq!(update, Update::DoNothing, "no user callback -> DoNothing");
2045 assert_eq!(
2046 display_writes(&changes),
2047 alloc::vec![(0usize, LayoutDisplay::None)],
2048 "the *container* (not the x) must be hidden"
2049 );
2050 assert_eq!(changes.len(), 1, "exactly one restyle per click");
2051 assert!(!wrapper_visible(&mut data), "state must flip to hidden");
2052 }
2053
2054 #[test]
2055 fn remove_invokes_the_user_callback_with_the_already_flipped_state() {
2056 let (mut data, mut log) = state_with_remove_log();
2057
2058 let (update, changes) = run_remove(Some(removable_styled_dom()), 2, data.clone());
2059
2060 assert_eq!(update, Update::RefreshDom, "the user callback's Update is returned");
2061 assert_eq!(
2062 log_calls(&mut log),
2063 alloc::vec![false],
2064 "the callback must see `visible == false` (already removed)"
2065 );
2066 assert!(!wrapper_visible(&mut data));
2067 assert_eq!(
2068 display_writes(&changes),
2069 alloc::vec![(0usize, LayoutDisplay::None)],
2070 "the container is hidden even after a user callback ran"
2071 );
2072 }
2073
2074 #[test]
2075 fn remove_twice_is_idempotent() {
2076 let (mut data, mut log) = state_with_remove_log();
2077
2078 for _ in 0..2 {
2079 let (update, changes) = run_remove(Some(removable_styled_dom()), 2, data.clone());
2080 assert_eq!(update, Update::RefreshDom);
2081 assert_eq!(display_writes(&changes), alloc::vec![(0usize, LayoutDisplay::None)]);
2082 }
2083
2084 assert!(!wrapper_visible(&mut data), "a second remove must not un-hide");
2085 assert_eq!(
2086 log_calls(&mut log),
2087 alloc::vec![false, false],
2088 "each click fires the callback exactly once, always with visible == false"
2089 );
2090 }
2091
2092 #[test]
2093 fn remove_on_a_root_hit_node_is_a_noop() {
2094 let mut data = RefAny::new(ChipStateWrapper::default());
2097
2098 let (update, changes) = run_remove(Some(removable_styled_dom()), 0, data.clone());
2099
2100 assert_eq!(update, Update::DoNothing);
2101 assert!(changes.is_empty(), "nothing may be restyled without a parent");
2102 assert!(wrapper_visible(&mut data), "state must not flip");
2103 }
2104
2105 #[test]
2106 fn remove_with_a_stale_hit_node_is_a_noop() {
2107 let mut data = RefAny::new(ChipStateWrapper::default());
2109
2110 let (update, changes) = run_remove(Some(removable_styled_dom()), 999, data.clone());
2111
2112 assert_eq!(update, Update::DoNothing);
2113 assert!(changes.is_empty());
2114 assert!(wrapper_visible(&mut data));
2115 }
2116
2117 #[test]
2118 fn remove_without_any_layout_result_is_a_noop() {
2119 let mut data = RefAny::new(ChipStateWrapper::default());
2120
2121 let (update, changes) = run_remove(None, 2, data.clone());
2122
2123 assert_eq!(update, Update::DoNothing);
2124 assert!(changes.is_empty());
2125 assert!(wrapper_visible(&mut data), "state must not flip");
2126 }
2127
2128 #[test]
2129 fn remove_with_a_foreign_payload_is_a_noop() {
2130 let data = RefAny::new(0xdead_beef_u64);
2132
2133 let (update, changes) = run_remove(Some(removable_styled_dom()), 2, data.clone());
2134
2135 assert_eq!(update, Update::DoNothing);
2136 assert!(changes.is_empty(), "a foreign payload must not hide the container");
2137 }
2138
2139 #[test]
2140 fn remove_fired_from_the_label_still_hides_the_container() {
2141 let mut data = RefAny::new(ChipStateWrapper::default());
2146
2147 let (update, changes) = run_remove(Some(removable_styled_dom()), 1, data.clone());
2148
2149 assert_eq!(update, Update::DoNothing);
2150 assert_eq!(display_writes(&changes), alloc::vec![(0usize, LayoutDisplay::None)]);
2151 assert!(!wrapper_visible(&mut data));
2152 }
2153
2154 #[test]
2155 fn remove_holds_the_state_borrow_across_the_user_callback() {
2156 let mut probe = RefAny::new(ReentrantProbe {
2164 state: RefAny::new(0u8),
2165 saw_state: Some(true),
2166 calls: 0,
2167 });
2168 let state = RefAny::new(ChipStateWrapper {
2169 inner: ChipState { visible: true },
2170 on_remove: Some(ChipOnRemove {
2171 callback: remove_cb(probe_state_reentrantly),
2172 refany: probe.clone(),
2173 })
2174 .into(),
2175 on_click: OptionChipOnClick::None,
2176 });
2177 {
2178 let mut p = probe.downcast_mut::<ReentrantProbe>().expect("ReentrantProbe");
2179 p.state = state.clone();
2180 }
2181
2182 let (update, changes) = run_remove(Some(removable_styled_dom()), 2, state.clone());
2183
2184 assert_eq!(update, Update::DoNothing);
2185 assert_eq!(display_writes(&changes), alloc::vec![(0usize, LayoutDisplay::None)]);
2186
2187 let p = probe.downcast_ref::<ReentrantProbe>().expect("ReentrantProbe");
2188 assert_eq!(p.calls, 1, "the user callback must have run exactly once");
2189 assert_eq!(
2190 p.saw_state, None,
2191 "a re-entrant read of the state must be refused, not aliased"
2192 );
2193 }
2194
2195 #[test]
2196 fn remove_end_to_end_through_the_real_dom_payload() {
2197 let chip = Chip::create(AzString::from("bye")).with_removable(true);
2200 let dom = chip.dom();
2201 let remove = &dom.children.as_ref()[1];
2202 let entry = &remove.root.get_callbacks().as_ref()[0];
2203 assert_eq!(entry.callback.cb, default_on_chip_remove as usize);
2204 let mut payload = entry.refany.clone();
2205
2206 let styled = StyledDom::create_from_dom(dom);
2207 let (update, changes) = run_remove(Some(styled), 2, payload.clone());
2208
2209 assert_eq!(update, Update::DoNothing);
2210 assert_eq!(display_writes(&changes), alloc::vec![(0usize, LayoutDisplay::None)]);
2211 assert!(
2212 !wrapper_visible(&mut payload),
2213 "the state living in the DOM must be flipped to hidden"
2214 );
2215 }
2216
2217 #[test]
2222 fn click_without_a_user_callback_is_a_noop() {
2223 let mut data = RefAny::new(ChipStateWrapper::default());
2224
2225 let (update, changes) = run_click(Some(removable_styled_dom()), 1, data.clone());
2226
2227 assert_eq!(update, Update::DoNothing);
2228 assert!(changes.is_empty(), "a click must never restyle anything");
2229 assert!(wrapper_visible(&mut data), "a click must not hide the chip");
2230 }
2231
2232 #[test]
2233 fn click_invokes_the_user_callback_with_the_current_state() {
2234 let (mut data, mut log) = state_with_click_log(true);
2235
2236 let (update, changes) = run_click(Some(removable_styled_dom()), 1, data.clone());
2237
2238 assert_eq!(update, Update::RefreshDom, "the user callback's Update is returned");
2239 assert_eq!(
2240 log_calls(&mut log),
2241 alloc::vec![true, true],
2242 "the callback must see the *unmodified* state (still visible)"
2243 );
2244 assert!(wrapper_visible(&mut data), "on_click must not flip `visible`");
2245 assert!(changes.is_empty(), "on_click must not write any CSS property");
2246 }
2247
2248 #[test]
2249 fn click_reports_a_hidden_chip_as_hidden() {
2250 let (data, mut log) = state_with_click_log(false);
2253
2254 let (update, _) = run_click(Some(removable_styled_dom()), 1, data);
2255
2256 assert_eq!(update, Update::RefreshDom);
2257 assert_eq!(log_calls(&mut log), alloc::vec![false, false]);
2258 }
2259
2260 #[test]
2261 fn click_does_not_need_the_node_hierarchy() {
2262 let (data, mut log) = state_with_click_log(true);
2265
2266 let (update, changes) = run_click(None, 0, data);
2267
2268 assert_eq!(update, Update::RefreshDom, "a missing layout result must not suppress on_click");
2269 assert!(changes.is_empty());
2270 assert_eq!(log_calls(&mut log), alloc::vec![true, true]);
2271 }
2272
2273 #[test]
2274 fn click_with_a_foreign_payload_is_a_noop() {
2275 let data = RefAny::new(0xdead_beef_u64);
2276
2277 let (update, changes) = run_click(Some(removable_styled_dom()), 1, data);
2278
2279 assert_eq!(update, Update::DoNothing);
2280 assert!(changes.is_empty());
2281 }
2282
2283 #[test]
2284 fn click_twice_is_stable_and_never_mutates_the_state() {
2285 let (mut data, mut log) = state_with_click_log(true);
2286
2287 for _ in 0..3 {
2288 let (update, changes) = run_click(Some(removable_styled_dom()), 1, data.clone());
2289 assert_eq!(update, Update::RefreshDom);
2290 assert!(changes.is_empty());
2291 }
2292
2293 assert!(wrapper_visible(&mut data), "repeated clicks must not hide the chip");
2294 assert_eq!(log_calls(&mut log).len(), 6, "each click fires the callback exactly once");
2295 }
2296
2297 #[test]
2298 fn click_end_to_end_through_the_real_dom_payload() {
2299 let log = RefAny::new(StateLog { calls: Vec::new() });
2300 let mut log_handle = log.clone();
2301 let chip = Chip::create(AzString::from("tag"))
2302 .with_removable(true)
2303 .with_on_click(log, click_cb(record_click));
2304 let dom = chip.dom();
2305
2306 let label = &dom.children.as_ref()[0];
2307 let entry = &label.root.get_callbacks().as_ref()[0];
2308 assert_eq!(entry.callback.cb, default_on_chip_click as usize);
2309 let mut payload = entry.refany.clone();
2310
2311 let styled = StyledDom::create_from_dom(dom);
2312 let (update, changes) = run_click(Some(styled), 1, payload.clone());
2313
2314 assert_eq!(update, Update::RefreshDom);
2315 assert!(changes.is_empty());
2316 assert_eq!(log_calls(&mut log_handle), alloc::vec![true, true]);
2317 assert!(wrapper_visible(&mut payload), "the shared state must be untouched by a click");
2318 }
2319
2320 #[test]
2321 fn remove_then_click_through_the_shared_dom_state_sees_the_hidden_chip() {
2322 let log = RefAny::new(StateLog { calls: Vec::new() });
2325 let mut log_handle = log.clone();
2326 let chip = Chip::create(AzString::from("tag"))
2327 .with_removable(true)
2328 .with_on_click(log, click_cb(record_click));
2329 let dom = chip.dom();
2330
2331 let click_payload = dom.children.as_ref()[0].root.get_callbacks().as_ref()[0].refany.clone();
2332 let remove_payload = dom.children.as_ref()[1].root.get_callbacks().as_ref()[0].refany.clone();
2333
2334 let styled = StyledDom::create_from_dom(dom);
2335 let (_, changes) = run_remove(Some(styled), 2, remove_payload);
2336 assert_eq!(display_writes(&changes), alloc::vec![(0usize, LayoutDisplay::None)]);
2337
2338 let (update, _) = run_click(Some(removable_styled_dom()), 1, click_payload);
2339 assert_eq!(update, Update::RefreshDom);
2340 assert_eq!(
2341 log_calls(&mut log_handle),
2342 alloc::vec![false, false],
2343 "after the x was clicked, the label handler must see a hidden chip"
2344 );
2345 }
2346}