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 pub 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 = Self {
58 layout: tui.config.layout.clone(),
59 area: tui.area,
60 config: config.ui,
61 };
62
63 let mut picker = PickerUI::new(
64 config.results,
65 config.status,
66 config.input,
67 config.header,
68 matcher,
69 worker,
70 selection_set,
71 );
72 picker.results.hidden_columns(hidden_columns);
73
74 let preview = if let Some(view) = view {
75 Some(PreviewUI::new(view, config.preview))
76 } else {
77 None
78 };
79
80 let footer = DisplayUI::new(config.footer);
81
82 (ui, picker, footer, preview)
83 }
84
85 pub fn update_dimensions(&mut self, area: Rect) {
86 self.area = area;
87 }
88
89 pub fn make_ui(&self) -> ratatui::widgets::Block<'_> {
90 self.config.border.as_block()
91 }
92
93 pub fn inner_area(&self, area: &Rect) -> Rect {
94 Rect {
95 x: area.x + self.config.border.left(),
96 y: area.y + self.config.border.top(),
97 width: area.width.saturating_sub(self.config.border.width()),
98 height: area.height.saturating_sub(self.config.border.height()),
99 }
100 }
101}
102
103pub struct PickerUI<'a, T: SSS, S: Selection> {
104 pub results: ResultsUI,
105 pub input: InputUI,
106 pub header: DisplayUI,
107 pub matcher: &'a mut nucleo::Matcher,
108 pub selector: Selector<T, S>,
109 pub worker: Worker<T>,
110}
111
112impl<'a, T: SSS, S: Selection> PickerUI<'a, T, S> {
113 pub fn new(
114 results_config: ResultsConfig,
115 status_config: StatusConfig,
116 input_config: InputConfig,
117 header_config: DisplayConfig,
118 matcher: &'a mut nucleo::Matcher,
119 worker: Worker<T>,
120 selections: Selector<T, S>,
121 ) -> Self {
122 Self {
123 results: ResultsUI::new(results_config, status_config),
124 input: InputUI::new(input_config),
125 header: DisplayUI::new(header_config),
126 matcher,
127 selector: selections,
128 worker,
129 }
130 }
131
132 pub fn layout(&self, area: Rect) -> [Rect; 4] {
133 let PickerUI {
134 input,
135 header,
136 results,
137 ..
138 } = self;
139
140 let mut constraints = [
141 Constraint::Length(1 + input.config.border.height()), Constraint::Length(results.status_config.show as u16), Constraint::Length(header.height()),
144 Constraint::Fill(1), ];
146
147 if self.reverse() {
148 constraints.reverse();
149 }
150
151 let chunks = Layout::default()
152 .direction(Direction::Vertical)
153 .constraints(constraints)
154 .split(area);
155
156 std::array::from_fn(|i| {
157 chunks[if self.reverse() {
158 chunks.len() - i - 1
159 } else {
160 i
161 }]
162 })
163 }
164}
165
166impl<'a, T: SSS, O: Selection> PickerUI<'a, T, O> {
167 pub fn make_table(&mut self, click: &mut Click) -> (Table<'_>, u16) {
168 let table =
169 self.results
170 .make_table(&mut self.worker, &mut self.selector, self.matcher, click);
171 let width = self.results.table_width();
172 (table, width)
173 }
174
175 pub fn update(&mut self) {
176 self.worker.find(&self.input.input);
177 }
178 pub fn tick(&mut self) {
179 self.worker.find(&self.input.input);
180 }
181
182 pub fn reverse(&self) -> bool {
184 self.results.reverse()
185 }
186}
187
188impl PreviewLayout {
189 pub fn split(&self, area: Rect) -> [Rect; 2] {
190 use crate::config::Side;
191 use ratatui::layout::{Constraint, Direction, Layout};
192
193 let direction = match self.side {
194 Side::Left | Side::Right => Direction::Horizontal,
195 Side::Top | Side::Bottom => Direction::Vertical,
196 };
197
198 let side_first = matches!(self.side, Side::Left | Side::Top);
199
200 let total = if matches!(direction, Direction::Horizontal) {
201 area.width
202 } else {
203 area.height
204 };
205
206 let p = self.percentage.inner();
207
208 let mut side_size = if p != 0 { total * p / 100 } else { 0 };
209
210 let min = if self.min < 0 {
211 total.saturating_sub((-self.min) as u16)
212 } else {
213 self.min as u16
214 };
215
216 let max = if self.max < 0 {
217 total.saturating_sub((-self.max) as u16)
218 } else {
219 self.max as u16
220 };
221
222 side_size = side_size.clamp(min, max);
223
224 let side_constraint = Constraint::Length(side_size);
225
226 let constraints = if side_first {
227 [side_constraint, Constraint::Min(0)]
228 } else {
229 [Constraint::Min(0), side_constraint]
230 };
231
232 let chunks = Layout::default()
233 .direction(direction)
234 .constraints(constraints)
235 .split(area);
236
237 if side_first {
238 [chunks[0], chunks[1]]
239 } else {
240 [chunks[1], chunks[0]]
241 }
242 }
243}