taffy/tree/cache.rs
1//! A cache for storing the results of layout computation
2use crate::geometry::Size;
3use crate::style::AvailableSpace;
4use crate::tree::{LayoutInput, LayoutOutput, RunMode};
5
6/// The number of cache entries for each node in the tree
7const CACHE_SIZE: usize = 9;
8
9/// Cached intermediate layout results
10#[derive(Debug, Clone, Copy, PartialEq)]
11#[cfg_attr(feature = "serde", derive(Serialize))]
12pub(crate) struct CacheEntry<T> {
13 /// The initial cached size of the node itself
14 known_dimensions: Size<Option<f32>>,
15 /// The initial cached size of the parent's node
16 available_space: Size<AvailableSpace>,
17 /// The cached size and baselines of the item
18 content: T,
19}
20
21/// A cache for caching the results of a sizing a Grid Item or Flexbox Item
22#[derive(Debug, Clone, PartialEq)]
23#[cfg_attr(feature = "serde", derive(Serialize))]
24pub struct Cache {
25 /// The cache entry for the node's final layout
26 final_layout_entry: Option<CacheEntry<LayoutOutput>>,
27 /// The cache entries for the node's preliminary size measurements
28 measure_entries: [Option<CacheEntry<Size<f32>>>; CACHE_SIZE],
29 /// Tracks if all cache entries are empty
30 is_empty: bool,
31}
32
33impl Default for Cache {
34 fn default() -> Self {
35 Self::new()
36 }
37}
38
39impl Cache {
40 /// Create a new empty cache
41 pub const fn new() -> Self {
42 Self { final_layout_entry: None, measure_entries: [None; CACHE_SIZE], is_empty: true }
43 }
44
45 /// Return the cache slot to cache the current computed result in
46 ///
47 /// ## Caching Strategy
48 ///
49 /// We need multiple cache slots, because a node's size is often queried by it's parent multiple times in the course of the layout
50 /// process, and we don't want later results to clobber earlier ones.
51 ///
52 /// The two variables that we care about when determining cache slot are:
53 ///
54 /// - How many "known_dimensions" are set. In the worst case, a node may be called first with neither dimension known, then with one
55 /// dimension known (either width of height - which doesn't matter for our purposes here), and then with both dimensions known.
56 /// - Whether unknown dimensions are being sized under a min-content or a max-content available space constraint (definite available space
57 /// shares a cache slot with max-content because a node will generally be sized under one or the other but not both).
58 ///
59 /// ## Cache slots:
60 ///
61 /// - Slot 0: Both known_dimensions were set
62 /// - Slots 1-4: 1 of 2 known_dimensions were set and:
63 /// - Slot 1: width but not height known_dimension was set and the other dimension was either a MaxContent or Definite available space constraintraint
64 /// - Slot 2: width but not height known_dimension was set and the other dimension was a MinContent constraint
65 /// - Slot 3: height but not width known_dimension was set and the other dimension was either a MaxContent or Definite available space constraintable space constraint
66 /// - Slot 4: height but not width known_dimension was set and the other dimension was a MinContent constraint
67 /// - Slots 5-8: Neither known_dimensions were set and:
68 /// - Slot 5: x-axis available space is MaxContent or Definite and y-axis available space is MaxContent or Definite
69 /// - Slot 6: x-axis available space is MaxContent or Definite and y-axis available space is MinContent
70 /// - Slot 7: x-axis available space is MinContent and y-axis available space is MaxContent or Definite
71 /// - Slot 8: x-axis available space is MinContent and y-axis available space is MinContent
72 #[inline]
73 fn compute_cache_slot(known_dimensions: Size<Option<f32>>, available_space: Size<AvailableSpace>) -> usize {
74 use AvailableSpace::{Definite, MaxContent, MinContent};
75
76 let has_known_width = known_dimensions.width.is_some();
77 let has_known_height = known_dimensions.height.is_some();
78
79 // Slot 0: Both known_dimensions were set
80 if has_known_width && has_known_height {
81 return 0;
82 }
83
84 // Slot 1: width but not height known_dimension was set and the other dimension was either a MaxContent or Definite available space constraint
85 // Slot 2: width but not height known_dimension was set and the other dimension was a MinContent constraint
86 if has_known_width && !has_known_height {
87 return 1 + (available_space.height == MinContent) as usize;
88 }
89
90 // Slot 3: height but not width known_dimension was set and the other dimension was either a MaxContent or Definite available space constraint
91 // Slot 4: height but not width known_dimension was set and the other dimension was a MinContent constraint
92 if has_known_height && !has_known_width {
93 return 3 + (available_space.width == MinContent) as usize;
94 }
95
96 // Slots 5-8: Neither known_dimensions were set and:
97 match (available_space.width, available_space.height) {
98 // Slot 5: x-axis available space is MaxContent or Definite and y-axis available space is MaxContent or Definite
99 (MaxContent | Definite(_), MaxContent | Definite(_)) => 5,
100 // Slot 6: x-axis available space is MaxContent or Definite and y-axis available space is MinContent
101 (MaxContent | Definite(_), MinContent) => 6,
102 // Slot 7: x-axis available space is MinContent and y-axis available space is MaxContent or Definite
103 (MinContent, MaxContent | Definite(_)) => 7,
104 // Slot 8: x-axis available space is MinContent and y-axis available space is MinContent
105 (MinContent, MinContent) => 8,
106 }
107 }
108
109 /// Try to retrieve a cached result from the cache
110 #[inline]
111 pub fn get(&self, input: &LayoutInput) -> Option<LayoutOutput> {
112 let known_dimensions = input.known_dimensions;
113 let available_space = input.available_space;
114
115 match input.run_mode {
116 RunMode::PerformLayout => self
117 .final_layout_entry
118 .filter(|entry| {
119 let cached_size = entry.content.size;
120 (known_dimensions.width == entry.known_dimensions.width
121 || known_dimensions.width == Some(cached_size.width))
122 && (known_dimensions.height == entry.known_dimensions.height
123 || known_dimensions.height == Some(cached_size.height))
124 && (known_dimensions.width.is_some()
125 || entry.available_space.width.is_roughly_equal(available_space.width))
126 && (known_dimensions.height.is_some()
127 || entry.available_space.height.is_roughly_equal(available_space.height))
128 })
129 .map(|e| e.content),
130 RunMode::ComputeSize => {
131 for entry in self.measure_entries.iter().flatten() {
132 let cached_size = entry.content;
133
134 if (known_dimensions.width == entry.known_dimensions.width
135 || known_dimensions.width == Some(cached_size.width))
136 && (known_dimensions.height == entry.known_dimensions.height
137 || known_dimensions.height == Some(cached_size.height))
138 && (known_dimensions.width.is_some()
139 || entry.available_space.width.is_roughly_equal(available_space.width))
140 && (known_dimensions.height.is_some()
141 || entry.available_space.height.is_roughly_equal(available_space.height))
142 {
143 return Some(LayoutOutput::from_outer_size(cached_size));
144 }
145 }
146
147 None
148 }
149 RunMode::PerformHiddenLayout => None,
150 }
151 }
152
153 /// Store a computed size in the cache
154 pub fn store(&mut self, input: &LayoutInput, layout_output: LayoutOutput) {
155 let known_dimensions = input.known_dimensions;
156 let available_space = input.available_space;
157
158 match input.run_mode {
159 RunMode::PerformLayout => {
160 self.is_empty = false;
161 self.final_layout_entry = Some(CacheEntry { known_dimensions, available_space, content: layout_output })
162 }
163 RunMode::ComputeSize => {
164 self.is_empty = false;
165 let cache_slot = Self::compute_cache_slot(known_dimensions, available_space);
166 self.measure_entries[cache_slot] =
167 Some(CacheEntry { known_dimensions, available_space, content: layout_output.size });
168 }
169 RunMode::PerformHiddenLayout => {}
170 }
171 }
172
173 /// Clear all cache entries and reports clear operation outcome ([`ClearState`])
174 pub fn clear(&mut self) -> ClearState {
175 if self.is_empty {
176 return ClearState::AlreadyEmpty;
177 }
178 self.is_empty = true;
179 self.final_layout_entry = None;
180 self.measure_entries = [None; CACHE_SIZE];
181 ClearState::Cleared
182 }
183
184 /// Returns true if all cache entries are None, else false
185 pub fn is_empty(&self) -> bool {
186 self.final_layout_entry.is_none() && !self.measure_entries.iter().any(|entry| entry.is_some())
187 }
188}
189
190/// Clear operation outcome. See [`Cache::clear`]
191pub enum ClearState {
192 /// Cleared some values
193 Cleared,
194 /// Everything was already cleared
195 AlreadyEmpty,
196}