Skip to main content

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;