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
let theme = app.theme().clone();
let background_block = Block::default().style(Style::default().bg(theme.background));
let full_area = frame.area();
frame.render_widget(background_block, full_area);
let frame_area = frame.area();
frame.render_widget(background_block, frame_area);
let (file_area, main_area) = if app.is_file_panel_collapsed() || full_area.width < 40 {
(None, full_area)
let title_line = Line::from(vec![Span::styled(
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 {
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 segments = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Length(sidebar_width), Constraint::Min(30)])
.split(full_area);
.split(content_area);
(Some(segments[0]), segments[1])
};
@@ -253,10 +272,7 @@ pub fn render_chat(frame: &mut Frame<'_>, app: &mut ChatApp) {
0
};
let mut constraints = vec![
Constraint::Length(3), // Header
Constraint::Min(8), // Messages
];
let mut constraints = vec![Constraint::Min(8)]; // Messages
if thinking_height > 0 {
constraints.push(Constraint::Length(thinking_height)); // Thinking
@@ -276,9 +292,6 @@ pub fn render_chat(frame: &mut Frame<'_>, app: &mut ChatApp) {
.split(chat_area);
let mut idx = 0;
render_header(frame, layout[idx], app);
idx += 1;
render_messages(frame, layout[idx], app);
idx += 1;
@@ -318,13 +331,16 @@ pub fn render_chat(frame: &mut Frame<'_>, app: &mut ChatApp) {
}
if app.is_model_info_visible() {
let panel_width = full_area
let panel_width = content_area
.width
.saturating_div(3)
.max(30)
.min(full_area.width.saturating_sub(20).max(30));
let x = full_area.x + full_area.width.saturating_sub(panel_width);
let area = Rect::new(x, full_area.y, panel_width, full_area.height);
.min(content_area.width.saturating_sub(20).max(30))
.min(content_area.width);
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);
let viewport_height = area.height.saturating_sub(2) as usize;
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_toasts(frame, app, full_area);
render_toasts(frame, app, content_area);
}
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
}
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>(
lines: Vec<Line<'a>>,
selection: Option<((usize, usize), (usize, usize))>,