截图模块优化调整
This commit is contained in:
@@ -134,6 +134,10 @@ pub async fn clipboard_save_settings(
|
||||
// 快捷键变化时重新注册
|
||||
if settings.shortcut != prev_shortcut {
|
||||
super::popup::register_shortcut(&app, &settings.shortcut)?;
|
||||
// 新快捷键非空时确保弹窗窗口已预创建
|
||||
if !settings.shortcut.trim().is_empty() {
|
||||
super::popup::ensure_popup_window(&app);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
//! 6. 窗口失焦自动隐藏
|
||||
|
||||
use std::sync::Mutex;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use tauri::{AppHandle, Manager, WebviewUrl, WebviewWindowBuilder, Emitter};
|
||||
use tauri::window::{Effect, EffectsBuilder};
|
||||
use tauri_plugin_global_shortcut::{GlobalShortcutExt, Shortcut, ShortcutState};
|
||||
@@ -19,6 +20,10 @@ pub const POPUP_LABEL: &str = "clipboard-popup";
|
||||
/// 当前注册的快捷键(用于注销旧快捷键)
|
||||
static CURRENT_SHORTCUT: Mutex<Option<String>> = Mutex::new(None);
|
||||
|
||||
/// 标志:show_popup 兜底创建路径设为 true,前端 onMounted 回调 show_window 时据此判断是否显示。
|
||||
/// 预创建路径不设置,避免应用启动时弹窗自动弹出。
|
||||
static POPUP_PENDING_SHOW: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
/// 解析快捷键字符串为 Shortcut(格式如 "Alt+V"、"Ctrl+Shift+V")
|
||||
/// 失败返回 None。
|
||||
pub fn parse_shortcut(s: &str) -> Option<Shortcut> {
|
||||
@@ -69,6 +74,58 @@ pub fn unregister_shortcut(app: &AppHandle) {
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建弹窗窗口(隐藏状态)并注册失焦监听。
|
||||
/// 位置默认在屏幕外,show_popup 时会重新定位。
|
||||
/// 预创建后首次按快捷键走"窗口已存在"分支直接 show,避免首次创建的时序问题。
|
||||
fn create_popup_window(app: &AppHandle) {
|
||||
let win = match WebviewWindowBuilder::new(
|
||||
app,
|
||||
POPUP_LABEL,
|
||||
WebviewUrl::App("index.html#clipboard-popup".into()),
|
||||
)
|
||||
.title("剪贴板")
|
||||
.inner_size(380.0, 460.0)
|
||||
.position(-10000.0, -10000.0) // 屏幕外,避免隐藏时一闪
|
||||
.decorations(false)
|
||||
.transparent(true)
|
||||
.shadow(true)
|
||||
.always_on_top(true)
|
||||
.skip_taskbar(true)
|
||||
.resizable(false)
|
||||
.visible(false)
|
||||
.focused(false) // 不抢占焦点,避免创建即触发 Focused(false)
|
||||
.effects(EffectsBuilder::new().effects(vec![Effect::Mica]).build())
|
||||
.build()
|
||||
{
|
||||
Ok(w) => w,
|
||||
Err(e) => {
|
||||
eprintln!("[clipboard] 创建弹窗失败: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// 监听窗口失焦:自动隐藏
|
||||
let app_handle = app.clone();
|
||||
let win_handle = win.clone();
|
||||
win.on_window_event(move |event| {
|
||||
if let tauri::WindowEvent::Focused(false) = event {
|
||||
let _ = win_handle.hide();
|
||||
let _ = app_handle.emit("clipboard-popup-hide", ());
|
||||
}
|
||||
});
|
||||
|
||||
eprintln!("[clipboard] 弹窗窗口已预创建(隐藏状态)");
|
||||
}
|
||||
|
||||
/// 应用启动时预创建弹窗窗口(隐藏)。
|
||||
/// 这样首次按快捷键时窗口已存在,直接 show + 定位,避免首次创建时序问题。
|
||||
pub fn ensure_popup_window(app: &AppHandle) {
|
||||
if app.get_webview_window(POPUP_LABEL).is_some() {
|
||||
return;
|
||||
}
|
||||
create_popup_window(app);
|
||||
}
|
||||
|
||||
/// 在鼠标当前位置显示弹窗。
|
||||
/// 窗口不存在则创建(隐藏状态,等前端挂载后调用 show_window 显示)。
|
||||
/// 窗口已存在则移动到鼠标位置并显示。
|
||||
@@ -115,49 +172,18 @@ pub fn show_popup(app: &AppHandle) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 首次创建窗口:先隐藏,等前端挂载完成后再显示
|
||||
// 这样确保主题效果(dark class、mica)在窗口可见前已应用
|
||||
let win = match WebviewWindowBuilder::new(
|
||||
app,
|
||||
POPUP_LABEL,
|
||||
WebviewUrl::App("index.html#clipboard-popup".into()),
|
||||
)
|
||||
.title("剪贴板")
|
||||
.inner_size(w, h)
|
||||
.position(x, y)
|
||||
.decorations(false)
|
||||
.transparent(true)
|
||||
.shadow(true)
|
||||
.always_on_top(true)
|
||||
.skip_taskbar(true)
|
||||
.resizable(false)
|
||||
.visible(false) // 关键:先隐藏,前端 onMounted 后调用 show_window
|
||||
.focused(true)
|
||||
.effects(EffectsBuilder::new().effects(vec![Effect::Mica]).build())
|
||||
.build()
|
||||
{
|
||||
Ok(w) => w,
|
||||
Err(e) => {
|
||||
eprintln!("[clipboard] 创建弹窗失败: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// 监听窗口失焦:自动隐藏
|
||||
let app_handle = app.clone();
|
||||
let win_handle = win.clone();
|
||||
win.on_window_event(move |event| {
|
||||
if let tauri::WindowEvent::Focused(false) = event {
|
||||
let _ = win_handle.hide();
|
||||
let _ = app_handle.emit("clipboard-popup-hide", ());
|
||||
}
|
||||
});
|
||||
|
||||
eprintln!("[clipboard] 弹窗窗口已创建(隐藏状态)");
|
||||
// 兜底:窗口被销毁时重新创建(隐藏),等前端 onMounted 回调 show_window
|
||||
POPUP_PENDING_SHOW.store(true, Ordering::SeqCst);
|
||||
create_popup_window(app);
|
||||
}
|
||||
|
||||
/// 显示已创建的弹窗窗口(由前端 onMounted 后调用)。
|
||||
/// 预创建路径下前端 onMounted 也会调用此函数,但 POPUP_PENDING_SHOW 为 false 时直接跳过,
|
||||
/// 避免应用启动时弹窗自动弹出。仅 show_popup 兜底创建路径才真正显示。
|
||||
pub fn show_window(app: &AppHandle) {
|
||||
if !POPUP_PENDING_SHOW.swap(false, Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
if let Some(win) = app.get_webview_window(POPUP_LABEL) {
|
||||
let _ = win.show();
|
||||
let _ = win.set_focus();
|
||||
|
||||
@@ -47,11 +47,11 @@ use process_manager::{
|
||||
};
|
||||
use screenshot::commands::{
|
||||
screenshot_capture_fullscreen, screenshot_capture_window, screenshot_clear_fullscreen,
|
||||
screenshot_copy_image, screenshot_crop_copy_stored, screenshot_crop_stored,
|
||||
screenshot_cursor_pos, screenshot_disable_transitions, screenshot_enum_windows,
|
||||
screenshot_fullscreen_png, screenshot_get_editor_image, screenshot_get_fullscreen_bmp,
|
||||
screenshot_register_shortcut, screenshot_save_png, screenshot_set_editor_image,
|
||||
screenshot_unregister_shortcut, screenshot_window_from_point,
|
||||
screenshot_compose_copy, screenshot_copy_image, screenshot_crop_copy_stored,
|
||||
screenshot_crop_stored, screenshot_cursor_pos, screenshot_disable_transitions,
|
||||
screenshot_enum_windows, screenshot_fullscreen_png, screenshot_get_editor_image,
|
||||
screenshot_get_fullscreen_bmp, screenshot_register_shortcut, screenshot_save_png,
|
||||
screenshot_set_editor_image, screenshot_unregister_shortcut, screenshot_window_from_point,
|
||||
};
|
||||
use clipboard::{
|
||||
ClipboardManager,
|
||||
@@ -215,7 +215,8 @@ pub fn run() {
|
||||
screenshot_save_png,
|
||||
screenshot_register_shortcut,
|
||||
screenshot_unregister_shortcut,
|
||||
screenshot_disable_transitions
|
||||
screenshot_disable_transitions,
|
||||
screenshot_compose_copy
|
||||
])
|
||||
.setup(|app| {
|
||||
// 初始化日志系统,日志目录: {app_data_dir}/logs/
|
||||
@@ -280,6 +281,8 @@ pub fn run() {
|
||||
if let Err(e) = clipboard::popup::register_shortcut(&app_handle, &shortcut) {
|
||||
eprintln!("[clipboard] 快捷键注册失败: {}", e);
|
||||
}
|
||||
// 预创建弹窗窗口(隐藏),首次按快捷键时直接 show,避免首次创建时序问题
|
||||
clipboard::popup::ensure_popup_window(&app_handle);
|
||||
}
|
||||
app.manage(clipboard);
|
||||
|
||||
|
||||
@@ -78,12 +78,6 @@ fn bgra_to_bmp(bgra: &[u8], width: i32, height: i32) -> Result<Vec<u8>, String>
|
||||
let data_size = w * h * 4;
|
||||
let file_size = 14 + 40 + data_size;
|
||||
|
||||
// BitBlt 取出的 alpha 通道未定义(常为 0),强制不透明
|
||||
let mut pixels = bgra.to_vec();
|
||||
for a in pixels.iter_mut().skip(3).step_by(4) {
|
||||
*a = 255;
|
||||
}
|
||||
|
||||
let mut bmp = Vec::with_capacity(file_size);
|
||||
// BITMAPFILEHEADER
|
||||
bmp.extend_from_slice(b"BM");
|
||||
@@ -103,7 +97,12 @@ fn bgra_to_bmp(bgra: &[u8], width: i32, height: i32) -> Result<Vec<u8>, String>
|
||||
bmp.extend_from_slice(&0u32.to_le_bytes()); // y ppm
|
||||
bmp.extend_from_slice(&0u32.to_le_bytes()); // colors used
|
||||
bmp.extend_from_slice(&0u32.to_le_bytes()); // important colors
|
||||
bmp.extend_from_slice(&pixels);
|
||||
// 像素数据:memcpy 一次性拷贝,再原地修正 alpha(避免 to_vec 中间分配)
|
||||
let pixel_start = bmp.len();
|
||||
bmp.extend_from_slice(bgra);
|
||||
for i in (pixel_start + 3..bmp.len()).step_by(4) {
|
||||
bmp[i] = 255;
|
||||
}
|
||||
Ok(bmp)
|
||||
}
|
||||
|
||||
@@ -117,16 +116,16 @@ fn bgra_to_png(bgra: &[u8], width: i32, height: i32) -> Result<Vec<u8>, String>
|
||||
if width <= 0 || height <= 0 {
|
||||
return Err("无效尺寸".into());
|
||||
}
|
||||
let w = width as usize;
|
||||
let h = height as usize;
|
||||
let mut rgba = vec![0u8; w * h * 4];
|
||||
for i in 0..(w * h) {
|
||||
rgba[i * 4] = bgra[i * 4 + 2]; // R
|
||||
rgba[i * 4 + 1] = bgra[i * 4 + 1]; // G
|
||||
rgba[i * 4 + 2] = bgra[i * 4]; // B
|
||||
rgba[i * 4 + 3] = 255; // 强制不透明
|
||||
let pixel_count = (width as usize) * (height as usize);
|
||||
let mut rgba = vec![0u8; pixel_count * 4];
|
||||
// chunks_exact 迭代器写法:4 字节对齐连续读取,编译器更容易自动向量化
|
||||
for (src, dst) in bgra.chunks_exact(4).zip(rgba.chunks_exact_mut(4)) {
|
||||
dst[0] = src[2]; // R
|
||||
dst[1] = src[1]; // G
|
||||
dst[2] = src[0]; // B
|
||||
dst[3] = 255; // A 强制不透明
|
||||
}
|
||||
let mut buf = Vec::with_capacity(w * h * 4 / 3);
|
||||
let mut buf = Vec::with_capacity(pixel_count * 4 / 3);
|
||||
PngEncoder::new_with_quality(
|
||||
&mut buf,
|
||||
CompressionType::Fast,
|
||||
@@ -493,7 +492,9 @@ fn bgra_to_dib(bgra: &[u8], width: i32, height: i32) -> Vec<u8> {
|
||||
let header_size = std::mem::size_of::<BITMAPINFOHEADER>() as u32;
|
||||
let w = width as usize;
|
||||
let h = height as usize;
|
||||
let mut dib = Vec::with_capacity(header_size as usize + w * h * 4);
|
||||
let row_size = w * 4;
|
||||
let pixel_size = row_size * h;
|
||||
let mut dib = Vec::with_capacity(header_size as usize + pixel_size);
|
||||
let header = BITMAPINFOHEADER {
|
||||
biSize: header_size,
|
||||
biWidth: width,
|
||||
@@ -501,7 +502,7 @@ fn bgra_to_dib(bgra: &[u8], width: i32, height: i32) -> Vec<u8> {
|
||||
biPlanes: 1,
|
||||
biBitCount: 32,
|
||||
biCompression: BI_RGB,
|
||||
biSizeImage: (w * h * 4) as u32,
|
||||
biSizeImage: (pixel_size) as u32,
|
||||
biXPelsPerMeter: 0,
|
||||
biYPelsPerMeter: 0,
|
||||
biClrUsed: 0,
|
||||
@@ -514,15 +515,17 @@ fn bgra_to_dib(bgra: &[u8], width: i32, height: i32) -> Vec<u8> {
|
||||
)
|
||||
};
|
||||
dib.extend_from_slice(raw);
|
||||
// 从 top-down 行序倒序拷贝为 bottom-up,alpha 强制 255
|
||||
for y in (0..h).rev() {
|
||||
let row = &bgra[y * w * 4..(y + 1) * w * 4];
|
||||
for x in 0..w {
|
||||
dib.push(row[x * 4]); // B
|
||||
dib.push(row[x * 4 + 1]); // G
|
||||
dib.push(row[x * 4 + 2]); // R
|
||||
dib.push(255); // A
|
||||
}
|
||||
// 预分配像素区,按行 memcpy 拷贝(top-down → bottom-up 行序翻转),再原地修正 alpha
|
||||
let pixel_start = dib.len();
|
||||
dib.resize(dib.len() + pixel_size, 0);
|
||||
for y in 0..h {
|
||||
let src = y * row_size;
|
||||
let dst = (h - 1 - y) * row_size;
|
||||
dib[pixel_start + dst..pixel_start + dst + row_size]
|
||||
.copy_from_slice(&bgra[src..src + row_size]);
|
||||
}
|
||||
for i in (pixel_start + 3..pixel_start + pixel_size).step_by(4) {
|
||||
dib[i] = 255;
|
||||
}
|
||||
dib
|
||||
}
|
||||
@@ -592,11 +595,17 @@ pub fn copy_png_to_clipboard(png_base64: &str) -> Result<(), String> {
|
||||
write_dib_to_clipboard(&dib)
|
||||
}
|
||||
|
||||
/// RGBA → CF_DIB(BITMAPINFOHEADER + BGRA bottom-up,alpha 强制 255)
|
||||
fn rgba_to_dib(rgba: &image::RgbaImage, width: i32, height: i32) -> Vec<u8> {
|
||||
/// raw RGBA 像素 → CF_DIB(直接从连续像素切片构造,无需 image crate 解码)
|
||||
///
|
||||
/// 用于有标注导出路径:前端 canvas.getImageData() 直传 raw RGBA,
|
||||
/// 省去 toDataURL(PNG 编码+base64) → Rust base64 解码 → PNG 解码 的三次往返。
|
||||
fn rgba_raw_to_dib(rgba: &[u8], width: i32, height: i32) -> Vec<u8> {
|
||||
let header_size = std::mem::size_of::<BITMAPINFOHEADER>() as u32;
|
||||
let row_size = (width as usize) * 4;
|
||||
let mut dib = Vec::with_capacity(header_size as usize + row_size * height as usize);
|
||||
let w = width as usize;
|
||||
let h = height as usize;
|
||||
let row_size = w * 4;
|
||||
let pixel_size = row_size * h;
|
||||
let mut dib = Vec::with_capacity(header_size as usize + pixel_size);
|
||||
let header = BITMAPINFOHEADER {
|
||||
biSize: header_size,
|
||||
biWidth: width,
|
||||
@@ -604,7 +613,7 @@ fn rgba_to_dib(rgba: &image::RgbaImage, width: i32, height: i32) -> Vec<u8> {
|
||||
biPlanes: 1,
|
||||
biBitCount: 32,
|
||||
biCompression: BI_RGB,
|
||||
biSizeImage: (row_size * height as usize) as u32,
|
||||
biSizeImage: (pixel_size) as u32,
|
||||
biXPelsPerMeter: 0,
|
||||
biYPelsPerMeter: 0,
|
||||
biClrUsed: 0,
|
||||
@@ -617,14 +626,105 @@ fn rgba_to_dib(rgba: &image::RgbaImage, width: i32, height: i32) -> Vec<u8> {
|
||||
)
|
||||
};
|
||||
dib.extend_from_slice(raw);
|
||||
// bottom-up 行序,BGRA 像素,alpha 强制 255
|
||||
for y in (0..height as usize).rev() {
|
||||
for x in 0..width as usize {
|
||||
let p = rgba.get_pixel(x as u32, y as u32).0;
|
||||
dib.push(p[2]); // B
|
||||
dib.push(p[1]); // G
|
||||
dib.push(p[0]); // R
|
||||
dib.push(255); // A
|
||||
let pixel_start = dib.len();
|
||||
dib.resize(dib.len() + pixel_size, 0);
|
||||
// RGBA top-down → BGRA bottom-up,alpha 强制 255
|
||||
for y in 0..h {
|
||||
let src_off = (h - 1 - y) * row_size;
|
||||
let dst_off = y * row_size;
|
||||
for x in 0..w {
|
||||
let si = src_off + x * 4;
|
||||
let di = pixel_start + dst_off + x * 4;
|
||||
dib[di] = rgba[si + 2]; // B
|
||||
dib[di + 1] = rgba[si + 1]; // G
|
||||
dib[di + 2] = rgba[si]; // R
|
||||
dib[di + 3] = 255; // A
|
||||
}
|
||||
}
|
||||
dib
|
||||
}
|
||||
|
||||
/// raw RGBA → PNG(直接从连续像素切片编码,跳过 image crate 的 RgbaImage 包装)
|
||||
fn rgba_raw_to_png(rgba: &[u8], width: i32, height: i32) -> Result<Vec<u8>, String> {
|
||||
use image::codecs::png::{CompressionType, FilterType, PngEncoder};
|
||||
use image::ImageEncoder;
|
||||
if width <= 0 || height <= 0 {
|
||||
return Err("无效尺寸".into());
|
||||
}
|
||||
let mut buf = Vec::with_capacity(rgba.len() / 3);
|
||||
PngEncoder::new_with_quality(
|
||||
&mut buf,
|
||||
CompressionType::Fast,
|
||||
FilterType::NoFilter,
|
||||
)
|
||||
.write_image(rgba, width as u32, height as u32, image::ExtendedColorType::Rgba8)
|
||||
.map_err(|e| format!("PNG 编码失败: {}", e))?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
/// 有标注导出:raw RGBA → 剪贴板 DIB + PNG base64(一次调用完成复制+编码)
|
||||
///
|
||||
/// 替代原 composeBase64 + screenshot_copy_image 两步:
|
||||
/// 省去前端 toDataURL(PNG 编码+base64) + Rust base64 解码 + PNG 解码,
|
||||
/// 直接 raw RGBA 传输 → DIB 写剪贴板 + PNG 编码返回。
|
||||
pub fn compose_copy_rgba(rgba: &[u8], width: i32, height: i32) -> Result<CaptureData, String> {
|
||||
let expected = (width as usize) * (height as usize) * 4;
|
||||
if rgba.len() < expected {
|
||||
return Err(format!("像素数据不足: {} < {}", rgba.len(), expected));
|
||||
}
|
||||
let dib = rgba_raw_to_dib(rgba, width, height);
|
||||
write_dib_to_clipboard(&dib)?;
|
||||
let png = rgba_raw_to_png(rgba, width, height)?;
|
||||
Ok(CaptureData {
|
||||
png_base64: base64_encode(&png),
|
||||
width,
|
||||
height,
|
||||
})
|
||||
}
|
||||
|
||||
/// RGBA → CF_DIB(BITMAPINFOHEADER + BGRA bottom-up,alpha 强制 255)
|
||||
fn rgba_to_dib(rgba: &image::RgbaImage, width: i32, height: i32) -> Vec<u8> {
|
||||
let header_size = std::mem::size_of::<BITMAPINFOHEADER>() as u32;
|
||||
let w = width as usize;
|
||||
let h = height as usize;
|
||||
let row_size = w * 4;
|
||||
let pixel_size = row_size * h;
|
||||
let mut dib = Vec::with_capacity(header_size as usize + pixel_size);
|
||||
let header = BITMAPINFOHEADER {
|
||||
biSize: header_size,
|
||||
biWidth: width,
|
||||
biHeight: height, // 正值 = bottom-up
|
||||
biPlanes: 1,
|
||||
biBitCount: 32,
|
||||
biCompression: BI_RGB,
|
||||
biSizeImage: (pixel_size) as u32,
|
||||
biXPelsPerMeter: 0,
|
||||
biYPelsPerMeter: 0,
|
||||
biClrUsed: 0,
|
||||
biClrImportant: 0,
|
||||
};
|
||||
let raw = unsafe {
|
||||
std::slice::from_raw_parts(
|
||||
&header as *const _ as *const u8,
|
||||
std::mem::size_of::<BITMAPINFOHEADER>(),
|
||||
)
|
||||
};
|
||||
dib.extend_from_slice(raw);
|
||||
// 预分配像素区,用 as_raw() 连续切片替代逐像素 get_pixel
|
||||
let pixel_start = dib.len();
|
||||
dib.resize(dib.len() + pixel_size, 0);
|
||||
let src = rgba.as_raw(); // &[u8] RGBA top-down
|
||||
for y in 0..h {
|
||||
// bottom-up: DIB 第一行 = 图像最后一行
|
||||
let src_off = (h - 1 - y) * row_size;
|
||||
let dst_off = y * row_size;
|
||||
for x in 0..w {
|
||||
let si = src_off + x * 4;
|
||||
let di = pixel_start + dst_off + x * 4;
|
||||
dib[di] = src[si + 2]; // B
|
||||
dib[di + 1] = src[si + 1]; // G
|
||||
dib[di + 2] = src[si]; // R
|
||||
dib[di + 3] = 255; // A
|
||||
}
|
||||
}
|
||||
dib
|
||||
|
||||
@@ -279,6 +279,39 @@ pub async fn screenshot_copy_image(png_base64: String) -> Result<(), String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 有标注导出:接收 raw RGBA(前端 canvas.getImageData 直传),一次完成 剪贴板+PNG base64。
|
||||
///
|
||||
/// 省去前端 toDataURL(PNG 编码+base64) → Rust base64 解码 → PNG 解码 三次往返。
|
||||
/// body 格式:前 8 字节 = width(i32 LE) + height(i32 LE),之后为 raw RGBA 像素。
|
||||
#[tauri::command]
|
||||
pub async fn screenshot_compose_copy(
|
||||
request: tauri::ipc::Request<'_>,
|
||||
) -> Result<super::CaptureData, String> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let body = match request.body() {
|
||||
tauri::ipc::InvokeBody::Raw(data) => data.clone(),
|
||||
_ => return Err("需要 raw body(ArrayBuffer)".into()),
|
||||
};
|
||||
if body.len() < 8 {
|
||||
return Err("数据不足:缺少尺寸头".into());
|
||||
}
|
||||
let width = i32::from_le_bytes([body[0], body[1], body[2], body[3]]);
|
||||
let height = i32::from_le_bytes([body[4], body[5], body[6], body[7]]);
|
||||
let rgba = body[8..].to_vec();
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
super::capture::compose_copy_rgba(&rgba, width, height)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| format!("合成复制任务失败: {}", e))?
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
let _ = request;
|
||||
Err("截图仅支持 Windows".into())
|
||||
}
|
||||
}
|
||||
|
||||
/// 将 PNG base64 写入文件
|
||||
#[tauri::command]
|
||||
pub async fn screenshot_save_png(png_base64: String, path: String) -> Result<(), String> {
|
||||
|
||||
@@ -394,11 +394,6 @@ pub async fn tray_menu_action(
|
||||
let _ = app.emit("tray:new-download", ());
|
||||
hide_tray_menu(&app);
|
||||
}
|
||||
"screenshot_region" => {
|
||||
hide_tray_menu(&app);
|
||||
// 主窗口(隐藏运行中)接收事件后调用 screenshotStore.startCapture('region')
|
||||
let _ = app.emit("tray:start-screenshot", "region");
|
||||
}
|
||||
"settings" => {
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
window.show().ok();
|
||||
|
||||
Reference in New Issue
Block a user