feat(tui): responsive breakpoints for detail views (bd-a6yb)
Apply breakpoint-aware layout to issue_detail and mr_detail views: - Issue detail: hide labels on Xs, hide assignees on Xs/Sm, skip milestone row on Xs - MR detail: hide branch names and merge status on Xs/Sm - Issue detail allocate_sections gives description 60% on wide (Lg+) vs 40% narrow - Add responsive tests for both detail views - Close bd-a6yb: all TUI screens now adapt to terminal width 760 lib tests pass, clippy clean.
This commit is contained in:
@@ -17,22 +17,21 @@
|
||||
//! +-----------------------------------+
|
||||
//! ```
|
||||
|
||||
use ftui::layout::Breakpoint;
|
||||
use ftui::render::cell::{Cell, PackedRgba};
|
||||
use ftui::render::drawing::Draw;
|
||||
use ftui::render::frame::Frame;
|
||||
|
||||
use super::common::truncate_str;
|
||||
use super::{ACCENT, BG_SURFACE, TEXT, TEXT_MUTED};
|
||||
use crate::layout::classify_width;
|
||||
use crate::state::file_history::{FileHistoryResult, FileHistoryState};
|
||||
use crate::text_width::cursor_cell_offset;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Colors (Flexoki palette)
|
||||
// Colors (Flexoki palette — screen-specific)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
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 GREEN: PackedRgba = PackedRgba::rgb(0x87, 0x9A, 0x39); // green
|
||||
const CYAN: PackedRgba = PackedRgba::rgb(0x3A, 0xA9, 0x9F); // cyan
|
||||
const YELLOW: PackedRgba = PackedRgba::rgb(0xD0, 0xA2, 0x15); // yellow
|
||||
@@ -53,6 +52,7 @@ pub fn render_file_history(
|
||||
return; // Terminal too small.
|
||||
}
|
||||
|
||||
let bp = classify_width(area.width);
|
||||
let x = area.x;
|
||||
let max_x = area.right();
|
||||
let width = area.width;
|
||||
@@ -105,7 +105,7 @@ pub fn render_file_history(
|
||||
}
|
||||
|
||||
// --- MR list ---
|
||||
render_mr_list(frame, result, state, x, y, width, list_height);
|
||||
render_mr_list(frame, result, state, x, y, width, list_height, bp);
|
||||
|
||||
// --- Hint bar ---
|
||||
render_hint_bar(frame, x, hint_y, max_x);
|
||||
@@ -248,6 +248,33 @@ fn render_summary(frame: &mut Frame<'_>, result: &FileHistoryResult, x: u16, y:
|
||||
frame.print_text_clipped(x + 1, y, &summary, style, max_x);
|
||||
}
|
||||
|
||||
/// Responsive truncation widths for file history MR rows.
|
||||
const fn fh_title_max(bp: Breakpoint) -> usize {
|
||||
match bp {
|
||||
Breakpoint::Xs => 15,
|
||||
Breakpoint::Sm => 25,
|
||||
Breakpoint::Md => 35,
|
||||
Breakpoint::Lg | Breakpoint::Xl => 55,
|
||||
}
|
||||
}
|
||||
|
||||
const fn fh_author_max(bp: Breakpoint) -> usize {
|
||||
match bp {
|
||||
Breakpoint::Xs | Breakpoint::Sm => 8,
|
||||
Breakpoint::Md | Breakpoint::Lg | Breakpoint::Xl => 12,
|
||||
}
|
||||
}
|
||||
|
||||
const fn fh_disc_max(bp: Breakpoint) -> usize {
|
||||
match bp {
|
||||
Breakpoint::Xs => 25,
|
||||
Breakpoint::Sm => 40,
|
||||
Breakpoint::Md => 60,
|
||||
Breakpoint::Lg | Breakpoint::Xl => 80,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_mr_list(
|
||||
frame: &mut Frame<'_>,
|
||||
result: &FileHistoryResult,
|
||||
@@ -256,10 +283,14 @@ fn render_mr_list(
|
||||
start_y: u16,
|
||||
width: u16,
|
||||
height: usize,
|
||||
bp: Breakpoint,
|
||||
) {
|
||||
let max_x = x + width;
|
||||
let offset = state.scroll_offset as usize;
|
||||
|
||||
let title_max = fh_title_max(bp);
|
||||
let author_max = fh_author_max(bp);
|
||||
|
||||
for (i, mr) in result
|
||||
.merge_requests
|
||||
.iter()
|
||||
@@ -315,8 +346,8 @@ fn render_mr_list(
|
||||
};
|
||||
let after_iid = frame.print_text_clipped(after_icon, y, &iid_str, ref_style, max_x);
|
||||
|
||||
// Title (truncated).
|
||||
let title = truncate_str(&mr.title, 35);
|
||||
// Title (responsive truncation).
|
||||
let title = truncate_str(&mr.title, title_max);
|
||||
let title_style = Cell {
|
||||
fg: TEXT,
|
||||
bg: sel_bg,
|
||||
@@ -324,10 +355,10 @@ fn render_mr_list(
|
||||
};
|
||||
let after_title = frame.print_text_clipped(after_iid + 1, y, &title, title_style, max_x);
|
||||
|
||||
// @author + change_type
|
||||
// @author + change_type (responsive author width).
|
||||
let meta = format!(
|
||||
"@{} {}",
|
||||
truncate_str(&mr.author_username, 12),
|
||||
truncate_str(&mr.author_username, author_max),
|
||||
mr.change_type
|
||||
);
|
||||
let meta_style = Cell {
|
||||
@@ -339,13 +370,11 @@ fn render_mr_list(
|
||||
}
|
||||
|
||||
// Inline discussion snippets (rendered beneath MRs when toggled on).
|
||||
// For simplicity, discussions are shown as a separate block after the MR list
|
||||
// in this initial implementation. Full inline rendering (grouped by MR) is
|
||||
// a follow-up enhancement.
|
||||
if state.show_discussions && !result.discussions.is_empty() {
|
||||
let disc_start_y = start_y + result.merge_requests.len().min(height) as u16;
|
||||
let remaining = height.saturating_sub(result.merge_requests.len().min(height));
|
||||
render_discussions(frame, result, x, disc_start_y, max_x, remaining);
|
||||
let visible_mrs = result.merge_requests.len().saturating_sub(offset).min(height);
|
||||
let disc_start_y = start_y + visible_mrs as u16;
|
||||
let remaining = height.saturating_sub(visible_mrs);
|
||||
render_discussions(frame, result, x, disc_start_y, max_x, remaining, bp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -356,11 +385,14 @@ fn render_discussions(
|
||||
start_y: u16,
|
||||
max_x: u16,
|
||||
max_rows: usize,
|
||||
bp: Breakpoint,
|
||||
) {
|
||||
if max_rows == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let disc_max = fh_disc_max(bp);
|
||||
|
||||
let sep_style = Cell {
|
||||
fg: TEXT_MUTED,
|
||||
..Cell::default()
|
||||
@@ -390,7 +422,7 @@ fn render_discussions(
|
||||
author_style,
|
||||
max_x,
|
||||
);
|
||||
let snippet = truncate_str(&disc.body_snippet, 60);
|
||||
let snippet = truncate_str(&disc.body_snippet, disc_max);
|
||||
frame.print_text_clipped(after_author, y, &snippet, disc_style, max_x);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user