Skip to main content

agg_gui/widgets/
checkbox.rs

1//! `Checkbox` — a boolean toggle with a label.
2//!
3//! # Composition
4//!
5//! The checkbox label is rendered through a [`Label`] child with backbuffer
6//! caching enabled (the default).  The box + checkmark are drawn directly via
7//! path commands; only the text goes through the Label path.
8//!
9//! ```text
10//! Checkbox (box + checkmark drawn via paths)
11//!   └── Label (text, backbuffered)
12//! ```
13
14use std::cell::Cell;
15use std::rc::Rc;
16use std::sync::Arc;
17
18use crate::color::Color;
19use crate::event::{Event, EventResult, Key, MouseButton};
20use crate::geometry::{Rect, Size};
21use crate::draw_ctx::DrawCtx;
22use crate::layout_props::{HAnchor, Insets, VAnchor, WidgetBase};
23use crate::text::Font;
24use crate::widget::{Widget, paint_subtree};
25use crate::widgets::label::Label;
26
27const BOX_SIZE: f64 = 16.0;
28const GAP: f64 = 8.0;
29
30/// A boolean toggle with a square box and a text label.
31pub struct Checkbox {
32    bounds: Rect,
33    children: Vec<Box<dyn Widget>>, // always empty — label stored separately
34    base: WidgetBase,
35    font: Arc<Font>,
36    font_size: f64,
37    /// Explicit label color override.  `None` → follow active visuals.
38    label_color: Option<Color>,
39    checked: bool,
40    /// When set, this cell is the authoritative checked state.  `paint` reads
41    /// from it and `toggle` writes to it so the checkbox stays in sync with
42    /// external state changes (e.g. a window's close button setting it to false).
43    state_cell: Option<Rc<Cell<bool>>>,
44    hovered: bool,
45    focused: bool,
46    on_change: Option<Box<dyn FnMut(bool)>>,
47    /// Backbuffered text label — painted manually so we can position it.
48    label_widget: Label,
49}
50
51impl Checkbox {
52    pub fn new(label: impl Into<String>, font: Arc<Font>, checked: bool) -> Self {
53        let label_str: String = label.into();
54        let font_size = 14.0;
55        let label_widget = Label::new(&label_str, Arc::clone(&font))
56            .with_font_size(font_size);
57        Self {
58            bounds: Rect::default(),
59            children: Vec::new(),
60            base: WidgetBase::new(),
61            font,
62            font_size,
63            label_color: None,
64            checked,
65            state_cell: None,
66            hovered: false,
67            focused: false,
68            on_change: None,
69            label_widget,
70        }
71    }
72
73    pub fn with_font_size(mut self, size: f64) -> Self {
74        self.font_size = size;
75        self.label_widget = Label::new(
76            self.label_widget.text_str(),
77            Arc::clone(&self.font),
78        ).with_font_size(size);
79        self
80    }
81    pub fn with_label_color(mut self, c: Color) -> Self { self.label_color = Some(c); self }
82
83    /// Bind checked state to a shared cell.
84    ///
85    /// When set, `paint` reads from the cell (so external changes — e.g. a
86    /// window's close button — are reflected immediately), and `toggle` writes
87    /// to it so both directions stay in sync.
88    pub fn with_state_cell(mut self, cell: Rc<Cell<bool>>) -> Self {
89        self.state_cell = Some(cell);
90        self
91    }
92
93    pub fn with_margin(mut self, m: Insets)    -> Self { self.base.margin   = m; self }
94    pub fn with_h_anchor(mut self, h: HAnchor) -> Self { self.base.h_anchor = h; self }
95    pub fn with_v_anchor(mut self, v: VAnchor) -> Self { self.base.v_anchor = v; self }
96    pub fn with_min_size(mut self, s: Size)    -> Self { self.base.min_size = s; self }
97    pub fn with_max_size(mut self, s: Size)    -> Self { self.base.max_size = s; self }
98
99    pub fn on_change(mut self, cb: impl FnMut(bool) + 'static) -> Self {
100        self.on_change = Some(Box::new(cb));
101        self
102    }
103
104    pub fn checked(&self) -> bool { self.checked }
105    pub fn set_checked(&mut self, v: bool) { self.checked = v; }
106
107    fn toggle(&mut self) {
108        let new_val = !self.effective_checked();
109        self.checked = new_val;
110        if let Some(ref cell) = self.state_cell { cell.set(new_val); }
111        if let Some(cb) = self.on_change.as_mut() { cb(new_val); }
112    }
113
114    /// Returns the authoritative checked state: the cell value if bound, else
115    /// the internal `checked` field.
116    #[inline]
117    fn effective_checked(&self) -> bool {
118        if let Some(ref cell) = self.state_cell { cell.get() } else { self.checked }
119    }
120}
121
122impl Widget for Checkbox {
123    fn type_name(&self) -> &'static str { "Checkbox" }
124    fn bounds(&self) -> Rect { self.bounds }
125    fn set_bounds(&mut self, b: Rect) { self.bounds = b; }
126    fn children(&self) -> &[Box<dyn Widget>] { &self.children }
127    fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> { &mut self.children }
128
129    fn is_focusable(&self) -> bool { true }
130
131    fn margin(&self)   -> Insets  { self.base.margin }
132    fn h_anchor(&self) -> HAnchor { self.base.h_anchor }
133    fn v_anchor(&self) -> VAnchor { self.base.v_anchor }
134    fn min_size(&self) -> Size    { self.base.min_size }
135    fn max_size(&self) -> Size    { self.base.max_size }
136
137    fn layout(&mut self, available: Size) -> Size {
138        let h = BOX_SIZE.max(self.font_size * 1.5);
139        self.bounds = Rect::new(0.0, 0.0, available.width, h);
140        // Layout the label within the remaining width after the box + gap.
141        let label_avail_w = (available.width - BOX_SIZE - GAP).max(0.0);
142        let s = self.label_widget.layout(Size::new(label_avail_w, h));
143        self.label_widget.set_bounds(Rect::new(0.0, 0.0, s.width, s.height));
144        Size::new(available.width, h)
145    }
146
147    fn paint(&mut self, ctx: &mut dyn DrawCtx) {
148        let v = ctx.visuals();
149        let h = self.bounds.height;
150        let box_y = (h - BOX_SIZE) * 0.5;
151
152        // Focus ring
153        if self.focused {
154            ctx.set_stroke_color(v.accent_focus);
155            ctx.set_line_width(2.0);
156            ctx.begin_path();
157            ctx.rounded_rect(-1.5, box_y - 1.5, BOX_SIZE + 3.0, BOX_SIZE + 3.0, 4.0);
158            ctx.stroke();
159        }
160
161        let checked = self.effective_checked();
162
163        // Box background
164        let bg = if checked {
165            v.accent
166        } else if self.hovered {
167            v.widget_bg_hovered
168        } else {
169            v.widget_bg
170        };
171        ctx.set_fill_color(bg);
172        ctx.begin_path();
173        ctx.rounded_rect(0.0, box_y, BOX_SIZE, BOX_SIZE, 3.0);
174        ctx.fill();
175
176        // Box border
177        let border = if checked { v.widget_stroke_active } else { v.widget_stroke };
178        ctx.set_stroke_color(border);
179        ctx.set_line_width(1.5);
180        ctx.begin_path();
181        ctx.rounded_rect(0.0, box_y, BOX_SIZE, BOX_SIZE, 3.0);
182        ctx.stroke();
183
184        // Checkmark — coordinates in Y-up space (origin = box bottom-left).
185        if checked {
186            ctx.set_stroke_color(Color::white());
187            ctx.set_line_width(2.0);
188            ctx.begin_path();
189            let bx = 0.0;
190            let by = box_y;
191            ctx.move_to(bx + 3.0,              by + BOX_SIZE * 0.55);
192            ctx.line_to(bx + BOX_SIZE * 0.42,  by + BOX_SIZE * 0.28);
193            ctx.line_to(bx + BOX_SIZE - 3.0,   by + BOX_SIZE * 0.75);
194            ctx.stroke();
195        }
196
197        // Label — rendered through backbuffered Label child.
198        let label_color = self.label_color.unwrap_or(v.text_color);
199        self.label_widget.set_color(label_color);
200
201        let lw = self.label_widget.bounds().width;
202        let lh = self.label_widget.bounds().height;
203        let lx = BOX_SIZE + GAP;
204        let ly = (h - lh) * 0.5;
205        self.label_widget.set_bounds(Rect::new(lx, ly, lw, lh));
206
207        ctx.save();
208        ctx.translate(lx, ly);
209        paint_subtree(&mut self.label_widget, ctx);
210        ctx.restore();
211    }
212
213    fn on_event(&mut self, event: &Event) -> EventResult {
214        match event {
215            Event::MouseMove { pos } => {
216                let was = self.hovered;
217                self.hovered = self.hit_test(*pos);
218                if was != self.hovered { crate::animation::request_tick(); }
219                EventResult::Ignored
220            }
221            Event::MouseDown { button: MouseButton::Left, .. } => {
222                EventResult::Consumed
223            }
224            Event::MouseUp { button: MouseButton::Left, pos, .. } => {
225                if self.hit_test(*pos) {
226                    self.toggle();
227                    crate::animation::request_tick();
228                }
229                EventResult::Consumed
230            }
231            Event::KeyDown { key: Key::Char(' '), .. } => {
232                self.toggle();
233                crate::animation::request_tick();
234                EventResult::Consumed
235            }
236            Event::FocusGained => {
237                self.focused = true;
238                crate::animation::request_tick();
239                EventResult::Ignored
240            }
241            Event::FocusLost   => {
242                self.focused = false;
243                crate::animation::request_tick();
244                EventResult::Ignored
245            }
246            _ => EventResult::Ignored,
247        }
248    }
249}