television_screen/
remote_control.rs

1use rustc_hash::FxHashMap;
2
3use crate::colors::{Colorscheme, GeneralColorscheme};
4use crate::logo::build_remote_logo_paragraph;
5use crate::mode::{mode_color, Mode};
6use crate::results::build_results_list;
7use television_channels::entry::Entry;
8use television_utils::input::Input;
9
10use color_eyre::eyre::Result;
11use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect};
12use ratatui::prelude::Style;
13use ratatui::style::{Color, Stylize};
14use ratatui::text::{Line, Span};
15use ratatui::widgets::{
16    Block, BorderType, Borders, ListDirection, ListState, Padding, Paragraph,
17};
18use ratatui::Frame;
19
20#[allow(clippy::too_many_arguments)]
21pub fn draw_remote_control(
22    f: &mut Frame,
23    rect: Rect,
24    entries: &[Entry],
25    use_nerd_font_icons: bool,
26    picker_state: &mut ListState,
27    input_state: &mut Input,
28    icon_color_cache: &mut FxHashMap<String, Color>,
29    mode: &Mode,
30    colorscheme: &Colorscheme,
31) -> Result<()> {
32    let layout = Layout::default()
33        .direction(Direction::Vertical)
34        .constraints(
35            [
36                Constraint::Min(3),
37                Constraint::Length(3),
38                Constraint::Length(20),
39            ]
40            .as_ref(),
41        )
42        .split(rect);
43    draw_rc_channels(
44        f,
45        layout[0],
46        entries,
47        use_nerd_font_icons,
48        picker_state,
49        icon_color_cache,
50        colorscheme,
51    );
52    draw_rc_input(f, layout[1], input_state, colorscheme)?;
53    draw_rc_logo(
54        f,
55        layout[2],
56        mode_color(*mode, &colorscheme.mode),
57        &colorscheme.general,
58    );
59    Ok(())
60}
61
62fn draw_rc_channels(
63    f: &mut Frame,
64    area: Rect,
65    entries: &[Entry],
66    use_nerd_font_icons: bool,
67    picker_state: &mut ListState,
68    icon_color_cache: &mut FxHashMap<String, Color>,
69    colorscheme: &Colorscheme,
70) {
71    let rc_block = Block::default()
72        .borders(Borders::ALL)
73        .border_type(BorderType::Rounded)
74        .border_style(Style::default().fg(colorscheme.general.border_fg))
75        .style(
76            Style::default()
77                .bg(colorscheme.general.background.unwrap_or_default()),
78        )
79        .padding(Padding::right(1));
80
81    let channel_list = build_results_list(
82        rc_block,
83        entries,
84        None,
85        ListDirection::TopToBottom,
86        use_nerd_font_icons,
87        icon_color_cache,
88        &colorscheme.results,
89    );
90
91    f.render_stateful_widget(channel_list, area, picker_state);
92}
93
94fn draw_rc_input(
95    f: &mut Frame,
96    area: Rect,
97    input: &mut Input,
98    colorscheme: &Colorscheme,
99) -> Result<()> {
100    let input_block = Block::default()
101        .title_top(Line::from("Remote Control").alignment(Alignment::Center))
102        .borders(Borders::ALL)
103        .border_type(BorderType::Rounded)
104        .border_style(Style::default().fg(colorscheme.general.border_fg))
105        .style(
106            Style::default()
107                .bg(colorscheme.general.background.unwrap_or_default()),
108        );
109
110    let input_block_inner = input_block.inner(area);
111
112    f.render_widget(input_block, area);
113
114    // split input block into 2 parts: prompt symbol, input
115    let inner_input_chunks = Layout::default()
116        .direction(Direction::Horizontal)
117        .constraints([
118            // prompt symbol
119            Constraint::Length(2),
120            // input field
121            Constraint::Fill(1),
122        ])
123        .split(input_block_inner);
124
125    let prompt_symbol_block = Block::default();
126    let arrow = Paragraph::new(Span::styled(
127        "> ",
128        Style::default().fg(colorscheme.input.input_fg).bold(),
129    ))
130    .block(prompt_symbol_block);
131    f.render_widget(arrow, inner_input_chunks[0]);
132
133    let interactive_input_block = Block::default();
134    // keep 2 for borders and 1 for cursor
135    let width = inner_input_chunks[1].width.max(3) - 3;
136    let scroll = input.visual_scroll(width as usize);
137    let input_paragraph = Paragraph::new(input.value())
138        .scroll((0, u16::try_from(scroll)?))
139        .block(interactive_input_block)
140        .style(
141            Style::default()
142                .fg(colorscheme.input.input_fg)
143                .bold()
144                .italic(),
145        )
146        .alignment(Alignment::Left);
147    f.render_widget(input_paragraph, inner_input_chunks[1]);
148
149    // Make the cursor visible and ask tui-rs to put it at the
150    // specified coordinates after rendering
151    f.set_cursor_position((
152        // Put cursor past the end of the input text
153        inner_input_chunks[1].x
154            + u16::try_from(input.visual_cursor().max(scroll) - scroll)?,
155        // Move one line down, from the border to the input line
156        inner_input_chunks[1].y,
157    ));
158    Ok(())
159}
160fn draw_rc_logo(
161    f: &mut Frame,
162    area: Rect,
163    mode_color: Color,
164    colorscheme: &GeneralColorscheme,
165) {
166    let logo_block = Block::default().style(
167        Style::default()
168            .fg(mode_color)
169            .bg(colorscheme.background.unwrap_or_default()),
170    );
171
172    let logo_paragraph = build_remote_logo_paragraph()
173        .alignment(Alignment::Center)
174        .block(logo_block);
175
176    f.render_widget(logo_paragraph, area);
177}