use super::{Widget, element::Element};
use alloc::{boxed::Box, string::String, vec::Vec};
use core::marker::PhantomData;
use embedded_graphics::{
pixelcolor::PixelColor, prelude::*, primitives::Rectangle, text::Alignment,
};
use zest_core::{
Constraints, Horizontal, Length, RenderError, Renderer, TouchPhase, UiAction, Vertical,
WidgetId,
};
use zest_theme::Theme;
const ROW_HEIGHT: u32 = 36;
const TEXT_PAD: i32 = 8;
pub struct Dropdown<'a, C: PixelColor, M: Clone> {
options: Vec<String>,
selected: usize,
is_open: bool,
placeholder: String,
id: Option<WidgetId>,
on_toggle: Option<Box<dyn Fn(bool) -> M + 'a>>,
on_select: Option<Box<dyn Fn(usize) -> M + 'a>>,
width: Length,
height: Length,
stack: Option<Element<'a, C, M>>,
option_count: usize,
}
impl<'a, C: PixelColor + 'a, M: Clone + 'a> Dropdown<'a, C, M> {
pub fn new() -> Self {
Self {
options: Vec::new(),
selected: 0,
is_open: false,
placeholder: String::new(),
id: None,
on_toggle: None,
on_select: None,
width: Length::Fill,
height: Length::Fixed(ROW_HEIGHT),
stack: None,
option_count: 0,
}
}
#[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 options(mut self, options: &[&str]) -> Self {
self.options = options.iter().map(|s| String::from(*s)).collect();
self.option_count = self.options.len();
self
}
#[must_use]
pub fn selected(mut self, index: usize) -> Self {
self.selected = index;
self
}
#[must_use]
pub fn open(mut self, open: bool) -> Self {
self.is_open = open;
self
}
#[must_use]
pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
self.placeholder = placeholder.into();
self
}
#[must_use]
pub fn id(mut self, id: WidgetId) -> Self {
self.id = Some(id);
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 on_select<F: Fn(usize) -> M + 'a>(mut self, f: F) -> Self {
self.on_select = Some(Box::new(f));
self
}
fn build(&mut self) -> Element<'a, C, M> {
let label = self
.options
.get(self.selected)
.cloned()
.unwrap_or_else(|| self.placeholder.clone());
let field = DropdownField {
rect: Rectangle::zero(),
id: self.id,
label,
open: self.is_open,
on_toggle: self.on_toggle.take(),
focused: false,
pressed: false,
width: self.width,
height: self.height,
_color: PhantomData,
};
let mut stack = super::stack::Stack::new()
.width(self.width)
.height(self.height);
stack = stack.push_aligned(field, Horizontal::Left, Vertical::Top);
if self.is_open && !self.options.is_empty() {
let list = DropdownList {
rect: Rectangle::zero(),
base_id: self.id,
options: self.options.clone(),
selected: self.selected,
on_select: self.on_select.take(),
focused: None,
pressed: None,
_color: PhantomData,
};
stack = stack.push_aligned(list, Horizontal::Left, Vertical::Top);
}
Element::new(stack)
}
fn ensure_built(&mut self) {
if self.stack.is_none() {
self.stack = Some(self.build());
}
}
}
impl<'a, C: PixelColor + 'a, M: Clone + 'a> Default for Dropdown<'a, C, M> {
fn default() -> Self {
Self::new()
}
}
impl<'a, C: PixelColor + 'a, M: Clone + 'a> Widget<C, M> for Dropdown<'a, C, M> {
fn measure(&mut self, constraints: Constraints) -> Size {
self.ensure_built();
let w = self
.width
.resolve(constraints.max.width, constraints.max.width);
let h = self.height.resolve(ROW_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.ensure_built();
let extra = if self.is_open {
ROW_HEIGHT.saturating_mul(self.option_count as u32)
} else {
0
};
let region = Rectangle::new(
rect.top_left,
Size::new(rect.size.width, rect.size.height + extra),
);
if let Some(stack) = self.stack.as_mut() {
stack.arrange(region);
}
}
fn rect(&self) -> Rectangle {
self.stack.as_ref().map_or(Rectangle::zero(), |s| s.rect())
}
fn handle_touch(&mut self, point: Point, phase: TouchPhase) -> Option<M> {
self.ensure_built();
self.stack
.as_mut()
.and_then(|s| s.handle_touch(point, phase))
}
fn mark_pressed(&mut self, point: Point) {
self.ensure_built();
if let Some(stack) = self.stack.as_mut() {
stack.mark_pressed(point);
}
}
fn collect_focusable(&self, out: &mut Vec<WidgetId>) {
if let Some(id) = self.id
&& self.on_toggle.is_some()
{
out.push(id);
}
if self.is_open && self.on_select.is_some() {
for index in 0..self.options.len() {
if let Some(id) = self.row_id(index) {
out.push(id);
}
}
}
}
fn sync_focus(&mut self, focused: Option<WidgetId>) {
self.ensure_built();
if let Some(stack) = self.stack.as_mut() {
stack.sync_focus(focused);
}
}
fn route_action(&mut self, target: WidgetId, action: UiAction) -> Option<M> {
self.ensure_built();
self.stack
.as_mut()
.and_then(|stack| stack.route_action(target, action))
}
fn focus_rect(&self, target: WidgetId) -> Option<Rectangle> {
self.stack
.as_ref()
.and_then(|stack| stack.focus_rect(target))
}
fn focus_at(&self, point: Point) -> Option<WidgetId> {
self.stack.as_ref().and_then(|stack| stack.focus_at(point))
}
fn draw<'t>(
&self,
renderer: &mut dyn Renderer<C>,
theme: &Theme<'t, C>,
) -> Result<(), RenderError> {
if let Some(stack) = &self.stack {
stack.draw(renderer, theme)?;
}
Ok(())
}
}
impl<'a, C: PixelColor + 'a, M: Clone + 'a> Dropdown<'a, C, M> {
fn row_id(&self, index: usize) -> Option<WidgetId> {
self.id
.map(|id| WidgetId::new(id.raw().wrapping_add(index as u64 + 1)))
}
}
struct DropdownField<'a, C: PixelColor, M: Clone> {
rect: Rectangle,
id: Option<WidgetId>,
label: String,
open: bool,
on_toggle: Option<Box<dyn Fn(bool) -> M + 'a>>,
focused: bool,
pressed: bool,
width: Length,
height: Length,
_color: PhantomData<C>,
}
impl<C: PixelColor, M: Clone> DropdownField<'_, C, M> {
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<C: PixelColor, M: Clone> Widget<C, M> for DropdownField<'_, C, M> {
fn measure(&mut self, constraints: Constraints) -> Size {
let w = self
.width
.resolve(constraints.max.width, constraints.max.width);
let h = self.height.resolve(ROW_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.on_toggle.is_none() || !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;
let open = self.open;
self.on_toggle.as_ref().map(|cb| cb(!open))
} else {
None
}
}
TouchPhase::Moved => None,
}
}
fn mark_pressed(&mut self, point: Point) {
if self.on_toggle.is_some() && 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.on_toggle.is_some()
}
fn handle_action(&mut self, action: UiAction) -> Option<M> {
match action {
UiAction::Activate => self.on_toggle.as_ref().map(|cb| cb(!self.open)),
_ => 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 focus_rect(&self, target: WidgetId) -> Option<Rectangle> {
if self.id == Some(target) {
Some(self.rect)
} else {
None
}
}
fn draw<'t>(
&self,
renderer: &mut dyn Renderer<C>,
theme: &Theme<'t, C>,
) -> Result<(), RenderError> {
let comp = &theme.button;
let bg = if self.pressed {
comp.pressed
} else {
comp.base
};
let border = if self.focused {
theme.accent.base
} else {
comp.border
};
renderer.fill_rect(self.rect, bg)?;
renderer.stroke_rect(self.rect, border)?;
let font = theme.default_font();
let text_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(self.rect.top_left.x + TEXT_PAD, text_y),
font,
comp.on_base,
Alignment::Left,
)?;
let cx = self.rect.top_left.x + self.rect.size.width as i32 - TEXT_PAD - 6;
let cy = self.rect.top_left.y + self.rect.size.height as i32 / 2;
if self.open {
renderer.stroke_line(
Point::new(cx, cy + 3),
Point::new(cx + 6, cy + 3),
comp.on_base,
2,
)?;
renderer.stroke_line(
Point::new(cx, cy + 3),
Point::new(cx + 3, cy - 3),
comp.on_base,
2,
)?;
renderer.stroke_line(
Point::new(cx + 6, cy + 3),
Point::new(cx + 3, cy - 3),
comp.on_base,
2,
)?;
} else {
renderer.stroke_line(
Point::new(cx, cy - 3),
Point::new(cx + 6, cy - 3),
comp.on_base,
2,
)?;
renderer.stroke_line(
Point::new(cx, cy - 3),
Point::new(cx + 3, cy + 3),
comp.on_base,
2,
)?;
renderer.stroke_line(
Point::new(cx + 6, cy - 3),
Point::new(cx + 3, cy + 3),
comp.on_base,
2,
)?;
}
Ok(())
}
}
struct DropdownList<'a, C: PixelColor, M: Clone> {
rect: Rectangle,
base_id: Option<WidgetId>,
options: Vec<String>,
selected: usize,
on_select: Option<Box<dyn Fn(usize) -> M + 'a>>,
focused: Option<usize>,
pressed: Option<usize>,
_color: PhantomData<C>,
}
impl<C: PixelColor, M: Clone> DropdownList<'_, C, M> {
fn list_height(&self) -> u32 {
ROW_HEIGHT.saturating_mul(self.options.len() as u32)
}
fn row_at(&self, point: Point) -> Option<usize> {
let tl = self.rect.top_left;
if point.x < tl.x || point.x >= tl.x + self.rect.size.width as i32 {
return None;
}
let dy = point.y - tl.y;
if dy < 0 {
return None;
}
let idx = (dy as u32 / ROW_HEIGHT) as usize;
(idx < self.options.len()).then_some(idx)
}
fn row_id(&self, index: usize) -> Option<WidgetId> {
self.base_id
.map(|id| WidgetId::new(id.raw().wrapping_add(index as u64 + 1)))
}
}
impl<C: PixelColor, M: Clone> Widget<C, M> for DropdownList<'_, C, M> {
fn measure(&mut self, constraints: Constraints) -> Size {
let w = constraints.max.width;
let h = self.list_height().min(constraints.max.height);
constraints.clamp(Size::new(w, h))
}
fn preferred_size(&self) -> (Length, Length) {
(Length::Fill, Length::Fixed(self.list_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> {
let row = self.row_at(point);
match phase {
TouchPhase::Down => {
self.pressed = row;
None
}
TouchPhase::Up => {
let armed = self.pressed.take();
match (row, armed) {
(Some(r), Some(a)) if r == a => self.on_select.as_ref().map(|cb| cb(r)),
_ => None,
}
}
TouchPhase::Moved => {
if row != self.pressed {
self.pressed = None;
}
None
}
}
}
fn mark_pressed(&mut self, point: Point) {
if let Some(r) = self.row_at(point) {
self.pressed = Some(r);
}
}
fn collect_focusable(&self, out: &mut Vec<WidgetId>) {
if self.on_select.is_none() {
return;
}
for index in 0..self.options.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.options.len()).find(|index| self.row_id(*index) == Some(target))
});
}
fn route_action(&mut self, target: WidgetId, action: UiAction) -> Option<M> {
let index = (0..self.options.len()).find(|index| self.row_id(*index) == Some(target))?;
match action {
UiAction::Activate => self.on_select.as_ref().map(|cb| cb(index)),
_ => None,
}
}
fn focus_at(&self, point: Point) -> Option<WidgetId> {
self.row_at(point).and_then(|index| self.row_id(index))
}
fn focus_rect(&self, target: WidgetId) -> Option<Rectangle> {
let index =
(0..self.options.len()).find(|candidate| self.row_id(*candidate) == Some(target))?;
Some(Rectangle::new(
Point::new(
self.rect.top_left.x,
self.rect.top_left.y + index as i32 * ROW_HEIGHT as i32,
),
Size::new(self.rect.size.width, ROW_HEIGHT),
))
}
fn draw<'t>(
&self,
renderer: &mut dyn Renderer<C>,
theme: &Theme<'t, C>,
) -> Result<(), RenderError> {
let bg = theme.background.base;
let font = theme.default_font();
renderer.fill_rect(self.rect, bg)?;
renderer.stroke_rect(self.rect, theme.button.border)?;
let x = self.rect.top_left.x;
let w = self.rect.size.width;
for (i, opt) in self.options.iter().enumerate() {
let y = self.rect.top_left.y + (i as u32 * ROW_HEIGHT) as i32;
let row_rect = Rectangle::new(Point::new(x, y), Size::new(w, ROW_HEIGHT));
let highlighted = self.pressed == Some(i);
let selected = i == self.selected;
let focused = self.focused == Some(i);
if highlighted {
renderer.fill_rect(row_rect, theme.accent.pressed)?;
} else if selected {
renderer.fill_rect(row_rect, theme.accent.base)?;
}
let border = if focused {
theme.accent.base
} else {
theme.button.border
};
renderer.stroke_rect(row_rect, border)?;
let text_color = if highlighted || selected {
theme.accent.on_base
} else {
theme.background.on_base
};
let text_y = y + ROW_HEIGHT as i32 / 2 + font.character_size.height as i32 / 3;
renderer.draw_text(
opt,
Point::new(x + TEXT_PAD, text_y),
font,
text_color,
Alignment::Left,
)?;
if i + 1 < self.options.len() && !focused {
let sep_y = y + ROW_HEIGHT as i32 - 1;
renderer.fill_rect(
Rectangle::new(Point::new(x, sep_y), Size::new(w, 1)),
theme.background.divider,
)?;
}
}
Ok(())
}
}