From 5706d57ffeb6db3da471481fedf1d6a7666cfee3 Mon Sep 17 00:00:00 2001 From: Elena Torro Date: Wed, 8 Apr 2026 15:59:56 +0200 Subject: [PATCH] :wrench: Add loading state --- frontend/src/app/render_wasm/api.cljs | 40 +++++++++++------------- render-wasm/src/main.rs | 45 +++++++++++++++++++++++++++ render-wasm/src/render.rs | 33 ++++++++++++++++++++ render-wasm/src/state.rs | 4 ++- 4 files changed, 100 insertions(+), 22 deletions(-) diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index 27dc63e5c6..1c49785d16 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -1132,46 +1132,40 @@ (defn- set-objects-async "Asynchronously process shapes in chunks, yielding to the browser between chunks. - Returns a promise that resolves when all shapes are processed. - - Renders a preview only periodically during loading to show progress, - then does a full tile-based render at the end." + Returns a promise that resolves when all shapes are processed." [shapes render-callback] - (let [total-shapes (count shapes) - total-chunks (mth/ceil (/ total-shapes SHAPES_CHUNK_SIZE)) - ;; Render at 25%, 50%, 75% of loading - render-at-chunks (set [(mth/floor (* total-chunks 0.25)) - (mth/floor (* total-chunks 0.5)) - (mth/floor (* total-chunks 0.75))])] + (let [total-shapes (count shapes)] (p/create (fn [resolve _reject] - (letfn [(process-next-chunk [index thumbnails-acc full-acc chunk-count] + (letfn [(process-next-chunk [index thumbnails-acc full-acc] (if (< index total-shapes) ;; Process one chunk (let [{:keys [thumbnails full next-index]} (process-shapes-chunk shapes index SHAPES_CHUNK_SIZE - thumbnails-acc full-acc) - new-chunk-count (inc chunk-count)] - ;; Only render at specific progress milestones - (when (contains? render-at-chunks new-chunk-count) - (render-preview!)) - + thumbnails-acc full-acc)] ;; Yield to browser, then continue with next chunk (-> (yield-to-browser) (p/then (fn [_] - (process-next-chunk next-index thumbnails full new-chunk-count))))) + (process-next-chunk next-index thumbnails full))))) ;; All chunks done - finalize (do (perf/end-measure "set-objects") - (process-pending shapes thumbnails-acc full-acc noop-fn + ;; Rebuild tiles while loading=true so the first + ;; render can use the flag (e.g. for placeholders) + (h/call wasm/internal-module "_rebuild_all_tiles") + ;; Unblock rendering so shapes appear immediately + (end-shapes-loading!) + (process-pending shapes thumbnails-acc full-acc + ;; on-render: first render done, now clear loading flag + (fn [] + (h/call wasm/internal-module "_end_loading")) (fn [] - (end-shapes-loading!) (if render-callback (render-callback) (render-finish)) (ug/dispatch! (ug/event "penpot:wasm:set-objects")) (resolve nil))))))] - (process-next-chunk 0 [] [] 0)))))) + (process-next-chunk 0 [] [])))))) (defn- set-objects-sync "Synchronously process all shapes (for small shape counts)." @@ -1238,12 +1232,16 @@ (set-objects-sync shapes render-callback) (do (begin-shapes-loading!) + (h/call wasm/internal-module "_begin_loading") + (h/call wasm/internal-module "_render_loading_overlay") (try (-> (set-objects-async shapes render-callback) (p/catch (fn [error] + (h/call wasm/internal-module "_end_loading") (end-shapes-loading!) (js/console.error "Async WASM shape loading failed" error)))) (catch :default error + (h/call wasm/internal-module "_end_loading") (end-shapes-loading!) (js/console.error "Async WASM shape loading failed" error) (throw error))) diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index 42a8c46671..e0f62a49b7 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -244,6 +244,51 @@ pub extern "C" fn render_preview() -> Result<()> { Ok(()) } +/// Enter bulk-loading mode. While active, `state.loading` is `true`. +#[no_mangle] +#[wasm_error] +pub extern "C" fn begin_loading() -> Result<()> { + with_state_mut!(state, { + state.loading = true; + }); + Ok(()) +} + +/// Draw a full-screen loading overlay (background + "Loading…" text). +/// Called from CLJS right after begin_loading so the user sees +/// immediate feedback while shapes are being processed. +#[no_mangle] +#[wasm_error] +pub extern "C" fn render_loading_overlay() -> Result<()> { + with_state_mut!(state, { + state.render_state.render_loading_overlay(); + }); + Ok(()) +} + +/// Rebuild the full tile index after bulk loading. +/// Called while `loading` is still `true` so the first render +/// can use the loading flag (e.g. for placeholders). +#[no_mangle] +#[wasm_error] +pub extern "C" fn rebuild_all_tiles() -> Result<()> { + with_state_mut!(state, { + state.rebuild_tiles(); + }); + Ok(()) +} + +/// Leave bulk-loading mode. Should be called after the first +/// render so the loading flag is available during that render. +#[no_mangle] +#[wasm_error] +pub extern "C" fn end_loading() -> Result<()> { + with_state_mut!(state, { + state.loading = false; + }); + Ok(()) +} + #[no_mangle] #[wasm_error] pub extern "C" fn process_animation_frame(timestamp: i32) -> Result<()> { diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index c66bcdf2b3..515f8775bc 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -654,6 +654,39 @@ impl RenderState { self.surfaces.reset(self.background_color); } + /// FIXME + pub fn render_loading_overlay(&mut self) { + let canvas = self.surfaces.canvas(SurfaceId::Target); + let skia::ISize { width, height } = canvas.base_layer_size(); + + canvas.save(); + + // Full-screen background rect + let rect = skia::Rect::from_wh(width as f32, height as f32); + let mut bg_paint = skia::Paint::default(); + bg_paint.set_color(self.background_color); + bg_paint.set_style(skia::PaintStyle::Fill); + canvas.draw_rect(rect, &bg_paint); + + // Centered "Loading…" text + let mut text_paint = skia::Paint::default(); + text_paint.set_color(skia::Color::GRAY); + text_paint.set_anti_alias(true); + + let font = self.fonts.debug_font(); + // FIXME + let text = "Loading…"; + let (text_width, _) = font.measure_str(text, None); + let metrics = font.metrics(); + let text_height = metrics.1.cap_height; + let x = (width as f32 - text_width) / 2.0; + let y = (height as f32 + text_height) / 2.0; + canvas.draw_str(text, skia::Point::new(x, y), font, &text_paint); + + canvas.restore(); + self.flush_and_submit(); + } + #[allow(dead_code)] pub fn get_canvas_at(&mut self, surface_id: SurfaceId) -> &skia::Canvas { self.surfaces.canvas(surface_id) diff --git a/render-wasm/src/state.rs b/render-wasm/src/state.rs index 7b6ba8a65e..f39c539cf0 100644 --- a/render-wasm/src/state.rs +++ b/render-wasm/src/state.rs @@ -26,6 +26,8 @@ pub(crate) struct State { pub current_browser: u8, pub shapes: ShapesPool, pub saved_shapes: Option, + /// True while the first bulk load of shapes is in progress. + pub loading: bool, } impl State { @@ -36,8 +38,8 @@ impl State { current_id: None, current_browser: 0, shapes: ShapesPool::new(), - // TODO: Maybe this can be moved to a different object saved_shapes: None, + loading: false, }) }