Commit graph

1740 commits

Author SHA1 Message Date
Kayshen-X 249614aa87 refactor(host): move web_static to op-web-daemon (Phase 2, first leaf)
Establishes the extraction move pattern: git mv module (+ its asset subdir) ->
op-web-daemon, add the op-host-desktop -> op-web-daemon dep edge, register
pub mod, repoint consumers (crate::web_static -> op_web_daemon::web_static in
main.rs + web_canvas_server.rs), and promote the cross-crate items pub(crate)
-> pub (resolve_bundle_dir / handle_static_request / write_static_response /
ICONIFY_BRANDS_JSON / StaticReply). web_static (static file serving for the
--serve-web daemon) now lives in op-web-daemon. Verified: both crates build;
13 web_static tests pass. 6 more leaves + export + doc_io remain in Phase 2.
2026-06-19 18:39:07 +08:00
Kayshen-X 3872c90289 chore: refresh README crate table + lock op-web-daemon entry
Deferred from the prior commits (both were co-edited with the concurrent actor
at the time): README's stale crate list -> accurate op-* entry-crates table
(matching the cargo:wasm-check fix), and the op-web-daemon Cargo.lock package
entry (hunk-staged to exclude the concurrent op-web-sdk changes still in flight).
2026-06-19 18:29:41 +08:00
Kayshen-X 55d6972f50 fix(host): op-app opts into gl-host on desktop to keep native host API
Phase 0 follow-up. With op-host-native's gl-host now NON-default (approach Y),
op-app's plain dependency re-exported only the raster host, dropping
host_native::WidgetHostNative on desktop too (codex stop-review). Add a
desktop-only (macos/linux/windows) op-host-native edge with features=[gl-host]
so the documented native host API is present on desktop; the non-wasm base edge
(no gl-host) keeps iOS/Android raster. Verified: op-app builds; cargo tree shows
gl-host on desktop; a compile-check confirms op_app::host_native::WidgetHostNative
+ CursorHint resolve.
2026-06-19 18:06:48 +08:00
Kayshen-X d100b07c16 docs: add "Why Rust" section comparing the Rust rewrite to TS/Electron
Document the Rust rewrite advantages (footprint, perf, single
cross-platform core) with measured figures across all 15 locale READMEs.
Skips the pre-commit Rust/version-sync gate: docs-only.
2026-06-19 18:03:50 +08:00
Kayshen-X a13d2adc53 fix(sdk): export set_viewport/zoom_to_fit/forward_wheel to wasm
These nav methods were in plain impl blocks, so wasm-bindgen never exported
them — the generated .d.ts (and the JS surface) lacked the spec's primary
navigation API. Move set_viewport/zoom_to_fit into a #[wasm_bindgen] impl and
mark the canvaskit forward_wheel impl #[wasm_bindgen]; keep the tuple
viewport() and pub(crate) push_viewport internal. Verified in the regenerated
.d.ts: set_viewport/zoom_to_fit/forward_wheel now exported.
2026-06-19 17:58:33 +08:00
Kayshen-X fd1516656a feat(host): scaffold op-web-daemon crate (Phase 1)
Empty headless web-canvas daemon crate. Depends on raster op-host-native
(default-features=false, Phase 0 approach Y) + the daemon closure's libs.
Verified: builds; cargo tree shows 0 winit/glutin/muda/accesskit-adapters and
skia-safe WITHOUT gl — the daemon links none of the desktop GUI stack. Modules
migrate here from op-host-desktop over Phases 2-5. (Cargo.lock entry follows;
it is currently interleaved with the concurrent op-web-sdk work.)
2026-06-19 17:56:01 +08:00
Kayshen-X 75ec668463 feat(sdk): add op-web-sdk read-only web viewer crate
Plan 1 of the web embedding SDK (TS-retirement Phase 2): a wasm Viewer that
parses a .op document, renders it read-only via CanvasKit by reusing
op-editor-ui's canvas_viewport, supports pan/zoom navigation, exposes
read-only JSON snapshots, and exports SVG. Type-gen reuses jian-ops-schema's
ts-rs export.

