aboutsummaryrefslogtreecommitdiff
path: root/src/ui
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-09-02 14:24:33 +0300
committerGitHub <noreply@github.com>2026-09-02 14:24:33 +0300
commit6c0bb273d5d5405a00985621b17e36f8b4d64609 (patch)
treeceef5ea8adf337bf3c1e977b133e76cb763c6f1b /src/ui
parentcc88b3b8e5bb9f7d9f23ed6ae85a52fd7b5b9ed6 (diff)
feat: support independent multi-client tab views (#3526)
Diffstat (limited to 'src/ui')
-rw-r--r--src/ui/panes.rs76
-rw-r--r--src/ui/tab_surface.rs89
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,
};