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