1pub 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
46pub const TITLE: &str = "options.title";
48pub const LEAVE_QUESTION: &str = "options.question.keep";
50pub const QUESTION_ANSWERS: &str = "options.question.answers";
53pub const NOT_SAVED_MESSAGE: &str = "options.message.not_saved";
55pub const RESTORED_MESSAGE: &str = "options.message.restored";
57pub const ON_OFF: [&str; 2] = ["options.value.off", "options.value.on"];
59pub const HELP_CHANGE: &str = "options.help.change";
61pub const FULLSCREEN_ROW: &str = "options.row.fullscreen";
63pub const HELP_VOLUME: &str = "options.help.volume";
65pub const HELP_NUMBER_PAD: &str = "options.help.number_pad";
68pub const HELP_QUESTION: &str = "options.help.question";
70pub const HELP_LEAVE: &str = "options.help.leave";
72pub const HELP_OPEN: &str = "options.help.open";
74pub const FRAME_CAP_ROW: &str = "options.row.frame_cap";
76
77pub const VOLUME_STEP: u8 = 5;
80const MAX_DIGITS: usize = 3;
82
83pub struct OptionRow<G: GameData> {
85 pub key: &'static str,
87 pub value: RowValue<G>,
89 pub starts_group: bool,
91 pub unsaved: bool,
94}
95
96#[non_exhaustive]
101pub enum RowValue<G: GameData> {
102 #[non_exhaustive]
105 Toggle {
106 get: fn(&Ctx<G>) -> bool,
108 set: fn(&mut Ctx<G>, bool) -> bool,
110 },
111 #[non_exhaustive]
114 Choice {
115 values: fn(&Ctx<G>) -> Vec<&'static str>,
117 get: fn(&Ctx<G>) -> usize,
119 set: fn(&mut Ctx<G>, usize) -> bool,
121 shown: Option<fn(&Ctx<G>) -> String>,
124 },
125 #[non_exhaustive]
128 Volume {
129 get: fn(&Ctx<G>) -> u8,
131 set: fn(&mut Ctx<G>, u8) -> bool,
133 },
134 #[non_exhaustive]
136 Action {
137 value: fn(&Ctx<G>) -> Option<String>,
140 confirm: fn(&mut Ctx<G>) -> RowAction<G>,
142 help: fn(&Ctx<G>) -> &'static str,
144 },
145}
146
147impl<G: GameData> RowValue<G> {
148 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 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 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 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 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
206pub enum RowAction<G: GameData> {
208 Open(Box<dyn Screen<G>>),
210 Ask(Question<G>),
212 Refuse(Option<&'static str>),
215}
216
217pub struct Question<G: GameData> {
220 pub key: &'static str,
223 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 f.debug_tuple("Question").field(&self.key).finish()
239 }
240}
241
242impl<G: GameData> Question<G> {
243 pub fn ask(&self) -> Ask<bool> {
246 Ask::yes_no(self.key, QUESTION_ANSWERS)
247 }
248}
249
250impl<G: GameData> OptionRow<G> {
251 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 #[must_use]
263 pub fn starting_group(mut self) -> Self {
264 self.starts_group = true;
265 self
266 }
267
268 #[must_use]
271 pub fn unsaved(mut self) -> Self {
272 self.unsaved = true;
273 self
274 }
275
276 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 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 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 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 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 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 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 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 pub fn is_setting(&self) -> bool {
407 !matches!(self.value, RowValue::Action { .. })
408 }
409
410 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 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 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
447fn 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
458pub 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
467fn 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#[derive(Debug, Clone, PartialEq, Eq)]
482pub struct NumberBox {
483 digits: String,
485 pad: bool,
487}
488
489impl NumberBox {
490 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 pub fn digits(&self) -> &str {
498 &self.digits
499 }
500}
501
502#[derive(Debug, Clone, Copy, PartialEq, Eq)]
505pub enum LeaveChoice {
506 Keep,
508 Discard,
510 Back,
512}
513
514impl LeaveChoice {
515 pub const ALL: [LeaveChoice; 3] = [LeaveChoice::Keep, LeaveChoice::Discard, LeaveChoice::Back];
517
518 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 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
537pub type Rows<G> = fn(&Ctx<G>) -> Vec<OptionRow<G>>;
539
540pub type Paint<G> = fn(&Ctx<G>, &OptionsView, &mut GlyphBuffer);
543
544pub struct OptionsScreen<G: GameData> {
546 rows: Rows<G>,
548 paint: Paint<G>,
550 row: Option<&'static str>,
552 opened_with: Option<crate::settings::Settings<G::Settings>>,
555 opened_unsaved: Vec<(&'static str, usize)>,
558 leaving: Option<Ask<LeaveChoice>>,
560 asking: Option<Question<G>>,
562 typing: Option<NumberBox>,
564 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 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 pub const NAME: &'static str = "options";
585
586 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 #[must_use]
604 pub fn painted_by(mut self, paint: Paint<G>) -> Self {
605 self.paint = paint;
606 self
607 }
608
609 #[must_use]
612 pub fn focused_on(mut self, key: &'static str) -> Self {
613 self.row = Some(key);
614 self
615 }
616
617 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 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 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 pub fn asking(&self) -> Option<&'static str> {
649 self.asking.map(|q| q.key)
650 }
651
652 pub fn leaving(&self) -> Option<LeaveChoice> {
655 self.leaving.as_ref().and_then(Ask::focus)
656 }
657
658 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 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 pub fn typing(&self) -> Option<&NumberBox> {
680 self.typing.as_ref()
681 }
682
683 pub fn message(&self) -> Option<&str> {
685 self.message
686 }
687
688 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 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 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 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 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 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 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 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 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 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 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 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 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 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 pub fn help(&self, ctx: &Ctx<G>) -> String {
979 match &self.typing {
980 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 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 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 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 return transition;
1033 }
1034 continue;
1035 }
1036 if let Some(question) = self.asking {
1037 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 break;
1057 }
1058 }
1059 Some(UiAction::Cancel) => {
1060 let transition = self.leave(ctx);
1061 if !matches!(transition, Transition::None) {
1062 return transition;
1063 }
1064 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;