use super::Widget;
use alloc::string::String;
use core::marker::PhantomData;
use embedded_graphics::{
mono_font::MonoFont, pixelcolor::PixelColor, prelude::*, primitives::Rectangle, text::Alignment,
};
use zest_core::{Constraints, Length, RenderError, Renderer, TouchPhase, UiAction, WidgetId};
use zest_theme::{ButtonCatalog, ButtonClass, Status, Theme};
pub struct Button<'a, C: PixelColor, M: Clone> {
rect: Rectangle,
label: String,
on_press: Option<M>,
id: Option<WidgetId>,
focused: bool,
pressed: bool,
class: ButtonClass,
font_override: Option<&'a MonoFont<'a>>,
intrinsic_size: Size,
width: Length,
height: Length,
_color: PhantomData<C>,
}
impl<'a, C: PixelColor, M: Clone> Button<'a, C, M> {
pub fn new(label: impl Into<String>) -> Self {
Self {
rect: Rectangle::zero(),
label: label.into(),
on_press: None,
id: None,
focused: false,
pressed: false,
class: ButtonClass::Standard,
font_override: None,
intrinsic_size: Size::new(48, 32),
width: Length::Fill,
height: Length::Fill,
_color: PhantomData,
}
}
#[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
}
#[must_use]
pub fn on_press(mut self, msg: M) -> Self {
self.on_press = Some(msg);
self
}
#[must_use]
pub fn on_press_maybe(mut self, msg: Option<M>) -> Self {
self.on_press = msg;
self
}
#[must_use]
pub fn id(mut self, id: WidgetId) -> Self {
self.id = Some(id);
self
}
#[must_use]
pub fn label(mut self, label: impl Into<String>) -> Self {
self.label = label.into();
self
}
#[must_use]
pub fn class(mut self, class: ButtonClass) -> Self {
self.class = class;
self
}
#[must_use]
pub fn font(mut self, font: &'a MonoFont<'a>) -> Self {
self.font_override = Some(font);
self
}
#[must_use]
pub fn intrinsic_size(mut self, size: Size) -> Self {
self.intrinsic_size = size;
self
}
pub fn is_enabled(&self) -> bool {
self.on_press.is_some()
}
fn status(&self) -> Status {
if !self.is_enabled() {
Status::Disabled
} else if self.pressed {
Status::Pressed
} else if self.focused {
Status::Focused
} else {
Status::Active
}
}
fn resolved_font<'t>(&'t self, theme: &'t Theme<'a, C>) -> &'t MonoFont<'a> {
self.font_override.unwrap_or(theme.default_font())
}
fn hit_test(&self, point: Point) -> bool {
let top_left = self.rect.top_left;
let bot_right =
top_left + Point::new(self.rect.size.width as i32, self.rect.size.height as i32);
point.x >= top_left.x
&& point.x < bot_right.x
&& point.y >= top_left.y
&& point.y < bot_right.y
}
}
impl<'a, C: PixelColor, M: Clone> Widget<C, M> for Button<'a, C, M> {
fn measure(&mut self, constraints: Constraints) -> Size {
let w = self
.width
.resolve(self.intrinsic_size.width, constraints.max.width);
let h = self
.height
.resolve(self.intrinsic_size.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() {
return None;
}
if !self.hit_test(point) {
return None;
}
match phase {
TouchPhase::Down => {
self.pressed = true;
None
}
TouchPhase::Up => {
if self.pressed {
self.on_press.clone()
} 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_press.clone(),
_ => 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 appearance = theme.button(self.class, self.status());
let font = self.resolved_font(theme);
if let Some(bg) = appearance.background {
renderer.fill_rect(self.rect, bg)?;
}
if let Some(border) = appearance.border {
renderer.stroke_rect(self.rect, border)?;
}
let center_x = self.rect.top_left.x + self.rect.size.width as i32 / 2;
let center_y = self.rect.top_left.y
+ self.rect.size.height as i32 / 2
+ font.character_size.height as i32 / 3;
renderer.draw_text(
&self.label,
Point::new(center_x, center_y),
font,
appearance.text,
Alignment::Center,
)?;
Ok(())
}
}