diff --git a/src/tui/view/component/misc.rs b/src/tui/view/component/misc.rs index 333cbaee..a7caf550 100644 --- a/src/tui/view/component/misc.rs +++ b/src/tui/view/component/misc.rs @@ -12,7 +12,7 @@ use crate::{ }, state::Notification, util::{layout, ButtonBrick, ToTui}, - Frame, RenderContext, + DrawContext, }, }, }; @@ -58,26 +58,20 @@ impl Component for ErrorModal { } impl Draw for ErrorModal { - fn draw( - &self, - context: &RenderContext, - _: (), - frame: &mut Frame, - chunk: Rect, - ) { + fn draw(&self, context: &mut DrawContext, _: (), chunk: Rect) { let [content_chunk, footer_chunk] = layout( chunk, Direction::Vertical, [Constraint::Min(0), Constraint::Length(1)], ); - frame.render_widget( + context.frame.render_widget( Paragraph::new(self.0.to_tui(context)).wrap(Wrap::default()), content_chunk, ); // Prompt the user to get out of here - frame.render_widget( + context.frame.render_widget( Paragraph::new( ButtonBrick { text: "OK", @@ -181,14 +175,8 @@ impl Component for PromptModal { } impl Draw for PromptModal { - fn draw( - &self, - _context: &RenderContext, - _: (), - frame: &mut Frame, - chunk: Rect, - ) { - frame.render_widget(self.text_area.widget(), chunk); + fn draw(&self, context: &mut DrawContext, _: (), chunk: Rect) { + context.frame.render_widget(self.text_area.widget(), chunk); } } @@ -213,9 +201,8 @@ pub struct HelpTextProps { impl Draw for HelpText { fn draw( &self, - context: &RenderContext, + context: &mut DrawContext, props: HelpTextProps, - frame: &mut Frame, chunk: Rect, ) { // Decide which actions to show based on context. This is definitely @@ -255,7 +242,7 @@ impl Draw for HelpText { .unwrap_or_else(|| "???".into()) }) .join(" / "); - frame.render_widget(Paragraph::new(text), chunk); + context.frame.render_widget(Paragraph::new(text), chunk); } } @@ -271,14 +258,8 @@ impl NotificationText { } impl Draw for NotificationText { - fn draw( - &self, - context: &RenderContext, - _: (), - frame: &mut Frame, - chunk: Rect, - ) { - frame.render_widget( + fn draw(&self, context: &mut DrawContext, _: (), chunk: Rect) { + context.frame.render_widget( Paragraph::new(self.notification.to_tui(context)), chunk, ); diff --git a/src/tui/view/component/mod.rs b/src/tui/view/component/mod.rs index 3a80c85e..c52bcc18 100644 --- a/src/tui/view/component/mod.rs +++ b/src/tui/view/component/mod.rs @@ -92,20 +92,18 @@ impl UpdateContext { /// attaching a lifetime to the associated type makes using this in a trait /// object very difficult (maybe impossible?). This is an easy shortcut. pub trait Draw { - fn draw( - &self, - context: &RenderContext, - props: Props, - frame: &mut Frame, - chunk: Rect, - ); + fn draw(&self, context: &mut DrawContext, props: Props, chunk: Rect); } -/// Global readonly data that various components need during rendering +/// Global data that various components need during rendering. A mutable +/// reference to this is passed around to give access to the frame, but please +/// don't modify anything :) #[derive(Debug)] -pub struct RenderContext<'a> { +pub struct DrawContext<'a, 'f> { pub input_engine: &'a InputEngine, pub theme: &'a Theme, + // TODO refcell? + pub frame: &'a mut Frame<'f>, } /// A trigger for state change in the view. Events are handled by diff --git a/src/tui/view/component/modal.rs b/src/tui/view/component/modal.rs index 0e51b362..7ae09c92 100644 --- a/src/tui/view/component/modal.rs +++ b/src/tui/view/component/modal.rs @@ -1,13 +1,15 @@ use crate::tui::{ input::Action, view::{ - component::{Component, Draw, Event, UpdateContext, UpdateOutcome}, + component::{ + Component, Draw, DrawContext, Event, UpdateContext, UpdateOutcome, + }, util::centered_rect, }, }; use derive_more::Display; use ratatui::{ - prelude::Constraint, + prelude::{Constraint, Rect}, widgets::{Block, BorderType, Borders, Clear}, }; use std::{collections::VecDeque, ops::DerefMut}; @@ -138,13 +140,7 @@ impl Component for ModalQueue { } impl Draw for ModalQueue { - fn draw( - &self, - context: &crate::tui::view::RenderContext, - _: (), - frame: &mut crate::tui::view::Frame, - chunk: ratatui::prelude::Rect, - ) { + fn draw(&self, context: &mut DrawContext, _: (), chunk: Rect) { if let Some(modal) = self.queue.front() { let (x, y) = modal.dimensions(); let chunk = centered_rect(x, y, chunk); @@ -155,11 +151,11 @@ impl Draw for ModalQueue { let inner_chunk = block.inner(chunk); // Draw the outline of the modal - frame.render_widget(Clear, chunk); - frame.render_widget(block, chunk); + context.frame.render_widget(Clear, chunk); + context.frame.render_widget(block, chunk); // Render the actual content - modal.draw(context, (), frame, inner_chunk); + modal.draw(context, (), inner_chunk); } } } diff --git a/src/tui/view/component/primary.rs b/src/tui/view/component/primary.rs index 61e10638..9137a053 100644 --- a/src/tui/view/component/primary.rs +++ b/src/tui/view/component/primary.rs @@ -13,7 +13,7 @@ use crate::{ }, state::{FixedSelect, RequestState, StatefulList, StatefulSelect}, util::{layout, BlockBrick, ListBrick, ToTui}, - Frame, RenderContext, + DrawContext, }, }, }; @@ -164,9 +164,8 @@ impl Component for PrimaryView { impl<'a> Draw> for PrimaryView { fn draw( &self, - context: &RenderContext, + context: &mut DrawContext, props: PrimaryViewProps<'a>, - frame: &mut Frame, chunk: Rect, ) { // Split the main pane horizontally @@ -197,7 +196,6 @@ impl<'a> Draw> for PrimaryView { ListPaneProps { is_selected: panes.is_selected(&PrimaryPane::ProfileList), }, - frame, profiles_chunk, ); self.recipe_list_pane.draw( @@ -205,7 +203,6 @@ impl<'a> Draw> for PrimaryView { ListPaneProps { is_selected: panes.is_selected(&PrimaryPane::RecipeList), }, - frame, recipes_chunk, ); self.request_pane.draw( @@ -214,7 +211,6 @@ impl<'a> Draw> for PrimaryView { is_selected: panes.is_selected(&PrimaryPane::Request), selected_recipe: self.selected_recipe(), }, - frame, request_chunk, ); self.response_pane.draw( @@ -223,7 +219,6 @@ impl<'a> Draw> for PrimaryView { is_selected: panes.is_selected(&PrimaryPane::Response), active_request: props.active_request, }, - frame, response_chunk, ); } @@ -276,9 +271,8 @@ impl Component for ProfileListPane { impl Draw for ProfileListPane { fn draw( &self, - context: &RenderContext, + context: &mut DrawContext, props: ListPaneProps, - frame: &mut Frame, chunk: Rect, ) { let list = ListBrick { @@ -288,7 +282,7 @@ impl Draw for ProfileListPane { }, list: &self.profiles, }; - frame.render_stateful_widget( + context.frame.render_stateful_widget( list.to_tui(context), chunk, &mut self.profiles.state_mut(), @@ -356,9 +350,8 @@ impl Component for RecipeListPane { impl Draw for RecipeListPane { fn draw( &self, - context: &RenderContext, + context: &mut DrawContext, props: ListPaneProps, - frame: &mut Frame, chunk: Rect, ) { let pane_kind = PrimaryPane::RecipeList; @@ -369,7 +362,7 @@ impl Draw for RecipeListPane { }, list: &self.recipes, }; - frame.render_stateful_widget( + context.frame.render_stateful_widget( list.to_tui(context), chunk, &mut self.recipes.state_mut(), diff --git a/src/tui/view/component/request.rs b/src/tui/view/component/request.rs index b856731e..7abf1d18 100644 --- a/src/tui/view/component/request.rs +++ b/src/tui/view/component/request.rs @@ -9,7 +9,7 @@ use crate::{ }, state::{FixedSelect, StatefulSelect}, util::{layout, BlockBrick, TabBrick, ToTui}, - Frame, RenderContext, + DrawContext, }, }, }; @@ -76,9 +76,8 @@ impl Component for RequestPane { impl<'a> Draw> for RequestPane { fn draw( &self, - context: &RenderContext, + context: &mut DrawContext, props: RequestPaneProps<'a>, - frame: &mut Frame, chunk: Rect, ) { // Render outermost block @@ -89,7 +88,7 @@ impl<'a> Draw> for RequestPane { }; let block = block.to_tui(context); let inner_chunk = block.inner(chunk); - frame.render_widget(block, chunk); + context.frame.render_widget(block, chunk); // Render request contents if let Some(recipe) = props.selected_recipe { @@ -104,31 +103,34 @@ impl<'a> Draw> for RequestPane { ); // URL - frame.render_widget( + context.frame.render_widget( Paragraph::new(format!("{} {}", recipe.method, recipe.url)), url_chunk, ); // Navigation tabs let tabs = TabBrick { tabs: &self.tabs }; - frame.render_widget(tabs.to_tui(context), tabs_chunk); + context + .frame + .render_widget(tabs.to_tui(context), tabs_chunk); // Request content match self.tabs.selected() { RequestTab::Body => { if let Some(body) = recipe.body.as_deref() { - frame + context + .frame .render_widget(Paragraph::new(body), content_chunk); } } RequestTab::Query => { - frame.render_widget( + context.frame.render_widget( Paragraph::new(recipe.query.to_tui(context)), content_chunk, ); } RequestTab::Headers => { - frame.render_widget( + context.frame.render_widget( Paragraph::new(recipe.headers.to_tui(context)), content_chunk, ); diff --git a/src/tui/view/component/response.rs b/src/tui/view/component/response.rs index 2b986c58..ca0cec48 100644 --- a/src/tui/view/component/response.rs +++ b/src/tui/view/component/response.rs @@ -7,7 +7,7 @@ use crate::tui::{ }, state::{FixedSelect, RequestState, StatefulSelect}, util::{layout, BlockBrick, TabBrick, ToTui}, - Frame, RenderContext, + DrawContext, }, }; use derive_more::Display; @@ -75,9 +75,9 @@ impl Component for ResponsePane { impl<'a> Draw> for ResponsePane { fn draw( &self, - context: &RenderContext, + context: &mut DrawContext, props: ResponsePaneProps<'a>, - frame: &mut Frame, + chunk: Rect, ) { // Render outermost block @@ -88,7 +88,7 @@ impl<'a> Draw> for ResponsePane { }; let block = block.to_tui(context); let inner_chunk = block.inner(chunk); - frame.render_widget(block, chunk); + context.frame.render_widget(block, chunk); // Don't render anything else unless we have a request state if let Some(request_state) = props.active_request { @@ -108,7 +108,7 @@ impl<'a> Draw> for ResponsePane { if let (Some(start_time), Some(duration)) = (request_state.start_time(), request_state.duration()) { - frame.render_widget( + context.frame.render_widget( Paragraph::new(Line::from(vec![ start_time.to_tui(context), " / ".into(), @@ -121,7 +121,7 @@ impl<'a> Draw> for ResponsePane { match &request_state { RequestState::Building { .. } => { - frame.render_widget( + context.frame.render_widget( Paragraph::new("Initializing request..."), header_left_chunk, ); @@ -129,7 +129,7 @@ impl<'a> Draw> for ResponsePane { // :( RequestState::BuildError { error } => { - frame.render_widget( + context.frame.render_widget( Paragraph::new(error.to_tui(context)) .wrap(Wrap::default()), content_chunk, @@ -137,7 +137,7 @@ impl<'a> Draw> for ResponsePane { } RequestState::Loading { .. } => { - frame.render_widget( + context.frame.render_widget( Paragraph::new("Loading..."), header_left_chunk, ); @@ -149,7 +149,7 @@ impl<'a> Draw> for ResponsePane { } => { let response = &record.response; // Status code - frame.render_widget( + context.frame.render_widget( Paragraph::new(response.status.to_string()), header_left_chunk, ); @@ -163,7 +163,9 @@ impl<'a> Draw> for ResponsePane { // Navigation tabs let tabs = TabBrick { tabs: &self.tabs }; - frame.render_widget(tabs.to_tui(context), tabs_chunk); + context + .frame + .render_widget(tabs.to_tui(context), tabs_chunk); // Main content for the response match self.tabs.selected() { @@ -171,13 +173,13 @@ impl<'a> Draw> for ResponsePane { let body = pretty_body .as_deref() .unwrap_or(response.body.text()); - frame.render_widget( + context.frame.render_widget( Paragraph::new(body), content_chunk, ); } ResponseTab::Headers => { - frame.render_widget( + context.frame.render_widget( Paragraph::new( response.headers.to_tui(context), ), @@ -189,7 +191,7 @@ impl<'a> Draw> for ResponsePane { // Sadge RequestState::RequestError { error } => { - frame.render_widget( + context.frame.render_widget( Paragraph::new(error.to_tui(context)) .wrap(Wrap::default()), content_chunk, diff --git a/src/tui/view/component/root.rs b/src/tui/view/component/root.rs index d4c1d2b8..2caceca1 100644 --- a/src/tui/view/component/root.rs +++ b/src/tui/view/component/root.rs @@ -14,7 +14,7 @@ use crate::{ }, state::RequestState, util::layout, - Frame, RenderContext, + DrawContext, }, }, }; @@ -173,13 +173,7 @@ impl Component for Root { } impl Draw for Root { - fn draw( - &self, - context: &RenderContext, - _: (), - frame: &mut Frame, - chunk: Rect, - ) { + fn draw(&self, context: &mut DrawContext, _: (), chunk: Rect) { // Create layout let [main_chunk, footer_chunk] = layout( chunk, @@ -194,7 +188,6 @@ impl Draw for Root { PrimaryViewProps { active_request: self.active_request(), }, - frame, main_chunk, ), Some(FullscreenMode::Request) => { @@ -204,7 +197,6 @@ impl Draw for Root { is_selected: false, selected_recipe: self.primary_view.selected_recipe(), }, - frame, main_chunk, ); } @@ -215,7 +207,6 @@ impl Draw for Root { is_selected: false, active_request: self.active_request(), }, - frame, main_chunk, ); } @@ -224,7 +215,7 @@ impl Draw for Root { // Footer match &self.notification_text { Some(notification_text) => { - notification_text.draw(context, (), frame, footer_chunk) + notification_text.draw(context, (), footer_chunk) } None => HelpText.draw( context, @@ -233,12 +224,11 @@ impl Draw for Root { fullscreen_mode: self.fullscreen_mode, selected_pane: self.primary_view.selected_pane(), }, - frame, footer_chunk, ), } // Render modals last so they go on top - self.modal_queue.draw(context, (), frame, frame.size()); + self.modal_queue.draw(context, (), context.frame.size()); } } diff --git a/src/tui/view/mod.rs b/src/tui/view/mod.rs index c5f83b2e..223b5642 100644 --- a/src/tui/view/mod.rs +++ b/src/tui/view/mod.rs @@ -13,7 +13,7 @@ use crate::{ message::MessageSender, view::{ component::{ - Component, Draw, Event, IntoModal, RenderContext, Root, + Component, Draw, DrawContext, Event, IntoModal, Root, UpdateContext, UpdateOutcome, }, state::Notification, @@ -56,15 +56,20 @@ impl View { /// Draw the view to screen. This needs access to the input engine in order /// to render input bindings as help messages to the user. - pub fn draw(&self, input_engine: &InputEngine, frame: &mut Frame) { + pub fn draw<'a>( + &'a self, + input_engine: &'a InputEngine, + frame: &'a mut Frame, + ) { + let chunk = frame.size(); self.root.draw( - &RenderContext { + &mut DrawContext { input_engine, theme: &self.theme, + frame, }, (), - frame, - frame.size(), + chunk, ) } diff --git a/src/tui/view/util.rs b/src/tui/view/util.rs index 6e6627e0..4419f1c6 100644 --- a/src/tui/view/util.rs +++ b/src/tui/view/util.rs @@ -5,7 +5,7 @@ use crate::{ http::{RequestBuildError, RequestError}, tui::view::{ state::{FixedSelect, Notification, StatefulList, StatefulSelect}, - RenderContext, + DrawContext, }, }; use chrono::{DateTime, Duration, Local, Utc}; @@ -26,7 +26,7 @@ pub trait ToTui { Self: 'this; /// Build a UI element - fn to_tui(&self, context: &RenderContext) -> Self::Output<'_>; + fn to_tui(&self, context: &DrawContext) -> Self::Output<'_>; } /// A container with a title and border @@ -38,7 +38,7 @@ pub struct BlockBrick { impl ToTui for BlockBrick { type Output<'this> = Block<'this> where Self: 'this; - fn to_tui(&self, context: &RenderContext) -> Self::Output<'_> { + fn to_tui(&self, context: &DrawContext) -> Self::Output<'_> { Block::default() .borders(Borders::ALL) .border_style(context.theme.pane_border_style(self.is_focused)) @@ -55,7 +55,7 @@ pub struct ButtonBrick<'a> { impl<'a> ToTui for ButtonBrick<'a> { type Output<'this> = Text<'this> where Self: 'this; - fn to_tui(&self, context: &RenderContext) -> Self::Output<'_> { + fn to_tui(&self, context: &DrawContext) -> Self::Output<'_> { Text::styled(self.text, context.theme.text_highlight_style) } } @@ -67,7 +67,7 @@ pub struct TabBrick<'a, T: FixedSelect> { impl<'a, T: FixedSelect> ToTui for TabBrick<'a, T> { type Output<'this> = Tabs<'this> where Self: 'this; - fn to_tui(&self, context: &RenderContext) -> Self::Output<'_> { + fn to_tui(&self, context: &DrawContext) -> Self::Output<'_> { Tabs::new(T::iter().map(|e| e.to_string()).collect()) .select(self.tabs.selected_index()) .highlight_style(context.theme.text_highlight_style) @@ -83,7 +83,7 @@ pub struct ListBrick<'a, T: ToTui = Span<'a>>> { impl<'a, T: ToTui = Span<'a>>> ToTui for ListBrick<'a, T> { type Output<'this> = List<'this> where Self: 'this; - fn to_tui(&self, context: &RenderContext) -> Self::Output<'_> { + fn to_tui(&self, context: &DrawContext) -> Self::Output<'_> { let block = self.block.to_tui(context); // Convert each list item into text @@ -104,7 +104,7 @@ impl<'a, T: ToTui = Span<'a>>> ToTui for ListBrick<'a, T> { impl ToTui for Profile { type Output<'this> = Span<'this> where Self: 'this; - fn to_tui(&self, _context: &RenderContext) -> Self::Output<'_> { + fn to_tui(&self, _context: &DrawContext) -> Self::Output<'_> { self.name().to_owned().into() } } @@ -112,7 +112,7 @@ impl ToTui for Profile { impl ToTui for RequestRecipe { type Output<'this> = Span<'this> where Self: 'this; - fn to_tui(&self, _context: &RenderContext) -> Self::Output<'_> { + fn to_tui(&self, _context: &DrawContext) -> Self::Output<'_> { format!("[{}] {}", self.method, self.name()).into() } } @@ -120,7 +120,7 @@ impl ToTui for RequestRecipe { impl ToTui for Notification { type Output<'this> = Span<'this> where Self: 'this; - fn to_tui(&self, _context: &RenderContext) -> Self::Output<'_> { + fn to_tui(&self, _context: &DrawContext) -> Self::Output<'_> { format!( "[{}] {}", self.timestamp.with_timezone(&Local).format("%H:%M:%S"), @@ -134,7 +134,7 @@ impl ToTui for Notification { impl ToTui for DateTime { type Output<'this> = Span<'this> where Self: 'this; - fn to_tui(&self, _context: &RenderContext) -> Self::Output<'_> { + fn to_tui(&self, _context: &DrawContext) -> Self::Output<'_> { self.with_timezone(&Local) .format("%b %e %H:%M:%S") .to_string() @@ -146,7 +146,7 @@ impl ToTui for Duration { /// 'static because string is generated type Output<'this> = Span<'static>; - fn to_tui(&self, _context: &RenderContext) -> Self::Output<'_> { + fn to_tui(&self, _context: &DrawContext) -> Self::Output<'_> { let ms = self.num_milliseconds(); if ms < 1000 { format!("{ms}ms").into() @@ -159,7 +159,7 @@ impl ToTui for Duration { impl ToTui for Option { type Output<'this> = Span<'this> where Self: 'this; - fn to_tui(&self, context: &RenderContext) -> Self::Output<'_> { + fn to_tui(&self, context: &DrawContext) -> Self::Output<'_> { match self { Some(duration) => duration.to_tui(context), // For incomplete requests typically @@ -171,7 +171,7 @@ impl ToTui for Option { impl ToTui for IndexMap { type Output<'this> = Text<'this> where Self: 'this; - fn to_tui(&self, _context: &RenderContext) -> Self::Output<'_> { + fn to_tui(&self, _context: &DrawContext) -> Self::Output<'_> { self.iter() .map(|(key, value)| format!("{key} = {value}").into()) .collect::>() @@ -183,7 +183,7 @@ impl ToTui for HeaderMap { /// 'static because string is generated type Output<'this> = Text<'static>; - fn to_tui(&self, _context: &RenderContext) -> Self::Output<'_> { + fn to_tui(&self, _context: &DrawContext) -> Self::Output<'_> { self.iter() .map(|(key, value)| { format!( @@ -201,7 +201,7 @@ impl ToTui for anyhow::Error { /// 'static because string is generated type Output<'this> = Text<'static>; - fn to_tui(&self, _context: &RenderContext) -> Self::Output<'_> { + fn to_tui(&self, _context: &DrawContext) -> Self::Output<'_> { self.chain() .enumerate() .map(|(i, err)| { @@ -216,7 +216,7 @@ impl ToTui for anyhow::Error { impl ToTui for RequestBuildError { type Output<'this> = Text<'static>; - fn to_tui(&self, context: &RenderContext) -> Self::Output<'_> { + fn to_tui(&self, context: &DrawContext) -> Self::Output<'_> { // Defer to the underlying anyhow error self.error.to_tui(context) } @@ -225,7 +225,7 @@ impl ToTui for RequestBuildError { impl ToTui for RequestError { type Output<'this> = Text<'static>; - fn to_tui(&self, _context: &RenderContext) -> Self::Output<'_> { + fn to_tui(&self, _context: &DrawContext) -> Self::Output<'_> { self.error.to_string().into() } }