1use 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
38pub 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#[derive(Debug, Clone, PartialEq, Eq)]
63#[repr(C)]
64pub struct Segmented {
65 pub segmented_state: SegmentedStateWrapper,
66 pub labels: StringVec,
68 pub container_style: CssPropertyWithConditionsVec,
70}
71
72#[derive(Debug, Default, Clone, PartialEq, Eq)]
73#[repr(C)]
74pub struct SegmentedStateWrapper {
75 pub inner: SegmentedState,
77 pub on_change: OptionSegmentedOnChange,
79}
80
81#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
83#[repr(C)]
84pub struct SegmentedState {
85 pub selected_index: usize,
87}
88
89const SEG_BORDER_COLOR: ColorU = ColorU {
92 r: 206,
93 g: 212,
94 b: 218,
95 a: 255,
96};
97const SEG_SELECTED_BG_COLOR: ColorU = ColorU {
99 r: 13,
100 g: 110,
101 b: 253,
102 a: 255,
103};
104const SEG_UNSELECTED_BG_COLOR: ColorU = ColorU {
106 r: 255,
107 g: 255,
108 b: 255,
109 a: 255,
110};
111const SEG_SELECTED_TEXT: ColorU = ColorU {
113 r: 255,
114 g: 255,
115 b: 255,
116 a: 255,
117};
118const 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
137static 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#[allow(clippy::too_many_lines)] fn 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 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 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 #[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 #[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 #[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 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
379extern "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 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 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}