From 797406de39f43197c139114fcd656df7c61afaa7 Mon Sep 17 00:00:00 2001 From: YgorSouza <43298013+YgorSouza@users.noreply.github.com> Date: Sat, 6 Jan 2024 22:34:23 +0100 Subject: [PATCH] Add indeterminate state to checkbox (#3605) --- crates/egui/src/response.rs | 3 ++ crates/egui/src/widgets/button.rs | 44 ++++++++++++++++--- .../src/demo/misc_demo_window.rs | 20 +++++++++ 3 files changed, 60 insertions(+), 7 deletions(-) diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index 1ee3f312f0f..46cff117c9e 100644 --- a/crates/egui/src/response.rs +++ b/crates/egui/src/response.rs @@ -655,6 +655,9 @@ impl Response { } else { Checked::False }); + } else if matches!(info.typ, WidgetType::Checkbox) { + // Indeterminate state + builder.set_checked(Checked::Mixed); } } diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index 6ca7cf86e40..6d07bfaef24 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -356,6 +356,7 @@ impl Widget for Button<'_> { pub struct Checkbox<'a> { checked: &'a mut bool, text: WidgetText, + indeterminate: bool, } impl<'a> Checkbox<'a> { @@ -363,17 +364,32 @@ impl<'a> Checkbox<'a> { Checkbox { checked, text: text.into(), + indeterminate: false, } } pub fn without_text(checked: &'a mut bool) -> Self { Self::new(checked, WidgetText::default()) } + + /// Display an indeterminate state (neither checked nor unchecked) + /// + /// This only affects the checkbox's appearance. It will still toggle its boolean value when + /// clicked. + #[inline] + pub fn indeterminate(mut self, indeterminate: bool) -> Self { + self.indeterminate = indeterminate; + self + } } impl<'a> Widget for Checkbox<'a> { fn ui(self, ui: &mut Ui) -> Response { - let Checkbox { checked, text } = self; + let Checkbox { + checked, + text, + indeterminate, + } = self; let spacing = &ui.spacing(); let icon_width = spacing.icon_width; @@ -402,11 +418,18 @@ impl<'a> Widget for Checkbox<'a> { response.mark_changed(); } response.widget_info(|| { - WidgetInfo::selected( - WidgetType::Checkbox, - *checked, - galley.as_ref().map_or("", |x| x.text()), - ) + if indeterminate { + WidgetInfo::labeled( + WidgetType::Checkbox, + galley.as_ref().map_or("", |x| x.text()), + ) + } else { + WidgetInfo::selected( + WidgetType::Checkbox, + *checked, + galley.as_ref().map_or("", |x| x.text()), + ) + } }); if ui.is_rect_visible(rect) { @@ -420,7 +443,14 @@ impl<'a> Widget for Checkbox<'a> { visuals.bg_stroke, )); - if *checked { + if indeterminate { + // Horizontal line: + ui.painter().add(Shape::hline( + small_icon_rect.x_range(), + small_icon_rect.center().y, + visuals.fg_stroke, + )); + } else if *checked { // Check mark: ui.painter().add(Shape::line( vec![ diff --git a/crates/egui_demo_lib/src/demo/misc_demo_window.rs b/crates/egui_demo_lib/src/demo/misc_demo_window.rs index a90465cc19d..1883499d5ab 100644 --- a/crates/egui_demo_lib/src/demo/misc_demo_window.rs +++ b/crates/egui_demo_lib/src/demo/misc_demo_window.rs @@ -15,6 +15,7 @@ pub struct MiscDemoWindow { dummy_bool: bool, dummy_usize: usize, + checklist: [bool; 3], } impl Default for MiscDemoWindow { @@ -30,6 +31,7 @@ impl Default for MiscDemoWindow { dummy_bool: false, dummy_usize: 0, + checklist: std::array::from_fn(|i| i == 0), } } } @@ -107,6 +109,24 @@ impl View for MiscDemoWindow { } }); ui.radio_value(&mut self.dummy_usize, 64, "radio_value"); + ui.label("Checkboxes can be in an indeterminate state:"); + let mut all_checked = self.checklist.iter().all(|item| *item); + let any_checked = self.checklist.iter().any(|item| *item); + let indeterminate = any_checked && !all_checked; + if ui + .add( + Checkbox::new(&mut all_checked, "Check/uncheck all") + .indeterminate(indeterminate), + ) + .changed() + { + self.checklist + .iter_mut() + .for_each(|checked| *checked = all_checked); + } + for (i, checked) in self.checklist.iter_mut().enumerate() { + ui.checkbox(checked, format!("Item {}", i + 1)); + } }); ui.collapsing("Columns", |ui| {