use crate::event::{Event, EventCtx, Key, MouseButton, NamedKey};
use crate::geometry::{Color, Rect};
use crate::painter::Painter;
use crate::theme::Theme;
use crate::widget::Widget;
type ToggleHandler = Box<dyn FnMut(&mut EventCtx, bool)>;
const BOX_SIZE: i32 = 13;
const LABEL_GAP: i32 = 4;
const FOCUS_PAD_X: i32 = 2;
const FOCUS_PAD_Y: i32 = 1;
pub struct Checkbox {
rect: Rect,
label: String,
checked: bool,
pressed: bool,
armed: bool,
focused: bool,
enabled: bool,
on_toggle: Option<ToggleHandler>,
}
impl Checkbox {
pub fn new(rect: Rect, label: impl Into<String>) -> Self {
Self {
rect,
label: label.into(),
checked: false,
pressed: false,
armed: false,
focused: false,
enabled: true,
on_toggle: None,
}
}
pub fn checked(mut self, checked: bool) -> Self {
self.checked = checked;
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.pressed = false;
self.armed = false;
}
}
pub fn on_toggle<F>(mut self, handler: F) -> Self
where
F: FnMut(&mut EventCtx, bool) + 'static,
{
self.on_toggle = Some(Box::new(handler));
self
}
pub fn is_checked(&self) -> bool {
self.checked
}
pub fn set_checked(&mut self, checked: bool) {
self.checked = checked;
}
fn toggle(&mut self, ctx: &mut EventCtx) {
self.checked = !self.checked;
ctx.request_paint();
if let Some(handler) = self.on_toggle.as_mut() {
handler(ctx, self.checked);
}
}
fn box_rect(&self) -> Rect {
let y = self.rect.y + (self.rect.h - BOX_SIZE).max(0) / 2;
Rect::new(self.rect.x, y, BOX_SIZE, BOX_SIZE)
}
}
impl Widget for Checkbox {
fn bounds(&self) -> Rect {
self.rect
}
fn paint(&mut self, painter: &mut Painter, theme: &Theme) {
let box_rect = self.box_rect();
let pressed_visual = self.pressed && self.armed;
painter.fill_rect(
box_rect.inset(1),
if pressed_visual {
theme.face
} else {
theme.background
},
);
painter.stroke_rect(box_rect, theme.border);
let fg = if self.enabled {
theme.text
} else {
theme.disabled_text
};
if self.checked {
draw_check(painter, box_rect, fg);
}
let text_size = theme.font_size;
let measured = painter.measure_text(&self.label, text_size);
let text_x = box_rect.right() + LABEL_GAP;
let text_y = self.rect.y + ((self.rect.h - measured.h).max(0)) / 2;
painter.text(text_x, text_y, &self.label, text_size, fg);
if self.focused && self.enabled {
let focus_rect = Rect::new(
text_x - FOCUS_PAD_X,
text_y - FOCUS_PAD_Y,
measured.w + 2 * FOCUS_PAD_X,
measured.h + 2 * FOCUS_PAD_Y,
);
draw_focus_rect(painter, focus_rect, 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.pressed = true;
self.armed = true;
ctx.request_focus();
ctx.request_paint();
}
Event::PointerMove { pos } if self.pressed => {
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.pressed => {
let fire = self.armed && self.rect.contains(*pos);
self.pressed = false;
self.armed = false;
ctx.request_paint();
if fire {
self.toggle(ctx);
}
}
Event::PointerLeave if self.armed => {
self.armed = false;
ctx.request_paint();
}
Event::KeyDown { key, modifiers }
if self.focused
&& !modifiers.has_command()
&& matches!(key, Key::Named(NamedKey::Space)) =>
{
self.toggle(ctx);
ctx.consume_event();
}
_ => {}
}
}
fn captures_pointer(&self) -> bool {
self.pressed
}
fn focusable(&self) -> bool {
self.enabled
}
fn set_focused(&mut self, focused: bool) {
self.focused = focused;
}
fn layout(&mut self, bounds: Rect) {
self.rect = bounds;
}
}
fn draw_check(painter: &mut Painter, box_rect: Rect, color: Color) {
const PATTERN: &[&[u8]] = &[
b" X ",
b" XX ",
b" XXX ",
b" X XXX ",
b" XX XXX ",
b" XXXXX ",
b" XXX ",
b" X ",
];
let offset_y = 3;
for (row, line) in PATTERN.iter().enumerate() {
for (col, byte) in line.iter().enumerate() {
if *byte == b'X' {
painter.pixel(
box_rect.x + col as i32,
box_rect.y + offset_y + row as i32,
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;
}
}