Skip to main content

bern_target_api/check/
mod.rs

1//! Capability checks: the parts of the IR a target cannot build or show,
2//! as diagnostics pointing back to their declarations.
3
4mod features;
5
6use crate::{Diagnostic, Target, TargetDescriptor};
7use bern_ir::{Application, IR_VERSION};
8
9/// Check the parts of the IR every target must agree on: IR version,
10/// supported components and window presentation requirements.
11pub fn check_common(target: &dyn Target, app: &Application) -> Vec<Diagnostic> {
12    let descriptor = target.descriptor();
13    let mut diagnostics = Vec::new();
14    if !descriptor.supports_ir() || app.ir_version != IR_VERSION {
15        diagnostics.push(Diagnostic::error(format!(
16            "target `{}` consumes IR versions {:?}, the application uses IR version {}",
17            descriptor.id, descriptor.ir_versions, app.ir_version
18        )));
19    }
20    diagnostics.extend(check_components(&descriptor, app));
21    diagnostics.extend(check_styles(&descriptor, app));
22    diagnostics.extend(check_dynamic_items(&descriptor, app));
23    diagnostics.extend(features::check_features(&descriptor, app));
24    diagnostics.extend(check_text_marks(&descriptor, app));
25    diagnostics.extend(check_window_chrome(&descriptor, app));
26    diagnostics.extend(check_motion(&descriptor, app));
27    diagnostics.extend(check_media(&descriptor, app));
28    diagnostics.extend(check_presentation(&descriptor, app));
29    diagnostics
30}
31
32/// Elements, in the components and the windows, of kinds the target does
33/// not support.
34fn check_components(descriptor: &TargetDescriptor, app: &Application) -> Vec<Diagnostic> {
35    let mut diagnostics = Vec::new();
36    let mut visit = |node: &bern_ir::Node| {
37        if let bern_ir::Node::Element(element) = node
38            && !descriptor.components.contains(&element.kind.as_str())
39        {
40            diagnostics.push(
41                Diagnostic::error(format!(
42                    "target `{}` does not support `{}`",
43                    descriptor.id, element.kind
44                ))
45                .at(element.source.clone()),
46            );
47        }
48    };
49    app.walk(&mut |node, _| visit(node));
50    diagnostics
51}
52
53/// Style overrides `descriptor` cannot show on its platforms, unless the
54/// rule exempts the platform.
55fn check_styles(descriptor: &TargetDescriptor, app: &Application) -> Vec<Diagnostic> {
56    let mut diagnostics = Vec::new();
57    for platform in descriptor.platforms {
58        for rule in app.style.rules_for(platform) {
59            if rule.exempt.iter().any(|exempt| exempt == platform) {
60                continue;
61            }
62            let support = descriptor
63                .styles
64                .iter()
65                .find(|support| support.component == rule.component);
66            let effects =
67                rule.base
68                    .iter()
69                    .map(|effect| (None, effect))
70                    .chain(rule.states.iter().flat_map(|state| {
71                        state
72                            .effects
73                            .iter()
74                            .map(move |effect| (Some(state.state), effect))
75                    }));
76            for (state, effect) in effects {
77                // Lengths in place of written ones are laid out by every
78                // target alike.
79                if matches!(effect, bern_ir::style::StyleEffect::Metric { .. })
80                    || (matches!(effect, bern_ir::style::StyleEffect::Padding { .. })
81                        && matches!(rule.component.as_str(), "Row" | "Column"))
82                    || support.is_some_and(|support| support.shows(state, effect.name()))
83                {
84                    continue;
85                }
86                let place = match state {
87                    Some(state) => format!(" in the `{}` state", state.name()),
88                    None => String::new(),
89                };
90                diagnostics.push(
91                    Diagnostic::error(format!(
92                        "target `{}` cannot show `{}` on `{}`{place}; write `#[exempt({platform}, \"…\")]` on the rule to keep {platform}'s own look, or `#[target(…)]` to apply it elsewhere",
93                        descriptor.id,
94                        effect.name(),
95                        rule.component
96                    ))
97                    .at(rule.source.clone()),
98                );
99            }
100        }
101    }
102    diagnostics
103}
104
105/// Navigation items that come and go, on a target whose items of
106/// `Sidebar`, `Tabs` and `Toolbar` cannot.
107fn check_dynamic_items(descriptor: &TargetDescriptor, app: &Application) -> Vec<Diagnostic> {
108    let mut diagnostics = Vec::new();
109    if !descriptor.dynamic_items {
110        let mut dynamic = None;
111        app.walk(&mut |node, _| {
112            if let bern_ir::Node::Element(element) = node
113                && bern_ir::spec::DYNAMIC_ITEMS.contains(&element.kind.as_str())
114                && dynamic.is_none()
115                && has_dynamic_items(&element.children)
116            {
117                dynamic = Some(element);
118            }
119        });
120        if let Some(element) = dynamic {
121            diagnostics.push(
122                Diagnostic::error(format!(
123                    "target `{}` cannot build `{}` items that come and go with `if`, `match` or `ForEach`, including pages of modules that can be switched off",
124                    descriptor.id, element.kind
125                ))
126                .at(element.source.clone()),
127            );
128        }
129    }
130    diagnostics
131}
132
133/// Whether navigation items sit in conditions or repetitions, also inside
134/// a sidebar's sections.
135fn has_dynamic_items(nodes: &[bern_ir::Node]) -> bool {
136    nodes.iter().any(|node| match node {
137        bern_ir::Node::Element(element) if element.kind == "ForEach" => true,
138        bern_ir::Node::Element(element) if element.kind == "Section" => {
139            has_dynamic_items(&element.children)
140        }
141        bern_ir::Node::Element(_) | bern_ir::Node::Component(_) => false,
142        bern_ir::Node::If(_) | bern_ir::Node::IfLet(_) | bern_ir::Node::Match(_) => true,
143    })
144}
145
146/// `Text(mark:)` on a target that does not draw the highlight.
147fn check_text_marks(descriptor: &TargetDescriptor, app: &Application) -> Vec<Diagnostic> {
148    let mut diagnostics = Vec::new();
149    if !descriptor.text_marks {
150        let mut marked = false;
151        app.walk(&mut |node, _| {
152            if let bern_ir::Node::Element(element) = node
153                && element.kind == "Text"
154                && element.props.contains_key("mark")
155            {
156                marked = true;
157            }
158        });
159        if marked {
160            diagnostics.push(Diagnostic::warning(format!(
161                "target `{}` shows `Text(mark:)` as plain text, without the highlight",
162                descriptor.id
163            )));
164        }
165    }
166    diagnostics
167}
168
169/// `chrome`, `immersive` and `idle_after` on a target whose windows
170/// ignore them.
171fn check_window_chrome(descriptor: &TargetDescriptor, app: &Application) -> Vec<Diagnostic> {
172    let mut diagnostics = Vec::new();
173    if !descriptor.window_chrome {
174        for window in &app.windows.windows {
175            if window.chrome != bern_ir::WindowChrome::Standard
176                || window.immersive.is_some()
177                || window.idle_after.is_some()
178            {
179                diagnostics.push(Diagnostic::warning(format!(
180                    "target `{}` keeps window `{}`'s standard title bar: `chrome`, `immersive` and `idle_after` have no effect there, and `window.idle` stays false",
181                    descriptor.id, window.id
182                )));
183            }
184        }
185    }
186    diagnostics
187}
188
189/// Motion props the target shows without motion: one warning for each.
190fn check_motion(descriptor: &TargetDescriptor, app: &Application) -> Vec<Diagnostic> {
191    let mut diagnostics = Vec::new();
192    let mut still = std::collections::BTreeSet::new();
193    app.walk(&mut |node, _| {
194        if let bern_ir::Node::Element(element) = node {
195            for prop in bern_ir::spec::MOTION_PROPS {
196                if prop.name != "hero_key"
197                    && prop.name != "reveal"
198                    && prop.name != "appearance"
199                    && prop.name != "tint"
200                    && prop.name != "spin_period"
201                    && prop.name != "glow_opacity"
202                    && element.props.contains_key(prop.name)
203                    && !descriptor.motion.contains(&prop.name)
204                {
205                    still.insert(prop.name);
206                }
207            }
208        }
209    });
210    for prop in still {
211        let message = if prop == "stagger" {
212            format!(
213                "target `{}` does not stagger: children appear together",
214                descriptor.id
215            )
216        } else if prop == "nudge" {
217            format!(
218                "target `{}` does not nudge: the element stays still",
219                descriptor.id
220            )
221        } else if prop == "spin" {
222            format!(
223                "target `{}` does not spin: the element stays still",
224                descriptor.id
225            )
226        } else if prop == "glow" {
227            format!(
228                "target `{}` does not light elements under the pointer: `glow` shows nothing",
229                descriptor.id
230            )
231        } else if prop == "blur" {
232            format!(
233                "target `{}` does not blur: the element is drawn sharp",
234                descriptor.id
235            )
236        } else if prop == "idle_area" {
237            format!(
238                "target `{}` does not watch where the pointer rests: its windows go idle wherever it is",
239                descriptor.id
240            )
241        } else if prop.starts_with("scroll_") {
242            format!(
243                "target `{}` does not follow scrolling with `{prop}`: items keep their look",
244                descriptor.id
245            )
246        } else {
247            format!(
248                "target `{}` shows `{prop}` without motion: the change happens at once",
249                descriptor.id
250            )
251        };
252        diagnostics.push(Diagnostic::warning(message));
253    }
254    diagnostics
255}
256
257/// `Video` and players on a target without a media backend for this
258/// build's media contract.
259fn check_media(descriptor: &TargetDescriptor, app: &Application) -> Vec<Diagnostic> {
260    let mut diagnostics = Vec::new();
261    if app.uses_media() {
262        let mut video = None;
263        let mut find = |node: &bern_ir::Node| {
264            if let bern_ir::Node::Element(element) = node
265                && element.kind == "Video"
266                && video.is_none()
267            {
268                video = Some(element.source.clone());
269            }
270        };
271        app.walk(&mut |node, _| find(node));
272        let at = |diagnostic: Diagnostic| match &video {
273            Some(location) => diagnostic.at(location.clone()),
274            None => diagnostic,
275        };
276        match &descriptor.media {
277            None => diagnostics.push(at(Diagnostic::error(format!(
278                "target `{}` has no media backend; `Video` and application players cannot be built",
279                descriptor.id
280            )))),
281            Some(media)
282                if !media
283                    .contract_versions
284                    .contains(&bern_media::MEDIA_CONTRACT_VERSION) =>
285            {
286                diagnostics.push(at(Diagnostic::error(format!(
287                    "target `{}` pairs media backend `{}` and presenter `{}` for media contract versions {:?}; this build uses version {}",
288                    descriptor.id,
289                    media.backend,
290                    media.presenter,
291                    media.contract_versions,
292                    bern_media::MEDIA_CONTRACT_VERSION
293                ))));
294            }
295            Some(_) => {}
296        }
297    }
298    diagnostics
299}
300
301/// Window presentations the target cannot provide.
302fn check_presentation(descriptor: &TargetDescriptor, app: &Application) -> Vec<Diagnostic> {
303    let mut diagnostics = Vec::new();
304    for window in &app.windows.windows {
305        match window.presentation {
306            bern_ir::Presentation::NativeRequired if !descriptor.windows.native => {
307                diagnostics.push(Diagnostic::error(format!(
308                    "window `{}` requires native windows, which target `{}` cannot provide",
309                    window.id, descriptor.id
310                )));
311            }
312            bern_ir::Presentation::Floating if !descriptor.windows.floating => {
313                diagnostics.push(Diagnostic::error(format!(
314                    "window `{}` requests floating presentation, which target `{}` cannot provide",
315                    window.id, descriptor.id
316                )));
317            }
318            bern_ir::Presentation::Virtual if !descriptor.windows.virtual_windows => {
319                diagnostics.push(Diagnostic::error(format!(
320                    "window `{}` requests virtual presentation, which target `{}` has not implemented",
321                    window.id, descriptor.id
322                )));
323            }
324            _ => {}
325        }
326    }
327    diagnostics
328}