Skip to content

Commit

Permalink
Refactor modals to use a global queue
Browse files Browse the repository at this point in the history
- Ensures modal is always rendered on top
- Unifies more modal logic, reducing boilerplate for modal implementors
- Allows modal queue to be shared between modal types
  • Loading branch information
LucasPickering committed Oct 27, 2023
1 parent 7434abf commit 3f2f6ff
Show file tree
Hide file tree
Showing 10 changed files with 268 additions and 257 deletions.
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#![deny(clippy::all)]
#![feature(associated_type_defaults)]
#![feature(iterator_try_collect)]
#![feature(trait_upcasting)]
#![feature(try_blocks)]

mod cli;
Expand Down
7 changes: 3 additions & 4 deletions src/tui/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ impl InputEngine {
InputBinding::new(KeyCode::Left, Action::Left),
InputBinding::new(KeyCode::Right, Action::Right),
InputBinding::new(KeyCode::Enter, Action::Interact),
InputBinding::new(KeyCode::Esc, Action::Close),
InputBinding::new(KeyCode::Esc, Action::Cancel),
]
.into_iter()
.map(|binding| (binding.action, binding))
Expand Down Expand Up @@ -105,9 +105,8 @@ pub enum Action {
Right,
/// Do a thing. E.g. select an item in a list
Interact,
/// Close the current modal
#[display(fmt = "Close Dialog")]
Close,
/// Close the current modal/dialog/etc.
Cancel,
}

/// A mapping from a key input sequence to an action. This can optionally have
Expand Down
8 changes: 7 additions & 1 deletion src/tui/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ use crate::{
config::{ProfileId, RequestCollection, RequestRecipeId},
http::{RequestBuildError, RequestError, RequestId, RequestRecord},
template::{Prompt, Prompter},
util::ResultExt,
};
use anyhow::Context;
use derive_more::From;
use tokio::sync::mpsc::UnboundedSender;
use tracing::trace;
Expand All @@ -23,7 +25,11 @@ impl MessageSender {
pub fn send(&self, message: impl Into<Message>) {
let message: Message = message.into();
trace!(?message, "Queueing message");
self.0.send(message).expect("Message queue is closed")
let _ = self
.0
.send(message)
.context("Error enqueueing message")
.traced();
}
}

