1use crate::hosting::HostingView;
14use crate::objc::{self};
15#[cfg(target_os = "macos")]
18use crate::objc::NSRect;
19use crate::views::ViewHandle;
20use core::ffi::c_void;
21
22pub struct Rendered {
24 pub width: u32,
25 pub height: u32,
26 pub pixels: Vec<u8>,
27}
28
29impl Rendered {
30 pub fn diff_count(&self, other: &Rendered, tolerance: u8) -> u32 {
33 if self.width != other.width || self.height != other.height {
34 return u32::MAX;
35 }
36 if self.pixels == other.pixels {
40 return 0;
41 }
42 if tolerance == 0 {
44 return self
45 .pixels
46 .chunks_exact(4)
47 .zip(other.pixels.chunks_exact(4))
48 .filter(|(a, b)| a != b)
49 .count() as u32;
50 }
51 let mut n = 0;
52 for (a, b) in self.pixels.chunks_exact(4).zip(other.pixels.chunks_exact(4)) {
53 if a.iter().zip(b).any(|(x, y)| x.abs_diff(*y) > tolerance) {
54 n += 1;
55 }
56 }
57 n
58 }
59
60 pub fn distinct(&self) -> usize {
65 let pixels = self.pixels.chunks_exact(4);
76 let mut bits = 12;
80 let mut table = vec![u32::MAX; 1 << bits];
81 let mut has_sentinel = false;
84 let mut count = 0usize;
85 for px in pixels {
86 let key = u32::from_le_bytes([px[0], px[1], px[2], px[3]]);
87 if key == u32::MAX {
88 if !has_sentinel {
89 has_sentinel = true;
90 count += 1;
91 }
92 continue;
93 }
94 let mut i = ((key as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15) >> (64 - bits)) as usize;
98 loop {
99 let slot = table[i];
100 if slot == key {
101 break;
102 }
103 if slot == u32::MAX {
104 table[i] = key;
105 count += 1;
106 if count > (1 << bits) / 2 {
108 bits += 1;
109 let mut bigger = vec![u32::MAX; 1 << bits];
110 for &v in table.iter().filter(|&&v| v != u32::MAX) {
111 let mut j = ((v as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15)
112 >> (64 - bits)) as usize;
113 while bigger[j] != u32::MAX {
114 j = (j + 1) & ((1 << bits) - 1);
115 }
116 bigger[j] = v;
117 }
118 table = bigger;
119 }
120 break;
121 }
122 i = (i + 1) & ((1 << bits) - 1);
123 }
124 }
125 count
126 }
127
128 #[cfg(target_os = "macos")]
134 pub fn write_png(&self, path: &str) -> bool {
135 unsafe {
136 let rep = new_bitmap(self.width, self.height);
137 let data = objc::send_ret_ptr(rep, objc::sel(c"bitmapData")) as *mut u8;
138 let row = objc::send_ret_u64(rep, objc::sel(c"bytesPerRow")) as usize;
139 for y in 0..self.height as usize {
140 let src = &self.pixels[y * self.width as usize * 4..][..self.width as usize * 4];
141 core::ptr::copy_nonoverlapping(src.as_ptr(), data.add(y * row), src.len());
142 }
143 let ok = write_rep_png(rep, path);
144 objc::send(rep, objc::sel(c"release"));
145 ok
146 }
147 }
148
149 #[cfg(not(target_os = "macos"))]
151 pub fn write_png(&self, path: &str) -> bool {
152 crate::cg::write_png(self.width, self.height, &self.pixels, path)
153 }
154}
155
156#[cfg(target_os = "macos")]
158pub fn render(view: &ViewHandle, width: u32, height: u32) -> Rendered {
159 let hv = HostingView::new(view);
160 let out = render_hosting(&hv, width, height);
161 drop(hv);
165 drain();
166 out
167}
168
169pub fn drain() {
182 pump(0.0);
183}
184
185#[cfg(target_os = "macos")]
187pub fn render_hosting(hv: &HostingView, width: u32, height: u32) -> Rendered {
188 unsafe {
189 let _pool = objc::AutoreleasePool::new();
190 let app = objc::shared_app();
191 let _ = app;
192
193 let _ = offscreen_window_for(hv, width, height);
195 hv.set_frame(width as f64, height as f64);
196 capture_view(hv.as_ptr(), width, height)
197 }
198}
199
200#[cfg(target_os = "macos")]
202unsafe fn offscreen_window_for(hv: &HostingView, width: u32, height: u32) -> *mut c_void {
203 unsafe { hv.offscreen(width as f64, height as f64) }
204}
205
206#[cfg(target_os = "macos")]
216pub unsafe fn capture_view(view: *mut c_void, width: u32, height: u32) -> Rendered {
217 unsafe {
218 let _pool = objc::AutoreleasePool::new();
219 let bounds = NSRect::new(0.0, 0.0, width as f64, height as f64);
220 let rep = new_bitmap(width, height);
223 objc::send_rect_id(
224 view,
225 objc::sel(c"cacheDisplayInRect:toBitmapImageRep:"),
226 bounds,
227 rep,
228 );
229
230 let data = objc::send_ret_ptr(rep, objc::sel(c"bitmapData"));
231 let row = objc::send_ret_u64(rep, objc::sel(c"bytesPerRow")) as usize;
232 let mut pixels = vec![0u8; (width * height * 4) as usize];
233 for y in 0..height as usize {
234 core::ptr::copy_nonoverlapping(
235 data.add(y * row),
236 pixels.as_mut_ptr().add(y * width as usize * 4),
237 width as usize * 4,
238 );
239 }
240 objc::send(rep, objc::sel(c"release"));
241 Rendered { width, height, pixels }
242 }
243}
244
245#[cfg(target_os = "macos")]
254pub unsafe fn capture_window(window: *mut c_void, width: u32, height: u32) -> Rendered {
255 unsafe {
256 let content = objc::send(window, objc::sel(c"contentView"));
257 let frame_view = objc::send(content, objc::sel(c"superview"));
258 let target = if frame_view.is_null() { content } else { frame_view };
259 capture_view(target, width, height)
260 }
261}
262
263#[cfg(target_os = "macos")]
273pub fn render_chrome(view: &ViewHandle, width: u32, height: u32) -> Rendered {
274 unsafe {
275 let _pool = objc::AutoreleasePool::new();
276 let win = titled_window(view, width, height);
280 pump_until(0.45, || objc::send_ret_bool(win, objc::sel(c"isVisible")));
281 let out = capture_window(win, width, height);
282 objc::send(win, objc::sel(c"close"));
283 pump(0.01);
288 out
289 }
290}
291
292#[cfg(target_os = "macos")]
303pub fn render_sheet(view: &ViewHandle, win_w: u32, win_h: u32, w: u32, h: u32) -> Rendered {
304 unsafe {
305 let _pool = objc::AutoreleasePool::new();
306 let win = titled_window(view, win_w, win_h);
307 pump_until(0.7, || !objc::send(win, objc::sel(c"attachedSheet")).is_null());
308 let sheet = objc::send(win, objc::sel(c"attachedSheet"));
309 let out = if sheet.is_null() {
310 Rendered { width: w, height: h, pixels: vec![0; (w * h * 4) as usize] }
311 } else {
312 capture_window(sheet, w, h)
313 };
314 objc::send(win, objc::sel(c"close"));
315 pump(0.01);
320 out
321 }
322}
323
324#[cfg(target_os = "macos")]
336pub fn render_composited(view: &ViewHandle, width: u32, height: u32) -> Rendered {
337 unsafe {
338 let _pool = objc::AutoreleasePool::new();
339 let win = titled_window(view, width, height);
340 pump_until(0.7, || objc::send_ret_bool(win, objc::sel(c"isVisible")));
341 let out = capture_composited(win, width, height);
342 objc::send(win, objc::sel(c"close"));
343 pump(0.01);
348 out
349 }
350}
351
352#[cfg(target_os = "macos")]
359pub fn pump_until(limit: f64, mut ready: impl FnMut() -> bool) {
360 let step = 0.005;
366 let mut waited = 0.0;
367 while waited < limit {
368 pump(step);
369 waited += step;
370 if ready() {
371 pump(0.02);
373 return;
374 }
375 }
376}
377
378#[cfg(target_os = "macos")]
383pub unsafe fn capture_composited(window: *mut c_void, width: u32, height: u32) -> Rendered {
384 let _pool = objc::AutoreleasePool::new();
385 unsafe extern "C" {
386 fn CGWindowListCreateImage(
387 bounds: objc::NSRect,
388 list_option: u32,
389 window_id: u32,
390 image_option: u32,
391 ) -> *mut c_void;
392 }
393 unsafe {
394 let id = objc::send_ret_u64(window, objc::sel(c"windowNumber")) as u32;
395 let cg = CGWindowListCreateImage(
398 objc::NSRect::new(f64::INFINITY, f64::INFINITY, 0.0, 0.0),
400 1 << 3,
401 id,
402 (1 << 4) | 1,
403 );
404 if cg.is_null() {
405 return Rendered { width, height, pixels: vec![0; (width * height * 4) as usize] };
406 }
407 let rep = objc::send_id(
408 objc::send(objc::class(c"OBJC_CLASS_$_NSBitmapImageRep"), objc::sel(c"alloc")),
409 objc::sel(c"initWithCGImage:"),
410 cg,
411 );
412 let got_w = objc::send_ret_u64(rep, objc::sel(c"pixelsWide")) as usize;
413 let got_h = objc::send_ret_u64(rep, objc::sel(c"pixelsHigh")) as usize;
414 let row = objc::send_ret_u64(rep, objc::sel(c"bytesPerRow")) as usize;
415 let data = objc::send_ret_ptr(rep, objc::sel(c"bitmapData"));
416
417 let mut pixels = vec![0u8; (width * height * 4) as usize];
421 let copy_w = got_w.min(width as usize);
422 for y in 0..got_h.min(height as usize) {
423 core::ptr::copy_nonoverlapping(
424 data.add(y * row),
425 pixels.as_mut_ptr().add(y * width as usize * 4),
426 copy_w * 4,
427 );
428 }
429 Rendered { width, height, pixels }
430 }
431}
432
433#[cfg(target_os = "macos")]
441pub fn render_popover(view: &ViewHandle, win_w: u32, win_h: u32, w: u32, h: u32) -> Rendered {
442 unsafe {
443 let _pool = objc::AutoreleasePool::new();
444 let win = titled_window(view, win_w, win_h);
445 pump_until(0.8, || !find_window(|name| name.contains("Popover")).is_null());
451 let popover = find_window(|name| name.contains("Popover"));
452 let out = if popover.is_null() {
453 Rendered { width: w, height: h, pixels: vec![0; (w * h * 4) as usize] }
454 } else {
455 capture_window(popover, w, h)
456 };
457 objc::send(win, objc::sel(c"close"));
458 if !popover.is_null() {
462 objc::send_id(popover, objc::sel(c"orderOut:"), core::ptr::null_mut());
463 objc::send(popover, objc::sel(c"close"));
464 }
465 pump(0.01);
466 out
467 }
468}
469
470#[cfg(target_os = "macos")]
472unsafe fn find_window(pred: impl Fn(&str) -> bool) -> *mut c_void {
473 unsafe {
474 let wins = objc::send(objc::shared_app(), objc::sel(c"windows"));
475 if wins.is_null() {
476 return core::ptr::null_mut();
477 }
478 let n = objc::send_ret_u64(wins, objc::sel(c"count"));
479 for i in 0..n {
480 let w = objc::send_u64_id(wins, objc::sel(c"objectAtIndex:"), i, core::ptr::null_mut());
481 let cls = objc::send(w, objc::sel(c"className"));
482 let p = objc::send_ret_ptr(cls, objc::sel(c"UTF8String"));
483 if p.is_null() {
484 continue;
485 }
486 let name = core::ffi::CStr::from_ptr(p as *const i8).to_string_lossy();
487 if pred(&name) {
488 return w;
489 }
490 }
491 core::ptr::null_mut()
492 }
493}
494
495#[cfg(target_os = "macos")]
497unsafe fn titled_window(view: &ViewHandle, width: u32, height: u32) -> *mut c_void {
498 unsafe {
499 let app = objc::shared_app();
500 objc::send_i64(app, objc::sel(c"setActivationPolicy:"), 1);
502 objc::send_bool(
506 objc::class(c"OBJC_CLASS_$_NSWindow"),
507 objc::sel(c"setAllowsAutomaticWindowTabbing:"),
508 false,
509 );
510
511 let hosting = HostingView::new(view);
512 let bounds = NSRect::new(0.0, 0.0, width as f64, height as f64);
513 let win = objc::send(objc::class(c"OBJC_CLASS_$_NSWindow"), objc::sel(c"alloc"));
514 type InitWin =
515 unsafe extern "C" fn(*mut c_void, *const c_void, NSRect, u64, u64, bool) -> *mut c_void;
516 let init: InitWin = core::mem::transmute(objc::msg_send_addr());
517 let win = init(win, objc::sel(c"initWithContentRect:styleMask:backing:defer:"), bounds, 15, 2, false);
519
520 let name = objc::nsstring("NSAppearanceNameAqua");
521 let appearance = objc::send_id(
522 objc::class(c"OBJC_CLASS_$_NSAppearance"),
523 objc::sel(c"appearanceNamed:"),
524 name,
525 );
526 if !appearance.is_null() {
527 objc::send_id(win, objc::sel(c"setAppearance:"), appearance);
528 }
529 objc::send_i64(win, objc::sel(c"setTabbingMode:"), 2); objc::send_i64(win, objc::sel(c"setAnimationBehavior:"), 2);
536 objc::send_id(win, objc::sel(c"setTitle:"), objc::nsstring("parity"));
537 objc::send_id(win, objc::sel(c"setContentView:"), hosting.as_ptr());
538 type SetFrame = unsafe extern "C" fn(*mut c_void, *const c_void, NSRect, bool);
542 let set_frame: SetFrame = core::mem::transmute(objc::msg_send_addr());
543 set_frame(win, objc::sel(c"setFrame:display:"), bounds, true);
544 objc::send_id(win, objc::sel(c"orderFront:"), core::ptr::null_mut());
545 drop(hosting);
547 win
548 }
549}
550
551#[cfg(target_os = "macos")]
571pub unsafe fn during_tracking(delay: f64, interval: f64, body: fn() -> bool) {
572 #[repr(C)]
573 struct TimerContext {
574 version: isize,
575 info: *mut c_void,
576 retain: *const c_void,
577 release: *const c_void,
578 copy_description: *const c_void,
579 }
580 unsafe extern "C" fn fire(timer: *mut c_void, info: *mut c_void) {
581 unsafe {
582 let body: fn() -> bool = core::mem::transmute(info);
583 if body() {
584 let invalidate: unsafe extern "C" fn(*mut c_void) =
585 core::mem::transmute(cf(c"CFRunLoopTimerInvalidate"));
586 invalidate(timer);
587 }
588 }
589 }
590 unsafe {
591 type Create = unsafe extern "C" fn(
592 *const c_void, f64, f64, u64, isize,
593 unsafe extern "C" fn(*mut c_void, *mut c_void),
594 *mut TimerContext,
595 ) -> *mut c_void;
596 type Add = unsafe extern "C" fn(*mut c_void, *mut c_void, *mut c_void);
597 type Now = unsafe extern "C" fn() -> f64;
598 type Main = unsafe extern "C" fn() -> *mut c_void;
599 let create: Create = core::mem::transmute(cf(c"CFRunLoopTimerCreate"));
600 let add: Add = core::mem::transmute(cf(c"CFRunLoopAddTimer"));
601 let now: Now = core::mem::transmute(cf(c"CFAbsoluteTimeGetCurrent"));
602 let main: Main = core::mem::transmute(cf(c"CFRunLoopGetMain"));
603 let mut ctx = TimerContext {
604 version: 0,
605 info: body as *mut c_void,
606 retain: core::ptr::null(),
607 release: core::ptr::null(),
608 copy_description: core::ptr::null(),
609 };
610 let timer = create(core::ptr::null(), now() + delay, interval, 0, 0, fire, &mut ctx);
611 let modes = *(cf(c"kCFRunLoopCommonModes") as *const *mut c_void);
615 add(main(), timer, modes);
616 }
617}
618
619#[cfg(target_os = "macos")]
624fn cf(name: &core::ffi::CStr) -> *mut c_void {
625 let p = unsafe { libc::dlsym((-2isize) as *mut c_void, name.as_ptr()) };
626 assert!(!p.is_null(), "CoreFoundation: {}", name.to_string_lossy());
627 p
628}
629
630#[cfg(target_os = "macos")]
639pub unsafe fn post_key(key_code: u16, characters: &str) {
640 unsafe {
641 type Make = unsafe extern "C" fn(
642 *mut c_void, *const c_void, u64, objc::NSPoint, u64, f64, i64,
643 *mut c_void, *mut c_void, *mut c_void, bool, u16,
644 ) -> *mut c_void;
645 let make: Make = core::mem::transmute(objc::msg_send_addr());
646 let chars = objc::nsstring(characters);
647 for kind in [10u64, 11] {
649 let event = make(
650 objc::class(c"OBJC_CLASS_$_NSEvent"),
651 objc::sel(c"keyEventWithType:location:modifierFlags:timestamp:windowNumber:context:characters:charactersIgnoringModifiers:isARepeat:keyCode:"),
652 kind,
653 objc::NSPoint { x: 0.0, y: 0.0 },
654 0,
655 uptime(),
656 0,
657 core::ptr::null_mut(),
658 chars,
659 chars,
660 false,
661 key_code,
662 );
663 if !event.is_null() {
664 objc::send_id_bool(objc::shared_app(), objc::sel(c"postEvent:atStart:"), event, true);
665 }
666 }
667 }
668}
669
670#[cfg(target_os = "macos")]
680pub fn pump_events(seconds: f64) {
681 unsafe {
682 let app = objc::shared_app();
683 let deadline = uptime() + seconds;
684 while uptime() < deadline {
685 type Next = unsafe extern "C" fn(
686 *mut c_void, *const c_void, u64, *mut c_void, *mut c_void, bool,
687 ) -> *mut c_void;
688 let next: Next = core::mem::transmute(objc::msg_send_addr());
689 type DateFn = unsafe extern "C" fn(*mut c_void, *const c_void, f64) -> *mut c_void;
690 let date: DateFn = core::mem::transmute(objc::msg_send_addr());
691 let until = date(
692 objc::class(c"OBJC_CLASS_$_NSDate"),
693 objc::sel(c"dateWithTimeIntervalSinceNow:"),
694 0.01,
695 );
696 let event = next(
697 app,
698 objc::sel(c"nextEventMatchingMask:untilDate:inMode:dequeue:"),
699 u64::MAX,
700 until,
701 objc::nsstring("kCFRunLoopDefaultMode"),
702 true,
703 );
704 if !event.is_null() {
705 objc::send_id(app, objc::sel(c"sendEvent:"), event);
706 }
707 }
708 }
709}
710
711pub fn uptime() -> f64 {
713 unsafe {
714 let info = objc::send(
715 objc::class(c"OBJC_CLASS_$_NSProcessInfo"),
716 objc::sel(c"processInfo"),
717 );
718 objc::send_ret_f64(info, objc::sel(c"systemUptime"))
719 }
720}
721
722#[cfg(target_os = "macos")]
728pub fn activate(timeout: f64) -> bool {
729 unsafe {
730 let app = objc::shared_app();
731 objc::send_i64(app, objc::sel(c"setActivationPolicy:"), 0); objc::send_bool(app, objc::sel(c"activateIgnoringOtherApps:"), true);
738 if objc::responds(app, objc::sel(c"activate")) {
739 objc::send(app, objc::sel(c"activate"));
740 }
741 let running = objc::send(
742 objc::class(c"OBJC_CLASS_$_NSRunningApplication"),
743 objc::sel(c"currentApplication"),
744 );
745 if !running.is_null() {
746 objc::send_u64(running, objc::sel(c"activateWithOptions:"), 1 | 2);
748 }
749
750 let deadline = uptime() + timeout;
751 while uptime() < deadline {
752 if objc::send(app, objc::sel(c"isActive")) as usize & 1 == 1 {
753 return true;
754 }
755 pump_events(0.05);
756 }
757 false
758 }
759}
760
761#[cfg(target_os = "macos")]
768pub fn deactivate(timeout: f64) -> bool {
769 unsafe {
770 let app = objc::shared_app();
771 objc::send_i64(app, objc::sel(c"setActivationPolicy:"), 2);
775 objc::send_id(app, objc::sel(c"hide:"), core::ptr::null_mut());
776 objc::send(app, objc::sel(c"deactivate"));
777 let deadline = uptime() + timeout;
778 while uptime() < deadline {
779 if objc::send(app, objc::sel(c"isActive")) as usize & 1 == 0 {
780 return true;
781 }
782 pump_events(0.05);
783 }
784 false
785 }
786}
787
788#[cfg(target_os = "macos")]
795pub fn pump_events_until(limit: f64, mut ready: impl FnMut() -> bool) {
796 let step = 0.005;
797 let mut waited = 0.0;
798 while waited < limit {
799 pump_events(step);
800 waited += step;
801 if ready() {
802 pump_events(0.02);
803 return;
804 }
805 }
806}
807
808#[cfg(target_os = "macos")]
817pub unsafe fn post_mouse(window: *mut c_void, kind: u64, x: f64, y: f64, clicks: i64) {
818 unsafe {
819 let event = mouse_event(window, kind, x, y, clicks);
820 if !event.is_null() {
821 objc::send_id_bool(objc::shared_app(), objc::sel(c"postEvent:atStart:"), event, false);
822 }
823 }
824}
825
826#[cfg(target_os = "macos")]
837pub unsafe fn mouse_event_for(
838 window: *mut c_void,
839 kind: u64,
840 x: f64,
841 y: f64,
842 clicks: i64,
843) -> *mut c_void {
844 unsafe { mouse_event(window, kind, x, y, clicks) }
845}
846
847#[cfg(target_os = "macos")]
848unsafe fn mouse_event(window: *mut c_void, kind: u64, x: f64, y: f64, clicks: i64) -> *mut c_void {
849 unsafe {
850 let number = objc::send_ret_u64(window, objc::sel(c"windowNumber")) as i64;
851 type Make = unsafe extern "C" fn(
852 *mut c_void, *const c_void, u64, objc::NSPoint, u64, f64, i64, *mut c_void, i64, i64, f32,
853 ) -> *mut c_void;
854 let make: Make = core::mem::transmute(objc::msg_send_addr());
855 make(
856 objc::class(c"OBJC_CLASS_$_NSEvent"),
857 objc::sel(c"mouseEventWithType:location:modifierFlags:timestamp:windowNumber:context:eventNumber:clickCount:pressure:"),
858 kind,
859 objc::NSPoint { x, y },
860 0,
861 uptime(),
862 number,
863 core::ptr::null_mut(),
864 0,
865 clicks,
866 if kind == 2 || kind == 4 { 0.0 } else { 1.0 },
867 )
868 }
869}
870
871#[cfg(target_os = "macos")]
905pub unsafe fn send_mouse(window: *mut c_void, kind: u64, x: f64, y: f64, clicks: i64) {
906 unsafe {
907 let event = mouse_event(window, kind, x, y, clicks);
908 if !event.is_null() {
909 objc::send_id(window, objc::sel(c"sendEvent:"), event);
910 }
911 }
912}
913
914#[cfg(target_os = "macos")]
929pub unsafe fn send_key(window: *mut c_void, key_code: u16, characters: &str, flags: u64) {
930 unsafe {
931 type Make = unsafe extern "C" fn(
932 *mut c_void, *const c_void, u64, objc::NSPoint, u64, f64, i64,
933 *mut c_void, *mut c_void, *mut c_void, bool, u16,
934 ) -> *mut c_void;
935 let make: Make = core::mem::transmute(objc::msg_send_addr());
936 let chars = objc::nsstring(characters);
937 let number = objc::send_ret_u64(window, objc::sel(c"windowNumber")) as i64;
938 for kind in [10u64, 11] {
940 let event = make(
941 objc::class(c"OBJC_CLASS_$_NSEvent"),
942 objc::sel(c"keyEventWithType:location:modifierFlags:timestamp:windowNumber:context:characters:charactersIgnoringModifiers:isARepeat:keyCode:"),
943 kind,
944 objc::NSPoint { x: 0.0, y: 0.0 },
945 flags,
946 uptime(),
947 number,
948 core::ptr::null_mut(),
949 chars,
950 chars,
951 false,
952 key_code,
953 );
954 if !event.is_null() {
955 objc::send_id(window, objc::sel(c"sendEvent:"), event);
956 }
957 }
958 }
959}
960
961#[cfg(not(target_os = "macos"))]
972pub fn render(view: &ViewHandle, width: u32, height: u32) -> Rendered {
973 let hv = HostingView::new(view);
974 render_hosting(&hv, width, height)
975}
976
977#[cfg(not(target_os = "macos"))]
979pub fn render_hosting(hv: &HostingView, width: u32, height: u32) -> Rendered {
980 unsafe {
981 let _pool = objc::AutoreleasePool::new();
982 let window = hv.offscreen(width as f64, height as f64);
983 hv.set_frame(width as f64, height as f64);
984 objc::send(window, objc::sel(c"layoutIfNeeded"));
985 capture_view(hv.as_ptr(), width, height)
986 }
987}
988
989#[cfg(not(target_os = "macos"))]
994pub unsafe fn capture_view(view: *mut c_void, width: u32, height: u32) -> Rendered {
995 unsafe {
996 let layer = objc::send(view, objc::sel(c"layer"));
997 assert!(!layer.is_null(), "the view has no layer to render");
998 let pixels = crate::cg::capture_layer(layer, width, height);
999 Rendered { width, height, pixels }
1000 }
1001}
1002
1003pub fn pump(seconds: f64) {
1008 unsafe {
1009 let loop_ = objc::send(
1010 objc::class(c"OBJC_CLASS_$_NSRunLoop"),
1011 objc::sel(c"currentRunLoop"),
1012 );
1013 type DateFn = unsafe extern "C" fn(*mut c_void, *const c_void, f64) -> *mut c_void;
1014 let f: DateFn = core::mem::transmute(objc::msg_send_addr());
1015 let until = f(
1016 objc::class(c"OBJC_CLASS_$_NSDate"),
1017 objc::sel(c"dateWithTimeIntervalSinceNow:"),
1018 seconds,
1019 );
1020 objc::send_id(loop_, objc::sel(c"runUntilDate:"), until);
1021 }
1022}
1023
1024pub fn render_png(view: &ViewHandle, width: u32, height: u32, path: &str) -> bool {
1026 let img = render(view, width, height);
1027 img.write_png(path)
1028}
1029
1030#[cfg(target_os = "macos")]
1031unsafe fn new_bitmap(width: u32, height: u32) -> *mut c_void {
1032 let alloc = unsafe {
1033 objc::send(objc::class(c"OBJC_CLASS_$_NSBitmapImageRep"), objc::sel(c"alloc"))
1034 };
1035 #[allow(clippy::too_many_arguments)]
1036 type Init = unsafe extern "C" fn(
1037 *mut c_void,
1038 *const c_void,
1039 *mut u8, i64, i64, i64, i64, bool, bool, *mut c_void, i64, i64, ) -> *mut c_void;
1050 let init: Init = unsafe { core::mem::transmute(objc::msg_send_addr()) };
1051 let space = unsafe { objc::nsstring("NSDeviceRGBColorSpace") };
1052 let rep = unsafe {
1053 init(
1054 alloc,
1055 objc::sel(c"initWithBitmapDataPlanes:pixelsWide:pixelsHigh:bitsPerSample:samplesPerPixel:hasAlpha:isPlanar:colorSpaceName:bytesPerRow:bitsPerPixel:"),
1056 core::ptr::null_mut(),
1057 width as i64,
1058 height as i64,
1059 8,
1060 4,
1061 true,
1062 false,
1063 space,
1064 0,
1065 0,
1066 )
1067 };
1068 assert!(!rep.is_null(), "NSBitmapImageRep creation failed");
1069 rep
1070}
1071
1072#[cfg(target_os = "macos")]
1073unsafe fn write_rep_png(rep: *mut c_void, path: &str) -> bool {
1074 unsafe {
1075 let props =
1076 objc::send(objc::class(c"OBJC_CLASS_$_NSDictionary"), objc::sel(c"dictionary"));
1077 let data =
1079 objc::send_u64_id(rep, objc::sel(c"representationUsingType:properties:"), 4, props);
1080 if data.is_null() {
1081 return false;
1082 }
1083 objc::send_id_bool(
1084 data,
1085 objc::sel(c"writeToFile:atomically:"),
1086 objc::nsstring(path),
1087 true,
1088 )
1089 }
1090}
1091
1092
1093pub fn render_image(view: &ViewHandle, width: u32, height: u32) -> Rendered {
1104 unsafe {
1105 let _pool = objc::AutoreleasePool::new();
1106 let framed = crate::views::frame(view, width as f64, height as f64);
1107
1108 let meta = crate::abi::generic_metadata2(
1109 crate::resolve::image_renderer_ma(),
1110 crate::resolve::anyview_meta(),
1111 crate::resolve::anyview_wt(),
1112 );
1113 assert!(!meta.is_null(), "ImageRenderer<AnyView> metadata");
1114
1115 let mut content: u64 = framed.as_ptr() as u64;
1119 let renderer: *mut c_void;
1120 core::arch::asm!("blr {f}", f = in(reg) crate::resolve::image_renderer_init(),
1121 in("x0") &mut content as *mut u64,
1122 in("x20") meta,
1123 lateout("x0") renderer,
1124 lateout("x1") _, lateout("x2") _, lateout("x3") _, lateout("x4") _,
1125 lateout("x5") _, lateout("x6") _, lateout("x7") _, lateout("x8") _,
1126 lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
1127 lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
1128 lateout("x17") _, lateout("lr") _, lateout("x21") _, clobber_abi("C"),
1129 );
1130 assert!(!renderer.is_null(), "ImageRenderer creation failed");
1131
1132 core::arch::asm!("blr {f}", f = in(reg) crate::resolve::image_renderer_scale_set(),
1134 in("d0") 1.0f64,
1135 in("x20") renderer,
1136 lateout("x0") _, lateout("x1") _, lateout("x2") _, lateout("x3") _,
1137 lateout("x4") _, lateout("x5") _, lateout("x6") _, lateout("x7") _,
1138 lateout("x8") _, lateout("x9") _, lateout("x10") _, lateout("x11") _,
1139 lateout("x12") _, lateout("x13") _, lateout("x14") _, lateout("x15") _,
1140 lateout("x16") _, lateout("x17") _, lateout("lr") _, lateout("x21") _,
1141 clobber_abi("C"),
1142 );
1143
1144 let cg: *mut c_void;
1145 core::arch::asm!("blr {f}", f = in(reg) crate::resolve::image_renderer_cgimage(),
1146 in("x20") renderer,
1147 lateout("x0") cg,
1148 lateout("x1") _, lateout("x2") _, lateout("x3") _, lateout("x4") _,
1149 lateout("x5") _, lateout("x6") _, lateout("x7") _, lateout("x8") _,
1150 lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
1151 lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
1152 lateout("x17") _, lateout("lr") _, lateout("x21") _, clobber_abi("C"),
1153 );
1154 assert!(!cg.is_null(), "ImageRenderer produced no image");
1155
1156 let out = {
1157 #[cfg(target_os = "macos")]
1158 {
1159 let alloc =
1160 objc::send(objc::class(c"OBJC_CLASS_$_NSBitmapImageRep"), objc::sel(c"alloc"));
1161 let rep = objc::send_id(alloc, objc::sel(c"initWithCGImage:"), cg);
1162 assert!(!rep.is_null(), "NSBitmapImageRep from CGImage failed");
1163
1164 let data = objc::send_ret_ptr(rep, objc::sel(c"bitmapData"));
1165 let row = objc::send_ret_u64(rep, objc::sel(c"bytesPerRow")) as usize;
1166 let w = objc::send_ret_u64(rep, objc::sel(c"pixelsWide")) as u32;
1167 let h = objc::send_ret_u64(rep, objc::sel(c"pixelsHigh")) as u32;
1168
1169 let mut pixels = vec![0u8; (w * h * 4) as usize];
1170 for y in 0..h as usize {
1171 core::ptr::copy_nonoverlapping(
1172 data.add(y * row),
1173 pixels.as_mut_ptr().add(y * w as usize * 4),
1174 w as usize * 4,
1175 );
1176 }
1177 objc::send(rep, objc::sel(c"release"));
1178 Rendered { width: w, height: h, pixels }
1179 }
1180 #[cfg(not(target_os = "macos"))]
1181 {
1182 let (w, h, pixels) = crate::cg::cgimage_to_rgba(cg);
1183 Rendered { width: w, height: h, pixels }
1184 }
1185 };
1186
1187 unsafe extern "C" {
1191 fn CGImageRelease(image: *mut c_void);
1192 }
1193 CGImageRelease(cg);
1194 swift_runtime_sys::RuntimeRaw::swift_release(renderer);
1195 out
1196 }
1197}
1198
1199pub struct Animation {
1205 pub frames: Vec<Rendered>,
1206}
1207
1208impl Animation {
1209 pub fn sweep(
1216 count: usize,
1217 width: u32,
1218 height: u32,
1219 build: impl Fn(f64) -> ViewHandle,
1220 ) -> Self {
1221 Self::sweep_with(count, width, height, false, build)
1222 }
1223
1224 pub fn sweep_with(
1230 count: usize,
1231 width: u32,
1232 height: u32,
1233 via_image_renderer: bool,
1234 build: impl Fn(f64) -> ViewHandle,
1235 ) -> Self {
1236 let frames = (0..count)
1237 .map(|i| {
1238 let t = if count <= 1 { 0.0 } else { i as f64 / (count - 1) as f64 };
1239 let view = build(t);
1240 if via_image_renderer {
1241 render_image(&view, width, height)
1242 } else {
1243 render(&view, width, height)
1244 }
1245 })
1246 .collect();
1247 Self { frames }
1248 }
1249
1250 pub fn diff_count(&self, other: &Animation, tolerance: u8) -> u32 {
1253 if self.frames.len() != other.frames.len() {
1254 return u32::MAX;
1255 }
1256 self.frames
1257 .iter()
1258 .zip(&other.frames)
1259 .map(|(a, b)| a.diff_count(b, tolerance))
1260 .fold(0u32, |acc, n| acc.saturating_add(n))
1261 }
1262
1263 pub fn frame_deltas(&self) -> Vec<u32> {
1268 self.frames
1269 .windows(2)
1270 .map(|w| w[0].diff_count(&w[1], 0))
1271 .collect()
1272 }
1273
1274 pub fn write_filmstrip(&self, path: &str) -> bool {
1280 let Some(first) = self.frames.first() else { return false };
1281 let (fw, fh) = (first.width as usize, first.height as usize);
1282 let n = self.frames.len();
1283 let gap = 4usize;
1284 let width = fw * n + gap * (n - 1);
1285 let mut pixels = vec![0u8; width * fh * 4];
1286 for (i, frame) in self.frames.iter().enumerate() {
1287 let x0 = i * (fw + gap);
1288 for y in 0..fh {
1289 let src = &frame.pixels[y * fw * 4..(y + 1) * fw * 4];
1290 let dst = (y * width + x0) * 4;
1291 pixels[dst..dst + fw * 4].copy_from_slice(src);
1292 }
1293 }
1294 Rendered { width: width as u32, height: fh as u32, pixels }.write_png(path)
1295 }
1296
1297 pub fn write_apng(&self, path: &str, delay_ms: u16) -> std::io::Result<()> {
1299 std::fs::write(path, encode_apng(&self.frames, delay_ms))
1300 }
1301}
1302
1303fn crc32(data: &[u8]) -> u32 {
1311 let mut table = [0u32; 256];
1312 for (i, entry) in table.iter_mut().enumerate() {
1313 let mut c = i as u32;
1314 for _ in 0..8 {
1315 c = if c & 1 != 0 { 0xEDB8_8320 ^ (c >> 1) } else { c >> 1 };
1316 }
1317 *entry = c;
1318 }
1319 let mut c = 0xFFFF_FFFFu32;
1320 for &b in data {
1321 c = table[((c ^ b as u32) & 0xFF) as usize] ^ (c >> 8);
1322 }
1323 c ^ 0xFFFF_FFFF
1324}
1325
1326fn adler32(data: &[u8]) -> u32 {
1327 let (mut a, mut b) = (1u32, 0u32);
1328 for &byte in data {
1329 a = (a + byte as u32) % 65521;
1330 b = (b + a) % 65521;
1331 }
1332 (b << 16) | a
1333}
1334
1335fn zlib_stored(data: &[u8]) -> Vec<u8> {
1337 let mut out = vec![0x78, 0x01]; let mut chunks = data.chunks(65535).peekable();
1339 if data.is_empty() {
1340 out.extend_from_slice(&[0x01, 0x00, 0x00, 0xFF, 0xFF]);
1341 }
1342 while let Some(chunk) = chunks.next() {
1343 let last = chunks.peek().is_none();
1344 out.push(u8::from(last));
1345 let n = chunk.len() as u16;
1346 out.extend_from_slice(&n.to_le_bytes());
1347 out.extend_from_slice(&(!n).to_le_bytes());
1348 out.extend_from_slice(chunk);
1349 }
1350 out.extend_from_slice(&adler32(data).to_be_bytes());
1351 out
1352}
1353
1354fn chunk(kind: &[u8; 4], payload: &[u8]) -> Vec<u8> {
1355 let mut out = Vec::with_capacity(payload.len() + 12);
1356 out.extend_from_slice(&(payload.len() as u32).to_be_bytes());
1357 out.extend_from_slice(kind);
1358 out.extend_from_slice(payload);
1359 let mut crc_input = Vec::with_capacity(payload.len() + 4);
1360 crc_input.extend_from_slice(kind);
1361 crc_input.extend_from_slice(payload);
1362 out.extend_from_slice(&crc32(&crc_input).to_be_bytes());
1363 out
1364}
1365
1366fn scanlines(frame: &Rendered) -> Vec<u8> {
1368 let stride = frame.width as usize * 4;
1369 let mut raw = Vec::with_capacity((stride + 1) * frame.height as usize);
1370 for y in 0..frame.height as usize {
1371 raw.push(0);
1372 raw.extend_from_slice(&frame.pixels[y * stride..(y + 1) * stride]);
1373 }
1374 raw
1375}
1376
1377fn encode_apng(frames: &[Rendered], delay_ms: u16) -> Vec<u8> {
1378 assert!(!frames.is_empty(), "an APNG needs at least one frame");
1379 let (w, h) = (frames[0].width, frames[0].height);
1380
1381 let mut png = vec![0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A];
1382
1383 let mut ihdr = Vec::new();
1384 ihdr.extend_from_slice(&w.to_be_bytes());
1385 ihdr.extend_from_slice(&h.to_be_bytes());
1386 ihdr.extend_from_slice(&[8, 6, 0, 0, 0]); png.extend_from_slice(&chunk(b"IHDR", &ihdr));
1388
1389 let mut actl = Vec::new();
1390 actl.extend_from_slice(&(frames.len() as u32).to_be_bytes());
1391 actl.extend_from_slice(&0u32.to_be_bytes()); png.extend_from_slice(&chunk(b"acTL", &actl));
1393
1394 let mut seq = 0u32;
1395 for (i, frame) in frames.iter().enumerate() {
1396 let mut fctl = Vec::new();
1397 fctl.extend_from_slice(&seq.to_be_bytes());
1398 seq += 1;
1399 fctl.extend_from_slice(&w.to_be_bytes());
1400 fctl.extend_from_slice(&h.to_be_bytes());
1401 fctl.extend_from_slice(&0u32.to_be_bytes()); fctl.extend_from_slice(&0u32.to_be_bytes()); fctl.extend_from_slice(&delay_ms.to_be_bytes());
1404 fctl.extend_from_slice(&1000u16.to_be_bytes()); fctl.extend_from_slice(&[0, 0]); png.extend_from_slice(&chunk(b"fcTL", &fctl));
1407
1408 let data = zlib_stored(&scanlines(frame));
1409 if i == 0 {
1410 png.extend_from_slice(&chunk(b"IDAT", &data));
1411 } else {
1412 let mut fdat = seq.to_be_bytes().to_vec();
1413 seq += 1;
1414 fdat.extend_from_slice(&data);
1415 png.extend_from_slice(&chunk(b"fdAT", &fdat));
1416 }
1417 }
1418
1419 png.extend_from_slice(&chunk(b"IEND", &[]));
1420 png
1421}