Skip to content

Conversation

@softmarshmallow
Copy link
Member

@softmarshmallow softmarshmallow commented Jan 29, 2026

day-315-grida-canvas-outline-mode.mp4
  • outline (wirefrae) mode
  • respect/ignore clips content flag
  • CtrlCmd + Y or CtrlCmd + Shift + Y

Release Notes

  • New Features
    • Added outline/wireframe rendering mode to visualize designs with shape outlines
    • Toggle outline mode using Ctrl/Cmd+Shift+O or Ctrl/Cmd+Y keyboard shortcuts
    • Option to ignore content clips when outline mode is enabled
    • New outline mode controls available in the View menu

- Introduced a new example for rendering scenes in both normal and outline modes, generating a reference image.
- Refactored the picture cache to support multiple render variants.
- Implemented a comprehensive render policy system to manage rendering behaviors, including outline styles and effects.
- Updated the painter to handle standard and outline rendering modes effectively.
@vercel
Copy link

vercel bot commented Jan 29, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
docs Ready Ready Preview, Comment Jan 29, 2026 3:54pm
grida Ready Ready Preview, Comment Jan 29, 2026 3:54pm
5 Skipped Deployments
Project Deployment Review Updated (UTC)
code Ignored Ignored Jan 29, 2026 3:54pm
legacy Ignored Ignored Jan 29, 2026 3:54pm
backgrounds Skipped Skipped Jan 29, 2026 3:54pm
blog Skipped Skipped Jan 29, 2026 3:54pm
viewer Skipped Skipped Jan 29, 2026 3:54pm

Request Review

@coderabbitai
Copy link

coderabbitai bot commented Jan 29, 2026

Walkthrough

This pull request introduces a render policy system that enables conditional rendering modes (standard vs. wireframe outline), threads this policy through the rendering pipeline, implements variant-based picture caching, and exposes outline mode toggling via the editor UI, hotkeys, and WASM bindings.

Changes

