zellij/zellij-server/src/ui/pane_contents_and_ui.rs
Aram Drevekenin dd291e2a1f
feat(ux): pin floating panes (#3876)
* working

* ui indication

* add keybinding

* add to plugin panes

* fix with multiple cursors

* toggle with the mouse

* fix e2e tests and add new one

* some cleanups

* add to layouts

* make mouse click more lenient

* allow setting a new floating pane as pinned

* make toggle work throughthe command line

* add to plugin api

* get tests to pass

* style(fmt): rustfmt
2024-12-16 16:03:20 +01:00

292 lines
11 KiB
Rust

use crate::output::Output;
use crate::panes::PaneId;
use crate::tab::Pane;
use crate::ui::boundaries::Boundaries;
use crate::ui::pane_boundaries_frame::FrameParams;
use crate::ClientId;
use std::collections::HashMap;
use zellij_utils::data::{
client_id_to_colors, single_client_color, InputMode, PaletteColor, Style,
};
use zellij_utils::errors::prelude::*;
pub struct PaneContentsAndUi<'a> {
pane: &'a mut Box<dyn Pane>,
output: &'a mut Output,
style: Style,
focused_clients: Vec<ClientId>,
multiple_users_exist_in_session: bool,
z_index: Option<usize>,
pane_is_stacked_under: bool,
pane_is_stacked_over: bool,
should_draw_pane_frames: bool,
}
impl<'a> PaneContentsAndUi<'a> {
pub fn new(
pane: &'a mut Box<dyn Pane>,
output: &'a mut Output,
style: Style,
active_panes: &HashMap<ClientId, PaneId>,
multiple_users_exist_in_session: bool,
z_index: Option<usize>,
pane_is_stacked_under: bool,
pane_is_stacked_over: bool,
should_draw_pane_frames: bool,
) -> Self {
let mut focused_clients: Vec<ClientId> = active_panes
.iter()
.filter(|(_c_id, p_id)| **p_id == pane.pid())
.map(|(c_id, _p_id)| *c_id)
.collect();
focused_clients.sort_unstable();
PaneContentsAndUi {
pane,
output,
style,
focused_clients,
multiple_users_exist_in_session,
z_index,
pane_is_stacked_under,
pane_is_stacked_over,
should_draw_pane_frames,
}
}
pub fn render_pane_contents_to_multiple_clients(
&mut self,
clients: impl Iterator<Item = ClientId>,
) -> Result<()> {
let err_context = "failed to render pane contents to multiple clients";
// here we drop the fake cursors so that their lines will be updated
// and we can clear them from the UI below
drop(self.pane.drain_fake_cursors());
if let Some((character_chunks, raw_vte_output, sixel_image_chunks)) =
self.pane.render(None).context(err_context)?
{
let clients: Vec<ClientId> = clients.collect();
self.output
.add_character_chunks_to_multiple_clients(
character_chunks,
clients.iter().copied(),
self.z_index,
)
.context(err_context)?;
self.output.add_sixel_image_chunks_to_multiple_clients(
sixel_image_chunks,
clients.iter().copied(),
self.z_index,
);
if let Some(raw_vte_output) = raw_vte_output {
if !raw_vte_output.is_empty() {
self.output.add_post_vte_instruction_to_multiple_clients(
clients.iter().copied(),
&format!(
"\u{1b}[{};{}H\u{1b}[m{}",
self.pane.y() + 1,
self.pane.x() + 1,
raw_vte_output
),
);
}
}
}
Ok(())
}
pub fn render_pane_contents_for_client(&mut self, client_id: ClientId) -> Result<()> {
let err_context = || format!("failed to render pane contents for client {client_id}");
if let Some((character_chunks, raw_vte_output, sixel_image_chunks)) = self
.pane
.render(Some(client_id))
.with_context(err_context)?
{
self.output
.add_character_chunks_to_client(client_id, character_chunks, self.z_index)
.with_context(err_context)?;
self.output.add_sixel_image_chunks_to_client(
client_id,
sixel_image_chunks,
self.z_index,
);
if let Some(raw_vte_output) = raw_vte_output {
self.output.add_post_vte_instruction_to_client(
client_id,
&format!(
"\u{1b}[{};{}H\u{1b}[m{}",
self.pane.y() + 1,
self.pane.x() + 1,
raw_vte_output
),
);
}
}
Ok(())
}
pub fn render_fake_cursor_if_needed(&mut self, client_id: ClientId) -> Result<()> {
let pane_focused_for_client_id = self.focused_clients.contains(&client_id);
let pane_focused_for_different_client = self
.focused_clients
.iter()
.filter(|&&c_id| c_id != client_id)
.count()
> 0;
if pane_focused_for_different_client && !pane_focused_for_client_id {
let fake_cursor_client_id = self
.focused_clients
.iter()
.find(|&&c_id| c_id != client_id)
.with_context(|| {
format!("failed to render fake cursor if needed for client {client_id}")
})?;
if let Some(colors) = client_id_to_colors(*fake_cursor_client_id, self.style.colors) {
let cursor_is_visible = self
.pane
.cursor_coordinates()
.map(|(x, y)| {
self.output
.cursor_is_visible(self.pane.x() + x, self.pane.y() + y)
})
.unwrap_or(false);
if cursor_is_visible {
if let Some(vte_output) = self.pane.render_fake_cursor(colors.0, colors.1) {
self.output.add_post_vte_instruction_to_client(
client_id,
&format!(
"\u{1b}[{};{}H\u{1b}[m{}",
self.pane.y() + 1,
self.pane.x() + 1,
vte_output
),
);
}
}
}
}
Ok(())
}
pub fn render_terminal_title_if_needed(
&mut self,
client_id: ClientId,
client_mode: InputMode,
previous_title: &mut Option<String>,
) {
if !self.focused_clients.contains(&client_id) {
return;
}
let vte_output = self.pane.render_terminal_title(client_mode);
if let Some(previous_title) = previous_title {
if *previous_title == vte_output {
return;
}
}
*previous_title = Some(vte_output.clone());
self.output
.add_post_vte_instruction_to_client(client_id, &vte_output);
}
pub fn render_pane_frame(
&mut self,
client_id: ClientId,
client_mode: InputMode,
session_is_mirrored: bool,
pane_is_floating: bool,
) -> Result<()> {
let err_context = || format!("failed to render pane frame for client {client_id}");
let pane_focused_for_client_id = self.focused_clients.contains(&client_id);
let other_focused_clients: Vec<ClientId> = self
.focused_clients
.iter()
.filter(|&&c_id| c_id != client_id)
.copied()
.collect();
let pane_focused_for_differet_client = !other_focused_clients.is_empty();
let frame_color = self.frame_color(client_id, client_mode, session_is_mirrored);
let focused_client = if pane_focused_for_client_id {
Some(client_id)
} else if pane_focused_for_differet_client {
Some(*other_focused_clients.first().with_context(err_context)?)
} else {
None
};
let frame_params = if session_is_mirrored {
FrameParams {
focused_client,
is_main_client: pane_focused_for_client_id,
other_focused_clients: vec![],
style: self.style,
color: frame_color,
other_cursors_exist_in_session: false,
pane_is_stacked_over: self.pane_is_stacked_over,
pane_is_stacked_under: self.pane_is_stacked_under,
should_draw_pane_frames: self.should_draw_pane_frames,
pane_is_floating,
}
} else {
FrameParams {
focused_client,
is_main_client: pane_focused_for_client_id,
other_focused_clients,
style: self.style,
color: frame_color,
other_cursors_exist_in_session: self.multiple_users_exist_in_session,
pane_is_stacked_over: self.pane_is_stacked_over,
pane_is_stacked_under: self.pane_is_stacked_under,
should_draw_pane_frames: self.should_draw_pane_frames,
pane_is_floating,
}
};
if let Some((frame_terminal_characters, vte_output)) = self
.pane
.render_frame(client_id, frame_params, client_mode)
.with_context(err_context)?
{
self.output
.add_character_chunks_to_client(client_id, frame_terminal_characters, self.z_index)
.with_context(err_context)?;
if let Some(vte_output) = vte_output {
self.output
.add_post_vte_instruction_to_client(client_id, &vte_output);
}
}
Ok(())
}
pub fn render_pane_boundaries(
&self,
client_id: ClientId,
client_mode: InputMode,
boundaries: &mut Boundaries,
session_is_mirrored: bool,
) {
let color = self.frame_color(client_id, client_mode, session_is_mirrored);
boundaries.add_rect(self.pane.as_ref(), color);
}
fn frame_color(
&self,
client_id: ClientId,
mode: InputMode,
session_is_mirrored: bool,
) -> Option<PaletteColor> {
let pane_focused_for_client_id = self.focused_clients.contains(&client_id);
if let Some(override_color) = self.pane.frame_color_override() {
Some(override_color)
} else if pane_focused_for_client_id {
match mode {
InputMode::Normal | InputMode::Locked => {
if session_is_mirrored || !self.multiple_users_exist_in_session {
let colors = single_client_color(self.style.colors); // mirrored sessions only have one focused color
Some(colors.0)
} else {
let colors = client_id_to_colors(client_id, self.style.colors);
colors.map(|colors| colors.0)
}
},
_ => Some(self.style.colors.orange),
}
} else {
None
}
}
}