Skip to main content

qframe/animation/
load.rs

1//! Reading `[animations.<name>]` tables from icon set, theme and application files.
2
3use std::collections::BTreeMap;
4
5use toml::de::{DeTable, DeValue};
6
7use super::{
8    AnimationFrame, CellAnimation, CellColor, ColorMode, FrameTime, MAX_FRAMES, Playback, check_glyph, is_valid_name,
9};
10use crate::diagnostics::Diagnostic;
11use crate::doc::{self, Doc, Value};
12use crate::icons::GlyphMode;
13
14/// Keys of an animation table.
15const ANIMATION_KEYS: [&str; 5] = ["frame", "playback", "colors", "rest", "frames"];
16
17/// Keys of one frame.
18const FRAME_KEYS: [&str; 5] = ["nerd", "unicode", "ascii", "color", "duration"];
19
20/// Reads every `<name> = { … }` of an `[animations]` table into `animations`. A broken animation
21/// is reported with its location and skipped as a whole, so the one it would replace stays.
22pub(crate) fn read_animation_table(
23    doc: &Doc<'_>,
24    table: &DeTable<'_>,
25    animations: &mut BTreeMap<String, CellAnimation>,
26    report: &mut Vec<Diagnostic>,
27) {
28    for (key, value) in table {
29        let name = key.get_ref();
30        if !is_valid_name(name) {
31            report.push(
32                doc.error(
33                    &key.span(),
34                    format!("animation name `{name}` may use only lowercase letters, digits and `-`"),
35                ),
36            );
37            continue;
38        }
39        match read_animation(doc, name, value) {
40            Ok(animation) => {
41                animations.insert(name.to_string(), animation);
42            }
43            Err(diagnostic) => report.push(diagnostic),
44        }
45    }
46}
47
48/// Reads a file holding only `[animations.<name>]` tables, such as the animations an application
49/// or a tool saved. Returns the usable animations in file order and every problem found; a broken
50/// animation is skipped, a file that is not TOML yields no animations.
51#[must_use]
52pub fn parse_animations(file: &str, text: &str) -> (Vec<(String, CellAnimation)>, Vec<Diagnostic>) {
53    let doc = Doc::new(file, text);
54    let mut report = Vec::new();
55    let root = match doc.parse() {
56        Ok(root) => root,
57        Err(diagnostic) => return (Vec::new(), vec![diagnostic]),
58    };
59    let mut found = Vec::new();
60    for (key, value) in &root {
61        if key.get_ref() != "animations" {
62            report.push(doc.error(&key.span(), format!("unknown section `{}`; expected animations", key.get_ref())));
63            continue;
64        }
65        let table = match doc.table(value, "animations") {
66            Ok(table) => table,
67            Err(diagnostic) => {
68                report.push(diagnostic);
69                continue;
70            }
71        };
72        let mut animations = BTreeMap::new();
73        read_animation_table(&doc, table, &mut animations, &mut report);
74        // Keep the order of the file, which a map would lose.
75        for (name, _) in table {
76            if let Some(animation) = animations.remove(name.get_ref().as_ref()) {
77                found.push((name.get_ref().to_string(), animation));
78            }
79        }
80    }
81    (found, report)
82}
83
84fn read_animation(doc: &Doc<'_>, name: &str, value: &Value<'_>) -> Result<CellAnimation, Diagnostic> {
85    let table = doc.table(value, &format!("animation `{name}`"))?;
86    if let Some((unknown, entry)) = table.iter().find(|(key, _)| !ANIMATION_KEYS.contains(&key.get_ref().as_ref())) {
87        return Err(doc.error(
88            &entry.span(),
89            format!(
90                "animation `{name}` has unknown key `{}`; expected one of: {}",
91                unknown.get_ref(),
92                ANIMATION_KEYS.join(", ")
93            ),
94        ));
95    }
96    let mut animation = CellAnimation::new();
97    if let Some(entry) = doc::get(table, "frame") {
98        let text = doc.string(entry, &format!("animation `{name}`.frame"))?;
99        let time = FrameTime::parse(text).map_err(|message| doc.error(&entry.span(), message))?;
100        animation = animation.frame_time(time);
101    }
102    if let Some(entry) = doc::get(table, "playback") {
103        let text = doc.string(entry, &format!("animation `{name}`.playback"))?;
104        let playback = Playback::from_name(text).ok_or_else(|| {
105            doc.error(&entry.span(), format!("animation `{name}`.playback is `{text}`; use loop, once or bounce"))
106        })?;
107        animation = animation.playback(playback);
108    }
109    if let Some(entry) = doc::get(table, "colors") {
110        let text = doc.string(entry, &format!("animation `{name}`.colors"))?;
111        let colors = ColorMode::from_name(text).ok_or_else(|| {
112            doc.error(&entry.span(), format!("animation `{name}`.colors is `{text}`; use step or blend"))
113        })?;
114        animation = animation.colors(colors);
115    }
116    let Some(frames) = doc::get(table, "frames") else {
117        return Err(doc.error(&value.span(), format!("animation `{name}` has no `frames`")));
118    };
119    let DeValue::Array(items) = frames.get_ref() else {
120        return Err(doc.error(
121            &frames.span(),
122            format!("animation `{name}`.frames must be an array of frames, found {}", frames.get_ref().type_str()),
123        ));
124    };
125    if items.is_empty() {
126        return Err(doc.error(&frames.span(), format!("animation `{name}` needs at least one frame")));
127    }
128    if items.len() > MAX_FRAMES {
129        return Err(doc.error(
130            &frames.span(),
131            format!("animation `{name}` has {} frames; at most {MAX_FRAMES} are allowed", items.len()),
132        ));
133    }
134    for (index, item) in items.iter().enumerate() {
135        animation = animation.frame(read_frame(doc, &format!("animation `{name}` frame {}", index + 1), item)?);
136    }
137    if let Some(entry) = doc::get(table, "rest") {
138        let count = items.len();
139        let rest = integer(entry.get_ref())
140            .and_then(|rest| usize::try_from(rest).ok())
141            .filter(|rest| (1..=count).contains(rest))
142            .ok_or_else(|| {
143                doc.error(&entry.span(), format!("animation `{name}`.rest must be a frame number from 1 to {count}"))
144            })?;
145        animation = animation.rest(rest - 1);
146    }
147    Ok(animation)
148}
149
150fn read_frame(doc: &Doc<'_>, what: &str, value: &Value<'_>) -> Result<AnimationFrame, Diagnostic> {
151    let table = doc.table(value, what)?;
152    if let Some((unknown, entry)) = table.iter().find(|(key, _)| !FRAME_KEYS.contains(&key.get_ref().as_ref())) {
153        return Err(doc.error(
154            &entry.span(),
155            format!("{what} has unknown key `{}`; expected one of: {}", unknown.get_ref(), FRAME_KEYS.join(", ")),
156        ));
157    }
158    let glyph = |key: &str, mode: GlyphMode| -> Result<Option<String>, Diagnostic> {
159        let Some(entry) = doc::get(table, key) else {
160            return Ok(None);
161        };
162        let text = doc.string(entry, &format!("{what}.{key}"))?;
163        check_glyph(text, mode).map_err(|message| doc.error(&entry.span(), format!("{what}.{key}: {message}")))?;
164        Ok(Some(text.to_owned()))
165    };
166    let Some(ascii) = glyph("ascii", GlyphMode::Ascii)? else {
167        return Err(doc.error(&value.span(), format!("{what} is missing its `ascii` glyph, which every frame needs")));
168    };
169    let mut frame = AnimationFrame::new(ascii);
170    if let Some(unicode) = glyph("unicode", GlyphMode::Unicode)? {
171        frame = frame.unicode(unicode);
172    }
173    if let Some(nerd) = glyph("nerd", GlyphMode::Nerd)? {
174        frame = frame.nerd(nerd);
175    }
176    if let Some(entry) = doc::get(table, "color") {
177        let text = doc.string(entry, &format!("{what}.color"))?;
178        let color = CellColor::parse(text).map_err(|message| doc.error(&entry.span(), format!("{what}: {message}")))?;
179        frame = frame.color(color);
180    }
181    if let Some(entry) = doc::get(table, "duration") {
182        let text = doc.string(entry, &format!("{what}.duration"))?;
183        let time = FrameTime::parse(text).map_err(|message| doc.error(&entry.span(), format!("{what}: {message}")))?;
184        frame = frame.duration(time);
185    }
186    Ok(frame)
187}
188
189/// Reads an integer value, if `value` is one that fits in `i64`.
190fn integer(value: &DeValue<'_>) -> Option<i64> {
191    let int = value.as_integer()?;
192    i64::from_str_radix(int.as_str(), int.radix()).ok()
193}