blitz-pdf 0.1.4 → 0.1.6
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 +67 -3
- package/THIRD_PARTY_NOTICES.md +2 -0
- package/dist/api.d.ts +9 -1
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +25 -27
- package/dist/api.js.map +1 -1
- package/dist/browser.d.ts +1 -1
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +13 -1
- package/dist/browser.js.map +1 -1
- package/dist/node-worker.d.ts +2 -0
- package/dist/node-worker.d.ts.map +1 -0
- package/dist/node-worker.js +10 -0
- package/dist/node-worker.js.map +1 -0
- package/dist/node.d.ts +1 -1
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +18 -6
- package/dist/node.js.map +1 -1
- package/dist/render.d.ts +8 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +28 -0
- package/dist/render.js.map +1 -0
- package/dist/wasm.d.ts +3 -0
- package/dist/wasm.d.ts.map +1 -0
- package/dist/wasm.js +18 -0
- package/dist/wasm.js.map +1 -0
- package/dist/worker-host.d.ts +16 -0
- package/dist/worker-host.d.ts.map +1 -0
- package/dist/worker-host.js +31 -0
- package/dist/worker-host.js.map +1 -0
- package/dist/worker-protocol.d.ts +22 -0
- package/dist/worker-protocol.d.ts.map +1 -0
- package/dist/worker-protocol.js +2 -0
- package/dist/worker-protocol.js.map +1 -0
- package/dist/worker-runtime.d.ts +8 -0
- package/dist/worker-runtime.d.ts.map +1 -0
- package/dist/worker-runtime.js +34 -0
- package/dist/worker-runtime.js.map +1 -0
- package/dist/worker.d.ts +2 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +7 -0
- package/dist/worker.js.map +1 -0
- package/package.json +5 -2
- package/src/api.ts +33 -22
- package/src/browser.ts +13 -3
- package/src/node-worker.ts +9 -0
- package/src/node.ts +13 -4
- package/src/render.ts +28 -0
- package/src/wasm.ts +17 -0
- package/src/worker-host.ts +45 -0
- package/src/worker-protocol.ts +10 -0
- package/src/worker-runtime.ts +35 -0
- package/src/worker.ts +12 -0
- package/vendor/README.md +24 -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/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(¤t_color)
|
|
217
|
+
})
|
|
218
|
+
} else {
|
|
219
|
+
let current_color = root_element.primary_styles().unwrap().clone_color();
|
|
220
|
+
Some(html_color.resolve_to_absolute(¤t_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
|
+
}
|