477 lines
13 KiB
Rust
Raw Normal View History

2019-04-02 15:19:39 -04:00
use cursive::views::{Dialog, TextView, OnEventView, PaddedView, DialogFocus, EditView, ListView, LinearLayout, DummyView };
use cursive::traits::{View, Finder,Boxable,Identifiable,Scrollable};
use cursive::event::{EventResult, Event, EventTrigger};
use cursive::event::Key;
use cursive::Cursive;
use std::rc::Rc;
use cursive::view::ViewWrapper;
use cursive::direction::Direction;
use cursive::theme::ColorStyle;
pub fn confirm_dialog<F>(title: &str, message: &str, cb: F) -> impl View
where F: 'static + Fn(&mut Cursive)
{
let content = PaddedView::new((2,2,2,1), TextView::new(message));
let dialog = Dialog::around(content)
.title(title)
.button("Yes", move |s| {
s.pop_layer();
(cb)(s);
})
.dismiss_button("No");
OnEventView::new(dialog)
.on_event_inner('y', |d: &mut Dialog, _| {
Some(d.on_event(Event::Key(Key::Left)))
})
.on_event_inner('n', move |d: &mut Dialog, _| {
Some(d.on_event(Event::Key(Key::Right)))
})
// Eat these global events
.on_event_inner('?', |_,_| {
Some(EventResult::Consumed(None))
})
.on_event_inner('T', |_,_| {
Some(EventResult::Consumed(None))
})
}
// Set focus on dialog button at index `idx` by injecting events
// into the Dialog view.
//
// If the dialog content is currently in focus send Tab
// character to move focus to first button.
//
// Then inject Right/Left key events as needed to select
// the correct index.
pub fn select_dialog_button_index(dialog: &mut Dialog, idx: usize) {
let mut current = match dialog.focus() {
DialogFocus::Content => {
dialog.on_event(Event::Key(Key::Tab));
0
},
DialogFocus::Button(n) => {
n
},
};
while current < idx {
dialog.on_event(Event::Key(Key::Right));
current += 1;
}
while current > idx {
dialog.on_event(Event::Key(Key::Left));
current -= 1;
}
}
pub fn keyboard_navigation_adapter(dialog: Dialog, keys: &'static str) -> OnEventView<Dialog> {
// a trigger that matches any character in 'keys'
let trigger = EventTrigger::from_fn(move |ev| match ev {
Event::Char(c) => keys.contains(|ch: char| ch == *c),
_ => false,
});
OnEventView::new(dialog)
// The button navigation is a hack that depends on Dialog internal behavior
.on_event_inner(trigger, move |d: &mut Dialog,ev| {
if let Event::Char(c) = ev {
if let Some(idx) = keys.find(|ch: char| ch == *c) {
select_dialog_button_index(d, idx);
return Some(EventResult::Consumed(None))
}
}
None
})
.on_event_inner(Key::Enter, |v,_| Some(v.on_event(Event::Key(Key::Down))))
// 'q' to close dialog, but first see if some component of the dialog
// (such as a text field) wants this event
.on_pre_event_inner('q', |v,e| {
let result = match v.on_event(e.clone()) {
EventResult::Consumed(cb) => EventResult::Consumed(cb),
EventResult::Ignored => EventResult::with_cb(|s| {s.pop_layer();}),
};
Some(result)
})
// Eat these global events
.on_event_inner('?', |_,_| {
Some(EventResult::Consumed(None))
})
.on_event_inner('T', |_,_| {
Some(EventResult::Consumed(None))
})
.on_event(Key::Esc, |s| {
s.pop_layer();
})
}
pub struct FieldDialogBuilder {
layout: FieldLayout,
id: &'static str,
title: Option<&'static str>,
height: Option<usize>,
}
#[allow(dead_code)]
impl FieldDialogBuilder {
const DEFAULT_ID: &'static str = "field-dialog";
pub fn new(labels: &[&str], message: &str) -> Self {
FieldDialogBuilder {
layout: FieldLayout::new(labels, message),
id: Self::DEFAULT_ID,
title: None,
height: None,
}
}
pub fn add_edit_view(&mut self, id: &str, width: usize) {
self.layout.add_edit_view(id, width);
}
pub fn edit_view(mut self, id: &str, width: usize) -> Self {
self.add_edit_view(id, width);
self
}
pub fn add_field<V: View>(&mut self, view: V) {
self.layout.add_field(view);
}
pub fn field<V: View>(mut self, view: V) -> Self {
self.add_field(view);
self
}
pub fn set_id(&mut self, id: &'static str) {
self.id = id;
}
pub fn id(mut self, id: &'static str) -> Self {
self.set_id(id);
self
}
pub fn set_title(&mut self, title: &'static str) {
self.title = Some(title);
}
pub fn title(mut self, title: &'static str) -> Self {
self.set_title(title);
self
}
pub fn set_width(&mut self, width: usize) {
self.layout.set_width(width);
}
pub fn width(mut self, width: usize) -> Self {
self.set_width(width);
self
}
pub fn set_height(&mut self, height: usize) {
2020-06-19 10:48:39 -04:00
self.height = Some(height);
2019-04-02 15:19:39 -04:00
}
pub fn height(mut self, height: usize) -> Self {
self.set_height(height);
self
}
pub fn build<F: 'static + Fn(&mut Cursive)>(self, ok_cb: F) -> impl View {
let content = self.layout.build()
.padded(2,2,1,2);
let mut dialog = Dialog::around(content)
.dismiss_button("Cancel")
.button("Ok", ok_cb);
if let Some(title) = self.title {
dialog.set_title(title);
}
let height = self.height.unwrap_or(12);
dialog.with_id(self.id)
.min_height(height)
}
}
pub struct FieldLayout {
list: ListView,
message: String,
labels: Vec<String>,
index: usize,
width: usize,
}
#[allow(dead_code)]
impl FieldLayout {
const DEFAULT_WIDTH: usize = 48;
pub fn new(labels: &[&str], message: &str) -> FieldLayout {
let maxlen = labels.iter().fold(0, |max, &s| {
if s.len() > max { s.len() } else { max }
});
let pad_label = |s: &&str| {
if s.is_empty() {
" ".repeat(maxlen + 2)
} else {
" ".repeat(maxlen - s.len()) + s + ": "
}
};
let labels = labels
2019-04-03 16:05:09 -04:00
.iter()
2019-04-02 15:19:39 -04:00
.map(pad_label)
.collect();
FieldLayout {
list: ListView::new(),
message: message.to_string(),
labels,
index: 0,
width: Self::DEFAULT_WIDTH,
}
}
pub fn add_edit_view(&mut self, id: &str, width: usize) {
self.add_field(EditView::new()
.style(ColorStyle::tertiary())
.filler(" ")
.with_id(id)
.fixed_width(width))
}
pub fn edit_view(mut self, id: &str, width: usize) -> Self {
self.add_edit_view(id, width);
self
}
pub fn add_field<V: View>(&mut self, view: V) {
let field = LinearLayout::horizontal()
.child(view)
.child(DummyView);
let idx = self.index;
self.index += 1;
if idx > 0 {
self.list.add_delimiter();
}
self.list.add_child(self.labels[idx].as_str(), field);
}
pub fn field<V: View>(mut self, view: V) -> Self {
self.add_field(view);
self
}
pub fn set_width(&mut self, width: usize) {
self.width = width;
}
pub fn width(mut self, width: usize) -> Self {
self.set_width(width);
self
}
pub fn build(self) -> impl View {
LinearLayout::vertical()
.child(TextView::new(self.message).fixed_width(self.width))
.child(DummyView.fixed_height(2))
.child(self.list)
.child(DummyView)
.scrollable()
}
}
pub trait DialogButtonAdapter: Finder+ViewWrapper {
fn inner_id(&self) -> &'static str;
fn call_on_dialog<F,R>(&mut self, cb: F) -> R
where F: FnOnce(&mut Dialog) -> R
{
let id = self.inner_id();
self.call_on_id(id, cb)
2019-04-03 16:05:09 -04:00
.unwrap_or_else(|| panic!("failed call_on_id({})", id))
2019-04-02 15:19:39 -04:00
}
fn button_enabled(&mut self, button: usize) -> bool {
self.call_on_dialog(|d| {
d.buttons_mut()
.nth(button)
.map(|b| b.is_enabled())
.unwrap_or(false)
})
}
fn set_button_enabled(&mut self, button: usize, enabled: bool) {
self.call_on_dialog(|d| {
2019-04-03 16:05:09 -04:00
if let Some(b) = d.buttons_mut().nth(button) {
b.set_enabled(enabled);
}
2019-04-02 15:19:39 -04:00
})
}
fn select_button(&mut self, button: usize) -> EventResult {
if self.button_enabled(button) {
self.call_on_dialog(|d| select_dialog_button_index(d, button))
}
EventResult::Consumed(None)
}
fn navigate_to_button(&mut self, idx: usize) -> EventResult {
if !self.button_enabled(idx) {
return EventResult::Ignored;
}
self.call_on_dialog(|d| {
d.take_focus(Direction::down());
let mut current = d.buttons_len() - 1;
while current > idx {
d.on_event(Event::Key(Key::Left));
current -= 1;
}
});
EventResult::Consumed(None)
}
fn handle_char_event(&mut self, button_order: &str, ch: char) -> EventResult {
if let Some(EventResult::Consumed(cb)) = self.with_view_mut(|v| v.on_event(Event::Char(ch))) {
EventResult::Consumed(cb)
} else if ch == 'T' || ch == '?' {
EventResult::Consumed(None)
} else if let Some(idx) = button_order.find(|c| c == ch) {
self.navigate_to_button(idx)
} else {
EventResult::Ignored
}
}
fn handle_event(&mut self, button_order: &str, event: Event) -> EventResult {
match event {
Event::Char(ch) => self.handle_char_event(button_order, ch),
event => self.with_view_mut(|v| v.on_event(event)).unwrap()
}
}
}
pub trait Padable: View + Sized {
fn padded(self, left: usize, right: usize, top: usize, bottom: usize) -> PaddedView<Self> {
PaddedView::new((left,right,top,bottom), self)
}
}
impl <T: View> Padable for T {}
pub trait Validatable: View+Finder+Sized {
fn validator<F: 'static + Fn(&str) -> ValidatorResult>(mut self, id: &str, cb: F) -> Self {
TextValidator::set_validator(&mut self, id, cb);
self
}
}
impl <T: View+Finder> Validatable for T {}
pub enum ValidatorResult {
Allow(Box<dyn Fn(&mut Cursive)>),
Deny(Box<dyn Fn(&mut Cursive)>),
}
impl ValidatorResult {
pub fn create<F>(ok: bool, f: F) -> Self
where F: 'static + Fn(&mut Cursive) {
if ok {
Self::allow_with(f)
} else {
Self::deny_with(f)
}
}
pub fn allow_with<F>(f: F) -> Self
where F: 'static + Fn(&mut Cursive)
{
ValidatorResult::Allow(Box::new(f))
}
pub fn deny_with<F>(f: F) -> Self
where F: 'static + Fn(&mut Cursive)
{
ValidatorResult::Deny(Box::new(f))
}
fn process(self, siv: &mut Cursive) {
match self {
2019-04-03 16:05:09 -04:00
ValidatorResult::Allow(cb) | ValidatorResult::Deny(cb) => (cb)(siv),
2019-04-02 15:19:39 -04:00
}
}
fn deny_edit(&self) -> bool {
match self {
ValidatorResult::Allow(_) => false,
ValidatorResult::Deny(_) => true,
}
}
}
#[derive(Clone)]
pub struct TextValidator {
id: String,
2020-06-19 10:48:39 -04:00
is_valid: Rc<Box<dyn Fn(&str) -> ValidatorResult>>,
2019-04-02 15:19:39 -04:00
}
impl TextValidator {
pub fn set_validator<V: Finder,F: 'static + Fn(&str)->ValidatorResult>(view: &mut V, id: &str, cb: F) {
let validator = TextValidator{ id: id.to_string(), is_valid: Rc::new(Box::new(cb)) };
view.call_on_id(id, |v: &mut EditView| {
v.set_on_edit(move |s,content,cursor| {
let v = validator.clone();
v.on_edit(s, content, cursor);
});
});
}
fn on_edit(&self, siv: &mut Cursive, content: &str, cursor: usize) {
let result = (self.is_valid)(content);
if result.deny_edit() {
self.deny_edit(siv, cursor);
}
result.process(siv);
}
fn deny_edit(&self, siv: &mut Cursive, cursor: usize) {
if cursor > 0 {
let callback = self.call_on_edit(siv, |v| {
v.set_cursor(cursor - 1);
v.remove(1)
});
(callback)(siv);
}
}
fn call_on_edit<F,R>(&self, siv: &mut Cursive, f: F) -> R
where F: FnOnce(&mut EditView) -> R {
siv.call_on_id(&self.id, f)
2019-04-03 16:05:09 -04:00
.unwrap_or_else(|| panic!("call_on_id({})", self.id))
2019-04-02 15:19:39 -04:00
}
}