ui/screen.rs
1// screen -- the monitors, global shortcuts, a colour from anywhere on the screen and pictures of
2// the screen for the app (plat::screen, plat::hotkey)
3//
4// monitors() -> Vec<Monitor> every monitor, the primary one first (rect / work area in
5// desktop units, dpi, refresh, name); a change comes as
6// App::command(UIC_SETTINGS, plat::SET_MONITORS)
7// hotkey_register(id, chord, description) -> Result<(), HotkeyError> a global shortcut
8// ("Ctrl+Alt+K"; plat::Chord::parse names the keys): when it
9// is pressed anywhere, App::command(UIC_HOTKEY, id); when the
10// desktop drops it later (the portal's dialog),
11// App::command(UIC_HOTKEY_LOST, id) and hotkey_error(id)
12// tells why. Needs `input.global` in the manifest.
13// hotkey_unregister(id) -> bool hotkeys_available() -> bool
14// pick_color() -> bool the user clicks a pixel anywhere on the screen (Esc
15// cancels); then App::command(UIC_PICK, 1 picked / 0 not) and
16// picked_color() -> Some(0xRRGGBB); false: not possible here
17// pick_available() -> bool a colour picker with CP_PIPETTE shows its pipette then
18// screenshot(rect) -> bool a picture of the whole desktop (None) or a rectangle; then
19// App::command(UIC_SCREENSHOT, 1 / 0) and screenshot_take() ->
20// Option<Surface> (FMT_XRGB); needs `screen.capture`
21// screen_error() -> Option<ScreenError> why the last pick / picture failed
22//
23// Headless runs (scripts, --shot) touch nothing real: shortcuts are only recorded (the script
24// command `system hotkey ID` presses one), a pick waits for `system pick RRGGBB|cancel`, the
25// monitors are the window (or the script's `screen` rectangle) at the run's dpi, pictures are
26// Unavailable.
27
28use alloc::string::String;
29use alloc::vec::Vec;
30
31use gfx::{Surface, FMT_XRGB};
32use plat::hotkey::{self, Chord, HotkeyError, HotkeyEvent};
33use plat::screen::{self, Monitor, ScreenError, ScreenEvent, Shot};
34
35use super::consts::*;
36use super::Ui;
37
38/// the state of the screen services
39#[derive(Default)]
40pub(crate) struct S {
41 /// a pick is running; the colour picker that asked (0: the app itself)
42 pub(crate) picking: bool,
43 pub(crate) pick_for: u32,
44 /// the last picked colour (taken by picked_color / the colour picker)
45 pub(crate) picked: Option<u32>,
46 /// a picked colour for the colour picker pick_for, until its next pass applies it
47 pub(crate) pending: Option<(u32, u32)>,
48 shot: Option<Surface>,
49 error: Option<ScreenError>,
50 /// why shortcuts were dropped: (id, why)
51 lost: Vec<(u32, HotkeyError)>,
52}
53
54// the B, G, R, x rows of a shot (or its PNG) as a surface
55fn surface_of(s: &Shot) -> Option<Surface> {
56 if let Some((_, data)) = &s.file {
57 let dec = img_core::ImgDec { flags: 0, frame: 0, hint: 0, maxpx: 0, maxmem: 0, cancel: None };
58 return img_core::io::decode_ex(data, Some(&dec), None).ok();
59 }
60 if s.w <= 0 || s.h <= 0 || s.px.len() < (s.w * s.h * 4) as usize {
61 return None;
62 }
63 let mut sf = Surface::new(s.w, s.h, FMT_XRGB);
64 if sf.px.is_empty() {
65 return None;
66 }
67 let row = (s.w * 4) as usize;
68 for y in 0..s.h as usize {
69 let o = y * sf.stride as usize;
70 sf.px[o..o + row].copy_from_slice(&s.px[y * row..y * row + row]);
71 }
72 Some(sf)
73}
74
75impl Ui {
76 /// every monitor, the primary one first
77 pub fn monitors(&mut self) -> Vec<Monitor> {
78 if !self.headless {
79 return screen::monitors();
80 }
81 // headless: the window (or the script's virtual screen) as one monitor
82 let s = self.m.win.screen;
83 let r = if s[2] > 0 && s[3] > 0 { [self.dp(s[0]), self.dp(s[1]), self.dp(s[2]), self.dp(s[3])] } else { [0, 0, self.wp.rt.fb.w, self.wp.rt.fb.h] };
84 alloc::vec![Monitor { id: 0, name: String::from("headless"), rect: r, work: r, dpi: self.wp.rt.dpi.max(1) as u32, refresh_mhz: 60000, primary: true }]
85 }
86
87 /// whether global shortcuts work here
88 pub fn hotkeys_available(&mut self) -> bool {
89 hotkey::set_dry_run(self.headless);
90 hotkey::hotkey_available()
91 }
92
93 /// register chord ("Ctrl+Alt+K") as the global shortcut id; description tells the user
94 /// what it does (the desktop's dialog shows it)
95 pub fn hotkey_register(&mut self, id: u32, chord: &str, description: &str) -> Result<(), HotkeyError> {
96 hotkey::set_dry_run(self.headless);
97 let c = Chord::parse(chord).ok_or(HotkeyError::Invalid)?;
98 self.m.screen.lost.retain(|e| e.0 != id);
99 let r = hotkey::hotkey_register(id, c, description);
100 if let Err(e) = &r {
101 plat::log!(plat::LOG_INFO, "app", "global shortcut {chord}: {e}");
102 }
103 r
104 }
105
106 /// remove the global shortcut id -> false when it was not registered
107 pub fn hotkey_unregister(&mut self, id: u32) -> bool {
108 hotkey::hotkey_unregister(id)
109 }
110
111 /// why the desktop dropped the shortcut id (after UIC_HOTKEY_LOST)
112 pub fn hotkey_error(&self, id: u32) -> Option<HotkeyError> {
113 self.m.screen.lost.iter().find(|e| e.0 == id).map(|e| e.1.clone())
114 }
115
116 /// whether the user can pick a colour anywhere on the screen here (headless: yes, the
117 /// script answers)
118 pub fn pick_available(&self) -> bool {
119 self.headless || screen::pick_available()
120 }
121
122 /// the user picks a pixel anywhere on the screen; UIC_PICK reports the end -> false when
123 /// that is not possible here (or a pick runs)
124 pub fn pick_color(&mut self) -> bool {
125 self.pick_start_for(0)
126 }
127
128 // a pick for colour picker `owner` (0: the app)
129 pub(crate) fn pick_start_for(&mut self, owner: u32) -> bool {
130 if self.m.screen.picking {
131 return false;
132 }
133 self.m.screen.error = None;
134 if !self.headless {
135 if let Err(e) = screen::pick_start() {
136 self.m.screen.error = Some(e);
137 return false;
138 }
139 }
140 self.m.screen.picking = true;
141 self.m.screen.pick_for = owner;
142 true
143 }
144
145 /// the colour of the last pick (0xRRGGBB), once
146 pub fn picked_color(&mut self) -> Option<u32> {
147 self.m.screen.picked.take()
148 }
149
150 /// a picture of the whole desktop (None) or of a rectangle (desktop units); UIC_SCREENSHOT
151 /// reports it -> false when that is not possible here
152 pub fn screenshot(&mut self, rect: Option<[i32; 4]>) -> bool {
153 self.m.screen.error = None;
154 let r = if self.headless { Err(ScreenError::Unavailable) } else { screen::capture_start(rect) };
155 match r {
156 Ok(()) => true,
157 Err(e) => {
158 self.m.screen.error = Some(e);
159 false
160 }
161 }
162 }
163
164 /// the picture of the last screenshot, once
165 pub fn screenshot_take(&mut self) -> Option<Surface> {
166 self.m.screen.shot.take()
167 }
168
169 /// why the last pick or picture failed (None: it worked or was not asked for)
170 pub fn screen_error(&self) -> Option<ScreenError> {
171 self.m.screen.error.clone()
172 }
173
174 // a pick ended: remembered for its colour picker / the app, UIC_PICK
175 pub(crate) fn pick_done(&mut self, r: Result<u32, ScreenError>) {
176 let s = &mut self.m.screen;
177 if !s.picking {
178 return;
179 }
180 s.picking = false;
181 match r {
182 Ok(c) => {
183 if s.pick_for != 0 {
184 s.pending = Some((s.pick_for, c));
185 } else {
186 s.picked = Some(c);
187 }
188 }
189 Err(e) => s.error = Some(e),
190 }
191 let ok = self.m.screen.pending.is_some() || self.m.screen.picked.is_some();
192 if self.m.screen.pick_for == 0 {
193 self.cmd_run(UIC_PICK, ok as u32);
194 }
195 self.repass();
196 }
197
198 /// (E_WAKE) shortcuts pressed or dropped, picks and pictures finished -> App::command
199 pub(crate) fn screen_poll(&mut self) {
200 for e in hotkey::hotkey_take() {
201 match e {
202 HotkeyEvent::Pressed(id) => self.cmd_run(UIC_HOTKEY, id),
203 HotkeyEvent::Refused(id, why) => {
204 plat::log!(plat::LOG_INFO, "app", "global shortcut {id} dropped: {why}");
205 self.m.screen.lost.retain(|e| e.0 != id);
206 self.m.screen.lost.push((id, why));
207 self.cmd_run(UIC_HOTKEY_LOST, id);
208 }
209 }
210 }
211 for e in screen::screen_take() {
212 match e {
213 ScreenEvent::Color(r) => self.pick_done(r),
214 ScreenEvent::Shot(r) => {
215 let ok = match r {
216 Ok(s) => {
217 self.m.screen.shot = surface_of(&s);
218 if self.m.screen.shot.is_none() {
219 self.m.screen.error = Some(ScreenError::Failed(String::from("the picture could not be read")));
220 }
221 self.m.screen.shot.is_some()
222 }
223 Err(e) => {
224 self.m.screen.error = Some(e);
225 false
226 }
227 };
228 self.cmd_run(UIC_SCREENSHOT, ok as u32);
229 }
230 }
231 }
232 }
233}