Add vector render path for PDF export

This commit is contained in:
Elena Torro 2026-06-29 18:23:50 +02:00
parent 2d4a24bf97
commit a1299e1797
20 changed files with 195 additions and 147 deletions

View File

@ -179,6 +179,15 @@
(and (wasm-export-enabled? state)
(contains? wasm-export-types (:type export))))
(defn- request-simple-export-wasm
[export]
(ptk/reify ::request-simple-export-wasm
ptk/EffectEvent
(effect [_ _ _]
(case (:type export)
:pdf (wasm.exports/export-pdf export)
(wasm.exports/export-image export)))))
(defn request-simple-export
[{:keys [export]}]
(ptk/reify ::request-simple-export
@ -191,11 +200,7 @@
ptk/WatchEvent
(watch [_ state _]
(if (use-wasm-export? state export)
(do
(case (:type export)
:pdf (wasm.exports/export-pdf export)
(wasm.exports/export-image export))
(rx/empty))
(rx/of (request-simple-export-wasm export))
(let [profile-id (:profile-id state)
params {:exports [export]
:profile-id profile-id

View File

@ -4,7 +4,7 @@ use macros::wasm_error;
use crate::emscripten::init_gl;
use crate::mem;
use crate::render::{gpu_state::GpuState, RenderState};
use crate::render::{gpu_state::GpuState, RenderResources, RenderState};
use crate::state::{State, TextEditorState, UIState};
static mut DESIGN_STATE: *mut State = std::ptr::null_mut();
@ -23,7 +23,12 @@ static mut GPU_STATE: *mut GpuState = std::ptr::null_mut();
#[inline(always)]
pub(crate) fn get_gpu_state() -> &'static mut GpuState {
unsafe {
debug_assert!(!GPU_STATE.is_null(), "GPU State is null");
// `assert!` (not `debug_assert!`): a headless instance never inits GPU
// state, so an interactive call must fail-fast rather than deref null.
assert!(
!GPU_STATE.is_null(),
"GPU State is null (headless instance?)"
);
&mut *GPU_STATE
}
}
@ -39,6 +44,17 @@ pub(crate) fn get_render_state() -> &'static mut RenderState {
}
}
/// GPU-free resources for the headless export path
static mut RENDER_RESOURCES: *mut RenderResources = std::ptr::null_mut();
#[inline(always)]
pub(crate) fn get_resources() -> &'static mut RenderResources {
unsafe {
debug_assert!(!RENDER_RESOURCES.is_null(), "Render Resources is null");
&mut *RENDER_RESOURCES
}
}
/// Text Editor State
static mut TEXT_EDITOR_STATE: *mut TextEditorState = std::ptr::null_mut();
@ -113,6 +129,14 @@ fn render_init(width: i32, height: i32) {
}
}
/// Initializes the interactive RenderResources (GPU image store).
fn resources_init() {
unsafe {
let resources = RenderResources::try_new().expect("Cannot initialize RenderResources");
RENDER_RESOURCES = Box::into_raw(Box::new(resources));
}
}
/// Initializes DesignState.
fn design_init() {
unsafe {
@ -143,6 +167,7 @@ pub extern "C" fn init(width: i32, height: i32) -> Result<()> {
#[cfg(target_arch = "wasm32")]
init_gl!();
gpu_init();
resources_init();
render_init(width, height);
text_editor_init();
design_init();

View File

@ -22,7 +22,7 @@ use std::collections::HashMap;
use crate::error::{Error, Result};
use crate::render::{FrameType, RenderFlag};
use globals::{get_design_state, get_gpu_state, get_render_state};
use globals::{get_design_state, get_gpu_state, get_render_state, get_resources};
use macros::wasm_error;
use math::{Bounds, Matrix};
@ -720,7 +720,7 @@ pub extern "C" fn is_image_cached(
is_thumbnail: bool,
) -> Result<bool> {
let id = uuid_from_u32_quartet(a, b, c, d);
let result = get_render_state().has_image(&id, is_thumbnail);
let result = get_resources().images.contains(&id, is_thumbnail);
Ok(result)
}

View File

@ -7,6 +7,7 @@ pub mod grid_layout;
mod images;
mod options;
pub mod pdf;
mod resources;
pub mod rulers;
mod shadows;
pub mod shape_renderer;
@ -35,10 +36,11 @@ use crate::tiles::{self, PendingTiles, TileRect};
use crate::uuid::Uuid;
use crate::view::Viewbox;
use crate::wapi;
use crate::{get_gpu_state, performance};
use crate::{get_gpu_state, get_resources, performance};
pub use fonts::*;
pub use images::*;
pub(crate) use resources::RenderResources;
type ClipStack = Vec<(Rect, Option<Corners>, Matrix)>;
@ -349,15 +351,12 @@ pub(crate) struct RenderState {
pub options: RenderOptions,
stats: RenderStats,
pub surfaces: Surfaces,
pub fonts: FontStore,
pub viewbox: Viewbox,
pub cached_viewbox: Viewbox,
pub images: ImageStore,
pub background_color: skia::Color,
// Stack of nodes pending to be rendered.
pending_nodes: Vec<NodeRenderState>,
pub current_tile: Option<tiles::Tile>,
pub sampling_options: skia::SamplingOptions,
pub render_area: Rect,
// render_area expanded by surface margins — used for visibility checks so that
// shapes in the margin zone are rendered (needed for background blur sampling).
@ -538,19 +537,18 @@ impl RenderState {
pub fn try_new(width: i32, height: i32) -> Result<RenderState> {
// This needs to be done once per WebGL context.
let sampling_options =
skia::SamplingOptions::new(skia::FilterMode::Linear, skia::MipmapMode::Nearest);
let sampling_options = get_resources().sampling_options;
let fonts = FontStore::try_new()?;
let surfaces = Surfaces::try_new(
(width, height),
sampling_options,
tiles::get_tile_dimensions(),
)?;
// This is used multiple times everywhere so instead of creating new instances every
// time we reuse this one.
Self::assemble(width, height, surfaces)
}
fn assemble(width: i32, height: i32, surfaces: Surfaces) -> Result<RenderState> {
let viewbox = Viewbox::new(width as f32, height as f32);
let tiles = tiles::TileHashMap::new();
let options = RenderOptions::default();
@ -559,14 +557,11 @@ impl RenderState {
options,
stats: RenderStats::new(),
surfaces,
fonts,
viewbox,
cached_viewbox: Viewbox::new(0., 0.),
images: ImageStore::new(),
background_color: skia::Color::TRANSPARENT,
pending_nodes: vec![],
current_tile: None,
sampling_options,
render_area: Rect::new_empty(),
render_area_with_margins: Rect::new_empty(),
tiles,
@ -770,35 +765,6 @@ impl RenderState {
Ok(result)
}
pub fn fonts(&self) -> &FontStore {
&self.fonts
}
pub fn fonts_mut(&mut self) -> &mut FontStore {
&mut self.fonts
}
pub fn add_image(&mut self, id: Uuid, is_thumbnail: bool, image_data: &[u8]) -> Result<()> {
self.images.add(id, is_thumbnail, image_data)
}
/// Adds an image from an existing WebGL texture, avoiding re-decoding
pub fn add_image_from_gl_texture(
&mut self,
id: Uuid,
is_thumbnail: bool,
texture_id: u32,
width: i32,
height: i32,
) -> Result<()> {
self.images
.add_image_from_gl_texture(id, is_thumbnail, texture_id, width, height)
}
pub fn has_image(&self, id: &Uuid, is_thumbnail: bool) -> bool {
self.images.contains(id, is_thumbnail)
}
pub fn set_debug_flags(&mut self, debug: u32) {
self.options.flags = debug;
}
@ -813,7 +779,7 @@ impl RenderState {
self.viewbox.width().floor() as i32,
self.viewbox.height().floor() as i32,
)?;
self.fonts.set_scale_debug_font(dpr);
get_resources().fonts.set_scale_debug_font(dpr);
self.viewbox.set_dpr(dpr);
self.surfaces.set_dpr(dpr);
}
@ -961,7 +927,7 @@ impl RenderState {
text_paint.set_color(skia::Color::GRAY);
text_paint.set_anti_alias(true);
let font = self.fonts.debug_font();
let font = get_resources().fonts.debug_font();
// FIXME
let text = "Loading…";
let (text_width, _) = font.measure_str(text, None);
@ -1398,7 +1364,8 @@ impl RenderState {
if let Some(svg) = shape.svg.as_ref() {
svg.render(self.surfaces.canvas_and_mark_dirty(fills_surface_id));
} else {
let font_manager = skia::FontMgr::from(self.fonts().font_provider().clone());
let font_manager =
skia::FontMgr::from(get_resources().fonts.font_provider().clone());
let dom_result = skia::svg::Dom::from_str(&sr.content, font_manager);
match dom_result {
Ok(dom) => {
@ -2938,7 +2905,7 @@ impl RenderState {
surface.draw(
drop_canvas,
(0.0, 0.0),
self.sampling_options,
get_resources().sampling_options,
Some(&drop_paint),
);
drop_canvas.restore();
@ -2948,7 +2915,7 @@ impl RenderState {
surface.draw(
drop_canvas,
(0.0, 0.0),
self.sampling_options,
get_resources().sampling_options,
Some(&drop_paint),
);
drop_canvas.restore();

View File

@ -6,6 +6,7 @@ use macros::wasm_error;
#[cfg(target_arch = "wasm32")]
use crate::get_render_state;
use crate::get_resources;
use skia_safe::{self as skia, Rect};
@ -62,16 +63,16 @@ pub fn render_wasm_label(render_state: &mut RenderState) {
"WebGL rendering"
};
let dpr = render_state.options.dpr;
let (scalar, _) = render_state.fonts.debug_font().measure_str(str, None);
let (scalar, _) = get_resources().fonts.debug_font().measure_str(str, None);
let mut p = skia::Point::new(canvas_width - (20.0 * dpr) - scalar, 50.0 * dpr);
let debug_font = render_state.fonts.debug_font();
let debug_font = get_resources().fonts.debug_font();
canvas.draw_str(str, p, debug_font, &paint);
if render_state.options.is_text_editor_v3() {
str = "Text Editor v3";
let (scalar, _) = render_state.fonts.debug_font().measure_str(str, None);
let (scalar, _) = get_resources().fonts.debug_font().measure_str(str, None);
p.x = canvas_width - (20.0 * dpr) - scalar;
p.y += 15.0 * dpr;
canvas.draw_str(str, p, debug_font, &paint);
@ -85,7 +86,7 @@ pub fn render_debug_tiles_for_viewbox(render_state: &mut RenderState) {
paint.set_color(skia::Color::RED);
let str_rect = format!("{} {} {} {}", sx, sy, ex, ey);
let debug_font = render_state.fonts.debug_font();
let debug_font = get_resources().fonts.debug_font();
canvas.draw_str(str_rect, skia::Point::new(100.0, 150.0), debug_font, &paint);
}
@ -104,7 +105,7 @@ pub fn render_debug_viewbox_tiles(render_state: &mut RenderState) {
let str_rect = format!("{} {} {} {}", sx, sy, ex, ey);
let debug_font = render_state.fonts.debug_font();
let debug_font = get_resources().fonts.debug_font();
canvas.draw_str(str_rect, skia::Point::new(100.0, 100.0), debug_font, &paint);
let tile_size = tiles::get_tile_size(scale);
@ -114,7 +115,7 @@ pub fn render_debug_viewbox_tiles(render_state: &mut RenderState) {
let debug_rect = get_debug_rect(rect);
let p = skia::Point::new(debug_rect.x(), debug_rect.y() - 1.);
let str = format!("{}:{}", x, y);
let debug_font = render_state.fonts.debug_font();
let debug_font = get_resources().fonts.debug_font();
paint.set_style(skia::PaintStyle::Fill);
canvas.draw_str(str, p, debug_font, &paint);
canvas.draw_rect(debug_rect, &paint);
@ -153,7 +154,7 @@ pub fn render_workspace_current_tile(
let tile_position_origin = skia::Point::new(rect.x() + 10., rect.y() + 20.);
p.set_style(skia::PaintStyle::Fill);
let str = format!("{prefix} {}:{}", tile.x(), tile.y());
let mut debug_font = render_state.fonts.debug_font().clone();
let mut debug_font = get_resources().fonts.debug_font().clone();
debug_font.set_size(16.);
canvas.draw_str(str, tile_position_origin, &debug_font, &p);
}

View File

@ -2,6 +2,7 @@ use skia_safe::{self as skia, Paint, RRect};
use super::{filters, RenderState, SurfaceId};
use crate::error::Result;
use crate::get_resources;
use crate::render::get_source_rect;
use crate::shapes::{merge_fills, Fill, Frame, ImageFill, Rect, Shape, Type};
@ -13,7 +14,7 @@ fn draw_image_fill(
antialias: bool,
surface_id: SurfaceId,
) {
let Some(image) = render_state.images.get(&image_fill.id()) else {
let Some(image) = get_resources().images.get(&image_fill.id()) else {
return;
};
@ -83,7 +84,7 @@ fn draw_image_fill(
image,
Some((&src_rect, skia::canvas::SrcRectConstraint::Strict)),
dest_rect,
render_state.sampling_options,
get_resources().sampling_options,
paint,
);

View File

@ -2,6 +2,7 @@ use skia_safe::{self as skia, ImageFilter, Rect};
use super::{RenderState, SurfaceId};
use crate::error::Result;
use crate::get_resources;
/// Composes two image filters, returning a combined filter if both are present,
/// or the individual filter if only one is present, or None if neither is present.
@ -51,12 +52,12 @@ where
canvas.save();
canvas.scale((1.0 / scale, 1.0 / scale));
canvas.translate((bounds.left * scale, bounds.top * scale));
surface.draw(canvas, (0.0, 0.0), render_state.sampling_options, None);
surface.draw(canvas, (0.0, 0.0), get_resources().sampling_options, None);
canvas.restore();
} else {
canvas.save();
canvas.translate((bounds.left, bounds.top));
surface.draw(canvas, (0.0, 0.0), render_state.sampling_options, None);
surface.draw(canvas, (0.0, 0.0), get_resources().sampling_options, None);
canvas.restore();
}
Ok(true)

View File

@ -61,7 +61,8 @@ enum StoredImage {
pub struct ImageStore {
images: HashMap<(Uuid, bool), StoredImage>,
context: Box<DirectContext>,
/// gpu-only
context: Option<Box<DirectContext>>,
}
/// Creates a Skia image from an existing WebGL texture.
@ -149,7 +150,7 @@ impl ImageStore {
let context = &gpu_state.context;
Self {
images: HashMap::with_capacity(2048),
context: Box::new(context.clone()),
context: Some(Box::new(context.clone())),
}
}
@ -167,10 +168,18 @@ impl ImageStore {
let raw_data = image_data.to_vec();
if let Some(gpu_image) = decode_image(&mut self.context, &raw_data) {
self.images.insert(key, StoredImage::Gpu(gpu_image));
} else {
self.images.insert(key, StoredImage::Raw(raw_data));
match self.context.as_mut() {
Some(context) => {
if let Some(gpu_image) = decode_image(context, &raw_data) {
self.images.insert(key, StoredImage::Gpu(gpu_image));
} else {
self.images.insert(key, StoredImage::Raw(raw_data));
}
}
// GPU-free: keep the encoded bytes; decoded on the CPU at draw time.
None => {
self.images.insert(key, StoredImage::Raw(raw_data));
}
}
Ok(())
}
@ -193,7 +202,12 @@ impl ImageStore {
}
// Create a Skia image from the existing GL texture
let image = create_image_from_gl_texture(&mut self.context, texture_id, width, height)?;
let Some(context) = self.context.as_mut() else {
return Err(crate::error::Error::CriticalError(
"Cannot register a GL texture without a GPU context".to_string(),
));
};
let image = create_image_from_gl_texture(context, texture_id, width, height)?;
self.images.insert(key, StoredImage::Gpu(image));
Ok(())
@ -214,8 +228,27 @@ impl ImageStore {
}
pub fn get_cpu_image(&mut self, id: &Uuid) -> Option<Image> {
let gpu_image = self.get(id)?.clone();
gpu_image.make_non_texture_image(self.context.as_mut())
// GPU path: promote to a texture, then copy to a CPU image.
if self.context.is_some() {
let gpu_image = self.get(id)?.clone();
let context = self.context.as_mut()?;
return gpu_image.make_non_texture_image(context.as_mut());
}
// Headless (no GPU context): decode the stored encoded bytes directly to
// a CPU image, which draws fine on a raster/PDF canvas. Try full first,
// then thumbnail.
self.decode_raw_cpu_image(id, false)
.or_else(|| self.decode_raw_cpu_image(id, true))
}
fn decode_raw_cpu_image(&self, id: &Uuid, is_thumbnail: bool) -> Option<Image> {
match self.images.get(&(*id, is_thumbnail))? {
StoredImage::Raw(raw_data) => {
let data = unsafe { skia::Data::new_bytes(raw_data) };
Image::from_encoded(&data)
}
StoredImage::Gpu(img) => Some(img.clone()),
}
}
fn get_internal(&mut self, id: &Uuid, is_thumbnail: bool) -> Option<&Image> {
@ -225,7 +258,8 @@ impl ImageStore {
match entry {
StoredImage::Gpu(ref img) => Some(img),
StoredImage::Raw(raw_data) => {
let gpu_image = decode_image(&mut self.context, raw_data)?;
let context = self.context.as_mut()?;
let gpu_image = decode_image(context, raw_data)?;
*entry = StoredImage::Gpu(gpu_image);
if let StoredImage::Gpu(ref img) = entry {

View File

@ -69,6 +69,7 @@ impl RenderOptions {
self.fast_mode = enabled;
}
#[cfg(target_arch = "wasm32")]
pub fn set_capture_frames(&mut self, capture_frames: i32) {
self.capture_frames = capture_frames;
}

View File

@ -4,8 +4,8 @@ use crate::error::Result;
use crate::state::ShapesPoolRef;
use crate::uuid::Uuid;
use super::vector::{self, VectorTarget};
use super::RenderState;
use super::vector;
use super::RenderResources;
/// Renders a shape tree to a PDF document and returns the raw PDF bytes.
///
@ -16,7 +16,7 @@ use super::RenderState;
/// pixel-based (blur, shadows with blur) are rasterised internally by Skia's
/// PDF backend
pub fn render_to_pdf(
shared: &mut RenderState,
shared: &mut RenderResources,
id: &Uuid,
tree: ShapesPoolRef,
scale: f32,
@ -45,7 +45,7 @@ pub fn render_to_pdf(
let page_canvas = on_page.canvas();
page_canvas.scale((scale, scale));
page_canvas.translate((-bounds.left(), -bounds.top()));
vector::render_tree(shared, page_canvas, id, tree, scale, VectorTarget::Pdf)?;
vector::render_tree(shared, page_canvas, id, tree, scale)?;
}
let document = on_page.end_page();

View File

@ -0,0 +1,31 @@
use skia_safe as skia;
use crate::error::Result;
use super::{FontStore, ImageStore};
/// Render dependencies that the export (PDF) path needs independently of the
/// surface/tile/GPU machinery in [`RenderState`]: the font store, the
/// decoded-image store, and the default sampling options.
///
/// Splitting these out keeps the vector/PDF export path (which takes
/// `&mut RenderResources`) decoupled from the interactive renderer, and gives a
/// single source of truth reached through `get_resources()`.
pub(crate) struct RenderResources {
pub fonts: FontStore,
pub images: ImageStore,
pub sampling_options: skia::SamplingOptions,
}
impl RenderResources {
pub fn try_new() -> Result<Self> {
Ok(Self {
fonts: FontStore::try_new()?,
images: ImageStore::new(),
sampling_options: skia::SamplingOptions::new(
skia::FilterMode::Linear,
skia::MipmapMode::Nearest,
),
})
}
}

View File

@ -8,6 +8,7 @@ use skia_safe::{self as skia, ImageFilter, RRect};
use super::{filters, RenderState, SurfaceId};
use crate::error::{Error, Result};
use crate::get_resources;
use crate::render::filters::compose_filters;
use crate::render::{get_dest_rect, get_source_rect};
@ -467,7 +468,7 @@ fn draw_image_stroke_in_container(
surface_id: SurfaceId,
) -> Result<()> {
let scale = render_state.get_scale();
let Some(image) = render_state.images.get(&image_fill.id()) else {
let Some(image) = get_resources().images.get(&image_fill.id()) else {
return Ok(());
};
@ -575,7 +576,7 @@ fn draw_image_stroke_in_container(
image,
Some((&src_rect, skia::canvas::SrcRectConstraint::Strict)),
dest_rect,
render_state.sampling_options,
get_resources().sampling_options,
&image_paint,
);

View File

@ -106,6 +106,7 @@ impl DocAtlas {
})
}
// TODO: delete one docatlas is optional
pub fn is_empty(&self) -> bool {
self.size.width <= 0 || self.size.height <= 0
}

View File

@ -1,6 +1,7 @@
use skia_safe::{self as skia, Color4f};
use super::{RenderState, ShapesPoolRef, SurfaceId};
use crate::get_resources;
use crate::globals::get_ui_state;
use crate::render::{grid_layout, rulers};
use crate::shapes::{Layout, Type};
@ -64,7 +65,7 @@ pub fn render(render_state: &mut RenderState, shapes: ShapesPoolRef) {
let viewbox = render_state.viewbox;
let ruler_state = render_state.rulers;
rulers::render(canvas, viewbox, &render_state.fonts, &ruler_state);
rulers::render(canvas, viewbox, &get_resources().fonts, &ruler_state);
// Width of the ruler bars in document coordinates.
// Note that when rulers are hidden, guides are not shown either, so we

View File

@ -9,19 +9,9 @@ use crate::uuid::Uuid;
use super::shape_renderer::ShapeRenderer;
use super::text;
use super::RenderState;
use super::RenderResources;
use super::{get_dest_rect, get_source_rect};
// ---------------------------------------------------------------------------
// VectorTarget — vector export backend selector
// ---------------------------------------------------------------------------
/// Vector export backend selector (PDF today; SVG could be added as a variant).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum VectorTarget {
Pdf,
}
// ---------------------------------------------------------------------------
// VectorRenderer — implements ShapeRenderer for canvas-based vector export
// ---------------------------------------------------------------------------
@ -29,23 +19,16 @@ pub(super) enum VectorTarget {
/// Canvas-based vector render backend (CPU Skia canvas, no GPU surfaces).
pub(super) struct VectorRenderer<'a> {
canvas: &'a Canvas,
shared: &'a mut RenderState,
shared: &'a mut RenderResources,
scale: f32,
_target: VectorTarget,
}
impl<'a> VectorRenderer<'a> {
pub fn new(
canvas: &'a Canvas,
shared: &'a mut RenderState,
scale: f32,
target: VectorTarget,
) -> Self {
pub fn new(canvas: &'a Canvas, shared: &'a mut RenderResources, scale: f32) -> Self {
Self {
canvas,
shared,
scale,
_target: target,
}
}
}
@ -348,12 +331,11 @@ impl ShapeRenderer for VectorRenderer<'_> {
/// Depth-first render of the shape tree rooted at `id`.
pub(super) fn render_tree(
shared: &mut RenderState,
shared: &mut RenderResources,
canvas: &Canvas,
id: &Uuid,
tree: ShapesPoolRef,
scale: f32,
target: VectorTarget,
) -> Result<()> {
let Some(element) = tree.get(id) else {
return Ok(());
@ -365,10 +347,10 @@ pub(super) fn render_tree(
match &element.shape_type {
Type::Group(group) => {
render_group(shared, canvas, element, group.masked, tree, scale, target)?;
render_group(shared, canvas, element, group.masked, tree, scale)?;
}
Type::Frame(_) => {
render_frame(shared, canvas, element, tree, scale, target)?;
render_frame(shared, canvas, element, tree, scale)?;
}
// Leaf types listed explicitly (no `_`) so a new Type must be handled.
Type::Rect(_)
@ -377,7 +359,7 @@ pub(super) fn render_tree(
| Type::Bool(_)
| Type::Text(_)
| Type::SVGRaw(_) => {
render_leaf(shared, canvas, element, scale, target)?;
render_leaf(shared, canvas, element, scale)?;
}
}
@ -389,13 +371,12 @@ pub(super) fn render_tree(
// ---------------------------------------------------------------------------
fn render_group(
shared: &mut RenderState,
shared: &mut RenderResources,
canvas: &Canvas,
element: &Shape,
masked: bool,
tree: ShapesPoolRef,
scale: f32,
target: VectorTarget,
) -> Result<()> {
// A group has no geometry of its own and does NOT propagate a transform to
// its children: child shapes are stored in absolute coordinates and each
@ -404,7 +385,7 @@ fn render_group(
canvas.save();
// Group drop shadow: subtree silhouette, below the opacity/clip layer.
render_container_drop_shadows(shared, canvas, element, tree, scale, target, false)?;
render_container_drop_shadows(shared, canvas, element, tree, scale, false)?;
// Layer for opacity / blend mode (and group-level layer blur)
let needs_layer = element.needs_layer();
@ -437,21 +418,21 @@ fn render_group(
canvas.save_layer(&skia::canvas::SaveLayerRec::default().paint(&paint));
for child_id in &children {
render_tree(shared, canvas, child_id, tree, scale, target)?;
render_tree(shared, canvas, child_id, tree, scale)?;
}
if let Some(mask_id) = element.mask_id() {
let mut mask_paint = Paint::default();
mask_paint.set_blend_mode(skia::BlendMode::DstIn);
canvas.save_layer(&skia::canvas::SaveLayerRec::default().paint(&mask_paint));
render_tree(shared, canvas, mask_id, tree, scale, target)?;
render_tree(shared, canvas, mask_id, tree, scale)?;
canvas.restore(); // mask layer
}
canvas.restore(); // composition layer
} else {
for child_id in &children {
render_tree(shared, canvas, child_id, tree, scale, target)?;
render_tree(shared, canvas, child_id, tree, scale)?;
}
}
@ -467,12 +448,11 @@ fn render_group(
// ---------------------------------------------------------------------------
fn render_frame(
shared: &mut RenderState,
shared: &mut RenderResources,
canvas: &Canvas,
element: &Shape,
tree: ShapesPoolRef,
scale: f32,
target: VectorTarget,
) -> Result<()> {
// A frame's own geometry (background, clip, strokes) is placed by its
// `centered_transform`, but — like groups — it does NOT propagate that
@ -485,7 +465,7 @@ fn render_frame(
// Frame drop shadow: background + subtree silhouette, below the clip layer
// so it extends outside the frame bounds.
render_container_drop_shadows(shared, canvas, element, tree, scale, target, true)?;
render_container_drop_shadows(shared, canvas, element, tree, scale, true)?;
let needs_layer = element.needs_layer();
@ -524,7 +504,7 @@ fn render_frame(
if !element.fills.is_empty() {
canvas.save();
canvas.concat(&matrix);
let mut renderer = VectorRenderer::new(canvas, shared, scale, target);
let mut renderer = VectorRenderer::new(canvas, shared, scale);
renderer.draw_fills(element, &element.fills)?;
renderer.draw_fill_inner_shadows(element)?;
canvas.restore();
@ -533,7 +513,7 @@ fn render_frame(
// Children (absolute coords, no frame transform).
let children: Vec<Uuid> = element.children_ids_iter_forward(false).copied().collect();
for child_id in &children {
render_tree(shared, canvas, child_id, tree, scale, target)?;
render_tree(shared, canvas, child_id, tree, scale)?;
}
// Strokes over children (clipped frames), in the frame's space.
@ -541,7 +521,7 @@ fn render_frame(
if !visible_strokes.is_empty() {
canvas.save();
canvas.concat(&matrix);
let mut renderer = VectorRenderer::new(canvas, shared, scale, target);
let mut renderer = VectorRenderer::new(canvas, shared, scale);
renderer.draw_strokes(element, &visible_strokes)?;
canvas.restore();
}
@ -557,12 +537,11 @@ fn render_frame(
/// layer (its alpha becomes the shadow). `draw_fills` includes the frame
/// background in the silhouette.
fn render_container_drop_shadows(
shared: &mut RenderState,
shared: &mut RenderResources,
canvas: &Canvas,
element: &Shape,
tree: ShapesPoolRef,
scale: f32,
target: VectorTarget,
draw_fills: bool,
) -> Result<()> {
for shadow in element.drop_shadows_visible() {
@ -574,13 +553,13 @@ fn render_container_drop_shadows(
canvas.save_layer(&skia::canvas::SaveLayerRec::default().paint(&paint));
if draw_fills && !element.fills.is_empty() {
let mut renderer = VectorRenderer::new(canvas, shared, scale, target);
let mut renderer = VectorRenderer::new(canvas, shared, scale);
renderer.draw_fills(element, &element.fills)?;
}
let children: Vec<Uuid> = element.children_ids_iter_forward(false).copied().collect();
for child_id in &children {
render_tree(shared, canvas, child_id, tree, scale, target)?;
render_tree(shared, canvas, child_id, tree, scale)?;
}
canvas.restore();
@ -593,11 +572,10 @@ fn render_container_drop_shadows(
// ---------------------------------------------------------------------------
fn render_leaf(
shared: &mut RenderState,
shared: &mut RenderResources,
canvas: &Canvas,
element: &Shape,
scale: f32,
target: VectorTarget,
) -> Result<()> {
let needs_layer = element.needs_layer();
@ -615,7 +593,7 @@ fn render_leaf(
canvas.save_layer(&layer_rec);
}
let mut renderer = VectorRenderer::new(canvas, shared, scale, target);
let mut renderer = VectorRenderer::new(canvas, shared, scale);
// Layer blur (non-text shapes)
let blur_layer = if !matches!(element.shape_type, Type::Text(_)) {
@ -677,7 +655,7 @@ fn render_leaf_content<R: ShapeRenderer + ?Sized>(renderer: &mut R, shape: &Shap
// ---------------------------------------------------------------------------
fn draw_image_fill(
shared: &mut RenderState,
shared: &mut RenderResources,
canvas: &Canvas,
shape: &Shape,
image_fill: &crate::shapes::ImageFill,
@ -719,7 +697,7 @@ fn draw_image_fill(
fn draw_single_stroke(
canvas: &Canvas,
shared: &mut RenderState,
shared: &mut RenderResources,
scale: f32,
shape: &Shape,
stroke: &Stroke,
@ -830,7 +808,7 @@ fn draw_stroke_kind_aware(canvas: &Canvas, shape: &Shape, stroke: &Stroke, paint
/// CPU image over it with `SrcIn` so only the stroke area shows the image.
fn draw_image_stroke(
canvas: &Canvas,
shared: &mut RenderState,
shared: &mut RenderResources,
scale: f32,
shape: &Shape,
stroke: &Stroke,

View File

@ -1,4 +1,4 @@
use crate::get_render_state;
use crate::get_resources;
use crate::shapes::text::TextContent;
use skia_safe::{
self as skia, textlayout::Paragraph as SkiaParagraph, FontMetrics, Point, Rect, TextBlob,
@ -174,7 +174,7 @@ impl TextPaths {
) -> Option<(skia::Path, skia::Rect)> {
let utf16_text = span_text.encode_utf16().collect::<Vec<u16>>();
let text = unsafe { skia_safe::as_utf16_unchecked(&utf16_text) };
let emoji_font = get_render_state().fonts().get_emoji_font(font.size());
let emoji_font = get_resources().fonts.get_emoji_font(font.size());
let use_font = emoji_font.as_ref().unwrap_or(font);
if let Some(mut text_blob) = TextBlob::from_text(text, use_font) {

View File

@ -14,7 +14,7 @@ use crate::error::{Error, Result};
use crate::render::FrameType;
use crate::shapes::{grid_layout::grid_cell_data, Shape};
use crate::uuid::Uuid;
use crate::{get_render_state, tiles};
use crate::{get_render_state, get_resources, tiles};
/// This struct holds the state of the Rust application between JS calls.
///
@ -97,7 +97,7 @@ impl State {
}
pub fn render_shape_pdf(&mut self, id: &Uuid, scale: f32) -> Result<Vec<u8>> {
crate::render::pdf::render_to_pdf(get_render_state(), id, &self.shapes, scale)
crate::render::pdf::render_to_pdf(get_resources(), id, &self.shapes, scale)
}
pub fn start_render_loop(&mut self, timestamp: i32) -> Result<FrameType> {
@ -266,7 +266,7 @@ impl State {
}
pub fn font_collection(&self) -> &FontCollection {
get_render_state().fonts().font_collection()
get_resources().fonts.font_collection()
}
pub fn get_grid_coords(&self, pos_x: f32, pos_y: f32) -> Option<(i32, i32)> {

View File

@ -1,4 +1,4 @@
use crate::get_render_state;
use crate::get_resources;
use crate::skia::textlayout::FontCollection;
use crate::skia::Image;
use crate::uuid::Uuid;
@ -25,12 +25,12 @@ pub fn uuid_from_u32(id: [u32; 4]) -> Uuid {
}
pub fn get_image(image_id: &Uuid) -> Option<&Image> {
get_render_state().images.get(image_id)
get_resources().images.get(image_id)
}
// FIXME: move to a different place ?
pub fn get_fallback_fonts() -> &'static HashSet<String> {
get_render_state().fonts().get_fallback()
get_resources().fonts.get_fallback()
}
pub fn get_font_collection() -> &'static FontCollection {

View File

@ -1,5 +1,5 @@
use crate::error::{Error, Result};
use crate::get_render_state;
use crate::get_resources;
use crate::mem;
use crate::shapes::Fill;
use crate::state::State;
@ -104,7 +104,10 @@ pub extern "C" fn store_image() -> Result<()> {
let image_bytes = &bytes[IMAGE_HEADER_SIZE..];
with_state!(state, {
if let Err(msg) = get_render_state().add_image(ids.image_id, is_thumbnail, image_bytes) {
if let Err(msg) = get_resources()
.images
.add(ids.image_id, is_thumbnail, image_bytes)
{
eprintln!("{}", msg);
}
touch_shapes_with_image(state, ids.image_id);
@ -174,7 +177,7 @@ pub extern "C" fn store_image_from_texture() -> Result<()> {
);
with_state!(state, {
if let Err(msg) = get_render_state().add_image_from_gl_texture(
if let Err(msg) = get_resources().images.add_image_from_gl_texture(
ids.image_id,
is_thumbnail,
texture_id,

View File

@ -1,6 +1,6 @@
use macros::{wasm_error, ToJs};
use crate::get_render_state;
use crate::get_resources;
use crate::mem;
use crate::shapes::{FontFamily, FontStyle};
use crate::utils::uuid_from_u32_quartet;
@ -45,8 +45,8 @@ pub extern "C" fn store_font(
let font_style = RawFontStyle::from(style);
let family = FontFamily::new(id, weight, font_style.into());
let _ = get_render_state()
.fonts_mut()
let _ = get_resources()
.fonts
.add(family, &font_bytes, is_emoji, is_fallback);
mem::free_bytes()?;
@ -66,7 +66,5 @@ pub extern "C" fn is_font_uploaded(
let id = uuid_from_u32_quartet(a, b, c, d);
let font_style = RawFontStyle::from(style);
let family = FontFamily::new(id, weight, font_style.into());
let res = get_render_state().fonts().has_family(&family, is_emoji);
res
get_resources().fonts.has_family(&family, is_emoji)
}