Skip to main content

shuyi_ui/widgets/
ask.rs

1//! A question over the screen: a line of text and the answers to it, one
2//! in focus ("Keep these changes? Keep · Discard · Back"), or a yes/no
3//! answered by Confirm and Cancel alone, its keys named under it
4//! ([`Ask::yes_no`]).
5//!
6//! The screen holds an [`Ask`] while the question is open and hands it
7//! each action ([`Ask::handle`]): the cursor moves between the answers,
8//! Confirm gives the focused one and Cancel the answer it stands for, if
9//! any. What the question shows is [`Ask::view`], plain data, and
10//! [`glyph::paint`] draws that view in a box over whatever is under it.
11//! A farming game's "Sell all 12 turnips?" and a platformer's "Restart the
12//! level?" are the same widget.
13
14pub mod glyph;
15pub mod view;
16
17use crate::prelude::*;
18use core::fmt::Display;
19pub use view::{AnswerView, AskView};
20
21use shuyi_assets::keymap::{ActionSet, UiAction};
22
23use super::list::ListCursor;
24use crate::audio::{AudioQueue, MenuSound};
25use crate::ctx::{Ctx, GameData};
26use crate::input::help_keys::HelpKeys;
27
28/// A question and its answers, each a `T` the screen acts on.
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct Ask<T> {
31    /// The text key of the question.
32    key: &'static str,
33    /// The answers in the order shown, each with the text key of its name.
34    answers: Vec<(T, &'static str)>,
35    /// Which answer is in focus.
36    cursor: ListCursor,
37    /// What Cancel answers; `None`: Cancel does nothing.
38    cancel: Option<T>,
39    /// The text key of the line naming the keys that answer, shown instead
40    /// of the answers (a yes/no).
41    keys: Option<&'static str>,
42}
43
44impl<T: Copy + PartialEq> Ask<T> {
45    /// What a question reacts to ([`handle`](Self::handle)), for the
46    /// screen that shows it to declare
47    /// ([`Screen::actions`](crate::screen::Screen::actions)).
48    pub const ACTIONS: &'static [UiAction] = crate::input::NAVIGATION;
49
50    /// The question of the text key `key` with `answers` (each an answer
51    /// and the text key of its name), the first in focus. Cancel does
52    /// nothing until [`cancelled_by`](Self::cancelled_by) names an answer.
53    pub fn new(key: &'static str, answers: impl IntoIterator<Item = (T, &'static str)>) -> Self {
54        let answers: Vec<_> = answers.into_iter().collect();
55        Self {
56            key,
57            cursor: ListCursor::new(answers.len()),
58            answers,
59            cancel: None,
60            keys: None,
61        }
62    }
63
64    /// The same question focused on `answer` (on the first while it isn't
65    /// one of the answers).
66    #[must_use]
67    pub fn focused_on(mut self, answer: T) -> Self {
68        let i = self.answers.iter().position(|&(a, _)| a == answer);
69        self.cursor = self.cursor.at(i.unwrap_or(0));
70        self
71    }
72
73    /// The same question, which Cancel answers with `answer` (going back,
74    /// say).
75    #[must_use]
76    pub fn cancelled_by(mut self, answer: T) -> Self {
77        self.cancel = Some(answer);
78        self
79    }
80
81    /// The text key of the question; also what names it.
82    pub fn key(&self) -> &'static str {
83        self.key
84    }
85
86    /// The answer in focus; `None` with no answers.
87    pub fn focus(&self) -> Option<T> {
88        self.answers.get(self.cursor.focus()).map(|&(a, _)| a)
89    }
90
91    /// What Cancel answers, if anything.
92    pub fn cancel(&self) -> Option<T> {
93        self.cancel
94    }
95
96    /// An action while the question is open. The cursor's keys move the
97    /// focus to the next answer (right and down) or the one before (left
98    /// and up), going round at the ends, with the menu's move sound on
99    /// `audio`. Confirm gives the answer in focus, Cancel the answer it
100    /// stands for ([`cancelled_by`](Self::cancelled_by)); anything else
101    /// gives nothing. An answer's own sound is the screen's to play: what
102    /// it does may still be refused.
103    pub fn handle<A: ActionSet, V>(&mut self, action: A, audio: &mut AudioQueue<V>) -> Option<T> {
104        match action.ui()? {
105            UiAction::Up | UiAction::Left => self.step(false, audio),
106            UiAction::Down | UiAction::Right => self.step(true, audio),
107            UiAction::Confirm => return self.focus(),
108            UiAction::Cancel => return self.cancel,
109            UiAction::Debug => {}
110        }
111        None
112    }
113
114    /// Moves the focus one answer on (`forward`) or back, wrapping.
115    fn step<V>(&mut self, forward: bool, audio: &mut AudioQueue<V>) {
116        if self.cursor.step(forward, true, |_| true) {
117            audio.menu(MenuSound::Move);
118        }
119    }
120
121    /// The question as it is now, as plain data for a skin to paint
122    /// ([`glyph::paint`]): its text and its answers, or the line naming
123    /// its keys, in the player's language naming the active keys.
124    pub fn view<G: GameData>(&self, ctx: &Ctx<G>) -> AskView<T> {
125        self.view_with_keys(ctx, ctx.help_keys(), &[])
126    }
127
128    /// [`view`](Self::view), naming the keys `keys` has rather than the
129    /// active ones, and filling the question's placeholders from `args`
130    /// ([`Ctx::text_with_keys`]).
131    pub fn view_with_keys<G: GameData>(
132        &self,
133        ctx: &Ctx<G>,
134        keys: HelpKeys<'_, G::Action>,
135        args: &[(&str, &dyn Display)],
136    ) -> AskView<T> {
137        let focus = self.cursor.focus();
138        let answers = self
139            .answers
140            .iter()
141            .enumerate()
142            .map(|(i, &(answer, key))| AnswerView {
143                answer,
144                label: ctx.text(key).to_owned(),
145                focused: i == focus,
146            });
147        AskView {
148            text: ctx.text_with_keys(self.key, keys, args),
149            answers: match self.keys {
150                Some(_) => Vec::new(),
151                None => answers.collect(),
152            },
153            keys: self.keys.map(|key| ctx.text_with_keys(key, keys, &[])),
154        }
155    }
156}
157
158impl Ask<bool> {
159    /// A yes/no question of the text key `key`: Confirm answers yes
160    /// (`true`) and Cancel no (`false`). It shows no answers to move
161    /// between, but the line of the text key `keys`, which names the keys
162    /// for yes and no.
163    pub fn yes_no(key: &'static str, keys: &'static str) -> Self {
164        Self {
165            keys: Some(keys),
166            ..Self::new(key, [(true, keys)]).cancelled_by(false)
167        }
168    }
169}
170
171#[cfg(test)]
172mod tests;