1use azul_core::{
24 callbacks::{CoreCallback, CoreCallbackData, Update},
25 dom::{Dom, IdOrClass, IdOrClass::Class, IdOrClassVec, TabIndex},
26 refany::{OptionRefAny, RefAny},
27};
28use azul_css::dynamic_selector::CssPropertyWithConditions;
29use azul_css::dynamic_selector::CssPropertyWithConditionsVec;
30use azul_css::{
31 props::{
32 basic::{color::ColorU, StyleFontSize},
33 layout::{LayoutDisplay, LayoutFlexDirection, LayoutAlignItems, LayoutAlignSelf, LayoutFlexGrow, LayoutPaddingTop, LayoutPaddingBottom, LayoutPaddingLeft, LayoutPaddingRight, LayoutWidth, LayoutMarginLeft},
34 property::{CssProperty, *},
35 style::{StyleBackgroundContent, StyleBackgroundContentVec, LayoutBorderTopWidth, LayoutBorderBottomWidth, LayoutBorderLeftWidth, LayoutBorderRightWidth, StyleBorderTopStyle, BorderStyle, StyleBorderBottomStyle, StyleBorderLeftStyle, StyleBorderRightStyle, StyleBorderTopColor, StyleBorderBottomColor, StyleBorderLeftColor, StyleBorderRightColor, StyleBorderTopLeftRadius, StyleBorderTopRightRadius, StyleBorderBottomLeftRadius, StyleBorderBottomRightRadius, StyleTextAlign, StyleCursor, StyleUserSelect, StyleTextColor},
36 },
37 impl_option_inner, AzString,
38};
39
40use crate::callbacks::{Callback, CallbackInfo};
41
42static TIME_PICKER_CLASS: &[IdOrClass] =
44 &[Class(AzString::from_const_str("__azul-native-time-picker"))];
45static SPINNER_CLASS: &[IdOrClass] =
46 &[Class(AzString::from_const_str("__azul-native-time-picker-spinner"))];
47static DISPLAY_CLASS: &[IdOrClass] =
48 &[Class(AzString::from_const_str("__azul-native-time-picker-display"))];
49static ARROW_CLASS: &[IdOrClass] =
50 &[Class(AzString::from_const_str("__azul-native-time-picker-arrow"))];
51static SEPARATOR_CLASS: &[IdOrClass] =
52 &[Class(AzString::from_const_str("__azul-native-time-picker-separator"))];
53static AMPM_CLASS: &[IdOrClass] =
54 &[Class(AzString::from_const_str("__azul-native-time-picker-ampm"))];
55
56const UP_ARROW: AzString = AzString::from_const_str("\u{25B2}"); const DOWN_ARROW: AzString = AzString::from_const_str("\u{25BC}"); const SEPARATOR_TEXT: AzString = AzString::from_const_str(":");
59
60pub type TimePickerOnChangeCallbackType =
62 extern "C" fn(RefAny, CallbackInfo, TimePickerState) -> Update;
63impl_widget_callback!(
64 TimePickerOnChange,
65 OptionTimePickerOnChange,
66 TimePickerOnChangeCallback,
67 TimePickerOnChangeCallbackType
68);
69
70azul_core::impl_managed_callback! {
71 wrapper: TimePickerOnChangeCallback,
72 info_ty: CallbackInfo,
73 return_ty: Update,
74 default_ret: Update::DoNothing,
75 invoker_static: TIME_PICKER_ON_CHANGE_INVOKER,
76 invoker_ty: AzTimePickerOnChangeCallbackInvoker,
77 thunk_fn: az_time_picker_on_change_callback_thunk,
78 setter_fn: AzApp_setTimePickerOnChangeCallbackInvoker,
79 from_handle_fn: AzTimePickerOnChangeCallback_createFromHostHandle,
80 extra_args: [ state: TimePickerState ],
81}
82
83#[derive(Debug, Clone, PartialEq, Eq)]
86#[repr(C)]
87pub struct TimePicker {
88 pub state: TimePickerStateWrapper,
89 pub container_style: CssPropertyWithConditionsVec,
91}
92
93#[derive(Debug, Default, Clone, PartialEq, Eq)]
95#[repr(C)]
96pub struct TimePickerStateWrapper {
97 pub inner: TimePickerState,
98 pub on_change: OptionTimePickerOnChange,
99}
100
101#[derive(Debug, Copy, Clone, PartialEq, Eq)]
103#[repr(C)]
104pub struct TimePickerState {
105 pub hour: u32,
107 pub minute: u32,
109 pub is_pm: bool,
111 pub is_24h: bool,
113}
114
115impl Default for TimePickerState {
116 fn default() -> Self {
117 Self {
118 hour: 0,
119 minute: 0,
120 is_pm: false,
121 is_24h: true,
122 }
123 }
124}
125
126impl TimePickerState {
127 #[must_use] pub const fn canonical_hour(&self) -> u32 {
130 if self.is_24h {
131 self.hour
132 } else {
133 let h12 = self.hour % 12; h12 + if self.is_pm { 12 } else { 0 }
135 }
136 }
137
138 #[inline]
139 const fn hour_bounds(&self) -> (i64, i64) {
140 if self.is_24h {
141 (0, 23)
142 } else {
143 (1, 12)
144 }
145 }
146}
147
148const BORDER_COLOR: ColorU = ColorU { r: 206, g: 212, b: 218, a: 255 };
150const ARROW_COLOR: ColorU = ColorU { r: 73, g: 80, b: 87, a: 255 };
151const TEXT_COLOR: ColorU = ColorU { r: 33, g: 37, b: 41, a: 255 };
152const ACCENT_BG: ColorU = ColorU { r: 13, g: 110, b: 253, a: 255 };
153const WHITE: ColorU = ColorU { r: 255, g: 255, b: 255, a: 255 };
154
155const ACCENT_BG_ITEMS: &[StyleBackgroundContent] = &[StyleBackgroundContent::Color(ACCENT_BG)];
156const ACCENT_BG_VEC: StyleBackgroundContentVec =
157 StyleBackgroundContentVec::from_const_slice(ACCENT_BG_ITEMS);
158
159static CONTAINER_STYLE: &[CssPropertyWithConditions] = &[
161 CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Flex)),
162 CssPropertyWithConditions::simple(CssProperty::const_flex_direction(LayoutFlexDirection::Row)),
163 CssPropertyWithConditions::simple(CssProperty::const_align_items(LayoutAlignItems::Center)),
164 CssPropertyWithConditions::simple(CssProperty::align_self(LayoutAlignSelf::Start)),
165 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
166 CssPropertyWithConditions::simple(CssProperty::const_padding_top(LayoutPaddingTop::const_px(4))),
167 CssPropertyWithConditions::simple(CssProperty::const_padding_bottom(
168 LayoutPaddingBottom::const_px(4),
169 )),
170 CssPropertyWithConditions::simple(CssProperty::const_padding_left(LayoutPaddingLeft::const_px(
171 6,
172 ))),
173 CssPropertyWithConditions::simple(CssProperty::const_padding_right(
174 LayoutPaddingRight::const_px(6),
175 )),
176 CssPropertyWithConditions::simple(CssProperty::const_border_top_width(
177 LayoutBorderTopWidth::const_px(1),
178 )),
179 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_width(
180 LayoutBorderBottomWidth::const_px(1),
181 )),
182 CssPropertyWithConditions::simple(CssProperty::const_border_left_width(
183 LayoutBorderLeftWidth::const_px(1),
184 )),
185 CssPropertyWithConditions::simple(CssProperty::const_border_right_width(
186 LayoutBorderRightWidth::const_px(1),
187 )),
188 CssPropertyWithConditions::simple(CssProperty::const_border_top_style(StyleBorderTopStyle {
189 inner: BorderStyle::Solid,
190 })),
191 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_style(
192 StyleBorderBottomStyle {
193 inner: BorderStyle::Solid,
194 },
195 )),
196 CssPropertyWithConditions::simple(CssProperty::const_border_left_style(StyleBorderLeftStyle {
197 inner: BorderStyle::Solid,
198 })),
199 CssPropertyWithConditions::simple(CssProperty::const_border_right_style(
200 StyleBorderRightStyle {
201 inner: BorderStyle::Solid,
202 },
203 )),
204 CssPropertyWithConditions::simple(CssProperty::const_border_top_color(StyleBorderTopColor {
205 inner: BORDER_COLOR,
206 })),
207 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_color(
208 StyleBorderBottomColor { inner: BORDER_COLOR },
209 )),
210 CssPropertyWithConditions::simple(CssProperty::const_border_left_color(StyleBorderLeftColor {
211 inner: BORDER_COLOR,
212 })),
213 CssPropertyWithConditions::simple(CssProperty::const_border_right_color(
214 StyleBorderRightColor { inner: BORDER_COLOR },
215 )),
216 CssPropertyWithConditions::simple(CssProperty::const_border_top_left_radius(
217 StyleBorderTopLeftRadius::const_px(6),
218 )),
219 CssPropertyWithConditions::simple(CssProperty::const_border_top_right_radius(
220 StyleBorderTopRightRadius::const_px(6),
221 )),
222 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_left_radius(
223 StyleBorderBottomLeftRadius::const_px(6),
224 )),
225 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_right_radius(
226 StyleBorderBottomRightRadius::const_px(6),
227 )),
228];
229
230static SPINNER_STYLE: &[CssPropertyWithConditions] = &[
232 CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Flex)),
233 CssPropertyWithConditions::simple(CssProperty::const_flex_direction(LayoutFlexDirection::Column)),
234 CssPropertyWithConditions::simple(CssProperty::const_align_items(LayoutAlignItems::Center)),
235 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
236 CssPropertyWithConditions::simple(CssProperty::const_width(LayoutWidth::const_px(40))),
237];
238
239static ARROW_STYLE: &[CssPropertyWithConditions] = &[
241 CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(11))),
242 CssPropertyWithConditions::simple(CssProperty::const_text_align(StyleTextAlign::Center)),
243 CssPropertyWithConditions::simple(CssProperty::const_cursor(StyleCursor::Pointer)),
244 CssPropertyWithConditions::simple(CssProperty::user_select(StyleUserSelect::None)),
245 CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor {
246 inner: ARROW_COLOR,
247 })),
248 CssPropertyWithConditions::simple(CssProperty::const_padding_top(LayoutPaddingTop::const_px(2))),
249 CssPropertyWithConditions::simple(CssProperty::const_padding_bottom(
250 LayoutPaddingBottom::const_px(2),
251 )),
252];
253
254static DISPLAY_STYLE: &[CssPropertyWithConditions] = &[
256 CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(18))),
257 CssPropertyWithConditions::simple(CssProperty::const_text_align(StyleTextAlign::Center)),
258 CssPropertyWithConditions::simple(CssProperty::user_select(StyleUserSelect::None)),
259 CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor {
260 inner: TEXT_COLOR,
261 })),
262 CssPropertyWithConditions::simple(CssProperty::const_padding_top(LayoutPaddingTop::const_px(2))),
263 CssPropertyWithConditions::simple(CssProperty::const_padding_bottom(
264 LayoutPaddingBottom::const_px(2),
265 )),
266];
267
268static SEPARATOR_STYLE: &[CssPropertyWithConditions] = &[
270 CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(18))),
271 CssPropertyWithConditions::simple(CssProperty::user_select(StyleUserSelect::None)),
272 CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor {
273 inner: TEXT_COLOR,
274 })),
275 CssPropertyWithConditions::simple(CssProperty::const_padding_left(LayoutPaddingLeft::const_px(
276 2,
277 ))),
278 CssPropertyWithConditions::simple(CssProperty::const_padding_right(
279 LayoutPaddingRight::const_px(2),
280 )),
281];
282
283static AMPM_STYLE: &[CssPropertyWithConditions] = &[
285 CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(13))),
286 CssPropertyWithConditions::simple(CssProperty::const_text_align(StyleTextAlign::Center)),
287 CssPropertyWithConditions::simple(CssProperty::const_cursor(StyleCursor::Pointer)),
288 CssPropertyWithConditions::simple(CssProperty::user_select(StyleUserSelect::None)),
289 CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor { inner: WHITE })),
290 CssPropertyWithConditions::simple(CssProperty::const_background_content(ACCENT_BG_VEC)),
291 CssPropertyWithConditions::simple(CssProperty::const_margin_left(LayoutMarginLeft::const_px(8))),
292 CssPropertyWithConditions::simple(CssProperty::const_padding_top(LayoutPaddingTop::const_px(4))),
293 CssPropertyWithConditions::simple(CssProperty::const_padding_bottom(
294 LayoutPaddingBottom::const_px(4),
295 )),
296 CssPropertyWithConditions::simple(CssProperty::const_padding_left(LayoutPaddingLeft::const_px(
297 8,
298 ))),
299 CssPropertyWithConditions::simple(CssProperty::const_padding_right(
300 LayoutPaddingRight::const_px(8),
301 )),
302 CssPropertyWithConditions::simple(CssProperty::const_border_top_left_radius(
303 StyleBorderTopLeftRadius::const_px(4),
304 )),
305 CssPropertyWithConditions::simple(CssProperty::const_border_top_right_radius(
306 StyleBorderTopRightRadius::const_px(4),
307 )),
308 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_left_radius(
309 StyleBorderBottomLeftRadius::const_px(4),
310 )),
311 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_right_radius(
312 StyleBorderBottomRightRadius::const_px(4),
313 )),
314];
315
316impl TimePicker {
317 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] #[must_use] pub fn create(hour: u32, minute: u32) -> Self {
321 let mut inner = TimePickerState::default();
322 let (lo, hi) = inner.hour_bounds();
323 inner.hour = i64::from(hour).clamp(lo, hi) as u32;
324 inner.minute = i64::from(minute).clamp(0, 59) as u32;
325 Self {
326 state: TimePickerStateWrapper {
327 inner,
328 on_change: None.into(),
329 },
330 container_style: CssPropertyWithConditionsVec::from_const_slice(CONTAINER_STYLE),
331 }
332 }
333
334 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] pub fn set_24h(&mut self, is_24h: bool) {
338 self.state.inner.is_24h = is_24h;
339 let (lo, hi) = self.state.inner.hour_bounds();
340 self.state.inner.hour = i64::from(self.state.inner.hour).clamp(lo, hi) as u32;
341 }
342
343 #[must_use] pub fn with_24h(mut self, is_24h: bool) -> Self {
345 self.set_24h(is_24h);
346 self
347 }
348
349 pub const fn set_pm(&mut self, is_pm: bool) {
351 self.state.inner.is_pm = is_pm;
352 }
353
354 #[must_use] pub const fn with_pm(mut self, is_pm: bool) -> Self {
356 self.set_pm(is_pm);
357 self
358 }
359
360 pub fn set_on_change<C: Into<TimePickerOnChangeCallback>>(&mut self, data: RefAny, callback: C) {
362 self.state.on_change = Some(TimePickerOnChange {
363 callback: callback.into(),
364 refany: data,
365 })
366 .into();
367 }
368
369 #[must_use] pub fn with_on_change<C: Into<TimePickerOnChangeCallback>>(
371 mut self,
372 data: RefAny,
373 callback: C,
374 ) -> Self {
375 self.set_on_change(data, callback);
376 self
377 }
378
379 #[must_use] pub fn swap_with_default(&mut self) -> Self {
381 let mut s = Self::create(0, 0);
382 core::mem::swap(&mut s, self);
383 s
384 }
385
386 #[must_use] pub fn dom(self) -> Dom {
387 let inner = self.state.inner;
388 let is_24h = inner.is_24h;
389 let hour_text = AzString::from(format!("{}", inner.hour));
390 let minute_text = AzString::from(format!("{:02}", inner.minute));
391 let container_style = self.container_style.clone();
392
393 let state = RefAny::new(self.state);
394
395 let mut children = alloc::vec![
396 build_spinner(
397 hour_text,
398 state.clone(),
399 on_hour_up as usize,
400 on_hour_down as usize,
401 ),
402 Dom::create_text(SEPARATOR_TEXT)
403 .with_ids_and_classes(IdOrClassVec::from_const_slice(SEPARATOR_CLASS))
404 .with_css_props(CssPropertyWithConditionsVec::from_const_slice(SEPARATOR_STYLE)),
405 build_spinner(
406 minute_text,
407 state.clone(),
408 on_minute_up as usize,
409 on_minute_down as usize,
410 ),
411 ];
412
413 if !is_24h {
414 let ampm_text = if inner.is_pm {
415 AzString::from_const_str("PM")
416 } else {
417 AzString::from_const_str("AM")
418 };
419 children.push(
420 Dom::create_text(ampm_text)
421 .with_ids_and_classes(IdOrClassVec::from_const_slice(AMPM_CLASS))
422 .with_css_props(CssPropertyWithConditionsVec::from_const_slice(AMPM_STYLE))
423 .with_callbacks(
424 alloc::vec![CoreCallbackData {
425 event: azul_core::dom::EventFilter::Hover(
426 azul_core::dom::HoverEventFilter::MouseUp,
427 ),
428 callback: CoreCallback {
429 cb: on_ampm_toggle as usize,
430 ctx: OptionRefAny::None,
431 },
432 refany: state,
433 }]
434 .into(),
435 )
436 .with_tab_index(TabIndex::Auto),
437 );
438 }
439
440 Dom::create_div()
441 .with_ids_and_classes(IdOrClassVec::from_const_slice(TIME_PICKER_CLASS))
442 .with_css_props(container_style)
443 .with_children(children.into())
444 }
445}
446
447impl Default for TimePicker {
448 fn default() -> Self {
449 Self::create(0, 0)
450 }
451}
452
453fn build_spinner(value: AzString, state: RefAny, up_cb: usize, down_cb: usize) -> Dom {
457 use azul_core::dom::{EventFilter, HoverEventFilter};
458
459 let arrow_cell = |arrow: AzString, cb: usize, refany: RefAny| -> Dom {
460 Dom::create_text(arrow)
461 .with_ids_and_classes(IdOrClassVec::from_const_slice(ARROW_CLASS))
462 .with_css_props(CssPropertyWithConditionsVec::from_const_slice(ARROW_STYLE))
463 .with_callbacks(
464 alloc::vec![CoreCallbackData {
465 event: EventFilter::Hover(HoverEventFilter::MouseUp),
466 callback: CoreCallback {
467 cb,
468 ctx: OptionRefAny::None,
469 },
470 refany,
471 }]
472 .into(),
473 )
474 .with_tab_index(TabIndex::Auto)
475 };
476
477 Dom::create_div()
478 .with_ids_and_classes(IdOrClassVec::from_const_slice(SPINNER_CLASS))
479 .with_css_props(CssPropertyWithConditionsVec::from_const_slice(SPINNER_STYLE))
480 .with_children(
481 alloc::vec![
482 arrow_cell(UP_ARROW, up_cb, state.clone()),
483 Dom::create_text(value)
484 .with_ids_and_classes(IdOrClassVec::from_const_slice(DISPLAY_CLASS))
485 .with_css_props(CssPropertyWithConditionsVec::from_const_slice(DISPLAY_STYLE)),
486 arrow_cell(DOWN_ARROW, down_cb, state),
487 ]
488 .into(),
489 )
490}
491
492#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] fn adjust_spinner(mut data: RefAny, mut info: CallbackInfo, is_hour: bool, delta: i64) -> Update {
497 let hit = info.get_hit_node();
500 let Some(parent) = info.get_parent(hit) else {
501 return Update::DoNothing;
502 };
503 let Some(up) = info.get_first_child(parent) else {
504 return Update::DoNothing;
505 };
506 let Some(display) = info.get_next_sibling(up) else {
507 return Update::DoNothing;
508 };
509
510 let (update, display_text) = {
511 let Some(mut w) = data.downcast_mut::<TimePickerStateWrapper>() else {
512 return Update::DoNothing;
513 };
514
515 let display_text = if is_hour {
516 let (lo, hi) = w.inner.hour_bounds();
517 w.inner.hour = (i64::from(w.inner.hour) + delta).clamp(lo, hi) as u32;
518 AzString::from(format!("{}", w.inner.hour))
519 } else {
520 w.inner.minute = (i64::from(w.inner.minute) + delta).clamp(0, 59) as u32;
522 AzString::from(format!("{:02}", w.inner.minute))
523 };
524
525 let inner = w.inner;
526 let w = &mut *w;
527 let update = match w.on_change.as_mut() {
528 Some(TimePickerOnChange { callback, refany }) => {
529 (callback.cb)(refany.clone(), info, inner)
530 }
531 None => Update::DoNothing,
532 };
533 (update, display_text)
534 };
535
536 info.change_node_text(display, display_text);
537 update
538}
539
540extern "C" fn on_hour_up(data: RefAny, info: CallbackInfo) -> Update {
541 adjust_spinner(data, info, true, 1)
542}
543
544extern "C" fn on_hour_down(data: RefAny, info: CallbackInfo) -> Update {
545 adjust_spinner(data, info, true, -1)
546}
547
548extern "C" fn on_minute_up(data: RefAny, info: CallbackInfo) -> Update {
549 adjust_spinner(data, info, false, 1)
550}
551
552extern "C" fn on_minute_down(data: RefAny, info: CallbackInfo) -> Update {
553 adjust_spinner(data, info, false, -1)
554}
555
556extern "C" fn on_ampm_toggle(mut data: RefAny, mut info: CallbackInfo) -> Update {
558 let hit = info.get_hit_node();
559
560 let (update, text) = {
561 let Some(mut w) = data.downcast_mut::<TimePickerStateWrapper>() else {
562 return Update::DoNothing;
563 };
564 w.inner.is_pm = !w.inner.is_pm;
565 let inner = w.inner;
566 let text = if inner.is_pm {
567 AzString::from_const_str("PM")
568 } else {
569 AzString::from_const_str("AM")
570 };
571 let w = &mut *w;
572 let update = match w.on_change.as_mut() {
573 Some(TimePickerOnChange { callback, refany }) => {
574 (callback.cb)(refany.clone(), info, inner)
575 }
576 None => Update::DoNothing,
577 };
578 (update, text)
579 };
580
581 info.change_node_text(hit, text);
582 update
583}
584
585impl From<TimePicker> for Dom {
586 fn from(t: TimePicker) -> Self {
587 t.dom()
588 }
589}