Skip to main content

plat/
screen.rs

1// screen -- the monitors, the colour of any pixel on the screen, pictures of the screen
2//
3//   monitors() -> Vec<Monitor>        every monitor: id, name, rectangle and work area (desktop
4//                                     units), dpi, refresh rate, the primary one first; when the
5//                                     set or one of them changes, E_SETTINGS with SET_MONITORS
6//   monitor_at(x, y) -> Option<Monitor>   the monitor holding a desktop point, else the primary
7//   pick_available() -> bool          the user can pick a colour outside the app's windows here
8//   pick_start() -> Result<(), ScreenError>   the user clicks a pixel anywhere on the screen (Esc
9//                                     or a right click cancels); the answer is ScreenEvent::Color
10//   capture_available() -> bool       pictures of the screen can be taken here
11//   capture_start(rect) -> Result<(), ScreenError>   a picture of the whole desktop (None) or
12//                                     of a rectangle (desktop units); needs `screen.capture` in
13//                                     the manifest (else Denied, "PERMDENY screen.capture"); the
14//                                     answer is ScreenEvent::Shot
15//   screen_take() -> Vec<ScreenEvent> the answers since the last call (each one calls wake())
16//   pick_test_point(Some((x, y)))     (test aid) the next pick_start answers the pixel at the
17//                                     desktop point x, y without asking the user (Windows,
18//                                     X11; the portal and the browser ignore it)
19//
20// One pick and one capture at a time (else Busy).  Picking needs no right: the user chooses
21// the one pixel the app gets, like a file in the file dialog.  Call pick_start and
22// capture_start from the UI thread: their answers are made there (Windows, X11) or on a thread
23// of their own (the portal), and arrive through the event loop.
24//
25// Windows (win_screen.rs): EnumDisplayMonitors + GetMonitorInfoW (device name, work area,
26// primary), GetDpiForMonitor (shcore), EnumDisplaySettingsW (refresh), WM_DISPLAYCHANGE and a
27// changed work area -> SET_MONITORS; capture: BitBlt (CAPTUREBLT) from the screen DC into a DIB
28// section; pick: a topmost window over the virtual desktop showing a frozen picture of it with
29// a magnifier at the pointer.  Linux (linux_screen.rs): X11 RandR 1.5 (GetMonitors, the CRTC's
30// mode for the refresh rate, _NET_WORKAREA, RRScreenChangeNotify), GetImage of the root window,
31// a pointer grab for the pick; Wayland wl_output (+ zxdg_output_v1 for logical position and
32// size; no work area, no primary: the first output); in Wayland sessions and in Flatpak the
33// screenshot portal (Screenshot, PickColor: the desktop asks the user).  Browser: the host's
34// window.screen (getScreenDetails when the page may), the EyeDropper API (Chromium) for the
35// pick, no capture.  Android: the host's DisplayManager displays, no pick, no capture.
36
37use alloc::string::String;
38use alloc::vec::Vec;
39use core::sync::atomic::{AtomicBool, Ordering};
40
41use rt::sync::SpinLock;
42
43/// one monitor
44#[derive(Clone, Debug, Default, PartialEq, Eq)]
45pub struct Monitor {
46    /// stable while the monitor stays connected (Windows: a hash of its device name, X11 the
47    /// RandR monitor's name atom, Wayland the output's global name, browser / Android an index)
48    pub id: u64,
49    /// the system's name ("\\.\DISPLAY1", "HDMI-1", "rdp-0"); "" when unknown
50    pub name: String,
51    /// x, y, w, h in desktop units: physical pixels on Windows, X11 and Android; the
52    /// compositor's logical units on Wayland; CSS pixels in the browser
53    pub rect: [i32; 4],
54    /// the part windows should use (without taskbars, docks, panels); = rect where unknown
55    pub work: [i32; 4],
56    /// 96 = 100 %
57    pub dpi: u32,
58    /// refresh rate in mHz (60000 = 60 Hz), 0 = unknown
59    pub refresh_mhz: u32,
60    /// the primary monitor (exactly one in a non-empty list)
61    pub primary: bool,
62}
63
64/// why a pick or a capture did not happen
65#[derive(Clone, Debug, PartialEq, Eq)]
66#[non_exhaustive]
67pub enum ScreenError {
68    /// not possible here (browser without EyeDropper, Android, no X server / portal)
69    Unavailable,
70    /// the app's permissions lack `screen.capture`, or the system / the user refused
71    Denied,
72    /// the user cancelled (Esc, a right click, the portal's dialog dismissed)
73    Cancelled,
74    /// a pick (or capture) is still running
75    Busy,
76    /// the system failed: what it said
77    Failed(String),
78}
79
80impl core::fmt::Display for ScreenError {
81    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
82        match self {
83            ScreenError::Unavailable => f.write_str("not available here"),
84            ScreenError::Denied => f.write_str("refused"),
85            ScreenError::Cancelled => f.write_str("cancelled"),
86            ScreenError::Busy => f.write_str("busy"),
87            ScreenError::Failed(s) => write!(f, "failed: {s}"),
88        }
89    }
90}
91
92/// a picture of the screen
93#[derive(Clone, Debug, Default, PartialEq, Eq)]
94pub struct Shot {
95    /// where it was taken: x, y, w, h in desktop units ([0, 0, w, h] when the system did not
96    /// say: the portal)
97    pub rect: [i32; 4],
98    /// size in pixels
99    pub w: i32,
100    pub h: i32,
101    /// w x h pixels B, G, R, x (rows of w * 4 bytes); empty when `file` holds the picture
102    pub px: Vec<u8>,
103    /// the picture as the system wrote it (the screenshot portal's PNG): its name and bytes
104    pub file: Option<(String, Vec<u8>)>,
105}
106
107/// an answer of pick_start / capture_start
108#[derive(Clone, Debug, PartialEq, Eq)]
109pub enum ScreenEvent {
110    /// the picked colour 0xRRGGBB
111    Color(Result<u32, ScreenError>),
112    /// the picture
113    Shot(Result<Shot, ScreenError>),
114}
115
116static EVENTS: SpinLock<Vec<ScreenEvent>> = SpinLock::new(Vec::new());
117static PICKING: AtomicBool = AtomicBool::new(false);
118static CAPTURING: AtomicBool = AtomicBool::new(false);
119static TEST_POINT: SpinLock<Option<(i32, i32)>> = SpinLock::new(None);
120
121/// (platform) the answer of the running pick: queued, the UI thread woken
122#[allow(dead_code)]
123pub(crate) fn push_color(r: Result<u32, ScreenError>) {
124    EVENTS.lock().push(ScreenEvent::Color(r));
125    PICKING.store(false, Ordering::SeqCst);
126    crate::wake();
127}
128
129/// (platform) the answer of the running capture
130#[allow(dead_code)]
131pub(crate) fn push_shot(r: Result<Shot, ScreenError>) {
132    EVENTS.lock().push(ScreenEvent::Shot(r));
133    CAPTURING.store(false, Ordering::SeqCst);
134    crate::wake();
135}
136
137/// (platform) a pick is running
138#[allow(dead_code)]
139pub(crate) fn picking() -> bool {
140    PICKING.load(Ordering::SeqCst)
141}
142
143/// the answers since the last call, oldest first
144pub fn screen_take() -> Vec<ScreenEvent> {
145    core::mem::take(&mut *EVENTS.lock())
146}
147
148/// every monitor, the primary one first (empty when nothing is known: Linux before the first
149/// window, headless runs)
150pub fn monitors() -> Vec<Monitor> {
151    let mut v = native::monitors();
152    sort_primary(&mut v);
153    v
154}
155
156/// the primary monitor first, the others by position; exactly one primary
157pub(crate) fn sort_primary(v: &mut [Monitor]) {
158    if v.is_empty() {
159        return;
160    }
161    if !v.iter().any(|m| m.primary) {
162        v[0].primary = true;
163    }
164    let mut seen = false;
165    for m in v.iter_mut() {
166        if m.primary && seen {
167            m.primary = false;
168        }
169        seen |= m.primary;
170    }
171    v.sort_by_key(|m| (!m.primary, m.rect[1], m.rect[0]));
172}
173
174/// the monitor holding the desktop point x, y, else the primary one
175pub fn monitor_at(x: i32, y: i32) -> Option<Monitor> {
176    let v = monitors();
177    let inside = |m: &&Monitor| x >= m.rect[0] && y >= m.rect[1] && x < m.rect[0] + m.rect[2] && y < m.rect[1] + m.rect[3];
178    v.iter().find(inside).or(v.first()).cloned()
179}
180
181/// whether the user can pick a colour anywhere on the screen here
182pub fn pick_available() -> bool {
183    native::pick_available()
184}
185
186/// the user picks a pixel anywhere on the screen; the answer comes as ScreenEvent::Color
187pub fn pick_start() -> Result<(), ScreenError> {
188    if PICKING.swap(true, Ordering::SeqCst) {
189        return Err(ScreenError::Busy);
190    }
191    let test = TEST_POINT.lock().take();
192    let r = native::pick_start(test);
193    if r.is_err() {
194        PICKING.store(false, Ordering::SeqCst);
195    }
196    r
197}
198
199/// (test aid) the next pick_start answers the pixel at the desktop point x, y at once,
200/// without asking the user (Windows, X11; the portal and the browser ignore it); None: ask
201/// again
202pub fn pick_test_point(p: Option<(i32, i32)>) {
203    *TEST_POINT.lock() = p;
204}
205
206/// whether pictures of the screen can be taken here (the permission is checked when taking)
207pub fn capture_available() -> bool {
208    native::capture_available()
209}
210
211/// a picture of the whole desktop (None) or of the rectangle x, y, w, h (desktop units); the
212/// answer comes as ScreenEvent::Shot.  The screenshot portal takes the whole screen (or what
213/// the user chooses in its dialog) whatever rect says.
214pub fn capture_start(rect: Option<[i32; 4]>) -> Result<(), ScreenError> {
215    if rect.is_some_and(|r| r[2] <= 0 || r[3] <= 0 || r[2] > 32768 || r[3] > 32768) {
216        return Err(ScreenError::Failed(String::from("empty or too large rectangle")));
217    }
218    if !crate::policy::screen_capture_allowed() {
219        return Err(ScreenError::Denied);
220    }
221    if CAPTURING.swap(true, Ordering::SeqCst) {
222        return Err(ScreenError::Busy);
223    }
224    let r = native::capture_start(rect);
225    if r.is_err() {
226        CAPTURING.store(false, Ordering::SeqCst);
227    }
228    r
229}
230
231/// the pixel 0xRRGGBB at x, y of a shot's B, G, R, x rows (None outside)
232pub fn shot_pixel(s: &Shot, x: i32, y: i32) -> Option<u32> {
233    if x < 0 || y < 0 || x >= s.w || y >= s.h {
234        return None;
235    }
236    let o = (y as usize * s.w as usize + x as usize) * 4;
237    let p = s.px.get(o..o + 3)?;
238    Some((p[2] as u32) << 16 | (p[1] as u32) << 8 | p[0] as u32)
239}
240
241#[cfg(windows)]
242use crate::win_screen as native;
243
244#[cfg(target_os = "linux")]
245use crate::linux_screen as native;
246
247#[cfg(any(target_arch = "wasm32", target_os = "android"))]
248use crate::web_screen as native;