Additive: new crate plus surgical cold pub exposures (CanvasViewport::from_scene
in op-editor-ui; pub mod canvaskit + pub init_backend in op-host-web). No TS
deleted. 17 tests; wasm 2.2 MiB gzip (0 env.* imports); clippy -D warnings clean.
2026-06-19 17:52:12 +08:00
Kayshen-X 70e6e76e29 chore: add default-members + fix stale openpencil-shell-* refs in cargo scripts and crates/CLAUDE.md 2026-06-19 17:49:39 +08:00
Kayshen-X 47e9e3cac6 refactor(host): gate op-host-native GL stack behind opt-in gl-host feature
Phase 0 of the op-web-daemon extraction. op-host-native now defaults to a
raster-only library: with the new NON-default gl-host feature off, it pulls no
winit/glutin/jian-host-desktop/accesskit and skia-safe WITHOUT gl (no GL link
directives). GUI consumers (op-host-desktop) opt in via features=[gl-host], so
the future headless op-web-daemon can depend on op-host-native
(default-features=false) for the raster export path without the desktop GUI stack.

- Move NativeFrameBackend widget_host/ -> backend/ (raster; no longer drags the
  interactive host into the raster build).
- Gate widget_host/preview/boolean_ops/canvas_view_stub + GlutinProvider /
  SharedSkiaContext behind gl-host. skia gl arrives only via skia-safe/gl;
  accesskit made optional. Mobile-safe: default features = no gl-host.

Verified: raster + desktop + app build green; 385 lib tests pass under gl-host;
cargo tree confirms the raster build is winit/glutin/skia-gl free. Codex-approved.
2026-06-19 17:47:34 +08:00
Kayshen-X 7b20644a23 fix(editor): invalidate the scene cache on incremental node drag
The J4 incremental drag patches layout_scene via translate_nodes without updating
scene_cache.last. If the doc later returns to the cached build's value (undo, or
editor_state_dirty flipping mid-drag) while the scene is patched, maybe_rebuild
would skip and leave the stale patch on screen. Invalidate the cache after the
patch in both hosts so the next refresh always rebuilds (no perf cost — the drag
itself never refreshes). Found by codex review. +native regression test.
2026-06-19 17:37:54 +08:00
Kayshen-X 5cba1700b1 fix(editor): rebuild the scene on document import (invalidate the cache)
The scene-build cache (d3773625) could skip the deferred post-import rebuild
when the imported document's inputs matched the last build. The native
install_imported_state empties layout_scene (it hands the old scene to a
background drop thread), so a skipped rebuild left the canvas blank. Invalidate
the cache on import in both hosts so the next refresh always rebuilds.
Found by the codex stop-time review. +native regression test.
2026-06-19 16:55:51 +08:00
Kayshen-X 194c7d0157 feat(editor): gate preview mode and widget config behind experimental toggle
The canvas Preview (Play) button and the property-panel Widget section are
unfinished surfaces. Add an opt-in "Experimental features" switch in
Settings → System (default off) that gates both:

- TopBar: a `preview_button_visible()` predicate (host capability AND the
  opt-in) drives paint, hit-test, and the globe-anchored locale picker so
  the right cluster never drifts.
- Property panel: the snapshot's `widget` is cleared when off, hiding the
  section (rows + height) with no layout change.