Expand Down
6 changes: 3 additions & 3 deletions src/tui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ impl Tui {
while let Ok(message) = self.messages_rx.try_recv() {
// If an error occurs, store it so we can show the user
if let Err(error) = self.handle_message(message) {
self.view.set_error(error);
self.view.open_modal(error);
}
}

Expand Down Expand Up @@ -211,10 +211,10 @@ impl Tui {
}

Message::PromptStart(prompt) => {
self.view.set_prompt(prompt);
self.view.open_modal(prompt);
}

Message::Error { error } => self.view.set_error(error),
Message::Error { error } => self.view.open_modal(error),
Message::Quit => self.should_run = false,
}
Ok(())
Expand Down
168 changes: 80 additions & 88 deletions src/tui/view/component/misc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@ use crate::{
tui::{
input::Action,
view::{
component::{Component, Draw, UpdateOutcome, ViewMessage},
component::{
modal::IntoModal, Component, Draw, Modal, UpdateOutcome,
ViewMessage,
},
state::Notification,
util::{layout, ButtonBrick, Modal, ModalContent, ToTui},
util::{layout, ButtonBrick, ToTui},
Frame, RenderContext,
},
},
};
use derive_more::From;
use itertools::Itertools;
use ratatui::{
prelude::{Alignment, Constraint, Direction, Rect},
Expand All @@ -22,46 +24,34 @@ use ratatui::{
use std::fmt::Debug;
use tui_textarea::TextArea;

/// A modal to show the user a catastrophic error
pub type ErrorModal = Modal<ErrorModalInner>;
#[derive(Debug)]
pub struct ErrorModal(anyhow::Error);

impl Modal for ErrorModal {
fn title(&self) -> &str {
"Error"
}

fn dimensions(&self) -> (Constraint, Constraint) {
(Constraint::Percentage(60), Constraint::Percentage(20))
}
}

impl Component for ErrorModal {
fn update(&mut self, message: ViewMessage) -> UpdateOutcome {
match message {
// Open the modal
ViewMessage::Error(error) => {
self.open(error.into());
UpdateOutcome::Consumed
}

// Close the modal
// Extra close action
ViewMessage::InputAction {
action: Some(Action::Interact | Action::Close),
action: Some(Action::Interact),
..
} if self.is_open() => {
self.close();
UpdateOutcome::Consumed
}
} => UpdateOutcome::Propagate(ViewMessage::CloseModal),

_ => UpdateOutcome::Propagate(message),
}
}
}

#[derive(Debug, From)]
pub struct ErrorModalInner(anyhow::Error);

impl ModalContent for ErrorModalInner {
fn title(&self) -> &str {
"Error"
}

fn dimensions(&self) -> (Constraint, Constraint) {
(Constraint::Percentage(60), Constraint::Percentage(20))
}
}

impl Draw for ErrorModalInner {
impl Draw for ErrorModal {
fn draw(
&self,
context: &RenderContext,
Expand Down Expand Up @@ -95,93 +85,87 @@ impl Draw for ErrorModalInner {
}
}

/// A modal to prompt the user for some input
pub type PromptModal = Modal<PromptModalInner>;

impl Component for PromptModal {
fn update(&mut self, message: ViewMessage) -> UpdateOutcome {
match message {
// Open the prompt
ViewMessage::Prompt(prompt) => {
// Listen for this outside the child, because it won't be in
// focus while closed
self.open(PromptModalInner::new(prompt));
UpdateOutcome::Consumed
}

// Close
ViewMessage::InputAction {
action: Some(Action::Close),
..
} if self.is_open() => {
// Dropping the prompt returner here will tell the caller
// that we're not returning anything
self.close();
UpdateOutcome::Consumed
}

// Submit
ViewMessage::InputAction {
action: Some(Action::Interact),
..
} if self.is_open() => {
// Return the user's value and close the prompt
let inner = self.close().expect("We checked is_open");
let input = inner.text_area.into_lines().join("\n");
inner.prompt.respond(input);
UpdateOutcome::Consumed
}

// All other input gets forwarded to the text editor
ViewMessage::InputAction { event, .. } if self.is_open() => {
let text_area = match self {
Modal::Closed => unreachable!("We checked is_open"),
Modal::Open {
state: PromptModalInner { text_area, .. },
..
} => text_area,
};
text_area.input(event);
UpdateOutcome::Consumed
}
impl IntoModal for anyhow::Error {
type Target = ErrorModal;

_ => UpdateOutcome::Propagate(message),
}
fn into_modal(self) -> Self::Target {
ErrorModal(self)
}
}

/// Inner state for the prompt modal
#[derive(Debug)]
pub struct PromptModalInner {
pub struct PromptModal {
// Prompt currently being shown
prompt: Prompt,
/// A queue of additional prompts to shown. If the queue is populated,
/// closing one prompt will open a the next one.
// queue: VecDeque<Prompt>,
text_area: TextArea<'static>,
/// Flag set before closing to indicate if we should submit in `on_close``
submit: bool,
}

impl PromptModalInner {
impl PromptModal {
pub fn new(prompt: Prompt) -> Self {
let mut text_area = TextArea::default();
if prompt.sensitive() {
text_area.set_mask_char('\u{2022}');
}
Self { prompt, text_area }
Self {
prompt,
text_area,
submit: false,
}
}
}

impl ModalContent for PromptModalInner {
impl Modal for PromptModal {
fn title(&self) -> &str {
self.prompt.label()
}

fn dimensions(&self) -> (Constraint, Constraint) {
(Constraint::Percentage(60), Constraint::Length(3))
}

fn on_close(self: Box<Self>) {
if self.submit {
// Return the user's value and close the prompt
let input = self.text_area.into_lines().join("\n");
self.prompt.respond(input);
}
}
}

impl Draw for PromptModalInner {
impl Component for PromptModal {
fn update(&mut self, message: ViewMessage) -> UpdateOutcome {
match message {
// Submit
ViewMessage::InputAction {
action: Some(Action::Interact),
..
} => {
// Submission is handled in on_close. The control flow here is
// ugly but it's hard with the top-down nature of modals
self.submit = true;
UpdateOutcome::Propagate(ViewMessage::CloseModal)
}

// All other input gets forwarded to the text editor (except cancel)
ViewMessage::InputAction { event, action }
if action != Some(Action::Cancel) =>
{
self.text_area.input(event);
UpdateOutcome::Consumed
}

_ => UpdateOutcome::Propagate(message),
}
}
}

impl Draw for PromptModal {
fn draw(
&self,
_context: &RenderContext,
Expand All @@ -193,6 +177,14 @@ impl Draw for PromptModalInner {
}
}

impl IntoModal for Prompt {
type Target = PromptModal;

fn into_modal(self) -> Self::Target {
PromptModal::new(self)
}
}

#[derive(Debug)]
pub struct HelpText;

Expand All @@ -209,7 +201,7 @@ impl Draw for HelpText {
Action::ReloadCollection,
Action::FocusNext,
Action::FocusPrevious,
Action::Close,
Action::Cancel,
];
let text = actions
.into_iter()
Expand Down
Loading

0 comments on commit 3f2f6ff

Please sign in to comment.