1use crate::controls::MacControlSize;
11use crate::layout::subpixel_snap_rect;
12use crate::theme::MacTheme;
13use egui::{
14 CornerRadius, FontId, Key, Modifiers, Pos2, Rect, Response, Sense, Stroke, StrokeKind, Ui,
15 Vec2, Widget, WidgetText,
16};
17
18pub struct MacCheckbox<'a> {
37 checked: &'a mut bool,
38 label: WidgetText,
39 mixed: bool,
40 control_size: MacControlSize,
41 fixed_rect: Option<Rect>,
42 enabled: bool,
43 theme: Option<&'a MacTheme>,
44}
45
46impl<'a> MacCheckbox<'a> {
47 pub fn new(checked: &'a mut bool, label: impl Into<WidgetText>) -> Self {
49 Self {
50 checked,
51 label: label.into(),
52 mixed: false,
53 control_size: MacControlSize::Regular,
54 fixed_rect: None,
55 enabled: true,
56 theme: None,
57 }
58 }
59
60 pub fn mixed(mut self, mixed: bool) -> Self {
62 self.mixed = mixed;
63 self
64 }
65
66 pub fn control_size(mut self, size: MacControlSize) -> Self {
68 self.control_size = size;
69 self
70 }
71
72 pub fn fixed_rect(mut self, rect: Rect) -> Self {
74 self.fixed_rect = Some(rect);
75 self
76 }
77
78 pub fn enabled(mut self, enabled: bool) -> Self {
80 self.enabled = enabled;
81 self
82 }
83
84 pub fn theme(mut self, theme: &'a MacTheme) -> Self {
86 self.theme = Some(theme);
87 self
88 }
89
90 pub fn box_size(&self) -> f32 {
92 let theme_cow = MacTheme::resolve_or_default(self.theme);
93 let cm = theme_cow.checkbox();
94 match self.control_size {
95 MacControlSize::Regular | MacControlSize::Large => cm.regular.box_size.points(),
96 MacControlSize::Small | MacControlSize::CompactInfobox => cm.small.box_size.points(),
97 MacControlSize::Mini => cm.mini.box_size.points(),
98 }
99 }
100
101 pub fn show(self, ui: &mut Ui) -> Response {
103 ui.add(self)
104 }
105}
106
107impl<'a> Widget for MacCheckbox<'a> {
108 fn ui(self, ui: &mut Ui) -> Response {
109 let theme_cow = MacTheme::resolve(self.theme, ui.ctx());
110 let theme: &MacTheme = &theme_cow;
111 let cm = theme.checkbox();
112 let lm = theme.label_metrics();
113
114 let (sz_metrics, ascender, cell_h) = match self.control_size {
115 MacControlSize::Regular | MacControlSize::Large => (
116 &cm.regular,
117 lm.regular_ascender.points(),
118 lm.regular_height.points(),
119 ),
120 MacControlSize::Small | MacControlSize::CompactInfobox => (
121 &cm.small,
122 lm.small_ascender.points(),
123 lm.small_height.points(),
124 ),
125 MacControlSize::Mini => (
126 &cm.mini,
127 lm.mini_ascender.points(),
128 lm.mini_height.points(),
129 ),
130 };
131
132 let box_sz = sz_metrics.box_size.points();
133 let corner_r = sz_metrics.corner_radius.points();
134 let gap = sz_metrics.label_gap.points();
135 let font_sz = sz_metrics.font_size.points();
136 let check_stroke_w = sz_metrics.checkmark_stroke.points();
137 let baseline_from_top = sz_metrics.first_baseline_offset_from_top.points();
138 let border_w = cm.border_width.points();
139
140 let enabled = self.enabled && ui.is_enabled();
141 let alpha = if enabled {
142 1.0
143 } else {
144 theme.disabled_opacity()
145 };
146 let probe_pos = self.fixed_rect.map_or_else(
147 || ui.next_widget_position(),
148 |r| Pos2::new(r.left() + (box_sz * 0.5).min(r.width() * 0.5), r.center().y),
149 );
150 let surface_bg = theme.active_surface_at(ui, probe_pos);
151 let text_color = if enabled {
152 theme.label_on(surface_bg)
153 } else {
154 theme.tertiary_label_on(surface_bg)
155 };
156 let label_str = self.label.text();
157 let galley = if label_str.is_empty() {
158 None
159 } else {
160 Some(theme.layout_tracked_galley(
161 ui,
162 label_str,
163 FontId::proportional(font_sz),
164 text_color,
165 f32::INFINITY,
166 ))
167 };
168
169 let desired_w = match (&self.fixed_rect, &galley) {
170 (Some(r), _) => r.width(),
171 (None, Some(g)) => box_sz + gap + g.size().x,
172 (None, None) => box_sz,
173 };
174 let desired_h = self
175 .fixed_rect
176 .map(|r| r.height())
177 .unwrap_or_else(|| box_sz.max(cell_h));
178
179 let sense = if enabled {
180 Sense::click()
181 } else {
182 Sense::hover()
183 };
184 let ppp = ui.ctx().pixels_per_point();
185 let (rect, mut response) = if let Some(fixed) = self.fixed_rect {
186 let snapped = subpixel_snap_rect(fixed, ppp);
187 let id = ui.next_auto_id().with("mac_checkbox_fixed");
188 let resp = ui.interact(snapped, id, sense);
189 (snapped, resp)
190 } else {
191 let (r, resp) = ui.allocate_exact_size(Vec2::new(desired_w, desired_h), sense);
192 (subpixel_snap_rect(r, ppp), resp)
193 };
194
195 if enabled {
196 let key_toggle = response.has_focus()
197 && ui.input_mut(|i| i.consume_key(Modifiers::NONE, Key::Space));
198 if response.clicked() || key_toggle {
199 *self.checked = !*self.checked;
200 response.mark_changed();
201 }
202 }
203
204 let painter = ui.painter_at(rect);
205 let box_rect = subpixel_snap_rect(
206 Rect::from_min_size(
207 Pos2::new(rect.left(), rect.center().y - box_sz * 0.5),
208 Vec2::splat(box_sz),
209 ),
210 ppp,
211 );
212
213 if enabled && response.has_focus() {
214 theme.paint_focus_ring(&painter, box_rect, corner_r);
215 }
216
217 let is_active = *self.checked || self.mixed;
218 let fill_color = if is_active {
219 theme.control_accent().gamma_multiply(alpha)
220 } else if response.is_pointer_button_down_on() && enabled {
221 theme.control().gamma_multiply(alpha)
222 } else {
223 theme.control_background().gamma_multiply(alpha)
224 };
225
226 let stroke = if is_active {
227 Stroke::NONE
228 } else {
229 Stroke::new(border_w, theme.separator().gamma_multiply(alpha))
230 };
231
232 painter.rect(
233 box_rect,
234 CornerRadius::same(corner_r.round() as u8),
235 fill_color,
236 stroke,
237 StrokeKind::Inside,
238 );
239
240 let glyph_stroke = Stroke::new(check_stroke_w, theme.on_accent_text().gamma_multiply(alpha));
241 if self.mixed {
242 let inset = box_sz * cm.mixed_bar_inset_ratio.points();
243 let cy = box_rect.center().y;
244 painter.line_segment(
245 [
246 Pos2::new(box_rect.left() + inset, cy),
247 Pos2::new(box_rect.right() - inset, cy),
248 ],
249 glyph_stroke,
250 );
251 } else if *self.checked {
252 let p1 = Pos2::new(
253 box_rect.left() + box_sz * cm.check_p1_x.points(),
254 box_rect.top() + box_sz * cm.check_p1_y.points(),
255 );
256 let p2 = Pos2::new(
257 box_rect.left() + box_sz * cm.check_p2_x.points(),
258 box_rect.top() + box_sz * cm.check_p2_y.points(),
259 );
260 let p3 = Pos2::new(
261 box_rect.left() + box_sz * cm.check_p3_x.points(),
262 box_rect.top() + box_sz * cm.check_p3_y.points(),
263 );
264 painter.line_segment([p1, p2], glyph_stroke);
265 painter.line_segment([p2, p3], glyph_stroke);
266 }
267
268 if let Some(galley) = galley {
269 let text_y = theme.baseline_text_y(rect, baseline_from_top, ascender, ppp);
270 let text_pos = Pos2::new(box_rect.right() + gap, text_y);
271 painter.galley(text_pos, galley, text_color);
272 }
273
274 response
275 }
276}