Multi shortcuts!

This commit is contained in:
ClementTsang 2021-12-31 23:20:51 -05:00
parent b60704ccc1
commit d518f6564d
6 changed files with 236 additions and 41 deletions

View File

@ -91,7 +91,6 @@ fn main() -> Result<()> {
tuine::launch_with_application(app, receiver, &mut terminal)?; // FIXME: Move terminal construction INSIDE tuine::launch_with_application(app, receiver, &mut terminal)?; // FIXME: Move terminal construction INSIDE
// I think doing it in this order is safe...
*thread_termination_lock.lock().unwrap() = true; *thread_termination_lock.lock().unwrap() = true;
thread_termination_cvar.notify_all(); thread_termination_cvar.notify_all();

View File

@ -20,7 +20,7 @@ use std::{
}; };
use crossterm::{ use crossterm::{
event::{poll, read, DisableMouseCapture, EnableMouseCapture, MouseEventKind}, event::{read, DisableMouseCapture, EnableMouseCapture, MouseEventKind},
execute, execute,
style::Print, style::Print,
terminal::{ terminal::{

View File

@ -1,39 +1,54 @@
use std::{
collections::hash_map::Entry,
panic::Location,
time::{Duration, Instant},
};
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use tui::{backend::Backend, layout::Rect, Frame}; use tui::{backend::Backend, layout::Rect, Frame};
use crate::tuine::{ use crate::tuine::{
Bounds, DrawContext, Event, LayoutNode, Size, StateContext, Status, TmpComponent, Bounds, DrawContext, Event, Key, LayoutNode, Size, StateContext, StatefulComponent, Status,
TmpComponent,
}; };
enum MultiShortcutStep<Message, Child> const MAX_TIMEOUT: Duration = Duration::from_millis(400);
where
Child: TmpComponent<Message>, #[derive(Debug, PartialEq)]
{ enum ShortcutTriggerState {
NextStep(Event), /// Currently not waiting on any next input.
Action( Idle,
Box< /// Waiting for the next input, initially triggered at [`Instant`].
dyn Fn( Waiting {
&mut Child, /// When it was initially triggered.
&mut StateContext<'_>, trigger_instant: Instant,
&DrawContext<'_>,
Event, /// The currently built-up list of events.
&mut Vec<Message>, current_events: Vec<Event>,
) -> Status, },
>,
),
} }
/// A [`Component`] to handle keyboard shortcuts and assign actions to them. impl Default for ShortcutTriggerState {
/// fn default() -> Self {
/// Inspired by [Flutter's approach](https://docs.flutter.dev/development/ui/advanced/actions_and_shortcuts). ShortcutTriggerState::Idle
}
}
#[derive(Debug, PartialEq, Default)]
pub struct ShortcutState {
trigger_state: ShortcutTriggerState,
forest: FxHashMap<Vec<Event>, bool>,
}
/// Properties for a [`Shortcut`].
#[derive(Default)] #[derive(Default)]
pub struct Shortcut<Message, Child> pub struct ShortcutProps<Message, Child>
where where
Child: TmpComponent<Message>, Child: TmpComponent<Message>,
{ {
child: Option<Child>, child: Option<Child>,
shortcuts: FxHashMap< shortcuts: FxHashMap<
Event, Vec<Event>,
Box< Box<
dyn Fn( dyn Fn(
&mut Child, &mut Child,
@ -44,28 +59,27 @@ where
) -> Status, ) -> Status,
>, >,
>, >,
multi_shortcuts: FxHashMap<Event, MultiShortcutStep<Message, Child>>,
enabled_multi_shortcuts: FxHashMap<Event, MultiShortcutStep<Message, Child>>,
} }
impl<Message, Child> Shortcut<Message, Child> impl<Message, Child> ShortcutProps<Message, Child>
where where
Child: TmpComponent<Message>, Child: TmpComponent<Message>,
{ {
/// Creates a new [`ShortcutProps`] with a child.
pub fn with_child(child: Child) -> Self { pub fn with_child(child: Child) -> Self {
Self { Self {
child: Some(child), child: Some(child),
shortcuts: Default::default(), shortcuts: Default::default(),
multi_shortcuts: Default::default(),
enabled_multi_shortcuts: Default::default(),
} }
} }
/// Sets the child of the [`ShortcutProps`].
pub fn child(mut self, child: Option<Child>) -> Self { pub fn child(mut self, child: Option<Child>) -> Self {
self.child = child; self.child = child;
self self
} }
/// Inserts a shortcut that only needs a single [`Event`].
pub fn shortcut( pub fn shortcut(
mut self, event: Event, mut self, event: Event,
f: Box< f: Box<
@ -78,16 +92,110 @@ where
) -> Status, ) -> Status,
>, >,
) -> Self { ) -> Self {
self.shortcuts.insert(event, f); self.shortcuts.insert(vec![event], f);
self self
} }
pub fn remove_shortcut(mut self, event: &Event) -> Self { /// Inserts a shortcut that can take one or more [`Event`]s.
self.shortcuts.remove(event); pub fn multi_shortcut(
mut self, events: Vec<Event>,
f: Box<
dyn Fn(
&mut Child,
&mut StateContext<'_>,
&DrawContext<'_>,
Event,
&mut Vec<Message>,
) -> Status,
>,
) -> Self {
self.shortcuts.insert(events, f);
self self
} }
} }
/// A [`Component`] to handle keyboard shortcuts and assign actions to them.
///
/// Inspired by [Flutter's approach](https://docs.flutter.dev/development/ui/advanced/actions_and_shortcuts).
pub struct Shortcut<Message, Child>
where
Child: TmpComponent<Message>,
{
key: Key,
child: Option<Child>,
shortcuts: FxHashMap<
Vec<Event>,
Box<
dyn Fn(
&mut Child,
&mut StateContext<'_>,
&DrawContext<'_>,
Event,
&mut Vec<Message>,
) -> Status,
>,
>,
}
impl<Message, Child> StatefulComponent<Message> for Shortcut<Message, Child>
where
Child: TmpComponent<Message>,
{
type Properties = ShortcutProps<Message, Child>;
type ComponentState = ShortcutState;
fn build(ctx: &mut crate::tuine::ViewContext<'_>, props: Self::Properties) -> Self {
let (key, state) =
ctx.register_and_mut_state::<_, Self::ComponentState>(Location::caller());
let mut forest: FxHashMap<Vec<Event>, bool> = FxHashMap::default();
props.shortcuts.iter().for_each(|(events, _action)| {
if !events.is_empty() {
let mut visited = vec![];
let last = events.len() - 1;
for (itx, event) in events.iter().enumerate() {
visited.push(*event);
match forest.entry(visited.clone()) {
Entry::Occupied(mut occupied) => {
*occupied.get_mut() = *occupied.get() || itx == last;
}
Entry::Vacant(vacant) => {
vacant.insert(itx == last);
}
}
}
}
});
if forest != state.forest {
// Invalidate state.
*state = ShortcutState {
trigger_state: ShortcutTriggerState::Idle,
forest,
};
} else if let ShortcutTriggerState::Waiting {
trigger_instant,
current_events: _,
} = state.trigger_state
{
if Instant::now().duration_since(trigger_instant) > MAX_TIMEOUT {
// Invalidate state.
*state = ShortcutState {
trigger_state: ShortcutTriggerState::Idle,
forest,
};
}
}
Shortcut {
key,
child: props.child,
shortcuts: props.shortcuts,
}
}
}
impl<'a, Message, Child> TmpComponent<Message> for Shortcut<Message, Child> impl<'a, Message, Child> TmpComponent<Message> for Shortcut<Message, Child>
where where
Child: TmpComponent<Message>, Child: TmpComponent<Message>,
@ -116,8 +224,63 @@ where
return Status::Captured; return Status::Captured;
} }
Status::Ignored => { Status::Ignored => {
if let Some(f) = self.shortcuts.get(&event) { let state = state_ctx.mut_state::<ShortcutState>(self.key);
return f(child, state_ctx, &child_draw_ctx, event, messages); match &state.trigger_state {
ShortcutTriggerState::Idle => {
let current_events = vec![event];
if let Some(&should_fire) = state.forest.get(&current_events) {
state.trigger_state = ShortcutTriggerState::Waiting {
trigger_instant: Instant::now(),
current_events: current_events.clone(),
};
if should_fire {
if let Some(f) = self.shortcuts.get(&current_events) {
return f(
child,
state_ctx,
&child_draw_ctx,
event,
messages,
);
}
}
}
}
ShortcutTriggerState::Waiting {
trigger_instant,
current_events,
} => {
if Instant::now().duration_since(*trigger_instant) > MAX_TIMEOUT {
state.trigger_state = ShortcutTriggerState::Idle;
return self.on_event(state_ctx, draw_ctx, event, messages);
} else {
let mut current_events = current_events.clone();
current_events.push(event);
if let Some(&should_fire) = state.forest.get(&current_events) {
state.trigger_state = ShortcutTriggerState::Waiting {
trigger_instant: Instant::now(),
current_events: current_events.clone(),
};
if should_fire {
if let Some(f) = self.shortcuts.get(&current_events) {
return f(
child,
state_ctx,
&child_draw_ctx,
event,
messages,
);
}
}
} else {
state.trigger_state = ShortcutTriggerState::Idle;
return self.on_event(state_ctx, draw_ctx, event, messages);
}
}
}
} }
} }
} }

