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
// I think doing it in this order is safe...
*thread_termination_lock.lock().unwrap() = true;
thread_termination_cvar.notify_all();

View File

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

View File

@ -1,39 +1,54 @@
use std::{
collections::hash_map::Entry,
panic::Location,
time::{Duration, Instant},
};
use rustc_hash::FxHashMap;
use tui::{backend::Backend, layout::Rect, Frame};
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>
where
Child: TmpComponent<Message>,
{
NextStep(Event),
Action(
Box<
dyn Fn(
&mut Child,
&mut StateContext<'_>,
&DrawContext<'_>,
Event,
&mut Vec<Message>,
) -> Status,
>,
),
const MAX_TIMEOUT: Duration = Duration::from_millis(400);
#[derive(Debug, PartialEq)]
enum ShortcutTriggerState {
/// Currently not waiting on any next input.
Idle,
/// Waiting for the next input, initially triggered at [`Instant`].
Waiting {
/// When it was initially triggered.
trigger_instant: Instant,
/// The currently built-up list of events.
current_events: Vec<Event>,
},
}
/// 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).
impl Default for ShortcutTriggerState {
fn default() -> Self {
ShortcutTriggerState::Idle
}
}
#[derive(Debug, PartialEq, Default)]
pub struct ShortcutState {
trigger_state: ShortcutTriggerState,
forest: FxHashMap<Vec<Event>, bool>,
}
/// Properties for a [`Shortcut`].
#[derive(Default)]
pub struct Shortcut<Message, Child>
pub struct ShortcutProps<Message, Child>
where
Child: TmpComponent<Message>,
{
child: Option<Child>,
shortcuts: FxHashMap<
Event,
Vec<Event>,
Box<
dyn Fn(
&mut Child,
@ -44,28 +59,27 @@ where
) -> 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
Child: TmpComponent<Message>,
{
/// Creates a new [`ShortcutProps`] with a child.
pub fn with_child(child: Child) -> Self {
Self {
child: Some(child),
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 {
self.child = child;
self
}
/// Inserts a shortcut that only needs a single [`Event`].
pub fn shortcut(
mut self, event: Event,
f: Box<
@ -78,16 +92,110 @@ where
) -> Status,
>,
) -> Self {
self.shortcuts.insert(event, f);
self.shortcuts.insert(vec![event], f);
self
}
pub fn remove_shortcut(mut self, event: &Event) -> Self {
self.shortcuts.remove(event);
/// Inserts a shortcut that can take one or more [`Event`]s.
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
}
}
/// 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>
where
Child: TmpComponent<Message>,
@ -116,8 +224,63 @@ where
return Status::Captured;
}
Status::Ignored => {
if let Some(f) = self.shortcuts.get(&event) {
return f(child, state_ctx, &child_draw_ctx, event, messages);
let state = state_ctx.mut_state::<ShortcutState>(self.key);
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)]
pub struct TextTableState {
scroll: ScrollState,
pub scroll: ScrollState,
sort: SortType,
}
/// A sortable, scrollable table for text data.
pub struct TextTable<Message> {
key: Key,
pub key: Key,
column_widths: Vec<u16>,
columns: Vec<TextColumn>,
show_selected_entry: bool,

View File

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

View File

@ -1,9 +1,11 @@
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use tui::style::Style;
use crate::tuine::{
self, block,
text_table::{self, DataRow, SortType, TextTableProps},
Block, Shortcut, StatefulComponent, TextTable, TmpComponent, ViewContext,
shortcut::ShortcutProps,
text_table::{self, DataRow, SortType, TextTableProps, TextTableState},
Block, Event, Shortcut, StatefulComponent, Status, TextTable, TmpComponent, ViewContext,
};
/// 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>,
sort_index: usize,
) -> Self {
let shortcut = Shortcut::with_child(TextTable::build(
let text_table = TextTable::build(
ctx,
TextTableProps::new(columns)
.rows(data)
@ -39,7 +41,38 @@ impl<Message> SimpleTable<Message> {
selected_text: style.selected_text,
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 {
inner: Block::with_child(shortcut).style(block::StyleSheet {