Skip to content

Commit

Permalink
Add cursor navigation
Browse files Browse the repository at this point in the history
Involved yet another component refactor!
  • Loading branch information
LucasPickering committed Nov 26, 2023
1 parent 1a49302 commit dcf16dd
Show file tree
Hide file tree
Showing 20 changed files with 669 additions and 436 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

- Add setting to toggle cursor capture
- Add help modal
- Add cursor navigation

### Changed

Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ async-trait = "^0.1.73"
chrono = {version = "^0.4.31", default-features = false, features = ["clock", "serde", "std"]}
clap = {version = "^4.4.2", features = ["derive"]}
crossterm = "^0.27.0"
derive_more = {version = "1.0.0-beta.6", features = ["debug", "deref", "display", "from"]}
derive_more = {version = "1.0.0-beta.6", features = ["debug", "deref", "deref_mut", "display", "from"]}
dialoguer = {version = "^0.11.0", default-features = false, features = ["password"]}
dirs = "^5.0.1"
futures = "^0.3.28"
Expand Down
74 changes: 55 additions & 19 deletions src/tui/input.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
//! Logic related to input handling. This is considered part of the controller.

use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
use crossterm::event::{
Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseButton,
MouseEvent, MouseEventKind,
};
use derive_more::Display;
use indexmap::IndexMap;
use std::fmt::Debug;
Expand Down Expand Up @@ -64,28 +67,50 @@ impl InputEngine {

/// Convert an input event into its bound action, if any
pub fn action(&self, event: &Event) -> Option<Action> {
if let Event::Key(
key @ KeyEvent {
kind: KeyEventKind::Press,
..
let action = match event {
// Trigger click on mouse *up* (feels the most natural)
Event::Mouse(MouseEvent { kind, .. }) => match kind {
MouseEventKind::Up(MouseButton::Left) => {
Some(Action::LeftClick)
}
MouseEventKind::Up(MouseButton::Right) => {
Some(Action::RightClick)
}
MouseEventKind::Up(MouseButton::Middle) => None,
MouseEventKind::ScrollDown => Some(Action::ScrollDown),
MouseEventKind::ScrollUp => Some(Action::ScrollUp),
_ => None,
},
) = event
{
// Scan all bindings for a match
let action = self
.bindings
.values()
.find(|binding| binding.matches(key))
.map(|binding| binding.action);

if let Some(action) = action {
trace!(?action, "Input action");

Event::Key(
key @ KeyEvent {
kind: KeyEventKind::Press,
..
},
) => {
// Scan all bindings for a match
let action = self
.bindings
.values()
.find(|binding| binding.matches(key))
.map(|binding| binding.action);

action
}
_ => None,
};

action
} else {
None
if let Some(action) = action {
trace!(?action, "Input action");
}

action
}
}

impl Default for InputEngine {
fn default() -> Self {
Self::new()
}
}

Expand All @@ -98,6 +123,17 @@ impl InputEngine {
/// modal (but doesn't affect behavior).
#[derive(Copy, Clone, Debug, Display, Eq, Hash, PartialEq)]
pub enum Action {
/// This is mapped to mouse events, so it's a bit unique. Use the
/// associated event for button/position info
// #[display("Click")]
// Click(MouseEvent),
// Mouse actions do *not* get mapped, they're hard-coded. Use the
// associated raw event for button/position info if needed
LeftClick,
RightClick,
ScrollUp,
ScrollDown,

/// Exit the app
Quit,
/// A special keybinding that short-circuits the standard view input
Expand Down
50 changes: 31 additions & 19 deletions src/tui/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use crate::{
input::{Action, InputEngine},
message::MessageSender,
view::{
component::root::Root,
component::{root::Root, Component},
draw::{Draw, DrawContext},
event::{Event, EventHandler, Update, UpdateContext},
state::Notification,
Expand All @@ -36,7 +36,7 @@ use tracing::{error, trace, trace_span};
pub struct View {
messages_tx: MessageSender,
config: ViewConfig,
root: Root,
root: Component<Root>,
}

impl View {
Expand All @@ -47,7 +47,7 @@ impl View {
let mut view = Self {
messages_tx,
config: ViewConfig::default(),
root: Root::new(collection),
root: Root::new(collection).into(),
};
// Tell the components to wake up
view.handle_event(Event::Init);
Expand Down Expand Up @@ -124,14 +124,23 @@ impl View {
// Each event being handled could potentially queue more. Keep going
// until the queue is drained
while let Some(event) = event_queue.pop_front() {
// Certain events *just don't matter*, AT ALL. They're not even
// supposed to be around, like, in the area
if event.should_kill() {
continue;
}

let span = trace_span!("View event", ?event);
span.in_scope(|| {
let mut context = UpdateContext::new(
self.messages_tx.clone(),
&mut event_queue,
&mut self.config,
);
match Self::update_all(&mut self.root, &mut context, event) {

let update =
Self::update_all(self.root.as_child(), &mut context, event);
match update {
Update::Consumed => {
trace!("View event consumed")
}
Expand All @@ -148,22 +157,25 @@ impl View {
/// lowest descendant. Recursively walk up the tree until a component
/// consumes the event.
fn update_all(
component: &mut dyn EventHandler,
mut component: Component<&mut dyn EventHandler>,
context: &mut UpdateContext,
mut event: Event,
) -> Update {
// If we have a child, send them the event. If not, eat it ourselves
for child in component.children() {
let outcome = Self::update_all(child, context, event); // RECURSION
match outcome {
Update::Propagate(returned) => {
// Keep going to the next child. It's possible the child
// returned something other than the original event, which
// we'll just pass along anyway.
event = returned;
}
Update::Consumed => {
return outcome;
if event.should_handle(&child) {
let update = Self::update_all(child, context, event); // RECURSION
match update {
Update::Propagate(returned) => {
// Keep going to the next child. It's possible the child
// returned something other than the original event,
// which we'll just pass along
// anyway.
event = returned;
}
Update::Consumed => {
return update;
}
}
}
}
Expand All @@ -173,16 +185,16 @@ impl View {
// TODO figure out a way to print just the component type name
let span = trace_span!("Component handling", ?component);
span.in_scope(|| {
let outcome = component.update(context, event);
trace!(?outcome);
outcome
let update = component.update(context, event);
trace!(?update);
update
})
}
}

/// Settings that control the behavior of the view
#[derive(Debug)]
struct ViewConfig {
pub struct ViewConfig {
/// Should templates be rendered inline in the UI, or should we show the
/// raw text?
preview_templates: bool,
Expand Down
51 changes: 27 additions & 24 deletions src/tui/view/common/modal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ use crate::tui::{
draw::{Draw, DrawContext},
event::{Event, EventHandler, Update, UpdateContext},
util::centered_rect,
Component,
},
};
use ratatui::{
prelude::{Constraint, Rect},
widgets::{Block, BorderType, Borders, Clear},
};
use std::collections::VecDeque;
use std::{collections::VecDeque, ops::DerefMut};
use tracing::trace;

/// A modal (AKA popup or dialog) is a high-priority element to be shown to the
Expand All @@ -31,10 +32,6 @@ pub trait Modal: Draw<()> + EventHandler {
/// Optional callback when the modal is closed. Useful for finishing
/// operations that require ownership of the modal data.
fn on_close(self: Box<Self>) {}

/// Annoying thing to cast from a modal to a base component. Remove after
/// https://github.com/rust-lang/rust/issues/65991
fn as_event_handler(&mut self) -> &mut dyn EventHandler;
}

/// Define how a type can be converted into a modal. Often times, implementors
Expand All @@ -48,9 +45,11 @@ pub trait IntoModal {
fn into_modal(self) -> Self::Target;
}

#[derive(Debug)]
/// A singleton component to hold all modals at the root of the tree, so that
/// they render on top.
#[derive(Debug, Default)]
pub struct ModalQueue {
queue: VecDeque<Box<dyn Modal>>,
queue: VecDeque<Component<Box<dyn Modal>>>,
}

/// Priority defines where in the modal queue to add a new modal. Most modals
Expand All @@ -65,12 +64,6 @@ pub enum ModalPriority {
}

impl ModalQueue {
pub fn new() -> Self {
Self {
queue: VecDeque::new(),
}
}

/// Is there a modal open right now?
pub fn is_open(&self) -> bool {
!self.queue.is_empty()
Expand All @@ -82,18 +75,18 @@ impl ModalQueue {
trace!(?priority, "Opening modal");
match priority {
ModalPriority::Low => {
self.queue.push_back(modal);
self.queue.push_back(modal.into());
}
ModalPriority::High => {
self.queue.push_front(modal);
self.queue.push_front(modal.into());
}
}
}

/// Close the current modal, and return the closed modal if any
pub fn close(&mut self) -> Option<Box<dyn Modal>> {
trace!("Closing modal");
self.queue.pop_front()
self.queue.pop_front().map(Component::into_inner)
}
}

Expand Down Expand Up @@ -128,31 +121,41 @@ impl EventHandler for ModalQueue {
}
}

fn children(&mut self) -> Vec<&mut dyn EventHandler> {
fn children(&mut self) -> Vec<Component<&mut dyn EventHandler>> {
match self.queue.front_mut() {
Some(first) => vec![first.as_event_handler()],
Some(first) => vec![first.as_child()],
None => vec![],
}
}
}

impl Draw for ModalQueue {
fn draw(&self, context: &mut DrawContext, _: (), chunk: Rect) {
fn draw(&self, context: &mut DrawContext, _: (), area: Rect) {
if let Some(modal) = self.queue.front() {
let (x, y) = modal.dimensions();
let chunk = centered_rect(x, y, chunk);
let area = centered_rect(x, y, area);
let block = Block::default()
.title(modal.title())
.borders(Borders::ALL)
.border_type(BorderType::Thick);
let inner_chunk = block.inner(chunk);
let inner_area = block.inner(area);

// Draw the outline of the modal
context.frame.render_widget(Clear, chunk);
context.frame.render_widget(block, chunk);
context.frame.render_widget(Clear, area);
context.frame.render_widget(block, area);

// Render the actual content
modal.draw(context, (), inner_chunk);
modal.draw(context, (), inner_area);
}
}
}

impl EventHandler for Box<dyn Modal> {
fn update(&mut self, context: &mut UpdateContext, event: Event) -> Update {
self.deref_mut().update(context, event)
}

fn children(&mut self) -> Vec<Component<&mut dyn EventHandler>> {
self.deref_mut().children()
}
}
4 changes: 2 additions & 2 deletions src/tui/view/common/tabs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,14 @@ impl<T: FixedSelect> EventHandler for Tabs<T> {
}

impl<T: FixedSelect> Draw for Tabs<T> {
fn draw(&self, context: &mut DrawContext, _: (), chunk: Rect) {
fn draw(&self, context: &mut DrawContext, _: (), area: Rect) {
context.frame.render_widget(
ratatui::widgets::Tabs::new(
T::iter().map(|e| e.to_string()).collect(),
)
.select(self.tabs.selected_index())
.highlight_style(Theme::get().tab_highlight_style),
chunk,
area,
)
}
}
Loading

0 comments on commit dcf16dd

Please sign in to comment.