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
20use anyrender::{PaintScene, Scene};
21use blitz_dom::{BaseDocument, NodeId, util::Color};
22use render::BlitzDomPainter;
23
24const FONT_EMBOLDEN_ENABLED: bool = cfg!(any(
25    feature = "font-embolden",
26    all(feature = "apple-font-embolden", target_os = "macos"),
27    all(feature = "apple-font-embolden", target_os = "ios"),
28));
29
30/// The default color for text selection highlights
31const SELECTION_COLOR: Color = Color::from_rgb8(180, 213, 255);
32
33/// Pre-computed `Scene`s for each CustomWidget, keyed by `(document id, node id)`
34type CustomWidgetSceneMap = HashMap<(usize, NodeId), Scene>;
35
36/// Paint a [`blitz_dom::BaseDocument`] by pushing drawing commands into
37/// an impl [`anyrender::PaintScene`].
38///
39/// This function assumes that the styles and layout in the [`BaseDocument`] are already
40/// resolved. Please ensure that this is the case before trying to paint.
41///
42/// The implementation of [`PaintScene`] is responsible for handling the commands that are pushed into it.
43/// Generally this will involve executing them to draw a rasterized image/texture. But in some cases it may choose to
44/// transform them to a vector format (e.g. SVG/PDF) or serialize them in raw form for later use.
45pub fn paint_scene(
46    scene: &mut impl PaintScene,
47    doc: &mut BaseDocument,
48    scale: f64,
49    width: u32,
50    height: u32,
51    x_offset: u32,
52    y_offset: u32,
53) {
54    paint_scene_at_time(scene, doc, scale, width, height, x_offset, y_offset, 0.0)
55}
56
57// Eight arguments: the scene, the document, and six scalars describing the
58// target. `scale`, `width`, `height`, `x_offset` and `y_offset` would group
59// naturally into a viewport struct, which would take this to four and read
60// better. That is a public API change for every caller of blitz-paint,
61// including the pinned consumers, so it is not something to fold into the
62// commit that added the eighth argument.
63#[allow(clippy::too_many_arguments)]
64pub fn paint_scene_at_time(
65    scene: &mut impl PaintScene,
66    doc: &mut BaseDocument,
67    scale: f64,
68    width: u32,
69    height: u32,
70    x_offset: u32,
71    y_offset: u32,
72    animation_time: f64,
73) {
74    // Run `.paint()` on every custom widget in the document (and all subdocuments) ahead of time.
75    // This helps us avoid borrow-checker issues as we recurse down the tree (`.paint()` require `&mut self`).
76    //
77    // TODO: Take widget and sub-document visibility into account
78    #[allow(unused_mut)]
79    let mut custom_widget_scenes: CustomWidgetSceneMap = HashMap::new();
80    #[cfg(feature = "custom-widget")]
81    build_custom_widget_scenes(&mut custom_widget_scenes, doc, scene, scale);
82
83    let generator = BlitzDomPainter::new(
84        doc,
85        scale,
86        width,
87        height,
88        x_offset as f64,
89        y_offset as f64,
90        &custom_widget_scenes,
91        animation_time,
92    );
93    generator.paint_scene(scene);
94
95    // println!(
96    //     "Rendered using {} clips (depth: {}) (wanted: {})",
97    //     CLIPS_USED.load(atomic::Ordering::SeqCst),
98    //     CLIP_DEPTH_USED.load(atomic::Ordering::SeqCst),
99    //     CLIPS_WANTED.load(atomic::Ordering::SeqCst)
100    // );
101}
102
103#[cfg(feature = "custom-widget")]
104fn build_custom_widget_scenes(
105    custom_widget_scenes: &mut CustomWidgetSceneMap,
106    doc: &mut BaseDocument,
107    render_ctx: &mut impl anyrender::RenderContext,
108    scale: f64,
109) {
110    let doc_id = doc.id();
111
112    // Process scenes for every custom widget in the document
113    let custom_widget_node_ids = doc.custom_widget_node_ids();
114    for node_id in custom_widget_node_ids.into_iter() {
115        if let Some(scene) = process_custom_widget_node(doc, render_ctx, node_id, scale) {
116            custom_widget_scenes.insert((doc_id, node_id), scene);
117        }
118    }
119
120    // Recurse into sub documents
121    let sub_document_node_ids = doc.sub_document_node_ids();
122    for node_id in sub_document_node_ids.into_iter() {
123        if let Some(sub_doc) = doc.get_node_mut(node_id).and_then(|node| node.subdoc_mut()) {
124            let mut inner = sub_doc.inner_mut();
125            build_custom_widget_scenes(custom_widget_scenes, &mut inner, render_ctx, scale);
126        }
127    }
128}
129
130#[cfg(feature = "custom-widget")]
131fn process_custom_widget_node(
132    doc: &mut BaseDocument,
133    render_ctx: &mut impl anyrender::RenderContext,
134    node_id: NodeId,
135    scale: f64,
136) -> Option<Scene> {
137    use blitz_dom::node::{CustomWidgetStatus, ProxyRenderContext};
138
139    let node = doc.get_node_mut(node_id)?;
140    let width = (node.final_layout().size.width as f64 * scale) as u32;
141    let height = (node.final_layout().size.height as f64 * scale) as u32;
142
143    if width == 0 || height == 0 {
144        return None;
145    }
146
147    let style = (*node.stylo_element_data().primary_styles()?).clone();
148    let element = node.data.downcast_element_mut()?;
149    let widget_data = element.custom_widget_data_mut()?;
150
151    let mut render_ctx = ProxyRenderContext {
152        inner: render_ctx,
153        resource_ids: &mut widget_data.active_resource_ids,
154    };
155
156    if widget_data.status == CustomWidgetStatus::Suspended {
157        widget_data.widget.can_create_surfaces(&mut render_ctx);
158        widget_data.status = CustomWidgetStatus::Active;
159    }
160
161    let widget_scene = widget_data
162        .widget
163        .paint(&mut render_ctx, &style, width, height, scale);
164
165    Some(widget_scene)
166}