mirror of https://github.com/linebender/xilem
306 lines
9.5 KiB
Rust
306 lines
9.5 KiB
Rust
// Copyright 2019 the Xilem Authors and the Druid Authors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
//! A checkbox widget.
|
|
|
|
use accesskit::{Node, Role, Toggled};
|
|
use smallvec::{smallvec, SmallVec};
|
|
use tracing::{trace, trace_span, Span};
|
|
use vello::kurbo::{Affine, BezPath, Cap, Join, Size, Stroke};
|
|
use vello::Scene;
|
|
|
|
use crate::core::{
|
|
AccessCtx, AccessEvent, Action, ArcStr, BoxConstraints, EventCtx, LayoutCtx, PaintCtx,
|
|
PointerEvent, QueryCtx, RegisterCtx, TextEvent, Update, UpdateCtx, Widget, WidgetId, WidgetMut,
|
|
WidgetPod,
|
|
};
|
|
use crate::theme;
|
|
use crate::util::{fill_lin_gradient, stroke, UnitPoint};
|
|
use crate::widgets::Label;
|
|
|
|
/// A checkbox that can be toggled.
|
|
///
|
|
#[doc = crate::include_screenshot!("widget/screenshots/masonry__widget__checkbox__tests__hello_checked.png", "Checkbox with checked state.")]
|
|
pub struct Checkbox {
|
|
checked: bool,
|
|
label: WidgetPod<Label>,
|
|
}
|
|
|
|
impl Checkbox {
|
|
/// Create a new `Checkbox` with a text label.
|
|
pub fn new(checked: bool, text: impl Into<ArcStr>) -> Self {
|
|
Self {
|
|
checked,
|
|
label: WidgetPod::new(Label::new(text)),
|
|
}
|
|
}
|
|
|
|
/// Create a new `Checkbox` with the given label.
|
|
pub fn from_label(checked: bool, label: Label) -> Self {
|
|
Self {
|
|
checked,
|
|
label: WidgetPod::new(label),
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- MARK: WIDGETMUT ---
|
|
impl Checkbox {
|
|
pub fn set_checked(self: &mut WidgetMut<'_, Self>, checked: bool) {
|
|
self.widget.checked = checked;
|
|
// Checked state impacts appearance and accessibility node
|
|
self.ctx.request_render();
|
|
}
|
|
|
|
/// Set the text.
|
|
///
|
|
/// We enforce this to be an `ArcStr` to make the allocation explicit.
|
|
pub fn set_text(self: &mut WidgetMut<'_, Self>, new_text: ArcStr) {
|
|
Label::set_text(&mut self.label_mut(), new_text);
|
|
}
|
|
|
|
pub fn label_mut<'t>(self: &'t mut WidgetMut<'_, Self>) -> WidgetMut<'t, Label> {
|
|
self.ctx.get_mut(&mut self.widget.label)
|
|
}
|
|
}
|
|
|
|
// --- MARK: IMPL WIDGET ---
|
|
impl Widget for Checkbox {
|
|
fn on_pointer_event(&mut self, ctx: &mut EventCtx, event: &PointerEvent) {
|
|
match event {
|
|
PointerEvent::PointerDown(_, _) => {
|
|
if !ctx.is_disabled() {
|
|
ctx.capture_pointer();
|
|
// Checked state impacts appearance and accessibility node
|
|
ctx.request_render();
|
|
trace!("Checkbox {:?} pressed", ctx.widget_id());
|
|
}
|
|
}
|
|
PointerEvent::PointerUp(_, _) => {
|
|
if ctx.is_pointer_capture_target() && ctx.is_hovered() && !ctx.is_disabled() {
|
|
self.checked = !self.checked;
|
|
ctx.submit_action(Action::CheckboxToggled(self.checked));
|
|
trace!("Checkbox {:?} released", ctx.widget_id());
|
|
}
|
|
// Checked state impacts appearance and accessibility node
|
|
ctx.request_render();
|
|
}
|
|
_ => (),
|
|
}
|
|
}
|
|
|
|
fn on_text_event(&mut self, _ctx: &mut EventCtx, _event: &TextEvent) {}
|
|
|
|
fn on_access_event(&mut self, ctx: &mut EventCtx, event: &AccessEvent) {
|
|
if ctx.target() == ctx.widget_id() {
|
|
match event.action {
|
|
accesskit::Action::Click => {
|
|
self.checked = !self.checked;
|
|
ctx.submit_action(Action::CheckboxToggled(self.checked));
|
|
// Checked state impacts appearance and accessibility node
|
|
ctx.request_render();
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn update(&mut self, ctx: &mut UpdateCtx, event: &Update) {
|
|
match event {
|
|
Update::HoveredChanged(_) | Update::FocusChanged(_) | Update::DisabledChanged(_) => {
|
|
ctx.request_paint_only();
|
|
}
|
|
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
fn register_children(&mut self, ctx: &mut RegisterCtx) {
|
|
ctx.register_child(&mut self.label);
|
|
}
|
|
|
|
fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
|
|
let x_padding = theme::WIDGET_CONTROL_COMPONENT_PADDING;
|
|
let check_size = theme::BASIC_WIDGET_HEIGHT;
|
|
|
|
let label_size = ctx.run_layout(&mut self.label, bc);
|
|
ctx.place_child(&mut self.label, (check_size + x_padding, 0.0).into());
|
|
|
|
let desired_size = Size::new(
|
|
check_size + x_padding + label_size.width,
|
|
check_size.max(label_size.height),
|
|
);
|
|
let our_size = bc.constrain(desired_size);
|
|
let baseline =
|
|
ctx.child_baseline_offset(&self.label) + (our_size.height - label_size.height);
|
|
ctx.set_baseline_offset(baseline);
|
|
our_size
|
|
}
|
|
|
|
fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut Scene) {
|
|
let check_size = theme::BASIC_WIDGET_HEIGHT;
|
|
let border_width = 1.;
|
|
|
|
let rect = Size::new(check_size, check_size)
|
|
.to_rect()
|
|
.inset(-border_width / 2.)
|
|
.to_rounded_rect(2.);
|
|
|
|
fill_lin_gradient(
|
|
scene,
|
|
&rect,
|
|
[theme::BACKGROUND_LIGHT, theme::BACKGROUND_DARK],
|
|
UnitPoint::TOP,
|
|
UnitPoint::BOTTOM,
|
|
);
|
|
|
|
let border_color = if ctx.is_hovered() && !ctx.is_disabled() {
|
|
theme::BORDER_LIGHT
|
|
} else {
|
|
theme::BORDER_DARK
|
|
};
|
|
|
|
stroke(scene, &rect, border_color, border_width);
|
|
|
|
if self.checked {
|
|
// Paint the checkmark
|
|
let mut path = BezPath::new();
|
|
path.move_to((4.0, 9.0));
|
|
path.line_to((8.0, 13.0));
|
|
path.line_to((14.0, 5.0));
|
|
|
|
let style = Stroke {
|
|
width: 2.0,
|
|
join: Join::Round,
|
|
miter_limit: 10.0,
|
|
start_cap: Cap::Round,
|
|
end_cap: Cap::Round,
|
|
dash_pattern: Default::default(),
|
|
dash_offset: 0.0,
|
|
};
|
|
|
|
let brush = if ctx.is_disabled() {
|
|
theme::DISABLED_TEXT_COLOR
|
|
} else {
|
|
theme::TEXT_COLOR
|
|
};
|
|
|
|
scene.stroke(&style, Affine::IDENTITY, brush, None, &path);
|
|
}
|
|
}
|
|
|
|
fn accessibility_role(&self) -> Role {
|
|
Role::CheckBox
|
|
}
|
|
|
|
fn accessibility(&mut self, ctx: &mut AccessCtx, node: &mut Node) {
|
|
// IMPORTANT: We don't want to merge this code in practice, because
|
|
// the child label already has a 'name' property.
|
|
// This is more of a proof of concept of `get_raw_ref()`.
|
|
if false {
|
|
let label = ctx.get_raw_ref(&self.label);
|
|
let name = label.widget().text().as_ref().to_string();
|
|
node.set_value(name);
|
|
}
|
|
node.add_action(accesskit::Action::Click);
|
|
if self.checked {
|
|
node.set_toggled(Toggled::True);
|
|
} else {
|
|
node.set_toggled(Toggled::False);
|
|
}
|
|
}
|
|
|
|
fn children_ids(&self) -> SmallVec<[WidgetId; 16]> {
|
|
smallvec![self.label.id()]
|
|
}
|
|
|
|
fn make_trace_span(&self, ctx: &QueryCtx<'_>) -> Span {
|
|
trace_span!("Checkbox", id = ctx.widget_id().trace())
|
|
}
|
|
|
|
fn get_debug_text(&self) -> Option<String> {
|
|
if self.checked {
|
|
Some("[X]".to_string())
|
|
} else {
|
|
Some("[ ]".to_string())
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- MARK: TESTS ---
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use insta::assert_debug_snapshot;
|
|
|
|
use super::*;
|
|
use crate::assert_render_snapshot;
|
|
use crate::core::StyleProperty;
|
|
use crate::testing::{widget_ids, TestHarness, TestWidgetExt};
|
|
use crate::theme::PRIMARY_LIGHT;
|
|
|
|
#[test]
|
|
fn simple_checkbox() {
|
|
let [checkbox_id] = widget_ids();
|
|
let widget = Checkbox::new(false, "Hello").with_id(checkbox_id);
|
|
|
|
let mut harness = TestHarness::create(widget);
|
|
|
|
assert_debug_snapshot!(harness.root_widget());
|
|
assert_render_snapshot!(harness, "hello_unchecked");
|
|
|
|
assert_eq!(harness.pop_action(), None);
|
|
|
|
harness.mouse_click_on(checkbox_id);
|
|
assert_eq!(
|
|
harness.pop_action(),
|
|
Some((Action::CheckboxToggled(true), checkbox_id))
|
|
);
|
|
|
|
assert_debug_snapshot!(harness.root_widget());
|
|
assert_render_snapshot!(harness, "hello_checked");
|
|
|
|
harness.mouse_click_on(checkbox_id);
|
|
assert_eq!(
|
|
harness.pop_action(),
|
|
Some((Action::CheckboxToggled(false), checkbox_id))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn edit_checkbox() {
|
|
let image_1 = {
|
|
let checkbox = Checkbox::from_label(
|
|
true,
|
|
Label::new("The quick brown fox jumps over the lazy dog")
|
|
.with_brush(PRIMARY_LIGHT)
|
|
.with_style(StyleProperty::FontSize(20.0)),
|
|
);
|
|
|
|
let mut harness = TestHarness::create_with_size(checkbox, Size::new(50.0, 50.0));
|
|
|
|
harness.render()
|
|
};
|
|
|
|
let image_2 = {
|
|
let checkbox = Checkbox::new(false, "Hello world");
|
|
|
|
let mut harness = TestHarness::create_with_size(checkbox, Size::new(50.0, 50.0));
|
|
|
|
harness.edit_root_widget(|mut checkbox| {
|
|
let mut checkbox = checkbox.downcast::<Checkbox>();
|
|
checkbox.set_checked(true);
|
|
checkbox.set_text("The quick brown fox jumps over the lazy dog".into());
|
|
|
|
let mut label = checkbox.label_mut();
|
|
label.set_brush(PRIMARY_LIGHT);
|
|
label.insert_style(StyleProperty::FontSize(20.0));
|
|
});
|
|
|
|
harness.render()
|
|
};
|
|
|
|
// We don't use assert_eq because we don't want rich assert
|
|
assert!(image_1 == image_2);
|
|
}
|
|
}
|