From e630be1509424c6ac62676d34a969550f09838d1 Mon Sep 17 00:00:00 2001 From: Elena Torro Date: Tue, 17 Mar 2026 09:16:46 +0100 Subject: [PATCH] :tada: Add background blur for wasm render --- render-wasm/macros/src/lib.rs | 2 + render-wasm/src/main.rs | 76 +++---- render-wasm/src/render.rs | 203 ++++++++++-------- render-wasm/src/render/debug.rs | 12 +- render-wasm/src/render/fills.rs | 46 ++-- render-wasm/src/render/filters.rs | 21 +- render-wasm/src/render/fonts.rs | 17 +- render-wasm/src/render/gpu_state.rs | 45 ++-- render-wasm/src/render/images.rs | 20 +- render-wasm/src/render/shadows.rs | 15 +- render-wasm/src/render/strokes.rs | 84 ++++---- render-wasm/src/render/surfaces.rs | 96 ++++++--- render-wasm/src/render/text.rs | 17 +- render-wasm/src/shapes/modifiers.rs | 42 ++-- .../src/shapes/modifiers/constraints.rs | 11 +- .../src/shapes/modifiers/flex_layout.rs | 10 +- .../src/shapes/modifiers/grid_layout.rs | 10 +- render-wasm/src/state.rs | 56 ++--- render-wasm/src/tiles.rs | 8 +- render-wasm/src/wasm/fills/image.rs | 51 +++-- render-wasm/src/wasm/layouts/grid.rs | 32 ++- render-wasm/src/wasm/paths.rs | 57 +++-- render-wasm/src/wasm/shapes/base_props.rs | 15 +- render-wasm/src/wasm/text.rs | 5 +- render-wasm/src/wasm/transforms.rs | 21 +- 25 files changed, 584 insertions(+), 388 deletions(-) diff --git a/render-wasm/macros/src/lib.rs b/render-wasm/macros/src/lib.rs index 2d3536d3d1..876af71254 100644 --- a/render-wasm/macros/src/lib.rs +++ b/render-wasm/macros/src/lib.rs @@ -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); } } diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index 6e519cc249..017540a11b 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -30,6 +30,9 @@ use uuid::Uuid; pub(crate) static mut STATE: Option> = 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::() { - 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 = bytes .chunks(size_of::<::BytesType>()) - .map(|data| Uuid::try_from(data).unwrap()) - .collect(); + .map(|data| Uuid::try_from(data).map_err(|e| Error::RecoverableError(e.to_string()))) + .collect::>>()?; with_state_mut!(state, { state.set_focus_mode(entries); @@ -637,8 +628,8 @@ pub extern "C" fn set_children() -> Result<()> { let entries: Vec = bytes .chunks(size_of::<::BytesType>()) - .map(|data| Uuid::try_from(data).unwrap()) - .collect(); + .map(|data| Uuid::try_from(data).map_err(|e| Error::CriticalError(e.to_string()))) + .collect::>>()?; 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 = 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::>>()?; 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 = bytes .chunks(size_of::<::BytesType>()) - .map(|data| TransformEntry::try_from(data).unwrap()) - .collect(); + .map(|data| TransformEntry::try_from(data).map_err(|e| Error::CriticalError(e.to_string()))) + .collect::>>()?; let mut modifiers = HashMap::new(); let mut ids = Vec::::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(()) diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 68748effe0..bb0427ac78 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -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 { // 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(&mut self, f: F) -> R + fn with_nested_blurs_suppressed(&mut self, f: F) -> Result where - F: FnOnce(&mut RenderState) -> R, + F: FnOnce(&mut RenderState) -> Result, { 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>, outset: Option, - ) { + ) -> 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, - ) { + ) -> 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 { + 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 { + 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, - ) { + ) -> Result<()> { let mut transformed_shadow: Cow = 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::::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) { + pub fn rebuild_modifier_tiles( + &mut self, + tree: ShapesPoolMutRef<'_>, + ids: Vec, + ) -> 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 { diff --git a/render-wasm/src/render/debug.rs b/render-wasm/src/render/debug.rs index 41f68a663e..27f8da07a8 100644 --- a/render-wasm/src/render/debug.rs +++ b/render-wasm/src/render/debug.rs @@ -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;')")) diff --git a/render-wasm/src/render/fills.rs b/render-wasm/src/render/fills.rs index 6e098c9752..2a1968a93c 100644 --- a/render-wasm/src/render/fills.rs +++ b/render-wasm/src/render/fills.rs @@ -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, -) { +) -> 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, inset: Option, -) { +) -> 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)] diff --git a/render-wasm/src/render/filters.rs b/render-wasm/src/render/filters.rs index 149c598e94..34b33f403d 100644 --- a/render-wasm/src/render/filters.rs +++ b/render-wasm/src/render/filters.rs @@ -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( bounds: Rect, target_surface: SurfaceId, draw_fn: F, -) -> bool +) -> Result 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( bounds: Rect, extra_downscale: f32, draw_fn: F, -) -> Option<(skia::Surface, f32)> +) -> Result> 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))) } diff --git a/render-wasm/src/render/fonts.rs b/render-wasm/src/render/fonts.rs index 917fe134a8..d528d7b691 100644 --- a/render-wasm/src/render/fonts.rs +++ b/render-wasm/src/render/fonts.rs @@ -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 { 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 { diff --git a/render-wasm/src/render/gpu_state.rs b/render-wasm/src/render/gpu_state.rs index d4d90faaec..910c5beda2 100644 --- a/render-wasm/src/render/gpu_state.rs +++ b/render-wasm/src/render/gpu_state.rs @@ -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 { + 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 { 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 { 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 { 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) } } diff --git a/render-wasm/src/render/images.rs b/render-wasm/src/render/images.rs index 4faf393895..51bf9dbbe0 100644 --- a/render-wasm/src/render/images.rs +++ b/render-wasm/src/render/images.rs @@ -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 { +) -> Result { 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 diff --git a/render-wasm/src/render/shadows.rs b/render-wasm/src/render/shadows.rs index ea43322b70..d392305327 100644 --- a/render-wasm/src/render/shadows.rs +++ b/render-wasm/src/render/shadows.rs @@ -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, shadows: &[Paint], blur_filter: &Option, -) { +) -> 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(()) } diff --git a/render-wasm/src/render/strokes.rs b/render-wasm/src/render/strokes.rs index 319b921ac5..c5a3a26bf1 100644 --- a/render-wasm/src/render/strokes.rs +++ b/render-wasm/src/render/strokes.rs @@ -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, antialias: bool, outset: Option, -) { +) -> 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, -) { +) -> 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, -) { +) -> 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, -) { +) -> 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) diff --git a/render-wasm/src/render/surfaces.rs b/render-wasm/src/render/surfaces.rs index 536737daba..857ccfda6d 100644 --- a/render-wasm/src/render/surfaces.rs +++ b/render-wasm/src/render/surfaces.rs @@ -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 { 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 { 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 { + pub fn base64_snapshot_rect( + &mut self, + id: SurfaceId, + irect: skia::IRect, + ) -> Result> { 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( diff --git a/render-wasm/src/render/text.rs b/render-wasm/src/render/text.rs index 832503505d..85a150284b 100644 --- a/render-wasm/src/render/text.rs +++ b/render-wasm/src/render/text.rs @@ -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 = (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, layer_opacity: Option, -) { +) -> 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, layer_opacity: Option, -) { +) -> 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( diff --git a/render-wasm/src/shapes/modifiers.rs b/render-wasm/src/shapes/modifiers.rs index 9713c066a9..4c7f6d69d4 100644 --- a/render-wasm/src/shapes/modifiers.rs +++ b/render-wasm/src/shapes/modifiers.rs @@ -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, -) -> VecDeque { +) -> Result> { 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, bounds: &mut HashMap, modifiers: &mut HashMap, -) { +) -> 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, entries: &mut VecDeque, bounds: &mut HashMap, -) { +) -> 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 { +) -> Result> { 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); } diff --git a/render-wasm/src/shapes/modifiers/constraints.rs b/render-wasm/src/shapes/modifiers/constraints.rs index 190fd32734..4f5a9cb228 100644 --- a/render-wasm/src/shapes/modifiers/constraints.rs +++ b/render-wasm/src/shapes/modifiers/constraints.rs @@ -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 { // 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) } diff --git a/render-wasm/src/shapes/modifiers/flex_layout.rs b/render-wasm/src/shapes/modifiers/flex_layout.rs index 72191a32a2..7661d93088 100644 --- a/render-wasm/src/shapes/modifiers/flex_layout.rs +++ b/render-wasm/src/shapes/modifiers/flex_layout.rs @@ -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, -) -> VecDeque { +) -> Result> { 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) } diff --git a/render-wasm/src/shapes/modifiers/grid_layout.rs b/render-wasm/src/shapes/modifiers/grid_layout.rs index 7b2a314989..7ef2cb447b 100644 --- a/render-wasm/src/shapes/modifiers/grid_layout.rs +++ b/render-wasm/src/shapes/modifiers/grid_layout.rs @@ -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, -) -> VecDeque { +) -> Result> { let mut result = VecDeque::new(); let layout_bounds = bounds.find(shape); let children: HashSet = 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) } diff --git a/render-wasm/src/state.rs b/render-wasm/src/state.rs index b99b768334..3509b529aa 100644 --- a/render-wasm/src/state.rs +++ b/render-wasm/src/state.rs @@ -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 { + 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 { 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.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) { + pub fn rebuild_modifier_tiles(&mut self, ids: Vec) -> 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 { diff --git a/render-wasm/src/tiles.rs b/render-wasm/src/tiles.rs index 02bd5c5eb5..13ed4c1aeb 100644 --- a/render-wasm/src/tiles.rs +++ b/render-wasm/src/tiles.rs @@ -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); } diff --git a/render-wasm/src/wasm/fills/image.rs b/render-wasm/src/wasm/fills/image.rs index f0e5b36526..b122de4cc8 100644 --- a/render-wasm/src/wasm/fills/image.rs +++ b/render-wasm/src/wasm/fills/image.rs @@ -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> for ShapeImageIds { - type Error = &'static str; + type Error = Error; - fn try_from(value: Vec) -> Result { + fn try_from(value: Vec) -> Result { let mut arr = [0u8; IMAGE_IDS_SIZE]; arr.copy_from_slice(&value); Ok(ShapeImageIds::from(arr)) @@ -68,13 +68,16 @@ impl TryFrom> 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); diff --git a/render-wasm/src/wasm/layouts/grid.rs b/render-wasm/src/wasm/layouts/grid.rs index d1a0476814..aac2fd1928 100644 --- a/render-wasm/src/wasm/layouts/grid.rs +++ b/render-wasm/src/wasm/layouts/grid.rs @@ -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 = bytes .chunks(size_of::()) - .map(|data| data.try_into().unwrap()) - .map(|data: [u8; size_of::()]| RawGridTrack::from(data).into()) - .collect(); + .map(|data| { + let track_bytes: [u8; size_of::()] = data + .try_into() + .map_err(|_| Error::CriticalError("Invalid bytes for grid track".to_string()))?; + Ok(RawGridTrack::from(track_bytes).into()) + }) + .collect::>>()?; 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 = bytes .chunks(size_of::()) - .map(|data| data.try_into().unwrap()) - .map(|data: [u8; size_of::()]| RawGridTrack::from(data).into()) - .collect(); + .map(|data| { + let track_bytes: [u8; size_of::()] = data + .try_into() + .map_err(|_| Error::CriticalError("Invalid bytes for grid track".to_string()))?; + Ok(RawGridTrack::from(track_bytes).into()) + }) + .collect::>>()?; 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 = bytes .chunks(size_of::()) - .map(|data| data.try_into().expect("Invalid grid cell data")) - .map(|data: [u8; size_of::()]| RawGridCell::from(data)) - .collect(); + .map(|data| { + let cell_bytes: [u8; size_of::()] = data + .try_into() + .map_err(|_| Error::CriticalError("Invalid bytes for grid cell".to_string()))?; + Ok(RawGridCell::from(cell_bytes)) + }) + .collect::>>()?; with_current_shape_mut!(state, |shape: &mut Shape| { shape.set_grid_cells(cells.into_iter().map(|raw| raw.into()).collect()); diff --git a/render-wasm/src/wasm/paths.rs b/render-wasm/src/wasm/paths.rs index 0748111533..f700317633 100644 --- a/render-wasm/src/wasm/paths.rs +++ b/render-wasm/src/wasm/paths.rs @@ -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::()]> for RawSegmentData { } impl TryFrom<&[u8]> for RawSegmentData { - type Error = String; - fn try_from(bytes: &[u8]) -> Result { + type Error = Error; + fn try_from(bytes: &[u8]) -> Result { 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> { } #[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::(); + 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::(); - 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] diff --git a/render-wasm/src/wasm/shapes/base_props.rs b/render-wasm/src/wasm/shapes/base_props.rs index 265e4f7841..5e0146f276 100644 --- a/render-wasm/src/wasm/shapes/base_props.rs +++ b/render-wasm/src/wasm/shapes/base_props.rs @@ -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)] diff --git a/render-wasm/src/wasm/text.rs b/render-wasm/src/wasm/text.rs index 3635e0949f..f34aa10cf2 100644 --- a/render-wasm/src/wasm/text.rs +++ b/render-wasm/src/wasm/text.rs @@ -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 {:?}", diff --git a/render-wasm/src/wasm/transforms.rs b/render-wasm/src/wasm/transforms.rs index 88b888d1ef..b0e0a2d84d 100644 --- a/render-wasm/src/wasm/transforms.rs +++ b/render-wasm/src/wasm/transforms.rs @@ -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 { + type Error = Error; + fn try_from(bytes: &[u8]) -> Result { 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 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 = 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::>>()?; 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)) }) }