Skip to main content

azul_layout/widgets/
switch.rs

1//! Switch (toggle) widget — a boolean on/off control rendered as a rounded,
2//! pill-shaped "track" with a sliding circular "knob". A near-clone of
3//! [`crate::widgets::check_box::CheckBox`] (boolean state + an `on_toggle`
4//! callback) restyled as a switch: toggling flips the knob's horizontal
5//! position (via `margin-left`) and the track's background colour.
6//!
7//! Key types: [`Switch`], [`SwitchState`], [`SwitchOnToggle`].
8
9use azul_core::{
10    callbacks::{CoreCallbackData, Update},
11    dom::{Dom, IdOrClass, IdOrClass::Class, IdOrClassVec, TabIndex},
12    refany::RefAny,
13};
14use azul_css::dynamic_selector::{CssPropertyWithConditions, CssPropertyWithConditionsVec};
15use azul_css::{
16    props::{
17        basic::{color::ColorU, *},
18        layout::{LayoutDisplay, LayoutFlexDirection, LayoutAlignItems, LayoutAlignSelf, LayoutFlexGrow, LayoutWidth, LayoutHeight, LayoutPaddingLeft, LayoutPaddingRight, LayoutPaddingTop, LayoutPaddingBottom, LayoutMarginLeft},
19        property::{CssProperty, *},
20        style::{StyleBackgroundContent, StyleBackgroundContentVec, StyleBorderTopLeftRadius, StyleBorderTopRightRadius, StyleBorderBottomLeftRadius, StyleBorderBottomRightRadius, StyleCursor},
21    },
22    impl_option_inner, AzString,
23};
24
25use crate::callbacks::{Callback, CallbackInfo};
26
27static SWITCH_TRACK_CLASS: &[IdOrClass] =
28    &[Class(AzString::from_const_str("__azul-native-switch"))];
29static SWITCH_KNOB_CLASS: &[IdOrClass] =
30    &[Class(AzString::from_const_str("__azul-native-switch-knob"))];
31
32/// Callback function type invoked when the switch is toggled.
33pub type SwitchOnToggleCallbackType = extern "C" fn(RefAny, CallbackInfo, SwitchState) -> Update;
34impl_widget_callback!(
35    SwitchOnToggle,
36    OptionSwitchOnToggle,
37    SwitchOnToggleCallback,
38    SwitchOnToggleCallbackType
39);
40
41azul_core::impl_managed_callback! {
42    wrapper:        SwitchOnToggleCallback,
43    info_ty:        CallbackInfo,
44    return_ty:      Update,
45    default_ret:    Update::DoNothing,
46    invoker_static: SWITCH_ON_TOGGLE_INVOKER,
47    invoker_ty:     AzSwitchOnToggleCallbackInvoker,
48    thunk_fn:       az_switch_on_toggle_callback_thunk,
49    setter_fn:      AzApp_setSwitchOnToggleCallbackInvoker,
50    from_handle_fn: AzSwitchOnToggleCallback_createFromHostHandle,
51    extra_args:     [ state: SwitchState ],
52}
53
54/// A toggleable on/off switch widget with a sliding knob and toggle callback.
55#[derive(Debug, Clone, PartialEq, Eq)]
56#[repr(C)]
57pub struct Switch {
58    pub switch_state: SwitchStateWrapper,
59    /// Style for the switch track (the pill-shaped container)
60    pub track_style: CssPropertyWithConditionsVec,
61    /// Style for the sliding knob
62    pub knob_style: CssPropertyWithConditionsVec,
63}
64
65#[derive(Debug, Default, Clone, PartialEq, Eq)]
66#[repr(C)]
67pub struct SwitchStateWrapper {
68    /// On/off state of this Switch
69    pub inner: SwitchState,
70    /// Optional: function to call when the Switch is toggled
71    pub on_toggle: OptionSwitchOnToggle,
72}
73
74/// The on/off state of a [`Switch`].
75#[derive(Copy, Debug, Default, Clone, PartialEq, Eq)]
76#[repr(C)]
77pub struct SwitchState {
78    /// `true` = on (knob slid right), `false` = off (knob at left)
79    pub checked: bool,
80}
81
82// ---- dimensions ----
83const TRACK_WIDTH: isize = 36;
84const TRACK_HEIGHT: isize = 20;
85const TRACK_PADDING: isize = 2;
86const TRACK_RADIUS: isize = 10;
87const KNOB_SIZE: isize = 16;
88const KNOB_RADIUS: isize = 8;
89/// Horizontal travel of the knob = `track_width` − 2·padding − `knob_size`.
90const KNOB_TRAVEL: isize = TRACK_WIDTH - (2 * TRACK_PADDING) - KNOB_SIZE;
91
92// ---- colours ----
93const TRACK_OFF_COLOR: ColorU = ColorU {
94    r: 204,
95    g: 204,
96    b: 204,
97    a: 255,
98}; // #cccccc
99const TRACK_ON_COLOR: ColorU = ColorU {
100    r: 76,
101    g: 217,
102    b: 100,
103    a: 255,
104}; // #4cd964
105const KNOB_COLOR: ColorU = ColorU {
106    r: 255,
107    g: 255,
108    b: 255,
109    a: 255,
110}; // white
111
112const TRACK_OFF_BG_ITEMS: &[StyleBackgroundContent] =
113    &[StyleBackgroundContent::Color(TRACK_OFF_COLOR)];
114const TRACK_OFF_BG: StyleBackgroundContentVec =
115    StyleBackgroundContentVec::from_const_slice(TRACK_OFF_BG_ITEMS);
116const TRACK_ON_BG_ITEMS: &[StyleBackgroundContent] =
117    &[StyleBackgroundContent::Color(TRACK_ON_COLOR)];
118const TRACK_ON_BG: StyleBackgroundContentVec =
119    StyleBackgroundContentVec::from_const_slice(TRACK_ON_BG_ITEMS);
120const KNOB_BG_ITEMS: &[StyleBackgroundContent] = &[StyleBackgroundContent::Color(KNOB_COLOR)];
121const KNOB_BG: StyleBackgroundContentVec =
122    StyleBackgroundContentVec::from_const_slice(KNOB_BG_ITEMS);
123
124/// Build the track (pill container) style. Background colour is the only
125/// state-dependent property, so the style is built at runtime per the recipe's
126/// "runtime vec if param-dependent" path.
127fn build_track_style(checked: bool) -> CssPropertyWithConditionsVec {
128    let bg = if checked { TRACK_ON_BG } else { TRACK_OFF_BG };
129    CssPropertyWithConditionsVec::from_vec(alloc::vec![
130        CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Flex)),
131        CssPropertyWithConditions::simple(CssProperty::const_flex_direction(
132            LayoutFlexDirection::Row,
133        )),
134        CssPropertyWithConditions::simple(CssProperty::const_align_items(LayoutAlignItems::Center)),
135        CssPropertyWithConditions::simple(CssProperty::align_self(LayoutAlignSelf::Center)),
136        CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(
137            0,
138        ))),
139        CssPropertyWithConditions::simple(CssProperty::const_width(LayoutWidth::const_px(
140            TRACK_WIDTH,
141        ))),
142        CssPropertyWithConditions::simple(CssProperty::const_height(LayoutHeight::const_px(
143            TRACK_HEIGHT,
144        ))),
145        CssPropertyWithConditions::simple(CssProperty::const_padding_left(
146            LayoutPaddingLeft::const_px(TRACK_PADDING),
147        )),
148        CssPropertyWithConditions::simple(CssProperty::const_padding_right(
149            LayoutPaddingRight::const_px(TRACK_PADDING),
150        )),
151        CssPropertyWithConditions::simple(CssProperty::const_padding_top(
152            LayoutPaddingTop::const_px(TRACK_PADDING),
153        )),
154        CssPropertyWithConditions::simple(CssProperty::const_padding_bottom(
155            LayoutPaddingBottom::const_px(TRACK_PADDING),
156        )),
157        CssPropertyWithConditions::simple(CssProperty::const_border_top_left_radius(
158            StyleBorderTopLeftRadius::const_px(TRACK_RADIUS),
159        )),
160        CssPropertyWithConditions::simple(CssProperty::const_border_top_right_radius(
161            StyleBorderTopRightRadius::const_px(TRACK_RADIUS),
162        )),
163        CssPropertyWithConditions::simple(CssProperty::const_border_bottom_left_radius(
164            StyleBorderBottomLeftRadius::const_px(TRACK_RADIUS),
165        )),
166        CssPropertyWithConditions::simple(CssProperty::const_border_bottom_right_radius(
167            StyleBorderBottomRightRadius::const_px(TRACK_RADIUS),
168        )),
169        CssPropertyWithConditions::simple(CssProperty::const_cursor(StyleCursor::Pointer)),
170        CssPropertyWithConditions::simple(CssProperty::const_background_content(bg)),
171    ])
172}
173
174/// Build the knob style. The knob's `margin-left` is the state-dependent
175/// property that slides it between the off (left) and on (right) positions.
176fn build_knob_style(checked: bool) -> CssPropertyWithConditionsVec {
177    let margin = if checked { KNOB_TRAVEL } else { 0 };
178    CssPropertyWithConditionsVec::from_vec(alloc::vec![
179        CssPropertyWithConditions::simple(CssProperty::const_width(LayoutWidth::const_px(
180            KNOB_SIZE,
181        ))),
182        CssPropertyWithConditions::simple(CssProperty::const_height(LayoutHeight::const_px(
183            KNOB_SIZE,
184        ))),
185        CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(
186            0,
187        ))),
188        CssPropertyWithConditions::simple(CssProperty::const_border_top_left_radius(
189            StyleBorderTopLeftRadius::const_px(KNOB_RADIUS),
190        )),
191        CssPropertyWithConditions::simple(CssProperty::const_border_top_right_radius(
192            StyleBorderTopRightRadius::const_px(KNOB_RADIUS),
193        )),
194        CssPropertyWithConditions::simple(CssProperty::const_border_bottom_left_radius(
195            StyleBorderBottomLeftRadius::const_px(KNOB_RADIUS),
196        )),
197        CssPropertyWithConditions::simple(CssProperty::const_border_bottom_right_radius(
198            StyleBorderBottomRightRadius::const_px(KNOB_RADIUS),
199        )),
200        CssPropertyWithConditions::simple(CssProperty::const_background_content(KNOB_BG)),
201        CssPropertyWithConditions::simple(CssProperty::const_margin_left(
202            LayoutMarginLeft::const_px(margin),
203        )),
204    ])
205}
206
207impl Switch {
208    /// Creates a new switch in the given on/off state with default styling.
209    #[must_use] pub fn create(checked: bool) -> Self {
210        Self {
211            switch_state: SwitchStateWrapper {
212                inner: SwitchState { checked },
213                ..Default::default()
214            },
215            track_style: build_track_style(checked),
216            knob_style: build_knob_style(checked),
217        }
218    }
219
220    #[inline]
221    #[must_use] pub fn swap_with_default(&mut self) -> Self {
222        let mut s = Self::create(false);
223        core::mem::swap(&mut s, self);
224        s
225    }
226
227    #[inline]
228    pub fn set_on_toggle<C: Into<SwitchOnToggleCallback>>(&mut self, data: RefAny, on_toggle: C) {
229        self.switch_state.on_toggle = Some(SwitchOnToggle {
230            callback: on_toggle.into(),
231            refany: data,
232        })
233        .into();
234    }
235
236    #[inline]
237    #[must_use] pub fn with_on_toggle<C: Into<SwitchOnToggleCallback>>(
238        mut self,
239        data: RefAny,
240        on_toggle: C,
241    ) -> Self {
242        self.set_on_toggle(data, on_toggle);
243        self
244    }
245
246    #[inline]
247    #[must_use] pub fn dom(self) -> Dom {
248        use azul_core::{
249            callbacks::{CoreCallback, CoreCallbackData},
250            dom::{Dom, EventFilter, HoverEventFilter},
251        };
252
253        Dom::create_div()
254            .with_ids_and_classes(IdOrClassVec::from(SWITCH_TRACK_CLASS))
255            .with_css_props(self.track_style)
256            .with_callbacks(
257                vec![CoreCallbackData {
258                    event: EventFilter::Hover(HoverEventFilter::MouseUp),
259                    callback: CoreCallback {
260                        cb: input::default_on_switch_clicked as usize,
261                        ctx: azul_core::refany::OptionRefAny::None,
262                    },
263                    refany: RefAny::new(self.switch_state),
264                }]
265                .into(),
266            )
267            .with_tab_index(TabIndex::Auto)
268            .with_children(
269                vec![Dom::create_div()
270                    .with_ids_and_classes(IdOrClassVec::from(SWITCH_KNOB_CLASS))
271                    .with_css_props(self.knob_style)]
272                .into(),
273            )
274    }
275}
276
277impl Default for Switch {
278    fn default() -> Self {
279        Self::create(false)
280    }
281}
282
283// handle input events for the switch
284mod input {
285
286    use azul_core::{callbacks::Update, refany::RefAny};
287    use azul_css::props::{layout::LayoutMarginLeft, property::CssProperty};
288
289    use super::{SwitchOnToggle, SwitchStateWrapper, KNOB_TRAVEL, TRACK_OFF_BG, TRACK_ON_BG};
290    use crate::callbacks::CallbackInfo;
291
292    pub(super) extern "C" fn default_on_switch_clicked(
293        mut switch: RefAny,
294        mut info: CallbackInfo,
295    ) -> Update {
296        let Some(mut switch) = switch.downcast_mut::<SwitchStateWrapper>() else {
297            return Update::DoNothing;
298        };
299
300        let track_id = info.get_hit_node();
301        let Some(knob_id) = info.get_first_child(track_id) else {
302            return Update::DoNothing;
303        };
304
305        switch.inner.checked = !switch.inner.checked;
306
307        let result = {
308            // rustc doesn't understand the borrowing lifetime here
309            let switch = &mut *switch;
310            let on_toggle = &mut switch.on_toggle;
311            let inner = switch.inner;
312
313            match on_toggle.as_mut() {
314                Some(SwitchOnToggle {
315                    callback,
316                    refany: data,
317                }) => (callback.cb)(data.clone(), info, inner),
318                None => Update::DoNothing,
319            }
320        };
321
322        // CallbackInfo is Copy, so `info` is still usable after the call above.
323        if switch.inner.checked {
324            info.set_css_property(track_id, CssProperty::const_background_content(TRACK_ON_BG));
325            info.set_css_property(
326                knob_id,
327                CssProperty::const_margin_left(LayoutMarginLeft::const_px(KNOB_TRAVEL)),
328            );
329        } else {
330            info.set_css_property(track_id, CssProperty::const_background_content(TRACK_OFF_BG));
331            info.set_css_property(
332                knob_id,
333                CssProperty::const_margin_left(LayoutMarginLeft::const_px(0)),
334            );
335        }
336
337        result
338    }
339}
340
341impl From<Switch> for Dom {
342    fn from(s: Switch) -> Self {
343        s.dom()
344    }
345}