aboutsummaryrefslogtreecommitdiff
path: root/src/ui/panes.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/ui/panes.rs')
-rw-r--r--src/ui/panes.rs252
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,
+ );
+}