use std::rc::Rc;
use cranpose_core::DisposableEffect;
use cranpose_macros::composable;
use cranpose_ui::{
Alignment, LazyColumn, LazyColumnSpec, LazyItems, LazyListScope, LazyListState, Modifier,
ProgressBarRangeInfo, SemanticsSetProgress, Size, TextStyle, UiString, VerticalAlignment,
rememberLazyListStateWithPosition,
text::TextAlign,
widgets::{Box, BoxSpec, Row, RowSpec, Text},
};
use cranpose_ui_graphics::{Brush, CornerRadii, Point, Rect};
use crate::theme::{liquid_colors, liquid_typography};
const ROW: f32 = 40.0;
const VISIBLE_ROWS: usize = 5;
const PAD: usize = VISIBLE_ROWS / 2;
fn wheel_value(list: LazyListState, count: usize) -> usize {
let index = list.first_visible_item_index();
let past_half = list.first_visible_item_scroll_offset() > ROW / 2.0;
(index + usize::from(past_half)).min(count.saturating_sub(1))
}
#[derive(Clone, Copy, PartialEq)]
pub struct LiquidTimePickerState {
hours: LazyListState,
minutes: LazyListState,
periods: LazyListState,
twelve_hour: bool,
}
impl LiquidTimePickerState {
pub fn hour(&self) -> u8 {
if self.twelve_hour {
(wheel_value(self.hours, 12) + 12 * wheel_value(self.periods, 2)) as u8
} else {
wheel_value(self.hours, 24) as u8
}
}
pub fn minute(&self) -> u8 {
wheel_value(self.minutes, 60) as u8
}
pub fn set_time(&self, hour: u8, minute: u8) {
let hour = hour.min(23);
if self.twelve_hour {
self.hours.scroll_to_item(usize::from(hour % 12), 0.0);
self.periods.scroll_to_item(usize::from(hour / 12), 0.0);
} else {
self.hours.scroll_to_item(usize::from(hour), 0.0);
}
self.minutes
.scroll_to_item(usize::from(minute.min(59)), 0.0);
}
pub fn is_24_hour(&self) -> bool {
!self.twelve_hour
}
}
fn locale_uses_24_hours() -> bool {
#[cfg(feature = "localization-formatting")]
if let Ok(formatters) = cranpose_ui::localization::local_formatters() {
return formatters.uses_24_hour_clock();
}
false
}
fn day_periods() -> [String; 2] {
#[cfg(feature = "localization-formatting")]
if let Ok(periods) = cranpose_ui::localization::local_formatters()
.and_then(|formatters| formatters.day_periods())
{
return periods;
}
["AM".to_owned(), "PM".to_owned()]
}
#[composable]
pub fn rememberLiquidTimePickerState(
hour: u8,
minute: u8,
is_24_hour: Option<bool>,
) -> LiquidTimePickerState {
let twelve_hour = !is_24_hour.unwrap_or_else(locale_uses_24_hours);
let hour = hour.min(23);
let hour_row = if twelve_hour { hour % 12 } else { hour };
LiquidTimePickerState {
hours: rememberLazyListStateWithPosition(usize::from(hour_row), 0.0),
minutes: rememberLazyListStateWithPosition(usize::from(minute.min(59)), 0.0),
periods: rememberLazyListStateWithPosition(usize::from(hour / 12), 0.0),
twelve_hour,
}
}
#[composable]
pub fn LiquidTimePicker(state: LiquidTimePickerState, modifier: Modifier) {
let hours: Rc<[String]> = if state.twelve_hour {
(0..12)
.map(|hour| if hour == 0 { 12 } else { hour }.to_string())
.collect()
} else {
(0..24).map(|hour: u8| hour.to_string()).collect()
};
let minutes: Rc<[String]> = (0..60).map(|minute: u8| format!("{minute:02}")).collect();
let periods: Rc<[String]> = day_periods().into_iter().collect();
let (hour_name, minute_name) = (UiString::Hour.resolve(), UiString::Minute.resolve());
let title = liquid_typography().title3;
let style = TextStyle {
paragraph_style: cranpose_ui::ParagraphStyle {
text_align: TextAlign::Center,
..title.paragraph_style
},
..title
};
Row(
modifier,
RowSpec::default().vertical_alignment(VerticalAlignment::CenterVertically),
move || {
Wheel(
state.hours,
Rc::clone(&hours),
Some(hour_name.clone()),
64.0,
style.clone(),
);
Text(
":",
Modifier::empty().hide_from_accessibility(),
style.clone(),
);
Wheel(
state.minutes,
Rc::clone(&minutes),
Some(minute_name.clone()),
64.0,
style.clone(),
);
if state.twelve_hour {
Wheel(
state.periods,
Rc::clone(&periods),
None,
72.0,
style.clone(),
);
}
},
);
}
#[derive(Clone)]
struct Labels(Rc<[String]>);
impl PartialEq for Labels {
fn eq(&self, other: &Self) -> bool {
Rc::ptr_eq(&self.0, &other.0)
}
}
#[composable]
fn Wheel(
list: LazyListState,
labels: Rc<[String]>,
name: Option<String>,
width: f32,
style: TextStyle,
) {
DisposableEffect((), move |scope| {
list.set_settle_policy(Some(Rc::new(
|proposed, _velocity| {
if proposed > ROW / 2.0 { ROW } else { 0.0 }
},
)));
scope.on_dispose(move || list.set_settle_policy(None))
});
let count = labels.len();
let value = wheel_value(list, count);
let current = labels.get(value).cloned().unwrap_or_default();
let band = liquid_colors().fill;
let semantics = move |config: &mut cranpose_ui::SemanticsConfiguration| {
config.content_description.clone_from(&name);
config.state_description = Some(current.clone());
config.progress = Some(ProgressBarRangeInfo::new(
value as f32,
0.0,
count.saturating_sub(1) as f32,
count.saturating_sub(2) as u32,
));
config.set_progress = Some(SemanticsSetProgress::new(move |next: f32| {
let row = next.round().clamp(0.0, count.saturating_sub(1) as f32) as usize;
list.scroll_to_item(row, 0.0);
true
}));
};
let labels = Labels(labels);
Box(
Modifier::empty()
.size(Size::new(width, ROW * VISIBLE_ROWS as f32))
.draw_behind(move |scope| {
let size = scope.size();
scope.draw_round_rect_at(
Rect::from_origin_size(
Point::new(0.0, ROW * PAD as f32),
Size::new(size.width, ROW),
),
Brush::solid(band),
CornerRadii::uniform(8.0),
);
})
.value_picker()
.semantics(semantics),
BoxSpec::default(),
move || {
let labels = labels.clone();
let style = style.clone();
LazyColumn(
Modifier::empty().fill_max_size(),
list,
LazyColumnSpec::default(),
move |scope| {
let labels = labels.clone();
let style = style.clone();
scope.items(LazyItems::new(count + 2 * PAD), move |row| {
let label = row
.checked_sub(PAD)
.and_then(|value| labels.0.get(value))
.cloned();
let style = style.clone();
Box(
Modifier::empty().fill_max_width().height(ROW),
BoxSpec::default().content_alignment(Alignment::CENTER),
move || {
if let Some(label) = label.clone() {
Text(
label,
Modifier::empty().hide_from_accessibility(),
style.clone(),
);
}
},
);
});
},
);
},
);
}