1#[cfg(any(target_arch = "wasm32", target_os = "android", target_os = "ios"))]
39use alloc::collections::BTreeMap;
40use alloc::string::String;
41use alloc::vec::Vec;
42
43use super::consts::*;
44use super::{App, AppDesc, Style, Ui, UiInput, WinParts};
45use gfx::{Dc, Surface, FMT_XRGB};
46use plat::Event;
47use text::Text;
48
49const MAX_PASSES: u32 = 4;
50
51pub fn launch(desc: &AppDesc, app: impl App + 'static, style: &'static dyn Style) -> i32 {
56 #[cfg(not(any(target_arch = "wasm32", target_os = "android", target_os = "ios")))]
57 { let mut app = app; run(desc, &mut app, style) }
58 #[cfg(target_os = "ios")]
59 {
60 let a = Args(plat::args());
61 if a.value("--shot").is_some() || a.value("--script").is_some() || a.flag("--agent-json") || a.flag("--agent-mcp") {
62 let mut app = app;
63 return run(desc, &mut app, style);
64 }
65 }
66 #[cfg(any(target_arch = "wasm32", target_os = "android", target_os = "ios"))]
67 {
68 let (mut ui, w, h) = prepare(desc, style);
69 plat::win_hit_hook(Some(super::win::uiw_hit_pw));
70 plat::drag_hook(Some(super::win::uiw_drag_pw));
71 plat::set_min_size(desc.min_w as i32, desc.min_h as i32);
72 if !plat::window_create(desc.title, w, h, desc.flags) { return 2; }
73 ui.win_hooks_set(true);
74 ui.wp.rt.dpi = plat::dpi();
75 let (w, h) = plat::window_size();
76 ui.wp.rt.fb = Surface::new(w, h, FMT_XRGB);
77 ui.set_time(0);
78 ui.gp_window_mode(); ui.sys_apply(plat::sys_settings());
82 super::perm::ask_here(true); let mut app: alloc::boxed::Box<dyn App> = alloc::boxed::Box::new(app);
84 if ui.wp.rt.fb.px.is_empty() || !app.init(&mut ui) { return 2; }
85 ui.set_agent_enabled(Args(plat::args()).flag("--agent"));
86 ui.run_passes(app.as_mut());
87 ui.render();
88 ui.present();
89 let runtime = Runtime { ui, app, finished: false };
90 if plat::web::install(alloc::boxed::Box::new(HostRuntime {
91 runtime, sessions: BTreeMap::new(), session_clock: 0,
92 })) { 0 } else { 2 }
93 }
94}
95
96#[cfg(any(target_arch = "wasm32", target_os = "android", target_os = "ios"))]
97struct HostRuntime {
98 runtime: Runtime,
99 sessions: BTreeMap<String, (u64, agent_core::RpcSession)>,
100 session_clock: u64,
101}
102#[cfg(any(target_arch = "wasm32", target_os = "android", target_os = "ios"))]
103impl plat::web::WebApplication for HostRuntime {
104 fn frame_delay(&self) -> u32 {
105 let ui = &self.runtime.ui;
106 if ui.need || ui.need_render || ui.animating || ui.touch_busy() || ui.tasks_ready() { return 0; }
107 let task = ui.tasks_deadline().map(|t| (t.saturating_sub(plat::task::now_us()) / 1000).min(250) as i32);
108 let gpu = ui.gp_deadline().map(|t| (t.saturating_sub(plat::time_us()).div_ceil(1000)).min(250) as i32);
110 ui.timers.iter()
111 .map(|(_, deadline)| deadline.wrapping_sub(ui.now_ms) as i32)
112 .chain(task)
113 .chain(gpu)
114 .min().unwrap_or(250).clamp(0, 250) as u32
115 }
116 fn input(&mut self, time_ms: u32) {
117 if self.runtime.finished { return; }
118 self.runtime.ui.set_time(time_ms);
119 self.runtime.ui.process_events_deferred(self.runtime.app.as_mut(), false);
120 if self.runtime.ui.need {
123 self.runtime.ui.need = false;
124 self.runtime.ui.run_passes(self.runtime.app.as_mut());
125 }
126 }
127 fn set_control_enabled(&mut self, enabled: bool) {
128 self.runtime.set_agent_enabled(enabled);
129 if !enabled { self.sessions.clear(); }
130 }
131 fn tick(&mut self, time_ms: u32) {
132 #[cfg(any(target_os = "android", target_os = "ios"))]
133 let running = !self.runtime.finished;
134 self.runtime.step(time_ms, &[]);
135 #[cfg(any(target_os = "android", target_os = "ios"))]
137 if running && self.runtime.finished {
138 plat::app_finish();
139 }
140 self.runtime.ui.present();
141 self.runtime.ui.gp_tick();
142 }
143 #[cfg(target_os = "android")]
144 fn surface_gone(&mut self) -> bool {
145 self.runtime.ui.gp_surface_gone()
146 }
147 fn gpu_test(&mut self, op: u32) -> Option<String> {
148 let ui = &mut self.runtime.ui;
149 match op {
150 1 => ui.gpu_test_lose(),
151 2 => ui.gpu_settle(),
152 3 => {
153 ui.dirty_force_full();
154 ui.invalidate();
155 }
156 _ => {}
157 }
158 let o = ui.gpu_output(0);
159 #[cfg(any(windows, target_os = "linux", target_os = "android", target_vendor = "apple", target_arch = "wasm32"))]
161 let v = ui.m.gview.views.first().and_then(|e| ui.gpu_view_info(e.win as u32, e.id)).map(|i| {
162 alloc::format!(" view_device={} view_level={} view_gen={} view_apart={} view_attaches={} view_detaches={} view_frames={} view_reads={} view_read_frame={} view_held={} view_size={}x{}",
163 i.device as u8, i.level.map_or("none", |l| if l == gpu::Level::Voll { "Voll" } else { "Basis" }), i.generation, i.apart as u8, i.attaches,
164 i.detaches, i.frames, i.reads, i.read_frame, i.held, i.size.0, i.size.1)
165 }).unwrap_or_default();
166 #[cfg(not(any(windows, target_os = "linux", target_os = "android", target_vendor = "apple", target_arch = "wasm32")))]
167 let v = String::new();
168 Some(alloc::format!(
169 "wanted={} on={} building={} pending={} never={} adapter={} present_mode={} reserved={} frames={} cpu_frames={} upload_bytes={} upload_base={} \
170 layer_textures={} layer_bytes={} layer_store={} split_frames={} image_quads={} image_textures={} image_bytes={} canvas_apart={} canvas_quads={} \
171 canvas_frames={} canvas_tiles={} canvas_bytes={} canvas_fills={} canvas_settles={} canvas_diff_px={} canvas_diff_max={} base_px={} render_us={} present_us={} switches={} leaves={} verified={} verify_bad={} \
172 fb={}x{} dpi={} view_quads={}{v} error={}",
173 o.wanted as u8, o.on as u8, o.building as u8, o.pending as u8, o.never as u8, o.adapter.replace(' ', "_"), o.present_mode, o.reserved, o.frames, o.cpu_frames,
174 o.upload_bytes, o.upload_base, o.layer_textures, o.layer_bytes, o.layer_store, o.split_frames, o.image_quads, o.image_textures, o.image_bytes,
175 o.canvas_apart, o.canvas_quads, o.canvas_frames, o.canvas_tiles, o.canvas_bytes, o.canvas_fills, o.canvas_settles, o.canvas_diff_px, o.canvas_diff_max, o.base_px, o.render_us, o.present_us,
176 o.switches, o.leaves, o.verified, o.verify_bad, ui.wp.rt.fb.w, ui.wp.rt.fb.h, ui.wp.rt.dpi, o.view_quads, o.error.unwrap_or_default().replace(' ', "_")))
177 }
178 fn control(&mut self, line: &[u8]) -> Option<String> {
179 let envelope = agent_core::json::parse(line).ok()?;
180 let session = envelope.get("session").and_then(agent_core::Value::as_str).unwrap_or("page");
181 if session.is_empty() || session.len() > 128 { return None; }
182 if envelope.get("close").and_then(agent_core::Value::as_bool) == Some(true) {
183 self.sessions.remove(session);
184 return None;
185 }
186 let request = envelope.get("request")?;
187 let line = agent_core::json::stringify(request).ok()?;
188 if !self.sessions.contains_key(session) {
189 let Some(protocol) = agent_core::rpc::requested_protocol(line.as_bytes()) else {
190 let mut transient = agent_core::RpcSession::new(agent_core::Protocol::Json, self.runtime.ui.desc.title, "1");
192 return self.runtime.control(&mut transient, line.as_bytes());
193 };
194 if self.sessions.len() >= 16 {
195 let oldest = self.sessions.iter().filter(|(_, (_, rpc))| !rpc.requires_persistent_session())
196 .min_by_key(|(_, (used, _))| *used).map(|(name, _)| name.clone());
197 if let Some(oldest) = oldest {
198 self.sessions.remove(&oldest);
199 } else {
200 let id = request.get("id")?.clone();
203 return agent_core::json::stringify(&agent_core::Value::object([
204 ("jsonrpc", agent_core::Value::string("2.0")), ("id", id),
205 ("error", agent_core::Value::object([
206 ("code", agent_core::Value::i64(-32000)),
207 ("message", agent_core::Value::string("Browser session limit reached; close an initialized MCP session")),
208 ])),
209 ])).ok();
210 }
211 }
212 self.sessions.insert(String::from(session), (self.session_clock, agent_core::RpcSession::new(protocol, self.runtime.ui.desc.title, "1")));
213 }
214 self.session_clock = self.session_clock.wrapping_add(1);
215 let (used, rpc) = self.sessions.get_mut(session).unwrap();
216 *used = self.session_clock;
217 let response = self.runtime.control(rpc, line.as_bytes());
218 self.runtime.ui.present();
219 response
220 }
221}
222
223pub struct Runtime {
226 pub(crate) ui: Ui,
227 pub(crate) app: alloc::boxed::Box<dyn App>,
228 finished: bool,
229}
230impl Runtime {
231 pub(crate) fn is_finished(&self) -> bool { self.finished }
232 pub fn new(desc: &AppDesc, app: impl App + 'static, style: &'static dyn Style) -> Result<Self, i32> {
235 let (mut ui, w, h) = prepare(desc, style);
236 ui.headless = true;
237 ui.wp.rt.fb = Surface::new(ui.dp(w), ui.dp(h), FMT_XRGB);
238 ui.m.touch.on = false; ui.set_time(0);
240 let mut app: alloc::boxed::Box<dyn App> = alloc::boxed::Box::new(app);
241 if ui.wp.rt.fb.px.is_empty() || !app.init(&mut ui) { return Err(2); }
242 ui.run_passes(app.as_mut());
243 ui.render();
244 Ok(Self { ui, app, finished: false })
245 }
246 pub fn step(&mut self, now_ms: u32, events: &[Event]) -> bool {
249 if self.finished { return false; }
250 self.ui.set_time(now_ms);
251 for event in events {
252 if !plat::evq_push(*event) {
253 self.ui.process_events(self.app.as_mut());
254 plat::evq_push(*event);
255 }
256 }
257 self.ui.process_events(self.app.as_mut());
258 self.ui.tick_timers();
259 if self.ui.need || self.ui.animating || self.ui.tasks_ready() {
260 self.ui.need = false;
261 self.ui.run_passes(self.app.as_mut());
262 }
263 self.ui.render();
264 if self.ui.quit { self.finish(); }
265 !self.finished
266 }
267 pub fn frame(&self) -> &Surface { self.ui.fb() }
269 pub fn ui(&self) -> &Ui { &self.ui }
271 pub fn command(&mut self, id: u32, arg: u32) -> bool {
273 if self.finished { return false; }
274 let handled = self.app.command(&mut self.ui, id, arg);
275 self.ui.invalidate();
276 handled
277 }
278 pub fn finish(&mut self) {
280 if !self.finished {
281 self.finished = true;
282 self.ui.set_agent_enabled(false);
283 self.app.command(&mut self.ui, UIC_EXIT, 0);
284 }
285 }
286}
287impl Drop for Runtime {
288 fn drop(&mut self) { self.finish(); }
289}
290
291pub(crate) struct W {
294 pub(crate) fb: Surface,
295 pub(crate) shadow: Surface,
296 pub(crate) dpi: i32,
297 pub(crate) present_n: i32,
299}
300
301impl W {
302 pub(crate) fn new() -> W {
303 W { fb: Surface::new(0, 0, FMT_XRGB), shadow: Surface::new(0, 0, FMT_XRGB), dpi: 96, present_n: 0 }
304 }
305}
306
307pub(crate) fn parse_int(s: &[u8]) -> (i32, &[u8]) {
311 let mut i = 0;
312 while i < s.len() && s[i] == b' ' {
313 i += 1;
314 }
315 let mut neg = false;
316 if i < s.len() && s[i] == b'-' {
317 neg = true;
318 i += 1;
319 } else if i < s.len() && s[i] == b'+' {
320 i += 1;
321 }
322 let mut v: i32 = 0;
323 while i < s.len() && s[i].is_ascii_digit() {
324 v = v.wrapping_mul(10).wrapping_add((s[i] - b'0') as i32);
325 i += 1;
326 }
327 (if neg { v.wrapping_neg() } else { v }, &s[i..])
328}
329
330pub(crate) fn parse_hex(s: &[u8]) -> (u32, &[u8]) {
332 let mut i = 0;
333 if !s.is_empty() && s[0] == b'#' {
334 i = 1;
335 } else if s.len() >= 2 && s[0] == b'0' && s[1] | 32 == b'x' {
336 i = 2;
337 }
338 let mut v: u32 = 0;
339 while i < s.len() {
340 let d = match s[i] {
341 c @ b'0'..=b'9' => c - b'0',
342 c => {
343 let l = c | 32;
344 if (b'a'..=b'f').contains(&l) { l - b'a' + 10 } else { break }
345 }
346 };
347 v = v << 4 | d as u32;
348 i += 1;
349 }
350 (v, &s[i..])
351}
352
353struct Args(Vec<String>);
355
356impl Args {
357 fn value(&self, name: &str) -> Option<&str> {
358 let i = self.0.iter().skip(1).position(|a| a == name)? + 1;
359 self.0.get(i + 1).map(|s| s.as_str())
360 }
361 fn flag(&self, name: &str) -> bool {
362 self.0.iter().skip(1).any(|a| a == name)
363 }
364 fn int(&self, name: &str, def: i32) -> i32 {
366 self.value(name).map_or(def, |v| parse_int(v.as_bytes()).0)
367 }
368}
369
370pub(crate) fn scale_to_dpi(p: i32) -> i32 {
372 (p.clamp(50, 400) * 96 + 50) / 100
373}
374
375impl Ui {
376 pub(crate) fn tick_timers(&mut self) {
377 loop {
378 let (id, _) = self.timer_due();
379 if id == u32::MAX { break; }
380 self.cmd_run(UIC_TIMER, id);
381 self.need = true;
382 }
383 }
384 pub(crate) fn new(desc: &AppDesc, style: &'static dyn Style) -> Ui {
386 let mut ui = Ui {
387 agent: Default::default(),
388 desc: *desc,
389 style,
390 text: Text::new(style.fonts(), style.font_names()),
391 dc: Dc::new(0, 0),
392 wp: WinParts::new(),
393 m: Default::default(),
394 sems: Vec::new(),
395 semtext_len: 0,
396 sem_dummy: Default::default(),
397 keydown: [0; 256],
398 mpts: Vec::new(),
399 mprs: Vec::new(),
400 mtilt: [0; 2],
401 mpress: 0,
402 mptype: 0,
403 mpts_win: 0,
404 cmdq: alloc::collections::VecDeque::new(),
405 text_more: Vec::new(),
406 timers: Vec::new(),
407 limits: [0; super::ctx::LIM_N],
408 mods: 0,
409 frame: 0,
410 form: FF_DESKTOP,
411 now_ms: 0,
412 repass: false,
413 animating: false,
414 anim_off: false,
415 rtl: false,
416 rtl_mode: 0,
417 app_focus: 0,
418 app_active: false,
419 theme_sys: false,
420 theme_base_dark: false,
421 contrast_mode: 0,
422 high_contrast: false,
423 hc_sys: None,
424 accent_sys: false,
425 anim_forced: false,
426 anim_sys: false,
427 text_scale_forced: false,
428 text_scale: 100,
429 dbl_ms: 500,
430 font_prefs: (None, None, None),
431 tb_sys: plat::Titlebar::default(),
432 blink_ms: 530,
433 warns: 0,
434 ui_debug: plat::getenv("jfgui_DEBUG").is_some(),
435 dup_check: plat::getenv("jfgui_DEBUG").is_some(),
436 dup_warned: alloc::collections::BTreeSet::new(),
437 ids: Default::default(),
438 focus_req: 0,
439 focus_req_win: 0,
440 focus_req_old: 0,
441 focus_req_frame: 0,
442 uiw_cur: 0,
443 theme: [0; super::theme::THEME_N],
444 theme_dark: false,
445 accent: 0,
446 look: super::theme::Look::new(style),
447 title: String::new(),
448 t0_us: 0,
449 headless: false,
450 need: false,
451 need_render: false,
452 quit: false,
453 log_events: false,
454 open_reqs: Vec::new(),
455 autoclose_ms: 0,
456 accent_raw: 0,
457 settle_warned: false,
458 self_frames: 0,
459 self_warned: false,
460 verify_dirty: false,
461 frames_rendered: 0,
462 close_at: 0,
463 drects: Vec::new(),
464 dmg: Vec::new(),
465 cvs: Vec::new(),
466 icon_hook: None,
467 uiinput: UiInput::default(),
468 sc: Default::default(),
469 ucur: Default::default(),
470 dr: Default::default(),
471 dnd: Default::default(),
472 tasks: plat::task::Executor::ui(),
473 };
474 for (font, raster) in style.font_raster().iter().enumerate() {
475 ui.text.set_font_raster(font as u32, *raster);
476 }
477 ui.uiw_init();
478 ui
479 }
480
481 pub fn fb(&self) -> &Surface {
487 &self.wp.rt.fb
488 }
489
490 pub fn client_size(&self) -> (i32, i32) {
492 (self.px2dip(self.wp.rt.fb.w), self.px2dip(self.wp.rt.fb.h))
493 }
494
495 pub fn anim_off(&self) -> bool {
497 self.anim_off
498 }
499 pub fn set_anim_off(&mut self, off: bool) {
500 self.anim_off = off;
501 }
502
503 pub fn headless(&self) -> bool {
505 self.headless
506 }
507
508 pub fn invalidate(&mut self) {
510 self.need = true;
511 }
512
513 pub fn quit(&mut self) {
515 self.quit = true;
516 }
517
518 pub fn set_title(&mut self, s: &str) {
520 let mut n = s.len().min(255);
521 while !s.is_char_boundary(n) {
522 n -= 1;
523 }
524 self.title = String::from(&s[..n]);
525 if !self.headless {
526 plat::set_title(&self.title);
527 }
528 }
529
530 pub fn title(&self) -> &str {
532 if self.title.is_empty() { self.desc.title } else { &self.title }
533 }
534
535 pub(crate) fn frame_of(sf: &Surface) -> plat::Frame<'_> {
537 plat::Frame { px: &sf.px, w: sf.w, h: sf.h, stride: sf.stride }
538 }
539
540 pub(crate) fn save_png(sf: &Surface, path: &str) -> bool {
543 let png = img_png::encode(&sf.px, sf.w as usize, sf.h as usize, sf.stride as usize, false);
544 super::perm::trusted(|| plat::write_file(path, &png))
545 }
546
547 fn call_cmd(&mut self, app: &mut dyn App, id: u32, arg: u32) -> bool {
549 app.command(self, id, arg)
550 }
551
552 pub(crate) fn process_events(&mut self, app: &mut dyn App) {
558 self.process_events_deferred(app, true);
559 }
560
561 pub(crate) fn process_events_deferred(&mut self, app: &mut dyn App, render: bool) {
564 let mut text_pending = false;
565 let mut text_win: i32 = -1;
566 self.touch_tick(); self.uiw_use(0);
568 loop {
569 let made = self.touch_next();
572 let synth = made.is_some();
573 let Some(mut ev) = made.or_else(plat::evq_pop) else { break };
574 self.self_frames = 0; if !synth && self.hr_on() {
576 self.hr_popped(&ev); }
578 if !self.headless && !synth {
579 let w = self.uiw_from_pw(ev.win as i32);
580 if w < 0 {
581 ev_dropped(&ev);
582 continue; }
584 ev.win = w as u8;
585 }
586 if self.log_events {
587 log_event(&ev);
588 }
589 let win = ev.win as u32;
590 match ev.typ {
592 plat::E_FOCUS_IN => self.app_focus |= 1 << (win & 31),
593 plat::E_FOCUS_OUT => self.app_focus &= !(1 << (win & 31)),
594 _ => {}
595 }
596 match ev.typ {
597 plat::E_POWER => {
598 let (arg, what) = match ev.data {
601 0 => (0, "the system woke up"),
602 2 => (2, "the app went to the background"),
603 3 => (0, "the app is in the foreground again"),
604 _ => (1, "the system goes to sleep"),
605 };
606 plat::log_info("app", what);
607 plat::sensors::suspend(arg != 0); self.sensors_poll();
609 self.call_cmd(app, UIC_SUSPEND, arg);
610 self.run_passes(app); self.need_render = true;
612 continue;
613 }
614 plat::E_SESSION_END => {
615 plat::log_info("app", "the session ends");
616 self.call_cmd(app, UIC_SESSION_END, 0);
617 self.quit = true; self.run_passes(app);
619 self.need_render = true;
620 continue;
621 }
622 plat::E_SETTINGS => {
623 self.settings_changed(app, ev.data);
624 self.run_passes(app);
625 self.need_render = true;
626 continue;
627 }
628 plat::E_CLOSE => {
629 if win != 0 {
630 self.uiw_close_request(win); } else if !self.call_cmd(app, UIC_CLOSE, 0) {
632 self.quit = true; } else {
634 self.need = true;
637 }
638 continue;
639 }
640 plat::E_RESIZE => {
641 self.uiw_resize(app, win, ev.x, ev.y);
642 continue;
643 }
644 plat::E_DPI => {
645 self.uiw_dpi(win, ev.data as i32);
646 continue;
647 }
648 plat::E_PAINT => continue,
649 plat::E_WAKE => {
650 self.tasks_wake(); self.cmd_run(UIC_WAKE, 0); self.open_poll();
653 self.tray_poll();
654 self.screen_poll(); self.sensors_poll(); self.recent_poll(); self.a11y_poll(); self.spell_poll(); self.need = true;
660 continue;
661 }
662 plat::E_FOCUS_IN => {
663 self.uiw_focus_in(win);
664 continue;
665 }
666 plat::E_WINPOS => {
667 self.uiw_winpos(win, ev.x, ev.y);
668 continue;
669 }
670 plat::E_DROP => {
671 if !self.drop_receive(&ev) {
673 let id = self.uiw_rec_id(win);
674 self.cmd_run(UIC_DROP, id);
675 }
676 self.run_passes(app);
677 self.dnd_drop_settled(); self.need_render = true;
679 continue;
680 }
681 plat::E_DRAG => {
682 self.dnd_hover(&ev); self.run_passes(app);
684 self.need_render = true;
685 continue;
686 }
687 plat::E_DRAG_END => {
688 self.dnd_out_end(ev.data); self.run_passes(app);
690 self.need_render = true;
691 continue;
692 }
693 _ => {}
694 }
695 if !synth {
696 self.touch_kind(&ev); if self.touch_takes(&ev) {
698 self.touch_input(&ev); continue;
700 }
701 }
702 if self.m.ins.armed {
704 let r = self.ins_event(&ev);
705 if r != 0 {
706 if text_pending {
707 text_pending = false;
708 self.run_passes(app);
709 }
710 if r == 1 {
711 self.run_passes(app);
712 }
713 self.need_render = true;
714 continue;
715 }
716 }
717 let t = self.uiw_input_target(&ev);
718 if t < 0 {
719 ev_dropped(&ev);
720 continue; }
722 if text_pending && (ev.typ != plat::E_TEXT || t != text_win) {
724 text_pending = false;
725 self.run_passes(app);
726 self.need_render = true;
727 }
728 self.uiw_use(t as u32);
729 if !synth && self.hr_on() {
730 self.hr_input(&ev, t as u32); }
732 match self.input_event(&ev) {
733 2 => {
734 text_pending = true;
735 text_win = t;
736 }
737 0 => {}
738 _ => {
739 self.run_passes(app); self.need_render = true;
741 }
742 }
743 }
744 if text_pending {
745 self.run_passes(app);
746 self.need_render = true;
747 }
748 let active = self.app_focus != 0;
751 if active != self.app_active && self.hr_active(active) {
752 self.app_active = active;
753 if !active {
754 self.uiw_popups_dismiss(WIN_MAX as u32);
757 }
758 self.call_cmd(app, UIC_ACTIVE, active as u32);
759 self.run_passes(app);
760 self.need_render = true;
761 }
762 if self.uiw_moved_passes() {
763 self.run_passes(app);
764 self.need_render = true;
765 }
766 if self.hr_on() {
767 self.hr_batch_end(); }
769 if render && self.touch_moved() {
771 self.run_passes(app);
772 self.need_render = true;
773 }
774 if render && self.need_render {
775 self.need_render = false;
776 if !self.headless || self.verify_dirty || self.gp_scripted() {
778 self.render();
779 self.present();
780 }
781 }
782 self.uiw_use(0);
783 }
784
785 fn settings_changed(&mut self, app: &mut dyn App, bits: u32) {
787 plat::log!(plat::LOG_INFO, "app", "system settings changed ({bits:#x})");
788 if bits & (plat::SET_THEME | plat::SET_CONTRAST | plat::SET_FONTS | plat::SET_OTHER) != 0 {
789 let before = (self.theme_dark, self.high_contrast);
790 let s = if self.headless { plat::sys_settings_env() } else { plat::sys_settings() };
791 if bits & plat::SET_THEME != 0 && self.theme_sys && s.dark.is_none() {
792 self.theme_set(2); }
794 self.sys_apply(s);
795 if before != (self.theme_dark, self.high_contrast) {
796 self.call_cmd(app, UIC_THEME, 0);
797 }
798 }
799 if bits & plat::SET_LOCALE != 0 && !self.headless {
800 crate::i18n::use_system();
801 self.set_decimal_sep(crate::i18n::locale().decimal() as u32);
802 }
803 self.call_cmd(app, UIC_SETTINGS, bits);
804 self.dirty_force_full();
805 self.need = true;
806 }
807
808 pub(crate) fn refresh(&mut self, app: &mut dyn App) {
810 self.clock_update();
811 self.process_events(app);
812 self.run_passes(app);
813 self.render();
814 self.present();
815 }
816
817 pub(crate) fn resize(&mut self, app: &mut dyn App, w: i32, h: i32) {
819 if w <= 0 || h <= 0 || (w == self.wp.rt.fb.w && h == self.wp.rt.fb.h) {
820 return;
821 }
822 self.wp.rt.fb.resize(w, h);
823 self.dirty_force_full();
824 self.need = true;
825 if self.headless {
826 self.run_passes(app);
827 }
828 }
829
830 pub(crate) fn run_passes(&mut self, app: &mut dyn App) {
836 let (mut passes, mut build_us) = (0u32, 0u64); self.touch_apply(); let mut unseen = self.settle(app, &mut passes, &mut build_us);
839 while self.text_refill() {
841 unseen = self.settle(app, &mut passes, &mut build_us);
842 }
843 if self.a11y_reveal() {
844 unseen = self.settle(app, &mut passes, &mut build_us);
845 self.agent.reveal = None;
846 }
847 self.touch_after_passes();
848 self.dnd_after_passes(); self.focus_req_expire();
850 self.need_after_passes(unseen);
851 self.a11y_frame(); if self.m.ins.on {
853 self.ins_passes(passes, build_us);
854 } else if self.m.ins.gone {
855 self.ins_gone(); }
857 }
858
859 fn need_after_passes(&mut self, unseen: (bool, bool)) {
867 const SELF_FRAMES: u32 = 8;
868 let (tasks_ran, unseen) = unseen;
869 let asked = self.need || unseen;
870 self.self_frames = if asked { self.self_frames.saturating_add(1) } else { 0 };
871 if asked && self.self_frames > SELF_FRAMES && !tasks_ran {
872 if !self.self_warned {
873 self.self_warned = true;
874 self.warn("every frame asked for another one (ui.invalidate in App::frame, a command queued in every frame): the frames wait for the next event");
875 }
876 self.need = false;
877 } else {
878 self.need = asked;
879 }
880 }
881
882 fn settle(&mut self, app: &mut dyn App, passes: &mut u32, build_us: &mut u64) -> (bool, bool) {
886 let mut rounds = 3;
887 let mut tasks_ran;
888 loop {
889 self.need = false;
890 let mut pass = 0;
891 let mut keys_fwd = false; loop {
893 self.uiw_use(0);
894 self.frame_begin();
895 let (w, h) = (self.wp.rt.fb.w, self.wp.rt.fb.h);
896 self.uiw_pass_begin(w, h);
897 let csd = self.csd_begin();
898 let t0 = if self.m.ins.on { plat::time_us() } else { 0 };
899 app.frame(self);
900 if t0 != 0 {
901 *build_us += plat::time_us().saturating_sub(t0);
902 }
903 self.uiw_unwind(); self.ids_unwind(); self.perm_dialog(); if csd || self.desc.flags & AF_CUSTOM_TITLEBAR != 0 {
908 self.edge_grips();
909 }
910 if csd {
911 self.box_end();
912 self.box_end();
913 }
914 self.uiw_pass_end();
915 if self.m.ins.on {
916 self.inspect_frame();
919 }
920 let mut again = self.frame_end();
921 if pass == 0 {
922 self.uiw_clear_edges();
923 keys_fwd = self.uiw_fwd_inject();
926 again |= keys_fwd;
927 } else if keys_fwd {
928 keys_fwd = false;
929 self.uiw_clear_edges(); }
931 pass += 1;
932 *passes += 1;
933 if !again {
934 break;
935 }
936 if pass >= MAX_PASSES {
937 if !self.settle_warned {
938 self.settle_warned = true;
939 self.warn("layout did not settle within 4 passes");
940 }
941 break;
942 }
943 }
944 let changed = self.uiw_settle();
946 let mut ran = false;
948 while let Some((id, arg)) = self.cmd_take() {
949 ran = true;
950 if arg == super::inspect::INSPECT_WIN && self.ins_cmd(id) {
952 continue;
953 }
954 let r = self.call_cmd(app, id, arg);
955 self.uiw_cmd_done(id, arg, r); if !r && id == UIC_CLOSE {
957 self.quit = true;
958 }
959 }
960 tasks_ran = self.tasks_pump();
962 if !(ran || changed || tasks_ran) {
963 return (false, false);
964 }
965 rounds -= 1;
966 if rounds == 0 {
967 return (tasks_ran, true);
968 }
969 }
970 }
971
972 fn csd_begin(&mut self) -> bool {
978 if self.desc.flags & AF_CUSTOM_TITLEBAR != 0 || self.m.win.recs[0].fs {
980 return false;
981 }
982 let forced = plat::getenv("jfgui_CSD").is_some_and(|v| v.as_bytes().first() == Some(&b'1'));
983 if !forced && (self.headless || plat::window_state() & plat::WST_CSD == 0) {
984 return false;
985 }
986 self.next_size(-1, -1);
987 self.box_begin(0xFFFF_7420, BOX_V | BOX_FIXED, 0);
988 let t = String::from(self.title());
989 self.titlebar_begin(&t, 0);
990 self.titlebar_end();
991 self.next_size(-1, -1);
992 self.box_begin(0xFFFF_7421, BOX_V | BOX_FIXED, 0);
993 true
994 }
995
996 pub(crate) fn render(&mut self) {
1002 if !self.uiw_any_used() {
1003 self.render_live(); self.uiw_popups_render(0); self.present();
1006 return;
1007 }
1008 for i in 0..WIN_MAX as u32 {
1009 if !self.uiw_drawable(i) {
1010 self.uiw_popups_drop(i); continue;
1012 }
1013 self.uiw_use(i);
1014 if self.uiw_take_newshow(i) {
1015 self.dirty_force_full(); }
1017 self.render_live();
1018 self.uiw_popups_render(i); self.present();
1020 }
1021 self.uiw_use(0);
1022 self.uiw_sync_show(); }
1024
1025 fn render_live(&mut self) {
1027 self.wp.rt.present_n = 0;
1028 if self.wp.rt.fb.px.is_empty() {
1029 return; }
1031 let t0 = plat::time_us();
1032 let (w, h) = (self.wp.rt.fb.w, self.wp.rt.fb.h);
1033 self.wp.dirty.split = self.gp_split_wanted(); self.cmd_mirror(); let mut n = self.dirty_compute(w, h);
1036 let ins = self.m.ins.on;
1037 if ins {
1038 n = self.ins_render_begin(n); }
1040 self.wp.rt.present_n = self.scopy_present(n); if self.ui_debug {
1042 plat::write_stderr(alloc::format!("render: {} dirty rects (-1 = full)\n", n).as_bytes());
1043 }
1044 let split = self.gp_split_begin(n);
1045 if self.wp.rt.present_n == 0 {
1046 self.gp_frame_done(t0, 0, false);
1047 return; }
1049 let dirty_n = n;
1050 self.dc.begin(&self.wp.rt.fb);
1051 let font = self.text.get_font();
1054 if split {
1055 self.gp_split_draw(n); n = 0;
1057 } else if self.render_par(n) {
1058 n = 0; }
1060 if n < 0 {
1061 self.render_all_to(false);
1062 } else {
1063 for i in 0..n as usize {
1064 let r = self.drects[i];
1065 if self.ui_debug {
1066 plat::write_stderr(alloc::format!(" dirty {},{} {}x{}\n", r[0], r[1], r[2], r[3]).as_bytes());
1067 }
1068 self.dc.clip_push(r[0], r[1], r[2], r[3]);
1069 self.render_all_to(false);
1070 self.dc.clip_pop();
1071 }
1072 }
1073 self.dc.end();
1074 self.scopy_done(); if ins {
1076 self.ins_render_end(); }
1078 self.frames_rendered = self.frames_rendered.wrapping_add(1);
1079 if self.verify_dirty {
1080 self.verify_dirty();
1081 }
1082 self.text.set_font(font.0, font.1);
1083 self.gp_frame_done(t0, dirty_n, split);
1084 }
1085
1086 pub(crate) fn present(&mut self) {
1088 let n = self.wp.rt.present_n;
1089 if n == 0 {
1090 return;
1091 }
1092 self.uiw_present(n);
1093 self.wp.rt.present_n = 0;
1094 }
1095
1096 pub(crate) fn verify_dirty(&mut self) {
1100 self.gp_settle_all(); let (w, h) = (self.wp.rt.fb.w, self.wp.rt.fb.h);
1102 if self.wp.rt.shadow.w != w || self.wp.rt.shadow.h != h {
1103 self.wp.rt.shadow = Surface::new(w, h, FMT_XRGB);
1104 }
1105 self.dc.begin(&self.wp.rt.shadow);
1106 self.render_all_to(true);
1107 self.dc.end();
1108 if self.m.ins.on {
1109 self.ins_verify(true); }
1111 let (mut n, mut x0, mut y0, mut x1, mut y1) = (0u32, i32::MAX, i32::MAX, -1i32, -1i32);
1113 {
1114 let (a, b) = (&self.wp.rt.shadow, &self.wp.rt.fb);
1115 for y in 0..a.h {
1116 let ra = y as usize * a.stride as usize;
1117 let rb = y as usize * b.stride as usize;
1118 for x in 0..a.w {
1119 let i = x as usize * 4;
1120 if a.px[ra + i..ra + i + 3] != b.px[rb + i..rb + i + 3] {
1121 n += 1;
1122 x0 = x0.min(x);
1123 y0 = y0.min(y);
1124 x1 = x1.max(x);
1125 y1 = y; }
1127 }
1128 }
1129 }
1130 if n == 0 {
1131 return;
1132 }
1133 self.warn(&alloc::format!("dirty regions missed {} pixels", n));
1134 let s = alloc::format!(" in px {},{} .. {},{} (window {})\n", x0, y0, x1, y1, self.uiw_cur);
1135 plat::write_stderr(s.as_bytes());
1136 self.dc.begin(&self.wp.rt.fb);
1138 self.render_all_to(false);
1139 self.dc.end();
1140 if self.m.ins.on {
1141 self.ins_verify(false);
1142 }
1143 self.gp_repaired(); self.wp.rt.present_n = -1;
1145 }
1146
1147 fn clock_update(&mut self) {
1151 let ms = (plat::time_us().wrapping_sub(self.t0_us) / 1000) as u32;
1152 self.set_time(ms);
1153 }
1154
1155 fn timer_due_peek(&self) -> u32 {
1157 match self.timer_scan() {
1158 (Some(_), _) => self.now_ms,
1159 (None, next) => next,
1160 }
1161 }
1162}
1163
1164fn ev_dropped(ev: &Event) {
1167 if ev.typ == plat::E_IME_COMMIT {
1168 let _ = plat::ime_commit_take(ev.data);
1169 }
1170 if ev.typ == plat::E_DROP {
1172 let _ = plat::drop_data_take();
1173 plat::drop_finish(plat::drop_waiting(), 0);
1174 }
1175}
1176
1177fn log_event(ev: &Event) {
1179 let s = alloc::format!(
1180 "event type={} btn={} mods={:x} x={} y={} data={:x} win={}\n",
1181 ev.typ, ev.btn, ev.mods, ev.x, ev.y, ev.data, ev.win
1182 );
1183 plat::write_stdout(s.as_bytes());
1184}
1185
1186fn prepare(desc: &AppDesc, style: &'static dyn Style) -> (Ui, i32, i32) {
1188 img_core::io::set_file_io(img_core::io::FileIo { read: plat::read_file, write: plat::write_file });
1191 img_all::use_core_codecs();
1192 img_svg::register(); plat::set_print_deflate(Some(img_core::deflate::zlib_deflate)); img_core::set_parallel(icc_par); gfx::set_parallel(icc_par); let args = Args(plat::args());
1197 let mut ui = Ui::new(desc, style);
1198 ui.set_decimal_sep(crate::i18n::locale().decimal() as u32);
1200 let (mut w, mut h) = (desc.w as i32, desc.h as i32);
1202 if let Some(v) = args.value("--size") {
1203 let (a, r) = parse_int(v.as_bytes());
1205 if !r.is_empty() && r[0] | 32 == b'x' {
1206 let (b, _) = parse_int(&r[1..]);
1207 if a > 0 && b > 0 {
1208 w = a;
1209 h = b;
1210 }
1211 }
1212 }
1213 ui.wp.rt.dpi = scale_to_dpi(args.int("--scale", 100));
1214 ui.log_events = args.flag("--log-events");
1215 ui.verify_dirty = args.flag("--verify-dirty");
1216 ui.autoclose_ms = args.int("--auto-close", 0) as u32;
1217 ui.inspect_args(args.flag("--inspect")); if let Some(a) = args.value("--anim") {
1219 ui.anim_off = a == "off";
1220 ui.anim_forced = true;
1221 }
1222 match args.value("--contrast") {
1224 Some("high") => ui.contrast_mode = 1,
1225 Some("normal") => ui.contrast_mode = 2,
1226 _ => {}
1227 }
1228 if let Some(v) = args.value("--text-scale") {
1229 if let Ok(p) = v.parse::<u32>() {
1230 ui.text_scale = p.clamp(50, 300);
1231 ui.text_scale_forced = true;
1232 }
1233 }
1234 if args.flag("--rtl") {
1236 ui.set_rtl(Some(true));
1237 } else if args.flag("--ltr") {
1238 ui.set_rtl(Some(false));
1239 }
1240 let mut mode = 2;
1242 if desc.flags & AF_DARK != 0 {
1243 mode = 1;
1244 }
1245 if desc.flags & AF_LIGHT != 0 {
1246 mode = 0;
1247 }
1248 if let Some(t) = args.value("--theme") {
1249 if t == "dark" {
1250 mode = 1;
1251 }
1252 if t == "light" {
1253 mode = 0;
1254 }
1255 }
1256 ui.accent_raw = desc.accent & 0xFF_FFFF;
1257 if let Some(a) = args.value("--accent") {
1258 ui.accent_raw = parse_hex(a.as_bytes()).0 & 0xFF_FFFF;
1259 }
1260 let acc = ui.accent_raw;
1261 ui.accent_set(acc);
1262 ui.accent_sys = acc == 0; ui.theme_set(mode);
1264 (ui, w, h)
1265}
1266
1267fn cli_theme(ui: &mut Ui) {
1270 let args = Args(plat::args());
1271 if let Some(f) = args.value("--theme-file") {
1272 if let Err(e) = super::perm::trusted(|| ui.load_theme(f)) {
1273 plat::write_stderr(alloc::format!("error: --theme-file: {}\n", e).as_bytes());
1274 }
1275 }
1276}
1277
1278pub fn run(desc: &AppDesc, app: &mut dyn App, style: &'static dyn Style) -> i32 {
1279 let (mut ui, w, h) = prepare(desc, style);
1280 let ui = &mut ui;
1281 ui.hr_init(); let args = Args(plat::args());
1283 let shot = args.value("--shot");
1284 let script = args.value("--script");
1285 let agent_mode = if args.flag("--agent-json") { Some(agent_core::Protocol::Json) }
1286 else if args.flag("--agent-mcp") { Some(agent_core::Protocol::Mcp) } else { None };
1287 if shot.is_none() && script.is_none() && agent_mode.is_none() {
1288 return window_main(ui, app, w, h);
1289 }
1290 ui.headless = true;
1292 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')));
1296 let (pw, ph) = (ui.dp(w), ui.dp(h));
1297 ui.wp.rt.fb = Surface::new(pw, ph, FMT_XRGB);
1298 if ui.wp.rt.fb.px.is_empty() && pw * ph != 0 {
1299 plat::write_stderr(b"error: out of memory\n");
1300 return 2;
1301 }
1302 ui.set_time(0);
1303 if !app.init(ui) {
1304 plat::write_stderr(b"error: app initialisation failed\n");
1305 return 2;
1306 }
1307 cli_theme(ui);
1308 ui.run_passes(app);
1309 if !ui.hr_start(app) {
1311 return super::hotreload::HR_EXIT_DIVERGED;
1312 }
1313 if let Some(protocol) = agent_mode {
1314 let r = super::agent::serve(ui, app, protocol);
1315 ui.hr_end();
1316 return r;
1317 }
1318 if let Some(s) = script {
1319 let r = ui.script_run(app, s);
1320 ui.hr_end();
1321 return r;
1322 }
1323 let shot = shot.unwrap_or("");
1324 ui.render();
1325 if !Ui::save_png(&ui.wp.rt.fb, shot) {
1326 plat::write_stderr(alloc::format!("error: could not write {}\n", shot).as_bytes());
1327 return 1;
1328 }
1329 if args.flag("--dump") {
1330 ui.dump(None);
1331 }
1332 ui.hr_end();
1333 plat::write_stdout(alloc::format!("wrote {}\n", shot).as_bytes());
1334 0
1335}
1336
1337pub fn run_window(desc: &AppDesc, app: &mut dyn App, style: &'static dyn Style) -> i32 {
1340 let (mut ui, w, h) = prepare(desc, style);
1341 window_main(&mut ui, app, w, h)
1342}
1343
1344fn window_main(ui: &mut Ui, app: &mut dyn App, w: i32, h: i32) -> i32 {
1349 if ui.desc.flags & AF_SINGLE_INSTANCE != 0 && plat::instance::claim(&plat::vfs::app_id()) == plat::instance::Claim::Forwarded {
1351 return 0;
1352 }
1353 plat::win_hit_hook(Some(super::win::uiw_hit_pw)); plat::win_move_hook(Some(super::win::uiw_moving_pw)); plat::drag_hook(Some(super::win::uiw_drag_pw)); plat::drop_hold(true); plat::set_min_size(ui.desc.min_w as i32, ui.desc.min_h as i32);
1358 let state = super::winstate::state_file(ui.desc.flags);
1360 let saved = state.as_deref().map(base::Ini::load);
1361 let mut main = saved.as_ref().and_then(super::winstate::main_saved);
1362 if Args(plat::args()).value("--size").is_some() {
1363 main = None;
1364 }
1365 let (cw, ch) = main.map_or((w, h), |m| (m.w.max(ui.desc.min_w as i32), m.h.max(ui.desc.min_h as i32)));
1366 let (x, y) = match main {
1367 Some(m) if m.pos => (m.x, m.y),
1368 _ => (plat::PW_DEFAULT, plat::PW_DEFAULT),
1369 };
1370 if !plat::window_create_at(ui.desc.title, cw, ch, ui.desc.flags, x, y, main.is_some_and(|m| m.max)) {
1371 plat::write_stderr(b"error: no window could be created (use --shot FILE.png for a headless run)\n");
1372 return 2;
1373 }
1374 if main.is_some_and(|m| m.pos && !m.max) {
1375 main_onscreen(); }
1377 ui.win_hooks_set(true); log_start(ui);
1379 ui.sys_apply(plat::sys_settings()); ui.text.set_system_fallback(!plat::env("jfgui_FONT_FALLBACK").is_some_and(|v| v.starts_with('0')));
1382 ui.wp.rt.dpi = plat::dpi();
1383 let (pw, ph) = plat::window_size();
1384 ui.wp.rt.fb = Surface::new(pw, ph, FMT_XRGB);
1385 ui.t0_us = plat::time_us();
1386 ui.clock_update();
1387 super::perm::ask_here(true); if !app.init(ui) {
1389 plat::write_stderr(b"error: app initialisation failed\n");
1390 return 2;
1391 }
1392 cli_theme(ui);
1393 ui.need = true;
1394 if Args(plat::args()).flag("--agent") { ui.set_agent_enabled(true); }
1395 ui.close_at = ui.autoclose_ms;
1396 if let Some(m) = main {
1399 ui.m.win.norm = super::winstate::MainState { max: false, ..m };
1400 }
1401 ui.ws_track();
1402 if let Some(ini) = &saved {
1403 ui.ws_windows(ini);
1404 }
1405 ui.gp_window_mode(); if !ui.hr_start(app) {
1409 ui.call_cmd(app, UIC_EXIT, 0);
1410 ui.set_agent_enabled(false);
1411 super::perm::ask_here(false);
1412 return super::hotreload::HR_EXIT_DIVERGED;
1413 }
1414 loop {
1415 if ui.quit {
1416 break;
1417 }
1418 #[cfg(not(any(target_arch = "wasm32", target_os = "android")))]
1419 ui.agent_poll(app);
1420 if ui.need {
1421 ui.need = false;
1422 ui.run_passes(app);
1423 if ui.quit {
1426 break;
1427 }
1428 ui.render();
1429 ui.present();
1430 }
1431 let mut dl = if ui.agent_enabled() { ui.now_ms.wrapping_add(30) } else { u32::MAX };
1433 if ui.animating || ui.touch_busy() {
1434 dl = ui.now_ms.wrapping_add(16);
1435 }
1436 let t = ui.timer_due_peek();
1437 if t < dl {
1438 dl = t;
1439 }
1440 if ui.close_at != 0 && ui.close_at < dl {
1441 dl = ui.close_at;
1442 }
1443 if ui.hr_on() && ui.hr_deadline().wrapping_sub(ui.now_ms) < dl.wrapping_sub(ui.now_ms) {
1444 dl = ui.hr_deadline(); }
1446 let mut deadline = if dl == u32::MAX { u64::MAX } else { dl as u64 * 1000 + ui.t0_us };
1449 if let Some(t) = ui.tasks_deadline() {
1450 deadline = deadline.min(t.max(1));
1451 }
1452 if ui.tasks_ready() {
1453 deadline = 0;
1454 }
1455 if let Some(t) = ui.gp_deadline() {
1456 deadline = deadline.min(t.max(1)); }
1458 if let Some(t) = plat::a11y::check_due() {
1459 deadline = deadline.min(t.max(1)); }
1461 plat::wait_refresh(deadline, &mut || ui.refresh(app));
1463 let _ = plat::a11y::active();
1465 ui.clock_update();
1466 if ui.close_at != 0 && ui.close_at <= ui.now_ms {
1467 break;
1468 }
1469 if ui.animating || ui.tasks_ready() {
1470 ui.need = true;
1471 }
1472 if ui.hr_on() {
1473 ui.hr_poll(); }
1475 ui.tick_timers();
1476 ui.process_events(app);
1477 ui.gp_tick();
1478 }
1479 if let Some(p) = &state {
1482 let mut ini = base::Ini::load(p);
1483 ui.state_save(&mut ini);
1484 if !ini.save(p) {
1485 plat::log_warn("app", &alloc::format!("could not write {}", p));
1486 }
1487 }
1488 ui.hr_end(); ui.dnd_quit(); ui.call_cmd(app, UIC_EXIT, 0);
1491 ui.set_agent_enabled(false);
1492 ui.gp_shutdown();
1493 super::perm::ask_here(false); plat::log_info("app", "exit");
1495 0
1496}
1497
1498fn main_onscreen() {
1501 let (Some(r), Some(area)) = (plat::win_get_rect(0), plat::win_work_area(0)) else { return };
1502 let (x, y) = super::winstate::fit_area(r[0], r[1], r[2], r[3], area);
1503 if (x, y) != (r[0], r[1]) {
1504 plat::win_set_rect(0, x, y, plat::PW_KEEP, plat::PW_KEEP);
1505 }
1506}
1507
1508fn log_start(ui: &Ui) {
1510 let env = plat::env("jfgui_LOG_FILE");
1511 let path = match env.as_deref() {
1512 Some("0") | Some("") => None,
1513 Some("1") => Some(alloc::format!("logs://{}.log", plat::vfs::app_id())),
1514 Some(p) => Some(String::from(p)),
1515 None if ui.desc.flags & AF_LOG_FILE != 0 => Some(alloc::format!("logs://{}.log", plat::vfs::app_id())),
1516 None => None,
1517 };
1518 if let Some(p) = path {
1519 if !plat::log_file(Some(&p), 1 << 20, 3) {
1520 plat::log_warn("app", &alloc::format!("the log file {p} cannot be written"));
1521 }
1522 }
1523 plat::log!(plat::LOG_INFO, "app", "start {} ({}, {})", ui.desc.title, crate::i18n::locale_tag(), crate::i18n::language_code());
1524}
1525
1526fn icc_par(n: usize, f: &(dyn Fn(usize) + Sync)) {
1528 let _ = plat::parallel_for(n, &|i, _| f(i));
1529}