mirror of
https://github.com/harivansh-afk/deskctl.git
synced 2026-04-18 10:03:31 +00:00
Phase 3: screenshot annotation with bounding boxes and @wN labels
- Add imageproc and ab_glyph dependencies - Annotation module drawing colored bounding boxes per window - White @wN labels on dark background at each window's top-left - 8-color palette cycling for distinct window identification - Back-to-front iteration so topmost labels are not occluded - Embedded DejaVu Sans Mono font via include_bytes - Wire --annotate flag into snapshot pipeline
This commit is contained in:
parent
79e6e0e25c
commit
0072a260b8
6 changed files with 1903 additions and 11 deletions
72
src/backend/annotate.rs
Normal file
72
src/backend/annotate.rs
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
use ab_glyph::{FontArc, PxScale};
|
||||
use image::{Rgba, RgbaImage};
|
||||
use imageproc::drawing::{draw_filled_rect_mut, draw_hollow_rect_mut, draw_text_mut, text_size};
|
||||
use imageproc::rect::Rect;
|
||||
|
||||
use crate::core::types::WindowInfo;
|
||||
|
||||
// Embedded font - DejaVu Sans Mono for guaranteed availability
|
||||
const FONT_BYTES: &[u8] = include_bytes!("../../assets/DejaVuSansMono.ttf");
|
||||
|
||||
const COLORS: &[Rgba<u8>] = &[
|
||||
Rgba([0, 255, 0, 255]), // green
|
||||
Rgba([255, 100, 0, 255]), // orange
|
||||
Rgba([0, 150, 255, 255]), // blue
|
||||
Rgba([255, 0, 255, 255]), // magenta
|
||||
Rgba([255, 255, 0, 255]), // yellow
|
||||
Rgba([0, 255, 255, 255]), // cyan
|
||||
Rgba([255, 50, 50, 255]), // red
|
||||
Rgba([150, 100, 255, 255]), // purple
|
||||
];
|
||||
|
||||
const LABEL_BG: Rgba<u8> = Rgba([0, 0, 0, 200]);
|
||||
const LABEL_FG: Rgba<u8> = Rgba([255, 255, 255, 255]);
|
||||
|
||||
pub fn annotate_screenshot(image: &mut RgbaImage, windows: &[WindowInfo]) {
|
||||
let font = match FontArc::try_from_slice(FONT_BYTES) {
|
||||
Ok(f) => f,
|
||||
Err(_) => return, // Silently skip annotation if font fails to load
|
||||
};
|
||||
let scale = PxScale { x: 18.0, y: 18.0 };
|
||||
|
||||
// Iterate back-to-front so topmost window labels are drawn last (not occluded)
|
||||
for (i, win) in windows.iter().enumerate().rev() {
|
||||
if win.minimized {
|
||||
continue; // Don't annotate hidden windows
|
||||
}
|
||||
|
||||
let color = COLORS[i % COLORS.len()];
|
||||
let x = win.x.max(0);
|
||||
let y = win.y.max(0);
|
||||
let w = win.width;
|
||||
let h = win.height;
|
||||
|
||||
// Draw bounding box (2px thick by drawing at offset 0 and 1)
|
||||
if w > 0 && h > 0 {
|
||||
draw_hollow_rect_mut(image, Rect::at(x, y).of_size(w, h), color);
|
||||
if w > 2 && h > 2 {
|
||||
draw_hollow_rect_mut(
|
||||
image,
|
||||
Rect::at(x + 1, y + 1).of_size(w - 2, h - 2),
|
||||
color,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw label
|
||||
let label = format!("@{}", win.ref_id);
|
||||
let (tw, th) = text_size(scale, &font, &label);
|
||||
let label_x = x;
|
||||
let label_y = (y - th as i32 - 4).max(0);
|
||||
|
||||
// Label background
|
||||
draw_filled_rect_mut(
|
||||
image,
|
||||
Rect::at(label_x, label_y).of_size(tw + 6, th + 4),
|
||||
LABEL_BG,
|
||||
);
|
||||
|
||||
// Label text
|
||||
draw_text_mut(image, LABEL_FG, label_x + 3, label_y + 2, scale, &font, &label);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
pub mod annotate;
|
||||
pub mod x11;
|
||||
|
||||
use anyhow::Result;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
use anyhow::{Context, Result};
|
||||
|
||||
use crate::core::types::{Snapshot, WindowInfo};
|
||||
use super::annotate::annotate_screenshot;
|
||||
|
||||
pub struct X11Backend {
|
||||
// enigo and x11rb connections added in later phases
|
||||
|
|
@ -13,7 +14,7 @@ impl X11Backend {
|
|||
}
|
||||
|
||||
impl super::DesktopBackend for X11Backend {
|
||||
fn snapshot(&mut self, _annotate: bool) -> Result<Snapshot> {
|
||||
fn snapshot(&mut self, annotate: bool) -> Result<Snapshot> {
|
||||
// Get z-ordered window list via xcap (topmost first internally)
|
||||
let windows = xcap::Window::all()
|
||||
.context("Failed to enumerate windows")?;
|
||||
|
|
@ -24,18 +25,9 @@ impl super::DesktopBackend for X11Backend {
|
|||
let monitor = monitors.into_iter().next()
|
||||
.context("No monitor found")?;
|
||||
|
||||
let image = monitor.capture_image()
|
||||
let mut image = monitor.capture_image()
|
||||
.context("Failed to capture screenshot")?;
|
||||
|
||||
// Save screenshot
|
||||
let timestamp = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis();
|
||||
let screenshot_path = format!("/tmp/desktop-ctl-{timestamp}.png");
|
||||
image.save(&screenshot_path)
|
||||
.context("Failed to save screenshot")?;
|
||||
|
||||
// Build window info list
|
||||
let mut window_infos = Vec::new();
|
||||
let mut ref_counter = 1usize;
|
||||
|
|
@ -75,6 +67,20 @@ impl super::DesktopBackend for X11Backend {
|
|||
});
|
||||
}
|
||||
|
||||
// Annotate if requested - draw bounding boxes and @wN labels
|
||||
if annotate {
|
||||
annotate_screenshot(&mut image, &window_infos);
|
||||
}
|
||||
|
||||
// Save screenshot
|
||||
let timestamp = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis();
|
||||
let screenshot_path = format!("/tmp/desktop-ctl-{timestamp}.png");
|
||||
image.save(&screenshot_path)
|
||||
.context("Failed to save screenshot")?;
|
||||
|
||||
Ok(Snapshot {
|
||||
screenshot: screenshot_path,
|
||||
windows: window_infos,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue