Skip to content

Commit

Permalink
Use text area for text display
Browse files Browse the repository at this point in the history
This required some `RefCell` shenanigans, but I like the pattern.

Also, refactored update to add queueing for subsequent events, instead of just propagating up. This feels less icky and allows components to propagate events to their children.
  • Loading branch information
LucasPickering committed Nov 4, 2023
1 parent cc1e321 commit 19c10a1
Show file tree
Hide file tree
Showing 11 changed files with 279 additions and 299 deletions.
30 changes: 13 additions & 17 deletions src/tui/view/component/misc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use crate::{
view::{
component::{
modal::IntoModal, primary::PrimaryPane, root::FullscreenMode,
Component, Draw, Event, Modal, UpdateContext, UpdateOutcome,
Component, Draw, Event, Modal, Update, UpdateContext,
},
state::Notification,
util::{layout, ButtonBrick, ToTui},
Expand Down Expand Up @@ -40,19 +40,18 @@ impl Modal for ErrorModal {
}

impl Component for ErrorModal {
fn update(
&mut self,
_context: &mut UpdateContext,
event: Event,
) -> UpdateOutcome {
fn update(&mut self, context: &mut UpdateContext, event: Event) -> Update {
match event {
// Extra close action
Event::Input {
action: Some(Action::Submit),
..
} => UpdateOutcome::Propagate(Event::CloseModal),
} => {
context.queue_event(Event::CloseModal);
Update::Consumed
}

_ => UpdateOutcome::Propagate(event),
_ => Update::Propagate(event),
}
}
}
Expand Down Expand Up @@ -140,11 +139,7 @@ impl Modal for PromptModal {
}

impl Component for PromptModal {
fn update(
&mut self,
_context: &mut UpdateContext,
event: Event,
) -> UpdateOutcome {
fn update(&mut self, context: &mut UpdateContext, event: Event) -> Update {
match event {
// Submit
Event::Input {
Expand All @@ -154,22 +149,23 @@ impl Component for PromptModal {
// 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(Event::CloseModal)
context.queue_event(Event::CloseModal);
Update::Consumed
}

// Make sure cancel gets propagated to close the modal
event @ Event::Input {
action: Some(Action::Cancel),
..
} => UpdateOutcome::Propagate(event),
} => Update::Propagate(event),

// All other input gets forwarded to the text editor
Event::Input { event, .. } => {
self.text_area.input(event);
UpdateOutcome::Consumed
Update::Consumed
}

_ => UpdateOutcome::Propagate(event),
_ => Update::Propagate(event),
}
}
}
Expand Down
64 changes: 30 additions & 34 deletions src/tui/view/component/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ use crate::{
},
};
use ratatui::prelude::Rect;
use std::fmt::{Debug, Display};
use std::{
collections::VecDeque,
fmt::{Debug, Display},
};
use tracing::trace;

/// The main building block that makes up the view. This is modeled after React,
/// with some key differences:
Expand All @@ -45,13 +49,8 @@ pub trait Component: Debug + Display {
/// Update the state of *just* this component according to the message.
/// Returned outcome indicates what to do afterwards. Context allows updates
/// to trigger side-effects, e.g. launching an HTTP request.
fn update(
&mut self,
_context: &mut UpdateContext,
event: Event,
) -> UpdateOutcome {
// By default just forward to our parent
UpdateOutcome::Propagate(event)
fn update(&mut self, _context: &mut UpdateContext, event: Event) -> Update {
Update::Propagate(event)
}

/// Which, if any, of this component's children currently has focus? The
Expand All @@ -65,19 +64,32 @@ pub trait Component: Debug + Display {

/// Mutable context passed to each update call. Allows for triggering side
/// effects.
pub struct UpdateContext {
pub struct UpdateContext<'a> {
messages_tx: MessageSender,
event_queue: &'a mut VecDeque<Event>,
}

impl UpdateContext {
pub fn new(messages_tx: MessageSender) -> Self {
Self { messages_tx }
impl<'a> UpdateContext<'a> {
pub fn new(
messages_tx: MessageSender,
event_queue: &'a mut VecDeque<Event>,
) -> Self {
Self {
messages_tx,
event_queue,
}
}

/// Send a message to trigger an async action
pub fn send_message(&mut self, message: Message) {
self.messages_tx.send(message);
}

/// Queue a subsequent view event to be handled after the current one
pub fn queue_event(&mut self, event: Event) {
trace!(?event, "Queueing subsequent event");
self.event_queue.push_back(event);
}
}

/// Something that can be drawn onto screen as one or more TUI widgets.
Expand All @@ -104,7 +116,6 @@ pub trait Draw<Props = ()> {
pub struct DrawContext<'a, 'f> {
pub input_engine: &'a InputEngine,
pub theme: &'a Theme,
// TODO refcell?
pub frame: &'a mut Frame<'f>,
}

Expand Down Expand Up @@ -151,36 +162,21 @@ pub enum Event {
/// to implement custom close triggers.
CloseModal,

/// Propagated from downstream when the user changes changes in a tab
/// selection. Allows parents to react to the tab change. This does not
/// include the new tab value because that would require generices. You can
/// grab the value from the child though.
TabChanged,

/// Tell the user something informational
Notify(Notification),
}

/// The result of a component state update operation. This corresponds to a
/// single input [ViewMessage].
#[derive(Debug)]
pub enum UpdateOutcome {
pub enum Update {
/// The consuming component updated its state accordingly, and no further
/// changes are necessary
Consumed,
/// The returned message should be passed to the parent component. This can
/// mean one of two things:
///
/// - The updated component did not handle the message, and it should
/// bubble up the tree
/// - The updated component *did* make changes according to the message,
/// and is sending a related message up the tree for ripple-effect
/// changes
///
/// This dual meaning is maybe a little janky. There's an argument that
/// rippled changes should be a separate variant that would cause the
/// caller to reset back to the bottom of the component tree. There's
/// no immediate need for that though so I'm keeping it simpler for
/// now.
/// The message was not consumed by this component, and should be passed to
/// the parent component. While technically possible, this should *not* be
/// used to trigger additional events. Instead, use
/// [UpdateContext::queue_event] for that. That will ensure the entire tree
/// has a chance to respond to the entire event.
Propagate(Event),
}
16 changes: 6 additions & 10 deletions src/tui/view/component/modal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use crate::tui::{
input::Action,
view::{
component::{
Component, Draw, DrawContext, Event, UpdateContext, UpdateOutcome,
Component, Draw, DrawContext, Event, Update, UpdateContext,
},
util::centered_rect,
},
Expand Down Expand Up @@ -97,11 +97,7 @@ impl ModalQueue {
}

impl Component for ModalQueue {
fn update(
&mut self,
_context: &mut UpdateContext,
event: Event,
) -> UpdateOutcome {
fn update(&mut self, _context: &mut UpdateContext, event: Event) -> Update {
match event {
// Close the active modal. If there's no modal open, we'll propagate
// the event down
Expand All @@ -114,20 +110,20 @@ impl Component for ModalQueue {
Some(modal) => {
// Inform the modal of its terminal status
modal.on_close();
UpdateOutcome::Consumed
Update::Consumed
}
// Modal wasn't open, so don't consume the event
None => UpdateOutcome::Propagate(event),
None => Update::Propagate(event),
}
}

// Open a new modal
Event::OpenModal { modal, priority } => {
self.open(modal, priority);
UpdateOutcome::Consumed
Update::Consumed
}

_ => UpdateOutcome::Propagate(event),
_ => Update::Propagate(event),
}
}

Expand Down
43 changes: 16 additions & 27 deletions src/tui/view/component/primary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use crate::{
component::{
request::{RequestPane, RequestPaneProps},
response::{ResponsePane, ResponsePaneProps},
Component, Draw, Event, UpdateContext, UpdateOutcome,
Component, Draw, Event, Update, UpdateContext,
},
state::{FixedSelect, RequestState, StatefulList, StatefulSelect},
util::{layout, BlockBrick, ListBrick, ToTui},
Expand Down Expand Up @@ -110,11 +110,7 @@ impl PrimaryView {
}

impl Component for PrimaryView {
fn update(
&mut self,
context: &mut UpdateContext,
event: Event,
) -> UpdateOutcome {
fn update(&mut self, context: &mut UpdateContext, event: Event) -> Update {
match event {
// Send HTTP request (bubbled up from child)
Event::HttpSendRequest => {
Expand All @@ -127,7 +123,7 @@ impl Component for PrimaryView {
.map(|profile| profile.id.clone()),
});
}
UpdateOutcome::Consumed
Update::Consumed
}

// Input messages
Expand All @@ -136,17 +132,17 @@ impl Component for PrimaryView {
..
} => {
self.selected_pane.previous();
UpdateOutcome::Consumed
Update::Consumed
}
Event::Input {
action: Some(Action::NextPane),
..
} => {
self.selected_pane.next();
UpdateOutcome::Consumed
Update::Consumed
}

_ => UpdateOutcome::Propagate(event),
_ => Update::Propagate(event),
}
}

Expand Down Expand Up @@ -244,27 +240,23 @@ impl ProfileListPane {
}

impl Component for ProfileListPane {
fn update(
&mut self,
_context: &mut UpdateContext,
event: Event,
) -> UpdateOutcome {
fn update(&mut self, _context: &mut UpdateContext, event: Event) -> Update {
match event {
Event::Input {
action: Some(Action::Up),
..
} => {
self.profiles.previous();
UpdateOutcome::Consumed
Update::Consumed
}
Event::Input {
action: Some(Action::Down),
..
} => {
self.profiles.next();
UpdateOutcome::Consumed
Update::Consumed
}
_ => UpdateOutcome::Propagate(event),
_ => Update::Propagate(event),
}
}
}
Expand Down Expand Up @@ -306,18 +298,14 @@ impl RecipeListPane {
}

impl Component for RecipeListPane {
fn update(
&mut self,
context: &mut UpdateContext,
event: Event,
) -> UpdateOutcome {
let mut load_from_repo = |pane: &RecipeListPane| -> UpdateOutcome {
fn update(&mut self, context: &mut UpdateContext, event: Event) -> Update {
let mut load_from_repo = |pane: &RecipeListPane| -> Update {
if let Some(recipe) = pane.recipes.selected() {
context.send_message(Message::RepositoryStartLoad {
recipe_id: recipe.id.clone(),
});
}
UpdateOutcome::Consumed
Update::Consumed
};

match event {
Expand All @@ -327,7 +315,8 @@ impl Component for RecipeListPane {
} => {
// Parent has to be responsible for sending the request because
// it also needs access to the profile list state
UpdateOutcome::Propagate(Event::HttpSendRequest)
context.queue_event(Event::HttpSendRequest);
Update::Consumed
}
Event::Input {
action: Some(Action::Up),
Expand All @@ -343,7 +332,7 @@ impl Component for RecipeListPane {
self.recipes.next();
load_from_repo(self)
}
_ => UpdateOutcome::Propagate(event),
_ => Update::Propagate(event),
}
}
}
Expand Down
Loading

0 comments on commit 19c10a1

Please sign in to comment.