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] [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())); }), );