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/3] [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/3] [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/3] 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);