mirror of
https://github.com/herdrdev/herdr.git
synced 2026-09-22 00:01:06 +00:00
fix: tighten tab bar scrolling behavior
This commit is contained in:
+9
-42
@@ -4596,7 +4596,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clicking_rightmost_visible_tab_recenters_it_when_tabs_overflow() {
|
||||
fn clicking_last_visible_tab_at_right_edge_does_not_overscroll() {
|
||||
let mut app = app_for_mouse_test();
|
||||
let mut ws = Workspace::test_new("test");
|
||||
for name in [
|
||||
@@ -4607,28 +4607,14 @@ mod tests {
|
||||
app.state.workspaces = vec![ws];
|
||||
app.state.active = Some(0);
|
||||
app.state.selected = 0;
|
||||
app.state.tab_scroll = 2;
|
||||
app.state.tab_scroll = usize::MAX;
|
||||
app.state.tab_scroll_follow_active = false;
|
||||
crate::ui::compute_view(&mut app.state, Rect::new(0, 0, 52, 20));
|
||||
|
||||
let clicked_idx = app
|
||||
.state
|
||||
.view
|
||||
.tab_hit_areas
|
||||
.iter()
|
||||
.enumerate()
|
||||
.rev()
|
||||
.find(|(_, rect)| rect.width > 0)
|
||||
.map(|(idx, _)| idx)
|
||||
.expect("rightmost visible tab");
|
||||
let target = app.state.view.tab_hit_areas[clicked_idx];
|
||||
let first_visible_before = app
|
||||
.state
|
||||
.view
|
||||
.tab_hit_areas
|
||||
.iter()
|
||||
.position(|rect| rect.width > 0)
|
||||
.expect("first visible tab before click");
|
||||
let last_idx = app.state.workspaces[0].tabs.len() - 1;
|
||||
let target = app.state.view.tab_hit_areas[last_idx];
|
||||
let clamped_scroll = app.state.tab_scroll;
|
||||
assert!(target.width > 0, "last tab should already be visible");
|
||||
|
||||
app.handle_mouse(mouse(
|
||||
MouseEventKind::Down(MouseButton::Left),
|
||||
@@ -4641,28 +4627,9 @@ mod tests {
|
||||
target.y,
|
||||
));
|
||||
|
||||
let last_visible_after = app
|
||||
.state
|
||||
.view
|
||||
.tab_hit_areas
|
||||
.iter()
|
||||
.enumerate()
|
||||
.rev()
|
||||
.find(|(_, rect)| rect.width > 0)
|
||||
.map(|(idx, _)| idx)
|
||||
.expect("rightmost visible tab after click");
|
||||
|
||||
assert_eq!(app.state.workspaces[0].active_tab, clicked_idx);
|
||||
assert!(app.state.tab_scroll > 2);
|
||||
assert!(app.state.view.tab_hit_areas[clicked_idx].width > 0);
|
||||
assert!(last_visible_after > clicked_idx);
|
||||
assert!(app
|
||||
.state
|
||||
.view
|
||||
.tab_hit_areas
|
||||
.iter()
|
||||
.position(|rect| rect.width > 0)
|
||||
.is_some_and(|idx| idx > first_visible_before));
|
||||
assert_eq!(app.state.workspaces[0].active_tab, last_idx);
|
||||
assert_eq!(app.state.tab_scroll, clamped_scroll);
|
||||
assert!(app.state.view.tab_hit_areas[last_idx].width > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -573,6 +573,25 @@ fn centered_tab_scroll(ws: &crate::workspace::Workspace, area: Rect) -> usize {
|
||||
best_scroll
|
||||
}
|
||||
|
||||
fn trailing_tab_controls_x(tab_hit_areas: &[Rect], fallback_x: u16) -> u16 {
|
||||
tab_hit_areas
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|rect| rect.width > 0)
|
||||
.map(|rect| rect.x + rect.width)
|
||||
.unwrap_or(fallback_x)
|
||||
}
|
||||
|
||||
fn max_tab_scroll(ws: &crate::workspace::Workspace, area: Rect) -> usize {
|
||||
(0..ws.tabs.len())
|
||||
.find(|&scroll| {
|
||||
layout_tab_hit_areas(ws, area, scroll)
|
||||
.last()
|
||||
.is_some_and(|rect| rect.width > 0)
|
||||
})
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
pub(crate) fn compute_tab_bar_view(
|
||||
ws: &crate::workspace::Workspace,
|
||||
area: Rect,
|
||||
@@ -583,13 +602,7 @@ pub(crate) fn compute_tab_bar_view(
|
||||
return TabBarView::default();
|
||||
}
|
||||
|
||||
let new_tab_hit_area = Rect::new(
|
||||
area.x + area.width.saturating_sub(NEW_TAB_WIDTH),
|
||||
area.y,
|
||||
NEW_TAB_WIDTH.min(area.width),
|
||||
1,
|
||||
);
|
||||
|
||||
let area_right = area.x + area.width;
|
||||
let all_tabs_area = Rect::new(
|
||||
area.x,
|
||||
area.y,
|
||||
@@ -599,6 +612,13 @@ pub(crate) fn compute_tab_bar_view(
|
||||
let all_tabs = layout_tab_hit_areas(ws, all_tabs_area, 0);
|
||||
let overflow = all_tabs.iter().any(|rect| rect.width == 0);
|
||||
if !overflow {
|
||||
let new_tab_x = trailing_tab_controls_x(&all_tabs, area.x);
|
||||
let new_tab_hit_area = Rect::new(
|
||||
new_tab_x,
|
||||
area.y,
|
||||
area_right.saturating_sub(new_tab_x).min(NEW_TAB_WIDTH),
|
||||
1,
|
||||
);
|
||||
return TabBarView {
|
||||
scroll: 0,
|
||||
tab_hit_areas: all_tabs,
|
||||
@@ -609,14 +629,9 @@ pub(crate) fn compute_tab_bar_view(
|
||||
}
|
||||
|
||||
let left_hit_area = Rect::new(area.x, area.y, TAB_SCROLL_BUTTON_WIDTH.min(area.width), 1);
|
||||
let right_hit_area = Rect::new(
|
||||
new_tab_hit_area.x.saturating_sub(TAB_SCROLL_BUTTON_WIDTH),
|
||||
area.y,
|
||||
TAB_SCROLL_BUTTON_WIDTH.min(new_tab_hit_area.x.saturating_sub(area.x)),
|
||||
1,
|
||||
);
|
||||
let tab_area_x = left_hit_area.x + left_hit_area.width;
|
||||
let tab_area_right = right_hit_area.x;
|
||||
let reserved_trailing_width = NEW_TAB_WIDTH.saturating_add(TAB_SCROLL_BUTTON_WIDTH);
|
||||
let tab_area_right = area_right.saturating_sub(reserved_trailing_width);
|
||||
let tab_area = Rect::new(
|
||||
tab_area_x,
|
||||
area.y,
|
||||
@@ -624,15 +639,33 @@ pub(crate) fn compute_tab_bar_view(
|
||||
area.height,
|
||||
);
|
||||
|
||||
let max_scroll = max_tab_scroll(ws, tab_area);
|
||||
let scroll = if follow_active {
|
||||
centered_tab_scroll(ws, tab_area)
|
||||
centered_tab_scroll(ws, tab_area).min(max_scroll)
|
||||
} else {
|
||||
current_scroll.min(ws.tabs.len().saturating_sub(1))
|
||||
current_scroll.min(max_scroll)
|
||||
};
|
||||
let tab_hit_areas = layout_tab_hit_areas(ws, tab_area, scroll);
|
||||
let trailing_x = trailing_tab_controls_x(&tab_hit_areas, tab_area_x).min(tab_area_right);
|
||||
let right_hit_area = Rect::new(
|
||||
trailing_x,
|
||||
area.y,
|
||||
area_right
|
||||
.saturating_sub(trailing_x)
|
||||
.min(TAB_SCROLL_BUTTON_WIDTH),
|
||||
1,
|
||||
);
|
||||
let new_tab_x = right_hit_area.x + right_hit_area.width;
|
||||
let new_tab_hit_area = Rect::new(
|
||||
new_tab_x,
|
||||
area.y,
|
||||
area_right.saturating_sub(new_tab_x).min(NEW_TAB_WIDTH),
|
||||
1,
|
||||
);
|
||||
|
||||
TabBarView {
|
||||
scroll,
|
||||
tab_hit_areas: layout_tab_hit_areas(ws, tab_area, scroll),
|
||||
tab_hit_areas,
|
||||
scroll_left_hit_area: left_hit_area,
|
||||
scroll_right_hit_area: right_hit_area,
|
||||
new_tab_hit_area,
|
||||
@@ -3449,6 +3482,34 @@ mod tests {
|
||||
assert!(custom_style.add_modifier.contains(Modifier::BOLD));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_tab_button_tracks_rightmost_tab_when_tabs_fit() {
|
||||
let mut app = crate::app::state::AppState::test_new();
|
||||
let mut ws = Workspace::test_new("test");
|
||||
ws.test_add_tab(Some("logs"));
|
||||
|
||||
app.workspaces = vec![ws];
|
||||
app.active = Some(0);
|
||||
app.selected = 0;
|
||||
app.mode = Mode::Terminal;
|
||||
|
||||
compute_view(&mut app, Rect::new(0, 0, 80, 20));
|
||||
|
||||
let last_visible = app
|
||||
.view
|
||||
.tab_hit_areas
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|rect| rect.width > 0)
|
||||
.copied()
|
||||
.expect("last visible tab");
|
||||
|
||||
assert_eq!(
|
||||
app.view.new_tab_hit_area.x,
|
||||
last_visible.x + last_visible.width
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tab_bar_shows_scroll_controls_when_tabs_overflow() {
|
||||
let mut app = crate::app::state::AppState::test_new();
|
||||
@@ -3472,6 +3533,53 @@ mod tests {
|
||||
assert_eq!(app.view.tab_hit_areas[1].width, 0);
|
||||
assert!(app.view.tab_hit_areas[2].width > 0);
|
||||
assert!(app.view.new_tab_hit_area.width > 0);
|
||||
|
||||
let last_visible = app
|
||||
.view
|
||||
.tab_hit_areas
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|rect| rect.width > 0)
|
||||
.copied()
|
||||
.expect("last visible tab");
|
||||
|
||||
assert_eq!(
|
||||
app.view.tab_scroll_right_hit_area.x,
|
||||
last_visible.x + last_visible.width
|
||||
);
|
||||
assert_eq!(
|
||||
app.view.new_tab_hit_area.x,
|
||||
app.view.tab_scroll_right_hit_area.x + app.view.tab_scroll_right_hit_area.width
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tab_bar_clamps_manual_scroll_at_last_visible_tab() {
|
||||
let mut app = crate::app::state::AppState::test_new();
|
||||
let mut ws = Workspace::test_new("test");
|
||||
for name in [
|
||||
"one", "two", "three", "four", "five", "six", "seven", "eight",
|
||||
] {
|
||||
ws.test_add_tab(Some(name));
|
||||
}
|
||||
|
||||
app.workspaces = vec![ws];
|
||||
app.active = Some(0);
|
||||
app.selected = 0;
|
||||
app.mode = Mode::Terminal;
|
||||
app.tab_scroll_follow_active = false;
|
||||
app.tab_scroll = usize::MAX;
|
||||
|
||||
compute_view(&mut app, Rect::new(0, 0, 52, 20));
|
||||
|
||||
let last_idx = app.workspaces[0].tabs.len() - 1;
|
||||
assert!(app.view.tab_hit_areas[last_idx].width > 0);
|
||||
let clamped_scroll = app.tab_scroll;
|
||||
|
||||
app.scroll_tabs_right();
|
||||
|
||||
assert_eq!(app.tab_scroll, clamped_scroll);
|
||||
assert!(app.view.tab_hit_areas[last_idx].width > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user