Skip to main content

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}