#![allow(clippy::module_name_repetitions)] //! Responsive layout helpers for the Lore TUI. //! //! Wraps [`ftui::layout::Breakpoint`] and [`ftui::layout::Breakpoints`] with //! Lore-specific configuration: breakpoint thresholds, column counts per //! breakpoint, and preview-pane visibility rules. use ftui::layout::{Breakpoint, Breakpoints}; /// Lore-specific breakpoint thresholds. /// /// Uses the ftui defaults: Sm=60, Md=90, Lg=120, Xl=160 columns. pub const LORE_BREAKPOINTS: Breakpoints = Breakpoints::DEFAULT; /// Classify a terminal width into a [`Breakpoint`]. #[inline] pub fn classify_width(width: u16) -> Breakpoint { LORE_BREAKPOINTS.classify_width(width) } /// Number of dashboard columns for a given breakpoint. /// /// - `Xs` / `Sm`: 1 column (narrow terminals) /// - `Md`: 2 columns (standard width) /// - `Lg` / `Xl`: 3 columns (wide terminals) #[inline] pub const fn dashboard_columns(bp: Breakpoint) -> u16 { match bp { Breakpoint::Xs | Breakpoint::Sm => 1, Breakpoint::Md => 2, Breakpoint::Lg | Breakpoint::Xl => 3, } } /// Whether the preview pane should be visible at a given breakpoint. /// /// Preview requires at least `Md` width to avoid cramping the main list. #[inline] pub const fn show_preview_pane(bp: Breakpoint) -> bool { match bp { Breakpoint::Md | Breakpoint::Lg | Breakpoint::Xl => true, Breakpoint::Xs | Breakpoint::Sm => false, } } // --------------------------------------------------------------------------- // Per-screen responsive helpers // --------------------------------------------------------------------------- /// Whether detail views (issue/MR) should show a side panel for discussions. /// /// At `Lg`+ widths, enough room exists for a 60/40 or 50/50 split with /// description on the left and discussions/cross-refs on the right. #[inline] pub const fn detail_side_panel(bp: Breakpoint) -> bool { match bp { Breakpoint::Lg | Breakpoint::Xl => true, Breakpoint::Xs | Breakpoint::Sm | Breakpoint::Md => false, } } /// Number of stat columns for the Stats/Doctor screens. /// /// - `Xs` / `Sm`: 1 column (full-width stacked) /// - `Md`+: 2 columns (side-by-side sections) #[inline] pub const fn info_screen_columns(bp: Breakpoint) -> u16 { match bp { Breakpoint::Xs | Breakpoint::Sm => 1, Breakpoint::Md | Breakpoint::Lg | Breakpoint::Xl => 2, } } /// Whether to show the project path column in search results. /// /// On narrow terminals, the project path is dropped to give the title /// more room. #[inline] pub const fn search_show_project(bp: Breakpoint) -> bool { match bp { Breakpoint::Xs | Breakpoint::Sm => false, Breakpoint::Md | Breakpoint::Lg | Breakpoint::Xl => true, } } /// Width allocated for the relative-time column in timeline events. /// /// Narrow terminals get a compact time (e.g., "2h"), wider terminals /// get the full relative time (e.g., "2 hours ago"). #[inline] pub const fn timeline_time_width(bp: Breakpoint) -> u16 { match bp { Breakpoint::Xs => 5, Breakpoint::Sm => 8, Breakpoint::Md | Breakpoint::Lg | Breakpoint::Xl => 12, } } /// Whether to use abbreviated mode-tab labels in the Who screen. /// /// On narrow terminals, tabs are shortened to 3-char abbreviations /// (e.g., "Exp" instead of "Expert") to fit all 5 modes. #[inline] pub const fn who_abbreviated_tabs(bp: Breakpoint) -> bool { match bp { Breakpoint::Xs | Breakpoint::Sm => true, Breakpoint::Md | Breakpoint::Lg | Breakpoint::Xl => false, } } /// Width of the progress bar in the Sync screen. /// /// Scales with terminal width to use available space effectively. #[inline] pub const fn sync_progress_bar_width(bp: Breakpoint) -> u16 { match bp { Breakpoint::Xs => 15, Breakpoint::Sm => 25, Breakpoint::Md => 35, Breakpoint::Lg | Breakpoint::Xl => 50, } } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- #[cfg(test)] mod tests { use super::*; #[test] fn test_classify_width_boundaries() { // Xs: 0..59 assert_eq!(classify_width(59), Breakpoint::Xs); // Sm: 60..89 assert_eq!(classify_width(60), Breakpoint::Sm); assert_eq!(classify_width(89), Breakpoint::Sm); // Md: 90..119 assert_eq!(classify_width(90), Breakpoint::Md); assert_eq!(classify_width(119), Breakpoint::Md); // Lg: 120..159 assert_eq!(classify_width(120), Breakpoint::Lg); assert_eq!(classify_width(159), Breakpoint::Lg); // Xl: 160+ assert_eq!(classify_width(160), Breakpoint::Xl); } #[test] fn test_dashboard_columns_per_breakpoint() { assert_eq!(dashboard_columns(Breakpoint::Xs), 1); assert_eq!(dashboard_columns(Breakpoint::Sm), 1); assert_eq!(dashboard_columns(Breakpoint::Md), 2); assert_eq!(dashboard_columns(Breakpoint::Lg), 3); assert_eq!(dashboard_columns(Breakpoint::Xl), 3); } #[test] fn test_show_preview_pane_per_breakpoint() { assert!(!show_preview_pane(Breakpoint::Xs)); assert!(!show_preview_pane(Breakpoint::Sm)); assert!(show_preview_pane(Breakpoint::Md)); assert!(show_preview_pane(Breakpoint::Lg)); assert!(show_preview_pane(Breakpoint::Xl)); } #[test] fn test_edge_cases() { // Width 0 must not panic, should classify as Xs assert_eq!(classify_width(0), Breakpoint::Xs); // Very wide terminal assert_eq!(classify_width(300), Breakpoint::Xl); } #[test] fn test_lore_breakpoints_matches_defaults() { assert_eq!(LORE_BREAKPOINTS, Breakpoints::DEFAULT); } // -- Per-screen responsive helpers ---------------------------------------- #[test] fn test_detail_side_panel() { assert!(!detail_side_panel(Breakpoint::Xs)); assert!(!detail_side_panel(Breakpoint::Sm)); assert!(!detail_side_panel(Breakpoint::Md)); assert!(detail_side_panel(Breakpoint::Lg)); assert!(detail_side_panel(Breakpoint::Xl)); } #[test] fn test_info_screen_columns() { assert_eq!(info_screen_columns(Breakpoint::Xs), 1); assert_eq!(info_screen_columns(Breakpoint::Sm), 1); assert_eq!(info_screen_columns(Breakpoint::Md), 2); assert_eq!(info_screen_columns(Breakpoint::Lg), 2); assert_eq!(info_screen_columns(Breakpoint::Xl), 2); } #[test] fn test_search_show_project() { assert!(!search_show_project(Breakpoint::Xs)); assert!(!search_show_project(Breakpoint::Sm)); assert!(search_show_project(Breakpoint::Md)); assert!(search_show_project(Breakpoint::Lg)); assert!(search_show_project(Breakpoint::Xl)); } #[test] fn test_timeline_time_width() { assert_eq!(timeline_time_width(Breakpoint::Xs), 5); assert_eq!(timeline_time_width(Breakpoint::Sm), 8); assert_eq!(timeline_time_width(Breakpoint::Md), 12); assert_eq!(timeline_time_width(Breakpoint::Lg), 12); assert_eq!(timeline_time_width(Breakpoint::Xl), 12); } #[test] fn test_who_abbreviated_tabs() { assert!(who_abbreviated_tabs(Breakpoint::Xs)); assert!(who_abbreviated_tabs(Breakpoint::Sm)); assert!(!who_abbreviated_tabs(Breakpoint::Md)); assert!(!who_abbreviated_tabs(Breakpoint::Lg)); assert!(!who_abbreviated_tabs(Breakpoint::Xl)); } #[test] fn test_sync_progress_bar_width() { assert_eq!(sync_progress_bar_width(Breakpoint::Xs), 15); assert_eq!(sync_progress_bar_width(Breakpoint::Sm), 25); assert_eq!(sync_progress_bar_width(Breakpoint::Md), 35); assert_eq!(sync_progress_bar_width(Breakpoint::Lg), 50); assert_eq!(sync_progress_bar_width(Breakpoint::Xl), 50); } }