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.
Files changed (60) hide show
  1. package/README.md +61 -9
  2. package/THIRD_PARTY_NOTICES.md +20 -0
  3. package/dist/types.d.ts +2 -1
  4. package/dist/types.d.ts.map +1 -1
  5. package/licenses/Noto-OFL.txt +94 -0
  6. package/licenses/NotoCJK-OFL.txt +92 -0
  7. package/licenses/NotoEmoji-OFL.txt +93 -0
  8. package/licenses/Unifont-OFL.txt +90 -0
  9. package/licenses/font-sources.json +106 -0
  10. package/package.json +1 -1
  11. package/src/types.ts +2 -1
  12. package/vendor/README.md +43 -0
  13. package/vendor/blitz-paint/Cargo.toml +50 -0
  14. package/vendor/blitz-paint/LICENSE-APACHE +176 -0
  15. package/vendor/blitz-paint/LICENSE-MIT +23 -0
  16. package/vendor/blitz-paint/src/color.rs +66 -0
  17. package/vendor/blitz-paint/src/debug_overlay.rs +168 -0
  18. package/vendor/blitz-paint/src/filters.rs +40 -0
  19. package/vendor/blitz-paint/src/gradient.rs +536 -0
  20. package/vendor/blitz-paint/src/kurbo_css/css_box.rs +847 -0
  21. package/vendor/blitz-paint/src/kurbo_css/mod.rs +77 -0
  22. package/vendor/blitz-paint/src/kurbo_css/non_uniform_radii.rs +60 -0
  23. package/vendor/blitz-paint/src/layers.rs +95 -0
  24. package/vendor/blitz-paint/src/lib.rs +190 -0
  25. package/vendor/blitz-paint/src/render/background.rs +968 -0
  26. package/vendor/blitz-paint/src/render/border.rs +631 -0
  27. package/vendor/blitz-paint/src/render/box_shadow.rs +157 -0
  28. package/vendor/blitz-paint/src/render/clip_path.rs +492 -0
  29. package/vendor/blitz-paint/src/render/form_controls.rs +103 -0
  30. package/vendor/blitz-paint/src/render/mask.rs +144 -0
  31. package/vendor/blitz-paint/src/render.rs +1351 -0
  32. package/vendor/blitz-paint/src/sizing.rs +63 -0
  33. package/vendor/blitz-paint/src/text.rs +747 -0
  34. package/vendor/fontique/Cargo.toml +194 -0
  35. package/vendor/fontique/LICENSE-APACHE +201 -0
  36. package/vendor/fontique/LICENSE-MIT +25 -0
  37. package/vendor/fontique/README.md +54 -0
  38. package/vendor/fontique/examples/generic_families.rs +37 -0
  39. package/vendor/fontique/src/attributes.rs +42 -0
  40. package/vendor/fontique/src/backend/android.rs +218 -0
  41. package/vendor/fontique/src/backend/coretext.rs +206 -0
  42. package/vendor/fontique/src/backend/dwrite.rs +395 -0
  43. package/vendor/fontique/src/backend/fontconfig.rs +844 -0
  44. package/vendor/fontique/src/backend/mod.rs +75 -0
  45. package/vendor/fontique/src/charmap.rs +127 -0
  46. package/vendor/fontique/src/collection/mod.rs +1016 -0
  47. package/vendor/fontique/src/collection/query.rs +392 -0
  48. package/vendor/fontique/src/fallback.rs +293 -0
  49. package/vendor/fontique/src/family.rs +111 -0
  50. package/vendor/fontique/src/family_name.rs +153 -0
  51. package/vendor/fontique/src/font.rs +562 -0
  52. package/vendor/fontique/src/generic.rs +36 -0
  53. package/vendor/fontique/src/impl_fontconfig.rs +159 -0
  54. package/vendor/fontique/src/lib.rs +87 -0
  55. package/vendor/fontique/src/matching.rs +483 -0
  56. package/vendor/fontique/src/scan.rs +312 -0
  57. package/vendor/fontique/src/script.rs +271 -0
  58. package/vendor/fontique/src/source.rs +108 -0
  59. package/vendor/fontique/src/source_cache.rs +254 -0
  60. package/wasm/blitz_pdf_bg.wasm +0 -0
