Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,049 changes: 761 additions & 288 deletions Cargo.lock

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions assets/icons/arrow.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions assets/icons/brush.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions assets/icons/check.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions assets/icons/close.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions assets/icons/rectangle.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions crates/api_cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ edition = "2021"
clap = { version = "4", features=["derive"] }
tracing = { workspace = true }
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tracing-appender = "0.2"
anyhow = { workspace = true }
tokio = { workspace = true }
screenshot_core = { path = "../core" }
Expand Down
98 changes: 90 additions & 8 deletions crates/api_cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ use services::StubClipboard;
use services::{gen_file_name, ExportService};
use std::path::PathBuf;
use std::sync::Arc;
use tracing_subscriber::{fmt, EnvFilter};
use tracing_appender::rolling::{RollingFileAppender, Rotation};
use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};

#[derive(Parser)]
#[command(
Expand Down Expand Up @@ -60,15 +61,96 @@ struct CaptureInteractiveArgs {
clipboard: bool,
}

/// 初始化日志系统 - 同时输出到控制台和文件
fn init_logging() {
use chrono::Local;

// 创建日志目录
let log_dir = std::env::temp_dir().join("screenshot_logs");
std::fs::create_dir_all(&log_dir).ok();

// 生成带时间戳的日志文件名
let timestamp = Local::now().format("%Y%m%d_%H%M%S");
let log_filename = format!("screenshot_{}.log", timestamp);

// 创建文件 appender(每天滚动)
let file_appender = RollingFileAppender::builder()
.rotation(Rotation::DAILY)
.filename_prefix("screenshot")
.filename_suffix("log")
.max_log_files(7) // 保留最近 7 天的日志
.build(&log_dir)
.expect("Failed to create log file appender");

// 获取环境变量中的日志级别,默认为 info
let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));

// 创建控制台输出层
let console_layer = fmt::layer().with_target(false).with_writer(std::io::stdout);

// 创建文件输出层(包含更详细的信息)
let file_layer = fmt::layer()
.with_target(true)
.with_line_number(true)
.with_ansi(false) // 文件不需要 ANSI 颜色
.with_writer(file_appender);

// 组合所有层
tracing_subscriber::registry()
.with(env_filter)
.with(console_layer)
.with(file_layer)
.init();

// 打印日志文件位置
let log_path = format!("{}/{}", log_dir.display(), log_filename);
println!("📝 日志文件: {}", log_path);
tracing::info!("========================================");
tracing::info!("Screenshot Tool - Session Start");
tracing::info!("Version: {}", env!("CARGO_PKG_VERSION"));
tracing::info!("Log file: {}", log_path);
tracing::info!("========================================");

// 设置 panic hook,将 panic 信息记录到日志
let log_path_clone = log_path.clone();
std::panic::set_hook(Box::new(move |panic_info| {
let payload = panic_info.payload();
let message = if let Some(s) = payload.downcast_ref::<&str>() {
s.to_string()
} else if let Some(s) = payload.downcast_ref::<String>() {
s.clone()
} else {
"Unknown panic payload".to_string()
};

let location = if let Some(location) = panic_info.location() {
format!(
"{}:{}:{}",
location.file(),
location.line(),
location.column()
)
} else {
"Unknown location".to_string()
};

tracing::error!("========================================");
tracing::error!("PANIC OCCURRED!");
tracing::error!("Message: {}", message);
tracing::error!("Location: {}", location);
tracing::error!("========================================");

eprintln!("\n❌ 程序发生严重错误 (panic)!");
eprintln!("📝 详细错误信息已保存到日志文件: {}", log_path_clone);
eprintln!("💡 请将日志文件提供给开发者以便排查问题。");
eprintln!("\nPanic: {} at {}", message, location);
}));
}

