Skip to main content

shuyi_ui/
tips.rs

1//! One-time tips: which have been shown ([`TipsSeen`]), kept in
2//! [`Storage`] under [`TIPS_SEEN_KEY`] (the player profile's), and
3//! forgetting them all ([`reset_tips`]). When a tip shows, and where, is
4//! the game's; a tip's `{Action}` placeholders are filled like any text
5//! ([`Ctx::text_with`](crate::Ctx::text_with)).
6
7use crate::prelude::*;
8use alloc::collections::BTreeSet;
9
10use crate::storage::{Storage, StorageError};
11
12/// [`Storage`] key under which the ids of the tips already shown are saved,
13/// one per line.
14pub const TIPS_SEEN_KEY: &str = "tips_seen";
15
16/// The tips already shown to this profile.
17#[derive(Debug, Clone, Default, PartialEq, Eq)]
18pub struct TipsSeen {
19    ids: BTreeSet<String>,
20}
21
22impl TipsSeen {
23    /// The ids saved in `storage`; none if nothing is saved or it can't be
24    /// read (the tips then show again).
25    pub fn load(storage: &dyn Storage) -> Self {
26        let saved = storage.read(TIPS_SEEN_KEY).ok().flatten();
27        let ids = saved
28            .iter()
29            .flat_map(|s| s.lines())
30            .map(str::trim)
31            .filter(|l| !l.is_empty())
32            .map(str::to_owned)
33            .collect();
34        Self { ids }
35    }
36
37    /// Whether the tip `id` was shown.
38    pub fn contains(&self, id: &str) -> bool {
39        self.ids.contains(id)
40    }
41
42    /// Records that the tip `id` was shown and saves the list. The tip
43    /// counts as seen even if saving fails (it then shows again next
44    /// launch).
45    pub fn mark(&mut self, id: &str, storage: &mut dyn Storage) {
46        if self.ids.insert(id.to_owned()) {
47            let text: Vec<&str> = self.ids.iter().map(String::as_str).collect();
48            storage.write(TIPS_SEEN_KEY, &text.join("\n")).ok();
49        }
50    }
51}
52
53/// Forgets every tip seen, so they all show again (an options screen's
54/// "reset tips").
55pub fn reset_tips(storage: &mut dyn Storage) -> Result<(), StorageError> {
56    storage.delete(TIPS_SEEN_KEY)
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62    use crate::storage::MemoryStorage;
63
64    #[test]
65    fn seen_tips_are_saved_and_loaded() {
66        let mut storage = MemoryStorage::new();
67        let mut seen = TipsSeen::load(&storage);
68        assert!(!seen.contains("a"));
69        seen.mark("a", &mut storage);
70        seen.mark("b", &mut storage);
71        seen.mark("a", &mut storage);
72        assert_eq!(
73            storage.read(TIPS_SEEN_KEY).unwrap().as_deref(),
74            Some("a\nb")
75        );
76        let again = TipsSeen::load(&storage);
77        assert!(again.contains("a") && again.contains("b") && !again.contains("c"));
78        assert_eq!(again, seen);
79        // Blank lines and spaces around an id don't count.
80        storage.write(TIPS_SEEN_KEY, " c \n\n  \nd").unwrap();
81        let read = TipsSeen::load(&storage);
82        assert!(read.contains("c") && read.contains("d"));
83        assert_eq!(read.ids.len(), 2);
84    }
85
86    #[test]
87    fn resetting_forgets_every_tip() {
88        let mut storage = MemoryStorage::new();
89        TipsSeen::default().mark("a", &mut storage);
90        let mut seen = TipsSeen::load(&storage);
91        seen.mark("b", &mut storage);
92        reset_tips(&mut storage).unwrap();
93        assert_eq!(TipsSeen::load(&storage), TipsSeen::default());
94        // Nothing to reset is fine.
95        reset_tips(&mut storage).unwrap();
96    }
97}