Skip to main content

ui/
runtime.rs

1// runtime -- the command line, window or headless run, event loop, passes, rendering
2//                                                                     [src/ui/ui_main.s]
3//
4//   run(desc, app, style) -> exit code     jfgui_main of an app
5//   fb() -> &Surface   dpi() -> i32   dp(dip)   px2dip(px)   headless() -> bool
6//   invalidate()   quit()   client_size() -> (w, h) DIP   set_title(s)   title() -> &str
7// Runtime internals used by the script runner (auto.rs): run_passes, render,
8// process_events, resize(w_px, h_px).
9//
10// Command line (every app):
11//   --shot FILE.png     headless: render the first frame, write a PNG, exit
12//   --script FILE       headless: run a UI test script (auto.rs)
13//   --dump              headless: print the widget tree (JSON lines) after --shot
14//   --size WxH          client size in DIP (default: the AppDesc)
15//   --scale PERCENT     headless scale (100 = 96 dpi)
16//   --theme light|dark  force a theme (default: AppDesc flags, else the system)
17//   --accent RRGGBB     accent colour (default: the AppDesc, else the system's)
18//   --anim off          no animations (values jump to their targets; default: the system's
19//                       "reduce motion")
20//   --contrast high|normal   the high-contrast theme on / off (default: the system)
21//   --text-scale PERCENT     text sizes and line heights (default: the system's text scale)
22//   --rtl / --ltr       mirror the windows / never (default: the UI language)
23//   --log-events        print every platform event
24//   --auto-close MS     close the window after MS milliseconds (smoke tests)
25//   --verify-dirty      after every partial redraw compare with a full redraw
26//   --inspect           the live inspector open (inspect.rs; jfgui_INSPECT=1: its hot keys only)
27//   (jfgui_HOTRELOAD=DIR: hot reload's session of `build_rs.py watch`, development builds:
28//   the input recorded and replayed at the next start, hotreload.rs)
29//
30// The frame loop: platform events are applied to the input state one "edge" (button,
31// key, wheel) at a time; each edge runs App::frame (plus up to three settle passes when
32// the layout moved); commands queued by widgets are delivered to App::command in between;
33// then the recorded draw commands are rendered and presented.  Async tasks (ui.spawn,
34// tasks.rs) run after the commands of a round; their timers bound the wait.  Nothing runs
35// while the app is idle.  Several windows (win.rs): one App::frame builds all of them; every
36// event carries its window (Event::win).
37
38#[cfg(any(target_arch = "wasm32", target_os = "android", target_os = "ios"))]
39use alloc::collections::BTreeMap;
40use alloc::string::String;
41use alloc::vec::Vec;
42
43use super::consts::*;
44use super::{App, AppDesc, Style, Ui, UiInput, WinParts};
45use gfx::{Dc, Surface, FMT_XRGB};
46use plat::Event;
47use text::Text;
48
49const MAX_PASSES: u32 = 4;
50
51/// Launch the same owned app on a desktop event loop, browser worker, Android or iOS host.
52/// Host-driven applications remain alive after their entrypoint returns.  iOS: a headless run
53/// (--shot, --script, --agent-json, --agent-mcp) stays the desktop's, the rest is the UIKit host's
54/// (plat apple_ios.rs, which takes the main thread when this returned 0).
55pub fn launch(desc: &AppDesc, app: impl App + 'static, style: &'static dyn Style) -> i32 {
56    #[cfg(not(any(target_arch = "wasm32", target_os = "android", target_os = "ios")))]
57    { let mut app = app; run(desc, &mut app, style) }
58    #[cfg(target_os = "ios")]
59    {
60        let a = Args(plat::args());
61        if a.value("--shot").is_some() || a.value("--script").is_some() || a.flag("--agent-json") || a.flag("--agent-mcp") {
62            let mut app = app;
63            return run(desc, &mut app, style);
64        }
65    }
66    #[cfg(any(target_arch = "wasm32", target_os = "android", target_os = "ios"))]
67    {
68        let (mut ui, w, h) = prepare(desc, style);
69        plat::win_hit_hook(Some(super::win::uiw_hit_pw));
70        plat::drag_hook(Some(super::win::uiw_drag_pw));
71        plat::set_min_size(desc.min_w as i32, desc.min_h as i32);
72        if !plat::window_create(desc.title, w, h, desc.flags) { return 2; }
73        ui.win_hooks_set(true);
74        ui.wp.rt.dpi = plat::dpi();
75        let (w, h) = plat::window_size();
76        ui.wp.rt.fb = Surface::new(w, h, FMT_XRGB);
77        ui.set_time(0);
78        ui.gp_window_mode(); // the output through the GPU, Android and the browser: its work starts after the first frame (gpu_out.rs)
79        // the system's settings the host reported (media queries, Android's configuration;
80        // plat web_system.rs) and the jfgui_* overrides
81        ui.sys_apply(plat::sys_settings());
82        super::perm::ask_here(true); // permission questions: a dialog in the page (perm.rs)
83        let mut app: alloc::boxed::Box<dyn App> = alloc::boxed::Box::new(app);
84        if ui.wp.rt.fb.px.is_empty() || !app.init(&mut ui) { return 2; }
85        ui.set_agent_enabled(Args(plat::args()).flag("--agent"));
86        ui.run_passes(app.as_mut());
87        ui.render();
88        ui.present();
89        let runtime = Runtime { ui, app, finished: false };
90        if plat::web::install(alloc::boxed::Box::new(HostRuntime {
91            runtime, sessions: BTreeMap::new(), session_clock: 0,
92        })) { 0 } else { 2 }
93    }
94}
95
96#[cfg(any(target_arch = "wasm32", target_os = "android", target_os = "ios"))]
97struct HostRuntime {
98    runtime: Runtime,
99    sessions: BTreeMap<String, (u64, agent_core::RpcSession)>,
100    session_clock: u64,
101}
102#[cfg(any(target_arch = "wasm32", target_os = "android", target_os = "ios"))]
103impl plat::web::WebApplication for HostRuntime {
104    fn frame_delay(&self) -> u32 {
105        let ui = &self.runtime.ui;
106        if ui.need || ui.need_render || ui.animating || ui.touch_busy() || ui.tasks_ready() { return 0; }
107        let task = ui.tasks_deadline().map(|t| (t.saturating_sub(plat::task::now_us()) / 1000).min(250) as i32);
108        // the GPU output's jobs and frames still to show (Android, the browser)
109        let gpu = ui.gp_deadline().map(|t| (t.saturating_sub(plat::time_us()).div_ceil(1000)).min(250) as i32);
110        ui.timers.iter()
111            .map(|(_, deadline)| deadline.wrapping_sub(ui.now_ms) as i32)
112            .chain(task)
113            .chain(gpu)
114            .min().unwrap_or(250).clamp(0, 250) as u32
115    }
116    fn input(&mut self, time_ms: u32) {
117        if self.runtime.finished { return; }
118        self.runtime.ui.set_time(time_ms);
119        self.runtime.ui.process_events_deferred(self.runtime.app.as_mut(), false);
120        // Resize/drop/wake queue commands rather than an ordinary input edge. Settle
121        // their layout before routing the following pointer/key event in this batch.
122        if self.runtime.ui.need {
123            self.runtime.ui.need = false;
124            self.runtime.ui.run_passes(self.runtime.app.as_mut());
125        }
126    }
127    fn set_control_enabled(&mut self, enabled: bool) {
128        self.runtime.set_agent_enabled(enabled);
129        if !enabled { self.sessions.clear(); }
130    }
131    fn tick(&mut self, time_ms: u32) {
132        #[cfg(any(target_os = "android", target_os = "ios"))]
133        let running = !self.runtime.finished;
134        self.runtime.step(time_ms, &[]);
135        // ui.quit: there nobody ends the activity / the process but the app (the browser's page stays)
136        #[cfg(any(target_os = "android", target_os = "ios"))]
137        if running && self.runtime.finished {
138            plat::app_finish();
139        }
140        self.runtime.ui.present();
141        self.runtime.ui.gp_tick();
142    }
143    #[cfg(target_os = "android")]
144    fn surface_gone(&mut self) -> bool {
145        self.runtime.ui.gp_surface_gone()
146    }
147    fn gpu_test(&mut self, op: u32) -> Option<String> {
148        let ui = &mut self.runtime.ui;
149        match op {
150            1 => ui.gpu_test_lose(),
151            2 => ui.gpu_settle(),
152            3 => {
153                ui.dirty_force_full();
154                ui.invalidate();
155            }
156            _ => {}
157        }
158        let o = ui.gpu_output(0);
159        // (G5e2) the first view of GPU content (gpu_view.rs): what it does now
160        #[cfg(any(windows, target_os = "linux", target_os = "android", target_vendor = "apple", target_arch = "wasm32"))]
161        let v = ui.m.gview.views.first().and_then(|e| ui.gpu_view_info(e.win as u32, e.id)).map(|i| {
162            alloc::format!(" view_device={} view_level={} view_gen={} view_apart={} view_attaches={} view_detaches={} view_frames={} view_reads={} view_read_frame={} view_held={} view_size={}x{}",
163                           i.device as u8, i.level.map_or("none", |l| if l == gpu::Level::Voll { "Voll" } else { "Basis" }), i.generation, i.apart as u8, i.attaches,
164                           i.detaches, i.frames, i.reads, i.read_frame, i.held, i.size.0, i.size.1)
165        }).unwrap_or_default();
166        #[cfg(not(any(windows, target_os = "linux", target_os = "android", target_vendor = "apple", target_arch = "wasm32")))]
167        let v = String::new();
168        Some(alloc::format!(
169            "wanted={} on={} building={} pending={} never={} adapter={} present_mode={} reserved={} frames={} cpu_frames={} upload_bytes={} upload_base={} \
170             layer_textures={} layer_bytes={} layer_store={} split_frames={} image_quads={} image_textures={} image_bytes={} canvas_apart={} canvas_quads={} \
171             canvas_frames={} canvas_tiles={} canvas_bytes={} canvas_fills={} canvas_settles={} canvas_diff_px={} canvas_diff_max={} base_px={} render_us={} present_us={} switches={} leaves={} verified={} verify_bad={} \
172             fb={}x{} dpi={} view_quads={}{v} error={}",
173            o.wanted as u8, o.on as u8, o.building as u8, o.pending as u8, o.never as u8, o.adapter.replace(' ', "_"), o.present_mode, o.reserved, o.frames, o.cpu_frames,
174            o.upload_bytes, o.upload_base, o.layer_textures, o.layer_bytes, o.layer_store, o.split_frames, o.image_quads, o.image_textures, o.image_bytes,
175            o.canvas_apart, o.canvas_quads, o.canvas_frames, o.canvas_tiles, o.canvas_bytes, o.canvas_fills, o.canvas_settles, o.canvas_diff_px, o.canvas_diff_max, o.base_px, o.render_us, o.present_us,
176            o.switches, o.leaves, o.verified, o.verify_bad, ui.wp.rt.fb.w, ui.wp.rt.fb.h, ui.wp.rt.dpi, o.view_quads, o.error.unwrap_or_default().replace(' ', "_")))
177    }
178    fn control(&mut self, line: &[u8]) -> Option<String> {
179        let envelope = agent_core::json::parse(line).ok()?;
180        let session = envelope.get("session").and_then(agent_core::Value::as_str).unwrap_or("page");
181        if session.is_empty() || session.len() > 128 { return None; }
182        if envelope.get("close").and_then(agent_core::Value::as_bool) == Some(true) {
183            self.sessions.remove(session);
184            return None;
185        }
186        let request = envelope.get("request")?;
187        let line = agent_core::json::stringify(request).ok()?;
188        if !self.sessions.contains_key(session) {
189            let Some(protocol) = agent_core::rpc::requested_protocol(line.as_bytes()) else {
190                // Report an invalid envelope without binding an otherwise unused session ID.
191                let mut transient = agent_core::RpcSession::new(agent_core::Protocol::Json, self.runtime.ui.desc.title, "1");
192                return self.runtime.control(&mut transient, line.as_bytes());
193            };
194            if self.sessions.len() >= 16 {
195                let oldest = self.sessions.iter().filter(|(_, (_, rpc))| !rpc.requires_persistent_session())
196                    .min_by_key(|(_, (used, _))| *used).map(|(name, _)| name.clone());
197                if let Some(oldest) = oldest {
198                    self.sessions.remove(&oldest);
199                } else {
200                    // Never evict a negotiated legacy connection. Notifications have no reply;
201                    // requests retain their ID so the caller receives an immediate error.
202                    let id = request.get("id")?.clone();
203                    return agent_core::json::stringify(&agent_core::Value::object([
204                        ("jsonrpc", agent_core::Value::string("2.0")), ("id", id),
205                        ("error", agent_core::Value::object([
206                            ("code", agent_core::Value::i64(-32000)),
207                            ("message", agent_core::Value::string("Browser session limit reached; close an initialized MCP session")),
208                        ])),
209                    ])).ok();
210                }
211            }
212            self.sessions.insert(String::from(session), (self.session_clock, agent_core::RpcSession::new(protocol, self.runtime.ui.desc.title, "1")));
213        }
214        self.session_clock = self.session_clock.wrapping_add(1);
215        let (used, rpc) = self.sessions.get_mut(session).unwrap();
216        *used = self.session_clock;
217        let response = self.runtime.control(rpc, line.as_bytes());
218        self.runtime.ui.present();
219        response
220    }
221}
222
223/// An owned application driven one event/tick at a time. Browser hosts and automation
224/// use the same passes, input handling and software renderer as native windows.
225pub struct Runtime {
226    pub(crate) ui: Ui,
227    pub(crate) app: alloc::boxed::Box<dyn App>,
228    finished: bool,
229}
230impl Runtime {
231    pub(crate) fn is_finished(&self) -> bool { self.finished }
232    /// Initialize a headless surface with the normal command-line/theme configuration.
233    /// `Err(2)` means application initialization or framebuffer allocation failed.
234    pub fn new(desc: &AppDesc, app: impl App + 'static, style: &'static dyn Style) -> Result<Self, i32> {
235        let (mut ui, w, h) = prepare(desc, style);
236        ui.headless = true;
237        ui.wp.rt.fb = Surface::new(ui.dp(w), ui.dp(h), FMT_XRGB);
238        ui.m.touch.on = false; // (a run does not depend on the machine: `pointer touch`)
239        ui.set_time(0);
240        let mut app: alloc::boxed::Box<dyn App> = alloc::boxed::Box::new(app);
241        if ui.wp.rt.fb.px.is_empty() || !app.init(&mut ui) { return Err(2); }
242        ui.run_passes(app.as_mut());
243        ui.render();
244        Ok(Self { ui, app, finished: false })
245    }
246    /// Apply input edges and due timers, then render if required. False means closed.
247    /// Time is milliseconds since initialization; it must advance monotonically.
248    pub fn step(&mut self, now_ms: u32, events: &[Event]) -> bool {
249        if self.finished { return false; }
250        self.ui.set_time(now_ms);
251        for event in events {
252            if !plat::evq_push(*event) {
253                self.ui.process_events(self.app.as_mut());
254                plat::evq_push(*event);
255            }
256        }
257        self.ui.process_events(self.app.as_mut());
258        self.ui.tick_timers();
259        if self.ui.need || self.ui.animating || self.ui.tasks_ready() {
260            self.ui.need = false;
261            self.ui.run_passes(self.app.as_mut());
262        }
263        self.ui.render();
264        if self.ui.quit { self.finish(); }
265        !self.finished
266    }
267    /// Current framebuffer for presentation or screenshot encoding.
268    pub fn frame(&self) -> &Surface { self.ui.fb() }
269    /// Read the runtime context, including dimensions and semantic widgets.
270    pub fn ui(&self) -> &Ui { &self.ui }
271    /// Deliver a registered numeric command through the normal application callback.
272    pub fn command(&mut self, id: u32, arg: u32) -> bool {
273        if self.finished { return false; }
274        let handled = self.app.command(&mut self.ui, id, arg);
275        self.ui.invalidate();
276        handled
277    }
278    /// Close exactly once and deliver the existing application exit callback.
279    pub fn finish(&mut self) {
280        if !self.finished {
281            self.finished = true;
282            self.ui.set_agent_enabled(false);
283            self.app.command(&mut self.ui, UIC_EXIT, 0);
284        }
285    }
286}
287impl Drop for Runtime {
288    fn drop(&mut self) { self.finish(); }
289}
290
291/// the per-window part of the runtime (ui_main_wb0 .. ui_main_wb1): framebuffer, shadow
292/// surface of --verify-dirty, dpi, the pending present
293pub(crate) struct W {
294    pub(crate) fb: Surface,
295    pub(crate) shadow: Surface,
296    pub(crate) dpi: i32,
297    /// rectangles of the last render to present (-1 = everything, 0 = nothing)
298    pub(crate) present_n: i32,
299}
300
301impl W {
302    pub(crate) fn new() -> W {
303        W { fb: Surface::new(0, 0, FMT_XRGB), shadow: Surface::new(0, 0, FMT_XRGB), dpi: 96, present_n: 0 }
304    }
305}
306
307// ------------------------------------------------------------------ command line helpers
308
309/// parse_int (str.s): leading spaces, optional sign, decimal digits -> (value, rest)
310pub(crate) fn parse_int(s: &[u8]) -> (i32, &[u8]) {
311    let mut i = 0;
312    while i < s.len() && s[i] == b' ' {
313        i += 1;
314    }
315    let mut neg = false;
316    if i < s.len() && s[i] == b'-' {
317        neg = true;
318        i += 1;
319    } else if i < s.len() && s[i] == b'+' {
320        i += 1;
321    }
322    let mut v: i32 = 0;
323    while i < s.len() && s[i].is_ascii_digit() {
324        v = v.wrapping_mul(10).wrapping_add((s[i] - b'0') as i32);
325        i += 1;
326    }
327    (if neg { v.wrapping_neg() } else { v }, &s[i..])
328}
329
330/// parse_hex (str.s): optional "#" or "0x", hex digits -> (value, rest)
331pub(crate) fn parse_hex(s: &[u8]) -> (u32, &[u8]) {
332    let mut i = 0;
333    if !s.is_empty() && s[0] == b'#' {
334        i = 1;
335    } else if s.len() >= 2 && s[0] == b'0' && s[1] | 32 == b'x' {
336        i = 2;
337    }
338    let mut v: u32 = 0;
339    while i < s.len() {
340        let d = match s[i] {
341            c @ b'0'..=b'9' => c - b'0',
342            c => {
343                let l = c | 32;
344                if (b'a'..=b'f').contains(&l) { l - b'a' + 10 } else { break }
345            }
346        };
347        v = v << 4 | d as u32;
348        i += 1;
349    }
350    (v, &s[i..])
351}
352
353// the command line (argument 0 is the program)
354struct Args(Vec<String>);
355
356impl Args {
357    fn value(&self, name: &str) -> Option<&str> {
358        let i = self.0.iter().skip(1).position(|a| a == name)? + 1;
359        self.0.get(i + 1).map(|s| s.as_str())
360    }
361    fn flag(&self, name: &str) -> bool {
362        self.0.iter().skip(1).any(|a| a == name)
363    }
364    // arg_int: parse_int of the value, the default when the option is missing
365    fn int(&self, name: &str, def: i32) -> i32 {
366        self.value(name).map_or(def, |v| parse_int(v.as_bytes()).0)
367    }
368}
369
370/// scale_to_dpi(percent): percent clamped to 50..400 -> (p * 96 + 50) / 100
371pub(crate) fn scale_to_dpi(p: i32) -> i32 {
372    (p.clamp(50, 400) * 96 + 50) / 100
373}
374
375impl Ui {
376    pub(crate) fn tick_timers(&mut self) {
377        loop {
378            let (id, _) = self.timer_due();
379            if id == u32::MAX { break; }
380            self.cmd_run(UIC_TIMER, id);
381            self.need = true;
382        }
383    }
384    /// ui_init (+ gfx_init, font_init): the context of a fresh process
385    pub(crate) fn new(desc: &AppDesc, style: &'static dyn Style) -> Ui {
386        let mut ui = Ui {
387            agent: Default::default(),
388            desc: *desc,
389            style,
390            text: Text::new(style.fonts(), style.font_names()),
391            dc: Dc::new(0, 0),
392            wp: WinParts::new(),
393            m: Default::default(),
394            sems: Vec::new(),
395            semtext_len: 0,
396            sem_dummy: Default::default(),
397            keydown: [0; 256],
398            mpts: Vec::new(),
399            mprs: Vec::new(),
400            mtilt: [0; 2],
401            mpress: 0,
402            mptype: 0,
403            mpts_win: 0,
404            cmdq: alloc::collections::VecDeque::new(),
405            text_more: Vec::new(),
406            timers: Vec::new(),
407            limits: [0; super::ctx::LIM_N],
408            mods: 0,
409            frame: 0,
410            form: FF_DESKTOP,
411            now_ms: 0,
412            repass: false,
413            animating: false,
414            anim_off: false,
415            rtl: false,
416            rtl_mode: 0,
417            app_focus: 0,
418            app_active: false,
419            theme_sys: false,
420            theme_base_dark: false,
421            contrast_mode: 0,
422            high_contrast: false,
423            hc_sys: None,
424            accent_sys: false,
425            anim_forced: false,
426            anim_sys: false,
427            text_scale_forced: false,
428            text_scale: 100,
429            dbl_ms: 500,
430            font_prefs: (None, None, None),
431            tb_sys: plat::Titlebar::default(),
432            blink_ms: 530,
433            warns: 0,
434            ui_debug: plat::getenv("jfgui_DEBUG").is_some(),
435            dup_check: plat::getenv("jfgui_DEBUG").is_some(),
436            dup_warned: alloc::collections::BTreeSet::new(),
437            ids: Default::default(),
438            focus_req: 0,
439            focus_req_win: 0,
440            focus_req_old: 0,
441            focus_req_frame: 0,
442            uiw_cur: 0,
443            theme: [0; super::theme::THEME_N],
444            theme_dark: false,
445            accent: 0,
446            look: super::theme::Look::new(style),
447            title: String::new(),
448            t0_us: 0,
449            headless: false,
450            need: false,
451            need_render: false,
452            quit: false,
453            log_events: false,
454            open_reqs: Vec::new(),
455            autoclose_ms: 0,
456            accent_raw: 0,
457            settle_warned: false,
458            self_frames: 0,
459            self_warned: false,
460            verify_dirty: false,
461            frames_rendered: 0,
462            close_at: 0,
463            drects: Vec::new(),
464            dmg: Vec::new(),
465            cvs: Vec::new(),
466            icon_hook: None,
467            uiinput: UiInput::default(),
468            sc: Default::default(),
469            ucur: Default::default(),
470            dr: Default::default(),
471            dnd: Default::default(),
472            tasks: plat::task::Executor::ui(),
473        };
474        for (font, raster) in style.font_raster().iter().enumerate() {
475            ui.text.set_font_raster(font as u32, *raster);
476        }
477        ui.uiw_init();
478        ui
479    }
480
481    // ------------------------------------------------------------------ queries
482
483    /// ui_fb: the live window's framebuffer.  While its frames go through the GPU a big image may be
484    /// drawn only there, the view of a canvas while it moves (canvas.rs) too: Ui::gpu_settle() first
485    /// when the pixels matter
486    pub fn fb(&self) -> &Surface {
487        &self.wp.rt.fb
488    }
489
490    /// ui_client_size: client area in DIP
491    pub fn client_size(&self) -> (i32, i32) {
492        (self.px2dip(self.wp.rt.fb.w), self.px2dip(self.wp.rt.fb.h))
493    }
494
495    /// g_anim_off: animations off (--anim off, the settings)  // pn-core
496    pub fn anim_off(&self) -> bool {
497        self.anim_off
498    }
499    pub fn set_anim_off(&mut self, off: bool) {
500        self.anim_off = off;
501    }
502
503    /// ui_headless: running without a window (--shot / --script)
504    pub fn headless(&self) -> bool {
505        self.headless
506    }
507
508    /// ui_invalidate: request a new frame (state changed outside the callbacks)
509    pub fn invalidate(&mut self) {
510        self.need = true;
511    }
512
513    /// ui_quit: leave the main loop after this frame
514    pub fn quit(&mut self) {
515        self.quit = true;
516    }
517
518    /// ui_set_title(s): change the window title (kept for title() / title bars; 255 bytes)
519    pub fn set_title(&mut self, s: &str) {
520        let mut n = s.len().min(255);
521        while !s.is_char_boundary(n) {
522            n -= 1;
523        }
524        self.title = String::from(&s[..n]);
525        if !self.headless {
526            plat::set_title(&self.title);
527        }
528    }
529
530    /// ui_title: the window title (the AppDesc title until set_title)
531    pub fn title(&self) -> &str {
532        if self.title.is_empty() { self.desc.title } else { &self.title }
533    }
534
535    /// the framebuffer as the platform presents it
536    pub(crate) fn frame_of(sf: &Surface) -> plat::Frame<'_> {
537        plat::Frame { px: &sf.px, w: sf.w, h: sf.h, stride: sf.stride }
538    }
539
540    /// png_save(path, sf): write a surface as a PNG file -> written (--shot and the scripts'
541    /// shots: trusted, outside the app's permissions)
542    pub(crate) fn save_png(sf: &Surface, path: &str) -> bool {
543        let png = img_png::encode(&sf.px, sf.w as usize, sf.h as usize, sf.stride as usize, false);
544        super::perm::trusted(|| plat::write_file(path, &png))
545    }
546
547    // call_cmd(id, arg) -> App::command
548    fn call_cmd(&mut self, app: &mut dyn App, id: u32, arg: u32) -> bool {
549        app.command(self, id, arg)
550    }
551
552    // ------------------------------------------------------------------ events
553
554    /// ui_process_events: apply queued platform events; run passes for edges.  Every
555    /// event belongs to a window (Event::win); input goes to the window uiw_input_target
556    /// picks, which is made live for input_event.
557    pub(crate) fn process_events(&mut self, app: &mut dyn App) {
558        self.process_events_deferred(app, true);
559    }
560
561    // Browser batches retain every input edge and its app passes, but rasterize only
562    // after the batch. Routing/capture changes must take effect before the next edge.
563    pub(crate) fn process_events_deferred(&mut self, app: &mut dyn App, render: bool) {
564        let mut text_pending = false;
565        let mut text_win: i32 = -1;
566        self.touch_tick(); // the resting finger, the hover after it (touch.rs)
567        self.uiw_use(0);
568        loop {
569            // what the gesture recogniser sends to the widgets goes first: it belongs to the raw
570            // finger event before the next one (and has the UI's window already)
571            let made = self.touch_next();
572            let synth = made.is_some();
573            let Some(mut ev) = made.or_else(plat::evq_pop) else { break };
574            self.self_frames = 0; // an event: the frames after it are not ones the app asked for alone
575            if !synth && self.hr_on() {
576                self.hr_popped(&ev); // (hotreload.rs) replayed or recorded
577            }
578            if !self.headless && !synth {
579                let w = self.uiw_from_pw(ev.win as i32);
580                if w < 0 {
581                    ev_dropped(&ev);
582                    continue; // a window the UI does not know (any more)
583                }
584                ev.win = w as u8;
585            }
586            if self.log_events {
587                log_event(&ev);
588            }
589            let win = ev.win as u32;
590            // lifecycle: which windows have the keyboard (UIC_ACTIVE after the batch)
591            match ev.typ {
592                plat::E_FOCUS_IN => self.app_focus |= 1 << (win & 31),
593                plat::E_FOCUS_OUT => self.app_focus &= !(1 << (win & 31)),
594                _ => {}
595            }
596            match ev.typ {
597                plat::E_POWER => {
598                    // 1 sleep, 0 woke up; the browser / Android: 2 hidden / stopped (the app may
599                    // be ended without a word: UIC_SUSPEND 2), 3 visible / started again
600                    let (arg, what) = match ev.data {
601                        0 => (0, "the system woke up"),
602                        2 => (2, "the app went to the background"),
603                        3 => (0, "the app is in the foreground again"),
604                        _ => (1, "the system goes to sleep"),
605                    };
606                    plat::log_info("app", what);
607                    plat::sensors::suspend(arg != 0); // the sensors and the location stop with it
608                    self.sensors_poll();
609                    self.call_cmd(app, UIC_SUSPEND, arg);
610                    self.run_passes(app); // an edge: its own frame
611                    self.need_render = true;
612                    continue;
613                }
614                plat::E_SESSION_END => {
615                    plat::log_info("app", "the session ends");
616                    self.call_cmd(app, UIC_SESSION_END, 0);
617                    self.quit = true; // UIC_EXIT follows
618                    self.run_passes(app);
619                    self.need_render = true;
620                    continue;
621                }
622                plat::E_SETTINGS => {
623                    self.settings_changed(app, ev.data);
624                    self.run_passes(app);
625                    self.need_render = true;
626                    continue;
627                }
628                plat::E_CLOSE => {
629                    if win != 0 {
630                        self.uiw_close_request(win); // App::command(UIC_WINCLOSE, id)
631                    } else if !self.call_cmd(app, UIC_CLOSE, 0) {
632                        self.quit = true; // the app keeps the window open when handled
633                    } else {
634                        // kept open: what the app opened ("save changes?") shows in the next
635                        // frame, not only after the next input or timer
636                        self.need = true;
637                    }
638                    continue;
639                }
640                plat::E_RESIZE => {
641                    self.uiw_resize(app, win, ev.x, ev.y);
642                    continue;
643                }
644                plat::E_DPI => {
645                    self.uiw_dpi(win, ev.data as i32);
646                    continue;
647                }
648                plat::E_PAINT => continue,
649                plat::E_WAKE => {
650                    self.tasks_wake(); // wake-ups of tasks from other threads (tasks.rs)
651                    self.cmd_run(UIC_WAKE, 0); // plat::wake() from another thread
652                    self.open_poll();
653                    self.tray_poll();
654                    self.screen_poll(); // global shortcuts, picks, pictures (screen.rs)
655                    self.sensors_poll(); // sensor readings, location fixes (sensors.rs)
656                    self.recent_poll(); // the browser's answer to a reopen (recent.rs)
657                    self.a11y_poll(); // actions of screen readers (a11y.rs)
658                    self.spell_poll(); // suggestions made on a worker (edit_spell.rs)
659                    self.need = true;
660                    continue;
661                }
662                plat::E_FOCUS_IN => {
663                    self.uiw_focus_in(win);
664                    continue;
665                }
666                plat::E_WINPOS => {
667                    self.uiw_winpos(win, ev.x, ev.y);
668                    continue;
669                }
670                plat::E_DROP => {
671                    // (K1) the dropped content: onto a drop target (DS_DROP), else UIC_DROP
672                    if !self.drop_receive(&ev) {
673                        let id = self.uiw_rec_id(win);
674                        self.cmd_run(UIC_DROP, id);
675                    }
676                    self.run_passes(app);
677                    self.dnd_drop_settled(); // its answer to the source, unless the app deferred it
678                    self.need_render = true;
679                    continue;
680                }
681                plat::E_DRAG => {
682                    self.dnd_hover(&ev); // a drag over the window: the targets highlight
683                    self.run_passes(app);
684                    self.need_render = true;
685                    continue;
686                }
687                plat::E_DRAG_END => {
688                    self.dnd_out_end(ev.data); // the platform finished the app's drag
689                    self.run_passes(app);
690                    self.need_render = true;
691                    continue;
692                }
693                _ => {}
694            }
695            if !synth {
696                self.touch_kind(&ev); // a finger or a mouse: the scroll bars follow
697                if self.touch_takes(&ev) {
698                    self.touch_input(&ev); // the finger goes through the recogniser (touch.rs)
699                    continue;
700                }
701            }
702            // (inspect.rs) the inspector's hot keys and its pick take their events first
703            if self.m.ins.armed {
704                let r = self.ins_event(&ev);
705                if r != 0 {
706                    if text_pending {
707                        text_pending = false;
708                        self.run_passes(app);
709                    }
710                    if r == 1 {
711                        self.run_passes(app);
712                    }
713                    self.need_render = true;
714                    continue;
715                }
716            }
717            let t = self.uiw_input_target(&ev);
718            if t < 0 {
719                ev_dropped(&ev);
720                continue; // a window that is gone
721            }
722            // a run of text ends before any other input (and before text for another window)
723            if text_pending && (ev.typ != plat::E_TEXT || t != text_win) {
724                text_pending = false;
725                self.run_passes(app);
726                self.need_render = true;
727            }
728            self.uiw_use(t as u32);
729            if !synth && self.hr_on() {
730                self.hr_input(&ev, t as u32); // (hotreload.rs) recorded or checked
731            }
732            match self.input_event(&ev) {
733                2 => {
734                    text_pending = true;
735                    text_win = t;
736                }
737                0 => {}
738                _ => {
739                    self.run_passes(app); // an edge gets its own frame
740                    self.need_render = true;
741                }
742            }
743        }
744        if text_pending {
745            self.run_passes(app);
746            self.need_render = true;
747        }
748        // the app as a whole got or lost the keyboard (focus moving between its windows
749        // within one batch does not count; hot reload: recorded, not while replaying)
750        let active = self.app_focus != 0;
751        if active != self.app_active && self.hr_active(active) {
752            self.app_active = active;
753            if !active {
754                // (ยง6) like the system's menus: the popups of every window close (they may
755                // stand outside the windows, over other programs)
756                self.uiw_popups_dismiss(WIN_MAX as u32);
757            }
758            self.call_cmd(app, UIC_ACTIVE, active as u32);
759            self.run_passes(app);
760            self.need_render = true;
761        }
762        if self.uiw_moved_passes() {
763            self.run_passes(app);
764            self.need_render = true;
765        }
766        if self.hr_on() {
767            self.hr_batch_end(); // (hotreload.rs) the batch's records written
768        }
769        // the finger's way since the last frame: at most one frame for all its samples
770        if render && self.touch_moved() {
771            self.run_passes(app);
772            self.need_render = true;
773        }
774        if render && self.need_render {
775            self.need_render = false;
776            // headless: render only to verify, or for a script's `gpu on`
777            if !self.headless || self.verify_dirty || self.gp_scripted() {
778                self.render();
779                self.present();
780            }
781        }
782        self.uiw_use(0);
783    }
784
785    // E_SETTINGS: follow the system's theme and format, then tell the app (UIC_SETTINGS)
786    fn settings_changed(&mut self, app: &mut dyn App, bits: u32) {
787        plat::log!(plat::LOG_INFO, "app", "system settings changed ({bits:#x})");
788        if bits & (plat::SET_THEME | plat::SET_CONTRAST | plat::SET_FONTS | plat::SET_OTHER) != 0 {
789            let before = (self.theme_dark, self.high_contrast);
790            let s = if self.headless { plat::sys_settings_env() } else { plat::sys_settings() };
791            if bits & plat::SET_THEME != 0 && self.theme_sys && s.dark.is_none() {
792                self.theme_set(2); // no answer from the settings: plat::theme
793            }
794            self.sys_apply(s);
795            if before != (self.theme_dark, self.high_contrast) {
796                self.call_cmd(app, UIC_THEME, 0);
797            }
798        }
799        if bits & plat::SET_LOCALE != 0 && !self.headless {
800            crate::i18n::use_system();
801            self.set_decimal_sep(crate::i18n::locale().decimal() as u32);
802        }
803        self.call_cmd(app, UIC_SETTINGS, bits);
804        self.dirty_force_full();
805        self.need = true;
806    }
807
808    /// ui_refresh: process pending events and redraw now (live resize callback)
809    pub(crate) fn refresh(&mut self, app: &mut dyn App) {
810        self.clock_update();
811        self.process_events(app);
812        self.run_passes(app);
813        self.render();
814        self.present();
815    }
816
817    /// ui_resize(w_px, h_px): a new framebuffer size of the live window
818    pub(crate) fn resize(&mut self, app: &mut dyn App, w: i32, h: i32) {
819        if w <= 0 || h <= 0 || (w == self.wp.rt.fb.w && h == self.wp.rt.fb.h) {
820            return;
821        }
822        self.wp.rt.fb.resize(w, h);
823        self.dirty_force_full();
824        self.need = true;
825        if self.headless {
826            self.run_passes(app);
827        }
828    }
829
830    // ------------------------------------------------------------------ passes
831
832    /// ui_run_passes: App::frame until the layout settles, then deliver commands.  A pass
833    /// starts in the main window; window_begin / window_end inside App::frame build the
834    /// other windows in the same pass.
835    pub(crate) fn run_passes(&mut self, app: &mut dyn App) {
836        let (mut passes, mut build_us) = (0u32, 0u64); // (inspect.rs) statistics
837        self.touch_apply(); // the way of the finger and of a fling, into the views (touch.rs)
838        let mut unseen = self.settle(app, &mut passes, &mut build_us);
839        // typed text beyond a pass's queue (ctx.rs): part by part, each with passes of its own
840        while self.text_refill() {
841            unseen = self.settle(app, &mut passes, &mut build_us);
842        }
843        if self.a11y_reveal() {
844            unseen = self.settle(app, &mut passes, &mut build_us);
845            self.agent.reveal = None;
846        }
847        self.touch_after_passes();
848        self.dnd_after_passes(); // a DS_START without drag_set is no drag
849        self.focus_req_expire();
850        self.need_after_passes(unseen);
851        self.a11y_frame(); // the accessibility tree for screen readers (a11y.rs)
852        if self.m.ins.on {
853            self.ins_passes(passes, build_us);
854        } else if self.m.ins.gone {
855            self.ins_gone(); // the inspector closed: its window is freed (inspect.rs)
856        }
857    }
858
859    // need_after_passes(unseen): need accumulates over the passes -- what asked for a frame after
860    // the last round's frame (invalidate in its App::frame or a command, a window activated) and
861    // what the last round's commands, windows or tasks changed when the rounds ran out (unseen: no
862    // frame saw it) get the next frame (it was `need = tasks_ran`, which lost the frame of a
863    // command such as UIC_WAKE in the last round).  An app that asks again after every frame by
864    // itself gets SELF_FRAMES of them in a row without an event between them, then the frames
865    // wait for the next event (a UIWARN once); tasks keep theirs as before.
866    fn need_after_passes(&mut self, unseen: (bool, bool)) {
867        const SELF_FRAMES: u32 = 8;
868        let (tasks_ran, unseen) = unseen;
869        let asked = self.need || unseen;
870        self.self_frames = if asked { self.self_frames.saturating_add(1) } else { 0 };
871        if asked && self.self_frames > SELF_FRAMES && !tasks_ran {
872            if !self.self_warned {
873                self.self_warned = true;
874                self.warn("every frame asked for another one (ui.invalidate in App::frame, a command queued in every frame): the frames wait for the next event");
875            }
876            self.need = false;
877        } else {
878            self.need = asked;
879        }
880    }
881
882    // settle -> (async tasks ran in the last round, the last round changed something no frame
883    // saw: the rounds ran out): passes until the layout settles, the queued commands, at most
884    // three rounds.  Each round's frame answers what asked for a frame before it (need).
885    fn settle(&mut self, app: &mut dyn App, passes: &mut u32, build_us: &mut u64) -> (bool, bool) {
886        let mut rounds = 3;
887        let mut tasks_ran;
888        loop {
889            self.need = false;
890            let mut pass = 0;
891            let mut keys_fwd = false; // (U3) keys handed on for the next pass
892            loop {
893                self.uiw_use(0);
894                self.frame_begin();
895                let (w, h) = (self.wp.rt.fb.w, self.wp.rt.fb.h);
896                self.uiw_pass_begin(w, h);
897                let csd = self.csd_begin();
898                let t0 = if self.m.ins.on { plat::time_us() } else { 0 };
899                app.frame(self);
900                if t0 != 0 {
901                    *build_us += plat::time_us().saturating_sub(t0);
902                }
903                self.uiw_unwind(); // windows the app did not end
904                self.ids_unwind(); // id scopes it did not close
905                self.perm_dialog(); // an open permission question (perm.rs)
906                // frameless windows (own or library title bar): resize grips on top
907                if csd || self.desc.flags & AF_CUSTOM_TITLEBAR != 0 {
908                    self.edge_grips();
909                }
910                if csd {
911                    self.box_end();
912                    self.box_end();
913                }
914                self.uiw_pass_end();
915                if self.m.ins.on {
916                    // the inspector's tool window, after the main window's pass (with the
917                    // menus and tool tips of its end): inspect.rs
918                    self.inspect_frame();
919                }
920                let mut again = self.frame_end();
921                if pass == 0 {
922                    self.uiw_clear_edges();
923                    // (U3) keys a tool window borrowed but did not use: its owner's input
924                    // for pass 2
925                    keys_fwd = self.uiw_fwd_inject();
926                    again |= keys_fwd;
927                } else if keys_fwd {
928                    keys_fwd = false;
929                    self.uiw_clear_edges(); // the handed-on keys were seen once
930                }
931                pass += 1;
932                *passes += 1;
933                if !again {
934                    break;
935                }
936                if pass >= MAX_PASSES {
937                    if !self.settle_warned {
938                        self.settle_warned = true;
939                        self.warn("layout did not settle within 4 passes");
940                    }
941                    break;
942                }
943            }
944            // windows not built in this frame disappear; sizes that follow the content
945            let changed = self.uiw_settle();
946            // deliver queued commands; if any ran, build the frame again
947            let mut ran = false;
948            while let Some((id, arg)) = self.cmd_take() {
949                ran = true;
950                // the inspector's window: its close button, Esc and drops are its own
951                if arg == super::inspect::INSPECT_WIN && self.ins_cmd(id) {
952                    continue;
953                }
954                let r = self.call_cmd(app, id, arg);
955                self.uiw_cmd_done(id, arg, r); // UIC_WINCLOSE not handled: close it
956                if !r && id == UIC_CLOSE {
957                    self.quit = true;
958                }
959            }
960            // async tasks woken meanwhile (tasks.rs): what they changed shows in the next pass
961            tasks_ran = self.tasks_pump();
962            if !(ran || changed || tasks_ran) {
963                return (false, false);
964            }
965            rounds -= 1;
966            if rounds == 0 {
967                return (tasks_ran, true);
968            }
969        }
970    }
971
972    // csd_begin -> true when the library draws the title bar for the app: the window has no
973    // server-side decorations (WST_CSD), the app has no AF_CUSTOM_TITLEBAR and the window is
974    // not in full screen.  Then the
975    // frame is a column: title bar, the app's area (two boxes to close after App::frame).
976    // jfgui_CSD=1 forces it (tests, headless shots).
977    fn csd_begin(&mut self) -> bool {
978        // (L7) full screen: no title bar
979        if self.desc.flags & AF_CUSTOM_TITLEBAR != 0 || self.m.win.recs[0].fs {
980            return false;
981        }
982        let forced = plat::getenv("jfgui_CSD").is_some_and(|v| v.as_bytes().first() == Some(&b'1'));
983        if !forced && (self.headless || plat::window_state() & plat::WST_CSD == 0) {
984            return false;
985        }
986        self.next_size(-1, -1);
987        self.box_begin(0xFFFF_7420, BOX_V | BOX_FIXED, 0);
988        let t = String::from(self.title());
989        self.titlebar_begin(&t, 0);
990        self.titlebar_end();
991        self.next_size(-1, -1);
992        self.box_begin(0xFFFF_7421, BOX_V | BOX_FIXED, 0);
993        true
994    }
995
996    // ------------------------------------------------------------------ rendering
997
998    /// ui_render: replay the recorded commands of every shown window into its framebuffer,
999    /// only inside the dirty rectangles (everything on the first frame / after a resize),
1000    /// and present each window right away
1001    pub(crate) fn render(&mut self) {
1002        if !self.uiw_any_used() {
1003            self.render_live(); // the main window only
1004            self.uiw_popups_render(0); // (ยง6) its popups outside the window
1005            self.present();
1006            return;
1007        }
1008        for i in 0..WIN_MAX as u32 {
1009            if !self.uiw_drawable(i) {
1010                self.uiw_popups_drop(i); // (U3) not drawn: no popup windows
1011                continue;
1012            }
1013            self.uiw_use(i);
1014            if self.uiw_take_newshow(i) {
1015                self.dirty_force_full(); // a window shown again: everything
1016            }
1017            self.render_live();
1018            self.uiw_popups_render(i); // (U3) popups outside the window
1019            self.present();
1020        }
1021        self.uiw_use(0);
1022        self.uiw_sync_show(); // new windows appear with their content
1023    }
1024
1025    // render_live: ui_render for the live window
1026    fn render_live(&mut self) {
1027        self.wp.rt.present_n = 0;
1028        if self.wp.rt.fb.px.is_empty() {
1029            return; // no framebuffer (allocation failed)
1030        }
1031        let t0 = plat::time_us();
1032        let (w, h) = (self.wp.rt.fb.w, self.wp.rt.fb.h);
1033        self.wp.dirty.split = self.gp_split_wanted(); // (the GPU output keeps layers apart: gpu_out.rs)
1034        self.cmd_mirror(); // right-to-left: the recorded commands across the window
1035        let mut n = self.dirty_compute(w, h);
1036        let ins = self.m.ins.on;
1037        if ins {
1038            n = self.ins_render_begin(n); // the inspector's overlay (inspect.rs)
1039        }
1040        self.wp.rt.present_n = self.scopy_present(n); // + the cells a scroll copy moved (scrollcopy.rs)
1041        if self.ui_debug {
1042            plat::write_stderr(alloc::format!("render: {} dirty rects (-1 = full)\n", n).as_bytes());
1043        }
1044        let split = self.gp_split_begin(n);
1045        if self.wp.rt.present_n == 0 {
1046            self.gp_frame_done(t0, 0, false);
1047            return; // nothing changed
1048        }
1049        let dirty_n = n;
1050        self.dc.begin(&self.wp.rt.fb);
1051        // the text font the passes left set stays set (the replay sets each text's font,
1052        // custom draws their own; what stays must not depend on the dirty part or on threads)
1053        let font = self.text.get_font();
1054        if split {
1055            self.gp_split_draw(n); // layer by layer, what lies below stays (gpu_out.rs)
1056            n = 0;
1057        } else if self.render_par(n) {
1058            n = 0; // drawn on several threads (render_par.rs)
1059        }
1060        if n < 0 {
1061            self.render_all_to(false);
1062        } else {
1063            for i in 0..n as usize {
1064                let r = self.drects[i];
1065                if self.ui_debug {
1066                    plat::write_stderr(alloc::format!("  dirty {},{} {}x{}\n", r[0], r[1], r[2], r[3]).as_bytes());
1067                }
1068                self.dc.clip_push(r[0], r[1], r[2], r[3]);
1069                self.render_all_to(false);
1070                self.dc.clip_pop();
1071            }
1072        }
1073        self.dc.end();
1074        self.scopy_done(); // the frame's cost (scrollcopy.rs)
1075        if ins {
1076            self.ins_render_end(); // the inspector's overlay on top (inspect.rs)
1077        }
1078        self.frames_rendered = self.frames_rendered.wrapping_add(1);
1079        if self.verify_dirty {
1080            self.verify_dirty();
1081        }
1082        self.text.set_font(font.0, font.1);
1083        self.gp_frame_done(t0, dirty_n, split);
1084    }
1085
1086    /// ui_present: show what render changed in the live window
1087    pub(crate) fn present(&mut self) {
1088        let n = self.wp.rt.present_n;
1089        if n == 0 {
1090            return;
1091        }
1092        self.uiw_present(n);
1093        self.wp.rt.present_n = 0;
1094    }
1095
1096    // verify_dirty: render the whole frame into a shadow surface and compare
1097    // (--verify-dirty): any difference means a command's box was too small (auto.rs' bench
1098    // calls it after the timed render)
1099    pub(crate) fn verify_dirty(&mut self) {
1100        self.gp_settle_all(); // (the GPU output's images: the framebuffer's pixels are compared)
1101        let (w, h) = (self.wp.rt.fb.w, self.wp.rt.fb.h);
1102        if self.wp.rt.shadow.w != w || self.wp.rt.shadow.h != h {
1103            self.wp.rt.shadow = Surface::new(w, h, FMT_XRGB);
1104        }
1105        self.dc.begin(&self.wp.rt.shadow);
1106        self.render_all_to(true);
1107        self.dc.end();
1108        if self.m.ins.on {
1109            self.ins_verify(true); // the inspector's overlay belongs to the frame (inspect.rs)
1110        }
1111        // compare the RGB of every pixel
1112        let (mut n, mut x0, mut y0, mut x1, mut y1) = (0u32, i32::MAX, i32::MAX, -1i32, -1i32);
1113        {
1114            let (a, b) = (&self.wp.rt.shadow, &self.wp.rt.fb);
1115            for y in 0..a.h {
1116                let ra = y as usize * a.stride as usize;
1117                let rb = y as usize * b.stride as usize;
1118                for x in 0..a.w {
1119                    let i = x as usize * 4;
1120                    if a.px[ra + i..ra + i + 3] != b.px[rb + i..rb + i + 3] {
1121                        n += 1;
1122                        x0 = x0.min(x);
1123                        y0 = y0.min(y);
1124                        x1 = x1.max(x);
1125                        y1 = y; // rows run downwards: last = max
1126                    }
1127                }
1128            }
1129        }
1130        if n == 0 {
1131            return;
1132        }
1133        self.warn(&alloc::format!("dirty regions missed {} pixels", n));
1134        let s = alloc::format!("  in px {},{} .. {},{} (window {})\n", x0, y0, x1, y1, self.uiw_cur);
1135        plat::write_stderr(s.as_bytes());
1136        // repair the frame so later frames stay comparable
1137        self.dc.begin(&self.wp.rt.fb);
1138        self.render_all_to(false);
1139        self.dc.end();
1140        if self.m.ins.on {
1141            self.ins_verify(false);
1142        }
1143        self.gp_repaired(); // (the GPU output: the framebuffer goes up as a whole)
1144        self.wp.rt.present_n = -1;
1145    }
1146
1147    // ------------------------------------------------------------------ clock
1148
1149    // clock_update: now_ms from the platform clock (window mode)
1150    fn clock_update(&mut self) {
1151        let ms = (plat::time_us().wrapping_sub(self.t0_us) / 1000) as u32;
1152        self.set_time(ms);
1153    }
1154
1155    // ui_timer_due_peek -> earliest timer deadline (ms; "now" while one has expired) or -1
1156    fn timer_due_peek(&self) -> u32 {
1157        match self.timer_scan() {
1158            (Some(_), _) => self.now_ms,
1159            (None, next) => next,
1160        }
1161    }
1162}
1163
1164// ev_dropped: an event that goes to no window; a commit's text leaves the store with it (the
1165// store keeps a few: plat::ime_commit_set)
1166fn ev_dropped(ev: &Event) {
1167    if ev.typ == plat::E_IME_COMMIT {
1168        let _ = plat::ime_commit_take(ev.data);
1169    }
1170    // a drop onto a window that is gone: its content goes, its source learns nobody took it
1171    if ev.typ == plat::E_DROP {
1172        let _ = plat::drop_data_take();
1173        plat::drop_finish(plat::drop_waiting(), 0);
1174    }
1175}
1176
1177// log_event: print an event record
1178fn log_event(ev: &Event) {
1179    let s = alloc::format!(
1180        "event type={} btn={} mods={:x} x={} y={} data={:x} win={}\n",
1181        ev.typ, ev.btn, ev.mods, ev.x, ev.y, ev.data, ev.win
1182    );
1183    plat::write_stdout(s.as_bytes());
1184}
1185
1186/// jfgui_main of an app: the runtime for one AppDesc + App -> exit code
1187fn prepare(desc: &AppDesc, style: &'static dyn Style) -> (Ui, i32, i32) {
1188    // the image codecs: file access through plat, the core formats registered (the asm
1189    // links PNG / JPEG / BMP / GIF statically)
1190    img_core::io::set_file_io(img_core::io::FileIo { read: plat::read_file, write: plat::write_file });
1191    img_all::use_core_codecs();
1192    img_svg::register(); // (svg) .svg / .svgz through img_core::io::load / load_ex
1193    plat::set_print_deflate(Some(img_core::deflate::zlib_deflate)); // PDFs of plat::print as the asm's
1194    img_core::set_parallel(icc_par); // the codecs' parallel parts (YUV bands, ICC grids) on the pool
1195    gfx::set_parallel(icc_par); // large blits (a canvas's frame at 4K) in row bands on the pool
1196    let args = Args(plat::args());
1197    let mut ui = Ui::new(desc, style);
1198    // the decimal sign of number boxes: the user's format (headless: German)
1199    ui.set_decimal_sep(crate::i18n::locale().decimal() as u32);
1200    // ---- options
1201    let (mut w, mut h) = (desc.w as i32, desc.h as i32);
1202    if let Some(v) = args.value("--size") {
1203        // arg_size: "WxH", both > 0
1204        let (a, r) = parse_int(v.as_bytes());
1205        if !r.is_empty() && r[0] | 32 == b'x' {
1206            let (b, _) = parse_int(&r[1..]);
1207            if a > 0 && b > 0 {
1208                w = a;
1209                h = b;
1210            }
1211        }
1212    }
1213    ui.wp.rt.dpi = scale_to_dpi(args.int("--scale", 100));
1214    ui.log_events = args.flag("--log-events");
1215    ui.verify_dirty = args.flag("--verify-dirty");
1216    ui.autoclose_ms = args.int("--auto-close", 0) as u32;
1217    ui.inspect_args(args.flag("--inspect")); // the live inspector (inspect.rs)
1218    if let Some(a) = args.value("--anim") {
1219        ui.anim_off = a == "off";
1220        ui.anim_forced = true;
1221    }
1222    // high contrast and text scale: --contrast high|normal, --text-scale PERCENT
1223    match args.value("--contrast") {
1224        Some("high") => ui.contrast_mode = 1,
1225        Some("normal") => ui.contrast_mode = 2,
1226        _ => {}
1227    }
1228    if let Some(v) = args.value("--text-scale") {
1229        if let Ok(p) = v.parse::<u32>() {
1230            ui.text_scale = p.clamp(50, 300);
1231            ui.text_scale_forced = true;
1232        }
1233    }
1234    // right-to-left: --rtl / --ltr, else the UI language
1235    if args.flag("--rtl") {
1236        ui.set_rtl(Some(true));
1237    } else if args.flag("--ltr") {
1238        ui.set_rtl(Some(false));
1239    }
1240    // theme: --theme, else the app flags, else the system setting
1241    let mut mode = 2;
1242    if desc.flags & AF_DARK != 0 {
1243        mode = 1;
1244    }
1245    if desc.flags & AF_LIGHT != 0 {
1246        mode = 0;
1247    }
1248    if let Some(t) = args.value("--theme") {
1249        if t == "dark" {
1250            mode = 1;
1251        }
1252        if t == "light" {
1253            mode = 0;
1254        }
1255    }
1256    ui.accent_raw = desc.accent & 0xFF_FFFF;
1257    if let Some(a) = args.value("--accent") {
1258        ui.accent_raw = parse_hex(a.as_bytes()).0 & 0xFF_FFFF;
1259    }
1260    let acc = ui.accent_raw;
1261    ui.accent_set(acc);
1262    ui.accent_sys = acc == 0; // no accent of the app: the system's
1263    ui.theme_set(mode);
1264    (ui, w, h)
1265}
1266
1267// --theme-file PATH: an app theme from the command line, after App::init (it wins over the
1268// app's own); an error is printed and the app's theme stays
1269fn cli_theme(ui: &mut Ui) {
1270    let args = Args(plat::args());
1271    if let Some(f) = args.value("--theme-file") {
1272        if let Err(e) = super::perm::trusted(|| ui.load_theme(f)) {
1273            plat::write_stderr(alloc::format!("error: --theme-file: {}\n", e).as_bytes());
1274        }
1275    }
1276}
1277
1278pub fn run(desc: &AppDesc, app: &mut dyn App, style: &'static dyn Style) -> i32 {
1279    let (mut ui, w, h) = prepare(desc, style);
1280    let ui = &mut ui;
1281    ui.hr_init(); // hot reload: jfgui_HOTRELOAD (hotreload.rs; development builds)
1282    let args = Args(plat::args());
1283    let shot = args.value("--shot");
1284    let script = args.value("--script");
1285    let agent_mode = if args.flag("--agent-json") { Some(agent_core::Protocol::Json) }
1286        else if args.flag("--agent-mcp") { Some(agent_core::Protocol::Mcp) } else { None };
1287    if shot.is_none() && script.is_none() && agent_mode.is_none() {
1288        return window_main(ui, app, w, h);
1289    }
1290    // ---- headless run (virtual clock starting at 0)
1291    ui.headless = true;
1292    ui.m.touch.on = false; // (a run does not depend on the machine: `pointer touch`)
1293    ui.sys_apply(plat::sys_settings_env()); // only jfgui_* overrides: shots match everywhere
1294    // missing characters from system fonts: only on request, so shots stay the same everywhere
1295    ui.text.set_system_fallback(plat::env("jfgui_FONT_FALLBACK").is_some_and(|v| v.starts_with('1')));
1296    let (pw, ph) = (ui.dp(w), ui.dp(h));
1297    ui.wp.rt.fb = Surface::new(pw, ph, FMT_XRGB);
1298    if ui.wp.rt.fb.px.is_empty() && pw * ph != 0 {
1299        plat::write_stderr(b"error: out of memory\n");
1300        return 2;
1301    }
1302    ui.set_time(0);
1303    if !app.init(ui) {
1304        plat::write_stderr(b"error: app initialisation failed\n");
1305        return 2;
1306    }
1307    cli_theme(ui);
1308    ui.run_passes(app);
1309    // hot reload: the session's log replayed first (hotreload.rs)
1310    if !ui.hr_start(app) {
1311        return super::hotreload::HR_EXIT_DIVERGED;
1312    }
1313    if let Some(protocol) = agent_mode {
1314        let r = super::agent::serve(ui, app, protocol);
1315        ui.hr_end();
1316        return r;
1317    }
1318    if let Some(s) = script {
1319        let r = ui.script_run(app, s);
1320        ui.hr_end();
1321        return r;
1322    }
1323    let shot = shot.unwrap_or("");
1324    ui.render();
1325    if !Ui::save_png(&ui.wp.rt.fb, shot) {
1326        plat::write_stderr(alloc::format!("error: could not write {}\n", shot).as_bytes());
1327        return 1;
1328    }
1329    if args.flag("--dump") {
1330        ui.dump(None);
1331    }
1332    ui.hr_end();
1333    plat::write_stdout(alloc::format!("wrote {}\n", shot).as_bytes());
1334    0
1335}
1336
1337/// the real window and its loop whatever the command line says (test programs: `build_rs.py test`
1338/// passes --script to them)
1339pub fn run_window(desc: &AppDesc, app: &mut dyn App, style: &'static dyn Style) -> i32 {
1340    let (mut ui, w, h) = prepare(desc, style);
1341    window_main(&mut ui, app, w, h)
1342}
1343
1344/// the interactive window loop (ui_main.s .am_window): the platform's window-indexed API
1345/// (plat::window_create, wait_refresh, win_present, the hit hook) on every OS; the live
1346/// redraw inside the Win32 modal size / move loop goes through plat::wait_refresh (the
1347/// Linux backends have no modal loop)
1348fn window_main(ui: &mut Ui, app: &mut dyn App, w: i32, h: i32) -> i32 {
1349    // AF_SINGLE_INSTANCE: a running instance takes this start's command line (UIC_OPEN there)
1350    if ui.desc.flags & AF_SINGLE_INSTANCE != 0 && plat::instance::claim(&plat::vfs::app_id()) == plat::instance::Claim::Forwarded {
1351        return 0;
1352    }
1353    plat::win_hit_hook(Some(super::win::uiw_hit_pw)); // non-client hit tests ask the UI
1354    plat::win_move_hook(Some(super::win::uiw_moving_pw)); // user moves snap (window_snap)
1355    plat::drag_hook(Some(super::win::uiw_drag_pw)); // so do drags of other programs (dnd.rs)
1356    plat::drop_hold(true); // a drop's answer waits for its passes (drop_defer, dnd.rs)
1357    plat::set_min_size(ui.desc.min_w as i32, ui.desc.min_h as i32);
1358    // AF_SAVE_STATE / jfgui_STATE: the placement of the last run (winstate.rs); --size wins
1359    let state = super::winstate::state_file(ui.desc.flags);
1360    let saved = state.as_deref().map(base::Ini::load);
1361    let mut main = saved.as_ref().and_then(super::winstate::main_saved);
1362    if Args(plat::args()).value("--size").is_some() {
1363        main = None;
1364    }
1365    let (cw, ch) = main.map_or((w, h), |m| (m.w.max(ui.desc.min_w as i32), m.h.max(ui.desc.min_h as i32)));
1366    let (x, y) = match main {
1367        Some(m) if m.pos => (m.x, m.y),
1368        _ => (plat::PW_DEFAULT, plat::PW_DEFAULT),
1369    };
1370    if !plat::window_create_at(ui.desc.title, cw, ch, ui.desc.flags, x, y, main.is_some_and(|m| m.max)) {
1371        plat::write_stderr(b"error: no window could be created (use --shot FILE.png for a headless run)\n");
1372        return 2;
1373    }
1374    if main.is_some_and(|m| m.pos && !m.max) {
1375        main_onscreen(); // a monitor of the last run may be gone
1376    }
1377    ui.win_hooks_set(true); // ui_win_hooks_set(uiw_plat_hooks): window_begin windows are OS windows
1378    log_start(ui);
1379    ui.sys_apply(plat::sys_settings()); // theme, accent, contrast, motion, text scale, timing
1380    // missing characters from system fonts (jfgui_FONT_FALLBACK=0 turns it off)
1381    ui.text.set_system_fallback(!plat::env("jfgui_FONT_FALLBACK").is_some_and(|v| v.starts_with('0')));
1382    ui.wp.rt.dpi = plat::dpi();
1383    let (pw, ph) = plat::window_size();
1384    ui.wp.rt.fb = Surface::new(pw, ph, FMT_XRGB);
1385    ui.t0_us = plat::time_us();
1386    ui.clock_update();
1387    super::perm::ask_here(true); // permission questions: a dialog on this thread (perm.rs)
1388    if !app.init(ui) {
1389        plat::write_stderr(b"error: app initialisation failed\n");
1390        return 2;
1391    }
1392    cli_theme(ui);
1393    ui.need = true;
1394    if Args(plat::args()).flag("--agent") { ui.set_agent_enabled(true); }
1395    ui.close_at = ui.autoclose_ms;
1396    // the main window's normal placement until the platform reports one; the tool windows of
1397    // the last run wait for window_begin
1398    if let Some(m) = main {
1399        ui.m.win.norm = super::winstate::MainState { max: false, ..m };
1400    }
1401    ui.ws_track();
1402    if let Some(ini) = &saved {
1403        ui.ws_windows(ini);
1404    }
1405    ui.gp_window_mode(); // the output through the GPU: its work starts after the first frame (gpu_out.rs)
1406    // hot reload: the session's log replayed before the first wait (hotreload.rs); diverged:
1407    // the watcher starts the app again afresh
1408    if !ui.hr_start(app) {
1409        ui.call_cmd(app, UIC_EXIT, 0);
1410        ui.set_agent_enabled(false);
1411        super::perm::ask_here(false);
1412        return super::hotreload::HR_EXIT_DIVERGED;
1413    }
1414    loop {
1415        if ui.quit {
1416            break;
1417        }
1418        #[cfg(not(any(target_arch = "wasm32", target_os = "android")))]
1419        ui.agent_poll(app);
1420        if ui.need {
1421            ui.need = false;
1422            ui.run_passes(app);
1423            // a command of this frame may have ended the app (UIC_CLOSE, quit): leave
1424            // before waiting for the next event
1425            if ui.quit {
1426                break;
1427            }
1428            ui.render();
1429            ui.present();
1430        }
1431        // deadline in ms since start: animations (16 ms), timers, auto close
1432        let mut dl = if ui.agent_enabled() { ui.now_ms.wrapping_add(30) } else { u32::MAX };
1433        if ui.animating || ui.touch_busy() {
1434            dl = ui.now_ms.wrapping_add(16);
1435        }
1436        let t = ui.timer_due_peek();
1437        if t < dl {
1438            dl = t;
1439        }
1440        if ui.close_at != 0 && ui.close_at < dl {
1441            dl = ui.close_at;
1442        }
1443        if ui.hr_on() && ui.hr_deadline().wrapping_sub(ui.now_ms) < dl.wrapping_sub(ui.now_ms) {
1444            dl = ui.hr_deadline(); // hot reload: the watcher's files (hotreload.rs)
1445        }
1446        // ms since start -> absolute us; tasks: their timers (the task clock is time_us here),
1447        // no wait while some are ready
1448        let mut deadline = if dl == u32::MAX { u64::MAX } else { dl as u64 * 1000 + ui.t0_us };
1449        if let Some(t) = ui.tasks_deadline() {
1450            deadline = deadline.min(t.max(1));
1451        }
1452        if ui.tasks_ready() {
1453            deadline = 0;
1454        }
1455        if let Some(t) = ui.gp_deadline() {
1456            deadline = deadline.min(t.max(1)); // the GPU output's jobs and frames still to show
1457        }
1458        if let Some(t) = plat::a11y::check_due() {
1459            deadline = deadline.min(t.max(1)); // whether a screen reader left (UIC_A11Y)
1460        }
1461        // the live redraw inside the Win32 modal size / move loop (ui_refresh)
1462        plat::wait_refresh(deadline, &mut || ui.refresh(app));
1463        // (Windows) the fallback that notices a client left looks now; a change wakes the loop
1464        let _ = plat::a11y::active();
1465        ui.clock_update();
1466        if ui.close_at != 0 && ui.close_at <= ui.now_ms {
1467            break;
1468        }
1469        if ui.animating || ui.tasks_ready() {
1470            ui.need = true;
1471        }
1472        if ui.hr_on() {
1473            ui.hr_poll(); // hot reload: "quit", a failed build (hotreload.rs)
1474        }
1475        ui.tick_timers();
1476        ui.process_events(app);
1477        ui.gp_tick();
1478    }
1479    // the placement for the next run (winstate.rs), then every way out of the window loop
1480    // (close, quit, --auto-close) tells the app (U3)
1481    if let Some(p) = &state {
1482        let mut ini = base::Ini::load(p);
1483        ui.state_save(&mut ini);
1484        if !ini.save(p) {
1485            plat::log_warn("app", &alloc::format!("could not write {}", p));
1486        }
1487    }
1488    ui.hr_end(); // hot reload: the log's end, the carried state (hotreload.rs)
1489    ui.dnd_quit(); // a drop whose answer waits gets it now (dnd.rs)
1490    ui.call_cmd(app, UIC_EXIT, 0);
1491    ui.set_agent_enabled(false);
1492    ui.gp_shutdown();
1493    super::perm::ask_here(false); // threads still waiting for an answer are refused
1494    plat::log_info("app", "exit");
1495    0
1496}
1497
1498// the main window just created at a saved position: moved into the work area of the nearest
1499// monitor when it would be (almost) off every screen
1500fn main_onscreen() {
1501    let (Some(r), Some(area)) = (plat::win_get_rect(0), plat::win_work_area(0)) else { return };
1502    let (x, y) = super::winstate::fit_area(r[0], r[1], r[2], r[3], area);
1503    if (x, y) != (r[0], r[1]) {
1504        plat::win_set_rect(0, x, y, plat::PW_KEEP, plat::PW_KEEP);
1505    }
1506}
1507
1508// window mode: the log file (AF_LOG_FILE, jfgui_LOG_FILE = a path, 1 or 0) and the start line
1509fn log_start(ui: &Ui) {
1510    let env = plat::env("jfgui_LOG_FILE");
1511    let path = match env.as_deref() {
1512        Some("0") | Some("") => None,
1513        Some("1") => Some(alloc::format!("logs://{}.log", plat::vfs::app_id())),
1514        Some(p) => Some(String::from(p)),
1515        None if ui.desc.flags & AF_LOG_FILE != 0 => Some(alloc::format!("logs://{}.log", plat::vfs::app_id())),
1516        None => None,
1517    };
1518    if let Some(p) = path {
1519        if !plat::log_file(Some(&p), 1 << 20, 3) {
1520            plat::log_warn("app", &alloc::format!("the log file {p} cannot be written"));
1521        }
1522    }
1523    plat::log!(plat::LOG_INFO, "app", "start {} ({}, {})", ui.desc.title, crate::i18n::locale_tag(), crate::i18n::language_code());
1524}
1525
1526/// plat::parallel_for for img_core and gfx (which do not depend on plat)
1527fn icc_par(n: usize, f: &(dyn Fn(usize) + Sync)) {
1528    let _ = plat::parallel_for(n, &|i, _| f(i));
1529}