1mod header;
4mod layout;
5mod list;
6mod overlays;
7mod right_panel;
8mod status;
9mod types;
10
11pub use layout::tabs_rect_for_area;
12pub use types::{Focus, Tab};
13
14use crate::analyzer::AnalyzedItem;
15use crate::analyzer::CrateInfo;
16use crate::crates_io::CrateDocInfo;
17use crate::ui::animation::AnimationState;
18use crate::ui::components::TabBar;
19use crate::ui::search::{CompletionCandidate, SearchBar, SearchCompletion};
20use crate::ui::theme::Theme;
21
22use ratatui::{
23 buffer::Buffer,
24 layout::{Constraint, Direction, Layout, Rect},
25 style::Style,
26 widgets::{block::BorderType, Block, Borders, Widget},
27};
28
29pub struct OracleUi<'a> {
31 pub(super) items: &'a [AnalyzedItem],
33 pub(super) all_items_impl_lookup: Option<&'a [AnalyzedItem]>,
34 pub(super) filtered_items: &'a [&'a AnalyzedItem],
35 pub(super) candidates: &'a [CompletionCandidate],
36 pub(super) crate_info: Option<&'a CrateInfo>,
37 pub(super) dependency_tree: &'a [(String, usize)],
38 pub(super) filtered_dependency_indices: &'a [usize],
39 pub(super) crate_doc: Option<&'a CrateDocInfo>,
40 pub(super) crate_doc_loading: bool,
41 pub(super) crate_doc_failed: bool,
42 pub(super) selected_installed_crate: Option<&'a crate::analyzer::InstalledCrate>,
43 pub(super) installed_crate_items: &'a [&'a AnalyzedItem],
44 pub(super) target_size_bytes: Option<u64>,
45 pub(super) search_input: &'a str,
47 pub(super) current_tab: Tab,
48 pub(super) focus: Focus,
49 pub(super) list_selected: Option<usize>,
50 pub(super) selected_item: Option<&'a AnalyzedItem>,
51 pub(super) completion_selected: usize,
52 pub(super) show_completion: bool,
53 pub(super) show_help: bool,
54 pub(super) show_settings: bool,
55 pub(super) status_message: &'a str,
56 pub(super) inspector_scroll: usize,
57 pub(super) animation: Option<&'a AnimationState>,
58 pub(super) theme: &'a Theme,
59 pub(super) show_copilot_chat: bool,
61 pub(super) copilot_chat_messages: &'a [(String, String)],
62 pub(super) copilot_chat_input: &'a str,
63 pub(super) copilot_chat_loading: bool,
64 pub(super) copilot_chat_scroll: usize,
65}
66
67impl<'a> OracleUi<'a> {
68 pub fn new(theme: &'a Theme) -> Self {
69 Self {
70 items: &[],
71 all_items_impl_lookup: None,
72 filtered_items: &[],
73 candidates: &[],
74 crate_info: None,
75 dependency_tree: &[],
76 filtered_dependency_indices: &[],
77 crate_doc: None,
78 crate_doc_loading: false,
79 crate_doc_failed: false,
80 selected_installed_crate: None,
81 installed_crate_items: &[],
82 target_size_bytes: None,
83 search_input: "",
84 current_tab: Tab::default(),
85 focus: Focus::default(),
86 list_selected: None,
87 selected_item: None,
88 completion_selected: 0,
89 show_completion: false,
90 show_help: false,
91 show_settings: false,
92 status_message: "",
93 inspector_scroll: 0,
94 animation: None,
95 theme,
96 show_copilot_chat: false,
97 copilot_chat_messages: &[],
98 copilot_chat_input: "",
99 copilot_chat_loading: false,
100 copilot_chat_scroll: 0,
101 }
102 }
103
104 #[must_use]
105 pub fn items(mut self, items: &'a [AnalyzedItem]) -> Self {
106 self.items = items;
107 self
108 }
109 #[must_use]
110 pub fn all_items_impl_lookup(mut self, items: Option<&'a [AnalyzedItem]>) -> Self {
111 self.all_items_impl_lookup = items;
112 self
113 }
114 #[must_use]
115 pub fn filtered_items(mut self, items: &'a [&'a AnalyzedItem]) -> Self {
116 self.filtered_items = items;
117 self
118 }
119 #[must_use]
120 pub fn selected_installed_crate(
121 mut self,
122 crate_info: Option<&'a crate::analyzer::InstalledCrate>,
123 ) -> Self {
124 self.selected_installed_crate = crate_info;
125 self
126 }
127 #[must_use]
128 pub fn installed_crate_items(mut self, items: &'a [&'a AnalyzedItem]) -> Self {
129 self.installed_crate_items = items;
130 self
131 }
132 #[must_use]
133 pub fn target_size_bytes(mut self, bytes: Option<u64>) -> Self {
134 self.target_size_bytes = bytes;
135 self
136 }
137 #[must_use]
138 pub fn list_selected(mut self, selected: Option<usize>) -> Self {
139 self.list_selected = selected;
140 self
141 }
142 #[must_use]
143 pub fn candidates(mut self, candidates: &'a [CompletionCandidate]) -> Self {
144 self.candidates = candidates;
145 self
146 }
147 #[must_use]
148 pub fn crate_info(mut self, info: Option<&'a CrateInfo>) -> Self {
149 self.crate_info = info;
150 self
151 }
152 #[must_use]
153 pub fn dependency_tree(mut self, tree: &'a [(String, usize)]) -> Self {
154 self.dependency_tree = tree;
155 self
156 }
157 #[must_use]
158 pub fn filtered_dependency_indices(mut self, indices: &'a [usize]) -> Self {
159 self.filtered_dependency_indices = indices;
160 self
161 }
162 #[must_use]
163 pub fn crate_doc(mut self, doc: Option<&'a CrateDocInfo>) -> Self {
164 self.crate_doc = doc;
165 self
166 }
167 #[must_use]
168 pub fn crate_doc_loading(mut self, loading: bool) -> Self {
169 self.crate_doc_loading = loading;
170 self
171 }
172 #[must_use]
173 pub fn crate_doc_failed(mut self, failed: bool) -> Self {
174 self.crate_doc_failed = failed;
175 self
176 }
177 #[must_use]
178 pub fn search_input(mut self, input: &'a str) -> Self {
179 self.search_input = input;
180 self
181 }
182 #[must_use]
183 pub fn current_tab(mut self, tab: Tab) -> Self {
184 self.current_tab = tab;
185 self
186 }
187 #[must_use]
188 pub fn focus(mut self, focus: Focus) -> Self {
189 self.focus = focus;
190 self
191 }
192 #[must_use]
193 pub fn selected_item(mut self, item: Option<&'a AnalyzedItem>) -> Self {
194 self.selected_item = item;
195 self
196 }
197 #[must_use]
198 pub fn completion_selected(mut self, index: usize) -> Self {
199 self.completion_selected = index;
200 self
201 }
202 #[must_use]
203 pub fn show_completion(mut self, show: bool) -> Self {
204 self.show_completion = show;
205 self
206 }
207 #[must_use]
208 pub fn show_help(mut self, show: bool) -> Self {
209 self.show_help = show;
210 self
211 }
212 #[must_use]
213 pub fn show_settings(mut self, show: bool) -> Self {
214 self.show_settings = show;
215 self
216 }
217 #[must_use]
218 pub fn status_message(mut self, msg: &'a str) -> Self {
219 self.status_message = msg;
220 self
221 }
222 #[must_use]
223 pub fn inspector_scroll(mut self, scroll: usize) -> Self {
224 self.inspector_scroll = scroll;
225 self
226 }
227 #[must_use]
228 pub fn animation_state(mut self, animation: &'a AnimationState) -> Self {
229 self.animation = Some(animation);
230 self
231 }
232 #[must_use]
233 pub fn show_copilot_chat(mut self, show: bool) -> Self {
234 self.show_copilot_chat = show;
235 self
236 }
237 #[must_use]
238 pub fn copilot_chat_messages(mut self, messages: &'a [(String, String)]) -> Self {
239 self.copilot_chat_messages = messages;
240 self
241 }
242 #[must_use]
243 pub fn copilot_chat_input(mut self, input: &'a str) -> Self {
244 self.copilot_chat_input = input;
245 self
246 }
247 #[must_use]
248 pub fn copilot_chat_loading(mut self, loading: bool) -> Self {
249 self.copilot_chat_loading = loading;
250 self
251 }
252 #[must_use]
253 pub fn copilot_chat_scroll(mut self, scroll: usize) -> Self {
254 self.copilot_chat_scroll = scroll;
255 self
256 }
257
258 fn render_search(&self, area: Rect, buf: &mut Buffer) {
259 let placeholder = match self.current_tab {
260 Tab::Types => "Search types... (struct, enum, type)",
261 Tab::Functions => "Search functions...",
262 Tab::Modules => "Search modules...",
263 Tab::Crates => {
264 if self.selected_installed_crate.is_some() {
265 "Filter items... (e.g., de::Deserialize)"
266 } else {
267 "Search crates... (filter by name)"
268 }
269 }
270 };
271 let search = SearchBar::new(self.search_input, self.theme)
272 .focused(self.focus == Focus::Search)
273 .placeholder(placeholder);
274 search.render(area, buf);
275 }
276
277 fn render_completion(&self, search_area: Rect, buf: &mut Buffer) {
278 if !self.show_completion || self.candidates.is_empty() {
279 return;
280 }
281 let max_height = 12.min(self.candidates.len() as u16 + 2);
282 let dropdown_area = Rect {
283 x: search_area.x + 2,
284 y: search_area.y + search_area.height,
285 width: search_area.width.saturating_sub(4).min(60),
286 height: max_height,
287 };
288 let completion = SearchCompletion::new(self.candidates, self.theme)
289 .selected(self.completion_selected)
290 .filter(self.search_input)
291 .max_visible(10);
292 completion.render(dropdown_area, buf);
293 }
294
295 fn render_tabs(&self, area: Rect, buf: &mut Buffer) {
296 let titles: Vec<&str> = Tab::all().iter().map(|t| t.title()).collect();
297 let tab_bar = TabBar::new(titles, self.theme)
298 .select(self.current_tab.index())
299 .focused(self.focus == Focus::Inspector);
300 tab_bar.render(area, buf);
301 }
302}
303
304impl Widget for OracleUi<'_> {
305 fn render(self, area: Rect, buf: &mut Buffer) {
306 use layout::{BODY_MARGIN, HEADER_HEIGHT, STATUS_HEIGHT};
307
308 let outer = Block::default()
309 .borders(Borders::ALL)
310 .border_type(BorderType::Rounded)
311 .border_style(self.theme.style_border_glow())
312 .style(Style::default().bg(self.theme.bg));
313 let content_area = outer.inner(area);
314 outer.render(area, buf);
315
316 let padded = Rect {
317 x: content_area.x + BODY_MARGIN,
318 y: content_area.y + BODY_MARGIN,
319 width: content_area.width.saturating_sub(2 * BODY_MARGIN),
320 height: content_area.height.saturating_sub(2 * BODY_MARGIN),
321 };
322 let chunks = Layout::default()
323 .direction(Direction::Vertical)
324 .constraints([
325 Constraint::Length(HEADER_HEIGHT),
326 Constraint::Min(12),
327 Constraint::Length(STATUS_HEIGHT),
328 ])
329 .split(padded);
330
331 self.render_header(chunks[0], buf);
332
333 let body = chunks[1];
334 let left_div_right = Layout::default()
335 .direction(Direction::Horizontal)
336 .constraints([
337 Constraint::Ratio(1, 3),
338 Constraint::Length(1),
339 Constraint::Ratio(2, 3),
340 ])
341 .split(body);
342 let left_column = left_div_right[0];
343 let div_rect = left_div_right[1];
344 let right_column = left_div_right[2];
345
346 let left_split = Layout::default()
347 .direction(Direction::Vertical)
348 .constraints([Constraint::Length(3), Constraint::Min(6)])
349 .split(left_column);
350 let search_rect = left_split[0];
351 let list_rect = left_split[1];
352
353 let right_split = Layout::default()
354 .direction(Direction::Vertical)
355 .constraints([Constraint::Length(3), Constraint::Min(6)])
356 .split(right_column);
357 let tabs_rect = right_split[0];
358 let right_content = right_split[1];
359
360 let (inspector_rect, chat_rect) = if self.show_copilot_chat {
361 let horz = Layout::default()
362 .direction(Direction::Horizontal)
363 .constraints([Constraint::Percentage(55), Constraint::Percentage(45)])
364 .split(right_content);
365 (horz[0], horz[1])
366 } else {
367 (right_content, right_content) };
369
370 self.render_search(search_rect, buf);
371 self.render_list(list_rect, buf);
372 self.render_vertical_divider(div_rect, buf);
373 self.render_tabs(tabs_rect, buf);
374 self.render_inspector(inspector_rect, buf);
375 if self.show_copilot_chat {
376 self.render_copilot_chat(chat_rect, buf);
377 }
378 self.render_status(chunks[2], buf);
379 self.render_completion(search_rect, buf);
380 self.render_settings_overlay(area, buf);
381 self.render_help_overlay(area, buf);
382 }
383}