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