diff --git a/src/capture.rs b/src/capture.rs index 5c723d4..1c6d351 100644 --- a/src/capture.rs +++ b/src/capture.rs @@ -11,6 +11,23 @@ pub struct Region { pub height: u32, } +/// Captures the specified region in physical pixels. +pub fn capture_region(region: Region) -> Result { + let full = capture_all_outputs()?; + + let px = (region.x.max(0) as u32).min(full.width().saturating_sub(1)); + let py = (region.y.max(0) as u32).min(full.height().saturating_sub(1)); + let pw = region.width.min(full.width() - px); + let ph = region.height.min(full.height() - py); + + eprintln!( + "[capture] crop ({px},{py}) {pw}x{ph} from {}x{}", + full.width(), full.height(), + ); + + Ok(image::imageops::crop_imm(&full, px, py, pw, ph).to_image()) +} + /// Captures all connected outputs stitched together into one image. pub fn capture_all_outputs() -> Result { let conn = WayshotConnection::new() @@ -20,15 +37,14 @@ pub fn capture_all_outputs() -> Result { eprintln!("[capture] outputs: {:?}", all.iter().map(|o| &o.name).collect::>()); let active_name = crate::hyprland::active_monitor_name(); - let outputs: Vec<_> = if let Some(ref name) = active_name { - let filtered: Vec<_> = all.iter().filter(|o| &o.name == name).cloned().collect(); - if filtered.is_empty() { all.to_vec() } else { filtered } + let target_output = if let Some(ref name) = active_name { + all.iter().find(|o| &o.name == name).unwrap_or(&all[0]) } else { - all.iter().take(1).cloned().collect() + &all[0] }; let rgba = conn - .screenshot_outputs(&outputs, false) + .screenshot_single_output(target_output, false) .context("libwayshot failed to capture output")? .into_rgba8(); diff --git a/src/config.rs b/src/config.rs index ad090dd..b57dfeb 100644 --- a/src/config.rs +++ b/src/config.rs @@ -29,10 +29,15 @@ pub struct Config { /// Milliseconds to wait after launch before capturing the desktop snapshot. /// Increase this if the overlay background still shows the terminal/launcher - /// that started rs-pictures. Default: 800. + /// that started rs-pictures. Default: 200. #[serde(default = "default_capture_delay_ms")] pub capture_delay_ms: u64, + /// If true, the selection overlay will be transparent (live preview) instead of + /// a frozen screenshot. The final capture happens after selection. + #[serde(default)] + pub live_mode: bool, + /// Visual effects applied after capture. pub effects: EffectsConfig, } @@ -85,6 +90,7 @@ impl Default for Config { auto_save: false, auto_copy: false, capture_delay_ms: default_capture_delay_ms(), + live_mode: false, effects: EffectsConfig::default(), } } @@ -145,7 +151,7 @@ impl Config { } } -fn default_capture_delay_ms() -> u64 { 800 } +fn default_capture_delay_ms() -> u64 { 200 } fn dirs_default_pictures() -> Option { // Use XDG_PICTURES_DIR if available, otherwise ~/Pictures diff --git a/src/main.rs b/src/main.rs index 19e1dfd..4bbeeb3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,12 +25,14 @@ use overlay::{SelectionOverlay, SelectionResult}; use review::ReviewWindow; fn main() -> Result<()> { - // ── 1. Load config ──────────────────────────────────────────────────────── + // ── 1. Load config & arguments ──────────────────────────────────────────── let config = config::Config::load().context("Failed to load config")?; + let is_live_mode = config.live_mode || std::env::args().any(|a| a == "--live" || a == "-l"); // ── 2. Query Hyprland metadata (no window open yet) ─────────────────────── // Scale is only needed for window-rect conversion in the overlay. let scale = hyprland::active_monitor_scale(); + let (lw, lh) = hyprland::active_monitor_logical_size().unwrap_or((1920, 1080)); // ── 3. Pre-capture delay ────────────────────────────────────────────────── // Give the compositor time to unmap the terminal/launcher that started us @@ -39,11 +41,13 @@ fn main() -> Result<()> { std::thread::sleep(std::time::Duration::from_millis(config.capture_delay_ms)); // ── 4. Capture full desktop BEFORE opening any window ──────────────────── - // This must happen before eframe::run_native is called. eframe maps its - // window immediately on entry — before our first update() frame — causing - // a white rectangle to appear in the snapshot if we capture any later. - let background_snapshot = - capture::capture_all_outputs().context("Failed to capture desktop snapshot")?; + // In freeze mode (default), we snapshot before the overlay. + // In live mode, we skip this and capture later. + let background_snapshot = if is_live_mode { + None + } else { + Some(capture::capture_all_outputs().context("Failed to capture desktop snapshot")?) + }; // ── 5. Run the selection overlay ───────────────────────────────────────── let selection_result = { @@ -53,11 +57,14 @@ fn main() -> Result<()> { let native_options = eframe::NativeOptions { viewport: egui::ViewportBuilder::default() + .with_app_id("rs-pictures-overlay") + .with_inner_size([lw as f32, lh as f32]) + .with_position([0.0, 0.0]) .with_fullscreen(true) + .with_maximized(true) .with_decorations(false) .with_transparent(true) .with_always_on_top() - .with_active(false) .with_resizable(false), ..Default::default() }; @@ -91,13 +98,18 @@ fn main() -> Result<()> { } }; - // ── 7. Crop the selected region from the original snapshot ──────────────── - let raw_image = { - let px = (region.x.max(0) as u32).min(background_snapshot.width().saturating_sub(1)); - let py = (region.y.max(0) as u32).min(background_snapshot.height().saturating_sub(1)); - let pw = region.width.min(background_snapshot.width() - px); - let ph = region.height.min(background_snapshot.height() - py); - image::imageops::crop_imm(&background_snapshot, px, py, pw, ph).to_image() + // ── 7. Get the raw region image ─────────────────────────────────────────── + let raw_image = if let Some(bg) = background_snapshot { + // Freeze mode: Crop directly from the pre-captured snapshot. + let px = (region.x.max(0) as u32).min(bg.width().saturating_sub(1)); + let py = (region.y.max(0) as u32).min(bg.height().saturating_sub(1)); + let pw = region.width.min(bg.width() - px); + let ph = region.height.min(bg.height() - py); + image::imageops::crop_imm(&bg, px, py, pw, ph).to_image() + } else { + // Live mode: Wait for the compositor to clear the overlay, then capture just the region. + std::thread::sleep(std::time::Duration::from_millis(config.capture_delay_ms.max(200))); + capture::capture_region(region).context("Failed to capture region")? }; // ── 8a. Auto-mode — no review window ───────────────────────────────────── diff --git a/src/overlay.rs b/src/overlay.rs index 5ab3522..e6b2a14 100644 --- a/src/overlay.rs +++ b/src/overlay.rs @@ -36,10 +36,10 @@ enum SelectionState { Cancelled, } -const MAX_TEX: u32 = 2048; +const MAX_TEX: u32 = 8192; pub struct SelectionOverlay { - background: egui::TextureHandle, + background: Option, /// Hyprland monitor scale (logical→physical). Used only for window rect conversion. scale: f32, state: SelectionState, @@ -50,16 +50,17 @@ pub struct SelectionOverlay { } impl SelectionOverlay { - pub fn new(cc: &eframe::CreationContext<'_>, background_snapshot: RgbaImage, scale: f32) -> Self { - let tex_image = fit_to_max_texture(background_snapshot); - let (tw, th) = tex_image.dimensions(); - let color_image = egui::ColorImage::from_rgba_unmultiplied( - [tw as usize, th as usize], - tex_image.as_raw(), - ); - let texture = + pub fn new(cc: &eframe::CreationContext<'_>, background_snapshot: Option, scale: f32) -> Self { + let texture = background_snapshot.map(|img| { + let tex_image = fit_to_max_texture(img); + let (tw, th) = tex_image.dimensions(); + let color_image = egui::ColorImage::from_rgba_unmultiplied( + [tw as usize, th as usize], + tex_image.as_raw(), + ); cc.egui_ctx - .load_texture("background", color_image, egui::TextureOptions::LINEAR); + .load_texture("background", color_image, egui::TextureOptions::LINEAR) + }); let windows = crate::hyprland::active_workspace_windows(); @@ -201,13 +202,15 @@ impl eframe::App for SelectionOverlay { .show(ctx, |ui| { let painter = ui.painter(); - // 1. Background screenshot. - painter.image( - self.background.id(), - screen_rect, - Rect::from_min_max(Pos2::ZERO, Pos2::new(1.0, 1.0)), - Color32::WHITE, - ); + // 1. Background screenshot (if in freeze mode). + if let Some(bg) = &self.background { + painter.image( + bg.id(), + screen_rect, + Rect::from_min_max(Pos2::ZERO, Pos2::new(1.0, 1.0)), + Color32::WHITE, + ); + } // 2. Dim overlay. let active_rect = current_rect.or_else(|| { @@ -240,12 +243,10 @@ impl eframe::App for SelectionOverlay { Rounding::ZERO, dim, ); - let border_color = if self.hovered_window.is_some() && current_rect.is_none() { - Color32::from_rgb(255, 190, 50) - } else { - Color32::from_rgb(100, 180, 255) - }; - painter.rect_stroke(s, Rounding::ZERO, Stroke::new(1.5, border_color)); + // Only draw a stroke if we are actively dragging a selection. + if current_rect.is_some() { + painter.rect_stroke(s, Rounding::ZERO, Stroke::new(1.5, Color32::from_rgb(100, 180, 255))); + } // Size label in physical pixels. let label = format!("{} × {}", s.width().round() as u32, s.height().round() as u32); diff --git a/src/review.rs b/src/review.rs index f8ec881..d67e68b 100644 --- a/src/review.rs +++ b/src/review.rs @@ -20,7 +20,7 @@ use image::RgbaImage; use crate::{config::Config, effects::apply_effects}; -const MAX_TEX: u32 = 2048; +const MAX_TEX: u32 = 8192; /// How long to wait after the last setting change before spawning the worker. const DEBOUNCE: Duration = Duration::from_millis(150); diff --git a/test b/test new file mode 100755 index 0000000..a659000 Binary files /dev/null and b/test differ diff --git a/test.rs b/test.rs new file mode 100644 index 0000000..1d2da3e --- /dev/null +++ b/test.rs @@ -0,0 +1 @@ +fn main() { println!("test"); }