1use std::vec::Vec;
4
5use azul_core::{
6 callbacks::{CoreCallbackData, Update},
7 dom::{Dom, IdOrClass, IdOrClass::Class, IdOrClassVec, NodeType, TabIndex},
8 refany::RefAny,
9 resources::{ImageRef, OptionImageRef},
10};
11#[allow(clippy::wildcard_imports)] use azul_css::{
13 dynamic_selector::{CssPropertyWithConditions, CssPropertyWithConditionsVec},
14 props::{
15 basic::{
16 color::ColorU,
17 font::{StyleFontFamily, StyleFontFamilyVec},
18 *,
19 },
20 layout::*,
21 property::{CssProperty, *},
22 style::*,
23 },
24 system::SystemFontType,
25 *,
26};
27
28use crate::callbacks::{Callback, CallbackInfo};
29
30#[repr(C)]
35#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
36pub enum ButtonType {
37 #[default]
39 Default,
40 Primary,
42 Secondary,
44 Success,
46 Danger,
48 Warning,
50 Info,
52 Link,
54}
55
56impl ButtonType {
57 #[must_use] pub const fn class_name(&self) -> &'static str {
59 match self {
60 Self::Default => "__azul-btn-default",
61 Self::Primary => "__azul-btn-primary",
62 Self::Secondary => "__azul-btn-secondary",
63 Self::Success => "__azul-btn-success",
64 Self::Danger => "__azul-btn-danger",
65 Self::Warning => "__azul-btn-warning",
66 Self::Info => "__azul-btn-info",
67 Self::Link => "__azul-btn-link",
68 }
69 }
70}
71
72#[repr(C)]
73#[derive(Debug, Clone, PartialEq, Eq, PartialOrd)]
74pub struct Button {
75 pub label: AzString,
77 pub image: OptionImageRef,
79 pub button_type: ButtonType,
81 pub container_style: CssPropertyWithConditionsVec,
83 pub label_style: CssPropertyWithConditionsVec,
85 pub image_style: CssPropertyWithConditionsVec,
87 pub on_click: OptionButtonOnClick,
89}
90
91pub type ButtonOnClickCallbackType = extern "C" fn(RefAny, CallbackInfo) -> Update;
92impl_widget_callback!(
93 ButtonOnClick,
94 OptionButtonOnClick,
95 ButtonOnClickCallback,
96 ButtonOnClickCallbackType
97);
98
99azul_core::impl_managed_callback! {
101 wrapper: ButtonOnClickCallback,
102 info_ty: CallbackInfo,
103 return_ty: Update,
104 default_ret: Update::DoNothing,
105 invoker_static: BUTTON_ON_CLICK_INVOKER,
106 invoker_ty: AzButtonOnClickCallbackInvoker,
107 thunk_fn: az_button_on_click_callback_thunk,
108 setter_fn: AzApp_setButtonOnClickCallbackInvoker,
109 from_handle_fn: AzButtonOnClickCallback_createFromHostHandle,
110}
111
112const fn get_button_colors(button_type: ButtonType) -> (ColorU, ColorU, ColorU) {
117 match button_type {
119 ButtonType::Default => (
120 ColorU::rgb(248, 249, 250), ColorU::rgb(233, 236, 239), ColorU::rgb(218, 222, 226), ),
124 ButtonType::Primary => (
125 ColorU::bootstrap_primary(),
126 ColorU::bootstrap_primary_hover(),
127 ColorU::bootstrap_primary_active(),
128 ),
129 ButtonType::Secondary => (
130 ColorU::bootstrap_secondary(),
131 ColorU::bootstrap_secondary_hover(),
132 ColorU::bootstrap_secondary_active(),
133 ),
134 ButtonType::Success => (
135 ColorU::bootstrap_success(),
136 ColorU::bootstrap_success_hover(),
137 ColorU::bootstrap_success_active(),
138 ),
139 ButtonType::Danger => (
140 ColorU::bootstrap_danger(),
141 ColorU::bootstrap_danger_hover(),
142 ColorU::bootstrap_danger_active(),
143 ),
144 ButtonType::Warning => (
145 ColorU::bootstrap_warning(),
146 ColorU::bootstrap_warning_hover(),
147 ColorU::bootstrap_warning_active(),
148 ),
149 ButtonType::Info => (
150 ColorU::bootstrap_info(),
151 ColorU::bootstrap_info_hover(),
152 ColorU::bootstrap_info_active(),
153 ),
154 ButtonType::Link => (
155 ColorU::TRANSPARENT,
156 ColorU::TRANSPARENT,
157 ColorU::TRANSPARENT,
158 ),
159 }
160}
161
162const fn get_button_text_color(button_type: ButtonType) -> ColorU {
164 match button_type {
165 ButtonType::Default => ColorU::rgb(33, 37, 41), ButtonType::Warning => ColorU::BLACK, ButtonType::Link => ColorU::bootstrap_link(), _ => ColorU::WHITE, }
170}
171
172fn build_button_container_style(button_type: ButtonType) -> Vec<CssPropertyWithConditions> {
174 return alloc::vec![
184 CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Block)),
185 CssPropertyWithConditions::simple(CssProperty::const_padding_top(LayoutPaddingTop::const_px(6))),
186 CssPropertyWithConditions::simple(CssProperty::const_padding_bottom(LayoutPaddingBottom::const_px(6))),
187 ];
188 #[allow(unreachable_code)]
189 let (bg_normal, bg_hover, bg_active) = get_button_colors(button_type);
190 let text_color = get_button_text_color(button_type);
191
192 let focus_outline_color = ColorU::bootstrap_primary();
194
195 let mut props = Vec::with_capacity(40);
196
197 props.push(CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::InlineFlex)));
199 props.push(CssPropertyWithConditions::simple(CssProperty::const_flex_direction(LayoutFlexDirection::Row)));
200 props.push(CssPropertyWithConditions::simple(CssProperty::const_justify_content(LayoutJustifyContent::Center)));
201 props.push(CssPropertyWithConditions::simple(CssProperty::const_align_items(LayoutAlignItems::Center)));
202 props.push(CssPropertyWithConditions::simple(CssProperty::align_self(LayoutAlignSelf::Start)));
204 props.push(CssPropertyWithConditions::simple(CssProperty::const_cursor(StyleCursor::Pointer)));
205 props.push(CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))));
206
207 props.push(CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor { inner: text_color })));
209
210 props.push(CssPropertyWithConditions::simple(CssProperty::const_padding_top(LayoutPaddingTop::const_px(6))));
212 props.push(CssPropertyWithConditions::simple(CssProperty::const_padding_bottom(LayoutPaddingBottom::const_px(6))));
213 props.push(CssPropertyWithConditions::simple(CssProperty::const_padding_left(LayoutPaddingLeft::const_px(12))));
214 props.push(CssPropertyWithConditions::simple(CssProperty::const_padding_right(LayoutPaddingRight::const_px(12))));
215
216 props.push(CssPropertyWithConditions::simple(CssProperty::const_border_top_left_radius(StyleBorderTopLeftRadius::const_px(4))));
218 props.push(CssPropertyWithConditions::simple(CssProperty::const_border_top_right_radius(StyleBorderTopRightRadius::const_px(4))));
219 props.push(CssPropertyWithConditions::simple(CssProperty::const_border_bottom_left_radius(StyleBorderBottomLeftRadius::const_px(4))));
220 props.push(CssPropertyWithConditions::simple(CssProperty::const_border_bottom_right_radius(StyleBorderBottomRightRadius::const_px(4))));
221
222 if button_type == ButtonType::Link {
223 props.push(CssPropertyWithConditions::simple(CssProperty::const_background_content(
225 StyleBackgroundContentVec::from_const_slice(&[StyleBackgroundContent::Color(ColorU::TRANSPARENT)]),
226 )));
227
228 props.push(CssPropertyWithConditions::on_hover(CssProperty::TextDecoration(StyleTextDecoration::Underline.into())));
230 } else {
231 props.push(CssPropertyWithConditions::simple(CssProperty::const_background_content(
233 StyleBackgroundContentVec::from_vec(vec![StyleBackgroundContent::Color(bg_normal)]),
234 )));
235
236 let border_color = if button_type == ButtonType::Default {
238 ColorU::rgb(206, 212, 218)
239 } else {
240 bg_normal
241 };
242 props.push(CssPropertyWithConditions::simple(CssProperty::const_border_top_width(LayoutBorderTopWidth::const_px(1))));
243 props.push(CssPropertyWithConditions::simple(CssProperty::const_border_bottom_width(LayoutBorderBottomWidth::const_px(1))));
244 props.push(CssPropertyWithConditions::simple(CssProperty::const_border_left_width(LayoutBorderLeftWidth::const_px(1))));
245 props.push(CssPropertyWithConditions::simple(CssProperty::const_border_right_width(LayoutBorderRightWidth::const_px(1))));
246 props.push(CssPropertyWithConditions::simple(CssProperty::const_border_top_style(StyleBorderTopStyle { inner: BorderStyle::Solid })));
247 props.push(CssPropertyWithConditions::simple(CssProperty::const_border_bottom_style(StyleBorderBottomStyle { inner: BorderStyle::Solid })));
248 props.push(CssPropertyWithConditions::simple(CssProperty::const_border_left_style(StyleBorderLeftStyle { inner: BorderStyle::Solid })));
249 props.push(CssPropertyWithConditions::simple(CssProperty::const_border_right_style(StyleBorderRightStyle { inner: BorderStyle::Solid })));
250 props.push(CssPropertyWithConditions::simple(CssProperty::const_border_top_color(StyleBorderTopColor { inner: border_color })));
251 props.push(CssPropertyWithConditions::simple(CssProperty::const_border_bottom_color(StyleBorderBottomColor { inner: border_color })));
252 props.push(CssPropertyWithConditions::simple(CssProperty::const_border_left_color(StyleBorderLeftColor { inner: border_color })));
253 props.push(CssPropertyWithConditions::simple(CssProperty::const_border_right_color(StyleBorderRightColor { inner: border_color })));
254
255 props.push(CssPropertyWithConditions::on_hover(CssProperty::BackgroundContent(
257 StyleBackgroundContentVec::from_vec(vec![StyleBackgroundContent::Color(bg_hover)]).into(),
258 )));
259 if button_type == ButtonType::Default {
260 let hover_border = ColorU::rgb(173, 181, 189);
261 props.push(CssPropertyWithConditions::on_hover(CssProperty::BorderTopColor(StyleBorderTopColor { inner: hover_border }.into())));
262 props.push(CssPropertyWithConditions::on_hover(CssProperty::BorderBottomColor(StyleBorderBottomColor { inner: hover_border }.into())));
263 props.push(CssPropertyWithConditions::on_hover(CssProperty::BorderLeftColor(StyleBorderLeftColor { inner: hover_border }.into())));
264 props.push(CssPropertyWithConditions::on_hover(CssProperty::BorderRightColor(StyleBorderRightColor { inner: hover_border }.into())));
265 }
266
267 props.push(CssPropertyWithConditions::on_active(CssProperty::BackgroundContent(
269 StyleBackgroundContentVec::from_vec(vec![StyleBackgroundContent::Color(bg_active)]).into(),
270 )));
271
272 props.push(CssPropertyWithConditions::on_focus(CssProperty::BorderTopColor(StyleBorderTopColor { inner: focus_outline_color }.into())));
275 props.push(CssPropertyWithConditions::on_focus(CssProperty::BorderBottomColor(StyleBorderBottomColor { inner: focus_outline_color }.into())));
276 props.push(CssPropertyWithConditions::on_focus(CssProperty::BorderLeftColor(StyleBorderLeftColor { inner: focus_outline_color }.into())));
277 props.push(CssPropertyWithConditions::on_focus(CssProperty::BorderRightColor(StyleBorderRightColor { inner: focus_outline_color }.into())));
278 }
279
280 props
281}
282
283fn build_button_label_style() -> Vec<CssPropertyWithConditions> {
285 let font_family = StyleFontFamilyVec::from_vec(vec![
287 StyleFontFamily::SystemType(SystemFontType::Ui),
288 ]);
289
290 vec![
291 CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(14))),
292 CssPropertyWithConditions::simple(CssProperty::const_text_align(StyleTextAlign::Center)),
293 CssPropertyWithConditions::simple(CssProperty::const_font_family(font_family)),
294 CssPropertyWithConditions::simple(CssProperty::user_select(StyleUserSelect::None)),
295 ]
296}
297
298impl Button {
299 #[inline]
301 #[must_use] pub fn create(label: AzString) -> Self {
302 Self::with_type(label, ButtonType::Default)
303 }
304
305 #[inline]
307 #[must_use] pub fn with_type(label: AzString, button_type: ButtonType) -> Self {
308 let container_style = build_button_container_style(button_type);
309 let label_style = build_button_label_style();
310
311 Self {
312 label,
313 image: None.into(),
314 button_type,
315 on_click: None.into(),
316 container_style: CssPropertyWithConditionsVec::from_vec(container_style),
317 label_style: CssPropertyWithConditionsVec::from_vec(label_style.clone()),
318 image_style: CssPropertyWithConditionsVec::from_vec(label_style),
319 }
320 }
321
322 #[inline]
324 pub fn set_button_type(&mut self, button_type: ButtonType) {
325 self.button_type = button_type;
326 self.container_style = CssPropertyWithConditionsVec::from_vec(build_button_container_style(button_type));
327 }
328
329 #[inline]
331 #[must_use] pub fn with_button_type(mut self, button_type: ButtonType) -> Self {
332 self.set_button_type(button_type);
333 self
334 }
335
336 #[inline]
337 #[must_use]
338 pub fn swap_with_default(&mut self) -> Self {
339 let mut m = Self::create(AzString::from_const_str(""));
340 core::mem::swap(&mut m, self);
341 m
342 }
343
344 #[inline]
345 pub fn set_image(&mut self, image: ImageRef) {
346 self.image = Some(image).into();
347 }
348
349 #[inline]
350 pub fn set_on_click<C: Into<ButtonOnClickCallback>>(&mut self, data: RefAny, on_click: C) {
351 self.on_click = Some(ButtonOnClick {
352 refany: data,
353 callback: on_click.into(),
354 })
355 .into();
356 }
357
358 #[inline]
359 #[must_use]
360 pub fn with_on_click<C: Into<ButtonOnClickCallback>>(
361 mut self,
362 data: RefAny,
363 on_click: C,
364 ) -> Self {
365 self.set_on_click(data, on_click);
366 self
367 }
368
369 #[inline]
370 #[must_use] pub fn dom(self) -> Dom {
371 use azul_core::{
372 callbacks::{CoreCallback, CoreCallbackData},
373 dom::{EventFilter, HoverEventFilter},
374 };
375
376 let callbacks = match self.on_click.into_option() {
377 Some(ButtonOnClick {
378 refany: data,
379 callback,
380 }) => vec![CoreCallbackData {
381 event: EventFilter::Hover(HoverEventFilter::MouseUp),
382 callback: CoreCallback {
383 cb: callback.cb as *const () as usize,
384 ctx: callback.ctx,
385 },
386 refany: data,
387 }],
388 None => Vec::new(),
389 };
390
391 let type_class = self.button_type.class_name();
399 let classes: Vec<IdOrClass> = vec![
400 Class(AzString::from("__azul-native-button")),
401 Class(AzString::from(type_class)),
402 ];
403
404 let label_dom = Dom::create_text(self.label)
411 .with_css_props(self.label_style);
412
413 let mut button = Dom::create_node(NodeType::Button);
414
415 if let Some(image) = self.image.into_option() {
418 button = button.with_child(
419 Dom::create_image(image).with_css_props(self.image_style),
420 );
421 }
422
423 button
424 .with_child(label_dom)
425 .with_css_props(self.container_style)
426 .with_ids_and_classes(IdOrClassVec::from_vec(classes))
427 .with_callbacks(callbacks.into())
428 .with_tab_index(TabIndex::Auto)
429 }
430}
431
432#[cfg(test)]
433mod autotest_generated {
434 use std::collections::HashSet;
435
436 use azul_core::{
437 dom::{EventFilter, HoverEventFilter},
438 resources::RawImageFormat,
439 };
440 use azul_css::props::basic::{length::SizeMetric, pixel::PixelValue};
441
442 use super::*;
443
444 const ALL_TYPES: [ButtonType; 8] = [
451 ButtonType::Default,
452 ButtonType::Primary,
453 ButtonType::Secondary,
454 ButtonType::Success,
455 ButtonType::Danger,
456 ButtonType::Warning,
457 ButtonType::Info,
458 ButtonType::Link,
459 ];
460
461 const WHITE: ColorU = ColorU { r: 255, g: 255, b: 255, a: 255 };
462 const BLACK: ColorU = ColorU { r: 0, g: 0, b: 0, a: 255 };
463 const TRANSPARENT: ColorU = ColorU { r: 0, g: 0, b: 0, a: 0 };
464 const DARK: ColorU = ColorU { r: 33, g: 37, b: 41, a: 255 };
466
467 extern "C" fn test_click(_data: RefAny, _info: CallbackInfo) -> Update {
468 Update::DoNothing
469 }
470
471 extern "C" fn other_click(_data: RefAny, _info: CallbackInfo) -> Update {
472 Update::RefreshDom
473 }
474
475 fn btn(label: &str, button_type: ButtonType) -> Button {
476 Button::with_type(AzString::from(label), button_type)
477 }
478
479 fn properties(v: &CssPropertyWithConditionsVec) -> Vec<CssProperty> {
481 v.as_ref().iter().map(|p| p.property.clone()).collect()
482 }
483
484 fn px(pv: &PixelValue) -> f32 {
488 assert_eq!(pv.metric, SizeMetric::Px, "button geometry must be absolute px, got {:?}", pv.metric);
489 pv.number.get()
490 }
491
492 fn padding_top_bottom_px(v: &CssPropertyWithConditionsVec) -> (Option<f32>, Option<f32>) {
493 let find = |f: &dyn Fn(&CssProperty) -> Option<f32>| v.as_ref().iter().find_map(|p| f(&p.property));
494 (
495 find(&|p| match p {
496 CssProperty::PaddingTop(x) => x.get_property().map(|x| px(&x.inner)),
497 _ => None,
498 }),
499 find(&|p| match p {
500 CssProperty::PaddingBottom(x) => x.get_property().map(|x| px(&x.inner)),
501 _ => None,
502 }),
503 )
504 }
505
506 fn font_size_px(v: &CssPropertyWithConditionsVec) -> Option<f32> {
507 v.as_ref().iter().find_map(|p| match &p.property {
508 CssProperty::FontSize(f) => f.get_property().map(|f| px(&f.inner)),
509 _ => None,
510 })
511 }
512
513 fn classes(dom: &Dom) -> Vec<String> {
515 dom.root
516 .get_ids_and_classes()
517 .as_ref()
518 .iter()
519 .filter_map(|c| match c {
520 IdOrClass::Class(s) => Some(s.as_str().to_string()),
521 IdOrClass::Id(_) => None,
522 })
523 .collect()
524 }
525
526 fn inline_properties(dom: &Dom) -> Vec<CssProperty> {
528 dom.root.style.iter_inline_properties().map(|(p, _)| p.clone()).collect()
529 }
530
531 fn text_of(dom: &Dom) -> Option<&str> {
532 match dom.root.get_node_type() {
533 NodeType::Text(s) => Some(s.as_ref().as_str()),
534 _ => None,
535 }
536 }
537
538 fn count_descendants(dom: &Dom) -> usize {
542 dom.children.as_ref().iter().map(|c| 1 + count_descendants(c)).sum()
543 }
544
545 fn luma(c: ColorU) -> f32 {
549 0.2126 * f32::from(c.r) + 0.7152 * f32::from(c.g) + 0.0722 * f32::from(c.b)
550 }
551
552 fn adversarial_labels() -> Vec<String> {
556 let mut v: Vec<String> = [
557 "",
558 " ",
559 "OK",
560 "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}", "…\t\r\n", "__azul-btn-primary", ]
569 .iter()
570 .map(|s| (*s).to_string())
571 .collect();
572 v.push("x".repeat(100_000));
573 v
574 }
575
576 #[test]
581 fn class_name_returns_the_documented_class_for_every_type() {
582 let expected = [
583 (ButtonType::Default, "__azul-btn-default"),
584 (ButtonType::Primary, "__azul-btn-primary"),
585 (ButtonType::Secondary, "__azul-btn-secondary"),
586 (ButtonType::Success, "__azul-btn-success"),
587 (ButtonType::Danger, "__azul-btn-danger"),
588 (ButtonType::Warning, "__azul-btn-warning"),
589 (ButtonType::Info, "__azul-btn-info"),
590 (ButtonType::Link, "__azul-btn-link"),
591 ];
592 for (ty, class) in expected {
593 assert_eq!(ty.class_name(), class, "{ty:?}: wrong CSS class");
594 }
595 assert_eq!(expected.len(), ALL_TYPES.len(), "a ButtonType variant is missing from this table");
596 }
597
598 #[test]
599 fn class_name_is_unique_per_type() {
600 let mut seen = HashSet::new();
603 for ty in ALL_TYPES {
604 assert!(seen.insert(ty.class_name()), "{ty:?}: duplicate class name {}", ty.class_name());
605 }
606 assert_eq!(seen.len(), ALL_TYPES.len());
607 }
608
609 #[test]
610 fn class_name_is_a_well_formed_css_identifier() {
611 for ty in ALL_TYPES {
614 let c = ty.class_name();
615 assert!(!c.is_empty(), "{ty:?}: empty class name");
616 assert!(c.starts_with("__azul-btn-"), "{ty:?}: class {c} lost the widget prefix");
617 assert!(c.is_ascii(), "{ty:?}: non-ASCII class name {c}");
618 assert!(
619 c.chars().all(|ch| ch.is_ascii_alphanumeric() || ch == '-' || ch == '_'),
620 "{ty:?}: class {c} contains a character that needs CSS escaping",
621 );
622 }
623 }
624
625 #[test]
626 fn class_name_is_pure_and_const_evaluable() {
627 const DEFAULT_CLASS: &str = ButtonType::Default.class_name();
630 const LINK_CLASS: &str = ButtonType::Link.class_name();
631 assert_eq!(DEFAULT_CLASS, "__azul-btn-default");
632 assert_eq!(LINK_CLASS, "__azul-btn-link");
633
634 for ty in ALL_TYPES {
635 let a = ty.class_name();
636 let b = ty.class_name();
637 assert_eq!(a.as_ptr(), b.as_ptr(), "{ty:?}: class_name is not returning a stable static");
638 }
639 }
640
641 #[test]
642 fn class_name_of_the_default_type_matches_derived_default() {
643 assert_eq!(ButtonType::default(), ButtonType::Default);
646 assert_eq!(ButtonType::default().class_name(), "__azul-btn-default");
647 }
648
649 #[test]
654 fn get_button_colors_returns_the_documented_bootstrap_triples() {
655 let expected = [
656 (
657 ButtonType::Default,
658 ColorU::rgb(248, 249, 250),
659 ColorU::rgb(233, 236, 239),
660 ColorU::rgb(218, 222, 226),
661 ),
662 (
663 ButtonType::Primary,
664 ColorU::rgb(13, 110, 253),
665 ColorU::rgb(11, 94, 215),
666 ColorU::rgb(10, 88, 202),
667 ),
668 (
669 ButtonType::Secondary,
670 ColorU::rgb(108, 117, 125),
671 ColorU::rgb(92, 99, 106),
672 ColorU::rgb(86, 94, 100),
673 ),
674 (
675 ButtonType::Success,
676 ColorU::rgb(25, 135, 84),
677 ColorU::rgb(21, 115, 71),
678 ColorU::rgb(20, 108, 67),
679 ),
680 (
681 ButtonType::Danger,
682 ColorU::rgb(220, 53, 69),
683 ColorU::rgb(187, 45, 59),
684 ColorU::rgb(176, 42, 55),
685 ),
686 (
687 ButtonType::Warning,
688 ColorU::rgb(255, 193, 7),
689 ColorU::rgb(255, 202, 44),
690 ColorU::rgb(255, 205, 57),
691 ),
692 (
693 ButtonType::Info,
694 ColorU::rgb(13, 202, 240),
695 ColorU::rgb(49, 210, 242),
696 ColorU::rgb(61, 213, 243),
697 ),
698 (ButtonType::Link, TRANSPARENT, TRANSPARENT, TRANSPARENT),
699 ];
700 for (ty, normal, hover, active) in expected {
701 assert_eq!(get_button_colors(ty), (normal, hover, active), "{ty:?}: wrong colour triple");
702 }
703 }
704
705 #[test]
706 fn get_button_colors_is_pure() {
707 for ty in ALL_TYPES {
708 assert_eq!(get_button_colors(ty), get_button_colors(ty), "{ty:?}: colours are not deterministic");
709 }
710 }
711
712 #[test]
713 fn get_button_colors_keeps_link_fully_transparent_and_every_other_type_opaque() {
714 for ty in ALL_TYPES {
718 let (normal, hover, active) = get_button_colors(ty);
719 if ty == ButtonType::Link {
720 assert_eq!((normal, hover, active), (TRANSPARENT, TRANSPARENT, TRANSPARENT), "Link must paint nothing");
721 } else {
722 for (state, c) in [("normal", normal), ("hover", hover), ("active", active)] {
723 assert_eq!(c.a, 255, "{ty:?}: {state} background {c:?} is not opaque");
724 }
725 }
726 }
727 }
728
729 #[test]
730 fn get_button_colors_gives_every_state_a_visible_delta() {
731 for ty in ALL_TYPES {
734 if ty == ButtonType::Link {
735 continue; }
737 let (normal, hover, active) = get_button_colors(ty);
738 assert_ne!(normal, hover, "{ty:?}: hover state is indistinguishable from normal");
739 assert_ne!(hover, active, "{ty:?}: active state is indistinguishable from hover");
740 assert_ne!(normal, active, "{ty:?}: active state is indistinguishable from normal");
741 }
742 }
743
744 #[test]
745 fn get_button_colors_gives_every_type_a_distinguishable_background() {
746 let mut seen = HashSet::new();
748 for ty in ALL_TYPES {
749 let (normal, _, _) = get_button_colors(ty);
750 assert!(seen.insert((normal.r, normal.g, normal.b, normal.a)), "{ty:?}: duplicate background {normal:?}");
751 }
752 assert_eq!(seen.len(), ALL_TYPES.len());
753 }
754
755 #[test]
760 fn get_button_text_color_returns_the_documented_colour_for_every_type() {
761 let expected = [
762 (ButtonType::Default, DARK),
763 (ButtonType::Primary, WHITE),
764 (ButtonType::Secondary, WHITE),
765 (ButtonType::Success, WHITE),
766 (ButtonType::Danger, WHITE),
767 (ButtonType::Warning, BLACK), (ButtonType::Info, WHITE),
769 (ButtonType::Link, ColorU::rgb(13, 110, 253)),
770 ];
771 for (ty, text) in expected {
772 assert_eq!(get_button_text_color(ty), text, "{ty:?}: wrong text colour");
773 }
774 for ty in ALL_TYPES {
777 let is_special = matches!(ty, ButtonType::Default | ButtonType::Warning | ButtonType::Link);
778 assert_eq!(get_button_text_color(ty) != WHITE, is_special, "{ty:?}: text colour contradicts the documented variant");
779 }
780 }
781
782 #[test]
783 fn get_button_text_color_is_pure_and_opaque() {
784 for ty in ALL_TYPES {
785 let c = get_button_text_color(ty);
786 assert_eq!(c, get_button_text_color(ty), "{ty:?}: text colour is not deterministic");
787 assert_eq!(c.a, 255, "{ty:?}: invisible (translucent) text colour {c:?}");
788 }
789 }
790
791 #[test]
792 fn get_button_text_color_stays_readable_on_its_own_background() {
793 for ty in ALL_TYPES {
799 if ty == ButtonType::Link {
800 continue; }
802 let (bg, _, _) = get_button_colors(ty);
803 let text = get_button_text_color(ty);
804 let separation = (luma(bg) - luma(text)).abs();
805 assert!(separation >= 85.0, "{ty:?}: text {text:?} on {bg:?} is unreadable (luma separation {separation:.1})");
806
807 let alt = if text == WHITE { DARK } else { WHITE };
809 let alt_separation = (luma(bg) - luma(alt)).abs();
810 if ty != ButtonType::Info {
811 assert!(separation >= alt_separation, "{ty:?}: {alt:?} would be more readable than {text:?} on {bg:?}");
812 }
813 }
814 }
815
816 #[test]
821 fn build_button_container_style_never_panics_and_is_pure() {
822 for ty in ALL_TYPES {
823 let a = build_button_container_style(ty);
824 let b = build_button_container_style(ty);
825 assert!(!a.is_empty(), "{ty:?}: a button with no container style is invisible");
826 assert_eq!(a, b, "{ty:?}: container style is not deterministic");
827 }
828 }
829
830 #[test]
831 fn build_button_container_style_always_declares_display_and_symmetric_vertical_padding() {
832 for ty in ALL_TYPES {
835 let v = CssPropertyWithConditionsVec::from_vec(build_button_container_style(ty));
836 assert!(
837 v.as_ref().iter().any(|p| matches!(p.property, CssProperty::Display(_))),
838 "{ty:?}: container declares no `display`",
839 );
840 let (top, bottom) = padding_top_bottom_px(&v);
841 assert_eq!(top, Some(6.0), "{ty:?}: wrong padding-top");
842 assert_eq!(bottom, Some(6.0), "{ty:?}: wrong padding-bottom");
843 assert_eq!(top, bottom, "{ty:?}: vertical padding is asymmetric — the label will not be centred");
844 }
845 }
846
847 #[test]
848 fn build_button_container_style_currently_ignores_the_button_type() {
849 let baseline = properties(&CssPropertyWithConditionsVec::from_vec(build_button_container_style(ButtonType::Default)));
858 for ty in ALL_TYPES {
859 let v = CssPropertyWithConditionsVec::from_vec(build_button_container_style(ty));
860 assert_eq!(properties(&v), baseline, "{ty:?}: container style diverged — was the bisection probe reverted?");
861 assert!(
862 !v.as_ref().iter().any(|p| matches!(
863 p.property,
864 CssProperty::BackgroundContent(_) | CssProperty::TextColor(_)
865 )),
866 "{ty:?}: the probe is no longer returning early — restore the type-dependent assertions",
867 );
868 assert!(
869 v.as_ref().iter().all(|p| p.apply_if.as_ref().is_empty()),
870 "{ty:?}: the probe emits only unconditional properties",
871 );
872 }
873 }
874
875 #[test]
880 fn build_button_label_style_declares_the_four_documented_properties_unconditionally() {
881 let v = CssPropertyWithConditionsVec::from_vec(build_button_label_style());
882 assert_eq!(v.len(), 4, "label style gained/lost a property: {:?}", properties(&v));
883
884 assert_eq!(font_size_px(&v), Some(14.0), "wrong label font size");
885
886 let align = v.as_ref().iter().find_map(|p| match &p.property {
887 CssProperty::TextAlign(t) => t.get_property().copied(),
888 _ => None,
889 });
890 assert_eq!(align, Some(StyleTextAlign::Center), "a button label must be centred");
891
892 let family = v.as_ref().iter().find_map(|p| match &p.property {
893 CssProperty::FontFamily(f) => f.get_property().cloned(),
894 _ => None,
895 });
896 let family = family.expect("label style declares no font-family");
897 assert_eq!(
898 family.as_ref(),
899 [StyleFontFamily::SystemType(SystemFontType::Ui)].as_slice(),
900 "the label must use the system UI font",
901 );
902
903 let user_select = v.as_ref().iter().find_map(|p| match &p.property {
904 CssProperty::UserSelect(u) => u.get_property().copied(),
905 _ => None,
906 });
907 assert_eq!(user_select, Some(StyleUserSelect::None), "a button label must not be text-selectable");
908
909 assert!(v.as_ref().iter().all(|p| p.apply_if.as_ref().is_empty()), "label style must be unconditional");
912 }
913
914 #[test]
915 fn build_button_label_style_is_pure() {
916 assert_eq!(build_button_label_style(), build_button_label_style(), "label style is not deterministic");
917 }
918
919 #[test]
924 fn create_is_exactly_with_type_default() {
925 for label in adversarial_labels() {
926 let a = Button::create(AzString::from(label.as_str()));
927 let b = Button::with_type(AzString::from(label.as_str()), ButtonType::Default);
928 assert_eq!(a, b, "create() diverged from with_type(_, Default) for a {}-byte label", label.len());
929 assert_eq!(a.button_type, ButtonType::Default);
930 }
931 }
932
933 #[test]
934 fn with_type_holds_its_post_construction_invariants_for_every_type() {
935 for ty in ALL_TYPES {
936 for label in adversarial_labels() {
937 let b = btn(&label, ty);
938
939 assert_eq!(b.label.as_str(), label.as_str(), "{ty:?}: label was mangled");
941 assert_eq!(b.label.as_str().len(), label.len(), "{ty:?}: label length changed");
942 assert_eq!(b.button_type, ty, "{ty:?}: button_type field does not match the argument");
943
944 assert!(b.image.is_none(), "{ty:?}: a freshly built button must have no image");
946 assert!(b.on_click.is_none(), "{ty:?}: a freshly built button must have no callback");
947
948 let container = build_button_container_style(ty);
951 let label_style = build_button_label_style();
952 assert_eq!(b.container_style.len(), container.len(), "{ty:?}: container_style length is inconsistent");
953 assert_eq!(b.container_style.as_ref(), container.as_slice(), "{ty:?}: container_style does not match the builder");
954 assert_eq!(b.label_style.as_ref(), label_style.as_slice(), "{ty:?}: label_style does not match the builder");
955 assert_eq!(b.image_style.as_ref(), b.label_style.as_ref(), "{ty:?}: image_style diverged from label_style");
957 }
958 }
959 }
960
961 #[test]
962 fn with_type_survives_a_multi_megabyte_label() {
963 let huge = "\u{1F600}".repeat(1_000_000); let b = btn(&huge, ButtonType::Danger);
966 assert_eq!(b.label.as_str().len(), 4_000_000);
967 assert_eq!(b.label.as_str(), huge.as_str());
968 }
969
970 #[test]
971 fn buttons_are_cloneable_and_clones_compare_equal() {
972 for ty in ALL_TYPES {
973 let b = btn("Clone me", ty);
974 let c = b.clone();
975 assert_eq!(b, c, "{ty:?}: clone() produced a different button");
976 assert_eq!(format!("{b:?}"), format!("{c:?}"), "{ty:?}: Debug output diverged between clones");
977 }
978 }
979
980 #[test]
985 fn set_button_type_updates_both_the_field_and_the_container_style() {
986 let mut b = btn("Save", ButtonType::Default);
987 for ty in ALL_TYPES {
988 b.set_button_type(ty);
989 assert_eq!(b.button_type, ty, "{ty:?}: field not updated");
990 assert_eq!(
991 b.container_style.as_ref(),
992 build_button_container_style(ty).as_slice(),
993 "{ty:?}: container_style was not rebuilt for the new type",
994 );
995 }
996 }
997
998 #[test]
999 fn set_button_type_is_idempotent_and_never_accumulates_style() {
1000 let mut b = btn("Save", ButtonType::Primary);
1003 let len = b.container_style.len();
1004 for _ in 0..100 {
1005 b.set_button_type(ButtonType::Primary);
1006 }
1007 assert_eq!(b.container_style.len(), len, "container_style grew across repeated set_button_type calls");
1008 assert_eq!(b, btn("Save", ButtonType::Primary), "set_button_type(same) is not idempotent");
1009 }
1010
1011 #[test]
1012 fn set_button_type_leaves_the_label_and_the_other_styles_untouched() {
1013 let mut b = btn("Delete", ButtonType::Default);
1014 let label_style = b.label_style.clone();
1015 let image_style = b.image_style.clone();
1016 b.set_button_type(ButtonType::Danger);
1017 assert_eq!(b.label.as_str(), "Delete", "set_button_type clobbered the label");
1018 assert_eq!(b.label_style, label_style, "set_button_type clobbered label_style");
1019 assert_eq!(b.image_style, image_style, "set_button_type clobbered image_style");
1020 }
1021
1022 #[test]
1023 fn with_button_type_round_trips_to_with_type() {
1024 for ty in ALL_TYPES {
1026 for label in ["", "OK", "\u{1F600}\0"] {
1027 let built = Button::create(AzString::from(label)).with_button_type(ty);
1028 let direct = Button::with_type(AzString::from(label), ty);
1029 assert_eq!(built, direct, "{ty:?}: builder path diverged from with_type for {label:?}");
1030 }
1031 }
1032 }
1033
1034 #[test]
1035 fn with_button_type_chains_take_the_last_type() {
1036 let b = btn("x", ButtonType::Default)
1037 .with_button_type(ButtonType::Primary)
1038 .with_button_type(ButtonType::Link)
1039 .with_button_type(ButtonType::Warning);
1040 assert_eq!(b.button_type, ButtonType::Warning);
1041 assert_eq!(b, btn("x", ButtonType::Warning));
1042 }
1043
1044 #[test]
1049 fn swap_with_default_returns_the_original_and_leaves_a_default_button_behind() {
1050 let mut b = btn("Delete", ButtonType::Danger);
1051 b.set_image(ImageRef::null_image(1, 1, RawImageFormat::RGBA8, Vec::new()));
1052 b.set_on_click(RefAny::new(7u32), test_click as ButtonOnClickCallbackType);
1053
1054 let taken = b.swap_with_default();
1055
1056 assert_eq!(taken.label.as_str(), "Delete");
1058 assert_eq!(taken.button_type, ButtonType::Danger);
1059 assert!(taken.image.is_some(), "the image did not travel with the swapped-out button");
1060 assert!(taken.on_click.is_some(), "the callback did not travel with the swapped-out button");
1061
1062 assert_eq!(b.label.as_str(), "");
1064 assert_eq!(b.button_type, ButtonType::Default);
1065 assert!(b.image.is_none(), "the swapped-in default still carries an image");
1066 assert!(b.on_click.is_none(), "the swapped-in default still carries a callback");
1067 assert_eq!(b, Button::create(AzString::from("")), "swap_with_default left a non-default button");
1068 }
1069
1070 #[test]
1071 fn swap_with_default_is_stable_under_repetition() {
1072 let mut b = btn("x", ButtonType::Info);
1075 let first = b.swap_with_default();
1076 assert_eq!(first.label.as_str(), "x");
1077 for _ in 0..1000 {
1078 let taken = b.swap_with_default();
1079 assert_eq!(taken, Button::create(AzString::from("")));
1080 assert_eq!(b, Button::create(AzString::from("")));
1081 }
1082 }
1083
1084 #[test]
1089 fn set_image_stores_the_image_and_replaces_a_previous_one() {
1090 let mut b = btn("With icon", ButtonType::Primary);
1091 assert!(b.image.is_none());
1092
1093 b.set_image(ImageRef::null_image(16, 16, RawImageFormat::RGBA8, Vec::new()));
1094 assert!(b.image.is_some(), "set_image did not store the image");
1095 let first_id = b.image.as_ref().map(|i| i.id).expect("image must be present");
1096
1097 b.set_image(ImageRef::null_image(32, 32, RawImageFormat::RGB8, Vec::new()));
1098 let second_id = b.image.as_ref().map(|i| i.id).expect("image must be present");
1099 assert_ne!(first_id, second_id, "set_image did not replace the previous image");
1100 }
1101
1102 #[test]
1103 fn set_image_accepts_degenerate_and_extreme_dimensions() {
1104 let extremes = [
1107 (0usize, 0usize),
1108 (0, 4096),
1109 (4096, 0),
1110 (1, usize::MAX),
1111 (usize::MAX, usize::MAX),
1112 ];
1113 for (w, h) in extremes {
1114 let mut b = btn("x", ButtonType::Default);
1115 b.set_image(ImageRef::null_image(w, h, RawImageFormat::RGBA8, Vec::new()));
1116 assert!(b.image.is_some(), "{w}x{h}: image was dropped");
1117 let dom = b.dom();
1118 assert_eq!(dom.children.as_ref().len(), 2, "{w}x{h}: expected an image child and a label child");
1119 }
1120 }
1121
1122 #[test]
1127 fn set_on_click_stores_the_callback_and_replaces_a_previous_one() {
1128 let mut b = btn("Click", ButtonType::Primary);
1129 assert!(b.on_click.is_none());
1130
1131 b.set_on_click(RefAny::new(1u32), test_click as ButtonOnClickCallbackType);
1132 assert!(b.on_click.is_some(), "set_on_click did not store the callback");
1133
1134 b.set_on_click(RefAny::new(2u32), other_click as ButtonOnClickCallbackType);
1135 let stored = b.on_click.as_ref().expect("callback must be present");
1136 assert_eq!(
1137 stored.callback.cb as *const () as usize,
1138 other_click as ButtonOnClickCallbackType as *const () as usize,
1139 "the second set_on_click did not replace the first",
1140 );
1141
1142 let dom = b.dom();
1144 assert_eq!(dom.root.callbacks.as_ref().len(), 1, "a re-set callback was appended instead of replaced");
1145 }
1146
1147 #[test]
1148 fn with_on_click_round_trips_the_function_pointer_and_the_payload_into_the_dom() {
1149 let cb: ButtonOnClickCallbackType = test_click;
1150 let expected_ptr = cb as *const () as usize;
1151
1152 let dom = btn("Click", ButtonType::Success).with_on_click(RefAny::new(0xDEAD_BEEF_u32), cb).dom();
1153
1154 let callbacks = dom.root.callbacks.as_ref();
1155 assert_eq!(callbacks.len(), 1, "exactly one click callback is expected");
1156 assert_eq!(
1157 callbacks[0].event,
1158 EventFilter::Hover(HoverEventFilter::MouseUp),
1159 "the button must fire on mouse-up, not on any other filter",
1160 );
1161 assert_eq!(callbacks[0].callback.cb, expected_ptr, "the fn pointer was corrupted on the way into the DOM");
1162
1163 let mut data = callbacks[0].refany.clone();
1165 assert_eq!(*data.downcast_ref::<u32>().expect("payload changed type"), 0xDEAD_BEEF, "payload was corrupted");
1166 assert!(data.downcast_ref::<u64>().is_none(), "downcast to the wrong type must fail, not reinterpret");
1167 }
1168
1169 #[test]
1170 fn with_on_click_accepts_a_generic_callback_without_mangling_the_pointer() {
1171 let generic = Callback {
1174 cb: test_click,
1175 ctx: azul_core::refany::OptionRefAny::None,
1176 };
1177 let raw: ButtonOnClickCallbackType = test_click;
1178 let expected_ptr = raw as *const () as usize;
1179
1180 let dom = btn("Click", ButtonType::Info).with_on_click(RefAny::new(1u8), generic).dom();
1181 let callbacks = dom.root.callbacks.as_ref();
1182 assert_eq!(callbacks.len(), 1);
1183 assert_eq!(callbacks[0].callback.cb, expected_ptr, "the Callback -> ButtonOnClickCallback transmute mangled the pointer");
1184 }
1185
1186 #[test]
1187 fn a_button_without_a_callback_registers_no_callbacks() {
1188 for ty in ALL_TYPES {
1189 let dom = btn("Inert", ty).dom();
1190 assert!(dom.root.callbacks.as_ref().is_empty(), "{ty:?}: a callback appeared out of nowhere");
1191 }
1192 }
1193
1194 #[test]
1199 fn dom_builds_a_focusable_button_node_with_the_base_and_type_class() {
1200 for ty in ALL_TYPES {
1201 let dom = btn("OK", ty).dom();
1202
1203 assert!(matches!(dom.root.get_node_type(), NodeType::Button), "{ty:?}: root is not a Button node");
1204 assert_eq!(
1205 dom.root.flags.get_tab_index(),
1206 Some(TabIndex::Auto),
1207 "{ty:?}: the button is not keyboard-focusable",
1208 );
1209 assert_eq!(
1210 classes(&dom),
1211 vec!["__azul-native-button".to_string(), ty.class_name().to_string()],
1212 "{ty:?}: wrong classes (base class first, then the type class)",
1213 );
1214 assert!(!dom.root.style.is_empty(), "{ty:?}: the container style did not reach the node");
1215 }
1216 }
1217
1218 #[test]
1219 fn dom_carries_the_container_style_on_the_root_and_the_label_style_on_the_child() {
1220 for ty in ALL_TYPES {
1221 let b = btn("OK", ty);
1222 let container = properties(&b.container_style);
1223 let label_style = properties(&b.label_style);
1224 let dom = b.dom();
1225
1226 assert_eq!(inline_properties(&dom), container, "{ty:?}: the root inline style is not the container style");
1227
1228 let children = dom.children.as_ref();
1229 assert_eq!(children.len(), 1, "{ty:?}: an image-less button is a Button node with exactly one text child");
1230 assert_eq!(text_of(&children[0]), Some("OK"), "{ty:?}: the label was mangled");
1231 assert_eq!(inline_properties(&children[0]), label_style, "{ty:?}: the label style is not on the label node");
1232 }
1233 }
1234
1235 #[test]
1236 fn dom_puts_the_image_before_the_label_and_keeps_the_child_count_cache_honest() {
1237 let mut b = btn("Save", ButtonType::Primary);
1238 b.set_image(ImageRef::null_image(16, 16, RawImageFormat::RGBA8, Vec::new()));
1239 let image_style = properties(&b.image_style);
1240 let dom = b.dom();
1241
1242 let children = dom.children.as_ref();
1243 assert_eq!(children.len(), 2, "an image button renders the image and the label");
1244 assert!(matches!(children[0].root.get_node_type(), NodeType::Image(_)), "the image must come first (left of the label)");
1245 assert_eq!(text_of(&children[1]), Some("Save"), "the label must be the second child");
1246 assert_eq!(inline_properties(&children[0]), image_style, "the image style is not on the image node");
1247
1248 assert_eq!(
1251 dom.estimated_total_children,
1252 count_descendants(&dom),
1253 "estimated_total_children is out of sync with the real subtree",
1254 );
1255 }
1256
1257 #[test]
1258 fn dom_child_count_cache_is_honest_without_an_image_too() {
1259 for ty in ALL_TYPES {
1260 let dom = btn("x", ty).dom();
1261 assert_eq!(dom.estimated_total_children, count_descendants(&dom), "{ty:?}: stale estimated_total_children");
1262 assert_eq!(dom.estimated_total_children, 1, "{ty:?}: an image-less button has exactly one descendant");
1263 }
1264 }
1265
1266 #[test]
1267 fn dom_preserves_adversarial_labels_verbatim() {
1268 for label in adversarial_labels() {
1269 let dom = btn(&label, ButtonType::Default).dom();
1270 let children = dom.children.as_ref();
1271 assert_eq!(children.len(), 1);
1272 let text = text_of(&children[0]).expect("the label child is not a text node");
1273 assert_eq!(text, label.as_str(), "a {}-byte label was mangled", label.len());
1274 assert_eq!(text.len(), label.len(), "a NUL or a wide char truncated the label");
1275 }
1276 }
1277
1278 #[test]
1279 fn dom_of_a_button_whose_label_looks_like_a_class_name_does_not_leak_into_the_classes() {
1280 let dom = btn("__azul-btn-danger", ButtonType::Primary).dom();
1282 assert_eq!(
1283 classes(&dom),
1284 vec!["__azul-native-button".to_string(), "__azul-btn-primary".to_string()],
1285 "the label leaked into the class list",
1286 );
1287 }
1288
1289 #[test]
1290 fn dom_renders_the_type_the_button_was_last_set_to() {
1291 for ty in ALL_TYPES {
1292 let dom = btn("x", ButtonType::Default).with_button_type(ty).dom();
1293 assert!(
1294 classes(&dom).contains(&ty.class_name().to_string()),
1295 "{ty:?}: the DOM still carries the old type class",
1296 );
1297 }
1298 }
1299
1300 #[test]
1301 fn dom_is_deterministic_across_identical_buttons() {
1302 for ty in ALL_TYPES {
1305 let a = btn("Same", ty).dom();
1306 let b = btn("Same", ty).dom();
1307 assert_eq!(a.root.get_node_type(), b.root.get_node_type(), "{ty:?}: node type differs");
1308 assert_eq!(classes(&a), classes(&b), "{ty:?}: classes differ");
1309 assert_eq!(inline_properties(&a), inline_properties(&b), "{ty:?}: inline style differs");
1310 assert_eq!(a.children.as_ref().len(), b.children.as_ref().len(), "{ty:?}: child count differs");
1311 }
1312 }
1313}