Skip to content

Commit

Permalink
[Fixes #763] First attempt at MultiChoiceGroup<T>
Browse files Browse the repository at this point in the history
  • Loading branch information
michalfita committed Oct 25, 2023
1 parent 04c5ab0 commit cfdc5dd
Show file tree
Hide file tree
Showing 3 changed files with 167 additions and 39 deletions.
102 changes: 99 additions & 3 deletions cursive-core/src/views/checkbox.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,110 @@
use ahash::{HashSet, HashSetExt};

use crate::{
direction::Direction,
event::{Event, EventResult, Key, MouseButton, MouseEvent},
theme::PaletteStyle,
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<T> = dyn Fn(&mut Cursive, &HashSet<Rc<T>>);
type Callback = dyn Fn(&mut Cursive, bool);

struct SharedState<T> {
selections: HashSet<Rc<T>>,
values: Vec<Rc<T>>,

on_change: Option<Rc<GroupCallback<T>>>
}

impl<T> SharedState<T> {
pub fn selections(&self) -> &HashSet<Rc<T>> {
&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<T> {
// Given to every child button
state: Rc<RefCell<SharedState<T>>>,
}

// We have to manually implement Clone.
// Using derive(Clone) would add am unwanted `T: Clone` where-clause.
impl<T> Clone for MultiChoiceGroup<T> {
fn clone(&self) -> Self {
Self {
state: Rc::clone(&self.state),
}
}
}

impl<T: 'static + Hash + Eq> Default for MultiChoiceGroup<T> {
fn default() -> Self {
Self::new()
}
}

impl<T: 'static + Hash + Eq> MultiChoiceGroup<T> {
/// 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<S: Into<StyledString>>(&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<Rc<T>> {
self.state.borrow().selections().clone()
}

/// Sets a callback to be user when choices change.
pub fn set_on_change<F: 'static + Fn(&mut Cursive, &HashSet<Rc<T>>)>(&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<F: 'static + Fn(&mut Cursive, &HashSet<Rc<T>>)>(self, on_change: F) -> Self {
crate::With::with(self, |s| s.set_on_change(on_change))
}
}

/// Checkable box.
///
/// # Examples
Expand Down Expand Up @@ -45,12 +141,12 @@ impl Checkbox {
}

/// Creates a new, labelled, unchecked checkbox.
pub fn labelled(label: StyledString) -> Self {
pub fn labelled<S: Into<StyledString>>(label: S) -> Self {
Checkbox {
checked: false,
enabled: true,
on_change: None,
label
label: label.into()
}
}

Expand Down
2 changes: 1 addition & 1 deletion cursive-core/src/views/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down
102 changes: 67 additions & 35 deletions cursive/examples/checkbox.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand All @@ -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 {
Expand All @@ -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<RefCell<HashSet<Toppings>>> = Rc::new(RefCell::new(HashSet::new()));
let toppings: Rc<RefCell<HashSet<Toppings>>> = Rc::new(RefCell::new(HashSet::default()));

// The `MultiChoiceGroup<T>` can be used to maintain multiple choices.
let mut multichoice: MultiChoiceGroup<Extras> = 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();
Expand All @@ -72,7 +99,12 @@ fn main() {
.map(|t| t.to_string())
.collect::<Vec<String>>()
.join(", ");
let text = format!("Toppings: {toppings}");
let extras = multichoice.selections()
.iter()
.map(|e| e.to_string())
.collect::<Vec<String>>()
.join(", ");
let text = format!("Toppings: {toppings}\nExtras: {extras}");
s.add_layer(Dialog::text(text).button("Ok", |s| s.quit()));
}),
);
Expand Down

0 comments on commit cfdc5dd

Please sign in to comment.