use super::Widget;
use alloc::{string::String, vec::Vec};
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 ROW_H: u32 = 30;
const PAD_X: i32 = 10;
struct Entry<M> {
label: String,
message: M,
}
pub struct Menu<C: PixelColor, M: Clone> {
rect: Rectangle,
entries: Vec<Entry<M>>,
id: Option<WidgetId>,
selected: Option<usize>,
focused: Option<usize>,
pressed: Option<usize>,
row_h: u32,
width: Length,
height: Length,
_color: PhantomData<C>,
}
impl<C: PixelColor, M: Clone> Menu<C, M> {
pub fn new() -> Self {
Self {
rect: Rectangle::zero(),
entries: Vec::new(),
id: None,
selected: None,
focused: None,
pressed: None,
row_h: ROW_H,
width: Length::Fill,
height: Length::Shrink,
_color: PhantomData,
}
}
#[must_use]
pub fn entry(mut self, label: impl Into<String>, message: M) -> Self {
self.entries.push(Entry {
label: label.into(),
message,
});
self
}
#[must_use]
pub fn selected(mut self, index: usize) -> Self {
self.selected = Some(index);
self
}
#[must_use]
pub fn id(mut self, id: WidgetId) -> Self {
self.id = Some(id);
self
}
#[must_use]
pub fn row_height(mut self, h: u32) -> Self {
self.row_h = h.max(1);
self
}
#[must_use]
pub fn width(mut self, w: impl Into<Length>) -> Self {
self.width = w.into();
self
}
#[must_use]
pub fn height(mut self, h: impl Into<Length>) -> Self {
self.height = h.into();
self
}
fn row_rect(&self, i: usize) -> Rectangle {
Rectangle::new(
self.rect.top_left + Point::new(0, (i as u32 * self.row_h) as i32),
Size::new(self.rect.size.width, self.row_h),
)
}
fn row_at(&self, p: Point) -> Option<usize> {
let left = self.rect.top_left.x;
if p.x < left || p.x >= left + self.rect.size.width as i32 {
return None;
}
let dy = p.y - self.rect.top_left.y;
if dy < 0 {
return None;
}
let idx = (dy as u32 / self.row_h) as usize;
(idx < self.entries.len()).then_some(idx)
}
fn row_id(&self, index: usize) -> Option<WidgetId> {
self.id
.map(|id| WidgetId::new(id.raw().wrapping_add(index as u64 + 1)))
}
}
impl<C: PixelColor, M: Clone> Default for Menu<C, M> {
fn default() -> Self {
Self::new()
}
}
impl<C: PixelColor, M: Clone> Widget<C, M> for Menu<C, M> {
fn measure(&mut self, constraints: Constraints) -> Size {
let intrinsic_h = self.row_h * self.entries.len() as u32;
let w = self
.width
.resolve(constraints.max.width, constraints.max.width);
let h = self.height.resolve(intrinsic_h, 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> {
match phase {
TouchPhase::Down => {
self.pressed = self.row_at(point);
None
}
TouchPhase::Moved => {
if self.row_at(point) != self.pressed {
self.pressed = None;
}
None
}
TouchPhase::Up => {
let now = self.row_at(point);
let pressed = self.pressed.take();
if let (Some(i), Some(p)) = (now, pressed) {
if i == p {
return Some(self.entries[i].message.clone());
}
}
None
}
}
}
fn mark_pressed(&mut self, point: Point) {
if self.pressed.is_none() {
self.pressed = self.row_at(point);
}
}
fn collect_focusable(&self, out: &mut Vec<WidgetId>) {
for index in 0..self.entries.len() {
if let Some(id) = self.row_id(index) {
out.push(id);
}
}
}
fn sync_focus(&mut self, focused: Option<WidgetId>) {
self.focused = focused.and_then(|target| {
(0..self.entries.len()).find(|index| self.row_id(*index) == Some(target))
});
}
fn route_action(&mut self, target: WidgetId, action: UiAction) -> Option<M> {
let index = (0..self.entries.len()).find(|index| self.row_id(*index) == Some(target))?;
match action {
UiAction::Activate => Some(self.entries[index].message.clone()),
_ => None,
}
}
fn focus_rect(&self, target: WidgetId) -> Option<Rectangle> {
let index =
(0..self.entries.len()).find(|candidate| self.row_id(*candidate) == Some(target))?;
Some(self.row_rect(index))
}
fn focus_at(&self, point: Point) -> Option<WidgetId> {
self.row_at(point).and_then(|index| self.row_id(index))
}
fn draw<'t>(
&self,
renderer: &mut dyn Renderer<C>,
theme: &Theme<'t, C>,
) -> Result<(), RenderError> {
let font = theme.default_font();
let glyph_h = font.character_size.height as i32;
for (i, e) in self.entries.iter().enumerate() {
let r = self.row_rect(i);
let (bg, fg, border) = if Some(i) == self.selected {
(theme.accent.base, theme.accent.on_base, theme.button.border)
} else if Some(i) == self.focused {
(theme.button.base, theme.button.on_base, theme.accent.base)
} else if Some(i) == self.pressed {
(
theme.button.pressed,
theme.button.on_base,
theme.button.border,
)
} else {
(theme.button.base, theme.button.on_base, theme.button.border)
};
renderer.fill_rect(r, bg)?;
renderer.stroke_rect(r, border)?;
renderer.draw_text(
&e.label,
Point::new(
r.top_left.x + PAD_X,
r.top_left.y + self.row_h as i32 / 2 + glyph_h / 3,
),
font,
fg,
Alignment::Left,
)?;
}
Ok(())
}
}