Skip to main content

apace_paint/
lib.rs

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