1use crate::names::pascal;
5use bern_ir::style::{ColorRef, ControlSize, StyleEffect, StyleRule, StyleState};
6use bern_ir::{Application, Element, Prop, Srgb};
7use std::borrow::Cow;
8
9pub fn brand_accent(app: &Application) -> Option<ColorRef> {
15 if app.windows.accent.is_some() {
16 return None;
17 }
18 let seed = app.theme.brand_seed?;
19 let readable = |mode| {
20 let color = bern_theme::Srgb::new(seed.red, seed.green, seed.blue);
21 let adjusted = bern_theme::native_accent(color, mode);
22 Srgb {
23 red: adjusted.r,
24 green: adjusted.g,
25 blue: adjusted.b,
26 }
27 };
28 Some(ColorRef::Fixed {
29 light: readable(bern_theme::Mode::Light),
30 dark: readable(bern_theme::Mode::Dark),
31 })
32}
33
34pub struct Resolved {
37 pub name: String,
39 pub base: Vec<StyleEffect>,
40 pub states: Vec<(StyleState, Vec<StyleEffect>)>,
41 pub settle: Option<String>,
42}
43
44impl Resolved {
45 pub fn draws_look(&self) -> bool {
47 let look = |effect: &StyleEffect| {
48 matches!(
49 effect,
50 StyleEffect::Background { .. }
51 | StyleEffect::Foreground { .. }
52 | StyleEffect::Border { .. }
53 | StyleEffect::Corner { .. }
54 | StyleEffect::Padding { .. }
55 )
56 };
57 self.base.iter().any(look)
58 || self
59 .states
60 .iter()
61 .filter(|(state, _)| *state != StyleState::Selected)
62 .any(|(_, effects)| effects.iter().any(look))
63 }
64
65 pub fn size(&self) -> Option<ControlSize> {
66 self.base.iter().find_map(|effect| match effect {
67 StyleEffect::Size { size } => Some(*size),
68 _ => None,
69 })
70 }
71}
72
73pub fn popover_window_theme(app: &Application, platform: &str, element: &Element) -> bool {
76 match element.keyword("panel_appearance") {
77 Some("window") => true,
78 Some("surface") => resolve(app, platform, "PopoverButton", element.keyword("variant"))
79 .is_some_and(|style| {
80 style
81 .base
82 .iter()
83 .any(|effect| matches!(effect, StyleEffect::Background { .. }))
84 }),
85 _ => false,
86 }
87}
88
89fn applies(rule: &StyleRule, platform: &str) -> bool {
90 !rule.exempt.iter().any(|exempt| exempt == platform)
91}
92
93pub fn resolve(
96 app: &Application,
97 platform: &str,
98 component: &str,
99 variant: Option<&str>,
100) -> Option<Resolved> {
101 let rules: Vec<&StyleRule> = app
102 .style
103 .rules_for(platform)
104 .filter(|rule| rule.component == component && applies(rule, platform))
105 .filter(|rule| rule.variant.is_none() || rule.variant.as_deref() == variant)
106 .collect();
107 if rules.is_empty() {
108 return None;
109 }
110 let mut resolved = Resolved {
111 name: {
113 let mut chars = component.chars();
114 let first: String = chars
115 .next()
116 .map(|c| c.to_lowercase().collect())
117 .unwrap_or_default();
118 let suffix = variant.map(pascal).unwrap_or_default();
119 format!("{first}{}{suffix}", chars.as_str())
120 },
121 base: Vec::new(),
122 states: Vec::new(),
123 settle: None,
124 };
125 let ordered = rules
127 .iter()
128 .filter(|rule| rule.variant.is_none())
129 .chain(rules.iter().filter(|rule| rule.variant.is_some()));
130 for rule in ordered {
131 for effect in &rule.base {
132 resolved
133 .base
134 .retain(|existing| existing.name() != effect.name());
135 resolved.base.push(effect.clone());
136 }
137 for state in &rule.states {
138 match resolved
139 .states
140 .iter_mut()
141 .find(|(existing, _)| *existing == state.state)
142 {
143 Some((_, effects)) => *effects = state.effects.clone(),
144 None => resolved.states.push((state.state, state.effects.clone())),
145 }
146 }
147 if rule.settle.is_some() {
148 resolved.settle = rule.settle.clone();
149 }
150 }
151 Some(resolved)
152}
153
154pub fn used(app: &Application, platform: &str) -> Vec<Resolved> {
158 let mut keys: Vec<(&str, Option<&str>)> = Vec::new();
159 app.walk(&mut |node, _| {
160 if let bern_ir::Node::Element(element) = node {
161 let key = (element.kind.as_str(), element.keyword("variant"));
162 if !keys.contains(&key) {
163 keys.push(key);
164 }
165 }
166 });
167 keys.into_iter()
168 .filter_map(|(kind, variant)| resolve(app, platform, kind, variant))
169 .collect()
170}
171
172fn restyle(app: &Application, platforms: &[&str], element: &mut Element) {
177 let Some(style) = platforms
178 .iter()
179 .find_map(|platform| resolve(app, platform, &element.kind, element.keyword("variant")))
180 else {
181 return;
182 };
183 for effect in &style.base {
184 let (name, value) = match effect {
185 StyleEffect::Metric { metric, value } => (metric.name(), *value),
186 StyleEffect::Padding { length }
188 if matches!(element.kind.as_str(), "Row" | "Column") =>
189 {
190 ("padding", Some(*length))
191 }
192 _ => continue,
193 };
194 match value {
195 Some(value) => {
196 element.props.insert(name.into(), Prop::Number { value });
197 }
198 None => {
199 element.props.remove(name);
200 }
201 }
202 }
203}
204
205fn chip_as_capsule(element: &mut Element) {
208 if element.keyword("shape") == Some("chip") {
209 element.props.insert(
210 "shape".into(),
211 Prop::Keyword {
212 value: "capsule".into(),
213 },
214 );
215 element
216 .props
217 .entry("height".into())
218 .or_insert(Prop::Number { value: 30.0 });
219 }
220}
221
222pub fn for_platforms<'a>(app: &'a Application, platforms: &[&str]) -> Cow<'a, Application> {
227 let web = platforms.contains(&"browser");
228 let mut owned = app.clone();
229 let restyled = &mut |element: &mut Element| {
230 restyle(app, platforms, element);
231 if !web {
232 chip_as_capsule(element);
233 }
234 };
235 for component in &mut owned.components {
236 component.body.elements_mut(restyled);
237 }
238 for window in &mut owned.windows.windows {
239 window.content.elements_mut(restyled);
240 }
241 bern_ir::lower_lists(&mut owned);
244 if owned == *app {
245 Cow::Borrowed(app)
246 } else {
247 Cow::Owned(owned)
248 }
249}
250
251#[cfg(test)]
252mod tests {
253 use super::brand_accent;
254 use bern_ir::style::ColorRef;
255 use bern_ir::{Expr, ExprKind, Srgb};
256 use bern_protocol::{Type, Value};
257
258 #[test]
259 fn the_brand_seed_is_the_accent_each_mode_reads() {
260 let mut app = crate::texts::tests::app(Vec::new());
261 assert_eq!(brand_accent(&app), None);
262 app.theme.brand_seed = Some(Srgb {
265 red: 0x33,
266 green: 0x66,
267 blue: 0xCC,
268 });
269 let Some(ColorRef::Fixed { light, dark }) = brand_accent(&app) else {
270 panic!("no accent");
271 };
272 assert_eq!((light.red, light.green, light.blue), (0x33, 0x66, 0xCC));
273 assert_eq!((dark.red, dark.green, dark.blue), (0x33, 0x66, 0xCC));
274 app.theme.brand_seed = Some(Srgb {
275 red: 0xFF,
276 green: 0xE6,
277 blue: 0x6D,
278 });
279 let Some(ColorRef::Fixed { light, .. }) = brand_accent(&app) else {
280 panic!("no accent");
281 };
282 let sum = |c: Srgb| u32::from(c.red) + u32::from(c.green) + u32::from(c.blue);
283 assert!(sum(light) < 0xFF + 0xE6 + 0x6D);
284 app.windows.accent = Some(Expr {
286 kind: ExprKind::Literal {
287 value: Value::Text("blue".into()),
288 },
289 ty: Type::Text,
290 });
291 assert_eq!(brand_accent(&app), None);
292 }
293}