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