Added scroll()
This commit is contained in:
+95
-2
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user