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