Skip to main content

shuyi_ui/screens/
options.rs

1//! An options screen: the player's settings one per row, changed with the
2//! cursor's left and right keys, and rows that do something (open a
3//! screen, ask a question). The game gives the rows ([`OptionRow`]), each
4//! a label key and a typed value: on or off ([`RowValue::Toggle`]), one of
5//! several ([`RowValue::Choice`]), a volume ([`RowValue::Volume`]), or
6//! something Confirm does ([`RowValue::Action`]). The engine's own
7//! settings come as rows ready made ([`OptionRow::text_speed`],
8//! [`OptionRow::fullscreen`], [`OptionRow::frame_cap`], the volumes,
9//! [`OptionRow::restore_defaults`]).
10//!
11//! A change shows at once but is saved only when the player keeps it:
12//! leaving with anything changed asks whether to keep it, throw it away or
13//! stay ([`LeaveChoice`]). A row whose value isn't a setting
14//! ([`OptionRow::unsaved`]) is kept or put back with them. A volume goes
15//! from 0 to [`MAX_VOLUME`]: left and right move its slider, Confirm opens
16//! a box to type the exact number ([`NumberBox`]). Whatever does something
17//! big asks first ([`Question`]).
18//!
19//! **The look is a skin.** This module decides what the screen shows and
20//! does, and says it as plain data: an [`OptionsView`]
21//! ([`OptionsScreen::view`]). [`glyph::paint`] draws that view as glyphs,
22//! unless the game gives another painter ([`OptionsScreen::painted_by`]).
23//! Nothing here knows a cell, a colour or a position, so another look (a
24//! bought UI pack, another device's screen) is another painter of the same
25//! view. Tests of what happened read the view or the settings; only the
26//! painters' tests read cells.
27
28pub mod glyph;
29pub mod view;
30
31use crate::prelude::*;
32use core::fmt;
33pub use view::{OptionsView, RowView, ValueView};
34
35use shuyi_assets::keymap::{ActionSet, UiAction};
36
37use super::text_keys_help;
38use crate::audio::MenuSound;
39use crate::ctx::{Ctx, GameData};
40use crate::frame::glyph_buffer::GlyphBuffer;
41use crate::input::fixed::{TextKey, text_key};
42use crate::screen::{FrameInput, Screen, Transition};
43use crate::settings::{GameSettings, MAX_VOLUME, TextSpeed};
44use crate::widgets::{Ask, ListCursor};
45
46/// Text key ([`Ctx::text`]) of the screen title.
47pub const TITLE: &str = "options.title";
48/// Text key of the question asked on leaving with changes.
49pub const LEAVE_QUESTION: &str = "options.question.keep";
50/// Text key of how a yes/no question is answered, with the keys
51/// ([`Ask::yes_no`]).
52pub const QUESTION_ANSWERS: &str = "options.question.answers";
53/// Text key of the message if something couldn't be saved.
54pub const NOT_SAVED_MESSAGE: &str = "options.message.not_saved";
55/// Text key of the message once the defaults are back.
56pub const RESTORED_MESSAGE: &str = "options.message.restored";
57/// Text keys of an on/off value: off, then on.
58pub const ON_OFF: [&str; 2] = ["options.value.off", "options.value.on"];
59/// Text key of the help line on a row whose value the cursor changes.
60pub const HELP_CHANGE: &str = "options.help.change";
61/// Text key of the Fullscreen row's name ([`OptionRow::fullscreen`]).
62pub const FULLSCREEN_ROW: &str = "options.row.fullscreen";
63/// Text key of the help line on a volume.
64pub const HELP_VOLUME: &str = "options.help.volume";
65/// Text key of the help line while the volume box is open on a
66/// controller.
67pub const HELP_NUMBER_PAD: &str = "options.help.number_pad";
68/// Text key of the help line while a yes/no question is open.
69pub const HELP_QUESTION: &str = "options.help.question";
70/// Text key of the help line while the question asked on leaving is open.
71pub const HELP_LEAVE: &str = "options.help.leave";
72/// Text key of the help line on a row that opens a screen.
73pub const HELP_OPEN: &str = "options.help.open";
74/// Text key of the Frame rate row's label ([`OptionRow::frame_cap`]).
75pub const FRAME_CAP_ROW: &str = "options.row.frame_cap";
76
77/// How far the cursor's left and right keys move a volume's slider.
78/// *Tunable.*
79pub const VOLUME_STEP: u8 = 5;
80/// The most digits the volume box takes.
81const MAX_DIGITS: usize = 3;
82
83/// One row of an options screen, as the game gives it.
84pub struct OptionRow<G: GameData> {
85    /// The text key of its label; also what names the row (the focus).
86    pub key: &'static str,
87    /// What it shows beside its label, and what changes it.
88    pub value: RowValue<G>,
89    /// Whether a blank row comes before it (the first row of a group).
90    pub starts_group: bool,
91    /// Whether its value isn't one of the settings, so the screen keeps or
92    /// puts it back itself on leaving (a campaign's own level, say).
93    pub unsaved: bool,
94}
95
96/// What a row shows beside its label, and what changes it. Build it with
97/// [`RowValue::toggle`], [`choice`](RowValue::choice),
98/// [`volume`](RowValue::volume) or [`action`](RowValue::action): kinds of
99/// row and their fields may come, and never break a game (ADR-0039).
100#[non_exhaustive]
101pub enum RowValue<G: GameData> {
102    /// On or off: the cursor's left key turns it off, the right key on,
103    /// Confirm the other way.
104    #[non_exhaustive]
105    Toggle {
106        /// Whether it is on.
107        get: fn(&Ctx<G>) -> bool,
108        /// Turns it on or off; whether anything changed.
109        set: fn(&mut Ctx<G>, bool) -> bool,
110    },
111    /// One of several values: the cursor's keys step through them,
112    /// stopping at the ends, and Confirm goes round.
113    #[non_exhaustive]
114    Choice {
115        /// The text keys of the values offered, in the order they step.
116        values: fn(&Ctx<G>) -> Vec<&'static str>,
117        /// Which of them the row shows.
118        get: fn(&Ctx<G>) -> usize,
119        /// Puts value `i` in; whether anything changed.
120        set: fn(&mut Ctx<G>, usize) -> bool,
121        /// What the row shows, if not the text of its value's key: a value
122        /// with a number in it (the frame cap's "60 FPS").
123        shown: Option<fn(&Ctx<G>) -> String>,
124    },
125    /// A volume, 0 to [`MAX_VOLUME`]: the keys move its slider by
126    /// [`VOLUME_STEP`], Confirm opens a box to type the number.
127    #[non_exhaustive]
128    Volume {
129        /// The volume.
130        get: fn(&Ctx<G>) -> u8,
131        /// Sets it; whether anything changed.
132        set: fn(&mut Ctx<G>, u8) -> bool,
133    },
134    /// Something Confirm does.
135    #[non_exhaustive]
136    Action {
137        /// What the row shows beside its label, if anything (the layout
138        /// in use, say).
139        value: fn(&Ctx<G>) -> Option<String>,
140        /// What Confirm on it does.
141        confirm: fn(&mut Ctx<G>) -> RowAction<G>,
142        /// The text key of the help line while it has the focus.
143        help: fn(&Ctx<G>) -> &'static str,
144    },
145}
146
147impl<G: GameData> RowValue<G> {
148    /// An on-or-off row: `get` says whether it is on, `set` turns it on or
149    /// off.
150    pub const fn toggle(get: fn(&Ctx<G>) -> bool, set: fn(&mut Ctx<G>, bool) -> bool) -> Self {
151        RowValue::Toggle { get, set }
152    }
153
154    /// A row of one of `values`: `get` says which it shows, `set` puts one
155    /// in.
156    pub const fn choice(
157        values: fn(&Ctx<G>) -> Vec<&'static str>,
158        get: fn(&Ctx<G>) -> usize,
159        set: fn(&mut Ctx<G>, usize) -> bool,
160    ) -> Self {
161        RowValue::Choice {
162            values,
163            get,
164            set,
165            shown: None,
166        }
167    }
168
169    /// A row of one of `values`, as [`choice`](Self::choice) is, that
170    /// shows what `shown` says rather than the text of its value's key: a
171    /// value with a number in it.
172    pub const fn choice_shown(
173        values: fn(&Ctx<G>) -> Vec<&'static str>,
174        get: fn(&Ctx<G>) -> usize,
175        set: fn(&mut Ctx<G>, usize) -> bool,
176        shown: fn(&Ctx<G>) -> String,
177    ) -> Self {
178        RowValue::Choice {
179            values,
180            get,
181            set,
182            shown: Some(shown),
183        }
184    }
185
186    /// A volume row: `get` reads the volume, `set` sets it.
187    pub const fn volume(get: fn(&Ctx<G>) -> u8, set: fn(&mut Ctx<G>, u8) -> bool) -> Self {
188        RowValue::Volume { get, set }
189    }
190
191    /// A row Confirm does `confirm` on, showing `value` beside its label
192    /// and `help` while it has the focus.
193    pub const fn action(
194        value: fn(&Ctx<G>) -> Option<String>,
195        confirm: fn(&mut Ctx<G>) -> RowAction<G>,
196        help: fn(&Ctx<G>) -> &'static str,
197    ) -> Self {
198        RowValue::Action {
199            value,
200            confirm,
201            help,
202        }
203    }
204}
205
206/// What Confirm on an [`RowValue::Action`] row does.
207pub enum RowAction<G: GameData> {
208    /// Opens a screen over the options.
209    Open(Box<dyn Screen<G>>),
210    /// Asks first.
211    Ask(Question<G>),
212    /// Nothing can be done here: the denied sound, and this message under
213    /// the panel if there is one.
214    Refuse(Option<&'static str>),
215}
216
217/// A yes/no question asked before something that can't be taken back with
218/// one key ([`Ask::yes_no`] while it is open).
219pub struct Question<G: GameData> {
220    /// The text key of the question; also what names it
221    /// ([`OptionsScreen::asking`]).
222    pub key: &'static str,
223    /// Does it, on a yes: the text key of the message to show.
224    pub yes: fn(&mut Ctx<G>) -> &'static str,
225}
226
227impl<G: GameData> Clone for Question<G> {
228    fn clone(&self) -> Self {
229        *self
230    }
231}
232
233impl<G: GameData> Copy for Question<G> {}
234
235impl<G: GameData> fmt::Debug for Question<G> {
236    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
237        // check-text: not player text, the type name for Debug
238        f.debug_tuple("Question").field(&self.key).finish()
239    }
240}
241
242impl<G: GameData> Question<G> {
243    /// The question as it is asked: yes on Confirm, no on Cancel, its keys
244    /// named under it ([`QUESTION_ANSWERS`]).
245    pub fn ask(&self) -> Ask<bool> {
246        Ask::yes_no(self.key, QUESTION_ANSWERS)
247    }
248}
249
250impl<G: GameData> OptionRow<G> {
251    /// A row labelled by the text key `key`, showing `value`.
252    pub fn new(key: &'static str, value: RowValue<G>) -> Self {
253        Self {
254            key,
255            value,
256            starts_group: false,
257            unsaved: false,
258        }
259    }
260
261    /// The same row, first of a group: a blank row comes before it.
262    #[must_use]
263    pub fn starting_group(mut self) -> Self {
264        self.starts_group = true;
265        self
266    }
267
268    /// The same row, whose value isn't one of the settings: the screen
269    /// keeps it or puts it back itself on leaving.
270    #[must_use]
271    pub fn unsaved(mut self) -> Self {
272        self.unsaved = true;
273        self
274    }
275
276    /// The speed text is revealed at ([`TextSpeed`], in the settings).
277    pub fn text_speed() -> Self {
278        Self::new(
279            "options.row.text_speed",
280            RowValue::choice(
281                |_| TextSpeed::ALL.map(TextSpeed::key).to_vec(),
282                |ctx| {
283                    let speed = ctx.settings().text_speed;
284                    TextSpeed::ALL.iter().position(|&s| s == speed).unwrap_or(0)
285                },
286                |ctx, i| {
287                    let speed = TextSpeed::ALL.get(i).copied();
288                    ctx.preview_settings(|s| s.text_speed = speed.unwrap_or(s.text_speed))
289                },
290            ),
291        )
292    }
293
294    /// The most frames a second the platform shows (the frame cap, in the
295    /// settings), labelled Frame rate ([`FRAME_CAP_ROW`]): it steps through
296    /// the game's choices
297    /// ([`GameSettings::FRAME_CAP_CHOICES`]), each shown as "60 FPS"
298    /// ([`FPS_KEY`](crate::frame_cap::FPS_KEY)) or "Unlimited"
299    /// ([`UNLIMITED_KEY`](crate::frame_cap::UNLIMITED_KEY)). A cap that
300    /// isn't among them (set in code) shows as it is, and steps from the
301    /// first. The platform applies a change from the next frame, before
302    /// the player keeps it.
303    pub fn frame_cap() -> Self {
304        Self::new(
305            FRAME_CAP_ROW,
306            RowValue::choice_shown(
307                |_| {
308                    let choices = <G::Settings as GameSettings>::FRAME_CAP_CHOICES;
309                    choices.iter().map(|cap| cap.key()).collect()
310                },
311                |ctx| {
312                    let choices = <G::Settings as GameSettings>::FRAME_CAP_CHOICES;
313                    let cap = ctx.settings().frame_cap;
314                    choices.iter().position(|&c| c == cap).unwrap_or(0)
315                },
316                |ctx, i| {
317                    let choices = <G::Settings as GameSettings>::FRAME_CAP_CHOICES;
318                    let cap = choices.get(i).copied();
319                    ctx.preview_settings(|s| s.frame_cap = cap.unwrap_or(s.frame_cap))
320                },
321                |ctx| ctx.settings().frame_cap.name(ctx),
322            ),
323        )
324    }
325
326    /// Whether the game fills the screen (in the settings). The options
327    /// screen leaves it out where the game isn't in a window
328    /// ([`OptionsScreen::rows`]).
329    pub fn fullscreen() -> Self {
330        Self::new(
331            FULLSCREEN_ROW,
332            RowValue::Toggle {
333                get: |ctx| ctx.settings().fullscreen,
334                set: |ctx, on| ctx.preview_settings(|s| s.fullscreen = on),
335            },
336        )
337    }
338
339    /// How loud the music plays (in the settings).
340    pub fn music_volume() -> Self {
341        Self::new(
342            "options.row.music_volume",
343            RowValue::Volume {
344                get: |ctx| ctx.settings().music_volume,
345                set: |ctx, v| ctx.preview_settings(|s| s.music_volume = v),
346            },
347        )
348    }
349
350    /// How loud the sounds play (in the settings).
351    pub fn sound_volume() -> Self {
352        Self::new(
353            "options.row.sound_volume",
354            RowValue::Volume {
355                get: |ctx| ctx.settings().sound_volume,
356                set: |ctx, v| ctx.preview_settings(|s| s.sound_volume = v),
357            },
358        )
359    }
360
361    /// Whether voice clips play (in the settings). Turning it off stops
362    /// the voice that is playing.
363    pub fn voices() -> Self {
364        Self::new(
365            "options.row.voices",
366            RowValue::Toggle {
367                get: |ctx| ctx.settings().voices,
368                set: |ctx, on| ctx.preview_settings(|s| s.voices = on),
369            },
370        )
371    }
372
373    /// How loud voice clips play (in the settings).
374    pub fn voice_volume() -> Self {
375        Self::new(
376            "options.row.voice_volume",
377            RowValue::Volume {
378                get: |ctx| ctx.settings().voice_volume,
379                set: |ctx, v| ctx.preview_settings(|s| s.voice_volume = v),
380            },
381        )
382    }
383
384    /// Puts every setting back to its default but the layout, asking
385    /// first; kept or thrown away on leaving, like any change.
386    pub fn restore_defaults() -> Self {
387        Self::new(
388            "options.row.restore_defaults",
389            RowValue::Action {
390                value: |_| None,
391                confirm: |_| {
392                    RowAction::Ask(Question {
393                        key: "options.question.restore",
394                        yes: |ctx| {
395                            ctx.preview_settings(|s| *s = s.restored());
396                            RESTORED_MESSAGE
397                        },
398                    })
399                },
400                help: |_| "options.help.restore",
401            },
402        )
403    }
404
405    /// Whether the cursor's left and right keys change its value.
406    pub fn is_setting(&self) -> bool {
407        !matches!(self.value, RowValue::Action { .. })
408    }
409
410    /// The text key of the help line while it has the focus.
411    fn help(&self, ctx: &Ctx<G>) -> &'static str {
412        match &self.value {
413            RowValue::Toggle { .. } | RowValue::Choice { .. } => HELP_CHANGE,
414            RowValue::Volume { .. } => HELP_VOLUME,
415            RowValue::Action { help, .. } => help(ctx),
416        }
417    }
418
419    /// The value it shows now, as an index: a choice's, on (1) or off (0),
420    /// the volume; `None` for a row that only does something.
421    fn index(&self, ctx: &Ctx<G>) -> Option<usize> {
422        match &self.value {
423            RowValue::Toggle { get, .. } => Some(usize::from(get(ctx))),
424            RowValue::Choice { get, .. } => Some(get(ctx)),
425            RowValue::Volume { get, .. } => Some(usize::from(get(ctx))),
426            RowValue::Action { .. } => None,
427        }
428    }
429
430    /// Puts back the value [`index`](Self::index) said it had.
431    fn put_back(&self, ctx: &mut Ctx<G>, index: usize) {
432        match &self.value {
433            RowValue::Toggle { set, .. } => {
434                set(ctx, index == 1);
435            }
436            RowValue::Choice { set, .. } => {
437                set(ctx, index);
438            }
439            RowValue::Volume { set, .. } => {
440                set(ctx, u8::try_from(index).unwrap_or(MAX_VOLUME));
441            }
442            RowValue::Action { .. } => {}
443        }
444    }
445}
446
447/// `n` values' index one step after (`forward`) or before `current`,
448/// stopping at the ends (`wrap`: going round instead).
449fn stepped_index(n: usize, current: usize, forward: bool, wrap: bool) -> usize {
450    if n == 0 {
451        return current;
452    }
453    let mut cursor = ListCursor::new(n).at(current);
454    cursor.step(forward, wrap, |_| true);
455    cursor.focus()
456}
457
458/// `all`'s value one step after (`forward`) or before `current`, stopping
459/// at the ends (`wrap`: going round instead). A value not in `all` steps
460/// from the first.
461pub fn stepped<T: Copy + PartialEq>(all: &[T], current: T, forward: bool, wrap: bool) -> T {
462    let i = all.iter().position(|&v| v == current).unwrap_or(0);
463    let to = stepped_index(all.len(), i, forward, wrap);
464    all.get(to).copied().unwrap_or(current)
465}
466
467/// A volume one slider step ([`VOLUME_STEP`]) up or down, within 0 to
468/// [`MAX_VOLUME`].
469fn stepped_volume(volume: u8, forward: bool) -> u8 {
470    if forward {
471        volume.saturating_add(VOLUME_STEP).min(MAX_VOLUME)
472    } else {
473        volume.saturating_sub(VOLUME_STEP)
474    }
475}
476
477/// The box that takes a volume's exact number. From the keyboard the
478/// digits are typed (the game's keys do nothing meanwhile; the text box's
479/// fixed keys finish, delete and cancel). Opened with a controller button
480/// it holds the volume, and the cursor steps it by one.
481#[derive(Debug, Clone, PartialEq, Eq)]
482pub struct NumberBox {
483    /// What is typed so far (on a controller, the number as stepped).
484    digits: String,
485    /// Whether a controller opened it.
486    pad: bool,
487}
488
489impl NumberBox {
490    /// The number in the box, at most [`MAX_VOLUME`]; `None` while empty.
491    pub fn value(&self) -> Option<u8> {
492        let typed: u32 = self.digits.parse().ok()?;
493        Some(u8::try_from(typed.min(u32::from(MAX_VOLUME))).unwrap_or(MAX_VOLUME))
494    }
495
496    /// The digits in the box.
497    pub fn digits(&self) -> &str {
498        &self.digits
499    }
500}
501
502/// An answer to the question asked on leaving with changes
503/// ([`LEAVE_QUESTION`]).
504#[derive(Debug, Clone, Copy, PartialEq, Eq)]
505pub enum LeaveChoice {
506    /// Save the changes and leave.
507    Keep,
508    /// Put every setting back as it was when the screen opened, and leave.
509    Discard,
510    /// Stay on the screen.
511    Back,
512}
513
514impl LeaveChoice {
515    /// Every answer, in the order shown.
516    pub const ALL: [LeaveChoice; 3] = [LeaveChoice::Keep, LeaveChoice::Discard, LeaveChoice::Back];
517
518    /// The text key of its name.
519    pub const fn key(self) -> &'static str {
520        match self {
521            LeaveChoice::Keep => "options.leave.keep",
522            LeaveChoice::Discard => "options.leave.discard",
523            LeaveChoice::Back => "options.leave.back",
524        }
525    }
526
527    /// The question asked on leaving with changes ([`LEAVE_QUESTION`]):
528    /// every answer, focused on staying, which Cancel answers too.
529    pub fn ask() -> Ask<LeaveChoice> {
530        let answers = LeaveChoice::ALL.map(|choice| (choice, choice.key()));
531        Ask::new(LEAVE_QUESTION, answers)
532            .focused_on(LeaveChoice::Back)
533            .cancelled_by(LeaveChoice::Back)
534    }
535}
536
537/// The game's rows, as they are now: the ones `ctx` shows, top to bottom.
538pub type Rows<G> = fn(&Ctx<G>) -> Vec<OptionRow<G>>;
539
540/// What paints the screen's view: [`glyph::paint`], or the game's own
541/// ([`OptionsScreen::painted_by`]).
542pub type Paint<G> = fn(&Ctx<G>, &OptionsView, &mut GlyphBuffer);
543
544/// The options screen. Cancel closes it, asking first if anything changed.
545pub struct OptionsScreen<G: GameData> {
546    /// The game's rows.
547    rows: Rows<G>,
548    /// What paints the view.
549    paint: Paint<G>,
550    /// The focused row's key; the first row's until the cursor moves.
551    row: Option<&'static str>,
552    /// The settings as they were when the screen opened (taken on its
553    /// first frame), for Discard.
554    opened_with: Option<crate::settings::Settings<G::Settings>>,
555    /// The values of the rows that aren't settings, by key, when the
556    /// screen opened (taken with `opened_with`), for Discard.
557    opened_unsaved: Vec<(&'static str, usize)>,
558    /// The question asked on leaving, while it is open.
559    leaving: Option<Ask<LeaveChoice>>,
560    /// The question open, if any.
561    asking: Option<Question<G>>,
562    /// The volume box, while it is open on the focused volume row.
563    typing: Option<NumberBox>,
564    /// The text key of the message under the panel, if any.
565    message: Option<&'static str>,
566}
567
568impl<G: GameData> fmt::Debug for OptionsScreen<G> {
569    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
570        // check-text: not player text, the type name for Debug
571        f.debug_struct("OptionsScreen")
572            .field("row", &self.row)
573            .field("opened_unsaved", &self.opened_unsaved)
574            .field("leaving", &self.leaving)
575            .field("asking", &self.asking)
576            .field("typing", &self.typing)
577            .field("message", &self.message)
578            .finish_non_exhaustive()
579    }
580}
581
582impl<G: GameData> OptionsScreen<G> {
583    /// Name reported by [`Screen::name`].
584    pub const NAME: &'static str = "options";
585
586    /// The screen of `rows`, its first row focused.
587    pub fn new(rows: Rows<G>) -> Self {
588        Self {
589            rows,
590            paint: glyph::paint,
591            row: None,
592            opened_with: None,
593            opened_unsaved: Vec::new(),
594            leaving: None,
595            asking: None,
596            typing: None,
597            message: None,
598        }
599    }
600
601    /// The same screen with its view painted by `paint` instead of
602    /// [`glyph::paint`]: a game's look for a device of its own.
603    #[must_use]
604    pub fn painted_by(mut self, paint: Paint<G>) -> Self {
605        self.paint = paint;
606        self
607    }
608
609    /// The same screen focused on the row `key` (on the first row while
610    /// that one isn't shown).
611    #[must_use]
612    pub fn focused_on(mut self, key: &'static str) -> Self {
613        self.row = Some(key);
614        self
615    }
616
617    /// The rows `ctx` shows, top to bottom: the game's, less the
618    /// Fullscreen row ([`OptionRow::fullscreen`]'s key) where the game
619    /// isn't in a window ([`PlatformFacts::windowed`]): a device that is
620    /// always full screen.
621    ///
622    /// [`PlatformFacts::windowed`]: crate::PlatformFacts::windowed
623    pub fn rows(&self, ctx: &Ctx<G>) -> Vec<OptionRow<G>> {
624        let mut rows = (self.rows)(ctx);
625        if !ctx.platform.windowed {
626            rows.retain(|row| row.key != FULLSCREEN_ROW);
627        }
628        rows
629    }
630
631    /// The focused row's key (`""` with no rows).
632    pub fn focus(&self, ctx: &Ctx<G>) -> &'static str {
633        let rows = self.rows(ctx);
634        let shown = |key| rows.iter().any(|r| r.key == key);
635        match self.row {
636            Some(key) if shown(key) => key,
637            _ => rows.first().map_or("", |r| r.key),
638        }
639    }
640
641    /// The focused row.
642    fn focused_row(&self, ctx: &Ctx<G>) -> Option<OptionRow<G>> {
643        let key = self.focus(ctx);
644        self.rows(ctx).into_iter().find(|r| r.key == key)
645    }
646
647    /// The key of the question open, if any.
648    pub fn asking(&self) -> Option<&'static str> {
649        self.asking.map(|q| q.key)
650    }
651
652    /// The answer in focus while the question asked on leaving with
653    /// changes is open.
654    pub fn leaving(&self) -> Option<LeaveChoice> {
655        self.leaving.as_ref().and_then(Ask::focus)
656    }
657
658    /// Whether any setting, or any row that isn't one, differs from when
659    /// the screen opened (the layout aside: it is saved when picked).
660    pub fn changed(&self, ctx: &Ctx<G>) -> bool {
661        self.opened_with.as_ref().is_some_and(|opened| {
662            let opened = opened.clone().with_layout_of(ctx.settings());
663            opened != *ctx.settings() || self.unsaved_changed(ctx)
664        })
665    }
666
667    /// Whether a row that isn't a setting shows another value than when the
668    /// screen opened.
669    fn unsaved_changed(&self, ctx: &Ctx<G>) -> bool {
670        let rows = self.rows(ctx);
671        self.opened_unsaved.iter().any(|&(key, was)| {
672            rows.iter()
673                .find(|r| r.key == key)
674                .is_some_and(|r| r.index(ctx) != Some(was))
675        })
676    }
677
678    /// The volume box, while it is open.
679    pub fn typing(&self) -> Option<&NumberBox> {
680        self.typing.as_ref()
681    }
682
683    /// The text key of the message under the panel, if any.
684    pub fn message(&self) -> Option<&str> {
685        self.message
686    }
687
688    /// What `row` shows beside its name. (The volume box, while open, is
689    /// the screen's: [`view`](Self::view).)
690    pub fn value(ctx: &Ctx<G>, row: &OptionRow<G>) -> ValueView {
691        let words = |text: String| ValueView::Text {
692            text,
693            adjustable: row.is_setting(),
694        };
695        match &row.value {
696            RowValue::Toggle { get, .. } => {
697                words(ctx.text(ON_OFF[usize::from(get(ctx))]).to_owned())
698            }
699            RowValue::Choice {
700                shown: Some(shown), ..
701            } => words(shown(ctx)),
702            RowValue::Choice { values, get, .. } => {
703                let values = values(ctx);
704                match values.get(get(ctx)).or(values.first()) {
705                    Some(key) => words(ctx.text(key).to_owned()),
706                    // Nothing to choose from: nothing to show.
707                    None => ValueView::None,
708                }
709            }
710            RowValue::Volume { get, .. } => ValueView::Volume {
711                level: get(ctx),
712                max: MAX_VOLUME,
713            },
714            RowValue::Action { value, .. } => value(ctx).map_or(ValueView::None, words),
715        }
716    }
717
718    /// The screen as it is now, as plain data for a skin to paint
719    /// ([`glyph::paint`]): every row with its name and value, the row in
720    /// focus, the volume box if it is open, the message, the question and
721    /// the help line, all in the player's language and naming their keys.
722    pub fn view(&self, ctx: &Ctx<G>) -> OptionsView {
723        let focus = self.focus(ctx);
724        let rows = self.rows(ctx).into_iter().map(|row| {
725            let focused = row.key == focus;
726            let value = match self.typing.as_ref().filter(|_| focused) {
727                Some(typing) => ValueView::NumberBox {
728                    digits: typing.digits.clone(),
729                    typing: !typing.pad,
730                },
731                None => Self::value(ctx, &row),
732            };
733            RowView {
734                key: row.key,
735                label: ctx.text(row.key).to_owned(),
736                value,
737                focused,
738                starts_group: row.starts_group,
739            }
740        });
741        OptionsView {
742            title: ctx.text(TITLE).to_owned(),
743            rows: rows.collect(),
744            message: self.message.map(|key| ctx.text(key).to_owned()),
745            question: self.asking.map(|question| question.ask().view(ctx)),
746            leaving: self.leaving.as_ref().map(|ask| ask.view(ctx)),
747            help: self.help(ctx),
748        }
749    }
750
751    /// Moves the focus one row up or down, wrapping.
752    fn move_row(&mut self, ctx: &mut Ctx<G>, down: bool) {
753        let keys: Vec<_> = self.rows(ctx).iter().map(|r| r.key).collect();
754        self.row = Some(stepped(&keys, self.focus(ctx), down, true));
755        self.message = None;
756        ctx.audio.menu(MenuSound::Move);
757    }
758
759    /// Steps the focused row's value one on (`forward`) or back; `wrap`
760    /// goes round at the ends. If that changed anything, plays the move
761    /// sound (at the new volume, for the sound volume).
762    fn change(&mut self, ctx: &mut Ctx<G>, forward: bool, wrap: bool) {
763        let Some(row) = self.focused_row(ctx) else {
764            return;
765        };
766        let changed = match row.value {
767            RowValue::Toggle { get, set } => {
768                let to = stepped(&[false, true], get(ctx), forward, wrap);
769                set(ctx, to)
770            }
771            RowValue::Choice {
772                values, get, set, ..
773            } => {
774                let n = values(ctx).len();
775                let to = stepped_index(n, get(ctx), forward, wrap);
776                set(ctx, to)
777            }
778            RowValue::Volume { get, set } => {
779                let to = stepped_volume(get(ctx), forward);
780                set(ctx, to)
781            }
782            RowValue::Action { .. } => false,
783        };
784        if changed {
785            self.message = None;
786            ctx.audio.menu(MenuSound::Move);
787        }
788    }
789
790    /// Opens the volume box on the focused volume row: empty, to type in;
791    /// or, opened with a controller button (`pad`), holding the volume.
792    fn open_box(&mut self, ctx: &mut Ctx<G>, volume: u8, pad: bool) {
793        let digits = if pad {
794            volume.to_string()
795        } else {
796            String::new()
797        };
798        self.typing = Some(NumberBox { digits, pad });
799        ctx.audio.menu(MenuSound::Select);
800    }
801
802    /// Closes the volume box. `keep`: its number becomes the volume (an
803    /// empty box keeps the volume as it was).
804    fn close_box(&mut self, ctx: &mut Ctx<G>, keep: bool) {
805        let value = self.typing.take().and_then(|b| b.value()).filter(|_| keep);
806        let Some(value) = value else {
807            ctx.audio.menu(MenuSound::Cancel);
808            return;
809        };
810        ctx.audio.menu(MenuSound::Select);
811        let Some(RowValue::Volume { set, .. }) = self.focused_row(ctx).map(|r| r.value) else {
812            return;
813        };
814        if set(ctx, value) {
815            self.message = None;
816            ctx.audio.menu(MenuSound::Move);
817        }
818    }
819
820    /// One frame of the volume box.
821    fn type_number(&mut self, ctx: &mut Ctx<G>, input: &FrameInput<G::Action>) {
822        if self.typing.as_ref().is_some_and(|b| b.pad) {
823            for &action in &input.actions {
824                let up = match action.ui() {
825                    Some(UiAction::Up | UiAction::Right) => true,
826                    Some(UiAction::Down | UiAction::Left) => false,
827                    Some(UiAction::Confirm) => return self.close_box(ctx, true),
828                    Some(UiAction::Cancel) => return self.close_box(ctx, false),
829                    _ => continue,
830                };
831                let Some(b) = &mut self.typing else { return };
832                let now = b.value().unwrap_or(0);
833                let to = if up {
834                    now.saturating_add(1).min(MAX_VOLUME)
835                } else {
836                    now.saturating_sub(1)
837                };
838                if to != now {
839                    b.digits = to.to_string();
840                    ctx.audio.menu(MenuSound::Move);
841                }
842            }
843            return;
844        }
845        for &c in input.text() {
846            let Some(b) = &mut self.typing else { return };
847            if c.is_ascii_digit() && b.digits.len() < MAX_DIGITS {
848                b.digits.push(c);
849            } else {
850                ctx.audio.menu(MenuSound::Denied);
851            }
852        }
853        for &chord in input.pressed_chords() {
854            match text_key(chord) {
855                Some(TextKey::Done) => return self.close_box(ctx, true),
856                Some(TextKey::Cancel) => return self.close_box(ctx, false),
857                Some(TextKey::Delete) => {
858                    let Some(b) = &mut self.typing else { return };
859                    if b.digits.pop().is_some() {
860                        ctx.audio.menu(MenuSound::Cancel);
861                    }
862                }
863                None => {}
864            }
865        }
866    }
867
868    /// Confirm on the focused row (`pad`: with a controller button).
869    fn confirm(&mut self, ctx: &mut Ctx<G>, pad: bool) -> Transition<G> {
870        self.message = None;
871        let Some(row) = self.focused_row(ctx) else {
872            return Transition::None;
873        };
874        match row.value {
875            RowValue::Volume { get, .. } => {
876                let volume = get(ctx);
877                self.open_box(ctx, volume, pad);
878            }
879            RowValue::Action { confirm, .. } => match confirm(ctx) {
880                RowAction::Open(screen) => {
881                    ctx.audio.menu(MenuSound::Select);
882                    return Transition::Push(screen);
883                }
884                RowAction::Ask(question) => {
885                    self.asking = Some(question);
886                    ctx.audio.menu(MenuSound::Select);
887                }
888                RowAction::Refuse(message) => {
889                    self.message = message;
890                    ctx.audio.menu(MenuSound::Denied);
891                }
892            },
893            RowValue::Toggle { .. } | RowValue::Choice { .. } => self.change(ctx, true, true),
894        }
895        Transition::None
896    }
897
898    /// An action while `question` is open: Confirm does it, Cancel backs
899    /// out; either closes the question (and returns `true`). Nothing else
900    /// does anything.
901    fn answer(&mut self, ctx: &mut Ctx<G>, question: Question<G>, action: G::Action) -> bool {
902        match question.ask().handle(action, &mut ctx.audio) {
903            Some(true) => {
904                self.message = Some((question.yes)(ctx));
905                ctx.audio.menu(MenuSound::Select);
906            }
907            Some(false) => ctx.audio.menu(MenuSound::Cancel),
908            None => return false,
909        }
910        self.asking = None;
911        true
912    }
913
914    /// Cancel on the rows: leaves, or with anything changed asks whether
915    /// to keep the changes, focused on staying.
916    fn leave(&mut self, ctx: &mut Ctx<G>) -> Transition<G> {
917        if self.changed(ctx) {
918            self.leaving = Some(LeaveChoice::ask());
919            self.message = None;
920            ctx.audio.menu(MenuSound::Select);
921            return Transition::None;
922        }
923        ctx.audio.menu(MenuSound::Cancel);
924        Transition::Pop
925    }
926
927    /// An action while the question asked on leaving is open: the cursor
928    /// moves between the answers, Confirm picks one, Cancel stays. Returns
929    /// the transition and whether the question closed.
930    fn answer_leave(&mut self, ctx: &mut Ctx<G>, action: G::Action) -> (Transition<G>, bool) {
931        let Some(ask) = &mut self.leaving else {
932            return (Transition::None, true);
933        };
934        let Some(choice) = ask.handle(action, &mut ctx.audio) else {
935            return (Transition::None, false);
936        };
937        self.leaving = None;
938        match choice {
939            LeaveChoice::Keep => {
940                if ctx.keep_settings().is_err() {
941                    self.message = Some(NOT_SAVED_MESSAGE);
942                    ctx.audio.menu(MenuSound::Denied);
943                    return (Transition::None, true);
944                }
945                ctx.audio.menu(MenuSound::Select);
946                (Transition::Pop, true)
947            }
948            LeaveChoice::Discard => {
949                if let Some(opened) = self.opened_with.clone() {
950                    // Saves only if something else was saved meanwhile;
951                    // the screen closes either way.
952                    let _ = ctx.restore_settings(&opened);
953                    self.put_back_unsaved(ctx);
954                }
955                ctx.audio.menu(MenuSound::Cancel);
956                (Transition::Pop, true)
957            }
958            LeaveChoice::Back => {
959                ctx.audio.menu(MenuSound::Cancel);
960                (Transition::None, true)
961            }
962        }
963    }
964
965    /// Puts each row that isn't a setting back as it was when the screen
966    /// opened.
967    fn put_back_unsaved(&self, ctx: &mut Ctx<G>) {
968        for &(key, was) in &self.opened_unsaved {
969            if let Some(row) = self.rows(ctx).into_iter().find(|r| r.key == key) {
970                row.put_back(ctx, was);
971            }
972        }
973    }
974
975    /// The bottom help line, naming the keys of the active keymap (or
976    /// their buttons, on a controller): what Confirm does on the focused
977    /// row, if anything.
978    pub fn help(&self, ctx: &Ctx<G>) -> String {
979        match &self.typing {
980            // Typing: the text box's own keys.
981            Some(b) if !b.pad => text_keys_help(ctx),
982            Some(_) => ctx.text_with(HELP_NUMBER_PAD, &[]),
983            None => {
984                let key = if self.leaving.is_some() {
985                    HELP_LEAVE
986                } else if self.asking.is_some() {
987                    HELP_QUESTION
988                } else {
989                    self.focused_row(ctx).map_or(HELP_CHANGE, |r| r.help(ctx))
990                };
991                ctx.text_with(key, &[])
992            }
993        }
994    }
995
996    /// Takes what Discard puts back, on the first frame.
997    fn take_opened(&mut self, ctx: &Ctx<G>) {
998        if self.opened_with.is_some() {
999            return;
1000        }
1001        self.opened_with = Some(ctx.settings().clone());
1002        let rows = self.rows(ctx);
1003        let unsaved = rows.iter().filter(|r| r.unsaved);
1004        self.opened_unsaved = unsaved
1005            .filter_map(|r| Some((r.key, r.index(ctx)?)))
1006            .collect();
1007    }
1008}
1009
1010impl<G: GameData> Screen<G> for OptionsScreen<G> {
1011    fn name(&self) -> &'static str {
1012        Self::NAME
1013    }
1014
1015    /// Its rows, its questions and the volume box on a controller.
1016    fn actions(&self) -> Option<Vec<G::Action>> {
1017        Some(crate::input::ui_actions(crate::input::NAVIGATION))
1018    }
1019
1020    fn update(&mut self, ctx: &mut Ctx<G>, input: &FrameInput<G::Action>) -> Transition<G> {
1021        self.take_opened(ctx);
1022        if self.typing.is_some() {
1023            // The game's keys do nothing while the box is open.
1024            self.type_number(ctx, input);
1025            return Transition::None;
1026        }
1027        for &action in &input.actions {
1028            if self.leaving.is_some() {
1029                let (transition, closed) = self.answer_leave(ctx, action);
1030                if closed {
1031                    // The answer ends this frame's keys.
1032                    return transition;
1033                }
1034                continue;
1035            }
1036            if let Some(question) = self.asking {
1037                // The answer ends this frame's keys.
1038                if self.answer(ctx, question, action) {
1039                    break;
1040                }
1041                continue;
1042            }
1043            match action.ui() {
1044                Some(UiAction::Up) => self.move_row(ctx, false),
1045                Some(UiAction::Down) => self.move_row(ctx, true),
1046                Some(UiAction::Left) => self.change(ctx, false, false),
1047                Some(UiAction::Right) => self.change(ctx, true, false),
1048                Some(UiAction::Confirm) => {
1049                    let transition = self.confirm(ctx, input.pad_pressed());
1050                    if !matches!(transition, Transition::None) {
1051                        return transition;
1052                    }
1053                    if self.asking.is_some() || self.typing.is_some() {
1054                        // The key that asked isn't the answer, and the
1055                        // key that opened the box types nothing.
1056                        break;
1057                    }
1058                }
1059                Some(UiAction::Cancel) => {
1060                    let transition = self.leave(ctx);
1061                    if !matches!(transition, Transition::None) {
1062                        return transition;
1063                    }
1064                    // The key that asked isn't the answer.
1065                    break;
1066                }
1067                _ => {}
1068            }
1069        }
1070        Transition::None
1071    }
1072
1073    fn draw(&self, ctx: &Ctx<G>, buf: &mut GlyphBuffer) {
1074        (self.paint)(ctx, &self.view(ctx), buf);
1075    }
1076
1077    fn as_any(&self) -> Option<&dyn core::any::Any> {
1078        Some(self)
1079    }
1080}
1081
1082#[cfg(test)]
1083mod tests;