mirror of
https://github.com/ClementTsang/bottom.git
synced 2025-07-27 07:34:27 +02:00
Multi shortcuts!
This commit is contained in:
parent
b60704ccc1
commit
d518f6564d
@ -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();
|
||||
|
||||
|
@ -20,7 +20,7 @@ use std::{
|
||||
};
|
||||
|
||||
use crossterm::{
|
||||
event::{poll, read, DisableMouseCapture, EnableMouseCapture, MouseEventKind},
|
||||
event::{read, DisableMouseCapture, EnableMouseCapture, MouseEventKind},
|
||||
execute,
|
||||
style::Print,
|
||||
terminal::{
|
||||
|
@ -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(¤t_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(¤t_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(¤t_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(¤t_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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -1,4 +1,4 @@
|
||||
use crate::tuine::{State, ViewContext};
|
||||
use crate::tuine::{State, StateContext, ViewContext};
|
||||
|
||||
use super::TmpComponent;
|
||||
|
||||
|
@ -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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user