Cohort / File(s) Summary
Render Policy Core System
crates/grida-canvas/src/runtime/render_policy.rs, crates/grida-canvas/src/runtime/config.rs, crates/grida-canvas/src/runtime/mod.rs
Introduces RenderPolicy abstraction with content/effects/compositing modes, OutlineStyle configuration, default presets (STANDARD, WIREFRAME_DEFAULT), and flag-based WASM boundary (from_flags/to_flags conversions). Extends RuntimeRendererConfig to include render_policy field.
WASM & Runtime Bindings
crates/grida-canvas-wasm/lib/modules/canvas-bindings.d.ts, crates/grida-canvas-wasm/lib/modules/canvas.ts, crates/grida-canvas-wasm/src/wasm_application.rs, crates/grida-canvas-wasm/package.json
Adds TypeScript/WASM bindings for runtime_renderer_set_render_policy_flags and runtime_renderer_set_outline_mode; exposes Scene wrapper methods; implements FFI shims with enable flag-to-policy conversion; bumps package version to 0.90.0-canary.4.
Painter & Rendering Engine
crates/grida-canvas/src/painter/painter.rs, crates/grida-canvas/src/runtime/scene.rs
Extends Painter with policy-driven rendering: adds policy and variant_key fields, gates fill/stroke/effects/compositing by policy flags, implements wireframe outline drawing with per-layer transforms and text glyph path generation, integrates variant-based scene cache lookups, and conditionally branches between standard and outline rendering modes. Updates SceneCache with variant_key computation and policy-aware tile caching.
Picture Cache Variants
crates/grida-canvas/src/cache/picture.rs, crates/grida-canvas/src/cache/scene.rs
Splits monolithic node_pictures cache into default_store and variant_store to support per-render-variant picture reuse; adds get/set_node_picture_variant API with variant_key routing; updates len() and invalidate() to account for both stores.
Renderer Application API
crates/grida-canvas/src/window/application.rs, crates/grida-canvas/src/window/application_emscripten.rs
Adds ApplicationApi trait method runtime_renderer_set_render_policy_flags to convert flags to RenderPolicy and trigger redraws; implements on UnknownTargetApplication and EmscriptenApplication with delegation.
Example Code
crates/grida-canvas/examples/golden_outline_mode.rs
New golden-test example rendering a scene in normal and wireframe modes side-by-side, composing into labeled PNG output.
Editor State & Redux
editor/grida-canvas/editor.i.ts, editor/grida-canvas/action.ts, editor/grida-canvas/reducers/document.reducer.ts, editor/grida-canvas/reducers/surface.reducer.ts
Introduces IEditorFeatureOutlineModeState with outline_mode and outline_mode_ignores_clips; defines EditorSurface_OutlineModeAction and EditorSurface_OutlineModeIgnoresClipsAction; routes surface actions through document and surface reducers with UX guard (clips toggle only applies when outline_mode is "on").
Editor Runtime & Utilities
editor/grida-canvas/editor.ts, editor/grida-canvas/render-policy-flags.ts
Adds __runtime_renderer_set_outline_mode, surfaceConfigureOutlineMode, surfaceToggleOutlineMode, and clips-ignore toggle methods; exports flag constants (FILLS, STROKES, OUTLINES_ALWAYS, EFFECTS_ENABLED, COMPOSITING_ENABLED, IGNORE_CLIPS_CONTENT) and computeRenderPolicyFlagsForOutlineFeature helper.
Editor UI & Interactions
editor/grida-canvas-react/viewport/hotkeys.tsx, editor/grida-canvas-hosted/playground/uxhost-actions.ts, editor/grida-canvas-hosted/playground/uxhost-menu.tsx, editor/scaffolds/sidecontrol/controls/ext-zoom.tsx
Registers hotkeys (Ctrl/Cmd+Shift+O and Ctrl/Cmd+Y) for outline toggle; adds action entry to registry; integrates "Outlines" submenu in View menu with Show outlines and Ignore clips content toggles; extends zoom dropdown with outline controls.
Documentation
docs/editor/shortcuts/index.md
Updates View & Zoom shortcuts table with new "Toggle outline mode (wireframe)" entry and reformats for consistency.

Sequence Diagram(s)

sequenceDiagram
    participant UI as Editor UI<br/>(Hotkey/Menu)
    participant Editor as Editor Surface
    participant Canvas as Canvas Runtime
    participant Painter as Painter &<br/>Renderer
    participant Cache as Scene Cache

    UI->>Editor: surfaceToggleOutlineMode()
    Editor->>Editor: Dispatch outline-mode action
    Editor->>Editor: Compute RenderPolicyFlags
    Editor->>Canvas: __runtime_renderer_set_outline_mode(...)
    Canvas->>Canvas: Convert flags to RenderPolicy
    Canvas->>Painter: set_render_policy(policy)
    Painter->>Painter: Compute variant_key from policy
    Canvas->>Canvas: Queue redraw
    Canvas->>Painter: draw_layer_list(layers, policy)
    Painter->>Cache: get_node_picture_variant(id, variant_key)
    alt Cache hit
        Cache-->>Painter: Cached picture
    else Cache miss
        Painter->>Painter: Render with policy<br/>(wireframe/standard)
        Painter->>Cache: set_node_picture_variant(id, variant_key, picture)
        Cache-->>Painter: Stored
    end
    Painter->>Painter: Apply outline or fills/strokes<br/>based on policy
    Painter-->>UI: ✓ Updated canvas
Loading
sequenceDiagram
    participant Standard as Standard Render
    participant Wireframe as Wireframe Render
    participant Policy as RenderPolicy

    Standard->>Policy: is_wireframe() == false
    Policy-->>Standard: Render fills & strokes
    Standard->>Standard: Apply texture, effects,<br/>compositing
    
    Wireframe->>Policy: is_wireframe() == true
    Policy-->>Wireframe: outline_style()
    Wireframe->>Wireframe: Build glyph paths for text
    Wireframe->>Wireframe: Draw layer outlines<br/>with stroke paint
    Wireframe->>Policy: ignore_clips_content?
    Policy-->>Wireframe: Skip clip application
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested labels

