feat(ui): embed header in main block and base layout on inner content area

- Render the app title with version as the block title instead of a separate header widget.
- Compute `content_area` via `main_block.inner` and use it for file panel, main area, model info panel, and toast rendering.
- Remove header constraints and the `render_header` function, simplifying the layout.
- Add early exit when `content_area` has zero width or height to avoid rendering errors.
This commit is contained in:
2025-10-13 19:06:55 +02:00
parent ab0ae4fe04
commit 80dffa9f41

View File

@@ -186,18 +186,37 @@ pub fn render_chat(frame: &mut Frame<'_>, app: &mut ChatApp) {
// Set terminal background color // Set terminal background color
let theme = app.theme().clone(); let theme = app.theme().clone();
let background_block = Block::default().style(Style::default().bg(theme.background)); let background_block = Block::default().style(Style::default().bg(theme.background));
let full_area = frame.area(); let frame_area = frame.area();
frame.render_widget(background_block, full_area); frame.render_widget(background_block, frame_area);
let (file_area, main_area) = if app.is_file_panel_collapsed() || full_area.width < 40 { let title_line = Line::from(vec![Span::styled(
(None, full_area) format!(" 🦉 OWLEN v{} AI Assistant ", APP_VERSION),
Style::default()
.fg(theme.focused_panel_border)
.add_modifier(Modifier::BOLD),
)]);
let main_block = Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(theme.unfocused_panel_border))
.style(Style::default().bg(theme.background).fg(theme.text))
.title(title_line);
let content_area = main_block.inner(frame_area);
frame.render_widget(main_block, frame_area);
if content_area.width == 0 || content_area.height == 0 {
return;
}
let (file_area, main_area) = if app.is_file_panel_collapsed() || content_area.width < 40 {
(None, content_area)
} else { } else {
let max_sidebar = full_area.width.saturating_sub(30).max(10); let max_sidebar = content_area.width.saturating_sub(30).max(10);
let sidebar_width = app.file_panel_width().min(max_sidebar).max(10); let sidebar_width = app.file_panel_width().min(max_sidebar).max(10);
let segments = Layout::default() let segments = Layout::default()
.direction(Direction::Horizontal) .direction(Direction::Horizontal)
.constraints([Constraint::Length(sidebar_width), Constraint::Min(30)]) .constraints([Constraint::Length(sidebar_width), Constraint::Min(30)])
.split(full_area); .split(content_area);
(Some(segments[0]), segments[1]) (Some(segments[0]), segments[1])
}; };
@@ -253,10 +272,7 @@ pub fn render_chat(frame: &mut Frame<'_>, app: &mut ChatApp) {
0 0
}; };
let mut constraints = vec![ let mut constraints = vec![Constraint::Min(8)]; // Messages
Constraint::Length(3), // Header
Constraint::Min(8), // Messages
];
if thinking_height > 0 { if thinking_height > 0 {
constraints.push(Constraint::Length(thinking_height)); // Thinking constraints.push(Constraint::Length(thinking_height)); // Thinking
@@ -276,9 +292,6 @@ pub fn render_chat(frame: &mut Frame<'_>, app: &mut ChatApp) {
.split(chat_area); .split(chat_area);
let mut idx = 0; let mut idx = 0;
render_header(frame, layout[idx], app);
idx += 1;
render_messages(frame, layout[idx], app); render_messages(frame, layout[idx], app);
idx += 1; idx += 1;
@@ -318,13 +331,16 @@ pub fn render_chat(frame: &mut Frame<'_>, app: &mut ChatApp) {
} }
if app.is_model_info_visible() { if app.is_model_info_visible() {
let panel_width = full_area let panel_width = content_area
.width .width
.saturating_div(3) .saturating_div(3)
.max(30) .max(30)
.min(full_area.width.saturating_sub(20).max(30)); .min(content_area.width.saturating_sub(20).max(30))
let x = full_area.x + full_area.width.saturating_sub(panel_width); .min(content_area.width);
let area = Rect::new(x, full_area.y, panel_width, full_area.height); let x = content_area
.x
.saturating_add(content_area.width.saturating_sub(panel_width));
let area = Rect::new(x, content_area.y, panel_width, content_area.height);
frame.render_widget(Clear, area); frame.render_widget(Clear, area);
let viewport_height = area.height.saturating_sub(2) as usize; let viewport_height = area.height.saturating_sub(2) as usize;
app.set_model_info_viewport_height(viewport_height); app.set_model_info_viewport_height(viewport_height);
@@ -335,7 +351,7 @@ pub fn render_chat(frame: &mut Frame<'_>, app: &mut ChatApp) {
render_code_workspace(frame, area, app); render_code_workspace(frame, area, app);
} }
render_toasts(frame, app, full_area); render_toasts(frame, app, content_area);
} }
fn toast_palette(level: ToastLevel, theme: &Theme) -> (&'static str, Style, Style) { fn toast_palette(level: ToastLevel, theme: &Theme) -> (&'static str, Style, Style) {
@@ -1109,24 +1125,6 @@ fn wrap_line_segments(line: &str, width: usize) -> Vec<String> {
result result
} }
fn render_header(frame: &mut Frame<'_>, area: Rect, app: &ChatApp) {
let theme = app.theme();
let title_span = Span::styled(
format!(" 🦉 OWLEN v{} AI Assistant ", APP_VERSION),
Style::default()
.fg(theme.focused_panel_border)
.add_modifier(Modifier::BOLD),
);
let header_block = Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(theme.unfocused_panel_border))
.style(Style::default().bg(theme.background).fg(theme.text))
.title(Line::from(vec![title_span]));
frame.render_widget(header_block, area);
}
fn apply_visual_selection<'a>( fn apply_visual_selection<'a>(
lines: Vec<Line<'a>>, lines: Vec<Line<'a>>,
selection: Option<((usize, usize), (usize, usize))>, selection: Option<((usize, usize), (usize, usize))>,