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.rs306
1 files changed, 36 insertions, 270 deletions
diff --git a/src/ui/panes.rs b/src/ui/panes.rs
index f20b55b5..cd5ea34c 100644
--- a/src/ui/panes.rs
+++ b/src/ui/panes.rs
@@ -1,8 +1,8 @@
use ratatui::{
+ buffer::Buffer,
layout::Rect,
style::{Color, Modifier, Style},
- text::{Line, Span},
- widgets::{Block, Borders, Clear, Paragraph},
+ widgets::{Block, Borders},
Frame,
};
@@ -12,7 +12,7 @@ use super::text::display_width;
use super::text::truncate_end;
use super::widgets::panel_contrast_fg;
use crate::app::state::Palette;
-use crate::app::{AppState, Mode};
+use crate::app::AppState;
use crate::layout::PaneInfo;
use crate::popup_size::resolve_popup_geometry;
use crate::terminal::{TerminalRuntime, TerminalRuntimeRegistry};
@@ -352,60 +352,13 @@ pub(super) fn render_panes(
return;
};
- let multi_pane = ws.layout.pane_count() > 1;
- let terminal_active = app.mode == Mode::Terminal;
-
for info in pane_infos {
if let Some(rt) = app.runtime_for_pane_in_workspace(terminal_runtimes, ws_idx, info.id) {
let show_cursor = info.is_focused
- && terminal_active
&& !pane_is_scrolled_back(rt)
&& app.pane_exposes_host_cursor(ws_idx, info.id);
rt.render(frame, info.inner_rect, show_cursor);
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)];
- cell.set_style(cell.style().add_modifier(Modifier::DIM));
- }
- }
- }
-
- let (copy_search_top, copy_search_bottom, copy_search_matches) =
- validated_copy_mode_search_matches(app, info, rt);
- render_copy_mode_search_highlights(
- app,
- frame,
- info,
- copy_search_top,
- copy_search_bottom,
- &copy_search_matches,
- false,
- );
- render_selection_highlight(
- &app.selection,
- frame,
- info.id,
- info.inner_rect,
- rt.scroll_metrics(),
- &app.palette,
- app.host_terminal_theme,
- );
- render_copy_mode_search_highlights(
- app,
- frame,
- info,
- copy_search_top,
- copy_search_bottom,
- &copy_search_matches,
- true,
- );
- render_copy_mode_cursor(app, frame, info);
}
}
@@ -443,36 +396,6 @@ pub(super) fn resize_popup_pane(
}
}
-pub(super) fn render_popup_pane(
- app: &AppState,
- terminal_runtimes: &TerminalRuntimeRegistry,
- frame: &mut Frame,
- area: Rect,
-) {
- let Some(popup) = app.popup_pane.as_ref() else {
- return;
- };
- let Some((outer, inner)) = popup_pane_rects(app, area) else {
- return;
- };
- let Some(rt) = terminal_runtimes.get(&popup.terminal_id) else {
- return;
- };
- let title = app
- .terminals
- .get(&popup.terminal_id)
- .and_then(|terminal| terminal.manual_label.as_deref())
- .unwrap_or("popup");
- let block = Block::default()
- .borders(Borders::ALL)
- .border_style(Style::default().fg(app.palette.accent))
- .title(pane_border_title(title, outer.width, true).unwrap_or_default())
- .style(Style::default().bg(app.palette.panel_bg));
- frame.render_widget(Clear, outer);
- frame.render_widget(block, outer);
- rt.render(frame, inner, !pane_is_scrolled_back(rt));
-}
-
#[derive(Clone, Copy, Default)]
struct LineCell {
up: bool,
@@ -724,141 +647,25 @@ fn line_cell_symbol(line: LineCell) -> &'static str {
}
}
-fn render_copy_mode_cursor(app: &AppState, frame: &mut Frame, info: &PaneInfo) {
- if app.mode != Mode::Copy {
- return;
- }
- let Some(copy_mode) = app.copy_mode.as_ref() else {
- return;
- };
- if copy_mode.pane_id != info.id
- || copy_mode.cursor_row >= info.inner_rect.height
- || copy_mode.cursor_col >= info.inner_rect.width
- {
- return;
- }
-
- let x = info.inner_rect.x + copy_mode.cursor_col;
- let y = info.inner_rect.y + copy_mode.cursor_row;
- let cell = &mut frame.buffer_mut()[(x, y)];
- cell.set_style(
- Style::default()
- .fg(panel_contrast_fg(&app.palette))
- .bg(app.palette.accent)
- .add_modifier(Modifier::BOLD),
- );
-}
-
-fn validated_copy_mode_search_matches(
- app: &AppState,
- info: &PaneInfo,
- rt: &crate::terminal::TerminalRuntime,
-) -> (u32, u32, Vec<(usize, crate::pane::TerminalTextMatch)>) {
- let Some(copy_mode) = app.copy_mode.as_ref() else {
- return (0, 0, Vec::new());
- };
- if copy_mode.pane_id != info.id {
- return (0, 0, Vec::new());
- }
- let Some(metrics) = rt.scroll_metrics() else {
- return (0, 0, Vec::new());
- };
- let top = metrics
- .max_offset_from_bottom
- .saturating_sub(metrics.offset_from_bottom)
- .min(u32::MAX as usize) as u32;
- let bottom = top.saturating_add(u32::from(info.inner_rect.height.saturating_sub(1)));
- let first_visible = copy_mode
- .search
- .matches
- .partition_point(|text_match| text_match.end.row < top);
- let visible = &copy_mode.search.matches[first_visible..];
- let visible_len = visible.partition_point(|text_match| text_match.start.row <= bottom);
- let candidates = visible[..visible_len].to_vec();
- let validity = rt.text_matches_are_current(&candidates);
-
- let matches = candidates
- .into_iter()
- .zip(validity)
- .enumerate()
- .filter_map(|(offset, (text_match, is_current))| {
- is_current.then_some((first_visible + offset, text_match))
- })
- .collect();
- (top, bottom, matches)
-}
-
-fn render_copy_mode_search_highlights(
- app: &AppState,
- frame: &mut Frame,
- info: &PaneInfo,
- top: u32,
- bottom: u32,
- matches: &[(usize, crate::pane::TerminalTextMatch)],
- current_only: bool,
-) {
- let Some(copy_mode) = app.copy_mode.as_ref() else {
- return;
- };
- let current = copy_mode.search.current;
- let style = if current_only {
- Style::default()
- .fg(panel_contrast_fg(&app.palette))
- .bg(app.palette.accent)
- .add_modifier(Modifier::BOLD)
- } else {
- Style::default()
- .fg(app.palette.text)
- .bg(app.palette.surface1)
- };
-
- for &(index, text_match) in matches {
- if (current == Some(index)) != current_only {
- continue;
- }
- let start_row = text_match.start.row.max(top);
- let end_row = text_match.end.row.min(bottom);
- for absolute_row in start_row..=end_row {
- let viewport_row = absolute_row.saturating_sub(top) as u16;
- let start_col = if absolute_row == text_match.start.row {
- text_match.start.col
- } else {
- 0
- };
- let end_col = if absolute_row == text_match.end.row {
- text_match.end.col
- } else {
- info.inner_rect.width.saturating_sub(1)
- };
- for col in start_col..=end_col.min(info.inner_rect.width.saturating_sub(1)) {
- let x = info.inner_rect.x.saturating_add(col);
- let y = info.inner_rect.y.saturating_add(viewport_row);
- frame.buffer_mut()[(x, y)].set_style(style);
- }
- }
- }
-}
-
-fn render_selection_highlight(
- selection: &Option<crate::selection::Selection>,
- frame: &mut Frame,
- pane_id: crate::layout::PaneId,
+pub(crate) fn render_selection_highlight<P: PartialEq>(
+ selection: Option<&crate::selection::Selection<P>>,
+ buffer: &mut Buffer,
+ pane_id: &P,
inner: Rect,
scroll_metrics: Option<crate::pane::ScrollMetrics>,
p: &Palette,
host_theme: crate::terminal_theme::TerminalTheme,
) {
- if let Some(sel) = selection {
- if sel.is_visible() && sel.pane_id == pane_id {
- let buf = frame.buffer_mut();
- let style = automatic_selection_style(p, host_theme);
- 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);
- }
- }
+ let Some(selection) =
+ selection.filter(|selection| selection.is_visible() && &selection.pane_id == pane_id)
+ else {
+ return;
+ };
+ let style = automatic_selection_style(p, host_theme);
+ for y in 0..inner.height {
+ for x in 0..inner.width {
+ if selection.contains(y, x, scroll_metrics) {
+ buffer[(inner.x + x, inner.y + y)].set_style(style);
}
}
}
@@ -959,47 +766,6 @@ fn color_to_rgb(color: Color) -> Option<Rgb> {
}
}
-pub(super) 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(
- app.keybinds
- .new_workspace
- .label()
- .unwrap_or_else(|| "unset".to_string()),
- 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,
- );
-}
-
#[cfg(test)]
mod tests {
use super::*;
@@ -1009,14 +775,13 @@ mod tests {
use crate::terminal::TerminalState;
use crate::workspace::Workspace;
- fn render_view_pane_borders(app: &AppState, ws: &Workspace, frame: &mut Frame) {
- render_pane_borders(
- app,
- ws,
- &app.view.pane_infos,
- &app.view.split_borders,
- frame,
- );
+ fn render_view_pane_borders(
+ app: &AppState,
+ ws: &Workspace,
+ split_borders: &[crate::layout::SplitBorder],
+ frame: &mut Frame,
+ ) {
+ render_pane_borders(app, ws, &app.view.pane_infos, split_borders, frame);
}
#[test]
@@ -1052,7 +817,6 @@ mod tests {
#[test]
fn pane_border_renderer_places_adjacent_cjk_by_display_width() {
let mut app = AppState::test_new();
- app.mode = Mode::Terminal;
app.view.terminal_area = Rect::new(0, 0, 12, 3);
let ws = Workspace::test_new("test");
let pane_id = ws.tabs[0].root_pane;
@@ -1073,7 +837,7 @@ mod tests {
let mut terminal =
ratatui::Terminal::new(ratatui::backend::TestBackend::new(12, 3)).unwrap();
terminal
- .draw(|frame| render_view_pane_borders(&app, &ws, frame))
+ .draw(|frame| render_view_pane_borders(&app, &ws, &[], frame))
.unwrap();
let buffer = terminal.backend().buffer();
@@ -1208,7 +972,6 @@ mod tests {
#[test]
fn global_pane_border_renderer_composes_junctions_and_focus_style() {
let mut app = AppState::test_new();
- app.mode = Mode::Terminal;
app.view.terminal_area = Rect::new(0, 0, 4, 4);
app.view.pane_infos = vec![
PaneInfo {
@@ -1244,7 +1007,7 @@ mod tests {
is_focused: false,
},
];
- app.view.split_borders = vec![
+ let split_borders = vec![
crate::layout::SplitBorder {
pos: 2,
direction: ratatui::layout::Direction::Horizontal,
@@ -1265,7 +1028,7 @@ mod tests {
ratatui::Terminal::new(ratatui::backend::TestBackend::new(4, 4)).unwrap();
terminal
- .draw(|frame| render_view_pane_borders(&app, &ws, frame))
+ .draw(|frame| render_view_pane_borders(&app, &ws, &split_borders, frame))
.unwrap();
let buffer = terminal.backend().buffer();
@@ -1278,7 +1041,6 @@ mod tests {
#[test]
fn gapped_pane_focus_does_not_color_neighbor_border() {
let mut app = AppState::test_new();
- app.mode = Mode::Terminal;
app.pane_gaps = true;
app.view.terminal_area = Rect::new(0, 0, 4, 3);
app.view.pane_infos = vec![
@@ -1304,7 +1066,7 @@ mod tests {
ratatui::Terminal::new(ratatui::backend::TestBackend::new(4, 3)).unwrap();
terminal
- .draw(|frame| render_view_pane_borders(&app, &ws, frame))
+ .draw(|frame| render_view_pane_borders(&app, &ws, &[], frame))
.unwrap();
let buffer = terminal.backend().buffer();
@@ -1532,7 +1294,11 @@ mod tests {
..Default::default()
};
let expected_style = automatic_selection_style(&palette, host_theme);
- let selection = Some(Selection::range(PaneId::from_raw(1), 0, 0, 2, None));
+ let selection = Some(Selection::absolute_range(
+ PaneId::from_raw(1),
+ (0, 0),
+ (0, 2),
+ ));
let backend = ratatui::backend::TestBackend::new(4, 1);
let mut terminal = ratatui::Terminal::new(backend).unwrap();
@@ -1552,9 +1318,9 @@ mod tests {
);
buf[(2, 0)].set_style(Style::default().fg(Color::Blue).bg(Color::Reset));
render_selection_highlight(
- &selection,
- frame,
- PaneId::from_raw(1),
+ selection.as_ref(),
+ frame.buffer_mut(),
+ &PaneId::from_raw(1),
Rect::new(0, 0, 4, 1),
None,
&palette,