1mod features;
5
6use crate::{Diagnostic, Target, TargetDescriptor};
7use bern_ir::{Application, IR_VERSION};
8
9pub 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
32fn 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
53fn 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 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
105fn 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
133fn 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
146fn 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
169fn 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
189fn 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
257fn 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
301fn 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}