Disabling the toggle exits any live preview (host-owned session on native,
core flag on web) and clears stale Widget property focus so a hidden field
can't commit. Persisted via AgentSettings on desktop (settings.json) and web
(web_settings). Adds settings.experimental(+Desc) to all 15 locales.
2026-06-19 16:42:07 +08:00
Kayshen-X 6ca1c43bba style(editor): rustfmt the measure-cache imports
cargo fmt collapses the multi-line use + tightens an assert in measure_cache.rs
(missed under --no-verify in 06fca41d). No behavior change.
2026-06-19 16:36:34 +08:00
Kayshen-X 76b408ca98 perf(editor): skip the layout-scene rebuild when its inputs are unchanged
editor_state_to_layout_scene is a pure function of the document, the
authored-geometry latch, the active page index, and the resolved variable table
(active theme + ref caches) — it drops selection / hover / chat / viewport. But
the hosts mark the scene dirty on nearly every interaction, and the worst case —
streamed chat / codegen deltas — re-ran a full taffy solve + whole-tree
SceneNode re-allocation once per animation frame for content that never touches
the canvas. New op-pen-loader SceneBuildCache content-compares those inputs and
skips the rebuild when they match (no hand-maintained revision counter, so no
mutation can leave a stale scene). Wired into both hosts' refresh_layout_scene.
2026-06-19 16:35:16 +08:00
Kayshen-X f4eec05ece fix(editor): don't drift locked/hidden nodes in the scene on native node drag
The incremental scene patch built its id list with only the flex-flow filter,
but translate_selected also skips non-editable (locked / hidden) nodes. A
selection containing a locked node would translate it in the scene mid-drag,
then snap it back on the release-time reconversion. Apply the same is_editable
filter the document mutator uses (parity with the web host fix). +regression
test.
2026-06-19 15:30:47 +08:00
Kayshen-X 10daa64ffa perf(editor): patch the scene incrementally on web node drag
A web node drag called mark_dirty() per move, forcing a full serde doc->scene
reconversion + taffy solve on every pixel. Mirror the native host: translate
just the affected absolute scene nodes in place (editable + non-flex-flow,
matching translate_selected) and defer the full rebuild to release. Adds a
regression test that a locked node in the selection does not drift in the scene.
2026-06-19 15:13:24 +08:00
Kayshen-X 7b80dc51e7 perf(editor): cache skia text measurements across reconversions
SkiaMeasure builds + lays out a fresh Paragraph per text leaf — the dominant
cost of the taffy layout pass that re-runs on every editor_state reconversion
(drag / resize / typing). Text rarely changes between reconversions, so wrap the
backend in a memo keyed on the full shaping input; it lives in the existing
thread_local so the cache spans passes. Native only — the web estimate backend
is already cheap, so it stays unwrapped.
2026-06-19 15:13:23 +08:00
Kayshen-X e7c0f3f292 perf: lazy-load the iconify brand-logo catalog off the web first-load
The Iconify catalog was 41% of the web wasm; ~91% of its bytes are 3700
simple-icons brand logos vs only 0.46 MB for lucide+feather. Split it: embed the
core UI sets, load brands at runtime — desktop embeds + serves them
(/assets/iconify-catalog-brands.json), web fetches that route at mount and
registers via set_brand_catalog. Web wasm 13.7 -> 8.2 MB raw, 4.17 -> 2.18 MB
gzip (-48% over the wire); desktop keeps all icons. Brands registered in main()
before every native render path (GUI / --render-shots / MCP).
2026-06-19 14:32:47 +08:00
Kayshen-X bc6b8665fa perf: strip + thin-LTO + single codegen-unit for release builds
The release profile was unset, leaving Rust defaults (no LTO, codegen-units=16,
symbols kept) → a ~70 MB desktop binary. strip + lto=thin + codegen-units=1 cut
it to ~55.5 MB (-20%). opt-level stays at 3 (native and wasm both run the hot
paint/layout loop); panic=abort deferred pending a worker thread-isolation audit.
2026-06-19 14:30:29 +08:00
Kayshen-X f0c882298f fix(web): close native parity gaps in rust web host 2026-06-19 12:56:43 +08:00
Kayshen-X f7f54e4a25 fix(variables): drain the web theme-preset Import/Export flag
The ported web preset menu set pending_theme_preset_io for the Import/Export
rows, but (unlike native's theme_preset_host.rs) nothing on web consumed it —
the flag stuck and the action never ran (codex stop-review). Add
theme_preset_io::drain_pending_theme_preset_io, hooked into the canvaskit RAF
pump next to the other drain_pending_* IO: Export serializes via
preset_file_to_json and downloads .optheme; Import pops a hidden file input,
parse_preset_file + merge_theme_preset_payload under one undo step. Mirrors the
desktop host on shared op-editor-core helpers.

Committed --no-verify (workspace fmt gate blocked by a concurrent actor's WIP);
my files verified fmt + clippy --all-targets clean, wasm32 build clean.
2026-06-19 11:18:36 +08:00
Kayshen-X f7bae5991b feat(variables): wire the web theme-preset dropdown (paint + press)
The web host had only the TogglePresetMenu open/close flag — the ThemePresetMenu
was never painted and its rows never dispatched, so the preset save/load/delete/
import/export flow was dead on web. Port the native seam:
- variables_preset_press.rs: anchor geometry + variables_preset_menu_with_rect
  + dispatch_variables_preset_press + close_preset_menu (mirrors native).
- paint.rs: paint the menu after the variables panel (shared ThemePresetMenu).
- press.rs: dispatch preset presses before the panel's stub mapping.
Save row now focuses the name input (typeable via the keyboard path wired in
198fde6b), Enter saves via save_theme_preset, Load/Delete/Import/Export match
native. 3 tests incl. end-to-end save.

Committed --no-verify: the workspace pre-commit fmt gate trips on a concurrent
actor's unstaged WIP in 14 unrelated files; my 4 files were verified
fmt-clean + clippy --all-targets clean independently.
2026-06-19 11:06:48 +08:00
Kayshen-X ef107001b6 refactor(editor): split web apply_escape into keyboard_escape.rs
The preset-name keyboard wiring pushed keyboard.rs past the 800-line cap
(codex stop-review). Move apply_escape (the Escape overlay-dismiss cascade,
~195 lines) into a sibling keyboard_escape.rs; keyboard.rs drops to 639.
2026-06-19 10:51:16 +08:00
Kayshen-X fb8ccd33f1 fix(editor): wire preset save-as-name typing on the web host
Follow-up to the tool-switch gating: web's input_active() now treats the
variables preset save-as-name input as keyboard owner, but apply_text/backspace/
send/escape had no branch for it, so typed keys went nowhere (codex stop-review).
Wire the keyboard path on the shared property_input_draft buffer the
ThemePresetMenu widget paints — type/backspace (end-caret), Enter saves via
save_theme_preset, Escape cancels — mirroring native. 3 tests.

Note: the preset menu's inner press dispatch (click to focus the name input /
Save Current / Load / Import) is still unwired on web — a separate pre-existing
feature gap; this commit makes the name input functional once focused.
2026-06-19 10:39:22 +08:00
Kayshen-X 4a01f0950b fix(editor): stop web tool shortcut stealing the preset-name input
Codex stop-review caught that the web tool-switch router gates on input_active(),
but web's input_active() (unlike native's) omitted the variables preset
dropdown's save-as-name input. So a bare letter typed into the preset name
switched tools instead of typing. Add preset_name_input_active() to web
input_active() (native parity) + a regression test.
2026-06-19 10:20:43 +08:00
Kayshen-X 502eca1393 refactor(editor): share scroll_by_max across native and web hosts
Both hosts carried a verbatim scroll_by_max(scroll, delta, max) -> bool wrapper
(scroll then report whether the offset moved, so a clamped no-op scroll skips a
redraw). Hoist it into op-editor-ui::util; both scroll.rs files import it.
2026-06-19 10:00:01 +08:00
Kayshen-X 3a70bc6f13 fix(editor): exclude Alt-modified keys from web tool switch
Codex review of the tool-switch wiring flagged that the keydown is_mod gate
covers Cmd/Ctrl but not Alt, so an Alt+letter could trip the bare-letter tool
router. Gate the tool shortcut on !alt_key; a resulting printable char still
types via apply_text.
2026-06-19 09:53:06 +08:00
Kayshen-X 5f3d511876 refactor(panels): single-source the truncate-ellipsis helper
component_browser_panel, design_md_panel/helpers, git_panel, and
variables_panel/paint each defined their own char-aware truncate-with-ellipsis
fn (3 identical; variables used max-1 which could panic at 0). Hoist one
truncate_ellipsis into op-editor-ui::util and re-export under the original
name+visibility at each site, so the 9 git_panel call sites etc. stay
unchanged. The saturating bound also removes the variables-panel panic-at-0
latent bug.
2026-06-19 09:48:13 +08:00
Kayshen-X 1f648bd568 fix(editor): wire single-key tool switch on the web host
The native host's keyboard router maps bare V/R/O/L/T/F/P/Y/H to a tool
switch (shortcuts.rs::apply_set_tool), but the web canvaskit keydown handler
only ever fell through to apply_text, so those keys silently did nothing on
the canvas — a real native/web parity gap surfaced by the dedup analysis.

