Skip to main content

blitz_paint/
lib.rs

1//! Paint a [`blitz_dom::BaseDocument`] by pushing [`anyrender`] drawing commands into
2//! an impl [`anyrender::PaintScene`].
3
4#![allow(clippy::collapsible_if)]
5
6mod color;
7mod debug_overlay;
8mod filters;
9mod gradient;
10mod kurbo_css;
11mod layers;
12mod render;
13
14pub use layers::{LayerSite, SceneLayerCounts, latest_scene_layers};
15mod sizing;
16mod text;
17
18use std::collections::HashMap;
19
20/// A resolved document region painted into a smaller output surface.
21///
22/// Layout remains the layout of the full document. Only scene coordinates and
23/// culling change, so a diagnostic crop answers what the application actually
24/// drew at that position without rasterising unrelated window pixels.
25#[derive(Clone, Copy, Debug, PartialEq)]
26pub struct PaintRegion {
27    pub scale: f64,
28    pub width: u32,
29    pub height: u32,
30    scene_x: f64,
31    scene_y: f64,
32    document_clip: Rect,
33}
34
35impl PaintRegion {
36    /// Crop a CSS-pixel document rectangle into a device-pixel surface.
37    #[must_use]
38    pub fn crop(scale: f64, x: f64, y: f64, width: u32, height: u32) -> Self {
39        let left = x * scale;
40        let top = y * scale;
41        Self {
42            scale,
43            width,
44            height,
45            scene_x: -left,
46            scene_y: -top,
47            document_clip: Rect::new(left, top, left + f64::from(width), top + f64::from(height)),
48        }
49    }
50}
51
52use anyrender::{PaintScene, Scene};
53use blitz_dom::{BaseDocument, NodeId, util::Color};
54use kurbo::Rect;
55use render::BlitzDomPainter;
56
57const FONT_EMBOLDEN_ENABLED: bool = cfg!(any(
58    feature = "font-embolden",
59    all(feature = "apple-font-embolden", target_os = "macos"),
60    all(feature = "apple-font-embolden", target_os = "ios"),
61));
62
63/// The default color for text selection highlights
64const SELECTION_COLOR: Color = Color::from_rgb8(180, 213, 255);
65
66/// Pre-computed `Scene`s for each CustomWidget, keyed by `(document id, node id)`
67type CustomWidgetSceneMap = HashMap<(usize, NodeId), Scene>;
68
69/// Paint a [`blitz_dom::BaseDocument`] by pushing drawing commands into
70/// an impl [`anyrender::PaintScene`].
71///
72/// This function assumes that the styles and layout in the [`BaseDocument`] are already
73/// resolved. Please ensure that this is the case before trying to paint.
74///
75/// The implementation of [`PaintScene`] is responsible for handling the commands that are pushed into it.
76/// Generally this will involve executing them to draw a rasterized image/texture. But in some cases it may choose to
77/// transform them to a vector format (e.g. SVG/PDF) or serialize them in raw form for later use.
78pub fn paint_scene(
79    scene: &mut impl PaintScene,
80    doc: &mut BaseDocument,
81    scale: f64,
82    width: u32,
83    height: u32,
84    x_offset: u32,
85    y_offset: u32,
86) {
87    paint_scene_at_time(scene, doc, scale, width, height, x_offset, y_offset, 0.0)
88}
89
90/// Paint one resolved part of a document without laying the subtree out alone.
91pub fn paint_scene_region(
92    scene: &mut impl PaintScene,
93    doc: &mut BaseDocument,
94    region: PaintRegion,
95) {
96    paint_scene_region_at_time(scene, doc, region, 0.0);
97}
98
99// Eight arguments: the scene, the document, and six scalars describing the
100// target. `scale`, `width`, `height`, `x_offset` and `y_offset` would group
101// naturally into a viewport struct, which would take this to four and read
102// better. That is a public API change for every caller of blitz-paint,
103// including the pinned consumers, so it is not something to fold into the
104// commit that added the eighth argument.
105#[allow(clippy::too_many_arguments)]
106pub fn paint_scene_at_time(
107    scene: &mut impl PaintScene,
108    doc: &mut BaseDocument,
109    scale: f64,
110    width: u32,
111    height: u32,
112    x_offset: u32,
113    y_offset: u32,
114    animation_time: f64,
115) {
116    let region = PaintRegion {
117        scale,
118        width,
119        height,
120        scene_x: x_offset as f64,
121        scene_y: y_offset as f64,
122        document_clip: Rect::new(
123            0.0,
124            0.0,
125            f64::from(width) * scale,
126            f64::from(height) * scale,
127        ),
128    };
129    paint_scene_region_at_time(scene, doc, region, animation_time);
130}
131
132fn paint_scene_region_at_time(
133    scene: &mut impl PaintScene,
134    doc: &mut BaseDocument,
135    region: PaintRegion,
136    animation_time: f64,
137) {
138    // Run `.paint()` on every custom widget in the document (and all subdocuments) ahead of time.
139    // This helps us avoid borrow-checker issues as we recurse down the tree (`.paint()` require `&mut self`).
140    //
141    // TODO: Take widget and sub-document visibility into account
142    #[allow(unused_mut)]
143    let mut custom_widget_scenes: CustomWidgetSceneMap = HashMap::new();
144    #[cfg(feature = "custom-widget")]
145    build_custom_widget_scenes(&mut custom_widget_scenes, doc, scene, region.scale);
146
147    let generator = BlitzDomPainter::new_with_clip(
148        doc,
149        region.scale,
150        region.width,
151        region.height,
152        region.scene_x,
153        region.scene_y,
154        region.document_clip,
155        &custom_widget_scenes,
156        animation_time,
157    );
158    generator.paint_scene(scene);
159
160    // println!(
161    //     "Rendered using {} clips (depth: {}) (wanted: {})",
162    //     CLIPS_USED.load(atomic::Ordering::SeqCst),
163    //     CLIP_DEPTH_USED.load(atomic::Ordering::SeqCst),
164    //     CLIPS_WANTED.load(atomic::Ordering::SeqCst)
165    // );
166}
167
168#[cfg(feature = "custom-widget")]
169fn build_custom_widget_scenes(
170    custom_widget_scenes: &mut CustomWidgetSceneMap,
171    doc: &mut BaseDocument,
172    render_ctx: &mut impl anyrender::RenderContext,
173    scale: f64,
174) {
175    let doc_id = doc.id();
176
177    // Process scenes for every custom widget in the document
178    let custom_widget_node_ids = doc.custom_widget_node_ids();
179    for node_id in custom_widget_node_ids.into_iter() {
180        if let Some(scene) = process_custom_widget_node(doc, render_ctx, node_id, scale) {
181            custom_widget_scenes.insert((doc_id, node_id), scene);
182        }
183    }
184
185    // Recurse into sub documents
186    let sub_document_node_ids = doc.sub_document_node_ids();
187    for node_id in sub_document_node_ids.into_iter() {
188        if let Some(sub_doc) = doc.get_node_mut(node_id).and_then(|node| node.subdoc_mut()) {
189            let mut inner = sub_doc.inner_mut();
190            build_custom_widget_scenes(custom_widget_scenes, &mut inner, render_ctx, scale);
191        }
192    }
193}
194
195#[cfg(feature = "custom-widget")]
196fn process_custom_widget_node(
197    doc: &mut BaseDocument,
198    render_ctx: &mut impl anyrender::RenderContext,
199    node_id: NodeId,
200    scale: f64,
201) -> Option<Scene> {
202    use blitz_dom::node::{CustomWidgetStatus, ProxyRenderContext};
203
204    let node = doc.get_node_mut(node_id)?;
205    let width = (node.final_layout().size.width as f64 * scale) as u32;
206    let height = (node.final_layout().size.height as f64 * scale) as u32;
207
208    if width == 0 || height == 0 {
209        return None;
210    }
211
212    let style = (*node.stylo_element_data().primary_styles()?).clone();
213    let element = node.data.downcast_element_mut()?;
214    let widget_data = element.custom_widget_data_mut()?;
215
216    let mut render_ctx = ProxyRenderContext {
217        inner: render_ctx,
218        resource_ids: &mut widget_data.active_resource_ids,
219    };
220
221    if widget_data.status == CustomWidgetStatus::Suspended {
222        widget_data.widget.can_create_surfaces(&mut render_ctx);
223        widget_data.status = CustomWidgetStatus::Active;
224    }
225
226    let widget_scene = widget_data
227        .widget
228        .paint(&mut render_ctx, &style, width, height, scale);
229
230    Some(widget_scene)
231}