shuyi_ui/dialogue/text_box.rs
1//! A dialogue's text box: its text word-wrapped into pages, typed out at
2//! the player's text speed and faster while the key that goes on is held
3//! ([`TextBox`]); what it shows ([`TextView`]: the page as revealed, the
4//! replies of a choice, the question before skipping a scene, the marker
5//! that waits for a key); and its glyph look ([`glyph::paint`]).
6
7pub mod glyph;
8
9use crate::prelude::*;
10
11use crate::hurry::{HURRY_DELAY, HURRY_FACTOR, HurryClock};
12use crate::widgets::menu::MenuView;
13use crate::widgets::word_wrap;
14use shuyi_assets::font::CellWidths;
15
16/// Reveal speed multiplier while the key that goes on is held (the
17/// [`HurryClock`]'s default factor).
18pub const FAST_FORWARD: f32 = HURRY_FACTOR;
19/// Seconds that key must be held on a page before it reveals faster, so
20/// the tap that turned the page doesn't speed the next one up (the
21/// [`HurryClock`]'s default delay).
22pub const FAST_FORWARD_DELAY: f32 = HURRY_DELAY;
23/// The marker that says "press to go on" is lit for this long, then out
24/// for as long, while a page waits.
25pub const BLINK_S: f32 = 0.5;
26
27/// The text box's state: the text, wrapped, the page on screen and how
28/// much of it is revealed.
29#[derive(Debug, Clone, PartialEq)]
30pub struct TextBox {
31 /// The text, word-wrapped.
32 lines: Vec<String>,
33 /// Lines per page; longer text pages.
34 per_page: usize,
35 /// Which page of `lines` is shown.
36 page: usize,
37 /// Characters of the page revealed so far (fractional between frames).
38 shown: f32,
39 /// How long the key that goes on has been held on this page, for the
40 /// fast-forward ([`FAST_FORWARD_DELAY`]).
41 hurry: HurryClock,
42 /// Seconds since the page was fully revealed, for the blinking marker.
43 waiting: f32,
44}
45
46/// Characters on a page.
47fn page_len(lines: &[String]) -> f32 {
48 let n: usize = lines.iter().map(|l| l.chars().count()).sum();
49 // Pages are a few hundred characters at most, so this is exact.
50 #[expect(clippy::cast_precision_loss, reason = "at most a few hundred")]
51 let n = n as f32;
52 n
53}
54
55impl TextBox {
56 /// `text` wrapped at `width` cells ([`word_wrap`], a wide character of
57 /// `widths` two cells: [`Ctx::widths`](crate::Ctx::widths)), `per_page`
58 /// lines a page, on its first page with nothing revealed yet.
59 pub fn new(text: &str, width: usize, per_page: usize, widths: &CellWidths) -> Self {
60 Self {
61 lines: word_wrap(text, width, widths),
62 per_page: per_page.max(1),
63 page: 0,
64 shown: 0.0,
65 hurry: HurryClock::default(),
66 waiting: 0.0,
67 }
68 }
69
70 /// The lines of the page on screen, in full.
71 pub fn page_lines(&self) -> &[String] {
72 let start = (self.page * self.per_page).min(self.lines.len());
73 let end = (start + self.per_page).min(self.lines.len());
74 &self.lines[start..end]
75 }
76
77 /// How many of the page's characters are revealed, with the part of
78 /// the next one typed so far.
79 pub fn progress(&self) -> f32 {
80 self.shown
81 }
82
83 /// How many of the page's characters are revealed.
84 pub fn shown(&self) -> usize {
85 #[expect(
86 clippy::cast_possible_truncation,
87 clippy::cast_sign_loss,
88 reason = "shown is between 0 and the page length"
89 )]
90 let shown = self.shown as usize;
91 shown
92 }
93
94 /// Whether the whole page is revealed.
95 pub fn is_revealed(&self) -> bool {
96 self.shown >= page_len(self.page_lines())
97 }
98
99 /// Reveals the whole page at once.
100 pub fn reveal(&mut self) {
101 self.shown = page_len(self.page_lines());
102 }
103
104 /// Turns to the next page, if there is one: returns whether it did.
105 pub fn next_page(&mut self) -> bool {
106 if (self.page + 1) * self.per_page >= self.lines.len() {
107 return false;
108 }
109 self.page += 1;
110 self.start_page();
111 true
112 }
113
114 /// What the key that goes on does: reveals the page being typed
115 /// ([`Step::Revealed`]), else turns to the next page
116 /// ([`Step::NextPage`]), else says the box is [`Step::Done`] and leaves
117 /// it as it is (its owner moves on to the next line, or closes it).
118 pub fn confirm(&mut self) -> Step {
119 if !self.is_revealed() {
120 self.reveal();
121 Step::Revealed
122 } else if self.next_page() {
123 Step::NextPage
124 } else {
125 Step::Done
126 }
127 }
128
129 /// Shows the last page, all of it revealed (the line a choice answers).
130 pub fn to_last_page(&mut self) {
131 self.page = self.lines.len().saturating_sub(1) / self.per_page;
132 self.reveal();
133 }
134
135 /// The page starts over: nothing revealed, no key held, not waiting.
136 fn start_page(&mut self) {
137 self.shown = 0.0;
138 self.hurry.reset();
139 self.waiting = 0.0;
140 }
141
142 /// The key that goes on is no longer counted as held (the box waits on
143 /// something else).
144 pub fn release(&mut self) {
145 self.hurry.release();
146 }
147
148 /// `dt` seconds on, with the key that goes on `held` or not.
149 pub fn hold(&mut self, held: bool, dt: f32) {
150 self.hurry.hold(held, dt);
151 }
152
153 /// `dt` seconds of typing at `chars_per_s` (`None`: the whole page at
154 /// once), faster for the part of them past [`FAST_FORWARD_DELAY`] of
155 /// holding; once the page is revealed, the time it has waited.
156 pub fn type_on(&mut self, dt: f32, chars_per_s: Option<f32>) {
157 if self.is_revealed() {
158 self.waiting += dt;
159 return;
160 }
161 // The part of this frame past the delay goes faster.
162 let dt = self.hurry.passing(dt);
163 let len = page_len(self.page_lines());
164 self.shown = match chars_per_s {
165 Some(speed) => (self.shown + dt * speed).min(len),
166 None => len,
167 };
168 }
169
170 /// Whether the marker that says "press to go on" is lit: it blinks
171 /// while the page waits.
172 pub fn marker_lit(&self) -> bool {
173 self.waiting % (2.0 * BLINK_S) < BLINK_S
174 }
175}
176
177/// What the key that goes on did to a text box ([`TextBox::confirm`]).
178#[derive(Debug, Clone, Copy, PartialEq, Eq)]
179pub enum Step {
180 /// The page was being typed: now it is all revealed.
181 Revealed,
182 /// The page was revealed: the next one is on screen, typing.
183 NextPage,
184 /// The last page was revealed: the box is done.
185 Done,
186}
187
188/// What the text box shows. Build it with [`TextView::new`] and assign the
189/// rest (ADR-0039).
190#[derive(Debug, Clone, PartialEq, Eq)]
191#[non_exhaustive]
192pub struct TextView {
193 /// Whether the text is narration (nobody says it).
194 pub narration: bool,
195 /// The page on screen, word-wrapped, in full. At a choice, the last
196 /// page of the line being answered.
197 pub lines: Vec<String>,
198 /// How many of the page's characters are revealed, from its start.
199 pub shown: usize,
200 /// The replies, while a choice is open.
201 pub choices: Option<MenuView>,
202 /// The "Skip scene?" question, while it is open: shown instead of the
203 /// text.
204 pub skip: Option<SkipView>,
205 /// The page, all shown, waiting for the key that goes on.
206 pub waiting: Option<WaitView>,
207}
208
209impl TextView {
210 /// The page `lines`, its first `shown` characters revealed: not
211 /// narration, with no replies, no skip question and not waiting.
212 pub fn new(lines: Vec<String>, shown: usize) -> Self {
213 Self {
214 narration: false,
215 lines,
216 shown,
217 choices: None,
218 skip: None,
219 waiting: None,
220 }
221 }
222
223 /// The revealed part of each line of the page.
224 pub fn revealed(&self) -> Vec<String> {
225 let mut budget = self.shown;
226 let mut revealed = Vec::new();
227 for line in &self.lines {
228 revealed.push(line.chars().take(budget).collect());
229 budget = budget.saturating_sub(line.chars().count());
230 }
231 revealed
232 }
233}
234
235/// The question asked before skipping the scene.
236#[derive(Debug, Clone, PartialEq, Eq)]
237pub struct SkipView {
238 /// The question.
239 pub question: String,
240 /// How to answer, with the keys.
241 pub answers: String,
242}
243
244/// What shows while a page waits for the key that goes on. Build it with
245/// [`WaitView::new`] and assign the rest (ADR-0039).
246#[derive(Debug, Clone, PartialEq, Eq)]
247#[non_exhaustive]
248pub struct WaitView {
249 /// The name of the key that goes on.
250 pub key: String,
251 /// How to open the log, if its action has a key.
252 pub log_hint: Option<String>,
253 /// Whether the marker that says "press to go on" is lit: it blinks.
254 pub marker: bool,
255}
256
257impl WaitView {
258 /// Waiting for the key named `key`, the marker lit as `marker` says,
259 /// with no log hint.
260 pub fn new(key: impl Into<String>, marker: bool) -> Self {
261 Self {
262 key: key.into(),
263 log_hint: None,
264 marker,
265 }
266 }
267}
268
269#[cfg(test)]
270mod tests;