Skip to main content

azul_layout/widgets/
card.rs

1//! Card widget — an elevated, bordered content container with rounded corners,
2//! a soft drop shadow and padding, holding arbitrary child content. A near-clone
3//! of [`crate::widgets::frame::Frame`] (a container) but without the fieldset
4//! title/header — just a single styled box wrapping the body content.
5//!
6//! Key types: [`Card`], [`CardOnClick`].
7
8use azul_core::{
9    callbacks::Update,
10    dom::{Dom, IdOrClass, IdOrClass::Class, IdOrClassVec},
11    refany::RefAny,
12};
13use azul_css::css::BoxOrStatic;
14use azul_css::{
15    dynamic_selector::{CssPropertyWithConditions, CssPropertyWithConditionsVec},
16    props::{
17        basic::{ColorU, PixelValueNoPercent, PixelValue, FloatValue},
18        layout::{LayoutDisplay, LayoutFlexDirection, LayoutPaddingTop, LayoutPaddingBottom, LayoutPaddingLeft, LayoutPaddingRight, LayoutFlexGrow},
19        property::{CssProperty, StyleBoxShadowValue, LayoutFlexGrowValue},
20        style::{StyleBackgroundContent, StyleBackgroundContentVec, StyleBoxShadow, BoxShadowClipMode, LayoutBorderTopWidth, LayoutBorderBottomWidth, LayoutBorderLeftWidth, LayoutBorderRightWidth, StyleBorderTopStyle, BorderStyle, StyleBorderBottomStyle, StyleBorderLeftStyle, StyleBorderRightStyle, StyleBorderTopColor, StyleBorderBottomColor, StyleBorderLeftColor, StyleBorderRightColor, StyleBorderTopLeftRadius, StyleBorderTopRightRadius, StyleBorderBottomLeftRadius, StyleBorderBottomRightRadius},
21    },
22    impl_option_inner, AzString,
23};
24
25use crate::callbacks::CallbackInfo;
26
27/// Card border colour (#dee2e6).
28const CARD_BORDER_COLOR: ColorU = ColorU {
29    r: 222,
30    g: 226,
31    b: 230,
32    a: 255,
33};
34/// Card background colour (white).
35const CARD_BG_COLOR: ColorU = ColorU {
36    r: 255,
37    g: 255,
38    b: 255,
39    a: 255,
40};
41/// Soft drop-shadow colour (black @ ~15% alpha).
42const CARD_SHADOW_COLOR: ColorU = ColorU {
43    r: 0,
44    g: 0,
45    b: 0,
46    a: 38,
47};
48
49const CARD_BG_ITEMS: &[StyleBackgroundContent] = &[StyleBackgroundContent::Color(CARD_BG_COLOR)];
50const CARD_BG: StyleBackgroundContentVec =
51    StyleBackgroundContentVec::from_const_slice(CARD_BG_ITEMS);
52
53/// Shared drop-shadow descriptor referenced by all four edge box-shadows.
54static CARD_SHADOW: StyleBoxShadow = StyleBoxShadow {
55    offset_x: PixelValueNoPercent {
56        inner: PixelValue::const_px(0),
57    },
58    offset_y: PixelValueNoPercent {
59        inner: PixelValue::const_px(2),
60    },
61    blur_radius: PixelValueNoPercent {
62        inner: PixelValue::const_px(6),
63    },
64    spread_radius: PixelValueNoPercent {
65        inner: PixelValue::const_px(0),
66    },
67    clip_mode: BoxShadowClipMode::Outset,
68    color: CARD_SHADOW_COLOR,
69};
70
71const CARD_STYLE: &[CssPropertyWithConditions] = &[
72    CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Flex)),
73    CssPropertyWithConditions::simple(CssProperty::const_flex_direction(
74        LayoutFlexDirection::Column,
75    )),
76    CssPropertyWithConditions::simple(CssProperty::const_background_content(CARD_BG)),
77    // padding: 12px
78    CssPropertyWithConditions::simple(CssProperty::const_padding_top(LayoutPaddingTop::const_px(
79        12,
80    ))),
81    CssPropertyWithConditions::simple(CssProperty::const_padding_bottom(
82        LayoutPaddingBottom::const_px(12),
83    )),
84    CssPropertyWithConditions::simple(CssProperty::const_padding_left(LayoutPaddingLeft::const_px(
85        12,
86    ))),
87    CssPropertyWithConditions::simple(CssProperty::const_padding_right(
88        LayoutPaddingRight::const_px(12),
89    )),
90    // border: 1px solid #dee2e6
91    CssPropertyWithConditions::simple(CssProperty::const_border_top_width(
92        LayoutBorderTopWidth::const_px(1),
93    )),
94    CssPropertyWithConditions::simple(CssProperty::const_border_bottom_width(
95        LayoutBorderBottomWidth::const_px(1),
96    )),
97    CssPropertyWithConditions::simple(CssProperty::const_border_left_width(
98        LayoutBorderLeftWidth::const_px(1),
99    )),
100    CssPropertyWithConditions::simple(CssProperty::const_border_right_width(
101        LayoutBorderRightWidth::const_px(1),
102    )),
103    CssPropertyWithConditions::simple(CssProperty::const_border_top_style(StyleBorderTopStyle {
104        inner: BorderStyle::Solid,
105    })),
106    CssPropertyWithConditions::simple(CssProperty::const_border_bottom_style(
107        StyleBorderBottomStyle {
108            inner: BorderStyle::Solid,
109        },
110    )),
111    CssPropertyWithConditions::simple(CssProperty::const_border_left_style(StyleBorderLeftStyle {
112        inner: BorderStyle::Solid,
113    })),
114    CssPropertyWithConditions::simple(CssProperty::const_border_right_style(
115        StyleBorderRightStyle {
116            inner: BorderStyle::Solid,
117        },
118    )),
119    CssPropertyWithConditions::simple(CssProperty::const_border_top_color(StyleBorderTopColor {
120        inner: CARD_BORDER_COLOR,
121    })),
122    CssPropertyWithConditions::simple(CssProperty::const_border_bottom_color(
123        StyleBorderBottomColor {
124            inner: CARD_BORDER_COLOR,
125        },
126    )),
127    CssPropertyWithConditions::simple(CssProperty::const_border_left_color(StyleBorderLeftColor {
128        inner: CARD_BORDER_COLOR,
129    })),
130    CssPropertyWithConditions::simple(CssProperty::const_border_right_color(
131        StyleBorderRightColor {
132            inner: CARD_BORDER_COLOR,
133        },
134    )),
135    // border-radius: 8px
136    CssPropertyWithConditions::simple(CssProperty::const_border_top_left_radius(
137        StyleBorderTopLeftRadius::const_px(8),
138    )),
139    CssPropertyWithConditions::simple(CssProperty::const_border_top_right_radius(
140        StyleBorderTopRightRadius::const_px(8),
141    )),
142    CssPropertyWithConditions::simple(CssProperty::const_border_bottom_left_radius(
143        StyleBorderBottomLeftRadius::const_px(8),
144    )),
145    CssPropertyWithConditions::simple(CssProperty::const_border_bottom_right_radius(
146        StyleBorderBottomRightRadius::const_px(8),
147    )),
148    // soft drop shadow on all four edges
149    CssPropertyWithConditions::simple(CssProperty::BoxShadowTop(StyleBoxShadowValue::Exact(
150        BoxOrStatic::Static(&raw const CARD_SHADOW),
151    ))),
152    CssPropertyWithConditions::simple(CssProperty::BoxShadowBottom(StyleBoxShadowValue::Exact(
153        BoxOrStatic::Static(&raw const CARD_SHADOW),
154    ))),
155    CssPropertyWithConditions::simple(CssProperty::BoxShadowLeft(StyleBoxShadowValue::Exact(
156        BoxOrStatic::Static(&raw const CARD_SHADOW),
157    ))),
158    CssPropertyWithConditions::simple(CssProperty::BoxShadowRight(StyleBoxShadowValue::Exact(
159        BoxOrStatic::Static(&raw const CARD_SHADOW),
160    ))),
161];
162
163/// An elevated, bordered content container with rounded corners, a soft drop
164/// shadow and padding. Holds arbitrary child content (`content`).
165#[derive(Debug, Clone, PartialEq)]
166#[repr(C)]
167pub struct Card {
168    /// The body content rendered inside the card.
169    pub content: Dom,
170    /// `flex-grow` factor applied to the card container.
171    pub flex_grow: f32,
172    /// Optional: Function to call when the card is clicked
173    pub on_click: OptionCardOnClick,
174}
175
176/// Callback function type invoked when the card container is clicked.
177pub type CardOnClickCallbackType = extern "C" fn(RefAny, CallbackInfo) -> Update;
178impl_widget_callback!(
179    CardOnClick,
180    OptionCardOnClick,
181    CardOnClickCallback,
182    CardOnClickCallbackType
183);
184
185// Host-invoker plumbing for managed-FFI bindings — see core/src/host_invoker.rs.
186azul_core::impl_managed_callback! {
187    wrapper:        CardOnClickCallback,
188    info_ty:        CallbackInfo,
189    return_ty:      Update,
190    default_ret:    Update::DoNothing,
191    invoker_static: CARD_ON_CLICK_INVOKER,
192    invoker_ty:     AzCardOnClickCallbackInvoker,
193    thunk_fn:       az_card_on_click_callback_thunk,
194    setter_fn:      AzApp_setCardOnClickCallbackInvoker,
195    from_handle_fn: AzCardOnClickCallback_createFromHostHandle,
196}
197
198impl Card {
199    /// Creates a new `Card` wrapping the given content DOM.
200    #[must_use] pub const fn create(content: Dom) -> Self {
201        Self {
202            content,
203            flex_grow: 0.0,
204            on_click: OptionCardOnClick::None,
205        }
206    }
207
208    /// Replaces `self` with an empty default card and returns the original.
209    #[must_use] pub const fn swap_with_default(&mut self) -> Self {
210        let mut s = Self::create(Dom::create_div());
211        core::mem::swap(&mut s, self);
212        s
213    }
214
215    /// Sets the body content.
216    pub fn set_content(&mut self, content: Dom) {
217        self.content = content;
218    }
219
220    /// Builder-style setter for the body content.
221    #[must_use] pub fn with_content(mut self, content: Dom) -> Self {
222        self.set_content(content);
223        self
224    }
225
226    /// Sets the flex-grow factor for the card container.
227    pub const fn set_flex_grow(&mut self, flex_grow: f32) {
228        self.flex_grow = flex_grow;
229    }
230
231    /// Builder-style setter for the flex-grow factor.
232    #[must_use] pub const fn with_flex_grow(mut self, flex_grow: f32) -> Self {
233        self.set_flex_grow(flex_grow);
234        self
235    }
236
237    /// Sets the click callback, invoked when the card container is clicked.
238    pub fn set_on_click<C: Into<CardOnClickCallback>>(&mut self, data: RefAny, on_click: C) {
239        self.on_click = Some(CardOnClick {
240            refany: data,
241            callback: on_click.into(),
242        })
243        .into();
244    }
245
246    /// Builder-style setter for the click callback.
247    #[must_use] pub fn with_on_click<C: Into<CardOnClickCallback>>(
248        mut self,
249        data: RefAny,
250        on_click: C,
251    ) -> Self {
252        self.set_on_click(data, on_click);
253        self
254    }
255
256    #[must_use] pub fn dom(self) -> Dom {
257        use azul_core::{
258            callbacks::{CoreCallback, CoreCallbackData},
259            dom::{EventFilter, HoverEventFilter},
260        };
261
262        static CARD_CLASS: &[IdOrClass] =
263            &[Class(AzString::from_const_str("__azul-native-card"))];
264
265        // Optional click callback on the card's root container (same wiring
266        // as button's on_click).
267        let callbacks = match self.on_click.into_option() {
268            Some(CardOnClick {
269                refany: data,
270                callback,
271            }) => vec![CoreCallbackData {
272                event: EventFilter::Hover(HoverEventFilter::MouseUp),
273                callback: CoreCallback {
274                    cb: callback.cb as *const () as usize,
275                    ctx: callback.ctx,
276                },
277                refany: data,
278            }],
279            None => Vec::new(),
280        };
281
282        // Prepend the (param-dependent) flex-grow, then the static card style.
283        let mut props = vec![CssPropertyWithConditions::simple(CssProperty::FlexGrow(
284            LayoutFlexGrowValue::Exact(LayoutFlexGrow {
285                inner: FloatValue::new(self.flex_grow),
286            }),
287        ))];
288        props.extend_from_slice(CARD_STYLE);
289
290        Dom::create_div()
291            .with_ids_and_classes(IdOrClassVec::from_const_slice(CARD_CLASS))
292            .with_css_props(CssPropertyWithConditionsVec::from_vec(props))
293            .with_callbacks(callbacks.into())
294            .with_children(vec![self.content].into())
295    }
296}
297
298impl Default for Card {
299    fn default() -> Self {
300        Self::create(Dom::create_div())
301    }
302}
303
304impl From<Card> for Dom {
305    fn from(c: Card) -> Self {
306        c.dom()
307    }
308}