use crate::names::pascal;
use bern_ir::style::{ColorRef, ControlSize, StyleEffect, StyleRule, StyleState};
use bern_ir::{Application, Element, Prop, Srgb};
use std::borrow::Cow;
pub fn brand_accent(app: &Application) -> Option<ColorRef> {
if app.windows.accent.is_some() {
return None;
}
let seed = app.theme.brand_seed?;
let readable = |mode| {
let color = bern_theme::Srgb::new(seed.red, seed.green, seed.blue);
let adjusted = bern_theme::native_accent(color, mode);
Srgb {
red: adjusted.r,
green: adjusted.g,
blue: adjusted.b,
}
};
Some(ColorRef::Fixed {
light: readable(bern_theme::Mode::Light),
dark: readable(bern_theme::Mode::Dark),
})
}
pub struct Resolved {
pub name: String,
pub base: Vec<StyleEffect>,
pub states: Vec<(StyleState, Vec<StyleEffect>)>,
pub settle: Option<String>,
}
impl Resolved {
pub fn draws_look(&self) -> bool {
let look = |effect: &StyleEffect| {
matches!(
effect,
StyleEffect::Background { .. }
| StyleEffect::Foreground { .. }
| StyleEffect::Border { .. }
| StyleEffect::Corner { .. }
| StyleEffect::Padding { .. }
)
};
self.base.iter().any(look)
|| self
.states
.iter()
.filter(|(state, _)| *state != StyleState::Selected)
.any(|(_, effects)| effects.iter().any(look))
}
pub fn size(&self) -> Option<ControlSize> {
self.base.iter().find_map(|effect| match effect {
StyleEffect::Size { size } => Some(*size),
_ => None,
})
}
}
pub fn popover_window_theme(app: &Application, platform: &str, element: &Element) -> bool {
match element.keyword("panel_appearance") {
Some("window") => true,
Some("surface") => resolve(app, platform, "PopoverButton", element.keyword("variant"))
.is_some_and(|style| {
style
.base
.iter()
.any(|effect| matches!(effect, StyleEffect::Background { .. }))
}),
_ => false,
}
}
fn applies(rule: &StyleRule, platform: &str) -> bool {
!rule.exempt.iter().any(|exempt| exempt == platform)
}
pub fn resolve(
app: &Application,
platform: &str,
component: &str,
variant: Option<&str>,
) -> Option<Resolved> {
let rules: Vec<&StyleRule> = app
.style
.rules_for(platform)
.filter(|rule| rule.component == component && applies(rule, platform))
.filter(|rule| rule.variant.is_none() || rule.variant.as_deref() == variant)
.collect();
if rules.is_empty() {
return None;
}
let mut resolved = Resolved {
name: {
let mut chars = component.chars();
let first: String = chars
.next()
.map(|c| c.to_lowercase().collect())
.unwrap_or_default();
let suffix = variant.map(pascal).unwrap_or_default();
format!("{first}{}{suffix}", chars.as_str())
},
base: Vec::new(),
states: Vec::new(),
settle: None,
};
let ordered = rules
.iter()
.filter(|rule| rule.variant.is_none())
.chain(rules.iter().filter(|rule| rule.variant.is_some()));
for rule in ordered {
for effect in &rule.base {
resolved
.base
.retain(|existing| existing.name() != effect.name());
resolved.base.push(effect.clone());
}
for state in &rule.states {
match resolved
.states
.iter_mut()
.find(|(existing, _)| *existing == state.state)
{
Some((_, effects)) => *effects = state.effects.clone(),
None => resolved.states.push((state.state, state.effects.clone())),
}
}
if rule.settle.is_some() {
resolved.settle = rule.settle.clone();
}
}
Some(resolved)
}
pub fn used(app: &Application, platform: &str) -> Vec<Resolved> {
let mut keys: Vec<(&str, Option<&str>)> = Vec::new();
app.walk(&mut |node, _| {
if let bern_ir::Node::Element(element) = node {
let key = (element.kind.as_str(), element.keyword("variant"));
if !keys.contains(&key) {
keys.push(key);
}
}
});
keys.into_iter()
.filter_map(|(kind, variant)| resolve(app, platform, kind, variant))
.collect()
}
fn restyle(app: &Application, platforms: &[&str], element: &mut Element) {
let Some(style) = platforms
.iter()
.find_map(|platform| resolve(app, platform, &element.kind, element.keyword("variant")))
else {
return;
};
for effect in &style.base {
let (name, value) = match effect {
StyleEffect::Metric { metric, value } => (metric.name(), *value),
StyleEffect::Padding { length }
if matches!(element.kind.as_str(), "Row" | "Column") =>
{
("padding", Some(*length))
}
_ => continue,
};
match value {
Some(value) => {
element.props.insert(name.into(), Prop::Number { value });
}
None => {
element.props.remove(name);
}
}
}
}
fn chip_as_capsule(element: &mut Element) {
if element.keyword("shape") == Some("chip") {
element.props.insert(
"shape".into(),
Prop::Keyword {
value: "capsule".into(),
},
);
element
.props
.entry("height".into())
.or_insert(Prop::Number { value: 30.0 });
}
}
pub fn for_platforms<'a>(app: &'a Application, platforms: &[&str]) -> Cow<'a, Application> {
let web = platforms.contains(&"browser");
let mut owned = app.clone();
let restyled = &mut |element: &mut Element| {
restyle(app, platforms, element);
if !web {
chip_as_capsule(element);
}
};
for component in &mut owned.components {
component.body.elements_mut(restyled);
}
for window in &mut owned.windows.windows {
window.content.elements_mut(restyled);
}
bern_ir::lower_lists(&mut owned);
if owned == *app {
Cow::Borrowed(app)
} else {
Cow::Owned(owned)
}
}
#[cfg(test)]
mod tests {
use super::brand_accent;
use bern_ir::style::ColorRef;
use bern_ir::{Expr, ExprKind, Srgb};
use bern_protocol::{Type, Value};
#[test]
fn the_brand_seed_is_the_accent_each_mode_reads() {
let mut app = crate::texts::tests::app(Vec::new());
assert_eq!(brand_accent(&app), None);
app.theme.brand_seed = Some(Srgb {
red: 0x33,
green: 0x66,
blue: 0xCC,
});
let Some(ColorRef::Fixed { light, dark }) = brand_accent(&app) else {
panic!("no accent");
};
assert_eq!((light.red, light.green, light.blue), (0x33, 0x66, 0xCC));
assert_eq!((dark.red, dark.green, dark.blue), (0x33, 0x66, 0xCC));
app.theme.brand_seed = Some(Srgb {
red: 0xFF,
green: 0xE6,
blue: 0x6D,
});
let Some(ColorRef::Fixed { light, .. }) = brand_accent(&app) else {
panic!("no accent");
};
let sum = |c: Srgb| u32::from(c.red) + u32::from(c.green) + u32::from(c.blue);
assert!(sum(light) < 0xFF + 0xE6 + 0x6D);
app.windows.accent = Some(Expr {
kind: ExprKind::Literal {
value: Value::Text("blue".into()),
},
ty: Type::Text,
});
assert_eq!(brand_accent(&app), None);
}
}