diff --git a/src/core/engine/mod.rs b/src/core/engine/mod.rs index 13a0e31e..0cc0c65a 100644 --- a/src/core/engine/mod.rs +++ b/src/core/engine/mod.rs @@ -1685,6 +1685,12 @@ pub struct ContextMenuState { pub selected: usize, pub screen_x: u16, pub screen_y: u16, + /// Height of the trigger element in line_height units (f32 so + /// backends with sub-cell row heights — e.g. GTK's tab row at + /// ceil(1.6 * line_height) — can place the menu flush against the + /// trigger). Non-zero opts into `ContextMenuPlacement::Below` + /// (#434); right-click flows leave this at 0.0 and use AnchorPoint. + pub trigger_height: f32, } /// One visible row in the flat explorer file-tree list. diff --git a/src/core/engine/tests.rs b/src/core/engine/tests.rs index 5a1fcd44..b52b6425 100644 --- a/src/core/engine/tests.rs +++ b/src/core/engine/tests.rs @@ -18381,7 +18381,7 @@ fn test_editor_action_menu_opens() { let mut e = Engine::new(); e.buffer_mut().insert(0, "hello\n"); let gid = e.active_group; - e.open_editor_action_menu(gid, 0, 0); + e.open_editor_action_menu(gid, 0, 0, 1.0); assert!(e.context_menu.is_some()); let cm = e.context_menu.as_ref().unwrap(); assert!(matches!( @@ -18402,7 +18402,7 @@ fn test_editor_action_menu_close_others_disabled_with_one_tab() { let mut e = Engine::new(); e.buffer_mut().insert(0, "hello\n"); let gid = e.active_group; - e.open_editor_action_menu(gid, 0, 0); + e.open_editor_action_menu(gid, 0, 0, 1.0); let cm = e.context_menu.as_ref().unwrap(); // "Close Others" disabled with only 1 tab. let close_others = cm @@ -18422,7 +18422,7 @@ fn test_editor_action_menu_toggle_wrap() { e.buffer_mut().insert(0, "hello\n"); let gid = e.active_group; assert!(!e.settings.wrap); - e.open_editor_action_menu(gid, 0, 0); + e.open_editor_action_menu(gid, 0, 0, 1.0); // Select "toggle_wrap" and confirm. if let Some(ref mut cm) = e.context_menu { if let Some(idx) = cm.items.iter().position(|i| i.action == "toggle_wrap") { @@ -18442,7 +18442,7 @@ fn test_editor_action_menu_close_all() { e.new_tab(None); assert!(e.active_group().tabs.len() >= 2); let gid = e.active_group; - e.open_editor_action_menu(gid, 0, 0); + e.open_editor_action_menu(gid, 0, 0, 1.0); if let Some(ref mut cm) = e.context_menu { if let Some(idx) = cm.items.iter().position(|i| i.action == "close_all") { cm.selected = idx; diff --git a/src/core/engine/windows.rs b/src/core/engine/windows.rs index 93c48c43..43516723 100644 --- a/src/core/engine/windows.rs +++ b/src/core/engine/windows.rs @@ -691,11 +691,18 @@ impl Engine { selected, screen_x: x, screen_y: y, + trigger_height: 0.0, }); } /// Open the editor action menu ("..." button) for a tab bar group. - pub fn open_editor_action_menu(&mut self, group_id: GroupId, x: u16, y: u16) { + pub fn open_editor_action_menu( + &mut self, + group_id: GroupId, + x: u16, + y: u16, + trigger_height: f32, + ) { let group = match self.editor_groups.get(&group_id) { Some(g) => g, None => return, @@ -775,6 +782,7 @@ impl Engine { selected, screen_x: x, screen_y: y, + trigger_height, }); } @@ -957,6 +965,7 @@ impl Engine { selected: 0, screen_x: x, screen_y: y, + trigger_height: 0.0, }); } @@ -1041,6 +1050,7 @@ impl Engine { selected, screen_x: x, screen_y: y, + trigger_height: 0.0, }); } diff --git a/src/gtk/click.rs b/src/gtk/click.rs index 8164481f..6eb88cc0 100644 --- a/src/gtk/click.rs +++ b/src/gtk/click.rs @@ -324,7 +324,12 @@ pub(super) fn handle_mouse_click( ClickTarget::ActionMenuButton(group_id) => { let col = (x / char_width.max(1.0)) as u16; let row = (y / line_height.max(1.0)) as u16; - engine.open_editor_action_menu(group_id, col, row); + // #434: pass the trigger's exact height in line_height units so + // the menu sits flush against the button's bottom (no sub-cell + // gap). GTK's tab row is ceil(1.6 * line_height). + let trigger_h = + (render_mod::tab_row_height_px(line_height) / line_height.max(1.0)) as f32; + engine.open_editor_action_menu(group_id, col, row, trigger_h); (None, None) } _ => (None, None), diff --git a/src/gtk/draw.rs b/src/gtk/draw.rs index dfd62401..c8277d9a 100644 --- a/src/gtk/draw.rs +++ b/src/gtk/draw.rs @@ -1956,10 +1956,19 @@ pub(super) fn draw_context_menu_popup( let anchor_x = cm.screen_col as f64 * char_width; let anchor_y = cm.screen_row as f64 * line_height; + let trigger_height_px = cm.trigger_height as f64 * line_height; + // trigger_height is in line_height units (f32 — see ContextMenuState + // docs). GTK's tab row is 1.6× line_height, so the caller passes 1.6 + // for the action button and the menu sits flush against the button + // bottom (#434). let viewport = quadraui::Rect::new(0.0, 0.0, editor_width as f32, editor_height as f32); - let menu_layout = menu.layout( - anchor_x as f32, - anchor_y as f32, + let menu_layout = menu.layout_at( + quadraui::Rect::new( + anchor_x as f32, + anchor_y as f32, + 0.0, + trigger_height_px as f32, + ), viewport, menu_w as f32, item_height, diff --git a/src/render.rs b/src/render.rs index adf75d2e..7157206d 100644 --- a/src/render.rs +++ b/src/render.rs @@ -3559,6 +3559,11 @@ pub struct ContextMenuPanel { pub selected_idx: usize, pub screen_col: u16, pub screen_row: u16, + /// Trigger element height in line_height units (f32; supports + /// sub-cell rows like GTK's 1.6× tab row). 0.0 = no trigger → + /// render at click coords (AnchorPoint). Non-zero opts into + /// `ContextMenuPlacement::Below` (#434). + pub trigger_height: f32, } /// A single rendered context menu item. @@ -3607,12 +3612,17 @@ pub fn context_menu_panel_to_quadraui_context_menu( .get(panel.selected_idx) .copied() .unwrap_or(0); + let placement = if panel.trigger_height > 0.0 { + quadraui::ContextMenuPlacement::Below + } else { + quadraui::ContextMenuPlacement::AnchorPoint + }; quadraui::ContextMenu { id: quadraui::WidgetId::new("context_menu"), items, selected_idx, bg: None, - placement: quadraui::ContextMenuPlacement::default(), + placement, } } @@ -6215,6 +6225,7 @@ pub fn build_screen_layout( selected_idx: cm.selected, screen_col: cm.screen_x, screen_row: cm.screen_y, + trigger_height: cm.trigger_height, }), find_replace: if engine.find_replace_open { let match_info = if engine.search_matches.is_empty() { diff --git a/src/tui_main/mouse.rs b/src/tui_main/mouse.rs index 9022cf5d..80b924fb 100644 --- a/src/tui_main/mouse.rs +++ b/src/tui_main/mouse.rs @@ -2524,7 +2524,10 @@ pub(super) fn handle_mouse( } TabBarClickTarget::ActionMenu => { engine.active_group = group_id; - engine.open_editor_action_menu(group_id, col, row + 1); + // #434: pass tab-row height (1.0 row in TUI) so the + // engine drives Below placement; replaces the prior + // `row + 1` hack. + engine.open_editor_action_menu(group_id, col, row, 1.0); } _ => { engine.handle_tab_bar_click(group_id, target); @@ -2610,7 +2613,8 @@ pub(super) fn handle_mouse( engine.open_editor_group(SplitDirection::Horizontal); } "tab:action_menu" => { - engine.open_editor_action_menu(engine.active_group, col, row + 1); + // #434: pass tab-row height (1.0 row in TUI). + engine.open_editor_action_menu(engine.active_group, col, row, 1.0); } _ => {} } diff --git a/src/tui_main/render_impl.rs b/src/tui_main/render_impl.rs index 2fdd1f8c..c5cc89bf 100644 --- a/src/tui_main/render_impl.rs +++ b/src/tui_main/render_impl.rs @@ -968,9 +968,13 @@ pub(super) fn draw_frame( .unwrap_or(0); let outer_width = (max_label + max_shortcut + 6).clamp(20, 50) as f32; let inner_width = (outer_width - 2.0).max(1.0); - let layout = menu.layout( - ctx_menu.screen_col as f32 + 1.0, - ctx_menu.screen_row as f32 + 1.0, + let layout = menu.layout_at( + quadraui::Rect::new( + ctx_menu.screen_col as f32 + 1.0, + ctx_menu.screen_row as f32 + 1.0, + 0.0, + ctx_menu.trigger_height, + ), inner_viewport, inner_width, |_| quadraui::ContextMenuItemMeasure::new(1.0),