1use 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
50pub 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(); ui.sys_apply(plat::sys_settings());
71 super::perm::ask_here(true); 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 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 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 #[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 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 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
203pub 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 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; 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 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 pub fn frame(&self) -> &Surface { self.ui.fb() }
249 pub fn ui(&self) -> &Ui { &self.ui }
251 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 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
271pub(crate) struct W {
274 pub(crate) fb: Surface,
275 pub(crate) shadow: Surface,
276 pub(crate) dpi: i32,
277 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
287pub(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
310pub(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
333struct 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 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
350pub(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 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 pub fn fb(&self) -> &Surface {
465 &self.wp.rt.fb
466 }
467
468 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 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 pub fn headless(&self) -> bool {
483 self.headless
484 }
485
486 pub fn invalidate(&mut self) {
488 self.need = true;
489 }
490
491 pub fn quit(&mut self) {
493 self.quit = true;
494 }
495
496 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 pub fn title(&self) -> &str {
510 if self.title.is_empty() { self.desc.title } else { &self.title }
511 }
512
513 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 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 fn call_cmd(&mut self, app: &mut dyn App, id: u32, arg: u32) -> bool {
527 app.command(self, id, arg)
528 }
529
530 pub(crate) fn process_events(&mut self, app: &mut dyn App) {
536 self.process_events_deferred(app, true);
537 }
538
539 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(); self.uiw_use(0);
546 loop {
547 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); }
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; }
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 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 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); self.sensors_poll();
586 self.call_cmd(app, UIC_SUSPEND, arg);
587 self.run_passes(app); 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; 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); } else if !self.call_cmd(app, UIC_CLOSE, 0) {
609 self.quit = true; } else {
611 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(); self.cmd_run(UIC_WAKE, 0); self.open_poll();
630 self.tray_poll();
631 self.screen_poll(); self.sensors_poll(); self.recent_poll(); self.a11y_poll(); self.spell_poll(); 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 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); self.run_passes(app);
660 self.need_render = true;
661 continue;
662 }
663 plat::E_DRAG_END => {
664 self.dnd_out_end(ev.data); self.run_passes(app);
666 self.need_render = true;
667 continue;
668 }
669 _ => {}
670 }
671 if !synth {
672 self.touch_kind(&ev); if self.touch_takes(&ev) {
674 self.touch_input(&ev); continue;
676 }
677 }
678 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; }
698 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); }
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); self.need_render = true;
717 }
718 }
719 }
720 if text_pending {
721 self.run_passes(app);
722 self.need_render = true;
723 }
724 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 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(); }
745 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 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 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); }
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 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 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 pub(crate) fn run_passes(&mut self, app: &mut dyn App) {
812 let (mut passes, mut build_us) = (0u32, 0u64); self.touch_apply(); let mut tasks_ran = self.settle(app, &mut passes, &mut build_us);
815 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(); self.focus_req_expire();
826 self.need = tasks_ran; self.a11y_frame(); if self.m.ins.on {
829 self.ins_passes(passes, build_us);
830 } else if self.m.ins.gone {
831 self.ins_gone(); }
833 }
834
835 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; 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(); self.ids_unwind(); self.perm_dialog(); 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 self.inspect_frame();
870 }
871 let mut again = self.frame_end();
872 if pass == 0 {
873 self.uiw_clear_edges();
874 keys_fwd = self.uiw_fwd_inject();
877 again |= keys_fwd;
878 } else if keys_fwd {
879 keys_fwd = false;
880 self.uiw_clear_edges(); }
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 let changed = self.uiw_settle();
897 let mut ran = false;
899 while let Some((id, arg)) = self.cmd_take() {
900 ran = true;
901 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); if !r && id == UIC_CLOSE {
908 self.quit = true;
909 }
910 }
911 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 fn csd_begin(&mut self) -> bool {
930 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 pub(crate) fn render(&mut self) {
954 if !self.uiw_any_used() {
955 self.render_live(); self.uiw_popups_render(0); 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); continue;
964 }
965 self.uiw_use(i);
966 if self.uiw_take_newshow(i) {
967 self.dirty_force_full(); }
969 self.render_live();
970 self.uiw_popups_render(i); self.present();
972 }
973 self.uiw_use(0);
974 self.uiw_sync_show(); }
976
977 fn render_live(&mut self) {
979 self.wp.rt.present_n = 0;
980 if self.wp.rt.fb.px.is_empty() {
981 return; }
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(); self.cmd_mirror(); 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); }
992 self.wp.rt.present_n = self.scopy_present(n); 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; }
1001 let dirty_n = n;
1002 self.dc.begin(&self.wp.rt.fb);
1003 let font = self.text.get_font();
1006 if split {
1007 self.gp_split_draw(n); n = 0;
1009 } else if self.render_par(n) {
1010 n = 0; }
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(); if ins {
1028 self.ins_render_end(); }
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 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 pub(crate) fn verify_dirty(&mut self) {
1052 self.gp_settle_all(); 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); }
1063 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; }
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 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(); self.wp.rt.present_n = -1;
1097 }
1098
1099 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 fn timer_due_peek(&self) -> u32 {
1109 match self.timer_scan() {
1110 (Some(_), _) => self.now_ms,
1111 (None, next) => next,
1112 }
1113 }
1114}
1115
1116fn ev_dropped(ev: &Event) {
1119 if ev.typ == plat::E_IME_COMMIT {
1120 let _ = plat::ime_commit_take(ev.data);
1121 }
1122}
1123
1124fn 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
1133fn prepare(desc: &AppDesc, style: &'static dyn Style) -> (Ui, i32, i32) {
1135 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(); plat::set_print_deflate(Some(img_core::deflate::zlib_deflate)); img_core::set_parallel(icc_par); let args = Args(plat::args());
1143 let mut ui = Ui::new(desc, style);
1144 ui.set_decimal_sep(crate::i18n::locale().decimal() as u32);
1146 let (mut w, mut h) = (desc.w as i32, desc.h as i32);
1148 if let Some(v) = args.value("--size") {
1149 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")); if let Some(a) = args.value("--anim") {
1165 ui.anim_off = a == "off";
1166 ui.anim_forced = true;
1167 }
1168 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 if args.flag("--rtl") {
1182 ui.set_rtl(Some(true));
1183 } else if args.flag("--ltr") {
1184 ui.set_rtl(Some(false));
1185 }
1186 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; ui.theme_set(mode);
1210 (ui, w, h)
1211}
1212
1213fn 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(); 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 ui.headless = true;
1238 ui.m.touch.on = false; ui.sys_apply(plat::sys_settings_env()); 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 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
1283pub 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
1290fn window_main(ui: &mut Ui, app: &mut dyn App, w: i32, h: i32) -> i32 {
1295 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)); plat::win_move_hook(Some(super::win::uiw_moving_pw)); plat::drag_hook(Some(super::win::uiw_drag_pw)); plat::set_min_size(ui.desc.min_w as i32, ui.desc.min_h as i32);
1303 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(); }
1322 ui.win_hooks_set(true); log_start(ui);
1324 ui.sys_apply(plat::sys_settings()); 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); 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 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(); 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 if ui.quit {
1371 break;
1372 }
1373 ui.render();
1374 ui.present();
1375 }
1376 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(); }
1391 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)); }
1403 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(); }
1415 ui.tick_timers();
1416 ui.process_events(app);
1417 ui.gp_tick();
1418 }
1419 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(); ui.call_cmd(app, UIC_EXIT, 0);
1430 ui.set_agent_enabled(false);
1431 ui.gp_shutdown();
1432 super::perm::ask_here(false); plat::log_info("app", "exit");
1434 0
1435}
1436
1437fn 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
1447fn 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
1465fn icc_par(n: usize, f: &(dyn Fn(usize) + Sync)) {
1467 let _ = plat::parallel_for(n, &|i, _| f(i));
1468}