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 colours). 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            "minecraft:vanilla" => Some(Theme::from_static(VANILLA)),
60            _ => None,
61        }
62    }
63
64    /// Ids of the built-in themes.
65    pub const BUILTIN: &'static [&'static str] = &["minecraft:vanilla"];
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.iter().map(|(a, v)| (a.to_string(), v.to_string())).collect();
77                    (k.to_string(), attrs)
78                })
79                .collect(),
80        }
81    }
82
83    /// The defaults for element `kind` (its `.yui` name).
84    pub fn attrs(&self, kind: &str) -> &[(String, String)] {
85        self.rules
86            .iter()
87            .find(|(k, _)| k == kind)
88            .map_or(&[], |(_, a)| a.as_slice())
89    }
90
91    /// Adds to `attrs` (an element's own, styles already merged) every
92    /// default of `kind` it doesn't set.
93    pub fn apply(&self, kind: &str, attrs: &mut Vec<(String, String)>) {
94        for (k, v) in self.attrs(kind) {
95            if !attrs.iter().any(|(ak, _)| ak == k) {
96                attrs.push((k.clone(), v.clone()));
97            }
98        }
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    #[test]
107    fn own_attributes_win() {
108        let t = Theme::builtin("minecraft:vanilla").unwrap();
109        let mut attrs = vec![("color".to_string(), "#FF0000".to_string())];
110        t.apply("Button", &mut attrs);
111        assert_eq!(attrs[0].1, "#FF0000");
112        assert!(attrs.iter().any(|(k, v)| k == "bg" && v == "sprite:button"));
113        assert!(t.attrs("Spacer").is_empty());
114        assert!(Theme::builtin("nope:nope").is_none());
115    }
116}