Skip to main content

kozan_core/dom/
traits.rs

1//! DOM trait hierarchy — Chrome's class hierarchy as Rust traits.
2//!
3//! ```text
4//! HasHandle          ← fn handle() -> Handle   (base, derives generate this)
5//!   EventTarget      ← on(), off(), dispatch()  [in events/ module]
6//!     Node           ← parent, siblings, raw()  [Chrome: core/dom/node.h]
7//!       ContainerNode ← append, children         [Chrome: core/dom/container_node.h]
8//!         Element     ← attributes, tag name     [Chrome: core/dom/element.h]
9//! ```
10//!
11//! `HasHandle` lives here. `EventTarget` lives in `events/`.
12//! Both `Node` and `Window` extend `EventTarget` (same as Chrome).
13
14use crate::dom::handle::Handle;
15use crate::dom::node::NodeType;
16use crate::events::EventTarget;
17use crate::id::RawId;
18
19/// The single base trait: provides access to the inner [`Handle`].
20///
21/// This is the ONE method that derive macros generate.
22/// Every other trait in the hierarchy builds on top with default
23/// implementations — zero boilerplate for implementors.
24///
25/// # Implementors
26///
27/// - All DOM node types (`Text`, `HtmlDivElement`, `HtmlButtonElement`, ...)
28/// - `Window` (future)
29/// - Custom elements defined by users
30pub trait HasHandle: Copy + 'static {
31    /// Get the inner [`Handle`] for this node/target.
32    ///
33    /// The Handle provides low-level access to the document's parallel arenas.
34    /// Most users should call trait methods instead of using the Handle directly.
35    fn handle(&self) -> Handle;
36}
37
38/// All DOM node types: Text, Element, Document.
39///
40/// Like Chrome's `Node` class (`core/dom/node.h`).
41/// Extends [`EventTarget`] — every node can receive events.
42/// Provides tree position (parent, siblings) and identity (raw ID, node kind).
43///
44/// # What implements `Node`
45///
46/// - [`Text`](crate::Text) — text content, no children
47/// - All elements via [`Element`] → [`ContainerNode`] → `Node`
48/// - Document root (accessed via [`Document::root()`](crate::Document::root))
49pub trait Node: EventTarget {
50    /// Check if this node is still alive in the document.
51    ///
52    /// Returns `false` after [`destroy()`](Self::destroy) is called.
53    /// All other methods return defaults (None, empty, false) on dead nodes.
54    fn is_alive(&self) -> bool {
55        self.handle().is_alive()
56    }
57
58    /// Get the parent node, or `None` if this is the root or detached.
59    fn parent(&self) -> Option<Handle> {
60        self.handle().parent()
61    }
62
63    /// Get the next sibling in the parent's child list.
64    fn next_sibling(&self) -> Option<Handle> {
65        self.handle().next_sibling()
66    }
67
68    /// Get the previous sibling in the parent's child list.
69    fn prev_sibling(&self) -> Option<Handle> {
70        self.handle().prev_sibling()
71    }
72
73    /// Detach this node from its parent. The node stays alive but is no longer in the tree.
74    fn detach(&self) {
75        self.handle().detach();
76    }
77
78    /// Destroy this node. Frees the slot — all handles to it become stale.
79    fn destroy(&self) {
80        self.handle().destroy();
81    }
82
83    /// Get the raw ID for cross-thread communication.
84    ///
85    /// [`RawId`] is `Send` — use it to reference nodes from background tasks.
86    /// Resolve back to a Handle via [`Document::resolve()`](crate::Document::resolve).
87    fn raw(&self) -> RawId {
88        self.handle().raw()
89    }
90
91    /// Get the DOM node type (Element, Text, Document, etc.).
92    fn node_kind(&self) -> Option<NodeType> {
93        self.handle().node_kind()
94    }
95
96    /// Is this an element node?
97    fn is_element(&self) -> bool {
98        self.handle().is_element()
99    }
100
101    /// Is this a text node?
102    fn is_text(&self) -> bool {
103        self.handle().is_text()
104    }
105
106    /// Is this the document root?
107    fn is_document(&self) -> bool {
108        self.handle().is_document()
109    }
110
111    // ---- Style ----
112
113    /// Per-property style access — like JavaScript's `element.style`.
114    fn style(&self) -> crate::styling::builder::StyleAccess {
115        self.handle().style()
116    }
117
118    /// Apply styles via closure and return self for chaining.
119    ///
120    /// ```ignore
121    /// doc.div()
122    ///     .styled(|s| s.width(px(200.0)).height(px(100.0)).background_color(rgb8(255, 0, 0)))
123    ///     .child(doc.div().styled(|s| s.width(px(50.0)).height(px(50.0))))
124    /// ```
125    fn styled(self, f: impl FnOnce(&mut crate::styling::builder::StyleAccess)) -> Self {
126        f(&mut self.handle().style());
127        self
128    }
129}
130
131/// Nodes that can have children: Element and Document.
132///
133/// Like Chrome's `ContainerNode` (`core/dom/container_node.h`).
134/// Text nodes do NOT implement this — `text.append(child)` is a **compile error**.
135pub trait ContainerNode: Node {
136    /// Append `child` as the last child of this node.
137    ///
138    /// If `child` is already attached elsewhere, it is detached first (reparenting).
139    fn append(&self, child: impl HasHandle) {
140        self.handle().append(child.handle());
141    }
142
143    /// Append a child and return self for chaining (GPUI-style).
144    ///
145    /// Elements are `Copy` — zero-cost value return.
146    ///
147    /// ```ignore
148    /// doc.root().child(
149    ///     doc.div()
150    ///         .child(doc.div().bg(rgb8(255, 0, 0)))
151    ///         .child(doc.div().bg(rgb8(0, 255, 0)))
152    /// );
153    /// ```
154    fn child(self, child: impl HasHandle) -> Self {
155        self.handle().append(child.handle());
156        self
157    }
158
159    /// Append multiple children at once.
160    fn add_children<I, C>(self, items: I) -> Self
161    where
162        I: IntoIterator<Item = C>,
163        C: HasHandle,
164    {
165        for c in items {
166            self.handle().append(c.handle());
167        }
168        self
169    }
170
171    /// Insert `child` before this node in the parent's child list.
172    fn insert_before(&self, child: impl HasHandle) {
173        self.handle().insert_before(child.handle());
174    }
175
176    /// Get the first child, or `None` if empty.
177    fn first_child(&self) -> Option<Handle> {
178        self.handle().first_child()
179    }
180
181    /// Get the last child, or `None` if empty.
182    fn last_child(&self) -> Option<Handle> {
183        self.handle().last_child()
184    }
185
186    /// Collect all children as Handles.
187    fn children(&self) -> Vec<Handle> {
188        self.handle().children()
189    }
190}
191
192/// Element nodes — attributes, tag name, class, id.
193///
194/// Like Chrome's `Element` (`core/dom/element.h`).
195/// Text and Document do NOT implement this.
196pub trait Element: ContainerNode {
197    /// Element-specific data type (e.g., `ButtonData`). Use `()` if none.
198    type Data: Default + 'static;
199
200    /// The default tag name (e.g., `"div"`, `"button"`, `"input"`).
201    ///
202    /// Empty string means this element type has no fixed tag and MUST be
203    /// created via `Document::create_with_tag()` (e.g., `HtmlHeadingElement`
204    /// represents h1-h6 — the tag varies at runtime).
205    ///
206    /// Chrome: tag name is always runtime data on `Element::tagName()`.
207    /// This const is a Kozan convenience for single-tag elements.
208    const TAG_NAME: &'static str = "";
209
210    /// Whether this element is focusable by default.
211    const IS_FOCUSABLE: bool = false;
212
213    /// Wrap a raw Handle into this element's typed handle.
214    ///
215    /// Called by [`Document::create()`](crate::Document::create).
216    fn from_handle(handle: Handle) -> Self;
217
218    /// Get the tag name (runtime — reads from `ElementData`).
219    ///
220    /// Usually matches `TAG_NAME`, but can differ for elements created
221    /// via `Document::create_with_tag()` (e.g., `HtmlHeadingElement` represents h1-h6).
222    /// Chrome: `Element::tagName()` always returns the actual tag.
223    fn tag_name(&self) -> &'static str {
224        self.handle().tag_name().unwrap_or(Self::TAG_NAME)
225    }
226
227    /// Get the `id` attribute.
228    fn id(&self) -> String {
229        self.handle().id()
230    }
231
232    /// Set the `id` attribute.
233    fn set_id(&self, id: impl Into<String>) {
234        self.handle().set_id(id);
235    }
236
237    /// Get the `class` attribute.
238    fn class_name(&self) -> String {
239        self.handle().class_name()
240    }
241
242    /// Set the `class` attribute.
243    fn set_class_name(&self, class: impl Into<String>) {
244        self.handle().set_class_name(class);
245    }
246
247    // ── ClassList — Chrome: element.classList (DOMTokenList) ──
248
249    /// Add a CSS class. No-op if already present.
250    /// Chrome: `element.classList.add("name")`.
251    fn class_add(&self, name: &str) {
252        self.handle().class_add(name);
253    }
254
255    /// Remove a CSS class. No-op if absent.
256    /// Chrome: `element.classList.remove("name")`.
257    fn class_remove(&self, name: &str) {
258        self.handle().class_remove(name);
259    }
260
261    /// Toggle a CSS class. Returns true if now present.
262    /// Chrome: `element.classList.toggle("name")`.
263    fn class_toggle(&self, name: &str) -> bool {
264        self.handle().class_toggle(name)
265    }
266
267    /// Check if an element has a CSS class.
268    /// Chrome: `element.classList.contains("name")`.
269    fn class_contains(&self, name: &str) -> bool {
270        self.handle().class_contains(name)
271    }
272
273    // ── Attributes ──
274
275    /// Returns the attribute value for `name`, or `None` if absent.
276    fn attribute(&self, name: &str) -> Option<String> {
277        self.handle().attribute(name)
278    }
279
280    /// Set an attribute.
281    fn set_attribute(&self, name: &str, value: impl Into<String>) {
282        self.handle().set_attribute(name, value);
283    }
284
285    /// Remove an attribute. Returns the old value.
286    fn remove_attribute(&self, name: &str) -> Option<String> {
287        self.handle().remove_attribute(name)
288    }
289}