use std::{hash::Hash, time::Instant}; use anyhow::Result; use ratatui::{layout::Rect, Frame}; use rustc_hash::FxHashSet; use crate::{ action::Action, channels::entry::Entry, config::Config, picker::Picker, preview::PreviewState, screen::{ colors::Colorscheme, help::draw_help_bar, input::draw_input_box, keybindings::build_keybindings_table, layout::Layout, preview::draw_preview_content_block, remote_control::draw_remote_control, results::draw_results_list, spinner::Spinner, }, television::Mode, utils::metadata::AppMetadata, }; #[derive(Debug, Clone, PartialEq)] /// The state of the current television channel. /// /// This struct is passed along to the UI thread as part of the `TvState` struct. pub struct ChannelState { pub current_channel_name: String, pub selected_entries: FxHashSet, pub total_count: u32, pub running: bool, } impl ChannelState { pub fn new( current_channel_name: String, selected_entries: FxHashSet, total_count: u32, running: bool, ) -> Self { Self { current_channel_name, selected_entries, total_count, running, } } } impl Hash for ChannelState { fn hash(&self, state: &mut H) { self.current_channel_name.hash(state); self.selected_entries .iter() .for_each(|entry| entry.hash(state)); self.total_count.hash(state); self.running.hash(state); } } #[derive(Debug, Clone, PartialEq, Hash)] /// The state of the main thread `Television` struct. /// /// This struct is passed along to the UI thread as part of the `Ctx` struct. pub struct TvState { pub mode: Mode, pub selected_entry: Option, pub results_picker: Picker, pub rc_picker: Picker, pub channel_state: ChannelState, pub spinner: Spinner, pub preview_state: PreviewState, } impl TvState { #[allow(clippy::too_many_arguments)] pub fn new( mode: Mode, selected_entry: Option, results_picker: Picker, rc_picker: Picker, channel_state: ChannelState, spinner: Spinner, preview_state: PreviewState, ) -> Self { Self { mode, selected_entry, results_picker, rc_picker, channel_state, spinner, preview_state, } } } #[derive(Debug, Clone)] /// A drawing context that holds the current state of the application. /// /// This is used as a message passing object between the main thread /// and the UI thread and should contain all the information needed to /// draw a frame. pub struct Ctx { pub tv_state: TvState, pub config: Config, pub colorscheme: Colorscheme, pub app_metadata: AppMetadata, pub instant: Instant, pub layout: Layout, } impl Ctx { pub fn new( tv_state: TvState, config: Config, colorscheme: Colorscheme, app_metadata: AppMetadata, instant: Instant, layout: Layout, ) -> Self { Self { tv_state, config, colorscheme, app_metadata, instant, layout, } } } impl PartialEq for Ctx { fn eq(&self, other: &Self) -> bool { self.tv_state == other.tv_state && self.config == other.config && self.colorscheme == other.colorscheme && self.app_metadata == other.app_metadata } } impl Eq for Ctx {} impl Hash for Ctx { fn hash(&self, state: &mut H) { self.tv_state.hash(state); self.config.hash(state); self.colorscheme.hash(state); self.app_metadata.hash(state); } } impl PartialOrd for Ctx { fn partial_cmp(&self, other: &Self) -> Option { Some(self.instant.cmp(&other.instant)) } } impl Ord for Ctx { fn cmp(&self, other: &Self) -> std::cmp::Ordering { self.instant.cmp(&other.instant) } } /// Draw the current UI frame based on the given context. /// /// This function is responsible for drawing the entire UI frame based on the given context by /// ultimately flushing buffers down to the underlying terminal. /// /// This function is executed by the UI thread whenever it receives a render message from the main /// thread. /// /// It will draw the help bar, the results list, the input box, the preview content block, and the /// remote control. /// /// # Returns /// A `Result` containing the layout of the current frame if the drawing was successful. /// This layout can then be sent back to the main thread to serve for tasks where having that /// information can be useful or lead to optimizations. pub fn draw(ctx: &Ctx, f: &mut Frame<'_>, area: Rect) -> Result { let show_preview = ctx.config.ui.show_preview_panel && ctx.tv_state.preview_state.enabled; let show_remote = !matches!(ctx.tv_state.mode, Mode::Channel); let layout = Layout::build(area, &ctx.config.ui, show_remote, show_preview); // help bar (metadata, keymaps, logo) draw_help_bar( f, &layout.help_bar, &ctx.tv_state.channel_state.current_channel_name, build_keybindings_table( &ctx.config.keybindings.to_displayable(), ctx.tv_state.mode, &ctx.colorscheme, ), ctx.tv_state.mode, &ctx.app_metadata, &ctx.colorscheme, ); // results list draw_results_list( f, layout.results, &ctx.tv_state.results_picker.entries, &ctx.tv_state.channel_state.selected_entries, &mut ctx.tv_state.results_picker.relative_state.clone(), ctx.config.ui.input_bar_position, ctx.config.ui.use_nerd_font_icons, &ctx.colorscheme, &ctx.config .keybindings .get(&Action::ToggleHelp) // just display the first keybinding .unwrap() .to_string(), &ctx.config .keybindings .get(&Action::TogglePreview) // just display the first keybinding .unwrap() .to_string(), )?; // input box draw_input_box( f, layout.input, ctx.tv_state.results_picker.total_items, ctx.tv_state.channel_state.total_count, &ctx.tv_state.results_picker.input, &ctx.tv_state.results_picker.state, ctx.tv_state.channel_state.running, &ctx.tv_state.channel_state.current_channel_name, &ctx.tv_state.spinner, &ctx.colorscheme, )?; if layout.preview_window.is_some() { draw_preview_content_block( f, layout.preview_window.unwrap(), &ctx.tv_state.preview_state, ctx.config.ui.use_nerd_font_icons, &ctx.colorscheme, )?; } // remote control if show_remote { draw_remote_control( f, layout.remote_control.unwrap(), &ctx.tv_state.rc_picker.entries, ctx.config.ui.use_nerd_font_icons, &mut ctx.tv_state.rc_picker.state.clone(), &mut ctx.tv_state.rc_picker.input.clone(), &ctx.tv_state.mode, &ctx.colorscheme, )?; } Ok(layout) }