mirror of
https://github.com/herdrdev/herdr.git
synced 2026-11-06 00:06:18 +00:00
fix: keep sidebar collapse clickable with agent scrollbars (#4734)
* fix: keep sidebar collapse clickable with agent scrollbars refs #4692 * docs: document sidebar mouse handling and regression coverage refs #4692
This commit is contained in:
+15
-12
@@ -676,6 +676,10 @@ impl ClientShellState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Routes mouse input through overlays, shell controls, and pane interactions.
|
||||
///
|
||||
/// Hit-test order determines which overlapping control receives the event;
|
||||
/// the sidebar toggle takes precedence over the agent scrollbar beneath it.
|
||||
pub(super) fn handle_mouse(&mut self, mouse: MouseEvent, outcome: &mut ClientShellInput) {
|
||||
self.update_link_hover(mouse, outcome);
|
||||
let point = (mouse.column, mouse.row);
|
||||
@@ -1935,9 +1939,17 @@ impl ClientShellState {
|
||||
self.workspace_press = None;
|
||||
self.tab_press = None;
|
||||
self.chrome_drag = None;
|
||||
if super::contains(self.hits.sidebar_divider, point)
|
||||
&& !super::contains(self.hits.sidebar_toggle, point)
|
||||
{
|
||||
// The toggle is painted over the agent scrollbar's last cell.
|
||||
if super::contains(self.hits.sidebar_toggle, point) {
|
||||
self.sidebar_collapsed = !self.sidebar_collapsed;
|
||||
self.sidebar_collapsed_manual = true;
|
||||
self.invalidate_pane_surface();
|
||||
outcome.repaint = true;
|
||||
outcome.resize = true;
|
||||
self.persist_chrome_preferences(outcome);
|
||||
return;
|
||||
}
|
||||
if super::contains(self.hits.sidebar_divider, point) {
|
||||
let now = std::time::Instant::now();
|
||||
let double_click = self.last_sidebar_divider_click.is_some_and(|last| {
|
||||
now.duration_since(last) <= std::time::Duration::from_millis(350)
|
||||
@@ -2075,15 +2087,6 @@ impl ClientShellState {
|
||||
outcome.repaint = true;
|
||||
return;
|
||||
}
|
||||
if super::contains(self.hits.sidebar_toggle, point) {
|
||||
self.sidebar_collapsed = !self.sidebar_collapsed;
|
||||
self.sidebar_collapsed_manual = true;
|
||||
self.invalidate_pane_surface();
|
||||
outcome.repaint = true;
|
||||
outcome.resize = true;
|
||||
self.persist_chrome_preferences(outcome);
|
||||
return;
|
||||
}
|
||||
let group_toggle = self.hits.workspaces.iter().find_map(|hit| {
|
||||
let (rect, key) = hit.group_toggle.as_ref()?;
|
||||
super::contains(*rect, point).then(|| (hit.endpoint_id.clone(), key.clone()))
|
||||
|
||||
@@ -247,6 +247,54 @@ fn state_with_scrollable_agents() -> (ClientShellState, ClientEndpointId) {
|
||||
(state, remote)
|
||||
}
|
||||
|
||||
/// Checks that the visible toggle wins overlapping scrollbar clicks and can reopen
|
||||
/// the sidebar, for both endpoint layouts and both ends of the overflowing list.
|
||||
#[test]
|
||||
fn sidebar_toggle_remains_clickable_with_overflowing_agents() {
|
||||
for saved_machine in [false, true] {
|
||||
for scroll_to_bottom in [false, true] {
|
||||
let (mut state, _) = state_with_scrollable_agents();
|
||||
if !saved_machine {
|
||||
state.set_endpoint_catalog(&[]);
|
||||
}
|
||||
state.agent_scroll = if scroll_to_bottom { usize::MAX } else { 0 };
|
||||
let frame = state.compose(100, 28).expect("overflowing agent panel");
|
||||
assert!(!state.hits.agent_scrollbar.is_empty());
|
||||
let scroll = state.agent_scroll;
|
||||
|
||||
let toggle = state.hits.sidebar_toggle;
|
||||
let buffer = frame.to_ratatui_buffer().expect("sidebar buffer");
|
||||
assert_eq!(buffer[(toggle.x, toggle.y)].symbol(), "«");
|
||||
let outcome = state.handle_raw_events(vec![RawInputEvent::Mouse(MouseEvent {
|
||||
kind: MouseEventKind::Down(MouseButton::Left),
|
||||
column: toggle.x,
|
||||
row: toggle.y,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
})]);
|
||||
assert!(
|
||||
state.sidebar_collapsed,
|
||||
"collapse with saved_machine={saved_machine}, scroll_to_bottom={scroll_to_bottom}"
|
||||
);
|
||||
assert!(state.sidebar_collapsed_manual);
|
||||
assert!(outcome.repaint && outcome.resize);
|
||||
assert_eq!(state.agent_scroll, scroll);
|
||||
assert!(state.chrome_drag.is_none());
|
||||
|
||||
state.compose(100, 28).expect("collapsed sidebar");
|
||||
let toggle = state.hits.sidebar_toggle;
|
||||
let outcome = state.handle_raw_events(vec![RawInputEvent::Mouse(MouseEvent {
|
||||
kind: MouseEventKind::Down(MouseButton::Left),
|
||||
column: toggle.x,
|
||||
row: toggle.y,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
})]);
|
||||
assert!(!state.sidebar_collapsed);
|
||||
assert!(outcome.repaint && outcome.resize);
|
||||
assert!(state.chrome_drag.is_none());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_navigation_reveals_offscreen_targets() {
|
||||
use crate::input::KeybindAction;
|
||||
|
||||
Reference in New Issue
Block a user