ux, cg

Poem

🐰 Outlines spring forth in wireframe delight,
Policy threads through render and cache,
Toggle the mode with a keystroke so light—
Standard or outline, each serves their own dash!
From Rust to the canvas, the changes do dance. 🎨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Grida Canvas - Outline Mode' directly and clearly summarizes the main feature addition—an outline/wireframe rendering mode for the canvas. It matches the primary objective and is specific enough for scanning history.
Docstring Coverage ✅ Passed Docstring coverage is 84.34% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch canary

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

- Added functionality to ignore clip paths when outline mode is enabled, enhancing the debugging and inspection capabilities.
- Introduced new actions and state management for toggling the clip path behavior in the editor.
- Updated the render policy to accommodate the new outline mode preferences, ensuring consistent rendering behavior across different modes.
- Refactored the painter to utilize helper methods for optional clipping, improving code readability and maintainability.
@vercel vercel bot temporarily deployed to Preview – blog January 29, 2026 15:52 Inactive
@vercel vercel bot temporarily deployed to Preview – backgrounds January 29, 2026 15:52 Inactive
@vercel vercel bot temporarily deployed to Preview – viewer January 29, 2026 15:52 Inactive
@softmarshmallow softmarshmallow changed the title Outline mode Grida Canvas - Outline Mode Jan 29, 2026
@softmarshmallow softmarshmallow marked this pull request as ready for review January 29, 2026 16:14
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 46a705090a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1392 to +1394

