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 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 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 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 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 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}