Skip to main content

nifty_check/checker/
consts.rs

1//! Element arguments that must be written literally: layout numbers, fixed text, flags and
2//! styles.
3
4use nifty_runtime::{
5    Align, Backdrop, ButtonStyle, Icon, Justify, Surface, TextAlign, TextColor, TextStyle,
6};
7use nifty_syntax::ast::{self, ExprKind as Ast, StringPart, UnaryOp};
8
9use super::Checker;
10use crate::elements::Options;
11
12pub(super) const ALIGNMENTS: &[(&str, Align)] = &[
13    ("stretch", Align::Stretch),
14    ("start", Align::Start),
15    ("center", Align::Center),
16    ("end", Align::End),
17];
18
19pub(super) const JUSTIFICATIONS: &[(&str, Justify)] = &[
20    ("start", Justify::Start),
21    ("center", Justify::Center),
22    ("end", Justify::End),
23    ("space_between", Justify::SpaceBetween),
24];
25
26pub(super) const SURFACES: &[(&str, Surface)] = &[
27    ("none", Surface::None),
28    ("sidebar", Surface::Sidebar),
29    ("content", Surface::Content),
30    ("card", Surface::Card),
31];
32
33pub(super) const BACKDROPS: &[(&str, Backdrop)] = &[
34    ("standard", Backdrop::Standard),
35    ("translucent", Backdrop::Translucent),
36    ("solid", Backdrop::Solid),
37];
38
39pub(super) const TEXT_STYLES: &[(&str, TextStyle)] = &[
40    ("body", TextStyle::Body),
41    ("title", TextStyle::Title),
42    ("headline", TextStyle::Headline),
43    ("caption", TextStyle::Caption),
44    ("large_title", TextStyle::LargeTitle),
45];
46
47pub(super) const TEXT_COLORS: &[(&str, TextColor)] = &[
48    ("primary", TextColor::Primary),
49    ("secondary", TextColor::Secondary),
50    ("tertiary", TextColor::Tertiary),
51    ("accent", TextColor::Accent),
52    ("success", TextColor::Success),
53    ("warning", TextColor::Warning),
54    ("danger", TextColor::Danger),
55];
56
57pub(super) const TEXT_ALIGNMENTS: &[(&str, TextAlign)] = &[
58    ("start", TextAlign::Start),
59    ("center", TextAlign::Center),
60    ("end", TextAlign::End),
61];
62
63pub(super) const BUTTON_STYLES: &[(&str, ButtonStyle)] = &[
64    ("normal", ButtonStyle::Normal),
65    ("primary", ButtonStyle::Primary),
66    ("plain", ButtonStyle::Plain),
67    ("icon", ButtonStyle::Icon),
68];
69
70/// Every icon but `Icon::None`, which is what leaving `icon:` out means.
71pub(super) const ICONS: &[(&str, Icon)] = &[
72    ("plus", Icon::Plus),
73    ("trash", Icon::Trash),
74    ("star", Icon::Star),
75    ("star_fill", Icon::StarFill),
76    ("check", Icon::Check),
77    ("check_circle", Icon::CheckCircle),
78    ("circle", Icon::Circle),
79    ("home", Icon::Home),
80    ("inbox", Icon::Inbox),
81    ("archive", Icon::Archive),
82    ("list", Icon::List),
83    ("calendar", Icon::Calendar),
84    ("sun", Icon::Sun),
85    ("flag", Icon::Flag),
86    ("close", Icon::Close),
87    ("gear", Icon::Gear),
88    ("search", Icon::Search),
89    ("mail", Icon::Mail),
90    ("send", Icon::Send),
91    ("folder", Icon::Folder),
92    ("clock", Icon::Clock),
93    ("tag", Icon::Tag),
94    ("person", Icon::Person),
95    ("document", Icon::Document),
96    ("download", Icon::Download),
97    ("heart", Icon::Heart),
98    ("bell", Icon::Bell),
99    ("pencil", Icon::Pencil),
100    ("share", Icon::Share),
101    ("info", Icon::Info),
102];
103
104impl Options {
105    /// The values in this set, written after a `.`.
106    #[must_use]
107    pub fn names(self) -> Vec<&'static str> {
108        match self {
109            Self::Alignment => names(ALIGNMENTS),
110            Self::Justification => names(JUSTIFICATIONS),
111            Self::TextStyle => names(TEXT_STYLES),
112            Self::TextColor => names(TEXT_COLORS),
113            Self::TextAlignment => names(TEXT_ALIGNMENTS),
114            Self::ButtonStyle => names(BUTTON_STYLES),
115            Self::Surface => names(SURFACES),
116            Self::Icon => names(ICONS),
117            Self::Backdrop => names(BACKDROPS),
118        }
119    }
120}
121
122fn names<T>(options: &[(&'static str, T)]) -> Vec<&'static str> {
123    options.iter().map(|&(name, _)| name).collect()
124}
125
126/// Layout sizes are points on screen; anything larger is a mistake.
127const MAX_POINTS: f64 = 10_000.0;
128/// Whole numbers written into settings, such as a slider's range, stay this close to zero,
129/// so every platform's controls hold them exactly.
130pub(super) const MAX_FIXED_NUMBER: i64 = 1_000_000_000;
131
132impl Checker {
133    /// A size in points, written as a number literal.
134    pub(super) fn const_points(&mut self, expr: &ast::Expr) -> Option<f32> {
135        let value = match expr.kind {
136            Ast::Int(value) => u16::try_from(value).ok().map(f64::from),
137            Ast::Float(value) => Some(value),
138            _ => {
139                self.error("this must be a number such as `12`", expr.span);
140                return None;
141            }
142        };
143        if let Some(value) = value.filter(|value| (0.0..=MAX_POINTS).contains(value)) {
144            return Some(points(value));
145        }
146        self.error(
147            format!("this must be between 0 and {MAX_POINTS}"),
148            expr.span,
149        );
150        None
151    }
152
153    /// A whole number written literally, such as `10` or `-10`.
154    pub(super) fn const_number(&mut self, expr: &ast::Expr) -> Option<i64> {
155        let value = match &expr.kind {
156            Ast::Int(value) => Some(*value),
157            Ast::Unary {
158                op: UnaryOp::Neg,
159                operand,
160            } => match operand.kind {
161                Ast::Int(value) => Some(-value),
162                _ => None,
163            },
164            _ => None,
165        };
166        let Some(value) = value else {
167            self.error("this must be a whole number such as `10`", expr.span);
168            return None;
169        };
170        if (-MAX_FIXED_NUMBER..=MAX_FIXED_NUMBER).contains(&value) {
171            return Some(value);
172        }
173        self.error(
174            format!("this must be between -{MAX_FIXED_NUMBER} and {MAX_FIXED_NUMBER}"),
175            expr.span,
176        );
177        None
178    }
179
180    /// Text written as a plain string literal, without interpolation.
181    pub(super) fn const_string(&mut self, expr: &ast::Expr) -> Option<String> {
182        if let Ast::String(parts) = &expr.kind {
183            let text: Option<String> = parts
184                .iter()
185                .map(|part| match part {
186                    StringPart::Text(text) => Some(text.as_str()),
187                    StringPart::Expr(_) => None,
188                })
189                .collect();
190            if text.is_some() {
191                return text;
192            }
193        }
194        self.error("this must be fixed text such as \"Title\"", expr.span);
195        None
196    }
197
198    /// `true` or `false`, written literally.
199    pub(super) fn const_bool(&mut self, expr: &ast::Expr) -> Option<bool> {
200        if let Ast::Bool(value) = expr.kind {
201            return Some(value);
202        }
203        self.error("this must be `true` or `false`", expr.span);
204        None
205    }
206
207    /// One of `options`, written as `.name`.
208    pub(super) fn const_member<T: Copy>(
209        &mut self,
210        expr: &ast::Expr,
211        options: &[(&str, T)],
212    ) -> Option<T> {
213        if let Ast::ImplicitMember(name) = &expr.kind
214            && let Some((_, value)) = options.iter().find(|(option, _)| *option == name.name)
215        {
216            return Some(*value);
217        }
218        let names: Vec<String> = options
219            .iter()
220            .map(|(name, _)| format!("`.{name}`"))
221            .collect();
222        self.error(format!("expected one of {}", names.join(", ")), expr.span);
223        None
224    }
225}
226
227#[expect(
228    clippy::cast_possible_truncation,
229    reason = "range-checked to at most MAX_POINTS, where f32 is far finer than a point"
230)]
231fn points(value: f64) -> f32 {
232    value as f32
233}