diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-09-02 14:24:33 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-09-02 14:24:33 +0300 |
| commit | 6c0bb273d5d5405a00985621b17e36f8b4d64609 (patch) | |
| tree | ceef5ea8adf337bf3c1e977b133e76cb763c6f1b /src/ui | |
| parent | cc88b3b8e5bb9f7d9f23ed6ae85a52fd7b5b9ed6 (diff) | |
feat: support independent multi-client tab views (#3526)
Diffstat (limited to 'src/ui')
| -rw-r--r-- | src/ui/panes.rs | 76 | ||||
| -rw-r--r-- | src/ui/tab_surface.rs | 89 |
2 files changed, 138 insertions, 27 deletions
diff --git a/src/ui/panes.rs b/src/ui/panes.rs index cd5ea34c..e31ee0ad 100644 --- a/src/ui/panes.rs +++ b/src/ui/panes.rs @@ -158,13 +158,21 @@ pub(crate) fn apply_pane_chrome( } fn runtime_for_tab_pane<'a>( + _app: &'a AppState, terminal_runtimes: &'a TerminalRuntimeRegistry, + _workspace_index: usize, tab: &'a crate::workspace::Tab, pane_id: crate::layout::PaneId, ) -> Option<(&'a crate::terminal::TerminalId, &'a TerminalRuntime)> { let terminal_id = tab.terminal_id(pane_id)?; #[cfg(test)] - if let Some(runtime) = tab.runtimes.get(&pane_id) { + if let Some(runtime) = _app + .workspaces + .get(_workspace_index)? + .test_runtimes + .get(&pane_id) + .or_else(|| tab.runtimes.get(&pane_id)) + { return Some((terminal_id, runtime)); } terminal_runtimes @@ -199,6 +207,7 @@ fn stable_scrollbar_gutter( pub(super) fn resize_tab_panes( app: &AppState, terminal_runtimes: &TerminalRuntimeRegistry, + workspace_index: usize, tab: &crate::workspace::Tab, area: Rect, cell_size: crate::kitty_graphics::HostCellSize, @@ -207,7 +216,9 @@ pub(super) fn resize_tab_panes( if tab.zoomed { let focused_id = tab.layout.focused(); - if let Some((terminal_id, rt)) = runtime_for_tab_pane(terminal_runtimes, tab, focused_id) { + if let Some((terminal_id, rt)) = + runtime_for_tab_pane(app, terminal_runtimes, workspace_index, tab, focused_id) + { let borders = if multi_pane && app.pane_borders && app.pane_outer_borders { Borders::ALL } else { @@ -235,7 +246,9 @@ pub(super) fn resize_tab_panes( ) { let pane_inner = pane_inner_rect(info.rect, info.borders); - if let Some((terminal_id, rt)) = runtime_for_tab_pane(terminal_runtimes, tab, info.id) { + if let Some((terminal_id, rt)) = + runtime_for_tab_pane(app, terminal_runtimes, workspace_index, tab, info.id) + { let inner_rect = terminal_inner_rect(rt, pane_inner, app.pane_scrollbars); if !app.direct_attach_resize_locks.contains(terminal_id) { rt.resize( @@ -250,24 +263,27 @@ pub(super) fn resize_tab_panes( } /// Compute pane layout info and optionally resize pane runtimes to match. -pub(super) fn compute_pane_infos( +pub(super) fn compute_pane_infos_for_tab( app: &AppState, terminal_runtimes: &TerminalRuntimeRegistry, + ws_idx: usize, + tab_idx: usize, area: Rect, resize_panes: bool, cell_size: crate::kitty_graphics::HostCellSize, ) -> Vec<PaneInfo> { - let Some(ws_idx) = app.active else { - return Vec::new(); - }; - let Some(ws) = app.workspaces.get(ws_idx) else { + let Some(tab) = app + .workspaces + .get(ws_idx) + .and_then(|workspace| workspace.tabs.get(tab_idx)) + else { return Vec::new(); }; - let multi_pane = ws.layout.pane_count() > 1; + let multi_pane = tab.layout.pane_count() > 1; - if ws.zoomed { - let focused_id = ws.layout.focused(); + if tab.zoomed { + let focused_id = tab.layout.focused(); let borders = if multi_pane && app.pane_borders && app.pane_outer_borders { Borders::ALL } else { @@ -280,7 +296,7 @@ pub(super) fn compute_pane_infos( (inner_rect, scrollbar_rect) = stable_scrollbar_gutter(rt, pane_inner, app.pane_scrollbars); if resize_panes - && ws.terminal_id(focused_id).is_some_and(|terminal_id| { + && tab.terminal_id(focused_id).is_some_and(|terminal_id| { !app.direct_attach_resize_locks.contains(terminal_id) }) { @@ -303,7 +319,7 @@ pub(super) fn compute_pane_infos( } let mut pane_infos = apply_pane_chrome( - ws.layout.panes(area), + tab.layout.panes(area), app.pane_borders, app.pane_gaps, app.pane_outer_borders, @@ -318,7 +334,7 @@ pub(super) fn compute_pane_infos( (inner_rect, scrollbar_rect) = stable_scrollbar_gutter(rt, pane_inner, app.pane_scrollbars); if resize_panes - && ws.terminal_id(info.id).is_some_and(|terminal_id| { + && tab.terminal_id(info.id).is_some_and(|terminal_id| { !app.direct_attach_resize_locks.contains(terminal_id) }) { @@ -338,14 +354,44 @@ pub(super) fn compute_pane_infos( pane_infos } +#[cfg(test)] +fn compute_pane_infos( + app: &AppState, + terminal_runtimes: &TerminalRuntimeRegistry, + area: Rect, + resize_panes: bool, + cell_size: crate::kitty_graphics::HostCellSize, +) -> Vec<PaneInfo> { + let Some(workspace_index) = app.active else { + return Vec::new(); + }; + let Some(tab_index) = app + .workspaces + .get(workspace_index) + .map(crate::workspace::Workspace::active_tab_index) + else { + return Vec::new(); + }; + compute_pane_infos_for_tab( + app, + terminal_runtimes, + workspace_index, + tab_index, + area, + resize_panes, + cell_size, + ) +} + pub(super) fn render_panes( app: &AppState, terminal_runtimes: &TerminalRuntimeRegistry, frame: &mut Frame, + target: Option<super::tab_surface::TabSurfaceTarget>, pane_infos: &[PaneInfo], split_borders: &[crate::layout::SplitBorder], ) { - let Some(ws_idx) = app.active else { + let Some(ws_idx) = target.map(|target| target.workspace_index) else { return; }; let Some(ws) = app.workspaces.get(ws_idx) else { diff --git a/src/ui/tab_surface.rs b/src/ui/tab_surface.rs index 766c1d83..d7b80bff 100644 --- a/src/ui/tab_surface.rs +++ b/src/ui/tab_surface.rs @@ -1,18 +1,26 @@ use ratatui::{layout::Rect, Frame}; -use super::panes::{compute_pane_infos, render_panes, resize_tab_panes}; +use super::panes::{compute_pane_infos_for_tab, render_panes, resize_tab_panes}; use crate::app::AppState; use crate::layout::{PaneInfo, SplitBorder}; use crate::protocol::CursorState; use crate::terminal::TerminalRuntimeRegistry; +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct TabSurfaceTarget { + pub(crate) workspace_index: usize, + pub(crate) tab_index: usize, +} + pub(crate) struct TabSurfaceLayout { + pub(crate) target: Option<TabSurfaceTarget>, pub(crate) pane_infos: Vec<PaneInfo>, pub(crate) split_borders: Vec<SplitBorder>, } #[derive(Clone, Copy)] pub(crate) struct TabSurfaceView<'a> { + pub(crate) target: Option<TabSurfaceTarget>, pub(crate) pane_infos: &'a [PaneInfo], pub(crate) split_borders: &'a [SplitBorder], } @@ -24,20 +32,60 @@ pub(crate) fn compute_tab_surface( resize_panes: bool, cell_size: crate::kitty_graphics::HostCellSize, ) -> TabSurfaceLayout { - let split_borders = app - .active - .and_then(|i| app.workspaces.get(i)) - .map(|ws| { - if ws.zoomed { + let target = app.active.and_then(|workspace_index| { + let workspace = app.workspaces.get(workspace_index)?; + Some(TabSurfaceTarget { + workspace_index, + tab_index: workspace.active_tab_index(), + }) + }); + compute_tab_surface_for( + app, + terminal_runtimes, + target, + area, + resize_panes, + cell_size, + ) +} + +pub(crate) fn compute_tab_surface_for( + app: &AppState, + terminal_runtimes: &TerminalRuntimeRegistry, + target: Option<TabSurfaceTarget>, + area: Rect, + resize_panes: bool, + cell_size: crate::kitty_graphics::HostCellSize, +) -> TabSurfaceLayout { + let tab = target.and_then(|target| { + app.workspaces + .get(target.workspace_index)? + .tabs + .get(target.tab_index) + }); + let split_borders = tab + .map(|tab| { + if tab.zoomed { Vec::new() } else { - ws.layout.splits(area) + tab.layout.splits(area) } }) .unwrap_or_default(); - let pane_infos = compute_pane_infos(app, terminal_runtimes, area, resize_panes, cell_size); + let pane_infos = target.map_or_else(Vec::new, |target| { + compute_pane_infos_for_tab( + app, + terminal_runtimes, + target.workspace_index, + target.tab_index, + area, + resize_panes, + cell_size, + ) + }); TabSurfaceLayout { + target, pane_infos, split_borders, } @@ -46,11 +94,26 @@ pub(crate) fn compute_tab_surface( pub(crate) fn resize_tab_surface( app: &AppState, terminal_runtimes: &TerminalRuntimeRegistry, - tab: &crate::workspace::Tab, + workspace_index: usize, + tab_index: usize, area: Rect, cell_size: crate::kitty_graphics::HostCellSize, ) { - resize_tab_panes(app, terminal_runtimes, tab, area, cell_size); + let Some(tab) = app + .workspaces + .get(workspace_index) + .and_then(|workspace| workspace.tabs.get(tab_index)) + else { + return; + }; + resize_tab_panes( + app, + terminal_runtimes, + workspace_index, + tab, + area, + cell_size, + ); } pub(crate) fn render_tab_surface( @@ -63,6 +126,7 @@ pub(crate) fn render_tab_surface( app, terminal_runtimes, frame, + surface.target, surface.pane_infos, surface.split_borders, ); @@ -73,7 +137,7 @@ pub(crate) fn tab_surface_hyperlinks( terminal_runtimes: &TerminalRuntimeRegistry, surface: TabSurfaceView<'_>, ) -> Vec<((u16, u16), String, String)> { - let Some(ws_idx) = app.active else { + let Some(ws_idx) = surface.target.map(|target| target.workspace_index) else { return Vec::new(); }; if app.workspaces.get(ws_idx).is_none() { @@ -95,7 +159,7 @@ pub(crate) fn tab_surface_cursor( terminal_runtimes: &TerminalRuntimeRegistry, surface: TabSurfaceView<'_>, ) -> Option<CursorState> { - let ws_idx = app.active?; + let ws_idx = surface.target?.workspace_index; let info = surface.pane_infos.iter().find(|info| info.is_focused)?; if !app.pane_exposes_host_cursor(ws_idx, info.id) { return None; @@ -192,6 +256,7 @@ mod tests { app.view.pane_infos.clear(); let surface_view = TabSurfaceView { + target: surface.target, pane_infos: &surface.pane_infos, split_borders: &surface.split_borders, }; |
