use std::collections::HashSet; use std::sync::Arc; use anyhow::{Context as _, Result, bail}; use serde::{Deserialize, Serialize}; use crate::{ display_list::{Color, DisplayList, DrawCommand}, geometry::{Point, Rect}, ui_bridge::UiNode, }; pub const DEFAULT_NAMESPACE: &str = "app.main"; #[derive(Clone, Debug)] pub struct NamespaceState { pub name: String, pub revision: u64, pub submission: u64, pub document: RenderDocument, pub viewport: Option, pub window_viewports: std::collections::BTreeMap, pub runtime_session: u64, pub window_commands: Vec, } impl Default for NamespaceState { fn default() -> Self { Self { name: DEFAULT_NAMESPACE.to_owned(), revision: 0, submission: 0, document: RenderDocument::default(), viewport: None, window_viewports: std::collections::BTreeMap::new(), runtime_session: 0, window_commands: Vec::new(), } } } impl NamespaceState { pub fn publish_window_commands(&mut self, session: u64, commands: Vec) { if self.runtime_session != session { self.window_commands.clear(); // The primary shell survives VM replacement. Secondary native // resources belong to their originating runtime generation. self.window_viewports .retain(|id, _| *id == crate::window::MAIN_WINDOW); } self.runtime_session = session; if session == 0 { self.window_commands.clear(); return; } for command in commands { self.window_commands .retain(|old| !old.command.same_kind(&command)); self.window_commands .push(crate::window::Envelope { session, command }); } self.window_commands .sort_by_key(|command| command.command.sequence); } pub fn next_submission(&self) -> Result { self.submission .checked_add(1) .context("submission counter exhausted") } pub fn commit(&mut self, submission: u64, document: RenderDocument) -> Result> { if submission != self.submission { return Ok(None); } let revision = self .revision .checked_add(1) .context("revision counter exhausted")?; self.document = document; self.revision = revision; Ok(Some(revision)) } pub fn clear(&mut self) -> Result { let submission = self.next_submission()?; let revision = self .revision .checked_add(1) .context("revision counter exhausted")?; self.submission = submission; self.document = RenderDocument::default(); self.runtime_session = 0; self.window_commands.clear(); self.revision = revision; Ok(revision) } } #[derive(Clone, Debug, Default, Serialize, Deserialize)] pub struct RenderDocument { #[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")] pub windows: std::collections::BTreeMap, #[serde(skip)] pub ui_root: Option>, #[serde(default)] pub source: String, #[serde(default)] pub root: Option, #[serde(default)] pub frame: Frame, } impl RenderDocument { pub fn validate(&self) -> Result<()> { let surface = &self.frame.surface; if !surface.zoom.is_finite() || !(0.1..=4.0).contains(&surface.zoom) || ![surface.pan.x, surface.pan.y] .iter() .all(|v| v.is_finite() && v.abs() <= 1_000_000.0) || ![surface.extent.width, surface.extent.height] .iter() .all(|v| v.is_finite() && (0.0..=1_000_000.0).contains(v)) { bail!("invalid page camera or extent"); } if self.windows.len() > 15 { bail!("too many window projections"); } for (id, document) in &self.windows { if *id == crate::window::MAIN_WINDOW || *id != document.frame.window || !document.windows.is_empty() { bail!("invalid secondary window projection"); } document.validate()?; } 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"); } if self.frame.window == 0 { bail!("invalid window identity"); } parse_color(&self.frame.background)?; if !self.frame.command_clips.is_empty() && self.frame.command_clips.len() != self.frame.commands.len() { bail!("invalid command clip table"); } for clip in self.frame.command_clips.iter().flatten() { validate_rect(clip[0], clip[1], clip[2], clip[3], 0.0)?; } if !self.frame.command_views.is_empty() && self.frame.command_views.len() != self.frame.commands.len() { bail!("invalid command view table"); } for view in self.frame.command_views.iter().flatten() { if !view.valid() { bail!("invalid command view transform"); } } for command in &self.frame.commands { command.validate()?; } if self.frame.mounts.len() > 10_000 { bail!("frame exceeds the 10000 mount limit"); } let mut tab_ids = HashSet::new(); if self.frame.tabs.len() > crate::ui_bridge::MAX_PAGES || self.frame.tabs.iter().filter(|tab| tab.active).count() > 1 || self .frame .tabs .iter() .any(|tab| tab.id == 0 || !tab_ids.insert(tab.id) || tab.title.len() > 1024) { bail!("invalid tab projection"); } let mut mount_ids = HashSet::new(); for mount in &self.frame.mounts { if mount.view.is_some_and(|view| !view.valid()) { bail!("invalid Mount view transform"); } if mount.id == 0 || mount.block_id.is_empty() || !mount_ids.insert(mount.id) { bail!("invalid or duplicate mount identity"); } validate_rect(mount.x, mount.y, mount.width, mount.height, 0.0)?; if let Some(clip) = mount.clip { validate_rect(clip[0], clip[1], clip[2], clip[3], 0.0)?; } if let Some(clip) = mount.viewport_clip { validate_rect(clip[0], clip[1], clip[2], clip[3], 0.0)?; } if let Some(scroll) = mount.scroll && (!scroll.iter().all(|n| { n.is_finite() && (0.0..=crate::ui_bridge::MAX_DIMENSION as f32).contains(n) }) || scroll[0] > scroll[2] || scroll[1] > scroll[3]) { bail!("invalid scroll projection"); } } for mount in &self.frame.mounts { for linked in [mount.parent, mount.first_child, mount.next_sibling] .into_iter() .flatten() { if !mount_ids.contains(&linked) { bail!("mount links outside this frame"); } } } if let Some(root) = &self.ui_root && self .frame .mounts .first() .is_none_or(|mount| mount.block_id != root.block_id) { bail!("frame root does not match its visual projection"); } 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 { let frame = &self.frame; let mut list = DisplayList::default(); list.push(DrawCommand::Clear(parse_color(&frame.background)?)); for (index, command) in frame.commands.iter().enumerate() { let clip = frame.command_clips.get(index).copied().flatten(); if let Some(clip) = clip { list.push(DrawCommand::PushClip(scaled_rect( clip[0], clip[1], clip[2], clip[3], scale, ))); } let view = frame .command_views .get(index) .copied() .flatten() .unwrap_or_default(); list.push( command .to_draw_command(scale * view.zoom)? .translated(Point::new(view.x * scale, view.y * scale)), ); if clip.is_some() { list.push(DrawCommand::PopClip); } } if let Some(focus) = frame .focus .and_then(|id| frame.mounts.iter().find(|m| m.id == id)) .filter(|mount| mount.panel.is_none()) { if let Some(c) = focus.clip { list.push(DrawCommand::PushClip(scaled_rect( c[0], c[1], c[2], c[3], scale, ))); } list.push( RenderCommand::StrokeRoundedRect { x: focus .view .unwrap_or_default() .rect(Rect::new(focus.x, focus.y, focus.width, focus.height)) .x, y: focus .view .unwrap_or_default() .rect(Rect::new(focus.x, focus.y, focus.width, focus.height)) .y, width: focus.width * focus.view.unwrap_or_default().zoom, height: focus.height * focus.view.unwrap_or_default().zoom, radius: 7.0, stroke_width: 2.0, color: "#4F6FDB".into(), } .to_draw_command(scale)?, ); if focus.clip.is_some() { list.push(DrawCommand::PopClip); } } if let Some(error) = &frame.fault { list.push( RenderCommand::RoundedRect { x: 0.0, y: 0.0, width: 10000.0, height: 36.0, radius: 0.0, color: "#FFF1EF".into(), } .to_draw_command(scale)?, ); list.push( RenderCommand::Text { x: 12.0, y: 8.0, value: format!("运行已停止:{}", error.lines().next().unwrap_or_default()), size: 1, color: "#A32D25".into(), wrap_bounds: None, } .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, } 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(skip)] pub hit_index: Arc, #[serde(default)] pub surface: crate::page::Surface, #[serde(default = "default_window")] pub window: u64, #[serde(default = "default_background")] pub background: String, #[serde(default)] pub commands: Vec, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub command_clips: Vec>, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub command_views: Vec>, #[serde(skip)] pub command_mounts: Vec, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub mounts: Vec, #[serde(default)] pub layout_version: u64, #[serde(default)] pub session: u64, #[serde(default)] pub focus: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub tabs: Vec, #[serde(default)] pub fault: Option, } impl Default for Frame { fn default() -> Self { Self { hit_index: Arc::default(), surface: crate::page::Surface::default(), window: crate::window::MAIN_WINDOW, background: default_background(), commands: Vec::new(), command_clips: Vec::new(), command_views: Vec::new(), command_mounts: Vec::new(), mounts: Vec::new(), layout_version: 0, session: 0, focus: None, tabs: Vec::new(), fault: None, } } } /// A position in a published frame. Identifiers are opaque data, never VM handles. #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct MountGeometry { #[serde(default, skip_serializing_if = "Option::is_none")] pub panel: Option>, #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub canvas_viewport: bool, #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub scene3d_viewport: bool, #[serde(default)] pub camera_controls: bool, #[serde(default, skip_serializing_if = "Option::is_none")] pub viewport_clip: Option<[f32; 4]>, #[serde(default, skip_serializing_if = "Option::is_none")] pub view: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub clip: Option<[f32; 4]>, #[serde(default, skip_serializing_if = "Option::is_none")] pub scroll: Option<[f32; 4]>, pub id: u64, pub block_id: String, pub parent: Option, pub first_child: Option, pub next_sibling: Option, pub x: f32, pub y: f32, pub width: f32, pub height: f32, pub interactive: bool, } impl MountGeometry { pub fn view_point(&self, point: Point, clipped: bool) -> Option { self.panel .as_ref() .map_or(Some(point), |panel| panel.texture_point(point, clipped)) } pub fn logical_point(&self, point: Point) -> Option { let point = self.view_point(point, true)?; Some(self.view.unwrap_or_default().inverse(point)) } pub fn logical_delta(&self, point: Option<[f32; 2]>, delta: [f32; 2]) -> Option<[f32; 2]> { let zoom = self.view.unwrap_or_default().zoom; if self.panel.is_none() { return Some([delta[0] / zoom, delta[1] / zoom]); } let point = point?; let current = self.view_point(Point::new(point[0], point[1]), false)?; let previous = self.view_point(Point::new(point[0] + delta[0], point[1] + delta[1]), false)?; Some([ ((previous.x - current.x) / zoom).clamp(-1_000_000., 1_000_000.), ((previous.y - current.y) / zoom).clamp(-1_000_000., 1_000_000.), ]) } pub fn contains(&self, point: Point) -> bool { let Some(point) = self.view_point(point, true) else { return false; }; let local = self.view.map_or(point, |view| view.inverse(point)); Rect::new(self.x, self.y, self.width, self.height).contains(local) && self.clip.is_none_or(|c| { c[2] > 0.0 && c[3] > 0.0 && Rect::new(c[0], c[1], c[2], c[3]).contains(point) }) } } impl Frame { pub fn camera_at(&self, point: Point) -> Option<&MountGeometry> { self.hit_index .candidates(point, self.mounts.len()) .into_iter() .map(|index| &self.mounts[index]) .find(|mount| { (mount.canvas_viewport || mount.scene3d_viewport && mount.camera_controls) && mount.contains(point) && mount.viewport_clip.is_some_and(|clip| { mount.view_point(point, true).is_some_and(|point| { Rect::new(clip[0], clip[1], clip[2], clip[3]).contains(point) }) }) }) } pub fn hit_test(&self, point: Point) -> Option<&MountGeometry> { if self.fault.is_some() { return None; } self.hit_index .candidates(point, self.mounts.len()) .into_iter() .map(|index| &self.mounts[index]) .find(|mount| mount.interactive && mount.contains(point)) } } fn default_window() -> u64 { crate::window::MAIN_WINDOW } fn default_background() -> String { "#F7F7F5".to_owned() } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum RenderCommand { Scene3D { x: f32, y: f32, width: f32, height: f32, view: Arc, }, 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, #[serde(default, skip_serializing_if = "Option::is_none")] wrap_bounds: Option<[f32; 2]>, }, } impl RenderCommand { pub(crate) fn validate(&self) -> Result<()> { match self { Self::Scene3D { x, y, width, height, view, } => { validate_rect(*x, *y, *width, *height, 0.0)?; if !view.valid() { bail!("invalid 3D view"); } } 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, wrap_bounds, } => { if !x.is_finite() || !y.is_finite() { bail!("text coordinates must be finite"); } if let Some([width, height]) = wrap_bounds { validate_rect(*x, *y, *width, *height, 0.0)?; if [*x, *y, *width, *height, *x + *width, *y + *height] .iter() .any(|value| value.abs() > crate::ui_bridge::MAX_DIMENSION as f32) { bail!("text wrap bounds exceed limits"); } } 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 { Ok(match self { Self::Scene3D { x, y, width, height, view, } => DrawCommand::Scene3D { rect: scaled_rect(*x, *y, *width, *height, scale), view: view.clone(), }, 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, wrap_bounds, } => match wrap_bounds { Some([width, height]) => DrawCommand::WrappedText { rect: scaled_rect(*x, *y, *width, *height, scale), value: value.clone(), style: crate::text::TextStyle::ui(*size).scaled(scale), color: parse_color(color)?, }, None => DrawCommand::Text { origin: Point::new(x * scale, y * scale), value: value.clone(), style: crate::text::TextStyle::ui(*size).scaled(scale), 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 { let hex = value .strip_prefix('#') .ok_or_else(|| anyhow::anyhow!("color must begin with #"))?; if !hex.is_ascii() || (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 { #[test] fn text_wrap_bounds_validate_and_legacy_text_remains_unwrapped() { let mut command: RenderCommand = serde_json::from_str( r##"{"type":"text","x":0,"y":0,"value":"text","size":1,"color":"#000000"}"##, ) .unwrap(); assert!(matches!( command.to_draw_command(1.0).unwrap(), DrawCommand::Text { .. } )); if let RenderCommand::Text { wrap_bounds, .. } = &mut command { *wrap_bounds = Some([40.0, 20.0]); } command.validate().unwrap(); assert!( matches!(command.to_draw_command(1.25).unwrap(),DrawCommand::WrappedText{rect,..}if rect.width==50.0 && rect.height==25.0) ); if let RenderCommand::Text { wrap_bounds, .. } = &mut command { *wrap_bounds = Some([-1.0, 20.0]); } assert!(command.validate().is_err()); } 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) ); } } #[cfg(test)] mod window_projection_tests { use super::*; #[test] fn replacement_and_reset_discard_old_window_projections() { let mut state = NamespaceState::default(); let command = crate::window::Command { window: 1, sequence: 1, action: crate::window::Action::Focus, }; state.publish_window_commands(1, vec![command.clone()]); let resize = crate::window::Command { window: 1, sequence: 2, action: crate::window::Action::Resize { width: 900.0, height: 600.0, }, }; state.publish_window_commands(1, vec![resize.clone()]); assert_eq!(state.window_commands.len(), 2); state.publish_window_commands(2, vec![resize]); assert_eq!(state.window_commands.len(), 1); assert_eq!(state.window_commands[0].session, 2); state.clear().unwrap(); assert_eq!(state.runtime_session, 0); assert!(state.window_commands.is_empty()); } }