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;