1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
use markup5ever::{LocalName, local_name};
use taffy::{
AvailableSpace, BoxSizing, CoreStyle, LayoutInput, LayoutOutput, MaybeMath, MaybeResolve,
RequestedAxis, ResolveOrZero as _, RunMode, Size, SizingMode,
};
/// Whether an element is a replaced element laid out as a leaf box with an
/// intrinsic size. Note: `<object>` is deliberately excluded as its fallback
/// children should render when no resource is loaded.
pub(crate) fn is_replaced_element(tag_name: &LocalName) -> bool {
*tag_name == local_name!("img")
|| *tag_name == local_name!("svg")
|| *tag_name == local_name!("canvas")
|| *tag_name == local_name!("video")
|| *tag_name == local_name!("embed")
|| *tag_name == local_name!("iframe")
}
/// The intrinsic dimensions of a replaced element per CSS Images 3
/// (https://drafts.csswg.org/css-images/#intrinsic-dimensions): an intrinsic
/// width, an intrinsic height, and an intrinsic aspect ratio, each of which
/// may independently be absent.
#[derive(Debug, Clone, Copy, Default)]
pub struct IntrinsicSizes {
pub width: Option<f32>,
pub height: Option<f32>,
pub ratio: Option<f32>,
}
#[derive(Debug, Clone, Copy)]
pub struct ReplacedContext {
/// The element's intrinsic dimensions, each possibly absent.
pub intrinsic_sizes: IntrinsicSizes,
/// The default object size (https://drafts.csswg.org/css-images/#default-object-size)
/// used to fill in dimensions the intrinsic sizes leave unresolved. 300x150
/// for most replaced elements; zero for an image with no loaded resource.
pub default_object_size: taffy::Size<f32>,
}
/// Whether a height/width value is violating it's min- and max- constraints
/// The min- and max- constraints cannot both be violated because the max
/// constraint if floored by the min constraint (min constraint takes priority)
enum Violation {
/// Constraints are not violated
None,
/// Min constraint is violated
Min,
/// Max constraint is violated
Max,
}
pub fn compute_replaced_layout(
inputs: LayoutInput,
style: &impl CoreStyle,
resolve_calc_value: impl Fn(*const (), f32) -> f32,
context: &ReplacedContext,
) -> LayoutOutput {
let LayoutInput {
known_dimensions,
parent_size,
available_space,
sizing_mode,
run_mode,
axis: requested_axis,
..
} = inputs;
let padding = style
.padding()
.resolve_or_zero(parent_size.width, &resolve_calc_value);
let border = style
.border()
.resolve_or_zero(parent_size.width, &resolve_calc_value);
let padding_border = padding + border;
let pb_sum = Size {
width: padding_border.left + padding_border.right,
height: padding_border.top + padding_border.bottom,
};
let box_sizing_adjustment = if style.box_sizing() == BoxSizing::BorderBox {
pb_sum
} else {
Size::ZERO
};
// The scrollable overflow rect is measured from the scroll origin at the
// padding-box corner: the content-box size offset by the start padding. A
// scroll container's own padding at the end of the content is part of its
// scrollable overflow region, so it is included. Boxes that are not scroll
// containers do not extend their overflow region by their own padding.
let is_scroll_container = {
let overflow = style.overflow();
overflow.x.is_scroll_container() || overflow.y.is_scroll_container()
};
let is_rtl = style.direction() == taffy::Direction::Rtl;
let overflow_rect_for = |content_box_size: Size<f32>| {
let start_padding = if is_rtl { padding.right } else { padding.left };
let end_padding = if is_rtl { padding.left } else { padding.right };
taffy::Rect {
left: 0.0,
right: start_padding
+ content_box_size.width
+ if is_scroll_container {
end_padding
} else {
0.0
},
top: 0.0,
bottom: padding.top
+ content_box_size.height
+ if is_scroll_container {
padding.bottom
} else {
0.0
},
}
};
// Return early if both width and height are known
if run_mode == RunMode::ComputeSize {
if let Size {
width: Some(width),
height: Some(height),
} = known_dimensions
{
return LayoutOutput::from_outer_size(Size {
width: width.max(pb_sum.width),
height: height.max(pb_sum.height),
});
}
}
// Use aspect_ratio from style, fall back to inherent aspect ratio (if any).
//
// A degenerate ratio -- zero, infinite, or NaN -- is discarded. Transferring
// such a ratio between axes computes `width / ratio` or `height * ratio`,
// which yields an infinite (ratio == 0) or NaN (ratio is NaN) size in the
// other axis. CSS Sizing 4 4.1 says a degenerate ratio behaves as `auto`,
// so each source is filtered *before* the fallback: a degenerate style
// ratio falls back to the inherent one (that is what `auto` means for a
// replaced element), and only if both are degenerate or absent does the
// element get no preferred aspect ratio at all.
let is_usable_ratio = |ratio: &f32| ratio.is_finite() && *ratio > 0.0;
let intrinsic = context.intrinsic_sizes;
let aspect_ratio: Option<f32> = style
.aspect_ratio()
.filter(is_usable_ratio)
.or(intrinsic.ratio.filter(is_usable_ratio));
// Concrete object size per the CSS default sizing algorithm with no
// specified size (https://drafts.csswg.org/css-images/#default-sizing):
// missing intrinsic dimensions are derived from the aspect ratio when
// possible, otherwise taken from the default object size. An element with
// only an intrinsic aspect ratio uses the stretch-fit width in normal flow
// (CSS2 §10.3.2) when the available width is definite; shrink-to-fit
// contexts (min/max-content) contain it within the default object size.
//
// Only the intrinsic ratio participates here: the `aspect-ratio` property
// affects the sizing of the box, not the natural dimensions of its content.
let intrinsic_ratio = intrinsic.ratio.filter(is_usable_ratio);
let default_size = context.default_object_size;
let inherent_size = match (intrinsic.width, intrinsic.height) {
(Some(w), Some(h)) => Size {
width: w,
height: h,
},
(Some(w), None) => Size {
width: w,
height: intrinsic_ratio
.map(|r| w / r)
.unwrap_or(default_size.height),
},
(None, Some(h)) => Size {
width: intrinsic_ratio.map(|r| h * r).unwrap_or(default_size.width),
height: h,
},
(None, None) => match intrinsic_ratio {
Some(ratio) => {
if let AvailableSpace::Definite(available_width) = available_space.width {
Size {
width: available_width,
height: available_width / ratio,
}
} else {
// Contain within the default object size
let scale = (default_size.width / ratio).min(default_size.height);
Size {
width: scale * ratio,
height: scale,
}
}
}
None => default_size,
},
};
// See https://www.w3.org/TR/css-sizing-3/#replaced-percentage-min-contribution
let basis_for_max_and_preferred = Size {
width: if available_space.width == AvailableSpace::MinContent {
Some(0.0)
} else {
parent_size.width
},
height: if available_space.height == AvailableSpace::MinContent {
Some(0.0)
} else {
parent_size.height
},
};
// Resolve sizes
let style_size = style
.size()
.maybe_resolve(basis_for_max_and_preferred, &resolve_calc_value)
.maybe_sub(box_sizing_adjustment);
let mut min_size = style
.min_size()
.maybe_resolve(parent_size, &resolve_calc_value)
.maybe_sub(box_sizing_adjustment);
let max_size = style
.max_size()
.maybe_resolve(basis_for_max_and_preferred, &resolve_calc_value)
.or(available_space.into_options())
.maybe_min(available_space.into_options())
.maybe_max(min_size)
.maybe_sub(box_sizing_adjustment);
// For ContentSize mode, ignore preferred/min size styles in the axis being measured: the
// parent layout algorithm applies them itself, and content-based measurement should return
// the content size (the intrinsic size for replaced elements). Constraints in the opposite
// axis are retained as they transfer through the aspect ratio (transferred size suggestion).
let mut style_size = style_size;
if sizing_mode == SizingMode::ContentSize {
match requested_axis {
RequestedAxis::Horizontal => {
style_size.width = None;
min_size.width = None;
}
RequestedAxis::Vertical => {
style_size.height = None;
min_size.height = None;
}
RequestedAxis::Both => {}
}
}
// Known dimensions are the parent's current sizing inputs. Clamp them before transferring
// an aspect ratio so provisional cross-axis sizes do not bypass replaced-element limits.
if known_dimensions.width.is_some() | known_dimensions.height.is_some() {
// Style max sizes without the available-space fallback: available space must not
// constrain the aspect-ratio transfer of parent-resolved dimensions.
let style_max_size = style
.max_size()
.maybe_resolve(basis_for_max_and_preferred, &resolve_calc_value)
.maybe_sub(box_sizing_adjustment)
.maybe_max(min_size);
let content_box_known_dimensions = known_dimensions.maybe_sub(pb_sum);
let transferred = content_box_known_dimensions
.maybe_clamp(min_size, style_max_size)
.maybe_apply_aspect_ratio(aspect_ratio)
.unwrap_or(inherent_size);
// Known axes are authoritative (already resolved by the parent); only the axis
// derived via aspect-ratio transfer (or falling back to the intrinsic size) is
// clamped by this element's min/max sizes.
let size = content_box_known_dimensions
.unwrap_or(transferred.maybe_clamp(min_size, style_max_size));
let size = size.map(|s| s.max(0.0));
return LayoutOutput::from_sizes(size + pb_sum, overflow_rect_for(size));
}
let unclamped_size = if style_size.width.is_some() | style_size.height.is_some() {
style_size
.maybe_apply_aspect_ratio(aspect_ratio)
.unwrap_or(inherent_size)
} else {
inherent_size
};
// Floor size at zero
let size = unclamped_size.map(|s| s.max(0.0));
// Violations
let width_violation = if size.width < min_size.width.unwrap_or(0.0) {
Violation::Min
} else if size.width > max_size.width.unwrap_or(f32::INFINITY) {
Violation::Max
} else {
Violation::None
};
let height_violation = if size.height < min_size.height.unwrap_or(0.0) {
Violation::Min
} else if size.height > max_size.height.unwrap_or(f32::INFINITY) {
Violation::Max
} else {
Violation::None
};
// Without an intrinsic aspect ratio, each axis is clamped independently
let Some(aspect_ratio) = aspect_ratio else {
let size = size.maybe_clamp(min_size, max_size);
return LayoutOutput::from_sizes(size + pb_sum, overflow_rect_for(size));
};
let inv_aspect_ratio = 1.0 / aspect_ratio;
// Clamp following rules in table at
// https://www.w3.org/TR/CSS22/visudet.html#min-max-widths
let size = match (width_violation, height_violation) {
// No constraint violation
(Violation::None, Violation::None) => size,
// w > max-width
(Violation::Max, Violation::None) => {
let max_width = max_size.width.unwrap();
Size {
width: max_width,
height: (max_width * inv_aspect_ratio).maybe_max(min_size.height),
}
}
// w < min-width
(Violation::Min, Violation::None) => {
let min_width = min_size.width.unwrap();
Size {
width: min_width,
height: (min_width * inv_aspect_ratio).maybe_min(max_size.height),
}
}
// h > max-height
(Violation::None, Violation::Max) => {
let max_height = max_size.height.unwrap();
Size {
width: (max_height * aspect_ratio).maybe_max(min_size.width),
height: max_height,
}
}
// h < min-height
(Violation::None, Violation::Min) => {
let min_height = min_size.height.unwrap();
Size {
width: (min_height * aspect_ratio).maybe_min(max_size.width),
height: min_height,
}
}
// (w > max-width) and (h > max-height)
(Violation::Max, Violation::Max) => {
let max_width = max_size.width.unwrap();
let max_height = max_size.height.unwrap();
if max_width / size.width <= max_height / size.height {
Size {
width: max_width,
height: (max_width * inv_aspect_ratio).maybe_max(min_size.height),
}
} else {
Size {
width: (max_height * aspect_ratio).maybe_max(min_size.width),
height: max_height,
}
}
}
// (w < min-width) and (h < min-height)
(Violation::Min, Violation::Min) => {
let min_width = min_size.width.unwrap();
let min_height = min_size.height.unwrap();
if min_width / size.width <= min_height / size.height {
Size {
width: (min_height * aspect_ratio).maybe_min(max_size.width),
height: min_height,
}
} else {
Size {
width: min_width,
height: (min_width * inv_aspect_ratio).maybe_min(max_size.height),
}
}
}
// (w < min-width) and (h > max-height)
(Violation::Min, Violation::Max) => {
let min_width = min_size.width.unwrap();
let max_height = max_size.height.unwrap();
Size {
width: min_width,
height: max_height,
}
}
// (w < min-width) and (h > max-height)
(Violation::Max, Violation::Min) => {
let max_width = max_size.width.unwrap();
let min_height = min_size.height.unwrap();
Size {
width: max_width,
height: min_height,
}
}
};
LayoutOutput::from_sizes(size + pb_sum, overflow_rect_for(size))
}