diff options
| author | Aram Drevekenin <aram@poor.dev> | 2026-06-29 18:10:59 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-06-29 18:10:59 +0200 |
| commit | 245f5141c9bb7eb91f003efac4defc94470243fd (patch) | |
| tree | 52ae6d003366f97e41c8a1450602aef63c8fdca2 /default-plugins | |
| parent | feat: reload config on theme change (#5135) (diff) | |
| download | sl0ppij-245f5141c9bb7eb91f003efac4defc94470243fd.tar.gz sl0ppij-245f5141c9bb7eb91f003efac4defc94470243fd.tar.xz sl0ppij-245f5141c9bb7eb91f003efac4defc94470243fd.zip | |
Title frames (#5318)
* initial implementation
* improve single line title design
* hint text plugin infra
* add proto regen task
* display alt text in status bar
* don't display text with full frames
* style alt hover text
* fix + style control own pane hover
* make multiplayer indications responsive
* update snapshots
* plugin api
* single pane as tab
* better scroll styling
* fix tests
* tab-bar plus hover
* plugin surface tests
* integration tests
* adjust all integration tests
* reduce hover noise
* add pane id to in place
* more integration tests
* style: rustfmt
* update plugin assets
* update assets and snapshots
* add pr
Diffstat (limited to 'default-plugins')
| -rw-r--r-- | default-plugins/fixture-plugin-for-tests/src/main.rs | 5 | ||||
| -rw-r--r-- | default-plugins/status-bar/src/main.rs | 94 | ||||
| -rw-r--r-- | default-plugins/status-bar/src/one_line_ui.rs | 492 | ||||
| -rw-r--r-- | default-plugins/tab-bar/src/line.rs | 74 | ||||
| -rw-r--r-- | default-plugins/tab-bar/src/main.rs | 49 | ||||
| -rw-r--r-- | default-plugins/tab-bar/src/tab.rs | 13 |
6 files changed, 585 insertions, 142 deletions
diff --git a/default-plugins/fixture-plugin-for-tests/src/main.rs b/default-plugins/fixture-plugin-for-tests/src/main.rs index 4885054c..ec656443 100644 --- a/default-plugins/fixture-plugin-for-tests/src/main.rs +++ b/default-plugins/fixture-plugin-for-tests/src/main.rs @@ -173,6 +173,8 @@ impl ZellijPlugin for State { BareKey::Char('6') if key.has_no_modifiers() => close_focused_tab(), BareKey::Char('7') if key.has_no_modifiers() => undo_rename_tab(), BareKey::Char('8') if key.has_no_modifiers() => quit_zellij(), + BareKey::Char('9') if key.has_no_modifiers() => new_pane(), + BareKey::Char('0') if key.has_no_modifiers() => toggle_floating_panes(None), BareKey::Char('a') if key.has_only_modifiers(&[KeyModifier::Ctrl]) => { previous_swap_layout() }, @@ -626,6 +628,9 @@ impl ZellijPlugin for State { // Test clear_pane_highlights clear_pane_highlights(PaneId::Terminal(1)); }, + BareKey::Char('i') if key.has_only_modifiers(&[KeyModifier::Super]) => { + toggle_floating_panes(Some(2)); + }, BareKey::Char('a') if key.has_only_modifiers(&[KeyModifier::Ctrl, KeyModifier::Shift]) => { diff --git a/default-plugins/status-bar/src/main.rs b/default-plugins/status-bar/src/main.rs index 5412e6b6..6cffc5c7 100644 --- a/default-plugins/status-bar/src/main.rs +++ b/default-plugins/status-bar/src/main.rs @@ -42,6 +42,12 @@ struct State { classic_ui: bool, base_mode_is_locked: bool, cached_keybinds: KeybindsVec, + hint_text: Option<BTreeMap<usize, StyledText>>, + outstanding_hint_timeouts: usize, + new_pane_ribbon_range: Option<(usize, usize)>, + floating_ribbon_range: Option<(usize, usize)>, + new_pane_ribbon_hovered: bool, + floating_ribbon_hovered: bool, } register_plugin!(State); @@ -192,6 +198,13 @@ fn color_elements(palette: Styling, different_color_alternates: bool) -> Colored } } +fn col_in_range(range: Option<(usize, usize)>, col: usize) -> bool { + match range { + Some((start, end)) => col >= start && col < end, + None => false, + } +} + impl ZellijPlugin for State { fn load(&mut self, configuration: BTreeMap<String, String>) { // TODO: Should be able to choose whether to use the cache through config. @@ -209,6 +222,9 @@ impl ZellijPlugin for State { EventType::InputReceived, EventType::SystemClipboardFailure, EventType::InitialKeybinds, + EventType::HintText, + EventType::Timer, + EventType::Mouse, ]); } @@ -260,11 +276,55 @@ impl ZellijPlugin for State { Event::InputReceived => { if self.text_copy_destination.is_some() || self.display_system_clipboard_failure == true + || self.hint_text.is_some() { should_render = true; } self.text_copy_destination = None; self.display_system_clipboard_failure = false; + self.hint_text = None; + }, + Event::HintText(hint_variants) => { + if hint_variants.is_empty() { + if self.hint_text.is_some() { + self.hint_text = None; + should_render = true; + } + } else { + self.hint_text = Some(hint_variants); + self.outstanding_hint_timeouts += 1; + set_timeout(5.0); + should_render = true; + } + }, + Event::Timer(_) => { + self.outstanding_hint_timeouts = self.outstanding_hint_timeouts.saturating_sub(1); + if self.outstanding_hint_timeouts == 0 && self.hint_text.is_some() { + self.hint_text = None; + should_render = true; + } + }, + Event::Mouse(mouse_event) => match mouse_event { + Mouse::LeftClick(_, col) => { + if col_in_range(self.new_pane_ribbon_range, col) { + new_pane(); + } else if col_in_range(self.floating_ribbon_range, col) { + toggle_floating_panes(None); + } + }, + Mouse::Hover(_, col) => { + let new_pane_hovered = col_in_range(self.new_pane_ribbon_range, col); + let floating_hovered = + !new_pane_hovered && col_in_range(self.floating_ribbon_range, col); + if self.new_pane_ribbon_hovered != new_pane_hovered + || self.floating_ribbon_hovered != floating_hovered + { + self.new_pane_ribbon_hovered = new_pane_hovered; + self.floating_ribbon_hovered = floating_hovered; + should_render = true; + } + }, + _ => {}, }, _ => {}, }; @@ -287,21 +347,31 @@ impl ZellijPlugin for State { PaletteColor::EightBit(color) => format!("\u{1b}[48;5;{}m\u{1b}[0K", color), }; let active_tab = self.tabs.iter().find(|t| t.active); - print!( - "{}{}", - one_line_ui( - &self.mode_info, - active_tab, - cols, - separator, - self.base_mode_is_locked, - self.text_copy_destination, - self.display_system_clipboard_failure, - ), - fill_bg, + let full_pane_frames = self.mode_info.pane_frame_style == Some(PaneFrameStyle::Full); + let hint_text = if full_pane_frames { + None + } else { + self.hint_text.as_ref() + }; + let (line, new_pane_ribbon_range, floating_ribbon_range) = one_line_ui( + &self.mode_info, + active_tab, + cols, + separator, + self.base_mode_is_locked, + self.text_copy_destination, + self.display_system_clipboard_failure, + hint_text, + self.new_pane_ribbon_hovered, + self.floating_ribbon_hovered, ); + self.new_pane_ribbon_range = new_pane_ribbon_range; + self.floating_ribbon_range = floating_ribbon_range; + print!("{}{}", line, fill_bg); return; } + self.new_pane_ribbon_range = None; + self.floating_ribbon_range = None; //TODO: Switch to UI components here let active_tab = self.tabs.iter().find(|t| t.active); diff --git a/default-plugins/status-bar/src/one_line_ui.rs b/default-plugins/status-bar/src/one_line_ui.rs index d997fb36..97721167 100644 --- a/default-plugins/status-bar/src/one_line_ui.rs +++ b/default-plugins/status-bar/src/one_line_ui.rs @@ -3,10 +3,10 @@ use ansi_term::{ Color::{Fixed, RGB}, Style, }; -use std::collections::HashMap; +use std::collections::{BTreeMap, HashMap}; use zellij_tile::prelude::actions::Action; use zellij_tile::prelude::*; -use zellij_tile_utils::palette_match; +use zellij_tile_utils::{palette_match, style}; use crate::first_line::{to_char, KeyAction, KeyMode, KeyShortcut}; use crate::second_line::{system_clipboard_error, text_copied_hint}; @@ -22,30 +22,69 @@ pub fn one_line_ui( base_mode_is_locked: bool, text_copied_to_clipboard_destination: Option<CopyDestination>, clipboard_failure: bool, -) -> LinePart { + hint: Option<&BTreeMap<usize, StyledText>>, + new_pane_hovered: bool, + floating_hovered: bool, +) -> (LinePart, Option<(usize, usize)>, Option<(usize, usize)>) { if let Some(text_copied_to_clipboard_destination) = text_copied_to_clipboard_destination { - return text_copied_hint(text_copied_to_clipboard_destination); + return ( + text_copied_hint(text_copied_to_clipboard_destination), + None, + None, + ); } if clipboard_failure { - return system_clipboard_error(&help.style.colors); + return (system_clipboard_error(&help.style.colors), None, None); } let mut line_part_to_render = LinePart::default(); - let mut append = |line_part: &LinePart, max_len: &mut usize| { - line_part_to_render.append(line_part); - *max_len = max_len.saturating_sub(line_part.len); - }; + let mut new_pane_range = None; + let mut floating_range = None; + + let left_part = hint + .and_then(|hint| hint_line_part(hint, max_len)) + .or_else(|| render_mode_key_indicators(help, max_len, separator, base_mode_is_locked)); + if let Some(left) = left_part { + line_part_to_render.append(&left); + max_len = max_len.saturating_sub(left.len); + match help.mode { + InputMode::Normal | InputMode::Locked => { + let secondary_offset = line_part_to_render.len; + if let Some((secondary_info, new_pane, floating)) = render_secondary_info( + help, + tab_info, + max_len, + new_pane_hovered, + floating_hovered, + ) { + new_pane_range = new_pane + .map(|(start, end)| (secondary_offset + start, secondary_offset + end)); + floating_range = floating + .map(|(start, end)| (secondary_offset + start, secondary_offset + end)); + line_part_to_render.append(&secondary_info); + } + }, + _ => { + if let Some(key_group_separator) = add_keygroup_separator(help, max_len) { + line_part_to_render.append(&key_group_separator); + max_len = max_len.saturating_sub(key_group_separator.len); + if let Some(keybinds) = keybinds(help, max_len) { + line_part_to_render.append(&keybinds); + } + } + }, + } + } + (line_part_to_render, new_pane_range, floating_range) +} - render_mode_key_indicators(help, max_len, separator, base_mode_is_locked) - .map(|mode_key_indicators| append(&mode_key_indicators, &mut max_len)) - .and_then(|_| match help.mode { - InputMode::Normal | InputMode::Locked => render_secondary_info(help, tab_info, max_len) - .map(|secondary_info| append(&secondary_info, &mut max_len)), - _ => add_keygroup_separator(help, max_len) - .map(|key_group_separator| append(&key_group_separator, &mut max_len)) - .and_then(|_| keybinds(help, max_len)) - .map(|keybinds| append(&keybinds, &mut max_len)), - }); - line_part_to_render +fn hint_line_part(hint: &BTreeMap<usize, StyledText>, max_len: usize) -> Option<LinePart> { + let fitting_variant = hint + .range(..=max_len) + .next_back() + .map(|(_width, styled_text)| styled_text)?; + let len = fitting_variant.text.width(); + let part = serialize_text(&Text::from(fitting_variant.clone())); + Some(LinePart { part, len }) } fn to_base_mode(base_mode: InputMode) -> Action { @@ -730,11 +769,14 @@ fn render_secondary_info( help: &ModeInfo, tab_info: Option<&TabInfo>, max_len: usize, -) -> Option<LinePart> { + new_pane_hovered: bool, + floating_hovered: bool, +) -> Option<(LinePart, Option<(usize, usize)>, Option<(usize, usize)>)> { let mut secondary_info = LinePart::default(); let supports_arrow_fonts = !help.capabilities.arrow_fonts; let colored_elements = color_elements(help.style.colors, !supports_arrow_fonts); - let secondary_keybinds = secondary_keybinds(&help, tab_info, max_len); + let (secondary_keybinds, new_pane_range, floating_range) = + secondary_keybinds(&help, tab_info, max_len, new_pane_hovered, floating_hovered); secondary_info.append(&secondary_keybinds); let remaining_space = max_len.saturating_sub(secondary_info.len).saturating_sub(1); // 1 for the end padding of the line let mut padding = String::new(); @@ -746,7 +788,10 @@ fn render_secondary_info( secondary_info.part = format!("{}{}", padding, secondary_info.part); secondary_info.len += padding_len; if secondary_info.len <= max_len { - Some(secondary_info) + let shift = |range: Option<(usize, usize)>| { + range.map(|(start, end)| (start + padding_len, end + padding_len)) + }; + Some((secondary_info, shift(new_pane_range), shift(floating_range))) } else { None } @@ -764,8 +809,16 @@ fn should_show_focus_and_resize_shortcuts(tab_info: Option<&TabInfo>) -> bool { } } -fn secondary_keybinds(help: &ModeInfo, tab_info: Option<&TabInfo>, max_len: usize) -> LinePart { +fn secondary_keybinds( + help: &ModeInfo, + tab_info: Option<&TabInfo>, + max_len: usize, + new_pane_hovered: bool, + floating_hovered: bool, +) -> (LinePart, Option<(usize, usize)>, Option<(usize, usize)>) { let mut secondary_info = LinePart::default(); + let mut new_pane_range; + let mut floating_range; let binds = &help.get_mode_keybinds(); let should_show_focus_and_resize_shortcuts = should_show_focus_and_resize_shortcuts(tab_info); // New Pane @@ -908,13 +961,24 @@ fn secondary_keybinds(help: &ModeInfo, tab_info: Option<&TabInfo>, max_len: usiz let no_common_modifier = common_modifiers.is_empty(); if no_common_modifier { - secondary_info.append(&add_shortcut( - help, - "New Pane", - &new_pane_key_to_display, - false, - Some(0), - )); + let new_pane_start = secondary_info.len; + if new_pane_hovered { + secondary_info.append(&add_shortcut_hovered( + help, + "New Pane", + &new_pane_key_to_display, + Some(0), + )); + } else { + secondary_info.append(&add_shortcut( + help, + "New Pane", + &new_pane_key_to_display, + false, + Some(0), + )); + } + new_pane_range = ribbon_range(new_pane_start, secondary_info.len); if should_show_focus_and_resize_shortcuts { secondary_info.append(&add_shortcut( help, @@ -931,13 +995,24 @@ fn secondary_keybinds(help: &ModeInfo, tab_info: Option<&TabInfo>, max_len: usiz Some(0), )); } - secondary_info.append(&add_shortcut( - help, - "Floating", - &toggle_floating_key_to_display, - are_floating_panes_visible, - Some(0), - )); + let floating_start = secondary_info.len; + if floating_hovered { + secondary_info.append(&add_shortcut_hovered( + help, + "Floating", + &toggle_floating_key_to_display, + Some(0), + )); + } else { + secondary_info.append(&add_shortcut( + help, + "Floating", + &toggle_floating_key_to_display, + are_floating_panes_visible, + Some(0), + )); + } + floating_range = ribbon_range(floating_start, secondary_info.len); } else { let modifier_str = text_as_line_part_with_emphasis( format!( @@ -967,12 +1042,22 @@ fn secondary_keybinds(help: &ModeInfo, tab_info: Option<&TabInfo>, max_len: usiz .iter() .map(|k| k.strip_common_modifiers(&common_modifiers)) .collect(); - secondary_info.append(&add_shortcut_with_inline_key( - help, - "New Pane", - new_pane_key_to_display, - false, - )); + let new_pane_start = secondary_info.len; + if new_pane_hovered { + secondary_info.append(&add_shortcut_with_inline_key_hovered( + help, + "New Pane", + new_pane_key_to_display, + )); + } else { + secondary_info.append(&add_shortcut_with_inline_key( + help, + "New Pane", + new_pane_key_to_display, + false, + )); + } + new_pane_range = ribbon_range(new_pane_start, secondary_info.len); if should_show_focus_and_resize_shortcuts { secondary_info.append(&add_shortcut_with_inline_key( help, @@ -987,19 +1072,38 @@ fn secondary_keybinds(help: &ModeInfo, tab_info: Option<&TabInfo>, max_len: usiz false, )); } - secondary_info.append(&add_shortcut_with_inline_key( - help, - "Floating", - toggle_floating_key_to_display, - are_floating_panes_visible, - )); + let floating_start = secondary_info.len; + if floating_hovered { + secondary_info.append(&add_shortcut_with_inline_key_hovered( + help, + "Floating", + toggle_floating_key_to_display, + )); + } else { + secondary_info.append(&add_shortcut_with_inline_key( + help, + "Floating", + toggle_floating_key_to_display, + are_floating_panes_visible, + )); + } + floating_range = ribbon_range(floating_start, secondary_info.len); } if secondary_info.len <= max_len { - secondary_info - } else { - let mut short_line = LinePart::default(); - if no_common_modifier { + return (secondary_info, new_pane_range, floating_range); + } + let mut short_line = LinePart::default(); + if no_common_modifier { + let new_pane_start = short_line.len; + if new_pane_hovered { + short_line.append(&add_shortcut_hovered( + help, + "New", + &new_pane_key_to_display, + Some(0), + )); + } else { short_line.append(&add_shortcut( help, "New", @@ -1007,22 +1111,33 @@ fn secondary_keybinds(help: &ModeInfo, tab_info: Option<&TabInfo>, max_len: usiz false, Some(0), )); - if should_show_focus_and_resize_shortcuts { - short_line.append(&add_shortcut( - help, - "Focus", - &move_focus_shortcuts, - false, - Some(0), - )); - short_line.append(&add_shortcut( - help, - "Resize", - &resize_shortcuts, - false, - Some(0), - )); - } + } + new_pane_range = ribbon_range(new_pane_start, short_line.len); + if should_show_focus_and_resize_shortcuts { + short_line.append(&add_shortcut( + help, + "Focus", + &move_focus_shortcuts, + false, + Some(0), + )); + short_line.append(&add_shortcut( + help, + "Resize", + &resize_shortcuts, + false, + Some(0), + )); + } + let floating_start = short_line.len; + if floating_hovered { + short_line.append(&add_shortcut_hovered( + help, + "Floating", + &toggle_floating_key_to_display, + Some(0), + )); + } else { short_line.append(&add_shortcut( help, "Floating", @@ -1030,56 +1145,75 @@ fn secondary_keybinds(help: &ModeInfo, tab_info: Option<&TabInfo>, max_len: usiz are_floating_panes_visible, Some(0), )); - } else { - let modifier_str = text_as_line_part_with_emphasis( - format!( - "{} + ", - common_modifiers - .iter() - .map(|m| m.to_string()) - .collect::<Vec<_>>() - .join("-") - ), - 0, - ); - short_line.append(&modifier_str); - let new_pane_key_to_display: Vec<KeyWithModifier> = new_pane_key_to_display - .iter() - .map(|k| k.strip_common_modifiers(&common_modifiers)) - .collect(); - let move_focus_shortcuts: Vec<KeyWithModifier> = move_focus_shortcuts - .iter() - .map(|k| k.strip_common_modifiers(&common_modifiers)) - .collect(); - let resize_shortcuts: Vec<KeyWithModifier> = resize_shortcuts - .iter() - .map(|k| k.strip_common_modifiers(&common_modifiers)) - .collect(); - let toggle_floating_key_to_display: Vec<KeyWithModifier> = - toggle_floating_key_to_display + } + floating_range = ribbon_range(floating_start, short_line.len); + } else { + let modifier_str = text_as_line_part_with_emphasis( + format!( + "{} + ", + common_modifiers .iter() - .map(|k| k.strip_common_modifiers(&common_modifiers)) - .collect(); + .map(|m| m.to_string()) + .collect::<Vec<_>>() + .join("-") + ), + 0, + ); + short_line.append(&modifier_str); + let new_pane_key_to_display: Vec<KeyWithModifier> = new_pane_key_to_display + .iter() + .map(|k| k.strip_common_modifiers(&common_modifiers)) + .collect(); + let move_focus_shortcuts: Vec<KeyWithModifier> = move_focus_shortcuts + .iter() + .map(|k| k.strip_common_modifiers(&common_modifiers)) + .collect(); + let resize_shortcuts: Vec<KeyWithModifier> = resize_shortcuts + .iter() + .map(|k| k.strip_common_modifiers(&common_modifiers)) + .collect(); + let toggle_floating_key_to_display: Vec<KeyWithModifier> = toggle_floating_key_to_display + .iter() + .map(|k| k.strip_common_modifiers(&common_modifiers)) + .collect(); + let new_pane_start = short_line.len; + if new_pane_hovered { + short_line.append(&add_shortcut_with_inline_key_hovered( + help, + "New", + new_pane_key_to_display, + )); + } else { short_line.append(&add_shortcut_with_inline_key( help, "New", new_pane_key_to_display, false, )); - if should_show_focus_and_resize_shortcuts { - short_line.append(&add_shortcut_with_inline_key( - help, - "Focus", - move_focus_shortcuts, - false, - )); - short_line.append(&add_shortcut_with_inline_key( - help, - "Resize", - resize_shortcuts, - false, - )); - } + } + new_pane_range = ribbon_range(new_pane_start, short_line.len); + if should_show_focus_and_resize_shortcuts { + short_line.append(&add_shortcut_with_inline_key( + help, + "Focus", + move_focus_shortcuts, + false, + )); + short_line.append(&add_shortcut_with_inline_key( + help, + "Resize", + resize_shortcuts, + false, + )); + } + let floating_start = short_line.len; + if floating_hovered { + short_line.append(&add_shortcut_with_inline_key_hovered( + help, + "Floating", + toggle_floating_key_to_display, + )); + } else { short_line.append(&add_shortcut_with_inline_key( help, "Floating", @@ -1087,22 +1221,27 @@ fn secondary_keybinds(help: &ModeInfo, tab_info: Option<&TabInfo>, max_len: usiz are_floating_panes_visible, )); } - if short_line.len <= max_len { - short_line - } else if max_len >= 3 { - let part = serialize_text( - &Text::new(format!("{:>width$}", "...", width = max_len)) - .color_range(0, ..) - .opaque(), - ); - let len = max_len; - LinePart { part, len } - } else { + floating_range = ribbon_range(floating_start, short_line.len); + } + if short_line.len <= max_len { + (short_line, new_pane_range, floating_range) + } else if max_len >= 3 { + let part = serialize_text( + &Text::new(format!("{:>width$}", "...", width = max_len)) + .color_range(0, ..) + .opaque(), + ); + let len = max_len; + (LinePart { part, len }, None, None) + } else { + ( LinePart { part: "".to_owned(), len: 0, - } - } + }, + None, + None, + ) } } @@ -1126,6 +1265,107 @@ fn keybinds(help: &ModeInfo, max_width: usize) -> Option<LinePart> { Some(best_effort_shortcut_list(help, max_width)) } +fn ribbon_range(start: usize, end: usize) -> Option<(usize, usize)> { + if end > start { + Some((start, end)) + } else { + None + } +} + +fn hovered_ribbon_wrap(body: String, palette: Styling, supports_arrow_fonts: bool) -> String { + let ribbon_bg = palette.ribbon_unselected.emphasis_1; + let outer_bg = palette.text_unselected.background; + let arrow = if supports_arrow_fonts { + crate::ARROW_SEPARATOR + } else { + "" + }; + let left = style!(outer_bg, ribbon_bg).paint(arrow).to_string(); + let right = style!(ribbon_bg, outer_bg).paint(arrow).to_string(); + format!("{}{}{}", left, body, right) +} + +fn add_shortcut_hovered( + help: &ModeInfo, + text: &str, + keys: &Vec<KeyWithModifier>, + key_color_index: Option<usize>, +) -> LinePart { + let mut ret = LinePart::default(); + if keys.is_empty() { + return ret; + } + ret.append(&style_key_with_modifier(&keys, key_color_index)); + let palette = help.style.colors; + let supports_arrow_fonts = !help.capabilities.arrow_fonts; + let ribbon_bg = palette.ribbon_unselected.emphasis_1; + let body = style!(palette.ribbon_unselected.base, ribbon_bg) + .bold() + .paint(format!(" {} ", text)) + .to_string(); + let ribbon = hovered_ribbon_wrap(body, palette, supports_arrow_fonts); + ret.part = format!("{}{}", ret.part, ribbon); + ret.len += if supports_arrow_fonts { + text.width() + 4 + } else { + text.width() + 2 + }; + ret +} + +fn add_shortcut_with_inline_key_hovered( + help: &ModeInfo, + text: &str, + key: Vec<KeyWithModifier>, +) -> LinePart { + let mut ret = LinePart::default(); + if key.is_empty() { + return ret; + } + let key_separator = match key + .iter() + .map(|k| k.to_string()) + .collect::<Vec<_>>() + .join("") + .as_str() + { + "HJKL" => "", + "hjkl" => "", + "←↓↑→" => "", + "←→" => "", + "↓↑" => "", + "[]" => "", + "+-" => "", + _ => "|", + }; + let key_string = format!( + "{}", + key.iter() + .map(|k| k.to_string()) + .collect::<Vec<_>>() + .join(key_separator) + ); + let palette = help.style.colors; + let supports_arrow_fonts = !help.capabilities.arrow_fonts; + let ribbon_bg = palette.ribbon_unselected.emphasis_1; + let base = style!(palette.ribbon_unselected.base, ribbon_bg).bold(); + let emph = style!(palette.ribbon_unselected.emphasis_0, ribbon_bg).bold(); + let body = format!( + "{}{}{}", + base.paint(" <").to_string(), + emph.paint(key_string.clone()).to_string(), + base.paint(format!("> {} ", text)).to_string(), + ); + ret.part = hovered_ribbon_wrap(body, palette, supports_arrow_fonts); + ret.len += if supports_arrow_fonts { + text.width() + key_string.width() + 7 + } else { + text.width() + key_string.width() + 5 + }; + ret +} + fn add_shortcut( help: &ModeInfo, text: &str, diff --git a/default-plugins/tab-bar/src/line.rs b/default-plugins/tab-bar/src/line.rs index aa76ce27..99b85f05 100644 --- a/default-plugins/tab-bar/src/line.rs +++ b/default-plugins/tab-bar/src/line.rs @@ -224,7 +224,10 @@ pub fn tab_line( mode_info: &ModeInfo, hide_swap_layout_indicator: bool, background: &PaletteColor, -) -> Vec<LinePart> { + active_pane_scroll: Option<(usize, usize)>, + is_alternate_tab: bool, + new_tab_button_is_hovered: bool, +) -> (Vec<LinePart>, Option<(usize, usize)>) { let mut tabs_after_active = all_tabs.split_off(active_tab_index); let mut tabs_before_active = all_tabs; let active_tab = if !tabs_after_active.is_empty() { @@ -249,6 +252,9 @@ pub fn tab_line( ) }) }; + if swap_layout_indicator.is_none() { + swap_layout_indicator = active_pane_scroll.map(|scroll| scroll_status(scroll, palette)); + } // Drop indicator if it would cause wrapping (active tab always rendered unconditionally) let prefix_len = get_current_title_len(&prefix); @@ -279,6 +285,33 @@ pub fn tab_line( tab_index: None, }]); + let supports_arrow_fonts = !capabilities.arrow_fonts; + let new_tab_button_background = if supports_arrow_fonts { + if new_tab_button_is_hovered { + palette.ribbon_unselected.emphasis_1 + } else { + palette.ribbon_unselected.background + } + } else if is_alternate_tab { + palette.ribbon_unselected.emphasis_1 + } else { + palette.ribbon_unselected.background + }; + let new_tab_button = new_tab_button_line_part(palette, capabilities, new_tab_button_background); + let used = prefix.iter().map(|p| p.len).sum::<usize>(); + let current_indicator_len = swap_layout_indicator.as_ref().map(|s| s.len).unwrap_or(0); + let room = cols + .saturating_sub(used) + .saturating_sub(current_indicator_len); + let new_tab_button_range = if room >= new_tab_button.len { + let button_start = used; + let button_end = button_start + new_tab_button.len; + prefix.push(new_tab_button); + Some((button_start, button_end)) + } else { + None + }; + if let Some(mut swap_layout_indicator) = swap_layout_indicator.take() { let remaining_space = cols .saturating_sub(prefix.iter().fold(0, |len, part| len + part.len)) @@ -301,7 +334,7 @@ pub fn tab_line( prefix.push(swap_layout_indicator); } - prefix + (prefix, new_tab_button_range) } fn swap_layout_status( @@ -329,6 +362,43 @@ fn swap_layout_status( } } +fn scroll_status(scroll: (usize, usize), palette: Styling) -> LinePart { + let (position, length) = scroll; + let text = format!("[ SCROLL {}/{} ]", position, length); + let text_color = palette.text_unselected.base; + let bg_color = palette.text_unselected.background; + let styled_text = style!(text_color, bg_color).bold().paint(&text); + LinePart { + part: styled_text.to_string(), + len: text.width(), + tab_index: None, + } +} + +fn new_tab_button_line_part( + palette: Styling, + capabilities: PluginCapabilities, + background_color: PaletteColor, +) -> LinePart { + let separator = tab_separator(capabilities); + let separator_width = separator.width(); + let foreground_color = palette.ribbon_unselected.base; + let separator_fill_color = palette.text_unselected.background; + let left_separator = style!(separator_fill_color, background_color).paint(separator); + let right_separator = style!(background_color, separator_fill_color).paint(separator); + let text = "+"; + let styled_text = style!(foreground_color, background_color) + .bold() + .paint(format!(" {} ", text)); + let part = ANSIStrings(&[left_separator, styled_text, right_separator]).to_string(); + let len = text.width() + (separator_width * 2) + 2; + LinePart { + part, + len, + tab_index: None, + } +} + pub fn ribbon_as_line_part(text: &str, is_selected: bool, supports_arrow_fonts: bool) -> LinePart { let ribbon_text = if is_selected { Text::new(text).selected() diff --git a/default-plugins/tab-bar/src/main.rs b/default-plugins/tab-bar/src/main.rs index 6a3db351..6fa92c75 100644 --- a/default-plugins/tab-bar/src/main.rs +++ b/default-plugins/tab-bar/src/main.rs @@ -33,6 +33,10 @@ struct State { tab_line: Vec<LinePart>, hide_swap_layout_indication: bool, cached_keybinds: KeybindsVec, + active_pane_scroll: Option<(usize, usize)>, + new_tab_button_range: Option<(usize, usize)>, + hovered_tab_idx: Option<usize>, + hovered_new_tab_button: bool, } static ARROW_SEPARATOR: &str = ""; @@ -51,6 +55,7 @@ impl ZellijPlugin for State { EventType::ModeUpdate, EventType::Mouse, EventType::InitialKeybinds, + EventType::ActivePaneScroll, ]); } @@ -89,13 +94,48 @@ impl ZellijPlugin for State { eprintln!("Could not find active tab."); } }, + Event::ActivePaneScroll(scroll) => { + if self.active_pane_scroll != scroll { + should_render = true; + } + self.active_pane_scroll = scroll; + }, Event::Mouse(me) => match me { Mouse::LeftClick(_, col) => { + if let Some((start, end)) = self.new_tab_button_range { + if col >= start && col < end { + new_tab::<&str>(None, None); + return should_render; + } + } let tab_to_focus = get_tab_to_focus(&self.tab_line, self.active_tab_idx, col); if let Some(idx) = tab_to_focus { switch_tab_to(idx.try_into().unwrap()); } }, + Mouse::Hover(_, col) => { + let simplified_ui = self.mode_info.capabilities.arrow_fonts; + let mut new_hovered_new_tab_button = false; + let mut new_hovered_tab_idx = None; + if !simplified_ui { + if let Some((start, end)) = self.new_tab_button_range { + if col >= start && col < end { + new_hovered_new_tab_button = true; + } + } + if !new_hovered_new_tab_button { + new_hovered_tab_idx = + get_tab_to_focus(&self.tab_line, self.active_tab_idx, col); + } + } + if self.hovered_new_tab_button != new_hovered_new_tab_button + || self.hovered_tab_idx != new_hovered_tab_idx + { + self.hovered_new_tab_button = new_hovered_new_tab_button; + self.hovered_tab_idx = new_hovered_tab_idx; + should_render = true; + } + }, Mouse::ScrollUp(_) => { switch_tab_to(min(self.active_tab_idx + 1, self.tabs.len()) as u32); }, @@ -133,10 +173,12 @@ impl ZellijPlugin for State { } else if t.active { active_tab_index = t.position; } + let is_hovered = self.hovered_tab_idx == Some(t.position + 1); let tab = tab_style( tabname, t, is_alternate_tab, + is_hovered, self.mode_info.style.colors, self.mode_info.capabilities, ); @@ -146,7 +188,7 @@ impl ZellijPlugin for State { let background = self.mode_info.style.colors.text_unselected.background; - self.tab_line = tab_line( + let (line, new_tab_button_range) = tab_line( self.mode_info.session_name.as_deref(), all_tabs, active_tab_index, @@ -158,7 +200,12 @@ impl ZellijPlugin for State { &self.mode_info, self.hide_swap_layout_indication, &background, + self.active_pane_scroll, + is_alternate_tab, + self.hovered_new_tab_button, ); + self.tab_line = line; + self.new_tab_button_range = new_tab_button_range; let output = self .tab_line diff --git a/default-plugins/tab-bar/src/tab.rs b/default-plugins/tab-bar/src/tab.rs index d211d142..3db1bbb6 100644 --- a/default-plugins/tab-bar/src/tab.rs +++ b/default-plugins/tab-bar/src/tab.rs @@ -24,6 +24,7 @@ pub fn render_tab( text: String, tab: &TabInfo, is_alternate_tab: bool, + is_hovered: bool, palette: Styling, separator: &str, ) -> LinePart { @@ -37,6 +38,8 @@ pub fn render_tab( }; let background_color = if tab.active { palette.ribbon_selected.background + } else if is_hovered { + palette.ribbon_unselected.emphasis_1 } else if is_alternate_tab { alternate_tab_color } else { @@ -98,6 +101,7 @@ pub fn tab_style( mut tabname: String, tab: &TabInfo, mut is_alternate_tab: bool, + is_hovered: bool, palette: Styling, capabilities: PluginCapabilities, ) -> LinePart { @@ -116,7 +120,14 @@ pub fn tab_style( is_alternate_tab = false; } - render_tab(tabname, tab, is_alternate_tab, palette, separator) + render_tab( + tabname, + tab, + is_alternate_tab, + is_hovered, + palette, + separator, + ) } pub(crate) fn get_tab_to_focus( |
