pub mod spec;
pub mod style;
use bern_protocol::{LocalizedText, ProtocolDescriptor, Type, TypeTable, Value};
mod corners;
mod television;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
pub const IR_VERSION: u32 = 8;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Application {
pub ir_version: u32,
pub app: AppInfo,
pub languages: Languages,
pub theme: Theme,
pub protocol: ProtocolDescriptor,
pub types: TypeTable,
pub sessions: Vec<SessionDef>,
pub windows: WindowCatalog,
pub players: Vec<PlayerDef>,
pub components: Vec<ComponentDef>,
pub texts: Vec<LocalizedText>,
pub resources: Vec<ResourceDef>,
pub layouts: Option<LayoutConfig>,
pub contracts: Vec<FeatureContract>,
#[serde(default)]
pub style: style::StyleSheet,
}
impl Application {
pub fn walk<'a>(&'a self, visit: &mut impl FnMut(&'a Node, Option<&'a str>)) {
for component in &self.components {
component.body.walk_in(None, visit);
}
for window in &self.windows.windows {
window.content.walk_in(None, visit);
}
}
pub fn component(&self, name: &str) -> Option<&ComponentDef> {
self.components
.iter()
.find(|component| component.name == name)
}
pub fn session(&self, name: &str) -> Option<&SessionDef> {
self.sessions.iter().find(|session| session.name == name)
}
pub fn window(&self, id: &str) -> Option<&WindowDef> {
self.windows.windows.iter().find(|window| window.id == id)
}
pub fn uses_media(&self) -> bool {
let mut uses = !self.players.is_empty();
let mut visit = |node: &Node| {
if let Node::Element(element) = node
&& element.kind == "Video"
{
uses = true;
}
};
for component in &self.components {
component.body.walk(&mut visit);
}
for window in &self.windows.windows {
window.content.walk(&mut visit);
}
uses
}
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct LayoutConfig {
pub compact_below: f64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub medium_below: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expanded_below: Option<f64>,
}
impl LayoutConfig {
pub fn size(&self, width: f64) -> LayoutSize {
if width < self.compact_below {
LayoutSize::Compact
} else if self.medium_below.is_some_and(|below| width < below) {
LayoutSize::Medium
} else if self.expanded_below.is_none_or(|below| width < below) {
LayoutSize::Expanded
} else {
LayoutSize::Large
}
}
pub fn has(&self, size: LayoutSize) -> bool {
match size {
LayoutSize::Compact | LayoutSize::Expanded => true,
LayoutSize::Medium => self.medium_below.is_some(),
LayoutSize::Large => self.expanded_below.is_some(),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum LayoutSize {
Compact,
Medium,
Expanded,
Large,
}
impl LayoutSize {
pub const ALL: [LayoutSize; 4] = [
LayoutSize::Compact,
LayoutSize::Medium,
LayoutSize::Expanded,
LayoutSize::Large,
];
pub const REQUIRED: [LayoutSize; 2] = [LayoutSize::Compact, LayoutSize::Expanded];
pub fn name(self) -> &'static str {
match self {
LayoutSize::Compact => "compact",
LayoutSize::Medium => "medium",
LayoutSize::Expanded => "expanded",
LayoutSize::Large => "large",
}
}
pub fn shown(self, declared: &[LayoutSize]) -> Option<LayoutSize> {
if declared.contains(&self) {
return Some(self);
}
let larger = Self::ALL.iter().copied().filter(|size| *size > self);
let smaller = Self::ALL.iter().rev().copied().filter(|size| *size < self);
larger.chain(smaller).find(|size| declared.contains(size))
}
pub fn parse(name: &str) -> Option<Self> {
Self::ALL.into_iter().find(|size| size.name() == name)
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct FeatureContract {
pub name: String,
pub component: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub doc: String,
pub features: Vec<FeatureDef>,
pub source: SourceLocation,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct FeatureDef {
pub name: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub doc: String,
pub commands: Vec<String>,
pub layouts: Vec<LayoutSize>,
pub guaranteed: Option<String>,
pub exemptions: Vec<Exemption>,
pub coverage: Vec<SizeCoverage>,
pub source: SourceLocation,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Exemption {
pub platform: String,
pub reason: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SizeCoverage {
pub size: LayoutSize,
pub coverage: Coverage,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum Coverage {
Covered {
entries: Vec<String>,
},
Conditional {
entries: Vec<String>,
},
Missing,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "result", rename_all = "snake_case")]
pub enum FeatureResult {
Covered {
entries: Vec<String>,
},
Guaranteed {
entries: Vec<String>,
reason: String,
},
Exempt {
reason: String,
},
Missing,
Unproven {
entries: Vec<String>,
},
}
impl FeatureResult {
pub fn is_error(&self) -> bool {
matches!(
self,
FeatureResult::Missing | FeatureResult::Unproven { .. }
)
}
}
impl FeatureDef {
pub fn results(&self, platform: &str) -> Vec<(LayoutSize, FeatureResult)> {
let exemption = self
.exemptions
.iter()
.find(|exemption| exemption.platform == platform);
self.coverage
.iter()
.map(|SizeCoverage { size, coverage }| {
let result = match (coverage, exemption, &self.guaranteed) {
(Coverage::Covered { entries }, _, _) => FeatureResult::Covered {
entries: entries.clone(),
},
(_, Some(exemption), _) => FeatureResult::Exempt {
reason: exemption.reason.clone(),
},
(Coverage::Conditional { entries }, None, Some(reason)) => {
FeatureResult::Guaranteed {
entries: entries.clone(),
reason: reason.clone(),
}
}
(Coverage::Conditional { entries }, None, None) => FeatureResult::Unproven {
entries: entries.clone(),
},
(Coverage::Missing, None, _) => FeatureResult::Missing,
};
(*size, result)
})
.collect()
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ResourceDef {
pub name: String,
pub declared: SourceLocation,
#[serde(default)]
pub picture: Option<SvgPicture>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SvgPicture {
Still,
Tinted,
Themed,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PlayerDef {
pub id: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub doc: String,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct AppInfo {
pub id: String,
pub bundle_id: String,
pub name: LocalizedText,
pub version: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub icon: Option<String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub platform_icons: BTreeMap<String, String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub alternate_icons: BTreeMap<String, BTreeMap<String, String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub publisher: Option<String>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub files_folder: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub background_audio: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub overlays: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Languages {
pub supported: Vec<String>,
pub default: String,
pub strict: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ThemeMode {
System,
Light,
Dark,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Srgb {
pub red: u8,
pub green: u8,
pub blue: u8,
}
impl Srgb {
pub fn parse(hex: &str) -> Option<Self> {
let hex = hex.strip_prefix('#')?;
if hex.len() != 6 || !hex.chars().all(|c| c.is_ascii_hexdigit()) {
return None;
}
let byte = |index: usize| u8::from_str_radix(&hex[index..index + 2], 16).ok();
Some(Self {
red: byte(0)?,
green: byte(2)?,
blue: byte(4)?,
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Theme {
pub mode: ThemeMode,
pub brand_seed: Option<Srgb>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct SessionFieldDef {
pub name: String,
#[serde(rename = "type")]
pub ty: Type,
pub initial: Value,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct SessionDef {
pub name: String,
pub fields: Vec<SessionFieldDef>,
}
impl SessionDef {
pub fn field_index(&self, name: &str) -> Option<usize> {
self.fields.iter().position(|field| field.name == name)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum WindowInitial {
Open,
Closed,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Presentation {
Auto,
Virtual,
Floating,
NativeRequired,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum WindowRole {
Standard,
Settings,
Dialog,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct WindowDef {
pub id: String,
pub title: LocalizedText,
pub initial: WindowInitial,
pub presentation: Presentation,
pub role: WindowRole,
pub width: Option<f64>,
pub height: Option<f64>,
pub min_width: Option<f64>,
pub min_height: Option<f64>,
pub content: Node,
pub sessions: Vec<String>,
pub close: Option<CloseCheck>,
#[serde(default)]
pub chrome: WindowChrome,
#[serde(default)]
pub immersive: Option<Expr>,
#[serde(default)]
pub idle_after: Option<f64>,
#[serde(default)]
pub idle_enabled: Option<Expr>,
#[serde(default)]
pub landscape: Option<Expr>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum WindowChrome {
#[default]
Standard,
Unified,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct CloseCheck {
pub when: Expr,
pub message: Text,
pub save: Option<Action>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct WindowCatalog {
pub handle: String,
pub primary: String,
pub windows: Vec<WindowDef>,
#[serde(default)]
pub accent: Option<Expr>,
#[serde(default)]
pub palette: Option<Expr>,
#[serde(default)]
pub appearance: Option<Expr>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub icon: Option<Expr>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub menu_bar: Option<Element>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub status_item: Option<Element>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct SourceLocation {
pub file: String,
pub line: u32,
pub column: u32,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ParamKind {
Client { namespace: String },
Windows,
Players,
Value {
#[serde(rename = "type")]
ty: Type,
},
Session { session: String },
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ParamDef {
pub name: String,
pub kind: ParamKind,
#[serde(default, skip_serializing_if = "is_false")]
pub content: bool,
}
fn is_false(value: &bool) -> bool {
!value
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ComponentDef {
pub name: String,
pub module: Vec<String>,
pub params: Vec<ParamDef>,
pub body: Node,
pub source: SourceLocation,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BinaryOp {
Eq,
Ne,
Lt,
Le,
Gt,
Ge,
And,
Or,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "scope", rename_all = "snake_case")]
pub enum Binding {
Engine { field: usize, path: Vec<usize> },
Session {
session: String,
field: usize,
path: Vec<usize>,
},
Local {
name: String,
path: Vec<usize>,
root: Type,
},
Window { field: String },
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ExprKind {
Literal {
value: Value,
},
Binding {
binding: Binding,
},
Variant {
variant: u32,
fields: Vec<Expr>,
},
Not {
expr: Box<Expr>,
},
Neg {
expr: Box<Expr>,
},
Binary {
op: BinaryOp,
lhs: Box<Expr>,
rhs: Box<Expr>,
},
If {
cond: Box<Expr>,
then: Box<Expr>,
otherwise: Box<Expr>,
},
Corner {
name: String,
radius: f64,
},
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Expr {
pub kind: ExprKind,
#[serde(rename = "type")]
pub ty: Type,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum Text {
Localized {
text: LocalizedText,
args: Vec<(String, Expr)>,
},
Dynamic { expr: Expr },
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct FieldRef {
pub session: String,
pub field: usize,
#[serde(rename = "type")]
pub ty: Type,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum Prop {
Keyword {
value: String,
},
Number {
value: f64,
},
Icon {
name: String,
},
Resource {
name: String,
},
Text {
text: Text,
},
Expr {
expr: Expr,
},
Field {
field: FieldRef,
},
Window {
id: String,
},
TypeName {
name: String,
},
Key {
field: usize,
name: String,
},
Id {
value: String,
},
Player {
id: String,
},
Query {
query: QueryBinding,
},
Feature {
contract: String,
feature: String,
},
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct QueryBinding {
pub query: usize,
pub args: Vec<Expr>,
pub result: Type,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum WindowOp {
ToggleFullscreen,
Open,
Close,
Activate,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MediaOp {
Open,
Play,
Pause,
Seek,
SetVolume,
SetMuted,
}
impl MediaOp {
pub const ALL: [MediaOp; 6] = [
MediaOp::Open,
MediaOp::Play,
MediaOp::Pause,
MediaOp::Seek,
MediaOp::SetVolume,
MediaOp::SetMuted,
];
pub fn method(self) -> &'static str {
match self {
MediaOp::Open => "open",
MediaOp::Play => "play",
MediaOp::Pause => "pause",
MediaOp::Seek => "seek",
MediaOp::SetVolume => "set_volume",
MediaOp::SetMuted => "set_muted",
}
}
pub fn command(self) -> &'static str {
match self {
MediaOp::Open => "media.open",
MediaOp::Play => "media.play",
MediaOp::Pause => "media.pause",
MediaOp::Seek => "media.seek",
MediaOp::SetVolume => "media.set_volume",
MediaOp::SetMuted => "media.set_muted",
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum Action {
Engine { command: usize, args: Vec<Expr> },
Window { op: WindowOp, window: String },
Media {
player: String,
op: MediaOp,
args: Vec<Expr>,
},
Session { field: FieldRef, op: SessionOp },
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum SessionOp {
Set { value: Expr },
Toggle,
PushStr { value: Expr },
Pop,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Template {
pub var: String,
#[serde(rename = "type")]
pub ty: Type,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Element {
pub kind: String,
pub id: Option<String>,
pub path: String,
pub props: BTreeMap<String, Prop>,
pub template: Option<Template>,
pub children: Vec<Node>,
pub action: Option<Action>,
pub source: SourceLocation,
}
impl Element {
pub fn prop(&self, name: &str) -> Option<&Prop> {
self.props.get(name)
}
pub fn context_menu(&self) -> Option<Vec<&Element>> {
self.children.iter().find_map(|node| match node {
Node::Element(menu) if menu.kind == "ContextMenu" => Some(
menu.children
.iter()
.filter_map(|node| match node {
Node::Element(item) => Some(item),
_ => None,
})
.collect(),
),
_ => None,
})
}
pub fn has_content(&self) -> bool {
self.children
.iter()
.any(|node| !matches!(node, Node::Element(menu) if menu.kind == "ContextMenu"))
}
pub fn keyword(&self, name: &str) -> Option<&str> {
match self.props.get(name) {
Some(Prop::Keyword { value }) => Some(value),
_ => None,
}
}
pub fn number(&self, name: &str) -> Option<f64> {
match self.props.get(name) {
Some(Prop::Number { value }) => Some(*value),
_ => None,
}
}
pub fn text(&self, name: &str) -> Option<&Text> {
match self.props.get(name) {
Some(Prop::Text { text }) => Some(text),
_ => None,
}
}
pub fn icon(&self, name: &str) -> Option<&str> {
match self.props.get(name) {
Some(Prop::Icon { name }) => Some(name),
_ => None,
}
}
pub fn resource(&self, name: &str) -> Option<&str> {
match self.props.get(name) {
Some(Prop::Resource { name }) => Some(name),
_ => None,
}
}
pub fn clearing(&self) -> Option<Action> {
let field = if self.keyword("submit") == Some("clear") {
self.field("value")?
} else {
self.field("clears")?
};
Some(Action::Session {
field: field.clone(),
op: SessionOp::Set {
value: Expr {
kind: ExprKind::Literal {
value: Value::Text(String::new()),
},
ty: Type::Text,
},
},
})
}
pub fn field(&self, name: &str) -> Option<&FieldRef> {
match self.props.get(name) {
Some(Prop::Field { field }) => Some(field),
_ => None,
}
}
pub fn expr(&self, name: &str) -> Option<&Expr> {
match self.props.get(name) {
Some(Prop::Expr { expr }) => Some(expr),
_ => None,
}
}
pub fn query(&self, name: &str) -> Option<&QueryBinding> {
match self.props.get(name) {
Some(Prop::Query { query }) => Some(query),
_ => None,
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ComponentArg {
Client,
Windows,
Players,
Value { expr: Expr },
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ComponentRef {
pub component: String,
pub args: Vec<(String, ComponentArg)>,
pub source: SourceLocation,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct IfNode {
pub branches: Vec<(Expr, Vec<Node>)>,
pub otherwise: Vec<Node>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct IfLetNode {
pub var: String,
pub value: Expr,
#[serde(rename = "inner_type")]
pub inner: Type,
pub then: Vec<Node>,
pub otherwise: Vec<Node>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct MatchBinding {
pub name: String,
pub field: usize,
#[serde(rename = "type")]
pub ty: Type,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct MatchArm {
pub variant: Option<u32>,
pub bindings: Vec<MatchBinding>,
pub body: Vec<Node>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct MatchNode {
pub value: Expr,
pub enum_name: String,
pub arms: Vec<MatchArm>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "node", rename_all = "snake_case")]
pub enum Node {
Element(Element),
Component(ComponentRef),
If(IfNode),
IfLet(IfLetNode),
Match(MatchNode),
}
impl Node {
pub fn walk<'a>(&'a self, visit: &mut impl FnMut(&'a Node)) {
self.walk_in(None, &mut |node, _| visit(node));
}
pub fn elements_mut(&mut self, visit: &mut impl FnMut(&mut Element)) {
let children: Vec<&mut Node> = match self {
Node::Element(element) => {
visit(element);
element.children.iter_mut().collect()
}
Node::Component(_) => Vec::new(),
Node::If(node) => node
.branches
.iter_mut()
.flat_map(|(_, body)| body.iter_mut())
.chain(node.otherwise.iter_mut())
.collect(),
Node::IfLet(node) => node
.then
.iter_mut()
.chain(node.otherwise.iter_mut())
.collect(),
Node::Match(node) => node
.arms
.iter_mut()
.flat_map(|arm| arm.body.iter_mut())
.collect(),
};
for child in children {
child.elements_mut(visit);
}
}
pub fn walk_in<'a>(
&'a self,
parent: Option<&'a str>,
visit: &mut impl FnMut(&'a Node, Option<&'a str>),
) {
visit(self, parent);
let parent = match self {
Node::Element(element) => Some(element.kind.as_str()),
_ => parent,
};
match self {
Node::Element(element) => {
for child in &element.children {
child.walk_in(parent, visit);
}
}
Node::Component(_) => {}
Node::If(node) => {
for (_, body) in &node.branches {
for child in body {
child.walk_in(parent, visit);
}
}
for child in &node.otherwise {
child.walk_in(parent, visit);
}
}
Node::IfLet(node) => {
for child in node.then.iter().chain(&node.otherwise) {
child.walk_in(parent, visit);
}
}
Node::Match(node) => {
for arm in &node.arms {
for child in &arm.body {
child.walk_in(parent, visit);
}
}
}
}
}
}
impl Expr {
pub fn walk<'a>(&'a self, visit: &mut impl FnMut(&'a Expr)) {
visit(self);
match &self.kind {
ExprKind::Literal { .. } | ExprKind::Binding { .. } | ExprKind::Corner { .. } => {}
ExprKind::Variant { fields, .. } => {
for field in fields {
field.walk(visit);
}
}
ExprKind::Not { expr } | ExprKind::Neg { expr } => expr.walk(visit),
ExprKind::Binary { lhs, rhs, .. } => {
lhs.walk(visit);
rhs.walk(visit);
}
ExprKind::If {
cond,
then,
otherwise,
} => {
cond.walk(visit);
then.walk(visit);
otherwise.walk(visit);
}
}
}
pub fn sessions(&self) -> Vec<String> {
let mut sessions = Vec::new();
self.walk(&mut |expr| {
if let ExprKind::Binding {
binding: Binding::Session { session, .. },
} = &expr.kind
&& !sessions.contains(session)
{
sessions.push(session.clone());
}
});
sessions
}
pub fn reads_engine(&self) -> bool {
let mut reads = false;
self.walk(&mut |expr| {
if let ExprKind::Binding {
binding: Binding::Engine { .. },
} = &expr.kind
{
reads = true;
}
});
reads
}
}
impl Text {
pub fn exprs(&self) -> Vec<&Expr> {
match self {
Text::Localized { args, .. } => args.iter().map(|(_, expr)| expr).collect(),
Text::Dynamic { expr } => vec![expr],
}
}
}
impl CloseCheck {
pub fn exprs(&self) -> Vec<&Expr> {
let mut exprs = vec![&self.when];
exprs.extend(self.message.exprs());
if let Some(Action::Engine { args, .. }) = &self.save {
exprs.extend(args);
}
exprs
}
}
pub use bern_protocol::label_key;
pub const LIBRARY_DESCRIPTOR_VERSION: u32 = 1;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ComponentLibrary {
pub descriptor_version: u32,
pub ir_version: u32,
pub package: String,
pub version: String,
pub source_fingerprint: String,
pub languages: Vec<String>,
pub types: TypeTable,
pub components: Vec<ComponentDef>,
pub texts: Vec<LocalizedText>,
}
pub fn leads_row(node: &Node, next: Option<&Node>) -> bool {
let words = matches!(node, Node::Element(element)
if element.prop("width").is_none()
&& (element.kind == "Text"
|| (element.kind == "Column"
&& matches!(element.keyword("align"), None | Some("leading")))));
words && matches!(next, Some(Node::Element(spacer)) if spacer.kind == "Spacer")
}
pub fn lazy_grid_items(grid: &Element) -> Option<&Element> {
if grid.kind != "Grid" || grid.keyword("masonry") == Some("true") {
return None;
}
match grid.children.as_slice() {
[Node::Element(each)]
if each.kind == "ForEach"
&& each.template.is_some()
&& each.expr("items").is_some()
&& matches!(each.prop("key"), Some(Prop::Key { .. })) =>
{
Some(each)
}
_ => None,
}
}
pub fn lowered_list_align(grid: &Element) -> Option<&str> {
(grid.kind == "Grid")
.then(|| grid.keyword("align"))
.flatten()
}
pub fn holds_lowered_list(nodes: &[Node]) -> bool {
nodes.iter().any(|node| match node {
Node::Element(element) => lowered_list_align(element).is_some(),
Node::If(node) => {
node.branches
.iter()
.any(|(_, body)| holds_lowered_list(body))
|| holds_lowered_list(&node.otherwise)
}
Node::IfLet(node) => holds_lowered_list(&node.then) || holds_lowered_list(&node.otherwise),
Node::Match(node) => node.arms.iter().any(|arm| holds_lowered_list(&arm.body)),
Node::Component(_) => false,
})
}
pub fn lower_lists(app: &mut Application) {
let mut windows: Vec<&mut Node> = app
.windows
.windows
.iter_mut()
.map(|window| &mut window.content)
.collect();
let mut components: Vec<(&str, &mut Node)> = app
.components
.iter_mut()
.map(|component| (component.name.as_str(), &mut component.body))
.collect();
lower_bodies(&mut windows, &mut components);
}
fn lower_bodies(windows: &mut [&mut Node], components: &mut [(&str, &mut Node)]) {
let mut scrolled: BTreeMap<String, bool> = components
.iter()
.map(|(name, _)| (name.to_string(), true))
.collect();
loop {
let mut calls: BTreeMap<String, bool> = BTreeMap::new();
let mut note = |name: &str, inside: bool| {
let entry = calls.entry(name.to_string()).or_insert(true);
*entry = *entry && inside;
};
for window in windows.iter() {
calls_in(window, false, &mut note);
}
for (name, body) in components.iter() {
calls_in(body, scrolled[*name], &mut note);
}
let next: BTreeMap<String, bool> = scrolled
.keys()
.map(|name| (name.clone(), calls.get(name).copied().unwrap_or(false)))
.collect();
if next == scrolled {
break;
}
scrolled = next;
}
for window in windows.iter_mut() {
lower_in(window, false);
}
for (name, body) in components.iter_mut() {
lower_in(body, scrolled[*name]);
}
}
fn scrolls_inside(element: &Element, scrolling: bool) -> bool {
match element.kind.as_str() {
"ScrollView" => element.keyword("axis") != Some("horizontal"),
"Modal" | "PopoverButton" | "Menu" | "ContextMenu" | "Toolbar" | "SplitView"
| "Sidebar" | "Detail" | "Tabs" | "Tab" | "TabAccessory" | "Navigation" | "Destination"
| "Window" | "WindowSet" | "Form" | "Table" => false,
_ => scrolling,
}
}
fn calls_in(node: &Node, scrolling: bool, note: &mut impl FnMut(&str, bool)) {
match node {
Node::Element(element) => {
let inside = scrolls_inside(element, scrolling);
for child in &element.children {
calls_in(child, inside, note);
}
}
Node::Component(call) => note(&call.component, scrolling),
Node::If(node) => {
for (_, body) in &node.branches {
body.iter()
.for_each(|child| calls_in(child, scrolling, note));
}
node.otherwise
.iter()
.for_each(|child| calls_in(child, scrolling, note));
}
Node::IfLet(node) => {
node.then
.iter()
.chain(&node.otherwise)
.for_each(|child| calls_in(child, scrolling, note));
}
Node::Match(node) => {
for arm in &node.arms {
arm.body
.iter()
.for_each(|child| calls_in(child, scrolling, note));
}
}
}
}
fn lower_in(node: &mut Node, scrolling: bool) {
match node {
Node::Element(element) => {
let inside = scrolls_inside(element, scrolling);
for child in &mut element.children {
lower_in(child, inside);
}
if inside && element.kind == "Column" && element.prop("reflow").is_none() {
let look = ListLook {
gap: element.prop("gap").cloned(),
align: element.keyword("align").unwrap_or("leading").to_string(),
stagger: element.prop("stagger").cloned(),
path: element.path.clone(),
};
let mut count = 0;
lower_rows(&mut element.children, &look, &mut count);
}
}
Node::Component(_) => {}
Node::If(node) => {
for (_, body) in &mut node.branches {
body.iter_mut().for_each(|child| lower_in(child, scrolling));
}
node.otherwise
.iter_mut()
.for_each(|child| lower_in(child, scrolling));
}
Node::IfLet(node) => {
node.then
.iter_mut()
.chain(&mut node.otherwise)
.for_each(|child| lower_in(child, scrolling));
}
Node::Match(node) => {
for arm in &mut node.arms {
arm.body
.iter_mut()
.for_each(|child| lower_in(child, scrolling));
}
}
}
}
struct ListLook {
gap: Option<Prop>,
align: String,
stagger: Option<Prop>,
path: String,
}
fn lower_rows(children: &mut [Node], look: &ListLook, count: &mut usize) {
for child in children.iter_mut() {
match child {
Node::Element(each)
if each.kind == "ForEach"
&& each.template.is_some()
&& each.expr("items").is_some()
&& matches!(each.prop("key"), Some(Prop::Key { .. })) =>
{
let mut props = BTreeMap::new();
props.insert("min_width".to_string(), Prop::Number { value: 1.0e6 });
props.insert("gap".to_string(), Prop::Number { value: 0.0 });
if let Some(gap) = &look.gap {
props.insert("row_gap".to_string(), gap.clone());
}
props.insert(
"align".to_string(),
Prop::Keyword {
value: look.align.clone(),
},
);
if let Some(stagger) = &look.stagger {
props.insert("stagger".to_string(), stagger.clone());
}
let source = each.source.clone();
let path = format!("{}/list{}", look.path, count);
*count += 1;
let mut each = std::mem::replace(
child,
Node::Component(ComponentRef {
component: String::new(),
args: Vec::new(),
source: source.clone(),
}),
);
if let Node::Element(each) = &mut each {
stack_parts(each, look, &path);
}
*child = Node::Element(Element {
kind: "Grid".into(),
id: None,
path,
props,
template: None,
children: vec![each],
action: None,
source,
});
}
Node::If(node) => {
for (_, body) in &mut node.branches {
lower_rows(body, look, count);
}
lower_rows(&mut node.otherwise, look, count);
}
Node::IfLet(node) => {
lower_rows(&mut node.then, look, count);
lower_rows(&mut node.otherwise, look, count);
}
Node::Match(node) => {
for arm in &mut node.arms {
lower_rows(&mut arm.body, look, count);
}
}
_ => {}
}
}
}
fn stack_parts(each: &mut Element, look: &ListLook, path: &str) {
if each.children.len() < 2 {
return;
}
let mut props = BTreeMap::new();
if let Some(gap) = &look.gap {
props.insert("gap".to_string(), gap.clone());
}
props.insert(
"align".to_string(),
Prop::Keyword {
value: look.align.clone(),
},
);
each.children = vec![Node::Element(Element {
kind: "Column".into(),
id: None,
path: format!("{path}/row"),
props,
template: None,
children: std::mem::take(&mut each.children),
action: None,
source: each.source.clone(),
})];
}
pub fn text_key(component: &str, node_path: &str, prop: &str) -> String {
format!("{component}.{node_path}.{prop}")
}
#[cfg(test)]
mod tests {
use super::*;
fn element(kind: &str, children: Vec<Node>) -> Node {
Node::Element(Element {
kind: kind.into(),
id: None,
path: kind.into(),
props: BTreeMap::new(),
template: None,
children,
action: None,
source: SourceLocation::default(),
})
}
fn with_props(node: Node, props: &[(&str, Prop)]) -> Node {
let Node::Element(mut element) = node else {
unreachable!()
};
for (name, prop) in props {
element.props.insert(name.to_string(), prop.clone());
}
Node::Element(element)
}
fn keyed_rows() -> Node {
let Node::Element(mut each) = element("ForEach", vec![element("Text", vec![])]) else {
unreachable!()
};
each.template = Some(Template {
var: "row".into(),
ty: Type::Int,
});
each.props.insert(
"items".into(),
Prop::Expr {
expr: Expr {
kind: ExprKind::Literal {
value: Value::Int(0),
},
ty: Type::Int,
},
},
);
each.props.insert(
"key".into(),
Prop::Key {
field: 0,
name: "id".into(),
},
);
Node::Element(each)
}
fn kinds(node: &Node) -> Vec<String> {
let Node::Element(element) = node else {
return vec![];
};
element
.children
.iter()
.map(|child| match child {
Node::Element(child) => child.kind.clone(),
Node::If(_) => "if".into(),
_ => "other".into(),
})
.collect()
}
#[test]
fn a_list_in_a_scroll_view_becomes_a_lazy_column_of_its_rows() {
let column = with_props(
element(
"Column",
vec![
element("Text", vec![]),
keyed_rows(),
element("Spacer", vec![]),
],
),
&[
("gap", Prop::Number { value: 4.0 }),
(
"align",
Prop::Keyword {
value: "center".into(),
},
),
],
);
let mut tree = element("ScrollView", vec![column]);
lower_bodies(&mut [&mut tree], &mut []);
let Node::Element(scroll) = &tree else {
unreachable!()
};
assert_eq!(kinds(&scroll.children[0]), ["Text", "Grid", "Spacer"]);
let Node::Element(column) = &scroll.children[0] else {
unreachable!()
};
let Node::Element(grid) = &column.children[1] else {
unreachable!()
};
assert_eq!(lowered_list_align(grid), Some("center"));
assert_eq!(grid.number("row_gap"), Some(4.0));
assert!(
lazy_grid_items(grid).is_some(),
"it is a grid every target builds lazily"
);
}
#[test]
fn a_row_of_several_parts_keeps_them_one_under_another() {
let Node::Element(mut each) = keyed_rows() else {
unreachable!()
};
each.children = vec![
element("Row", vec![]),
element("Column", vec![element("Divider", vec![])]),
keyed_rows(),
];
let parts = each.children.clone();
let column = with_props(
element("Column", vec![Node::Element(each), keyed_rows()]),
&[
("gap", Prop::Number { value: 4.0 }),
(
"align",
Prop::Keyword {
value: "center".into(),
},
),
],
);
let mut tree = element("ScrollView", vec![column]);
lower_bodies(&mut [&mut tree], &mut []);
let rows_of = |at: usize| {
let Node::Element(scroll) = &tree else {
unreachable!()
};
let Node::Element(column) = &scroll.children[0] else {
unreachable!()
};
let Node::Element(grid) = &column.children[at] else {
unreachable!()
};
lazy_grid_items(grid).unwrap().clone()
};
let several = rows_of(0);
assert_eq!(kinds(&Node::Element(several.clone())), ["Column"]);
let Node::Element(stack) = &several.children[0] else {
unreachable!()
};
assert_eq!(stack.children, parts, "the parts stay as they were");
assert_eq!(stack.number("gap"), Some(4.0));
assert_eq!(stack.keyword("align"), Some("center"));
assert_eq!(stack.path, "Column/list0/row");
assert_eq!(kinds(&Node::Element(rows_of(1))), ["Text"]);
}
#[test]
fn lists_outside_scroll_views_and_reflowing_lists_stay_as_they_are() {
let mut outside = element("Column", vec![keyed_rows()]);
let mut sideways = with_props(
element("ScrollView", vec![element("Column", vec![keyed_rows()])]),
&[(
"axis",
Prop::Keyword {
value: "horizontal".into(),
},
)],
);
let mut reflowing = element(
"ScrollView",
vec![with_props(
element("Column", vec![keyed_rows()]),
&[(
"reflow",
Prop::Keyword {
value: "smooth".into(),
},
)],
)],
);
let before = (outside.clone(), sideways.clone(), reflowing.clone());
lower_bodies(&mut [&mut outside, &mut sideways, &mut reflowing], &mut []);
assert_eq!((outside, sideways, reflowing), before);
}
#[test]
fn a_list_in_a_condition_and_in_a_component_shown_in_a_scroll_view_is_lowered() {
let condition = Expr {
kind: ExprKind::Literal {
value: Value::Bool(true),
},
ty: Type::Bool,
};
let call = |name: &str| {
Node::Component(ComponentRef {
component: name.into(),
args: vec![],
source: SourceLocation::default(),
})
};
let mut window = element("ScrollView", vec![call("songs")]);
let mut songs = element(
"Column",
vec![Node::If(IfNode {
branches: vec![(condition, vec![keyed_rows()])],
otherwise: vec![],
})],
);
let mut window_two = element(
"Column",
vec![call("tags"), element("ScrollView", vec![call("tags")])],
);
let mut tags = element("Column", vec![keyed_rows()]);
let tags_before = tags.clone();
lower_bodies(
&mut [&mut window, &mut window_two],
&mut [("songs", &mut songs), ("tags", &mut tags)],
);
let Node::Element(column) = &songs else {
unreachable!()
};
let Node::If(branch) = &column.children[0] else {
unreachable!()
};
assert!(matches!(&branch.branches[0].1[0], Node::Element(grid) if grid.kind == "Grid"));
assert_eq!(tags, tags_before);
}
#[test]
fn walks_report_the_nearest_element_around_each_node() {
let condition = Expr {
kind: ExprKind::Literal {
value: Value::Bool(true),
},
ty: Type::Bool,
};
let tree = element(
"Sidebar",
vec![Node::If(IfNode {
branches: vec![(condition, vec![element("Search", vec![])])],
otherwise: vec![element("Column", vec![element("Text", vec![])])],
})],
);
let mut seen = Vec::new();
tree.walk_in(None, &mut |node, parent| {
let name = match node {
Node::Element(element) => element.kind.as_str(),
Node::If(_) => "if",
_ => "other",
};
seen.push((name, parent));
});
assert_eq!(
seen,
[
("Sidebar", None),
("if", Some("Sidebar")),
("Search", Some("Sidebar")),
("Column", Some("Sidebar")),
("Text", Some("Column")),
]
);
let mut count = 0;
tree.walk(&mut |_| count += 1);
assert_eq!(count, seen.len());
}
}