Skip to content

Commit

Permalink
init impl for checkbox
Browse files Browse the repository at this point in the history
  • Loading branch information
ynqa committed Feb 10, 2024
1 parent c4260fc commit e077ac4
Show file tree
Hide file tree
Showing 10 changed files with 378 additions and 0 deletions.
10 changes: 10 additions & 0 deletions examples/checkbox.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
use promkit::{error::Result, preset::Checkbox};

fn main() -> Result {
let mut p = Checkbox::new(0..100)
.title("What number do you like?")
.lines(5)
.prompt()?;
println!("result: {:?}", p.run()?);
Ok(())
}
1 change: 1 addition & 0 deletions src/core.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod checkbox;
mod cursor;
pub mod menu;
pub mod text;
Expand Down
70 changes: 70 additions & 0 deletions src/core/checkbox.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
use std::{collections::HashSet, fmt, iter::FromIterator};

mod render;
pub use render::Renderer;
mod build;
pub use build::Builder;

use crate::core::menu::Menu;

#[derive(Clone)]
pub struct Checkbox {
menu: Menu,
picked: HashSet<usize>,
}

impl<T: fmt::Display> FromIterator<T> for Checkbox {
fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
Self {
menu: Menu::from_iter(iter),
picked: HashSet::new(),
}
}
}

impl Checkbox {
pub fn items(&self) -> &Vec<String> {
self.menu.items()
}

pub fn position(&self) -> usize {
self.menu.position()
}

pub fn picked_indexes(&self) -> &HashSet<usize> {
&self.picked
}

pub fn get(&self) -> Vec<String> {
self.picked
.iter()
.fold(Vec::<String>::new(), |mut ret, idx| {
ret.push(self.menu.items().get(*idx).unwrap().to_owned());
ret
})
}

pub fn toggle(&mut self) {
if self.picked.contains(&self.menu.position()) {
self.picked.remove(&self.menu.position());
} else {
self.picked.insert(self.menu.position());
}
}

pub fn backward(&mut self) -> bool {
self.menu.backward()
}

pub fn forward(&mut self) -> bool {
self.menu.forward()
}

pub fn move_to_head(&mut self) {
self.menu.move_to_head()
}

pub fn move_to_tail(&mut self) {
self.menu.move_to_tail()
}
}
68 changes: 68 additions & 0 deletions src/core/checkbox/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
use std::{fmt::Display, iter::FromIterator};

use crate::{crossterm::style::ContentStyle, error::Result, render::State};

use super::{Checkbox, Renderer};

#[derive(Clone)]
pub struct Builder {
checkbox: Checkbox,
style: ContentStyle,
mark: String,
cursor: String,
cursor_style: ContentStyle,
lines: Option<usize>,
}

impl Builder {
pub fn new<T: Display, I: IntoIterator<Item = T>>(items: I) -> Self {
Self {
checkbox: Checkbox::from_iter(items),
style: Default::default(),
mark: Default::default(),
cursor: Default::default(),
cursor_style: Default::default(),
lines: Default::default(),
}
}

pub fn mark<T: AsRef<str>>(mut self, mark: T) -> Self {
self.mark = mark.as_ref().to_string();
self
}

pub fn style(mut self, style: ContentStyle) -> Self {
self.style = style;
self
}

pub fn cursor<T: AsRef<str>>(mut self, cursor: T) -> Self {
self.cursor = cursor.as_ref().to_string();
self
}

pub fn cursor_style(mut self, style: ContentStyle) -> Self {
self.cursor_style = style;
self
}

pub fn lines(mut self, lines: usize) -> Self {
self.lines = Some(lines);
self
}

pub fn build(self) -> Result<Renderer> {
Ok(Renderer {
checkbox: self.checkbox,
mark: self.mark,
style: self.style,
cursor: self.cursor,
cursor_style: self.cursor_style,
lines: self.lines,
})
}

pub fn build_state(self) -> Result<Box<State<Renderer>>> {
Ok(Box::new(State::<Renderer>::new(self.build()?)))
}
}
130 changes: 130 additions & 0 deletions src/core/checkbox/render.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
use std::any::Any;

use crate::{
crossterm::{
event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers},
style::ContentStyle,
},
grapheme::{trim, Graphemes},
pane::Pane,
render::{AsAny, Renderable},
};

use super::Checkbox;

#[derive(Clone)]
pub struct Renderer {
pub checkbox: Checkbox,

pub style: ContentStyle,
pub cursor: String,
pub cursor_style: ContentStyle,
pub mark: String,
pub lines: Option<usize>,
}

