Skip to main content

azul_layout/widgets/
button.rs

1//! Button widget with Bootstrap-inspired type-based styling (`ButtonType`).
2
3use 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)] // widget/render module pulls in the css property/value types it builds with
12use 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/// The semantic type/role of a button.
31/// 
32/// Each type has distinct styling to indicate its purpose to the user.
33/// Colors are based on Bootstrap's button variants for familiarity.
34#[repr(C)]
35#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
36pub enum ButtonType {
37    /// Default button style - neutral/gray appearance
38    #[default]
39    Default,
40    /// Primary action button - blue, uses system accent color on macOS
41    Primary,
42    /// Secondary button - gray, less prominent than primary
43    Secondary,
44    /// Success/confirmation button - green with white text
45    Success,
46    /// Danger/destructive button - red with white text
47    Danger,
48    /// Warning button - yellow with BLACK text
49    Warning,
50    /// Informational button - teal/cyan with white text
51    Info,
52    /// Link-style button - appears as a hyperlink, no background
53    Link,
54}
55
56impl ButtonType {
57    /// Get the CSS class name for this button type
58    #[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    /// Content (image or text) of this button, centered by default
76    pub label: AzString,
77    /// Optional image that is displayed next to the label
78    pub image: OptionImageRef,
79    /// The semantic type of this button (Primary, Success, Danger, etc.)
80    pub button_type: ButtonType,
81    /// Style for this button container
82    pub container_style: CssPropertyWithConditionsVec,
83    /// Style of the label
84    pub label_style: CssPropertyWithConditionsVec,
85    /// Style of the image
86    pub image_style: CssPropertyWithConditionsVec,
87    /// Optional: Function to call when the button is clicked
88    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
99// Host-invoker plumbing for managed-FFI bindings — see core/src/host_invoker.rs.
100azul_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
112// ButtonType-specific styling
113// ============================================================
114
115/// Get the background color for a button type
116const fn get_button_colors(button_type: ButtonType) -> (ColorU, ColorU, ColorU) {
117    // Returns (normal, hover, active) colors
118    match button_type {
119        ButtonType::Default => (
120            ColorU::rgb(248, 249, 250), // Light gray
121            ColorU::rgb(233, 236, 239), // Darker gray on hover
122            ColorU::rgb(218, 222, 226), // Even darker on active
123        ),
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
162/// Get the text color for a button type
163const fn get_button_text_color(button_type: ButtonType) -> ColorU {
164    match button_type {
165        ButtonType::Default => ColorU::rgb(33, 37, 41),   // Dark text
166        ButtonType::Warning => ColorU::BLACK,             // Black text on yellow
167        ButtonType::Link => ColorU::bootstrap_link(),     // Blue link color
168        _ => ColorU::WHITE,                               // White text on colored buttons
169    }
170}
171
172/// Build container style properties for a button type
173fn build_button_container_style(button_type: ButtonType) -> Vec<CssPropertyWithConditions> {
174    // ⚠ BISECTION PROBE (2026-06-02, REVERT): return a MINIMAL container style — no const
175    // background, no hover/active gradients, no conditions — to test whether the
176    // container's COMPLEX props cause the web cascade OOB on AzButton. If web-button-nocb
177    // RUNS with this → the complex container props are the root; if it still OOBs → the
178    // label/structure is. Remove this `return` to restore the real button styling.
179    // ⚠ BISECTION step 7 (REVERT): InlineFlex → Block. The cascade + inline style now work
180    // (rules=3, disp correct), but layout returns InvalidTree + width=0 because InlineFlex
181    // triggers the (deferred) taffy flex-algorithm lift gap. Block layout is known-good on web.
182    // If this lays out (no InvalidTree, sized button) → confirms flex is the layout blocker.
183    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    // Focus outline uses system accent color
193    let focus_outline_color = ColorU::bootstrap_primary();
194    
195    let mut props = Vec::with_capacity(40);
196    
197    // Basic layout - use InlineFlex so flex properties (justify-content, align-items) work
198    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    // Prevent stretching when inside a flex column container
203    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    // Text color
208    props.push(CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor { inner: text_color })));
209    
210    // Padding (Bootstrap-like)
211    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    // Border radius
217    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        // Link buttons have no background or border
224        props.push(CssPropertyWithConditions::simple(CssProperty::const_background_content(
225            StyleBackgroundContentVec::from_const_slice(&[StyleBackgroundContent::Color(ColorU::TRANSPARENT)]),
226        )));
227        
228        // Underline on hover - use TextDecoration::Underline variant
229        props.push(CssPropertyWithConditions::on_hover(CssProperty::TextDecoration(StyleTextDecoration::Underline.into())));
230    } else {
231        // Normal background
232        props.push(CssPropertyWithConditions::simple(CssProperty::const_background_content(
233            StyleBackgroundContentVec::from_vec(vec![StyleBackgroundContent::Color(bg_normal)]),
234        )));
235        
236        // Border (subtle for Default, transparent for others to maintain size)
237        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        // Hover state
256        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        // Active (pressed) state
268        props.push(CssPropertyWithConditions::on_active(CssProperty::BackgroundContent(
269            StyleBackgroundContentVec::from_vec(vec![StyleBackgroundContent::Color(bg_active)]).into(),
270        )));
271        
272        // Focus state - uses accent color for outline
273        // This makes the button feel "native" as it uses the system accent
274        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
283/// Build label style properties
284fn build_button_label_style() -> Vec<CssPropertyWithConditions> {
285    // Use system UI font
286    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    /// Create a button with `ButtonType::Default` styling.
300    #[inline]
301    #[must_use] pub fn create(label: AzString) -> Self {
302        Self::with_type(label, ButtonType::Default)
303    }
304    
305    /// Create a button with a specific type (Primary, Success, Danger, etc.)
306    #[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    /// Set the button type and update styling accordingly
323    #[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    /// Builder method to set the button type
330    #[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        // Add both the base class and the type-specific class
392        // ⚠ BISECTION step 5 (REVERT): const-str classes → HEAP classes (AzString::from(&str)
393        // = s.to_string().into()). Decisive test: if web-button-nocb RUNS now → the const-str
394        // CLONE (s.clone() of a NoDestructor/borrowed AzString in set_ids_and_classes) mis-lifts
395        // (deref of unmirrored .rodata); fix = transpiler const-str mirror OR heap classes here.
396        // If it still OOBs → the AttributeTypeVec machinery (swap/into_library_owned_vec/retain/
397        // push/set_attributes) is the lift bug, independent of const-str.
398        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        // (2026-06-10: the June-02 bisection strips are REVERTED — the underlying corruption
405        // was the alloc collect-machinery Leaf-stub in the web transpiler, fixed there. The
406        // label keeps its inline css; the button carries its on_click callbacks + tab index
407        // again — without them every Button click was a silent no-op on ALL backends, and the
408        // web route-walk discovered 0 callbacks. The FIX-A ordering (container style before
409        // ids/classes) is kept: builder-order is semantically neutral natively.)
410        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 an image was set via `set_image`, render it as the first child
416        // (left of the label, since the container is a horizontal flex row).
417        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}