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 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 let help_bar_chunks = layout::Layout::default()
131 .direction(Direction::Horizontal)
132 .constraints([
133 Constraint::Fill(1),
135 Constraint::Fill(1),
137 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 let mut constraints = vec![Constraint::Fill(1)];
155 if with_preview {
156 constraints.push(Constraint::Fill(1));
157 }
158 if with_remote {
159 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 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 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
210fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
212 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 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] }
232
233const UI_WIDTH_PERCENT: u16 = 95;
235const UI_HEIGHT_PERCENT: u16 = 95;