1use alloc::string::String;
172use alloc::vec::Vec;
173use core::fmt::Write;
174
175use super::consts::*;
176use super::keys::*;
177use super::layout::{dp_at, px2dip_at};
178use super::runtime::{parse_hex, parse_int};
179use super::{App, Sem, Ui};
180use gfx::{Surface, FMT_XRGB};
181use plat::{DragData, Event};
182
183const LINE_MAX: usize = 1024;
184
185pub(crate) struct S {
188 lineno: u32,
189 fails: u32,
190 parse_err: bool,
191 line: Vec<u8>,
192 script_mods: u32,
193 sel_err: u32,
194 sel_kind: i32,
195 sel_wf: i32,
196 sel_win: u32,
197 sel_x: i32,
198 sel_y: i32,
199 sel_label: Vec<u8>,
200 sc_win: u32,
201 sc_x: i32,
202 sc_y: i32,
203 sc_pdata: u32,
204 finger_id: u32,
206 warn_mark: u32,
207 sc_scope: i32,
208 pub(crate) agent: super::auto_perm::S,
210 theme_err: Option<String>,
212}
213
214impl Default for S {
215 fn default() -> S {
216 S {
217 lineno: 0,
218 fails: 0,
219 parse_err: false,
220 line: Vec::new(),
221 script_mods: 0,
222 sel_err: 0,
223 sel_kind: -1,
224 sel_wf: -1,
225 sel_win: 0,
226 sel_x: 0,
227 sel_y: 0,
228 sel_label: Vec::new(),
229 sc_win: 0,
230 sc_x: 0,
231 sc_y: 0,
232 sc_pdata: 0,
233 finger_id: 0,
234 warn_mark: 0,
235 sc_scope: -1, agent: Default::default(),
237 theme_err: None,
238 }
239 }
240}
241
242#[derive(Clone, Copy)]
244pub(crate) enum Sel {
245 Rec(usize),
246 Point,
247}
248
249pub(crate) fn skip_ws(p: &[u8]) -> &[u8] {
253 let i = p.iter().position(|&c| c != b' ' && c != b'\t').unwrap_or(p.len());
254 &p[i..]
255}
256
257pub(crate) fn word_eq<'a>(p: &'a [u8], w: &str) -> Option<&'a [u8]> {
260 let w = w.as_bytes();
261 if !p.starts_with(w) {
262 return None;
263 }
264 match p.get(w.len()) {
265 None | Some(b' ') | Some(b'\t') => Some(&p[w.len()..]),
266 _ => None,
267 }
268}
269
270pub(crate) fn next_word(p: &[u8]) -> &[u8] {
272 let i = p.iter().position(|&c| c == b' ' || c == b'\t').unwrap_or(p.len());
273 skip_ws(&p[i..])
274}
275
276pub(crate) fn parse_qstring(p: &[u8], cap: usize) -> Option<(Vec<u8>, &[u8])> {
279 if p.first() != Some(&b'"') {
280 return None;
281 }
282 let mut out = Vec::new();
283 let mut i = 1;
284 loop {
285 let c = *p.get(i)?;
286 i += 1;
287 let ch = match c {
288 b'"' => return Some((out, &p[i..])),
289 b'\\' => {
290 let e = *p.get(i)?; i += 1;
292 match e {
293 b'n' => 10,
294 b't' => 9,
295 o => o,
296 }
297 }
298 o => o,
299 };
300 if out.len() < cap - 1 {
301 out.push(ch);
302 }
303 }
304}
305
306fn utf8_decode(s: &[u8], i: usize) -> (u32, usize) {
309 let b0 = s.get(i).copied().unwrap_or(0) as u32;
310 if b0 < 0x80 {
311 return (b0, i + 1);
312 }
313 let bad = (0xFFFD, i + 1);
314 let (mut cp, n, min) = if b0 < 0xC2 {
315 return bad;
316 } else if b0 < 0xE0 {
317 (b0 & 0x1F, 1, 0x80)
318 } else if b0 < 0xF0 {
319 (b0 & 0x0F, 2, 0x800)
320 } else if b0 < 0xF5 {
321 (b0 & 7, 3, 0x10000)
322 } else {
323 return bad;
324 };
325 let mut j = i + 1;
326 for _ in 0..n {
327 let c = s.get(j).copied().unwrap_or(0) as u32;
328 if c & 0xC0 != 0x80 {
329 return bad;
330 }
331 cp = cp << 6 | (c & 0x3F);
332 j += 1;
333 }
334 if cp < min || cp > 0x10FFFF || cp & 0xFFFF_F800 == 0xD800 {
335 return bad;
336 }
337 (cp, j)
338}
339
340fn parse_button(p: &[u8]) -> u32 {
342 match p.first().map(|c| c | 32) {
343 Some(b'r') => BTN_RIGHT,
344 Some(b'm') => BTN_MIDDLE,
345 _ => BTN_LEFT,
346 }
347}
348
349fn chord(p: &[u8]) -> Option<(u32, u32, &[u8])> {
352 let mut p = p;
353 let mut mods = 0;
354 loop {
355 if p.starts_with(b"ctrl+") {
356 mods |= MOD_CTRL;
357 p = &p[5..];
358 } else if p.starts_with(b"shift+") {
359 mods |= MOD_SHIFT;
360 p = &p[6..];
361 } else if p.starts_with(b"alt+") {
362 mods |= MOD_ALT;
363 p = &p[4..];
364 } else {
365 break;
366 }
367 }
368 let vk = key_lookup(p);
370 (vk != 0).then(|| (mods, vk, next_word(p)))
371}
372
373fn mod_word(p: &[u8]) -> u32 {
376 if p.starts_with(b"shift") {
377 MOD_SHIFT
378 } else if p.starts_with(b"ctrl") {
379 MOD_CTRL
380 } else if p.starts_with(b"alt") {
381 MOD_ALT
382 } else {
383 0
384 }
385}
386
387fn parse_pct(p: &[u8]) -> (i32, &[u8]) {
389 let (v, r) = parse_int(p);
390 let v = v.clamp(0, 100) as u32;
391 ((v * 65535 / 100) as i32, r)
392}
393
394fn key_lookup(p: &[u8]) -> u32 {
396 let c0 = p.first().copied().unwrap_or(0);
397 let c1 = p.get(1).copied().unwrap_or(0);
398 if c1 == b' ' || c1 == 0 {
399 if c0.is_ascii_lowercase() {
401 return (c0 - 32) as u32;
402 }
403 if c0.is_ascii_uppercase() || c0.is_ascii_digit() {
404 return c0 as u32;
405 }
406 }
407 const NAMES: &[(&str, u32)] = &[
408 ("enter", VK_RETURN), ("esc", VK_ESCAPE), ("tab", VK_TAB), ("space", VK_SPACE),
409 ("backspace", VK_BACK), ("delete", VK_DELETE), ("left", VK_LEFT), ("right", VK_RIGHT),
410 ("up", VK_UP), ("down", VK_DOWN), ("home", VK_HOME), ("end", VK_END),
411 ("pgup", VK_PRIOR), ("pgdn", VK_NEXT), ("insert", VK_INSERT), ("alt", VK_MENU), ("apps", VK_APPS),
412 ("plus", VK_OEM_PLUS), ("minus", VK_OEM_MINUS),
413 ("[", VK_OEM_4), ("]", VK_OEM_6), (",", VK_OEM_COMMA), (".", VK_OEM_PERIOD),
415 ("-", VK_OEM_MINUS), ("+", VK_OEM_PLUS), ("/", VK_OEM_2),
416 ("numadd", VK_ADD), ("numsub", VK_SUBTRACT), ("nummul", VK_MULTIPLY), ("numdiv", VK_DIVIDE),
417 ("numdec", VK_DECIMAL),
418 ("num0", VK_NUMPAD0), ("num1", VK_NUMPAD0 + 1), ("num2", VK_NUMPAD0 + 2), ("num3", VK_NUMPAD0 + 3),
419 ("num4", VK_NUMPAD0 + 4), ("num5", VK_NUMPAD0 + 5), ("num6", VK_NUMPAD0 + 6), ("num7", VK_NUMPAD0 + 7),
420 ("num8", VK_NUMPAD0 + 8), ("num9", VK_NUMPAD0 + 9),
421 ];
422 for &(n, vk) in NAMES {
423 if word_eq(p, n).is_some() {
424 return vk;
425 }
426 }
427 if c0 | 32 == b'f' {
429 let (n, _) = parse_int(&p[1..]);
430 if (1..=12).contains(&n) {
431 return VK_F1 + n as u32 - 1;
432 }
433 }
434 0
435}
436
437fn json_escape(out: &mut Vec<u8>, src: &[u8], cap: usize) {
440 let lim = cap - 8;
441 let mut n = 0;
442 for &c in src {
443 if n >= lim || c == 0 {
444 break;
445 }
446 match c {
447 b'"' | b'\\' => {
448 out.push(b'\\');
449 out.push(c);
450 n += 2;
451 }
452 c if c < 32 => {
453 out.push(b' '); n += 1;
455 }
456 c => {
457 out.push(c);
458 n += 1;
459 }
460 }
461 }
462}
463
464const WFLAG_NAMES: [&str; 10] = ["tool", "dialog", "modal", "resizable", "noclose", "sysframe", "noactivate", "autoheight", "keys", "touchhold"];
465
466fn blit_copy(dst: &mut Surface, src: &Surface, x: i32, y: i32) {
468 let x0 = x.max(0);
469 let y0 = y.max(0);
470 let x1 = (x + src.w).min(dst.w);
471 let y1 = (y + src.h).min(dst.h);
472 if x1 <= x0 || y1 <= y0 {
473 return;
474 }
475 let n = (x1 - x0) as usize * 4;
476 for yy in y0..y1 {
477 let d = yy as usize * dst.stride as usize + x0 as usize * 4;
478 let s = (yy - y) as usize * src.stride as usize + (x0 - x) as usize * 4;
479 dst.px[d..d + n].copy_from_slice(&src.px[s..s + n]);
480 }
481}
482
483fn crop_surface(sf: &Surface, dpi: i32, c: [i32; 4]) -> Option<Surface> {
486 let x = dp_at(c[0], dpi);
487 let y = dp_at(c[1], dpi);
488 let mut w = dp_at(c[2], dpi);
489 let mut h = dp_at(c[3], dpi);
490 if x < 0 || y < 0 {
491 return None;
492 }
493 let ww = sf.w - x;
494 if ww <= 0 {
495 return None;
496 }
497 w = w.min(ww);
498 let hh = sf.h - y;
499 if hh <= 0 {
500 return None;
501 }
502 h = h.min(hh);
503 if w <= 0 || h <= 0 {
504 return None;
505 }
506 let mut out = Surface::new(w, h, sf.fmt);
507 for r in 0..h as usize {
508 let s = (y as usize + r) * sf.stride as usize + x as usize * 4;
509 let d = r * out.stride as usize;
510 out.px[d..d + w as usize * 4].copy_from_slice(&sf.px[s..s + w as usize * 4]);
511 }
512 Some(out)
513}
514
515impl Ui {
516 pub fn dump(&mut self, path: Option<&[u8]>) {
521 let mut out: Vec<u8> = Vec::new();
522 self.dump_header(&mut out);
523 for i in 1..WIN_MAX as u32 {
524 if self.uiw_drawable(i) {
525 self.dump_window(&mut out, i);
526 }
527 }
528 for k in 0..self.sems.len() {
529 let r = self.sems[k].clone();
530 self.dump_record(&mut out, &r);
531 }
532 match path {
533 Some(p) => {
534 super::perm::trusted(|| plat::write_file(&String::from_utf8_lossy(p), &out));
535 }
536 None => plat::write_stdout(&out),
537 }
538 }
539
540 fn dump_header(&self, out: &mut Vec<u8>) {
541 let mut s = String::new();
542 let scale = ((self.wp.rt.dpi as u32).wrapping_mul(100).wrapping_add(48)) / 96;
543 let _ = write!(
544 s,
545 "{{\"frame\":{},\"size\":[{},{}],\"scale\":{},\"theme\":\"{}\",\"focus\":{},\"hot\":{},\"keys\":{}}}\n",
546 self.frame,
547 self.px2dip(self.wp.rt.fb.w),
548 self.px2dip(self.wp.rt.fb.h),
549 scale,
550 if self.theme_dark { "dark" } else { "light" },
551 self.wp.ctx.focus,
552 self.wp.ctx.hot,
553 self.uiw_rec_id(self.uiw_keys_window()) );
555 out.extend_from_slice(s.as_bytes());
556 }
557
558 fn dump_window(&self, out: &mut Vec<u8>, i: u32) {
561 let rec = &self.m.win.recs[i as usize]; let mut title = Vec::new();
563 json_escape(&mut title, self.uiw_title(i).as_bytes(), 180);
564 let mut flags = String::new();
565 for (b, n) in WFLAG_NAMES.iter().enumerate() {
566 if rec.flags & (1 << b) != 0 {
567 if !flags.is_empty() {
568 flags.push(' ');
569 }
570 flags.push_str(n);
571 }
572 }
573 let dpi = self.win_dpi(i);
574 let fb = self.win_fb(i);
575 out.extend_from_slice(alloc::format!("{{\"window\":{},\"title\":\"", rec.id).as_bytes());
576 out.extend_from_slice(&title);
577 let r = rec.rect.get(); let s = alloc::format!(
579 "\",\"flags\":\"{}\",\"rect\":[{},{},{},{}],\"size\":[{},{}],\"scale\":{},\"active\":{},\"focus\":{}}}\n",
580 flags,
581 r.x,
582 r.y,
583 r.w,
584 r.h,
585 px2dip_at(fb.w, dpi),
586 px2dip_at(fb.h, dpi),
587 ((dpi as u32).wrapping_mul(100).wrapping_add(48)) / 96,
588 (i == self.m.win.active) as u32,
589 self.uiw_parts(i).map_or(0, |p| p.ctx.focus)
590 );
591 out.extend_from_slice(s.as_bytes());
592 }
593
594 fn dump_record(&self, out: &mut Vec<u8>, r: &Sem) {
596 let mut st = String::new();
597 for (k, n) in STATE_NAMES.iter().enumerate() {
598 if r.state & (1 << k) != 0 {
599 if !st.is_empty() {
600 st.push(',');
601 }
602 st.push('"');
603 st.push_str(n);
604 st.push('"');
605 }
606 }
607 let mut lab = Vec::new();
608 json_escape(&mut lab, r.label.as_deref().unwrap_or("").as_bytes(), 180);
609 let dpi = self.win_dpi(r.win as u32);
611 let kind = if r.kind < K_KIND_COUNT { KIND_NAMES[r.kind as usize] } else { KIND_NAMES[0] };
612 out.extend_from_slice(
613 alloc::format!("{{\"l\":{},\"d\":{},\"id\":{},\"k\":\"{}\",\"t\":\"", r.layer, r.depth, r.id, kind).as_bytes(),
614 );
615 out.extend_from_slice(&lab);
616 let mut s = alloc::format!(
617 "\",\"r\":[{},{},{},{}],\"s\":[{}],\"v\":{},\"max\":{}",
618 px2dip_at(r.rect[0], dpi),
619 px2dip_at(r.rect[1], dpi),
620 px2dip_at(r.rect[2], dpi),
621 px2dip_at(r.rect[3], dpi),
622 st,
623 r.value,
624 r.max
625 );
626 if r.win != 0 {
627 let _ = write!(s, ",\"w\":{}", self.uiw_rec_id(r.win as u32));
629 }
630 s.push_str("}\n");
631 out.extend_from_slice(s.as_bytes());
632 }
633
634 pub(crate) fn script_run(&mut self, app: &mut dyn App, path: &str) -> i32 {
638 let Some(data) = super::perm::trusted(|| plat::read_file(path)) else {
639 plat::write_stderr(alloc::format!("error: cannot read script {}\n", path).as_bytes());
640 return 2;
641 };
642 self.sc.fails = 0;
643 self.sc.parse_err = false;
644 self.sc.lineno = 0;
645 self.dup_check = true;
646 let mut pos = 0;
647 while pos < data.len() && data[pos] != 0 {
648 self.sc.lineno += 1;
649 let mut line = Vec::new();
651 while pos < data.len() && data[pos] != 0 {
652 let c = data[pos];
653 pos += 1;
654 if c == b'\n' {
655 break;
656 }
657 if c == b'\r' {
658 continue;
659 }
660 if line.len() < LINE_MAX - 1 {
661 line.push(c);
662 }
663 }
664 if self.script_exec(app, &line) {
665 break; }
667 if self.sc.parse_err {
668 break;
669 }
670 }
671 if self.sc.parse_err {
672 2
673 } else if self.sc.fails != 0 {
674 1
675 } else {
676 0
677 }
678 }
679
680 pub(crate) fn fail(&mut self, detail: &[u8]) {
682 self.fail_more(detail);
683 plat::write_stdout(b"\n");
684 }
685
686 fn fail_more(&mut self, detail: &[u8]) {
688 self.sc.fails += 1;
689 let mut o = alloc::format!("FAIL line {}: ", self.sc.lineno).into_bytes();
690 o.extend_from_slice(&self.sc.line);
691 o.extend_from_slice(b" -> ");
692 o.extend_from_slice(detail);
693 plat::write_stdout(&o);
694 }
695
696 pub(crate) fn sel_fail(&mut self) {
698 let m: &[u8] = match self.sc.sel_err {
699 2 => b"ambiguous selector",
700 3 => b"bad selector",
701 _ => b"no such widget",
702 };
703 self.fail(m);
704 }
705
706 fn script_exec(&mut self, app: &mut dyn App, line: &[u8]) -> bool {
708 self.sc.line = line.to_vec(); let l = skip_ws(line);
710 if l.is_empty() || l[0] == b'#' {
711 return false;
712 }
713 const CMDS: [&str; 54] = [
714 "quit", "print", "size", "theme", "move", "down", "up", "click", "rclick", "dblclick", "drag", "wheel",
715 "key", "keys", "type", "commit", "cmd", "wait", "shot", "dump", "expect", "bench", "window", "close",
716 "movewin", "resizewin", "activate", "scale", "sync", "expect_warn", "expect_nowarn", "focus", "drop",
717 "clip", "pointer", "system", "dragover", "dragleave", "dragout", "screen", "perm", "agent", "inspect",
718 "style", "sensor", "location", "tap", "longpress", "swipe", "flick", "finger", "pinch", "pan2", "gpu",
719 ];
720 let Some(&c) = CMDS.iter().find(|c| word_eq(l, c).is_some()) else {
721 let mut o = alloc::format!("error: line {}: unknown command: ", self.sc.lineno).into_bytes();
722 o.extend_from_slice(l);
723 o.push(b'\n');
724 plat::write_stderr(&o);
725 self.sc.parse_err = true;
726 return false;
727 };
728 let a = next_word(l);
729 match c {
730 "quit" => return true,
731 "system" => self.sc_system(app, a),
732 "sensor" => self.sc_sensor(app, a),
733 "location" => self.sc_location(app, a),
734 "print" => {
735 let mut o = a.to_vec();
736 o.push(b'\n');
737 plat::write_stdout(&o);
738 }
739 "size" => {
740 let (w, r) = parse_int(a);
741 let (h, _) = parse_int(r);
742 let (w, h) = (self.dp(w), self.dp(h));
743 self.resize(app, w, h);
744 }
745 "theme" => {
746 if let Some(r) = word_eq(a, "file") {
747 let path = String::from(String::from_utf8_lossy(skip_ws(r)).trim());
749 let res = super::perm::trusted(|| self.load_theme(&path));
750 self.sc.theme_err = res.err().map(|e| {
751 plat::write_stdout(alloc::format!("theme: {}\n", e).as_bytes());
752 e.msg
753 });
754 } else if word_eq(a, "none").is_some() {
755 self.clear_theme();
756 } else if word_eq(a, "contrast").is_some() {
757 self.set_contrast(Some(true));
758 } else {
759 self.set_contrast(Some(false));
760 let dark = word_eq(a, "dark").is_some();
761 self.theme_set(dark as u32);
762 }
763 self.cmd_run(UIC_THEME, 0);
764 self.run_passes(app);
765 }
766 "move" => match self.resolve(a).0 {
767 None => self.sel_fail(),
768 Some(_) => {
769 let (x, y) = (self.sc.sel_x, self.sc.sel_y);
770 self.mouse_to(app, x, y);
771 }
772 },
773 "down" => self.sc_button(app, a, plat::E_DOWN),
774 "up" => self.sc_button(app, a, plat::E_UP),
775 "click" => {
776 let (s, rest) = self.resolve(a);
777 if s.is_none() {
778 self.sel_fail();
779 } else {
780 let (x, y) = (self.sc.sel_x, self.sc.sel_y);
781 self.mouse_to(app, x, y);
782 let b = parse_button(skip_ws(rest));
783 self.click_here(app, b);
784 }
785 }
786 "rclick" => {
787 if self.resolve(a).0.is_none() {
788 self.sel_fail();
789 } else {
790 let (x, y) = (self.sc.sel_x, self.sc.sel_y);
791 self.mouse_to(app, x, y);
792 self.click_here(app, BTN_RIGHT);
793 }
794 }
795 "dblclick" => {
796 let (s, rest) = self.resolve(a);
797 if s.is_none() {
798 self.sel_fail();
799 } else {
800 let saved_mods = self.sc.script_mods;
802 let mut mods = saved_mods;
803 let mut q = skip_ws(rest);
804 while !q.is_empty() {
805 let m = mod_word(q);
806 if m == 0 {
807 let mut d = b"unknown dblclick option ".to_vec();
808 d.extend_from_slice(q);
809 self.fail(&d);
810 return false;
811 }
812 mods |= m;
813 q = next_word(q);
814 }
815 let (x, y) = (self.sc.sel_x, self.sc.sel_y);
816 self.mouse_to(app, x, y);
817 self.sc.script_mods = mods;
818 self.click_here(app, BTN_LEFT);
819 self.advance_time(app, 50);
820 self.click_here(app, BTN_LEFT);
821 self.sc.script_mods = saved_mods;
822 }
823 }
824 "drag" => self.sc_drag(app, a),
825 "wheel" => self.sc_wheel(app, a),
826 "tap" => self.sc_tap(app, a, 0),
827 "longpress" => self.sc_tap(app, a, 600),
828 "swipe" => self.sc_swipe(app, a),
829 "flick" => self.sc_flick(app, a),
830 "finger" => self.sc_finger(app, a),
831 "pinch" => self.sc_two(app, a, true),
832 "pan2" => self.sc_two(app, a, false),
833 "key" => self.sc_key(app, a),
834 "keys" => self.sc_keys(app, a),
835 "type" => self.sc_type(app, a),
836 "commit" => self.sc_commit(app, a),
837 "cmd" => {
838 let (id, r) = parse_int(a);
839 let (arg, _) = parse_int(r);
840 self.cmd_run(id as u32, arg as u32);
841 self.run_passes(app);
842 }
843 "wait" => {
844 let (ms, _) = parse_int(a);
845 self.advance_time(app, ms);
846 }
847 "shot" => self.sc_shot(app, a),
848 "dump" => self.dump(if a.is_empty() { None } else { Some(a) }),
849 "expect" => self.sc_expect(a),
850 "bench" => self.sc_bench(app, a),
851 "window" => {
852 self.sc.sc_scope = if word_eq(a, "any").is_some() {
853 -1
854 } else if word_eq(a, "main").is_some() {
855 0
856 } else {
857 parse_int(a).0
858 };
859 }
860 "close" => self.sc_close(app, a),
861 "movewin" => {
862 let (i, r) = self.window_arg(a);
863 if i <= 0 {
864 self.fail(b"no such window");
865 } else {
866 let (x, r) = parse_int(r);
867 let (y, _) = parse_int(r);
868 let (x, y) = self.uiw_snap_dip(i as u32, x, y);
870 self.uiw_user_rect(i as u32, x, y, 0, 0);
871 self.run_passes(app);
872 }
873 }
874 "resizewin" => {
875 let (i, r) = self.window_arg(a);
876 if i < 0 {
877 self.fail(b"no such window");
878 } else {
879 let (w, r) = parse_int(r);
880 let (h, _) = parse_int(r);
881 if i == 0 {
882 let (w, h) = (self.dp(w), self.dp(h));
884 self.resize(app, w, h);
885 } else {
886 self.uiw_user_rect(i as u32, i32::MIN, 0, w, h);
887 self.run_passes(app);
888 }
889 }
890 }
891 "activate" => {
892 let (i, _) = self.window_arg(a);
893 if i < 0 {
894 self.fail(b"no such window");
895 } else {
896 let id = self.uiw_rec_id(i as u32);
897 self.window_activate(id);
898 self.run_passes(app);
899 }
900 }
901 "scale" => self.sc_scale(app, a),
902 "screen" => {
903 self.m.win.screen = if word_eq(a, "off").is_some() {
905 [0; 4]
906 } else {
907 let (x, r) = parse_int(a);
908 let (y, r) = parse_int(r);
909 let (w, r) = parse_int(r);
910 let (h, _) = parse_int(r);
911 [x, y, w.max(0), h.max(0)]
912 };
913 self.invalidate();
914 self.run_passes(app);
915 }
916 "sync" => self.sc_sync(app, a),
917 "style" => {
918 let name = String::from_utf8_lossy(skip_ws(a)).into_owned();
920 if self.set_style(name.trim()) {
921 self.run_passes(app);
922 } else {
923 self.fail(b"no such style");
924 }
925 }
926 "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" => {
931 let (w, m) = (self.warns, self.sc.warn_mark);
932 self.sc.warn_mark = w;
933 if w == m {
934 self.fail(b"no warning");
935 }
936 }
937 "expect_nowarn" => {
938 let d = self.warns.wrapping_sub(self.sc.warn_mark);
939 self.sc.warn_mark = self.warns;
940 if d != 0 {
941 self.fail(alloc::format!("{} warnings", d as i32).as_bytes());
942 }
943 }
944 "focus" => match self.resolve(a).0 {
945 None => self.sel_fail(),
946 Some(s) => {
947 let id = self.sel_rec(s).id;
948 if id == 0 {
949 self.fail(b"the widget has no id");
950 } else {
951 self.focus(id);
952 self.run_passes(app);
953 }
954 }
955 },
956 "drop" => self.sc_drop(app, a),
957 "dragover" => self.sc_dragover(app, a, false),
958 "dragleave" => self.sc_dragover(app, a, true),
959 "dragout" => self.sc_dragout(app, a),
960 "clip" => self.sc_clip(app, a),
961 "pointer" => self.sc_pointer(app, a),
962 _ => {}
963 }
964 false
965 }
966
967 pub(crate) fn sel_rec(&self, s: Sel) -> Sem {
969 match s {
970 Sel::Rec(i) => self.sems[i].clone(),
971 Sel::Point => Sem::default(),
972 }
973 }
974
975 pub(crate) fn resolve<'a>(&mut self, p: &'a [u8]) -> (Option<Sel>, &'a [u8]) {
981 self.sc.sel_err = 3;
982 self.sc.sel_kind = -1;
983 self.sc.sel_wf = self.scope_index(); self.sc.sel_win = 0;
985 let c0 = p.first().copied().unwrap_or(0);
986 if c0 == b'#' {
987 return self.resolve_id(p);
988 }
989 let mut q = p;
990 if c0 != b'"' {
991 if c0.is_ascii_digit() || c0 == b'-' {
992 return self.resolve_point(p);
993 }
994 let mut found = false;
996 for (k, n) in KIND_NAMES.iter().enumerate() {
997 if p.starts_with(n.as_bytes()) && p.get(n.len()) == Some(&b':') {
998 self.sc.sel_kind = k as i32;
999 q = &p[n.len() + 1..];
1000 found = true;
1001 break;
1002 }
1003 }
1004 if !found || q.first() != Some(&b'"') {
1005 self.sc.sel_err = 3;
1006 return (None, p);
1007 }
1008 }
1009 self.resolve_label(p, q)
1010 }
1011
1012 fn resolve_point<'a>(&mut self, p: &'a [u8]) -> (Option<Sel>, &'a [u8]) {
1014 let (x, r) = parse_int(p);
1015 if r.first() != Some(&b',') {
1016 self.sc.sel_err = 3;
1017 return (None, p);
1018 }
1019 let (y, r) = parse_int(&r[1..]);
1020 let w = self.sc.sel_wf.max(0) as u32;
1021 self.sc.sel_win = w;
1022 let dpi = self.win_dpi(w);
1023 self.sc.sel_x = dp_at(x, dpi);
1024 self.sc.sel_y = dp_at(y, dpi);
1025 (Some(Sel::Point), r)
1026 }
1027
1028 fn sel_win_ok(&self, r: &Sem) -> bool {
1030 self.sc.sel_wf < 0 || self.sc.sel_wf as u8 == r.win
1031 }
1032
1033 fn resolve_id<'a>(&mut self, p: &'a [u8]) -> (Option<Sel>, &'a [u8]) {
1034 let q = &p[1..];
1035 let (id, rest) = if q.len() >= 2 && q[0] == b'0' && q[1] == b'x' {
1036 parse_hex(q)
1037 } else {
1038 let (v, r) = parse_int(q);
1039 (v as u32, r)
1040 };
1041 let mut mask = 0u32;
1043 for r in self.sems.iter().rev() {
1044 if r.id == id && self.sel_win_ok(r) {
1045 mask |= 1 << (r.win as u32 & (WIN_MAX as u32 - 1));
1046 }
1047 }
1048 if mask == 0 {
1049 self.sc.sel_err = 1;
1050 return (None, rest);
1051 }
1052 if mask & (mask - 1) != 0 {
1053 self.warn(&alloc::format!("duplicate id {} in several windows (mask {:x})", id, mask));
1054 self.sc.sel_wf = self.win_topmost(mask) as i32;
1055 }
1056 let found = (0..self.sems.len()).rev().find(|&i| self.sems[i].id == id && self.sel_win_ok(&self.sems[i]));
1058 match found {
1059 Some(i) => {
1060 self.sel_found(i);
1061 (Some(Sel::Rec(i)), rest)
1062 }
1063 None => {
1064 self.sc.sel_err = 1;
1065 (None, rest)
1066 }
1067 }
1068 }
1069
1070 fn label_match(&self, r: &Sem) -> bool {
1071 let Some(l) = r.label.as_deref() else { return false };
1072 if !self.sel_win_ok(r) {
1073 return false;
1074 }
1075 if self.sc.sel_kind >= 0 && r.kind as i32 != self.sc.sel_kind {
1076 return false;
1077 }
1078 l.as_bytes() == self.sc.sel_label.as_slice()
1079 }
1080
1081 fn resolve_label<'a>(&mut self, p: &'a [u8], q: &'a [u8]) -> (Option<Sel>, &'a [u8]) {
1082 let Some((label, rest)) = parse_qstring(q, 256) else {
1083 self.sc.sel_err = 3;
1084 return (None, p);
1085 };
1086 self.sc.sel_label = label;
1087 let mask = self.label_windows();
1089 if mask & mask.wrapping_sub(1) != 0 {
1090 self.sc.sel_wf = self.win_topmost(mask) as i32;
1091 }
1092 let (mut n, mut nt) = (0, 0);
1094 let (mut last, mut last_nt) = (0, 0);
1095 for i in 0..self.sems.len() {
1096 if !self.label_match(&self.sems[i]) {
1097 continue;
1098 }
1099 n += 1;
1100 last = i;
1101 if self.sems[i].kind != K_TEXT {
1102 nt += 1;
1103 last_nt = i;
1104 }
1105 }
1106 if n == 0 {
1107 self.sc.sel_err = 1;
1108 return (None, rest);
1109 }
1110 let pick = if n == 1 {
1111 Some(last)
1112 } else if nt == 1 {
1113 Some(last_nt)
1114 } else {
1115 self.top_layer_match()
1118 };
1119 match pick {
1120 Some(i) => {
1121 self.sel_found(i);
1122 (Some(Sel::Rec(i)), rest)
1123 }
1124 None => {
1125 self.sc.sel_err = 2;
1126 (None, rest)
1127 }
1128 }
1129 }
1130
1131 fn sel_found(&mut self, i: usize) {
1132 let r = &self.sems[i];
1133 self.sc.sel_err = 0;
1134 self.sc.sel_win = r.win as u32 & (WIN_MAX as u32 - 1);
1135 self.sc.sel_x = (r.rect[2] >> 1).wrapping_add(r.rect[0]);
1136 self.sc.sel_y = (r.rect[3] >> 1).wrapping_add(r.rect[1]);
1137 }
1138
1139 fn top_layer_match(&self) -> Option<usize> {
1142 let mut top = -1;
1143 let mut n = 0;
1144 let mut m = 0;
1145 for i in 0..self.sems.len() {
1146 let r = &self.sems[i];
1147 if r.kind == K_TEXT || !self.label_match(r) {
1148 continue;
1149 }
1150 let l = r.layer as i32;
1151 if l < top {
1152 continue;
1153 }
1154 if l > top {
1155 top = l;
1156 n = 0;
1157 }
1158 n += 1;
1159 m = i;
1160 }
1161 if n == 1 { Some(m) } else { None }
1162 }
1163
1164 fn label_windows(&self) -> u32 {
1167 let (mut ctl, mut any) = (0u32, 0u32);
1168 for r in &self.sems {
1169 if !self.label_match(r) {
1170 continue;
1171 }
1172 let b = 1 << (r.win as u32 & (WIN_MAX as u32 - 1));
1173 any |= b;
1174 if r.kind != K_TEXT {
1175 ctl |= b;
1176 }
1177 }
1178 if ctl != 0 { ctl } else { any }
1179 }
1180
1181 fn win_topmost(&self, mask: u32) -> u32 {
1184 let modal = self.m.win.modal;
1185 if modal != 0 && mask & (1 << modal) != 0 {
1186 return modal;
1187 }
1188 let mut best = 0;
1189 let mut z: i64 = -1;
1190 for i in 0..WIN_MAX as u32 {
1191 if mask & (1 << i) == 0 {
1192 continue;
1193 }
1194 let wz = self.m.win.recs[i as usize].z as i32 as i64;
1195 if wz > z {
1196 z = wz;
1197 best = i;
1198 }
1199 }
1200 best
1201 }
1202
1203 fn scope_index(&self) -> i32 {
1205 if self.sc.sc_scope == -1 {
1206 return -1;
1207 }
1208 let i = self.uiw_index(self.sc.sc_scope as u32);
1209 if i < 0 { -2 } else { i }
1210 }
1211
1212 fn window_arg<'a>(&self, p: &'a [u8]) -> (i32, &'a [u8]) {
1214 let p = skip_ws(p);
1215 if let Some(r) = word_eq(p, "main") {
1216 return (0, r);
1217 }
1218 let (id, r) = parse_int(p);
1219 let i = self.uiw_index(id as u32);
1220 if i < 0 {
1221 return (i, r);
1222 }
1223 if self.m.win.recs[i as usize].state == 0 {
1224 return (-1, r);
1225 }
1226 (i, r)
1227 }
1228
1229 fn sc_system(&mut self, app: &mut dyn App, a: &[u8]) {
1239 let w = |n: &str| word_eq(a, n).is_some();
1240 if w("env") {
1241 let r = next_word(a);
1242 let name_len = r.iter().position(|&c| c == b' ' || c == b'\t').unwrap_or(r.len());
1243 let name = String::from_utf8_lossy(&r[..name_len]).into_owned();
1244 let value = String::from(String::from_utf8_lossy(skip_ws(&r[name_len..])).trim_end());
1245 if !name.starts_with("jfgui_") || value.is_empty() {
1246 self.fail(b"system env: a jfgui_* NAME and a VALUE (- removes it)");
1247 return;
1248 }
1249 plat::setenv(&name, if value == "-" { None } else { Some(&value) });
1250 self.run_passes(app);
1251 return;
1252 }
1253 if w("fullscreen") {
1254 let r = next_word(a);
1255 let on = word_eq(r, "on").is_some();
1256 if !on && word_eq(r, "off").is_none() {
1257 self.fail(b"system fullscreen: on or off");
1258 return;
1259 }
1260 let rest = next_word(r);
1261 let i = if skip_ws(rest).is_empty() { 0 } else { self.window_arg(rest).0 };
1262 if i < 0 {
1263 self.fail(b"no such window");
1264 return;
1265 }
1266 if self.m.win.recs[i as usize].fs != on {
1267 self.uiw_fs_changed(i as u32, on);
1268 }
1269 self.run_passes(app);
1270 return;
1271 }
1272 let (typ, data) = if w("focus") {
1273 let r = next_word(a);
1274 (if word_eq(r, "out").is_some() { plat::E_FOCUS_OUT } else { plat::E_FOCUS_IN }, 0)
1275 } else if w("suspend") {
1276 (plat::E_POWER, 1)
1277 } else if w("resume") {
1278 (plat::E_POWER, 0)
1279 } else if w("background") {
1280 (plat::E_POWER, 2) } else if w("foreground") {
1282 (plat::E_POWER, 3)
1283 } else if w("session-end") {
1284 (plat::E_SESSION_END, 0)
1285 } else if w("settings") {
1286 let r = next_word(a);
1287 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)]
1288 .iter()
1289 .find(|(n, _)| word_eq(r, n).is_some())
1290 .map_or_else(|| parse_int(r).0 as u32, |(_, b)| *b);
1291 (plat::E_SETTINGS, bits)
1292 } else if w("hotkey") {
1293 let id = parse_int(next_word(a)).0 as u32;
1295 if !plat::hotkey::hotkey_test_press(id) {
1296 self.fail(b"system hotkey: no global shortcut with this id");
1297 return;
1298 }
1299 self.inject(app, plat::E_WAKE, 0, 0, 0, 0);
1300 self.run_passes(app);
1301 return;
1302 } else if w("pick") {
1303 let r = next_word(a);
1305 if !self.m.screen.picking {
1306 self.fail(b"system pick: no colour pick is running");
1307 return;
1308 }
1309 let ans = if word_eq(r, "cancel").is_some() { Err(plat::screen::ScreenError::Cancelled) } else { Ok(super::runtime::parse_hex(r).0 & 0xFF_FFFF) };
1310 self.pick_done(ans);
1311 self.run_passes(app);
1312 return;
1313 } else {
1314 self.fail(b"system: focus in|out, suspend, resume, session-end, settings WHAT, hotkey ID, pick RRGGBB|cancel or fullscreen on|off");
1315 return;
1316 };
1317 self.inject(app, typ, 0, 0, 0, data);
1318 }
1319
1320 fn inject(&mut self, app: &mut dyn App, typ: u8, btn: u32, x: i32, y: i32, data: u32) {
1321 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 };
1322 let finger = matches!(typ, plat::E_MOVE | plat::E_DOWN | plat::E_UP) && (data >> plat::PD_TYPE_SHIFT) & 3 == PTR_TOUCH;
1324 let btn = if finger { self.sc.finger_id } else { btn };
1325 let ev = Event {
1326 typ,
1327 btn: btn as u8,
1328 mods: self.sc.script_mods as u8,
1329 win: (w & (WIN_MAX as u32 - 1)) as u8,
1330 x,
1331 y,
1332 data,
1333 };
1334 plat::evq_push(ev);
1335 self.process_events(app);
1336 }
1337
1338 fn mouse_to(&mut self, app: &mut dyn App, x: i32, y: i32) {
1340 self.sc.sc_win = self.sc.sel_win;
1341 self.sc.sc_x = x;
1342 self.sc.sc_y = y;
1343 let d = self.sc.sc_pdata;
1344 self.inject(app, plat::E_MOVE, 0, x, y, d);
1345 }
1346
1347 fn click_here(&mut self, app: &mut dyn App, btn: u32) {
1349 let (x, y, d) = (self.sc.sc_x, self.sc.sc_y, self.sc.sc_pdata);
1350 self.inject(app, plat::E_DOWN, btn, x, y, d);
1351 let (x, y, d) = (self.sc.sc_x, self.sc.sc_y, self.sc.sc_pdata);
1352 self.inject(app, plat::E_UP, btn, x, y, d);
1353 }
1354
1355 fn sc_button(&mut self, app: &mut dyn App, a: &[u8], typ: u8) {
1357 let mut rest = a;
1358 let c = a.first().map(|c| c | 32);
1359 if !a.is_empty() && !matches!(c, Some(b'l' | b'r' | b'm')) {
1360 let (s, r) = self.resolve(a);
1361 if s.is_none() {
1362 self.sel_fail();
1363 return;
1364 }
1365 let (x, y) = (self.sc.sel_x, self.sc.sel_y);
1366 self.mouse_to(app, x, y);
1367 rest = skip_ws(r);
1368 }
1369 let b = parse_button(rest);
1370 let (x, y, d) = (self.sc.sc_x, self.sc.sc_y, self.sc.sc_pdata);
1371 self.inject(app, typ, b, x, y, d);
1372 }
1373
1374 fn sc_drag(&mut self, app: &mut dyn App, a: &[u8]) {
1376 let mut pen_type = 0u32;
1377 let (mut p0, mut p1) = (0i32, 0i32);
1378 let saved_pdata = self.sc.sc_pdata;
1379 let (s, r) = self.resolve(a);
1380 if s.is_none() {
1381 self.sel_fail();
1382 return;
1383 }
1384 let (bx, by) = (self.sc.sel_x, self.sc.sel_y);
1385 let press_win = self.sc.sel_win; let (s, r) = self.resolve(skip_ws(r));
1387 if s.is_none() {
1388 self.sel_fail();
1389 return;
1390 }
1391 self.point_to_window(self.sc.sel_win, press_win);
1393 self.sc.sel_win = press_win;
1394 let (ex, ey) = (self.sc.sel_x, self.sc.sel_y);
1395 let mut q = skip_ws(r);
1396 let mut steps = 8;
1397 if q.first().is_some_and(|c| c.is_ascii_digit()) {
1399 let (n, r) = parse_int(q);
1400 if n > 0 {
1401 steps = n;
1402 }
1403 q = r;
1404 }
1405 let mut btn = BTN_LEFT;
1407 let mut hold = 0;
1408 let saved_mods = self.sc.script_mods;
1409 let mut mods = saved_mods;
1410 loop {
1411 let w = skip_ws(q);
1412 if w.is_empty() {
1413 break;
1414 }
1415 let mut after = w;
1416 let m = mod_word(w);
1417 if m != 0 {
1418 mods |= m;
1419 } else if w.starts_with(b"right") {
1420 btn = BTN_RIGHT;
1421 } else if w.starts_with(b"middle") {
1422 btn = BTN_MIDDLE;
1423 } else if w.starts_with(b"pen") || w.starts_with(b"eraser") {
1424 pen_type = if w.starts_with(b"pen") { PTR_PEN } else { PTR_ERASER };
1426 let (a0, r) = parse_pct(next_word(w));
1427 let (a1, r) = parse_pct(skip_ws(r));
1428 p0 = a0;
1429 p1 = a1;
1430 after = r;
1431 } else if let Some(r) = word_eq(w, "hold") {
1432 let (ms, r) = parse_int(skip_ws(r));
1433 hold = ms;
1434 after = r;
1435 } else {
1436 let mut d = b"unknown drag option ".to_vec();
1437 d.extend_from_slice(w);
1438 self.fail(&d);
1439 return;
1440 }
1441 let i = after.iter().position(|&c| c <= 32).unwrap_or(after.len());
1443 q = &after[i..];
1444 }
1445 self.sc.script_mods = mods;
1446 if pen_type != 0 {
1447 self.sc.sc_pdata = plat::evq_pen_data(p0, pen_type, 0, 0); }
1449 self.mouse_to(app, bx, by);
1450 let d = self.sc.sc_pdata;
1451 self.inject(app, plat::E_DOWN, btn, bx, by, d);
1452 self.advance_time(app, hold);
1453 for i in 1..=steps {
1454 if pen_type != 0 {
1455 let pr = (p1 - p0).wrapping_mul(i) / steps + p0;
1457 self.sc.sc_pdata = plat::evq_pen_data(pr, pen_type, 0, 0);
1458 }
1459 let x = bx + (ex - bx).wrapping_mul(i) / steps;
1461 let y = by + (ey - by).wrapping_mul(i) / steps;
1462 self.mouse_to(app, x, y);
1463 self.advance_time(app, 16);
1464 }
1465 let d = self.sc.sc_pdata;
1466 self.inject(app, plat::E_UP, btn, ex, ey, d);
1467 self.sc.script_mods = saved_mods;
1468 self.sc.sc_pdata = saved_pdata; }
1470
1471 fn finger_at(&mut self, app: &mut dyn App, typ: u8, x: i32, y: i32) {
1473 self.sc.sc_win = self.sc.sel_win;
1474 self.sc.sc_x = x;
1475 self.sc.sc_y = y;
1476 let btn = if typ == plat::E_MOVE { 0 } else { BTN_LEFT };
1477 self.inject(app, typ, btn, x, y, PTR_TOUCH << plat::PD_TYPE_SHIFT);
1478 }
1479
1480 fn sc_tap(&mut self, app: &mut dyn App, a: &[u8], rest_ms: i32) {
1482 let (s, r) = self.resolve(a);
1483 if s.is_none() {
1484 self.sel_fail();
1485 return;
1486 }
1487 let (x, y) = (self.sc.sel_x, self.sc.sel_y);
1488 let q = skip_ws(r);
1489 let ms = if rest_ms > 0 && q.first().is_some_and(|c| c.is_ascii_digit()) { parse_int(q).0 } else { rest_ms };
1490 self.finger_at(app, plat::E_DOWN, x, y);
1491 self.advance_time(app, ms);
1492 self.finger_at(app, plat::E_UP, x, y);
1493 }
1494
1495 fn finger_path(&mut self, app: &mut dyn App, b: [i32; 2], e: [i32; 2], ms: i32, hold: bool) {
1497 self.finger_at(app, plat::E_DOWN, b[0], b[1]);
1498 let ms = ms.max(1);
1499 let steps = (ms / 8).max(1);
1500 for i in 1..=steps {
1501 self.advance_time(app, ms * i / steps - ms * (i - 1) / steps);
1502 let x = b[0] + (e[0] - b[0]).wrapping_mul(i) / steps;
1503 let y = b[1] + (e[1] - b[1]).wrapping_mul(i) / steps;
1504 self.finger_at(app, plat::E_MOVE, x, y);
1505 }
1506 if hold {
1507 self.advance_time(app, 100);
1508 }
1509 self.finger_at(app, plat::E_UP, e[0], e[1]);
1510 }
1511
1512 fn sc_swipe(&mut self, app: &mut dyn App, a: &[u8]) {
1514 let (s, r) = self.resolve(a);
1515 if s.is_none() {
1516 self.sel_fail();
1517 return;
1518 }
1519 let b = [self.sc.sel_x, self.sc.sel_y];
1520 let press_win = self.sc.sel_win;
1521 let (s, r) = self.resolve(skip_ws(r));
1522 if s.is_none() {
1523 self.sel_fail();
1524 return;
1525 }
1526 self.point_to_window(self.sc.sel_win, press_win);
1528 self.sc.sel_win = press_win;
1529 let e = [self.sc.sel_x, self.sc.sel_y];
1530 let mut q = skip_ws(r);
1531 let mut ms = 300;
1532 if q.first().is_some_and(|c| c.is_ascii_digit()) {
1533 let (n, r) = parse_int(q);
1534 ms = n;
1535 q = skip_ws(r);
1536 }
1537 self.finger_path(app, b, e, ms, word_eq(q, "hold").is_some());
1538 }
1539
1540 fn sc_flick(&mut self, app: &mut dyn App, a: &[u8]) {
1542 let (s, r) = self.resolve(a);
1543 if s.is_none() {
1544 self.sel_fail();
1545 return;
1546 }
1547 let b = [self.sc.sel_x, self.sc.sel_y];
1548 let (dx, r) = parse_int(r);
1549 let (dy, r) = parse_int(r);
1550 let q = skip_ws(r);
1551 let ms = if q.first().is_some_and(|c| c.is_ascii_digit()) { parse_int(q).0 } else { 60 };
1552 let dpi = self.win_dpi(self.sc.sel_win);
1553 let e = [b[0] + dp_at(dx, dpi), b[1] + dp_at(dy, dpi)];
1554 self.finger_path(app, b, e, ms, false);
1555 }
1556
1557 fn sc_finger(&mut self, app: &mut dyn App, a: &[u8]) {
1559 if a.first().is_some_and(|c| c.is_ascii_digit()) {
1560 let (n, r) = parse_int(a);
1561 self.sc.finger_id = n as u32;
1562 self.sc_finger(app, skip_ws(r));
1563 self.sc.finger_id = 0;
1564 return;
1565 }
1566 if word_eq(a, "up").is_some() || word_eq(a, "cancel").is_some() {
1567 let (x, y) = (self.sc.sc_x, self.sc.sc_y);
1568 let cancel = word_eq(a, "cancel").is_some();
1569 self.sc.sel_win = self.sc.sc_win;
1570 if cancel {
1571 self.inject(app, plat::E_UP, 0, x, y, PTR_TOUCH << plat::PD_TYPE_SHIFT | plat::PD_CANCEL);
1572 } else {
1573 self.finger_at(app, plat::E_UP, x, y);
1574 }
1575 return;
1576 }
1577 let (typ, s) = if let Some(r) = word_eq(a, "down") {
1578 (plat::E_DOWN, r)
1579 } else if let Some(r) = word_eq(a, "move") {
1580 (plat::E_MOVE, r)
1581 } else {
1582 self.fail(b"finger: down S, move S, up or cancel");
1583 return;
1584 };
1585 if self.resolve(skip_ws(s)).0.is_none() {
1586 self.sel_fail();
1587 return;
1588 }
1589 if typ == plat::E_MOVE {
1590 self.point_to_window(self.sc.sel_win, self.sc.sc_win);
1592 self.sc.sel_win = self.sc.sc_win;
1593 }
1594 let (x, y) = (self.sc.sel_x, self.sc.sel_y);
1595 self.finger_at(app, typ, x, y);
1596 }
1597
1598 fn sc_two(&mut self, app: &mut dyn App, a: &[u8], pinch: bool) {
1600 let (s, r) = self.resolve(a);
1601 if s.is_none() {
1602 self.sel_fail();
1603 return;
1604 }
1605 let c = [self.sc.sel_x, self.sc.sel_y];
1606 let (p0, r) = parse_int(r);
1607 let (p1, r) = parse_int(r);
1608 let q = skip_ws(r);
1609 let ms = if q.first().is_some_and(|c| c.is_ascii_digit()) { parse_int(q).0 } else { 300 }.max(1);
1610 let dpi = self.win_dpi(self.sc.sel_win);
1611 let (h0, h1, way) = if pinch {
1613 (dp_at(p0, dpi) / 2, dp_at(p1, dpi) / 2, [0, 0])
1614 } else {
1615 (dp_at(40, dpi), dp_at(40, dpi), [dp_at(p0, dpi), dp_at(p1, dpi)])
1616 };
1617 self.two_at(app, plat::E_DOWN, c, h0);
1618 let steps = (ms / 8).max(1);
1619 for i in 1..=steps {
1620 self.advance_time(app, ms * i / steps - ms * (i - 1) / steps);
1621 let h = h0 + (h1 - h0) * i / steps;
1622 self.two_at(app, plat::E_MOVE, [c[0] + way[0] * i / steps, c[1] + way[1] * i / steps], h);
1623 }
1624 self.two_at(app, plat::E_UP, [c[0] + way[0], c[1] + way[1]], h1);
1625 }
1626
1627 fn two_at(&mut self, app: &mut dyn App, typ: u8, c: [i32; 2], h: i32) {
1629 for (id, sign) in [(0u32, -1), (1, 1)] {
1630 self.sc.finger_id = id;
1631 self.finger_at(app, typ, c[0] + sign * h, c[1]);
1632 }
1633 self.sc.finger_id = 0;
1634 }
1635
1636 fn sc_wheel(&mut self, app: &mut dyn App, a: &[u8]) {
1639 let (s, r) = self.resolve(a);
1640 if s.is_none() {
1641 self.sel_fail();
1642 return;
1643 }
1644 let (dy, r) = parse_int(r);
1645 let saved_mods = self.sc.script_mods;
1646 let mut mods = saved_mods;
1647 let mut q = skip_ws(r);
1648 while !q.is_empty() {
1649 let m = mod_word(q);
1650 if m == 0 {
1651 let mut d = b"unknown wheel option ".to_vec();
1652 d.extend_from_slice(q);
1653 self.fail(&d);
1654 return;
1655 }
1656 mods |= m;
1657 q = next_word(q);
1658 }
1659 let (x, y) = (self.sc.sel_x, self.sc.sel_y);
1660 self.mouse_to(app, x, y);
1661 let (sx, sy) = (self.sc.sc_x, self.sc.sc_y);
1662 self.sc.script_mods = mods;
1663 self.inject(app, plat::E_WHEEL, 0, sx, sy, dy as u16 as u32);
1664 self.sc.script_mods = saved_mods;
1665 }
1666
1667 fn point_to_window(&mut self, from: u32, to: u32) {
1669 if from == to {
1670 return;
1671 }
1672 let dpi = self.win_dpi(from);
1673 let (dx, dy) = self.uiw_desk_pos(from);
1674 let x = dx + px2dip_at(self.sc.sel_x, dpi);
1675 let y = dy + px2dip_at(self.sc.sel_y, dpi);
1676 let (tx, ty) = self.uiw_desk_pos(to);
1677 let tdpi = self.win_dpi(to);
1678 self.sc.sel_x = dp_at(x - tx, tdpi);
1679 self.sc.sel_y = dp_at(y - ty, tdpi);
1680 }
1681
1682 fn sc_pointer(&mut self, app: &mut dyn App, a: &[u8]) {
1685 let names = ["mouse", "pen", "eraser", "touch"];
1686 let Some(t) = names.iter().position(|n| word_eq(a, n).is_some()) else {
1687 let mut d = b"unknown pointer type ".to_vec();
1688 d.extend_from_slice(a);
1689 self.fail(&d);
1690 return;
1691 };
1692 let mut pressure = -1;
1693 let (mut tx, mut ty) = (0, 0);
1694 let q = next_word(a);
1695 if q.first().is_some_and(|c| c.is_ascii_digit()) {
1696 let (pv, r) = parse_pct(q);
1697 pressure = pv;
1698 let r = skip_ws(r);
1699 if r.first().is_some_and(|&c| c == b'-' || c.is_ascii_digit()) {
1700 let (x, r) = parse_int(r);
1701 let (y, _) = parse_int(skip_ws(r));
1702 tx = x;
1703 ty = y;
1704 }
1705 }
1706 self.sc.sc_pdata = plat::evq_pen_data(pressure, t as u32, tx, ty);
1707 if self.m.touch.on != (t as u32 == PTR_TOUCH) {
1709 self.touch_mode_set(t as u32 == PTR_TOUCH);
1710 self.run_passes(app);
1711 }
1712 }
1713
1714 fn sc_key(&mut self, app: &mut dyn App, a: &[u8]) {
1716 let Some(keys) = self.chords(a) else { return };
1717 for k in keys {
1718 self.press(app, k >> 16, k & 0xFFFF);
1719 }
1720 }
1721
1722 fn sc_keys(&mut self, app: &mut dyn App, a: &[u8]) {
1725 let (n, r) = if a.first().is_some_and(|c| c.is_ascii_digit()) { parse_int(a) } else { (1, a) };
1726 let Some(keys) = self.chords(r) else { return };
1727 let w = self.m.win.active & (WIN_MAX as u32 - 1);
1728 if w != 0 && !self.uiw_drawable(w) {
1729 self.fail(b"the window with the keyboard is not shown");
1730 return;
1731 }
1732 let all: Vec<u32> = (0..n.max(0)).flat_map(|_| keys.iter().copied()).collect();
1733 self.uiw_use(w);
1734 self.keys_inject(&all);
1735 self.m.win.edges |= 1 << w; self.uiw_use(0);
1737 self.run_passes(app);
1738 }
1739
1740 fn chords(&mut self, a: &[u8]) -> Option<Vec<u32>> {
1743 let mut out = Vec::new();
1744 let mut p = a;
1745 loop {
1746 p = skip_ws(p);
1747 if p.is_empty() {
1748 return Some(out);
1749 }
1750 let Some((mods, vk, rest)) = chord(p) else {
1751 self.fail(b"unknown key");
1752 return None;
1753 };
1754 out.push(vk | mods << 16);
1755 p = rest;
1756 }
1757 }
1758
1759 fn press(&mut self, app: &mut dyn App, mods: u32, vk: u32) {
1761 self.sc.script_mods = mods;
1762 self.inject(app, plat::E_KEY_DOWN, 0, 0, 0, vk);
1763 self.inject(app, plat::E_KEY_UP, 0, 0, 0, vk);
1764 self.sc.script_mods = 0;
1765 }
1766
1767 fn sc_commit(&mut self, app: &mut dyn App, a: &[u8]) {
1770 let Some((s, r)) = parse_qstring(a, 1024) else {
1771 self.fail(b"expected a quoted string");
1772 return;
1773 };
1774 let text = String::from_utf8_lossy(&s).repeat(parse_int(r).0.max(1) as usize);
1775 let w = (self.m.win.active & (WIN_MAX as u32 - 1)) as u8;
1776 let mut at = 0;
1777 while at < text.len() {
1778 let n = plat::ime_commit_text(w, &text[at..]);
1779 if n == 0 {
1780 self.fail(b"the commit was not queued");
1781 return;
1782 }
1783 at += n;
1784 self.process_events(app);
1785 }
1786 }
1787
1788 fn sc_type(&mut self, app: &mut dyn App, a: &[u8]) {
1790 let Some((s, _)) = parse_qstring(a, 256) else {
1791 self.fail(b"expected a quoted string");
1792 return;
1793 };
1794 let mut i = 0;
1795 loop {
1796 let (cp, n) = utf8_decode(&s, i);
1797 if cp == 0 {
1798 break;
1799 }
1800 i = n;
1801 if cp == 10 {
1802 self.inject(app, plat::E_KEY_DOWN, 0, 0, 0, VK_RETURN);
1803 self.inject(app, plat::E_KEY_UP, 0, 0, 0, VK_RETURN);
1804 } else {
1805 self.inject(app, plat::E_TEXT, 0, 0, 0, cp);
1806 }
1807 }
1808 }
1809
1810 fn advance_time(&mut self, app: &mut dyn App, ms: i32) {
1812 let mut left = ms;
1813 while left > 0 {
1814 let step = left.min(16);
1815 left -= step;
1816 let t = self.now_ms.wrapping_add(step as u32);
1817 self.set_time(t);
1818 if self.touch_busy() {
1819 self.process_events(app); }
1821 let mut fired = self.tasks_ready(); loop {
1823 let (id, _) = self.timer_due();
1824 if id == u32::MAX {
1825 break;
1826 }
1827 self.cmd_run(UIC_TIMER, id);
1828 fired = true;
1829 }
1830 if self.animating || fired {
1831 self.run_passes(app);
1832 }
1833 }
1834 }
1835
1836 fn sc_shot(&mut self, app: &mut dyn App, a: &[u8]) {
1838 let _ = app;
1839 let i = a.iter().position(|&c| c == b' ' || c == b'\t').unwrap_or(a.len());
1841 let file = String::from_utf8_lossy(&a[..i]).into_owned();
1842 let mut rest = skip_ws(if i < a.len() { &a[i + 1..] } else { &a[i..] });
1843 let mut win = 0u32;
1844 let mut desktop = false;
1845 if let Some(r) = word_eq(rest, "desktop") {
1846 desktop = true;
1847 rest = r;
1848 } else if let Some(r) = word_eq(rest, "window") {
1849 let (w, r) = self.window_arg(r);
1850 if w < 0 {
1851 self.fail(b"no such window");
1852 return;
1853 }
1854 win = w as u32;
1855 rest = r;
1856 }
1857 let mut crop = None;
1858 let q = skip_ws(rest);
1859 if !q.is_empty() {
1860 let (x, r) = parse_int(q);
1861 let (y, r) = parse_int(r);
1862 let (w, r) = parse_int(r);
1863 let (h, _) = parse_int(r);
1864 crop = Some([x, y, w, h]);
1865 }
1866 self.render();
1867 self.gp_settle_all();
1868 let ok = if desktop {
1869 self.shot_desktop(&file, crop)
1870 } else {
1871 let sf = self.win_fb(win);
1872 match crop {
1873 Some(c) => match crop_surface(sf, self.win_dpi(win), c) {
1874 Some(v) => Ui::save_png(&v, &file),
1875 None => false,
1876 },
1877 None => Ui::save_png(sf, &file),
1878 }
1879 };
1880 if !ok {
1881 self.fail(b"cannot write the screenshot");
1882 return;
1883 }
1884 plat::write_stdout(alloc::format!("wrote {}\n", file).as_bytes());
1885 }
1886
1887 fn shot_desktop(&mut self, path: &str, crop: Option<[i32; 4]>) -> bool {
1891 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)];
1893 let scr = self.m.win.screen;
1894 if scr[2] > 0 && scr[3] > 0 {
1895 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])];
1896 }
1897 for i in 1..WIN_MAX as u32 {
1898 if !self.uiw_drawable(i) {
1899 continue;
1900 }
1901 let r = self.win_desk_rect(i);
1902 bb[0] = bb[0].min(r[0]);
1903 bb[1] = bb[1].min(r[1]);
1904 bb[2] = bb[2].max(r[0] + r[2]);
1905 bb[3] = bb[3].max(r[1] + r[3]);
1906 }
1907 let mut sf = Surface::new(dp_at(bb[2] - bb[0], dpi), dp_at(bb[3] - bb[1], dpi), FMT_XRGB);
1908 if sf.px.is_empty() {
1909 return false;
1910 }
1911 let mut dc = gfx::Dc::new(sf.w, sf.h);
1912 dc.set_color(0xFF5A_6B7C); let (sw, sh) = (sf.w, sf.h);
1914 dc.fill(&mut sf, 0, 0, sw, sh);
1915 self.sd_blit(&mut sf, 0, bb[0], bb[1], dpi);
1917 let mut last_z: i64 = -1;
1918 loop {
1919 let mut next = None;
1920 let mut nz: i64 = i32::MAX as i64;
1921 for i in 1..WIN_MAX as u32 {
1922 if !self.uiw_drawable(i) {
1923 continue;
1924 }
1925 let z = self.m.win.recs[i as usize].z as i32 as i64;
1926 if z > last_z && z < nz {
1927 nz = z;
1928 next = Some(i);
1929 }
1930 }
1931 let Some(i) = next else { break };
1932 last_z = nz;
1933 self.sd_blit(&mut sf, i, bb[0], bb[1], dpi);
1934 self.uiw_popups_blit(&mut sf, i, bb[0], bb[1], dpi); }
1936 self.uiw_popups_blit(&mut sf, 0, bb[0], bb[1], dpi); match crop {
1938 Some(c) => match crop_surface(&sf, dpi, c) {
1939 Some(v) => Ui::save_png(&v, path),
1940 None => false,
1941 },
1942 None => Ui::save_png(&sf, path),
1943 }
1944 }
1945
1946 fn sd_blit(&mut self, sf: &mut Surface, i: u32, x0: i32, y0: i32, dpi: i32) {
1948 let r = self.win_desk_rect(i);
1949 let x = dp_at(r[0] - x0, dpi);
1950 let y = dp_at(r[1] - y0, dpi);
1951 blit_copy(sf, self.win_fb(i), x, y);
1952 }
1953
1954 fn win_desk_rect(&self, i: u32) -> [i32; 4] {
1956 let (x, y) = self.uiw_desk_pos(i);
1957 let dpi = self.win_dpi(i);
1958 let fb = self.win_fb(i);
1959 [x, y, px2dip_at(fb.w, dpi), px2dip_at(fb.h, dpi)]
1960 }
1961
1962 fn sc_expect(&mut self, a: &[u8]) {
1964 if let Some(r) = word_eq(a, "window") {
1965 self.expect_window(r);
1966 return;
1967 }
1968 if let Some(r) = word_eq(a, "sound") {
1969 let want = skip_ws(r);
1971 let got = self.m.sound.last.take();
1972 let name = got.map_or("none", plat::audio::sound_name);
1973 if !want.starts_with(name.as_bytes()) || (want.len() > name.len() && want[name.len()] > b' ') {
1974 self.fail(alloc::format!("sound {name}").as_bytes());
1975 }
1976 return;
1977 }
1978 if let Some(r) = word_eq(a, "keyboard") {
1979 let on = self.m.touch.kb_on;
1981 if word_eq(skip_ws(r), "shown").is_some() != on {
1982 self.fail(if on { b"keyboard shown" } else { b"keyboard hidden" });
1983 }
1984 return;
1985 }
1986 if let Some(r) = word_eq(a, "wm") {
1987 let want = String::from(String::from_utf8_lossy(skip_ws(r)).trim_end());
1989 let got = super::chrome::wm_req_name(core::mem::take(&mut self.m.chrome.wm_req));
1990 if want != got {
1991 self.fail(alloc::format!("wm {got}").as_bytes());
1992 }
1993 return;
1994 }
1995 if let Some(r) = word_eq(a, "scrollcopy") {
1996 let want = skip_ws(r);
1999 let got = core::mem::take(&mut self.m.scopy.frames);
2000 self.m.scopy.cells = 0;
2001 let ok = if word_eq(want, "none").is_some() { got == 0 } else { got as i32 >= parse_int(want).0.max(1) };
2002 if !ok {
2003 self.fail(alloc::format!("scroll copies: {got}").as_bytes());
2004 }
2005 return;
2006 }
2007 if let Some(r) = word_eq(a, "theme") {
2009 self.expect_theme(skip_ws(r));
2010 return;
2011 }
2012 if let Some(r) = word_eq(a, "style") {
2013 let want = String::from_utf8_lossy(skip_ws(r));
2014 let have = self.style_name();
2015 if want.trim() != have {
2016 self.fail(alloc::format!("style is {}", have).as_bytes());
2017 }
2018 return;
2019 }
2020 if let Some(r) = word_eq(a, "token") {
2021 self.expect_token(skip_ws(r));
2022 return;
2023 }
2024 if let Some(r) = word_eq(a, "pixel") {
2025 self.expect_pixel(r);
2026 return;
2027 }
2028 if let Some(r) = word_eq(a, "same") {
2029 self.expect_same(skip_ws(r));
2030 return;
2031 }
2032 let (s, rest) = self.resolve(a);
2033 if s.is_none() && self.sc.sel_err == 3 {
2034 self.sel_fail(); return;
2036 }
2037 let cond = skip_ws(rest);
2038 if word_eq(cond, "missing").is_some() {
2040 if s.is_some() {
2041 self.fail(b"widget exists");
2042 }
2043 return;
2044 }
2045 let Some(s) = s else {
2046 self.sel_fail();
2047 return;
2048 };
2049 let rec = self.sel_rec(s);
2050 const STATES: [(&str, u16, bool); 11] = [
2051 ("hover", SS_HOVER, true),
2052 ("nohover", SS_HOVER, false),
2053 ("unpressed", SS_PRESSED, false),
2054 ("focused", SS_FOCUSED, true),
2055 ("checked", SS_CHECKED, true),
2056 ("unchecked", SS_CHECKED, false),
2057 ("disabled", SS_DISABLED, true),
2058 ("enabled", SS_DISABLED, false),
2059 ("selected", SS_SELECTED, true),
2060 ("open", SS_OPEN, true),
2061 ("pressed", SS_PRESSED, true),
2062 ];
2063 for (n, mask, want) in STATES {
2064 if word_eq(cond, n).is_some() {
2065 let set = rec.state & mask != 0;
2066 if set != want {
2067 self.fail(if set { b"state is set" } else { b"state is not set" });
2068 }
2069 return;
2070 }
2071 }
2072 if let Some(r) = word_eq(cond, "value") {
2073 let (lo, rest) = parse_int(r);
2074 let hi = if rest.starts_with(b"..") { parse_int(&rest[2..]).0 } else { lo };
2076 if rec.value < lo || rec.value > hi {
2077 self.fail(alloc::format!("value is {}", rec.value).as_bytes());
2078 }
2079 } else if let Some(r) = word_eq(cond, "max") {
2080 if parse_int(r).0 != rec.max {
2081 self.fail(alloc::format!("max is {}", rec.max).as_bytes());
2082 }
2083 } else if let Some(r) = word_eq(cond, "text") {
2084 let Some((t, _)) = parse_qstring(skip_ws(r), 256) else {
2085 self.fail(b"expected a quoted string");
2086 return;
2087 };
2088 let have = rec.label.as_deref().unwrap_or("");
2089 if have.as_bytes() != t.as_slice() {
2090 let mut d = b"text is \"".to_vec();
2091 d.extend_from_slice(have.as_bytes());
2092 d.push(b'"');
2093 self.fail(&d);
2094 }
2095 } else if word_eq(cond, "exists").is_some() {
2096 } else if let Some(r) = word_eq(cond, "rect") {
2097 self.expect_rect(r, &rec);
2098 } else {
2099 self.fail(b"unknown condition");
2100 }
2101 }
2102
2103 fn expect_rect(&mut self, p: &[u8], rec: &Sem) {
2106 let mut want = [None; 4];
2107 let mut q = p;
2108 for v in want.iter_mut() {
2109 q = skip_ws(q);
2110 if q.first() == Some(&b'*') {
2111 q = &q[1..];
2112 continue;
2113 }
2114 let (n, r) = parse_int(q);
2115 *v = Some(n);
2116 q = r;
2117 }
2118 let dpi = self.win_dpi(rec.win as u32);
2119 let have = rec.rect.map(|v| px2dip_at(v, dpi));
2120 if have.iter().zip(want).any(|(h, w)| w.is_some_and(|w| w != *h)) {
2121 self.fail_more(alloc::format!("rect is {} {}", have[0], have[1]).as_bytes());
2122 plat::write_stdout(alloc::format!(" {} {}\n", have[2], have[3]).as_bytes());
2123 }
2124 }
2125
2126 fn expect_theme(&mut self, p: &[u8]) {
2129 if let Some(r) = word_eq(p, "error") {
2130 let r = skip_ws(r);
2131 let have = self.sc.theme_err.clone();
2132 if word_eq(r, "none").is_some() {
2133 if let Some(h) = have {
2134 self.fail(alloc::format!("error: {}", h).as_bytes());
2135 }
2136 return;
2137 }
2138 let Some((t, _)) = parse_qstring(r, 256) else {
2139 self.fail(b"expected a quoted string");
2140 return;
2141 };
2142 let t = String::from_utf8_lossy(&t).into_owned();
2143 match have {
2144 Some(h) if h.contains(&t) => {}
2145 Some(h) => self.fail(alloc::format!("error: {}", h).as_bytes()),
2146 None => self.fail(b"no error"),
2147 }
2148 return;
2149 }
2150 let have = self.app_theme().map(|t| t.name.clone());
2151 if word_eq(p, "none").is_some() {
2152 if let Some(h) = have {
2153 self.fail(alloc::format!("theme is \"{}\"", h).as_bytes());
2154 }
2155 return;
2156 }
2157 let want = match parse_qstring(p, 256) {
2158 Some((t, _)) => String::from_utf8_lossy(&t).into_owned(),
2159 None => String::from(String::from_utf8_lossy(p).trim()),
2160 };
2161 match have {
2162 Some(h) if h == want => {}
2163 Some(h) => self.fail(alloc::format!("theme is \"{}\"", h).as_bytes()),
2164 None => self.fail(b"no theme"),
2165 }
2166 }
2167
2168 fn expect_token(&mut self, p: &[u8]) {
2170 let name = String::from_utf8_lossy(&p[..p.iter().position(|&c| c == b' ' || c == b'\t').unwrap_or(p.len())]).into_owned();
2171 let want = super::theme::parse_color(String::from_utf8_lossy(next_word(p)).trim());
2172 let (Some(i), Some(want)) = (super::theme::token_index(&name), want) else {
2173 self.fail(b"expect token NAME #RRGGBB | #AARRGGBB");
2174 return;
2175 };
2176 let have = self.color(i as u32 * 4);
2177 if have != want {
2178 self.fail(alloc::format!("token is #{:08X}", have).as_bytes());
2179 }
2180 }
2181
2182 fn expect_pixel(&mut self, p: &[u8]) {
2184 let (x, r) = parse_int(p);
2185 let (y, r) = parse_int(r);
2186 let Some(want) = super::theme::parse_color(String::from_utf8_lossy(skip_ws(r)).trim()) else {
2187 self.fail(b"expect pixel X Y #RRGGBB");
2188 return;
2189 };
2190 self.render();
2191 self.gp_settle_all();
2192 let dpi = self.win_dpi(0);
2193 let sf = self.win_fb(0);
2194 let (px, py) = (dp_at(x, dpi), dp_at(y, dpi));
2195 if px < 0 || py < 0 || px >= sf.w || py >= sf.h {
2196 self.fail(b"outside the window");
2197 return;
2198 }
2199 let o = py as usize * sf.stride as usize + px as usize * 4;
2200 let have = u32::from_le_bytes([sf.px[o], sf.px[o + 1], sf.px[o + 2], sf.px[o + 3]]) & 0xFF_FFFF;
2201 if have != want & 0xFF_FFFF {
2202 self.fail(alloc::format!("pixel is #{:06X}", have).as_bytes());
2203 }
2204 }
2205
2206 fn expect_same(&mut self, p: &[u8]) {
2209 let p = skip_ws(p);
2210 let i = p.iter().position(|&c| c == b' ' || c == b'\t').unwrap_or(p.len());
2211 let path = String::from(String::from_utf8_lossy(&p[..i]).trim());
2212 let q = skip_ws(&p[i..]);
2213 self.render();
2214 self.gp_settle_all();
2215 let sf = self.win_fb(0);
2216 let png = if q.is_empty() {
2217 img_png::encode(&sf.px, sf.w as usize, sf.h as usize, sf.stride as usize, false)
2218 } else {
2219 let (x, r) = parse_int(q);
2220 let (y, r) = parse_int(r);
2221 let (w, r) = parse_int(r);
2222 let (h, _) = parse_int(r);
2223 match crop_surface(sf, self.win_dpi(0), [x, y, w, h]) {
2224 Some(v) => img_png::encode(&v.px, v.w as usize, v.h as usize, v.stride as usize, false),
2225 None => {
2226 self.fail(b"the crop lies outside the window");
2227 return;
2228 }
2229 }
2230 };
2231 let file = super::perm::trusted(|| plat::read_file(&path));
2232 match file {
2233 None => self.fail(b"no such file"),
2234 Some(f) if f != png => self.fail(b"the pixels differ"),
2235 Some(_) => {}
2236 }
2237 }
2238
2239 fn expect_window(&mut self, p: &[u8]) {
2242 let (i, r) = self.window_arg(p);
2243 let cond = skip_ws(r);
2244 let mut st = 0u32; if i >= 0 && self.uiw_drawable(i as u32) {
2246 st |= 1;
2247 if i as u32 == self.m.win.active {
2248 st |= 2;
2249 }
2250 if i != 0 && i as u32 == self.m.win.modal {
2251 st |= 4;
2252 }
2253 if i as u32 == self.uiw_keys_window() {
2254 st |= 8;
2255 }
2256 if self.m.win.recs[i as usize].fs {
2257 st |= 16;
2258 }
2259 }
2260 const STATES: [(&str, u32, bool); 8] = [
2261 ("open", 1, true),
2262 ("closed", 1, false),
2263 ("visible", 1, true),
2264 ("active", 2, true),
2265 ("modal", 4, true),
2266 ("keys", 8, true),
2267 ("fullscreen", 16, true),
2268 ("windowed", 16, false),
2269 ];
2270 for (n, mask, want) in STATES {
2271 if word_eq(cond, n).is_some() {
2272 let set = st & mask != 0;
2273 if set != want {
2274 self.fail(if set { b"window state is set" } else { b"window state is not set" });
2275 }
2276 return;
2277 }
2278 }
2279 if let Some(r) = word_eq(cond, "rect") {
2280 if i < 0 {
2281 self.fail(b"no such window");
2282 return;
2283 }
2284 let rect = self.m.win.recs[i as usize].rect.get(); let have = [rect.x, rect.y, rect.w, rect.h];
2286 let mut q = r;
2287 for &h in have.iter() {
2288 let (n, rr) = parse_int(q);
2289 q = rr;
2290 if n != h {
2291 self.fail_more(alloc::format!("rect is {} {}", rect.x, rect.y).as_bytes());
2292 plat::write_stdout(alloc::format!(" {} {}\n", rect.w, rect.h).as_bytes());
2293 return;
2294 }
2295 }
2296 return;
2297 }
2298 self.fail(b"unknown window condition");
2299 }
2300
2301 fn sc_close(&mut self, app: &mut dyn App, a: &[u8]) {
2303 let Some(r) = word_eq(a, "window") else {
2304 self.fail(b"expected: close window ID");
2305 return;
2306 };
2307 let (i, _) = self.window_arg(r);
2308 if i < 0 {
2309 self.fail(b"no such window");
2310 return;
2311 }
2312 self.sc.sc_win = i as u32;
2313 self.inject(app, plat::E_CLOSE, 0, 0, 0, 0);
2314 self.run_passes(app);
2315 }
2316
2317 fn sc_scale(&mut self, app: &mut dyn App, a: &[u8]) {
2319 let (p, r) = parse_int(a);
2320 let dpi = super::runtime::scale_to_dpi(p);
2321 let q = skip_ws(r);
2322 if let Some(r) = word_eq(q, "window") {
2323 let (i, _) = self.window_arg(r);
2324 if i < 0 {
2325 self.fail(b"no such window");
2326 return;
2327 }
2328 if i != 0 {
2329 self.uiw_dpi(i as u32, dpi);
2330 self.run_passes(app);
2331 return;
2332 }
2333 }
2334 let w = self.px2dip(self.wp.rt.fb.w);
2336 let h = self.px2dip(self.wp.rt.fb.h);
2337 self.uiw_dpi(0, dpi);
2338 let (w, h) = (self.dp(w), self.dp(h));
2339 self.resize(app, w, h);
2340 self.run_passes(app);
2341 }
2342
2343 fn sc_sync(&mut self, app: &mut dyn App, a: &[u8]) {
2346 let (ms, _) = parse_int(a);
2347 let ms = if ms > 0 { ms } else { 10000 };
2348 let deadline = plat::time_us() + ms as u64 * 1000;
2349 while self.uiw_sync_pending() || self.tasks_busy() || self.spell_busy() {
2350 plat::wait_events(plat::time_us() + 2_000, &mut |e| {
2353 plat::evq_push(*e);
2354 });
2355 self.process_events(app);
2356 self.run_passes(app);
2357 if plat::time_us() >= deadline {
2358 self.fail(b"background jobs still running");
2359 break;
2360 }
2361 }
2362 self.process_events(app); self.spell_poll(); self.run_passes(app);
2365 }
2366
2367 fn sc_bench(&mut self, app: &mut dyn App, a: &[u8]) {
2371 let (n, r) = parse_int(a);
2372 let w = skip_ws(r);
2373 if !w.is_empty() {
2374 self.sc_bench_moving(app, n.max(1) as usize, w);
2375 return;
2376 }
2377 let n = if n > 0 { n as u64 } else { 100 };
2378 let t0 = plat::time_us();
2379 for _ in 0..n {
2380 self.run_passes(app);
2381 }
2382 let build = plat::time_us() - t0;
2383 let t0 = plat::time_us();
2384 for _ in 0..n {
2385 self.dirty_force_full();
2386 self.render();
2387 }
2388 let full = plat::time_us() - t0;
2389 let t0 = plat::time_us();
2390 for _ in 0..n {
2391 self.run_passes(app);
2392 self.render();
2393 }
2394 let idle = plat::time_us() - t0;
2395 let s = alloc::format!(
2396 "bench: frame build {} us, full render {} us, unchanged frame {} us ({}x{} px)\n",
2397 build / n,
2398 full / n,
2399 idle / n,
2400 self.wp.rt.fb.w,
2401 self.wp.rt.fb.h
2402 );
2403 plat::write_stdout(s.as_bytes());
2404 }
2405
2406 fn sc_bench_moving(&mut self, app: &mut dyn App, n: usize, a: &[u8]) {
2410 enum Kind {
2411 Anim,
2412 Drag { from: (i32, i32), to: (i32, i32), btn: u32, mods: u32, win: u32 },
2413 Wheel { at: (i32, i32), dy: i32, mods: u32, win: u32 },
2414 Key { mods: u32, vk: u32 },
2415 Type(Vec<u32>),
2416 }
2417 let opts = |r: &[u8], drag: bool| -> Result<(u32, u32, usize), Vec<u8>> {
2419 let (mut btn, mut mods, mut q) = (BTN_LEFT, 0, r);
2420 loop {
2421 q = skip_ws(q);
2422 if q.is_empty() || q[0].is_ascii_digit() {
2423 return Ok((btn, mods, r.len() - q.len()));
2424 }
2425 let m = mod_word(q);
2426 if m != 0 {
2427 mods |= m;
2428 } else if drag && word_eq(q, "right").is_some() {
2429 btn = BTN_RIGHT;
2430 } else if drag && word_eq(q, "middle").is_some() {
2431 btn = BTN_MIDDLE;
2432 } else {
2433 let mut d = b"unknown bench option ".to_vec();
2434 d.extend_from_slice(q);
2435 return Err(d);
2436 }
2437 let i = q.iter().position(|&c| c == b' ' || c == b'\t').unwrap_or(q.len());
2438 q = &q[i..];
2439 }
2440 };
2441 let (kind, rest): (Kind, &[u8]) = if let Some(r) = word_eq(a, "anim") {
2442 (Kind::Anim, r)
2443 } else if let Some(r) = word_eq(a, "drag") {
2444 let (s, r) = self.resolve(skip_ws(r));
2445 if s.is_none() {
2446 self.sel_fail();
2447 return;
2448 }
2449 let (from, win) = ((self.sc.sel_x, self.sc.sel_y), self.sc.sel_win);
2450 let (s, r) = self.resolve(skip_ws(r));
2451 if s.is_none() {
2452 self.sel_fail();
2453 return;
2454 }
2455 self.point_to_window(self.sc.sel_win, win);
2456 let to = (self.sc.sel_x, self.sc.sel_y);
2457 match opts(r, true) {
2458 Ok((btn, mods, k)) => (Kind::Drag { from, to, btn, mods, win }, &r[k..]),
2459 Err(d) => {
2460 self.fail(&d);
2461 return;
2462 }
2463 }
2464 } else if let Some(r) = word_eq(a, "wheel") {
2465 let (s, r) = self.resolve(skip_ws(r));
2466 if s.is_none() {
2467 self.sel_fail();
2468 return;
2469 }
2470 let (at, win) = ((self.sc.sel_x, self.sc.sel_y), self.sc.sel_win);
2471 let (dy, r) = parse_int(skip_ws(r));
2472 match opts(r, false) {
2473 Ok((_, mods, k)) => (Kind::Wheel { at, dy, mods, win }, &r[k..]),
2474 Err(d) => {
2475 self.fail(&d);
2476 return;
2477 }
2478 }
2479 } else if let Some(r) = word_eq(a, "key") {
2480 let Some((mods, vk, r)) = chord(skip_ws(r)) else {
2481 self.fail(b"unknown key");
2482 return;
2483 };
2484 (Kind::Key { mods, vk }, r)
2485 } else if let Some(r) = word_eq(a, "type") {
2486 let Some((s, r)) = parse_qstring(skip_ws(r), 256) else {
2487 self.fail(b"expected a quoted string");
2488 return;
2489 };
2490 (Kind::Type(String::from_utf8_lossy(&s).chars().map(|c| c as u32).collect()), r)
2491 } else {
2492 self.fail(b"expected: bench N [anim | drag S1 S2 [options] | wheel S DY [options] | key CHORD | type \"s\"] [MAX_US]");
2493 return;
2494 };
2495 let (max, _) = parse_int(skip_ws(rest));
2496 let name = match kind {
2497 Kind::Anim => "anim",
2498 Kind::Drag { .. } => "drag",
2499 Kind::Wheel { .. } => "wheel",
2500 Kind::Key { .. } => "key",
2501 Kind::Type(_) => "type",
2502 };
2503 let runs = if max > 0 { 3 } else { 1 };
2504 let mut best = u64::MAX;
2505 for run in 0..runs {
2506 let mut t: Vec<(u64, u64)> = Vec::with_capacity(n);
2508 match kind {
2509 Kind::Anim => {
2510 for _ in 0..n {
2511 t.push(self.bench_frame(app, |ui, app| ui.run_passes(app)));
2512 }
2513 }
2514 Kind::Drag { from, to, btn, mods, win } => {
2515 let (a, b) = if run % 2 == 0 { (from, to) } else { (to, from) };
2517 let saved = self.sc.script_mods;
2518 self.sc.sel_win = win;
2519 self.mouse_to(app, a.0, a.1);
2520 self.sc.script_mods = mods;
2521 let d = self.sc.sc_pdata;
2522 self.inject(app, plat::E_DOWN, btn, a.0, a.1, d);
2523 for i in 1..=n as i32 {
2524 let (x, y) = (a.0 + (b.0 - a.0) * i / n as i32, a.1 + (b.1 - a.1) * i / n as i32);
2525 t.push(self.bench_frame(app, |ui, app| ui.mouse_to(app, x, y)));
2526 }
2527 let d = self.sc.sc_pdata;
2528 self.inject(app, plat::E_UP, btn, b.0, b.1, d);
2529 self.sc.script_mods = saved;
2530 }
2531 Kind::Wheel { at, dy, mods, win } => {
2532 self.sc.sel_win = win;
2533 self.mouse_to(app, at.0, at.1);
2534 let saved = self.sc.script_mods;
2535 self.sc.script_mods = mods;
2536 for i in 0..n {
2537 let v = if i % 2 == 0 { dy } else { -dy };
2538 let (x, y) = (self.sc.sc_x, self.sc.sc_y);
2539 t.push(self.bench_frame(app, |ui, app| ui.inject(app, plat::E_WHEEL, 0, x, y, v as u16 as u32)));
2540 }
2541 self.sc.script_mods = saved;
2542 }
2543 Kind::Key { mods, vk } => {
2544 for _ in 0..n {
2545 t.push(self.bench_frame(app, |ui, app| ui.press(app, mods, vk)));
2546 }
2547 }
2548 Kind::Type(ref cps) => {
2549 for _ in 0..n {
2550 t.push(self.bench_frame(app, |ui, app| {
2551 for &cp in cps {
2552 ui.inject(app, plat::E_TEXT, 0, 0, 0, cp);
2553 }
2554 }));
2555 }
2556 }
2557 }
2558 let mut s: Vec<u64> = t.iter().map(|x| x.0).collect();
2559 s.sort_unstable();
2560 let (mean, median, top) = (s.iter().sum::<u64>() / s.len() as u64, s[s.len() / 2], s[s.len() - 1]);
2561 let render = t.iter().map(|x| x.1).sum::<u64>() / t.len() as u64;
2562 best = best.min(median);
2563 let o = alloc::format!(
2564 "bench {}: {} frames, mean {} us (render {} us), median {} us, max {} us ({}x{} px)\n",
2565 name,
2566 s.len(),
2567 mean,
2568 render,
2569 median,
2570 top,
2571 self.wp.rt.fb.w,
2572 self.wp.rt.fb.h
2573 );
2574 plat::write_stdout(o.as_bytes());
2575 if max <= 0 || median <= max as u64 {
2576 return;
2577 }
2578 }
2579 self.fail(alloc::format!("the median stayed above {} us in {} runs (best {} us)", max, runs, best).as_bytes());
2580 }
2581
2582 fn bench_frame(&mut self, app: &mut dyn App, f: impl FnOnce(&mut Ui, &mut dyn App)) -> (u64, u64) {
2586 let verify = self.verify_dirty;
2587 self.verify_dirty = false;
2588 let t0 = plat::time_us();
2589 let t = self.now_ms.wrapping_add(16);
2590 self.set_time(t);
2591 loop {
2592 let (id, _) = self.timer_due();
2593 if id == u32::MAX {
2594 break;
2595 }
2596 self.cmd_run(UIC_TIMER, id);
2597 }
2598 f(self, app);
2599 let t1 = plat::time_us();
2600 self.render();
2601 let t2 = plat::time_us();
2602 self.verify_dirty = verify;
2603 if verify {
2604 self.verify_dirty();
2605 }
2606 (t2.saturating_sub(t0), t2.saturating_sub(t1))
2607 }
2608
2609 fn k1_word(p: &[u8], cap: usize) -> Option<(Vec<u8>, &[u8])> {
2614 let p = skip_ws(p);
2615 if p.is_empty() {
2616 return None;
2617 }
2618 if p[0] == b'"' {
2619 let (w, r) = parse_qstring(p, cap)?;
2620 return Some((w, skip_ws(r)));
2621 }
2622 let e = p.iter().position(|&c| c == b' ' || c == b'\t').unwrap_or(p.len());
2623 let w = p[..e.min(cap - 1)].to_vec();
2624 Some((w, skip_ws(&p[e..])))
2625 }
2626
2627 fn sc_drop(&mut self, app: &mut dyn App, a: &[u8]) {
2631 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] | drop X Y self";
2632 let (x, r) = parse_int(a);
2633 let (y, r) = parse_int(r);
2634 let w = self.scope_index().max(0) as u32;
2635 let dpi = self.win_dpi(w);
2636 let (px, py) = (dp_at(x, dpi), dp_at(y, dpi));
2637 let Some(data) = self.sc_drag_data(r) else {
2638 self.fail(BAD);
2639 return;
2640 };
2641 if data.types() == 0 && word_eq(skip_ws(r), "self").is_none() {
2642 self.fail(BAD);
2643 return;
2644 }
2645 let n = data.files.len() as u32;
2646 self.drop_inject(data);
2647 self.sc.sc_win = w;
2648 self.inject(app, plat::E_DROP, 0, px, py, n);
2649 self.run_passes(app);
2650 }
2651
2652 fn sc_drag_data(&mut self, a: &[u8]) -> Option<DragData> {
2656 let mut d = DragData::default();
2657 let mut r = a;
2658 let mut kind: &[u8] = b"files";
2659 plat::drop_reset();
2660 while let Some((word, rest)) = Ui::k1_word(r, 4096) {
2661 r = rest;
2662 let w = word.as_slice();
2663 match w {
2664 b"files" | b"text" | b"html" | b"url" | b"image" if !matches!(kind, b"text" | b"html" | b"url" | b"image") => {
2665 kind = match w {
2666 b"files" => b"files",
2667 b"text" => b"text",
2668 b"html" => b"html",
2669 b"url" => b"url",
2670 _ => b"image",
2671 };
2672 continue;
2673 }
2674 b"copy" | b"move" | b"link" if kind == b"files" => {
2675 d.effects = match w {
2676 b"copy" => plat::DE_COPY,
2677 b"move" => plat::DE_MOVE,
2678 _ => plat::DE_LINK,
2679 };
2680 continue;
2681 }
2682 b"self" if kind == b"files" => continue,
2683 _ => {}
2684 }
2685 let s = String::from_utf8_lossy(w).into_owned();
2686 match kind {
2687 b"text" => d.text = Some(s),
2688 b"html" => d.html = Some(s),
2689 b"url" => d.urls.push(s),
2690 b"image" => d.image = Some(super::perm::trusted(|| plat::read_file(&s))?),
2691 _ => {
2692 plat::drop_add(&s);
2693 }
2694 }
2695 kind = b"files"; }
2697 if matches!(kind, b"text" | b"html" | b"url" | b"image") {
2698 return None; }
2700 d.files = plat::drop_take();
2701 Some(d)
2702 }
2703
2704 fn sc_dragover(&mut self, app: &mut dyn App, a: &[u8], leave: bool) {
2707 let w = self.scope_index().max(0) as u32;
2708 self.sc.sc_win = w;
2709 if leave {
2710 self.inject(app, plat::E_DRAG, 0, -1, -1, 0);
2711 return;
2712 }
2713 let (x, r) = parse_int(a);
2714 let (y, mut r) = parse_int(r);
2715 let dpi = self.win_dpi(w);
2716 let (px, py) = (dp_at(x, dpi), dp_at(y, dpi));
2717 let (mut types, mut effects) = (0u32, 0u32);
2718 while let Some((word, rest)) = Ui::k1_word(r, 16) {
2719 r = rest;
2720 match word.as_slice() {
2721 b"files" => types |= plat::DT_FILES,
2722 b"text" => types |= plat::DT_TEXT,
2723 b"url" => types |= plat::DT_URL,
2724 b"html" => types |= plat::DT_HTML,
2725 b"image" => types |= plat::DT_IMAGE,
2726 b"copy" => effects |= plat::DE_COPY,
2727 b"move" => effects |= plat::DE_MOVE,
2728 b"link" => effects |= plat::DE_LINK,
2729 _ => {
2730 self.fail(b"expected: dragover X Y [files] [text] [url] [html] [image] [copy|move|link]");
2731 return;
2732 }
2733 }
2734 }
2735 if types == 0 {
2736 types = plat::DT_FILES;
2737 }
2738 if effects == 0 {
2739 effects = plat::DE_COPY | plat::DE_MOVE;
2740 }
2741 self.inject(app, plat::E_DRAG, 0, px, py, types | effects << 8);
2742 }
2743
2744 fn sc_dragout(&mut self, app: &mut dyn App, a: &[u8]) {
2748 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";
2749 let Some((verb, r)) = Ui::k1_word(a, 16) else {
2750 self.fail(BAD);
2751 return;
2752 };
2753 if verb == b"end" {
2754 let e = match Ui::k1_word(r, 16).map(|w| w.0) {
2755 Some(w) if w == b"copy" => plat::DE_COPY,
2756 Some(w) if w == b"move" => plat::DE_MOVE,
2757 Some(w) if w == b"link" => plat::DE_LINK,
2758 Some(w) if w == b"none" => 0,
2759 _ => {
2760 self.fail(BAD);
2761 return;
2762 }
2763 };
2764 self.inject(app, plat::E_DRAG_END, 0, 0, 0, e);
2765 return;
2766 }
2767 if verb != b"expect" {
2768 self.fail(BAD);
2769 return;
2770 }
2771 let Some((what, r)) = Ui::k1_word(r, 16) else {
2772 self.fail(BAD);
2773 return;
2774 };
2775 let out = self.dnd_out_data().cloned();
2776 let arg = Ui::k1_word(r, 4096).map(|w| w.0);
2777 let got: Result<(), String> = match (what.as_slice(), &out) {
2778 (b"none", None) => Ok(()),
2779 (b"none", Some(_)) => Err(String::from("a drag left the window")),
2780 (_, None) => Err(String::from("no drag left the window")),
2781 (b"image", Some(d)) => if d.image.is_some() { Ok(()) } else { Err(String::from("no image")) },
2782 (b"files" | b"kinds", Some(d)) => {
2783 let want = parse_int(arg.as_deref().unwrap_or(b"")).0;
2784 let n = if what == b"files" { d.files.len() as i32 } else { d.types() as i32 };
2785 if n == want { Ok(()) } else { Err(alloc::format!("{n}")) }
2786 }
2787 (b"text" | b"html" | b"url", Some(d)) => {
2788 let want = String::from_utf8_lossy(arg.as_deref().unwrap_or(b"")).into_owned();
2789 let have = match what.as_slice() {
2790 b"text" => d.text.clone(),
2791 b"html" => d.html.clone(),
2792 _ => d.urls.first().cloned(),
2793 };
2794 if have.as_deref() == Some(want.as_str()) { Ok(()) } else { Err(alloc::format!("{have:?}")) }
2795 }
2796 _ => Err(String::from("bad kind")),
2797 };
2798 if let Err(e) = got {
2799 self.fail(e.as_bytes());
2800 }
2801 }
2802
2803 fn sc_clip(&mut self, app: &mut dyn App, a: &[u8]) {
2805 const BAD: &[u8] = b"expected: clip text \"s\" | file F | dib F | clear | save F [bmp] | expect none|text|image|png";
2806 let Some((verb, r)) = Ui::k1_word(a, 16) else {
2807 self.fail(BAD);
2808 self.run_passes(app);
2809 return;
2810 };
2811 let v4 = |s: &[u8]| verb.len() >= 4 && &verb[..4] == s;
2812 let v3 = |s: &[u8]| verb.len() >= 3 && &verb[..3] == s;
2813 if verb == b"data" {
2814 let mut rest = r;
2816 let mut entries = Vec::new();
2817 let mut valid = true;
2818 while !rest.is_empty() && entries.len() < 4 {
2819 let item = Ui::k1_word(rest, 16).and_then(|(kind, next)| {
2820 let fmt = match kind.as_slice() { b"text" => CLIP_TEXT, b"html" => CLIP_HTML, b"rtf" => CLIP_RTF, b"docx" => CLIP_DOCX, _ => return None };
2821 let (file, next) = Ui::k1_word(next, 1040)?;
2822 Some((fmt, super::perm::trusted(|| plat::read_file(&String::from_utf8_lossy(&file)))?, next))
2823 });
2824 match item {
2825 Some((fmt, data, next)) => { entries.push((fmt, data)); rest = next; }
2826 None => { valid = false; break; }
2827 }
2828 }
2829 let refs: Vec<_> = entries.iter().map(|(fmt, data)| (*fmt, data.as_slice())).collect();
2830 if !valid || !rest.is_empty() || self.clip_set_many(&refs) != 0 {
2831 self.fail(b"clip data requires one to four text|html|rtf|docx PATH pairs with valid payloads");
2832 }
2833 } else if verb == b"utf8" {
2834 match Ui::k1_word(r, 1040).and_then(|(name, _)| super::perm::trusted(|| plat::read_file(&String::from_utf8_lossy(&name)))) {
2837 Some(data) if data.len() <= 4 << 20 && core::str::from_utf8(&data).is_ok() => { self.clip_set(CLIP_TEXT, &data); }
2838 _ => self.fail(b"clip utf8 requires a readable UTF-8 file of at most 4 MiB"),
2839 }
2840 } else if v4(b"text") {
2841 match parse_qstring(r, 1040) {
2842 Some((t, _)) => {
2843 self.clip_set(CLIP_TEXT, &t);
2844 }
2845 None => self.fail(BAD),
2846 }
2847 } else if v4(b"file") || v3(b"dib") {
2848 match Ui::k1_word(r, 1040) {
2849 None => self.fail(BAD),
2850 Some((f, _)) => {
2851 let name = String::from_utf8_lossy(&f).into_owned();
2852 match super::perm::trusted(|| plat::read_file(&name)) {
2853 None => {
2854 let mut d = b"cannot read ".to_vec();
2855 d.extend_from_slice(&f);
2856 self.fail(&d);
2857 }
2858 Some(data) => {
2859 let bmp = if v3(b"dib") {
2861 Some(plat::clip_dib_to_bmp(&data))
2862 } else if data.len() >= 2 && data[0] == b'B' && data[1] == b'M' {
2863 Some(plat::clip_bmp_fix(&data))
2864 } else {
2865 None
2866 };
2867 match bmp {
2868 Some(None) => self.fail(b"invalid DIB / BMP data"),
2869 Some(Some(b)) => self.hclip_img_set_parts(None, Some(&b)),
2870 None => self.hclip_img_set_parts(Some(&data), None),
2871 }
2872 }
2873 }
2874 }
2875 }
2876 } else if v4(b"clea") {
2877 self.hclip_img_clear();
2878 self.hclip_text_clear();
2879 } else if v4(b"save") {
2880 match Ui::k1_word(r, 1040) {
2881 None => self.fail(BAD),
2882 Some((f, rest)) => {
2883 let name = String::from_utf8_lossy(&f).into_owned();
2884 let data = match rest {
2885 b"text" => self.clip_read(CLIP_TEXT),
2886 b"html" => self.clip_read(CLIP_HTML),
2887 b"rtf" => self.clip_read(CLIP_RTF),
2888 b"docx" => self.clip_read(CLIP_DOCX),
2889 _ if rest.first() == Some(&b'b') => self.dr.hi_bmp.clone(),
2890 _ => self.hclip_img_get(CLIP_PNG),
2891 };
2892 match data {
2893 Some(d) => {
2894 super::perm::trusted(|| plat::write_file(&name, &d));
2895 }
2896 None => self.fail(b"the clipboard holds no image"),
2897 }
2898 }
2899 }
2900 } else if v4(b"expe") {
2901 match Ui::k1_word(r, 16) {
2902 None => self.fail(BAD),
2903 Some((w, _)) => {
2904 let text = self.hclip_text_has();
2905 let png = self.hclip_img_has(CLIP_PNG);
2906 let image = self.hclip_img_has(CLIP_IMAGE);
2907 let w4 = |s: &[u8]| w.len() >= 4 && &w[..4] == s;
2908 let ok = if w4(b"none") {
2909 Some(!text && !image && self.m.edit.hclip_rich.is_empty())
2910 } else if w == b"text-only" {
2911 Some(text && !image && self.m.edit.hclip_rich.is_empty())
2912 } else if w4(b"text") {
2913 Some(text)
2914 } else if w == b"html" {
2915 Some(self.clip_has(CLIP_HTML))
2916 } else if w == b"rtf" {
2917 Some(self.clip_has(CLIP_RTF))
2918 } else if w == b"docx" {
2919 Some(self.clip_has(CLIP_DOCX))
2920 } else if w4(b"imag") {
2921 Some(image)
2922 } else if w.len() >= 3 && &w[..3] == b"png" {
2923 Some(png)
2924 } else {
2925 None
2926 };
2927 match ok {
2928 None => self.fail(BAD),
2929 Some(false) => self.fail(alloc::format!("unexpected clipboard content (text {})", text as u32).as_bytes()),
2930 Some(true) => {}
2931 }
2932 }
2933 }
2934 } else {
2935 self.fail(BAD);
2936 }
2937 self.run_passes(app);
2938 }
2939}