diff --git a/apps/desktop-shell/scripts/check-config.mjs b/apps/desktop-shell/scripts/check-config.mjs index 845913123..5caccb1f2 100644 --- a/apps/desktop-shell/scripts/check-config.mjs +++ b/apps/desktop-shell/scripts/check-config.mjs @@ -2357,6 +2357,9 @@ const requiredRustHostSnippets = [ '__genarrativeDesktopHistoryIndex', "console.warn('desktop navigation state sync failed', error)", 'file.imageDropped', + 'normalize_desktop_drop_position', + '.round().max(0.0) as i32', + 'desktop_drop_position_is_rounded_and_clamped_to_window_bounds', 'emit_desktop_image_drop_event(&drop_window, paths, drop_position)', 'app.notification().builder()', 'desktop_entry_url_with_host_context', diff --git a/apps/desktop-shell/src-tauri/src/shell/file_drop.rs b/apps/desktop-shell/src-tauri/src/shell/file_drop.rs index 992f601cc..7bfe31c79 100644 --- a/apps/desktop-shell/src-tauri/src/shell/file_drop.rs +++ b/apps/desktop-shell/src-tauri/src/shell/file_drop.rs @@ -15,6 +15,10 @@ fn first_valid_desktop_image_drop_payload( .find_map(|path| import_image_file_payload(path.clone(), "dropped", Some(position)).ok()) } +fn normalize_desktop_drop_position(x: f64, y: f64) -> (i32, i32) { + (x.round().max(0.0) as i32, y.round().max(0.0) as i32) +} + fn emit_desktop_image_drop_event( window: &WebviewWindow, paths: &[PathBuf], @@ -33,7 +37,7 @@ pub(crate) fn register_desktop_file_drop_events(window: &WebviewWindow) { let drop_window = window.clone(); window.on_window_event(move |event| { if let WindowEvent::DragDrop(DragDropEvent::Drop { paths, position }) = event { - let drop_position = (position.x.round() as i32, position.y.round() as i32); + let drop_position = normalize_desktop_drop_position(position.x, position.y); log_desktop_host_event_result( "file.imageDropped", emit_desktop_image_drop_event(&drop_window, paths, drop_position), @@ -103,4 +107,10 @@ mod tests { fs::remove_file(text_path).expect("remove text drop file"); fs::remove_dir(directory_path).expect("remove drop directory"); } + + #[test] + fn desktop_drop_position_is_rounded_and_clamped_to_window_bounds() { + assert_eq!(normalize_desktop_drop_position(12.4, 24.6), (12, 25)); + assert_eq!(normalize_desktop_drop_position(-4.2, -0.6), (0, 0)); + } } diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index c0dd38901..f9cbbfc73 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -16,6 +16,14 @@ --- +## 2026-06-20 桌面图片拖拽坐标非负边界 + +- 背景:Tauri 桌面壳通过系统拖拽事件向 H5 发送 `file.imageDropped`,拖拽坐标来自窗口事件;窗口边缘或平台差异可能产生负数或小数坐标,H5 只负责校验有限 number,不负责裁剪桌面系统坐标。 +- 决策:桌面壳在发送图片拖拽 HostBridge 事件前,必须把拖拽坐标 round 成整数并裁剪到非负值,再组装 import image payload;配置检查反查坐标归一 helper 和对应 Rust 单测。 +- 影响范围:`apps/desktop-shell/src-tauri/src/shell/file_drop.rs`、`apps/desktop-shell/scripts/check-config.mjs`。 +- 验证方式:`npm run desktop-shell:test -- file_drop`、`npm run desktop-shell:typecheck`、`npm run check:native-shells`、`npm run check:encoding`、`git diff --check`。 +- 关联文档:`docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md`、`docs/【前端架构】宿主壳能力统一协议-2026-06-17.md`。 + ## 2026-06-20 移动扫码权限异步取消边界 - 背景:Expo 移动壳 `scanner.scanQrCode` 会打开真实相机权限请求和扫码 overlay;如果用户在系统权限 Promise 返回前关闭扫码,旧权限结果不能重新激活 CameraView,也不能完成已经取消的 HostBridge 请求。