Skip to content

Commit

Permalink
Refactor component update to use imperative side effects
Browse files Browse the repository at this point in the history
  • Loading branch information
LucasPickering committed Nov 1, 2023
1 parent 8c3d859 commit 157ab0d
Show file tree
Hide file tree
Showing 8 changed files with 110 additions and 63 deletions.
18 changes: 14 additions & 4 deletions src/tui/view/component/misc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
31 changes: 24 additions & 7 deletions src/tui/view/component/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use crate::{
config::RequestRecipeId,
tui::{
input::{Action, InputEngine},
message::Message,
message::{Message, MessageSender},
view::{
component::root::RootMode,
state::{Notification, RequestState},
Expand All @@ -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)
}
Expand All @@ -56,6 +61,22 @@ pub trait Component: Debug + Display {
}
}

/// TODO
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
Expand Down Expand Up @@ -155,8 +176,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),
}
8 changes: 6 additions & 2 deletions src/tui/view/component/modal.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::tui::{
input::Action,
view::{
component::{Component, Draw, Event, UpdateOutcome},
component::{Component, Draw, Event, UpdateContext, UpdateOutcome},
util::centered_rect,
},
};
Expand Down Expand Up @@ -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
Expand Down
45 changes: 26 additions & 19 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, UpdateOutcome,
Component, Draw, Event, UpdateContext, UpdateOutcome,
},
state::{FixedSelect, RequestState, StatefulList, StatefulSelect},
util::{layout, BlockBrick, ListBrick, ToTui},
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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),
Expand Down Expand 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 {
Expand Down
9 changes: 7 additions & 2 deletions src/tui/view/component/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -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),
Expand Down
8 changes: 6 additions & 2 deletions src/tui/view/component/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -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),
Expand Down
34 changes: 16 additions & 18 deletions src/tui/view/component/root.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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> {
Expand Down
20 changes: 11 additions & 9 deletions src/tui/view/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use crate::{
view::{
component::{
Component, Draw, Event, IntoModal, RenderContext, Root,
UpdateOutcome,
UpdateContext, UpdateOutcome,
},
state::Notification,
theme::Theme,
Expand Down Expand Up @@ -114,33 +114,35 @@ 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")
}
// Consumer didn't eat the message - huh?
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
Expand All @@ -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
})
Expand Down

0 comments on commit 157ab0d

Please sign in to comment.