use crate::event::{Event, EventCtx, Key, MouseButton, NamedKey};
use crate::geometry::Rect;
use crate::painter::Painter;
use crate::theme::Theme;
use crate::widget::Widget;
type ClickHandler = Box<dyn FnMut(&mut EventCtx)>;
#[derive(Clone, Copy, PartialEq, Eq)]
enum Press {
None,
Mouse,
Keyboard,
}
pub struct Button {
pub rect: Rect,
pub label: String,
pub default: bool,
press: Press,
armed: bool,
focused: bool,
enabled: bool,
on_click: Option<ClickHandler>,
}
impl Button {
pub fn new(rect: Rect, label: impl Into<String>) -> Self {
Self {
rect,
label: label.into(),
default: false,
press: Press::None,
armed: false,
focused: false,
enabled: true,
on_click: None,
}
}
pub fn default(mut self, default: bool) -> Self {
self.default = default;
self
}
pub fn with_enabled(mut self, enabled: bool) -> Self {
self.set_enabled(enabled);
self
}
pub fn is_enabled(&self) -> bool {
self.enabled
}
pub fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;
if !enabled {
self.press = Press::None;
self.armed = false;
}
}
pub fn on_click<F>(mut self, handler: F) -> Self
where
F: FnMut(&mut EventCtx) + 'static,
{
self.on_click = Some(Box::new(handler));
self
}
fn fire(&mut self, ctx: &mut EventCtx) {
if let Some(handler) = self.on_click.as_mut() {
handler(ctx);
}
}
fn keyboard_arms(&self, key: &Key) -> bool {
if self.focused {
matches!(
key,
Key::Named(NamedKey::Enter) | Key::Named(NamedKey::Space)
)
} else {
self.default && matches!(key, Key::Named(NamedKey::Enter))
}
}
}
impl Widget for Button {
fn bounds(&self) -> Rect {
self.rect
}
fn paint(&mut self, painter: &mut Painter, theme: &Theme) {
let pressed_visual = self.press != Press::None && self.armed;
painter.button(self.rect, theme, pressed_visual, self.default);
let mut label_rect = self.rect;
if pressed_visual {
label_rect.x += 1;
label_rect.y += 1;
}
if self.enabled {
painter.text_centered(label_rect, &self.label, theme.font_size, theme.text);
} else {
let mut emboss = label_rect;
emboss.x += 1;
emboss.y += 1;
painter.text_centered(emboss, &self.label, theme.font_size, theme.highlight);
painter.text_centered(
label_rect,
&self.label,
theme.font_size,
theme.disabled_text,
);
}
if self.focused && self.enabled {
let inset = if self.default { 4 } else { 3 };
painter.focus_rect(self.rect.inset(inset), theme.text);
}
}
fn event(&mut self, event: &Event, ctx: &mut EventCtx) {
if !self.enabled {
return;
}
match event {
Event::PointerDown {
pos,
button: MouseButton::Left,
..
} if self.rect.contains(*pos) && self.press == Press::None => {
self.press = Press::Mouse;
self.armed = true;
ctx.request_focus();
ctx.request_paint();
}
Event::PointerMove { pos } if self.press == Press::Mouse => {
let armed_now = self.rect.contains(*pos);
if armed_now != self.armed {
self.armed = armed_now;
ctx.request_paint();
}
}
Event::PointerUp {
pos,
button: MouseButton::Left,
..
} if self.press == Press::Mouse => {
let fire = self.armed && self.rect.contains(*pos);
self.press = Press::None;
self.armed = false;
ctx.request_paint();
if fire {
self.fire(ctx);
}
}
Event::PointerLeave if self.press == Press::Mouse && self.armed => {
self.armed = false;
ctx.request_paint();
}
Event::KeyDown { key, modifiers }
if self.press != Press::Mouse
&& !modifiers.has_command()
&& self.keyboard_arms(key) =>
{
if self.press == Press::None {
self.press = Press::Keyboard;
self.armed = true;
ctx.request_paint();
}
ctx.consume_event();
}
Event::KeyUp { key, modifiers }
if self.press == Press::Keyboard
&& !modifiers.has_command()
&& self.keyboard_arms(key) =>
{
let fire = self.armed;
self.press = Press::None;
self.armed = false;
ctx.request_paint();
if fire {
self.fire(ctx);
}
ctx.consume_event();
}
Event::KeyDown {
key: Key::Named(NamedKey::Escape),
..
} if self.press == Press::Keyboard => {
self.press = Press::None;
self.armed = false;
ctx.request_paint();
ctx.consume_event();
}
_ => {}
}
}
fn captures_pointer(&self) -> bool {
self.press == Press::Mouse
}
fn focusable(&self) -> bool {
self.enabled
}
fn set_focused(&mut self, focused: bool) {
self.focused = focused;
if !focused && self.press == Press::Keyboard {
self.press = Press::None;
self.armed = false;
}
}
fn layout(&mut self, bounds: Rect) {
self.rect = bounds;
}
fn accepts_accelerators(&self) -> bool {
self.default && self.enabled
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::event::Modifiers;
use crate::geometry::Point;
fn enter() -> Event {
Event::KeyDown {
key: Key::Named(NamedKey::Enter),
modifiers: Modifiers::default(),
}
}
#[test]
fn a_button_leaves_the_pointer_at_the_default_arrow() {
let mut button = Button::new(Rect::new(0, 0, 70, 26), "OK");
let mut ctx = EventCtx::new();
button.event(
&Event::PointerMove {
pos: Point::new(10, 10),
},
&mut ctx,
);
assert_eq!(ctx.cursor_request, None);
}
#[test]
fn held_default_button_consumes_its_autorepeat() {
let mut button = Button::new(Rect::new(0, 0, 70, 26), "OK").default(true);
let mut ctx = EventCtx::new();
button.event(&enter(), &mut ctx);
assert!(ctx.is_consumed(), "the first Enter arms and is consumed");
let mut ctx = EventCtx::new();
button.event(&enter(), &mut ctx);
assert!(
ctx.is_consumed(),
"an autorepeated Enter is still consumed while armed"
);
}
}