Skip to main content

bexa_ui_core/widgets/
checkbox.rs

1use glyphon::Metrics;
2use glyphon::cosmic_text::Align;
3use taffy::prelude::*;
4use winit::event::{ElementState, KeyEvent, MouseButton, WindowEvent};
5use winit::keyboard::{Key, ModifiersState, NamedKey};
6
7use crate::framework::{DrawContext, EventContext, Widget};
8use crate::icons;
9use crate::signal::{Signal, SetSignal};
10
11pub struct Checkbox {
12    label: String,
13    checked: Signal<bool>,
14    set_checked: SetSignal<bool>,
15    metrics: Metrics,
16    box_size: f32,
17    gap: f32,
18    border_radius: f32,
19    // Colors
20    box_bg: [f32; 4],
21    box_checked_bg: [f32; 4],
22    box_border: [f32; 4],
23    check_color: [u8; 3],
24    text_color: [u8; 3],
25    // State
26    hover: bool,
27    focus: bool,
28}
29
30impl Checkbox {
31    pub fn new(
32        label: impl Into<String>,
33        checked: Signal<bool>,
34        set_checked: SetSignal<bool>,
35        metrics: Metrics,
36    ) -> Self {
37        Self {
38            label: label.into(),
39            checked,
40            set_checked,
41            metrics,
42            box_size: 20.0,
43            gap: 8.0,
44            border_radius: 4.0,
45            box_bg: [0.16, 0.28, 0.38, 1.0],
46            box_checked_bg: [0.20, 0.65, 0.85, 1.0],
47            box_border: [0.4, 0.55, 0.7, 1.0],
48            check_color: [255, 255, 255],
49            text_color: [230, 230, 230],
50            hover: false,
51            focus: false,
52        }
53    }
54
55    pub fn with_box_size(mut self, size: f32) -> Self {
56        self.box_size = size;
57        self
58    }
59
60    pub fn with_gap(mut self, gap: f32) -> Self {
61        self.gap = gap;
62        self
63    }
64
65    pub fn with_border_radius(mut self, radius: f32) -> Self {
66        self.border_radius = radius;
67        self
68    }
69
70    pub fn with_colors(
71        mut self,
72        box_bg: [f32; 4],
73        box_checked_bg: [f32; 4],
74        box_border: [f32; 4],
75        check_color: [u8; 3],
76    ) -> Self {
77        self.box_bg = box_bg;
78        self.box_checked_bg = box_checked_bg;
79        self.box_border = box_border;
80        self.check_color = check_color;
81        self
82    }
83
84    pub fn with_text_color(mut self, color: [u8; 3]) -> Self {
85        self.text_color = color;
86        self
87    }
88
89    fn toggle(&self) {
90        let current = self.checked.get();
91        self.set_checked.set(!current);
92    }
93
94    fn hit_test(&self, layout: &Layout, x: f32, y: f32) -> bool {
95        x >= layout.location.x
96            && x <= layout.location.x + layout.size.width
97            && y >= layout.location.y
98            && y <= layout.location.y + layout.size.height
99    }
100}
101
102impl Widget for Checkbox {
103    fn style(&self) -> Style {
104        let height = self.box_size.max(self.metrics.line_height) + 8.0;
105        Style {
106            size: Size {
107                width: Dimension::Auto,
108                height: Dimension::Length(height),
109            },
110            flex_shrink: 0.0,
111            ..Default::default()
112        }
113    }
114
115    fn draw(&self, ctx: &mut DrawContext) {
116        let layout = ctx.layout;
117        let is_checked = self.checked.get();
118
119        // 1. Draw the box
120        let box_x = layout.location.x + 4.0;
121        let box_y = layout.location.y + (layout.size.height - self.box_size) / 2.0;
122        let bg = if is_checked { self.box_checked_bg } else { self.box_bg };
123        let border_w = if self.focus { 2.0 } else { 1.0 };
124        let border_c = if self.focus {
125            [0.3, 0.6, 0.9, 1.0]
126        } else {
127            self.box_border
128        };
129        ctx.renderer.fill_rect_styled(
130            (box_x, box_y, self.box_size, self.box_size),
131            bg,
132            self.border_radius,
133            border_w,
134            border_c,
135        );
136
137        // 2. Draw checkmark icon if checked
138        if is_checked {
139            let icon_size = self.box_size * 0.7;
140            let icon_metrics = Metrics::new(icon_size, icon_size);
141            let icon_x = box_x + (self.box_size - icon_size) / 2.0;
142            let icon_y = box_y + (self.box_size - icon_size) / 2.0;
143            ctx.renderer.draw_text_with_font(
144                icons::CHECK,
145                (icon_x, icon_y),
146                self.check_color,
147                (icon_size, icon_size),
148                icon_metrics,
149                Align::Center,
150                icons::NERD_FONT_FAMILY,
151            );
152        }
153
154        // 3. Draw label text
155        let text_x = box_x + self.box_size + self.gap;
156        let text_y = layout.location.y + (layout.size.height - self.metrics.line_height) / 2.0;
157        let text_w = (layout.size.width - (text_x - layout.location.x)).max(0.0);
158        ctx.renderer.draw_text(
159            &self.label,
160            (text_x, text_y),
161            self.text_color,
162            (text_w, self.metrics.line_height),
163            self.metrics,
164            Align::Left,
165        );
166    }
167
168    fn handle_event(&mut self, ctx: &mut EventContext) -> bool {
169        let layout = ctx.layout;
170        let mut changed = false;
171        match ctx.event {
172            WindowEvent::CursorMoved { position, .. } => {
173                let over = self.hit_test(layout, position.x as f32, position.y as f32);
174                if over != self.hover {
175                    self.hover = over;
176                    changed = true;
177                }
178            }
179            WindowEvent::MouseInput {
180                state: ElementState::Pressed,
181                button: MouseButton::Left,
182                ..
183            } => {
184                if self.hover {
185                    self.toggle();
186                    changed = true;
187                }
188            }
189            _ => {}
190        }
191        changed
192    }
193
194    fn handle_key_event(&mut self, event: &KeyEvent, _modifiers: ModifiersState) -> bool {
195        if event.state != ElementState::Pressed {
196            return false;
197        }
198        match &event.logical_key {
199            Key::Named(NamedKey::Space) | Key::Named(NamedKey::Enter) => {
200                self.toggle();
201                true
202            }
203            _ => false,
204        }
205    }
206
207    fn is_focusable(&self) -> bool {
208        true
209    }
210
211    fn set_focus(&mut self, focused: bool) {
212        self.focus = focused;
213    }
214
215    fn activate(&mut self) {
216        self.toggle();
217    }
218}