Skip to main content

kozan_core/dom/
element_data.rs

1// Per-element data — the single "row" for each element node.
2//
3// All element fields in one struct, indexed by node slot.
4// Mutations update interned fields immediately.
5// Inline styles stored as plain PDB (cheap mutation),
6// converted to Arc<Locked<>> lazily before style traversal.
7
8use std::cell::Cell;
9use std::collections::HashSet;
10
11use selectors::matching::ElementSelectorFlags;
12use servo_arc::Arc;
13use style::Atom;
14use style::data::ElementDataWrapper;
15use style::properties::declaration_block::PropertyDeclarationBlock;
16use style::properties::{Importance, PropertyDeclaration};
17use style::shared_lock::Locked;
18use style_dom::ElementState;
19use web_atoms::{LocalName, Namespace};
20
21use crate::dom::attribute::AttributeCollection;
22
23/// HTML namespace.
24static HTML_NS: &str = "http://www.w3.org/1999/xhtml";
25
26/// All data for one element node. Stored in `Storage<ElementData>`.
27pub struct ElementData {
28    // ── Identity (interned at creation, updated on attribute change) ──
29    /// Raw tag name (compile-time known, e.g. "div").
30    pub(crate) tag_name: &'static str,
31
32    /// Interned tag name for Stylo selector matching.
33    pub(crate) local_name: LocalName,
34
35    /// Element namespace (HTML for all Kozan elements).
36    pub(crate) namespace: Namespace,
37
38    /// Interned id attribute. Updated on `set_attribute("id", ...)`.
39    pub(crate) id: Option<Atom>,
40
41    /// Interned class names. O(1) add/remove/contains.
42    /// Chrome equivalent: `DOMTokenList` backing store.
43    pub(crate) classes: HashSet<Atom>,
44
45    // ── State ──
46    /// Element state flags (focus, hover, active, enabled, etc.)
47    pub(crate) element_state: ElementState,
48
49    // Focus management not yet implemented.
50    #[allow(dead_code)]
51    pub(crate) is_focusable: bool,
52    #[allow(dead_code)]
53    pub(crate) tab_index: i32,
54
55    // ── DOM attributes ──
56    /// All attributes (id, class, data-*, custom).
57    pub(crate) attributes: AttributeCollection,
58
59    // ── Inline styles ──
60    //
61    // Two-layer design:
62    // - `inline_styles`: plain PDB, cheap to mutate (push = one Vec append)
63    // - `style_attribute`: Arc<Locked<PDB>> cache for Stylo, rebuilt before traversal
64    //
65    // Property setters modify `inline_styles` only. No Arc/Lock touching.
66    // Before traversal, `flush_inline_styles()` creates the locked cache.
67    /// Plain PDB for inline styles. Mutated by property setters.
68    pub(crate) inline_styles: PropertyDeclarationBlock,
69
70    /// Locked cache for Stylo's `style_attribute()`. Rebuilt from `inline_styles`.
71    pub(crate) style_attribute: Option<Arc<Locked<PropertyDeclarationBlock>>>,
72
73    /// Whether `inline_styles` has been modified since last flush.
74    pub(crate) inline_dirty: Cell<bool>,
75
76    // ── Stylo data (style traversal reads/writes these) ──
77    /// Stylo's computed styles + restyle damage + hints.
78    pub(crate) stylo_data: ElementDataWrapper,
79
80    /// Whether any descendant needs style processing.
81    pub(crate) dirty_descendants: Cell<bool>,
82
83    /// Whether this element has a state/attribute snapshot.
84    pub(crate) has_snapshot: Cell<bool>,
85
86    /// Whether the snapshot has been handled.
87    pub(crate) handled_snapshot: Cell<bool>,
88
89    /// Counter for bottom-up traversal.
90    pub(crate) children_to_process: Cell<isize>,
91
92    /// Selector flags set by Stylo during matching.
93    pub(crate) selector_flags: Cell<ElementSelectorFlags>,
94}
95
96impl ElementData {
97    pub(crate) fn new(tag_name: &'static str, is_focusable: bool) -> Self {
98        let mut state = ElementState::DEFINED;
99        if is_focusable {
100            state.insert(ElementState::ENABLED);
101        }
102
103        Self {
104            tag_name,
105            local_name: LocalName::from(tag_name),
106            namespace: Namespace::from(HTML_NS),
107            id: None,
108            classes: HashSet::new(),
109            element_state: state,
110            is_focusable,
111            tab_index: if is_focusable { 0 } else { -1 },
112            attributes: AttributeCollection::new(),
113            inline_styles: PropertyDeclarationBlock::new(),
114            style_attribute: None,
115            inline_dirty: Cell::new(false),
116            stylo_data: ElementDataWrapper::default(),
117            dirty_descendants: Cell::new(true),
118            has_snapshot: Cell::new(false),
119            handled_snapshot: Cell::new(false),
120            children_to_process: Cell::new(0),
121            selector_flags: Cell::new(ElementSelectorFlags::empty()),
122        }
123    }
124
125    // ── Inline style mutation (cheap — no Arc/Lock) ──
126
127    /// Push a property declaration into inline styles.
128    /// Just a Vec push — O(1), no allocation beyond PDB growth.
129    pub(crate) fn set_inline_property(&mut self, decl: PropertyDeclaration) {
130        self.inline_styles.push(decl, Importance::Normal);
131        self.inline_dirty.set(true);
132    }
133
134    /// Overwrite all inline styles from a CSS string.
135    pub(crate) fn set_inline_from_css(
136        &mut self,
137        value: &str,
138        guard: &style::shared_lock::SharedRwLock,
139    ) {
140        let url = url::Url::parse("kozan://inline").expect("hardcoded URL is always valid");
141        let url_data = style::stylesheets::UrlExtraData(Arc::new(url));
142        self.inline_styles = style::properties::parse_style_attribute(
143            value,
144            &url_data,
145            None,
146            selectors::matching::QuirksMode::NoQuirks,
147            style::stylesheets::CssRuleType::Style,
148        );
149        self.inline_dirty.set(true);
150        // Also rebuild the cache immediately for `style="..."` attribute.
151        self.style_attribute = Some(Arc::new(guard.wrap(self.inline_styles.clone())));
152        self.inline_dirty.set(false);
153    }
154
155    /// Clear all inline styles.
156    pub(crate) fn clear_inline_styles(&mut self) {
157        self.inline_styles = PropertyDeclarationBlock::new();
158        self.style_attribute = None;
159        self.inline_dirty.set(false);
160    }
161
162    /// Flush `inline_styles` → `style_attribute` cache (called before traversal).
163    pub(crate) fn flush_inline_styles(&mut self, guard: &style::shared_lock::SharedRwLock) {
164        if !self.inline_dirty.get() {
165            return;
166        }
167        if self.inline_styles.is_empty() {
168            self.style_attribute = None;
169        } else {
170            self.style_attribute = Some(Arc::new(guard.wrap(self.inline_styles.clone())));
171        }
172        self.inline_dirty.set(false);
173    }
174
175    // ── Restyle marking ──
176
177    pub(crate) fn mark_for_restyle(&self) {
178        use style::invalidation::element::restyle_hints::RestyleHint;
179        let mut data = self.stylo_data.borrow_mut();
180        data.hint.insert(RestyleHint::RESTYLE_SELF);
181    }
182
183    // ── Attribute change hooks ──
184
185    pub(crate) fn on_attribute_set(
186        &mut self,
187        name: &str,
188        value: &str,
189        guard: &style::shared_lock::SharedRwLock,
190    ) -> bool {
191        match name {
192            "id" => {
193                self.id = Some(Atom::from(value));
194                true
195            }
196            "class" => {
197                self.classes = value.split_ascii_whitespace().map(Atom::from).collect();
198                true
199            }
200            "style" => {
201                self.set_inline_from_css(value, guard);
202                true
203            }
204            _ => false,
205        }
206    }
207
208    // ── ClassList — Chrome: DOMTokenList (element.classList) ──
209
210    /// Add a class. Returns true if it was newly inserted.
211    pub(crate) fn class_add(&mut self, name: &str) -> bool {
212        self.classes.insert(Atom::from(name))
213    }
214
215    /// Remove a class. Returns true if it was present.
216    pub(crate) fn class_remove(&mut self, name: &str) -> bool {
217        self.classes.remove(&Atom::from(name))
218    }
219
220    /// Toggle a class. Returns true if now present, false if removed.
221    pub(crate) fn class_toggle(&mut self, name: &str) -> bool {
222        let atom = Atom::from(name);
223        if !self.classes.remove(&atom) {
224            self.classes.insert(atom);
225            true
226        } else {
227            false
228        }
229    }
230
231    /// Check if a class is present.
232    pub(crate) fn class_contains(&self, name: &str) -> bool {
233        self.classes.contains(&Atom::from(name))
234    }
235
236    pub(crate) fn on_attribute_removed(&mut self, name: &str) -> bool {
237        match name {
238            "id" => {
239                self.id = None;
240                true
241            }
242            "class" => {
243                self.classes.clear();
244                true
245            }
246            "style" => {
247                self.clear_inline_styles();
248                true
249            }
250            _ => false,
251        }
252    }
253}