feat: modal dialog

This commit is contained in:
Jeremy Wall 2024-11-22 14:57:08 -05:00
parent a8f436894c
commit 86f008a2a8
3 changed files with 176 additions and 16 deletions

98
Cargo.lock generated
View File

@ -496,6 +496,41 @@ dependencies = [
"thiserror",
]
[[package]]
name = "darling"
version = "0.20.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989"
dependencies = [
"darling_core",
"darling_macro",
]
[[package]]
name = "darling_core"
version = "0.20.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn",
]
[[package]]
name = "darling_macro"
version = "0.20.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
dependencies = [
"darling_core",
"quote",
"syn",
]
[[package]]
name = "deranged"
version = "0.3.11"
@ -505,6 +540,29 @@ dependencies = [
"powerfmt",
]
[[package]]
name = "derive-getters"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74ef43543e701c01ad77d3a5922755c6a1d71b22d942cb8042be4994b380caff"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "derive_setters"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e8ef033054e131169b8f0f9a7af8f5533a9436fadf3c500ed547f730f07090d"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "digest"
version = "0.10.7"
@ -516,6 +574,15 @@ dependencies = [
"subtle",
]
[[package]]
name = "document-features"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb6969eaabd2421f8a2775cfd2471a2b634372b4a25d41e3bd647b79912850a0"
dependencies = [
"litrs",
]
[[package]]
name = "either"
version = "1.13.0"
@ -548,6 +615,12 @@ dependencies = [
"miniz_oxide",
]
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foldhash"
version = "0.1.3"
@ -743,6 +816,12 @@ dependencies = [
"cc",
]
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "indexmap"
version = "2.6.0"
@ -878,6 +957,12 @@ version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
[[package]]
name = "litrs"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5"
[[package]]
name = "lock_api"
version = "0.4.12"
@ -1401,6 +1486,7 @@ dependencies = [
"ratatui",
"slice-utils",
"thiserror",
"tui-popup",
"tui-prompts",
"tui-textarea",
]
@ -1583,6 +1669,18 @@ dependencies = [
"winnow",
]
[[package]]
name = "tui-popup"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9ee3d08800c83ba0a2efaec44d225bcc3f885f30e2b520a17e2cd962b7da6ab"
dependencies = [
"derive-getters",
"derive_setters",
"document-features",
"ratatui",
]
[[package]]
name = "tui-prompts"
version = "0.5.0"

View File

@ -17,3 +17,4 @@ thiserror = "1.0.65"
tui-textarea = "0.7.0"
tui-prompts = "0.5.0"
slice-utils = { git = "https://dev.zaphar.net/zaphar/slice-cursor-rs.git", ref = "main" }
tui-popup = "0.6.0"

View File

