🎉 Add background blur for wasm render

This commit is contained in:
Elena Torro 2026-03-17 09:16:46 +01:00
parent 5ba53f7296
commit e630be1509
25 changed files with 584 additions and 388 deletions

View File

@ -61,11 +61,13 @@ pub fn wasm_error(_attr: TokenStream, item: TokenStream) -> TokenStream {
let _: &dyn std::error::Error = &__e;
let __msg = __e.to_string();
crate::mem::set_error_code(__e.into());
crate::mem::free_bytes().expect("Failed to free bytes");
panic!("WASM error: {}",__msg);
}
},
Err(__payload) => {
crate::mem::set_error_code(0x02); // critical, same as Error::Critical
crate::mem::free_bytes().expect("Failed to free bytes");
std::panic::resume_unwind(__payload);
}
}

View File

@ -30,6 +30,9 @@ use uuid::Uuid;
pub(crate) static mut STATE: Option<Box<State>> = None;
// FIXME: These with_state* macros should be using our CriticalError instead of expect.
// But to do that, we need to not use them at domain-level (i.e. in business logic), just
// in the context of the wasm call.
#[macro_export]
macro_rules! with_state_mut {
($state:ident, $block:block) => {{
@ -102,7 +105,7 @@ macro_rules! with_state_mut_current_shape {
#[no_mangle]
#[wasm_error]
pub extern "C" fn init(width: i32, height: i32) -> Result<()> {
let state_box = Box::new(State::new(width, height));
let state_box = Box::new(State::try_new(width, height)?);
unsafe {
STATE = Some(state_box);
}
@ -138,7 +141,7 @@ pub extern "C" fn set_render_options(debug: u32, dpr: f32) -> Result<()> {
with_state_mut!(state, {
let render_state = state.render_state_mut();
render_state.set_debug_flags(debug);
render_state.set_dpr(dpr);
render_state.set_dpr(dpr)?;
});
Ok(())
}
@ -162,7 +165,7 @@ pub extern "C" fn render(_: i32) -> Result<()> {
state.rebuild_touched_tiles();
state
.start_render_loop(performance::get_time())
.expect("Error rendering");
.map_err(|_| Error::RecoverableError("Error rendering".to_string()))?;
});
Ok(())
}
@ -174,7 +177,7 @@ pub extern "C" fn render_sync() -> Result<()> {
state.rebuild_tiles();
state
.render_sync(performance::get_time())
.expect("Error rendering");
.map_err(|_| Error::RecoverableError("Error rendering".to_string()))?;
});
Ok(())
}
@ -236,24 +239,12 @@ pub extern "C" fn render_preview() -> Result<()> {
#[no_mangle]
#[wasm_error]
pub extern "C" fn process_animation_frame(timestamp: i32) -> Result<()> {
let result = std::panic::catch_unwind(|| {
with_state_mut!(state, {
state
.process_animation_frame(timestamp)
.expect("Error processing animation frame");
});
});
let result = with_state_mut!(state, { state.process_animation_frame(timestamp) });
match result {
Ok(_) => {}
Err(err) => {
match err.downcast_ref::<String>() {
Some(message) => println!("process_animation_frame error: {}", message),
None => println!("process_animation_frame error: {:?}", err),
}
std::panic::resume_unwind(err);
}
if let Err(err) = result {
eprintln!("process_animation_frame error: {}", err);
}
Ok(())
}
@ -270,7 +261,7 @@ pub extern "C" fn reset_canvas() -> Result<()> {
#[wasm_error]
pub extern "C" fn resize_viewbox(width: i32, height: i32) -> Result<()> {
with_state_mut!(state, {
state.resize(width, height);
state.resize(width, height)?;
});
Ok(())
}
@ -362,8 +353,8 @@ pub extern "C" fn set_focus_mode() -> Result<()> {
let entries: Vec<Uuid> = bytes
.chunks(size_of::<<Uuid as SerializableResult>::BytesType>())
.map(|data| Uuid::try_from(data).unwrap())
.collect();
.map(|data| Uuid::try_from(data).map_err(|e| Error::RecoverableError(e.to_string())))
.collect::<Result<Vec<Uuid>>>()?;
with_state_mut!(state, {
state.set_focus_mode(entries);
@ -637,8 +628,8 @@ pub extern "C" fn set_children() -> Result<()> {
let entries: Vec<Uuid> = bytes
.chunks(size_of::<<Uuid as SerializableResult>::BytesType>())
.map(|data| Uuid::try_from(data).unwrap())
.collect();
.map(|data| Uuid::try_from(data).map_err(|e| Error::CriticalError(e.to_string())))
.collect::<Result<Vec<Uuid>>>()?;
set_children_set(entries)?;
@ -761,10 +752,15 @@ pub extern "C" fn get_selection_rect() -> Result<*mut u8> {
pub extern "C" fn set_structure_modifiers() -> Result<()> {
let bytes = mem::bytes();
let entries: Vec<_> = bytes
let entries: Vec<StructureEntry> = bytes
.chunks(44)
.map(|data| StructureEntry::from_bytes(data.try_into().unwrap()))
.collect();
.map(|chunk| {
let data = chunk
.try_into()
.map_err(|_| Error::CriticalError("Invalid StructureEntry bytes".to_string()))?;
Ok(StructureEntry::from_bytes(data))
})
.collect::<Result<Vec<_>>>()?;
with_state_mut!(state, {
let mut structure = HashMap::new();
@ -783,7 +779,9 @@ pub extern "C" fn set_structure_modifiers() -> Result<()> {
structure.entry(entry.parent).or_insert_with(Vec::new);
structure
.get_mut(&entry.parent)
.expect("Parent not found for entry")
.ok_or(Error::CriticalError(
"Parent not found for entry".to_string(),
))?
.push(entry);
}
}
@ -814,10 +812,10 @@ pub extern "C" fn clean_modifiers() -> Result<()> {
pub extern "C" fn set_modifiers() -> Result<()> {
let bytes = mem::bytes();
let entries: Vec<_> = bytes
let entries: Vec<TransformEntry> = bytes
.chunks(size_of::<<TransformEntry as SerializableResult>::BytesType>())
.map(|data| TransformEntry::try_from(data).unwrap())
.collect();
.map(|data| TransformEntry::try_from(data).map_err(|e| Error::CriticalError(e.to_string())))
.collect::<Result<Vec<_>>>()?;
let mut modifiers = HashMap::new();
let mut ids = Vec::<Uuid>::new();
@ -828,7 +826,7 @@ pub extern "C" fn set_modifiers() -> Result<()> {
with_state_mut!(state, {
state.set_modifiers(modifiers);
state.rebuild_modifier_tiles(ids);
state.rebuild_modifier_tiles(ids)?;
});
Ok(())
}
@ -838,8 +836,10 @@ pub extern "C" fn set_modifiers() -> Result<()> {
pub extern "C" fn start_temp_objects() -> Result<()> {
unsafe {
#[allow(static_mut_refs)]
let mut state = STATE.take().expect("Got an invalid state pointer");
state = Box::new(state.start_temp_objects());
let mut state = STATE.take().ok_or(Error::CriticalError(
"Got an invalid state pointer".to_string(),
))?;
state = Box::new(state.start_temp_objects()?);
STATE = Some(state);
}
Ok(())
@ -850,8 +850,10 @@ pub extern "C" fn start_temp_objects() -> Result<()> {
pub extern "C" fn end_temp_objects() -> Result<()> {
unsafe {
#[allow(static_mut_refs)]
let mut state = STATE.take().expect("Got an invalid state pointer");
state = Box::new(state.end_temp_objects());
let mut state = STATE.take().ok_or(Error::CriticalError(
"Got an invalid state pointer".to_string(),
))?;
state = Box::new(state.end_temp_objects()?);
STATE = Some(state);
}
Ok(())

View File

@ -21,6 +21,7 @@ use gpu_state::GpuState;
use options::RenderOptions;
pub use surfaces::{SurfaceId, Surfaces};
use crate::error::{Error, Result};
use crate::performance;
use crate::shapes::{
all_with_ancestors, radius_to_sigma, Blur, BlurType, Corners, Fill, Shadow, Shape, SolidColor,
@ -326,19 +327,19 @@ pub fn get_cache_size(viewbox: Viewbox, scale: f32) -> skia::ISize {
}
impl RenderState {
pub fn new(width: i32, height: i32) -> RenderState {
pub fn try_new(width: i32, height: i32) -> Result<RenderState> {
// This needs to be done once per WebGL context.
let mut gpu_state = GpuState::new();
let mut gpu_state = GpuState::try_new()?;
let sampling_options =
skia::SamplingOptions::new(skia::FilterMode::Linear, skia::MipmapMode::Nearest);
let fonts = FontStore::new();
let surfaces = Surfaces::new(
let fonts = FontStore::try_new()?;
let surfaces = Surfaces::try_new(
&mut gpu_state,
(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.
@ -346,7 +347,7 @@ impl RenderState {
let viewbox = Viewbox::new(width as f32, height as f32);
let tiles = tiles::TileHashMap::new();
RenderState {
Ok(RenderState {
gpu_state: gpu_state.clone(),
options: RenderOptions::default(),
surfaces,
@ -377,7 +378,7 @@ impl RenderState {
touched_ids: HashSet::default(),
ignore_nested_blurs: false,
preview_mode: false,
}
})
}
/// Combines every visible layer blur currently active (ancestors + shape)
@ -531,15 +532,15 @@ impl RenderState {
/// Runs `f` with `ignore_nested_blurs` temporarily forced to `true`.
/// Certain off-screen passes (e.g. shadow masks) must render shapes without
/// inheriting ancestor blur. This helper guarantees the flag is restored.
fn with_nested_blurs_suppressed<F, R>(&mut self, f: F) -> R
fn with_nested_blurs_suppressed<F, R>(&mut self, f: F) -> Result<R>
where
F: FnOnce(&mut RenderState) -> R,
F: FnOnce(&mut RenderState) -> Result<R>,
{
let previous = self.ignore_nested_blurs;
self.ignore_nested_blurs = true;
let result = f(self);
let result = f(self)?;
self.ignore_nested_blurs = previous;
result
Ok(result)
}
pub fn fonts(&self) -> &FontStore {
@ -550,12 +551,7 @@ impl RenderState {
&mut self.fonts
}
pub fn add_image(
&mut self,
id: Uuid,
is_thumbnail: bool,
image_data: &[u8],
) -> Result<(), String> {
pub fn add_image(&mut self, id: Uuid, is_thumbnail: bool, image_data: &[u8]) -> Result<()> {
self.images.add(id, is_thumbnail, image_data)
}
@ -567,7 +563,7 @@ impl RenderState {
texture_id: u32,
width: i32,
height: i32,
) -> Result<(), String> {
) -> Result<()> {
self.images
.add_image_from_gl_texture(id, is_thumbnail, texture_id, width, height)
}
@ -580,15 +576,16 @@ impl RenderState {
self.options.flags = debug;
}
pub fn set_dpr(&mut self, dpr: f32) {
pub fn set_dpr(&mut self, dpr: f32) -> Result<()> {
if Some(dpr) != self.options.dpr {
self.options.dpr = Some(dpr);
self.resize(
self.viewbox.width.floor() as i32,
self.viewbox.height.floor() as i32,
);
)?;
self.fonts.set_scale_debug_font(dpr);
}
Ok(())
}
pub fn set_background_color(&mut self, color: skia::Color) {
@ -599,13 +596,15 @@ impl RenderState {
self.preview_mode = enabled;
}
pub fn resize(&mut self, width: i32, height: i32) {
pub fn resize(&mut self, width: i32, height: i32) -> Result<()> {
let dpr_width = (width as f32 * self.options.dpr()).floor() as i32;
let dpr_height = (height as f32 * self.options.dpr()).floor() as i32;
self.surfaces
.resize(&mut self.gpu_state, dpr_width, dpr_height);
.resize(&mut self.gpu_state, dpr_width, dpr_height)?;
self.viewbox.set_wh(width as f32, height as f32);
self.tile_viewbox.update(self.viewbox, self.get_scale());
Ok(())
}
pub fn flush_and_submit(&mut self) {
@ -627,19 +626,23 @@ impl RenderState {
self.surfaces.canvas(surface_id).restore();
}
pub fn apply_render_to_final_canvas(&mut self, rect: skia::Rect) {
let tile_rect = self.get_current_aligned_tile_bounds();
pub fn apply_render_to_final_canvas(&mut self, rect: skia::Rect) -> Result<()> {
let tile_rect = self.get_current_aligned_tile_bounds()?;
self.surfaces.cache_current_tile_texture(
&self.tile_viewbox,
&self.current_tile.unwrap(),
&self
.current_tile
.ok_or(Error::CriticalError("Current tile not found".to_string()))?,
&tile_rect,
);
self.surfaces.draw_cached_tile_surface(
self.current_tile.unwrap(),
self.current_tile
.ok_or(Error::CriticalError("Current tile not found".to_string()))?,
rect,
self.background_color,
);
Ok(())
}
pub fn apply_drawing_to_render_canvas(&mut self, shape: Option<&Shape>) {
@ -748,7 +751,7 @@ impl RenderState {
offset: Option<(f32, f32)>,
parent_shadows: Option<Vec<skia_safe::Paint>>,
outset: Option<f32>,
) {
) -> Result<()> {
let surface_ids = fills_surface_id as u32
| strokes_surface_id as u32
| innershadows_surface_id as u32
@ -813,7 +816,7 @@ impl RenderState {
antialias,
SurfaceId::Current,
None,
);
)?;
// Pass strokes in natural order; stroke merging handles top-most ordering internally.
let visible_strokes: Vec<&Stroke> = shape.visible_strokes().collect();
@ -824,7 +827,7 @@ impl RenderState {
Some(SurfaceId::Current),
antialias,
outset,
);
)?;
self.surfaces.apply_mut(SurfaceId::Current as u32, |s| {
s.canvas().restore();
@ -840,7 +843,7 @@ impl RenderState {
s.canvas().restore();
});
}
return;
return Ok(());
}
// set clipping
@ -1017,7 +1020,7 @@ impl RenderState {
None,
text_fill_inset,
None,
);
)?;
for (stroke_paragraphs, layer_opacity) in stroke_paragraphs_list
.iter_mut()
@ -1034,7 +1037,7 @@ impl RenderState {
text_stroke_blur_outset,
None,
*layer_opacity,
);
)?;
}
} else {
let mut drop_shadows = shape.drop_shadow_paints();
@ -1077,7 +1080,7 @@ impl RenderState {
blur_filter.as_ref(),
None,
None,
);
)?;
}
} else {
shadows::render_text_shadows(
@ -1088,7 +1091,7 @@ impl RenderState {
text_drop_shadows_surface_id.into(),
&parent_shadows,
&blur_filter,
);
)?;
}
} else {
// 1. Text drop shadows
@ -1104,7 +1107,7 @@ impl RenderState {
blur_filter.as_ref(),
None,
None,
);
)?;
}
}
@ -1119,7 +1122,7 @@ impl RenderState {
blur_filter.as_ref(),
text_fill_inset,
None,
);
)?;
// 3. Stroke drop shadows
shadows::render_text_shadows(
@ -1130,7 +1133,7 @@ impl RenderState {
text_drop_shadows_surface_id.into(),
&drop_shadows,
&blur_filter,
);
)?;
// 4. Stroke fills
for (stroke_paragraphs, layer_opacity) in stroke_paragraphs_list
@ -1148,7 +1151,7 @@ impl RenderState {
text_stroke_blur_outset,
None,
*layer_opacity,
);
)?;
}
// 5. Stroke inner shadows
@ -1160,7 +1163,7 @@ impl RenderState {
Some(innershadows_surface_id),
&inner_shadows,
&blur_filter,
);
)?;
// 6. Fill Inner shadows
if !shape.has_visible_strokes() {
@ -1175,7 +1178,7 @@ impl RenderState {
blur_filter.as_ref(),
None,
None,
);
)?;
}
}
}
@ -1223,7 +1226,7 @@ impl RenderState {
antialias,
fills_surface_id,
outset,
);
)?;
}
} else {
fills::render(
@ -1233,7 +1236,7 @@ impl RenderState {
antialias,
fills_surface_id,
outset,
);
)?;
}
// Skip stroke rendering for clipped frames - they are drawn in render_shape_exit
@ -1249,7 +1252,7 @@ impl RenderState {
Some(strokes_surface_id),
antialias,
outset,
);
)?;
if !fast_mode {
for stroke in &visible_strokes {
shadows::render_stroke_inner_shadows(
@ -1258,7 +1261,7 @@ impl RenderState {
stroke,
antialias,
innershadows_surface_id,
);
)?;
}
}
}
@ -1295,6 +1298,7 @@ impl RenderState {
s.canvas().restore();
});
}
Ok(())
}
pub fn update_render_context(&mut self, tile: tiles::Tile) {
@ -1373,7 +1377,7 @@ impl RenderState {
/// Render a preview of the shapes during loading.
/// This rebuilds tiles for touched shapes and renders synchronously.
pub fn render_preview(&mut self, tree: ShapesPoolRef, timestamp: i32) -> Result<(), String> {
pub fn render_preview(&mut self, tree: ShapesPoolRef, timestamp: i32) -> Result<()> {
let _start = performance::begin_timed_log!("render_preview");
performance::begin_measure!("render_preview");
@ -1403,7 +1407,7 @@ impl RenderState {
tree: ShapesPoolRef,
timestamp: i32,
sync_render: bool,
) -> Result<(), String> {
) -> Result<()> {
let _start = performance::begin_timed_log!("start_render_loop");
let scale = self.get_scale();
@ -1430,7 +1434,7 @@ impl RenderState {
|| viewbox_cache_size.height > cached_viewbox_cache_size.height
{
self.surfaces
.resize_cache(viewbox_cache_size, VIEWPORT_INTEREST_AREA_THRESHOLD);
.resize_cache(viewbox_cache_size, VIEWPORT_INTEREST_AREA_THRESHOLD)?;
}
// FIXME - review debug
@ -1475,7 +1479,7 @@ impl RenderState {
base_object: Option<&Uuid>,
tree: ShapesPoolRef,
timestamp: i32,
) -> Result<(), String> {
) -> Result<()> {
performance::begin_measure!("process_animation_frame");
if self.render_in_progress {
if tree.len() != 0 {
@ -1499,7 +1503,7 @@ impl RenderState {
base_object: Option<&Uuid>,
tree: ShapesPoolRef,
timestamp: i32,
) -> Result<(), String> {
) -> Result<()> {
if tree.len() != 0 {
self.render_shape_tree_partial(base_object, tree, timestamp, false)?;
}
@ -1589,7 +1593,7 @@ impl RenderState {
element: &Shape,
visited_mask: bool,
clip_bounds: Option<ClipStack>,
) {
) -> Result<()> {
if visited_mask {
// Because masked groups needs two rendering passes (first drawing
// the content and then drawing the mask), we need to do an
@ -1660,7 +1664,7 @@ impl RenderState {
None,
None,
None,
);
)?;
}
// Only restore if we created a layer (optimization for simple shapes)
@ -1672,19 +1676,22 @@ impl RenderState {
}
self.focus_mode.exit(&element.id);
Ok(())
}
pub fn get_current_tile_bounds(&mut self) -> Rect {
let tiles::Tile(tile_x, tile_y) = self.current_tile.unwrap();
pub fn get_current_tile_bounds(&mut self) -> Result<Rect> {
let tiles::Tile(tile_x, tile_y) = self
.current_tile
.ok_or(Error::CriticalError("Current tile not found".to_string()))?;
let scale = self.get_scale();
let offset_x = self.viewbox.area.left * scale;
let offset_y = self.viewbox.area.top * scale;
Rect::from_xywh(
Ok(Rect::from_xywh(
(tile_x as f32 * tiles::TILE_SIZE) - offset_x,
(tile_y as f32 * tiles::TILE_SIZE) - offset_y,
tiles::TILE_SIZE,
tiles::TILE_SIZE,
)
))
}
pub fn get_rect_bounds(&mut self, rect: skia::Rect) -> Rect {
@ -1732,8 +1739,11 @@ impl RenderState {
// lower multiple of `TILE_SIZE`. This ensures the tile bounds are aligned
// with the global tile grid, which is useful for rendering tiles in a
/// consistent and predictable layout.
pub fn get_current_aligned_tile_bounds(&mut self) -> Rect {
self.get_aligned_tile_bounds(self.current_tile.unwrap())
pub fn get_current_aligned_tile_bounds(&mut self) -> Result<Rect> {
Ok(self.get_aligned_tile_bounds(
self.current_tile
.ok_or(Error::CriticalError("Current tile not found".to_string()))?,
))
}
/// Renders a drop shadow effect for the given shape.
@ -1750,7 +1760,7 @@ impl RenderState {
scale: f32,
translation: (f32, f32),
extra_layer_blur: Option<Blur>,
) {
) -> Result<()> {
let mut transformed_shadow: Cow<Shadow> = Cow::Borrowed(shadow);
transformed_shadow.to_mut().offset = (0.0, 0.0);
transformed_shadow.to_mut().color = skia::Color::BLACK;
@ -1805,7 +1815,7 @@ impl RenderState {
plain_shape_mut.clip_content = false;
let Some(drop_filter) = transformed_shadow.get_drop_shadow_filter() else {
return;
return Ok(());
};
let mut bounds = drop_filter.compute_fast_bounds(shape_bounds);
@ -1813,7 +1823,7 @@ impl RenderState {
bounds.offset(world_offset);
// Early cull if the shadow bounds are outside the render area.
if !bounds.intersects(self.render_area_with_margins) {
return;
return Ok(());
}
// blur=0 at high zoom: draw directly on DropShadows with geometric spread (no filter).
@ -1835,11 +1845,11 @@ impl RenderState {
Some(shadow.offset),
None,
Some(shadow.spread),
);
});
)
})?;
self.surfaces.canvas(SurfaceId::DropShadows).restore();
return;
return Ok(());
}
// Create filter with blur only (no offset, no spread - handled geometrically)
@ -1877,11 +1887,11 @@ impl RenderState {
Some(shadow.offset), // Offset is geometric
None,
Some(shadow.spread),
);
});
)
})?;
self.surfaces.canvas(SurfaceId::DropShadows).restore();
return;
return Ok(());
}
// Adaptive downscale for large blur values (lossless GPU optimization).
@ -1918,12 +1928,13 @@ impl RenderState {
Some(shadow.offset), // Offset is geometric
None,
Some(shadow.spread),
);
});
)
})?;
state.surfaces.canvas(temp_surface).restore();
Ok(())
},
);
)?;
if let Some((mut surface, filter_scale)) = filter_result {
let drop_canvas = self.surfaces.canvas(SurfaceId::DropShadows);
@ -1958,6 +1969,7 @@ impl RenderState {
}
drop_canvas.restore();
}
Ok(())
}
/// Renders element drop shadows to DropShadows surface and composites to Current.
@ -1972,7 +1984,7 @@ impl RenderState {
scale: f32,
translation: (f32, f32),
node_render_state: &NodeRenderState,
) {
) -> Result<()> {
let element_extrect = extrect.get_or_insert_with(|| element.extrect(tree, scale));
let inherited_layer_blur = match element.shape_type {
Type::Frame(_) | Type::Group(_) => element.blur,
@ -1994,7 +2006,7 @@ impl RenderState {
scale,
translation,
None,
);
)?;
if !matches!(element.shape_type, Type::Bool(_)) {
let shadow_children = if element.is_recursive() {
@ -2023,7 +2035,7 @@ impl RenderState {
scale,
translation,
inherited_layer_blur,
);
)?;
} else {
let paint = skia::Paint::default();
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
@ -2059,8 +2071,8 @@ impl RenderState {
None,
Some(vec![new_shadow_paint.clone()]),
None,
);
});
)
})?;
self.surfaces.canvas(SurfaceId::DropShadows).restore();
}
}
@ -2117,6 +2129,7 @@ impl RenderState {
self.surfaces
.canvas(SurfaceId::DropShadows)
.clear(skia::Color::TRANSPARENT);
Ok(())
}
pub fn render_shape_tree_partial_uncached(
@ -2124,7 +2137,7 @@ impl RenderState {
tree: ShapesPoolRef,
timestamp: i32,
allow_stop: bool,
) -> Result<(bool, bool), String> {
) -> Result<(bool, bool)> {
let mut iteration = 0;
let mut is_empty = true;
@ -2152,7 +2165,7 @@ impl RenderState {
if visited_children {
if !node_render_state.flattened {
self.render_shape_exit(element, visited_mask, clip_bounds);
self.render_shape_exit(element, visited_mask, clip_bounds)?;
}
continue;
}
@ -2226,7 +2239,13 @@ impl RenderState {
scale,
translation,
&node_render_state,
);
)?;
}
// Render background blur BEFORE save_layer so it modifies
// the backdrop independently of the shape's opacity.
if !node_render_state.is_root() && self.focus_mode.is_active() {
self.render_background_blur(element);
}
// Render background blur BEFORE save_layer so it modifies
@ -2262,7 +2281,7 @@ impl RenderState {
scale,
translation,
&node_render_state,
);
)?;
}
self.render_shape(
@ -2276,7 +2295,7 @@ impl RenderState {
None,
None,
None,
);
)?;
self.surfaces
.canvas(SurfaceId::DropShadows)
@ -2373,14 +2392,14 @@ impl RenderState {
tree: ShapesPoolRef,
timestamp: i32,
allow_stop: bool,
) -> Result<(), String> {
) -> Result<()> {
let mut should_stop = false;
let root_ids = {
if let Some(shape_id) = base_object {
vec![*shape_id]
} else {
let Some(root) = tree.get(&Uuid::nil()) else {
return Err(String::from("Root shape not found"));
return Err(Error::CriticalError("Root shape not found".to_string()));
};
root.children_ids(false)
}
@ -2390,7 +2409,7 @@ impl RenderState {
if let Some(current_tile) = self.current_tile {
if self.surfaces.has_cached_tile_surface(current_tile) {
performance::begin_measure!("render_shape_tree::cached");
let tile_rect = self.get_current_tile_bounds();
let tile_rect = self.get_current_tile_bounds()?;
self.surfaces.draw_cached_tile_surface(
current_tile,
tile_rect,
@ -2420,9 +2439,9 @@ impl RenderState {
return Ok(());
}
performance::end_measure!("render_shape_tree::uncached");
let tile_rect = self.get_current_tile_bounds();
let tile_rect = self.get_current_tile_bounds()?;
if !is_empty {
self.apply_render_to_final_canvas(tile_rect);
self.apply_render_to_final_canvas(tile_rect)?;
if self.options.is_debug_visible() {
debug::render_workspace_current_tile(
@ -2760,7 +2779,11 @@ impl RenderState {
///
/// This is useful when you have a pre-computed set of shape IDs that need to be refreshed,
/// regardless of their relationship to other shapes (e.g., ancestors, descendants, or any other collection).
pub fn update_tiles_shapes(&mut self, shape_ids: &[Uuid], tree: ShapesPoolMutRef<'_>) {
pub fn update_tiles_shapes(
&mut self,
shape_ids: &[Uuid],
tree: ShapesPoolMutRef<'_>,
) -> Result<()> {
performance::begin_measure!("invalidate_and_update_tiles");
let mut all_tiles = HashSet::<tiles::Tile>::new();
for shape_id in shape_ids {
@ -2772,6 +2795,7 @@ impl RenderState {
self.remove_cached_tile(tile);
}
performance::end_measure!("invalidate_and_update_tiles");
Ok(())
}
/// Rebuilds tiles for shapes with modifiers and processes their ancestors
@ -2780,9 +2804,14 @@ impl RenderState {
/// Additionally, it processes all ancestors of modified shapes to ensure their
/// extended rectangles are properly recalculated and their tiles are updated.
/// This is crucial for frames and groups that contain transformed children.
pub fn rebuild_modifier_tiles(&mut self, tree: ShapesPoolMutRef<'_>, ids: Vec<Uuid>) {
pub fn rebuild_modifier_tiles(
&mut self,
tree: ShapesPoolMutRef<'_>,
ids: Vec<Uuid>,
) -> Result<()> {
let ancestors = all_with_ancestors(&ids, tree, false);
self.update_tiles_shapes(&ancestors, tree);
self.update_tiles_shapes(&ancestors, tree)?;
Ok(())
}
pub fn get_scale(&self) -> f32 {

View File

@ -179,9 +179,12 @@ pub fn render_debug_shape(
#[cfg(target_arch = "wasm32")]
#[allow(dead_code)]
pub fn console_debug_surface(render_state: &mut RenderState, id: SurfaceId) {
let base64_image = render_state.surfaces.base64_snapshot(id);
let base64_image = render_state
.surfaces
.base64_snapshot(id)
.expect("Failed to get base64 image");
run_script!(format!("console.log('%c ', 'font-size: 1px; background: url(data:image/png;base64,{base64_image}) no-repeat; padding: 100px; background-size: contain;')"))
run_script!(format!("console.log('%c ', 'font-size: 1px; background: url(data:image/png;base64,{base64_image}) no-repeat; padding: 100px; background-size: contain;')"));
}
#[allow(dead_code)]
@ -194,7 +197,10 @@ pub fn console_debug_surface_rect(render_state: &mut RenderState, id: SurfaceId,
rect.bottom as i32,
);
let base64_image = render_state.surfaces.base64_snapshot_rect(id, int_rect);
let base64_image = render_state
.surfaces
.base64_snapshot_rect(id, int_rect)
.expect("Failed to get base64 image");
if let Some(base64_image) = base64_image {
run_script!(format!("console.log('%c ', 'font-size: 1px; background: url(data:image/png;base64,{base64_image}) no-repeat; padding: 100px; background-size: contain;')"))

View File

@ -1,6 +1,7 @@
use skia_safe::{self as skia, Paint, RRect};
use super::{filters, RenderState, SurfaceId};
use crate::error::Result;
use crate::render::get_source_rect;
use crate::shapes::{merge_fills, Fill, Frame, ImageFill, Rect, Shape, StrokeKind, Type};
@ -20,12 +21,11 @@ fn draw_image_fill(
antialias: bool,
surface_id: SurfaceId,
) {
let image = render_state.images.get(&image_fill.id());
if image.is_none() {
let Some(image) = render_state.images.get(&image_fill.id()) else {
return;
}
};
let size = image.unwrap().dimensions();
let size = image.dimensions();
let canvas = render_state.surfaces.canvas_and_mark_dirty(surface_id);
let container = &shape.selrect;
let path_transform = shape.to_path_transform();
@ -85,15 +85,13 @@ fn draw_image_fill(
}
// Draw the image with the calculated destination rectangle
if let Some(image) = image {
canvas.draw_image_rect_with_sampling_options(
image,
Some((&src_rect, skia::canvas::SrcRectConstraint::Strict)),
dest_rect,
render_state.sampling_options,
paint,
);
}
canvas.draw_image_rect_with_sampling_options(
image,
Some((&src_rect, skia::canvas::SrcRectConstraint::Strict)),
dest_rect,
render_state.sampling_options,
paint,
);
// Restore the canvas to remove the clipping
canvas.restore();
@ -109,9 +107,9 @@ pub fn render(
antialias: bool,
surface_id: SurfaceId,
outset: Option<f32>,
) {
) -> Result<()> {
if fills.is_empty() {
return;
return Ok(());
}
let scale = render_state.get_scale().max(1e-6);
@ -134,9 +132,9 @@ pub fn render(
surface_id,
outset,
inset,
);
)?;
}
return;
return Ok(());
}
let mut paint = merge_fills(fills, shape.selrect);
@ -152,15 +150,17 @@ pub fn render(
let mut filtered_paint = paint.clone();
filtered_paint.set_image_filter(image_filter.clone());
draw_fill_to_surface(state, shape, temp_surface, &filtered_paint, outset, inset);
Ok(())
},
) {
return;
)? {
return Ok(());
} else {
paint.set_image_filter(image_filter);
}
}
draw_fill_to_surface(render_state, shape, surface_id, &paint, outset, inset);
Ok(())
}
/// Draws a single paint (with a merged shader) to the appropriate surface
@ -203,7 +203,7 @@ fn render_single_fill(
surface_id: SurfaceId,
outset: Option<f32>,
inset: Option<f32>,
) {
) -> Result<()> {
let mut paint = fill.to_paint(&shape.selrect, antialias);
if let Some(image_filter) = shape.image_filter(1.) {
let bounds = image_filter.compute_fast_bounds(shape.selrect);
@ -224,9 +224,10 @@ fn render_single_fill(
outset,
inset,
);
Ok(())
},
) {
return;
)? {
return Ok(());
} else {
paint.set_image_filter(image_filter);
}
@ -242,6 +243,7 @@ fn render_single_fill(
outset,
inset,
);
Ok(())
}
#[allow(clippy::too_many_arguments)]

View File

@ -1,6 +1,7 @@
use skia_safe::{self as skia, ImageFilter, Rect};
use super::{RenderState, SurfaceId};
use crate::error::Result;
/// 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.
@ -36,12 +37,12 @@ pub fn render_with_filter_surface<F>(
bounds: Rect,
target_surface: SurfaceId,
draw_fn: F,
) -> bool
) -> Result<bool>
where
F: FnOnce(&mut RenderState, SurfaceId),
F: FnOnce(&mut RenderState, SurfaceId) -> Result<()>,
{
if let Some((mut surface, scale)) =
render_into_filter_surface(render_state, bounds, 1.0, draw_fn)
render_into_filter_surface(render_state, bounds, 1.0, draw_fn)?
{
let canvas = render_state.surfaces.canvas_and_mark_dirty(target_surface);
@ -58,9 +59,9 @@ where
surface.draw(canvas, (0.0, 0.0), render_state.sampling_options, None);
canvas.restore();
}
true
Ok(true)
} else {
false
Ok(false)
}
}
@ -81,12 +82,12 @@ pub fn render_into_filter_surface<F>(
bounds: Rect,
extra_downscale: f32,
draw_fn: F,
) -> Option<(skia::Surface, f32)>
) -> Result<Option<(skia::Surface, f32)>>
where
F: FnOnce(&mut RenderState, SurfaceId),
F: FnOnce(&mut RenderState, SurfaceId) -> Result<()>,
{
if !bounds.is_finite() || bounds.width() <= 0.0 || bounds.height() <= 0.0 {
return None;
return Ok(None);
}
let filter_id = SurfaceId::Filter;
@ -125,10 +126,10 @@ where
canvas.translate((-bounds.left, -bounds.top));
}
draw_fn(render_state, filter_id);
draw_fn(render_state, filter_id)?;
render_state.surfaces.canvas(filter_id).restore();
let filter_surface = render_state.surfaces.surface_clone(filter_id);
Some((filter_surface, scale))
Ok(Some((filter_surface, scale)))
}

View File

@ -1,6 +1,7 @@
use skia_safe::{self as skia, textlayout, Font, FontMgr};
use std::collections::HashSet;
use crate::error::{Error, Result};
use crate::shapes::{FontFamily, FontStyle};
use crate::uuid::Uuid;
@ -26,7 +27,7 @@ pub struct FontStore {
}
impl FontStore {
pub fn new() -> Self {
pub fn try_new() -> Result<Self> {
let font_mgr = FontMgr::new();
let font_provider = load_default_provider(&font_mgr);
let mut font_collection = skia::textlayout::FontCollection::new();
@ -34,17 +35,19 @@ impl FontStore {
let debug_typeface = font_provider
.match_family_style(default_font().as_str(), skia::FontStyle::default())
.unwrap();
.ok_or(Error::CriticalError(
"Failed to match default font".to_string(),
))?;
let debug_font = skia::Font::new(debug_typeface, 10.0);
Self {
Ok(Self {
font_mgr,
font_provider,
font_collection,
debug_font,
fallback_fonts: HashSet::new(),
}
})
}
pub fn set_scale_debug_font(&mut self, dpr: f32) {
@ -70,7 +73,7 @@ impl FontStore {
font_data: &[u8],
is_emoji: bool,
is_fallback: bool,
) -> Result<(), String> {
) -> Result<()> {
if self.has_family(&family, is_emoji) {
return Ok(());
}
@ -78,7 +81,9 @@ impl FontStore {
let typeface = self
.font_mgr
.new_from_data(font_data, None)
.ok_or("Failed to create typeface")?;
.ok_or(Error::CriticalError(
"Failed to create typeface".to_string(),
))?;
let alias = format!("{}", family);
let font_name = if is_emoji {

View File

@ -1,3 +1,4 @@
use crate::error::{Error, Result};
use skia_safe::gpu::{self, gl::FramebufferInfo, gl::TextureInfo, DirectContext};
use skia_safe::{self as skia, ISize};
@ -8,24 +9,30 @@ pub struct GpuState {
}
impl GpuState {
pub fn new() -> Self {
let interface = gpu::gl::Interface::new_native().unwrap();
let context = gpu::direct_contexts::make_gl(interface, None).unwrap();
pub fn try_new() -> Result<Self> {
let interface = gpu::gl::Interface::new_native().ok_or(Error::CriticalError(
"Failed to create GL interface".to_string(),
))?;
let context = gpu::direct_contexts::make_gl(interface, None).ok_or(
Error::CriticalError("Failed to create GL context".to_string()),
)?;
let framebuffer_info = {
let mut fboid: gl::types::GLint = 0;
unsafe { gl::GetIntegerv(gl::FRAMEBUFFER_BINDING, &mut fboid) };
FramebufferInfo {
fboid: fboid.try_into().unwrap(),
fboid: fboid.try_into().map_err(|_| {
Error::CriticalError("Failed to convert GL framebuffer ID to u32".to_string())
})?,
format: gpu::gl::Format::RGBA8.into(),
protected: gpu::Protected::No,
}
};
GpuState {
Ok(GpuState {
context,
framebuffer_info,
}
})
}
fn create_webgl_texture(&mut self, width: i32, height: i32) -> gl::types::GLuint {
@ -56,7 +63,11 @@ impl GpuState {
texture_id
}
pub fn create_surface_with_isize(&mut self, label: String, size: ISize) -> skia::Surface {
pub fn create_surface_with_isize(
&mut self,
label: String,
size: ISize,
) -> Result<skia::Surface> {
self.create_surface_with_dimensions(label, size.width, size.height)
}
@ -65,7 +76,7 @@ impl GpuState {
label: String,
width: i32,
height: i32,
) -> skia::Surface {
) -> Result<skia::Surface> {
let backend_texture = unsafe {
let texture_id = self.create_webgl_texture(width, height);
let texture_info = TextureInfo {
@ -77,7 +88,7 @@ impl GpuState {
gpu::backend_textures::make_gl((width, height), gpu::Mipmapped::No, texture_info, label)
};
gpu::surfaces::wrap_backend_texture(
let surface = gpu::surfaces::wrap_backend_texture(
&mut self.context,
&backend_texture,
gpu::SurfaceOrigin::BottomLeft,
@ -86,15 +97,19 @@ impl GpuState {
None,
None,
)
.unwrap()
.ok_or(Error::CriticalError(
"Failed to create Skia surface".to_string(),
))?;
Ok(surface)
}
/// Create a Skia surface that will be used for rendering.
pub fn create_target_surface(&mut self, width: i32, height: i32) -> skia::Surface {
pub fn create_target_surface(&mut self, width: i32, height: i32) -> Result<skia::Surface> {
let backend_render_target =
gpu::backend_render_targets::make_gl((width, height), 1, 8, self.framebuffer_info);
gpu::surfaces::wrap_backend_render_target(
let surface = gpu::surfaces::wrap_backend_render_target(
&mut self.context,
&backend_render_target,
gpu::SurfaceOrigin::BottomLeft,
@ -102,6 +117,10 @@ impl GpuState {
None,
None,
)
.unwrap()
.ok_or(Error::CriticalError(
"Failed to create Skia surface".to_string(),
))?;
Ok(surface)
}
}

View File

@ -2,6 +2,7 @@ use crate::math::Rect as MathRect;
use crate::shapes::ImageFill;
use crate::uuid::Uuid;
use crate::error::{Error, Result};
use skia_safe::gpu::{surfaces, Budgeted, DirectContext};
use skia_safe::{self as skia, Codec, ISize};
use std::collections::HashMap;
@ -70,7 +71,7 @@ fn create_image_from_gl_texture(
texture_id: u32,
width: i32,
height: i32,
) -> Result<Image, String> {
) -> Result<Image> {
use skia_safe::gpu;
use skia_safe::gpu::gl::TextureInfo;
@ -99,7 +100,9 @@ fn create_image_from_gl_texture(
skia::AlphaType::Premul,
None,
)
.ok_or("Failed to create Skia image from GL texture")?;
.ok_or(crate::error::Error::CriticalError(
"Failed to create Skia image from GL texture".to_string(),
))?;
Ok(image)
}
@ -147,11 +150,16 @@ impl ImageStore {
}
}
pub fn add(&mut self, id: Uuid, is_thumbnail: bool, image_data: &[u8]) -> Result<(), String> {
pub fn add(
&mut self,
id: Uuid,
is_thumbnail: bool,
image_data: &[u8],
) -> crate::error::Result<()> {
let key = (id, is_thumbnail);
if self.images.contains_key(&key) {
return Err("Image already exists".to_string());
return Err(Error::RecoverableError("Image already exists".to_string()));
}
let raw_data = image_data.to_vec();
@ -174,11 +182,11 @@ impl ImageStore {
texture_id: u32,
width: i32,
height: i32,
) -> Result<(), String> {
) -> Result<()> {
let key = (id, is_thumbnail);
if self.images.contains_key(&key) {
return Err("Image already exists".to_string());
return Err(Error::RecoverableError("Image already exists".to_string()));
}
// Create a Skia image from the existing GL texture

View File

@ -3,6 +3,7 @@ use crate::render::strokes;
use crate::shapes::{ParagraphBuilderGroup, Shadow, Shape, Stroke, Type};
use skia_safe::{canvas::SaveLayerRec, Paint, Path};
use crate::error::Result;
use crate::render::text;
// Fill Shadows
@ -36,7 +37,7 @@ pub fn render_stroke_inner_shadows(
stroke: &Stroke,
antialias: bool,
surface_id: SurfaceId,
) {
) -> Result<()> {
if !shape.has_fills() {
for shadow in shape.inner_shadows_visible() {
let filter = shadow.get_inner_shadow_filter();
@ -48,9 +49,10 @@ pub fn render_stroke_inner_shadows(
filter.as_ref(),
antialias,
None, // Inner shadows don't use spread
)
)?;
}
}
Ok(())
}
// Render text paths (unused)
@ -133,9 +135,9 @@ pub fn render_text_shadows(
surface_id: Option<SurfaceId>,
shadows: &[Paint],
blur_filter: &Option<skia_safe::ImageFilter>,
) {
) -> Result<()> {
if stroke_paragraphs_group.is_empty() {
return;
return Ok(());
}
let canvas = render_state
@ -156,7 +158,7 @@ pub fn render_text_shadows(
blur_filter.as_ref(),
None,
None,
);
)?;
for stroke_paragraphs in stroke_paragraphs_group.iter_mut() {
text::render(
@ -169,9 +171,10 @@ pub fn render_text_shadows(
blur_filter.as_ref(),
None,
None,
);
)?;
}
canvas.restore();
}
Ok(())
}

View File

@ -6,6 +6,7 @@ use crate::shapes::{
use skia_safe::{self as skia, ImageFilter, RRect};
use super::{filters, RenderState, SurfaceId};
use crate::error::{Error, Result};
use crate::render::filters::compose_filters;
use crate::render::{get_dest_rect, get_source_rect};
@ -294,16 +295,16 @@ fn handle_stroke_caps(
blur: Option<&ImageFilter>,
_antialias: bool,
) {
let mut points = path.points().to_vec();
// Curves can have duplicated points, so let's remove consecutive duplicated points
points.dedup();
let c_points = points.len();
// Closed shapes don't have caps
if c_points >= 2 && is_open {
let first_point = points.first().unwrap();
let last_point = points.last().unwrap();
if !is_open {
return;
}
// Curves can have duplicated points, so let's remove consecutive duplicated points
let mut points = path.points().to_vec();
points.dedup();
if let [first_point, .., last_point] = points.as_slice() {
let mut paint_stroke = paint.clone();
if let Some(filter) = blur {
@ -328,7 +329,7 @@ fn handle_stroke_caps(
stroke.width,
&mut paint_stroke,
last_point,
&points[c_points - 2],
&points[points.len() - 2],
);
}
}
@ -456,14 +457,13 @@ fn draw_image_stroke_in_container(
image_fill: &ImageFill,
antialias: bool,
surface_id: SurfaceId,
) {
) -> Result<()> {
let scale = render_state.get_scale();
let image = render_state.images.get(&image_fill.id());
if image.is_none() {
return;
}
let Some(image) = render_state.images.get(&image_fill.id()) else {
return Ok(());
};
let size = image.unwrap().dimensions();
let size = image.dimensions();
let canvas = render_state.surfaces.canvas_and_mark_dirty(surface_id);
let container = &shape.selrect;
let path_transform = shape.to_path_transform();
@ -509,7 +509,10 @@ fn draw_image_stroke_in_container(
shape_type @ (Type::Path(_) | Type::Bool(_)) => {
if let Some(p) = shape_type.path() {
canvas.save();
let path = p.to_skia_path().make_transform(&path_transform.unwrap());
let path = p.to_skia_path().make_transform(
&path_transform.ok_or(Error::CriticalError("No path transform".to_string()))?,
);
let stroke_kind = stroke.render_kind(p.is_open());
match stroke_kind {
StrokeKind::Inner => {
@ -561,7 +564,7 @@ fn draw_image_stroke_in_container(
canvas.clip_rect(dest_rect, skia::ClipOp::Intersect, antialias);
canvas.draw_image_rect_with_sampling_options(
image.unwrap(),
image,
Some((&src_rect, skia::canvas::SrcRectConstraint::Strict)),
dest_rect,
render_state.sampling_options,
@ -571,7 +574,9 @@ fn draw_image_stroke_in_container(
// Clear outer stroke for paths if necessary. When adding an outer stroke we need to empty the stroke added too in the inner area.
if let Type::Path(p) = &shape.shape_type {
if stroke.render_kind(p.is_open()) == StrokeKind::Outer {
let path = p.to_skia_path().make_transform(&path_transform.unwrap());
let path = p.to_skia_path().make_transform(
&path_transform.ok_or(Error::CriticalError("No path transform".to_string()))?,
);
let mut clear_paint = skia::Paint::default();
clear_paint.set_blend_mode(skia::BlendMode::Clear);
clear_paint.set_anti_alias(antialias);
@ -581,6 +586,7 @@ fn draw_image_stroke_in_container(
// Restore canvas state
canvas.restore();
Ok(())
}
/// Renders all strokes for a shape. Merges strokes that share the same
@ -593,9 +599,9 @@ pub fn render(
surface_id: Option<SurfaceId>,
antialias: bool,
outset: Option<f32>,
) {
) -> Result<()> {
if strokes.is_empty() {
return;
return Ok(());
}
let has_image_fills = strokes.iter().any(|s| matches!(s.fill, Fill::Image(_)));
@ -655,13 +661,14 @@ pub fn render(
true,
true,
outset,
);
)?;
}
state.surfaces.canvas(temp_surface).restore();
Ok(())
},
) {
return;
)? {
return Ok(());
}
}
@ -675,9 +682,9 @@ pub fn render(
None,
antialias,
outset,
);
)?;
}
return;
return Ok(());
}
render_merged(
@ -688,7 +695,7 @@ pub fn render(
antialias,
false,
outset,
);
)
}
fn strokes_share_geometry(strokes: &[&Stroke]) -> bool {
@ -709,7 +716,7 @@ fn render_merged(
antialias: bool,
bypass_filter: bool,
outset: Option<f32>,
) {
) -> Result<()> {
let representative = *strokes
.last()
.expect("render_merged expects at least one stroke");
@ -761,14 +768,15 @@ fn render_merged(
antialias,
true,
outset,
);
)?;
state.surfaces.apply_mut(temp_surface as u32, |surface| {
surface.canvas().restore();
});
Ok(())
},
) {
return;
)? {
return Ok(());
}
}
}
@ -844,6 +852,7 @@ fn render_merged(
}
_ => unreachable!("This shape should not have strokes"),
}
Ok(())
}
/// Renders a single stroke. Used by the shadow module which needs per-stroke
@ -857,7 +866,7 @@ pub fn render_single(
shadow: Option<&ImageFilter>,
antialias: bool,
outset: Option<f32>,
) {
) -> Result<()> {
render_single_internal(
render_state,
shape,
@ -868,7 +877,7 @@ pub fn render_single(
false,
false,
outset,
);
)
}
#[allow(clippy::too_many_arguments)]
@ -882,7 +891,7 @@ fn render_single_internal(
bypass_filter: bool,
skip_blur: bool,
outset: Option<f32>,
) {
) -> Result<()> {
if !bypass_filter {
if let Some(image_filter) = shape.image_filter(1.) {
let mut content_bounds = shape.selrect;
@ -916,10 +925,10 @@ fn render_single_internal(
true,
true,
outset,
);
)
},
) {
return;
)? {
return Ok(());
}
}
}
@ -949,7 +958,7 @@ fn render_single_internal(
image_fill,
antialias,
target_surface,
);
)?;
}
} else {
match &shape.shape_type {
@ -1014,6 +1023,7 @@ fn render_single_internal(
_ => unreachable!("This shape should not have strokes"),
}
}
Ok(())
}
// Render text paths (unused)

View File

@ -1,3 +1,4 @@
use crate::error::{Error, Result};
use crate::performance;
use crate::shapes::Shape;
@ -61,38 +62,39 @@ pub struct Surfaces {
#[allow(dead_code)]
impl Surfaces {
pub fn new(
pub fn try_new(
gpu_state: &mut GpuState,
(width, height): (i32, i32),
sampling_options: skia::SamplingOptions,
tile_dims: skia::ISize,
) -> Self {
) -> Result<Self> {
let extra_tile_dims = skia::ISize::new(
tile_dims.width * TILE_SIZE_MULTIPLIER,
tile_dims.height * TILE_SIZE_MULTIPLIER,
);
let margins = skia::ISize::new(extra_tile_dims.width / 4, extra_tile_dims.height / 4);
let target = gpu_state.create_target_surface(width, height);
let filter = gpu_state.create_surface_with_isize("filter".to_string(), extra_tile_dims);
let cache = gpu_state.create_surface_with_dimensions("cache".to_string(), width, height);
let current = gpu_state.create_surface_with_isize("current".to_string(), extra_tile_dims);
let target = gpu_state.create_target_surface(width, height)?;
let filter = gpu_state.create_surface_with_isize("filter".to_string(), extra_tile_dims)?;
let cache = gpu_state.create_surface_with_dimensions("cache".to_string(), width, height)?;
let current =
gpu_state.create_surface_with_isize("current".to_string(), extra_tile_dims)?;
let drop_shadows =
gpu_state.create_surface_with_isize("drop_shadows".to_string(), extra_tile_dims);
gpu_state.create_surface_with_isize("drop_shadows".to_string(), extra_tile_dims)?;
let inner_shadows =
gpu_state.create_surface_with_isize("inner_shadows".to_string(), extra_tile_dims);
let text_drop_shadows =
gpu_state.create_surface_with_isize("text_drop_shadows".to_string(), extra_tile_dims);
gpu_state.create_surface_with_isize("inner_shadows".to_string(), extra_tile_dims)?;
let text_drop_shadows = gpu_state
.create_surface_with_isize("text_drop_shadows".to_string(), extra_tile_dims)?;
let shape_fills =
gpu_state.create_surface_with_isize("shape_fills".to_string(), extra_tile_dims);
gpu_state.create_surface_with_isize("shape_fills".to_string(), extra_tile_dims)?;
let shape_strokes =
gpu_state.create_surface_with_isize("shape_strokes".to_string(), extra_tile_dims);
gpu_state.create_surface_with_isize("shape_strokes".to_string(), extra_tile_dims)?;
let ui = gpu_state.create_surface_with_dimensions("ui".to_string(), width, height);
let debug = gpu_state.create_surface_with_dimensions("debug".to_string(), width, height);
let ui = gpu_state.create_surface_with_dimensions("ui".to_string(), width, height)?;
let debug = gpu_state.create_surface_with_dimensions("debug".to_string(), width, height)?;
let tiles = TileTextureCache::new();
Surfaces {
Ok(Surfaces {
target,
filter,
cache,
@ -108,7 +110,7 @@ impl Surfaces {
sampling_options,
margins,
dirty_surfaces: 0,
}
})
}
pub fn clear_tiles(&mut self) {
@ -119,8 +121,14 @@ impl Surfaces {
self.margins
}
pub fn resize(&mut self, gpu_state: &mut GpuState, new_width: i32, new_height: i32) {
self.reset_from_target(gpu_state.create_target_surface(new_width, new_height));
pub fn resize(
&mut self,
gpu_state: &mut GpuState,
new_width: i32,
new_height: i32,
) -> Result<()> {
self.reset_from_target(gpu_state.create_target_surface(new_width, new_height)?)?;
Ok(())
}
pub fn snapshot(&mut self, id: SurfaceId) -> skia::Image {
@ -132,26 +140,33 @@ impl Surfaces {
(self.filter.width(), self.filter.height())
}
pub fn base64_snapshot(&mut self, id: SurfaceId) -> String {
pub fn base64_snapshot(&mut self, id: SurfaceId) -> Result<String> {
let surface = self.get_mut(id);
let image = surface.image_snapshot();
let mut context = surface.direct_context();
let encoded_image = image
.encode(context.as_mut(), skia::EncodedImageFormat::PNG, None)
.unwrap();
general_purpose::STANDARD.encode(encoded_image.as_bytes())
.ok_or(Error::CriticalError("Failed to encode image".to_string()))?;
Ok(general_purpose::STANDARD.encode(encoded_image.as_bytes()))
}
pub fn base64_snapshot_rect(&mut self, id: SurfaceId, irect: skia::IRect) -> Option<String> {
pub fn base64_snapshot_rect(
&mut self,
id: SurfaceId,
irect: skia::IRect,
) -> Result<Option<String>> {
let surface = self.get_mut(id);
if let Some(image) = surface.image_snapshot_with_bounds(irect) {
let mut context = surface.direct_context();
let encoded_image = image
.encode(context.as_mut(), skia::EncodedImageFormat::PNG, None)
.unwrap();
return Some(general_purpose::STANDARD.encode(encoded_image.as_bytes()));
.ok_or(Error::CriticalError("Failed to encode image".to_string()))?;
Ok(Some(
general_purpose::STANDARD.encode(encoded_image.as_bytes()),
))
} else {
Ok(None)
}
None
}
/// Returns a mutable reference to the canvas and automatically marks
@ -341,22 +356,41 @@ impl Surfaces {
}
}
fn reset_from_target(&mut self, target: skia::Surface) {
fn reset_from_target(&mut self, target: skia::Surface) -> Result<()> {
let dim = (target.width(), target.height());
self.target = target;
self.filter = self.target.new_surface_with_dimensions(dim).unwrap();
self.debug = self.target.new_surface_with_dimensions(dim).unwrap();
self.ui = self.target.new_surface_with_dimensions(dim).unwrap();
self.filter = self
.target
.new_surface_with_dimensions(dim)
.ok_or(Error::CriticalError("Failed to create surface".to_string()))?;
self.debug = self
.target
.new_surface_with_dimensions(dim)
.ok_or(Error::CriticalError("Failed to create surface".to_string()))?;
self.ui = self
.target
.new_surface_with_dimensions(dim)
.ok_or(Error::CriticalError("Failed to create surface".to_string()))?;
// The rest are tile size surfaces
Ok(())
}
pub fn resize_cache(&mut self, cache_dims: skia::ISize, interest_area_threshold: i32) {
self.cache = self.target.new_surface_with_dimensions(cache_dims).unwrap();
pub fn resize_cache(
&mut self,
cache_dims: skia::ISize,
interest_area_threshold: i32,
) -> Result<()> {
self.cache = self
.target
.new_surface_with_dimensions(cache_dims)
.ok_or(Error::CriticalError("Failed to create surface".to_string()))?;
self.cache.canvas().reset_matrix();
self.cache.canvas().translate((
(interest_area_threshold as f32 * TILE_SIZE),
(interest_area_threshold as f32 * TILE_SIZE),
));
Ok(())
}
pub fn draw_rect_to(

View File

@ -1,5 +1,6 @@
use super::{filters, RenderState, Shape, SurfaceId};
use crate::{
error::Result,
math::Rect,
shapes::{
calculate_position_data, calculate_text_layout_data, merge_fills, set_paint_fill,
@ -66,7 +67,7 @@ pub fn stroke_paragraph_builder_group_from_text(
}
let stroke_paragraphs: Vec<ParagraphBuilder> = (0..stroke_paragraphs_map.len())
.map(|i| stroke_paragraphs_map.remove(&i).unwrap())
.filter_map(|i| stroke_paragraphs_map.remove(&i))
.collect();
paragraph_group.push(stroke_paragraphs);
@ -195,7 +196,7 @@ pub fn render_with_bounds_outset(
stroke_bounds_outset: f32,
fill_inset: Option<f32>,
layer_opacity: Option<f32>,
) {
) -> Result<()> {
if let Some(render_state) = render_state {
let target_surface = surface_id.unwrap_or(SurfaceId::Fills);
@ -225,9 +226,10 @@ pub fn render_with_bounds_outset(
fill_inset,
layer_opacity,
);
Ok(())
},
) {
return;
)? {
return Ok(());
}
}
}
@ -242,7 +244,7 @@ pub fn render_with_bounds_outset(
fill_inset,
layer_opacity,
);
return;
return Ok(());
}
if let Some(canvas) = canvas {
@ -256,6 +258,7 @@ pub fn render_with_bounds_outset(
layer_opacity,
);
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
@ -269,7 +272,7 @@ pub fn render(
blur: Option<&ImageFilter>,
fill_inset: Option<f32>,
layer_opacity: Option<f32>,
) {
) -> Result<()> {
render_with_bounds_outset(
render_state,
canvas,
@ -281,7 +284,7 @@ pub fn render(
0.0,
fill_inset,
layer_opacity,
);
)
}
fn render_text_on_canvas(

View File

@ -9,6 +9,7 @@ pub mod grid_layout;
use crate::math::{self as math, bools, identitish, is_close_to, Bounds, Matrix, Point};
use common::GetBounds;
use crate::error::Result;
use crate::shapes::{
ConstraintH, ConstraintV, Frame, Group, GrowType, Layout, Modifier, Shape, TransformEntry,
TransformEntrySource, Type,
@ -24,9 +25,9 @@ fn propagate_children(
parent_bounds_after: &Bounds,
transform: Matrix,
bounds: &HashMap<Uuid, Bounds>,
) -> VecDeque<Modifier> {
) -> Result<VecDeque<Modifier>> {
if identitish(&transform) {
return VecDeque::new();
return Ok(VecDeque::new());
}
let mut result = VecDeque::new();
@ -74,12 +75,12 @@ fn propagate_children(
constraint_v,
transform,
child.ignore_constraints,
);
)?;
result.push_back(Modifier::transform_propagate(*child_id, transform));
}
result
Ok(result)
}
fn calculate_group_bounds(
@ -172,9 +173,9 @@ fn propagate_transform(
entries: &mut VecDeque<Modifier>,
bounds: &mut HashMap<Uuid, Bounds>,
modifiers: &mut HashMap<Uuid, Matrix>,
) {
) -> Result<()> {
let Some(shape) = state.shapes.get(&entry.id) else {
return;
return Ok(());
};
let shapes = &state.shapes;
@ -249,7 +250,7 @@ fn propagate_transform(
&shape_bounds_after,
transform,
bounds,
);
)?;
entries.append(&mut children);
}
@ -275,6 +276,7 @@ fn propagate_transform(
entries.push_back(Modifier::reflow(parent.id, false));
}
}
Ok(())
}
fn propagate_reflow(
@ -338,34 +340,35 @@ fn reflow_shape(
reflown: &mut HashSet<Uuid>,
entries: &mut VecDeque<Modifier>,
bounds: &mut HashMap<Uuid, Bounds>,
) {
) -> Result<()> {
let Some(shape) = state.shapes.get(id) else {
return;
return Ok(());
};
let shapes = &state.shapes;
let Type::Frame(frame_data) = &shape.shape_type else {
return;
return Ok(());
};
if let Some(Layout::FlexLayout(layout_data, flex_data)) = &frame_data.layout {
let mut children =
flex_layout::reflow_flex_layout(shape, layout_data, flex_data, shapes, bounds);
flex_layout::reflow_flex_layout(shape, layout_data, flex_data, shapes, bounds)?;
entries.append(&mut children);
} else if let Some(Layout::GridLayout(layout_data, grid_data)) = &frame_data.layout {
let mut children =
grid_layout::reflow_grid_layout(shape, layout_data, grid_data, shapes, bounds);
grid_layout::reflow_grid_layout(shape, layout_data, grid_data, shapes, bounds)?;
entries.append(&mut children);
}
reflown.insert(*id);
Ok(())
}
pub fn propagate_modifiers(
state: &State,
modifiers: &[TransformEntry],
pixel_precision: bool,
) -> Vec<TransformEntry> {
) -> Result<Vec<TransformEntry>> {
let mut entries: VecDeque<_> = modifiers
.iter()
.map(|entry| {
@ -399,7 +402,7 @@ pub fn propagate_modifiers(
&mut entries,
&mut bounds,
&mut modifiers,
),
)?,
Modifier::Reflow(id, force_reflow) => {
if force_reflow {
reflown.remove(&id);
@ -437,16 +440,16 @@ pub fn propagate_modifiers(
if reflown.contains(id) {
continue;
}
reflow_shape(id, state, &mut reflown, &mut entries, &mut bounds_temp);
reflow_shape(id, state, &mut reflown, &mut entries, &mut bounds_temp)?;
}
layout_reflows = HashSet::new();
}
#[allow(dead_code)]
modifiers
// #[allow(dead_code)]
Ok(modifiers
.iter()
.map(|(key, val)| TransformEntry::from_input(*key, *val))
.collect()
.collect())
}
#[cfg(test)]
@ -494,7 +497,8 @@ mod tests {
&bounds_after,
transform,
&HashMap::new(),
);
)
.unwrap();
assert_eq!(result.len(), 1);
}

View File

@ -1,3 +1,4 @@
use crate::error::{Error, Result};
use crate::math::{is_move_only_matrix, Bounds, Matrix};
use crate::shapes::{ConstraintH, ConstraintV};
@ -105,14 +106,14 @@ pub fn propagate_shape_constraints(
constraint_v: ConstraintV,
transform: Matrix,
ignore_constrainst: bool,
) -> Matrix {
) -> Result<Matrix> {
// if the constrains are scale & scale or the transform has only moves we
// can propagate as is
if (ignore_constrainst
|| constraint_h == ConstraintH::Scale && constraint_v == ConstraintV::Scale)
|| is_move_only_matrix(&transform)
{
return transform;
return Ok(transform);
}
let mut transform = transform;
@ -133,7 +134,9 @@ pub fn propagate_shape_constraints(
parent_transform.post_translate(center);
parent_transform.pre_translate(-center);
let parent_transform_inv = &parent_transform.invert().unwrap();
let parent_transform_inv = &parent_transform.invert().ok_or(Error::CriticalError(
"Failed to invert parent transform".to_string(),
))?;
let origin = parent_transform_inv.map_point(child_bounds_after.nw);
let mut scale = Matrix::scale((scale_width, scale_height));
@ -160,5 +163,5 @@ pub fn propagate_shape_constraints(
transform.post_concat(&Matrix::translate(th + tv));
}
transform
Ok(transform)
}

View File

@ -1,4 +1,6 @@
#![allow(dead_code)]
use crate::error::{Error, Result};
use crate::math::{self as math, Bounds, Matrix, Point, Vector, VectorExt};
use crate::shapes::{
AlignContent, AlignItems, AlignSelf, FlexData, JustifyContent, LayoutData, LayoutItem,
@ -588,7 +590,7 @@ pub fn reflow_flex_layout(
flex_data: &FlexData,
shapes: ShapesPoolRef,
bounds: &mut HashMap<Uuid, Bounds>,
) -> VecDeque<Modifier> {
) -> Result<VecDeque<Modifier>> {
let mut result = VecDeque::new();
let layout_bounds = &bounds.find(shape);
let layout_axis = LayoutAxis::new(shape, layout_bounds, layout_data, flex_data);
@ -724,7 +726,9 @@ pub fn reflow_flex_layout(
let parent_transform = layout_bounds.transform_matrix().unwrap_or_default();
let parent_transform_inv = &parent_transform.invert().unwrap();
let parent_transform_inv = &parent_transform.invert().ok_or(Error::CriticalError(
"Failed to invert parent transform".to_string(),
))?;
let origin = parent_transform_inv.map_point(layout_bounds.nw);
let mut scale = Matrix::scale((scale_width, scale_height));
@ -737,5 +741,5 @@ pub fn reflow_flex_layout(
result.push_back(Modifier::parent(shape.id, scale));
bounds.insert(shape.id, layout_bounds_after);
}
result
Ok(result)
}

View File

@ -1,3 +1,4 @@
use crate::error::{Error, Result};
use crate::math::{self as math, intersect_rays, Bounds, Matrix, Point, Ray, Vector, VectorExt};
use crate::shapes::{
AlignContent, AlignItems, AlignSelf, Frame, GridCell, GridData, GridTrack, GridTrackType,
@ -6,6 +7,7 @@ use crate::shapes::{
};
use crate::state::ShapesPoolRef;
use crate::uuid::Uuid;
use std::collections::{HashMap, HashSet, VecDeque};
use super::common::GetBounds;
@ -704,7 +706,7 @@ pub fn reflow_grid_layout(
grid_data: &GridData,
shapes: ShapesPoolRef,
bounds: &mut HashMap<Uuid, Bounds>,
) -> VecDeque<Modifier> {
) -> Result<VecDeque<Modifier>> {
let mut result = VecDeque::new();
let layout_bounds = bounds.find(shape);
let children: HashSet<Uuid> = shape.children_ids_iter(true).copied().collect();
@ -825,7 +827,9 @@ pub fn reflow_grid_layout(
let parent_transform = layout_bounds.transform_matrix().unwrap_or_default();
let parent_transform_inv = &parent_transform.invert().unwrap();
let parent_transform_inv = &parent_transform.invert().ok_or(Error::CriticalError(
"Failed to invert parent transform".to_string(),
))?;
let origin = parent_transform_inv.map_point(layout_bounds.nw);
let mut scale = Matrix::scale((scale_width, scale_height));
@ -839,5 +843,5 @@ pub fn reflow_grid_layout(
bounds.insert(shape.id, layout_bounds_after);
}
result
Ok(result)
}

View File

@ -6,6 +6,7 @@ mod text_editor;
pub use shapes_pool::{ShapesPool, ShapesPoolMutRef, ShapesPoolRef};
pub use text_editor::*;
use crate::error::{Error, Result};
use crate::render::RenderState;
use crate::shapes::Shape;
use crate::tiles;
@ -28,41 +29,44 @@ pub(crate) struct State {
}
impl State {
pub fn new(width: i32, height: i32) -> Self {
State {
render_state: RenderState::new(width, height),
pub fn try_new(width: i32, height: i32) -> Result<Self> {
Ok(State {
render_state: RenderState::try_new(width, height)?,
text_editor_state: TextEditorState::new(),
current_id: None,
current_browser: 0,
shapes: ShapesPool::new(),
// TODO: Maybe this can be moved to a different object
saved_shapes: None,
}
})
}
// Creates a new temporary shapes pool.
// Will panic if a previous temporary pool exists.
pub fn start_temp_objects(mut self) -> Self {
pub fn start_temp_objects(mut self) -> Result<Self> {
if self.saved_shapes.is_some() {
panic!("Tried to start a temp objects while the previous have not been restored");
return Err(Error::CriticalError(
"Tried to start a temp objects while the previous have not been restored"
.to_string(),
));
}
self.saved_shapes = Some(self.shapes);
self.shapes = ShapesPool::new();
self
Ok(self)
}
// Disposes of the temporary shapes pool restoring the normal pool
// Will panic if a there is no temporary pool.
pub fn end_temp_objects(mut self) -> Self {
self.shapes = self
.saved_shapes
.expect("Tried to end temp objects but not content to be restored is present");
pub fn end_temp_objects(mut self) -> Result<Self> {
self.shapes = self.saved_shapes.ok_or(Error::CriticalError(
"Tried to end temp objects but not content to be restored is present".to_string(),
))?;
self.saved_shapes = None;
self
Ok(self)
}
pub fn resize(&mut self, width: i32, height: i32) {
self.render_state.resize(width, height);
pub fn resize(&mut self, width: i32, height: i32) -> Result<()> {
self.render_state.resize(width, height)
}
pub fn render_state_mut(&mut self) -> &mut RenderState {
@ -87,19 +91,17 @@ impl State {
self.render_state.render_from_cache(&self.shapes);
}
pub fn render_sync(&mut self, timestamp: i32) -> Result<(), String> {
pub fn render_sync(&mut self, timestamp: i32) -> Result<()> {
self.render_state
.start_render_loop(None, &self.shapes, timestamp, true)?;
Ok(())
.start_render_loop(None, &self.shapes, timestamp, true)
}
pub fn render_sync_shape(&mut self, id: &Uuid, timestamp: i32) -> Result<(), String> {
pub fn render_sync_shape(&mut self, id: &Uuid, timestamp: i32) -> Result<()> {
self.render_state
.start_render_loop(Some(id), &self.shapes, timestamp, true)?;
Ok(())
.start_render_loop(Some(id), &self.shapes, timestamp, true)
}
pub fn start_render_loop(&mut self, timestamp: i32) -> Result<(), String> {
pub fn start_render_loop(&mut self, timestamp: i32) -> Result<()> {
// If zoom changed, we MUST rebuild the tile index before using it.
// Otherwise, the index will have tiles from the old zoom level, causing visible
// tiles to appear empty. This can happen if start_render_loop() is called before
@ -111,14 +113,12 @@ impl State {
}
self.render_state
.start_render_loop(None, &self.shapes, timestamp, false)?;
Ok(())
.start_render_loop(None, &self.shapes, timestamp, false)
}
pub fn process_animation_frame(&mut self, timestamp: i32) -> Result<(), String> {
pub fn process_animation_frame(&mut self, timestamp: i32) -> Result<()> {
self.render_state
.process_animation_frame(None, &self.shapes, timestamp)?;
Ok(())
.process_animation_frame(None, &self.shapes, timestamp)
}
pub fn clear_focus_mode(&mut self) {
@ -227,10 +227,10 @@ impl State {
let _ = self.render_state.render_preview(&self.shapes, timestamp);
}
pub fn rebuild_modifier_tiles(&mut self, ids: Vec<Uuid>) {
pub fn rebuild_modifier_tiles(&mut self, ids: Vec<Uuid>) -> Result<()> {
// Index-based storage is safe
self.render_state
.rebuild_modifier_tiles(&mut self.shapes, ids);
.rebuild_modifier_tiles(&mut self.shapes, ids)
}
pub fn font_collection(&self) -> &FontCollection {

View File

@ -3,7 +3,6 @@ use crate::uuid::Uuid;
use crate::view::Viewbox;
use skia_safe as skia;
use std::collections::{HashMap, HashSet};
#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)]
pub struct Tile(pub i32, pub i32);
@ -178,13 +177,10 @@ impl TileHashMap {
}
pub fn add_shape_at(&mut self, tile: Tile, shape_id: Uuid) {
self.grid.entry(tile).or_default();
self.index.entry(shape_id).or_default();
let tile_set = self.grid.get_mut(&tile).unwrap();
let tile_set = self.grid.entry(tile).or_default();
tile_set.insert(shape_id);
let index_set = self.index.get_mut(&shape_id).unwrap();
let index_set = self.index.entry(shape_id).or_default();
index_set.insert(tile);
}

View File

@ -1,11 +1,10 @@
use crate::error::{Error, Result};
use crate::mem;
use macros::wasm_error;
// use crate::mem::SerializableResult;
use crate::error::Error;
use crate::uuid::Uuid;
use crate::with_state_mut;
use crate::STATE;
use crate::{shapes::ImageFill, utils::uuid_from_u32_quartet};
use macros::wasm_error;
const FLAG_KEEP_ASPECT_RATIO: u8 = 1 << 0;
const IMAGE_IDS_SIZE: usize = 32;
@ -50,6 +49,7 @@ pub struct ShapeImageIds {
impl From<[u8; IMAGE_IDS_SIZE]> for ShapeImageIds {
fn from(bytes: [u8; IMAGE_IDS_SIZE]) -> Self {
// FIXME: this should probably be a try_from instead
let shape_id = Uuid::try_from(&bytes[0..16]).unwrap();
let image_id = Uuid::try_from(&bytes[16..32]).unwrap();
ShapeImageIds { shape_id, image_id }
@ -57,9 +57,9 @@ impl From<[u8; IMAGE_IDS_SIZE]> for ShapeImageIds {
}
impl TryFrom<Vec<u8>> for ShapeImageIds {
type Error = &'static str;
type Error = Error;
fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
fn try_from(value: Vec<u8>) -> Result<Self> {
let mut arr = [0u8; IMAGE_IDS_SIZE];
arr.copy_from_slice(&value);
Ok(ShapeImageIds::from(arr))
@ -68,13 +68,16 @@ impl TryFrom<Vec<u8>> for ShapeImageIds {
#[no_mangle]
#[wasm_error]
pub extern "C" fn store_image() -> crate::error::Result<()> {
pub extern "C" fn store_image() -> Result<()> {
let bytes = mem::bytes();
let ids = ShapeImageIds::try_from(bytes[0..IMAGE_IDS_SIZE].to_vec()).unwrap();
let ids = ShapeImageIds::try_from(bytes[0..IMAGE_IDS_SIZE].to_vec())?;
// Read is_thumbnail flag (4 bytes as u32)
let is_thumbnail_bytes = &bytes[IMAGE_IDS_SIZE..IMAGE_HEADER_SIZE];
let is_thumbnail_value = u32::from_le_bytes(is_thumbnail_bytes.try_into().unwrap());
let is_thumbnail_value =
u32::from_le_bytes(is_thumbnail_bytes.try_into().map_err(|_| {
Error::CriticalError("Invalid bytes for is_thumbnail flag".to_string())
})?);
let is_thumbnail = is_thumbnail_value != 0;
let image_bytes = &bytes[IMAGE_HEADER_SIZE..];
@ -104,9 +107,10 @@ pub extern "C" fn store_image() -> crate::error::Result<()> {
/// - bytes 44-47: height (i32)
#[no_mangle]
#[wasm_error]
pub extern "C" fn store_image_from_texture() -> crate::error::Result<()> {
pub extern "C" fn store_image_from_texture() -> Result<()> {
let bytes = mem::bytes();
// FIXME: where does this 48 come from?
if bytes.len() < 48 {
// FIXME: Review if this should be an critical or a recoverable error.
eprintln!("store_image_from_texture: insufficient data");
@ -116,23 +120,41 @@ pub extern "C" fn store_image_from_texture() -> crate::error::Result<()> {
));
}
let ids = ShapeImageIds::try_from(bytes[0..IMAGE_IDS_SIZE].to_vec()).unwrap();
let ids = ShapeImageIds::try_from(bytes[0..IMAGE_IDS_SIZE].to_vec())
.map_err(|_| Error::CriticalError("Invalid image ids".to_string()))?;
// FIXME: read bytes in a safe way
// Read is_thumbnail flag (4 bytes as u32)
let is_thumbnail_bytes = &bytes[IMAGE_IDS_SIZE..IMAGE_HEADER_SIZE];
let is_thumbnail_value = u32::from_le_bytes(is_thumbnail_bytes.try_into().unwrap());
let is_thumbnail_value =
u32::from_le_bytes(is_thumbnail_bytes.try_into().map_err(|_| {
Error::CriticalError("Invalid bytes for is_thumbnail flag".to_string())
})?);
let is_thumbnail = is_thumbnail_value != 0;
// Read GL texture ID (4 bytes as u32)
let texture_id_bytes = &bytes[36..40];
let texture_id = u32::from_le_bytes(texture_id_bytes.try_into().unwrap());
let texture_id = u32::from_le_bytes(
texture_id_bytes
.try_into()
.map_err(|_| Error::CriticalError("Invalid bytes for texture id".to_string()))?,
);
// Read width and height (8 bytes as two i32s)
let width_bytes = &bytes[40..44];
let width = i32::from_le_bytes(width_bytes.try_into().unwrap());
let width = i32::from_le_bytes(
width_bytes
.try_into()
.map_err(|_| Error::CriticalError("Invalid bytes for width".to_string()))?,
);
let height_bytes = &bytes[44..48];
let height = i32::from_le_bytes(height_bytes.try_into().unwrap());
let height = i32::from_le_bytes(
height_bytes
.try_into()
.map_err(|_| Error::CriticalError("Invalid bytes for height".to_string()))?,
);
with_state_mut!(state, {
if let Err(msg) = state.render_state_mut().add_image_from_gl_texture(
@ -142,6 +164,7 @@ pub extern "C" fn store_image_from_texture() -> crate::error::Result<()> {
width,
height,
) {
// FIXME: Review if we should return a RecoverableError
eprintln!("store_image_from_texture error: {}", msg);
}
state.touch_shape(ids.shape_id);

View File

@ -8,7 +8,7 @@ use crate::{uuid_from_u32_quartet, with_current_shape_mut, with_state, with_stat
use super::align;
#[allow(unused_imports)]
use crate::error::Result;
use crate::error::{Error, Result};
#[derive(Debug)]
#[repr(C, align(1))]
@ -177,9 +177,13 @@ pub extern "C" fn set_grid_columns() -> Result<()> {
let entries: Vec<GridTrack> = bytes
.chunks(size_of::<RawGridTrack>())
.map(|data| data.try_into().unwrap())
.map(|data: [u8; size_of::<RawGridTrack>()]| RawGridTrack::from(data).into())
.collect();
.map(|data| {
let track_bytes: [u8; size_of::<RawGridTrack>()] = data
.try_into()
.map_err(|_| Error::CriticalError("Invalid bytes for grid track".to_string()))?;
Ok(RawGridTrack::from(track_bytes).into())
})
.collect::<Result<Vec<_>>>()?;
with_current_shape_mut!(state, |shape: &mut Shape| {
shape.set_grid_columns(entries);
@ -196,9 +200,13 @@ pub extern "C" fn set_grid_rows() -> Result<()> {
let entries: Vec<GridTrack> = bytes
.chunks(size_of::<RawGridTrack>())
.map(|data| data.try_into().unwrap())
.map(|data: [u8; size_of::<RawGridTrack>()]| RawGridTrack::from(data).into())
.collect();
.map(|data| {
let track_bytes: [u8; size_of::<RawGridTrack>()] = data
.try_into()
.map_err(|_| Error::CriticalError("Invalid bytes for grid track".to_string()))?;
Ok(RawGridTrack::from(track_bytes).into())
})
.collect::<Result<Vec<_>>>()?;
with_current_shape_mut!(state, |shape: &mut Shape| {
shape.set_grid_rows(entries);
@ -215,9 +223,13 @@ pub extern "C" fn set_grid_cells() -> Result<()> {
let cells: Vec<RawGridCell> = bytes
.chunks(size_of::<RawGridCell>())
.map(|data| data.try_into().expect("Invalid grid cell data"))
.map(|data: [u8; size_of::<RawGridCell>()]| RawGridCell::from(data))
.collect();
.map(|data| {
let cell_bytes: [u8; size_of::<RawGridCell>()] = data
.try_into()
.map_err(|_| Error::CriticalError("Invalid bytes for grid cell".to_string()))?;
Ok(RawGridCell::from(cell_bytes))
})
.collect::<Result<Vec<_>>>()?;
with_current_shape_mut!(state, |shape: &mut Shape| {
shape.set_grid_cells(cells.into_iter().map(|raw| raw.into()).collect());

View File

@ -4,6 +4,7 @@ use mem::SerializableResult;
use std::mem::size_of;
use std::sync::{Mutex, OnceLock};
use crate::error::{Error, Result};
use crate::shapes::{Path, Segment, ToPath};
use crate::{mem, with_current_shape, with_current_shape_mut, STATE};
@ -41,12 +42,12 @@ impl From<[u8; size_of::<RawSegmentData>()]> for RawSegmentData {
}
impl TryFrom<&[u8]> for RawSegmentData {
type Error = String;
fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
type Error = Error;
fn try_from(bytes: &[u8]) -> Result<Self> {
let data: [u8; RAW_SEGMENT_DATA_SIZE] = bytes
.get(0..RAW_SEGMENT_DATA_SIZE)
.and_then(|slice| slice.try_into().ok())
.ok_or("Invalid path data".to_string())?;
.ok_or(Error::CriticalError("Invalid path data".to_string()))?;
Ok(RawSegmentData::from(data))
}
}
@ -154,10 +155,14 @@ fn get_path_upload_buffer() -> &'static Mutex<Vec<u8>> {
}
#[no_mangle]
pub extern "C" fn start_shape_path_buffer() {
#[wasm_error]
pub extern "C" fn start_shape_path_buffer() -> Result<()> {
let buffer = get_path_upload_buffer();
let mut buffer = buffer.lock().unwrap();
let mut buffer = buffer
.lock()
.map_err(|_| Error::CriticalError("Failed to lock path buffer".to_string()))?;
buffer.clear();
Ok(())
}
#[no_mangle]
@ -165,32 +170,40 @@ pub extern "C" fn start_shape_path_buffer() {
pub extern "C" fn set_shape_path_chunk_buffer() -> Result<()> {
let bytes = mem::bytes();
let buffer = get_path_upload_buffer();
let mut buffer = buffer.lock().unwrap();
let mut buffer = buffer
.lock()
.map_err(|_| Error::CriticalError("Failed to lock path buffer".to_string()))?;
buffer.extend_from_slice(&bytes);
mem::free_bytes()?;
Ok(())
}
#[no_mangle]
pub extern "C" fn set_shape_path_buffer() {
#[wasm_error]
pub extern "C" fn set_shape_path_buffer() -> Result<()> {
let buffer = get_path_upload_buffer();
let mut buffer = buffer
.lock()
.map_err(|_| Error::CriticalError("Failed to lock path buffer".to_string()))?;
let chunk_size = size_of::<RawSegmentData>();
if !buffer.len().is_multiple_of(chunk_size) {
// FIXME
println!("Warning: buffer length is not a multiple of chunk size!");
}
let mut segments = Vec::new();
for (i, chunk) in buffer.chunks(chunk_size).enumerate() {
match RawSegmentData::try_from(chunk) {
Ok(seg) => segments.push(Segment::from(seg)),
Err(e) => println!("Error at segment {}: {}", i, e),
}
}
with_current_shape_mut!(state, |shape: &mut Shape| {
let buffer = get_path_upload_buffer();
let mut buffer = buffer.lock().unwrap();
let chunk_size = size_of::<RawSegmentData>();
if !buffer.len().is_multiple_of(chunk_size) {
// FIXME
println!("Warning: buffer length is not a multiple of chunk size!");
}
let mut segments = Vec::new();
for (i, chunk) in buffer.chunks(chunk_size).enumerate() {
match RawSegmentData::try_from(chunk) {
Ok(seg) => segments.push(Segment::from(seg)),
Err(e) => println!("Error at segment {}: {}", i, e),
}
}
shape.set_path_segments(segments);
buffer.clear();
});
buffer.clear();
Ok(())
}
#[no_mangle]

View File

@ -6,6 +6,10 @@ use crate::wasm::blend::RawBlendMode;
use crate::wasm::layouts::constraints::{RawConstraintH, RawConstraintV};
use crate::{with_state_mut, STATE};
#[allow(unused_imports)]
use crate::error::{Error, Result};
use macros::wasm_error;
use super::RawShapeType;
const FLAG_CLIP_CONTENT: u8 = 0b0000_0001;
@ -106,14 +110,18 @@ impl From<[u8; RAW_BASE_PROPS_SIZE]> for RawBasePropsData {
}
#[no_mangle]
pub extern "C" fn set_shape_base_props() {
#[wasm_error]
pub extern "C" fn set_shape_base_props() -> Result<()> {
let bytes = mem::bytes();
if bytes.len() < RAW_BASE_PROPS_SIZE {
return;
return Ok(());
}
let data: [u8; RAW_BASE_PROPS_SIZE] = bytes[..RAW_BASE_PROPS_SIZE].try_into().unwrap();
// FIXME: this should just be a try_from
let data: [u8; RAW_BASE_PROPS_SIZE] = bytes[..RAW_BASE_PROPS_SIZE]
.try_into()
.map_err(|_| Error::CriticalError("Invalid bytes for base props".to_string()))?;
let raw = RawBasePropsData::from(data);
let id = raw.id();
@ -151,6 +159,7 @@ pub extern "C" fn set_shape_base_props() {
shape.set_corners((raw.corner_r1, raw.corner_r2, raw.corner_r3, raw.corner_r4));
}
});
Ok(())
}
#[cfg(test)]

View File

@ -292,9 +292,10 @@ pub extern "C" fn clear_shape_text() {
#[wasm_error]
pub extern "C" fn set_shape_text_content() -> crate::error::Result<()> {
let bytes = mem::bytes();
with_current_shape_mut!(state, |shape: &mut Shape| {
let raw_text_data = RawParagraph::try_from(&bytes).unwrap();
let raw_text_data = RawParagraph::try_from(&bytes)
.map_err(|_| Error::CriticalError("Invalid text data".to_string()))?;
with_current_shape_mut!(state, |shape: &mut Shape| {
shape.add_paragraph(raw_text_data.into()).map_err(|_| {
Error::RecoverableError(format!(
"Error with set_shape_text_content on {:?}",

View File

@ -1,4 +1,6 @@
use macros::ToJs;
#[allow(unused_imports)]
use crate::error::{Error, Result};
use macros::{wasm_error, ToJs};
use skia_safe as skia;
@ -39,11 +41,11 @@ impl From<[u8; RAW_TRANSFORM_ENTRY_SIZE]> for RawTransformEntry {
}
impl TryFrom<&[u8]> for RawTransformEntry {
type Error = String;
fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
type Error = Error;
fn try_from(bytes: &[u8]) -> Result<Self> {
let bytes: [u8; RAW_TRANSFORM_ENTRY_SIZE] = bytes
.try_into()
.map_err(|_| "Invalid transform entry bytes".to_string())?;
.map_err(|_| Error::CriticalError("Invalid transform entry bytes".to_string()))?;
Ok(RawTransformEntry::from(bytes))
}
}
@ -73,16 +75,17 @@ impl From<RawTransformEntry> for TransformEntry {
}
#[no_mangle]
pub extern "C" fn propagate_modifiers(pixel_precision: bool) -> *mut u8 {
#[wasm_error]
pub extern "C" fn propagate_modifiers(pixel_precision: bool) -> Result<*mut u8> {
let bytes = mem::bytes();
let entries: Vec<TransformEntry> = bytes
.chunks(RAW_TRANSFORM_ENTRY_SIZE)
.map(|data| RawTransformEntry::try_from(data).unwrap().into())
.collect();
.map(|data| RawTransformEntry::try_from(data).map(|entry| entry.into()))
.collect::<Result<Vec<_>>>()?;
with_state!(state, {
let result = shapes::propagate_modifiers(state, &entries, pixel_precision);
mem::write_vec(result)
let result = shapes::propagate_modifiers(state, &entries, pixel_precision)?;
Ok(mem::write_vec(result))
})
}