diff options
Diffstat (limited to 'src/ui/panes.rs')
| -rw-r--r-- | src/ui/panes.rs | 252 |
1 files changed, 252 insertions, 0 deletions
diff --git a/src/ui/panes.rs b/src/ui/panes.rs new file mode 100644 index 00000000..7e1490db --- /dev/null +++ b/src/ui/panes.rs @@ -0,0 +1,252 @@ +use ratatui::{ + layout::Rect, + style::{Color, Modifier, Style}, + text::{Line, Span}, + widgets::{Block, Borders, Paragraph}, + Frame, +}; + +use super::scrollbar::{render_pane_scrollbar, should_show_scrollbar}; +use super::widgets::panel_contrast_fg; +use crate::app::state::Palette; +use crate::app::{AppState, Mode}; +use crate::layout::PaneInfo; + +/// Compute pane layout info and optionally resize pane runtimes to match. +pub(super) fn compute_pane_infos(app: &AppState, area: Rect, resize_panes: bool) -> Vec<PaneInfo> { + let Some(ws_idx) = app.active else { + return Vec::new(); + }; + let Some(ws) = app.workspaces.get(ws_idx) else { + return Vec::new(); + }; + + let multi_pane = ws.layout.pane_count() > 1; + let terminal_active = app.mode == Mode::Terminal; + + if ws.zoomed { + let focused_id = ws.layout.focused(); + let inner_rect = area; + let mut scrollbar_rect = None; + if let Some(rt) = ws.runtimes.get(&focused_id) { + if rt + .scroll_metrics() + .is_some_and(|metrics| should_show_scrollbar(metrics) && area.width > 0) + { + scrollbar_rect = Some(Rect::new( + area.x + area.width.saturating_sub(1), + area.y, + 1, + area.height, + )); + } + if resize_panes { + rt.resize(inner_rect.height, inner_rect.width); + } + } + return vec![PaneInfo { + id: focused_id, + rect: area, + inner_rect, + scrollbar_rect, + is_focused: true, + }]; + } + + let mut pane_infos = ws.layout.panes(area); + + for info in &mut pane_infos { + let pane_inner = if multi_pane { + let border_set = if info.is_focused && terminal_active { + ratatui::symbols::border::THICK + } else { + ratatui::symbols::border::PLAIN + }; + let block = Block::default() + .borders(Borders::ALL) + .border_set(border_set); + block.inner(info.rect) + } else { + area + }; + + let inner_rect = pane_inner; + let mut scrollbar_rect = None; + if let Some(rt) = ws.runtimes.get(&info.id) { + if rt + .scroll_metrics() + .is_some_and(|metrics| should_show_scrollbar(metrics) && pane_inner.width > 0) + { + scrollbar_rect = Some(Rect::new( + pane_inner.x + pane_inner.width.saturating_sub(1), + pane_inner.y, + 1, + pane_inner.height, + )); + } + if resize_panes { + rt.resize(inner_rect.height, inner_rect.width); + } + } + + info.inner_rect = inner_rect; + info.scrollbar_rect = scrollbar_rect; + } + + pane_infos +} + +pub(super) fn render_panes(app: &AppState, frame: &mut Frame, area: Rect) { + let Some(ws_idx) = app.active else { + render_empty(app, frame, area); + return; + }; + let Some(ws) = app.workspaces.get(ws_idx) else { + render_empty(app, frame, area); + return; + }; + + let multi_pane = ws.layout.pane_count() > 1; + let terminal_active = app.mode == Mode::Terminal; + + for info in &app.view.pane_infos { + if let Some(rt) = ws.runtimes.get(&info.id) { + if multi_pane { + let (border_style, border_set) = if info.is_focused && terminal_active { + ( + Style::default().fg(app.palette.accent), + ratatui::symbols::border::THICK, + ) + } else if info.is_focused { + ( + Style::default().fg(app.palette.accent), + ratatui::symbols::border::PLAIN, + ) + } else { + ( + Style::default().fg(app.palette.overlay0), + ratatui::symbols::border::PLAIN, + ) + }; + + let block = Block::default() + .borders(Borders::ALL) + .border_style(border_style) + .border_set(border_set); + frame.render_widget(block, info.rect); + } + + rt.render(frame, info.inner_rect, info.is_focused && terminal_active); + render_pane_scrollbar(app, frame, info, rt); + + let should_dim = !info.is_focused && multi_pane && !terminal_active; + if should_dim { + let inner = info.inner_rect; + let buf = frame.buffer_mut(); + for y in inner.y..inner.y + inner.height { + for x in inner.x..inner.x + inner.width { + let cell = &mut buf[(x, y)]; + let style = cell.style(); + let fg = style.fg.unwrap_or(Color::White); + let dimmed_fg = dim_color(fg); + cell.set_style(style.fg(dimmed_fg)); + } + } + } + + render_selection_highlight( + &app.selection, + frame, + info.id, + info.inner_rect, + rt.scroll_metrics(), + &app.palette, + ); + } + } +} + +/// Render selection highlight for a pane by inverting fg/bg colors. +/// Reduce a color's brightness by blending it toward black. +fn dim_color(color: Color) -> Color { + match color { + Color::Rgb(r, g, b) => Color::Rgb(r / 3, g / 3, b / 3), + Color::White => Color::DarkGray, + Color::Gray => Color::DarkGray, + Color::DarkGray => Color::Rgb(30, 30, 30), + Color::Red => Color::Rgb(60, 0, 0), + Color::Green => Color::Rgb(0, 60, 0), + Color::Yellow => Color::Rgb(60, 60, 0), + Color::Blue => Color::Rgb(0, 0, 60), + Color::Magenta => Color::Rgb(60, 0, 60), + Color::Cyan => Color::Rgb(0, 60, 60), + Color::LightRed => Color::Rgb(80, 30, 30), + Color::LightGreen => Color::Rgb(30, 80, 30), + Color::LightYellow => Color::Rgb(80, 80, 30), + Color::LightBlue => Color::Rgb(30, 30, 80), + Color::LightMagenta => Color::Rgb(80, 30, 80), + Color::LightCyan => Color::Rgb(30, 80, 80), + _ => Color::DarkGray, + } +} + +fn render_selection_highlight( + selection: &Option<crate::selection::Selection>, + frame: &mut Frame, + pane_id: crate::layout::PaneId, + inner: Rect, + scroll_metrics: Option<crate::pane::ScrollMetrics>, + p: &Palette, +) { + if let Some(sel) = selection { + if sel.is_visible() && sel.pane_id == pane_id { + let buf = frame.buffer_mut(); + for y in 0..inner.height { + for x in 0..inner.width { + if sel.contains(y, x, scroll_metrics) { + let cell = &mut buf[(inner.x + x, inner.y + y)]; + cell.set_style(Style::default().fg(panel_contrast_fg(p)).bg(p.blue)); + } + } + } + } + } +} + +fn render_empty(app: &AppState, frame: &mut Frame, area: Rect) { + let p = &app.palette; + let lines = vec![ + Line::from(""), + Line::from(""), + Line::from(Span::styled( + " No workspaces yet", + Style::default().fg(p.overlay0), + )), + Line::from(""), + Line::from(Span::styled( + " A workspace is one project context.", + Style::default().fg(p.overlay1), + )), + Line::from(Span::styled( + " Its root pane (top-left) sets the default repo or folder name.", + Style::default().fg(p.overlay1), + )), + Line::from(""), + Line::from(vec![ + Span::styled(" Press ", Style::default().fg(p.overlay0)), + Span::styled( + format!("{}", app.keybinds.new_workspace_label), + Style::default().fg(p.accent).add_modifier(Modifier::BOLD), + ), + Span::styled(" to create one", Style::default().fg(p.overlay0)), + ]), + ]; + frame.render_widget( + Paragraph::new(lines).block( + Block::default() + .borders(Borders::ALL) + .border_style(Style::default().fg(p.surface_dim)), + ), + area, + ); +} |
