268 lines
7.3 KiB
Rust

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<Entry>,
pub total_count: u32,
pub running: bool,
}
impl ChannelState {
pub fn new(
current_channel_name: String,
selected_entries: FxHashSet<Entry>,
total_count: u32,
running: bool,
) -> Self {
Self {
current_channel_name,
selected_entries,
total_count,
running,
}
}
}
impl Hash for ChannelState {
fn hash<H: std::hash::Hasher>(&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<Entry>,
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<Entry>,
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<H: std::hash::Hasher>(&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<std::cmp::Ordering> {
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<Layout> {
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)
}