From 55fe53235db95b2a810a4f12c080d5ecadc5aaf3 Mon Sep 17 00:00:00 2001 From: DaZuo0122 <1085701449@qq.com> Date: Fri, 13 Feb 2026 11:22:46 +0800 Subject: [PATCH] Add: just commands for release build --- crates/sprimo-app/src/main.rs | 31 +++++--- crates/sprimo-platform/src/lib.rs | 2 +- crates/sprimo-runtime-core/src/lib.rs | 46 ++++++++--- crates/sprimo-tauri/capabilities/default.json | 1 + .../gen/schemas/capabilities.json | 2 +- crates/sprimo-tauri/src/main.rs | 29 ++++++- crates/sprimo-tauri/tauri.conf.json | 4 +- docs/API_SPEC.md | 9 ++- docs/CONFIG_REFERENCE.md | 3 +- docs/FRONTEND_REQUIREMENTS.md | 45 +++++------ docs/IMPLEMENTATION_STATUS.md | 4 +- docs/MVP_ACCEPTANCE.md | 2 +- docs/PLATFORM_CAPABILITY_MATRIX.md | 5 +- docs/RELEASE_TESTING.md | 30 +++++-- docs/TAURI_FRONTEND_DESIGN.md | 3 + docs/TAURI_RUNTIME_TESTING.md | 4 + frontend/tauri-ui/src/main.tsx | 15 +++- frontend/tauri-ui/src/styles.css | 1 + issues/issue2.md | 14 +++- justfile | 21 +++++ scripts/package_windows.py | 79 ++++++++++++++----- 21 files changed, 253 insertions(+), 97 deletions(-) diff --git a/crates/sprimo-app/src/main.rs b/crates/sprimo-app/src/main.rs index 11aa77d..d5eb0f5 100644 --- a/crates/sprimo-app/src/main.rs +++ b/crates/sprimo-app/src/main.rs @@ -26,6 +26,7 @@ use tracing::{error, info, warn}; const APP_NAME: &str = "sprimo"; const DEFAULT_PACK: &str = "default"; const WINDOW_PADDING: f32 = 16.0; +const STARTUP_WINDOW_SIZE: f32 = 416.0; const MAGENTA_KEY: [u8; 3] = [255, 0, 255]; const CHROMA_KEY_TOLERANCE: u8 = 24; const CHROMA_KEY_FORCE_RATIO: f32 = 0.15; @@ -180,7 +181,13 @@ fn main() -> Result<(), AppError> { .compact() .init(); - let (config_path, config) = sprimo_config::load_or_create(APP_NAME)?; + let (config_path, mut config) = sprimo_config::load_or_create(APP_NAME)?; + if config.window.click_through { + config.window.click_through = false; + if let Err(err) = save(&config_path, &config) { + warn!(%err, "failed to persist click-through disable at startup"); + } + } let platform: Arc = create_adapter().into(); let capabilities = platform.capabilities(); @@ -188,7 +195,7 @@ fn main() -> Result<(), AppError> { snapshot.x = config.window.x; snapshot.y = config.window.y; snapshot.scale = config.window.scale; - snapshot.flags.click_through = config.window.click_through; + snapshot.flags.click_through = false; snapshot.flags.always_on_top = config.window.always_on_top; snapshot.flags.visible = config.window.visible; snapshot.active_sprite_pack = config.sprite.selected_pack.clone(); @@ -223,7 +230,10 @@ fn main() -> Result<(), AppError> { decorations: false, resizable: false, window_level: WindowLevel::AlwaysOnTop, - resolution: bevy::window::WindowResolution::new(544.0, 544.0), + resolution: bevy::window::WindowResolution::new( + STARTUP_WINDOW_SIZE, + STARTUP_WINDOW_SIZE, + ), position: WindowPosition::At(IVec2::new( config.window.x.round() as i32, config.window.y.round() as i32, @@ -721,7 +731,6 @@ fn attach_window_handle_once( .read() .expect("frontend snapshot lock poisoned"); let _ = platform.0.set_always_on_top(guard.flags.always_on_top); - let _ = platform.0.set_click_through(guard.flags.click_through); let _ = platform.0.set_visible(guard.flags.visible); let _ = platform.0.set_window_position(guard.x, guard.y); *attached = true; @@ -746,14 +755,14 @@ fn poll_hotkey_recovery( ) { while ingress.0.try_recv().is_ok() { info!("recovery hotkey received"); - let _ = platform.0.set_click_through(false); + let _ = platform.0.set_always_on_top(true); let _ = platform.0.set_visible(true); if let Ok(mut visibility) = pet_query.get_single_mut() { *visibility = Visibility::Visible; } - config.config.window.click_through = false; + config.config.window.always_on_top = true; config.config.window.visible = true; if let Err(err) = save(&config.path, &config.config) { warn!(%err, "failed to persist config after hotkey recovery"); @@ -761,6 +770,7 @@ fn poll_hotkey_recovery( if let Ok(mut guard) = snapshot.0.write() { guard.flags.click_through = false; + guard.flags.always_on_top = true; guard.flags.visible = true; guard.last_error = None; } @@ -927,9 +937,8 @@ fn poll_backend_commands( always_on_top, visible, } => { - if let Some(value) = click_through { - let _ = platform.0.set_click_through(value); - config.config.window.click_through = value; + if click_through.is_some() { + config.config.window.click_through = false; } if let Some(value) = always_on_top { let _ = platform.0.set_always_on_top(value); @@ -949,9 +958,7 @@ fn poll_backend_commands( warn!(%err, "failed to persist flag config"); } if let Ok(mut guard) = snapshot.0.write() { - if let Some(value) = click_through { - guard.flags.click_through = value; - } + guard.flags.click_through = false; if let Some(value) = always_on_top { guard.flags.always_on_top = value; } diff --git a/crates/sprimo-platform/src/lib.rs b/crates/sprimo-platform/src/lib.rs index eaa0a2d..78da3bd 100644 --- a/crates/sprimo-platform/src/lib.rs +++ b/crates/sprimo-platform/src/lib.rs @@ -171,7 +171,7 @@ mod windows { impl PlatformAdapter for WindowsAdapter { fn capabilities(&self) -> CapabilityFlags { CapabilityFlags { - supports_click_through: true, + supports_click_through: false, supports_transparency: true, supports_tray: false, supports_global_hotkey: true, diff --git a/crates/sprimo-runtime-core/src/lib.rs b/crates/sprimo-runtime-core/src/lib.rs index 750e5a5..9b5ed74 100644 --- a/crates/sprimo-runtime-core/src/lib.rs +++ b/crates/sprimo-runtime-core/src/lib.rs @@ -35,14 +35,16 @@ impl RuntimeCore { pub fn new_with_config( config_path: PathBuf, - config_value: AppConfig, + mut config_value: AppConfig, capabilities: CapabilityFlags, ) -> Result { + let click_through_was_enabled = config_value.window.click_through; + config_value.window.click_through = false; let mut snapshot = FrontendStateSnapshot::idle(capabilities); snapshot.x = config_value.window.x; snapshot.y = config_value.window.y; snapshot.scale = config_value.window.scale; - snapshot.flags.click_through = config_value.window.click_through; + snapshot.flags.click_through = false; snapshot.flags.always_on_top = config_value.window.always_on_top; snapshot.flags.visible = config_value.window.visible; snapshot.active_sprite_pack = config_value.sprite.selected_pack.clone(); @@ -50,14 +52,18 @@ impl RuntimeCore { let api_config = ApiConfig::default_with_token(config_value.api.auth_token.clone()); let (command_tx, command_rx) = mpsc::channel(1_024); - Ok(Self { + let core = Self { config_path, config: Arc::new(RwLock::new(config_value)), snapshot: Arc::new(RwLock::new(snapshot)), api_config, command_tx, command_rx: Arc::new(Mutex::new(command_rx)), - }) + }; + if click_through_was_enabled { + core.persist_config()?; + } + Ok(core) } pub fn snapshot(&self) -> Arc> { @@ -169,7 +175,7 @@ impl RuntimeCore { self.persist_config()?; } FrontendCommand::SetFlags { - click_through, + click_through: _click_through, always_on_top, visible, } => { @@ -178,9 +184,7 @@ impl RuntimeCore { .snapshot .write() .map_err(|_| RuntimeCoreError::SnapshotPoisoned)?; - if let Some(value) = click_through { - snapshot.flags.click_through = *value; - } + snapshot.flags.click_through = false; if let Some(value) = always_on_top { snapshot.flags.always_on_top = *value; } @@ -194,9 +198,7 @@ impl RuntimeCore { .config .write() .map_err(|_| RuntimeCoreError::ConfigPoisoned)?; - if let Some(value) = click_through { - config.window.click_through = *value; - } + config.window.click_through = false; if let Some(value) = always_on_top { config.window.always_on_top = *value; } @@ -259,4 +261,26 @@ mod tests { assert_eq!(snapshot.state, FrontendState::Active); assert_eq!(snapshot.current_animation, "active"); } + + #[test] + fn click_through_flag_is_ignored_and_forced_false() { + let temp = TempDir::new().expect("tempdir"); + let path = temp.path().join("config.toml"); + let mut config = AppConfig::default(); + config.window.click_through = true; + let core = RuntimeCore::new_with_config(path, config, CapabilityFlags::default()) + .expect("core init"); + + core.apply_command(&FrontendCommand::SetFlags { + click_through: Some(true), + always_on_top: None, + visible: None, + }) + .expect("apply"); + + let snapshot = core.snapshot().read().expect("snapshot lock").clone(); + assert!(!snapshot.flags.click_through); + let config = core.config().read().expect("config lock").clone(); + assert!(!config.window.click_through); + } } diff --git a/crates/sprimo-tauri/capabilities/default.json b/crates/sprimo-tauri/capabilities/default.json index a5a81ec..d4d74c2 100644 --- a/crates/sprimo-tauri/capabilities/default.json +++ b/crates/sprimo-tauri/capabilities/default.json @@ -5,6 +5,7 @@ "windows": ["*"], "permissions": [ "core:default", + "core:window:allow-start-dragging", "core:event:allow-listen", "core:event:allow-unlisten" ] diff --git a/crates/sprimo-tauri/gen/schemas/capabilities.json b/crates/sprimo-tauri/gen/schemas/capabilities.json index fbed6e5..04b215a 100644 --- a/crates/sprimo-tauri/gen/schemas/capabilities.json +++ b/crates/sprimo-tauri/gen/schemas/capabilities.json @@ -1 +1 @@ -{"default":{"identifier":"default","description":"Default capability for sprimo-tauri main window runtime APIs.","local":true,"windows":["*"],"permissions":["core:default","core:event:allow-listen","core:event:allow-unlisten"]}} \ No newline at end of file +{"default":{"identifier":"default","description":"Default capability for sprimo-tauri main window runtime APIs.","local":true,"windows":["*"],"permissions":["core:default","core:window:allow-start-dragging","core:event:allow-listen","core:event:allow-unlisten"]}} \ No newline at end of file diff --git a/crates/sprimo-tauri/src/main.rs b/crates/sprimo-tauri/src/main.rs index e3e47f6..839c60d 100644 --- a/crates/sprimo-tauri/src/main.rs +++ b/crates/sprimo-tauri/src/main.rs @@ -153,6 +153,8 @@ fn main() -> Result<(), AppError> { } let command_rx = runtime_core.command_receiver(); + let runtime_core_for_commands = Arc::clone(&runtime_core); + let app_handle_for_commands = app_handle.clone(); runtime.spawn(async move { loop { let next = { @@ -163,24 +165,45 @@ fn main() -> Result<(), AppError> { break; }; - if let Err(err) = runtime_core.apply_command(&envelope.command) { + if let Err(err) = runtime_core_for_commands.apply_command(&envelope.command) { warn!(%err, "failed to apply command in tauri runtime"); continue; } let payload = { - let snapshot = runtime_core.snapshot(); + let snapshot = runtime_core_for_commands.snapshot(); match snapshot.read() { Ok(s) => Some(to_ui_snapshot(&s)), Err(_) => None, } }; if let Some(value) = payload { - let _ = app_handle.emit("runtime:snapshot", value); + let _ = app_handle_for_commands.emit("runtime:snapshot", value); } } }); + if let Some(window) = app.get_webview_window("main") { + let runtime_core = Arc::clone(&runtime_core); + let app_handle = app_handle.clone(); + window.on_window_event(move |event| { + if let tauri::WindowEvent::Moved(position) = event { + let command = sprimo_protocol::v1::FrontendCommand::SetTransform { + x: Some(position.x as f32), + y: Some(position.y as f32), + anchor: None, + scale: None, + opacity: None, + }; + if runtime_core.apply_command(&command).is_ok() { + if let Ok(snapshot) = runtime_core.snapshot().read() { + let _ = app_handle.emit("runtime:snapshot", to_ui_snapshot(&snapshot)); + } + } + } + }); + } + let _ = app; Ok(()) }) diff --git a/crates/sprimo-tauri/tauri.conf.json b/crates/sprimo-tauri/tauri.conf.json index 27a3615..215d032 100644 --- a/crates/sprimo-tauri/tauri.conf.json +++ b/crates/sprimo-tauri/tauri.conf.json @@ -12,8 +12,8 @@ "windows": [ { "title": "sprimo-tauri", - "width": 640, - "height": 640, + "width": 416, + "height": 416, "decorations": false, "transparent": true, "alwaysOnTop": true, diff --git a/docs/API_SPEC.md b/docs/API_SPEC.md index 2ad5ecf..e1414e0 100644 --- a/docs/API_SPEC.md +++ b/docs/API_SPEC.md @@ -18,9 +18,9 @@ Auth: `Authorization: Bearer ` required on all endpoints except `/v1/heal "uptime_seconds": 12, "active_sprite_pack": "default", "capabilities": { - "supports_click_through": true, + "supports_click_through": false, "supports_transparency": true, - "supports_tray": true, + "supports_tray": false, "supports_global_hotkey": true, "supports_skip_taskbar": true } @@ -70,6 +70,11 @@ Auth: `Authorization: Bearer ` required on all endpoints except `/v1/heal } ``` +## Compatibility Notes + +- `SetFlags.click_through` is deprecated for compatibility and ignored at runtime. +- Frontend state always reports `flags.click_through = false`. + ## Error Response ```json diff --git a/docs/CONFIG_REFERENCE.md b/docs/CONFIG_REFERENCE.md index c11704a..2dd9dbf 100644 --- a/docs/CONFIG_REFERENCE.md +++ b/docs/CONFIG_REFERENCE.md @@ -45,5 +45,6 @@ backend = "bevy" - `auth_token` is generated on first run if config does not exist. - `window.x`, `window.y`, `window.scale`, and flag fields are persisted after matching commands. -- On Windows, `recovery_hotkey` provides click-through recovery even when the window is non-interactive. +- `window.click_through` is deprecated and ignored at runtime; it is always forced to `false`. +- On Windows, `recovery_hotkey` now forces `visible = true` and `always_on_top = true` for recovery. - `frontend.backend` selects runtime frontend implementation (`bevy` or `tauri`). diff --git a/docs/FRONTEND_REQUIREMENTS.md b/docs/FRONTEND_REQUIREMENTS.md index dda96a1..0c5fa98 100644 --- a/docs/FRONTEND_REQUIREMENTS.md +++ b/docs/FRONTEND_REQUIREMENTS.md @@ -10,7 +10,7 @@ # 1. Overview -The frontend is a **desktop overlay pet renderer** implemented in Rust (Bevy). It presents an animated character in a **transparent, borderless window** that can be **always-on-top** and optionally **click-through**. It receives control instructions from a local backend process via **localhost REST API**, applies them to its animation/state machine, and persists user preferences locally. +The frontend is a **desktop overlay pet renderer** implemented in Rust (Bevy). It presents an animated character in a **transparent, borderless window** that can be **always-on-top**. It receives control instructions from a local backend process via **localhost REST API**, applies them to its animation/state machine, and persists user preferences locally. The frontend must be able to run standalone (idle animation) even if the backend is not running. @@ -22,7 +22,7 @@ The frontend must be able to run standalone (idle animation) even if the backend 1. **Render a cute animated character overlay** with smooth sprite animation. 2. Provide a **stable command interface** (REST) for backend control. -3. Offer **essential user controls** (tray/menu + hotkeys optional) to avoid “locking” the pet in click-through mode. +3. Offer **essential user controls** (tray/menu + hotkeys optional) to keep the pet recoverable and visible. 4. Persist **window position, scale, sprite pack choice, and flags**. 5. Be **cross-platform** (Windows/macOS/Linux) with documented degradations, especially on Linux Wayland. @@ -43,7 +43,6 @@ The frontend must be able to run standalone (idle animation) even if the backend * As a user, I can see the pet on my desktop immediately after launch. * As a user, I can drag the pet to a preferred location. -* As a user, I can toggle click-through so the pet doesn’t block my mouse. * As a user, I can toggle always-on-top so the pet stays visible. * As a user, I can change the character (sprite pack). @@ -55,7 +54,7 @@ The frontend must be able to run standalone (idle animation) even if the backend ## Safety -* As a user, I can always recover control of the pet even if click-through is enabled (hotkey/tray item). +* As a user, I can always recover the pet visibility/interaction state via hotkey or tray item. --- @@ -82,22 +81,19 @@ The frontend must be able to run standalone (idle animation) even if the backend * When ON, window stays above normal windows. -### FR-FW-3 Click-through (mouse pass-through) +### FR-FW-3 Interaction model -* Support enabling/disabling click-through: - - * ON: mouse events pass to windows underneath. - * OFF: pet receives mouse input (drag, context menu). -* Must provide a **failsafe** mechanism to disable click-through without clicking the pet. +* Click-through is not required. +* Pet remains interactive while visible. +* Must provide a **failsafe** mechanism to recover visibility and interaction state. **Acceptance** -* With click-through enabled, user can click apps behind pet. -* User can disable click-through via tray or hotkey reliably. +* Recovery hotkey/tray action restores visible, interactive pet state reliably. ### FR-FW-4 Dragging & anchoring -* When click-through is OFF, user can drag the pet. +* User can drag the pet. * Dragging updates persisted position in config. * Optional: snapping to screen edges. @@ -130,12 +126,12 @@ The frontend must be able to run standalone (idle animation) even if the backend ### Platform notes (requirements) -* **Windows:** click-through uses extended window styles (WS_EX_TRANSPARENT / layered), always-on-top via SetWindowPos. +* **Windows:** always-on-top via SetWindowPos. * **macOS:** NSWindow level + ignoresMouseEvents. * **Linux:** best effort: * X11: possible with shape/input region. - * Wayland: click-through may be unavailable; document limitation. + * Wayland: overlay behavior limitations may apply; document limitation. --- @@ -273,7 +269,7 @@ Each state maps to a default animation (configurable by sprite pack): * `PlayAnimation { name, priority, duration_ms?, interrupt? }` * `SetSpritePack { pack_id_or_path }` * `SetTransform { x?, y?, anchor?, scale?, opacity? }` -* `SetFlags { click_through?, always_on_top?, visible? }` +* `SetFlags { click_through?, always_on_top?, visible? }` (`click_through` is deprecated/ignored) * `Toast { text, ttl_ms? }` (optional but recommended) ### FR-API-4 Idempotency & dedupe @@ -303,7 +299,6 @@ Each state maps to a default animation (configurable by sprite pack): Provide tray/menu bar items: * Show/Hide -* Toggle Click-through * Toggle Always-on-top * Sprite Pack selection (at least “Default” + “Open sprite folder…”) * Reload sprite packs @@ -315,7 +310,7 @@ If tray is too hard on Linux in v0.1, provide a fallback (hotkey + config). At minimum one global hotkey: -* Toggle click-through OR “enter interactive mode” +* Force visible + interactive recovery mode Example default: @@ -323,7 +318,7 @@ Example default: **Acceptance** -* User can recover control even if pet is click-through and cannot be clicked. +* User can recover visibility and interaction state even when the pet was hidden or misplaced. ### FR-CTL-3 Context menu (optional) @@ -348,7 +343,7 @@ Right click pet (when interactive) to open a minimal menu. * position (x,y) + monitor id (best-effort) * scale * always_on_top - * click_through + * click_through (deprecated/ignored; always false) * visible * animation: @@ -432,10 +427,10 @@ Frontend must expose in logs (and optionally `/v1/health`) capability flags: Example: -* Windows: all true -* macOS: all true +* Windows: click-through false; others vary by implementation status +* macOS: click-through false; others vary by implementation status * Linux X11: most true -* Linux Wayland: click-through likely false, skip-taskbar variable +* Linux Wayland: skip-taskbar variable and overlay behavior limitations --- @@ -444,8 +439,8 @@ Example: ## Window 1. Launch: window appears borderless & transparent. -2. Drag: with click-through OFF, drag updates position; restart restores. -3. Click-through: toggle via hotkey; pet becomes non-interactive; toggle back works. +2. Drag: drag updates position; restart restores. +3. Recovery: hotkey restores visible + always-on-top behavior reliably. 4. Always-on-top: verify staying above typical apps. ## Animation diff --git a/docs/IMPLEMENTATION_STATUS.md b/docs/IMPLEMENTATION_STATUS.md index 954f1a7..2253588 100644 --- a/docs/IMPLEMENTATION_STATUS.md +++ b/docs/IMPLEMENTATION_STATUS.md @@ -11,9 +11,9 @@ Date: 2026-02-12 | Command/state pipeline | Implemented | Command queue, state snapshot updates, transient state TTL rollback | | Config persistence | Implemented | `config.toml` bootstrap/load/save with generated token | | Sprite pack contract | Implemented | `manifest.json` loader and selected->default fallback | -| Platform abstraction | Implemented | Windows adapter now applies click-through/top-most/visibility/position using Win32 APIs | +| Platform abstraction | Implemented | Windows adapter applies top-most/visibility/position using Win32 APIs; click-through is disabled by current requirements | | Overlay rendering | Implemented (MVP) | Bevy runtime with transparent undecorated window, sprite playback, command bridge | -| Global failsafe | Implemented (Windows) | Global recovery hotkey `Ctrl+Alt+P` disables click-through and forces visibility | +| Global failsafe | Implemented (Windows) | Global recovery hotkey `Ctrl+Alt+P` forces visibility and top-most recovery | | Embedded default pack | Implemented | Bundled under `assets/sprite-packs/default/` using `sprite.png` (8x7, 512x512 frames) | | Build/package automation | Implemented (Windows) | `justfile` and `scripts/package_windows.py` generate portable ZIP + SHA256 | | QA/documentation workflow | Implemented | `docs/QA_WORKFLOW.md`, issue/evidence templates, and `scripts/qa_validate.py` with `just qa-validate` | diff --git a/docs/MVP_ACCEPTANCE.md b/docs/MVP_ACCEPTANCE.md index 019ed69..2f37f78 100644 --- a/docs/MVP_ACCEPTANCE.md +++ b/docs/MVP_ACCEPTANCE.md @@ -14,7 +14,7 @@ - [x] `SetState` updates state and default animation mapping. - [x] transient state with `ttl_ms` returns to durable state. - [x] `SetTransform` persists x/y/scale. -- [x] `SetFlags` persists click-through/always-on-top/visible. +- [x] `SetFlags` persists always-on-top/visible and ignores deprecated click-through. ## Config diff --git a/docs/PLATFORM_CAPABILITY_MATRIX.md b/docs/PLATFORM_CAPABILITY_MATRIX.md index c489df3..6fce375 100644 --- a/docs/PLATFORM_CAPABILITY_MATRIX.md +++ b/docs/PLATFORM_CAPABILITY_MATRIX.md @@ -4,7 +4,7 @@ Date: 2026-02-12 | Capability | Windows | Linux X11 | Linux Wayland | macOS | |------------|---------|-----------|---------------|-------| -| `supports_click_through` | true (implemented) | false (current) | false | false (current) | +| `supports_click_through` | false (disabled by product requirement) | false | false | false | | `supports_transparency` | true | true | true | true | | `supports_tray` | false (current) | false (current) | false (current) | false (current) | | `supports_global_hotkey` | true (implemented) | false (current) | false (current) | false (current) | @@ -12,6 +12,7 @@ Date: 2026-02-12 ## Notes -- Current code applies real Win32 operations for click-through, visibility, top-most, and positioning. +- Current code applies real Win32 operations for visibility, top-most, and positioning. +- Click-through is intentionally disabled by current product requirements. - Non-Windows targets currently use a no-op adapter with conservative flags. - Wayland limitations remain an expected degradation in v0.1. diff --git a/docs/RELEASE_TESTING.md b/docs/RELEASE_TESTING.md index 645a90b..090064c 100644 --- a/docs/RELEASE_TESTING.md +++ b/docs/RELEASE_TESTING.md @@ -23,13 +23,24 @@ Use `just` for command entry: ```powershell just check just test -just build-release -just package-win -just smoke-win +just build-release-bevy +just package-win-bevy +just smoke-win-bevy +just build-release-tauri +just package-win-tauri +just smoke-win-tauri ``` -`just package-win` calls `scripts/package_windows.py package`. -`just smoke-win` calls `scripts/package_windows.py smoke`. +Compatibility aliases: + +- `just build-release` -> Bevy release build. +- `just package-win` -> Bevy package. +- `just smoke-win` -> Bevy smoke package check. + +Packaging script target selection: + +- Bevy: `python scripts/package_windows.py package --frontend bevy` +- Tauri: `python scripts/package_windows.py package --frontend tauri` ## Behavior Test Checklist (Packaged App) @@ -37,8 +48,8 @@ Run tests from an unpacked ZIP folder, not from the workspace run. 1. Launch `sprimo-app.exe`; verify default sprite renders. 2. Verify no terminal window appears when launching release build by double-click. -3. Verify global hotkey recovery (`Ctrl+Alt+P`) forces interactive mode. -4. Verify click-through and always-on-top toggles via API commands. +3. Verify global hotkey recovery (`Ctrl+Alt+P`) forces visibility and top-most recovery. +4. Verify `SetFlags` applies always-on-top and visibility via API commands. 5. Verify `/v1/health` and `/v1/state` behavior with auth. 6. Verify `SetSpritePack`: - valid pack switches runtime visuals @@ -46,6 +57,7 @@ Run tests from an unpacked ZIP folder, not from the workspace run. 7. Restart app and verify persisted config behavior. 8. Confirm overlay background is transparent (desktop visible behind non-sprite pixels). 9. Confirm no magenta matte remains around sprite in default pack. +10. Confirm default startup window footprint is reduced (416x416 before runtime pack resize). ## Test Log Template @@ -90,6 +102,10 @@ Authoritative workflow: `docs/TAURI_RUNTIME_TESTING.md`. - `/v1/state` with auth - `/v1/command` - `/v1/commands` +7. Verify tauri frameless drag: +- left-mouse drag moves window +- window remains non-resizable +- moved position updates runtime snapshot `x/y` and persists after restart ### Packaged Mode (Required Once Tauri Packaging Exists) diff --git a/docs/TAURI_FRONTEND_DESIGN.md b/docs/TAURI_FRONTEND_DESIGN.md index 3655ea3..5660f56 100644 --- a/docs/TAURI_FRONTEND_DESIGN.md +++ b/docs/TAURI_FRONTEND_DESIGN.md @@ -50,6 +50,9 @@ Frontend: - `runtime:snapshot` event after command application. - React/Vite frontend now renders sprite atlas frames with PixiJS and updates animation/scale from runtime snapshot events. +- Tauri window drag is implemented for undecorated mode: + - left-mouse drag starts native window dragging + - moved position is synced into runtime-core snapshot/config state. - Bevy frontend remains intact. - Runtime QA workflow is defined in `docs/TAURI_RUNTIME_TESTING.md`. diff --git a/docs/TAURI_RUNTIME_TESTING.md b/docs/TAURI_RUNTIME_TESTING.md index ef3f368..f06b000 100644 --- a/docs/TAURI_RUNTIME_TESTING.md +++ b/docs/TAURI_RUNTIME_TESTING.md @@ -68,6 +68,10 @@ An issue touching Tauri runtime behaviors must satisfy all requirements before ` 7. Verify sprite-pack loading: - valid selected pack loads correctly - invalid pack path failure is surfaced and runtime remains alive +8. Verify frameless window drag behavior: +- left-mouse drag moves the window +- window remains non-resizable +- moved position is reflected in runtime snapshot state (`x`, `y`) and persists after restart ## API + Runtime Contract Checklist diff --git a/frontend/tauri-ui/src/main.tsx b/frontend/tauri-ui/src/main.tsx index 27a5f04..cae4281 100644 --- a/frontend/tauri-ui/src/main.tsx +++ b/frontend/tauri-ui/src/main.tsx @@ -2,11 +2,10 @@ import React from "react"; import ReactDOM from "react-dom/client"; import { invoke } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; +import { getCurrentWindow } from "@tauri-apps/api/window"; import { PixiPetRenderer, type UiSpritePack, type UiSnapshot } from "./renderer/pixi_pet"; import "./styles.css"; -const UI_BUILD_MARKER = "issue2-fix3"; - function App(): JSX.Element { const [snapshot, setSnapshot] = React.useState(null); const [error, setError] = React.useState(null); @@ -56,12 +55,20 @@ function App(): JSX.Element { }; }, []); + const onMouseDown = React.useCallback((event: React.MouseEvent) => { + if (event.button !== 0) { + return; + } + void getCurrentWindow().startDragging().catch((err) => { + setError(String(err)); + }); + }, []); + return ( -
+

sprimo-tauri

-

ui build: {UI_BUILD_MARKER}

{error !== null ?

{error}

: null} {snapshot === null ? (

Loading snapshot...

diff --git a/frontend/tauri-ui/src/styles.css b/frontend/tauri-ui/src/styles.css index 7d41cff..b2db418 100644 --- a/frontend/tauri-ui/src/styles.css +++ b/frontend/tauri-ui/src/styles.css @@ -13,6 +13,7 @@ body { width: 100vw; height: 100vh; position: relative; + user-select: none; } .canvas-host { diff --git a/issues/issue2.md b/issues/issue2.md index c240711..1df257f 100644 --- a/issues/issue2.md +++ b/issues/issue2.md @@ -63,7 +63,7 @@ This breaks sprite presentation and leaves the UI in an error state. Implemented: 1. `frontend/tauri-ui/package.json` -- Added `@pixi/assets` dependency. +- Removed temporary `@pixi/assets` dependency after switching loader strategy. 2. `frontend/tauri-ui/src/renderer/pixi_pet.ts` - Switched atlas loading to `Assets.load(pack.atlas_data_url)`. @@ -85,6 +85,7 @@ Implemented: 6. `frontend/tauri-ui/src/main.tsx` - Added visible UI build marker (`issue2-fix3`) to detect stale embedded frontend artifacts. +- Removed temporary UI build marker after verification passed. 7. `crates/sprimo-tauri/capabilities/default.json` - Added default capability with: @@ -102,6 +103,9 @@ Implemented: ### Commands Run +- [x] `cargo check --workspace` +- [x] `cargo test --workspace` +- [x] `just qa-validate` - [x] `cargo check -p sprimo-tauri` - [x] `cargo check -p sprimo-runtime-core` - [x] `just build-tauri-ui` @@ -128,8 +132,8 @@ Implemented: ### Result -- Current Status: `Fix Implemented` -- Notes: build/check/qa validation passed; manual runtime visual verification still required. +- Current Status: `Verification Passed` +- Notes: reporter confirmed fix works on runtime behavior; closure gate evidence still to be completed. ## Status History @@ -143,10 +147,12 @@ Implemented: - `2026-02-13 15:05` - codex - `In Progress` - added explicit UI build marker to detect stale executable/frontend embedding. - `2026-02-13 15:20` - reporter - `In Progress` - provided `issue2-after-fix3` screenshot; stale-build issue resolved, permission + chroma-key defects observed. - `2026-02-13 15:35` - codex - `Fix Implemented` - added tauri capability permission file and tauri-side chroma-key conversion. +- `2026-02-13 15:50` - reporter - `Verification Passed` - confirmed the runtime fix works. +- `2026-02-13 16:05` - codex - `Verification Passed` - completed workspace check/test checklist and normalized issue record. ## Closure -- Current Status: `Fix Implemented` +- Current Status: `Verification Passed` - Close Date: - Owner: - Linked PR/commit: diff --git a/justfile b/justfile index 23f91c5..e2cbbe5 100644 --- a/justfile +++ b/justfile @@ -18,6 +18,27 @@ package-win: smoke-win: {{python}} scripts/package_windows.py smoke +build-release-bevy: + cargo build --release -p sprimo-app + +build-release-tauri: + just build-tauri-ui + cargo build --release -p sprimo-tauri + +package-win-bevy: + {{python}} scripts/package_windows.py package --frontend bevy + +smoke-win-bevy: + {{python}} scripts/package_windows.py smoke --frontend bevy + +package-win-tauri: + just build-tauri-ui + {{python}} scripts/package_windows.py package --frontend tauri + +smoke-win-tauri: + just build-tauri-ui + {{python}} scripts/package_windows.py smoke --frontend tauri + qa-validate: {{python}} scripts/qa_validate.py diff --git a/scripts/package_windows.py b/scripts/package_windows.py index 5118993..5edc692 100644 --- a/scripts/package_windows.py +++ b/scripts/package_windows.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Build and package a portable Windows ZIP for sprimo-app.""" +"""Build and package portable Windows ZIPs for Bevy/Tauri frontends.""" from __future__ import annotations @@ -18,7 +18,6 @@ from typing import Iterable ROOT = Path(__file__).resolve().parents[1] DIST = ROOT / "dist" -BIN_REL = ROOT / "target" / "release" / "sprimo-app.exe" ASSETS_REL = ROOT / "assets" @@ -26,8 +25,36 @@ class PackagingError(RuntimeError): """Raised when packaging preconditions are not met.""" +@dataclass(frozen=True) +class FrontendLayout: + id: str + crate: str + binary_name: str + artifact_name: str + readme_run: str + + +FRONTENDS: dict[str, FrontendLayout] = { + "bevy": FrontendLayout( + id="bevy", + crate="sprimo-app", + binary_name="sprimo-app.exe", + artifact_name="sprimo-windows-x64", + readme_run="sprimo-app.exe", + ), + "tauri": FrontendLayout( + id="tauri", + crate="sprimo-tauri", + binary_name="sprimo-tauri.exe", + artifact_name="sprimo-tauri-windows-x64", + readme_run="sprimo-tauri.exe", + ), +} + + @dataclass(frozen=True) class PackageLayout: + frontend: str version: str zip_path: Path checksum_path: Path @@ -72,12 +99,13 @@ def read_version() -> str: raise PackagingError("could not determine version") -def ensure_release_binary() -> Path: - if not BIN_REL.exists(): - run(["cargo", "build", "--release", "-p", "sprimo-app"]) - if not BIN_REL.exists(): - raise PackagingError(f"release binary missing: {BIN_REL}") - return BIN_REL +def ensure_release_binary(frontend: FrontendLayout) -> Path: + binary_path = ROOT / "target" / "release" / frontend.binary_name + if not binary_path.exists(): + run(["cargo", "build", "--release", "-p", frontend.crate]) + if not binary_path.exists(): + raise PackagingError(f"release binary missing: {binary_path}") + return binary_path def ensure_assets() -> None: @@ -108,13 +136,13 @@ def sha256_file(path: Path) -> str: return digest.hexdigest() -def package() -> PackageLayout: +def package(frontend: FrontendLayout) -> PackageLayout: version = read_version() ensure_assets() - binary = ensure_release_binary() + binary = ensure_release_binary(frontend) DIST.mkdir(parents=True, exist_ok=True) - artifact_name = f"sprimo-windows-x64-v{version}" + artifact_name = f"{frontend.artifact_name}-v{version}" zip_path = DIST / f"{artifact_name}.zip" checksum_path = DIST / f"{artifact_name}.zip.sha256" @@ -122,13 +150,14 @@ def package() -> PackageLayout: stage = Path(temp_dir) / artifact_name stage.mkdir(parents=True, exist_ok=True) - shutil.copy2(binary, stage / "sprimo-app.exe") + shutil.copy2(binary, stage / frontend.binary_name) shutil.copytree(ASSETS_REL, stage / "assets", dirs_exist_ok=True) readme = stage / "README.txt" readme.write_text( "Sprimo portable package\n" - "Run: sprimo-app.exe\n" + f"Frontend: {frontend.id}\n" + f"Run: {frontend.readme_run}\n" "Assets are expected at ./assets relative to the executable.\n", encoding="utf-8", ) @@ -143,11 +172,16 @@ def package() -> PackageLayout: checksum = sha256_file(zip_path) checksum_path.write_text(f"{checksum} {zip_path.name}\n", encoding="utf-8") - return PackageLayout(version=version, zip_path=zip_path, checksum_path=checksum_path) + return PackageLayout( + frontend=frontend.id, + version=version, + zip_path=zip_path, + checksum_path=checksum_path, + ) -def smoke() -> None: - layout = package() +def smoke(frontend: FrontendLayout) -> None: + layout = package(frontend) print(f"package created: {layout.zip_path}") print(f"checksum file: {layout.checksum_path}") @@ -160,7 +194,7 @@ def smoke() -> None: pkg_root = root_candidates[0] required = [ - pkg_root / "sprimo-app.exe", + pkg_root / frontend.binary_name, pkg_root / "assets" / "sprite-packs" / "default" / "manifest.json", pkg_root / "assets" / "sprite-packs" / "default" / "sprite.png", ] @@ -175,18 +209,25 @@ def smoke() -> None: def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="Sprimo Windows packaging helper") parser.add_argument("command", choices=["package", "smoke"], help="action to execute") + parser.add_argument( + "--frontend", + choices=sorted(FRONTENDS.keys()), + default="bevy", + help="frontend package target", + ) return parser.parse_args() def main() -> int: args = parse_args() + frontend = FRONTENDS[args.frontend] try: if args.command == "package": - layout = package() + layout = package(frontend) print(f"created: {layout.zip_path}") print(f"sha256: {layout.checksum_path}") else: - smoke() + smoke(frontend) return 0 except PackagingError as exc: print(f"error: {exc}", file=sys.stderr)