Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/core/engine/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2867,6 +2867,9 @@ pub struct Engine {
/// Cached hit data from the last paint of the terminal toolbar (find bar
/// or tab strip). Written at paint time; read by `resolve_terminal_toolbar_click`.
pub terminal_toolbar_hits: std::cell::RefCell<Option<TerminalToolbarHits>>,
/// Cached layout from the last paint of the menu bar strip.
/// Written at paint time; read by click/hover handlers.
pub menu_bar_layout: std::cell::RefCell<Option<quadraui::MenuBarLayout>>,
/// Launch arguments stored between `initialize` send and response receipt.
/// We defer `launch`/`attach` until the adapter confirms `initialize` to avoid a race
/// where codelldb processes both requests concurrently and reads arguments
Expand Down Expand Up @@ -3536,6 +3539,7 @@ impl Engine {
bottom_panel_kind: BottomPanelKind::Terminal,
bottom_tab_bar_hits: std::cell::RefCell::new(None),
terminal_toolbar_hits: std::cell::RefCell::new(None),
menu_bar_layout: std::cell::RefCell::new(None),
dap_pending_launch: None,
bottom_panel_open: false,
dap_wants_sidebar: false,
Expand Down
106 changes: 42 additions & 64 deletions src/gtk/draw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2834,8 +2834,9 @@ pub(super) fn draw_command_line(
}
}

/// Returns `(back_x, back_end, fwd_x, fwd_end, unit_end)` — pixel hit rects for nav arrows
/// and the right edge of the entire interactive area (arrows + search box).
/// Returns `(MenuBarLayout, (back_x, back_end, fwd_x, fwd_end, unit_end))` — the quadraui
/// layout for menu label hit-testing plus pixel hit rects for nav arrows / search box.
#[allow(clippy::type_complexity)]
pub(super) fn draw_menu_bar(
cr: &Context,
data: &render::MenuBarData,
Expand All @@ -2844,63 +2845,43 @@ pub(super) fn draw_menu_bar(
y: f64,
width: f64,
height: f64,
) -> (f64, f64, f64, f64, f64) {
// Title bar background: use tab_bar_bg (adapts to light/dark themes).
let (tbr, tbg, tbb) = theme.tab_bar_bg.to_cairo();
cr.set_source_rgb(tbr, tbg, tbb);
cr.rectangle(x, y, width, height);
let _ = cr.fill();

) -> (quadraui::MenuBarLayout, (f64, f64, f64, f64, f64)) {
let pango_ctx = pangocairo::create_context(cr);
let font_desc = pango::FontDescription::from_string(&UI_FONT());
let layout = pango::Layout::new(&pango_ctx);
layout.set_font_description(Some(&font_desc));
let pango_layout = pango::Layout::new(&pango_ctx);
pango_layout.set_font_description(Some(&font_desc));

let (fr, fg, fb) = theme.foreground.to_cairo();
cr.set_source_rgb(fr, fg, fb);
let q_theme = super::quadraui_gtk::q_theme(theme);
let bar = render::build_menu_bar_view(data.open_menu_idx);
let mb_layout =
quadraui::gtk::draw_menu_bar(cr, &pango_layout, x, y, width, height, &bar, &q_theme);

// Menu labels
let mut cursor_x = x + 8.0;
let (fr, fg, fb) = theme.foreground.to_cairo();

for (idx, (name, _, _)) in render::MENU_STRUCTURE.iter().enumerate() {
let is_open = data.open_menu_idx == Some(idx);
if is_open {
let (ar, ag, ab) = theme.keyword.to_cairo();
cr.set_source_rgb(ar, ag, ab);
} else {
cr.set_source_rgb(fr, fg, fb);
}
layout.set_text(name);
let (_lw, lh) = layout.pixel_size();
cr.move_to(cursor_x, y + (height - lh as f64) / 2.0);
pangocairo::show_layout(cr, &layout);
// Use same metric as click/hover handlers: 7px/char + 10px padding.
cursor_x += name.len() as f64 * 7.0 + 10.0;
}
let menu_end_x = mb_layout
.visible_items
.last()
.map(|vi| x + (vi.bounds.x + vi.bounds.width) as f64)
.unwrap_or(x);

// Centered nav arrows + search box (like VSCode Command Center).
// The entire unit is centered between the menu labels and the right edge.
let menu_end_x = cursor_x;

// Measure arrow widths.
layout.set_text("\u{25C0}"); // ◀
let (back_w, _) = layout.pixel_size();
layout.set_text("\u{25B6}"); // ▶
let (fwd_w, _) = layout.pixel_size();
pango_layout.set_text("\u{25C0}"); // ◀
let (back_w, _) = pango_layout.pixel_size();
pango_layout.set_text("\u{25B6}"); // ▶
let (fwd_w, _) = pango_layout.pixel_size();
let arrow_gap = 6.0;
let arrows_w = back_w as f64 + arrow_gap + fwd_w as f64;

// Measure search box text.
let display = if data.title.is_empty() {
String::new()
} else {
format!("\u{1f50d} {}", data.title)
};
let box_pad = 12.0;
let min_box_w = 280.0; // minimum search bar width to match VSCode proportions
let min_box_w = 280.0;
let (box_text_w, _) = if !display.is_empty() {
layout.set_text(&display);
layout.pixel_size()
pango_layout.set_text(&display);
pango_layout.pixel_size()
} else {
(0, 0)
};
Expand All @@ -2912,11 +2893,9 @@ pub(super) fn draw_menu_bar(
let gap_between = if box_w > 0.0 { 10.0 } else { 0.0 };
let total_unit_w = arrows_w + gap_between + box_w;

// Center the unit between menu_end_x and right edge.
let available = x + width - menu_end_x;
let unit_x = (menu_end_x + (available - total_unit_w) / 2.0).max(menu_end_x + 8.0);

// Draw back arrow.
let dim_fg = theme.line_number_fg;
let back_color = if data.nav_back_enabled {
theme.foreground
Expand All @@ -2925,34 +2904,32 @@ pub(super) fn draw_menu_bar(
};
let (br2, bg2, bb2) = back_color.to_cairo();
cr.set_source_rgb(br2, bg2, bb2);
layout.set_text("\u{25C0}");
let (_, bh) = layout.pixel_size();
pango_layout.set_text("\u{25C0}");
pango_layout.set_attributes(None);
let (_, bh) = pango_layout.pixel_size();
cr.move_to(unit_x, y + (height - bh as f64) / 2.0);
pangocairo::show_layout(cr, &layout);
pangocairo::show_layout(cr, &pango_layout);

// Draw forward arrow.
let fwd_color = if data.nav_forward_enabled {
theme.foreground
} else {
dim_fg
};
let (fr2, fg2, fb2) = fwd_color.to_cairo();
cr.set_source_rgb(fr2, fg2, fb2);
layout.set_text("\u{25B6}");
let (_, fh) = layout.pixel_size();
pango_layout.set_text("\u{25B6}");
let (_, fh) = pango_layout.pixel_size();
cr.move_to(
unit_x + back_w as f64 + arrow_gap,
y + (height - fh as f64) / 2.0,
);
pangocairo::show_layout(cr, &layout);
pangocairo::show_layout(cr, &pango_layout);

// Draw search box.
if !display.is_empty() {
let bx = unit_x + arrows_w + gap_between;
let by = y + 3.0;
let bh_box = height - 6.0;
let radius = 4.0;
// Border
let (sr, sg, sb) = theme.separator.to_cairo();
cr.set_source_rgb(sr, sg, sb);
cr.new_path();
Expand Down Expand Up @@ -2987,23 +2964,24 @@ pub(super) fn draw_menu_bar(
cr.close_path();
cr.set_line_width(1.0);
let _ = cr.stroke();
// Text inside box — same color as menu labels (foreground)
cr.set_source_rgb(fr, fg, fb);
layout.set_text(&display);
let (_, th) = layout.pixel_size();
pango_layout.set_text(&display);
let (_, th) = pango_layout.pixel_size();
cr.move_to(bx + box_pad, y + (height - th as f64) / 2.0);
pangocairo::show_layout(cr, &layout);
pangocairo::show_layout(cr, &pango_layout);
}

// Return pixel hit rects for back and forward arrows + interactive area end.
let fwd_x = unit_x + back_w as f64 + arrow_gap;
let fwd_x_pos = unit_x + back_w as f64 + arrow_gap;
let unit_end = unit_x + total_unit_w;
(
unit_x,
unit_x + back_w as f64,
fwd_x,
fwd_x + fwd_w as f64,
unit_end,
mb_layout,
(
unit_x,
unit_x + back_w as f64,
fwd_x_pos,
fwd_x_pos + fwd_w as f64,
unit_end,
),
)
}

Expand Down
57 changes: 25 additions & 32 deletions src/gtk/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2219,6 +2219,8 @@ impl SimpleComponent for App {
#[allow(clippy::type_complexity)]
let nav_arrow_rects_cell: Rc<RefCell<(f64, f64, f64, f64, f64)>> =
Rc::new(RefCell::new((0.0, 0.0, 0.0, 0.0, 0.0)));
let menu_bar_layout_cell: Rc<RefCell<Option<quadraui::MenuBarLayout>>> =
Rc::new(RefCell::new(None));
let sidebar_inner_sw_ref: Rc<RefCell<Option<gtk4::ScrolledWindow>>> =
Rc::new(RefCell::new(None));
let sidebar_revealer_ref: Rc<RefCell<Option<gtk4::Revealer>>> = Rc::new(RefCell::new(None));
Expand Down Expand Up @@ -3037,9 +3039,9 @@ impl SimpleComponent for App {
{
let engine = engine.clone();
let nav_rects = nav_arrow_rects_cell.clone();
let mb_layout_draw = menu_bar_layout_cell.clone();
widgets.menu_bar_da.set_draw_func(move |da, cr, _w, _h| {
let engine = engine.borrow();
// Menu bar is always visible in GTK (acts as the window title bar).
let theme = Theme::from_name(&engine.settings.colorscheme);
let open_items: Vec<render::MenuItemData> = if let Some(midx) = engine.menu_open_idx
{
Expand Down Expand Up @@ -3080,38 +3082,35 @@ impl SimpleComponent for App {
};
let w = da.width() as f64;
let h = da.height() as f64;
let rects = draw_menu_bar(cr, &data, &theme, 0.0, 0.0, w, h);
*nav_rects.borrow_mut() = rects;
let (mb_layout, arrow_rects) = draw_menu_bar(cr, &data, &theme, 0.0, 0.0, w, h);
*nav_rects.borrow_mut() = arrow_rects;
*mb_layout_draw.borrow_mut() = Some(mb_layout);
});
}
// Click gesture: open/close individual menus (no hamburger zone here).
{
let sender_menu = sender.input_sender().clone();
let engine_menu = engine.clone();
let nav_rects_click = nav_arrow_rects_cell.clone();
let mb_layout_click = menu_bar_layout_cell.clone();
let gesture = gtk4::GestureClick::new();
gesture.set_button(1);
gesture.connect_pressed(move |gest, _, x, _y| {
let engine = engine_menu.borrow();
// Scan menu labels from left edge (no hamburger on this widget).
// Use ~7px/char + 10px padding as approximation for UI font metrics.
let mut cursor_x = 8.0_f64;
for (idx, (name, _, _)) in render::MENU_STRUCTURE.iter().enumerate() {
let item_w = name.len() as f64 * 7.0 + 10.0;
if x >= cursor_x && x < cursor_x + item_w {
if engine.menu_open_idx == Some(idx) {
sender_menu.send(Msg::CloseMenu).ok();
} else {
sender_menu.send(Msg::OpenMenu(idx)).ok();
}
return;
let mb_hit = mb_layout_click
.borrow()
.as_ref()
.map(|l| l.hit_test(x as f32, 0.5));
if let Some(quadraui::MenuBarHit::Item(idx)) = mb_hit {
if engine.menu_open_idx == Some(idx) {
sender_menu.send(Msg::CloseMenu).ok();
} else {
sender_menu.send(Msg::OpenMenu(idx)).ok();
}
cursor_x += item_w;
return;
}
// Use cached arrow pixel positions from draw_menu_bar.
let (back_x, back_end, fwd_x, fwd_end, unit_end) = *nav_rects_click.borrow();
if x >= back_x && x < back_end {
// Claim the gesture so WindowHandle doesn't maximize on double-click.
gest.set_state(gtk4::EventSequenceState::Claimed);
sender_menu.send(Msg::MruNavBack).ok();
return;
Expand All @@ -3121,18 +3120,14 @@ impl SimpleComponent for App {
sender_menu.send(Msg::MruNavForward).ok();
return;
}
// Click on the search box area → open Command Center.
if x >= fwd_end && x < unit_end {
gest.set_state(gtk4::EventSequenceState::Claimed);
sender_menu.send(Msg::OpenCommandCenter).ok();
return;
}
// Claim clicks within the nav+search box area to prevent
// WindowHandle double-click-to-maximize on the search box.
if x >= back_x && x < unit_end {
gest.set_state(gtk4::EventSequenceState::Claimed);
}
// Click in empty part of bar → close any open dropdown
if engine.menu_open_idx.is_some() {
sender_menu.send(Msg::CloseMenu).ok();
}
Expand All @@ -3143,23 +3138,21 @@ impl SimpleComponent for App {
{
let sender_hover = sender.input_sender().clone();
let engine_hover = engine.clone();
let mb_layout_hover = menu_bar_layout_cell.clone();
let motion = gtk4::EventControllerMotion::new();
motion.connect_motion(move |_, x, _y| {
let engine = engine_hover.borrow();
// Only switch if a menu is already open.
let Some(current) = engine.menu_open_idx else {
return;
};
let mut cursor_x = 8.0_f64;
for (idx, (name, _, _)) in render::MENU_STRUCTURE.iter().enumerate() {
let item_w = name.len() as f64 * 7.0 + 10.0;
if x >= cursor_x && x < cursor_x + item_w {
if idx != current {
sender_hover.send(Msg::OpenMenu(idx)).ok();
}
return;
let hit = mb_layout_hover
.borrow()
.as_ref()
.map(|l| l.hit_test(x as f32, 0.5));
if let Some(quadraui::MenuBarHit::Item(idx)) = hit {
if idx != current {
sender_hover.send(Msg::OpenMenu(idx)).ok();
}
cursor_x += item_w;
}
});
widgets.menu_bar_da.add_controller(motion);
Expand Down
20 changes: 20 additions & 0 deletions src/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3441,6 +3441,26 @@ pub fn menu_dropdown_action_index(id: &quadraui::WidgetId) -> Option<usize> {
id.as_str().strip_prefix("menu:")?.parse().ok()
}

/// Build a `quadraui::MenuBar` descriptor from the static `MENU_STRUCTURE`
/// and the engine's current open-menu state.
pub fn build_menu_bar_view(open_menu_idx: Option<usize>) -> quadraui::MenuBar {
let items = MENU_STRUCTURE
.iter()
.enumerate()
.map(|(i, (name, _alt_key, _))| quadraui::MenuBarItem {
id: quadraui::WidgetId::new(format!("menubar:{i}")),
label: format!("&{name}"),
disabled: false,
})
.collect();
quadraui::MenuBar {
id: quadraui::WidgetId::new("menubar"),
items,
open_item: open_menu_idx,
focused_item: None,
}
}

/// A modal dialog displayed over the editor.
#[derive(Debug, Clone)]
pub struct DialogPanel {
Expand Down
Loading