diff --git a/cursive-core/src/views/checkbox.rs b/cursive-core/src/views/checkbox.rs index 179de04c..92477451 100644 --- a/cursive-core/src/views/checkbox.rs +++ b/cursive-core/src/views/checkbox.rs @@ -1,14 +1,141 @@ +use ahash::{HashSet, HashSetExt}; + use crate::{ direction::Direction, event::{Event, EventResult, Key, MouseButton, MouseEvent}, style::PaletteStyle, + utils::markup::StyledString, view::{CannotFocus, View}, Cursive, Printer, Vec2, With, }; +use parking_lot::Mutex; +use std::hash::Hash; 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 { + items: Vec>, + + on_change: Option>>, +} + +impl SharedState { + 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() + } +} + +/// 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: Arc>>, +} + +// 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: Arc::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: Arc::new(Mutex::new(SharedState { + items: Vec::new(), + on_change: None, + })), + } + } + + /// 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 + where + T: Send + Sync, + { + let i = self.state.lock().add(value, false); + Checkbox::labelled(label).on_change({ + let groupstate = Arc::clone(&self.state); + move |_, checked| { + groupstate.lock().set_checked(i, checked); + } + }) + } + + /// 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. + pub fn set_on_change(&mut self, on_change: F) + where + F: Send + Sync + 'static + Fn(&mut Cursive, &HashSet>), + { + self.state.lock().on_change = Some(Arc::new(on_change)); + } + + /// Set a callback to use used when choices change. + /// + /// Chainable variant. + pub fn on_change(self, on_change: F) -> Self + where + F: Send + Sync + 'static + Fn(&mut Cursive, &HashSet>), + { + crate::With::with(self, |s| s.set_on_change(on_change)) + } +} + /// Checkable box. /// /// # Examples @@ -25,6 +152,8 @@ pub struct Checkbox { enabled: bool, on_change: Option>, + + label: StyledString, } new_default!(Checkbox); @@ -32,12 +161,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: S) -> Self { + Checkbox { + checked: false, + enabled: true, + on_change: None, + label: label.into(), } } @@ -140,12 +280,22 @@ 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); + } } } impl View for Checkbox { fn required_size(&mut self, _: Vec2) -> Vec2 { - Vec2::new(3, 1) + if self.label.is_empty() { + Vec2::new(3, 1) + } else { + Vec2::new(3 + 1 + self.label.width(), 1) + } } fn take_focus(&mut self, _: Direction) -> Result { diff --git a/cursive-core/src/views/mod.rs b/cursive-core/src/views/mod.rs index 17d728d8..5e31f4b1 100644 --- a/cursive-core/src/views/mod.rs +++ b/cursive-core/src/views/mod.rs @@ -104,7 +104,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/Cargo.toml b/cursive/Cargo.toml index 07026924..49546382 100644 --- a/cursive/Cargo.toml +++ b/cursive/Cargo.toml @@ -96,3 +96,4 @@ rand = "0.9" pretty-bytes = "0.2" serde_json = "1.0.85" serde_yaml = "0.9.13" +parking_lot = "0.12" diff --git a/cursive/examples/Readme.md b/cursive/examples/Readme.md index 81825ccb..734979d7 100644 --- a/cursive/examples/Readme.md +++ b/cursive/examples/Readme.md @@ -119,3 +119,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..7b385788 --- /dev/null +++ b/cursive/examples/checkbox.rs @@ -0,0 +1,149 @@ +//! 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}; +use parking_lot::Mutex; +use std::fmt::Display; +use std::sync::Arc; + +// This example uses checkboxes. +#[derive(Debug, PartialEq, Eq, Hash)] +enum Toppings { + ChocolateSprinkles, + CrushedAlmonds, + StrawberrySauce, +} + +// #[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 { + 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"), + } + } +} + +impl Display for Extras { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + 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(", ") + ) + } +} + +fn main() { + let mut siv = cursive::default(); + + // Application wide container w/toppings choices. + let toppings: Arc>> = Arc::new(Mutex::new(HashSet::default())); + + // Application wide container w/extras choices. + let extras: Arc> = Arc::new(Mutex::new(Extras::default())); + + 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| { + if checked { + toppings.lock().insert(Toppings::ChocolateSprinkles); + } else { + toppings.lock().remove(&Toppings::ChocolateSprinkles); + } + } + })) + .child(Checkbox::labelled("Crushed Almonds").on_change({ + let toppings = Arc::clone(&toppings); + move |_, checked| { + if checked { + toppings.lock().insert(Toppings::CrushedAlmonds); + } else { + toppings.lock().remove(&Toppings::CrushedAlmonds); + } + } + })) + .child(Checkbox::labelled("Strawberry Sauce").on_change({ + let toppings = Arc::clone(&toppings); + move |_, checked| { + if checked { + toppings.lock().insert(Toppings::StrawberrySauce); + } else { + toppings.lock().remove(&Toppings::StrawberrySauce); + } + } + })), + ) + .child(DummyView) + .child( + LinearLayout::vertical() + .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| { + s.pop_layer(); + let toppings = toppings + .lock() + .iter() + .map(|t| t.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())); + }), + ); + + siv.run(); +} 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(); +}