Files
Glint-Runtime/src/interpreter/types.rs
Glint Dev b6fa841d11 Phase 9: Keyed widgets (9.1 + 9.2)
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.
2026-07-24 19:53:02 +03:00

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 {}