Skip to main content

azul_layout/widgets/
segmented.rs

1//! Segmented control / button-group widget — a joined row of mutually-exclusive
2//! buttons where exactly one is selected. A blend of the `tabs::TabHeader` row of
3//! clickable labels and `button.rs`'s styling, with the stateful 3-type split
4//! (state / state-wrapper / widget) of the other interactive widgets.
5//!
6//! Clicking a segment selects it: the internal handler computes the clicked
7//! segment's index from its position among its siblings, updates the
8//! `selected_index`, invokes the user's `on_change(index)`, and live-restyles
9//! every segment (selected vs unselected) via `set_css_property`.
10//!
11//! Key types: [`Segmented`], [`SegmentedState`], [`SegmentedOnChange`].
12
13use std::vec::Vec;
14
15use azul_core::{
16    callbacks::{CoreCallbackData, Update},
17    dom::{Dom, IdOrClass, IdOrClass::Class, IdOrClassVec, TabIndex},
18    refany::RefAny,
19};
20use azul_css::dynamic_selector::{CssPropertyWithConditions, CssPropertyWithConditionsVec};
21use azul_css::{
22    props::{
23        basic::{color::ColorU, StyleFontSize},
24        layout::{LayoutDisplay, LayoutFlexDirection, LayoutAlignItems, LayoutAlignSelf, LayoutFlexGrow, LayoutJustifyContent, LayoutPaddingTop, LayoutPaddingBottom, LayoutPaddingLeft, LayoutPaddingRight},
25        property::{CssProperty, *},
26        style::{StyleBackgroundContent, StyleBackgroundContentVec, LayoutBorderTopWidth, LayoutBorderBottomWidth, LayoutBorderRightWidth, StyleBorderTopStyle, BorderStyle, StyleBorderBottomStyle, StyleBorderRightStyle, StyleBorderTopColor, StyleBorderBottomColor, StyleBorderRightColor, StyleCursor, StyleTextAlign, StyleUserSelect, StyleTextColor, LayoutBorderLeftWidth, StyleBorderLeftStyle, StyleBorderLeftColor, StyleBorderTopLeftRadius, StyleBorderBottomLeftRadius, StyleBorderTopRightRadius, StyleBorderBottomRightRadius},
27    },
28    impl_option_inner, AzString, StringVec,
29};
30
31use crate::callbacks::{Callback, CallbackInfo};
32
33static SEGMENTED_CLASS: &[IdOrClass] =
34    &[Class(AzString::from_const_str("__azul-native-segmented"))];
35static SEGMENT_ITEM_CLASS: &[IdOrClass] =
36    &[Class(AzString::from_const_str("__azul-native-segmented-item"))];
37
38/// Callback function type invoked when the selected segment changes.
39pub type SegmentedOnChangeCallbackType =
40    extern "C" fn(RefAny, CallbackInfo, SegmentedState) -> Update;
41impl_widget_callback!(
42    SegmentedOnChange,
43    OptionSegmentedOnChange,
44    SegmentedOnChangeCallback,
45    SegmentedOnChangeCallbackType
46);
47
48azul_core::impl_managed_callback! {
49    wrapper:        SegmentedOnChangeCallback,
50    info_ty:        CallbackInfo,
51    return_ty:      Update,
52    default_ret:    Update::DoNothing,
53    invoker_static: SEGMENTED_ON_CHANGE_INVOKER,
54    invoker_ty:     AzSegmentedOnChangeCallbackInvoker,
55    thunk_fn:       az_segmented_on_change_callback_thunk,
56    setter_fn:      AzApp_setSegmentedOnChangeCallbackInvoker,
57    from_handle_fn: AzSegmentedOnChangeCallback_createFromHostHandle,
58    extra_args:     [ state: SegmentedState ],
59}
60
61/// A joined row of mutually-exclusive segments with a selection callback.
62#[derive(Debug, Clone, PartialEq, Eq)]
63#[repr(C)]
64pub struct Segmented {
65    pub segmented_state: SegmentedStateWrapper,
66    /// The label of each segment, in order.
67    pub labels: StringVec,
68    /// Style for the row container.
69    pub container_style: CssPropertyWithConditionsVec,
70}
71
72#[derive(Debug, Default, Clone, PartialEq, Eq)]
73#[repr(C)]
74pub struct SegmentedStateWrapper {
75    /// The current selection.
76    pub inner: SegmentedState,
77    /// Optional: function to call when the selection changes.
78    pub on_change: OptionSegmentedOnChange,
79}
80
81/// State of a [`Segmented`]: the index of the currently selected segment.
82#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
83#[repr(C)]
84pub struct SegmentedState {
85    /// Zero-based index of the selected segment.
86    pub selected_index: usize,
87}
88
89// ---- colours ----
90/// Segment border colour (#ced4da).
91const SEG_BORDER_COLOR: ColorU = ColorU {
92    r: 206,
93    g: 212,
94    b: 218,
95    a: 255,
96};
97/// Selected-segment background (#0d6efd, accent blue).
98const SEG_SELECTED_BG_COLOR: ColorU = ColorU {
99    r: 13,
100    g: 110,
101    b: 253,
102    a: 255,
103};
104/// Unselected-segment background (white).
105const SEG_UNSELECTED_BG_COLOR: ColorU = ColorU {
106    r: 255,
107    g: 255,
108    b: 255,
109    a: 255,
110};
111/// Selected-segment text colour (white).
112const SEG_SELECTED_TEXT: ColorU = ColorU {
113    r: 255,
114    g: 255,
115    b: 255,
116    a: 255,
117};
118/// Unselected-segment text colour (#212529, dark).
119const SEG_UNSELECTED_TEXT: ColorU = ColorU {
120    r: 33,
121    g: 37,
122    b: 41,
123    a: 255,
124};
125
126const SEG_SELECTED_BG_ITEMS: &[StyleBackgroundContent] =
127    &[StyleBackgroundContent::Color(SEG_SELECTED_BG_COLOR)];
128const SEG_SELECTED_BG: StyleBackgroundContentVec =
129    StyleBackgroundContentVec::from_const_slice(SEG_SELECTED_BG_ITEMS);
130const SEG_UNSELECTED_BG_ITEMS: &[StyleBackgroundContent] =
131    &[StyleBackgroundContent::Color(SEG_UNSELECTED_BG_COLOR)];
132const SEG_UNSELECTED_BG: StyleBackgroundContentVec =
133    StyleBackgroundContentVec::from_const_slice(SEG_UNSELECTED_BG_ITEMS);
134
135const SEG_RADIUS: isize = 6;
136
137/// Row container: a horizontal flex row that hugs its content.
138static SEGMENTED_CONTAINER_STYLE: &[CssPropertyWithConditions] = &[
139    CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Flex)),
140    CssPropertyWithConditions::simple(CssProperty::const_flex_direction(LayoutFlexDirection::Row)),
141    CssPropertyWithConditions::simple(CssProperty::const_align_items(LayoutAlignItems::Center)),
142    CssPropertyWithConditions::simple(CssProperty::align_self(LayoutAlignSelf::Start)),
143    CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
144];
145
146/// Builds the style for one segment. The selected/unselected colours and the
147/// rounding of the outer corners (only the first segment is rounded on the left,
148/// only the last on the right) are the position-dependent properties, so the
149/// style is built at runtime.
150#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
151fn build_segment_style(selected: bool, is_first: bool, is_last: bool) -> CssPropertyWithConditionsVec {
152    let (bg, text) = if selected {
153        (SEG_SELECTED_BG, SEG_SELECTED_TEXT)
154    } else {
155        (SEG_UNSELECTED_BG, SEG_UNSELECTED_TEXT)
156    };
157
158    let mut v: Vec<CssPropertyWithConditions> = alloc::vec![
159        CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Flex)),
160        CssPropertyWithConditions::simple(CssProperty::const_flex_direction(
161            LayoutFlexDirection::Row,
162        )),
163        CssPropertyWithConditions::simple(CssProperty::const_justify_content(
164            LayoutJustifyContent::Center,
165        )),
166        CssPropertyWithConditions::simple(CssProperty::const_align_items(LayoutAlignItems::Center)),
167        CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(
168            0,
169        ))),
170        // padding: 6px 12px
171        CssPropertyWithConditions::simple(CssProperty::const_padding_top(LayoutPaddingTop::const_px(
172            6,
173        ))),
174        CssPropertyWithConditions::simple(CssProperty::const_padding_bottom(
175            LayoutPaddingBottom::const_px(6),
176        )),
177        CssPropertyWithConditions::simple(CssProperty::const_padding_left(
178            LayoutPaddingLeft::const_px(12),
179        )),
180        CssPropertyWithConditions::simple(CssProperty::const_padding_right(
181            LayoutPaddingRight::const_px(12),
182        )),
183        // top/bottom/right borders (the left border is added only for the first segment,
184        // so adjacent segments share a single 1px separator)
185        CssPropertyWithConditions::simple(CssProperty::const_border_top_width(
186            LayoutBorderTopWidth::const_px(1),
187        )),
188        CssPropertyWithConditions::simple(CssProperty::const_border_bottom_width(
189            LayoutBorderBottomWidth::const_px(1),
190        )),
191        CssPropertyWithConditions::simple(CssProperty::const_border_right_width(
192            LayoutBorderRightWidth::const_px(1),
193        )),
194        CssPropertyWithConditions::simple(CssProperty::const_border_top_style(StyleBorderTopStyle {
195            inner: BorderStyle::Solid,
196        })),
197        CssPropertyWithConditions::simple(CssProperty::const_border_bottom_style(
198            StyleBorderBottomStyle {
199                inner: BorderStyle::Solid,
200            },
201        )),
202        CssPropertyWithConditions::simple(CssProperty::const_border_right_style(
203            StyleBorderRightStyle {
204                inner: BorderStyle::Solid,
205            },
206        )),
207        CssPropertyWithConditions::simple(CssProperty::const_border_top_color(StyleBorderTopColor {
208            inner: SEG_BORDER_COLOR,
209        })),
210        CssPropertyWithConditions::simple(CssProperty::const_border_bottom_color(
211            StyleBorderBottomColor {
212                inner: SEG_BORDER_COLOR,
213            },
214        )),
215        CssPropertyWithConditions::simple(CssProperty::const_border_right_color(
216            StyleBorderRightColor {
217                inner: SEG_BORDER_COLOR,
218            },
219        )),
220        CssPropertyWithConditions::simple(CssProperty::const_cursor(StyleCursor::Pointer)),
221        CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(13))),
222        CssPropertyWithConditions::simple(CssProperty::const_text_align(StyleTextAlign::Center)),
223        CssPropertyWithConditions::simple(CssProperty::user_select(StyleUserSelect::None)),
224        CssPropertyWithConditions::simple(CssProperty::const_background_content(bg)),
225        CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor {
226            inner: text,
227        })),
228    ];
229
230    if is_first {
231        v.push(CssPropertyWithConditions::simple(
232            CssProperty::const_border_left_width(LayoutBorderLeftWidth::const_px(1)),
233        ));
234        v.push(CssPropertyWithConditions::simple(
235            CssProperty::const_border_left_style(StyleBorderLeftStyle {
236                inner: BorderStyle::Solid,
237            }),
238        ));
239        v.push(CssPropertyWithConditions::simple(
240            CssProperty::const_border_left_color(StyleBorderLeftColor {
241                inner: SEG_BORDER_COLOR,
242            }),
243        ));
244        v.push(CssPropertyWithConditions::simple(
245            CssProperty::const_border_top_left_radius(StyleBorderTopLeftRadius::const_px(SEG_RADIUS)),
246        ));
247        v.push(CssPropertyWithConditions::simple(
248            CssProperty::const_border_bottom_left_radius(StyleBorderBottomLeftRadius::const_px(
249                SEG_RADIUS,
250            )),
251        ));
252    }
253    if is_last {
254        v.push(CssPropertyWithConditions::simple(
255            CssProperty::const_border_top_right_radius(StyleBorderTopRightRadius::const_px(
256                SEG_RADIUS,
257            )),
258        ));
259        v.push(CssPropertyWithConditions::simple(
260            CssProperty::const_border_bottom_right_radius(StyleBorderBottomRightRadius::const_px(
261                SEG_RADIUS,
262            )),
263        ));
264    }
265
266    CssPropertyWithConditionsVec::from_vec(v)
267}
268
269impl Segmented {
270    /// Creates a segmented control from the given labels, with the first segment selected.
271    #[must_use] pub fn create(labels: StringVec) -> Self {
272        Self {
273            segmented_state: SegmentedStateWrapper {
274                inner: SegmentedState { selected_index: 0 },
275                ..Default::default()
276            },
277            labels,
278            container_style: CssPropertyWithConditionsVec::from_const_slice(
279                SEGMENTED_CONTAINER_STYLE,
280            ),
281        }
282    }
283
284    /// Sets the currently selected segment index.
285    #[inline]
286    pub const fn set_selected_index(&mut self, selected_index: usize) {
287        self.segmented_state.inner.selected_index = selected_index;
288    }
289
290    /// Builder-style setter for the selected segment index.
291    #[inline]
292    #[must_use] pub const fn with_selected_index(mut self, selected_index: usize) -> Self {
293        self.set_selected_index(selected_index);
294        self
295    }
296
297    #[inline]
298    #[must_use] pub fn swap_with_default(&mut self) -> Self {
299        let mut s = Self::create(StringVec::from_const_slice(&[]));
300        core::mem::swap(&mut s, self);
301        s
302    }
303
304    #[inline]
305    pub fn set_on_change<C: Into<SegmentedOnChangeCallback>>(
306        &mut self,
307        data: RefAny,
308        on_change: C,
309    ) {
310        self.segmented_state.on_change = Some(SegmentedOnChange {
311            callback: on_change.into(),
312            refany: data,
313        })
314        .into();
315    }
316
317    #[inline]
318    #[must_use] pub fn with_on_change<C: Into<SegmentedOnChangeCallback>>(
319        mut self,
320        data: RefAny,
321        on_change: C,
322    ) -> Self {
323        self.set_on_change(data, on_change);
324        self
325    }
326
327    #[must_use] pub fn dom(self) -> Dom {
328        use azul_core::{
329            callbacks::CoreCallback,
330            dom::{EventFilter, HoverEventFilter},
331            refany::OptionRefAny,
332        };
333
334        let selected = self.segmented_state.inner.selected_index;
335        let count = self.labels.as_ref().len();
336
337        // One shared RefAny across every segment's callback (RefAny::clone shares
338        // the underlying state — same pattern as tabs/map).
339        let state = RefAny::new(self.segmented_state);
340
341        let mut children: Vec<Dom> = Vec::with_capacity(count);
342        for (i, label) in self.labels.as_ref().iter().enumerate() {
343            let is_first = i == 0;
344            let is_last = i + 1 == count;
345            let seg_style = build_segment_style(i == selected, is_first, is_last);
346
347            children.push(
348                Dom::create_text(label.clone())
349                    .with_ids_and_classes(IdOrClassVec::from_const_slice(SEGMENT_ITEM_CLASS))
350                    .with_css_props(seg_style)
351                    .with_callbacks(
352                        vec![CoreCallbackData {
353                            event: EventFilter::Hover(HoverEventFilter::MouseUp),
354                            callback: CoreCallback {
355                                cb: on_segment_click as usize,
356                                ctx: OptionRefAny::None,
357                            },
358                            refany: state.clone(),
359                        }]
360                        .into(),
361                    )
362                    .with_tab_index(TabIndex::Auto),
363            );
364        }
365
366        Dom::create_div()
367            .with_ids_and_classes(IdOrClassVec::from_const_slice(SEGMENTED_CLASS))
368            .with_css_props(self.container_style)
369            .with_children(children.into())
370    }
371}
372
373impl Default for Segmented {
374    fn default() -> Self {
375        Self::create(StringVec::from_const_slice(&[]))
376    }
377}
378
379/// Click handler shared by all segments. Determines the clicked segment's index
380/// from its position among its siblings, updates the selection, invokes the user
381/// callback, and live-restyles every segment.
382extern "C" fn on_segment_click(mut data: RefAny, mut info: CallbackInfo) -> Update {
383    use azul_core::dom::DomNodeId;
384
385    let clicked = info.get_hit_node();
386    let Some(parent) = info.get_parent(clicked) else {
387        return Update::DoNothing;
388    };
389
390    // Collect the segment siblings in document order.
391    let mut segments: Vec<DomNodeId> = Vec::new();
392    let mut cur = info.get_first_child(parent);
393    while let Some(node) = cur {
394        segments.push(node);
395        cur = info.get_next_sibling(node);
396    }
397
398    let Some(selected) = segments.iter().position(|n| *n == clicked) else {
399        return Update::DoNothing;
400    };
401
402    let result = {
403        let Some(mut seg) = data.downcast_mut::<SegmentedStateWrapper>() else {
404            return Update::DoNothing;
405        };
406        seg.inner.selected_index = selected;
407        let inner = seg.inner;
408        let seg = &mut *seg;
409        match seg.on_change.as_mut() {
410            Some(SegmentedOnChange { callback, refany }) => (callback.cb)(refany.clone(), info, inner),
411            None => Update::DoNothing,
412        }
413    };
414
415    // Live-restyle: selected segment gets the accent fill + light text,
416    // the rest get the neutral fill + dark text.
417    for (i, node) in segments.iter().enumerate() {
418        if i == selected {
419            info.set_css_property(*node, CssProperty::const_background_content(SEG_SELECTED_BG));
420            info.set_css_property(
421                *node,
422                CssProperty::const_text_color(StyleTextColor {
423                    inner: SEG_SELECTED_TEXT,
424                }),
425            );
426        } else {
427            info.set_css_property(
428                *node,
429                CssProperty::const_background_content(SEG_UNSELECTED_BG),
430            );
431            info.set_css_property(
432                *node,
433                CssProperty::const_text_color(StyleTextColor {
434                    inner: SEG_UNSELECTED_TEXT,
435                }),
436            );
437        }
438    }
439
440    result
441}
442
443impl From<Segmented> for Dom {
444    fn from(s: Segmented) -> Self {
445        s.dom()
446    }
447}