#[tokio::main]
async fn main() {
// 初始化日志
let _ = fmt()
.with_env_filter(
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")),
)
.with_target(false)
.try_init();
// 初始化日志 - 同时输出到控制台和文件
init_logging();

let cli = Cli::parse();
match cli.command {
Expand Down
59 changes: 46 additions & 13 deletions crates/platform_mac/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -416,9 +416,9 @@ impl MacCapturer {
let monitor_id = monitor.id().ok()?;
let display_info = virtual_desktop.displays.iter().find(|d| d.id == monitor_id)?;

// 捕获当前显示器
// 捕获当前显示器(注意:xcap 的 capture_image() 返回物理像素图像)
let img = monitor.capture_image().ok()?;
let (mon_width, mon_height) = (img.width(), img.height());
let (mon_width_physical, mon_height_physical) = (img.width(), img.height());
let rgba_data = img.into_raw();

// 计算在虚拟桌面画布中的位置(物理坐标)
Expand All @@ -430,32 +430,35 @@ impl MacCapturer {

#[cfg(debug_assertions)]
tracing::debug!(
"显示器合成 [物理坐标]: 显示器{} 逻辑({},{}) -> 物理({},{}) -> canvas({},{}) 尺寸{}x{}",
"显示器合成: 显示器{} 逻辑({},{}) scale={} -> 物理({},{}) -> canvas({},{}) 图像尺寸{}x{} (物理)",
monitor_id,
display_info.x,
display_info.y,
scale,
phys_x,
phys_y,
canvas_x,
canvas_y,
mon_width,
mon_height
mon_width_physical,
mon_height_physical
);

Some((rgba_data, mon_width, mon_height, canvas_x, canvas_y))
Some((rgba_data, mon_width_physical, mon_height_physical, canvas_x, canvas_y))
})
.collect();

// 串行合成到画布(避免数据竞争)
for (rgba_data, mon_width, mon_height, canvas_x, canvas_y) in captured_monitors {
for row in 0..mon_height {
// 串行合成到画布(图像已经是物理像素,直接复制)
for (rgba_data, mon_width_physical, mon_height_physical, canvas_x, canvas_y) in
captured_monitors
{
for row in 0..mon_height_physical {
if canvas_y + row >= canvas_height {
break;
}
let src_row_start = (row * mon_width * 4) as usize;
let src_row_end = src_row_start + (mon_width * 4) as usize;
let src_row_start = (row * mon_width_physical * 4) as usize;
let src_row_end = src_row_start + (mon_width_physical * 4) as usize;
let dst_row_start = (((canvas_y + row) * canvas_width + canvas_x) * 4) as usize;
let dst_row_end = dst_row_start + (mon_width * 4) as usize;
let dst_row_end = dst_row_start + (mon_width_physical * 4) as usize;

if dst_row_end <= canvas.len() && src_row_end <= rgba_data.len() {
canvas[dst_row_start..dst_row_end]
Expand Down Expand Up @@ -639,6 +642,15 @@ impl MacCapturer {
}

// 构建虚拟桌面信息用于区域选择(物理坐标)
#[cfg(debug_assertions)]
tracing::info!(
"[platform_mac] 传递给 ui_overlay 的 virtual_bounds (物理坐标): min=({}, {}), size={}x{}",
physical_min_x,
physical_min_y,
physical_width,
physical_height
);

let virtual_bounds = (
physical_min_x,
physical_min_y,
Expand Down Expand Up @@ -673,18 +685,39 @@ impl MacCapturer {
}
};

// 坐标已经是物理虚拟桌面坐标,直接使用
// 坐标已经是逻辑坐标,需要乘以 scale 转换为物理坐标
let scale = if rect.scale.is_finite() && rect.scale > 0.0 {
rect.scale
} else {
1.0
} as f32;

tracing::info!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
tracing::info!("[platform_mac] 收到的 Region (逻辑坐标):");
tracing::info!(
" └─ x={}, y={}, w={}, h={}",
rect.x,
rect.y,
rect.w,
rect.h
);
tracing::info!(" └─ scale={}", rect.scale);

let x_virtual = (rect.x * scale).floor() as i32; // 不使用 max(0.0),保留负坐标
let y_virtual = (rect.y * scale).floor() as i32; // 不使用 max(0.0),保留负坐标
let w = (rect.w * scale).round().max(0.0) as u32;
let h = (rect.h * scale).round().max(0.0) as u32;

tracing::info!("[platform_mac] 转换为物理坐标:");
tracing::info!(
" └─ x_virtual={}, y_virtual={}, w={}, h={}",
x_virtual,
y_virtual,
w,
h
);
tracing::info!(" └─ 计算公式: 逻辑坐标 * scale = 物理坐标");

// 在虚拟桌面坐标系中进行裁剪
// 注意:现在选择区域和画布都是物理坐标,所以使用物理边界
let canvas_x = (x_virtual - physical_min_x).max(0) as u32;
Expand Down
20 changes: 9 additions & 11 deletions crates/ui_overlay/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,21 @@ tokio = { workspace = true }
rayon = { workspace = true }
winit = "0.30"
image = { workspace = true }
softbuffer = "0.4.6"
raw-window-handle = "0.6"

# 默认使用工作区的 skia-safe 配置(GL + textlayout + svg)
skia-safe = { workspace = true }
# egui UI 框架(用于 UI 渲染和标注工具栏)
egui = "0.33"
egui-winit = "0.33"
egui-wgpu = "0.33"
# 不显式指定 wgpu 版本,使用 egui-wgpu 依赖的版本
pollster = "0.4"
# 用于背景渲染
bytemuck = { version = "1.14", features = ["derive"] }

# macOS 依赖:用于窗口层级/行为控制和 Metal GPU 渲染
# macOS 依赖:用于窗口层级/行为控制
[target.'cfg(target_os = "macos")'.dependencies]
# 在 macOS 上启用 Metal 特性
skia-safe = { version = "0.88", features = ["gl", "metal", "textlayout", "svg"] }

# Objective-C 运行时交互(平台集成)
objc2 = "0.6.2"
objc2-app-kit = "0.3.1"
objc2-quartz-core = "0.3.1"

# Metal GPU 渲染
metal = "0.29"
core-graphics-types = "0.1"

Loading