9.1: Add content_hash field to Element, computed during VDOM evaluation from type_name + properties + children content_hashes via DefaultHasher. 9.2: Assign stable iced::widget::Id to text_input and scrollable widgets so Iced preserves widget state (cursor position, scroll offset) across frames. Ids are cached in a thread_local HashMap to avoid leaks. 9.3 skipped: iced::Element is not Clone and contains Box<dyn Widget>, making element-level caching impractical without unsafe lifetime hacks.
347 lines
8.9 KiB
Rust
347 lines
8.9 KiB
Rust
use std::borrow::Cow;
|
|
use std::collections::HashMap;
|
|
use std::fmt;
|
|
use std::ops::Range;
|
|
|
|
|
|
use super::reactive::{ElementId, ReactiveTracker};
|
|
use super::style::{ComputedStyle, StyleSheet};
|
|
use compact_str::CompactString;
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
|
pub struct InternedStr(u32);
|
|
|
|
impl InternedStr {
|
|
pub const fn from_raw(id: u32) -> Self {
|
|
Self(id)
|
|
}
|
|
|
|
pub fn raw(&self) -> u32 {
|
|
self.0
|
|
}
|
|
}
|
|
|
|
impl InternedStr {
|
|
pub fn eq_str(&self, other: &str) -> bool {
|
|
with_interner(|interner| interner.lookup(*self) == other)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct Interner {
|
|
strings: Vec<String>,
|
|
map: HashMap<String, u32>,
|
|
next_id: u32,
|
|
}
|
|
|
|
impl Interner {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
strings: Vec::new(),
|
|
map: HashMap::new(),
|
|
next_id: 0,
|
|
}
|
|
}
|
|
|
|
pub fn intern(&mut self, s: &str) -> InternedStr {
|
|
if let Some(&id) = self.map.get(s) {
|
|
return InternedStr(id);
|
|
}
|
|
let id = self.next_id;
|
|
self.next_id += 1;
|
|
self.strings.push(s.to_string());
|
|
self.map.insert(s.to_string(), id);
|
|
InternedStr(id)
|
|
}
|
|
|
|
pub fn lookup(&self, id: InternedStr) -> &str {
|
|
&self.strings[id.0 as usize]
|
|
}
|
|
|
|
pub fn intern_or_none(&mut self, s: Option<&str>) -> Option<InternedStr> {
|
|
s.map(|s| self.intern(s))
|
|
}
|
|
}
|
|
|
|
use std::cell::RefCell;
|
|
|
|
thread_local! {
|
|
static GLOBAL_INTERNER: RefCell<Interner> = RefCell::new(Interner::new());
|
|
}
|
|
|
|
pub fn with_interner<F, R>(f: F) -> R
|
|
where
|
|
F: FnOnce(&mut Interner) -> R,
|
|
{
|
|
GLOBAL_INTERNER.with(|i| f(&mut *i.borrow_mut()))
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub enum Value {
|
|
Str(CompactString),
|
|
Int(i64),
|
|
Float(f64),
|
|
Bool(bool),
|
|
Array(Vec<Value>),
|
|
None,
|
|
}
|
|
|
|
impl Value {
|
|
pub fn as_str(&self) -> Option<&str> {
|
|
match self {
|
|
Value::Str(s) => Some(s.as_str()),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub fn to_owned_string(&self) -> CompactString {
|
|
match self {
|
|
Value::Str(s) => s.clone(),
|
|
Value::Int(i) => CompactString::new(i.to_string()),
|
|
Value::Float(f) => CompactString::new(if f.fract() == 0.0 {
|
|
format!("{:.1}", f)
|
|
} else {
|
|
f.to_string()
|
|
}),
|
|
Value::Bool(b) => CompactString::new(b.to_string()),
|
|
Value::Array(a) => CompactString::new(a.iter().map(|v| v.to_owned_string()).collect::<Vec<_>>().join(",")),
|
|
Value::None => CompactString::new(""),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<&str> for Value {
|
|
fn from(s: &str) -> Self {
|
|
Value::Str(CompactString::new(s))
|
|
}
|
|
}
|
|
|
|
impl From<String> for Value {
|
|
fn from(s: String) -> Self {
|
|
Value::Str(CompactString::new(s))
|
|
}
|
|
}
|
|
|
|
impl From<i64> for Value {
|
|
fn from(i: i64) -> Self {
|
|
Value::Int(i)
|
|
}
|
|
}
|
|
|
|
impl From<f64> for Value {
|
|
fn from(f: f64) -> Self {
|
|
Value::Float(f)
|
|
}
|
|
}
|
|
|
|
impl From<bool> for Value {
|
|
fn from(b: bool) -> Self {
|
|
Value::Bool(b)
|
|
}
|
|
}
|
|
|
|
impl<T: Into<Value>> From<Vec<T>> for Value {
|
|
fn from(v: Vec<T>) -> Self {
|
|
Value::Array(v.into_iter().map(Into::into).collect())
|
|
}
|
|
}
|
|
|
|
impl PartialEq for Value {
|
|
fn eq(&self, other: &Self) -> bool {
|
|
match (self, other) {
|
|
(Value::Str(a), Value::Str(b)) => a == b,
|
|
(Value::Int(a), Value::Int(b)) => a == b,
|
|
(Value::Float(a), Value::Float(b)) => (a - b).abs() < f64::EPSILON,
|
|
(Value::Bool(a), Value::Bool(b)) => a == b,
|
|
(Value::Array(a), Value::Array(b)) => a == b,
|
|
(Value::None, Value::None) => true,
|
|
_ => false,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct Element<'a> {
|
|
pub type_name: &'a str,
|
|
pub properties: Vec<(Cow<'a, str>, Cow<'a, str>)>,
|
|
pub children: Vec<Element<'a>>,
|
|
pub computed_style: ComputedStyle,
|
|
pub element_id: ElementId,
|
|
pub content_hash: u64,
|
|
}
|
|
|
|
impl<'a> Element<'a> {
|
|
pub fn id(&self) -> Option<&str> {
|
|
self.properties.iter().find_map(|(k, v)| {
|
|
if **k == *"id" { Some(v.as_ref()) } else { None }
|
|
})
|
|
}
|
|
|
|
pub fn new(type_name: &'a str) -> Self {
|
|
Self {
|
|
type_name,
|
|
properties: Vec::with_capacity(8),
|
|
children: Vec::with_capacity(4),
|
|
computed_style: ComputedStyle::default(),
|
|
element_id: ElementId(u32::MAX),
|
|
content_hash: 0,
|
|
}
|
|
}
|
|
|
|
pub fn new_with_id(type_name: &'a str, id: ElementId) -> Self {
|
|
Self {
|
|
type_name,
|
|
properties: Vec::with_capacity(8),
|
|
children: Vec::with_capacity(4),
|
|
computed_style: ComputedStyle::default(),
|
|
element_id: id,
|
|
content_hash: 0,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct ComponentDef<'a> {
|
|
pub name: String,
|
|
pub params: Vec<(String, String)>,
|
|
pub children: Vec<Element<'a>>,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct Document<'a> {
|
|
pub roots: Vec<Element<'a>>,
|
|
pub components: HashMap<String, ComponentDef<'a>>,
|
|
pub variables: HashMap<String, Value>,
|
|
pub rhei_scripts: Vec<String>,
|
|
pub stylesheet: StyleSheet,
|
|
pub interner: Interner,
|
|
pub tracker: ReactiveTracker,
|
|
}
|
|
|
|
pub type NodeId = u32;
|
|
pub type PropertyIdx = usize;
|
|
pub type NodeIdx = usize;
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct VNode<'a> {
|
|
pub id: NodeId,
|
|
pub type_name: &'a str,
|
|
pub properties: Range<PropertyIdx>,
|
|
pub children_range: Range<NodeIdx>,
|
|
pub computed_style: ComputedStyle,
|
|
pub element_id: ElementId,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct FlatVDom<'a> {
|
|
pub nodes: Vec<VNode<'a>>,
|
|
pub properties: Vec<(String, String)>,
|
|
root_indices: Vec<NodeIdx>,
|
|
}
|
|
|
|
impl<'a> FlatVDom<'a> {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
nodes: Vec::new(),
|
|
properties: Vec::new(),
|
|
root_indices: Vec::new(),
|
|
}
|
|
}
|
|
|
|
pub fn from_elements(elements: &[Element<'a>]) -> Self {
|
|
let mut fv = FlatVDom::new();
|
|
fv.append_elements(elements, true);
|
|
fv
|
|
}
|
|
|
|
fn append_elements(&mut self, elements: &[Element<'a>], is_root: bool) {
|
|
for el in elements {
|
|
let node_idx = self.nodes.len();
|
|
if is_root {
|
|
self.root_indices.push(node_idx);
|
|
}
|
|
let prop_start = self.properties.len();
|
|
for (k, v) in &el.properties {
|
|
self.properties.push((k.clone().into_owned(), v.clone().into_owned()));
|
|
}
|
|
self.nodes.push(VNode {
|
|
id: 0,
|
|
type_name: el.type_name,
|
|
properties: prop_start..self.properties.len(),
|
|
children_range: 0..0,
|
|
computed_style: el.computed_style,
|
|
element_id: el.element_id,
|
|
});
|
|
let child_start = self.nodes.len();
|
|
self.append_elements(&el.children, false);
|
|
self.nodes[node_idx].children_range = child_start..self.nodes.len();
|
|
}
|
|
}
|
|
|
|
pub fn into_elements(self) -> Vec<Element<'a>> {
|
|
let mut result = Vec::with_capacity(self.root_indices.len());
|
|
for &root_idx in &self.root_indices {
|
|
if let Some(el) = Self::node_to_element(&self.nodes, &self.properties, root_idx) {
|
|
result.push(el);
|
|
}
|
|
}
|
|
result
|
|
}
|
|
|
|
fn node_to_element(nodes: &[VNode<'a>], props: &[(String, String)], idx: NodeIdx) -> Option<Element<'a>> {
|
|
let vn = nodes.get(idx)?;
|
|
let mut el = Element::new(vn.type_name);
|
|
el.element_id = vn.element_id;
|
|
el.computed_style = vn.computed_style;
|
|
for i in vn.properties.clone() {
|
|
if i < props.len() {
|
|
let (k, v) = &props[i];
|
|
el.push_prop(k.clone(), v.clone());
|
|
}
|
|
}
|
|
for child_idx in vn.children_range.clone() {
|
|
if let Some(child) = Self::node_to_element(nodes, props, child_idx) {
|
|
el.children.push(child);
|
|
}
|
|
}
|
|
Some(el)
|
|
}
|
|
|
|
pub fn get_node(&self, idx: NodeIdx) -> Option<&VNode<'a>> {
|
|
self.nodes.get(idx)
|
|
}
|
|
|
|
pub fn node_count(&self) -> usize {
|
|
self.nodes.len()
|
|
}
|
|
|
|
pub fn root_count(&self) -> usize {
|
|
self.root_indices.len()
|
|
}
|
|
|
|
pub fn root_indices(&self) -> &[NodeIdx] {
|
|
&self.root_indices
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum InterpError {
|
|
BadMagic,
|
|
UnexpectedEof,
|
|
InvalidUtf8,
|
|
UnexpectedPop,
|
|
}
|
|
|
|
impl fmt::Display for InterpError {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
Self::BadMagic => write!(f, "Bad magic bytes — not a .glbc file"),
|
|
Self::UnexpectedEof => write!(f, "Unexpected end of bytecode"),
|
|
Self::InvalidUtf8 => write!(f, "String is not valid UTF-8"),
|
|
Self::UnexpectedPop => write!(f, "OP_ELEM_POP without matching OP_ELEM_PUSH"),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for InterpError {}
|