use cranpose_animation::{AnimationSpec, AnimationType, Easing, animateColorAsState};
use cranpose_macros::composable;
use cranpose_ui::{
Alignment, Modifier, Point, SemanticsWidgetRole, Size, ToggleableState,
widgets::{Box, BoxSpec},
};
use cranpose_ui_graphics::{Brush, Color, CornerRadii, Rect, Stroke, StrokeCap, StrokeJoin};
use crate::theme::liquid_colors;
const MARK_SIZE: f32 = 22.0;
const BOX_RADIUS: f32 = 6.0;
const OUTLINE_WIDTH: f32 = 2.0;
const CHECK_WIDTH: f32 = 2.5;
const DOT_RADIUS: f32 = 5.0;
fn mark_motion() -> AnimationType {
AnimationType::Tween(AnimationSpec::tween(160, Easing::EaseOut))
}
#[composable]
pub fn LiquidCheckbox(modifier: Modifier, checked: bool, on_change: impl Fn(bool) + 'static) {
LiquidTriStateCheckbox(modifier, checked.into(), move || on_change(!checked));
}
#[composable]
pub fn LiquidTriStateCheckbox(
modifier: Modifier,
state: ToggleableState,
on_click: impl Fn() + 'static,
) {
let colors = liquid_colors();
let marked = state != ToggleableState::Off;
let fill = animateColorAsState(
if marked {
colors.accent
} else {
Color::TRANSPARENT
},
mark_motion(),
"checkbox-fill",
)
.value();
let outline = if marked {
colors.accent
} else {
colors.secondary_label
};
let mark = colors.on_accent;
let control =
modifier.tri_state_toggleable(state, None, Some(SemanticsWidgetRole::Checkbox), on_click);
SelectionControl(control, move |scope| {
let bounds = Rect::from_size(scope.size());
let radii = CornerRadii::uniform(BOX_RADIUS);
scope.draw_round_rect_at(bounds, Brush::solid(fill), radii);
let half = OUTLINE_WIDTH * 0.5;
let outline_rect = Rect {
x: half,
y: half,
width: bounds.width - OUTLINE_WIDTH,
height: bounds.height - OUTLINE_WIDTH,
};
scope.draw_round_rect_at_stroked(
outline_rect,
Brush::solid(outline),
CornerRadii::uniform(BOX_RADIUS - half),
Stroke::new(OUTLINE_WIDTH),
);
match state {
ToggleableState::On => draw_check(scope, mark),
ToggleableState::Indeterminate => draw_dash(scope, mark),
ToggleableState::Off => {}
}
});
}
#[composable]
pub fn LiquidRadioButton(modifier: Modifier, selected: bool, on_click: impl Fn() + 'static) {
let colors = liquid_colors();
let outline = animateColorAsState(
if selected {
colors.accent
} else {
colors.secondary_label
},
mark_motion(),
"radio-outline",
)
.value();
let dot = colors.accent;
let control = modifier.selectable(selected, Some(SemanticsWidgetRole::RadioButton), on_click);
SelectionControl(control, move |scope| {
let size = scope.size();
let center = Point::new(size.width * 0.5, size.height * 0.5);
scope.draw_circle_stroked(
Brush::solid(outline),
center,
(MARK_SIZE - OUTLINE_WIDTH) * 0.5,
Stroke::new(OUTLINE_WIDTH),
);
if selected {
scope.draw_circle(Brush::solid(dot), center, DOT_RADIUS);
}
});
}
#[composable]
fn SelectionControl(
control: Modifier,
draw: impl Fn(&mut dyn cranpose_ui_graphics::DrawScope) + 'static,
) {
let mark = Modifier::empty()
.size(Size::new(MARK_SIZE, MARK_SIZE))
.draw_behind(draw);
Box(
control.minimum_interactive_component_size(),
BoxSpec::default().content_alignment(Alignment::CENTER),
move || {
Box(mark.clone(), BoxSpec::default(), || {});
},
);
}
fn draw_dash(scope: &mut dyn cranpose_ui_graphics::DrawScope, color: Color) {
let stroke = Stroke::new(CHECK_WIDTH).with_cap(StrokeCap::Round);
scope.draw_line(
Brush::solid(color),
Point::new(MARK_SIZE * 0.3, MARK_SIZE * 0.5),
Point::new(MARK_SIZE * 0.7, MARK_SIZE * 0.5),
stroke,
);
}
fn draw_check(scope: &mut dyn cranpose_ui_graphics::DrawScope, color: Color) {
let stroke = Stroke::new(CHECK_WIDTH)
.with_cap(StrokeCap::Round)
.with_join(StrokeJoin::Round);
let at = |x: f32, y: f32| Point::new(MARK_SIZE * x, MARK_SIZE * y);
let brush = Brush::solid(color);
scope.draw_line(brush.clone(), at(0.27, 0.52), at(0.43, 0.68), stroke);
scope.draw_line(brush, at(0.43, 0.68), at(0.74, 0.34), stroke);
}