mirror of
https://github.com/herdrdev/herdr.git
synced 2026-09-21 16:01:04 +00:00
1914 lines
71 KiB
Rust
1914 lines
71 KiB
Rust
use bytes::Bytes;
|
|
use crossterm::event::{MouseButton, MouseEvent, MouseEventKind};
|
|
use ratatui::layout::{Direction, Rect};
|
|
use tracing::warn;
|
|
|
|
use crate::{
|
|
app::state::{
|
|
AgentPanelScope, AppState, ContextMenuKind, ContextMenuState, DragState, DragTarget,
|
|
MenuListState, Mode, TabPressState, ViewLayout, WorkspacePressState,
|
|
},
|
|
layout::{PaneInfo, SplitBorder},
|
|
selection::Selection,
|
|
};
|
|
|
|
#[cfg(test)]
|
|
use super::WheelRouting;
|
|
use super::{
|
|
modal::{
|
|
apply_context_menu_action, apply_global_menu_action, apply_rename_action,
|
|
confirm_close_accept, confirm_close_cancel, global_menu_actions, leave_modal,
|
|
modal_action_from_buttons, open_global_menu, open_new_tab_dialog, ModalAction,
|
|
},
|
|
settings::SettingsAction,
|
|
ScrollbarClickTarget, TAB_DRAG_THRESHOLD, WORKSPACE_DRAG_THRESHOLD,
|
|
};
|
|
|
|
impl AppState {
|
|
pub(super) fn handle_mouse(&mut self, mouse: MouseEvent) -> Option<SettingsAction> {
|
|
if self.mode == Mode::Onboarding {
|
|
self.handle_onboarding_mouse(mouse);
|
|
return None;
|
|
}
|
|
|
|
if self.clickable_toast_at(mouse.column, mouse.row)
|
|
&& matches!(mouse.kind, MouseEventKind::Down(MouseButton::Left))
|
|
{
|
|
self.focus_toast_target();
|
|
return None;
|
|
}
|
|
|
|
if self.clickable_toast_at(mouse.column, mouse.row)
|
|
&& matches!(mouse.kind, MouseEventKind::Up(MouseButton::Left))
|
|
{
|
|
return None;
|
|
}
|
|
|
|
if self.mode == Mode::Settings {
|
|
return self.handle_settings_mouse(mouse);
|
|
}
|
|
|
|
let launcher_enabled = self.view.layout != ViewLayout::Mobile
|
|
&& !self.sidebar_collapsed
|
|
&& matches!(
|
|
self.mode,
|
|
Mode::Terminal
|
|
| Mode::Navigate
|
|
| Mode::Resize
|
|
| Mode::GlobalMenu
|
|
| Mode::KeybindHelp
|
|
);
|
|
let launcher = self.global_launcher_rect();
|
|
let launcher_hit = launcher_enabled
|
|
&& mouse.column >= launcher.x
|
|
&& mouse.column < launcher.x + launcher.width
|
|
&& mouse.row >= launcher.y
|
|
&& mouse.row < launcher.y + launcher.height;
|
|
|
|
if matches!(mouse.kind, MouseEventKind::Moved) && self.mode == Mode::GlobalMenu {
|
|
let actions = global_menu_actions(self);
|
|
let hovered = self
|
|
.global_menu_item_at(mouse.column, mouse.row)
|
|
.and_then(|action| actions.iter().position(|item| *item == action));
|
|
self.global_menu.hover(hovered);
|
|
return None;
|
|
}
|
|
|
|
if matches!(mouse.kind, MouseEventKind::Down(MouseButton::Left)) && launcher_hit {
|
|
if self.mode == Mode::GlobalMenu {
|
|
leave_modal(self);
|
|
} else {
|
|
open_global_menu(self);
|
|
}
|
|
return None;
|
|
}
|
|
|
|
if self.mode == Mode::GlobalMenu {
|
|
if matches!(mouse.kind, MouseEventKind::Down(MouseButton::Left)) {
|
|
if let Some(action) = self.global_menu_item_at(mouse.column, mouse.row) {
|
|
apply_global_menu_action(self, action);
|
|
} else {
|
|
leave_modal(self);
|
|
}
|
|
}
|
|
return None;
|
|
}
|
|
|
|
if self.mode == Mode::KeybindHelp {
|
|
return None;
|
|
}
|
|
|
|
if self.view.layout == ViewLayout::Mobile && self.handle_mobile_mouse(mouse) {
|
|
return None;
|
|
}
|
|
|
|
let sidebar = self.view.sidebar_rect;
|
|
let in_sidebar = mouse.column >= sidebar.x
|
|
&& mouse.column < sidebar.x + sidebar.width
|
|
&& mouse.row >= sidebar.y
|
|
&& mouse.row < sidebar.y + sidebar.height;
|
|
|
|
match mouse.kind {
|
|
MouseEventKind::Down(MouseButton::Left) => {
|
|
self.selection = None;
|
|
self.workspace_press = None;
|
|
|
|
if self.mode == Mode::ConfirmClose {
|
|
let popup = self.confirm_close_rect();
|
|
let inner = Rect::new(
|
|
popup.x + 1,
|
|
popup.y + 1,
|
|
popup.width.saturating_sub(2),
|
|
popup.height.saturating_sub(2),
|
|
);
|
|
let (confirm, cancel) = crate::ui::confirm_close_button_rects(inner);
|
|
match modal_action_from_buttons(
|
|
mouse.column,
|
|
mouse.row,
|
|
&[
|
|
(confirm, ModalAction::Confirm),
|
|
(cancel, ModalAction::Cancel),
|
|
],
|
|
) {
|
|
Some(ModalAction::Confirm) => confirm_close_accept(self),
|
|
Some(ModalAction::Cancel) | None => confirm_close_cancel(self),
|
|
_ => {}
|
|
}
|
|
return None;
|
|
}
|
|
|
|
if matches!(
|
|
self.mode,
|
|
Mode::RenameWorkspace | Mode::RenameTab | Mode::RenamePane
|
|
) {
|
|
let action = self
|
|
.rename_modal_inner()
|
|
.map(crate::ui::rename_button_rects)
|
|
.and_then(|(save, clear, cancel)| {
|
|
modal_action_from_buttons(
|
|
mouse.column,
|
|
mouse.row,
|
|
&[
|
|
(save, ModalAction::Save),
|
|
(clear, ModalAction::Clear),
|
|
(cancel, ModalAction::Cancel),
|
|
],
|
|
)
|
|
})
|
|
.unwrap_or(ModalAction::Cancel);
|
|
apply_rename_action(self, action);
|
|
return None;
|
|
}
|
|
|
|
if self.mode == Mode::ContextMenu {
|
|
let item_idx = self.context_menu_item_at(mouse.column, mouse.row);
|
|
if let Some(menu) = self.context_menu.take() {
|
|
if let Some(idx) = item_idx {
|
|
apply_context_menu_action(self, menu, idx);
|
|
} else {
|
|
leave_modal(self);
|
|
}
|
|
}
|
|
return None;
|
|
}
|
|
|
|
if self.on_sidebar_divider(mouse.column, mouse.row) {
|
|
self.drag = Some(DragState {
|
|
target: DragTarget::SidebarDivider,
|
|
});
|
|
self.set_manual_sidebar_width(mouse.column);
|
|
return None;
|
|
}
|
|
|
|
if self.on_sidebar_section_divider(mouse.column, mouse.row) {
|
|
self.drag = Some(DragState {
|
|
target: DragTarget::SidebarSectionDivider,
|
|
});
|
|
self.set_sidebar_section_split(mouse.row);
|
|
return None;
|
|
}
|
|
|
|
if !in_sidebar {
|
|
if let Some(border) = self.find_border_at(mouse.column, mouse.row) {
|
|
self.drag = Some(DragState {
|
|
target: DragTarget::PaneSplit {
|
|
path: border.path.clone(),
|
|
direction: border.direction,
|
|
area: border.area,
|
|
},
|
|
});
|
|
return None;
|
|
}
|
|
|
|
if let Some((pane_id, target)) =
|
|
self.scrollbar_target_at(mouse.column, mouse.row)
|
|
{
|
|
self.focus_pane(pane_id);
|
|
match target {
|
|
ScrollbarClickTarget::Thumb { grab_row_offset } => {
|
|
self.drag = Some(DragState {
|
|
target: DragTarget::PaneScrollbar {
|
|
pane_id,
|
|
grab_row_offset,
|
|
},
|
|
});
|
|
}
|
|
ScrollbarClickTarget::Track { offset_from_bottom } => {
|
|
self.set_pane_scroll_offset(pane_id, offset_from_bottom);
|
|
}
|
|
}
|
|
if self.mode != Mode::Terminal {
|
|
self.mode = Mode::Terminal;
|
|
}
|
|
return None;
|
|
}
|
|
}
|
|
|
|
if self.on_tab_scroll_left_button(mouse.column, mouse.row) {
|
|
self.scroll_tabs_left();
|
|
return None;
|
|
}
|
|
if self.on_tab_scroll_right_button(mouse.column, mouse.row) {
|
|
self.scroll_tabs_right();
|
|
return None;
|
|
}
|
|
if let (Some(ws_idx), Some(tab_idx)) =
|
|
(self.active, self.tab_at(mouse.column, mouse.row))
|
|
{
|
|
self.tab_press = Some(TabPressState {
|
|
ws_idx,
|
|
tab_idx,
|
|
start_col: mouse.column,
|
|
start_row: mouse.row,
|
|
});
|
|
return None;
|
|
}
|
|
if self.on_new_tab_button(mouse.column, mouse.row) {
|
|
open_new_tab_dialog(self);
|
|
return None;
|
|
}
|
|
|
|
if in_sidebar {
|
|
if self.sidebar_collapsed {
|
|
if self.on_collapsed_sidebar_toggle(mouse.column, mouse.row) {
|
|
self.sidebar_collapsed = false;
|
|
return None;
|
|
}
|
|
|
|
if let Some(idx) = self.collapsed_workspace_at_row(mouse.row) {
|
|
self.switch_workspace(idx);
|
|
self.mode = Mode::Terminal;
|
|
return None;
|
|
}
|
|
|
|
if let Some((ws_idx, tab_idx, pane_id)) =
|
|
self.collapsed_agent_detail_target_at(mouse.row)
|
|
{
|
|
self.switch_workspace(ws_idx);
|
|
self.switch_tab(tab_idx);
|
|
self.focus_pane(pane_id);
|
|
self.mode = Mode::Terminal;
|
|
}
|
|
return None;
|
|
}
|
|
|
|
let new_button = self.sidebar_new_button_rect();
|
|
let on_new_button = mouse.row >= new_button.y
|
|
&& mouse.row < new_button.y + new_button.height
|
|
&& mouse.column >= new_button.x
|
|
&& mouse.column < new_button.x + new_button.width;
|
|
if on_new_button {
|
|
self.request_new_workspace = true;
|
|
return None;
|
|
}
|
|
|
|
if let Some(target) =
|
|
self.workspace_list_scrollbar_target_at(mouse.column, mouse.row)
|
|
{
|
|
match target {
|
|
ScrollbarClickTarget::Thumb { grab_row_offset } => {
|
|
self.drag = Some(DragState {
|
|
target: DragTarget::WorkspaceListScrollbar { grab_row_offset },
|
|
});
|
|
}
|
|
ScrollbarClickTarget::Track { offset_from_bottom } => {
|
|
self.set_workspace_list_offset_from_bottom(offset_from_bottom);
|
|
}
|
|
}
|
|
return None;
|
|
}
|
|
|
|
if let Some(idx) = self.workspace_at_row(mouse.row) {
|
|
self.workspace_press = Some(WorkspacePressState {
|
|
ws_idx: idx,
|
|
start_col: mouse.column,
|
|
start_row: mouse.row,
|
|
});
|
|
return None;
|
|
}
|
|
|
|
if self.on_agent_panel_scope_toggle(mouse.column, mouse.row) {
|
|
self.agent_panel_scope = match self.agent_panel_scope {
|
|
AgentPanelScope::CurrentWorkspace => AgentPanelScope::AllWorkspaces,
|
|
AgentPanelScope::AllWorkspaces => AgentPanelScope::CurrentWorkspace,
|
|
};
|
|
self.agent_panel_scroll = 0;
|
|
self.mark_session_dirty();
|
|
return None;
|
|
}
|
|
|
|
if let Some(target) =
|
|
self.agent_panel_scrollbar_target_at(mouse.column, mouse.row)
|
|
{
|
|
match target {
|
|
ScrollbarClickTarget::Thumb { grab_row_offset } => {
|
|
self.drag = Some(DragState {
|
|
target: DragTarget::AgentPanelScrollbar { grab_row_offset },
|
|
});
|
|
}
|
|
ScrollbarClickTarget::Track { offset_from_bottom } => {
|
|
self.set_agent_panel_offset_from_bottom(offset_from_bottom);
|
|
}
|
|
}
|
|
return None;
|
|
}
|
|
|
|
if let Some((ws_idx, tab_idx, pane_id)) = self.agent_detail_target_at(mouse.row)
|
|
{
|
|
self.switch_workspace(ws_idx);
|
|
self.switch_tab(tab_idx);
|
|
self.focus_pane(pane_id);
|
|
self.mode = Mode::Terminal;
|
|
return None;
|
|
}
|
|
} else if let Some(info) = self.pane_at(mouse.column, mouse.row).cloned() {
|
|
self.focus_pane(info.id);
|
|
if self.mode != Mode::Terminal {
|
|
self.mode = Mode::Terminal;
|
|
}
|
|
|
|
if self.forward_pane_mouse_button(&info, mouse) {
|
|
self.selection = None;
|
|
return None;
|
|
}
|
|
|
|
let (row, col) = (
|
|
mouse.row - info.inner_rect.y,
|
|
mouse.column - info.inner_rect.x,
|
|
);
|
|
self.selection = Some(Selection::anchor(
|
|
info.id,
|
|
row,
|
|
col,
|
|
self.pane_scroll_metrics(info.id),
|
|
));
|
|
} else if let Some(info) = self.view.pane_infos.iter().find(|p| {
|
|
mouse.column >= p.rect.x
|
|
&& mouse.column < p.rect.x + p.rect.width
|
|
&& mouse.row >= p.rect.y
|
|
&& mouse.row < p.rect.y + p.rect.height
|
|
}) {
|
|
let id = info.id;
|
|
self.focus_pane(id);
|
|
if self.mode != Mode::Terminal {
|
|
self.mode = Mode::Terminal;
|
|
}
|
|
}
|
|
}
|
|
|
|
MouseEventKind::Drag(MouseButton::Left) => {
|
|
if self.selection.is_some() {
|
|
self.update_selection_drag(mouse.column, mouse.row);
|
|
return None;
|
|
}
|
|
|
|
if self.drag.is_none() {
|
|
if let Some(info) = self.pane_mouse_target(mouse.column, mouse.row).cloned() {
|
|
if self.forward_pane_mouse_button(&info, mouse) {
|
|
self.selection = None;
|
|
return None;
|
|
}
|
|
}
|
|
}
|
|
|
|
let workspace_drop_index = self.workspace_drop_index_at_row(mouse.row);
|
|
let tab_drop_index = self.tab_drop_index_at(mouse.column, mouse.row);
|
|
if self.drag.is_none() {
|
|
if let Some(press) = &self.workspace_press {
|
|
let delta_col = mouse.column.abs_diff(press.start_col);
|
|
let delta_row = mouse.row.abs_diff(press.start_row);
|
|
if delta_col.max(delta_row) >= WORKSPACE_DRAG_THRESHOLD {
|
|
self.drag = Some(DragState {
|
|
target: DragTarget::WorkspaceReorder {
|
|
source_ws_idx: press.ws_idx,
|
|
insert_idx: workspace_drop_index,
|
|
},
|
|
});
|
|
}
|
|
} else if let Some(press) = &self.tab_press {
|
|
let delta_col = mouse.column.abs_diff(press.start_col);
|
|
let delta_row = mouse.row.abs_diff(press.start_row);
|
|
if delta_col.max(delta_row) >= TAB_DRAG_THRESHOLD {
|
|
self.drag = Some(DragState {
|
|
target: DragTarget::TabReorder {
|
|
ws_idx: press.ws_idx,
|
|
source_tab_idx: press.tab_idx,
|
|
insert_idx: tab_drop_index,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Some(DragState {
|
|
target: DragTarget::WorkspaceReorder { insert_idx, .. },
|
|
}) = &mut self.drag
|
|
{
|
|
*insert_idx = workspace_drop_index;
|
|
} else if let Some(DragState {
|
|
target:
|
|
DragTarget::TabReorder {
|
|
ws_idx, insert_idx, ..
|
|
},
|
|
}) = &mut self.drag
|
|
{
|
|
if self.active == Some(*ws_idx) {
|
|
*insert_idx = tab_drop_index;
|
|
}
|
|
} else if let Some(drag) = &self.drag {
|
|
match &drag.target {
|
|
DragTarget::WorkspaceReorder { .. } | DragTarget::TabReorder { .. } => {}
|
|
DragTarget::WorkspaceListScrollbar { grab_row_offset } => {
|
|
if let Some(offset_from_bottom) =
|
|
self.workspace_list_offset_for_drag_row(mouse.row, *grab_row_offset)
|
|
{
|
|
self.set_workspace_list_offset_from_bottom(offset_from_bottom);
|
|
}
|
|
}
|
|
DragTarget::AgentPanelScrollbar { grab_row_offset } => {
|
|
if let Some(offset_from_bottom) =
|
|
self.agent_panel_offset_for_drag_row(mouse.row, *grab_row_offset)
|
|
{
|
|
self.set_agent_panel_offset_from_bottom(offset_from_bottom);
|
|
}
|
|
}
|
|
DragTarget::PaneSplit {
|
|
path,
|
|
direction,
|
|
area,
|
|
} => {
|
|
let ratio = match direction {
|
|
Direction::Horizontal => {
|
|
(mouse.column.saturating_sub(area.x)) as f32
|
|
/ area.width.max(1) as f32
|
|
}
|
|
Direction::Vertical => {
|
|
(mouse.row.saturating_sub(area.y)) as f32
|
|
/ area.height.max(1) as f32
|
|
}
|
|
};
|
|
let ratio = ratio.clamp(0.1, 0.9);
|
|
let path = path.clone();
|
|
if let Some(ws) = self.active.and_then(|i| self.workspaces.get_mut(i)) {
|
|
ws.layout.set_ratio_at(&path, ratio);
|
|
self.mark_session_dirty();
|
|
}
|
|
}
|
|
DragTarget::PaneScrollbar {
|
|
pane_id,
|
|
grab_row_offset,
|
|
} => {
|
|
if let Some(offset_from_bottom) = self.scrollbar_offset_for_pane_row(
|
|
*pane_id,
|
|
mouse.row,
|
|
*grab_row_offset,
|
|
) {
|
|
self.set_pane_scroll_offset(*pane_id, offset_from_bottom);
|
|
}
|
|
}
|
|
DragTarget::SidebarDivider => {
|
|
self.set_manual_sidebar_width(mouse.column);
|
|
}
|
|
DragTarget::SidebarSectionDivider => {
|
|
self.set_sidebar_section_split(mouse.row);
|
|
}
|
|
DragTarget::ReleaseNotesScrollbar { .. }
|
|
| DragTarget::KeybindHelpScrollbar { .. } => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
MouseEventKind::Up(MouseButton::Left) => {
|
|
if self.selection.is_some() {
|
|
self.workspace_press = None;
|
|
self.tab_press = None;
|
|
self.drag = None;
|
|
let was_click = self.selection.as_ref().is_some_and(|s| s.was_just_click());
|
|
if was_click {
|
|
self.selection = None;
|
|
} else {
|
|
self.copy_selection();
|
|
}
|
|
return None;
|
|
}
|
|
|
|
if self.drag.is_none() {
|
|
if let Some(info) = self.pane_mouse_target(mouse.column, mouse.row).cloned() {
|
|
if self.forward_pane_mouse_button(&info, mouse) {
|
|
self.selection = None;
|
|
self.workspace_press = None;
|
|
self.tab_press = None;
|
|
self.drag = None;
|
|
return None;
|
|
}
|
|
}
|
|
}
|
|
|
|
let workspace_press = self.workspace_press.take();
|
|
let tab_press = self.tab_press.take();
|
|
match self.drag.take() {
|
|
Some(DragState {
|
|
target:
|
|
DragTarget::WorkspaceReorder {
|
|
source_ws_idx,
|
|
insert_idx: Some(insert_idx),
|
|
},
|
|
}) => {
|
|
self.move_workspace(source_ws_idx, insert_idx);
|
|
}
|
|
Some(DragState {
|
|
target:
|
|
DragTarget::TabReorder {
|
|
ws_idx,
|
|
source_tab_idx,
|
|
insert_idx: Some(insert_idx),
|
|
},
|
|
}) => {
|
|
if self.active == Some(ws_idx) {
|
|
self.move_tab(source_tab_idx, insert_idx);
|
|
self.mode = Mode::Terminal;
|
|
}
|
|
}
|
|
Some(_) => {}
|
|
None => {
|
|
if let Some(press) = workspace_press {
|
|
self.switch_workspace(press.ws_idx);
|
|
self.mode = Mode::Terminal;
|
|
return None;
|
|
}
|
|
if let Some(press) = tab_press {
|
|
if self.active == Some(press.ws_idx) {
|
|
self.switch_tab(press.tab_idx);
|
|
self.mode = Mode::Terminal;
|
|
return None;
|
|
}
|
|
}
|
|
let was_click = self.selection.as_ref().is_some_and(|s| s.was_just_click());
|
|
if was_click {
|
|
self.selection = None;
|
|
} else {
|
|
self.copy_selection();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
MouseEventKind::Up(MouseButton::Middle) | MouseEventKind::Drag(MouseButton::Middle)
|
|
if !in_sidebar =>
|
|
{
|
|
if let Some(info) = self.pane_mouse_target(mouse.column, mouse.row).cloned() {
|
|
let _ = self.forward_pane_mouse_button(&info, mouse);
|
|
}
|
|
}
|
|
|
|
MouseEventKind::ScrollUp | MouseEventKind::ScrollDown
|
|
if self.on_tab_bar(mouse.column, mouse.row) =>
|
|
{
|
|
match mouse.kind {
|
|
MouseEventKind::ScrollUp => self.previous_tab(),
|
|
MouseEventKind::ScrollDown => self.next_tab(),
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
MouseEventKind::ScrollUp | MouseEventKind::ScrollDown
|
|
if !in_sidebar && self.scroll_selection_with_wheel(mouse) => {}
|
|
|
|
MouseEventKind::ScrollUp | MouseEventKind::ScrollDown if !in_sidebar => {
|
|
self.selection = None;
|
|
self.handle_terminal_wheel(mouse);
|
|
}
|
|
|
|
MouseEventKind::ScrollUp if in_sidebar => {
|
|
let agent_area = self.agent_panel_rect();
|
|
let over_agent_panel = agent_area != Rect::default()
|
|
&& mouse.row >= agent_area.y
|
|
&& mouse.row < agent_area.y + agent_area.height;
|
|
if over_agent_panel {
|
|
if crate::ui::should_show_scrollbar(crate::ui::agent_panel_scroll_metrics(
|
|
self, agent_area,
|
|
)) {
|
|
self.scroll_agent_panel(-1);
|
|
}
|
|
} else if crate::ui::should_show_scrollbar(
|
|
crate::ui::workspace_list_scroll_metrics(self, self.workspace_list_rect()),
|
|
) {
|
|
self.scroll_workspace_list(-1);
|
|
} else if self.selected > 0 {
|
|
self.selected -= 1;
|
|
self.ensure_workspace_visible(self.selected);
|
|
}
|
|
}
|
|
MouseEventKind::ScrollDown if in_sidebar => {
|
|
let agent_area = self.agent_panel_rect();
|
|
let over_agent_panel = agent_area != Rect::default()
|
|
&& mouse.row >= agent_area.y
|
|
&& mouse.row < agent_area.y + agent_area.height;
|
|
if over_agent_panel {
|
|
if crate::ui::should_show_scrollbar(crate::ui::agent_panel_scroll_metrics(
|
|
self, agent_area,
|
|
)) {
|
|
self.scroll_agent_panel(1);
|
|
}
|
|
} else if crate::ui::should_show_scrollbar(
|
|
crate::ui::workspace_list_scroll_metrics(self, self.workspace_list_rect()),
|
|
) {
|
|
self.scroll_workspace_list(1);
|
|
} else if !self.workspaces.is_empty() && self.selected < self.workspaces.len() - 1 {
|
|
self.selected += 1;
|
|
self.ensure_workspace_visible(self.selected);
|
|
}
|
|
}
|
|
|
|
MouseEventKind::Moved if self.mode == Mode::ContextMenu => {
|
|
let hovered = self.context_menu_item_at(mouse.column, mouse.row);
|
|
if let Some(menu) = &mut self.context_menu {
|
|
menu.list.hover(hovered);
|
|
}
|
|
}
|
|
|
|
MouseEventKind::Down(MouseButton::Right) if in_sidebar && !self.sidebar_collapsed => {
|
|
if self
|
|
.workspace_list_scrollbar_target_at(mouse.column, mouse.row)
|
|
.is_some()
|
|
{
|
|
return None;
|
|
}
|
|
if let Some(idx) = self.workspace_at_row(mouse.row) {
|
|
self.selected = idx;
|
|
self.context_menu = Some(ContextMenuState {
|
|
kind: ContextMenuKind::Workspace { ws_idx: idx },
|
|
x: mouse.column,
|
|
y: mouse.row,
|
|
list: MenuListState::new(0),
|
|
});
|
|
self.mode = Mode::ContextMenu;
|
|
}
|
|
}
|
|
|
|
MouseEventKind::Down(MouseButton::Right)
|
|
if 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))
|
|
{
|
|
self.switch_tab(tab_idx);
|
|
self.context_menu = Some(ContextMenuState {
|
|
kind: ContextMenuKind::Tab { ws_idx, tab_idx },
|
|
x: mouse.column,
|
|
y: mouse.row,
|
|
list: MenuListState::new(0),
|
|
});
|
|
self.mode = Mode::ContextMenu;
|
|
}
|
|
}
|
|
|
|
MouseEventKind::Down(MouseButton::Right) if !in_sidebar => {
|
|
if let Some(info) = self.pane_mouse_target(mouse.column, mouse.row).cloned() {
|
|
self.focus_pane(info.id);
|
|
let has_manual_label = self
|
|
.active
|
|
.and_then(|ws_idx| self.workspaces.get(ws_idx))
|
|
.and_then(|ws| ws.pane_state(info.id))
|
|
.and_then(|pane| pane.manual_label.as_ref())
|
|
.is_some();
|
|
self.context_menu = Some(ContextMenuState {
|
|
kind: ContextMenuKind::Pane {
|
|
pane_id: info.id,
|
|
has_manual_label,
|
|
},
|
|
x: mouse.column,
|
|
y: mouse.row,
|
|
list: MenuListState::new(0),
|
|
});
|
|
self.mode = Mode::ContextMenu;
|
|
}
|
|
}
|
|
|
|
_ => {}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
fn handle_mobile_mouse(&mut self, mouse: MouseEvent) -> bool {
|
|
if self.mode == Mode::Navigate {
|
|
match mouse.kind {
|
|
MouseEventKind::ScrollUp => {
|
|
self.scroll_mobile_switcher_at(mouse.column, mouse.row, -1);
|
|
return true;
|
|
}
|
|
MouseEventKind::ScrollDown => {
|
|
self.scroll_mobile_switcher_at(mouse.column, mouse.row, 1);
|
|
return true;
|
|
}
|
|
MouseEventKind::Down(MouseButton::Left) => {}
|
|
_ => return true,
|
|
}
|
|
} else if !matches!(mouse.kind, MouseEventKind::Down(MouseButton::Left)) {
|
|
return false;
|
|
}
|
|
|
|
if self.mode != Mode::Navigate {
|
|
if !matches!(self.mode, Mode::Terminal | Mode::Resize) {
|
|
return false;
|
|
}
|
|
if rect_contains(self.view.mobile_menu_hit_area, mouse.column, mouse.row) {
|
|
self.mobile_switcher_scroll = 0;
|
|
self.mode = Mode::Navigate;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
let areas = crate::ui::mobile_switcher_areas(self);
|
|
if rect_contains(areas.close, mouse.column, mouse.row) {
|
|
self.mode = Mode::Terminal;
|
|
return true;
|
|
}
|
|
|
|
match crate::ui::mobile_switcher_target_at(self, mouse.column, mouse.row) {
|
|
Some(crate::ui::MobileSwitcherTarget::NewWorkspace) => {
|
|
self.request_new_workspace = true;
|
|
}
|
|
Some(crate::ui::MobileSwitcherTarget::Workspace(ws_idx)) => {
|
|
self.switch_workspace(ws_idx);
|
|
self.mode = Mode::Terminal;
|
|
}
|
|
Some(crate::ui::MobileSwitcherTarget::NewTab) => {
|
|
open_new_tab_dialog(self);
|
|
}
|
|
Some(crate::ui::MobileSwitcherTarget::Tab(tab_idx)) => {
|
|
self.switch_tab(tab_idx);
|
|
self.mode = Mode::Terminal;
|
|
}
|
|
Some(crate::ui::MobileSwitcherTarget::Agent {
|
|
ws_idx,
|
|
tab_idx,
|
|
pane_id,
|
|
}) => {
|
|
self.switch_workspace(ws_idx);
|
|
self.switch_tab(tab_idx);
|
|
self.focus_pane(pane_id);
|
|
self.mode = Mode::Terminal;
|
|
}
|
|
Some(crate::ui::MobileSwitcherTarget::Menu(action_idx)) => {
|
|
let actions = global_menu_actions(self);
|
|
if let Some(action) = actions.get(action_idx).copied() {
|
|
apply_global_menu_action(self, action);
|
|
}
|
|
}
|
|
None => {}
|
|
}
|
|
|
|
true
|
|
}
|
|
|
|
fn scroll_mobile_switcher_at(&mut self, _col: u16, _row: u16, delta: i16) {
|
|
let max_scroll = crate::ui::mobile_switcher_max_scroll(self);
|
|
apply_scroll(
|
|
&mut self.mobile_switcher_scroll,
|
|
delta.saturating_mul(2),
|
|
max_scroll,
|
|
);
|
|
}
|
|
|
|
pub(super) fn screen_rect(&self) -> Rect {
|
|
let sidebar = self.view.sidebar_rect;
|
|
let terminal = self.view.terminal_area;
|
|
let x = sidebar.x.min(terminal.x);
|
|
let y = sidebar.y.min(terminal.y);
|
|
let right = (sidebar.x + sidebar.width).max(terminal.x + terminal.width);
|
|
let bottom = (sidebar.y + sidebar.height).max(terminal.y + terminal.height);
|
|
Rect::new(x, y, right.saturating_sub(x), bottom.saturating_sub(y))
|
|
}
|
|
|
|
pub(crate) fn context_menu_rect(&self) -> Option<Rect> {
|
|
let menu = self.context_menu.as_ref()?;
|
|
let screen = self.screen_rect();
|
|
let max_item_w = menu
|
|
.items()
|
|
.iter()
|
|
.map(|item| item.len() as u16)
|
|
.max()
|
|
.unwrap_or(0);
|
|
let menu_w = (max_item_w + 4).max(14).min(screen.width.max(1));
|
|
let menu_h = (menu.items().len() as u16 + 2).min(screen.height.max(1));
|
|
let x = menu.x.min(screen.x + screen.width.saturating_sub(menu_w));
|
|
let y = menu.y.min(screen.y + screen.height.saturating_sub(menu_h));
|
|
Some(Rect::new(x, y, menu_w, menu_h))
|
|
}
|
|
|
|
pub(crate) fn confirm_close_rect(&self) -> Rect {
|
|
crate::ui::confirm_close_popup_rect(self.view.terminal_area).unwrap_or_default()
|
|
}
|
|
|
|
fn context_menu_item_at(&self, col: u16, row: u16) -> Option<usize> {
|
|
let menu_rect = self.context_menu_rect()?;
|
|
let inner_x = menu_rect.x + 1;
|
|
let inner_y = menu_rect.y + 1;
|
|
let inner_w = menu_rect.width.saturating_sub(2);
|
|
let inner_h = menu_rect.height.saturating_sub(2);
|
|
let item_count = self
|
|
.context_menu
|
|
.as_ref()
|
|
.map(|menu| menu.items().len() as u16)
|
|
.unwrap_or(0);
|
|
if col >= inner_x
|
|
&& col < inner_x + inner_w
|
|
&& row >= inner_y
|
|
&& row < inner_y + inner_h.min(item_count)
|
|
{
|
|
Some((row - inner_y) as usize)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
pub(super) fn tab_at(&self, col: u16, row: u16) -> Option<usize> {
|
|
self.view
|
|
.tab_hit_areas
|
|
.iter()
|
|
.enumerate()
|
|
.find_map(|(idx, area)| {
|
|
(area.width > 0
|
|
&& row >= area.y
|
|
&& row < area.y + area.height
|
|
&& col >= area.x
|
|
&& col < area.x + area.width)
|
|
.then_some(idx)
|
|
})
|
|
}
|
|
|
|
pub(super) fn on_tab_bar(&self, col: u16, row: u16) -> bool {
|
|
let area = self.view.tab_bar_rect;
|
|
area.width > 0
|
|
&& row >= area.y
|
|
&& row < area.y + area.height
|
|
&& col >= area.x
|
|
&& col < area.x + area.width
|
|
}
|
|
|
|
pub(super) fn on_tab_scroll_left_button(&self, col: u16, row: u16) -> bool {
|
|
let area = self.view.tab_scroll_left_hit_area;
|
|
area.width > 0
|
|
&& row >= area.y
|
|
&& row < area.y + area.height
|
|
&& col >= area.x
|
|
&& col < area.x + area.width
|
|
}
|
|
|
|
pub(super) fn on_tab_scroll_right_button(&self, col: u16, row: u16) -> bool {
|
|
let area = self.view.tab_scroll_right_hit_area;
|
|
area.width > 0
|
|
&& row >= area.y
|
|
&& row < area.y + area.height
|
|
&& col >= area.x
|
|
&& col < area.x + area.width
|
|
}
|
|
|
|
pub(super) fn tab_drop_index_at(&self, col: u16, row: u16) -> Option<usize> {
|
|
if !self.on_tab_bar(col, row) {
|
|
return None;
|
|
}
|
|
|
|
let visible_tabs: Vec<_> = self
|
|
.view
|
|
.tab_hit_areas
|
|
.iter()
|
|
.enumerate()
|
|
.filter(|(_, rect)| rect.width > 0)
|
|
.collect();
|
|
let (first_idx, first_rect) = *visible_tabs.first()?;
|
|
let (last_idx, last_rect) = *visible_tabs.last()?;
|
|
|
|
if self.on_tab_scroll_left_button(col, row) {
|
|
return Some(0);
|
|
}
|
|
if self.on_tab_scroll_right_button(col, row) {
|
|
return self
|
|
.active
|
|
.and_then(|idx| self.workspaces.get(idx))
|
|
.map(|ws| ws.tabs.len());
|
|
}
|
|
|
|
let left_edge = if first_idx == 0 {
|
|
first_rect.x
|
|
} else {
|
|
self.view.tab_scroll_left_hit_area.x + self.view.tab_scroll_left_hit_area.width
|
|
};
|
|
let right_edge = if self
|
|
.active
|
|
.and_then(|idx| self.workspaces.get(idx))
|
|
.is_some_and(|ws| last_idx + 1 >= ws.tabs.len())
|
|
{
|
|
last_rect.x + last_rect.width
|
|
} else {
|
|
self.view.tab_scroll_right_hit_area.x.saturating_sub(1)
|
|
};
|
|
|
|
if col <= left_edge {
|
|
return Some(first_idx);
|
|
}
|
|
if col >= right_edge {
|
|
return Some(last_idx + 1);
|
|
}
|
|
|
|
for (idx, rect) in visible_tabs {
|
|
let midpoint = rect.x + rect.width / 2;
|
|
if col < midpoint {
|
|
return Some(idx);
|
|
}
|
|
if col < rect.x + rect.width {
|
|
return Some(idx + 1);
|
|
}
|
|
}
|
|
|
|
Some(last_idx + 1)
|
|
}
|
|
|
|
pub(super) fn on_new_tab_button(&self, col: u16, row: u16) -> bool {
|
|
let area = self.view.new_tab_hit_area;
|
|
area.width > 0
|
|
&& row >= area.y
|
|
&& row < area.y + area.height
|
|
&& col >= area.x
|
|
&& col < area.x + area.width
|
|
}
|
|
|
|
pub(super) fn find_border_at(&self, col: u16, row: u16) -> Option<&SplitBorder> {
|
|
self.view.split_borders.iter().find(|b| match b.direction {
|
|
Direction::Horizontal => {
|
|
(col as i32 - b.pos as i32).unsigned_abs() <= 1
|
|
&& row >= b.area.y
|
|
&& row < b.area.y + b.area.height
|
|
}
|
|
Direction::Vertical => {
|
|
(row as i32 - b.pos as i32).unsigned_abs() <= 1
|
|
&& col >= b.area.x
|
|
&& col < b.area.x + b.area.width
|
|
}
|
|
})
|
|
}
|
|
|
|
pub(super) fn pane_at(&self, col: u16, row: u16) -> Option<&PaneInfo> {
|
|
self.view.pane_infos.iter().find(|p| {
|
|
col >= p.inner_rect.x
|
|
&& col < p.inner_rect.x + p.inner_rect.width
|
|
&& row >= p.inner_rect.y
|
|
&& row < p.inner_rect.y + p.inner_rect.height
|
|
})
|
|
}
|
|
|
|
pub(super) fn pane_mouse_target(&self, col: u16, row: u16) -> Option<&PaneInfo> {
|
|
self.pane_at(col, row)
|
|
.or_else(|| self.pane_frame_at(col, row))
|
|
}
|
|
|
|
pub(super) fn pane_info_by_id(&self, pane_id: crate::layout::PaneId) -> Option<&PaneInfo> {
|
|
self.view.pane_infos.iter().find(|info| info.id == pane_id)
|
|
}
|
|
|
|
pub(super) fn pane_frame_at(&self, col: u16, row: u16) -> Option<&PaneInfo> {
|
|
self.view.pane_infos.iter().find(|p| {
|
|
col >= p.rect.x
|
|
&& col < p.rect.x + p.rect.width
|
|
&& row >= p.rect.y
|
|
&& row < p.rect.y + p.rect.height
|
|
})
|
|
}
|
|
|
|
pub(super) fn focus_pane(&mut self, pane_id: crate::layout::PaneId) {
|
|
if let Some(ws) = self.active.and_then(|i| self.workspaces.get_mut(i)) {
|
|
if ws.layout.focused() != pane_id {
|
|
ws.layout.focus_pane(pane_id);
|
|
self.mark_session_dirty();
|
|
}
|
|
}
|
|
}
|
|
|
|
fn clickable_toast_at(&self, col: u16, row: u16) -> bool {
|
|
self.toast
|
|
.as_ref()
|
|
.is_some_and(|toast| toast.target.is_some())
|
|
&& rect_contains(self.view.toast_hit_area, col, row)
|
|
}
|
|
|
|
pub(crate) fn focus_toast_target(&mut self) {
|
|
let Some(target) = self.toast.as_ref().and_then(|toast| toast.target.clone()) else {
|
|
return;
|
|
};
|
|
let Some(ws_idx) = self
|
|
.workspaces
|
|
.iter()
|
|
.position(|workspace| workspace.id == target.workspace_id)
|
|
else {
|
|
return;
|
|
};
|
|
let Some(tab_idx) = self.workspaces[ws_idx].find_tab_index_for_pane(target.pane_id) else {
|
|
return;
|
|
};
|
|
|
|
self.switch_workspace(ws_idx);
|
|
self.switch_tab(tab_idx);
|
|
self.focus_pane(target.pane_id);
|
|
self.toast = None;
|
|
self.mode = Mode::Terminal;
|
|
}
|
|
|
|
pub(super) fn scroll_pane_up(&self, pane_id: crate::layout::PaneId, lines: usize) {
|
|
if let Some(ws) = self.active.and_then(|i| self.workspaces.get(i)) {
|
|
if let Some(rt) = ws.runtimes.get(&pane_id) {
|
|
rt.scroll_up(lines);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(super) fn scroll_pane_down(&self, pane_id: crate::layout::PaneId, lines: usize) {
|
|
if let Some(ws) = self.active.and_then(|i| self.workspaces.get(i)) {
|
|
if let Some(rt) = ws.runtimes.get(&pane_id) {
|
|
rt.scroll_down(lines);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(super) fn pane_scroll_metrics(
|
|
&self,
|
|
pane_id: crate::layout::PaneId,
|
|
) -> Option<crate::pane::ScrollMetrics> {
|
|
self.active
|
|
.and_then(|i| self.workspaces.get(i))
|
|
.and_then(|ws| ws.runtime(pane_id))
|
|
.and_then(crate::pane::PaneRuntime::scroll_metrics)
|
|
}
|
|
|
|
pub(super) fn handle_terminal_wheel(&mut self, mouse: MouseEvent) {
|
|
const LINES_PER_NOTCH: usize = 3;
|
|
|
|
if let Some(info) = self.pane_at(mouse.column, mouse.row).cloned() {
|
|
self.focus_pane(info.id);
|
|
if self.forward_pane_wheel(&info, mouse) {
|
|
return;
|
|
}
|
|
match mouse.kind {
|
|
MouseEventKind::ScrollUp => self.scroll_pane_up(info.id, LINES_PER_NOTCH),
|
|
MouseEventKind::ScrollDown => self.scroll_pane_down(info.id, LINES_PER_NOTCH),
|
|
_ => {}
|
|
}
|
|
return;
|
|
}
|
|
|
|
if let Some(info) = self.pane_frame_at(mouse.column, mouse.row).cloned() {
|
|
self.focus_pane(info.id);
|
|
match mouse.kind {
|
|
MouseEventKind::ScrollUp => self.scroll_pane_up(info.id, LINES_PER_NOTCH),
|
|
MouseEventKind::ScrollDown => self.scroll_pane_down(info.id, LINES_PER_NOTCH),
|
|
_ => {}
|
|
}
|
|
return;
|
|
}
|
|
|
|
if let Some(ws) = self.active.and_then(|i| self.workspaces.get(i)) {
|
|
if let Some(rt) = ws.focused_runtime() {
|
|
match mouse.kind {
|
|
MouseEventKind::ScrollUp => rt.scroll_up(LINES_PER_NOTCH),
|
|
MouseEventKind::ScrollDown => rt.scroll_down(LINES_PER_NOTCH),
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(super) fn forward_pane_mouse_button(&self, info: &PaneInfo, mouse: MouseEvent) -> bool {
|
|
let Some(ws) = self.active.and_then(|i| self.workspaces.get(i)) else {
|
|
return false;
|
|
};
|
|
let Some(rt) = ws.runtimes.get(&info.id) else {
|
|
return false;
|
|
};
|
|
let column = mouse.column.saturating_sub(info.inner_rect.x);
|
|
let row = mouse.row.saturating_sub(info.inner_rect.y);
|
|
let Some(bytes) = rt.encode_mouse_button(mouse.kind, column, row, mouse.modifiers) else {
|
|
return false;
|
|
};
|
|
rt.scroll_reset();
|
|
if let Err(err) = rt.try_send_bytes(Bytes::from(bytes)) {
|
|
warn!(pane = info.id.raw(), err = %err, kind = ?mouse.kind, "failed to forward mouse button event");
|
|
}
|
|
true
|
|
}
|
|
|
|
pub(super) fn forward_pane_wheel(&self, info: &PaneInfo, mouse: MouseEvent) -> bool {
|
|
let Some(ws) = self.active.and_then(|i| self.workspaces.get(i)) else {
|
|
return false;
|
|
};
|
|
let Some(rt) = ws.runtimes.get(&info.id) else {
|
|
return false;
|
|
};
|
|
match rt.wheel_routing() {
|
|
Some(crate::pane::WheelRouting::HostScroll) | None => false,
|
|
Some(crate::pane::WheelRouting::MouseReport) => {
|
|
rt.scroll_reset();
|
|
let column = mouse.column.saturating_sub(info.inner_rect.x);
|
|
let row = mouse.row.saturating_sub(info.inner_rect.y);
|
|
let Some(bytes) = rt.encode_mouse_wheel(mouse.kind, column, row, mouse.modifiers)
|
|
else {
|
|
warn!(pane = info.id.raw(), kind = ?mouse.kind, "failed to encode mouse wheel event");
|
|
return true;
|
|
};
|
|
if let Err(err) = rt.try_send_bytes(Bytes::from(bytes)) {
|
|
warn!(pane = info.id.raw(), err = %err, "failed to forward mouse wheel event");
|
|
}
|
|
true
|
|
}
|
|
Some(crate::pane::WheelRouting::AlternateScroll) => {
|
|
rt.scroll_reset();
|
|
let Some(bytes) = rt.encode_alternate_scroll(mouse.kind) else {
|
|
return true;
|
|
};
|
|
if let Err(err) = rt.try_send_bytes(Bytes::from(bytes)) {
|
|
warn!(pane = info.id.raw(), err = %err, "failed to forward alternate-scroll key");
|
|
}
|
|
true
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(super) fn set_pane_scroll_offset(
|
|
&self,
|
|
pane_id: crate::layout::PaneId,
|
|
offset_from_bottom: usize,
|
|
) {
|
|
if let Some(ws) = self.active.and_then(|i| self.workspaces.get(i)) {
|
|
if let Some(rt) = ws.runtimes.get(&pane_id) {
|
|
rt.set_scroll_offset_from_bottom(offset_from_bottom);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(super) fn scrollbar_target_at(
|
|
&self,
|
|
col: u16,
|
|
row: u16,
|
|
) -> Option<(crate::layout::PaneId, ScrollbarClickTarget)> {
|
|
let ws = self.active.and_then(|i| self.workspaces.get(i))?;
|
|
let info = self.view.pane_infos.iter().find(|info| {
|
|
crate::ui::pane_scrollbar_rect(info).is_some_and(|track| {
|
|
col >= track.x
|
|
&& col < track.x + track.width
|
|
&& row >= track.y
|
|
&& row < track.y + track.height
|
|
})
|
|
})?;
|
|
let rt = ws.runtimes.get(&info.id)?;
|
|
let metrics = rt.scroll_metrics()?;
|
|
if metrics.max_offset_from_bottom == 0 {
|
|
return None;
|
|
}
|
|
let track = crate::ui::pane_scrollbar_rect(info)?;
|
|
if let Some(grab_row_offset) = crate::ui::scrollbar_thumb_grab_offset(metrics, track, row) {
|
|
Some((info.id, ScrollbarClickTarget::Thumb { grab_row_offset }))
|
|
} else {
|
|
Some((
|
|
info.id,
|
|
ScrollbarClickTarget::Track {
|
|
offset_from_bottom: crate::ui::scrollbar_offset_from_row(metrics, track, row),
|
|
},
|
|
))
|
|
}
|
|
}
|
|
|
|
pub(super) fn scrollbar_offset_for_pane_row(
|
|
&self,
|
|
pane_id: crate::layout::PaneId,
|
|
row: u16,
|
|
grab_row_offset: u16,
|
|
) -> Option<usize> {
|
|
let ws = self.active.and_then(|i| self.workspaces.get(i))?;
|
|
let info = self
|
|
.view
|
|
.pane_infos
|
|
.iter()
|
|
.find(|info| info.id == pane_id)?;
|
|
let track = crate::ui::pane_scrollbar_rect(info)?;
|
|
let rt = ws.runtimes.get(&pane_id)?;
|
|
let metrics = rt.scroll_metrics()?;
|
|
if metrics.max_offset_from_bottom == 0 {
|
|
return None;
|
|
}
|
|
Some(crate::ui::scrollbar_offset_from_drag_row(
|
|
metrics,
|
|
track,
|
|
row,
|
|
grab_row_offset,
|
|
))
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub(super) fn wheel_routing(input_state: crate::pane::InputState) -> WheelRouting {
|
|
if input_state.mouse_protocol_mode.reporting_enabled() {
|
|
WheelRouting::MouseReport
|
|
} else if input_state.alternate_screen && input_state.mouse_alternate_scroll {
|
|
WheelRouting::AlternateScroll
|
|
} else {
|
|
WheelRouting::HostScroll
|
|
}
|
|
}
|
|
|
|
fn rect_contains(rect: Rect, col: u16, row: u16) -> bool {
|
|
rect.width > 0
|
|
&& rect.height > 0
|
|
&& col >= rect.x
|
|
&& col < rect.x + rect.width
|
|
&& row >= rect.y
|
|
&& row < rect.y + rect.height
|
|
}
|
|
|
|
fn apply_scroll(scroll: &mut usize, delta: i16, max_scroll: usize) {
|
|
if delta.is_negative() {
|
|
*scroll = scroll.saturating_sub(delta.unsigned_abs() as usize);
|
|
} else {
|
|
*scroll = scroll.saturating_add(delta as usize).min(max_scroll);
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEventKind};
|
|
use ratatui::layout::{Direction, Rect};
|
|
|
|
use super::super::{
|
|
app_for_mouse_test, capture_snapshot, handle_context_menu_key, mouse, root_layout_ratio,
|
|
};
|
|
use super::*;
|
|
use crate::{
|
|
app::state::{ContextMenuKind, ContextMenuState, MenuListState, Mode, ViewLayout},
|
|
detect::{Agent, AgentState},
|
|
workspace::Workspace,
|
|
};
|
|
|
|
#[test]
|
|
fn hovering_context_menu_updates_highlight() {
|
|
let mut app = app_for_mouse_test();
|
|
app.state.context_menu = Some(ContextMenuState {
|
|
kind: ContextMenuKind::Workspace { ws_idx: 0 },
|
|
x: 2,
|
|
y: 2,
|
|
list: MenuListState::new(0),
|
|
});
|
|
app.state.mode = Mode::ContextMenu;
|
|
|
|
let menu = app.state.context_menu_rect().unwrap();
|
|
app.handle_mouse(mouse(MouseEventKind::Moved, menu.x + 2, menu.y + 2));
|
|
|
|
assert_eq!(app.state.context_menu.unwrap().list.highlighted, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn clicking_agent_toast_focuses_target_pane() {
|
|
let mut app = app_for_mouse_test();
|
|
let active = Workspace::test_new("active");
|
|
let mut background = Workspace::test_new("background");
|
|
let first_pane = background.tabs[0].root_pane;
|
|
let target_pane = background.test_split(Direction::Horizontal);
|
|
background.tabs[0].layout.focus_pane(first_pane);
|
|
|
|
app.state.workspaces = vec![active, background];
|
|
app.state.active = Some(0);
|
|
app.state.selected = 0;
|
|
app.state.toast_config.delivery = crate::config::ToastDelivery::Herdr;
|
|
app.state.workspaces[1]
|
|
.panes
|
|
.get_mut(&target_pane)
|
|
.unwrap()
|
|
.state = AgentState::Working;
|
|
|
|
app.state
|
|
.handle_app_event(crate::events::AppEvent::StateChanged {
|
|
pane_id: target_pane,
|
|
agent: Some(Agent::Pi),
|
|
state: AgentState::Idle,
|
|
});
|
|
crate::ui::compute_view(&mut app.state, Rect::new(0, 0, 106, 20));
|
|
|
|
let hit = app.state.view.toast_hit_area;
|
|
app.handle_mouse(mouse(
|
|
MouseEventKind::Down(MouseButton::Left),
|
|
hit.x + 1,
|
|
hit.y + 1,
|
|
));
|
|
|
|
assert_eq!(app.state.active, Some(1));
|
|
assert_eq!(app.state.workspaces[1].focused_pane_id(), Some(target_pane));
|
|
assert!(app.state.toast.is_none());
|
|
assert_eq!(app.state.mode, Mode::Terminal);
|
|
}
|
|
|
|
#[test]
|
|
fn clicking_confirm_close_accepts_workspace_close() {
|
|
let mut app = app_for_mouse_test();
|
|
app.state.workspaces = vec![Workspace::test_new("a"), Workspace::test_new("b")];
|
|
app.state.active = Some(0);
|
|
app.state.selected = 1;
|
|
app.state.mode = Mode::ConfirmClose;
|
|
|
|
let popup = app.state.confirm_close_rect();
|
|
let inner = Rect::new(
|
|
popup.x + 1,
|
|
popup.y + 1,
|
|
popup.width.saturating_sub(2),
|
|
popup.height.saturating_sub(2),
|
|
);
|
|
let (confirm, _) = crate::ui::confirm_close_button_rects(inner);
|
|
|
|
app.handle_mouse(mouse(
|
|
MouseEventKind::Down(MouseButton::Left),
|
|
confirm.x,
|
|
confirm.y,
|
|
));
|
|
|
|
assert_eq!(app.state.workspaces.len(), 1);
|
|
assert_eq!(app.state.mode, Mode::Terminal);
|
|
}
|
|
|
|
#[test]
|
|
fn clicking_confirm_close_accepts_after_workspace_context_menu_close() {
|
|
let mut app = app_for_mouse_test();
|
|
app.state.workspaces = vec![Workspace::test_new("a"), Workspace::test_new("b")];
|
|
app.state.active = Some(0);
|
|
app.state.selected = 0;
|
|
app.state.mode = Mode::Terminal;
|
|
|
|
app.state.context_menu = Some(ContextMenuState {
|
|
kind: ContextMenuKind::Workspace { ws_idx: 1 },
|
|
x: 2,
|
|
y: 2,
|
|
list: MenuListState::new(1),
|
|
});
|
|
app.state.mode = Mode::ContextMenu;
|
|
handle_context_menu_key(
|
|
&mut app.state,
|
|
KeyEvent::new(KeyCode::Enter, KeyModifiers::empty()),
|
|
);
|
|
assert_eq!(app.state.mode, Mode::ConfirmClose);
|
|
assert_eq!(app.state.selected, 1);
|
|
|
|
let popup = app.state.confirm_close_rect();
|
|
let inner = Rect::new(
|
|
popup.x + 1,
|
|
popup.y + 1,
|
|
popup.width.saturating_sub(2),
|
|
popup.height.saturating_sub(2),
|
|
);
|
|
let (confirm, _) = crate::ui::confirm_close_button_rects(inner);
|
|
app.handle_mouse(mouse(
|
|
MouseEventKind::Down(MouseButton::Left),
|
|
confirm.x + 1,
|
|
confirm.y,
|
|
));
|
|
|
|
assert_eq!(app.state.workspaces.len(), 1);
|
|
assert_eq!(app.state.workspaces[0].display_name(), "a");
|
|
}
|
|
|
|
#[test]
|
|
fn dragging_pane_split_updates_captured_layout_ratio() {
|
|
let mut app = app_for_mouse_test();
|
|
app.state.workspaces = vec![Workspace::test_new("test")];
|
|
app.state.active = Some(0);
|
|
app.state.selected = 0;
|
|
app.state.workspaces[0].test_split(Direction::Horizontal);
|
|
crate::ui::compute_view(&mut app.state, Rect::new(0, 0, 106, 20));
|
|
let border = app.state.view.split_borders[0].clone();
|
|
let before = capture_snapshot(&app.state);
|
|
let drag_row = border.area.y.saturating_add(1);
|
|
|
|
app.handle_mouse(mouse(
|
|
MouseEventKind::Down(MouseButton::Left),
|
|
border.pos,
|
|
drag_row,
|
|
));
|
|
app.handle_mouse(mouse(
|
|
MouseEventKind::Drag(MouseButton::Left),
|
|
border.pos.saturating_add(6),
|
|
drag_row,
|
|
));
|
|
|
|
let after = capture_snapshot(&app.state);
|
|
assert_ne!(root_layout_ratio(&before), root_layout_ratio(&after));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn dragging_vertical_pane_split_still_resizes_when_pane_mouse_reporting_is_enabled() {
|
|
let mut app = app_for_mouse_test();
|
|
let mut ws = Workspace::test_new("test");
|
|
let first_pane = ws.tabs[0].root_pane;
|
|
let second_pane = ws.test_split(Direction::Vertical);
|
|
|
|
app.state.workspaces = vec![ws];
|
|
app.state.active = Some(0);
|
|
app.state.selected = 0;
|
|
|
|
crate::ui::compute_view(&mut app.state, Rect::new(0, 0, 106, 20));
|
|
|
|
let pane_infos = app.state.view.pane_infos.clone();
|
|
let first_info = pane_infos
|
|
.iter()
|
|
.find(|info| info.id == first_pane)
|
|
.expect("first pane info")
|
|
.clone();
|
|
let second_info = pane_infos
|
|
.iter()
|
|
.find(|info| info.id == second_pane)
|
|
.expect("second pane info")
|
|
.clone();
|
|
|
|
app.state.workspaces[0].tabs[0].runtimes.insert(
|
|
first_pane,
|
|
crate::pane::PaneRuntime::test_with_screen_bytes(
|
|
first_info.inner_rect.width.max(1),
|
|
first_info.inner_rect.height.max(1),
|
|
b"\x1b[?1002h",
|
|
),
|
|
);
|
|
app.state.workspaces[0].tabs[0].runtimes.insert(
|
|
second_pane,
|
|
crate::pane::PaneRuntime::test_with_screen_bytes(
|
|
second_info.inner_rect.width.max(1),
|
|
second_info.inner_rect.height.max(1),
|
|
b"\x1b[?1002h",
|
|
),
|
|
);
|
|
|
|
crate::ui::compute_view(&mut app.state, Rect::new(0, 0, 106, 20));
|
|
let border = app
|
|
.state
|
|
.view
|
|
.split_borders
|
|
.iter()
|
|
.find(|border| border.direction == Direction::Vertical)
|
|
.expect("vertical split border")
|
|
.clone();
|
|
let before = capture_snapshot(&app.state);
|
|
let drag_col = border.area.x.saturating_add(1);
|
|
|
|
app.handle_mouse(mouse(
|
|
MouseEventKind::Down(MouseButton::Left),
|
|
drag_col,
|
|
border.pos,
|
|
));
|
|
app.handle_mouse(mouse(
|
|
MouseEventKind::Drag(MouseButton::Left),
|
|
drag_col,
|
|
border.pos.saturating_add(4),
|
|
));
|
|
|
|
let after = capture_snapshot(&app.state);
|
|
assert_ne!(root_layout_ratio(&before), root_layout_ratio(&after));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn dragging_horizontal_pane_split_still_resizes_when_pane_mouse_reporting_is_enabled() {
|
|
let mut app = app_for_mouse_test();
|
|
let mut ws = Workspace::test_new("test");
|
|
let first_pane = ws.tabs[0].root_pane;
|
|
let second_pane = ws.test_split(Direction::Horizontal);
|
|
|
|
app.state.workspaces = vec![ws];
|
|
app.state.active = Some(0);
|
|
app.state.selected = 0;
|
|
|
|
crate::ui::compute_view(&mut app.state, Rect::new(0, 0, 106, 20));
|
|
|
|
let pane_infos = app.state.view.pane_infos.clone();
|
|
let first_info = pane_infos
|
|
.iter()
|
|
.find(|info| info.id == first_pane)
|
|
.expect("first pane info")
|
|
.clone();
|
|
let second_info = pane_infos
|
|
.iter()
|
|
.find(|info| info.id == second_pane)
|
|
.expect("second pane info")
|
|
.clone();
|
|
|
|
app.state.workspaces[0].tabs[0].runtimes.insert(
|
|
first_pane,
|
|
crate::pane::PaneRuntime::test_with_screen_bytes(
|
|
first_info.inner_rect.width.max(1),
|
|
first_info.inner_rect.height.max(1),
|
|
b"\x1b[?1002h",
|
|
),
|
|
);
|
|
app.state.workspaces[0].tabs[0].runtimes.insert(
|
|
second_pane,
|
|
crate::pane::PaneRuntime::test_with_screen_bytes(
|
|
second_info.inner_rect.width.max(1),
|
|
second_info.inner_rect.height.max(1),
|
|
b"\x1b[?1002h",
|
|
),
|
|
);
|
|
|
|
crate::ui::compute_view(&mut app.state, Rect::new(0, 0, 106, 20));
|
|
let border = app
|
|
.state
|
|
.view
|
|
.split_borders
|
|
.iter()
|
|
.find(|border| border.direction == Direction::Horizontal)
|
|
.expect("horizontal split border")
|
|
.clone();
|
|
let before = capture_snapshot(&app.state);
|
|
let drag_row = border.area.y.saturating_add(1);
|
|
|
|
app.handle_mouse(mouse(
|
|
MouseEventKind::Down(MouseButton::Left),
|
|
border.pos,
|
|
drag_row,
|
|
));
|
|
app.handle_mouse(mouse(
|
|
MouseEventKind::Drag(MouseButton::Left),
|
|
border.pos.saturating_add(6),
|
|
drag_row,
|
|
));
|
|
|
|
let after = capture_snapshot(&app.state);
|
|
assert_ne!(root_layout_ratio(&before), root_layout_ratio(&after));
|
|
}
|
|
|
|
#[test]
|
|
fn wheel_routing_prefers_mouse_reporting() {
|
|
let input_state = crate::pane::InputState {
|
|
alternate_screen: true,
|
|
application_cursor: false,
|
|
bracketed_paste: false,
|
|
focus_reporting: false,
|
|
mouse_protocol_mode: crate::input::MouseProtocolMode::ButtonMotion,
|
|
mouse_protocol_encoding: crate::input::MouseProtocolEncoding::Sgr,
|
|
mouse_alternate_scroll: true,
|
|
};
|
|
|
|
assert_eq!(wheel_routing(input_state), WheelRouting::MouseReport);
|
|
}
|
|
|
|
#[test]
|
|
fn wheel_over_tab_bar_switches_tabs() {
|
|
let mut app = app_for_mouse_test();
|
|
let mut ws = Workspace::test_new("one");
|
|
ws.test_add_tab(Some("two"));
|
|
ws.test_add_tab(Some("three"));
|
|
app.state.workspaces = vec![ws];
|
|
app.state.active = Some(0);
|
|
app.state.selected = 0;
|
|
app.state.mode = Mode::Terminal;
|
|
|
|
crate::ui::compute_view(&mut app.state, Rect::new(0, 0, 106, 20));
|
|
let tab_bar = app.state.view.tab_bar_rect;
|
|
|
|
app.handle_mouse(mouse(MouseEventKind::ScrollDown, tab_bar.x + 1, tab_bar.y));
|
|
assert_eq!(app.state.workspaces[0].active_tab, 1);
|
|
|
|
app.handle_mouse(mouse(MouseEventKind::ScrollUp, tab_bar.x + 1, tab_bar.y));
|
|
assert_eq!(app.state.workspaces[0].active_tab, 0);
|
|
|
|
app.handle_mouse(mouse(MouseEventKind::ScrollUp, tab_bar.x + 1, tab_bar.y));
|
|
assert_eq!(app.state.workspaces[0].active_tab, 2);
|
|
|
|
app.handle_mouse(mouse(
|
|
MouseEventKind::ScrollDown,
|
|
tab_bar.x + tab_bar.width.saturating_sub(1),
|
|
tab_bar.y,
|
|
));
|
|
assert_eq!(app.state.workspaces[0].active_tab, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn wheel_over_overflowing_tab_bar_switches_tabs() {
|
|
let mut app = app_for_mouse_test();
|
|
let mut ws = Workspace::test_new("one");
|
|
ws.tabs[0].set_custom_name("very-long-one".into());
|
|
ws.test_add_tab(Some("very-long-two"));
|
|
ws.test_add_tab(Some("very-long-three"));
|
|
app.state.workspaces = vec![ws];
|
|
app.state.active = Some(0);
|
|
app.state.selected = 0;
|
|
app.state.mode = Mode::Terminal;
|
|
|
|
crate::ui::compute_view(&mut app.state, Rect::new(0, 0, 65, 20));
|
|
assert!(app.state.view.tab_scroll_right_hit_area.width > 0);
|
|
let tab_bar = app.state.view.tab_bar_rect;
|
|
|
|
app.handle_mouse(mouse(
|
|
MouseEventKind::ScrollDown,
|
|
tab_bar.x + tab_bar.width.saturating_sub(2),
|
|
tab_bar.y,
|
|
));
|
|
assert_eq!(app.state.workspaces[0].active_tab, 1);
|
|
|
|
app.handle_mouse(mouse(
|
|
MouseEventKind::ScrollDown,
|
|
tab_bar.x + tab_bar.width.saturating_sub(2),
|
|
tab_bar.y,
|
|
));
|
|
assert_eq!(app.state.workspaces[0].active_tab, 2);
|
|
}
|
|
|
|
#[test]
|
|
fn wheel_outside_tab_bar_does_not_switch_tabs() {
|
|
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::Terminal;
|
|
|
|
crate::ui::compute_view(&mut app.state, Rect::new(0, 0, 106, 20));
|
|
let terminal = app.state.view.terminal_area;
|
|
|
|
app.handle_mouse(mouse(
|
|
MouseEventKind::ScrollDown,
|
|
terminal.x + 1,
|
|
terminal.y + 1,
|
|
));
|
|
|
|
assert_eq!(app.state.workspaces[0].active_tab, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn mobile_switch_button_opens_switcher_and_workspace_row_switches_workspace() {
|
|
let mut app = app_for_mouse_test();
|
|
app.state.workspaces = vec![Workspace::test_new("one"), Workspace::test_new("two")];
|
|
app.state.active = Some(0);
|
|
app.state.selected = 0;
|
|
app.state.mode = Mode::Terminal;
|
|
|
|
crate::ui::compute_view(&mut app.state, Rect::new(0, 0, 44, 20));
|
|
assert_eq!(app.state.view.layout, ViewLayout::Mobile);
|
|
|
|
let switch = app.state.view.mobile_menu_hit_area;
|
|
app.handle_mouse(mouse(
|
|
MouseEventKind::Down(MouseButton::Left),
|
|
switch.x + 1,
|
|
switch.y + 1,
|
|
));
|
|
|
|
assert_eq!(app.state.mode, Mode::Navigate);
|
|
|
|
let viewport = crate::ui::mobile_switcher_areas(&app.state).viewport;
|
|
app.handle_mouse(mouse(
|
|
MouseEventKind::Down(MouseButton::Left),
|
|
viewport.x + 2,
|
|
viewport.y + 4,
|
|
));
|
|
|
|
assert_eq!(app.state.active, Some(1));
|
|
assert_eq!(app.state.mode, Mode::Terminal);
|
|
}
|
|
|
|
#[test]
|
|
fn mobile_workspace_panel_scroll_reaches_extra_workspaces() {
|
|
let mut app = app_for_mouse_test();
|
|
app.state.workspaces = (0..12)
|
|
.map(|idx| Workspace::test_new(&format!("ws-{idx}")))
|
|
.collect();
|
|
app.state.active = Some(0);
|
|
app.state.selected = 0;
|
|
app.state.mode = Mode::Terminal;
|
|
|
|
crate::ui::compute_view(&mut app.state, Rect::new(0, 0, 44, 20));
|
|
let switch = app.state.view.mobile_menu_hit_area;
|
|
app.handle_mouse(mouse(
|
|
MouseEventKind::Down(MouseButton::Left),
|
|
switch.x + 1,
|
|
switch.y + 1,
|
|
));
|
|
assert_eq!(app.state.mode, Mode::Navigate);
|
|
|
|
let viewport = crate::ui::mobile_switcher_areas(&app.state).viewport;
|
|
app.handle_mouse(mouse(
|
|
MouseEventKind::ScrollDown,
|
|
viewport.x + 2,
|
|
viewport.y,
|
|
));
|
|
crate::ui::compute_view(&mut app.state, Rect::new(0, 0, 44, 20));
|
|
assert_eq!(app.state.mobile_switcher_scroll, 2);
|
|
|
|
app.handle_mouse(mouse(
|
|
MouseEventKind::Down(MouseButton::Left),
|
|
viewport.x + 2,
|
|
viewport.y + 2,
|
|
));
|
|
|
|
assert_eq!(app.state.active, Some(1));
|
|
assert_eq!(app.state.mode, Mode::Terminal);
|
|
}
|
|
|
|
#[test]
|
|
fn mobile_global_scroll_reaches_tabs_and_switches_tab() {
|
|
let mut app = app_for_mouse_test();
|
|
let mut ws = Workspace::test_new("one");
|
|
ws.test_add_tab(Some("two"));
|
|
ws.test_add_tab(Some("three"));
|
|
ws.test_add_tab(Some("four"));
|
|
app.state.workspaces = vec![ws];
|
|
app.state.active = Some(0);
|
|
app.state.selected = 0;
|
|
app.state.mode = Mode::Terminal;
|
|
|
|
crate::ui::compute_view(&mut app.state, Rect::new(0, 0, 44, 12));
|
|
let switch = app.state.view.mobile_menu_hit_area;
|
|
app.handle_mouse(mouse(
|
|
MouseEventKind::Down(MouseButton::Left),
|
|
switch.x + 1,
|
|
switch.y + 1,
|
|
));
|
|
|
|
let viewport = crate::ui::mobile_switcher_areas(&app.state).viewport;
|
|
|
|
app.handle_mouse(mouse(
|
|
MouseEventKind::ScrollDown,
|
|
viewport.x + 2,
|
|
viewport.y,
|
|
));
|
|
app.handle_mouse(mouse(
|
|
MouseEventKind::ScrollDown,
|
|
viewport.x + 2,
|
|
viewport.y,
|
|
));
|
|
assert_eq!(app.state.mobile_switcher_scroll, 4);
|
|
app.handle_mouse(mouse(
|
|
MouseEventKind::Down(MouseButton::Left),
|
|
viewport.x + 2,
|
|
viewport.y + 4,
|
|
));
|
|
assert_eq!(app.state.workspaces[0].active_tab, 2);
|
|
}
|
|
|
|
#[test]
|
|
fn mobile_switcher_action_rows_create_workspace_and_open_tab_dialog() {
|
|
let mut app = app_for_mouse_test();
|
|
let mut ws = Workspace::test_new("one");
|
|
ws.test_add_tab(Some("logs"));
|
|
app.state.workspaces = vec![ws];
|
|
app.state.active = Some(0);
|
|
app.state.selected = 0;
|
|
app.state.mode = Mode::Terminal;
|
|
|
|
crate::ui::compute_view(&mut app.state, Rect::new(0, 0, 44, 20));
|
|
let switch = app.state.view.mobile_menu_hit_area;
|
|
app.handle_mouse(mouse(
|
|
MouseEventKind::Down(MouseButton::Left),
|
|
switch.x + 1,
|
|
switch.y + 1,
|
|
));
|
|
let viewport = crate::ui::mobile_switcher_areas(&app.state).viewport;
|
|
|
|
app.handle_mouse(mouse(
|
|
MouseEventKind::Down(MouseButton::Left),
|
|
viewport.x + 2,
|
|
viewport.y + 1,
|
|
));
|
|
assert!(app.state.request_new_workspace);
|
|
|
|
app.state.request_new_workspace = false;
|
|
app.state.mode = Mode::Navigate;
|
|
app.handle_mouse(mouse(
|
|
MouseEventKind::Down(MouseButton::Left),
|
|
viewport.x + 2,
|
|
viewport.y + 5,
|
|
));
|
|
assert_eq!(app.state.mode, Mode::RenameTab);
|
|
assert!(app.state.creating_new_tab);
|
|
}
|
|
|
|
#[test]
|
|
fn mobile_switcher_swallows_non_left_mouse_events() {
|
|
let mut app = app_for_mouse_test();
|
|
app.state.workspaces = vec![Workspace::test_new("one")];
|
|
app.state.active = Some(0);
|
|
app.state.selected = 0;
|
|
app.state.mode = Mode::Terminal;
|
|
|
|
crate::ui::compute_view(&mut app.state, Rect::new(0, 0, 44, 20));
|
|
let switch = app.state.view.mobile_menu_hit_area;
|
|
app.handle_mouse(mouse(
|
|
MouseEventKind::Down(MouseButton::Left),
|
|
switch.x + 1,
|
|
switch.y + 1,
|
|
));
|
|
assert_eq!(app.state.mode, Mode::Navigate);
|
|
|
|
let viewport = crate::ui::mobile_switcher_areas(&app.state).viewport;
|
|
app.handle_mouse(mouse(
|
|
MouseEventKind::Down(MouseButton::Right),
|
|
viewport.x + 2,
|
|
viewport.y + 2,
|
|
));
|
|
|
|
assert_eq!(app.state.mode, Mode::Navigate);
|
|
assert!(app.state.context_menu.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn mobile_switch_button_does_not_bypass_rename_modal() {
|
|
let mut app = app_for_mouse_test();
|
|
app.state.workspaces = vec![Workspace::test_new("one")];
|
|
app.state.active = Some(0);
|
|
app.state.selected = 0;
|
|
app.state.mode = Mode::RenameTab;
|
|
app.state.creating_new_tab = true;
|
|
app.state.name_input = "new tab".into();
|
|
|
|
crate::ui::compute_view(&mut app.state, Rect::new(0, 0, 44, 20));
|
|
let switch = app.state.view.mobile_menu_hit_area;
|
|
app.handle_mouse(mouse(
|
|
MouseEventKind::Down(MouseButton::Left),
|
|
switch.x + 1,
|
|
switch.y + 1,
|
|
));
|
|
|
|
assert_eq!(app.state.mode, Mode::Terminal);
|
|
assert!(!app.state.creating_new_tab);
|
|
assert!(!app.state.request_new_tab);
|
|
}
|
|
|
|
#[test]
|
|
fn mobile_switcher_close_returns_to_terminal() {
|
|
let mut app = app_for_mouse_test();
|
|
app.state.workspaces = vec![Workspace::test_new("one")];
|
|
app.state.active = Some(0);
|
|
app.state.selected = 0;
|
|
app.state.mode = Mode::Terminal;
|
|
|
|
crate::ui::compute_view(&mut app.state, Rect::new(0, 0, 44, 20));
|
|
let switch = app.state.view.mobile_menu_hit_area;
|
|
app.handle_mouse(mouse(
|
|
MouseEventKind::Down(MouseButton::Left),
|
|
switch.x + 1,
|
|
switch.y + 1,
|
|
));
|
|
assert_eq!(app.state.mode, Mode::Navigate);
|
|
|
|
let close = crate::ui::mobile_switcher_areas(&app.state).close;
|
|
app.handle_mouse(mouse(
|
|
MouseEventKind::Down(MouseButton::Left),
|
|
close.x + 1,
|
|
close.y,
|
|
));
|
|
|
|
assert_eq!(app.state.mode, Mode::Terminal);
|
|
}
|
|
|
|
#[test]
|
|
fn wheel_routing_uses_alternate_scroll_in_fullscreen_without_mouse_reporting() {
|
|
let input_state = crate::pane::InputState {
|
|
alternate_screen: true,
|
|
application_cursor: false,
|
|
bracketed_paste: false,
|
|
focus_reporting: false,
|
|
mouse_protocol_mode: crate::input::MouseProtocolMode::None,
|
|
mouse_protocol_encoding: crate::input::MouseProtocolEncoding::Default,
|
|
mouse_alternate_scroll: true,
|
|
};
|
|
|
|
assert_eq!(wheel_routing(input_state), WheelRouting::AlternateScroll);
|
|
}
|
|
|
|
#[test]
|
|
fn wheel_routing_falls_back_to_host_scrollback() {
|
|
let input_state = crate::pane::InputState {
|
|
alternate_screen: false,
|
|
application_cursor: false,
|
|
bracketed_paste: false,
|
|
focus_reporting: false,
|
|
mouse_protocol_mode: crate::input::MouseProtocolMode::None,
|
|
mouse_protocol_encoding: crate::input::MouseProtocolEncoding::Default,
|
|
mouse_alternate_scroll: true,
|
|
};
|
|
|
|
assert_eq!(wheel_routing(input_state), WheelRouting::HostScroll);
|
|
}
|
|
}
|