Feat: fixing ratatui big-text and UX

This commit is contained in:
2026-02-16 15:43:22 +01:00
parent af6732db1c
commit c749ed6f85
11 changed files with 526 additions and 151 deletions

View File

@@ -4,7 +4,6 @@ use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line as RLine, Span};
use ratatui::widgets::{Block, Borders, Padding, Paragraph, Wrap};
use ratatui::Frame;
#[cfg(not(feature = "desktop"))]
use tui_big_text::{BigText, PixelSize};
use crate::app::App;
@@ -129,10 +128,7 @@ fn render_topics(frame: &mut Frame, app: &App, area: Rect) {
}
const WELCOME_TOPIC: usize = 0;
#[cfg(not(feature = "desktop"))]
const BIG_TITLE_HEIGHT: u16 = 6;
#[cfg(feature = "desktop")]
const BIG_TITLE_HEIGHT: u16 = 3;
fn render_content(frame: &mut Frame, app: &App, area: Rect) {
let theme = theme::get();
@@ -146,19 +142,12 @@ fn render_content(frame: &mut Frame, app: &App, area: Rect) {
Layout::vertical([Constraint::Length(BIG_TITLE_HEIGHT), Constraint::Fill(1)])
.areas(area);
#[cfg(not(feature = "desktop"))]
let big_title = BigText::builder()
.pixel_size(PixelSize::Quadrant)
.style(Style::new().fg(theme.markdown.h1).bold())
.lines(vec!["CAGIRE".into()])
.centered()
.build();
#[cfg(feature = "desktop")]
let big_title = Paragraph::new(RLine::from(Span::styled(
"CAGIRE",
Style::new().fg(theme.markdown.h1).bold(),
)))
.alignment(ratatui::layout::Alignment::Center);
let subtitle = Paragraph::new(RLine::from(Span::styled(
"A Forth Sequencer",
@@ -166,12 +155,8 @@ fn render_content(frame: &mut Frame, app: &App, area: Rect) {
)))
.alignment(ratatui::layout::Alignment::Center);
#[cfg(not(feature = "desktop"))]
let [big_area, subtitle_area] =
Layout::vertical([Constraint::Length(4), Constraint::Length(2)]).areas(title_area);
#[cfg(feature = "desktop")]
let [big_area, subtitle_area] =
Layout::vertical([Constraint::Length(1), Constraint::Length(2)]).areas(title_area);
frame.render_widget(big_title, big_area);
frame.render_widget(subtitle, subtitle_area);

View File

@@ -601,20 +601,38 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
inner
}
Modal::Onboarding => {
let (desc, keys) = app.page.onboarding();
let text_width = 51usize; // inner width minus 2 for padding
Modal::Onboarding { page } => {
let pages = app.page.onboarding();
let page_idx = (*page).min(pages.len().saturating_sub(1));
let (desc, keys) = pages[page_idx];
let page_count = pages.len();
let text_width = 51usize;
let desc_lines = {
let mut lines = 0u16;
for line in desc.split('\n') {
lines += (line.len() as u16).max(1).div_ceil(text_width as u16);
let mut col = 0usize;
for word in line.split_whitespace() {
let wlen = word.len();
if col > 0 && col + 1 + wlen > text_width {
lines += 1;
col = wlen;
} else {
col += if col > 0 { 1 + wlen } else { wlen };
}
}
lines += 1;
}
lines
};
let key_lines = keys.len() as u16;
let modal_height = (3 + desc_lines + 1 + key_lines + 2).min(term.height.saturating_sub(4)); // border + pad + desc + gap + keys + pad + hint
let modal_height = (3 + desc_lines + 1 + key_lines + 2).min(term.height.saturating_sub(4));
let inner = ModalFrame::new(&format!(" {} ", app.page.name()))
let title = if page_count > 1 {
format!(" {} ({}/{}) ", app.page.name(), page_idx + 1, page_count)
} else {
format!(" {} ", app.page.name())
};
let inner = ModalFrame::new(&title)
.width(57)
.height(modal_height)
.border_color(theme.modal.confirm)
@@ -650,7 +668,15 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
}
let hint_area = Rect::new(inner.x, inner.y + inner.height - 1, inner.width, 1);
let hints = hint_line(&[("Enter", "don't show again"), ("any key", "dismiss")]);
let mut hints_vec: Vec<(&str, &str)> = Vec::new();
if page_count > 1 {
hints_vec.push(("\u{2190}\u{2192}", "page"));
}
if app.page.help_topic_index().is_some() {
hints_vec.push(("?", "help"));
}
hints_vec.push(("Enter", "don't show again"));
let hints = hint_line(&hints_vec);
frame.render_widget(Paragraph::new(hints).alignment(Alignment::Center), hint_area);
inner

View File

@@ -1,9 +1,8 @@
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
use ratatui::style::Style;
use ratatui::text::{Line, Span};
use ratatui::widgets::Paragraph;
use ratatui::widgets::{Cell, Paragraph, Row, Table};
use ratatui::Frame;
#[cfg(not(feature = "desktop"))]
use tui_big_text::{BigText, PixelSize};
use crate::state::ui::UiState;
@@ -17,7 +16,6 @@ pub fn render(frame: &mut Frame, area: Rect, ui: &UiState) {
let link_style = Style::new().fg(theme.title.link);
let license_style = Style::new().fg(theme.title.license);
#[cfg(not(feature = "desktop"))]
let big_title = BigText::builder()
.pixel_size(PixelSize::Full)
.style(Style::new().fg(theme.title.big_title).bold())
@@ -25,99 +23,111 @@ pub fn render(frame: &mut Frame, area: Rect, ui: &UiState) {
.centered()
.build();
#[cfg(feature = "desktop")]
let big_title = Paragraph::new(Line::from(Span::styled(
"CAGIRE",
Style::new().fg(theme.title.big_title).bold(),
)))
.alignment(Alignment::Center);
let version_style = Style::new().fg(theme.title.subtitle);
let subtitle_lines = vec![
let info_lines = vec![
Line::from(""),
Line::from(Span::styled(
"A Forth Music Sequencer",
Style::new().fg(theme.title.subtitle),
)),
Line::from(vec![
Span::styled("A Forth Music Sequencer by ", Style::new().fg(theme.title.subtitle)),
Span::styled("BuboBubo", author_style),
]),
Line::from(Span::styled(
format!("v{}", env!("CARGO_PKG_VERSION")),
version_style,
Style::new().fg(theme.title.subtitle),
)),
Line::from(""),
Line::from(Span::styled("by BuboBubo", author_style)),
Line::from(""),
Line::from(Span::styled("https://raphaelforment.fr", link_style)),
Line::from(""),
Line::from(Span::styled("AGPL-3.0", license_style)),
Line::from(""),
Line::from(""),
Line::from(vec![
Span::styled("Ctrl+Arrows", Style::new().fg(theme.title.link)),
Span::styled(": navigate views", Style::new().fg(theme.title.prompt)),
]),
Line::from(vec![
Span::styled("Enter", Style::new().fg(theme.title.link)),
Span::styled(": edit step ", Style::new().fg(theme.title.prompt)),
Span::styled("Space", Style::new().fg(theme.title.link)),
Span::styled(": play/stop", Style::new().fg(theme.title.prompt)),
]),
Line::from(vec![
Span::styled("s", Style::new().fg(theme.title.link)),
Span::styled(": save ", Style::new().fg(theme.title.prompt)),
Span::styled("l", Style::new().fg(theme.title.link)),
Span::styled(": load ", Style::new().fg(theme.title.prompt)),
Span::styled("q", Style::new().fg(theme.title.link)),
Span::styled(": quit", Style::new().fg(theme.title.prompt)),
]),
Line::from(vec![
Span::styled("?", Style::new().fg(theme.title.link)),
Span::styled(": keybindings", Style::new().fg(theme.title.prompt)),
]),
Line::from(""),
Line::from(Span::styled(
"Press any key to continue",
Style::new().fg(theme.title.subtitle),
)),
];
#[cfg(not(feature = "desktop"))]
let big_text_height = 8;
#[cfg(feature = "desktop")]
let big_text_height = 1;
let min_title_width = 30;
let subtitle_height = subtitle_lines.len() as u16;
let keybindings = [
("Ctrl+Arrows", "Navigate Views"),
("Enter", "Edit Step"),
("Space", "Play/Stop"),
("s", "Save"),
("l", "Load"),
("q", "Quit"),
("?", "Keybindings"),
];
let key_style = Style::new().fg(theme.modal.confirm);
let desc_style = Style::new().fg(theme.ui.text_primary);
let rows: Vec<Row> = keybindings
.iter()
.enumerate()
.map(|(i, (key, desc))| {
let bg = if i % 2 == 0 {
theme.table.row_even
} else {
theme.table.row_odd
};
Row::new(vec![
Cell::from(*key).style(key_style),
Cell::from(*desc).style(desc_style),
])
.style(Style::new().bg(bg))
})
.collect();
let table = Table::new(
rows,
[Constraint::Length(14), Constraint::Fill(1)],
)
.column_spacing(2);
let press_line = Line::from(Span::styled(
"Press any key to continue",
Style::new().fg(theme.title.subtitle),
));
let info_height = info_lines.len() as u16;
let table_height = keybindings.len() as u16;
let table_width: u16 = 42;
let big_text_height: u16 = 8;
let content_height = info_height + table_height + 3; // +3 for gap + empty line + press line
let show_big_title =
area.height >= (big_text_height + subtitle_height) && area.width >= min_title_width;
area.height >= (big_text_height + content_height) && area.width >= 30;
let total_height = if show_big_title {
big_text_height + content_height
} else {
content_height
};
let v_pad = area.height.saturating_sub(total_height) / 2;
let mut constraints = Vec::new();
constraints.push(Constraint::Length(v_pad));
if show_big_title {
constraints.push(Constraint::Length(big_text_height));
}
constraints.push(Constraint::Length(info_height));
constraints.push(Constraint::Length(1)); // gap
constraints.push(Constraint::Length(table_height));
constraints.push(Constraint::Length(1)); // empty line
constraints.push(Constraint::Length(1)); // press any key
constraints.push(Constraint::Fill(1));
let areas = Layout::vertical(constraints).split(area);
let mut idx = 1; // skip padding
if show_big_title {
let total_height = big_text_height + subtitle_height;
let vertical_padding = area.height.saturating_sub(total_height) / 2;
let [_, title_area, subtitle_area, _] = Layout::vertical([
Constraint::Length(vertical_padding),
Constraint::Length(big_text_height),
Constraint::Length(subtitle_height),
Constraint::Fill(1),
])
.areas(area);
frame.render_widget(big_title, title_area);
let subtitle = Paragraph::new(subtitle_lines).alignment(Alignment::Center);
frame.render_widget(subtitle, subtitle_area);
} else {
let vertical_padding = area.height.saturating_sub(subtitle_height) / 2;
let [_, subtitle_area, _] = Layout::vertical([
Constraint::Length(vertical_padding),
Constraint::Length(subtitle_height),
Constraint::Fill(1),
])
.areas(area);
let subtitle = Paragraph::new(subtitle_lines).alignment(Alignment::Center);
frame.render_widget(subtitle, subtitle_area);
frame.render_widget(big_title, areas[idx]);
idx += 1;
}
let info = Paragraph::new(info_lines).alignment(Alignment::Center);
frame.render_widget(info, areas[idx]);
idx += 1;
idx += 1; // skip gap
let tw = table_width.min(areas[idx].width);
let tx = areas[idx].x + (areas[idx].width.saturating_sub(tw)) / 2;
let table_area = Rect::new(tx, areas[idx].y, tw, areas[idx].height);
frame.render_widget(table, table_area);
idx += 2; // skip empty line
let press = Paragraph::new(press_line).alignment(Alignment::Center);
frame.render_widget(press, areas[idx]);
}