View File

@ -44,13 +44,13 @@ pub struct StyleSheet {
#[derive(PartialEq, Default)] #[derive(PartialEq, Default)]
pub struct TextTableState { pub struct TextTableState {
scroll: ScrollState, pub scroll: ScrollState,
sort: SortType, sort: SortType,
} }
/// A sortable, scrollable table for text data. /// A sortable, scrollable table for text data.
pub struct TextTable<Message> { pub struct TextTable<Message> {
key: Key, pub key: Key,
column_widths: Vec<u16>, column_widths: Vec<u16>,
columns: Vec<TextColumn>, columns: Vec<TextColumn>,
show_selected_entry: bool, show_selected_entry: bool,

View File

@ -1,4 +1,4 @@
use crate::tuine::{State, ViewContext}; use crate::tuine::{State, StateContext, ViewContext};
use super::TmpComponent; use super::TmpComponent;

View File

@ -1,9 +1,11 @@
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use tui::style::Style; use tui::style::Style;
use crate::tuine::{ use crate::tuine::{
self, block, self, block,
text_table::{self, DataRow, SortType, TextTableProps}, shortcut::ShortcutProps,
Block, Shortcut, StatefulComponent, TextTable, TmpComponent, ViewContext, text_table::{self, DataRow, SortType, TextTableProps, TextTableState},
Block, Event, Shortcut, StatefulComponent, Status, TextTable, TmpComponent, ViewContext,
}; };
/// A set of styles for a [`SimpleTable`]. /// A set of styles for a [`SimpleTable`].
@ -29,7 +31,7 @@ impl<Message> SimpleTable<Message> {
ctx: &mut ViewContext<'_>, style: StyleSheet, columns: Vec<C>, data: Vec<R>, ctx: &mut ViewContext<'_>, style: StyleSheet, columns: Vec<C>, data: Vec<R>,
sort_index: usize, sort_index: usize,
) -> Self { ) -> Self {
let shortcut = Shortcut::with_child(TextTable::build( let text_table = TextTable::build(
ctx, ctx,
TextTableProps::new(columns) TextTableProps::new(columns)
.rows(data) .rows(data)
@ -39,7 +41,38 @@ impl<Message> SimpleTable<Message> {
selected_text: style.selected_text, selected_text: style.selected_text,
table_header: style.table_header, table_header: style.table_header,
}), }),
)); );
let shortcut = Shortcut::build(
ctx,
ShortcutProps::with_child(text_table)
.shortcut(
Event::Keyboard(KeyEvent::new(KeyCode::Char('G'), KeyModifiers::empty())),
Box::new(|t, s, _d, _e, _m| {
let state = s.mut_state::<TextTableState>(t.key);
state.scroll.jump_to_last();
Status::Captured
}),
)
.shortcut(
Event::Keyboard(KeyEvent::new(KeyCode::Char('G'), KeyModifiers::SHIFT)),
Box::new(|t, s, _d, _e, _m| {
let state = s.mut_state::<TextTableState>(t.key);
state.scroll.jump_to_last();
Status::Captured
}),
)
.multi_shortcut(
vec![
Event::Keyboard(KeyEvent::new(KeyCode::Char('g'), KeyModifiers::empty())),
Event::Keyboard(KeyEvent::new(KeyCode::Char('g'), KeyModifiers::empty())),
],
Box::new(|t, s, _d, _e, _m| {
let state = s.mut_state::<TextTableState>(t.key);
state.scroll.jump_to_first();
Status::Captured
}),
),
);
Self { Self {
inner: Block::with_child(shortcut).style(block::StyleSheet { inner: Block::with_child(shortcut).style(block::StyleSheet {