blitz-pdf 0.1.5 → 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.
@@ -0,0 +1,103 @@
1
+ use super::ElementCx;
2
+ use crate::color::{Color, ToColorColor as _};
3
+ use anyrender::PaintScene;
4
+ use blitz_dom::local_name;
5
+ use kurbo::{Affine, BezPath, Cap, Circle, Join, Point, RoundedRect, Stroke, Vec2};
6
+ use peniko::Fill;
7
+ use style::dom::TElement as _;
8
+
9
+ impl ElementCx<'_, '_> {
10
+ pub(super) fn draw_input(&self, scene: &mut impl PaintScene) {
11
+ if self.node.local_name() != "input" {
12
+ return;
13
+ }
14
+ let Some(checked) = self.element.checkbox_input_checked() else {
15
+ return;
16
+ };
17
+
18
+ let type_attr = self.node.attr(local_name!("type"));
19
+ let disabled = self.node.attr(local_name!("disabled")).is_some();
20
+
21
+ // TODO this should be coming from css accent-color, but I couldn't find how to retrieve it
22
+ let accent_color = if disabled {
23
+ Color::from_rgba8(209, 209, 209, 255)
24
+ } else {
25
+ self.style.clone_color().as_srgb_color()
26
+ };
27
+
28
+ let width = self.frame.border_box.width();
29
+ let height = self.frame.border_box.height();
30
+ let min_dimension = width.min(height);
31
+ let scale = (min_dimension - 4.0).max(0.0) / 16.0;
32
+
33
+ let frame = self.frame.border_box.to_rounded_rect(scale * 2.0);
34
+
35
+ match type_attr {
36
+ Some("checkbox") => {
37
+ draw_checkbox(scene, checked, frame, self.transform, accent_color, scale);
38
+ }
39
+ Some("radio") => {
40
+ let center = frame.center();
41
+ draw_radio_button(scene, checked, center, self.transform, accent_color, scale);
42
+ }
43
+ _ => {}
44
+ }
45
+ }
46
+ }
47
+
48
+ fn draw_checkbox(
49
+ scene: &mut impl PaintScene,
50
+ checked: bool,
51
+ frame: RoundedRect,
52
+ transform: Affine,
53
+ accent_color: Color,
54
+ scale: f64,
55
+ ) {
56
+ if checked {
57
+ scene.fill(Fill::NonZero, transform, accent_color, None, &frame);
58
+ //Tick code derived from masonry
59
+ let mut path = BezPath::new();
60
+ path.move_to((2.0, 9.0));
61
+ path.line_to((6.0, 13.0));
62
+ path.line_to((14.0, 2.0));
63
+
64
+ path.apply_affine(Affine::translate(Vec2 { x: 2.0, y: 1.0 }).then_scale(scale));
65
+
66
+ let style = Stroke {
67
+ width: 2.0 * scale,
68
+ join: Join::Round,
69
+ miter_limit: 10.0,
70
+ start_cap: Cap::Round,
71
+ end_cap: Cap::Round,
72
+ dash_pattern: Default::default(),
73
+ dash_offset: 0.0,
74
+ };
75
+
76
+ scene.stroke(&style, transform, Color::WHITE, None, &path);
77
+ } else {
78
+ scene.fill(Fill::NonZero, transform, Color::WHITE, None, &frame);
79
+ scene.stroke(&Stroke::default(), transform, accent_color, None, &frame);
80
+ }
81
+ }
82
+
83
+ fn draw_radio_button(
84
+ scene: &mut impl PaintScene,
85
+ checked: bool,
86
+ center: Point,
87
+ transform: Affine,
88
+ accent_color: Color,
89
+ scale: f64,
90
+ ) {
91
+ let outer_ring = Circle::new(center, 8.0 * scale);
92
+ let gap = Circle::new(center, 6.0 * scale);
93
+ let inner_circle = Circle::new(center, 4.0 * scale);
94
+ if checked {
95
+ scene.fill(Fill::NonZero, transform, accent_color, None, &outer_ring);
96
+ scene.fill(Fill::NonZero, transform, Color::WHITE, None, &gap);
97
+ scene.fill(Fill::NonZero, transform, accent_color, None, &inner_circle);
98
+ } else {
99
+ const GRAY: Color = color::palette::css::GRAY;
100
+ scene.fill(Fill::NonZero, transform, GRAY, None, &outer_ring);
101
+ scene.fill(Fill::NonZero, transform, Color::WHITE, None, &gap);
102
+ }
103
+ }
@@ -0,0 +1,144 @@
1
+ //! Rendering for the CSS `mask` properties (`mask-image`, `mask-position`,
2
+ //! `mask-size`, `mask-repeat`, `mask-clip`, `mask-origin`).
3
+ //!
4
+ //! Masks are applied by:
5
+ //!
6
+ //! 1. Pushing an isolation layer (clipped to the mask painting area) before the
7
+ //! element is painted
8
+ //! 2. Painting the element (and its descendants) as normal
9
+ //! 3. Pushing a `Compose::DestIn` layer and drawing the mask image layers into it,
10
+ //! which multiplies the alpha of the already-painted element by the alpha of
11
+ //! the mask
12
+ //! 4. Popping both layers
13
+ //!
14
+ //! The mask image layers themselves are positioned/sized/repeated using the same
15
+ //! code as `background-image` layers (see `background.rs`), as the `mask-*` and
16
+ //! `background-*` properties share computed value types.
17
+ use crate::render::background::get_cyclic;
18
+
19
+ use super::ElementCx;
20
+ use super::background::ImageLayerStyles;
21
+ use anyrender::PaintScene;
22
+ use peniko::{BlendMode, Compose, Fill, Mix};
23
+ use style::properties::generated::longhands::mask_composite::single_value::computed_value::T as StyloMaskComposite;
24
+ use style::values::generics::image::GenericImage;
25
+
26
+ #[cfg(feature = "tracing")]
27
+ use tracing::warn;
28
+
29
+ /// An opacity just below 1.0, used to force the renderer to allocate an isolated
30
+ /// buffer for the mask isolation layer. Renderers (e.g. vello_cpu) may optimize
31
+ /// layers with `Mix::Normal`/`Compose::SrcOver` and an opacity of exactly 1.0 into
32
+ /// plain non-isolated clip layers, in which case the `Compose::DestIn` mask
33
+ /// compositing would erase the backdrop behind the element rather than just the
34
+ /// element's own content.
35
+ const ALMOST_OPAQUE: f32 = 1.0 - f32::EPSILON;
36
+
37
+ impl ElementCx<'_, '_> {
38
+ /// Whether the element has any CSS `mask-image` layers
39
+ pub(super) fn has_css_mask(&self) -> bool {
40
+ self.style
41
+ .get_svg()
42
+ .mask_image
43
+ .0
44
+ .iter()
45
+ .any(|image| !matches!(image, GenericImage::None))
46
+ }
47
+
48
+ /// If the element has a CSS mask, push an isolation layer for the masked content
49
+ /// to be drawn into. Returns whether a layer was pushed, which should later be
50
+ /// passed to [`maybe_pop_css_mask_layer`](Self::maybe_pop_css_mask_layer).
51
+ pub(super) fn maybe_push_css_mask_layer(&self, scene: &mut impl PaintScene) -> bool {
52
+ if !self.has_css_mask() {
53
+ return false;
54
+ }
55
+
56
+ // Content outside of the mask painting area (at largest the border box) has
57
+ // a mask alpha of 0, so we can clip the isolation layer to the border box.
58
+ scene.push_layer(
59
+ Fill::NonZero,
60
+ Mix::Normal,
61
+ ALMOST_OPAQUE,
62
+ self.transform,
63
+ &self.frame.border_box_path(),
64
+ None,
65
+ None,
66
+ );
67
+ true
68
+ }
69
+
70
+ /// Apply the CSS mask to the content drawn since the corresponding
71
+ /// [`maybe_push_css_mask_layer`](Self::maybe_push_css_mask_layer) by drawing the
72
+ /// mask image layers with `Compose::DestIn`, then pop the isolation layer.
73
+ pub(super) fn maybe_pop_css_mask_layer(&self, scene: &mut impl PaintScene, layer_pushed: bool) {
74
+ if !layer_pushed {
75
+ return;
76
+ }
77
+
78
+ scene.push_layer(
79
+ Fill::NonZero,
80
+ BlendMode::new(Mix::Normal, Compose::DestIn),
81
+ 1.0,
82
+ self.transform,
83
+ &self.frame.border_box_path(),
84
+ None,
85
+ None,
86
+ );
87
+ self.draw_css_mask(scene);
88
+ scene.pop_layer(); // Mask (DestIn) layer
89
+ scene.pop_layer(); // Isolation layer
90
+ }
91
+
92
+ /// Draw the mask image layers (analogous to `draw_background` for `background-image`)
93
+ fn draw_css_mask(&self, scene: &mut impl PaintScene) {
94
+ let svg_styles = self.style.get_svg();
95
+ let image_data = &self.element.mask_images;
96
+ let layer_count = svg_styles.mask_image.0.len();
97
+
98
+ for idx in (0..layer_count).rev() {
99
+ let layer = ImageLayerStyles::from_svg(svg_styles, image_data, idx);
100
+ let mask_clip_path = self.box_path(layer.clip);
101
+
102
+ // TODO: support `mask-mode: luminance` (luminance masks are currently
103
+ // rendered as alpha masks).
104
+ #[cfg(feature = "tracing")]
105
+ {
106
+ use style::properties::generated::longhands::mask_mode::single_value::computed_value::T as StyloMaskMode;
107
+ let mask_mode = &svg_styles.mask_mode.0;
108
+ if matches!(get_cyclic(&mask_mode, idx), StyloMaskMode::Luminance) {
109
+ warn!("mask-mode: luminance is not supported (falling back to alpha)");
110
+ }
111
+ }
112
+
113
+ // Each mask layer is composited with the (already drawn) layers below it
114
+ // using the Porter-Duff operator given by its `mask-composite` value. The
115
+ // bottommost layer has no layers below it and is always drawn with SrcOver.
116
+ let compose = if idx == layer_count - 1 {
117
+ Compose::SrcOver
118
+ } else {
119
+ let composite_list = &svg_styles.mask_composite.0;
120
+ match get_cyclic(composite_list, idx) {
121
+ StyloMaskComposite::Add => Compose::SrcOver,
122
+ StyloMaskComposite::Subtract => Compose::SrcOut,
123
+ StyloMaskComposite::Intersect => Compose::SrcIn,
124
+ StyloMaskComposite::Exclude => Compose::Xor,
125
+ }
126
+ };
127
+
128
+ // The layer is pushed unconditionally (even for `mask-image: none` layers,
129
+ // which draw nothing) as compositing a transparent black layer with e.g.
130
+ // `intersect` clears the mask built up so far.
131
+ scene.push_layer(
132
+ Fill::NonZero,
133
+ BlendMode::new(Mix::Normal, compose),
134
+ 1.0,
135
+ self.transform,
136
+ &mask_clip_path,
137
+ None,
138
+ None,
139
+ );
140
+ self.draw_image_layer(scene, &layer);
141
+ scene.pop_layer();
142
+ }
143
+ }
144
+ }