blob: 1d649f21e9df2f6f05907c105ac32bc5dbbfec1f [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
use maka_ui_runtime::{ComponentSlot, NativeView};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use std::collections::{BTreeMap, BTreeSet};
use std::rc::Rc;
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum Direction {
Row,
Column,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
#[schemars(rename = "UiNode")]
pub enum Node {
Stack {
key: String,
direction: Direction,
gap: u16,
padding: u16,
background: u32,
children: Vec<Node>,
},
Text {
key: String,
text: String,
color: u32,
},
Button {
key: String,
label: String,
},
Component(ComponentSlot),
}
impl Node {
pub(crate) fn validate_root(&self) -> Result<(), String> {
self.validate(0, &mut BTreeSet::new())
}
fn validate(&self, depth: usize, keys: &mut BTreeSet<String>) -> Result<(), String> {
let key = match self {
Self::Stack { key, .. } | Self::Text { key, .. } | Self::Button { key, .. } => key,
Self::Component(slot) => &slot.key,
};
if depth > 32
|| keys.len() >= 4096
|| key.is_empty()
|| key.len() > 128
|| !keys.insert(key.clone())
{
return Err("invalid or duplicate UI node identity".into());
}
match self {
Self::Stack {
gap,
padding,
background,
children,
..
} => {
if *gap > 1024 || *padding > 1024 || *background > 0xffffff {
return Err("invalid UI style".into());
}
for child in children {
child.validate(depth + 1, keys)?;
}
}
Self::Text { text, color, .. } => {
if text.len() > 64 * 1024 || *color > 0xffffff {
return Err("invalid text".into());
}
}
Self::Button { label, .. } => {
if label.len() > 4096 {
return Err("invalid button label".into());
}
}
Self::Component(slot) => {
if slot.binding.is_empty() || slot.binding.len() > 128 {
return Err("invalid slot binding".into());
}
}
}
Ok(())
}
fn metadata(
&self,
slots: &mut Vec<ComponentSlot>,
inputs: &mut BTreeMap<String, Value>,
) -> Result<(), String> {
match self {
Self::Component(slot) => {
slots.push(slot.clone());
}
Self::Button { key, .. } => {
inputs.insert(key.clone(), json!(true));
}
Self::Stack { children, .. } => {
for child in children {
child.metadata(slots, inputs)?;
}
}
_ => {}
}
Ok(())
}
}
pub(crate) fn compile<V: Default + 'static>(
source: &Value,
) -> maka_ui_runtime::Result<NativeView<V>> {
prepare(source)?.finish::<Node, V>(|_| Rc::new(V::default()))
}
pub(crate) fn finish<V: Default + 'static>(
prepared: maka_ui_runtime::PreparedScene,
) -> maka_ui_runtime::Result<NativeView<V>> {
prepared.finish::<Node, V>(|_| Rc::new(V::default()))
}
pub(crate) fn prepare(source: &Value) -> maka_ui_runtime::Result<maka_ui_runtime::PreparedScene> {
fn scan(source: &Value, depth: usize) -> maka_ui_runtime::Result<()> {
if depth > 32 {
return Err(maka_ui_runtime::Error::Invalid(
"scene source depth limit".into(),
));
}
if let Some(children) = source.get("children").and_then(Value::as_array) {
for child in children {
scan(child, depth + 1)?;
}
}
Ok(())
}
scan(source, 0)?;
let node: Node = serde_json::from_value(source.clone())
.map_err(|e| maka_ui_runtime::Error::Invalid(e.to_string()))?;
node.validate_root()
.map_err(maka_ui_runtime::Error::Invalid)?;
let mut slots = vec![];
let mut inputs = BTreeMap::new();
node.metadata(&mut slots, &mut inputs)
.map_err(maka_ui_runtime::Error::Invalid)?;
maka_ui_runtime::PreparedScene::new(node, inputs, slots)
}
pub(crate) fn view<V: Default + 'static>(node: Node) -> maka_ui_runtime::Result<NativeView<V>> {
compile(&serde_json::to_value(node).unwrap())
}
pub(crate) fn registry<V: Default + 'static>(
target: maka_plugin_core::composition::PresentationTarget,
) -> maka_ui_runtime::NativeRegistry<V> {
maka_ui_runtime::NativeRegistry::with_surface(target)
.with_scene_compiler(language(target), prepare, |prepared| {
prepared.finish::<Node, V>(|_| Rc::new(V::default()))
})
.expect("valid fixture scene language")
}
// This fixture compiles the terminal subset used by lifecycle tests.
pub(crate) fn language(
target: maka_plugin_core::composition::PresentationTarget,
) -> maka_plugin_core::package::SceneLanguage {
maka_plugin_core::package::SceneLanguage {
name: match target {
maka_plugin_core::composition::PresentationTarget::Tui => "maka.tui.scene",
}
.into(),
version: 1,
}
}