1use std::vec::Vec;
25
26use azul_core::{
27 callbacks::{CoreCallbackData, Update},
28 dom::{Dom, IdOrClass, IdOrClass::Class, IdOrClassVec, TabIndex},
29 refany::RefAny,
30};
31use azul_css::dynamic_selector::{CssPropertyWithConditions, CssPropertyWithConditionsVec};
32use azul_css::{
33 props::{
34 basic::{color::ColorU, PixelValue, StyleFontSize},
35 layout::{LayoutDisplay, LayoutFlexDirection, LayoutAlignItems, LayoutFlexGrow, LayoutFlexBasis, LayoutWidth, LayoutJustifyContent, LayoutHeight, LayoutMinWidth, LayoutPaddingTop},
36 property::{CssProperty, LayoutFlexBasisValue, LayoutWidthValue},
37 style::{StyleBackgroundContent, StyleBackgroundContentVec, StyleCursor, StyleBorderTopLeftRadius, StyleBorderTopRightRadius, StyleBorderBottomLeftRadius, StyleBorderBottomRightRadius, StyleTextAlign, StyleUserSelect, StyleTextColor},
38 },
39 impl_option_inner, AzString, StringVec,
40};
41
42use crate::callbacks::CallbackInfo;
43
44static STEPPER_CLASS: &[IdOrClass] = &[Class(AzString::from_const_str("__azul-native-stepper"))];
45static STEPPER_STEP_CLASS: &[IdOrClass] =
46 &[Class(AzString::from_const_str("__azul-native-stepper-step"))];
47static STEPPER_ROW_CLASS: &[IdOrClass] =
48 &[Class(AzString::from_const_str("__azul-native-stepper-row"))];
49static STEPPER_CIRCLE_CLASS: &[IdOrClass] =
50 &[Class(AzString::from_const_str("__azul-native-stepper-circle"))];
51static STEPPER_CONNECTOR_CLASS: &[IdOrClass] =
52 &[Class(AzString::from_const_str("__azul-native-stepper-connector"))];
53static STEPPER_LABEL_CLASS: &[IdOrClass] =
54 &[Class(AzString::from_const_str("__azul-native-stepper-label"))];
55
56pub type StepperOnStepChangeCallbackType =
58 extern "C" fn(RefAny, CallbackInfo, StepperState) -> Update;
59impl_widget_callback!(
60 StepperOnStepChange,
61 OptionStepperOnStepChange,
62 StepperOnStepChangeCallback,
63 StepperOnStepChangeCallbackType
64);
65
66azul_core::impl_managed_callback! {
67 wrapper: StepperOnStepChangeCallback,
68 info_ty: CallbackInfo,
69 return_ty: Update,
70 default_ret: Update::DoNothing,
71 invoker_static: STEPPER_ON_STEP_CHANGE_INVOKER,
72 invoker_ty: AzStepperOnStepChangeCallbackInvoker,
73 thunk_fn: az_stepper_on_step_change_callback_thunk,
74 setter_fn: AzApp_setStepperOnStepChangeCallbackInvoker,
75 from_handle_fn: AzStepperOnStepChangeCallback_createFromHostHandle,
76 extra_args: [ state: StepperState ],
77}
78
79#[derive(Debug, Clone, PartialEq, Eq)]
81#[repr(C)]
82pub struct Stepper {
83 pub stepper_state: StepperStateWrapper,
84 pub labels: StringVec,
86 pub container_style: CssPropertyWithConditionsVec,
88}
89
90#[derive(Debug, Default, Clone, PartialEq, Eq)]
91#[repr(C)]
92pub struct StepperStateWrapper {
93 pub inner: StepperState,
95 pub on_step_change: OptionStepperOnStepChange,
97}
98
99#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
101#[repr(C)]
102pub struct StepperState {
103 pub current_step: usize,
105 pub total_steps: usize,
107}
108
109const ACCENT_COLOR: ColorU = ColorU { r: 13, g: 110, b: 253, a: 255 };
112const WHITE: ColorU = ColorU { r: 255, g: 255, b: 255, a: 255 };
114const MUTED_CIRCLE_COLOR: ColorU = ColorU { r: 233, g: 236, b: 239, a: 255 };
116const MUTED_TEXT_COLOR: ColorU = ColorU { r: 134, g: 142, b: 150, a: 255 };
118const DARK_TEXT_COLOR: ColorU = ColorU { r: 33, g: 37, b: 41, a: 255 };
120const CONNECTOR_MUTED_COLOR: ColorU = ColorU { r: 206, g: 212, b: 218, a: 255 };
122const TRANSPARENT_COLOR: ColorU = ColorU { r: 0, g: 0, b: 0, a: 0 };
124
125const ACCENT_BG_ITEMS: &[StyleBackgroundContent] = &[StyleBackgroundContent::Color(ACCENT_COLOR)];
126const ACCENT_BG: StyleBackgroundContentVec =
127 StyleBackgroundContentVec::from_const_slice(ACCENT_BG_ITEMS);
128const MUTED_CIRCLE_BG_ITEMS: &[StyleBackgroundContent] =
129 &[StyleBackgroundContent::Color(MUTED_CIRCLE_COLOR)];
130const MUTED_CIRCLE_BG: StyleBackgroundContentVec =
131 StyleBackgroundContentVec::from_const_slice(MUTED_CIRCLE_BG_ITEMS);
132const CONNECTOR_MUTED_BG_ITEMS: &[StyleBackgroundContent] =
133 &[StyleBackgroundContent::Color(CONNECTOR_MUTED_COLOR)];
134const CONNECTOR_MUTED_BG: StyleBackgroundContentVec =
135 StyleBackgroundContentVec::from_const_slice(CONNECTOR_MUTED_BG_ITEMS);
136const TRANSPARENT_BG_ITEMS: &[StyleBackgroundContent] =
137 &[StyleBackgroundContent::Color(TRANSPARENT_COLOR)];
138const TRANSPARENT_BG: StyleBackgroundContentVec =
139 StyleBackgroundContentVec::from_const_slice(TRANSPARENT_BG_ITEMS);
140
141const CIRCLE_SIZE: isize = 28;
142const CIRCLE_RADIUS: isize = 14;
143const CONNECTOR_HEIGHT: isize = 2;
144
145#[derive(Copy, Clone)]
147enum ConnFill {
148 Accent,
150 Muted,
152 Hidden,
154}
155
156impl ConnFill {
157 const fn bg(self) -> StyleBackgroundContentVec {
158 match self {
159 Self::Accent => ACCENT_BG,
160 Self::Muted => CONNECTOR_MUTED_BG,
161 Self::Hidden => TRANSPARENT_BG,
162 }
163 }
164}
165
166static STEPPER_CONTAINER_STYLE: &[CssPropertyWithConditions] = &[
168 CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Flex)),
169 CssPropertyWithConditions::simple(CssProperty::const_flex_direction(LayoutFlexDirection::Row)),
170 CssPropertyWithConditions::simple(CssProperty::const_align_items(LayoutAlignItems::Start)),
171 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
172];
173
174static STEPPER_STEP_STYLE: &[CssPropertyWithConditions] = &[
177 CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Flex)),
178 CssPropertyWithConditions::simple(CssProperty::const_flex_direction(LayoutFlexDirection::Column)),
179 CssPropertyWithConditions::simple(CssProperty::const_align_items(LayoutAlignItems::Center)),
180 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(1))),
181 CssPropertyWithConditions::simple(CssProperty::FlexBasis(LayoutFlexBasisValue::Exact(
182 LayoutFlexBasis::Exact(PixelValue::const_px(0)),
183 ))),
184 CssPropertyWithConditions::simple(CssProperty::const_cursor(StyleCursor::Pointer)),
185];
186
187fn row_style() -> CssPropertyWithConditionsVec {
190 CssPropertyWithConditionsVec::from_vec(vec![
191 CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Flex)),
192 CssPropertyWithConditions::simple(CssProperty::const_flex_direction(
193 LayoutFlexDirection::Row,
194 )),
195 CssPropertyWithConditions::simple(CssProperty::const_align_items(LayoutAlignItems::Center)),
196 CssPropertyWithConditions::simple(CssProperty::Width(LayoutWidthValue::Exact(
198 LayoutWidth::Px(PixelValue::percent(100.0)),
199 ))),
200 ])
201}
202
203fn circle_style(reached: bool) -> CssPropertyWithConditionsVec {
206 let (bg, text) = if reached {
207 (ACCENT_BG, WHITE)
208 } else {
209 (MUTED_CIRCLE_BG, MUTED_TEXT_COLOR)
210 };
211 CssPropertyWithConditionsVec::from_vec(vec![
212 CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Flex)),
213 CssPropertyWithConditions::simple(CssProperty::const_flex_direction(
214 LayoutFlexDirection::Row,
215 )),
216 CssPropertyWithConditions::simple(CssProperty::const_justify_content(
217 LayoutJustifyContent::Center,
218 )),
219 CssPropertyWithConditions::simple(CssProperty::const_align_items(LayoutAlignItems::Center)),
220 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
221 CssPropertyWithConditions::simple(CssProperty::const_width(LayoutWidth::const_px(
222 CIRCLE_SIZE,
223 ))),
224 CssPropertyWithConditions::simple(CssProperty::const_height(LayoutHeight::const_px(
225 CIRCLE_SIZE,
226 ))),
227 CssPropertyWithConditions::simple(CssProperty::const_min_width(LayoutMinWidth::const_px(
228 CIRCLE_SIZE,
229 ))),
230 CssPropertyWithConditions::simple(CssProperty::const_border_top_left_radius(
231 StyleBorderTopLeftRadius::const_px(CIRCLE_RADIUS),
232 )),
233 CssPropertyWithConditions::simple(CssProperty::const_border_top_right_radius(
234 StyleBorderTopRightRadius::const_px(CIRCLE_RADIUS),
235 )),
236 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_left_radius(
237 StyleBorderBottomLeftRadius::const_px(CIRCLE_RADIUS),
238 )),
239 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_right_radius(
240 StyleBorderBottomRightRadius::const_px(CIRCLE_RADIUS),
241 )),
242 CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(13))),
243 CssPropertyWithConditions::simple(CssProperty::const_text_align(StyleTextAlign::Center)),
244 CssPropertyWithConditions::simple(CssProperty::user_select(StyleUserSelect::None)),
245 CssPropertyWithConditions::simple(CssProperty::const_cursor(StyleCursor::Pointer)),
246 CssPropertyWithConditions::simple(CssProperty::const_background_content(bg)),
247 CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor {
248 inner: text,
249 })),
250 ])
251}
252
253fn connector_style(fill: ConnFill) -> CssPropertyWithConditionsVec {
255 CssPropertyWithConditionsVec::from_vec(vec![
256 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(1))),
257 CssPropertyWithConditions::simple(CssProperty::const_height(LayoutHeight::const_px(
258 CONNECTOR_HEIGHT,
259 ))),
260 CssPropertyWithConditions::simple(CssProperty::const_background_content(fill.bg())),
261 ])
262}
263
264fn label_style(reached: bool) -> CssPropertyWithConditionsVec {
266 let text = if reached { DARK_TEXT_COLOR } else { MUTED_TEXT_COLOR };
267 CssPropertyWithConditionsVec::from_vec(vec![
268 CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(12))),
269 CssPropertyWithConditions::simple(CssProperty::const_text_align(StyleTextAlign::Center)),
270 CssPropertyWithConditions::simple(CssProperty::user_select(StyleUserSelect::None)),
271 CssPropertyWithConditions::simple(CssProperty::const_cursor(StyleCursor::Pointer)),
272 CssPropertyWithConditions::simple(CssProperty::const_padding_top(LayoutPaddingTop::const_px(
273 6,
274 ))),
275 CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor {
276 inner: text,
277 })),
278 ])
279}
280
281const fn conn_left_fill(i: usize, current: usize) -> ConnFill {
283 if i == 0 {
284 ConnFill::Hidden
285 } else if i <= current {
286 ConnFill::Accent
287 } else {
288 ConnFill::Muted
289 }
290}
291
292const fn conn_right_fill(i: usize, last: usize, current: usize) -> ConnFill {
294 if i == last {
295 ConnFill::Hidden
296 } else if i < current {
297 ConnFill::Accent
298 } else {
299 ConnFill::Muted
300 }
301}
302
303impl Stepper {
304 #[must_use] pub fn create(labels: StringVec) -> Self {
306 let total_steps = labels.as_ref().len();
307 Self {
308 stepper_state: StepperStateWrapper {
309 inner: StepperState {
310 current_step: 0,
311 total_steps,
312 },
313 ..Default::default()
314 },
315 labels,
316 container_style: CssPropertyWithConditionsVec::from_const_slice(
317 STEPPER_CONTAINER_STYLE,
318 ),
319 }
320 }
321
322 #[inline]
324 pub fn set_current_step(&mut self, current_step: usize) {
325 let total = self.stepper_state.inner.total_steps;
326 self.stepper_state.inner.current_step = if total == 0 {
327 0
328 } else {
329 current_step.min(total - 1)
330 };
331 }
332
333 #[inline]
335 #[must_use] pub fn with_current_step(mut self, current_step: usize) -> Self {
336 self.set_current_step(current_step);
337 self
338 }
339
340 #[inline]
341 #[must_use] pub fn swap_with_default(&mut self) -> Self {
342 let mut s = Self::create(StringVec::from_const_slice(&[]));
343 core::mem::swap(&mut s, self);
344 s
345 }
346
347 #[inline]
348 pub fn set_on_step_change<C: Into<StepperOnStepChangeCallback>>(
349 &mut self,
350 data: RefAny,
351 on_step_change: C,
352 ) {
353 self.stepper_state.on_step_change = Some(StepperOnStepChange {
354 callback: on_step_change.into(),
355 refany: data,
356 })
357 .into();
358 }
359
360 #[inline]
361 #[must_use] pub fn with_on_step_change<C: Into<StepperOnStepChangeCallback>>(
362 mut self,
363 data: RefAny,
364 on_step_change: C,
365 ) -> Self {
366 self.set_on_step_change(data, on_step_change);
367 self
368 }
369
370 #[must_use] pub fn dom(self) -> Dom {
371 use azul_core::{
372 callbacks::CoreCallback,
373 dom::{EventFilter, HoverEventFilter},
374 refany::OptionRefAny,
375 };
376
377 let current = self.stepper_state.inner.current_step;
378 let count = self.labels.as_ref().len();
379 let last = count.saturating_sub(1);
380
381 let state = RefAny::new(self.stepper_state);
384
385 let mut children: Vec<Dom> = Vec::with_capacity(count);
386 for (i, label) in self.labels.as_ref().iter().enumerate() {
387 let reached = i <= current;
388
389 let row = Dom::create_div()
391 .with_ids_and_classes(IdOrClassVec::from_const_slice(STEPPER_ROW_CLASS))
392 .with_css_props(row_style())
393 .with_children(
394 vec![
395 Dom::create_div()
396 .with_ids_and_classes(IdOrClassVec::from_const_slice(
397 STEPPER_CONNECTOR_CLASS,
398 ))
399 .with_css_props(connector_style(conn_left_fill(i, current))),
400 Dom::create_text(AzString::from(format!("{}", i + 1).as_str()))
401 .with_ids_and_classes(IdOrClassVec::from_const_slice(
402 STEPPER_CIRCLE_CLASS,
403 ))
404 .with_css_props(circle_style(reached)),
405 Dom::create_div()
406 .with_ids_and_classes(IdOrClassVec::from_const_slice(
407 STEPPER_CONNECTOR_CLASS,
408 ))
409 .with_css_props(connector_style(conn_right_fill(i, last, current))),
410 ]
411 .into(),
412 );
413
414 let cell = Dom::create_div()
415 .with_ids_and_classes(IdOrClassVec::from_const_slice(STEPPER_STEP_CLASS))
416 .with_css_props(CssPropertyWithConditionsVec::from_const_slice(
417 STEPPER_STEP_STYLE,
418 ))
419 .with_callbacks(
420 vec![CoreCallbackData {
421 event: EventFilter::Hover(HoverEventFilter::MouseUp),
422 callback: CoreCallback {
423 cb: on_step_click as usize,
424 ctx: OptionRefAny::None,
425 },
426 refany: state.clone(),
427 }]
428 .into(),
429 )
430 .with_tab_index(TabIndex::Auto)
431 .with_children(
432 vec![
433 row,
434 Dom::create_text(label.clone())
435 .with_ids_and_classes(IdOrClassVec::from_const_slice(
436 STEPPER_LABEL_CLASS,
437 ))
438 .with_css_props(label_style(reached)),
439 ]
440 .into(),
441 );
442
443 children.push(cell);
444 }
445
446 Dom::create_div()
447 .with_ids_and_classes(IdOrClassVec::from_const_slice(STEPPER_CLASS))
448 .with_css_props(self.container_style)
449 .with_children(children.into())
450 }
451}
452
453impl Default for Stepper {
454 fn default() -> Self {
455 Self::create(StringVec::from_const_slice(&[]))
456 }
457}
458
459extern "C" fn on_step_click(mut data: RefAny, mut info: CallbackInfo) -> Update {
464 use azul_core::dom::DomNodeId;
465
466 let clicked = info.get_hit_node();
467 let Some(parent) = info.get_parent(clicked) else {
468 return Update::DoNothing;
469 };
470
471 let mut cells: Vec<DomNodeId> = Vec::new();
473 let mut cur = info.get_first_child(parent);
474 while let Some(node) = cur {
475 cells.push(node);
476 cur = info.get_next_sibling(node);
477 }
478 let count = cells.len();
479 if count == 0 {
480 return Update::DoNothing;
481 }
482 let last = count - 1;
483
484 let Some(clicked_idx) = cells.iter().position(|n| *n == clicked) else {
485 return Update::DoNothing;
486 };
487
488 let current = {
489 let Some(st) = data.downcast_ref::<StepperStateWrapper>() else {
490 return Update::DoNothing;
491 };
492 st.inner.current_step
493 };
494 if clicked_idx == current {
495 return Update::DoNothing;
497 }
498
499 let result = {
500 let Some(mut st) = data.downcast_mut::<StepperStateWrapper>() else {
501 return Update::DoNothing;
502 };
503 st.inner.current_step = clicked_idx;
504 let inner = st.inner;
505 let st = &mut *st;
506 match st.on_step_change.as_mut() {
507 Some(StepperOnStepChange { callback, refany }) => {
508 (callback.cb)(refany.clone(), info, inner)
509 }
510 None => Update::DoNothing,
511 }
512 };
513
514 for (i, cell) in cells.iter().enumerate() {
517 let reached = i <= clicked_idx;
518
519 let Some(row) = info.get_first_child(*cell) else {
520 continue;
521 };
522 let conn_left = info.get_first_child(row);
523 let circle = conn_left.and_then(|cl| info.get_next_sibling(cl));
524 let conn_right = circle.and_then(|c| info.get_next_sibling(c));
525 let label = info.get_next_sibling(row);
526
527 if let Some(circle) = circle {
528 let (bg, text) = if reached {
529 (ACCENT_BG, WHITE)
530 } else {
531 (MUTED_CIRCLE_BG, MUTED_TEXT_COLOR)
532 };
533 info.set_css_property(circle, CssProperty::const_background_content(bg));
534 info.set_css_property(
535 circle,
536 CssProperty::const_text_color(StyleTextColor { inner: text }),
537 );
538 }
539 if let Some(cl) = conn_left {
540 info.set_css_property(
541 cl,
542 CssProperty::const_background_content(conn_left_fill(i, clicked_idx).bg()),
543 );
544 }
545 if let Some(cr) = conn_right {
546 info.set_css_property(
547 cr,
548 CssProperty::const_background_content(conn_right_fill(i, last, clicked_idx).bg()),
549 );
550 }
551 if let Some(label) = label {
552 let text = if reached { DARK_TEXT_COLOR } else { MUTED_TEXT_COLOR };
553 info.set_css_property(
554 label,
555 CssProperty::const_text_color(StyleTextColor { inner: text }),
556 );
557 }
558 }
559
560 result
561}
562
563impl From<Stepper> for Dom {
564 fn from(s: Stepper) -> Self {
565 s.dom()
566 }
567}