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