diff --git a/src/tui_main/panels.rs b/src/tui_main/panels.rs index 1bf219ff..7bcd47f3 100644 --- a/src/tui_main/panels.rs +++ b/src/tui_main/panels.rs @@ -26,16 +26,8 @@ pub(super) fn render_sidebar( theme: &Theme, _explorer_drop_target: Option, ) { - let buf = frame.buffer_mut(); - let default_fg = rc(theme.explorer_file_fg); - let row_bg = rc(theme.tab_bar_bg); - // Extension panel (plugin-provided) if sidebar.ext_panel_name.is_some() { - // Drop the buffer borrow before passing frame to render_ext_panel - // — the new TreeView-based renderer takes the backend + frame so it - // can route draw calls through quadraui primitives. - let _ = buf; render_ext_panel(backend, frame, area, engine, theme); return; } @@ -63,20 +55,64 @@ pub(super) fn render_sidebar( return; } Some(PANEL_AI) => { - render_ai_sidebar(buf, area, engine, theme); + render_ai_sidebar(frame.buffer_mut(), area, engine, theme); return; } _ => {} } - // ── Background fill — covers empty space below tree rows ──────────── + // Do NOT open a nested `enter_frame_scope` here — `render_sidebar` is + // called from `draw_frame`, which already runs inside the caller's + // single `with_frame_scope` (see mod.rs's `terminal.draw` closures). + // Re-entering would just be a no-op round trip on `current_frame_ptr`, + // but it contradicts the "entered once per draw closure" invariant. + render_explorer_sidebar_content(backend, area, engine, theme); +} + +/// Render the explorer tree panel's body: background fill + the +/// `TreeController` itself + its scroll-surface registration. +/// +/// Extracted from `render_sidebar`'s default (explorer) branch (#607) so +/// both the live `draw_frame` path (via `render_sidebar` above) and +/// `TuiShellApp::render_content` (`shell_app.rs`, which never has a raw +/// `Frame`/`Buffer` — see that module's doc comment) share one +/// implementation instead of two copies that could drift. The background +/// fill that used to be a raw `set_cell` loop over `frame.buffer_mut()` is +/// now painted via `Backend::draw_status_bar` with a single blank segment +/// per row — `draw_status_bar`'s TUI rasteriser always fills the *entire* +/// row with the first segment's `bg` before painting segment text +/// (`quadraui/src/tui/status_bar.rs`'s `fill_bg` loop), so an empty-text +/// segment is enough to reproduce the old solid-fill behavior exactly. This +/// is the same "solid `StatusBar` as background fill" trick quadraui's own +/// `AppShell::render` uses for its resize divider (`compose/app_shell.rs`'s +/// `divider_bounds` block) — the issue's suggested stand-in for raw +/// background fills that have no direct `Backend::draw_*` equivalent. +pub(super) fn render_explorer_sidebar_content( + backend: &mut dyn quadraui::Backend, + area: Rect, + engine: &Engine, + theme: &Theme, +) { if area.height == 0 { return; } + + backend.set_theme(super::quadraui_tui::q_theme(theme)); + + let bg_bar = quadraui::StatusBar { + id: quadraui::WidgetId::new("explorer:bg"), + left_segments: vec![quadraui::StatusBarSegment { + text: String::new(), + fg: render::to_quadraui_color(theme.explorer_file_fg), + bg: render::to_quadraui_color(theme.tab_bar_bg), + bold: false, + action_id: None, + }], + right_segments: vec![], + }; for y in area.y..area.y + area.height { - for x in area.x..area.x + area.width { - set_cell(buf, x, y, ' ', default_fg, row_bg); - } + let row_rect = quadraui::Rect::new(area.x as f32, y as f32, area.width as f32, 1.0); + let _ = backend.draw_status_bar(row_rect, &bg_bar, None, None); } let q_rect = quadraui::Rect::new( @@ -88,12 +124,7 @@ pub(super) fn render_sidebar( engine.explorer_tree_rect.set(q_rect); engine.explorer_viewport_rows.set(area.height as usize); render::populate_explorer_tree_controller(engine, theme); - // Do NOT open a nested `enter_frame_scope` here — `render_sidebar` is - // called from `draw_frame`, which already runs inside the caller's - // single `with_frame_scope` (see mod.rs's `terminal.draw` closures). - // Re-entering would just be a no-op round trip on `current_frame_ptr`, - // but it contradicts the "entered once per draw closure" invariant. - backend.set_current_theme(super::quadraui_tui::q_theme(theme)); + backend.set_theme(super::quadraui_tui::q_theme(theme)); engine.explorer_tree.borrow().render(backend, q_rect); // TreeController.render() draws the scrollbar internally. @@ -103,16 +134,52 @@ pub(super) fn render_sidebar( .borrow_mut() .push(quadraui::ScrollSurface { id: quadraui::WidgetId::new("explorer:sb"), - bounds: quadraui::Rect::new( - area.x as f32, - area.y as f32, - area.width as f32, - area.height as f32, - ), + bounds: q_rect, scrollbar: None, }); } +/// Sidebar panel content for [`super::shell_app::TuiShellApp::render_content`] +/// (#607). `render_sidebar` above stays the live `draw_frame` entry point +/// (unchanged behavior, still frame-having); this is a parallel, narrower +/// dispatcher over the subset of panels whose renderers need nothing but +/// `Backend::draw_*` trait calls — no raw `Frame`/`Buffer` access — mirroring +/// how `render_content` itself already has its own parallel entry points for +/// editor content (`build_screen_for_shell_content` + `paint_editor_popups` +/// in `render_impl.rs`, #601) and key dispatch +/// (`dispatch_panel_accelerator_sizeless`, `handle_key_pressed`, above in +/// `shell_app.rs`). +/// +/// Ported: explorer (default panel, via [`render_explorer_sidebar_content`]), +/// search (`render_search_panel`, already trait-pure — no raw buffer use at +/// all), debug (`render_debug_sidebar`, likewise already trait-pure). Every +/// other panel is a documented, deferred gap for this stage — see +/// `shell_app.rs`'s module doc for the specific raw-buffer blocker each one +/// hits (settings chrome, source-control header/clear/hint rows, the +/// extensions-sidebar header/search rows, the plugin extension panel's +/// chrome + popups, and the AI panel's fully-`Buffer`-typed signature). +pub(super) fn render_sidebar_content( + backend: &mut dyn quadraui::Backend, + area: Rect, + sidebar: &TuiSidebar, + engine: &Engine, + theme: &Theme, +) { + if sidebar.ext_panel_name.is_some() { + // Deferred — see this fn's doc comment and shell_app.rs's module doc. + return; + } + + match engine.app_shell.active_panel_id().map(|w| w.as_str()) { + Some(PANEL_SEARCH) => render_search_panel(backend, area, engine, theme), + Some(PANEL_DEBUG) => render_debug_sidebar(backend, area, engine, theme), + // Settings, source control, extensions, and AI: deferred — see this + // fn's doc comment and shell_app.rs's module doc. + Some(PANEL_SETTINGS | PANEL_GIT | PANEL_EXTENSIONS | PANEL_AI) => {} + _ => render_explorer_sidebar_content(backend, area, engine, theme), + } +} + /// Render the settings panel — shows current key settings and the file path. /// /// B5c.4: routes the form rendering through `Backend::draw_form` so @@ -189,8 +256,16 @@ pub(super) fn render_settings_panel( } /// Render the project search panel via SidebarSystem (Form + TreeView). +/// +/// `backend` is `&mut dyn quadraui::Backend` (not the concrete `TuiBackend`) +/// — this renderer was already trait-pure (no raw `Frame`/`Buffer` access), +/// so #607 widened the parameter the same way #601 did for +/// `render_tab_bar`/`draw_breadcrumb_bar`, letting +/// `TuiShellApp::render_content` call it via [`render_sidebar_content`] +/// without a concrete backend. `render_sidebar`'s own call site keeps compiling +/// unchanged: `&mut TuiBackend` coerces to `&mut dyn Backend` at the call. pub(super) fn render_search_panel( - backend: &mut super::backend::TuiBackend, + backend: &mut dyn quadraui::Backend, area: Rect, engine: &Engine, theme: &Theme, @@ -217,7 +292,7 @@ pub(super) fn render_search_panel( ); engine.search_sidebar_body_rect.set(q_rect); - backend.set_current_theme(super::quadraui_tui::q_theme(theme)); + backend.set_theme(super::quadraui_tui::q_theme(theme)); engine .search_sidebar_system .borrow() @@ -1191,14 +1266,14 @@ pub(super) fn render_ai_sidebar( /// section. Panel header (row 0) + Run/Stop button (row 1) + per-section /// title rows + per-section scrollbar overlays remain panel-specific /// chrome; item rendering goes through `Backend::draw_tree`. +/// #607: `backend` widened to `&mut dyn quadraui::Backend` — this renderer +/// was already trait-pure, same rationale as `render_search_panel` above. pub(super) fn render_debug_sidebar( - backend: &mut super::backend::TuiBackend, + backend: &mut dyn quadraui::Backend, area: Rect, engine: &Engine, theme: &Theme, ) { - use quadraui::Backend; - if area.height == 0 { return; } @@ -1212,7 +1287,7 @@ pub(super) fn render_debug_sidebar( let q_theme = super::quadraui_tui::q_theme(theme); let title_rect = quadraui::Rect::new(area.x as f32, area.y as f32, area.width as f32, 1.0); - backend.set_current_theme(q_theme); + backend.set_theme(q_theme); let _ = backend.draw_status_bar(title_rect, &title_bar, None, None); if area.height < 2 { @@ -1221,7 +1296,7 @@ pub(super) fn render_debug_sidebar( let action_rect = quadraui::Rect::new(area.x as f32, (area.y + 1) as f32, area.width as f32, 1.0); - backend.set_current_theme(q_theme); + backend.set_theme(q_theme); let hits = backend.draw_status_bar(action_rect, &action_bar, None, None); engine.dap_sidebar_action_hits.replace(Some(hits)); @@ -1237,7 +1312,7 @@ pub(super) fn render_debug_sidebar( ); engine.dap_sidebar_body_rect.set(msv_rect); render::populate_dap_sidebar_system(engine); - backend.set_current_theme(q_theme); + backend.set_theme(q_theme); engine.dap_sidebar_system.borrow().render(backend, msv_rect); } diff --git a/src/tui_main/shell_app.rs b/src/tui_main/shell_app.rs index c9315da8..c8d17f89 100644 --- a/src/tui_main/shell_app.rs +++ b/src/tui_main/shell_app.rs @@ -42,16 +42,45 @@ //! per-window status lines, and the completion/hover/editor-hover/ //! diff-peek/signature-help popups — into `render_content`, via //! `render_impl.rs::build_screen_for_shell_content` + -//! `paint_editor_popups`. What #601 still cannot paint (true raw-buffer +//! `paint_editor_popups`. #607 (Stage 2a) additionally wires the +//! trait-pure subset of sidebar panel content — explorer (the default +//! panel), search, debug — into `layout.sidebar_content_bounds`, via +//! `panels::render_sidebar_content`; see that function's doc comment for +//! the per-panel audit. What's still unpainted (true raw-buffer //! holdouts, each filed as its own follow-on, all blocking #605 -//! cutover): sidebar panel content (#607), quickfix panel + bottom -//! panel/terminal PTY content (#608), and window/group divider lines + -//! tab-drag overlay + tab-hover tooltip (#609). The menu bar row is -//! reserved in the layout math but not painted either (out of scope for -//! #601; folds into key dispatch, #603). Cursor placement used to be a -//! fourth raw-buffer holdout in this list (it needs `Frame:: -//! set_cursor_position`, and `render_content` has no `Frame`) but #604 -//! closed it a different way — see gap 3 below, now resolved. +//! cutover): +//! +//! - The rest of the sidebar (#607's own known-gap list, no separate +//! issue): **settings** (`render_settings_panel`'s header + search-box +//! chrome is a free `quadraui::tui::draw_settings_chrome` rasteriser +//! with no `Backend::draw_*` trait equivalent — its own "Stage 1 scope +//! note" doc comment already flags this; the form body below the +//! chrome *is* trait-pure via `FormController::render_and_cache`, but +//! painting only the body and leaving the chrome blank was judged not +//! worth the coordinate-mismatch risk for this stage), **source +//! control** (header row, focused-hint row, and full-area background +//! clear are raw `set_cell` loops over `frame.buffer_mut()` — the +//! `draw_status_bar`-blank-segment trick #607 used for the explorer's +//! background would work here too, just not attempted this stage), +//! **extensions** (`render_ext_sidebar`'s two chrome rows are the same +//! raw-`set_cell` pattern; likewise a `draw_status_bar` candidate), +//! the **plugin extension panel** (`render_ext_panel`'s chrome is the +//! same non-trait `draw_settings_chrome` as settings, *and* its help +//! popup overlay and manual scrollbar are raw `set_cell` box-drawing +//! with no primitive stand-in checked yet), and the **AI panel** +//! (`render_ai_sidebar` takes `buf: &mut ratatui::buffer::Buffer` +//! directly — no backend parameter at all, the most raw of the +//! lot). +//! - Quickfix panel + bottom panel/terminal PTY content (#608). +//! - Window/group divider lines + tab-drag overlay + tab-hover tooltip +//! (#609). +//! +//! The menu bar row is reserved in the layout math but not painted +//! either (out of scope for #601; folds into key dispatch, #603). +//! Cursor placement used to be a raw-buffer holdout in this list (it +//! needs `Frame::set_cursor_position`, and `render_content` has no +//! `Frame`) but #604 closed it a different way — see gap 3 below, now +//! resolved. //! 2. **Mouse handling (#602, largely resolved).** `mouse::handle_mouse` //! (~4,100 lines) takes `&mut quadraui::DragState` + `&mut //! quadraui::ModalStack` directly via `TuiBackend::drag_and_modal_mut()` @@ -701,9 +730,33 @@ impl ShellApp for TuiShellApp { // windows, tab bars, breadcrumb bars, per-window status lines, and // the editor-anchored popups — into `layout.main_content_bounds`. // See the module doc's gap (1) for exactly what's still deferred - // (sidebar content #607, quickfix/bottom panel #608, dividers/ - // drag-overlay/tab-tooltip #609, cursor placement #604) and why. + // (quickfix/bottom panel #608, dividers/drag-overlay/tab-tooltip + // #609, cursor placement #604) and why. let theme = self.theme(); + + // ── Sidebar panel content (#607) ───────────────────────────────── + // `AppShell::render` (quadraui, called by the runner before + // `render_content`) already painted the generic sidebar chrome + // (activity bar + header) — this paints the *active panel's* body + // into `layout.sidebar_content_bounds`. Independent of + // `main_content_bounds` below (distinct screen regions), so it + // isn't gated on that guard. See `panels::render_sidebar_content`'s + // doc comment for exactly which panels are ported (explorer — + // the default panel, search, debug) vs. still a documented, + // deferred gap for this stage (settings, source control, + // extensions, AI, the plugin extension panel). + if let Some(sb) = layout.sidebar_content_bounds { + if sb.width >= 1.0 && sb.height >= 1.0 { + let sb_area = Rect { + x: sb.x.round() as u16, + y: sb.y.round() as u16, + width: sb.width.round() as u16, + height: sb.height.round() as u16, + }; + render_sidebar_content(backend, sb_area, &self.sidebar, &self.engine, &theme); + } + } + let main = layout.main_content_bounds; if main.width < 1.0 || main.height < 1.0 { return; @@ -1538,6 +1591,45 @@ mod tests { ); } + /// #607: `render_content` must also paint the *sidebar's* content — + /// the explorer tree, since explorer is the default active panel — into + /// `layout.sidebar_content_bounds`, via + /// `panels::render_sidebar_content`. Mirrors the temp-dir-plus- + /// `explorer_rebuild_rows` pattern `core::engine::tests`'s explorer + /// tests already use (`test_goto_tab_reveals_file_in_explorer` et al.): + /// write a file with a distinctive name under a fresh temp dir, point + /// `engine.cwd` at it, and reveal the file via `explorer_reveal_path` + /// (which expands the root row *and* rebuilds the rows — the root + /// starts collapsed, so `explorer_rebuild_rows` alone would only show + /// the root folder's own row, not its children) before asserting the + /// file name shows up in the painted sidebar column. The marker is kept + /// short (well under `SIDEBAR_WIDTH`) so it survives the tree row's + /// icon/indent prefix without truncation. + #[test] + fn render_content_paints_explorer_sidebar_content_via_shell_app() { + let dir = std::env::temp_dir().join(format!( + "vimcode_test_607_shell_app_explorer_{:?}", + std::thread::current().id() + )); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let marker_file = dir.join("zqxw607.txt"); + std::fs::write(&marker_file, "marker").unwrap(); + + let mut app = TuiShellApp::new(None); + app.engine.cwd = dir.clone(); + app.engine.explorer_reveal_path(&marker_file); + + let driver = driver_with_shell(app, config(), 80, 24); + let screen = driver.screen(); + assert!( + screen.contains("zqxw607.txt"), + "explorer sidebar content should paint via TuiShellApp::render_content; screen:\n{screen}" + ); + + let _ = std::fs::remove_dir_all(&dir); + } + /// `dispatch_panel_accelerator_sizeless`'s `ACC_TERMINAL_TOGGLE_MAX` arm /// must derive `terminal_max_rows` from `screen_h` (the terminal's row /// count), not `screen_w` — the bug review iteration 1 of vimcode#595