diff --git a/crates/tui/src/view/component/internal.rs b/crates/tui/src/view/component/internal.rs index 9388b56a..41c2f9cf 100644 --- a/crates/tui/src/view/component/internal.rs +++ b/crates/tui/src/view/component/internal.rs @@ -215,17 +215,45 @@ impl Component { ) where T: Draw, { + self.draw_inner(frame, props, area, has_focus, &self.inner); + } + + fn draw_inner, Props>( + &self, + frame: &mut Frame, + props: Props, + area: Rect, + has_focus: bool, + inner: &D, + ) { let guard = DrawGuard::new(self.id); // Update internal state for event handling let metadata = DrawMetadata::new_dangerous(area, has_focus); self.metadata.set(metadata); - self.inner.draw(frame, props, metadata); + inner.draw(frame, props, metadata); drop(guard); // Make sure guard stays alive until here } } +impl Component> { + /// For components with optional data, draw the contents if present + pub fn draw_opt( + &self, + frame: &mut Frame, + props: Props, + area: Rect, + has_focus: bool, + ) where + T: Draw, + { + if let Some(inner) = &self.inner { + self.draw_inner(frame, props, area, has_focus, inner); + } + } +} + // Derive impl doesn't work because the constructor gets the correct name impl Default for Component { fn default() -> Self { diff --git a/crates/tui/src/view/component/recipe_pane.rs b/crates/tui/src/view/component/recipe_pane.rs index a8578d06..9921634f 100644 --- a/crates/tui/src/view/component/recipe_pane.rs +++ b/crates/tui/src/view/component/recipe_pane.rs @@ -38,7 +38,7 @@ use strum::{EnumCount, EnumIter}; pub struct RecipePane { /// All UI state derived from the recipe is stored together, and reset when /// the recipe or profile changes - recipe_state: StateCell>>, + recipe_state: StateCell>>, } #[derive(Clone)] @@ -56,7 +56,7 @@ impl RecipePane { let recipe_id = state_key.recipe_id.clone()?; let profile_id = state_key.selected_profile_id.clone(); let recipe_state = self.recipe_state.get()?; - let options = recipe_state.as_ref()?.data().build_options(); + let options = recipe_state.data().as_ref()?.build_options(); Some(RequestConfig { recipe_id, profile_id, @@ -80,8 +80,8 @@ impl EventHandler for RecipePane { RecipeMenuAction::disabled_actions( state.is_some(), state - .and_then(Option::as_mut) - .is_some_and(|state| state.data().has_body()), + .and_then(|state| state.data().as_ref()) + .is_some_and(|state| state.has_body()), ), )) } @@ -96,7 +96,7 @@ impl EventHandler for RecipePane { fn children(&mut self) -> Vec>> { self.recipe_state .get_mut() - .and_then(|state| Some(state.as_mut()?.to_child_mut())) + .map(|state| state.to_child_mut()) .into_iter() .collect() } @@ -138,11 +138,14 @@ impl<'a> Draw> for RecipePane { .map(RecipeNode::id) .cloned(), }, - || match props.selected_recipe_node { - Some(RecipeNode::Recipe(recipe)) => { - Some(RecipeDisplay::new(recipe).into()) + || { + match props.selected_recipe_node { + Some(RecipeNode::Recipe(recipe)) => { + Some(RecipeDisplay::new(recipe)) + } + Some(RecipeNode::Folder(_)) | None => None, } - Some(RecipeNode::Folder(_)) | None => None, + .into() }, ); @@ -158,11 +161,7 @@ impl<'a> Draw> for RecipePane { frame.render_widget(folder.generate(), inner_area); } Some(RecipeNode::Recipe(_)) => { - // Unwrap is safe because we just initialized state above - recipe_state - .as_ref() - .unwrap() - .draw(frame, (), inner_area, true) + recipe_state.draw_opt(frame, (), inner_area, true) } }; } diff --git a/crates/tui/src/view/component/recipe_pane/recipe.rs b/crates/tui/src/view/component/recipe_pane/recipe.rs index b27658c0..ee045685 100644 --- a/crates/tui/src/view/component/recipe_pane/recipe.rs +++ b/crates/tui/src/view/component/recipe_pane/recipe.rs @@ -41,8 +41,8 @@ pub struct RecipeDisplay { method: Method, query: Component>, headers: Component>, - body: Option>, - authentication: Option>, + body: Component>, + authentication: Component>, } impl RecipeDisplay { @@ -85,19 +85,22 @@ impl RecipeDisplay { ), ) .into(), - body: recipe.body.as_ref().map(|body| { - RecipeBodyDisplay::new(body, recipe.id.clone()).into() - }), + body: recipe + .body + .as_ref() + .map(|body| RecipeBodyDisplay::new(body, recipe.id.clone())) + .into(), // Map authentication type - authentication: recipe.authentication.as_ref().map( - |authentication| { + authentication: recipe + .authentication + .as_ref() + .map(|authentication| { AuthenticationDisplay::new( recipe.id.clone(), authentication.clone(), ) - .into() - }, - ), + }) + .into(), } } @@ -105,12 +108,14 @@ impl RecipeDisplay { pub fn build_options(&self) -> BuildOptions { let authentication = self .authentication + .data() .as_ref() - .and_then(|authentication| authentication.data().override_value()); + .and_then(|authentication| authentication.override_value()); let form_fields = self .body + .data() .as_ref() - .and_then(|body| match body.data() { + .and_then(|body| match body { RecipeBodyDisplay::Raw(_) => None, RecipeBodyDisplay::Form(form) => { Some(form.data().to_build_overrides()) @@ -119,8 +124,9 @@ impl RecipeDisplay { .unwrap_or_default(); let body = self .body + .data() .as_ref() - .and_then(|body| body.data().override_value()); + .and_then(|body| body.override_value()); BuildOptions { authentication, @@ -133,22 +139,19 @@ impl RecipeDisplay { /// Does the recipe have a body defined? pub fn has_body(&self) -> bool { - self.body.is_some() + self.body.data().is_some() } } impl EventHandler for RecipeDisplay { fn children(&mut self) -> Vec>> { - [ - Some(self.tabs.to_child_mut()), - self.body.as_mut().map(Component::to_child_mut), - Some(self.query.to_child_mut()), - Some(self.headers.to_child_mut()), - self.authentication.as_mut().map(Component::to_child_mut), + vec![ + self.tabs.to_child_mut(), + self.body.to_child_mut(), + self.query.to_child_mut(), + self.headers.to_child_mut(), + self.authentication.to_child_mut(), ] - .into_iter() - .flatten() - .collect() } } @@ -196,11 +199,7 @@ impl Draw for RecipeDisplay { // Recipe content match self.tabs.data().selected() { - Tab::Body => { - if let Some(body) = &self.body { - body.draw(frame, (), content_area, true); - } - } + Tab::Body => self.body.draw_opt(frame, (), content_area, true), Tab::Query => self.query.draw( frame, RecipeFieldTableProps { @@ -220,9 +219,7 @@ impl Draw for RecipeDisplay { true, ), Tab::Authentication => { - if let Some(authentication) = &self.authentication { - authentication.draw(frame, (), content_area, true) - } + self.authentication.draw_opt(frame, (), content_area, true) } } } diff --git a/crates/tui/src/view/component/root.rs b/crates/tui/src/view/component/root.rs index cf5cdd34..524fb529 100644 --- a/crates/tui/src/view/component/root.rs +++ b/crates/tui/src/view/component/root.rs @@ -37,7 +37,7 @@ pub struct Root { // ==== Children ===== primary_view: Component, modal_queue: Component, - notification_text: Option>, + notification_text: Component>, } impl Root { @@ -54,7 +54,7 @@ impl Root { // Children primary_view: primary_view.into(), modal_queue: Component::default(), - notification_text: None, + notification_text: Component::default(), } } @@ -138,7 +138,7 @@ impl EventHandler for Root { Event::Notify(notification) => { self.notification_text = - Some(NotificationText::new(notification).into()) + Some(NotificationText::new(notification)).into() } Event::Input { @@ -218,9 +218,8 @@ impl<'a> Draw> for Root { Constraint::Length(footer.width() as u16), ]) .areas(footer_area); - if let Some(notification_text) = &self.notification_text { - notification_text.draw(frame, (), notification_area, false); - } + self.notification_text + .draw_opt(frame, (), notification_area, false); frame.render_widget(footer, help_area); // Render modals last so they go on top diff --git a/crates/tui/src/view/event.rs b/crates/tui/src/view/event.rs index 50a37d29..0e467ea4 100644 --- a/crates/tui/src/view/event.rs +++ b/crates/tui/src/view/event.rs @@ -49,6 +49,17 @@ pub trait EventHandler { } } +/// Enable `Component>` with an empty event handler +impl EventHandler for Option { + fn update(&mut self, _: &mut UpdateContext, event: Event) -> Update { + Update::Propagate(event) + } + + fn children(&mut self) -> Vec>> { + Vec::new() + } +} + // We can't do a blanket impl of EventHandler based on DerefMut because of the // PersistedLazy's custom ToChild impl, which interferes with the blanket // ToChild impl