use super::Widget;
use alloc::{boxed::Box, string::String};
use core::marker::PhantomData;
use embedded_graphics::{
pixelcolor::PixelColor, prelude::*, primitives::Rectangle, text::Alignment,
};
use zest_core::{Constraints, Length, RenderError, Renderer, TouchPhase, UiAction, WidgetId};
use zest_theme::Theme;
const BOX_SIZE: u32 = 20;
const LABEL_GAP: u32 = 6;
pub struct Checkbox<'a, C: PixelColor, M: Clone> {
rect: Rectangle,
checked: bool,
label: Option<String>,
id: Option<WidgetId>,
on_toggle: Option<Box<dyn Fn(bool) -> M + 'a>>,
focused: bool,
pressed: bool,
width: Length,
height: Length,
_color: PhantomData<C>,
}
impl<'a, C: PixelColor, M: Clone> Checkbox<'a, C, M> {
pub fn new(checked: bool) -> Self {
Self {
rect: Rectangle::zero(),
checked,
label: None,
id: None,
on_toggle: None,
focused: false,
pressed: false,
width: Length::Shrink,
height: Length::Fixed(BOX_SIZE),
_color: PhantomData,
}
}
#[must_use]
pub fn label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
#[must_use]
pub fn on_toggle<F: Fn(bool) -> M + 'a>(mut self, f: F) -> Self {
self.on_toggle = Some(Box::new(f));
self
}
#[must_use]
pub fn id(mut self, id: WidgetId) -> Self {
self.id = Some(id);
self
}
#[must_use]
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = width.into();
self
}
#[must_use]
pub fn height(mut self, height: impl Into<Length>) -> Self {
self.height = height.into();
self
}
pub fn is_enabled(&self) -> bool {
self.on_toggle.is_some()
}
fn intrinsic(&self) -> Size {
let label_w = self
.label
.as_ref()
.map_or(0, |l| LABEL_GAP + l.chars().count() as u32 * 8);
Size::new(BOX_SIZE + label_w, BOX_SIZE)
}
fn box_rect(&self) -> Rectangle {
let y = self.rect.top_left.y + (self.rect.size.height.saturating_sub(BOX_SIZE) / 2) as i32;
Rectangle::new(
Point::new(self.rect.top_left.x, y),
Size::new(BOX_SIZE, BOX_SIZE),
)
}
fn hit_test(&self, point: Point) -> bool {
let tl = self.rect.top_left;
let br = tl + Point::new(self.rect.size.width as i32, self.rect.size.height as i32);
point.x >= tl.x && point.x < br.x && point.y >= tl.y && point.y < br.y
}
}
impl<'a, C: PixelColor, M: Clone> Widget<C, M> for Checkbox<'a, C, M> {
fn measure(&mut self, constraints: Constraints) -> Size {
let intrinsic = self.intrinsic();
let w = self.width.resolve(intrinsic.width, constraints.max.width);
let h = self
.height
.resolve(intrinsic.height, constraints.max.height);
constraints.clamp(Size::new(w, h))
}
fn preferred_size(&self) -> (Length, Length) {
(self.width, self.height)
}
fn arrange(&mut self, rect: Rectangle) {
self.rect = rect;
}
fn rect(&self) -> Rectangle {
self.rect
}
fn handle_touch(&mut self, point: Point, phase: TouchPhase) -> Option<M> {
if !self.is_enabled() || !self.hit_test(point) {
if matches!(phase, TouchPhase::Up | TouchPhase::Moved) {
self.pressed = false;
}
return None;
}
match phase {
TouchPhase::Down => {
self.pressed = true;
None
}
TouchPhase::Up => {
if self.pressed {
self.pressed = false;
self.on_toggle.as_ref().map(|cb| cb(!self.checked))
} else {
None
}
}
TouchPhase::Moved => None,
}
}
fn mark_pressed(&mut self, point: Point) {
if self.is_enabled() && self.hit_test(point) {
self.pressed = true;
}
}
fn widget_id(&self) -> Option<WidgetId> {
self.id
}
fn is_focusable(&self) -> bool {
self.id.is_some() && self.is_enabled()
}
fn handle_action(&mut self, action: UiAction) -> Option<M> {
if !self.is_enabled() {
return None;
}
match action {
UiAction::Activate => self.on_toggle.as_ref().map(|cb| cb(!self.checked)),
_ => None,
}
}
fn sync_focus(&mut self, focused: Option<WidgetId>) {
self.focused = self.id.is_some() && self.id == focused;
}
fn focus_at(&self, point: Point) -> Option<WidgetId> {
if self.is_focusable() && self.hit_test(point) {
self.id
} else {
None
}
}
fn draw<'t>(
&self,
renderer: &mut dyn Renderer<C>,
theme: &Theme<'t, C>,
) -> Result<(), RenderError> {
let accent = &theme.accent;
let box_rect = self.box_rect();
if self.checked {
let fill = if self.pressed {
accent.pressed
} else {
accent.base
};
let border = if self.focused {
accent.base
} else {
accent.border
};
renderer.fill_rect(box_rect, fill)?;
renderer.stroke_rect(box_rect, border)?;
let x = box_rect.top_left.x;
let y = box_rect.top_left.y;
let s = BOX_SIZE as i32;
renderer.stroke_line(
Point::new(x + s * 3 / 16, y + s / 2),
Point::new(x + s * 7 / 16, y + s * 11 / 16),
accent.on_base,
2,
)?;
renderer.stroke_line(
Point::new(x + s * 7 / 16, y + s * 11 / 16),
Point::new(x + s * 13 / 16, y + s * 5 / 16),
accent.on_base,
2,
)?;
} else {
let bg = if self.pressed {
accent.pressed
} else {
theme.background.base
};
let border = if self.focused {
accent.base
} else {
accent.border
};
renderer.fill_rect(box_rect, bg)?;
renderer.stroke_rect(box_rect, border)?;
}
if let Some(label) = &self.label {
let font = theme.default_font();
let text_x = box_rect.top_left.x + BOX_SIZE as i32 + LABEL_GAP as i32;
let center_y = self.rect.top_left.y
+ self.rect.size.height as i32 / 2
+ font.character_size.height as i32 / 3;
let color = if !self.is_enabled() {
theme.palette.neutral_2
} else if self.focused {
theme.accent.base
} else {
theme.background.on_base
};
renderer.draw_text(
label,
Point::new(text_x, center_y),
font,
color,
Alignment::Left,
)?;
}
Ok(())
}
}