Skip to main content

yog_ui/
theme.rs

1//! Themes: the look of `.yui` screens, apart from their markup.
2//!
3//! A screen's markup describes what's on it; without a theme it draws in
4//! yog-ui's plain look (flat colors). A theme gives each kind of element
5//! (`Button`, `ListView`, `Screen`, …) default attributes — the same
6//! attributes `.yui` writes (`bg`, `color`, `hover:bg`, …) — that apply
7//! wherever the element doesn't set them itself or through a `style`.
8//!
9//! A `.yui` picks one with `<Screen theme="minecraft:vanilla">`: a built-in
10//! theme ([`Theme::builtin`]) or a mod's `.ytheme` file — JSON shaped like
11//! [`VANILLA`]: `{ "Button": { "bg": "sprite:button", ... }, ... }`.
12
13/// One element kind's defaults: `(attribute, value)`.
14pub type Rule = (String, Vec<(String, String)>);
15
16/// Element kind → default attributes.
17#[derive(Debug, Clone, Default, PartialEq)]
18pub struct Theme {
19    rules: Vec<Rule>,
20}
21
22/// Vanilla Minecraft's look: the grey window, stone buttons, inset slots
23/// for lists, dark-grey labels.
24pub const VANILLA: &[(&str, &[(&str, &str)])] = &[
25    ("Screen", &[("bg", "sprite:panel")]),
26    ("Label", &[("color", "#404040"), ("shadow", "false")]),
27    (
28        "Button",
29        &[
30            ("bg", "sprite:button"),
31            ("color", "#FFFFFF"),
32            ("shadow", "true"),
33            ("hover:bg", "sprite:button_hover"),
34            ("disabled:bg", "sprite:button_disabled"),
35            ("disabled:color", "#A0A0A0"),
36        ],
37    ),
38    (
39        "ToggleButton",
40        &[
41            ("bg", "sprite:button"),
42            ("color", "#FFFFFF"),
43            ("shadow", "true"),
44            ("hover:bg", "sprite:button_hover"),
45            ("checked:bg", "sprite:button_disabled"),
46            ("checked:color", "#FFFFA0"),
47            ("disabled:bg", "sprite:button_disabled"),
48            ("disabled:color", "#A0A0A0"),
49        ],
50    ),
51    ("ListView", &[("bg", "sprite:slot")]),
52    ("ScrollView", &[("bg", "sprite:slot")]),
53];
54
55impl Theme {
56    /// A built-in theme by id: `minecraft:vanilla`.
57    pub fn builtin(id: &str) -> Option<Theme> {
58        match id {
59            yog_ids::game::VANILLA_THEME => Some(Theme::from_static(VANILLA)),
60            _ => None,
61        }
62    }
63
64    /// Ids of the built-in themes.
65    pub const BUILTIN: &'static [&'static str] = &[yog_ids::game::VANILLA_THEME];
66
67    pub fn from_rules(rules: Vec<Rule>) -> Theme {
68        Theme { rules }
69    }
70
71    fn from_static(rules: &[(&str, &[(&str, &str)])]) -> Theme {
72        Theme {
73            rules: rules
74                .iter()
75                .map(|(k, attrs)| {
76                    let attrs = attrs
77                        .iter()
78                        .map(|(a, v)| (a.to_string(), v.to_string()))
79                        .collect();
80                    (k.to_string(), attrs)
81                })
82                .collect(),
83        }
84    }
85
86    /// The defaults for element `kind` (its `.yui` name).
87    pub fn attrs(&self, kind: &str) -> &[(String, String)] {
88        self.rules
89            .iter()
90            .find(|(k, _)| k == kind)
91            .map_or(&[], |(_, a)| a.as_slice())
92    }
93
94    /// Adds to `attrs` (an element's own, styles already merged) every
95    /// default of `kind` it doesn't set.
96    pub fn apply(&self, kind: &str, attrs: &mut Vec<(String, String)>) {
97        for (k, v) in self.attrs(kind) {
98            if !attrs.iter().any(|(ak, _)| ak == k) {
99                attrs.push((k.clone(), v.clone()));
100            }
101        }
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    #[test]
110    fn own_attributes_win() {
111        let t = Theme::builtin("minecraft:vanilla").unwrap();
112        let mut attrs = vec![("color".to_string(), "#FF0000".to_string())];
113        t.apply("Button", &mut attrs);
114        assert_eq!(attrs[0].1, "#FF0000");
115        assert!(attrs.iter().any(|(k, v)| k == "bg" && v == "sprite:button"));
116        assert!(t.attrs("Spacer").is_empty());
117        assert!(Theme::builtin("nope:nope").is_none());
118    }
119}