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