mirror of
https://github.com/penpot/penpot.git
synced 2026-04-25 11:18:36 +00:00
🎉 Add background blur for wasm render
This commit is contained in:
parent
5ba53f7296
commit
e630be1509
@ -61,11 +61,13 @@ pub fn wasm_error(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
let _: &dyn std::error::Error = &__e;
|
||||
let __msg = __e.to_string();
|
||||
crate::mem::set_error_code(__e.into());
|
||||
crate::mem::free_bytes().expect("Failed to free bytes");
|
||||
panic!("WASM error: {}",__msg);
|
||||
}
|
||||
},
|
||||
Err(__payload) => {
|
||||
crate::mem::set_error_code(0x02); // critical, same as Error::Critical
|
||||
crate::mem::free_bytes().expect("Failed to free bytes");
|
||||
std::panic::resume_unwind(__payload);
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,6 +30,9 @@ use uuid::Uuid;
|
||||
|
||||
pub(crate) static mut STATE: Option<Box<State>> = None;
|
||||
|
||||
// FIXME: These with_state* macros should be using our CriticalError instead of expect.
|
||||
// But to do that, we need to not use them at domain-level (i.e. in business logic), just
|
||||
// in the context of the wasm call.
|
||||
#[macro_export]
|
||||
macro_rules! with_state_mut {
|
||||
($state:ident, $block:block) => {{
|
||||
@ -102,7 +105,7 @@ macro_rules! with_state_mut_current_shape {
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn init(width: i32, height: i32) -> Result<()> {
|
||||
let state_box = Box::new(State::new(width, height));
|
||||
let state_box = Box::new(State::try_new(width, height)?);
|
||||
unsafe {
|
||||
STATE = Some(state_box);
|
||||
}
|
||||
@ -138,7 +141,7 @@ pub extern "C" fn set_render_options(debug: u32, dpr: f32) -> Result<()> {
|
||||
with_state_mut!(state, {
|
||||
let render_state = state.render_state_mut();
|
||||
render_state.set_debug_flags(debug);
|
||||
render_state.set_dpr(dpr);
|
||||
render_state.set_dpr(dpr)?;
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
@ -162,7 +165,7 @@ pub extern "C" fn render(_: i32) -> Result<()> {
|
||||
state.rebuild_touched_tiles();
|
||||
state
|
||||
.start_render_loop(performance::get_time())
|
||||
.expect("Error rendering");
|
||||
.map_err(|_| Error::RecoverableError("Error rendering".to_string()))?;
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
@ -174,7 +177,7 @@ pub extern "C" fn render_sync() -> Result<()> {
|
||||
state.rebuild_tiles();
|
||||
state
|
||||
.render_sync(performance::get_time())
|
||||
.expect("Error rendering");
|
||||
.map_err(|_| Error::RecoverableError("Error rendering".to_string()))?;
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
@ -236,24 +239,12 @@ pub extern "C" fn render_preview() -> Result<()> {
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn process_animation_frame(timestamp: i32) -> Result<()> {
|
||||
let result = std::panic::catch_unwind(|| {
|
||||
with_state_mut!(state, {
|
||||
state
|
||||
.process_animation_frame(timestamp)
|
||||
.expect("Error processing animation frame");
|
||||
});
|
||||
});
|
||||
let result = with_state_mut!(state, { state.process_animation_frame(timestamp) });
|
||||
|
||||
match result {
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
match err.downcast_ref::<String>() {
|
||||
Some(message) => println!("process_animation_frame error: {}", message),
|
||||
None => println!("process_animation_frame error: {:?}", err),
|
||||
}
|
||||
std::panic::resume_unwind(err);
|
||||
}
|
||||
if let Err(err) = result {
|
||||
eprintln!("process_animation_frame error: {}", err);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -270,7 +261,7 @@ pub extern "C" fn reset_canvas() -> Result<()> {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn resize_viewbox(width: i32, height: i32) -> Result<()> {
|
||||
with_state_mut!(state, {
|
||||
state.resize(width, height);
|
||||
state.resize(width, height)?;
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
@ -362,8 +353,8 @@ pub extern "C" fn set_focus_mode() -> Result<()> {
|
||||
|
||||
let entries: Vec<Uuid> = bytes
|
||||
.chunks(size_of::<<Uuid as SerializableResult>::BytesType>())
|
||||
.map(|data| Uuid::try_from(data).unwrap())
|
||||
.collect();
|
||||
.map(|data| Uuid::try_from(data).map_err(|e| Error::RecoverableError(e.to_string())))
|
||||
.collect::<Result<Vec<Uuid>>>()?;
|
||||
|
||||
with_state_mut!(state, {
|
||||
state.set_focus_mode(entries);
|
||||
@ -637,8 +628,8 @@ pub extern "C" fn set_children() -> Result<()> {
|
||||
|
||||
let entries: Vec<Uuid> = bytes
|
||||
.chunks(size_of::<<Uuid as SerializableResult>::BytesType>())
|
||||
.map(|data| Uuid::try_from(data).unwrap())
|
||||
.collect();
|
||||
.map(|data| Uuid::try_from(data).map_err(|e| Error::CriticalError(e.to_string())))
|
||||
.collect::<Result<Vec<Uuid>>>()?;
|
||||
|
||||
set_children_set(entries)?;
|
||||
|
||||
@ -761,10 +752,15 @@ pub extern "C" fn get_selection_rect() -> Result<*mut u8> {
|
||||
pub extern "C" fn set_structure_modifiers() -> Result<()> {
|
||||
let bytes = mem::bytes();
|
||||
|
||||
let entries: Vec<_> = bytes
|
||||
let entries: Vec<StructureEntry> = bytes
|
||||
.chunks(44)
|
||||
.map(|data| StructureEntry::from_bytes(data.try_into().unwrap()))
|
||||
.collect();
|
||||
.map(|chunk| {
|
||||
let data = chunk
|
||||
.try_into()
|
||||
.map_err(|_| Error::CriticalError("Invalid StructureEntry bytes".to_string()))?;
|
||||
Ok(StructureEntry::from_bytes(data))
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
with_state_mut!(state, {
|
||||
let mut structure = HashMap::new();
|
||||
@ -783,7 +779,9 @@ pub extern "C" fn set_structure_modifiers() -> Result<()> {
|
||||
structure.entry(entry.parent).or_insert_with(Vec::new);
|
||||
structure
|
||||
.get_mut(&entry.parent)
|
||||
.expect("Parent not found for entry")
|
||||
.ok_or(Error::CriticalError(
|
||||
"Parent not found for entry".to_string(),
|
||||
))?
|
||||
.push(entry);
|
||||
}
|
||||
}
|
||||
@ -814,10 +812,10 @@ pub extern "C" fn clean_modifiers() -> Result<()> {
|
||||
pub extern "C" fn set_modifiers() -> Result<()> {
|
||||
let bytes = mem::bytes();
|
||||
|
||||
let entries: Vec<_> = bytes
|
||||
let entries: Vec<TransformEntry> = bytes
|
||||
.chunks(size_of::<<TransformEntry as SerializableResult>::BytesType>())
|
||||
.map(|data| TransformEntry::try_from(data).unwrap())
|
||||
.collect();
|
||||
.map(|data| TransformEntry::try_from(data).map_err(|e| Error::CriticalError(e.to_string())))
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
let mut modifiers = HashMap::new();
|
||||
let mut ids = Vec::<Uuid>::new();
|
||||
@ -828,7 +826,7 @@ pub extern "C" fn set_modifiers() -> Result<()> {
|
||||
|
||||
with_state_mut!(state, {
|
||||
state.set_modifiers(modifiers);
|
||||
state.rebuild_modifier_tiles(ids);
|
||||
state.rebuild_modifier_tiles(ids)?;
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
@ -838,8 +836,10 @@ pub extern "C" fn set_modifiers() -> Result<()> {
|
||||
pub extern "C" fn start_temp_objects() -> Result<()> {
|
||||
unsafe {
|
||||
#[allow(static_mut_refs)]
|
||||
let mut state = STATE.take().expect("Got an invalid state pointer");
|
||||
state = Box::new(state.start_temp_objects());
|
||||
let mut state = STATE.take().ok_or(Error::CriticalError(
|
||||
"Got an invalid state pointer".to_string(),
|
||||
))?;
|
||||
state = Box::new(state.start_temp_objects()?);
|
||||
STATE = Some(state);
|
||||
}
|
||||
Ok(())
|
||||
@ -850,8 +850,10 @@ pub extern "C" fn start_temp_objects() -> Result<()> {
|
||||
pub extern "C" fn end_temp_objects() -> Result<()> {
|
||||
unsafe {
|
||||
#[allow(static_mut_refs)]
|
||||
let mut state = STATE.take().expect("Got an invalid state pointer");
|
||||
state = Box::new(state.end_temp_objects());
|
||||
let mut state = STATE.take().ok_or(Error::CriticalError(
|
||||
"Got an invalid state pointer".to_string(),
|
||||
))?;
|
||||
state = Box::new(state.end_temp_objects()?);
|
||||
STATE = Some(state);
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@ -21,6 +21,7 @@ use gpu_state::GpuState;
|
||||
use options::RenderOptions;
|
||||
pub use surfaces::{SurfaceId, Surfaces};
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::performance;
|
||||
use crate::shapes::{
|
||||
all_with_ancestors, radius_to_sigma, Blur, BlurType, Corners, Fill, Shadow, Shape, SolidColor,
|
||||
@ -326,19 +327,19 @@ pub fn get_cache_size(viewbox: Viewbox, scale: f32) -> skia::ISize {
|
||||
}
|
||||
|
||||
impl RenderState {
|
||||
pub fn new(width: i32, height: i32) -> RenderState {
|
||||
pub fn try_new(width: i32, height: i32) -> Result<RenderState> {
|
||||
// This needs to be done once per WebGL context.
|
||||
let mut gpu_state = GpuState::new();
|
||||
let mut gpu_state = GpuState::try_new()?;
|
||||
let sampling_options =
|
||||
skia::SamplingOptions::new(skia::FilterMode::Linear, skia::MipmapMode::Nearest);
|
||||
|
||||
let fonts = FontStore::new();
|
||||
let surfaces = Surfaces::new(
|
||||
let fonts = FontStore::try_new()?;
|
||||
let surfaces = Surfaces::try_new(
|
||||
&mut gpu_state,
|
||||
(width, height),
|
||||
sampling_options,
|
||||
tiles::get_tile_dimensions(),
|
||||
);
|
||||
)?;
|
||||
|
||||
// This is used multiple times everywhere so instead of creating new instances every
|
||||
// time we reuse this one.
|
||||
@ -346,7 +347,7 @@ impl RenderState {
|
||||
let viewbox = Viewbox::new(width as f32, height as f32);
|
||||
let tiles = tiles::TileHashMap::new();
|
||||
|
||||
RenderState {
|
||||
Ok(RenderState {
|
||||
gpu_state: gpu_state.clone(),
|
||||
options: RenderOptions::default(),
|
||||
surfaces,
|
||||
@ -377,7 +378,7 @@ impl RenderState {
|
||||
touched_ids: HashSet::default(),
|
||||
ignore_nested_blurs: false,
|
||||
preview_mode: false,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Combines every visible layer blur currently active (ancestors + shape)
|
||||
@ -531,15 +532,15 @@ impl RenderState {
|
||||
/// Runs `f` with `ignore_nested_blurs` temporarily forced to `true`.
|
||||
/// Certain off-screen passes (e.g. shadow masks) must render shapes without
|
||||
/// inheriting ancestor blur. This helper guarantees the flag is restored.
|
||||
fn with_nested_blurs_suppressed<F, R>(&mut self, f: F) -> R
|
||||
fn with_nested_blurs_suppressed<F, R>(&mut self, f: F) -> Result<R>
|
||||
where
|
||||
F: FnOnce(&mut RenderState) -> R,
|
||||
F: FnOnce(&mut RenderState) -> Result<R>,
|
||||
{
|
||||
let previous = self.ignore_nested_blurs;
|
||||
self.ignore_nested_blurs = true;
|
||||
let result = f(self);
|
||||
let result = f(self)?;
|
||||
self.ignore_nested_blurs = previous;
|
||||
result
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn fonts(&self) -> &FontStore {
|
||||
@ -550,12 +551,7 @@ impl RenderState {
|
||||
&mut self.fonts
|
||||
}
|
||||
|
||||
pub fn add_image(
|
||||
&mut self,
|
||||
id: Uuid,
|
||||
is_thumbnail: bool,
|
||||
image_data: &[u8],
|
||||
) -> Result<(), String> {
|
||||
pub fn add_image(&mut self, id: Uuid, is_thumbnail: bool, image_data: &[u8]) -> Result<()> {
|
||||
self.images.add(id, is_thumbnail, image_data)
|
||||
}
|
||||
|
||||
@ -567,7 +563,7 @@ impl RenderState {
|
||||
texture_id: u32,
|
||||
width: i32,
|
||||
height: i32,
|
||||
) -> Result<(), String> {
|
||||
) -> Result<()> {
|
||||
self.images
|
||||
.add_image_from_gl_texture(id, is_thumbnail, texture_id, width, height)
|
||||
}
|
||||
@ -580,15 +576,16 @@ impl RenderState {
|
||||
self.options.flags = debug;
|
||||
}
|
||||
|
||||
pub fn set_dpr(&mut self, dpr: f32) {
|
||||
pub fn set_dpr(&mut self, dpr: f32) -> Result<()> {
|
||||
if Some(dpr) != self.options.dpr {
|
||||
self.options.dpr = Some(dpr);
|
||||
self.resize(
|
||||
self.viewbox.width.floor() as i32,
|
||||
self.viewbox.height.floor() as i32,
|
||||
);
|
||||
)?;
|
||||
self.fonts.set_scale_debug_font(dpr);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_background_color(&mut self, color: skia::Color) {
|
||||
@ -599,13 +596,15 @@ impl RenderState {
|
||||
self.preview_mode = enabled;
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, width: i32, height: i32) {
|
||||
pub fn resize(&mut self, width: i32, height: i32) -> Result<()> {
|
||||
let dpr_width = (width as f32 * self.options.dpr()).floor() as i32;
|
||||
let dpr_height = (height as f32 * self.options.dpr()).floor() as i32;
|
||||
self.surfaces
|
||||
.resize(&mut self.gpu_state, dpr_width, dpr_height);
|
||||
.resize(&mut self.gpu_state, dpr_width, dpr_height)?;
|
||||
self.viewbox.set_wh(width as f32, height as f32);
|
||||
self.tile_viewbox.update(self.viewbox, self.get_scale());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn flush_and_submit(&mut self) {
|
||||
@ -627,19 +626,23 @@ impl RenderState {
|
||||
self.surfaces.canvas(surface_id).restore();
|
||||
}
|
||||
|
||||
pub fn apply_render_to_final_canvas(&mut self, rect: skia::Rect) {
|
||||
let tile_rect = self.get_current_aligned_tile_bounds();
|
||||
pub fn apply_render_to_final_canvas(&mut self, rect: skia::Rect) -> Result<()> {
|
||||
let tile_rect = self.get_current_aligned_tile_bounds()?;
|
||||
self.surfaces.cache_current_tile_texture(
|
||||
&self.tile_viewbox,
|
||||
&self.current_tile.unwrap(),
|
||||
&self
|
||||
.current_tile
|
||||
.ok_or(Error::CriticalError("Current tile not found".to_string()))?,
|
||||
&tile_rect,
|
||||
);
|
||||
|
||||
self.surfaces.draw_cached_tile_surface(
|
||||
self.current_tile.unwrap(),
|
||||
self.current_tile
|
||||
.ok_or(Error::CriticalError("Current tile not found".to_string()))?,
|
||||
rect,
|
||||
self.background_color,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn apply_drawing_to_render_canvas(&mut self, shape: Option<&Shape>) {
|
||||
@ -748,7 +751,7 @@ impl RenderState {
|
||||
offset: Option<(f32, f32)>,
|
||||
parent_shadows: Option<Vec<skia_safe::Paint>>,
|
||||
outset: Option<f32>,
|
||||
) {
|
||||
) -> Result<()> {
|
||||
let surface_ids = fills_surface_id as u32
|
||||
| strokes_surface_id as u32
|
||||
| innershadows_surface_id as u32
|
||||
@ -813,7 +816,7 @@ impl RenderState {
|
||||
antialias,
|
||||
SurfaceId::Current,
|
||||
None,
|
||||
);
|
||||
)?;
|
||||
|
||||
// Pass strokes in natural order; stroke merging handles top-most ordering internally.
|
||||
let visible_strokes: Vec<&Stroke> = shape.visible_strokes().collect();
|
||||
@ -824,7 +827,7 @@ impl RenderState {
|
||||
Some(SurfaceId::Current),
|
||||
antialias,
|
||||
outset,
|
||||
);
|
||||
)?;
|
||||
|
||||
self.surfaces.apply_mut(SurfaceId::Current as u32, |s| {
|
||||
s.canvas().restore();
|
||||
@ -840,7 +843,7 @@ impl RenderState {
|
||||
s.canvas().restore();
|
||||
});
|
||||
}
|
||||
return;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// set clipping
|
||||
@ -1017,7 +1020,7 @@ impl RenderState {
|
||||
None,
|
||||
text_fill_inset,
|
||||
None,
|
||||
);
|
||||
)?;
|
||||
|
||||
for (stroke_paragraphs, layer_opacity) in stroke_paragraphs_list
|
||||
.iter_mut()
|
||||
@ -1034,7 +1037,7 @@ impl RenderState {
|
||||
text_stroke_blur_outset,
|
||||
None,
|
||||
*layer_opacity,
|
||||
);
|
||||
)?;
|
||||
}
|
||||
} else {
|
||||
let mut drop_shadows = shape.drop_shadow_paints();
|
||||
@ -1077,7 +1080,7 @@ impl RenderState {
|
||||
blur_filter.as_ref(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
)?;
|
||||
}
|
||||
} else {
|
||||
shadows::render_text_shadows(
|
||||
@ -1088,7 +1091,7 @@ impl RenderState {
|
||||
text_drop_shadows_surface_id.into(),
|
||||
&parent_shadows,
|
||||
&blur_filter,
|
||||
);
|
||||
)?;
|
||||
}
|
||||
} else {
|
||||
// 1. Text drop shadows
|
||||
@ -1104,7 +1107,7 @@ impl RenderState {
|
||||
blur_filter.as_ref(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1119,7 +1122,7 @@ impl RenderState {
|
||||
blur_filter.as_ref(),
|
||||
text_fill_inset,
|
||||
None,
|
||||
);
|
||||
)?;
|
||||
|
||||
// 3. Stroke drop shadows
|
||||
shadows::render_text_shadows(
|
||||
@ -1130,7 +1133,7 @@ impl RenderState {
|
||||
text_drop_shadows_surface_id.into(),
|
||||
&drop_shadows,
|
||||
&blur_filter,
|
||||
);
|
||||
)?;
|
||||
|
||||
// 4. Stroke fills
|
||||
for (stroke_paragraphs, layer_opacity) in stroke_paragraphs_list
|
||||
@ -1148,7 +1151,7 @@ impl RenderState {
|
||||
text_stroke_blur_outset,
|
||||
None,
|
||||
*layer_opacity,
|
||||
);
|
||||
)?;
|
||||
}
|
||||
|
||||
// 5. Stroke inner shadows
|
||||
@ -1160,7 +1163,7 @@ impl RenderState {
|
||||
Some(innershadows_surface_id),
|
||||
&inner_shadows,
|
||||
&blur_filter,
|
||||
);
|
||||
)?;
|
||||
|
||||
// 6. Fill Inner shadows
|
||||
if !shape.has_visible_strokes() {
|
||||
@ -1175,7 +1178,7 @@ impl RenderState {
|
||||
blur_filter.as_ref(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1223,7 +1226,7 @@ impl RenderState {
|
||||
antialias,
|
||||
fills_surface_id,
|
||||
outset,
|
||||
);
|
||||
)?;
|
||||
}
|
||||
} else {
|
||||
fills::render(
|
||||
@ -1233,7 +1236,7 @@ impl RenderState {
|
||||
antialias,
|
||||
fills_surface_id,
|
||||
outset,
|
||||
);
|
||||
)?;
|
||||
}
|
||||
|
||||
// Skip stroke rendering for clipped frames - they are drawn in render_shape_exit
|
||||
@ -1249,7 +1252,7 @@ impl RenderState {
|
||||
Some(strokes_surface_id),
|
||||
antialias,
|
||||
outset,
|
||||
);
|
||||
)?;
|
||||
if !fast_mode {
|
||||
for stroke in &visible_strokes {
|
||||
shadows::render_stroke_inner_shadows(
|
||||
@ -1258,7 +1261,7 @@ impl RenderState {
|
||||
stroke,
|
||||
antialias,
|
||||
innershadows_surface_id,
|
||||
);
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1295,6 +1298,7 @@ impl RenderState {
|
||||
s.canvas().restore();
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update_render_context(&mut self, tile: tiles::Tile) {
|
||||
@ -1373,7 +1377,7 @@ impl RenderState {
|
||||
|
||||
/// Render a preview of the shapes during loading.
|
||||
/// This rebuilds tiles for touched shapes and renders synchronously.
|
||||
pub fn render_preview(&mut self, tree: ShapesPoolRef, timestamp: i32) -> Result<(), String> {
|
||||
pub fn render_preview(&mut self, tree: ShapesPoolRef, timestamp: i32) -> Result<()> {
|
||||
let _start = performance::begin_timed_log!("render_preview");
|
||||
performance::begin_measure!("render_preview");
|
||||
|
||||
@ -1403,7 +1407,7 @@ impl RenderState {
|
||||
tree: ShapesPoolRef,
|
||||
timestamp: i32,
|
||||
sync_render: bool,
|
||||
) -> Result<(), String> {
|
||||
) -> Result<()> {
|
||||
let _start = performance::begin_timed_log!("start_render_loop");
|
||||
let scale = self.get_scale();
|
||||
|
||||
@ -1430,7 +1434,7 @@ impl RenderState {
|
||||
|| viewbox_cache_size.height > cached_viewbox_cache_size.height
|
||||
{
|
||||
self.surfaces
|
||||
.resize_cache(viewbox_cache_size, VIEWPORT_INTEREST_AREA_THRESHOLD);
|
||||
.resize_cache(viewbox_cache_size, VIEWPORT_INTEREST_AREA_THRESHOLD)?;
|
||||
}
|
||||
|
||||
// FIXME - review debug
|
||||
@ -1475,7 +1479,7 @@ impl RenderState {
|
||||
base_object: Option<&Uuid>,
|
||||
tree: ShapesPoolRef,
|
||||
timestamp: i32,
|
||||
) -> Result<(), String> {
|
||||
) -> Result<()> {
|
||||
performance::begin_measure!("process_animation_frame");
|
||||
if self.render_in_progress {
|
||||
if tree.len() != 0 {
|
||||
@ -1499,7 +1503,7 @@ impl RenderState {
|
||||
base_object: Option<&Uuid>,
|
||||
tree: ShapesPoolRef,
|
||||
timestamp: i32,
|
||||
) -> Result<(), String> {
|
||||
) -> Result<()> {
|
||||
if tree.len() != 0 {
|
||||
self.render_shape_tree_partial(base_object, tree, timestamp, false)?;
|
||||
}
|
||||
@ -1589,7 +1593,7 @@ impl RenderState {
|
||||
element: &Shape,
|
||||
visited_mask: bool,
|
||||
clip_bounds: Option<ClipStack>,
|
||||
) {
|
||||
) -> Result<()> {
|
||||
if visited_mask {
|
||||
// Because masked groups needs two rendering passes (first drawing
|
||||
// the content and then drawing the mask), we need to do an
|
||||
@ -1660,7 +1664,7 @@ impl RenderState {
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
)?;
|
||||
}
|
||||
|
||||
// Only restore if we created a layer (optimization for simple shapes)
|
||||
@ -1672,19 +1676,22 @@ impl RenderState {
|
||||
}
|
||||
|
||||
self.focus_mode.exit(&element.id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_current_tile_bounds(&mut self) -> Rect {
|
||||
let tiles::Tile(tile_x, tile_y) = self.current_tile.unwrap();
|
||||
pub fn get_current_tile_bounds(&mut self) -> Result<Rect> {
|
||||
let tiles::Tile(tile_x, tile_y) = self
|
||||
.current_tile
|
||||
.ok_or(Error::CriticalError("Current tile not found".to_string()))?;
|
||||
let scale = self.get_scale();
|
||||
let offset_x = self.viewbox.area.left * scale;
|
||||
let offset_y = self.viewbox.area.top * scale;
|
||||
Rect::from_xywh(
|
||||
Ok(Rect::from_xywh(
|
||||
(tile_x as f32 * tiles::TILE_SIZE) - offset_x,
|
||||
(tile_y as f32 * tiles::TILE_SIZE) - offset_y,
|
||||
tiles::TILE_SIZE,
|
||||
tiles::TILE_SIZE,
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
pub fn get_rect_bounds(&mut self, rect: skia::Rect) -> Rect {
|
||||
@ -1732,8 +1739,11 @@ impl RenderState {
|
||||
// lower multiple of `TILE_SIZE`. This ensures the tile bounds are aligned
|
||||
// with the global tile grid, which is useful for rendering tiles in a
|
||||
/// consistent and predictable layout.
|
||||
pub fn get_current_aligned_tile_bounds(&mut self) -> Rect {
|
||||
self.get_aligned_tile_bounds(self.current_tile.unwrap())
|
||||
pub fn get_current_aligned_tile_bounds(&mut self) -> Result<Rect> {
|
||||
Ok(self.get_aligned_tile_bounds(
|
||||
self.current_tile
|
||||
.ok_or(Error::CriticalError("Current tile not found".to_string()))?,
|
||||
))
|
||||
}
|
||||
|
||||
/// Renders a drop shadow effect for the given shape.
|
||||
@ -1750,7 +1760,7 @@ impl RenderState {
|
||||
scale: f32,
|
||||
translation: (f32, f32),
|
||||
extra_layer_blur: Option<Blur>,
|
||||
) {
|
||||
) -> Result<()> {
|
||||
let mut transformed_shadow: Cow<Shadow> = Cow::Borrowed(shadow);
|
||||
transformed_shadow.to_mut().offset = (0.0, 0.0);
|
||||
transformed_shadow.to_mut().color = skia::Color::BLACK;
|
||||
@ -1805,7 +1815,7 @@ impl RenderState {
|
||||
plain_shape_mut.clip_content = false;
|
||||
|
||||
let Some(drop_filter) = transformed_shadow.get_drop_shadow_filter() else {
|
||||
return;
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let mut bounds = drop_filter.compute_fast_bounds(shape_bounds);
|
||||
@ -1813,7 +1823,7 @@ impl RenderState {
|
||||
bounds.offset(world_offset);
|
||||
// Early cull if the shadow bounds are outside the render area.
|
||||
if !bounds.intersects(self.render_area_with_margins) {
|
||||
return;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// blur=0 at high zoom: draw directly on DropShadows with geometric spread (no filter).
|
||||
@ -1835,11 +1845,11 @@ impl RenderState {
|
||||
Some(shadow.offset),
|
||||
None,
|
||||
Some(shadow.spread),
|
||||
);
|
||||
});
|
||||
)
|
||||
})?;
|
||||
|
||||
self.surfaces.canvas(SurfaceId::DropShadows).restore();
|
||||
return;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Create filter with blur only (no offset, no spread - handled geometrically)
|
||||
@ -1877,11 +1887,11 @@ impl RenderState {
|
||||
Some(shadow.offset), // Offset is geometric
|
||||
None,
|
||||
Some(shadow.spread),
|
||||
);
|
||||
});
|
||||
)
|
||||
})?;
|
||||
|
||||
self.surfaces.canvas(SurfaceId::DropShadows).restore();
|
||||
return;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Adaptive downscale for large blur values (lossless GPU optimization).
|
||||
@ -1918,12 +1928,13 @@ impl RenderState {
|
||||
Some(shadow.offset), // Offset is geometric
|
||||
None,
|
||||
Some(shadow.spread),
|
||||
);
|
||||
});
|
||||
)
|
||||
})?;
|
||||
|
||||
state.surfaces.canvas(temp_surface).restore();
|
||||
Ok(())
|
||||
},
|
||||
);
|
||||
)?;
|
||||
|
||||
if let Some((mut surface, filter_scale)) = filter_result {
|
||||
let drop_canvas = self.surfaces.canvas(SurfaceId::DropShadows);
|
||||
@ -1958,6 +1969,7 @@ impl RenderState {
|
||||
}
|
||||
drop_canvas.restore();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Renders element drop shadows to DropShadows surface and composites to Current.
|
||||
@ -1972,7 +1984,7 @@ impl RenderState {
|
||||
scale: f32,
|
||||
translation: (f32, f32),
|
||||
node_render_state: &NodeRenderState,
|
||||
) {
|
||||
) -> Result<()> {
|
||||
let element_extrect = extrect.get_or_insert_with(|| element.extrect(tree, scale));
|
||||
let inherited_layer_blur = match element.shape_type {
|
||||
Type::Frame(_) | Type::Group(_) => element.blur,
|
||||
@ -1994,7 +2006,7 @@ impl RenderState {
|
||||
scale,
|
||||
translation,
|
||||
None,
|
||||
);
|
||||
)?;
|
||||
|
||||
if !matches!(element.shape_type, Type::Bool(_)) {
|
||||
let shadow_children = if element.is_recursive() {
|
||||
@ -2023,7 +2035,7 @@ impl RenderState {
|
||||
scale,
|
||||
translation,
|
||||
inherited_layer_blur,
|
||||
);
|
||||
)?;
|
||||
} else {
|
||||
let paint = skia::Paint::default();
|
||||
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
|
||||
@ -2059,8 +2071,8 @@ impl RenderState {
|
||||
None,
|
||||
Some(vec![new_shadow_paint.clone()]),
|
||||
None,
|
||||
);
|
||||
});
|
||||
)
|
||||
})?;
|
||||
self.surfaces.canvas(SurfaceId::DropShadows).restore();
|
||||
}
|
||||
}
|
||||
@ -2117,6 +2129,7 @@ impl RenderState {
|
||||
self.surfaces
|
||||
.canvas(SurfaceId::DropShadows)
|
||||
.clear(skia::Color::TRANSPARENT);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn render_shape_tree_partial_uncached(
|
||||
@ -2124,7 +2137,7 @@ impl RenderState {
|
||||
tree: ShapesPoolRef,
|
||||
timestamp: i32,
|
||||
allow_stop: bool,
|
||||
) -> Result<(bool, bool), String> {
|
||||
) -> Result<(bool, bool)> {
|
||||
let mut iteration = 0;
|
||||
let mut is_empty = true;
|
||||
|
||||
@ -2152,7 +2165,7 @@ impl RenderState {
|
||||
|
||||
if visited_children {
|
||||
if !node_render_state.flattened {
|
||||
self.render_shape_exit(element, visited_mask, clip_bounds);
|
||||
self.render_shape_exit(element, visited_mask, clip_bounds)?;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@ -2226,7 +2239,13 @@ impl RenderState {
|
||||
scale,
|
||||
translation,
|
||||
&node_render_state,
|
||||
);
|
||||
)?;
|
||||
}
|
||||
|
||||
// Render background blur BEFORE save_layer so it modifies
|
||||
// the backdrop independently of the shape's opacity.
|
||||
if !node_render_state.is_root() && self.focus_mode.is_active() {
|
||||
self.render_background_blur(element);
|
||||
}
|
||||
|
||||
// Render background blur BEFORE save_layer so it modifies
|
||||
@ -2262,7 +2281,7 @@ impl RenderState {
|
||||
scale,
|
||||
translation,
|
||||
&node_render_state,
|
||||
);
|
||||
)?;
|
||||
}
|
||||
|
||||
self.render_shape(
|
||||
@ -2276,7 +2295,7 @@ impl RenderState {
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
)?;
|
||||
|
||||
self.surfaces
|
||||
.canvas(SurfaceId::DropShadows)
|
||||
@ -2373,14 +2392,14 @@ impl RenderState {
|
||||
tree: ShapesPoolRef,
|
||||
timestamp: i32,
|
||||
allow_stop: bool,
|
||||
) -> Result<(), String> {
|
||||
) -> Result<()> {
|
||||
let mut should_stop = false;
|
||||
let root_ids = {
|
||||
if let Some(shape_id) = base_object {
|
||||
vec![*shape_id]
|
||||
} else {
|
||||
let Some(root) = tree.get(&Uuid::nil()) else {
|
||||
return Err(String::from("Root shape not found"));
|
||||
return Err(Error::CriticalError("Root shape not found".to_string()));
|
||||
};
|
||||
root.children_ids(false)
|
||||
}
|
||||
@ -2390,7 +2409,7 @@ impl RenderState {
|
||||
if let Some(current_tile) = self.current_tile {
|
||||
if self.surfaces.has_cached_tile_surface(current_tile) {
|
||||
performance::begin_measure!("render_shape_tree::cached");
|
||||
let tile_rect = self.get_current_tile_bounds();
|
||||
let tile_rect = self.get_current_tile_bounds()?;
|
||||
self.surfaces.draw_cached_tile_surface(
|
||||
current_tile,
|
||||
tile_rect,
|
||||
@ -2420,9 +2439,9 @@ impl RenderState {
|
||||
return Ok(());
|
||||
}
|
||||
performance::end_measure!("render_shape_tree::uncached");
|
||||
let tile_rect = self.get_current_tile_bounds();
|
||||
let tile_rect = self.get_current_tile_bounds()?;
|
||||
if !is_empty {
|
||||
self.apply_render_to_final_canvas(tile_rect);
|
||||
self.apply_render_to_final_canvas(tile_rect)?;
|
||||
|
||||
if self.options.is_debug_visible() {
|
||||
debug::render_workspace_current_tile(
|
||||
@ -2760,7 +2779,11 @@ impl RenderState {
|
||||
///
|
||||
/// This is useful when you have a pre-computed set of shape IDs that need to be refreshed,
|
||||
/// regardless of their relationship to other shapes (e.g., ancestors, descendants, or any other collection).
|
||||
pub fn update_tiles_shapes(&mut self, shape_ids: &[Uuid], tree: ShapesPoolMutRef<'_>) {
|
||||
pub fn update_tiles_shapes(
|
||||
&mut self,
|
||||
shape_ids: &[Uuid],
|
||||
tree: ShapesPoolMutRef<'_>,
|
||||
) -> Result<()> {
|
||||
performance::begin_measure!("invalidate_and_update_tiles");
|
||||
let mut all_tiles = HashSet::<tiles::Tile>::new();
|
||||
for shape_id in shape_ids {
|
||||
@ -2772,6 +2795,7 @@ impl RenderState {
|
||||
self.remove_cached_tile(tile);
|
||||
}
|
||||
performance::end_measure!("invalidate_and_update_tiles");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Rebuilds tiles for shapes with modifiers and processes their ancestors
|
||||
@ -2780,9 +2804,14 @@ impl RenderState {
|
||||
/// Additionally, it processes all ancestors of modified shapes to ensure their
|
||||
/// extended rectangles are properly recalculated and their tiles are updated.
|
||||
/// This is crucial for frames and groups that contain transformed children.
|
||||
pub fn rebuild_modifier_tiles(&mut self, tree: ShapesPoolMutRef<'_>, ids: Vec<Uuid>) {
|
||||
pub fn rebuild_modifier_tiles(
|
||||
&mut self,
|
||||
tree: ShapesPoolMutRef<'_>,
|
||||
ids: Vec<Uuid>,
|
||||
) -> Result<()> {
|
||||
let ancestors = all_with_ancestors(&ids, tree, false);
|
||||
self.update_tiles_shapes(&ancestors, tree);
|
||||
self.update_tiles_shapes(&ancestors, tree)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_scale(&self) -> f32 {
|
||||
|
||||
@ -179,9 +179,12 @@ pub fn render_debug_shape(
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[allow(dead_code)]
|
||||
pub fn console_debug_surface(render_state: &mut RenderState, id: SurfaceId) {
|
||||
let base64_image = render_state.surfaces.base64_snapshot(id);
|
||||
let base64_image = render_state
|
||||
.surfaces
|
||||
.base64_snapshot(id)
|
||||
.expect("Failed to get base64 image");
|
||||
|
||||
run_script!(format!("console.log('%c ', 'font-size: 1px; background: url(data:image/png;base64,{base64_image}) no-repeat; padding: 100px; background-size: contain;')"))
|
||||
run_script!(format!("console.log('%c ', 'font-size: 1px; background: url(data:image/png;base64,{base64_image}) no-repeat; padding: 100px; background-size: contain;')"));
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
@ -194,7 +197,10 @@ pub fn console_debug_surface_rect(render_state: &mut RenderState, id: SurfaceId,
|
||||
rect.bottom as i32,
|
||||
);
|
||||
|
||||
let base64_image = render_state.surfaces.base64_snapshot_rect(id, int_rect);
|
||||
let base64_image = render_state
|
||||
.surfaces
|
||||
.base64_snapshot_rect(id, int_rect)
|
||||
.expect("Failed to get base64 image");
|
||||
|
||||
if let Some(base64_image) = base64_image {
|
||||
run_script!(format!("console.log('%c ', 'font-size: 1px; background: url(data:image/png;base64,{base64_image}) no-repeat; padding: 100px; background-size: contain;')"))
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
use skia_safe::{self as skia, Paint, RRect};
|
||||
|
||||
use super::{filters, RenderState, SurfaceId};
|
||||
use crate::error::Result;
|
||||
use crate::render::get_source_rect;
|
||||
use crate::shapes::{merge_fills, Fill, Frame, ImageFill, Rect, Shape, StrokeKind, Type};
|
||||
|
||||
@ -20,12 +21,11 @@ fn draw_image_fill(
|
||||
antialias: bool,
|
||||
surface_id: SurfaceId,
|
||||
) {
|
||||
let image = render_state.images.get(&image_fill.id());
|
||||
if image.is_none() {
|
||||
let Some(image) = render_state.images.get(&image_fill.id()) else {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let size = image.unwrap().dimensions();
|
||||
let size = image.dimensions();
|
||||
let canvas = render_state.surfaces.canvas_and_mark_dirty(surface_id);
|
||||
let container = &shape.selrect;
|
||||
let path_transform = shape.to_path_transform();
|
||||
@ -85,15 +85,13 @@ fn draw_image_fill(
|
||||
}
|
||||
|
||||
// Draw the image with the calculated destination rectangle
|
||||
if let Some(image) = image {
|
||||
canvas.draw_image_rect_with_sampling_options(
|
||||
image,
|
||||
Some((&src_rect, skia::canvas::SrcRectConstraint::Strict)),
|
||||
dest_rect,
|
||||
render_state.sampling_options,
|
||||
paint,
|
||||
);
|
||||
}
|
||||
canvas.draw_image_rect_with_sampling_options(
|
||||
image,
|
||||
Some((&src_rect, skia::canvas::SrcRectConstraint::Strict)),
|
||||
dest_rect,
|
||||
render_state.sampling_options,
|
||||
paint,
|
||||
);
|
||||
|
||||
// Restore the canvas to remove the clipping
|
||||
canvas.restore();
|
||||
@ -109,9 +107,9 @@ pub fn render(
|
||||
antialias: bool,
|
||||
surface_id: SurfaceId,
|
||||
outset: Option<f32>,
|
||||
) {
|
||||
) -> Result<()> {
|
||||
if fills.is_empty() {
|
||||
return;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let scale = render_state.get_scale().max(1e-6);
|
||||
@ -134,9 +132,9 @@ pub fn render(
|
||||
surface_id,
|
||||
outset,
|
||||
inset,
|
||||
);
|
||||
)?;
|
||||
}
|
||||
return;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut paint = merge_fills(fills, shape.selrect);
|
||||
@ -152,15 +150,17 @@ pub fn render(
|
||||
let mut filtered_paint = paint.clone();
|
||||
filtered_paint.set_image_filter(image_filter.clone());
|
||||
draw_fill_to_surface(state, shape, temp_surface, &filtered_paint, outset, inset);
|
||||
Ok(())
|
||||
},
|
||||
) {
|
||||
return;
|
||||
)? {
|
||||
return Ok(());
|
||||
} else {
|
||||
paint.set_image_filter(image_filter);
|
||||
}
|
||||
}
|
||||
|
||||
draw_fill_to_surface(render_state, shape, surface_id, &paint, outset, inset);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Draws a single paint (with a merged shader) to the appropriate surface
|
||||
@ -203,7 +203,7 @@ fn render_single_fill(
|
||||
surface_id: SurfaceId,
|
||||
outset: Option<f32>,
|
||||
inset: Option<f32>,
|
||||
) {
|
||||
) -> Result<()> {
|
||||
let mut paint = fill.to_paint(&shape.selrect, antialias);
|
||||
if let Some(image_filter) = shape.image_filter(1.) {
|
||||
let bounds = image_filter.compute_fast_bounds(shape.selrect);
|
||||
@ -224,9 +224,10 @@ fn render_single_fill(
|
||||
outset,
|
||||
inset,
|
||||
);
|
||||
Ok(())
|
||||
},
|
||||
) {
|
||||
return;
|
||||
)? {
|
||||
return Ok(());
|
||||
} else {
|
||||
paint.set_image_filter(image_filter);
|
||||
}
|
||||
@ -242,6 +243,7 @@ fn render_single_fill(
|
||||
outset,
|
||||
inset,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
use skia_safe::{self as skia, ImageFilter, Rect};
|
||||
|
||||
use super::{RenderState, SurfaceId};
|
||||
use crate::error::Result;
|
||||
|
||||
/// Composes two image filters, returning a combined filter if both are present,
|
||||
/// or the individual filter if only one is present, or None if neither is present.
|
||||
@ -36,12 +37,12 @@ pub fn render_with_filter_surface<F>(
|
||||
bounds: Rect,
|
||||
target_surface: SurfaceId,
|
||||
draw_fn: F,
|
||||
) -> bool
|
||||
) -> Result<bool>
|
||||
where
|
||||
F: FnOnce(&mut RenderState, SurfaceId),
|
||||
F: FnOnce(&mut RenderState, SurfaceId) -> Result<()>,
|
||||
{
|
||||
if let Some((mut surface, scale)) =
|
||||
render_into_filter_surface(render_state, bounds, 1.0, draw_fn)
|
||||
render_into_filter_surface(render_state, bounds, 1.0, draw_fn)?
|
||||
{
|
||||
let canvas = render_state.surfaces.canvas_and_mark_dirty(target_surface);
|
||||
|
||||
@ -58,9 +59,9 @@ where
|
||||
surface.draw(canvas, (0.0, 0.0), render_state.sampling_options, None);
|
||||
canvas.restore();
|
||||
}
|
||||
true
|
||||
Ok(true)
|
||||
} else {
|
||||
false
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
@ -81,12 +82,12 @@ pub fn render_into_filter_surface<F>(
|
||||
bounds: Rect,
|
||||
extra_downscale: f32,
|
||||
draw_fn: F,
|
||||
) -> Option<(skia::Surface, f32)>
|
||||
) -> Result<Option<(skia::Surface, f32)>>
|
||||
where
|
||||
F: FnOnce(&mut RenderState, SurfaceId),
|
||||
F: FnOnce(&mut RenderState, SurfaceId) -> Result<()>,
|
||||
{
|
||||
if !bounds.is_finite() || bounds.width() <= 0.0 || bounds.height() <= 0.0 {
|
||||
return None;
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let filter_id = SurfaceId::Filter;
|
||||
@ -125,10 +126,10 @@ where
|
||||
canvas.translate((-bounds.left, -bounds.top));
|
||||
}
|
||||
|
||||
draw_fn(render_state, filter_id);
|
||||
draw_fn(render_state, filter_id)?;
|
||||
|
||||
render_state.surfaces.canvas(filter_id).restore();
|
||||
|
||||
let filter_surface = render_state.surfaces.surface_clone(filter_id);
|
||||
Some((filter_surface, scale))
|
||||
Ok(Some((filter_surface, scale)))
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
use skia_safe::{self as skia, textlayout, Font, FontMgr};
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::shapes::{FontFamily, FontStyle};
|
||||
use crate::uuid::Uuid;
|
||||
|
||||
@ -26,7 +27,7 @@ pub struct FontStore {
|
||||
}
|
||||
|
||||
impl FontStore {
|
||||
pub fn new() -> Self {
|
||||
pub fn try_new() -> Result<Self> {
|
||||
let font_mgr = FontMgr::new();
|
||||
let font_provider = load_default_provider(&font_mgr);
|
||||
let mut font_collection = skia::textlayout::FontCollection::new();
|
||||
@ -34,17 +35,19 @@ impl FontStore {
|
||||
|
||||
let debug_typeface = font_provider
|
||||
.match_family_style(default_font().as_str(), skia::FontStyle::default())
|
||||
.unwrap();
|
||||
.ok_or(Error::CriticalError(
|
||||
"Failed to match default font".to_string(),
|
||||
))?;
|
||||
|
||||
let debug_font = skia::Font::new(debug_typeface, 10.0);
|
||||
|
||||
Self {
|
||||
Ok(Self {
|
||||
font_mgr,
|
||||
font_provider,
|
||||
font_collection,
|
||||
debug_font,
|
||||
fallback_fonts: HashSet::new(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_scale_debug_font(&mut self, dpr: f32) {
|
||||
@ -70,7 +73,7 @@ impl FontStore {
|
||||
font_data: &[u8],
|
||||
is_emoji: bool,
|
||||
is_fallback: bool,
|
||||
) -> Result<(), String> {
|
||||
) -> Result<()> {
|
||||
if self.has_family(&family, is_emoji) {
|
||||
return Ok(());
|
||||
}
|
||||
@ -78,7 +81,9 @@ impl FontStore {
|
||||
let typeface = self
|
||||
.font_mgr
|
||||
.new_from_data(font_data, None)
|
||||
.ok_or("Failed to create typeface")?;
|
||||
.ok_or(Error::CriticalError(
|
||||
"Failed to create typeface".to_string(),
|
||||
))?;
|
||||
|
||||
let alias = format!("{}", family);
|
||||
let font_name = if is_emoji {
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
use crate::error::{Error, Result};
|
||||
use skia_safe::gpu::{self, gl::FramebufferInfo, gl::TextureInfo, DirectContext};
|
||||
use skia_safe::{self as skia, ISize};
|
||||
|
||||
@ -8,24 +9,30 @@ pub struct GpuState {
|
||||
}
|
||||
|
||||
impl GpuState {
|
||||
pub fn new() -> Self {
|
||||
let interface = gpu::gl::Interface::new_native().unwrap();
|
||||
let context = gpu::direct_contexts::make_gl(interface, None).unwrap();
|
||||
pub fn try_new() -> Result<Self> {
|
||||
let interface = gpu::gl::Interface::new_native().ok_or(Error::CriticalError(
|
||||
"Failed to create GL interface".to_string(),
|
||||
))?;
|
||||
let context = gpu::direct_contexts::make_gl(interface, None).ok_or(
|
||||
Error::CriticalError("Failed to create GL context".to_string()),
|
||||
)?;
|
||||
let framebuffer_info = {
|
||||
let mut fboid: gl::types::GLint = 0;
|
||||
unsafe { gl::GetIntegerv(gl::FRAMEBUFFER_BINDING, &mut fboid) };
|
||||
|
||||
FramebufferInfo {
|
||||
fboid: fboid.try_into().unwrap(),
|
||||
fboid: fboid.try_into().map_err(|_| {
|
||||
Error::CriticalError("Failed to convert GL framebuffer ID to u32".to_string())
|
||||
})?,
|
||||
format: gpu::gl::Format::RGBA8.into(),
|
||||
protected: gpu::Protected::No,
|
||||
}
|
||||
};
|
||||
|
||||
GpuState {
|
||||
Ok(GpuState {
|
||||
context,
|
||||
framebuffer_info,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn create_webgl_texture(&mut self, width: i32, height: i32) -> gl::types::GLuint {
|
||||
@ -56,7 +63,11 @@ impl GpuState {
|
||||
texture_id
|
||||
}
|
||||
|
||||
pub fn create_surface_with_isize(&mut self, label: String, size: ISize) -> skia::Surface {
|
||||
pub fn create_surface_with_isize(
|
||||
&mut self,
|
||||
label: String,
|
||||
size: ISize,
|
||||
) -> Result<skia::Surface> {
|
||||
self.create_surface_with_dimensions(label, size.width, size.height)
|
||||
}
|
||||
|
||||
@ -65,7 +76,7 @@ impl GpuState {
|
||||
label: String,
|
||||
width: i32,
|
||||
height: i32,
|
||||
) -> skia::Surface {
|
||||
) -> Result<skia::Surface> {
|
||||
let backend_texture = unsafe {
|
||||
let texture_id = self.create_webgl_texture(width, height);
|
||||
let texture_info = TextureInfo {
|
||||
@ -77,7 +88,7 @@ impl GpuState {
|
||||
gpu::backend_textures::make_gl((width, height), gpu::Mipmapped::No, texture_info, label)
|
||||
};
|
||||
|
||||
gpu::surfaces::wrap_backend_texture(
|
||||
let surface = gpu::surfaces::wrap_backend_texture(
|
||||
&mut self.context,
|
||||
&backend_texture,
|
||||
gpu::SurfaceOrigin::BottomLeft,
|
||||
@ -86,15 +97,19 @@ impl GpuState {
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap()
|
||||
.ok_or(Error::CriticalError(
|
||||
"Failed to create Skia surface".to_string(),
|
||||
))?;
|
||||
|
||||
Ok(surface)
|
||||
}
|
||||
|
||||
/// Create a Skia surface that will be used for rendering.
|
||||
pub fn create_target_surface(&mut self, width: i32, height: i32) -> skia::Surface {
|
||||
pub fn create_target_surface(&mut self, width: i32, height: i32) -> Result<skia::Surface> {
|
||||
let backend_render_target =
|
||||
gpu::backend_render_targets::make_gl((width, height), 1, 8, self.framebuffer_info);
|
||||
|
||||
gpu::surfaces::wrap_backend_render_target(
|
||||
let surface = gpu::surfaces::wrap_backend_render_target(
|
||||
&mut self.context,
|
||||
&backend_render_target,
|
||||
gpu::SurfaceOrigin::BottomLeft,
|
||||
@ -102,6 +117,10 @@ impl GpuState {
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap()
|
||||
.ok_or(Error::CriticalError(
|
||||
"Failed to create Skia surface".to_string(),
|
||||
))?;
|
||||
|
||||
Ok(surface)
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ use crate::math::Rect as MathRect;
|
||||
use crate::shapes::ImageFill;
|
||||
use crate::uuid::Uuid;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use skia_safe::gpu::{surfaces, Budgeted, DirectContext};
|
||||
use skia_safe::{self as skia, Codec, ISize};
|
||||
use std::collections::HashMap;
|
||||
@ -70,7 +71,7 @@ fn create_image_from_gl_texture(
|
||||
texture_id: u32,
|
||||
width: i32,
|
||||
height: i32,
|
||||
) -> Result<Image, String> {
|
||||
) -> Result<Image> {
|
||||
use skia_safe::gpu;
|
||||
use skia_safe::gpu::gl::TextureInfo;
|
||||
|
||||
@ -99,7 +100,9 @@ fn create_image_from_gl_texture(
|
||||
skia::AlphaType::Premul,
|
||||
None,
|
||||
)
|
||||
.ok_or("Failed to create Skia image from GL texture")?;
|
||||
.ok_or(crate::error::Error::CriticalError(
|
||||
"Failed to create Skia image from GL texture".to_string(),
|
||||
))?;
|
||||
|
||||
Ok(image)
|
||||
}
|
||||
@ -147,11 +150,16 @@ impl ImageStore {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add(&mut self, id: Uuid, is_thumbnail: bool, image_data: &[u8]) -> Result<(), String> {
|
||||
pub fn add(
|
||||
&mut self,
|
||||
id: Uuid,
|
||||
is_thumbnail: bool,
|
||||
image_data: &[u8],
|
||||
) -> crate::error::Result<()> {
|
||||
let key = (id, is_thumbnail);
|
||||
|
||||
if self.images.contains_key(&key) {
|
||||
return Err("Image already exists".to_string());
|
||||
return Err(Error::RecoverableError("Image already exists".to_string()));
|
||||
}
|
||||
|
||||
let raw_data = image_data.to_vec();
|
||||
@ -174,11 +182,11 @@ impl ImageStore {
|
||||
texture_id: u32,
|
||||
width: i32,
|
||||
height: i32,
|
||||
) -> Result<(), String> {
|
||||
) -> Result<()> {
|
||||
let key = (id, is_thumbnail);
|
||||
|
||||
if self.images.contains_key(&key) {
|
||||
return Err("Image already exists".to_string());
|
||||
return Err(Error::RecoverableError("Image already exists".to_string()));
|
||||
}
|
||||
|
||||
// Create a Skia image from the existing GL texture
|
||||
|
||||
@ -3,6 +3,7 @@ use crate::render::strokes;
|
||||
use crate::shapes::{ParagraphBuilderGroup, Shadow, Shape, Stroke, Type};
|
||||
use skia_safe::{canvas::SaveLayerRec, Paint, Path};
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::render::text;
|
||||
|
||||
// Fill Shadows
|
||||
@ -36,7 +37,7 @@ pub fn render_stroke_inner_shadows(
|
||||
stroke: &Stroke,
|
||||
antialias: bool,
|
||||
surface_id: SurfaceId,
|
||||
) {
|
||||
) -> Result<()> {
|
||||
if !shape.has_fills() {
|
||||
for shadow in shape.inner_shadows_visible() {
|
||||
let filter = shadow.get_inner_shadow_filter();
|
||||
@ -48,9 +49,10 @@ pub fn render_stroke_inner_shadows(
|
||||
filter.as_ref(),
|
||||
antialias,
|
||||
None, // Inner shadows don't use spread
|
||||
)
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Render text paths (unused)
|
||||
@ -133,9 +135,9 @@ pub fn render_text_shadows(
|
||||
surface_id: Option<SurfaceId>,
|
||||
shadows: &[Paint],
|
||||
blur_filter: &Option<skia_safe::ImageFilter>,
|
||||
) {
|
||||
) -> Result<()> {
|
||||
if stroke_paragraphs_group.is_empty() {
|
||||
return;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let canvas = render_state
|
||||
@ -156,7 +158,7 @@ pub fn render_text_shadows(
|
||||
blur_filter.as_ref(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
)?;
|
||||
|
||||
for stroke_paragraphs in stroke_paragraphs_group.iter_mut() {
|
||||
text::render(
|
||||
@ -169,9 +171,10 @@ pub fn render_text_shadows(
|
||||
blur_filter.as_ref(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
)?;
|
||||
}
|
||||
|
||||
canvas.restore();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ use crate::shapes::{
|
||||
use skia_safe::{self as skia, ImageFilter, RRect};
|
||||
|
||||
use super::{filters, RenderState, SurfaceId};
|
||||
use crate::error::{Error, Result};
|
||||
use crate::render::filters::compose_filters;
|
||||
use crate::render::{get_dest_rect, get_source_rect};
|
||||
|
||||
@ -294,16 +295,16 @@ fn handle_stroke_caps(
|
||||
blur: Option<&ImageFilter>,
|
||||
_antialias: bool,
|
||||
) {
|
||||
let mut points = path.points().to_vec();
|
||||
// Curves can have duplicated points, so let's remove consecutive duplicated points
|
||||
points.dedup();
|
||||
let c_points = points.len();
|
||||
|
||||
// Closed shapes don't have caps
|
||||
if c_points >= 2 && is_open {
|
||||
let first_point = points.first().unwrap();
|
||||
let last_point = points.last().unwrap();
|
||||
if !is_open {
|
||||
return;
|
||||
}
|
||||
|
||||
// Curves can have duplicated points, so let's remove consecutive duplicated points
|
||||
let mut points = path.points().to_vec();
|
||||
points.dedup();
|
||||
|
||||
if let [first_point, .., last_point] = points.as_slice() {
|
||||
let mut paint_stroke = paint.clone();
|
||||
|
||||
if let Some(filter) = blur {
|
||||
@ -328,7 +329,7 @@ fn handle_stroke_caps(
|
||||
stroke.width,
|
||||
&mut paint_stroke,
|
||||
last_point,
|
||||
&points[c_points - 2],
|
||||
&points[points.len() - 2],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -456,14 +457,13 @@ fn draw_image_stroke_in_container(
|
||||
image_fill: &ImageFill,
|
||||
antialias: bool,
|
||||
surface_id: SurfaceId,
|
||||
) {
|
||||
) -> Result<()> {
|
||||
let scale = render_state.get_scale();
|
||||
let image = render_state.images.get(&image_fill.id());
|
||||
if image.is_none() {
|
||||
return;
|
||||
}
|
||||
let Some(image) = render_state.images.get(&image_fill.id()) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let size = image.unwrap().dimensions();
|
||||
let size = image.dimensions();
|
||||
let canvas = render_state.surfaces.canvas_and_mark_dirty(surface_id);
|
||||
let container = &shape.selrect;
|
||||
let path_transform = shape.to_path_transform();
|
||||
@ -509,7 +509,10 @@ fn draw_image_stroke_in_container(
|
||||
shape_type @ (Type::Path(_) | Type::Bool(_)) => {
|
||||
if let Some(p) = shape_type.path() {
|
||||
canvas.save();
|
||||
let path = p.to_skia_path().make_transform(&path_transform.unwrap());
|
||||
|
||||
let path = p.to_skia_path().make_transform(
|
||||
&path_transform.ok_or(Error::CriticalError("No path transform".to_string()))?,
|
||||
);
|
||||
let stroke_kind = stroke.render_kind(p.is_open());
|
||||
match stroke_kind {
|
||||
StrokeKind::Inner => {
|
||||
@ -561,7 +564,7 @@ fn draw_image_stroke_in_container(
|
||||
|
||||
canvas.clip_rect(dest_rect, skia::ClipOp::Intersect, antialias);
|
||||
canvas.draw_image_rect_with_sampling_options(
|
||||
image.unwrap(),
|
||||
image,
|
||||
Some((&src_rect, skia::canvas::SrcRectConstraint::Strict)),
|
||||
dest_rect,
|
||||
render_state.sampling_options,
|
||||
@ -571,7 +574,9 @@ fn draw_image_stroke_in_container(
|
||||
// Clear outer stroke for paths if necessary. When adding an outer stroke we need to empty the stroke added too in the inner area.
|
||||
if let Type::Path(p) = &shape.shape_type {
|
||||
if stroke.render_kind(p.is_open()) == StrokeKind::Outer {
|
||||
let path = p.to_skia_path().make_transform(&path_transform.unwrap());
|
||||
let path = p.to_skia_path().make_transform(
|
||||
&path_transform.ok_or(Error::CriticalError("No path transform".to_string()))?,
|
||||
);
|
||||
let mut clear_paint = skia::Paint::default();
|
||||
clear_paint.set_blend_mode(skia::BlendMode::Clear);
|
||||
clear_paint.set_anti_alias(antialias);
|
||||
@ -581,6 +586,7 @@ fn draw_image_stroke_in_container(
|
||||
|
||||
// Restore canvas state
|
||||
canvas.restore();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Renders all strokes for a shape. Merges strokes that share the same
|
||||
@ -593,9 +599,9 @@ pub fn render(
|
||||
surface_id: Option<SurfaceId>,
|
||||
antialias: bool,
|
||||
outset: Option<f32>,
|
||||
) {
|
||||
) -> Result<()> {
|
||||
if strokes.is_empty() {
|
||||
return;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let has_image_fills = strokes.iter().any(|s| matches!(s.fill, Fill::Image(_)));
|
||||
@ -655,13 +661,14 @@ pub fn render(
|
||||
true,
|
||||
true,
|
||||
outset,
|
||||
);
|
||||
)?;
|
||||
}
|
||||
|
||||
state.surfaces.canvas(temp_surface).restore();
|
||||
Ok(())
|
||||
},
|
||||
) {
|
||||
return;
|
||||
)? {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
@ -675,9 +682,9 @@ pub fn render(
|
||||
None,
|
||||
antialias,
|
||||
outset,
|
||||
);
|
||||
)?;
|
||||
}
|
||||
return;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
render_merged(
|
||||
@ -688,7 +695,7 @@ pub fn render(
|
||||
antialias,
|
||||
false,
|
||||
outset,
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
fn strokes_share_geometry(strokes: &[&Stroke]) -> bool {
|
||||
@ -709,7 +716,7 @@ fn render_merged(
|
||||
antialias: bool,
|
||||
bypass_filter: bool,
|
||||
outset: Option<f32>,
|
||||
) {
|
||||
) -> Result<()> {
|
||||
let representative = *strokes
|
||||
.last()
|
||||
.expect("render_merged expects at least one stroke");
|
||||
@ -761,14 +768,15 @@ fn render_merged(
|
||||
antialias,
|
||||
true,
|
||||
outset,
|
||||
);
|
||||
)?;
|
||||
|
||||
state.surfaces.apply_mut(temp_surface as u32, |surface| {
|
||||
surface.canvas().restore();
|
||||
});
|
||||
Ok(())
|
||||
},
|
||||
) {
|
||||
return;
|
||||
)? {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -844,6 +852,7 @@ fn render_merged(
|
||||
}
|
||||
_ => unreachable!("This shape should not have strokes"),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Renders a single stroke. Used by the shadow module which needs per-stroke
|
||||
@ -857,7 +866,7 @@ pub fn render_single(
|
||||
shadow: Option<&ImageFilter>,
|
||||
antialias: bool,
|
||||
outset: Option<f32>,
|
||||
) {
|
||||
) -> Result<()> {
|
||||
render_single_internal(
|
||||
render_state,
|
||||
shape,
|
||||
@ -868,7 +877,7 @@ pub fn render_single(
|
||||
false,
|
||||
false,
|
||||
outset,
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
@ -882,7 +891,7 @@ fn render_single_internal(
|
||||
bypass_filter: bool,
|
||||
skip_blur: bool,
|
||||
outset: Option<f32>,
|
||||
) {
|
||||
) -> Result<()> {
|
||||
if !bypass_filter {
|
||||
if let Some(image_filter) = shape.image_filter(1.) {
|
||||
let mut content_bounds = shape.selrect;
|
||||
@ -916,10 +925,10 @@ fn render_single_internal(
|
||||
true,
|
||||
true,
|
||||
outset,
|
||||
);
|
||||
)
|
||||
},
|
||||
) {
|
||||
return;
|
||||
)? {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -949,7 +958,7 @@ fn render_single_internal(
|
||||
image_fill,
|
||||
antialias,
|
||||
target_surface,
|
||||
);
|
||||
)?;
|
||||
}
|
||||
} else {
|
||||
match &shape.shape_type {
|
||||
@ -1014,6 +1023,7 @@ fn render_single_internal(
|
||||
_ => unreachable!("This shape should not have strokes"),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Render text paths (unused)
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
use crate::error::{Error, Result};
|
||||
use crate::performance;
|
||||
use crate::shapes::Shape;
|
||||
|
||||
@ -61,38 +62,39 @@ pub struct Surfaces {
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl Surfaces {
|
||||
pub fn new(
|
||||
pub fn try_new(
|
||||
gpu_state: &mut GpuState,
|
||||
(width, height): (i32, i32),
|
||||
sampling_options: skia::SamplingOptions,
|
||||
tile_dims: skia::ISize,
|
||||
) -> Self {
|
||||
) -> Result<Self> {
|
||||
let extra_tile_dims = skia::ISize::new(
|
||||
tile_dims.width * TILE_SIZE_MULTIPLIER,
|
||||
tile_dims.height * TILE_SIZE_MULTIPLIER,
|
||||
);
|
||||
let margins = skia::ISize::new(extra_tile_dims.width / 4, extra_tile_dims.height / 4);
|
||||
|
||||
let target = gpu_state.create_target_surface(width, height);
|
||||
let filter = gpu_state.create_surface_with_isize("filter".to_string(), extra_tile_dims);
|
||||
let cache = gpu_state.create_surface_with_dimensions("cache".to_string(), width, height);
|
||||
let current = gpu_state.create_surface_with_isize("current".to_string(), extra_tile_dims);
|
||||
let target = gpu_state.create_target_surface(width, height)?;
|
||||
let filter = gpu_state.create_surface_with_isize("filter".to_string(), extra_tile_dims)?;
|
||||
let cache = gpu_state.create_surface_with_dimensions("cache".to_string(), width, height)?;
|
||||
let current =
|
||||
gpu_state.create_surface_with_isize("current".to_string(), extra_tile_dims)?;
|
||||
let drop_shadows =
|
||||
gpu_state.create_surface_with_isize("drop_shadows".to_string(), extra_tile_dims);
|
||||
gpu_state.create_surface_with_isize("drop_shadows".to_string(), extra_tile_dims)?;
|
||||
let inner_shadows =
|
||||
gpu_state.create_surface_with_isize("inner_shadows".to_string(), extra_tile_dims);
|
||||
let text_drop_shadows =
|
||||
gpu_state.create_surface_with_isize("text_drop_shadows".to_string(), extra_tile_dims);
|
||||
gpu_state.create_surface_with_isize("inner_shadows".to_string(), extra_tile_dims)?;
|
||||
let text_drop_shadows = gpu_state
|
||||
.create_surface_with_isize("text_drop_shadows".to_string(), extra_tile_dims)?;
|
||||
let shape_fills =
|
||||
gpu_state.create_surface_with_isize("shape_fills".to_string(), extra_tile_dims);
|
||||
gpu_state.create_surface_with_isize("shape_fills".to_string(), extra_tile_dims)?;
|
||||
let shape_strokes =
|
||||
gpu_state.create_surface_with_isize("shape_strokes".to_string(), extra_tile_dims);
|
||||
gpu_state.create_surface_with_isize("shape_strokes".to_string(), extra_tile_dims)?;
|
||||
|
||||
let ui = gpu_state.create_surface_with_dimensions("ui".to_string(), width, height);
|
||||
let debug = gpu_state.create_surface_with_dimensions("debug".to_string(), width, height);
|
||||
let ui = gpu_state.create_surface_with_dimensions("ui".to_string(), width, height)?;
|
||||
let debug = gpu_state.create_surface_with_dimensions("debug".to_string(), width, height)?;
|
||||
|
||||
let tiles = TileTextureCache::new();
|
||||
Surfaces {
|
||||
Ok(Surfaces {
|
||||
target,
|
||||
filter,
|
||||
cache,
|
||||
@ -108,7 +110,7 @@ impl Surfaces {
|
||||
sampling_options,
|
||||
margins,
|
||||
dirty_surfaces: 0,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn clear_tiles(&mut self) {
|
||||
@ -119,8 +121,14 @@ impl Surfaces {
|
||||
self.margins
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, gpu_state: &mut GpuState, new_width: i32, new_height: i32) {
|
||||
self.reset_from_target(gpu_state.create_target_surface(new_width, new_height));
|
||||
pub fn resize(
|
||||
&mut self,
|
||||
gpu_state: &mut GpuState,
|
||||
new_width: i32,
|
||||
new_height: i32,
|
||||
) -> Result<()> {
|
||||
self.reset_from_target(gpu_state.create_target_surface(new_width, new_height)?)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn snapshot(&mut self, id: SurfaceId) -> skia::Image {
|
||||
@ -132,26 +140,33 @@ impl Surfaces {
|
||||
(self.filter.width(), self.filter.height())
|
||||
}
|
||||
|
||||
pub fn base64_snapshot(&mut self, id: SurfaceId) -> String {
|
||||
pub fn base64_snapshot(&mut self, id: SurfaceId) -> Result<String> {
|
||||
let surface = self.get_mut(id);
|
||||
let image = surface.image_snapshot();
|
||||
let mut context = surface.direct_context();
|
||||
let encoded_image = image
|
||||
.encode(context.as_mut(), skia::EncodedImageFormat::PNG, None)
|
||||
.unwrap();
|
||||
general_purpose::STANDARD.encode(encoded_image.as_bytes())
|
||||
.ok_or(Error::CriticalError("Failed to encode image".to_string()))?;
|
||||
Ok(general_purpose::STANDARD.encode(encoded_image.as_bytes()))
|
||||
}
|
||||
|
||||
pub fn base64_snapshot_rect(&mut self, id: SurfaceId, irect: skia::IRect) -> Option<String> {
|
||||
pub fn base64_snapshot_rect(
|
||||
&mut self,
|
||||
id: SurfaceId,
|
||||
irect: skia::IRect,
|
||||
) -> Result<Option<String>> {
|
||||
let surface = self.get_mut(id);
|
||||
if let Some(image) = surface.image_snapshot_with_bounds(irect) {
|
||||
let mut context = surface.direct_context();
|
||||
let encoded_image = image
|
||||
.encode(context.as_mut(), skia::EncodedImageFormat::PNG, None)
|
||||
.unwrap();
|
||||
return Some(general_purpose::STANDARD.encode(encoded_image.as_bytes()));
|
||||
.ok_or(Error::CriticalError("Failed to encode image".to_string()))?;
|
||||
Ok(Some(
|
||||
general_purpose::STANDARD.encode(encoded_image.as_bytes()),
|
||||
))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the canvas and automatically marks
|
||||
@ -341,22 +356,41 @@ impl Surfaces {
|
||||
}
|
||||
}
|
||||
|
||||
fn reset_from_target(&mut self, target: skia::Surface) {
|
||||
fn reset_from_target(&mut self, target: skia::Surface) -> Result<()> {
|
||||
let dim = (target.width(), target.height());
|
||||
self.target = target;
|
||||
self.filter = self.target.new_surface_with_dimensions(dim).unwrap();
|
||||
self.debug = self.target.new_surface_with_dimensions(dim).unwrap();
|
||||
self.ui = self.target.new_surface_with_dimensions(dim).unwrap();
|
||||
self.filter = self
|
||||
.target
|
||||
.new_surface_with_dimensions(dim)
|
||||
.ok_or(Error::CriticalError("Failed to create surface".to_string()))?;
|
||||
self.debug = self
|
||||
.target
|
||||
.new_surface_with_dimensions(dim)
|
||||
.ok_or(Error::CriticalError("Failed to create surface".to_string()))?;
|
||||
self.ui = self
|
||||
.target
|
||||
.new_surface_with_dimensions(dim)
|
||||
.ok_or(Error::CriticalError("Failed to create surface".to_string()))?;
|
||||
// The rest are tile size surfaces
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn resize_cache(&mut self, cache_dims: skia::ISize, interest_area_threshold: i32) {
|
||||
self.cache = self.target.new_surface_with_dimensions(cache_dims).unwrap();
|
||||
pub fn resize_cache(
|
||||
&mut self,
|
||||
cache_dims: skia::ISize,
|
||||
interest_area_threshold: i32,
|
||||
) -> Result<()> {
|
||||
self.cache = self
|
||||
.target
|
||||
.new_surface_with_dimensions(cache_dims)
|
||||
.ok_or(Error::CriticalError("Failed to create surface".to_string()))?;
|
||||
self.cache.canvas().reset_matrix();
|
||||
self.cache.canvas().translate((
|
||||
(interest_area_threshold as f32 * TILE_SIZE),
|
||||
(interest_area_threshold as f32 * TILE_SIZE),
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn draw_rect_to(
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
use super::{filters, RenderState, Shape, SurfaceId};
|
||||
use crate::{
|
||||
error::Result,
|
||||
math::Rect,
|
||||
shapes::{
|
||||
calculate_position_data, calculate_text_layout_data, merge_fills, set_paint_fill,
|
||||
@ -66,7 +67,7 @@ pub fn stroke_paragraph_builder_group_from_text(
|
||||
}
|
||||
|
||||
let stroke_paragraphs: Vec<ParagraphBuilder> = (0..stroke_paragraphs_map.len())
|
||||
.map(|i| stroke_paragraphs_map.remove(&i).unwrap())
|
||||
.filter_map(|i| stroke_paragraphs_map.remove(&i))
|
||||
.collect();
|
||||
|
||||
paragraph_group.push(stroke_paragraphs);
|
||||
@ -195,7 +196,7 @@ pub fn render_with_bounds_outset(
|
||||
stroke_bounds_outset: f32,
|
||||
fill_inset: Option<f32>,
|
||||
layer_opacity: Option<f32>,
|
||||
) {
|
||||
) -> Result<()> {
|
||||
if let Some(render_state) = render_state {
|
||||
let target_surface = surface_id.unwrap_or(SurfaceId::Fills);
|
||||
|
||||
@ -225,9 +226,10 @@ pub fn render_with_bounds_outset(
|
||||
fill_inset,
|
||||
layer_opacity,
|
||||
);
|
||||
Ok(())
|
||||
},
|
||||
) {
|
||||
return;
|
||||
)? {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -242,7 +244,7 @@ pub fn render_with_bounds_outset(
|
||||
fill_inset,
|
||||
layer_opacity,
|
||||
);
|
||||
return;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(canvas) = canvas {
|
||||
@ -256,6 +258,7 @@ pub fn render_with_bounds_outset(
|
||||
layer_opacity,
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
@ -269,7 +272,7 @@ pub fn render(
|
||||
blur: Option<&ImageFilter>,
|
||||
fill_inset: Option<f32>,
|
||||
layer_opacity: Option<f32>,
|
||||
) {
|
||||
) -> Result<()> {
|
||||
render_with_bounds_outset(
|
||||
render_state,
|
||||
canvas,
|
||||
@ -281,7 +284,7 @@ pub fn render(
|
||||
0.0,
|
||||
fill_inset,
|
||||
layer_opacity,
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
fn render_text_on_canvas(
|
||||
|
||||
@ -9,6 +9,7 @@ pub mod grid_layout;
|
||||
use crate::math::{self as math, bools, identitish, is_close_to, Bounds, Matrix, Point};
|
||||
use common::GetBounds;
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::shapes::{
|
||||
ConstraintH, ConstraintV, Frame, Group, GrowType, Layout, Modifier, Shape, TransformEntry,
|
||||
TransformEntrySource, Type,
|
||||
@ -24,9 +25,9 @@ fn propagate_children(
|
||||
parent_bounds_after: &Bounds,
|
||||
transform: Matrix,
|
||||
bounds: &HashMap<Uuid, Bounds>,
|
||||
) -> VecDeque<Modifier> {
|
||||
) -> Result<VecDeque<Modifier>> {
|
||||
if identitish(&transform) {
|
||||
return VecDeque::new();
|
||||
return Ok(VecDeque::new());
|
||||
}
|
||||
|
||||
let mut result = VecDeque::new();
|
||||
@ -74,12 +75,12 @@ fn propagate_children(
|
||||
constraint_v,
|
||||
transform,
|
||||
child.ignore_constraints,
|
||||
);
|
||||
)?;
|
||||
|
||||
result.push_back(Modifier::transform_propagate(*child_id, transform));
|
||||
}
|
||||
|
||||
result
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn calculate_group_bounds(
|
||||
@ -172,9 +173,9 @@ fn propagate_transform(
|
||||
entries: &mut VecDeque<Modifier>,
|
||||
bounds: &mut HashMap<Uuid, Bounds>,
|
||||
modifiers: &mut HashMap<Uuid, Matrix>,
|
||||
) {
|
||||
) -> Result<()> {
|
||||
let Some(shape) = state.shapes.get(&entry.id) else {
|
||||
return;
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let shapes = &state.shapes;
|
||||
@ -249,7 +250,7 @@ fn propagate_transform(
|
||||
&shape_bounds_after,
|
||||
transform,
|
||||
bounds,
|
||||
);
|
||||
)?;
|
||||
entries.append(&mut children);
|
||||
}
|
||||
|
||||
@ -275,6 +276,7 @@ fn propagate_transform(
|
||||
entries.push_back(Modifier::reflow(parent.id, false));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn propagate_reflow(
|
||||
@ -338,34 +340,35 @@ fn reflow_shape(
|
||||
reflown: &mut HashSet<Uuid>,
|
||||
entries: &mut VecDeque<Modifier>,
|
||||
bounds: &mut HashMap<Uuid, Bounds>,
|
||||
) {
|
||||
) -> Result<()> {
|
||||
let Some(shape) = state.shapes.get(id) else {
|
||||
return;
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let shapes = &state.shapes;
|
||||
|
||||
let Type::Frame(frame_data) = &shape.shape_type else {
|
||||
return;
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if let Some(Layout::FlexLayout(layout_data, flex_data)) = &frame_data.layout {
|
||||
let mut children =
|
||||
flex_layout::reflow_flex_layout(shape, layout_data, flex_data, shapes, bounds);
|
||||
flex_layout::reflow_flex_layout(shape, layout_data, flex_data, shapes, bounds)?;
|
||||
entries.append(&mut children);
|
||||
} else if let Some(Layout::GridLayout(layout_data, grid_data)) = &frame_data.layout {
|
||||
let mut children =
|
||||
grid_layout::reflow_grid_layout(shape, layout_data, grid_data, shapes, bounds);
|
||||
grid_layout::reflow_grid_layout(shape, layout_data, grid_data, shapes, bounds)?;
|
||||
entries.append(&mut children);
|
||||
}
|
||||
reflown.insert(*id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn propagate_modifiers(
|
||||
state: &State,
|
||||
modifiers: &[TransformEntry],
|
||||
pixel_precision: bool,
|
||||
) -> Vec<TransformEntry> {
|
||||
) -> Result<Vec<TransformEntry>> {
|
||||
let mut entries: VecDeque<_> = modifiers
|
||||
.iter()
|
||||
.map(|entry| {
|
||||
@ -399,7 +402,7 @@ pub fn propagate_modifiers(
|
||||
&mut entries,
|
||||
&mut bounds,
|
||||
&mut modifiers,
|
||||
),
|
||||
)?,
|
||||
Modifier::Reflow(id, force_reflow) => {
|
||||
if force_reflow {
|
||||
reflown.remove(&id);
|
||||
@ -437,16 +440,16 @@ pub fn propagate_modifiers(
|
||||
if reflown.contains(id) {
|
||||
continue;
|
||||
}
|
||||
reflow_shape(id, state, &mut reflown, &mut entries, &mut bounds_temp);
|
||||
reflow_shape(id, state, &mut reflown, &mut entries, &mut bounds_temp)?;
|
||||
}
|
||||
layout_reflows = HashSet::new();
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
modifiers
|
||||
// #[allow(dead_code)]
|
||||
Ok(modifiers
|
||||
.iter()
|
||||
.map(|(key, val)| TransformEntry::from_input(*key, *val))
|
||||
.collect()
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@ -494,7 +497,8 @@ mod tests {
|
||||
&bounds_after,
|
||||
transform,
|
||||
&HashMap::new(),
|
||||
);
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(result.len(), 1);
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
use crate::error::{Error, Result};
|
||||
use crate::math::{is_move_only_matrix, Bounds, Matrix};
|
||||
use crate::shapes::{ConstraintH, ConstraintV};
|
||||
|
||||
@ -105,14 +106,14 @@ pub fn propagate_shape_constraints(
|
||||
constraint_v: ConstraintV,
|
||||
transform: Matrix,
|
||||
ignore_constrainst: bool,
|
||||
) -> Matrix {
|
||||
) -> Result<Matrix> {
|
||||
// if the constrains are scale & scale or the transform has only moves we
|
||||
// can propagate as is
|
||||
if (ignore_constrainst
|
||||
|| constraint_h == ConstraintH::Scale && constraint_v == ConstraintV::Scale)
|
||||
|| is_move_only_matrix(&transform)
|
||||
{
|
||||
return transform;
|
||||
return Ok(transform);
|
||||
}
|
||||
|
||||
let mut transform = transform;
|
||||
@ -133,7 +134,9 @@ pub fn propagate_shape_constraints(
|
||||
parent_transform.post_translate(center);
|
||||
parent_transform.pre_translate(-center);
|
||||
|
||||
let parent_transform_inv = &parent_transform.invert().unwrap();
|
||||
let parent_transform_inv = &parent_transform.invert().ok_or(Error::CriticalError(
|
||||
"Failed to invert parent transform".to_string(),
|
||||
))?;
|
||||
let origin = parent_transform_inv.map_point(child_bounds_after.nw);
|
||||
|
||||
let mut scale = Matrix::scale((scale_width, scale_height));
|
||||
@ -160,5 +163,5 @@ pub fn propagate_shape_constraints(
|
||||
transform.post_concat(&Matrix::translate(th + tv));
|
||||
}
|
||||
|
||||
transform
|
||||
Ok(transform)
|
||||
}
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::math::{self as math, Bounds, Matrix, Point, Vector, VectorExt};
|
||||
use crate::shapes::{
|
||||
AlignContent, AlignItems, AlignSelf, FlexData, JustifyContent, LayoutData, LayoutItem,
|
||||
@ -588,7 +590,7 @@ pub fn reflow_flex_layout(
|
||||
flex_data: &FlexData,
|
||||
shapes: ShapesPoolRef,
|
||||
bounds: &mut HashMap<Uuid, Bounds>,
|
||||
) -> VecDeque<Modifier> {
|
||||
) -> Result<VecDeque<Modifier>> {
|
||||
let mut result = VecDeque::new();
|
||||
let layout_bounds = &bounds.find(shape);
|
||||
let layout_axis = LayoutAxis::new(shape, layout_bounds, layout_data, flex_data);
|
||||
@ -724,7 +726,9 @@ pub fn reflow_flex_layout(
|
||||
|
||||
let parent_transform = layout_bounds.transform_matrix().unwrap_or_default();
|
||||
|
||||
let parent_transform_inv = &parent_transform.invert().unwrap();
|
||||
let parent_transform_inv = &parent_transform.invert().ok_or(Error::CriticalError(
|
||||
"Failed to invert parent transform".to_string(),
|
||||
))?;
|
||||
let origin = parent_transform_inv.map_point(layout_bounds.nw);
|
||||
|
||||
let mut scale = Matrix::scale((scale_width, scale_height));
|
||||
@ -737,5 +741,5 @@ pub fn reflow_flex_layout(
|
||||
result.push_back(Modifier::parent(shape.id, scale));
|
||||
bounds.insert(shape.id, layout_bounds_after);
|
||||
}
|
||||
result
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
use crate::error::{Error, Result};
|
||||
use crate::math::{self as math, intersect_rays, Bounds, Matrix, Point, Ray, Vector, VectorExt};
|
||||
use crate::shapes::{
|
||||
AlignContent, AlignItems, AlignSelf, Frame, GridCell, GridData, GridTrack, GridTrackType,
|
||||
@ -6,6 +7,7 @@ use crate::shapes::{
|
||||
};
|
||||
use crate::state::ShapesPoolRef;
|
||||
use crate::uuid::Uuid;
|
||||
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
|
||||
use super::common::GetBounds;
|
||||
@ -704,7 +706,7 @@ pub fn reflow_grid_layout(
|
||||
grid_data: &GridData,
|
||||
shapes: ShapesPoolRef,
|
||||
bounds: &mut HashMap<Uuid, Bounds>,
|
||||
) -> VecDeque<Modifier> {
|
||||
) -> Result<VecDeque<Modifier>> {
|
||||
let mut result = VecDeque::new();
|
||||
let layout_bounds = bounds.find(shape);
|
||||
let children: HashSet<Uuid> = shape.children_ids_iter(true).copied().collect();
|
||||
@ -825,7 +827,9 @@ pub fn reflow_grid_layout(
|
||||
|
||||
let parent_transform = layout_bounds.transform_matrix().unwrap_or_default();
|
||||
|
||||
let parent_transform_inv = &parent_transform.invert().unwrap();
|
||||
let parent_transform_inv = &parent_transform.invert().ok_or(Error::CriticalError(
|
||||
"Failed to invert parent transform".to_string(),
|
||||
))?;
|
||||
let origin = parent_transform_inv.map_point(layout_bounds.nw);
|
||||
|
||||
let mut scale = Matrix::scale((scale_width, scale_height));
|
||||
@ -839,5 +843,5 @@ pub fn reflow_grid_layout(
|
||||
bounds.insert(shape.id, layout_bounds_after);
|
||||
}
|
||||
|
||||
result
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ mod text_editor;
|
||||
pub use shapes_pool::{ShapesPool, ShapesPoolMutRef, ShapesPoolRef};
|
||||
pub use text_editor::*;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::render::RenderState;
|
||||
use crate::shapes::Shape;
|
||||
use crate::tiles;
|
||||
@ -28,41 +29,44 @@ pub(crate) struct State {
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn new(width: i32, height: i32) -> Self {
|
||||
State {
|
||||
render_state: RenderState::new(width, height),
|
||||
pub fn try_new(width: i32, height: i32) -> Result<Self> {
|
||||
Ok(State {
|
||||
render_state: RenderState::try_new(width, height)?,
|
||||
text_editor_state: TextEditorState::new(),
|
||||
current_id: None,
|
||||
current_browser: 0,
|
||||
shapes: ShapesPool::new(),
|
||||
// TODO: Maybe this can be moved to a different object
|
||||
saved_shapes: None,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Creates a new temporary shapes pool.
|
||||
// Will panic if a previous temporary pool exists.
|
||||
pub fn start_temp_objects(mut self) -> Self {
|
||||
pub fn start_temp_objects(mut self) -> Result<Self> {
|
||||
if self.saved_shapes.is_some() {
|
||||
panic!("Tried to start a temp objects while the previous have not been restored");
|
||||
return Err(Error::CriticalError(
|
||||
"Tried to start a temp objects while the previous have not been restored"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
self.saved_shapes = Some(self.shapes);
|
||||
self.shapes = ShapesPool::new();
|
||||
self
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
// Disposes of the temporary shapes pool restoring the normal pool
|
||||
// Will panic if a there is no temporary pool.
|
||||
pub fn end_temp_objects(mut self) -> Self {
|
||||
self.shapes = self
|
||||
.saved_shapes
|
||||
.expect("Tried to end temp objects but not content to be restored is present");
|
||||
pub fn end_temp_objects(mut self) -> Result<Self> {
|
||||
self.shapes = self.saved_shapes.ok_or(Error::CriticalError(
|
||||
"Tried to end temp objects but not content to be restored is present".to_string(),
|
||||
))?;
|
||||
self.saved_shapes = None;
|
||||
self
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, width: i32, height: i32) {
|
||||
self.render_state.resize(width, height);
|
||||
pub fn resize(&mut self, width: i32, height: i32) -> Result<()> {
|
||||
self.render_state.resize(width, height)
|
||||
}
|
||||
|
||||
pub fn render_state_mut(&mut self) -> &mut RenderState {
|
||||
@ -87,19 +91,17 @@ impl State {
|
||||
self.render_state.render_from_cache(&self.shapes);
|
||||
}
|
||||
|
||||
pub fn render_sync(&mut self, timestamp: i32) -> Result<(), String> {
|
||||
pub fn render_sync(&mut self, timestamp: i32) -> Result<()> {
|
||||
self.render_state
|
||||
.start_render_loop(None, &self.shapes, timestamp, true)?;
|
||||
Ok(())
|
||||
.start_render_loop(None, &self.shapes, timestamp, true)
|
||||
}
|
||||
|
||||
pub fn render_sync_shape(&mut self, id: &Uuid, timestamp: i32) -> Result<(), String> {
|
||||
pub fn render_sync_shape(&mut self, id: &Uuid, timestamp: i32) -> Result<()> {
|
||||
self.render_state
|
||||
.start_render_loop(Some(id), &self.shapes, timestamp, true)?;
|
||||
Ok(())
|
||||
.start_render_loop(Some(id), &self.shapes, timestamp, true)
|
||||
}
|
||||
|
||||
pub fn start_render_loop(&mut self, timestamp: i32) -> Result<(), String> {
|
||||
pub fn start_render_loop(&mut self, timestamp: i32) -> Result<()> {
|
||||
// If zoom changed, we MUST rebuild the tile index before using it.
|
||||
// Otherwise, the index will have tiles from the old zoom level, causing visible
|
||||
// tiles to appear empty. This can happen if start_render_loop() is called before
|
||||
@ -111,14 +113,12 @@ impl State {
|
||||
}
|
||||
|
||||
self.render_state
|
||||
.start_render_loop(None, &self.shapes, timestamp, false)?;
|
||||
Ok(())
|
||||
.start_render_loop(None, &self.shapes, timestamp, false)
|
||||
}
|
||||
|
||||
pub fn process_animation_frame(&mut self, timestamp: i32) -> Result<(), String> {
|
||||
pub fn process_animation_frame(&mut self, timestamp: i32) -> Result<()> {
|
||||
self.render_state
|
||||
.process_animation_frame(None, &self.shapes, timestamp)?;
|
||||
Ok(())
|
||||
.process_animation_frame(None, &self.shapes, timestamp)
|
||||
}
|
||||
|
||||
pub fn clear_focus_mode(&mut self) {
|
||||
@ -227,10 +227,10 @@ impl State {
|
||||
let _ = self.render_state.render_preview(&self.shapes, timestamp);
|
||||
}
|
||||
|
||||
pub fn rebuild_modifier_tiles(&mut self, ids: Vec<Uuid>) {
|
||||
pub fn rebuild_modifier_tiles(&mut self, ids: Vec<Uuid>) -> Result<()> {
|
||||
// Index-based storage is safe
|
||||
self.render_state
|
||||
.rebuild_modifier_tiles(&mut self.shapes, ids);
|
||||
.rebuild_modifier_tiles(&mut self.shapes, ids)
|
||||
}
|
||||
|
||||
pub fn font_collection(&self) -> &FontCollection {
|
||||
|
||||
@ -3,7 +3,6 @@ use crate::uuid::Uuid;
|
||||
use crate::view::Viewbox;
|
||||
use skia_safe as skia;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)]
|
||||
pub struct Tile(pub i32, pub i32);
|
||||
|
||||
@ -178,13 +177,10 @@ impl TileHashMap {
|
||||
}
|
||||
|
||||
pub fn add_shape_at(&mut self, tile: Tile, shape_id: Uuid) {
|
||||
self.grid.entry(tile).or_default();
|
||||
self.index.entry(shape_id).or_default();
|
||||
|
||||
let tile_set = self.grid.get_mut(&tile).unwrap();
|
||||
let tile_set = self.grid.entry(tile).or_default();
|
||||
tile_set.insert(shape_id);
|
||||
|
||||
let index_set = self.index.get_mut(&shape_id).unwrap();
|
||||
let index_set = self.index.entry(shape_id).or_default();
|
||||
index_set.insert(tile);
|
||||
}
|
||||
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
use crate::error::{Error, Result};
|
||||
use crate::mem;
|
||||
use macros::wasm_error;
|
||||
// use crate::mem::SerializableResult;
|
||||
use crate::error::Error;
|
||||
use crate::uuid::Uuid;
|
||||
use crate::with_state_mut;
|
||||
use crate::STATE;
|
||||
use crate::{shapes::ImageFill, utils::uuid_from_u32_quartet};
|
||||
use macros::wasm_error;
|
||||
|
||||
const FLAG_KEEP_ASPECT_RATIO: u8 = 1 << 0;
|
||||
const IMAGE_IDS_SIZE: usize = 32;
|
||||
@ -50,6 +49,7 @@ pub struct ShapeImageIds {
|
||||
|
||||
impl From<[u8; IMAGE_IDS_SIZE]> for ShapeImageIds {
|
||||
fn from(bytes: [u8; IMAGE_IDS_SIZE]) -> Self {
|
||||
// FIXME: this should probably be a try_from instead
|
||||
let shape_id = Uuid::try_from(&bytes[0..16]).unwrap();
|
||||
let image_id = Uuid::try_from(&bytes[16..32]).unwrap();
|
||||
ShapeImageIds { shape_id, image_id }
|
||||
@ -57,9 +57,9 @@ impl From<[u8; IMAGE_IDS_SIZE]> for ShapeImageIds {
|
||||
}
|
||||
|
||||
impl TryFrom<Vec<u8>> for ShapeImageIds {
|
||||
type Error = &'static str;
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
|
||||
fn try_from(value: Vec<u8>) -> Result<Self> {
|
||||
let mut arr = [0u8; IMAGE_IDS_SIZE];
|
||||
arr.copy_from_slice(&value);
|
||||
Ok(ShapeImageIds::from(arr))
|
||||
@ -68,13 +68,16 @@ impl TryFrom<Vec<u8>> for ShapeImageIds {
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn store_image() -> crate::error::Result<()> {
|
||||
pub extern "C" fn store_image() -> Result<()> {
|
||||
let bytes = mem::bytes();
|
||||
let ids = ShapeImageIds::try_from(bytes[0..IMAGE_IDS_SIZE].to_vec()).unwrap();
|
||||
let ids = ShapeImageIds::try_from(bytes[0..IMAGE_IDS_SIZE].to_vec())?;
|
||||
|
||||
// Read is_thumbnail flag (4 bytes as u32)
|
||||
let is_thumbnail_bytes = &bytes[IMAGE_IDS_SIZE..IMAGE_HEADER_SIZE];
|
||||
let is_thumbnail_value = u32::from_le_bytes(is_thumbnail_bytes.try_into().unwrap());
|
||||
let is_thumbnail_value =
|
||||
u32::from_le_bytes(is_thumbnail_bytes.try_into().map_err(|_| {
|
||||
Error::CriticalError("Invalid bytes for is_thumbnail flag".to_string())
|
||||
})?);
|
||||
let is_thumbnail = is_thumbnail_value != 0;
|
||||
|
||||
let image_bytes = &bytes[IMAGE_HEADER_SIZE..];
|
||||
@ -104,9 +107,10 @@ pub extern "C" fn store_image() -> crate::error::Result<()> {
|
||||
/// - bytes 44-47: height (i32)
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn store_image_from_texture() -> crate::error::Result<()> {
|
||||
pub extern "C" fn store_image_from_texture() -> Result<()> {
|
||||
let bytes = mem::bytes();
|
||||
|
||||
// FIXME: where does this 48 come from?
|
||||
if bytes.len() < 48 {
|
||||
// FIXME: Review if this should be an critical or a recoverable error.
|
||||
eprintln!("store_image_from_texture: insufficient data");
|
||||
@ -116,23 +120,41 @@ pub extern "C" fn store_image_from_texture() -> crate::error::Result<()> {
|
||||
));
|
||||
}
|
||||
|
||||
let ids = ShapeImageIds::try_from(bytes[0..IMAGE_IDS_SIZE].to_vec()).unwrap();
|
||||
let ids = ShapeImageIds::try_from(bytes[0..IMAGE_IDS_SIZE].to_vec())
|
||||
.map_err(|_| Error::CriticalError("Invalid image ids".to_string()))?;
|
||||
|
||||
// FIXME: read bytes in a safe way
|
||||
|
||||
// Read is_thumbnail flag (4 bytes as u32)
|
||||
let is_thumbnail_bytes = &bytes[IMAGE_IDS_SIZE..IMAGE_HEADER_SIZE];
|
||||
let is_thumbnail_value = u32::from_le_bytes(is_thumbnail_bytes.try_into().unwrap());
|
||||
let is_thumbnail_value =
|
||||
u32::from_le_bytes(is_thumbnail_bytes.try_into().map_err(|_| {
|
||||
Error::CriticalError("Invalid bytes for is_thumbnail flag".to_string())
|
||||
})?);
|
||||
let is_thumbnail = is_thumbnail_value != 0;
|
||||
|
||||
// Read GL texture ID (4 bytes as u32)
|
||||
let texture_id_bytes = &bytes[36..40];
|
||||
let texture_id = u32::from_le_bytes(texture_id_bytes.try_into().unwrap());
|
||||
let texture_id = u32::from_le_bytes(
|
||||
texture_id_bytes
|
||||
.try_into()
|
||||
.map_err(|_| Error::CriticalError("Invalid bytes for texture id".to_string()))?,
|
||||
);
|
||||
|
||||
// Read width and height (8 bytes as two i32s)
|
||||
let width_bytes = &bytes[40..44];
|
||||
let width = i32::from_le_bytes(width_bytes.try_into().unwrap());
|
||||
let width = i32::from_le_bytes(
|
||||
width_bytes
|
||||
.try_into()
|
||||
.map_err(|_| Error::CriticalError("Invalid bytes for width".to_string()))?,
|
||||
);
|
||||
|
||||
let height_bytes = &bytes[44..48];
|
||||
let height = i32::from_le_bytes(height_bytes.try_into().unwrap());
|
||||
let height = i32::from_le_bytes(
|
||||
height_bytes
|
||||
.try_into()
|
||||
.map_err(|_| Error::CriticalError("Invalid bytes for height".to_string()))?,
|
||||
);
|
||||
|
||||
with_state_mut!(state, {
|
||||
if let Err(msg) = state.render_state_mut().add_image_from_gl_texture(
|
||||
@ -142,6 +164,7 @@ pub extern "C" fn store_image_from_texture() -> crate::error::Result<()> {
|
||||
width,
|
||||
height,
|
||||
) {
|
||||
// FIXME: Review if we should return a RecoverableError
|
||||
eprintln!("store_image_from_texture error: {}", msg);
|
||||
}
|
||||
state.touch_shape(ids.shape_id);
|
||||
|
||||
@ -8,7 +8,7 @@ use crate::{uuid_from_u32_quartet, with_current_shape_mut, with_state, with_stat
|
||||
use super::align;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
use crate::error::Result;
|
||||
use crate::error::{Error, Result};
|
||||
|
||||
#[derive(Debug)]
|
||||
#[repr(C, align(1))]
|
||||
@ -177,9 +177,13 @@ pub extern "C" fn set_grid_columns() -> Result<()> {
|
||||
|
||||
let entries: Vec<GridTrack> = bytes
|
||||
.chunks(size_of::<RawGridTrack>())
|
||||
.map(|data| data.try_into().unwrap())
|
||||
.map(|data: [u8; size_of::<RawGridTrack>()]| RawGridTrack::from(data).into())
|
||||
.collect();
|
||||
.map(|data| {
|
||||
let track_bytes: [u8; size_of::<RawGridTrack>()] = data
|
||||
.try_into()
|
||||
.map_err(|_| Error::CriticalError("Invalid bytes for grid track".to_string()))?;
|
||||
Ok(RawGridTrack::from(track_bytes).into())
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||||
shape.set_grid_columns(entries);
|
||||
@ -196,9 +200,13 @@ pub extern "C" fn set_grid_rows() -> Result<()> {
|
||||
|
||||
let entries: Vec<GridTrack> = bytes
|
||||
.chunks(size_of::<RawGridTrack>())
|
||||
.map(|data| data.try_into().unwrap())
|
||||
.map(|data: [u8; size_of::<RawGridTrack>()]| RawGridTrack::from(data).into())
|
||||
.collect();
|
||||
.map(|data| {
|
||||
let track_bytes: [u8; size_of::<RawGridTrack>()] = data
|
||||
.try_into()
|
||||
.map_err(|_| Error::CriticalError("Invalid bytes for grid track".to_string()))?;
|
||||
Ok(RawGridTrack::from(track_bytes).into())
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||||
shape.set_grid_rows(entries);
|
||||
@ -215,9 +223,13 @@ pub extern "C" fn set_grid_cells() -> Result<()> {
|
||||
|
||||
let cells: Vec<RawGridCell> = bytes
|
||||
.chunks(size_of::<RawGridCell>())
|
||||
.map(|data| data.try_into().expect("Invalid grid cell data"))
|
||||
.map(|data: [u8; size_of::<RawGridCell>()]| RawGridCell::from(data))
|
||||
.collect();
|
||||
.map(|data| {
|
||||
let cell_bytes: [u8; size_of::<RawGridCell>()] = data
|
||||
.try_into()
|
||||
.map_err(|_| Error::CriticalError("Invalid bytes for grid cell".to_string()))?;
|
||||
Ok(RawGridCell::from(cell_bytes))
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||||
shape.set_grid_cells(cells.into_iter().map(|raw| raw.into()).collect());
|
||||
|
||||
@ -4,6 +4,7 @@ use mem::SerializableResult;
|
||||
use std::mem::size_of;
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::shapes::{Path, Segment, ToPath};
|
||||
use crate::{mem, with_current_shape, with_current_shape_mut, STATE};
|
||||
|
||||
@ -41,12 +42,12 @@ impl From<[u8; size_of::<RawSegmentData>()]> for RawSegmentData {
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for RawSegmentData {
|
||||
type Error = String;
|
||||
fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
|
||||
type Error = Error;
|
||||
fn try_from(bytes: &[u8]) -> Result<Self> {
|
||||
let data: [u8; RAW_SEGMENT_DATA_SIZE] = bytes
|
||||
.get(0..RAW_SEGMENT_DATA_SIZE)
|
||||
.and_then(|slice| slice.try_into().ok())
|
||||
.ok_or("Invalid path data".to_string())?;
|
||||
.ok_or(Error::CriticalError("Invalid path data".to_string()))?;
|
||||
Ok(RawSegmentData::from(data))
|
||||
}
|
||||
}
|
||||
@ -154,10 +155,14 @@ fn get_path_upload_buffer() -> &'static Mutex<Vec<u8>> {
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn start_shape_path_buffer() {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn start_shape_path_buffer() -> Result<()> {
|
||||
let buffer = get_path_upload_buffer();
|
||||
let mut buffer = buffer.lock().unwrap();
|
||||
let mut buffer = buffer
|
||||
.lock()
|
||||
.map_err(|_| Error::CriticalError("Failed to lock path buffer".to_string()))?;
|
||||
buffer.clear();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@ -165,32 +170,40 @@ pub extern "C" fn start_shape_path_buffer() {
|
||||
pub extern "C" fn set_shape_path_chunk_buffer() -> Result<()> {
|
||||
let bytes = mem::bytes();
|
||||
let buffer = get_path_upload_buffer();
|
||||
let mut buffer = buffer.lock().unwrap();
|
||||
let mut buffer = buffer
|
||||
.lock()
|
||||
.map_err(|_| Error::CriticalError("Failed to lock path buffer".to_string()))?;
|
||||
buffer.extend_from_slice(&bytes);
|
||||
mem::free_bytes()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn set_shape_path_buffer() {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_shape_path_buffer() -> Result<()> {
|
||||
let buffer = get_path_upload_buffer();
|
||||
let mut buffer = buffer
|
||||
.lock()
|
||||
.map_err(|_| Error::CriticalError("Failed to lock path buffer".to_string()))?;
|
||||
let chunk_size = size_of::<RawSegmentData>();
|
||||
if !buffer.len().is_multiple_of(chunk_size) {
|
||||
// FIXME
|
||||
println!("Warning: buffer length is not a multiple of chunk size!");
|
||||
}
|
||||
let mut segments = Vec::new();
|
||||
for (i, chunk) in buffer.chunks(chunk_size).enumerate() {
|
||||
match RawSegmentData::try_from(chunk) {
|
||||
Ok(seg) => segments.push(Segment::from(seg)),
|
||||
Err(e) => println!("Error at segment {}: {}", i, e),
|
||||
}
|
||||
}
|
||||
|
||||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||||
let buffer = get_path_upload_buffer();
|
||||
let mut buffer = buffer.lock().unwrap();
|
||||
let chunk_size = size_of::<RawSegmentData>();
|
||||
if !buffer.len().is_multiple_of(chunk_size) {
|
||||
// FIXME
|
||||
println!("Warning: buffer length is not a multiple of chunk size!");
|
||||
}
|
||||
let mut segments = Vec::new();
|
||||
for (i, chunk) in buffer.chunks(chunk_size).enumerate() {
|
||||
match RawSegmentData::try_from(chunk) {
|
||||
Ok(seg) => segments.push(Segment::from(seg)),
|
||||
Err(e) => println!("Error at segment {}: {}", i, e),
|
||||
}
|
||||
}
|
||||
shape.set_path_segments(segments);
|
||||
buffer.clear();
|
||||
});
|
||||
buffer.clear();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
|
||||
@ -6,6 +6,10 @@ use crate::wasm::blend::RawBlendMode;
|
||||
use crate::wasm::layouts::constraints::{RawConstraintH, RawConstraintV};
|
||||
use crate::{with_state_mut, STATE};
|
||||
|
||||
#[allow(unused_imports)]
|
||||
use crate::error::{Error, Result};
|
||||
use macros::wasm_error;
|
||||
|
||||
use super::RawShapeType;
|
||||
|
||||
const FLAG_CLIP_CONTENT: u8 = 0b0000_0001;
|
||||
@ -106,14 +110,18 @@ impl From<[u8; RAW_BASE_PROPS_SIZE]> for RawBasePropsData {
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn set_shape_base_props() {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_shape_base_props() -> Result<()> {
|
||||
let bytes = mem::bytes();
|
||||
|
||||
if bytes.len() < RAW_BASE_PROPS_SIZE {
|
||||
return;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let data: [u8; RAW_BASE_PROPS_SIZE] = bytes[..RAW_BASE_PROPS_SIZE].try_into().unwrap();
|
||||
// FIXME: this should just be a try_from
|
||||
let data: [u8; RAW_BASE_PROPS_SIZE] = bytes[..RAW_BASE_PROPS_SIZE]
|
||||
.try_into()
|
||||
.map_err(|_| Error::CriticalError("Invalid bytes for base props".to_string()))?;
|
||||
let raw = RawBasePropsData::from(data);
|
||||
|
||||
let id = raw.id();
|
||||
@ -151,6 +159,7 @@ pub extern "C" fn set_shape_base_props() {
|
||||
shape.set_corners((raw.corner_r1, raw.corner_r2, raw.corner_r3, raw.corner_r4));
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@ -292,9 +292,10 @@ pub extern "C" fn clear_shape_text() {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_shape_text_content() -> crate::error::Result<()> {
|
||||
let bytes = mem::bytes();
|
||||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||||
let raw_text_data = RawParagraph::try_from(&bytes).unwrap();
|
||||
let raw_text_data = RawParagraph::try_from(&bytes)
|
||||
.map_err(|_| Error::CriticalError("Invalid text data".to_string()))?;
|
||||
|
||||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||||
shape.add_paragraph(raw_text_data.into()).map_err(|_| {
|
||||
Error::RecoverableError(format!(
|
||||
"Error with set_shape_text_content on {:?}",
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
use macros::ToJs;
|
||||
#[allow(unused_imports)]
|
||||
use crate::error::{Error, Result};
|
||||
use macros::{wasm_error, ToJs};
|
||||
|
||||
use skia_safe as skia;
|
||||
|
||||
@ -39,11 +41,11 @@ impl From<[u8; RAW_TRANSFORM_ENTRY_SIZE]> for RawTransformEntry {
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for RawTransformEntry {
|
||||
type Error = String;
|
||||
fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
|
||||
type Error = Error;
|
||||
fn try_from(bytes: &[u8]) -> Result<Self> {
|
||||
let bytes: [u8; RAW_TRANSFORM_ENTRY_SIZE] = bytes
|
||||
.try_into()
|
||||
.map_err(|_| "Invalid transform entry bytes".to_string())?;
|
||||
.map_err(|_| Error::CriticalError("Invalid transform entry bytes".to_string()))?;
|
||||
Ok(RawTransformEntry::from(bytes))
|
||||
}
|
||||
}
|
||||
@ -73,16 +75,17 @@ impl From<RawTransformEntry> for TransformEntry {
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn propagate_modifiers(pixel_precision: bool) -> *mut u8 {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn propagate_modifiers(pixel_precision: bool) -> Result<*mut u8> {
|
||||
let bytes = mem::bytes();
|
||||
|
||||
let entries: Vec<TransformEntry> = bytes
|
||||
.chunks(RAW_TRANSFORM_ENTRY_SIZE)
|
||||
.map(|data| RawTransformEntry::try_from(data).unwrap().into())
|
||||
.collect();
|
||||
.map(|data| RawTransformEntry::try_from(data).map(|entry| entry.into()))
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
with_state!(state, {
|
||||
let result = shapes::propagate_modifiers(state, &entries, pixel_precision);
|
||||
mem::write_vec(result)
|
||||
let result = shapes::propagate_modifiers(state, &entries, pixel_precision)?;
|
||||
Ok(mem::write_vec(result))
|
||||
})
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user