mirror of
https://github.com/zaphar/sheetsui.git
synced 2025-07-23 13:29: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",
|
"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"
|
||||||
|
@ -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"
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user