matchmaker/ui/
mod.rs

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