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", "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]] [[package]]
name = "deranged" name = "deranged"
version = "0.3.11" version = "0.3.11"
@ -505,6 +540,29 @@ dependencies = [
"powerfmt", "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]] [[package]]
name = "digest" name = "digest"
version = "0.10.7" version = "0.10.7"
@ -516,6 +574,15 @@ dependencies = [
"subtle", "subtle",
] ]
[[package]]
name = "document-features"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb6969eaabd2421f8a2775cfd2471a2b634372b4a25d41e3bd647b79912850a0"
dependencies = [
"litrs",
]
[[package]] [[package]]
name = "either" name = "either"
version = "1.13.0" version = "1.13.0"
@ -548,6 +615,12 @@ dependencies = [
"miniz_oxide", "miniz_oxide",
] ]
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]] [[package]]
name = "foldhash" name = "foldhash"
version = "0.1.3" version = "0.1.3"
@ -743,6 +816,12 @@ dependencies = [
"cc", "cc",
] ]
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.6.0" version = "2.6.0"
@ -878,6 +957,12 @@ version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
[[package]]
name = "litrs"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5"
[[package]] [[package]]
name = "lock_api" name = "lock_api"
version = "0.4.12" version = "0.4.12"
@ -1401,6 +1486,7 @@ dependencies = [
"ratatui", "ratatui",
"slice-utils", "slice-utils",
"thiserror", "thiserror",
"tui-popup",
"tui-prompts", "tui-prompts",
"tui-textarea", "tui-textarea",
] ]
@ -1583,6 +1669,18 @@ dependencies = [
"winnow", "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]] [[package]]
name = "tui-prompts" name = "tui-prompts"
version = "0.5.0" version = "0.5.0"

View File

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