1use azul_core::{
5 dom::{NodeId, NodeType},
6 geom::LogicalSize,
7 id::NodeId as CoreNodeId,
8 styled_dom::{StyledDom, StyledNodeState},
9};
10use azul_css::{
11 css::CssPropertyValue,
12 props::{
13 basic::{
14 font::{StyleFontFamily, StyleFontFamilyVec, StyleFontStyle, StyleFontWeight},
15 pixel::{DEFAULT_FONT_SIZE, PT_TO_PX},
16 ColorU, PhysicalSize, PixelValue, PropertyContext, ResolutionContext,
17 },
18 layout::{
19 grid::GridTemplateAreas, BoxDecorationBreak, BreakInside, LayoutAlignContent,
20 LayoutAlignItems, LayoutBoxSizing, LayoutClear, LayoutDisplay, LayoutFlexDirection,
21 LayoutFlexWrap, LayoutFloat, LayoutHeight, LayoutJustifyContent, LayoutOverflow,
22 LayoutPosition, LayoutWidth, LayoutWritingMode, Orphans, PageBreak,
23 StyleOverflowClipMargin, StyleScrollbarGutter, Widows,
24 },
25 property::{
26 CssProperty, CssPropertyType, LayoutAlignContentValue, LayoutAlignItemsValue,
27 LayoutAlignSelfValue, LayoutFlexBasisValue, LayoutFlexDirectionValue,
28 LayoutFlexGrowValue, LayoutFlexShrinkValue, LayoutFlexWrapValue, LayoutGapValue,
29 LayoutGridAutoColumnsValue, LayoutGridAutoFlowValue, LayoutGridAutoRowsValue,
30 LayoutGridColumnValue, LayoutGridRowValue, LayoutGridTemplateColumnsValue,
31 LayoutGridTemplateRowsValue, LayoutJustifyContentValue, LayoutJustifyItemsValue,
32 LayoutJustifySelfValue,
33 },
34 style::{
35 border_radius::StyleBorderRadius,
36 lists::{StyleListStylePosition, StyleListStyleType},
37 StyleAlignmentBaseline, StyleBaselineSource, StyleDirection, StyleDominantBaseline,
38 StyleInitialLetterAlign, StyleLineFitEdge,
39 StyleInitialLetterWrap, StyleTextAlign, StyleTextBoxEdge, StyleTextBoxTrim,
40 StyleUnicodeBidi, StyleUserSelect, StyleVerticalAlign, StyleVisibility,
41 StyleWhiteSpace,
42 },
43 },
44};
45
46use crate::{
47 font_traits::{ParsedFontTrait, StyleProperties},
48 solver3::{
49 display_list::{BorderRadius, PhysicalSizeImport},
50 layout_tree::LayoutNode,
51 scrollbar::ScrollbarRequirements,
52 },
53};
54
55const DEFAULT_EM_SIZE: f32 = 16.0;
56const DEFAULT_CARET_WIDTH_PX: f32 = 2.0;
57const DEFAULT_CARET_BLINK_MS: u32 = 500;
58const DEFAULT_TAB_SIZE: f32 = 8.0;
59const SCROLLBAR_WIDTH_THIN: f32 = 8.0;
60const SCROLLBAR_WIDTH_AUTO: f32 = 12.0;
61const SCROLLBAR_HOVER_EXPAND_PX: f32 = 4.0;
62const THUMB_HOVER_LIGHTEN: u8 = 30;
63const THUMB_HOVER_ALPHA_ADD: u8 = 40;
64const THUMB_ACTIVE_DARKEN: u8 = 15;
65
66#[must_use] pub fn get_element_font_size(
87 styled_dom: &StyledDom,
88 dom_id: NodeId,
89 node_state: &StyledNodeState,
90) -> f32 {
91 let _ = compute_all_font_sizes_px; resolve_font_size_slow(styled_dom, dom_id, node_state)
104}
105
106fn compute_all_font_sizes_px(styled_dom: &StyledDom) -> Vec<f32> {
124 use azul_css::props::{
125 basic::length::SizeMetric,
126 property::{CssProperty, CssPropertyType},
127 };
128
129 let n = styled_dom.node_data.len();
130 let mut sizes = alloc::vec![DEFAULT_FONT_SIZE; n];
131 if n == 0 {
132 return sizes;
133 }
134
135 let data_container = styled_dom.node_data.as_container();
136 let state_container = styled_dom.styled_nodes.as_container();
137 let hierarchy = styled_dom.node_hierarchy.as_container();
138 let cache = &styled_dom.css_property_cache.ptr;
139
140 for idx in 0..n {
141 let dom_id = NodeId::new(idx);
142
143 if let Some(vec) = cache.computed_values.get(idx) {
145 if let Ok(cv_idx) = vec.binary_search_by_key(&CssPropertyType::FontSize, |(k, _)| *k) {
146 if let CssProperty::FontSize(css_val) = &vec[cv_idx].1.property {
147 if let Some(fs) = css_val.get_property() {
148 if fs.inner.metric == SizeMetric::Px {
149 sizes[idx] = fs.inner.number.get();
150 continue;
151 }
152 }
153 }
154 }
155 }
156
157 let parent_font_size = hierarchy
159 .get(dom_id)
160 .and_then(azul_core::styled_dom::NodeHierarchyItem::parent_id)
161 .map_or(DEFAULT_FONT_SIZE, |p| sizes[p.index()]);
162 let root_font_size = sizes[0];
163
164 let Some(node_data) = data_container.internal.get(idx) else {
165 sizes[idx] = DEFAULT_FONT_SIZE;
166 continue;
167 };
168 let Some(styled) = state_container.internal.get(idx) else {
169 sizes[idx] = DEFAULT_FONT_SIZE;
170 continue;
171 };
172 let node_state = &styled.styled_node_state;
173
174 let mut fast_fs: Option<f32> = None;
178 let mut compact_said_inherit = false;
179 if node_state.is_normal() {
180 if let Some(ref cc) = cache.compact_cache {
181 let raw = cc.get_font_size_raw(idx);
182 if raw == azul_css::compact_cache::U32_SENTINEL
183 || raw == azul_css::compact_cache::U32_INHERIT
184 || raw == azul_css::compact_cache::U32_INITIAL
185 {
186 compact_said_inherit = true;
187 } else if let Some(pv) = azul_css::compact_cache::decode_pixel_value_u32(raw) {
188 if pv.metric == SizeMetric::Px {
190 fast_fs = Some(pv.number.get());
191 } else {
192 let context = ResolutionContext {
194 element_font_size: DEFAULT_FONT_SIZE,
195 parent_font_size,
196 root_font_size,
197 containing_block_size: PhysicalSize::new(0.0, 0.0),
198 element_size: None,
199 viewport_size: PhysicalSize::new(0.0, 0.0),
200 };
201 fast_fs =
202 Some(pv.resolve_with_context(&context, PropertyContext::FontSize));
203 }
204 }
205 }
206 }
207 if let Some(fs) = fast_fs {
208 sizes[idx] = fs;
209 continue;
210 }
211 if compact_said_inherit {
212 sizes[idx] = parent_font_size;
213 continue;
214 }
215
216 let resolved = cache
217 .get_font_size(node_data, &dom_id, node_state)
218 .and_then(|v| v.get_property().copied())
219 .map(|v| {
220 let context = ResolutionContext {
221 element_font_size: DEFAULT_FONT_SIZE,
222 parent_font_size,
223 root_font_size,
224 containing_block_size: PhysicalSize::new(0.0, 0.0),
225 element_size: None,
226 viewport_size: PhysicalSize::new(0.0, 0.0),
227 };
228 v.inner
229 .resolve_with_context(&context, PropertyContext::FontSize)
230 });
231
232 sizes[idx] = resolved.unwrap_or(DEFAULT_FONT_SIZE);
234 }
235 sizes
236}
237
238fn resolve_font_size_slow(
243 styled_dom: &StyledDom,
244 dom_id: NodeId,
245 node_state: &StyledNodeState,
246) -> f32 {
247 let hierarchy = styled_dom.node_hierarchy.as_container();
258 let states = styled_dom.styled_nodes.as_container();
259 let root_id = NodeId::new(0);
260
261 let root_font_size = if dom_id == root_id {
264 DEFAULT_FONT_SIZE
265 } else {
266 let root_state = &states[root_id].styled_node_state;
267 resolve_font_size_one(
268 styled_dom,
269 root_id,
270 root_state,
271 DEFAULT_FONT_SIZE,
272 DEFAULT_FONT_SIZE,
273 )
274 };
275
276 let mut chain = Vec::new();
278 let mut cur = Some(dom_id);
279 while let Some(id) = cur {
280 chain.push(id);
281 cur = hierarchy
282 .get(id)
283 .and_then(azul_core::styled_dom::NodeHierarchyItem::parent_id);
284 }
285
286 let mut parent_font_size = DEFAULT_FONT_SIZE;
289 let mut resolved = DEFAULT_FONT_SIZE;
290 for &id in chain.iter().rev() {
291 let this_state = if id == dom_id {
294 node_state
295 } else {
296 &states[id].styled_node_state
297 };
298 let this_root_fs = if id == root_id {
299 DEFAULT_FONT_SIZE
300 } else {
301 root_font_size
302 };
303 resolved =
304 resolve_font_size_one(styled_dom, id, this_state, parent_font_size, this_root_fs);
305 parent_font_size = resolved;
306 }
307 resolved
308}
309
310fn resolve_font_size_one(
315 styled_dom: &StyledDom,
316 dom_id: NodeId,
317 node_state: &StyledNodeState,
318 parent_font_size: f32,
319 root_font_size: f32,
320) -> f32 {
321 let node_data = &styled_dom.node_data.as_container()[dom_id];
322 let cache = &styled_dom.css_property_cache.ptr;
323
324 if let Some(vec) = cache.computed_values.get(dom_id.index()) {
325 if let Ok(idx) = vec.binary_search_by_key(&CssPropertyType::FontSize, |(k, _)| *k) {
326 if let CssProperty::FontSize(css_val) = &vec[idx].1.property {
327 if let Some(fs) = css_val.get_property() {
328 if fs.inner.metric == azul_css::props::basic::length::SizeMetric::Px {
329 return fs.inner.number.get();
330 }
331 }
332 }
333 }
334 }
335
336 cache
337 .get_font_size(node_data, &dom_id, node_state)
338 .and_then(|v| v.get_property().copied())
339 .map_or(DEFAULT_FONT_SIZE, |v| {
340 let context = ResolutionContext {
341 element_font_size: DEFAULT_FONT_SIZE,
342 parent_font_size,
343 root_font_size,
344 containing_block_size: PhysicalSize::new(0.0, 0.0),
345 element_size: None,
346 viewport_size: PhysicalSize::new(0.0, 0.0),
347 };
348 v.inner
349 .resolve_with_context(&context, PropertyContext::FontSize)
350 })
351}
352
353#[must_use] pub fn get_parent_font_size(
359 styled_dom: &StyledDom,
360 dom_id: NodeId,
361 _node_state: &StyledNodeState, ) -> f32 {
363 styled_dom
364 .node_hierarchy
365 .as_container()
366 .get(dom_id)
367 .and_then(azul_core::styled_dom::NodeHierarchyItem::parent_id)
368 .map_or(DEFAULT_FONT_SIZE, |parent_id| {
369 let parent_state = &styled_dom.styled_nodes.as_container()[parent_id].styled_node_state;
370 get_element_font_size(styled_dom, parent_id, parent_state)
371 })
372}
373
374#[must_use] pub fn get_root_font_size(styled_dom: &StyledDom, _node_state: &StyledNodeState) -> f32 {
379 let root_id = NodeId::new(0);
380 let root_state = &styled_dom.styled_nodes.as_container()[root_id].styled_node_state;
381 get_element_font_size(styled_dom, root_id, root_state)
382}
383
384#[derive(Debug, Copy, Clone, PartialEq, Eq)]
387#[derive(Default)]
388pub enum MultiValue<T> {
389 #[default]
391 Auto,
392 Initial,
394 Inherit,
396 Exact(T),
398}
399
400impl<T> MultiValue<T> {
401 pub const fn is_auto(&self) -> bool {
403 matches!(self, Self::Auto)
404 }
405
406 pub const fn is_exact(&self) -> bool {
408 matches!(self, Self::Exact(_))
409 }
410
411 pub fn exact(self) -> Option<T> {
413 match self {
414 Self::Exact(v) => Some(v),
415 _ => None,
416 }
417 }
418
419 pub fn unwrap_or(self, default: T) -> T {
421 match self {
422 Self::Exact(v) => v,
423 _ => default,
424 }
425 }
426
427 pub fn unwrap_or_default(self) -> T
429 where
430 T: Default,
431 {
432 match self {
433 Self::Exact(v) => v,
434 _ => T::default(),
435 }
436 }
437
438 pub fn map<U, F>(self, f: F) -> MultiValue<U>
440 where
441 F: FnOnce(T) -> U,
442 {
443 match self {
444 Self::Exact(v) => MultiValue::Exact(f(v)),
445 Self::Auto => MultiValue::Auto,
446 Self::Initial => MultiValue::Initial,
447 Self::Inherit => MultiValue::Inherit,
448 }
449 }
450}
451
452impl MultiValue<LayoutOverflow> {
454 #[must_use] pub const fn is_clipped(&self) -> bool {
457 matches!(
458 self,
459 Self::Exact(
460 LayoutOverflow::Hidden
461 | LayoutOverflow::Clip
462 | LayoutOverflow::Auto
463 | LayoutOverflow::Scroll
464 )
465 )
466 }
467
468 #[must_use] pub const fn is_scroll(&self) -> bool {
469 matches!(
470 self,
471 Self::Exact(LayoutOverflow::Scroll | LayoutOverflow::Auto)
472 )
473 }
474
475 #[must_use] pub const fn is_auto_overflow(&self) -> bool {
476 matches!(self, Self::Exact(LayoutOverflow::Auto))
477 }
478
479 #[must_use] pub const fn is_hidden(&self) -> bool {
480 matches!(self, Self::Exact(LayoutOverflow::Hidden))
481 }
482
483 #[must_use] pub const fn is_hidden_or_clip(&self) -> bool {
484 matches!(
485 self,
486 Self::Exact(LayoutOverflow::Hidden | LayoutOverflow::Clip)
487 )
488 }
489
490 #[must_use] pub const fn is_scroll_explicit(&self) -> bool {
491 matches!(self, Self::Exact(LayoutOverflow::Scroll))
492 }
493
494 #[must_use] pub const fn is_clip(&self) -> bool {
495 matches!(self, Self::Exact(LayoutOverflow::Clip))
496 }
497
498 #[must_use] pub const fn is_visible_or_clip(&self) -> bool {
499 matches!(
500 self,
501 Self::Exact(LayoutOverflow::Visible | LayoutOverflow::Clip)
502 )
503 }
504
505 #[must_use] pub const fn establishes_bfc(&self) -> bool {
513 matches!(
514 self,
515 Self::Exact(LayoutOverflow::Hidden | LayoutOverflow::Scroll | LayoutOverflow::Auto)
516 )
517 }
518
519 #[must_use] pub const fn resolve_computed(
524 &self,
525 other_axis: &Self,
526 ) -> Self {
527 match (self, other_axis) {
528 (Self::Exact(val), Self::Exact(other)) => {
529 Self::Exact(val.resolve_computed(*other))
530 }
531 _ => *self,
532 }
533 }
534}
535
536impl MultiValue<LayoutPosition> {
538 #[must_use] pub const fn is_absolute_or_fixed(&self) -> bool {
539 matches!(
540 self,
541 Self::Exact(LayoutPosition::Absolute | LayoutPosition::Fixed)
542 )
543 }
544}
545
546impl MultiValue<LayoutFloat> {
548 #[must_use] pub const fn is_none(&self) -> bool {
549 matches!(
550 self,
551 Self::Auto
552 | Self::Initial
553 | Self::Inherit
554 | Self::Exact(LayoutFloat::None)
555 )
556 }
557}
558
559
560macro_rules! get_css_property_pixel {
563 ($fn_name:ident, $cache_method:ident, $ua_property:expr, compact_i16 = $compact_method:ident) => {
565 #[must_use] pub fn $fn_name(
566 styled_dom: &StyledDom,
567 node_id: NodeId,
568 node_state: &StyledNodeState,
569 ) -> MultiValue<PixelValue> {
570 if node_state.is_normal() {
572 if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
573 let raw = cc.$compact_method(node_id.index());
574 if raw == azul_css::compact_cache::I16_AUTO {
575 return MultiValue::Auto;
576 }
577 if raw == azul_css::compact_cache::I16_INITIAL {
578 return MultiValue::Initial;
579 }
580 if raw < azul_css::compact_cache::I16_SENTINEL_THRESHOLD {
581 return MultiValue::Exact(PixelValue::px(f32::from(raw) / 10.0));
583 }
584 }
586 }
587
588 let node_data = &styled_dom.node_data.as_container()[node_id];
589
590 let author_css = styled_dom
591 .css_property_cache
592 .ptr
593 .$cache_method(node_data, &node_id, node_state);
594
595 if let Some(ref val) = author_css {
596 if val.is_auto() {
597 return MultiValue::Auto;
598 }
599 if let Some(exact) = val.get_property().copied() {
600 return MultiValue::Exact(exact.inner);
601 }
602 }
603
604 let ua_css = azul_core::ua_css::get_ua_property(&node_data.node_type, $ua_property);
605
606 if let Some(ua_prop) = ua_css {
607 if let Some(inner) = ua_prop.get_pixel_inner() {
608 return MultiValue::Exact(inner);
609 }
610 }
611
612 MultiValue::Initial
613 }
614 };
615}
616
617trait CssPropertyPixelInner {
619 fn get_pixel_inner(&self) -> Option<PixelValue>;
620}
621
622impl CssPropertyPixelInner for CssProperty {
623 fn get_pixel_inner(&self) -> Option<PixelValue> {
624 match self {
625 Self::Left(CssPropertyValue::Exact(v)) => Some(v.inner),
626 Self::Right(CssPropertyValue::Exact(v)) => Some(v.inner),
627 Self::Top(CssPropertyValue::Exact(v)) => Some(v.inner),
628 Self::Bottom(CssPropertyValue::Exact(v)) => Some(v.inner),
629 Self::MarginLeft(CssPropertyValue::Exact(v)) => Some(v.inner),
630 Self::MarginRight(CssPropertyValue::Exact(v)) => Some(v.inner),
631 Self::MarginTop(CssPropertyValue::Exact(v)) => Some(v.inner),
632 Self::MarginBottom(CssPropertyValue::Exact(v)) => Some(v.inner),
633 Self::PaddingLeft(CssPropertyValue::Exact(v)) => Some(v.inner),
634 Self::PaddingRight(CssPropertyValue::Exact(v)) => Some(v.inner),
635 Self::PaddingTop(CssPropertyValue::Exact(v)) => Some(v.inner),
636 Self::PaddingBottom(CssPropertyValue::Exact(v)) => Some(v.inner),
637 _ => None,
638 }
639 }
640}
641
642macro_rules! get_css_property {
644 ($fn_name:ident, $cache_method:ident, $return_type:ty, $ua_property:expr, compact = $compact_method:ident) => {
646 #[must_use] pub fn $fn_name(
647 styled_dom: &StyledDom,
648 node_id: NodeId,
649 node_state: &StyledNodeState,
650 ) -> MultiValue<$return_type> {
651 if node_state.is_normal() {
657 if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
658 return MultiValue::Exact(cc.$compact_method(node_id.index()));
659 }
660 }
661
662 let node_data = &styled_dom.node_data.as_container()[node_id];
664
665 let author_css = styled_dom
667 .css_property_cache
668 .ptr
669 .$cache_method(node_data, &node_id, node_state);
670
671 if let Some(val) = author_css.and_then(|v| v.get_property().cloned()) {
672 return MultiValue::Exact(val);
673 }
674
675 let ua_css = azul_core::ua_css::get_ua_property(&node_data.node_type, $ua_property);
677
678 if let Some(ua_prop) = ua_css {
679 if let Some(val) = extract_property_value::<$return_type>(ua_prop) {
680 return MultiValue::Exact(val);
681 }
682 }
683
684 MultiValue::Auto
686 }
687 };
688 ($fn_name:ident, $cache_method:ident, $return_type:ty, $ua_property:expr, compact_u32_dim = $compact_raw_method:ident, $px_variant:path, $auto_variant:path, $min_content_variant:path, $max_content_variant:path) => {
691 #[must_use] pub fn $fn_name(
692 styled_dom: &StyledDom,
693 node_id: NodeId,
694 node_state: &StyledNodeState,
695 ) -> MultiValue<$return_type> {
696 if node_state.is_normal() {
698 if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
699 let raw = cc.$compact_raw_method(node_id.index());
700 match raw {
701 azul_css::compact_cache::U32_AUTO => return MultiValue::Auto,
702 azul_css::compact_cache::U32_INITIAL => return MultiValue::Initial,
703 azul_css::compact_cache::U32_NONE => return MultiValue::Auto,
704 azul_css::compact_cache::U32_MIN_CONTENT => return MultiValue::Exact($min_content_variant),
705 azul_css::compact_cache::U32_MAX_CONTENT => return MultiValue::Exact($max_content_variant),
706 azul_css::compact_cache::U32_SENTINEL | azul_css::compact_cache::U32_INHERIT => {
707 }
709 _ => {
710 if let Some(pv) = azul_css::compact_cache::decode_pixel_value_u32(raw) {
712 return MultiValue::Exact($px_variant(pv));
713 }
714 }
716 }
717 }
718 }
719
720 let node_data = &styled_dom.node_data.as_container()[node_id];
722
723 let author_css = styled_dom
724 .css_property_cache
725 .ptr
726 .$cache_method(node_data, &node_id, node_state);
727
728 if let Some(val) = author_css.and_then(|v| v.get_property().cloned()) {
729 return MultiValue::Exact(val);
730 }
731
732 let ua_css = azul_core::ua_css::get_ua_property(&node_data.node_type, $ua_property);
733
734 if let Some(ua_prop) = ua_css {
735 if let Some(val) = extract_property_value::<$return_type>(ua_prop) {
736 return MultiValue::Exact(val);
737 }
738 }
739
740 MultiValue::Auto
741 }
742 };
743 ($fn_name:ident, $cache_method:ident, $return_type:ty, $ua_property:expr, compact_u32_struct = $compact_raw_method:ident) => {
746 #[must_use] pub fn $fn_name(
747 styled_dom: &StyledDom,
748 node_id: NodeId,
749 node_state: &StyledNodeState,
750 ) -> MultiValue<$return_type> {
751 if node_state.is_normal() {
753 if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
754 let raw = cc.$compact_raw_method(node_id.index());
755 match raw {
756 azul_css::compact_cache::U32_AUTO | azul_css::compact_cache::U32_NONE => return MultiValue::Auto,
757 azul_css::compact_cache::U32_INITIAL => return MultiValue::Initial,
758 azul_css::compact_cache::U32_SENTINEL | azul_css::compact_cache::U32_INHERIT => {
759 }
761 _ => {
762 if let Some(pv) = azul_css::compact_cache::decode_pixel_value_u32(raw) {
763 return MultiValue::Exact(
764 <$return_type as azul_css::props::PixelValueTaker>::from_pixel_value(pv)
765 );
766 }
767 }
768 }
769 }
770 }
771
772 let node_data = &styled_dom.node_data.as_container()[node_id];
774
775 let author_css = styled_dom
776 .css_property_cache
777 .ptr
778 .$cache_method(node_data, &node_id, node_state);
779
780 if let Some(val) = author_css.and_then(|v| v.get_property().cloned()) {
781 return MultiValue::Exact(val);
782 }
783
784 let ua_css = azul_core::ua_css::get_ua_property(&node_data.node_type, $ua_property);
785
786 if let Some(ua_prop) = ua_css {
787 if let Some(val) = extract_property_value::<$return_type>(ua_prop) {
788 return MultiValue::Exact(val);
789 }
790 }
791
792 MultiValue::Auto
793 }
794 };
795 ($fn_name:ident, $cache_method:ident, $return_type:ty, $ua_property:expr) => {
797 #[must_use] pub fn $fn_name(
798 styled_dom: &StyledDom,
799 node_id: NodeId,
800 node_state: &StyledNodeState,
801 ) -> MultiValue<$return_type> {
802 let node_data = &styled_dom.node_data.as_container()[node_id];
803
804 let author_css = styled_dom
806 .css_property_cache
807 .ptr
808 .$cache_method(node_data, &node_id, node_state);
809
810 if let Some(val) = author_css.and_then(|v| v.get_property().cloned()) {
811 return MultiValue::Exact(val);
812 }
813
814 let ua_css = azul_core::ua_css::get_ua_property(&node_data.node_type, $ua_property);
816
817 if let Some(ua_prop) = ua_css {
818 if let Some(val) = extract_property_value::<$return_type>(ua_prop) {
819 return MultiValue::Exact(val);
820 }
821 }
822
823 MultiValue::Auto
825 }
826 };
827}
828
829trait ExtractPropertyValue<T> {
831 fn extract(&self) -> Option<T>;
832}
833
834fn extract_property_value<T>(prop: &CssProperty) -> Option<T>
835where
836 CssProperty: ExtractPropertyValue<T>,
837{
838 prop.extract()
839}
840
841impl ExtractPropertyValue<LayoutWidth> for CssProperty {
844 fn extract(&self) -> Option<LayoutWidth> {
845 match self {
846 Self::Width(CssPropertyValue::Exact(v)) => Some(v.clone()),
847 _ => None,
848 }
849 }
850}
851
852impl ExtractPropertyValue<LayoutHeight> for CssProperty {
853 fn extract(&self) -> Option<LayoutHeight> {
854 match self {
855 Self::Height(CssPropertyValue::Exact(v)) => Some(v.clone()),
856 _ => None,
857 }
858 }
859}
860
861impl ExtractPropertyValue<LayoutMinWidth> for CssProperty {
862 fn extract(&self) -> Option<LayoutMinWidth> {
863 match self {
864 Self::MinWidth(CssPropertyValue::Exact(v)) => Some(*v),
865 _ => None,
866 }
867 }
868}
869
870impl ExtractPropertyValue<LayoutMinHeight> for CssProperty {
871 fn extract(&self) -> Option<LayoutMinHeight> {
872 match self {
873 Self::MinHeight(CssPropertyValue::Exact(v)) => Some(*v),
874 _ => None,
875 }
876 }
877}
878
879impl ExtractPropertyValue<LayoutMaxWidth> for CssProperty {
880 fn extract(&self) -> Option<LayoutMaxWidth> {
881 match self {
882 Self::MaxWidth(CssPropertyValue::Exact(v)) => Some(*v),
883 _ => None,
884 }
885 }
886}
887
888impl ExtractPropertyValue<LayoutMaxHeight> for CssProperty {
889 fn extract(&self) -> Option<LayoutMaxHeight> {
890 match self {
891 Self::MaxHeight(CssPropertyValue::Exact(v)) => Some(*v),
892 _ => None,
893 }
894 }
895}
896
897impl ExtractPropertyValue<LayoutDisplay> for CssProperty {
898 fn extract(&self) -> Option<LayoutDisplay> {
899 match self {
900 Self::Display(CssPropertyValue::Exact(v)) => Some(*v),
901 _ => None,
902 }
903 }
904}
905
906impl ExtractPropertyValue<LayoutWritingMode> for CssProperty {
907 fn extract(&self) -> Option<LayoutWritingMode> {
908 match self {
909 Self::WritingMode(CssPropertyValue::Exact(v)) => Some(*v),
910 _ => None,
911 }
912 }
913}
914
915impl ExtractPropertyValue<LayoutFlexWrap> for CssProperty {
916 fn extract(&self) -> Option<LayoutFlexWrap> {
917 match self {
918 Self::FlexWrap(CssPropertyValue::Exact(v)) => Some(*v),
919 _ => None,
920 }
921 }
922}
923
924impl ExtractPropertyValue<LayoutJustifyContent> for CssProperty {
925 fn extract(&self) -> Option<LayoutJustifyContent> {
926 match self {
927 Self::JustifyContent(CssPropertyValue::Exact(v)) => Some(*v),
928 _ => None,
929 }
930 }
931}
932
933impl ExtractPropertyValue<StyleTextAlign> for CssProperty {
934 fn extract(&self) -> Option<StyleTextAlign> {
935 match self {
936 Self::TextAlign(CssPropertyValue::Exact(v)) => Some(*v),
937 _ => None,
938 }
939 }
940}
941
942impl ExtractPropertyValue<LayoutFloat> for CssProperty {
943 fn extract(&self) -> Option<LayoutFloat> {
944 match self {
945 Self::Float(CssPropertyValue::Exact(v)) => Some(*v),
946 _ => None,
947 }
948 }
949}
950
951impl ExtractPropertyValue<LayoutClear> for CssProperty {
952 fn extract(&self) -> Option<LayoutClear> {
953 match self {
954 Self::Clear(CssPropertyValue::Exact(v)) => Some(*v),
955 _ => None,
956 }
957 }
958}
959
960impl ExtractPropertyValue<LayoutOverflow> for CssProperty {
961 fn extract(&self) -> Option<LayoutOverflow> {
962 match self {
963 Self::OverflowX(CssPropertyValue::Exact(v))
964 | Self::OverflowY(CssPropertyValue::Exact(v))
965 | Self::OverflowBlock(CssPropertyValue::Exact(v))
966 | Self::OverflowInline(CssPropertyValue::Exact(v)) => Some(*v),
967 _ => None,
968 }
969 }
970}
971
972impl ExtractPropertyValue<LayoutPosition> for CssProperty {
973 fn extract(&self) -> Option<LayoutPosition> {
974 match self {
975 Self::Position(CssPropertyValue::Exact(v)) => Some(*v),
976 _ => None,
977 }
978 }
979}
980
981impl ExtractPropertyValue<LayoutBoxSizing> for CssProperty {
982 fn extract(&self) -> Option<LayoutBoxSizing> {
983 match self {
984 Self::BoxSizing(CssPropertyValue::Exact(v)) => Some(*v),
985 _ => None,
986 }
987 }
988}
989
990impl ExtractPropertyValue<PixelValue> for CssProperty {
991 fn extract(&self) -> Option<PixelValue> {
992 self.get_pixel_inner()
993 }
994}
995
996impl ExtractPropertyValue<LayoutFlexDirection> for CssProperty {
997 fn extract(&self) -> Option<LayoutFlexDirection> {
998 match self {
999 Self::FlexDirection(CssPropertyValue::Exact(v)) => Some(*v),
1000 _ => None,
1001 }
1002 }
1003}
1004
1005impl ExtractPropertyValue<LayoutAlignItems> for CssProperty {
1006 fn extract(&self) -> Option<LayoutAlignItems> {
1007 match self {
1008 Self::AlignItems(CssPropertyValue::Exact(v)) => Some(*v),
1009 _ => None,
1010 }
1011 }
1012}
1013
1014impl ExtractPropertyValue<LayoutAlignContent> for CssProperty {
1015 fn extract(&self) -> Option<LayoutAlignContent> {
1016 match self {
1017 Self::AlignContent(CssPropertyValue::Exact(v)) => Some(*v),
1018 _ => None,
1019 }
1020 }
1021}
1022
1023impl ExtractPropertyValue<StyleFontWeight> for CssProperty {
1024 fn extract(&self) -> Option<StyleFontWeight> {
1025 match self {
1026 Self::FontWeight(CssPropertyValue::Exact(v)) => Some(*v),
1027 _ => None,
1028 }
1029 }
1030}
1031
1032impl ExtractPropertyValue<StyleFontStyle> for CssProperty {
1033 fn extract(&self) -> Option<StyleFontStyle> {
1034 match self {
1035 Self::FontStyle(CssPropertyValue::Exact(v)) => Some(*v),
1036 _ => None,
1037 }
1038 }
1039}
1040
1041impl ExtractPropertyValue<StyleVisibility> for CssProperty {
1042 fn extract(&self) -> Option<StyleVisibility> {
1043 match self {
1044 Self::Visibility(CssPropertyValue::Exact(v)) => Some(*v),
1045 _ => None,
1046 }
1047 }
1048}
1049
1050impl ExtractPropertyValue<StyleWhiteSpace> for CssProperty {
1051 fn extract(&self) -> Option<StyleWhiteSpace> {
1052 match self {
1053 Self::WhiteSpace(CssPropertyValue::Exact(v)) => Some(*v),
1054 _ => None,
1055 }
1056 }
1057}
1058
1059impl ExtractPropertyValue<StyleDirection> for CssProperty {
1060 fn extract(&self) -> Option<StyleDirection> {
1061 match self {
1062 Self::Direction(CssPropertyValue::Exact(v)) => Some(*v),
1063 _ => None,
1064 }
1065 }
1066}
1067
1068impl ExtractPropertyValue<StyleUnicodeBidi> for CssProperty {
1069 fn extract(&self) -> Option<StyleUnicodeBidi> {
1070 match self {
1071 Self::UnicodeBidi(CssPropertyValue::Exact(v)) => Some(*v),
1072 _ => None,
1073 }
1074 }
1075}
1076
1077impl ExtractPropertyValue<StyleTextBoxTrim> for CssProperty {
1078 fn extract(&self) -> Option<StyleTextBoxTrim> {
1079 match self {
1080 Self::TextBoxTrim(CssPropertyValue::Exact(v)) => Some(*v),
1081 _ => None,
1082 }
1083 }
1084}
1085
1086impl ExtractPropertyValue<StyleTextBoxEdge> for CssProperty {
1087 fn extract(&self) -> Option<StyleTextBoxEdge> {
1088 match self {
1089 Self::TextBoxEdge(CssPropertyValue::Exact(v)) => Some(*v),
1090 _ => None,
1091 }
1092 }
1093}
1094
1095impl ExtractPropertyValue<StyleDominantBaseline> for CssProperty {
1096 fn extract(&self) -> Option<StyleDominantBaseline> {
1097 match self {
1098 Self::DominantBaseline(CssPropertyValue::Exact(v)) => Some(*v),
1099 _ => None,
1100 }
1101 }
1102}
1103
1104impl ExtractPropertyValue<StyleAlignmentBaseline> for CssProperty {
1105 fn extract(&self) -> Option<StyleAlignmentBaseline> {
1106 match self {
1107 Self::AlignmentBaseline(CssPropertyValue::Exact(v)) => Some(*v),
1108 _ => None,
1109 }
1110 }
1111}
1112
1113impl ExtractPropertyValue<StyleBaselineSource> for CssProperty {
1114 fn extract(&self) -> Option<StyleBaselineSource> {
1115 match self {
1116 Self::BaselineSource(CssPropertyValue::Exact(v)) => Some(*v),
1117 _ => None,
1118 }
1119 }
1120}
1121
1122impl ExtractPropertyValue<StyleLineFitEdge> for CssProperty {
1123 fn extract(&self) -> Option<StyleLineFitEdge> {
1124 match self {
1125 Self::LineFitEdge(CssPropertyValue::Exact(v)) => Some(*v),
1126 _ => None,
1127 }
1128 }
1129}
1130
1131impl ExtractPropertyValue<StyleInitialLetterAlign> for CssProperty {
1132 fn extract(&self) -> Option<StyleInitialLetterAlign> {
1133 match self {
1134 Self::InitialLetterAlign(CssPropertyValue::Exact(v)) => Some(*v),
1135 _ => None,
1136 }
1137 }
1138}
1139
1140impl ExtractPropertyValue<StyleInitialLetterWrap> for CssProperty {
1141 fn extract(&self) -> Option<StyleInitialLetterWrap> {
1142 match self {
1143 Self::InitialLetterWrap(CssPropertyValue::Exact(v)) => Some(*v),
1144 _ => None,
1145 }
1146 }
1147}
1148
1149impl ExtractPropertyValue<StyleScrollbarGutter> for CssProperty {
1150 fn extract(&self) -> Option<StyleScrollbarGutter> {
1151 match self {
1152 Self::ScrollbarGutter(CssPropertyValue::Exact(v)) => Some(*v),
1153 _ => None,
1154 }
1155 }
1156}
1157
1158impl ExtractPropertyValue<StyleOverflowClipMargin> for CssProperty {
1159 fn extract(&self) -> Option<StyleOverflowClipMargin> {
1160 match self {
1161 Self::OverflowClipMargin(CssPropertyValue::Exact(v)) => Some(*v),
1162 _ => None,
1163 }
1164 }
1165}
1166
1167impl ExtractPropertyValue<StyleVerticalAlign> for CssProperty {
1168 fn extract(&self) -> Option<StyleVerticalAlign> {
1169 match self {
1170 Self::VerticalAlign(CssPropertyValue::Exact(v)) => Some(*v),
1171 _ => None,
1172 }
1173 }
1174}
1175
1176get_css_property!(
1177 get_writing_mode,
1178 get_writing_mode,
1179 LayoutWritingMode,
1180 CssPropertyType::WritingMode,
1181 compact = get_writing_mode
1182);
1183
1184get_css_property!(
1185 get_css_width,
1186 get_width,
1187 LayoutWidth,
1188 CssPropertyType::Width,
1189 compact_u32_dim = get_width_raw,
1190 LayoutWidth::Px,
1191 LayoutWidth::Auto,
1192 LayoutWidth::MinContent,
1193 LayoutWidth::MaxContent
1194);
1195
1196get_css_property!(
1197 get_css_height,
1198 get_height,
1199 LayoutHeight,
1200 CssPropertyType::Height,
1201 compact_u32_dim = get_height_raw,
1202 LayoutHeight::Px,
1203 LayoutHeight::Auto,
1204 LayoutHeight::MinContent,
1205 LayoutHeight::MaxContent
1206);
1207
1208get_css_property!(
1209 get_wrap,
1210 get_flex_wrap,
1211 LayoutFlexWrap,
1212 CssPropertyType::FlexWrap,
1213 compact = get_flex_wrap
1214);
1215
1216get_css_property!(
1217 get_justify_content,
1218 get_justify_content,
1219 LayoutJustifyContent,
1220 CssPropertyType::JustifyContent,
1221 compact = get_justify_content
1222);
1223
1224get_css_property!(
1225 get_text_align,
1226 get_text_align,
1227 StyleTextAlign,
1228 CssPropertyType::TextAlign,
1229 compact = get_text_align
1230);
1231
1232get_css_property!(
1233 get_float,
1234 get_float,
1235 LayoutFloat,
1236 CssPropertyType::Float,
1237 compact = get_float
1238);
1239
1240get_css_property!(
1241 get_clear,
1242 get_clear,
1243 LayoutClear,
1244 CssPropertyType::Clear,
1245 compact = get_clear
1246);
1247
1248get_css_property!(
1249 get_overflow_x,
1250 get_overflow_x,
1251 LayoutOverflow,
1252 CssPropertyType::OverflowX,
1253 compact = get_overflow_x
1254);
1255
1256get_css_property!(
1257 get_overflow_y,
1258 get_overflow_y,
1259 LayoutOverflow,
1260 CssPropertyType::OverflowY,
1261 compact = get_overflow_y
1262);
1263
1264get_css_property!(
1266 get_overflow_block,
1267 get_overflow_block,
1268 LayoutOverflow,
1269 CssPropertyType::OverflowBlock
1270);
1271
1272get_css_property!(
1273 get_overflow_inline,
1274 get_overflow_inline,
1275 LayoutOverflow,
1276 CssPropertyType::OverflowInline
1277);
1278
1279get_css_property!(
1280 get_position,
1281 get_position,
1282 LayoutPosition,
1283 CssPropertyType::Position,
1284 compact = get_position
1285);
1286
1287get_css_property!(
1288 get_css_box_sizing,
1289 get_box_sizing,
1290 LayoutBoxSizing,
1291 CssPropertyType::BoxSizing,
1292 compact = get_box_sizing
1293);
1294
1295get_css_property!(
1296 get_flex_direction,
1297 get_flex_direction,
1298 LayoutFlexDirection,
1299 CssPropertyType::FlexDirection,
1300 compact = get_flex_direction
1301);
1302
1303get_css_property!(
1304 get_align_items,
1305 get_align_items,
1306 LayoutAlignItems,
1307 CssPropertyType::AlignItems,
1308 compact = get_align_items
1309);
1310
1311get_css_property!(
1312 get_align_content,
1313 get_align_content,
1314 LayoutAlignContent,
1315 CssPropertyType::AlignContent,
1316 compact = get_align_content
1317);
1318
1319get_css_property!(
1320 get_font_weight_property,
1321 get_font_weight,
1322 StyleFontWeight,
1323 CssPropertyType::FontWeight,
1324 compact = get_font_weight
1325);
1326
1327get_css_property!(
1328 get_font_style_property,
1329 get_font_style,
1330 StyleFontStyle,
1331 CssPropertyType::FontStyle,
1332 compact = get_font_style
1333);
1334
1335get_css_property!(
1336 get_visibility,
1337 get_visibility,
1338 StyleVisibility,
1339 CssPropertyType::Visibility,
1340 compact = get_visibility
1341);
1342
1343get_css_property!(
1344 get_white_space_property,
1345 get_white_space,
1346 StyleWhiteSpace,
1347 CssPropertyType::WhiteSpace,
1348 compact = get_white_space
1349);
1350
1351get_css_property!(
1353 get_direction_property,
1354 get_direction,
1355 StyleDirection,
1356 CssPropertyType::Direction,
1357 compact = get_direction
1358);
1359
1360get_css_property!(
1364 get_unicode_bidi_property,
1365 get_unicode_bidi,
1366 StyleUnicodeBidi,
1367 CssPropertyType::UnicodeBidi
1368);
1369
1370get_css_property!(
1373 get_text_box_trim_property,
1374 get_text_box_trim,
1375 StyleTextBoxTrim,
1376 CssPropertyType::TextBoxTrim
1377);
1378
1379get_css_property!(
1380 get_text_box_edge_property,
1381 get_text_box_edge,
1382 StyleTextBoxEdge,
1383 CssPropertyType::TextBoxEdge
1384);
1385
1386get_css_property!(
1387 get_dominant_baseline_property,
1388 get_dominant_baseline,
1389 StyleDominantBaseline,
1390 CssPropertyType::DominantBaseline
1391);
1392
1393get_css_property!(
1394 get_alignment_baseline_property,
1395 get_alignment_baseline,
1396 StyleAlignmentBaseline,
1397 CssPropertyType::AlignmentBaseline
1398);
1399
1400get_css_property!(
1401 get_baseline_source_property,
1402 get_baseline_source,
1403 StyleBaselineSource,
1404 CssPropertyType::BaselineSource
1405);
1406
1407get_css_property!(
1408 get_line_fit_edge_property,
1409 get_line_fit_edge,
1410 StyleLineFitEdge,
1411 CssPropertyType::LineFitEdge
1412);
1413
1414get_css_property!(
1415 get_initial_letter_align_property,
1416 get_initial_letter_align,
1417 StyleInitialLetterAlign,
1418 CssPropertyType::InitialLetterAlign
1419);
1420
1421get_css_property!(
1422 get_initial_letter_wrap_property,
1423 get_initial_letter_wrap,
1424 StyleInitialLetterWrap,
1425 CssPropertyType::InitialLetterWrap
1426);
1427
1428#[allow(clippy::match_same_arms)] #[must_use] pub fn get_scrollbar_gutter_property(
1435 styled_dom: &StyledDom,
1436 node_id: NodeId,
1437 node_state: &StyledNodeState,
1438) -> MultiValue<StyleScrollbarGutter> {
1439 if node_state.is_normal() {
1441 if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
1442 let bits = cc.get_scrollbar_gutter_bits(node_id.index());
1443 let val = match bits {
1444 azul_css::compact_cache::SCROLLBAR_GUTTER_AUTO => StyleScrollbarGutter::Auto,
1445 azul_css::compact_cache::SCROLLBAR_GUTTER_STABLE => StyleScrollbarGutter::Stable,
1446 azul_css::compact_cache::SCROLLBAR_GUTTER_BOTH_EDGES => {
1447 StyleScrollbarGutter::StableBothEdges
1448 }
1449 _ => StyleScrollbarGutter::Auto,
1450 };
1451 return MultiValue::Exact(val);
1452 }
1453 }
1454
1455 let node_data = &styled_dom.node_data.as_container()[node_id];
1457 let author_css = styled_dom
1458 .css_property_cache
1459 .ptr
1460 .get_scrollbar_gutter(node_data, &node_id, node_state);
1461 if let Some(val) = author_css.and_then(|v| v.get_property().copied()) {
1462 return MultiValue::Exact(val);
1463 }
1464 MultiValue::Auto
1465}
1466
1467get_css_property!(
1468 get_overflow_clip_margin_property,
1469 get_overflow_clip_margin,
1470 StyleOverflowClipMargin,
1471 CssPropertyType::OverflowClipMargin
1472);
1473
1474get_css_property!(
1475 get_object_fit_property,
1476 get_object_fit,
1477 StyleObjectFit,
1478 CssPropertyType::ObjectFit
1479);
1480
1481get_css_property!(
1482 get_text_overflow_property,
1483 get_text_overflow,
1484 StyleTextOverflow,
1485 CssPropertyType::TextOverflow
1486);
1487
1488#[must_use] pub fn get_text_orientation_property(
1494 styled_dom: &StyledDom,
1495 node_id: NodeId,
1496 node_state: &StyledNodeState,
1497) -> MultiValue<StyleTextOrientation> {
1498 if node_state.is_normal() {
1499 if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
1500 if !cc.has_text_orientation(node_id.index()) {
1501 return MultiValue::Auto;
1502 }
1503 }
1504 }
1505 let node_data = &styled_dom.node_data.as_container()[node_id];
1506 if let Some(val) = styled_dom
1507 .css_property_cache
1508 .ptr
1509 .get_text_orientation(node_data, &node_id, node_state)
1510 .and_then(|v| v.get_property().copied())
1511 {
1512 return MultiValue::Exact(val);
1513 }
1514 let ua = azul_core::ua_css::get_ua_property(
1515 &node_data.node_type,
1516 CssPropertyType::TextOrientation,
1517 );
1518 if let Some(ua_prop) = ua {
1519 if let Some(val) = extract_property_value::<StyleTextOrientation>(ua_prop) {
1520 return MultiValue::Exact(val);
1521 }
1522 }
1523 MultiValue::Auto
1524}
1525
1526get_css_property!(
1527 get_object_position_property,
1528 get_object_position,
1529 StyleObjectPosition,
1530 CssPropertyType::ObjectPosition
1531);
1532
1533get_css_property!(
1534 get_aspect_ratio_property,
1535 get_aspect_ratio,
1536 StyleAspectRatio,
1537 CssPropertyType::AspectRatio
1538);
1539
1540#[must_use] pub fn get_vertical_align_property(
1544 styled_dom: &StyledDom,
1545 node_id: NodeId,
1546 node_state: &StyledNodeState,
1547) -> MultiValue<StyleVerticalAlign> {
1548 let node_data = &styled_dom.node_data.as_container()[node_id];
1549
1550 let author_css = styled_dom
1551 .css_property_cache
1552 .ptr
1553 .get_vertical_align(node_data, &node_id, node_state);
1554
1555 if let Some(val) = author_css.and_then(|v| v.get_property().copied()) {
1556 return MultiValue::Exact(val);
1557 }
1558
1559 let ua_css = azul_core::ua_css::get_ua_property(
1560 &node_data.node_type,
1561 CssPropertyType::VerticalAlign,
1562 );
1563
1564 if let Some(ua_prop) = ua_css {
1565 if let Some(val) = extract_property_value::<StyleVerticalAlign>(ua_prop) {
1566 return MultiValue::Exact(val);
1567 }
1568 }
1569
1570 MultiValue::Auto
1571}
1572#[must_use] pub fn get_style_border_radius(
1576 styled_dom: &StyledDom,
1577 node_id: NodeId,
1578 node_state: &StyledNodeState,
1579) -> StyleBorderRadius {
1580 use azul_css::props::basic::pixel::PixelValue;
1581 if node_state.is_normal() {
1584 if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
1585 let idx = node_id.index();
1586 let decode = |raw: i16| -> PixelValue {
1587 if raw >= azul_css::compact_cache::I16_SENTINEL_THRESHOLD {
1588 PixelValue::px(0.0)
1589 } else {
1590 PixelValue::px(f32::from(raw) / 10.0)
1591 }
1592 };
1593 return StyleBorderRadius {
1594 top_left: decode(cc.get_border_top_left_radius_raw(idx)),
1595 top_right: decode(cc.get_border_top_right_radius_raw(idx)),
1596 bottom_right: decode(cc.get_border_bottom_right_radius_raw(idx)),
1597 bottom_left: decode(cc.get_border_bottom_left_radius_raw(idx)),
1598 };
1599 }
1600 }
1601 let node_data = &styled_dom.node_data.as_container()[node_id];
1602
1603 let top_left = styled_dom
1604 .css_property_cache
1605 .ptr
1606 .get_border_top_left_radius(node_data, &node_id, node_state)
1607 .and_then(|br| br.get_property_or_default())
1608 .map(|v| v.inner)
1609 .unwrap_or_default();
1610
1611 let top_right = styled_dom
1612 .css_property_cache
1613 .ptr
1614 .get_border_top_right_radius(node_data, &node_id, node_state)
1615 .and_then(|br| br.get_property_or_default())
1616 .map(|v| v.inner)
1617 .unwrap_or_default();
1618
1619 let bottom_right = styled_dom
1620 .css_property_cache
1621 .ptr
1622 .get_border_bottom_right_radius(node_data, &node_id, node_state)
1623 .and_then(|br| br.get_property_or_default())
1624 .map(|v| v.inner)
1625 .unwrap_or_default();
1626
1627 let bottom_left = styled_dom
1628 .css_property_cache
1629 .ptr
1630 .get_border_bottom_left_radius(node_data, &node_id, node_state)
1631 .and_then(|br| br.get_property_or_default())
1632 .map(|v| v.inner)
1633 .unwrap_or_default();
1634
1635 StyleBorderRadius {
1636 top_left,
1637 top_right,
1638 bottom_right,
1639 bottom_left,
1640 }
1641}
1642
1643#[must_use] pub fn get_border_radius(
1649 styled_dom: &StyledDom,
1650 node_id: NodeId,
1651 node_state: &StyledNodeState,
1652 element_size: PhysicalSizeImport,
1653 viewport_size: LogicalSize,
1654) -> BorderRadius {
1655 use azul_css::props::basic::{PhysicalSize, PropertyContext, ResolutionContext};
1656
1657 if node_state.is_normal() {
1661 if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
1662 let idx = node_id.index();
1663 let tl = cc.get_border_top_left_radius_raw(idx);
1664 let tr = cc.get_border_top_right_radius_raw(idx);
1665 let br = cc.get_border_bottom_right_radius_raw(idx);
1666 let bl = cc.get_border_bottom_left_radius_raw(idx);
1667 let thresh = azul_css::compact_cache::I16_SENTINEL_THRESHOLD;
1669 let decode = |raw: i16| -> f32 {
1670 if raw >= thresh {
1671 0.0
1672 } else {
1673 f32::from(raw) / 10.0
1674 }
1675 };
1676 return BorderRadius {
1677 top_left: decode(tl),
1678 top_right: decode(tr),
1679 bottom_right: decode(br),
1680 bottom_left: decode(bl),
1681 };
1682 }
1683 }
1684
1685 let node_data = &styled_dom.node_data.as_container()[node_id];
1686
1687 let element_font_size = get_element_font_size(styled_dom, node_id, node_state);
1689 let parent_font_size = styled_dom
1690 .node_hierarchy
1691 .as_container()
1692 .get(node_id)
1693 .and_then(azul_core::styled_dom::NodeHierarchyItem::parent_id)
1694 .map_or(DEFAULT_FONT_SIZE, |p| get_element_font_size(styled_dom, p, node_state));
1695 let root_font_size = get_root_font_size(styled_dom, node_state);
1696
1697 let context = ResolutionContext {
1699 element_font_size,
1700 parent_font_size,
1701 root_font_size,
1702 containing_block_size: PhysicalSize::new(0.0, 0.0), element_size: Some(PhysicalSize::new(element_size.width, element_size.height)),
1704 viewport_size: PhysicalSize::new(viewport_size.width, viewport_size.height),
1705 };
1706
1707 let top_left = styled_dom
1708 .css_property_cache
1709 .ptr
1710 .get_border_top_left_radius(node_data, &node_id, node_state)
1711 .and_then(|br| br.get_property().copied())
1712 .unwrap_or_default();
1713
1714 let top_right = styled_dom
1715 .css_property_cache
1716 .ptr
1717 .get_border_top_right_radius(node_data, &node_id, node_state)
1718 .and_then(|br| br.get_property().copied())
1719 .unwrap_or_default();
1720
1721 let bottom_right = styled_dom
1722 .css_property_cache
1723 .ptr
1724 .get_border_bottom_right_radius(node_data, &node_id, node_state)
1725 .and_then(|br| br.get_property().copied())
1726 .unwrap_or_default();
1727
1728 let bottom_left = styled_dom
1729 .css_property_cache
1730 .ptr
1731 .get_border_bottom_left_radius(node_data, &node_id, node_state)
1732 .and_then(|br| br.get_property().copied())
1733 .unwrap_or_default();
1734
1735 BorderRadius {
1736 top_left: top_left
1737 .inner
1738 .resolve_with_context(&context, PropertyContext::BorderRadius),
1739 top_right: top_right
1740 .inner
1741 .resolve_with_context(&context, PropertyContext::BorderRadius),
1742 bottom_right: bottom_right
1743 .inner
1744 .resolve_with_context(&context, PropertyContext::BorderRadius),
1745 bottom_left: bottom_left
1746 .inner
1747 .resolve_with_context(&context, PropertyContext::BorderRadius),
1748 }
1749}
1750
1751#[must_use] pub fn get_z_index(styled_dom: &StyledDom, node_id: Option<NodeId>) -> i32 {
1759 use azul_css::props::layout::position::LayoutZIndex;
1760
1761 let Some(node_id) = node_id else {
1762 return 0;
1763 };
1764
1765 let node_state = &styled_dom.styled_nodes.as_container()[node_id].styled_node_state;
1766
1767 if node_state.is_normal() {
1769 if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
1770 let raw = cc.get_z_index(node_id.index());
1771 if raw == azul_css::compact_cache::I16_AUTO {
1772 return 0;
1773 }
1774 if raw < azul_css::compact_cache::I16_SENTINEL_THRESHOLD {
1775 return i32::from(raw);
1776 }
1777 }
1779 }
1780
1781 let node_data = &styled_dom.node_data.as_container()[node_id];
1783
1784 styled_dom
1785 .css_property_cache
1786 .ptr
1787 .get_z_index(node_data, &node_id, node_state)
1788 .and_then(|v| v.get_property())
1789 .map_or(0, |z| match z {
1790 LayoutZIndex::Auto => 0,
1791 LayoutZIndex::Integer(i) => *i,
1792 })
1793}
1794
1795#[must_use] pub fn is_z_index_auto(styled_dom: &StyledDom, node_id: Option<NodeId>) -> bool {
1800 use azul_css::props::layout::position::LayoutZIndex;
1801
1802 let Some(node_id) = node_id else {
1803 return true;
1804 };
1805
1806 let node_state = &styled_dom.styled_nodes.as_container()[node_id].styled_node_state;
1807
1808 if node_state.is_normal() {
1810 if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
1811 let raw = cc.get_z_index(node_id.index());
1812 if raw == azul_css::compact_cache::I16_AUTO {
1813 return true;
1814 }
1815 if raw < azul_css::compact_cache::I16_SENTINEL_THRESHOLD {
1816 return false; }
1818 }
1820 }
1821
1822 let node_data = &styled_dom.node_data.as_container()[node_id];
1824
1825 styled_dom
1826 .css_property_cache
1827 .ptr
1828 .get_z_index(node_data, &node_id, node_state)
1829 .and_then(|v| v.get_property())
1830 .is_none_or(|z| matches!(z, LayoutZIndex::Auto)) }
1832
1833#[allow(clippy::match_same_arms)] #[must_use] pub fn get_background_color(
1857 styled_dom: &StyledDom,
1858 node_id: NodeId,
1859 node_state: &StyledNodeState,
1860) -> ColorU {
1861 let node_data = &styled_dom.node_data.as_container()[node_id];
1862 let cache = &styled_dom.css_property_cache.ptr;
1863
1864 let get_node_bg = |nid: NodeId, ndata: &azul_core::dom::NodeData, state: &StyledNodeState| {
1869 if state.is_normal() {
1870 if let Some(ref cc) = cache.compact_cache {
1871 if !cc.has_background(nid.index()) {
1872 return None;
1873 }
1874 }
1875 }
1876 cache
1877 .get_background_content(ndata, &nid, state)
1878 .and_then(|bg| bg.get_property())
1879 .and_then(|bg_vec| bg_vec.get(0).cloned())
1880 .and_then(|first_bg| match &first_bg {
1881 azul_css::props::style::StyleBackgroundContent::Color(color) => Some(*color),
1882 azul_css::props::style::StyleBackgroundContent::Image(_) => None, _ => None,
1884 })
1885 };
1886
1887 let own_bg = get_node_bg(node_id, node_data, node_state);
1888
1889 if !matches!(node_data.node_type, NodeType::Html) || own_bg.is_some() {
1893 return own_bg.unwrap_or(ColorU {
1895 r: 0,
1896 g: 0,
1897 b: 0,
1898 a: 0,
1899 });
1900 }
1901
1902 let first_child = styled_dom
1904 .node_hierarchy
1905 .as_container()
1906 .get(node_id)
1907 .and_then(|node| node.first_child_id(node_id));
1908
1909 let Some(first_child) = first_child else {
1910 return ColorU {
1911 r: 0,
1912 g: 0,
1913 b: 0,
1914 a: 0,
1915 };
1916 };
1917
1918 let first_child_data = &styled_dom.node_data.as_container()[first_child];
1919
1920 if !matches!(first_child_data.node_type, NodeType::Body) {
1922 return ColorU {
1923 r: 0,
1924 g: 0,
1925 b: 0,
1926 a: 0,
1927 };
1928 }
1929
1930 let first_child_state = &styled_dom.styled_nodes.as_container()[first_child].styled_node_state;
1932 get_node_bg(first_child, first_child_data, first_child_state).unwrap_or(ColorU {
1933 r: 0,
1934 g: 0,
1935 b: 0,
1936 a: 0,
1937 })
1938}
1939
1940#[must_use] pub fn get_background_contents(
1947 styled_dom: &StyledDom,
1948 node_id: NodeId,
1949 node_state: &StyledNodeState,
1950) -> Vec<azul_css::props::style::StyleBackgroundContent> {
1951 use azul_core::dom::NodeType;
1952 use azul_css::props::style::StyleBackgroundContent;
1953
1954 let node_data = &styled_dom.node_data.as_container()[node_id];
1955 let cache = &styled_dom.css_property_cache.ptr;
1956
1957 let get_node_backgrounds = |nid: NodeId,
1961 ndata: &azul_core::dom::NodeData,
1962 state: &StyledNodeState|
1963 -> Vec<StyleBackgroundContent> {
1964 if state.is_normal() {
1965 if let Some(ref cc) = cache.compact_cache {
1966 if !cc.has_background(nid.index()) {
1967 return Vec::new();
1968 }
1969 }
1970 }
1971 cache
1972 .get_background_content(ndata, &nid, state)
1973 .and_then(|bg| bg.get_property())
1974 .map(|bg_vec| bg_vec.iter().cloned().collect())
1975 .unwrap_or_default()
1976 };
1977
1978 let own_backgrounds = get_node_backgrounds(node_id, node_data, node_state);
1979
1980 if !matches!(node_data.node_type, NodeType::Html) || !own_backgrounds.is_empty() {
1983 return own_backgrounds;
1984 }
1985
1986 let first_child = styled_dom
1988 .node_hierarchy
1989 .as_container()
1990 .get(node_id)
1991 .and_then(|node| node.first_child_id(node_id));
1992
1993 let Some(first_child) = first_child else {
1994 return own_backgrounds;
1995 };
1996
1997 let first_child_data = &styled_dom.node_data.as_container()[first_child];
1998
1999 if !matches!(first_child_data.node_type, NodeType::Body) {
2001 return own_backgrounds;
2002 }
2003
2004 let first_child_state = &styled_dom.styled_nodes.as_container()[first_child].styled_node_state;
2006 get_node_backgrounds(first_child, first_child_data, first_child_state)
2007}
2008
2009#[derive(Copy, Clone, Debug)]
2011pub struct BorderInfo {
2012 pub widths: crate::solver3::display_list::StyleBorderWidths,
2013 pub colors: crate::solver3::display_list::StyleBorderColors,
2014 pub styles: crate::solver3::display_list::StyleBorderStyles,
2015}
2016
2017#[allow(clippy::too_many_lines)] #[must_use] pub fn get_border_info(
2019 styled_dom: &StyledDom,
2020 node_id: NodeId,
2021 node_state: &StyledNodeState,
2022) -> BorderInfo {
2023 use crate::solver3::display_list::{StyleBorderColors, StyleBorderStyles, StyleBorderWidths};
2024 use azul_css::css::CssPropertyValue;
2025 use azul_css::props::basic::color::ColorU;
2026 use azul_css::props::basic::pixel::PixelValue;
2027 use azul_css::props::style::border::{
2028 BorderStyle, StyleBorderBottomColor, StyleBorderBottomStyle, StyleBorderLeftColor,
2029 StyleBorderLeftStyle, StyleBorderRightColor, StyleBorderRightStyle, StyleBorderTopColor,
2030 StyleBorderTopStyle,
2031 };
2032 use azul_css::props::style::{
2033 LayoutBorderBottomWidth, LayoutBorderLeftWidth, LayoutBorderRightWidth,
2034 LayoutBorderTopWidth,
2035 };
2036
2037 if node_state.is_normal() {
2039 if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
2040 let idx = node_id.index();
2041
2042 let make_width_px = |raw: i16| -> Option<PixelValue> {
2048 if raw == azul_css::compact_cache::I16_AUTO
2049 || raw == azul_css::compact_cache::I16_INITIAL
2050 || raw >= azul_css::compact_cache::I16_SENTINEL_THRESHOLD
2051 {
2052 None
2053 } else {
2054 Some(PixelValue::px(f32::from(raw) / 10.0))
2055 }
2056 };
2057 let widths = StyleBorderWidths {
2058 top: make_width_px(cc.get_border_top_width_raw(idx))
2059 .map(|px| CssPropertyValue::Exact(LayoutBorderTopWidth { inner: px })),
2060 right: make_width_px(cc.get_border_right_width_raw(idx))
2061 .map(|px| CssPropertyValue::Exact(LayoutBorderRightWidth { inner: px })),
2062 bottom: make_width_px(cc.get_border_bottom_width_raw(idx))
2063 .map(|px| CssPropertyValue::Exact(LayoutBorderBottomWidth { inner: px })),
2064 left: make_width_px(cc.get_border_left_width_raw(idx))
2065 .map(|px| CssPropertyValue::Exact(LayoutBorderLeftWidth { inner: px })),
2066 };
2067
2068 let make_color = |raw: u32| -> Option<ColorU> {
2070 if raw == 0 {
2071 None
2072 } else {
2073 Some(ColorU {
2074 r: ((raw >> 24) & 0xFF) as u8,
2075 g: ((raw >> 16) & 0xFF) as u8,
2076 b: ((raw >> 8) & 0xFF) as u8,
2077 a: (raw & 0xFF) as u8,
2078 })
2079 }
2080 };
2081
2082 let colors = StyleBorderColors {
2083 top: make_color(cc.get_border_top_color_raw(idx))
2084 .map(|c| CssPropertyValue::Exact(StyleBorderTopColor { inner: c })),
2085 right: make_color(cc.get_border_right_color_raw(idx))
2086 .map(|c| CssPropertyValue::Exact(StyleBorderRightColor { inner: c })),
2087 bottom: make_color(cc.get_border_bottom_color_raw(idx))
2088 .map(|c| CssPropertyValue::Exact(StyleBorderBottomColor { inner: c })),
2089 left: make_color(cc.get_border_left_color_raw(idx))
2090 .map(|c| CssPropertyValue::Exact(StyleBorderLeftColor { inner: c })),
2091 };
2092
2093 let styles = StyleBorderStyles {
2095 top: Some(CssPropertyValue::Exact(StyleBorderTopStyle {
2096 inner: cc.get_border_top_style(idx),
2097 })),
2098 right: Some(CssPropertyValue::Exact(StyleBorderRightStyle {
2099 inner: cc.get_border_right_style(idx),
2100 })),
2101 bottom: Some(CssPropertyValue::Exact(StyleBorderBottomStyle {
2102 inner: cc.get_border_bottom_style(idx),
2103 })),
2104 left: Some(CssPropertyValue::Exact(StyleBorderLeftStyle {
2105 inner: cc.get_border_left_style(idx),
2106 })),
2107 };
2108
2109 return BorderInfo {
2110 widths,
2111 colors,
2112 styles,
2113 };
2114 }
2115 }
2116
2117 let node_data = &styled_dom.node_data.as_container()[node_id];
2119
2120 let widths = StyleBorderWidths {
2122 top: styled_dom
2123 .css_property_cache
2124 .ptr
2125 .get_border_top_width(node_data, &node_id, node_state)
2126 .copied(),
2127 right: styled_dom
2128 .css_property_cache
2129 .ptr
2130 .get_border_right_width(node_data, &node_id, node_state)
2131 .copied(),
2132 bottom: styled_dom
2133 .css_property_cache
2134 .ptr
2135 .get_border_bottom_width(node_data, &node_id, node_state)
2136 .copied(),
2137 left: styled_dom
2138 .css_property_cache
2139 .ptr
2140 .get_border_left_width(node_data, &node_id, node_state)
2141 .copied(),
2142 };
2143
2144 let colors = StyleBorderColors {
2146 top: styled_dom
2147 .css_property_cache
2148 .ptr
2149 .get_border_top_color(node_data, &node_id, node_state)
2150 .copied(),
2151 right: styled_dom
2152 .css_property_cache
2153 .ptr
2154 .get_border_right_color(node_data, &node_id, node_state)
2155 .copied(),
2156 bottom: styled_dom
2157 .css_property_cache
2158 .ptr
2159 .get_border_bottom_color(node_data, &node_id, node_state)
2160 .copied(),
2161 left: styled_dom
2162 .css_property_cache
2163 .ptr
2164 .get_border_left_color(node_data, &node_id, node_state)
2165 .copied(),
2166 };
2167
2168 let styles = StyleBorderStyles {
2170 top: styled_dom
2171 .css_property_cache
2172 .ptr
2173 .get_border_top_style(node_data, &node_id, node_state)
2174 .copied(),
2175 right: styled_dom
2176 .css_property_cache
2177 .ptr
2178 .get_border_right_style(node_data, &node_id, node_state)
2179 .copied(),
2180 bottom: styled_dom
2181 .css_property_cache
2182 .ptr
2183 .get_border_bottom_style(node_data, &node_id, node_state)
2184 .copied(),
2185 left: styled_dom
2186 .css_property_cache
2187 .ptr
2188 .get_border_left_style(node_data, &node_id, node_state)
2189 .copied(),
2190 };
2191
2192 BorderInfo {
2193 widths,
2194 colors,
2195 styles,
2196 }
2197}
2198
2199#[allow(clippy::too_many_lines)] fn get_inline_border_info(
2205 styled_dom: &StyledDom,
2206 node_id: NodeId,
2207 node_state: &StyledNodeState,
2208 border_info: &BorderInfo,
2209 viewport: PhysicalSize,
2210) -> Option<crate::text3::cache::InlineBorderInfo> {
2211 use crate::text3::cache::InlineBorderInfo;
2212
2213 fn resolve_padding(
2216 mv: MultiValue<PixelValue>,
2217 viewport: PhysicalSize,
2218 ) -> f32 {
2219 match mv {
2220 MultiValue::Exact(pv) => super::calc::resolve_pixel_value_with_viewport(
2221 &pv,
2222 0.0,
2223 DEFAULT_FONT_SIZE,
2224 DEFAULT_FONT_SIZE,
2225 viewport.width,
2226 viewport.height,
2227 ),
2228 _ => 0.0,
2229 }
2230 }
2231
2232 macro_rules! border_width_px {
2233 ($field:expr) => {
2234 $field
2235 .as_ref()
2236 .and_then(|v| v.get_property())
2237 .map(|w| w.inner.number.get())
2238 .unwrap_or(0.0)
2239 };
2240 }
2241
2242 macro_rules! border_color {
2243 ($field:expr) => {
2244 $field
2245 .as_ref()
2246 .and_then(|v| v.get_property())
2247 .map(|c| c.inner)
2248 .unwrap_or(ColorU::BLACK)
2249 };
2250 }
2251
2252 fn get_border_radius_px(
2254 styled_dom: &StyledDom,
2255 node_id: NodeId,
2256 node_state: &StyledNodeState,
2257 ) -> Option<f32> {
2258 let node_data = &styled_dom.node_data.as_container()[node_id];
2259
2260 let top_left = styled_dom
2261 .css_property_cache
2262 .ptr
2263 .get_border_top_left_radius(node_data, &node_id, node_state)
2264 .and_then(|br| br.get_property().copied())
2265 .map(|v| v.inner.number.get());
2266
2267 let top_right = styled_dom
2268 .css_property_cache
2269 .ptr
2270 .get_border_top_right_radius(node_data, &node_id, node_state)
2271 .and_then(|br| br.get_property().copied())
2272 .map(|v| v.inner.number.get());
2273
2274 let bottom_left = styled_dom
2275 .css_property_cache
2276 .ptr
2277 .get_border_bottom_left_radius(node_data, &node_id, node_state)
2278 .and_then(|br| br.get_property().copied())
2279 .map(|v| v.inner.number.get());
2280
2281 let bottom_right = styled_dom
2282 .css_property_cache
2283 .ptr
2284 .get_border_bottom_right_radius(node_data, &node_id, node_state)
2285 .and_then(|br| br.get_property().copied())
2286 .map(|v| v.inner.number.get());
2287
2288 let radii: Vec<f32> = [top_left, top_right, bottom_left, bottom_right]
2290 .into_iter()
2291 .flatten()
2292 .collect();
2293
2294 if radii.is_empty() {
2295 None
2296 } else {
2297 Some(radii.into_iter().fold(0.0f32, f32::max))
2298 }
2299 }
2300
2301 let top = border_width_px!(&border_info.widths.top);
2302 let right = border_width_px!(&border_info.widths.right);
2303 let bottom = border_width_px!(&border_info.widths.bottom);
2304 let left = border_width_px!(&border_info.widths.left);
2305
2306 let p_top = resolve_padding(get_css_padding_top(styled_dom, node_id, node_state), viewport);
2307 let p_right = resolve_padding(get_css_padding_right(styled_dom, node_id, node_state), viewport);
2308 let p_bottom = resolve_padding(get_css_padding_bottom(styled_dom, node_id, node_state), viewport);
2309 let p_left = resolve_padding(get_css_padding_left(styled_dom, node_id, node_state), viewport);
2310
2311 let has_border = top > 0.0 || right > 0.0 || bottom > 0.0 || left > 0.0;
2313 let has_padding = p_top > 0.0 || p_right > 0.0 || p_bottom > 0.0 || p_left > 0.0;
2314 if !has_border && !has_padding {
2315 return None;
2316 }
2317
2318 let is_rtl = matches!(
2320 get_direction_property(styled_dom, node_id, node_state),
2321 MultiValue::Exact(StyleDirection::Rtl)
2322 );
2323
2324 Some(InlineBorderInfo {
2325 top,
2326 right,
2327 bottom,
2328 left,
2329 top_color: border_color!(&border_info.colors.top),
2330 right_color: border_color!(&border_info.colors.right),
2331 bottom_color: border_color!(&border_info.colors.bottom),
2332 left_color: border_color!(&border_info.colors.left),
2333 radius: get_border_radius_px(styled_dom, node_id, node_state),
2334 padding_top: p_top,
2335 padding_right: p_right,
2336 padding_bottom: p_bottom,
2337 padding_left: p_left,
2338 is_first_fragment: true,
2339 is_last_fragment: true,
2340 is_rtl,
2341 })
2342}
2343
2344#[derive(Debug, Clone, Copy, Default)]
2348pub struct SelectionStyle {
2349 pub bg_color: ColorU,
2351 pub text_color: Option<ColorU>,
2353 pub radius: f32,
2355}
2356
2357#[must_use] pub fn get_selection_style(
2359 styled_dom: &StyledDom,
2360 node_id: Option<NodeId>,
2361 system_style: Option<&std::sync::Arc<azul_css::system::SystemStyle>>,
2362) -> SelectionStyle {
2363 let Some(node_id) = node_id else {
2364 return SelectionStyle::default();
2365 };
2366
2367 let node_data = &styled_dom.node_data.as_container()[node_id];
2368 let node_state = &StyledNodeState::default();
2369
2370 let default_bg = system_style
2372 .and_then(|ss| ss.colors.selection_background.as_option().copied())
2373 .unwrap_or(ColorU {
2374 r: 51,
2375 g: 153,
2376 b: 255, a: 128, });
2379
2380 let bg_color = styled_dom
2381 .css_property_cache
2382 .ptr
2383 .get_selection_background_color(node_data, &node_id, node_state)
2384 .and_then(|c| c.get_property().copied())
2385 .map_or(default_bg, |c| c.inner);
2386
2387 let default_text = system_style.and_then(|ss| ss.colors.selection_text.as_option().copied());
2389
2390 let text_color = styled_dom
2391 .css_property_cache
2392 .ptr
2393 .get_selection_color(node_data, &node_id, node_state)
2394 .and_then(|c| c.get_property().copied())
2395 .map(|c| c.inner)
2396 .or(default_text);
2397
2398 let radius = styled_dom
2399 .css_property_cache
2400 .ptr
2401 .get_selection_radius(node_data, &node_id, node_state)
2402 .and_then(|r| r.get_property().copied())
2403 .map_or(0.0, |r| r.inner.to_pixels_internal(0.0, DEFAULT_EM_SIZE, DEFAULT_EM_SIZE));
2404
2405 SelectionStyle {
2406 bg_color,
2407 text_color,
2408 radius,
2409 }
2410}
2411
2412#[derive(Debug, Clone, Copy)]
2414pub struct CaretStyle {
2415 pub color: ColorU,
2417 pub width: f32,
2419 pub animation_duration: u32,
2421}
2422
2423impl Default for CaretStyle {
2424 fn default() -> Self {
2425 Self {
2426 color: ColorU::BLACK,
2427 width: DEFAULT_CARET_WIDTH_PX,
2428 animation_duration: DEFAULT_CARET_BLINK_MS,
2429 }
2430 }
2431}
2432
2433#[must_use] pub fn get_caret_style(styled_dom: &StyledDom, node_id: Option<NodeId>) -> CaretStyle {
2435 let Some(node_id) = node_id else {
2436 return CaretStyle::default();
2437 };
2438
2439 let node_data = &styled_dom.node_data.as_container()[node_id];
2440 let node_state = &StyledNodeState::default();
2441
2442 let color = styled_dom
2443 .css_property_cache
2444 .ptr
2445 .get_caret_color(node_data, &node_id, node_state)
2446 .and_then(|c| c.get_property().copied())
2447 .map_or_else(|| {
2453 styled_dom
2454 .css_property_cache
2455 .ptr
2456 .get_text_color_or_default(node_data, &node_id, node_state)
2457 .inner
2458 }, |c| c.inner);
2459
2460 let width = styled_dom
2461 .css_property_cache
2462 .ptr
2463 .get_caret_width(node_data, &node_id, node_state)
2464 .and_then(|w| w.get_property().copied())
2465 .map_or(DEFAULT_CARET_WIDTH_PX, |w| w.inner.to_pixels_internal(0.0, DEFAULT_EM_SIZE, DEFAULT_EM_SIZE));
2466
2467 let animation_duration = styled_dom
2468 .css_property_cache
2469 .ptr
2470 .get_caret_animation_duration(node_data, &node_id, node_state)
2471 .and_then(|d| d.get_property().copied())
2472 .map_or(DEFAULT_CARET_BLINK_MS, |d| d.inner.inner);
2473
2474 CaretStyle {
2475 color,
2476 width,
2477 animation_duration,
2478 }
2479}
2480
2481#[must_use] pub fn get_scrollbar_info_from_layout(node: &LayoutNode) -> ScrollbarRequirements {
2493 node.scrollbar_info.unwrap_or_default()
2494}
2495
2496pub fn get_layout_scrollbar_width_px<T: ParsedFontTrait>(
2511 ctx: &crate::solver3::LayoutContext<'_, T>,
2512 dom_id: NodeId,
2513 styled_node_state: &StyledNodeState,
2514) -> f32 {
2515 let style = get_scrollbar_style(
2520 ctx.styled_dom,
2521 dom_id,
2522 styled_node_state,
2523 ctx.system_style.as_deref(),
2524 );
2525 style.reserve_width_px
2526}
2527
2528get_css_property!(
2529 get_display_property_internal,
2530 get_display,
2531 LayoutDisplay,
2532 CssPropertyType::Display,
2533 compact = get_display
2534);
2535
2536#[must_use] pub fn get_display_property(
2537 styled_dom: &StyledDom,
2538 dom_id: Option<NodeId>,
2539) -> MultiValue<LayoutDisplay> {
2540 let Some(id) = dom_id else {
2541 return MultiValue::Exact(LayoutDisplay::Inline);
2542 };
2543 let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
2544 get_display_property_internal(styled_dom, id, node_state)
2545}
2546
2547#[allow(clippy::match_same_arms)] #[must_use] pub const fn blockify_display(raw_display: LayoutDisplay) -> LayoutDisplay {
2554 match raw_display {
2555 LayoutDisplay::Inline => LayoutDisplay::Block,
2557 LayoutDisplay::InlineBlock => LayoutDisplay::Block,
2560 LayoutDisplay::InlineFlex => LayoutDisplay::Flex,
2561 LayoutDisplay::InlineTable => LayoutDisplay::Table,
2562 LayoutDisplay::InlineGrid => LayoutDisplay::Grid,
2563 LayoutDisplay::TableRowGroup
2566 | LayoutDisplay::TableColumn
2567 | LayoutDisplay::TableColumnGroup
2568 | LayoutDisplay::TableHeaderGroup
2569 | LayoutDisplay::TableFooterGroup
2570 | LayoutDisplay::TableRow
2571 | LayoutDisplay::TableCell
2572 | LayoutDisplay::TableCaption => LayoutDisplay::Block,
2573 other => other,
2575 }
2576}
2577
2578#[allow(clippy::fn_params_excessive_bools)]
2590#[must_use] pub fn get_computed_display(
2591 raw_display: LayoutDisplay,
2592 is_absolute_or_fixed: bool,
2593 is_floated: bool,
2594 is_root: bool,
2595 is_flex_grid_child: bool,
2596) -> LayoutDisplay {
2597 if raw_display == LayoutDisplay::None {
2598 return LayoutDisplay::None;
2599 }
2600 if is_absolute_or_fixed || is_floated || is_root || is_flex_grid_child {
2602 blockify_display(raw_display)
2603 } else {
2604 raw_display
2605 }
2606}
2607
2608#[must_use] pub fn get_vertical_align_for_node(
2613 styled_dom: &StyledDom,
2614 dom_id: NodeId,
2615) -> crate::text3::cache::VerticalAlign {
2616 let node_state = &styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
2617 let va = match get_vertical_align_property(styled_dom, dom_id, node_state) {
2618 MultiValue::Exact(v) => v,
2619 _ => StyleVerticalAlign::default(),
2620 };
2621 match va {
2622 StyleVerticalAlign::Baseline => crate::text3::cache::VerticalAlign::Baseline,
2623 StyleVerticalAlign::Top => crate::text3::cache::VerticalAlign::Top,
2624 StyleVerticalAlign::Middle => crate::text3::cache::VerticalAlign::Middle,
2625 StyleVerticalAlign::Bottom => crate::text3::cache::VerticalAlign::Bottom,
2626 StyleVerticalAlign::Sub => crate::text3::cache::VerticalAlign::Sub,
2627 StyleVerticalAlign::Superscript => crate::text3::cache::VerticalAlign::Super,
2628 StyleVerticalAlign::TextTop => crate::text3::cache::VerticalAlign::TextTop,
2629 StyleVerticalAlign::TextBottom => crate::text3::cache::VerticalAlign::TextBottom,
2630 StyleVerticalAlign::Percentage(p) => {
2632 let font_size = get_element_font_size(styled_dom, dom_id, node_state);
2633 let line_height = get_line_height_value(styled_dom, dom_id, node_state)
2640 .map_or(font_size * 1.2, |lh| {
2641 let n = lh.inner.normalized();
2642 if n < 0.0 { -n } else { n * font_size }
2643 });
2644 crate::text3::cache::VerticalAlign::Offset(p.normalized() * line_height)
2645 }
2646 StyleVerticalAlign::Length(l) => {
2648 let font_size = get_element_font_size(styled_dom, dom_id, node_state);
2649 let px = super::calc::resolve_pixel_value(&l, 0.0, font_size, font_size);
2657 crate::text3::cache::VerticalAlign::Offset(px)
2658 }
2659 }
2660}
2661
2662#[allow(clippy::cast_possible_truncation)] #[allow(clippy::too_many_lines, clippy::cognitive_complexity)] pub fn get_style_properties(
2668 styled_dom: &StyledDom,
2669 dom_id: NodeId,
2670 system_style: Option<&std::sync::Arc<azul_css::system::SystemStyle>>,
2671 viewport_size: PhysicalSize,
2672) -> StyleProperties {
2673 use azul_css::props::basic::{PhysicalSize, PropertyContext, ResolutionContext};
2674
2675 let node_data = &styled_dom.node_data.as_container()[dom_id];
2676 let node_state = &styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
2677 let cache = &styled_dom.css_property_cache.ptr;
2678
2679 let font_families = if node_state.is_normal() {
2682 cache
2683 .compact_cache
2684 .as_ref()
2685 .and_then(|cc| {
2686 let fh = cc.tier2b_text[dom_id.index()].font_family_hash;
2687 if fh == 0 {
2688 return None;
2689 }
2690 cc.font_hash_to_families.get(&fh).cloned()
2691 })
2692 .unwrap_or_else(|| {
2693 StyleFontFamilyVec::from_vec(vec![StyleFontFamily::System("serif".into())])
2694 })
2695 } else {
2696 cache
2697 .get_font_family(node_data, &dom_id, node_state)
2698 .and_then(|v| v.get_property().cloned())
2699 .unwrap_or_else(|| {
2700 StyleFontFamilyVec::from_vec(vec![StyleFontFamily::System("serif".into())])
2701 })
2702 };
2703
2704 let parent_font_size = get_parent_font_size(styled_dom, dom_id, node_state);
2710
2711 let root_font_size = get_root_font_size(styled_dom, node_state);
2712
2713 let font_size_context = ResolutionContext {
2715 element_font_size: DEFAULT_FONT_SIZE, parent_font_size,
2717 root_font_size,
2718 containing_block_size: PhysicalSize::new(0.0, 0.0),
2719 element_size: None,
2720 viewport_size,
2721 };
2722
2723 let font_size = {
2727 let mut fast_font_size: Option<f32> = None;
2732 let mut compact_said_inherit = false;
2733 if node_state.is_normal() {
2734 if let Some(ref cc) = cache.compact_cache {
2735 let raw = cc.get_font_size_raw(dom_id.index());
2736 if raw == azul_css::compact_cache::U32_SENTINEL
2737 || raw == azul_css::compact_cache::U32_INHERIT
2738 || raw == azul_css::compact_cache::U32_INITIAL
2739 {
2740 compact_said_inherit = true;
2741 } else if let Some(pv) = azul_css::compact_cache::decode_pixel_value_u32(raw) {
2742 fast_font_size = Some(
2743 pv.resolve_with_context(&font_size_context, PropertyContext::FontSize),
2744 );
2745 }
2746 }
2747 }
2748 fast_font_size.unwrap_or_else(|| {
2749 if compact_said_inherit {
2750 parent_font_size
2751 } else {
2752 cache
2753 .get_font_size(node_data, &dom_id, node_state)
2754 .and_then(|v| v.get_property().copied())
2755 .map_or(parent_font_size, |v| {
2756 v.inner
2757 .resolve_with_context(&font_size_context, PropertyContext::FontSize)
2758 })
2759 }
2760 })
2761 };
2762
2763 let color_from_cache = {
2764 let mut fast_color = None;
2766 if node_state.is_normal() {
2767 if let Some(ref cc) = cache.compact_cache {
2768 let raw = cc.get_text_color_raw(dom_id.index());
2769 if raw != 0 {
2770 fast_color = Some(ColorU {
2772 r: (raw >> 24) as u8,
2773 g: (raw >> 16) as u8,
2774 b: (raw >> 8) as u8,
2775 a: raw as u8,
2776 });
2777 }
2778 }
2779 }
2780 fast_color.or_else(|| {
2781 cache
2782 .get_text_color(node_data, &dom_id, node_state)
2783 .and_then(|v| v.get_property().copied())
2784 .map(|v| v.inner)
2785 })
2786 };
2787
2788 let color = color_from_cache.unwrap_or(ColorU::BLACK);
2794
2795 let line_height = {
2797 let mut fast_lh = None;
2806 let mut sentinel_normal = false;
2807 if node_state.is_normal() {
2808 if let Some(ref cc) = cache.compact_cache {
2809 if let Some(normalized) = cc.get_line_height(dom_id.index()) {
2810 let n = normalized / 100.0;
2817 fast_lh = Some(crate::text3::cache::LineHeight::Px(
2818 if n < 0.0 { -n } else { n * font_size },
2819 ));
2820 } else {
2821 sentinel_normal = true;
2823 }
2824 }
2825 }
2826 if sentinel_normal {
2827 crate::text3::cache::LineHeight::Normal
2828 } else {
2829 fast_lh.unwrap_or_else(|| {
2830 cache
2831 .get_line_height(node_data, &dom_id, node_state)
2832 .and_then(|v| v.get_property().copied())
2833 .map_or(crate::text3::cache::LineHeight::Normal, |v| {
2834 let n = v.inner.normalized();
2837 crate::text3::cache::LineHeight::Px(if n < 0.0 { -n } else { n * font_size })
2838 })
2839 })
2840 }
2841 };
2842
2843 let display = match get_display_property(styled_dom, Some(dom_id)) {
2854 MultiValue::Exact(v) => v,
2855 _ => LayoutDisplay::Inline,
2856 };
2857
2858 let (background_color, background_content, border) =
2861 if matches!(display, LayoutDisplay::Inline | LayoutDisplay::InlineBlock) {
2862 let bg = get_background_color(styled_dom, dom_id, node_state);
2863 let bg_color = if bg.a > 0 { Some(bg) } else { None };
2864
2865 let bg_contents = get_background_contents(styled_dom, dom_id, node_state);
2867
2868 let border_info = get_border_info(styled_dom, dom_id, node_state);
2870 let inline_border =
2871 get_inline_border_info(styled_dom, dom_id, node_state, &border_info, viewport_size);
2872
2873 (bg_color, bg_contents, inline_border)
2874 } else {
2875 (None, Vec::new(), None)
2878 };
2879
2880 let font_weight = match get_font_weight_property(styled_dom, dom_id, node_state) {
2882 MultiValue::Exact(v) => v,
2883 _ => StyleFontWeight::Normal,
2884 };
2885
2886 let font_style = match get_font_style_property(styled_dom, dom_id, node_state) {
2888 MultiValue::Exact(v) => v,
2889 _ => StyleFontStyle::Normal,
2890 };
2891
2892 let fc_weight = super::fc::convert_font_weight(font_weight);
2894 let fc_style = super::fc::convert_font_style(font_style);
2895
2896 let font_stack = {
2899 let font_ref = (0..font_families.len()).find_map(|i| match font_families.get(i).unwrap() {
2900 StyleFontFamily::Ref(r) => Some(r.clone()),
2901 _ => None,
2902 });
2903
2904 font_ref.map_or_else(
2905 || {
2906 let platform = system_style.map(|ss| &ss.platform);
2911 FontStack::Stack(build_font_selector_stack(
2912 &font_families,
2913 platform,
2914 fc_weight,
2915 fc_style,
2916 ))
2917 },
2918 FontStack::Ref,
2919 )
2920 };
2921
2922 let letter_spacing = {
2924 let mut fast_ls = None;
2926 if node_state.is_normal() {
2927 if let Some(ref cc) = cache.compact_cache {
2928 if let Some(px_val) = cc.get_letter_spacing(dom_id.index()) {
2929 fast_ls = Some(crate::text3::cache::Spacing::PxF(px_val));
2930 }
2931 }
2932 }
2933 fast_ls.unwrap_or_else(|| {
2934 cache
2935 .get_letter_spacing(node_data, &dom_id, node_state)
2936 .and_then(|v| v.get_property().copied())
2937 .map(|v| {
2938 let px_value = v
2939 .inner
2940 .resolve_with_context(&font_size_context, PropertyContext::FontSize);
2941 crate::text3::cache::Spacing::PxF(px_value)
2942 })
2943 .unwrap_or_default()
2944 })
2945 };
2946
2947 let word_spacing = {
2949 let mut fast_ws = None;
2951 if node_state.is_normal() {
2952 if let Some(ref cc) = cache.compact_cache {
2953 if let Some(px_val) = cc.get_word_spacing(dom_id.index()) {
2954 fast_ws = Some(crate::text3::cache::Spacing::PxF(px_val));
2955 }
2956 }
2957 }
2958 fast_ws.unwrap_or_else(|| {
2959 cache
2960 .get_word_spacing(node_data, &dom_id, node_state)
2961 .and_then(|v| v.get_property().copied())
2962 .map(|v| {
2963 let px_value = v
2964 .inner
2965 .resolve_with_context(&font_size_context, PropertyContext::FontSize);
2966 crate::text3::cache::Spacing::PxF(px_value)
2967 })
2968 .unwrap_or_default()
2969 })
2970 };
2971
2972 let text_decoration = {
2979 let mut skip_walk = false;
2980 if node_state.is_normal() {
2981 if let Some(ref cc) = cache.compact_cache {
2982 if !cc.has_text_decoration(dom_id.index()) {
2983 skip_walk = true;
2984 }
2985 }
2986 }
2987 if skip_walk {
2988 crate::text3::cache::TextDecoration::default()
2989 } else {
2990 cache
2991 .get_text_decoration(node_data, &dom_id, node_state)
2992 .and_then(|v| v.get_property().copied())
2993 .map(crate::text3::cache::TextDecoration::from_css)
2994 .unwrap_or_default()
2995 }
2996 };
2997
2998 let tab_size = {
3010 let mut fast_tab = None;
3011 if node_state.is_normal() {
3012 if let Some(ref cc) = cache.compact_cache {
3013 let raw = cc.get_tab_size_raw(dom_id.index());
3014 if raw < azul_css::compact_cache::I16_SENTINEL_THRESHOLD {
3015 fast_tab = Some(f32::from(raw) / 10.0);
3016 } else {
3017 fast_tab = Some(8.0);
3019 }
3020 }
3021 }
3022 fast_tab.unwrap_or_else(|| {
3023 cache
3024 .get_tab_size(node_data, &dom_id, node_state)
3025 .and_then(|v| v.get_property().copied())
3026 .map_or(DEFAULT_TAB_SIZE, |v| v.inner.number.get())
3027 })
3028 };
3029
3030 let text_transform = cache
3034 .get_text_transform(node_data, &dom_id, node_state)
3035 .and_then(|v| v.get_property().copied())
3036 .map(|t| {
3037 use azul_css::props::style::text::StyleTextTransform as Css;
3038 use crate::text3::cache::TextTransform as T3;
3039 match t {
3040 Css::None => T3::None,
3041 Css::Uppercase => T3::Uppercase,
3042 Css::Lowercase => T3::Lowercase,
3043 Css::Capitalize => T3::Capitalize,
3044 Css::FullWidth => T3::FullWidth,
3045 }
3046 })
3047 .unwrap_or_default();
3048
3049 StyleProperties {
3050 font_stack,
3051 font_size_px: font_size,
3052 color,
3053 background_color,
3054 background_content,
3055 border,
3056 line_height,
3057 letter_spacing,
3058 word_spacing,
3059 text_decoration,
3060 tab_size,
3061 text_transform,
3062 vertical_align: get_vertical_align_for_node(styled_dom, dom_id),
3067 ..Default::default()
3071 }
3072}
3073
3074#[must_use] pub fn get_list_style_type(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> StyleListStyleType {
3075 let Some(id) = dom_id else {
3076 return StyleListStyleType::default();
3077 };
3078 let node_data = &styled_dom.node_data.as_container()[id];
3079 let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
3080 styled_dom
3081 .css_property_cache
3082 .ptr
3083 .get_list_style_type(node_data, &id, node_state)
3084 .and_then(|v| v.get_property().copied())
3085 .unwrap_or_default()
3086}
3087
3088#[must_use] pub fn get_list_style_position(
3089 styled_dom: &StyledDom,
3090 dom_id: Option<NodeId>,
3091) -> StyleListStylePosition {
3092 let Some(id) = dom_id else {
3093 return StyleListStylePosition::default();
3094 };
3095 let node_data = &styled_dom.node_data.as_container()[id];
3096 let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
3097 styled_dom
3098 .css_property_cache
3099 .ptr
3100 .get_list_style_position(node_data, &id, node_state)
3101 .and_then(|v| v.get_property().copied())
3102 .unwrap_or_default()
3103}
3104
3105use azul_css::props::layout::{
3108 LayoutInsetBottom, LayoutLeft, LayoutMarginBottom, LayoutMarginLeft, LayoutMarginRight,
3109 LayoutMarginTop, LayoutMaxHeight, LayoutMaxWidth, LayoutMinHeight, LayoutMinWidth,
3110 LayoutPaddingBottom, LayoutPaddingLeft, LayoutPaddingRight, LayoutPaddingTop, LayoutRight,
3111 LayoutTop,
3112};
3113
3114get_css_property_pixel!(
3116 get_css_left,
3117 get_left,
3118 CssPropertyType::Left,
3119 compact_i16 = get_left
3120);
3121get_css_property_pixel!(
3122 get_css_right,
3123 get_right,
3124 CssPropertyType::Right,
3125 compact_i16 = get_right
3126);
3127get_css_property_pixel!(
3128 get_css_top,
3129 get_top,
3130 CssPropertyType::Top,
3131 compact_i16 = get_top
3132);
3133get_css_property_pixel!(
3134 get_css_bottom,
3135 get_bottom,
3136 CssPropertyType::Bottom,
3137 compact_i16 = get_bottom
3138);
3139
3140get_css_property_pixel!(
3142 get_css_margin_left,
3143 get_margin_left,
3144 CssPropertyType::MarginLeft,
3145 compact_i16 = get_margin_left_raw
3146);
3147get_css_property_pixel!(
3148 get_css_margin_right,
3149 get_margin_right,
3150 CssPropertyType::MarginRight,
3151 compact_i16 = get_margin_right_raw
3152);
3153get_css_property_pixel!(
3154 get_css_margin_top,
3155 get_margin_top,
3156 CssPropertyType::MarginTop,
3157 compact_i16 = get_margin_top_raw
3158);
3159get_css_property_pixel!(
3160 get_css_margin_bottom,
3161 get_margin_bottom,
3162 CssPropertyType::MarginBottom,
3163 compact_i16 = get_margin_bottom_raw
3164);
3165
3166get_css_property_pixel!(
3168 get_css_padding_left,
3169 get_padding_left,
3170 CssPropertyType::PaddingLeft,
3171 compact_i16 = get_padding_left_raw
3172);
3173get_css_property_pixel!(
3174 get_css_padding_right,
3175 get_padding_right,
3176 CssPropertyType::PaddingRight,
3177 compact_i16 = get_padding_right_raw
3178);
3179get_css_property_pixel!(
3180 get_css_padding_top,
3181 get_padding_top,
3182 CssPropertyType::PaddingTop,
3183 compact_i16 = get_padding_top_raw
3184);
3185get_css_property_pixel!(
3186 get_css_padding_bottom,
3187 get_padding_bottom,
3188 CssPropertyType::PaddingBottom,
3189 compact_i16 = get_padding_bottom_raw
3190);
3191
3192get_css_property!(
3194 get_css_min_width,
3195 get_min_width,
3196 LayoutMinWidth,
3197 CssPropertyType::MinWidth,
3198 compact_u32_struct = get_min_width_raw
3199);
3200
3201get_css_property!(
3202 get_css_min_height,
3203 get_min_height,
3204 LayoutMinHeight,
3205 CssPropertyType::MinHeight,
3206 compact_u32_struct = get_min_height_raw
3207);
3208
3209get_css_property!(
3210 get_css_max_width,
3211 get_max_width,
3212 LayoutMaxWidth,
3213 CssPropertyType::MaxWidth,
3214 compact_u32_struct = get_max_width_raw
3215);
3216
3217get_css_property!(
3218 get_css_max_height,
3219 get_max_height,
3220 LayoutMaxHeight,
3221 CssPropertyType::MaxHeight,
3222 compact_u32_struct = get_max_height_raw
3223);
3224
3225get_css_property_pixel!(
3227 get_css_border_left_width,
3228 get_border_left_width,
3229 CssPropertyType::BorderLeftWidth,
3230 compact_i16 = get_border_left_width_raw
3231);
3232get_css_property_pixel!(
3233 get_css_border_right_width,
3234 get_border_right_width,
3235 CssPropertyType::BorderRightWidth,
3236 compact_i16 = get_border_right_width_raw
3237);
3238get_css_property_pixel!(
3239 get_css_border_top_width,
3240 get_border_top_width,
3241 CssPropertyType::BorderTopWidth,
3242 compact_i16 = get_border_top_width_raw
3243);
3244get_css_property_pixel!(
3245 get_css_border_bottom_width,
3246 get_border_bottom_width,
3247 CssPropertyType::BorderBottomWidth,
3248 compact_i16 = get_border_bottom_width_raw
3249);
3250
3251#[must_use] pub fn get_break_before(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> PageBreak {
3255 let Some(id) = dom_id else {
3256 return PageBreak::Auto;
3257 };
3258 let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
3259 if node_state.is_normal() {
3261 if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
3262 if !cc.has_break(id.index()) {
3263 return PageBreak::Auto;
3264 }
3265 }
3266 }
3267 let node_data = &styled_dom.node_data.as_container()[id];
3268 styled_dom
3269 .css_property_cache
3270 .ptr
3271 .get_break_before(node_data, &id, node_state)
3272 .and_then(|v| v.get_property().copied())
3273 .unwrap_or(PageBreak::Auto)
3274}
3275
3276#[must_use] pub fn get_break_after(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> PageBreak {
3278 let Some(id) = dom_id else {
3279 return PageBreak::Auto;
3280 };
3281 let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
3282 if node_state.is_normal() {
3283 if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
3284 if !cc.has_break(id.index()) {
3285 return PageBreak::Auto;
3286 }
3287 }
3288 }
3289 let node_data = &styled_dom.node_data.as_container()[id];
3290 styled_dom
3291 .css_property_cache
3292 .ptr
3293 .get_break_after(node_data, &id, node_state)
3294 .and_then(|v| v.get_property().copied())
3295 .unwrap_or(PageBreak::Auto)
3296}
3297
3298#[must_use] pub const fn is_forced_page_break(page_break: PageBreak) -> bool {
3300 matches!(
3301 page_break,
3302 PageBreak::Always
3303 | PageBreak::Page
3304 | PageBreak::Left
3305 | PageBreak::Right
3306 | PageBreak::Recto
3307 | PageBreak::Verso
3308 | PageBreak::All
3309 )
3310}
3311
3312#[must_use] pub fn get_break_inside(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> BreakInside {
3314 let Some(id) = dom_id else {
3315 return BreakInside::Auto;
3316 };
3317 let node_data = &styled_dom.node_data.as_container()[id];
3318 let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
3319 styled_dom
3320 .css_property_cache
3321 .ptr
3322 .get_break_inside(node_data, &id, node_state)
3323 .and_then(|v| v.get_property().copied())
3324 .unwrap_or(BreakInside::Auto)
3325}
3326
3327#[must_use] pub fn get_orphans(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> u32 {
3329 let Some(id) = dom_id else {
3330 return 2; };
3332 let node_data = &styled_dom.node_data.as_container()[id];
3333 let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
3334 styled_dom
3335 .css_property_cache
3336 .ptr
3337 .get_orphans(node_data, &id, node_state)
3338 .and_then(|v| v.get_property().copied())
3339 .map_or(2, |o| o.inner)
3340}
3341
3342#[must_use] pub fn get_widows(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> u32 {
3344 let Some(id) = dom_id else {
3345 return 2; };
3347 let node_data = &styled_dom.node_data.as_container()[id];
3348 let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
3349 styled_dom
3350 .css_property_cache
3351 .ptr
3352 .get_widows(node_data, &id, node_state)
3353 .and_then(|v| v.get_property().copied())
3354 .map_or(2, |w| w.inner)
3355}
3356
3357#[must_use] pub fn get_box_decoration_break(
3359 styled_dom: &StyledDom,
3360 dom_id: Option<NodeId>,
3361) -> BoxDecorationBreak {
3362 let Some(id) = dom_id else {
3363 return BoxDecorationBreak::Slice;
3364 };
3365 let node_data = &styled_dom.node_data.as_container()[id];
3366 let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
3367 styled_dom
3368 .css_property_cache
3369 .ptr
3370 .get_box_decoration_break(node_data, &id, node_state)
3371 .and_then(|v| v.get_property().copied())
3372 .unwrap_or(BoxDecorationBreak::Slice)
3373}
3374
3375#[must_use] pub const fn is_avoid_page_break(page_break: &PageBreak) -> bool {
3379 matches!(page_break, PageBreak::Avoid | PageBreak::AvoidPage)
3380}
3381
3382#[must_use] pub const fn is_avoid_break_inside(break_inside: &BreakInside) -> bool {
3384 matches!(
3385 break_inside,
3386 BreakInside::Avoid | BreakInside::AvoidPage | BreakInside::AvoidColumn
3387 )
3388}
3389
3390use std::collections::HashMap;
3393
3394use rust_fontconfig::{
3395 FcFontCache, FcWeight, FontFallbackChain, PatternMatch, UnicodeRange,
3396 DEFAULT_UNICODE_FALLBACK_SCRIPTS,
3397};
3398
3399use crate::text3::cache::{FontChainKey, FontChainKeyOrRef, FontSelector, FontStack, FontStyle};
3400
3401#[allow(clippy::option_if_let_else)]
3417fn build_font_selector_stack(
3418 font_families: &StyleFontFamilyVec,
3419 platform: Option<&azul_css::system::Platform>,
3420 fc_weight: FcWeight,
3421 fc_style: FontStyle,
3422) -> Vec<FontSelector> {
3423 let mut stack = Vec::with_capacity(font_families.len() + 3);
3424
3425 for i in 0..font_families.len() {
3426 let family = font_families.get(i).unwrap();
3427 if matches!(family, StyleFontFamily::Ref(_)) {
3428 continue;
3429 }
3430 if let StyleFontFamily::SystemType(system_type) = family {
3431 let current;
3432 let platform = if let Some(p) = platform { p } else {
3433 current = azul_css::system::Platform::current();
3434 ¤t
3435 };
3436 let font_names = system_type.get_fallback_chain(platform);
3437 let system_weight = if system_type.is_bold() {
3438 FcWeight::Bold
3439 } else {
3440 fc_weight
3441 };
3442 let system_style = if system_type.is_italic() {
3443 FontStyle::Italic
3444 } else {
3445 fc_style
3446 };
3447 for font_name in font_names {
3448 stack.push(FontSelector {
3449 family: font_name.to_string(),
3450 weight: system_weight,
3451 style: system_style,
3452 unicode_ranges: Vec::new(),
3453 });
3454 }
3455 } else {
3456 stack.push(FontSelector {
3457 family: family.as_query_string(),
3461 weight: fc_weight,
3462 style: fc_style,
3463 unicode_ranges: Vec::new(),
3464 });
3465 }
3466 }
3467
3468 for fallback in &["sans-serif", "serif", "monospace"] {
3469 if !stack
3470 .iter()
3471 .any(|f| f.family.eq_ignore_ascii_case(fallback))
3472 {
3473 stack.push(FontSelector {
3474 family: (*fallback).to_string(),
3475 weight: FcWeight::Normal,
3476 style: FontStyle::Normal,
3477 unicode_ranges: Vec::new(),
3478 });
3479 }
3480 }
3481
3482 stack
3483}
3484
3485#[derive(Debug, Clone)]
3488pub struct CollectedFontStacks {
3489 pub font_stacks: Vec<Vec<FontSelector>>,
3491 pub hash_to_index: HashMap<u64, usize>,
3493 pub font_refs: HashMap<usize, azul_css::props::basic::font::FontRef>,
3496}
3497
3498#[derive(Debug, Clone, Default)]
3501pub struct ResolvedFontChains {
3502 pub chains: HashMap<FontChainKeyOrRef, FontFallbackChain>,
3506 pub unresolved_families: std::collections::BTreeSet<String>,
3517 pub last_resort_chains: usize,
3521}
3522
3523impl ResolvedFontChains {
3524 #[must_use] pub fn get(&self, key: &FontChainKeyOrRef) -> Option<&FontFallbackChain> {
3526 self.chains.get(key)
3527 }
3528
3529 #[must_use] pub fn get_by_chain_key(&self, key: &FontChainKey) -> Option<&FontFallbackChain> {
3531 self.chains.get(&FontChainKeyOrRef::Chain(key.clone()))
3532 }
3533
3534 #[must_use] pub fn get_for_font_stack(&self, font_stack: &[FontSelector]) -> Option<&FontFallbackChain> {
3536 let key = FontChainKeyOrRef::Chain(FontChainKey::from_selectors(font_stack));
3537 self.chains.get(&key)
3538 }
3539
3540 #[must_use] pub fn get_for_font_ref(&self, ptr: usize) -> Option<&FontFallbackChain> {
3542 self.chains.get(&FontChainKeyOrRef::Ref(ptr))
3543 }
3544
3545 #[must_use] pub fn into_inner(self) -> HashMap<FontChainKeyOrRef, FontFallbackChain> {
3549 self.chains
3550 }
3551
3552 #[must_use] pub fn into_fontconfig_chains(self) -> HashMap<FontChainKey, FontFallbackChain> {
3557 let mut out: HashMap<FontChainKey, FontFallbackChain> = HashMap::new();
3561 if self.chains.is_empty() {
3562 return out;
3563 }
3564 for (key, chain) in self.chains {
3565 if let FontChainKeyOrRef::Chain(chain_key) = key {
3566 out.insert(chain_key, chain);
3567 }
3568 }
3569 out
3570 }
3571
3572 #[must_use] pub fn len(&self) -> usize {
3574 self.chains.len()
3575 }
3576
3577 #[must_use] pub fn is_empty(&self) -> bool {
3579 self.chains.is_empty()
3580 }
3581
3582 #[must_use] pub fn font_refs_len(&self) -> usize {
3584 self.chains.keys().filter(|k| k.is_ref()).count()
3585 }
3586}
3587
3588#[allow(clippy::cast_possible_truncation)] #[allow(clippy::too_many_lines)] #[must_use] pub fn collect_font_stacks_from_styled_dom(
3602 styled_dom: &StyledDom,
3603 platform: &azul_css::system::Platform,
3604) -> CollectedFontStacks {
3605 use azul_css::compact_cache::{
3606 FONT_STYLE_MASK, FONT_STYLE_SHIFT, FONT_WEIGHT_MASK, FONT_WEIGHT_SHIFT,
3607 };
3608
3609 let mut font_stacks = Vec::new();
3610 let mut hash_to_index: HashMap<u64, usize> = HashMap::new();
3611 let mut font_refs: HashMap<usize, azul_css::props::basic::font::FontRef> = HashMap::new();
3612
3613 let node_data = styled_dom.node_data.as_container();
3614 let cache = &styled_dom.css_property_cache.ptr;
3615 let Some(compact) = cache.compact_cache.as_ref() else {
3616 return CollectedFontStacks {
3617 font_stacks,
3618 hash_to_index,
3619 font_refs,
3620 };
3621 };
3622
3623 let mut unique_font_keys: HashMap<(u64, u8, u8), usize> = HashMap::new();
3632 let node_count = node_data.internal.len();
3633
3634 if node_count > 1 {
3637 let p1 = (&raw const node_data.internal[1].node_type).cast::<u8>();
3638 let p0 = (&raw const node_data.internal[0].node_type).cast::<u8>();
3639 unsafe {
3640 crate::az_mark(0x606D0_u32, u32::from(core::ptr::read(p1)));
3641 crate::az_mark(0x606D4_u32, u32::from(core::ptr::read(p1.add(1))));
3642 crate::az_mark(0x606D8_u32, u32::from(core::ptr::read(p1.add(2))));
3643 crate::az_mark(0x606DC_u32, u32::from(core::ptr::read(p1.add(4))));
3644 crate::az_mark(0x606E0_u32, u32::from(core::ptr::read(p0)));
3645 }
3646 }
3647 for i in 0..node_count {
3648 let nt_disc = unsafe {
3655 core::ptr::read((&raw const node_data.internal[i].node_type).cast::<u8>())
3656 };
3657 let is_text = nt_disc == 177
3658 || matches!(node_data.internal[i].node_type, NodeType::Text(_));
3659 if !is_text {
3660 continue;
3661 }
3662 let fh = compact.tier2b_text[i].font_family_hash;
3663 let t1 = compact.tier1_enums[i];
3664 let weight_bits = ((t1 >> FONT_WEIGHT_SHIFT) & FONT_WEIGHT_MASK) as u8;
3665 let style_bits = ((t1 >> FONT_STYLE_SHIFT) & FONT_STYLE_MASK) as u8;
3666 let key = (fh, weight_bits, style_bits);
3667 unique_font_keys.entry(key).or_insert(i);
3668 }
3669
3670 {
3676 let mut raw_text = 0u32;
3677 for i in 0..node_count {
3678 let nt_ptr = (&raw const node_data.internal[i].node_type).cast::<u8>();
3680 let disc = unsafe { core::ptr::read_volatile(nt_ptr) };
3681 if disc != unsafe { core::ptr::read_volatile((&raw const node_data.internal[0].node_type).cast::<u8>()) } {
3683 raw_text += 1;
3684 }
3685 }
3686 unsafe {
3687 crate::az_mark(0x606C0_u32, (0x5E5E_0003_u32));
3688 crate::az_mark(0x606C4_u32, (node_count as u32));
3689 crate::az_mark(0x606C8_u32, (unique_font_keys.len() as u32));
3690 crate::az_mark(0x606CC_u32, (raw_text));
3691 }
3692 }
3693
3694 let styled_nodes = styled_dom.styled_nodes.as_container();
3697
3698 for (&(fh, _wb, _sb), &repr_idx) in &unique_font_keys {
3699 let Some(dom_id) = NodeId::from_usize(repr_idx) else {
3700 continue;
3701 };
3702 let node_state = &styled_nodes[dom_id].styled_node_state;
3703
3704 let font_families = compact
3708 .font_hash_to_families
3709 .get(&fh)
3710 .cloned()
3711 .unwrap_or_else(|| {
3712 StyleFontFamilyVec::from_vec(vec![StyleFontFamily::System("serif".into())])
3713 });
3714
3715 if let Some(StyleFontFamily::Ref(font_ref)) = font_families.get(0) {
3717 let ptr = font_ref.parsed as usize;
3718 font_refs.entry(ptr).or_insert_with(|| font_ref.clone());
3719 continue;
3720 }
3721
3722 let font_weight = match get_font_weight_property(styled_dom, dom_id, node_state) {
3723 MultiValue::Exact(v) => v,
3724 _ => StyleFontWeight::Normal,
3725 };
3726 let font_style = match get_font_style_property(styled_dom, dom_id, node_state) {
3727 MultiValue::Exact(v) => v,
3728 _ => StyleFontStyle::Normal,
3729 };
3730
3731 let fc_weight = super::fc::convert_font_weight(font_weight);
3732 let fc_style = super::fc::convert_font_style(font_style);
3733
3734 let font_stack =
3735 build_font_selector_stack(&font_families, Some(platform), fc_weight, fc_style);
3736
3737 if font_stack.is_empty() {
3738 continue;
3739 }
3740
3741 let key = FontChainKey::from_selectors(&font_stack);
3742 let hash = {
3743 use std::hash::{Hash, Hasher};
3744 let mut hasher = std::collections::hash_map::DefaultHasher::new();
3745 key.hash(&mut hasher);
3746 hasher.finish()
3747 };
3748
3749 hash_to_index.entry(hash).or_insert_with(|| {
3750 let idx = font_stacks.len();
3751 font_stacks.push(font_stack);
3752 idx
3753 });
3754 }
3755
3756 CollectedFontStacks {
3757 font_stacks,
3758 hash_to_index,
3759 font_refs,
3760 }
3761}
3762
3763#[must_use] pub fn collect_used_codepoints(styled_dom: &StyledDom) -> std::collections::BTreeSet<u32> {
3786 let mut out = std::collections::BTreeSet::new();
3787 let node_data = styled_dom.node_data.as_container();
3788 for node in node_data.internal {
3789 let NodeType::Text(s) = &node.node_type else {
3790 continue;
3791 };
3792 for c in s.as_str().chars() {
3793 let cp = c as u32;
3794 if cp >= 0x80 {
3795 out.insert(cp);
3796 }
3797 }
3798 }
3799 out
3800}
3801
3802#[must_use] pub fn collect_used_codepoints_all(styled_dom: &StyledDom) -> std::collections::BTreeSet<char> {
3816 let mut out = std::collections::BTreeSet::new();
3817 let node_data = styled_dom.node_data.as_container();
3818 for node in node_data.internal {
3819 let NodeType::Text(s) = &node.node_type else {
3820 continue;
3821 };
3822 for c in s.as_str().chars() {
3823 out.insert(c);
3824 }
3825 }
3826 out
3827}
3828
3829pub fn prune_chain_to_used_chars(
3851 chain: &mut FontFallbackChain,
3852 used_chars: &std::collections::BTreeSet<u32>,
3853) {
3854 fn fm_covers(fm: &rust_fontconfig::FontMatch, cp: u32) -> bool {
3855 fm.unicode_ranges
3856 .iter()
3857 .any(|r| cp >= r.start && cp <= r.end)
3858 }
3859
3860 for group in &mut chain.css_fallbacks {
3861 if group.fonts.is_empty() {
3862 continue;
3863 }
3864 let mut needed: Vec<u32> = used_chars.iter().copied().collect();
3867 needed.retain(|&cp| !fm_covers(&group.fonts[0], cp));
3868 let mut keep = 1;
3869 for fm in group.fonts.iter().skip(1) {
3870 if needed.is_empty() {
3871 break;
3872 }
3873 keep += 1;
3874 needed.retain(|&cp| !fm_covers(fm, cp));
3875 }
3876 group.fonts.truncate(keep);
3877 }
3878
3879 chain
3880 .unicode_fallbacks
3881 .retain(|fm| used_chars.iter().any(|&cp| fm_covers(fm, cp)));
3882}
3883
3884#[must_use] pub fn scripts_present_in_styled_dom(styled_dom: &StyledDom) -> Vec<UnicodeRange> {
3899 let scripts = DEFAULT_UNICODE_FALLBACK_SCRIPTS;
3900 let mut seen = vec![false; scripts.len()];
3901 let mut hits = 0usize;
3902 let node_data = styled_dom.node_data.as_container();
3903 'outer: for node in node_data.internal {
3904 let text: &str = match &node.node_type {
3905 NodeType::Text(s) => s.as_str(),
3906 _ => continue,
3907 };
3908 for c in text.chars() {
3909 let cp = c as u32;
3910 if cp < 0x0400 {
3914 continue;
3915 }
3916 for (idx, r) in scripts.iter().enumerate() {
3917 if !seen[idx] && cp >= r.start && cp <= r.end {
3918 seen[idx] = true;
3919 hits += 1;
3920 if hits == scripts.len() {
3921 break 'outer;
3922 }
3923 break;
3924 }
3925 }
3926 }
3927 }
3928 scripts
3929 .iter()
3930 .enumerate()
3931 .filter_map(|(i, r)| if seen[i] { Some(*r) } else { None })
3932 .collect()
3933}
3934
3935#[must_use] pub fn resolve_font_chains(
3950 collected: &CollectedFontStacks,
3951 fc_cache: &FcFontCache,
3952 scripts_hint: Option<&[UnicodeRange]>,
3953) -> ResolvedFontChains {
3954 resolve_font_chains_with_registry(collected, fc_cache, None, scripts_hint, &HashMap::new())
3955}
3956
3957fn split_memory_matches(
3969 font_families: &[String],
3970 memory_families: &HashMap<String, Vec<crate::text3::cache::MemoryFace>>,
3971 weight: FcWeight,
3972 italic: bool,
3973 oblique: bool,
3974) -> (Vec<rust_fontconfig::CssFallbackGroup>, Vec<String>) {
3975 let mut groups = Vec::new();
3976 let mut disk = Vec::new();
3977 for family in font_families {
3978 let norm = rust_fontconfig::utils::normalize_family_name(family);
3979 if let Some(face) = memory_families
3980 .get(&norm)
3981 .and_then(|faces| pick_memory_face(faces, weight, italic, oblique))
3982 {
3983 groups.push(rust_fontconfig::CssFallbackGroup {
3984 css_name: family.clone(),
3985 fonts: vec![face.font_match.clone()],
3986 });
3987 } else {
3988 disk.push(family.clone());
3989 }
3990 }
3991 (groups, disk)
3992}
3993
3994fn pick_memory_face(
4004 faces: &[crate::text3::cache::MemoryFace],
4005 weight: FcWeight,
4006 italic: bool,
4007 oblique: bool,
4008) -> Option<&crate::text3::cache::MemoryFace> {
4009 if faces.is_empty() {
4010 return None;
4011 }
4012 let want_slanted = italic || oblique;
4013 let slant_pool: Vec<&crate::text3::cache::MemoryFace> = faces
4016 .iter()
4017 .filter(|f| (f.italic || f.oblique) == want_slanted)
4018 .collect();
4019 let pool: Vec<&crate::text3::cache::MemoryFace> = if slant_pool.is_empty() {
4020 faces.iter().collect()
4021 } else {
4022 slant_pool
4023 };
4024 let req = f32::from(weight as u16);
4026 if let Some(vf) = pool
4027 .iter()
4028 .copied()
4029 .find(|f| f.weight_axis.is_some_and(|(min, max)| req >= min && req <= max))
4030 {
4031 return Some(vf);
4032 }
4033 let avail: Vec<FcWeight> = pool.iter().map(|f| f.weight).collect();
4035 let best = weight.find_best_match(&avail).unwrap_or(weight);
4036 pool.iter()
4037 .copied()
4038 .find(|f| f.weight == best)
4039 .or_else(|| pool.first().copied())
4040}
4041
4042#[allow(clippy::implicit_hasher)] #[must_use] pub fn resolve_font_chains_with_registry(
4058 collected: &CollectedFontStacks,
4059 fc_cache: &FcFontCache,
4060 registry: Option<&rust_fontconfig::registry::FcFontRegistry>,
4061 scripts_hint: Option<&[UnicodeRange]>,
4062 memory_families: &HashMap<String, Vec<crate::text3::cache::MemoryFace>>,
4063) -> ResolvedFontChains {
4064 let mut chains = HashMap::new();
4065 let mut unresolved: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
4066
4067 for font_stack in &collected.font_stacks {
4069 if font_stack.is_empty() {
4070 continue;
4071 }
4072
4073 let canonical_key = FontChainKey::from_selectors(font_stack);
4078 let font_families = canonical_key.font_families.clone();
4079
4080 let weight = font_stack[0].weight;
4081 let is_italic = font_stack[0].style == FontStyle::Italic;
4082 let is_oblique = font_stack[0].style == FontStyle::Oblique;
4083
4084 let cache_key = FontChainKeyOrRef::Chain(FontChainKey {
4085 font_families: font_families.clone(),
4086 weight,
4087 italic: is_italic,
4088 oblique: is_oblique,
4089 });
4090
4091 if chains.contains_key(&cache_key) {
4093 continue;
4094 }
4095
4096 let italic = if is_italic {
4101 PatternMatch::True
4102 } else {
4103 PatternMatch::False
4104 };
4105 let oblique = if is_oblique {
4106 PatternMatch::True
4107 } else {
4108 PatternMatch::False
4109 };
4110
4111 let (mem_groups, disk_families) =
4115 split_memory_matches(&font_families, memory_families, weight, is_italic, is_oblique);
4116
4117 let mut chain = if disk_families.is_empty() {
4120 FontFallbackChain {
4121 css_fallbacks: Vec::new(),
4122 unicode_fallbacks: Vec::new(),
4123 original_stack: font_families.clone(),
4124 }
4125 } else {
4126 registry.map_or_else(
4127 || {
4128 let mut trace = Vec::new();
4129 fc_cache.resolve_font_chain_with_scripts(
4130 &disk_families,
4131 weight,
4132 italic,
4133 oblique,
4134 scripts_hint,
4135 &mut trace,
4136 )
4137 },
4138 |reg| {
4139 reg.request_and_resolve_with_scripts(
4140 &disk_families,
4141 weight,
4142 italic,
4143 oblique,
4144 scripts_hint,
4145 )
4146 },
4147 )
4148 };
4149 if !mem_groups.is_empty() {
4150 let mut merged = mem_groups;
4151 merged.append(&mut chain.css_fallbacks);
4152 chain.css_fallbacks = merged;
4153 }
4154
4155 for family in &font_families {
4158 let matched = chain
4159 .css_fallbacks
4160 .iter()
4161 .any(|g| g.css_name.eq_ignore_ascii_case(family) && !g.fonts.is_empty());
4162 if !matched && !is_generic_family(family) {
4163 unresolved.insert(family.clone());
4164 }
4165 }
4166
4167 let total_fonts = chain.css_fallbacks.iter().map(|g| g.fonts.len()).sum::<usize>()
4174 + chain.unicode_fallbacks.len();
4175 if total_fonts == 0 {
4176 if let Some((_pattern, id)) = fc_cache.list().first() {
4177 chain.unicode_fallbacks.push(rust_fontconfig::FontMatch {
4180 id: *id,
4181 unicode_ranges: Vec::new(),
4182 fallbacks: Vec::new(),
4183 });
4184 }
4185 }
4186
4187 chains.insert(cache_key, chain);
4188 }
4189
4190 let out = ResolvedFontChains {
4195 chains,
4196 unresolved_families: unresolved,
4197 last_resort_chains: 0,
4198 };
4199 report_unresolved_families(&out);
4200 out
4201}
4202
4203fn ensure_chains_nonempty(resolved: &mut ResolvedFontChains, fc_cache: &FcFontCache) {
4214 let fallback_id = match fc_cache.list().first() {
4215 Some((_pattern, id)) => *id,
4216 None => return,
4217 };
4218 let keys: Vec<FontChainKeyOrRef> = resolved.chains.keys().cloned().collect();
4219 let mut rebuilt: HashMap<FontChainKeyOrRef, FontFallbackChain> =
4220 HashMap::new();
4221 let mut last_resort = 0usize;
4222 for key in keys {
4223 if let Some(mut chain) = resolved.chains.remove(&key) {
4224 let total = chain.css_fallbacks.iter().map(|g| g.fonts.len()).sum::<usize>()
4225 + chain.unicode_fallbacks.len();
4226 if total == 0 {
4227 last_resort += 1;
4233 if let FontChainKeyOrRef::Chain(k) = &key {
4234 eprintln!(
4235 "[azul][font] LAST-RESORT fallback for font stack {:?}: nothing in \
4236 the stack matched, rendering in an arbitrary system font.",
4237 k.font_families
4238 );
4239 }
4240 chain.unicode_fallbacks.push(rust_fontconfig::FontMatch {
4241 id: fallback_id,
4242 unicode_ranges: Vec::new(),
4243 fallbacks: Vec::new(),
4244 });
4245 }
4246 rebuilt.insert(key, chain);
4247 }
4248 }
4249 resolved.chains = rebuilt;
4250 resolved.last_resort_chains = last_resort;
4251}
4252
4253pub fn collect_and_resolve_font_chains_with_registration<T: ParsedFontTrait>(
4267 styled_dom: &StyledDom,
4268 fc_cache: &FcFontCache,
4269 font_manager: &crate::text3::cache::FontManager<T>,
4270 platform: &azul_css::system::Platform,
4271) -> ResolvedFontChains {
4272 let collected = collect_font_stacks_from_styled_dom(styled_dom, platform);
4273
4274 for font_ref in collected.font_refs.values() {
4276 font_manager.register_embedded_font(font_ref);
4277 }
4278
4279 if let Some(registry) = font_manager.registry.as_deref() {
4292 let used_chars = collect_used_codepoints_all(styled_dom);
4293 if !used_chars.is_empty() {
4294 let mut fast = resolve_font_chains_fast(
4295 &collected,
4296 registry,
4297 &used_chars,
4298 &font_manager.memory_families,
4299 );
4300 ensure_chains_nonempty(&mut fast, fc_cache);
4301 return fast;
4302 }
4303 }
4304
4305 let scripts = scripts_present_in_styled_dom(styled_dom);
4309 let mut resolved = resolve_font_chains_with_registry(
4310 &collected,
4311 fc_cache,
4312 font_manager.registry.as_deref(),
4313 Some(&scripts),
4314 &font_manager.memory_families,
4315 );
4316
4317 let used_chars = collect_used_codepoints(styled_dom);
4318 for chain in resolved.chains.values_mut() {
4319 prune_chain_to_used_chars(chain, &used_chars);
4320 }
4321 ensure_chains_nonempty(&mut resolved, fc_cache);
4329 resolved
4330}
4331
4332#[allow(clippy::implicit_hasher)] pub fn resolve_font_chains_fast(
4343 collected: &CollectedFontStacks,
4344 registry: &rust_fontconfig::registry::FcFontRegistry,
4345 codepoints: &std::collections::BTreeSet<char>,
4346 memory_families: &HashMap<String, Vec<crate::text3::cache::MemoryFace>>,
4347) -> ResolvedFontChains {
4348 use rust_fontconfig::PatternMatch;
4349
4350 static DBG: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
4351 let dbg = *DBG.get_or_init(|| std::env::var_os("AZ_FAST_RESOLVE_DEBUG").is_some());
4352
4353 let mut chains: HashMap<FontChainKeyOrRef, FontFallbackChain> = HashMap::new();
4354 let mut unresolved: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
4355
4356 for font_stack in &collected.font_stacks {
4357 if font_stack.is_empty() {
4358 continue;
4359 }
4360
4361 let canonical_key = FontChainKey::from_selectors(font_stack);
4365 let font_families = canonical_key.font_families.clone();
4366
4367 let weight = font_stack[0].weight;
4368 let is_italic = font_stack[0].style == FontStyle::Italic;
4369 let is_oblique = font_stack[0].style == FontStyle::Oblique;
4370
4371 let cache_key = FontChainKeyOrRef::Chain(FontChainKey {
4372 font_families: font_families.clone(),
4373 weight,
4374 italic: is_italic,
4375 oblique: is_oblique,
4376 });
4377
4378 if chains.contains_key(&cache_key) {
4379 continue;
4380 }
4381
4382 let italic_match = if is_italic {
4383 PatternMatch::True
4384 } else {
4385 PatternMatch::False
4386 };
4387
4388 let (mut css_fallbacks, disk_families) =
4399 split_memory_matches(&font_families, memory_families, weight, is_italic, is_oblique);
4400
4401 let request = vec![(disk_families.clone(), codepoints.clone())];
4402 let mut chains_out = if disk_families.is_empty() {
4403 Vec::new()
4404 } else {
4405 registry.request_fonts_fast(&request, weight, italic_match)
4406 };
4407 if dbg {
4408 let total_fonts: usize = chains_out
4409 .iter()
4410 .map(|c| c.css_fallbacks.iter().map(|g| g.fonts.len()).sum::<usize>())
4411 .sum();
4412 eprintln!(
4413 "[FAST] stack {:?} w={:?} i={:?} → {} groups, {} faces",
4414 font_families,
4415 weight,
4416 italic_match,
4417 chains_out
4418 .first()
4419 .map_or(0, |c| c.css_fallbacks.len()),
4420 total_fonts,
4421 );
4422 }
4423 let mut chain = chains_out.pop().unwrap_or_else(|| FontFallbackChain {
4426 css_fallbacks: Vec::new(),
4427 unicode_fallbacks: Vec::new(),
4428 original_stack: font_families.clone(),
4429 });
4430 if !css_fallbacks.is_empty() {
4431 css_fallbacks.append(&mut chain.css_fallbacks);
4432 chain.css_fallbacks = css_fallbacks;
4433 }
4434
4435 for family in &font_families {
4439 let matched = chain
4440 .css_fallbacks
4441 .iter()
4442 .any(|g| g.css_name.eq_ignore_ascii_case(family) && !g.fonts.is_empty());
4443 if !matched && !is_generic_family(family) {
4444 unresolved.insert(family.clone());
4445 }
4446 }
4447
4448 chains.insert(cache_key, chain);
4449 }
4450
4451 let out = ResolvedFontChains {
4452 chains,
4453 unresolved_families: unresolved,
4454 last_resort_chains: 0,
4455 };
4456 report_unresolved_families(&out);
4457 out
4458}
4459
4460fn is_generic_family(family: &str) -> bool {
4464 matches!(
4465 family.to_ascii_lowercase().as_str(),
4466 "serif"
4467 | "sans-serif"
4468 | "monospace"
4469 | "cursive"
4470 | "fantasy"
4471 | "system-ui"
4472 | "ui-serif"
4473 | "ui-sans-serif"
4474 | "ui-monospace"
4475 | "ui-rounded"
4476 | "emoji"
4477 | "math"
4478 | "fangsong"
4479 )
4480}
4481
4482fn report_unresolved_families(resolved: &ResolvedFontChains) {
4490 use std::sync::{Mutex, OnceLock};
4491 static SEEN: OnceLock<Mutex<std::collections::BTreeSet<String>>> = OnceLock::new();
4492 if resolved.unresolved_families.is_empty() {
4493 return;
4494 }
4495 let seen = SEEN.get_or_init(|| Mutex::new(std::collections::BTreeSet::new()));
4496 let Ok(mut seen) = seen.lock() else { return };
4497 for family in &resolved.unresolved_families {
4498 if seen.insert(family.clone()) {
4499 eprintln!(
4500 "[azul][font] UNRESOLVED font-family {family:?}: no font file and no \
4501 registered in-memory font matches this family. Text that asks for it \
4502 renders in a FALLBACK font. Register it with \
4503 FontManager::register_named_font(), or install it."
4504 );
4505 }
4506 }
4507}
4508
4509#[must_use] pub fn collect_and_resolve_font_chains(
4513 styled_dom: &StyledDom,
4514 fc_cache: &FcFontCache,
4515 platform: &azul_css::system::Platform,
4516) -> ResolvedFontChains {
4517 let collected = collect_font_stacks_from_styled_dom(styled_dom, platform);
4518 resolve_font_chains(&collected, fc_cache, None)
4519}
4520
4521pub fn register_embedded_fonts_from_styled_dom<T: ParsedFontTrait>(
4523 styled_dom: &StyledDom,
4524 font_manager: &crate::text3::cache::FontManager<T>,
4525 platform: &azul_css::system::Platform,
4526) {
4527 let collected = collect_font_stacks_from_styled_dom(styled_dom, platform);
4528 for font_ref in collected.font_refs.values() {
4529 font_manager.register_embedded_font(font_ref);
4530 }
4531}
4532
4533use std::collections::HashSet;
4536
4537use rust_fontconfig::FontId;
4538
4539#[must_use] pub fn collect_font_ids_from_chains(chains: &ResolvedFontChains) -> HashSet<FontId> {
4544 let mut font_ids = HashSet::new();
4545
4546 if chains.chains.is_empty() {
4550 return font_ids;
4551 }
4552
4553 for chain in chains.chains.values() {
4554 for group in &chain.css_fallbacks {
4556 for font in &group.fonts {
4557 font_ids.insert(font.id);
4558 }
4559 }
4560
4561 for font in &chain.unicode_fallbacks {
4563 font_ids.insert(font.id);
4564 }
4565 }
4566
4567 font_ids
4568}
4569
4570#[allow(clippy::implicit_hasher)] #[must_use] pub fn compute_fonts_to_load(
4580 required_fonts: &HashSet<FontId>,
4581 already_loaded: &HashSet<FontId>,
4582) -> HashSet<FontId> {
4583 if required_fonts.is_empty() {
4586 return HashSet::new();
4587 }
4588 required_fonts.difference(already_loaded).copied().collect()
4589}
4590
4591#[derive(Debug)]
4593pub struct FontLoadResult<T> {
4594 pub loaded: HashMap<FontId, T>,
4596 pub failed: Vec<(FontId, String)>,
4598}
4599
4600#[allow(clippy::implicit_hasher)] pub fn load_fonts_from_disk<T, F>(
4615 font_ids: &HashSet<FontId>,
4616 fc_cache: &FcFontCache,
4617 load_fn: F,
4618) -> FontLoadResult<T>
4619where
4620 F: Fn(
4625 std::sync::Arc<rust_fontconfig::FontBytes>,
4626 usize,
4627 ) -> Result<T, crate::text3::cache::LayoutError>,
4628{
4629 let mut loaded = HashMap::new();
4630 let mut failed = Vec::new();
4631
4632 for font_id in font_ids {
4633 let Some(font_bytes) = fc_cache.get_font_bytes(font_id) else {
4637 failed.push((
4638 *font_id,
4639 format!("Could not get font bytes for {font_id:?}"),
4640 ));
4641 continue;
4642 };
4643
4644 let font_index = fc_cache
4646 .get_font_by_id(font_id)
4647 .map_or(0, |source| match source {
4648 rust_fontconfig::OwnedFontSource::Disk(path) => path.font_index,
4649 rust_fontconfig::OwnedFontSource::Memory(font) => font.font_index,
4650 });
4651
4652 match load_fn(font_bytes, font_index) {
4654 Ok(font) => {
4655 loaded.insert(*font_id, font);
4656 }
4657 Err(e) => {
4658 failed.push((
4659 *font_id,
4660 format!("Failed to parse font {font_id:?}: {e:?}"),
4661 ));
4662 }
4663 }
4664 }
4665
4666 FontLoadResult { loaded, failed }
4667}
4668
4669#[allow(clippy::implicit_hasher)] pub fn resolve_and_load_fonts<T, F>(
4689 styled_dom: &StyledDom,
4690 fc_cache: &FcFontCache,
4691 already_loaded: &HashSet<FontId>,
4692 load_fn: F,
4693 platform: &azul_css::system::Platform,
4694) -> (ResolvedFontChains, FontLoadResult<T>)
4695where
4696 F: Fn(
4697 std::sync::Arc<rust_fontconfig::FontBytes>,
4698 usize,
4699 ) -> Result<T, crate::text3::cache::LayoutError>,
4700{
4701 let chains = collect_and_resolve_font_chains(styled_dom, fc_cache, platform);
4703
4704 let required_fonts = collect_font_ids_from_chains(&chains);
4706
4707 let fonts_to_load = compute_fonts_to_load(&required_fonts, already_loaded);
4709
4710 let load_result = load_fonts_from_disk(&fonts_to_load, fc_cache, load_fn);
4712
4713 (chains, load_result)
4714}
4715
4716use azul_css::props::style::scrollbar::{
4721 LayoutScrollbarWidth, ScrollbarVisibilityMode, StyleScrollbarColor,
4722};
4723
4724#[derive(Copy, Debug, Clone)]
4741pub struct ComputedScrollbarStyle {
4742 pub width_mode: LayoutScrollbarWidth,
4744 pub visual_width_px: f32,
4747 pub reserve_width_px: f32,
4750 pub thumb_color: ColorU,
4752 pub track_color: ColorU,
4754 pub button_color: ColorU,
4756 pub corner_color: ColorU,
4758 pub clip_to_container_border: bool,
4760 pub fade_delay_ms: u32,
4762 pub fade_duration_ms: u32,
4764 pub visibility: ScrollbarVisibilityMode,
4766 pub show_scroll_buttons: bool,
4769 pub scroll_button_size_px: f32,
4772 pub show_corner_rect: bool,
4774 pub thumb_color_hover: Option<ColorU>,
4776 pub thumb_color_active: Option<ColorU>,
4778 pub track_color_hover: Option<ColorU>,
4780 pub visual_width_px_hover: Option<f32>,
4782 pub visual_width_px_active: Option<f32>,
4784}
4785
4786impl Default for ComputedScrollbarStyle {
4787 fn default() -> Self {
4788 let ctx = azul_css::dynamic_selector::DynamicSelectorContext::default();
4791 let ua = azul_core::ua_css::evaluate_ua_scrollbar_css(&ctx);
4792 Self::from_ua_resolved(&ua)
4793 }
4794}
4795
4796impl ComputedScrollbarStyle {
4797 fn from_ua_resolved(ua: &azul_core::ua_css::ResolvedUaScrollbar) -> Self {
4801 let width_mode = ua.width;
4802 let visibility = ua.visibility;
4803 let fade_delay_ms = ua.fade_delay.ms;
4804 let fade_duration_ms = ua.fade_duration.ms;
4805
4806 let visual_width_px = match width_mode {
4807 LayoutScrollbarWidth::Thin => SCROLLBAR_WIDTH_THIN,
4808 LayoutScrollbarWidth::Auto => SCROLLBAR_WIDTH_AUTO,
4809 LayoutScrollbarWidth::None => 0.0,
4810 };
4811
4812 let is_overlay = visibility == ScrollbarVisibilityMode::WhenScrolling;
4814 let reserve_width_px = if is_overlay { 0.0 } else { visual_width_px };
4815 let show_scroll_buttons = !is_overlay;
4816 let scroll_button_size_px = if is_overlay { 0.0 } else { visual_width_px };
4817 let show_corner_rect = !is_overlay;
4818
4819 let (thumb_color, track_color) = match ua.color {
4820 StyleScrollbarColor::Custom(c) => (c.thumb, c.track),
4821 StyleScrollbarColor::Auto => (ColorU::TRANSPARENT, ColorU::TRANSPARENT),
4822 };
4823
4824 let thumb_hover = ColorU {
4828 r: thumb_color.r.saturating_add(THUMB_HOVER_LIGHTEN),
4829 g: thumb_color.g.saturating_add(THUMB_HOVER_LIGHTEN),
4830 b: thumb_color.b.saturating_add(THUMB_HOVER_LIGHTEN),
4831 a: thumb_color.a.saturating_add(THUMB_HOVER_ALPHA_ADD),
4832 };
4833 let thumb_active = ColorU {
4834 r: thumb_color.r.saturating_sub(THUMB_ACTIVE_DARKEN),
4835 g: thumb_color.g.saturating_sub(THUMB_ACTIVE_DARKEN),
4836 b: thumb_color.b.saturating_sub(THUMB_ACTIVE_DARKEN),
4837 a: 255,
4838 };
4839 let track_hover = ColorU {
4840 r: track_color.r,
4841 g: track_color.g,
4842 b: track_color.b,
4843 a: track_color.a.saturating_add(THUMB_HOVER_ALPHA_ADD),
4844 };
4845 let hover_width = visual_width_px + SCROLLBAR_HOVER_EXPAND_PX;
4846 let active_width = visual_width_px + SCROLLBAR_HOVER_EXPAND_PX;
4847
4848 Self {
4849 width_mode,
4850 visual_width_px,
4851 reserve_width_px,
4852 thumb_color,
4853 track_color,
4854 button_color: ColorU::TRANSPARENT,
4855 corner_color: ColorU::TRANSPARENT,
4856 clip_to_container_border: is_overlay,
4857 fade_delay_ms,
4858 fade_duration_ms,
4859 visibility,
4860 show_scroll_buttons,
4861 scroll_button_size_px,
4862 show_corner_rect,
4863 thumb_color_hover: Some(thumb_hover),
4864 thumb_color_active: Some(thumb_active),
4865 track_color_hover: Some(track_hover),
4866 visual_width_px_hover: Some(hover_width),
4867 visual_width_px_active: Some(active_width),
4868 }
4869 }
4870}
4871
4872#[allow(clippy::too_many_lines)] #[must_use] pub fn get_scrollbar_style(
4888 styled_dom: &StyledDom,
4889 node_id: NodeId,
4890 node_state: &StyledNodeState,
4891 system_style: Option<&azul_css::system::SystemStyle>,
4892) -> ComputedScrollbarStyle {
4893 let node_data = &styled_dom.node_data.as_container()[node_id];
4894
4895 let ctx = system_style.map_or_else(
4897 azul_css::dynamic_selector::DynamicSelectorContext::default,
4898 azul_css::dynamic_selector::DynamicSelectorContext::from_system_style,
4899 );
4900 let ua = azul_core::ua_css::evaluate_ua_scrollbar_css(&ctx);
4901 let result = ComputedScrollbarStyle::from_ua_resolved(&ua);
4902
4903 if node_state.is_normal() {
4905 if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
4906 if !cc.has_scrollbar_css(node_id.index()) {
4907 return result;
4908 }
4909 }
4910 }
4911 let mut result = result;
4912
4913 if let Some(track) = styled_dom
4915 .css_property_cache
4916 .ptr
4917 .get_scrollbar_track(node_data, &node_id, node_state)
4918 .and_then(|v| v.get_property())
4919 {
4920 result.track_color = extract_color_from_background(track);
4921 }
4922 if let Some(thumb) = styled_dom
4923 .css_property_cache
4924 .ptr
4925 .get_scrollbar_thumb(node_data, &node_id, node_state)
4926 .and_then(|v| v.get_property())
4927 {
4928 result.thumb_color = extract_color_from_background(thumb);
4929 }
4930 if let Some(button) = styled_dom
4931 .css_property_cache
4932 .ptr
4933 .get_scrollbar_button(node_data, &node_id, node_state)
4934 .and_then(|v| v.get_property())
4935 {
4936 result.button_color = extract_color_from_background(button);
4937 }
4938 if let Some(corner) = styled_dom
4939 .css_property_cache
4940 .ptr
4941 .get_scrollbar_corner(node_data, &node_id, node_state)
4942 .and_then(|v| v.get_property())
4943 {
4944 result.corner_color = extract_color_from_background(corner);
4945 }
4946
4947 if let Some(scrollbar_width) = styled_dom
4949 .css_property_cache
4950 .ptr
4951 .get_scrollbar_width(node_data, &node_id, node_state)
4952 .and_then(|v| v.get_property())
4953 {
4954 result.width_mode = *scrollbar_width;
4955 let w = match scrollbar_width {
4956 LayoutScrollbarWidth::Auto => SCROLLBAR_WIDTH_AUTO,
4957 LayoutScrollbarWidth::Thin => SCROLLBAR_WIDTH_THIN,
4958 LayoutScrollbarWidth::None => 0.0,
4959 };
4960 result.visual_width_px = w;
4961 if result.visibility != ScrollbarVisibilityMode::WhenScrolling {
4962 result.reserve_width_px = w;
4963 }
4964 }
4965
4966 if let Some(scrollbar_color) = styled_dom
4968 .css_property_cache
4969 .ptr
4970 .get_scrollbar_color(node_data, &node_id, node_state)
4971 .and_then(|v| v.get_property())
4972 {
4973 match scrollbar_color {
4974 StyleScrollbarColor::Auto => { }
4975 StyleScrollbarColor::Custom(custom) => {
4976 result.thumb_color = custom.thumb;
4977 result.track_color = custom.track;
4978 }
4979 }
4980 }
4981
4982 if let Some(vis) = styled_dom
4984 .css_property_cache
4985 .ptr
4986 .get_scrollbar_visibility(node_data, &node_id, node_state)
4987 .and_then(|v| v.get_property())
4988 {
4989 result.visibility = *vis;
4990 result.clip_to_container_border = *vis == ScrollbarVisibilityMode::WhenScrolling;
4991 let is_overlay = *vis == ScrollbarVisibilityMode::WhenScrolling;
4993 if is_overlay {
4994 result.reserve_width_px = 0.0;
4995 result.show_scroll_buttons = false;
4996 result.scroll_button_size_px = 0.0;
4997 result.show_corner_rect = false;
4998 } else {
4999 result.reserve_width_px = result.visual_width_px;
5000 }
5001 }
5002
5003 if let Some(delay) = styled_dom
5005 .css_property_cache
5006 .ptr
5007 .get_scrollbar_fade_delay(node_data, &node_id, node_state)
5008 .and_then(|v| v.get_property())
5009 {
5010 result.fade_delay_ms = delay.ms;
5011 }
5012
5013 if let Some(dur) = styled_dom
5015 .css_property_cache
5016 .ptr
5017 .get_scrollbar_fade_duration(node_data, &node_id, node_state)
5018 .and_then(|v| v.get_property())
5019 {
5020 result.fade_duration_ms = dur.ms;
5021 }
5022
5023 result
5024}
5025
5026pub fn get_scrollbar_style_cached<T: ParsedFontTrait>(
5039 ctx: &crate::solver3::LayoutContext<'_, T>,
5040 node_id: NodeId,
5041 node_state: &StyledNodeState,
5042) -> ComputedScrollbarStyle {
5043 if let Some(s) = ctx.scrollbar_style_cache.borrow().get(&node_id) {
5044 return *s;
5045 }
5046 let style = get_scrollbar_style(
5047 ctx.styled_dom,
5048 node_id,
5049 node_state,
5050 ctx.system_style.as_deref(),
5051 );
5052 ctx.scrollbar_style_cache
5053 .borrow_mut()
5054 .insert(node_id, style);
5055 style
5056}
5057
5058const fn extract_color_from_background(
5060 bg: &azul_css::props::style::background::StyleBackgroundContent,
5061) -> ColorU {
5062 use azul_css::props::style::background::StyleBackgroundContent;
5063 match bg {
5064 StyleBackgroundContent::Color(c) => *c,
5065 _ => ColorU::TRANSPARENT,
5066 }
5067}
5068
5069#[must_use] pub fn should_clip_scrollbar_to_border(
5071 styled_dom: &StyledDom,
5072 node_id: NodeId,
5073 node_state: &StyledNodeState,
5074) -> bool {
5075 let style = get_scrollbar_style(styled_dom, node_id, node_state, None);
5076 style.clip_to_container_border
5077}
5078
5079#[must_use] pub fn get_scrollbar_width_px(
5081 styled_dom: &StyledDom,
5082 node_id: NodeId,
5083 node_state: &StyledNodeState,
5084) -> f32 {
5085 let style = get_scrollbar_style(styled_dom, node_id, node_state, None);
5086 style.visual_width_px
5087}
5088
5089#[must_use] pub fn is_text_selectable(
5094 styled_dom: &StyledDom,
5095 node_id: NodeId,
5096 node_state: &StyledNodeState,
5097) -> bool {
5098 let node_data = &styled_dom.node_data.as_container()[node_id];
5099
5100 styled_dom
5101 .css_property_cache
5102 .ptr
5103 .get_user_select(node_data, &node_id, node_state)
5104 .and_then(|v| v.get_property())
5105 .is_none_or(|us| *us != StyleUserSelect::None) }
5107
5108#[must_use] pub fn is_node_contenteditable(styled_dom: &StyledDom, node_id: NodeId) -> bool {
5116 use azul_core::dom::AttributeType;
5117
5118 let node_data = &styled_dom.node_data.as_container()[node_id];
5119
5120 if node_data.is_contenteditable() {
5122 return true;
5123 }
5124
5125 node_data
5128 .attributes()
5129 .as_ref()
5130 .iter()
5131 .any(|attr| matches!(attr, AttributeType::ContentEditable(true)))
5132}
5133use azul_css::props::layout::table::{
5138 LayoutTableLayout, StyleBorderCollapse, StyleCaptionSide, StyleEmptyCells,
5139};
5140use azul_css::props::layout::text::LayoutTextJustify;
5141use azul_css::props::style::effects::StyleAspectRatio;
5142use azul_css::props::style::effects::StyleCursor;
5143use azul_css::props::style::effects::StyleObjectFit;
5144use azul_css::props::style::effects::StyleObjectPosition;
5145use azul_css::props::layout::overflow::StyleTextOverflow;
5146use azul_css::props::style::effects::StyleTextOrientation;
5147use azul_css::props::style::text::StyleHyphens;
5148use azul_css::props::style::text::StyleLineBreak;
5149use azul_css::props::style::text::StyleOverflowWrap;
5150use azul_css::props::style::text::StyleTextAlignLast;
5151use azul_css::props::style::text::StyleWordBreak;
5152
5153impl ExtractPropertyValue<LayoutTextJustify> for CssProperty {
5154 fn extract(&self) -> Option<LayoutTextJustify> {
5155 match self {
5156 Self::TextJustify(CssPropertyValue::Exact(v)) => Some(*v),
5157 _ => None,
5158 }
5159 }
5160}
5161
5162impl ExtractPropertyValue<StyleHyphens> for CssProperty {
5163 fn extract(&self) -> Option<StyleHyphens> {
5164 match self {
5165 Self::Hyphens(CssPropertyValue::Exact(v)) => Some(*v),
5166 _ => None,
5167 }
5168 }
5169}
5170
5171impl ExtractPropertyValue<StyleWordBreak> for CssProperty {
5172 fn extract(&self) -> Option<StyleWordBreak> {
5173 match self {
5174 Self::WordBreak(CssPropertyValue::Exact(v)) => Some(*v),
5175 _ => None,
5176 }
5177 }
5178}
5179
5180impl ExtractPropertyValue<StyleOverflowWrap> for CssProperty {
5181 fn extract(&self) -> Option<StyleOverflowWrap> {
5182 match self {
5183 Self::OverflowWrap(CssPropertyValue::Exact(v)) => Some(*v),
5184 _ => None,
5185 }
5186 }
5187}
5188
5189impl ExtractPropertyValue<StyleLineBreak> for CssProperty {
5190 fn extract(&self) -> Option<StyleLineBreak> {
5191 match self {
5192 Self::LineBreak(CssPropertyValue::Exact(v)) => Some(*v),
5193 _ => None,
5194 }
5195 }
5196}
5197
5198impl ExtractPropertyValue<StyleTextAlignLast> for CssProperty {
5199 fn extract(&self) -> Option<StyleTextAlignLast> {
5200 match self {
5201 Self::TextAlignLast(CssPropertyValue::Exact(v)) => Some(*v),
5202 _ => None,
5203 }
5204 }
5205}
5206
5207impl ExtractPropertyValue<StyleObjectFit> for CssProperty {
5208 fn extract(&self) -> Option<StyleObjectFit> {
5209 match self {
5210 Self::ObjectFit(CssPropertyValue::Exact(v)) => Some(*v),
5211 _ => None,
5212 }
5213 }
5214}
5215
5216impl ExtractPropertyValue<StyleTextOverflow> for CssProperty {
5217 fn extract(&self) -> Option<StyleTextOverflow> {
5218 match self {
5219 Self::TextOverflow(CssPropertyValue::Exact(v)) => Some(*v),
5220 _ => None,
5221 }
5222 }
5223}
5224
5225impl ExtractPropertyValue<StyleTextOrientation> for CssProperty {
5226 fn extract(&self) -> Option<StyleTextOrientation> {
5227 match self {
5228 Self::TextOrientation(CssPropertyValue::Exact(v)) => Some(*v),
5229 _ => None,
5230 }
5231 }
5232}
5233
5234impl ExtractPropertyValue<StyleObjectPosition> for CssProperty {
5235 fn extract(&self) -> Option<StyleObjectPosition> {
5236 match self {
5237 Self::ObjectPosition(CssPropertyValue::Exact(v)) => Some(*v),
5238 _ => None,
5239 }
5240 }
5241}
5242
5243impl ExtractPropertyValue<StyleAspectRatio> for CssProperty {
5244 fn extract(&self) -> Option<StyleAspectRatio> {
5245 match self {
5246 Self::AspectRatio(CssPropertyValue::Exact(v)) => Some(*v),
5247 _ => None,
5248 }
5249 }
5250}
5251
5252impl ExtractPropertyValue<LayoutTableLayout> for CssProperty {
5253 fn extract(&self) -> Option<LayoutTableLayout> {
5254 match self {
5255 Self::TableLayout(CssPropertyValue::Exact(v)) => Some(*v),
5256 _ => None,
5257 }
5258 }
5259}
5260
5261impl ExtractPropertyValue<StyleBorderCollapse> for CssProperty {
5262 fn extract(&self) -> Option<StyleBorderCollapse> {
5263 match self {
5264 Self::BorderCollapse(CssPropertyValue::Exact(v)) => Some(*v),
5265 _ => None,
5266 }
5267 }
5268}
5269
5270impl ExtractPropertyValue<StyleCaptionSide> for CssProperty {
5271 fn extract(&self) -> Option<StyleCaptionSide> {
5272 match self {
5273 Self::CaptionSide(CssPropertyValue::Exact(v)) => Some(*v),
5274 _ => None,
5275 }
5276 }
5277}
5278
5279impl ExtractPropertyValue<StyleEmptyCells> for CssProperty {
5280 fn extract(&self) -> Option<StyleEmptyCells> {
5281 match self {
5282 Self::EmptyCells(CssPropertyValue::Exact(v)) => Some(*v),
5283 _ => None,
5284 }
5285 }
5286}
5287
5288impl ExtractPropertyValue<StyleCursor> for CssProperty {
5289 fn extract(&self) -> Option<StyleCursor> {
5290 match self {
5291 Self::Cursor(CssPropertyValue::Exact(v)) => Some(*v),
5292 _ => None,
5293 }
5294 }
5295}
5296
5297get_css_property!(
5302 get_text_justify,
5303 get_text_justify,
5304 LayoutTextJustify,
5305 CssPropertyType::TextJustify
5306);
5307
5308get_css_property!(
5309 get_hyphens,
5310 get_hyphens,
5311 StyleHyphens,
5312 CssPropertyType::Hyphens
5313);
5314
5315get_css_property!(
5316 get_word_break,
5317 get_word_break,
5318 StyleWordBreak,
5319 CssPropertyType::WordBreak
5320);
5321
5322get_css_property!(
5323 get_overflow_wrap,
5324 get_overflow_wrap,
5325 StyleOverflowWrap,
5326 CssPropertyType::OverflowWrap
5327);
5328
5329get_css_property!(
5330 get_line_break,
5331 get_line_break,
5332 StyleLineBreak,
5333 CssPropertyType::LineBreak
5334);
5335
5336get_css_property!(
5337 get_text_align_last,
5338 get_text_align_last,
5339 StyleTextAlignLast,
5340 CssPropertyType::TextAlignLast
5341);
5342
5343get_css_property!(
5344 get_table_layout,
5345 get_table_layout,
5346 LayoutTableLayout,
5347 CssPropertyType::TableLayout
5348);
5349
5350get_css_property!(
5351 get_border_collapse,
5352 get_border_collapse,
5353 StyleBorderCollapse,
5354 CssPropertyType::BorderCollapse,
5355 compact = get_border_collapse
5356);
5357
5358get_css_property!(
5359 get_caption_side,
5360 get_caption_side,
5361 StyleCaptionSide,
5362 CssPropertyType::CaptionSide
5363);
5364
5365get_css_property!(
5366 get_empty_cells,
5367 get_empty_cells,
5368 StyleEmptyCells,
5369 CssPropertyType::EmptyCells
5370);
5371
5372get_css_property!(
5373 get_cursor_property,
5374 get_cursor,
5375 StyleCursor,
5376 CssPropertyType::Cursor
5377);
5378
5379#[must_use] pub fn get_height_value(
5385 styled_dom: &StyledDom,
5386 node_id: NodeId,
5387 node_state: &StyledNodeState,
5388) -> Option<LayoutHeight> {
5389 let node_data = &styled_dom.node_data.as_container()[node_id];
5390 styled_dom
5391 .css_property_cache
5392 .ptr
5393 .get_height(node_data, &node_id, node_state)
5394 .and_then(|v| v.get_property())
5395 .cloned()
5396}
5397
5398#[must_use] pub fn get_shape_inside(
5400 styled_dom: &StyledDom,
5401 node_id: NodeId,
5402 node_state: &StyledNodeState,
5403) -> Option<azul_css::props::layout::shape::ShapeInside> {
5404 let node_data = &styled_dom.node_data.as_container()[node_id];
5405 styled_dom
5406 .css_property_cache
5407 .ptr
5408 .get_shape_inside(node_data, &node_id, node_state)
5409 .and_then(|v| v.get_property())
5410 .cloned()
5411}
5412
5413#[must_use] pub fn get_shape_outside(
5415 styled_dom: &StyledDom,
5416 node_id: NodeId,
5417 node_state: &StyledNodeState,
5418) -> Option<azul_css::props::layout::shape::ShapeOutside> {
5419 let node_data = &styled_dom.node_data.as_container()[node_id];
5420 styled_dom
5421 .css_property_cache
5422 .ptr
5423 .get_shape_outside(node_data, &node_id, node_state)
5424 .and_then(|v| v.get_property())
5425 .cloned()
5426}
5427
5428#[must_use] pub fn get_line_height_value(
5430 styled_dom: &StyledDom,
5431 node_id: NodeId,
5432 node_state: &StyledNodeState,
5433) -> Option<azul_css::props::style::text::StyleLineHeight> {
5434 let node_data = &styled_dom.node_data.as_container()[node_id];
5435 styled_dom
5436 .css_property_cache
5437 .ptr
5438 .get_line_height(node_data, &node_id, node_state)
5439 .and_then(|v| v.get_property())
5440 .copied()
5441}
5442
5443#[must_use] pub fn get_text_indent_value(
5445 styled_dom: &StyledDom,
5446 node_id: NodeId,
5447 node_state: &StyledNodeState,
5448) -> Option<azul_css::props::style::text::StyleTextIndent> {
5449 let node_data = &styled_dom.node_data.as_container()[node_id];
5450 styled_dom
5451 .css_property_cache
5452 .ptr
5453 .get_text_indent(node_data, &node_id, node_state)
5454 .and_then(|v| v.get_property())
5455 .copied()
5456}
5457
5458#[must_use] pub fn get_column_count(
5460 styled_dom: &StyledDom,
5461 node_id: NodeId,
5462 node_state: &StyledNodeState,
5463) -> Option<azul_css::props::layout::column::ColumnCount> {
5464 let node_data = &styled_dom.node_data.as_container()[node_id];
5465 styled_dom
5466 .css_property_cache
5467 .ptr
5468 .get_column_count(node_data, &node_id, node_state)
5469 .and_then(|v| v.get_property())
5470 .copied()
5471}
5472
5473#[must_use] pub fn get_initial_letter(
5475 styled_dom: &StyledDom,
5476 node_id: NodeId,
5477 node_state: &StyledNodeState,
5478) -> Option<azul_css::props::style::text::StyleInitialLetter> {
5479 let node_data = &styled_dom.node_data.as_container()[node_id];
5480 styled_dom
5481 .css_property_cache
5482 .ptr
5483 .get_initial_letter(node_data, &node_id, node_state)
5484 .and_then(|v| v.get_property())
5485 .copied()
5486}
5487
5488#[must_use] pub fn get_line_clamp(
5490 styled_dom: &StyledDom,
5491 node_id: NodeId,
5492 node_state: &StyledNodeState,
5493) -> Option<azul_css::props::style::text::StyleLineClamp> {
5494 let node_data = &styled_dom.node_data.as_container()[node_id];
5495 styled_dom
5496 .css_property_cache
5497 .ptr
5498 .get_line_clamp(node_data, &node_id, node_state)
5499 .and_then(|v| v.get_property())
5500 .copied()
5501}
5502
5503#[must_use] pub fn get_hanging_punctuation(
5505 styled_dom: &StyledDom,
5506 node_id: NodeId,
5507 node_state: &StyledNodeState,
5508) -> Option<azul_css::props::style::text::StyleHangingPunctuation> {
5509 let node_data = &styled_dom.node_data.as_container()[node_id];
5510 styled_dom
5511 .css_property_cache
5512 .ptr
5513 .get_hanging_punctuation(node_data, &node_id, node_state)
5514 .and_then(|v| v.get_property())
5515 .copied()
5516}
5517
5518#[must_use] pub fn get_text_combine_upright(
5520 styled_dom: &StyledDom,
5521 node_id: NodeId,
5522 node_state: &StyledNodeState,
5523) -> Option<azul_css::props::style::text::StyleTextCombineUpright> {
5524 let node_data = &styled_dom.node_data.as_container()[node_id];
5525 styled_dom
5526 .css_property_cache
5527 .ptr
5528 .get_text_combine_upright(node_data, &node_id, node_state)
5529 .and_then(|v| v.get_property())
5530 .copied()
5531}
5532
5533#[must_use] pub fn get_exclusion_margin(
5535 styled_dom: &StyledDom,
5536 node_id: NodeId,
5537 node_state: &StyledNodeState,
5538) -> f32 {
5539 let node_data = &styled_dom.node_data.as_container()[node_id];
5540 styled_dom
5541 .css_property_cache
5542 .ptr
5543 .get_exclusion_margin(node_data, &node_id, node_state)
5544 .and_then(|v| v.get_property())
5545 .map_or(0.0, |v| v.inner.get())
5546}
5547
5548#[must_use] pub fn get_hyphenation_language(
5550 styled_dom: &StyledDom,
5551 node_id: NodeId,
5552 node_state: &StyledNodeState,
5553) -> Option<azul_css::props::style::exclusion::StyleHyphenationLanguage> {
5554 let node_data = &styled_dom.node_data.as_container()[node_id];
5555 styled_dom
5556 .css_property_cache
5557 .ptr
5558 .get_hyphenation_language(node_data, &node_id, node_state)
5559 .and_then(|v| v.get_property())
5560 .cloned()
5561}
5562
5563#[must_use] pub fn get_border_spacing(
5565 styled_dom: &StyledDom,
5566 node_id: NodeId,
5567 node_state: &StyledNodeState,
5568) -> azul_css::props::layout::table::LayoutBorderSpacing {
5569 use azul_css::props::basic::pixel::PixelValue;
5570
5571 if node_state.is_normal() {
5573 if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
5574 let h_raw = cc.get_border_spacing_h_raw(node_id.index());
5575 let v_raw = cc.get_border_spacing_v_raw(node_id.index());
5576 if h_raw < azul_css::compact_cache::I16_SENTINEL_THRESHOLD
5579 && v_raw < azul_css::compact_cache::I16_SENTINEL_THRESHOLD
5580 {
5581 return azul_css::props::layout::table::LayoutBorderSpacing {
5582 horizontal: PixelValue::px(f32::from(h_raw) / 10.0),
5583 vertical: PixelValue::px(f32::from(v_raw) / 10.0),
5584 };
5585 }
5586 }
5587 }
5588
5589 let node_data = &styled_dom.node_data.as_container()[node_id];
5591 styled_dom
5592 .css_property_cache
5593 .ptr
5594 .get_border_spacing(node_data, &node_id, node_state)
5595 .and_then(|v| v.get_property())
5596 .copied()
5597 .unwrap_or_default()
5598}
5599
5600#[must_use] pub fn get_opacity(styled_dom: &StyledDom, node_id: NodeId, node_state: &StyledNodeState) -> f32 {
5606 if node_state.is_normal() {
5608 if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
5609 let raw = cc.get_opacity_raw(node_id.index());
5610 if raw == azul_css::compact_cache::OPACITY_SENTINEL {
5611 return 1.0;
5612 }
5613 return f32::from(raw) / 254.0;
5614 }
5615 }
5616 let node_data = &styled_dom.node_data.as_container()[node_id];
5618 styled_dom
5619 .css_property_cache
5620 .ptr
5621 .get_opacity(node_data, &node_id, node_state)
5622 .and_then(|v| v.get_property())
5623 .map_or(1.0, |v| v.inner.normalized())
5624}
5625
5626#[must_use] pub fn get_filter(
5628 styled_dom: &StyledDom,
5629 node_id: NodeId,
5630 node_state: &StyledNodeState,
5631) -> Option<azul_css::props::style::filter::StyleFilterVec> {
5632 if node_state.is_normal() {
5633 if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
5634 if !cc.has_filter(node_id.index()) {
5635 return None;
5636 }
5637 }
5638 }
5639 let node_data = &styled_dom.node_data.as_container()[node_id];
5640 styled_dom
5641 .css_property_cache
5642 .ptr
5643 .get_filter(node_data, &node_id, node_state)
5644 .and_then(|v| v.get_property())
5645 .cloned()
5646}
5647
5648#[must_use] pub fn get_backdrop_filter(
5650 styled_dom: &StyledDom,
5651 node_id: NodeId,
5652 node_state: &StyledNodeState,
5653) -> Option<azul_css::props::style::filter::StyleFilterVec> {
5654 if node_state.is_normal() {
5655 if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
5656 if !cc.has_backdrop_filter(node_id.index()) {
5657 return None;
5658 }
5659 }
5660 }
5661 let node_data = &styled_dom.node_data.as_container()[node_id];
5662 styled_dom
5663 .css_property_cache
5664 .ptr
5665 .get_backdrop_filter(node_data, &node_id, node_state)
5666 .and_then(|v| v.get_property())
5667 .cloned()
5668}
5669
5670#[inline]
5673fn box_shadow_fast_bail(
5674 styled_dom: &StyledDom,
5675 node_id: NodeId,
5676 node_state: &StyledNodeState,
5677) -> bool {
5678 if !node_state.is_normal() {
5679 return false;
5680 }
5681 if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
5682 return !cc.has_box_shadow(node_id.index());
5683 }
5684 false
5685}
5686
5687#[must_use] pub fn get_box_shadow_left(
5689 styled_dom: &StyledDom,
5690 node_id: NodeId,
5691 node_state: &StyledNodeState,
5692) -> Option<azul_css::props::style::box_shadow::StyleBoxShadow> {
5693 if box_shadow_fast_bail(styled_dom, node_id, node_state) {
5694 return None;
5695 }
5696 let node_data = &styled_dom.node_data.as_container()[node_id];
5697 styled_dom
5698 .css_property_cache
5699 .ptr
5700 .get_box_shadow_left(node_data, &node_id, node_state)
5701 .and_then(|v| v.get_property())
5702 .map(|v| (**v))
5703}
5704
5705#[must_use] pub fn get_box_shadow_right(
5707 styled_dom: &StyledDom,
5708 node_id: NodeId,
5709 node_state: &StyledNodeState,
5710) -> Option<azul_css::props::style::box_shadow::StyleBoxShadow> {
5711 if box_shadow_fast_bail(styled_dom, node_id, node_state) {
5712 return None;
5713 }
5714 let node_data = &styled_dom.node_data.as_container()[node_id];
5715 styled_dom
5716 .css_property_cache
5717 .ptr
5718 .get_box_shadow_right(node_data, &node_id, node_state)
5719 .and_then(|v| v.get_property())
5720 .map(|v| (**v))
5721}
5722
5723#[must_use] pub fn get_box_shadow_top(
5725 styled_dom: &StyledDom,
5726 node_id: NodeId,
5727 node_state: &StyledNodeState,
5728) -> Option<azul_css::props::style::box_shadow::StyleBoxShadow> {
5729 if box_shadow_fast_bail(styled_dom, node_id, node_state) {
5730 return None;
5731 }
5732 let node_data = &styled_dom.node_data.as_container()[node_id];
5733 styled_dom
5734 .css_property_cache
5735 .ptr
5736 .get_box_shadow_top(node_data, &node_id, node_state)
5737 .and_then(|v| v.get_property())
5738 .map(|v| (**v))
5739}
5740
5741#[must_use] pub fn get_box_shadow_bottom(
5743 styled_dom: &StyledDom,
5744 node_id: NodeId,
5745 node_state: &StyledNodeState,
5746) -> Option<azul_css::props::style::box_shadow::StyleBoxShadow> {
5747 if box_shadow_fast_bail(styled_dom, node_id, node_state) {
5748 return None;
5749 }
5750 let node_data = &styled_dom.node_data.as_container()[node_id];
5751 styled_dom
5752 .css_property_cache
5753 .ptr
5754 .get_box_shadow_bottom(node_data, &node_id, node_state)
5755 .and_then(|v| v.get_property())
5756 .map(|v| (**v))
5757}
5758
5759#[must_use] pub fn get_text_shadow(
5761 styled_dom: &StyledDom,
5762 node_id: NodeId,
5763 node_state: &StyledNodeState,
5764) -> Option<azul_css::props::style::box_shadow::StyleBoxShadow> {
5765 if node_state.is_normal() {
5766 if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
5767 if !cc.has_text_shadow(node_id.index()) {
5768 return None;
5769 }
5770 }
5771 }
5772 let node_data = &styled_dom.node_data.as_container()[node_id];
5773 styled_dom
5774 .css_property_cache
5775 .ptr
5776 .get_text_shadow(node_data, &node_id, node_state)
5777 .and_then(|v| v.get_property())
5778 .map(|v| (**v))
5779}
5780
5781#[must_use] pub fn get_transform(
5788 styled_dom: &StyledDom,
5789 node_id: NodeId,
5790 node_state: &StyledNodeState,
5791) -> Option<azul_css::props::style::transform::StyleTransformVec> {
5792 if node_state.is_normal() {
5794 if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
5795 if !cc.has_transform(node_id.index()) {
5796 return None;
5797 }
5798 }
5800 }
5801 let node_data = &styled_dom.node_data.as_container()[node_id];
5802 styled_dom
5803 .css_property_cache
5804 .ptr
5805 .get_transform(node_data, &node_id, node_state)
5806 .and_then(|v| v.get_property())
5807 .cloned()
5808}
5809
5810#[must_use] pub fn get_counter_reset(
5812 styled_dom: &StyledDom,
5813 node_id: NodeId,
5814 node_state: &StyledNodeState,
5815) -> Option<azul_css::props::style::content::CounterReset> {
5816 let node_data = &styled_dom.node_data.as_container()[node_id];
5817 styled_dom
5818 .css_property_cache
5819 .ptr
5820 .get_counter_reset(node_data, &node_id, node_state)
5821 .and_then(|v| v.get_property())
5822 .cloned()
5823}
5824
5825#[must_use] pub fn get_counter_increment(
5827 styled_dom: &StyledDom,
5828 node_id: NodeId,
5829 node_state: &StyledNodeState,
5830) -> Option<azul_css::props::style::content::CounterIncrement> {
5831 let node_data = &styled_dom.node_data.as_container()[node_id];
5832 styled_dom
5833 .css_property_cache
5834 .ptr
5835 .get_counter_increment(node_data, &node_id, node_state)
5836 .and_then(|v| v.get_property())
5837 .cloned()
5838}
5839
5840#[must_use] pub fn is_node_contenteditable_inherited(styled_dom: &StyledDom, node_id: NodeId) -> bool {
5866 use azul_core::dom::AttributeType;
5867
5868 let node_data_container = styled_dom.node_data.as_container();
5869 let hierarchy = styled_dom.node_hierarchy.as_container();
5870
5871 let mut current_node_id = Some(node_id);
5872
5873 while let Some(nid) = current_node_id {
5874 let node_data = &node_data_container[nid];
5875
5876 if node_data.is_contenteditable() {
5879 return true;
5880 }
5881
5882 for attr in node_data.attributes().as_ref() {
5885 if let AttributeType::ContentEditable(is_editable) = attr {
5886 return *is_editable;
5889 }
5890 }
5891
5892 current_node_id = hierarchy.get(nid).and_then(azul_core::styled_dom::NodeHierarchyItem::parent_id);
5894 }
5895
5896 false
5898}
5899
5900#[must_use] pub fn find_contenteditable_ancestor(styled_dom: &StyledDom, node_id: NodeId) -> Option<NodeId> {
5910 use azul_core::dom::AttributeType;
5911
5912 let node_data_container = styled_dom.node_data.as_container();
5913 let hierarchy = styled_dom.node_hierarchy.as_container();
5914
5915 let mut current_node_id = Some(node_id);
5916
5917 while let Some(nid) = current_node_id {
5918 let node_data = &node_data_container[nid];
5919
5920 if node_data.is_contenteditable() {
5922 return Some(nid);
5923 }
5924
5925 for attr in node_data.attributes().as_ref() {
5927 if let AttributeType::ContentEditable(is_editable) = attr {
5928 if *is_editable {
5929 return Some(nid);
5930 }
5931 return None;
5933 }
5934 }
5935
5936 current_node_id = hierarchy.get(nid).and_then(azul_core::styled_dom::NodeHierarchyItem::parent_id);
5938 }
5939
5940 None
5941}
5942
5943macro_rules! get_css_property_value {
5951 ($fn_name:ident, $cache_method:ident, $ret_type:ty) => {
5952 #[must_use] pub fn $fn_name(
5953 styled_dom: &StyledDom,
5954 node_id: NodeId,
5955 node_state: &StyledNodeState,
5956 ) -> Option<$ret_type> {
5957 let node_data = &styled_dom.node_data.as_container()[node_id];
5958 styled_dom
5959 .css_property_cache
5960 .ptr
5961 .$cache_method(node_data, &node_id, node_state)
5962 .cloned()
5963 }
5964 };
5965}
5966
5967get_css_property_value!(
5969 get_flex_direction_prop,
5970 get_flex_direction,
5971 LayoutFlexDirectionValue
5972);
5973get_css_property_value!(get_flex_wrap_prop, get_flex_wrap, LayoutFlexWrapValue);
5974get_css_property_value!(get_flex_grow_prop, get_flex_grow, LayoutFlexGrowValue);
5975get_css_property_value!(get_flex_shrink_prop, get_flex_shrink, LayoutFlexShrinkValue);
5976get_css_property_value!(get_flex_basis_prop, get_flex_basis, LayoutFlexBasisValue);
5977
5978get_css_property_value!(get_align_items_prop, get_align_items, LayoutAlignItemsValue);
5980get_css_property_value!(get_align_self_prop, get_align_self, LayoutAlignSelfValue);
5981get_css_property_value!(
5982 get_align_content_prop,
5983 get_align_content,
5984 LayoutAlignContentValue
5985);
5986get_css_property_value!(
5987 get_justify_content_prop,
5988 get_justify_content,
5989 LayoutJustifyContentValue
5990);
5991get_css_property_value!(
5992 get_justify_items_prop,
5993 get_justify_items,
5994 LayoutJustifyItemsValue
5995);
5996get_css_property_value!(
5997 get_justify_self_prop,
5998 get_justify_self,
5999 LayoutJustifySelfValue
6000);
6001
6002get_css_property_value!(get_gap_prop, get_gap, LayoutGapValue);
6004
6005get_css_property_value!(
6007 get_grid_template_rows_prop,
6008 get_grid_template_rows,
6009 LayoutGridTemplateRowsValue
6010);
6011get_css_property_value!(
6012 get_grid_template_columns_prop,
6013 get_grid_template_columns,
6014 LayoutGridTemplateColumnsValue
6015);
6016get_css_property_value!(
6017 get_grid_auto_rows_prop,
6018 get_grid_auto_rows,
6019 LayoutGridAutoRowsValue
6020);
6021get_css_property_value!(
6022 get_grid_auto_columns_prop,
6023 get_grid_auto_columns,
6024 LayoutGridAutoColumnsValue
6025);
6026get_css_property_value!(
6027 get_grid_auto_flow_prop,
6028 get_grid_auto_flow,
6029 LayoutGridAutoFlowValue
6030);
6031get_css_property_value!(get_grid_column_prop, get_grid_column, LayoutGridColumnValue);
6032get_css_property_value!(get_grid_row_prop, get_grid_row, LayoutGridRowValue);
6033
6034#[must_use] pub fn get_grid_template_areas_prop(
6039 styled_dom: &StyledDom,
6040 node_id: NodeId,
6041 node_state: &StyledNodeState,
6042) -> Option<GridTemplateAreas> {
6043 let node_data = &styled_dom.node_data.as_container()[node_id];
6044 styled_dom
6045 .css_property_cache
6046 .ptr
6047 .get_property(
6048 node_data,
6049 &node_id,
6050 node_state,
6051 &CssPropertyType::GridTemplateAreas,
6052 )
6053 .and_then(|p| {
6054 if let CssProperty::GridTemplateAreas(v) = p {
6055 v.get_property().cloned()
6056 } else {
6057 None
6058 }
6059 })
6060}
6061
6062#[must_use] pub fn get_clip_path(
6068 styled_dom: &StyledDom,
6069 node_id: NodeId,
6070 node_state: &StyledNodeState,
6071) -> Option<azul_css::props::layout::shape::ClipPath> {
6072 if node_state.is_normal() {
6074 if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
6075 if !cc.has_clip_path(node_id.index()) {
6076 return None;
6077 }
6078 }
6079 }
6080 let node_data = &styled_dom.node_data.as_container()[node_id];
6081 styled_dom
6082 .css_property_cache
6083 .ptr
6084 .get_clip_path(node_data, &node_id, node_state)
6085 .and_then(|v| v.get_property())
6086 .cloned()
6087}
6088
6089#[cfg(test)]
6090#[allow(clippy::float_cmp, clippy::too_many_lines)]
6091mod autotest_generated {
6092 use azul_core::{dom::Dom, ua_css::ResolvedUaScrollbar};
6093 use azul_css::{
6094 css::Css,
6095 props::style::{
6096 background::StyleBackgroundContent,
6097 scrollbar::{ScrollbarColorCustom, ScrollbarFadeDelay, ScrollbarFadeDuration},
6098 },
6099 };
6100 use rust_fontconfig::{CssFallbackGroup, FontMatch};
6101
6102 use super::*;
6103
6104 const ALL_OVERFLOW: [LayoutOverflow; 5] = [
6110 LayoutOverflow::Scroll,
6111 LayoutOverflow::Auto,
6112 LayoutOverflow::Hidden,
6113 LayoutOverflow::Visible,
6114 LayoutOverflow::Clip,
6115 ];
6116
6117 const ALL_DISPLAY: [LayoutDisplay; 23] = [
6119 LayoutDisplay::None,
6120 LayoutDisplay::Block,
6121 LayoutDisplay::Inline,
6122 LayoutDisplay::InlineBlock,
6123 LayoutDisplay::Flex,
6124 LayoutDisplay::InlineFlex,
6125 LayoutDisplay::Table,
6126 LayoutDisplay::InlineTable,
6127 LayoutDisplay::TableRowGroup,
6128 LayoutDisplay::TableHeaderGroup,
6129 LayoutDisplay::TableFooterGroup,
6130 LayoutDisplay::TableRow,
6131 LayoutDisplay::TableColumnGroup,
6132 LayoutDisplay::TableColumn,
6133 LayoutDisplay::TableCell,
6134 LayoutDisplay::TableCaption,
6135 LayoutDisplay::FlowRoot,
6136 LayoutDisplay::ListItem,
6137 LayoutDisplay::RunIn,
6138 LayoutDisplay::Marker,
6139 LayoutDisplay::Grid,
6140 LayoutDisplay::InlineGrid,
6141 LayoutDisplay::Contents,
6142 ];
6143
6144 const ALL_PAGE_BREAK: [PageBreak; 12] = [
6146 PageBreak::Auto,
6147 PageBreak::Avoid,
6148 PageBreak::Always,
6149 PageBreak::All,
6150 PageBreak::Page,
6151 PageBreak::AvoidPage,
6152 PageBreak::Left,
6153 PageBreak::Right,
6154 PageBreak::Recto,
6155 PageBreak::Verso,
6156 PageBreak::Column,
6157 PageBreak::AvoidColumn,
6158 ];
6159
6160 const ALL_BREAK_INSIDE: [BreakInside; 4] = [
6162 BreakInside::Auto,
6163 BreakInside::Avoid,
6164 BreakInside::AvoidPage,
6165 BreakInside::AvoidColumn,
6166 ];
6167
6168 const ALL_SCROLLBAR_WIDTH: [LayoutScrollbarWidth; 3] = [
6170 LayoutScrollbarWidth::Auto,
6171 LayoutScrollbarWidth::Thin,
6172 LayoutScrollbarWidth::None,
6173 ];
6174
6175 const ALL_VISIBILITY: [ScrollbarVisibilityMode; 3] = [
6177 ScrollbarVisibilityMode::Always,
6178 ScrollbarVisibilityMode::WhenScrolling,
6179 ScrollbarVisibilityMode::Auto,
6180 ];
6181
6182 fn parse(css: &str) -> Css {
6183 azul_css::parser2::new_from_str(css).0
6184 }
6185
6186 fn body_with_divs(n: usize, css: &str) -> StyledDom {
6189 let children: Vec<Dom> = (0..n).map(|_| Dom::create_div()).collect();
6190 let mut dom = Dom::create_body().with_children(children.into());
6191 StyledDom::create(&mut dom, parse(css))
6192 }
6193
6194 fn body_with_text(text: &str) -> StyledDom {
6196 let mut dom = Dom::create_body().with_children(vec![Dom::create_text(text)].into());
6197 StyledDom::create(&mut dom, Css::empty())
6198 }
6199
6200 fn normal() -> StyledNodeState {
6201 StyledNodeState::default()
6202 }
6203
6204 fn hovered() -> StyledNodeState {
6207 StyledNodeState {
6208 hover: true,
6209 ..StyledNodeState::default()
6210 }
6211 }
6212
6213 fn state_of(sd: &StyledDom, id: NodeId) -> StyledNodeState {
6214 sd.get_styled_node_state(&id)
6215 }
6216
6217 fn empty_chains() -> ResolvedFontChains {
6218 ResolvedFontChains {
6219 chains: HashMap::new(),
6220 ..Default::default()
6221 }
6222 }
6223
6224 fn chain_key(family: &str) -> FontChainKey {
6225 FontChainKey {
6226 font_families: vec![family.to_string()],
6227 weight: FcWeight::Normal,
6228 italic: false,
6229 oblique: false,
6230 }
6231 }
6232
6233 fn font_match(id: u128, ranges: &[(u32, u32)]) -> FontMatch {
6235 FontMatch {
6236 id: FontId(id),
6237 unicode_ranges: ranges
6238 .iter()
6239 .map(|&(start, end)| UnicodeRange { start, end })
6240 .collect(),
6241 fallbacks: Vec::new(),
6242 }
6243 }
6244
6245 fn chain_with(groups: Vec<CssFallbackGroup>, unicode: Vec<FontMatch>) -> FontFallbackChain {
6246 FontFallbackChain {
6247 css_fallbacks: groups,
6248 unicode_fallbacks: unicode,
6249 original_stack: Vec::new(),
6250 }
6251 }
6252
6253 fn bare_layout_node(scrollbar_info: Option<ScrollbarRequirements>) -> LayoutNode {
6255 use azul_core::{diff::NodeDataFingerprint, dom::FormattingContext};
6256
6257 use crate::solver3::{
6258 geometry::{BoxProps, UnresolvedBoxProps},
6259 layout_tree::{ComputedLayoutStyle, DirtyFlag, SubtreeHash},
6260 };
6261
6262 LayoutNode {
6263 box_props: BoxProps::default(),
6264 dom_node_id: None,
6265 children: Vec::new(),
6266 used_size: None,
6267 formatting_context: FormattingContext::Inline,
6268 parent: None,
6269 intrinsic_sizes: None,
6270 baseline: None,
6271 inline_layout_result: None,
6272 scrollbar_info,
6273 relative_position: None,
6274 overflow_content_size: None,
6275 taffy_cache: taffy::Cache::new(),
6276 computed_style: ComputedLayoutStyle::default(),
6277 pseudo_element: None,
6278 escaped_top_margin: None,
6279 escaped_bottom_margin: None,
6280 parent_formatting_context: None,
6281 ifc_membership: None,
6282 containing_block_index: None,
6283 anonymous_type: None,
6284 node_data_fingerprint: NodeDataFingerprint::default(),
6285 subtree_hash: SubtreeHash(0),
6286 dirty_flag: DirtyFlag::Layout,
6287 unresolved_box_props: UnresolvedBoxProps::default(),
6288 ifc_id: None,
6289 }
6290 }
6291
6292 #[test]
6297 fn multivalue_default_is_auto() {
6298 let v: MultiValue<i32> = MultiValue::default();
6299 assert!(v.is_auto());
6300 assert!(!v.is_exact());
6301 }
6302
6303 #[test]
6304 fn multivalue_is_auto_and_is_exact_are_mutually_exclusive() {
6305 let cases: [MultiValue<i32>; 4] = [
6306 MultiValue::Auto,
6307 MultiValue::Initial,
6308 MultiValue::Inherit,
6309 MultiValue::Exact(7),
6310 ];
6311 for v in cases {
6312 assert!(
6313 !(v.is_auto() && v.is_exact()),
6314 "a value cannot be both Auto and Exact: {v:?}"
6315 );
6316 }
6317 assert!(MultiValue::<i32>::Auto.is_auto());
6318 assert!(!MultiValue::<i32>::Initial.is_auto());
6319 assert!(!MultiValue::<i32>::Inherit.is_auto());
6320 assert!(!MultiValue::Exact(7).is_auto());
6321
6322 assert!(MultiValue::Exact(7).is_exact());
6323 assert!(!MultiValue::<i32>::Auto.is_exact());
6324 assert!(!MultiValue::<i32>::Initial.is_exact());
6325 assert!(!MultiValue::<i32>::Inherit.is_exact());
6326 }
6327
6328 #[test]
6329 fn multivalue_exact_returns_some_only_for_the_exact_variant() {
6330 assert_eq!(MultiValue::Exact(42_i32).exact(), Some(42));
6331 assert_eq!(MultiValue::<i32>::Auto.exact(), None);
6332 assert_eq!(MultiValue::<i32>::Initial.exact(), None);
6333 assert_eq!(MultiValue::<i32>::Inherit.exact(), None);
6334 }
6335
6336 #[test]
6337 fn multivalue_exact_round_trips_extreme_payloads() {
6338 for probe in [i32::MIN, -1, 0, 1, i32::MAX] {
6340 assert_eq!(MultiValue::Exact(probe).exact(), Some(probe));
6341 }
6342 let nan = MultiValue::Exact(f32::NAN).exact().unwrap();
6344 assert!(nan.is_nan());
6345 assert_eq!(MultiValue::Exact(f32::INFINITY).exact(), Some(f32::INFINITY));
6346 assert_eq!(
6347 MultiValue::Exact(f32::NEG_INFINITY).exact(),
6348 Some(f32::NEG_INFINITY)
6349 );
6350 }
6351
6352 #[test]
6353 fn multivalue_unwrap_or_uses_the_default_for_every_non_exact_variant() {
6354 assert_eq!(MultiValue::Exact(5_i32).unwrap_or(99), 5);
6355 assert_eq!(MultiValue::<i32>::Auto.unwrap_or(99), 99);
6356 assert_eq!(MultiValue::<i32>::Initial.unwrap_or(99), 99);
6357 assert_eq!(MultiValue::<i32>::Inherit.unwrap_or(99), 99);
6358 assert!(MultiValue::<f32>::Auto.unwrap_or(f32::NAN).is_nan());
6360 }
6361
6362 #[test]
6363 fn multivalue_unwrap_or_default_falls_back_to_t_default() {
6364 assert_eq!(MultiValue::Exact(5_i32).unwrap_or_default(), 5);
6365 assert_eq!(MultiValue::<i32>::Auto.unwrap_or_default(), 0);
6366 assert_eq!(MultiValue::<i32>::Initial.unwrap_or_default(), 0);
6367 assert_eq!(MultiValue::<i32>::Inherit.unwrap_or_default(), 0);
6368 assert_eq!(
6370 MultiValue::<LayoutOverflow>::Inherit.unwrap_or_default(),
6371 LayoutOverflow::Visible
6372 );
6373 }
6374
6375 #[test]
6376 fn multivalue_map_transforms_exact_and_preserves_the_keyword_variants() {
6377 assert_eq!(MultiValue::Exact(2_i32).map(|v| v * 2), MultiValue::Exact(4));
6378 assert_eq!(MultiValue::<i32>::Auto.map(|v| v * 2), MultiValue::Auto);
6379 assert_eq!(
6380 MultiValue::<i32>::Initial.map(|v| v * 2),
6381 MultiValue::Initial
6382 );
6383 assert_eq!(
6384 MultiValue::<i32>::Inherit.map(|v| v * 2),
6385 MultiValue::Inherit
6386 );
6387 }
6388
6389 #[test]
6390 fn multivalue_map_never_invokes_the_closure_for_keyword_variants() {
6391 let auto: MultiValue<i32> = MultiValue::Auto;
6393 let _ = auto.map(|_| -> i32 { panic!("map() called f() on MultiValue::Auto") });
6394 let initial: MultiValue<i32> = MultiValue::Initial;
6395 let _ = initial.map(|_| -> i32 { panic!("map() called f() on MultiValue::Initial") });
6396 let inherit: MultiValue<i32> = MultiValue::Inherit;
6397 let _ = inherit.map(|_| -> i32 { panic!("map() called f() on MultiValue::Inherit") });
6398 }
6399
6400 #[test]
6401 fn multivalue_map_can_change_the_payload_type() {
6402 let mapped: MultiValue<usize> = MultiValue::Exact("hello").map(str::len);
6403 assert_eq!(mapped, MultiValue::Exact(5));
6404 let abs: MultiValue<i32> = MultiValue::Exact(i32::MIN).map(i32::wrapping_abs);
6407 assert_eq!(abs, MultiValue::Exact(i32::MIN));
6408 }
6409
6410 #[test]
6415 fn overflow_predicates_match_the_spec_for_every_exact_variant() {
6416 for o in ALL_OVERFLOW {
6417 let v = MultiValue::Exact(o);
6418 assert_eq!(
6419 v.is_clipped(),
6420 o != LayoutOverflow::Visible,
6421 "is_clipped is every value except Visible ({o:?})"
6422 );
6423 assert_eq!(
6424 v.is_scroll(),
6425 matches!(o, LayoutOverflow::Scroll | LayoutOverflow::Auto),
6426 "is_scroll ({o:?})"
6427 );
6428 assert_eq!(
6429 v.is_auto_overflow(),
6430 o == LayoutOverflow::Auto,
6431 "is_auto_overflow ({o:?})"
6432 );
6433 assert_eq!(
6434 v.is_hidden(),
6435 o == LayoutOverflow::Hidden,
6436 "is_hidden ({o:?})"
6437 );
6438 assert_eq!(
6439 v.is_hidden_or_clip(),
6440 matches!(o, LayoutOverflow::Hidden | LayoutOverflow::Clip),
6441 "is_hidden_or_clip ({o:?})"
6442 );
6443 assert_eq!(
6444 v.is_scroll_explicit(),
6445 o == LayoutOverflow::Scroll,
6446 "is_scroll_explicit ({o:?})"
6447 );
6448 assert_eq!(v.is_clip(), o == LayoutOverflow::Clip, "is_clip ({o:?})");
6449 assert_eq!(
6450 v.is_visible_or_clip(),
6451 matches!(o, LayoutOverflow::Visible | LayoutOverflow::Clip),
6452 "is_visible_or_clip ({o:?})"
6453 );
6454 assert_eq!(
6455 v.establishes_bfc(),
6456 matches!(
6457 o,
6458 LayoutOverflow::Hidden | LayoutOverflow::Scroll | LayoutOverflow::Auto
6459 ),
6460 "establishes_bfc ({o:?})"
6461 );
6462 }
6463 }
6464
6465 #[test]
6466 fn overflow_predicates_are_false_for_every_keyword_variant() {
6467 let keywords: [MultiValue<LayoutOverflow>; 3] = [
6471 MultiValue::Auto,
6472 MultiValue::Initial,
6473 MultiValue::Inherit,
6474 ];
6475 for v in keywords {
6476 assert!(!v.is_clipped(), "{v:?}");
6477 assert!(!v.is_scroll(), "{v:?}");
6478 assert!(!v.is_auto_overflow(), "{v:?}");
6479 assert!(!v.is_hidden(), "{v:?}");
6480 assert!(!v.is_hidden_or_clip(), "{v:?}");
6481 assert!(!v.is_scroll_explicit(), "{v:?}");
6482 assert!(!v.is_clip(), "{v:?}");
6483 assert!(!v.is_visible_or_clip(), "{v:?}");
6484 assert!(!v.establishes_bfc(), "{v:?}");
6486 }
6487 }
6488
6489 #[test]
6490 fn overflow_scroll_implies_clipped_and_clip_implies_hidden_or_clip() {
6491 for o in ALL_OVERFLOW {
6492 let v = MultiValue::Exact(o);
6493 assert!(
6494 !v.is_scroll() || v.is_clipped(),
6495 "anything that scrolls also clips ({o:?})"
6496 );
6497 assert!(
6498 !v.is_clip() || v.is_hidden_or_clip(),
6499 "clip is a subset of hidden_or_clip ({o:?})"
6500 );
6501 assert!(
6502 !v.is_scroll_explicit() || v.is_scroll(),
6503 "explicit scroll is a subset of scroll ({o:?})"
6504 );
6505 }
6506 }
6507
6508 #[test]
6510 fn overflow_resolve_computed_matches_css_overflow_3_section_3_1() {
6511 for this in ALL_OVERFLOW {
6512 for other in ALL_OVERFLOW {
6513 let got = MultiValue::Exact(this).resolve_computed(&MultiValue::Exact(other));
6514 let other_is_scrollable =
6515 !matches!(other, LayoutOverflow::Visible | LayoutOverflow::Clip);
6516 let want = if other_is_scrollable {
6517 match this {
6518 LayoutOverflow::Visible => LayoutOverflow::Auto,
6519 LayoutOverflow::Clip => LayoutOverflow::Hidden,
6520 keep => keep,
6521 }
6522 } else {
6523 this
6524 };
6525 assert_eq!(
6526 got,
6527 MultiValue::Exact(want),
6528 "resolve_computed({this:?}, {other:?})"
6529 );
6530 }
6531 }
6532 }
6533
6534 #[test]
6535 fn overflow_resolve_computed_is_a_no_op_unless_both_axes_are_exact() {
6536 let keywords: [MultiValue<LayoutOverflow>; 3] = [
6537 MultiValue::Auto,
6538 MultiValue::Initial,
6539 MultiValue::Inherit,
6540 ];
6541 for v in keywords {
6543 for other in ALL_OVERFLOW {
6544 assert_eq!(v.resolve_computed(&MultiValue::Exact(other)), v);
6545 }
6546 assert_eq!(v.resolve_computed(&MultiValue::Auto), v);
6547 }
6548 for this in ALL_OVERFLOW {
6550 let v = MultiValue::Exact(this);
6551 for other in keywords {
6552 assert_eq!(v.resolve_computed(&other), v, "{this:?} vs {other:?}");
6553 }
6554 }
6555 }
6556
6557 #[test]
6558 fn overflow_resolve_computed_is_idempotent() {
6559 for this in ALL_OVERFLOW {
6560 for other in ALL_OVERFLOW {
6561 let other_mv = MultiValue::Exact(other);
6562 let once = MultiValue::Exact(this).resolve_computed(&other_mv);
6563 let twice = once.resolve_computed(&other_mv);
6564 assert_eq!(once, twice, "resolve_computed({this:?}, {other:?}) twice");
6565 }
6566 }
6567 }
6568
6569 #[test]
6574 fn position_is_absolute_or_fixed_only_for_absolute_and_fixed() {
6575 let all = [
6576 LayoutPosition::Static,
6577 LayoutPosition::Relative,
6578 LayoutPosition::Absolute,
6579 LayoutPosition::Fixed,
6580 LayoutPosition::Sticky,
6581 ];
6582 for p in all {
6583 assert_eq!(
6584 MultiValue::Exact(p).is_absolute_or_fixed(),
6585 matches!(p, LayoutPosition::Absolute | LayoutPosition::Fixed),
6586 "{p:?}"
6587 );
6588 }
6589 assert!(!MultiValue::<LayoutPosition>::Auto.is_absolute_or_fixed());
6591 assert!(!MultiValue::<LayoutPosition>::Initial.is_absolute_or_fixed());
6592 assert!(!MultiValue::<LayoutPosition>::Inherit.is_absolute_or_fixed());
6593 }
6594
6595 #[test]
6596 fn float_is_none_treats_every_keyword_variant_as_not_floated() {
6597 assert!(MultiValue::Exact(LayoutFloat::None).is_none());
6598 assert!(!MultiValue::Exact(LayoutFloat::Left).is_none());
6599 assert!(!MultiValue::Exact(LayoutFloat::Right).is_none());
6600 assert!(MultiValue::<LayoutFloat>::Auto.is_none());
6603 assert!(MultiValue::<LayoutFloat>::Initial.is_none());
6604 assert!(MultiValue::<LayoutFloat>::Inherit.is_none());
6605 assert!(MultiValue::<LayoutFloat>::default().is_none());
6606 }
6607
6608 #[test]
6613 fn blockify_display_follows_the_css_display_3_table() {
6614 for d in ALL_DISPLAY {
6615 let want = match d {
6616 LayoutDisplay::Inline | LayoutDisplay::InlineBlock => LayoutDisplay::Block,
6617 LayoutDisplay::InlineFlex => LayoutDisplay::Flex,
6618 LayoutDisplay::InlineTable => LayoutDisplay::Table,
6619 LayoutDisplay::InlineGrid => LayoutDisplay::Grid,
6620 LayoutDisplay::TableRowGroup
6621 | LayoutDisplay::TableColumn
6622 | LayoutDisplay::TableColumnGroup
6623 | LayoutDisplay::TableHeaderGroup
6624 | LayoutDisplay::TableFooterGroup
6625 | LayoutDisplay::TableRow
6626 | LayoutDisplay::TableCell
6627 | LayoutDisplay::TableCaption => LayoutDisplay::Block,
6628 other => other,
6629 };
6630 assert_eq!(blockify_display(d), want, "blockify_display({d:?})");
6631 }
6632 }
6633
6634 #[test]
6635 fn blockify_display_is_idempotent_and_never_produces_an_inline_level_value() {
6636 for d in ALL_DISPLAY {
6637 let once = blockify_display(d);
6638 assert_eq!(
6639 blockify_display(once),
6640 once,
6641 "blockify_display is not idempotent for {d:?}"
6642 );
6643 assert!(
6644 !matches!(
6645 once,
6646 LayoutDisplay::Inline
6647 | LayoutDisplay::InlineBlock
6648 | LayoutDisplay::InlineFlex
6649 | LayoutDisplay::InlineTable
6650 | LayoutDisplay::InlineGrid
6651 ),
6652 "blockified {d:?} is still inline-level: {once:?}"
6653 );
6654 }
6655 }
6656
6657 #[test]
6658 fn get_computed_display_keeps_none_regardless_of_the_flags() {
6659 for flags in 0_u8..16 {
6661 let got = get_computed_display(
6662 LayoutDisplay::None,
6663 flags & 1 != 0,
6664 flags & 2 != 0,
6665 flags & 4 != 0,
6666 flags & 8 != 0,
6667 );
6668 assert_eq!(got, LayoutDisplay::None, "flags={flags:#06b}");
6669 }
6670 }
6671
6672 #[test]
6673 fn get_computed_display_is_the_identity_when_no_flag_is_set() {
6674 for d in ALL_DISPLAY {
6675 assert_eq!(
6676 get_computed_display(d, false, false, false, false),
6677 d,
6678 "an in-flow, non-root, non-flex-child box keeps its specified display ({d:?})"
6679 );
6680 }
6681 }
6682
6683 #[test]
6684 fn get_computed_display_blockifies_whenever_any_flag_is_set() {
6685 for d in ALL_DISPLAY {
6686 if d == LayoutDisplay::None {
6687 continue; }
6689 for flags in 1_u8..16 {
6692 let got = get_computed_display(
6693 d,
6694 flags & 1 != 0,
6695 flags & 2 != 0,
6696 flags & 4 != 0,
6697 flags & 8 != 0,
6698 );
6699 assert_eq!(
6700 got,
6701 blockify_display(d),
6702 "get_computed_display({d:?}, flags={flags:#06b})"
6703 );
6704 }
6705 }
6706 }
6707
6708 #[test]
6713 fn is_forced_page_break_covers_exactly_the_forcing_keywords() {
6714 for pb in ALL_PAGE_BREAK {
6715 let want = matches!(
6716 pb,
6717 PageBreak::Always
6718 | PageBreak::Page
6719 | PageBreak::Left
6720 | PageBreak::Right
6721 | PageBreak::Recto
6722 | PageBreak::Verso
6723 | PageBreak::All
6724 );
6725 assert_eq!(is_forced_page_break(pb), want, "{pb:?}");
6726 }
6727 assert!(!is_forced_page_break(PageBreak::Column));
6729 assert!(!is_forced_page_break(PageBreak::Auto));
6730 assert!(!is_forced_page_break(PageBreak::default()));
6731 }
6732
6733 #[test]
6734 fn is_avoid_page_break_covers_exactly_avoid_and_avoid_page() {
6735 for pb in ALL_PAGE_BREAK {
6736 let want = matches!(pb, PageBreak::Avoid | PageBreak::AvoidPage);
6737 assert_eq!(is_avoid_page_break(&pb), want, "{pb:?}");
6738 }
6739 assert!(!is_avoid_page_break(&PageBreak::AvoidColumn));
6741 }
6742
6743 #[test]
6744 fn forced_and_avoid_page_break_are_never_both_true() {
6745 for pb in ALL_PAGE_BREAK {
6746 assert!(
6747 !(is_forced_page_break(pb) && is_avoid_page_break(&pb)),
6748 "{pb:?} is simultaneously forced and avoided"
6749 );
6750 }
6751 }
6752
6753 #[test]
6754 fn is_avoid_break_inside_is_true_for_every_variant_except_auto() {
6755 for bi in ALL_BREAK_INSIDE {
6756 assert_eq!(is_avoid_break_inside(&bi), bi != BreakInside::Auto, "{bi:?}");
6757 }
6758 assert!(!is_avoid_break_inside(&BreakInside::default()));
6759 }
6760
6761 fn ua(
6766 width: LayoutScrollbarWidth,
6767 visibility: ScrollbarVisibilityMode,
6768 color: StyleScrollbarColor,
6769 delay_ms: u32,
6770 duration_ms: u32,
6771 ) -> ResolvedUaScrollbar {
6772 ResolvedUaScrollbar {
6773 color,
6774 width,
6775 visibility,
6776 fade_delay: ScrollbarFadeDelay { ms: delay_ms },
6777 fade_duration: ScrollbarFadeDuration { ms: duration_ms },
6778 }
6779 }
6780
6781 #[test]
6782 fn from_ua_resolved_holds_its_invariants_across_the_whole_width_visibility_matrix() {
6783 for width in ALL_SCROLLBAR_WIDTH {
6784 for visibility in ALL_VISIBILITY {
6785 let s = ComputedScrollbarStyle::from_ua_resolved(&ua(
6786 width,
6787 visibility,
6788 StyleScrollbarColor::Auto,
6789 0,
6790 0,
6791 ));
6792
6793 assert_eq!(s.width_mode, width);
6794 assert_eq!(s.visibility, visibility);
6795
6796 let expected_visual = match width {
6797 LayoutScrollbarWidth::Thin => SCROLLBAR_WIDTH_THIN,
6798 LayoutScrollbarWidth::Auto => SCROLLBAR_WIDTH_AUTO,
6799 LayoutScrollbarWidth::None => 0.0,
6800 };
6801 assert_eq!(s.visual_width_px, expected_visual, "{width:?}");
6802
6803 let is_overlay = visibility == ScrollbarVisibilityMode::WhenScrolling;
6805 assert_eq!(s.clip_to_container_border, is_overlay);
6806 assert_eq!(s.show_scroll_buttons, !is_overlay);
6807 assert_eq!(s.show_corner_rect, !is_overlay);
6808 if is_overlay {
6809 assert_eq!(s.reserve_width_px, 0.0, "overlay reserves no layout space");
6810 assert_eq!(s.scroll_button_size_px, 0.0);
6811 } else {
6812 assert_eq!(s.reserve_width_px, s.visual_width_px);
6813 assert_eq!(s.scroll_button_size_px, s.visual_width_px);
6814 }
6815
6816 assert_eq!(
6818 s.visual_width_px_hover,
6819 Some(s.visual_width_px + SCROLLBAR_HOVER_EXPAND_PX)
6820 );
6821 assert_eq!(
6822 s.visual_width_px_active,
6823 Some(s.visual_width_px + SCROLLBAR_HOVER_EXPAND_PX)
6824 );
6825 assert!(s.reserve_width_px <= s.visual_width_px);
6826 assert!(s.visual_width_px.is_finite());
6827 }
6828 }
6829 }
6830
6831 #[test]
6832 fn from_ua_resolved_saturates_the_hover_and_active_colour_maths_at_the_u8_boundaries() {
6833 let white = ColorU {
6835 r: 255,
6836 g: 255,
6837 b: 255,
6838 a: 255,
6839 };
6840 let s = ComputedScrollbarStyle::from_ua_resolved(&ua(
6841 LayoutScrollbarWidth::Auto,
6842 ScrollbarVisibilityMode::Always,
6843 StyleScrollbarColor::Custom(ScrollbarColorCustom {
6844 thumb: white,
6845 track: white,
6846 }),
6847 0,
6848 0,
6849 ));
6850 let hover = s.thumb_color_hover.expect("hover thumb colour");
6851 assert_eq!((hover.r, hover.g, hover.b, hover.a), (255, 255, 255, 255));
6852 let track_hover = s.track_color_hover.expect("hover track colour");
6853 assert_eq!(track_hover.a, 255);
6854
6855 let black0 = ColorU {
6858 r: 0,
6859 g: 0,
6860 b: 0,
6861 a: 0,
6862 };
6863 let s = ComputedScrollbarStyle::from_ua_resolved(&ua(
6864 LayoutScrollbarWidth::Auto,
6865 ScrollbarVisibilityMode::Always,
6866 StyleScrollbarColor::Custom(ScrollbarColorCustom {
6867 thumb: black0,
6868 track: black0,
6869 }),
6870 0,
6871 0,
6872 ));
6873 let active = s.thumb_color_active.expect("active thumb colour");
6874 assert_eq!((active.r, active.g, active.b), (0, 0, 0));
6875 assert_eq!(active.a, 255, "the active thumb is always fully opaque");
6876 let hover = s.thumb_color_hover.expect("hover thumb colour");
6877 assert_eq!(
6878 (hover.r, hover.g, hover.b, hover.a),
6879 (
6880 THUMB_HOVER_LIGHTEN,
6881 THUMB_HOVER_LIGHTEN,
6882 THUMB_HOVER_LIGHTEN,
6883 THUMB_HOVER_ALPHA_ADD
6884 )
6885 );
6886 }
6887
6888 #[test]
6889 fn from_ua_resolved_passes_extreme_fade_timings_through_without_overflow() {
6890 let s = ComputedScrollbarStyle::from_ua_resolved(&ua(
6891 LayoutScrollbarWidth::Thin,
6892 ScrollbarVisibilityMode::WhenScrolling,
6893 StyleScrollbarColor::Auto,
6894 u32::MAX,
6895 u32::MAX,
6896 ));
6897 assert_eq!(s.fade_delay_ms, u32::MAX);
6898 assert_eq!(s.fade_duration_ms, u32::MAX);
6899
6900 let s = ComputedScrollbarStyle::from_ua_resolved(&ua(
6901 LayoutScrollbarWidth::Thin,
6902 ScrollbarVisibilityMode::WhenScrolling,
6903 StyleScrollbarColor::Auto,
6904 0,
6905 0,
6906 ));
6907 assert_eq!(s.fade_delay_ms, 0);
6908 assert_eq!(s.fade_duration_ms, 0);
6909 }
6910
6911 #[test]
6912 fn from_ua_resolved_maps_scrollbar_color_auto_to_transparent() {
6913 let s = ComputedScrollbarStyle::from_ua_resolved(&ua(
6914 LayoutScrollbarWidth::Auto,
6915 ScrollbarVisibilityMode::Always,
6916 StyleScrollbarColor::Auto,
6917 0,
6918 0,
6919 ));
6920 assert_eq!(s.thumb_color, ColorU::TRANSPARENT);
6921 assert_eq!(s.track_color, ColorU::TRANSPARENT);
6922 assert_eq!(s.button_color, ColorU::TRANSPARENT);
6923 assert_eq!(s.corner_color, ColorU::TRANSPARENT);
6924 }
6925
6926 #[test]
6927 fn computed_scrollbar_style_default_is_internally_consistent() {
6928 let d = ComputedScrollbarStyle::default();
6929 assert!(d.visual_width_px.is_finite() && d.visual_width_px >= 0.0);
6930 assert!(d.reserve_width_px.is_finite() && d.reserve_width_px >= 0.0);
6931 assert!(d.reserve_width_px <= d.visual_width_px);
6932 let overlay = d.visibility == ScrollbarVisibilityMode::WhenScrolling;
6933 assert_eq!(d.show_scroll_buttons, !overlay);
6934 assert_eq!(d.clip_to_container_border, overlay);
6935 }
6936
6937 #[test]
6942 fn extract_color_from_background_returns_the_solid_colour_verbatim() {
6943 for probe in [
6944 ColorU::TRANSPARENT,
6945 ColorU::BLACK,
6946 ColorU::WHITE,
6947 ColorU {
6948 r: 1,
6949 g: 2,
6950 b: 3,
6951 a: 4,
6952 },
6953 ColorU {
6954 r: 255,
6955 g: 0,
6956 b: 255,
6957 a: 0,
6958 },
6959 ] {
6960 assert_eq!(
6961 extract_color_from_background(&StyleBackgroundContent::Color(probe)),
6962 probe
6963 );
6964 }
6965 }
6966
6967 #[test]
6968 fn extract_color_from_background_falls_back_to_transparent_for_non_colour_layers() {
6969 let img = StyleBackgroundContent::Image("does-not-exist.png".into());
6971 assert_eq!(extract_color_from_background(&img), ColorU::TRANSPARENT);
6972 let empty = StyleBackgroundContent::Image(String::new().into());
6974 assert_eq!(extract_color_from_background(&empty), ColorU::TRANSPARENT);
6975 let unicode = StyleBackgroundContent::Image("картинка-🎉.png".into());
6976 assert_eq!(extract_color_from_background(&unicode), ColorU::TRANSPARENT);
6977 }
6978
6979 #[test]
6984 fn get_scrollbar_info_from_layout_defaults_to_no_scrollbars_when_layout_never_set_it() {
6985 let node = bare_layout_node(None);
6986 let got = get_scrollbar_info_from_layout(&node);
6987 assert!(!got.needs_horizontal);
6988 assert!(!got.needs_vertical);
6989 assert_eq!(got.scrollbar_width, 0.0);
6990 assert_eq!(got.scrollbar_height, 0.0);
6991 assert_eq!(got.visual_width_px, 0.0);
6992 }
6993
6994 #[test]
6995 fn get_scrollbar_info_from_layout_returns_whatever_layout_stored_including_degenerate_floats() {
6996 let stored = ScrollbarRequirements {
6997 needs_horizontal: true,
6998 needs_vertical: true,
6999 scrollbar_width: f32::NAN,
7000 scrollbar_height: f32::INFINITY,
7001 visual_width_px: -1.0,
7002 };
7003 let got = get_scrollbar_info_from_layout(&bare_layout_node(Some(stored)));
7004 assert!(got.needs_horizontal && got.needs_vertical);
7005 assert!(got.scrollbar_width.is_nan(), "the getter must not sanitise");
7006 assert_eq!(got.scrollbar_height, f32::INFINITY);
7007 assert_eq!(got.visual_width_px, -1.0);
7008 }
7009
7010 #[test]
7015 fn resolved_font_chains_empty_instance_answers_every_query_with_none() {
7016 let r = empty_chains();
7017 assert_eq!(r.len(), 0);
7018 assert!(r.is_empty());
7019 assert_eq!(r.font_refs_len(), 0);
7020
7021 assert!(r.get(&FontChainKeyOrRef::Ref(0)).is_none());
7022 assert!(r.get_by_chain_key(&chain_key("Arial")).is_none());
7023 assert!(r.get_for_font_stack(&[]).is_none());
7024 assert!(r.get_for_font_ref(0).is_none());
7025 assert!(r.get_for_font_ref(usize::MAX).is_none());
7026 assert!(r.get_for_font_ref(usize::MAX / 2).is_none());
7027
7028 assert!(r.clone().into_inner().is_empty());
7029 assert!(r.into_fontconfig_chains().is_empty());
7030 }
7031
7032 #[test]
7033 fn resolved_font_chains_get_by_chain_key_round_trips_the_inserted_key() {
7034 let key = chain_key("Iosevka");
7035 let mut chains = HashMap::new();
7036 chains.insert(
7037 FontChainKeyOrRef::Chain(key.clone()),
7038 chain_with(Vec::new(), Vec::new()),
7039 );
7040 let r = ResolvedFontChains { chains, ..Default::default() };
7041
7042 assert!(r.get_by_chain_key(&key).is_some());
7043 assert!(r.get(&FontChainKeyOrRef::Chain(key.clone())).is_some());
7044 let heavier = FontChainKey {
7046 weight: FcWeight::Bold,
7047 ..key.clone()
7048 };
7049 assert!(r.get_by_chain_key(&heavier).is_none());
7050 let italic = FontChainKey {
7052 italic: true,
7053 ..key
7054 };
7055 assert!(r.get_by_chain_key(&italic).is_none());
7056 }
7057
7058 #[test]
7059 fn resolved_font_chains_counts_and_filters_ref_entries() {
7060 let mut chains = HashMap::new();
7061 chains.insert(
7062 FontChainKeyOrRef::Chain(chain_key("Arial")),
7063 chain_with(Vec::new(), Vec::new()),
7064 );
7065 chains.insert(
7066 FontChainKeyOrRef::Ref(0xDEAD_BEEF),
7067 chain_with(Vec::new(), Vec::new()),
7068 );
7069 chains.insert(
7070 FontChainKeyOrRef::Ref(usize::MAX),
7071 chain_with(Vec::new(), Vec::new()),
7072 );
7073 let r = ResolvedFontChains { chains, ..Default::default() };
7074
7075 assert_eq!(r.len(), 3);
7076 assert!(!r.is_empty());
7077 assert_eq!(r.font_refs_len(), 2, "two Ref keys, one Chain key");
7078 assert!(r.get_for_font_ref(0xDEAD_BEEF).is_some());
7079 assert!(r.get_for_font_ref(usize::MAX).is_some());
7080 assert!(r.get_for_font_ref(0).is_none());
7081
7082 let fc_only = r.into_fontconfig_chains();
7084 assert_eq!(fc_only.len(), 1);
7085 assert!(fc_only.contains_key(&chain_key("Arial")));
7086 }
7087
7088 #[test]
7089 fn resolved_font_chains_get_for_font_stack_uses_the_canonical_selector_key() {
7090 let selectors = vec![FontSelector {
7091 family: "Arial".to_string(),
7092 weight: FcWeight::Normal,
7093 style: FontStyle::Normal,
7094 unicode_ranges: Vec::new(),
7095 }];
7096 let key = FontChainKey::from_selectors(&selectors);
7097 let mut chains = HashMap::new();
7098 chains.insert(
7099 FontChainKeyOrRef::Chain(key),
7100 chain_with(Vec::new(), Vec::new()),
7101 );
7102 let r = ResolvedFontChains { chains, ..Default::default() };
7103
7104 assert!(r.get_for_font_stack(&selectors).is_some());
7105 assert!(r.get_for_font_stack(&[]).is_none());
7107 }
7108
7109 #[test]
7114 fn collect_font_ids_from_chains_dedupes_across_groups_and_unicode_fallbacks() {
7115 let mut chains = HashMap::new();
7116 chains.insert(
7117 FontChainKeyOrRef::Chain(chain_key("Arial")),
7118 chain_with(
7119 vec![
7120 CssFallbackGroup {
7121 css_name: "Arial".to_string(),
7122 fonts: vec![font_match(1, &[]), font_match(2, &[])],
7123 },
7124 CssFallbackGroup {
7125 css_name: "sans-serif".to_string(),
7126 fonts: vec![font_match(1, &[]), font_match(3, &[])],
7128 },
7129 ],
7130 vec![font_match(3, &[]), font_match(u128::MAX, &[])],
7131 ),
7132 );
7133 let ids = collect_font_ids_from_chains(&ResolvedFontChains { chains, ..Default::default() });
7134 assert_eq!(ids.len(), 4, "ids 1, 2, 3 and u128::MAX, each exactly once");
7135 for probe in [1_u128, 2, 3, u128::MAX] {
7136 assert!(ids.contains(&FontId(probe)), "missing FontId({probe})");
7137 }
7138 assert!(!ids.contains(&FontId(0)));
7139 }
7140
7141 #[test]
7142 fn collect_font_ids_from_chains_returns_empty_for_an_empty_or_fontless_chain_set() {
7143 assert!(collect_font_ids_from_chains(&empty_chains()).is_empty());
7144
7145 let mut chains = HashMap::new();
7147 chains.insert(
7148 FontChainKeyOrRef::Chain(chain_key("Nonexistent")),
7149 chain_with(
7150 vec![CssFallbackGroup {
7151 css_name: "Nonexistent".to_string(),
7152 fonts: Vec::new(),
7153 }],
7154 Vec::new(),
7155 ),
7156 );
7157 assert!(collect_font_ids_from_chains(&ResolvedFontChains { chains, ..Default::default() }).is_empty());
7158 }
7159
7160 #[test]
7161 fn compute_fonts_to_load_is_the_set_difference_and_bails_early_on_an_empty_requirement() {
7162 let a = FontId(0);
7163 let b = FontId(1);
7164 let c = FontId(u128::MAX);
7165
7166 let empty: HashSet<FontId> = HashSet::new();
7167 let all: HashSet<FontId> = [a, b, c].into_iter().collect();
7168 let loaded_b: HashSet<FontId> = [b].into_iter().collect();
7169
7170 assert!(compute_fonts_to_load(&empty, &empty).is_empty());
7172 assert!(compute_fonts_to_load(&empty, &all).is_empty());
7173
7174 assert_eq!(compute_fonts_to_load(&all, &empty), all);
7176
7177 let todo = compute_fonts_to_load(&all, &loaded_b);
7179 assert_eq!(todo.len(), 2);
7180 assert!(todo.contains(&a) && todo.contains(&c));
7181 assert!(!todo.contains(&b));
7182
7183 assert!(compute_fonts_to_load(&loaded_b, &all).is_empty());
7185 assert!(compute_fonts_to_load(&all, &all).is_empty());
7186 }
7187
7188 #[test]
7193 fn prune_chain_to_used_chars_keeps_the_first_match_of_every_group_when_nothing_is_needed() {
7194 let mut chain = chain_with(
7195 vec![
7196 CssFallbackGroup {
7197 css_name: "A".to_string(),
7198 fonts: vec![font_match(1, &[(0, 0x10_FFFF)]), font_match(2, &[]), font_match(3, &[])],
7199 },
7200 CssFallbackGroup {
7201 css_name: "B".to_string(),
7202 fonts: vec![font_match(4, &[]), font_match(5, &[])],
7203 },
7204 ],
7205 vec![font_match(6, &[(0x4E00, 0x9FFF)])],
7206 );
7207
7208 prune_chain_to_used_chars(&mut chain, &std::collections::BTreeSet::new());
7209
7210 assert_eq!(chain.css_fallbacks[0].fonts.len(), 1);
7212 assert_eq!(chain.css_fallbacks[0].fonts[0].id, FontId(1));
7213 assert_eq!(chain.css_fallbacks[1].fonts.len(), 1);
7214 assert_eq!(chain.css_fallbacks[1].fonts[0].id, FontId(4));
7215 assert!(chain.unicode_fallbacks.is_empty());
7217 }
7218
7219 #[test]
7220 fn prune_chain_to_used_chars_keeps_walking_until_every_codepoint_is_covered() {
7221 let mut chain = chain_with(
7223 vec![CssFallbackGroup {
7224 css_name: "A".to_string(),
7225 fonts: vec![
7226 font_match(1, &[(0x20, 0x7F)]), font_match(2, &[(0x80, 0x24F)]), font_match(3, &[(0x0, 0x10_FFFF)]), ],
7230 }],
7231 Vec::new(),
7232 );
7233 let used: std::collections::BTreeSet<u32> = [0xE9_u32].into_iter().collect();
7234
7235 prune_chain_to_used_chars(&mut chain, &used);
7236
7237 assert_eq!(
7238 chain.css_fallbacks[0].fonts.len(),
7239 2,
7240 "walk stops as soon as the needed codepoints are covered"
7241 );
7242 assert_eq!(chain.css_fallbacks[0].fonts[1].id, FontId(2));
7243 }
7244
7245 #[test]
7246 fn prune_chain_to_used_chars_keeps_the_whole_group_when_nothing_ever_covers_the_codepoint() {
7247 let mut chain = chain_with(
7248 vec![CssFallbackGroup {
7249 css_name: "A".to_string(),
7250 fonts: vec![font_match(1, &[(0x20, 0x7F)]), font_match(2, &[(0x20, 0x7F)])],
7251 }],
7252 vec![font_match(3, &[(0x20, 0x7F)])],
7253 );
7254 let used: std::collections::BTreeSet<u32> = [u32::MAX].into_iter().collect();
7256
7257 prune_chain_to_used_chars(&mut chain, &used);
7258
7259 assert_eq!(
7260 chain.css_fallbacks[0].fonts.len(),
7261 2,
7262 "an uncoverable codepoint must not silently drop CSS fonts"
7263 );
7264 assert!(
7265 chain.unicode_fallbacks.is_empty(),
7266 "no unicode fallback intersects U+FFFFFFFF"
7267 );
7268 }
7269
7270 #[test]
7271 fn prune_chain_to_used_chars_treats_unicode_ranges_as_inclusive_on_both_ends() {
7272 for probe in [0x4E00_u32, 0x9FFF] {
7273 let mut chain = chain_with(
7274 Vec::new(),
7275 vec![font_match(1, &[(0x4E00, 0x9FFF)]), font_match(2, &[(0x20, 0x7F)])],
7276 );
7277 let used: std::collections::BTreeSet<u32> = [probe].into_iter().collect();
7278 prune_chain_to_used_chars(&mut chain, &used);
7279 assert_eq!(
7280 chain.unicode_fallbacks.len(),
7281 1,
7282 "U+{probe:04X} is inside the inclusive CJK range"
7283 );
7284 assert_eq!(chain.unicode_fallbacks[0].id, FontId(1));
7285 }
7286 for probe in [0x4DFF_u32, 0xA000] {
7288 let mut chain = chain_with(Vec::new(), vec![font_match(1, &[(0x4E00, 0x9FFF)])]);
7289 let used: std::collections::BTreeSet<u32> = [probe].into_iter().collect();
7290 prune_chain_to_used_chars(&mut chain, &used);
7291 assert!(chain.unicode_fallbacks.is_empty(), "U+{probe:04X}");
7292 }
7293 }
7294
7295 #[test]
7296 fn prune_chain_to_used_chars_survives_empty_chains_and_empty_groups() {
7297 let mut empty = chain_with(Vec::new(), Vec::new());
7298 prune_chain_to_used_chars(&mut empty, &std::collections::BTreeSet::new());
7299 assert!(empty.css_fallbacks.is_empty());
7300 assert!(empty.unicode_fallbacks.is_empty());
7301
7302 let mut fontless = chain_with(
7304 vec![CssFallbackGroup {
7305 css_name: "A".to_string(),
7306 fonts: Vec::new(),
7307 }],
7308 Vec::new(),
7309 );
7310 let used: std::collections::BTreeSet<u32> = [0x1F389_u32].into_iter().collect();
7311 prune_chain_to_used_chars(&mut fontless, &used);
7312 assert_eq!(fontless.css_fallbacks.len(), 1);
7313 assert!(fontless.css_fallbacks[0].fonts.is_empty());
7314 }
7315
7316 #[test]
7321 fn build_font_selector_stack_always_appends_the_three_generic_fallbacks() {
7322 let families = StyleFontFamilyVec::from_vec(Vec::new());
7323 let stack = build_font_selector_stack(&families, None, FcWeight::Normal, FontStyle::Normal);
7324
7325 let names: Vec<&str> = stack.iter().map(|s| s.family.as_str()).collect();
7326 assert_eq!(names, ["sans-serif", "serif", "monospace"]);
7327 for s in &stack {
7328 assert_eq!(s.weight, FcWeight::Normal);
7329 assert_eq!(s.style, FontStyle::Normal);
7330 }
7331 }
7332
7333 #[test]
7334 fn build_font_selector_stack_puts_the_authored_families_first() {
7335 let families = StyleFontFamilyVec::from_vec(vec![
7336 StyleFontFamily::System("Iosevka".to_string().into()),
7337 StyleFontFamily::System("Menlo".to_string().into()),
7338 ]);
7339 let stack = build_font_selector_stack(&families, None, FcWeight::Bold, FontStyle::Italic);
7340
7341 assert_eq!(stack.len(), 5, "2 authored + 3 generic fallbacks");
7342 assert_eq!(stack[0].family, "Iosevka");
7343 assert_eq!(stack[1].family, "Menlo");
7344 assert_eq!(stack[0].weight, FcWeight::Bold);
7346 assert_eq!(stack[0].style, FontStyle::Italic);
7347 assert_eq!(stack[4].family, "monospace");
7349 assert_eq!(stack[4].weight, FcWeight::Normal);
7350 assert_eq!(stack[4].style, FontStyle::Normal);
7351 }
7352
7353 #[test]
7354 fn build_font_selector_stack_does_not_duplicate_a_generic_the_author_already_listed() {
7355 let families =
7357 StyleFontFamilyVec::from_vec(vec![StyleFontFamily::System("MONOSPACE".to_string().into())]);
7358 let stack = build_font_selector_stack(&families, None, FcWeight::Normal, FontStyle::Normal);
7359
7360 assert_eq!(stack.len(), 3, "MONOSPACE + sans-serif + serif");
7361 assert_eq!(stack[0].family, "MONOSPACE");
7362 let lower: Vec<String> = stack.iter().map(|s| s.family.to_lowercase()).collect();
7363 assert_eq!(
7364 lower.iter().filter(|f| f.as_str() == "monospace").count(),
7365 1,
7366 "the generic must appear exactly once"
7367 );
7368
7369 let families = StyleFontFamilyVec::from_vec(vec![
7371 StyleFontFamily::System("serif".to_string().into()),
7372 StyleFontFamily::System("Sans-Serif".to_string().into()),
7373 StyleFontFamily::System("monospace".to_string().into()),
7374 ]);
7375 let stack = build_font_selector_stack(&families, None, FcWeight::Normal, FontStyle::Normal);
7376 assert_eq!(stack.len(), 3);
7377 }
7378
7379 #[test]
7380 fn build_font_selector_stack_passes_hostile_family_names_through_untouched() {
7381 let huge = "A".repeat(10_000);
7382 let families = StyleFontFamilyVec::from_vec(vec![
7383 StyleFontFamily::System(String::new().into()),
7384 StyleFontFamily::System(" \t\n ".to_string().into()),
7385 StyleFontFamily::System("M🎉 ǝɔɐɟdʎʇ — «Шрифт»".to_string().into()),
7386 StyleFontFamily::System(huge.clone().into()),
7387 ]);
7388 let stack = build_font_selector_stack(&families, None, FcWeight::Normal, FontStyle::Normal);
7389
7390 assert_eq!(stack.len(), 7, "4 authored + 3 generic fallbacks");
7391 assert_eq!(stack[0].family, "");
7392 assert_eq!(stack[2].family, "M🎉 ǝɔɐɟdʎʇ — «Шрифт»");
7393 assert_eq!(stack[3].family.len(), huge.len());
7394 assert_eq!(stack[6].family, "monospace");
7395 }
7396
7397 #[test]
7402 fn resolve_font_chains_yields_nothing_for_an_empty_or_degenerate_collection() {
7403 let fc = FcFontCache::default();
7404
7405 let collected = CollectedFontStacks {
7406 font_stacks: Vec::new(),
7407 hash_to_index: HashMap::new(),
7408 font_refs: HashMap::new(),
7409 };
7410 assert!(resolve_font_chains(&collected, &fc, Some(&[])).is_empty());
7411
7412 let collected = CollectedFontStacks {
7414 font_stacks: vec![Vec::new()],
7415 hash_to_index: HashMap::new(),
7416 font_refs: HashMap::new(),
7417 };
7418 assert!(resolve_font_chains(&collected, &fc, Some(&[])).is_empty());
7419 }
7420
7421 #[test]
7426 fn font_size_getters_return_the_default_for_an_unstyled_dom() {
7427 let sd = StyledDom::default();
7428 let root = NodeId::new(0);
7429 let st = normal();
7430
7431 assert_eq!(get_element_font_size(&sd, root, &st), DEFAULT_FONT_SIZE);
7432 assert_eq!(get_root_font_size(&sd, &st), DEFAULT_FONT_SIZE);
7433 assert_eq!(get_parent_font_size(&sd, root, &st), DEFAULT_FONT_SIZE);
7435 assert_eq!(
7436 resolve_font_size_slow(&sd, root, &st),
7437 DEFAULT_FONT_SIZE,
7438 "the slow path must agree with the memoised one"
7439 );
7440 }
7441
7442 #[test]
7443 fn font_size_resolution_is_identical_on_the_normal_and_the_pseudo_state_paths() {
7444 let sd = body_with_divs(1, "body { font-size: 32px; }");
7447 let child = NodeId::new(1);
7448 assert_eq!(
7449 get_element_font_size(&sd, child, &normal()),
7450 get_element_font_size(&sd, child, &hovered()),
7451 );
7452 }
7453
7454 #[test]
7455 fn font_size_em_resolves_against_the_parent_and_not_the_default() {
7456 let sd = body_with_divs(1, "body { font-size: 32px; } div { font-size: 2em; }");
7457 let root = NodeId::new(0);
7458 let child = NodeId::new(1);
7459
7460 assert_eq!(get_element_font_size(&sd, root, &state_of(&sd, root)), 32.0);
7461 assert_eq!(
7462 get_element_font_size(&sd, child, &state_of(&sd, child)),
7463 64.0,
7464 "2em under a 32px parent is 64px — resolving against DEFAULT_FONT_SIZE would give 32"
7465 );
7466 assert_eq!(
7467 get_parent_font_size(&sd, child, &state_of(&sd, child)),
7468 32.0
7469 );
7470 assert_eq!(get_root_font_size(&sd, &state_of(&sd, child)), 32.0);
7471 }
7472
7473 #[test]
7474 fn font_size_getters_stay_finite_for_hostile_stylesheet_values() {
7475 for css in [
7478 "body { font-size: 0px; }",
7479 "body { font-size: 0; }",
7480 "body { font-size: 999999px; }",
7481 "body { font-size: -10px; }",
7482 "body { font-size: 1e30px; }",
7483 "div { font-size: 1000em; }",
7484 "div { font-size: 0em; }",
7485 ] {
7486 let sd = body_with_divs(1, css);
7487 for id in [NodeId::new(0), NodeId::new(1)] {
7488 let st = state_of(&sd, id);
7489 let px = get_element_font_size(&sd, id, &st);
7490 assert!(
7491 px.is_finite(),
7492 "{css:?} produced a non-finite font-size ({px}) on node {id:?}"
7493 );
7494 assert_eq!(
7495 px,
7496 resolve_font_size_slow(&sd, id, &st),
7497 "memoised and slow paths disagree for {css:?}"
7498 );
7499 }
7500 }
7501 }
7502
7503 #[test]
7504 fn font_size_resolution_walks_a_deep_ancestor_chain_without_recursing() {
7505 const DEPTH: usize = 64;
7508 let mut dom = Dom::create_div();
7509 for _ in 0..DEPTH {
7510 dom = Dom::create_div().with_children(vec![dom].into());
7511 }
7512 let mut root = Dom::create_body().with_children(vec![dom].into());
7513 let sd = StyledDom::create(&mut root, parse("body { font-size: 20px; }"));
7514
7515 let deepest = NodeId::new(sd.node_data.len() - 1);
7516 let st = state_of(&sd, deepest);
7517 let px = get_element_font_size(&sd, deepest, &st);
7518 assert!(px.is_finite() && px > 0.0);
7519 assert_eq!(px, resolve_font_size_slow(&sd, deepest, &st));
7520 }
7521
7522 #[test]
7523 fn resolve_font_size_one_is_stable_under_nan_and_infinite_context_sizes() {
7524 let sd = StyledDom::default();
7527 let root = NodeId::new(0);
7528 let st = normal();
7529 for (parent, rootsz) in [
7530 (0.0_f32, 0.0_f32),
7531 (f32::NAN, f32::NAN),
7532 (f32::INFINITY, f32::NEG_INFINITY),
7533 (f32::MAX, f32::MIN),
7534 (-1.0, -1.0),
7535 ] {
7536 let px = resolve_font_size_one(&sd, root, &st, parent, rootsz);
7537 assert_eq!(
7538 px, DEFAULT_FONT_SIZE,
7539 "an unstyled node ignores the context and falls back to the default \
7540 (parent={parent}, root={rootsz})"
7541 );
7542 }
7543 }
7544
7545 #[test]
7550 fn optional_node_getters_return_their_documented_defaults_for_none() {
7551 let sd = StyledDom::default();
7552
7553 assert_eq!(get_z_index(&sd, None), 0);
7554 assert!(is_z_index_auto(&sd, None));
7555 assert_eq!(get_break_before(&sd, None), PageBreak::Auto);
7556 assert_eq!(get_break_after(&sd, None), PageBreak::Auto);
7557 assert_eq!(get_break_inside(&sd, None), BreakInside::Auto);
7558 assert_eq!(get_orphans(&sd, None), 2);
7559 assert_eq!(get_widows(&sd, None), 2);
7560 assert_eq!(
7561 get_box_decoration_break(&sd, None),
7562 BoxDecorationBreak::Slice
7563 );
7564 assert_eq!(
7565 get_display_property(&sd, None),
7566 MultiValue::Exact(LayoutDisplay::Inline),
7567 "a missing node is treated as anonymous inline content"
7568 );
7569 assert_eq!(get_list_style_type(&sd, None), StyleListStyleType::default());
7570 assert_eq!(
7571 get_list_style_position(&sd, None),
7572 StyleListStylePosition::default()
7573 );
7574 assert_eq!(get_caret_style(&sd, None).width, DEFAULT_CARET_WIDTH_PX);
7575 assert_eq!(
7576 get_caret_style(&sd, None).animation_duration,
7577 DEFAULT_CARET_BLINK_MS
7578 );
7579 let sel = get_selection_style(&sd, None, None);
7580 assert_eq!(sel.radius, 0.0);
7581 assert_eq!(sel.text_color, None);
7582 }
7583
7584 #[test]
7585 fn z_index_defaults_to_auto_and_reads_back_explicit_integers() {
7586 let sd = body_with_divs(1, "");
7587 let root = NodeId::new(0);
7588 assert_eq!(get_z_index(&sd, Some(root)), 0);
7589 assert!(is_z_index_auto(&sd, Some(root)));
7590
7591 for (css, want) in [
7592 ("div { z-index: 0; }", 0_i32),
7593 ("div { z-index: 7; }", 7),
7594 ("div { z-index: -7; }", -7),
7595 ] {
7596 let sd = body_with_divs(1, css);
7597 let child = Some(NodeId::new(1));
7598 assert_eq!(get_z_index(&sd, child), want, "{css:?}");
7599 assert!(
7600 !is_z_index_auto(&sd, child),
7601 "an explicit integer is not auto ({css:?})"
7602 );
7603 }
7604
7605 let sd = body_with_divs(1, "div { z-index: auto; }");
7607 let child = Some(NodeId::new(1));
7608 assert_eq!(get_z_index(&sd, child), 0);
7609 assert!(is_z_index_auto(&sd, child));
7610 }
7611
7612 #[test]
7613 fn z_index_reads_back_the_i16_encoding_boundaries_and_falls_through_above_them() {
7614 for (css, want) in [
7618 ("div { z-index: 32763; }", 32_763_i32), ("div { z-index: -32768; }", -32_768), ("div { z-index: 32764; }", 32_764), ("div { z-index: 99999; }", 99_999), ("div { z-index: 2147483647; }", i32::MAX),
7623 ] {
7624 let sd = body_with_divs(1, css);
7625 let child = Some(NodeId::new(1));
7626 assert_eq!(
7627 get_z_index(&sd, child),
7628 want,
7629 "{css:?} must survive the i16 compact encoding"
7630 );
7631 assert!(
7632 !is_z_index_auto(&sd, child),
7633 "an explicit (if huge) integer is not auto ({css:?})"
7634 );
7635 }
7636 }
7637
7638 #[test]
7643 fn border_radius_is_zero_by_default_for_every_degenerate_element_and_viewport_size() {
7644 let sd = StyledDom::default();
7645 let root = NodeId::new(0);
7646 let st = normal();
7647
7648 let sizes = [
7649 (0.0_f32, 0.0_f32),
7650 (-100.0, -100.0),
7651 (f32::NAN, f32::NAN),
7652 (f32::INFINITY, f32::INFINITY),
7653 (f32::MAX, f32::MAX),
7654 (f32::MIN_POSITIVE, f32::MIN_POSITIVE),
7655 ];
7656 for (w, h) in sizes {
7657 let element = PhysicalSizeImport {
7658 width: w,
7659 height: h,
7660 };
7661 let viewport = LogicalSize::new(w, h);
7662 let r = get_border_radius(&sd, root, &st, element, viewport);
7663 assert_eq!(r.top_left, 0.0, "element=({w}, {h})");
7664 assert_eq!(r.top_right, 0.0, "element=({w}, {h})");
7665 assert_eq!(r.bottom_left, 0.0, "element=({w}, {h})");
7666 assert_eq!(r.bottom_right, 0.0, "element=({w}, {h})");
7667 }
7668 }
7669
7670 #[test]
7671 fn border_radius_resolves_authored_pixels_on_both_the_normal_and_the_pseudo_path() {
7672 let sd = body_with_divs(1, "div { border-radius: 12px; }");
7673 let child = NodeId::new(1);
7674 let element = PhysicalSizeImport {
7675 width: 100.0,
7676 height: 50.0,
7677 };
7678 let viewport = LogicalSize::new(800.0, 600.0);
7679
7680 for st in [normal(), hovered()] {
7681 let r = get_border_radius(&sd, child, &st, element, viewport);
7682 for corner in [r.top_left, r.top_right, r.bottom_left, r.bottom_right] {
7683 assert!(corner.is_finite(), "corner must stay finite");
7684 assert_eq!(corner, 12.0);
7685 }
7686 }
7687
7688 let raw = get_style_border_radius(&sd, child, &normal());
7689 assert!(raw.top_left.number.get().is_finite());
7690 }
7691
7692 #[test]
7693 fn border_radius_percentages_stay_finite_for_zero_and_infinite_element_sizes() {
7694 let sd = body_with_divs(1, "div { border-radius: 50%; }");
7695 let child = NodeId::new(1);
7696 let viewport = LogicalSize::new(0.0, 0.0);
7697
7698 for (w, h) in [
7699 (0.0_f32, 0.0_f32),
7700 (f32::MAX, f32::MAX),
7701 (-10.0, -10.0),
7702 (f32::INFINITY, 1.0),
7703 ] {
7704 let element = PhysicalSizeImport {
7705 width: w,
7706 height: h,
7707 };
7708 let r = get_border_radius(&sd, child, &hovered(), element, viewport);
7711 for corner in [r.top_left, r.top_right, r.bottom_left, r.bottom_right] {
7712 assert!(
7713 !corner.is_nan(),
7714 "a {w}x{h} element produced a NaN corner radius"
7715 );
7716 }
7717 }
7718 }
7719
7720 #[test]
7725 fn optional_style_getters_are_all_none_on_an_unstyled_node() {
7726 let sd = body_with_divs(1, "");
7727 let id = NodeId::new(1);
7728
7729 for st in [normal(), hovered()] {
7730 assert!(get_shape_inside(&sd, id, &st).is_none());
7731 assert!(get_shape_outside(&sd, id, &st).is_none());
7732 assert!(get_line_clamp(&sd, id, &st).is_none());
7733 assert!(get_initial_letter(&sd, id, &st).is_none());
7734 assert!(get_hanging_punctuation(&sd, id, &st).is_none());
7735 assert!(get_text_combine_upright(&sd, id, &st).is_none());
7736 assert!(get_hyphenation_language(&sd, id, &st).is_none());
7737 assert!(get_column_count(&sd, id, &st).is_none());
7738 assert!(get_filter(&sd, id, &st).is_none());
7739 assert!(get_backdrop_filter(&sd, id, &st).is_none());
7740 assert!(get_box_shadow_left(&sd, id, &st).is_none());
7741 assert!(get_box_shadow_right(&sd, id, &st).is_none());
7742 assert!(get_box_shadow_top(&sd, id, &st).is_none());
7743 assert!(get_box_shadow_bottom(&sd, id, &st).is_none());
7744 assert!(get_text_shadow(&sd, id, &st).is_none());
7745 assert!(get_transform(&sd, id, &st).is_none());
7746 assert!(get_counter_reset(&sd, id, &st).is_none());
7747 assert!(get_counter_increment(&sd, id, &st).is_none());
7748 assert!(get_clip_path(&sd, id, &st).is_none());
7749 assert!(get_grid_template_areas_prop(&sd, id, &st).is_none());
7750 }
7751 }
7752
7753 #[test]
7754 fn numeric_style_getters_use_their_documented_defaults() {
7755 let sd = body_with_divs(1, "");
7756 let id = NodeId::new(1);
7757
7758 for st in [normal(), hovered()] {
7759 assert_eq!(get_opacity(&sd, id, &st), 1.0, "opacity defaults to 1.0");
7760 assert_eq!(
7761 get_exclusion_margin(&sd, id, &st),
7762 0.0,
7763 "exclusion-margin defaults to 0.0"
7764 );
7765 assert!(get_scrollbar_width_px(&sd, id, &st).is_finite());
7766 assert!(get_scrollbar_width_px(&sd, id, &st) >= 0.0);
7767 }
7768 }
7769
7770 #[test]
7771 fn opacity_in_range_agrees_on_the_compact_and_the_cascade_path() {
7772 for css in [
7773 "div { opacity: 0; }",
7774 "div { opacity: 1; }",
7775 "div { opacity: 0.5; }",
7776 ] {
7777 let sd = body_with_divs(1, css);
7778 let id = NodeId::new(1);
7779 let fast = get_opacity(&sd, id, &normal()); let slow = get_opacity(&sd, id, &hovered()); assert!(fast.is_finite() && slow.is_finite(), "{css:?}");
7782 assert!(
7783 (0.0..=1.0).contains(&fast),
7784 "{css:?} read back out of range on the compact path: {fast}"
7785 );
7786 assert!(
7787 (fast - slow).abs() < 0.01,
7788 "{css:?}: compact path says {fast}, cascade path says {slow}"
7789 );
7790 }
7791
7792 let half = get_opacity(&body_with_divs(1, "div { opacity: 0.5; }"), NodeId::new(1), &normal());
7794 assert!(half < 1.0 && half > 0.0, "opacity: 0.5 read back as {half}");
7795 }
7796
7797 #[test]
7798 fn opacity_never_returns_nan_or_infinity_for_out_of_range_authored_values() {
7799 for css in [
7805 "div { opacity: 5; }",
7806 "div { opacity: -3; }",
7807 "div { opacity: 1e30; }",
7808 ] {
7809 let sd = body_with_divs(1, css);
7810 let id = NodeId::new(1);
7811 for st in [normal(), hovered()] {
7812 let o = get_opacity(&sd, id, &st);
7813 assert!(o.is_finite(), "{css:?} produced a non-finite opacity: {o}");
7814 }
7815 let fast = get_opacity(&sd, id, &normal());
7817 assert!(
7818 (0.0..=1.0).contains(&fast),
7819 "{css:?} escaped the compact-cache clamp: {fast}"
7820 );
7821 }
7822 }
7823
7824 #[test]
7825 fn enum_property_getters_stay_deterministic_across_pseudo_states() {
7826 let sd = body_with_divs(1, "");
7827 let id = NodeId::new(1);
7828
7829 for st in [normal(), hovered()] {
7830 let gutter = get_scrollbar_gutter_property(&sd, id, &st);
7833 assert_eq!(gutter, get_scrollbar_gutter_property(&sd, id, &st));
7834 let orientation = get_text_orientation_property(&sd, id, &st);
7835 assert_eq!(orientation, get_text_orientation_property(&sd, id, &st));
7836 let valign = get_vertical_align_property(&sd, id, &st);
7837 assert_eq!(valign, get_vertical_align_property(&sd, id, &st));
7838
7839 let _ = get_background_color(&sd, id, &st);
7840 let _ = get_background_contents(&sd, id, &st);
7841 let _ = get_border_info(&sd, id, &st);
7842 let _ = get_border_spacing(&sd, id, &st);
7843 let _ = get_height_value(&sd, id, &st);
7844 let _ = get_line_height_value(&sd, id, &st);
7845 let _ = get_text_indent_value(&sd, id, &st);
7846 }
7847
7848 assert!(matches!(
7850 get_vertical_align_for_node(&sd, id),
7851 crate::text3::cache::VerticalAlign::Baseline
7852 ));
7853 }
7854
7855 #[test]
7856 fn get_inline_border_info_is_none_without_borders_and_survives_a_degenerate_viewport() {
7857 let sd = body_with_divs(1, "");
7858 let id = NodeId::new(1);
7859 let st = normal();
7860 let info = get_border_info(&sd, id, &st);
7861
7862 for viewport in [
7863 PhysicalSize::new(0.0, 0.0),
7864 PhysicalSize::new(f32::NAN, f32::NAN),
7865 PhysicalSize::new(f32::INFINITY, f32::INFINITY),
7866 PhysicalSize::new(-1.0, -1.0),
7867 PhysicalSize::new(f32::MAX, f32::MAX),
7868 ] {
7869 assert!(
7870 get_inline_border_info(&sd, id, &st, &info, viewport).is_none(),
7871 "a node with neither border nor padding has no inline border box"
7872 );
7873 }
7874 }
7875
7876 #[test]
7877 fn get_inline_border_info_reports_finite_widths_for_a_bordered_node() {
7878 let sd = body_with_divs(1, "div { border: 3px solid red; padding: 5px; }");
7879 let id = NodeId::new(1);
7880 let st = normal();
7881 let info = get_border_info(&sd, id, &st);
7882
7883 let inline = get_inline_border_info(&sd, id, &st, &info, PhysicalSize::new(800.0, 600.0))
7884 .expect("a bordered + padded node must produce an InlineBorderInfo");
7885 for w in [inline.top, inline.right, inline.bottom, inline.left] {
7886 assert!(w.is_finite() && w >= 0.0, "border width {w}");
7887 }
7888 for p in [
7889 inline.padding_top,
7890 inline.padding_right,
7891 inline.padding_bottom,
7892 inline.padding_left,
7893 ] {
7894 assert!(p.is_finite() && p >= 0.0, "padding {p}");
7895 }
7896 assert!(inline.is_first_fragment && inline.is_last_fragment);
7897 assert!(!inline.is_rtl, "the default direction is ltr");
7898
7899 let nan_vp = get_inline_border_info(&sd, id, &st, &info, PhysicalSize::new(f32::NAN, f32::NAN))
7902 .expect("px borders do not depend on the viewport");
7903 assert!(nan_vp.top.is_finite() && nan_vp.padding_top.is_finite());
7904 }
7905
7906 #[test]
7907 fn get_style_properties_stays_finite_for_every_degenerate_viewport() {
7908 let sd = body_with_text("hello");
7909 for viewport in [
7910 PhysicalSize::new(0.0, 0.0),
7911 PhysicalSize::new(-1.0, -1.0),
7912 PhysicalSize::new(f32::NAN, f32::NAN),
7913 PhysicalSize::new(f32::INFINITY, f32::INFINITY),
7914 PhysicalSize::new(f32::MAX, f32::MAX),
7915 ] {
7916 for id in [NodeId::new(0), NodeId::new(1)] {
7917 let props = get_style_properties(&sd, id, None, viewport);
7918 assert!(
7919 props.font_size_px.is_finite(),
7920 "viewport {viewport:?} produced a non-finite font size"
7921 );
7922 assert!(props.font_size_px > 0.0);
7923 }
7924 }
7925 }
7926
7927 #[test]
7932 fn is_text_selectable_is_true_by_default_and_false_for_user_select_none() {
7933 let sd = body_with_divs(1, "");
7934 assert!(
7935 is_text_selectable(&sd, NodeId::new(1), &normal()),
7936 "text is selectable unless user-select says otherwise"
7937 );
7938
7939 let sd = body_with_divs(1, "div { user-select: none; }");
7940 assert!(!is_text_selectable(&sd, NodeId::new(1), &normal()));
7941
7942 let sd = body_with_divs(1, "div { user-select: text; }");
7943 assert!(is_text_selectable(&sd, NodeId::new(1), &normal()));
7944 }
7945
7946 #[test]
7947 fn contenteditable_is_false_everywhere_on_a_plain_dom() {
7948 let sd = body_with_divs(2, "");
7949 for idx in 0..sd.node_data.len() {
7950 let id = NodeId::new(idx);
7951 assert!(!is_node_contenteditable(&sd, id), "node {idx}");
7952 assert!(!is_node_contenteditable_inherited(&sd, id), "node {idx}");
7953 assert_eq!(find_contenteditable_ancestor(&sd, id), None, "node {idx}");
7954 }
7955 }
7956
7957 #[test]
7958 fn contenteditable_is_inherited_by_descendants_but_not_reported_as_direct() {
7959 let mut editable = Dom::create_div().with_children(vec![Dom::create_div()].into());
7961 editable.root.set_contenteditable(true);
7962 let mut dom = Dom::create_body().with_children(vec![editable].into());
7963 let sd = StyledDom::create(&mut dom, Css::empty());
7964 assert_eq!(sd.node_data.len(), 3);
7965
7966 let (body, editable, child) = (NodeId::new(0), NodeId::new(1), NodeId::new(2));
7967
7968 assert!(!is_node_contenteditable(&sd, body));
7969 assert!(is_node_contenteditable(&sd, editable));
7970 assert!(
7971 !is_node_contenteditable(&sd, child),
7972 "the direct check must not walk up the tree"
7973 );
7974
7975 assert!(!is_node_contenteditable_inherited(&sd, body));
7976 assert!(is_node_contenteditable_inherited(&sd, editable));
7977 assert!(
7978 is_node_contenteditable_inherited(&sd, child),
7979 "editability is inherited from the ancestor"
7980 );
7981
7982 assert_eq!(find_contenteditable_ancestor(&sd, body), None);
7983 assert_eq!(find_contenteditable_ancestor(&sd, editable), Some(editable));
7984 assert_eq!(
7985 find_contenteditable_ancestor(&sd, child),
7986 Some(editable),
7987 "a nested node resolves to its editable container, not to itself"
7988 );
7989 }
7990
7991 #[test]
7996 fn collect_used_codepoints_strips_ascii_while_the_all_variant_keeps_it() {
7997 let sd = body_with_text("aé漢🎉");
7999
8000 let non_ascii = collect_used_codepoints(&sd);
8001 assert_eq!(non_ascii.len(), 3, "the ASCII 'a' is dropped");
8002 assert!(non_ascii.contains(&0x00E9));
8003 assert!(non_ascii.contains(&0x6F22));
8004 assert!(non_ascii.contains(&0x0001_F389), "astral plane codepoint");
8005 assert!(!non_ascii.contains(&u32::from(b'a')));
8006
8007 let all = collect_used_codepoints_all(&sd);
8008 assert_eq!(all.len(), 4);
8009 assert!(all.contains(&'a'));
8010 assert!(all.contains(&'🎉'));
8011 }
8012
8013 #[test]
8014 fn collect_used_codepoints_dedupes_and_handles_empty_and_ascii_only_text() {
8015 let sd = body_with_text("ααα");
8017 assert_eq!(collect_used_codepoints(&sd).len(), 1);
8018
8019 let sd = body_with_text("");
8020 assert!(collect_used_codepoints(&sd).is_empty());
8021 assert!(collect_used_codepoints_all(&sd).is_empty());
8022
8023 let sd = body_with_divs(3, "");
8024 assert!(
8025 collect_used_codepoints(&sd).is_empty(),
8026 "element nodes carry no codepoints"
8027 );
8028
8029 let sd = body_with_text("plain ascii");
8030 assert!(collect_used_codepoints(&sd).is_empty());
8031 assert!(!collect_used_codepoints_all(&sd).is_empty());
8032 }
8033
8034 #[test]
8035 fn scripts_present_in_styled_dom_is_empty_for_ascii_and_bounded_by_the_default_set() {
8036 let ascii = body_with_text("hello world");
8037 assert!(
8038 scripts_present_in_styled_dom(&ascii).is_empty(),
8039 "an ASCII-only page must not drag in any unicode fallback script"
8040 );
8041
8042 let empty = StyledDom::default();
8043 assert!(scripts_present_in_styled_dom(&empty).is_empty());
8044
8045 let cjk = body_with_text("漢字");
8046 let scripts = scripts_present_in_styled_dom(&cjk);
8047 assert!(!scripts.is_empty(), "CJK text must report at least one script");
8048 assert!(
8049 scripts.len() <= DEFAULT_UNICODE_FALLBACK_SCRIPTS.len(),
8050 "the result is always a subset of the default script set"
8051 );
8052 for r in &scripts {
8053 assert!(r.start <= r.end, "a script range must not be inverted");
8054 }
8055 }
8056
8057 #[test]
8058 fn collect_font_stacks_from_styled_dom_keeps_its_index_map_consistent() {
8059 let platform = azul_css::system::Platform::current();
8060
8061 for sd in [
8062 StyledDom::default(),
8063 body_with_text("hello"),
8064 body_with_divs(3, "div { font-family: Iosevka, monospace; }"),
8065 ] {
8066 let collected = collect_font_stacks_from_styled_dom(&sd, &platform);
8067 assert_eq!(
8068 collected.hash_to_index.len(),
8069 collected.font_stacks.len(),
8070 "every recorded hash must map to exactly one stack"
8071 );
8072 for &idx in collected.hash_to_index.values() {
8073 assert!(
8074 idx < collected.font_stacks.len(),
8075 "hash_to_index points past the end of font_stacks"
8076 );
8077 }
8078 for stack in &collected.font_stacks {
8079 assert!(!stack.is_empty(), "an empty font stack is never recorded");
8080 }
8081 }
8082 }
8083}