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