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