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 time::CssDuration,
17 ColorU, PhysicalSize, PixelValue, PropertyContext, ResolutionContext,
18 },
19 layout::{
20 grid::GridTemplateAreas, BoxDecorationBreak, BreakInside, LayoutAlignContent,
21 LayoutAlignItems, LayoutBoxSizing, LayoutClear, LayoutDisplay, LayoutFlexDirection,
22 LayoutFlexWrap, LayoutFloat, LayoutHeight, LayoutJustifyContent, LayoutOverflow,
23 LayoutPosition, LayoutWidth, LayoutWritingMode, Orphans, PageBreak,
24 StyleOverflowClipMargin, StyleScrollbarGutter, Widows,
25 },
26 property::{
27 CssProperty, CssPropertyType, LayoutAlignContentValue, LayoutAlignItemsValue,
28 LayoutAlignSelfValue, LayoutFlexBasisValue, LayoutFlexDirectionValue,
29 LayoutFlexGrowValue, LayoutFlexShrinkValue, LayoutFlexWrapValue, LayoutGapValue,
30 LayoutGridAutoColumnsValue, LayoutGridAutoFlowValue, LayoutGridAutoRowsValue,
31 LayoutGridColumnValue, LayoutGridRowValue, LayoutGridTemplateColumnsValue,
32 LayoutGridTemplateRowsValue, LayoutJustifyContentValue, LayoutJustifyItemsValue,
33 LayoutJustifySelfValue,
34 },
35 style::{
36 border_radius::StyleBorderRadius,
37 lists::{StyleListStylePosition, StyleListStyleType},
38 StyleAlignmentBaseline, StyleBaselineSource, StyleDirection, StyleDominantBaseline,
39 StyleInitialLetterAlign, StyleLineFitEdge,
40 StyleInitialLetterWrap, StyleTextAlign, StyleTextBoxEdge, StyleTextBoxTrim,
41 StyleUnicodeBidi, StyleUserSelect, StyleVerticalAlign, StyleVisibility,
42 StyleWhiteSpace,
43 },
44 },
45};
46
47use crate::{
48 font_traits::{ParsedFontTrait, StyleProperties},
49 solver3::{
50 display_list::{BorderRadius, PhysicalSizeImport},
51 layout_tree::LayoutNode,
52 scrollbar::ScrollbarRequirements,
53 },
54};
55
56const DEFAULT_EM_SIZE: f32 = 16.0;
57const DEFAULT_CARET_WIDTH_PX: f32 = 2.0;
58const DEFAULT_CARET_BLINK_MS: u32 = 500;
59const DEFAULT_TAB_SIZE: f32 = 8.0;
60const SCROLLBAR_WIDTH_THIN: f32 = 8.0;
61const SCROLLBAR_WIDTH_AUTO: f32 = 12.0;
62const SCROLLBAR_HOVER_EXPAND_PX: f32 = 4.0;
63const THUMB_HOVER_LIGHTEN: u8 = 30;
64const THUMB_HOVER_ALPHA_ADD: u8 = 40;
65const THUMB_ACTIVE_DARKEN: u8 = 15;
66
67#[must_use] pub fn get_element_font_size(
88 styled_dom: &StyledDom,
89 dom_id: NodeId,
90 node_state: &StyledNodeState,
91) -> f32 {
92 let _ = compute_all_font_sizes_px; resolve_font_size_slow(styled_dom, dom_id, node_state)
105}
106
107fn compute_all_font_sizes_px(styled_dom: &StyledDom) -> Vec<f32> {
125 use azul_css::props::{
126 basic::length::SizeMetric,
127 property::{CssProperty, CssPropertyType},
128 };
129
130 let n = styled_dom.node_data.len();
131 let mut sizes = alloc::vec![DEFAULT_FONT_SIZE; n];
132 if n == 0 {
133 return sizes;
134 }
135
136 let data_container = styled_dom.node_data.as_container();
137 let state_container = styled_dom.styled_nodes.as_container();
138 let hierarchy = styled_dom.node_hierarchy.as_container();
139 let cache = &styled_dom.css_property_cache.ptr;
140
141 for idx in 0..n {
142 let dom_id = NodeId::new(idx);
143
144 if let Some(vec) = cache.computed_values.get(idx) {
146 if let Ok(cv_idx) = vec.binary_search_by_key(&CssPropertyType::FontSize, |(k, _)| *k) {
147 if let CssProperty::FontSize(css_val) = &vec[cv_idx].1.property {
148 if let Some(fs) = css_val.get_property() {
149 if fs.inner.metric == SizeMetric::Px {
150 sizes[idx] = fs.inner.number.get();
151 continue;
152 }
153 }
154 }
155 }
156 }
157
158 let parent_font_size = hierarchy
160 .get(dom_id)
161 .and_then(azul_core::styled_dom::NodeHierarchyItem::parent_id)
162 .map_or(DEFAULT_FONT_SIZE, |p| sizes[p.index()]);
163 let root_font_size = sizes[0];
164
165 let Some(node_data) = data_container.internal.get(idx) else {
166 sizes[idx] = DEFAULT_FONT_SIZE;
167 continue;
168 };
169 let Some(styled) = state_container.internal.get(idx) else {
170 sizes[idx] = DEFAULT_FONT_SIZE;
171 continue;
172 };
173 let node_state = &styled.styled_node_state;
174
175 let mut fast_fs: Option<f32> = None;
179 let mut compact_said_inherit = false;
180 if node_state.is_normal() {
181 if let Some(ref cc) = cache.compact_cache {
182 let raw = cc.get_font_size_raw(idx);
183 if raw == azul_css::compact_cache::U32_SENTINEL
184 || raw == azul_css::compact_cache::U32_INHERIT
185 || raw == azul_css::compact_cache::U32_INITIAL
186 {
187 compact_said_inherit = true;
188 } else if let Some(pv) = azul_css::compact_cache::decode_pixel_value_u32(raw) {
189 if pv.metric == SizeMetric::Px {
191 fast_fs = Some(pv.number.get());
192 } else {
193 let context = ResolutionContext {
195 element_font_size: DEFAULT_FONT_SIZE,
196 parent_font_size,
197 root_font_size,
198 containing_block_size: PhysicalSize::new(0.0, 0.0),
199 element_size: None,
200 viewport_size: PhysicalSize::new(0.0, 0.0),
201 };
202 fast_fs =
203 Some(pv.resolve_with_context(&context, PropertyContext::FontSize));
204 }
205 }
206 }
207 }
208 if let Some(fs) = fast_fs {
209 sizes[idx] = fs;
210 continue;
211 }
212 if compact_said_inherit {
213 sizes[idx] = parent_font_size;
214 continue;
215 }
216
217 let resolved = cache
218 .get_font_size(node_data, &dom_id, node_state)
219 .and_then(|v| v.get_property().copied())
220 .map(|v| {
221 let context = ResolutionContext {
222 element_font_size: DEFAULT_FONT_SIZE,
223 parent_font_size,
224 root_font_size,
225 containing_block_size: PhysicalSize::new(0.0, 0.0),
226 element_size: None,
227 viewport_size: PhysicalSize::new(0.0, 0.0),
228 };
229 v.inner
230 .resolve_with_context(&context, PropertyContext::FontSize)
231 });
232
233 sizes[idx] = resolved.unwrap_or(DEFAULT_FONT_SIZE);
235 }
236 sizes
237}
238
239fn resolve_font_size_slow(
244 styled_dom: &StyledDom,
245 dom_id: NodeId,
246 node_state: &StyledNodeState,
247) -> f32 {
248 let hierarchy = styled_dom.node_hierarchy.as_container();
259 let states = styled_dom.styled_nodes.as_container();
260 let root_id = NodeId::new(0);
261
262 let root_font_size = if dom_id == root_id {
265 DEFAULT_FONT_SIZE
266 } else {
267 let root_state = &states[root_id].styled_node_state;
268 resolve_font_size_one(
269 styled_dom,
270 root_id,
271 root_state,
272 DEFAULT_FONT_SIZE,
273 DEFAULT_FONT_SIZE,
274 )
275 };
276
277 let mut chain = Vec::new();
279 let mut cur = Some(dom_id);
280 while let Some(id) = cur {
281 chain.push(id);
282 cur = hierarchy
283 .get(id)
284 .and_then(azul_core::styled_dom::NodeHierarchyItem::parent_id);
285 }
286
287 let mut parent_font_size = DEFAULT_FONT_SIZE;
290 let mut resolved = DEFAULT_FONT_SIZE;
291 for &id in chain.iter().rev() {
292 let this_state = if id == dom_id {
295 node_state
296 } else {
297 &states[id].styled_node_state
298 };
299 let this_root_fs = if id == root_id {
300 DEFAULT_FONT_SIZE
301 } else {
302 root_font_size
303 };
304 resolved =
305 resolve_font_size_one(styled_dom, id, this_state, parent_font_size, this_root_fs);
306 parent_font_size = resolved;
307 }
308 resolved
309}
310
311fn resolve_font_size_one(
316 styled_dom: &StyledDom,
317 dom_id: NodeId,
318 node_state: &StyledNodeState,
319 parent_font_size: f32,
320 root_font_size: f32,
321) -> f32 {
322 let node_data = &styled_dom.node_data.as_container()[dom_id];
323 let cache = &styled_dom.css_property_cache.ptr;
324
325 if let Some(vec) = cache.computed_values.get(dom_id.index()) {
326 if let Ok(idx) = vec.binary_search_by_key(&CssPropertyType::FontSize, |(k, _)| *k) {
327 if let CssProperty::FontSize(css_val) = &vec[idx].1.property {
328 if let Some(fs) = css_val.get_property() {
329 if fs.inner.metric == azul_css::props::basic::length::SizeMetric::Px {
330 return fs.inner.number.get();
331 }
332 }
333 }
334 }
335 }
336
337 cache
338 .get_font_size(node_data, &dom_id, node_state)
339 .and_then(|v| v.get_property().copied())
340 .map_or(DEFAULT_FONT_SIZE, |v| {
341 let context = ResolutionContext {
342 element_font_size: DEFAULT_FONT_SIZE,
343 parent_font_size,
344 root_font_size,
345 containing_block_size: PhysicalSize::new(0.0, 0.0),
346 element_size: None,
347 viewport_size: PhysicalSize::new(0.0, 0.0),
348 };
349 v.inner
350 .resolve_with_context(&context, PropertyContext::FontSize)
351 })
352}
353
354#[must_use] pub fn get_parent_font_size(
360 styled_dom: &StyledDom,
361 dom_id: NodeId,
362 _node_state: &StyledNodeState, ) -> f32 {
364 styled_dom
365 .node_hierarchy
366 .as_container()
367 .get(dom_id)
368 .and_then(azul_core::styled_dom::NodeHierarchyItem::parent_id)
369 .map_or(DEFAULT_FONT_SIZE, |parent_id| {
370 let parent_state = &styled_dom.styled_nodes.as_container()[parent_id].styled_node_state;
371 get_element_font_size(styled_dom, parent_id, parent_state)
372 })
373}
374
375#[must_use] pub fn get_root_font_size(styled_dom: &StyledDom, _node_state: &StyledNodeState) -> f32 {
380 let root_id = NodeId::new(0);
381 let root_state = &styled_dom.styled_nodes.as_container()[root_id].styled_node_state;
382 get_element_font_size(styled_dom, root_id, root_state)
383}
384
385#[derive(Debug, Copy, Clone, PartialEq, Eq)]
388#[derive(Default)]
389pub enum MultiValue<T> {
390 #[default]
392 Auto,
393 Initial,
395 Inherit,
397 Exact(T),
399}
400
401impl<T> MultiValue<T> {
402 pub const fn is_auto(&self) -> bool {
404 matches!(self, Self::Auto)
405 }
406
407 pub const fn is_exact(&self) -> bool {
409 matches!(self, Self::Exact(_))
410 }
411
412 pub fn exact(self) -> Option<T> {
414 match self {
415 Self::Exact(v) => Some(v),
416 _ => None,
417 }
418 }
419
420 pub fn unwrap_or(self, default: T) -> T {
422 match self {
423 Self::Exact(v) => v,
424 _ => default,
425 }
426 }
427
428 pub fn unwrap_or_default(self) -> T
430 where
431 T: Default,
432 {
433 match self {
434 Self::Exact(v) => v,
435 _ => T::default(),
436 }
437 }
438
439 pub fn map<U, F>(self, f: F) -> MultiValue<U>
441 where
442 F: FnOnce(T) -> U,
443 {
444 match self {
445 Self::Exact(v) => MultiValue::Exact(f(v)),
446 Self::Auto => MultiValue::Auto,
447 Self::Initial => MultiValue::Initial,
448 Self::Inherit => MultiValue::Inherit,
449 }
450 }
451}
452
453impl MultiValue<LayoutOverflow> {
455 #[must_use] pub const fn is_clipped(&self) -> bool {
458 matches!(
459 self,
460 Self::Exact(
461 LayoutOverflow::Hidden
462 | LayoutOverflow::Clip
463 | LayoutOverflow::Auto
464 | LayoutOverflow::Scroll
465 )
466 )
467 }
468
469 #[must_use] pub const fn is_scroll(&self) -> bool {
470 matches!(
471 self,
472 Self::Exact(LayoutOverflow::Scroll | LayoutOverflow::Auto)
473 )
474 }
475
476 #[must_use] pub const fn is_auto_overflow(&self) -> bool {
477 matches!(self, Self::Exact(LayoutOverflow::Auto))
478 }
479
480 #[must_use] pub const fn is_hidden(&self) -> bool {
481 matches!(self, Self::Exact(LayoutOverflow::Hidden))
482 }
483
484 #[must_use] pub const fn is_hidden_or_clip(&self) -> bool {
485 matches!(
486 self,
487 Self::Exact(LayoutOverflow::Hidden | LayoutOverflow::Clip)
488 )
489 }
490
491 #[must_use] pub const fn is_scroll_explicit(&self) -> bool {
492 matches!(self, Self::Exact(LayoutOverflow::Scroll))
493 }
494
495 #[must_use] pub const fn is_clip(&self) -> bool {
496 matches!(self, Self::Exact(LayoutOverflow::Clip))
497 }
498
499 #[must_use] pub const fn is_visible_or_clip(&self) -> bool {
500 matches!(
501 self,
502 Self::Exact(LayoutOverflow::Visible | LayoutOverflow::Clip)
503 )
504 }
505
506 #[must_use] pub const fn establishes_bfc(&self) -> bool {
514 matches!(
515 self,
516 Self::Exact(LayoutOverflow::Hidden | LayoutOverflow::Scroll | LayoutOverflow::Auto)
517 )
518 }
519
520 #[must_use] pub const fn resolve_computed(
525 &self,
526 other_axis: &Self,
527 ) -> Self {
528 match (self, other_axis) {
529 (Self::Exact(val), Self::Exact(other)) => {
530 Self::Exact(val.resolve_computed(*other))
531 }
532 _ => *self,
533 }
534 }
535}
536
537impl MultiValue<LayoutPosition> {
539 #[must_use] pub const fn is_absolute_or_fixed(&self) -> bool {
540 matches!(
541 self,
542 Self::Exact(LayoutPosition::Absolute | LayoutPosition::Fixed)
543 )
544 }
545}
546
547impl MultiValue<LayoutFloat> {
549 #[must_use] pub const fn is_none(&self) -> bool {
550 matches!(
551 self,
552 Self::Auto
553 | Self::Initial
554 | Self::Inherit
555 | Self::Exact(LayoutFloat::None)
556 )
557 }
558}
559
560
561macro_rules! get_css_property_pixel {
564 ($fn_name:ident, $cache_method:ident, $ua_property:expr, compact_i16 = $compact_method:ident) => {
566 #[must_use] pub fn $fn_name(
567 styled_dom: &StyledDom,
568 node_id: NodeId,
569 node_state: &StyledNodeState,
570 ) -> MultiValue<PixelValue> {
571 if node_state.is_normal() {
573 if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
574 let raw = cc.$compact_method(node_id.index());
575 if raw == azul_css::compact_cache::I16_AUTO {
576 return MultiValue::Auto;
577 }
578 if raw == azul_css::compact_cache::I16_INITIAL {
579 return MultiValue::Initial;
580 }
581 if raw < azul_css::compact_cache::I16_SENTINEL_THRESHOLD {
582 return MultiValue::Exact(PixelValue::px(f32::from(raw) / 10.0));
584 }
585 }
587 }
588
589 let node_data = &styled_dom.node_data.as_container()[node_id];
590
591 let author_css = styled_dom
592 .css_property_cache
593 .ptr
594 .$cache_method(node_data, &node_id, node_state);
595
596 if let Some(ref val) = author_css {
597 if val.is_auto() {
598 return MultiValue::Auto;
599 }
600 if let Some(exact) = val.get_property().copied() {
601 return MultiValue::Exact(exact.inner);
602 }
603 }
604
605 let ua_css = azul_core::ua_css::get_ua_property(&node_data.node_type, $ua_property);
606
607 if let Some(ua_prop) = ua_css {
608 if let Some(inner) = ua_prop.get_pixel_inner() {
609 return MultiValue::Exact(inner);
610 }
611 }
612
613 MultiValue::Initial
614 }
615 };
616}
617
618trait CssPropertyPixelInner {
620 fn get_pixel_inner(&self) -> Option<PixelValue>;
621}
622
623impl CssPropertyPixelInner for CssProperty {
624 fn get_pixel_inner(&self) -> Option<PixelValue> {
625 match self {
626 Self::Left(CssPropertyValue::Exact(v)) => Some(v.inner),
627 Self::Right(CssPropertyValue::Exact(v)) => Some(v.inner),
628 Self::Top(CssPropertyValue::Exact(v)) => Some(v.inner),
629 Self::Bottom(CssPropertyValue::Exact(v)) => Some(v.inner),
630 Self::MarginLeft(CssPropertyValue::Exact(v)) => Some(v.inner),
631 Self::MarginRight(CssPropertyValue::Exact(v)) => Some(v.inner),
632 Self::MarginTop(CssPropertyValue::Exact(v)) => Some(v.inner),
633 Self::MarginBottom(CssPropertyValue::Exact(v)) => Some(v.inner),
634 Self::PaddingLeft(CssPropertyValue::Exact(v)) => Some(v.inner),
635 Self::PaddingRight(CssPropertyValue::Exact(v)) => Some(v.inner),
636 Self::PaddingTop(CssPropertyValue::Exact(v)) => Some(v.inner),
637 Self::PaddingBottom(CssPropertyValue::Exact(v)) => Some(v.inner),
638 _ => None,
639 }
640 }
641}
642
643macro_rules! get_css_property {
645 ($fn_name:ident, $cache_method:ident, $return_type:ty, $ua_property:expr, compact = $compact_method:ident) => {
647 #[must_use] pub fn $fn_name(
648 styled_dom: &StyledDom,
649 node_id: NodeId,
650 node_state: &StyledNodeState,
651 ) -> MultiValue<$return_type> {
652 if node_state.is_normal() {
658 if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
659 return MultiValue::Exact(cc.$compact_method(node_id.index()));
660 }
661 }
662
663 let node_data = &styled_dom.node_data.as_container()[node_id];
665
666 let author_css = styled_dom
668 .css_property_cache
669 .ptr
670 .$cache_method(node_data, &node_id, node_state);
671
672 if let Some(val) = author_css.and_then(|v| v.get_property().cloned()) {
673 return MultiValue::Exact(val);
674 }
675
676 let ua_css = azul_core::ua_css::get_ua_property(&node_data.node_type, $ua_property);
678
679 if let Some(ua_prop) = ua_css {
680 if let Some(val) = extract_property_value::<$return_type>(ua_prop) {
681 return MultiValue::Exact(val);
682 }
683 }
684
685 MultiValue::Auto
687 }
688 };
689 ($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) => {
692 #[must_use] pub fn $fn_name(
693 styled_dom: &StyledDom,
694 node_id: NodeId,
695 node_state: &StyledNodeState,
696 ) -> MultiValue<$return_type> {
697 if node_state.is_normal() {
699 if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
700 let raw = cc.$compact_raw_method(node_id.index());
701 match raw {
702 azul_css::compact_cache::U32_AUTO => return MultiValue::Auto,
703 azul_css::compact_cache::U32_INITIAL => return MultiValue::Initial,
704 azul_css::compact_cache::U32_NONE => return MultiValue::Auto,
705 azul_css::compact_cache::U32_MIN_CONTENT => return MultiValue::Exact($min_content_variant),
706 azul_css::compact_cache::U32_MAX_CONTENT => return MultiValue::Exact($max_content_variant),
707 azul_css::compact_cache::U32_SENTINEL | azul_css::compact_cache::U32_INHERIT => {
708 }
710 _ => {
711 if let Some(pv) = azul_css::compact_cache::decode_pixel_value_u32(raw) {
713 return MultiValue::Exact($px_variant(pv));
714 }
715 }
717 }
718 }
719 }
720
721 let node_data = &styled_dom.node_data.as_container()[node_id];
723
724 let author_css = styled_dom
725 .css_property_cache
726 .ptr
727 .$cache_method(node_data, &node_id, node_state);
728
729 if let Some(val) = author_css.and_then(|v| v.get_property().cloned()) {
730 return MultiValue::Exact(val);
731 }
732
733 let ua_css = azul_core::ua_css::get_ua_property(&node_data.node_type, $ua_property);
734
735 if let Some(ua_prop) = ua_css {
736 if let Some(val) = extract_property_value::<$return_type>(ua_prop) {
737 return MultiValue::Exact(val);
738 }
739 }
740
741 MultiValue::Auto
742 }
743 };
744 ($fn_name:ident, $cache_method:ident, $return_type:ty, $ua_property:expr, compact_u32_struct = $compact_raw_method:ident) => {
747 #[must_use] pub fn $fn_name(
748 styled_dom: &StyledDom,
749 node_id: NodeId,
750 node_state: &StyledNodeState,
751 ) -> MultiValue<$return_type> {
752 if node_state.is_normal() {
754 if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
755 let raw = cc.$compact_raw_method(node_id.index());
756 match raw {
757 azul_css::compact_cache::U32_AUTO | azul_css::compact_cache::U32_NONE => return MultiValue::Auto,
758 azul_css::compact_cache::U32_INITIAL => return MultiValue::Initial,
759 azul_css::compact_cache::U32_SENTINEL | azul_css::compact_cache::U32_INHERIT => {
760 }
762 _ => {
763 if let Some(pv) = azul_css::compact_cache::decode_pixel_value_u32(raw) {
764 return MultiValue::Exact(
765 <$return_type as azul_css::props::PixelValueTaker>::from_pixel_value(pv)
766 );
767 }
768 }
769 }
770 }
771 }
772
773 let node_data = &styled_dom.node_data.as_container()[node_id];
775
776 let author_css = styled_dom
777 .css_property_cache
778 .ptr
779 .$cache_method(node_data, &node_id, node_state);
780
781 if let Some(val) = author_css.and_then(|v| v.get_property().cloned()) {
782 return MultiValue::Exact(val);
783 }
784
785 let ua_css = azul_core::ua_css::get_ua_property(&node_data.node_type, $ua_property);
786
787 if let Some(ua_prop) = ua_css {
788 if let Some(val) = extract_property_value::<$return_type>(ua_prop) {
789 return MultiValue::Exact(val);
790 }
791 }
792
793 MultiValue::Auto
794 }
795 };
796 ($fn_name:ident, $cache_method:ident, $return_type:ty, $ua_property:expr) => {
798 #[must_use] pub fn $fn_name(
799 styled_dom: &StyledDom,
800 node_id: NodeId,
801 node_state: &StyledNodeState,
802 ) -> MultiValue<$return_type> {
803 let node_data = &styled_dom.node_data.as_container()[node_id];
804
805 let author_css = styled_dom
807 .css_property_cache
808 .ptr
809 .$cache_method(node_data, &node_id, node_state);
810
811 if let Some(val) = author_css.and_then(|v| v.get_property().cloned()) {
812 return MultiValue::Exact(val);
813 }
814
815 let ua_css = azul_core::ua_css::get_ua_property(&node_data.node_type, $ua_property);
817
818 if let Some(ua_prop) = ua_css {
819 if let Some(val) = extract_property_value::<$return_type>(ua_prop) {
820 return MultiValue::Exact(val);
821 }
822 }
823
824 MultiValue::Auto
826 }
827 };
828}
829
830trait ExtractPropertyValue<T> {
832 fn extract(&self) -> Option<T>;
833}
834
835fn extract_property_value<T>(prop: &CssProperty) -> Option<T>
836where
837 CssProperty: ExtractPropertyValue<T>,
838{
839 prop.extract()
840}
841
842impl ExtractPropertyValue<LayoutWidth> for CssProperty {
845 fn extract(&self) -> Option<LayoutWidth> {
846 match self {
847 Self::Width(CssPropertyValue::Exact(v)) => Some(v.clone()),
848 _ => None,
849 }
850 }
851}
852
853impl ExtractPropertyValue<LayoutHeight> for CssProperty {
854 fn extract(&self) -> Option<LayoutHeight> {
855 match self {
856 Self::Height(CssPropertyValue::Exact(v)) => Some(v.clone()),
857 _ => None,
858 }
859 }
860}
861
862impl ExtractPropertyValue<LayoutMinWidth> for CssProperty {
863 fn extract(&self) -> Option<LayoutMinWidth> {
864 match self {
865 Self::MinWidth(CssPropertyValue::Exact(v)) => Some(*v),
866 _ => None,
867 }
868 }
869}
870
871impl ExtractPropertyValue<LayoutMinHeight> for CssProperty {
872 fn extract(&self) -> Option<LayoutMinHeight> {
873 match self {
874 Self::MinHeight(CssPropertyValue::Exact(v)) => Some(*v),
875 _ => None,
876 }
877 }
878}
879
880impl ExtractPropertyValue<LayoutMaxWidth> for CssProperty {
881 fn extract(&self) -> Option<LayoutMaxWidth> {
882 match self {
883 Self::MaxWidth(CssPropertyValue::Exact(v)) => Some(*v),
884 _ => None,
885 }
886 }
887}
888
889impl ExtractPropertyValue<LayoutMaxHeight> for CssProperty {
890 fn extract(&self) -> Option<LayoutMaxHeight> {
891 match self {
892 Self::MaxHeight(CssPropertyValue::Exact(v)) => Some(*v),
893 _ => None,
894 }
895 }
896}
897
898impl ExtractPropertyValue<LayoutDisplay> for CssProperty {
899 fn extract(&self) -> Option<LayoutDisplay> {
900 match self {
901 Self::Display(CssPropertyValue::Exact(v)) => Some(*v),
902 _ => None,
903 }
904 }
905}
906
907impl ExtractPropertyValue<LayoutWritingMode> for CssProperty {
908 fn extract(&self) -> Option<LayoutWritingMode> {
909 match self {
910 Self::WritingMode(CssPropertyValue::Exact(v)) => Some(*v),
911 _ => None,
912 }
913 }
914}
915
916impl ExtractPropertyValue<LayoutFlexWrap> for CssProperty {
917 fn extract(&self) -> Option<LayoutFlexWrap> {
918 match self {
919 Self::FlexWrap(CssPropertyValue::Exact(v)) => Some(*v),
920 _ => None,
921 }
922 }
923}
924
925impl ExtractPropertyValue<LayoutJustifyContent> for CssProperty {
926 fn extract(&self) -> Option<LayoutJustifyContent> {
927 match self {
928 Self::JustifyContent(CssPropertyValue::Exact(v)) => Some(*v),
929 _ => None,
930 }
931 }
932}
933
934impl ExtractPropertyValue<StyleTextAlign> for CssProperty {
935 fn extract(&self) -> Option<StyleTextAlign> {
936 match self {
937 Self::TextAlign(CssPropertyValue::Exact(v)) => Some(*v),
938 _ => None,
939 }
940 }
941}
942
943impl ExtractPropertyValue<LayoutFloat> for CssProperty {
944 fn extract(&self) -> Option<LayoutFloat> {
945 match self {
946 Self::Float(CssPropertyValue::Exact(v)) => Some(*v),
947 _ => None,
948 }
949 }
950}
951
952impl ExtractPropertyValue<LayoutClear> for CssProperty {
953 fn extract(&self) -> Option<LayoutClear> {
954 match self {
955 Self::Clear(CssPropertyValue::Exact(v)) => Some(*v),
956 _ => None,
957 }
958 }
959}
960
961impl ExtractPropertyValue<LayoutOverflow> for CssProperty {
962 fn extract(&self) -> Option<LayoutOverflow> {
963 match self {
964 Self::OverflowX(CssPropertyValue::Exact(v))
965 | Self::OverflowY(CssPropertyValue::Exact(v))
966 | Self::OverflowBlock(CssPropertyValue::Exact(v))
967 | Self::OverflowInline(CssPropertyValue::Exact(v)) => Some(*v),
968 _ => None,
969 }
970 }
971}
972
973impl ExtractPropertyValue<LayoutPosition> for CssProperty {
974 fn extract(&self) -> Option<LayoutPosition> {
975 match self {
976 Self::Position(CssPropertyValue::Exact(v)) => Some(*v),
977 _ => None,
978 }
979 }
980}
981
982impl ExtractPropertyValue<LayoutBoxSizing> for CssProperty {
983 fn extract(&self) -> Option<LayoutBoxSizing> {
984 match self {
985 Self::BoxSizing(CssPropertyValue::Exact(v)) => Some(*v),
986 _ => None,
987 }
988 }
989}
990
991impl ExtractPropertyValue<PixelValue> for CssProperty {
992 fn extract(&self) -> Option<PixelValue> {
993 self.get_pixel_inner()
994 }
995}
996
997impl ExtractPropertyValue<LayoutFlexDirection> for CssProperty {
998 fn extract(&self) -> Option<LayoutFlexDirection> {
999 match self {
1000 Self::FlexDirection(CssPropertyValue::Exact(v)) => Some(*v),
1001 _ => None,
1002 }
1003 }
1004}
1005
1006impl ExtractPropertyValue<LayoutAlignItems> for CssProperty {
1007 fn extract(&self) -> Option<LayoutAlignItems> {
1008 match self {
1009 Self::AlignItems(CssPropertyValue::Exact(v)) => Some(*v),
1010 _ => None,
1011 }
1012 }
1013}
1014
1015impl ExtractPropertyValue<LayoutAlignContent> for CssProperty {
1016 fn extract(&self) -> Option<LayoutAlignContent> {
1017 match self {
1018 Self::AlignContent(CssPropertyValue::Exact(v)) => Some(*v),
1019 _ => None,
1020 }
1021 }
1022}
1023
1024impl ExtractPropertyValue<StyleFontWeight> for CssProperty {
1025 fn extract(&self) -> Option<StyleFontWeight> {
1026 match self {
1027 Self::FontWeight(CssPropertyValue::Exact(v)) => Some(*v),
1028 _ => None,
1029 }
1030 }
1031}
1032
1033impl ExtractPropertyValue<StyleFontStyle> for CssProperty {
1034 fn extract(&self) -> Option<StyleFontStyle> {
1035 match self {
1036 Self::FontStyle(CssPropertyValue::Exact(v)) => Some(*v),
1037 _ => None,
1038 }
1039 }
1040}
1041
1042impl ExtractPropertyValue<StyleVisibility> for CssProperty {
1043 fn extract(&self) -> Option<StyleVisibility> {
1044 match self {
1045 Self::Visibility(CssPropertyValue::Exact(v)) => Some(*v),
1046 _ => None,
1047 }
1048 }
1049}
1050
1051impl ExtractPropertyValue<StyleWhiteSpace> for CssProperty {
1052 fn extract(&self) -> Option<StyleWhiteSpace> {
1053 match self {
1054 Self::WhiteSpace(CssPropertyValue::Exact(v)) => Some(*v),
1055 _ => None,
1056 }
1057 }
1058}
1059
1060impl ExtractPropertyValue<StyleDirection> for CssProperty {
1061 fn extract(&self) -> Option<StyleDirection> {
1062 match self {
1063 Self::Direction(CssPropertyValue::Exact(v)) => Some(*v),
1064 _ => None,
1065 }
1066 }
1067}
1068
1069impl ExtractPropertyValue<StyleUnicodeBidi> for CssProperty {
1070 fn extract(&self) -> Option<StyleUnicodeBidi> {
1071 match self {
1072 Self::UnicodeBidi(CssPropertyValue::Exact(v)) => Some(*v),
1073 _ => None,
1074 }
1075 }
1076}
1077
1078impl ExtractPropertyValue<StyleTextBoxTrim> for CssProperty {
1079 fn extract(&self) -> Option<StyleTextBoxTrim> {
1080 match self {
1081 Self::TextBoxTrim(CssPropertyValue::Exact(v)) => Some(*v),
1082 _ => None,
1083 }
1084 }
1085}
1086
1087impl ExtractPropertyValue<StyleTextBoxEdge> for CssProperty {
1088 fn extract(&self) -> Option<StyleTextBoxEdge> {
1089 match self {
1090 Self::TextBoxEdge(CssPropertyValue::Exact(v)) => Some(*v),
1091 _ => None,
1092 }
1093 }
1094}
1095
1096impl ExtractPropertyValue<StyleDominantBaseline> for CssProperty {
1097 fn extract(&self) -> Option<StyleDominantBaseline> {
1098 match self {
1099 Self::DominantBaseline(CssPropertyValue::Exact(v)) => Some(*v),
1100 _ => None,
1101 }
1102 }
1103}
1104
1105impl ExtractPropertyValue<StyleAlignmentBaseline> for CssProperty {
1106 fn extract(&self) -> Option<StyleAlignmentBaseline> {
1107 match self {
1108 Self::AlignmentBaseline(CssPropertyValue::Exact(v)) => Some(*v),
1109 _ => None,
1110 }
1111 }
1112}
1113
1114impl ExtractPropertyValue<StyleBaselineSource> for CssProperty {
1115 fn extract(&self) -> Option<StyleBaselineSource> {
1116 match self {
1117 Self::BaselineSource(CssPropertyValue::Exact(v)) => Some(*v),
1118 _ => None,
1119 }
1120 }
1121}
1122
1123impl ExtractPropertyValue<StyleLineFitEdge> for CssProperty {
1124 fn extract(&self) -> Option<StyleLineFitEdge> {
1125 match self {
1126 Self::LineFitEdge(CssPropertyValue::Exact(v)) => Some(*v),
1127 _ => None,
1128 }
1129 }
1130}
1131
1132impl ExtractPropertyValue<StyleInitialLetterAlign> for CssProperty {
1133 fn extract(&self) -> Option<StyleInitialLetterAlign> {
1134 match self {
1135 Self::InitialLetterAlign(CssPropertyValue::Exact(v)) => Some(*v),
1136 _ => None,
1137 }
1138 }
1139}
1140
1141impl ExtractPropertyValue<StyleInitialLetterWrap> for CssProperty {
1142 fn extract(&self) -> Option<StyleInitialLetterWrap> {
1143 match self {
1144 Self::InitialLetterWrap(CssPropertyValue::Exact(v)) => Some(*v),
1145 _ => None,
1146 }
1147 }
1148}
1149
1150impl ExtractPropertyValue<StyleScrollbarGutter> for CssProperty {
1151 fn extract(&self) -> Option<StyleScrollbarGutter> {
1152 match self {
1153 Self::ScrollbarGutter(CssPropertyValue::Exact(v)) => Some(*v),
1154 _ => None,
1155 }
1156 }
1157}
1158
1159impl ExtractPropertyValue<StyleOverflowClipMargin> for CssProperty {
1160 fn extract(&self) -> Option<StyleOverflowClipMargin> {
1161 match self {
1162 Self::OverflowClipMargin(CssPropertyValue::Exact(v)) => Some(*v),
1163 _ => None,
1164 }
1165 }
1166}
1167
1168impl ExtractPropertyValue<StyleVerticalAlign> for CssProperty {
1169 fn extract(&self) -> Option<StyleVerticalAlign> {
1170 match self {
1171 Self::VerticalAlign(CssPropertyValue::Exact(v)) => Some(*v),
1172 _ => None,
1173 }
1174 }
1175}
1176
1177get_css_property!(
1178 get_writing_mode,
1179 get_writing_mode,
1180 LayoutWritingMode,
1181 CssPropertyType::WritingMode,
1182 compact = get_writing_mode
1183);
1184
1185get_css_property!(
1186 get_css_width,
1187 get_width,
1188 LayoutWidth,
1189 CssPropertyType::Width,
1190 compact_u32_dim = get_width_raw,
1191 LayoutWidth::Px,
1192 LayoutWidth::Auto,
1193 LayoutWidth::MinContent,
1194 LayoutWidth::MaxContent
1195);
1196
1197get_css_property!(
1198 get_css_height,
1199 get_height,
1200 LayoutHeight,
1201 CssPropertyType::Height,
1202 compact_u32_dim = get_height_raw,
1203 LayoutHeight::Px,
1204 LayoutHeight::Auto,
1205 LayoutHeight::MinContent,
1206 LayoutHeight::MaxContent
1207);
1208
1209get_css_property!(
1210 get_wrap,
1211 get_flex_wrap,
1212 LayoutFlexWrap,
1213 CssPropertyType::FlexWrap,
1214 compact = get_flex_wrap
1215);
1216
1217get_css_property!(
1218 get_justify_content,
1219 get_justify_content,
1220 LayoutJustifyContent,
1221 CssPropertyType::JustifyContent,
1222 compact = get_justify_content
1223);
1224
1225get_css_property!(
1226 get_text_align,
1227 get_text_align,
1228 StyleTextAlign,
1229 CssPropertyType::TextAlign,
1230 compact = get_text_align
1231);
1232
1233get_css_property!(
1234 get_float,
1235 get_float,
1236 LayoutFloat,
1237 CssPropertyType::Float,
1238 compact = get_float
1239);
1240
1241get_css_property!(
1242 get_clear,
1243 get_clear,
1244 LayoutClear,
1245 CssPropertyType::Clear,
1246 compact = get_clear
1247);
1248
1249get_css_property!(
1250 get_overflow_x,
1251 get_overflow_x,
1252 LayoutOverflow,
1253 CssPropertyType::OverflowX,
1254 compact = get_overflow_x
1255);
1256
1257get_css_property!(
1258 get_overflow_y,
1259 get_overflow_y,
1260 LayoutOverflow,
1261 CssPropertyType::OverflowY,
1262 compact = get_overflow_y
1263);
1264
1265get_css_property!(
1267 get_overflow_block,
1268 get_overflow_block,
1269 LayoutOverflow,
1270 CssPropertyType::OverflowBlock
1271);
1272
1273get_css_property!(
1274 get_overflow_inline,
1275 get_overflow_inline,
1276 LayoutOverflow,
1277 CssPropertyType::OverflowInline
1278);
1279
1280get_css_property!(
1281 get_position,
1282 get_position,
1283 LayoutPosition,
1284 CssPropertyType::Position,
1285 compact = get_position
1286);
1287
1288get_css_property!(
1289 get_css_box_sizing,
1290 get_box_sizing,
1291 LayoutBoxSizing,
1292 CssPropertyType::BoxSizing,
1293 compact = get_box_sizing
1294);
1295
1296get_css_property!(
1297 get_flex_direction,
1298 get_flex_direction,
1299 LayoutFlexDirection,
1300 CssPropertyType::FlexDirection,
1301 compact = get_flex_direction
1302);
1303
1304get_css_property!(
1305 get_align_items,
1306 get_align_items,
1307 LayoutAlignItems,
1308 CssPropertyType::AlignItems,
1309 compact = get_align_items
1310);
1311
1312get_css_property!(
1313 get_align_content,
1314 get_align_content,
1315 LayoutAlignContent,
1316 CssPropertyType::AlignContent,
1317 compact = get_align_content
1318);
1319
1320get_css_property!(
1321 get_font_weight_property,
1322 get_font_weight,
1323 StyleFontWeight,
1324 CssPropertyType::FontWeight,
1325 compact = get_font_weight
1326);
1327
1328get_css_property!(
1329 get_font_style_property,
1330 get_font_style,
1331 StyleFontStyle,
1332 CssPropertyType::FontStyle,
1333 compact = get_font_style
1334);
1335
1336get_css_property!(
1337 get_visibility,
1338 get_visibility,
1339 StyleVisibility,
1340 CssPropertyType::Visibility,
1341 compact = get_visibility
1342);
1343
1344get_css_property!(
1345 get_white_space_property,
1346 get_white_space,
1347 StyleWhiteSpace,
1348 CssPropertyType::WhiteSpace,
1349 compact = get_white_space
1350);
1351
1352get_css_property!(
1354 get_direction_property,
1355 get_direction,
1356 StyleDirection,
1357 CssPropertyType::Direction,
1358 compact = get_direction
1359);
1360
1361get_css_property!(
1365 get_unicode_bidi_property,
1366 get_unicode_bidi,
1367 StyleUnicodeBidi,
1368 CssPropertyType::UnicodeBidi
1369);
1370
1371get_css_property!(
1374 get_text_box_trim_property,
1375 get_text_box_trim,
1376 StyleTextBoxTrim,
1377 CssPropertyType::TextBoxTrim
1378);
1379
1380get_css_property!(
1381 get_text_box_edge_property,
1382 get_text_box_edge,
1383 StyleTextBoxEdge,
1384 CssPropertyType::TextBoxEdge
1385);
1386
1387get_css_property!(
1388 get_dominant_baseline_property,
1389 get_dominant_baseline,
1390 StyleDominantBaseline,
1391 CssPropertyType::DominantBaseline
1392);
1393
1394get_css_property!(
1395 get_alignment_baseline_property,
1396 get_alignment_baseline,
1397 StyleAlignmentBaseline,
1398 CssPropertyType::AlignmentBaseline
1399);
1400
1401get_css_property!(
1402 get_baseline_source_property,
1403 get_baseline_source,
1404 StyleBaselineSource,
1405 CssPropertyType::BaselineSource
1406);
1407
1408get_css_property!(
1409 get_line_fit_edge_property,
1410 get_line_fit_edge,
1411 StyleLineFitEdge,
1412 CssPropertyType::LineFitEdge
1413);
1414
1415get_css_property!(
1416 get_initial_letter_align_property,
1417 get_initial_letter_align,
1418 StyleInitialLetterAlign,
1419 CssPropertyType::InitialLetterAlign
1420);
1421
1422get_css_property!(
1423 get_initial_letter_wrap_property,
1424 get_initial_letter_wrap,
1425 StyleInitialLetterWrap,
1426 CssPropertyType::InitialLetterWrap
1427);
1428
1429#[allow(clippy::match_same_arms)] #[must_use] pub fn get_scrollbar_gutter_property(
1436 styled_dom: &StyledDom,
1437 node_id: NodeId,
1438 node_state: &StyledNodeState,
1439) -> MultiValue<StyleScrollbarGutter> {
1440 if node_state.is_normal() {
1442 if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
1443 let bits = cc.get_scrollbar_gutter_bits(node_id.index());
1444 let val = match bits {
1445 azul_css::compact_cache::SCROLLBAR_GUTTER_AUTO => StyleScrollbarGutter::Auto,
1446 azul_css::compact_cache::SCROLLBAR_GUTTER_STABLE => StyleScrollbarGutter::Stable,
1447 azul_css::compact_cache::SCROLLBAR_GUTTER_BOTH_EDGES => {
1448 StyleScrollbarGutter::StableBothEdges
1449 }
1450 _ => StyleScrollbarGutter::Auto,
1451 };
1452 return MultiValue::Exact(val);
1453 }
1454 }
1455
1456 let node_data = &styled_dom.node_data.as_container()[node_id];
1458 let author_css = styled_dom
1459 .css_property_cache
1460 .ptr
1461 .get_scrollbar_gutter(node_data, &node_id, node_state);
1462 if let Some(val) = author_css.and_then(|v| v.get_property().copied()) {
1463 return MultiValue::Exact(val);
1464 }
1465 MultiValue::Auto
1466}
1467
1468get_css_property!(
1469 get_overflow_clip_margin_property,
1470 get_overflow_clip_margin,
1471 StyleOverflowClipMargin,
1472 CssPropertyType::OverflowClipMargin
1473);
1474
1475get_css_property!(
1476 get_object_fit_property,
1477 get_object_fit,
1478 StyleObjectFit,
1479 CssPropertyType::ObjectFit
1480);
1481
1482get_css_property!(
1483 get_text_overflow_property,
1484 get_text_overflow,
1485 StyleTextOverflow,
1486 CssPropertyType::TextOverflow
1487);
1488
1489#[must_use] pub fn get_text_orientation_property(
1495 styled_dom: &StyledDom,
1496 node_id: NodeId,
1497 node_state: &StyledNodeState,
1498) -> MultiValue<StyleTextOrientation> {
1499 if node_state.is_normal() {
1500 if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
1501 if !cc.has_text_orientation(node_id.index()) {
1502 return MultiValue::Auto;
1503 }
1504 }
1505 }
1506 let node_data = &styled_dom.node_data.as_container()[node_id];
1507 if let Some(val) = styled_dom
1508 .css_property_cache
1509 .ptr
1510 .get_text_orientation(node_data, &node_id, node_state)
1511 .and_then(|v| v.get_property().copied())
1512 {
1513 return MultiValue::Exact(val);
1514 }
1515 let ua = azul_core::ua_css::get_ua_property(
1516 &node_data.node_type,
1517 CssPropertyType::TextOrientation,
1518 );
1519 if let Some(ua_prop) = ua {
1520 if let Some(val) = extract_property_value::<StyleTextOrientation>(ua_prop) {
1521 return MultiValue::Exact(val);
1522 }
1523 }
1524 MultiValue::Auto
1525}
1526
1527get_css_property!(
1528 get_object_position_property,
1529 get_object_position,
1530 StyleObjectPosition,
1531 CssPropertyType::ObjectPosition
1532);
1533
1534get_css_property!(
1535 get_aspect_ratio_property,
1536 get_aspect_ratio,
1537 StyleAspectRatio,
1538 CssPropertyType::AspectRatio
1539);
1540
1541#[must_use] pub fn get_vertical_align_property(
1545 styled_dom: &StyledDom,
1546 node_id: NodeId,
1547 node_state: &StyledNodeState,
1548) -> MultiValue<StyleVerticalAlign> {
1549 let node_data = &styled_dom.node_data.as_container()[node_id];
1550
1551 let author_css = styled_dom
1552 .css_property_cache
1553 .ptr
1554 .get_vertical_align(node_data, &node_id, node_state);
1555
1556 if let Some(val) = author_css.and_then(|v| v.get_property().copied()) {
1557 return MultiValue::Exact(val);
1558 }
1559
1560 let ua_css = azul_core::ua_css::get_ua_property(
1561 &node_data.node_type,
1562 CssPropertyType::VerticalAlign,
1563 );
1564
1565 if let Some(ua_prop) = ua_css {
1566 if let Some(val) = extract_property_value::<StyleVerticalAlign>(ua_prop) {
1567 return MultiValue::Exact(val);
1568 }
1569 }
1570
1571 MultiValue::Auto
1572}
1573#[must_use] pub fn get_style_border_radius(
1577 styled_dom: &StyledDom,
1578 node_id: NodeId,
1579 node_state: &StyledNodeState,
1580) -> StyleBorderRadius {
1581 use azul_css::props::basic::pixel::PixelValue;
1582 if node_state.is_normal() {
1585 if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
1586 let idx = node_id.index();
1587 let decode = |raw: i16| -> PixelValue {
1588 if raw >= azul_css::compact_cache::I16_SENTINEL_THRESHOLD {
1589 PixelValue::px(0.0)
1590 } else {
1591 PixelValue::px(f32::from(raw) / 10.0)
1592 }
1593 };
1594 return StyleBorderRadius {
1595 top_left: decode(cc.get_border_top_left_radius_raw(idx)),
1596 top_right: decode(cc.get_border_top_right_radius_raw(idx)),
1597 bottom_right: decode(cc.get_border_bottom_right_radius_raw(idx)),
1598 bottom_left: decode(cc.get_border_bottom_left_radius_raw(idx)),
1599 };
1600 }
1601 }
1602 let node_data = &styled_dom.node_data.as_container()[node_id];
1603
1604 let top_left = styled_dom
1605 .css_property_cache
1606 .ptr
1607 .get_border_top_left_radius(node_data, &node_id, node_state)
1608 .and_then(|br| br.get_property_or_default())
1609 .map(|v| v.inner)
1610 .unwrap_or_default();
1611
1612 let top_right = styled_dom
1613 .css_property_cache
1614 .ptr
1615 .get_border_top_right_radius(node_data, &node_id, node_state)
1616 .and_then(|br| br.get_property_or_default())
1617 .map(|v| v.inner)
1618 .unwrap_or_default();
1619
1620 let bottom_right = styled_dom
1621 .css_property_cache
1622 .ptr
1623 .get_border_bottom_right_radius(node_data, &node_id, node_state)
1624 .and_then(|br| br.get_property_or_default())
1625 .map(|v| v.inner)
1626 .unwrap_or_default();
1627
1628 let bottom_left = styled_dom
1629 .css_property_cache
1630 .ptr
1631 .get_border_bottom_left_radius(node_data, &node_id, node_state)
1632 .and_then(|br| br.get_property_or_default())
1633 .map(|v| v.inner)
1634 .unwrap_or_default();
1635
1636 StyleBorderRadius {
1637 top_left,
1638 top_right,
1639 bottom_right,
1640 bottom_left,
1641 }
1642}
1643
1644#[must_use] pub fn get_border_radius(
1650 styled_dom: &StyledDom,
1651 node_id: NodeId,
1652 node_state: &StyledNodeState,
1653 element_size: PhysicalSizeImport,
1654 viewport_size: LogicalSize,
1655) -> BorderRadius {
1656 use azul_css::props::basic::{PhysicalSize, PropertyContext, ResolutionContext};
1657
1658 if node_state.is_normal() {
1662 if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
1663 let idx = node_id.index();
1664 let tl = cc.get_border_top_left_radius_raw(idx);
1665 let tr = cc.get_border_top_right_radius_raw(idx);
1666 let br = cc.get_border_bottom_right_radius_raw(idx);
1667 let bl = cc.get_border_bottom_left_radius_raw(idx);
1668 let thresh = azul_css::compact_cache::I16_SENTINEL_THRESHOLD;
1670 let decode = |raw: i16| -> f32 {
1671 if raw >= thresh {
1672 0.0
1673 } else {
1674 f32::from(raw) / 10.0
1675 }
1676 };
1677 return BorderRadius {
1678 top_left: decode(tl),
1679 top_right: decode(tr),
1680 bottom_right: decode(br),
1681 bottom_left: decode(bl),
1682 };
1683 }
1684 }
1685
1686 let node_data = &styled_dom.node_data.as_container()[node_id];
1687
1688 let element_font_size = get_element_font_size(styled_dom, node_id, node_state);
1690 let parent_font_size = styled_dom
1691 .node_hierarchy
1692 .as_container()
1693 .get(node_id)
1694 .and_then(azul_core::styled_dom::NodeHierarchyItem::parent_id)
1695 .map_or(DEFAULT_FONT_SIZE, |p| get_element_font_size(styled_dom, p, node_state));
1696 let root_font_size = get_root_font_size(styled_dom, node_state);
1697
1698 let context = ResolutionContext {
1700 element_font_size,
1701 parent_font_size,
1702 root_font_size,
1703 containing_block_size: PhysicalSize::new(0.0, 0.0), element_size: Some(PhysicalSize::new(element_size.width, element_size.height)),
1705 viewport_size: PhysicalSize::new(viewport_size.width, viewport_size.height),
1706 };
1707
1708 let top_left = styled_dom
1709 .css_property_cache
1710 .ptr
1711 .get_border_top_left_radius(node_data, &node_id, node_state)
1712 .and_then(|br| br.get_property().copied())
1713 .unwrap_or_default();
1714
1715 let top_right = styled_dom
1716 .css_property_cache
1717 .ptr
1718 .get_border_top_right_radius(node_data, &node_id, node_state)
1719 .and_then(|br| br.get_property().copied())
1720 .unwrap_or_default();
1721
1722 let bottom_right = styled_dom
1723 .css_property_cache
1724 .ptr
1725 .get_border_bottom_right_radius(node_data, &node_id, node_state)
1726 .and_then(|br| br.get_property().copied())
1727 .unwrap_or_default();
1728
1729 let bottom_left = styled_dom
1730 .css_property_cache
1731 .ptr
1732 .get_border_bottom_left_radius(node_data, &node_id, node_state)
1733 .and_then(|br| br.get_property().copied())
1734 .unwrap_or_default();
1735
1736 BorderRadius {
1737 top_left: top_left
1738 .inner
1739 .resolve_with_context(&context, PropertyContext::BorderRadius),
1740 top_right: top_right
1741 .inner
1742 .resolve_with_context(&context, PropertyContext::BorderRadius),
1743 bottom_right: bottom_right
1744 .inner
1745 .resolve_with_context(&context, PropertyContext::BorderRadius),
1746 bottom_left: bottom_left
1747 .inner
1748 .resolve_with_context(&context, PropertyContext::BorderRadius),
1749 }
1750}
1751
1752#[must_use] pub fn get_z_index(styled_dom: &StyledDom, node_id: Option<NodeId>) -> i32 {
1760 use azul_css::props::layout::position::LayoutZIndex;
1761
1762 let Some(node_id) = node_id else {
1763 return 0;
1764 };
1765
1766 let node_state = &styled_dom.styled_nodes.as_container()[node_id].styled_node_state;
1767
1768 if node_state.is_normal() {
1770 if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
1771 let raw = cc.get_z_index(node_id.index());
1772 if raw == azul_css::compact_cache::I16_AUTO {
1773 return 0;
1774 }
1775 if raw < azul_css::compact_cache::I16_SENTINEL_THRESHOLD {
1776 return i32::from(raw);
1777 }
1778 }
1780 }
1781
1782 let node_data = &styled_dom.node_data.as_container()[node_id];
1784
1785 styled_dom
1786 .css_property_cache
1787 .ptr
1788 .get_z_index(node_data, &node_id, node_state)
1789 .and_then(|v| v.get_property())
1790 .map_or(0, |z| match z {
1791 LayoutZIndex::Auto => 0,
1792 LayoutZIndex::Integer(i) => *i,
1793 })
1794}
1795
1796#[must_use] pub fn is_z_index_auto(styled_dom: &StyledDom, node_id: Option<NodeId>) -> bool {
1801 use azul_css::props::layout::position::LayoutZIndex;
1802
1803 let Some(node_id) = node_id else {
1804 return true;
1805 };
1806
1807 let node_state = &styled_dom.styled_nodes.as_container()[node_id].styled_node_state;
1808
1809 if node_state.is_normal() {
1811 if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
1812 let raw = cc.get_z_index(node_id.index());
1813 if raw == azul_css::compact_cache::I16_AUTO {
1814 return true;
1815 }
1816 if raw < azul_css::compact_cache::I16_SENTINEL_THRESHOLD {
1817 return false; }
1819 }
1821 }
1822
1823 let node_data = &styled_dom.node_data.as_container()[node_id];
1825
1826 styled_dom
1827 .css_property_cache
1828 .ptr
1829 .get_z_index(node_data, &node_id, node_state)
1830 .and_then(|v| v.get_property())
1831 .is_none_or(|z| matches!(z, LayoutZIndex::Auto)) }
1833
1834#[allow(clippy::match_same_arms)] #[must_use] pub fn get_background_color(
1858 styled_dom: &StyledDom,
1859 node_id: NodeId,
1860 node_state: &StyledNodeState,
1861) -> ColorU {
1862 let node_data = &styled_dom.node_data.as_container()[node_id];
1863 let cache = &styled_dom.css_property_cache.ptr;
1864
1865 let get_node_bg = |nid: NodeId, ndata: &azul_core::dom::NodeData, state: &StyledNodeState| {
1870 if state.is_normal() {
1871 if let Some(ref cc) = cache.compact_cache {
1872 if !cc.has_background(nid.index()) {
1873 return None;
1874 }
1875 }
1876 }
1877 cache
1878 .get_background_content(ndata, &nid, state)
1879 .and_then(|bg| bg.get_property())
1880 .and_then(|bg_vec| bg_vec.get(0).cloned())
1881 .and_then(|first_bg| match &first_bg {
1882 azul_css::props::style::StyleBackgroundContent::Color(color) => Some(*color),
1883 azul_css::props::style::StyleBackgroundContent::Image(_) => None, _ => None,
1885 })
1886 };
1887
1888 let own_bg = get_node_bg(node_id, node_data, node_state);
1889
1890 if !matches!(node_data.node_type, NodeType::Html) || own_bg.is_some() {
1894 return own_bg.unwrap_or(ColorU {
1896 r: 0,
1897 g: 0,
1898 b: 0,
1899 a: 0,
1900 });
1901 }
1902
1903 let first_child = styled_dom
1905 .node_hierarchy
1906 .as_container()
1907 .get(node_id)
1908 .and_then(|node| node.first_child_id(node_id));
1909
1910 let Some(first_child) = first_child else {
1911 return ColorU {
1912 r: 0,
1913 g: 0,
1914 b: 0,
1915 a: 0,
1916 };
1917 };
1918
1919 let first_child_data = &styled_dom.node_data.as_container()[first_child];
1920
1921 if !matches!(first_child_data.node_type, NodeType::Body) {
1923 return ColorU {
1924 r: 0,
1925 g: 0,
1926 b: 0,
1927 a: 0,
1928 };
1929 }
1930
1931 let first_child_state = &styled_dom.styled_nodes.as_container()[first_child].styled_node_state;
1933 get_node_bg(first_child, first_child_data, first_child_state).unwrap_or(ColorU {
1934 r: 0,
1935 g: 0,
1936 b: 0,
1937 a: 0,
1938 })
1939}
1940
1941#[must_use] pub fn get_background_contents(
1948 styled_dom: &StyledDom,
1949 node_id: NodeId,
1950 node_state: &StyledNodeState,
1951) -> Vec<azul_css::props::style::StyleBackgroundContent> {
1952 use azul_core::dom::NodeType;
1953 use azul_css::props::style::StyleBackgroundContent;
1954
1955 let node_data = &styled_dom.node_data.as_container()[node_id];
1956 let cache = &styled_dom.css_property_cache.ptr;
1957
1958 let get_node_backgrounds = |nid: NodeId,
1962 ndata: &azul_core::dom::NodeData,
1963 state: &StyledNodeState|
1964 -> Vec<StyleBackgroundContent> {
1965 if state.is_normal() {
1966 if let Some(ref cc) = cache.compact_cache {
1967 if !cc.has_background(nid.index()) {
1968 return Vec::new();
1969 }
1970 }
1971 }
1972 cache
1973 .get_background_content(ndata, &nid, state)
1974 .and_then(|bg| bg.get_property())
1975 .map(|bg_vec| bg_vec.iter().cloned().collect())
1976 .unwrap_or_default()
1977 };
1978
1979 let own_backgrounds = get_node_backgrounds(node_id, node_data, node_state);
1980
1981 if !matches!(node_data.node_type, NodeType::Html) || !own_backgrounds.is_empty() {
1984 return own_backgrounds;
1985 }
1986
1987 let first_child = styled_dom
1989 .node_hierarchy
1990 .as_container()
1991 .get(node_id)
1992 .and_then(|node| node.first_child_id(node_id));
1993
1994 let Some(first_child) = first_child else {
1995 return own_backgrounds;
1996 };
1997
1998 let first_child_data = &styled_dom.node_data.as_container()[first_child];
1999
2000 if !matches!(first_child_data.node_type, NodeType::Body) {
2002 return own_backgrounds;
2003 }
2004
2005 let first_child_state = &styled_dom.styled_nodes.as_container()[first_child].styled_node_state;
2007 get_node_backgrounds(first_child, first_child_data, first_child_state)
2008}
2009
2010#[derive(Copy, Clone, Debug)]
2012pub struct BorderInfo {
2013 pub widths: crate::solver3::display_list::StyleBorderWidths,
2014 pub colors: crate::solver3::display_list::StyleBorderColors,
2015 pub styles: crate::solver3::display_list::StyleBorderStyles,
2016}
2017
2018#[allow(clippy::too_many_lines)] #[must_use] pub fn get_border_info(
2020 styled_dom: &StyledDom,
2021 node_id: NodeId,
2022 node_state: &StyledNodeState,
2023) -> BorderInfo {
2024 use crate::solver3::display_list::{StyleBorderColors, StyleBorderStyles, StyleBorderWidths};
2025 use azul_css::css::CssPropertyValue;
2026 use azul_css::props::basic::color::ColorU;
2027 use azul_css::props::basic::pixel::PixelValue;
2028 use azul_css::props::style::border::{
2029 BorderStyle, StyleBorderBottomColor, StyleBorderBottomStyle, StyleBorderLeftColor,
2030 StyleBorderLeftStyle, StyleBorderRightColor, StyleBorderRightStyle, StyleBorderTopColor,
2031 StyleBorderTopStyle,
2032 };
2033 use azul_css::props::style::{
2034 LayoutBorderBottomWidth, LayoutBorderLeftWidth, LayoutBorderRightWidth,
2035 LayoutBorderTopWidth,
2036 };
2037
2038 if node_state.is_normal() {
2040 if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
2041 let idx = node_id.index();
2042
2043 let make_width_px = |raw: i16| -> Option<PixelValue> {
2049 if raw == azul_css::compact_cache::I16_AUTO
2050 || raw == azul_css::compact_cache::I16_INITIAL
2051 || raw >= azul_css::compact_cache::I16_SENTINEL_THRESHOLD
2052 {
2053 None
2054 } else {
2055 Some(PixelValue::px(f32::from(raw) / 10.0))
2056 }
2057 };
2058 let widths = StyleBorderWidths {
2059 top: make_width_px(cc.get_border_top_width_raw(idx))
2060 .map(|px| CssPropertyValue::Exact(LayoutBorderTopWidth { inner: px })),
2061 right: make_width_px(cc.get_border_right_width_raw(idx))
2062 .map(|px| CssPropertyValue::Exact(LayoutBorderRightWidth { inner: px })),
2063 bottom: make_width_px(cc.get_border_bottom_width_raw(idx))
2064 .map(|px| CssPropertyValue::Exact(LayoutBorderBottomWidth { inner: px })),
2065 left: make_width_px(cc.get_border_left_width_raw(idx))
2066 .map(|px| CssPropertyValue::Exact(LayoutBorderLeftWidth { inner: px })),
2067 };
2068
2069 let make_color = |raw: u32| -> Option<ColorU> {
2071 if raw == 0 {
2072 None
2073 } else {
2074 Some(ColorU {
2075 r: ((raw >> 24) & 0xFF) as u8,
2076 g: ((raw >> 16) & 0xFF) as u8,
2077 b: ((raw >> 8) & 0xFF) as u8,
2078 a: (raw & 0xFF) as u8,
2079 })
2080 }
2081 };
2082
2083 let colors = StyleBorderColors {
2084 top: make_color(cc.get_border_top_color_raw(idx))
2085 .map(|c| CssPropertyValue::Exact(StyleBorderTopColor { inner: c })),
2086 right: make_color(cc.get_border_right_color_raw(idx))
2087 .map(|c| CssPropertyValue::Exact(StyleBorderRightColor { inner: c })),
2088 bottom: make_color(cc.get_border_bottom_color_raw(idx))
2089 .map(|c| CssPropertyValue::Exact(StyleBorderBottomColor { inner: c })),
2090 left: make_color(cc.get_border_left_color_raw(idx))
2091 .map(|c| CssPropertyValue::Exact(StyleBorderLeftColor { inner: c })),
2092 };
2093
2094 let styles = StyleBorderStyles {
2096 top: Some(CssPropertyValue::Exact(StyleBorderTopStyle {
2097 inner: cc.get_border_top_style(idx),
2098 })),
2099 right: Some(CssPropertyValue::Exact(StyleBorderRightStyle {
2100 inner: cc.get_border_right_style(idx),
2101 })),
2102 bottom: Some(CssPropertyValue::Exact(StyleBorderBottomStyle {
2103 inner: cc.get_border_bottom_style(idx),
2104 })),
2105 left: Some(CssPropertyValue::Exact(StyleBorderLeftStyle {
2106 inner: cc.get_border_left_style(idx),
2107 })),
2108 };
2109
2110 return BorderInfo {
2111 widths,
2112 colors,
2113 styles,
2114 };
2115 }
2116 }
2117
2118 let node_data = &styled_dom.node_data.as_container()[node_id];
2120
2121 let widths = StyleBorderWidths {
2123 top: styled_dom
2124 .css_property_cache
2125 .ptr
2126 .get_border_top_width(node_data, &node_id, node_state)
2127 .copied(),
2128 right: styled_dom
2129 .css_property_cache
2130 .ptr
2131 .get_border_right_width(node_data, &node_id, node_state)
2132 .copied(),
2133 bottom: styled_dom
2134 .css_property_cache
2135 .ptr
2136 .get_border_bottom_width(node_data, &node_id, node_state)
2137 .copied(),
2138 left: styled_dom
2139 .css_property_cache
2140 .ptr
2141 .get_border_left_width(node_data, &node_id, node_state)
2142 .copied(),
2143 };
2144
2145 let colors = StyleBorderColors {
2147 top: styled_dom
2148 .css_property_cache
2149 .ptr
2150 .get_border_top_color(node_data, &node_id, node_state)
2151 .copied(),
2152 right: styled_dom
2153 .css_property_cache
2154 .ptr
2155 .get_border_right_color(node_data, &node_id, node_state)
2156 .copied(),
2157 bottom: styled_dom
2158 .css_property_cache
2159 .ptr
2160 .get_border_bottom_color(node_data, &node_id, node_state)
2161 .copied(),
2162 left: styled_dom
2163 .css_property_cache
2164 .ptr
2165 .get_border_left_color(node_data, &node_id, node_state)
2166 .copied(),
2167 };
2168
2169 let styles = StyleBorderStyles {
2171 top: styled_dom
2172 .css_property_cache
2173 .ptr
2174 .get_border_top_style(node_data, &node_id, node_state)
2175 .copied(),
2176 right: styled_dom
2177 .css_property_cache
2178 .ptr
2179 .get_border_right_style(node_data, &node_id, node_state)
2180 .copied(),
2181 bottom: styled_dom
2182 .css_property_cache
2183 .ptr
2184 .get_border_bottom_style(node_data, &node_id, node_state)
2185 .copied(),
2186 left: styled_dom
2187 .css_property_cache
2188 .ptr
2189 .get_border_left_style(node_data, &node_id, node_state)
2190 .copied(),
2191 };
2192
2193 BorderInfo {
2194 widths,
2195 colors,
2196 styles,
2197 }
2198}
2199
2200#[allow(clippy::too_many_lines)] fn get_inline_border_info(
2206 styled_dom: &StyledDom,
2207 node_id: NodeId,
2208 node_state: &StyledNodeState,
2209 border_info: &BorderInfo,
2210 viewport: PhysicalSize,
2211) -> Option<crate::text3::cache::InlineBorderInfo> {
2212 use crate::text3::cache::InlineBorderInfo;
2213
2214 fn resolve_padding(
2217 mv: MultiValue<PixelValue>,
2218 viewport: PhysicalSize,
2219 ) -> f32 {
2220 match mv {
2221 MultiValue::Exact(pv) => super::calc::resolve_pixel_value_with_viewport(
2222 &pv,
2223 0.0,
2224 DEFAULT_FONT_SIZE,
2225 DEFAULT_FONT_SIZE,
2226 viewport.width,
2227 viewport.height,
2228 ),
2229 _ => 0.0,
2230 }
2231 }
2232
2233 macro_rules! border_width_px {
2234 ($field:expr) => {
2235 $field
2236 .as_ref()
2237 .and_then(|v| v.get_property())
2238 .map(|w| w.inner.number.get())
2239 .unwrap_or(0.0)
2240 };
2241 }
2242
2243 macro_rules! border_color {
2244 ($field:expr) => {
2245 $field
2246 .as_ref()
2247 .and_then(|v| v.get_property())
2248 .map(|c| c.inner)
2249 .unwrap_or(ColorU::BLACK)
2250 };
2251 }
2252
2253 fn get_border_radius_px(
2255 styled_dom: &StyledDom,
2256 node_id: NodeId,
2257 node_state: &StyledNodeState,
2258 ) -> Option<f32> {
2259 let node_data = &styled_dom.node_data.as_container()[node_id];
2260
2261 let top_left = styled_dom
2262 .css_property_cache
2263 .ptr
2264 .get_border_top_left_radius(node_data, &node_id, node_state)
2265 .and_then(|br| br.get_property().copied())
2266 .map(|v| v.inner.number.get());
2267
2268 let top_right = styled_dom
2269 .css_property_cache
2270 .ptr
2271 .get_border_top_right_radius(node_data, &node_id, node_state)
2272 .and_then(|br| br.get_property().copied())
2273 .map(|v| v.inner.number.get());
2274
2275 let bottom_left = styled_dom
2276 .css_property_cache
2277 .ptr
2278 .get_border_bottom_left_radius(node_data, &node_id, node_state)
2279 .and_then(|br| br.get_property().copied())
2280 .map(|v| v.inner.number.get());
2281
2282 let bottom_right = styled_dom
2283 .css_property_cache
2284 .ptr
2285 .get_border_bottom_right_radius(node_data, &node_id, node_state)
2286 .and_then(|br| br.get_property().copied())
2287 .map(|v| v.inner.number.get());
2288
2289 let radii: Vec<f32> = [top_left, top_right, bottom_left, bottom_right]
2291 .into_iter()
2292 .flatten()
2293 .collect();
2294
2295 if radii.is_empty() {
2296 None
2297 } else {
2298 Some(radii.into_iter().fold(0.0f32, f32::max))
2299 }
2300 }
2301
2302 let top = border_width_px!(&border_info.widths.top);
2303 let right = border_width_px!(&border_info.widths.right);
2304 let bottom = border_width_px!(&border_info.widths.bottom);
2305 let left = border_width_px!(&border_info.widths.left);
2306
2307 let p_top = resolve_padding(get_css_padding_top(styled_dom, node_id, node_state), viewport);
2308 let p_right = resolve_padding(get_css_padding_right(styled_dom, node_id, node_state), viewport);
2309 let p_bottom = resolve_padding(get_css_padding_bottom(styled_dom, node_id, node_state), viewport);
2310 let p_left = resolve_padding(get_css_padding_left(styled_dom, node_id, node_state), viewport);
2311
2312 let has_border = top > 0.0 || right > 0.0 || bottom > 0.0 || left > 0.0;
2314 let has_padding = p_top > 0.0 || p_right > 0.0 || p_bottom > 0.0 || p_left > 0.0;
2315 if !has_border && !has_padding {
2316 return None;
2317 }
2318
2319 let is_rtl = matches!(
2321 get_direction_property(styled_dom, node_id, node_state),
2322 MultiValue::Exact(StyleDirection::Rtl)
2323 );
2324
2325 Some(InlineBorderInfo {
2326 top,
2327 right,
2328 bottom,
2329 left,
2330 top_color: border_color!(&border_info.colors.top),
2331 right_color: border_color!(&border_info.colors.right),
2332 bottom_color: border_color!(&border_info.colors.bottom),
2333 left_color: border_color!(&border_info.colors.left),
2334 radius: get_border_radius_px(styled_dom, node_id, node_state),
2335 padding_top: p_top,
2336 padding_right: p_right,
2337 padding_bottom: p_bottom,
2338 padding_left: p_left,
2339 is_first_fragment: true,
2340 is_last_fragment: true,
2341 is_rtl,
2342 })
2343}
2344
2345#[derive(Debug, Clone, Copy, Default)]
2349pub struct SelectionStyle {
2350 pub bg_color: ColorU,
2352 pub text_color: Option<ColorU>,
2354 pub radius: f32,
2356}
2357
2358#[must_use] pub fn get_selection_style(
2360 styled_dom: &StyledDom,
2361 node_id: Option<NodeId>,
2362 system_style: Option<&std::sync::Arc<azul_css::system::SystemStyle>>,
2363) -> SelectionStyle {
2364 let Some(node_id) = node_id else {
2365 return SelectionStyle::default();
2366 };
2367
2368 let node_data = &styled_dom.node_data.as_container()[node_id];
2369 let node_state = &StyledNodeState::default();
2370
2371 let default_bg = system_style
2373 .and_then(|ss| ss.colors.selection_background.as_option().copied())
2374 .unwrap_or(ColorU {
2375 r: 51,
2376 g: 153,
2377 b: 255, a: 128, });
2380
2381 let bg_color = styled_dom
2382 .css_property_cache
2383 .ptr
2384 .get_selection_background_color(node_data, &node_id, node_state)
2385 .and_then(|c| c.get_property().copied())
2386 .map_or(default_bg, |c| c.inner);
2387
2388 let default_text = system_style.and_then(|ss| ss.colors.selection_text.as_option().copied());
2390
2391 let text_color = styled_dom
2392 .css_property_cache
2393 .ptr
2394 .get_selection_color(node_data, &node_id, node_state)
2395 .and_then(|c| c.get_property().copied())
2396 .map(|c| c.inner)
2397 .or(default_text);
2398
2399 let radius = styled_dom
2400 .css_property_cache
2401 .ptr
2402 .get_selection_radius(node_data, &node_id, node_state)
2403 .and_then(|r| r.get_property().copied())
2404 .map_or(0.0, |r| r.inner.to_pixels_internal(0.0, DEFAULT_EM_SIZE, DEFAULT_EM_SIZE));
2405
2406 SelectionStyle {
2407 bg_color,
2408 text_color,
2409 radius,
2410 }
2411}
2412
2413#[derive(Debug, Clone, Copy)]
2415pub struct CaretStyle {
2416 pub color: ColorU,
2418 pub width: f32,
2420 pub animation_duration: CssDuration,
2427}
2428
2429impl Default for CaretStyle {
2430 fn default() -> Self {
2431 Self {
2432 color: ColorU::BLACK,
2433 width: DEFAULT_CARET_WIDTH_PX,
2434 animation_duration: CssDuration::from_millis(DEFAULT_CARET_BLINK_MS),
2435 }
2436 }
2437}
2438
2439#[must_use] pub fn get_caret_style(styled_dom: &StyledDom, node_id: Option<NodeId>) -> CaretStyle {
2441 let Some(node_id) = node_id else {
2442 return CaretStyle::default();
2443 };
2444
2445 let node_data = &styled_dom.node_data.as_container()[node_id];
2446 let node_state = &StyledNodeState::default();
2447
2448 let color = styled_dom
2449 .css_property_cache
2450 .ptr
2451 .get_caret_color(node_data, &node_id, node_state)
2452 .and_then(|c| c.get_property().copied())
2453 .map_or_else(|| {
2459 styled_dom
2460 .css_property_cache
2461 .ptr
2462 .get_text_color_or_default(node_data, &node_id, node_state)
2463 .inner
2464 }, |c| c.inner);
2465
2466 let width = styled_dom
2467 .css_property_cache
2468 .ptr
2469 .get_caret_width(node_data, &node_id, node_state)
2470 .and_then(|w| w.get_property().copied())
2471 .map_or(DEFAULT_CARET_WIDTH_PX, |w| w.inner.to_pixels_internal(0.0, DEFAULT_EM_SIZE, DEFAULT_EM_SIZE));
2472
2473 let default_blink = CssDuration::from_millis(DEFAULT_CARET_BLINK_MS);
2476 let animation_duration = styled_dom
2477 .css_property_cache
2478 .ptr
2479 .get_caret_animation_duration(node_data, &node_id, node_state)
2480 .and_then(|d| d.get_property().copied())
2481 .map_or(default_blink, |d| d.inner);
2482
2483 CaretStyle {
2484 color,
2485 width,
2486 animation_duration,
2487 }
2488}
2489
2490#[must_use] pub fn get_scrollbar_info_from_layout(node: &LayoutNode) -> ScrollbarRequirements {
2502 node.scrollbar_info.unwrap_or_default()
2503}
2504
2505pub fn get_layout_scrollbar_width_px<T: ParsedFontTrait>(
2520 ctx: &crate::solver3::LayoutContext<'_, T>,
2521 dom_id: NodeId,
2522 styled_node_state: &StyledNodeState,
2523) -> f32 {
2524 let style = get_scrollbar_style(
2529 ctx.styled_dom,
2530 dom_id,
2531 styled_node_state,
2532 ctx.system_style.as_deref(),
2533 );
2534 style.reserve_width_px
2535}
2536
2537get_css_property!(
2538 get_display_property_internal,
2539 get_display,
2540 LayoutDisplay,
2541 CssPropertyType::Display,
2542 compact = get_display
2543);
2544
2545#[must_use] pub fn get_display_property(
2546 styled_dom: &StyledDom,
2547 dom_id: Option<NodeId>,
2548) -> MultiValue<LayoutDisplay> {
2549 let Some(id) = dom_id else {
2550 return MultiValue::Exact(LayoutDisplay::Inline);
2551 };
2552 let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
2553 get_display_property_internal(styled_dom, id, node_state)
2554}
2555
2556#[allow(clippy::match_same_arms)] #[must_use] pub const fn blockify_display(raw_display: LayoutDisplay) -> LayoutDisplay {
2563 match raw_display {
2564 LayoutDisplay::Inline => LayoutDisplay::Block,
2566 LayoutDisplay::InlineBlock => LayoutDisplay::Block,
2569 LayoutDisplay::InlineFlex => LayoutDisplay::Flex,
2570 LayoutDisplay::InlineTable => LayoutDisplay::Table,
2571 LayoutDisplay::InlineGrid => LayoutDisplay::Grid,
2572 LayoutDisplay::TableRowGroup
2575 | LayoutDisplay::TableColumn
2576 | LayoutDisplay::TableColumnGroup
2577 | LayoutDisplay::TableHeaderGroup
2578 | LayoutDisplay::TableFooterGroup
2579 | LayoutDisplay::TableRow
2580 | LayoutDisplay::TableCell
2581 | LayoutDisplay::TableCaption => LayoutDisplay::Block,
2582 other => other,
2584 }
2585}
2586
2587#[allow(clippy::fn_params_excessive_bools)]
2599#[must_use] pub fn get_computed_display(
2600 raw_display: LayoutDisplay,
2601 is_absolute_or_fixed: bool,
2602 is_floated: bool,
2603 is_root: bool,
2604 is_flex_grid_child: bool,
2605) -> LayoutDisplay {
2606 if raw_display == LayoutDisplay::None {
2607 return LayoutDisplay::None;
2608 }
2609 if is_absolute_or_fixed || is_floated || is_root || is_flex_grid_child {
2611 blockify_display(raw_display)
2612 } else {
2613 raw_display
2614 }
2615}
2616
2617#[must_use] pub fn get_vertical_align_for_node(
2622 styled_dom: &StyledDom,
2623 dom_id: NodeId,
2624) -> crate::text3::cache::VerticalAlign {
2625 let node_state = &styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
2626 let va = match get_vertical_align_property(styled_dom, dom_id, node_state) {
2627 MultiValue::Exact(v) => v,
2628 _ => StyleVerticalAlign::default(),
2629 };
2630 match va {
2631 StyleVerticalAlign::Baseline => crate::text3::cache::VerticalAlign::Baseline,
2632 StyleVerticalAlign::Top => crate::text3::cache::VerticalAlign::Top,
2633 StyleVerticalAlign::Middle => crate::text3::cache::VerticalAlign::Middle,
2634 StyleVerticalAlign::Bottom => crate::text3::cache::VerticalAlign::Bottom,
2635 StyleVerticalAlign::Sub => crate::text3::cache::VerticalAlign::Sub,
2636 StyleVerticalAlign::Superscript => crate::text3::cache::VerticalAlign::Super,
2637 StyleVerticalAlign::TextTop => crate::text3::cache::VerticalAlign::TextTop,
2638 StyleVerticalAlign::TextBottom => crate::text3::cache::VerticalAlign::TextBottom,
2639 StyleVerticalAlign::Percentage(p) => {
2641 let font_size = get_element_font_size(styled_dom, dom_id, node_state);
2642 let line_height = get_line_height_value(styled_dom, dom_id, node_state)
2649 .map_or(font_size * 1.2, |lh| {
2650 let n = lh.inner.normalized();
2651 if n < 0.0 { -n } else { n * font_size }
2652 });
2653 crate::text3::cache::VerticalAlign::Offset(p.normalized() * line_height)
2654 }
2655 StyleVerticalAlign::Length(l) => {
2657 let font_size = get_element_font_size(styled_dom, dom_id, node_state);
2658 let px = super::calc::resolve_pixel_value(&l, 0.0, font_size, font_size);
2666 crate::text3::cache::VerticalAlign::Offset(px)
2667 }
2668 }
2669}
2670
2671#[allow(clippy::cast_possible_truncation)] #[allow(clippy::too_many_lines, clippy::cognitive_complexity)] pub fn get_style_properties(
2677 styled_dom: &StyledDom,
2678 dom_id: NodeId,
2679 system_style: Option<&std::sync::Arc<azul_css::system::SystemStyle>>,
2680 viewport_size: PhysicalSize,
2681) -> StyleProperties {
2682 use azul_css::props::basic::{PhysicalSize, PropertyContext, ResolutionContext};
2683
2684 let node_data = &styled_dom.node_data.as_container()[dom_id];
2685 let node_state = &styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
2686 let cache = &styled_dom.css_property_cache.ptr;
2687
2688 let font_families = if node_state.is_normal() {
2691 cache
2692 .compact_cache
2693 .as_ref()
2694 .and_then(|cc| {
2695 let fh = cc.tier2b_text[dom_id.index()].font_family_hash;
2696 if fh == 0 {
2697 return None;
2698 }
2699 cc.font_hash_to_families.get(&fh).cloned()
2700 })
2701 .unwrap_or_else(|| {
2702 StyleFontFamilyVec::from_vec(vec![StyleFontFamily::System("serif".into())])
2703 })
2704 } else {
2705 cache
2706 .get_font_family(node_data, &dom_id, node_state)
2707 .and_then(|v| v.get_property().cloned())
2708 .unwrap_or_else(|| {
2709 StyleFontFamilyVec::from_vec(vec![StyleFontFamily::System("serif".into())])
2710 })
2711 };
2712
2713 let parent_font_size = get_parent_font_size(styled_dom, dom_id, node_state);
2719
2720 let root_font_size = get_root_font_size(styled_dom, node_state);
2721
2722 let font_size_context = ResolutionContext {
2724 element_font_size: DEFAULT_FONT_SIZE, parent_font_size,
2726 root_font_size,
2727 containing_block_size: PhysicalSize::new(0.0, 0.0),
2728 element_size: None,
2729 viewport_size,
2730 };
2731
2732 let font_size = {
2736 let mut fast_font_size: Option<f32> = None;
2741 let mut compact_said_inherit = false;
2742 if node_state.is_normal() {
2743 if let Some(ref cc) = cache.compact_cache {
2744 let raw = cc.get_font_size_raw(dom_id.index());
2745 if raw == azul_css::compact_cache::U32_SENTINEL
2746 || raw == azul_css::compact_cache::U32_INHERIT
2747 || raw == azul_css::compact_cache::U32_INITIAL
2748 {
2749 compact_said_inherit = true;
2750 } else if let Some(pv) = azul_css::compact_cache::decode_pixel_value_u32(raw) {
2751 fast_font_size = Some(
2752 pv.resolve_with_context(&font_size_context, PropertyContext::FontSize),
2753 );
2754 }
2755 }
2756 }
2757 fast_font_size.unwrap_or_else(|| {
2758 if compact_said_inherit {
2759 parent_font_size
2760 } else {
2761 cache
2762 .get_font_size(node_data, &dom_id, node_state)
2763 .and_then(|v| v.get_property().copied())
2764 .map_or(parent_font_size, |v| {
2765 v.inner
2766 .resolve_with_context(&font_size_context, PropertyContext::FontSize)
2767 })
2768 }
2769 })
2770 };
2771
2772 let color_from_cache = {
2773 let mut fast_color = None;
2775 if node_state.is_normal() {
2776 if let Some(ref cc) = cache.compact_cache {
2777 let raw = cc.get_text_color_raw(dom_id.index());
2778 if raw != 0 {
2779 fast_color = Some(ColorU {
2781 r: (raw >> 24) as u8,
2782 g: (raw >> 16) as u8,
2783 b: (raw >> 8) as u8,
2784 a: raw as u8,
2785 });
2786 }
2787 }
2788 }
2789 fast_color.or_else(|| {
2790 cache
2791 .get_text_color(node_data, &dom_id, node_state)
2792 .and_then(|v| v.get_property().copied())
2793 .map(|v| v.inner)
2794 })
2795 };
2796
2797 let color = color_from_cache.unwrap_or(ColorU::BLACK);
2803
2804 let line_height = {
2806 let mut fast_lh = None;
2815 let mut sentinel_normal = false;
2816 if node_state.is_normal() {
2817 if let Some(ref cc) = cache.compact_cache {
2818 if let Some(normalized) = cc.get_line_height(dom_id.index()) {
2819 let n = normalized / 100.0;
2826 fast_lh = Some(crate::text3::cache::LineHeight::Px(
2827 if n < 0.0 { -n } else { n * font_size },
2828 ));
2829 } else {
2830 sentinel_normal = true;
2832 }
2833 }
2834 }
2835 if sentinel_normal {
2836 crate::text3::cache::LineHeight::Normal
2837 } else {
2838 fast_lh.unwrap_or_else(|| {
2839 cache
2840 .get_line_height(node_data, &dom_id, node_state)
2841 .and_then(|v| v.get_property().copied())
2842 .map_or(crate::text3::cache::LineHeight::Normal, |v| {
2843 let n = v.inner.normalized();
2846 crate::text3::cache::LineHeight::Px(if n < 0.0 { -n } else { n * font_size })
2847 })
2848 })
2849 }
2850 };
2851
2852 let display = match get_display_property(styled_dom, Some(dom_id)) {
2863 MultiValue::Exact(v) => v,
2864 _ => LayoutDisplay::Inline,
2865 };
2866
2867 let (background_color, background_content, border) =
2870 if matches!(display, LayoutDisplay::Inline | LayoutDisplay::InlineBlock) {
2871 let bg = get_background_color(styled_dom, dom_id, node_state);
2872 let bg_color = if bg.a > 0 { Some(bg) } else { None };
2873
2874 let bg_contents = get_background_contents(styled_dom, dom_id, node_state);
2876
2877 let border_info = get_border_info(styled_dom, dom_id, node_state);
2879 let inline_border =
2880 get_inline_border_info(styled_dom, dom_id, node_state, &border_info, viewport_size);
2881
2882 (bg_color, bg_contents, inline_border)
2883 } else {
2884 (None, Vec::new(), None)
2887 };
2888
2889 let font_weight = match get_font_weight_property(styled_dom, dom_id, node_state) {
2891 MultiValue::Exact(v) => v,
2892 _ => StyleFontWeight::Normal,
2893 };
2894
2895 let font_style = match get_font_style_property(styled_dom, dom_id, node_state) {
2897 MultiValue::Exact(v) => v,
2898 _ => StyleFontStyle::Normal,
2899 };
2900
2901 let fc_weight = super::fc::convert_font_weight(font_weight);
2903 let fc_style = super::fc::convert_font_style(font_style);
2904
2905 let font_stack = {
2908 let font_ref = (0..font_families.len()).find_map(|i| match font_families.get(i).unwrap() {
2909 StyleFontFamily::Ref(r) => Some(r.clone()),
2910 _ => None,
2911 });
2912
2913 font_ref.map_or_else(
2914 || {
2915 let platform = system_style.map(|ss| &ss.platform);
2920 FontStack::Stack(build_font_selector_stack(
2921 &font_families,
2922 platform,
2923 fc_weight,
2924 fc_style,
2925 ))
2926 },
2927 FontStack::Ref,
2928 )
2929 };
2930
2931 let letter_spacing = {
2933 let mut fast_ls = None;
2935 if node_state.is_normal() {
2936 if let Some(ref cc) = cache.compact_cache {
2937 if let Some(px_val) = cc.get_letter_spacing(dom_id.index()) {
2938 fast_ls = Some(crate::text3::cache::Spacing::PxF(px_val));
2939 }
2940 }
2941 }
2942 fast_ls.unwrap_or_else(|| {
2943 cache
2944 .get_letter_spacing(node_data, &dom_id, node_state)
2945 .and_then(|v| v.get_property().copied())
2946 .map(|v| {
2947 let px_value = v
2948 .inner
2949 .resolve_with_context(&font_size_context, PropertyContext::FontSize);
2950 crate::text3::cache::Spacing::PxF(px_value)
2951 })
2952 .unwrap_or_default()
2953 })
2954 };
2955
2956 let word_spacing = {
2958 let mut fast_ws = None;
2960 if node_state.is_normal() {
2961 if let Some(ref cc) = cache.compact_cache {
2962 if let Some(px_val) = cc.get_word_spacing(dom_id.index()) {
2963 fast_ws = Some(crate::text3::cache::Spacing::PxF(px_val));
2964 }
2965 }
2966 }
2967 fast_ws.unwrap_or_else(|| {
2968 cache
2969 .get_word_spacing(node_data, &dom_id, node_state)
2970 .and_then(|v| v.get_property().copied())
2971 .map(|v| {
2972 let px_value = v
2973 .inner
2974 .resolve_with_context(&font_size_context, PropertyContext::FontSize);
2975 crate::text3::cache::Spacing::PxF(px_value)
2976 })
2977 .unwrap_or_default()
2978 })
2979 };
2980
2981 let text_decoration = {
2988 let mut skip_walk = false;
2989 if node_state.is_normal() {
2990 if let Some(ref cc) = cache.compact_cache {
2991 if !cc.has_text_decoration(dom_id.index()) {
2992 skip_walk = true;
2993 }
2994 }
2995 }
2996 if skip_walk {
2997 crate::text3::cache::TextDecoration::default()
2998 } else {
2999 cache
3000 .get_text_decoration(node_data, &dom_id, node_state)
3001 .and_then(|v| v.get_property().copied())
3002 .map(crate::text3::cache::TextDecoration::from_css)
3003 .unwrap_or_default()
3004 }
3005 };
3006
3007 let tab_size = {
3019 let mut fast_tab = None;
3020 if node_state.is_normal() {
3021 if let Some(ref cc) = cache.compact_cache {
3022 let raw = cc.get_tab_size_raw(dom_id.index());
3023 if raw < azul_css::compact_cache::I16_SENTINEL_THRESHOLD {
3024 fast_tab = Some(f32::from(raw) / 10.0);
3025 } else {
3026 fast_tab = Some(8.0);
3028 }
3029 }
3030 }
3031 fast_tab.unwrap_or_else(|| {
3032 cache
3033 .get_tab_size(node_data, &dom_id, node_state)
3034 .and_then(|v| v.get_property().copied())
3035 .map_or(DEFAULT_TAB_SIZE, |v| v.inner.number.get())
3036 })
3037 };
3038
3039 let text_transform = cache
3043 .get_text_transform(node_data, &dom_id, node_state)
3044 .and_then(|v| v.get_property().copied())
3045 .map(|t| {
3046 use azul_css::props::style::text::StyleTextTransform as Css;
3047 use crate::text3::cache::TextTransform as T3;
3048 match t {
3049 Css::None => T3::None,
3050 Css::Uppercase => T3::Uppercase,
3051 Css::Lowercase => T3::Lowercase,
3052 Css::Capitalize => T3::Capitalize,
3053 Css::FullWidth => T3::FullWidth,
3054 }
3055 })
3056 .unwrap_or_default();
3057
3058 StyleProperties {
3059 font_stack,
3060 font_size_px: font_size,
3061 color,
3062 background_color,
3063 background_content,
3064 border,
3065 line_height,
3066 letter_spacing,
3067 word_spacing,
3068 text_decoration,
3069 tab_size,
3070 text_transform,
3071 vertical_align: get_vertical_align_for_node(styled_dom, dom_id),
3076 ..Default::default()
3080 }
3081}
3082
3083#[must_use] pub fn get_list_style_type(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> StyleListStyleType {
3084 let Some(id) = dom_id else {
3085 return StyleListStyleType::default();
3086 };
3087 let node_data = &styled_dom.node_data.as_container()[id];
3088 let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
3089 styled_dom
3090 .css_property_cache
3091 .ptr
3092 .get_list_style_type(node_data, &id, node_state)
3093 .and_then(|v| v.get_property().copied())
3094 .unwrap_or_default()
3095}
3096
3097#[must_use] pub fn get_list_style_position(
3098 styled_dom: &StyledDom,
3099 dom_id: Option<NodeId>,
3100) -> StyleListStylePosition {
3101 let Some(id) = dom_id else {
3102 return StyleListStylePosition::default();
3103 };
3104 let node_data = &styled_dom.node_data.as_container()[id];
3105 let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
3106 styled_dom
3107 .css_property_cache
3108 .ptr
3109 .get_list_style_position(node_data, &id, node_state)
3110 .and_then(|v| v.get_property().copied())
3111 .unwrap_or_default()
3112}
3113
3114use azul_css::props::layout::{
3117 LayoutInsetBottom, LayoutLeft, LayoutMarginBottom, LayoutMarginLeft, LayoutMarginRight,
3118 LayoutMarginTop, LayoutMaxHeight, LayoutMaxWidth, LayoutMinHeight, LayoutMinWidth,
3119 LayoutPaddingBottom, LayoutPaddingLeft, LayoutPaddingRight, LayoutPaddingTop, LayoutRight,
3120 LayoutTop,
3121};
3122
3123get_css_property_pixel!(
3125 get_css_left,
3126 get_left,
3127 CssPropertyType::Left,
3128 compact_i16 = get_left
3129);
3130get_css_property_pixel!(
3131 get_css_right,
3132 get_right,
3133 CssPropertyType::Right,
3134 compact_i16 = get_right
3135);
3136get_css_property_pixel!(
3137 get_css_top,
3138 get_top,
3139 CssPropertyType::Top,
3140 compact_i16 = get_top
3141);
3142get_css_property_pixel!(
3143 get_css_bottom,
3144 get_bottom,
3145 CssPropertyType::Bottom,
3146 compact_i16 = get_bottom
3147);
3148
3149get_css_property_pixel!(
3151 get_css_margin_left,
3152 get_margin_left,
3153 CssPropertyType::MarginLeft,
3154 compact_i16 = get_margin_left_raw
3155);
3156get_css_property_pixel!(
3157 get_css_margin_right,
3158 get_margin_right,
3159 CssPropertyType::MarginRight,
3160 compact_i16 = get_margin_right_raw
3161);
3162get_css_property_pixel!(
3163 get_css_margin_top,
3164 get_margin_top,
3165 CssPropertyType::MarginTop,
3166 compact_i16 = get_margin_top_raw
3167);
3168get_css_property_pixel!(
3169 get_css_margin_bottom,
3170 get_margin_bottom,
3171 CssPropertyType::MarginBottom,
3172 compact_i16 = get_margin_bottom_raw
3173);
3174
3175get_css_property_pixel!(
3177 get_css_padding_left,
3178 get_padding_left,
3179 CssPropertyType::PaddingLeft,
3180 compact_i16 = get_padding_left_raw
3181);
3182get_css_property_pixel!(
3183 get_css_padding_right,
3184 get_padding_right,
3185 CssPropertyType::PaddingRight,
3186 compact_i16 = get_padding_right_raw
3187);
3188get_css_property_pixel!(
3189 get_css_padding_top,
3190 get_padding_top,
3191 CssPropertyType::PaddingTop,
3192 compact_i16 = get_padding_top_raw
3193);
3194get_css_property_pixel!(
3195 get_css_padding_bottom,
3196 get_padding_bottom,
3197 CssPropertyType::PaddingBottom,
3198 compact_i16 = get_padding_bottom_raw
3199);
3200
3201get_css_property!(
3203 get_css_min_width,
3204 get_min_width,
3205 LayoutMinWidth,
3206 CssPropertyType::MinWidth,
3207 compact_u32_struct = get_min_width_raw
3208);
3209
3210get_css_property!(
3211 get_css_min_height,
3212 get_min_height,
3213 LayoutMinHeight,
3214 CssPropertyType::MinHeight,
3215 compact_u32_struct = get_min_height_raw
3216);
3217
3218get_css_property!(
3219 get_css_max_width,
3220 get_max_width,
3221 LayoutMaxWidth,
3222 CssPropertyType::MaxWidth,
3223 compact_u32_struct = get_max_width_raw
3224);
3225
3226get_css_property!(
3227 get_css_max_height,
3228 get_max_height,
3229 LayoutMaxHeight,
3230 CssPropertyType::MaxHeight,
3231 compact_u32_struct = get_max_height_raw
3232);
3233
3234get_css_property_pixel!(
3236 get_css_border_left_width,
3237 get_border_left_width,
3238 CssPropertyType::BorderLeftWidth,
3239 compact_i16 = get_border_left_width_raw
3240);
3241get_css_property_pixel!(
3242 get_css_border_right_width,
3243 get_border_right_width,
3244 CssPropertyType::BorderRightWidth,
3245 compact_i16 = get_border_right_width_raw
3246);
3247get_css_property_pixel!(
3248 get_css_border_top_width,
3249 get_border_top_width,
3250 CssPropertyType::BorderTopWidth,
3251 compact_i16 = get_border_top_width_raw
3252);
3253get_css_property_pixel!(
3254 get_css_border_bottom_width,
3255 get_border_bottom_width,
3256 CssPropertyType::BorderBottomWidth,
3257 compact_i16 = get_border_bottom_width_raw
3258);
3259
3260#[must_use] pub fn get_break_before(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> PageBreak {
3264 let Some(id) = dom_id else {
3265 return PageBreak::Auto;
3266 };
3267 let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
3268 if node_state.is_normal() {
3270 if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
3271 if !cc.has_break(id.index()) {
3272 return PageBreak::Auto;
3273 }
3274 }
3275 }
3276 let node_data = &styled_dom.node_data.as_container()[id];
3277 styled_dom
3278 .css_property_cache
3279 .ptr
3280 .get_break_before(node_data, &id, node_state)
3281 .and_then(|v| v.get_property().copied())
3282 .unwrap_or(PageBreak::Auto)
3283}
3284
3285#[must_use] pub fn get_break_after(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> PageBreak {
3287 let Some(id) = dom_id else {
3288 return PageBreak::Auto;
3289 };
3290 let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
3291 if node_state.is_normal() {
3292 if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
3293 if !cc.has_break(id.index()) {
3294 return PageBreak::Auto;
3295 }
3296 }
3297 }
3298 let node_data = &styled_dom.node_data.as_container()[id];
3299 styled_dom
3300 .css_property_cache
3301 .ptr
3302 .get_break_after(node_data, &id, node_state)
3303 .and_then(|v| v.get_property().copied())
3304 .unwrap_or(PageBreak::Auto)
3305}
3306
3307#[must_use] pub const fn is_forced_page_break(page_break: PageBreak) -> bool {
3309 matches!(
3310 page_break,
3311 PageBreak::Always
3312 | PageBreak::Page
3313 | PageBreak::Left
3314 | PageBreak::Right
3315 | PageBreak::Recto
3316 | PageBreak::Verso
3317 | PageBreak::All
3318 )
3319}
3320
3321#[must_use] pub fn get_break_inside(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> BreakInside {
3323 let Some(id) = dom_id else {
3324 return BreakInside::Auto;
3325 };
3326 let node_data = &styled_dom.node_data.as_container()[id];
3327 let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
3328 styled_dom
3329 .css_property_cache
3330 .ptr
3331 .get_break_inside(node_data, &id, node_state)
3332 .and_then(|v| v.get_property().copied())
3333 .unwrap_or(BreakInside::Auto)
3334}
3335
3336#[must_use] pub fn get_orphans(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> u32 {
3338 let Some(id) = dom_id else {
3339 return 2; };
3341 let node_data = &styled_dom.node_data.as_container()[id];
3342 let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
3343 styled_dom
3344 .css_property_cache
3345 .ptr
3346 .get_orphans(node_data, &id, node_state)
3347 .and_then(|v| v.get_property().copied())
3348 .map_or(2, |o| o.inner)
3349}
3350
3351#[must_use] pub fn get_widows(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> u32 {
3353 let Some(id) = dom_id else {
3354 return 2; };
3356 let node_data = &styled_dom.node_data.as_container()[id];
3357 let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
3358 styled_dom
3359 .css_property_cache
3360 .ptr
3361 .get_widows(node_data, &id, node_state)
3362 .and_then(|v| v.get_property().copied())
3363 .map_or(2, |w| w.inner)
3364}
3365
3366#[must_use] pub fn get_box_decoration_break(
3368 styled_dom: &StyledDom,
3369 dom_id: Option<NodeId>,
3370) -> BoxDecorationBreak {
3371 let Some(id) = dom_id else {
3372 return BoxDecorationBreak::Slice;
3373 };
3374 let node_data = &styled_dom.node_data.as_container()[id];
3375 let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
3376 styled_dom
3377 .css_property_cache
3378 .ptr
3379 .get_box_decoration_break(node_data, &id, node_state)
3380 .and_then(|v| v.get_property().copied())
3381 .unwrap_or(BoxDecorationBreak::Slice)
3382}
3383
3384#[must_use] pub const fn is_avoid_page_break(page_break: &PageBreak) -> bool {
3388 matches!(page_break, PageBreak::Avoid | PageBreak::AvoidPage)
3389}
3390
3391#[must_use] pub const fn is_avoid_break_inside(break_inside: &BreakInside) -> bool {
3393 matches!(
3394 break_inside,
3395 BreakInside::Avoid | BreakInside::AvoidPage | BreakInside::AvoidColumn
3396 )
3397}
3398
3399use std::collections::HashMap;
3402
3403use rust_fontconfig::{
3404 FcFontCache, FcWeight, FontFallbackChain, PatternMatch, UnicodeRange,
3405 DEFAULT_UNICODE_FALLBACK_SCRIPTS,
3406};
3407
3408use crate::text3::cache::{FontChainKey, FontChainKeyOrRef, FontSelector, FontStack, FontStyle};
3409
3410#[allow(clippy::option_if_let_else)]
3426fn build_font_selector_stack(
3427 font_families: &StyleFontFamilyVec,
3428 platform: Option<&azul_css::system::Platform>,
3429 fc_weight: FcWeight,
3430 fc_style: FontStyle,
3431) -> Vec<FontSelector> {
3432 let mut stack = Vec::with_capacity(font_families.len() + 3);
3433
3434 for i in 0..font_families.len() {
3435 let family = font_families.get(i).unwrap();
3436 if matches!(family, StyleFontFamily::Ref(_)) {
3437 continue;
3438 }
3439 if let StyleFontFamily::SystemType(system_type) = family {
3440 let current;
3441 let platform = if let Some(p) = platform { p } else {
3442 current = azul_css::system::Platform::current();
3443 ¤t
3444 };
3445 let font_names = system_type.get_fallback_chain(platform);
3446 let system_weight = if system_type.is_bold() {
3447 FcWeight::Bold
3448 } else {
3449 fc_weight
3450 };
3451 let system_style = if system_type.is_italic() {
3452 FontStyle::Italic
3453 } else {
3454 fc_style
3455 };
3456 for font_name in font_names {
3457 stack.push(FontSelector {
3458 family: font_name.to_string(),
3459 weight: system_weight,
3460 style: system_style,
3461 unicode_ranges: Vec::new(),
3462 });
3463 }
3464 } else {
3465 stack.push(FontSelector {
3466 family: family.as_query_string(),
3470 weight: fc_weight,
3471 style: fc_style,
3472 unicode_ranges: Vec::new(),
3473 });
3474 }
3475 }
3476
3477 for fallback in &["sans-serif", "serif", "monospace"] {
3478 if !stack
3479 .iter()
3480 .any(|f| f.family.eq_ignore_ascii_case(fallback))
3481 {
3482 stack.push(FontSelector {
3483 family: (*fallback).to_string(),
3484 weight: FcWeight::Normal,
3485 style: FontStyle::Normal,
3486 unicode_ranges: Vec::new(),
3487 });
3488 }
3489 }
3490
3491 stack
3492}
3493
3494#[derive(Debug, Clone)]
3497pub struct CollectedFontStacks {
3498 pub font_stacks: Vec<Vec<FontSelector>>,
3500 pub hash_to_index: HashMap<u64, usize>,
3502 pub font_refs: HashMap<usize, azul_css::props::basic::font::FontRef>,
3505}
3506
3507#[derive(Debug, Clone, Default)]
3510pub struct ResolvedFontChains {
3511 pub chains: HashMap<FontChainKeyOrRef, FontFallbackChain>,
3515 pub unresolved_families: std::collections::BTreeSet<String>,
3526 pub last_resort_chains: usize,
3530}
3531
3532impl ResolvedFontChains {
3533 #[must_use] pub fn get(&self, key: &FontChainKeyOrRef) -> Option<&FontFallbackChain> {
3535 self.chains.get(key)
3536 }
3537
3538 #[must_use] pub fn get_by_chain_key(&self, key: &FontChainKey) -> Option<&FontFallbackChain> {
3540 self.chains.get(&FontChainKeyOrRef::Chain(key.clone()))
3541 }
3542
3543 #[must_use] pub fn get_for_font_stack(&self, font_stack: &[FontSelector]) -> Option<&FontFallbackChain> {
3545 let key = FontChainKeyOrRef::Chain(FontChainKey::from_selectors(font_stack));
3546 self.chains.get(&key)
3547 }
3548
3549 #[must_use] pub fn get_for_font_ref(&self, ptr: usize) -> Option<&FontFallbackChain> {
3551 self.chains.get(&FontChainKeyOrRef::Ref(ptr))
3552 }
3553
3554 #[must_use] pub fn into_inner(self) -> HashMap<FontChainKeyOrRef, FontFallbackChain> {
3558 self.chains
3559 }
3560
3561 #[must_use] pub fn into_fontconfig_chains(self) -> HashMap<FontChainKey, FontFallbackChain> {
3566 let mut out: HashMap<FontChainKey, FontFallbackChain> = HashMap::new();
3570 if self.chains.is_empty() {
3571 return out;
3572 }
3573 for (key, chain) in self.chains {
3574 if let FontChainKeyOrRef::Chain(chain_key) = key {
3575 out.insert(chain_key, chain);
3576 }
3577 }
3578 out
3579 }
3580
3581 #[must_use] pub fn len(&self) -> usize {
3583 self.chains.len()
3584 }
3585
3586 #[must_use] pub fn is_empty(&self) -> bool {
3588 self.chains.is_empty()
3589 }
3590
3591 #[must_use] pub fn font_refs_len(&self) -> usize {
3593 self.chains.keys().filter(|k| k.is_ref()).count()
3594 }
3595}
3596
3597#[allow(clippy::cast_possible_truncation)] #[allow(clippy::too_many_lines)] #[must_use] pub fn collect_font_stacks_from_styled_dom(
3611 styled_dom: &StyledDom,
3612 platform: &azul_css::system::Platform,
3613) -> CollectedFontStacks {
3614 use azul_css::compact_cache::{
3615 FONT_STYLE_MASK, FONT_STYLE_SHIFT, FONT_WEIGHT_MASK, FONT_WEIGHT_SHIFT,
3616 };
3617
3618 let mut font_stacks = Vec::new();
3619 let mut hash_to_index: HashMap<u64, usize> = HashMap::new();
3620 let mut font_refs: HashMap<usize, azul_css::props::basic::font::FontRef> = HashMap::new();
3621
3622 let node_data = styled_dom.node_data.as_container();
3623 let cache = &styled_dom.css_property_cache.ptr;
3624 let Some(compact) = cache.compact_cache.as_ref() else {
3625 return CollectedFontStacks {
3626 font_stacks,
3627 hash_to_index,
3628 font_refs,
3629 };
3630 };
3631
3632 let mut unique_font_keys: HashMap<(u64, u8, u8), usize> = HashMap::new();
3641 let node_count = node_data.internal.len();
3642
3643 if node_count > 1 {
3646 let p1 = (&raw const node_data.internal[1].node_type).cast::<u8>();
3647 let p0 = (&raw const node_data.internal[0].node_type).cast::<u8>();
3648 unsafe {
3649 crate::az_mark(0x606D0_u32, u32::from(core::ptr::read(p1)));
3650 crate::az_mark(0x606D4_u32, u32::from(core::ptr::read(p1.add(1))));
3651 crate::az_mark(0x606D8_u32, u32::from(core::ptr::read(p1.add(2))));
3652 crate::az_mark(0x606DC_u32, u32::from(core::ptr::read(p1.add(4))));
3653 crate::az_mark(0x606E0_u32, u32::from(core::ptr::read(p0)));
3654 }
3655 }
3656 for i in 0..node_count {
3657 let nt_disc = unsafe {
3664 core::ptr::read((&raw const node_data.internal[i].node_type).cast::<u8>())
3665 };
3666 let is_text = nt_disc == 177
3667 || matches!(node_data.internal[i].node_type, NodeType::Text(_));
3668 if !is_text {
3669 continue;
3670 }
3671 let fh = compact.tier2b_text[i].font_family_hash;
3672 let t1 = compact.tier1_enums[i];
3673 let weight_bits = ((t1 >> FONT_WEIGHT_SHIFT) & FONT_WEIGHT_MASK) as u8;
3674 let style_bits = ((t1 >> FONT_STYLE_SHIFT) & FONT_STYLE_MASK) as u8;
3675 let key = (fh, weight_bits, style_bits);
3676 unique_font_keys.entry(key).or_insert(i);
3677 }
3678
3679 {
3685 let mut raw_text = 0u32;
3686 for i in 0..node_count {
3687 let nt_ptr = (&raw const node_data.internal[i].node_type).cast::<u8>();
3689 let disc = unsafe { core::ptr::read_volatile(nt_ptr) };
3690 if disc != unsafe { core::ptr::read_volatile((&raw const node_data.internal[0].node_type).cast::<u8>()) } {
3692 raw_text += 1;
3693 }
3694 }
3695 unsafe {
3696 crate::az_mark(0x606C0_u32, (0x5E5E_0003_u32));
3697 crate::az_mark(0x606C4_u32, (node_count as u32));
3698 crate::az_mark(0x606C8_u32, (unique_font_keys.len() as u32));
3699 crate::az_mark(0x606CC_u32, (raw_text));
3700 }
3701 }
3702
3703 let styled_nodes = styled_dom.styled_nodes.as_container();
3706
3707 for (&(fh, _wb, _sb), &repr_idx) in &unique_font_keys {
3708 let Some(dom_id) = NodeId::from_usize(repr_idx) else {
3709 continue;
3710 };
3711 let node_state = &styled_nodes[dom_id].styled_node_state;
3712
3713 let font_families = compact
3717 .font_hash_to_families
3718 .get(&fh)
3719 .cloned()
3720 .unwrap_or_else(|| {
3721 StyleFontFamilyVec::from_vec(vec![StyleFontFamily::System("serif".into())])
3722 });
3723
3724 if let Some(StyleFontFamily::Ref(font_ref)) = font_families.get(0) {
3726 let ptr = font_ref.parsed as usize;
3727 font_refs.entry(ptr).or_insert_with(|| font_ref.clone());
3728 continue;
3729 }
3730
3731 let font_weight = match get_font_weight_property(styled_dom, dom_id, node_state) {
3732 MultiValue::Exact(v) => v,
3733 _ => StyleFontWeight::Normal,
3734 };
3735 let font_style = match get_font_style_property(styled_dom, dom_id, node_state) {
3736 MultiValue::Exact(v) => v,
3737 _ => StyleFontStyle::Normal,
3738 };
3739
3740 let fc_weight = super::fc::convert_font_weight(font_weight);
3741 let fc_style = super::fc::convert_font_style(font_style);
3742
3743 let font_stack =
3744 build_font_selector_stack(&font_families, Some(platform), fc_weight, fc_style);
3745
3746 if font_stack.is_empty() {
3747 continue;
3748 }
3749
3750 let key = FontChainKey::from_selectors(&font_stack);
3751 let hash = {
3752 use std::hash::{Hash, Hasher};
3753 let mut hasher = std::collections::hash_map::DefaultHasher::new();
3754 key.hash(&mut hasher);
3755 hasher.finish()
3756 };
3757
3758 hash_to_index.entry(hash).or_insert_with(|| {
3759 let idx = font_stacks.len();
3760 font_stacks.push(font_stack);
3761 idx
3762 });
3763 }
3764
3765 CollectedFontStacks {
3766 font_stacks,
3767 hash_to_index,
3768 font_refs,
3769 }
3770}
3771
3772#[must_use] pub fn collect_used_codepoints(styled_dom: &StyledDom) -> std::collections::BTreeSet<u32> {
3795 let mut out = std::collections::BTreeSet::new();
3796 let node_data = styled_dom.node_data.as_container();
3797 for node in node_data.internal {
3798 let NodeType::Text(s) = &node.node_type else {
3799 continue;
3800 };
3801 for c in s.as_str().chars() {
3802 let cp = c as u32;
3803 if cp >= 0x80 {
3804 out.insert(cp);
3805 }
3806 }
3807 }
3808 out
3809}
3810
3811#[must_use] pub fn collect_used_codepoints_all(styled_dom: &StyledDom) -> std::collections::BTreeSet<char> {
3825 let mut out = std::collections::BTreeSet::new();
3826 let node_data = styled_dom.node_data.as_container();
3827 for node in node_data.internal {
3828 let NodeType::Text(s) = &node.node_type else {
3829 continue;
3830 };
3831 for c in s.as_str().chars() {
3832 out.insert(c);
3833 }
3834 }
3835 out
3836}
3837
3838pub fn prune_chain_to_used_chars(
3860 chain: &mut FontFallbackChain,
3861 used_chars: &std::collections::BTreeSet<u32>,
3862) {
3863 fn fm_covers(fm: &rust_fontconfig::FontMatch, cp: u32) -> bool {
3864 fm.unicode_ranges
3865 .iter()
3866 .any(|r| cp >= r.start && cp <= r.end)
3867 }
3868
3869 for group in &mut chain.css_fallbacks {
3870 if group.fonts.is_empty() {
3871 continue;
3872 }
3873 let mut needed: Vec<u32> = used_chars.iter().copied().collect();
3876 needed.retain(|&cp| !fm_covers(&group.fonts[0], cp));
3877 let mut keep = 1;
3878 for fm in group.fonts.iter().skip(1) {
3879 if needed.is_empty() {
3880 break;
3881 }
3882 keep += 1;
3883 needed.retain(|&cp| !fm_covers(fm, cp));
3884 }
3885 group.fonts.truncate(keep);
3886 }
3887
3888 chain
3889 .unicode_fallbacks
3890 .retain(|fm| used_chars.iter().any(|&cp| fm_covers(fm, cp)));
3891}
3892
3893#[must_use] pub fn scripts_present_in_styled_dom(styled_dom: &StyledDom) -> Vec<UnicodeRange> {
3908 let scripts = DEFAULT_UNICODE_FALLBACK_SCRIPTS;
3909 let mut seen = vec![false; scripts.len()];
3910 let mut hits = 0usize;
3911 let node_data = styled_dom.node_data.as_container();
3912 'outer: for node in node_data.internal {
3913 let text: &str = match &node.node_type {
3914 NodeType::Text(s) => s.as_str(),
3915 _ => continue,
3916 };
3917 for c in text.chars() {
3918 let cp = c as u32;
3919 if cp < 0x0400 {
3923 continue;
3924 }
3925 for (idx, r) in scripts.iter().enumerate() {
3926 if !seen[idx] && cp >= r.start && cp <= r.end {
3927 seen[idx] = true;
3928 hits += 1;
3929 if hits == scripts.len() {
3930 break 'outer;
3931 }
3932 break;
3933 }
3934 }
3935 }
3936 }
3937 scripts
3938 .iter()
3939 .enumerate()
3940 .filter_map(|(i, r)| if seen[i] { Some(*r) } else { None })
3941 .collect()
3942}
3943
3944#[must_use] pub fn resolve_font_chains(
3959 collected: &CollectedFontStacks,
3960 fc_cache: &FcFontCache,
3961 scripts_hint: Option<&[UnicodeRange]>,
3962) -> ResolvedFontChains {
3963 resolve_font_chains_with_registry(collected, fc_cache, None, scripts_hint, &HashMap::new())
3964}
3965
3966fn split_memory_matches(
3978 font_families: &[String],
3979 memory_families: &HashMap<String, Vec<crate::text3::cache::MemoryFace>>,
3980 weight: FcWeight,
3981 italic: bool,
3982 oblique: bool,
3983) -> (
3984 Vec<rust_fontconfig::CssFallbackGroup>,
3985 Vec<String>,
3986 Vec<rust_fontconfig::CssFallbackGroup>,
3987) {
3988 use crate::text3::cache::MemoryFontTier;
3989
3990 let mut groups = Vec::new();
3991 let mut disk = Vec::new();
3992 let mut fallback = Vec::new();
3993 for family in font_families {
3994 let norm = rust_fontconfig::utils::normalize_family_name(family);
3995 let faces = memory_families.get(&norm);
3996
3997 if let Some(face) =
3999 faces.and_then(|f| pick_memory_face(f, weight, italic, oblique, MemoryFontTier::Primary))
4000 {
4001 groups.push(rust_fontconfig::CssFallbackGroup {
4002 css_name: family.clone(),
4003 fonts: vec![face.font_match.clone()],
4004 });
4005 continue;
4006 }
4007
4008 disk.push(family.clone());
4011 if let Some(face) = faces
4012 .and_then(|f| pick_memory_face(f, weight, italic, oblique, MemoryFontTier::Fallback))
4013 {
4014 fallback.push(rust_fontconfig::CssFallbackGroup {
4015 css_name: family.clone(),
4016 fonts: vec![face.font_match.clone()],
4017 });
4018 }
4019 }
4020 (groups, disk, fallback)
4021}
4022
4023fn pick_memory_face(
4033 faces: &[crate::text3::cache::MemoryFace],
4034 weight: FcWeight,
4035 italic: bool,
4036 oblique: bool,
4037 tier: crate::text3::cache::MemoryFontTier,
4038) -> Option<&crate::text3::cache::MemoryFace> {
4039 let faces: Vec<&crate::text3::cache::MemoryFace> =
4040 faces.iter().filter(|f| f.tier == tier).collect();
4041 if faces.is_empty() {
4042 return None;
4043 }
4044 let want_slanted = italic || oblique;
4045 let slant_pool: Vec<&crate::text3::cache::MemoryFace> = faces
4048 .iter()
4049 .copied()
4050 .filter(|f| (f.italic || f.oblique) == want_slanted)
4051 .collect();
4052 let pool: Vec<&crate::text3::cache::MemoryFace> = if slant_pool.is_empty() {
4053 faces.clone()
4054 } else {
4055 slant_pool
4056 };
4057 let req = f32::from(weight as u16);
4059 if let Some(vf) = pool
4060 .iter()
4061 .copied()
4062 .find(|f| f.weight_axis.is_some_and(|(min, max)| req >= min && req <= max))
4063 {
4064 return Some(vf);
4065 }
4066 let avail: Vec<FcWeight> = pool.iter().map(|f| f.weight).collect();
4068 let best = weight.find_best_match(&avail).unwrap_or(weight);
4069 pool.iter()
4070 .copied()
4071 .find(|f| f.weight == best)
4072 .or_else(|| pool.first().copied())
4073}
4074
4075#[allow(clippy::implicit_hasher)] #[must_use] pub fn resolve_font_chains_with_registry(
4091 collected: &CollectedFontStacks,
4092 fc_cache: &FcFontCache,
4093 registry: Option<&rust_fontconfig::registry::FcFontRegistry>,
4094 scripts_hint: Option<&[UnicodeRange]>,
4095 memory_families: &HashMap<String, Vec<crate::text3::cache::MemoryFace>>,
4096) -> ResolvedFontChains {
4097 let mut chains = HashMap::new();
4098 let mut unresolved: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
4099
4100 for font_stack in &collected.font_stacks {
4102 if font_stack.is_empty() {
4103 continue;
4104 }
4105
4106 let canonical_key = FontChainKey::from_selectors(font_stack);
4111 let font_families = canonical_key.font_families.clone();
4112
4113 let weight = font_stack[0].weight;
4114 let is_italic = font_stack[0].style == FontStyle::Italic;
4115 let is_oblique = font_stack[0].style == FontStyle::Oblique;
4116
4117 let cache_key = FontChainKeyOrRef::Chain(FontChainKey {
4118 font_families: font_families.clone(),
4119 weight,
4120 italic: is_italic,
4121 oblique: is_oblique,
4122 });
4123
4124 if chains.contains_key(&cache_key) {
4126 continue;
4127 }
4128
4129 let italic = if is_italic {
4134 PatternMatch::True
4135 } else {
4136 PatternMatch::False
4137 };
4138 let oblique = if is_oblique {
4139 PatternMatch::True
4140 } else {
4141 PatternMatch::False
4142 };
4143
4144 let (mem_groups, disk_families, mem_fallbacks) =
4148 split_memory_matches(&font_families, memory_families, weight, is_italic, is_oblique);
4149
4150 let mut chain = if disk_families.is_empty() {
4153 FontFallbackChain {
4154 css_fallbacks: Vec::new(),
4155 unicode_fallbacks: Vec::new(),
4156 original_stack: font_families.clone(),
4157 }
4158 } else {
4159 registry.map_or_else(
4160 || {
4161 let mut trace = Vec::new();
4162 fc_cache.resolve_font_chain_with_scripts(
4163 &disk_families,
4164 weight,
4165 italic,
4166 oblique,
4167 scripts_hint,
4168 &mut trace,
4169 )
4170 },
4171 |reg| {
4172 reg.request_and_resolve_with_scripts(
4173 &disk_families,
4174 weight,
4175 italic,
4176 oblique,
4177 scripts_hint,
4178 )
4179 },
4180 )
4181 };
4182 if !mem_groups.is_empty() {
4183 let mut merged = mem_groups;
4184 merged.append(&mut chain.css_fallbacks);
4185 chain.css_fallbacks = merged;
4186 }
4187 chain.css_fallbacks.extend(mem_fallbacks);
4191
4192 for family in &font_families {
4195 let matched = chain
4196 .css_fallbacks
4197 .iter()
4198 .any(|g| g.css_name.eq_ignore_ascii_case(family) && !g.fonts.is_empty());
4199 if !matched && !is_generic_family(family) {
4200 unresolved.insert(family.clone());
4201 }
4202 }
4203
4204 let total_fonts = chain.css_fallbacks.iter().map(|g| g.fonts.len()).sum::<usize>()
4211 + chain.unicode_fallbacks.len();
4212 if total_fonts == 0 {
4213 if let Some((_pattern, id)) = fc_cache.list().first() {
4214 chain.unicode_fallbacks.push(rust_fontconfig::FontMatch {
4217 id: *id,
4218 unicode_ranges: Vec::new(),
4219 fallbacks: Vec::new(),
4220 });
4221 }
4222 }
4223
4224 chains.insert(cache_key, chain);
4225 }
4226
4227 let out = ResolvedFontChains {
4232 chains,
4233 unresolved_families: unresolved,
4234 last_resort_chains: 0,
4235 };
4236 report_unresolved_families(&out);
4237 out
4238}
4239
4240fn ensure_chains_nonempty(resolved: &mut ResolvedFontChains, fc_cache: &FcFontCache) {
4251 let fallback_id = match fc_cache.list().first() {
4252 Some((_pattern, id)) => *id,
4253 None => return,
4254 };
4255 let keys: Vec<FontChainKeyOrRef> = resolved.chains.keys().cloned().collect();
4256 let mut rebuilt: HashMap<FontChainKeyOrRef, FontFallbackChain> =
4257 HashMap::new();
4258 let mut last_resort = 0usize;
4259 for key in keys {
4260 if let Some(mut chain) = resolved.chains.remove(&key) {
4261 let total = chain.css_fallbacks.iter().map(|g| g.fonts.len()).sum::<usize>()
4262 + chain.unicode_fallbacks.len();
4263 if total == 0 {
4264 last_resort += 1;
4270 if let FontChainKeyOrRef::Chain(k) = &key {
4271 eprintln!(
4272 "[azul][font] LAST-RESORT fallback for font stack {:?}: nothing in \
4273 the stack matched, rendering in an arbitrary system font.",
4274 k.font_families
4275 );
4276 }
4277 chain.unicode_fallbacks.push(rust_fontconfig::FontMatch {
4278 id: fallback_id,
4279 unicode_ranges: Vec::new(),
4280 fallbacks: Vec::new(),
4281 });
4282 }
4283 rebuilt.insert(key, chain);
4284 }
4285 }
4286 resolved.chains = rebuilt;
4287 resolved.last_resort_chains = last_resort;
4288}
4289
4290pub fn collect_and_resolve_font_chains_with_registration<T: ParsedFontTrait>(
4304 styled_dom: &StyledDom,
4305 fc_cache: &FcFontCache,
4306 font_manager: &crate::text3::cache::FontManager<T>,
4307 platform: &azul_css::system::Platform,
4308) -> ResolvedFontChains {
4309 let collected = collect_font_stacks_from_styled_dom(styled_dom, platform);
4310
4311 for font_ref in collected.font_refs.values() {
4313 font_manager.register_embedded_font(font_ref);
4314 }
4315
4316 if let Some(registry) = font_manager.registry.as_deref() {
4329 let used_chars = collect_used_codepoints_all(styled_dom);
4330 if !used_chars.is_empty() {
4331 let mut fast = resolve_font_chains_fast(
4332 &collected,
4333 registry,
4334 &used_chars,
4335 &font_manager.memory_families,
4336 );
4337 ensure_chains_nonempty(&mut fast, fc_cache);
4338 return fast;
4339 }
4340 }
4341
4342 let scripts = scripts_present_in_styled_dom(styled_dom);
4346 let mut resolved = resolve_font_chains_with_registry(
4347 &collected,
4348 fc_cache,
4349 font_manager.registry.as_deref(),
4350 Some(&scripts),
4351 &font_manager.memory_families,
4352 );
4353
4354 let used_chars = collect_used_codepoints(styled_dom);
4355 for chain in resolved.chains.values_mut() {
4356 prune_chain_to_used_chars(chain, &used_chars);
4357 }
4358 ensure_chains_nonempty(&mut resolved, fc_cache);
4366 resolved
4367}
4368
4369#[allow(clippy::implicit_hasher)] pub fn resolve_font_chains_fast(
4380 collected: &CollectedFontStacks,
4381 registry: &rust_fontconfig::registry::FcFontRegistry,
4382 codepoints: &std::collections::BTreeSet<char>,
4383 memory_families: &HashMap<String, Vec<crate::text3::cache::MemoryFace>>,
4384) -> ResolvedFontChains {
4385 use rust_fontconfig::PatternMatch;
4386
4387 static DBG: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
4388 let dbg = *DBG.get_or_init(|| std::env::var_os("AZ_FAST_RESOLVE_DEBUG").is_some());
4389
4390 let mut chains: HashMap<FontChainKeyOrRef, FontFallbackChain> = HashMap::new();
4391 let mut unresolved: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
4392
4393 for font_stack in &collected.font_stacks {
4394 if font_stack.is_empty() {
4395 continue;
4396 }
4397
4398 let canonical_key = FontChainKey::from_selectors(font_stack);
4402 let font_families = canonical_key.font_families.clone();
4403
4404 let weight = font_stack[0].weight;
4405 let is_italic = font_stack[0].style == FontStyle::Italic;
4406 let is_oblique = font_stack[0].style == FontStyle::Oblique;
4407
4408 let cache_key = FontChainKeyOrRef::Chain(FontChainKey {
4409 font_families: font_families.clone(),
4410 weight,
4411 italic: is_italic,
4412 oblique: is_oblique,
4413 });
4414
4415 if chains.contains_key(&cache_key) {
4416 continue;
4417 }
4418
4419 let italic_match = if is_italic {
4420 PatternMatch::True
4421 } else {
4422 PatternMatch::False
4423 };
4424
4425 let (mut css_fallbacks, disk_families, mem_fallbacks) =
4436 split_memory_matches(&font_families, memory_families, weight, is_italic, is_oblique);
4437
4438 let request = vec![(disk_families.clone(), codepoints.clone())];
4439 let mut chains_out = if disk_families.is_empty() {
4440 Vec::new()
4441 } else {
4442 registry.request_fonts_fast(&request, weight, italic_match)
4443 };
4444 if dbg {
4445 let total_fonts: usize = chains_out
4446 .iter()
4447 .map(|c| c.css_fallbacks.iter().map(|g| g.fonts.len()).sum::<usize>())
4448 .sum();
4449 eprintln!(
4450 "[FAST] stack {:?} w={:?} i={:?} → {} groups, {} faces",
4451 font_families,
4452 weight,
4453 italic_match,
4454 chains_out
4455 .first()
4456 .map_or(0, |c| c.css_fallbacks.len()),
4457 total_fonts,
4458 );
4459 }
4460 let mut chain = chains_out.pop().unwrap_or_else(|| FontFallbackChain {
4463 css_fallbacks: Vec::new(),
4464 unicode_fallbacks: Vec::new(),
4465 original_stack: font_families.clone(),
4466 });
4467 if !css_fallbacks.is_empty() {
4468 css_fallbacks.append(&mut chain.css_fallbacks);
4469 chain.css_fallbacks = css_fallbacks;
4470 }
4471 chain.css_fallbacks.extend(mem_fallbacks);
4475
4476 for family in &font_families {
4480 let matched = chain
4481 .css_fallbacks
4482 .iter()
4483 .any(|g| g.css_name.eq_ignore_ascii_case(family) && !g.fonts.is_empty());
4484 if !matched && !is_generic_family(family) {
4485 unresolved.insert(family.clone());
4486 }
4487 }
4488
4489 chains.insert(cache_key, chain);
4490 }
4491
4492 let out = ResolvedFontChains {
4493 chains,
4494 unresolved_families: unresolved,
4495 last_resort_chains: 0,
4496 };
4497 report_unresolved_families(&out);
4498 out
4499}
4500
4501fn is_generic_family(family: &str) -> bool {
4505 matches!(
4506 family.to_ascii_lowercase().as_str(),
4507 "serif"
4508 | "sans-serif"
4509 | "monospace"
4510 | "cursive"
4511 | "fantasy"
4512 | "system-ui"
4513 | "ui-serif"
4514 | "ui-sans-serif"
4515 | "ui-monospace"
4516 | "ui-rounded"
4517 | "emoji"
4518 | "math"
4519 | "fangsong"
4520 )
4521}
4522
4523fn report_unresolved_families(resolved: &ResolvedFontChains) {
4531 use std::sync::{Mutex, OnceLock};
4532 static SEEN: OnceLock<Mutex<std::collections::BTreeSet<String>>> = OnceLock::new();
4533 if resolved.unresolved_families.is_empty() {
4534 return;
4535 }
4536 let seen = SEEN.get_or_init(|| Mutex::new(std::collections::BTreeSet::new()));
4537 let Ok(mut seen) = seen.lock() else { return };
4538 for family in &resolved.unresolved_families {
4539 if seen.insert(family.clone()) {
4540 eprintln!(
4541 "[azul][font] UNRESOLVED font-family {family:?}: no font file and no \
4542 registered in-memory font matches this family. Text that asks for it \
4543 renders in a FALLBACK font. Register it with \
4544 FontManager::register_named_font(), or install it."
4545 );
4546 }
4547 }
4548}
4549
4550#[must_use] pub fn collect_and_resolve_font_chains(
4554 styled_dom: &StyledDom,
4555 fc_cache: &FcFontCache,
4556 platform: &azul_css::system::Platform,
4557) -> ResolvedFontChains {
4558 let collected = collect_font_stacks_from_styled_dom(styled_dom, platform);
4559 resolve_font_chains(&collected, fc_cache, None)
4560}
4561
4562pub fn register_embedded_fonts_from_styled_dom<T: ParsedFontTrait>(
4564 styled_dom: &StyledDom,
4565 font_manager: &crate::text3::cache::FontManager<T>,
4566 platform: &azul_css::system::Platform,
4567) {
4568 let collected = collect_font_stacks_from_styled_dom(styled_dom, platform);
4569 for font_ref in collected.font_refs.values() {
4570 font_manager.register_embedded_font(font_ref);
4571 }
4572}
4573
4574use std::collections::HashSet;
4577
4578use rust_fontconfig::FontId;
4579
4580#[must_use] pub fn collect_font_ids_from_chains(chains: &ResolvedFontChains) -> HashSet<FontId> {
4585 let mut font_ids = HashSet::new();
4586
4587 if chains.chains.is_empty() {
4591 return font_ids;
4592 }
4593
4594 for chain in chains.chains.values() {
4595 for group in &chain.css_fallbacks {
4597 for font in &group.fonts {
4598 font_ids.insert(font.id);
4599 }
4600 }
4601
4602 for font in &chain.unicode_fallbacks {
4604 font_ids.insert(font.id);
4605 }
4606 }
4607
4608 font_ids
4609}
4610
4611#[allow(clippy::implicit_hasher)] #[must_use] pub fn compute_fonts_to_load(
4621 required_fonts: &HashSet<FontId>,
4622 already_loaded: &HashSet<FontId>,
4623) -> HashSet<FontId> {
4624 if required_fonts.is_empty() {
4627 return HashSet::new();
4628 }
4629 required_fonts.difference(already_loaded).copied().collect()
4630}
4631
4632#[derive(Debug)]
4634pub struct FontLoadResult<T> {
4635 pub loaded: HashMap<FontId, T>,
4637 pub failed: Vec<(FontId, String)>,
4639}
4640
4641#[allow(clippy::implicit_hasher)] pub fn load_fonts_from_disk<T, F>(
4656 font_ids: &HashSet<FontId>,
4657 fc_cache: &FcFontCache,
4658 load_fn: F,
4659) -> FontLoadResult<T>
4660where
4661 F: Fn(
4666 std::sync::Arc<rust_fontconfig::FontBytes>,
4667 usize,
4668 ) -> Result<T, crate::text3::cache::LayoutError>,
4669{
4670 let mut loaded = HashMap::new();
4671 let mut failed = Vec::new();
4672
4673 for font_id in font_ids {
4674 let Some(font_bytes) = fc_cache.get_font_bytes(font_id) else {
4678 failed.push((
4679 *font_id,
4680 format!("Could not get font bytes for {font_id:?}"),
4681 ));
4682 continue;
4683 };
4684
4685 let font_index = fc_cache
4687 .get_font_by_id(font_id)
4688 .map_or(0, |source| match source {
4689 rust_fontconfig::OwnedFontSource::Disk(path) => path.font_index,
4690 rust_fontconfig::OwnedFontSource::Memory(font) => font.font_index,
4691 });
4692
4693 match load_fn(font_bytes, font_index) {
4695 Ok(font) => {
4696 loaded.insert(*font_id, font);
4697 }
4698 Err(e) => {
4699 failed.push((
4700 *font_id,
4701 format!("Failed to parse font {font_id:?}: {e:?}"),
4702 ));
4703 }
4704 }
4705 }
4706
4707 FontLoadResult { loaded, failed }
4708}
4709
4710#[allow(clippy::implicit_hasher)] pub fn resolve_and_load_fonts<T, F>(
4730 styled_dom: &StyledDom,
4731 fc_cache: &FcFontCache,
4732 already_loaded: &HashSet<FontId>,
4733 load_fn: F,
4734 platform: &azul_css::system::Platform,
4735) -> (ResolvedFontChains, FontLoadResult<T>)
4736where
4737 F: Fn(
4738 std::sync::Arc<rust_fontconfig::FontBytes>,
4739 usize,
4740 ) -> Result<T, crate::text3::cache::LayoutError>,
4741{
4742 let chains = collect_and_resolve_font_chains(styled_dom, fc_cache, platform);
4744
4745 let required_fonts = collect_font_ids_from_chains(&chains);
4747
4748 let fonts_to_load = compute_fonts_to_load(&required_fonts, already_loaded);
4750
4751 let load_result = load_fonts_from_disk(&fonts_to_load, fc_cache, load_fn);
4753
4754 (chains, load_result)
4755}
4756
4757use azul_css::props::style::scrollbar::{
4762 LayoutScrollbarWidth, ScrollbarVisibilityMode, StyleScrollbarColor,
4763};
4764
4765#[derive(Copy, Debug, Clone)]
4782pub struct ComputedScrollbarStyle {
4783 pub width_mode: LayoutScrollbarWidth,
4785 pub visual_width_px: f32,
4788 pub reserve_width_px: f32,
4791 pub thumb_color: ColorU,
4793 pub track_color: ColorU,
4795 pub button_color: ColorU,
4797 pub corner_color: ColorU,
4799 pub clip_to_container_border: bool,
4801 pub fade_delay_ms: u32,
4803 pub fade_duration_ms: u32,
4805 pub visibility: ScrollbarVisibilityMode,
4807 pub show_scroll_buttons: bool,
4810 pub scroll_button_size_px: f32,
4813 pub show_corner_rect: bool,
4815 pub thumb_color_hover: Option<ColorU>,
4817 pub thumb_color_active: Option<ColorU>,
4819 pub track_color_hover: Option<ColorU>,
4821 pub visual_width_px_hover: Option<f32>,
4823 pub visual_width_px_active: Option<f32>,
4825}
4826
4827impl Default for ComputedScrollbarStyle {
4828 fn default() -> Self {
4829 let ctx = azul_css::dynamic_selector::DynamicSelectorContext::default();
4832 let ua = azul_core::ua_css::evaluate_ua_scrollbar_css(&ctx);
4833 Self::from_ua_resolved(&ua)
4834 }
4835}
4836
4837impl ComputedScrollbarStyle {
4838 fn from_ua_resolved(ua: &azul_core::ua_css::ResolvedUaScrollbar) -> Self {
4842 let width_mode = ua.width;
4843 let visibility = ua.visibility;
4844 let fade_delay_ms = ua.fade_delay.ms;
4845 let fade_duration_ms = ua.fade_duration.ms;
4846
4847 let visual_width_px = match width_mode {
4848 LayoutScrollbarWidth::Thin => SCROLLBAR_WIDTH_THIN,
4849 LayoutScrollbarWidth::Auto => SCROLLBAR_WIDTH_AUTO,
4850 LayoutScrollbarWidth::None => 0.0,
4851 };
4852
4853 let is_overlay = visibility == ScrollbarVisibilityMode::WhenScrolling;
4855 let reserve_width_px = if is_overlay { 0.0 } else { visual_width_px };
4856 let show_scroll_buttons = !is_overlay;
4857 let scroll_button_size_px = if is_overlay { 0.0 } else { visual_width_px };
4858 let show_corner_rect = !is_overlay;
4859
4860 let (thumb_color, track_color) = match ua.color {
4861 StyleScrollbarColor::Custom(c) => (c.thumb, c.track),
4862 StyleScrollbarColor::Auto => (ColorU::TRANSPARENT, ColorU::TRANSPARENT),
4863 };
4864
4865 let thumb_hover = ColorU {
4869 r: thumb_color.r.saturating_add(THUMB_HOVER_LIGHTEN),
4870 g: thumb_color.g.saturating_add(THUMB_HOVER_LIGHTEN),
4871 b: thumb_color.b.saturating_add(THUMB_HOVER_LIGHTEN),
4872 a: thumb_color.a.saturating_add(THUMB_HOVER_ALPHA_ADD),
4873 };
4874 let thumb_active = ColorU {
4875 r: thumb_color.r.saturating_sub(THUMB_ACTIVE_DARKEN),
4876 g: thumb_color.g.saturating_sub(THUMB_ACTIVE_DARKEN),
4877 b: thumb_color.b.saturating_sub(THUMB_ACTIVE_DARKEN),
4878 a: 255,
4879 };
4880 let track_hover = ColorU {
4881 r: track_color.r,
4882 g: track_color.g,
4883 b: track_color.b,
4884 a: track_color.a.saturating_add(THUMB_HOVER_ALPHA_ADD),
4885 };
4886 let hover_width = visual_width_px + SCROLLBAR_HOVER_EXPAND_PX;
4887 let active_width = visual_width_px + SCROLLBAR_HOVER_EXPAND_PX;
4888
4889 Self {
4890 width_mode,
4891 visual_width_px,
4892 reserve_width_px,
4893 thumb_color,
4894 track_color,
4895 button_color: ColorU::TRANSPARENT,
4896 corner_color: ColorU::TRANSPARENT,
4897 clip_to_container_border: is_overlay,
4898 fade_delay_ms,
4899 fade_duration_ms,
4900 visibility,
4901 show_scroll_buttons,
4902 scroll_button_size_px,
4903 show_corner_rect,
4904 thumb_color_hover: Some(thumb_hover),
4905 thumb_color_active: Some(thumb_active),
4906 track_color_hover: Some(track_hover),
4907 visual_width_px_hover: Some(hover_width),
4908 visual_width_px_active: Some(active_width),
4909 }
4910 }
4911}
4912
4913#[allow(clippy::too_many_lines)] #[must_use] pub fn get_scrollbar_style(
4929 styled_dom: &StyledDom,
4930 node_id: NodeId,
4931 node_state: &StyledNodeState,
4932 system_style: Option<&azul_css::system::SystemStyle>,
4933) -> ComputedScrollbarStyle {
4934 let node_data = &styled_dom.node_data.as_container()[node_id];
4935
4936 let ctx = system_style.map_or_else(
4938 azul_css::dynamic_selector::DynamicSelectorContext::default,
4939 azul_css::dynamic_selector::DynamicSelectorContext::from_system_style,
4940 );
4941 let ua = azul_core::ua_css::evaluate_ua_scrollbar_css(&ctx);
4942 let result = ComputedScrollbarStyle::from_ua_resolved(&ua);
4943
4944 if node_state.is_normal() {
4946 if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
4947 if !cc.has_scrollbar_css(node_id.index()) {
4948 return result;
4949 }
4950 }
4951 }
4952 let mut result = result;
4953
4954 if let Some(track) = styled_dom
4956 .css_property_cache
4957 .ptr
4958 .get_scrollbar_track(node_data, &node_id, node_state)
4959 .and_then(|v| v.get_property())
4960 {
4961 result.track_color = extract_color_from_background(track);
4962 }
4963 if let Some(thumb) = styled_dom
4964 .css_property_cache
4965 .ptr
4966 .get_scrollbar_thumb(node_data, &node_id, node_state)
4967 .and_then(|v| v.get_property())
4968 {
4969 result.thumb_color = extract_color_from_background(thumb);
4970 }
4971 if let Some(button) = styled_dom
4972 .css_property_cache
4973 .ptr
4974 .get_scrollbar_button(node_data, &node_id, node_state)
4975 .and_then(|v| v.get_property())
4976 {
4977 result.button_color = extract_color_from_background(button);
4978 }
4979 if let Some(corner) = styled_dom
4980 .css_property_cache
4981 .ptr
4982 .get_scrollbar_corner(node_data, &node_id, node_state)
4983 .and_then(|v| v.get_property())
4984 {
4985 result.corner_color = extract_color_from_background(corner);
4986 }
4987
4988 if let Some(scrollbar_width) = styled_dom
4990 .css_property_cache
4991 .ptr
4992 .get_scrollbar_width(node_data, &node_id, node_state)
4993 .and_then(|v| v.get_property())
4994 {
4995 result.width_mode = *scrollbar_width;
4996 let w = match scrollbar_width {
4997 LayoutScrollbarWidth::Auto => SCROLLBAR_WIDTH_AUTO,
4998 LayoutScrollbarWidth::Thin => SCROLLBAR_WIDTH_THIN,
4999 LayoutScrollbarWidth::None => 0.0,
5000 };
5001 result.visual_width_px = w;
5002 if result.visibility != ScrollbarVisibilityMode::WhenScrolling {
5003 result.reserve_width_px = w;
5004 }
5005 }
5006
5007 if let Some(scrollbar_color) = styled_dom
5009 .css_property_cache
5010 .ptr
5011 .get_scrollbar_color(node_data, &node_id, node_state)
5012 .and_then(|v| v.get_property())
5013 {
5014 match scrollbar_color {
5015 StyleScrollbarColor::Auto => { }
5016 StyleScrollbarColor::Custom(custom) => {
5017 result.thumb_color = custom.thumb;
5018 result.track_color = custom.track;
5019 }
5020 }
5021 }
5022
5023 if let Some(vis) = styled_dom
5025 .css_property_cache
5026 .ptr
5027 .get_scrollbar_visibility(node_data, &node_id, node_state)
5028 .and_then(|v| v.get_property())
5029 {
5030 result.visibility = *vis;
5031 result.clip_to_container_border = *vis == ScrollbarVisibilityMode::WhenScrolling;
5032 let is_overlay = *vis == ScrollbarVisibilityMode::WhenScrolling;
5034 if is_overlay {
5035 result.reserve_width_px = 0.0;
5036 result.show_scroll_buttons = false;
5037 result.scroll_button_size_px = 0.0;
5038 result.show_corner_rect = false;
5039 } else {
5040 result.reserve_width_px = result.visual_width_px;
5041 }
5042 }
5043
5044 if let Some(delay) = styled_dom
5046 .css_property_cache
5047 .ptr
5048 .get_scrollbar_fade_delay(node_data, &node_id, node_state)
5049 .and_then(|v| v.get_property())
5050 {
5051 result.fade_delay_ms = delay.ms;
5052 }
5053
5054 if let Some(dur) = styled_dom
5056 .css_property_cache
5057 .ptr
5058 .get_scrollbar_fade_duration(node_data, &node_id, node_state)
5059 .and_then(|v| v.get_property())
5060 {
5061 result.fade_duration_ms = dur.ms;
5062 }
5063
5064 result
5065}
5066
5067pub fn get_scrollbar_style_cached<T: ParsedFontTrait>(
5080 ctx: &crate::solver3::LayoutContext<'_, T>,
5081 node_id: NodeId,
5082 node_state: &StyledNodeState,
5083) -> ComputedScrollbarStyle {
5084 if let Some(s) = ctx.scrollbar_style_cache.borrow().get(&node_id) {
5085 return *s;
5086 }
5087 let style = get_scrollbar_style(
5088 ctx.styled_dom,
5089 node_id,
5090 node_state,
5091 ctx.system_style.as_deref(),
5092 );
5093 ctx.scrollbar_style_cache
5094 .borrow_mut()
5095 .insert(node_id, style);
5096 style
5097}
5098
5099const fn extract_color_from_background(
5101 bg: &azul_css::props::style::background::StyleBackgroundContent,
5102) -> ColorU {
5103 use azul_css::props::style::background::StyleBackgroundContent;
5104 match bg {
5105 StyleBackgroundContent::Color(c) => *c,
5106 _ => ColorU::TRANSPARENT,
5107 }
5108}
5109
5110#[must_use] pub fn should_clip_scrollbar_to_border(
5112 styled_dom: &StyledDom,
5113 node_id: NodeId,
5114 node_state: &StyledNodeState,
5115) -> bool {
5116 let style = get_scrollbar_style(styled_dom, node_id, node_state, None);
5117 style.clip_to_container_border
5118}
5119
5120#[must_use] pub fn get_scrollbar_width_px(
5122 styled_dom: &StyledDom,
5123 node_id: NodeId,
5124 node_state: &StyledNodeState,
5125) -> f32 {
5126 let style = get_scrollbar_style(styled_dom, node_id, node_state, None);
5127 style.visual_width_px
5128}
5129
5130#[must_use] pub fn is_text_selectable(
5135 styled_dom: &StyledDom,
5136 node_id: NodeId,
5137 node_state: &StyledNodeState,
5138) -> bool {
5139 let node_data = &styled_dom.node_data.as_container()[node_id];
5140
5141 styled_dom
5142 .css_property_cache
5143 .ptr
5144 .get_user_select(node_data, &node_id, node_state)
5145 .and_then(|v| v.get_property())
5146 .is_none_or(|us| *us != StyleUserSelect::None) }
5148
5149#[must_use] pub fn is_node_contenteditable(styled_dom: &StyledDom, node_id: NodeId) -> bool {
5157 use azul_core::dom::AttributeType;
5158
5159 let node_data = &styled_dom.node_data.as_container()[node_id];
5160
5161 if node_data.is_contenteditable() {
5163 return true;
5164 }
5165
5166 node_data
5169 .attributes()
5170 .as_ref()
5171 .iter()
5172 .any(|attr| matches!(attr, AttributeType::ContentEditable(true)))
5173}
5174use azul_css::props::layout::table::{
5179 LayoutTableLayout, StyleBorderCollapse, StyleCaptionSide, StyleEmptyCells,
5180};
5181use azul_css::props::layout::text::LayoutTextJustify;
5182use azul_css::props::style::effects::StyleAspectRatio;
5183use azul_css::props::style::effects::StyleCursor;
5184use azul_css::props::style::effects::StyleObjectFit;
5185use azul_css::props::style::effects::StyleObjectPosition;
5186use azul_css::props::layout::overflow::StyleTextOverflow;
5187use azul_css::props::style::effects::StyleTextOrientation;
5188use azul_css::props::style::text::StyleHyphens;
5189use azul_css::props::style::text::StyleLineBreak;
5190use azul_css::props::style::text::StyleOverflowWrap;
5191use azul_css::props::style::text::StyleTextAlignLast;
5192use azul_css::props::style::text::StyleWordBreak;
5193
5194impl ExtractPropertyValue<LayoutTextJustify> for CssProperty {
5195 fn extract(&self) -> Option<LayoutTextJustify> {
5196 match self {
5197 Self::TextJustify(CssPropertyValue::Exact(v)) => Some(*v),
5198 _ => None,
5199 }
5200 }
5201}
5202
5203impl ExtractPropertyValue<StyleHyphens> for CssProperty {
5204 fn extract(&self) -> Option<StyleHyphens> {
5205 match self {
5206 Self::Hyphens(CssPropertyValue::Exact(v)) => Some(*v),
5207 _ => None,
5208 }
5209 }
5210}
5211
5212impl ExtractPropertyValue<StyleWordBreak> for CssProperty {
5213 fn extract(&self) -> Option<StyleWordBreak> {
5214 match self {
5215 Self::WordBreak(CssPropertyValue::Exact(v)) => Some(*v),
5216 _ => None,
5217 }
5218 }
5219}
5220
5221impl ExtractPropertyValue<StyleOverflowWrap> for CssProperty {
5222 fn extract(&self) -> Option<StyleOverflowWrap> {
5223 match self {
5224 Self::OverflowWrap(CssPropertyValue::Exact(v)) => Some(*v),
5225 _ => None,
5226 }
5227 }
5228}
5229
5230impl ExtractPropertyValue<StyleLineBreak> for CssProperty {
5231 fn extract(&self) -> Option<StyleLineBreak> {
5232 match self {
5233 Self::LineBreak(CssPropertyValue::Exact(v)) => Some(*v),
5234 _ => None,
5235 }
5236 }
5237}
5238
5239impl ExtractPropertyValue<StyleTextAlignLast> for CssProperty {
5240 fn extract(&self) -> Option<StyleTextAlignLast> {
5241 match self {
5242 Self::TextAlignLast(CssPropertyValue::Exact(v)) => Some(*v),
5243 _ => None,
5244 }
5245 }
5246}
5247
5248impl ExtractPropertyValue<StyleObjectFit> for CssProperty {
5249 fn extract(&self) -> Option<StyleObjectFit> {
5250 match self {
5251 Self::ObjectFit(CssPropertyValue::Exact(v)) => Some(*v),
5252 _ => None,
5253 }
5254 }
5255}
5256
5257impl ExtractPropertyValue<StyleTextOverflow> for CssProperty {
5258 fn extract(&self) -> Option<StyleTextOverflow> {
5259 match self {
5260 Self::TextOverflow(CssPropertyValue::Exact(v)) => Some(*v),
5261 _ => None,
5262 }
5263 }
5264}
5265
5266impl ExtractPropertyValue<StyleTextOrientation> for CssProperty {
5267 fn extract(&self) -> Option<StyleTextOrientation> {
5268 match self {
5269 Self::TextOrientation(CssPropertyValue::Exact(v)) => Some(*v),
5270 _ => None,
5271 }
5272 }
5273}
5274
5275impl ExtractPropertyValue<StyleObjectPosition> for CssProperty {
5276 fn extract(&self) -> Option<StyleObjectPosition> {
5277 match self {
5278 Self::ObjectPosition(CssPropertyValue::Exact(v)) => Some(*v),
5279 _ => None,
5280 }
5281 }
5282}
5283
5284impl ExtractPropertyValue<StyleAspectRatio> for CssProperty {
5285 fn extract(&self) -> Option<StyleAspectRatio> {
5286 match self {
5287 Self::AspectRatio(CssPropertyValue::Exact(v)) => Some(*v),
5288 _ => None,
5289 }
5290 }
5291}
5292
5293impl ExtractPropertyValue<LayoutTableLayout> for CssProperty {
5294 fn extract(&self) -> Option<LayoutTableLayout> {
5295 match self {
5296 Self::TableLayout(CssPropertyValue::Exact(v)) => Some(*v),
5297 _ => None,
5298 }
5299 }
5300}
5301
5302impl ExtractPropertyValue<StyleBorderCollapse> for CssProperty {
5303 fn extract(&self) -> Option<StyleBorderCollapse> {
5304 match self {
5305 Self::BorderCollapse(CssPropertyValue::Exact(v)) => Some(*v),
5306 _ => None,
5307 }
5308 }
5309}
5310
5311impl ExtractPropertyValue<StyleCaptionSide> for CssProperty {
5312 fn extract(&self) -> Option<StyleCaptionSide> {
5313 match self {
5314 Self::CaptionSide(CssPropertyValue::Exact(v)) => Some(*v),
5315 _ => None,
5316 }
5317 }
5318}
5319
5320impl ExtractPropertyValue<StyleEmptyCells> for CssProperty {
5321 fn extract(&self) -> Option<StyleEmptyCells> {
5322 match self {
5323 Self::EmptyCells(CssPropertyValue::Exact(v)) => Some(*v),
5324 _ => None,
5325 }
5326 }
5327}
5328
5329impl ExtractPropertyValue<StyleCursor> for CssProperty {
5330 fn extract(&self) -> Option<StyleCursor> {
5331 match self {
5332 Self::Cursor(CssPropertyValue::Exact(v)) => Some(*v),
5333 _ => None,
5334 }
5335 }
5336}
5337
5338get_css_property!(
5343 get_text_justify,
5344 get_text_justify,
5345 LayoutTextJustify,
5346 CssPropertyType::TextJustify
5347);
5348
5349get_css_property!(
5350 get_hyphens,
5351 get_hyphens,
5352 StyleHyphens,
5353 CssPropertyType::Hyphens
5354);
5355
5356get_css_property!(
5357 get_word_break,
5358 get_word_break,
5359 StyleWordBreak,
5360 CssPropertyType::WordBreak
5361);
5362
5363get_css_property!(
5364 get_overflow_wrap,
5365 get_overflow_wrap,
5366 StyleOverflowWrap,
5367 CssPropertyType::OverflowWrap
5368);
5369
5370get_css_property!(
5371 get_line_break,
5372 get_line_break,
5373 StyleLineBreak,
5374 CssPropertyType::LineBreak
5375);
5376
5377get_css_property!(
5378 get_text_align_last,
5379 get_text_align_last,
5380 StyleTextAlignLast,
5381 CssPropertyType::TextAlignLast
5382);
5383
5384get_css_property!(
5385 get_table_layout,
5386 get_table_layout,
5387 LayoutTableLayout,
5388 CssPropertyType::TableLayout
5389);
5390
5391get_css_property!(
5392 get_border_collapse,
5393 get_border_collapse,
5394 StyleBorderCollapse,
5395 CssPropertyType::BorderCollapse,
5396 compact = get_border_collapse
5397);
5398
5399get_css_property!(
5400 get_caption_side,
5401 get_caption_side,
5402 StyleCaptionSide,
5403 CssPropertyType::CaptionSide
5404);
5405
5406get_css_property!(
5407 get_empty_cells,
5408 get_empty_cells,
5409 StyleEmptyCells,
5410 CssPropertyType::EmptyCells
5411);
5412
5413get_css_property!(
5414 get_cursor_property,
5415 get_cursor,
5416 StyleCursor,
5417 CssPropertyType::Cursor
5418);
5419
5420#[must_use] pub fn get_height_value(
5426 styled_dom: &StyledDom,
5427 node_id: NodeId,
5428 node_state: &StyledNodeState,
5429) -> Option<LayoutHeight> {
5430 let node_data = &styled_dom.node_data.as_container()[node_id];
5431 styled_dom
5432 .css_property_cache
5433 .ptr
5434 .get_height(node_data, &node_id, node_state)
5435 .and_then(|v| v.get_property())
5436 .cloned()
5437}
5438
5439#[must_use] pub fn get_shape_inside(
5441 styled_dom: &StyledDom,
5442 node_id: NodeId,
5443 node_state: &StyledNodeState,
5444) -> Option<azul_css::props::layout::shape::ShapeInside> {
5445 let node_data = &styled_dom.node_data.as_container()[node_id];
5446 styled_dom
5447 .css_property_cache
5448 .ptr
5449 .get_shape_inside(node_data, &node_id, node_state)
5450 .and_then(|v| v.get_property())
5451 .cloned()
5452}
5453
5454#[must_use] pub fn get_shape_outside(
5456 styled_dom: &StyledDom,
5457 node_id: NodeId,
5458 node_state: &StyledNodeState,
5459) -> Option<azul_css::props::layout::shape::ShapeOutside> {
5460 let node_data = &styled_dom.node_data.as_container()[node_id];
5461 styled_dom
5462 .css_property_cache
5463 .ptr
5464 .get_shape_outside(node_data, &node_id, node_state)
5465 .and_then(|v| v.get_property())
5466 .cloned()
5467}
5468
5469#[must_use] pub fn get_line_height_value(
5471 styled_dom: &StyledDom,
5472 node_id: NodeId,
5473 node_state: &StyledNodeState,
5474) -> Option<azul_css::props::style::text::StyleLineHeight> {
5475 let node_data = &styled_dom.node_data.as_container()[node_id];
5476 styled_dom
5477 .css_property_cache
5478 .ptr
5479 .get_line_height(node_data, &node_id, node_state)
5480 .and_then(|v| v.get_property())
5481 .copied()
5482}
5483
5484#[must_use] pub fn get_text_indent_value(
5486 styled_dom: &StyledDom,
5487 node_id: NodeId,
5488 node_state: &StyledNodeState,
5489) -> Option<azul_css::props::style::text::StyleTextIndent> {
5490 let node_data = &styled_dom.node_data.as_container()[node_id];
5491 styled_dom
5492 .css_property_cache
5493 .ptr
5494 .get_text_indent(node_data, &node_id, node_state)
5495 .and_then(|v| v.get_property())
5496 .copied()
5497}
5498
5499#[must_use] pub fn get_column_count(
5501 styled_dom: &StyledDom,
5502 node_id: NodeId,
5503 node_state: &StyledNodeState,
5504) -> Option<azul_css::props::layout::column::ColumnCount> {
5505 let node_data = &styled_dom.node_data.as_container()[node_id];
5506 styled_dom
5507 .css_property_cache
5508 .ptr
5509 .get_column_count(node_data, &node_id, node_state)
5510 .and_then(|v| v.get_property())
5511 .copied()
5512}
5513
5514#[must_use] pub fn get_initial_letter(
5516 styled_dom: &StyledDom,
5517 node_id: NodeId,
5518 node_state: &StyledNodeState,
5519) -> Option<azul_css::props::style::text::StyleInitialLetter> {
5520 let node_data = &styled_dom.node_data.as_container()[node_id];
5521 styled_dom
5522 .css_property_cache
5523 .ptr
5524 .get_initial_letter(node_data, &node_id, node_state)
5525 .and_then(|v| v.get_property())
5526 .copied()
5527}
5528
5529#[must_use] pub fn get_line_clamp(
5531 styled_dom: &StyledDom,
5532 node_id: NodeId,
5533 node_state: &StyledNodeState,
5534) -> Option<azul_css::props::style::text::StyleLineClamp> {
5535 let node_data = &styled_dom.node_data.as_container()[node_id];
5536 styled_dom
5537 .css_property_cache
5538 .ptr
5539 .get_line_clamp(node_data, &node_id, node_state)
5540 .and_then(|v| v.get_property())
5541 .copied()
5542}
5543
5544#[must_use] pub fn get_hanging_punctuation(
5546 styled_dom: &StyledDom,
5547 node_id: NodeId,
5548 node_state: &StyledNodeState,
5549) -> Option<azul_css::props::style::text::StyleHangingPunctuation> {
5550 let node_data = &styled_dom.node_data.as_container()[node_id];
5551 styled_dom
5552 .css_property_cache
5553 .ptr
5554 .get_hanging_punctuation(node_data, &node_id, node_state)
5555 .and_then(|v| v.get_property())
5556 .copied()
5557}
5558
5559#[must_use] pub fn get_text_combine_upright(
5561 styled_dom: &StyledDom,
5562 node_id: NodeId,
5563 node_state: &StyledNodeState,
5564) -> Option<azul_css::props::style::text::StyleTextCombineUpright> {
5565 let node_data = &styled_dom.node_data.as_container()[node_id];
5566 styled_dom
5567 .css_property_cache
5568 .ptr
5569 .get_text_combine_upright(node_data, &node_id, node_state)
5570 .and_then(|v| v.get_property())
5571 .copied()
5572}
5573
5574#[must_use] pub fn get_exclusion_margin(
5576 styled_dom: &StyledDom,
5577 node_id: NodeId,
5578 node_state: &StyledNodeState,
5579) -> f32 {
5580 let node_data = &styled_dom.node_data.as_container()[node_id];
5581 styled_dom
5582 .css_property_cache
5583 .ptr
5584 .get_exclusion_margin(node_data, &node_id, node_state)
5585 .and_then(|v| v.get_property())
5586 .map_or(0.0, |v| v.inner.get())
5587}
5588
5589#[must_use] pub fn get_hyphenation_language(
5591 styled_dom: &StyledDom,
5592 node_id: NodeId,
5593 node_state: &StyledNodeState,
5594) -> Option<azul_css::props::style::exclusion::StyleHyphenationLanguage> {
5595 let node_data = &styled_dom.node_data.as_container()[node_id];
5596 styled_dom
5597 .css_property_cache
5598 .ptr
5599 .get_hyphenation_language(node_data, &node_id, node_state)
5600 .and_then(|v| v.get_property())
5601 .cloned()
5602}
5603
5604#[must_use] pub fn get_border_spacing(
5606 styled_dom: &StyledDom,
5607 node_id: NodeId,
5608 node_state: &StyledNodeState,
5609) -> azul_css::props::layout::table::LayoutBorderSpacing {
5610 use azul_css::props::basic::pixel::PixelValue;
5611
5612 if node_state.is_normal() {
5614 if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
5615 let h_raw = cc.get_border_spacing_h_raw(node_id.index());
5616 let v_raw = cc.get_border_spacing_v_raw(node_id.index());
5617 if h_raw < azul_css::compact_cache::I16_SENTINEL_THRESHOLD
5620 && v_raw < azul_css::compact_cache::I16_SENTINEL_THRESHOLD
5621 {
5622 return azul_css::props::layout::table::LayoutBorderSpacing {
5623 horizontal: PixelValue::px(f32::from(h_raw) / 10.0),
5624 vertical: PixelValue::px(f32::from(v_raw) / 10.0),
5625 };
5626 }
5627 }
5628 }
5629
5630 let node_data = &styled_dom.node_data.as_container()[node_id];
5632 styled_dom
5633 .css_property_cache
5634 .ptr
5635 .get_border_spacing(node_data, &node_id, node_state)
5636 .and_then(|v| v.get_property())
5637 .copied()
5638 .unwrap_or_default()
5639}
5640
5641#[must_use] pub fn get_opacity(styled_dom: &StyledDom, node_id: NodeId, node_state: &StyledNodeState) -> f32 {
5647 if node_state.is_normal() {
5649 if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
5650 let raw = cc.get_opacity_raw(node_id.index());
5651 if raw == azul_css::compact_cache::OPACITY_SENTINEL {
5652 return 1.0;
5653 }
5654 return f32::from(raw) / 254.0;
5655 }
5656 }
5657 let node_data = &styled_dom.node_data.as_container()[node_id];
5659 styled_dom
5660 .css_property_cache
5661 .ptr
5662 .get_opacity(node_data, &node_id, node_state)
5663 .and_then(|v| v.get_property())
5664 .map_or(1.0, |v| v.inner.normalized())
5665}
5666
5667#[must_use] pub fn get_filter(
5669 styled_dom: &StyledDom,
5670 node_id: NodeId,
5671 node_state: &StyledNodeState,
5672) -> Option<azul_css::props::style::filter::StyleFilterVec> {
5673 if node_state.is_normal() {
5674 if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
5675 if !cc.has_filter(node_id.index()) {
5676 return None;
5677 }
5678 }
5679 }
5680 let node_data = &styled_dom.node_data.as_container()[node_id];
5681 styled_dom
5682 .css_property_cache
5683 .ptr
5684 .get_filter(node_data, &node_id, node_state)
5685 .and_then(|v| v.get_property())
5686 .cloned()
5687}
5688
5689#[must_use] pub fn get_backdrop_filter(
5691 styled_dom: &StyledDom,
5692 node_id: NodeId,
5693 node_state: &StyledNodeState,
5694) -> Option<azul_css::props::style::filter::StyleFilterVec> {
5695 if node_state.is_normal() {
5696 if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
5697 if !cc.has_backdrop_filter(node_id.index()) {
5698 return None;
5699 }
5700 }
5701 }
5702 let node_data = &styled_dom.node_data.as_container()[node_id];
5703 styled_dom
5704 .css_property_cache
5705 .ptr
5706 .get_backdrop_filter(node_data, &node_id, node_state)
5707 .and_then(|v| v.get_property())
5708 .cloned()
5709}
5710
5711#[inline]
5714fn box_shadow_fast_bail(
5715 styled_dom: &StyledDom,
5716 node_id: NodeId,
5717 node_state: &StyledNodeState,
5718) -> bool {
5719 if !node_state.is_normal() {
5720 return false;
5721 }
5722 if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
5723 return !cc.has_box_shadow(node_id.index());
5724 }
5725 false
5726}
5727
5728#[must_use] pub fn get_box_shadow_left(
5730 styled_dom: &StyledDom,
5731 node_id: NodeId,
5732 node_state: &StyledNodeState,
5733) -> Option<azul_css::props::style::box_shadow::StyleBoxShadow> {
5734 if box_shadow_fast_bail(styled_dom, node_id, node_state) {
5735 return None;
5736 }
5737 let node_data = &styled_dom.node_data.as_container()[node_id];
5738 styled_dom
5739 .css_property_cache
5740 .ptr
5741 .get_box_shadow_left(node_data, &node_id, node_state)
5742 .and_then(|v| v.get_property())
5743 .map(|v| (**v))
5744}
5745
5746#[must_use] pub fn get_box_shadow_right(
5748 styled_dom: &StyledDom,
5749 node_id: NodeId,
5750 node_state: &StyledNodeState,
5751) -> Option<azul_css::props::style::box_shadow::StyleBoxShadow> {
5752 if box_shadow_fast_bail(styled_dom, node_id, node_state) {
5753 return None;
5754 }
5755 let node_data = &styled_dom.node_data.as_container()[node_id];
5756 styled_dom
5757 .css_property_cache
5758 .ptr
5759 .get_box_shadow_right(node_data, &node_id, node_state)
5760 .and_then(|v| v.get_property())
5761 .map(|v| (**v))
5762}
5763
5764#[must_use] pub fn get_box_shadow_top(
5766 styled_dom: &StyledDom,
5767 node_id: NodeId,
5768 node_state: &StyledNodeState,
5769) -> Option<azul_css::props::style::box_shadow::StyleBoxShadow> {
5770 if box_shadow_fast_bail(styled_dom, node_id, node_state) {
5771 return None;
5772 }
5773 let node_data = &styled_dom.node_data.as_container()[node_id];
5774 styled_dom
5775 .css_property_cache
5776 .ptr
5777 .get_box_shadow_top(node_data, &node_id, node_state)
5778 .and_then(|v| v.get_property())
5779 .map(|v| (**v))
5780}
5781
5782#[must_use] pub fn get_box_shadow_bottom(
5784 styled_dom: &StyledDom,
5785 node_id: NodeId,
5786 node_state: &StyledNodeState,
5787) -> Option<azul_css::props::style::box_shadow::StyleBoxShadow> {
5788 if box_shadow_fast_bail(styled_dom, node_id, node_state) {
5789 return None;
5790 }
5791 let node_data = &styled_dom.node_data.as_container()[node_id];
5792 styled_dom
5793 .css_property_cache
5794 .ptr
5795 .get_box_shadow_bottom(node_data, &node_id, node_state)
5796 .and_then(|v| v.get_property())
5797 .map(|v| (**v))
5798}
5799
5800#[must_use] pub fn get_text_shadow(
5802 styled_dom: &StyledDom,
5803 node_id: NodeId,
5804 node_state: &StyledNodeState,
5805) -> Option<azul_css::props::style::box_shadow::StyleBoxShadow> {
5806 if node_state.is_normal() {
5807 if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
5808 if !cc.has_text_shadow(node_id.index()) {
5809 return None;
5810 }
5811 }
5812 }
5813 let node_data = &styled_dom.node_data.as_container()[node_id];
5814 styled_dom
5815 .css_property_cache
5816 .ptr
5817 .get_text_shadow(node_data, &node_id, node_state)
5818 .and_then(|v| v.get_property())
5819 .map(|v| (**v))
5820}
5821
5822#[must_use] pub fn get_transform(
5829 styled_dom: &StyledDom,
5830 node_id: NodeId,
5831 node_state: &StyledNodeState,
5832) -> Option<azul_css::props::style::transform::StyleTransformVec> {
5833 if node_state.is_normal() {
5835 if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
5836 if !cc.has_transform(node_id.index()) {
5837 return None;
5838 }
5839 }
5841 }
5842 let node_data = &styled_dom.node_data.as_container()[node_id];
5843 styled_dom
5844 .css_property_cache
5845 .ptr
5846 .get_transform(node_data, &node_id, node_state)
5847 .and_then(|v| v.get_property())
5848 .cloned()
5849}
5850
5851#[must_use] pub fn get_counter_reset(
5853 styled_dom: &StyledDom,
5854 node_id: NodeId,
5855 node_state: &StyledNodeState,
5856) -> Option<azul_css::props::style::content::CounterReset> {
5857 let node_data = &styled_dom.node_data.as_container()[node_id];
5858 styled_dom
5859 .css_property_cache
5860 .ptr
5861 .get_counter_reset(node_data, &node_id, node_state)
5862 .and_then(|v| v.get_property())
5863 .cloned()
5864}
5865
5866#[must_use] pub fn get_counter_increment(
5868 styled_dom: &StyledDom,
5869 node_id: NodeId,
5870 node_state: &StyledNodeState,
5871) -> Option<azul_css::props::style::content::CounterIncrement> {
5872 let node_data = &styled_dom.node_data.as_container()[node_id];
5873 styled_dom
5874 .css_property_cache
5875 .ptr
5876 .get_counter_increment(node_data, &node_id, node_state)
5877 .and_then(|v| v.get_property())
5878 .cloned()
5879}
5880
5881#[must_use] pub fn is_node_contenteditable_inherited(styled_dom: &StyledDom, node_id: NodeId) -> bool {
5907 use azul_core::dom::AttributeType;
5908
5909 let node_data_container = styled_dom.node_data.as_container();
5910 let hierarchy = styled_dom.node_hierarchy.as_container();
5911
5912 let mut current_node_id = Some(node_id);
5913
5914 while let Some(nid) = current_node_id {
5915 let node_data = &node_data_container[nid];
5916
5917 if node_data.is_contenteditable() {
5920 return true;
5921 }
5922
5923 for attr in node_data.attributes().as_ref() {
5926 if let AttributeType::ContentEditable(is_editable) = attr {
5927 return *is_editable;
5930 }
5931 }
5932
5933 current_node_id = hierarchy.get(nid).and_then(azul_core::styled_dom::NodeHierarchyItem::parent_id);
5935 }
5936
5937 false
5939}
5940
5941#[must_use] pub fn find_contenteditable_ancestor(styled_dom: &StyledDom, node_id: NodeId) -> Option<NodeId> {
5951 use azul_core::dom::AttributeType;
5952
5953 let node_data_container = styled_dom.node_data.as_container();
5954 let hierarchy = styled_dom.node_hierarchy.as_container();
5955
5956 let mut current_node_id = Some(node_id);
5957
5958 while let Some(nid) = current_node_id {
5959 let node_data = &node_data_container[nid];
5960
5961 if node_data.is_contenteditable() {
5963 return Some(nid);
5964 }
5965
5966 for attr in node_data.attributes().as_ref() {
5968 if let AttributeType::ContentEditable(is_editable) = attr {
5969 if *is_editable {
5970 return Some(nid);
5971 }
5972 return None;
5974 }
5975 }
5976
5977 current_node_id = hierarchy.get(nid).and_then(azul_core::styled_dom::NodeHierarchyItem::parent_id);
5979 }
5980
5981 None
5982}
5983
5984macro_rules! get_css_property_value {
5992 ($fn_name:ident, $cache_method:ident, $ret_type:ty) => {
5993 #[must_use] pub fn $fn_name(
5994 styled_dom: &StyledDom,
5995 node_id: NodeId,
5996 node_state: &StyledNodeState,
5997 ) -> Option<$ret_type> {
5998 let node_data = &styled_dom.node_data.as_container()[node_id];
5999 styled_dom
6000 .css_property_cache
6001 .ptr
6002 .$cache_method(node_data, &node_id, node_state)
6003 .cloned()
6004 }
6005 };
6006}
6007
6008get_css_property_value!(
6010 get_flex_direction_prop,
6011 get_flex_direction,
6012 LayoutFlexDirectionValue
6013);
6014get_css_property_value!(get_flex_wrap_prop, get_flex_wrap, LayoutFlexWrapValue);
6015get_css_property_value!(get_flex_grow_prop, get_flex_grow, LayoutFlexGrowValue);
6016get_css_property_value!(get_flex_shrink_prop, get_flex_shrink, LayoutFlexShrinkValue);
6017get_css_property_value!(get_flex_basis_prop, get_flex_basis, LayoutFlexBasisValue);
6018
6019get_css_property_value!(get_align_items_prop, get_align_items, LayoutAlignItemsValue);
6021get_css_property_value!(get_align_self_prop, get_align_self, LayoutAlignSelfValue);
6022get_css_property_value!(
6023 get_align_content_prop,
6024 get_align_content,
6025 LayoutAlignContentValue
6026);
6027get_css_property_value!(
6028 get_justify_content_prop,
6029 get_justify_content,
6030 LayoutJustifyContentValue
6031);
6032get_css_property_value!(
6033 get_justify_items_prop,
6034 get_justify_items,
6035 LayoutJustifyItemsValue
6036);
6037get_css_property_value!(
6038 get_justify_self_prop,
6039 get_justify_self,
6040 LayoutJustifySelfValue
6041);
6042
6043get_css_property_value!(get_gap_prop, get_gap, LayoutGapValue);
6045
6046get_css_property_value!(
6048 get_grid_template_rows_prop,
6049 get_grid_template_rows,
6050 LayoutGridTemplateRowsValue
6051);
6052get_css_property_value!(
6053 get_grid_template_columns_prop,
6054 get_grid_template_columns,
6055 LayoutGridTemplateColumnsValue
6056);
6057get_css_property_value!(
6058 get_grid_auto_rows_prop,
6059 get_grid_auto_rows,
6060 LayoutGridAutoRowsValue
6061);
6062get_css_property_value!(
6063 get_grid_auto_columns_prop,
6064 get_grid_auto_columns,
6065 LayoutGridAutoColumnsValue
6066);
6067get_css_property_value!(
6068 get_grid_auto_flow_prop,
6069 get_grid_auto_flow,
6070 LayoutGridAutoFlowValue
6071);
6072get_css_property_value!(get_grid_column_prop, get_grid_column, LayoutGridColumnValue);
6073get_css_property_value!(get_grid_row_prop, get_grid_row, LayoutGridRowValue);
6074
6075#[must_use] pub fn get_grid_template_areas_prop(
6080 styled_dom: &StyledDom,
6081 node_id: NodeId,
6082 node_state: &StyledNodeState,
6083) -> Option<GridTemplateAreas> {
6084 let node_data = &styled_dom.node_data.as_container()[node_id];
6085 styled_dom
6086 .css_property_cache
6087 .ptr
6088 .get_property(
6089 node_data,
6090 &node_id,
6091 node_state,
6092 &CssPropertyType::GridTemplateAreas,
6093 )
6094 .and_then(|p| {
6095 if let CssProperty::GridTemplateAreas(v) = p {
6096 v.get_property().cloned()
6097 } else {
6098 None
6099 }
6100 })
6101}
6102
6103#[must_use] pub fn get_clip_path(
6109 styled_dom: &StyledDom,
6110 node_id: NodeId,
6111 node_state: &StyledNodeState,
6112) -> Option<azul_css::props::layout::shape::ClipPath> {
6113 if node_state.is_normal() {
6115 if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
6116 if !cc.has_clip_path(node_id.index()) {
6117 return None;
6118 }
6119 }
6120 }
6121 let node_data = &styled_dom.node_data.as_container()[node_id];
6122 styled_dom
6123 .css_property_cache
6124 .ptr
6125 .get_clip_path(node_data, &node_id, node_state)
6126 .and_then(|v| v.get_property())
6127 .cloned()
6128}
6129
6130#[cfg(test)]
6131#[allow(clippy::float_cmp, clippy::too_many_lines)]
6132mod autotest_generated {
6133 use azul_core::{dom::Dom, ua_css::ResolvedUaScrollbar};
6134 use azul_css::{
6135 css::Css,
6136 props::style::{
6137 background::StyleBackgroundContent,
6138 scrollbar::{ScrollbarColorCustom, ScrollbarFadeDelay, ScrollbarFadeDuration},
6139 },
6140 };
6141 use rust_fontconfig::{CssFallbackGroup, FontMatch};
6142
6143 use super::*;
6144
6145 const ALL_OVERFLOW: [LayoutOverflow; 5] = [
6151 LayoutOverflow::Scroll,
6152 LayoutOverflow::Auto,
6153 LayoutOverflow::Hidden,
6154 LayoutOverflow::Visible,
6155 LayoutOverflow::Clip,
6156 ];
6157
6158 const ALL_DISPLAY: [LayoutDisplay; 23] = [
6160 LayoutDisplay::None,
6161 LayoutDisplay::Block,
6162 LayoutDisplay::Inline,
6163 LayoutDisplay::InlineBlock,
6164 LayoutDisplay::Flex,
6165 LayoutDisplay::InlineFlex,
6166 LayoutDisplay::Table,
6167 LayoutDisplay::InlineTable,
6168 LayoutDisplay::TableRowGroup,
6169 LayoutDisplay::TableHeaderGroup,
6170 LayoutDisplay::TableFooterGroup,
6171 LayoutDisplay::TableRow,
6172 LayoutDisplay::TableColumnGroup,
6173 LayoutDisplay::TableColumn,
6174 LayoutDisplay::TableCell,
6175 LayoutDisplay::TableCaption,
6176 LayoutDisplay::FlowRoot,
6177 LayoutDisplay::ListItem,
6178 LayoutDisplay::RunIn,
6179 LayoutDisplay::Marker,
6180 LayoutDisplay::Grid,
6181 LayoutDisplay::InlineGrid,
6182 LayoutDisplay::Contents,
6183 ];
6184
6185 const ALL_PAGE_BREAK: [PageBreak; 12] = [
6187 PageBreak::Auto,
6188 PageBreak::Avoid,
6189 PageBreak::Always,
6190 PageBreak::All,
6191 PageBreak::Page,
6192 PageBreak::AvoidPage,
6193 PageBreak::Left,
6194 PageBreak::Right,
6195 PageBreak::Recto,
6196 PageBreak::Verso,
6197 PageBreak::Column,
6198 PageBreak::AvoidColumn,
6199 ];
6200
6201 const ALL_BREAK_INSIDE: [BreakInside; 4] = [
6203 BreakInside::Auto,
6204 BreakInside::Avoid,
6205 BreakInside::AvoidPage,
6206 BreakInside::AvoidColumn,
6207 ];
6208
6209 const ALL_SCROLLBAR_WIDTH: [LayoutScrollbarWidth; 3] = [
6211 LayoutScrollbarWidth::Auto,
6212 LayoutScrollbarWidth::Thin,
6213 LayoutScrollbarWidth::None,
6214 ];
6215
6216 const ALL_VISIBILITY: [ScrollbarVisibilityMode; 3] = [
6218 ScrollbarVisibilityMode::Always,
6219 ScrollbarVisibilityMode::WhenScrolling,
6220 ScrollbarVisibilityMode::Auto,
6221 ];
6222
6223 fn parse(css: &str) -> Css {
6224 azul_css::parser2::new_from_str(css).0
6225 }
6226
6227 fn body_with_divs(n: usize, css: &str) -> StyledDom {
6230 let children: Vec<Dom> = (0..n).map(|_| Dom::create_div()).collect();
6231 let mut dom = Dom::create_body().with_children(children.into());
6232 StyledDom::create(&mut dom, parse(css))
6233 }
6234
6235 fn body_with_text(text: &str) -> StyledDom {
6237 let mut dom = Dom::create_body().with_children(vec![Dom::create_text(text)].into());
6238 StyledDom::create(&mut dom, Css::empty())
6239 }
6240
6241 fn normal() -> StyledNodeState {
6242 StyledNodeState::default()
6243 }
6244
6245 fn hovered() -> StyledNodeState {
6248 StyledNodeState {
6249 hover: true,
6250 ..StyledNodeState::default()
6251 }
6252 }
6253
6254 fn state_of(sd: &StyledDom, id: NodeId) -> StyledNodeState {
6255 sd.get_styled_node_state(&id)
6256 }
6257
6258 fn empty_chains() -> ResolvedFontChains {
6259 ResolvedFontChains {
6260 chains: HashMap::new(),
6261 ..Default::default()
6262 }
6263 }
6264
6265 fn chain_key(family: &str) -> FontChainKey {
6266 FontChainKey {
6267 font_families: vec![family.to_string()],
6268 weight: FcWeight::Normal,
6269 italic: false,
6270 oblique: false,
6271 }
6272 }
6273
6274 fn font_match(id: u128, ranges: &[(u32, u32)]) -> FontMatch {
6276 FontMatch {
6277 id: FontId(id),
6278 unicode_ranges: ranges
6279 .iter()
6280 .map(|&(start, end)| UnicodeRange { start, end })
6281 .collect(),
6282 fallbacks: Vec::new(),
6283 }
6284 }
6285
6286 fn chain_with(groups: Vec<CssFallbackGroup>, unicode: Vec<FontMatch>) -> FontFallbackChain {
6287 FontFallbackChain {
6288 css_fallbacks: groups,
6289 unicode_fallbacks: unicode,
6290 original_stack: Vec::new(),
6291 }
6292 }
6293
6294 fn bare_layout_node(scrollbar_info: Option<ScrollbarRequirements>) -> LayoutNode {
6296 use azul_core::{diff::NodeDataFingerprint, dom::FormattingContext};
6297
6298 use crate::solver3::{
6299 geometry::{BoxProps, UnresolvedBoxProps},
6300 layout_tree::{ComputedLayoutStyle, DirtyFlag, SubtreeHash},
6301 };
6302
6303 LayoutNode {
6304 box_props: BoxProps::default(),
6305 dom_node_id: None,
6306 children: Vec::new(),
6307 used_size: None,
6308 formatting_context: FormattingContext::Inline,
6309 parent: None,
6310 intrinsic_sizes: None,
6311 baseline: None,
6312 inline_layout_result: None,
6313 scrollbar_info,
6314 relative_position: None,
6315 overflow_content_size: None,
6316 taffy_cache: taffy::Cache::new(),
6317 computed_style: ComputedLayoutStyle::default(),
6318 pseudo_element: None,
6319 escaped_top_margin: None,
6320 escaped_bottom_margin: None,
6321 parent_formatting_context: None,
6322 ifc_membership: None,
6323 containing_block_index: None,
6324 anonymous_type: None,
6325 node_data_fingerprint: NodeDataFingerprint::default(),
6326 subtree_hash: SubtreeHash(0),
6327 dirty_flag: DirtyFlag::Layout,
6328 unresolved_box_props: UnresolvedBoxProps::default(),
6329 ifc_id: None,
6330 }
6331 }
6332
6333 #[test]
6338 fn multivalue_default_is_auto() {
6339 let v: MultiValue<i32> = MultiValue::default();
6340 assert!(v.is_auto());
6341 assert!(!v.is_exact());
6342 }
6343
6344 #[test]
6345 fn multivalue_is_auto_and_is_exact_are_mutually_exclusive() {
6346 let cases: [MultiValue<i32>; 4] = [
6347 MultiValue::Auto,
6348 MultiValue::Initial,
6349 MultiValue::Inherit,
6350 MultiValue::Exact(7),
6351 ];
6352 for v in cases {
6353 assert!(
6354 !(v.is_auto() && v.is_exact()),
6355 "a value cannot be both Auto and Exact: {v:?}"
6356 );
6357 }
6358 assert!(MultiValue::<i32>::Auto.is_auto());
6359 assert!(!MultiValue::<i32>::Initial.is_auto());
6360 assert!(!MultiValue::<i32>::Inherit.is_auto());
6361 assert!(!MultiValue::Exact(7).is_auto());
6362
6363 assert!(MultiValue::Exact(7).is_exact());
6364 assert!(!MultiValue::<i32>::Auto.is_exact());
6365 assert!(!MultiValue::<i32>::Initial.is_exact());
6366 assert!(!MultiValue::<i32>::Inherit.is_exact());
6367 }
6368
6369 #[test]
6370 fn multivalue_exact_returns_some_only_for_the_exact_variant() {
6371 assert_eq!(MultiValue::Exact(42_i32).exact(), Some(42));
6372 assert_eq!(MultiValue::<i32>::Auto.exact(), None);
6373 assert_eq!(MultiValue::<i32>::Initial.exact(), None);
6374 assert_eq!(MultiValue::<i32>::Inherit.exact(), None);
6375 }
6376
6377 #[test]
6378 fn multivalue_exact_round_trips_extreme_payloads() {
6379 for probe in [i32::MIN, -1, 0, 1, i32::MAX] {
6381 assert_eq!(MultiValue::Exact(probe).exact(), Some(probe));
6382 }
6383 let nan = MultiValue::Exact(f32::NAN).exact().unwrap();
6385 assert!(nan.is_nan());
6386 assert_eq!(MultiValue::Exact(f32::INFINITY).exact(), Some(f32::INFINITY));
6387 assert_eq!(
6388 MultiValue::Exact(f32::NEG_INFINITY).exact(),
6389 Some(f32::NEG_INFINITY)
6390 );
6391 }
6392
6393 #[test]
6394 fn multivalue_unwrap_or_uses_the_default_for_every_non_exact_variant() {
6395 assert_eq!(MultiValue::Exact(5_i32).unwrap_or(99), 5);
6396 assert_eq!(MultiValue::<i32>::Auto.unwrap_or(99), 99);
6397 assert_eq!(MultiValue::<i32>::Initial.unwrap_or(99), 99);
6398 assert_eq!(MultiValue::<i32>::Inherit.unwrap_or(99), 99);
6399 assert!(MultiValue::<f32>::Auto.unwrap_or(f32::NAN).is_nan());
6401 }
6402
6403 #[test]
6404 fn multivalue_unwrap_or_default_falls_back_to_t_default() {
6405 assert_eq!(MultiValue::Exact(5_i32).unwrap_or_default(), 5);
6406 assert_eq!(MultiValue::<i32>::Auto.unwrap_or_default(), 0);
6407 assert_eq!(MultiValue::<i32>::Initial.unwrap_or_default(), 0);
6408 assert_eq!(MultiValue::<i32>::Inherit.unwrap_or_default(), 0);
6409 assert_eq!(
6411 MultiValue::<LayoutOverflow>::Inherit.unwrap_or_default(),
6412 LayoutOverflow::Visible
6413 );
6414 }
6415
6416 #[test]
6417 fn multivalue_map_transforms_exact_and_preserves_the_keyword_variants() {
6418 assert_eq!(MultiValue::Exact(2_i32).map(|v| v * 2), MultiValue::Exact(4));
6419 assert_eq!(MultiValue::<i32>::Auto.map(|v| v * 2), MultiValue::Auto);
6420 assert_eq!(
6421 MultiValue::<i32>::Initial.map(|v| v * 2),
6422 MultiValue::Initial
6423 );
6424 assert_eq!(
6425 MultiValue::<i32>::Inherit.map(|v| v * 2),
6426 MultiValue::Inherit
6427 );
6428 }
6429
6430 #[test]
6431 fn multivalue_map_never_invokes_the_closure_for_keyword_variants() {
6432 let auto: MultiValue<i32> = MultiValue::Auto;
6434 let _ = auto.map(|_| -> i32 { panic!("map() called f() on MultiValue::Auto") });
6435 let initial: MultiValue<i32> = MultiValue::Initial;
6436 let _ = initial.map(|_| -> i32 { panic!("map() called f() on MultiValue::Initial") });
6437 let inherit: MultiValue<i32> = MultiValue::Inherit;
6438 let _ = inherit.map(|_| -> i32 { panic!("map() called f() on MultiValue::Inherit") });
6439 }
6440
6441 #[test]
6442 fn multivalue_map_can_change_the_payload_type() {
6443 let mapped: MultiValue<usize> = MultiValue::Exact("hello").map(str::len);
6444 assert_eq!(mapped, MultiValue::Exact(5));
6445 let abs: MultiValue<i32> = MultiValue::Exact(i32::MIN).map(i32::wrapping_abs);
6448 assert_eq!(abs, MultiValue::Exact(i32::MIN));
6449 }
6450
6451 #[test]
6456 fn overflow_predicates_match_the_spec_for_every_exact_variant() {
6457 for o in ALL_OVERFLOW {
6458 let v = MultiValue::Exact(o);
6459 assert_eq!(
6460 v.is_clipped(),
6461 o != LayoutOverflow::Visible,
6462 "is_clipped is every value except Visible ({o:?})"
6463 );
6464 assert_eq!(
6465 v.is_scroll(),
6466 matches!(o, LayoutOverflow::Scroll | LayoutOverflow::Auto),
6467 "is_scroll ({o:?})"
6468 );
6469 assert_eq!(
6470 v.is_auto_overflow(),
6471 o == LayoutOverflow::Auto,
6472 "is_auto_overflow ({o:?})"
6473 );
6474 assert_eq!(
6475 v.is_hidden(),
6476 o == LayoutOverflow::Hidden,
6477 "is_hidden ({o:?})"
6478 );
6479 assert_eq!(
6480 v.is_hidden_or_clip(),
6481 matches!(o, LayoutOverflow::Hidden | LayoutOverflow::Clip),
6482 "is_hidden_or_clip ({o:?})"
6483 );
6484 assert_eq!(
6485 v.is_scroll_explicit(),
6486 o == LayoutOverflow::Scroll,
6487 "is_scroll_explicit ({o:?})"
6488 );
6489 assert_eq!(v.is_clip(), o == LayoutOverflow::Clip, "is_clip ({o:?})");
6490 assert_eq!(
6491 v.is_visible_or_clip(),
6492 matches!(o, LayoutOverflow::Visible | LayoutOverflow::Clip),
6493 "is_visible_or_clip ({o:?})"
6494 );
6495 assert_eq!(
6496 v.establishes_bfc(),
6497 matches!(
6498 o,
6499 LayoutOverflow::Hidden | LayoutOverflow::Scroll | LayoutOverflow::Auto
6500 ),
6501 "establishes_bfc ({o:?})"
6502 );
6503 }
6504 }
6505
6506 #[test]
6507 fn overflow_predicates_are_false_for_every_keyword_variant() {
6508 let keywords: [MultiValue<LayoutOverflow>; 3] = [
6512 MultiValue::Auto,
6513 MultiValue::Initial,
6514 MultiValue::Inherit,
6515 ];
6516 for v in keywords {
6517 assert!(!v.is_clipped(), "{v:?}");
6518 assert!(!v.is_scroll(), "{v:?}");
6519 assert!(!v.is_auto_overflow(), "{v:?}");
6520 assert!(!v.is_hidden(), "{v:?}");
6521 assert!(!v.is_hidden_or_clip(), "{v:?}");
6522 assert!(!v.is_scroll_explicit(), "{v:?}");
6523 assert!(!v.is_clip(), "{v:?}");
6524 assert!(!v.is_visible_or_clip(), "{v:?}");
6525 assert!(!v.establishes_bfc(), "{v:?}");
6527 }
6528 }
6529
6530 #[test]
6531 fn overflow_scroll_implies_clipped_and_clip_implies_hidden_or_clip() {
6532 for o in ALL_OVERFLOW {
6533 let v = MultiValue::Exact(o);
6534 assert!(
6535 !v.is_scroll() || v.is_clipped(),
6536 "anything that scrolls also clips ({o:?})"
6537 );
6538 assert!(
6539 !v.is_clip() || v.is_hidden_or_clip(),
6540 "clip is a subset of hidden_or_clip ({o:?})"
6541 );
6542 assert!(
6543 !v.is_scroll_explicit() || v.is_scroll(),
6544 "explicit scroll is a subset of scroll ({o:?})"
6545 );
6546 }
6547 }
6548
6549 #[test]
6551 fn overflow_resolve_computed_matches_css_overflow_3_section_3_1() {
6552 for this in ALL_OVERFLOW {
6553 for other in ALL_OVERFLOW {
6554 let got = MultiValue::Exact(this).resolve_computed(&MultiValue::Exact(other));
6555 let other_is_scrollable =
6556 !matches!(other, LayoutOverflow::Visible | LayoutOverflow::Clip);
6557 let want = if other_is_scrollable {
6558 match this {
6559 LayoutOverflow::Visible => LayoutOverflow::Auto,
6560 LayoutOverflow::Clip => LayoutOverflow::Hidden,
6561 keep => keep,
6562 }
6563 } else {
6564 this
6565 };
6566 assert_eq!(
6567 got,
6568 MultiValue::Exact(want),
6569 "resolve_computed({this:?}, {other:?})"
6570 );
6571 }
6572 }
6573 }
6574
6575 #[test]
6576 fn overflow_resolve_computed_is_a_no_op_unless_both_axes_are_exact() {
6577 let keywords: [MultiValue<LayoutOverflow>; 3] = [
6578 MultiValue::Auto,
6579 MultiValue::Initial,
6580 MultiValue::Inherit,
6581 ];
6582 for v in keywords {
6584 for other in ALL_OVERFLOW {
6585 assert_eq!(v.resolve_computed(&MultiValue::Exact(other)), v);
6586 }
6587 assert_eq!(v.resolve_computed(&MultiValue::Auto), v);
6588 }
6589 for this in ALL_OVERFLOW {
6591 let v = MultiValue::Exact(this);
6592 for other in keywords {
6593 assert_eq!(v.resolve_computed(&other), v, "{this:?} vs {other:?}");
6594 }
6595 }
6596 }
6597
6598 #[test]
6599 fn overflow_resolve_computed_is_idempotent() {
6600 for this in ALL_OVERFLOW {
6601 for other in ALL_OVERFLOW {
6602 let other_mv = MultiValue::Exact(other);
6603 let once = MultiValue::Exact(this).resolve_computed(&other_mv);
6604 let twice = once.resolve_computed(&other_mv);
6605 assert_eq!(once, twice, "resolve_computed({this:?}, {other:?}) twice");
6606 }
6607 }
6608 }
6609
6610 #[test]
6615 fn position_is_absolute_or_fixed_only_for_absolute_and_fixed() {
6616 let all = [
6617 LayoutPosition::Static,
6618 LayoutPosition::Relative,
6619 LayoutPosition::Absolute,
6620 LayoutPosition::Fixed,
6621 LayoutPosition::Sticky,
6622 ];
6623 for p in all {
6624 assert_eq!(
6625 MultiValue::Exact(p).is_absolute_or_fixed(),
6626 matches!(p, LayoutPosition::Absolute | LayoutPosition::Fixed),
6627 "{p:?}"
6628 );
6629 }
6630 assert!(!MultiValue::<LayoutPosition>::Auto.is_absolute_or_fixed());
6632 assert!(!MultiValue::<LayoutPosition>::Initial.is_absolute_or_fixed());
6633 assert!(!MultiValue::<LayoutPosition>::Inherit.is_absolute_or_fixed());
6634 }
6635
6636 #[test]
6637 fn float_is_none_treats_every_keyword_variant_as_not_floated() {
6638 assert!(MultiValue::Exact(LayoutFloat::None).is_none());
6639 assert!(!MultiValue::Exact(LayoutFloat::Left).is_none());
6640 assert!(!MultiValue::Exact(LayoutFloat::Right).is_none());
6641 assert!(MultiValue::<LayoutFloat>::Auto.is_none());
6644 assert!(MultiValue::<LayoutFloat>::Initial.is_none());
6645 assert!(MultiValue::<LayoutFloat>::Inherit.is_none());
6646 assert!(MultiValue::<LayoutFloat>::default().is_none());
6647 }
6648
6649 #[test]
6654 fn blockify_display_follows_the_css_display_3_table() {
6655 for d in ALL_DISPLAY {
6656 let want = match d {
6657 LayoutDisplay::Inline | LayoutDisplay::InlineBlock => LayoutDisplay::Block,
6658 LayoutDisplay::InlineFlex => LayoutDisplay::Flex,
6659 LayoutDisplay::InlineTable => LayoutDisplay::Table,
6660 LayoutDisplay::InlineGrid => LayoutDisplay::Grid,
6661 LayoutDisplay::TableRowGroup
6662 | LayoutDisplay::TableColumn
6663 | LayoutDisplay::TableColumnGroup
6664 | LayoutDisplay::TableHeaderGroup
6665 | LayoutDisplay::TableFooterGroup
6666 | LayoutDisplay::TableRow
6667 | LayoutDisplay::TableCell
6668 | LayoutDisplay::TableCaption => LayoutDisplay::Block,
6669 other => other,
6670 };
6671 assert_eq!(blockify_display(d), want, "blockify_display({d:?})");
6672 }
6673 }
6674
6675 #[test]
6676 fn blockify_display_is_idempotent_and_never_produces_an_inline_level_value() {
6677 for d in ALL_DISPLAY {
6678 let once = blockify_display(d);
6679 assert_eq!(
6680 blockify_display(once),
6681 once,
6682 "blockify_display is not idempotent for {d:?}"
6683 );
6684 assert!(
6685 !matches!(
6686 once,
6687 LayoutDisplay::Inline
6688 | LayoutDisplay::InlineBlock
6689 | LayoutDisplay::InlineFlex
6690 | LayoutDisplay::InlineTable
6691 | LayoutDisplay::InlineGrid
6692 ),
6693 "blockified {d:?} is still inline-level: {once:?}"
6694 );
6695 }
6696 }
6697
6698 #[test]
6699 fn get_computed_display_keeps_none_regardless_of_the_flags() {
6700 for flags in 0_u8..16 {
6702 let got = get_computed_display(
6703 LayoutDisplay::None,
6704 flags & 1 != 0,
6705 flags & 2 != 0,
6706 flags & 4 != 0,
6707 flags & 8 != 0,
6708 );
6709 assert_eq!(got, LayoutDisplay::None, "flags={flags:#06b}");
6710 }
6711 }
6712
6713 #[test]
6714 fn get_computed_display_is_the_identity_when_no_flag_is_set() {
6715 for d in ALL_DISPLAY {
6716 assert_eq!(
6717 get_computed_display(d, false, false, false, false),
6718 d,
6719 "an in-flow, non-root, non-flex-child box keeps its specified display ({d:?})"
6720 );
6721 }
6722 }
6723
6724 #[test]
6725 fn get_computed_display_blockifies_whenever_any_flag_is_set() {
6726 for d in ALL_DISPLAY {
6727 if d == LayoutDisplay::None {
6728 continue; }
6730 for flags in 1_u8..16 {
6733 let got = get_computed_display(
6734 d,
6735 flags & 1 != 0,
6736 flags & 2 != 0,
6737 flags & 4 != 0,
6738 flags & 8 != 0,
6739 );
6740 assert_eq!(
6741 got,
6742 blockify_display(d),
6743 "get_computed_display({d:?}, flags={flags:#06b})"
6744 );
6745 }
6746 }
6747 }
6748
6749 #[test]
6754 fn is_forced_page_break_covers_exactly_the_forcing_keywords() {
6755 for pb in ALL_PAGE_BREAK {
6756 let want = matches!(
6757 pb,
6758 PageBreak::Always
6759 | PageBreak::Page
6760 | PageBreak::Left
6761 | PageBreak::Right
6762 | PageBreak::Recto
6763 | PageBreak::Verso
6764 | PageBreak::All
6765 );
6766 assert_eq!(is_forced_page_break(pb), want, "{pb:?}");
6767 }
6768 assert!(!is_forced_page_break(PageBreak::Column));
6770 assert!(!is_forced_page_break(PageBreak::Auto));
6771 assert!(!is_forced_page_break(PageBreak::default()));
6772 }
6773
6774 #[test]
6775 fn is_avoid_page_break_covers_exactly_avoid_and_avoid_page() {
6776 for pb in ALL_PAGE_BREAK {
6777 let want = matches!(pb, PageBreak::Avoid | PageBreak::AvoidPage);
6778 assert_eq!(is_avoid_page_break(&pb), want, "{pb:?}");
6779 }
6780 assert!(!is_avoid_page_break(&PageBreak::AvoidColumn));
6782 }
6783
6784 #[test]
6785 fn forced_and_avoid_page_break_are_never_both_true() {
6786 for pb in ALL_PAGE_BREAK {
6787 assert!(
6788 !(is_forced_page_break(pb) && is_avoid_page_break(&pb)),
6789 "{pb:?} is simultaneously forced and avoided"
6790 );
6791 }
6792 }
6793
6794 #[test]
6795 fn is_avoid_break_inside_is_true_for_every_variant_except_auto() {
6796 for bi in ALL_BREAK_INSIDE {
6797 assert_eq!(is_avoid_break_inside(&bi), bi != BreakInside::Auto, "{bi:?}");
6798 }
6799 assert!(!is_avoid_break_inside(&BreakInside::default()));
6800 }
6801
6802 fn ua(
6807 width: LayoutScrollbarWidth,
6808 visibility: ScrollbarVisibilityMode,
6809 color: StyleScrollbarColor,
6810 delay_ms: u32,
6811 duration_ms: u32,
6812 ) -> ResolvedUaScrollbar {
6813 ResolvedUaScrollbar {
6814 color,
6815 width,
6816 visibility,
6817 fade_delay: ScrollbarFadeDelay { ms: delay_ms },
6818 fade_duration: ScrollbarFadeDuration { ms: duration_ms },
6819 }
6820 }
6821
6822 #[test]
6823 fn from_ua_resolved_holds_its_invariants_across_the_whole_width_visibility_matrix() {
6824 for width in ALL_SCROLLBAR_WIDTH {
6825 for visibility in ALL_VISIBILITY {
6826 let s = ComputedScrollbarStyle::from_ua_resolved(&ua(
6827 width,
6828 visibility,
6829 StyleScrollbarColor::Auto,
6830 0,
6831 0,
6832 ));
6833
6834 assert_eq!(s.width_mode, width);
6835 assert_eq!(s.visibility, visibility);
6836
6837 let expected_visual = match width {
6838 LayoutScrollbarWidth::Thin => SCROLLBAR_WIDTH_THIN,
6839 LayoutScrollbarWidth::Auto => SCROLLBAR_WIDTH_AUTO,
6840 LayoutScrollbarWidth::None => 0.0,
6841 };
6842 assert_eq!(s.visual_width_px, expected_visual, "{width:?}");
6843
6844 let is_overlay = visibility == ScrollbarVisibilityMode::WhenScrolling;
6846 assert_eq!(s.clip_to_container_border, is_overlay);
6847 assert_eq!(s.show_scroll_buttons, !is_overlay);
6848 assert_eq!(s.show_corner_rect, !is_overlay);
6849 if is_overlay {
6850 assert_eq!(s.reserve_width_px, 0.0, "overlay reserves no layout space");
6851 assert_eq!(s.scroll_button_size_px, 0.0);
6852 } else {
6853 assert_eq!(s.reserve_width_px, s.visual_width_px);
6854 assert_eq!(s.scroll_button_size_px, s.visual_width_px);
6855 }
6856
6857 assert_eq!(
6859 s.visual_width_px_hover,
6860 Some(s.visual_width_px + SCROLLBAR_HOVER_EXPAND_PX)
6861 );
6862 assert_eq!(
6863 s.visual_width_px_active,
6864 Some(s.visual_width_px + SCROLLBAR_HOVER_EXPAND_PX)
6865 );
6866 assert!(s.reserve_width_px <= s.visual_width_px);
6867 assert!(s.visual_width_px.is_finite());
6868 }
6869 }
6870 }
6871
6872 #[test]
6873 fn from_ua_resolved_saturates_the_hover_and_active_colour_maths_at_the_u8_boundaries() {
6874 let white = ColorU {
6876 r: 255,
6877 g: 255,
6878 b: 255,
6879 a: 255,
6880 };
6881 let s = ComputedScrollbarStyle::from_ua_resolved(&ua(
6882 LayoutScrollbarWidth::Auto,
6883 ScrollbarVisibilityMode::Always,
6884 StyleScrollbarColor::Custom(ScrollbarColorCustom {
6885 thumb: white,
6886 track: white,
6887 }),
6888 0,
6889 0,
6890 ));
6891 let hover = s.thumb_color_hover.expect("hover thumb colour");
6892 assert_eq!((hover.r, hover.g, hover.b, hover.a), (255, 255, 255, 255));
6893 let track_hover = s.track_color_hover.expect("hover track colour");
6894 assert_eq!(track_hover.a, 255);
6895
6896 let black0 = ColorU {
6899 r: 0,
6900 g: 0,
6901 b: 0,
6902 a: 0,
6903 };
6904 let s = ComputedScrollbarStyle::from_ua_resolved(&ua(
6905 LayoutScrollbarWidth::Auto,
6906 ScrollbarVisibilityMode::Always,
6907 StyleScrollbarColor::Custom(ScrollbarColorCustom {
6908 thumb: black0,
6909 track: black0,
6910 }),
6911 0,
6912 0,
6913 ));
6914 let active = s.thumb_color_active.expect("active thumb colour");
6915 assert_eq!((active.r, active.g, active.b), (0, 0, 0));
6916 assert_eq!(active.a, 255, "the active thumb is always fully opaque");
6917 let hover = s.thumb_color_hover.expect("hover thumb colour");
6918 assert_eq!(
6919 (hover.r, hover.g, hover.b, hover.a),
6920 (
6921 THUMB_HOVER_LIGHTEN,
6922 THUMB_HOVER_LIGHTEN,
6923 THUMB_HOVER_LIGHTEN,
6924 THUMB_HOVER_ALPHA_ADD
6925 )
6926 );
6927 }
6928
6929 #[test]
6930 fn from_ua_resolved_passes_extreme_fade_timings_through_without_overflow() {
6931 let s = ComputedScrollbarStyle::from_ua_resolved(&ua(
6932 LayoutScrollbarWidth::Thin,
6933 ScrollbarVisibilityMode::WhenScrolling,
6934 StyleScrollbarColor::Auto,
6935 u32::MAX,
6936 u32::MAX,
6937 ));
6938 assert_eq!(s.fade_delay_ms, u32::MAX);
6939 assert_eq!(s.fade_duration_ms, u32::MAX);
6940
6941 let s = ComputedScrollbarStyle::from_ua_resolved(&ua(
6942 LayoutScrollbarWidth::Thin,
6943 ScrollbarVisibilityMode::WhenScrolling,
6944 StyleScrollbarColor::Auto,
6945 0,
6946 0,
6947 ));
6948 assert_eq!(s.fade_delay_ms, 0);
6949 assert_eq!(s.fade_duration_ms, 0);
6950 }
6951
6952 #[test]
6953 fn from_ua_resolved_maps_scrollbar_color_auto_to_transparent() {
6954 let s = ComputedScrollbarStyle::from_ua_resolved(&ua(
6955 LayoutScrollbarWidth::Auto,
6956 ScrollbarVisibilityMode::Always,
6957 StyleScrollbarColor::Auto,
6958 0,
6959 0,
6960 ));
6961 assert_eq!(s.thumb_color, ColorU::TRANSPARENT);
6962 assert_eq!(s.track_color, ColorU::TRANSPARENT);
6963 assert_eq!(s.button_color, ColorU::TRANSPARENT);
6964 assert_eq!(s.corner_color, ColorU::TRANSPARENT);
6965 }
6966
6967 #[test]
6968 fn computed_scrollbar_style_default_is_internally_consistent() {
6969 let d = ComputedScrollbarStyle::default();
6970 assert!(d.visual_width_px.is_finite() && d.visual_width_px >= 0.0);
6971 assert!(d.reserve_width_px.is_finite() && d.reserve_width_px >= 0.0);
6972 assert!(d.reserve_width_px <= d.visual_width_px);
6973 let overlay = d.visibility == ScrollbarVisibilityMode::WhenScrolling;
6974 assert_eq!(d.show_scroll_buttons, !overlay);
6975 assert_eq!(d.clip_to_container_border, overlay);
6976 }
6977
6978 #[test]
6983 fn extract_color_from_background_returns_the_solid_colour_verbatim() {
6984 for probe in [
6985 ColorU::TRANSPARENT,
6986 ColorU::BLACK,
6987 ColorU::WHITE,
6988 ColorU {
6989 r: 1,
6990 g: 2,
6991 b: 3,
6992 a: 4,
6993 },
6994 ColorU {
6995 r: 255,
6996 g: 0,
6997 b: 255,
6998 a: 0,
6999 },
7000 ] {
7001 assert_eq!(
7002 extract_color_from_background(&StyleBackgroundContent::Color(probe)),
7003 probe
7004 );
7005 }
7006 }
7007
7008 #[test]
7009 fn extract_color_from_background_falls_back_to_transparent_for_non_colour_layers() {
7010 let img = StyleBackgroundContent::Image("does-not-exist.png".into());
7012 assert_eq!(extract_color_from_background(&img), ColorU::TRANSPARENT);
7013 let empty = StyleBackgroundContent::Image(String::new().into());
7015 assert_eq!(extract_color_from_background(&empty), ColorU::TRANSPARENT);
7016 let unicode = StyleBackgroundContent::Image("картинка-🎉.png".into());
7017 assert_eq!(extract_color_from_background(&unicode), ColorU::TRANSPARENT);
7018 }
7019
7020 #[test]
7025 fn get_scrollbar_info_from_layout_defaults_to_no_scrollbars_when_layout_never_set_it() {
7026 let node = bare_layout_node(None);
7027 let got = get_scrollbar_info_from_layout(&node);
7028 assert!(!got.needs_horizontal);
7029 assert!(!got.needs_vertical);
7030 assert_eq!(got.scrollbar_width, 0.0);
7031 assert_eq!(got.scrollbar_height, 0.0);
7032 assert_eq!(got.visual_width_px, 0.0);
7033 }
7034
7035 #[test]
7036 fn get_scrollbar_info_from_layout_returns_whatever_layout_stored_including_degenerate_floats() {
7037 let stored = ScrollbarRequirements {
7038 needs_horizontal: true,
7039 needs_vertical: true,
7040 scrollbar_width: f32::NAN,
7041 scrollbar_height: f32::INFINITY,
7042 visual_width_px: -1.0,
7043 };
7044 let got = get_scrollbar_info_from_layout(&bare_layout_node(Some(stored)));
7045 assert!(got.needs_horizontal && got.needs_vertical);
7046 assert!(got.scrollbar_width.is_nan(), "the getter must not sanitise");
7047 assert_eq!(got.scrollbar_height, f32::INFINITY);
7048 assert_eq!(got.visual_width_px, -1.0);
7049 }
7050
7051 #[test]
7056 fn resolved_font_chains_empty_instance_answers_every_query_with_none() {
7057 let r = empty_chains();
7058 assert_eq!(r.len(), 0);
7059 assert!(r.is_empty());
7060 assert_eq!(r.font_refs_len(), 0);
7061
7062 assert!(r.get(&FontChainKeyOrRef::Ref(0)).is_none());
7063 assert!(r.get_by_chain_key(&chain_key("Arial")).is_none());
7064 assert!(r.get_for_font_stack(&[]).is_none());
7065 assert!(r.get_for_font_ref(0).is_none());
7066 assert!(r.get_for_font_ref(usize::MAX).is_none());
7067 assert!(r.get_for_font_ref(usize::MAX / 2).is_none());
7068
7069 assert!(r.clone().into_inner().is_empty());
7070 assert!(r.into_fontconfig_chains().is_empty());
7071 }
7072
7073 #[test]
7074 fn resolved_font_chains_get_by_chain_key_round_trips_the_inserted_key() {
7075 let key = chain_key("Iosevka");
7076 let mut chains = HashMap::new();
7077 chains.insert(
7078 FontChainKeyOrRef::Chain(key.clone()),
7079 chain_with(Vec::new(), Vec::new()),
7080 );
7081 let r = ResolvedFontChains { chains, ..Default::default() };
7082
7083 assert!(r.get_by_chain_key(&key).is_some());
7084 assert!(r.get(&FontChainKeyOrRef::Chain(key.clone())).is_some());
7085 let heavier = FontChainKey {
7087 weight: FcWeight::Bold,
7088 ..key.clone()
7089 };
7090 assert!(r.get_by_chain_key(&heavier).is_none());
7091 let italic = FontChainKey {
7093 italic: true,
7094 ..key
7095 };
7096 assert!(r.get_by_chain_key(&italic).is_none());
7097 }
7098
7099 #[test]
7100 fn resolved_font_chains_counts_and_filters_ref_entries() {
7101 let mut chains = HashMap::new();
7102 chains.insert(
7103 FontChainKeyOrRef::Chain(chain_key("Arial")),
7104 chain_with(Vec::new(), Vec::new()),
7105 );
7106 chains.insert(
7107 FontChainKeyOrRef::Ref(0xDEAD_BEEF),
7108 chain_with(Vec::new(), Vec::new()),
7109 );
7110 chains.insert(
7111 FontChainKeyOrRef::Ref(usize::MAX),
7112 chain_with(Vec::new(), Vec::new()),
7113 );
7114 let r = ResolvedFontChains { chains, ..Default::default() };
7115
7116 assert_eq!(r.len(), 3);
7117 assert!(!r.is_empty());
7118 assert_eq!(r.font_refs_len(), 2, "two Ref keys, one Chain key");
7119 assert!(r.get_for_font_ref(0xDEAD_BEEF).is_some());
7120 assert!(r.get_for_font_ref(usize::MAX).is_some());
7121 assert!(r.get_for_font_ref(0).is_none());
7122
7123 let fc_only = r.into_fontconfig_chains();
7125 assert_eq!(fc_only.len(), 1);
7126 assert!(fc_only.contains_key(&chain_key("Arial")));
7127 }
7128
7129 #[test]
7130 fn resolved_font_chains_get_for_font_stack_uses_the_canonical_selector_key() {
7131 let selectors = vec![FontSelector {
7132 family: "Arial".to_string(),
7133 weight: FcWeight::Normal,
7134 style: FontStyle::Normal,
7135 unicode_ranges: Vec::new(),
7136 }];
7137 let key = FontChainKey::from_selectors(&selectors);
7138 let mut chains = HashMap::new();
7139 chains.insert(
7140 FontChainKeyOrRef::Chain(key),
7141 chain_with(Vec::new(), Vec::new()),
7142 );
7143 let r = ResolvedFontChains { chains, ..Default::default() };
7144
7145 assert!(r.get_for_font_stack(&selectors).is_some());
7146 assert!(r.get_for_font_stack(&[]).is_none());
7148 }
7149
7150 #[test]
7155 fn collect_font_ids_from_chains_dedupes_across_groups_and_unicode_fallbacks() {
7156 let mut chains = HashMap::new();
7157 chains.insert(
7158 FontChainKeyOrRef::Chain(chain_key("Arial")),
7159 chain_with(
7160 vec![
7161 CssFallbackGroup {
7162 css_name: "Arial".to_string(),
7163 fonts: vec![font_match(1, &[]), font_match(2, &[])],
7164 },
7165 CssFallbackGroup {
7166 css_name: "sans-serif".to_string(),
7167 fonts: vec![font_match(1, &[]), font_match(3, &[])],
7169 },
7170 ],
7171 vec![font_match(3, &[]), font_match(u128::MAX, &[])],
7172 ),
7173 );
7174 let ids = collect_font_ids_from_chains(&ResolvedFontChains { chains, ..Default::default() });
7175 assert_eq!(ids.len(), 4, "ids 1, 2, 3 and u128::MAX, each exactly once");
7176 for probe in [1_u128, 2, 3, u128::MAX] {
7177 assert!(ids.contains(&FontId(probe)), "missing FontId({probe})");
7178 }
7179 assert!(!ids.contains(&FontId(0)));
7180 }
7181
7182 #[test]
7183 fn collect_font_ids_from_chains_returns_empty_for_an_empty_or_fontless_chain_set() {
7184 assert!(collect_font_ids_from_chains(&empty_chains()).is_empty());
7185
7186 let mut chains = HashMap::new();
7188 chains.insert(
7189 FontChainKeyOrRef::Chain(chain_key("Nonexistent")),
7190 chain_with(
7191 vec![CssFallbackGroup {
7192 css_name: "Nonexistent".to_string(),
7193 fonts: Vec::new(),
7194 }],
7195 Vec::new(),
7196 ),
7197 );
7198 assert!(collect_font_ids_from_chains(&ResolvedFontChains { chains, ..Default::default() }).is_empty());
7199 }
7200
7201 #[test]
7202 fn compute_fonts_to_load_is_the_set_difference_and_bails_early_on_an_empty_requirement() {
7203 let a = FontId(0);
7204 let b = FontId(1);
7205 let c = FontId(u128::MAX);
7206
7207 let empty: HashSet<FontId> = HashSet::new();
7208 let all: HashSet<FontId> = [a, b, c].into_iter().collect();
7209 let loaded_b: HashSet<FontId> = [b].into_iter().collect();
7210
7211 assert!(compute_fonts_to_load(&empty, &empty).is_empty());
7213 assert!(compute_fonts_to_load(&empty, &all).is_empty());
7214
7215 assert_eq!(compute_fonts_to_load(&all, &empty), all);
7217
7218 let todo = compute_fonts_to_load(&all, &loaded_b);
7220 assert_eq!(todo.len(), 2);
7221 assert!(todo.contains(&a) && todo.contains(&c));
7222 assert!(!todo.contains(&b));
7223
7224 assert!(compute_fonts_to_load(&loaded_b, &all).is_empty());
7226 assert!(compute_fonts_to_load(&all, &all).is_empty());
7227 }
7228
7229 #[test]
7234 fn prune_chain_to_used_chars_keeps_the_first_match_of_every_group_when_nothing_is_needed() {
7235 let mut chain = chain_with(
7236 vec![
7237 CssFallbackGroup {
7238 css_name: "A".to_string(),
7239 fonts: vec![font_match(1, &[(0, 0x10_FFFF)]), font_match(2, &[]), font_match(3, &[])],
7240 },
7241 CssFallbackGroup {
7242 css_name: "B".to_string(),
7243 fonts: vec![font_match(4, &[]), font_match(5, &[])],
7244 },
7245 ],
7246 vec![font_match(6, &[(0x4E00, 0x9FFF)])],
7247 );
7248
7249 prune_chain_to_used_chars(&mut chain, &std::collections::BTreeSet::new());
7250
7251 assert_eq!(chain.css_fallbacks[0].fonts.len(), 1);
7253 assert_eq!(chain.css_fallbacks[0].fonts[0].id, FontId(1));
7254 assert_eq!(chain.css_fallbacks[1].fonts.len(), 1);
7255 assert_eq!(chain.css_fallbacks[1].fonts[0].id, FontId(4));
7256 assert!(chain.unicode_fallbacks.is_empty());
7258 }
7259
7260 #[test]
7261 fn prune_chain_to_used_chars_keeps_walking_until_every_codepoint_is_covered() {
7262 let mut chain = chain_with(
7264 vec![CssFallbackGroup {
7265 css_name: "A".to_string(),
7266 fonts: vec![
7267 font_match(1, &[(0x20, 0x7F)]), font_match(2, &[(0x80, 0x24F)]), font_match(3, &[(0x0, 0x10_FFFF)]), ],
7271 }],
7272 Vec::new(),
7273 );
7274 let used: std::collections::BTreeSet<u32> = [0xE9_u32].into_iter().collect();
7275
7276 prune_chain_to_used_chars(&mut chain, &used);
7277
7278 assert_eq!(
7279 chain.css_fallbacks[0].fonts.len(),
7280 2,
7281 "walk stops as soon as the needed codepoints are covered"
7282 );
7283 assert_eq!(chain.css_fallbacks[0].fonts[1].id, FontId(2));
7284 }
7285
7286 #[test]
7287 fn prune_chain_to_used_chars_keeps_the_whole_group_when_nothing_ever_covers_the_codepoint() {
7288 let mut chain = chain_with(
7289 vec![CssFallbackGroup {
7290 css_name: "A".to_string(),
7291 fonts: vec![font_match(1, &[(0x20, 0x7F)]), font_match(2, &[(0x20, 0x7F)])],
7292 }],
7293 vec![font_match(3, &[(0x20, 0x7F)])],
7294 );
7295 let used: std::collections::BTreeSet<u32> = [u32::MAX].into_iter().collect();
7297
7298 prune_chain_to_used_chars(&mut chain, &used);
7299
7300 assert_eq!(
7301 chain.css_fallbacks[0].fonts.len(),
7302 2,
7303 "an uncoverable codepoint must not silently drop CSS fonts"
7304 );
7305 assert!(
7306 chain.unicode_fallbacks.is_empty(),
7307 "no unicode fallback intersects U+FFFFFFFF"
7308 );
7309 }
7310
7311 #[test]
7312 fn prune_chain_to_used_chars_treats_unicode_ranges_as_inclusive_on_both_ends() {
7313 for probe in [0x4E00_u32, 0x9FFF] {
7314 let mut chain = chain_with(
7315 Vec::new(),
7316 vec![font_match(1, &[(0x4E00, 0x9FFF)]), font_match(2, &[(0x20, 0x7F)])],
7317 );
7318 let used: std::collections::BTreeSet<u32> = [probe].into_iter().collect();
7319 prune_chain_to_used_chars(&mut chain, &used);
7320 assert_eq!(
7321 chain.unicode_fallbacks.len(),
7322 1,
7323 "U+{probe:04X} is inside the inclusive CJK range"
7324 );
7325 assert_eq!(chain.unicode_fallbacks[0].id, FontId(1));
7326 }
7327 for probe in [0x4DFF_u32, 0xA000] {
7329 let mut chain = chain_with(Vec::new(), vec![font_match(1, &[(0x4E00, 0x9FFF)])]);
7330 let used: std::collections::BTreeSet<u32> = [probe].into_iter().collect();
7331 prune_chain_to_used_chars(&mut chain, &used);
7332 assert!(chain.unicode_fallbacks.is_empty(), "U+{probe:04X}");
7333 }
7334 }
7335
7336 #[test]
7337 fn prune_chain_to_used_chars_survives_empty_chains_and_empty_groups() {
7338 let mut empty = chain_with(Vec::new(), Vec::new());
7339 prune_chain_to_used_chars(&mut empty, &std::collections::BTreeSet::new());
7340 assert!(empty.css_fallbacks.is_empty());
7341 assert!(empty.unicode_fallbacks.is_empty());
7342
7343 let mut fontless = chain_with(
7345 vec![CssFallbackGroup {
7346 css_name: "A".to_string(),
7347 fonts: Vec::new(),
7348 }],
7349 Vec::new(),
7350 );
7351 let used: std::collections::BTreeSet<u32> = [0x1F389_u32].into_iter().collect();
7352 prune_chain_to_used_chars(&mut fontless, &used);
7353 assert_eq!(fontless.css_fallbacks.len(), 1);
7354 assert!(fontless.css_fallbacks[0].fonts.is_empty());
7355 }
7356
7357 #[test]
7362 fn build_font_selector_stack_always_appends_the_three_generic_fallbacks() {
7363 let families = StyleFontFamilyVec::from_vec(Vec::new());
7364 let stack = build_font_selector_stack(&families, None, FcWeight::Normal, FontStyle::Normal);
7365
7366 let names: Vec<&str> = stack.iter().map(|s| s.family.as_str()).collect();
7367 assert_eq!(names, ["sans-serif", "serif", "monospace"]);
7368 for s in &stack {
7369 assert_eq!(s.weight, FcWeight::Normal);
7370 assert_eq!(s.style, FontStyle::Normal);
7371 }
7372 }
7373
7374 #[test]
7375 fn build_font_selector_stack_puts_the_authored_families_first() {
7376 let families = StyleFontFamilyVec::from_vec(vec![
7377 StyleFontFamily::System("Iosevka".to_string().into()),
7378 StyleFontFamily::System("Menlo".to_string().into()),
7379 ]);
7380 let stack = build_font_selector_stack(&families, None, FcWeight::Bold, FontStyle::Italic);
7381
7382 assert_eq!(stack.len(), 5, "2 authored + 3 generic fallbacks");
7383 assert_eq!(stack[0].family, "Iosevka");
7384 assert_eq!(stack[1].family, "Menlo");
7385 assert_eq!(stack[0].weight, FcWeight::Bold);
7387 assert_eq!(stack[0].style, FontStyle::Italic);
7388 assert_eq!(stack[4].family, "monospace");
7390 assert_eq!(stack[4].weight, FcWeight::Normal);
7391 assert_eq!(stack[4].style, FontStyle::Normal);
7392 }
7393
7394 #[test]
7395 fn build_font_selector_stack_does_not_duplicate_a_generic_the_author_already_listed() {
7396 let families =
7398 StyleFontFamilyVec::from_vec(vec![StyleFontFamily::System("MONOSPACE".to_string().into())]);
7399 let stack = build_font_selector_stack(&families, None, FcWeight::Normal, FontStyle::Normal);
7400
7401 assert_eq!(stack.len(), 3, "MONOSPACE + sans-serif + serif");
7402 assert_eq!(stack[0].family, "MONOSPACE");
7403 let lower: Vec<String> = stack.iter().map(|s| s.family.to_lowercase()).collect();
7404 assert_eq!(
7405 lower.iter().filter(|f| f.as_str() == "monospace").count(),
7406 1,
7407 "the generic must appear exactly once"
7408 );
7409
7410 let families = StyleFontFamilyVec::from_vec(vec![
7412 StyleFontFamily::System("serif".to_string().into()),
7413 StyleFontFamily::System("Sans-Serif".to_string().into()),
7414 StyleFontFamily::System("monospace".to_string().into()),
7415 ]);
7416 let stack = build_font_selector_stack(&families, None, FcWeight::Normal, FontStyle::Normal);
7417 assert_eq!(stack.len(), 3);
7418 }
7419
7420 #[test]
7421 fn build_font_selector_stack_passes_hostile_family_names_through_untouched() {
7422 let huge = "A".repeat(10_000);
7423 let families = StyleFontFamilyVec::from_vec(vec![
7424 StyleFontFamily::System(String::new().into()),
7425 StyleFontFamily::System(" \t\n ".to_string().into()),
7426 StyleFontFamily::System("M🎉 ǝɔɐɟdʎʇ — «Шрифт»".to_string().into()),
7427 StyleFontFamily::System(huge.clone().into()),
7428 ]);
7429 let stack = build_font_selector_stack(&families, None, FcWeight::Normal, FontStyle::Normal);
7430
7431 assert_eq!(stack.len(), 7, "4 authored + 3 generic fallbacks");
7432 assert_eq!(stack[0].family, "");
7433 assert_eq!(stack[2].family, "M🎉 ǝɔɐɟdʎʇ — «Шрифт»");
7434 assert_eq!(stack[3].family.len(), huge.len());
7435 assert_eq!(stack[6].family, "monospace");
7436 }
7437
7438 #[test]
7443 fn resolve_font_chains_yields_nothing_for_an_empty_or_degenerate_collection() {
7444 let fc = FcFontCache::default();
7445
7446 let collected = CollectedFontStacks {
7447 font_stacks: Vec::new(),
7448 hash_to_index: HashMap::new(),
7449 font_refs: HashMap::new(),
7450 };
7451 assert!(resolve_font_chains(&collected, &fc, Some(&[])).is_empty());
7452
7453 let collected = CollectedFontStacks {
7455 font_stacks: vec![Vec::new()],
7456 hash_to_index: HashMap::new(),
7457 font_refs: HashMap::new(),
7458 };
7459 assert!(resolve_font_chains(&collected, &fc, Some(&[])).is_empty());
7460 }
7461
7462 #[test]
7467 fn font_size_getters_return_the_default_for_an_unstyled_dom() {
7468 let sd = StyledDom::default();
7469 let root = NodeId::new(0);
7470 let st = normal();
7471
7472 assert_eq!(get_element_font_size(&sd, root, &st), DEFAULT_FONT_SIZE);
7473 assert_eq!(get_root_font_size(&sd, &st), DEFAULT_FONT_SIZE);
7474 assert_eq!(get_parent_font_size(&sd, root, &st), DEFAULT_FONT_SIZE);
7476 assert_eq!(
7477 resolve_font_size_slow(&sd, root, &st),
7478 DEFAULT_FONT_SIZE,
7479 "the slow path must agree with the memoised one"
7480 );
7481 }
7482
7483 #[test]
7484 fn font_size_resolution_is_identical_on_the_normal_and_the_pseudo_state_paths() {
7485 let sd = body_with_divs(1, "body { font-size: 32px; }");
7488 let child = NodeId::new(1);
7489 assert_eq!(
7490 get_element_font_size(&sd, child, &normal()),
7491 get_element_font_size(&sd, child, &hovered()),
7492 );
7493 }
7494
7495 #[test]
7496 fn font_size_em_resolves_against_the_parent_and_not_the_default() {
7497 let sd = body_with_divs(1, "body { font-size: 32px; } div { font-size: 2em; }");
7498 let root = NodeId::new(0);
7499 let child = NodeId::new(1);
7500
7501 assert_eq!(get_element_font_size(&sd, root, &state_of(&sd, root)), 32.0);
7502 assert_eq!(
7503 get_element_font_size(&sd, child, &state_of(&sd, child)),
7504 64.0,
7505 "2em under a 32px parent is 64px — resolving against DEFAULT_FONT_SIZE would give 32"
7506 );
7507 assert_eq!(
7508 get_parent_font_size(&sd, child, &state_of(&sd, child)),
7509 32.0
7510 );
7511 assert_eq!(get_root_font_size(&sd, &state_of(&sd, child)), 32.0);
7512 }
7513
7514 #[test]
7515 fn font_size_getters_stay_finite_for_hostile_stylesheet_values() {
7516 for css in [
7519 "body { font-size: 0px; }",
7520 "body { font-size: 0; }",
7521 "body { font-size: 999999px; }",
7522 "body { font-size: -10px; }",
7523 "body { font-size: 1e30px; }",
7524 "div { font-size: 1000em; }",
7525 "div { font-size: 0em; }",
7526 ] {
7527 let sd = body_with_divs(1, css);
7528 for id in [NodeId::new(0), NodeId::new(1)] {
7529 let st = state_of(&sd, id);
7530 let px = get_element_font_size(&sd, id, &st);
7531 assert!(
7532 px.is_finite(),
7533 "{css:?} produced a non-finite font-size ({px}) on node {id:?}"
7534 );
7535 assert_eq!(
7536 px,
7537 resolve_font_size_slow(&sd, id, &st),
7538 "memoised and slow paths disagree for {css:?}"
7539 );
7540 }
7541 }
7542 }
7543
7544 #[test]
7545 fn font_size_resolution_walks_a_deep_ancestor_chain_without_recursing() {
7546 const DEPTH: usize = 64;
7549 let mut dom = Dom::create_div();
7550 for _ in 0..DEPTH {
7551 dom = Dom::create_div().with_children(vec![dom].into());
7552 }
7553 let mut root = Dom::create_body().with_children(vec![dom].into());
7554 let sd = StyledDom::create(&mut root, parse("body { font-size: 20px; }"));
7555
7556 let deepest = NodeId::new(sd.node_data.len() - 1);
7557 let st = state_of(&sd, deepest);
7558 let px = get_element_font_size(&sd, deepest, &st);
7559 assert!(px.is_finite() && px > 0.0);
7560 assert_eq!(px, resolve_font_size_slow(&sd, deepest, &st));
7561 }
7562
7563 #[test]
7564 fn resolve_font_size_one_is_stable_under_nan_and_infinite_context_sizes() {
7565 let sd = StyledDom::default();
7568 let root = NodeId::new(0);
7569 let st = normal();
7570 for (parent, rootsz) in [
7571 (0.0_f32, 0.0_f32),
7572 (f32::NAN, f32::NAN),
7573 (f32::INFINITY, f32::NEG_INFINITY),
7574 (f32::MAX, f32::MIN),
7575 (-1.0, -1.0),
7576 ] {
7577 let px = resolve_font_size_one(&sd, root, &st, parent, rootsz);
7578 assert_eq!(
7579 px, DEFAULT_FONT_SIZE,
7580 "an unstyled node ignores the context and falls back to the default \
7581 (parent={parent}, root={rootsz})"
7582 );
7583 }
7584 }
7585
7586 #[test]
7591 fn optional_node_getters_return_their_documented_defaults_for_none() {
7592 let sd = StyledDom::default();
7593
7594 assert_eq!(get_z_index(&sd, None), 0);
7595 assert!(is_z_index_auto(&sd, None));
7596 assert_eq!(get_break_before(&sd, None), PageBreak::Auto);
7597 assert_eq!(get_break_after(&sd, None), PageBreak::Auto);
7598 assert_eq!(get_break_inside(&sd, None), BreakInside::Auto);
7599 assert_eq!(get_orphans(&sd, None), 2);
7600 assert_eq!(get_widows(&sd, None), 2);
7601 assert_eq!(
7602 get_box_decoration_break(&sd, None),
7603 BoxDecorationBreak::Slice
7604 );
7605 assert_eq!(
7606 get_display_property(&sd, None),
7607 MultiValue::Exact(LayoutDisplay::Inline),
7608 "a missing node is treated as anonymous inline content"
7609 );
7610 assert_eq!(get_list_style_type(&sd, None), StyleListStyleType::default());
7611 assert_eq!(
7612 get_list_style_position(&sd, None),
7613 StyleListStylePosition::default()
7614 );
7615 assert_eq!(get_caret_style(&sd, None).width, DEFAULT_CARET_WIDTH_PX);
7616 assert_eq!(
7617 get_caret_style(&sd, None).animation_duration,
7618 CssDuration::from_millis(DEFAULT_CARET_BLINK_MS)
7619 );
7620 let sel = get_selection_style(&sd, None, None);
7621 assert_eq!(sel.radius, 0.0);
7622 assert_eq!(sel.text_color, None);
7623 }
7624
7625 #[test]
7630 fn caret_animation_duration_preserves_the_unit_the_stylesheet_used() {
7631 let child = Some(NodeId::new(1));
7632
7633 let sd = body_with_divs(1, "div { caret-animation-duration: 5t; }");
7634 assert_eq!(
7635 get_caret_style(&sd, child).animation_duration,
7636 CssDuration::from_ticks(5)
7637 );
7638
7639 let sd = body_with_divs(1, "div { caret-animation-duration: 250ms; }");
7640 assert_eq!(
7641 get_caret_style(&sd, child).animation_duration,
7642 CssDuration::from_millis(250)
7643 );
7644
7645 let sd = body_with_divs(1, "div { caret-animation-duration: 1s; }");
7646 assert_eq!(
7647 get_caret_style(&sd, child).animation_duration,
7648 CssDuration::from_millis(1000)
7649 );
7650
7651 let sd = body_with_divs(1, "div { caret-animation-duration: 60t; }");
7654 assert_ne!(
7655 get_caret_style(&sd, child).animation_duration,
7656 CssDuration::from_millis(1000)
7657 );
7658 }
7659
7660 #[test]
7661 fn z_index_defaults_to_auto_and_reads_back_explicit_integers() {
7662 let sd = body_with_divs(1, "");
7663 let root = NodeId::new(0);
7664 assert_eq!(get_z_index(&sd, Some(root)), 0);
7665 assert!(is_z_index_auto(&sd, Some(root)));
7666
7667 for (css, want) in [
7668 ("div { z-index: 0; }", 0_i32),
7669 ("div { z-index: 7; }", 7),
7670 ("div { z-index: -7; }", -7),
7671 ] {
7672 let sd = body_with_divs(1, css);
7673 let child = Some(NodeId::new(1));
7674 assert_eq!(get_z_index(&sd, child), want, "{css:?}");
7675 assert!(
7676 !is_z_index_auto(&sd, child),
7677 "an explicit integer is not auto ({css:?})"
7678 );
7679 }
7680
7681 let sd = body_with_divs(1, "div { z-index: auto; }");
7683 let child = Some(NodeId::new(1));
7684 assert_eq!(get_z_index(&sd, child), 0);
7685 assert!(is_z_index_auto(&sd, child));
7686 }
7687
7688 #[test]
7689 fn z_index_reads_back_the_i16_encoding_boundaries_and_falls_through_above_them() {
7690 for (css, want) in [
7694 ("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),
7699 ] {
7700 let sd = body_with_divs(1, css);
7701 let child = Some(NodeId::new(1));
7702 assert_eq!(
7703 get_z_index(&sd, child),
7704 want,
7705 "{css:?} must survive the i16 compact encoding"
7706 );
7707 assert!(
7708 !is_z_index_auto(&sd, child),
7709 "an explicit (if huge) integer is not auto ({css:?})"
7710 );
7711 }
7712 }
7713
7714 #[test]
7719 fn border_radius_is_zero_by_default_for_every_degenerate_element_and_viewport_size() {
7720 let sd = StyledDom::default();
7721 let root = NodeId::new(0);
7722 let st = normal();
7723
7724 let sizes = [
7725 (0.0_f32, 0.0_f32),
7726 (-100.0, -100.0),
7727 (f32::NAN, f32::NAN),
7728 (f32::INFINITY, f32::INFINITY),
7729 (f32::MAX, f32::MAX),
7730 (f32::MIN_POSITIVE, f32::MIN_POSITIVE),
7731 ];
7732 for (w, h) in sizes {
7733 let element = PhysicalSizeImport {
7734 width: w,
7735 height: h,
7736 };
7737 let viewport = LogicalSize::new(w, h);
7738 let r = get_border_radius(&sd, root, &st, element, viewport);
7739 assert_eq!(r.top_left, 0.0, "element=({w}, {h})");
7740 assert_eq!(r.top_right, 0.0, "element=({w}, {h})");
7741 assert_eq!(r.bottom_left, 0.0, "element=({w}, {h})");
7742 assert_eq!(r.bottom_right, 0.0, "element=({w}, {h})");
7743 }
7744 }
7745
7746 #[test]
7747 fn border_radius_resolves_authored_pixels_on_both_the_normal_and_the_pseudo_path() {
7748 let sd = body_with_divs(1, "div { border-radius: 12px; }");
7749 let child = NodeId::new(1);
7750 let element = PhysicalSizeImport {
7751 width: 100.0,
7752 height: 50.0,
7753 };
7754 let viewport = LogicalSize::new(800.0, 600.0);
7755
7756 for st in [normal(), hovered()] {
7757 let r = get_border_radius(&sd, child, &st, element, viewport);
7758 for corner in [r.top_left, r.top_right, r.bottom_left, r.bottom_right] {
7759 assert!(corner.is_finite(), "corner must stay finite");
7760 assert_eq!(corner, 12.0);
7761 }
7762 }
7763
7764 let raw = get_style_border_radius(&sd, child, &normal());
7765 assert!(raw.top_left.number.get().is_finite());
7766 }
7767
7768 #[test]
7769 fn border_radius_percentages_stay_finite_for_zero_and_infinite_element_sizes() {
7770 let sd = body_with_divs(1, "div { border-radius: 50%; }");
7771 let child = NodeId::new(1);
7772 let viewport = LogicalSize::new(0.0, 0.0);
7773
7774 for (w, h) in [
7775 (0.0_f32, 0.0_f32),
7776 (f32::MAX, f32::MAX),
7777 (-10.0, -10.0),
7778 (f32::INFINITY, 1.0),
7779 ] {
7780 let element = PhysicalSizeImport {
7781 width: w,
7782 height: h,
7783 };
7784 let r = get_border_radius(&sd, child, &hovered(), element, viewport);
7787 for corner in [r.top_left, r.top_right, r.bottom_left, r.bottom_right] {
7788 assert!(
7789 !corner.is_nan(),
7790 "a {w}x{h} element produced a NaN corner radius"
7791 );
7792 }
7793 }
7794 }
7795
7796 #[test]
7801 fn optional_style_getters_are_all_none_on_an_unstyled_node() {
7802 let sd = body_with_divs(1, "");
7803 let id = NodeId::new(1);
7804
7805 for st in [normal(), hovered()] {
7806 assert!(get_shape_inside(&sd, id, &st).is_none());
7807 assert!(get_shape_outside(&sd, id, &st).is_none());
7808 assert!(get_line_clamp(&sd, id, &st).is_none());
7809 assert!(get_initial_letter(&sd, id, &st).is_none());
7810 assert!(get_hanging_punctuation(&sd, id, &st).is_none());
7811 assert!(get_text_combine_upright(&sd, id, &st).is_none());
7812 assert!(get_hyphenation_language(&sd, id, &st).is_none());
7813 assert!(get_column_count(&sd, id, &st).is_none());
7814 assert!(get_filter(&sd, id, &st).is_none());
7815 assert!(get_backdrop_filter(&sd, id, &st).is_none());
7816 assert!(get_box_shadow_left(&sd, id, &st).is_none());
7817 assert!(get_box_shadow_right(&sd, id, &st).is_none());
7818 assert!(get_box_shadow_top(&sd, id, &st).is_none());
7819 assert!(get_box_shadow_bottom(&sd, id, &st).is_none());
7820 assert!(get_text_shadow(&sd, id, &st).is_none());
7821 assert!(get_transform(&sd, id, &st).is_none());
7822 assert!(get_counter_reset(&sd, id, &st).is_none());
7823 assert!(get_counter_increment(&sd, id, &st).is_none());
7824 assert!(get_clip_path(&sd, id, &st).is_none());
7825 assert!(get_grid_template_areas_prop(&sd, id, &st).is_none());
7826 }
7827 }
7828
7829 #[test]
7830 fn numeric_style_getters_use_their_documented_defaults() {
7831 let sd = body_with_divs(1, "");
7832 let id = NodeId::new(1);
7833
7834 for st in [normal(), hovered()] {
7835 assert_eq!(get_opacity(&sd, id, &st), 1.0, "opacity defaults to 1.0");
7836 assert_eq!(
7837 get_exclusion_margin(&sd, id, &st),
7838 0.0,
7839 "exclusion-margin defaults to 0.0"
7840 );
7841 assert!(get_scrollbar_width_px(&sd, id, &st).is_finite());
7842 assert!(get_scrollbar_width_px(&sd, id, &st) >= 0.0);
7843 }
7844 }
7845
7846 #[test]
7847 fn opacity_in_range_agrees_on_the_compact_and_the_cascade_path() {
7848 for css in [
7849 "div { opacity: 0; }",
7850 "div { opacity: 1; }",
7851 "div { opacity: 0.5; }",
7852 ] {
7853 let sd = body_with_divs(1, css);
7854 let id = NodeId::new(1);
7855 let fast = get_opacity(&sd, id, &normal()); let slow = get_opacity(&sd, id, &hovered()); assert!(fast.is_finite() && slow.is_finite(), "{css:?}");
7858 assert!(
7859 (0.0..=1.0).contains(&fast),
7860 "{css:?} read back out of range on the compact path: {fast}"
7861 );
7862 assert!(
7863 (fast - slow).abs() < 0.01,
7864 "{css:?}: compact path says {fast}, cascade path says {slow}"
7865 );
7866 }
7867
7868 let half = get_opacity(&body_with_divs(1, "div { opacity: 0.5; }"), NodeId::new(1), &normal());
7870 assert!(half < 1.0 && half > 0.0, "opacity: 0.5 read back as {half}");
7871 }
7872
7873 #[test]
7874 fn opacity_never_returns_nan_or_infinity_for_out_of_range_authored_values() {
7875 for css in [
7881 "div { opacity: 5; }",
7882 "div { opacity: -3; }",
7883 "div { opacity: 1e30; }",
7884 ] {
7885 let sd = body_with_divs(1, css);
7886 let id = NodeId::new(1);
7887 for st in [normal(), hovered()] {
7888 let o = get_opacity(&sd, id, &st);
7889 assert!(o.is_finite(), "{css:?} produced a non-finite opacity: {o}");
7890 }
7891 let fast = get_opacity(&sd, id, &normal());
7893 assert!(
7894 (0.0..=1.0).contains(&fast),
7895 "{css:?} escaped the compact-cache clamp: {fast}"
7896 );
7897 }
7898 }
7899
7900 #[test]
7901 fn enum_property_getters_stay_deterministic_across_pseudo_states() {
7902 let sd = body_with_divs(1, "");
7903 let id = NodeId::new(1);
7904
7905 for st in [normal(), hovered()] {
7906 let gutter = get_scrollbar_gutter_property(&sd, id, &st);
7909 assert_eq!(gutter, get_scrollbar_gutter_property(&sd, id, &st));
7910 let orientation = get_text_orientation_property(&sd, id, &st);
7911 assert_eq!(orientation, get_text_orientation_property(&sd, id, &st));
7912 let valign = get_vertical_align_property(&sd, id, &st);
7913 assert_eq!(valign, get_vertical_align_property(&sd, id, &st));
7914
7915 let _ = get_background_color(&sd, id, &st);
7916 let _ = get_background_contents(&sd, id, &st);
7917 let _ = get_border_info(&sd, id, &st);
7918 let _ = get_border_spacing(&sd, id, &st);
7919 let _ = get_height_value(&sd, id, &st);
7920 let _ = get_line_height_value(&sd, id, &st);
7921 let _ = get_text_indent_value(&sd, id, &st);
7922 }
7923
7924 assert!(matches!(
7926 get_vertical_align_for_node(&sd, id),
7927 crate::text3::cache::VerticalAlign::Baseline
7928 ));
7929 }
7930
7931 #[test]
7932 fn get_inline_border_info_is_none_without_borders_and_survives_a_degenerate_viewport() {
7933 let sd = body_with_divs(1, "");
7934 let id = NodeId::new(1);
7935 let st = normal();
7936 let info = get_border_info(&sd, id, &st);
7937
7938 for viewport in [
7939 PhysicalSize::new(0.0, 0.0),
7940 PhysicalSize::new(f32::NAN, f32::NAN),
7941 PhysicalSize::new(f32::INFINITY, f32::INFINITY),
7942 PhysicalSize::new(-1.0, -1.0),
7943 PhysicalSize::new(f32::MAX, f32::MAX),
7944 ] {
7945 assert!(
7946 get_inline_border_info(&sd, id, &st, &info, viewport).is_none(),
7947 "a node with neither border nor padding has no inline border box"
7948 );
7949 }
7950 }
7951
7952 #[test]
7953 fn get_inline_border_info_reports_finite_widths_for_a_bordered_node() {
7954 let sd = body_with_divs(1, "div { border: 3px solid red; padding: 5px; }");
7955 let id = NodeId::new(1);
7956 let st = normal();
7957 let info = get_border_info(&sd, id, &st);
7958
7959 let inline = get_inline_border_info(&sd, id, &st, &info, PhysicalSize::new(800.0, 600.0))
7960 .expect("a bordered + padded node must produce an InlineBorderInfo");
7961 for w in [inline.top, inline.right, inline.bottom, inline.left] {
7962 assert!(w.is_finite() && w >= 0.0, "border width {w}");
7963 }
7964 for p in [
7965 inline.padding_top,
7966 inline.padding_right,
7967 inline.padding_bottom,
7968 inline.padding_left,
7969 ] {
7970 assert!(p.is_finite() && p >= 0.0, "padding {p}");
7971 }
7972 assert!(inline.is_first_fragment && inline.is_last_fragment);
7973 assert!(!inline.is_rtl, "the default direction is ltr");
7974
7975 let nan_vp = get_inline_border_info(&sd, id, &st, &info, PhysicalSize::new(f32::NAN, f32::NAN))
7978 .expect("px borders do not depend on the viewport");
7979 assert!(nan_vp.top.is_finite() && nan_vp.padding_top.is_finite());
7980 }
7981
7982 #[test]
7983 fn get_style_properties_stays_finite_for_every_degenerate_viewport() {
7984 let sd = body_with_text("hello");
7985 for viewport in [
7986 PhysicalSize::new(0.0, 0.0),
7987 PhysicalSize::new(-1.0, -1.0),
7988 PhysicalSize::new(f32::NAN, f32::NAN),
7989 PhysicalSize::new(f32::INFINITY, f32::INFINITY),
7990 PhysicalSize::new(f32::MAX, f32::MAX),
7991 ] {
7992 for id in [NodeId::new(0), NodeId::new(1)] {
7993 let props = get_style_properties(&sd, id, None, viewport);
7994 assert!(
7995 props.font_size_px.is_finite(),
7996 "viewport {viewport:?} produced a non-finite font size"
7997 );
7998 assert!(props.font_size_px > 0.0);
7999 }
8000 }
8001 }
8002
8003 #[test]
8008 fn is_text_selectable_is_true_by_default_and_false_for_user_select_none() {
8009 let sd = body_with_divs(1, "");
8010 assert!(
8011 is_text_selectable(&sd, NodeId::new(1), &normal()),
8012 "text is selectable unless user-select says otherwise"
8013 );
8014
8015 let sd = body_with_divs(1, "div { user-select: none; }");
8016 assert!(!is_text_selectable(&sd, NodeId::new(1), &normal()));
8017
8018 let sd = body_with_divs(1, "div { user-select: text; }");
8019 assert!(is_text_selectable(&sd, NodeId::new(1), &normal()));
8020 }
8021
8022 #[test]
8023 fn contenteditable_is_false_everywhere_on_a_plain_dom() {
8024 let sd = body_with_divs(2, "");
8025 for idx in 0..sd.node_data.len() {
8026 let id = NodeId::new(idx);
8027 assert!(!is_node_contenteditable(&sd, id), "node {idx}");
8028 assert!(!is_node_contenteditable_inherited(&sd, id), "node {idx}");
8029 assert_eq!(find_contenteditable_ancestor(&sd, id), None, "node {idx}");
8030 }
8031 }
8032
8033 #[test]
8034 fn contenteditable_is_inherited_by_descendants_but_not_reported_as_direct() {
8035 let mut editable = Dom::create_div().with_children(vec![Dom::create_div()].into());
8037 editable.root.set_contenteditable(true);
8038 let mut dom = Dom::create_body().with_children(vec![editable].into());
8039 let sd = StyledDom::create(&mut dom, Css::empty());
8040 assert_eq!(sd.node_data.len(), 3);
8041
8042 let (body, editable, child) = (NodeId::new(0), NodeId::new(1), NodeId::new(2));
8043
8044 assert!(!is_node_contenteditable(&sd, body));
8045 assert!(is_node_contenteditable(&sd, editable));
8046 assert!(
8047 !is_node_contenteditable(&sd, child),
8048 "the direct check must not walk up the tree"
8049 );
8050
8051 assert!(!is_node_contenteditable_inherited(&sd, body));
8052 assert!(is_node_contenteditable_inherited(&sd, editable));
8053 assert!(
8054 is_node_contenteditable_inherited(&sd, child),
8055 "editability is inherited from the ancestor"
8056 );
8057
8058 assert_eq!(find_contenteditable_ancestor(&sd, body), None);
8059 assert_eq!(find_contenteditable_ancestor(&sd, editable), Some(editable));
8060 assert_eq!(
8061 find_contenteditable_ancestor(&sd, child),
8062 Some(editable),
8063 "a nested node resolves to its editable container, not to itself"
8064 );
8065 }
8066
8067 #[test]
8072 fn collect_used_codepoints_strips_ascii_while_the_all_variant_keeps_it() {
8073 let sd = body_with_text("aé漢🎉");
8075
8076 let non_ascii = collect_used_codepoints(&sd);
8077 assert_eq!(non_ascii.len(), 3, "the ASCII 'a' is dropped");
8078 assert!(non_ascii.contains(&0x00E9));
8079 assert!(non_ascii.contains(&0x6F22));
8080 assert!(non_ascii.contains(&0x0001_F389), "astral plane codepoint");
8081 assert!(!non_ascii.contains(&u32::from(b'a')));
8082
8083 let all = collect_used_codepoints_all(&sd);
8084 assert_eq!(all.len(), 4);
8085 assert!(all.contains(&'a'));
8086 assert!(all.contains(&'🎉'));
8087 }
8088
8089 #[test]
8090 fn collect_used_codepoints_dedupes_and_handles_empty_and_ascii_only_text() {
8091 let sd = body_with_text("ααα");
8093 assert_eq!(collect_used_codepoints(&sd).len(), 1);
8094
8095 let sd = body_with_text("");
8096 assert!(collect_used_codepoints(&sd).is_empty());
8097 assert!(collect_used_codepoints_all(&sd).is_empty());
8098
8099 let sd = body_with_divs(3, "");
8100 assert!(
8101 collect_used_codepoints(&sd).is_empty(),
8102 "element nodes carry no codepoints"
8103 );
8104
8105 let sd = body_with_text("plain ascii");
8106 assert!(collect_used_codepoints(&sd).is_empty());
8107 assert!(!collect_used_codepoints_all(&sd).is_empty());
8108 }
8109
8110 #[test]
8111 fn scripts_present_in_styled_dom_is_empty_for_ascii_and_bounded_by_the_default_set() {
8112 let ascii = body_with_text("hello world");
8113 assert!(
8114 scripts_present_in_styled_dom(&ascii).is_empty(),
8115 "an ASCII-only page must not drag in any unicode fallback script"
8116 );
8117
8118 let empty = StyledDom::default();
8119 assert!(scripts_present_in_styled_dom(&empty).is_empty());
8120
8121 let cjk = body_with_text("漢字");
8122 let scripts = scripts_present_in_styled_dom(&cjk);
8123 assert!(!scripts.is_empty(), "CJK text must report at least one script");
8124 assert!(
8125 scripts.len() <= DEFAULT_UNICODE_FALLBACK_SCRIPTS.len(),
8126 "the result is always a subset of the default script set"
8127 );
8128 for r in &scripts {
8129 assert!(r.start <= r.end, "a script range must not be inverted");
8130 }
8131 }
8132
8133 #[test]
8134 fn collect_font_stacks_from_styled_dom_keeps_its_index_map_consistent() {
8135 let platform = azul_css::system::Platform::current();
8136
8137 for sd in [
8138 StyledDom::default(),
8139 body_with_text("hello"),
8140 body_with_divs(3, "div { font-family: Iosevka, monospace; }"),
8141 ] {
8142 let collected = collect_font_stacks_from_styled_dom(&sd, &platform);
8143 assert_eq!(
8144 collected.hash_to_index.len(),
8145 collected.font_stacks.len(),
8146 "every recorded hash must map to exactly one stack"
8147 );
8148 for &idx in collected.hash_to_index.values() {
8149 assert!(
8150 idx < collected.font_stacks.len(),
8151 "hash_to_index points past the end of font_stacks"
8152 );
8153 }
8154 for stack in &collected.font_stacks {
8155 assert!(!stack.is_empty(), "an empty font stack is never recorded");
8156 }
8157 }
8158 }
8159}
8160
8161#[cfg(test)]
8162mod memory_font_tier_tests {
8163 use super::*;
8164 use crate::text3::cache::{MemoryFace, MemoryFontTier};
8165
8166 fn face(tier: MemoryFontTier) -> MemoryFace {
8167 MemoryFace {
8168 tier,
8169 font_match: rust_fontconfig::FontMatch {
8170 id: rust_fontconfig::FontId::new(),
8171 unicode_ranges: Vec::new(),
8172 fallbacks: Vec::new(),
8173 },
8174 weight: FcWeight::Normal,
8175 italic: false,
8176 oblique: false,
8177 stretch: rust_fontconfig::FcStretch::Normal,
8178 weight_axis: None,
8179 }
8180 }
8181
8182 fn split(
8183 stack: &[&str],
8184 registered: &[(&str, MemoryFontTier)],
8185 ) -> (Vec<String>, Vec<String>, Vec<String>) {
8186 let mut memory_families: HashMap<String, Vec<MemoryFace>> = HashMap::new();
8187 for (family, tier) in registered {
8188 memory_families
8189 .entry(rust_fontconfig::utils::normalize_family_name(family))
8190 .or_default()
8191 .push(face(*tier));
8192 }
8193 let families: Vec<String> = stack.iter().map(|s| (*s).to_string()).collect();
8194 let (primary, disk, fallback) =
8195 split_memory_matches(&families, &memory_families, FcWeight::Normal, false, false);
8196 (
8197 primary.into_iter().map(|g| g.css_name).collect(),
8198 disk,
8199 fallback.into_iter().map(|g| g.css_name).collect(),
8200 )
8201 }
8202
8203 #[test]
8205 fn a_primary_face_takes_the_family_from_the_disk() {
8206 let (primary, disk, fallback) =
8207 split(&["Helvetica"], &[("Helvetica", MemoryFontTier::Primary)]);
8208 assert_eq!(primary, ["Helvetica"]);
8209 assert!(disk.is_empty());
8210 assert!(fallback.is_empty());
8211 }
8212
8213 #[test]
8218 fn a_fallback_face_leaves_the_family_to_the_disk() {
8219 let (primary, disk, fallback) =
8220 split(&["sans-serif"], &[("sans-serif", MemoryFontTier::Fallback)]);
8221 assert!(primary.is_empty());
8222 assert_eq!(disk, ["sans-serif"], "the disk must still get first refusal");
8223 assert_eq!(fallback, ["sans-serif"]);
8224 }
8225
8226 #[test]
8229 fn both_tiers_can_appear_in_one_stack() {
8230 let (primary, disk, fallback) = split(
8231 &["Helvetica", "Arial", "sans-serif"],
8232 &[
8233 ("Helvetica", MemoryFontTier::Primary),
8234 ("sans-serif", MemoryFontTier::Fallback),
8235 ],
8236 );
8237 assert_eq!(primary, ["Helvetica"]);
8238 assert_eq!(disk, ["Arial", "sans-serif"]);
8239 assert_eq!(fallback, ["sans-serif"]);
8240 }
8241
8242 #[test]
8245 fn primary_beats_fallback_for_the_same_family() {
8246 let (primary, disk, fallback) = split(
8247 &["Helvetica"],
8248 &[
8249 ("Helvetica", MemoryFontTier::Fallback),
8250 ("Helvetica", MemoryFontTier::Primary),
8251 ],
8252 );
8253 assert_eq!(primary, ["Helvetica"]);
8254 assert!(disk.is_empty());
8255 assert!(fallback.is_empty());
8256 }
8257}