Skip to main content

azul_core/
dom.rs

1//! Defines the core Document Object Model (DOM) structures.
2//!
3//! This module is responsible for representing the UI as a tree of nodes,
4//! similar to the HTML DOM. It includes definitions for node types, event handling
5//! and the main `Dom` and `CompactDom` structures.
6
7#[cfg(not(feature = "std"))]
8use alloc::string::ToString;
9use alloc::{boxed::Box, collections::btree_map::BTreeMap, string::String, vec::Vec};
10use core::{
11    fmt,
12    hash::{Hash, Hasher},
13    iter::FromIterator,
14    mem,
15    sync::atomic::{AtomicUsize, Ordering},
16};
17
18use azul_css::{
19    css::{BoxOrStatic, Css, NodeTypeTag},
20    codegen::format::GetHash,
21    props::{
22        basic::{FloatValue, FontRef},
23        layout::{LayoutDisplay, LayoutFloat, LayoutPosition},
24        property::CssProperty,
25    },
26    AzString, OptionString,
27};
28
29// Re-exported from a11y.rs and events.rs
30pub use crate::a11y::*;
31pub use crate::events::{
32    ApplicationEventFilter, ComponentEventFilter, EventFilter, FocusEventFilter, HoverEventFilter,
33    WindowEventFilter,
34};
35pub use crate::id::{Node, NodeHierarchy, NodeId};
36use crate::{
37    callbacks::{
38        CoreCallback, CoreCallbackData, CoreCallbackDataVec, CoreCallbackType, VirtualViewCallback,
39        VirtualViewCallbackType,
40    },
41    geom::LogicalPosition,
42    id::{NodeDataContainer, NodeDataContainerRef, NodeDataContainerRefMut},
43    menu::Menu,
44    prop_cache::{CssPropertyCache, CssPropertyCachePtr},
45    refany::{OptionRefAny, RefAny},
46    resources::{
47        image_ref_get_hash, CoreImageCallback, ImageMask, ImageRef, ImageRefHash, RendererResources,
48    },
49    styled_dom::{
50        CompactDom, NodeHierarchyItemId, StyleFontFamilyHash, StyledDom, StyledNode,
51        StyledNodeState,
52    },
53    window::OptionVirtualKeyCodeCombo,
54};
55pub use azul_css::dynamic_selector::{CssPropertyWithConditions, CssPropertyWithConditionsVec};
56
57static TAG_ID: AtomicUsize = AtomicUsize::new(1);
58
59/// Strongly-typed input element types for HTML `<input>` elements.
60#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
61#[repr(C)]
62pub enum InputType {
63    /// Text input (default)
64    Text,
65    /// Button
66    Button,
67    /// Checkbox
68    Checkbox,
69    /// Color picker
70    Color,
71    /// Date picker
72    Date,
73    /// Date and time picker
74    Datetime,
75    /// Date and time picker (local)
76    DatetimeLocal,
77    /// Email address input
78    Email,
79    /// File upload
80    File,
81    /// Hidden input
82    Hidden,
83    /// Image button
84    Image,
85    /// Month picker
86    Month,
87    /// Number input
88    Number,
89    /// Password input
90    Password,
91    /// Radio button
92    Radio,
93    /// Range slider
94    Range,
95    /// Reset button
96    Reset,
97    /// Search input
98    Search,
99    /// Submit button
100    Submit,
101    /// Telephone number input
102    Tel,
103    /// Time picker
104    Time,
105    /// URL input
106    Url,
107    /// Week picker
108    Week,
109}
110
111impl InputType {
112    /// Returns the HTML attribute value for this input type
113    #[must_use] pub const fn as_str(&self) -> &'static str {
114        match self {
115            Self::Text => "text",
116            Self::Button => "button",
117            Self::Checkbox => "checkbox",
118            Self::Color => "color",
119            Self::Date => "date",
120            Self::Datetime => "datetime",
121            Self::DatetimeLocal => "datetime-local",
122            Self::Email => "email",
123            Self::File => "file",
124            Self::Hidden => "hidden",
125            Self::Image => "image",
126            Self::Month => "month",
127            Self::Number => "number",
128            Self::Password => "password",
129            Self::Radio => "radio",
130            Self::Range => "range",
131            Self::Reset => "reset",
132            Self::Search => "search",
133            Self::Submit => "submit",
134            Self::Tel => "tel",
135            Self::Time => "time",
136            Self::Url => "url",
137            Self::Week => "week",
138        }
139    }
140}
141
142#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
143#[repr(C)]
144pub struct TagId {
145    pub inner: u64,
146}
147
148impl ::core::fmt::Display for TagId {
149    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150        f.debug_struct("TagId").field("inner", &self.inner).finish()
151    }
152}
153
154impl_option!(
155    TagId,
156    OptionTagId,
157    [Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash]
158);
159
160impl TagId {
161    #[must_use] pub const fn into_crate_internal(&self) -> Self {
162        Self { inner: self.inner }
163    }
164    #[must_use] pub const fn from_crate_internal(t: Self) -> Self {
165        t
166    }
167
168    /// Creates a new, unique hit-testing tag ID.
169    /// Wraps around to 1 on overflow (0 is reserved for "no tag").
170    ///
171    /// AUDIT: the wrap is only reachable after 2^64 - 1 allocations (a process
172    /// running long enough to exhaust the counter is not realistic), but note
173    /// that on wrap the freshly-issued id could theoretically collide with a
174    /// still-live tag from very early in the process. This is left as a
175    /// documented, non-triggerable limitation rather than adding a live-tag
176    /// registry to detect collisions on every allocation. AUDIT-TODO: revisit
177    /// if `TagId` is ever narrowed below 64 bits.
178    pub fn unique() -> Self {
179        loop {
180            let current = TAG_ID.load(Ordering::SeqCst);
181            let next = if current == usize::MAX { 1 } else { current + 1 };
182            if TAG_ID.compare_exchange(current, next, Ordering::SeqCst, Ordering::SeqCst).is_ok() {
183                return Self { inner: current as u64 };
184            }
185        }
186    }
187}
188
189/// Same as the `TagId`, but only for scrollable nodes.
190/// This provides a typed distinction for tags associated with scrolling containers.
191#[derive(Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
192#[repr(C)]
193pub struct ScrollTagId {
194    pub inner: TagId,
195}
196
197impl ::core::fmt::Display for ScrollTagId {
198    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
199        f.debug_struct("ScrollTagId")
200            .field("inner", &self.inner)
201            .finish()
202    }
203}
204
205impl ::core::fmt::Debug for ScrollTagId {
206    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
207        write!(f, "{self}")
208    }
209}
210
211impl ScrollTagId {
212    /// Creates a new, unique scroll tag ID. Note that this should not
213    /// be used for identifying nodes, use the `DomNodeHash` instead.
214    #[must_use] pub fn unique() -> Self {
215        Self {
216            inner: TagId::unique(),
217        }
218    }
219}
220
221/// Orientation of a scrollbar.
222#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
223#[repr(C)]
224pub enum ScrollbarOrientation {
225    Horizontal,
226    Vertical,
227}
228
229/// Calculated hash of a DOM node, used for identifying identical DOM
230/// nodes across frames for efficient diffing and state preservation.
231#[derive(Copy, Clone, Hash, PartialEq, Eq, Ord, PartialOrd)]
232#[repr(C)]
233pub struct DomNodeHash {
234    pub inner: u64,
235}
236
237impl ::core::fmt::Debug for DomNodeHash {
238    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
239        write!(f, "DomNodeHash({})", self.inner)
240    }
241}
242
243/// List of core DOM node types built into `azul`.
244/// This enum defines the building blocks of the UI, similar to HTML tags.
245#[derive(Debug, Clone, PartialEq, Hash, Eq, PartialOrd, Ord)]
246#[repr(C, u8)]
247pub enum NodeType {
248    // Root and container elements
249    /// Root HTML element.
250    Html,
251    /// Document head (metadata container).
252    Head,
253    /// Root element of the document body.
254    Body,
255    /// Generic block-level container.
256    Div,
257    /// Paragraph.
258    P,
259    /// Article content.
260    Article,
261    /// Section of a document.
262    Section,
263    /// Navigation links.
264    Nav,
265    /// Sidebar/tangential content.
266    Aside,
267    /// Header section.
268    Header,
269    /// Footer section.
270    Footer,
271    /// Main content.
272    Main,
273    /// Figure with optional caption.
274    Figure,
275    /// Caption for figure element.
276    FigCaption,
277    /// Headings.
278    H1,
279    H2,
280    H3,
281    H4,
282    H5,
283    H6,
284    /// Line break.
285    Br,
286    /// Horizontal rule.
287    Hr,
288    /// Preformatted text.
289    Pre,
290    /// Block quote.
291    BlockQuote,
292    /// Address.
293    Address,
294    /// Details disclosure widget.
295    Details,
296    /// Summary for details element.
297    Summary,
298    /// Dialog box or window.
299    Dialog,
300
301    // List elements
302    /// Unordered list.
303    Ul,
304    /// Ordered list.
305    Ol,
306    /// List item.
307    Li,
308    /// Definition list.
309    Dl,
310    /// Definition term.
311    Dt,
312    /// Definition description.
313    Dd,
314    /// Menu list.
315    Menu,
316    /// Menu item.
317    MenuItem,
318    /// Directory list (deprecated).
319    Dir,
320
321    // Table elements
322    /// Table container.
323    Table,
324    /// Table caption.
325    Caption,
326    /// Table header.
327    THead,
328    /// Table body.
329    TBody,
330    /// Table footer.
331    TFoot,
332    /// Table row.
333    Tr,
334    /// Table header cell.
335    Th,
336    /// Table data cell.
337    Td,
338    /// Table column group.
339    ColGroup,
340    /// Table column.
341    Col,
342
343    // Form elements
344    /// Form container.
345    Form,
346    /// Form fieldset.
347    FieldSet,
348    /// Fieldset legend.
349    Legend,
350    /// Label for form controls.
351    Label,
352    /// Input control.
353    Input,
354    /// Button control.
355    Button,
356    /// Select dropdown.
357    Select,
358    /// Option group.
359    OptGroup,
360    /// Select option.
361    SelectOption,
362    /// Multiline text input.
363    TextArea,
364    /// Form output element.
365    Output,
366    /// Progress indicator.
367    Progress,
368    /// Scalar measurement within a known range.
369    Meter,
370    /// List of predefined options for input.
371    DataList,
372
373    // Inline elements
374    /// Generic inline container.
375    Span,
376    /// Anchor/hyperlink.
377    A,
378    /// Emphasized text.
379    Em,
380    /// Strongly emphasized text.
381    Strong,
382    /// Bold text (deprecated - use `Dom::create_strong()` for semantic importance).
383    B,
384    /// Italic text (deprecated - use `Dom::create_em()` for emphasis or `Dom::create_cite()` for citations).
385    I,
386    /// Underline text.
387    U,
388    /// Strikethrough text.
389    S,
390    /// Marked/highlighted text.
391    Mark,
392    /// Deleted text.
393    Del,
394    /// Inserted text.
395    Ins,
396    /// Code.
397    Code,
398    /// Sample output.
399    Samp,
400    /// Keyboard input.
401    Kbd,
402    /// Variable.
403    Var,
404    /// Citation.
405    Cite,
406    /// Defining instance of a term.
407    Dfn,
408    /// Abbreviation.
409    Abbr,
410    /// Acronym.
411    Acronym,
412    /// Inline quotation.
413    Q,
414    /// Date/time.
415    Time,
416    /// Subscript.
417    Sub,
418    /// Superscript.
419    Sup,
420    /// Small text (deprecated - use CSS `font-size` instead).
421    Small,
422    /// Big text (deprecated - use CSS `font-size` instead).
423    Big,
424    /// Bi-directional override.
425    Bdo,
426    /// Bi-directional isolate.
427    Bdi,
428    /// Word break opportunity.
429    Wbr,
430    /// Ruby annotation.
431    Ruby,
432    /// Ruby text.
433    Rt,
434    /// Ruby text container.
435    Rtc,
436    /// Ruby parenthesis.
437    Rp,
438    /// Machine-readable data.
439    Data,
440
441    // Embedded content
442    /// Canvas for graphics.
443    Canvas,
444    /// Embedded object.
445    Object,
446    /// Embedded object parameter.
447    Param,
448    /// External resource embed.
449    Embed,
450    /// Audio content.
451    Audio,
452    /// Video content.
453    Video,
454    /// Media source.
455    Source,
456    /// Text track for media.
457    Track,
458    /// Image map.
459    Map,
460    /// Image map area.
461    Area,
462    // SVG elements — container
463    /// SVG `<svg>` root graphics container.
464    Svg,
465    /// SVG `<g>` group element.
466    SvgG,
467    /// SVG `<defs>` — reusable definitions (not rendered directly).
468    SvgDefs,
469    /// SVG `<symbol>` — like defs but with its own viewBox.
470    SvgSymbol,
471    /// SVG `<use>` — references and instantiates a defs element.
472    SvgUse,
473    /// SVG `<switch>` — conditional processing.
474    SvgSwitch,
475
476    // SVG elements — shape
477    /// SVG `<path>` element.
478    SvgPath,
479    /// SVG `<circle>` element.
480    SvgCircle,
481    /// SVG `<rect>` element.
482    SvgRect,
483    /// SVG `<ellipse>` element.
484    SvgEllipse,
485    /// SVG `<line>` element.
486    SvgLine,
487    /// SVG `<polygon>` element.
488    SvgPolygon,
489    /// SVG `<polyline>` element.
490    SvgPolyline,
491
492    // SVG elements — text
493    /// SVG `<text>` element.
494    SvgText(AzString),
495    /// SVG `<tspan>` element.
496    SvgTspan,
497    /// SVG `<textPath>` element.
498    SvgTextPath,
499
500    // SVG elements — paint servers
501    /// SVG `<linearGradient>` element.
502    SvgLinearGradient,
503    /// SVG `<radialGradient>` element.
504    SvgRadialGradient,
505    /// SVG `<stop>` gradient stop element.
506    SvgStop,
507    /// SVG `<pattern>` element.
508    SvgPattern,
509
510    // SVG elements — clipping / masking
511    /// SVG `<clipPath>` element.
512    SvgClipPathElement,
513    /// SVG `<mask>` element.
514    SvgMask,
515
516    // SVG elements — filter
517    /// SVG `<filter>` container element.
518    SvgFilter,
519    /// SVG `<feBlend>`.
520    SvgFeBlend,
521    /// SVG `<feColorMatrix>`.
522    SvgFeColorMatrix,
523    /// SVG `<feComponentTransfer>`.
524    SvgFeComponentTransfer,
525    /// SVG `<feComposite>`.
526    SvgFeComposite,
527    /// SVG `<feConvolveMatrix>`.
528    SvgFeConvolveMatrix,
529    /// SVG `<feDiffuseLighting>`.
530    SvgFeDiffuseLighting,
531    /// SVG `<feDisplacementMap>`.
532    SvgFeDisplacementMap,
533    /// SVG `<feDistantLight>`.
534    SvgFeDistantLight,
535    /// SVG `<feDropShadow>`.
536    SvgFeDropShadow,
537    /// SVG `<feFlood>`.
538    SvgFeFlood,
539    /// SVG `<feFuncR>`.
540    SvgFeFuncR,
541    /// SVG `<feFuncG>`.
542    SvgFeFuncG,
543    /// SVG `<feFuncB>`.
544    SvgFeFuncB,
545    /// SVG `<feFuncA>`.
546    SvgFeFuncA,
547    /// SVG `<feGaussianBlur>`.
548    SvgFeGaussianBlur,
549    /// SVG `<feImage>`.
550    SvgFeImage,
551    /// SVG `<feMerge>`.
552    SvgFeMerge,
553    /// SVG `<feMergeNode>`.
554    SvgFeMergeNode,
555    /// SVG `<feMorphology>`.
556    SvgFeMorphology,
557    /// SVG `<feOffset>`.
558    SvgFeOffset,
559    /// SVG `<fePointLight>`.
560    SvgFePointLight,
561    /// SVG `<feSpecularLighting>`.
562    SvgFeSpecularLighting,
563    /// SVG `<feSpotLight>`.
564    SvgFeSpotLight,
565    /// SVG `<feTile>`.
566    SvgFeTile,
567    /// SVG `<feTurbulence>`.
568    SvgFeTurbulence,
569
570    // SVG elements — marker / image / foreign
571    /// SVG `<marker>` element (not the CSS `::marker` pseudo-element).
572    SvgMarker,
573    /// SVG `<image>` element (embedded raster image in SVG).
574    SvgImage(ImageRef),
575    /// SVG `<foreignObject>` element.
576    SvgForeignObject,
577
578    // SVG elements — descriptive / structural
579    /// SVG `<title>` element (distinct from HTML `<title>`).
580    SvgTitle,
581    /// SVG `<desc>` element.
582    SvgDesc,
583    /// SVG `<metadata>` element.
584    SvgMetadata,
585    /// SVG `<a>` hyperlink element (distinct from HTML `<a>`).
586    SvgA,
587    /// SVG `<view>` element.
588    SvgView,
589    /// SVG `<style>` element (distinct from HTML `<style>`).
590    SvgStyle,
591    /// SVG `<script>` element (distinct from HTML `<script>`).
592    SvgScript,
593
594    // SVG elements — animation
595    /// SVG `<animate>` element.
596    SvgAnimate,
597    /// SVG `<animateMotion>` element.
598    SvgAnimateMotion,
599    /// SVG `<animateTransform>` element.
600    SvgAnimateTransform,
601    /// SVG `<set>` element.
602    SvgSet,
603    /// SVG `<mpath>` element.
604    SvgMpath,
605
606    // Metadata elements
607    /// Document title.
608    Title,
609    /// Metadata.
610    Meta,
611    /// External resource link.
612    Link,
613    /// Embedded or referenced script.
614    Script,
615    /// Style information.
616    Style,
617    /// Base URL for relative URLs.
618    Base,
619
620    // Pseudo-elements (transformed into real elements)
621    /// `::before` pseudo-element.
622    Before,
623    /// `::after` pseudo-element.
624    After,
625    /// `::marker` pseudo-element.
626    Marker,
627    /// `::placeholder` pseudo-element.
628    Placeholder,
629
630    // Special content types
631    /// Text content, `::text`.
632    /// Uses `BoxOrStatic` to keep `NodeType` small (~16B vs ~72B with inline `AzString`)
633    /// and to allow static text references in the future.
634    Text(BoxOrStatic<AzString>),
635    /// Image element, `::image`.
636    /// Uses `BoxOrStatic` to keep `NodeType` small.
637    Image(BoxOrStatic<ImageRef>),
638    /// `VirtualView` (embedded content) - payload stored in `NodeDataExt.virtual_view`
639    VirtualView,
640    /// Icon element - resolved to actual content by `IconProvider`.
641    /// The string is the icon name (e.g., "home", "settings", "search").
642    /// Uses `BoxOrStatic` to keep `NodeType` small.
643    Icon(BoxOrStatic<AzString>),
644    /// Invisible probe node that signals "this subtree needs the user's
645    /// GPS / network location". Zero-size in layout, skipped in the
646    /// display list. The `GeolocationManager` walks the styled DOM for
647    /// these at end-of-layout and starts / stops the matching native
648    /// subscription. See `SUPER_PLAN_2.md` §1.5 + research/08.
649    GeolocationProbe(crate::geolocation::GeolocationProbeConfig),
650}
651
652/// Type alias: `BoxOrStatic<ImageRef>` — used by `NodeType::Image` for FFI monomorphization.
653pub type BoxOrStaticImageRef = BoxOrStatic<ImageRef>;
654
655impl_option!(NodeType, OptionNodeType, copy = false, [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]);
656
657impl NodeType {
658    #[allow(clippy::too_many_lines)] // large but cohesive: single-purpose parser/builder/dispatch (one branch per input variant)
659    fn to_library_owned_nodetype(&self) -> Self {
660        use self::NodeType::{Html, Head, Body, Div, P, Article, Section, Nav, Aside, Header, Footer, Main, Figure, FigCaption, H1, H2, H3, H4, H5, H6, Br, Hr, Pre, BlockQuote, Address, Details, Summary, Dialog, Ul, Ol, Li, Dl, Dt, Dd, Menu, MenuItem, Dir, Table, Caption, THead, TBody, TFoot, Tr, Th, Td, ColGroup, Col, Form, FieldSet, Legend, Label, Input, Button, Select, OptGroup, SelectOption, TextArea, Output, Progress, Meter, DataList, Span, A, Em, Strong, B, I, U, S, Mark, Del, Ins, Code, Samp, Kbd, Var, Cite, Dfn, Abbr, Acronym, Q, Time, Sub, Sup, Small, Big, Bdo, Bdi, Wbr, Ruby, Rt, Rtc, Rp, Data, Canvas, Object, Param, Embed, Audio, Video, Source, Track, Map, Area, Svg, SvgG, SvgDefs, SvgSymbol, SvgUse, SvgSwitch, SvgPath, SvgCircle, SvgRect, SvgEllipse, SvgLine, SvgPolygon, SvgPolyline, SvgText, SvgTspan, SvgTextPath, SvgLinearGradient, SvgRadialGradient, SvgStop, SvgPattern, SvgClipPathElement, SvgMask, SvgFilter, SvgFeBlend, SvgFeColorMatrix, SvgFeComponentTransfer, SvgFeComposite, SvgFeConvolveMatrix, SvgFeDiffuseLighting, SvgFeDisplacementMap, SvgFeDistantLight, SvgFeDropShadow, SvgFeFlood, SvgFeFuncR, SvgFeFuncG, SvgFeFuncB, SvgFeFuncA, SvgFeGaussianBlur, SvgFeImage, SvgFeMerge, SvgFeMergeNode, SvgFeMorphology, SvgFeOffset, SvgFePointLight, SvgFeSpecularLighting, SvgFeSpotLight, SvgFeTile, SvgFeTurbulence, SvgMarker, SvgImage, SvgForeignObject, SvgTitle, SvgDesc, SvgMetadata, SvgA, SvgView, SvgStyle, SvgScript, SvgAnimate, SvgAnimateMotion, SvgAnimateTransform, SvgSet, SvgMpath, Title, Meta, Link, Script, Style, Base, Before, After, Marker, Placeholder, Text, Image, VirtualView, Icon, GeolocationProbe};
661        match self {
662            Html => Html,
663            Head => Head,
664            Body => Body,
665            Div => Div,
666            P => P,
667            Article => Article,
668            Section => Section,
669            Nav => Nav,
670            Aside => Aside,
671            Header => Header,
672            Footer => Footer,
673            Main => Main,
674            Figure => Figure,
675            FigCaption => FigCaption,
676            H1 => H1,
677            H2 => H2,
678            H3 => H3,
679            H4 => H4,
680            H5 => H5,
681            H6 => H6,
682            Br => Br,
683            Hr => Hr,
684            Pre => Pre,
685            BlockQuote => BlockQuote,
686            Address => Address,
687            Details => Details,
688            Summary => Summary,
689            Dialog => Dialog,
690            Ul => Ul,
691            Ol => Ol,
692            Li => Li,
693            Dl => Dl,
694            Dt => Dt,
695            Dd => Dd,
696            Menu => Menu,
697            MenuItem => MenuItem,
698            Dir => Dir,
699            Table => Table,
700            Caption => Caption,
701            THead => THead,
702            TBody => TBody,
703            TFoot => TFoot,
704            Tr => Tr,
705            Th => Th,
706            Td => Td,
707            ColGroup => ColGroup,
708            Col => Col,
709            Form => Form,
710            FieldSet => FieldSet,
711            Legend => Legend,
712            Label => Label,
713            Input => Input,
714            Button => Button,
715            Select => Select,
716            OptGroup => OptGroup,
717            SelectOption => SelectOption,
718            TextArea => TextArea,
719            Output => Output,
720            Progress => Progress,
721            Meter => Meter,
722            DataList => DataList,
723            Span => Span,
724            A => A,
725            Em => Em,
726            Strong => Strong,
727            B => B,
728            I => I,
729            U => U,
730            S => S,
731            Mark => Mark,
732            Del => Del,
733            Ins => Ins,
734            Code => Code,
735            Samp => Samp,
736            Kbd => Kbd,
737            Var => Var,
738            Cite => Cite,
739            Dfn => Dfn,
740            Abbr => Abbr,
741            Acronym => Acronym,
742            Q => Q,
743            Time => Time,
744            Sub => Sub,
745            Sup => Sup,
746            Small => Small,
747            Big => Big,
748            Bdo => Bdo,
749            Bdi => Bdi,
750            Wbr => Wbr,
751            Ruby => Ruby,
752            Rt => Rt,
753            Rtc => Rtc,
754            Rp => Rp,
755            Data => Data,
756            Canvas => Canvas,
757            Object => Object,
758            Param => Param,
759            Embed => Embed,
760            Audio => Audio,
761            Video => Video,
762            Source => Source,
763            Track => Track,
764            Map => Map,
765            Area => Area,
766            // SVG container
767            Svg => Svg, SvgG => SvgG, SvgDefs => SvgDefs, SvgSymbol => SvgSymbol,
768            SvgUse => SvgUse, SvgSwitch => SvgSwitch,
769            // SVG shape
770            SvgPath => SvgPath, SvgCircle => SvgCircle, SvgRect => SvgRect,
771            SvgEllipse => SvgEllipse, SvgLine => SvgLine,
772            SvgPolygon => SvgPolygon, SvgPolyline => SvgPolyline,
773            // SVG text
774            SvgText(s) => SvgText(s.clone_self()),
775            SvgTspan => SvgTspan, SvgTextPath => SvgTextPath,
776            // SVG paint
777            SvgLinearGradient => SvgLinearGradient, SvgRadialGradient => SvgRadialGradient,
778            SvgStop => SvgStop, SvgPattern => SvgPattern,
779            // SVG clip/mask
780            SvgClipPathElement => SvgClipPathElement, SvgMask => SvgMask,
781            // SVG filter
782            SvgFilter => SvgFilter, SvgFeBlend => SvgFeBlend,
783            SvgFeColorMatrix => SvgFeColorMatrix,
784            SvgFeComponentTransfer => SvgFeComponentTransfer,
785            SvgFeComposite => SvgFeComposite, SvgFeConvolveMatrix => SvgFeConvolveMatrix,
786            SvgFeDiffuseLighting => SvgFeDiffuseLighting,
787            SvgFeDisplacementMap => SvgFeDisplacementMap,
788            SvgFeDistantLight => SvgFeDistantLight, SvgFeDropShadow => SvgFeDropShadow,
789            SvgFeFlood => SvgFeFlood,
790            SvgFeFuncR => SvgFeFuncR, SvgFeFuncG => SvgFeFuncG,
791            SvgFeFuncB => SvgFeFuncB, SvgFeFuncA => SvgFeFuncA,
792            SvgFeGaussianBlur => SvgFeGaussianBlur, SvgFeImage => SvgFeImage,
793            SvgFeMerge => SvgFeMerge, SvgFeMergeNode => SvgFeMergeNode,
794            SvgFeMorphology => SvgFeMorphology, SvgFeOffset => SvgFeOffset,
795            SvgFePointLight => SvgFePointLight,
796            SvgFeSpecularLighting => SvgFeSpecularLighting,
797            SvgFeSpotLight => SvgFeSpotLight,
798            SvgFeTile => SvgFeTile, SvgFeTurbulence => SvgFeTurbulence,
799            // SVG marker/image/foreign
800            SvgMarker => SvgMarker,
801            SvgImage(i) => SvgImage(i.clone()),
802            SvgForeignObject => SvgForeignObject,
803            // SVG descriptive/structural
804            SvgTitle => SvgTitle, SvgDesc => SvgDesc, SvgMetadata => SvgMetadata,
805            SvgA => SvgA, SvgView => SvgView,
806            SvgStyle => SvgStyle, SvgScript => SvgScript,
807            // SVG animation
808            SvgAnimate => SvgAnimate, SvgAnimateMotion => SvgAnimateMotion,
809            SvgAnimateTransform => SvgAnimateTransform,
810            SvgSet => SvgSet, SvgMpath => SvgMpath,
811            // HTML metadata
812            Title => Title,
813            Meta => Meta,
814            Link => Link,
815            Script => Script,
816            Style => Style,
817            Base => Base,
818            Before => Before,
819            After => After,
820            Marker => Marker,
821            Placeholder => Placeholder,
822
823            Text(s) => Text(BoxOrStatic::heap(s.clone_self())),
824            Image(i) => Image(i.clone()),
825            VirtualView => VirtualView,
826            Icon(s) => Icon(BoxOrStatic::heap(s.clone_self())),
827            GeolocationProbe(cfg) => GeolocationProbe(*cfg),
828        }
829    }
830
831    #[must_use] pub fn format(&self) -> Option<String> {
832        use self::NodeType::{Text, Image, VirtualView, Icon, GeolocationProbe};
833        match self {
834            Text(s) => Some(format!("{s}")),
835            Image(id) => Some(format!("image({id:?})")),
836            VirtualView => Some("virtualized-view".to_string()),
837            Icon(s) => Some(format!("icon({s})")),
838            GeolocationProbe(cfg) => Some(format!(
839                "geolocation-probe(hi={}, bg={}, max={}m, every={}ms)",
840                cfg.high_accuracy, cfg.background, cfg.max_accuracy_m, cfg.min_interval_ms
841            )),
842            _ => None,
843        }
844    }
845
846    /// Returns the `NodeTypeTag` for CSS selector matching.
847    #[allow(clippy::too_many_lines)] // large but cohesive: single-purpose parser/builder/dispatch (one branch per input variant)
848    #[must_use] pub const fn get_path(&self) -> NodeTypeTag {
849        match self {
850            Self::Html => NodeTypeTag::Html,
851            Self::Head => NodeTypeTag::Head,
852            Self::Body => NodeTypeTag::Body,
853            Self::Div => NodeTypeTag::Div,
854            Self::P => NodeTypeTag::P,
855            Self::Article => NodeTypeTag::Article,
856            Self::Section => NodeTypeTag::Section,
857            Self::Nav => NodeTypeTag::Nav,
858            Self::Aside => NodeTypeTag::Aside,
859            Self::Header => NodeTypeTag::Header,
860            Self::Footer => NodeTypeTag::Footer,
861            Self::Main => NodeTypeTag::Main,
862            Self::Figure => NodeTypeTag::Figure,
863            Self::FigCaption => NodeTypeTag::FigCaption,
864            Self::H1 => NodeTypeTag::H1,
865            Self::H2 => NodeTypeTag::H2,
866            Self::H3 => NodeTypeTag::H3,
867            Self::H4 => NodeTypeTag::H4,
868            Self::H5 => NodeTypeTag::H5,
869            Self::H6 => NodeTypeTag::H6,
870            Self::Br => NodeTypeTag::Br,
871            Self::Hr => NodeTypeTag::Hr,
872            Self::Pre => NodeTypeTag::Pre,
873            Self::BlockQuote => NodeTypeTag::BlockQuote,
874            Self::Address => NodeTypeTag::Address,
875            Self::Details => NodeTypeTag::Details,
876            Self::Summary => NodeTypeTag::Summary,
877            Self::Dialog => NodeTypeTag::Dialog,
878            Self::Ul => NodeTypeTag::Ul,
879            Self::Ol => NodeTypeTag::Ol,
880            Self::Li => NodeTypeTag::Li,
881            Self::Dl => NodeTypeTag::Dl,
882            Self::Dt => NodeTypeTag::Dt,
883            Self::Dd => NodeTypeTag::Dd,
884            Self::Menu => NodeTypeTag::Menu,
885            Self::MenuItem => NodeTypeTag::MenuItem,
886            Self::Dir => NodeTypeTag::Dir,
887            Self::Table => NodeTypeTag::Table,
888            Self::Caption => NodeTypeTag::Caption,
889            Self::THead => NodeTypeTag::THead,
890            Self::TBody => NodeTypeTag::TBody,
891            Self::TFoot => NodeTypeTag::TFoot,
892            Self::Tr => NodeTypeTag::Tr,
893            Self::Th => NodeTypeTag::Th,
894            Self::Td => NodeTypeTag::Td,
895            Self::ColGroup => NodeTypeTag::ColGroup,
896            Self::Col => NodeTypeTag::Col,
897            Self::Form => NodeTypeTag::Form,
898            Self::FieldSet => NodeTypeTag::FieldSet,
899            Self::Legend => NodeTypeTag::Legend,
900            Self::Label => NodeTypeTag::Label,
901            Self::Input => NodeTypeTag::Input,
902            Self::Button => NodeTypeTag::Button,
903            Self::Select => NodeTypeTag::Select,
904            Self::OptGroup => NodeTypeTag::OptGroup,
905            Self::SelectOption => NodeTypeTag::SelectOption,
906            Self::TextArea => NodeTypeTag::TextArea,
907            Self::Output => NodeTypeTag::Output,
908            Self::Progress => NodeTypeTag::Progress,
909            Self::Meter => NodeTypeTag::Meter,
910            Self::DataList => NodeTypeTag::DataList,
911            Self::Span => NodeTypeTag::Span,
912            Self::A => NodeTypeTag::A,
913            Self::Em => NodeTypeTag::Em,
914            Self::Strong => NodeTypeTag::Strong,
915            Self::B => NodeTypeTag::B,
916            Self::I => NodeTypeTag::I,
917            Self::U => NodeTypeTag::U,
918            Self::S => NodeTypeTag::S,
919            Self::Mark => NodeTypeTag::Mark,
920            Self::Del => NodeTypeTag::Del,
921            Self::Ins => NodeTypeTag::Ins,
922            Self::Code => NodeTypeTag::Code,
923            Self::Samp => NodeTypeTag::Samp,
924            Self::Kbd => NodeTypeTag::Kbd,
925            Self::Var => NodeTypeTag::Var,
926            Self::Cite => NodeTypeTag::Cite,
927            Self::Dfn => NodeTypeTag::Dfn,
928            Self::Abbr => NodeTypeTag::Abbr,
929            Self::Acronym => NodeTypeTag::Acronym,
930            Self::Q => NodeTypeTag::Q,
931            Self::Time => NodeTypeTag::Time,
932            Self::Sub => NodeTypeTag::Sub,
933            Self::Sup => NodeTypeTag::Sup,
934            Self::Small => NodeTypeTag::Small,
935            Self::Big => NodeTypeTag::Big,
936            Self::Bdo => NodeTypeTag::Bdo,
937            Self::Bdi => NodeTypeTag::Bdi,
938            Self::Wbr => NodeTypeTag::Wbr,
939            Self::Ruby => NodeTypeTag::Ruby,
940            Self::Rt => NodeTypeTag::Rt,
941            Self::Rtc => NodeTypeTag::Rtc,
942            Self::Rp => NodeTypeTag::Rp,
943            Self::Data => NodeTypeTag::Data,
944            Self::Canvas => NodeTypeTag::Canvas,
945            Self::Object => NodeTypeTag::Object,
946            Self::Param => NodeTypeTag::Param,
947            Self::Embed => NodeTypeTag::Embed,
948            Self::Audio => NodeTypeTag::Audio,
949            Self::Video => NodeTypeTag::Video,
950            Self::Source => NodeTypeTag::Source,
951            Self::Track => NodeTypeTag::Track,
952            Self::Map => NodeTypeTag::Map,
953            Self::Area => NodeTypeTag::Area,
954            // SVG — all variants map 1:1 to NodeTypeTag
955            Self::Svg => NodeTypeTag::Svg,
956            Self::SvgG => NodeTypeTag::SvgG,
957            Self::SvgDefs => NodeTypeTag::SvgDefs,
958            Self::SvgSymbol => NodeTypeTag::SvgSymbol,
959            Self::SvgUse => NodeTypeTag::SvgUse,
960            Self::SvgSwitch => NodeTypeTag::SvgSwitch,
961            Self::SvgPath => NodeTypeTag::SvgPath,
962            Self::SvgCircle => NodeTypeTag::SvgCircle,
963            Self::SvgRect => NodeTypeTag::SvgRect,
964            Self::SvgEllipse => NodeTypeTag::SvgEllipse,
965            Self::SvgLine => NodeTypeTag::SvgLine,
966            Self::SvgPolygon => NodeTypeTag::SvgPolygon,
967            Self::SvgPolyline => NodeTypeTag::SvgPolyline,
968            Self::SvgText(_) => NodeTypeTag::SvgText,
969            Self::SvgTspan => NodeTypeTag::SvgTspan,
970            Self::SvgTextPath => NodeTypeTag::SvgTextPath,
971            Self::SvgLinearGradient => NodeTypeTag::SvgLinearGradient,
972            Self::SvgRadialGradient => NodeTypeTag::SvgRadialGradient,
973            Self::SvgStop => NodeTypeTag::SvgStop,
974            Self::SvgPattern => NodeTypeTag::SvgPattern,
975            Self::SvgClipPathElement => NodeTypeTag::SvgClipPathElement,
976            Self::SvgMask => NodeTypeTag::SvgMask,
977            Self::SvgFilter => NodeTypeTag::SvgFilter,
978            Self::SvgFeBlend => NodeTypeTag::SvgFeBlend,
979            Self::SvgFeColorMatrix => NodeTypeTag::SvgFeColorMatrix,
980            Self::SvgFeComponentTransfer => NodeTypeTag::SvgFeComponentTransfer,
981            Self::SvgFeComposite => NodeTypeTag::SvgFeComposite,
982            Self::SvgFeConvolveMatrix => NodeTypeTag::SvgFeConvolveMatrix,
983            Self::SvgFeDiffuseLighting => NodeTypeTag::SvgFeDiffuseLighting,
984            Self::SvgFeDisplacementMap => NodeTypeTag::SvgFeDisplacementMap,
985            Self::SvgFeDistantLight => NodeTypeTag::SvgFeDistantLight,
986            Self::SvgFeDropShadow => NodeTypeTag::SvgFeDropShadow,
987            Self::SvgFeFlood => NodeTypeTag::SvgFeFlood,
988            Self::SvgFeFuncR => NodeTypeTag::SvgFeFuncR,
989            Self::SvgFeFuncG => NodeTypeTag::SvgFeFuncG,
990            Self::SvgFeFuncB => NodeTypeTag::SvgFeFuncB,
991            Self::SvgFeFuncA => NodeTypeTag::SvgFeFuncA,
992            Self::SvgFeGaussianBlur => NodeTypeTag::SvgFeGaussianBlur,
993            Self::SvgFeImage => NodeTypeTag::SvgFeImage,
994            Self::SvgFeMerge => NodeTypeTag::SvgFeMerge,
995            Self::SvgFeMergeNode => NodeTypeTag::SvgFeMergeNode,
996            Self::SvgFeMorphology => NodeTypeTag::SvgFeMorphology,
997            Self::SvgFeOffset => NodeTypeTag::SvgFeOffset,
998            Self::SvgFePointLight => NodeTypeTag::SvgFePointLight,
999            Self::SvgFeSpecularLighting => NodeTypeTag::SvgFeSpecularLighting,
1000            Self::SvgFeSpotLight => NodeTypeTag::SvgFeSpotLight,
1001            Self::SvgFeTile => NodeTypeTag::SvgFeTile,
1002            Self::SvgFeTurbulence => NodeTypeTag::SvgFeTurbulence,
1003            Self::SvgMarker => NodeTypeTag::SvgMarker,
1004            Self::SvgImage(_) => NodeTypeTag::SvgImage,
1005            Self::SvgForeignObject => NodeTypeTag::SvgForeignObject,
1006            Self::SvgTitle => NodeTypeTag::SvgTitle,
1007            Self::SvgDesc => NodeTypeTag::SvgDesc,
1008            Self::SvgMetadata => NodeTypeTag::SvgMetadata,
1009            Self::SvgA => NodeTypeTag::SvgA,
1010            Self::SvgView => NodeTypeTag::SvgView,
1011            Self::SvgStyle => NodeTypeTag::SvgStyle,
1012            Self::SvgScript => NodeTypeTag::SvgScript,
1013            Self::SvgAnimate => NodeTypeTag::SvgAnimate,
1014            Self::SvgAnimateMotion => NodeTypeTag::SvgAnimateMotion,
1015            Self::SvgAnimateTransform => NodeTypeTag::SvgAnimateTransform,
1016            Self::SvgSet => NodeTypeTag::SvgSet,
1017            Self::SvgMpath => NodeTypeTag::SvgMpath,
1018            // HTML metadata
1019            Self::Title => NodeTypeTag::Title,
1020            Self::Meta => NodeTypeTag::Meta,
1021            Self::Link => NodeTypeTag::Link,
1022            Self::Script => NodeTypeTag::Script,
1023            Self::Style => NodeTypeTag::Style,
1024            Self::Base => NodeTypeTag::Base,
1025            Self::Text(_) => NodeTypeTag::Text,
1026            Self::Image(_) => NodeTypeTag::Img,
1027            Self::VirtualView => NodeTypeTag::VirtualView,
1028            Self::Icon(_) => NodeTypeTag::Icon,
1029            Self::GeolocationProbe(_) => NodeTypeTag::GeolocationProbe,
1030            Self::Before => NodeTypeTag::Before,
1031            Self::After => NodeTypeTag::After,
1032            Self::Marker => NodeTypeTag::Marker,
1033            Self::Placeholder => NodeTypeTag::Placeholder,
1034        }
1035    }
1036
1037    /// Returns whether this node type is a semantic HTML element that should
1038    /// automatically generate an accessibility tree node.
1039    ///
1040    /// These are elements with inherent semantic meaning that assistive
1041    /// technologies should be aware of, even without explicit ARIA attributes.
1042    #[must_use] pub const fn is_semantic_for_accessibility(&self) -> bool {
1043        matches!(
1044            self,
1045            Self::Button
1046                | Self::Input
1047                | Self::TextArea
1048                | Self::Select
1049                | Self::A
1050                | Self::H1
1051                | Self::H2
1052                | Self::H3
1053                | Self::H4
1054                | Self::H5
1055                | Self::H6
1056                | Self::Article
1057                | Self::Section
1058                | Self::Nav
1059                | Self::Main
1060                | Self::Header
1061                | Self::Footer
1062                | Self::Aside
1063        )
1064    }
1065}
1066
1067/// Represents the CSS formatting context for an element
1068#[derive(Clone, Copy, PartialEq, Eq)]
1069// [g147f az-web-lift] `#[repr(C, u8)]` forces an explicit u8 discriminant at offset 0 instead of letting
1070// Rust niche-pack the other variants' discriminants into the payload variants' (Block{bool}/Float/OutOfFlow)
1071// invalid byte values. The remill lift mis-decodes that niche encoding: `Block` (byte 0/1) reads correctly
1072// but `Inline` (a niche value) reads as garbage → `match` falls to `_` → nested <div>text</div> dispatches
1073// to layout_bfc instead of layout_ifc and its text never lays out (g147 root cause). Same fix pattern as the
1074// text3 enums (InlineContent/LogicalItem/ShapedItem/FontStack/LayoutError). Harmless + correct for native.
1075#[repr(C, u8)]
1076// +spec:display-property:844893 - block-level box establishing a new formatting context (BFC) modeled here
1077pub enum FormattingContext {
1078    /// Block-level formatting context
1079    Block {
1080        /// Whether this element establishes a new block formatting context
1081        establishes_new_context: bool,
1082    },
1083    /// Inline-level formatting context
1084    Inline,
1085    /// Inline-block (participates in an IFC but creates a BFC)
1086    InlineBlock,
1087    /// Flex formatting context
1088    Flex,
1089    /// Float (left or right)
1090    Float(LayoutFloat),
1091    /// Absolutely positioned (out of flow)
1092    OutOfFlow(LayoutPosition),
1093    /// Table formatting context (container)
1094    Table,
1095    /// Table row group formatting context (thead, tbody, tfoot)
1096    TableRowGroup,
1097    /// Table row formatting context
1098    TableRow,
1099    /// Table cell formatting context (td, th)
1100    TableCell,
1101    /// Table column group formatting context
1102    TableColumnGroup,
1103    /// Table caption formatting context
1104    TableCaption,
1105    /// Grid formatting context
1106    Grid,
1107    /// display:contents - element generates no box, children promoted to parent
1108    Contents,
1109    /// No formatting context (display: none)
1110    None,
1111}
1112
1113impl fmt::Debug for FormattingContext {
1114    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1115        match self {
1116            Self::Block {
1117                establishes_new_context,
1118            } => write!(
1119                f,
1120                "Block {{ establishes_new_context: {establishes_new_context:?} }}"
1121            ),
1122            Self::Inline => write!(f, "Inline"),
1123            Self::InlineBlock => write!(f, "InlineBlock"),
1124            Self::Flex => write!(f, "Flex"),
1125            Self::Float(layout_float) => write!(f, "Float({layout_float:?})"),
1126            Self::OutOfFlow(layout_position) => {
1127                write!(f, "OutOfFlow({layout_position:?})")
1128            }
1129            Self::Grid => write!(f, "Grid"),
1130            Self::None => write!(f, "None"),
1131            Self::Table => write!(f, "Table"),
1132            Self::TableRowGroup => write!(f, "TableRowGroup"),
1133            Self::TableRow => write!(f, "TableRow"),
1134            Self::TableCell => write!(f, "TableCell"),
1135            Self::TableColumnGroup => write!(f, "TableColumnGroup"),
1136            Self::TableCaption => write!(f, "TableCaption"),
1137            Self::Contents => write!(f, "Contents"),
1138        }
1139    }
1140}
1141
1142impl Default for FormattingContext {
1143    fn default() -> Self {
1144        Self::Block {
1145            establishes_new_context: false,
1146        }
1147    }
1148}
1149
1150/// Defines the type of event that can trigger a callback action.
1151#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
1152#[repr(C)]
1153pub enum On {
1154    /// Mouse cursor is hovering over the element.
1155    MouseOver,
1156    /// Mouse cursor has is over element and is pressed
1157    /// (not good for "click" events - use `MouseUp` instead).
1158    MouseDown,
1159    /// (Specialization of `MouseDown`). Fires only if the left mouse button
1160    /// has been pressed while cursor was over the element.
1161    LeftMouseDown,
1162    /// (Specialization of `MouseDown`). Fires only if the middle mouse button
1163    /// has been pressed while cursor was over the element.
1164    MiddleMouseDown,
1165    /// (Specialization of `MouseDown`). Fires only if the right mouse button
1166    /// has been pressed while cursor was over the element.
1167    RightMouseDown,
1168    /// Mouse button has been released while cursor was over the element.
1169    MouseUp,
1170    /// (Specialization of `MouseUp`). Fires only if the left mouse button has
1171    /// been released while cursor was over the element.
1172    LeftMouseUp,
1173    /// (Specialization of `MouseUp`). Fires only if the middle mouse button has
1174    /// been released while cursor was over the element.
1175    MiddleMouseUp,
1176    /// (Specialization of `MouseUp`). Fires only if the right mouse button has
1177    /// been released while cursor was over the element.
1178    RightMouseUp,
1179    /// Mouse cursor has entered the element.
1180    MouseEnter,
1181    /// Mouse cursor has left the element.
1182    MouseLeave,
1183    /// Mousewheel / touchpad scrolling.
1184    Scroll,
1185    /// The window received a unicode character (also respects the system locale).
1186    /// Check `keyboard_state.current_char` to get the current pressed character.
1187    TextInput,
1188    /// A **virtual keycode** was pressed. Note: This is only the virtual keycode,
1189    /// not the actual char. If you want to get the character, use `TextInput` instead.
1190    /// A virtual key does not have to map to a printable character.
1191    ///
1192    /// You can get all currently pressed virtual keycodes in the
1193    /// `keyboard_state.current_virtual_keycodes` and / or just the last keycode in the
1194    /// `keyboard_state.latest_virtual_keycode`.
1195    VirtualKeyDown,
1196    /// A **virtual keycode** was release. See `VirtualKeyDown` for more info.
1197    VirtualKeyUp,
1198    /// A file has been dropped on the element.
1199    HoveredFile,
1200    /// A file is being hovered on the element.
1201    DroppedFile,
1202    /// A file was hovered, but has exited the window.
1203    HoveredFileCancelled,
1204    /// Equivalent to `onfocus`.
1205    FocusReceived,
1206    /// Equivalent to `onblur`.
1207    FocusLost,
1208
1209    // Accessibility-specific events
1210    /// Default action triggered by screen reader (usually same as click/activate)
1211    Default,
1212    /// Element should collapse (e.g., accordion panel, tree node)
1213    Collapse,
1214    /// Element should expand (e.g., accordion panel, tree node)
1215    Expand,
1216    /// Increment value (e.g., number input, slider)
1217    Increment,
1218    /// Decrement value (e.g., number input, slider)
1219    Decrement,
1220}
1221
1222/// Contains the necessary information to render an embedded `VirtualView` node.
1223#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1224#[repr(C)]
1225pub struct VirtualViewNode {
1226    /// The callback function that returns the DOM for the virtualized view's content.
1227    pub callback: VirtualViewCallback,
1228    /// The application data passed to the virtualized view's layout callback.
1229    pub refany: RefAny,
1230}
1231
1232/// An enum that holds either a CSS ID or a class name as a string.
1233#[repr(C, u8)]
1234#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
1235pub enum IdOrClass {
1236    Id(AzString),
1237    Class(AzString),
1238}
1239
1240impl_option!(
1241    IdOrClass,
1242    OptionIdOrClass,
1243    copy = false,
1244    [Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord]
1245);
1246
1247impl_vec!(IdOrClass, IdOrClassVec, IdOrClassVecDestructor, IdOrClassVecDestructorType, IdOrClassVecSlice, OptionIdOrClass);
1248impl_vec_debug!(IdOrClass, IdOrClassVec);
1249impl_vec_partialord!(IdOrClass, IdOrClassVec);
1250impl_vec_ord!(IdOrClass, IdOrClassVec);
1251impl_vec_clone!(IdOrClass, IdOrClassVec, IdOrClassVecDestructor);
1252impl_vec_partialeq!(IdOrClass, IdOrClassVec);
1253impl_vec_eq!(IdOrClass, IdOrClassVec);
1254impl_vec_hash!(IdOrClass, IdOrClassVec);
1255
1256impl IdOrClass {
1257    #[must_use] pub fn as_id(&self) -> Option<&str> {
1258        match self {
1259            Self::Id(s) => Some(s.as_str()),
1260            Self::Class(_) => None,
1261        }
1262    }
1263    #[must_use] pub fn as_class(&self) -> Option<&str> {
1264        match self {
1265            Self::Class(s) => Some(s.as_str()),
1266            Self::Id(_) => None,
1267        }
1268    }
1269}
1270
1271/// Name-value pair for custom attributes (data-*, aria-*, etc.)
1272#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1273#[repr(C)]
1274pub struct AttributeNameValue {
1275    pub attr_name: AzString,
1276    pub value: AzString,
1277}
1278
1279/// Strongly-typed HTML attribute with type-safe values.
1280///
1281/// This enum provides a type-safe way to represent HTML attributes, ensuring that
1282/// values are validated at compile-time and properly converted to their string
1283/// representations at runtime.
1284#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1285#[repr(C, u8)]
1286pub enum AttributeType {
1287    /// Element ID attribute (`id="..."`)
1288    Id(AzString),
1289    /// CSS class attribute (`class="..."`)
1290    Class(AzString),
1291    /// Accessible name/label (`aria-label="..."`)
1292    AriaLabel(AzString),
1293    /// Element that labels this one (`aria-labelledby="..."`)
1294    AriaLabelledBy(AzString),
1295    /// Element that describes this one (`aria-describedby="..."`)
1296    AriaDescribedBy(AzString),
1297    /// Role for accessibility (`role="..."`)
1298    AriaRole(AzString),
1299    /// Current state of an element (`aria-checked`, `aria-selected`, etc.)
1300    AriaState(AttributeNameValue),
1301    /// ARIA property (`aria-*`)
1302    AriaProperty(AttributeNameValue),
1303
1304    /// Hyperlink target URL (`href="..."`)
1305    Href(AzString),
1306    /// Link relationship (`rel="..."`)
1307    Rel(AzString),
1308    /// Link target frame (`target="..."`)
1309    Target(AzString),
1310
1311    /// Image source URL (`src="..."`)
1312    Src(AzString),
1313    /// Alternative text for images (`alt="..."`)
1314    Alt(AzString),
1315    /// Image title (tooltip) (`title="..."`)
1316    Title(AzString),
1317
1318    /// Form input name (`name="..."`)
1319    Name(AzString),
1320    /// Form input value (`value="..."`)
1321    Value(AzString),
1322    /// Input type (`type="text|password|email|..."`)
1323    InputType(AzString),
1324    /// Placeholder text (`placeholder="..."`)
1325    Placeholder(AzString),
1326    /// Input is required (`required`)
1327    Required,
1328    /// Input is disabled (`disabled`)
1329    Disabled,
1330    /// Input is readonly (`readonly`)
1331    Readonly,
1332    /// Input is checked (checkbox/radio) (`checked`)
1333    CheckedTrue,
1334    /// Input is unchecked (checkbox/radio)
1335    CheckedFalse,
1336    /// Input is selected (option) (`selected`)
1337    Selected,
1338    /// Maximum value for number inputs (`max="..."`)
1339    Max(AzString),
1340    /// Minimum value for number inputs (`min="..."`)
1341    Min(AzString),
1342    /// Step value for number inputs (`step="..."`)
1343    Step(AzString),
1344    /// Input pattern for validation (`pattern="..."`)
1345    Pattern(AzString),
1346    /// Minimum length (`minlength="..."`)
1347    MinLength(i32),
1348    /// Maximum length (`maxlength="..."`)
1349    MaxLength(i32),
1350    /// Autocomplete behavior (`autocomplete="on|off|..."`)
1351    Autocomplete(AzString),
1352
1353    /// Table header scope (`scope="row|col|rowgroup|colgroup"`)
1354    Scope(AzString),
1355    /// Number of columns to span (`colspan="..."`)
1356    ColSpan(i32),
1357    /// Number of rows to span (`rowspan="..."`)
1358    RowSpan(i32),
1359
1360    /// Tab index for keyboard navigation (`tabindex="..."`)
1361    TabIndex(i32),
1362    /// Element can receive focus (`tabindex="0"` equivalent)
1363    Focusable,
1364
1365    /// Language code (`lang="..."`)
1366    Lang(AzString),
1367    /// Text direction (`dir="ltr|rtl|auto"`)
1368    Dir(AzString),
1369
1370    /// Content is editable (`contenteditable="true|false"`)
1371    ContentEditable(bool),
1372    /// Element is draggable (`draggable="true|false"`)
1373    Draggable(bool),
1374    /// Element is hidden (`hidden`)
1375    Hidden,
1376
1377    /// Generic data attribute (`data-*="..."`)
1378    Data(AttributeNameValue),
1379    /// Generic custom attribute (for future extensibility)
1380    Custom(AttributeNameValue),
1381}
1382
1383impl_option!(
1384    AttributeType,
1385    OptionAttributeType,
1386    copy = false,
1387    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
1388);
1389
1390impl_vec!(AttributeType, AttributeTypeVec, AttributeTypeVecDestructor, AttributeTypeVecDestructorType, AttributeTypeVecSlice, OptionAttributeType);
1391impl_vec_debug!(AttributeType, AttributeTypeVec);
1392impl_vec_partialord!(AttributeType, AttributeTypeVec);
1393impl_vec_ord!(AttributeType, AttributeTypeVec);
1394impl_vec_clone!(AttributeType, AttributeTypeVec, AttributeTypeVecDestructor);
1395impl_vec_partialeq!(AttributeType, AttributeTypeVec);
1396impl_vec_eq!(AttributeType, AttributeTypeVec);
1397impl_vec_hash!(AttributeType, AttributeTypeVec);
1398
1399impl AttributeType {
1400    /// Returns the id string if this is an `Id` attribute, `None` otherwise.
1401    #[must_use] pub fn as_id(&self) -> Option<&str> {
1402        match self {
1403            Self::Id(s) => Some(s.as_str()),
1404            _ => None,
1405        }
1406    }
1407    /// Returns the class string if this is a `Class` attribute, `None` otherwise.
1408    #[must_use] pub fn as_class(&self) -> Option<&str> {
1409        match self {
1410            Self::Class(s) => Some(s.as_str()),
1411            _ => None,
1412        }
1413    }
1414    /// Get the attribute name (e.g., "href", "aria-label", "data-foo")
1415    #[must_use] pub fn name(&self) -> &str {
1416        match self {
1417            Self::Id(_) => "id",
1418            Self::Class(_) => "class",
1419            Self::AriaLabel(_) => "aria-label",
1420            Self::AriaLabelledBy(_) => "aria-labelledby",
1421            Self::AriaDescribedBy(_) => "aria-describedby",
1422            Self::AriaRole(_) => "role",
1423            Self::AriaState(nv)
1424            | Self::AriaProperty(nv)
1425            | Self::Data(nv)
1426            | Self::Custom(nv) => nv.attr_name.as_str(),
1427            Self::Href(_) => "href",
1428            Self::Rel(_) => "rel",
1429            Self::Target(_) => "target",
1430            Self::Src(_) => "src",
1431            Self::Alt(_) => "alt",
1432            Self::Title(_) => "title",
1433            Self::Name(_) => "name",
1434            Self::Value(_) => "value",
1435            Self::InputType(_) => "type",
1436            Self::Placeholder(_) => "placeholder",
1437            Self::Required => "required",
1438            Self::Disabled => "disabled",
1439            Self::Readonly => "readonly",
1440            Self::CheckedTrue | Self::CheckedFalse => "checked",
1441            Self::Selected => "selected",
1442            Self::Max(_) => "max",
1443            Self::Min(_) => "min",
1444            Self::Step(_) => "step",
1445            Self::Pattern(_) => "pattern",
1446            Self::MinLength(_) => "minlength",
1447            Self::MaxLength(_) => "maxlength",
1448            Self::Autocomplete(_) => "autocomplete",
1449            Self::Scope(_) => "scope",
1450            Self::ColSpan(_) => "colspan",
1451            Self::RowSpan(_) => "rowspan",
1452            Self::TabIndex(_) | Self::Focusable => "tabindex",
1453            Self::Lang(_) => "lang",
1454            Self::Dir(_) => "dir",
1455            Self::ContentEditable(_) => "contenteditable",
1456            Self::Draggable(_) => "draggable",
1457            Self::Hidden => "hidden",
1458        }
1459    }
1460
1461    /// Get the attribute value as a string
1462    #[must_use] pub fn value(&self) -> AzString {
1463        match self {
1464            Self::Id(v)
1465            | Self::Class(v)
1466            | Self::AriaLabel(v)
1467            | Self::AriaLabelledBy(v)
1468            | Self::AriaDescribedBy(v)
1469            | Self::AriaRole(v)
1470            | Self::Href(v)
1471            | Self::Rel(v)
1472            | Self::Target(v)
1473            | Self::Src(v)
1474            | Self::Alt(v)
1475            | Self::Title(v)
1476            | Self::Name(v)
1477            | Self::Value(v)
1478            | Self::InputType(v)
1479            | Self::Placeholder(v)
1480            | Self::Max(v)
1481            | Self::Min(v)
1482            | Self::Step(v)
1483            | Self::Pattern(v)
1484            | Self::Autocomplete(v)
1485            | Self::Scope(v)
1486            | Self::Lang(v)
1487            | Self::Dir(v) => v.clone(),
1488
1489            Self::AriaState(nv)
1490            | Self::AriaProperty(nv)
1491            | Self::Data(nv)
1492            | Self::Custom(nv) => nv.value.clone(),
1493
1494            Self::MinLength(n)
1495            | Self::MaxLength(n)
1496            | Self::ColSpan(n)
1497            | Self::RowSpan(n)
1498            | Self::TabIndex(n) => n.to_string().into(),
1499
1500            Self::Focusable => "0".into(),
1501            Self::ContentEditable(b) | Self::Draggable(b) => {
1502                if *b {
1503                    "true".into()
1504                } else {
1505                    "false".into()
1506                }
1507            }
1508
1509            Self::Required
1510            | Self::Disabled
1511            | Self::Readonly
1512            | Self::CheckedTrue
1513                | Self::CheckedFalse
1514            | Self::Selected
1515            | Self::Hidden => "".into(), // Boolean attributes
1516        }
1517    }
1518
1519    /// Check if this is a boolean attribute (present = true, absent = false)
1520    #[must_use] pub const fn is_boolean(&self) -> bool {
1521        matches!(
1522            self,
1523            Self::Required
1524                | Self::Disabled
1525                | Self::Readonly
1526                | Self::CheckedTrue
1527                | Self::CheckedFalse
1528                | Self::Selected
1529                | Self::Hidden
1530        )
1531    }
1532}
1533
1534/// Represents all data associated with a single DOM node, such as its type,
1535/// classes, IDs, callbacks, and inline styles.
1536#[repr(C)]
1537#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
1538pub struct NodeData {
1539    /// `div`, `p`, `img`, etc.
1540    pub node_type: NodeType,
1541    /// Callbacks attached to this node:
1542    ///
1543    /// `On::MouseUp` -> `Callback(my_button_click_handler)`
1544    pub callbacks: CoreCallbackDataVec,
1545    /// Inline style: a `Css` value that applies only to this node (implicit `:scope`).
1546    /// Each rule carries conditions (@media/@os/:hover/...) and declarations; rules
1547    /// produced by parsing inline strings are tagged `rule_priority::INLINE`, while
1548    /// widget defaults pushed via `with_css_props` keep the same INLINE priority so
1549    /// they override author CSS — preserving the cascade priority that the previous
1550    /// per-property `css_props` field had.
1551    pub style: azul_css::css::Css,
1552    /// Packed flags: `tab_index` + contenteditable + `is_anonymous`.
1553    pub flags: NodeFlags,
1554    /// Optional extra accessibility information about this DOM node (MSAA, AT-SPI, UA).
1555    /// 8 bytes (Option<Box<T>> is pointer-sized).
1556    pub accessibility: Option<Box<AccessibilityInfo>>,
1557    /// Stores "extra", not commonly used data of the node: clip-mask, menus, etc.
1558    ///
1559    /// SHOULD NOT EXPOSED IN THE API - necessary to retroactively add functionality
1560    /// to the node without breaking the ABI.
1561    extra: Option<Box<NodeDataExt>>,
1562}
1563
1564impl_option!(
1565    NodeData,
1566    OptionNodeData,
1567    copy = false,
1568    [Debug, PartialEq, Eq, PartialOrd, Ord]
1569);
1570
1571impl Hash for NodeData {
1572    fn hash<H: Hasher>(&self, state: &mut H) {
1573        self.node_type.hash(state);
1574        self.attributes().as_ref().hash(state);
1575        self.flags.hash(state);
1576
1577        // NOTE: callbacks are NOT hashed regularly, otherwise
1578        // they'd cause inconsistencies because of the scroll callback
1579        for callback in self.callbacks.as_ref() {
1580            callback.event.hash(state);
1581            callback.callback.hash(state);
1582            callback.refany.get_type_id().hash(state);
1583        }
1584
1585        // Hash inline CSS properties (Static declarations only — same set the
1586        // legacy `css_props` field hashed). Conditions are intentionally
1587        // skipped to match the previous behaviour.
1588        for (prop, _conds) in self.style.iter_inline_properties() {
1589            mem::discriminant(prop).hash(state);
1590        }
1591        if let Some(ext) = self.extra.as_ref() {
1592            if let Some(ds) = ext.dataset.as_ref() {
1593                ds.hash(state);
1594            }
1595            if let Some(c) = ext.svg_data.as_ref() {
1596                c.hash(state);
1597            }
1598            if let Some(c) = ext.menu_bar.as_ref() {
1599                c.hash(state);
1600            }
1601            if let Some(c) = ext.context_menu.as_ref() {
1602                c.hash(state);
1603            }
1604            if let Some(vv) = ext.virtual_view.as_ref() {
1605                vv.hash(state);
1606            }
1607        }
1608    }
1609}
1610
1611/// Tracks which component rendered a DOM subtree.
1612///
1613/// When a component's `render_fn` returns a `StyledDom`, the framework stamps the
1614/// root node(s) of the output with a `ComponentOrigin`. This enables:
1615/// - The debugger to show a "Component Tree" alongside the DOM tree
1616/// - Code generation roundtrips (rendered DOM → component invocations → code)
1617/// - Clicking a DOM node to navigate to the component that produced it
1618#[derive(Debug, Clone, PartialEq)]
1619pub struct ComponentOrigin {
1620    /// Qualified component name, e.g. "shadcn:card", "builtin:div"
1621    pub component_id: AzString,
1622    /// Snapshot of the data model at render time, stored as a JSON value.
1623    /// The debug server can inspect typed values; the frontend serializes
1624    /// them back to JSON for display and editing.
1625    pub data_model_json: crate::json::Json,
1626}
1627
1628// Manual impls because Json contains f64 (no Eq/Ord/Hash derive),
1629// but we need them for NodeDataExt. We compare on the Display string.
1630impl Eq for ComponentOrigin {}
1631
1632impl PartialOrd for ComponentOrigin {
1633    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
1634        Some(self.cmp(other))
1635    }
1636}
1637
1638impl Ord for ComponentOrigin {
1639    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
1640        self.component_id.cmp(&other.component_id)
1641            .then_with(|| {
1642                let a = alloc::format!("{}", self.data_model_json);
1643                let b = alloc::format!("{}", other.data_model_json);
1644                a.cmp(&b)
1645            })
1646    }
1647}
1648
1649impl Hash for ComponentOrigin {
1650    fn hash<H: Hasher>(&self, state: &mut H) {
1651        self.component_id.hash(state);
1652        alloc::format!("{}", self.data_model_json).hash(state);
1653    }
1654}
1655
1656impl Default for ComponentOrigin {
1657    fn default() -> Self {
1658        Self {
1659            component_id: AzString::from_const_str(""),
1660            data_model_json: crate::json::Json::null(),
1661        }
1662    }
1663}
1664
1665/// SVG-specific data stored on a DOM node.
1666///
1667/// Each SVG element type stores its parsed attribute data here.
1668/// Also used for raster image clip masks (legacy C API).
1669#[derive(Debug, Clone, PartialOrd)]
1670pub enum SvgNodeData {
1671    /// Raster R8 image clip mask (legacy C API for chart.c style manual masks).
1672    ImageClipMask(ImageMask),
1673    /// `<path d="...">` — resolved path geometry.
1674    Path(crate::svg::SvgMultiPolygon),
1675    /// `<circle cx="" cy="" r="">`.
1676    Circle { cx: f32, cy: f32, r: f32 },
1677    /// `<rect x="" y="" width="" height="" rx="" ry="">`.
1678    Rect { x: f32, y: f32, width: f32, height: f32, rx: f32, ry: f32 },
1679    /// `<ellipse cx="" cy="" rx="" ry="">`.
1680    Ellipse { cx: f32, cy: f32, rx: f32, ry: f32 },
1681    /// `<line x1="" y1="" x2="" y2="">`.
1682    Line { x1: f32, y1: f32, x2: f32, y2: f32 },
1683    /// `<polygon points="">` / `<polyline points="">` — parsed point list.
1684    PointsList { points: alloc::vec::Vec<azul_css::props::basic::SvgPoint>, closed: bool },
1685    /// `<svg viewBox="" width="" height="">` — viewport attributes.
1686    ViewBox { min_x: f32, min_y: f32, width: f32, height: f32 },
1687    /// `<linearGradient>` attributes.
1688    LinearGradient { x1: f32, y1: f32, x2: f32, y2: f32 },
1689    /// `<radialGradient>` attributes.
1690    RadialGradient { cx: f32, cy: f32, r: f32, fx: f32, fy: f32 },
1691    /// `<stop offset="" stop-color="" stop-opacity="">`.
1692    GradientStop { offset: f32 },
1693    /// `<use href="" x="" y="">`.
1694    Use { href: AzString, x: f32, y: f32 },
1695    /// `<image href="" x="" y="" width="" height="">`.
1696    SvgImageData { href: AzString, x: f32, y: f32, width: f32, height: f32 },
1697}
1698
1699// PartialEq compares f32 fields by BIT PATTERN (to_bits), mirroring the Hash impl
1700// below, so a NaN coordinate is equal to itself and Eq/Hash agree. A derived PartialEq
1701// used raw float `==` (NaN != NaN), breaking Eq's reflexivity for e.g. a NaN Rect —
1702// and NodeType embeds this type, so the break propagated.
1703impl PartialEq for SvgNodeData {
1704    #[allow(clippy::match_same_arms, clippy::similar_names)] // SVG coord names (cx/cy/fx/fy, min_x/min_y) are domain-standard
1705    fn eq(&self, other: &Self) -> bool {
1706        // f32 bit-equality (matches Hash's to_bits).
1707        const fn fb(a: f32, b: f32) -> bool {
1708            a.to_bits() == b.to_bits()
1709        }
1710        use self::SvgNodeData::{
1711            Circle, Ellipse, GradientStop, ImageClipMask, Line, LinearGradient, Path,
1712            PointsList, RadialGradient, Rect, SvgImageData, Use, ViewBox,
1713        };
1714        match (self, other) {
1715            (ImageClipMask(a), ImageClipMask(b)) => a == b,
1716            (Path(a), Path(b)) => {
1717                let ra = a.rings.as_ref();
1718                let rb = b.rings.as_ref();
1719                ra.len() == rb.len()
1720                    && ra.iter().zip(rb.iter()).all(|(x, y)| {
1721                        let ia = x.items.as_ref();
1722                        let ib = y.items.as_ref();
1723                        ia.len() == ib.len()
1724                            && ia.iter().zip(ib.iter()).all(|(p, q)| svg_path_element_bits_eq(p, q))
1725                    })
1726            }
1727            (Circle { cx, cy, r }, Circle { cx: cx2, cy: cy2, r: r2 }) => {
1728                fb(*cx, *cx2) && fb(*cy, *cy2) && fb(*r, *r2)
1729            }
1730            (
1731                Rect { x, y, width, height, rx, ry },
1732                Rect { x: x2, y: y2, width: w2, height: h2, rx: rx2, ry: ry2 },
1733            ) => {
1734                fb(*x, *x2) && fb(*y, *y2) && fb(*width, *w2)
1735                    && fb(*height, *h2) && fb(*rx, *rx2) && fb(*ry, *ry2)
1736            }
1737            (Ellipse { cx, cy, rx, ry }, Ellipse { cx: cx2, cy: cy2, rx: rx2, ry: ry2 }) => {
1738                fb(*cx, *cx2) && fb(*cy, *cy2) && fb(*rx, *rx2) && fb(*ry, *ry2)
1739            }
1740            (Line { x1, y1, x2, y2 }, Line { x1: a1, y1: b1, x2: a2, y2: b2 })
1741            | (LinearGradient { x1, y1, x2, y2 }, LinearGradient { x1: a1, y1: b1, x2: a2, y2: b2 }) => {
1742                fb(*x1, *a1) && fb(*y1, *b1) && fb(*x2, *a2) && fb(*y2, *b2)
1743            }
1744            (PointsList { points: pa, closed: ca }, PointsList { points: pb, closed: cb }) => {
1745                ca == cb
1746                    && pa.len() == pb.len()
1747                    && pa.iter().zip(pb.iter()).all(|(p, q)| fb(p.x, q.x) && fb(p.y, q.y))
1748            }
1749            (
1750                ViewBox { min_x, min_y, width, height },
1751                ViewBox { min_x: a, min_y: b, width: w, height: h },
1752            ) => fb(*min_x, *a) && fb(*min_y, *b) && fb(*width, *w) && fb(*height, *h),
1753            (
1754                RadialGradient { cx, cy, r, fx, fy },
1755                RadialGradient { cx: cx2, cy: cy2, r: r2, fx: fx2, fy: fy2 },
1756            ) => {
1757                fb(*cx, *cx2) && fb(*cy, *cy2) && fb(*r, *r2) && fb(*fx, *fx2) && fb(*fy, *fy2)
1758            }
1759            (GradientStop { offset: a }, GradientStop { offset: b }) => fb(*a, *b),
1760            (Use { href, x, y }, Use { href: h2, x: x2, y: y2 }) => {
1761                href == h2 && fb(*x, *x2) && fb(*y, *y2)
1762            }
1763            (
1764                SvgImageData { href, x, y, width, height },
1765                SvgImageData { href: h2, x: x2, y: y2, width: w2, height: hh2 },
1766            ) => {
1767                href == h2 && fb(*x, *x2) && fb(*y, *y2) && fb(*width, *w2) && fb(*height, *hh2)
1768            }
1769            // Different variants are never equal.
1770            _ => false,
1771        }
1772    }
1773}
1774
1775/// Bit-equality for two `SvgPathElement`s (matches the Hash impl's per-coordinate
1776/// `to_bits`), so NaN path coordinates are self-equal.
1777const fn svg_path_element_bits_eq(
1778    a: &crate::svg::SvgPathElement,
1779    b: &crate::svg::SvgPathElement,
1780) -> bool {
1781    use crate::svg::SvgPathElement::{CubicCurve, Line, QuadraticCurve};
1782    const fn pb(a: azul_css::props::basic::SvgPoint, b: azul_css::props::basic::SvgPoint) -> bool {
1783        a.x.to_bits() == b.x.to_bits() && a.y.to_bits() == b.y.to_bits()
1784    }
1785    match (a, b) {
1786        (Line(a), Line(b)) => pb(a.start, b.start) && pb(a.end, b.end),
1787        (QuadraticCurve(a), QuadraticCurve(b)) => {
1788            pb(a.start, b.start) && pb(a.ctrl, b.ctrl) && pb(a.end, b.end)
1789        }
1790        (CubicCurve(a), CubicCurve(b)) => {
1791            pb(a.start, b.start) && pb(a.ctrl_1, b.ctrl_1)
1792                && pb(a.ctrl_2, b.ctrl_2) && pb(a.end, b.end)
1793        }
1794        _ => false,
1795    }
1796}
1797
1798impl Eq for SvgNodeData {}
1799
1800// SvgNodeData contains f32 (svg coords) so Ord can't be derived; this Ord is
1801// defined *in terms of* the derived field-wise PartialOrd (unwrap_or Equal), so
1802// the two cannot disagree — the derive_ord_xor_partial_ord concern doesn't apply.
1803#[allow(clippy::derive_ord_xor_partial_ord)]
1804impl Ord for SvgNodeData {
1805    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
1806        self.partial_cmp(other).unwrap_or(core::cmp::Ordering::Equal)
1807    }
1808}
1809
1810impl Hash for SvgNodeData {
1811    fn hash<H: Hasher>(&self, state: &mut H) {
1812        mem::discriminant(self).hash(state);
1813        match self {
1814            Self::ImageClipMask(m) => m.hash(state),
1815            Self::Path(mp) => {
1816                for ring in mp.rings.as_ref() {
1817                    for item in ring.items.as_ref() {
1818                        match item {
1819                            crate::svg::SvgPathElement::Line(l) => {
1820                                0u8.hash(state);
1821                                l.start.x.to_bits().hash(state);
1822                                l.start.y.to_bits().hash(state);
1823                                l.end.x.to_bits().hash(state);
1824                                l.end.y.to_bits().hash(state);
1825                            }
1826                            crate::svg::SvgPathElement::QuadraticCurve(q) => {
1827                                1u8.hash(state);
1828                                q.start.x.to_bits().hash(state);
1829                                q.start.y.to_bits().hash(state);
1830                                q.ctrl.x.to_bits().hash(state);
1831                                q.ctrl.y.to_bits().hash(state);
1832                                q.end.x.to_bits().hash(state);
1833                                q.end.y.to_bits().hash(state);
1834                            }
1835                            crate::svg::SvgPathElement::CubicCurve(c) => {
1836                                2u8.hash(state);
1837                                c.start.x.to_bits().hash(state);
1838                                c.start.y.to_bits().hash(state);
1839                                c.ctrl_1.x.to_bits().hash(state);
1840                                c.ctrl_1.y.to_bits().hash(state);
1841                                c.ctrl_2.x.to_bits().hash(state);
1842                                c.ctrl_2.y.to_bits().hash(state);
1843                                c.end.x.to_bits().hash(state);
1844                                c.end.y.to_bits().hash(state);
1845                            }
1846                        }
1847                    }
1848                }
1849            }
1850            Self::Circle { cx, cy, r } => {
1851                cx.to_bits().hash(state); cy.to_bits().hash(state); r.to_bits().hash(state);
1852            }
1853            Self::Rect { x, y, width, height, rx, ry } => {
1854                x.to_bits().hash(state); y.to_bits().hash(state);
1855                width.to_bits().hash(state); height.to_bits().hash(state);
1856                rx.to_bits().hash(state); ry.to_bits().hash(state);
1857            }
1858            Self::Ellipse { cx, cy, rx, ry } => {
1859                cx.to_bits().hash(state); cy.to_bits().hash(state);
1860                rx.to_bits().hash(state); ry.to_bits().hash(state);
1861            }
1862            // Line and LinearGradient share a { x1, y1, x2, y2 } shape and hash
1863            // identically (Eq still distinguishes the variants); fold the duplicate bodies.
1864            Self::Line { x1, y1, x2, y2 } | Self::LinearGradient { x1, y1, x2, y2 } => {
1865                x1.to_bits().hash(state); y1.to_bits().hash(state);
1866                x2.to_bits().hash(state); y2.to_bits().hash(state);
1867            }
1868            Self::PointsList { points, closed } => {
1869                for p in points {
1870                    p.x.to_bits().hash(state); p.y.to_bits().hash(state);
1871                }
1872                closed.hash(state);
1873            }
1874            Self::ViewBox { min_x, min_y, width, height } => {
1875                min_x.to_bits().hash(state); min_y.to_bits().hash(state);
1876                width.to_bits().hash(state); height.to_bits().hash(state);
1877            }
1878            Self::RadialGradient { cx, cy, r, fx, fy } => {
1879                cx.to_bits().hash(state); cy.to_bits().hash(state);
1880                r.to_bits().hash(state); fx.to_bits().hash(state);
1881                fy.to_bits().hash(state);
1882            }
1883            Self::GradientStop { offset } => {
1884                offset.to_bits().hash(state);
1885            }
1886            Self::Use { href, x, y } => {
1887                href.hash(state);
1888                x.to_bits().hash(state); y.to_bits().hash(state);
1889            }
1890            Self::SvgImageData { href, x, y, width, height } => {
1891                href.hash(state);
1892                x.to_bits().hash(state); y.to_bits().hash(state);
1893                width.to_bits().hash(state); height.to_bits().hash(state);
1894            }
1895        }
1896    }
1897}
1898
1899/// NOTE: NOT EXPOSED IN THE API! Stores extra,
1900/// not commonly used information for the `NodeData`.
1901/// This helps keep the primary `NodeData` struct smaller for common cases.
1902#[repr(C)]
1903#[derive(Debug, Default, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
1904pub struct NodeDataExt {
1905    /// Strongly-typed HTML attributes (aria-*, href, alt, etc.)
1906    /// IDs and classes are stored as `AttributeType::Id` and `AttributeType::Class` entries.
1907    /// Moved from `NodeData` to save 48B for the ~95% of nodes with no attributes.
1908    pub attributes: AttributeTypeVec,
1909    /// `VirtualView` callback data, only set when `node_type` == `NodeType::VirtualView`.
1910    pub virtual_view: Option<VirtualViewNode>,
1911    /// `data-*` attributes for this node, useful to store UI-related data on the node itself.
1912    pub dataset: Option<RefAny>,
1913    /// SVG-specific data or raster clip mask for this DOM node.
1914    pub svg_data: Option<SvgNodeData>,
1915    /// Menu bar that should be displayed at the top of this nodes rect.
1916    pub menu_bar: Option<Box<Menu>>,
1917    /// Context menu that should be opened when the item is left-clicked.
1918    pub context_menu: Option<Box<Menu>>,
1919    /// Stable key for reconciliation. If provided, allows the framework to track
1920    /// this node across frames even if its position in the array changes.
1921    /// This is crucial for correct lifecycle events when lists are reordered.
1922    pub key: Option<u64>,
1923    /// Callback to merge dataset state from a previous frame's node into the current node.
1924    /// This enables heavy resource preservation (video decoders, GL textures) across frames.
1925    pub dataset_merge_callback: Option<DatasetMergeCallback>,
1926    /// Tracks which component rendered this DOM subtree.
1927    /// Set by the framework during component rendering — the root node(s) of a
1928    /// component's output DOM get stamped with the component's qualified name.
1929    /// Enables the debugger to reconstruct the component invocation tree from the
1930    /// flat rendered DOM, and enables code generation roundtrips.
1931    pub component_origin: Option<ComponentOrigin>,
1932}
1933
1934/// A callback function used to merge the state of an old dataset into a new one.
1935///
1936/// This enables components with heavy internal state (video players, WebGL contexts)
1937/// to preserve their resources across frames, while the DOM tree is recreated.
1938///
1939/// The callback receives both the old and new datasets as `RefAny` (cheap shallow clones)
1940/// and returns the dataset that should be used for the new node.
1941///
1942/// # Example
1943///
1944/// ```rust,ignore
1945/// fn merge_video_state(new_data: RefAny, old_data: RefAny) -> RefAny {
1946///     // Transfer heavy resources from old to new
1947///     if let (Some(mut new), Some(old)) = (
1948///         new_data.downcast_mut::<VideoState>(),
1949///         old_data.downcast_ref::<VideoState>()
1950///     ) {
1951///         new.decoder = old.decoder.take();
1952///         new.gl_texture = old.gl_texture.take();
1953///     }
1954///     new_data // Return the merged state
1955/// }
1956/// ```
1957#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
1958#[repr(C)]
1959pub struct DatasetMergeCallback {
1960    /// The function pointer that performs the merge.
1961    /// Signature: `fn(new_data: RefAny, old_data: RefAny) -> RefAny`
1962    pub cb: DatasetMergeCallbackType,
1963    /// Optional callable for FFI language bindings (Python, etc.)
1964    /// When set, the FFI layer can invoke this instead of `cb`.
1965    pub callable: OptionRefAny,
1966}
1967
1968impl fmt::Debug for DatasetMergeCallback {
1969    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1970        f.debug_struct("DatasetMergeCallback")
1971            .field("cb", &(self.cb as usize))
1972            .field("callable", &self.callable)
1973            .finish()
1974    }
1975}
1976
1977/// Allow creating `DatasetMergeCallback` from a raw function pointer.
1978/// This enables the `Into<DatasetMergeCallback>` pattern for Python bindings.
1979impl From<DatasetMergeCallbackType> for DatasetMergeCallback {
1980    fn from(cb: DatasetMergeCallbackType) -> Self {
1981        Self {
1982            cb,
1983            callable: OptionRefAny::None,
1984        }
1985    }
1986}
1987
1988impl DatasetMergeCallback {
1989    /// Build from a raw `DatasetMergeCallbackType` function pointer (callable =
1990    /// None). The concrete parameter is a coercion site, so callers can pass a
1991    /// bare `extern "C" fn` item without an `as DatasetMergeCallbackType` cast.
1992    #[must_use]
1993    pub fn from_ptr(cb: DatasetMergeCallbackType) -> Self {
1994        Self::from(cb)
1995    }
1996}
1997
1998impl_option!(
1999    DatasetMergeCallback,
2000    OptionDatasetMergeCallback,
2001    copy = false,
2002    [Debug, Clone]
2003);
2004
2005/// Function pointer type for dataset merge callbacks.
2006///
2007/// Arguments:
2008/// - `new_data`: The new node's dataset (shallow clone, cheap)
2009/// - `old_data`: The old node's dataset (shallow clone, cheap)
2010///
2011/// Returns:
2012/// - The `RefAny` that should be used as the dataset for the new node
2013pub type DatasetMergeCallbackType = extern "C" fn(RefAny, RefAny) -> RefAny;
2014
2015impl Clone for NodeData {
2016    #[inline]
2017    fn clone(&self) -> Self {
2018        Self {
2019            node_type: self.node_type.to_library_owned_nodetype(),
2020            style: self.style.clone(),
2021            callbacks: self.callbacks.clone(),
2022            flags: self.flags,
2023            accessibility: self.accessibility.clone(),
2024            extra: self.extra.clone(),
2025        }
2026    }
2027}
2028
2029// Clone, PartialEq, Eq, Hash, PartialOrd, Ord
2030impl_vec!(NodeData, NodeDataVec, NodeDataVecDestructor, NodeDataVecDestructorType, NodeDataVecSlice, OptionNodeData);
2031impl_vec_clone!(NodeData, NodeDataVec, NodeDataVecDestructor);
2032impl_vec_mut!(NodeData, NodeDataVec);
2033impl_vec_debug!(NodeData, NodeDataVec);
2034impl_vec_partialord!(NodeData, NodeDataVec);
2035impl_vec_ord!(NodeData, NodeDataVec);
2036impl_vec_partialeq!(NodeData, NodeDataVec);
2037impl_vec_eq!(NodeData, NodeDataVec);
2038impl_vec_hash!(NodeData, NodeDataVec);
2039
2040impl NodeDataVec {
2041    #[inline]
2042    #[must_use] pub fn as_container(&self) -> NodeDataContainerRef<'_, NodeData> {
2043        NodeDataContainerRef {
2044            internal: self.as_ref(),
2045        }
2046    }
2047    #[inline]
2048    pub fn as_container_mut(&mut self) -> NodeDataContainerRefMut<'_, NodeData> {
2049        NodeDataContainerRefMut {
2050            internal: self.as_mut(),
2051        }
2052    }
2053}
2054
2055// SAFETY: All fields in NodeData are either Send (NodeType, NodeFlags, CssPropertyWithConditionsVec),
2056// Arc-wrapped (RefAny), or plain data (Box<AccessibilityInfo>, Box<NodeDataExt>).
2057// Function pointers (callbacks) are inherently Send. The RefAny uses atomic reference counting.
2058unsafe impl Send for NodeData {}
2059
2060/// Determines the behavior of an element in sequential focus navigation
2061// (e.g., using the Tab key).
2062#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
2063#[repr(C, u8)]
2064#[derive(Default)]
2065pub enum TabIndex {
2066    /// Automatic tab index, similar to simply setting `focusable = "true"` or `tabindex = 0`
2067    /// (both have the effect of making the element focusable).
2068    ///
2069    /// Sidenote: See <https://www.w3.org/TR/html5/editing.html#sequential-focus-navigation-and-the-tabindex-attribute>
2070    /// for interesting notes on tabindex and accessibility
2071    #[default]
2072    Auto,
2073    /// Set the tab index in relation to its parent element. I.e. if you have a list of elements,
2074    /// the focusing order is restricted to the current parent.
2075    ///
2076    /// When pressing tab repeatedly, the focusing order will be
2077    /// determined by `OverrideInParent` elements taking precedence among global order.
2078    OverrideInParent(u32),
2079    /// Elements can be focused in callbacks, but are not accessible via
2080    /// keyboard / tab navigation (-1).
2081    NoKeyboardFocus,
2082}
2083
2084impl_option!(
2085    TabIndex,
2086    OptionTabIndex,
2087    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
2088);
2089
2090impl TabIndex {
2091    /// Returns the HTML-compatible number of the `tabindex` element.
2092    // const fn: TryFrom isn't const, and u32 -> isize is lossless on every
2093    // supported (>= 32-bit) target, so the `as` cast cannot actually wrap here.
2094    #[allow(clippy::cast_possible_wrap)]
2095    #[must_use] pub const fn get_index(&self) -> isize {
2096        use self::TabIndex::{Auto, OverrideInParent, NoKeyboardFocus};
2097        match self {
2098            Auto => 0,
2099            OverrideInParent(x) => *x as isize,
2100            NoKeyboardFocus => -1,
2101        }
2102    }
2103}
2104
2105
2106/// Packed representation of tab index + contenteditable flag.
2107///
2108/// Bit layout (32 bits):
2109///   [31]     contenteditable flag (1 = true)
2110///   [30:29]  `tab_index` variant:
2111///              00 = None (no tab index set)
2112///              01 = Auto
2113///              10 = `OverrideInParent` (value in bits [28:0])
2114///              11 = `NoKeyboardFocus`
2115///   [28]     `is_anonymous` (1 = anonymous box for table layout)
2116///   [27:0]   `OverrideInParent` value (max ~268 million)
2117#[repr(C)]
2118#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
2119#[derive(Default)]
2120pub struct NodeFlags {
2121    pub inner: u32,
2122}
2123
2124
2125impl NodeFlags {
2126    const CONTENTEDITABLE_BIT: u32 = 1 << 31;
2127    const TAB_INDEX_MASK: u32      = 0b11 << 29;
2128    const ANONYMOUS_BIT: u32       = 1 << 28;
2129    const TAB_VALUE_MASK: u32      = (1 << 28) - 1;
2130
2131    const TAB_NONE: u32            = 0b00 << 29;
2132    const TAB_AUTO: u32            = 0b01 << 29;
2133    const TAB_OVERRIDE: u32        = 0b10 << 29;
2134    const TAB_NO_KEYBOARD: u32     = 0b11 << 29;
2135
2136    #[must_use] pub const fn new() -> Self {
2137        Self { inner: 0 }
2138    }
2139
2140    #[must_use] pub const fn is_contenteditable(&self) -> bool {
2141        (self.inner & Self::CONTENTEDITABLE_BIT) != 0
2142    }
2143
2144    #[must_use] pub const fn set_contenteditable(mut self, v: bool) -> Self {
2145        if v {
2146            self.inner |= Self::CONTENTEDITABLE_BIT;
2147        } else {
2148            self.inner &= !Self::CONTENTEDITABLE_BIT;
2149        }
2150        self
2151    }
2152
2153    pub const fn set_contenteditable_mut(&mut self, v: bool) {
2154        if v {
2155            self.inner |= Self::CONTENTEDITABLE_BIT;
2156        } else {
2157            self.inner &= !Self::CONTENTEDITABLE_BIT;
2158        }
2159    }
2160
2161    #[must_use] pub const fn get_tab_index(&self) -> Option<TabIndex> {
2162        match self.inner & Self::TAB_INDEX_MASK {
2163            x if x == Self::TAB_NONE => None,
2164            x if x == Self::TAB_AUTO => Some(TabIndex::Auto),
2165            x if x == Self::TAB_OVERRIDE => {
2166                let val = self.inner & Self::TAB_VALUE_MASK;
2167                Some(TabIndex::OverrideInParent(val))
2168            }
2169            x if x == Self::TAB_NO_KEYBOARD => Some(TabIndex::NoKeyboardFocus),
2170            _ => None,
2171        }
2172    }
2173
2174    /// Returns whether this node is an anonymous box generated for table layout.
2175    #[must_use] pub const fn is_anonymous(&self) -> bool {
2176        (self.inner & Self::ANONYMOUS_BIT) != 0
2177    }
2178
2179    pub const fn set_anonymous(&mut self, v: bool) {
2180        if v {
2181            self.inner |= Self::ANONYMOUS_BIT;
2182        } else {
2183            self.inner &= !Self::ANONYMOUS_BIT;
2184        }
2185    }
2186
2187    pub const fn set_tab_index(&mut self, tab_index: Option<TabIndex>) {
2188        // Clear tab index bits (bits 29-30) and value bits (bits 0-27)
2189        // keep contenteditable bit (31) and anonymous bit (28)
2190        self.inner &= Self::CONTENTEDITABLE_BIT | Self::ANONYMOUS_BIT;
2191        match tab_index {
2192            None => { /* TAB_NONE = 0, already cleared */ }
2193            Some(TabIndex::Auto) => {
2194                self.inner |= Self::TAB_AUTO;
2195            }
2196            Some(TabIndex::OverrideInParent(val)) => {
2197                self.inner |= Self::TAB_OVERRIDE | (val & Self::TAB_VALUE_MASK);
2198            }
2199            Some(TabIndex::NoKeyboardFocus) => {
2200                self.inner |= Self::TAB_NO_KEYBOARD;
2201            }
2202        }
2203    }
2204}
2205
2206impl Default for NodeData {
2207    fn default() -> Self {
2208        Self::create_node(NodeType::Div)
2209    }
2210}
2211
2212impl fmt::Display for NodeData {
2213    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2214        let html_type = self.node_type.get_path();
2215        let attributes_string = node_data_to_string(self);
2216
2217        match self.node_type.format() {
2218            Some(content) => write!(
2219                f,
2220                "<{html_type}{attributes_string}>{content}</{html_type}>"
2221            ),
2222            None => write!(f, "<{html_type}{attributes_string}/>"),
2223        }
2224    }
2225}
2226
2227fn node_data_to_string(node_data: &NodeData) -> String {
2228    let mut id_string = String::new();
2229    let ids = node_data
2230        .attributes()
2231        .as_ref()
2232        .iter()
2233        .filter_map(|s| s.as_id())
2234        .collect::<Vec<_>>()
2235        .join(" ");
2236
2237    if !ids.is_empty() {
2238        id_string = format!(" id=\"{ids}\" ");
2239    }
2240
2241    let mut class_string = String::new();
2242    let classes = node_data
2243        .attributes()
2244        .as_ref()
2245        .iter()
2246        .filter_map(|s| s.as_class())
2247        .collect::<Vec<_>>()
2248        .join(" ");
2249
2250    if !classes.is_empty() {
2251        class_string = format!(" class=\"{classes}\" ");
2252    }
2253
2254    let mut tabindex_string = String::new();
2255    if let Some(tab_index) = node_data.get_tab_index() {
2256        tabindex_string = format!(" tabindex=\"{}\" ", tab_index.get_index());
2257    }
2258
2259    format!("{id_string}{class_string}{tabindex_string}")
2260}
2261
2262impl NodeData {
2263    /// Creates a new `NodeData` instance from a given `NodeType`.
2264    #[inline]
2265    #[must_use] pub const fn create_node(node_type: NodeType) -> Self {
2266        Self {
2267            node_type,
2268            callbacks: CoreCallbackDataVec::from_const_slice(&[]),
2269            style: azul_css::css::Css {
2270                rules: azul_css::css::CssRuleBlockVec::from_const_slice(&[]),
2271            },
2272            flags: NodeFlags::new(),
2273            accessibility: None,
2274            extra: None,
2275        }
2276    }
2277
2278    /// Returns a reference to the node's attributes (from `NodeDataExt`).
2279    /// Returns an empty slice if no attributes have been set.
2280    #[inline]
2281    #[must_use] pub fn attributes(&self) -> &AttributeTypeVec {
2282        static EMPTY: AttributeTypeVec = AttributeTypeVec::from_const_slice(&[]);
2283        self.extra.as_ref().map_or(&EMPTY, |ext| &ext.attributes)
2284    }
2285
2286    /// Returns a mutable reference to the node's attributes,
2287    /// lazily allocating `NodeDataExt` if needed.
2288    #[inline]
2289    pub fn attributes_mut(&mut self) -> &mut AttributeTypeVec {
2290        &mut self.extra.get_or_insert_with(|| Box::new(NodeDataExt::default())).attributes
2291    }
2292
2293    /// Sets the node's attributes, replacing any existing ones.
2294    #[inline]
2295    pub fn set_attributes(&mut self, attrs: AttributeTypeVec) {
2296        self.extra.get_or_insert_with(|| Box::new(NodeDataExt::default())).attributes = attrs;
2297    }
2298
2299    /// Shorthand for `NodeData::create_node(NodeType::Body)`.
2300    #[inline]
2301    #[must_use] pub const fn create_body() -> Self {
2302        Self::create_node(NodeType::Body)
2303    }
2304
2305    /// Shorthand for `NodeData::create_node(NodeType::Div)`.
2306    #[inline]
2307    #[must_use] pub const fn create_div() -> Self {
2308        Self::create_node(NodeType::Div)
2309    }
2310
2311    /// Shorthand for `NodeData::create_node(NodeType::Br)`.
2312    #[inline]
2313    #[must_use] pub const fn create_br() -> Self {
2314        Self::create_node(NodeType::Br)
2315    }
2316
2317    /// Shorthand for `NodeData::create_node(NodeType::Text(value.into()))`.
2318    #[inline]
2319    pub fn create_text<S: Into<AzString>>(value: S) -> Self {
2320        Self::create_node(NodeType::Text(BoxOrStatic::heap(value.into())))
2321    }
2322
2323    /// Shorthand for `NodeData::create_node(NodeType::Image(image_id))`.
2324    #[inline]
2325    #[must_use] pub fn create_image(image: ImageRef) -> Self {
2326        Self::create_node(NodeType::Image(BoxOrStatic::heap(image)))
2327    }
2328
2329    #[inline]
2330    pub fn create_virtual_view(data: RefAny, callback: impl Into<VirtualViewCallback>) -> Self {
2331        let mut nd = Self::create_node(NodeType::VirtualView);
2332        let ext = nd.extra.get_or_insert_with(|| Box::new(NodeDataExt::default()));
2333        ext.virtual_view = Some(VirtualViewNode {
2334            callback: callback.into(),
2335            refany: data,
2336        });
2337        nd
2338    }
2339
2340    // -- Accessibility-aware NodeData constructors --
2341    // Each a11y-able element has two constructors: the canonical one takes a
2342    // `SmallAriaInfo` so the caller must opt in to an accessible name, and the
2343    // `*_no_a11y` variant is a deliberate escape hatch with a longer name.
2344
2345    fn with_attribute(mut self, attr: AttributeType) -> Self {
2346        let mut v = self.attributes().clone().into_library_owned_vec();
2347        v.push(attr);
2348        self.set_attributes(v.into());
2349        self
2350    }
2351
2352    /// Creates a button `NodeData` with accessibility information.
2353    #[inline]
2354    #[allow(clippy::needless_pass_by_value)] // owned azul C-ABI value taken by value (FFI ownership-transfer convention)
2355    #[must_use] pub fn create_button(aria: SmallAriaInfo) -> Self {
2356        let mut nd = Self::create_node(NodeType::Button);
2357        nd.set_accessibility_info(aria.to_full_info());
2358        nd
2359    }
2360
2361    /// Creates a button `NodeData` without accessibility information.
2362    #[inline]
2363    #[must_use] pub const fn create_button_no_a11y() -> Self {
2364        Self::create_node(NodeType::Button)
2365    }
2366
2367    /// Creates an anchor `NodeData` with an href and accessibility information.
2368    #[inline]
2369    #[allow(clippy::needless_pass_by_value)] // owned azul C-ABI value taken by value (FFI ownership-transfer convention)
2370    #[must_use] pub fn create_a(href: AzString, aria: SmallAriaInfo) -> Self {
2371        let mut nd = Self::create_node(NodeType::A).with_attribute(AttributeType::Href(href));
2372        nd.set_accessibility_info(aria.to_full_info());
2373        nd
2374    }
2375
2376    /// Creates an anchor `NodeData` with an href but no accessibility information.
2377    #[inline]
2378    #[must_use] pub fn create_a_no_a11y(href: AzString) -> Self {
2379        Self::create_node(NodeType::A).with_attribute(AttributeType::Href(href))
2380    }
2381
2382    /// Creates an input `NodeData` with accessibility information.
2383    #[inline]
2384    #[allow(clippy::needless_pass_by_value)] // owned azul C-ABI value taken by value (FFI ownership-transfer convention)
2385    #[must_use] pub fn create_input(
2386        input_type: AzString,
2387        name: AzString,
2388        label: AzString,
2389        aria: SmallAriaInfo,
2390    ) -> Self {
2391        let mut nd = Self::create_node(NodeType::Input)
2392            .with_attribute(AttributeType::InputType(input_type))
2393            .with_attribute(AttributeType::Name(name))
2394            .with_attribute(AttributeType::AriaLabel(label));
2395        nd.set_accessibility_info(aria.to_full_info());
2396        nd
2397    }
2398
2399    /// Creates an input `NodeData` without accessibility information.
2400    #[inline]
2401    #[must_use] pub fn create_input_no_a11y(input_type: AzString, name: AzString, label: AzString) -> Self {
2402        Self::create_node(NodeType::Input)
2403            .with_attribute(AttributeType::InputType(input_type))
2404            .with_attribute(AttributeType::Name(name))
2405            .with_attribute(AttributeType::AriaLabel(label))
2406    }
2407
2408    /// Creates a textarea `NodeData` with accessibility information.
2409    #[inline]
2410    #[allow(clippy::needless_pass_by_value)] // owned azul C-ABI value taken by value (FFI ownership-transfer convention)
2411    #[must_use] pub fn create_textarea(name: AzString, label: AzString, aria: SmallAriaInfo) -> Self {
2412        let mut nd = Self::create_node(NodeType::TextArea)
2413            .with_attribute(AttributeType::Name(name))
2414            .with_attribute(AttributeType::AriaLabel(label));
2415        nd.set_accessibility_info(aria.to_full_info());
2416        nd
2417    }
2418
2419    /// Creates a textarea `NodeData` without accessibility information.
2420    #[inline]
2421    #[must_use] pub fn create_textarea_no_a11y(name: AzString, label: AzString) -> Self {
2422        Self::create_node(NodeType::TextArea)
2423            .with_attribute(AttributeType::Name(name))
2424            .with_attribute(AttributeType::AriaLabel(label))
2425    }
2426
2427    /// Creates a select `NodeData` with accessibility information.
2428    #[inline]
2429    #[allow(clippy::needless_pass_by_value)] // owned azul C-ABI value taken by value (FFI ownership-transfer convention)
2430    #[must_use] pub fn create_select(name: AzString, label: AzString, aria: SmallAriaInfo) -> Self {
2431        let mut nd = Self::create_node(NodeType::Select)
2432            .with_attribute(AttributeType::Name(name))
2433            .with_attribute(AttributeType::AriaLabel(label));
2434        nd.set_accessibility_info(aria.to_full_info());
2435        nd
2436    }
2437
2438    /// Creates a select `NodeData` without accessibility information.
2439    #[inline]
2440    #[must_use] pub fn create_select_no_a11y(name: AzString, label: AzString) -> Self {
2441        Self::create_node(NodeType::Select)
2442            .with_attribute(AttributeType::Name(name))
2443            .with_attribute(AttributeType::AriaLabel(label))
2444    }
2445
2446    /// Creates a table `NodeData` with accessibility information.
2447    #[inline]
2448    #[allow(clippy::needless_pass_by_value)] // owned azul C-ABI value taken by value (FFI ownership-transfer convention)
2449    #[must_use] pub fn create_table(aria: SmallAriaInfo) -> Self {
2450        let mut nd = Self::create_node(NodeType::Table);
2451        nd.set_accessibility_info(aria.to_full_info());
2452        nd
2453    }
2454
2455    /// Creates a table `NodeData` without accessibility information.
2456    #[inline]
2457    #[must_use] pub const fn create_table_no_a11y() -> Self {
2458        Self::create_node(NodeType::Table)
2459    }
2460
2461    /// Creates a label `NodeData` with an associated control ID and accessibility
2462    /// information.
2463    #[inline]
2464    #[allow(clippy::needless_pass_by_value)] // owned azul C-ABI value taken by value (FFI ownership-transfer convention)
2465    #[must_use] pub fn create_label(for_id: AzString, aria: SmallAriaInfo) -> Self {
2466        let mut nd = Self::create_node(NodeType::Label).with_attribute(AttributeType::Custom(
2467            AttributeNameValue {
2468                attr_name: "for".into(),
2469                value: for_id,
2470            },
2471        ));
2472        nd.set_accessibility_info(aria.to_full_info());
2473        nd
2474    }
2475
2476    /// Creates a label `NodeData` with an associated control ID but no
2477    /// accessibility information.
2478    #[inline]
2479    #[must_use] pub fn create_label_no_a11y(for_id: AzString) -> Self {
2480        Self::create_node(NodeType::Label).with_attribute(AttributeType::Custom(AttributeNameValue {
2481            attr_name: "for".into(),
2482            value: for_id,
2483        }))
2484    }
2485
2486    /// Checks whether this node is of the given node type (div, image, text).
2487    #[inline]
2488    #[allow(clippy::needless_pass_by_value)] // owned azul C-ABI value taken by value (FFI ownership-transfer convention)
2489    #[must_use] pub fn is_node_type(&self, searched_type: NodeType) -> bool {
2490        self.node_type == searched_type
2491    }
2492
2493    /// Checks whether this node has the searched ID attached.
2494    #[must_use] pub fn has_id(&self, id: &str) -> bool {
2495        self.attributes()
2496            .iter()
2497            .any(|attr| attr.as_id() == Some(id))
2498    }
2499
2500    /// Checks whether this node has the searched class attached.
2501    #[must_use] pub fn has_class(&self, class: &str) -> bool {
2502        self.attributes()
2503            .iter()
2504            .any(|attr| attr.as_class() == Some(class))
2505    }
2506
2507    #[must_use] pub fn has_context_menu(&self) -> bool {
2508        self.extra
2509            .as_ref()
2510            .is_some_and(|m| m.context_menu.is_some())
2511    }
2512
2513    #[must_use] pub const fn is_text_node(&self) -> bool {
2514        matches!(self.node_type, NodeType::Text(_))
2515    }
2516
2517    #[must_use] pub const fn is_virtual_view_node(&self) -> bool {
2518        matches!(self.node_type, NodeType::VirtualView)
2519    }
2520
2521    // NOTE: Getters are used here in order to allow changing the memory allocator for the NodeData
2522    // in the future (which is why the fields are all private).
2523
2524    #[inline]
2525    #[must_use] pub const fn get_node_type(&self) -> &NodeType {
2526        &self.node_type
2527    }
2528    #[inline]
2529    pub fn get_dataset_mut(&mut self) -> Option<&mut RefAny> {
2530        self.extra.as_mut().and_then(|e| e.dataset.as_mut())
2531    }
2532    #[inline]
2533    #[must_use] pub fn get_dataset(&self) -> Option<&RefAny> {
2534        self.extra.as_ref().and_then(|e| e.dataset.as_ref())
2535    }
2536    /// Take the dataset out of the node, replacing it with None.
2537    pub fn take_dataset(&mut self) -> Option<RefAny> {
2538        self.extra.as_mut().and_then(|e| e.dataset.take())
2539    }
2540    /// Returns IDs and classes as a computed `IdOrClassVec`.
2541    /// Note: this allocates a new vec each time, prefer `has_id()`/`has_class()` for checks.
2542    #[inline]
2543    #[must_use] pub fn get_ids_and_classes(&self) -> IdOrClassVec {
2544        let v: Vec<IdOrClass> = self.attributes().as_ref().iter().filter_map(|attr| {
2545            match attr {
2546                AttributeType::Id(s) => Some(IdOrClass::Id(s.clone())),
2547                AttributeType::Class(s) => Some(IdOrClass::Class(s.clone())),
2548                _ => None,
2549            }
2550        }).collect();
2551        v.into()
2552    }
2553    #[inline]
2554    #[must_use] pub const fn get_callbacks(&self) -> &CoreCallbackDataVec {
2555        &self.callbacks
2556    }
2557    #[inline]
2558    #[must_use] pub const fn get_style(&self) -> &azul_css::css::Css {
2559        &self.style
2560    }
2561
2562    #[inline]
2563    #[must_use] pub fn get_svg_data(&self) -> Option<&SvgNodeData> {
2564        self.extra.as_ref().and_then(|e| e.svg_data.as_ref())
2565    }
2566
2567    /// Legacy accessor for raster clip mask. Returns `Some` only for `SvgNodeData::ImageClipMask`.
2568    #[inline]
2569    #[must_use] pub fn get_image_clip_mask(&self) -> Option<&ImageMask> {
2570        match self.get_svg_data()? {
2571            SvgNodeData::ImageClipMask(m) => Some(m),
2572            _ => None,
2573        }
2574    }
2575    #[inline]
2576    #[must_use] pub const fn get_tab_index(&self) -> Option<TabIndex> {
2577        self.flags.get_tab_index()
2578    }
2579    #[inline]
2580    #[must_use] pub fn get_accessibility_info(&self) -> Option<&AccessibilityInfo> {
2581        self.accessibility.as_deref()
2582    }
2583    #[inline]
2584    #[must_use] pub fn get_menu_bar(&self) -> Option<&Menu> {
2585        self.extra.as_ref().and_then(|e| e.menu_bar.as_deref())
2586    }
2587    #[inline]
2588    #[must_use] pub fn get_context_menu(&self) -> Option<&Menu> {
2589        self.extra.as_ref().and_then(|e| e.context_menu.as_deref())
2590    }
2591
2592    /// Returns whether this node is an anonymous box generated for table layout.
2593    #[inline]
2594    #[must_use] pub const fn is_anonymous(&self) -> bool {
2595        self.flags.is_anonymous()
2596    }
2597
2598    #[inline]
2599    pub fn set_node_type(&mut self, node_type: NodeType) {
2600        self.node_type = node_type;
2601    }
2602    #[inline]
2603    pub fn set_dataset(&mut self, data: OptionRefAny) {
2604        match data {
2605            OptionRefAny::None => {
2606                if let Some(ext) = self.extra.as_mut() {
2607                    ext.dataset = None;
2608                }
2609            }
2610            OptionRefAny::Some(r) => {
2611                self.extra
2612                    .get_or_insert_with(|| Box::new(NodeDataExt::default()))
2613                    .dataset = Some(r);
2614            }
2615        }
2616    }
2617    /// Sets the IDs and classes by converting `IdOrClassVec` entries into
2618    /// `AttributeType::Id`/`AttributeType::Class` and merging them into `self.attributes`.
2619    /// Any existing Id/Class attributes are removed first.
2620    #[inline]
2621    #[allow(clippy::needless_pass_by_value)] // owned azul C-ABI value taken by value (FFI ownership-transfer convention)
2622    pub fn set_ids_and_classes(&mut self, ids_and_classes: IdOrClassVec) {
2623        // Remove existing Id/Class from attributes
2624        let mut v: AttributeTypeVec = Vec::new().into();
2625        mem::swap(&mut v, self.attributes_mut());
2626        let mut v = v.into_library_owned_vec();
2627        v.retain(|a| !matches!(a, AttributeType::Id(_) | AttributeType::Class(_)));
2628        // Convert and append
2629        for ioc in ids_and_classes.as_ref() {
2630            match ioc {
2631                IdOrClass::Id(s) => v.push(AttributeType::Id(s.clone())),
2632                IdOrClass::Class(s) => v.push(AttributeType::Class(s.clone())),
2633            }
2634        }
2635        self.set_attributes(v.into());
2636    }
2637    #[inline]
2638    pub fn set_callbacks(&mut self, callbacks: CoreCallbackDataVec) {
2639        self.callbacks = callbacks;
2640    }
2641    /// Legacy: replace this node's inline style with a flat list of property+conditions.
2642    /// Each entry becomes a single-declaration rule at `rule_priority::INLINE`. Prefer
2643    /// `set_style` (or `with_style` / `with_css(&str)`) for new code.
2644    #[inline]
2645    pub fn set_css_props(&mut self, css_props: CssPropertyWithConditionsVec) {
2646        self.style = css_props.into();
2647    }
2648    /// Replace this node's inline style with a `Css` value. The Css's rules apply only
2649    /// to this node (implicit `:scope`).
2650    #[inline]
2651    pub fn set_style(&mut self, style: azul_css::css::Css) {
2652        self.style = style;
2653    }
2654    #[inline]
2655    pub fn set_clip_mask(&mut self, clip_mask: ImageMask) {
2656        self.extra
2657            .get_or_insert_with(|| Box::new(NodeDataExt::default()))
2658            .svg_data = Some(SvgNodeData::ImageClipMask(clip_mask));
2659    }
2660    #[inline]
2661    pub fn set_svg_data(&mut self, data: SvgNodeData) {
2662        self.extra
2663            .get_or_insert_with(|| Box::new(NodeDataExt::default()))
2664            .svg_data = Some(data);
2665    }
2666    #[inline]
2667    pub const fn set_tab_index(&mut self, tab_index: TabIndex) {
2668        self.flags.set_tab_index(Some(tab_index));
2669    }
2670    #[inline]
2671    pub const fn set_contenteditable(&mut self, contenteditable: bool) {
2672        self.flags.set_contenteditable_mut(contenteditable);
2673    }
2674    #[inline]
2675    #[must_use] pub const fn is_contenteditable(&self) -> bool {
2676        self.flags.is_contenteditable()
2677    }
2678    #[inline]
2679    pub fn set_accessibility_info(&mut self, accessibility_info: AccessibilityInfo) {
2680        self.accessibility = Some(Box::new(accessibility_info));
2681    }
2682
2683    /// Marks this node as an anonymous box (generated for table layout).
2684    #[inline]
2685    pub const fn set_anonymous(&mut self, is_anonymous: bool) {
2686        self.flags.set_anonymous(is_anonymous);
2687    }
2688    #[inline]
2689    pub fn set_menu_bar(&mut self, menu_bar: Menu) {
2690        self.extra
2691            .get_or_insert_with(|| Box::new(NodeDataExt::default()))
2692            .menu_bar = Some(Box::new(menu_bar));
2693    }
2694    #[inline]
2695    pub fn set_context_menu(&mut self, context_menu: Menu) {
2696        self.extra
2697            .get_or_insert_with(|| Box::new(NodeDataExt::default()))
2698            .context_menu = Some(Box::new(context_menu));
2699    }
2700
2701    /// Sets a stable key for this node used in reconciliation.
2702    ///
2703    /// This key is used to track node identity across DOM updates, enabling
2704    /// the framework to distinguish between "moving" a node and "destroying/creating" one.
2705    /// This is crucial for correct lifecycle events when lists are reordered.
2706    ///
2707    /// # Example
2708    /// ```rust
2709    /// # use azul_core::dom::NodeData;
2710    /// # let mut node_data = NodeData::create_div();
2711    /// node_data.set_key("user-123");
2712    /// ```
2713    #[inline]
2714    pub fn set_key<K: Hash>(&mut self, key: K) {
2715        use core::hash::Hasher;
2716        let mut hasher = crate::hash::DefaultHasher::new();
2717        key.hash(&mut hasher);
2718        self.extra
2719            .get_or_insert_with(|| Box::new(NodeDataExt::default()))
2720            .key = Some(hasher.finish());
2721    }
2722
2723    /// Gets the key for this node, if set.
2724    #[inline]
2725    #[must_use] pub fn get_key(&self) -> Option<u64> {
2726        self.extra.as_ref().and_then(|ext| ext.key)
2727    }
2728
2729    /// Sets a dataset merge callback for this node.
2730    ///
2731    /// The merge callback is invoked during reconciliation when a node from the
2732    /// previous frame is matched with a node in the new frame. It allows heavy
2733    /// resources (video decoders, GL textures, network connections) to be
2734    /// transferred from the old node to the new node instead of being destroyed.
2735    ///
2736    /// # Type Safety
2737    ///
2738    /// The callback stores the `TypeId` of `T`. During execution, both the old
2739    /// and new datasets must match this type, otherwise the merge is skipped.
2740    ///
2741    /// # Example
2742    /// ```rust,ignore
2743    /// struct VideoPlayer {
2744    ///     url: String,
2745    ///     decoder: Option<DecoderHandle>,
2746    /// }
2747    ///
2748    /// extern "C" fn merge_video(new_data: RefAny, old_data: RefAny) -> RefAny {
2749    ///     // Transfer the heavy decoder handle from old to new
2750    ///     if let (Some(mut new), Some(old)) = (
2751    ///         new_data.downcast_mut::<VideoPlayer>(),
2752    ///         old_data.downcast_ref::<VideoPlayer>()
2753    ///     ) {
2754    ///         new.decoder = old.decoder.take();
2755    ///     }
2756    ///     new_data
2757    /// }
2758    ///
2759    /// node_data.set_merge_callback(merge_video);
2760    /// ```
2761    #[inline]
2762    pub fn set_merge_callback<C: Into<DatasetMergeCallback>>(&mut self, callback: C) {
2763        self.extra
2764            .get_or_insert_with(|| Box::new(NodeDataExt::default()))
2765            .dataset_merge_callback = Some(callback.into());
2766    }
2767
2768    /// Gets the merge callback for this node, if set.
2769    #[inline]
2770    #[must_use] pub fn get_merge_callback(&self) -> Option<DatasetMergeCallback> {
2771        self.extra.as_ref().and_then(|ext| ext.dataset_merge_callback.clone())
2772    }
2773
2774    /// Sets the component origin for this node.
2775    ///
2776    /// This stamps the node with information about which component rendered it,
2777    /// enabling the debugger to reconstruct the component invocation tree.
2778    #[inline]
2779    pub fn set_component_origin(&mut self, origin: ComponentOrigin) {
2780        self.extra
2781            .get_or_insert_with(|| Box::new(NodeDataExt::default()))
2782            .component_origin = Some(origin);
2783    }
2784
2785    /// Gets the component origin for this node, if set.
2786    #[inline]
2787    #[must_use] pub fn get_component_origin(&self) -> Option<&ComponentOrigin> {
2788        self.extra.as_ref().and_then(|ext| ext.component_origin.as_ref())
2789    }
2790
2791    #[inline]
2792    #[must_use] pub fn with_menu_bar(mut self, menu_bar: Menu) -> Self {
2793        self.set_menu_bar(menu_bar);
2794        self
2795    }
2796
2797    #[inline]
2798    #[must_use] pub fn with_context_menu(mut self, context_menu: Menu) -> Self {
2799        self.set_context_menu(context_menu);
2800        self
2801    }
2802
2803    #[inline]
2804    pub fn add_callback<C: Into<CoreCallback>>(
2805        &mut self,
2806        event: EventFilter,
2807        data: RefAny,
2808        callback: C,
2809    ) {
2810        let callback = callback.into();
2811        let mut v: CoreCallbackDataVec = Vec::new().into();
2812        mem::swap(&mut v, &mut self.callbacks);
2813        let mut v = v.into_library_owned_vec();
2814        v.push(CoreCallbackData {
2815            event,
2816            refany: data,
2817            callback,
2818        });
2819        self.callbacks = v.into();
2820    }
2821
2822    #[inline]
2823    pub fn add_id(&mut self, s: AzString) {
2824        let mut v: AttributeTypeVec = Vec::new().into();
2825        mem::swap(&mut v, self.attributes_mut());
2826        let mut v = v.into_library_owned_vec();
2827        v.push(AttributeType::Id(s));
2828        self.set_attributes(v.into());
2829    }
2830    #[inline]
2831    pub fn add_class(&mut self, s: AzString) {
2832        let mut v: AttributeTypeVec = Vec::new().into();
2833        mem::swap(&mut v, self.attributes_mut());
2834        let mut v = v.into_library_owned_vec();
2835        v.push(AttributeType::Class(s));
2836        self.set_attributes(v.into());
2837    }
2838
2839    /// Add a CSS property with optional conditions (hover, focus, active, etc.).
2840    ///
2841    /// Wraps the property in a single-declaration rule at `rule_priority::INLINE`
2842    /// and appends it to this node's inline style.
2843    #[inline]
2844    pub fn add_css_property(&mut self, p: CssPropertyWithConditions) {
2845        use azul_css::css::{rule_priority, CssDeclaration, CssPath, CssRuleBlock};
2846        let rule = CssRuleBlock {
2847            path: CssPath { selectors: Vec::new().into() },
2848            declarations: vec![CssDeclaration::Static(p.property)].into(),
2849            conditions: p.apply_if,
2850            priority: rule_priority::INLINE,
2851        };
2852        let mut v: azul_css::css::CssRuleBlockVec = Vec::new().into();
2853        mem::swap(&mut v, &mut self.style.rules);
2854        let mut v = v.into_library_owned_vec();
2855        v.push(rule);
2856        self.style.rules = v.into();
2857    }
2858
2859    /// Calculates a deterministic node hash for this node.
2860    #[must_use] pub fn calculate_node_data_hash(&self) -> DomNodeHash {
2861        use core::hash::Hasher;
2862        let mut hasher = crate::hash::DefaultHasher::new();
2863        self.hash(&mut hasher);
2864        let h = hasher.finish();
2865        DomNodeHash { inner: h }
2866    }
2867
2868    /// Calculates a structural hash for DOM reconciliation that ignores text content.
2869    ///
2870    /// This hash is used for matching nodes across DOM frames where the text content
2871    /// may have changed (e.g., contenteditable text being edited). It hashes:
2872    /// - Node type discriminant (but NOT the text content for Text nodes)
2873    /// - IDs and classes
2874    /// - Attributes (but NOT contenteditable state which may change with focus)
2875    /// - Callback events and types
2876    ///
2877    /// This allows a Text("Hello") node to match Text("Hello World") during reconciliation,
2878    /// preserving cursor position and selection state.
2879    #[must_use] pub fn calculate_structural_hash(&self) -> DomNodeHash {
2880        use core::hash::Hasher;
2881        use core::hash::Hasher as StdHasher;
2882
2883        let mut hasher = crate::hash::DefaultHasher::new();
2884
2885        // Hash node type discriminant only, not content
2886        // This means Text("A") and Text("B") have the same structural hash
2887        mem::discriminant(&self.node_type).hash(&mut hasher);
2888
2889        // For VirtualView nodes, hash the callback to distinguish different virtualized views
2890        if self.node_type == NodeType::VirtualView {
2891            if let Some(ext) = self.extra.as_ref() {
2892                if let Some(vv) = ext.virtual_view.as_ref() {
2893                    vv.hash(&mut hasher);
2894                }
2895            }
2896        }
2897
2898        // For Image nodes, hash the image reference to distinguish different images.
2899        // For callback images, hash the callback function pointer and RefAny type ID
2900        // instead of the heap pointer, so that the same callback produces the same
2901        // structural hash across frames (the heap pointer differs each frame because
2902        // ImageRef::new() does Box::into_raw(Box::new(...))).
2903        if let NodeType::Image(ref img_ref) = self.node_type {
2904            match img_ref.get_data() {
2905                crate::resources::DecodedImage::Callback(cb) => {
2906                    // Hash callback function pointer (stable across frames)
2907                    cb.callback.cb.hash(&mut hasher);
2908                    // Hash RefAny type ID (not instance pointer)
2909                    cb.refany.get_type_id().hash(&mut hasher);
2910                }
2911                _ => {
2912                    // Raw images / GL textures: hash normally (pointer identity)
2913                    img_ref.hash(&mut hasher);
2914                }
2915            }
2916        }
2917
2918        // Hash IDs and classes - these are structural and shouldn't change
2919        // (They are now stored as AttributeType::Id / AttributeType::Class in attributes)
2920        for attr in self.attributes().as_ref() {
2921            match attr {
2922                AttributeType::Id(s) => { 0u8.hash(&mut hasher); s.as_str().hash(&mut hasher); }
2923                AttributeType::Class(s) => { 1u8.hash(&mut hasher); s.as_str().hash(&mut hasher); }
2924                _ => {}
2925            }
2926        }
2927
2928        // Hash other attributes - but skip contenteditable since that might change
2929        // Also skip Id/Class since they were already hashed above
2930        for attr in self.attributes().as_ref() {
2931            if !matches!(attr, AttributeType::ContentEditable(_) | AttributeType::Id(_) | AttributeType::Class(_)) {
2932                attr.hash(&mut hasher);
2933            }
2934        }
2935
2936        // Hash callback events (not the actual callback function pointers)
2937        for callback in self.callbacks.as_ref() {
2938            callback.event.hash(&mut hasher);
2939        }
2940
2941        let h = hasher.finish();
2942        DomNodeHash { inner: h }
2943    }
2944
2945    #[inline]
2946    #[must_use] pub const fn with_tab_index(mut self, tab_index: TabIndex) -> Self {
2947        self.set_tab_index(tab_index);
2948        self
2949    }
2950    #[inline]
2951    #[must_use] pub const fn with_contenteditable(mut self, contenteditable: bool) -> Self {
2952        self.set_contenteditable(contenteditable);
2953        self
2954    }
2955    #[inline]
2956    #[must_use] pub fn with_node_type(mut self, node_type: NodeType) -> Self {
2957        self.set_node_type(node_type);
2958        self
2959    }
2960    #[inline]
2961    #[must_use]
2962    pub fn with_callback<C: Into<CoreCallback>>(
2963        mut self,
2964        event: EventFilter,
2965        data: RefAny,
2966        callback: C,
2967    ) -> Self {
2968        self.add_callback(event, data, callback);
2969        self
2970    }
2971    #[inline]
2972    #[must_use] pub fn with_dataset(mut self, data: OptionRefAny) -> Self {
2973        self.set_dataset(data);
2974        self
2975    }
2976    #[inline]
2977    #[must_use] pub fn with_ids_and_classes(mut self, ids_and_classes: IdOrClassVec) -> Self {
2978        self.set_ids_and_classes(ids_and_classes);
2979        self
2980    }
2981    #[inline]
2982    #[must_use] pub fn with_callbacks(mut self, callbacks: CoreCallbackDataVec) -> Self {
2983        self.callbacks = callbacks;
2984        self
2985    }
2986    /// Legacy: builder-form of `set_css_props`. Each `CssPropertyWithConditions`
2987    /// becomes a single-declaration rule at `rule_priority::INLINE`.
2988    /// Prefer `with_style(Css)` for new code.
2989    #[inline]
2990    #[must_use] pub fn with_css_props(mut self, css_props: CssPropertyWithConditionsVec) -> Self {
2991        self.style = css_props.into();
2992        self
2993    }
2994    /// Builder-form of `set_style`.
2995    #[inline]
2996    #[must_use] pub fn with_style(mut self, style: azul_css::css::Css) -> Self {
2997        self.style = style;
2998        self
2999    }
3000
3001    /// Assigns a stable key to this node for reconciliation.
3002    ///
3003    /// This is crucial for performance and correct state preservation when
3004    /// lists of items change order or items are inserted/removed. Without keys,
3005    /// the reconciliation algorithm falls back to hash-based matching.
3006    ///
3007    /// # Example
3008    /// ```rust
3009    /// # use azul_core::dom::NodeData;
3010    /// NodeData::create_div()
3011    ///     .with_key("user-avatar-123");
3012    /// ```
3013    #[inline]
3014    #[must_use]
3015    pub fn with_key<K: Hash>(mut self, key: K) -> Self {
3016        self.set_key(key);
3017        self
3018    }
3019
3020    /// Registers a callback to merge dataset state from the previous frame.
3021    ///
3022    /// This is used for components that maintain heavy internal state (video players,
3023    /// WebGL contexts, network connections) that should not be destroyed and recreated
3024    /// on every render frame.
3025    ///
3026    /// The callback receives both datasets as `RefAny` (cheap shallow clones) and
3027    /// returns the `RefAny` that should be used for the new node.
3028    ///
3029    /// # Example
3030    /// ```rust,ignore
3031    /// struct VideoPlayer {
3032    ///     url: String,
3033    ///     decoder_handle: Option<DecoderHandle>,
3034    /// }
3035    ///
3036    /// extern "C" fn merge_video(new_data: RefAny, old_data: RefAny) -> RefAny {
3037    ///     if let (Some(mut new), Some(old)) = (
3038    ///         new_data.downcast_mut::<VideoPlayer>(),
3039    ///         old_data.downcast_ref::<VideoPlayer>()
3040    ///     ) {
3041    ///         new.decoder_handle = old.decoder_handle.take();
3042    ///     }
3043    ///     new_data
3044    /// }
3045    ///
3046    /// NodeData::create_div()
3047    ///     .with_dataset(RefAny::new(VideoPlayer::new("movie.mp4")).into())
3048    ///     .with_merge_callback(merge_video)
3049    /// ```
3050    #[inline]
3051    #[must_use]
3052    pub fn with_merge_callback<C: Into<DatasetMergeCallback>>(mut self, callback: C) -> Self {
3053        self.set_merge_callback(callback);
3054        self
3055    }
3056
3057    /// Parse and set CSS styles with full selector support.
3058    ///
3059    /// This is the unified API for setting inline CSS on a node. It supports:
3060    /// - Simple properties: `color: red; font-size: 14px;`
3061    /// - Pseudo-selectors: `:hover { background: blue; }`
3062    /// - @-rules: `@os linux { font-size: 14px; }`
3063    /// - Nesting: `@os linux { font-size: 14px; :hover { color: red; }}`
3064    ///
3065    /// # Examples
3066    /// ```rust
3067    /// # use azul_core::dom::NodeData;
3068    /// NodeData::create_div().with_css("
3069    ///     color: blue;
3070    ///     :hover { color: red; }
3071    ///     @os linux { font-size: 14px; }
3072    /// ");
3073    /// ```
3074    pub fn set_css(&mut self, style: &str) {
3075        // Parse via Css::parse_inline so the inline path goes through the same
3076        // selector + nesting machinery as author CSS. Rules are tagged
3077        // `rule_priority::INLINE` and appended to whatever this node already has.
3078        let parsed = azul_css::css::Css::parse_inline(style);
3079        let mut current: azul_css::css::CssRuleBlockVec = Vec::new().into();
3080        mem::swap(&mut current, &mut self.style.rules);
3081        let mut v = current.into_library_owned_vec();
3082        v.extend(parsed.rules.into_library_owned_vec());
3083        self.style.rules = v.into();
3084    }
3085
3086    /// Builder method for `set_css`
3087    #[must_use] pub fn with_css(mut self, style: &str) -> Self {
3088        self.set_css(style);
3089        self
3090    }
3091
3092    #[inline]
3093    #[must_use]
3094    pub const fn swap_with_default(&mut self) -> Self {
3095        let mut s = Self::create_div();
3096        mem::swap(&mut s, self);
3097        s
3098    }
3099
3100    #[inline]
3101    #[must_use] pub fn copy_special(&self) -> Self {
3102        Self {
3103            node_type: self.node_type.to_library_owned_nodetype(),
3104            style: self.style.clone(),
3105            callbacks: self.callbacks.clone(),
3106            flags: self.flags,
3107            accessibility: self.accessibility.clone(),
3108            extra: self.extra.clone(),
3109        }
3110    }
3111
3112    /// Like [`copy_special`], but MOVES the inline `style` and the `extra` (`NodeDataExt`)
3113    /// box out of `self` into the returned copy instead of cloning them.
3114    ///
3115    /// Both the derived `Clone` for the `CssProperty` values inside `style` AND the derived
3116    /// `Clone` for `Box<NodeDataExt>` (which transitively clones an `AttributeTypeVec` of
3117    /// `AzString`s, menus, etc.) lower to indirect-jump jump tables that remill mis-lifts on
3118    /// the web backend: the mis-lifted clone reads/writes wrong-sized data, which on the
3119    /// stack clobbers the adjacent `style` temporary inside `copy_special` and produces a
3120    /// "memory access out of bounds" later in the cascade (`StyledDom::create` → `restyle`'s
3121    /// inheritance loop reads the corrupted `style`). Native builds are unaffected.
3122    ///
3123    /// `convert_dom_into_compact_dom` consumes the `Dom`, so moving these fields out is sound:
3124    /// `copy_special` then clones an EMPTY style + `None` extra (no broken clone runs), and we
3125    /// restore the moved-out values afterward. Mirrors the pre-existing `style`-only fix.
3126    pub(crate) fn copy_special_moving_complex(&mut self) -> Self {
3127        // WEB-LIFT (2026-06-03): `copy_special`'s `to_library_owned_nodetype()` RECONSTRUCTS the
3128        // node_type (Text/Image arms clone the boxed AzString + rebuild the variant); the lifted
3129        // sret store of that data-bearing variant DROPS the whole thing (disc 177->0 AND the box
3130        // ptr -> styled_dom text node_type = all-zero, box LOST). Earlier attempts to fix this
3131        // "trapped" — but that was the missing `-C target-feature=-lse` build flag (LSE atomics
3132        // remill can't lift), NOT this code. With -lse + the fork remill, MOVE the node_type out
3133        // bitwise instead of reconstructing it: transfers the ORIGINAL box (preserving disc + the
3134        // AzString) with no clone. The Dom is consumed by convert_dom_into_compact_dom so moving is
3135        // sound; self.node_type becomes Div (no heap) -> dropped trivially. ptr::write avoids
3136        // dropping copy's placeholder Div (whose auto-Drop disc-match could mis-lift).
3137        let taken_style = mem::take(&mut self.style);
3138        let taken_extra = self.extra.take();
3139        let taken_node_type = mem::replace(&mut self.node_type, NodeType::Div);
3140        let mut copy = self.copy_special();
3141        // SAFETY: `&raw mut copy.node_type` is aligned and points at an initialized
3142        // `NodeType` (the placeholder `Div` that `copy_special` reconstructed from
3143        // `self.node_type`, which we replaced with `NodeType::Div` above). `ptr::write`
3144        // overwrites it WITHOUT running its `Drop` — this is deliberate (the Drop
3145        // mis-lifts on the web backend) and leaks nothing, because the overwritten
3146        // value is a heap-free `Div`. Kept unsafe (not a plain `=` assignment)
3147        // specifically to skip that Drop.
3148        unsafe { core::ptr::write(&raw mut copy.node_type, taken_node_type); }
3149        copy.style = taken_style;
3150        copy.extra = taken_extra;
3151        copy
3152    }
3153
3154    #[must_use] pub fn is_focusable(&self) -> bool {
3155        // Inherently focusable elements per HTML spec
3156        if matches!(self.node_type,
3157            NodeType::A | NodeType::Button | NodeType::Input
3158            | NodeType::Select | NodeType::TextArea
3159        ) {
3160            return true;
3161        }
3162        // Contenteditable elements are implicitly focusable (W3C spec)
3163        if self.is_contenteditable() {
3164            return true;
3165        }
3166        // Element is focusable if it has a tab index or any focus-related callback
3167        self.get_tab_index().is_some()
3168            || self
3169                .get_callbacks()
3170                .iter()
3171                .any(|cb| cb.event.is_focus_callback())
3172    }
3173
3174    /// Returns true if this element has "activation behavior" per HTML5 spec.
3175    ///
3176    /// Elements with activation behavior can be activated via Enter or Space key
3177    /// when focused, which generates a synthetic click event.
3178    ///
3179    /// Per HTML5 spec, elements with activation behavior include:
3180    /// - Button elements
3181    /// - Input elements (submit, button, reset, checkbox, radio)
3182    /// - Anchor elements with href
3183    /// - Any element with a click callback (implicit activation)
3184    ///
3185    /// See: <https://html.spec.whatwg.org/multipage/interaction.html#activation-behavior>
3186    #[must_use] pub fn has_activation_behavior(&self) -> bool {
3187        use crate::events::{EventFilter, HoverEventFilter};
3188
3189        // Inherently activatable elements per HTML spec
3190        if matches!(self.node_type, NodeType::A | NodeType::Button) {
3191            return true;
3192        }
3193
3194        // Check for click callback (most common case for Azul)
3195        // In Azul, "click" is typically LeftMouseUp
3196        let has_click_callback = self
3197            .get_callbacks()
3198            .iter()
3199            .any(|cb| matches!(
3200                cb.event,
3201                EventFilter::Hover(HoverEventFilter::MouseUp | HoverEventFilter::LeftMouseUp)
3202            ));
3203
3204        if has_click_callback {
3205            return true;
3206        }
3207
3208        // Check accessibility role for button-like elements
3209        if let Some(ref accessibility) = self.accessibility {
3210            use crate::a11y::AccessibilityRole;
3211            match accessibility.role {
3212                AccessibilityRole::PushButton  // Button
3213                | AccessibilityRole::Link
3214                | AccessibilityRole::CheckButton  // Checkbox
3215                | AccessibilityRole::RadioButton  // Radio
3216                | AccessibilityRole::MenuItem
3217                | AccessibilityRole::PageTab  // Tab
3218                => return true,
3219                _ => {}
3220            }
3221        }
3222
3223        false
3224    }
3225
3226    /// Returns true if this element is currently activatable.
3227    ///
3228    /// An element is activatable if it has activation behavior AND is not disabled.
3229    /// This checks for common disability patterns (aria-disabled, disabled attribute).
3230    #[must_use] pub fn is_activatable(&self) -> bool {
3231        if !self.has_activation_behavior() {
3232            return false;
3233        }
3234
3235        // Check for disabled state in accessibility info
3236        if let Some(ref accessibility) = self.accessibility {
3237            // Check if explicitly marked as unavailable
3238            if accessibility
3239                .states
3240                .as_ref()
3241                .iter()
3242                .any(|s| matches!(s, AccessibilityState::Unavailable))
3243            {
3244                return false;
3245            }
3246        }
3247
3248        // Not disabled, so activatable
3249        true
3250    }
3251
3252    /// Returns the tab index for this element.
3253    ///
3254    /// Tab index determines keyboard navigation order:
3255    /// - `None`: Not in tab order (unless naturally focusable)
3256    /// - `Some(-1)`: Focusable programmatically but not via Tab
3257    /// - `Some(0)`: In natural tab order
3258    /// - `Some(n > 0)`: In tab order with priority n (higher = later)
3259    #[must_use] pub fn get_effective_tabindex(&self) -> Option<i32> {
3260        self.flags.get_tab_index().map_or_else(|| if self.get_callbacks().iter().any(|cb| cb.event.is_focus_callback()) {
3261                    Some(0)
3262                } else {
3263                    None
3264                }, |tab_idx| match tab_idx {
3265                    TabIndex::Auto => Some(0),
3266                    TabIndex::OverrideInParent(n) => Some(i32::try_from(n).unwrap_or(i32::MAX)),
3267                    TabIndex::NoKeyboardFocus => Some(-1),
3268                })
3269    }
3270
3271    /// Returns the accessible label for this node.
3272    ///
3273    /// Priority: `aria-label` attribute > `alt` attribute > `title` attribute > None.
3274    /// Does NOT include child text — the caller should collect that separately
3275    /// using the DOM hierarchy.
3276    #[must_use] pub fn get_accessible_label(&self) -> Option<&str> {
3277        for attr in self.attributes().as_ref() {
3278            if let AttributeType::AriaLabel(s) = attr { return Some(s.as_str()) }
3279        }
3280        for attr in self.attributes().as_ref() {
3281            match attr {
3282                AttributeType::Alt(s) | AttributeType::Title(s) => return Some(s.as_str()),
3283                _ => {}
3284            }
3285        }
3286        None
3287    }
3288
3289    /// Returns the accessible value for this node.
3290    ///
3291    /// Priority: `value` attribute > None.
3292    /// For text inputs, this is the input's current value.
3293    #[must_use] pub fn get_accessible_value(&self) -> Option<&str> {
3294        for attr in self.attributes().as_ref() {
3295            if let AttributeType::Value(s) = attr {
3296                return Some(s.as_str());
3297            }
3298        }
3299        None
3300    }
3301
3302    /// Returns the placeholder text for this node.
3303    #[must_use] pub fn get_placeholder(&self) -> Option<&str> {
3304        for attr in self.attributes().as_ref() {
3305            if let AttributeType::Placeholder(s) = attr {
3306                return Some(s.as_str());
3307            }
3308        }
3309        None
3310    }
3311
3312    pub fn get_virtual_view_node(&mut self) -> Option<&mut VirtualViewNode> {
3313        self.extra.as_mut()?.virtual_view.as_mut()
3314    }
3315
3316    #[must_use] pub fn get_virtual_view_node_ref(&self) -> Option<&VirtualViewNode> {
3317        self.extra.as_ref()?.virtual_view.as_ref()
3318    }
3319
3320    pub fn get_render_image_callback_node(
3321        &mut self,
3322    ) -> Option<(&mut CoreImageCallback, ImageRefHash)> {
3323        match &mut self.node_type {
3324            NodeType::Image(ref mut img) => {
3325                let hash = image_ref_get_hash(img.as_ref());
3326                img.as_mut().get_image_callback_mut().map(|r| (r, hash))
3327            }
3328            _ => None,
3329        }
3330    }
3331
3332    pub fn debug_print_start(
3333        &self,
3334        css_cache: &CssPropertyCache,
3335        node_id: &NodeId,
3336        node_state: &StyledNodeState,
3337    ) -> String {
3338        let html_type = self.node_type.get_path();
3339        let attributes_string = node_data_to_string(self);
3340        let style = css_cache.get_computed_css_style_string(self, node_id, node_state);
3341        format!(
3342            "<{} data-az-node-id=\"{}\" {} {style}>",
3343            html_type,
3344            node_id.index(),
3345            attributes_string,
3346            style = if style.trim().is_empty() {
3347                String::new()
3348            } else {
3349                format!("style=\"{style}\"")
3350            }
3351        )
3352    }
3353
3354    #[must_use] pub fn debug_print_end(&self) -> String {
3355        let html_type = self.node_type.get_path();
3356        format!("</{html_type}>")
3357    }
3358}
3359
3360impl crate::events::ActivationBehavior for NodeData {
3361    fn has_activation_behavior(&self) -> bool {
3362        Self::has_activation_behavior(self)
3363    }
3364
3365    fn is_activatable(&self) -> bool {
3366        Self::is_activatable(self)
3367    }
3368}
3369
3370impl crate::events::Focusable for NodeData {
3371    fn get_tabindex(&self) -> Option<i32> {
3372        self.get_effective_tabindex()
3373    }
3374
3375    fn is_focusable(&self) -> bool {
3376        Self::is_focusable(self)
3377    }
3378
3379    fn is_naturally_focusable(&self) -> bool {
3380        matches!(
3381            self.node_type,
3382            NodeType::A
3383                | NodeType::Button
3384                | NodeType::Input
3385                | NodeType::Select
3386                | NodeType::TextArea
3387        )
3388    }
3389}
3390
3391/// A unique, runtime-generated identifier for a single `Dom` instance.
3392#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
3393#[repr(C)]
3394pub struct DomId {
3395    pub inner: usize,
3396}
3397
3398impl fmt::Display for DomId {
3399    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3400        write!(f, "{}", self.inner)
3401    }
3402}
3403
3404impl DomId {
3405    pub const ROOT_ID: Self = Self { inner: 0 };
3406}
3407
3408impl Default for DomId {
3409    fn default() -> Self {
3410        Self::ROOT_ID
3411    }
3412}
3413
3414impl_option!(
3415    DomId,
3416    OptionDomId,
3417    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
3418);
3419
3420impl_vec!(DomId, DomIdVec, DomIdVecDestructor, DomIdVecDestructorType, DomIdVecSlice, OptionDomId);
3421impl_vec_debug!(DomId, DomIdVec);
3422impl_vec_clone!(DomId, DomIdVec, DomIdVecDestructor);
3423impl_vec_partialeq!(DomId, DomIdVec);
3424impl_vec_partialord!(DomId, DomIdVec);
3425
3426/// A UUID for a DOM node within a `LayoutWindow`.
3427#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
3428#[repr(C)]
3429pub struct DomNodeId {
3430    /// The ID of the `Dom` this node belongs to.
3431    pub dom: DomId,
3432    /// The hierarchical ID of the node within its `Dom`.
3433    pub node: NodeHierarchyItemId,
3434}
3435
3436impl_option!(
3437    DomNodeId,
3438    OptionDomNodeId,
3439    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
3440);
3441
3442impl DomNodeId {
3443    pub const ROOT: Self = Self {
3444        dom: DomId::ROOT_ID,
3445        node: NodeHierarchyItemId::NONE,
3446    };
3447}
3448
3449/// The document model, similar to HTML. This is a create-only structure, you don't actually read
3450/// anything back from it. It's designed for ease of construction.
3451///
3452/// This is the "slow" tree-based DOM. For bulk construction (XML parsing),
3453/// use `FastDom` which builds flat arenas directly and skips the tree→arena conversion.
3454#[repr(C)]
3455#[derive(PartialEq, Clone)]
3456pub struct Dom {
3457    /// The data for the root node of this DOM (or sub-DOM).
3458    pub root: NodeData,
3459    /// The children of this DOM node.
3460    pub children: DomVec,
3461    /// Ordered list of CSS stylesheets to apply to this DOM subtree.
3462    /// Stylesheets are applied in push order during the single deferred cascade pass.
3463    /// Later entries override earlier ones (higher cascade priority).
3464    pub css: azul_css::css::CssVec,
3465    // Tracks the number of sub-children of the current children, so that
3466    // the `Dom` can be converted into a `CompactDom`.
3467    //
3468    // AUDIT: this is a cached count that MUST equal the recursive
3469    // `1-per-descendant` total of `children`. The builder methods
3470    // (`add_child` / `set_children` / `with_child*` / `FromIterator`) keep it in
3471    // sync, but `children` is a public field — mutating it directly desyncs this
3472    // counter. A too-small value makes `convert_dom_into_compact_dom` under-allocate
3473    // its arenas and panic on out-of-bounds writes. Call
3474    // `fixup_children_estimated()` after any direct `children` mutation;
3475    // `StyledDom::new` already does so as a safety net. Debug builds assert
3476    // consistency in the builder methods (see `recompute_estimated_total_children`).
3477    pub estimated_total_children: usize,
3478}
3479
3480/// CSS stylesheet associated with a specific node ID in the flat arena.
3481///
3482/// In the tree DOM, each node carries its own `css` field. In the flat arena,
3483/// we record which node a stylesheet scopes to (e.g. for `<style>` tags
3484/// in different parts of the document).
3485#[repr(C)]
3486#[derive(Debug, Clone, PartialEq, Eq, PartialOrd)]
3487pub struct CssWithNodeId {
3488    /// 1-based encoded `NodeId` (0 = root / global scope).
3489    pub node_id: usize,
3490    /// The CSS stylesheet.
3491    pub css: azul_css::css::Css,
3492}
3493
3494impl_vec!(CssWithNodeId, CssWithNodeIdVec, CssWithNodeIdVecDestructor, CssWithNodeIdVecDestructorType, CssWithNodeIdVecSlice, OptionCssWithNodeId);
3495impl_option!(CssWithNodeId, OptionCssWithNodeId, copy = false, [Debug, Clone, PartialEq, Eq, PartialOrd]);
3496impl_vec_clone!(CssWithNodeId, CssWithNodeIdVec, CssWithNodeIdVecDestructor);
3497impl_vec_mut!(CssWithNodeId, CssWithNodeIdVec);
3498impl_vec_debug!(CssWithNodeId, CssWithNodeIdVec);
3499impl_vec_partialord!(CssWithNodeId, CssWithNodeIdVec);
3500impl_vec_partialeq!(CssWithNodeId, CssWithNodeIdVec);
3501
3502/// Arena-based DOM for bulk construction (e.g. XML/XHTML parsing).
3503/// The hierarchy and node data are stored in two parallel flat vectors,
3504/// skipping the tree→arena conversion step entirely.
3505///
3506/// Use `FastDom::into_dom()` to convert to a tree-based `Dom` if needed.
3507/// `StyledDom::create_from_fast_dom()` consumes this directly without conversion.
3508#[repr(C)]
3509#[derive(Debug, Clone, PartialEq, PartialOrd)]
3510pub struct FastDom {
3511    /// Flat arena of parent/child/sibling relationships.
3512    pub node_hierarchy: crate::styled_dom::NodeHierarchyItemVec,
3513    /// Flat arena of node data, parallel to `node_hierarchy`.
3514    pub node_data: NodeDataVec,
3515    /// CSS stylesheets with the node ID they scope to.
3516    pub css: CssWithNodeIdVec,
3517}
3518
3519// Manual Eq/Hash/Ord impls that skip the transient `css` field,
3520// since CssVec does not implement Eq/Hash/Ord.
3521impl Eq for Dom {}
3522
3523impl Hash for Dom {
3524    fn hash<H: Hasher>(&self, state: &mut H) {
3525        self.root.hash(state);
3526        self.children.hash(state);
3527        self.estimated_total_children.hash(state);
3528    }
3529}
3530
3531// PartialOrd delegates to the field-wise Ord so the two never diverge.
3532impl PartialOrd for Dom {
3533    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
3534        Some(self.cmp(other))
3535    }
3536}
3537impl Ord for Dom {
3538    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
3539        self.root.cmp(&other.root)
3540            .then_with(|| self.children.cmp(&other.children))
3541            .then_with(|| self.estimated_total_children.cmp(&other.estimated_total_children))
3542    }
3543}
3544
3545impl_option!(
3546    Dom,
3547    OptionDom,
3548    copy = false,
3549    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
3550);
3551
3552impl_vec!(Dom, DomVec, DomVecDestructor, DomVecDestructorType, DomVecSlice, OptionDom);
3553impl_vec_clone!(Dom, DomVec, DomVecDestructor);
3554impl_vec_mut!(Dom, DomVec);
3555impl_vec_debug!(Dom, DomVec);
3556impl_vec_partialord!(Dom, DomVec);
3557impl_vec_ord!(Dom, DomVec);
3558impl_vec_partialeq!(Dom, DomVec);
3559impl_vec_eq!(Dom, DomVec);
3560impl_vec_hash!(Dom, DomVec);
3561
3562/// An empty `<body>` DOM. Used as the safe fallback return value when a layout
3563/// callback cannot produce a DOM (e.g. a foreign-language binding's trampoline
3564/// raised, or an app-data downcast failed). `StyledDom` is the post-cascade
3565/// CSSOM; layout callbacks return an un-cascaded `Dom`, so an empty body is the
3566/// natural "nothing to show" default.
3567impl Default for Dom {
3568    fn default() -> Self {
3569        Self::create_body()
3570    }
3571}
3572
3573impl Dom {
3574    // ----- DOM CONSTRUCTORS
3575
3576    /// Creates an empty DOM with a give `NodeType`. Note: This is a `const fn` and
3577    /// doesn't allocate, it only allocates once you add at least one child node.
3578    #[inline]
3579    #[must_use] pub fn create_node(node_type: NodeType) -> Self {
3580        Self {
3581            root: NodeData::create_node(node_type),
3582            children: Vec::new().into(),
3583            css: Vec::new().into(),
3584            estimated_total_children: 0,
3585        }
3586    }
3587    #[inline]
3588    #[must_use] pub fn create_from_data(node_data: NodeData) -> Self {
3589        Self {
3590            root: node_data,
3591            children: Vec::new().into(),
3592            css: Vec::new().into(),
3593            estimated_total_children: 0,
3594        }
3595    }
3596
3597    // Document Structure Elements
3598
3599    /// Creates the root HTML element.
3600    ///
3601    /// **Accessibility**: The `<html>` element is the root of an HTML document and should have a
3602    /// `lang` attribute.
3603    #[inline]
3604    #[must_use] pub const fn create_html() -> Self {
3605        Self {
3606            root: NodeData::create_node(NodeType::Html),
3607            children: DomVec::from_const_slice(&[]),
3608            css: azul_css::css::CssVec::from_const_slice(&[]),
3609            estimated_total_children: 0,
3610        }
3611    }
3612
3613    /// Creates the document head element.
3614    ///
3615    /// **Accessibility**: The `<head>` contains metadata. Use `<title>` for page titles.
3616    #[inline]
3617    #[must_use] pub const fn create_head() -> Self {
3618        Self {
3619            root: NodeData::create_node(NodeType::Head),
3620            children: DomVec::from_const_slice(&[]),
3621            css: azul_css::css::CssVec::from_const_slice(&[]),
3622            estimated_total_children: 0,
3623        }
3624    }
3625
3626    #[inline]
3627    #[must_use] pub const fn create_body() -> Self {
3628        Self {
3629            root: NodeData::create_node(NodeType::Body),
3630            children: DomVec::from_const_slice(&[]),
3631            css: azul_css::css::CssVec::from_const_slice(&[]),
3632            estimated_total_children: 0,
3633        }
3634    }
3635
3636    /// Creates a generic block-level container.
3637    ///
3638    /// **Accessibility**: Prefer semantic elements like `<article>`, `<section>`, `<nav>` when
3639    /// applicable.
3640    #[inline]
3641    #[must_use] pub const fn create_div() -> Self {
3642        Self {
3643            root: NodeData::create_node(NodeType::Div),
3644            children: DomVec::from_const_slice(&[]),
3645            css: azul_css::css::CssVec::from_const_slice(&[]),
3646            estimated_total_children: 0,
3647        }
3648    }
3649
3650    // Semantic Structure Elements
3651
3652    /// Creates an article element.
3653    ///
3654    /// **Accessibility**: Represents self-contained content that could be distributed
3655    /// independently. Screen readers can navigate by articles. Consider adding aria-label for
3656    /// multiple articles.
3657    #[inline]
3658    #[must_use] pub const fn create_article() -> Self {
3659        Self {
3660            root: NodeData::create_node(NodeType::Article),
3661            children: DomVec::from_const_slice(&[]),
3662            css: azul_css::css::CssVec::from_const_slice(&[]),
3663            estimated_total_children: 0,
3664        }
3665    }
3666
3667    /// Creates a section element.
3668    ///
3669    /// **Accessibility**: Represents a thematic grouping of content with a heading.
3670    /// Should typically have a heading (h1-h6) as a child. Consider aria-labelledby.
3671    #[inline]
3672    #[must_use] pub const fn create_section() -> Self {
3673        Self {
3674            root: NodeData::create_node(NodeType::Section),
3675            children: DomVec::from_const_slice(&[]),
3676            css: azul_css::css::CssVec::from_const_slice(&[]),
3677            estimated_total_children: 0,
3678        }
3679    }
3680
3681    /// Creates a navigation element.
3682    ///
3683    /// **Accessibility**: Represents navigation links. Screen readers can jump to navigation.
3684    /// Use aria-label to distinguish multiple nav elements (e.g., "Main navigation", "Footer
3685    /// links").
3686    #[inline]
3687    #[must_use] pub const fn create_nav() -> Self {
3688        Self {
3689            root: NodeData::create_node(NodeType::Nav),
3690            children: DomVec::from_const_slice(&[]),
3691            css: azul_css::css::CssVec::from_const_slice(&[]),
3692            estimated_total_children: 0,
3693        }
3694    }
3695
3696    /// Creates an aside element.
3697    ///
3698    /// **Accessibility**: Represents content tangentially related to main content (sidebars,
3699    /// callouts). Screen readers announce this as complementary content.
3700    #[inline]
3701    #[must_use] pub const fn create_aside() -> Self {
3702        Self {
3703            root: NodeData::create_node(NodeType::Aside),
3704            children: DomVec::from_const_slice(&[]),
3705            css: azul_css::css::CssVec::from_const_slice(&[]),
3706            estimated_total_children: 0,
3707        }
3708    }
3709
3710    /// Creates a header element.
3711    ///
3712    /// **Accessibility**: Represents introductory content or navigational aids.
3713    /// Can be used for page headers or section headers.
3714    #[inline]
3715    #[must_use] pub const fn create_header() -> Self {
3716        Self {
3717            root: NodeData::create_node(NodeType::Header),
3718            children: DomVec::from_const_slice(&[]),
3719            css: azul_css::css::CssVec::from_const_slice(&[]),
3720            estimated_total_children: 0,
3721        }
3722    }
3723
3724    /// Creates a footer element.
3725    ///
3726    /// **Accessibility**: Represents footer for nearest section or page.
3727    /// Typically contains copyright, author info, or related links.
3728    #[inline]
3729    #[must_use] pub const fn create_footer() -> Self {
3730        Self {
3731            root: NodeData::create_node(NodeType::Footer),
3732            children: DomVec::from_const_slice(&[]),
3733            css: azul_css::css::CssVec::from_const_slice(&[]),
3734            estimated_total_children: 0,
3735        }
3736    }
3737
3738    /// Creates a main content element.
3739    ///
3740    /// **Accessibility**: Represents the dominant content. There should be only ONE main per page.
3741    /// Screen readers can jump directly to main content. Do not nest inside
3742    /// article/aside/footer/header/nav.
3743    #[inline]
3744    #[must_use] pub const fn create_main() -> Self {
3745        Self {
3746            root: NodeData::create_node(NodeType::Main),
3747            children: DomVec::from_const_slice(&[]),
3748            css: azul_css::css::CssVec::from_const_slice(&[]),
3749            estimated_total_children: 0,
3750        }
3751    }
3752
3753    /// Creates a figure element.
3754    ///
3755    /// **Accessibility**: Represents self-contained content like diagrams, photos, code listings.
3756    /// Use with `<figcaption>` to provide a caption. Screen readers associate caption with figure.
3757    #[inline]
3758    #[must_use] pub const fn create_figure() -> Self {
3759        Self {
3760            root: NodeData::create_node(NodeType::Figure),
3761            children: DomVec::from_const_slice(&[]),
3762            css: azul_css::css::CssVec::from_const_slice(&[]),
3763            estimated_total_children: 0,
3764        }
3765    }
3766
3767    /// Creates a figure caption element.
3768    ///
3769    /// **Accessibility**: Provides a caption for `<figure>`. Screen readers announce this as the
3770    /// figure description.
3771    #[inline]
3772    #[must_use] pub const fn create_figcaption() -> Self {
3773        Self {
3774            root: NodeData::create_node(NodeType::FigCaption),
3775            children: DomVec::from_const_slice(&[]),
3776            css: azul_css::css::CssVec::from_const_slice(&[]),
3777            estimated_total_children: 0,
3778        }
3779    }
3780
3781    // Interactive Elements
3782
3783    /// Creates a details disclosure element without accessibility information.
3784    ///
3785    /// Prefer [`Dom::create_details`] so that screen readers announce the
3786    /// disclosure widget's purpose.
3787    #[inline]
3788    #[must_use] pub const fn create_details_no_a11y() -> Self {
3789        Self {
3790            root: NodeData::create_node(NodeType::Details),
3791            children: DomVec::from_const_slice(&[]),
3792            css: azul_css::css::CssVec::from_const_slice(&[]),
3793            estimated_total_children: 0,
3794        }
3795    }
3796
3797    /// Creates a details disclosure element with accessibility information.
3798    ///
3799    /// **Accessibility**: Creates a disclosure widget. Screen readers announce expanded/collapsed
3800    /// state. Must contain a `<summary>` element. Keyboard accessible by default.
3801    ///
3802    /// Use [`Dom::create_details_no_a11y`] only as a deliberate escape hatch.
3803    #[inline]
3804    #[allow(clippy::needless_pass_by_value)] // owned azul C-ABI value taken by value (FFI ownership-transfer convention)
3805    #[must_use] pub fn create_details(aria: SmallAriaInfo) -> Self {
3806        Self::create_details_no_a11y().with_accessibility_info(aria.to_full_info())
3807    }
3808
3809    /// Creates an empty summary element for details without accessibility information.
3810    ///
3811    /// Prefer [`Dom::create_summary`] so that screen readers can announce the
3812    /// disclosure heading.
3813    #[inline]
3814    #[must_use] pub const fn create_summary_no_a11y() -> Self {
3815        Self {
3816            root: NodeData::create_node(NodeType::Summary),
3817            children: DomVec::from_const_slice(&[]),
3818            css: azul_css::css::CssVec::from_const_slice(&[]),
3819            estimated_total_children: 0,
3820        }
3821    }
3822
3823    /// Creates an empty summary element for details with accessibility information.
3824    ///
3825    /// **Accessibility**: The visible heading/label for `<details>`.
3826    /// Must be the first child of details. Keyboard accessible (Enter/Space to toggle).
3827    ///
3828    /// Use [`Dom::create_summary_no_a11y`] only as a deliberate escape hatch.
3829    #[inline]
3830    #[allow(clippy::needless_pass_by_value)] // owned azul C-ABI value taken by value (FFI ownership-transfer convention)
3831    #[must_use] pub fn create_summary(aria: SmallAriaInfo) -> Self {
3832        Self::create_summary_no_a11y().with_accessibility_info(aria.to_full_info())
3833    }
3834
3835    /// Creates a summary element with text without accessibility information.
3836    ///
3837    /// Prefer [`Dom::create_summary_with_text`] so that screen readers
3838    /// announce the disclosure heading.
3839    #[inline]
3840    pub fn create_summary_with_text_no_a11y<S: Into<AzString>>(text: S) -> Self {
3841        Self::create_summary_no_a11y().with_child(Self::create_text(text))
3842    }
3843
3844    /// Creates a summary element with text and accessibility information for details.
3845    ///
3846    /// **Accessibility**: The visible heading/label for `<details>`.
3847    /// Must be the first child of details. Keyboard accessible (Enter/Space to toggle).
3848    ///
3849    /// Use [`Dom::create_summary_with_text_no_a11y`] only as a deliberate escape hatch.
3850    #[inline]
3851    #[allow(clippy::needless_pass_by_value)] // owned azul C-ABI value taken by value (FFI ownership-transfer convention)
3852    pub fn create_summary_with_text<S: Into<AzString>>(text: S, aria: SmallAriaInfo) -> Self {
3853        Self::create_summary_with_text_no_a11y(text).with_accessibility_info(aria.to_full_info())
3854    }
3855
3856    /// Creates a dialog element without accessibility information.
3857    ///
3858    /// Prefer [`Dom::create_dialog`] so that the dialog's purpose, modality,
3859    /// and described-by relationship are surfaced to assistive technologies.
3860    #[inline]
3861    #[must_use] pub const fn create_dialog_no_a11y() -> Self {
3862        Self {
3863            root: NodeData::create_node(NodeType::Dialog),
3864            children: DomVec::from_const_slice(&[]),
3865            css: azul_css::css::CssVec::from_const_slice(&[]),
3866            estimated_total_children: 0,
3867        }
3868    }
3869
3870    /// Creates a dialog element with accessibility information.
3871    ///
3872    /// **Accessibility**: Represents a modal or non-modal dialog.
3873    /// When opened as modal, focus is trapped. Use aria-label or aria-labelledby.
3874    /// Escape key should close modal dialogs.
3875    ///
3876    /// Use [`Dom::create_dialog_no_a11y`] only as a deliberate escape hatch.
3877    #[inline]
3878    #[allow(clippy::needless_pass_by_value)] // owned azul C-ABI value taken by value (FFI ownership-transfer convention)
3879    #[must_use] pub fn create_dialog(aria: DialogAriaInfo) -> Self {
3880        Self::create_dialog_no_a11y().with_accessibility_info(aria.to_full_info())
3881    }
3882
3883    // Basic Structural Elements
3884
3885    #[inline]
3886    #[must_use] pub const fn create_br() -> Self {
3887        Self {
3888            root: NodeData::create_node(NodeType::Br),
3889            children: DomVec::from_const_slice(&[]),
3890            css: azul_css::css::CssVec::from_const_slice(&[]),
3891            estimated_total_children: 0,
3892        }
3893    }
3894    #[inline]
3895    pub fn create_text<S: Into<AzString>>(value: S) -> Self {
3896        Self::create_node(NodeType::Text(BoxOrStatic::heap(value.into())))
3897    }
3898    #[inline]
3899    #[must_use] pub fn create_image(image: ImageRef) -> Self {
3900        Self::create_node(NodeType::Image(BoxOrStatic::heap(image)))
3901    }
3902    /// Creates an icon node with the given icon name.
3903    ///
3904    /// The icon name should match names from the icon provider (e.g., "home", "settings", "search").
3905    /// Icons are resolved to actual content (font glyph, image, etc.) during `StyledDom` creation
3906    /// based on the configured `IconProvider`.
3907    ///
3908    /// # Example
3909    /// ```rust,ignore
3910    /// Dom::create_icon("home")
3911    ///     .with_class("nav-icon")
3912    /// ```
3913    #[inline]
3914    pub fn create_icon<S: Into<AzString>>(icon_name: S) -> Self {
3915        Self::create_node(NodeType::Icon(BoxOrStatic::heap(icon_name.into())))
3916    }
3917
3918    #[inline]
3919    pub fn create_virtual_view(data: RefAny, callback: impl Into<VirtualViewCallback>) -> Self {
3920        Self::create_from_data(NodeData::create_virtual_view(data, callback))
3921    }
3922
3923    /// Creates an invisible `NodeType::GeolocationProbe` node that
3924    /// signals "this subtree needs the user's location". Lays out as
3925    /// zero-size and is skipped in the display list - the framework
3926    /// scans for it at end-of-layout and starts / stops the native
3927    /// `CLLocationManager` / `LocationManager` / `geoclue`
3928    /// subscription. See `SUPER_PLAN_2.md` section 1.5.
3929    #[inline]
3930    #[must_use] pub fn create_geolocation_probe(config: crate::geolocation::GeolocationProbeConfig) -> Self {
3931        Self::create_node(NodeType::GeolocationProbe(config))
3932    }
3933
3934    // Semantic HTML Elements with Accessibility Guidance
3935
3936    /// Creates a paragraph element.
3937    ///
3938    /// **Accessibility**: Paragraphs provide semantic structure for screen readers.
3939    #[inline]
3940    #[must_use] pub const fn create_p() -> Self {
3941        Self {
3942            root: NodeData::create_node(NodeType::P),
3943            children: DomVec::from_const_slice(&[]),
3944            css: azul_css::css::CssVec::from_const_slice(&[]),
3945            estimated_total_children: 0,
3946        }
3947    }
3948
3949    /// Creates an empty heading level 1 element.
3950    ///
3951    /// **Accessibility**: Use `h1` for the main page title. There should typically be only one `h1`
3952    /// per page.
3953    #[inline]
3954    #[must_use] pub const fn create_h1() -> Self {
3955        Self {
3956            root: NodeData::create_node(NodeType::H1),
3957            children: DomVec::from_const_slice(&[]),
3958            css: azul_css::css::CssVec::from_const_slice(&[]),
3959            estimated_total_children: 0,
3960        }
3961    }
3962
3963    /// Creates a heading level 1 element with text.
3964    ///
3965    /// **Accessibility**: Use `h1` for the main page title. There should typically be only one `h1`
3966    /// per page.
3967    ///
3968    /// **Parameters:**
3969    /// - `text`: Heading text
3970    #[inline]
3971    pub fn create_h1_with_text<S: Into<AzString>>(text: S) -> Self {
3972        Self::create_h1().with_child(Self::create_text(text))
3973    }
3974
3975    /// Creates an empty heading level 2 element.
3976    ///
3977    /// **Accessibility**: Use `h2` for major section headings under `h1`.
3978    #[inline]
3979    #[must_use] pub const fn create_h2() -> Self {
3980        Self {
3981            root: NodeData::create_node(NodeType::H2),
3982            children: DomVec::from_const_slice(&[]),
3983            css: azul_css::css::CssVec::from_const_slice(&[]),
3984            estimated_total_children: 0,
3985        }
3986    }
3987
3988    /// Creates a heading level 2 element with text.
3989    ///
3990    /// **Accessibility**: Use `h2` for major section headings under `h1`.
3991    ///
3992    /// **Parameters:**
3993    /// - `text`: Heading text
3994    #[inline]
3995    pub fn create_h2_with_text<S: Into<AzString>>(text: S) -> Self {
3996        Self::create_h2().with_child(Self::create_text(text))
3997    }
3998
3999    /// Creates an empty heading level 3 element.
4000    ///
4001    /// **Accessibility**: Use `h3` for subsections under `h2`.
4002    #[inline]
4003    #[must_use] pub const fn create_h3() -> Self {
4004        Self {
4005            root: NodeData::create_node(NodeType::H3),
4006            children: DomVec::from_const_slice(&[]),
4007            css: azul_css::css::CssVec::from_const_slice(&[]),
4008            estimated_total_children: 0,
4009        }
4010    }
4011
4012    /// Creates a heading level 3 element with text.
4013    ///
4014    /// **Accessibility**: Use `h3` for subsections under `h2`.
4015    ///
4016    /// **Parameters:**
4017    /// - `text`: Heading text
4018    #[inline]
4019    pub fn create_h3_with_text<S: Into<AzString>>(text: S) -> Self {
4020        Self::create_h3().with_child(Self::create_text(text))
4021    }
4022
4023    /// Creates an empty heading level 4 element.
4024    #[inline]
4025    #[must_use] pub const fn create_h4() -> Self {
4026        Self {
4027            root: NodeData::create_node(NodeType::H4),
4028            children: DomVec::from_const_slice(&[]),
4029            css: azul_css::css::CssVec::from_const_slice(&[]),
4030            estimated_total_children: 0,
4031        }
4032    }
4033
4034    /// Creates a heading level 4 element with text.
4035    ///
4036    /// **Parameters:**
4037    /// - `text`: Heading text
4038    #[inline]
4039    pub fn create_h4_with_text<S: Into<AzString>>(text: S) -> Self {
4040        Self::create_h4().with_child(Self::create_text(text))
4041    }
4042
4043    /// Creates an empty heading level 5 element.
4044    #[inline]
4045    #[must_use] pub const fn create_h5() -> Self {
4046        Self {
4047            root: NodeData::create_node(NodeType::H5),
4048            children: DomVec::from_const_slice(&[]),
4049            css: azul_css::css::CssVec::from_const_slice(&[]),
4050            estimated_total_children: 0,
4051        }
4052    }
4053
4054    /// Creates a heading level 5 element with text.
4055    ///
4056    /// **Parameters:**
4057    /// - `text`: Heading text
4058    #[inline]
4059    pub fn create_h5_with_text<S: Into<AzString>>(text: S) -> Self {
4060        Self::create_h5().with_child(Self::create_text(text))
4061    }
4062
4063    /// Creates an empty heading level 6 element.
4064    #[inline]
4065    #[must_use] pub const fn create_h6() -> Self {
4066        Self {
4067            root: NodeData::create_node(NodeType::H6),
4068            children: DomVec::from_const_slice(&[]),
4069            css: azul_css::css::CssVec::from_const_slice(&[]),
4070            estimated_total_children: 0,
4071        }
4072    }
4073
4074    /// Creates a heading level 6 element with text.
4075    ///
4076    /// **Parameters:**
4077    /// - `text`: Heading text
4078    #[inline]
4079    pub fn create_h6_with_text<S: Into<AzString>>(text: S) -> Self {
4080        Self::create_h6().with_child(Self::create_text(text))
4081    }
4082
4083    /// Creates an empty generic inline container (span).
4084    ///
4085    /// **Accessibility**: Prefer semantic elements like `strong`, `em`, `code`, etc. when
4086    /// applicable.
4087    #[inline]
4088    #[must_use] pub const fn create_span() -> Self {
4089        Self {
4090            root: NodeData::create_node(NodeType::Span),
4091            children: DomVec::from_const_slice(&[]),
4092            css: azul_css::css::CssVec::from_const_slice(&[]),
4093            estimated_total_children: 0,
4094        }
4095    }
4096
4097    /// Creates a generic inline container (span) with text.
4098    ///
4099    /// **Accessibility**: Prefer semantic elements like `strong`, `em`, `code`, etc. when
4100    /// applicable.
4101    ///
4102    /// **Parameters:**
4103    /// - `text`: Span content
4104    #[inline]
4105    pub fn create_span_with_text<S: Into<AzString>>(text: S) -> Self {
4106        Self::create_span().with_child(Self::create_text(text))
4107    }
4108
4109    /// Creates an empty strong importance element.
4110    ///
4111    /// **Accessibility**: Use `strong` instead of `b` for semantic meaning.
4112    #[inline]
4113    #[must_use] pub const fn create_strong() -> Self {
4114        Self {
4115            root: NodeData::create_node(NodeType::Strong),
4116            children: DomVec::from_const_slice(&[]),
4117            css: azul_css::css::CssVec::from_const_slice(&[]),
4118            estimated_total_children: 0,
4119        }
4120    }
4121
4122    /// Creates a strongly emphasized text element with text (strong importance).
4123    ///
4124    /// **Accessibility**: Use `strong` instead of `b` for semantic meaning. Screen readers can
4125    /// convey the importance. Use for text that has strong importance, seriousness, or urgency.
4126    ///
4127    /// **Parameters:**
4128    /// - `text`: Text to emphasize
4129    #[inline]
4130    pub fn create_strong_with_text<S: Into<AzString>>(text: S) -> Self {
4131        Self::create_strong().with_child(Self::create_text(text))
4132    }
4133
4134    /// Creates an empty emphasis element (stress emphasis).
4135    ///
4136    /// **Accessibility**: Use `em` instead of `i` for semantic meaning.
4137    #[inline]
4138    #[must_use] pub const fn create_em() -> Self {
4139        Self {
4140            root: NodeData::create_node(NodeType::Em),
4141            children: DomVec::from_const_slice(&[]),
4142            css: azul_css::css::CssVec::from_const_slice(&[]),
4143            estimated_total_children: 0,
4144        }
4145    }
4146
4147    /// Creates an emphasized text element with text (stress emphasis).
4148    ///
4149    /// **Accessibility**: Use `em` instead of `i` for semantic meaning. Screen readers can
4150    /// convey the emphasis. Use for text that has stress emphasis.
4151    ///
4152    /// **Parameters:**
4153    /// - `text`: Text to emphasize
4154    #[inline]
4155    pub fn create_em_with_text<S: Into<AzString>>(text: S) -> Self {
4156        Self::create_em().with_child(Self::create_text(text))
4157    }
4158
4159    /// Creates an empty code element.
4160    ///
4161    /// **Accessibility**: Represents a fragment of computer code.
4162    #[inline]
4163    #[must_use] pub fn create_code() -> Self {
4164        Self::create_node(NodeType::Code)
4165    }
4166
4167    /// Creates a code/computer code element with text.
4168    ///
4169    /// **Accessibility**: Represents a fragment of computer code. Screen readers can identify
4170    /// this as code content.
4171    ///
4172    /// **Parameters:**
4173    /// - `code`: Code content
4174    #[inline]
4175    pub fn create_code_with_text<S: Into<AzString>>(code: S) -> Self {
4176        Self::create_code().with_child(Self::create_text(code))
4177    }
4178
4179    /// Creates an empty preformatted text element.
4180    ///
4181    /// **Accessibility**: Preserves whitespace and line breaks.
4182    #[inline]
4183    #[must_use] pub fn create_pre() -> Self {
4184        Self::create_node(NodeType::Pre)
4185    }
4186
4187    /// Creates a preformatted text element with text.
4188    ///
4189    /// **Accessibility**: Preserves whitespace and line breaks. Useful for code blocks or
4190    /// ASCII art. Screen readers will read the content as-is.
4191    ///
4192    /// **Parameters:**
4193    /// - `text`: Preformatted content
4194    #[inline]
4195    pub fn create_pre_with_text<S: Into<AzString>>(text: S) -> Self {
4196        Self::create_pre().with_child(Self::create_text(text))
4197    }
4198
4199    /// Creates an empty blockquote element.
4200    ///
4201    /// **Accessibility**: Represents a section quoted from another source.
4202    #[inline]
4203    #[must_use] pub fn create_blockquote() -> Self {
4204        Self::create_node(NodeType::BlockQuote)
4205    }
4206
4207    /// Creates a blockquote element with text.
4208    ///
4209    /// **Accessibility**: Represents a section quoted from another source. Screen readers
4210    /// can identify quoted content. Consider adding a `cite` attribute.
4211    ///
4212    /// **Parameters:**
4213    /// - `text`: Quote content
4214    #[inline]
4215    pub fn create_blockquote_with_text<S: Into<AzString>>(text: S) -> Self {
4216        Self::create_blockquote().with_child(Self::create_text(text))
4217    }
4218
4219    /// Creates an empty citation element.
4220    ///
4221    /// **Accessibility**: Represents a reference to a creative work.
4222    #[inline]
4223    #[must_use] pub fn create_cite() -> Self {
4224        Self::create_node(NodeType::Cite)
4225    }
4226
4227    /// Creates a citation element with text.
4228    ///
4229    /// **Accessibility**: Represents a reference to a creative work. Screen readers can
4230    /// identify citations.
4231    ///
4232    /// **Parameters:**
4233    /// - `text`: Citation text
4234    #[inline]
4235    pub fn create_cite_with_text<S: Into<AzString>>(text: S) -> Self {
4236        Self::create_cite().with_child(Self::create_text(text))
4237    }
4238
4239    /// Creates an empty abbreviation element.
4240    ///
4241    /// **Accessibility**: Represents an abbreviation or acronym. Use with a `title` attribute
4242    /// to provide the full expansion for screen readers.
4243    #[inline]
4244    #[must_use] pub fn create_abbr() -> Self {
4245        Self::create_node(NodeType::Abbr)
4246    }
4247
4248    /// Creates an abbreviation element with abbreviated text and a `title` expansion.
4249    ///
4250    /// **Accessibility**: Represents an abbreviation or acronym. The `title` attribute
4251    /// provides the full expansion for screen readers.
4252    ///
4253    /// **Parameters:**
4254    /// - `abbr_text`: Abbreviated text
4255    /// - `title`: Full expansion
4256    #[inline]
4257    #[must_use] pub fn create_abbr_with_title(abbr_text: AzString, title: AzString) -> Self {
4258        Self::create_node(NodeType::Abbr)
4259            .with_attribute(AttributeType::Title(title))
4260            .with_child(Self::create_text(abbr_text))
4261    }
4262
4263    /// Creates an empty keyboard input element.
4264    ///
4265    /// **Accessibility**: Represents keyboard input or key combinations.
4266    #[inline]
4267    #[must_use] pub fn create_kbd() -> Self {
4268        Self::create_node(NodeType::Kbd)
4269    }
4270
4271    /// Creates a keyboard input element with text.
4272    ///
4273    /// **Accessibility**: Represents keyboard input or key combinations. Screen readers can
4274    /// identify keyboard instructions.
4275    ///
4276    /// **Parameters:**
4277    /// - `text`: Keyboard instruction
4278    #[inline]
4279    pub fn create_kbd_with_text<S: Into<AzString>>(text: S) -> Self {
4280        Self::create_kbd().with_child(Self::create_text(text))
4281    }
4282
4283    /// Creates an empty sample output element.
4284    ///
4285    /// **Accessibility**: Represents sample output from a program or computing system.
4286    #[inline]
4287    #[must_use] pub fn create_samp() -> Self {
4288        Self::create_node(NodeType::Samp)
4289    }
4290
4291    /// Creates a sample output element with text.
4292    ///
4293    /// **Accessibility**: Represents sample output from a program or computing system.
4294    ///
4295    /// **Parameters:**
4296    /// - `text`: Sample text
4297    #[inline]
4298    pub fn create_samp_with_text<S: Into<AzString>>(text: S) -> Self {
4299        Self::create_samp().with_child(Self::create_text(text))
4300    }
4301
4302    /// Creates an empty variable element.
4303    ///
4304    /// **Accessibility**: Represents a variable in mathematical expressions or programming.
4305    #[inline]
4306    #[must_use] pub fn create_var() -> Self {
4307        Self::create_node(NodeType::Var)
4308    }
4309
4310    /// Creates a variable element with text.
4311    ///
4312    /// **Accessibility**: Represents a variable in mathematical expressions or programming.
4313    ///
4314    /// **Parameters:**
4315    /// - `text`: Variable name
4316    #[inline]
4317    pub fn create_var_with_text<S: Into<AzString>>(text: S) -> Self {
4318        Self::create_var().with_child(Self::create_text(text))
4319    }
4320
4321    /// Creates an empty subscript element.
4322    #[inline]
4323    #[must_use] pub fn create_sub() -> Self {
4324        Self::create_node(NodeType::Sub)
4325    }
4326
4327    /// Creates a subscript element with text.
4328    ///
4329    /// **Accessibility**: Screen readers may announce subscript formatting.
4330    ///
4331    /// **Parameters:**
4332    /// - `text`: Subscript content
4333    #[inline]
4334    pub fn create_sub_with_text<S: Into<AzString>>(text: S) -> Self {
4335        Self::create_sub().with_child(Self::create_text(text))
4336    }
4337
4338    /// Creates an empty superscript element.
4339    #[inline]
4340    #[must_use] pub fn create_sup() -> Self {
4341        Self::create_node(NodeType::Sup)
4342    }
4343
4344    /// Creates a superscript element with text.
4345    ///
4346    /// **Accessibility**: Screen readers may announce superscript formatting.
4347    ///
4348    /// **Parameters:**
4349    /// - `text`: Superscript content
4350    #[inline]
4351    pub fn create_sup_with_text<S: Into<AzString>>(text: S) -> Self {
4352        Self::create_sup().with_child(Self::create_text(text))
4353    }
4354
4355    /// Creates an empty underline element.
4356    #[inline]
4357    #[must_use] pub fn create_u() -> Self {
4358        Self::create_node(NodeType::U)
4359    }
4360
4361    /// Creates an underline text element with text.
4362    ///
4363    /// **Accessibility**: Screen readers typically don't announce underline formatting.
4364    /// Use semantic elements when possible (e.g., `<em>` for emphasis).
4365    #[inline]
4366    pub fn create_u_with_text<S: Into<AzString>>(text: S) -> Self {
4367        Self::create_u().with_child(Self::create_text(text))
4368    }
4369
4370    /// Creates an empty strikethrough element.
4371    #[inline]
4372    #[must_use] pub fn create_s() -> Self {
4373        Self::create_node(NodeType::S)
4374    }
4375
4376    /// Creates a strikethrough text element with text.
4377    ///
4378    /// **Accessibility**: Represents text that is no longer accurate or relevant.
4379    /// Consider using `<del>` for deleted content with datetime attribute.
4380    #[inline]
4381    pub fn create_s_with_text<S: Into<AzString>>(text: S) -> Self {
4382        Self::create_s().with_child(Self::create_text(text))
4383    }
4384
4385    /// Creates an empty mark element.
4386    #[inline]
4387    #[must_use] pub fn create_mark() -> Self {
4388        Self::create_node(NodeType::Mark)
4389    }
4390
4391    /// Creates a marked/highlighted text element with text.
4392    ///
4393    /// **Accessibility**: Represents text marked for reference or notation purposes.
4394    /// Screen readers may announce this as "highlighted".
4395    #[inline]
4396    pub fn create_mark_with_text<S: Into<AzString>>(text: S) -> Self {
4397        Self::create_mark().with_child(Self::create_text(text))
4398    }
4399
4400    /// Creates an empty deleted text element.
4401    #[inline]
4402    #[must_use] pub fn create_del() -> Self {
4403        Self::create_node(NodeType::Del)
4404    }
4405
4406    /// Creates a deleted text element with text.
4407    ///
4408    /// **Accessibility**: Represents deleted content in document edits.
4409    /// Use with `datetime` and `cite` attributes for edit tracking.
4410    #[inline]
4411    pub fn create_del_with_text<S: Into<AzString>>(text: S) -> Self {
4412        Self::create_del().with_child(Self::create_text(text))
4413    }
4414
4415    /// Creates an empty inserted text element.
4416    #[inline]
4417    #[must_use] pub fn create_ins() -> Self {
4418        Self::create_node(NodeType::Ins)
4419    }
4420
4421    /// Creates an inserted text element with text.
4422    ///
4423    /// **Accessibility**: Represents inserted content in document edits.
4424    /// Use with `datetime` and `cite` attributes for edit tracking.
4425    #[inline]
4426    pub fn create_ins_with_text<S: Into<AzString>>(text: S) -> Self {
4427        Self::create_ins().with_child(Self::create_text(text))
4428    }
4429
4430    /// Creates an empty definition element.
4431    #[inline]
4432    #[must_use] pub fn create_dfn() -> Self {
4433        Self::create_node(NodeType::Dfn)
4434    }
4435
4436    /// Creates a definition element with text.
4437    ///
4438    /// **Accessibility**: Represents the defining instance of a term.
4439    /// Often used within a definition list or with `<abbr>`.
4440    #[inline]
4441    pub fn create_dfn_with_text<S: Into<AzString>>(text: S) -> Self {
4442        Self::create_dfn().with_child(Self::create_text(text))
4443    }
4444
4445    /// Creates a time element.
4446    ///
4447    /// **Accessibility**: Represents a specific time or date.
4448    /// Use `datetime` attribute for machine-readable format.
4449    ///
4450    /// **Parameters:**
4451    /// - `text`: Human-readable time/date
4452    /// - `datetime`: Optional machine-readable datetime
4453    #[inline]
4454    #[must_use] pub fn create_time(text: AzString, datetime: OptionString) -> Self {
4455        let mut element = Self::create_node(NodeType::Time).with_child(Self::create_text(text));
4456        if let OptionString::Some(dt) = datetime {
4457            element = element.with_attribute(AttributeType::Custom(AttributeNameValue {
4458                attr_name: "datetime".into(),
4459                value: dt,
4460            }));
4461        }
4462        element
4463    }
4464
4465    /// Creates an empty bi-directional override element.
4466    ///
4467    /// **Accessibility**: Overrides text direction. Use `dir` attribute (ltr/rtl).
4468    #[inline]
4469    #[must_use] pub fn create_bdo() -> Self {
4470        Self::create_node(NodeType::Bdo)
4471    }
4472
4473    /// Creates a bi-directional override element with text.
4474    ///
4475    /// **Accessibility**: Overrides text direction. Use `dir` attribute (ltr/rtl).
4476    #[inline]
4477    pub fn create_bdo_with_text<S: Into<AzString>>(text: S) -> Self {
4478        Self::create_bdo().with_child(Self::create_text(text))
4479    }
4480
4481    // Additional inline / text-level elements
4482
4483    /// Creates an empty bold element.
4484    ///
4485    /// **Accessibility**: Prefer `<strong>` for semantic emphasis. `<b>` is purely stylistic.
4486    #[inline]
4487    #[must_use] pub fn create_b() -> Self {
4488        Self::create_node(NodeType::B)
4489    }
4490
4491    /// Creates a bold element with text.
4492    ///
4493    /// **Accessibility**: Prefer `<strong>` for semantic emphasis. `<b>` is purely stylistic.
4494    ///
4495    /// **Parameters:**
4496    /// - `text`: Bold text content
4497    #[inline]
4498    pub fn create_b_with_text<S: Into<AzString>>(text: S) -> Self {
4499        Self::create_b().with_child(Self::create_text(text))
4500    }
4501
4502    /// Creates an empty italic element.
4503    ///
4504    /// **Accessibility**: Prefer `<em>` for stress emphasis. `<i>` is purely stylistic.
4505    #[inline]
4506    #[must_use] pub fn create_i() -> Self {
4507        Self::create_node(NodeType::I)
4508    }
4509
4510    /// Creates an italic element with text.
4511    ///
4512    /// **Accessibility**: Prefer `<em>` for stress emphasis. `<i>` is purely stylistic.
4513    ///
4514    /// **Parameters:**
4515    /// - `text`: Italic text content
4516    #[inline]
4517    pub fn create_i_with_text<S: Into<AzString>>(text: S) -> Self {
4518        Self::create_i().with_child(Self::create_text(text))
4519    }
4520
4521    /// Creates an empty small text element.
4522    ///
4523    /// **Accessibility**: Represents side-comments and small print like copyright/legal text.
4524    #[inline]
4525    #[must_use] pub fn create_small() -> Self {
4526        Self::create_node(NodeType::Small)
4527    }
4528
4529    /// Creates a small text element with text.
4530    ///
4531    /// **Parameters:**
4532    /// - `text`: Small text content
4533    #[inline]
4534    pub fn create_small_with_text<S: Into<AzString>>(text: S) -> Self {
4535        Self::create_small().with_child(Self::create_text(text))
4536    }
4537
4538    /// Creates an empty `<big>` element.
4539    ///
4540    /// **Note**: Deprecated in HTML5. Prefer CSS `font-size`.
4541    #[inline]
4542    #[must_use] pub fn create_big() -> Self {
4543        Self::create_node(NodeType::Big)
4544    }
4545
4546    /// Creates a `<big>` element with text.
4547    ///
4548    /// **Note**: Deprecated in HTML5. Prefer CSS `font-size`.
4549    #[inline]
4550    pub fn create_big_with_text<S: Into<AzString>>(text: S) -> Self {
4551        Self::create_big().with_child(Self::create_text(text))
4552    }
4553
4554    /// Creates an empty bi-directional isolate element.
4555    ///
4556    /// **Accessibility**: Used to isolate text whose direction is unknown,
4557    /// keeping it from affecting surrounding bidi layout.
4558    #[inline]
4559    #[must_use] pub fn create_bdi() -> Self {
4560        Self::create_node(NodeType::Bdi)
4561    }
4562
4563    /// Creates a bi-directional isolate element with text.
4564    ///
4565    /// **Accessibility**: Used to isolate text whose direction is unknown,
4566    /// keeping it from affecting surrounding bidi layout.
4567    #[inline]
4568    pub fn create_bdi_with_text<S: Into<AzString>>(text: S) -> Self {
4569        Self::create_bdi().with_child(Self::create_text(text))
4570    }
4571
4572    /// Creates an empty word break opportunity element.
4573    ///
4574    /// **Note**: `<wbr>` is a self-closing element that suggests a line-break opportunity.
4575    /// It does not take text content.
4576    #[inline]
4577    #[must_use] pub fn create_wbr() -> Self {
4578        Self::create_node(NodeType::Wbr)
4579    }
4580
4581    /// Creates an empty ruby annotation element.
4582    ///
4583    /// **Accessibility**: Used for East Asian typography to provide
4584    /// pronunciation/translation annotations. Wraps `<rt>`/`<rp>` children.
4585    #[inline]
4586    #[must_use] pub fn create_ruby() -> Self {
4587        Self::create_node(NodeType::Ruby)
4588    }
4589
4590    /// Creates an empty ruby text element.
4591    ///
4592    /// **Accessibility**: Pronunciation/translation annotation inside `<ruby>`.
4593    #[inline]
4594    #[must_use] pub fn create_rt() -> Self {
4595        Self::create_node(NodeType::Rt)
4596    }
4597
4598    /// Creates a ruby text element with text.
4599    ///
4600    /// **Parameters:**
4601    /// - `text`: Ruby annotation content
4602    #[inline]
4603    pub fn create_rt_with_text<S: Into<AzString>>(text: S) -> Self {
4604        Self::create_rt().with_child(Self::create_text(text))
4605    }
4606
4607    /// Creates an empty ruby text container element.
4608    ///
4609    /// **Accessibility**: Container for ruby text annotations.
4610    #[inline]
4611    #[must_use] pub fn create_rtc() -> Self {
4612        Self::create_node(NodeType::Rtc)
4613    }
4614
4615    /// Creates an empty ruby fallback parenthesis element.
4616    ///
4617    /// **Accessibility**: Provides parentheses around `<rt>` for browsers without ruby support.
4618    #[inline]
4619    #[must_use] pub fn create_rp() -> Self {
4620        Self::create_node(NodeType::Rp)
4621    }
4622
4623    /// Creates a ruby fallback parenthesis element with text.
4624    ///
4625    /// **Parameters:**
4626    /// - `text`: Parenthesis text (typically "(" or ")")
4627    #[inline]
4628    pub fn create_rp_with_text<S: Into<AzString>>(text: S) -> Self {
4629        Self::create_rp().with_child(Self::create_text(text))
4630    }
4631
4632    /// Creates a `<data>` element binding a machine-readable value to its content.
4633    ///
4634    /// **Parameters:**
4635    /// - `value`: Machine-readable value for the `value` attribute.
4636    #[inline]
4637    #[must_use] pub fn create_data(value: AzString) -> Self {
4638        Self::create_node(NodeType::Data).with_attribute(AttributeType::Value(value))
4639    }
4640
4641    /// Creates a `<data>` element with both a machine-readable value and visible text.
4642    ///
4643    /// **Parameters:**
4644    /// - `value`: Machine-readable value for the `value` attribute.
4645    /// - `text`: Human-readable text content.
4646    #[inline]
4647    #[must_use] pub fn create_data_with_text(value: AzString, text: AzString) -> Self {
4648        Self::create_data(value).with_child(Self::create_text(text))
4649    }
4650
4651    /// Creates an empty directory list element.
4652    ///
4653    /// **Note**: Deprecated in HTML5. Use `<ul>` instead.
4654    #[inline]
4655    #[must_use] pub fn create_dir() -> Self {
4656        Self::create_node(NodeType::Dir)
4657    }
4658
4659    /// Creates an empty SVG container element.
4660    ///
4661    /// **Accessibility**: Provide `aria-label` or `<title>` child for assistive tech.
4662    #[inline]
4663    #[must_use] pub fn create_svg() -> Self {
4664        Self::create_node(NodeType::Svg)
4665    }
4666
4667    /// Creates an anchor/hyperlink element without accessibility information.
4668    ///
4669    /// Prefer [`Dom::create_a`] so that screen readers get a meaningful label.
4670    ///
4671    /// **Parameters:**
4672    /// - `href`: Link destination URL
4673    /// - `label`: Link text (pass `None` for image-only links with alt text)
4674    #[inline]
4675    #[must_use] pub fn create_a_no_a11y(href: AzString, label: OptionString) -> Self {
4676        let mut link = Self::create_node(NodeType::A).with_attribute(AttributeType::Href(href));
4677        if let OptionString::Some(text) = label {
4678            link = link.with_child(Self::create_text(text));
4679        }
4680        link
4681    }
4682
4683    /// Creates a button element without accessibility information.
4684    ///
4685    /// Prefer [`Dom::create_button`] so that the element has a meaningful accessible
4686    /// name for screen readers.
4687    ///
4688    /// **Parameters:**
4689    /// - `text`: Button label text
4690    #[inline]
4691    #[must_use] pub fn create_button_no_a11y(text: AzString) -> Self {
4692        Self::create_node(NodeType::Button).with_child(Self::create_text(text))
4693    }
4694
4695    /// Creates a label element for form controls without accessibility information.
4696    ///
4697    /// Prefer [`Dom::create_label`] so that screen readers get a descriptive label.
4698    ///
4699    /// **Parameters:**
4700    /// - `for_id`: ID of the associated form control
4701    /// - `text`: Label text
4702    #[inline]
4703    #[must_use] pub fn create_label_no_a11y(for_id: AzString, text: AzString) -> Self {
4704        Self::create_node(NodeType::Label)
4705            .with_attribute(AttributeType::Custom(AttributeNameValue {
4706                attr_name: "for".into(),
4707                value: for_id,
4708            }))
4709            .with_child(Self::create_text(text))
4710    }
4711
4712    /// Creates an input element without accessibility information.
4713    ///
4714    /// Prefer [`Dom::create_input`] so that screen readers get a descriptive label
4715    /// beyond the HTML `aria-label` attribute.
4716    ///
4717    /// **Parameters:**
4718    /// - `input_type`: Input type (text, password, email, etc.)
4719    /// - `name`: Form field name
4720    /// - `label`: Accessibility label (required)
4721    #[inline]
4722    #[must_use] pub fn create_input_no_a11y(input_type: AzString, name: AzString, label: AzString) -> Self {
4723        Self::create_node(NodeType::Input)
4724            .with_attribute(AttributeType::InputType(input_type))
4725            .with_attribute(AttributeType::Name(name))
4726            .with_attribute(AttributeType::AriaLabel(label))
4727    }
4728
4729    /// Creates a textarea element without accessibility information.
4730    ///
4731    /// Prefer [`Dom::create_textarea`] so that screen readers get an accurate
4732    /// description of the control.
4733    ///
4734    /// **Parameters:**
4735    /// - `name`: Form field name
4736    /// - `label`: Accessibility label (required)
4737    #[inline]
4738    #[must_use] pub fn create_textarea_no_a11y(name: AzString, label: AzString) -> Self {
4739        Self::create_node(NodeType::TextArea)
4740            .with_attribute(AttributeType::Name(name))
4741            .with_attribute(AttributeType::AriaLabel(label))
4742    }
4743
4744    /// Creates a select dropdown element without accessibility information.
4745    ///
4746    /// Prefer [`Dom::create_select`] so that screen readers announce the control
4747    /// appropriately.
4748    ///
4749    /// **Parameters:**
4750    /// - `name`: Form field name
4751    /// - `label`: Accessibility label (required)
4752    #[inline]
4753    #[must_use] pub fn create_select_no_a11y(name: AzString, label: AzString) -> Self {
4754        Self::create_node(NodeType::Select)
4755            .with_attribute(AttributeType::Name(name))
4756            .with_attribute(AttributeType::AriaLabel(label))
4757    }
4758
4759    /// Creates an option element for select dropdowns.
4760    ///
4761    /// **Parameters:**
4762    /// - `value`: Option value
4763    /// - `text`: Display text
4764    #[inline]
4765    #[must_use] pub fn create_option_no_a11y(value: AzString, text: AzString) -> Self {
4766        Self::create_node(NodeType::SelectOption)
4767            .with_attribute(AttributeType::Value(value))
4768            .with_child(Self::create_text(text))
4769    }
4770
4771    /// Creates an option element for select dropdowns with accessibility information.
4772    ///
4773    /// **Parameters:**
4774    /// - `value`: Option value
4775    /// - `text`: Display text
4776    /// - `aria`: Accessibility information (description, etc.)
4777    ///
4778    /// Use [`Dom::create_option_no_a11y`] only as a deliberate escape hatch.
4779    #[inline]
4780    #[allow(clippy::needless_pass_by_value)] // owned azul C-ABI value taken by value (FFI ownership-transfer convention)
4781    #[must_use] pub fn create_option(value: AzString, text: AzString, aria: SmallAriaInfo) -> Self {
4782        Self::create_option_no_a11y(value, text).with_accessibility_info(aria.to_full_info())
4783    }
4784
4785    /// Creates an unordered list element.
4786    ///
4787    /// **Accessibility**: Screen readers announce lists and item counts, helping users
4788    /// understand content structure.
4789    #[inline]
4790    #[must_use] pub fn create_ul() -> Self {
4791        Self::create_node(NodeType::Ul)
4792    }
4793
4794    /// Creates an ordered list element.
4795    ///
4796    /// **Accessibility**: Screen readers announce lists and item counts, helping users
4797    /// understand content structure and numbering.
4798    #[inline]
4799    #[must_use] pub fn create_ol() -> Self {
4800        Self::create_node(NodeType::Ol)
4801    }
4802
4803    /// Creates a list item element.
4804    ///
4805    /// **Accessibility**: Must be a child of `ul`, `ol`, or `menu`. Screen readers announce
4806    /// list item position (e.g., "2 of 5").
4807    #[inline]
4808    #[must_use] pub fn create_li() -> Self {
4809        Self::create_node(NodeType::Li)
4810    }
4811
4812    /// Creates a table element without accessibility information.
4813    ///
4814    /// Prefer [`Dom::create_table`] so that screen readers can announce the table's
4815    /// purpose alongside its caption.
4816    #[inline]
4817    #[must_use] pub fn create_table_no_a11y() -> Self {
4818        Self::create_node(NodeType::Table)
4819    }
4820
4821    /// Creates a table caption element.
4822    ///
4823    /// **Accessibility**: Describes the purpose of the table. Screen readers announce this first.
4824    #[inline]
4825    #[must_use] pub fn create_caption() -> Self {
4826        Self::create_node(NodeType::Caption)
4827    }
4828
4829    /// Creates a table header element.
4830    ///
4831    /// **Accessibility**: Groups header rows. Screen readers can navigate table structure.
4832    #[inline]
4833    #[must_use] pub fn create_thead() -> Self {
4834        Self::create_node(NodeType::THead)
4835    }
4836
4837    /// Creates a table body element.
4838    ///
4839    /// **Accessibility**: Groups body rows. Screen readers can navigate table structure.
4840    #[inline]
4841    #[must_use] pub fn create_tbody() -> Self {
4842        Self::create_node(NodeType::TBody)
4843    }
4844
4845    /// Creates a table footer element.
4846    ///
4847    /// **Accessibility**: Groups footer rows. Screen readers can navigate table structure.
4848    #[inline]
4849    #[must_use] pub fn create_tfoot() -> Self {
4850        Self::create_node(NodeType::TFoot)
4851    }
4852
4853    /// Creates a table row element.
4854    #[inline]
4855    #[must_use] pub fn create_tr() -> Self {
4856        Self::create_node(NodeType::Tr)
4857    }
4858
4859    /// Creates a table header cell element.
4860    ///
4861    /// **Accessibility**: Use `scope` attribute ("col" or "row") to associate headers with
4862    /// data cells. Screen readers use this to announce cell context.
4863    #[inline]
4864    #[must_use] pub fn create_th() -> Self {
4865        Self::create_node(NodeType::Th)
4866    }
4867
4868    /// Creates a table data cell element.
4869    #[inline]
4870    #[must_use] pub fn create_td() -> Self {
4871        Self::create_node(NodeType::Td)
4872    }
4873
4874    /// Creates a form element without accessibility information.
4875    ///
4876    /// Prefer [`Dom::create_form`] so that screen readers can announce the form's purpose.
4877    #[inline]
4878    #[must_use] pub fn create_form_no_a11y() -> Self {
4879        Self::create_node(NodeType::Form)
4880    }
4881
4882    /// Creates a form element with accessibility information.
4883    ///
4884    /// **Accessibility**: Group related form controls with `fieldset` and `legend`.
4885    /// Provide clear labels for all inputs. Consider `aria-describedby` for instructions.
4886    ///
4887    /// Use [`Dom::create_form_no_a11y`] only as a deliberate escape hatch.
4888    #[inline]
4889    #[allow(clippy::needless_pass_by_value)] // owned azul C-ABI value taken by value (FFI ownership-transfer convention)
4890    #[must_use] pub fn create_form(aria: SmallAriaInfo) -> Self {
4891        Self::create_form_no_a11y().with_accessibility_info(aria.to_full_info())
4892    }
4893
4894    /// Creates a fieldset element for grouping form controls without accessibility info.
4895    ///
4896    /// Prefer [`Dom::create_fieldset`] so that screen readers can announce the group's purpose.
4897    #[inline]
4898    #[must_use] pub fn create_fieldset_no_a11y() -> Self {
4899        Self::create_node(NodeType::FieldSet)
4900    }
4901
4902    /// Creates a fieldset element with accessibility information.
4903    ///
4904    /// **Accessibility**: Groups related form controls. Always include a `legend` as the
4905    /// first child to describe the group. Screen readers announce the legend when entering
4906    /// the fieldset.
4907    ///
4908    /// Use [`Dom::create_fieldset_no_a11y`] only as a deliberate escape hatch.
4909    #[inline]
4910    #[allow(clippy::needless_pass_by_value)] // owned azul C-ABI value taken by value (FFI ownership-transfer convention)
4911    #[must_use] pub fn create_fieldset(aria: SmallAriaInfo) -> Self {
4912        Self::create_fieldset_no_a11y().with_accessibility_info(aria.to_full_info())
4913    }
4914
4915    /// Creates a legend element without accessibility information.
4916    ///
4917    /// Prefer [`Dom::create_legend`] so that the legend's accessible name is explicit.
4918    #[inline]
4919    #[must_use] pub fn create_legend_no_a11y() -> Self {
4920        Self::create_node(NodeType::Legend)
4921    }
4922
4923    /// Creates a legend element with accessibility information.
4924    ///
4925    /// **Accessibility**: Describes the purpose of a fieldset. Must be the first child of
4926    /// a fieldset. Screen readers announce this when entering the fieldset.
4927    ///
4928    /// Use [`Dom::create_legend_no_a11y`] only as a deliberate escape hatch.
4929    #[inline]
4930    #[allow(clippy::needless_pass_by_value)] // owned azul C-ABI value taken by value (FFI ownership-transfer convention)
4931    #[must_use] pub fn create_legend(aria: SmallAriaInfo) -> Self {
4932        Self::create_legend_no_a11y().with_accessibility_info(aria.to_full_info())
4933    }
4934
4935    /// Creates a horizontal rule element.
4936    ///
4937    /// **Accessibility**: Represents a thematic break. Screen readers may announce this as
4938    /// a separator. Consider using CSS borders for purely decorative lines.
4939    #[inline]
4940    #[must_use] pub fn create_hr() -> Self {
4941        Self::create_node(NodeType::Hr)
4942    }
4943
4944    // Additional Element Constructors
4945
4946    /// Creates an address element.
4947    ///
4948    /// **Accessibility**: Represents contact information. Screen readers identify this
4949    /// as address content.
4950    #[inline]
4951    #[must_use] pub const fn create_address() -> Self {
4952        Self {
4953            root: NodeData::create_node(NodeType::Address),
4954            children: DomVec::from_const_slice(&[]),
4955            css: azul_css::css::CssVec::from_const_slice(&[]),
4956            estimated_total_children: 0,
4957        }
4958    }
4959
4960    /// Creates a definition list element.
4961    ///
4962    /// **Accessibility**: Screen readers announce definition lists and their structure.
4963    #[inline]
4964    #[must_use] pub const fn create_dl() -> Self {
4965        Self {
4966            root: NodeData::create_node(NodeType::Dl),
4967            children: DomVec::from_const_slice(&[]),
4968            css: azul_css::css::CssVec::from_const_slice(&[]),
4969            estimated_total_children: 0,
4970        }
4971    }
4972
4973    /// Creates a definition term element.
4974    ///
4975    /// **Accessibility**: Must be a child of `dl`. Represents the term being defined.
4976    #[inline]
4977    #[must_use] pub const fn create_dt() -> Self {
4978        Self {
4979            root: NodeData::create_node(NodeType::Dt),
4980            children: DomVec::from_const_slice(&[]),
4981            css: azul_css::css::CssVec::from_const_slice(&[]),
4982            estimated_total_children: 0,
4983        }
4984    }
4985
4986    /// Creates a definition description element.
4987    ///
4988    /// **Accessibility**: Must be a child of `dl`. Provides the definition for the term.
4989    #[inline]
4990    #[must_use] pub const fn create_dd() -> Self {
4991        Self {
4992            root: NodeData::create_node(NodeType::Dd),
4993            children: DomVec::from_const_slice(&[]),
4994            css: azul_css::css::CssVec::from_const_slice(&[]),
4995            estimated_total_children: 0,
4996        }
4997    }
4998
4999    /// Creates a table column group element.
5000    #[inline]
5001    #[must_use] pub const fn create_colgroup() -> Self {
5002        Self {
5003            root: NodeData::create_node(NodeType::ColGroup),
5004            children: DomVec::from_const_slice(&[]),
5005            css: azul_css::css::CssVec::from_const_slice(&[]),
5006            estimated_total_children: 0,
5007        }
5008    }
5009
5010    /// Creates a table column element.
5011    #[inline]
5012    #[must_use] pub fn create_col(span: i32) -> Self {
5013        Self::create_node(NodeType::Col).with_attribute(AttributeType::ColSpan(span))
5014    }
5015
5016    /// Creates an optgroup element for grouping select options without accessibility info.
5017    ///
5018    /// Prefer [`Dom::create_optgroup`] so that screen readers can announce the group's purpose.
5019    ///
5020    /// **Parameters:**
5021    /// - `label`: Label for the option group
5022    #[inline]
5023    #[must_use] pub fn create_optgroup_no_a11y(label: AzString) -> Self {
5024        Self::create_node(NodeType::OptGroup).with_attribute(AttributeType::AriaLabel(label))
5025    }
5026
5027    /// Creates an optgroup element for grouping select options with accessibility information.
5028    ///
5029    /// **Parameters:**
5030    /// - `label`: Label for the option group (visible)
5031    /// - `aria`: Additional accessibility information (description, etc.)
5032    ///
5033    /// Use [`Dom::create_optgroup_no_a11y`] only as a deliberate escape hatch.
5034    #[inline]
5035    #[allow(clippy::needless_pass_by_value)] // owned azul C-ABI value taken by value (FFI ownership-transfer convention)
5036    #[must_use] pub fn create_optgroup(label: AzString, aria: SmallAriaInfo) -> Self {
5037        Self::create_optgroup_no_a11y(label).with_accessibility_info(aria.to_full_info())
5038    }
5039
5040    /// Creates a quotation element.
5041    ///
5042    /// **Accessibility**: Represents an inline quotation.
5043    #[inline]
5044    #[must_use] pub const fn create_q() -> Self {
5045        Self {
5046            root: NodeData::create_node(NodeType::Q),
5047            children: DomVec::from_const_slice(&[]),
5048            css: azul_css::css::CssVec::from_const_slice(&[]),
5049            estimated_total_children: 0,
5050        }
5051    }
5052
5053    /// Creates an empty acronym element.
5054    ///
5055    /// **Note**: Deprecated in HTML5. Consider using `create_abbr()` instead.
5056    #[inline]
5057    #[must_use] pub const fn create_acronym() -> Self {
5058        Self {
5059            root: NodeData::create_node(NodeType::Acronym),
5060            children: DomVec::from_const_slice(&[]),
5061            css: azul_css::css::CssVec::from_const_slice(&[]),
5062            estimated_total_children: 0,
5063        }
5064    }
5065
5066    /// Creates an acronym element with text.
5067    ///
5068    /// **Note**: Deprecated in HTML5. Consider using `create_abbr_with_title()` instead.
5069    #[inline]
5070    pub fn create_acronym_with_text<S: Into<AzString>>(text: S) -> Self {
5071        Self::create_acronym().with_child(Self::create_text(text))
5072    }
5073
5074    /// Creates a menu element without accessibility information.
5075    ///
5076    /// Prefer [`Dom::create_menu`] so that the menu's purpose is announced.
5077    #[inline]
5078    #[must_use] pub const fn create_menu_no_a11y() -> Self {
5079        Self {
5080            root: NodeData::create_node(NodeType::Menu),
5081            children: DomVec::from_const_slice(&[]),
5082            css: azul_css::css::CssVec::from_const_slice(&[]),
5083            estimated_total_children: 0,
5084        }
5085    }
5086
5087    /// Creates a menu element with accessibility information.
5088    ///
5089    /// **Accessibility**: Represents a list of commands. Similar to `<ul>` but semantic for
5090    /// toolbars/menus.
5091    ///
5092    /// Use [`Dom::create_menu_no_a11y`] only as a deliberate escape hatch.
5093    #[inline]
5094    #[allow(clippy::needless_pass_by_value)] // owned azul C-ABI value taken by value (FFI ownership-transfer convention)
5095    #[must_use] pub fn create_menu(aria: SmallAriaInfo) -> Self {
5096        Self::create_menu_no_a11y().with_accessibility_info(aria.to_full_info())
5097    }
5098
5099    /// Creates an empty menu item element without accessibility information.
5100    ///
5101    /// Prefer [`Dom::create_menuitem`] so that the menu item's purpose is announced.
5102    #[inline]
5103    #[must_use] pub const fn create_menuitem_no_a11y() -> Self {
5104        Self {
5105            root: NodeData::create_node(NodeType::MenuItem),
5106            children: DomVec::from_const_slice(&[]),
5107            css: azul_css::css::CssVec::from_const_slice(&[]),
5108            estimated_total_children: 0,
5109        }
5110    }
5111
5112    /// Creates an empty menu item element with accessibility information.
5113    ///
5114    /// **Accessibility**: Represents a command in a menu. Use with appropriate role/aria
5115    /// attributes.
5116    ///
5117    /// Use [`Dom::create_menuitem_no_a11y`] only as a deliberate escape hatch.
5118    #[inline]
5119    #[allow(clippy::needless_pass_by_value)] // owned azul C-ABI value taken by value (FFI ownership-transfer convention)
5120    #[must_use] pub fn create_menuitem(aria: SmallAriaInfo) -> Self {
5121        Self::create_menuitem_no_a11y().with_accessibility_info(aria.to_full_info())
5122    }
5123
5124    /// Creates a menu item element with text but without accessibility information.
5125    ///
5126    /// Prefer [`Dom::create_menuitem_with_text`] so that screen readers get a
5127    /// distinct accessible name in addition to the visible text.
5128    #[inline]
5129    pub fn create_menuitem_with_text_no_a11y<S: Into<AzString>>(text: S) -> Self {
5130        Self::create_menuitem_no_a11y().with_child(Self::create_text(text))
5131    }
5132
5133    /// Creates a menu item element with text and accessibility information.
5134    ///
5135    /// **Accessibility**: Represents a command in a menu. Use with appropriate role/aria
5136    /// attributes.
5137    ///
5138    /// Use [`Dom::create_menuitem_with_text_no_a11y`] only as a deliberate escape hatch.
5139    #[inline]
5140    #[allow(clippy::needless_pass_by_value)] // owned azul C-ABI value taken by value (FFI ownership-transfer convention)
5141    pub fn create_menuitem_with_text<S: Into<AzString>>(text: S, aria: SmallAriaInfo) -> Self {
5142        Self::create_menuitem_with_text_no_a11y(text).with_accessibility_info(aria.to_full_info())
5143    }
5144
5145    /// Creates an output element without accessibility information.
5146    ///
5147    /// Prefer [`Dom::create_output`] so that screen readers can announce the
5148    /// computed value's purpose.
5149    #[inline]
5150    #[must_use] pub const fn create_output_no_a11y() -> Self {
5151        Self {
5152            root: NodeData::create_node(NodeType::Output),
5153            children: DomVec::from_const_slice(&[]),
5154            css: azul_css::css::CssVec::from_const_slice(&[]),
5155            estimated_total_children: 0,
5156        }
5157    }
5158
5159    /// Creates an output element with accessibility information.
5160    ///
5161    /// **Accessibility**: Represents the result of a calculation or user action.
5162    /// Use `for` attribute to associate with input elements. Screen readers announce updates.
5163    ///
5164    /// Use [`Dom::create_output_no_a11y`] only as a deliberate escape hatch.
5165    #[inline]
5166    #[allow(clippy::needless_pass_by_value)] // owned azul C-ABI value taken by value (FFI ownership-transfer convention)
5167    #[must_use] pub fn create_output(aria: SmallAriaInfo) -> Self {
5168        Self::create_output_no_a11y().with_accessibility_info(aria.to_full_info())
5169    }
5170
5171    /// Creates a progress indicator element without accessibility information.
5172    ///
5173    /// Prefer [`Dom::create_progress`] so that the task being measured is announced.
5174    ///
5175    /// **Parameters:**
5176    /// - `value`: Current progress value
5177    /// - `max`: Maximum value
5178    #[inline]
5179    #[must_use] pub fn create_progress_no_a11y(value: f32, max: f32) -> Self {
5180        Self::create_node(NodeType::Progress)
5181            .with_attribute(AttributeType::Custom(AttributeNameValue {
5182                attr_name: "value".into(),
5183                value: value.to_string().into(),
5184            }))
5185            .with_attribute(AttributeType::Custom(AttributeNameValue {
5186                attr_name: "max".into(),
5187                value: max.to_string().into(),
5188            }))
5189    }
5190
5191    /// Creates a progress indicator element with accessibility information.
5192    ///
5193    /// **Accessibility**: Represents task progress. Screen readers announce progress
5194    /// percentage. The `aria` value carries the label, current value, max, and an
5195    /// indeterminate flag for spinners with no known endpoint.
5196    ///
5197    /// Use [`Dom::create_progress_no_a11y`] only as a deliberate escape hatch.
5198    #[inline]
5199    #[allow(clippy::needless_pass_by_value)] // owned azul C-ABI value taken by value (FFI ownership-transfer convention)
5200    #[must_use] pub fn create_progress(aria: ProgressAriaInfo) -> Self {
5201        let mut node = Self::create_node(NodeType::Progress);
5202        if !aria.indeterminate {
5203            if let azul_css::OptionF32::Some(v) = aria.current_value {
5204                node = node.with_attribute(AttributeType::Custom(AttributeNameValue {
5205                    attr_name: "value".into(),
5206                    value: v.to_string().into(),
5207                }));
5208            }
5209        }
5210        if let azul_css::OptionF32::Some(m) = aria.max {
5211            node = node.with_attribute(AttributeType::Custom(AttributeNameValue {
5212                attr_name: "max".into(),
5213                value: m.to_string().into(),
5214            }));
5215        }
5216        node.with_accessibility_info(aria.to_full_info())
5217    }
5218
5219    /// Creates a meter gauge element without accessibility information.
5220    ///
5221    /// Prefer [`Dom::create_meter`] so that the measurement's purpose is announced.
5222    ///
5223    /// **Parameters:**
5224    /// - `value`: Current meter value
5225    /// - `min`: Minimum value
5226    /// - `max`: Maximum value
5227    #[inline]
5228    #[must_use] pub fn create_meter_no_a11y(value: f32, min: f32, max: f32) -> Self {
5229        Self::create_node(NodeType::Meter)
5230            .with_attribute(AttributeType::Custom(AttributeNameValue {
5231                attr_name: "value".into(),
5232                value: value.to_string().into(),
5233            }))
5234            .with_attribute(AttributeType::Custom(AttributeNameValue {
5235                attr_name: "min".into(),
5236                value: min.to_string().into(),
5237            }))
5238            .with_attribute(AttributeType::Custom(AttributeNameValue {
5239                attr_name: "max".into(),
5240                value: max.to_string().into(),
5241            }))
5242    }
5243
5244    /// Creates a meter gauge element with accessibility information.
5245    ///
5246    /// **Accessibility**: Represents a scalar measurement within a known range.
5247    /// Screen readers announce the measurement. The `aria` value carries the
5248    /// label plus value/min/max/low/high/optimum metadata.
5249    ///
5250    /// Use [`Dom::create_meter_no_a11y`] only as a deliberate escape hatch.
5251    #[inline]
5252    #[allow(clippy::needless_pass_by_value)] // owned azul C-ABI value taken by value (FFI ownership-transfer convention)
5253    #[must_use] pub fn create_meter(aria: MeterAriaInfo) -> Self {
5254        let mut node = Self::create_meter_no_a11y(aria.current_value, aria.min, aria.max);
5255        if let azul_css::OptionF32::Some(v) = aria.low {
5256            node = node.with_attribute(AttributeType::Custom(AttributeNameValue {
5257                attr_name: "low".into(),
5258                value: v.to_string().into(),
5259            }));
5260        }
5261        if let azul_css::OptionF32::Some(v) = aria.high {
5262            node = node.with_attribute(AttributeType::Custom(AttributeNameValue {
5263                attr_name: "high".into(),
5264                value: v.to_string().into(),
5265            }));
5266        }
5267        if let azul_css::OptionF32::Some(v) = aria.optimum {
5268            node = node.with_attribute(AttributeType::Custom(AttributeNameValue {
5269                attr_name: "optimum".into(),
5270                value: v.to_string().into(),
5271            }));
5272        }
5273        node.with_accessibility_info(aria.to_full_info())
5274    }
5275
5276    /// Creates a datalist element without accessibility information.
5277    ///
5278    /// Prefer [`Dom::create_datalist`] so that the suggestion list's purpose is announced.
5279    #[inline]
5280    #[must_use] pub const fn create_datalist_no_a11y() -> Self {
5281        Self {
5282            root: NodeData::create_node(NodeType::DataList),
5283            children: DomVec::from_const_slice(&[]),
5284            css: azul_css::css::CssVec::from_const_slice(&[]),
5285            estimated_total_children: 0,
5286        }
5287    }
5288
5289    /// Creates a datalist element with accessibility information.
5290    ///
5291    /// **Accessibility**: Provides autocomplete options for inputs.
5292    /// Associate with input using `list` attribute. Screen readers announce available options.
5293    ///
5294    /// Use [`Dom::create_datalist_no_a11y`] only as a deliberate escape hatch.
5295    #[inline]
5296    #[allow(clippy::needless_pass_by_value)] // owned azul C-ABI value taken by value (FFI ownership-transfer convention)
5297    #[must_use] pub fn create_datalist(aria: SmallAriaInfo) -> Self {
5298        Self::create_datalist_no_a11y().with_accessibility_info(aria.to_full_info())
5299    }
5300
5301    // Embedded Content Elements
5302
5303    /// Creates a canvas element for graphics without accessibility information.
5304    ///
5305    /// Prefer [`Dom::create_canvas`] so that the canvas's purpose is announced; canvas
5306    /// content is otherwise opaque to assistive technologies.
5307    #[inline]
5308    #[must_use] pub const fn create_canvas_no_a11y() -> Self {
5309        Self {
5310            root: NodeData::create_node(NodeType::Canvas),
5311            children: DomVec::from_const_slice(&[]),
5312            css: azul_css::css::CssVec::from_const_slice(&[]),
5313            estimated_total_children: 0,
5314        }
5315    }
5316
5317    /// Creates a canvas element for graphics with accessibility information.
5318    ///
5319    /// **Accessibility**: Canvas content is not accessible by default.
5320    /// Always provide fallback content as children and/or detailed aria-label.
5321    /// Consider using SVG for accessible graphics when possible.
5322    ///
5323    /// Use [`Dom::create_canvas_no_a11y`] only as a deliberate escape hatch.
5324    #[inline]
5325    #[allow(clippy::needless_pass_by_value)] // owned azul C-ABI value taken by value (FFI ownership-transfer convention)
5326    #[must_use] pub fn create_canvas(aria: SmallAriaInfo) -> Self {
5327        Self::create_canvas_no_a11y().with_accessibility_info(aria.to_full_info())
5328    }
5329
5330    /// Creates an object element for embedded content.
5331    ///
5332    /// **Accessibility**: Provide fallback content as children. Use aria-label to describe content.
5333    #[inline]
5334    #[must_use] pub const fn create_object() -> Self {
5335        Self {
5336            root: NodeData::create_node(NodeType::Object),
5337            children: DomVec::from_const_slice(&[]),
5338            css: azul_css::css::CssVec::from_const_slice(&[]),
5339            estimated_total_children: 0,
5340        }
5341    }
5342
5343    /// Creates a param element for object parameters.
5344    ///
5345    /// **Parameters:**
5346    /// - `name`: Parameter name
5347    /// - `value`: Parameter value
5348    #[inline]
5349    #[must_use] pub fn create_param(name: AzString, value: AzString) -> Self {
5350        Self::create_node(NodeType::Param)
5351            .with_attribute(AttributeType::Name(name))
5352            .with_attribute(AttributeType::Value(value))
5353    }
5354
5355    /// Creates an embed element.
5356    ///
5357    /// **Accessibility**: Provide alternative content or link. Use aria-label to describe embedded
5358    /// content.
5359    #[inline]
5360    #[must_use] pub const fn create_embed() -> Self {
5361        Self {
5362            root: NodeData::create_node(NodeType::Embed),
5363            children: DomVec::from_const_slice(&[]),
5364            css: azul_css::css::CssVec::from_const_slice(&[]),
5365            estimated_total_children: 0,
5366        }
5367    }
5368
5369    /// Creates an audio element without accessibility information.
5370    ///
5371    /// Prefer [`Dom::create_audio`] so that screen readers announce the audio's purpose.
5372    #[inline]
5373    #[must_use] pub const fn create_audio_no_a11y() -> Self {
5374        Self {
5375            root: NodeData::create_node(NodeType::Audio),
5376            children: DomVec::from_const_slice(&[]),
5377            css: azul_css::css::CssVec::from_const_slice(&[]),
5378            estimated_total_children: 0,
5379        }
5380    }
5381
5382    /// Creates an audio element with accessibility information.
5383    ///
5384    /// **Accessibility**: Always provide controls. Use `<track>` for captions/subtitles.
5385    /// Provide fallback text for unsupported browsers.
5386    ///
5387    /// Use [`Dom::create_audio_no_a11y`] only as a deliberate escape hatch.
5388    #[inline]
5389    #[allow(clippy::needless_pass_by_value)] // owned azul C-ABI value taken by value (FFI ownership-transfer convention)
5390    #[must_use] pub fn create_audio(aria: SmallAriaInfo) -> Self {
5391        Self::create_audio_no_a11y().with_accessibility_info(aria.to_full_info())
5392    }
5393
5394    /// Creates a video element without accessibility information.
5395    ///
5396    /// Prefer [`Dom::create_video`] so that screen readers announce the video's purpose.
5397    #[inline]
5398    #[must_use] pub const fn create_video_no_a11y() -> Self {
5399        Self {
5400            root: NodeData::create_node(NodeType::Video),
5401            children: DomVec::from_const_slice(&[]),
5402            css: azul_css::css::CssVec::from_const_slice(&[]),
5403            estimated_total_children: 0,
5404        }
5405    }
5406
5407    /// Creates a video element with accessibility information.
5408    ///
5409    /// **Accessibility**: Always provide controls. Use `<track>` for
5410    /// captions/subtitles/descriptions. Provide fallback text. Consider providing transcript.
5411    ///
5412    /// Use [`Dom::create_video_no_a11y`] only as a deliberate escape hatch.
5413    #[inline]
5414    #[allow(clippy::needless_pass_by_value)] // owned azul C-ABI value taken by value (FFI ownership-transfer convention)
5415    #[must_use] pub fn create_video(aria: SmallAriaInfo) -> Self {
5416        Self::create_video_no_a11y().with_accessibility_info(aria.to_full_info())
5417    }
5418
5419    /// Creates a source element for media.
5420    ///
5421    /// **Parameters:**
5422    /// - `src`: Media source URL
5423    /// - `media_type`: MIME type (e.g., "video/mp4", "audio/ogg")
5424    #[inline]
5425    #[must_use] pub fn create_source(src: AzString, media_type: AzString) -> Self {
5426        Self::create_node(NodeType::Source)
5427            .with_attribute(AttributeType::Src(src))
5428            .with_attribute(AttributeType::Custom(AttributeNameValue {
5429                attr_name: "type".into(),
5430                value: media_type,
5431            }))
5432    }
5433
5434    /// Creates a track element for media captions/subtitles.
5435    ///
5436    /// **Accessibility**: Essential for deaf/hard-of-hearing users and non-native speakers.
5437    /// Use `kind` (subtitles/captions/descriptions), `srclang`, and `label` attributes.
5438    ///
5439    /// **Parameters:**
5440    /// - `src`: Track file URL (`WebVTT` format)
5441    /// - `kind`: Track kind ("subtitles", "captions", "descriptions", "chapters", "metadata")
5442    #[inline]
5443    #[must_use] pub fn create_track(src: AzString, kind: AzString) -> Self {
5444        Self::create_node(NodeType::Track)
5445            .with_attribute(AttributeType::Src(src))
5446            .with_attribute(AttributeType::Custom(AttributeNameValue {
5447                attr_name: "kind".into(),
5448                value: kind,
5449            }))
5450    }
5451
5452    /// Creates a map element for image maps.
5453    ///
5454    /// **Accessibility**: Provide text alternatives. Ensure all areas have alt text.
5455    #[inline]
5456    #[must_use] pub const fn create_map() -> Self {
5457        Self {
5458            root: NodeData::create_node(NodeType::Map),
5459            children: DomVec::from_const_slice(&[]),
5460            css: azul_css::css::CssVec::from_const_slice(&[]),
5461            estimated_total_children: 0,
5462        }
5463    }
5464
5465    /// Creates an area element for image map regions without accessibility information.
5466    ///
5467    /// Prefer [`Dom::create_area`] so that screen readers can announce the region's purpose.
5468    #[inline]
5469    #[must_use] pub const fn create_area_no_a11y() -> Self {
5470        Self {
5471            root: NodeData::create_node(NodeType::Area),
5472            children: DomVec::from_const_slice(&[]),
5473            css: azul_css::css::CssVec::from_const_slice(&[]),
5474            estimated_total_children: 0,
5475        }
5476    }
5477
5478    /// Creates an area element for image map regions with accessibility information.
5479    ///
5480    /// **Accessibility**: Always provide `alt` text describing the region/link purpose.
5481    /// Keyboard users should be able to navigate areas.
5482    ///
5483    /// Use [`Dom::create_area_no_a11y`] only as a deliberate escape hatch.
5484    #[inline]
5485    #[allow(clippy::needless_pass_by_value)] // owned azul C-ABI value taken by value (FFI ownership-transfer convention)
5486    #[must_use] pub fn create_area(aria: SmallAriaInfo) -> Self {
5487        Self::create_area_no_a11y().with_accessibility_info(aria.to_full_info())
5488    }
5489
5490    // Metadata Elements
5491
5492    /// Creates an empty title element for document title.
5493    ///
5494    /// **Accessibility**: Required for all pages. Screen readers announce this first.
5495    #[inline]
5496    #[must_use] pub fn create_title() -> Self {
5497        Self::create_node(NodeType::Title)
5498    }
5499
5500    /// Creates a title element for document title with text.
5501    ///
5502    /// **Accessibility**: Required for all pages. Screen readers announce this first.
5503    /// Should be unique and descriptive. Keep under 60 characters.
5504    #[inline]
5505    pub fn create_title_with_text<S: Into<AzString>>(text: S) -> Self {
5506        Self::create_title().with_child(Self::create_text(text))
5507    }
5508
5509    /// Creates a meta element.
5510    ///
5511    /// **Accessibility**: Use for charset, viewport, description. Crucial for proper text display.
5512    #[inline]
5513    #[must_use] pub const fn create_meta() -> Self {
5514        Self {
5515            root: NodeData::create_node(NodeType::Meta),
5516            children: DomVec::from_const_slice(&[]),
5517            css: azul_css::css::CssVec::from_const_slice(&[]),
5518            estimated_total_children: 0,
5519        }
5520    }
5521
5522    /// Creates a link element for external resources.
5523    ///
5524    /// **Accessibility**: Use for stylesheets, icons, alternate versions.
5525    /// Provide meaningful `title` attribute for alternate stylesheets.
5526    #[inline]
5527    #[must_use] pub const fn create_link() -> Self {
5528        Self {
5529            root: NodeData::create_node(NodeType::Link),
5530            children: DomVec::from_const_slice(&[]),
5531            css: azul_css::css::CssVec::from_const_slice(&[]),
5532            estimated_total_children: 0,
5533        }
5534    }
5535
5536    /// Creates a script element.
5537    ///
5538    /// **Accessibility**: Ensure scripted content is accessible.
5539    /// Provide noscript fallbacks for critical functionality.
5540    #[inline]
5541    #[must_use] pub const fn create_script() -> Self {
5542        Self {
5543            root: NodeData::create_node(NodeType::Script),
5544            children: DomVec::from_const_slice(&[]),
5545            css: azul_css::css::CssVec::from_const_slice(&[]),
5546            estimated_total_children: 0,
5547        }
5548    }
5549
5550    /// Creates an empty style element for embedded CSS.
5551    ///
5552    /// **Note**: In Azul, use `.with_css()` instead for styling.
5553    /// This creates a `<style>` HTML element for embedded stylesheets.
5554    #[inline]
5555    #[must_use] pub const fn create_style() -> Self {
5556        Self {
5557            root: NodeData::create_node(NodeType::Style),
5558            children: DomVec::from_const_slice(&[]),
5559            css: azul_css::css::CssVec::from_const_slice(&[]),
5560            estimated_total_children: 0,
5561        }
5562    }
5563
5564    /// Creates a style element for embedded CSS with the given stylesheet text.
5565    ///
5566    /// **Note**: In Azul, use `.with_css()` instead for styling.
5567    /// This creates a `<style>` HTML element for embedded stylesheets.
5568    #[inline]
5569    pub fn create_style_with_text<S: Into<AzString>>(text: S) -> Self {
5570        Self::create_style().with_child(Self::create_text(text))
5571    }
5572
5573    /// Creates a base element for document base URL.
5574    ///
5575    /// **Parameters:**
5576    /// - `href`: Base URL for relative URLs in the document
5577    #[inline]
5578    #[must_use] pub fn create_base(href: AzString) -> Self {
5579        Self::create_node(NodeType::Base).with_attribute(AttributeType::Href(href))
5580    }
5581
5582    // Advanced Constructors with Parameters
5583
5584    /// Creates a table header cell with scope.
5585    ///
5586    /// **Parameters:**
5587    /// - `scope`: "col", "row", "colgroup", or "rowgroup"
5588    /// - `text`: Header text
5589    ///
5590    /// **Accessibility**: The scope attribute is crucial for associating headers with data cells.
5591    #[inline]
5592    #[must_use] pub fn create_th_with_scope(scope: AzString, text: AzString) -> Self {
5593        Self::create_node(NodeType::Th)
5594            .with_attribute(AttributeType::Scope(scope))
5595            .with_child(Self::create_text(text))
5596    }
5597
5598    /// Creates a table data cell with text.
5599    ///
5600    /// **Parameters:**
5601    /// - `text`: Cell content
5602    #[inline]
5603    pub fn create_td_with_text<S: Into<AzString>>(text: S) -> Self {
5604        Self::create_td().with_child(Self::create_text(text))
5605    }
5606
5607    /// Creates a table header cell with text.
5608    ///
5609    /// **Parameters:**
5610    /// - `text`: Header text
5611    #[inline]
5612    pub fn create_th_with_text<S: Into<AzString>>(text: S) -> Self {
5613        Self::create_th().with_child(Self::create_text(text))
5614    }
5615
5616    /// Creates a list item with text.
5617    ///
5618    /// **Parameters:**
5619    /// - `text`: List item content
5620    #[inline]
5621    pub fn create_li_with_text<S: Into<AzString>>(text: S) -> Self {
5622        Self::create_li().with_child(Self::create_text(text))
5623    }
5624
5625    /// Creates a paragraph with text.
5626    ///
5627    /// **Parameters:**
5628    /// - `text`: Paragraph content
5629    #[inline]
5630    pub fn create_p_with_text<S: Into<AzString>>(text: S) -> Self {
5631        Self::create_p().with_child(Self::create_text(text))
5632    }
5633
5634    // Accessibility-Aware Constructors
5635    // These constructors require explicit accessibility information.
5636    // Use the `*_no_a11y` variants only as a deliberate escape hatch.
5637
5638    /// Creates a button with text content and accessibility information.
5639    ///
5640    /// Use [`Dom::create_button_no_a11y`] to skip the accessibility information.
5641    ///
5642    /// **Parameters:**
5643    /// - `text`: The visible button text
5644    /// - `aria`: Accessibility information (role, description, etc.)
5645    #[inline]
5646    #[allow(clippy::needless_pass_by_value)] // owned azul C-ABI value taken by value (FFI ownership-transfer convention)
5647    pub fn create_button<S: Into<AzString>>(text: S, aria: SmallAriaInfo) -> Self {
5648        let mut btn = Self::create_button_no_a11y(text.into());
5649        btn.root.set_accessibility_info(aria.to_full_info());
5650        btn
5651    }
5652
5653    /// Creates a link (anchor) with href, text, and accessibility information.
5654    ///
5655    /// Use [`Dom::create_a_no_a11y`] to skip the accessibility information (e.g. for
5656    /// image-only links whose accessible name comes from an `<img alt>`).
5657    ///
5658    /// **Parameters:**
5659    /// - `href`: The link destination
5660    /// - `text`: The visible link text
5661    /// - `aria`: Accessibility information (expanded description, etc.)
5662    #[inline]
5663    #[allow(clippy::needless_pass_by_value)] // owned azul C-ABI value taken by value (FFI ownership-transfer convention)
5664    pub fn create_a<S1: Into<AzString>, S2: Into<AzString>>(
5665        href: S1,
5666        text: S2,
5667        aria: SmallAriaInfo,
5668    ) -> Self {
5669        let mut link = Self::create_a_no_a11y(href.into(), OptionString::Some(text.into()));
5670        link.root.set_accessibility_info(aria.to_full_info());
5671        link
5672    }
5673
5674    /// Creates an input element with type, name, and accessibility information.
5675    ///
5676    /// Use [`Dom::create_input_no_a11y`] to skip the accessibility information.
5677    ///
5678    /// **Parameters:**
5679    /// - `input_type`: The input type (text, password, email, etc.)
5680    /// - `name`: The form field name
5681    /// - `label`: Base accessibility label
5682    /// - `aria`: Additional accessibility information (description, etc.)
5683    #[inline]
5684    #[allow(clippy::needless_pass_by_value)] // owned azul C-ABI value taken by value (FFI ownership-transfer convention)
5685    pub fn create_input<S1: Into<AzString>, S2: Into<AzString>, S3: Into<AzString>>(
5686        input_type: S1,
5687        name: S2,
5688        label: S3,
5689        aria: SmallAriaInfo,
5690    ) -> Self {
5691        let mut input = Self::create_input_no_a11y(input_type.into(), name.into(), label.into());
5692        input.root.set_accessibility_info(aria.to_full_info());
5693        input
5694    }
5695
5696    /// Creates a textarea with name and accessibility information.
5697    ///
5698    /// Use [`Dom::create_textarea_no_a11y`] to skip the accessibility information.
5699    ///
5700    /// **Parameters:**
5701    /// - `name`: The form field name
5702    /// - `label`: Base accessibility label
5703    /// - `aria`: Additional accessibility information (description, etc.)
5704    #[inline]
5705    #[allow(clippy::needless_pass_by_value)] // owned azul C-ABI value taken by value (FFI ownership-transfer convention)
5706    pub fn create_textarea<S1: Into<AzString>, S2: Into<AzString>>(
5707        name: S1,
5708        label: S2,
5709        aria: SmallAriaInfo,
5710    ) -> Self {
5711        let mut textarea = Self::create_textarea_no_a11y(name.into(), label.into());
5712        textarea.root.set_accessibility_info(aria.to_full_info());
5713        textarea
5714    }
5715
5716    /// Creates a select dropdown with name and accessibility information.
5717    ///
5718    /// Use [`Dom::create_select_no_a11y`] to skip the accessibility information.
5719    ///
5720    /// **Parameters:**
5721    /// - `name`: The form field name
5722    /// - `label`: Base accessibility label
5723    /// - `aria`: Additional accessibility information (description, etc.)
5724    #[inline]
5725    #[allow(clippy::needless_pass_by_value)] // owned azul C-ABI value taken by value (FFI ownership-transfer convention)
5726    pub fn create_select<S1: Into<AzString>, S2: Into<AzString>>(
5727        name: S1,
5728        label: S2,
5729        aria: SmallAriaInfo,
5730    ) -> Self {
5731        let mut select = Self::create_select_no_a11y(name.into(), label.into());
5732        select.root.set_accessibility_info(aria.to_full_info());
5733        select
5734    }
5735
5736    /// Creates a table with caption and accessibility information.
5737    ///
5738    /// Use [`Dom::create_table_no_a11y`] to skip the caption and accessibility
5739    /// information.
5740    ///
5741    /// **Parameters:**
5742    /// - `caption`: Table caption (visible title)
5743    /// - `aria`: Accessibility information describing table purpose
5744    #[inline]
5745    #[allow(clippy::needless_pass_by_value)] // owned azul C-ABI value taken by value (FFI ownership-transfer convention)
5746    pub fn create_table<S: Into<AzString>>(caption: S, aria: SmallAriaInfo) -> Self {
5747        let mut table = Self::create_table_no_a11y()
5748            .with_child(Self::create_caption().with_child(Self::create_text(caption)));
5749        table.root.set_accessibility_info(aria.to_full_info());
5750        table
5751    }
5752
5753    /// Creates a label for a form control with additional accessibility information.
5754    ///
5755    /// Use [`Dom::create_label_no_a11y`] to skip the accessibility information.
5756    ///
5757    /// **Parameters:**
5758    /// - `for_id`: The ID of the associated form control
5759    /// - `text`: The visible label text
5760    /// - `aria`: Additional accessibility information (description, etc.)
5761    #[inline]
5762    #[allow(clippy::needless_pass_by_value)] // owned azul C-ABI value taken by value (FFI ownership-transfer convention)
5763    pub fn create_label<S1: Into<AzString>, S2: Into<AzString>>(
5764        for_id: S1,
5765        text: S2,
5766        aria: SmallAriaInfo,
5767    ) -> Self {
5768        let mut label = Self::create_label_no_a11y(for_id.into(), text.into());
5769        label.root.set_accessibility_info(aria.to_full_info());
5770        label
5771    }
5772
5773    /// Parse XML/XHTML string into a DOM
5774    ///
5775    /// This is a simple wrapper that parses XML and converts it to a DOM.
5776    /// For now, it just creates a text node with the content since full XML parsing
5777    /// requires the xml feature and more complex parsing logic.
5778    #[cfg(feature = "xml")]
5779    pub fn from_xml<S: AsRef<str>>(xml_str: S) -> Self {
5780        // TODO: Implement full XML parsing
5781        // For now, just create a text node showing that XML was loaded
5782        Self::create_text(format!(
5783            "XML content loaded ({} bytes)",
5784            xml_str.as_ref().len()
5785        ))
5786    }
5787
5788    /// Parse XML/XHTML string into a DOM (fallback without xml feature)
5789    #[cfg(not(feature = "xml"))]
5790    pub fn from_xml<S: AsRef<str>>(xml_str: S) -> Self {
5791        Self::create_text(format!(
5792            "XML parsing requires 'xml' feature ({} bytes)",
5793            xml_str.as_ref().len()
5794        ))
5795    }
5796
5797    // Swaps `self` with a default DOM, necessary for builder methods
5798    #[inline]
5799    #[must_use]
5800    pub const fn swap_with_default(&mut self) -> Self {
5801        let mut s = Self {
5802            root: NodeData::create_div(),
5803            children: DomVec::from_const_slice(&[]),
5804            css: azul_css::css::CssVec::from_const_slice(&[]),
5805            estimated_total_children: 0,
5806        };
5807        mem::swap(&mut s, self);
5808        s
5809    }
5810
5811    /// AUDIT: recompute the authoritative descendant count (1 per descendant)
5812    /// from `children`, without mutating anything. Used by the debug-only
5813    /// consistency assertions in the builder methods so a stale
5814    /// `estimated_total_children` (from direct `children` mutation) is caught in
5815    /// tests before it can under-allocate the arena in
5816    /// `convert_dom_into_compact_dom`.
5817    #[must_use]
5818    pub fn recompute_estimated_total_children(&self) -> usize {
5819        self.children
5820            .iter()
5821            .map(|c| c.recompute_estimated_total_children() + 1)
5822            .sum()
5823    }
5824
5825    #[inline]
5826    pub fn add_child(&mut self, child: Self) {
5827        // AUDIT: cheap ONE-LEVEL consistency check (O(child.children), not the
5828        // full O(subtree) recompute — `add_child` is a per-child hot path, so a
5829        // recursive assert would make debug DOM construction O(n^2)). Assuming
5830        // grandchildren are already consistent (they are when the tree is built
5831        // bottom-up), this catches a direct mutation of `child.children` that
5832        // skipped `fixup_children_estimated()`.
5833        debug_assert_eq!(
5834            child.estimated_total_children,
5835            child
5836                .children
5837                .iter()
5838                .map(|c| c.estimated_total_children + 1)
5839                .sum::<usize>(),
5840            "Dom.estimated_total_children desynced for added child; call \
5841             fixup_children_estimated() after mutating `children` directly",
5842        );
5843        let estimated = child.estimated_total_children;
5844        let mut v: DomVec = Vec::new().into();
5845        mem::swap(&mut v, &mut self.children);
5846        let mut v = v.into_library_owned_vec();
5847        v.push(child);
5848        self.children = v.into();
5849        self.estimated_total_children += estimated + 1;
5850    }
5851
5852    #[inline]
5853    pub fn set_children(&mut self, children: DomVec) {
5854        // AUDIT: one-level check per child (see `add_child`) — verifies each
5855        // child's own cached estimate is internally consistent before we trust
5856        // it, without an O(subtree) recompute.
5857        debug_assert!(
5858            children.iter().all(|c| c.estimated_total_children
5859                == c
5860                    .children
5861                    .iter()
5862                    .map(|g| g.estimated_total_children + 1)
5863                    .sum::<usize>()),
5864            "Dom.estimated_total_children desynced in set_children; a child's own \
5865             estimate was stale — call fixup_children_estimated() first",
5866        );
5867        let children_estimated = children
5868            .iter()
5869            .map(|s| s.estimated_total_children + 1)
5870            .sum();
5871        self.children = children;
5872        self.estimated_total_children = children_estimated;
5873    }
5874
5875    #[must_use]
5876    pub fn copy_except_for_root(&mut self) -> Self {
5877        Self {
5878            root: self.root.copy_special(),
5879            children: self.children.clone(),
5880            css: self.css.clone(),
5881            estimated_total_children: self.estimated_total_children,
5882        }
5883    }
5884    #[must_use] pub const fn node_count(&self) -> usize {
5885        self.estimated_total_children + 1
5886    }
5887
5888    /// Push a parsed `Css` onto this Dom subtree's `.css` list (the
5889    /// `@scope`-like mechanism that `with_css(&str)` also feeds — a string
5890    /// parses to a `Css` and lands here). The cascade selector-matches every
5891    /// entry against the subtree; later pushes win at equal specificity.
5892    /// This is the low-level Css-struct entry point; prefer `with_css(&str)`.
5893    pub fn add_component_css(&mut self, css: azul_css::css::Css) {
5894        let mut v = Vec::new().into();
5895        mem::swap(&mut v, &mut self.css);
5896        let mut v: Vec<azul_css::css::Css> = v.into_library_owned_vec();
5897        v.push(css);
5898        self.css = v.into();
5899    }
5900
5901    /// Replace the subtree's entire component-level CSS list with the
5902    /// provided one. Use `add_component_css` / `with_component_css` for
5903    /// stacking; this is the wholesale-replace form.
5904    pub fn set_component_css(&mut self, css: azul_css::css::CssVec) {
5905        self.css = css;
5906    }
5907    #[inline]
5908    #[must_use] pub fn with_children(mut self, children: DomVec) -> Self {
5909        self.set_children(children);
5910        self
5911    }
5912    #[inline]
5913    #[must_use] pub fn with_child(mut self, child: Self) -> Self {
5914        self.add_child(child);
5915        self
5916    }
5917    #[inline]
5918    #[must_use] pub fn with_node_type(mut self, node_type: NodeType) -> Self {
5919        self.root.set_node_type(node_type);
5920        self
5921    }
5922    #[inline]
5923    #[must_use] pub fn with_id(mut self, id: AzString) -> Self {
5924        self.root.add_id(id);
5925        self
5926    }
5927    #[inline]
5928    #[must_use] pub fn with_class(mut self, class: AzString) -> Self {
5929        self.root.add_class(class);
5930        self
5931    }
5932    #[inline]
5933    #[must_use]
5934    pub fn with_callback<C: Into<CoreCallback>>(
5935        mut self,
5936        event: EventFilter,
5937        data: RefAny,
5938        callback: C,
5939    ) -> Self {
5940        self.root.add_callback(event, data, callback);
5941        self
5942    }
5943    /// Add a CSS property with optional conditions (hover, focus, active, etc.)
5944    #[inline]
5945    #[must_use] pub fn with_css_property(mut self, prop: CssPropertyWithConditions) -> Self {
5946        self.root.add_css_property(prop);
5947        self
5948    }
5949    /// Add a CSS property with optional conditions (hover, focus, active, etc.)
5950    #[inline]
5951    pub fn add_css_property(&mut self, prop: CssPropertyWithConditions) {
5952        self.root.add_css_property(prop);
5953    }
5954    #[inline]
5955    pub fn add_class(&mut self, class: AzString) {
5956        self.root.add_class(class);
5957    }
5958    #[inline]
5959    pub fn add_callback<C: Into<CoreCallback>>(
5960        &mut self,
5961        event: EventFilter,
5962        data: RefAny,
5963        callback: C,
5964    ) {
5965        self.root.add_callback(event, data, callback);
5966    }
5967    #[inline]
5968    pub const fn set_tab_index(&mut self, tab_index: TabIndex) {
5969        self.root.set_tab_index(tab_index);
5970    }
5971    #[inline]
5972    pub const fn set_contenteditable(&mut self, contenteditable: bool) {
5973        self.root.set_contenteditable(contenteditable);
5974    }
5975    #[inline]
5976    #[must_use] pub const fn with_tab_index(mut self, tab_index: TabIndex) -> Self {
5977        self.root.set_tab_index(tab_index);
5978        self
5979    }
5980    #[inline]
5981    #[must_use] pub const fn with_contenteditable(mut self, contenteditable: bool) -> Self {
5982        self.root.set_contenteditable(contenteditable);
5983        self
5984    }
5985    #[inline]
5986    #[must_use] pub fn with_dataset(mut self, data: OptionRefAny) -> Self {
5987        self.root.set_dataset(data);
5988        self
5989    }
5990    #[inline]
5991    #[must_use] pub fn with_ids_and_classes(mut self, ids_and_classes: IdOrClassVec) -> Self {
5992        self.root.set_ids_and_classes(ids_and_classes);
5993        self
5994    }
5995
5996    /// Adds an attribute to this DOM element.
5997    #[inline]
5998    #[must_use] pub fn with_attribute(mut self, attr: AttributeType) -> Self {
5999        let mut attrs = self.root.attributes().clone();
6000        let mut v = attrs.into_library_owned_vec();
6001        v.push(attr);
6002        self.root.set_attributes(v.into());
6003        self
6004    }
6005
6006    /// Adds multiple attributes to this DOM element.
6007    #[inline]
6008    #[must_use] pub fn with_attributes(mut self, attributes: AttributeTypeVec) -> Self {
6009        self.root.set_attributes(attributes);
6010        self
6011    }
6012
6013    #[inline]
6014    #[must_use] pub fn with_callbacks(mut self, callbacks: CoreCallbackDataVec) -> Self {
6015        self.root.callbacks = callbacks;
6016        self
6017    }
6018    /// Legacy: builder-form for the flat property+conditions list. Each entry
6019    /// becomes a single-declaration rule at `rule_priority::INLINE`.
6020    #[inline]
6021    #[must_use] pub fn with_css_props(mut self, css_props: CssPropertyWithConditionsVec) -> Self {
6022        self.root.style = css_props.into();
6023        self
6024    }
6025    /// Builder-form for setting the inline `Css` directly.
6026    #[inline]
6027    #[must_use] pub fn with_style(mut self, style: azul_css::css::Css) -> Self {
6028        self.root.style = style;
6029        self
6030    }
6031
6032    /// Assigns a stable key to the root node of this DOM for reconciliation.
6033    ///
6034    /// This is crucial for performance and correct state preservation when
6035    /// lists of items change order or items are inserted/removed.
6036    ///
6037    /// # Example
6038    /// ```rust
6039    /// # use azul_core::dom::Dom;
6040    /// Dom::create_div()
6041    ///     .with_key("user-avatar-123");
6042    /// ```
6043    #[inline]
6044    #[must_use]
6045    pub fn with_key<K: Hash>(mut self, key: K) -> Self {
6046        self.root.set_key(key);
6047        self
6048    }
6049
6050    /// Registers a callback to merge dataset state from the previous frame.
6051    ///
6052    /// This is used for components that maintain heavy internal state (video players,
6053    /// WebGL contexts, network connections) that should not be destroyed and recreated
6054    /// on every render frame.
6055    ///
6056    /// The callback receives both datasets as `RefAny` (cheap shallow clones) and
6057    /// returns the `RefAny` that should be used for the new node.
6058    #[inline]
6059    #[must_use]
6060    pub fn with_merge_callback<C: Into<DatasetMergeCallback>>(mut self, callback: C) -> Self {
6061        self.root.set_merge_callback(callback);
6062        self
6063    }
6064
6065    /// Parse and set CSS styles with full selector support.
6066    ///
6067    /// This is the unified API for setting inline CSS on a DOM node. It supports:
6068    /// - Simple properties: `color: red; font-size: 14px;`
6069    /// - Pseudo-selectors: `:hover { background: blue; }`
6070    /// - @-rules: `@os linux { font-size: 14px; }`
6071    /// - Nesting: `@os linux { font-size: 14px; :hover { color: red; }}`
6072    ///
6073    /// # Examples
6074    /// ```rust
6075    /// # use azul_core::dom::Dom;
6076    /// // Simple inline styles
6077    /// Dom::create_div().with_css("color: red; font-size: 14px;");
6078    ///
6079    /// // With hover and active states
6080    /// Dom::create_div().with_css("
6081    ///     color: blue;
6082    ///     :hover { color: red; }
6083    ///     :active { color: green; }
6084    /// ");
6085    ///
6086    /// // OS-specific with nested hover
6087    /// Dom::create_div().with_css("
6088    ///     font-size: 12px;
6089    ///     @os linux { font-size: 14px; :hover { color: red; }}
6090    ///     @os windows { font-size: 13px; }
6091    /// ");
6092    /// ```
6093    pub fn set_css(&mut self, style: &str) {
6094        // Unified, `@scope`-like model: a CSS string parses into a `Css` struct that is
6095        // pushed onto THIS Dom subtree's `.css` vec, where the cascade selector-matches
6096        // it against the subtree (`collect_css_from_dom` → `CssPropertyCache::restyle`).
6097        // `with_css` is the single CSS entry point — there is no separate node-only inline
6098        // path, and the old `with_component_css` is folded into this. A bare-declaration
6099        // string (`color: red`) parses to `* { color: red }` and so applies to the whole
6100        // subtree, exactly like attaching a `@scope { :scope { ... } }` block.
6101        self.add_component_css(azul_css::css::Css::parse_inline(style));
6102    }
6103
6104    /// Builder method for `set_css`
6105    #[must_use] pub fn with_css(mut self, style: &str) -> Self {
6106        self.set_css(style);
6107        self
6108    }
6109
6110    /// Sets the context menu for the root node
6111    #[inline]
6112    pub fn set_context_menu(&mut self, context_menu: Menu) {
6113        self.root.set_context_menu(context_menu);
6114    }
6115
6116    #[inline]
6117    #[must_use] pub fn with_context_menu(mut self, context_menu: Menu) -> Self {
6118        self.set_context_menu(context_menu);
6119        self
6120    }
6121
6122    /// Sets the menu bar for the root node
6123    #[inline]
6124    pub fn set_menu_bar(&mut self, menu_bar: Menu) {
6125        self.root.set_menu_bar(menu_bar);
6126    }
6127
6128    #[inline]
6129    #[must_use] pub fn with_menu_bar(mut self, menu_bar: Menu) -> Self {
6130        self.set_menu_bar(menu_bar);
6131        self
6132    }
6133
6134    #[inline]
6135    #[must_use] pub fn with_clip_mask(mut self, clip_mask: ImageMask) -> Self {
6136        self.root.set_clip_mask(clip_mask);
6137        self
6138    }
6139
6140    #[inline]
6141    #[must_use] pub fn with_svg_clip_path(mut self, clip: crate::svg::SvgMultiPolygon) -> Self {
6142        self.root.set_svg_data(SvgNodeData::Path(clip));
6143        self
6144    }
6145
6146    #[inline]
6147    #[must_use] pub fn with_svg_data(mut self, data: SvgNodeData) -> Self {
6148        self.root.set_svg_data(data);
6149        self
6150    }
6151
6152    #[inline]
6153    #[must_use] pub fn with_accessibility_info(mut self, accessibility_info: AccessibilityInfo) -> Self {
6154        self.root.set_accessibility_info(accessibility_info);
6155        self
6156    }
6157
6158    pub fn fixup_children_estimated(&mut self) -> usize {
6159        if self.children.is_empty() {
6160            self.estimated_total_children = 0;
6161        } else {
6162            self.estimated_total_children = self
6163                .children
6164                .iter_mut()
6165                .map(|s| s.fixup_children_estimated() + 1)
6166                .sum();
6167        }
6168        self.estimated_total_children
6169    }
6170}
6171
6172impl core::iter::FromIterator<Self> for Dom {
6173    fn from_iter<I: IntoIterator<Item = Self>>(iter: I) -> Self {
6174        let mut estimated_total_children = 0;
6175        let children = iter
6176            .into_iter()
6177            .inspect(|c| {
6178                estimated_total_children += c.estimated_total_children + 1;
6179            })
6180            .collect::<Vec<Self>>();
6181
6182        Self {
6183            root: NodeData::create_div(),
6184            children: children.into(),
6185            css: azul_css::css::CssVec::from_const_slice(&[]),
6186            estimated_total_children,
6187        }
6188    }
6189}
6190
6191impl fmt::Debug for Dom {
6192    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6193        fn print_dom(d: &Dom, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6194            write!(f, "Dom {{\r\n")?;
6195            write!(f, "\troot: {:#?}\r\n", d.root)?;
6196            write!(
6197                f,
6198                "\testimated_total_children: {:#?}\r\n",
6199                d.estimated_total_children
6200            )?;
6201            write!(f, "\tchildren: [\r\n")?;
6202            for c in &d.children {
6203                print_dom(c, f)?;
6204            }
6205            write!(f, "\t]\r\n")?;
6206            write!(f, "}}\r\n")?;
6207            Ok(())
6208        }
6209
6210        print_dom(self, f)
6211    }
6212}
6213
6214#[cfg(test)]
6215mod audit_tests {
6216    use super::*;
6217
6218    #[test]
6219    fn node_count_matches_recompute() {
6220        // root + [A(+grandchild), B] = 3 descendants, node_count 4.
6221        let dom = Dom::create_div()
6222            .with_child(Dom::create_div().with_child(Dom::create_div()))
6223            .with_child(Dom::create_div());
6224        assert_eq!(
6225            dom.estimated_total_children,
6226            dom.recompute_estimated_total_children()
6227        );
6228        assert_eq!(dom.estimated_total_children, 3);
6229        assert_eq!(dom.node_count(), 4);
6230    }
6231
6232    #[test]
6233    fn single_node_dom_node_count() {
6234        let dom = Dom::create_div();
6235        assert_eq!(dom.estimated_total_children, 0);
6236        assert_eq!(dom.node_count(), 1);
6237        assert_eq!(dom.recompute_estimated_total_children(), 0);
6238    }
6239
6240    #[test]
6241    fn fixup_repairs_desynced_estimate() {
6242        let mut dom = Dom::create_div().with_child(Dom::create_div());
6243        // Corrupt the public cached field directly.
6244        dom.estimated_total_children = 999;
6245        let repaired = dom.fixup_children_estimated();
6246        assert_eq!(repaired, 1);
6247        assert_eq!(
6248            dom.estimated_total_children,
6249            dom.recompute_estimated_total_children()
6250        );
6251    }
6252
6253    // The debug_assert only fires with debug_assertions enabled.
6254    #[cfg(debug_assertions)]
6255    #[test]
6256    #[should_panic(expected = "desynced")]
6257    fn add_child_with_stale_estimate_panics_in_debug() {
6258        let mut child = Dom::create_div().with_child(Dom::create_div());
6259        child.estimated_total_children = 0; // corrupt: should be 1
6260        let mut parent = Dom::create_div();
6261        parent.add_child(child);
6262    }
6263
6264    // NodeData carries a manual `unsafe impl Send`. This is a compile-time
6265    // assertion that the marker holds (fails to build if a non-Send field is
6266    // ever added), documenting the invariant the unsafe impl relies on.
6267    #[test]
6268    fn node_data_is_send() {
6269        fn assert_send<T: Send>() {}
6270        assert_send::<NodeData>();
6271    }
6272
6273    // Exercises the `core::ptr::write` unsafe path in `copy_special_moving_complex`:
6274    // the boxed Text `node_type` must be MOVED bitwise into the copy (box pointer
6275    // preserved, string intact), `self.node_type` must be left as `Div`, and the
6276    // moved-out `style`/`extra` must land on the copy. Small heap-only tree, so
6277    // Miri can validate the raw write / box ownership transfer for UB.
6278    #[test]
6279    fn copy_special_moving_complex_moves_text_node_type() {
6280        let mut nd = NodeData::create_text("hello").with_css("color: red;");
6281        assert!(!nd.style.rules.is_empty(), "precondition: style set");
6282
6283        let copy = nd.copy_special_moving_complex();
6284
6285        // The Text box was transferred to the copy with its string intact.
6286        match copy.get_node_type() {
6287            NodeType::Text(s) => assert_eq!(s.as_ref().as_str(), "hello"),
6288            other => panic!("expected Text node_type on copy, got {other:?}"),
6289        }
6290        // The source's node_type was replaced with the heap-free Div placeholder.
6291        assert!(matches!(nd.get_node_type(), NodeType::Div));
6292        // `style` was moved out of `self` onto the copy.
6293        assert!(nd.style.rules.is_empty());
6294        assert!(!copy.style.rules.is_empty());
6295    }
6296
6297    // A non-boxed (Div) node_type must also survive the ptr::write path unchanged.
6298    #[test]
6299    fn copy_special_moving_complex_moves_div_node_type() {
6300        let mut nd = NodeData::create_div();
6301        let copy = nd.copy_special_moving_complex();
6302        assert!(matches!(copy.get_node_type(), NodeType::Div));
6303        assert!(matches!(nd.get_node_type(), NodeType::Div));
6304    }
6305}
6306
6307#[cfg(test)]
6308#[allow(clippy::cast_possible_wrap, clippy::too_many_lines)]
6309mod autotest_generated {
6310    use super::*;
6311
6312    // ---------------------------------------------------------------------
6313    // helpers
6314    // ---------------------------------------------------------------------
6315
6316    fn hash_of<T: Hash>(t: &T) -> u64 {
6317        let mut h = crate::hash::DefaultHasher::new();
6318        t.hash(&mut h);
6319        h.finish()
6320    }
6321
6322    extern "C" fn merge_cb_a(new_data: RefAny, _old: RefAny) -> RefAny {
6323        new_data
6324    }
6325
6326    extern "C" fn merge_cb_b(_new: RefAny, old_data: RefAny) -> RefAny {
6327        old_data
6328    }
6329
6330    /// A `VirtualViewCallbackType`-shaped stub. Never invoked — the tests only need
6331    /// a well-typed callback to hang off a `NodeType::VirtualView` node.
6332    extern "C" fn virtual_view_cb(
6333        _data: RefAny,
6334        _info: crate::callbacks::VirtualViewCallbackInfo,
6335    ) -> crate::callbacks::VirtualViewReturn {
6336        unreachable!("virtual view callback is never invoked by these tests")
6337    }
6338
6339    fn virtual_view_callback() -> VirtualViewCallback {
6340        VirtualViewCallback {
6341            cb: virtual_view_cb,
6342            ctx: OptionRefAny::None,
6343        }
6344    }
6345
6346    /// A ~100k-char string with multi-byte codepoints, for "huge input" cases.
6347    fn huge_unicode_string() -> String {
6348        "ä🎉本".repeat(25_000)
6349    }
6350
6351    /// Every `AttributeType` variant, so invariant sweeps can't silently miss one.
6352    fn all_attribute_variants() -> Vec<AttributeType> {
6353        let nv = || AttributeNameValue {
6354            attr_name: "data-x".into(),
6355            value: "v".into(),
6356        };
6357        vec![
6358            AttributeType::Id("i".into()),
6359            AttributeType::Class("c".into()),
6360            AttributeType::AriaLabel("l".into()),
6361            AttributeType::AriaLabelledBy("lb".into()),
6362            AttributeType::AriaDescribedBy("db".into()),
6363            AttributeType::AriaRole("r".into()),
6364            AttributeType::AriaState(nv()),
6365            AttributeType::AriaProperty(nv()),
6366            AttributeType::Href("h".into()),
6367            AttributeType::Rel("rel".into()),
6368            AttributeType::Target("t".into()),
6369            AttributeType::Src("s".into()),
6370            AttributeType::Alt("a".into()),
6371            AttributeType::Title("ti".into()),
6372            AttributeType::Name("n".into()),
6373            AttributeType::Value("v".into()),
6374            AttributeType::InputType("text".into()),
6375            AttributeType::Placeholder("p".into()),
6376            AttributeType::Required,
6377            AttributeType::Disabled,
6378            AttributeType::Readonly,
6379            AttributeType::CheckedTrue,
6380            AttributeType::CheckedFalse,
6381            AttributeType::Selected,
6382            AttributeType::Max("10".into()),
6383            AttributeType::Min("0".into()),
6384            AttributeType::Step("1".into()),
6385            AttributeType::Pattern(".*".into()),
6386            AttributeType::MinLength(i32::MIN),
6387            AttributeType::MaxLength(i32::MAX),
6388            AttributeType::Autocomplete("off".into()),
6389            AttributeType::Scope("row".into()),
6390            AttributeType::ColSpan(-1),
6391            AttributeType::RowSpan(0),
6392            AttributeType::TabIndex(i32::MIN),
6393            AttributeType::Focusable,
6394            AttributeType::Lang("en".into()),
6395            AttributeType::Dir("rtl".into()),
6396            AttributeType::ContentEditable(true),
6397            AttributeType::Draggable(false),
6398            AttributeType::Hidden,
6399            AttributeType::Data(nv()),
6400            AttributeType::Custom(nv()),
6401        ]
6402    }
6403
6404    /// A spread of `NodeType`s, including every payload-carrying variant.
6405    fn representative_node_types() -> Vec<NodeType> {
6406        vec![
6407            NodeType::Html,
6408            NodeType::Body,
6409            NodeType::Div,
6410            NodeType::Br,
6411            NodeType::Button,
6412            NodeType::Input,
6413            NodeType::TextArea,
6414            NodeType::Select,
6415            NodeType::A,
6416            NodeType::H1,
6417            NodeType::H6,
6418            NodeType::Table,
6419            NodeType::Td,
6420            NodeType::Svg,
6421            NodeType::SvgPath,
6422            NodeType::SvgText("svg text".into()),
6423            NodeType::SvgImage(ImageRef::null_image(
6424                1,
6425                1,
6426                crate::resources::RawImageFormat::R8,
6427                Vec::new(),
6428            )),
6429            NodeType::Before,
6430            NodeType::After,
6431            NodeType::Marker,
6432            NodeType::Placeholder,
6433            NodeType::Text(BoxOrStatic::heap(AzString::from("hello"))),
6434            NodeType::Image(BoxOrStatic::heap(ImageRef::null_image(
6435                2,
6436                2,
6437                crate::resources::RawImageFormat::RGBA8,
6438                Vec::new(),
6439            ))),
6440            NodeType::VirtualView,
6441            NodeType::Icon(BoxOrStatic::heap(AzString::from("home"))),
6442            NodeType::GeolocationProbe(crate::geolocation::GeolocationProbeConfig::default()),
6443        ]
6444    }
6445
6446    // =====================================================================
6447    // NodeFlags — bit-packing round-trips, boundaries, field independence
6448    // =====================================================================
6449
6450    #[test]
6451    fn node_flags_new_is_empty_and_matches_default() {
6452        let f = NodeFlags::new();
6453        assert_eq!(f.inner, 0);
6454        assert_eq!(f, NodeFlags::default());
6455        assert!(!f.is_contenteditable());
6456        assert!(!f.is_anonymous());
6457        assert_eq!(f.get_tab_index(), None);
6458    }
6459
6460    #[test]
6461    fn node_flags_tab_index_round_trips_for_all_variants() {
6462        for ti in [
6463            None,
6464            Some(TabIndex::Auto),
6465            Some(TabIndex::NoKeyboardFocus),
6466            Some(TabIndex::OverrideInParent(0)),
6467            Some(TabIndex::OverrideInParent(1)),
6468            Some(TabIndex::OverrideInParent(1_000)),
6469        ] {
6470            let mut f = NodeFlags::new();
6471            f.set_tab_index(ti);
6472            assert_eq!(f.get_tab_index(), ti, "round-trip failed for {ti:?}");
6473        }
6474    }
6475
6476    #[test]
6477    fn node_flags_tab_index_round_trips_at_the_28_bit_boundary() {
6478        // The value field is bits [27:0], so 2^28 - 1 is the largest exactly
6479        // representable OverrideInParent value.
6480        const MAX_EXACT: u32 = (1 << 28) - 1;
6481        let mut f = NodeFlags::new();
6482        f.set_tab_index(Some(TabIndex::OverrideInParent(MAX_EXACT)));
6483        assert_eq!(
6484            f.get_tab_index(),
6485            Some(TabIndex::OverrideInParent(MAX_EXACT))
6486        );
6487    }
6488
6489    #[test]
6490    fn node_flags_tab_index_above_28_bits_truncates_without_corrupting_other_flags() {
6491        // AUDIT: `set_tab_index` masks the value with TAB_VALUE_MASK ((1 << 28) - 1),
6492        // so any OverrideInParent >= 2^28 is SILENTLY TRUNCATED rather than rejected
6493        // or saturated. That is lossy, but the safety-critical property is that the
6494        // overflowing bits must not bleed into the anonymous / contenteditable /
6495        // tab-variant bits. Pin both facts.
6496        const OVERFLOW: u32 = 1 << 28;
6497        let mut f = NodeFlags::new();
6498        f.set_tab_index(Some(TabIndex::OverrideInParent(OVERFLOW)));
6499        assert_eq!(
6500            f.get_tab_index(),
6501            Some(TabIndex::OverrideInParent(0)),
6502            "2^28 truncates to 0 (documented lossiness)"
6503        );
6504        assert!(!f.is_anonymous(), "overflow bit must not set ANONYMOUS");
6505        assert!(!f.is_contenteditable());
6506
6507        let mut f = NodeFlags::new();
6508        f.set_tab_index(Some(TabIndex::OverrideInParent(u32::MAX)));
6509        assert_eq!(
6510            f.get_tab_index(),
6511            Some(TabIndex::OverrideInParent((1 << 28) - 1)),
6512            "u32::MAX truncates to the 28-bit mask"
6513        );
6514        assert!(!f.is_anonymous(), "u32::MAX must not set ANONYMOUS");
6515        assert!(!f.is_contenteditable(), "u32::MAX must not set CONTENTEDITABLE");
6516    }
6517
6518    #[test]
6519    fn node_flags_set_tab_index_preserves_contenteditable_and_anonymous() {
6520        let mut f = NodeFlags::new();
6521        f.set_contenteditable_mut(true);
6522        f.set_anonymous(true);
6523
6524        for ti in [
6525            None,
6526            Some(TabIndex::Auto),
6527            Some(TabIndex::NoKeyboardFocus),
6528            // NodeFlags packs the override into a documented 28-bit field
6529            // (TAB_VALUE_MASK), so u32::MAX would truncate to (1<<28)-1 and not
6530            // round-trip. (1<<28)-1 IS the largest encodable value -- still the
6531            // boundary case, but one this API can actually represent.
6532            Some(TabIndex::OverrideInParent((1 << 28) - 1)),
6533            Some(TabIndex::OverrideInParent(7)),
6534        ] {
6535            f.set_tab_index(ti);
6536            assert!(f.is_contenteditable(), "contenteditable lost for {ti:?}");
6537            assert!(f.is_anonymous(), "anonymous lost for {ti:?}");
6538            assert_eq!(f.get_tab_index(), ti);
6539        }
6540    }
6541
6542    #[test]
6543    fn node_flags_set_contenteditable_preserves_tab_index_and_anonymous() {
6544        let mut f = NodeFlags::new();
6545        f.set_anonymous(true);
6546        f.set_tab_index(Some(TabIndex::OverrideInParent(12_345)));
6547
6548        f.set_contenteditable_mut(true);
6549        assert!(f.is_contenteditable());
6550        assert!(f.is_anonymous());
6551        assert_eq!(f.get_tab_index(), Some(TabIndex::OverrideInParent(12_345)));
6552
6553        f.set_contenteditable_mut(false);
6554        assert!(!f.is_contenteditable());
6555        assert!(f.is_anonymous());
6556        assert_eq!(f.get_tab_index(), Some(TabIndex::OverrideInParent(12_345)));
6557    }
6558
6559    #[test]
6560    fn node_flags_set_anonymous_preserves_tab_index_and_contenteditable() {
6561        let mut f = NodeFlags::new();
6562        f.set_contenteditable_mut(true);
6563        f.set_tab_index(Some(TabIndex::NoKeyboardFocus));
6564
6565        f.set_anonymous(true);
6566        assert!(f.is_anonymous());
6567        assert!(f.is_contenteditable());
6568        assert_eq!(f.get_tab_index(), Some(TabIndex::NoKeyboardFocus));
6569
6570        f.set_anonymous(false);
6571        assert!(!f.is_anonymous());
6572        assert!(f.is_contenteditable());
6573        assert_eq!(f.get_tab_index(), Some(TabIndex::NoKeyboardFocus));
6574    }
6575
6576    #[test]
6577    fn node_flags_consecutive_set_contenteditable_is_idempotent() {
6578        let mut f = NodeFlags::new();
6579        f.set_contenteditable_mut(true);
6580        let once = f;
6581        f.set_contenteditable_mut(true);
6582        assert_eq!(f, once, "setting twice must not toggle");
6583    }
6584
6585    #[test]
6586    fn node_flags_builder_and_mut_setter_agree() {
6587        for v in [true, false] {
6588            let builder = NodeFlags::new().set_contenteditable(v);
6589            let mut mutated = NodeFlags::new();
6590            mutated.set_contenteditable_mut(v);
6591            assert_eq!(builder, mutated, "builder/mut disagree for {v}");
6592        }
6593    }
6594
6595    #[test]
6596    fn node_flags_all_bits_set_decodes_without_panicking() {
6597        // Adversarial: a NodeFlags whose `inner` was never produced by the setters
6598        // (e.g. deserialized from a hostile FFI caller). Every getter must still
6599        // return a deterministic value instead of panicking.
6600        let f = NodeFlags { inner: u32::MAX };
6601        assert!(f.is_contenteditable());
6602        assert!(f.is_anonymous());
6603        // bits [30:29] == 0b11 == TAB_NO_KEYBOARD
6604        assert_eq!(f.get_tab_index(), Some(TabIndex::NoKeyboardFocus));
6605    }
6606
6607    #[test]
6608    fn node_flags_get_tab_index_is_total_over_the_tag_bits() {
6609        // The `_ => None` arm of get_tab_index is unreachable (2 bits => 4 patterns,
6610        // all matched). Prove every tag pattern decodes to Some/None deterministically.
6611        for tag in 0u32..4 {
6612            for extra in [0u32, u32::MAX] {
6613                let inner = (tag << 29) | (extra & !(0b11 << 29));
6614                let f = NodeFlags { inner };
6615                let decoded = f.get_tab_index();
6616                match tag {
6617                    0 => assert_eq!(decoded, None),
6618                    1 => assert_eq!(decoded, Some(TabIndex::Auto)),
6619                    2 => assert!(matches!(decoded, Some(TabIndex::OverrideInParent(_)))),
6620                    _ => assert_eq!(decoded, Some(TabIndex::NoKeyboardFocus)),
6621                }
6622            }
6623        }
6624    }
6625
6626    // =====================================================================
6627    // TabIndex — numeric limits
6628    // =====================================================================
6629
6630    #[test]
6631    fn tab_index_default_is_auto_with_index_zero() {
6632        assert_eq!(TabIndex::default(), TabIndex::Auto);
6633        assert_eq!(TabIndex::default().get_index(), 0);
6634    }
6635
6636    #[test]
6637    fn tab_index_get_index_at_numeric_limits() {
6638        assert_eq!(TabIndex::Auto.get_index(), 0);
6639        assert_eq!(TabIndex::NoKeyboardFocus.get_index(), -1);
6640        assert_eq!(TabIndex::OverrideInParent(0).get_index(), 0);
6641        // u32 -> isize must widen, never wrap negative (isize is >= 32 bits on all
6642        // supported targets, so u32::MAX stays positive).
6643        let max = TabIndex::OverrideInParent(u32::MAX).get_index();
6644        assert_eq!(max, u32::MAX as isize);
6645        assert!(max > 0, "u32::MAX must not wrap to a negative isize");
6646    }
6647
6648    #[test]
6649    fn get_effective_tabindex_saturates_into_i32() {
6650        // Reached through NodeFlags, OverrideInParent is capped at 2^28 - 1, which
6651        // always fits i32 — so the i32::MAX saturation arm is not reachable via a
6652        // NodeData. Pin what IS reachable.
6653        let nd = NodeData::create_div().with_tab_index(TabIndex::OverrideInParent(u32::MAX));
6654        assert_eq!(nd.get_effective_tabindex(), Some((1 << 28) - 1));
6655
6656        assert_eq!(
6657            NodeData::create_div()
6658                .with_tab_index(TabIndex::Auto)
6659                .get_effective_tabindex(),
6660            Some(0)
6661        );
6662        assert_eq!(
6663            NodeData::create_div()
6664                .with_tab_index(TabIndex::NoKeyboardFocus)
6665                .get_effective_tabindex(),
6666            Some(-1)
6667        );
6668        assert_eq!(NodeData::create_div().get_effective_tabindex(), None);
6669    }
6670
6671    #[test]
6672    fn get_effective_tabindex_falls_back_to_zero_for_focus_callbacks() {
6673        let nd = NodeData::create_div().with_callback(
6674            EventFilter::Focus(FocusEventFilter::MouseDown),
6675            RefAny::new(0u32),
6676            0usize,
6677        );
6678        assert_eq!(nd.get_effective_tabindex(), Some(0));
6679    }
6680
6681    // =====================================================================
6682    // TagId / ScrollTagId
6683    // =====================================================================
6684
6685    #[test]
6686    fn tag_id_unique_never_returns_zero_and_never_repeats() {
6687        // 0 is reserved for "no tag". Other tests in this binary also allocate tags,
6688        // so assert distinctness rather than a specific starting value.
6689        let ids: Vec<TagId> = (0..512).map(|_| TagId::unique()).collect();
6690        for id in &ids {
6691            assert_ne!(id.inner, 0, "TagId 0 is reserved for 'no tag'");
6692        }
6693        let mut sorted: Vec<u64> = ids.iter().map(|t| t.inner).collect();
6694        sorted.sort_unstable();
6695        sorted.dedup();
6696        assert_eq!(sorted.len(), 512, "TagId::unique() handed out a duplicate");
6697    }
6698
6699    #[test]
6700    fn tag_id_crate_internal_conversions_are_identity_at_limits() {
6701        for inner in [0u64, 1, u64::MAX, u64::MAX - 1] {
6702            let t = TagId { inner };
6703            assert_eq!(t.into_crate_internal(), t);
6704            assert_eq!(TagId::from_crate_internal(t), t);
6705            // Round-trip through both directions.
6706            assert_eq!(
6707                TagId::from_crate_internal(t.into_crate_internal()).inner,
6708                inner
6709            );
6710        }
6711    }
6712
6713    #[test]
6714    fn tag_id_display_is_non_empty_at_numeric_limits() {
6715        for inner in [0u64, 1, u64::MAX] {
6716            let s = format!("{}", TagId { inner });
6717            assert!(!s.is_empty());
6718            assert!(s.contains(&inner.to_string()), "{s} should contain {inner}");
6719        }
6720    }
6721
6722    #[test]
6723    fn scroll_tag_id_unique_is_distinct_and_debug_matches_display() {
6724        let a = ScrollTagId::unique();
6725        let b = ScrollTagId::unique();
6726        assert_ne!(a, b);
6727        assert_ne!(a.inner.inner, 0);
6728
6729        let s = ScrollTagId {
6730            inner: TagId { inner: u64::MAX },
6731        };
6732        assert_eq!(format!("{s:?}"), format!("{s}"));
6733        assert!(!format!("{s}").is_empty());
6734    }
6735
6736    // =====================================================================
6737    // AttributeType — getters / predicates / serializer invariants
6738    // =====================================================================
6739
6740    #[test]
6741    fn attribute_boolean_attrs_always_have_an_empty_value() {
6742        // Invariant: is_boolean() means "present == true", so there is nothing to
6743        // serialize on the right-hand side.
6744        for attr in all_attribute_variants() {
6745            if attr.is_boolean() {
6746                assert_eq!(
6747                    attr.value().as_str(),
6748                    "",
6749                    "boolean attr {} must have an empty value",
6750                    attr.name()
6751                );
6752            }
6753        }
6754    }
6755
6756    #[test]
6757    fn attribute_name_and_value_never_panic_for_any_variant() {
6758        for attr in all_attribute_variants() {
6759            let name = attr.name();
6760            let value = attr.value();
6761            // Every built-in variant has a non-empty name; only a Custom/Data
6762            // attribute can carry a caller-supplied empty name (see next test).
6763            assert!(!name.is_empty(), "empty name for {attr:?}");
6764            let _ = value.as_str();
6765        }
6766    }
6767
6768    #[test]
6769    fn attribute_custom_with_empty_name_returns_empty_name_without_panicking() {
6770        let attr = AttributeType::Custom(AttributeNameValue {
6771            attr_name: "".into(),
6772            value: "".into(),
6773        });
6774        assert_eq!(attr.name(), "");
6775        assert_eq!(attr.value().as_str(), "");
6776        assert!(!attr.is_boolean());
6777    }
6778
6779    #[test]
6780    fn attribute_as_id_and_as_class_are_mutually_exclusive() {
6781        for attr in all_attribute_variants() {
6782            match &attr {
6783                AttributeType::Id(s) => {
6784                    assert_eq!(attr.as_id(), Some(s.as_str()));
6785                    assert_eq!(attr.as_class(), None);
6786                }
6787                AttributeType::Class(s) => {
6788                    assert_eq!(attr.as_class(), Some(s.as_str()));
6789                    assert_eq!(attr.as_id(), None);
6790                }
6791                _ => {
6792                    assert_eq!(attr.as_id(), None, "as_id must be None for {attr:?}");
6793                    assert_eq!(attr.as_class(), None, "as_class must be None for {attr:?}");
6794                }
6795            }
6796        }
6797    }
6798
6799    #[test]
6800    fn attribute_numeric_values_serialize_at_i32_limits() {
6801        assert_eq!(
6802            AttributeType::MinLength(i32::MIN).value().as_str(),
6803            "-2147483648"
6804        );
6805        assert_eq!(
6806            AttributeType::MaxLength(i32::MAX).value().as_str(),
6807            "2147483647"
6808        );
6809        assert_eq!(AttributeType::ColSpan(0).value().as_str(), "0");
6810        assert_eq!(AttributeType::RowSpan(-1).value().as_str(), "-1");
6811        assert_eq!(
6812            AttributeType::TabIndex(i32::MIN).value().as_str(),
6813            "-2147483648"
6814        );
6815    }
6816
6817    #[test]
6818    fn attribute_focusable_is_tabindex_zero_and_not_boolean() {
6819        // Boundary: `Focusable` shares the "tabindex" name with TabIndex(i32) but,
6820        // unlike the boolean attrs, serializes a value ("0").
6821        let f = AttributeType::Focusable;
6822        assert_eq!(f.name(), "tabindex");
6823        assert_eq!(f.value().as_str(), "0");
6824        assert!(!f.is_boolean());
6825        assert_eq!(AttributeType::TabIndex(0).name(), "tabindex");
6826    }
6827
6828    #[test]
6829    fn attribute_checked_true_and_false_are_both_boolean_and_share_a_name() {
6830        // AUDIT: CheckedFalse is_boolean() == true and value() == "", so a serializer
6831        // that emits boolean attrs as bare names would render `checked` for the
6832        // *unchecked* state. Pinning current behaviour — see report.
6833        assert!(AttributeType::CheckedTrue.is_boolean());
6834        assert!(AttributeType::CheckedFalse.is_boolean());
6835        assert_eq!(AttributeType::CheckedTrue.name(), "checked");
6836        assert_eq!(AttributeType::CheckedFalse.name(), "checked");
6837        assert_eq!(AttributeType::CheckedFalse.value().as_str(), "");
6838        // The two are still distinguishable as values.
6839        assert_ne!(AttributeType::CheckedTrue, AttributeType::CheckedFalse);
6840    }
6841
6842    #[test]
6843    fn attribute_content_editable_and_draggable_stringify_bools() {
6844        assert_eq!(AttributeType::ContentEditable(true).value().as_str(), "true");
6845        assert_eq!(
6846            AttributeType::ContentEditable(false).value().as_str(),
6847            "false"
6848        );
6849        assert_eq!(AttributeType::Draggable(true).value().as_str(), "true");
6850        assert_eq!(AttributeType::Draggable(false).value().as_str(), "false");
6851        assert!(!AttributeType::ContentEditable(false).is_boolean());
6852    }
6853
6854    #[test]
6855    fn attribute_round_trips_huge_unicode_values() {
6856        let big = huge_unicode_string();
6857        let attr = AttributeType::Value(big.clone().into());
6858        assert_eq!(attr.value().as_str(), big.as_str());
6859        assert_eq!(attr.name(), "value");
6860
6861        let id = AttributeType::Id(big.clone().into());
6862        assert_eq!(id.as_id(), Some(big.as_str()));
6863    }
6864
6865    #[test]
6866    fn id_or_class_accessors_are_mutually_exclusive() {
6867        let id = IdOrClass::Id("my-id".into());
6868        let class = IdOrClass::Class("my-class".into());
6869        assert_eq!(id.as_id(), Some("my-id"));
6870        assert_eq!(id.as_class(), None);
6871        assert_eq!(class.as_class(), Some("my-class"));
6872        assert_eq!(class.as_id(), None);
6873
6874        // Empty strings are legal and round-trip as Some("").
6875        assert_eq!(IdOrClass::Id("".into()).as_id(), Some(""));
6876        assert_eq!(IdOrClass::Class("".into()).as_class(), Some(""));
6877    }
6878
6879    // =====================================================================
6880    // InputType
6881    // =====================================================================
6882
6883    #[test]
6884    fn input_type_as_str_is_non_empty_and_unique_per_variant() {
6885        let all = [
6886            InputType::Text,
6887            InputType::Button,
6888            InputType::Checkbox,
6889            InputType::Color,
6890            InputType::Date,
6891            InputType::Datetime,
6892            InputType::DatetimeLocal,
6893            InputType::Email,
6894            InputType::File,
6895            InputType::Hidden,
6896            InputType::Image,
6897            InputType::Month,
6898            InputType::Number,
6899            InputType::Password,
6900            InputType::Radio,
6901            InputType::Range,
6902            InputType::Reset,
6903            InputType::Search,
6904            InputType::Submit,
6905            InputType::Tel,
6906            InputType::Time,
6907            InputType::Url,
6908            InputType::Week,
6909        ];
6910        let mut seen: Vec<&str> = all.iter().map(InputType::as_str).collect();
6911        for s in &seen {
6912            assert!(!s.is_empty());
6913            assert!(
6914                !s.contains(char::is_whitespace),
6915                "{s} is not a valid HTML attribute value"
6916            );
6917        }
6918        let len = seen.len();
6919        seen.sort_unstable();
6920        seen.dedup();
6921        assert_eq!(seen.len(), len, "two InputType variants share an as_str()");
6922
6923        assert_eq!(InputType::DatetimeLocal.as_str(), "datetime-local");
6924        assert_eq!(InputType::Text.as_str(), "text");
6925    }
6926
6927    // =====================================================================
6928    // NodeType
6929    // =====================================================================
6930
6931    #[test]
6932    fn node_type_to_library_owned_round_trips_every_variant() {
6933        // encode == decode: the deep-copy must be value-equal to the original,
6934        // including the payload-carrying (boxed) variants.
6935        for nt in representative_node_types() {
6936            let owned = nt.to_library_owned_nodetype();
6937            assert_eq!(owned, nt, "to_library_owned_nodetype lost data for {nt:?}");
6938            assert_eq!(owned.get_path(), nt.get_path());
6939        }
6940    }
6941
6942    #[test]
6943    fn node_type_get_path_and_format_never_panic() {
6944        for nt in representative_node_types() {
6945            let _tag = nt.get_path();
6946            let _fmt = nt.format();
6947            let _semantic = nt.is_semantic_for_accessibility();
6948        }
6949    }
6950
6951    #[test]
6952    fn node_type_format_returns_content_only_for_content_variants() {
6953        assert_eq!(NodeType::Div.format(), None);
6954        assert_eq!(NodeType::Br.format(), None);
6955        assert_eq!(NodeType::Button.format(), None);
6956
6957        assert_eq!(
6958            NodeType::Text(BoxOrStatic::heap(AzString::from("hi"))).format(),
6959            Some("hi".to_string())
6960        );
6961        assert_eq!(
6962            NodeType::VirtualView.format(),
6963            Some("virtualized-view".to_string())
6964        );
6965        assert_eq!(
6966            NodeType::Icon(BoxOrStatic::heap(AzString::from("home"))).format(),
6967            Some("icon(home)".to_string())
6968        );
6969    }
6970
6971    #[test]
6972    fn node_type_format_handles_empty_and_unicode_text() {
6973        assert_eq!(
6974            NodeType::Text(BoxOrStatic::heap(AzString::from(""))).format(),
6975            Some(String::new())
6976        );
6977        let unicode = "日本語 🎉 ünïcødé";
6978        assert_eq!(
6979            NodeType::Text(BoxOrStatic::heap(AzString::from(unicode))).format(),
6980            Some(unicode.to_string())
6981        );
6982    }
6983
6984    #[test]
6985    fn node_type_format_of_geolocation_probe_survives_nan_and_infinity() {
6986        // Adversarial floats: the probe config is formatted with `{}`, which must
6987        // print NaN/inf rather than panicking.
6988        for max_accuracy_m in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY, -0.0, f32::MAX] {
6989            let cfg = crate::geolocation::GeolocationProbeConfig {
6990                high_accuracy: true,
6991                background: true,
6992                max_accuracy_m,
6993                min_interval_ms: u32::MAX,
6994            };
6995            let out = NodeType::GeolocationProbe(cfg)
6996                .format()
6997                .expect("GeolocationProbe always formats");
6998            assert!(out.starts_with("geolocation-probe("));
6999            assert!(out.contains("4294967295"), "min_interval_ms must be printed");
7000        }
7001    }
7002
7003    #[test]
7004    fn geolocation_probe_nan_is_self_equal_and_hash_consistent() {
7005        // GeolocationProbeConfig gives f32 a total order via to_bits, so (unlike raw
7006        // f32) NaN == NaN. Eq and Hash must agree, or NodeType breaks as a HashMap key.
7007        let cfg = crate::geolocation::GeolocationProbeConfig {
7008            max_accuracy_m: f32::NAN,
7009            ..Default::default()
7010        };
7011        let a = NodeType::GeolocationProbe(cfg);
7012        let b = NodeType::GeolocationProbe(cfg);
7013        assert_eq!(a, b, "bitwise-NaN configs must compare equal");
7014        assert_eq!(
7015            hash_of(&a),
7016            hash_of(&b),
7017            "Eq == true but hashes differ: violates the Hash/Eq contract"
7018        );
7019        assert_eq!(a.cmp(&b), core::cmp::Ordering::Equal);
7020    }
7021
7022    #[test]
7023    fn node_type_is_semantic_for_accessibility_known_true_and_false() {
7024        for nt in [
7025            NodeType::Button,
7026            NodeType::Input,
7027            NodeType::TextArea,
7028            NodeType::Select,
7029            NodeType::A,
7030            NodeType::H1,
7031            NodeType::H6,
7032            NodeType::Article,
7033            NodeType::Nav,
7034            NodeType::Main,
7035        ] {
7036            assert!(
7037                nt.is_semantic_for_accessibility(),
7038                "{nt:?} should be semantic"
7039            );
7040        }
7041        for nt in [
7042            NodeType::Div,
7043            NodeType::Span,
7044            NodeType::Br,
7045            NodeType::VirtualView,
7046            NodeType::Text(BoxOrStatic::heap(AzString::from("x"))),
7047        ] {
7048            assert!(
7049                !nt.is_semantic_for_accessibility(),
7050                "{nt:?} should not be semantic"
7051            );
7052        }
7053    }
7054
7055    #[test]
7056    fn node_type_text_variants_are_content_sensitive() {
7057        let a = NodeType::Text(BoxOrStatic::heap(AzString::from("a")));
7058        let b = NodeType::Text(BoxOrStatic::heap(AzString::from("b")));
7059        assert_ne!(a, b);
7060        assert_eq!(a.get_path(), b.get_path(), "same tag, different content");
7061    }
7062
7063    // =====================================================================
7064    // NodeData — attributes, ids, classes
7065    // =====================================================================
7066
7067    #[test]
7068    fn node_data_default_has_no_attributes_and_no_extra_state() {
7069        let nd = NodeData::default();
7070        assert!(nd.is_node_type(NodeType::Div));
7071        assert!(nd.attributes().as_ref().is_empty());
7072        assert!(nd.get_ids_and_classes().as_ref().is_empty());
7073        assert!(nd.get_dataset().is_none());
7074        assert!(nd.get_key().is_none());
7075        assert!(nd.get_menu_bar().is_none());
7076        assert!(nd.get_context_menu().is_none());
7077        assert!(nd.get_svg_data().is_none());
7078        assert!(nd.get_image_clip_mask().is_none());
7079        assert!(nd.get_accessibility_info().is_none());
7080        assert!(nd.get_merge_callback().is_none());
7081        assert!(nd.get_component_origin().is_none());
7082        assert!(!nd.has_context_menu());
7083        assert!(!nd.is_contenteditable());
7084        assert!(!nd.is_anonymous());
7085        assert_eq!(nd.get_tab_index(), None);
7086    }
7087
7088    #[test]
7089    fn attributes_mut_lazily_allocates_but_stays_empty() {
7090        let mut nd = NodeData::create_div();
7091        assert!(nd.attributes().as_ref().is_empty());
7092        let _ = nd.attributes_mut(); // allocates NodeDataExt
7093        assert!(
7094            nd.attributes().as_ref().is_empty(),
7095            "lazy alloc must not invent attributes"
7096        );
7097        nd.add_id("x".into());
7098        assert_eq!(nd.attributes().as_ref().len(), 1);
7099    }
7100
7101    #[test]
7102    fn has_id_and_has_class_match_exactly_not_by_prefix() {
7103        let mut nd = NodeData::create_div();
7104        nd.add_id("header".into());
7105        nd.add_class("btn".into());
7106
7107        assert!(nd.has_id("header"));
7108        assert!(nd.has_class("btn"));
7109        // No prefix/substring matching.
7110        assert!(!nd.has_id("head"));
7111        assert!(!nd.has_id("header2"));
7112        assert!(!nd.has_class("bt"));
7113        assert!(!nd.has_class(""));
7114        // Ids and classes must not cross over.
7115        assert!(!nd.has_class("header"));
7116        assert!(!nd.has_id("btn"));
7117    }
7118
7119    #[test]
7120    fn has_id_matches_the_empty_string_id() {
7121        let mut nd = NodeData::create_div();
7122        assert!(!nd.has_id(""), "no ids at all => empty id must not match");
7123        nd.add_id("".into());
7124        assert!(nd.has_id(""), "an explicitly-added empty id must match");
7125        assert!(!nd.has_id("x"));
7126    }
7127
7128    #[test]
7129    fn has_id_and_has_class_handle_unicode_and_huge_strings() {
7130        let unicode = "日本語-🎉-ünïcødé";
7131        let big = huge_unicode_string();
7132
7133        let mut nd = NodeData::create_div();
7134        nd.add_id(unicode.into());
7135        nd.add_class(big.clone().into());
7136
7137        assert!(nd.has_id(unicode));
7138        assert!(nd.has_class(big.as_str()));
7139        // A truncated-at-a-codepoint-boundary prefix must not match.
7140        assert!(!nd.has_id("日本語"));
7141    }
7142
7143    #[test]
7144    fn duplicate_ids_are_kept_and_still_match() {
7145        let mut nd = NodeData::create_div();
7146        nd.add_id("dup".into());
7147        nd.add_id("dup".into());
7148        assert!(nd.has_id("dup"));
7149        assert_eq!(
7150            nd.get_ids_and_classes().as_ref().len(),
7151            2,
7152            "add_id does not deduplicate"
7153        );
7154    }
7155
7156    #[test]
7157    fn get_ids_and_classes_preserves_insertion_order_and_kind() {
7158        let mut nd = NodeData::create_div();
7159        nd.add_id("i1".into());
7160        nd.add_class("c1".into());
7161        nd.add_id("i2".into());
7162
7163        let v = nd.get_ids_and_classes();
7164        let v = v.as_ref();
7165        assert_eq!(v.len(), 3);
7166        assert_eq!(v[0], IdOrClass::Id("i1".into()));
7167        assert_eq!(v[1], IdOrClass::Class("c1".into()));
7168        assert_eq!(v[2], IdOrClass::Id("i2".into()));
7169    }
7170
7171    #[test]
7172    fn get_ids_and_classes_ignores_non_id_class_attributes() {
7173        let mut nd = NodeData::create_div();
7174        nd.set_attributes(
7175            vec![
7176                AttributeType::Href("/x".into()),
7177                AttributeType::Id("i".into()),
7178                AttributeType::Disabled,
7179                AttributeType::Class("c".into()),
7180            ]
7181            .into(),
7182        );
7183        let v = nd.get_ids_and_classes();
7184        assert_eq!(v.as_ref().len(), 2);
7185    }
7186
7187    #[test]
7188    fn set_ids_and_classes_replaces_ids_but_preserves_other_attributes() {
7189        // The dangerous part of set_ids_and_classes: it rebuilds the attribute vec.
7190        // Non-Id/Class attributes must survive.
7191        let mut nd = NodeData::create_div();
7192        nd.set_attributes(
7193            vec![
7194                AttributeType::Href("/old".into()),
7195                AttributeType::Id("old-id".into()),
7196                AttributeType::Class("old-class".into()),
7197                AttributeType::Disabled,
7198            ]
7199            .into(),
7200        );
7201
7202        nd.set_ids_and_classes(vec![IdOrClass::Class("new-class".into())].into());
7203
7204        assert!(!nd.has_id("old-id"), "old id must be dropped");
7205        assert!(!nd.has_class("old-class"), "old class must be dropped");
7206        assert!(nd.has_class("new-class"));
7207        // Href / Disabled must NOT have been collateral damage.
7208        let attrs = nd.attributes().as_ref();
7209        assert!(attrs.contains(&AttributeType::Href("/old".into())));
7210        assert!(attrs.contains(&AttributeType::Disabled));
7211        assert_eq!(attrs.len(), 3);
7212    }
7213
7214    #[test]
7215    fn set_ids_and_classes_with_an_empty_vec_clears_all_ids_and_classes() {
7216        let mut nd = NodeData::create_div();
7217        nd.add_id("i".into());
7218        nd.add_class("c".into());
7219        nd.set_ids_and_classes(Vec::new().into());
7220        assert!(nd.get_ids_and_classes().as_ref().is_empty());
7221        assert!(!nd.has_id("i"));
7222        assert!(!nd.has_class("c"));
7223    }
7224
7225    #[test]
7226    fn set_ids_and_classes_is_idempotent_when_reapplied() {
7227        let mut nd = NodeData::create_div();
7228        let ids: IdOrClassVec = vec![
7229            IdOrClass::Id("i".into()),
7230            IdOrClass::Class("c".into()),
7231        ]
7232        .into();
7233        nd.set_ids_and_classes(ids.clone());
7234        let after_first = nd.attributes().clone();
7235        nd.set_ids_and_classes(ids);
7236        assert_eq!(
7237            nd.attributes().as_ref(),
7238            after_first.as_ref(),
7239            "re-applying the same ids/classes must not duplicate them"
7240        );
7241    }
7242
7243    #[test]
7244    fn with_attribute_appends_without_dropping_existing_attributes() {
7245        // `with_attribute` is private, so this can only be exercised from an inline
7246        // test module.
7247        let nd = NodeData::create_div()
7248            .with_attribute(AttributeType::Href("/a".into()))
7249            .with_attribute(AttributeType::Alt("alt".into()));
7250        let attrs = nd.attributes().as_ref();
7251        assert_eq!(attrs.len(), 2);
7252        assert_eq!(attrs[0], AttributeType::Href("/a".into()));
7253        assert_eq!(attrs[1], AttributeType::Alt("alt".into()));
7254    }
7255
7256    // =====================================================================
7257    // NodeData — constructors
7258    // =====================================================================
7259
7260    #[test]
7261    fn create_node_shorthands_produce_the_right_node_type() {
7262        assert!(NodeData::create_body().is_node_type(NodeType::Body));
7263        assert!(NodeData::create_div().is_node_type(NodeType::Div));
7264        assert!(NodeData::create_br().is_node_type(NodeType::Br));
7265        assert!(NodeData::create_button_no_a11y().is_node_type(NodeType::Button));
7266        assert!(NodeData::create_table_no_a11y().is_node_type(NodeType::Table));
7267    }
7268
7269    #[test]
7270    fn create_text_accepts_empty_unicode_and_huge_input() {
7271        for s in ["", "x", "日本語 🎉"] {
7272            let nd = NodeData::create_text(s);
7273            assert!(nd.is_text_node());
7274            assert_eq!(nd.get_node_type().format(), Some(s.to_string()));
7275        }
7276        let big = huge_unicode_string();
7277        let nd = NodeData::create_text(big.clone());
7278        assert!(nd.is_text_node());
7279        assert_eq!(nd.get_node_type().format(), Some(big));
7280    }
7281
7282    #[test]
7283    fn create_a_stores_href_and_accessibility_name() {
7284        let nd = NodeData::create_a("/home".into(), SmallAriaInfo::label("Home"));
7285        assert!(nd.is_node_type(NodeType::A));
7286        assert!(nd
7287            .attributes()
7288            .as_ref()
7289            .contains(&AttributeType::Href("/home".into())));
7290        let info = nd
7291            .get_accessibility_info()
7292            .expect("create_a must set accessibility info");
7293        assert_eq!(info.accessibility_name, OptionString::Some("Home".into()));
7294    }
7295
7296    #[test]
7297    fn create_a_no_a11y_has_href_but_no_accessibility_info() {
7298        let nd = NodeData::create_a_no_a11y("/x".into());
7299        assert!(nd
7300            .attributes()
7301            .as_ref()
7302            .contains(&AttributeType::Href("/x".into())));
7303        assert!(nd.get_accessibility_info().is_none());
7304    }
7305
7306    #[test]
7307    fn create_a_accepts_an_empty_href() {
7308        let nd = NodeData::create_a_no_a11y("".into());
7309        assert_eq!(
7310            nd.attributes().as_ref()[0],
7311            AttributeType::Href("".into()),
7312            "empty href is stored verbatim, not dropped"
7313        );
7314    }
7315
7316    #[test]
7317    fn create_input_stores_all_three_attributes_in_order() {
7318        let nd = NodeData::create_input_no_a11y("text".into(), "user".into(), "Username".into());
7319        assert!(nd.is_node_type(NodeType::Input));
7320        let attrs = nd.attributes().as_ref();
7321        assert_eq!(attrs.len(), 3);
7322        assert_eq!(attrs[0], AttributeType::InputType("text".into()));
7323        assert_eq!(attrs[1], AttributeType::Name("user".into()));
7324        assert_eq!(attrs[2], AttributeType::AriaLabel("Username".into()));
7325    }
7326
7327    #[test]
7328    fn create_input_with_a11y_sets_both_attributes_and_accessibility_info() {
7329        let nd = NodeData::create_input(
7330            "password".into(),
7331            "pw".into(),
7332            "Password".into(),
7333            SmallAriaInfo::label("Password").with_role(AccessibilityRole::Text),
7334        );
7335        assert_eq!(nd.attributes().as_ref().len(), 3);
7336        let info = nd.get_accessibility_info().expect("a11y info");
7337        assert_eq!(info.role, AccessibilityRole::Text);
7338    }
7339
7340    #[test]
7341    fn create_textarea_and_select_store_name_and_label() {
7342        let ta = NodeData::create_textarea_no_a11y("body".into(), "Body".into());
7343        assert!(ta.is_node_type(NodeType::TextArea));
7344        assert_eq!(ta.attributes().as_ref().len(), 2);
7345
7346        let sel = NodeData::create_select_no_a11y("country".into(), "Country".into());
7347        assert!(sel.is_node_type(NodeType::Select));
7348        assert_eq!(
7349            sel.attributes().as_ref()[0],
7350            AttributeType::Name("country".into())
7351        );
7352    }
7353
7354    #[test]
7355    fn create_label_uses_a_custom_for_attribute() {
7356        let nd = NodeData::create_label_no_a11y("email-input".into());
7357        assert!(nd.is_node_type(NodeType::Label));
7358        assert_eq!(
7359            nd.attributes().as_ref()[0],
7360            AttributeType::Custom(AttributeNameValue {
7361                attr_name: "for".into(),
7362                value: "email-input".into(),
7363            })
7364        );
7365        assert_eq!(nd.attributes().as_ref()[0].name(), "for");
7366        assert_eq!(nd.attributes().as_ref()[0].value().as_str(), "email-input");
7367    }
7368
7369    #[test]
7370    fn create_button_and_table_with_aria_set_accessibility_info() {
7371        let btn = NodeData::create_button(
7372            SmallAriaInfo::label("Save").with_role(AccessibilityRole::PushButton),
7373        );
7374        let info = btn.get_accessibility_info().expect("a11y info");
7375        assert_eq!(info.role, AccessibilityRole::PushButton);
7376        assert_eq!(info.accessibility_name, OptionString::Some("Save".into()));
7377
7378        let table = NodeData::create_table(SmallAriaInfo::label("Results"));
7379        assert!(table.is_node_type(NodeType::Table));
7380        assert!(table.get_accessibility_info().is_some());
7381    }
7382
7383    #[test]
7384    fn a11y_constructors_accept_empty_aria_labels() {
7385        let btn = NodeData::create_button(SmallAriaInfo::label(""));
7386        let info = btn.get_accessibility_info().expect("a11y info");
7387        assert_eq!(info.accessibility_name, OptionString::Some("".into()));
7388        // An unset role degrades to Unknown rather than panicking.
7389        assert_eq!(info.role, AccessibilityRole::Unknown);
7390    }
7391
7392    #[test]
7393    fn create_image_and_is_node_type_round_trip() {
7394        let img = ImageRef::null_image(4, 4, crate::resources::RawImageFormat::RGBA8, Vec::new());
7395        let nd = NodeData::create_image(img.clone());
7396        assert!(!nd.is_text_node());
7397        assert_eq!(nd.get_node_type().get_path(), NodeTypeTag::Img);
7398        assert!(nd.is_node_type(NodeType::Image(BoxOrStatic::heap(img))));
7399    }
7400
7401    // =====================================================================
7402    // NodeData — predicates
7403    // =====================================================================
7404
7405    #[test]
7406    fn is_node_type_is_content_sensitive_for_text() {
7407        let nd = NodeData::create_text("a");
7408        assert!(nd.is_node_type(NodeType::Text(BoxOrStatic::heap(AzString::from("a")))));
7409        assert!(
7410            !nd.is_node_type(NodeType::Text(BoxOrStatic::heap(AzString::from("b")))),
7411            "is_node_type compares payloads, not just the discriminant"
7412        );
7413        assert!(!nd.is_node_type(NodeType::Div));
7414    }
7415
7416    #[test]
7417    fn is_text_node_and_is_virtual_view_node() {
7418        assert!(NodeData::create_text("x").is_text_node());
7419        assert!(!NodeData::create_div().is_text_node());
7420
7421        let vv = NodeData::create_virtual_view(RefAny::new(1u32), virtual_view_callback());
7422        assert!(vv.is_virtual_view_node());
7423        assert!(!vv.is_text_node());
7424        assert!(vv.get_virtual_view_node_ref().is_some());
7425        assert!(!NodeData::create_div().is_virtual_view_node());
7426        assert!(NodeData::create_div().get_virtual_view_node_ref().is_none());
7427    }
7428
7429    #[test]
7430    fn has_context_menu_flips_only_after_set_context_menu() {
7431        let mut nd = NodeData::create_div();
7432        assert!(!nd.has_context_menu());
7433        // A menu bar is a different slot and must not be mistaken for a context menu.
7434        nd.set_menu_bar(Menu::create(Vec::new().into()));
7435        assert!(
7436            !nd.has_context_menu(),
7437            "menu_bar must not satisfy has_context_menu"
7438        );
7439        assert!(nd.get_menu_bar().is_some());
7440
7441        nd.set_context_menu(Menu::create(Vec::new().into()));
7442        assert!(nd.has_context_menu());
7443        assert!(nd.get_context_menu().is_some());
7444    }
7445
7446    #[test]
7447    fn with_menu_bar_and_with_context_menu_are_independent_slots() {
7448        let nd = NodeData::create_div()
7449            .with_menu_bar(Menu::create(Vec::new().into()))
7450            .with_context_menu(Menu::create(Vec::new().into()));
7451        assert!(nd.get_menu_bar().is_some());
7452        assert!(nd.get_context_menu().is_some());
7453        assert!(nd.has_context_menu());
7454    }
7455
7456    #[test]
7457    fn is_focusable_for_naturally_focusable_and_opted_in_nodes() {
7458        for nt in [
7459            NodeType::A,
7460            NodeType::Button,
7461            NodeType::Input,
7462            NodeType::Select,
7463            NodeType::TextArea,
7464        ] {
7465            assert!(
7466                NodeData::create_node(nt.clone()).is_focusable(),
7467                "{nt:?} is naturally focusable"
7468            );
7469        }
7470        assert!(!NodeData::create_div().is_focusable());
7471        assert!(NodeData::create_div()
7472            .with_contenteditable(true)
7473            .is_focusable());
7474        assert!(NodeData::create_div()
7475            .with_tab_index(TabIndex::NoKeyboardFocus)
7476            .is_focusable());
7477        assert!(NodeData::create_div()
7478            .with_callback(
7479                EventFilter::Focus(FocusEventFilter::MouseDown),
7480                RefAny::new(0u32),
7481                0usize,
7482            )
7483            .is_focusable());
7484        // A non-focus callback must NOT make a plain div focusable.
7485        assert!(!NodeData::create_div()
7486            .with_callback(
7487                EventFilter::Hover(HoverEventFilter::MouseOver),
7488                RefAny::new(0u32),
7489                0usize,
7490            )
7491            .is_focusable());
7492    }
7493
7494    #[test]
7495    fn has_activation_behavior_for_elements_callbacks_and_roles() {
7496        assert!(NodeData::create_node(NodeType::A).has_activation_behavior());
7497        assert!(NodeData::create_button_no_a11y().has_activation_behavior());
7498        assert!(!NodeData::create_div().has_activation_behavior());
7499
7500        for f in [HoverEventFilter::MouseUp, HoverEventFilter::LeftMouseUp] {
7501            assert!(NodeData::create_div()
7502                .with_callback(EventFilter::Hover(f), RefAny::new(0u32), 0usize)
7503                .has_activation_behavior());
7504        }
7505        // MouseDown is not a click.
7506        assert!(!NodeData::create_div()
7507            .with_callback(
7508                EventFilter::Hover(HoverEventFilter::MouseDown),
7509                RefAny::new(0u32),
7510                0usize,
7511            )
7512            .has_activation_behavior());
7513
7514        let mut nd = NodeData::create_div();
7515        nd.set_accessibility_info(
7516            SmallAriaInfo::label("x")
7517                .with_role(AccessibilityRole::PushButton)
7518                .to_full_info(),
7519        );
7520        assert!(nd.has_activation_behavior(), "role=PushButton activates");
7521    }
7522
7523    #[test]
7524    fn is_activatable_is_false_for_unavailable_elements() {
7525        let mut nd = NodeData::create_button_no_a11y();
7526        assert!(nd.is_activatable());
7527
7528        let mut info = SmallAriaInfo::label("Save")
7529            .with_role(AccessibilityRole::PushButton)
7530            .to_full_info();
7531        info.states = vec![AccessibilityState::Unavailable].into();
7532        nd.set_accessibility_info(info);
7533
7534        assert!(nd.has_activation_behavior());
7535        assert!(
7536            !nd.is_activatable(),
7537            "an Unavailable (disabled) button must not be activatable"
7538        );
7539
7540        // Something with no activation behaviour at all is never activatable.
7541        assert!(!NodeData::create_div().is_activatable());
7542    }
7543
7544    // =====================================================================
7545    // NodeData — accessible label / value / placeholder
7546    // =====================================================================
7547
7548    #[test]
7549    fn get_accessible_label_prefers_aria_label_over_alt_and_title() {
7550        let mut nd = NodeData::create_div();
7551        nd.set_attributes(
7552            vec![
7553                AttributeType::Title("title".into()),
7554                AttributeType::Alt("alt".into()),
7555                AttributeType::AriaLabel("aria".into()),
7556            ]
7557            .into(),
7558        );
7559        assert_eq!(
7560            nd.get_accessible_label(),
7561            Some("aria"),
7562            "aria-label wins regardless of attribute order"
7563        );
7564    }
7565
7566    #[test]
7567    fn get_accessible_label_alt_vs_title_is_order_dependent() {
7568        // AUDIT: the doc comment promises `aria-label > alt > title`, but the
7569        // implementation's second pass matches `Alt(s) | Title(s)` in a single arm,
7570        // so whichever appears FIRST in the attribute vec wins. With [Title, Alt]
7571        // that yields "title" — contradicting the documented priority. Pinned here
7572        // so a fix has to update this test deliberately. See report.
7573        let mut title_first = NodeData::create_div();
7574        title_first.set_attributes(
7575            vec![
7576                AttributeType::Title("title".into()),
7577                AttributeType::Alt("alt".into()),
7578            ]
7579            .into(),
7580        );
7581        assert_eq!(title_first.get_accessible_label(), Some("title"));
7582
7583        let mut alt_first = NodeData::create_div();
7584        alt_first.set_attributes(
7585            vec![
7586                AttributeType::Alt("alt".into()),
7587                AttributeType::Title("title".into()),
7588            ]
7589            .into(),
7590        );
7591        assert_eq!(alt_first.get_accessible_label(), Some("alt"));
7592    }
7593
7594    #[test]
7595    fn get_accessible_label_value_and_placeholder_default_to_none() {
7596        let nd = NodeData::create_div();
7597        assert_eq!(nd.get_accessible_label(), None);
7598        assert_eq!(nd.get_accessible_value(), None);
7599        assert_eq!(nd.get_placeholder(), None);
7600    }
7601
7602    #[test]
7603    fn get_accessible_value_and_placeholder_return_the_first_match() {
7604        let mut nd = NodeData::create_div();
7605        nd.set_attributes(
7606            vec![
7607                AttributeType::Value("first".into()),
7608                AttributeType::Value("second".into()),
7609                AttributeType::Placeholder("ph".into()),
7610            ]
7611            .into(),
7612        );
7613        assert_eq!(nd.get_accessible_value(), Some("first"));
7614        assert_eq!(nd.get_placeholder(), Some("ph"));
7615    }
7616
7617    #[test]
7618    fn get_accessible_label_returns_empty_string_not_none_for_empty_aria_label() {
7619        // Boundary: an empty aria-label is still "present" — Some("") not None.
7620        let mut nd = NodeData::create_div();
7621        nd.set_attributes(vec![AttributeType::AriaLabel("".into())].into());
7622        assert_eq!(nd.get_accessible_label(), Some(""));
7623    }
7624
7625    // =====================================================================
7626    // NodeData — dataset / key / merge callback / component origin
7627    // =====================================================================
7628
7629    #[test]
7630    fn dataset_set_get_take_round_trip() {
7631        let mut nd = NodeData::create_div();
7632        assert!(nd.get_dataset().is_none());
7633        assert!(nd.take_dataset().is_none(), "take on empty must be None");
7634
7635        nd.set_dataset(OptionRefAny::Some(RefAny::new(42u32)));
7636        assert!(nd.get_dataset().is_some());
7637        assert!(nd.get_dataset_mut().is_some());
7638
7639        let mut taken = nd.take_dataset().expect("dataset was set");
7640        assert_eq!(taken.downcast_ref::<u32>().map(|r| *r), Some(42));
7641        assert!(nd.get_dataset().is_none(), "take must clear the slot");
7642        assert!(nd.take_dataset().is_none(), "double-take must be None");
7643    }
7644
7645    #[test]
7646    fn set_dataset_none_clears_without_allocating_extra() {
7647        let mut nd = NodeData::create_div();
7648        // Setting None on a node that never had a dataset must be a no-op, not a panic.
7649        nd.set_dataset(OptionRefAny::None);
7650        assert!(nd.get_dataset().is_none());
7651
7652        nd.set_dataset(OptionRefAny::Some(RefAny::new(1u8)));
7653        nd.set_dataset(OptionRefAny::None);
7654        assert!(nd.get_dataset().is_none());
7655    }
7656
7657    #[test]
7658    fn set_key_is_deterministic_and_input_sensitive() {
7659        let mut a = NodeData::create_div();
7660        let mut b = NodeData::create_div();
7661        a.set_key("user-123");
7662        b.set_key("user-123");
7663        assert_eq!(a.get_key(), b.get_key(), "same key input => same hash");
7664        assert!(a.get_key().is_some());
7665
7666        let mut c = NodeData::create_div();
7667        c.set_key("user-124");
7668        assert_ne!(a.get_key(), c.get_key(), "different inputs => different keys");
7669    }
7670
7671    #[test]
7672    fn set_key_hashes_str_and_string_identically() {
7673        let mut a = NodeData::create_div();
7674        let mut b = NodeData::create_div();
7675        a.set_key("x");
7676        b.set_key(String::from("x"));
7677        assert_eq!(
7678            a.get_key(),
7679            b.get_key(),
7680            "&str and String must hash the same (Hash for str)"
7681        );
7682    }
7683
7684    #[test]
7685    fn set_key_accepts_extreme_inputs() {
7686        for nd in [
7687            NodeData::create_div().with_key(""),
7688            NodeData::create_div().with_key(u64::MAX),
7689            NodeData::create_div().with_key(i64::MIN),
7690            NodeData::create_div().with_key(huge_unicode_string()),
7691        ] {
7692            assert!(nd.get_key().is_some());
7693        }
7694    }
7695
7696    #[test]
7697    fn set_key_overwrites_rather_than_accumulating() {
7698        let mut nd = NodeData::create_div();
7699        nd.set_key("a");
7700        let first = nd.get_key();
7701        nd.set_key("b");
7702        assert_ne!(nd.get_key(), first, "the last set_key wins");
7703    }
7704
7705    #[test]
7706    fn merge_callback_round_trips_the_function_pointer() {
7707        let mut nd = NodeData::create_div();
7708        assert!(nd.get_merge_callback().is_none());
7709
7710        nd.set_merge_callback(merge_cb_a as DatasetMergeCallbackType);
7711        let cb = nd.get_merge_callback().expect("merge callback was set");
7712        assert_eq!(cb.cb as usize, merge_cb_a as usize);
7713        assert_eq!(cb.callable, OptionRefAny::None);
7714
7715        // Overwriting swaps the pointer.
7716        nd.set_merge_callback(merge_cb_b as DatasetMergeCallbackType);
7717        let cb = nd.get_merge_callback().expect("merge callback was replaced");
7718        assert_eq!(cb.cb as usize, merge_cb_b as usize);
7719    }
7720
7721    #[test]
7722    fn dataset_merge_callback_from_ptr_matches_the_from_impl() {
7723        let via_ptr = DatasetMergeCallback::from_ptr(merge_cb_a);
7724        let via_from = DatasetMergeCallback::from(merge_cb_a as DatasetMergeCallbackType);
7725        assert_eq!(via_ptr, via_from);
7726        assert_eq!(via_ptr.cb as usize, merge_cb_a as usize);
7727        assert_eq!(via_ptr.callable, OptionRefAny::None);
7728
7729        // Distinct functions must not compare equal.
7730        assert_ne!(via_ptr, DatasetMergeCallback::from_ptr(merge_cb_b));
7731    }
7732
7733    #[test]
7734    fn dataset_merge_callback_debug_is_non_empty_and_names_the_type() {
7735        let cb = DatasetMergeCallback::from_ptr(merge_cb_a);
7736        let s = format!("{cb:?}");
7737        assert!(s.contains("DatasetMergeCallback"));
7738        assert!(s.contains("cb"));
7739    }
7740
7741    #[test]
7742    fn merge_callback_is_actually_callable_through_the_stored_pointer() {
7743        let cb = DatasetMergeCallback::from_ptr(merge_cb_b);
7744        let mut out = (cb.cb)(RefAny::new(1u32), RefAny::new(2u32));
7745        assert_eq!(
7746            out.downcast_ref::<u32>().map(|r| *r),
7747            Some(2),
7748            "merge_cb_b returns the OLD data"
7749        );
7750    }
7751
7752    #[test]
7753    fn component_origin_round_trips_and_defaults_to_none() {
7754        let mut nd = NodeData::create_div();
7755        assert!(nd.get_component_origin().is_none());
7756
7757        nd.set_component_origin(ComponentOrigin {
7758            component_id: "shadcn:card".into(),
7759            data_model_json: crate::json::Json::null(),
7760        });
7761        let origin = nd.get_component_origin().expect("origin was set");
7762        assert_eq!(origin.component_id.as_str(), "shadcn:card");
7763
7764        // The Default impl is well-formed and hashable.
7765        let d = ComponentOrigin::default();
7766        assert_eq!(d.component_id.as_str(), "");
7767        assert_eq!(hash_of(&d), hash_of(&ComponentOrigin::default()));
7768    }
7769
7770    // =====================================================================
7771    // NodeData — svg data / clip mask
7772    // =====================================================================
7773
7774    #[test]
7775    fn get_image_clip_mask_returns_none_for_non_mask_svg_data() {
7776        let mut nd = NodeData::create_div();
7777        assert!(nd.get_image_clip_mask().is_none());
7778
7779        nd.set_svg_data(SvgNodeData::Circle {
7780            cx: 1.0,
7781            cy: 2.0,
7782            r: 3.0,
7783        });
7784        assert!(nd.get_svg_data().is_some());
7785        assert!(
7786            nd.get_image_clip_mask().is_none(),
7787            "a Circle is not an ImageClipMask"
7788        );
7789    }
7790
7791    #[test]
7792    fn set_clip_mask_is_readable_through_get_image_clip_mask() {
7793        let mask = ImageMask {
7794            image: ImageRef::null_image(2, 2, crate::resources::RawImageFormat::R8, Vec::new()),
7795            rect: crate::geom::LogicalRect::new(
7796                LogicalPosition { x: 0.0, y: 0.0 },
7797                crate::geom::LogicalSize {
7798                    width: 2.0,
7799                    height: 2.0,
7800                },
7801            ),
7802            repeat: false,
7803        };
7804        let mut nd = NodeData::create_div();
7805        nd.set_clip_mask(mask.clone());
7806        assert_eq!(nd.get_image_clip_mask(), Some(&mask));
7807        // set_clip_mask stores through the same slot as set_svg_data.
7808        assert!(matches!(
7809            nd.get_svg_data(),
7810            Some(SvgNodeData::ImageClipMask(_))
7811        ));
7812    }
7813
7814    #[test]
7815    fn svg_node_data_with_nan_coords_is_self_equal_and_hash_consistent() {
7816        // SvgNodeData hashes f32 via to_bits and derives Eq, so a NaN-carrying shape
7817        // must be equal to (and hash like) itself, or NodeData's Hash/Eq contract
7818        // breaks for SVG nodes.
7819        let a = SvgNodeData::Rect {
7820            x: f32::NAN,
7821            y: f32::INFINITY,
7822            width: f32::NEG_INFINITY,
7823            height: -0.0,
7824            rx: 0.0,
7825            ry: f32::MAX,
7826        };
7827        let b = a.clone();
7828        assert_eq!(a, b);
7829        assert_eq!(hash_of(&a), hash_of(&b));
7830        assert_eq!(a.cmp(&b), core::cmp::Ordering::Equal);
7831    }
7832
7833    #[test]
7834    fn svg_node_data_line_and_linear_gradient_are_distinct_despite_a_shared_hash_body() {
7835        // The Hash impl deliberately folds Line and LinearGradient into one arm, so
7836        // they can hash alike — but Eq must still tell them apart.
7837        let line = SvgNodeData::Line {
7838            x1: 1.0,
7839            y1: 2.0,
7840            x2: 3.0,
7841            y2: 4.0,
7842        };
7843        let grad = SvgNodeData::LinearGradient {
7844            x1: 1.0,
7845            y1: 2.0,
7846            x2: 3.0,
7847            y2: 4.0,
7848        };
7849        assert_ne!(line, grad, "same field values, different variants");
7850    }
7851
7852    // =====================================================================
7853    // NodeData — hashing
7854    // =====================================================================
7855
7856    #[test]
7857    fn calculate_node_data_hash_is_deterministic_and_equal_for_equal_nodes() {
7858        let a = NodeData::create_div().with_key("k").with_contenteditable(true);
7859        let b = a.clone();
7860        assert_eq!(a, b);
7861        assert_eq!(a.calculate_node_data_hash(), b.calculate_node_data_hash());
7862        assert_eq!(
7863            a.calculate_node_data_hash(),
7864            a.calculate_node_data_hash(),
7865            "hashing must not depend on call count"
7866        );
7867    }
7868
7869    #[test]
7870    fn structural_hash_ignores_text_content_but_data_hash_does_not() {
7871        // Documented behaviour: Text("Hello") must match Text("Hello World") during
7872        // reconciliation so the cursor survives an edit.
7873        let a = NodeData::create_text("Hello");
7874        let b = NodeData::create_text("Hello World");
7875
7876        assert_eq!(
7877            a.calculate_structural_hash(),
7878            b.calculate_structural_hash(),
7879            "structural hash must ignore text content"
7880        );
7881        assert_ne!(
7882            a.calculate_node_data_hash(),
7883            b.calculate_node_data_hash(),
7884            "the full data hash must NOT ignore text content"
7885        );
7886    }
7887
7888    #[test]
7889    fn structural_hash_ignores_contenteditable_but_data_hash_does_not() {
7890        let plain = NodeData::create_div();
7891        let editable = NodeData::create_div().with_contenteditable(true);
7892
7893        assert_eq!(
7894            plain.calculate_structural_hash(),
7895            editable.calculate_structural_hash(),
7896            "contenteditable flips with focus; it must not move the structural hash"
7897        );
7898        assert_ne!(
7899            plain.calculate_node_data_hash(),
7900            editable.calculate_node_data_hash(),
7901            "flags ARE part of the full data hash"
7902        );
7903    }
7904
7905    #[test]
7906    fn structural_hash_is_sensitive_to_ids_classes_and_node_type() {
7907        let mut a = NodeData::create_div();
7908        a.add_id("a".into());
7909        let mut b = NodeData::create_div();
7910        b.add_id("b".into());
7911        assert_ne!(a.calculate_structural_hash(), b.calculate_structural_hash());
7912
7913        let mut c = NodeData::create_div();
7914        c.add_class("a".into());
7915        assert_ne!(
7916            a.calculate_structural_hash(),
7917            c.calculate_structural_hash(),
7918            "id=\"a\" and class=\"a\" must not collide"
7919        );
7920
7921        assert_ne!(
7922            NodeData::create_div().calculate_structural_hash(),
7923            NodeData::create_br().calculate_structural_hash()
7924        );
7925    }
7926
7927    #[test]
7928    fn node_data_eq_implies_equal_hash_for_a_richly_populated_node() {
7929        let mut a = NodeData::create_div();
7930        a.add_id("id".into());
7931        a.add_class("cls".into());
7932        a.set_tab_index(TabIndex::OverrideInParent(9));
7933        a.set_contenteditable(true);
7934        a.set_anonymous(true);
7935        a.set_key("key");
7936        a.set_dataset(OptionRefAny::Some(RefAny::new(7u64)));
7937        a.set_svg_data(SvgNodeData::GradientStop { offset: 0.5 });
7938        a.set_context_menu(Menu::create(Vec::new().into()));
7939        a.set_merge_callback(merge_cb_a as DatasetMergeCallbackType);
7940        a.set_css("color: red;");
7941
7942        let b = a.clone();
7943        assert_eq!(a, b, "clone must be value-equal");
7944        assert_eq!(
7945            hash_of(&a),
7946            hash_of(&b),
7947            "Eq == true but hashes differ: Hash/Eq contract violated"
7948        );
7949        assert_eq!(a.calculate_node_data_hash(), b.calculate_node_data_hash());
7950        // copy_special must agree with Clone.
7951        assert_eq!(a.copy_special(), b);
7952    }
7953
7954    // =====================================================================
7955    // NodeData — Display / node_data_to_string (serializer)
7956    // =====================================================================
7957
7958    #[test]
7959    fn node_data_to_string_is_empty_for_a_bare_node() {
7960        // Private fn — only reachable from an inline test module.
7961        assert_eq!(node_data_to_string(&NodeData::create_div()), "");
7962    }
7963
7964    #[test]
7965    fn node_data_to_string_emits_ids_classes_and_tabindex() {
7966        let mut nd = NodeData::create_div();
7967        nd.add_id("i1".into());
7968        nd.add_id("i2".into());
7969        nd.add_class("c1".into());
7970        nd.set_tab_index(TabIndex::NoKeyboardFocus);
7971
7972        let s = node_data_to_string(&nd);
7973        assert!(s.contains(r#"id="i1 i2""#), "ids are space-joined: {s}");
7974        assert!(s.contains(r#"class="c1""#), "{s}");
7975        assert!(s.contains(r#"tabindex="-1""#), "{s}");
7976    }
7977
7978    #[test]
7979    fn node_data_display_is_self_closing_without_content() {
7980        let s = format!("{}", NodeData::create_div());
7981        assert!(s.starts_with('<'), "{s}");
7982        assert!(s.ends_with("/>"), "content-less nodes self-close: {s}");
7983    }
7984
7985    #[test]
7986    fn node_data_display_wraps_text_content_in_a_tag_pair() {
7987        let s = format!("{}", NodeData::create_text("hello"));
7988        assert!(s.starts_with('<'));
7989        assert!(s.ends_with('>'));
7990        assert!(s.contains("hello"), "{s}");
7991        assert!(!s.ends_with("/>"), "a node with content must not self-close");
7992    }
7993
7994    #[test]
7995    fn node_data_display_does_not_panic_on_hostile_text() {
7996        // NOTE: Display is a debug/inspection aid and does NOT escape markup — a text
7997        // node containing `<script>` reproduces it verbatim. Assert only that it is
7998        // total (no panic) and round-trips the bytes; see report.
7999        for text in [
8000            "",
8001            "<script>alert(1)</script>",
8002            "\" onload=\"x",
8003            "日本語 🎉",
8004            "line\nbreak\ttab",
8005        ] {
8006            let s = format!("{}", NodeData::create_text(text));
8007            assert!(s.contains(text), "Display dropped content for {text:?}");
8008        }
8009    }
8010
8011    #[test]
8012    fn node_data_display_survives_a_huge_text_payload() {
8013        let big = huge_unicode_string();
8014        let s = format!("{}", NodeData::create_text(big.clone()));
8015        assert!(s.len() > big.len());
8016    }
8017
8018    #[test]
8019    fn debug_print_end_matches_the_node_tag() {
8020        let s = NodeData::create_div().debug_print_end();
8021        assert!(s.starts_with("</"));
8022        assert!(s.ends_with('>'));
8023    }
8024
8025    // =====================================================================
8026    // NodeData — setters / builders / swap
8027    // =====================================================================
8028
8029    #[test]
8030    fn set_node_type_replaces_the_type_and_keeps_the_attributes() {
8031        let mut nd = NodeData::create_div();
8032        nd.add_id("keep".into());
8033        nd.set_node_type(NodeType::Span);
8034        assert!(nd.is_node_type(NodeType::Span));
8035        assert!(nd.has_id("keep"), "changing the tag must not drop attributes");
8036    }
8037
8038    #[test]
8039    fn add_callback_appends_and_get_callbacks_reflects_it() {
8040        let mut nd = NodeData::create_div();
8041        assert!(nd.get_callbacks().as_ref().is_empty());
8042
8043        nd.add_callback(
8044            EventFilter::Hover(HoverEventFilter::MouseUp),
8045            RefAny::new(1u32),
8046            0usize,
8047        );
8048        nd.add_callback(
8049            EventFilter::Focus(FocusEventFilter::MouseDown),
8050            RefAny::new(2u32),
8051            1usize,
8052        );
8053        assert_eq!(nd.get_callbacks().as_ref().len(), 2);
8054        assert_eq!(
8055            nd.get_callbacks().as_ref()[0].event,
8056            EventFilter::Hover(HoverEventFilter::MouseUp)
8057        );
8058    }
8059
8060    #[test]
8061    fn add_css_property_appends_an_inline_rule() {
8062        use azul_css::props::property::{CssProperty, CssPropertyType};
8063
8064        let mut nd = NodeData::create_div();
8065        assert!(nd.get_style().rules.as_ref().is_empty());
8066
8067        nd.add_css_property(CssPropertyWithConditions {
8068            property: CssProperty::const_none(CssPropertyType::Display),
8069            apply_if: Vec::new().into(),
8070        });
8071        assert_eq!(nd.get_style().rules.as_ref().len(), 1);
8072
8073        nd.add_css_property(CssPropertyWithConditions {
8074            property: CssProperty::const_none(CssPropertyType::Display),
8075            apply_if: Vec::new().into(),
8076        });
8077        assert_eq!(
8078            nd.get_style().rules.as_ref().len(),
8079            2,
8080            "add_css_property appends, it does not replace"
8081        );
8082    }
8083
8084    #[test]
8085    fn set_style_replaces_whereas_set_css_appends() {
8086        let mut nd = NodeData::create_div();
8087        nd.set_css("color: red;");
8088        let after_first = nd.get_style().rules.as_ref().len();
8089        assert!(after_first > 0);
8090
8091        nd.set_css("color: blue;");
8092        assert!(
8093            nd.get_style().rules.as_ref().len() > after_first,
8094            "set_css appends to the existing inline style"
8095        );
8096
8097        nd.set_style(azul_css::css::Css {
8098            rules: Vec::new().into(),
8099        });
8100        assert!(
8101            nd.get_style().rules.as_ref().is_empty(),
8102            "set_style replaces wholesale"
8103        );
8104    }
8105
8106    #[test]
8107    fn set_css_with_empty_and_malformed_input_does_not_panic() {
8108        for style in [
8109            "",
8110            "   ",
8111            ";;;;",
8112            "color",
8113            "color:",
8114            ":",
8115            "}",
8116            "{",
8117            "color: ;",
8118            "not-a-property: not-a-value;",
8119            ":hover {",
8120            "@os {",
8121            "color: red",           // no trailing semicolon
8122            "\u{0}color: red;",     // NUL byte
8123            "color: 日本語;",
8124        ] {
8125            let nd = NodeData::create_div().with_css(style);
8126            // The only contract for malformed input is "don't panic"; whether a rule
8127            // survives parsing is the CSS parser's business.
8128            let _ = nd.get_style().rules.as_ref().len();
8129        }
8130    }
8131
8132    #[test]
8133    fn swap_with_default_returns_the_original_and_leaves_a_div() {
8134        let mut nd = NodeData::create_text("payload");
8135        let taken = nd.swap_with_default();
8136        assert!(taken.is_text_node());
8137        assert!(nd.is_node_type(NodeType::Div), "the slot becomes a fresh div");
8138        assert!(nd.attributes().as_ref().is_empty());
8139    }
8140
8141    #[test]
8142    fn node_data_builders_are_equivalent_to_their_setters() {
8143        let built = NodeData::create_div()
8144            .with_tab_index(TabIndex::Auto)
8145            .with_contenteditable(true)
8146            .with_node_type(NodeType::Span);
8147
8148        let mut set = NodeData::create_div();
8149        set.set_tab_index(TabIndex::Auto);
8150        set.set_contenteditable(true);
8151        set.set_node_type(NodeType::Span);
8152
8153        assert_eq!(built, set);
8154    }
8155
8156    // =====================================================================
8157    // NodeDataVec containers
8158    // =====================================================================
8159
8160    #[test]
8161    fn node_data_vec_as_container_is_empty_for_an_empty_vec() {
8162        let v: NodeDataVec = Vec::new().into();
8163        assert_eq!(v.as_container().internal.len(), 0);
8164    }
8165
8166    #[test]
8167    fn node_data_vec_containers_expose_and_mutate_the_backing_slice() {
8168        let mut v: NodeDataVec = vec![
8169            NodeData::create_div(),
8170            NodeData::create_br(),
8171            NodeData::create_text("t"),
8172        ]
8173        .into();
8174        assert_eq!(v.as_container().internal.len(), 3);
8175        assert!(v.as_container().internal[2].is_text_node());
8176
8177        v.as_container_mut().internal[0].set_node_type(NodeType::Span);
8178        assert!(v.as_container().internal[0].is_node_type(NodeType::Span));
8179    }
8180
8181    // =====================================================================
8182    // Dom — child bookkeeping
8183    // =====================================================================
8184
8185    #[test]
8186    fn dom_default_is_an_empty_body() {
8187        let d = Dom::default();
8188        assert!(d.root.is_node_type(NodeType::Body));
8189        assert_eq!(d.estimated_total_children, 0);
8190        assert_eq!(d.node_count(), 1);
8191    }
8192
8193    #[test]
8194    fn dom_set_children_recomputes_the_estimate_from_scratch() {
8195        let child = Dom::create_div().with_child(Dom::create_div());
8196        let mut parent = Dom::create_div();
8197        parent.add_child(Dom::create_div());
8198        assert_eq!(parent.estimated_total_children, 1);
8199
8200        // set_children REPLACES; the old child must not be counted twice.
8201        parent.set_children(vec![child].into());
8202        assert_eq!(parent.estimated_total_children, 2);
8203        assert_eq!(
8204            parent.estimated_total_children,
8205            parent.recompute_estimated_total_children()
8206        );
8207    }
8208
8209    #[test]
8210    fn dom_set_children_with_an_empty_vec_zeroes_the_estimate() {
8211        let mut d = Dom::create_div().with_child(Dom::create_div().with_child(Dom::create_div()));
8212        assert_eq!(d.estimated_total_children, 2);
8213        d.set_children(Vec::new().into());
8214        assert_eq!(d.estimated_total_children, 0);
8215        assert_eq!(d.node_count(), 1);
8216    }
8217
8218    #[test]
8219    fn dom_deeply_nested_chain_keeps_an_exact_estimate() {
8220        // 256-deep chain: every level adds exactly one descendant.
8221        const DEPTH: usize = 256;
8222        let mut d = Dom::create_div();
8223        for _ in 0..DEPTH {
8224            d = Dom::create_div().with_child(d);
8225        }
8226        assert_eq!(d.estimated_total_children, DEPTH);
8227        assert_eq!(d.node_count(), DEPTH + 1);
8228        assert_eq!(d.recompute_estimated_total_children(), DEPTH);
8229    }
8230
8231    #[test]
8232    fn dom_very_wide_child_list_keeps_an_exact_estimate() {
8233        const WIDTH: usize = 5_000;
8234        let children: Vec<Dom> = (0..WIDTH).map(|_| Dom::create_div()).collect();
8235        let d = Dom::create_div().with_children(children.into());
8236        assert_eq!(d.estimated_total_children, WIDTH);
8237        assert_eq!(d.node_count(), WIDTH + 1);
8238    }
8239
8240    #[test]
8241    fn dom_from_iterator_counts_nested_grandchildren() {
8242        let empty: Dom = Vec::new().into_iter().collect();
8243        assert_eq!(empty.estimated_total_children, 0);
8244        assert!(empty.root.is_node_type(NodeType::Div));
8245
8246        // Two children, one of which has a child of its own => 3 descendants.
8247        let d: Dom = vec![
8248            Dom::create_div().with_child(Dom::create_div()),
8249            Dom::create_div(),
8250        ]
8251        .into_iter()
8252        .collect();
8253        assert_eq!(d.estimated_total_children, 3);
8254        assert_eq!(d.estimated_total_children, d.recompute_estimated_total_children());
8255        assert_eq!(d.node_count(), 4);
8256    }
8257
8258    #[test]
8259    fn dom_fixup_repairs_a_corrupted_estimate_at_every_depth() {
8260        let mut d = Dom::create_div()
8261            .with_child(Dom::create_div().with_child(Dom::create_div()))
8262            .with_child(Dom::create_div());
8263
8264        // Corrupt the cached counter at BOTH levels (the public field makes this
8265        // reachable from safe code, which is what fixup exists to undo).
8266        d.estimated_total_children = 0;
8267        d.children.as_mut()[0].estimated_total_children = 99;
8268
8269        let repaired = d.fixup_children_estimated();
8270        assert_eq!(repaired, 3);
8271        assert_eq!(d.children.as_ref()[0].estimated_total_children, 1);
8272        assert_eq!(
8273            d.estimated_total_children,
8274            d.recompute_estimated_total_children()
8275        );
8276    }
8277
8278    #[test]
8279    fn dom_fixup_on_a_leaf_zeroes_a_bogus_estimate() {
8280        let mut d = Dom::create_div();
8281        d.estimated_total_children = usize::MAX;
8282        assert_eq!(d.fixup_children_estimated(), 0);
8283        assert_eq!(d.node_count(), 1, "node_count is safe again after fixup");
8284    }
8285
8286    // `node_count()` is `estimated_total_children + 1` with no checked add. Because
8287    // `estimated_total_children` is a public field, a corrupted usize::MAX makes it
8288    // overflow — a debug-build panic (and a silent wrap to 0 in release). Only
8289    // meaningful when overflow checks are on.
8290    #[cfg(debug_assertions)]
8291    #[test]
8292    #[should_panic(expected = "overflow")]
8293    fn dom_node_count_overflows_on_a_corrupted_max_estimate() {
8294        let mut d = Dom::create_div();
8295        d.estimated_total_children = usize::MAX;
8296        let _ = d.node_count();
8297    }
8298
8299    #[test]
8300    fn dom_swap_with_default_returns_the_original_tree() {
8301        let mut d = Dom::create_div().with_child(Dom::create_div());
8302        let taken = d.swap_with_default();
8303        assert_eq!(taken.estimated_total_children, 1);
8304        assert_eq!(d.estimated_total_children, 0, "the slot is reset");
8305        assert!(d.root.is_node_type(NodeType::Div));
8306    }
8307
8308    // =====================================================================
8309    // Dom — builders
8310    // =====================================================================
8311
8312    #[test]
8313    fn dom_with_id_and_with_class_apply_to_the_root() {
8314        let d = Dom::create_div()
8315            .with_id("root".into())
8316            .with_class("card".into());
8317        assert!(d.root.has_id("root"));
8318        assert!(d.root.has_class("card"));
8319    }
8320
8321    #[test]
8322    fn dom_with_attribute_appends_and_with_attributes_replaces() {
8323        let d = Dom::create_div()
8324            .with_attribute(AttributeType::Href("/a".into()))
8325            .with_attribute(AttributeType::Alt("alt".into()));
8326        assert_eq!(d.root.attributes().as_ref().len(), 2);
8327
8328        let d = d.with_attributes(vec![AttributeType::Disabled].into());
8329        assert_eq!(
8330            d.root.attributes().as_ref().len(),
8331            1,
8332            "with_attributes replaces wholesale"
8333        );
8334        assert_eq!(d.root.attributes().as_ref()[0], AttributeType::Disabled);
8335    }
8336
8337    #[test]
8338    fn dom_add_component_css_stacks_stylesheets() {
8339        let mut d = Dom::create_div();
8340        assert!(d.css.as_ref().is_empty());
8341        d.set_css("color: red;");
8342        d.set_css("color: blue;");
8343        assert_eq!(d.css.as_ref().len(), 2, "each set_css pushes a stylesheet");
8344
8345        d.set_component_css(Vec::new().into());
8346        assert!(d.css.as_ref().is_empty(), "set_component_css replaces");
8347    }
8348
8349    #[test]
8350    fn dom_with_css_does_not_panic_on_malformed_input() {
8351        for style in ["", "}}}", "@os {", "color:", "\u{0}"] {
8352            let d = Dom::create_div().with_css(style);
8353            assert_eq!(d.css.as_ref().len(), 1, "a Css is pushed even if it parses empty");
8354        }
8355    }
8356
8357    #[test]
8358    fn dom_text_helpers_produce_a_text_child() {
8359        let d = Dom::create_h1_with_text("Title");
8360        assert!(d.root.is_node_type(NodeType::H1));
8361        assert_eq!(d.estimated_total_children, 1);
8362        assert!(d.children.as_ref()[0].root.is_text_node());
8363    }
8364
8365    #[test]
8366    fn dom_create_geolocation_probe_carries_its_config() {
8367        let cfg = crate::geolocation::GeolocationProbeConfig {
8368            high_accuracy: true,
8369            background: false,
8370            max_accuracy_m: 25.0,
8371            min_interval_ms: 1_000,
8372        };
8373        let d = Dom::create_geolocation_probe(cfg);
8374        match d.root.get_node_type() {
8375            NodeType::GeolocationProbe(c) => {
8376                assert!(c.high_accuracy);
8377                assert_eq!(c.min_interval_ms, 1_000);
8378            }
8379            other => panic!("expected GeolocationProbe, got {other:?}"),
8380        }
8381    }
8382
8383    #[test]
8384    fn dom_clone_and_eq_agree_on_a_nested_tree() {
8385        let d = Dom::create_div()
8386            .with_id("r".into())
8387            .with_child(Dom::create_text("a"))
8388            .with_child(Dom::create_div().with_child(Dom::create_text("b")));
8389        let c = d.clone();
8390        assert_eq!(d, c);
8391        assert_eq!(hash_of(&d), hash_of(&c));
8392        // text("a") + div + text("b") == 3 descendants.
8393        assert_eq!(c.estimated_total_children, 3);
8394        assert_eq!(c.node_count(), 4);
8395    }
8396
8397    #[test]
8398    fn dom_debug_does_not_panic_on_a_nested_tree() {
8399        let d = Dom::create_div()
8400            .with_child(Dom::create_text("日本語 🎉"))
8401            .with_child(Dom::create_div().with_child(Dom::create_br()));
8402        let s = format!("{d:?}");
8403        assert!(s.contains("Dom"));
8404        assert!(s.contains("estimated_total_children"));
8405    }
8406
8407    // =====================================================================
8408    // DomId / DomNodeId
8409    // =====================================================================
8410
8411    #[test]
8412    fn dom_id_root_is_zero_and_is_the_default() {
8413        assert_eq!(DomId::ROOT_ID.inner, 0);
8414        assert_eq!(DomId::default(), DomId::ROOT_ID);
8415        assert_eq!(format!("{}", DomId::ROOT_ID), "0");
8416        assert_eq!(format!("{}", DomId { inner: usize::MAX }), usize::MAX.to_string());
8417    }
8418
8419    #[test]
8420    fn dom_node_id_root_points_at_the_root_dom_and_no_node() {
8421        assert_eq!(DomNodeId::ROOT.dom, DomId::ROOT_ID);
8422        assert_eq!(DomNodeId::ROOT.node, NodeHierarchyItemId::NONE);
8423    }
8424}