ps-blitz-paint 0.4.11

Paint a Blitz Document using anyrender
Documentation
//! Paint a [`blitz_dom::BaseDocument`] by pushing [`anyrender`] drawing commands into
//! an impl [`anyrender::PaintScene`].

#![allow(clippy::collapsible_if)]

mod color;
mod debug_overlay;
mod filters;
mod gradient;
mod kurbo_css;
mod layers;
mod render;

pub use layers::{LayerSite, SceneLayerCounts, latest_scene_layers};
mod sizing;
mod text;

use std::collections::HashMap;

/// A resolved document region painted into a smaller output surface.
///
/// Layout remains the layout of the full document. Only scene coordinates and
/// culling change, so a diagnostic crop answers what the application actually
/// drew at that position without rasterising unrelated window pixels.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct PaintRegion {
    pub scale: f64,
    pub width: u32,
    pub height: u32,
    scene_x: f64,
    scene_y: f64,
    document_clip: Rect,
}

impl PaintRegion {
    /// Crop a CSS-pixel document rectangle into a device-pixel surface.
    #[must_use]
    pub fn crop(scale: f64, x: f64, y: f64, width: u32, height: u32) -> Self {
        let left = x * scale;
        let top = y * scale;
        Self {
            scale,
            width,
            height,
            scene_x: -left,
            scene_y: -top,
            document_clip: Rect::new(left, top, left + f64::from(width), top + f64::from(height)),
        }
    }
}

use anyrender::{PaintScene, Scene};
use blitz_dom::{BaseDocument, NodeId, util::Color};
use kurbo::Rect;
use render::BlitzDomPainter;

const FONT_EMBOLDEN_ENABLED: bool = cfg!(any(
    feature = "font-embolden",
    all(feature = "apple-font-embolden", target_os = "macos"),
    all(feature = "apple-font-embolden", target_os = "ios"),
));

/// The default color for text selection highlights
const SELECTION_COLOR: Color = Color::from_rgb8(180, 213, 255);

/// Pre-computed `Scene`s for each CustomWidget, keyed by `(document id, node id)`
type CustomWidgetSceneMap = HashMap<(usize, NodeId), Scene>;

/// Paint a [`blitz_dom::BaseDocument`] by pushing drawing commands into
/// an impl [`anyrender::PaintScene`].
///
/// This function assumes that the styles and layout in the [`BaseDocument`] are already
/// resolved. Please ensure that this is the case before trying to paint.
///
/// The implementation of [`PaintScene`] is responsible for handling the commands that are pushed into it.
/// Generally this will involve executing them to draw a rasterized image/texture. But in some cases it may choose to
/// transform them to a vector format (e.g. SVG/PDF) or serialize them in raw form for later use.
pub fn paint_scene(
    scene: &mut impl PaintScene,
    doc: &mut BaseDocument,
    scale: f64,
    width: u32,
    height: u32,
    x_offset: u32,
    y_offset: u32,
) {
    paint_scene_at_time(scene, doc, scale, width, height, x_offset, y_offset, 0.0)
}

/// Paint one resolved part of a document without laying the subtree out alone.
pub fn paint_scene_region(
    scene: &mut impl PaintScene,
    doc: &mut BaseDocument,
    region: PaintRegion,
) {
    paint_scene_region_at_time(scene, doc, region, 0.0);
}

// Eight arguments: the scene, the document, and six scalars describing the
// target. `scale`, `width`, `height`, `x_offset` and `y_offset` would group
// naturally into a viewport struct, which would take this to four and read
// better. That is a public API change for every caller of blitz-paint,
// including the pinned consumers, so it is not something to fold into the
// commit that added the eighth argument.
#[allow(clippy::too_many_arguments)]
pub fn paint_scene_at_time(
    scene: &mut impl PaintScene,
    doc: &mut BaseDocument,
    scale: f64,
    width: u32,
    height: u32,
    x_offset: u32,
    y_offset: u32,
    animation_time: f64,
) {
    let region = PaintRegion {
        scale,
        width,
        height,
        scene_x: x_offset as f64,
        scene_y: y_offset as f64,
        document_clip: Rect::new(
            0.0,
            0.0,
            f64::from(width) * scale,
            f64::from(height) * scale,
        ),
    };
    paint_scene_region_at_time(scene, doc, region, animation_time);
}

