1use crate::check::location;
6use bern_ir::style::{
7 AccentDef, BUILT_IN_MOTION, Band, COLOR_ROLES, ColorRef, ControlSize, CornerDef, Edge, Metric,
8 MotionToken, PatternDef, StateStyle, StyleEffect, StyleRule, StyleSheet, StyleState,
9 TransitionDef, TransitionKind, built_in_transitions,
10};
11use bern_ir::{SourceLocation, Srgb};
12use bern_syntax::{StyleArg, StyleItem, StyleValue};
13use bern_target_api::Diagnostic;
14use proc_macro2::{Ident, Span};
15
16pub const PLATFORMS: [&str; 7] = [
18 "macos", "ios", "tvos", "windows", "linux", "android", "browser",
19];
20
21const STYLEABLE: &[(&str, &[StyleState])] = &[
23 ("Stack", &[]),
24 ("PopoverButton", &[]),
26 ("Sidebar", &[]),
27 ("Detail", &[]),
28 ("Row", &[]),
30 ("Column", &[]),
31 ("Form", &[]),
33 (
34 "Button",
35 &[
36 StyleState::Hover,
37 StyleState::Pressed,
38 StyleState::Focus,
39 StyleState::Disabled,
40 ],
41 ),
42 (
43 "IconButton",
44 &[
45 StyleState::Hover,
46 StyleState::Pressed,
47 StyleState::Focus,
48 StyleState::Disabled,
49 ],
50 ),
51 (
52 "SidebarItem",
53 &[
54 StyleState::Hover,
55 StyleState::Pressed,
56 StyleState::Selected,
57 StyleState::Focus,
58 ],
59 ),
60];
61
62const BASE_EFFECTS: &[&str] = &[
64 "background",
65 "foreground",
66 "border",
67 "corner",
68 "padding",
69 "size",
70 "shadow",
71 "gap",
72 "padding_x",
73 "padding_y",
74 "width",
75 "height",
76 "font_size",
77 "row_height",
78];
79
80fn writes(metric: Metric) -> &'static [&'static str] {
82 match metric {
83 Metric::Gap | Metric::PaddingX | Metric::PaddingY => &["Row", "Column", "Form"],
84 Metric::RowHeight => &["Form"],
85 Metric::Width => &["Row", "Column", "Stack"],
86 Metric::Height => &["Row", "Column", "Stack", "IconButton"],
87 Metric::FontSize => &["IconButton"],
88 }
89}
90const STATE_EFFECTS: &[&str] = &[
91 "background",
92 "foreground",
93 "border",
94 "scale",
95 "offset",
96 "opacity",
97 "shadow",
98 "system",
99 "none",
100];
101const SELECTED_EFFECTS: &[&str] = &["fill", "indicator"];
103
104struct Lowering<'a> {
105 file: &'a str,
106 diagnostics: &'a mut Vec<Diagnostic>,
107}
108
109impl Lowering<'_> {
110 fn error(&mut self, span: Span, message: impl Into<String>) {
111 self.diagnostics
112 .push(Diagnostic::error(message.into()).at(location(self.file, span)));
113 }
114
115 fn location(&self, span: Span) -> SourceLocation {
116 location(self.file, span)
117 }
118
119 fn number(&mut self, value: &StyleValue, what: &str) -> Option<f64> {
120 match value {
121 StyleValue::Number(number, _) => Some(*number),
122 other => {
123 self.error(other.span(), format!("{what} is a number"));
124 None
125 }
126 }
127 }
128
129 fn length(&mut self, value: &StyleValue, what: &str) -> Option<f64> {
131 let length = self.number(value, what)?;
132 if length < 0.0 {
133 self.error(value.span(), format!("{what} is 0 or more"));
134 return None;
135 }
136 Some(length)
137 }
138
139 fn edge(&mut self, value: &StyleValue) -> Option<Edge> {
140 match value.dotted().as_deref() {
141 Some("leading") => Some(Edge::Leading),
142 Some("trailing") => Some(Edge::Trailing),
143 Some("top") => Some(Edge::Top),
144 Some("bottom") => Some(Edge::Bottom),
145 Some(other) => {
146 self.error(
147 value.span(),
148 format!("`{other}` is not an edge; use leading, trailing, top or bottom"),
149 );
150 None
151 }
152 None => {
153 self.error(value.span(), "an edge is leading, trailing, top or bottom");
154 None
155 }
156 }
157 }
158
159 fn color(&mut self, value: &StyleValue) -> Option<ColorRef> {
160 match value {
161 StyleValue::Name(_) => {
162 let role = value.dotted().unwrap_or_default();
163 if COLOR_ROLES.contains(&role.as_str()) {
164 Some(ColorRef::Role { role })
165 } else {
166 self.error(
167 value.span(),
168 format!(
169 "`{role}` is not a color role; use one of {}",
170 COLOR_ROLES.join(", ")
171 ),
172 );
173 None
174 }
175 }
176 StyleValue::Call { name, args } if name == "color" => {
177 let mut light = None;
178 let mut dark = None;
179 for arg in args {
180 let slot = match arg.name.as_ref().map(ToString::to_string).as_deref() {
181 Some("light") => &mut light,
182 Some("dark") => &mut dark,
183 _ => {
184 self.error(
185 name.span(),
186 "write `color(light: \"#RRGGBB\", dark: \"#RRGGBB\")`",
187 );
188 return None;
189 }
190 };
191 match &arg.value {
192 StyleValue::Text(hex, span) => match Srgb::parse(hex) {
193 Some(color) => *slot = Some(color),
194 None => self.error(*span, format!("`{hex}` is not a `#RRGGBB` color")),
195 },
196 other => self.error(other.span(), "a color is written `\"#RRGGBB\"`"),
197 }
198 }
199 match (light, dark) {
200 (Some(light), Some(dark)) => Some(ColorRef::Fixed { light, dark }),
201 _ => {
202 self.error(
203 name.span(),
204 "a fixed color gives both modes: `color(light: \"#…\", dark: \"#…\")`",
205 );
206 None
207 }
208 }
209 }
210 other => {
211 self.error(
212 other.span(),
213 "a color is a role such as `accent` or `color(light: …, dark: …)`",
214 );
215 None
216 }
217 }
218 }
219
220 fn motion(&mut self, args: &[StyleArg], sheet: &StyleSheet) -> String {
222 let named = args
223 .iter()
224 .rev()
225 .find(|arg| arg.name.is_none() && matches!(arg.value, StyleValue::Name(_)));
226 match named.and_then(|arg| arg.value.dotted()) {
227 Some(token) if sheet.token(&token).is_some() => token,
228 Some(token) => {
229 let span = named
230 .map(|arg| arg.value.span())
231 .unwrap_or_else(Span::call_site);
232 self.error(
233 span,
234 format!("`{token}` is not a motion token; declare it under `motion {{ … }}`"),
235 );
236 "smooth".into()
237 }
238 None => "smooth".into(),
239 }
240 }
241
242 fn numbers(&mut self, args: &[StyleArg]) -> (Vec<f64>, Vec<(String, f64)>) {
243 let mut positional = Vec::new();
244 let mut named = Vec::new();
245 for arg in args {
246 if let StyleValue::Number(number, _) = arg.value {
247 match &arg.name {
248 Some(name) => named.push((name.to_string(), number)),
249 None => positional.push(number),
250 }
251 }
252 }
253 (positional, named)
254 }
255
256 fn edge_transition<'a>(
262 &mut self,
263 name: &Ident,
264 args: &'a [StyleArg],
265 sheet: &StyleSheet,
266 ) -> (TransitionKind, Option<&'a StyleArg>) {
267 let slide = name == "slide";
268 let moves = name == "move";
269 let usage = if slide {
270 "write `slide(bottom, 16, token)` or `slide(edge: bottom, distance: 16, token)`"
271 } else if moves {
272 "write `move(bottom, token)` or `move(edge: bottom, token)`"
273 } else {
274 "write `grow(bottom, token)` or `grow(edge: bottom, token)`"
275 };
276 let mut edge = None;
277 let mut distance = None;
278 let mut names = Vec::new();
279 for arg in args {
280 match (
281 arg.name.as_ref().map(ToString::to_string).as_deref(),
282 &arg.value,
283 ) {
284 (Some("edge"), StyleValue::Name(_)) if edge.is_none() => edge = Some(&arg.value),
285 (Some("distance") | None, StyleValue::Number(..))
286 if slide && distance.is_none() =>
287 {
288 distance = Some(&arg.value)
289 }
290 (None, StyleValue::Name(_)) => names.push(arg),
291 _ => self.error(arg.value.span(), usage),
292 }
293 }
294 let is_edge = |arg: &StyleArg| {
295 arg.value
296 .dotted()
297 .is_some_and(|word| ["leading", "trailing", "top", "bottom"].contains(&&*word))
298 };
299 let token = match names.as_slice() {
300 [] => None,
301 [only] if edge.is_none() && is_edge(only) => {
302 edge = Some(&only.value);
303 None
304 }
305 [token] if edge.is_none() => {
306 let word = token.value.dotted().unwrap_or_default();
307 if sheet.token(&word).is_some() {
308 Some(*token)
309 } else {
310 self.error(
311 token.value.span(),
312 format!(
313 "`{word}` is neither an edge nor a motion token; an edge is leading, trailing, top or bottom"
314 ),
315 );
316 None
317 }
318 }
319 [token] => Some(*token),
320 [first, token] if edge.is_none() => {
321 edge = Some(&first.value);
322 Some(*token)
323 }
324 _ => {
325 self.error(name.span(), usage);
326 None
327 }
328 };
329 let edge = edge
330 .and_then(|value| self.edge(value))
331 .unwrap_or(Edge::Bottom);
332 let kind = if slide {
333 TransitionKind::Slide {
334 edge,
335 distance: distance
336 .and_then(|value| self.length(value, "a slide's distance"))
337 .unwrap_or(16.0),
338 }
339 } else if moves {
340 TransitionKind::Move { edge }
341 } else {
342 TransitionKind::Grow { edge }
343 };
344 (kind, token)
345 }
346
347 fn effect(&mut self, value: &StyleValue, sheet: &StyleSheet) -> Option<StyleEffect> {
348 match value {
349 StyleValue::Name(path) if path.len() == 1 && path[0] == "system" => {
350 Some(StyleEffect::System)
351 }
352 StyleValue::Name(path) if path.len() == 1 && path[0] == "none" => {
353 Some(StyleEffect::Nothing)
354 }
355 StyleValue::Call { name, args } => {
356 let kind = name.to_string();
357 let first = args
359 .iter()
360 .find(|arg| arg.name.is_none())
361 .map(|arg| &arg.value);
362 if first.is_none()
363 && ["background", "foreground", "fill", "border", "indicator"]
364 .contains(&kind.as_str())
365 {
366 self.error(
367 name.span(),
368 format!("`{kind}` takes a color, such as `{kind}(accent)`"),
369 );
370 return None;
371 }
372 let (positional, named) = self.numbers(args);
373 let get = |key: &str| {
374 named
375 .iter()
376 .find(|(name, _)| name == key)
377 .map(|(_, value)| *value)
378 };
379 let width = |lowering: &mut Self, default: f64, what: &str| {
380 let width = get("width").unwrap_or(default);
381 if width < 0.0 {
382 lowering.error(name.span(), format!("{what} is 0 or more"));
383 return None;
384 }
385 Some(width)
386 };
387 match kind.as_str() {
388 "background" => Some(StyleEffect::Background {
389 color: self.color(first?)?,
390 }),
391 "foreground" => Some(StyleEffect::Foreground {
392 color: self.color(first?)?,
393 }),
394 "fill" => Some(StyleEffect::Fill {
395 color: self.color(first?)?,
396 }),
397 "border" => Some(StyleEffect::Border {
398 color: self.color(first?)?,
399 width: width(self, 1.0, "a border's width")?,
400 }),
401 "indicator" => {
402 let edge = match args
403 .iter()
404 .find(|arg| arg.name.as_ref().is_some_and(|name| name == "edge"))
405 {
406 Some(arg) => self.edge(&arg.value).unwrap_or(Edge::Leading),
407 None => Edge::Leading,
408 };
409 Some(StyleEffect::Indicator {
410 color: self.color(first?)?,
411 edge,
412 width: width(self, 3.0, "an indicator's width")?,
413 })
414 }
415 "scale" => {
416 let uniform = positional.first().copied();
417 let x = get("x").or(uniform);
418 let y = get("y").or(uniform);
419 let (Some(x), Some(y)) = (x, y) else {
420 self.error(
421 name.span(),
422 "write `scale(1.03, token)` or `scale(x: 0.97, y: 0.9, token)`",
423 );
424 return None;
425 };
426 Some(StyleEffect::Scale {
427 x,
428 y,
429 motion: self.motion(args, sheet),
430 })
431 }
432 "offset" => Some(StyleEffect::Offset {
433 x: get("x").unwrap_or(0.0),
434 y: get("y").unwrap_or(0.0),
435 motion: self.motion(args, sheet),
436 }),
437 "opacity" => {
438 let Some(value) = positional.first().copied() else {
439 self.error(name.span(), "write `opacity(0.6, token)`");
440 return None;
441 };
442 if !(0.0..=1.0).contains(&value) {
443 self.error(name.span(), "an opacity is from 0 to 1");
444 return None;
445 }
446 Some(StyleEffect::Opacity {
447 value,
448 motion: self.motion(args, sheet),
449 })
450 }
451 "shadow" => {
452 let Some(radius) = positional.first().copied() else {
453 self.error(
454 name.span(),
455 "write `shadow(12)` or `shadow(12, y: 4, opacity: 0.2)`",
456 );
457 return None;
458 };
459 let opacity = get("opacity").unwrap_or(0.2);
460 if radius < 0.0 || !(0.0..=1.0).contains(&opacity) {
461 self.error(
462 name.span(),
463 "a shadow has a radius of 0 or more and an opacity from 0 to 1",
464 );
465 return None;
466 }
467 Some(StyleEffect::Shadow {
468 radius,
469 y: get("y").unwrap_or(radius / 3.0),
470 opacity,
471 })
472 }
473 other => {
474 self.error(name.span(), format!("`{other}` is not a style effect"));
475 None
476 }
477 }
478 }
479 other => {
480 self.error(other.span(), "expected an effect such as `scale(1.03, snappy)`, `fill(accent)`, `system` or `none`");
481 None
482 }
483 }
484 }
485}
486
487pub fn lower(items: &[(String, StyleItem)], diagnostics: &mut Vec<Diagnostic>) -> StyleSheet {
490 let mut sheet = StyleSheet {
491 motion: BUILT_IN_MOTION
492 .iter()
493 .map(|(name, period, damping)| MotionToken {
494 name: (*name).into(),
495 period: *period,
496 damping: *damping,
497 })
498 .collect(),
499 rules: Vec::new(),
500 transitions: built_in_transitions(),
501 patterns: Vec::new(),
502 accents: Vec::new(),
503 text_weights: Vec::new(),
504 corners: Vec::new(),
505 };
506 if items.len() > 1 {
507 for (file, item) in &items[1..] {
508 let mut lowering = Lowering { file, diagnostics };
509 lowering.error(item.span, "an application declares at most one `style!`");
510 }
511 }
512 let Some((file, item)) = items.first() else {
513 return sheet;
514 };
515 let mut lowering = Lowering { file, diagnostics };
516 lower_corners(&mut lowering, item, &mut sheet);
517 for decl in &item.motion {
518 let StyleValue::Call { name, args } = &decl.value else {
519 lowering.error(
520 decl.value.span(),
521 "a motion token is `spring(period: T, damping: ζ)`",
522 );
523 continue;
524 };
525 if name != "spring" {
526 lowering.error(
527 name.span(),
528 "a motion token is `spring(period: T, damping: ζ)`",
529 );
530 continue;
531 }
532 let (_, named) = lowering.numbers(args);
533 let get = |key: &str| {
534 named
535 .iter()
536 .find(|(name, _)| name == key)
537 .map(|(_, value)| *value)
538 };
539 let (Some(period), Some(damping)) = (get("period"), get("damping")) else {
540 lowering.error(name.span(), "a spring needs `period:` and `damping:`");
541 continue;
542 };
543 if !(period > 0.0 && damping > 0.0) {
544 lowering.error(name.span(), "a spring's period and damping are positive");
545 continue;
546 }
547 let token = MotionToken {
548 name: decl.name.to_string(),
549 period,
550 damping,
551 };
552 match sheet
553 .motion
554 .iter_mut()
555 .find(|existing| existing.name == token.name)
556 {
557 Some(existing) => *existing = token,
558 None => sheet.motion.push(token),
559 }
560 }
561 for decl in &item.transitions {
562 let StyleValue::Call { name, args } = &decl.value else {
563 lowering.error(decl.value.span(), "a transition is `fade(token)`, `pop(from: 0.8, token)`, `slide(edge: bottom, distance: 16, token)` or `move(edge: bottom, token)`");
564 continue;
565 };
566 let (positional, named) = lowering.numbers(args);
567 let get = |key: &str| {
568 named
569 .iter()
570 .find(|(name, _)| name == key)
571 .map(|(_, value)| *value)
572 };
573 let mut motion = args.as_slice();
574 let kind = match name.to_string().as_str() {
575 "fade" => TransitionKind::Fade,
576 "pop" => TransitionKind::Pop {
577 from: get("from").or(positional.first().copied()).unwrap_or(0.8),
578 },
579 "slide" | "grow" | "move" => {
580 let (kind, token) = lowering.edge_transition(name, args, &sheet);
581 motion = token.map(std::slice::from_ref).unwrap_or_default();
582 kind
583 }
584 other => {
585 lowering.error(
586 name.span(),
587 format!("`{other}` is not a transition; use fade, pop, grow, slide or move"),
588 );
589 continue;
590 }
591 };
592 let transition = TransitionDef {
593 name: decl.name.to_string(),
594 kind,
595 motion: lowering.motion(motion, &sheet),
596 };
597 match sheet
598 .transitions
599 .iter_mut()
600 .find(|existing| existing.name == transition.name)
601 {
602 Some(existing) => *existing = transition,
603 None => sheet.transitions.push(transition),
604 }
605 }
606 for decl in &item.patterns {
607 let StyleValue::Call { name, args } = &decl.value else {
608 lowering.error(
609 decl.value.span(),
610 "a pattern is `check(band(color, width, opacity), gap(width), …, twill: true)`",
611 );
612 continue;
613 };
614 if name != "check" {
615 lowering.error(name.span(), "a pattern is `check(band(…), gap(…), …)`");
616 continue;
617 }
618 let mut pattern = PatternDef {
619 name: decl.name.to_string(),
620 bands: Vec::new(),
621 twill: false,
622 };
623 for arg in args {
624 match (&arg.name, &arg.value) {
625 (Some(key), StyleValue::Name(path)) if key == "twill" => {
626 pattern.twill = path.len() == 1 && path[0] == "true";
627 }
628 (
629 None,
630 StyleValue::Call {
631 name: kind,
632 args: band,
633 },
634 ) if kind == "band" => {
635 let (Some(color), Some(width)) = (band.first(), band.get(1)) else {
636 lowering.error(kind.span(), "a band is `band(color, width, opacity)`");
637 continue;
638 };
639 let Some(color) = lowering.color(&color.value) else {
640 continue;
641 };
642 let Some(width) = lowering.length(&width.value, "a band's width") else {
643 continue;
644 };
645 let opacity = match band.get(2) {
646 Some(opacity) => {
647 match lowering.number(&opacity.value, "a band's opacity") {
648 Some(value) if !(0.0..=1.0).contains(&value) => {
649 lowering.error(
650 opacity.value.span(),
651 "a band's opacity is from 0 to 1",
652 );
653 continue;
654 }
655 value => value.unwrap_or(1.0),
656 }
657 }
658 None => 1.0,
659 };
660 pattern.bands.push(Band {
661 color: Some(color),
662 width,
663 opacity,
664 });
665 }
666 (
667 None,
668 StyleValue::Call {
669 name: kind,
670 args: gap,
671 },
672 ) if kind == "gap" => {
673 let Some(width) = gap.first() else {
674 lowering.error(kind.span(), "a gap is `gap(width)`");
675 continue;
676 };
677 let Some(width) = lowering.length(&width.value, "a gap's width") else {
678 continue;
679 };
680 pattern.bands.push(Band {
681 color: None,
682 width,
683 opacity: 0.0,
684 });
685 }
686 (_, other) => lowering.error(
687 other.span(),
688 "a pattern holds `band(…)`, `gap(…)` and `twill: true`",
689 ),
690 }
691 }
692 if !pattern
693 .bands
694 .iter()
695 .any(|band| band.color.is_some() && band.width > 0.0)
696 {
697 lowering.error(
698 name.span(),
699 "a pattern needs a colored band wider than zero",
700 );
701 continue;
702 }
703 match sheet
704 .patterns
705 .iter_mut()
706 .find(|existing| existing.name == pattern.name)
707 {
708 Some(existing) => *existing = pattern,
709 None => sheet.patterns.push(pattern),
710 }
711 }
712 for decl in &item.accents {
713 let color = match &decl.value {
714 StyleValue::Text(hex, span) => match Srgb::parse(hex) {
719 Some(color) => {
720 let readable = |mode| {
721 let theme = bern_theme::Srgb::new(color.red, color.green, color.blue);
722 let adjusted = bern_theme::native_accent(theme, mode);
723 Srgb {
724 red: adjusted.r,
725 green: adjusted.g,
726 blue: adjusted.b,
727 }
728 };
729 Some(ColorRef::Fixed {
730 light: readable(bern_theme::Mode::Light),
731 dark: readable(bern_theme::Mode::Dark),
732 })
733 }
734 None => {
735 lowering.error(*span, format!("`{hex}` is not a `#RRGGBB` color"));
736 None
737 }
738 },
739 value @ StyleValue::Call { .. } => lowering.color(value),
740 other => {
741 lowering.error(
742 other.span(),
743 "an accent is `\"#RRGGBB\"` or `color(light: \"#…\", dark: \"#…\")`",
744 );
745 None
746 }
747 };
748 let Some(color) = color else { continue };
749 if matches!(color, ColorRef::Role { .. }) {
750 lowering.error(decl.value.span(), "an accent is a color, not a role");
751 continue;
752 }
753 let accent = AccentDef {
754 name: decl.name.to_string(),
755 color,
756 };
757 match sheet
758 .accents
759 .iter_mut()
760 .find(|existing| existing.name == accent.name)
761 {
762 Some(existing) => *existing = accent,
763 None => sheet.accents.push(accent),
764 }
765 }
766 for decl in &item.text {
767 let style = decl.name.to_string();
768 if style == "control" {
769 lowering.error(
770 decl.name.span(),
771 "`control` is the platform's control font, as its buttons have it; it takes no size or weight of the application's",
772 );
773 continue;
774 }
775 if !bern_ir::spec::TEXT_STYLES.contains(&style.as_str()) {
776 lowering.error(
777 decl.name.span(),
778 format!(
779 "`{style}` is not a text style; text styles: {}",
780 bern_ir::spec::TEXT_STYLES.join(", ")
781 ),
782 );
783 continue;
784 }
785 let (weight, size, tv_size) = match &decl.value {
788 StyleValue::Name(path) if path.len() == 1 => (path[0].to_string(), None, None),
789 StyleValue::Call { name, args } if name == "font" => {
790 let mut weight = None;
791 let mut size = None;
792 let mut tv_size = None;
793 for arg in args {
794 match (
795 arg.name.as_ref().map(|name| name.to_string()).as_deref(),
796 &arg.value,
797 ) {
798 (Some("weight"), StyleValue::Name(path)) if path.len() == 1 => {
799 weight = Some(path[0].to_string())
800 }
801 (Some(name @ ("size" | "tv")), StyleValue::Number(value, span)) => {
802 if *value <= 0.0 {
803 lowering.error(*span, "a text size is more than 0 points");
804 } else if name == "tv" {
805 tv_size = Some(*value);
806 } else {
807 size = Some(*value);
808 }
809 }
810 _ => lowering.error(
811 arg.value.span(),
812 "`font` takes `size: points`, `tv: points` (the size on a television) and `weight: name`",
813 ),
814 }
815 }
816 (weight.unwrap_or_else(|| "regular".into()), size, tv_size)
817 }
818 other => {
819 lowering.error(
820 other.span(),
821 "a text style takes a weight, such as `heavy`, or `font(size: 30, weight: heavy)`",
822 );
823 continue;
824 }
825 };
826 if !bern_ir::style::TEXT_WEIGHTS.contains(&weight.as_str()) {
827 lowering.error(
828 decl.value.span(),
829 format!(
830 "`{weight}` is not a weight; weights: {}",
831 bern_ir::style::TEXT_WEIGHTS.join(", ")
832 ),
833 );
834 continue;
835 }
836 let entry = bern_ir::style::TextWeight {
837 style,
838 weight,
839 size,
840 tv_size,
841 };
842 match sheet
843 .text_weights
844 .iter_mut()
845 .find(|existing| existing.style == entry.style)
846 {
847 Some(existing) => *existing = entry,
848 None => sheet.text_weights.push(entry),
849 }
850 }
851 for rule in &item.rules {
852 let component = rule.component.to_string();
853 let Some((_, states)) = STYLEABLE.iter().find(|(name, _)| *name == component) else {
854 lowering.error(
855 rule.component.span(),
856 format!(
857 "`{component}` does not take style overrides yet; styleable: {}",
858 STYLEABLE
859 .iter()
860 .map(|(name, _)| *name)
861 .collect::<Vec<_>>()
862 .join(", ")
863 ),
864 );
865 continue;
866 };
867 let mut targets = Vec::new();
868 for target in &rule.targets {
869 let name = target.to_string();
870 if PLATFORMS.contains(&name.as_str()) {
871 targets.push(name);
872 } else {
873 lowering.error(
874 target.span(),
875 format!("`{name}` is not a platform; use {}", PLATFORMS.join(", ")),
876 );
877 }
878 }
879 let mut exempt = Vec::new();
880 for (target, _) in &rule.exempt {
881 let name = target.to_string();
882 if PLATFORMS.contains(&name.as_str()) {
883 exempt.push(name);
884 } else {
885 lowering.error(
886 target.span(),
887 format!("`{name}` is not a platform; use {}", PLATFORMS.join(", ")),
888 );
889 }
890 }
891 let mut lowered = StyleRule {
892 component: component.clone(),
893 variant: rule.variant.as_ref().map(ToString::to_string),
894 targets,
895 exempt,
896 base: Vec::new(),
897 states: Vec::new(),
898 settle: None,
899 source: lowering.location(rule.span),
900 };
901 let mut corner_names: Vec<String> = Vec::new();
903 for prop in &rule.props {
904 let name = prop.name.to_string();
905 if name == "settle" {
906 match prop.value.dotted() {
907 Some(token) if sheet.token(&token).is_some() => lowered.settle = Some(token),
908 _ => lowering.error(prop.value.span(), "`settle` names a motion token"),
909 }
910 continue;
911 }
912 if let Some(state) = StyleState::ALL.iter().find(|state| state.name() == name) {
913 if !states.contains(state) {
914 lowering.error(
915 prop.name.span(),
916 format!("`{component}` has no `{name}` state"),
917 );
918 continue;
919 }
920 let mut effects = Vec::new();
921 for term in prop.value.terms() {
922 let Some(effect) = lowering.effect(term, &sheet) else {
923 continue;
924 };
925 let allowed = STATE_EFFECTS.contains(&effect.name())
926 || (*state == StyleState::Selected
927 && SELECTED_EFFECTS.contains(&effect.name()));
928 if allowed {
929 effects.push(effect);
930 } else {
931 lowering.error(
932 term.span(),
933 format!("`{}` does not apply in the `{name}` state", effect.name()),
934 );
935 }
936 }
937 lowered.states.push(StateStyle {
938 state: *state,
939 effects,
940 });
941 continue;
942 }
943 let effect = match name.as_str() {
944 "corner" if prop.value.dotted().is_some() => {
948 let name = prop.value.dotted().unwrap_or_default();
949 match sheet.corner(&name, "") {
950 Some(radius) => {
951 corner_names.push(name);
952 Some(StyleEffect::Corner { radius })
953 }
954 None => {
955 let declared = sheet
956 .corners
957 .iter()
958 .filter(|corner| corner.targets.is_empty())
959 .map(|corner| corner.name.as_str());
960 lowering.error(prop.value.span(), unknown_corner(&name, declared));
961 None
962 }
963 }
964 }
965 "corner" => lowering
966 .length(&prop.value, "`corner`")
967 .map(|radius| StyleEffect::Corner { radius }),
968 "padding" => lowering
969 .length(&prop.value, "`padding`")
970 .map(|length| StyleEffect::Padding { length }),
971 "size" => match prop.value.dotted().as_deref() {
972 Some("small") => Some(StyleEffect::Size {
973 size: ControlSize::Small,
974 }),
975 Some("regular") => Some(StyleEffect::Size {
976 size: ControlSize::Regular,
977 }),
978 Some("large") => Some(StyleEffect::Size {
979 size: ControlSize::Large,
980 }),
981 Some("extra_large") => Some(StyleEffect::Size {
982 size: ControlSize::ExtraLarge,
983 }),
984 _ => {
985 lowering.error(
986 prop.value.span(),
987 "`size` is small, regular, large or extra_large",
988 );
989 None
990 }
991 },
992 "shadow" if matches!(&prop.value, StyleValue::Call { name, .. } if name == "shadow") => {
994 lowering.effect(&prop.value, &sheet)
995 }
996 "border" if matches!(&prop.value, StyleValue::Call { name, .. } if name == "border") => {
998 lowering.effect(&prop.value, &sheet)
999 }
1000 name if Metric::ALL.iter().any(|metric| metric.name() == name) => {
1001 let metric = *Metric::ALL
1002 .iter()
1003 .find(|metric| metric.name() == name)
1004 .unwrap();
1005 let fits = matches!(metric, Metric::Width | Metric::Height);
1006 if !writes(metric).contains(&component.as_str()) {
1007 lowering.error(
1008 prop.name.span(),
1009 format!(
1010 "`{component}` writes no `{name}`; a style sets it on {}",
1011 writes(metric).join(", ")
1012 ),
1013 );
1014 None
1015 } else if fits && prop.value.dotted().as_deref() == Some("fit") {
1016 Some(StyleEffect::Metric {
1017 metric,
1018 value: None,
1019 })
1020 } else if metric == Metric::FontSize {
1021 match lowering.number(&prop.value, "`font_size`") {
1022 Some(size) if size > 0.0 => Some(StyleEffect::Metric {
1023 metric,
1024 value: Some(size),
1025 }),
1026 Some(_) => {
1027 lowering.error(prop.value.span(), "`font_size` is more than 0");
1028 None
1029 }
1030 None => None,
1031 }
1032 } else if fits && !matches!(prop.value, StyleValue::Number(..)) {
1033 lowering.error(prop.value.span(), format!("`{name}` is a length or `fit`"));
1034 None
1035 } else {
1036 lowering
1037 .length(&prop.value, &format!("`{name}`"))
1038 .map(|length| StyleEffect::Metric {
1039 metric,
1040 value: Some(length),
1041 })
1042 }
1043 }
1044 "background" | "foreground" | "border" | "shadow" => {
1045 let call = StyleValue::Call {
1046 name: prop.name.clone(),
1047 args: vec![StyleArg {
1048 name: None,
1049 value: prop.value.clone(),
1050 }],
1051 };
1052 lowering.effect(&call, &sheet)
1053 }
1054 other => {
1055 lowering.error(
1056 prop.name.span(),
1057 format!(
1058 "`{other}` is not a style property; use {}, a state ({}) or `settle`",
1059 BASE_EFFECTS.join(", "),
1060 states
1061 .iter()
1062 .map(|state| state.name())
1063 .collect::<Vec<_>>()
1064 .join(", ")
1065 ),
1066 );
1067 None
1068 }
1069 };
1070 lowered.base.extend(effect);
1071 }
1072 let platforms: Vec<String> = if lowered.targets.is_empty() {
1073 PLATFORMS
1074 .iter()
1075 .map(|platform| (*platform).to_string())
1076 .collect()
1077 } else {
1078 lowered.targets.clone()
1079 };
1080 let mut overrides = Vec::new();
1081 for name in &corner_names {
1082 let everywhere = sheet.corner(name, "");
1083 for platform in &platforms {
1084 let Some(radius) = sheet.corner(name, platform) else {
1085 continue;
1086 };
1087 if Some(radius) == everywhere {
1088 continue;
1089 }
1090 overrides.push(StyleRule {
1091 component: lowered.component.clone(),
1092 variant: lowered.variant.clone(),
1093 targets: vec![platform.clone()],
1094 exempt: Vec::new(),
1095 base: vec![StyleEffect::Corner { radius }],
1096 states: Vec::new(),
1097 settle: None,
1098 source: lowered.source.clone(),
1099 });
1100 }
1101 }
1102 sheet.rules.push(lowered);
1103 sheet.rules.extend(overrides);
1104 }
1105 sheet
1106}
1107
1108fn lower_corners(lowering: &mut Lowering<'_>, item: &StyleItem, sheet: &mut StyleSheet) {
1112 let blocks = item
1113 .corners
1114 .iter()
1115 .filter(|block| block.targets.is_empty())
1116 .chain(
1117 item.corners
1118 .iter()
1119 .filter(|block| !block.targets.is_empty()),
1120 );
1121 for block in blocks {
1122 let mut targets = Vec::new();
1123 for target in &block.targets {
1124 let name = target.to_string();
1125 if PLATFORMS.contains(&name.as_str()) {
1126 targets.push(name);
1127 } else {
1128 lowering.error(
1129 target.span(),
1130 format!("`{name}` is not a platform; use {}", PLATFORMS.join(", ")),
1131 );
1132 }
1133 }
1134 if !block.targets.is_empty() && targets.is_empty() {
1135 continue;
1136 }
1137 let mut seen = Vec::new();
1138 for decl in &block.corners {
1139 let name = decl.name.to_string();
1140 if seen.contains(&name) {
1141 lowering.error(
1142 decl.name.span(),
1143 format!("corner `{name}` is named twice in this block"),
1144 );
1145 continue;
1146 }
1147 seen.push(name.clone());
1148 let everywhere = sheet
1149 .corners
1150 .iter()
1151 .any(|corner| corner.name == name && corner.targets.is_empty());
1152 if targets.is_empty() && everywhere {
1153 lowering.error(
1154 decl.name.span(),
1155 format!("corner `{name}` already has a radius for every platform"),
1156 );
1157 continue;
1158 }
1159 if !targets.is_empty() && !everywhere {
1160 lowering.error(
1161 decl.name.span(),
1162 format!(
1163 "corner `{name}` needs a radius for every platform first, in a `corner` block without `#[target(…)]`"
1164 ),
1165 );
1166 continue;
1167 }
1168 let Some(radius) = lowering.length(&decl.value, "a corner radius") else {
1169 continue;
1170 };
1171 sheet.corners.push(CornerDef {
1172 name,
1173 radius,
1174 targets: targets.clone(),
1175 });
1176 }
1177 }
1178}
1179
1180pub fn unknown_corner<'a>(name: &str, declared: impl Iterator<Item = &'a str>) -> String {
1183 let names: Vec<&str> = declared.collect();
1184 if names.is_empty() {
1185 format!(
1186 "corner `{name}` is not declared; name it in a `corner` block of `style!`, such as `corner {{ {name}: 12 }}`"
1187 )
1188 } else {
1189 format!(
1190 "corner `{name}` is not declared; `style!` names {}",
1191 names.join(", ")
1192 )
1193 }
1194}
1195
1196pub fn variants(sheet: &StyleSheet) -> Vec<(String, String)> {
1198 sheet
1199 .rules
1200 .iter()
1201 .filter_map(|rule| Some((rule.component.clone(), rule.variant.clone()?)))
1202 .collect()
1203}