Added scroll()

This commit is contained in:
2026-06-10 01:42:33 -05:00
parent c2168b992d
commit f73c02437d
7 changed files with 375 additions and 43 deletions
+95 -2
View File
@@ -8,13 +8,13 @@
use crate::cp437::tile_to_char;
use color::Rgba8;
use kiln_core::Board;
use kiln_core::game::SpeechBubble;
use kiln_core::game::{Scroll, ScrollLine, SpeechBubble};
use kiln_core::log::LogLine;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::{Color, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::Widget;
use ratatui::widgets::{Block, Paragraph, Widget};
/// Converts a core [`Rgba8`] color into a ratatui truecolor [`Color`].
///
@@ -234,3 +234,96 @@ fn write_cell(buf: &mut Buffer, x: u16, y: u16, ch: char, style: Style) {
cell.set_char(ch).set_style(style);
}
}
/// Draws a scrollable text/choice overlay centered on `area`, used for `scroll()` calls.
///
/// The overlay dims the entire background, then renders a `Block`-bordered window
/// whose height animates from 0 → full (opening) or full → 0 (closing) based on
/// `anim` (0.0 = invisible, 1.0 = fully open). Text is pre-wrapped so the exact
/// line count is known for the animation; a `Paragraph` renders the content.
/// Choice lines show an `[a]` / `[b]` key prefix in yellow. `offset` scrolls the
/// content vertically.
pub fn draw_scroll_overlay(buf: &mut Buffer, area: Rect, scroll: &Scroll, offset: u16, anim: f32) {
if anim <= 0.0 { return; }
let bg = Color::Rgb(30, 25, 10);
let text_style = Style::default().fg(Color::White).bg(bg);
let key_style = Style::default().fg(Color::Yellow).bg(bg);
let choice_style = Style::default().fg(Color::Rgb(200, 200, 100)).bg(bg);
let border_style = Style::default().fg(Color::Rgb(180, 160, 100)).bg(bg);
// Pre-wrap text so we know the total line count for the animation height.
// max_text_w is the usable text width inside the block (border + 1 pad each side).
let max_text_w = (area.width as usize).saturating_sub(6).clamp(10, 60);
let mut lines: Vec<Line> = Vec::new();
let mut choice_letter = b'a';
for item in &scroll.lines {
match item {
ScrollLine::Text(s) => {
for wrapped in wrap_text(s, max_text_w) {
// Leading space provides left-padding inside the border.
lines.push(Line::styled(format!(" {wrapped}"), text_style));
}
}
ScrollLine::Choice { display, .. } => {
let key = choice_letter as char;
lines.push(Line::from(vec![
Span::styled(format!(" [{key}] "), key_style),
Span::styled(display.clone(), choice_style),
]));
choice_letter += 1;
}
}
}
// Derive overlay dimensions. Width fits the longest line; height animates.
let content_w = lines.iter().map(|l| l.width()).max().unwrap_or(0).min(max_text_w);
let outer_w = (content_w + 4) as u16; // border (1) + pad (1) each side = +4 total
let full_outer_h = (lines.len() + 2) as u16; // +2 for top/bottom border rows
let max_outer_h = ((area.height as f32 * 0.85) as u16).max(4);
let actual_outer_h = ((full_outer_h as f32 * anim).ceil() as u16)
.min(max_outer_h)
.max(1);
let left = area.x + area.width.saturating_sub(outer_w) / 2;
let top = area.y + area.height.saturating_sub(actual_outer_h) / 2;
let overlay_rect = Rect { x: left, y: top, width: outer_w, height: actual_outer_h };
// Dim the background — no ratatui widget does this, so it stays manual.
let dim_style = Style::default().fg(Color::Rgb(60, 60, 60)).bg(Color::Black);
for y in area.top()..area.bottom() {
for x in area.left()..area.right() {
if let Some(cell) = buf.cell_mut((x, y)) {
cell.set_style(dim_style);
}
}
}
// Render the bordered box, then the scrollable content inside it.
let block = Block::bordered().border_style(border_style).style(Style::default().bg(bg));
let inner = block.inner(overlay_rect);
block.render(overlay_rect, buf);
Paragraph::new(lines).scroll((offset, 0)).render(inner, buf);
}
/// Wraps `text` at word boundaries so no line exceeds `max_width` characters.
/// Used to pre-compute line count for the scroll overlay's animated height.
fn wrap_text(text: &str, max_width: usize) -> Vec<String> {
if max_width == 0 { return vec![text.to_string()]; }
let mut lines: Vec<String> = Vec::new();
let mut current = String::new();
for word in text.split_whitespace() {
if current.is_empty() {
current.push_str(word);
} else if current.len() + 1 + word.len() <= max_width {
current.push(' ');
current.push_str(word);
} else {
lines.push(std::mem::take(&mut current));
current.push_str(word);
}
}
if !current.is_empty() { lines.push(current); }
if lines.is_empty() { lines.push(String::new()); }
lines
}