diff --git a/src/gtk/mod.rs b/src/gtk/mod.rs index a8d6a063..61a80e53 100644 --- a/src/gtk/mod.rs +++ b/src/gtk/mod.rs @@ -456,134 +456,6 @@ struct App { backend: Rc>, } -/// Map GDK key names to the engine's expected key names. -/// -/// This is the canonical superset mapping — callers that only care about a -/// subset simply ignore the extra translations (they're harmless). -/// A.6f: adapter — build the `quadraui::ActivityBar` primitive that the -/// GTK activity bar DrawingArea renders each frame. -/// -/// Item order matches the pre-migration view! macro layout: -/// * Top: explorer · search · debug · git · extensions · AI -/// · dynamically-registered extension panels (sorted by name) -/// * Bottom: settings -/// -/// `is_keyboard_selected` is set from `engine.activity_bar_focused` + -/// `engine.activity_bar_selected` so the keyboard cursor renders on both -/// backends using the same engine state. Hover state is layered in by the -/// draw function via a separate `hovered_idx` parameter. -fn build_gtk_activity_bar_primitive( - engine: &crate::core::engine::Engine, - theme: &crate::render::Theme, -) -> quadraui::ActivityBar { - use crate::core::engine::sidebar::*; - let sb_visible = engine.app_shell.sidebar_visible(); - let has_ext = engine.ext_panel_active.is_some(); - let active_id = engine.app_shell.active_panel_id().map(|w| w.as_str()); - // Keyboard highlight: index 1-6 map to the fixed panel items (0-based in top[]). - // top[0] = hamburger (idx 0), top[1]=Explorer(1), ..., top[6]=AI(6), top[7+]=ext panels. - // bottom[0] = settings (idx 7). - let kbd_sel = |idx: u16| engine.activity_bar_focused && engine.activity_bar_selected == idx; - - // Note: GTK doesn't show a hamburger menu button (item 0) in the fixed list - // — the menu bar is toggled by keyboard only. The indices for fixed panels - // start at 1 (Explorer) matching the engine toolbar index. - let fixed: [(&str, &str, &str, &str, u16); 6] = [ - ( - PANEL_EXPLORER, - icons::EXPLORER.nerd, - "Explorer (Ctrl+Shift+E)", - "activity:explorer", - 1, - ), - ( - PANEL_SEARCH, - icons::SEARCH_COD.nerd, - "Search (Ctrl+Shift+F)", - "activity:search", - 2, - ), - (PANEL_DEBUG, icons::DEBUG.nerd, "Debug", "activity:debug", 3), - ( - PANEL_GIT, - icons::GIT_BRANCH.nerd, - "Source Control", - "activity:git", - 4, - ), - ( - PANEL_EXTENSIONS, - icons::EXTENSIONS.nerd, - "Extensions", - "activity:extensions", - 5, - ), - ( - PANEL_AI, - icons::AI_CHAT.nerd, - "AI Assistant", - "activity:ai", - 6, - ), - ]; - - let mut top: Vec = fixed - .iter() - .map( - |(panel_id, icon, tooltip, activity_id, toolbar_idx)| quadraui::ActivityItem { - id: quadraui::WidgetId::new(*activity_id), - icon: (*icon).to_string(), - tooltip: (*tooltip).to_string(), - is_active: sb_visible && !has_ext && active_id == Some(*panel_id), - is_keyboard_selected: kbd_sel(*toolbar_idx), - }, - ) - .collect(); - - let mut ext_panels: Vec<_> = engine.ext_panels.values().collect(); - ext_panels.sort_by(|a, b| a.name.cmp(&b.name)); - for (i, panel) in ext_panels.iter().enumerate() { - let is_active = sb_visible && engine.ext_panel_active.as_deref() == Some(&panel.name); - let toolbar_idx = 8 + i as u16; - top.push(quadraui::ActivityItem { - id: quadraui::WidgetId::new(format!("activity:ext:{}", panel.name)), - icon: panel.resolved_icon().to_string(), - tooltip: panel.title.clone(), - is_active, - is_keyboard_selected: kbd_sel(toolbar_idx), - }); - } - - let bottom = vec![quadraui::ActivityItem { - id: quadraui::WidgetId::new("activity:settings"), - icon: icons::SETTINGS.nerd.to_string(), - tooltip: "Settings".to_string(), - is_active: sb_visible && !has_ext && active_id == Some(PANEL_SETTINGS), - is_keyboard_selected: kbd_sel(7), - }]; - - quadraui::ActivityBar { - id: quadraui::WidgetId::new("activity-bar"), - top_items: top, - bottom_items: bottom, - active_accent: Some(quadraui::Color::rgb( - theme.cursor.r, - theme.cursor.g, - theme.cursor.b, - )), - selection_bg: Some(quadraui::Color::rgb( - theme.cursor.r, - theme.cursor.g, - theme.cursor.b, - )), - // Signals to the quadraui GTK backend that this bar owns the - // keyboard. The backend's draw_activity_bar impl records the bar ID - // in GtkBackend::focused_activity_bar so window-level key events can - // be converted to ActivityBarEvent::KeyPressed (Q#368). - is_keyboard_focused: engine.activity_bar_focused, - } -} - /// Decode an activity bar widget ID into a panel ID for `Msg::SwitchPanel`. fn activity_id_to_panel_id(id: &str) -> Option { match id { @@ -600,6 +472,10 @@ fn activity_id_to_panel_id(id: &str) -> Option { } } +/// Map GDK key names to the engine's expected key names. +/// +/// This is the canonical superset mapping — callers that only care about a +/// subset simply ignore the extra translations (they're harmless). fn map_gtk_key_name(gdk_name: &str) -> &str { match gdk_name { "Return" | "KP_Enter" => "Return", @@ -3339,7 +3215,12 @@ impl SimpleComponent for App { let theme = Theme::from_name(&engine.settings.colorscheme); let pango_ctx = pangocairo::create_context(cr); let layout = pango::Layout::new(&pango_ctx); - let bar = build_gtk_activity_bar_primitive(&engine, &theme); + let bar = crate::render::build_activity_bar( + &engine, + &theme, + false, + engine.ext_panel_active.as_deref(), + ); let hovered = hover_d.get(); let hits = quadraui::gtk::draw_activity_bar( cr, diff --git a/src/render.rs b/src/render.rs index adc39c91..ed664519 100644 --- a/src/render.rs +++ b/src/render.rs @@ -2795,7 +2795,8 @@ pub enum UiElement { TabTooltip, /// Diff toolbar (change navigation buttons in tab bar). DiffToolbar, - /// Activity bar (sidebar icon strip) — rendered by backends, not in ScreenLayout directly. + /// Activity bar (sidebar icon strip) — built by `render::build_activity_bar()` and + /// painted by `quadraui::{tui,gtk}::draw_activity_bar`; not stored in `ScreenLayout`. ActivityBar, /// Sidebar panel content — rendered by backends from ScreenLayout sidebar data. Sidebar, @@ -7404,6 +7405,145 @@ fn build_dap_bp_rows(engine: &Engine, session_active: bool) -> Vec, +) -> quadraui::ActivityBar { + use crate::core::engine::sidebar::{ + PANEL_AI, PANEL_DEBUG, PANEL_EXPLORER, PANEL_EXTENSIONS, PANEL_GIT, PANEL_SEARCH, + PANEL_SETTINGS, + }; + + let kbd_sel = |idx: u16| engine.activity_bar_focused && engine.activity_bar_selected == idx; + let sb_visible = engine.app_shell.sidebar_visible(); + let has_ext = active_ext_panel.is_some(); + let active_id = engine.app_shell.active_panel_id().map(|w| w.as_str()); + + let mut top = Vec::new(); + + if include_hamburger { + top.push(quadraui::ActivityItem { + id: quadraui::WidgetId::new("activity:menu"), + icon: icons::HAMBURGER.s().to_string(), + tooltip: "Menu".to_string(), + is_active: false, + is_keyboard_selected: kbd_sel(0), + }); + } + + // (toolbar_idx, panel_id, icon, tooltip, activity_id) + // Toolbar-keyboard selection indices: + // 0 = hamburger (TUI only), 1-6 = fixed panels, 7 = settings, 8+ = ext panels. + let fixed: [(u16, &str, &str, &str, &str); 6] = [ + ( + 1, + PANEL_EXPLORER, + icons::EXPLORER.s(), + "Explorer (Ctrl+Shift+E)", + "activity:explorer", + ), + ( + 2, + PANEL_SEARCH, + icons::SEARCH.s(), + "Search (Ctrl+Shift+F)", + "activity:search", + ), + (3, PANEL_DEBUG, icons::DEBUG.s(), "Debug", "activity:debug"), + ( + 4, + PANEL_GIT, + icons::GIT_BRANCH.s(), + "Source Control", + "activity:git", + ), + ( + 5, + PANEL_EXTENSIONS, + icons::EXTENSIONS.s(), + "Extensions", + "activity:extensions", + ), + ( + 6, + PANEL_AI, + icons::AI_CHAT.s(), + "AI Assistant", + "activity:ai", + ), + ]; + + for (toolbar_idx, panel_id, icon, tooltip, activity_id) in fixed { + top.push(quadraui::ActivityItem { + id: quadraui::WidgetId::new(activity_id), + icon: icon.to_string(), + tooltip: tooltip.to_string(), + is_active: sb_visible && !has_ext && active_id == Some(panel_id), + is_keyboard_selected: kbd_sel(toolbar_idx), + }); + } + + // Dynamic extension panels (sorted by name; toolbar indices 8+). + let mut ext_panels: Vec<_> = engine.ext_panels.values().collect(); + ext_panels.sort_by(|a, b| a.name.cmp(&b.name)); + for (i, panel) in ext_panels.iter().enumerate() { + let toolbar_idx = 8 + i as u16; + let is_active = sb_visible && active_ext_panel == Some(panel.name.as_str()); + top.push(quadraui::ActivityItem { + id: quadraui::WidgetId::new(format!("activity:ext:{}", panel.name)), + icon: panel.resolved_icon().to_string(), + tooltip: panel.title.clone(), + is_active, + is_keyboard_selected: kbd_sel(toolbar_idx), + }); + } + + let bottom = vec![quadraui::ActivityItem { + id: quadraui::WidgetId::new("activity:settings"), + icon: icons::SETTINGS.s().to_string(), + tooltip: "Settings".to_string(), + is_active: sb_visible && !has_ext && active_id == Some(PANEL_SETTINGS), + is_keyboard_selected: kbd_sel(7), + }]; + + quadraui::ActivityBar { + id: quadraui::WidgetId::new("activity-bar"), + top_items: top, + bottom_items: bottom, + active_accent: Some(quadraui::Color::rgb( + theme.cursor.r, + theme.cursor.g, + theme.cursor.b, + )), + selection_bg: Some(quadraui::Color::rgb( + theme.cursor.r, + theme.cursor.g, + theme.cursor.b, + )), + // Signals to the quadraui backend that this bar owns the keyboard so + // it intercepts KeyPressed as ActivityBarEvent::KeyPressed (Q#368). + is_keyboard_focused: engine.activity_bar_focused, + } +} + /// Adapt one section of the debug sidebar (`Variables` / `Watch` / /// `Call Stack` / `Breakpoints`) into a `quadraui::TreeView` for the /// shared `draw_tree` primitive (#281). diff --git a/src/tui_main/panels.rs b/src/tui_main/panels.rs index 19d47d1f..cc92e98b 100644 --- a/src/tui_main/panels.rs +++ b/src/tui_main/panels.rs @@ -8,118 +8,17 @@ pub(super) fn render_activity_bar( _menu_bar_visible: bool, engine: &Engine, ) { - // A.6e: activity bar rendering delegates to the `quadraui::ActivityBar` - // primitive. Build the declarative state from Engine (keyboard focus) + - // TuiSidebar (ext panel name), then call `draw_activity_bar`. - let bar = build_activity_bar_primitive(sidebar, engine, theme); + // Delegate to the shared adapter in render.rs (#133). TUI includes the + // hamburger item (index 0) because there is no native menu bar. + let bar = crate::render::build_activity_bar( + engine, + theme, + true, + sidebar.ext_panel_name.as_deref(), + ); super::quadraui_tui::draw_activity_bar(buf, area, &bar, theme); } -/// Build a `quadraui::ActivityBar` describing the current sidebar state. -/// -/// Item ordering (matches the pre-migration layout): -/// * Top: hamburger (menu) · explorer · search · debug · git · extensions -/// · AI · dynamically-registered extension panels -/// * Bottom: settings -/// -/// Toolbar-keyboard selection indices are preserved: -/// 0 = hamburger, 1-6 = fixed panels, 7 = settings, 8+ = extension panels. -fn build_activity_bar_primitive( - sidebar: &TuiSidebar, - engine: &Engine, - theme: &Theme, -) -> quadraui::ActivityBar { - // Keyboard highlight uses engine state (shared with GTK). - let kbd_sel = |idx: u16| engine.activity_bar_focused && engine.activity_bar_selected == idx; - let sb_visible = engine.app_shell.sidebar_visible(); - let has_ext_panel = sidebar.ext_panel_name.is_some(); - let active_id = engine.app_shell.active_panel_id().map(|w| w.as_str()); - - let mut top = Vec::new(); - top.push(quadraui::ActivityItem { - id: quadraui::WidgetId::new("activity:menu"), - icon: crate::icons::HAMBURGER.c().to_string(), - tooltip: "Menu".to_string(), - is_active: false, - is_keyboard_selected: kbd_sel(0), - }); - - let fixed: [(u16, &str, char, &str); 6] = [ - (1, PANEL_EXPLORER, crate::icons::EXPLORER.c(), "Explorer"), - (2, PANEL_SEARCH, crate::icons::SEARCH.c(), "Search"), - (3, PANEL_DEBUG, crate::icons::DEBUG.c(), "Debug"), - (4, PANEL_GIT, crate::icons::GIT_BRANCH.c(), "Source Control"), - ( - 5, - PANEL_EXTENSIONS, - crate::icons::EXTENSIONS.c(), - "Extensions", - ), - (6, PANEL_AI, crate::icons::AI_CHAT.c(), "AI Assistant"), - ]; - for (idx, panel_id, icon, tooltip) in fixed { - let activity_id = match panel_id { - PANEL_EXPLORER => "activity:explorer", - PANEL_SEARCH => "activity:search", - PANEL_DEBUG => "activity:debug", - PANEL_GIT => "activity:git", - PANEL_EXTENSIONS => "activity:extensions", - PANEL_AI => "activity:ai", - _ => "activity:unknown", - }; - top.push(quadraui::ActivityItem { - id: quadraui::WidgetId::new(activity_id), - icon: icon.to_string(), - tooltip: tooltip.to_string(), - is_active: sb_visible && !has_ext_panel && active_id == Some(panel_id), - is_keyboard_selected: kbd_sel(idx), - }); - } - - // Dynamic extension panels (sorted by name; toolbar indices 8+). - let mut ext_panels: Vec<_> = engine.ext_panels.values().collect(); - ext_panels.sort_by(|a, b| a.name.cmp(&b.name)); - for (i, panel) in ext_panels.iter().enumerate() { - let toolbar_idx = 8 + i as u16; - let is_active = sidebar.ext_panel_name.as_deref() == Some(&panel.name) && sb_visible; - top.push(quadraui::ActivityItem { - id: quadraui::WidgetId::new(format!("activity:ext:{}", panel.name)), - icon: panel.resolved_icon().to_string(), - tooltip: panel.title.clone(), - is_active, - is_keyboard_selected: kbd_sel(toolbar_idx), - }); - } - - let bottom = vec![quadraui::ActivityItem { - id: quadraui::WidgetId::new("activity:settings"), - icon: crate::icons::SETTINGS.c().to_string(), - tooltip: "Settings".to_string(), - is_active: sb_visible && !has_ext_panel && active_id == Some(PANEL_SETTINGS), - is_keyboard_selected: kbd_sel(7), - }]; - - quadraui::ActivityBar { - id: quadraui::WidgetId::new("activity-bar"), - top_items: top, - bottom_items: bottom, - active_accent: Some(quadraui::Color::rgb( - theme.cursor.r, - theme.cursor.g, - theme.cursor.b, - )), - selection_bg: Some(quadraui::Color::rgb( - theme.cursor.r, - theme.cursor.g, - theme.cursor.b, - )), - // Signals to the quadraui TUI backend that this bar owns the - // keyboard so it intercepts KeyPressed as ActivityBarEvent::KeyPressed - // (Q#368 protocol). - is_keyboard_focused: engine.activity_bar_focused, - } -} - // ─── Sidebar rendering ──────────────────────────────────────────────────────── pub(super) fn render_sidebar(