#![allow(dead_code)] // Phase 1: screen content renders added in Phase 2+ //! Top-level view dispatch for the lore TUI. //! //! [`render_screen`] is the entry point called from `LoreApp::view()`. //! It composes the layout: breadcrumb bar, screen content area, status //! bar, and optional overlays (help, error toast). pub mod bootstrap; pub mod command_palette; pub mod common; pub mod dashboard; pub mod doctor; pub mod file_history; pub mod issue_detail; pub mod issue_list; pub mod mr_detail; pub mod mr_list; pub mod scope_picker; pub mod search; pub mod stats; pub mod sync; pub mod timeline; pub mod trace; pub mod who; use ftui::layout::{Constraint, Flex}; use ftui::render::cell::PackedRgba; use ftui::render::frame::Frame; use crate::app::LoreApp; use crate::message::Screen; use bootstrap::render_bootstrap; use command_palette::render_command_palette; use common::{ render_breadcrumb, render_error_toast, render_help_overlay, render_loading, render_status_bar, }; use dashboard::render_dashboard; use doctor::render_doctor; use file_history::render_file_history; use issue_detail::render_issue_detail; use issue_list::render_issue_list; use mr_detail::render_mr_detail; use mr_list::render_mr_list; use scope_picker::render_scope_picker; use search::render_search; use stats::render_stats; use sync::render_sync; use timeline::render_timeline; use trace::render_trace; use who::render_who; // --------------------------------------------------------------------------- // Colors (hardcoded Flexoki palette — will use Theme in Phase 2) // --------------------------------------------------------------------------- const TEXT: PackedRgba = PackedRgba::rgb(0xCE, 0xCD, 0xC3); // tx const TEXT_MUTED: PackedRgba = PackedRgba::rgb(0x87, 0x87, 0x80); // tx-2 const BG_SURFACE: PackedRgba = PackedRgba::rgb(0x28, 0x28, 0x24); // bg-2 const ACCENT: PackedRgba = PackedRgba::rgb(0xDA, 0x70, 0x2C); // orange const ERROR_BG: PackedRgba = PackedRgba::rgb(0xAF, 0x3A, 0x29); // red const ERROR_FG: PackedRgba = PackedRgba::rgb(0xCE, 0xCD, 0xC3); // tx const BORDER: PackedRgba = PackedRgba::rgb(0x87, 0x87, 0x80); // tx-2 // --------------------------------------------------------------------------- // render_screen // --------------------------------------------------------------------------- /// Top-level view dispatch: composes breadcrumb + content + status bar + overlays. /// /// Called from `LoreApp::view()`. The layout is: /// ```text /// +-----------------------------------+ /// | Breadcrumb (1 row) | /// +-----------------------------------+ /// | | /// | Screen content (fill) | /// | | /// +-----------------------------------+ /// | Status bar (1 row) | /// +-----------------------------------+ /// ``` /// /// Overlays (help, error toast) render on top of existing content. pub fn render_screen(frame: &mut Frame<'_>, app: &LoreApp) { let bounds = frame.bounds(); if bounds.width < 3 || bounds.height < 3 { return; // Terminal too small to render anything useful. } // Split vertically: breadcrumb (1) | content (fill) | status bar (1). let regions = Flex::vertical() .constraints([ Constraint::Fixed(1), // breadcrumb Constraint::Fill, // content Constraint::Fixed(1), // status bar ]) .split(bounds); let breadcrumb_area = regions[0]; let content_area = regions[1]; let status_area = regions[2]; let screen = app.navigation.current(); // --- Breadcrumb --- render_breadcrumb(frame, breadcrumb_area, &app.navigation, TEXT, TEXT_MUTED); // --- Screen content --- let load_state = app.state.load_state.get(screen); // tick=0 placeholder — animation wired up when Msg::Tick increments a counter. render_loading(frame, content_area, load_state, TEXT, TEXT_MUTED, 0); // Per-screen content dispatch (other screens wired in later phases). if screen == &Screen::Bootstrap { render_bootstrap(frame, &app.state.bootstrap, content_area); } else if screen == &Screen::Sync { render_sync(frame, &app.state.sync, content_area); } else if screen == &Screen::Dashboard { render_dashboard(frame, &app.state.dashboard, content_area); } else if screen == &Screen::IssueList { render_issue_list(frame, &app.state.issue_list, content_area); } else if screen == &Screen::MrList { render_mr_list(frame, &app.state.mr_list, content_area); } else if matches!(screen, Screen::IssueDetail(_)) { render_issue_detail(frame, &app.state.issue_detail, content_area, &*app.clock); } else if matches!(screen, Screen::MrDetail(_)) { render_mr_detail(frame, &app.state.mr_detail, content_area, &*app.clock); } else if screen == &Screen::Search { render_search(frame, &app.state.search, content_area); } else if screen == &Screen::Timeline { render_timeline(frame, &app.state.timeline, content_area, &*app.clock); } else if screen == &Screen::Who { render_who(frame, &app.state.who, content_area); } else if screen == &Screen::FileHistory { render_file_history(frame, &app.state.file_history, content_area); } else if screen == &Screen::Trace { render_trace(frame, &app.state.trace, content_area); } else if screen == &Screen::Doctor { render_doctor(frame, &app.state.doctor, content_area); } else if screen == &Screen::Stats { render_stats(frame, &app.state.stats, content_area); } // --- Status bar --- render_status_bar( frame, status_area, &app.command_registry, screen, &app.input_mode, BG_SURFACE, TEXT, ACCENT, ); // --- Overlays (render last, on top of everything) --- // Error toast. if let Some(ref error_msg) = app.state.error_toast { render_error_toast(frame, bounds, error_msg, ERROR_BG, ERROR_FG); } // Command palette overlay. render_command_palette(frame, &app.state.command_palette, bounds); // Scope picker overlay. render_scope_picker( frame, &app.state.scope_picker, &app.state.global_scope, bounds, ); // Help overlay. if app.state.show_help { render_help_overlay( frame, bounds, &app.command_registry, screen, BORDER, TEXT, TEXT_MUTED, 0, // scroll_offset — tracked in future phase ); } } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- #[cfg(test)] mod tests { use super::*; use crate::app::LoreApp; use ftui::render::grapheme_pool::GraphemePool; macro_rules! with_frame { ($width:expr, $height:expr, |$frame:ident| $body:block) => {{ let mut pool = GraphemePool::new(); let mut $frame = Frame::new($width, $height, &mut pool); $body }}; } #[test] fn test_render_screen_does_not_panic() { with_frame!(80, 24, |frame| { let app = LoreApp::new(); render_screen(&mut frame, &app); }); } #[test] fn test_render_screen_tiny_terminal_noop() { with_frame!(2, 2, |frame| { let app = LoreApp::new(); render_screen(&mut frame, &app); // Should not panic — early return for tiny terminals. }); } #[test] fn test_render_screen_with_error_toast() { with_frame!(80, 24, |frame| { let mut app = LoreApp::new(); app.state.set_error("test error".into()); render_screen(&mut frame, &app); // Should render without panicking. }); } #[test] fn test_render_screen_with_help_overlay() { with_frame!(80, 24, |frame| { let mut app = LoreApp::new(); app.state.show_help = true; render_screen(&mut frame, &app); // Should render without panicking. }); } #[test] fn test_render_screen_narrow_terminal() { with_frame!(20, 5, |frame| { let app = LoreApp::new(); render_screen(&mut frame, &app); }); } #[test] fn test_render_screen_sync_has_content() { with_frame!(80, 24, |frame| { let mut app = LoreApp::new(); app.navigation.push(Screen::Sync); render_screen(&mut frame, &app); let has_content = (20..60u16).any(|x| { (8..16u16).any(|y| frame.buffer.get(x, y).is_some_and(|cell| !cell.is_empty())) }); assert!(has_content, "Expected sync idle content in center area"); }); } }