feat: bootstrap namespace-driven Tessera renderer

This commit is contained in:
2026-10-04 19:32:04 +08:00
commit 2f4c8f0fe2
14 changed files with 4262 additions and 0 deletions
+326
View File
@@ -0,0 +1,326 @@
use std::collections::HashSet;
use anyhow::{Result, bail};
use serde::{Deserialize, Serialize};
use crate::{
display_list::{Color, DisplayList, DrawCommand},
geometry::{Point, Rect},
};
pub const DEFAULT_NAMESPACE: &str = "app.main";
#[derive(Clone, Debug)]
pub struct NamespaceState {
pub name: String,
pub revision: u64,
pub document: RenderDocument,
}
impl Default for NamespaceState {
fn default() -> Self {
Self {
name: DEFAULT_NAMESPACE.to_owned(),
revision: 0,
document: RenderDocument::default(),
}
}
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct RenderDocument {
#[serde(default)]
pub source: String,
#[serde(default)]
pub root: Option<CodeNode>,
#[serde(default)]
pub frame: Frame,
}
impl RenderDocument {
pub fn validate(&self) -> Result<()> {
if self.source.len() > 1_048_576 {
bail!("source exceeds the 1 MiB limit");
}
if self.frame.commands.len() > 10_000 {
bail!("frame exceeds the 10,000 command limit");
}
parse_color(&self.frame.background)?;
for command in &self.frame.commands {
command.validate()?;
}
let mut ids = HashSet::new();
if let Some(root) = &self.root {
root.validate(0, &mut ids)?;
}
Ok(())
}
pub fn display_list(&self, scale: f32) -> Result<DisplayList> {
let mut list = DisplayList::default();
list.push(DrawCommand::Clear(parse_color(&self.frame.background)?));
for command in &self.frame.commands {
list.push(command.to_draw_command(scale)?);
}
Ok(list)
}
pub fn node(&self, id: &str) -> Option<&CodeNode> {
self.root.as_ref()?.find(id)
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CodeNode {
pub id: String,
pub name: String,
pub kind: String,
#[serde(default)]
pub start_line: usize,
#[serde(default)]
pub end_line: usize,
#[serde(default)]
pub children: Vec<CodeNode>,
}
impl CodeNode {
fn validate<'a>(&'a self, depth: usize, ids: &mut HashSet<&'a str>) -> Result<()> {
if depth > 64 {
bail!("architecture tree exceeds 64 levels");
}
if self.id.is_empty() {
bail!("architecture node id cannot be empty");
}
if !ids.insert(&self.id) {
bail!("duplicate architecture node id: {}", self.id);
}
if self.start_line > 0 && self.end_line > 0 && self.start_line > self.end_line {
bail!("invalid source range for node {}", self.id);
}
for child in &self.children {
child.validate(depth + 1, ids)?;
}
Ok(())
}
fn find(&self, id: &str) -> Option<&CodeNode> {
if self.id == id {
return Some(self);
}
self.children.iter().find_map(|child| child.find(id))
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Frame {
#[serde(default = "default_background")]
pub background: String,
#[serde(default)]
pub commands: Vec<RenderCommand>,
}
impl Default for Frame {
fn default() -> Self {
Self {
background: default_background(),
commands: Vec::new(),
}
}
}
fn default_background() -> String {
"#F7F7F5".to_owned()
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum RenderCommand {
RoundedRect {
x: f32,
y: f32,
width: f32,
height: f32,
#[serde(default)]
radius: f32,
color: String,
},
StrokeRoundedRect {
x: f32,
y: f32,
width: f32,
height: f32,
#[serde(default)]
radius: f32,
#[serde(default = "default_stroke_width")]
stroke_width: f32,
color: String,
},
Text {
x: f32,
y: f32,
value: String,
#[serde(default = "default_text_size")]
size: u32,
color: String,
},
}
impl RenderCommand {
fn validate(&self) -> Result<()> {
match self {
Self::RoundedRect {
x,
y,
width,
height,
radius,
color,
} => {
validate_rect(*x, *y, *width, *height, *radius)?;
parse_color(color)?;
}
Self::StrokeRoundedRect {
x,
y,
width,
height,
radius,
stroke_width,
color,
} => {
validate_rect(*x, *y, *width, *height, *radius)?;
if !stroke_width.is_finite() || *stroke_width <= 0.0 {
bail!("stroke_width must be a positive finite number");
}
parse_color(color)?;
}
Self::Text {
x,
y,
value,
size,
color,
} => {
if !x.is_finite() || !y.is_finite() {
bail!("text coordinates must be finite");
}
if value.len() > 16_384 {
bail!("text command exceeds 16 KiB");
}
if !(1..=8).contains(size) {
bail!("text size must be between 1 and 8");
}
parse_color(color)?;
}
}
Ok(())
}
fn to_draw_command(&self, scale: f32) -> Result<DrawCommand> {
Ok(match self {
Self::RoundedRect {
x,
y,
width,
height,
radius,
color,
} => DrawCommand::FillRoundedRect {
rect: scaled_rect(*x, *y, *width, *height, scale),
radius: radius * scale,
color: parse_color(color)?,
},
Self::StrokeRoundedRect {
x,
y,
width,
height,
radius,
stroke_width,
color,
} => DrawCommand::StrokeRoundedRect {
rect: scaled_rect(*x, *y, *width, *height, scale),
radius: radius * scale,
width: stroke_width * scale,
color: parse_color(color)?,
},
Self::Text {
x,
y,
value,
size,
color,
} => DrawCommand::Text {
origin: Point::new(x * scale, y * scale),
value: value.clone(),
scale: ((*size as f32 * scale).round() as u32).max(1),
color: parse_color(color)?,
},
})
}
}
fn default_stroke_width() -> f32 {
1.0
}
fn default_text_size() -> u32 {
1
}
fn validate_rect(x: f32, y: f32, width: f32, height: f32, radius: f32) -> Result<()> {
if !x.is_finite() || !y.is_finite() || !width.is_finite() || !height.is_finite() {
bail!("rectangle values must be finite");
}
if width < 0.0 || height < 0.0 || !radius.is_finite() || radius < 0.0 {
bail!("rectangle dimensions and radius cannot be negative");
}
Ok(())
}
fn scaled_rect(x: f32, y: f32, width: f32, height: f32, scale: f32) -> Rect {
Rect::new(x * scale, y * scale, width * scale, height * scale)
}
pub fn parse_color(value: &str) -> Result<Color> {
let hex = value
.strip_prefix('#')
.ok_or_else(|| anyhow::anyhow!("color must begin with #"))?;
if hex.len() != 6 && hex.len() != 8 {
bail!("color must use #RRGGBB or #RRGGBBAA");
}
let parse = |range| u8::from_str_radix(&hex[range], 16);
Ok(Color::rgba(
parse(0..2)?,
parse(2..4)?,
parse(4..6)?,
if hex.len() == 8 { parse(6..8)? } else { 255 },
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_document_is_valid_and_blank() {
let document = RenderDocument::default();
document.validate().unwrap();
assert!(document.root.is_none());
assert!(document.frame.commands.is_empty());
}
#[test]
fn parses_rgb_and_rgba_colors() {
assert_eq!(
parse_color("#102030").unwrap(),
Color::rgba(16, 32, 48, 255)
);
assert_eq!(
parse_color("#10203080").unwrap(),
Color::rgba(16, 32, 48, 128)
);
}
}