use crate::event::{Event, EventCtx, Key, MouseButton, NamedKey};
use crate::geometry::{Color, Point, Rect};
use crate::painter::Painter;
use crate::theme::Theme;
use crate::widget::{PopupKind, PopupRequest, Widget};
type ChangeHandler = Box<dyn FnMut(&mut EventCtx, usize)>;
const ARROW_BTN_W: i32 = 17;
const ITEM_HEIGHT: i32 = 18;
const POPUP_PAD_Y: i32 = 2;
const TEXT_PAD_X: i32 = 5;
const SHADOW_SIZE: i32 = 2;
const SHADOW_COLOR: Color = Color::DARK_GRAY;
pub struct Dropdown {
rect: Rect,
items: Vec<String>,
selected: Option<usize>,
highlighted: Option<usize>,
open: bool,
focused: bool,
enabled: bool,
on_change: Option<ChangeHandler>,
}
impl Dropdown {
pub fn new(rect: Rect) -> Self {
Self {
rect,
items: Vec::new(),
selected: None,
highlighted: None,
open: false,
focused: false,
enabled: true,
on_change: None,
}
}
pub fn with_items<I, S>(mut self, items: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.set_items(items.into_iter().map(Into::into).collect());
self
}
pub fn set_items(&mut self, items: Vec<String>) {
self.items = items;
self.selected = match self.selected {
Some(i) if i < self.items.len() => Some(i),
_ if self.items.is_empty() => None,
_ => Some(0),
};
self.highlighted = None;
self.open = false;
}
pub fn with_selected(mut self, idx: usize) -> Self {
self.set_selected(Some(idx));
self
}
pub fn with_enabled(mut self, enabled: bool) -> Self {
self.set_enabled(enabled);
self
}
pub fn on_change<F>(mut self, handler: F) -> Self
where
F: FnMut(&mut EventCtx, usize) + 'static,
{
self.on_change = Some(Box::new(handler));
self
}
pub fn set_on_change<F>(&mut self, handler: F)
where
F: FnMut(&mut EventCtx, usize) + 'static,
{
self.on_change = Some(Box::new(handler));
}
pub fn items(&self) -> &[String] {
&self.items
}
pub fn selected_index(&self) -> Option<usize> {
self.selected
}
pub fn selected_text(&self) -> Option<&str> {
self.selected
.and_then(|i| self.items.get(i))
.map(String::as_str)
}
pub fn set_selected(&mut self, idx: Option<usize>) {
self.selected = idx.filter(|&i| i < self.items.len());
}
pub fn is_enabled(&self) -> bool {
self.enabled
}
pub fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;
if !enabled {
self.close();
}
}
pub fn is_open(&self) -> bool {
self.open
}
pub fn open(&mut self) {
self.open_list();
}
pub fn rect(&self) -> Rect {
self.rect
}
pub fn set_rect(&mut self, rect: Rect) {
self.rect = rect;
}
fn arrow_rect(&self) -> Rect {
let inner = self.rect.inset(1);
Rect::new(
inner.right() - ARROW_BTN_W,
inner.y,
ARROW_BTN_W,
inner.h.max(0),
)
}
fn text_area(&self) -> Rect {
let inner = self.rect.inset(2);
let w = (inner.w - ARROW_BTN_W).max(0);
Rect::new(inner.x, inner.y, w, inner.h)
}
fn popup_rect(&self) -> Rect {
let h = POPUP_PAD_Y * 2 + self.items.len() as i32 * ITEM_HEIGHT;
Rect::new(self.rect.x, self.rect.bottom(), self.rect.w, h)
}
fn hit_item(&self, pos: Point) -> Option<usize> {
if !self.open {
return None;
}
let popup = self.popup_rect();
if !popup.contains(pos) {
return None;
}
let local = pos.y - (popup.y + POPUP_PAD_Y);
if local < 0 {
return None;
}
let idx = (local / ITEM_HEIGHT) as usize;
(idx < self.items.len()).then_some(idx)
}
fn open_list(&mut self) {
if self.items.is_empty() {
return;
}
self.open = true;
self.highlighted = self.selected.or(Some(0));
}
fn close(&mut self) {
self.open = false;
self.highlighted = None;
}
fn commit(&mut self, idx: usize, ctx: &mut EventCtx) {
if idx >= self.items.len() {
return;
}
let changed = self.selected != Some(idx);
self.selected = Some(idx);
if changed && let Some(handler) = self.on_change.as_mut() {
handler(ctx, idx);
}
}
fn move_highlight(&mut self, delta: i32) {
if self.items.is_empty() {
return;
}
let n = self.items.len() as i32;
let cur = self.highlighted.or(self.selected).unwrap_or(0) as i32;
self.highlighted = Some((cur + delta).clamp(0, n - 1) as usize);
}
fn handle_key(&mut self, key: &Key, ctx: &mut EventCtx) {
if self.open {
match key {
Key::Named(NamedKey::Up) => self.move_highlight(-1),
Key::Named(NamedKey::Down) => self.move_highlight(1),
Key::Named(NamedKey::Home) => self.highlighted = Some(0),
Key::Named(NamedKey::End) => {
self.highlighted = self.items.len().checked_sub(1);
}
Key::Named(NamedKey::Enter) | Key::Named(NamedKey::Space) => {
if let Some(idx) = self.highlighted {
self.commit(idx, ctx);
}
self.close();
}
Key::Named(NamedKey::Escape) => self.close(),
_ => return,
}
} else {
match key {
Key::Named(NamedKey::Up) => {
let target = self.selected.unwrap_or(0).saturating_sub(1);
self.commit(target, ctx);
}
Key::Named(NamedKey::Down) => {
let target = self.selected.map(|i| i + 1).unwrap_or(0);
self.commit(target.min(self.items.len().saturating_sub(1)), ctx);
}
Key::Named(NamedKey::Home) => self.commit(0, ctx),
Key::Named(NamedKey::End) => {
if let Some(last) = self.items.len().checked_sub(1) {
self.commit(last, ctx);
}
}
Key::Named(NamedKey::Space) => self.open_list(),
_ => return,
}
}
ctx.request_paint();
ctx.consume_event();
}
}
impl Widget for Dropdown {
fn bounds(&self) -> Rect {
self.rect
}
fn paint(&mut self, painter: &mut Painter, theme: &Theme) {
let bg = if self.enabled {
Color::WHITE
} else {
theme.face
};
painter.fill_rect(self.rect, bg);
painter.sunken_bevel(self.rect, theme.highlight, theme.shadow);
painter.stroke_rect(self.rect, theme.border);
let btn = self.arrow_rect();
painter.button(btn, theme, self.open, false);
let arrow_color = if self.enabled {
theme.text
} else {
theme.disabled_text
};
draw_down_arrow(painter, btn, arrow_color);
if let Some(text) = self.selected_text() {
let area = self.text_area();
let saved = painter.push_clip(area);
let th = painter.measure_text(text, theme.font_size).h;
let ty = self.rect.y + ((self.rect.h - th) / 2).max(0);
let fg = if self.enabled {
theme.text
} else {
theme.disabled_text
};
painter.text(area.x + TEXT_PAD_X, ty, text, theme.font_size, fg);
painter.restore_clip(saved);
}
if self.focused && self.enabled {
draw_focus_rect(painter, self.text_area().inset(1), theme.text);
}
}
fn paint_overlay(&mut self, painter: &mut Painter, theme: &Theme) {
if !painter.is_popup_pass() || !self.open {
return;
}
let popup = self.popup_rect();
painter.fill_rect(
Rect::new(popup.x + SHADOW_SIZE, popup.bottom(), popup.w, SHADOW_SIZE),
SHADOW_COLOR,
);
painter.fill_rect(
Rect::new(popup.right(), popup.y + SHADOW_SIZE, SHADOW_SIZE, popup.h),
SHADOW_COLOR,
);
painter.fill_rect(popup, theme.background);
painter.stroke_rect(popup, theme.border);
let mut y = popup.y + POPUP_PAD_Y;
for (i, item) in self.items.iter().enumerate() {
let row = Rect::new(popup.x + 1, y, (popup.w - 2).max(0), ITEM_HEIGHT);
let (bg, fg) = if self.highlighted == Some(i) {
(theme.highlight_bg, theme.highlight_text)
} else {
(theme.background, theme.text)
};
painter.fill_rect(row, bg);
let th = painter.measure_text(item, theme.font_size).h;
let ty = row.y + ((row.h - th) / 2).max(0);
painter.text(row.x + TEXT_PAD_X, ty, item, theme.font_size, fg);
y += ITEM_HEIGHT;
}
}
fn event(&mut self, event: &Event, ctx: &mut EventCtx) {
if !self.enabled {
return;
}
match event {
Event::PointerDown {
pos,
button: MouseButton::Left,
} => {
if self.open {
if let Some(idx) = self.hit_item(*pos) {
self.commit(idx, ctx);
}
self.close();
ctx.request_paint();
} else if self.rect.contains(*pos) {
self.open_list();
ctx.request_focus();
ctx.request_paint();
}
}
Event::PointerMove { pos } if self.open => {
if let Some(hit) = self.hit_item(*pos)
&& self.highlighted != Some(hit)
{
self.highlighted = Some(hit);
ctx.request_paint();
}
}
Event::KeyDown { key, modifiers } if self.focused && !modifiers.has_command() => {
self.handle_key(key, ctx);
}
_ => {}
}
}
fn captures_pointer(&self) -> bool {
self.open
}
fn focusable(&self) -> bool {
self.enabled
}
fn set_focused(&mut self, focused: bool) {
self.focused = focused;
if !focused {
self.close();
}
}
fn layout(&mut self, bounds: Rect) {
self.rect = bounds;
}
fn popup_request(&self) -> Option<PopupRequest> {
if !self.open || self.items.is_empty() {
return None;
}
let popup = self.popup_rect();
Some(PopupRequest {
rect: Rect::new(
popup.x,
popup.y,
popup.w + SHADOW_SIZE,
popup.h + SHADOW_SIZE,
),
kind: PopupKind::Popup,
title: None,
})
}
}
fn draw_down_arrow(painter: &mut Painter, btn: Rect, color: Color) {
let cx = btn.x + btn.w / 2;
let top = btn.y + (btn.h - 4) / 2;
for row in 0..4 {
let half = 3 - row;
painter.fill_rect(Rect::new(cx - half, top + row, half * 2 + 1, 1), color);
}
}
fn draw_focus_rect(painter: &mut Painter, rect: Rect, color: Color) {
if rect.w <= 0 || rect.h <= 0 {
return;
}
let right = rect.right() - 1;
let bottom = rect.bottom() - 1;
let mut x = rect.x;
while x <= right {
painter.pixel(x, rect.y, color);
painter.pixel(x, bottom, color);
x += 2;
}
let mut y = rect.y;
while y <= bottom {
painter.pixel(rect.x, y, color);
painter.pixel(right, y, color);
y += 2;
}
}