aboutsummaryrefslogtreecommitdiff
path: root/src/ui.rs
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/ui.rs
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/ui.rs')
-rw-r--r--src/ui.rs85
1 files changed, 78 insertions, 7 deletions
diff --git a/src/ui.rs b/src/ui.rs
index bf67d9fd..60143fe4 100644
--- a/src/ui.rs
+++ b/src/ui.rs
@@ -195,9 +195,18 @@ fn desktop_tab_bar_and_terminal_area(
) -> (Rect, Rect) {
let hide_single_tab_bar = app.hide_tab_bar_when_single_tab && ws.tabs.len() == 1;
if !hide_single_tab_bar && main_area.height > 1 {
- let [tab_bar_rect, terminal_area] =
- Layout::vertical([Constraint::Length(1), Constraint::Min(1)]).areas(main_area);
- (tab_bar_rect, terminal_area)
+ match app.tab_bar_position {
+ crate::config::TabBarPositionConfig::Top => {
+ let [tab_bar_rect, terminal_area] =
+ Layout::vertical([Constraint::Length(1), Constraint::Min(1)]).areas(main_area);
+ (tab_bar_rect, terminal_area)
+ }
+ crate::config::TabBarPositionConfig::Bottom => {
+ let [terminal_area, tab_bar_rect] =
+ Layout::vertical([Constraint::Min(1), Constraint::Length(1)]).areas(main_area);
+ (tab_bar_rect, terminal_area)
+ }
+ }
} else {
(Rect::default(), main_area)
}
@@ -410,6 +419,15 @@ pub fn render_with_runtime_registry(
render_notifications(app, frame, terminal_area);
render_popup_pane(app, terminal_runtimes, frame, terminal_area);
+ let mode_bar_area = if app.view.layout == ViewLayout::Desktop
+ && app.tab_bar_position == crate::config::TabBarPositionConfig::Bottom
+ && tab_bar_area.height > 0
+ {
+ tab_bar_area
+ } else {
+ terminal_area
+ };
+
match app.mode {
Mode::Onboarding => render_onboarding_overlay(app, frame, frame.area()),
Mode::ReleaseNotes => render_release_notes_overlay(app, frame, frame.area()),
@@ -417,10 +435,10 @@ pub fn render_with_runtime_registry(
Mode::Navigate if app.view.layout == ViewLayout::Mobile => {
render_mobile_panel(app, terminal_runtimes, frame, frame.area())
}
- Mode::Navigate => render_navigate_overlay(app, frame, terminal_area),
- Mode::Prefix => render_prefix_overlay(app, frame, terminal_area),
- Mode::Copy => render_copy_mode_overlay(app, frame, terminal_area),
- Mode::Resize => render_resize_overlay(app, frame, terminal_area),
+ Mode::Navigate => render_navigate_overlay(app, frame, mode_bar_area),
+ Mode::Prefix => render_prefix_overlay(app, frame, mode_bar_area),
+ Mode::Copy => render_copy_mode_overlay(app, frame, mode_bar_area),
+ Mode::Resize => render_resize_overlay(app, frame, mode_bar_area),
Mode::ConfirmClose => {
render_confirm_close_overlay(app, terminal_runtimes, frame, terminal_area)
}
@@ -794,6 +812,35 @@ mod tests {
}
#[test]
+ fn desktop_tab_bar_position_controls_geometry_and_mode_bar_placement() {
+ let mut app = crate::app::state::AppState::test_new();
+ app.workspaces = vec![Workspace::test_new("one")];
+ app.active = Some(0);
+ app.selected = 0;
+ app.mode = Mode::Prefix;
+
+ compute_view(&mut app, Rect::new(0, 0, 80, 20));
+ assert_eq!(app.view.tab_bar_rect, Rect::new(26, 0, 54, 1));
+ assert_eq!(app.view.terminal_area, Rect::new(26, 1, 54, 19));
+
+ app.tab_bar_position = crate::config::TabBarPositionConfig::Bottom;
+ compute_view(&mut app, Rect::new(0, 0, 80, 20));
+ assert_eq!(app.view.terminal_area, Rect::new(26, 0, 54, 19));
+ assert_eq!(app.view.tab_bar_rect, Rect::new(26, 19, 54, 1));
+ assert!(app.view.tab_hit_areas.iter().all(|rect| rect.y == 19));
+ assert_eq!(app.view.new_tab_hit_area.y, 19);
+
+ let mut terminal = Terminal::new(TestBackend::new(80, 20)).unwrap();
+ terminal.draw(|frame| render(&app, frame)).unwrap();
+ let mode_row = buffer_row_text(
+ terminal.backend().buffer(),
+ app.view.tab_bar_rect,
+ app.view.tab_bar_rect.y,
+ );
+ assert!(mode_row.contains("PREFIX"), "{mode_row}");
+ }
+
+ #[test]
fn hide_tab_bar_when_single_tab_toggles_geometry_with_tab_count() {
let mut app = crate::app::state::AppState::test_new();
app.hide_tab_bar_when_single_tab = true;
@@ -827,6 +874,30 @@ mod tests {
assert_eq!(app.view.new_tab_hit_area, Rect::default());
}
+ #[test]
+ fn bottom_tab_bar_still_hides_when_single_tab() {
+ let mut app = crate::app::state::AppState::test_new();
+ app.hide_tab_bar_when_single_tab = true;
+ app.tab_bar_position = crate::config::TabBarPositionConfig::Bottom;
+ app.workspaces = vec![Workspace::test_new("one")];
+ app.active = Some(0);
+ app.selected = 0;
+ app.mode = Mode::Prefix;
+
+ compute_view(&mut app, Rect::new(0, 0, 80, 20));
+ assert_eq!(app.view.tab_bar_rect, Rect::default());
+ assert_eq!(app.view.terminal_area, Rect::new(26, 0, 54, 20));
+
+ let mut terminal = Terminal::new(TestBackend::new(80, 20)).unwrap();
+ terminal.draw(|frame| render(&app, frame)).unwrap();
+ let mode_row = buffer_row_text(
+ terminal.backend().buffer(),
+ app.view.terminal_area,
+ app.view.terminal_area.y + app.view.terminal_area.height - 1,
+ );
+ assert!(mode_row.contains("PREFIX"), "{mode_row}");
+ }
+
#[tokio::test]
async fn hide_tab_bar_when_single_tab_resizes_background_tabs_per_workspace() {
let mut app = crate::app::state::AppState::test_new();