for entry in &list.layers {
if let Some(scene_cache) = self.scene_cache {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply masks even when outline mode respects clips

In wireframe mode the renderer iterates list.layers directly, so it never executes PainterRenderCommand::MaskGroup. That means masks are always ignored in outline mode, even when the user disables “Ignore clips content” and expects masks/clips to apply. In masked scenes this will draw outlines outside the mask and diverge from the standard render; you likely need to route outline rendering through the command list or explicitly handle MaskGroup when ignore_clips_content is false.

Useful? React with 👍 / 👎.

Comment on lines +467 to +469
useHotkeys("meta+shift+o, ctrl+shift+o", () => {
const v = editor.surface.surfaceToggleOutlineMode();
toast.success(`Outline Mode ${v === "on" ? "on" : "off"}`);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Prevent default for Cmd/Ctrl+Shift+O hotkey

Cmd/Ctrl+Shift+O is bound by browsers to open the bookmarks manager. The new outline toggle handler doesn’t call preventDefault, so in Chrome/Edge/Firefox this shortcut will open the browser UI and the app hotkey will be unreliable. Add preventDefault: true (like the Cmd/Ctrl+Y handler just below) to keep the shortcut working inside the editor.

Useful? React with 👍 / 👎.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/grida-canvas/src/window/application_emscripten.rs (1)

339-339: Address clippy warning: unreachable expression.

The pipeline reports a warning about an unreachable expression on this line. The unreachable!() macro is placed after a return statement in the #[cfg(target_os = "emscripten")] block (line 336), making it dead code when compiling for emscripten.

Consider restructuring to avoid the warning:

🛠️ Suggested fix
         #[cfg(target_os = "emscripten")]
         unsafe {
             // Register the animation frame callback with the leaked pointer.
             let app_ptr = Box::into_raw(_app);
             emscripten_request_animation_frame_loop(
                 Some(request_animation_frame_callback),
                 app_ptr as *mut _,
             );
             // Reconstruct the box so the caller retains ownership.
             return Box::from_raw(app_ptr);
         }

-        unreachable!("emscipten cannot be initialized on native")
+        #[cfg(not(target_os = "emscripten"))]
+        unreachable!("emscripten cannot be initialized on native")
     }
🧹 Nitpick comments (4)
crates/grida-canvas/src/cache/picture.rs (1)

59-64: Consider removing unnecessary .clone() on NodeId.

NodeId is u64, which implements Copy. The .clone() call on line 63 is unnecessary since Copy types are implicitly copied. This applies to set_node_picture_variant as well where id is moved.

♻️ Suggested simplification
     pub fn get_node_picture_variant(&self, id: &NodeId, variant_key: u64) -> Option<&Picture> {
         if variant_key == 0 {
             return self.default_store.get(id);
         }
-        self.variant_store.get(&(id.clone(), variant_key))
+        self.variant_store.get(&(*id, variant_key))
     }
crates/grida-canvas/src/runtime/render_policy.rs (1)

207-294: Add a doc comment in Rust referencing the TypeScript file to aid cross-language synchronization.

Flag values are currently synchronized between Rust and TypeScript. However, the TypeScript file (editor/grida-canvas/render-policy-flags.ts) already documents that Rust is the source of truth, while the Rust side has no reciprocal reference. Consider adding a module-level or section comment in render_policy.rs similar to the one in the TypeScript file to remind maintainers that these constants define an ABI boundary and must stay in sync across both files. For example:

/// Render-policy bitflags used by the WASM renderer.
///
/// **Note**: Keep these synchronized with `editor/grida-canvas/render-policy-flags.ts`.
/// These constants define an ABI boundary between the Rust renderer and the TypeScript editor.

This bidirectional cross-reference will make the synchronization requirement explicit in both files.

editor/grida-canvas-react/viewport/hotkeys.tsx (1)

472-483: Redundant preventDefault call.

The e.preventDefault() call on line 476 is redundant since preventDefault: true is already specified in the options object on line 481. Consider removing one of them for clarity.

♻️ Suggested simplification
   useHotkeys(
     "meta+y, ctrl+y",
-    (e) => {
-      // prevent default browser behavior (e.g. open history)
-      e.preventDefault();
+    () => {
       const v = editor.surface.surfaceToggleOutlineMode();
       toast.success(`Outline Mode ${v === "on" ? "on" : "off"}`);
     },
     {
       preventDefault: true,
     }
   );
crates/grida-canvas/src/painter/painter.rs (1)

1254-1281: Consider extracting glyph path building into a shared helper.

The glyph path iteration logic in build_text_glyph_path (lines 1260-1279) is nearly identical to the logic in draw_text_backdrop_blur (lines 462-481). Both visit paragraph glyphs, extract paths, apply position offsets, and accumulate into a PathBuilder.

♻️ Suggested refactor to reduce duplication

Extract a shared helper method:

fn build_glyph_path_from_paragraph(
    paragraph: &Rc<RefCell<textlayout::Paragraph>>,
    y_offset: f32,
) -> Path {
    let mut builder = PathBuilder::new();
    paragraph.borrow_mut().visit(|_, info| {
        if let Some(info) = info {
            let glyphs = info.glyphs();
            let positions = info.positions();
            let origin = info.origin();
            let font = info.font();
            for (glyph, pos) in glyphs.iter().zip(positions.iter()) {
                if let Some(glyph_path) = font.get_path(*glyph) {
                    let offset = Point::new(pos.x + origin.x, pos.y + origin.y + y_offset);
                    if offset.x != 0.0 || offset.y != 0.0 {
                        let transformed =
                            glyph_path.make_transform(&Matrix::translate((offset.x, offset.y)));
                        builder.add_path(&transformed);
                    } else {
                        builder.add_path(&glyph_path);
                    }
                }
            }
        }
    });
    builder.detach()
}

Then both draw_text_backdrop_blur and build_text_glyph_path can use this shared helper.

Also applies to: 454-482

@softmarshmallow softmarshmallow merged commit 3c0fac3 into main Jan 29, 2026
11 of 12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant