diff options
| author | user <user@clank> | 2026-08-12 15:50:04 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-08-12 15:50:04 +0200 |
| commit | 266147d1d5ecadafda6a4893a60019c80f3c2c85 (patch) | |
| tree | 761be672fca76155d407ffda8be4a0c99033fc62 /zellij-server/src | |
| parent | sl0p: document the pane status bar protocol (diff) | |
| download | sl0ppij-266147d1d5ecadafda6a4893a60019c80f3c2c85.tar.gz sl0ppij-266147d1d5ecadafda6a4893a60019c80f3c2c85.tar.xz sl0ppij-266147d1d5ecadafda6a4893a60019c80f3c2c85.zip | |
sl0p: highlight the status bar button under the pointer
A click target with no hover feedback gives you no way to tell what you are about
to hit, which matters more here than elsewhere because the buttons sit in a frame
row that is only one cell tall.
The hovered button is inverted rather than merely recoloured: swapping
fore/background reads regardless of which colours a theme uses, and makes "the
pointer is on THIS one" unmistakable next to its unhovered neighbours.
Tracked per client, because hover belongs to a pointer rather than to a pane --
with two users attached, one pointer must not light up the other's button. Stored
on the pane keyed by client id, which the frame renderer already has.
Updated before the action dispatch rather than in the hover action, because
motion over the ACTIVE pane is dispatched as SendToTerminal, not UpdateHover --
and the pane you are about to click a button in is usually the one you are
already in. Every other pane's hover for that client is cleared on each motion,
so moving between panes cannot leave a button lit behind.
Diffstat (limited to 'zellij-server/src')
| -rw-r--r-- | zellij-server/src/panes/terminal_pane.rs | 28 | ||||
| -rw-r--r-- | zellij-server/src/tab/mod.rs | 11 | ||||
| -rw-r--r-- | zellij-server/src/tab/mouse_handler.rs | 43 | ||||
| -rw-r--r-- | zellij-server/src/ui/pane_boundaries_frame.rs | 24 | ||||
| -rw-r--r-- | zellij-server/src/ui/pane_contents_and_ui.rs | 2 |
5 files changed, 103 insertions, 5 deletions
diff --git a/zellij-server/src/panes/terminal_pane.rs b/zellij-server/src/panes/terminal_pane.rs index e663a314..10a5c2a6 100644 --- a/zellij-server/src/panes/terminal_pane.rs +++ b/zellij-server/src/panes/terminal_pane.rs @@ -166,6 +166,10 @@ pub struct TerminalPane { // held on startup and can possibly be used to display some errors pane_frame_color_override: Option<(PaletteColor, Option<String>)>, has_bell_notification: bool, + /// Status bar button the mouse is over, per client. Kept per client because + /// hover is a property of a pointer, not of the pane -- two attached users + /// must not highlight each other's buttons. + hovered_status_bar_button: HashMap<ClientId, String>, invoked_with: Option<Run>, #[allow(dead_code)] arrow_fonts: bool, @@ -1248,6 +1252,29 @@ impl Pane for TerminalPane { fn status_bar_buttons(&self) -> Vec<StatusBarButton> { self.grid.status_bar_buttons.clone() } + fn hovered_status_bar_button(&self, client_id: ClientId) -> Option<String> { + self.hovered_status_bar_button.get(&client_id).cloned() + } + fn set_hovered_status_bar_button( + &mut self, + client_id: ClientId, + button_id: Option<String>, + ) -> bool { + let previous = self.hovered_status_bar_button.get(&client_id).cloned(); + if previous == button_id { + return false; + } + match button_id { + Some(id) => { + self.hovered_status_bar_button.insert(client_id, id); + }, + None => { + self.hovered_status_bar_button.remove(&client_id); + }, + } + self.set_should_render(true); + true + } /// Id of the status bar button at `position`, if any. /// /// Pure: the caller writes the report, because delivering it needs the tab's @@ -1593,6 +1620,7 @@ impl TerminalPane { banner: None, pane_frame_color_override: None, has_bell_notification: false, + hovered_status_bar_button: HashMap::new(), invoked_with, arrow_fonts, notification_end, diff --git a/zellij-server/src/tab/mod.rs b/zellij-server/src/tab/mod.rs index 8f049cad..8da96d41 100644 --- a/zellij-server/src/tab/mod.rs +++ b/zellij-server/src/tab/mod.rs @@ -899,6 +899,17 @@ pub trait Pane { fn status_bar_button_at(&self, _position: &Position) -> Option<String> { None } + fn hovered_status_bar_button(&self, _client_id: ClientId) -> Option<String> { + None + } + /// Returns true if the hovered button changed, i.e. a re-render is needed. + fn set_hovered_status_bar_button( + &mut self, + _client_id: ClientId, + _button_id: Option<String>, + ) -> bool { + false + } fn stack_list_entry_label(&self) -> String { self.current_title() } diff --git a/zellij-server/src/tab/mouse_handler.rs b/zellij-server/src/tab/mouse_handler.rs index ab8b2383..cb1b79f6 100644 --- a/zellij-server/src/tab/mouse_handler.rs +++ b/zellij-server/src/tab/mouse_handler.rs @@ -410,9 +410,50 @@ impl MouseHandler { if let Some(effect) = Self::intercept_status_bar_button_click(tab, event)? { return Ok(effect); } + // Not an intercept: hover state is updated and the event still goes on to + // its normal handling. + let hover_changed = Self::update_status_bar_hover(tab, event, client_id)?; let context = Self::gather_mouse_event_context(tab, event, client_id)?; let action = Self::determine_mouse_action(event, &context)?; - Self::execute_mouse_action(tab, action, event, client_id) + let mut effect = Self::execute_mouse_action(tab, action, event, client_id)?; + if hover_changed { + effect.state_changed = true; + } + Ok(effect) + } + + /// Track which status bar button the pointer is over, so it can be drawn + /// highlighted. + /// + /// Done here rather than in the hover action, because motion over the ACTIVE + /// pane is dispatched as SendToTerminal, not UpdateHover -- and the pane you + /// are about to click a button in is usually the one you are already in. + /// + /// Returns whether anything changed, so the caller can force a render. + fn update_status_bar_hover( + tab: &mut Tab, + event: &MouseEvent, + client_id: ClientId, + ) -> Result<bool> { + if event.event_type != MouseEventType::Motion { + return Ok(false); + } + let hovered = Self::get_pane_at(tab, &event.position, false)? + .and_then(|pane| pane.status_bar_button_at(&event.position).map(|id| (pane.pid(), id))); + let mut changed = false; + // clear every other pane's hover for this client, so moving between panes + // cannot leave a button lit behind us + let pane_ids: Vec<PaneId> = tab.get_all_pane_ids(); + for pane_id in pane_ids { + let wanted = match &hovered { + Some((hovered_id, button_id)) if *hovered_id == pane_id => Some(button_id.clone()), + _ => None, + }; + if let Some(pane) = tab.get_pane_with_id_mut(pane_id) { + changed |= pane.set_hovered_status_bar_button(client_id, wanted); + } + } + Ok(changed) } /// A click on a pane's status bar button reports it to the program in that diff --git a/zellij-server/src/ui/pane_boundaries_frame.rs b/zellij-server/src/ui/pane_boundaries_frame.rs index 0a3155c1..cb4aa167 100644 --- a/zellij-server/src/ui/pane_boundaries_frame.rs +++ b/zellij-server/src/ui/pane_boundaries_frame.rs @@ -182,6 +182,8 @@ pub struct FrameParams { /// Program-supplied text for the bottom frame row (OSC 5577). pub status_bar: Option<String>, pub status_bar_buttons: Vec<StatusBarButton>, + /// Button this client's pointer is over, drawn highlighted. + pub hovered_status_bar_button: Option<String>, pub highlight_tooltip: Option<String>, pub omit_title: bool, pub frame_geom_override: Option<PaneGeom>, @@ -217,6 +219,7 @@ pub struct PaneFrame { show_help_text: bool, status_bar: Option<String>, status_bar_buttons: Vec<StatusBarButton>, + hovered_status_bar_button: Option<String>, highlight_tooltip: Option<String>, omit_title: bool, stack_list_entry: Option<StackListEntry>, @@ -257,6 +260,7 @@ impl PaneFrame { show_help_text: frame_params.show_help_text, status_bar: frame_params.status_bar, status_bar_buttons: frame_params.status_bar_buttons, + hovered_status_bar_button: frame_params.hovered_status_bar_button, highlight_tooltip: frame_params.highlight_tooltip, omit_title: frame_params.omit_title, stack_list_entry: frame_params.stack_list_entry, @@ -1338,11 +1342,22 @@ impl PaneFrame { } } let button_color = self.style.colors.text_unselected.emphasis_3; + let hovered = self.hovered_status_bar_button.as_deref(); for span in &layout.buttons { - for (i, character) in foreground_color(&span.rendered, Some(button_color)) - .into_iter() - .enumerate() - { + // The hovered button is inverted rather than merely recoloured: a + // button is a click target, so "the pointer is on THIS one" wants to + // be unmistakable, and swapping fore/background reads regardless of + // which colours a theme happens to use. + let characters = if hovered == Some(span.id.as_str()) { + inverted_color( + &span.rendered, + button_color, + self.style.colors.text_unselected.background, + ) + } else { + foreground_color(&span.rendered, Some(button_color)) + }; + for (i, character) in characters.into_iter().enumerate() { if let Some(slot) = inner.get_mut(span.start + i) { *slot = character; } @@ -1740,6 +1755,7 @@ mod tests { show_help_text: true, status_bar: None, status_bar_buttons: vec![], + hovered_status_bar_button: None, highlight_tooltip: None, omit_title: false, frame_geom_override: None, diff --git a/zellij-server/src/ui/pane_contents_and_ui.rs b/zellij-server/src/ui/pane_contents_and_ui.rs index b5162536..faf72875 100644 --- a/zellij-server/src/ui/pane_contents_and_ui.rs +++ b/zellij-server/src/ui/pane_contents_and_ui.rs @@ -396,6 +396,7 @@ impl<'a> PaneContentsAndUi<'a> { show_help_text: self.show_help_text, status_bar: self.pane.status_bar_text(), status_bar_buttons: self.pane.status_bar_buttons(), + hovered_status_bar_button: self.pane.hovered_status_bar_button(client_id), highlight_tooltip: highlight_tooltip.clone(), omit_title: self.omit_title, frame_geom_override: self.frame_geom_override, @@ -426,6 +427,7 @@ impl<'a> PaneContentsAndUi<'a> { show_help_text: self.show_help_text, status_bar: self.pane.status_bar_text(), status_bar_buttons: self.pane.status_bar_buttons(), + hovered_status_bar_button: self.pane.hovered_status_bar_button(client_id), highlight_tooltip, omit_title: self.omit_title, frame_geom_override: self.frame_geom_override, |
