1mod display;
2mod input;
3mod overlay;
4mod preview;
5mod results;
6pub use display::*;
7pub use input::*;
8pub use overlay::*;
9pub use preview::*;
10pub use results::*;
11
12pub use ratatui::{
13 Frame,
14 layout::{Constraint, Direction, Layout, Rect},
15 widgets::Table,
16}; use crate::{
19 SSS, Selection, Selector,
20 config::{
21 DisplayConfig, InputConfig, PreviewLayout, RenderConfig, ResultsConfig, StatusConfig,
22 TerminalLayoutSettings, UiConfig,
23 },
24 nucleo::Worker,
25 preview::Preview,
26 render::Click,
27 tui::Tui,
28};
29pub struct UI {
31 pub layout: Option<TerminalLayoutSettings>,
32 area: Rect, pub config: UiConfig,
34}
35
36impl UI {
38 pub fn new<'a, T: SSS, S: Selection, W: std::io::Write>(
39 mut config: RenderConfig,
40 matcher: &'a mut nucleo::Matcher,
41 worker: Worker<T>,
42 selection_set: Selector<T, S>,
43 view: Option<Preview>,
44 tui: &mut Tui<W>,
45 hidden_columns: Vec<bool>,
46 ) -> (Self, PickerUI<'a, T, S>, DisplayUI, Option<PreviewUI>) {
47 assert!(!worker.columns.is_empty());
48
49 if config.results.reverse.is_none() {
50 config.results.reverse = (
51 tui.is_fullscreen() && tui.area.y < tui.area.height / 2
52 )
54 .into()
55 }
56
57 let ui_area = [
58 tui.area.width.saturating_sub(config.ui.border.width()),
59 tui.area.height.saturating_sub(config.ui.border.height()),
60 ];
61
62 let area = Rect {
63 x: tui.area.x + config.ui.border.left(),
64 y: tui.area.y + config.ui.border.top(),
65 width: ui_area[0],
66 height: ui_area[1],
67 };
68
69 let ui = Self {
70 layout: tui.config.layout.clone(),
71 area,
72 config: config.ui,
73 };
74
75 let mut picker = PickerUI::new(
76 config.results,
77 config.status,
78 config.input,
79 config.header,
80 matcher,
81 worker,
82 selection_set,
83 );
84 picker.results.hidden_columns(hidden_columns);
85
86 let preview = if let Some(view) = view {
87 Some(PreviewUI::new(view, config.preview, ui_area))
88 } else {
89 None
90 };
91
92 let footer = DisplayUI::new(config.footer);
93
94 (ui, picker, footer, preview)
95 }
96
97 pub fn update_dimensions(&mut self, area: Rect) {
98 let border = &self.config.border;
99
100 self.area = Rect {
101 x: area.x + border.left(),
102 y: area.y + border.top(),
103 width: area.width.saturating_sub(border.width()),
104 height: area.height.saturating_sub(border.height()),
105 };
106 }
107
108 pub fn make_ui(&self) -> ratatui::widgets::Block<'_> {
109 self.config.border.as_block()
110 }
111
112 pub fn area(&self) -> Rect {
113 self.area
114 }
115
116 pub fn compute_area(&self, area: &Rect) -> Rect {
117 Rect {
118 x: area.x + self.config.border.left(),
119 y: area.y + self.config.border.top(),
120 width: area.width.saturating_sub(self.config.border.width()),
121 height: area.height.saturating_sub(self.config.border.height()),
122 }
123 }
124
125 pub fn full_area(&self) -> Rect {
126 Rect {
127 x: self.area.x - self.config.border.left(),
128 y: self.area.y - self.config.border.top(),
129 width: self.area.width + self.config.border.width(),
130 height: self.area.height + self.config.border.height(),
131 }
132 }
133}
134
135pub struct PickerUI<'a, T: SSS, S: Selection> {
136 pub results: ResultsUI,
137 pub input: InputUI,
138 pub header: DisplayUI,
139 pub matcher: &'a mut nucleo::Matcher,
140 pub selector: Selector<T, S>,
141 pub worker: Worker<T>,
142}
143
144impl<'a, T: SSS, S: Selection> PickerUI<'a, T, S> {
145 pub fn new(
146 results_config: ResultsConfig,
147 status_config: StatusConfig,
148 input_config: InputConfig,
149 header_config: DisplayConfig,
150 matcher: &'a mut nucleo::Matcher,
151 worker: Worker<T>,
152 selections: Selector<T, S>,
153 ) -> Self {
154 Self {
155 results: ResultsUI::new(results_config, status_config),
156 input: InputUI::new(input_config),
157 header: DisplayUI::new(header_config),
158 matcher,
159 selector: selections,
160 worker,
161 }
162 }
163
164 pub fn layout(&self, area: Rect) -> [Rect; 4] {
165 let PickerUI {
166 input,
167 header,
168 results,
169 ..
170 } = self;
171
172 let mut constraints = [
173 Constraint::Length(1 + input.config.border.height()), Constraint::Length(results.status_config.show as u16), Constraint::Length(header.height()),
176 Constraint::Fill(1), ];
178
179 if self.reverse() {
180 constraints.reverse();
181 }
182
183 let chunks = Layout::default()
184 .direction(Direction::Vertical)
185 .constraints(constraints)
186 .split(area);
187
188 std::array::from_fn(|i| {
189 chunks[if self.reverse() {
190 chunks.len() - i - 1
191 } else {
192 i
193 }]
194 })
195 }
196}
197
198impl<'a, T: SSS, O: Selection> PickerUI<'a, T, O> {
199 pub fn make_table(&mut self, click: &mut Click) -> (Table<'_>, u16) {
200 let cursor_byte = self.input.byte_index(self.input.cursor() as usize);
201 let active_column = self.worker.query.active_column_index(cursor_byte);
202
203 let table = self.results.make_table(
204 active_column,
205 &mut self.worker,
206 &mut self.selector,
207 self.matcher,
208 click,
209 );
210 let width = self.results.table_width();
211 (table, width)
212 }
213
214 pub fn update(&mut self) {
215 self.worker.find(&self.input.input);
216 }
217 pub fn tick(&mut self) {
218 self.worker.find(&self.input.input);
219 }
220
221 pub fn reverse(&self) -> bool {
223 self.results.reverse()
224 }
225}
226
227impl PreviewLayout {
228 pub fn split(&self, area: Rect) -> [Rect; 2] {
229 use crate::config::Side;
230 use ratatui::layout::{Constraint, Direction, Layout};
231
232 let direction = match self.side {
233 Side::Left | Side::Right => Direction::Horizontal,
234 Side::Top | Side::Bottom => Direction::Vertical,
235 };
236
237 let side_first = matches!(self.side, Side::Left | Side::Top);
238
239 let total = if matches!(direction, Direction::Horizontal) {
240 area.width
241 } else {
242 area.height
243 };
244
245 let min = if self.min < 0 {
246 total.saturating_sub((-self.min) as u16)
247 } else {
248 self.min as u16
249 };
250
251 let max = if self.max < 0 {
252 total.saturating_sub((-self.max) as u16)
253 } else {
254 self.max as u16
255 };
256
257 let side_size = if min <= max {
258 self.percentage.compute_clamped(total, min, max)
259 } else {
260 log::error!("PreviewLayout min > max: {min} > {max}. Ignoring max.");
261 self.percentage.compute_clamped(total, min, 0)
262 };
263
264 let side_constraint = Constraint::Length(side_size);
265
266 let constraints = if side_first {
267 [side_constraint, Constraint::Min(0)]
268 } else {
269 [Constraint::Min(0), side_constraint]
270 };
271
272 let chunks = Layout::default()
273 .direction(direction)
274 .constraints(constraints)
275 .split(area);
276
277 if side_first {
278 [chunks[0], chunks[1]]
279 } else {
280 [chunks[1], chunks[0]]
281 }
282 }
283}