mirror of
https://github.com/ClementTsang/bottom.git
synced 2025-07-27 15:44:17 +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
|
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();
|
||||||
|
|
||||||
|
@ -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::{
|
||||||
|
@ -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(¤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)]
|
#[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,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use crate::tuine::{State, ViewContext};
|
use crate::tuine::{State, StateContext, ViewContext};
|
||||||
|
|
||||||
use super::TmpComponent;
|
use super::TmpComponent;
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user