Răsfoiți Sursa

app: plumbing for shared shape tokens in themes

x 1 săptămână în urmă
părinte
comite
91c53e3089

+ 2 - 1
bin/app/src/app/node.rs

@@ -128,12 +128,13 @@ pub fn create_vector_art(name: &str) -> SceneNode {
     // Shape content: schema builds neutral shapes; themes rebuild themed
     // shapes structurally over them (design non-goal keeps shape colors
     // baked, so themes override the whole shape).
-    let prop = Property::new(
+    let mut prop = Property::new(
         "shape",
         PropertyType::VectorShape,
         PropertySubType::Null,
         PropertyPermission { read: Role::ALL, write: Role::App | Role::Theme | Role::User },
     );
+    prop.allow_exprs();
     node.add_property(prop).unwrap();
 
     let mut prop = Property::new(

+ 13 - 0
bin/app/src/prop/mod.rs

@@ -979,6 +979,19 @@ impl Property {
         atom.add(self.clone(), role, ModifyAction::SetCache(idxs));
         Ok(())
     }
+
+    pub fn set_cache_shape(
+        self: &Arc<Self>,
+        atom: &mut PropertyAtomicGuard,
+        role: Role,
+        i: usize,
+        val: Arc<VectorShape>,
+    ) -> Result<()> {
+        self.set_cache(i, PropertyValue::VectorShape(val))?;
+        atom.add(self.clone(), role, ModifyAction::SetCache(vec![i]));
+        Ok(())
+    }
+
     pub fn set_cache_u32_range(
         self: &Arc<Self>,
         atom: &mut PropertyAtomicGuard,

+ 52 - 1
bin/app/src/prop/wrap.rs

@@ -18,7 +18,7 @@
 
 use crate::{
     error::{Error, Result},
-    expr::{SExprMachine, SExprVal},
+    expr::{Op, SExprMachine, SExprVal},
     gfx::{Dimension, Rectangle},
     scene::SceneNode as SceneNode3,
     ui::VectorShape,
@@ -85,6 +85,50 @@ pub fn eval_f32_multi(
     Ok(())
 }
 
+/// Shape variant of the eval. The value slot holds a default-expr
+/// whose single variable is fed by one dependency edge: the shared
+/// shape-token pattern (`wire_shape`). On each call the dependency's
+/// current shape is copied (an `Arc` clone) into the cache, so
+/// `get_value` resolves the expr slot to the token's live shape.
+///
+/// Plain-value slots are untouched; exprs are restricted to exactly
+/// `[Op::LoadVar(local_name)]`.
+///
+/// Read-mask enforcement: dependency reads happen on behalf of `role`.
+pub fn eval_shape(
+    prop: &PropertyPtr,
+    atom: &mut PropertyAtomicGuard,
+    role: Role,
+    idx: usize,
+) -> Result<()> {
+    if !prop.is_expr(idx)? {
+        return Ok(())
+    }
+
+    let expr = prop.get_expr(idx)?;
+    let name = match expr.as_slice() {
+        [Op::LoadVar(name)] => name,
+        _ => return Err(Error::PropertyWrongType),
+    };
+
+    for dep in prop.get_depends() {
+        if dep.local_name != *name {
+            continue
+        }
+        let Some(dep_prop) = dep.prop.upgrade() else { continue };
+
+        if !dep_prop.can_read(role) {
+            return Err(Error::PropertyPermissionDenied)
+        }
+
+        let shape = dep_prop.get_value(dep.i)?.as_shape()?;
+        prop.set_cache_shape(atom, role, idx, shape)?;
+        return Ok(())
+    }
+
+    Ok(())
+}
+
 #[derive(Clone)]
 pub struct PropertyShape {
     prop: PropertyPtr,
@@ -114,6 +158,13 @@ impl PropertyShape {
         self.prop.get_shape(self.idx).unwrap()
     }
 
+    /// Re-evaluate this property's shape expression (if any) against its
+    /// dependency globals. Called from the owning widget's draw path so
+    /// every pass computes from current dependency values.
+    pub fn eval(&self, atom: &mut PropertyAtomicGuard) -> Result<()> {
+        eval_shape(&self.prop(), atom, self.role, self.idx)
+    }
+
     #[allow(dead_code)]
     pub fn set(&self, atom: &mut PropertyAtomicGuard, val: VectorShape) {
         self.prop().set_shape(atom, self.role, self.idx, val).unwrap()

+ 40 - 1
bin/app/src/theme/mod.rs

@@ -42,7 +42,7 @@ use crate::{
     },
     pubsub::Subscription,
     scene::{Pimpl, SceneNode, SceneNodePtr, SceneNodeType},
-    ui::{get_ui_object3, get_ui_object_ptr, RedrawTrigger},
+    ui::{get_ui_object3, get_ui_object_ptr, RedrawTrigger, VectorShape},
 };
 
 pub mod light;
@@ -64,6 +64,19 @@ fn color_prop(name: &str, val: [f32; 4]) -> Property {
     prop
 }
 
+/// Shape token: same as [`color_prop`], but the value is a whole
+/// `VectorShape`.
+fn shape_prop(name: &str, val: VectorShape) -> Property {
+    let prop = Property::new(
+        name,
+        PropertyType::VectorShape,
+        PropertySubType::Null,
+        PropertyPermission { read: Role::ALL, write: Role::Theme },
+    );
+    prop.set_default_shape(0, val).unwrap();
+    prop
+}
+
 /// The shared token vocabulary with the `minimal` palette as defaults.
 /// Every token here is wired by schema `wire_color` calls — nothing
 /// unused ships in the vocabulary. A default must never dangle when
@@ -145,6 +158,20 @@ pub fn wire_color(
     Ok(())
 }
 
+/// Shape variant of [`wire_color`].
+pub fn wire_shape(
+    node: &SceneNode,
+    prop_name: &str,
+    theme: &SceneNode,
+    token_name: &str,
+) -> Result<()> {
+    let prop = node.get_property(prop_name).ok_or(Error::PropertyNotFound)?;
+    let token = theme.get_property(token_name).ok_or(Error::PropertyNotFound)?;
+    prop.set_default_expr(0, expr::load_var(token_name))?;
+    prop.add_depend(Role::App, &token, 0, token_name);
+    Ok(())
+}
+
 // ============================================================================
 // Theme engine (design D8/D9)
 // ============================================================================
@@ -353,6 +380,18 @@ impl ThemeCtx {
         let prop = theme.get_property(name).ok_or(Error::PropertyNotFound)?;
         self.set_touched_color(atom, &prop, val)
     }
+
+    /// Shape-token variant of [`ThemeCtx::set_shared_token_color`].
+    pub fn set_shared_token_shape(
+        &self,
+        atom: &mut PropertyAtomicGuard,
+        name: &str,
+        val: VectorShape,
+    ) -> Result<()> {
+        let theme = self.sg_root.lookup_node("/theme").ok_or(Error::NodeNotFound)?;
+        let prop = theme.get_property(name).ok_or(Error::PropertyNotFound)?;
+        self.set_touched(atom, &prop, 0, PropertyValue::VectorShape(Arc::new(val)))
+    }
 }
 
 /// A theme: a name and an apply routine. `minimal` is the registry's

+ 8 - 0
bin/app/src/ui/vector_art/mod.rs

@@ -146,6 +146,14 @@ impl VectorArt {
         }
         let rect_changed = self.rect.get() != prev_rect;
 
+        // The shape may follow a shared token
+        if let Err(e) = self.shape.eval(atom) {
+            warn!(
+                target: "ui::vector_art",
+                "Shape eval failure: {e}, node: {:?}", self.node.upgrade().unwrap()
+            );
+        }
+
         // Compute under the cache lock: the compute is synchronous, so a
         // concurrent invalidation either lands before us (we see None and
         // recompute with the newer state) or after us (it clears our result