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