Skip to content
Merged
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
154 changes: 152 additions & 2 deletions cursive-core/src/views/checkbox.rs
Original file line number Diff line number Diff line change
@@ -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<T> = dyn Fn(&mut Cursive, &HashSet<Arc<T>>) + Send + Sync;
type Callback = dyn Fn(&mut Cursive, bool) + Send + Sync;

struct Item<T> {
value: Arc<T>,
checked: bool,
}

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

struct SharedState<T> {
items: Vec<Item<T>>,

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

impl<T> SharedState<T> {
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<Arc<T>> {
self.items
.iter()
.filter(|item| item.checked)
.cloned()
.map(|item| item.value)
Comment on lines +56 to +57
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might be able to switch the two around, so it only clones the Arc<T>, which means you don't need to implement Clone for Item<T> anymore.

.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<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 {
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<S: Into<StyledString>>(&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<Arc<T>> {
self.state.lock().selections()
}

/// 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 +152,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 +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<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 @@ -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},
Expand Down
1 change: 1 addition & 0 deletions cursive/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
4 changes: 4 additions & 0 deletions cursive/examples/Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
149 changes: 149 additions & 0 deletions cursive/examples/checkbox.rs
Original file line number Diff line number Diff line change
@@ -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::<Vec<&str>>()
.join(", ")
)
}
}

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()));

// Application wide container w/extras choices.
let extras: Arc<Mutex<Extras>> = 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::<Vec<String>>()
.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();
}
Loading