mirror of
https://github.com/zaphar/sheetsui.git
synced 2025-07-23 05:19:48 -04:00
feat: modal dialog
This commit is contained in:
parent
a8f436894c
commit
86f008a2a8
98
Cargo.lock
generated
98
Cargo.lock
generated
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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,11 +402,16 @@ 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(())
|
||||
}
|
||||
|
||||
fn exit_edit_mode(&mut self) -> Result<()> {
|
||||
self.text_area.set_cursor_line_style(Style::default());
|
||||
self.text_area.set_cursor_style(Style::default());
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user