blitz-pdf 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -9
- package/THIRD_PARTY_NOTICES.md +20 -0
- package/dist/types.d.ts +2 -1
- package/dist/types.d.ts.map +1 -1
- package/licenses/Noto-OFL.txt +94 -0
- package/licenses/NotoCJK-OFL.txt +92 -0
- package/licenses/NotoEmoji-OFL.txt +93 -0
- package/licenses/Unifont-OFL.txt +90 -0
- package/licenses/font-sources.json +106 -0
- package/package.json +1 -1
- package/src/types.ts +2 -1
- package/vendor/README.md +43 -0
- package/vendor/blitz-paint/Cargo.toml +50 -0
- package/vendor/blitz-paint/LICENSE-APACHE +176 -0
- package/vendor/blitz-paint/LICENSE-MIT +23 -0
- package/vendor/blitz-paint/src/color.rs +66 -0
- package/vendor/blitz-paint/src/debug_overlay.rs +168 -0
- package/vendor/blitz-paint/src/filters.rs +40 -0
- package/vendor/blitz-paint/src/gradient.rs +536 -0
- package/vendor/blitz-paint/src/kurbo_css/css_box.rs +847 -0
- package/vendor/blitz-paint/src/kurbo_css/mod.rs +77 -0
- package/vendor/blitz-paint/src/kurbo_css/non_uniform_radii.rs +60 -0
- package/vendor/blitz-paint/src/layers.rs +95 -0
- package/vendor/blitz-paint/src/lib.rs +190 -0
- package/vendor/blitz-paint/src/render/background.rs +968 -0
- package/vendor/blitz-paint/src/render/border.rs +631 -0
- package/vendor/blitz-paint/src/render/box_shadow.rs +157 -0
- package/vendor/blitz-paint/src/render/clip_path.rs +492 -0
- package/vendor/blitz-paint/src/render/form_controls.rs +103 -0
- package/vendor/blitz-paint/src/render/mask.rs +144 -0
- package/vendor/blitz-paint/src/render.rs +1351 -0
- package/vendor/blitz-paint/src/sizing.rs +63 -0
- package/vendor/blitz-paint/src/text.rs +747 -0
- package/vendor/fontique/Cargo.toml +194 -0
- package/vendor/fontique/LICENSE-APACHE +201 -0
- package/vendor/fontique/LICENSE-MIT +25 -0
- package/vendor/fontique/README.md +54 -0
- package/vendor/fontique/examples/generic_families.rs +37 -0
- package/vendor/fontique/src/attributes.rs +42 -0
- package/vendor/fontique/src/backend/android.rs +218 -0
- package/vendor/fontique/src/backend/coretext.rs +206 -0
- package/vendor/fontique/src/backend/dwrite.rs +395 -0
- package/vendor/fontique/src/backend/fontconfig.rs +844 -0
- package/vendor/fontique/src/backend/mod.rs +75 -0
- package/vendor/fontique/src/charmap.rs +127 -0
- package/vendor/fontique/src/collection/mod.rs +1016 -0
- package/vendor/fontique/src/collection/query.rs +392 -0
- package/vendor/fontique/src/fallback.rs +293 -0
- package/vendor/fontique/src/family.rs +111 -0
- package/vendor/fontique/src/family_name.rs +153 -0
- package/vendor/fontique/src/font.rs +562 -0
- package/vendor/fontique/src/generic.rs +36 -0
- package/vendor/fontique/src/impl_fontconfig.rs +159 -0
- package/vendor/fontique/src/lib.rs +87 -0
- package/vendor/fontique/src/matching.rs +483 -0
- package/vendor/fontique/src/scan.rs +312 -0
- package/vendor/fontique/src/script.rs +271 -0
- package/vendor/fontique/src/source.rs +108 -0
- package/vendor/fontique/src/source_cache.rs +254 -0
- package/wasm/blitz_pdf_bg.wasm +0 -0
|
@@ -0,0 +1,968 @@
|
|
|
1
|
+
use super::{ElementCx, to_image_quality, to_peniko_image};
|
|
2
|
+
use crate::color::{Color, ToColorColor};
|
|
3
|
+
use crate::gradient::to_peniko_gradient;
|
|
4
|
+
use anyrender::PaintScene;
|
|
5
|
+
use blitz_dom::NodeId;
|
|
6
|
+
use blitz_dom::node::{ImageData, ImageResourceData, SpecialElementData};
|
|
7
|
+
use kurbo::{self, Affine, BezPath, Point, Rect, Shape, Size, Vec2};
|
|
8
|
+
use peniko::{self, Fill};
|
|
9
|
+
use style::{
|
|
10
|
+
computed_values::border_collapse::T as BorderCollapse,
|
|
11
|
+
properties::{
|
|
12
|
+
generated::longhands::{
|
|
13
|
+
background_attachment::single_value::computed_value::T as StyloBackgroundAttachment,
|
|
14
|
+
background_clip::single_value::computed_value::T as StyloBackgroundClip,
|
|
15
|
+
background_origin::single_value::computed_value::T as StyloBackgroundOrigin,
|
|
16
|
+
mask_origin::single_value::computed_value::T as StyloMaskOrigin,
|
|
17
|
+
},
|
|
18
|
+
style_structs::{Background, SVG},
|
|
19
|
+
},
|
|
20
|
+
values::{
|
|
21
|
+
computed::{
|
|
22
|
+
BackgroundRepeat, Gradient as StyloGradient, Image as ComputedImage, LengthPercentage,
|
|
23
|
+
background::BackgroundSize,
|
|
24
|
+
},
|
|
25
|
+
generics::image::GenericImage,
|
|
26
|
+
specified::background::BackgroundRepeatKeyword,
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
#[cfg(feature = "tracing")]
|
|
31
|
+
use tracing::warn;
|
|
32
|
+
|
|
33
|
+
/// A box from the CSS box model. Abstracts over the (structurally identical)
|
|
34
|
+
/// computed value types of the `background-clip`/`background-origin` and
|
|
35
|
+
/// `mask-clip`/`mask-origin` properties.
|
|
36
|
+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
37
|
+
#[allow(clippy::enum_variant_names)] // The variants are named after the CSS keywords
|
|
38
|
+
pub(super) enum BoxModelBox {
|
|
39
|
+
BorderBox,
|
|
40
|
+
PaddingBox,
|
|
41
|
+
ContentBox,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Also covers MaskClip as the type is the same
|
|
45
|
+
impl From<StyloBackgroundClip> for BoxModelBox {
|
|
46
|
+
fn from(value: StyloBackgroundClip) -> Self {
|
|
47
|
+
match value {
|
|
48
|
+
StyloBackgroundClip::BorderBox => Self::BorderBox,
|
|
49
|
+
StyloBackgroundClip::PaddingBox => Self::PaddingBox,
|
|
50
|
+
StyloBackgroundClip::ContentBox => Self::ContentBox,
|
|
51
|
+
|
|
52
|
+
// TODO: support BorderArea
|
|
53
|
+
StyloBackgroundClip::BorderArea => Self::BorderBox,
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
impl From<StyloBackgroundOrigin> for BoxModelBox {
|
|
59
|
+
fn from(value: StyloBackgroundOrigin) -> Self {
|
|
60
|
+
match value {
|
|
61
|
+
StyloBackgroundOrigin::BorderBox => Self::BorderBox,
|
|
62
|
+
StyloBackgroundOrigin::PaddingBox => Self::PaddingBox,
|
|
63
|
+
StyloBackgroundOrigin::ContentBox => Self::ContentBox,
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
impl From<StyloMaskOrigin> for BoxModelBox {
|
|
69
|
+
fn from(value: StyloMaskOrigin) -> Self {
|
|
70
|
+
match value {
|
|
71
|
+
StyloMaskOrigin::BorderBox => Self::BorderBox,
|
|
72
|
+
StyloMaskOrigin::PaddingBox => Self::PaddingBox,
|
|
73
|
+
StyloMaskOrigin::ContentBox => Self::ContentBox,
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/// The styles and image data for a single layer of a CSS image layer list
|
|
79
|
+
/// (`background-image` or `mask-image`). The `background-*` and `mask-*`
|
|
80
|
+
/// properties share computed value types, which allows the layer painting code
|
|
81
|
+
/// to be shared.
|
|
82
|
+
pub(super) struct ImageLayerStyles<'a> {
|
|
83
|
+
/// The computed value of the `background-image`/`mask-image` layer
|
|
84
|
+
pub stylo_image: &'a ComputedImage,
|
|
85
|
+
/// The loaded image resource if `stylo_image` is a `url()` image
|
|
86
|
+
pub image_data: Option<&'a ImageResourceData>,
|
|
87
|
+
pub position_x: &'a LengthPercentage,
|
|
88
|
+
pub position_y: &'a LengthPercentage,
|
|
89
|
+
pub repeat: &'a BackgroundRepeat,
|
|
90
|
+
pub size: &'a BackgroundSize,
|
|
91
|
+
pub clip: BoxModelBox,
|
|
92
|
+
pub origin: BoxModelBox,
|
|
93
|
+
pub attachment: StyloBackgroundAttachment,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
impl<'a> ImageLayerStyles<'a> {
|
|
97
|
+
pub(super) fn from_background(
|
|
98
|
+
bg_styles: &'a Background,
|
|
99
|
+
image_data: &'a [Option<ImageResourceData>],
|
|
100
|
+
idx: usize,
|
|
101
|
+
) -> Self {
|
|
102
|
+
Self {
|
|
103
|
+
stylo_image: &bg_styles.background_image.0[idx],
|
|
104
|
+
image_data: image_data.get(idx).and_then(Option::as_ref),
|
|
105
|
+
position_x: get_cyclic(&bg_styles.background_position_x.0, idx),
|
|
106
|
+
position_y: get_cyclic(&bg_styles.background_position_y.0, idx),
|
|
107
|
+
repeat: get_cyclic(&bg_styles.background_repeat.0, idx),
|
|
108
|
+
size: get_cyclic(&bg_styles.background_size.0, idx),
|
|
109
|
+
clip: (*get_cyclic(&bg_styles.background_clip.0, idx)).into(),
|
|
110
|
+
origin: (*get_cyclic(&bg_styles.background_origin.0, idx)).into(),
|
|
111
|
+
attachment: *get_cyclic(&bg_styles.background_attachment.0, idx),
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
pub(super) fn from_svg(
|
|
116
|
+
svg_styles: &'a SVG,
|
|
117
|
+
image_data: &'a [Option<ImageResourceData>],
|
|
118
|
+
idx: usize,
|
|
119
|
+
) -> Self {
|
|
120
|
+
Self {
|
|
121
|
+
stylo_image: &svg_styles.mask_image.0[idx],
|
|
122
|
+
image_data: image_data.get(idx).and_then(Option::as_ref),
|
|
123
|
+
position_x: get_cyclic(&svg_styles.mask_position_x.0, idx),
|
|
124
|
+
position_y: get_cyclic(&svg_styles.mask_position_y.0, idx),
|
|
125
|
+
repeat: get_cyclic(&svg_styles.mask_repeat.0, idx),
|
|
126
|
+
size: get_cyclic(&svg_styles.mask_size.0, idx),
|
|
127
|
+
clip: (*get_cyclic(&svg_styles.mask_clip.0, idx)).into(),
|
|
128
|
+
origin: (*get_cyclic(&svg_styles.mask_origin.0, idx)).into(),
|
|
129
|
+
// There is no `mask-attachment` property
|
|
130
|
+
attachment: StyloBackgroundAttachment::Scroll,
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
impl ElementCx<'_, '_> {
|
|
136
|
+
pub(super) fn draw_background(&self, scene: &mut impl PaintScene) {
|
|
137
|
+
let bg_styles = &self.style.get_background();
|
|
138
|
+
let image_data = &self.element.background_images;
|
|
139
|
+
let layer_count = bg_styles.background_image.0.len();
|
|
140
|
+
|
|
141
|
+
// The background color is clipped by the clip of the last layer in the list
|
|
142
|
+
let background_clip: BoxModelBox =
|
|
143
|
+
(*get_cyclic(&bg_styles.background_clip.0, layer_count - 1)).into();
|
|
144
|
+
let background_clip_path = self.box_path(background_clip);
|
|
145
|
+
|
|
146
|
+
// Draw background color (if any)
|
|
147
|
+
self.draw_solid_bg(scene, &background_clip_path);
|
|
148
|
+
|
|
149
|
+
for idx in (0..layer_count).rev() {
|
|
150
|
+
let layer = ImageLayerStyles::from_background(bg_styles, image_data, idx);
|
|
151
|
+
if layer_paints_nothing(&layer) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
let background_clip_path = self.box_path(layer.clip);
|
|
155
|
+
|
|
156
|
+
self.context.layer_manager.maybe_with_layer(
|
|
157
|
+
scene,
|
|
158
|
+
true,
|
|
159
|
+
1.0,
|
|
160
|
+
self.transform,
|
|
161
|
+
&background_clip_path,
|
|
162
|
+
None,
|
|
163
|
+
None,
|
|
164
|
+
|scene| {
|
|
165
|
+
self.draw_image_layer(scene, &layer);
|
|
166
|
+
},
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/// The path of the given CSS box model box for this element
|
|
172
|
+
pub(super) fn box_path(&self, css_box: BoxModelBox) -> BezPath {
|
|
173
|
+
match css_box {
|
|
174
|
+
BoxModelBox::BorderBox => self.frame.border_box_path(),
|
|
175
|
+
BoxModelBox::PaddingBox => self.frame.padding_box_path(),
|
|
176
|
+
BoxModelBox::ContentBox => self.frame.content_box_path(),
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/// The rect of the given CSS box model box for this element
|
|
181
|
+
fn box_rect(&self, css_box: BoxModelBox) -> Rect {
|
|
182
|
+
match css_box {
|
|
183
|
+
BoxModelBox::BorderBox => self.frame.border_box,
|
|
184
|
+
BoxModelBox::PaddingBox => self.frame.padding_box,
|
|
185
|
+
BoxModelBox::ContentBox => self.frame.content_box,
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/// Draw a single layer of a CSS image layer list (`background-image` or `mask-image`)
|
|
190
|
+
pub(super) fn draw_image_layer(&self, scene: &mut impl PaintScene, layer: &ImageLayerStyles) {
|
|
191
|
+
match layer.stylo_image {
|
|
192
|
+
GenericImage::None => {
|
|
193
|
+
// Do nothing
|
|
194
|
+
}
|
|
195
|
+
GenericImage::Gradient(gradient) => self.draw_gradient_layer(scene, gradient, layer),
|
|
196
|
+
GenericImage::Url(_) => {
|
|
197
|
+
self.draw_raster_image_layer(scene, layer);
|
|
198
|
+
#[cfg(feature = "svg")]
|
|
199
|
+
self.draw_svg_image_layer(scene, layer);
|
|
200
|
+
}
|
|
201
|
+
GenericImage::LightDark(_) => {
|
|
202
|
+
#[cfg(feature = "tracing")]
|
|
203
|
+
warn!("Implement image layer drawing for ImageLightDark")
|
|
204
|
+
}
|
|
205
|
+
GenericImage::PaintWorklet(_) => {
|
|
206
|
+
#[cfg(feature = "tracing")]
|
|
207
|
+
warn!("Implement image layer drawing for Image::PaintWorklet")
|
|
208
|
+
}
|
|
209
|
+
GenericImage::CrossFade(_) => {
|
|
210
|
+
#[cfg(feature = "tracing")]
|
|
211
|
+
warn!("Implement image layer drawing for Image::CrossFade")
|
|
212
|
+
}
|
|
213
|
+
GenericImage::Image(_) => {
|
|
214
|
+
#[cfg(feature = "tracing")]
|
|
215
|
+
warn!("Implement image layer drawing for Image::Image")
|
|
216
|
+
}
|
|
217
|
+
GenericImage::ImageSet(_) => {
|
|
218
|
+
#[cfg(feature = "tracing")]
|
|
219
|
+
warn!("Implement image layer drawing for Image::ImageSet")
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/// Paint the backgrounds of the table's column groups, columns, row groups and rows
|
|
225
|
+
/// (in that order, bottom to top: CSS 2.1 §17.5.1).
|
|
226
|
+
///
|
|
227
|
+
/// These backgrounds are only painted behind the table's cells (not in the
|
|
228
|
+
/// `border-spacing` between them, nor where a row has no cell), so they are
|
|
229
|
+
/// painted cell by cell.
|
|
230
|
+
pub(super) fn draw_table_row_backgrounds(&self, scene: &mut impl PaintScene) {
|
|
231
|
+
let SpecialElementData::TableRoot(table) = &self.element.special_data else {
|
|
232
|
+
return;
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
let background_color = |node_id: NodeId| -> Option<Color> {
|
|
236
|
+
let node = self.context.dom.get_node(node_id)?;
|
|
237
|
+
let style = node.primary_styles()?;
|
|
238
|
+
let current_color = style.clone_color();
|
|
239
|
+
let color = style
|
|
240
|
+
.get_background()
|
|
241
|
+
.background_color
|
|
242
|
+
.resolve_to_absolute(¤t_color)
|
|
243
|
+
.as_srgb_color();
|
|
244
|
+
(color != Color::TRANSPARENT).then_some(color)
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
// Resolve the (group, track) background colors once per track rather than once per cell
|
|
248
|
+
let column_colors: Vec<[Option<Color>; 2]> = table
|
|
249
|
+
.columns
|
|
250
|
+
.iter()
|
|
251
|
+
.map(|col| {
|
|
252
|
+
[
|
|
253
|
+
col.group.and_then(background_color),
|
|
254
|
+
background_color(col.node_id),
|
|
255
|
+
]
|
|
256
|
+
})
|
|
257
|
+
.collect();
|
|
258
|
+
let row_colors: Vec<[Option<Color>; 2]> = table
|
|
259
|
+
.rows
|
|
260
|
+
.iter()
|
|
261
|
+
.map(|row| {
|
|
262
|
+
[
|
|
263
|
+
row.group.and_then(background_color),
|
|
264
|
+
background_color(row.node_id),
|
|
265
|
+
]
|
|
266
|
+
})
|
|
267
|
+
.collect();
|
|
268
|
+
let has_color =
|
|
269
|
+
|colors: &[[Option<Color>; 2]]| colors.iter().flatten().any(Option::is_some);
|
|
270
|
+
if !has_color(&column_colors) && !has_color(&row_colors) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
let cell_rect = |cell_id: NodeId| -> Option<Rect> {
|
|
275
|
+
let layout = self.context.dom.get_node(cell_id)?.final_layout();
|
|
276
|
+
Some(Rect::new(
|
|
277
|
+
layout.location.x as f64,
|
|
278
|
+
layout.location.y as f64,
|
|
279
|
+
(layout.location.x + layout.size.width) as f64,
|
|
280
|
+
(layout.location.y + layout.size.height) as f64,
|
|
281
|
+
))
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
// In the collapsed borders model the borders are laid out as gutters between the
|
|
285
|
+
// cells, but a cell's border box (and so its background) extends to the middle of
|
|
286
|
+
// the borders around it. The gutter is split on a whole pixel so that the
|
|
287
|
+
// backgrounds of adjacent cells tile exactly.
|
|
288
|
+
let collapsed = table.border_collapse == BorderCollapse::Collapse;
|
|
289
|
+
let (gutter, bounds) = if collapsed {
|
|
290
|
+
let gap = table.style.gap.map(|gap| gap.into_raw().value() as f64);
|
|
291
|
+
let bounds = table
|
|
292
|
+
.cells
|
|
293
|
+
.iter()
|
|
294
|
+
.filter_map(|cell| cell_rect(cell.node_id))
|
|
295
|
+
.reduce(|a, b| a.union(b));
|
|
296
|
+
(Size::new(gap.width, gap.height), bounds)
|
|
297
|
+
} else {
|
|
298
|
+
(Size::ZERO, None)
|
|
299
|
+
};
|
|
300
|
+
let before = Size::new((gutter.width / 2.0).ceil(), (gutter.height / 2.0).ceil());
|
|
301
|
+
let after = gutter - before;
|
|
302
|
+
|
|
303
|
+
for cell in &table.cells {
|
|
304
|
+
let no_colors = [None, None];
|
|
305
|
+
let column = column_colors
|
|
306
|
+
.get(cell.column as usize)
|
|
307
|
+
.unwrap_or(&no_colors);
|
|
308
|
+
let row = row_colors.get(cell.row as usize).unwrap_or(&no_colors);
|
|
309
|
+
let mut colors = column.iter().chain(row).flatten().peekable();
|
|
310
|
+
if colors.peek().is_none() {
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
let Some(mut rect) = cell_rect(cell.node_id) else {
|
|
314
|
+
continue;
|
|
315
|
+
};
|
|
316
|
+
if let Some(bounds) = bounds {
|
|
317
|
+
rect = Rect::new(
|
|
318
|
+
rect.x0 - before.width,
|
|
319
|
+
rect.y0 - before.height,
|
|
320
|
+
rect.x1 + after.width,
|
|
321
|
+
rect.y1 + after.height,
|
|
322
|
+
)
|
|
323
|
+
.intersect(bounds);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
let shape = rect.scale_from_origin(self.scale);
|
|
327
|
+
for color in colors {
|
|
328
|
+
scene.fill(Fill::NonZero, self.transform, *color, None, &shape);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
fn draw_solid_bg(&self, scene: &mut impl PaintScene, shape: &BezPath) {
|
|
334
|
+
let current_color = self.style.clone_color();
|
|
335
|
+
let background_color = &self.style.get_background().background_color;
|
|
336
|
+
let bg_color = background_color
|
|
337
|
+
.resolve_to_absolute(¤t_color)
|
|
338
|
+
.as_srgb_color();
|
|
339
|
+
|
|
340
|
+
if bg_color != Color::TRANSPARENT {
|
|
341
|
+
// Fill the color
|
|
342
|
+
scene.fill(Fill::NonZero, self.transform, bg_color, None, shape);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/// Whether the layer is positioned against the viewport
|
|
347
|
+
/// (`background-attachment: fixed`) rather than the element's origin box.
|
|
348
|
+
/// `fixed` behaves as `scroll` on elements affected by a CSS transform
|
|
349
|
+
/// (the transformed element acts as the layer's containing block).
|
|
350
|
+
fn layer_is_fixed(&self, layer: &ImageLayerStyles) -> bool {
|
|
351
|
+
layer.attachment == StyloBackgroundAttachment::Fixed && !self.is_transformed()
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/// The background positioning area and the transform from its coordinate
|
|
355
|
+
/// space to the scene for a fixed layer: the viewport, unaffected by any
|
|
356
|
+
/// scrolling.
|
|
357
|
+
fn fixed_positioning_area(&self) -> (Rect, Affine) {
|
|
358
|
+
let viewport_rect = Rect::new(
|
|
359
|
+
0.0,
|
|
360
|
+
0.0,
|
|
361
|
+
self.context.width as f64,
|
|
362
|
+
self.context.height as f64,
|
|
363
|
+
);
|
|
364
|
+
let transform = Affine::translate((self.context.initial_x, self.context.initial_y));
|
|
365
|
+
(viewport_rect, transform)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/// Whether this element or any of its ancestors has a CSS transform
|
|
369
|
+
///
|
|
370
|
+
/// TODO: this misses transformed elements whose resolved transform is the
|
|
371
|
+
/// identity (e.g. `transform: translate(0)`), and elements with
|
|
372
|
+
/// `will-change: transform`, both of which should also degrade `fixed`
|
|
373
|
+
/// to `scroll` (see WPT css/css-transforms/transform-fixed-bg-005/008)
|
|
374
|
+
fn is_transformed(&self) -> bool {
|
|
375
|
+
let mut current = Some(self.node.id);
|
|
376
|
+
while let Some(node) = current.and_then(|id| self.context.dom.get_node(id)) {
|
|
377
|
+
if node.transform().is_some() {
|
|
378
|
+
return true;
|
|
379
|
+
}
|
|
380
|
+
current = node.parent;
|
|
381
|
+
}
|
|
382
|
+
false
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
#[cfg(feature = "svg")]
|
|
386
|
+
fn draw_svg_image_layer(&self, scene: &mut impl PaintScene, layer: &ImageLayerStyles) {
|
|
387
|
+
let Some(bg_image) = layer.image_data else {
|
|
388
|
+
return;
|
|
389
|
+
};
|
|
390
|
+
let ImageData::Svg(svg) = &bg_image.image else {
|
|
391
|
+
return;
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
// A zero-sized `viewBox` disables rendering of the SVG
|
|
395
|
+
if svg.intrinsic_dimensions.degenerate_view_box {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
let (origin_rect, base_transform) = if self.layer_is_fixed(layer) {
|
|
400
|
+
self.fixed_positioning_area()
|
|
401
|
+
} else {
|
|
402
|
+
(self.box_rect(layer.origin), self.transform)
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
let frame_w = (origin_rect.width() / self.scale) as f32;
|
|
406
|
+
let frame_h = (origin_rect.height() / self.scale) as f32;
|
|
407
|
+
|
|
408
|
+
// Size the SVG per the CSS default sizing algorithm
|
|
409
|
+
// (https://drafts.csswg.org/css-images/#default-sizing). An SVG image
|
|
410
|
+
// may lack an intrinsic width, height, and/or aspect ratio, so each is
|
|
411
|
+
// passed separately (usvg's resolved `Tree::size` is only used as the
|
|
412
|
+
// source coordinate space of the rendered tree).
|
|
413
|
+
let intrinsic_width = svg.intrinsic_width().filter(|w| w.is_finite() && *w > 0.0);
|
|
414
|
+
let intrinsic_height = svg.intrinsic_height().filter(|h| h.is_finite() && *h > 0.0);
|
|
415
|
+
let aspect_ratio = match (intrinsic_width, intrinsic_height) {
|
|
416
|
+
(Some(w), Some(h)) => Some(w / h),
|
|
417
|
+
_ => svg.viewbox_aspect_ratio(),
|
|
418
|
+
}
|
|
419
|
+
.filter(|r| r.is_finite() && *r > 0.0);
|
|
420
|
+
|
|
421
|
+
let bg_size = compute_layer_size(
|
|
422
|
+
layer,
|
|
423
|
+
frame_w,
|
|
424
|
+
frame_h,
|
|
425
|
+
BackgroundSizeComputeMode::Intrinsic {
|
|
426
|
+
width: intrinsic_width,
|
|
427
|
+
height: intrinsic_height,
|
|
428
|
+
ratio: aspect_ratio,
|
|
429
|
+
},
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
if bg_size.width <= 0.0 || bg_size.height <= 0.0 {
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
let viewport = super::svg_viewport_affine(
|
|
437
|
+
svg,
|
|
438
|
+
(bg_size.width * self.scale) as f32,
|
|
439
|
+
(bg_size.height * self.scale) as f32,
|
|
440
|
+
);
|
|
441
|
+
|
|
442
|
+
let bg_pos = compute_layer_position(
|
|
443
|
+
layer,
|
|
444
|
+
frame_w - bg_size.width as f32,
|
|
445
|
+
frame_h - bg_size.height as f32,
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
let transform = base_transform
|
|
449
|
+
* kurbo::Affine::translate((
|
|
450
|
+
origin_rect.x0 + bg_pos.x * self.scale,
|
|
451
|
+
origin_rect.y0 + bg_pos.y * self.scale,
|
|
452
|
+
))
|
|
453
|
+
* viewport;
|
|
454
|
+
|
|
455
|
+
anyrender_svg::render_svg_tree(scene, &svg.tree, transform);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
fn draw_raster_image_layer(&self, scene: &mut impl PaintScene, layer: &ImageLayerStyles) {
|
|
459
|
+
let Some(bg_image) = layer.image_data else {
|
|
460
|
+
return;
|
|
461
|
+
};
|
|
462
|
+
let ImageData::Raster(image_data) = &bg_image.image else {
|
|
463
|
+
return;
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
let image_rendering = self.style.clone_image_rendering();
|
|
467
|
+
let quality = to_image_quality(image_rendering);
|
|
468
|
+
|
|
469
|
+
let (origin_rect, base_transform) = if self.layer_is_fixed(layer) {
|
|
470
|
+
self.fixed_positioning_area()
|
|
471
|
+
} else {
|
|
472
|
+
(self.box_rect(layer.origin), self.transform)
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
let image_width = image_data.width as f64;
|
|
476
|
+
let image_height = image_data.height as f64;
|
|
477
|
+
|
|
478
|
+
let (bg_pos, bg_size) = compute_layer_position_and_size(
|
|
479
|
+
layer,
|
|
480
|
+
origin_rect.width() / self.scale,
|
|
481
|
+
origin_rect.height() / self.scale,
|
|
482
|
+
BackgroundSizeComputeMode::Size(image_width as f32, image_height as f32),
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
let bg_pos = (bg_pos.to_vec2() * self.scale).to_point();
|
|
486
|
+
let bg_size = bg_size * self.scale;
|
|
487
|
+
|
|
488
|
+
let x_ratio = bg_size.width / image_width;
|
|
489
|
+
let y_ratio = bg_size.height / image_height;
|
|
490
|
+
|
|
491
|
+
let BackgroundRepeat(repeat_x, repeat_y) = layer.repeat;
|
|
492
|
+
|
|
493
|
+
let x = raster_axis_tiling(
|
|
494
|
+
*repeat_x,
|
|
495
|
+
origin_rect.x0,
|
|
496
|
+
origin_rect.width(),
|
|
497
|
+
bg_pos.x,
|
|
498
|
+
bg_size.width,
|
|
499
|
+
image_width,
|
|
500
|
+
x_ratio,
|
|
501
|
+
);
|
|
502
|
+
let y = raster_axis_tiling(
|
|
503
|
+
*repeat_y,
|
|
504
|
+
origin_rect.y0,
|
|
505
|
+
origin_rect.height(),
|
|
506
|
+
bg_pos.y,
|
|
507
|
+
bg_size.height,
|
|
508
|
+
image_height,
|
|
509
|
+
y_ratio,
|
|
510
|
+
);
|
|
511
|
+
|
|
512
|
+
let transform = base_transform
|
|
513
|
+
.pre_scale_non_uniform(x_ratio, y_ratio)
|
|
514
|
+
.then_translate(Vec2 {
|
|
515
|
+
x: x.translate,
|
|
516
|
+
y: y.translate,
|
|
517
|
+
});
|
|
518
|
+
let tile_rect = Rect::new(0.0, 0.0, x.rect_len, y.rect_len);
|
|
519
|
+
|
|
520
|
+
for hc in 0..y.count {
|
|
521
|
+
for wc in 0..x.count {
|
|
522
|
+
let transform = transform.then_translate(Vec2 {
|
|
523
|
+
x: wc as f64 * x.stride,
|
|
524
|
+
y: hc as f64 * y.stride,
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
scene.fill(
|
|
528
|
+
peniko::Fill::NonZero,
|
|
529
|
+
transform,
|
|
530
|
+
to_peniko_image(image_data, quality).as_ref(),
|
|
531
|
+
None,
|
|
532
|
+
&tile_rect,
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
fn draw_gradient_layer(
|
|
539
|
+
&self,
|
|
540
|
+
scene: &mut impl PaintScene,
|
|
541
|
+
gradient: &StyloGradient,
|
|
542
|
+
layer: &ImageLayerStyles,
|
|
543
|
+
) {
|
|
544
|
+
// For a fixed layer the positioning area (the viewport) already covers
|
|
545
|
+
// everything visible, so it also serves as the clip rect (no extension
|
|
546
|
+
// towards the clip box is needed).
|
|
547
|
+
let (origin_rect, base_transform, clip_rect) = if self.layer_is_fixed(layer) {
|
|
548
|
+
let (viewport_rect, transform) = self.fixed_positioning_area();
|
|
549
|
+
(viewport_rect, transform, viewport_rect)
|
|
550
|
+
} else {
|
|
551
|
+
(
|
|
552
|
+
self.box_rect(layer.origin),
|
|
553
|
+
self.transform,
|
|
554
|
+
self.box_rect(layer.clip),
|
|
555
|
+
)
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
let (bg_pos, bg_size) = compute_layer_position_and_size(
|
|
559
|
+
layer,
|
|
560
|
+
origin_rect.width() / self.scale,
|
|
561
|
+
origin_rect.height() / self.scale,
|
|
562
|
+
BackgroundSizeComputeMode::Auto,
|
|
563
|
+
);
|
|
564
|
+
|
|
565
|
+
let bg_pos = (bg_pos.to_vec2() * self.scale).to_point();
|
|
566
|
+
let bg_size = bg_size * self.scale;
|
|
567
|
+
|
|
568
|
+
let BackgroundRepeat(repeat_x, repeat_y) = layer.repeat;
|
|
569
|
+
|
|
570
|
+
let x = gradient_axis_tiling(
|
|
571
|
+
*repeat_x,
|
|
572
|
+
origin_rect.x0,
|
|
573
|
+
origin_rect.width(),
|
|
574
|
+
clip_rect.x0,
|
|
575
|
+
clip_rect.width(),
|
|
576
|
+
bg_pos.x,
|
|
577
|
+
bg_size.width,
|
|
578
|
+
);
|
|
579
|
+
let y = gradient_axis_tiling(
|
|
580
|
+
*repeat_y,
|
|
581
|
+
origin_rect.y0,
|
|
582
|
+
origin_rect.height(),
|
|
583
|
+
clip_rect.y0,
|
|
584
|
+
clip_rect.height(),
|
|
585
|
+
bg_pos.y,
|
|
586
|
+
bg_size.height,
|
|
587
|
+
);
|
|
588
|
+
|
|
589
|
+
// FIXME: https://wpt.live/css/css-backgrounds/background-size/background-size-near-zero-gradient.html
|
|
590
|
+
if x.count as u64 * y.count as u64 > 500 {
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
let tile_rect = Rect::new(0.0, 0.0, x.rect_len, y.rect_len);
|
|
595
|
+
let bounding_box = self.frame.border_box.bounding_box();
|
|
596
|
+
let current_color = self.style.clone_color();
|
|
597
|
+
|
|
598
|
+
let (gradient, gradient_transform) = to_peniko_gradient(
|
|
599
|
+
gradient,
|
|
600
|
+
tile_rect,
|
|
601
|
+
bounding_box,
|
|
602
|
+
self.scale,
|
|
603
|
+
¤t_color,
|
|
604
|
+
);
|
|
605
|
+
let brush = anyrender::Paint::Gradient(&gradient);
|
|
606
|
+
|
|
607
|
+
let transform = base_transform.then_translate(Vec2 {
|
|
608
|
+
x: x.translate,
|
|
609
|
+
y: y.translate,
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
for hc in 0..y.count {
|
|
613
|
+
for wc in 0..x.count {
|
|
614
|
+
let transform = transform.then_translate(Vec2 {
|
|
615
|
+
x: wc as f64 * x.stride,
|
|
616
|
+
y: hc as f64 * y.stride,
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
scene.fill(
|
|
620
|
+
peniko::Fill::NonZero,
|
|
621
|
+
transform,
|
|
622
|
+
brush.clone(),
|
|
623
|
+
gradient_transform,
|
|
624
|
+
&tile_rect,
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
fn compute_layer_position_and_size(
|
|
632
|
+
layer: &ImageLayerStyles,
|
|
633
|
+
container_w: f64,
|
|
634
|
+
container_h: f64,
|
|
635
|
+
size_mode: BackgroundSizeComputeMode,
|
|
636
|
+
) -> (Point, Size) {
|
|
637
|
+
use BackgroundRepeatKeyword::*;
|
|
638
|
+
|
|
639
|
+
let bg_size = compute_layer_size(layer, container_w as f32, container_h as f32, size_mode);
|
|
640
|
+
|
|
641
|
+
let bg_pos = compute_layer_position(
|
|
642
|
+
layer,
|
|
643
|
+
(container_w - bg_size.width) as f32,
|
|
644
|
+
(container_h - bg_size.height) as f32,
|
|
645
|
+
);
|
|
646
|
+
|
|
647
|
+
let BackgroundRepeat(repeat_x, repeat_y) = layer.repeat;
|
|
648
|
+
|
|
649
|
+
let bg_size = if matches!(repeat_x, Round) && matches!(repeat_y, Round) {
|
|
650
|
+
let count = (container_w / bg_size.width).round();
|
|
651
|
+
let width = container_w / count;
|
|
652
|
+
|
|
653
|
+
let count = (container_h / bg_size.height).round();
|
|
654
|
+
let height = container_h / count;
|
|
655
|
+
|
|
656
|
+
Size::new(width, height)
|
|
657
|
+
} else if matches!(repeat_x, Round) {
|
|
658
|
+
let count = (container_w / bg_size.width).round();
|
|
659
|
+
let width = container_w / count;
|
|
660
|
+
Size::new(width, bg_size.height)
|
|
661
|
+
} else if matches!(repeat_y, Round) {
|
|
662
|
+
let count = (container_h / bg_size.height).round();
|
|
663
|
+
let height = container_h / count;
|
|
664
|
+
Size::new(bg_size.width, height)
|
|
665
|
+
} else {
|
|
666
|
+
bg_size
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
(bg_pos, bg_size)
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
#[inline]
|
|
673
|
+
fn compute_layer_position(layer: &ImageLayerStyles, width: f32, height: f32) -> Point {
|
|
674
|
+
use style::values::computed::Length;
|
|
675
|
+
|
|
676
|
+
let bg_pos_x = layer.position_x.resolve(Length::new(width)).px() as f64;
|
|
677
|
+
let bg_pos_y = layer.position_y.resolve(Length::new(height)).px() as f64;
|
|
678
|
+
|
|
679
|
+
Point::new(bg_pos_x, bg_pos_y)
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
fn compute_layer_size(
|
|
683
|
+
layer: &ImageLayerStyles,
|
|
684
|
+
container_w: f32,
|
|
685
|
+
container_h: f32,
|
|
686
|
+
mode: BackgroundSizeComputeMode,
|
|
687
|
+
) -> kurbo::Size {
|
|
688
|
+
use style::values::computed::Length;
|
|
689
|
+
use style::values::generics::length::GenericLengthPercentageOrAuto as Lpa;
|
|
690
|
+
|
|
691
|
+
let (width, height): (f32, f32) = match layer.size {
|
|
692
|
+
BackgroundSize::ExplicitSize { width, height } => {
|
|
693
|
+
let width = width.map(|w| w.0.resolve(Length::new(container_w)));
|
|
694
|
+
let height = height.map(|h| h.0.resolve(Length::new(container_h)));
|
|
695
|
+
|
|
696
|
+
match (width, height) {
|
|
697
|
+
(Lpa::LengthPercentage(width), Lpa::LengthPercentage(height)) => {
|
|
698
|
+
let width = width.px();
|
|
699
|
+
let height = height.px();
|
|
700
|
+
match mode {
|
|
701
|
+
BackgroundSizeComputeMode::Auto => (width, height),
|
|
702
|
+
BackgroundSizeComputeMode::Size(_, _) => (width, height),
|
|
703
|
+
BackgroundSizeComputeMode::Intrinsic { .. } => (width, height),
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
(Lpa::LengthPercentage(width), Lpa::Auto) => {
|
|
707
|
+
let width = width.px();
|
|
708
|
+
let height = match mode {
|
|
709
|
+
BackgroundSizeComputeMode::Auto => container_h,
|
|
710
|
+
BackgroundSizeComputeMode::Size(bg_w, bg_h) => bg_h / bg_w * width,
|
|
711
|
+
BackgroundSizeComputeMode::Intrinsic { height, ratio, .. } => ratio
|
|
712
|
+
.map(|ratio| width / ratio)
|
|
713
|
+
.or(height)
|
|
714
|
+
.unwrap_or(container_h),
|
|
715
|
+
};
|
|
716
|
+
(width, height)
|
|
717
|
+
}
|
|
718
|
+
(Lpa::Auto, Lpa::LengthPercentage(height)) => {
|
|
719
|
+
let height = height.px();
|
|
720
|
+
let width = match mode {
|
|
721
|
+
BackgroundSizeComputeMode::Auto => container_w,
|
|
722
|
+
BackgroundSizeComputeMode::Size(bg_w, bg_h) => bg_w / bg_h * height,
|
|
723
|
+
BackgroundSizeComputeMode::Intrinsic { width, ratio, .. } => ratio
|
|
724
|
+
.map(|ratio| height * ratio)
|
|
725
|
+
.or(width)
|
|
726
|
+
.unwrap_or(container_w),
|
|
727
|
+
};
|
|
728
|
+
(width, height)
|
|
729
|
+
}
|
|
730
|
+
(Lpa::Auto, Lpa::Auto) => match mode {
|
|
731
|
+
BackgroundSizeComputeMode::Auto => (container_w, container_h),
|
|
732
|
+
BackgroundSizeComputeMode::Size(bg_w, bg_h) => (bg_w, bg_h),
|
|
733
|
+
BackgroundSizeComputeMode::Intrinsic {
|
|
734
|
+
width,
|
|
735
|
+
height,
|
|
736
|
+
ratio,
|
|
737
|
+
} => default_sizing(width, height, ratio, container_w, container_h),
|
|
738
|
+
},
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
BackgroundSize::Cover => match mode {
|
|
742
|
+
BackgroundSizeComputeMode::Auto => (container_w, container_h),
|
|
743
|
+
BackgroundSizeComputeMode::Size(bg_w, bg_h) => {
|
|
744
|
+
// Scale to the smallest size that covers both axes
|
|
745
|
+
let ratio = (container_w / bg_w).max(container_h / bg_h);
|
|
746
|
+
(bg_w * ratio, bg_h * ratio)
|
|
747
|
+
}
|
|
748
|
+
BackgroundSizeComputeMode::Intrinsic { ratio, .. } => match ratio {
|
|
749
|
+
// Scale the aspect ratio to the smallest size that covers both axes
|
|
750
|
+
Some(ratio) => {
|
|
751
|
+
let scale = (container_w / ratio).max(container_h);
|
|
752
|
+
(scale * ratio, scale)
|
|
753
|
+
}
|
|
754
|
+
// No intrinsic aspect ratio: fill the positioning area
|
|
755
|
+
None => (container_w, container_h),
|
|
756
|
+
},
|
|
757
|
+
},
|
|
758
|
+
BackgroundSize::Contain => match mode {
|
|
759
|
+
BackgroundSizeComputeMode::Auto => (container_w, container_h),
|
|
760
|
+
BackgroundSizeComputeMode::Size(bg_w, bg_h) => {
|
|
761
|
+
// Scale to the largest size contained by both axes
|
|
762
|
+
let ratio = (container_w / bg_w).min(container_h / bg_h);
|
|
763
|
+
(bg_w * ratio, bg_h * ratio)
|
|
764
|
+
}
|
|
765
|
+
BackgroundSizeComputeMode::Intrinsic { ratio, .. } => match ratio {
|
|
766
|
+
// Scale the aspect ratio to the largest size contained by both axes
|
|
767
|
+
Some(ratio) => {
|
|
768
|
+
let scale = (container_w / ratio).min(container_h);
|
|
769
|
+
(scale * ratio, scale)
|
|
770
|
+
}
|
|
771
|
+
// No intrinsic aspect ratio: fill the positioning area
|
|
772
|
+
None => (container_w, container_h),
|
|
773
|
+
},
|
|
774
|
+
},
|
|
775
|
+
};
|
|
776
|
+
|
|
777
|
+
kurbo::Size {
|
|
778
|
+
width: width as f64,
|
|
779
|
+
height: height as f64,
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
enum BackgroundSizeComputeMode {
|
|
784
|
+
Auto,
|
|
785
|
+
Size(f32, f32),
|
|
786
|
+
/// Intrinsic dimensions of an image which may lack an intrinsic width,
|
|
787
|
+
/// height, and/or aspect ratio (e.g. SVG), sized per the CSS default
|
|
788
|
+
/// sizing algorithm (https://drafts.csswg.org/css-images/#default-sizing)
|
|
789
|
+
Intrinsic {
|
|
790
|
+
width: Option<f32>,
|
|
791
|
+
height: Option<f32>,
|
|
792
|
+
ratio: Option<f32>,
|
|
793
|
+
},
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/// The CSS default sizing algorithm for the unconstrained (`auto auto`) case:
|
|
797
|
+
/// resolve the concrete object size from whichever intrinsic dimensions exist,
|
|
798
|
+
/// falling back to the default object size (the background positioning area).
|
|
799
|
+
fn default_sizing(
|
|
800
|
+
width: Option<f32>,
|
|
801
|
+
height: Option<f32>,
|
|
802
|
+
ratio: Option<f32>,
|
|
803
|
+
container_w: f32,
|
|
804
|
+
container_h: f32,
|
|
805
|
+
) -> (f32, f32) {
|
|
806
|
+
match (width, height) {
|
|
807
|
+
(Some(w), Some(h)) => (w, h),
|
|
808
|
+
(Some(w), None) => (w, ratio.map(|r| w / r).unwrap_or(container_h)),
|
|
809
|
+
(None, Some(h)) => (ratio.map(|r| h * r).unwrap_or(container_w), h),
|
|
810
|
+
(None, None) => match ratio {
|
|
811
|
+
// Intrinsic aspect ratio only: size as if `contain` were specified
|
|
812
|
+
Some(ratio) => {
|
|
813
|
+
let scale = (container_w / ratio).min(container_h);
|
|
814
|
+
(scale * ratio, scale)
|
|
815
|
+
}
|
|
816
|
+
None => (container_w, container_h),
|
|
817
|
+
},
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/// The placement and tiling of a background layer along one axis: a
|
|
822
|
+
/// translation applied to the layer as a whole, the length of each filled
|
|
823
|
+
/// rect, and the number of explicit tiles with the stride between them.
|
|
824
|
+
struct AxisTiling {
|
|
825
|
+
/// Translation (in device pixels) positioning the first tile
|
|
826
|
+
translate: f64,
|
|
827
|
+
/// Length of each filled rect, in the coordinate space of the fill's transform
|
|
828
|
+
rect_len: f64,
|
|
829
|
+
/// Number of explicitly drawn tiles
|
|
830
|
+
count: u32,
|
|
831
|
+
/// Stride (in device pixels) between the starts of consecutive tiles
|
|
832
|
+
stride: f64,
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/// Per-axis placement and tiling for a raster image layer. `Repeat`/`Round`
|
|
836
|
+
/// produce a single fill covering the whole positioning area (relying on the
|
|
837
|
+
/// image brush repeating), while `Space` produces `count` explicit tiles
|
|
838
|
+
/// spaced `stride` apart.
|
|
839
|
+
///
|
|
840
|
+
/// The fill rect is in image pixel coordinates (the drawing transform is
|
|
841
|
+
/// pre-scaled by `ratio`), while translations are in device pixels.
|
|
842
|
+
fn raster_axis_tiling(
|
|
843
|
+
repeat: BackgroundRepeatKeyword,
|
|
844
|
+
origin_start: f64,
|
|
845
|
+
origin_len: f64,
|
|
846
|
+
bg_pos: f64,
|
|
847
|
+
tile_len: f64,
|
|
848
|
+
image_len: f64,
|
|
849
|
+
ratio: f64,
|
|
850
|
+
) -> AxisTiling {
|
|
851
|
+
use BackgroundRepeatKeyword::*;
|
|
852
|
+
|
|
853
|
+
match repeat {
|
|
854
|
+
Repeat | Round => {
|
|
855
|
+
let extend_len = extend(bg_pos, tile_len);
|
|
856
|
+
AxisTiling {
|
|
857
|
+
translate: origin_start - extend_len,
|
|
858
|
+
rect_len: (origin_len + extend_len) / ratio,
|
|
859
|
+
count: 1,
|
|
860
|
+
stride: 0.0,
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
Space => {
|
|
864
|
+
let (count, stride) = compute_space_count_and_stride(origin_len, tile_len);
|
|
865
|
+
AxisTiling {
|
|
866
|
+
translate: origin_start + if count == 1 { bg_pos } else { 0.0 },
|
|
867
|
+
rect_len: image_len,
|
|
868
|
+
count,
|
|
869
|
+
stride,
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
NoRepeat => AxisTiling {
|
|
873
|
+
translate: origin_start + bg_pos,
|
|
874
|
+
rect_len: image_len,
|
|
875
|
+
count: 1,
|
|
876
|
+
stride: 0.0,
|
|
877
|
+
},
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
/// Per-axis placement and tiling for a gradient layer. Unlike raster images,
|
|
882
|
+
/// gradients cannot rely on brush repetition, so `Repeat`/`Round` also produce
|
|
883
|
+
/// explicit tiles. When the clip box extends beyond the origin box, tiling
|
|
884
|
+
/// starts from the clip box edge so the pattern covers the whole clipped area.
|
|
885
|
+
fn gradient_axis_tiling(
|
|
886
|
+
repeat: BackgroundRepeatKeyword,
|
|
887
|
+
origin_start: f64,
|
|
888
|
+
origin_len: f64,
|
|
889
|
+
clip_start: f64,
|
|
890
|
+
clip_len: f64,
|
|
891
|
+
bg_pos: f64,
|
|
892
|
+
tile_len: f64,
|
|
893
|
+
) -> AxisTiling {
|
|
894
|
+
use BackgroundRepeatKeyword::*;
|
|
895
|
+
|
|
896
|
+
match repeat {
|
|
897
|
+
Repeat | Round => {
|
|
898
|
+
// The clip and origin boxes are nested, so the clip box extends
|
|
899
|
+
// beyond the origin box iff it does so at either end
|
|
900
|
+
let clip_is_outer =
|
|
901
|
+
clip_start < origin_start || clip_start + clip_len > origin_start + origin_len;
|
|
902
|
+
let (area_start, area_len) = if clip_is_outer {
|
|
903
|
+
(clip_start, clip_len)
|
|
904
|
+
} else {
|
|
905
|
+
(origin_start, origin_len)
|
|
906
|
+
};
|
|
907
|
+
let extend_len = extend((origin_start - area_start) + bg_pos, tile_len);
|
|
908
|
+
let count = ((area_len + extend_len) / tile_len).ceil() as u32;
|
|
909
|
+
AxisTiling {
|
|
910
|
+
translate: area_start - extend_len,
|
|
911
|
+
rect_len: tile_len,
|
|
912
|
+
count,
|
|
913
|
+
stride: tile_len,
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
Space => {
|
|
917
|
+
let (count, stride) = compute_space_count_and_stride(origin_len, tile_len);
|
|
918
|
+
AxisTiling {
|
|
919
|
+
translate: origin_start + if count == 1 { bg_pos } else { 0.0 },
|
|
920
|
+
rect_len: tile_len,
|
|
921
|
+
count,
|
|
922
|
+
stride,
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
NoRepeat => AxisTiling {
|
|
926
|
+
translate: origin_start + bg_pos,
|
|
927
|
+
rect_len: tile_len,
|
|
928
|
+
count: 1,
|
|
929
|
+
stride: 0.0,
|
|
930
|
+
},
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
fn compute_space_count_and_stride(bg_size: f64, size: f64) -> (u32, f64) {
|
|
935
|
+
let modulo = bg_size % size;
|
|
936
|
+
let count = (((bg_size - modulo) / size) as u32).max(1);
|
|
937
|
+
let stride = if count > 1 {
|
|
938
|
+
modulo / (count - 1) as f64
|
|
939
|
+
} else {
|
|
940
|
+
0.0
|
|
941
|
+
} + size;
|
|
942
|
+
|
|
943
|
+
(count, stride)
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
/// Whether drawing the layer is guaranteed to paint nothing, making its clip
|
|
947
|
+
/// layer unnecessary
|
|
948
|
+
fn layer_paints_nothing(layer: &ImageLayerStyles) -> bool {
|
|
949
|
+
match layer.stylo_image {
|
|
950
|
+
GenericImage::None => true,
|
|
951
|
+
GenericImage::Url(_) => layer.image_data.is_none(),
|
|
952
|
+
_ => false,
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
#[inline]
|
|
957
|
+
pub(super) fn get_cyclic<T>(values: &[T], layer_index: usize) -> &T {
|
|
958
|
+
&values[layer_index % values.len()]
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
fn extend(offset: f64, length: f64) -> f64 {
|
|
962
|
+
let extend_length = offset % length;
|
|
963
|
+
if extend_length > 0.0 {
|
|
964
|
+
length - extend_length
|
|
965
|
+
} else {
|
|
966
|
+
-extend_length
|
|
967
|
+
}
|
|
968
|
+
}
|