@ -5,7 +5,7 @@ use std::{path::PathBuf, process::ExitCode};
use crate::book::Book;
use anyhow::Result;
use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers};
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
use ratatui::{
self,
buffer::Buffer,
@ -15,6 +15,7 @@ use ratatui::{
widgets::{Block, Cell, Paragraph, Row, Table, TableState, Widget},
Frame,
};
use tui_popup::Popup;
use tui_prompts::{State, Status, TextPrompt, TextState};
use tui_textarea::{CursorMove, TextArea};
@ -24,22 +25,44 @@ mod test;
use cmd::Cmd;
#[derive(Default, Debug, PartialEq)]
#[derive(Default, Debug, PartialEq, Clone)]
pub enum Modality {
#[default]
Navigate,
CellEdit,
Command,
// TODO(zaphar): Command Mode?
Dialog,
}
#[derive(Default, Debug)]
#[derive(Debug)]
pub struct AppState<'ws> {
pub modality: Modality,
pub modality_stack: Vec<Modality>,
pub table_state: TableState,
pub command_state: TextState<'ws>,
}
impl<'ws> Default for AppState<'ws> {
fn default() -> Self {
AppState {
modality_stack: vec![Modality::default()],
table_state: Default::default(),
command_state: Default::default()
}
}
}
impl<'ws> AppState<'ws> {
pub fn modality(&'ws self) -> &'ws Modality {
self.modality_stack.last().unwrap()
}
pub fn pop_modality(&mut self) {
if self.modality_stack.len() > 1 {
self.modality_stack.pop();
}
}
}
// TODO(jwall): This should probably move to a different module.
/// The Address in a Table.
#[derive(Debug, PartialEq, PartialOrd, Ord, Eq, Clone)]
@ -70,6 +93,7 @@ pub struct Workspace<'ws> {
text_area: TextArea<'ws>,
dirty: bool,
show_help: bool,
popup: String
}
impl<'ws> Workspace<'ws> {
@ -81,6 +105,7 @@ impl<'ws> Workspace<'ws> {
text_area: reset_text_area("".to_owned()),
dirty: false,
show_help: false,
popup: String::new(),
};
ws.handle_movement_change();
ws
@ -140,10 +165,11 @@ impl<'ws> Workspace<'ws> {
pub fn handle_input(&mut self) -> Result<Option<ExitCode>> {
if let Event::Key(key) = event::read()? {
let result = match self.state.modality {
let result = match self.state.modality() {
Modality::Navigate => self.handle_navigation_input(key)?,
Modality::CellEdit => self.handle_edit_input(key)?,
Modality::Command => self.handle_command_input(key)?,
Modality::Dialog => self.handle_dialog_input(key)?,
};
return Ok(result);
}
@ -152,7 +178,7 @@ impl<'ws> Workspace<'ws> {
fn render_help_text(&self) -> impl Widget {
let info_block = Block::bordered().title("Help");
Paragraph::new(match self.state.modality {
Paragraph::new(match self.state.modality() {
Modality::Navigate => Text::from(vec![
"Navigate Mode:".into(),
"* e: Enter edit mode for current cell".into(),
@ -171,6 +197,10 @@ impl<'ws> Workspace<'ws> {
"Command Mode:".into(),
"* ESC: Exit command mode".into(),
]),
Modality::Dialog => Text::from(vec![
"Dialog Mode:".into(),
"* ESC: Exit dialog".into(),
]),
})
.block(info_block)
}
@ -188,6 +218,18 @@ impl<'ws> Workspace<'ws> {
Ok(None)
}
fn handle_dialog_input(&mut self, key: event::KeyEvent) -> Result<Option<ExitCode>> {
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Esc | KeyCode::Enter | KeyCode::Char('q') => self.exit_dialog_mode()?,
_ => {
// NOOP
}
}
}
Ok(None)
}
fn handle_edit_input(&mut self, key: event::KeyEvent) -> Result<Option<ExitCode>> {
if key.kind == KeyEventKind::Press {
match key.code {
@ -220,7 +262,7 @@ impl<'ws> Workspace<'ws> {
Ok(true)
}
Ok(Some(Cmd::Help(_maybe_topic))) => {
// TODO(jeremy): Modal dialogs?
self.enter_dialog_mode("TODO help topic".to_owned());
Ok(true)
}
Ok(Some(Cmd::Write(maybe_path))) => {
@ -245,9 +287,12 @@ impl<'ws> Workspace<'ws> {
// TODO(zaphar): We probably need to do better than this
std::process::exit(0);
},
Ok(None) => Ok(false),
Err(_msg) => {
// TODO(jeremy): Modal dialogs?
Ok(None) => {
self.enter_dialog_mode(format!("Unrecognized commmand {}", cmd_text));
Ok(false)
},
Err(msg) => {
self.enter_dialog_mode(msg.to_owned());
Ok(false)
}
}
@ -328,18 +373,23 @@ impl<'ws> Workspace<'ws> {
}
fn enter_navigation_mode(&mut self) {
self.state.modality = Modality::Navigate;
self.state.modality_stack.push(Modality::Navigate);
}
fn enter_command_mode(&mut self) {
self.state.modality = Modality::Command;
self.state.modality_stack.push(Modality::Command);
self.state.command_state.truncate();
*self.state.command_state.status_mut() = Status::Pending;
self.state.command_state.focus();
}
fn enter_dialog_mode(&mut self, msg: String) {
self.popup = msg;
self.state.modality_stack.push(Modality::Dialog);
}
fn enter_edit_mode(&mut self) {
self.state.modality = Modality::CellEdit;
self.state.modality_stack.push(Modality::CellEdit);
self.text_area
.set_cursor_line_style(Style::default().add_modifier(Modifier::UNDERLINED));
self.text_area
@ -352,8 +402,13 @@ impl<'ws> Workspace<'ws> {
let cmd = self.state.command_state.value().to_owned();
self.state.command_state.blur();
*self.state.command_state.status_mut() = Status::Done;
self.state.pop_modality();
self.handle_command(cmd)?;
self.enter_navigation_mode();
Ok(())
}
fn exit_dialog_mode(&mut self) -> Result<()> {
self.state.pop_modality();
Ok(())
}
@ -418,7 +473,7 @@ impl<'ws> Workspace<'ws> {
info_para.render(rect, buf);
}));
}
if self.state.modality == Modality::Command {
if self.state.modality() == &Modality::Command {
cs.push(Constraint::Max(1));
rs.push(Box::new(|rect: Rect, buf: &mut Buffer, ws: &mut Self| {
StatefulWidget::render(
@ -474,10 +529,11 @@ impl<'widget, 'ws: 'widget> Widget for &'widget mut Workspace<'ws> {
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|| String::from("Unknown")),
))
.title_bottom(match &self.state.modality {
.title_bottom(match self.state.modality() {
Modality::Navigate => "navigate",
Modality::CellEdit => "edit",
Modality::Command => "command",
Modality::Dialog => "",
})
.title_bottom(
Line::from(format!(
@ -492,6 +548,11 @@ impl<'widget, 'ws: 'widget> Widget for &'widget mut Workspace<'ws> {
}
outer_block.render(area, buf);
if self.state.modality() == &Modality::Dialog {
let popup = Popup::new(Text::from(self.popup.clone()));
popup.render(area, buf);
}
}
}