From 04c5ab00f5735ede2c3d8611a6e24d2b07bcb967 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Fita?= <4925040+michalfita@users.noreply.github.com> Date: Wed, 25 Oct 2023 11:55:08 +0100 Subject: [PATCH 1/5] [Fixes #763] Introduced labelled checkboxes --- cursive-core/src/views/checkbox.rs | 33 ++++++++++-- cursive/examples/Readme.md | 4 ++ cursive/examples/checkbox.rs | 81 ++++++++++++++++++++++++++++++ 3 files changed, 115 insertions(+), 3 deletions(-) create mode 100644 cursive/examples/checkbox.rs diff --git a/cursive-core/src/views/checkbox.rs b/cursive-core/src/views/checkbox.rs index ba814bea..7f709710 100644 --- a/cursive-core/src/views/checkbox.rs +++ b/cursive-core/src/views/checkbox.rs @@ -3,7 +3,7 @@ use crate::{ event::{Event, EventResult, Key, MouseButton, MouseEvent}, theme::PaletteStyle, view::{CannotFocus, View}, - Cursive, Printer, Vec2, With, + Cursive, Printer, Vec2, With, utils::markup::StyledString, }; use std::rc::Rc; @@ -25,6 +25,8 @@ pub struct Checkbox { enabled: bool, on_change: Option>, + + label: StyledString, } new_default!(Checkbox); @@ -32,12 +34,23 @@ new_default!(Checkbox); impl Checkbox { impl_enabled!(self.enabled); - /// Creates a new, unchecked checkbox. + /// Creates a new, unlabelled, unchecked checkbox. pub fn new() -> Self { Checkbox { checked: false, enabled: true, on_change: None, + label: StyledString::new(), + } + } + + /// Creates a new, labelled, unchecked checkbox. + pub fn labelled(label: StyledString) -> Self { + Checkbox { + checked: false, + enabled: true, + on_change: None, + label } } @@ -134,12 +147,26 @@ impl Checkbox { if self.checked { printer.print((1, 0), "X"); } + + if !self.label.is_empty() { + // We want the space to be highlighted if focused + printer.print((3, 0), " "); + printer.print_styled((4, 0), &self.label); + } + } + + fn req_size(&self) -> Vec2 { + if self.label.is_empty() { + Vec2::new(3, 1) + } else { + Vec2::new(3 + 1 + self.label.width(), 1) + } } } impl View for Checkbox { fn required_size(&mut self, _: Vec2) -> Vec2 { - Vec2::new(3, 1) + self.req_size() } fn take_focus(&mut self, _: Direction) -> Result { diff --git a/cursive/examples/Readme.md b/cursive/examples/Readme.md index 18cd45e0..04ec7130 100644 --- a/cursive/examples/Readme.md +++ b/cursive/examples/Readme.md @@ -111,3 +111,7 @@ A larger example showing an implementation of minesweeper. ## [`window_title`](./window_title.rs) This shows how to change the terminal window title. + +## [`checkbox`](./checkbox.rs) + +This shows how to use `Checkbox`. diff --git a/cursive/examples/checkbox.rs b/cursive/examples/checkbox.rs new file mode 100644 index 00000000..3fca9e65 --- /dev/null +++ b/cursive/examples/checkbox.rs @@ -0,0 +1,81 @@ +use std::{cell::RefCell, collections::HashSet, fmt::Display, rc::Rc}; + +use cursive::views::{Checkbox, Dialog, DummyView, LinearLayout}; + +// This example uses checkboxes. +#[derive(Debug, PartialEq, Eq, Hash)] +enum Toppings { + ChocolateSprinkles, + CrushedAlmonds, + StrawberrySauce, +} + +impl Display for Toppings { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match *self { + Toppings::ChocolateSprinkles => write!(f, "Chocolate Sprinkles"), + Toppings::CrushedAlmonds => write!(f, "Crushed Almonds"), + Toppings::StrawberrySauce => write!(f, "Strawberry Sauce"), + } + } +} + +fn main() { + let mut siv = cursive::default(); + + // TODO: placeholder for MultiChoiceGroup. + + // Application wide container w/toppings choices. + let toppings: Rc>> = Rc::new(RefCell::new(HashSet::new())); + + siv.add_layer( + Dialog::new() + .title("Make your selections") + .content( + LinearLayout::vertical() + .child(Checkbox::labelled("Chocolate Sprinkles".into()).on_change({ + let toppings = toppings.clone(); + move |_, checked| { + if checked { + toppings.borrow_mut().insert(Toppings::ChocolateSprinkles); + } else { + toppings.borrow_mut().remove(&Toppings::ChocolateSprinkles); + } + } + })) + .child(Checkbox::labelled("Crushed Almonds".into()).on_change({ + let toppings = toppings.clone(); + move |_, checked| { + if checked { + toppings.borrow_mut().insert(Toppings::CrushedAlmonds); + } else { + toppings.borrow_mut().remove(&Toppings::CrushedAlmonds); + } + } + })) + .child(Checkbox::labelled("Strawberry Sauce".into()).on_change({ + let toppings = toppings.clone(); + move |_, checked| { + if checked { + toppings.borrow_mut().insert(Toppings::StrawberrySauce); + } else { + toppings.borrow_mut().remove(&Toppings::StrawberrySauce); + } + } + })), + ) + .button("Ok", move |s| { + s.pop_layer(); + let toppings = toppings + .borrow() + .iter() + .map(|t| t.to_string()) + .collect::>() + .join(", "); + let text = format!("Toppings: {toppings}"); + s.add_layer(Dialog::text(text).button("Ok", |s| s.quit())); + }), + ); + + siv.run(); +} From cfdc5dd90c90298fc59cc8acf56c6261cf420b40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Fita?= <4925040+michalfita@users.noreply.github.com> Date: Wed, 25 Oct 2023 15:48:32 +0100 Subject: [PATCH 2/5] [Fixes #763] First attempt at `MultiChoiceGroup` --- cursive-core/src/views/checkbox.rs | 102 ++++++++++++++++++++++++++++- cursive-core/src/views/mod.rs | 2 +- cursive/examples/checkbox.rs | 102 +++++++++++++++++++---------- 3 files changed, 167 insertions(+), 39 deletions(-) diff --git a/cursive-core/src/views/checkbox.rs b/cursive-core/src/views/checkbox.rs index 7f709710..70da8bd3 100644 --- a/cursive-core/src/views/checkbox.rs +++ b/cursive-core/src/views/checkbox.rs @@ -1,3 +1,5 @@ +use ahash::{HashSet, HashSetExt}; + use crate::{ direction::Direction, event::{Event, EventResult, Key, MouseButton, MouseEvent}, @@ -5,10 +7,104 @@ use crate::{ view::{CannotFocus, View}, Cursive, Printer, Vec2, With, utils::markup::StyledString, }; -use std::rc::Rc; +use std::{rc::Rc, cell::RefCell}; +use std::hash::Hash; +type GroupCallback = dyn Fn(&mut Cursive, &HashSet>); type Callback = dyn Fn(&mut Cursive, bool); +struct SharedState { + selections: HashSet>, + values: Vec>, + + on_change: Option>> +} + +impl SharedState { + pub fn selections(&self) -> &HashSet> { + &self.selections + } +} + +/// Group to coordinate multiple checkboxes. +/// +/// A `MultiChoiceGroup` can be used to create and manage multiple [`Checkbox`]es. +/// +/// A `MultiChoiceGroup` can be cloned; it will keep shared state (pointing to the same group). +pub struct MultiChoiceGroup { + // Given to every child button + state: Rc>>, +} + +// We have to manually implement Clone. +// Using derive(Clone) would add am unwanted `T: Clone` where-clause. +impl Clone for MultiChoiceGroup { + fn clone(&self) -> Self { + Self { + state: Rc::clone(&self.state), + } + } +} + +impl Default for MultiChoiceGroup { + fn default() -> Self { + Self::new() + } +} + +impl MultiChoiceGroup { + /// Creates an empty group for check boxes. + pub fn new() -> Self { + Self { + state: Rc::new(RefCell::new(SharedState { + selections: HashSet::new(), + values: Vec::new(), + on_change: None, + })), + } + } + + // TODO: Handling of the global state + + /// Adds a new checkbox to the group. + /// + /// The checkbox will display `label` next to it, and will ~embed~ `value`. + pub fn checkbox>(&mut self, value: T, label: S) -> Checkbox { + let element = Rc::new(value); + self.state.borrow_mut().values.push(element.clone()); + Checkbox::labelled(label).on_change({ // TODO: consider consequences + let selectable = Rc::downgrade(&element); + let groupstate = self.state.clone(); + move |_, checked| if checked { + if let Some(v) = selectable.upgrade() { + groupstate.borrow_mut().selections.insert(v); + } + } else { + if let Some(v) = selectable.upgrade() { + groupstate.borrow_mut().selections.remove(&v); + } + } + }) + } + + /// Returns the reference to a set associated with the selected checkboxes. + pub fn selections(&self) -> HashSet> { + self.state.borrow().selections().clone() + } + + /// Sets a callback to be user when choices change. + pub fn set_on_change>)>(&mut self, on_change: F) { + self.state.borrow_mut().on_change = Some(Rc::new(on_change)); + } + + /// Set a callback to use used when choices change. + /// + /// Chainable variant. + pub fn on_change>)>(self, on_change: F) -> Self { + crate::With::with(self, |s| s.set_on_change(on_change)) + } +} + /// Checkable box. /// /// # Examples @@ -45,12 +141,12 @@ impl Checkbox { } /// Creates a new, labelled, unchecked checkbox. - pub fn labelled(label: StyledString) -> Self { + pub fn labelled>(label: S) -> Self { Checkbox { checked: false, enabled: true, on_change: None, - label + label: label.into() } } diff --git a/cursive-core/src/views/mod.rs b/cursive-core/src/views/mod.rs index c912a610..c82588d7 100644 --- a/cursive-core/src/views/mod.rs +++ b/cursive-core/src/views/mod.rs @@ -103,7 +103,7 @@ pub use self::{ boxed_view::BoxedView, button::Button, canvas::Canvas, - checkbox::Checkbox, + checkbox::{Checkbox, MultiChoiceGroup}, circular_focus::CircularFocus, debug_view::DebugView, dialog::{Dialog, DialogFocus}, diff --git a/cursive/examples/checkbox.rs b/cursive/examples/checkbox.rs index 3fca9e65..cd13946b 100644 --- a/cursive/examples/checkbox.rs +++ b/cursive/examples/checkbox.rs @@ -1,6 +1,6 @@ -use std::{cell::RefCell, collections::HashSet, fmt::Display, rc::Rc}; - -use cursive::views::{Checkbox, Dialog, DummyView, LinearLayout}; +use std::{cell::RefCell, fmt::Display, rc::Rc}; +use ahash::HashSet; +use cursive::views::{Checkbox, MultiChoiceGroup, Dialog, DummyView, LinearLayout}; // This example uses checkboxes. #[derive(Debug, PartialEq, Eq, Hash)] @@ -10,6 +10,13 @@ enum Toppings { StrawberrySauce, } +#[derive(Debug, PartialEq, Eq, Hash)] +enum Extras { + Tissues, + DarkCone, + ChocolateFlake, +} + impl Display for Toppings { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match *self { @@ -20,49 +27,69 @@ impl Display for Toppings { } } +impl Display for Extras { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match *self { + Extras::Tissues => write!(f, "Tissues"), + Extras::DarkCone => write!(f, "Dark Cone"), + Extras::ChocolateFlake => write!(f, "Chocolate Flake"), + } + } +} + fn main() { let mut siv = cursive::default(); - // TODO: placeholder for MultiChoiceGroup. - // Application wide container w/toppings choices. - let toppings: Rc>> = Rc::new(RefCell::new(HashSet::new())); + let toppings: Rc>> = Rc::new(RefCell::new(HashSet::default())); + + // The `MultiChoiceGroup` can be used to maintain multiple choices. + let mut multichoice: MultiChoiceGroup = MultiChoiceGroup::new(); siv.add_layer( Dialog::new() .title("Make your selections") .content( - LinearLayout::vertical() - .child(Checkbox::labelled("Chocolate Sprinkles".into()).on_change({ - let toppings = toppings.clone(); - move |_, checked| { - if checked { - toppings.borrow_mut().insert(Toppings::ChocolateSprinkles); - } else { - toppings.borrow_mut().remove(&Toppings::ChocolateSprinkles); + LinearLayout::horizontal() + .child( + LinearLayout::vertical() + .child(Checkbox::labelled("Chocolate Sprinkles").on_change({ + let toppings = toppings.clone(); + move |_, checked| { + if checked { + toppings.borrow_mut().insert(Toppings::ChocolateSprinkles); + } else { + toppings.borrow_mut().remove(&Toppings::ChocolateSprinkles); + } } - } - })) - .child(Checkbox::labelled("Crushed Almonds".into()).on_change({ - let toppings = toppings.clone(); - move |_, checked| { - if checked { - toppings.borrow_mut().insert(Toppings::CrushedAlmonds); - } else { - toppings.borrow_mut().remove(&Toppings::CrushedAlmonds); + })) + .child(Checkbox::labelled("Crushed Almonds").on_change({ + let toppings = toppings.clone(); + move |_, checked| { + if checked { + toppings.borrow_mut().insert(Toppings::CrushedAlmonds); + } else { + toppings.borrow_mut().remove(&Toppings::CrushedAlmonds); + } } - } - })) - .child(Checkbox::labelled("Strawberry Sauce".into()).on_change({ - let toppings = toppings.clone(); - move |_, checked| { - if checked { - toppings.borrow_mut().insert(Toppings::StrawberrySauce); - } else { - toppings.borrow_mut().remove(&Toppings::StrawberrySauce); + })) + .child(Checkbox::labelled("Strawberry Sauce").on_change({ + let toppings = toppings.clone(); + move |_, checked| { + if checked { + toppings.borrow_mut().insert(Toppings::StrawberrySauce); + } else { + toppings.borrow_mut().remove(&Toppings::StrawberrySauce); + } } - } - })), + })), + ) + .child(DummyView) + .child(LinearLayout::vertical() + .child(multichoice.checkbox(Extras::ChocolateFlake, "Chocolate Flake")) + .child(multichoice.checkbox(Extras::DarkCone, "Dark Cone")) + .child(multichoice.checkbox(Extras::Tissues, "Tissues")) + ) ) .button("Ok", move |s| { s.pop_layer(); @@ -72,7 +99,12 @@ fn main() { .map(|t| t.to_string()) .collect::>() .join(", "); - let text = format!("Toppings: {toppings}"); + let extras = multichoice.selections() + .iter() + .map(|e| e.to_string()) + .collect::>() + .join(", "); + let text = format!("Toppings: {toppings}\nExtras: {extras}"); s.add_layer(Dialog::text(text).button("Ok", |s| s.quit())); }), ); From 202a73248f14601f191705af0d45354cb4ecc58d Mon Sep 17 00:00:00 2001 From: Alexandre Bury Date: Fri, 31 May 2024 10:48:18 -0400 Subject: [PATCH 3/5] Fix clippy warnings --- cursive-core/src/views/checkbox.rs | 6 ++---- cursive/examples/checkbox.rs | 6 +++--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/cursive-core/src/views/checkbox.rs b/cursive-core/src/views/checkbox.rs index 74b5af91..a2a51b67 100644 --- a/cursive-core/src/views/checkbox.rs +++ b/cursive-core/src/views/checkbox.rs @@ -86,10 +86,8 @@ impl MultiChoiceGroup { if let Some(v) = selectable.upgrade() { groupstate.lock().selections.insert(v); } - } else { - if let Some(v) = selectable.upgrade() { - groupstate.lock().selections.remove(&v); - } + } else if let Some(v) = selectable.upgrade() { + groupstate.lock().selections.remove(&v); } } }) diff --git a/cursive/examples/checkbox.rs b/cursive/examples/checkbox.rs index 8dda76cf..b9432e8f 100644 --- a/cursive/examples/checkbox.rs +++ b/cursive/examples/checkbox.rs @@ -56,7 +56,7 @@ fn main() { .child( LinearLayout::vertical() .child(Checkbox::labelled("Chocolate Sprinkles").on_change({ - let toppings = toppings.clone(); + let toppings = Arc::clone(&toppings); move |_, checked| { if checked { toppings.lock().insert(Toppings::ChocolateSprinkles); @@ -66,7 +66,7 @@ fn main() { } })) .child(Checkbox::labelled("Crushed Almonds").on_change({ - let toppings = toppings.clone(); + let toppings = Arc::clone(&toppings); move |_, checked| { if checked { toppings.lock().insert(Toppings::CrushedAlmonds); @@ -76,7 +76,7 @@ fn main() { } })) .child(Checkbox::labelled("Strawberry Sauce").on_change({ - let toppings = toppings.clone(); + let toppings = Arc::clone(&toppings); move |_, checked| { if checked { toppings.lock().insert(Toppings::StrawberrySauce); From 5a41fa2166479fe8abbace9aab2a8b55eb554dee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Fita?= <4925040+michalfita@users.noreply.github.com> Date: Fri, 10 Oct 2025 18:35:41 +0100 Subject: [PATCH 4/5] Some clean up of forgotten code --- cursive-core/src/views/checkbox.rs | 1 + cursive/examples/checkbox_multichoicegroup.rs | 113 ++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 cursive/examples/checkbox_multichoicegroup.rs diff --git a/cursive-core/src/views/checkbox.rs b/cursive-core/src/views/checkbox.rs index 909955f4..f08fde93 100644 --- a/cursive-core/src/views/checkbox.rs +++ b/cursive-core/src/views/checkbox.rs @@ -4,6 +4,7 @@ use crate::{ direction::Direction, event::{Event, EventResult, Key, MouseButton, MouseEvent}, style::PaletteStyle, + utils::markup::StyledString, view::{CannotFocus, View}, Cursive, Printer, Vec2, With, }; diff --git a/cursive/examples/checkbox_multichoicegroup.rs b/cursive/examples/checkbox_multichoicegroup.rs new file mode 100644 index 00000000..6e7df444 --- /dev/null +++ b/cursive/examples/checkbox_multichoicegroup.rs @@ -0,0 +1,113 @@ +//! This example demonstrates how to use a multi-choice group with checkboxes to +//! allow users to select multiple values in a set. +use cursive::views::{Checkbox, Dialog, DummyView, LinearLayout, MultiChoiceGroup}; +use parking_lot::Mutex; +use std::fmt::Display; +use std::sync::Arc; + +/// This example uses standalone checkboxes. +#[derive(Debug, Default, PartialEq, Eq, Hash)] +struct Toppings { + chocolate_sprinkles: bool, + crushed_almonds: bool, + strawberry_sauce: bool, +} + +/// This example uses checkboxes from the multi-choice group. +#[derive(Debug, PartialEq, Eq, Hash)] +enum Extras { + Tissues, + DarkCone, + ChocolateFlake, +} + +impl Display for Toppings { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.chocolate_sprinkles { + write!(f, "Chocolate Sprinkles")?; + } + if self.chocolate_sprinkles && self.crushed_almonds { + write!(f, ", ")?; + } + if self.crushed_almonds { + write!(f, "Crushed Almonds")?; + } + if self.crushed_almonds && self.strawberry_sauce { + write!(f, ", ")?; + } + if self.strawberry_sauce { + write!(f, "Strawberry Sauce")?; + } + Ok(()) + } +} + +impl Display for Extras { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match *self { + Extras::Tissues => write!(f, "Tissues"), + Extras::DarkCone => write!(f, "Dark Cone"), + Extras::ChocolateFlake => write!(f, "Chocolate Flake"), + } + } +} + +fn main() { + let mut siv = cursive::default(); + + // Application wide container w/toppings choices. + let toppings: Arc> = Arc::new(Mutex::new(Toppings::default())); + + // The `MultiChoiceGroup` can be used to maintain multiple choices. + let mut multichoice: MultiChoiceGroup = MultiChoiceGroup::new(); + + siv.add_layer( + Dialog::new() + .title("Make your selections") + .content( + LinearLayout::horizontal() + .child( + LinearLayout::vertical() + .child(Checkbox::labelled("Chocolate Sprinkles").on_change({ + let toppings = Arc::clone(&toppings); + move |_, checked| { + toppings.lock().chocolate_sprinkles = checked; + } + })) + .child(Checkbox::labelled("Crushed Almonds").on_change({ + let toppings = Arc::clone(&toppings); + move |_, checked| { + toppings.lock().crushed_almonds = checked; + } + })) + .child(Checkbox::labelled("Strawberry Sauce").on_change({ + let toppings = Arc::clone(&toppings); + move |_, checked| { + toppings.lock().strawberry_sauce = checked; + } + })), + ) + .child(DummyView) + .child( + LinearLayout::vertical() + .child(multichoice.checkbox(Extras::ChocolateFlake, "Chocolate Flake")) + .child(multichoice.checkbox(Extras::DarkCone, "Dark Cone")) + .child(multichoice.checkbox(Extras::Tissues, "Tissues")), + ), + ) + .button("Ok", move |s| { + s.pop_layer(); + let toppings = toppings.lock().to_string(); + let extras = multichoice + .selections() + .iter() + .map(|e| e.to_string()) + .collect::>() + .join(", "); + let text = format!("Toppings: {toppings}\nExtras: {extras}"); + s.add_layer(Dialog::text(text).button("Ok", |s| s.quit())); + }), + ); + + siv.run(); +} From 8fe896bb835a0ea5f4995a6aa3e72a11fa3734fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Fita?= <4925040+michalfita@users.noreply.github.com> Date: Thu, 16 Oct 2025 11:25:21 +0100 Subject: [PATCH 5/5] Address review comments --- cursive-core/src/views/checkbox.rs | 75 +++++++++++++++++------------ cursive/examples/checkbox.rs | 76 +++++++++++++++++++++--------- 2 files changed, 99 insertions(+), 52 deletions(-) diff --git a/cursive-core/src/views/checkbox.rs b/cursive-core/src/views/checkbox.rs index f08fde93..92477451 100644 --- a/cursive-core/src/views/checkbox.rs +++ b/cursive-core/src/views/checkbox.rs @@ -15,16 +15,47 @@ use std::sync::Arc; type GroupCallback = dyn Fn(&mut Cursive, &HashSet>) + Send + Sync; type Callback = dyn Fn(&mut Cursive, bool) + Send + Sync; +struct Item { + value: Arc, + checked: bool, +} + +// We have to manually implement Clone. +// Using derive(Clone) would add am unwanted `T: Clone` where-clause. +impl Clone for Item { + fn clone(&self) -> Self { + Self { + value: Arc::clone(&self.value), + checked: self.checked, + } + } +} + struct SharedState { - selections: HashSet>, - values: Vec>, + items: Vec>, on_change: Option>>, } impl SharedState { - pub fn selections(&self) -> &HashSet> { - &self.selections + fn add(&mut self, value: T, checked: bool) -> usize { + let value = Arc::new(value); + let i = self.items.len(); + self.items.push(Item { value, checked }); + i + } + + fn set_checked(&mut self, i: usize, checked: bool) { + self.items[i].checked = checked; + } + + fn selections(&self) -> Vec> { + self.items + .iter() + .filter(|item| item.checked) + .cloned() + .map(|item| item.value) + .collect() } } @@ -59,15 +90,12 @@ impl MultiChoiceGroup { pub fn new() -> Self { Self { state: Arc::new(Mutex::new(SharedState { - selections: HashSet::new(), - values: Vec::new(), + items: Vec::new(), on_change: None, })), } } - // TODO: Handling of the global state - /// Adds a new checkbox to the group. /// /// The checkbox will display `label` next to it, and will ~embed~ `value`. @@ -75,27 +103,18 @@ impl MultiChoiceGroup { where T: Send + Sync, { - let element = Arc::new(value); - self.state.lock().values.push(element.clone()); + let i = self.state.lock().add(value, false); Checkbox::labelled(label).on_change({ - // TODO: consider consequences - let selectable = Arc::downgrade(&element); - let groupstate = self.state.clone(); + let groupstate = Arc::clone(&self.state); move |_, checked| { - if checked { - if let Some(v) = selectable.upgrade() { - groupstate.lock().selections.insert(v); - } - } else if let Some(v) = selectable.upgrade() { - groupstate.lock().selections.remove(&v); - } + groupstate.lock().set_checked(i, checked); } }) } - /// Returns the reference to a set associated with the selected checkboxes. - pub fn selections(&self) -> HashSet> { - self.state.lock().selections().clone() + /// Returns the reference to a vector associated with the selected checkboxes. + pub fn selections(&self) -> Vec> { + self.state.lock().selections() } /// Sets a callback to be user when choices change. @@ -268,20 +287,16 @@ impl Checkbox { printer.print_styled((4, 0), &self.label); } } +} - fn req_size(&self) -> Vec2 { +impl View for Checkbox { + fn required_size(&mut self, _: Vec2) -> Vec2 { if self.label.is_empty() { Vec2::new(3, 1) } else { Vec2::new(3 + 1 + self.label.width(), 1) } } -} - -impl View for Checkbox { - fn required_size(&mut self, _: Vec2) -> Vec2 { - self.req_size() - } fn take_focus(&mut self, _: Direction) -> Result { self.enabled.then(EventResult::consumed).ok_or(CannotFocus) diff --git a/cursive/examples/checkbox.rs b/cursive/examples/checkbox.rs index b9432e8f..7b385788 100644 --- a/cursive/examples/checkbox.rs +++ b/cursive/examples/checkbox.rs @@ -1,5 +1,7 @@ +//! This example demonstrates how to use a checkboxes manually to +//! allow users to select multiple values in a set. use ahash::HashSet; -use cursive::views::{Checkbox, Dialog, DummyView, LinearLayout, MultiChoiceGroup}; +use cursive::views::{Checkbox, Dialog, DummyView, LinearLayout}; use parking_lot::Mutex; use std::fmt::Display; use std::sync::Arc; @@ -12,11 +14,18 @@ enum Toppings { StrawberrySauce, } -#[derive(Debug, PartialEq, Eq, Hash)] -enum Extras { - Tissues, - DarkCone, - ChocolateFlake, +// #[derive(Debug, PartialEq, Eq, Hash)] +// enum Extras { +// Tissues, +// DarkCone, +// ChocolateFlake, +// } + +#[derive(Debug, Default)] +struct Extras { + tissues: bool, + dark_cone: bool, + chocolate_flake: bool, } impl Display for Toppings { @@ -31,11 +40,24 @@ impl Display for Toppings { impl Display for Extras { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match *self { - Extras::Tissues => write!(f, "Tissues"), - Extras::DarkCone => write!(f, "Dark Cone"), - Extras::ChocolateFlake => write!(f, "Chocolate Flake"), - } + let extras = [ + if self.tissues { "Tissues" } else { "" }, + if self.dark_cone { "Dark Cone" } else { "" }, + if self.chocolate_flake { + "Chocolate Flake" + } else { + "" + }, + ]; + write!( + f, + "{}", + extras + .into_iter() + .filter(|s| !s.is_empty()) + .collect::>() + .join(", ") + ) } } @@ -45,8 +67,8 @@ fn main() { // Application wide container w/toppings choices. let toppings: Arc>> = Arc::new(Mutex::new(HashSet::default())); - // The `MultiChoiceGroup` can be used to maintain multiple choices. - let mut multichoice: MultiChoiceGroup = MultiChoiceGroup::new(); + // Application wide container w/extras choices. + let extras: Arc> = Arc::new(Mutex::new(Extras::default())); siv.add_layer( Dialog::new() @@ -89,9 +111,24 @@ fn main() { .child(DummyView) .child( LinearLayout::vertical() - .child(multichoice.checkbox(Extras::ChocolateFlake, "Chocolate Flake")) - .child(multichoice.checkbox(Extras::DarkCone, "Dark Cone")) - .child(multichoice.checkbox(Extras::Tissues, "Tissues")), + .child(Checkbox::labelled("Chocolate Flake").on_change({ + let extras = Arc::clone(&extras); + move |_, checked| { + extras.lock().chocolate_flake = checked; + } + })) + .child(Checkbox::labelled("Dark Cone").on_change({ + let extras = Arc::clone(&extras); + move |_, checked| { + extras.lock().dark_cone = checked; + } + })) + .child(Checkbox::labelled("Tissues").on_change({ + let extras = Arc::clone(&extras); + move |_, checked| { + extras.lock().tissues = checked; + } + })), ), ) .button("Ok", move |s| { @@ -102,12 +139,7 @@ fn main() { .map(|t| t.to_string()) .collect::>() .join(", "); - let extras = multichoice - .selections() - .iter() - .map(|e| e.to_string()) - .collect::>() - .join(", "); + let extras = extras.lock().to_string(); let text = format!("Toppings: {toppings}\nExtras: {extras}"); s.add_layer(Dialog::text(text).button("Ok", |s| s.quit())); }),