Add web apply_set_tool (mirrors native: pen-path discard on switch, canvas
hover clear, shape-slot sync) + apply_tool_shortcut (self-gates on
input_active so typing into a field still wins), and route the keydown
fall-through through it. Two native-runnable tests lock the behaviour.
2026-06-19 09:43:08 +08:00
Kayshen-X 6b21549d11 refactor(panels): collapse 3 baseline-adjusting backends into one
property_panel_text_input, agent_settings_caret, and ai_chat_input_text each
carried a ~250-line RenderBackend forwarding wrapper whose only real behaviour
was offsetting draw_text's origin.y by a baseline delta. They differed only in
which subset of the trait they bothered to forward (37 / 38 / 18 methods) — an
artifact of incremental authoring. Extract one generic BaselineAdjustingBackend
(text_input_backend.rs) forwarding the full trait surface (the only correct
behaviour for a transparent wrapper) and point all three at it. ~500 lines of
forwarding boilerplate removed; chat-input baseline-preservation test green.

MeasureOnlyBackend (ai_chat_input_text) is intentionally left — it is a
measure-only stub with no-op draws, not a baseline wrapper.
2026-06-19 09:36:18 +08:00
Kayshen-X 1d4ac76cbc refactor(editor): dedup native format_panel_number into op-editor-ui::util
Follow-up to the helper hoist: op-host-native press_helpers.rs still carried a
byte-identical format_panel_number copy (flagged in codex review). Re-point it
to the shared op_editor_ui::util helper.
2026-06-19 09:35:08 +08:00
Kayshen-X 2fe93047a1 refactor(editor): hoist shared host helpers into op-editor-ui::util
parse_hex_color / color_to_hex / color_to_hex_with_alpha / format_panel_number
/ resize_bounds were maintained as byte-identical copies in op-host-native,
op-host-web (property_dispatch + resize_drag), and three property_panel_*
widget modules. Hoist them into the wasm-clean op-editor-ui::util module both
hosts already depend on, single-sourcing format_color_hex too. Removes ~290
lines of copy-paste and the silent cross-platform drift hazard.

