aboutsummaryrefslogtreecommitdiff
path: root/src/app
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-07-31 18:15:55 +0300
committerGitHub <noreply@github.com>2026-07-31 18:15:55 +0300
commit02fe7d7659e37d9f05eed80af382d1dcdccb6876 (patch)
tree0ca1ad4e912b36eb3e3294501cc73ac8a0d9466f /src/app
parentb1d14fe984c92ab4a81438ba07c7d94b464d0d3a (diff)
feat: support bottom tab bar placement (#2118)
* feat: support bottom tab bar placement * fix: preserve mouse cleanup under bottom mode bar refs #2117
Diffstat (limited to 'src/app')
-rw-r--r--src/app/input/mouse.rs76
-rw-r--r--src/app/mod.rs2
-rw-r--r--src/app/state.rs6
3 files changed, 82 insertions, 2 deletions
diff --git a/src/app/input/mouse.rs b/src/app/input/mouse.rs
index 56b9b676..2c32c95a 100644
--- a/src/app/input/mouse.rs
+++ b/src/app/input/mouse.rs
@@ -483,6 +483,10 @@ impl AppState {
}
}
+ if self.mode_bar_covers_tab_row(mouse.column, mouse.row) {
+ return None;
+ }
+
if self.on_tab_scroll_left_button(mouse.column, mouse.row) {
self.scroll_tabs_left();
return None;
@@ -913,6 +917,9 @@ impl AppState {
}
MouseEventKind::ScrollUp | MouseEventKind::ScrollDown
+ if self.mode_bar_covers_tab_row(mouse.column, mouse.row) => {}
+
+ MouseEventKind::ScrollUp | MouseEventKind::ScrollDown
if self.on_tab_bar(mouse.column, mouse.row) =>
{
match mouse.kind {
@@ -1063,7 +1070,8 @@ impl AppState {
}
MouseEventKind::Down(MouseButton::Right)
- if self.tab_at(mouse.column, mouse.row).is_some() =>
+ if !self.mode_bar_covers_tab_row(mouse.column, mouse.row)
+ && self.tab_at(mouse.column, mouse.row).is_some() =>
{
if let (Some(ws_idx), Some(tab_idx)) =
(self.active, self.tab_at(mouse.column, mouse.row))
@@ -1272,6 +1280,15 @@ impl AppState {
})
}
+ fn mode_bar_covers_tab_row(&self, col: u16, row: u16) -> bool {
+ self.tab_bar_position == crate::config::TabBarPositionConfig::Bottom
+ && matches!(
+ self.mode,
+ Mode::Navigate | Mode::Prefix | Mode::Copy | Mode::Resize
+ )
+ && self.on_tab_bar(col, row)
+ }
+
pub(super) fn on_tab_bar(&self, col: u16, row: u16) -> bool {
let area = self.view.tab_bar_rect;
area.width > 0
@@ -3393,6 +3410,63 @@ mod tests {
}
#[test]
+ fn bottom_mode_bar_consumes_hidden_tab_mouse_actions() {
+ let mut app = app_for_mouse_test();
+ let mut ws = Workspace::test_new("one");
+ ws.test_add_tab(Some("two"));
+ app.state.workspaces = vec![ws];
+ app.state.active = Some(0);
+ app.state.selected = 0;
+ app.state.mode = Mode::Prefix;
+ app.state.tab_bar_position = crate::config::TabBarPositionConfig::Bottom;
+
+ crate::ui::compute_view(&mut app.state, Rect::new(0, 0, 106, 20));
+ let second_tab = app.state.view.tab_hit_areas[1];
+ let new_tab = app.state.view.new_tab_hit_area;
+
+ app.handle_mouse(mouse(
+ MouseEventKind::Down(MouseButton::Left),
+ second_tab.x,
+ second_tab.y,
+ ));
+ app.handle_mouse(mouse(
+ MouseEventKind::Up(MouseButton::Left),
+ second_tab.x,
+ second_tab.y,
+ ));
+ app.handle_mouse(mouse(
+ MouseEventKind::ScrollDown,
+ second_tab.x,
+ second_tab.y,
+ ));
+ app.handle_mouse(mouse(
+ MouseEventKind::Down(MouseButton::Right),
+ second_tab.x,
+ second_tab.y,
+ ));
+ app.handle_mouse(mouse(
+ MouseEventKind::Down(MouseButton::Left),
+ new_tab.x,
+ new_tab.y,
+ ));
+
+ app.state.drag = Some(DragState {
+ target: DragTarget::SidebarDivider,
+ });
+ app.handle_mouse(mouse(
+ MouseEventKind::Up(MouseButton::Left),
+ second_tab.x,
+ second_tab.y,
+ ));
+
+ assert_eq!(app.state.workspaces[0].active_tab, 0);
+ assert_eq!(app.state.workspaces[0].tabs.len(), 2);
+ assert!(app.state.context_menu.is_none());
+ assert!(app.state.tab_press.is_none());
+ assert!(app.state.drag.is_none());
+ }
+
+ #[test]
fn right_click_inactive_tab_opens_menu_without_switching_tabs() {
let mut app = app_for_mouse_test();
let mut ws = Workspace::test_new("one");
diff --git a/src/app/mod.rs b/src/app/mod.rs
index 2530a05b..f4bc5008 100644
--- a/src/app/mod.rs
+++ b/src/app/mod.rs
@@ -642,6 +642,7 @@ impl App {
pane_gaps: config.ui.pane_gaps,
show_agent_labels_on_pane_borders: config.ui.show_agent_labels_on_pane_borders,
hide_tab_bar_when_single_tab: config.ui.hide_tab_bar_when_single_tab,
+ tab_bar_position: config.ui.tab_bar_position,
pane_history_persistence: config.experimental.pane_history,
reveal_hidden_cursor_for_cjk_ime: config.experimental.reveal_hidden_cursor_for_cjk_ime,
cjk_ime_agent_filter_configured: !config.experimental.cjk_ime_agents.is_empty(),
@@ -1450,6 +1451,7 @@ impl App {
self.state.show_agent_labels_on_pane_borders =
config.ui.show_agent_labels_on_pane_borders;
self.state.hide_tab_bar_when_single_tab = config.ui.hide_tab_bar_when_single_tab;
+ self.state.tab_bar_position = config.ui.tab_bar_position;
self.state.agent_panel_sort =
agent_panel_sort_from_config(config.ui.agent_panel_sort);
self.state.sidebar_agents = config.ui.sidebar.agents.clone();
diff --git a/src/app/state.rs b/src/app/state.rs
index a1c5afc1..975144b4 100644
--- a/src/app/state.rs
+++ b/src/app/state.rs
@@ -1,4 +1,6 @@
-use crate::config::{Keybinds, NewTerminalCwdConfig, SoundConfig, ToastConfig, ToastDelivery};
+use crate::config::{
+ Keybinds, NewTerminalCwdConfig, SoundConfig, TabBarPositionConfig, ToastConfig, ToastDelivery,
+};
use crossterm::event::{KeyCode, KeyModifiers};
use ratatui::layout::{Direction, Rect};
use ratatui::style::Color;
@@ -1525,6 +1527,7 @@ pub struct AppState {
pub pane_gaps: bool,
pub show_agent_labels_on_pane_borders: bool,
pub hide_tab_bar_when_single_tab: bool,
+ pub tab_bar_position: TabBarPositionConfig,
pub pane_history_persistence: bool,
/// Expose the focused pane's cursor anchor to the outer terminal even when
/// the pane requested `?25l`. See `[experimental] reveal_hidden_cursor_for_cjk_ime`.
@@ -1899,6 +1902,7 @@ impl AppState {
pane_gaps: false,
show_agent_labels_on_pane_borders: false,
hide_tab_bar_when_single_tab: false,
+ tab_bar_position: TabBarPositionConfig::Top,
pane_history_persistence: false,
reveal_hidden_cursor_for_cjk_ime: false,
cjk_ime_agent_filter_configured: false,