fn paint_scene_region_at_time(
    scene: &mut impl PaintScene,
    doc: &mut BaseDocument,
    region: PaintRegion,
    animation_time: f64,
) {
    // Run `.paint()` on every custom widget in the document (and all subdocuments) ahead of time.
    // This helps us avoid borrow-checker issues as we recurse down the tree (`.paint()` require `&mut self`).
    //
    // TODO: Take widget and sub-document visibility into account
    #[allow(unused_mut)]
    let mut custom_widget_scenes: CustomWidgetSceneMap = HashMap::new();
    #[cfg(feature = "custom-widget")]
    build_custom_widget_scenes(&mut custom_widget_scenes, doc, scene, region.scale);

    let generator = BlitzDomPainter::new_with_clip(
        doc,
        region.scale,
        region.width,
        region.height,
        region.scene_x,
        region.scene_y,
        region.document_clip,
        &custom_widget_scenes,
        animation_time,
    );
    generator.paint_scene(scene);

    // println!(
    //     "Rendered using {} clips (depth: {}) (wanted: {})",
    //     CLIPS_USED.load(atomic::Ordering::SeqCst),
    //     CLIP_DEPTH_USED.load(atomic::Ordering::SeqCst),
    //     CLIPS_WANTED.load(atomic::Ordering::SeqCst)
    // );
}

#[cfg(feature = "custom-widget")]
fn build_custom_widget_scenes(
    custom_widget_scenes: &mut CustomWidgetSceneMap,
    doc: &mut BaseDocument,
    render_ctx: &mut impl anyrender::RenderContext,
    scale: f64,
) {
    let doc_id = doc.id();

    // Process scenes for every custom widget in the document
    let custom_widget_node_ids = doc.custom_widget_node_ids();
    for node_id in custom_widget_node_ids.into_iter() {
        if let Some(scene) = process_custom_widget_node(doc, render_ctx, node_id, scale) {
            custom_widget_scenes.insert((doc_id, node_id), scene);
        }
    }

    // Recurse into sub documents
    let sub_document_node_ids = doc.sub_document_node_ids();
    for node_id in sub_document_node_ids.into_iter() {
        if let Some(sub_doc) = doc.get_node_mut(node_id).and_then(|node| node.subdoc_mut()) {
            let mut inner = sub_doc.inner_mut();
            build_custom_widget_scenes(custom_widget_scenes, &mut inner, render_ctx, scale);
        }
    }
}

#[cfg(feature = "custom-widget")]
fn process_custom_widget_node(
    doc: &mut BaseDocument,
    render_ctx: &mut impl anyrender::RenderContext,
    node_id: NodeId,
    scale: f64,
) -> Option<Scene> {
    use blitz_dom::node::{CustomWidgetStatus, ProxyRenderContext};

    let node = doc.get_node_mut(node_id)?;
    let width = (node.final_layout().size.width as f64 * scale) as u32;
    let height = (node.final_layout().size.height as f64 * scale) as u32;

    if width == 0 || height == 0 {
        return None;
    }

    let style = (*node.stylo_element_data().primary_styles()?).clone();
    let element = node.data.downcast_element_mut()?;
    let widget_data = element.custom_widget_data_mut()?;

    let mut render_ctx = ProxyRenderContext {
        inner: render_ctx,
        resource_ids: &mut widget_data.active_resource_ids,
    };

    if widget_data.status == CustomWidgetStatus::Suspended {
        widget_data.widget.can_create_surfaces(&mut render_ctx);
        widget_data.status = CustomWidgetStatus::Active;
    }

    let widget_scene = widget_data
        .widget
        .paint(&mut render_ctx, &style, width, height, scale);

    Some(widget_scene)
}