@@ -0,0 +1,1351 @@
1
+ mod background;
2
+ mod border;
3
+ mod box_shadow;
4
+ mod clip_path;
5
+ mod form_controls;
6
+ mod mask;
7
+
8
+ use std::cell::RefCell;
9
+ use std::collections::HashMap;
10
+ use std::sync::Arc;
11
+
12
+ use super::kurbo_css::CssBox;
13
+ use crate::color::{Color, ToColorColor};
14
+ use crate::debug_overlay::render_debug_overlay;
15
+ use crate::filters::convert_filters;
16
+ use crate::kurbo_css::NonUniformRoundedRectRadii;
17
+ use crate::layers::LayerManager;
18
+ use crate::sizing::compute_object_fit;
19
+ use crate::text::DrawTextContext;
20
+ use crate::{CustomWidgetSceneMap, SELECTION_COLOR};
21
+ use anyrender::{PaintScene, Scene};
22
+ use blitz_dom::node::{
23
+ ListItemLayout, ListItemLayoutPosition, Marker, NodeData, RasterImageData, TextInputData,
24
+ TextNodeData,
25
+ };
26
+ use blitz_dom::{BaseDocument, ElementData, Node, NodeId, local_name};
27
+ use blitz_traits::devtools::DevtoolSettings;
28
+
29
+ use style::values::computed::{BorderCornerRadius, ColorOrAuto};
30
+ use style::{
31
+ dom::TElement,
32
+ properties::{
33
+ ComputedValues, generated::longhands::visibility::computed_value::T as StyloVisibility,
34
+ style_structs::Font,
35
+ },
36
+ values::{
37
+ computed::{CSSPixelLength, Contain, Overflow},
38
+ specified::box_::{DisplayInside, DisplayOutside},
39
+ specified::image::ImageRendering,
40
+ },
41
+ };
42
+
43
+ use kurbo::{self, Affine, Insets, Point, Rect, Shape, Size, Stroke, Vec2};
44
+ use peniko::{self, Fill, ImageData, ImageSampler};
45
+ use style::values::generics::color::GenericColor;
46
+ use taffy::Layout;
47
+
48
+ /// A view of the track positions reported by taffy in physical (left-to-right /
49
+ /// top-to-bottom) order. Taffy reports tracks in logical order, which for RTL grid
50
+ /// containers is physically reversed.
51
+ #[derive(Clone, Copy)]
52
+ pub(crate) struct PhysicalTracks<'a> {
53
+ positions: &'a [taffy::Line<f32>],
54
+ reversed: bool,
55
+ }
56
+
57
+ impl<'a> PhysicalTracks<'a> {
58
+ pub(crate) fn from_tracks<S: taffy::CheapCloneStr>(
59
+ tracks: &'a taffy::DetailedGridTracksInfo<S>,
60
+ ) -> Self {
61
+ let positions = tracks.positions.as_slice();
62
+ let reversed = positions
63
+ .first()
64
+ .zip(positions.last())
65
+ .is_some_and(|(first, last)| first.start > last.start);
66
+ Self {
67
+ positions,
68
+ reversed,
69
+ }
70
+ }
71
+
72
+ fn get(self, index: usize) -> taffy::Line<f32> {
73
+ if self.reversed {
74
+ self.positions[self.positions.len() - 1 - index]
75
+ } else {
76
+ self.positions[index]
77
+ }
78
+ }
79
+
80
+ /// Iterate track positions in physical order
81
+ pub(crate) fn iter(self) -> impl ExactSizeIterator<Item = taffy::Line<f32>> + 'a {
82
+ (0..self.positions.len()).map(move |index| self.get(index))
83
+ }
84
+
85
+ /// The physical start position of the first track
86
+ pub(crate) fn origin(self) -> f32 {
87
+ if self.positions.is_empty() {
88
+ 0.0
89
+ } else {
90
+ self.get(0).start
91
+ }
92
+ }
93
+
94
+ /// Total distance from the start of the first track to the end of the last track
95
+ pub(crate) fn span(self) -> f32 {
96
+ if self.positions.is_empty() {
97
+ 0.0
98
+ } else {
99
+ self.get(self.positions.len() - 1).end - self.get(0).start
100
+ }
101
+ }
102
+ }
103
+
104
+ /// A short-lived struct which holds a bunch of parameters for rendering a scene so
105
+ /// that we don't have to pass them down as parameters
106
+ pub struct BlitzDomPainter<'dom, 'a> {
107
+ /// Input parameters (read only) for generating the Scene
108
+ pub(crate) dom: &'dom BaseDocument,
109
+ pub(crate) scale: f64,
110
+ pub(crate) width: u32,
111
+ pub(crate) height: u32,
112
+ pub(crate) initial_x: f64,
113
+ pub(crate) initial_y: f64,
114
+ /// The id of the document's root element (cached to avoid re-resolving it for every element)
115
+ pub(crate) root_element_id: Option<NodeId>,
116
+ /// Scrollbar hover/drag state, resolved once per scene like the root element
117
+ #[cfg(feature = "scrollbars")]
118
+ pub(crate) hovered_scrollbar: Option<blitz_dom::node::ScrollbarRef>,
119
+ #[cfg(feature = "scrollbars")]
120
+ pub(crate) scrollbar_drag_target: Option<blitz_dom::node::ScrollbarRef>,
121
+ pub(crate) layer_manager: LayerManager,
122
+ /// Reusable scratch allocations shared by all text layouts painted for this document.
123
+ pub(crate) draw_text_context: RefCell<DrawTextContext>,
124
+ pub(crate) text_observer: Option<&'a RefCell<dyn crate::TextObserver>>,
125
+ /// Cached selection ranges for O(1) lookup: node_id -> (start_offset, end_offset)
126
+ pub(crate) selection_ranges: HashMap<NodeId, (usize, usize)>,
127
+
128
+ // Pre-computed `Scene`s for each CustomWidget
129
+ pub(crate) custom_widget_scenes: &'a CustomWidgetSceneMap,
130
+ }
131
+
132
+ impl<'dom, 'a> BlitzDomPainter<'dom, 'a> {
133
+ /// Create a new BlitzDomPainter for the given document
134
+ pub fn new(
135
+ dom: &'dom BaseDocument,
136
+ scale: f64,
137
+ width: u32,
138
+ height: u32,
139
+ initial_x: f64,
140
+ initial_y: f64,
141
+ custom_widget_scenes: &'a CustomWidgetSceneMap,
142
+ text_observer: Option<&'a RefCell<dyn crate::TextObserver>>,
143
+ ) -> Self {
144
+ let selection_ranges: HashMap<NodeId, (usize, usize)> = dom
145
+ .get_text_selection_ranges()
146
+ .into_iter()
147
+ .map(|(node_id, start, end)| (node_id, (start, end)))
148
+ .collect();
149
+
150
+ let layer_manager = LayerManager::default();
151
+ let root_element_id = dom.try_root_element().map(|el| el.id);
152
+
153
+ Self {
154
+ dom,
155
+ scale,
156
+ width,
157
+ height,
158
+ initial_x,
159
+ initial_y,
160
+ root_element_id,
161
+ #[cfg(feature = "scrollbars")]
162
+ hovered_scrollbar: dom.hovered_scrollbar(),
163
+ #[cfg(feature = "scrollbars")]
164
+ scrollbar_drag_target: dom.scrollbar_drag_target(),
165
+ layer_manager,
166
+ draw_text_context: RefCell::new(DrawTextContext::default()),
167
+ text_observer,
168
+ selection_ranges,
169
+ custom_widget_scenes,
170
+ }
171
+ }
172
+
173
+ /// Draw the current tree to current render surface
174
+ /// Eventually we'll want the surface itself to be passed into the render function, along with things like the viewport
175
+ ///
176
+ /// This assumes styles are resolved and layout is complete.
177
+ /// Make sure you do those before trying to render
178
+ pub fn paint_scene(&self, scene: &mut impl PaintScene) {
179
+ if self.dom.has_pending_critical_resources() {
180
+ return;
181
+ }
182
+
183
+ // Simply render the document (the root element (note that this is not the same as the root node)))
184
+ // scene.reset();
185
+ let viewport_scroll = self.dom.as_ref().viewport_scroll();
186
+
187
+ // A document without a root element (e.g. an empty iframe sub-document) has
188
+ // nothing to paint.
189
+ let Some(root_element) = self.dom.as_ref().try_root_element() else {
190
+ return;
191
+ };
192
+ let root_id = root_element.id;
193
+ let bg_width = (self.width as f32).max(root_element.final_layout().size.width);
194
+ let bg_height = (self.height as f32).max(root_element.final_layout().size.height);
195
+
196
+ let background_color = {
197
+ let html_color = root_element
198
+ .primary_styles()
199
+ .map(|s| s.clone_background_color())
200
+ .unwrap_or(GenericColor::TRANSPARENT_BLACK);
201
+ if html_color == GenericColor::TRANSPARENT_BLACK {
202
+ root_element
203
+ .children
204
+ .iter()
205
+ .find_map(|id| {
206
+ self.dom
207
+ .as_ref()
208
+ .get_node(*id)
209
+ .filter(|node| node.data.is_element_with_tag_name(&local_name!("body")))
210
+ })
211
+ .and_then(|body| body.primary_styles())
212
+ .map(|style| {
213
+ let current_color = style.clone_color();
214
+ style
215
+ .clone_background_color()
216
+ .resolve_to_absolute(&current_color)
217
+ })
218
+ } else {
219
+ let current_color = root_element.primary_styles().unwrap().clone_color();
220
+ Some(html_color.resolve_to_absolute(&current_color))
221
+ }
222
+ };
223
+
224
+ if let Some(bg_color) = background_color {
225
+ let bg_color = bg_color.as_srgb_color();
226
+ let rect = Rect::from_origin_size(
227
+ (self.initial_x, self.initial_y),
228
+ (bg_width as f64, bg_height as f64),
229
+ );
230
+ scene.fill(Fill::NonZero, Affine::IDENTITY, bg_color, None, &rect);
231
+ }
232
+
233
+ // The root clip rectangle is the viewport (in screen coordinates, with the
234
+ // initial offset already subtracted). Elements outside of this are culled, and
235
+ // scrollports narrow this rectangle further for their descendants.
236
+ let viewport_clip_rect = Rect::new(0.0, 0.0, self.width as f64, self.height as f64);
237
+
238
+ self.render_element(
239
+ scene,
240
+ root_id,
241
+ Affine::translate(Vec2 {
242
+ x: self.initial_x - (viewport_scroll.x * self.scale),
243
+ y: self.initial_y - (viewport_scroll.y * self.scale),
244
+ }),
245
+ viewport_clip_rect,
246
+ );
247
+
248
+ // Render debug overlay
249
+ if self.dom.devtools().highlight_hover {
250
+ if let Some(node_id) = self.dom.as_ref().get_hover_node_id() {
251
+ render_debug_overlay(
252
+ scene,
253
+ self.dom,
254
+ node_id,
255
+ self.scale,
256
+ self.initial_x,
257
+ self.initial_y,
258
+ );
259
+ }
260
+ }
261
+ if let Some(node_id) = self.dom.devtools().highlight_node {
262
+ // Only nodes with element data have a layout to visualise
263
+ if self
264
+ .dom
265
+ .as_ref()
266
+ .get_node(node_id)
267
+ .is_some_and(|node| node.element_data().is_some())
268
+ {
269
+ render_debug_overlay(
270
+ scene,
271
+ self.dom,
272
+ node_id,
273
+ self.scale,
274
+ self.initial_x,
275
+ self.initial_y,
276
+ );
277
+ }
278
+ }
279
+ }
280
+
281
+ /// Renders a node, but is guaranteed that the node is an element
282
+ /// This is because the font_size is calculated from layout resolution and all text is rendered directly here, instead
283
+ /// of a separate text stroking phase.
284
+ ///
285
+ /// In Blitz, text styling gets its attributes from its container element/resolved styles
286
+ /// In other libraries, text gets its attributes from a `text` element - this is not how HTML works.
287
+ ///
288
+ /// Approaching rendering this way guarantees we have all the styles we need when rendering text with not having
289
+ /// to traverse back to the parent for its styles, or needing to pass down styles
290
+ fn render_element(
291
+ &self,
292
+ scene: &mut impl PaintScene,
293
+ node_id: NodeId,
294
+ parent_style_transform: Affine,
295
+ clip_rect: Rect,
296
+ ) {
297
+ let node = &self.dom.as_ref().tree()[node_id];
298
+
299
+ // Early return if the element is hidden
300
+ if matches!(node.taffy_display(), taffy::Display::None) {
301
+ return;
302
+ }
303
+
304
+ // Only draw elements with a style
305
+ let Some(styles) = node.primary_styles() else {
306
+ return;
307
+ };
308
+
309
+ // Hide inputs with type=hidden
310
+ // Implemented here rather than using the style engine for performance reasons
311
+ if node.local_name() == "input" && node.attr(local_name!("type")) == Some("hidden") {
312
+ return;
313
+ }
314
+
315
+ // Hide elements with a visibility style other than visible
316
+ if styles.get_inherited_box().visibility != StyloVisibility::Visible {
317
+ return;
318
+ }
319
+
320
+ let effects = styles.get_effects();
321
+ let opacity = effects.opacity;
322
+ if opacity == 0.0 {
323
+ return;
324
+ }
325
+ let has_opacity = opacity < 1.0;
326
+
327
+ // TODO: account for overflow_x vs overflow_y
328
+ let overflow_x = styles.get_box().overflow_x;
329
+ let overflow_y = styles.get_box().overflow_y;
330
+ // `contain: paint` (and stronger values like `strict`/`content`) clips the element's
331
+ // contents to its padding box. Paint containment does not apply to non-atomic inlines
332
+ // or internal table boxes other than table-cell.
333
+ let contain_paint = styles.get_box().clone_contain().contains(Contain::PAINT) && {
334
+ let display = styles.clone_display();
335
+ let is_internal_table_box_other_than_cell = display.outside()
336
+ == DisplayOutside::InternalTable
337
+ && display.inside() != DisplayInside::TableCell;
338
+ !display.is_inline_flow() && !is_internal_table_box_other_than_cell
339
+ };
340
+ let is_image = node
341
+ .element_data()
342
+ .and_then(|e| e.raster_image_data())
343
+ .is_some();
344
+ let is_sub_doc = node
345
+ .element_data()
346
+ .and_then(|el| el.sub_doc_data())
347
+ .is_some();
348
+ let is_text_input = node
349
+ .element_data()
350
+ .and_then(|el| el.text_input_data())
351
+ .is_some();
352
+ // The root element's overflow is propagated to the viewport (which is clipped by the
353
+ // window/surface bounds), so the root element must not clip its own overflow.
354
+ let is_root_element = self.root_element_id == Some(node_id);
355
+ let should_clip = !is_root_element
356
+ && (is_image
357
+ || is_sub_doc
358
+ || is_text_input
359
+ || contain_paint
360
+ || !matches!(overflow_x, Overflow::Visible)
361
+ || !matches!(overflow_y, Overflow::Visible));
362
+
363
+ // Apply padding/border offset to inline root
364
+ let taffy::Layout {
365
+ size,
366
+ border,
367
+ padding,
368
+ location,
369
+ ..
370
+ } = *node.final_layout();
371
+ let box_position = Vec2::new(location.x as f64, location.y as f64) * self.scale;
372
+ let box_size = Size::new(size.width as f64, size.height as f64);
373
+ let border_box = Rect::from_origin_size(box_position.to_point(), box_size);
374
+ let scaled_pb = (padding + border).map(f64::from);
375
+ let content_position = kurbo::Point {
376
+ x: scaled_pb.left,
377
+ y: scaled_pb.top,
378
+ };
379
+ let content_box_size = kurbo::Size {
380
+ width: (size.width as f64 - scaled_pb.left - scaled_pb.right) * self.scale,
381
+ height: (size.height as f64 - scaled_pb.top - scaled_pb.bottom) * self.scale,
382
+ };
383
+
384
+ // Don't render things that are out of view
385
+ let overflow = *node.scrollable_overflow();
386
+ let transform = parent_style_transform
387
+ * Affine::translate(box_position)
388
+ * node.transform().as_deref().copied().unwrap_or_default();
389
+
390
+ let screen_transform = Affine::translate(Vec2 {
391
+ x: -self.initial_x,
392
+ y: -self.initial_y,
393
+ }) * transform;
394
+ let screen_bbox = screen_transform.transform_rect_bbox(overflow.union(border_box));
395
+
396
+ // Cull elements that fall entirely outside the current clip rectangle. In addition to
397
+ // the viewport, `clip_rect` is narrowed by any ancestor scrollport (see below), so this
398
+ // also culls elements scrolled out of view inside a clipping/scrolling container.
399
+ if screen_bbox.x1 < clip_rect.x0
400
+ || screen_bbox.x0 > clip_rect.x1
401
+ || screen_bbox.y1 < clip_rect.y0
402
+ || screen_bbox.y0 > clip_rect.y1
403
+ {
404
+ return;
405
+ }
406
+
407
+ // Cull elements whose transformed geometry is not representable in f32 (e.g.
408
+ // `transform: scale(99e99)`): the renderer works in f32 and flattening such paths
409
+ // can attempt to allocate an unbounded number of line segments.
410
+ let f32_max = f64::from(f32::MAX);
411
+ if !screen_bbox.is_finite()
412
+ || [
413
+ screen_bbox.x0,
414
+ screen_bbox.y0,
415
+ screen_bbox.x1,
416
+ screen_bbox.y1,
417
+ ]
418
+ .iter()
419
+ .any(|c| c.abs() > f32_max)
420
+ {
421
+ return;
422
+ }
423
+
424
+ // Optimise zero-area (/very small area) clips by not rendering at all
425
+ let clip_area = content_box_size.width * content_box_size.height;
426
+ let overflow_area =
427
+ node.scrollable_overflow().width() * node.scrollable_overflow().height();
428
+ if should_clip && clip_area < 0.01 && overflow_area < 0.01 {
429
+ return;
430
+ }
431
+
432
+ #[cfg(feature = "custom-widget")]
433
+ let custom_widget_scene = self.custom_widget_scenes.get(&(self.dom.id(), node_id));
434
+ #[cfg(not(feature = "custom-widget"))]
435
+ let custom_widget_scene = None;
436
+
437
+ // Apply CSS transform property (where transforms are 2d)
438
+
439
+ let mut cx = self.element_cx(node, *node.final_layout(), transform, custom_widget_scene);
440
+
441
+ // If this element clips its overflow it establishes a scrollport: narrow the clip
442
+ // rectangle passed to descendants to the visible (clipped) region so that content
443
+ // scrolled out of view is culled rather than drawn and clipped away. The box used
444
+ // here matches the clip applied to the content below.
445
+ let child_clip_rect = if should_clip {
446
+ let clip_box = if is_text_input {
447
+ cx.frame.content_box_path()
448
+ } else {
449
+ cx.frame.padding_box_path()
450
+ };
451
+ clip_rect.intersect(screen_transform.transform_rect_bbox(clip_box.bounding_box()))
452
+ } else {
453
+ clip_rect
454
+ };
455
+
456
+ // Compute clip-path (if any) and wrap all rendering in a clip layer
457
+ let clip_path_shape = cx.clip_path_shape();
458
+ let has_clip_path = clip_path_shape.is_some();
459
+ let default_clip = cx.frame.border_box_path();
460
+ let mut clip_path_for_layer = clip_path_shape.unwrap_or(default_clip);
461
+ clip_path_for_layer.apply_affine(Affine::scale(self.scale));
462
+
463
+ // CSS 2 `clip` property (absolutely positioned elements only). Applies to the whole of
464
+ // the element's rendering (including outlines and shadows), so it is the outermost layer.
465
+ let css_clip_rect = cx.css_clip_rect();
466
+ if css_clip_rect.is_some_and(|rect| rect.is_zero_area()) {
467
+ return;
468
+ }
469
+ let css_clip_layer_pushed = self.layer_manager.maybe_push_layer(
470
+ scene,
471
+ css_clip_rect.is_some(),
472
+ 1.0,
473
+ cx.transform,
474
+ &css_clip_rect.unwrap_or(Rect::ZERO),
475
+ None,
476
+ None,
477
+ );
478
+
479
+ cx.draw_outline(scene);
480
+ cx.draw_outset_box_shadow(scene);
481
+
482
+ // clip-path clip ayer
483
+ self.layer_manager.maybe_with_layer(
484
+ scene,
485
+ has_clip_path,
486
+ 1.0,
487
+ cx.transform,
488
+ &clip_path_for_layer,
489
+ None,
490
+ None,
491
+ |scene| {
492
+ // If the element has a CSS `mask`, then push an isolation layer for the
493
+ // masked content. The mask is applied when the layer is popped below.
494
+ let mask_layer_pushed = cx.maybe_push_css_mask_layer(scene);
495
+ // `cx.transform` is mutated to apply scroll offsets while drawing content.
496
+ // Save it so that the mask can be drawn untransformed by scroll offsets.
497
+ let unscrolled_transform = cx.transform;
498
+
499
+ let filter = convert_filters(&effects.filter.0).map(Arc::new);
500
+ let backdrop_filter = convert_filters(&effects.backdrop_filter.0).map(Arc::new);
501
+
502
+ // Adjust effect layer clip by filter expansion area
503
+ //
504
+ // Returns a rectangle centered at the origin representing how much the filter
505
+ // expands the processing region in each direction. The rect coordinates are:
506
+ // - x0: negative left expansion
507
+ // - y0: negative top expansion
508
+ // - x1: positive right expansion
509
+ // - y1: positive bottom expansion
510
+ let filter_expansion_area = filter
511
+ .as_ref()
512
+ .map(|f| f.expansion_rect())
513
+ .unwrap_or(Rect::ZERO);
514
+
515
+ let mut effect_layer_clip = cx.frame.border_box_path().bounding_box();
516
+ effect_layer_clip.x0 += filter_expansion_area.x0;
517
+ effect_layer_clip.y0 += filter_expansion_area.y0;
518
+ effect_layer_clip.x1 += filter_expansion_area.x1;
519
+ effect_layer_clip.y1 += filter_expansion_area.y1;
520
+
521
+ // Opacity/Filter layer if box has opacity or a filter.
522
+ // Clipped to border-box as it needs to include the background and borders.
523
+ self.layer_manager.maybe_with_layer(
524
+ scene,
525
+ has_opacity || filter.is_some() || backdrop_filter.is_some(),
526
+ opacity,
527
+ cx.transform,
528
+ &effect_layer_clip,
529
+ filter,
530
+ backdrop_filter,
531
+ |scene| {
532
+ cx.draw_background(scene);
533
+ cx.draw_inset_box_shadow(scene);
534
+ cx.draw_table_row_backgrounds(scene);
535
+ cx.draw_table_borders(scene);
536
+ cx.draw_border(scene);
537
+ cx.stroke_devtools(scene);
538
+
539
+ // TODO: allow layers with opacity to be unclipped (overflow: visible)
540
+ let clip = if is_text_input {
541
+ &cx.frame.content_box_path()
542
+ } else {
543
+ &cx.frame.padding_box_path()
544
+ };
545
+
546
+ // Clip layer if box requires clipping. Opacity set to 1.0
547
+ self.layer_manager.maybe_with_layer(
548
+ scene,
549
+ should_clip,
550
+ 1.0, // opacity
551
+ cx.transform,
552
+ clip,
553
+ None,
554
+ None,
555
+ |scene| {
556
+ // Now that background has been drawn, offset pos and cx in order to draw our contents scrolled
557
+ let content_position = Point {
558
+ x: content_position.x - node.scroll_offset().x,
559
+ y: content_position.y - node.scroll_offset().y,
560
+ };
561
+
562
+ cx.transform = cx.transform.then_translate(Vec2 {
563
+ x: -node.scroll_offset().x * self.scale,
564
+ y: -node.scroll_offset().y * self.scale,
565
+ });
566
+ cx.draw_image(scene);
567
+ #[cfg(feature = "svg")]
568
+ cx.draw_svg(scene);
569
+ #[cfg(feature = "custom-widget")]
570
+ cx.draw_custom_widget(scene);
571
+ cx.draw_sub_document(scene);
572
+ cx.draw_input(scene);
573
+ cx.draw_text_input_text(scene, content_position);
574
+ cx.draw_inline_layout(scene, content_position);
575
+ cx.draw_marker(scene, content_position);
576
+ cx.draw_children(scene, cx.transform, child_clip_rect);
577
+ },
578
+ );
579
+
580
+ // Overlay scrollbars, drawn unscrolled above the
581
+ // clipped content.
582
+ #[cfg(feature = "scrollbars")]
583
+ {
584
+ cx.transform = unscrolled_transform;
585
+ cx.draw_scrollbars(scene);
586
+ }
587
+ },
588
+ );
589
+
590
+ // Apply the CSS `mask` (if any) to the content drawn above
591
+ cx.transform = unscrolled_transform;
592
+ cx.maybe_pop_css_mask_layer(scene, mask_layer_pushed);
593
+ },
594
+ );
595
+
596
+ self.layer_manager
597
+ .maybe_pop_layer(scene, css_clip_layer_pushed);
598
+ }
599
+
600
+ fn render_node(
601
+ &self,
602
+ scene: &mut impl PaintScene,
603
+ node_id: NodeId,
604
+ parent_style_transform: Affine,
605
+ clip_rect: Rect,
606
+ ) {
607
+ let node = &self.dom.as_ref().tree()[node_id];
608
+
609
+ match &node.data {
610
+ NodeData::Element(_) | NodeData::AnonymousBlock(_) => {
611
+ self.render_element(scene, node_id, parent_style_transform, clip_rect)
612
+ }
613
+ NodeData::Text(TextNodeData { .. }) => {
614
+ // Text nodes should never be rendered directly
615
+ // (they should always be rendered as part of an inline layout)
616
+ // unreachable!()
617
+ }
618
+ NodeData::Document(_) => {}
619
+ // NodeData::Doctype => {}
620
+ NodeData::Comment { .. } => {} // NodeData::ProcessingInstruction { .. } => {}
621
+ }
622
+ }
623
+
624
+ fn element_cx(
625
+ &'dom self,
626
+ node: &'dom Node,
627
+ layout: Layout,
628
+ transform: Affine,
629
+ custom_widget_scene: Option<&'a Scene>,
630
+ ) -> ElementCx<'dom, 'a> {
631
+ let style = node
632
+ .stylo_element_data()
633
+ .primary_styles()
634
+ .as_ref()
635
+ .map(|styles| (*styles).clone())
636
+ .unwrap_or_else(|| {
637
+ ComputedValues::initial_values_with_font_override(Font::initial_values()).to_arc()
638
+ });
639
+
640
+ let scale = self.scale;
641
+
642
+ // todo: maybe cache this so we don't need to constantly be figuring it out
643
+ // It is quite a bit of math to calculate during render/traverse
644
+ // Also! we can cache the bezpaths themselves, saving us a bunch of work
645
+ let frame = create_css_rect(&style, &layout, scale);
646
+
647
+ let element = node.element_data().unwrap();
648
+
649
+ ElementCx {
650
+ context: self,
651
+ frame,
652
+ scale,
653
+ style,
654
+ node,
655
+ element,
656
+ transform,
657
+ #[cfg(feature = "svg")]
658
+ svg: match element.image_data() {
659
+ Some(blitz_dom::node::ImageData::Svg(svg)) => Some(svg),
660
+ _ => None,
661
+ },
662
+ text_input: element.text_input_data(),
663
+ list_item: element.list_item_data.as_deref(),
664
+ devtools: self.dom.devtools(),
665
+ custom_widget_scene,
666
+ }
667
+ }
668
+ }
669
+
670
+ fn to_image_quality(image_rendering: ImageRendering) -> peniko::ImageQuality {
671
+ match image_rendering {
672
+ ImageRendering::Auto => peniko::ImageQuality::Medium,
673
+ ImageRendering::CrispEdges => peniko::ImageQuality::Low,
674
+ ImageRendering::Pixelated => peniko::ImageQuality::Low,
675
+ }
676
+ }
677
+
678
+ /// The transform from an SVG's rendered tree to a viewport of the given size.
679
+ #[cfg(feature = "svg")]
680
+ fn svg_viewport_affine(svg: &blitz_dom::node::SvgImageData, width: f32, height: f32) -> Affine {
681
+ let t = svg.viewport_transform(width, height);
682
+ Affine::new([t.sx, t.ky, t.kx, t.sy, t.tx, t.ty].map(f64::from))
683
+ }
684
+
685
+ /// Ensure that the `resized_image` field has a correctly sized image
686
+ fn to_peniko_image(image: &RasterImageData, quality: peniko::ImageQuality) -> peniko::ImageBrush {
687
+ peniko::ImageBrush {
688
+ image: ImageData {
689
+ data: image.data.clone(),
690
+ format: peniko::ImageFormat::Rgba8,
691
+ width: image.width,
692
+ height: image.height,
693
+ alpha_type: peniko::ImageAlphaType::Alpha,
694
+ },
695
+ sampler: ImageSampler {
696
+ x_extend: peniko::Extend::Repeat,
697
+ y_extend: peniko::Extend::Repeat,
698
+ quality,
699
+ alpha: 1.0,
700
+ },
701
+ }
702
+ }
703
+
704
+ /// A context of loaded and hot data to draw the element from
705
+ struct ElementCx<'dom, 'a> {
706
+ context: &'dom BlitzDomPainter<'dom, 'a>,
707
+ frame: CssBox,
708
+ style: style::servo_arc::Arc<ComputedValues>,
709
+ scale: f64,
710
+ node: &'dom Node,
711
+ element: &'dom ElementData,
712
+ transform: Affine,
713
+ #[cfg(feature = "svg")]
714
+ svg: Option<&'dom blitz_dom::node::SvgImageData>,
715
+ text_input: Option<&'dom TextInputData>,
716
+ list_item: Option<&'dom ListItemLayout>,
717
+ devtools: &'dom DevtoolSettings,
718
+ #[cfg_attr(not(feature = "custom-widget"), expect(unused))]
719
+ custom_widget_scene: Option<&'a Scene>,
720
+ }
721
+
722
+ /// Converts parley BoundingBox into peniko Rect
723
+ fn convert_rect(rect: &parley::BoundingBox) -> kurbo::Rect {
724
+ peniko::kurbo::Rect::new(rect.x0, rect.y0, rect.x1, rect.y1)
725
+ }
726
+
727
+ impl ElementCx<'_, '_> {
728
+ /// Paint overlay scrollbar thumbs for scroll containers: `overflow:
729
+ /// scroll`, or `auto` when the content overflows (never `hidden`/`clip`,
730
+ /// which scroll only programmatically). Thumbs appear on scroll and fade
731
+ /// out after a delay ([`BaseDocument::scrollbar_opacity`]); never-scrolled
732
+ /// containers paint nothing, keeping thumbs out of static reftest
733
+ /// screenshots.
734
+ ///
735
+ /// Geometry comes from [`Node::scrollbar_thumb`], shared with the
736
+ /// thumb-drag hit testing in blitz-dom.
737
+ #[cfg(feature = "scrollbars")]
738
+ fn draw_scrollbars(&self, scene: &mut impl PaintScene) {
739
+ // css-scrollbars-1 scrollbar-color: author thumb/track colors
740
+ use blitz_dom::node::{ScrollbarColor, ScrollbarRef};
741
+ use taffy::AbsoluteAxis;
742
+ let (custom_thumb, custom_track) = match self.node.scrollbar_color() {
743
+ ScrollbarColor::Auto => (None, None),
744
+ ScrollbarColor::Colors { thumb, track } => {
745
+ (Some(thumb.as_srgb_color()), Some(track.as_srgb_color()))
746
+ }
747
+ };
748
+
749
+ let drag_target = self.context.scrollbar_drag_target;
750
+ let hovered_thumb = self.context.hovered_scrollbar;
751
+
752
+ // scrollbar-color doesn't affect overlay visibility: persistence is
753
+ // UA policy, not author styling.
754
+ let node_id = self.node.id;
755
+ let opacity = self.context.dom.scrollbar_opacity(node_id);
756
+ if opacity == 0.0 {
757
+ return;
758
+ }
759
+
760
+ // Default thumb palette for the used color scheme; thumbs paint as
761
+ // fill plus a thin contrast stroke so they read over same-colored
762
+ // content.
763
+ let dark_scheme =
764
+ self.context.dom.viewport().color_scheme == blitz_traits::shell::ColorScheme::Dark;
765
+ let (thumb_rest, thumb_hover, thumb_active, stroke_color) = if dark_scheme {
766
+ (
767
+ Color::from_rgba8(214, 214, 214, 178),
768
+ Color::from_rgba8(190, 190, 190, 222),
769
+ Color::from_rgba8(172, 172, 172, 255),
770
+ Color::from_rgba8(0, 0, 0, 102),
771
+ )
772
+ } else {
773
+ (
774
+ Color::from_rgba8(128, 128, 128, 178),
775
+ Color::from_rgba8(152, 152, 152, 222),
776
+ Color::from_rgba8(170, 170, 170, 255),
777
+ Color::from_rgba8(255, 255, 255, 102),
778
+ )
779
+ };
780
+
781
+ // Chromium's hovered/pressed scrollbar contrast ratios.
782
+ const HOVER_CONTRAST: f32 = 1.8;
783
+ const ACTIVE_CONTRAST: f32 = 1.3;
784
+
785
+ for axis in [AbsoluteAxis::Vertical, AbsoluteAxis::Horizontal] {
786
+ if !self.node.wants_scrollbar(axis) {
787
+ continue;
788
+ }
789
+ let Some(thumb) = self.node.scrollbar_thumb(axis) else {
790
+ continue;
791
+ };
792
+
793
+ let rect = thumb.scale_from_origin(self.scale);
794
+
795
+ // Track (only when the author specified a track color)
796
+ if let Some(track_color) = custom_track {
797
+ let padding_box = self.frame.padding_box;
798
+ let track_rect = match axis {
799
+ AbsoluteAxis::Horizontal => {
800
+ Rect::new(padding_box.x0, rect.y0, padding_box.x1, rect.y1)
801
+ }
802
+ AbsoluteAxis::Vertical => {
803
+ Rect::new(rect.x0, padding_box.y0, rect.x1, padding_box.y1)
804
+ }
805
+ };
806
+ scene.fill(
807
+ Fill::NonZero,
808
+ self.transform,
809
+ track_color.multiply_alpha(opacity),
810
+ None,
811
+ &track_rect,
812
+ );
813
+ }
814
+
815
+ let this = ScrollbarRef { node_id, axis };
816
+ let is_active = drag_target == Some(this);
817
+ let is_hovered = hovered_thumb == Some(this);
818
+ let color = match custom_thumb {
819
+ Some(base) if is_active => crate::color::blend_for_contrast(base, ACTIVE_CONTRAST),
820
+ Some(base) if is_hovered => crate::color::blend_for_contrast(base, HOVER_CONTRAST),
821
+ Some(base) => base,
822
+ None if is_active => thumb_active,
823
+ None if is_hovered => thumb_hover,
824
+ None => thumb_rest,
825
+ };
826
+ let radius = match axis {
827
+ AbsoluteAxis::Horizontal => rect.height() / 2.0,
828
+ AbsoluteAxis::Vertical => rect.width() / 2.0,
829
+ };
830
+ scene.fill(
831
+ Fill::NonZero,
832
+ self.transform,
833
+ color.multiply_alpha(opacity),
834
+ None,
835
+ &rect.to_rounded_rect(radius),
836
+ );
837
+ // Contrast stroke, default thumbs only: an author-specified
838
+ // scrollbar-color is rendered exactly as given.
839
+ if custom_thumb.is_none() {
840
+ let stroke_width = self.scale;
841
+ let stroke_rect = rect.inset(-stroke_width / 2.0);
842
+ scene.stroke(
843
+ &Stroke::new(stroke_width),
844
+ self.transform,
845
+ stroke_color.multiply_alpha(opacity),
846
+ None,
847
+ &stroke_rect.to_rounded_rect(radius - stroke_width / 2.0),
848
+ );
849
+ }
850
+ }
851
+ }
852
+
853
+ fn draw_inline_layout(&self, scene: &mut impl PaintScene, pos: Point) {
854
+ if self.node.flags.is_inline_root() {
855
+ let text_layout = self.element
856
+ .inline_layout_data
857
+ .as_ref()
858
+ .unwrap_or_else(|| {
859
+ panic!("Tried to render node marked as inline root that does not have an inline layout: {:?}", self.node);
860
+ });
861
+
862
+ let pos = Point {
863
+ x: pos.x,
864
+ y: pos.y + text_layout.block_offset as f64,
865
+ };
866
+ let transform =
867
+ self.transform * Affine::translate((pos.x * self.scale, pos.y * self.scale));
868
+
869
+ // Render inline element backgrounds (e.g. `<span style="background: ...">`)
870
+ // behind the text and selection highlight.
871
+ crate::text::draw_inline_backgrounds(
872
+ scene,
873
+ text_layout.layout.lines(),
874
+ self.context.dom,
875
+ transform,
876
+ self.node.id,
877
+ );
878
+
879
+ // Render text selection highlight (if any) using cached selection ranges
880
+ if let Some(&(sel_start, sel_end)) = self.context.selection_ranges.get(&self.node.id) {
881
+ crate::text::draw_text_selection(
882
+ scene,
883
+ &text_layout.layout,
884
+ transform,
885
+ sel_start,
886
+ sel_end,
887
+ );
888
+ }
889
+
890
+ // Render text
891
+ if let Some(observer) = self.context.text_observer {
892
+ observer
893
+ .borrow_mut()
894
+ .observe(&text_layout.layout, &text_layout.text);
895
+ }
896
+ let mut draw_text_context = self.context.draw_text_context.borrow_mut();
897
+ crate::text::stroke_text(
898
+ scene,
899
+ text_layout.layout.lines(),
900
+ self.context.dom,
901
+ transform,
902
+ self.scale,
903
+ self.node.id,
904
+ &mut draw_text_context,
905
+ );
906
+ }
907
+ }
908
+
909
+ fn draw_text_input_text(&self, scene: &mut impl PaintScene, pos: Point) {
910
+ // Render the text in text inputs
911
+ if let Some(input_data) = self.text_input {
912
+ // For single-line inputs, add an offset to vertically center the text input layout
913
+ // within the content box of it's node.
914
+ let y_offset = self.node.text_input_v_centering_offset(self.scale);
915
+ let pos = Point {
916
+ x: pos.x,
917
+ y: pos.y + y_offset,
918
+ };
919
+
920
+ // Apply the scroll offset (stored in CSS pixels, scaled here to device pixels) so
921
+ // that the caret stays visible. Single-line inputs scroll horizontally; multi-line
922
+ // inputs scroll vertically.
923
+ let scroll_offset = input_data.scroll_offset as f64 * self.scale;
924
+ let (scroll_x, scroll_y) = if input_data.is_multiline {
925
+ (0.0, scroll_offset)
926
+ } else {
927
+ (scroll_offset, 0.0)
928
+ };
929
+
930
+ let transform = self.transform
931
+ * Affine::translate((pos.x * self.scale - scroll_x, pos.y * self.scale - scroll_y));
932
+
933
+ if self.node.is_focussed() {
934
+ // Render selection/caret
935
+ for (rect, _line_idx) in input_data.editor.selection_geometry().iter() {
936
+ scene.fill(
937
+ Fill::NonZero,
938
+ transform,
939
+ SELECTION_COLOR,
940
+ None,
941
+ &convert_rect(rect),
942
+ );
943
+ }
944
+ if let Some(cursor) = input_data.editor.cursor_geometry(1.5) {
945
+ let color = self.style.get_inherited_text().color;
946
+ let caret_color = match &self.style.get_inherited_ui().caret_color.0 {
947
+ ColorOrAuto::Auto => color,
948
+ ColorOrAuto::Color(caret_color) => caret_color.resolve_to_absolute(&color),
949
+ };
950
+
951
+ scene.fill(
952
+ Fill::NonZero,
953
+ transform,
954
+ caret_color.as_srgb_color(),
955
+ None,
956
+ &convert_rect(&cursor),
957
+ );
958
+ };
959
+ }
960
+
961
+ // Render text
962
+ if let Some(observer) = self.context.text_observer {
963
+ observer.borrow_mut().observe(
964
+ input_data.editor.try_layout().unwrap(),
965
+ input_data.editor.raw_text(),
966
+ );
967
+ }
968
+ let mut draw_text_context = self.context.draw_text_context.borrow_mut();
969
+ crate::text::stroke_text(
970
+ scene,
971
+ input_data.editor.try_layout().unwrap().lines(),
972
+ self.context.dom,
973
+ transform,
974
+ self.scale,
975
+ self.node.id,
976
+ &mut draw_text_context,
977
+ );
978
+ }
979
+ }
980
+
981
+ fn draw_marker(&self, scene: &mut impl PaintScene, pos: Point) {
982
+ if let Some(ListItemLayout {
983
+ marker,
984
+ position: ListItemLayoutPosition::Outside(layout),
985
+ }) = self.list_item
986
+ {
987
+ // Right align and pad the bullet when rendering outside
988
+ let x_padding = match marker {
989
+ Marker::Char(_) => 8.0,
990
+ Marker::String(_) => 0.0,
991
+ };
992
+ // Outside markers are placed outside the list item's border box
993
+ // (`pos` is the origin of its content box)
994
+ let item_layout = self.node.final_layout();
995
+ let x_offset = -(layout.full_width() / layout.scale()
996
+ + x_padding
997
+ + item_layout.padding.left
998
+ + item_layout.border.left);
999
+
1000
+ // Align the marker with the baseline of the first line of text in the list item
1001
+ let y_offset = if let Some((text_layout, first_text_line)) = &self
1002
+ .element
1003
+ .inline_layout_data
1004
+ .as_ref()
1005
+ .and_then(|text_layout| Some((text_layout, text_layout.layout.lines().next()?)))
1006
+ {
1007
+ (first_text_line.metrics().baseline
1008
+ - layout.lines().next().unwrap().metrics().baseline)
1009
+ / layout.scale()
1010
+ + text_layout.block_offset
1011
+ } else {
1012
+ 0.0
1013
+ };
1014
+
1015
+ let pos = Point {
1016
+ x: pos.x + x_offset as f64,
1017
+ y: pos.y + y_offset as f64,
1018
+ };
1019
+
1020
+ let transform =
1021
+ self.transform * Affine::translate((pos.x * self.scale, pos.y * self.scale));
1022
+
1023
+ if let Some(observer) = self.context.text_observer {
1024
+ let text = match marker {
1025
+ Marker::Char(c) => c.to_string(),
1026
+ Marker::String(s) => s.clone(),
1027
+ };
1028
+ observer.borrow_mut().observe(layout, &text);
1029
+ }
1030
+ let mut draw_text_context = self.context.draw_text_context.borrow_mut();
1031
+ crate::text::stroke_text(
1032
+ scene,
1033
+ layout.lines(),
1034
+ self.context.dom,
1035
+ transform,
1036
+ self.scale,
1037
+ self.node.id,
1038
+ &mut draw_text_context,
1039
+ );
1040
+ }
1041
+ }
1042
+
1043
+ fn draw_children(
1044
+ &self,
1045
+ scene: &mut impl PaintScene,
1046
+ parent_style_transform: Affine,
1047
+ clip_rect: Rect,
1048
+ ) {
1049
+ // Negative z_index hoisted nodes
1050
+
1051
+ if let Some(hoisted) = &self.node.stacking_context {
1052
+ for hoisted_child in hoisted.neg_z_hoisted_children() {
1053
+ let position = hoisted_child.position(self.node.tree(), self.node.id);
1054
+ let pos = kurbo::Vec2 {
1055
+ x: position.x as f64 * self.scale,
1056
+ y: position.y as f64 * self.scale,
1057
+ };
1058
+ self.render_node(
1059
+ scene,
1060
+ hoisted_child.node_id,
1061
+ parent_style_transform.pre_translate(pos),
1062
+ clip_rect,
1063
+ );
1064
+ }
1065
+ }
1066
+
1067
+ // Regular children
1068
+ if let Some(children) = &*self.node.paint_children.borrow() {
1069
+ for child_id in children {
1070
+ // Fixed-position children of the root element are positioned against
1071
+ // the viewport and do not scroll with it, so cancel out the viewport
1072
+ // scroll (applied in `paint_scene`). A fixed box contained by a
1073
+ // transformed ancestor scrolls with it like any out-of-flow box.
1074
+ let child = &self.context.dom.as_ref().tree()[*child_id];
1075
+ let child_transform = if child.taffy_position() == taffy::Position::Fixed
1076
+ && Some(self.node.id) == self.context.root_element_id
1077
+ {
1078
+ let scroll = self.context.dom.as_ref().viewport_scroll();
1079
+ parent_style_transform.pre_translate(kurbo::Vec2 {
1080
+ x: scroll.x * self.scale,
1081
+ y: scroll.y * self.scale,
1082
+ })
1083
+ } else {
1084
+ parent_style_transform
1085
+ };
1086
+ self.render_node(scene, *child_id, child_transform, clip_rect);
1087
+ }
1088
+ }
1089
+
1090
+ // Positive z_index hoisted nodes
1091
+ if let Some(hoisted) = &self.node.stacking_context {
1092
+ for hoisted_child in hoisted.pos_z_hoisted_children() {
1093
+ let position = hoisted_child.position(self.node.tree(), self.node.id);
1094
+ let pos = kurbo::Vec2 {
1095
+ x: position.x as f64 * self.scale,
1096
+ y: position.y as f64 * self.scale,
1097
+ };
1098
+ self.render_node(
1099
+ scene,
1100
+ hoisted_child.node_id,
1101
+ parent_style_transform.pre_translate(pos),
1102
+ clip_rect,
1103
+ );
1104
+ }
1105
+ }
1106
+ }
1107
+
1108
+ #[cfg(feature = "svg")]
1109
+ fn draw_svg(&self, scene: &mut impl PaintScene) {
1110
+ use style::properties::generated::longhands::object_fit::computed_value::T as ObjectFit;
1111
+
1112
+ let Some(svg) = self.svg else {
1113
+ return;
1114
+ };
1115
+
1116
+ let width = self.frame.content_box.width() as f32;
1117
+ let height = self.frame.content_box.height() as f32;
1118
+ let svg_size = svg.tree.size();
1119
+
1120
+ let x = self.frame.content_box.origin().x;
1121
+ let y = self.frame.content_box.origin().y;
1122
+
1123
+ // An SVG with an aspect ratio but no natural size is sized by the default sizing
1124
+ // algorithm, which for `none` and `scale-down` contains it within the content box.
1125
+ let object_fit = match self.style.clone_object_fit() {
1126
+ ObjectFit::None | ObjectFit::ScaleDown
1127
+ if svg.intrinsic_width().is_none()
1128
+ && svg.intrinsic_height().is_none()
1129
+ && svg.viewbox_aspect_ratio().is_some() =>
1130
+ {
1131
+ ObjectFit::Contain
1132
+ }
1133
+ object_fit => object_fit,
1134
+ };
1135
+ let object_position = self.style.clone_object_position();
1136
+
1137
+ // Apply object-fit algorithm
1138
+ let container_size = taffy::Size { width, height };
1139
+ let object_size = taffy::Size {
1140
+ width: svg_size.width(),
1141
+ height: svg_size.height(),
1142
+ };
1143
+ let paint_size = compute_object_fit(container_size, Some(object_size), object_fit);
1144
+
1145
+ // Compute object-position
1146
+ let x_offset = object_position.horizontal.resolve(
1147
+ CSSPixelLength::new(container_size.width - paint_size.width) / self.scale as f32,
1148
+ ) * self.scale as f32;
1149
+ let y_offset = object_position.vertical.resolve(
1150
+ CSSPixelLength::new(container_size.height - paint_size.height) / self.scale as f32,
1151
+ ) * self.scale as f32;
1152
+ let x = x + x_offset.px() as f64;
1153
+ let y = y + y_offset.px() as f64;
1154
+
1155
+ let viewport = svg_viewport_affine(svg, paint_size.width, paint_size.height);
1156
+ let transform = (self.transform * viewport).then_translate(Vec2 { x, y });
1157
+
1158
+ self.with_replaced_content_clip(
1159
+ scene,
1160
+ container_size,
1161
+ paint_size,
1162
+ x_offset.px(),
1163
+ y_offset.px(),
1164
+ |scene| {
1165
+ anyrender_svg::render_svg_tree(scene, &svg.tree, transform);
1166
+ },
1167
+ );
1168
+ }
1169
+
1170
+ /// Clip replaced content to the content box when its object-fit/object-position
1171
+ /// rect overflows it.
1172
+ fn with_replaced_content_clip<S: PaintScene>(
1173
+ &self,
1174
+ scene: &mut S,
1175
+ container_size: taffy::Size<f32>,
1176
+ paint_size: taffy::Size<f32>,
1177
+ x_offset: f32,
1178
+ y_offset: f32,
1179
+ paint: impl FnOnce(&mut S),
1180
+ ) {
1181
+ let overflows = x_offset < 0.0
1182
+ || y_offset < 0.0
1183
+ || x_offset + paint_size.width > container_size.width
1184
+ || y_offset + paint_size.height > container_size.height;
1185
+ self.context.layer_manager.maybe_with_layer(
1186
+ scene,
1187
+ overflows,
1188
+ 1.0,
1189
+ self.transform,
1190
+ &self.frame.content_box_path(),
1191
+ None,
1192
+ None,
1193
+ paint,
1194
+ );
1195
+ }
1196
+
1197
+ fn draw_image(&self, scene: &mut impl PaintScene) {
1198
+ if let Some(image) = self.element.raster_image_data() {
1199
+ let width = self.frame.content_box.width() as u32;
1200
+ let height = self.frame.content_box.height() as u32;
1201
+ let x = self.frame.content_box.origin().x;
1202
+ let y = self.frame.content_box.origin().y;
1203
+
1204
+ let object_fit = self.style.clone_object_fit();
1205
+ let object_position = self.style.clone_object_position();
1206
+ let image_rendering = self.style.clone_image_rendering();
1207
+ let quality = to_image_quality(image_rendering);
1208
+
1209
+ // Apply object-fit algorithm
1210
+ let container_size = taffy::Size {
1211
+ width: width as f32,
1212
+ height: height as f32,
1213
+ };
1214
+ let object_size = taffy::Size {
1215
+ width: image.width as f32,
1216
+ height: image.height as f32,
1217
+ };
1218
+ let paint_size = compute_object_fit(container_size, Some(object_size), object_fit);
1219
+
1220
+ // Compute object-position
1221
+ let x_offset = object_position.horizontal.resolve(
1222
+ CSSPixelLength::new(container_size.width - paint_size.width) / self.scale as f32,
1223
+ ) * self.scale as f32;
1224
+ let y_offset = object_position.vertical.resolve(
1225
+ CSSPixelLength::new(container_size.height - paint_size.height) / self.scale as f32,
1226
+ ) * self.scale as f32;
1227
+ let x = x + x_offset.px() as f64;
1228
+ let y = y + y_offset.px() as f64;
1229
+
1230
+ let x_scale = paint_size.width as f64 / object_size.width as f64;
1231
+ let y_scale = paint_size.height as f64 / object_size.height as f64;
1232
+ let transform = self
1233
+ .transform
1234
+ .pre_translate(Vec2 { x, y })
1235
+ .pre_scale_non_uniform(x_scale, y_scale);
1236
+
1237
+ self.with_replaced_content_clip(
1238
+ scene,
1239
+ container_size,
1240
+ paint_size,
1241
+ x_offset.px(),
1242
+ y_offset.px(),
1243
+ |scene| {
1244
+ scene.draw_image(to_peniko_image(image, quality).as_ref(), transform);
1245
+ },
1246
+ );
1247
+ }
1248
+ }
1249
+
1250
+ #[cfg(feature = "custom-widget")]
1251
+ fn draw_custom_widget(&self, scene: &mut impl PaintScene) {
1252
+ if let Some(widget_scene) = self.custom_widget_scene {
1253
+ let x = self.frame.content_box.origin().x;
1254
+ let y = self.frame.content_box.origin().y;
1255
+ let transform = self.transform.then_translate(Vec2 { x, y });
1256
+
1257
+ // TODO: eliminate clone
1258
+ scene.append_scene(widget_scene.clone(), transform);
1259
+ }
1260
+ }
1261
+
1262
+ fn draw_sub_document(&self, scene: &mut impl PaintScene) {
1263
+ if let Some(sub_doc) = self.element.sub_doc_data().map(|doc| doc.inner()) {
1264
+ let scale = self.scale;
1265
+ let width = self.frame.content_box.width() as u32;
1266
+ let height = self.frame.content_box.height() as u32;
1267
+
1268
+ // TODO: Support arbitrary transforms of subdocuments
1269
+ let translation = self.transform.translation();
1270
+ let initial_x = translation.x + self.frame.content_box.origin().x;
1271
+ let initial_y = translation.y + self.frame.content_box.origin().y;
1272
+ // let transform = self.transform.then_translate(Vec2 { x, y });
1273
+
1274
+ let painter = BlitzDomPainter::new(
1275
+ &sub_doc,
1276
+ scale,
1277
+ width,
1278
+ height,
1279
+ initial_x,
1280
+ initial_y,
1281
+ self.custom_widget_scenes,
1282
+ self.context.text_observer,
1283
+ );
1284
+ painter.paint_scene(scene);
1285
+ }
1286
+ }
1287
+
1288
+ fn stroke_devtools(&self, scene: &mut impl PaintScene) {
1289
+ if self.devtools.show_layout {
1290
+ let shape = &self.frame.border_box;
1291
+ let stroke = Stroke::new(self.scale);
1292
+
1293
+ let stroke_color = match self.node.taffy_display() {
1294
+ taffy::Display::Block | taffy::Display::FlowRoot => {
1295
+ Color::new([1.0, 0.0, 0.0, 1.0])
1296
+ }
1297
+ taffy::Display::Flex => Color::new([0.0, 1.0, 0.0, 1.0]),
1298
+ taffy::Display::Grid => Color::new([0.0, 0.0, 1.0, 1.0]),
1299
+ taffy::Display::None => Color::new([0.0, 0.0, 1.0, 1.0]),
1300
+ };
1301
+
1302
+ scene.stroke(&stroke, self.transform, stroke_color, None, &shape);
1303
+ }
1304
+ }
1305
+ }
1306
+ impl<'dom, 'a> std::ops::Deref for ElementCx<'dom, 'a> {
1307
+ type Target = BlitzDomPainter<'dom, 'a>;
1308
+ fn deref(&self) -> &Self::Target {
1309
+ self.context
1310
+ }
1311
+ }
1312
+
1313
+ fn insets_from_taffy_rect(input: taffy::Rect<f64>) -> Insets {
1314
+ Insets {
1315
+ x0: input.left,
1316
+ y0: input.top,
1317
+ x1: input.right,
1318
+ y1: input.bottom,
1319
+ }
1320
+ }
1321
+
1322
+ /// Convert Stylo and Taffy types into Kurbo types
1323
+ fn create_css_rect(style: &ComputedValues, layout: &Layout, scale: f64) -> CssBox {
1324
+ // Resolve and rescale
1325
+ // We have to scale since document pixels are not same same as rendered pixels
1326
+ let width: f64 = layout.size.width as f64;
1327
+ let height: f64 = layout.size.height as f64;
1328
+ let border_box = Rect::new(0.0, 0.0, width * scale, height * scale);
1329
+ let border = insets_from_taffy_rect(layout.border.map(|p| p as f64 * scale));
1330
+ let padding = insets_from_taffy_rect(layout.padding.map(|p| p as f64 * scale));
1331
+ let outline_width = style.get_outline().outline_width.0.to_f64_px() * scale;
1332
+
1333
+ // Resolve the radii to a length. need to downscale since the radii are in document pixels
1334
+ let resolve_w = CSSPixelLength::new(width as _);
1335
+ let resolve_h = CSSPixelLength::new(height as _);
1336
+ let resolve_radii = |radius: &BorderCornerRadius| -> Vec2 {
1337
+ Vec2 {
1338
+ x: scale * radius.0.width.0.resolve(resolve_w).px() as f64,
1339
+ y: scale * radius.0.height.0.resolve(resolve_h).px() as f64,
1340
+ }
1341
+ };
1342
+ let s_border = style.get_border();
1343
+ let border_radii = NonUniformRoundedRectRadii {
1344
+ top_left: resolve_radii(&s_border.border_top_left_radius),
1345
+ top_right: resolve_radii(&s_border.border_top_right_radius),
1346
+ bottom_right: resolve_radii(&s_border.border_bottom_right_radius),
1347
+ bottom_left: resolve_radii(&s_border.border_bottom_left_radius),
1348
+ };
1349
+
1350
+ CssBox::new(border_box, border, padding, outline_width, border_radii)
1351
+ }