1use crate::{Theme, render_themed};
15use ratatui::{
16 Frame,
17 layout::{Alignment, Constraint, Direction, Layout, Rect},
18 style::{Color, Modifier, Style},
19 text::{Line, Span},
20 widgets::{Block, BorderType, Borders, Clear, List, ListItem, ListState, Paragraph, Wrap},
21};
22
23use crate::app::{App, CopyProgress, Modal, Snackbar};
24use crate::inline_editor::render_inline_editor;
25use crate::preview::render_preview;
26use tui_slider::{Slider, SliderOrientation, SliderState, style::SliderStyle};
27
28fn key_span<'a>(s: &'a str, theme: &Theme) -> Span<'a> {
32 Span::styled(
33 s,
34 Style::default()
35 .fg(theme.accent)
36 .add_modifier(Modifier::BOLD),
37 )
38}
39
40fn dim_span<'a>(s: &'a str, theme: &Theme) -> Span<'a> {
42 Span::styled(s, Style::default().fg(theme.dim))
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
49pub enum HintLayout {
50 #[default]
52 Auto,
53 Horizontal,
55 Vertical,
57}
58
59impl HintLayout {
60 #[must_use]
62 pub fn resolve(self, width: u16) -> Self {
63 match self {
64 Self::Auto if width < 90 => Self::Vertical,
65 Self::Auto => Self::Horizontal,
66 layout => layout,
67 }
68 }
69
70 #[must_use]
72 pub fn action_bar_rows(self, width: u16) -> u16 {
73 match self.resolve(width) {
74 Self::Horizontal => 6,
75 Self::Vertical => 12,
76 Self::Auto => unreachable!(),
77 }
78 }
79
80 #[must_use]
82 pub fn row_height(self, width: u16) -> u16 {
83 self.action_bar_rows(width) / 2
84 }
85}
86
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89pub struct AppViewOptions {
90 pub show_action_bar: bool,
92 pub show_debug_panel: bool,
94 pub theme_panel_width: u16,
96 pub settings_panel_width: u16,
98 pub panes_with_preview_percent: u16,
100 pub minimum_pane_width: u16,
103 pub hint_layout: HintLayout,
105}
106
107impl Default for AppViewOptions {
108 fn default() -> Self {
109 Self {
110 show_action_bar: true,
111 show_debug_panel: true,
112 theme_panel_width: 32,
113 settings_panel_width: 42,
114 panes_with_preview_percent: 50,
115 minimum_pane_width: 24,
116 hint_layout: HintLayout::Auto,
117 }
118 }
119}
120
121pub fn draw(app: &mut App, frame: &mut Frame) {
123 draw_in(app, frame, frame.area());
124}
125
126pub fn draw_in(app: &mut App, frame: &mut Frame, area: Rect) {
130 draw_in_with_options(app, frame, area, AppViewOptions::default());
131}
132
133pub fn draw_in_with_options(app: &mut App, frame: &mut Frame, area: Rect, options: AppViewOptions) {
135 if area.is_empty() {
136 return;
137 }
138 if app.panes.is_empty() {
139 return;
140 }
141 app.poll_background();
143 let theme = *app.theme();
144 let full = area;
145
146 if theme.bg != Color::Reset {
150 frame.render_widget(Block::default().style(Style::default().bg(theme.bg)), full);
151 }
152
153 if let Some(ref editor) = app.inline_editor {
155 render_inline_editor(frame, full, editor, &theme);
156 return;
157 }
158
159 let debug_height = if full.height >= 30 {
162 10
163 } else if full.height >= 20 {
164 6
165 } else {
166 3
167 };
168
169 let show_debug = app.verbose && options.show_debug_panel;
170 let hint_layout = options.hint_layout.resolve(full.width);
171 let action_bar_height = hint_layout.action_bar_rows(full.width);
172 let action_row_height = hint_layout.row_height(full.width);
173 let mut vertical_constraints = vec![Constraint::Min(0)];
174 if show_debug {
175 vertical_constraints.push(Constraint::Length(debug_height));
176 }
177 if options.show_action_bar {
178 vertical_constraints.push(Constraint::Length(action_bar_height));
179 }
180 let v_chunks = Layout::default()
181 .direction(Direction::Vertical)
182 .constraints(vertical_constraints)
183 .split(full);
184
185 let main_area = v_chunks[0];
186 let action_area = options
187 .show_action_bar
188 .then(|| v_chunks[v_chunks.len() - 1]);
189
190 let action_rows = action_area.map(|area| {
194 Layout::default()
195 .direction(Direction::Vertical)
196 .constraints([
197 Constraint::Length(action_row_height),
198 Constraint::Length(action_row_height),
199 ])
200 .split(area)
201 });
202
203 if show_debug {
205 render_debug_panel(frame, v_chunks[1], app, &theme);
206 }
207
208 let pane_capacity = (main_area.width / options.minimum_pane_width.max(1)).max(1) as usize;
210 let visible_pane_count = if app.single_pane {
211 1
212 } else {
213 app.panes.len().min(pane_capacity)
214 };
215 let visible_start = if app.single_pane {
216 app.active_idx
217 } else {
218 app.active_idx
219 .saturating_sub(visible_pane_count.saturating_sub(1))
220 .min(app.panes.len().saturating_sub(visible_pane_count))
221 };
222
223 let mut h_constraints = vec![];
224 if app.show_preview {
225 if app.single_pane {
228 h_constraints.push(Constraint::Percentage(40));
229 } else {
230 let pct = (options.panes_with_preview_percent.min(100)
231 / crate::to_u16(visible_pane_count.max(1)))
232 .max(1);
233 for _ in 0..visible_pane_count {
234 h_constraints.push(Constraint::Percentage(pct));
235 }
236 }
237 h_constraints.push(Constraint::Min(0)); } else {
239 let pct = crate::to_u16(100 / visible_pane_count.max(1));
240 for _ in 0..visible_pane_count {
241 h_constraints.push(Constraint::Percentage(pct));
242 }
243 }
244 if app.show_theme_panel {
245 h_constraints.push(Constraint::Length(options.theme_panel_width));
246 }
247 if app.show_options_panel {
248 h_constraints.push(Constraint::Length(options.settings_panel_width));
249 }
250 if app.show_editor_panel {
251 h_constraints.push(Constraint::Length(options.settings_panel_width));
252 }
253 let h_chunks = Layout::default()
254 .direction(Direction::Horizontal)
255 .constraints(h_constraints)
256 .split(main_area);
257
258 let active_theme = theme;
260 let inactive_theme = theme.accent(theme.dim).brand(theme.dim);
261
262 let theme_changed = app.panes[0].theme_name != app.theme_name();
266 if theme_changed {
267 let theme_name = app.theme_name().to_string();
268 for p in &mut app.panes {
269 p.theme_name = theme_name.clone();
270 }
271 }
272
273 let editor_label = if app.editor == crate::app::Editor::None {
274 ""
275 } else {
276 app.editor.label()
277 };
278 if app.panes[0].editor_name != editor_label {
279 let editor_name = editor_label.to_string();
280 for p in &mut app.panes {
281 p.editor_name = editor_name.clone();
282 }
283 }
284
285 let active_idx = app.active_idx;
286 if app.single_pane {
287 render_themed(
288 &mut app.panes[active_idx],
289 frame,
290 h_chunks[0],
291 &active_theme,
292 );
293 } else {
294 for (slot, pane_index) in (visible_start..visible_start + visible_pane_count).enumerate() {
295 let pane_theme = if pane_index == active_idx {
296 &active_theme
297 } else {
298 &inactive_theme
299 };
300 render_themed(
301 &mut app.panes[pane_index],
302 frame,
303 h_chunks[slot],
304 pane_theme,
305 );
306 }
307 }
308
309 if app.show_preview {
311 let preview_idx = visible_pane_count;
312 let preview_area = h_chunks[preview_idx];
313
314 let current_path = app.active_pane().current_entry().map(|e| e.path.clone());
316 app.preview_state.update(
317 current_path.as_deref(),
318 preview_area.width,
319 preview_area.height,
320 );
321
322 render_preview(frame, preview_area, &app.preview_state, &theme);
323 }
324
325 if app.show_theme_panel {
327 let panel_area = h_chunks[h_chunks.len() - 1];
328 render_theme_panel(frame, panel_area, app);
329 }
330
331 if app.show_options_panel {
333 let panel_area = h_chunks[h_chunks.len() - 1];
334 render_options_panel(frame, panel_area, app);
335 }
336
337 if app.show_editor_panel {
339 let panel_area = h_chunks[h_chunks.len() - 1];
340 render_editor_panel(frame, panel_area, app);
341 }
342
343 if let Some(rows) = action_rows {
345 render_nav_hints_with_layout(frame, rows[0], rows[1], app, &theme, hint_layout);
346 }
347
348 if let Some(picker) = &app.picker {
350 crate::picker::render_picker(frame, full, picker, &theme);
351 }
352
353 if let Some(modal) = &app.modal {
355 render_modal_for(frame, full, modal, &theme, app.use_trash);
356 }
357
358 if let Some(progress) = &app.copy_progress {
360 render_copy_progress(frame, full, progress, &theme);
361 }
362
363 if app
366 .snackbar
367 .as_ref()
368 .is_some_and(super::app::Snackbar::is_expired)
369 {
370 app.snackbar = None;
371 }
372 if let Some(snackbar) = &app.snackbar {
373 render_snackbar(frame, full, snackbar, &theme);
374 }
375}
376
377mod action_bar;
378mod modal;
379mod overlays;
380mod panels;
381
382pub use self::action_bar::{render_action_bar, render_nav_hints, render_nav_hints_with_layout};
383#[cfg(test)]
384pub use self::action_bar::{render_action_bar_spans, render_nav_hints_spans};
385pub use self::modal::{render_modal, render_modal_for};
386pub use self::overlays::{render_copy_progress, render_debug_panel, render_snackbar};
387pub use self::panels::{render_editor_panel, render_options_panel, render_theme_panel};
388
389#[cfg(test)]
390#[path = "tests.rs"]
391mod tests;