diff --git a/src/tui/view/component/misc.rs b/src/tui/view/component/misc.rs index 5ce83b12..17c36ae7 100644 --- a/src/tui/view/component/misc.rs +++ b/src/tui/view/component/misc.rs @@ -7,7 +7,8 @@ use crate::{ input::Action, view::{ component::{ - modal::IntoModal, Component, Draw, Event, Modal, UpdateOutcome, + modal::IntoModal, Component, Draw, Event, Modal, UpdateContext, + UpdateOutcome, }, state::Notification, util::{layout, ButtonBrick, ToTui}, @@ -39,7 +40,11 @@ impl Modal for ErrorModal { } impl Component for ErrorModal { - fn update(&mut self, message: Event) -> UpdateOutcome { + fn update( + &mut self, + _context: &mut UpdateContext, + message: Event, + ) -> UpdateOutcome { match message { // Extra close action Event::Input { @@ -94,7 +99,8 @@ impl IntoModal for anyhow::Error { } } -/// Inner state for the prompt modal +/// Inner state forfn update(&mut self, context:&mut UpdateContext, message: +/// Event) -> UpdateOutcome the prompt modal #[derive(Debug, Display)] #[display(fmt = "PromptModal")] pub struct PromptModal { @@ -140,7 +146,11 @@ impl Modal for PromptModal { } impl Component for PromptModal { - fn update(&mut self, message: Event) -> UpdateOutcome { + fn update( + &mut self, + _context: &mut UpdateContext, + message: Event, + ) -> UpdateOutcome { match message { // Submit Event::Input { diff --git a/src/tui/view/component/mod.rs b/src/tui/view/component/mod.rs index 93e90579..15d62310 100644 --- a/src/tui/view/component/mod.rs +++ b/src/tui/view/component/mod.rs @@ -14,7 +14,7 @@ use crate::{ config::RequestRecipeId, tui::{ input::{Action, InputEngine}, - message::Message, + message::{Message, MessageSender}, view::{ component::root::RootMode, state::{Notification, RequestState}, @@ -41,8 +41,13 @@ use std::fmt::{Debug, Display}; /// component name. pub trait Component: Debug + Display { /// Update the state of *just* this component according to the message. - /// Returned outcome indicates what to do afterwards. - fn update(&mut self, message: Event) -> UpdateOutcome { + /// 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, + message: Event, + ) -> UpdateOutcome { // By default just forward to our parent UpdateOutcome::Propagate(message) } @@ -56,6 +61,23 @@ pub trait Component: Debug + Display { } } +/// Mutable context passed to each update call. Allows for triggering side +/// effects. +pub struct UpdateContext { + messages_tx: MessageSender, +} + +impl UpdateContext { + pub fn new(messages_tx: MessageSender) -> Self { + Self { messages_tx } + } + + /// Send a message to trigger an async action + pub fn send_message(&mut self, message: Message) { + self.messages_tx.send(message); + } +} + /// Something that can be drawn onto screen as one or more TUI widgets. /// /// Conceptually this is bascially part of `Component`, but having it separate @@ -155,8 +177,4 @@ pub enum UpdateOutcome { /// no immediate need for that though so I'm keeping it simpler for /// now. Propagate(Event), - /// The component consumed the message, and wants to trigger an app-wide - /// action in response to it. The action should be queued on the controller - /// so it can be handled asyncronously. - SideEffect(Message), } diff --git a/src/tui/view/component/modal.rs b/src/tui/view/component/modal.rs index c68c1123..551485f1 100644 --- a/src/tui/view/component/modal.rs +++ b/src/tui/view/component/modal.rs @@ -1,7 +1,7 @@ use crate::tui::{ input::Action, view::{ - component::{Component, Draw, Event, UpdateOutcome}, + component::{Component, Draw, Event, UpdateContext, UpdateOutcome}, util::centered_rect, }, }; @@ -91,7 +91,11 @@ impl ModalQueue { } impl Component for ModalQueue { - fn update(&mut self, message: Event) -> UpdateOutcome { + fn update( + &mut self, + _context: &mut UpdateContext, + message: Event, + ) -> UpdateOutcome { match message { // Close the active modal. If there's no modal open, we'll propagate // the event down diff --git a/src/tui/view/component/primary.rs b/src/tui/view/component/primary.rs index b32e5abd..7bf6cd47 100644 --- a/src/tui/view/component/primary.rs +++ b/src/tui/view/component/primary.rs @@ -9,7 +9,7 @@ use crate::{ component::{ request::{RequestPane, RequestPaneProps}, response::{ResponsePane, ResponsePaneProps}, - Component, Draw, Event, UpdateOutcome, + Component, Draw, Event, UpdateContext, UpdateOutcome, }, state::{FixedSelect, RequestState, StatefulList, StatefulSelect}, util::{layout, BlockBrick, ListBrick, ToTui}, @@ -94,21 +94,24 @@ impl PrimaryView { } impl Component for PrimaryView { - fn update(&mut self, message: Event) -> UpdateOutcome { + fn update( + &mut self, + context: &mut UpdateContext, + message: Event, + ) -> UpdateOutcome { match message { // Send HTTP request (bubbled up from child) Event::HttpSendRequest => { if let Some(recipe) = self.selected_recipe() { - UpdateOutcome::SideEffect(Message::HttpBeginRequest { + context.send_message(Message::HttpBeginRequest { // Reach into the children to grab state (ugly!) recipe_id: recipe.id.clone(), profile_id: self .selected_profile() .map(|profile| profile.id.clone()), - }) - } else { - UpdateOutcome::Consumed + }); } + UpdateOutcome::Consumed } // Input messages @@ -230,7 +233,11 @@ impl ProfileListPane { } impl Component for ProfileListPane { - fn update(&mut self, message: Event) -> UpdateOutcome { + fn update( + &mut self, + _context: &mut UpdateContext, + message: Event, + ) -> UpdateOutcome { match message { Event::Input { action: Some(Action::Up), @@ -289,19 +296,19 @@ impl RecipeListPane { } impl Component for RecipeListPane { - fn update(&mut self, message: Event) -> UpdateOutcome { - /// Helper to load a request from the repo whenever we select a new - /// recipe - fn load_from_repo(pane: &RecipeListPane) -> UpdateOutcome { - match pane.recipes.selected() { - Some(recipe) => { - UpdateOutcome::SideEffect(Message::RepositoryStartLoad { - recipe_id: recipe.id.clone(), - }) - } - None => UpdateOutcome::Consumed, + fn update( + &mut self, + context: &mut UpdateContext, + message: Event, + ) -> UpdateOutcome { + let mut load_from_repo = |pane: &RecipeListPane| -> UpdateOutcome { + if let Some(recipe) = pane.recipes.selected() { + context.send_message(Message::RepositoryStartLoad { + recipe_id: recipe.id.clone(), + }); } - } + UpdateOutcome::Consumed + }; match message { Event::Input { diff --git a/src/tui/view/component/request.rs b/src/tui/view/component/request.rs index 1d4abf58..ebd03909 100644 --- a/src/tui/view/component/request.rs +++ b/src/tui/view/component/request.rs @@ -4,7 +4,8 @@ use crate::{ input::Action, view::{ component::{ - primary::PrimaryPane, Component, Draw, Event, UpdateOutcome, + primary::PrimaryPane, Component, Draw, Event, UpdateContext, + UpdateOutcome, }, state::{FixedSelect, StatefulSelect}, util::{layout, BlockBrick, TabBrick, ToTui}, @@ -41,7 +42,11 @@ enum RequestTab { impl FixedSelect for RequestTab {} impl Component for RequestPane { - fn update(&mut self, message: Event) -> UpdateOutcome { + fn update( + &mut self, + _context: &mut UpdateContext, + message: Event, + ) -> UpdateOutcome { match message { Event::Input { action: Some(action), diff --git a/src/tui/view/component/response.rs b/src/tui/view/component/response.rs index c1568212..2d9d4a62 100644 --- a/src/tui/view/component/response.rs +++ b/src/tui/view/component/response.rs @@ -3,7 +3,7 @@ use crate::tui::{ view::{ component::{ primary::PrimaryPane, root::RootMode, Component, Draw, Event, - UpdateOutcome, + UpdateContext, UpdateOutcome, }, state::{FixedSelect, RequestState, StatefulSelect}, util::{layout, BlockBrick, TabBrick, ToTui}, @@ -40,7 +40,11 @@ enum ResponseTab { impl FixedSelect for ResponseTab {} impl Component for ResponsePane { - fn update(&mut self, message: Event) -> UpdateOutcome { + fn update( + &mut self, + _context: &mut UpdateContext, + message: Event, + ) -> UpdateOutcome { match message { Event::Input { action: Some(action), diff --git a/src/tui/view/component/root.rs b/src/tui/view/component/root.rs index 79d6e913..694f2dfa 100644 --- a/src/tui/view/component/root.rs +++ b/src/tui/view/component/root.rs @@ -9,7 +9,7 @@ use crate::{ modal::ModalQueue, primary::{PrimaryView, PrimaryViewProps}, response::ResponsePaneProps, - Component, Draw, Event, UpdateOutcome, + Component, Draw, Event, UpdateContext, UpdateOutcome, }, state::RequestState, util::layout, @@ -103,51 +103,49 @@ impl Root { } impl Component for Root { - fn update(&mut self, message: Event) -> UpdateOutcome { + fn update( + &mut self, + context: &mut UpdateContext, + message: Event, + ) -> UpdateOutcome { match message { Event::Init => { // Load the initial state for the selected recipe if let Some(recipe) = self.primary_view.selected_recipe() { - UpdateOutcome::SideEffect(Message::RepositoryStartLoad { + context.send_message(Message::RepositoryStartLoad { recipe_id: recipe.id.clone(), - }) - } else { - UpdateOutcome::Consumed + }); } } // Update state of HTTP request Event::HttpSetState { recipe_id, state } => { - self.update_request(recipe_id, state); - UpdateOutcome::Consumed + self.update_request(recipe_id, state) } // Other state messages - Event::OpenView(mode) => { - self.mode = mode; - UpdateOutcome::Consumed - } + Event::OpenView(mode) => self.mode = mode, Event::Notify(notification) => { self.notification_text = - Some(NotificationText::new(notification)); - UpdateOutcome::Consumed + Some(NotificationText::new(notification)) } // Input messages Event::Input { action: Some(Action::Quit), .. - } => UpdateOutcome::SideEffect(Message::Quit), + } => context.send_message(Message::Quit), Event::Input { action: Some(Action::ReloadCollection), .. - } => UpdateOutcome::SideEffect(Message::CollectionStartReload), + } => context.send_message(Message::CollectionStartReload), // Any other user input should get thrown away - Event::Input { .. } => UpdateOutcome::Consumed, + Event::Input { .. } => {} // There shouldn't be anything left unhandled. Bubble up to log it - _ => UpdateOutcome::Propagate(message), + _ => return UpdateOutcome::Propagate(message), } + UpdateOutcome::Consumed } fn children(&mut self) -> Vec<&mut dyn Component> { diff --git a/src/tui/view/mod.rs b/src/tui/view/mod.rs index 90b24c47..f6e5920b 100644 --- a/src/tui/view/mod.rs +++ b/src/tui/view/mod.rs @@ -14,7 +14,7 @@ use crate::{ view::{ component::{ Component, Draw, Event, IntoModal, RenderContext, Root, - UpdateOutcome, + UpdateContext, UpdateOutcome, }, state::Notification, theme::Theme, @@ -114,7 +114,8 @@ impl View { fn handle_message(&mut self, message: Event) { let span = trace_span!("View message", ?message); span.in_scope(|| { - match Self::update_all(&mut self.root, message) { + let mut context = self.update_context(); + match Self::update_all(&mut self.root, &mut context, message) { UpdateOutcome::Consumed => { trace!("View message consumed") } @@ -122,25 +123,26 @@ impl View { UpdateOutcome::Propagate(_) => { error!("View message was unhandled"); } - // Consumer wants to trigger a new event - UpdateOutcome::SideEffect(m) => { - trace!(message = ?m, "View message produced side-effect"); - self.messages_tx.send(m); - } } }); } + /// Context object passed to each update call + fn update_context(&self) -> UpdateContext { + UpdateContext::new(self.messages_tx.clone()) + } + /// Update the state of a component *and* its children, starting at the /// lowest descendant. Recursively walk up the tree until a component /// consumes the message. fn update_all( component: &mut dyn Component, + context: &mut UpdateContext, mut message: Event, ) -> UpdateOutcome { // If we have a child, send them the message. If not, eat it ourselves for child in component.children() { - let outcome = Self::update_all(child, message); // RECURSION + let outcome = Self::update_all(child, context, message); // RECURSION if let UpdateOutcome::Propagate(returned) = outcome { // Keep going to the next child. It's possible the child // returned something other than the original message, which @@ -156,7 +158,7 @@ impl View { // Message is already traced in the parent span, so don't dupe it. let span = trace_span!("Component handling message", %component); span.in_scope(|| { - let outcome = component.update(message); + let outcome = component.update(context, message); trace!(?outcome); outcome })