🔧 Add loading state

This commit is contained in:
Elena Torro 2026-04-08 15:59:56 +02:00
parent e8e7900911
commit 5706d57ffe
4 changed files with 100 additions and 22 deletions

View File

@ -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)))

View File

@ -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<()> {

View File

@ -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)

View File

@ -26,6 +26,8 @@ pub(crate) struct State {
pub current_browser: u8,
pub shapes: ShapesPool,
pub saved_shapes: Option<ShapesPool>,
/// 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,
})
}