1use alloc::string::String;
211use alloc::vec::Vec;
212use core::fmt::Write;
213
214use super::consts::*;
215use super::keys::*;
216use super::layout::{dp_at, px2dip_at};
217use super::runtime::{parse_hex, parse_int};
218use super::{App, Sem, Ui};
219use gfx::{Surface, FMT_XRGB};
220use plat::{DragData, Event};
221
222const LINE_MAX: usize = 16384;
225const DUMP_TEXT_MAX: usize = 65536;
228const DUMP_TITLE_MAX: usize = 172;
230
231pub(crate) struct S {
234 lineno: u32,
235 fails: u32,
236 parse_err: bool,
237 line: Vec<u8>,
238 script_mods: u32,
239 sel_err: u32,
240 sel_kind: i32,
241 sel_wf: i32,
242 sel_win: u32,
243 sel_x: i32,
244 sel_y: i32,
245 sel_label: Vec<u8>,
246 sc_win: u32,
247 sc_x: i32,
248 sc_y: i32,
249 sc_pdata: u32,
250 finger_id: u32,
252 warn_mark: u32,
253 sc_scope: i32,
254 pub(crate) agent: super::auto_perm::S,
256 theme_err: Option<String>,
258}
259
260impl Default for S {
261 fn default() -> S {
262 S {
263 lineno: 0,
264 fails: 0,
265 parse_err: false,
266 line: Vec::new(),
267 script_mods: 0,
268 sel_err: 0,
269 sel_kind: -1,
270 sel_wf: -1,
271 sel_win: 0,
272 sel_x: 0,
273 sel_y: 0,
274 sel_label: Vec::new(),
275 sc_win: 0,
276 sc_x: 0,
277 sc_y: 0,
278 sc_pdata: 0,
279 finger_id: 0,
280 warn_mark: 0,
281 sc_scope: -1, agent: Default::default(),
283 theme_err: None,
284 }
285 }
286}
287
288#[derive(Clone, Copy)]
290pub(crate) enum Sel {
291 Rec(usize),
292 Point,
293}
294
295pub(crate) fn skip_ws(p: &[u8]) -> &[u8] {
299 let i = p.iter().position(|&c| c != b' ' && c != b'\t').unwrap_or(p.len());
300 &p[i..]
301}
302
303pub(crate) fn word_eq<'a>(p: &'a [u8], w: &str) -> Option<&'a [u8]> {
306 let w = w.as_bytes();
307 if !p.starts_with(w) {
308 return None;
309 }
310 match p.get(w.len()) {
311 None | Some(b' ') | Some(b'\t') => Some(&p[w.len()..]),
312 _ => None,
313 }
314}
315
316pub(crate) fn next_word(p: &[u8]) -> &[u8] {
318 let i = p.iter().position(|&c| c == b' ' || c == b'\t').unwrap_or(p.len());
319 skip_ws(&p[i..])
320}
321
322pub(crate) fn parse_qstring(p: &[u8], cap: usize) -> Option<(Vec<u8>, &[u8])> {
325 if p.first() != Some(&b'"') {
326 return None;
327 }
328 let mut out = Vec::new();
329 let mut i = 1;
330 loop {
331 let c = *p.get(i)?;
332 i += 1;
333 let ch = match c {
334 b'"' => return Some((out, &p[i..])),
335 b'\\' => {
336 let e = *p.get(i)?; i += 1;
338 match e {
339 b'n' => 10,
340 b't' => 9,
341 o => o,
342 }
343 }
344 o => o,
345 };
346 if out.len() < cap - 1 {
347 out.push(ch);
348 }
349 }
350}
351
352fn utf8_decode(s: &[u8], i: usize) -> (u32, usize) {
355 let b0 = s.get(i).copied().unwrap_or(0) as u32;
356 if b0 < 0x80 {
357 return (b0, i + 1);
358 }
359 let bad = (0xFFFD, i + 1);
360 let (mut cp, n, min) = if b0 < 0xC2 {
361 return bad;
362 } else if b0 < 0xE0 {
363 (b0 & 0x1F, 1, 0x80)
364 } else if b0 < 0xF0 {
365 (b0 & 0x0F, 2, 0x800)
366 } else if b0 < 0xF5 {
367 (b0 & 7, 3, 0x10000)
368 } else {
369 return bad;
370 };
371 let mut j = i + 1;
372 for _ in 0..n {
373 let c = s.get(j).copied().unwrap_or(0) as u32;
374 if c & 0xC0 != 0x80 {
375 return bad;
376 }
377 cp = cp << 6 | (c & 0x3F);
378 j += 1;
379 }
380 if cp < min || cp > 0x10FFFF || cp & 0xFFFF_F800 == 0xD800 {
381 return bad;
382 }
383 (cp, j)
384}
385
386fn parse_button(p: &[u8]) -> u32 {
388 match p.first().map(|c| c | 32) {
389 Some(b'r') => BTN_RIGHT,
390 Some(b'm') => BTN_MIDDLE,
391 _ => BTN_LEFT,
392 }
393}
394
395fn chord(p: &[u8]) -> Option<(u32, u32, &[u8])> {
398 let mut p = p;
399 let mut mods = 0;
400 loop {
401 if p.starts_with(b"ctrl+") {
402 mods |= MOD_CTRL;
403 p = &p[5..];
404 } else if p.starts_with(b"shift+") {
405 mods |= MOD_SHIFT;
406 p = &p[6..];
407 } else if p.starts_with(b"alt+") {
408 mods |= MOD_ALT;
409 p = &p[4..];
410 } else {
411 break;
412 }
413 }
414 let vk = key_lookup(p);
416 (vk != 0).then(|| (mods, vk, next_word(p)))
417}
418
419fn mod_word(p: &[u8]) -> u32 {
422 if p.starts_with(b"shift") {
423 MOD_SHIFT
424 } else if p.starts_with(b"ctrl") {
425 MOD_CTRL
426 } else if p.starts_with(b"alt") {
427 MOD_ALT
428 } else {
429 0
430 }
431}
432
433fn parse_pct(p: &[u8]) -> (i32, &[u8]) {
435 let (v, r) = parse_int(p);
436 let v = v.clamp(0, 100) as u32;
437 ((v * 65535 / 100) as i32, r)
438}
439
440fn key_lookup(p: &[u8]) -> u32 {
442 let c0 = p.first().copied().unwrap_or(0);
443 let c1 = p.get(1).copied().unwrap_or(0);
444 if c1 == b' ' || c1 == 0 {
445 if c0.is_ascii_lowercase() {
447 return (c0 - 32) as u32;
448 }
449 if c0.is_ascii_uppercase() || c0.is_ascii_digit() {
450 return c0 as u32;
451 }
452 }
453 const NAMES: &[(&str, u32)] = &[
454 ("enter", VK_RETURN), ("esc", VK_ESCAPE), ("tab", VK_TAB), ("space", VK_SPACE),
455 ("backspace", VK_BACK), ("delete", VK_DELETE), ("left", VK_LEFT), ("right", VK_RIGHT),
456 ("up", VK_UP), ("down", VK_DOWN), ("home", VK_HOME), ("end", VK_END),
457 ("pgup", VK_PRIOR), ("pgdn", VK_NEXT), ("insert", VK_INSERT), ("alt", VK_MENU), ("apps", VK_APPS),
458 ("shift", VK_SHIFT), ("ctrl", VK_CONTROL), ("plus", VK_OEM_PLUS), ("minus", VK_OEM_MINUS),
460 ("[", VK_OEM_4), ("]", VK_OEM_6), (",", VK_OEM_COMMA), (".", VK_OEM_PERIOD),
462 ("-", VK_OEM_MINUS), ("+", VK_OEM_PLUS), ("/", VK_OEM_2),
463 ("numadd", VK_ADD), ("numsub", VK_SUBTRACT), ("nummul", VK_MULTIPLY), ("numdiv", VK_DIVIDE),
464 ("numdec", VK_DECIMAL),
465 ("num0", VK_NUMPAD0), ("num1", VK_NUMPAD0 + 1), ("num2", VK_NUMPAD0 + 2), ("num3", VK_NUMPAD0 + 3),
466 ("num4", VK_NUMPAD0 + 4), ("num5", VK_NUMPAD0 + 5), ("num6", VK_NUMPAD0 + 6), ("num7", VK_NUMPAD0 + 7),
467 ("num8", VK_NUMPAD0 + 8), ("num9", VK_NUMPAD0 + 9),
468 ];
469 for &(n, vk) in NAMES {
470 if word_eq(p, n).is_some() {
471 return vk;
472 }
473 }
474 if c0 | 32 == b'f' {
476 let (n, _) = parse_int(&p[1..]);
477 if (1..=12).contains(&n) {
478 return VK_F1 + n as u32 - 1;
479 }
480 }
481 0
482}
483
484fn json_escape(out: &mut Vec<u8>, src: &[u8], max: usize) {
487 let mut end = src.len().min(max);
488 while end > 0 && end < src.len() && src[end] & 0xC0 == 0x80 {
489 end -= 1; }
491 for &c in &src[..end] {
492 if c == 0 {
493 break;
494 }
495 match c {
496 b'"' | b'\\' => {
497 out.push(b'\\');
498 out.push(c);
499 }
500 c if c < 32 => out.push(b' '), c => out.push(c),
502 }
503 }
504}
505
506const WFLAG_NAMES: [&str; 10] = ["tool", "dialog", "modal", "resizable", "noclose", "sysframe", "noactivate", "autoheight", "keys", "touchhold"];
507
508fn blit_copy(dst: &mut Surface, src: &Surface, x: i32, y: i32) {
510 let x0 = x.max(0);
511 let y0 = y.max(0);
512 let x1 = (x + src.w).min(dst.w);
513 let y1 = (y + src.h).min(dst.h);
514 if x1 <= x0 || y1 <= y0 {
515 return;
516 }
517 let n = (x1 - x0) as usize * 4;
518 for yy in y0..y1 {
519 let d = yy as usize * dst.stride as usize + x0 as usize * 4;
520 let s = (yy - y) as usize * src.stride as usize + (x0 - x) as usize * 4;
521 dst.px[d..d + n].copy_from_slice(&src.px[s..s + n]);
522 }
523}
524
525fn crop_surface(sf: &Surface, dpi: i32, c: [i32; 4]) -> Option<Surface> {
528 let x = dp_at(c[0], dpi);
529 let y = dp_at(c[1], dpi);
530 let mut w = dp_at(c[2], dpi);
531 let mut h = dp_at(c[3], dpi);
532 if x < 0 || y < 0 {
533 return None;
534 }
535 let ww = sf.w - x;
536 if ww <= 0 {
537 return None;
538 }
539 w = w.min(ww);
540 let hh = sf.h - y;
541 if hh <= 0 {
542 return None;
543 }
544 h = h.min(hh);
545 if w <= 0 || h <= 0 {
546 return None;
547 }
548 let mut out = Surface::new(w, h, sf.fmt);
549 for r in 0..h as usize {
550 let s = (y as usize + r) * sf.stride as usize + x as usize * 4;
551 let d = r * out.stride as usize;
552 out.px[d..d + w as usize * 4].copy_from_slice(&sf.px[s..s + w as usize * 4]);
553 }
554 Some(out)
555}
556
557impl Ui {
558 pub fn dump(&mut self, path: Option<&[u8]>) {
563 let mut out: Vec<u8> = Vec::new();
564 self.dump_header(&mut out);
565 for i in 1..WIN_MAX as u32 {
566 if self.uiw_drawable(i) {
567 self.dump_window(&mut out, i);
568 }
569 }
570 for k in 0..self.sems.len() {
571 let r = self.sems[k].clone();
572 self.dump_record(&mut out, &r);
573 }
574 match path {
575 Some(p) => {
576 super::perm::trusted(|| plat::write_file(&String::from_utf8_lossy(p), &out));
577 }
578 None => plat::write_stdout(&out),
579 }
580 }
581
582 fn dump_header(&self, out: &mut Vec<u8>) {
583 let mut s = String::new();
584 let scale = ((self.wp.rt.dpi as u32).wrapping_mul(100).wrapping_add(48)) / 96;
585 let _ = write!(
586 s,
587 "{{\"frame\":{},\"size\":[{},{}],\"scale\":{},\"theme\":\"{}\",\"focus\":{},\"hot\":{},\"keys\":{}}}\n",
588 self.frame,
589 self.px2dip(self.wp.rt.fb.w),
590 self.px2dip(self.wp.rt.fb.h),
591 scale,
592 if self.theme_dark { "dark" } else { "light" },
593 self.wp.ctx.focus,
594 self.wp.ctx.hot,
595 self.uiw_rec_id(self.uiw_keys_window()) );
597 out.extend_from_slice(s.as_bytes());
598 }
599
600 fn dump_window(&self, out: &mut Vec<u8>, i: u32) {
603 let rec = &self.m.win.recs[i as usize]; let mut title = Vec::new();
605 json_escape(&mut title, self.uiw_title(i).as_bytes(), DUMP_TITLE_MAX);
606 let mut flags = String::new();
607 for (b, n) in WFLAG_NAMES.iter().enumerate() {
608 if rec.flags & (1 << b) != 0 {
609 if !flags.is_empty() {
610 flags.push(' ');
611 }
612 flags.push_str(n);
613 }
614 }
615 let dpi = self.win_dpi(i);
616 let fb = self.win_fb(i);
617 out.extend_from_slice(alloc::format!("{{\"window\":{},\"title\":\"", rec.id).as_bytes());
618 out.extend_from_slice(&title);
619 let r = rec.rect.get(); let s = alloc::format!(
621 "\",\"flags\":\"{}\",\"rect\":[{},{},{},{}],\"size\":[{},{}],\"scale\":{},\"active\":{},\"focus\":{}}}\n",
622 flags,
623 r.x,
624 r.y,
625 r.w,
626 r.h,
627 px2dip_at(fb.w, dpi),
628 px2dip_at(fb.h, dpi),
629 ((dpi as u32).wrapping_mul(100).wrapping_add(48)) / 96,
630 (i == self.m.win.active) as u32,
631 self.uiw_parts(i).map_or(0, |p| p.ctx.focus)
632 );
633 out.extend_from_slice(s.as_bytes());
634 }
635
636 fn dump_record(&self, out: &mut Vec<u8>, r: &Sem) {
638 let mut st = String::new();
639 for (k, n) in STATE_NAMES.iter().enumerate() {
640 if r.state & (1 << k) != 0 {
641 if !st.is_empty() {
642 st.push(',');
643 }
644 st.push('"');
645 st.push_str(n);
646 st.push('"');
647 }
648 }
649 let mut lab = Vec::new();
650 json_escape(&mut lab, r.label.as_deref().unwrap_or("").as_bytes(), DUMP_TEXT_MAX);
651 let dpi = self.win_dpi(r.win as u32);
653 let kind = kind_name(r.kind);
654 out.extend_from_slice(
655 alloc::format!("{{\"l\":{},\"d\":{},\"id\":{},\"k\":\"{}\",\"t\":\"", r.layer, r.depth, r.id, kind).as_bytes(),
656 );
657 out.extend_from_slice(&lab);
658 let mut s = alloc::format!(
659 "\",\"r\":[{},{},{},{}],\"s\":[{}],\"v\":{},\"max\":{}",
660 px2dip_at(r.rect[0], dpi),
661 px2dip_at(r.rect[1], dpi),
662 px2dip_at(r.rect[2], dpi),
663 px2dip_at(r.rect[3], dpi),
664 st,
665 r.value,
666 r.max
667 );
668 if r.win != 0 {
669 let _ = write!(s, ",\"w\":{}", self.uiw_rec_id(r.win as u32));
671 }
672 s.push_str("}\n");
673 out.extend_from_slice(s.as_bytes());
674 }
675
676 pub(crate) fn script_run(&mut self, app: &mut dyn App, path: &str) -> i32 {
680 let Some(data) = super::perm::trusted(|| plat::read_file(path)) else {
681 plat::write_stderr(alloc::format!("error: cannot read script {}\n", path).as_bytes());
682 return 2;
683 };
684 self.sc.fails = 0;
685 self.sc.parse_err = false;
686 self.sc.lineno = 0;
687 self.dup_check = true;
688 let mut pos = 0;
689 while pos < data.len() && data[pos] != 0 {
690 self.sc.lineno += 1;
691 let mut line = Vec::new();
693 while pos < data.len() && data[pos] != 0 {
694 let c = data[pos];
695 pos += 1;
696 if c == b'\n' {
697 break;
698 }
699 if c == b'\r' {
700 continue;
701 }
702 if line.len() < LINE_MAX - 1 {
703 line.push(c);
704 }
705 }
706 if self.script_exec(app, &line) {
707 break; }
709 if self.sc.parse_err {
710 break;
711 }
712 }
713 if self.sc.parse_err {
714 2
715 } else if self.sc.fails != 0 {
716 1
717 } else {
718 0
719 }
720 }
721
722 pub(crate) fn fail(&mut self, detail: &[u8]) {
724 self.fail_more(detail);
725 plat::write_stdout(b"\n");
726 }
727
728 fn fail_more(&mut self, detail: &[u8]) {
730 self.sc.fails += 1;
731 let mut o = alloc::format!("FAIL line {}: ", self.sc.lineno).into_bytes();
732 o.extend_from_slice(&self.sc.line);
733 o.extend_from_slice(b" -> ");
734 o.extend_from_slice(detail);
735 plat::write_stdout(&o);
736 }
737
738 pub(crate) fn sel_fail(&mut self) {
740 let m: &[u8] = match self.sc.sel_err {
741 2 => b"ambiguous selector",
742 3 => b"bad selector",
743 _ => b"no such widget",
744 };
745 self.fail(m);
746 }
747
748 fn script_exec(&mut self, app: &mut dyn App, line: &[u8]) -> bool {
750 self.sc.line = line.to_vec(); let l = skip_ws(line);
752 if l.is_empty() || l[0] == b'#' {
753 return false;
754 }
755 const CMDS: [&str; 58] = [
756 "a11y", "quit", "print", "size", "theme", "move", "down", "up", "click", "rclick", "dblclick", "drag", "wheel",
757 "key", "keys", "keydown", "keyup", "type", "commit", "preedit", "cmd", "wait", "shot", "dump", "expect", "bench", "window", "close",
758 "movewin", "resizewin", "activate", "scale", "sync", "expect_warn", "expect_nowarn", "focus", "drop",
759 "clip", "pointer", "system", "dragover", "dragleave", "dragout", "screen", "perm", "agent", "inspect",
760 "style", "sensor", "location", "tap", "longpress", "swipe", "flick", "finger", "pinch", "pan2", "gpu",
761 ];
762 let Some(&c) = CMDS.iter().find(|c| word_eq(l, c).is_some()) else {
763 let mut o = alloc::format!("error: line {}: unknown command: ", self.sc.lineno).into_bytes();
764 o.extend_from_slice(l);
765 o.push(b'\n');
766 plat::write_stderr(&o);
767 self.sc.parse_err = true;
768 return false;
769 };
770 let a = next_word(l);
771 match c {
772 "quit" => return true,
773 "a11y" => self.sc_a11y(app, a), "system" => self.sc_system(app, a),
775 "sensor" => self.sc_sensor(app, a),
776 "location" => self.sc_location(app, a),
777 "print" => {
778 let mut o = a.to_vec();
779 o.push(b'\n');
780 plat::write_stdout(&o);
781 }
782 "size" => {
783 let (w, r) = parse_int(a);
784 let (h, _) = parse_int(r);
785 let (w, h) = (self.dp(w), self.dp(h));
786 self.resize(app, w, h);
787 }
788 "theme" => {
789 if let Some(r) = word_eq(a, "file") {
790 let path = String::from(String::from_utf8_lossy(skip_ws(r)).trim());
792 let res = super::perm::trusted(|| self.load_theme(&path));
793 self.sc.theme_err = res.err().map(|e| {
794 plat::write_stdout(alloc::format!("theme: {}\n", e).as_bytes());
795 e.msg
796 });
797 } else if word_eq(a, "none").is_some() {
798 self.clear_theme();
799 } else if word_eq(a, "contrast").is_some() {
800 self.set_contrast(Some(true));
801 } else {
802 self.set_contrast(Some(false));
803 let dark = word_eq(a, "dark").is_some();
804 self.theme_set(dark as u32);
805 }
806 self.cmd_run(UIC_THEME, 0);
807 self.run_passes(app);
808 }
809 "move" => match self.resolve(a).0 {
810 None => self.sel_fail(),
811 Some(_) => {
812 let (x, y) = (self.sc.sel_x, self.sc.sel_y);
813 self.mouse_to(app, x, y);
814 }
815 },
816 "down" => self.sc_button(app, a, plat::E_DOWN),
817 "up" => self.sc_button(app, a, plat::E_UP),
818 "click" => {
819 let (s, rest) = self.resolve(a);
820 if s.is_none() {
821 self.sel_fail();
822 } else {
823 let saved_mods = self.sc.script_mods;
825 let mut mods = saved_mods;
826 let mut b = BTN_LEFT;
827 let mut q = skip_ws(rest);
828 while !q.is_empty() {
829 let m = mod_word(q);
830 if m != 0 {
831 mods |= m;
832 } else {
833 b = parse_button(q);
834 }
835 q = next_word(q);
836 }
837 let (x, y) = (self.sc.sel_x, self.sc.sel_y);
838 self.mouse_to(app, x, y);
839 self.sc.script_mods = mods;
840 self.click_here(app, b);
841 self.sc.script_mods = saved_mods;
842 }
843 }
844 "rclick" => {
845 if self.resolve(a).0.is_none() {
846 self.sel_fail();
847 } else {
848 let (x, y) = (self.sc.sel_x, self.sc.sel_y);
849 self.mouse_to(app, x, y);
850 self.click_here(app, BTN_RIGHT);
851 }
852 }
853 "dblclick" => {
854 let (s, rest) = self.resolve(a);
855 if s.is_none() {
856 self.sel_fail();
857 } else {
858 let saved_mods = self.sc.script_mods;
860 let mut mods = saved_mods;
861 let mut q = skip_ws(rest);
862 while !q.is_empty() {
863 let m = mod_word(q);
864 if m == 0 {
865 let mut d = b"unknown dblclick option ".to_vec();
866 d.extend_from_slice(q);
867 self.fail(&d);
868 return false;
869 }
870 mods |= m;
871 q = next_word(q);
872 }
873 let (x, y) = (self.sc.sel_x, self.sc.sel_y);
874 self.mouse_to(app, x, y);
875 self.sc.script_mods = mods;
876 self.click_here(app, BTN_LEFT);
877 self.advance_time(app, 50);
878 self.click_here(app, BTN_LEFT);
879 self.sc.script_mods = saved_mods;
880 }
881 }
882 "drag" => self.sc_drag(app, a),
883 "wheel" => self.sc_wheel(app, a),
884 "tap" => self.sc_tap(app, a, 0),
885 "longpress" => self.sc_tap(app, a, 600),
886 "swipe" => self.sc_swipe(app, a),
887 "flick" => self.sc_flick(app, a),
888 "finger" => self.sc_finger(app, a),
889 "pinch" => self.sc_two(app, a, true),
890 "pan2" => self.sc_two(app, a, false),
891 "key" => self.sc_key(app, a),
892 "keydown" => self.sc_keyraw(app, a, true),
893 "keyup" => self.sc_keyraw(app, a, false),
894 "keys" => self.sc_keys(app, a),
895 "type" => self.sc_type(app, a),
896 "commit" => self.sc_commit(app, a),
897 "preedit" => self.sc_preedit(app, a),
898 "cmd" => {
899 let (id, r) = parse_int(a);
900 let (arg, _) = parse_int(r);
901 self.cmd_run(id as u32, arg as u32);
902 self.run_passes(app);
903 }
904 "wait" => {
905 if let Some(r) = word_eq(a, "real") {
906 let (ms, _) = parse_int(r);
907 self.wait_real(app, ms);
908 } else {
909 let (ms, _) = parse_int(a);
910 self.advance_time(app, ms);
911 }
912 }
913 "shot" => self.sc_shot(app, a),
914 "dump" => self.dump(if a.is_empty() { None } else { Some(a) }),
915 "expect" => self.sc_expect(a),
916 "bench" => self.sc_bench(app, a),
917 "window" => {
918 self.sc.sc_scope = if word_eq(a, "any").is_some() {
919 -1
920 } else if word_eq(a, "main").is_some() {
921 0
922 } else {
923 parse_int(a).0
924 };
925 }
926 "close" => self.sc_close(app, a),
927 "movewin" => {
928 let (i, r) = self.window_arg(a);
929 if i <= 0 {
930 self.fail(b"no such window");
931 } else {
932 let (x, r) = parse_int(r);
933 let (y, _) = parse_int(r);
934 let (x, y) = self.uiw_snap_dip(i as u32, x, y);
936 self.uiw_user_rect(i as u32, x, y, 0, 0);
937 self.run_passes(app);
938 }
939 }
940 "resizewin" => {
941 let (i, r) = self.window_arg(a);
942 if i < 0 {
943 self.fail(b"no such window");
944 } else {
945 let (w, r) = parse_int(r);
946 let (h, _) = parse_int(r);
947 if i == 0 {
948 let (w, h) = (self.dp(w), self.dp(h));
950 self.resize(app, w, h);
951 } else {
952 self.uiw_user_rect(i as u32, i32::MIN, 0, w, h);
953 self.run_passes(app);
954 }
955 }
956 }
957 "activate" => {
958 let (i, _) = self.window_arg(a);
959 if i < 0 {
960 self.fail(b"no such window");
961 } else {
962 let id = self.uiw_rec_id(i as u32);
963 self.window_activate(id);
964 self.run_passes(app);
965 }
966 }
967 "scale" => self.sc_scale(app, a),
968 "screen" => {
969 self.m.win.screen = if word_eq(a, "off").is_some() {
971 [0; 4]
972 } else {
973 let (x, r) = parse_int(a);
974 let (y, r) = parse_int(r);
975 let (w, r) = parse_int(r);
976 let (h, _) = parse_int(r);
977 [x, y, w.max(0), h.max(0)]
978 };
979 self.invalidate();
980 self.run_passes(app);
981 }
982 "sync" => self.sc_sync(app, a),
983 "style" => {
984 let name = String::from_utf8_lossy(skip_ws(a)).into_owned();
986 if self.set_style(name.trim()) {
987 self.run_passes(app);
988 } else {
989 self.fail(b"no such style");
990 }
991 }
992 "gpu" => self.sc_gpu(app, a), "perm" => self.sc_perm(app, a), "agent" => self.sc_agent(app, a), "inspect" => self.sc_inspect(app, a), "expect_warn" => {
997 let (w, m) = (self.warns, self.sc.warn_mark);
998 self.sc.warn_mark = w;
999 if w == m {
1000 self.fail(b"no warning");
1001 }
1002 }
1003 "expect_nowarn" => {
1004 let d = self.warns.wrapping_sub(self.sc.warn_mark);
1005 self.sc.warn_mark = self.warns;
1006 if d != 0 {
1007 self.fail(alloc::format!("{} warnings", d as i32).as_bytes());
1008 }
1009 }
1010 "focus" => match self.resolve(a).0 {
1011 None => self.sel_fail(),
1012 Some(s) => {
1013 let id = self.sel_rec(s).id;
1014 if id == 0 {
1015 self.fail(b"the widget has no id");
1016 } else {
1017 self.focus(id);
1018 self.run_passes(app);
1019 }
1020 }
1021 },
1022 "drop" => self.sc_drop(app, a),
1023 "dragover" => self.sc_dragover(app, a, false),
1024 "dragleave" => self.sc_dragover(app, a, true),
1025 "dragout" => self.sc_dragout(app, a),
1026 "clip" => self.sc_clip(app, a),
1027 "pointer" => self.sc_pointer(app, a),
1028 _ => {}
1029 }
1030 false
1031 }
1032
1033 pub(crate) fn sel_rec(&self, s: Sel) -> Sem {
1035 match s {
1036 Sel::Rec(i) => self.sems[i].clone(),
1037 Sel::Point => Sem::default(),
1038 }
1039 }
1040
1041 pub(crate) fn resolve<'a>(&mut self, p: &'a [u8]) -> (Option<Sel>, &'a [u8]) {
1047 self.sc.sel_err = 3;
1048 self.sc.sel_kind = -1;
1049 self.sc.sel_wf = self.scope_index(); self.sc.sel_win = 0;
1051 let c0 = p.first().copied().unwrap_or(0);
1052 if c0 == b'#' {
1053 return self.resolve_id(p);
1054 }
1055 let mut q = p;
1056 if c0 != b'"' {
1057 if c0.is_ascii_digit() || c0 == b'-' {
1058 return self.resolve_point(p);
1059 }
1060 let mut found = false;
1062 for k in 0..K_KIND_END {
1063 let n = kind_name(k);
1064 if p.starts_with(n.as_bytes()) && p.get(n.len()) == Some(&b':') {
1065 self.sc.sel_kind = k as i32;
1066 q = &p[n.len() + 1..];
1067 found = true;
1068 break;
1069 }
1070 }
1071 if !found || q.first() != Some(&b'"') {
1072 self.sc.sel_err = 3;
1073 return (None, p);
1074 }
1075 }
1076 self.resolve_label(p, q)
1077 }
1078
1079 fn resolve_point<'a>(&mut self, p: &'a [u8]) -> (Option<Sel>, &'a [u8]) {
1081 let (x, r) = parse_int(p);
1082 if r.first() != Some(&b',') {
1083 self.sc.sel_err = 3;
1084 return (None, p);
1085 }
1086 let (y, r) = parse_int(&r[1..]);
1087 let w = self.sc.sel_wf.max(0) as u32;
1088 self.sc.sel_win = w;
1089 let dpi = self.win_dpi(w);
1090 self.sc.sel_x = dp_at(x, dpi);
1091 self.sc.sel_y = dp_at(y, dpi);
1092 (Some(Sel::Point), r)
1093 }
1094
1095 fn sel_win_ok(&self, r: &Sem) -> bool {
1097 self.sc.sel_wf < 0 || self.sc.sel_wf as u8 == r.win
1098 }
1099
1100 fn resolve_id<'a>(&mut self, p: &'a [u8]) -> (Option<Sel>, &'a [u8]) {
1101 let q = &p[1..];
1102 let (id, rest) = if q.len() >= 2 && q[0] == b'0' && q[1] == b'x' {
1103 parse_hex(q)
1104 } else {
1105 let (v, r) = parse_int(q);
1106 (v as u32, r)
1107 };
1108 let mut mask = 0u32;
1110 for r in self.sems.iter().rev() {
1111 if r.id == id && self.sel_win_ok(r) {
1112 mask |= 1 << (r.win as u32 & (WIN_MAX as u32 - 1));
1113 }
1114 }
1115 if mask == 0 {
1116 self.sc.sel_err = 1;
1117 return (None, rest);
1118 }
1119 if mask & (mask - 1) != 0 {
1120 self.warn(&alloc::format!("duplicate id {} in several windows (mask {:x})", id, mask));
1121 self.sc.sel_wf = self.win_topmost(mask) as i32;
1122 }
1123 let found = (0..self.sems.len()).rev().find(|&i| self.sems[i].id == id && self.sel_win_ok(&self.sems[i]));
1125 match found {
1126 Some(i) => {
1127 self.sel_found(i);
1128 (Some(Sel::Rec(i)), rest)
1129 }
1130 None => {
1131 self.sc.sel_err = 1;
1132 (None, rest)
1133 }
1134 }
1135 }
1136
1137 fn label_match(&self, r: &Sem) -> bool {
1138 let Some(l) = r.label.as_deref() else { return false };
1139 if !self.sel_win_ok(r) {
1140 return false;
1141 }
1142 if self.sc.sel_kind >= 0 && r.kind as i32 != self.sc.sel_kind {
1143 return false;
1144 }
1145 l.as_bytes() == self.sc.sel_label.as_slice()
1146 }
1147
1148 fn resolve_label<'a>(&mut self, p: &'a [u8], q: &'a [u8]) -> (Option<Sel>, &'a [u8]) {
1149 let Some((label, rest)) = parse_qstring(q, LINE_MAX) else {
1150 self.sc.sel_err = 3;
1151 return (None, p);
1152 };
1153 self.sc.sel_label = label;
1154 let mask = self.label_windows();
1156 if mask & mask.wrapping_sub(1) != 0 {
1157 self.sc.sel_wf = self.win_topmost(mask) as i32;
1158 }
1159 let (mut n, mut nt) = (0, 0);
1161 let (mut last, mut last_nt) = (0, 0);
1162 for i in 0..self.sems.len() {
1163 if !self.label_match(&self.sems[i]) {
1164 continue;
1165 }
1166 n += 1;
1167 last = i;
1168 if self.sems[i].kind != K_TEXT {
1169 nt += 1;
1170 last_nt = i;
1171 }
1172 }
1173 if n == 0 {
1174 self.sc.sel_err = 1;
1175 return (None, rest);
1176 }
1177 let pick = if n == 1 {
1178 Some(last)
1179 } else if nt == 1 {
1180 Some(last_nt)
1181 } else {
1182 self.top_layer_match()
1185 };
1186 match pick {
1187 Some(i) => {
1188 self.sel_found(i);
1189 (Some(Sel::Rec(i)), rest)
1190 }
1191 None => {
1192 self.sc.sel_err = 2;
1193 (None, rest)
1194 }
1195 }
1196 }
1197
1198 fn sel_found(&mut self, i: usize) {
1199 let r = &self.sems[i];
1200 self.sc.sel_err = 0;
1201 self.sc.sel_win = r.win as u32 & (WIN_MAX as u32 - 1);
1202 self.sc.sel_x = (r.rect[2] >> 1).wrapping_add(r.rect[0]);
1203 self.sc.sel_y = (r.rect[3] >> 1).wrapping_add(r.rect[1]);
1204 }
1205
1206 fn top_layer_match(&self) -> Option<usize> {
1209 let mut top = -1;
1210 let mut n = 0;
1211 let mut m = 0;
1212 for i in 0..self.sems.len() {
1213 let r = &self.sems[i];
1214 if r.kind == K_TEXT || !self.label_match(r) {
1215 continue;
1216 }
1217 let l = r.layer as i32;
1218 if l < top {
1219 continue;
1220 }
1221 if l > top {
1222 top = l;
1223 n = 0;
1224 }
1225 n += 1;
1226 m = i;
1227 }
1228 if n == 1 { Some(m) } else { None }
1229 }
1230
1231 fn label_windows(&self) -> u32 {
1234 let (mut ctl, mut any) = (0u32, 0u32);
1235 for r in &self.sems {
1236 if !self.label_match(r) {
1237 continue;
1238 }
1239 let b = 1 << (r.win as u32 & (WIN_MAX as u32 - 1));
1240 any |= b;
1241 if r.kind != K_TEXT {
1242 ctl |= b;
1243 }
1244 }
1245 if ctl != 0 { ctl } else { any }
1246 }
1247
1248 fn win_topmost(&self, mask: u32) -> u32 {
1251 let modal = self.m.win.modal;
1252 if modal != 0 && mask & (1 << modal) != 0 {
1253 return modal;
1254 }
1255 let mut best = 0;
1256 let mut z: i64 = -1;
1257 for i in 0..WIN_MAX as u32 {
1258 if mask & (1 << i) == 0 {
1259 continue;
1260 }
1261 let wz = self.m.win.recs[i as usize].z as i32 as i64;
1262 if wz > z {
1263 z = wz;
1264 best = i;
1265 }
1266 }
1267 best
1268 }
1269
1270 fn scope_index(&self) -> i32 {
1272 if self.sc.sc_scope == -1 {
1273 return -1;
1274 }
1275 let i = self.uiw_index(self.sc.sc_scope as u32);
1276 if i < 0 { -2 } else { i }
1277 }
1278
1279 fn window_arg<'a>(&self, p: &'a [u8]) -> (i32, &'a [u8]) {
1281 let p = skip_ws(p);
1282 if let Some(r) = word_eq(p, "main") {
1283 return (0, r);
1284 }
1285 let (id, r) = parse_int(p);
1286 let i = self.uiw_index(id as u32);
1287 if i < 0 {
1288 return (i, r);
1289 }
1290 if self.m.win.recs[i as usize].state == 0 {
1291 return (-1, r);
1292 }
1293 (i, r)
1294 }
1295
1296 fn sc_system(&mut self, app: &mut dyn App, a: &[u8]) {
1306 let w = |n: &str| word_eq(a, n).is_some();
1307 if w("env") {
1308 let r = next_word(a);
1309 let name_len = r.iter().position(|&c| c == b' ' || c == b'\t').unwrap_or(r.len());
1310 let name = String::from_utf8_lossy(&r[..name_len]).into_owned();
1311 let value = String::from(String::from_utf8_lossy(skip_ws(&r[name_len..])).trim_end());
1312 if !name.starts_with("jfgui_") || value.is_empty() {
1313 self.fail(b"system env: a jfgui_* NAME and a VALUE (- removes it)");
1314 return;
1315 }
1316 plat::setenv(&name, if value == "-" { None } else { Some(&value) });
1317 if name == "jfgui_FONT_FALLBACK" {
1318 self.text.set_system_fallback(value.starts_with('1'));
1321 self.dirty_force_full();
1322 }
1323 self.run_passes(app);
1324 return;
1325 }
1326 if w("browser") {
1327 let r = next_word(a);
1329 let s = if word_eq(r, "default").is_some() {
1330 plat::BrowserStatus::Default
1331 } else if word_eq(r, "other").is_some() {
1332 plat::BrowserStatus::NotDefault
1333 } else if word_eq(r, "unknown").is_some() {
1334 plat::BrowserStatus::Unknown
1335 } else {
1336 self.fail(b"system browser: default, other or unknown");
1337 return;
1338 };
1339 super::assoc::browser_script_set(s);
1340 let had = self.app_focus;
1344 let back = if had != 0 { had.trailing_zeros() } else { self.sc.sc_win };
1345 let mut f = had;
1346 while f != 0 {
1347 let w = f.trailing_zeros();
1348 f &= f - 1;
1349 self.push_window_event(plat::E_FOCUS_OUT, w);
1350 }
1351 if had != 0 {
1352 self.process_events(app);
1353 }
1354 self.push_window_event(plat::E_FOCUS_IN, back);
1355 self.process_events(app);
1356 self.run_passes(app);
1357 return;
1358 }
1359 if w("fullscreen") {
1360 let r = next_word(a);
1361 let on = word_eq(r, "on").is_some();
1362 if !on && word_eq(r, "off").is_none() {
1363 self.fail(b"system fullscreen: on or off");
1364 return;
1365 }
1366 let rest = next_word(r);
1367 let i = if skip_ws(rest).is_empty() { 0 } else { self.window_arg(rest).0 };
1368 if i < 0 {
1369 self.fail(b"no such window");
1370 return;
1371 }
1372 if self.m.win.recs[i as usize].fs != on {
1373 self.uiw_fs_changed(i as u32, on);
1374 }
1375 self.run_passes(app);
1376 return;
1377 }
1378 let (typ, data) = if w("focus") {
1379 let r = next_word(a);
1380 (if word_eq(r, "out").is_some() { plat::E_FOCUS_OUT } else { plat::E_FOCUS_IN }, 0)
1381 } else if w("suspend") {
1382 (plat::E_POWER, 1)
1383 } else if w("resume") {
1384 (plat::E_POWER, 0)
1385 } else if w("background") {
1386 (plat::E_POWER, 2) } else if w("foreground") {
1388 (plat::E_POWER, 3)
1389 } else if w("session-end") {
1390 (plat::E_SESSION_END, 0)
1391 } else if w("settings") {
1392 let r = next_word(a);
1393 let bits = [("theme", plat::SET_THEME), ("locale", plat::SET_LOCALE), ("contrast", plat::SET_CONTRAST), ("fonts", plat::SET_FONTS), ("other", plat::SET_OTHER), ("monitors", plat::SET_MONITORS)]
1394 .iter()
1395 .find(|(n, _)| word_eq(r, n).is_some())
1396 .map_or_else(|| parse_int(r).0 as u32, |(_, b)| *b);
1397 (plat::E_SETTINGS, bits)
1398 } else if w("hotkey") {
1399 let id = parse_int(next_word(a)).0 as u32;
1401 if !plat::hotkey::hotkey_test_press(id) {
1402 self.fail(b"system hotkey: no global shortcut with this id");
1403 return;
1404 }
1405 self.inject(app, plat::E_WAKE, 0, 0, 0, 0);
1406 self.run_passes(app);
1407 return;
1408 } else if w("pick") {
1409 let r = next_word(a);
1411 if !self.m.screen.picking {
1412 self.fail(b"system pick: no colour pick is running");
1413 return;
1414 }
1415 let ans = if word_eq(r, "cancel").is_some() { Err(plat::screen::ScreenError::Cancelled) } else { Ok(super::runtime::parse_hex(r).0 & 0xFF_FFFF) };
1416 self.pick_done(ans);
1417 self.run_passes(app);
1418 return;
1419 } else {
1420 self.fail(b"system: focus in|out, suspend, resume, session-end, settings WHAT, hotkey ID, pick RRGGBB|cancel or fullscreen on|off");
1421 return;
1422 };
1423 self.inject(app, typ, 0, 0, 0, data);
1424 }
1425
1426 fn push_window_event(&mut self, typ: u8, w: u32) {
1428 let ev = Event { typ, btn: 0, mods: self.sc.script_mods as u8, win: (w & (WIN_MAX as u32 - 1)) as u8, x: 0, y: 0, data: 0 };
1429 plat::evq_push(ev);
1430 }
1431
1432 fn inject(&mut self, app: &mut dyn App, typ: u8, btn: u32, x: i32, y: i32, data: u32) {
1433 let w = if matches!(typ, plat::E_KEY_DOWN | plat::E_KEY_UP | plat::E_TEXT) { self.m.win.active } else { self.sc.sc_win };
1434 let finger = matches!(typ, plat::E_MOVE | plat::E_DOWN | plat::E_UP) && (data >> plat::PD_TYPE_SHIFT) & 3 == PTR_TOUCH;
1436 let btn = if finger { self.sc.finger_id } else { btn };
1437 let ev = Event {
1438 typ,
1439 btn: btn as u8,
1440 mods: self.sc.script_mods as u8,
1441 win: (w & (WIN_MAX as u32 - 1)) as u8,
1442 x,
1443 y,
1444 data,
1445 };
1446 plat::evq_push(ev);
1447 self.process_events(app);
1448 }
1449
1450 fn mouse_to(&mut self, app: &mut dyn App, x: i32, y: i32) {
1452 self.sc.sc_win = self.sc.sel_win;
1453 self.sc.sc_x = x;
1454 self.sc.sc_y = y;
1455 let d = self.sc.sc_pdata;
1456 self.inject(app, plat::E_MOVE, 0, x, y, d);
1457 }
1458
1459 fn click_here(&mut self, app: &mut dyn App, btn: u32) {
1461 let (x, y, d) = (self.sc.sc_x, self.sc.sc_y, self.sc.sc_pdata);
1462 self.inject(app, plat::E_DOWN, btn, x, y, d);
1463 let (x, y, d) = (self.sc.sc_x, self.sc.sc_y, self.sc.sc_pdata);
1464 self.inject(app, plat::E_UP, btn, x, y, d);
1465 }
1466
1467 fn sc_button(&mut self, app: &mut dyn App, a: &[u8], typ: u8) {
1469 let mut rest = a;
1470 let c = a.first().map(|c| c | 32);
1471 if !a.is_empty() && !matches!(c, Some(b'l' | b'r' | b'm')) {
1472 let (s, r) = self.resolve(a);
1473 if s.is_none() {
1474 self.sel_fail();
1475 return;
1476 }
1477 let (x, y) = (self.sc.sel_x, self.sc.sel_y);
1478 self.mouse_to(app, x, y);
1479 rest = skip_ws(r);
1480 }
1481 let b = parse_button(rest);
1482 let (x, y, d) = (self.sc.sc_x, self.sc.sc_y, self.sc.sc_pdata);
1483 self.inject(app, typ, b, x, y, d);
1484 }
1485
1486 fn sc_drag(&mut self, app: &mut dyn App, a: &[u8]) {
1488 let mut pen_type = 0u32;
1489 let (mut p0, mut p1) = (0i32, 0i32);
1490 let saved_pdata = self.sc.sc_pdata;
1491 let (s, r) = self.resolve(a);
1492 if s.is_none() {
1493 self.sel_fail();
1494 return;
1495 }
1496 let (bx, by) = (self.sc.sel_x, self.sc.sel_y);
1497 let press_win = self.sc.sel_win; let (s, r) = self.resolve(skip_ws(r));
1499 if s.is_none() {
1500 self.sel_fail();
1501 return;
1502 }
1503 self.point_to_window(self.sc.sel_win, press_win);
1505 self.sc.sel_win = press_win;
1506 let (ex, ey) = (self.sc.sel_x, self.sc.sel_y);
1507 let mut q = skip_ws(r);
1508 let mut steps = 8;
1509 if q.first().is_some_and(|c| c.is_ascii_digit()) {
1511 let (n, r) = parse_int(q);
1512 if n > 0 {
1513 steps = n;
1514 }
1515 q = r;
1516 }
1517 let mut btn = BTN_LEFT;
1519 let mut hold = 0;
1520 let saved_mods = self.sc.script_mods;
1521 let mut mods = saved_mods;
1522 loop {
1523 let w = skip_ws(q);
1524 if w.is_empty() {
1525 break;
1526 }
1527 let mut after = w;
1528 let m = mod_word(w);
1529 if m != 0 {
1530 mods |= m;
1531 } else if w.starts_with(b"right") {
1532 btn = BTN_RIGHT;
1533 } else if w.starts_with(b"middle") {
1534 btn = BTN_MIDDLE;
1535 } else if w.starts_with(b"pen") || w.starts_with(b"eraser") {
1536 pen_type = if w.starts_with(b"pen") { PTR_PEN } else { PTR_ERASER };
1538 let (a0, r) = parse_pct(next_word(w));
1539 let (a1, r) = parse_pct(skip_ws(r));
1540 p0 = a0;
1541 p1 = a1;
1542 after = r;
1543 } else if let Some(r) = word_eq(w, "hold") {
1544 let (ms, r) = parse_int(skip_ws(r));
1545 hold = ms;
1546 after = r;
1547 } else {
1548 let mut d = b"unknown drag option ".to_vec();
1549 d.extend_from_slice(w);
1550 self.fail(&d);
1551 return;
1552 }
1553 let i = after.iter().position(|&c| c <= 32).unwrap_or(after.len());
1555 q = &after[i..];
1556 }
1557 self.sc.script_mods = mods;
1558 if pen_type != 0 {
1559 self.sc.sc_pdata = plat::evq_pen_data(p0, pen_type, 0, 0); }
1561 self.mouse_to(app, bx, by);
1562 let d = self.sc.sc_pdata;
1563 self.inject(app, plat::E_DOWN, btn, bx, by, d);
1564 self.advance_time(app, hold);
1565 for i in 1..=steps {
1566 if pen_type != 0 {
1567 let pr = (p1 - p0).wrapping_mul(i) / steps + p0;
1569 self.sc.sc_pdata = plat::evq_pen_data(pr, pen_type, 0, 0);
1570 }
1571 let x = bx + (ex - bx).wrapping_mul(i) / steps;
1573 let y = by + (ey - by).wrapping_mul(i) / steps;
1574 self.mouse_to(app, x, y);
1575 self.advance_time(app, 16);
1576 }
1577 let d = self.sc.sc_pdata;
1578 self.inject(app, plat::E_UP, btn, ex, ey, d);
1579 self.sc.script_mods = saved_mods;
1580 self.sc.sc_pdata = saved_pdata; }
1582
1583 fn finger_at(&mut self, app: &mut dyn App, typ: u8, x: i32, y: i32) {
1585 self.sc.sc_win = self.sc.sel_win;
1586 self.sc.sc_x = x;
1587 self.sc.sc_y = y;
1588 let btn = if typ == plat::E_MOVE { 0 } else { BTN_LEFT };
1589 self.inject(app, typ, btn, x, y, PTR_TOUCH << plat::PD_TYPE_SHIFT);
1590 }
1591
1592 fn sc_tap(&mut self, app: &mut dyn App, a: &[u8], rest_ms: i32) {
1594 let (s, r) = self.resolve(a);
1595 if s.is_none() {
1596 self.sel_fail();
1597 return;
1598 }
1599 let (x, y) = (self.sc.sel_x, self.sc.sel_y);
1600 let q = skip_ws(r);
1601 let ms = if rest_ms > 0 && q.first().is_some_and(|c| c.is_ascii_digit()) { parse_int(q).0 } else { rest_ms };
1602 self.finger_at(app, plat::E_DOWN, x, y);
1603 self.advance_time(app, ms);
1604 self.finger_at(app, plat::E_UP, x, y);
1605 }
1606
1607 fn finger_path(&mut self, app: &mut dyn App, b: [i32; 2], e: [i32; 2], ms: i32, hold: bool) {
1609 self.finger_at(app, plat::E_DOWN, b[0], b[1]);
1610 let ms = ms.max(1);
1611 let steps = (ms / 8).max(1);
1612 for i in 1..=steps {
1613 self.advance_time(app, ms * i / steps - ms * (i - 1) / steps);
1614 let x = b[0] + (e[0] - b[0]).wrapping_mul(i) / steps;
1615 let y = b[1] + (e[1] - b[1]).wrapping_mul(i) / steps;
1616 self.finger_at(app, plat::E_MOVE, x, y);
1617 }
1618 if hold {
1619 self.advance_time(app, 100);
1620 }
1621 self.finger_at(app, plat::E_UP, e[0], e[1]);
1622 }
1623
1624 fn sc_swipe(&mut self, app: &mut dyn App, a: &[u8]) {
1626 let (s, r) = self.resolve(a);
1627 if s.is_none() {
1628 self.sel_fail();
1629 return;
1630 }
1631 let b = [self.sc.sel_x, self.sc.sel_y];
1632 let press_win = self.sc.sel_win;
1633 let (s, r) = self.resolve(skip_ws(r));
1634 if s.is_none() {
1635 self.sel_fail();
1636 return;
1637 }
1638 self.point_to_window(self.sc.sel_win, press_win);
1640 self.sc.sel_win = press_win;
1641 let e = [self.sc.sel_x, self.sc.sel_y];
1642 let mut q = skip_ws(r);
1643 let mut ms = 300;
1644 if q.first().is_some_and(|c| c.is_ascii_digit()) {
1645 let (n, r) = parse_int(q);
1646 ms = n;
1647 q = skip_ws(r);
1648 }
1649 self.finger_path(app, b, e, ms, word_eq(q, "hold").is_some());
1650 }
1651
1652 fn sc_flick(&mut self, app: &mut dyn App, a: &[u8]) {
1654 let (s, r) = self.resolve(a);
1655 if s.is_none() {
1656 self.sel_fail();
1657 return;
1658 }
1659 let b = [self.sc.sel_x, self.sc.sel_y];
1660 let (dx, r) = parse_int(r);
1661 let (dy, r) = parse_int(r);
1662 let q = skip_ws(r);
1663 let ms = if q.first().is_some_and(|c| c.is_ascii_digit()) { parse_int(q).0 } else { 60 };
1664 let dpi = self.win_dpi(self.sc.sel_win);
1665 let e = [b[0] + dp_at(dx, dpi), b[1] + dp_at(dy, dpi)];
1666 self.finger_path(app, b, e, ms, false);
1667 }
1668
1669 fn sc_finger(&mut self, app: &mut dyn App, a: &[u8]) {
1671 if a.first().is_some_and(|c| c.is_ascii_digit()) {
1672 let (n, r) = parse_int(a);
1673 self.sc.finger_id = n as u32;
1674 self.sc_finger(app, skip_ws(r));
1675 self.sc.finger_id = 0;
1676 return;
1677 }
1678 if word_eq(a, "up").is_some() || word_eq(a, "cancel").is_some() {
1679 let (x, y) = (self.sc.sc_x, self.sc.sc_y);
1680 let cancel = word_eq(a, "cancel").is_some();
1681 self.sc.sel_win = self.sc.sc_win;
1682 if cancel {
1683 self.inject(app, plat::E_UP, 0, x, y, PTR_TOUCH << plat::PD_TYPE_SHIFT | plat::PD_CANCEL);
1684 } else {
1685 self.finger_at(app, plat::E_UP, x, y);
1686 }
1687 return;
1688 }
1689 let (typ, s) = if let Some(r) = word_eq(a, "down") {
1690 (plat::E_DOWN, r)
1691 } else if let Some(r) = word_eq(a, "move") {
1692 (plat::E_MOVE, r)
1693 } else {
1694 self.fail(b"finger: down S, move S, up or cancel");
1695 return;
1696 };
1697 if self.resolve(skip_ws(s)).0.is_none() {
1698 self.sel_fail();
1699 return;
1700 }
1701 if typ == plat::E_MOVE {
1702 self.point_to_window(self.sc.sel_win, self.sc.sc_win);
1704 self.sc.sel_win = self.sc.sc_win;
1705 }
1706 let (x, y) = (self.sc.sel_x, self.sc.sel_y);
1707 self.finger_at(app, typ, x, y);
1708 }
1709
1710 fn sc_two(&mut self, app: &mut dyn App, a: &[u8], pinch: bool) {
1712 let (s, r) = self.resolve(a);
1713 if s.is_none() {
1714 self.sel_fail();
1715 return;
1716 }
1717 let c = [self.sc.sel_x, self.sc.sel_y];
1718 let (p0, r) = parse_int(r);
1719 let (p1, r) = parse_int(r);
1720 let q = skip_ws(r);
1721 let ms = if q.first().is_some_and(|c| c.is_ascii_digit()) { parse_int(q).0 } else { 300 }.max(1);
1722 let dpi = self.win_dpi(self.sc.sel_win);
1723 let (h0, h1, way) = if pinch {
1725 (dp_at(p0, dpi) / 2, dp_at(p1, dpi) / 2, [0, 0])
1726 } else {
1727 (dp_at(40, dpi), dp_at(40, dpi), [dp_at(p0, dpi), dp_at(p1, dpi)])
1728 };
1729 self.two_at(app, plat::E_DOWN, c, h0);
1730 let steps = (ms / 8).max(1);
1731 for i in 1..=steps {
1732 self.advance_time(app, ms * i / steps - ms * (i - 1) / steps);
1733 let h = h0 + (h1 - h0) * i / steps;
1734 self.two_at(app, plat::E_MOVE, [c[0] + way[0] * i / steps, c[1] + way[1] * i / steps], h);
1735 }
1736 self.two_at(app, plat::E_UP, [c[0] + way[0], c[1] + way[1]], h1);
1737 }
1738
1739 fn two_at(&mut self, app: &mut dyn App, typ: u8, c: [i32; 2], h: i32) {
1741 for (id, sign) in [(0u32, -1), (1, 1)] {
1742 self.sc.finger_id = id;
1743 self.finger_at(app, typ, c[0] + sign * h, c[1]);
1744 }
1745 self.sc.finger_id = 0;
1746 }
1747
1748 fn sc_wheel(&mut self, app: &mut dyn App, a: &[u8]) {
1751 let (s, r) = self.resolve(a);
1752 if s.is_none() {
1753 self.sel_fail();
1754 return;
1755 }
1756 let (dy, r) = parse_int(r);
1757 let saved_mods = self.sc.script_mods;
1758 let mut mods = saved_mods;
1759 let mut q = skip_ws(r);
1760 while !q.is_empty() {
1761 let m = mod_word(q);
1762 if m == 0 {
1763 let mut d = b"unknown wheel option ".to_vec();
1764 d.extend_from_slice(q);
1765 self.fail(&d);
1766 return;
1767 }
1768 mods |= m;
1769 q = next_word(q);
1770 }
1771 let (x, y) = (self.sc.sel_x, self.sc.sel_y);
1772 self.mouse_to(app, x, y);
1773 let (sx, sy) = (self.sc.sc_x, self.sc.sc_y);
1774 self.sc.script_mods = mods;
1775 self.inject(app, plat::E_WHEEL, 0, sx, sy, dy as u16 as u32);
1776 self.sc.script_mods = saved_mods;
1777 }
1778
1779 fn point_to_window(&mut self, from: u32, to: u32) {
1781 if from == to {
1782 return;
1783 }
1784 let dpi = self.win_dpi(from);
1785 let (dx, dy) = self.uiw_desk_pos(from);
1786 let x = dx + px2dip_at(self.sc.sel_x, dpi);
1787 let y = dy + px2dip_at(self.sc.sel_y, dpi);
1788 let (tx, ty) = self.uiw_desk_pos(to);
1789 let tdpi = self.win_dpi(to);
1790 self.sc.sel_x = dp_at(x - tx, tdpi);
1791 self.sc.sel_y = dp_at(y - ty, tdpi);
1792 }
1793
1794 fn sc_pointer(&mut self, app: &mut dyn App, a: &[u8]) {
1797 let names = ["mouse", "pen", "eraser", "touch"];
1798 let Some(t) = names.iter().position(|n| word_eq(a, n).is_some()) else {
1799 let mut d = b"unknown pointer type ".to_vec();
1800 d.extend_from_slice(a);
1801 self.fail(&d);
1802 return;
1803 };
1804 let mut pressure = -1;
1805 let (mut tx, mut ty) = (0, 0);
1806 let q = next_word(a);
1807 if q.first().is_some_and(|c| c.is_ascii_digit()) {
1808 let (pv, r) = parse_pct(q);
1809 pressure = pv;
1810 let r = skip_ws(r);
1811 if r.first().is_some_and(|&c| c == b'-' || c.is_ascii_digit()) {
1812 let (x, r) = parse_int(r);
1813 let (y, _) = parse_int(skip_ws(r));
1814 tx = x;
1815 ty = y;
1816 }
1817 }
1818 self.sc.sc_pdata = plat::evq_pen_data(pressure, t as u32, tx, ty);
1819 if self.m.touch.on != (t as u32 == PTR_TOUCH) {
1821 self.touch_mode_set(t as u32 == PTR_TOUCH);
1822 self.run_passes(app);
1823 }
1824 }
1825
1826 fn sc_key(&mut self, app: &mut dyn App, a: &[u8]) {
1828 let Some(keys) = self.chords(a) else { return };
1829 for k in keys {
1830 self.press(app, k >> 16, k & 0xFFFF);
1831 }
1832 }
1833
1834 fn sc_keys(&mut self, app: &mut dyn App, a: &[u8]) {
1837 let (n, r) = if a.first().is_some_and(|c| c.is_ascii_digit()) { parse_int(a) } else { (1, a) };
1838 let Some(keys) = self.chords(r) else { return };
1839 let w = self.m.win.active & (WIN_MAX as u32 - 1);
1840 if w != 0 && !self.uiw_drawable(w) {
1841 self.fail(b"the window with the keyboard is not shown");
1842 return;
1843 }
1844 let all: Vec<u32> = (0..n.max(0)).flat_map(|_| keys.iter().copied()).collect();
1845 self.uiw_use(w);
1846 self.keys_inject(&all);
1847 self.m.win.edges |= 1 << w; self.uiw_use(0);
1849 self.run_passes(app);
1850 }
1851
1852 fn chords(&mut self, a: &[u8]) -> Option<Vec<u32>> {
1855 let mut out = Vec::new();
1856 let mut p = a;
1857 loop {
1858 p = skip_ws(p);
1859 if p.is_empty() {
1860 return Some(out);
1861 }
1862 let Some((mods, vk, rest)) = chord(p) else {
1863 self.fail(b"unknown key");
1864 return None;
1865 };
1866 out.push(vk | mods << 16);
1867 p = rest;
1868 }
1869 }
1870
1871 fn press(&mut self, app: &mut dyn App, mods: u32, vk: u32) {
1873 let held = self.sc.script_mods;
1874 self.sc.script_mods = held | mods;
1875 self.inject(app, plat::E_KEY_DOWN, 0, 0, 0, vk);
1876 self.inject(app, plat::E_KEY_UP, 0, 0, 0, vk);
1877 self.sc.script_mods = held;
1878 }
1879
1880 fn sc_keyraw(&mut self, app: &mut dyn App, a: &[u8], down: bool) {
1883 let Some((mods, vk, rest)) = chord(skip_ws(a)) else {
1884 self.fail(b"unknown key");
1885 return;
1886 };
1887 let (mut scan, mut loc) = (0i32, KL_STANDARD);
1888 let mut q = skip_ws(rest);
1889 while !q.is_empty() {
1890 if let Some(r) = word_eq(q, "scan") {
1891 let (n, r) = parse_int(skip_ws(r));
1892 scan = n;
1893 q = skip_ws(r);
1894 continue;
1895 }
1896 loc = if word_eq(q, "left").is_some() {
1897 KL_LEFT
1898 } else if word_eq(q, "right").is_some() {
1899 KL_RIGHT
1900 } else if word_eq(q, "numpad").is_some() {
1901 KL_NUMPAD
1902 } else {
1903 let mut d = b"unknown keydown / keyup option ".to_vec();
1904 d.extend_from_slice(q);
1905 self.fail(&d);
1906 return;
1907 };
1908 q = next_word(q);
1909 }
1910 let own = match vk {
1911 VK_SHIFT => MOD_SHIFT,
1912 VK_CONTROL => MOD_CTRL,
1913 VK_MENU => MOD_ALT,
1914 _ => 0,
1915 };
1916 let held = self.sc.script_mods;
1917 if down {
1918 let repeat = if self.keydown[(vk & 255) as usize] != 0 { 1 << 31 } else { 0 };
1920 self.sc.script_mods = held | own | mods;
1921 self.inject(app, plat::E_KEY_DOWN, 0, scan, loc as i32, vk | repeat);
1922 self.sc.script_mods = held | own;
1923 } else {
1924 self.sc.script_mods = (held | mods) & !own;
1925 self.inject(app, plat::E_KEY_UP, 0, scan, loc as i32, vk);
1926 self.sc.script_mods = held & !own;
1927 }
1928 }
1929
1930 fn sc_commit(&mut self, app: &mut dyn App, a: &[u8]) {
1933 let Some((s, r)) = parse_qstring(a, 1024) else {
1934 self.fail(b"expected a quoted string");
1935 return;
1936 };
1937 let text = String::from_utf8_lossy(&s).repeat(parse_int(r).0.max(1) as usize);
1938 let w = (self.m.win.active & (WIN_MAX as u32 - 1)) as u8;
1939 let mut at = 0;
1940 while at < text.len() {
1941 let n = plat::ime_commit_text(w, &text[at..]);
1942 if n == 0 {
1943 self.fail(b"the commit was not queued");
1944 return;
1945 }
1946 at += n;
1947 self.process_events(app);
1948 }
1949 }
1950
1951 fn sc_preedit(&mut self, app: &mut dyn App, a: &[u8]) {
1954 let Some((s, r)) = parse_qstring(a, 1024) else {
1955 self.fail(b"expected a quoted string");
1956 return;
1957 };
1958 let text = String::from_utf8_lossy(&s).into_owned();
1959 let r = skip_ws(r);
1960 let cursor = if r.first().is_some_and(|c| c.is_ascii_digit()) { parse_int(r).0.max(0) as usize } else { text.len() };
1961 let w = (self.m.win.active & (WIN_MAX as u32 - 1)) as u8;
1962 plat::ime_preedit_set(w, &text, cursor);
1963 self.process_events(app);
1964 }
1965
1966 fn sc_type(&mut self, app: &mut dyn App, a: &[u8]) {
1968 let Some((s, _)) = parse_qstring(a, 256) else {
1969 self.fail(b"expected a quoted string");
1970 return;
1971 };
1972 let mut i = 0;
1973 loop {
1974 let (cp, n) = utf8_decode(&s, i);
1975 if cp == 0 {
1976 break;
1977 }
1978 i = n;
1979 if cp == 10 {
1980 self.inject(app, plat::E_KEY_DOWN, 0, 0, 0, VK_RETURN);
1981 self.inject(app, plat::E_KEY_UP, 0, 0, 0, VK_RETURN);
1982 } else {
1983 self.inject(app, plat::E_TEXT, 0, 0, 0, cp);
1984 }
1985 }
1986 }
1987
1988 fn advance_time(&mut self, app: &mut dyn App, ms: i32) {
1990 let mut left = ms;
1991 while left > 0 {
1992 let step = left.min(16);
1993 left -= step;
1994 let t = self.now_ms.wrapping_add(step as u32);
1995 self.set_time(t);
1996 if self.touch_busy() {
1997 self.process_events(app); }
1999 let mut fired = self.tasks_ready(); loop {
2001 let (id, _) = self.timer_due();
2002 if id == u32::MAX {
2003 break;
2004 }
2005 self.cmd_run(UIC_TIMER, id);
2006 fired = true;
2007 }
2008 if self.animating || fired || self.need {
2010 self.need = false;
2011 self.run_passes(app);
2012 }
2013 }
2014 }
2015
2016 fn sc_shot(&mut self, app: &mut dyn App, a: &[u8]) {
2018 let _ = app;
2019 let i = a.iter().position(|&c| c == b' ' || c == b'\t').unwrap_or(a.len());
2021 let file = String::from_utf8_lossy(&a[..i]).into_owned();
2022 let mut rest = skip_ws(if i < a.len() { &a[i + 1..] } else { &a[i..] });
2023 let mut win = 0u32;
2024 let mut desktop = false;
2025 if let Some(r) = word_eq(rest, "desktop") {
2026 desktop = true;
2027 rest = r;
2028 } else if let Some(r) = word_eq(rest, "window") {
2029 let (w, r) = self.window_arg(r);
2030 if w < 0 {
2031 self.fail(b"no such window");
2032 return;
2033 }
2034 win = w as u32;
2035 rest = r;
2036 }
2037 let mut crop = None;
2038 let q = skip_ws(rest);
2039 if !q.is_empty() {
2040 let (x, r) = parse_int(q);
2041 let (y, r) = parse_int(r);
2042 let (w, r) = parse_int(r);
2043 let (h, _) = parse_int(r);
2044 crop = Some([x, y, w, h]);
2045 }
2046 self.render();
2047 self.gp_settle_all();
2048 let ok = if desktop {
2049 self.shot_desktop(&file, crop)
2050 } else {
2051 let sf = self.win_fb(win);
2052 match crop {
2053 Some(c) => match crop_surface(sf, self.win_dpi(win), c) {
2054 Some(v) => Ui::save_png(&v, &file),
2055 None => false,
2056 },
2057 None => Ui::save_png(sf, &file),
2058 }
2059 };
2060 if !ok {
2061 self.fail(b"cannot write the screenshot");
2062 return;
2063 }
2064 plat::write_stdout(alloc::format!("wrote {}\n", file).as_bytes());
2065 }
2066
2067 fn shot_desktop(&mut self, path: &str, crop: Option<[i32; 4]>) -> bool {
2071 let dpi = self.wp.rt.dpi; let mut bb = [0, 0, px2dip_at(self.wp.rt.fb.w, dpi), px2dip_at(self.wp.rt.fb.h, dpi)];
2073 let scr = self.m.win.screen;
2074 if scr[2] > 0 && scr[3] > 0 {
2075 bb = [bb[0].min(scr[0]), bb[1].min(scr[1]), bb[2].max(scr[0] + scr[2]), bb[3].max(scr[1] + scr[3])];
2076 }
2077 for i in 1..WIN_MAX as u32 {
2078 if !self.uiw_drawable(i) {
2079 continue;
2080 }
2081 let r = self.win_desk_rect(i);
2082 bb[0] = bb[0].min(r[0]);
2083 bb[1] = bb[1].min(r[1]);
2084 bb[2] = bb[2].max(r[0] + r[2]);
2085 bb[3] = bb[3].max(r[1] + r[3]);
2086 }
2087 let mut sf = Surface::new(dp_at(bb[2] - bb[0], dpi), dp_at(bb[3] - bb[1], dpi), FMT_XRGB);
2088 if sf.px.is_empty() {
2089 return false;
2090 }
2091 let mut dc = gfx::Dc::new(sf.w, sf.h);
2092 dc.set_color(0xFF5A_6B7C); let (sw, sh) = (sf.w, sf.h);
2094 dc.fill(&mut sf, 0, 0, sw, sh);
2095 self.sd_blit(&mut sf, 0, bb[0], bb[1], dpi);
2097 let mut last_z: i64 = -1;
2098 loop {
2099 let mut next = None;
2100 let mut nz: i64 = i32::MAX as i64;
2101 for i in 1..WIN_MAX as u32 {
2102 if !self.uiw_drawable(i) {
2103 continue;
2104 }
2105 let z = self.m.win.recs[i as usize].z as i32 as i64;
2106 if z > last_z && z < nz {
2107 nz = z;
2108 next = Some(i);
2109 }
2110 }
2111 let Some(i) = next else { break };
2112 last_z = nz;
2113 self.sd_blit(&mut sf, i, bb[0], bb[1], dpi);
2114 self.uiw_popups_blit(&mut sf, i, bb[0], bb[1], dpi); }
2116 self.uiw_popups_blit(&mut sf, 0, bb[0], bb[1], dpi); match crop {
2118 Some(c) => match crop_surface(&sf, dpi, c) {
2119 Some(v) => Ui::save_png(&v, path),
2120 None => false,
2121 },
2122 None => Ui::save_png(&sf, path),
2123 }
2124 }
2125
2126 fn sd_blit(&mut self, sf: &mut Surface, i: u32, x0: i32, y0: i32, dpi: i32) {
2128 let r = self.win_desk_rect(i);
2129 let x = dp_at(r[0] - x0, dpi);
2130 let y = dp_at(r[1] - y0, dpi);
2131 blit_copy(sf, self.win_fb(i), x, y);
2132 }
2133
2134 fn win_desk_rect(&self, i: u32) -> [i32; 4] {
2136 let (x, y) = self.uiw_desk_pos(i);
2137 let dpi = self.win_dpi(i);
2138 let fb = self.win_fb(i);
2139 [x, y, px2dip_at(fb.w, dpi), px2dip_at(fb.h, dpi)]
2140 }
2141
2142 fn sc_expect(&mut self, a: &[u8]) {
2144 if let Some(r) = word_eq(a, "window") {
2145 self.expect_window(r);
2146 return;
2147 }
2148 if let Some(r) = word_eq(a, "sound") {
2149 let want = skip_ws(r);
2151 let got = self.m.sound.last.take();
2152 let name = got.map_or("none", plat::audio::sound_name);
2153 if !want.starts_with(name.as_bytes()) || (want.len() > name.len() && want[name.len()] > b' ') {
2154 self.fail(alloc::format!("sound {name}").as_bytes());
2155 }
2156 return;
2157 }
2158 if let Some(r) = word_eq(a, "keyboard") {
2159 let on = self.m.touch.kb_on;
2161 if word_eq(skip_ws(r), "shown").is_some() != on {
2162 self.fail(if on { b"keyboard shown" } else { b"keyboard hidden" });
2163 }
2164 return;
2165 }
2166 if let Some(r) = word_eq(a, "wm") {
2167 let want = String::from(String::from_utf8_lossy(skip_ws(r)).trim_end());
2169 let got = super::chrome::wm_req_name(core::mem::take(&mut self.m.chrome.wm_req));
2170 if want != got {
2171 self.fail(alloc::format!("wm {got}").as_bytes());
2172 }
2173 return;
2174 }
2175 if let Some(r) = word_eq(a, "scrollcopy") {
2176 let want = skip_ws(r);
2179 let got = core::mem::take(&mut self.m.scopy.frames);
2180 self.m.scopy.cells = 0;
2181 let ok = if word_eq(want, "none").is_some() { got == 0 } else { got as i32 >= parse_int(want).0.max(1) };
2182 if !ok {
2183 self.fail(alloc::format!("scroll copies: {got}").as_bytes());
2184 }
2185 return;
2186 }
2187 if let Some(r) = word_eq(a, "theme") {
2189 self.expect_theme(skip_ws(r));
2190 return;
2191 }
2192 if let Some(r) = word_eq(a, "style") {
2193 let want = String::from_utf8_lossy(skip_ws(r));
2194 let have = self.style_name();
2195 if want.trim() != have {
2196 self.fail(alloc::format!("style is {}", have).as_bytes());
2197 }
2198 return;
2199 }
2200 if let Some(r) = word_eq(a, "token") {
2201 self.expect_token(skip_ws(r));
2202 return;
2203 }
2204 if let Some(r) = word_eq(a, "pixel") {
2205 self.expect_pixel(r);
2206 return;
2207 }
2208 if let Some(r) = word_eq(a, "same") {
2209 self.expect_same(skip_ws(r));
2210 return;
2211 }
2212 let (s, rest) = self.resolve(a);
2213 if s.is_none() && self.sc.sel_err == 3 {
2214 self.sel_fail(); return;
2216 }
2217 let cond = skip_ws(rest);
2218 if word_eq(cond, "missing").is_some() {
2220 if s.is_some() {
2221 self.fail(b"widget exists");
2222 }
2223 return;
2224 }
2225 let Some(s) = s else {
2226 self.sel_fail();
2227 return;
2228 };
2229 let rec = self.sel_rec(s);
2230 const STATES: [(&str, u16, bool); 14] = [
2231 ("hover", SS_HOVER, true),
2232 ("nohover", SS_HOVER, false),
2233 ("unpressed", SS_PRESSED, false),
2234 ("focused", SS_FOCUSED, true),
2235 ("unfocused", SS_FOCUSED, false),
2236 ("unselected", SS_SELECTED, false),
2237 ("closed", SS_OPEN, false),
2238 ("checked", SS_CHECKED, true),
2239 ("unchecked", SS_CHECKED, false),
2240 ("disabled", SS_DISABLED, true),
2241 ("enabled", SS_DISABLED, false),
2242 ("selected", SS_SELECTED, true),
2243 ("open", SS_OPEN, true),
2244 ("pressed", SS_PRESSED, true),
2245 ];
2246 for (n, mask, want) in STATES {
2247 if word_eq(cond, n).is_some() {
2248 let set = rec.state & mask != 0;
2249 if set != want {
2250 self.fail(if set { b"state is set" } else { b"state is not set" });
2251 }
2252 return;
2253 }
2254 }
2255 if let Some(r) = word_eq(cond, "value") {
2256 let (lo, rest) = parse_int(r);
2257 let hi = if rest.starts_with(b"..") { parse_int(&rest[2..]).0 } else { lo };
2259 if rec.value < lo || rec.value > hi {
2260 self.fail(alloc::format!("value is {}", rec.value).as_bytes());
2261 }
2262 } else if let Some(r) = word_eq(cond, "max") {
2263 if parse_int(r).0 != rec.max {
2264 self.fail(alloc::format!("max is {}", rec.max).as_bytes());
2265 }
2266 } else if let Some(r) = word_eq(cond, "text") {
2267 let Some((t, _)) = parse_qstring(skip_ws(r), LINE_MAX) else {
2268 self.fail(b"expected a quoted string");
2269 return;
2270 };
2271 let have = rec.label.as_deref().unwrap_or("");
2272 if have.as_bytes() != t.as_slice() {
2273 let mut d = b"text is \"".to_vec();
2274 d.extend_from_slice(have.as_bytes());
2275 d.push(b'"');
2276 self.fail(&d);
2277 }
2278 } else if word_eq(cond, "exists").is_some() {
2279 } else if let Some(r) = word_eq(cond, "rect") {
2280 self.expect_rect(r, &rec);
2281 } else {
2282 self.fail(b"unknown condition");
2283 }
2284 }
2285
2286 fn expect_rect(&mut self, p: &[u8], rec: &Sem) {
2289 let mut want = [None; 4];
2290 let mut q = p;
2291 for v in want.iter_mut() {
2292 q = skip_ws(q);
2293 if q.first() == Some(&b'*') {
2294 q = &q[1..];
2295 continue;
2296 }
2297 let (n, r) = parse_int(q);
2298 *v = Some(n);
2299 q = r;
2300 }
2301 let dpi = self.win_dpi(rec.win as u32);
2302 let have = rec.rect.map(|v| px2dip_at(v, dpi));
2303 if have.iter().zip(want).any(|(h, w)| w.is_some_and(|w| w != *h)) {
2304 self.fail_more(alloc::format!("rect is {} {}", have[0], have[1]).as_bytes());
2305 plat::write_stdout(alloc::format!(" {} {}\n", have[2], have[3]).as_bytes());
2306 }
2307 }
2308
2309 fn expect_theme(&mut self, p: &[u8]) {
2312 if let Some(r) = word_eq(p, "error") {
2313 let r = skip_ws(r);
2314 let have = self.sc.theme_err.clone();
2315 if word_eq(r, "none").is_some() {
2316 if let Some(h) = have {
2317 self.fail(alloc::format!("error: {}", h).as_bytes());
2318 }
2319 return;
2320 }
2321 let Some((t, _)) = parse_qstring(r, 256) else {
2322 self.fail(b"expected a quoted string");
2323 return;
2324 };
2325 let t = String::from_utf8_lossy(&t).into_owned();
2326 match have {
2327 Some(h) if h.contains(&t) => {}
2328 Some(h) => self.fail(alloc::format!("error: {}", h).as_bytes()),
2329 None => self.fail(b"no error"),
2330 }
2331 return;
2332 }
2333 let have = self.app_theme().map(|t| t.name.clone());
2334 if word_eq(p, "none").is_some() {
2335 if let Some(h) = have {
2336 self.fail(alloc::format!("theme is \"{}\"", h).as_bytes());
2337 }
2338 return;
2339 }
2340 let want = match parse_qstring(p, 256) {
2341 Some((t, _)) => String::from_utf8_lossy(&t).into_owned(),
2342 None => String::from(String::from_utf8_lossy(p).trim()),
2343 };
2344 match have {
2345 Some(h) if h == want => {}
2346 Some(h) => self.fail(alloc::format!("theme is \"{}\"", h).as_bytes()),
2347 None => self.fail(b"no theme"),
2348 }
2349 }
2350
2351 fn expect_token(&mut self, p: &[u8]) {
2353 let name = String::from_utf8_lossy(&p[..p.iter().position(|&c| c == b' ' || c == b'\t').unwrap_or(p.len())]).into_owned();
2354 let want = super::theme::parse_color(String::from_utf8_lossy(next_word(p)).trim());
2355 let (Some(i), Some(want)) = (super::theme::token_index(&name), want) else {
2356 self.fail(b"expect token NAME #RRGGBB | #AARRGGBB");
2357 return;
2358 };
2359 let have = self.color(i as u32 * 4);
2360 if have != want {
2361 self.fail(alloc::format!("token is #{:08X}", have).as_bytes());
2362 }
2363 }
2364
2365 fn expect_pixel(&mut self, p: &[u8]) {
2367 let (x, r) = parse_int(p);
2368 let (y, r) = parse_int(r);
2369 let Some(want) = super::theme::parse_color(String::from_utf8_lossy(skip_ws(r)).trim()) else {
2370 self.fail(b"expect pixel X Y #RRGGBB");
2371 return;
2372 };
2373 self.render();
2374 self.gp_settle_all();
2375 let dpi = self.win_dpi(0);
2376 let sf = self.win_fb(0);
2377 let (px, py) = (dp_at(x, dpi), dp_at(y, dpi));
2378 if px < 0 || py < 0 || px >= sf.w || py >= sf.h {
2379 self.fail(b"outside the window");
2380 return;
2381 }
2382 let o = py as usize * sf.stride as usize + px as usize * 4;
2383 let have = u32::from_le_bytes([sf.px[o], sf.px[o + 1], sf.px[o + 2], sf.px[o + 3]]) & 0xFF_FFFF;
2384 if have != want & 0xFF_FFFF {
2385 self.fail(alloc::format!("pixel is #{:06X}", have).as_bytes());
2386 }
2387 }
2388
2389 fn expect_same(&mut self, p: &[u8]) {
2392 let p = skip_ws(p);
2393 let i = p.iter().position(|&c| c == b' ' || c == b'\t').unwrap_or(p.len());
2394 let path = String::from(String::from_utf8_lossy(&p[..i]).trim());
2395 let q = skip_ws(&p[i..]);
2396 self.render();
2397 self.gp_settle_all();
2398 let sf = self.win_fb(0);
2399 let png = if q.is_empty() {
2400 img_png::encode(&sf.px, sf.w as usize, sf.h as usize, sf.stride as usize, false)
2401 } else {
2402 let (x, r) = parse_int(q);
2403 let (y, r) = parse_int(r);
2404 let (w, r) = parse_int(r);
2405 let (h, _) = parse_int(r);
2406 match crop_surface(sf, self.win_dpi(0), [x, y, w, h]) {
2407 Some(v) => img_png::encode(&v.px, v.w as usize, v.h as usize, v.stride as usize, false),
2408 None => {
2409 self.fail(b"the crop lies outside the window");
2410 return;
2411 }
2412 }
2413 };
2414 let file = super::perm::trusted(|| plat::read_file(&path));
2415 match file {
2416 None => self.fail(b"no such file"),
2417 Some(f) if f != png => self.fail(b"the pixels differ"),
2418 Some(_) => {}
2419 }
2420 }
2421
2422 fn expect_window(&mut self, p: &[u8]) {
2425 let (i, r) = self.window_arg(p);
2426 let cond = skip_ws(r);
2427 let mut st = 0u32; if i >= 0 && self.uiw_drawable(i as u32) {
2429 st |= 1;
2430 if i as u32 == self.m.win.active {
2431 st |= 2;
2432 }
2433 if i != 0 && i as u32 == self.m.win.modal {
2434 st |= 4;
2435 }
2436 if i as u32 == self.uiw_keys_window() {
2437 st |= 8;
2438 }
2439 if self.m.win.recs[i as usize].fs {
2440 st |= 16;
2441 }
2442 }
2443 const STATES: [(&str, u32, bool); 8] = [
2444 ("open", 1, true),
2445 ("closed", 1, false),
2446 ("visible", 1, true),
2447 ("active", 2, true),
2448 ("modal", 4, true),
2449 ("keys", 8, true),
2450 ("fullscreen", 16, true),
2451 ("windowed", 16, false),
2452 ];
2453 for (n, mask, want) in STATES {
2454 if word_eq(cond, n).is_some() {
2455 let set = st & mask != 0;
2456 if set != want {
2457 self.fail(if set { b"window state is set" } else { b"window state is not set" });
2458 }
2459 return;
2460 }
2461 }
2462 if let Some(r) = word_eq(cond, "rect") {
2463 if i < 0 {
2464 self.fail(b"no such window");
2465 return;
2466 }
2467 let rect = self.m.win.recs[i as usize].rect.get(); let have = [rect.x, rect.y, rect.w, rect.h];
2469 let mut q = r;
2470 for &h in have.iter() {
2471 let (n, rr) = parse_int(q);
2472 q = rr;
2473 if n != h {
2474 self.fail_more(alloc::format!("rect is {} {}", rect.x, rect.y).as_bytes());
2475 plat::write_stdout(alloc::format!(" {} {}\n", rect.w, rect.h).as_bytes());
2476 return;
2477 }
2478 }
2479 return;
2480 }
2481 self.fail(b"unknown window condition");
2482 }
2483
2484 fn sc_close(&mut self, app: &mut dyn App, a: &[u8]) {
2486 let Some(r) = word_eq(a, "window") else {
2487 self.fail(b"expected: close window ID");
2488 return;
2489 };
2490 let (i, _) = self.window_arg(r);
2491 if i < 0 {
2492 self.fail(b"no such window");
2493 return;
2494 }
2495 self.sc.sc_win = i as u32;
2496 self.inject(app, plat::E_CLOSE, 0, 0, 0, 0);
2497 self.run_passes(app);
2498 }
2499
2500 fn sc_scale(&mut self, app: &mut dyn App, a: &[u8]) {
2502 let (p, r) = parse_int(a);
2503 let dpi = super::runtime::scale_to_dpi(p);
2504 let q = skip_ws(r);
2505 if let Some(r) = word_eq(q, "window") {
2506 let (i, _) = self.window_arg(r);
2507 if i < 0 {
2508 self.fail(b"no such window");
2509 return;
2510 }
2511 if i != 0 {
2512 self.uiw_dpi(i as u32, dpi);
2513 self.run_passes(app);
2514 return;
2515 }
2516 }
2517 let w = self.px2dip(self.wp.rt.fb.w);
2519 let h = self.px2dip(self.wp.rt.fb.h);
2520 self.uiw_dpi(0, dpi);
2521 let (w, h) = (self.dp(w), self.dp(h));
2522 self.resize(app, w, h);
2523 self.run_passes(app);
2524 }
2525
2526 fn wait_real(&mut self, app: &mut dyn App, ms: i32) {
2530 let end = plat::time_us() + ms.max(0) as u64 * 1000;
2531 let mut clock = plat::time_us(); loop {
2533 let now = plat::time_us();
2534 let gone = ((now - clock) / 1000) as i32;
2535 if gone > 0 {
2536 clock += gone as u64 * 1000;
2537 self.advance_time(app, gone);
2538 }
2539 self.process_events(app); if self.need {
2541 self.need = false;
2542 self.run_passes(app);
2543 }
2544 if now >= end {
2545 break;
2546 }
2547 plat::wait_events((now + 16_000).min(end), &mut |e| {
2549 plat::evq_push(*e);
2550 });
2551 }
2552 self.process_events(app);
2553 self.run_passes(app);
2554 }
2555
2556 fn sc_sync(&mut self, app: &mut dyn App, a: &[u8]) {
2559 let (ms, r) = parse_int(a);
2560 let soft = word_eq(skip_ws(a), "soft").is_some() || word_eq(skip_ws(r), "soft").is_some();
2561 let ms = if ms > 0 { ms } else { 10000 };
2562 let deadline = plat::time_us() + ms as u64 * 1000;
2563 while self.uiw_sync_pending() || self.tasks_busy() || self.spell_busy() {
2564 plat::wait_events(plat::time_us() + 2_000, &mut |e| {
2567 plat::evq_push(*e);
2568 });
2569 self.process_events(app);
2570 self.run_passes(app);
2571 if plat::time_us() >= deadline {
2572 if soft {
2573 plat::write_stdout(alloc::format!("sync: background jobs still running after {ms} ms (soft: no failure)\n").as_bytes());
2574 } else {
2575 self.fail(b"background jobs still running");
2576 }
2577 break;
2578 }
2579 }
2580 self.process_events(app); self.spell_poll(); self.run_passes(app);
2583 }
2584
2585 fn sc_bench(&mut self, app: &mut dyn App, a: &[u8]) {
2589 let (n, r) = parse_int(a);
2590 let w = skip_ws(r);
2591 if !w.is_empty() {
2592 self.sc_bench_moving(app, n.max(1) as usize, w);
2593 return;
2594 }
2595 let n = if n > 0 { n as u64 } else { 100 };
2596 let t0 = plat::time_us();
2597 for _ in 0..n {
2598 self.run_passes(app);
2599 }
2600 let build = plat::time_us() - t0;
2601 let t0 = plat::time_us();
2602 for _ in 0..n {
2603 self.dirty_force_full();
2604 self.render();
2605 }
2606 let full = plat::time_us() - t0;
2607 let t0 = plat::time_us();
2608 for _ in 0..n {
2609 self.run_passes(app);
2610 self.render();
2611 }
2612 let idle = plat::time_us() - t0;
2613 let s = alloc::format!(
2614 "bench: frame build {} us, full render {} us, unchanged frame {} us ({}x{} px)\n",
2615 build / n,
2616 full / n,
2617 idle / n,
2618 self.wp.rt.fb.w,
2619 self.wp.rt.fb.h
2620 );
2621 plat::write_stdout(s.as_bytes());
2622 }
2623
2624 fn sc_bench_moving(&mut self, app: &mut dyn App, n: usize, a: &[u8]) {
2628 enum Kind {
2629 Anim,
2630 Drag { from: (i32, i32), to: (i32, i32), btn: u32, mods: u32, win: u32 },
2631 Wheel { at: (i32, i32), dy: i32, mods: u32, win: u32 },
2632 Key { mods: u32, vk: u32 },
2633 Type(Vec<u32>),
2634 }
2635 let opts = |r: &[u8], drag: bool| -> Result<(u32, u32, usize), Vec<u8>> {
2637 let (mut btn, mut mods, mut q) = (BTN_LEFT, 0, r);
2638 loop {
2639 q = skip_ws(q);
2640 if q.is_empty() || q[0].is_ascii_digit() {
2641 return Ok((btn, mods, r.len() - q.len()));
2642 }
2643 let m = mod_word(q);
2644 if m != 0 {
2645 mods |= m;
2646 } else if drag && word_eq(q, "right").is_some() {
2647 btn = BTN_RIGHT;
2648 } else if drag && word_eq(q, "middle").is_some() {
2649 btn = BTN_MIDDLE;
2650 } else {
2651 let mut d = b"unknown bench option ".to_vec();
2652 d.extend_from_slice(q);
2653 return Err(d);
2654 }
2655 let i = q.iter().position(|&c| c == b' ' || c == b'\t').unwrap_or(q.len());
2656 q = &q[i..];
2657 }
2658 };
2659 let (kind, rest): (Kind, &[u8]) = if let Some(r) = word_eq(a, "anim") {
2660 (Kind::Anim, r)
2661 } else if let Some(r) = word_eq(a, "drag") {
2662 let (s, r) = self.resolve(skip_ws(r));
2663 if s.is_none() {
2664 self.sel_fail();
2665 return;
2666 }
2667 let (from, win) = ((self.sc.sel_x, self.sc.sel_y), self.sc.sel_win);
2668 let (s, r) = self.resolve(skip_ws(r));
2669 if s.is_none() {
2670 self.sel_fail();
2671 return;
2672 }
2673 self.point_to_window(self.sc.sel_win, win);
2674 let to = (self.sc.sel_x, self.sc.sel_y);
2675 match opts(r, true) {
2676 Ok((btn, mods, k)) => (Kind::Drag { from, to, btn, mods, win }, &r[k..]),
2677 Err(d) => {
2678 self.fail(&d);
2679 return;
2680 }
2681 }
2682 } else if let Some(r) = word_eq(a, "wheel") {
2683 let (s, r) = self.resolve(skip_ws(r));
2684 if s.is_none() {
2685 self.sel_fail();
2686 return;
2687 }
2688 let (at, win) = ((self.sc.sel_x, self.sc.sel_y), self.sc.sel_win);
2689 let (dy, r) = parse_int(skip_ws(r));
2690 match opts(r, false) {
2691 Ok((_, mods, k)) => (Kind::Wheel { at, dy, mods, win }, &r[k..]),
2692 Err(d) => {
2693 self.fail(&d);
2694 return;
2695 }
2696 }
2697 } else if let Some(r) = word_eq(a, "key") {
2698 let Some((mods, vk, r)) = chord(skip_ws(r)) else {
2699 self.fail(b"unknown key");
2700 return;
2701 };
2702 (Kind::Key { mods, vk }, r)
2703 } else if let Some(r) = word_eq(a, "type") {
2704 let Some((s, r)) = parse_qstring(skip_ws(r), 256) else {
2705 self.fail(b"expected a quoted string");
2706 return;
2707 };
2708 (Kind::Type(String::from_utf8_lossy(&s).chars().map(|c| c as u32).collect()), r)
2709 } else {
2710 self.fail(b"expected: bench N [anim | drag S1 S2 [options] | wheel S DY [options] | key CHORD | type \"s\"] [MAX_US]");
2711 return;
2712 };
2713 let (max, _) = parse_int(skip_ws(rest));
2714 let name = match kind {
2715 Kind::Anim => "anim",
2716 Kind::Drag { .. } => "drag",
2717 Kind::Wheel { .. } => "wheel",
2718 Kind::Key { .. } => "key",
2719 Kind::Type(_) => "type",
2720 };
2721 let runs = if max > 0 { 3 } else { 1 };
2722 let mut best = u64::MAX;
2723 for run in 0..runs {
2724 let mut t: Vec<(u64, u64)> = Vec::with_capacity(n);
2726 match kind {
2727 Kind::Anim => {
2728 for _ in 0..n {
2729 t.push(self.bench_frame(app, |ui, app| ui.run_passes(app)));
2730 }
2731 }
2732 Kind::Drag { from, to, btn, mods, win } => {
2733 let (a, b) = if run % 2 == 0 { (from, to) } else { (to, from) };
2735 let saved = self.sc.script_mods;
2736 self.sc.sel_win = win;
2737 self.mouse_to(app, a.0, a.1);
2738 self.sc.script_mods = mods;
2739 let d = self.sc.sc_pdata;
2740 self.inject(app, plat::E_DOWN, btn, a.0, a.1, d);
2741 for i in 1..=n as i32 {
2742 let (x, y) = (a.0 + (b.0 - a.0) * i / n as i32, a.1 + (b.1 - a.1) * i / n as i32);
2743 t.push(self.bench_frame(app, |ui, app| ui.mouse_to(app, x, y)));
2744 }
2745 let d = self.sc.sc_pdata;
2746 self.inject(app, plat::E_UP, btn, b.0, b.1, d);
2747 self.sc.script_mods = saved;
2748 }
2749 Kind::Wheel { at, dy, mods, win } => {
2750 self.sc.sel_win = win;
2751 self.mouse_to(app, at.0, at.1);
2752 let saved = self.sc.script_mods;
2753 self.sc.script_mods = mods;
2754 for i in 0..n {
2755 let v = if i % 2 == 0 { dy } else { -dy };
2756 let (x, y) = (self.sc.sc_x, self.sc.sc_y);
2757 t.push(self.bench_frame(app, |ui, app| ui.inject(app, plat::E_WHEEL, 0, x, y, v as u16 as u32)));
2758 }
2759 self.sc.script_mods = saved;
2760 }
2761 Kind::Key { mods, vk } => {
2762 for _ in 0..n {
2763 t.push(self.bench_frame(app, |ui, app| ui.press(app, mods, vk)));
2764 }
2765 }
2766 Kind::Type(ref cps) => {
2767 for _ in 0..n {
2768 t.push(self.bench_frame(app, |ui, app| {
2769 for &cp in cps {
2770 ui.inject(app, plat::E_TEXT, 0, 0, 0, cp);
2771 }
2772 }));
2773 }
2774 }
2775 }
2776 let mut s: Vec<u64> = t.iter().map(|x| x.0).collect();
2777 s.sort_unstable();
2778 let (mean, median, top) = (s.iter().sum::<u64>() / s.len() as u64, s[s.len() / 2], s[s.len() - 1]);
2779 let render = t.iter().map(|x| x.1).sum::<u64>() / t.len() as u64;
2780 best = best.min(median);
2781 let o = alloc::format!(
2782 "bench {}: {} frames, mean {} us (render {} us), median {} us, max {} us ({}x{} px)\n",
2783 name,
2784 s.len(),
2785 mean,
2786 render,
2787 median,
2788 top,
2789 self.wp.rt.fb.w,
2790 self.wp.rt.fb.h
2791 );
2792 plat::write_stdout(o.as_bytes());
2793 if max <= 0 || median <= max as u64 {
2794 return;
2795 }
2796 }
2797 self.fail(alloc::format!("the median stayed above {} us in {} runs (best {} us)", max, runs, best).as_bytes());
2798 }
2799
2800 fn bench_frame(&mut self, app: &mut dyn App, f: impl FnOnce(&mut Ui, &mut dyn App)) -> (u64, u64) {
2804 let verify = self.verify_dirty;
2805 self.verify_dirty = false;
2806 let t0 = plat::time_us();
2807 let t = self.now_ms.wrapping_add(16);
2808 self.set_time(t);
2809 loop {
2810 let (id, _) = self.timer_due();
2811 if id == u32::MAX {
2812 break;
2813 }
2814 self.cmd_run(UIC_TIMER, id);
2815 }
2816 f(self, app);
2817 let t1 = plat::time_us();
2818 self.render();
2819 let t2 = plat::time_us();
2820 self.verify_dirty = verify;
2821 if verify {
2822 self.verify_dirty();
2823 }
2824 (t2.saturating_sub(t0), t2.saturating_sub(t1))
2825 }
2826
2827 fn k1_word(p: &[u8], cap: usize) -> Option<(Vec<u8>, &[u8])> {
2832 let p = skip_ws(p);
2833 if p.is_empty() {
2834 return None;
2835 }
2836 if p[0] == b'"' {
2837 let (w, r) = parse_qstring(p, cap)?;
2838 return Some((w, skip_ws(r)));
2839 }
2840 let e = p.iter().position(|&c| c == b' ' || c == b'\t').unwrap_or(p.len());
2841 let w = p[..e.min(cap - 1)].to_vec();
2842 Some((w, skip_ws(&p[e..])))
2843 }
2844
2845 fn sc_drop(&mut self, app: &mut dyn App, a: &[u8]) {
2850 const BAD: &[u8] = b"expected: drop X Y FILE... | drop X Y [files F...] [text \"s\"] [html \"s\"] [url U] [image F] [copy|move|link] [sync] [allows copy|move|link...] | drop X Y self | drop expect copy|move|link|none|pending";
2851 if let Some((verb, r)) = Ui::k1_word(a, 16)
2852 && verb == b"expect"
2853 {
2854 self.sc_drop_expect(r);
2855 return;
2856 }
2857 let (x, r) = parse_int(a);
2858 let (y, r) = parse_int(r);
2859 let w = self.scope_index().max(0) as u32;
2860 let dpi = self.win_dpi(w);
2861 let (px, py) = (dp_at(x, dpi), dp_at(y, dpi));
2862 let Some((data, allowed)) = self.sc_drag_data(r) else {
2863 self.dnd.sync_next = false;
2864 self.fail(BAD);
2865 return;
2866 };
2867 if data.types() == 0 && word_eq(skip_ws(r), "self").is_none() {
2868 self.dnd.sync_next = false;
2869 self.fail(BAD);
2870 return;
2871 }
2872 let n = data.files.len() as u32;
2873 self.drop_inject(data, allowed);
2874 self.sc.sc_win = w;
2875 self.inject(app, plat::E_DROP, 0, px, py, n);
2876 self.run_passes(app);
2877 }
2878
2879 fn sc_drop_expect(&mut self, a: &[u8]) {
2882 let name = |e: u32| match e {
2883 plat::DE_COPY => "copy",
2884 plat::DE_MOVE => "move",
2885 plat::DE_LINK => "link",
2886 _ => "none",
2887 };
2888 let want = match Ui::k1_word(a, 16).map(|w| w.0) {
2889 Some(w) if w == b"copy" => Some(plat::DE_COPY),
2890 Some(w) if w == b"move" => Some(plat::DE_MOVE),
2891 Some(w) if w == b"link" => Some(plat::DE_LINK),
2892 Some(w) if w == b"none" => Some(0),
2893 Some(w) if w == b"pending" => None,
2894 _ => {
2895 self.fail(b"expected: drop expect copy|move|link|none|pending");
2896 return;
2897 }
2898 };
2899 match (self.dnd_answered(), want) {
2900 (None, _) => self.fail(b"no drop yet"),
2901 (Some(have), want) if have == want => {}
2902 (Some(None), _) => self.fail(b"the answer waits (drop_defer)"),
2903 (Some(Some(e)), _) => self.fail(alloc::format!("the source was answered {}", name(e)).as_bytes()),
2904 }
2905 }
2906
2907 fn sc_drag_data(&mut self, a: &[u8]) -> Option<(DragData, u32)> {
2912 let mut d = DragData::default();
2913 let mut r = a;
2914 let mut kind: &[u8] = b"files";
2915 let (mut allows, mut allowed) = (false, 0u32);
2916 plat::drop_reset();
2917 while let Some((word, rest)) = Ui::k1_word(r, 4096) {
2918 r = rest;
2919 let w = word.as_slice();
2920 if !matches!(w, b"copy" | b"move" | b"link") {
2921 allows = false;
2922 }
2923 match w {
2924 b"files" | b"text" | b"html" | b"url" | b"image" if !matches!(kind, b"text" | b"html" | b"url" | b"image") => {
2925 kind = match w {
2926 b"files" => b"files",
2927 b"text" => b"text",
2928 b"html" => b"html",
2929 b"url" => b"url",
2930 _ => b"image",
2931 };
2932 continue;
2933 }
2934 b"copy" | b"move" | b"link" if kind == b"files" => {
2935 let e = match w {
2936 b"copy" => plat::DE_COPY,
2937 b"move" => plat::DE_MOVE,
2938 _ => plat::DE_LINK,
2939 };
2940 if allows {
2941 allowed |= e;
2942 } else {
2943 d.effects = e;
2944 }
2945 continue;
2946 }
2947 b"allows" if kind == b"files" => {
2948 allows = true;
2949 continue;
2950 }
2951 b"self" if kind == b"files" => continue,
2952 b"sync" if kind == b"files" => {
2953 self.dnd.sync_next = true;
2954 continue;
2955 }
2956 _ => {}
2957 }
2958 let s = String::from_utf8_lossy(w).into_owned();
2959 match kind {
2960 b"text" => d.text = Some(s),
2961 b"html" => d.html = Some(s),
2962 b"url" => d.urls.push(s),
2963 b"image" => d.image = Some(super::perm::trusted(|| plat::read_file(&s))?),
2964 _ => {
2965 plat::drop_add(&s);
2966 }
2967 }
2968 kind = b"files"; }
2970 if matches!(kind, b"text" | b"html" | b"url" | b"image") {
2971 return None; }
2973 d.files = plat::drop_take();
2974 Some((d, allowed))
2975 }
2976
2977 fn sc_dragover(&mut self, app: &mut dyn App, a: &[u8], leave: bool) {
2980 let w = self.scope_index().max(0) as u32;
2981 self.sc.sc_win = w;
2982 if leave {
2983 self.inject(app, plat::E_DRAG, 0, -1, -1, 0);
2984 return;
2985 }
2986 if let Some((verb, r)) = Ui::k1_word(a, 16)
2987 && verb == b"expect"
2988 {
2989 self.sc_dragover_expect(r);
2990 return;
2991 }
2992 let (x, r) = parse_int(a);
2993 let (y, mut r) = parse_int(r);
2994 let dpi = self.win_dpi(w);
2995 let (px, py) = (dp_at(x, dpi), dp_at(y, dpi));
2996 let (mut types, mut effects) = (0u32, 0u32);
2997 while let Some((word, rest)) = Ui::k1_word(r, 16) {
2998 r = rest;
2999 match word.as_slice() {
3000 b"files" => types |= plat::DT_FILES,
3001 b"text" => types |= plat::DT_TEXT,
3002 b"url" => types |= plat::DT_URL,
3003 b"html" => types |= plat::DT_HTML,
3004 b"image" => types |= plat::DT_IMAGE,
3005 b"copy" => effects |= plat::DE_COPY,
3006 b"move" => effects |= plat::DE_MOVE,
3007 b"link" => effects |= plat::DE_LINK,
3008 _ => {
3009 self.fail(b"expected: dragover X Y [files] [text] [url] [html] [image] [copy|move|link]");
3010 return;
3011 }
3012 }
3013 }
3014 if types == 0 {
3015 types = plat::DT_FILES;
3016 }
3017 if effects == 0 {
3018 effects = plat::DE_COPY | plat::DE_MOVE;
3019 }
3020 self.inject(app, plat::E_DRAG, 0, px, py, types | effects << 8);
3021 }
3022
3023 fn sc_dragover_expect(&mut self, a: &[u8]) {
3027 const BAD: &[u8] = b"expected: dragover expect copy|move|link|none [ctrl] [shift]";
3028 let Some((want, mut r)) = Ui::k1_word(a, 16) else {
3029 self.fail(BAD);
3030 return;
3031 };
3032 let want = match want.as_slice() {
3033 b"copy" => plat::DE_COPY,
3034 b"move" => plat::DE_MOVE,
3035 b"link" => plat::DE_LINK,
3036 b"none" => 0,
3037 _ => {
3038 self.fail(BAD);
3039 return;
3040 }
3041 };
3042 let mut mods = 0u8;
3043 while let Some((word, rest)) = Ui::k1_word(r, 16) {
3044 r = rest;
3045 match word.as_slice() {
3046 b"ctrl" => mods |= plat::MOD_CTRL,
3047 b"shift" => mods |= plat::MOD_SHIFT,
3048 _ => {
3049 self.fail(BAD);
3050 return;
3051 }
3052 }
3053 }
3054 let name = |e: u32| match e {
3055 plat::DE_COPY => "copy",
3056 plat::DE_MOVE => "move",
3057 plat::DE_LINK => "link",
3058 _ => "none",
3059 };
3060 match self.dnd_answer_now(mods) {
3061 None => self.fail(b"no drag hovers (dragover X Y first)"),
3062 Some((_, e, all)) if e != want => {
3063 let msg = alloc::format!("the platform would answer {} (the target takes {})", name(e), {
3064 let v: Vec<&str> = [plat::DE_COPY, plat::DE_MOVE, plat::DE_LINK].into_iter().filter(|f| all & f != 0).map(name).collect();
3065 if v.is_empty() { String::from("none") } else { v.join("+") }
3066 });
3067 self.fail(msg.as_bytes());
3068 }
3069 Some(_) => {}
3070 }
3071 }
3072
3073 fn sc_dragout(&mut self, app: &mut dyn App, a: &[u8]) {
3077 const BAD: &[u8] = b"expected: dragout expect none|text \"s\"|html \"s\"|url \"s\"|files N|image|kinds N | dragout end copy|move|link|none";
3078 let Some((verb, r)) = Ui::k1_word(a, 16) else {
3079 self.fail(BAD);
3080 return;
3081 };
3082 if verb == b"end" {
3083 let e = match Ui::k1_word(r, 16).map(|w| w.0) {
3084 Some(w) if w == b"copy" => plat::DE_COPY,
3085 Some(w) if w == b"move" => plat::DE_MOVE,
3086 Some(w) if w == b"link" => plat::DE_LINK,
3087 Some(w) if w == b"none" => 0,
3088 _ => {
3089 self.fail(BAD);
3090 return;
3091 }
3092 };
3093 self.inject(app, plat::E_DRAG_END, 0, 0, 0, e);
3094 return;
3095 }
3096 if verb != b"expect" {
3097 self.fail(BAD);
3098 return;
3099 }
3100 let Some((what, r)) = Ui::k1_word(r, 16) else {
3101 self.fail(BAD);
3102 return;
3103 };
3104 let out = self.dnd_out_data().cloned();
3105 let arg = Ui::k1_word(r, 4096).map(|w| w.0);
3106 let got: Result<(), String> = match (what.as_slice(), &out) {
3107 (b"none", None) => Ok(()),
3108 (b"none", Some(_)) => Err(String::from("a drag left the window")),
3109 (_, None) => Err(String::from("no drag left the window")),
3110 (b"image", Some(d)) => if d.image.is_some() { Ok(()) } else { Err(String::from("no image")) },
3111 (b"files" | b"kinds", Some(d)) => {
3112 let want = parse_int(arg.as_deref().unwrap_or(b"")).0;
3113 let n = if what == b"files" { d.files.len() as i32 } else { d.types() as i32 };
3114 if n == want { Ok(()) } else { Err(alloc::format!("{n}")) }
3115 }
3116 (b"text" | b"html" | b"url", Some(d)) => {
3117 let want = String::from_utf8_lossy(arg.as_deref().unwrap_or(b"")).into_owned();
3118 let have = match what.as_slice() {
3119 b"text" => d.text.clone(),
3120 b"html" => d.html.clone(),
3121 _ => d.urls.first().cloned(),
3122 };
3123 if have.as_deref() == Some(want.as_str()) { Ok(()) } else { Err(alloc::format!("{have:?}")) }
3124 }
3125 _ => Err(String::from("bad kind")),
3126 };
3127 if let Err(e) = got {
3128 self.fail(e.as_bytes());
3129 }
3130 }
3131
3132 fn sc_clip(&mut self, app: &mut dyn App, a: &[u8]) {
3134 const BAD: &[u8] = b"expected: clip text \"s\" | file F | dib F | clear | save F [bmp] | expect none|text|image|png";
3135 let Some((verb, r)) = Ui::k1_word(a, 16) else {
3136 self.fail(BAD);
3137 self.run_passes(app);
3138 return;
3139 };
3140 let v4 = |s: &[u8]| verb.len() >= 4 && &verb[..4] == s;
3141 let v3 = |s: &[u8]| verb.len() >= 3 && &verb[..3] == s;
3142 if verb == b"data" {
3143 let mut rest = r;
3145 let mut entries = Vec::new();
3146 let mut valid = true;
3147 while !rest.is_empty() && entries.len() < 4 {
3148 let item = Ui::k1_word(rest, 16).and_then(|(kind, next)| {
3149 let fmt = match kind.as_slice() { b"text" => CLIP_TEXT, b"html" => CLIP_HTML, b"rtf" => CLIP_RTF, b"docx" => CLIP_DOCX, _ => return None };
3150 let (file, next) = Ui::k1_word(next, 1040)?;
3151 Some((fmt, super::perm::trusted(|| plat::read_file(&String::from_utf8_lossy(&file)))?, next))
3152 });
3153 match item {
3154 Some((fmt, data, next)) => { entries.push((fmt, data)); rest = next; }
3155 None => { valid = false; break; }
3156 }
3157 }
3158 let refs: Vec<_> = entries.iter().map(|(fmt, data)| (*fmt, data.as_slice())).collect();
3159 if !valid || !rest.is_empty() || self.clip_set_many(&refs) != 0 {
3160 self.fail(b"clip data requires one to four text|html|rtf|docx PATH pairs with valid payloads");
3161 }
3162 } else if verb == b"utf8" {
3163 match Ui::k1_word(r, 1040).and_then(|(name, _)| super::perm::trusted(|| plat::read_file(&String::from_utf8_lossy(&name)))) {
3166 Some(data) if data.len() <= 4 << 20 && core::str::from_utf8(&data).is_ok() => { self.clip_set(CLIP_TEXT, &data); }
3167 _ => self.fail(b"clip utf8 requires a readable UTF-8 file of at most 4 MiB"),
3168 }
3169 } else if v4(b"text") {
3170 match parse_qstring(r, 1040) {
3171 Some((t, _)) => {
3172 self.clip_set(CLIP_TEXT, &t);
3173 }
3174 None => self.fail(BAD),
3175 }
3176 } else if v4(b"file") || v3(b"dib") {
3177 match Ui::k1_word(r, 1040) {
3178 None => self.fail(BAD),
3179 Some((f, _)) => {
3180 let name = String::from_utf8_lossy(&f).into_owned();
3181 match super::perm::trusted(|| plat::read_file(&name)) {
3182 None => {
3183 let mut d = b"cannot read ".to_vec();
3184 d.extend_from_slice(&f);
3185 self.fail(&d);
3186 }
3187 Some(data) => {
3188 let bmp = if v3(b"dib") {
3190 Some(plat::clip_dib_to_bmp(&data))
3191 } else if data.len() >= 2 && data[0] == b'B' && data[1] == b'M' {
3192 Some(plat::clip_bmp_fix(&data))
3193 } else {
3194 None
3195 };
3196 match bmp {
3197 Some(None) => self.fail(b"invalid DIB / BMP data"),
3198 Some(Some(b)) => self.hclip_img_set_parts(None, Some(&b)),
3199 None => self.hclip_img_set_parts(Some(&data), None),
3200 }
3201 }
3202 }
3203 }
3204 }
3205 } else if v4(b"clea") {
3206 self.hclip_img_clear();
3207 self.hclip_text_clear();
3208 } else if v4(b"save") {
3209 match Ui::k1_word(r, 1040) {
3210 None => self.fail(BAD),
3211 Some((f, rest)) => {
3212 let name = String::from_utf8_lossy(&f).into_owned();
3213 let data = match rest {
3214 b"text" => self.clip_read(CLIP_TEXT),
3215 b"html" => self.clip_read(CLIP_HTML),
3216 b"rtf" => self.clip_read(CLIP_RTF),
3217 b"docx" => self.clip_read(CLIP_DOCX),
3218 _ if rest.first() == Some(&b'b') => self.dr.hi_bmp.clone(),
3219 _ => self.hclip_img_get(CLIP_PNG),
3220 };
3221 match data {
3222 Some(d) => {
3223 super::perm::trusted(|| plat::write_file(&name, &d));
3224 }
3225 None => self.fail(b"the clipboard holds no image"),
3226 }
3227 }
3228 }
3229 } else if v4(b"expe") {
3230 match Ui::k1_word(r, 16) {
3231 None => self.fail(BAD),
3232 Some((w, _)) => {
3233 let text = self.hclip_text_has();
3234 let png = self.hclip_img_has(CLIP_PNG);
3235 let image = self.hclip_img_has(CLIP_IMAGE);
3236 let w4 = |s: &[u8]| w.len() >= 4 && &w[..4] == s;
3237 let ok = if w4(b"none") {
3238 Some(!text && !image && self.m.edit.hclip_rich.is_empty())
3239 } else if w == b"text-only" {
3240 Some(text && !image && self.m.edit.hclip_rich.is_empty())
3241 } else if w4(b"text") {
3242 Some(text)
3243 } else if w == b"html" {
3244 Some(self.clip_has(CLIP_HTML))
3245 } else if w == b"rtf" {
3246 Some(self.clip_has(CLIP_RTF))
3247 } else if w == b"docx" {
3248 Some(self.clip_has(CLIP_DOCX))
3249 } else if w4(b"imag") {
3250 Some(image)
3251 } else if w.len() >= 3 && &w[..3] == b"png" {
3252 Some(png)
3253 } else {
3254 None
3255 };
3256 match ok {
3257 None => self.fail(BAD),
3258 Some(false) => self.fail(alloc::format!("unexpected clipboard content (text {})", text as u32).as_bytes()),
3259 Some(true) => {}
3260 }
3261 }
3262 }
3263 } else {
3264 self.fail(BAD);
3265 }
3266 self.run_passes(app);
3267 }
3268}