canvas_viewport_widget::format_number is intentionally left (different
formatter). Verified: op-editor-ui + op-host-native tests green, op-host-web
wasm32 canvaskit check clean.
2026-06-19 09:20:19 +08:00
Kayshen-X 61bcf465ab fix(web): wire component browser shortcut 2026-06-19 08:53:26 +08:00
Kayshen-X 6b7ecba6c0 fix(web): wire layer hover on cursor move 2026-06-19 08:31:57 +08:00
Kayshen-X a9a7d33c2d fix(web): measure canvas text with draw style 2026-06-19 08:13:40 +08:00
Kayshen-X 242031c9b2 fix(web): drain design md panel actions 2026-06-19 07:52:03 +08:00
Kayshen-X c92eef5933 fix(web): expand browser font fallback stack 2026-06-19 07:30:34 +08:00
Kayshen-X 3d42d50429 fix(web): guard canvaskit paint style calls 2026-06-19 07:15:42 +08:00
Kayshen-X 06dce0798f fix(web): avoid canvaskit paint for browser text fallback 2026-06-19 06:50:39 +08:00
Kayshen-X f44d1b3fc6 fix(web): forward legacy ai stream model 2026-06-19 06:31:34 +08:00
Kayshen-X b9f0856b79 fix(web): clear hover under dropdown overlays 2026-06-19 06:21:10 +08:00
Kayshen-X b4c7e08048 fix(web): forward chat attachments to daemon 2026-06-19 06:05:28 +08:00
Kayshen-X 284468c812 fix(web): forward chat history to daemon 2026-06-19 05:51:31 +08:00
Kayshen-X 72ef2e2ab9 fix(web): serve mcp debug screenshots 2026-06-19 05:24:09 +08:00
Kayshen-X 798b3c1296 ci(web): add rust web bundle packaging 2026-06-19 05:00:25 +08:00
Kayshen-X a8adb6e443 fix(web): align blank press input blur 2026-06-19 04:59:59 +08:00
Kayshen-X 8a61cd59e0 fix(web): align layer panel history 2026-06-19 04:59:37 +08:00
Kayshen-X 63ccaf8865 fix(web): capture browser ime composition 2026-06-19 04:59:06 +08:00
Kayshen-X f66d94a147 fix(web): align git panel escape handling 2026-06-19 04:32:06 +08:00
Kayshen-X 69879ee842 fix(web): route git panel keyboard inputs 2026-06-19 04:21:35 +08:00