impl Renderable for Renderer {
fn make_pane(&self, width: u16) -> Pane {
let f = |idx: usize, item: &String| -> String {
if self.checkbox.picked_indexes().contains(&idx) {
format!("[{}] {}", self.mark, item)
} else {
format!(
"[{}] {}",
" ".repeat(Graphemes::new(self.mark.clone()).widths()),
item
)
}
};

let matrix = self
.checkbox
.items()
.iter()
.enumerate()
.map(|(i, item)| {
if i == self.checkbox.position() {
Graphemes::new_with_style(
format!("{}{}", self.cursor, f(i, item)),
self.cursor_style,
)
} else {
Graphemes::new_with_style(
format!(
"{}{}",
" ".repeat(Graphemes::new(self.cursor.clone()).widths()),
f(i, item)
),
self.style,
)
}
})
.collect::<Vec<Graphemes>>();

let trimed = matrix.iter().map(|row| trim(width as usize, row)).collect();

Pane::new(trimed, self.checkbox.position(), self.lines)
}

/// Default key bindings for item picker.
///
/// | Key | Description
/// | :-- | :--
/// | <kbd> Enter </kbd> | Exit the event-loop
/// | <kbd> CTRL + C </kbd> | Exit the event-loop with an error
/// | <kbd> ↑ </kbd> | Move backward
/// | <kbd> ↓ </kbd> | Move forward
/// | <kbd> CTRL + A </kbd> | Move to the beginning of the items
/// | <kbd> CTRL + E </kbd> | Move to the end of the items
fn handle_event(&mut self, event: &Event) {
match event {
// Move cursor.
Event::Key(KeyEvent {
code: KeyCode::Up,
modifiers: KeyModifiers::NONE,
kind: KeyEventKind::Press,
state: KeyEventState::NONE,
}) => {
self.checkbox.backward();
}
Event::Key(KeyEvent {
code: KeyCode::Down,
modifiers: KeyModifiers::NONE,
kind: KeyEventKind::Press,
state: KeyEventState::NONE,
}) => {
self.checkbox.forward();
}
Event::Key(KeyEvent {
code: KeyCode::Char('a'),
modifiers: KeyModifiers::CONTROL,
kind: KeyEventKind::Press,
state: KeyEventState::NONE,
}) => self.checkbox.move_to_head(),
Event::Key(KeyEvent {
code: KeyCode::Char('e'),
modifiers: KeyModifiers::CONTROL,
kind: KeyEventKind::Press,
state: KeyEventState::NONE,
}) => self.checkbox.move_to_tail(),
Event::Key(KeyEvent {
code: KeyCode::Char(' '),
modifiers: KeyModifiers::NONE,
kind: KeyEventKind::Press,
state: KeyEventState::NONE,
}) => self.checkbox.toggle(),

_ => (),
}
}

fn postrun(&mut self) {
self.checkbox.move_to_head()
}
}

impl AsAny for Renderer {
fn as_any(&self) -> &dyn Any {
self
}
}
4 changes: 4 additions & 0 deletions src/grapheme.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ impl Grapheme {
Grapheme::new_with_style(ch, ContentStyle::new())
}

pub fn width(&self) -> usize {
self.width
}

pub fn new_with_style(ch: char, style: ContentStyle) -> Self {
Self {
ch,
Expand Down
2 changes: 2 additions & 0 deletions src/preset.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
mod checkbox;
pub use checkbox::Checkbox;
mod confirm;
pub use confirm::Confirm;
mod readline;
Expand Down
66 changes: 66 additions & 0 deletions src/preset/checkbox.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
use std::fmt::Display;

use crate::{
checkbox::{Builder as MenuRendererBuilder, Renderer as MenuRenderer},
error::Result,
preset::theme::checkbox::Theme,
render::{Renderable, State},
text::Builder as TextRendererBuilder,
Prompt,
};

pub struct Checkbox {
title_builder: TextRendererBuilder,
menu_builder: MenuRendererBuilder,
}

impl Checkbox {
pub fn new<T: Display, I: IntoIterator<Item = T>>(items: I) -> Self {
Self {
title_builder: Default::default(),
menu_builder: MenuRendererBuilder::new(items),
}
.theme(Theme::default())
}

pub fn theme(mut self, theme: Theme) -> Self {
self.title_builder = self.title_builder.style(theme.title_style);
self.menu_builder = self
.menu_builder
.mark(theme.mark)
.cursor(theme.cursor)
.style(theme.item_style)
.cursor_style(theme.cursor_style);
self
}

pub fn title<T: AsRef<str>>(mut self, text: T) -> Self {
self.title_builder = self.title_builder.text(text);
self
}

pub fn lines(mut self, lines: usize) -> Self {
self.menu_builder = self.menu_builder.lines(lines);
self
}

pub fn prompt(self) -> Result<Prompt<Vec<String>>> {
Prompt::try_new(
vec![
self.title_builder.build_state()?,
self.menu_builder.build_state()?,
],
|_, _| Ok(true),
|renderables: &Vec<Box<dyn Renderable + 'static>>| -> Result<Vec<String>> {
Ok(renderables[1]
.as_any()
.downcast_ref::<State<MenuRenderer>>()
.unwrap()
.after
.borrow()
.checkbox
.get())
},
)
}
}
1 change: 1 addition & 0 deletions src/preset/theme.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod checkbox;
pub mod confirm;
pub mod password;
pub mod queryselect;
Expand Down
Loading

0 comments on commit e077ac4

Please sign in to comment.