aboutsummaryrefslogtreecommitdiff
path: root/src/server/headless/render.rs
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/server/headless/render.rs
parentcc88b3b8e5bb9f7d9f23ed6ae85a52fd7b5b9ed6 (diff)
feat: support independent multi-client tab views (#3526)
Diffstat (limited to 'src/server/headless/render.rs')
-rw-r--r--src/server/headless/render.rs260
1 files changed, 152 insertions, 108 deletions
diff --git a/src/server/headless/render.rs b/src/server/headless/render.rs
index 19581abc..991d1861 100644
--- a/src/server/headless/render.rs
+++ b/src/server/headless/render.rs
@@ -1,55 +1,58 @@
use super::*;
impl HeadlessServer {
- fn focused_pane_graphics_demand(&self) -> bool {
+ fn shell_focused_runtime(
+ &self,
+ client_id: u64,
+ ) -> Option<(&crate::terminal::TerminalRuntime, crate::layout::PaneId)> {
+ if self.popup_owner_tab_id == self.shell_tab_id_for_client(client_id) {
+ if let Some(popup) = &self.app.state.popup_pane {
+ return self
+ .app
+ .terminal_runtimes
+ .get(&popup.terminal_id)
+ .map(|runtime| (runtime, popup.pane_id));
+ }
+ }
+ let target = self.shell_target_for_client(client_id)?;
+ let tab = self
+ .app
+ .state
+ .workspaces
+ .get(target.workspace_index)?
+ .tabs
+ .get(target.tab_index)?;
+ let pane_id = tab.layout.focused();
self.app
.state
- .active
- .and_then(|ws_idx| self.app.state.workspaces.get(ws_idx))
- .and_then(crate::workspace::Workspace::focused_pane_id)
- .is_some_and(|pane_id| self.app.pane_graphics.active_for_pane(pane_id))
+ .runtime_for_pane_in_workspace(
+ &self.app.terminal_runtimes,
+ target.workspace_index,
+ pane_id,
+ )
+ .map(|runtime| (runtime, pane_id))
}
pub(super) fn stream_host_mouse_capture_mode(&mut self) {
- let endpoint_shell_mouse = self.app.state.popup_pane.is_some()
- || self
- .app
- .state
- .focused_pane_requests_mouse_capture_from(&self.app.terminal_runtimes);
- let pixel_mouse_requested = self
- .clients
- .values()
- .any(|client| client.is_shell_client() && client.pixel_mouse);
- let shell_sgr_pixels = pixel_mouse_requested
- && self.focused_pane_graphics_demand()
- && self
- .app
- .state
- .active
- .and_then(|ws_idx| {
- self.app
- .state
- .workspaces
- .get(ws_idx)
- .and_then(crate::workspace::Workspace::focused_pane_id)
- .and_then(|pane_id| {
- self.app.state.runtime_for_pane_in_workspace(
- &self.app.terminal_runtimes,
- ws_idx,
- pane_id,
- )
- })
- })
- .is_some_and(crate::terminal::TerminalRuntime::sgr_pixel_mouse_enabled);
let requested = self
.clients
.iter()
.filter_map(|(&client_id, client)| match &client.mode {
- ClientConnectionMode::ClientShell => Some((
- client_id,
- client.shell_mouse_capture || endpoint_shell_mouse,
- shell_sgr_pixels && client.pixel_mouse,
- )),
+ ClientConnectionMode::ClientShell => {
+ let focused = self.shell_focused_runtime(client_id);
+ let child_requests_mouse =
+ focused.is_some_and(|(runtime, _)| runtime.mouse_reporting_enabled());
+ let sgr_pixels = client.pixel_mouse
+ && focused.is_some_and(|(runtime, pane_id)| {
+ self.app.pane_graphics.active_for_pane(pane_id)
+ && runtime.sgr_pixel_mouse_enabled()
+ });
+ Some((
+ client_id,
+ client.shell_mouse_capture || child_requests_mouse,
+ sgr_pixels,
+ ))
+ }
ClientConnectionMode::TerminalAttach { terminal_id } => {
let runtime = self.runtime_for_terminal_id_string(terminal_id);
let child_requests_mouse = runtime
@@ -106,45 +109,49 @@ impl HeadlessServer {
}
pub(super) fn stream_direct_terminal_keyboard_mode(&mut self) {
- let shell_report_all = {
- let runtime = if self.app.state.popup_pane.is_some() {
- self.app.popup_runtime()
- } else {
- self.app.state.active.and_then(|workspace_index| {
- self.app
- .state
- .focused_runtime_in_workspace(&self.app.terminal_runtimes, workspace_index)
- })
- };
- runtime.is_some_and(|runtime| {
- let protocol = runtime.keyboard_protocol();
- protocol.reports_all_keys()
- || (protocol.reports_event_types() && runtime.modify_other_keys_level() > 0)
+ let shell_modes = self
+ .clients
+ .iter()
+ .filter(|(_, client)| client.is_shell_client())
+ .map(|(&client_id, _)| {
+ let report_all =
+ self.shell_focused_runtime(client_id)
+ .is_some_and(|(runtime, _)| {
+ let protocol = runtime.keyboard_protocol();
+ protocol.reports_all_keys()
+ || (protocol.reports_event_types()
+ && runtime.modify_other_keys_level() > 0)
+ });
+ (client_id, report_all)
})
- };
- let serialized_shell =
- Self::frame_server_message(&ServerMessage::ClientShellKeyboardReportAll {
- enabled: shell_report_all,
- });
+ .collect::<Vec<_>>();
let mut broken_clients = Vec::new();
- for (&client_id, client) in &mut self.clients {
- if !client.is_shell_client()
- || client.host_keyboard_report_all_active == Some(shell_report_all)
- {
+ for (client_id, report_all) in shell_modes {
+ let Some(client) = self.clients.get_mut(&client_id) else {
+ continue;
+ };
+ if client.host_keyboard_report_all_active == Some(report_all) {
continue;
}
let Some(writer) = &client.writer else {
continue;
};
- let Ok(serialized) = serialized_shell.as_ref() else {
- warn!("failed to serialize client shell keyboard report-all mode");
- break;
+ let serialized = match Self::frame_server_message(
+ &ServerMessage::ClientShellKeyboardReportAll {
+ enabled: report_all,
+ },
+ ) {
+ Ok(serialized) => serialized,
+ Err(err) => {
+ warn!(err = %err, "failed to serialize client shell keyboard report-all mode");
+ continue;
+ }
};
- if writer.control.send(serialized.clone()).is_err() {
+ if writer.control.send(serialized).is_err() {
broken_clients.push(client_id);
continue;
}
- client.host_keyboard_report_all_active = Some(shell_report_all);
+ client.host_keyboard_report_all_active = Some(report_all);
}
let requested = self
@@ -214,11 +221,36 @@ impl HeadlessServer {
pub(super) fn sync_immediate_pty_sources(&self) {
let (has_app_target, direct_terminal_targets) = self.pty_render_targets();
- let mut pane_ids = if has_app_target {
- self.app.state.app_surface_pane_ids()
- } else {
- HashSet::new()
- };
+ let mut pane_ids = HashSet::new();
+ if has_app_target {
+ for (&client_id, client) in &self.clients {
+ if !client.is_shell_client() || client.writer.is_none() {
+ continue;
+ }
+ let Some(target) = self.shell_target_for_client(client_id) else {
+ continue;
+ };
+ let Some(tab) = self
+ .app
+ .state
+ .workspaces
+ .get(target.workspace_index)
+ .and_then(|workspace| workspace.tabs.get(target.tab_index))
+ else {
+ continue;
+ };
+ if tab.zoomed {
+ pane_ids.insert(tab.layout.focused());
+ } else {
+ pane_ids.extend(tab.layout.pane_ids());
+ }
+ if self.popup_owner_tab_id == self.shell_tab_id_for_client(client_id) {
+ if let Some(popup) = &self.app.state.popup_pane {
+ pane_ids.insert(popup.pane_id);
+ }
+ }
+ }
+ }
if !direct_terminal_targets.is_empty() {
for workspace in &self.app.state.workspaces {
for tab in &workspace.tabs {
@@ -265,7 +297,7 @@ impl HeadlessServer {
direct_terminal_targets: &HashSet<&str>,
) -> bool {
let terminal_id = self.terminal_id_for_pane(pane_id);
- (has_app_target && (terminal_id.is_none() || self.app_surface_contains_pane(pane_id)))
+ (has_app_target && (terminal_id.is_none() || self.any_shell_surface_contains_pane(pane_id)))
|| terminal_id.is_none_or(|source| direct_terminal_targets.contains(source.as_str()))
}
@@ -305,31 +337,34 @@ impl HeadlessServer {
.map(|(_, pane)| &pane.attached_terminal_id)
}
- fn app_surface_contains_pane(&self, pane_id: crate::layout::PaneId) -> bool {
- if self
- .app
- .state
- .popup_pane
- .as_ref()
- .is_some_and(|popup| popup.pane_id == pane_id)
- {
- return true;
- }
- let Some(workspace) = self
- .app
- .state
- .active
- .and_then(|ws_idx| self.app.state.workspaces.get(ws_idx))
- else {
- return false;
- };
- let Some(tab) = workspace.active_tab() else {
- return false;
- };
- if !tab.panes.contains_key(&pane_id) {
- return false;
- }
- !tab.zoomed || tab.layout.focused() == pane_id
+ fn any_shell_surface_contains_pane(&self, pane_id: crate::layout::PaneId) -> bool {
+ self.clients.iter().any(|(&client_id, client)| {
+ if !client.is_shell_client() || client.writer.is_none() {
+ return false;
+ }
+ if self
+ .app
+ .state
+ .popup_pane
+ .as_ref()
+ .is_some_and(|popup| popup.pane_id == pane_id)
+ {
+ return self.popup_owner_tab_id == self.shell_tab_id_for_client(client_id);
+ }
+ let Some(target) = self.shell_target_for_client(client_id) else {
+ return false;
+ };
+ let Some(tab) = self
+ .app
+ .state
+ .workspaces
+ .get(target.workspace_index)
+ .and_then(|workspace| workspace.tabs.get(target.tab_index))
+ else {
+ return false;
+ };
+ tab.panes.contains_key(&pane_id) && (!tab.zoomed || tab.layout.focused() == pane_id)
+ })
}
pub(super) fn render_and_stream(&mut self) {
@@ -362,21 +397,28 @@ impl HeadlessServer {
return;
}
- let shell_snapshot_template = render_targets
- .iter()
- .any(|(_, _, _, _, mode)| matches!(mode, ClientConnectionMode::ClientShell))
- .then(|| client_shell_snapshot(&self.app, &self.client_shell_boot_id, 0, None));
let mut broken_clients: Vec<u64> = Vec::new();
- for (client_id, (cols, rows), cell_size, is_foreground, mode) in render_targets {
+ for (client_id, (cols, rows), cell_size, _is_foreground, mode) in render_targets {
let area = Rect::new(0, 0, cols, rows);
+ let shell_target = self.shell_target_for_client(client_id);
+ let shell_tab_id = self.shell_tab_id_for_client(client_id);
+ let shell_shows_popup = shell_tab_id.as_deref() == self.popup_owner_tab_id.as_deref();
let mut shell_projection_revision = 0;
if matches!(mode, ClientConnectionMode::ClientShell) {
+ let location = self
+ .clients
+ .get(&client_id)
+ .and_then(|client| client.shell_location.clone());
let Some(client) = self.clients.get_mut(&client_id) else {
continue;
};
- let Some(mut candidate) = shell_snapshot_template.clone() else {
- continue;
- };
+ let mut candidate = client_shell_snapshot(
+ &self.app,
+ &self.client_shell_boot_id,
+ client.shell_projection_revision,
+ None,
+ location.as_ref(),
+ );
candidate.config_diagnostic = if client.shell_uses_endpoint_keybindings {
self.server_config_diagnostic.clone()
} else {
@@ -438,8 +480,10 @@ impl HeadlessServer {
graphics_delivery: next_graphics_delivery,
} = render_client_shell_pane_surface(
&mut self.app,
+ shell_target,
area,
- is_foreground,
+ false,
+ shell_shows_popup,
render_cell_size,
&shell_graphics_delivery,
client_id,