Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Fixes #763] Introduced labelled checkboxes #765

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 137 additions & 2 deletions cursive-core/src/views/checkbox.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,122 @@
use ahash::{HashSet, HashSetExt};

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

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

on_change: Option<Arc<GroupCallback<T>>>,
}

impl<T> SharedState<T> {
pub fn selections(&self) -> &HashSet<Arc<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: Arc<Mutex<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: Arc::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: Arc::new(Mutex::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
where
T: Send + Sync,
{
let element = Arc::new(value);
self.state.lock().values.push(element.clone());
Checkbox::labelled(label).on_change({
// TODO: consider consequences
let selectable = Arc::downgrade(&element);
let groupstate = self.state.clone();
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);
}
}
})
}

/// Returns the reference to a set associated with the selected checkboxes.
pub fn selections(&self) -> HashSet<Arc<T>> {
self.state.lock().selections().clone()
}

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

/// Checkable box.
///
/// # Examples
Expand All @@ -25,19 +133,32 @@ pub struct Checkbox {
enabled: bool,

on_change: Option<Arc<Callback>>,

label: StyledString,
}

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<S: Into<StyledString>>(label: S) -> Self {
Checkbox {
checked: false,
enabled: true,
on_change: None,
label: label.into(),
}
}

Expand Down Expand Up @@ -140,12 +261,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<EventResult, CannotFocus> {
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
1 change: 1 addition & 0 deletions cursive/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,4 @@ atty = "0.2"
pretty-bytes = "0.2"
serde_json = "1.0.85"
serde_yaml = "0.9.13"
parking_lot = "0.12"
4 changes: 4 additions & 0 deletions cursive/examples/Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,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`.
117 changes: 117 additions & 0 deletions cursive/examples/checkbox.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
use ahash::HashSet;
use cursive::views::{Checkbox, Dialog, DummyView, LinearLayout, MultiChoiceGroup};
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,
}

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 {
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<Mutex<HashSet<Toppings>>> = Arc::new(Mutex::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::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(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()
.iter()
.map(|t| t.to_string())
.collect::<Vec<String>>()
.join(", ");
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()));
}),
);

siv.run();
}