mcap/src/app/screen_capture_macos.jai

773 lines
36 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// The screen capture overlay on macOS (app/screen_capture.jai is the rest; the Windows one is
// screen_capture_win32.jai, which this follows). The moment the hotkey fires, every screen is copied
// with ScreenCaptureKit (modules/Screen_Record/macos/capture.m), and each is shown frozen and darkened
// in a window of its own: a window cannot span screens on macOS, as the one Windows overlay does. A
// rectangle dragged shows at full brightness, and letting go takes it; a click takes the whole screen
// the cursor is on. A magnifier beside the cursor shows the pixels under it, with the position or the
// selection's size, so a region can be chosen to the pixel; the arrow keys move the cursor a pixel at
// a time (ten with Shift). Escape or a right click cancels; Return takes what is dragged.
//
// A drag stays on the screen it began on: its pixels are that screen's, two to a point on a Retina
// screen. Everything is worked out in each screen's own pixels, which the cursor's position in points
// is turned into.
//
// The overlay windows are non-activating panels at the overlay level, on every Space and over
// full-screen apps, as Ex4's overlays are (Ex4-Jai/src/base/platform/macos_overlay.jai): the app you
// were in stays the active one. They are drawn with Core Graphics, only where something changed, and
// run in an event loop of their own: platform_screen_capture_select returns once a region is taken or
// the capture is cancelled.
//
// Needs the Screen Recording permission: without it, macOS gives only the desktop, so mcap asks for
// it and opens its pane in System Settings instead. The window it came from is found in the window
// server's list, whose titles need that permission too.
// The region taken, and the window it was taken from.
Screen_Shot :: struct {
pixels: [] u8; // RGBA, rows packed; heap
width, height: s32; // in pixels
left, top: s32; // in points, from the main screen's top left
whole_screen: bool; // a click took the screen under the cursor
window_title: string; // the window under the region's middle; heap, as are the two below
app_name: string; // its app ("Safari")
app_path: string; // its app's bundle, or executable
}
screen_shot_free :: (shot: *Screen_Shot) {
push_allocator(context.default_allocator);
array_free(shot.pixels);
free(shot.window_title);
free(shot.app_name);
free(shot.app_path);
shot.* = .{};
}
// Freezes the screens and lets a region be chosen. With immediate, no overlay: the whole screen under
// the cursor is taken at once. ok is false when it was cancelled (error "") or could not start (error
// says why). Recording (video) is not on macOS yet.
platform_screen_capture_select :: (video := false, immediate := false) -> Screen_Shot, ok: bool, error: string {
shot: Screen_Shot;
if video return shot, false, "screen recording is not on macOS yet";
init_objective_c();
pool := objc_new(NSAutoreleasePool);
defer NSAutoreleasePool.drain(pool);
if !CGPreflightScreenCaptureAccess() {
// Asked for once (macOS shows its prompt the first time only), and its pane opened, as Ex4 does.
CGRequestScreenCaptureAccess();
run_command("open", SCREEN_RECORDING_PANE);
return shot, false, "mcap needs the Screen Recording permission: turn it on for mcap in System Settings > Privacy & Security > Screen Recording, then capture again";
}
// The app in front, before the overlay is up: what the capture is of when no window is found.
front_name, front_path := frontmost_app();
ok, error := capture_freeze();
defer capture_release();
if !ok return shot, false, error;
if immediate {
x, y := capture_cursor_points();
capture.result_screen = capture_screen_at(x, y);
capture.result = .{0, 0, capture.screens[capture.result_screen].pixel_width, capture.screens[capture.result_screen].pixel_height};
capture.whole_screen = true;
} else {
if !capture_open() return shot, false, "the capture windows could not be opened";
capture_choose();
capture_close();
if capture.cancelled return shot, false, "";
}
screen := *capture.screens[capture.result_screen];
r := capture.result;
shot.width = r.right - r.left;
shot.height = r.bottom - r.top;
if shot.width <= 0 || shot.height <= 0 return shot, false, "";
shot.left = cast(s32) (screen.left + r.left / screen.scale);
shot.top = cast(s32) (screen.top + r.top / screen.scale);
shot.whole_screen = capture.whole_screen;
shot.pixels = NewArray(cast(s64) shot.width * shot.height * 4, u8, initialized = false,, context.default_allocator);
row_bytes := cast(s64) shot.width * 4;
for y: 0..shot.height - 1 {
from := screen.pixels + (cast(s64) (r.top + y) * screen.pixel_width + r.left) * 4;
memcpy(shot.pixels.data + y * row_bytes, from, row_bytes);
}
// The window under the region's middle, now that the overlay is gone; else the app that was in front.
center_x := screen.left + (r.left + r.right) / 2.0 / screen.scale;
center_y := screen.top + (r.top + r.bottom) / 2.0 / screen.scale;
window_info(*shot, center_x, center_y, front_name, front_path);
return shot, true, "";
}
SCREEN_RECORDING_PANE :: "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture";
#scope_file
Capture_Screen :: struct {
left, top, width, height: float64; // in points, from the main screen's top left
scale: float64; // pixels a point
display_id: u32;
pixel_width, pixel_height: s32;
pixels: *u8; // RGBA, from the shim (mcap_sck_free)
image: CGImageRef; // over pixels
window: *NSWindow;
view: *NSView;
dirty: [..] CGRect; // what is drawn now that is not the plain dimmed screen, in its points
}
Pixel :: struct { x, y: s32; }
Pixel_Rect :: struct { left, top, right, bottom: s32; }
capture: struct {
screens: [..] Capture_Screen;
cursor_screen: s64; // the screen the cursor is on, and its pixel there
cursor: Pixel;
dragging: bool;
anchor_screen: s64; // where the drag began
anchor: Pixel;
done, cancelled: bool;
result_screen: s64;
result: Pixel_Rect; // in that screen's pixels
whole_screen: bool;
}
MAGNIFIER_PIXELS :: 21; // the pixels it shows across: odd, so one is in the middle
MAGNIFIER_ZOOM :: 6.0; // points each is shown as
MAGNIFIER_OFFSET :: 24.0; // points from the cursor
BORDER :: 2.0; // the selection's outline, in points
CLICK_SLOP :: 3; // a drag smaller than this either way, in pixels, is a click
DIM :: 0.5; // how dark the frozen screens are
ACCENT :: Vector4.{1, 0.647, 0, 1}; // orange, as on Windows
LABEL :: Vector4.{0.125, 0.125, 0.125, 0.92};
TEXT :: Vector4.{1, 1, 1, 1};
// Every screen: its place and size, then its pixels, all copied at once.
capture_freeze :: () -> ok: bool, error: string {
screens := objc_msgSend_typed(objc_getClass("NSScreen"), selector("screens"));
count := ifx screens then cast(s64) objc_msgSend_typed(screens, selector("count")) else 0;
if !count return false, "there are no screens";
main_height: float64;
capture.screens.allocator = context.default_allocator;
for 0..count - 1 {
ns := msg_object_at(screens, it);
frame := msg_rect(ns, "frame");
if it == 0 main_height = frame.size.height;
s := array_add(*capture.screens);
s.left = frame.origin.x;
s.top = main_height - frame.origin.y - frame.size.height;
s.width = frame.size.width;
s.height = frame.size.height;
s.scale = msg_float(ns, "backingScaleFactor");
s.pixel_width = cast(s32) (s.width * s.scale + 0.5);
s.pixel_height = cast(s32) (s.height * s.scale + 0.5);
description := objc_msgSend_typed(ns, selector("deviceDescription"));
number := msg_object_for_key(description, "NSScreenNumber");
s.display_id = cast(u32) msg_integer(number, "unsignedIntValue");
}
shots := NewArray(capture.screens.count, Mcap_Sck_Shot,, temp);
for capture.screens shots[it_index] = .{display_id = it.display_id, width = it.pixel_width, height = it.pixel_height};
error_text: [512] u8;
copied := mcap_sck_screenshots(shots.data, xx shots.count, error_text.data, error_text.count);
for * capture.screens {
it.pixels = shots[it_index].pixels;
if it.pixels { it.pixel_width, it.pixel_height = shots[it_index].width, shots[it_index].height; }
}
if !copied return false, tprint("the screens could not be copied: %", to_string(error_text.data));
srgb := CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
defer CGColorSpaceRelease(srgb);
for * capture.screens {
provider := CGDataProviderCreateWithData(null, it.pixels, cast(u64) it.pixel_width * cast(u64) it.pixel_height * 4, null);
it.image = CGImageCreate(xx it.pixel_width, xx it.pixel_height, 8, 32, cast(u64) it.pixel_width * 4, srgb, kCGImageAlphaNoneSkipLast | kCGBitmapByteOrder32Big, provider, null, false, kCGRenderingIntentDefault);
CGDataProviderRelease(provider);
if !it.image return false, "the copied screens could not be shown";
}
return true, "";
}
capture_release :: () {
for * capture.screens {
if it.image CGImageRelease(it.image);
if it.pixels mcap_sck_free(it.pixels);
array_free(it.dirty);
}
array_free(capture.screens);
capture = .{};
}
// A window over each screen, the one under the cursor taking the keys.
capture_open :: () -> bool {
view_class, panel_class := capture_classes();
if !view_class || !panel_class return false;
init_core_graphics_runtime_constants();
capture.cursor_screen = -1;
capture_follow_cursor();
for * screen, index: capture.screens {
main_height := capture.screens[0].height;
frame := NSMakeRect(screen.left, main_height - screen.top - screen.height, screen.width, screen.height);
NSWindowStyleMaskNonactivatingPanel :: 1 << 7;
window := NSWindow.initWithContentRect(cast(*NSWindow) objc_alloc(panel_class), frame, NSWindowStyleMaskBorderless | NSWindowStyleMaskNonactivatingPanel, NSBackingStoreBuffered, NO);
if !window return false;
NSWindow.setReleasedWhenClosed(window, NO);
msg_bool(window, "setHasShadow:", false);
msg_bool(window, "setOpaque:", true);
NSWindow.setLevel(window, kCGOverlayWindowLevel);
NSWindow.setCollectionBehavior(window, NSWindowCollectionBehaviorCanJoinAllSpaces | NSWindowCollectionBehaviorStationary |
NSWindowCollectionBehaviorFullScreenAuxiliary | NSWindowCollectionBehaviorIgnoresCycle);
NSWindowAnimationBehaviorNone :: 2;
msg_integer_set(window, "setAnimationBehavior:", NSWindowAnimationBehaviorNone);
view := cast(*NSView) objc_init_with_frame(objc_alloc(view_class), NSMakeRect(0, 0, screen.width, screen.height));
(cast(*s64) object_getIndexedIvars(view)).* = index;
NSWindow.setContentView(window, view);
release(view);
screen.window, screen.view = window, view;
NSWindow.display(window); // drawn before it shows, so its first frame is not an empty one
objc_msgSend_typed(window, selector("orderFrontRegardless"));
}
objc_msgSend_typed(capture.screens[max(capture.cursor_screen, 0)].window, selector("makeKeyWindow"));
capture_update();
return true;
}
capture_close :: () {
for * capture.screens if it.window {
NSWindow.orderOut(it.window, null);
NSWindow.close(it.window);
release(it.window);
it.window, it.view = null;
}
objc_msgSend_typed(objc_msgSend_typed(objc_getClass("NSCursor"), selector("arrowCursor")), selector("set"));
}
// The overlay's own event loop: the cursor followed, clicks and keys taken, until it is done.
capture_choose :: () {
application := NSApplication.sharedApplication();
crosshair := objc_msgSend_typed(objc_getClass("NSCursor"), selector("crosshairCursor"));
while !capture.done {
objc_msgSend_typed(crosshair, selector("set")); // another app's cursor rects may have set theirs
func_date :: #type (*void, Selector, float64) -> *NSObject #c_call;
until := (cast(func_date) objc_msgSend)(objc_getClass("NSDate"), selector("dateWithTimeIntervalSinceNow:"), 1.0 / 120);
event := NSApplication.nextEventMatchingMask(application, NSEventMaskAny, xx until, NSDefaultRunLoopMode, YES);
if event {
type := NSEvent.type(event);
if type == {
case NSEventTypeLeftMouseDown;
capture_follow_cursor();
capture.dragging = true;
capture.anchor_screen = capture.cursor_screen;
capture.anchor = capture.cursor;
capture_update();
case NSEventTypeLeftMouseUp;
if capture.dragging { capture_follow_cursor(); capture_take(); }
case NSEventTypeRightMouseDown;
if capture.dragging { capture.dragging = false; capture_update(); } // let go, and start again
else capture_cancel();
case NSEventTypeKeyDown;
capture_key(NSEvent.keyCode(event), (NSEvent.modifierFlags(event) & NSEventModifierFlagShift) != 0);
case NSEventTypeLeftMouseDragged; #through;
case NSEventTypeMouseMoved; #through;
case NSEventTypeKeyUp; #through;
case NSEventTypeFlagsChanged; #through;
case NSEventTypeRightMouseUp;
// Taken here, not handed on: the windows have nothing to do with them.
case;
NSApplication.sendEvent(application, event);
}
}
if capture_follow_cursor() capture_update();
}
}
capture_cancel :: () {
capture.cancelled = true;
capture.done = true;
}
// Escape cancels, Return takes the drag (or the screen), the arrows move the cursor a pixel.
capture_key :: (key_code: u16, shift: bool) {
KEY_RETURN :: 36; KEY_ENTER :: 76; KEY_ESCAPE :: 53;
KEY_LEFT :: 123; KEY_RIGHT :: 124; KEY_DOWN :: 125; KEY_UP :: 126;
step: s32 = 1;
if shift step = 10;
dx, dy: s32;
if key_code == {
case KEY_ESCAPE; capture_cancel(); return;
case KEY_RETURN; #through;
case KEY_ENTER; capture_take(); return;
case KEY_LEFT; dx = -step;
case KEY_RIGHT; dx = step;
case KEY_UP; dy = -step;
case KEY_DOWN; dy = step;
case; return;
}
// The cursor moved to the middle of the pixel; following it does the rest.
screen := *capture.screens[capture.cursor_screen];
x := clamp(capture.cursor.x + dx, 0, screen.pixel_width - 1);
y := clamp(capture.cursor.y + dy, 0, screen.pixel_height - 1);
CGWarpMouseCursorPosition(.{screen.left + (x + 0.5) / screen.scale, screen.top + (y + 0.5) / screen.scale});
CGAssociateMouseAndMouseCursorPosition(1);
}
// Where the cursor is now, in points from the main screen's top left.
capture_cursor_points :: () -> float64, float64 {
location := NSEvent.mouseLocation();
return location.x, capture.screens[0].height - location.y;
}
capture_screen_at :: (x: float64, y: float64) -> s64 {
for capture.screens if x >= it.left && x < it.left + it.width && y >= it.top && y < it.top + it.height return it_index;
return 0;
}
// The cursor's screen and pixel, from where it is; true when either changed.
capture_follow_cursor :: () -> bool {
x, y := capture_cursor_points();
index := capture_screen_at(x, y);
screen := *capture.screens[index];
pixel := Pixel.{clamp(cast(s32) floor((x - screen.left) * screen.scale), 0, screen.pixel_width - 1),
clamp(cast(s32) floor((y - screen.top) * screen.scale), 0, screen.pixel_height - 1)};
changed := index != capture.cursor_screen || pixel.x != capture.cursor.x || pixel.y != capture.cursor.y;
capture.cursor_screen, capture.cursor = index, pixel;
return changed;
}
// From the anchor to the cursor, both pixels in it, on the anchor's screen.
capture_selection :: () -> Pixel_Rect {
screen := *capture.screens[capture.anchor_screen];
c := capture.cursor;
if capture.cursor_screen != capture.anchor_screen {
// The cursor on another screen: where it would be on this one, kept to its edges.
other := *capture.screens[capture.cursor_screen];
gx := other.left + capture.cursor.x / other.scale;
gy := other.top + capture.cursor.y / other.scale;
c = .{cast(s32) floor((gx - screen.left) * screen.scale), cast(s32) floor((gy - screen.top) * screen.scale)};
}
c.x = clamp(c.x, 0, screen.pixel_width - 1);
c.y = clamp(c.y, 0, screen.pixel_height - 1);
a := capture.anchor;
return .{min(a.x, c.x), min(a.y, c.y), max(a.x, c.x) + 1, max(a.y, c.y) + 1};
}
// The selection, or on a click (or Return without a drag) the screen under the cursor.
capture_take :: () {
if capture.dragging {
selection := capture_selection();
if selection.right - selection.left >= CLICK_SLOP || selection.bottom - selection.top >= CLICK_SLOP {
capture.result_screen = capture.anchor_screen;
capture.result = selection;
capture.dragging = false;
capture.done = true;
return;
}
}
capture.dragging = false;
capture.result_screen = capture.cursor_screen;
screen := capture.screens[capture.cursor_screen];
capture.result = .{0, 0, screen.pixel_width, screen.pixel_height};
capture.whole_screen = true;
capture.done = true;
}
// What each screen draws now beyond the dimmed copy, and which parts of it to draw again: where they
// were and where they are.
capture_update :: () {
for * screen, index: capture.screens {
if !screen.view continue;
now: [..] CGRect;
now.allocator = context.default_allocator;
for capture_parts(index) array_add(*now, it);
for screen.dirty NSView_setNeedsDisplayInRect(screen.view, it);
for now NSView_setNeedsDisplayInRect(screen.view, it);
array_free(screen.dirty);
screen.dirty = now;
}
}
// The parts of screen index that differ from the dimmed copy, in its points: the selection and its
// outline, or the crosshair; and the magnifier and its label.
capture_parts :: (index: s64) -> [] CGRect {
parts: [..] CGRect;
parts.allocator = temp;
screen := *capture.screens[index];
if capture.dragging && index == capture.anchor_screen {
s := points_of(screen, capture_selection());
array_add(*parts, .{.{s.origin.x - BORDER, s.origin.y - BORDER}, .{s.size.width + 2 * BORDER, s.size.height + 2 * BORDER}});
}
if index == capture.cursor_screen {
if !capture.dragging {
x, y := capture.cursor.x / screen.scale, capture.cursor.y / screen.scale;
array_add(*parts, .{.{0, y}, .{screen.width, 1 / screen.scale}});
array_add(*parts, .{.{x, 0}, .{1 / screen.scale, screen.height}});
}
magnifier, label := magnifier_layout(screen);
array_add(*parts, .{.{magnifier.origin.x - 1, magnifier.origin.y - 1}, .{magnifier.size.width + 2, magnifier.size.height + 2}});
array_add(*parts, label);
}
return parts;
}
points_of :: (screen: *Capture_Screen, r: Pixel_Rect) -> CGRect {
return .{.{r.left / screen.scale, r.top / screen.scale}, .{(r.right - r.left) / screen.scale, (r.bottom - r.top) / screen.scale}};
}
// The magnifier below and right of the cursor, or on the other side where the screen ends, and the
// label under it.
magnifier_layout :: (screen: *Capture_Screen) -> magnifier: CGRect, label: CGRect {
side: float64 = MAGNIFIER_PIXELS * MAGNIFIER_ZOOM + 2;
label_height: float64 = 22;
label_width := max(side, text_size(label_text(screen)).width + 12);
x := capture.cursor.x / screen.scale + MAGNIFIER_OFFSET;
y := capture.cursor.y / screen.scale + MAGNIFIER_OFFSET;
if x + max(side, label_width) > screen.width x = capture.cursor.x / screen.scale - MAGNIFIER_OFFSET - max(side, label_width);
if y + side + label_height > screen.height y = capture.cursor.y / screen.scale - MAGNIFIER_OFFSET - side - label_height;
return .{.{x, y}, .{side, side}}, .{.{x, y + side}, .{label_width, label_height}};
}
// The position (in the screen's pixels), or the selection's size.
label_text :: (screen: *Capture_Screen) -> string {
if capture.dragging {
s := capture_selection();
return tprint("% × %", s.right - s.left, s.bottom - s.top);
}
return tprint("%, %", capture.cursor.x, capture.cursor.y);
}
// drawRect: for screen index's view, in its points (flipped: y goes down).
capture_draw :: (index: s64, cg: CGContextRef) {
screen := *capture.screens[index];
whole := CGRect.{.{0, 0}, .{screen.width, screen.height}};
draw_image(cg, screen.image, whole);
CGContextSetRGBFillColor(cg, 0, 0, 0, DIM);
CGContextFillRect(cg, whole);
fill :: (cg: CGContextRef, r: CGRect, color: Vector4) {
CGContextSetRGBFillColor(cg, color.x, color.y, color.z, color.w);
CGContextFillRect(cg, r);
}
if capture.dragging && index == capture.anchor_screen {
s := points_of(screen, capture_selection());
CGContextSaveGState(cg);
CGContextClipToRect(cg, s);
draw_image(cg, screen.image, whole);
CGContextRestoreGState(cg);
// The outline just outside it, so every pixel taken shows as it is.
fill(cg, .{.{s.origin.x - BORDER, s.origin.y - BORDER}, .{s.size.width + 2 * BORDER, BORDER}}, ACCENT);
fill(cg, .{.{s.origin.x - BORDER, s.origin.y + s.size.height}, .{s.size.width + 2 * BORDER, BORDER}}, ACCENT);
fill(cg, .{.{s.origin.x - BORDER, s.origin.y}, .{BORDER, s.size.height}}, ACCENT);
fill(cg, .{.{s.origin.x + s.size.width, s.origin.y}, .{BORDER, s.size.height}}, ACCENT);
}
if index != capture.cursor_screen return;
pixel := 1 / screen.scale;
cx, cy := capture.cursor.x * pixel, capture.cursor.y * pixel;
if !capture.dragging {
fill(cg, .{.{0, cy}, .{screen.width, pixel}}, ACCENT);
fill(cg, .{.{cx, 0}, .{pixel, screen.height}}, ACCENT);
}
// The magnifier: the pixels around the cursor, as copied, each MAGNIFIER_ZOOM points wide; past
// the screen's edges it is black.
magnifier, label := magnifier_layout(screen);
fill(cg, magnifier, .{1, 1, 1, 1});
inner := CGRect.{.{magnifier.origin.x + 1, magnifier.origin.y + 1}, .{magnifier.size.width - 2, magnifier.size.height - 2}};
fill(cg, inner, .{0, 0, 0, 1});
half :: MAGNIFIER_PIXELS / 2;
left := max(capture.cursor.x - half, 0);
top := max(capture.cursor.y - half, 0);
right := min(capture.cursor.x + half + 1, screen.pixel_width);
bottom := min(capture.cursor.y + half + 1, screen.pixel_height);
if right > left && bottom > top {
part := CGImageCreateWithImageInRect(screen.image, .{.{xx left, xx top}, .{xx (right - left), xx (bottom - top)}});
if part {
CGContextSetInterpolationQuality(cg, kCGInterpolationNone);
draw_image(cg, part, .{.{inner.origin.x + (left - (capture.cursor.x - half)) * MAGNIFIER_ZOOM, inner.origin.y + (top - (capture.cursor.y - half)) * MAGNIFIER_ZOOM},
.{(right - left) * MAGNIFIER_ZOOM, (bottom - top) * MAGNIFIER_ZOOM}});
CGImageRelease(part);
}
}
// The pixel under the cursor, outlined.
c := CGRect.{.{inner.origin.x + half * MAGNIFIER_ZOOM, inner.origin.y + half * MAGNIFIER_ZOOM}, .{MAGNIFIER_ZOOM, MAGNIFIER_ZOOM}};
fill(cg, .{.{c.origin.x - 1, c.origin.y - 1}, .{c.size.width + 2, 1}}, ACCENT);
fill(cg, .{.{c.origin.x - 1, c.origin.y + c.size.height}, .{c.size.width + 2, 1}}, ACCENT);
fill(cg, .{.{c.origin.x - 1, c.origin.y}, .{1, c.size.height}}, ACCENT);
fill(cg, .{.{c.origin.x + c.size.width, c.origin.y}, .{1, c.size.height}}, ACCENT);
// Under it, the position, or the selection's size.
fill(cg, label, LABEL);
draw_text(label_text(screen), label.origin.x + 6, label.origin.y + 3);
}
// image upright in rect, in a flipped context.
draw_image :: (cg: CGContextRef, image: CGImageRef, rect: CGRect) {
CGContextSaveGState(cg);
CGContextTranslateCTM(cg, rect.origin.x, rect.origin.y + rect.size.height);
CGContextScaleCTM(cg, 1, -1);
CGContextDrawImage(cg, .{.{0, 0}, rect.size}, image);
CGContextRestoreGState(cg);
}
// The label's font: the system's, semibold, 13 points, white.
text_attributes :: () -> *NSObject {
func_font :: #type (*void, Selector, CGFloat, CGFloat) -> *NSObject #c_call;
font := (cast(func_font) objc_msgSend)(objc_getClass("NSFont"), selector("monospacedDigitSystemFontOfSize:weight:"), 13, 0.3);
func_color :: #type (*void, Selector, CGFloat, CGFloat, CGFloat, CGFloat) -> *NSObject #c_call;
color := (cast(func_color) objc_msgSend)(objc_getClass("NSColor"), selector("colorWithSRGBRed:green:blue:alpha:"), TEXT.x, TEXT.y, TEXT.z, TEXT.w);
keys := *void.[NSFontAttributeName, NSForegroundColorAttributeName];
values := *void.[font, color];
func_dictionary :: #type (*void, Selector, **void, **void, NSUInteger) -> *NSObject #c_call;
return (cast(func_dictionary) objc_msgSend)(objc_getClass("NSDictionary"), selector("dictionaryWithObjects:forKeys:count:"), values.data, keys.data, 2);
}
text_size :: (text: string) -> NSSize {
func :: #type (*void, Selector, *NSObject) -> NSSize #c_call;
return (cast(func) objc_msgSend)(NSString.getTempString(text), selector("sizeWithAttributes:"), text_attributes());
}
draw_text :: (text: string, x: float64, y: float64) {
func :: #type (*void, Selector, NSPoint, *NSObject) -> void #c_call;
(cast(func) objc_msgSend)(NSString.getTempString(text), selector("drawAtPoint:withAttributes:"), NSMakePoint(x, y), text_attributes());
}
// ---------------------------------------------------------------- the window it came from
// The front-most window, other than mcap's, whose bounds hold the point; its title and app. Else the
// app that was in front.
window_info :: (shot: *Screen_Shot, x: float64, y: float64, front_name: string, front_path: string) {
push_allocator(context.default_allocator);
windows := CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements, 0);
own := getpid();
found := false;
if windows {
defer CFRelease(windows);
for 0..CFArrayGetCount(windows) - 1 {
info := CFArrayGetValueAtIndex(windows, it);
layer := cf_integer(CFDictionaryGetValue(info, kCGWindowLayer));
pid := cf_integer(CFDictionaryGetValue(info, kCGWindowOwnerPID));
if layer != 0 || pid == own continue; // the menu bar, the Dock, overlays; and mcap's own
bounds: CGRect;
if !CGRectMakeWithDictionaryRepresentation(CFDictionaryGetValue(info, kCGWindowBounds), *bounds) continue;
if x < bounds.origin.x || y < bounds.origin.y || x >= bounds.origin.x + bounds.size.width || y >= bounds.origin.y + bounds.size.height continue;
shot.window_title = copy_string(cf_string(CFDictionaryGetValue(info, kCGWindowName)));
name, path := app_of(cast(s32) pid);
if !name name = cf_string(CFDictionaryGetValue(info, kCGWindowOwnerName));
shot.app_name = copy_string(name);
shot.app_path = copy_string(path);
found = true;
break;
}
}
if !found {
shot.app_name = copy_string(front_name);
shot.app_path = copy_string(front_path);
}
}
// The app in front: its name and bundle. Temp.
frontmost_app :: () -> name: string, path: string {
workspace := objc_msgSend_typed(objc_getClass("NSWorkspace"), selector("sharedWorkspace"));
app := objc_msgSend_typed(workspace, selector("frontmostApplication"));
name, path := app_name_and_path(app);
return name, path;
}
app_of :: (pid: s32) -> name: string, path: string {
func :: #type (*void, Selector, s32) -> *NSObject #c_call;
app := (cast(func) objc_msgSend)(objc_getClass("NSRunningApplication"), selector("runningApplicationWithProcessIdentifier:"), pid);
name, path := app_name_and_path(app);
return name, path;
}
app_name_and_path :: (app: *void) -> name: string, path: string {
if !app return "", "";
name := ns_string(objc_msgSend_typed(app, selector("localizedName")));
url := objc_msgSend_typed(app, selector("bundleURL"));
if !url url = objc_msgSend_typed(app, selector("executableURL"));
path := ifx url then ns_string(objc_msgSend_typed(url, selector("path"))) else "";
return name, path;
}
ns_string :: (s: *void) -> string {
if !s return "";
return copy_string(to_string(cast(*NSString) s),, temp);
}
cf_integer :: (number: *void) -> s64 {
value: s64;
if number CFNumberGetValue(number, kCFNumberSInt64Type, *value);
return value;
}
cf_string :: (s: *void) -> string {
if !s return "";
buffer: [1024] u8;
if !CFStringGetCString(s, buffer.data, buffer.count, kCFStringEncodingUTF8) return "";
return copy_string(to_string(buffer.data),, temp);
}
// ---------------------------------------------------------------- the classes, and messages
// McapCaptureView, an NSView whose drawRect: draws its screen and whose y goes down, its extra bytes
// its screen's index; and McapCapturePanel, an NSPanel that can be the key window with no title bar.
capture_classes :: () -> view: Class, panel: Class {
if view_class && panel_class return view_class, panel_class;
view_class = objc_getClass("McapCaptureView");
if !view_class {
view_class = objc_allocateClassPair(objc_getClass("NSView"), "McapCaptureView", size_of(s64));
if !view_class return null, null;
draw_rect :: (self: id, _sel: Selector, dirty: NSRect) #c_call {
push_context capture_context {
index := (cast(*s64) object_getIndexedIvars(self)).*;
if index < 0 || index >= capture.screens.count return;
context_object := objc_msgSend_typed(objc_getClass("NSGraphicsContext"), selector("currentContext"));
cg := ifx context_object then cast(CGContextRef) objc_msgSend_typed(context_object, selector("CGContext")) else null;
if cg capture_draw(index, cg);
}
}
yes :: (self: id, _sel: Selector) -> BOOL #c_call { return YES; }
yes_for_event :: (self: id, _sel: Selector, event: *NSEvent) -> BOOL #c_call { return YES; }
class_addMethod(view_class, selector("drawRect:"), xx draw_rect, "v@:{CGRect={CGPoint=dd}{CGSize=dd}}");
class_addMethod(view_class, selector("isFlipped"), xx yes, "c@:");
class_addMethod(view_class, selector("isOpaque"), xx yes, "c@:");
class_addMethod(view_class, selector("acceptsFirstMouse:"), xx yes_for_event, "c@:@");
objc_registerClassPair(view_class);
}
panel_class = objc_getClass("McapCapturePanel");
if !panel_class {
panel_class = objc_allocateClassPair(objc_getClass("NSPanel"), "McapCapturePanel", 0);
if !panel_class return null, null;
yes :: (self: id, _sel: Selector) -> BOOL #c_call { return YES; }
no :: (self: id, _sel: Selector) -> BOOL #c_call { return NO; }
class_addMethod(panel_class, selector("canBecomeKeyWindow"), xx yes, "c@:");
class_addMethod(panel_class, selector("canBecomeMainWindow"), xx no, "c@:");
objc_registerClassPair(panel_class);
}
capture_context = context;
return view_class, panel_class;
}
view_class, panel_class: Class;
capture_context: #Context; // drawRect: runs on the main thread, in the capture's own event loop
msg_object_at :: (array: *void, index: s64) -> *void {
func :: #type (*void, Selector, u64) -> *void #c_call;
return (cast(func) objc_msgSend)(array, selector("objectAtIndex:"), xx index);
}
msg_object_for_key :: (dictionary: *void, key: string) -> *void {
func :: #type (*void, Selector, *NSString) -> *void #c_call;
return (cast(func) objc_msgSend)(dictionary, selector("objectForKey:"), NSString.getTempString(key));
}
msg_rect :: (object: *void, name: string) -> NSRect {
func :: #type (*void, Selector) -> NSRect #c_call;
return (cast(func) objc_msgSend)(object, selector(name));
}
msg_float :: (object: *void, name: string) -> float64 {
func :: #type (*void, Selector) -> float64 #c_call;
return (cast(func) objc_msgSend)(object, selector(name));
}
msg_integer :: (object: *void, name: string) -> s64 {
func :: #type (*void, Selector) -> s64 #c_call;
return (cast(func) objc_msgSend)(object, selector(name));
}
msg_integer_set :: (object: *void, name: string, value: s64) {
func :: #type (*void, Selector, s64) -> void #c_call;
(cast(func) objc_msgSend)(object, selector(name), value);
}
msg_bool :: (object: *void, name: string, value: bool) {
func :: #type (*void, Selector, BOOL) -> void #c_call;
(cast(func) objc_msgSend)(object, selector(name), ifx value then YES else NO);
}
objc_init_with_frame :: (object: *void, frame: NSRect) -> *void {
func :: #type (*void, Selector, NSRect) -> *void #c_call;
return (cast(func) objc_msgSend)(object, selector("initWithFrame:"), frame);
}
NSView_setNeedsDisplayInRect :: (view: *NSView, rect: CGRect) {
func :: #type (*void, Selector, NSRect) -> void #c_call;
(cast(func) objc_msgSend)(view, selector("setNeedsDisplayInRect:"), NSMakeRect(rect.origin.x, rect.origin.y, rect.size.width, rect.size.height));
}
// ---------------------------------------------------------------- foreign
Mcap_Sck_Shot :: struct {
display_id: u32;
width, height: s32;
pixels: *u8;
}
mcap_capture :: #library,no_dll "../../modules/Screen_Record/macos/capture";
mcap_sck_screenshots :: (shots: *Mcap_Sck_Shot, count: s32, error: *u8, error_size: s32) -> s32 #foreign mcap_capture;
mcap_sck_free :: (pointer: *void) #foreign mcap_capture;
ScreenCaptureKit_Framework :: #library,system,link_always "ScreenCaptureKit";
CGContextRef :: *void;
CGImageRef :: *void;
CGPoint :: struct { x, y: float64; }
CGSize :: struct { width, height: float64; }
CGRect :: struct { origin: CGPoint; size: CGSize; }
kCGImageAlphaNoneSkipLast :: 5;
kCGBitmapByteOrder32Big :: 4 << 12;
kCGRenderingIntentDefault :: 0;
kCGInterpolationNone :: 1;
kCGWindowListOptionOnScreenOnly :: 1 << 0;
kCGWindowListExcludeDesktopElements :: 1 << 4;
kCFNumberSInt64Type :: 4;
kCFStringEncodingUTF8 :: 0x0800_0100;
CoreGraphics_Framework :: #library,system "CoreGraphics";
CoreFoundation_Framework :: #library,system "CoreFoundation";
kCGColorSpaceSRGB: *void #elsewhere CoreGraphics_Framework;
kCGWindowLayer: *void #elsewhere CoreGraphics_Framework;
kCGWindowBounds: *void #elsewhere CoreGraphics_Framework;
kCGWindowName: *void #elsewhere CoreGraphics_Framework;
kCGWindowOwnerName: *void #elsewhere CoreGraphics_Framework;
kCGWindowOwnerPID: *void #elsewhere CoreGraphics_Framework;
AppKit_Framework :: #library,system "AppKit"; // the Objective_C module keeps its own to itself
NSFontAttributeName: *NSString #elsewhere AppKit_Framework;
NSForegroundColorAttributeName: *NSString #elsewhere AppKit_Framework;
CGPreflightScreenCaptureAccess :: () -> bool #foreign CoreGraphics_Framework;
CGRequestScreenCaptureAccess :: () -> bool #foreign CoreGraphics_Framework;
CGWarpMouseCursorPosition :: (point: CGPoint) -> s32 #foreign CoreGraphics_Framework;
CGAssociateMouseAndMouseCursorPosition :: (connected: s32) -> s32 #foreign CoreGraphics_Framework;
CGColorSpaceCreateWithName :: (name: *void) -> *void #foreign CoreGraphics_Framework;
CGColorSpaceRelease :: (space: *void) #foreign CoreGraphics_Framework;
CGDataProviderCreateWithData :: (info: *void, data: *void, size: u64, release: *void) -> *void #foreign CoreGraphics_Framework;
CGDataProviderRelease :: (provider: *void) #foreign CoreGraphics_Framework;
CGImageCreate :: (width: u64, height: u64, bits_per_component: u64, bits_per_pixel: u64, bytes_per_row: u64, space: *void, bitmap_info: u32, provider: *void, decode: *float64, interpolate: bool, intent: s32) -> CGImageRef #foreign CoreGraphics_Framework;
CGImageCreateWithImageInRect :: (image: CGImageRef, rect: CGRect) -> CGImageRef #foreign CoreGraphics_Framework;
CGImageRelease :: (image: CGImageRef) #foreign CoreGraphics_Framework;
CGContextDrawImage :: (c: CGContextRef, rect: CGRect, image: CGImageRef) #foreign CoreGraphics_Framework;
CGContextSaveGState :: (c: CGContextRef) #foreign CoreGraphics_Framework;
CGContextRestoreGState :: (c: CGContextRef) #foreign CoreGraphics_Framework;
CGContextTranslateCTM :: (c: CGContextRef, x: float64, y: float64) #foreign CoreGraphics_Framework;
CGContextScaleCTM :: (c: CGContextRef, x: float64, y: float64) #foreign CoreGraphics_Framework;
CGContextClipToRect :: (c: CGContextRef, rect: CGRect) #foreign CoreGraphics_Framework;
CGContextSetRGBFillColor :: (c: CGContextRef, r: float64, g: float64, b: float64, a: float64) #foreign CoreGraphics_Framework;
CGContextFillRect :: (c: CGContextRef, rect: CGRect) #foreign CoreGraphics_Framework;
CGContextSetInterpolationQuality :: (c: CGContextRef, quality: s32) #foreign CoreGraphics_Framework;
CGWindowListCopyWindowInfo :: (option: u32, relative_to: u32) -> *void #foreign CoreGraphics_Framework;
CGRectMakeWithDictionaryRepresentation :: (dictionary: *void, rect: *CGRect) -> bool #foreign CoreGraphics_Framework;
CFArrayGetCount :: (array: *void) -> s64 #foreign CoreFoundation_Framework;
CFArrayGetValueAtIndex :: (array: *void, index: s64) -> *void #foreign CoreFoundation_Framework;
CFDictionaryGetValue :: (dictionary: *void, key: *void) -> *void #foreign CoreFoundation_Framework;
CFNumberGetValue :: (number: *void, type: s64, value: *void) -> bool #foreign CoreFoundation_Framework;
CFStringGetCString :: (s: *void, buffer: *u8, size: s64, encoding: u32) -> bool #foreign CoreFoundation_Framework;
CFRelease :: (object: *void) #foreign CoreFoundation_Framework;
getpid :: () -> s32 #foreign libc;
libc :: #library,system "libc";
#import "Objective_C";
#import "Objective_C/AppKit";
#import "Process";