🎉 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 _: &dyn std::error::Error = &__e;
let __msg = __e.to_string(); let __msg = __e.to_string();
crate::mem::set_error_code(__e.into()); crate::mem::set_error_code(__e.into());
crate::mem::free_bytes().expect("Failed to free bytes");
panic!("WASM error: {}",__msg); panic!("WASM error: {}",__msg);
} }
}, },
Err(__payload) => { Err(__payload) => {
crate::mem::set_error_code(0x02); // critical, same as Error::Critical 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); std::panic::resume_unwind(__payload);
} }
} }

View File

@ -30,6 +30,9 @@ use uuid::Uuid;
pub(crate) static mut STATE: Option<Box<State>> = None; 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_export]
macro_rules! with_state_mut { macro_rules! with_state_mut {
($state:ident, $block:block) => {{ ($state:ident, $block:block) => {{
@ -102,7 +105,7 @@ macro_rules! with_state_mut_current_shape {
#[no_mangle] #[no_mangle]
#[wasm_error] #[wasm_error]
pub extern "C" fn init(width: i32, height: i32) -> Result<()> { 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 { unsafe {
STATE = Some(state_box); STATE = Some(state_box);
} }
@ -138,7 +141,7 @@ pub extern "C" fn set_render_options(debug: u32, dpr: f32) -> Result<()> {
with_state_mut!(state, { with_state_mut!(state, {
let render_state = state.render_state_mut(); let render_state = state.render_state_mut();
render_state.set_debug_flags(debug); render_state.set_debug_flags(debug);
render_state.set_dpr(dpr); render_state.set_dpr(dpr)?;
}); });
Ok(()) Ok(())
} }
@ -162,7 +165,7 @@ pub extern "C" fn render(_: i32) -> Result<()> {
state.rebuild_touched_tiles(); state.rebuild_touched_tiles();
state state
.start_render_loop(performance::get_time()) .start_render_loop(performance::get_time())
.expect("Error rendering"); .map_err(|_| Error::RecoverableError("Error rendering".to_string()))?;
}); });
Ok(()) Ok(())
} }
@ -174,7 +177,7 @@ pub extern "C" fn render_sync() -> Result<()> {
state.rebuild_tiles(); state.rebuild_tiles();
state state
.render_sync(performance::get_time()) .render_sync(performance::get_time())
.expect("Error rendering"); .map_err(|_| Error::RecoverableError("Error rendering".to_string()))?;
}); });
Ok(()) Ok(())
} }
@ -236,24 +239,12 @@ pub extern "C" fn render_preview() -> Result<()> {
#[no_mangle] #[no_mangle]
#[wasm_error] #[wasm_error]
pub extern "C" fn process_animation_frame(timestamp: i32) -> Result<()> { pub extern "C" fn process_animation_frame(timestamp: i32) -> Result<()> {
let result = std::panic::catch_unwind(|| { let result = with_state_mut!(state, { state.process_animation_frame(timestamp) });
with_state_mut!(state, {
state
.process_animation_frame(timestamp)
.expect("Error processing animation frame");
});
});
match result { if let Err(err) = result {
Ok(_) => {} eprintln!("process_animation_frame error: {}", err);
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);
}
} }
Ok(()) Ok(())
} }
@ -270,7 +261,7 @@ pub extern "C" fn reset_canvas() -> Result<()> {
#[wasm_error] #[wasm_error]
pub extern "C" fn resize_viewbox(width: i32, height: i32) -> Result<()> { pub extern "C" fn resize_viewbox(width: i32, height: i32) -> Result<()> {
with_state_mut!(state, { with_state_mut!(state, {
state.resize(width, height); state.resize(width, height)?;
}); });
Ok(()) Ok(())
} }
@ -362,8 +353,8 @@ pub extern "C" fn set_focus_mode() -> Result<()> {
let entries: Vec<Uuid> = bytes let entries: Vec<Uuid> = bytes
.chunks(size_of::<<Uuid as SerializableResult>::BytesType>()) .chunks(size_of::<<Uuid as SerializableResult>::BytesType>())
.map(|data| Uuid::try_from(data).unwrap()) .map(|data| Uuid::try_from(data).map_err(|e| Error::RecoverableError(e.to_string())))
.collect(); .collect::<Result<Vec<Uuid>>>()?;
with_state_mut!(state, { with_state_mut!(state, {
state.set_focus_mode(entries); state.set_focus_mode(entries);
@ -637,8 +628,8 @@ pub extern "C" fn set_children() -> Result<()> {
let entries: Vec<Uuid> = bytes let entries: Vec<Uuid> = bytes
.chunks(size_of::<<Uuid as SerializableResult>::BytesType>()) .chunks(size_of::<<Uuid as SerializableResult>::BytesType>())
.map(|data| Uuid::try_from(data).unwrap()) .map(|data| Uuid::try_from(data).map_err(|e| Error::CriticalError(e.to_string())))
.collect(); .collect::<Result<Vec<Uuid>>>()?;
set_children_set(entries)?; 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<()> { pub extern "C" fn set_structure_modifiers() -> Result<()> {
let bytes = mem::bytes(); let bytes = mem::bytes();
let entries: Vec<_> = bytes let entries: Vec<StructureEntry> = bytes
.chunks(44) .chunks(44)
.map(|data| StructureEntry::from_bytes(data.try_into().unwrap())) .map(|chunk| {
.collect(); 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, { with_state_mut!(state, {
let mut structure = HashMap::new(); 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.entry(entry.parent).or_insert_with(Vec::new);
structure structure
.get_mut(&entry.parent) .get_mut(&entry.parent)
.expect("Parent not found for entry") .ok_or(Error::CriticalError(
"Parent not found for entry".to_string(),
))?
.push(entry); .push(entry);
} }
} }
@ -814,10 +812,10 @@ pub extern "C" fn clean_modifiers() -> Result<()> {
pub extern "C" fn set_modifiers() -> Result<()> { pub extern "C" fn set_modifiers() -> Result<()> {
let bytes = mem::bytes(); let bytes = mem::bytes();
let entries: Vec<_> = bytes let entries: Vec<TransformEntry> = bytes
.chunks(size_of::<<TransformEntry as SerializableResult>::BytesType>()) .chunks(size_of::<<TransformEntry as SerializableResult>::BytesType>())
.map(|data| TransformEntry::try_from(data).unwrap()) .map(|data| TransformEntry::try_from(data).map_err(|e| Error::CriticalError(e.to_string())))
.collect(); .collect::<Result<Vec<_>>>()?;
let mut modifiers = HashMap::new(); let mut modifiers = HashMap::new();
let mut ids = Vec::<Uuid>::new(); let mut ids = Vec::<Uuid>::new();
@ -828,7 +826,7 @@ pub extern "C" fn set_modifiers() -> Result<()> {
with_state_mut!(state, { with_state_mut!(state, {
state.set_modifiers(modifiers); state.set_modifiers(modifiers);
state.rebuild_modifier_tiles(ids); state.rebuild_modifier_tiles(ids)?;
}); });
Ok(()) Ok(())
} }
@ -838,8 +836,10 @@ pub extern "C" fn set_modifiers() -> Result<()> {
pub extern "C" fn start_temp_objects() -> Result<()> { pub extern "C" fn start_temp_objects() -> Result<()> {
unsafe { unsafe {
#[allow(static_mut_refs)] #[allow(static_mut_refs)]
let mut state = STATE.take().expect("Got an invalid state pointer"); let mut state = STATE.take().ok_or(Error::CriticalError(
state = Box::new(state.start_temp_objects()); "Got an invalid state pointer".to_string(),
))?;
state = Box::new(state.start_temp_objects()?);
STATE = Some(state); STATE = Some(state);
} }
Ok(()) Ok(())
@ -850,8 +850,10 @@ pub extern "C" fn start_temp_objects() -> Result<()> {
pub extern "C" fn end_temp_objects() -> Result<()> { pub extern "C" fn end_temp_objects() -> Result<()> {
unsafe { unsafe {
#[allow(static_mut_refs)] #[allow(static_mut_refs)]
let mut state = STATE.take().expect("Got an invalid state pointer"); let mut state = STATE.take().ok_or(Error::CriticalError(
state = Box::new(state.end_temp_objects()); "Got an invalid state pointer".to_string(),
))?;
state = Box::new(state.end_temp_objects()?);
STATE = Some(state); STATE = Some(state);
} }
Ok(()) Ok(())

View File

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

View File

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

View File

@ -1,6 +1,7 @@
use skia_safe::{self as skia, ImageFilter, Rect}; use skia_safe::{self as skia, ImageFilter, Rect};
use super::{RenderState, SurfaceId}; use super::{RenderState, SurfaceId};
use crate::error::Result;
/// Composes two image filters, returning a combined filter if both are present, /// 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. /// 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, bounds: Rect,
target_surface: SurfaceId, target_surface: SurfaceId,
draw_fn: F, draw_fn: F,
) -> bool ) -> Result<bool>
where where
F: FnOnce(&mut RenderState, SurfaceId), F: FnOnce(&mut RenderState, SurfaceId) -> Result<()>,
{ {
if let Some((mut surface, scale)) = 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); 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); surface.draw(canvas, (0.0, 0.0), render_state.sampling_options, None);
canvas.restore(); canvas.restore();
} }
true Ok(true)
} else { } else {
false Ok(false)
} }
} }
@ -81,12 +82,12 @@ pub fn render_into_filter_surface<F>(
bounds: Rect, bounds: Rect,
extra_downscale: f32, extra_downscale: f32,
draw_fn: F, draw_fn: F,
) -> Option<(skia::Surface, f32)> ) -> Result<Option<(skia::Surface, f32)>>
where where
F: FnOnce(&mut RenderState, SurfaceId), F: FnOnce(&mut RenderState, SurfaceId) -> Result<()>,
{ {
if !bounds.is_finite() || bounds.width() <= 0.0 || bounds.height() <= 0.0 { if !bounds.is_finite() || bounds.width() <= 0.0 || bounds.height() <= 0.0 {
return None; return Ok(None);
} }
let filter_id = SurfaceId::Filter; let filter_id = SurfaceId::Filter;
@ -125,10 +126,10 @@ where
canvas.translate((-bounds.left, -bounds.top)); 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(); render_state.surfaces.canvas(filter_id).restore();
let filter_surface = render_state.surfaces.surface_clone(filter_id); 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 skia_safe::{self as skia, textlayout, Font, FontMgr};
use std::collections::HashSet; use std::collections::HashSet;
use crate::error::{Error, Result};
use crate::shapes::{FontFamily, FontStyle}; use crate::shapes::{FontFamily, FontStyle};
use crate::uuid::Uuid; use crate::uuid::Uuid;
@ -26,7 +27,7 @@ pub struct FontStore {
} }
impl FontStore { impl FontStore {
pub fn new() -> Self { pub fn try_new() -> Result<Self> {
let font_mgr = FontMgr::new(); let font_mgr = FontMgr::new();
let font_provider = load_default_provider(&font_mgr); let font_provider = load_default_provider(&font_mgr);
let mut font_collection = skia::textlayout::FontCollection::new(); let mut font_collection = skia::textlayout::FontCollection::new();
@ -34,17 +35,19 @@ impl FontStore {
let debug_typeface = font_provider let debug_typeface = font_provider
.match_family_style(default_font().as_str(), skia::FontStyle::default()) .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); let debug_font = skia::Font::new(debug_typeface, 10.0);
Self { Ok(Self {
font_mgr, font_mgr,
font_provider, font_provider,
font_collection, font_collection,
debug_font, debug_font,
fallback_fonts: HashSet::new(), fallback_fonts: HashSet::new(),
} })
} }
pub fn set_scale_debug_font(&mut self, dpr: f32) { pub fn set_scale_debug_font(&mut self, dpr: f32) {
@ -70,7 +73,7 @@ impl FontStore {
font_data: &[u8], font_data: &[u8],
is_emoji: bool, is_emoji: bool,
is_fallback: bool, is_fallback: bool,
) -> Result<(), String> { ) -> Result<()> {
if self.has_family(&family, is_emoji) { if self.has_family(&family, is_emoji) {
return Ok(()); return Ok(());
} }
@ -78,7 +81,9 @@ impl FontStore {
let typeface = self let typeface = self
.font_mgr .font_mgr
.new_from_data(font_data, None) .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 alias = format!("{}", family);
let font_name = if is_emoji { 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::gpu::{self, gl::FramebufferInfo, gl::TextureInfo, DirectContext};
use skia_safe::{self as skia, ISize}; use skia_safe::{self as skia, ISize};
@ -8,24 +9,30 @@ pub struct GpuState {
} }
impl GpuState { impl GpuState {
pub fn new() -> Self { pub fn try_new() -> Result<Self> {
let interface = gpu::gl::Interface::new_native().unwrap(); let interface = gpu::gl::Interface::new_native().ok_or(Error::CriticalError(
let context = gpu::direct_contexts::make_gl(interface, None).unwrap(); "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 framebuffer_info = {
let mut fboid: gl::types::GLint = 0; let mut fboid: gl::types::GLint = 0;
unsafe { gl::GetIntegerv(gl::FRAMEBUFFER_BINDING, &mut fboid) }; unsafe { gl::GetIntegerv(gl::FRAMEBUFFER_BINDING, &mut fboid) };
FramebufferInfo { 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(), format: gpu::gl::Format::RGBA8.into(),
protected: gpu::Protected::No, protected: gpu::Protected::No,
} }
}; };
GpuState { Ok(GpuState {
context, context,
framebuffer_info, framebuffer_info,
} })
} }
fn create_webgl_texture(&mut self, width: i32, height: i32) -> gl::types::GLuint { fn create_webgl_texture(&mut self, width: i32, height: i32) -> gl::types::GLuint {
@ -56,7 +63,11 @@ impl GpuState {
texture_id 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) self.create_surface_with_dimensions(label, size.width, size.height)
} }
@ -65,7 +76,7 @@ impl GpuState {
label: String, label: String,
width: i32, width: i32,
height: i32, height: i32,
) -> skia::Surface { ) -> Result<skia::Surface> {
let backend_texture = unsafe { let backend_texture = unsafe {
let texture_id = self.create_webgl_texture(width, height); let texture_id = self.create_webgl_texture(width, height);
let texture_info = TextureInfo { let texture_info = TextureInfo {
@ -77,7 +88,7 @@ impl GpuState {
gpu::backend_textures::make_gl((width, height), gpu::Mipmapped::No, texture_info, label) 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, &mut self.context,
&backend_texture, &backend_texture,
gpu::SurfaceOrigin::BottomLeft, gpu::SurfaceOrigin::BottomLeft,
@ -86,15 +97,19 @@ impl GpuState {
None, None,
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. /// 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 = let backend_render_target =
gpu::backend_render_targets::make_gl((width, height), 1, 8, self.framebuffer_info); 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, &mut self.context,
&backend_render_target, &backend_render_target,
gpu::SurfaceOrigin::BottomLeft, gpu::SurfaceOrigin::BottomLeft,
@ -102,6 +117,10 @@ impl GpuState {
None, None,
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::shapes::ImageFill;
use crate::uuid::Uuid; use crate::uuid::Uuid;
use crate::error::{Error, Result};
use skia_safe::gpu::{surfaces, Budgeted, DirectContext}; use skia_safe::gpu::{surfaces, Budgeted, DirectContext};
use skia_safe::{self as skia, Codec, ISize}; use skia_safe::{self as skia, Codec, ISize};
use std::collections::HashMap; use std::collections::HashMap;
@ -70,7 +71,7 @@ fn create_image_from_gl_texture(
texture_id: u32, texture_id: u32,
width: i32, width: i32,
height: i32, height: i32,
) -> Result<Image, String> { ) -> Result<Image> {
use skia_safe::gpu; use skia_safe::gpu;
use skia_safe::gpu::gl::TextureInfo; use skia_safe::gpu::gl::TextureInfo;
@ -99,7 +100,9 @@ fn create_image_from_gl_texture(
skia::AlphaType::Premul, skia::AlphaType::Premul,
None, 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) 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); let key = (id, is_thumbnail);
if self.images.contains_key(&key) { 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(); let raw_data = image_data.to_vec();
@ -174,11 +182,11 @@ impl ImageStore {
texture_id: u32, texture_id: u32,
width: i32, width: i32,
height: i32, height: i32,
) -> Result<(), String> { ) -> Result<()> {
let key = (id, is_thumbnail); let key = (id, is_thumbnail);
if self.images.contains_key(&key) { 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 // 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 crate::shapes::{ParagraphBuilderGroup, Shadow, Shape, Stroke, Type};
use skia_safe::{canvas::SaveLayerRec, Paint, Path}; use skia_safe::{canvas::SaveLayerRec, Paint, Path};
use crate::error::Result;
use crate::render::text; use crate::render::text;
// Fill Shadows // Fill Shadows
@ -36,7 +37,7 @@ pub fn render_stroke_inner_shadows(
stroke: &Stroke, stroke: &Stroke,
antialias: bool, antialias: bool,
surface_id: SurfaceId, surface_id: SurfaceId,
) { ) -> Result<()> {
if !shape.has_fills() { if !shape.has_fills() {
for shadow in shape.inner_shadows_visible() { for shadow in shape.inner_shadows_visible() {
let filter = shadow.get_inner_shadow_filter(); let filter = shadow.get_inner_shadow_filter();
@ -48,9 +49,10 @@ pub fn render_stroke_inner_shadows(
filter.as_ref(), filter.as_ref(),
antialias, antialias,
None, // Inner shadows don't use spread None, // Inner shadows don't use spread
) )?;
} }
} }
Ok(())
} }
// Render text paths (unused) // Render text paths (unused)
@ -133,9 +135,9 @@ pub fn render_text_shadows(
surface_id: Option<SurfaceId>, surface_id: Option<SurfaceId>,
shadows: &[Paint], shadows: &[Paint],
blur_filter: &Option<skia_safe::ImageFilter>, blur_filter: &Option<skia_safe::ImageFilter>,
) { ) -> Result<()> {
if stroke_paragraphs_group.is_empty() { if stroke_paragraphs_group.is_empty() {
return; return Ok(());
} }
let canvas = render_state let canvas = render_state
@ -156,7 +158,7 @@ pub fn render_text_shadows(
blur_filter.as_ref(), blur_filter.as_ref(),
None, None,
None, None,
); )?;
for stroke_paragraphs in stroke_paragraphs_group.iter_mut() { for stroke_paragraphs in stroke_paragraphs_group.iter_mut() {
text::render( text::render(
@ -169,9 +171,10 @@ pub fn render_text_shadows(
blur_filter.as_ref(), blur_filter.as_ref(),
None, None,
None, None,
); )?;
} }
canvas.restore(); canvas.restore();
} }
Ok(())
} }

View File

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

View File

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

View File

@ -1,5 +1,6 @@
use super::{filters, RenderState, Shape, SurfaceId}; use super::{filters, RenderState, Shape, SurfaceId};
use crate::{ use crate::{
error::Result,
math::Rect, math::Rect,
shapes::{ shapes::{
calculate_position_data, calculate_text_layout_data, merge_fills, set_paint_fill, 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()) 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(); .collect();
paragraph_group.push(stroke_paragraphs); paragraph_group.push(stroke_paragraphs);
@ -195,7 +196,7 @@ pub fn render_with_bounds_outset(
stroke_bounds_outset: f32, stroke_bounds_outset: f32,
fill_inset: Option<f32>, fill_inset: Option<f32>,
layer_opacity: Option<f32>, layer_opacity: Option<f32>,
) { ) -> Result<()> {
if let Some(render_state) = render_state { if let Some(render_state) = render_state {
let target_surface = surface_id.unwrap_or(SurfaceId::Fills); let target_surface = surface_id.unwrap_or(SurfaceId::Fills);
@ -225,9 +226,10 @@ pub fn render_with_bounds_outset(
fill_inset, fill_inset,
layer_opacity, layer_opacity,
); );
Ok(())
}, },
) { )? {
return; return Ok(());
} }
} }
} }
@ -242,7 +244,7 @@ pub fn render_with_bounds_outset(
fill_inset, fill_inset,
layer_opacity, layer_opacity,
); );
return; return Ok(());
} }
if let Some(canvas) = canvas { if let Some(canvas) = canvas {
@ -256,6 +258,7 @@ pub fn render_with_bounds_outset(
layer_opacity, layer_opacity,
); );
} }
Ok(())
} }
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
@ -269,7 +272,7 @@ pub fn render(
blur: Option<&ImageFilter>, blur: Option<&ImageFilter>,
fill_inset: Option<f32>, fill_inset: Option<f32>,
layer_opacity: Option<f32>, layer_opacity: Option<f32>,
) { ) -> Result<()> {
render_with_bounds_outset( render_with_bounds_outset(
render_state, render_state,
canvas, canvas,
@ -281,7 +284,7 @@ pub fn render(
0.0, 0.0,
fill_inset, fill_inset,
layer_opacity, layer_opacity,
); )
} }
fn render_text_on_canvas( 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 crate::math::{self as math, bools, identitish, is_close_to, Bounds, Matrix, Point};
use common::GetBounds; use common::GetBounds;
use crate::error::Result;
use crate::shapes::{ use crate::shapes::{
ConstraintH, ConstraintV, Frame, Group, GrowType, Layout, Modifier, Shape, TransformEntry, ConstraintH, ConstraintV, Frame, Group, GrowType, Layout, Modifier, Shape, TransformEntry,
TransformEntrySource, Type, TransformEntrySource, Type,
@ -24,9 +25,9 @@ fn propagate_children(
parent_bounds_after: &Bounds, parent_bounds_after: &Bounds,
transform: Matrix, transform: Matrix,
bounds: &HashMap<Uuid, Bounds>, bounds: &HashMap<Uuid, Bounds>,
) -> VecDeque<Modifier> { ) -> Result<VecDeque<Modifier>> {
if identitish(&transform) { if identitish(&transform) {
return VecDeque::new(); return Ok(VecDeque::new());
} }
let mut result = VecDeque::new(); let mut result = VecDeque::new();
@ -74,12 +75,12 @@ fn propagate_children(
constraint_v, constraint_v,
transform, transform,
child.ignore_constraints, child.ignore_constraints,
); )?;
result.push_back(Modifier::transform_propagate(*child_id, transform)); result.push_back(Modifier::transform_propagate(*child_id, transform));
} }
result Ok(result)
} }
fn calculate_group_bounds( fn calculate_group_bounds(
@ -172,9 +173,9 @@ fn propagate_transform(
entries: &mut VecDeque<Modifier>, entries: &mut VecDeque<Modifier>,
bounds: &mut HashMap<Uuid, Bounds>, bounds: &mut HashMap<Uuid, Bounds>,
modifiers: &mut HashMap<Uuid, Matrix>, modifiers: &mut HashMap<Uuid, Matrix>,
) { ) -> Result<()> {
let Some(shape) = state.shapes.get(&entry.id) else { let Some(shape) = state.shapes.get(&entry.id) else {
return; return Ok(());
}; };
let shapes = &state.shapes; let shapes = &state.shapes;
@ -249,7 +250,7 @@ fn propagate_transform(
&shape_bounds_after, &shape_bounds_after,
transform, transform,
bounds, bounds,
); )?;
entries.append(&mut children); entries.append(&mut children);
} }
@ -275,6 +276,7 @@ fn propagate_transform(
entries.push_back(Modifier::reflow(parent.id, false)); entries.push_back(Modifier::reflow(parent.id, false));
} }
} }
Ok(())
} }
fn propagate_reflow( fn propagate_reflow(
@ -338,34 +340,35 @@ fn reflow_shape(
reflown: &mut HashSet<Uuid>, reflown: &mut HashSet<Uuid>,
entries: &mut VecDeque<Modifier>, entries: &mut VecDeque<Modifier>,
bounds: &mut HashMap<Uuid, Bounds>, bounds: &mut HashMap<Uuid, Bounds>,
) { ) -> Result<()> {
let Some(shape) = state.shapes.get(id) else { let Some(shape) = state.shapes.get(id) else {
return; return Ok(());
}; };
let shapes = &state.shapes; let shapes = &state.shapes;
let Type::Frame(frame_data) = &shape.shape_type else { let Type::Frame(frame_data) = &shape.shape_type else {
return; return Ok(());
}; };
if let Some(Layout::FlexLayout(layout_data, flex_data)) = &frame_data.layout { if let Some(Layout::FlexLayout(layout_data, flex_data)) = &frame_data.layout {
let mut children = 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); entries.append(&mut children);
} else if let Some(Layout::GridLayout(layout_data, grid_data)) = &frame_data.layout { } else if let Some(Layout::GridLayout(layout_data, grid_data)) = &frame_data.layout {
let mut children = 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); entries.append(&mut children);
} }
reflown.insert(*id); reflown.insert(*id);
Ok(())
} }
pub fn propagate_modifiers( pub fn propagate_modifiers(
state: &State, state: &State,
modifiers: &[TransformEntry], modifiers: &[TransformEntry],
pixel_precision: bool, pixel_precision: bool,
) -> Vec<TransformEntry> { ) -> Result<Vec<TransformEntry>> {
let mut entries: VecDeque<_> = modifiers let mut entries: VecDeque<_> = modifiers
.iter() .iter()
.map(|entry| { .map(|entry| {
@ -399,7 +402,7 @@ pub fn propagate_modifiers(
&mut entries, &mut entries,
&mut bounds, &mut bounds,
&mut modifiers, &mut modifiers,
), )?,
Modifier::Reflow(id, force_reflow) => { Modifier::Reflow(id, force_reflow) => {
if force_reflow { if force_reflow {
reflown.remove(&id); reflown.remove(&id);
@ -437,16 +440,16 @@ pub fn propagate_modifiers(
if reflown.contains(id) { if reflown.contains(id) {
continue; 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(); layout_reflows = HashSet::new();
} }
#[allow(dead_code)] // #[allow(dead_code)]
modifiers Ok(modifiers
.iter() .iter()
.map(|(key, val)| TransformEntry::from_input(*key, *val)) .map(|(key, val)| TransformEntry::from_input(*key, *val))
.collect() .collect())
} }
#[cfg(test)] #[cfg(test)]
@ -494,7 +497,8 @@ mod tests {
&bounds_after, &bounds_after,
transform, transform,
&HashMap::new(), &HashMap::new(),
); )
.unwrap();
assert_eq!(result.len(), 1); 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::math::{is_move_only_matrix, Bounds, Matrix};
use crate::shapes::{ConstraintH, ConstraintV}; use crate::shapes::{ConstraintH, ConstraintV};
@ -105,14 +106,14 @@ pub fn propagate_shape_constraints(
constraint_v: ConstraintV, constraint_v: ConstraintV,
transform: Matrix, transform: Matrix,
ignore_constrainst: bool, ignore_constrainst: bool,
) -> Matrix { ) -> Result<Matrix> {
// if the constrains are scale & scale or the transform has only moves we // if the constrains are scale & scale or the transform has only moves we
// can propagate as is // can propagate as is
if (ignore_constrainst if (ignore_constrainst
|| constraint_h == ConstraintH::Scale && constraint_v == ConstraintV::Scale) || constraint_h == ConstraintH::Scale && constraint_v == ConstraintV::Scale)
|| is_move_only_matrix(&transform) || is_move_only_matrix(&transform)
{ {
return transform; return Ok(transform);
} }
let mut transform = transform; let mut transform = transform;
@ -133,7 +134,9 @@ pub fn propagate_shape_constraints(
parent_transform.post_translate(center); parent_transform.post_translate(center);
parent_transform.pre_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 origin = parent_transform_inv.map_point(child_bounds_after.nw);
let mut scale = Matrix::scale((scale_width, scale_height)); 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.post_concat(&Matrix::translate(th + tv));
} }
transform Ok(transform)
} }

View File

@ -1,4 +1,6 @@
#![allow(dead_code)] #![allow(dead_code)]
use crate::error::{Error, Result};
use crate::math::{self as math, Bounds, Matrix, Point, Vector, VectorExt}; use crate::math::{self as math, Bounds, Matrix, Point, Vector, VectorExt};
use crate::shapes::{ use crate::shapes::{
AlignContent, AlignItems, AlignSelf, FlexData, JustifyContent, LayoutData, LayoutItem, AlignContent, AlignItems, AlignSelf, FlexData, JustifyContent, LayoutData, LayoutItem,
@ -588,7 +590,7 @@ pub fn reflow_flex_layout(
flex_data: &FlexData, flex_data: &FlexData,
shapes: ShapesPoolRef, shapes: ShapesPoolRef,
bounds: &mut HashMap<Uuid, Bounds>, bounds: &mut HashMap<Uuid, Bounds>,
) -> VecDeque<Modifier> { ) -> Result<VecDeque<Modifier>> {
let mut result = VecDeque::new(); let mut result = VecDeque::new();
let layout_bounds = &bounds.find(shape); let layout_bounds = &bounds.find(shape);
let layout_axis = LayoutAxis::new(shape, layout_bounds, layout_data, flex_data); 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 = 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 origin = parent_transform_inv.map_point(layout_bounds.nw);
let mut scale = Matrix::scale((scale_width, scale_height)); 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)); result.push_back(Modifier::parent(shape.id, scale));
bounds.insert(shape.id, layout_bounds_after); 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::math::{self as math, intersect_rays, Bounds, Matrix, Point, Ray, Vector, VectorExt};
use crate::shapes::{ use crate::shapes::{
AlignContent, AlignItems, AlignSelf, Frame, GridCell, GridData, GridTrack, GridTrackType, AlignContent, AlignItems, AlignSelf, Frame, GridCell, GridData, GridTrack, GridTrackType,
@ -6,6 +7,7 @@ use crate::shapes::{
}; };
use crate::state::ShapesPoolRef; use crate::state::ShapesPoolRef;
use crate::uuid::Uuid; use crate::uuid::Uuid;
use std::collections::{HashMap, HashSet, VecDeque}; use std::collections::{HashMap, HashSet, VecDeque};
use super::common::GetBounds; use super::common::GetBounds;
@ -704,7 +706,7 @@ pub fn reflow_grid_layout(
grid_data: &GridData, grid_data: &GridData,
shapes: ShapesPoolRef, shapes: ShapesPoolRef,
bounds: &mut HashMap<Uuid, Bounds>, bounds: &mut HashMap<Uuid, Bounds>,
) -> VecDeque<Modifier> { ) -> Result<VecDeque<Modifier>> {
let mut result = VecDeque::new(); let mut result = VecDeque::new();
let layout_bounds = bounds.find(shape); let layout_bounds = bounds.find(shape);
let children: HashSet<Uuid> = shape.children_ids_iter(true).copied().collect(); 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 = 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 origin = parent_transform_inv.map_point(layout_bounds.nw);
let mut scale = Matrix::scale((scale_width, scale_height)); 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); 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 shapes_pool::{ShapesPool, ShapesPoolMutRef, ShapesPoolRef};
pub use text_editor::*; pub use text_editor::*;
use crate::error::{Error, Result};
use crate::render::RenderState; use crate::render::RenderState;
use crate::shapes::Shape; use crate::shapes::Shape;
use crate::tiles; use crate::tiles;
@ -28,41 +29,44 @@ pub(crate) struct State {
} }
impl State { impl State {
pub fn new(width: i32, height: i32) -> Self { pub fn try_new(width: i32, height: i32) -> Result<Self> {
State { Ok(State {
render_state: RenderState::new(width, height), render_state: RenderState::try_new(width, height)?,
text_editor_state: TextEditorState::new(), text_editor_state: TextEditorState::new(),
current_id: None, current_id: None,
current_browser: 0, current_browser: 0,
shapes: ShapesPool::new(), shapes: ShapesPool::new(),
// TODO: Maybe this can be moved to a different object // TODO: Maybe this can be moved to a different object
saved_shapes: None, saved_shapes: None,
} })
} }
// Creates a new temporary shapes pool. // Creates a new temporary shapes pool.
// Will panic if a previous temporary pool exists. // 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() { 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.saved_shapes = Some(self.shapes);
self.shapes = ShapesPool::new(); self.shapes = ShapesPool::new();
self Ok(self)
} }
// Disposes of the temporary shapes pool restoring the normal pool // Disposes of the temporary shapes pool restoring the normal pool
// Will panic if a there is no temporary pool. // Will panic if a there is no temporary pool.
pub fn end_temp_objects(mut self) -> Self { pub fn end_temp_objects(mut self) -> Result<Self> {
self.shapes = self self.shapes = self.saved_shapes.ok_or(Error::CriticalError(
.saved_shapes "Tried to end temp objects but not content to be restored is present".to_string(),
.expect("Tried to end temp objects but not content to be restored is present"); ))?;
self.saved_shapes = None; self.saved_shapes = None;
self Ok(self)
} }
pub fn resize(&mut self, width: i32, height: i32) { pub fn resize(&mut self, width: i32, height: i32) -> Result<()> {
self.render_state.resize(width, height); self.render_state.resize(width, height)
} }
pub fn render_state_mut(&mut self) -> &mut RenderState { pub fn render_state_mut(&mut self) -> &mut RenderState {
@ -87,19 +91,17 @@ impl State {
self.render_state.render_from_cache(&self.shapes); 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 self.render_state
.start_render_loop(None, &self.shapes, timestamp, true)?; .start_render_loop(None, &self.shapes, timestamp, true)
Ok(())
} }
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 self.render_state
.start_render_loop(Some(id), &self.shapes, timestamp, true)?; .start_render_loop(Some(id), &self.shapes, timestamp, true)
Ok(())
} }
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. // 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 // 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 // tiles to appear empty. This can happen if start_render_loop() is called before
@ -111,14 +113,12 @@ impl State {
} }
self.render_state self.render_state
.start_render_loop(None, &self.shapes, timestamp, false)?; .start_render_loop(None, &self.shapes, timestamp, false)
Ok(())
} }
pub fn process_animation_frame(&mut self, timestamp: i32) -> Result<(), String> { pub fn process_animation_frame(&mut self, timestamp: i32) -> Result<()> {
self.render_state self.render_state
.process_animation_frame(None, &self.shapes, timestamp)?; .process_animation_frame(None, &self.shapes, timestamp)
Ok(())
} }
pub fn clear_focus_mode(&mut self) { pub fn clear_focus_mode(&mut self) {
@ -227,10 +227,10 @@ impl State {
let _ = self.render_state.render_preview(&self.shapes, timestamp); 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 // Index-based storage is safe
self.render_state self.render_state
.rebuild_modifier_tiles(&mut self.shapes, ids); .rebuild_modifier_tiles(&mut self.shapes, ids)
} }
pub fn font_collection(&self) -> &FontCollection { pub fn font_collection(&self) -> &FontCollection {

View File

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

View File

@ -1,11 +1,10 @@
use crate::error::{Error, Result};
use crate::mem; use crate::mem;
use macros::wasm_error;
// use crate::mem::SerializableResult;
use crate::error::Error;
use crate::uuid::Uuid; use crate::uuid::Uuid;
use crate::with_state_mut; use crate::with_state_mut;
use crate::STATE; use crate::STATE;
use crate::{shapes::ImageFill, utils::uuid_from_u32_quartet}; use crate::{shapes::ImageFill, utils::uuid_from_u32_quartet};
use macros::wasm_error;
const FLAG_KEEP_ASPECT_RATIO: u8 = 1 << 0; const FLAG_KEEP_ASPECT_RATIO: u8 = 1 << 0;
const IMAGE_IDS_SIZE: usize = 32; const IMAGE_IDS_SIZE: usize = 32;
@ -50,6 +49,7 @@ pub struct ShapeImageIds {
impl From<[u8; IMAGE_IDS_SIZE]> for ShapeImageIds { impl From<[u8; IMAGE_IDS_SIZE]> for ShapeImageIds {
fn from(bytes: [u8; IMAGE_IDS_SIZE]) -> Self { 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 shape_id = Uuid::try_from(&bytes[0..16]).unwrap();
let image_id = Uuid::try_from(&bytes[16..32]).unwrap(); let image_id = Uuid::try_from(&bytes[16..32]).unwrap();
ShapeImageIds { shape_id, image_id } ShapeImageIds { shape_id, image_id }
@ -57,9 +57,9 @@ impl From<[u8; IMAGE_IDS_SIZE]> for ShapeImageIds {
} }
impl TryFrom<Vec<u8>> 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]; let mut arr = [0u8; IMAGE_IDS_SIZE];
arr.copy_from_slice(&value); arr.copy_from_slice(&value);
Ok(ShapeImageIds::from(arr)) Ok(ShapeImageIds::from(arr))
@ -68,13 +68,16 @@ impl TryFrom<Vec<u8>> for ShapeImageIds {
#[no_mangle] #[no_mangle]
#[wasm_error] #[wasm_error]
pub extern "C" fn store_image() -> crate::error::Result<()> { pub extern "C" fn store_image() -> Result<()> {
let bytes = mem::bytes(); 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) // Read is_thumbnail flag (4 bytes as u32)
let is_thumbnail_bytes = &bytes[IMAGE_IDS_SIZE..IMAGE_HEADER_SIZE]; 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 is_thumbnail = is_thumbnail_value != 0;
let image_bytes = &bytes[IMAGE_HEADER_SIZE..]; 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) /// - bytes 44-47: height (i32)
#[no_mangle] #[no_mangle]
#[wasm_error] #[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(); let bytes = mem::bytes();
// FIXME: where does this 48 come from?
if bytes.len() < 48 { if bytes.len() < 48 {
// FIXME: Review if this should be an critical or a recoverable error. // FIXME: Review if this should be an critical or a recoverable error.
eprintln!("store_image_from_texture: insufficient data"); 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) // Read is_thumbnail flag (4 bytes as u32)
let is_thumbnail_bytes = &bytes[IMAGE_IDS_SIZE..IMAGE_HEADER_SIZE]; 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 is_thumbnail = is_thumbnail_value != 0;
// Read GL texture ID (4 bytes as u32) // Read GL texture ID (4 bytes as u32)
let texture_id_bytes = &bytes[36..40]; 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) // Read width and height (8 bytes as two i32s)
let width_bytes = &bytes[40..44]; 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_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, { with_state_mut!(state, {
if let Err(msg) = state.render_state_mut().add_image_from_gl_texture( 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, width,
height, height,
) { ) {
// FIXME: Review if we should return a RecoverableError
eprintln!("store_image_from_texture error: {}", msg); eprintln!("store_image_from_texture error: {}", msg);
} }
state.touch_shape(ids.shape_id); 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; use super::align;
#[allow(unused_imports)] #[allow(unused_imports)]
use crate::error::Result; use crate::error::{Error, Result};
#[derive(Debug)] #[derive(Debug)]
#[repr(C, align(1))] #[repr(C, align(1))]
@ -177,9 +177,13 @@ pub extern "C" fn set_grid_columns() -> Result<()> {
let entries: Vec<GridTrack> = bytes let entries: Vec<GridTrack> = bytes
.chunks(size_of::<RawGridTrack>()) .chunks(size_of::<RawGridTrack>())
.map(|data| data.try_into().unwrap()) .map(|data| {
.map(|data: [u8; size_of::<RawGridTrack>()]| RawGridTrack::from(data).into()) let track_bytes: [u8; size_of::<RawGridTrack>()] = data
.collect(); .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| { with_current_shape_mut!(state, |shape: &mut Shape| {
shape.set_grid_columns(entries); shape.set_grid_columns(entries);
@ -196,9 +200,13 @@ pub extern "C" fn set_grid_rows() -> Result<()> {
let entries: Vec<GridTrack> = bytes let entries: Vec<GridTrack> = bytes
.chunks(size_of::<RawGridTrack>()) .chunks(size_of::<RawGridTrack>())
.map(|data| data.try_into().unwrap()) .map(|data| {
.map(|data: [u8; size_of::<RawGridTrack>()]| RawGridTrack::from(data).into()) let track_bytes: [u8; size_of::<RawGridTrack>()] = data
.collect(); .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| { with_current_shape_mut!(state, |shape: &mut Shape| {
shape.set_grid_rows(entries); shape.set_grid_rows(entries);
@ -215,9 +223,13 @@ pub extern "C" fn set_grid_cells() -> Result<()> {
let cells: Vec<RawGridCell> = bytes let cells: Vec<RawGridCell> = bytes
.chunks(size_of::<RawGridCell>()) .chunks(size_of::<RawGridCell>())
.map(|data| data.try_into().expect("Invalid grid cell data")) .map(|data| {
.map(|data: [u8; size_of::<RawGridCell>()]| RawGridCell::from(data)) let cell_bytes: [u8; size_of::<RawGridCell>()] = data
.collect(); .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| { with_current_shape_mut!(state, |shape: &mut Shape| {
shape.set_grid_cells(cells.into_iter().map(|raw| raw.into()).collect()); 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::mem::size_of;
use std::sync::{Mutex, OnceLock}; use std::sync::{Mutex, OnceLock};
use crate::error::{Error, Result};
use crate::shapes::{Path, Segment, ToPath}; use crate::shapes::{Path, Segment, ToPath};
use crate::{mem, with_current_shape, with_current_shape_mut, STATE}; 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 { impl TryFrom<&[u8]> for RawSegmentData {
type Error = String; type Error = Error;
fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> { fn try_from(bytes: &[u8]) -> Result<Self> {
let data: [u8; RAW_SEGMENT_DATA_SIZE] = bytes let data: [u8; RAW_SEGMENT_DATA_SIZE] = bytes
.get(0..RAW_SEGMENT_DATA_SIZE) .get(0..RAW_SEGMENT_DATA_SIZE)
.and_then(|slice| slice.try_into().ok()) .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)) Ok(RawSegmentData::from(data))
} }
} }
@ -154,10 +155,14 @@ fn get_path_upload_buffer() -> &'static Mutex<Vec<u8>> {
} }
#[no_mangle] #[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 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(); buffer.clear();
Ok(())
} }
#[no_mangle] #[no_mangle]
@ -165,32 +170,40 @@ pub extern "C" fn start_shape_path_buffer() {
pub extern "C" fn set_shape_path_chunk_buffer() -> Result<()> { pub extern "C" fn set_shape_path_chunk_buffer() -> Result<()> {
let bytes = mem::bytes(); let bytes = mem::bytes();
let buffer = get_path_upload_buffer(); 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); buffer.extend_from_slice(&bytes);
mem::free_bytes()?; mem::free_bytes()?;
Ok(()) Ok(())
} }
#[no_mangle] #[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| { 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); shape.set_path_segments(segments);
buffer.clear();
}); });
buffer.clear();
Ok(())
} }
#[no_mangle] #[no_mangle]

View File

@ -6,6 +6,10 @@ use crate::wasm::blend::RawBlendMode;
use crate::wasm::layouts::constraints::{RawConstraintH, RawConstraintV}; use crate::wasm::layouts::constraints::{RawConstraintH, RawConstraintV};
use crate::{with_state_mut, STATE}; use crate::{with_state_mut, STATE};
#[allow(unused_imports)]
use crate::error::{Error, Result};
use macros::wasm_error;
use super::RawShapeType; use super::RawShapeType;
const FLAG_CLIP_CONTENT: u8 = 0b0000_0001; const FLAG_CLIP_CONTENT: u8 = 0b0000_0001;
@ -106,14 +110,18 @@ impl From<[u8; RAW_BASE_PROPS_SIZE]> for RawBasePropsData {
} }
#[no_mangle] #[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(); let bytes = mem::bytes();
if bytes.len() < RAW_BASE_PROPS_SIZE { 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 raw = RawBasePropsData::from(data);
let id = raw.id(); 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)); shape.set_corners((raw.corner_r1, raw.corner_r2, raw.corner_r3, raw.corner_r4));
} }
}); });
Ok(())
} }
#[cfg(test)] #[cfg(test)]

View File

@ -292,9 +292,10 @@ pub extern "C" fn clear_shape_text() {
#[wasm_error] #[wasm_error]
pub extern "C" fn set_shape_text_content() -> crate::error::Result<()> { pub extern "C" fn set_shape_text_content() -> crate::error::Result<()> {
let bytes = mem::bytes(); let bytes = mem::bytes();
with_current_shape_mut!(state, |shape: &mut Shape| { let raw_text_data = RawParagraph::try_from(&bytes)
let raw_text_data = RawParagraph::try_from(&bytes).unwrap(); .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(|_| { shape.add_paragraph(raw_text_data.into()).map_err(|_| {
Error::RecoverableError(format!( Error::RecoverableError(format!(
"Error with set_shape_text_content on {:?}", "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; use skia_safe as skia;
@ -39,11 +41,11 @@ impl From<[u8; RAW_TRANSFORM_ENTRY_SIZE]> for RawTransformEntry {
} }
impl TryFrom<&[u8]> for RawTransformEntry { impl TryFrom<&[u8]> for RawTransformEntry {
type Error = String; type Error = Error;
fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> { fn try_from(bytes: &[u8]) -> Result<Self> {
let bytes: [u8; RAW_TRANSFORM_ENTRY_SIZE] = bytes let bytes: [u8; RAW_TRANSFORM_ENTRY_SIZE] = bytes
.try_into() .try_into()
.map_err(|_| "Invalid transform entry bytes".to_string())?; .map_err(|_| Error::CriticalError("Invalid transform entry bytes".to_string()))?;
Ok(RawTransformEntry::from(bytes)) Ok(RawTransformEntry::from(bytes))
} }
} }
@ -73,16 +75,17 @@ impl From<RawTransformEntry> for TransformEntry {
} }
#[no_mangle] #[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 bytes = mem::bytes();
let entries: Vec<TransformEntry> = bytes let entries: Vec<TransformEntry> = bytes
.chunks(RAW_TRANSFORM_ENTRY_SIZE) .chunks(RAW_TRANSFORM_ENTRY_SIZE)
.map(|data| RawTransformEntry::try_from(data).unwrap().into()) .map(|data| RawTransformEntry::try_from(data).map(|entry| entry.into()))
.collect(); .collect::<Result<Vec<_>>>()?;
with_state!(state, { with_state!(state, {
let result = shapes::propagate_modifiers(state, &entries, pixel_precision); let result = shapes::propagate_modifiers(state, &entries, pixel_precision)?;
mem::write_vec(result) Ok(mem::write_vec(result))
}) })
} }