television_screen/
layout.rs

1use std::fmt::Display;
2
3use ratatui::layout;
4use ratatui::layout::{Constraint, Direction, Rect};
5use serde::Deserialize;
6
7pub struct Dimensions {
8    pub x: u16,
9    pub y: u16,
10}
11
12impl Dimensions {
13    pub fn new(x: u16, y: u16) -> Self {
14        Self { x, y }
15    }
16}
17
18impl From<u16> for Dimensions {
19    fn from(x: u16) -> Self {
20        Self::new(x, x)
21    }
22}
23
24impl Default for Dimensions {
25    fn default() -> Self {
26        Self::new(UI_WIDTH_PERCENT, UI_HEIGHT_PERCENT)
27    }
28}
29
30#[derive(Debug, Clone, Copy)]
31pub struct HelpBarLayout {
32    pub left: Rect,
33    pub middle: Rect,
34    pub right: Rect,
35}
36
37impl HelpBarLayout {
38    pub fn new(left: Rect, middle: Rect, right: Rect) -> Self {
39        Self {
40            left,
41            middle,
42            right,
43        }
44    }
45}
46
47#[derive(Debug, Clone, Copy, Deserialize, Default, PartialEq)]
48pub enum InputPosition {
49    #[serde(rename = "top")]
50    Top,
51    #[serde(rename = "bottom")]
52    #[default]
53    Bottom,
54}
55
56impl Display for InputPosition {
57    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        match self {
59            InputPosition::Top => write!(f, "top"),
60            InputPosition::Bottom => write!(f, "bottom"),
61        }
62    }
63}
64
65#[derive(Debug, Clone, Copy, Deserialize, Default)]
66pub enum PreviewTitlePosition {
67    #[serde(rename = "top")]
68    #[default]
69    Top,
70    #[serde(rename = "bottom")]
71    Bottom,
72}
73
74impl Display for PreviewTitlePosition {
75    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76        match self {
77            PreviewTitlePosition::Top => write!(f, "top"),
78            PreviewTitlePosition::Bottom => write!(f, "bottom"),
79        }
80    }
81}
82
83pub struct Layout {
84    pub help_bar: Option<HelpBarLayout>,
85    pub results: Rect,
86    pub input: Rect,
87    pub preview_window: Option<Rect>,
88    pub remote_control: Option<Rect>,
89}
90
91impl Layout {
92    #[allow(clippy::too_many_arguments)]
93    pub fn new(
94        help_bar: Option<HelpBarLayout>,
95        results: Rect,
96        input: Rect,
97        preview_window: Option<Rect>,
98        remote_control: Option<Rect>,
99    ) -> Self {
100        Self {
101            help_bar,
102            results,
103            input,
104            preview_window,
105            remote_control,
106        }
107    }
108
109    pub fn build(
110        dimensions: &Dimensions,
111        area: Rect,
112        with_remote: bool,
113        with_help_bar: bool,
114        with_preview: bool,
115        input_position: InputPosition,
116    ) -> Self {
117        let main_block = centered_rect(dimensions.x, dimensions.y, area);
118        // split the main block into two vertical chunks (help bar + rest)
119        let main_rect: Rect;
120        let help_bar_layout: Option<HelpBarLayout>;
121
122        if with_help_bar {
123            let hz_chunks = layout::Layout::default()
124                .direction(Direction::Vertical)
125                .constraints([Constraint::Max(9), Constraint::Fill(1)])
126                .split(main_block);
127            main_rect = hz_chunks[1];
128
129            // split the help bar into three horizontal chunks (left + center + right)
130            let help_bar_chunks = layout::Layout::default()
131                .direction(Direction::Horizontal)
132                .constraints([
133                    // metadata
134                    Constraint::Fill(1),
135                    // keymaps
136                    Constraint::Fill(1),
137                    // logo
138                    Constraint::Length(24),
139                ])
140                .split(hz_chunks[0]);
141
142            help_bar_layout = Some(HelpBarLayout {
143                left: help_bar_chunks[0],
144                middle: help_bar_chunks[1],
145                right: help_bar_chunks[2],
146            });
147        } else {
148            main_rect = main_block;
149            help_bar_layout = None;
150        }
151
152        // split the main block into 1, 2, or 3 vertical chunks
153        // (results + preview + remote)
154        let mut constraints = vec![Constraint::Fill(1)];
155        if with_preview {
156            constraints.push(Constraint::Fill(1));
157        }
158        if with_remote {
159            // in order to fit with the help bar logo
160            constraints.push(Constraint::Length(24));
161        }
162        let vt_chunks = layout::Layout::default()
163            .direction(Direction::Horizontal)
164            .constraints(constraints)
165            .split(main_rect);
166
167        // left block: results + input field
168        let results_constraints =
169            vec![Constraint::Min(3), Constraint::Length(3)];
170
171        let left_chunks = layout::Layout::default()
172            .direction(Direction::Vertical)
173            .constraints(match input_position {
174                InputPosition::Top => {
175                    results_constraints.into_iter().rev().collect()
176                }
177                InputPosition::Bottom => results_constraints,
178            })
179            .split(vt_chunks[0]);
180        let (input, results) = match input_position {
181            InputPosition::Bottom => (left_chunks[1], left_chunks[0]),
182            InputPosition::Top => (left_chunks[0], left_chunks[1]),
183        };
184
185        // right block: preview title + preview
186        let mut remote_idx = 1;
187        let preview_window = if with_preview {
188            remote_idx += 1;
189            Some(vt_chunks[1])
190        } else {
191            None
192        };
193
194        let remote_control = if with_remote {
195            Some(vt_chunks[remote_idx])
196        } else {
197            None
198        };
199
200        Self::new(
201            help_bar_layout,
202            results,
203            input,
204            preview_window,
205            remote_control,
206        )
207    }
208}
209
210/// helper function to create a centered rect using up certain percentage of the available rect `r`
211fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
212    // Cut the given rectangle into three vertical pieces
213    let popup_layout = layout::Layout::default()
214        .direction(Direction::Vertical)
215        .constraints([
216            Constraint::Percentage((100 - percent_y) / 2),
217            Constraint::Percentage(percent_y),
218            Constraint::Percentage((100 - percent_y) / 2),
219        ])
220        .split(r);
221
222    // Then cut the middle vertical piece into three width-wise pieces
223    layout::Layout::default()
224        .direction(Direction::Horizontal)
225        .constraints([
226            Constraint::Percentage((100 - percent_x) / 2),
227            Constraint::Percentage(percent_x),
228            Constraint::Percentage((100 - percent_x) / 2),
229        ])
230        .split(popup_layout[1])[1] // Return the middle chunk
231}
232
233// UI size
234const UI_WIDTH_PERCENT: u16 = 95;
235const UI_HEIGHT_PERCENT: u16 = 95;