Skip to main content

plat/
linux_disp.rs

1// linux_disp -- display backend dispatch on Linux: Wayland (linux_wl*.rs) or X11 (linux_x11*.rs)
2//                                                        [src/plat/linux/disp.s, lx_popup.s]
3//
4// The display-dependent functions go through the backend object (trait Display).  The
5// first win_create (the main window, owner PW_NOOWNER) picks the backend:
6//   jfgui_BACKEND=wayland | x11     forces one (only the first letter counts, like disp.s)
7//   WAYLAND_DISPLAY set              Wayland first, then X11 if DISPLAY is set
8//   only DISPLAY set                 X11 first, then Wayland (default socket)
9//   neither                          Wayland (default socket), else an error
10// When no backend can open its main window the process ends with exit code 1 and a
11// message (as disp.s).  Before that the minimum size of window 0 and the refresh callback
12// are remembered and handed to the chosen backend.  wait() without a backend (headless
13// runs) sleeps until its deadline or a wake() (lx_wait_nodisp).
14//
15// The trait is the contract between the two backend agents and with Windows (win_api.rs has
16// the same free functions): change it only together and say so in the report.
17//
18// plat-x11 (R4): the backend choice follows disp.s exactly (first letter of
19// jfgui_BACKEND, the fallback order, exit 1 when every backend fails, the Wayland
20// backend's "quiet" flag: disp_wl_quiet()); Display::pen_devices (default -1) was added
21// for plat_pen_devices; cursor_dpi without a display answers the dpi (pre_cdpi); the
22// headless wait is lx_wait_nodisp (wake fd, E_WAKE).
23
24use alloc::boxed::Box;
25use alloc::string::String;
26use alloc::vec;
27use alloc::vec::Vec;
28use core::sync::atomic::{AtomicBool, Ordering};
29
30use rt::sync::SpinLock;
31
32use crate::Frame;
33
34pub const BK_NONE: u32 = 0;
35pub const BK_WAYLAND: u32 = 1;
36pub const BK_X11: u32 = 2;
37
38/// a display backend (window-indexed; see the free functions below for the semantics)
39pub trait Display: Send {
40    fn ime_pos(&mut self,_x:i32,_y:i32,_h:i32) {}
41    fn win_create(&mut self, title: &str, w_dip: i32, h_dip: i32, flags: u32, owner: i32, x: i32, y: i32) -> i32;
42    fn win_destroy(&mut self, w: i32);
43    fn win_show(&mut self, w: i32, mode: u32);
44    fn win_activate(&mut self, w: i32);
45    fn win_enable(&mut self, w: i32, on: bool);
46    fn win_set_rect(&mut self, w: i32, x: i32, y: i32, cw: i32, ch: i32);
47    fn win_get_rect(&mut self, w: i32) -> Option<[i32; 4]>;
48    fn win_work_area(&mut self, w: i32) -> Option<[i32; 4]>;
49    fn win_set_title(&mut self, w: i32, title: &str);
50    fn win_state(&mut self, w: i32) -> u32;
51    fn win_cmd(&mut self, w: i32, cmd: u32);
52    fn win_begin_move(&mut self, w: i32);
53    fn win_begin_resize(&mut self, w: i32, edges: u32);
54    /// the window manager's window menu at x, y (px of the window) on a button press; default
55    /// none (X11)
56    fn win_show_menu(&mut self, _w: i32, _x: i32, _y: i32) {}
57    fn win_size(&mut self, w: i32) -> (i32, i32);
58    fn win_dpi(&mut self, w: i32) -> i32;
59    fn win_set_min_size(&mut self, w: i32, w_dip: i32, h_dip: i32);
60    fn win_present(&mut self, w: i32, f: &Frame, rects: Option<&[[i32; 4]]>);
61    fn win_set_cursor(&mut self, w: i32, shape: u32);
62    fn set_refresh(&mut self, f: Option<fn()>);
63    fn wait(&mut self, deadline_us: u64) -> u32;
64    fn popup(&mut self, slot: u32, owner: i32, rect: Option<[i32; 4]>, f: Option<&Frame>);
65    /// (§6) how the next popup() of the slot hangs at its anchor -- a backend that places popups
66    /// itself (Wayland) builds its positioner from it; the others show them where popup() says
67    fn popup_anchor(&mut self, _slot: u32, _anchor: [i32; 4], _kind: u32, _nat: [i32; 2]) {}
68    /// where the backend put the slot's popup when it is not where the UI asked (Wayland)
69    fn popup_placed(&mut self, _slot: u32) -> Option<[i32; 2]> {
70        None
71    }
72    fn cursor_create(&mut self, px: &[u8], w: i32, h: i32, hot_x: i32, hot_y: i32, dpi: i32) -> i32;
73    fn cursor_destroy(&mut self, id: i32);
74    fn cursor_dpi(&mut self, dpi: i32) -> i32;
75    fn clip_set(&mut self, fmt: u32, data: &[u8]) -> bool;
76    fn clip_set_many(&mut self, entries: &[(u32, &[u8])]) -> bool;
77    fn clip_get(&mut self, fmt: u32) -> Option<Vec<u8>>;
78    fn clip_has(&mut self, fmt: u32) -> bool;
79    /// pens / pen tablets the backend sees, -1 = unknown (plat_pen_devices: wl_pen_devices /
80    /// x11_pen_devices)  -- plat-x11: added with a default, the Wayland backend overrides it
81    fn pen_devices(&mut self) -> i32 {
82        -1
83    }
84    /// drag data out of the app (dnd.rs drag_start; the left button is held) -> started;
85    /// E_DRAG_END follows -- added with a default for the drag and drop work (X11, Wayland)
86    fn drag_start(&mut self, _data: crate::DragData) -> bool {
87        false
88    }
89}
90
91struct Disp {
92    bk: Option<Box<dyn Display>>,
93    kind: u32,
94    min0: (i32, i32),
95    refresh: Option<fn()>,
96    hit: Option<fn(i32, i32, i32) -> u32>,
97    mv: Option<fn(i32, &mut [i32; 4])>,
98}
99
100static DISP: SpinLock<Disp> =
101    SpinLock::new(Disp { bk: None, kind: BK_NONE, min0: (0, 0), refresh: None, hit: None, mv: None });
102static WL_QUIET: AtomicBool = AtomicBool::new(false);
103
104/// 0 none, 1 Wayland, 2 X11
105pub fn disp_backend() -> u32 {
106    DISP.lock().kind
107}
108
109/// (disp.s g_wl_quiet) the Wayland backend is being tried without jfgui_BACKEND=wayland:
110/// it should fail without messages (X11 may follow)
111pub fn disp_wl_quiet() -> bool {
112    WL_QUIET.load(Ordering::Relaxed)
113}
114
115fn nonempty_env(name: &str) -> bool {
116    crate::env(name).is_some_and(|v| !v.is_empty())
117}
118
119// one backend: connect, replay the remembered minimum size / refresh callback, create the
120// main window -> (backend, index) or None
121fn try_backend(k: u32, title: &str, w_dip: i32, h_dip: i32, flags: u32, x: i32, y: i32) -> Option<(Box<dyn Display>, i32)> {
122    let mut b = if k == BK_WAYLAND { crate::wl_connect()? } else { crate::x11_connect()? };
123    let (min0, refresh) = {
124        let d = DISP.lock();
125        (d.min0, d.refresh)
126    };
127    b.win_set_min_size(0, min0.0, min0.1);
128    if refresh.is_some() {
129        b.set_refresh(refresh);
130    }
131    let w = b.win_create(title, w_dip, h_dip, flags, crate::PW_NOOWNER, x, y);
132    if w < 0 { None } else { Some((b, w)) }
133}
134
135// the first window chooses the backend (pre_win_create); every failure ends the process
136fn choose(title: &str, w_dip: i32, h_dip: i32, flags: u32, x: i32, y: i32) -> i32 {
137    // forced: 0 auto, BK_WAYLAND, BK_X11 (only the first letter of jfgui_BACKEND counts)
138    let forced = match crate::env("jfgui_BACKEND").and_then(|v| v.bytes().next()) {
139        Some(b'x') => BK_X11,
140        Some(b'w') => BK_WAYLAND,
141        _ => 0,
142    };
143    let mut x11_first = false;
144    let mut next = BK_WAYLAND;
145    if forced != 0 {
146        next = forced;
147    } else if !nonempty_env("WAYLAND_DISPLAY") && nonempty_env("DISPLAY") {
148        x11_first = true;
149        next = BK_X11;
150    }
151    loop {
152        if next == BK_WAYLAND {
153            WL_QUIET.store(forced != BK_WAYLAND, Ordering::Relaxed);
154            DISP.lock().kind = BK_WAYLAND;
155            if let Some((b, w)) = try_backend(BK_WAYLAND, title, w_dip, h_dip, flags, x, y) {
156                DISP.lock().bk = Some(b);
157                return w;
158            }
159            if forced != 0 || x11_first || !nonempty_env("DISPLAY") {
160                break;
161            }
162            next = BK_X11;
163        } else {
164            DISP.lock().kind = BK_X11;
165            if let Some((b, w)) = try_backend(BK_X11, title, w_dip, h_dip, flags, x, y) {
166                DISP.lock().bk = Some(b);
167                return w;
168            }
169            if forced != 0 || !x11_first {
170                break;
171            }
172            x11_first = false; // X11 first failed: try Wayland too
173            next = BK_WAYLAND;
174        }
175    }
176    crate::write_stderr(
177        b"error: no display available -- neither a Wayland compositor (WAYLAND_DISPLAY) nor an X11 server \
178(DISPLAY) could be opened (use --shot FILE.png for a headless render)\n",
179    );
180    crate::exit(1)
181}
182
183// run f on the backend (None without one).  The backend is taken out of the lock for the
184// call: a call that re-enters the plat API from inside a backend method (a refresh or hit
185// hook) finds no backend and gets the headless answer instead of a deadlock.
186fn with<R>(f: impl FnOnce(&mut dyn Display) -> R) -> Option<R> {
187    let mut b = DISP.lock().bk.take()?;
188    let r = f(b.as_mut());
189    DISP.lock().bk = Some(b);
190    Some(r)
191}
192
193/// see win_api.rs (Windows) for the semantics of every function
194pub fn win_create(title: &str, w_dip: i32, h_dip: i32, flags: u32, owner: i32, x: i32, y: i32) -> i32 {
195    let none = {
196        let d = DISP.lock();
197        d.bk.is_none() && d.kind == BK_NONE
198    };
199    if none {
200        if owner != crate::PW_NOOWNER {
201            return -1; // an owned window before any display
202        }
203        let w = choose(title, w_dip, h_dip, flags, x, y);
204        if w >= 0 {
205            crate::life_start(); // SIGTERM, sleep and settings for the app
206        }
207        return w;
208    }
209    with(|b| b.win_create(title, w_dip, h_dip, flags, owner, x, y)).unwrap_or(-1)
210}
211
212pub fn win_destroy(w: i32) {
213    with(|b| b.win_destroy(w));
214}
215
216pub fn win_show(w: i32, mode: u32) {
217    with(|b| b.win_show(w, mode));
218}
219
220pub fn win_activate(w: i32) {
221    with(|b| b.win_activate(w));
222}
223
224pub fn win_enable(w: i32, on: bool) {
225    with(|b| b.win_enable(w, on));
226}
227
228pub fn win_set_rect(w: i32, x: i32, y: i32, cw: i32, ch: i32) {
229    with(|b| b.win_set_rect(w, x, y, cw, ch));
230}
231
232pub fn win_get_rect(w: i32) -> Option<[i32; 4]> {
233    with(|b| b.win_get_rect(w)).flatten()
234}
235
236pub fn win_work_area(w: i32) -> Option<[i32; 4]> {
237    with(|b| b.win_work_area(w)).flatten()
238}
239
240pub fn win_set_title(w: i32, title: &str) {
241    with(|b| b.win_set_title(w, title));
242}
243
244pub fn win_state(w: i32) -> u32 {
245    with(|b| b.win_state(w)).unwrap_or(0)
246}
247
248pub fn win_cmd(w: i32, cmd: u32) {
249    with(|b| b.win_cmd(w, cmd));
250}
251
252pub fn win_begin_move(w: i32) {
253    with(|b| b.win_begin_move(w));
254}
255
256pub fn win_begin_resize(w: i32, edges: u32) {
257    with(|b| b.win_begin_resize(w, edges));
258}
259
260/// the compositor's window menu (move, resize, always on top, ...) of window w at x, y (px of
261/// the window); call on the press of the right button over a library-drawn title bar
262pub fn win_show_menu(w: i32, x: i32, y: i32) {
263    with(|b| b.win_show_menu(w, x, y));
264}
265
266pub fn win_size(w: i32) -> (i32, i32) {
267    with(|b| b.win_size(w)).unwrap_or((0, 0))
268}
269
270pub fn win_dpi(w: i32) -> i32 {
271    with(|b| b.win_dpi(w)).unwrap_or(96)
272}
273
274pub fn win_set_min_size(w: i32, w_dip: i32, h_dip: i32) {
275    let mut d = DISP.lock();
276    match d.bk.as_mut() {
277        Some(b) => b.win_set_min_size(w, w_dip, h_dip),
278        None if w == 0 => d.min0 = (w_dip, h_dip),
279        None => {}
280    }
281}
282
283/// show the frame's dirty rectangles (x, y, w, h; None = all) in window w; pixels outside
284/// them must be what the last present of w showed (linux_x11_present.rs, linux_wl_present.rs)
285pub fn win_present(w: i32, f: &Frame, rects: Option<&[[i32; 4]]>) {
286    let t0 = crate::time_us();
287    with(|b| b.win_present(w, f, rects));
288    crate::present_account(t0, true);
289}
290
291pub fn win_set_cursor(w: i32, shape: u32) {
292    with(|b| b.win_set_cursor(w, shape));
293}
294
295/// stored: the compositor needs no hit tests, the Wayland backend asks it for the click
296/// activation of child windows (disp_hit)
297pub fn win_hit_hook(f: Option<fn(i32, i32, i32) -> u32>) {
298    DISP.lock().hit = f;
299}
300
301/// stored: the window manager or compositor moves toplevels and does not ask; the Wayland
302/// backend snaps the child windows it moves itself (disp_moving)
303pub fn win_move_hook(f: Option<fn(i32, &mut [i32; 4])>) {
304    DISP.lock().mv = f;
305}
306
307/// (backends) the UI's HB_* flags at a client point of window w (0 without a hook)
308pub(crate) fn disp_hit(w: i32, x: i32, y: i32) -> u32 {
309    let f = DISP.lock().hit;
310    f.map_or(0, |f| f(w, x, y))
311}
312
313/// (backends, a window moved by the library) the move hook may snap rect (x, y, w, h px;
314/// a child window's position relative to its owner)
315pub(crate) fn disp_moving(w: i32, rect: &mut [i32; 4]) {
316    let f = DISP.lock().mv;
317    if let Some(f) = f {
318        f(w, rect);
319    }
320}
321
322pub fn set_refresh(f: Option<fn()>) {
323    let mut d = DISP.lock();
324    d.refresh = f;
325    if let Some(b) = d.bk.as_mut() {
326        b.set_refresh(f);
327    }
328}
329
330/// pump the display until an event is queued or the deadline passes (0 = poll, u64::MAX =
331/// forever); without a display (headless) sleep until the deadline or a wake()
332/// [lx_wait_nodisp]
333pub fn wait(deadline_us: u64) -> u32 {
334    crate::life_poll(); // a SIGTERM: E_SESSION_END
335    if crate::evq_count() != 0 {
336        return crate::evq_count();
337    }
338    if let Some(n) = with(|b| b.wait(deadline_us)) {
339        crate::life_poll();
340        return n.max(crate::evq_count());
341    }
342    if crate::evq_count() == 0 {
343        crate::wait_events(deadline_us, &mut |e| {
344            crate::evq_push(*e);
345        });
346    }
347    crate::evq_count()
348}
349
350/// show / move / present popup slot `slot` over window `owner` (rect None hides it)  [lx_popup.s]
351pub fn popup(slot: u32, owner: i32, rect: Option<[i32; 4]>, f: Option<&Frame>) {
352    with(|b| b.popup(slot, owner, rect, f));
353}
354
355/// (§6) how the next popup() of slot `slot` hangs at its anchor: anchor x, y, w, h and the
356/// popup's natural place x, y (owner px), PA_* kind.  Wayland builds the xdg_positioner from
357/// it (the compositor flips / slides the popup to keep it on screen); X11 places it as asked.
358pub fn popup_anchor(slot: u32, anchor: [i32; 4], kind: u32, nat: [i32; 2]) {
359    with(|b| b.popup_anchor(slot, anchor, kind, nat));
360}
361
362/// (§6) where the platform put popup slot `slot` (owner px) when that is not its natural place
363/// (Wayland: the compositor's configure); None: where the UI asked, or not known yet
364pub fn popup_placed(slot: u32) -> Option<[i32; 2]> {
365    with(|b| b.popup_placed(slot)).flatten()
366}
367
368pub fn cursor_create(px: &[u8], w: i32, h: i32, hot_x: i32, hot_y: i32, dpi: i32) -> i32 {
369    with(|b| b.cursor_create(px, w, h, hot_x, hot_y, dpi)).unwrap_or(-1)
370}
371
372pub fn cursor_destroy(id: i32) {
373    with(|b| b.cursor_destroy(id));
374}
375
376/// the dpi to draw a cursor for a window of this dpi (no display yet: the dpi itself)
377pub fn cursor_dpi(dpi: i32) -> i32 {
378    with(|b| b.cursor_dpi(dpi)).unwrap_or(dpi)
379}
380
381pub fn clip_set(fmt: u32, data: &[u8]) -> bool {
382    with(|b| b.clip_set(fmt, data)).unwrap_or(false)
383}
384
385/// Publish distinct TEXT/HTML/RTF/DOCX representations of one selection.
386pub fn clip_set_many(entries: &[(u32, &[u8])]) -> bool {
387    if !crate::clip_many_valid(entries) { return false; }
388    with(|b| b.clip_set_many(entries)).unwrap_or(false)
389}
390
391pub fn clip_get(fmt: u32) -> Option<Vec<u8>> {
392    let data = with(|b| b.clip_get(fmt)).flatten()?;
393    if matches!(fmt, crate::CLIP_HTML | crate::CLIP_RTF | crate::CLIP_DOCX) && data.len() > crate::CLIP_RICH_MAX {
394        return None;
395    }
396    if fmt == crate::CLIP_HTML && (data.contains(&0) || core::str::from_utf8(&data).is_err()) {
397        return None;
398    }
399    Some(data)
400}
401
402pub fn clip_has(fmt: u32) -> bool {
403    with(|b| b.clip_has(fmt)).unwrap_or(false)
404}
405
406/// Forward caret position to backends with text-input support.
407pub fn ime_pos(x: i32, y: i32, h: i32) { with(|b| b.ime_pos(x,y,h)); }
408
409/// (dnd.rs) the backend drags data out of the app
410pub(crate) fn os_drag_start(data: crate::DragData) -> bool {
411    with(|b| b.drag_start(data)).unwrap_or(false)
412}
413
414// ---------------------------------------------------------------- process level (plat-x11 agent)
415// [src/plat/linux/plat_linux.s, x11_xi.s]
416
417/// 0 light, 1 dark, -1 unknown: jfgui_THEME=light|dark wins, then GTK_THEME "Name:dark" or
418/// "Name-dark", then the settings portal's answer at the last sys_settings (color-scheme;
419/// GNOME's "Default" is light; unknown before the first, e.g. in headless runs)
420/// [plat_theme]
421pub fn theme() -> i32 {
422    if let Some(t) = crate::env("jfgui_THEME") {
423        if t == "dark" {
424            return 1;
425        }
426        if t == "light" {
427            return 0;
428        }
429    }
430    if crate::env("GTK_THEME").is_some_and(|g| crate::linux_settings::theme_is_dark(&g)) {
431        return 1;
432    }
433    crate::linux_settings::portal_dark()
434}
435
436/// pens / pen tablets of the display backend (-1 = unknown or no display)  [plat_pen_devices]
437pub fn pen_devices() -> i32 {
438    with(|b| b.pen_devices()).unwrap_or(-1)
439}
440
441/// $HOME (or "/")  [plat_home]
442pub fn home() -> Option<String> {
443    Some(crate::env("HOME").unwrap_or_else(|| String::from("/")))
444}
445
446/// the root "/"  [plat_drives]
447pub fn drives() -> Vec<String> {
448    vec![String::from("/")]
449}
450
451const SPAWN_ARGS: usize = 32; // argv entries at most (argv[0] included)
452
453/// fork; the child execs /proc/self/exe with argv[0], the arguments (at most 31) and the
454/// current environment (exit 127 when that fails) -> 0 / -1  [plat_spawn_self]
455pub fn spawn_self(args: &[&str]) -> i32 {
456    spawn_exe(b"/proc/self/exe\0", args)
457}
458
459/// like spawn_self, but the program file now at this process's path: after an update put a
460/// new file there, /proc/self/exe still names the old one ("PATH (deleted)") -> 0 / -1
461pub fn restart_self(args: &[&str]) -> i32 {
462    let Some(p) = crate::current_exe() else { return -1 };
463    let mut path = String::from(p.strip_suffix(" (deleted)").unwrap_or(&p)).into_bytes();
464    if path.contains(&0) {
465        return -1;
466    }
467    path.push(0);
468    spawn_exe(&path, args)
469}
470
471/// like restart_self, but the program file at path (pkg::restart_appimage: the AppImage of
472/// this program, whose runtime unpacks and runs the image it holds now) -> 0 / -1
473pub(crate) fn spawn_file(path: &str, args: &[&str]) -> i32 {
474    if path.is_empty() || path.contains('\0') {
475        return -1;
476    }
477    let mut p = String::from(path).into_bytes();
478    p.push(0);
479    spawn_exe(&p, args)
480}
481
482// fork; the child execs exe (NUL-terminated) with argv[0], args and the environment
483fn spawn_exe(exe: &[u8], args: &[&str]) -> i32 {
484    if args.len() >= SPAWN_ARGS {
485        return -1;
486    }
487    fn z(s: &[u8]) -> Vec<u8> {
488        let mut v = s.to_vec();
489        v.push(0);
490        v
491    }
492    // every string and pointer array is built before the fork: the child must not allocate
493    let a0 = super::args().into_iter().next().unwrap_or_default();
494    let mut strs: Vec<Vec<u8>> = Vec::with_capacity(args.len() + 1);
495    strs.push(z(a0.as_bytes()));
496    for a in args {
497        strs.push(z(a.as_bytes()));
498    }
499    let envs: Vec<Vec<u8>> = {
500        let mut g = super::ENV.lock();
501        super::env_list(&mut g).iter().map(|e| z(e)).collect()
502    };
503    let mut argv: Vec<usize> = strs.iter().map(|s| s.as_ptr() as usize).collect();
504    argv.push(0);
505    let mut envp: Vec<usize> = envs.iter().map(|s| s.as_ptr() as usize).collect();
506    envp.push(0);
507    // SAFETY: fork has no arguments; the child only calls execve with the NUL-terminated
508    // strings and 0-terminated pointer arrays built above, then exit_group.
509    unsafe {
510        let pid = rt::os::fork();
511        if pid < 0 {
512            return -1;
513        }
514        if pid == 0 {
515            rt::os::syscall3(rt::os::SYS_EXECVE, exe.as_ptr() as usize, argv.as_ptr() as usize, envp.as_ptr() as usize);
516            rt::os::syscall3(rt::os::SYS_EXIT_GROUP, 127, 0, 0);
517            loop {}
518        }
519    }
520    0
521}