From 45a989637c2b8b39792ec8c4544def0b45b5780c Mon Sep 17 00:00:00 2001 From: teernisse Date: Wed, 18 Feb 2026 23:58:30 -0500 Subject: [PATCH] feat(tui): add per-screen responsive layout helpers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce breakpoint-aware helper functions in layout.rs that centralize per-screen responsive decisions. Each function maps a Breakpoint to a screen-specific value, replacing scattered hardcoded checks across view modules: - detail_side_panel: show discussions side panel at Lg+ - info_screen_columns: 1 column on Xs/Sm, 2 on Md+ - search_show_project: hide project path column on narrow terminals - timeline_time_width: compact time on Xs (5), full on Md+ (12) - who_abbreviated_tabs: shorten tab labels on Xs/Sm - sync_progress_bar_width: scale progress bar 15→50 with width All functions are const fn with exhaustive match arms. Includes 6 unit tests covering every breakpoint variant. --- crates/lore-tui/src/layout.rs | 134 ++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) diff --git a/crates/lore-tui/src/layout.rs b/crates/lore-tui/src/layout.rs index a601400..2fcdff0 100644 --- a/crates/lore-tui/src/layout.rs +++ b/crates/lore-tui/src/layout.rs @@ -44,6 +44,84 @@ pub const fn show_preview_pane(bp: Breakpoint) -> bool { } } +// --------------------------------------------------------------------------- +// 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 // --------------------------------------------------------------------------- @@ -99,4 +177,60 @@ mod tests { 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); + } }