1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
//! Checkbox component for gpuikit
use crate::a11y::FocusNavigation;
use crate::elements::form;
use crate::layout::h_stack;
use crate::theme::{ActiveTheme, ControlSize, Themeable};
use crate::traits::control_sized::ControlSized;
use crate::traits::disableable::Disableable;
use crate::traits::labelable::Labelable;
use crate::traits::selectable::Selectable;
use gpui::{
div, prelude::*, px, App, Context, ElementId, EventEmitter, InteractiveElement, IntoElement,
MouseButton, ParentElement, Render, RenderOnce, SharedString, StatefulInteractiveElement,
Styled, Window,
};
/// Event emitted when the checkbox state changes
pub struct CheckboxChanged {
pub checked: bool,
}
/// The three states the box itself can be drawn in.
///
/// `Checkbox` carries `checked` and `indeterminate` as two booleans for
/// backwards compatibility; this is the same information as one value, and it
/// is what a caller that draws boxes it does not own — a table's selection
/// column — passes to [`checkbox_box`].
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum CheckState {
/// Nothing this box stands for is checked.
#[default]
Unchecked,
/// Everything this box stands for is checked.
Checked,
/// Some but not all of it is. Drawn as a bar rather than a tick.
Indeterminate,
}
impl CheckState {
/// The state a box standing for `total` things of which `selected` are
/// checked should be drawn in.
///
/// Zero of zero is `Unchecked`: a box over nothing is not "all of it".
pub fn from_count(selected: usize, total: usize) -> Self {
if selected == 0 || total == 0 {
CheckState::Unchecked
} else if selected >= total {
CheckState::Checked
} else {
CheckState::Indeterminate
}
}
/// What a click on a box in this state asks for.
///
/// Indeterminate becomes `Checked` rather than `Unchecked` — the
/// convention every platform toolkit follows, on the reasoning that a
/// partial selection was arrived at by adding rather than by removing.
pub fn toggled(self) -> Self {
match self {
CheckState::Checked => CheckState::Unchecked,
CheckState::Unchecked | CheckState::Indeterminate => CheckState::Checked,
}
}
/// Whether this state reads as "on" to a caller storing a bool.
pub fn is_checked(self) -> bool {
matches!(self, CheckState::Checked)
}
/// Whether this state is the partial one.
pub fn is_indeterminate(self) -> bool {
matches!(self, CheckState::Indeterminate)
}
}
/// The box a checkbox draws, without the row, the label or the click handling.
///
/// `Checkbox` is an entity, so an element that draws one box per row — a
/// table's selection column — cannot mint one per frame. Without this it would
/// draw its own approximation of the box instead, which is exactly the drift
/// [`ControlMetrics::track`](crate::theme::ControlMetrics::track) exists to
/// prevent for `Switch` and `Toggle`. `Checkbox::render` goes through this, so
/// there is one box in the crate.
#[derive(IntoElement)]
pub struct CheckboxBox {
state: CheckState,
disabled: bool,
size: ControlSize,
}
impl CheckboxBox {
/// A box in the given state, on the default rung.
pub fn new(state: CheckState) -> Self {
Self {
state,
disabled: false,
size: ControlSize::default(),
}
}
/// Draws the box in its disabled colours. Does not affect interaction —
/// the box has none; that belongs to whatever contains it.
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
}
/// Convenience function to create a bare checkbox box.
pub fn checkbox_box(state: CheckState) -> CheckboxBox {
CheckboxBox::new(state)
}
impl ControlSized for CheckboxBox {
fn control_size(mut self, size: ControlSize) -> Self {
self.size = size;
self
}
}
impl RenderOnce for CheckboxBox {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme();
let metrics = theme.control(self.size);
let disabled = self.disabled;
let checked = self.state.is_checked();
let indeterminate = self.state.is_indeterminate();
// The row is the rung; the box is the ink inside it. Sizing the row
// off the box is what put the checkbox on a rung of its own.
let box_size = metrics.ink;
let box_bg = if disabled {
theme.surface_tertiary()
} else if checked || indeterminate {
theme.accent()
} else {
theme.surface()
};
let box_border = if disabled {
theme.border_subtle()
} else if checked || indeterminate {
theme.accent()
} else {
theme.border()
};
let check_color = if disabled {
theme.fg_disabled()
} else {
theme.surface()
};
div()
.size(box_size)
.flex_none()
.flex()
.items_center()
.justify_center()
.bg(box_bg)
.border_1()
.border_color(box_border)
.rounded(metrics.radius)
.when(!disabled, |this| {
this.hover(|style| {
style.border_color(if checked || indeterminate {
theme.accent()
} else {
theme.border_secondary()
})
})
})
.when(checked && !indeterminate, |this| {
// The glyph sizes off the box, not off a constant, so it stays
// proportional on every rung.
this.child(
div()
.text_size(box_size * 0.75)
.line_height(box_size)
.text_color(check_color)
.child("✓"),
)
})
.when(indeterminate, |this| {
this.child(
div()
.w(box_size * 0.5)
.h(px(2.))
.bg(check_color)
.rounded(px(1.)),
)
})
}
}
/// A checkbox component for toggling boolean values
pub struct Checkbox {
id: ElementId,
label: Option<SharedString>,
checked: bool,
disabled: bool,
indeterminate: bool,
size: ControlSize,
}
impl EventEmitter<CheckboxChanged> for Checkbox {}
impl Checkbox {
pub fn new(id: impl Into<ElementId>, checked: bool) -> Self {
Self {
id: id.into(),
label: None,
checked,
disabled: false,
indeterminate: false,
size: ControlSize::default(),
}
}
pub fn label(mut self, label: impl Into<SharedString>) -> Self {
self.label = Some(label.into());
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
pub fn indeterminate(mut self, indeterminate: bool) -> Self {
self.indeterminate = indeterminate;
self
}
pub fn is_checked(&self) -> bool {
self.checked
}
pub fn is_indeterminate(&self) -> bool {
self.indeterminate
}
pub fn set_checked(&mut self, checked: bool, cx: &mut Context<Self>) {
if self.checked != checked {
self.checked = checked;
self.indeterminate = false;
cx.emit(CheckboxChanged {
checked: self.checked,
});
cx.notify();
}
}
pub fn toggle(&mut self, cx: &mut Context<Self>) {
self.set_checked(!self.checked, cx);
}
fn on_click(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
if !self.disabled {
self.toggle(cx);
}
}
}
impl Render for Checkbox {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let theme = cx.theme();
let metrics = theme.control(self.size);
// The first control in the crate to adopt `crate::elements::form`'s
// ambient context, and the whole of what adopting it costs: a
// `Fieldset` or a `Field` around this checkbox disables it without
// anything being threaded here by hand.
let disabled = form::disabled_here(self.disabled);
// The handle an enclosing `Field`'s label click lands on. Tracking it
// is what turns that click into focus on this control rather than on a
// handle nothing watches.
let field_focus = form::focus_handle_here();
let label = self.label.clone();
let state = if self.indeterminate {
CheckState::Indeterminate
} else if self.checked {
CheckState::Checked
} else {
CheckState::Unchecked
};
h_stack()
.id(self.id.clone())
// `debug_selector` compiles to a no-op that never calls its
// closure unless gpui's `test-support` is on, so a consumer pays
// nothing for it — the same trade `src/elements/table.rs` makes.
// It is what makes "a fieldset disabled this checkbox" assertable:
// gpui has no `aria_disabled`, so the only observable difference is
// whether a click on the row does anything.
.debug_selector({
let id = self.id.clone();
move || format!("gpuikit-checkbox-{id:?}")
})
.when_some(field_focus, |this, handle| {
// `track_focus` does not make the handle a tab stop by itself
// — the same thing `a11y::Announce` has to do for a
// caller-supplied handle.
this.track_focus(&handle.tab_stop(true))
.moves_focus_on_tab()
})
.h(metrics.height)
// A label outside the control's own box wants more room than the
// gap between an icon and a label inside one.
.gap(metrics.gap * 2.0)
.items_center()
.when(!disabled, |this| {
this.cursor_pointer()
.on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default())
.on_click(cx.listener(|this, _, window, cx| {
this.on_click(window, cx);
}))
})
.when(disabled, |this| this.cursor_not_allowed().opacity(0.65))
.child(
checkbox_box(state)
.disabled(disabled)
.control_size(self.size),
)
.when_some(label, |this, label| {
this.child(
div()
.text_size(metrics.text_size)
.line_height(metrics.line_height)
.text_color(if disabled {
theme.fg_disabled()
} else {
theme.fg()
})
.child(label),
)
})
}
}
/// Convenience function to create a checkbox
pub fn checkbox(id: impl Into<ElementId>, checked: bool) -> Checkbox {
Checkbox::new(id, checked)
}
impl Disableable for Checkbox {
fn is_disabled(&self) -> bool {
self.disabled
}
fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
}
impl Selectable for Checkbox {
fn is_selected(&self) -> bool {
self.checked
}
fn selected(mut self, selected: bool) -> Self {
self.checked = selected;
self
}
}
impl ControlSized for Checkbox {
fn control_size(mut self, size: ControlSize) -> Self {
self.size = size;
self
}
}
impl Labelable for Checkbox {
fn label(mut self, label: impl Into<SharedString>) -> Self {
self.label = Some(label.into());
self
}
}