//! Variables (data merge): named values bound to objects, with datasets that apply //! them all at once. One template document produces many variants (badges, price //! lists, localized copies) by selecting a dataset instead of editing each object. use std::collections::{BTreeMap, BTreeSet}; use serde::{Deserialize, Serialize}; use super::{Document, NodeId, NodeKind}; /// What a variable drives. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum VariableKind { /// A type object's characters. #[default] Text, /// Whether bound objects show. Visibility, } impl VariableKind { /// Parse the `kind` of `variable.define`: `text` or `visibility` (`visible` is accepted /// as the same thing, as the panel and the docs call it both). pub fn parse(s: &str) -> Option { match s.to_ascii_lowercase().as_str() { "text" => Some(Self::Text), "visibility" | "visible" => Some(Self::Visibility), _ => None, } } /// The kind's own name, as `variable.define` takes it and `variable.list` reports it. pub fn label(self) -> &'static str { match self { Self::Text => "text", Self::Visibility => "visibility", } } } /// One named variable. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Variable { pub name: String, pub kind: VariableKind, } /// One value a dataset gives a variable. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum DataValue { Text(String), Visible(bool), } impl DataValue { /// Whether this value fits the variable's kind (a dataset may carry values for /// variables of either kind; each binding takes what fits it). pub fn fits(&self, kind: VariableKind) -> bool { matches!((self, kind), (Self::Text(_), VariableKind::Text) | (Self::Visible(_), VariableKind::Visibility)) } } /// One named row of values, applied with `dataset.select`. #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] pub struct DataSet { pub name: String, #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub values: BTreeMap, } impl DataSet { /// Whether the art, as [`Document::captured_values`] read it, still holds what this row /// says. A value the art can't report (its object is locked or gone) isn't drift: the row /// keeps it, and applying it again would skip it too. pub fn matches(&self, current: &BTreeMap) -> bool { self.values.iter().all(|(k, v)| current.get(k).is_none_or(|c| c == v)) } } /// The variable state of a document: definitions, datasets, bindings /// (object id → the variables bound to it) and which dataset is active. /// /// An object may be driven by several variables — the reference app binds the selected object /// to the selected variable, one click at a time, and never says a second binding replaces the /// first — so a binding is a set. `dataset.select` applies them in the order the variables are /// defined, which is what makes two text variables on one object deterministic: the last one /// wins, and `applied` counts both. #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] pub struct Variables { #[serde(default, skip_serializing_if = "Vec::is_empty")] pub variables: Vec, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub datasets: Vec, #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub bindings: BTreeMap>, #[serde(default, skip_serializing_if = "Option::is_none")] pub active_dataset: Option, } impl Variables { /// The variable `name`, if the document defines it. pub fn variable(&self, name: &str) -> Option<&Variable> { self.variables.iter().find(|v| v.name == name) } /// The dataset `name`, if the document has it. pub fn dataset(&self, name: &str) -> Option<&DataSet> { self.datasets.iter().find(|d| d.name == name) } /// Bind `id` to the variable `name`. True when it was not bound to it already. pub fn bind(&mut self, id: NodeId, name: &str) -> bool { self.bindings.entry(id).or_default().insert(name.to_string()) } /// Drop `id`'s binding to `name` (and the binding itself when that was its last variable). /// True when there was one. pub fn unbind(&mut self, id: NodeId, name: &str) -> bool { let Some(bound) = self.bindings.get_mut(&id) else { return false }; let dropped = bound.remove(name); if bound.is_empty() { self.bindings.remove(&id); } dropped } /// The objects bound to the variable `name`, in document order. pub fn objects_of(&self, name: &str) -> Vec { let mut ids: Vec = self.bindings.iter().filter(|(_, v)| v.contains(name)).map(|(id, _)| *id).collect(); ids.sort_unstable(); ids } /// Drop the variables `names`, the dataset values that name them and the bindings to /// them. Datasets named here are left alone: their names are their own namespace (the /// active one is dropped by whoever deletes the dataset). pub fn prune(&mut self, names: &[String]) -> usize { // A set, so a long list of names costs a lookup per value rather than a scan. let names: BTreeSet<&str> = names.iter().map(String::as_str).collect(); let before = self.variables.len(); self.variables.retain(|v| !names.contains(v.name.as_str())); for dataset in &mut self.datasets { dataset.values.retain(|k, _| !names.contains(k.as_str())); } for bound in self.bindings.values_mut() { bound.retain(|v| !names.contains(v.as_str())); } self.bindings.retain(|_, v| !v.is_empty()); before - self.variables.len() } } impl Document { /// Forget the bindings of objects that are no longer in the document (after every edit), /// like [`Document::prune_assets`] does for Asset Export's. A binding outliving its object /// would count as a skip in every dataset application and grow the saved file forever. pub fn prune_variable_bindings(&mut self) { if self.variables.bindings.keys().all(|id| self.node(*id).is_some()) { return; } let mut bindings = std::mem::take(&mut self.variables.bindings); bindings.retain(|id, _| self.node(*id).is_some()); self.variables.bindings = bindings; } /// Whether a variable may read and rewrite the object `id`: it is in the document, neither /// it nor anything above it is locked, and nothing above it is hidden. Its own visibility /// doesn't count, as that is what a visibility variable drives: a row that hid it must be /// able to show it again, and a capture must record it hidden. pub fn variable_target(&self, id: NodeId) -> bool { self.ancestry(id).is_some_and(|a| a.iter().all(|i| self.node(*i).is_some_and(|n| !n.locked && (n.visible || *i == id)))) } /// What the object `id` holds for the variable of `kind`: its characters, or whether it /// shows. `None` when the object is gone, is not of that kind, or is locked or on a hidden /// or locked layer (the document's value is whatever it was left at). pub fn variable_value(&self, id: NodeId, kind: VariableKind) -> Option { if !self.variable_target(id) { return None; } let node = self.node(id)?; match (kind, &node.kind) { (VariableKind::Text, NodeKind::Text(t)) => Some(DataValue::Text(t.plain_text())), (VariableKind::Visibility, _) => Some(DataValue::Visible(node.visible)), (VariableKind::Text, _) => None, } } /// The values a dataset captured right now: what each bound object holds, in the order the /// variables are defined. `dataset.capture` writes these into a new row and /// `dataset.update` into the active one; the Variables panel compares the active row with /// them to tell whether the art still matches what it says. pub fn captured_values(&self) -> BTreeMap { let mut out = BTreeMap::new(); for v in &self.variables.variables { // An object driven by several variables of the same kind reads from the first one // bound, so a capture is one value per variable and applies back the same way. let value = self.variables.objects_of(&v.name).into_iter().find_map(|id| self.variable_value(id, v.kind)); if let Some(value) = value { out.insert(v.name.clone(), value); } } out } }