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
141 changes: 108 additions & 33 deletions src/tui_main/panels.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,8 @@ pub(super) fn render_sidebar(
theme: &Theme,
_explorer_drop_target: Option<usize>,
) {
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;
}
Expand Down Expand Up @@ -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(
Expand All @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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()
Expand Down Expand Up @@ -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;
}
Expand All @@ -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 {
Expand All @@ -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));

Expand All @@ -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);
}

Expand Down
114 changes: 103 additions & 11 deletions src/tui_main/shell_app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()`
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Loading