use ratatui::{
Frame,
layout::{Constraint, Direction, Layout},
prelude::Rect,
style::Style,
};
use crate::i18n;
use crate::state::types::AppMode;
use crate::state::{AppState, Focus};
use crate::theme::theme;
mod install;
mod installed_only;
mod recent;
mod search;
pub fn render_middle(f: &mut Frame, app: &mut AppState, area: Rect) {
if matches!(app.app_mode, AppMode::News) {
render_middle_news(f, app, area);
return;
}
let left_pct = if app.show_recent_pane {
app.layout_left_pct.min(100)
} else {
0
};
let mut right_pct = if app.show_install_pane {
app.layout_right_pct.min(100)
} else {
0
};
if app.installed_only_mode && right_pct > 0 {
let max_right = 100u16.saturating_sub(left_pct);
let widened = ((u32::from(right_pct) * 3) / 2)
.min(u32::from(u16::MAX))
.try_into()
.unwrap_or(u16::MAX); right_pct = widened.min(max_right);
}
let center_pct = 100u16
.saturating_sub(left_pct)
.saturating_sub(right_pct)
.min(100);
let middle = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage(left_pct),
Constraint::Percentage(center_pct),
Constraint::Percentage(right_pct),
])
.split(area);
search::render_search(f, app, middle[1]);
recent::render_recent(f, app, middle[0]);
if app.show_install_pane && middle[2].width > 0 {
if app.installed_only_mode {
installed_only::render_installed_only(f, app, middle[2]);
} else {
install::render_install(f, app, middle[2]);
}
} else {
app.install_rect = None;
if matches!(app.focus, Focus::Install) {
app.focus = Focus::Search;
}
}
}
fn render_middle_news(f: &mut Frame, app: &mut AppState, area: Rect) {
let left_pct = if app.show_news_history_pane { 25 } else { 0 };
let right_pct = if app.show_news_bookmarks_pane { 25 } else { 0 };
let center_pct = 100u16
.saturating_sub(left_pct)
.saturating_sub(right_pct)
.min(100);
let middle = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage(left_pct),
Constraint::Percentage(center_pct),
Constraint::Percentage(right_pct),
])
.split(area);
search::render_search(f, app, middle[1]);
recent::render_news_recent(f, app, middle[0]);
if app.show_news_bookmarks_pane && middle[2].width > 0 {
let th = theme();
let bookmarks_focused = matches!(app.focus, Focus::Install);
let items: Vec<ratatui::widgets::ListItem> = app
.news_bookmarks
.iter()
.map(|b| {
ratatui::widgets::ListItem::new(ratatui::text::Span::styled(
b.item.title.clone(),
Style::default().fg(if bookmarks_focused {
th.text
} else {
th.subtext0
}),
))
})
.collect();
if app.install_state.selected().is_none() && !app.news_bookmarks.is_empty() {
app.install_state.select(Some(0));
}
let title = if bookmarks_focused {
i18n::t(app, "app.titles.news_bookmarks_focused")
} else {
i18n::t(app, "app.titles.news_bookmarks")
};
let list = ratatui::widgets::List::new(items)
.block(
ratatui::widgets::Block::default()
.borders(ratatui::widgets::Borders::ALL)
.border_type(ratatui::widgets::BorderType::Rounded)
.title(title)
.border_style(Style::default().fg(if bookmarks_focused {
th.mauve
} else {
th.surface1
})),
)
.style(
Style::default()
.fg(if bookmarks_focused {
th.text
} else {
th.subtext0
})
.bg(th.base),
)
.highlight_style(Style::default().fg(th.text).bg(th.surface2))
.highlight_symbol("â–¶ ");
f.render_stateful_widget(list, middle[2], &mut app.install_state);
app.install_rect = Some((middle[2].x, middle[2].y, middle[2].width, middle[2].height));
} else {
app.install_rect = None;
if matches!(app.focus, Focus::Install) {
app.focus = Focus::Search;
}
}
}
#[cfg(test)]
mod tests {
#[test]
fn middle_sets_rects_and_cursor_positions() {
use ratatui::{Terminal, backend::TestBackend};
let backend = TestBackend::new(120, 30);
let mut term = Terminal::new(backend).expect("Failed to create terminal for test");
let mut app = crate::state::AppState {
show_recent_pane: true,
show_install_pane: true,
focus: crate::state::Focus::Search,
input: "hello".into(),
..Default::default()
};
term.draw(|f| {
let area = f.area();
super::render_middle(f, &mut app, area);
})
.expect("Failed to render middle pane");
assert!(app.recent_rect.is_some());
assert!(app.install_rect.is_some());
app.focus = crate::state::Focus::Install;
app.show_install_pane = false;
term.draw(|f| {
let area = f.area();
super::render_middle(f, &mut app, area);
})
.expect("Failed to render middle pane");
assert!(matches!(app.focus, crate::state::Focus::Search));
}
#[test]
fn middle_enlarges_right_pane_in_installed_only_mode() {
use ratatui::{Terminal, backend::TestBackend};
let backend = TestBackend::new(120, 30);
let mut term = Terminal::new(backend).expect("Failed to create terminal for test");
let mut app = crate::state::AppState {
show_recent_pane: true,
show_install_pane: true,
installed_only_mode: true,
layout_left_pct: 20,
layout_right_pct: 30, ..Default::default()
};
term.draw(|f| {
let area = f.area();
super::render_middle(f, &mut app, area);
})
.expect("Failed to render middle pane");
assert!(app.install_rect.is_some());
assert!(app.downgrade_rect.is_some());
}
#[test]
fn middle_reassigns_space_when_panes_hidden() {
use ratatui::{Terminal, backend::TestBackend};
let backend = TestBackend::new(120, 30);
let mut term = Terminal::new(backend).expect("Failed to create terminal for test");
let mut app = crate::state::AppState {
show_recent_pane: false,
show_install_pane: false,
focus: crate::state::Focus::Search,
input: "test".into(),
..Default::default()
};
term.draw(|f| {
let area = f.area();
super::render_middle(f, &mut app, area);
})
.expect("Failed to render middle pane");
assert!(app.recent_rect.is_none());
assert!(app.install_rect.is_none());
}
#[test]
fn middle_handles_zero_width_area() {
use ratatui::{Terminal, backend::TestBackend};
let backend = TestBackend::new(0, 30);
let mut term = Terminal::new(backend).expect("Failed to create terminal for test");
let mut app = crate::state::AppState {
show_recent_pane: true,
show_install_pane: true,
..Default::default()
};
term.draw(|f| {
let area = f.area();
super::render_middle(f, &mut app, area);
})
.expect("Failed to render middle pane");
}
#[test]
fn middle_switches_focus_when_install_hidden() {
use ratatui::{Terminal, backend::TestBackend};
let backend = TestBackend::new(120, 30);
let mut term = Terminal::new(backend).expect("Failed to create terminal for test");
let mut app = crate::state::AppState {
show_install_pane: true,
focus: crate::state::Focus::Install,
..Default::default()
};
app.show_install_pane = false;
term.draw(|f| {
let area = f.area();
super::render_middle(f, &mut app, area);
})
.expect("Failed to render middle pane");
assert!(matches!(app.focus, crate::state::Focus::Search));
}
}