use ratatui::{
Frame,
layout::{Constraint, Flex, Layout, Rect},
style::{Modifier, Style},
text::{Line, Span, Text},
widgets::{Block, Borders, Clear, Paragraph, Wrap},
};
use crate::app::{App, MENTAL_MODEL_LABELS};
use crate::theme::Theme;
use crate::ui::styles;
pub fn render(frame: &mut Frame, area: Rect, app: &App, theme: &Theme) {
let desired_width: u16 = 76.min(area.width.saturating_sub(4));
let desired_height: u16 = 24.min(area.height.saturating_sub(4));
let popup_area = centered_rect(desired_width, desired_height, area);
frame.render_widget(Clear, popup_area);
let byte_limit = app.mental_model_byte_limit;
let title = format!(" Mental Model — {byte_limit} bytes/field ");
let block = Block::default()
.title(title)
.borders(Borders::ALL)
.style(styles::popup_style(theme))
.border_style(styles::border_style(theme, true));
let inner = block.inner(popup_area);
frame.render_widget(block, popup_area);
if inner.height < 8 {
return;
}
let rows = Layout::vertical(field_constraints(inner.height)).split(inner);
for (idx, row) in rows.iter().enumerate() {
render_field(frame, *row, idx, app, theme);
}
}
fn render_field(frame: &mut Frame, area: Rect, idx: usize, app: &App, theme: &Theme) {
let focused = app.mental_model_edit.focused == idx;
let label = MENTAL_MODEL_LABELS[idx];
let body = &app.mental_model_edit.drafts[idx];
let mut block = Block::default()
.title(format!(
" {} {}/{} ",
label,
body.len(),
app.mental_model_byte_limit
))
.borders(Borders::TOP | Borders::LEFT | Borders::RIGHT | Borders::BOTTOM);
if focused {
block = block.border_style(
Style::default()
.fg(theme.fg_primary)
.add_modifier(Modifier::BOLD),
);
} else {
block = block.border_style(Style::default().fg(theme.fg_secondary));
}
let inner = block.inner(area);
frame.render_widget(block, area);
let mut lines: Vec<Line<'static>> = body
.split('\n')
.map(|segment| {
Line::from(Span::styled(
segment.to_string(),
Style::default().fg(theme.fg_primary),
))
})
.collect();
if focused && let Some(last) = lines.last_mut() {
last.spans.push(Span::styled(
"\u{2588}".to_string(),
Style::default().fg(theme.fg_primary),
));
}
let paragraph = Paragraph::new(Text::from(lines)).wrap(Wrap { trim: false });
frame.render_widget(paragraph, inner);
}
fn field_constraints(inner_height: u16) -> [Constraint; 4] {
let field_height = inner_height / 4;
[
Constraint::Length(field_height),
Constraint::Length(field_height),
Constraint::Length(field_height),
Constraint::Fill(1),
]
}
fn centered_rect(width: u16, height: u16, area: Rect) -> Rect {
let horiz = Layout::horizontal([Constraint::Length(width)])
.flex(Flex::Center)
.split(area);
let vert = Layout::vertical([Constraint::Length(height)])
.flex(Flex::Center)
.split(horiz[0]);
vert[0]
}
#[cfg(test)]
mod tests {
use super::*;
fn split_body(inner: Rect) -> std::rc::Rc<[Rect]> {
Layout::vertical(field_constraints(inner.height)).split(inner)
}
#[test]
fn layout_divides_into_four_rows() {
let inner = Rect::new(0, 0, 74, 20); let rows = split_body(inner);
assert_eq!(rows.len(), 4);
for row in rows.iter() {
assert_eq!(row.height, 5, "each row gets a quarter of 20 rows");
}
}
#[test]
fn layout_last_row_absorbs_remainder_on_non_divisible_heights() {
let inner = Rect::new(0, 0, 74, 22);
let rows = split_body(inner);
assert_eq!(rows.len(), 4);
assert_eq!(rows[0].height, 5);
assert_eq!(rows[1].height, 5);
assert_eq!(rows[2].height, 5);
assert_eq!(
rows[3].height, 7,
"fourth row must absorb the 22 % 4 = 2 leftover rows"
);
let total: u16 = rows.iter().map(|r| r.height).sum();
assert_eq!(total, inner.height);
}
#[test]
fn layout_rows_cover_inner_without_gaps() {
for height in [8, 9, 10, 11, 12, 17, 21, 24] {
let inner = Rect::new(0, 0, 74, height);
let rows = split_body(inner);
let total: u16 = rows.iter().map(|r| r.height).sum();
assert_eq!(
total, height,
"rows must tile inner exactly at height={height}"
);
for pair in rows.windows(2) {
assert_eq!(
pair[0].y + pair[0].height,
pair[1].y,
"row gap detected at height={height}"
);
}
}
}
#[test]
fn centered_rect_sits_inside_area() {
let outer = Rect::new(0, 0, 100, 50);
let centered = centered_rect(40, 20, outer);
assert_eq!(centered.width, 40);
assert_eq!(centered.height, 20);
assert!(centered.x >= outer.x);
assert!(centered.y >= outer.y);
assert!(centered.x + centered.width <= outer.x + outer.width);
assert!(centered.y + centered.height <= outer.y + outer.height);
}
}