773 lines
36 KiB
Plaintext
773 lines
36 KiB
Plaintext
// 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";
|