[0.1.33] macOS phase 6: screen capture stills with Cmd+Shift+2, through ScreenCaptureKit

This commit is contained in:
Musa Mahmood 2026-10-16 20:47:56 -04:00
parent 91f6cf768f
commit 63a0cdb64e
11 changed files with 976 additions and 14 deletions

3
.gitignore vendored
View File

@ -29,5 +29,8 @@ Thumbs.db
/modules/Segment/models/
# FFmpeg for macOS: the source, build and headers modules/ffmpeg/build_macos.jai makes (its libraries in macos/ are kept)
/modules/ffmpeg/macos-build/
# The ScreenCaptureKit shim, as first.jai compiles it
/modules/Screen_Record/macos/*.o
/modules/Screen_Record/macos/*.a
# Releases: made by release.jai
/release/

View File

@ -9,9 +9,9 @@ and labels are tested on Windows with an NVIDIA GPU).
The `.mcap` files come from the **Import** window, which takes any folder of images (PNG, JPEG, HEIC,
AVIF, WebP, GIF, BMP, TIFF, camera RAW), videos (MP4, MOV, MKV, WebM, AVI, WMV, MPEG) and Snagit
`.snagx` captures, or from `snagit-converter`
(`D:\McapIndex\SnagitConverter`); `modules/Capture_File/README.md` describes the format. On
Windows, **PrintScreen** captures a region of the screen straight into the library, as Snagit does;
see [Screen capture](#screen-capture).
(`D:\McapIndex\SnagitConverter`); `modules/Capture_File/README.md` describes the format. **PrintScreen**
on Windows, **Cmd+Shift+2** on macOS, captures a region of the screen straight into the library, as
Snagit does; see [Screen capture](#screen-capture).
## Getting started
@ -182,8 +182,8 @@ of them are segmented, it also shows **What is in the images**: how many images
## Screen capture
On Windows, **PrintScreen** captures any part of the screen, from any app, even while mcap is hidden
in the tray:
**PrintScreen** on Windows, **Cmd+Shift+2** on macOS, captures any part of the screen, from any app,
even while mcap is hidden in the tray or the menu bar:
1. **The screens freeze** the moment it is pressed: everything on every screen is copied and shown
darkened over them, so menus, tooltips and video hold still while you choose.
@ -206,6 +206,18 @@ have PrintScreen: if Snagit is running it has it, and so does Windows while Sett
Accessibility > Keyboard > "Use the Print screen key to open screen capture" is on. When mcap cannot
register the key, the Event Log says so; choose another, such as `Ctrl-Shift-S capture_region`.
On macOS (Cmd+Shift+3, 4 and 5 are the system's own screenshot keys):
- **Screen Recording** must be allowed for mcap (System Settings > Privacy & Security > Screen
Recording). The first capture asks for it and opens that pane; macOS may want mcap restarted after.
The permission belongs to the signed `mcap.app`, so start that, not the bare executable, which
would have it asked for on behalf of the terminal.
- Each screen freezes in a window of its own, and a selection stays on the screen it began on. What
is taken is that screen's own pixels, two to a point on a Retina screen, and the magnifier, the
position and the size are in them.
- The screens are copied with ScreenCaptureKit (`modules/Screen_Record/macos/capture.m`, which the
build compiles with clang).
### Recording the screen
**Ctrl+PrintScreen** records a region of one screen as a video:

View File

@ -5,5 +5,5 @@
# Changes apply as soon as the file is saved.
none bring_to_foreground
none capture_region
Cmd-Shift-2 capture_region
none record_region

View File

@ -40,6 +40,9 @@ SHIM, jai_modules));
options.import_path = paths;
}
// The macOS check links nothing, but the ScreenCaptureKit shim is built as first.jai builds it.
#if OS == .MACOS if target == .MACOS build_macos_shim();
w := compiler_create_workspace("check");
set_build_options(options, w);
compiler_begin_intercept(w);
@ -49,7 +52,7 @@ APP_NAME :: "mcap";
APP_FOLDER_NAME :: "mcap";
GIT_BRANCH :: "check";
GIT_HASH :: "-";
VERSION :: "0.1.32";
VERSION :: "0.1.33";
BUILD_DATE :: "-";
DEBUG :: true;
COMPILE_TESTS :: true;
@ -67,6 +70,31 @@ S, w);
compiler_end_intercept(w);
}
#if OS == .MACOS {
// macOS: modules/Screen_Record/macos/capture.m, mcap's ScreenCaptureKit shim, compiled with clang
// into capture.a beside it whenever the source is newer, for the app to link (screen_capture_macos.jai).
// check_targets.jai has the same; keep the two in step.
build_macos_shim :: () -> bool {
FOLDER :: "modules/Screen_Record/macos";
source := tprint("%/capture.m", FOLDER);
object := tprint("%/capture.o", FOLDER);
library := tprint("%/capture.a", FOLDER);
source_time, _, source_ok := file_modtime_and_size(source);
header_time := file_modtime_and_size(tprint("%/capture.h", FOLDER));
library_time, _, library_ok := file_modtime_and_size(library);
if source_ok && library_ok && library_time >= source_time && library_time >= header_time return true;
result := run_command("clang", "-c", "-fobjc-arc", "-O2", "-Wall", "-mmacosx-version-min=14.0", source, "-o", object);
if result.exit_code != 0 { log_error("ERROR: clang could not compile %.", source); return false; }
file_delete(library);
result = run_command("ar", "rcs", library, object);
if result.exit_code != 0 { log_error("ERROR: ar could not make %.", library); return false; }
log("Built %", library);
return true;
}
#import "Process";
#import "File_Utilities";
}
#import "Basic";
#import "Compiler";
#import "File";

View File

@ -8,7 +8,7 @@ APP_NAME :: "mcap";
APP_FOLDER_NAME :: "mcap";
APP_BUNDLE_IDENTIFIER :: "com.example.mcap";
VERSION :: "0.1.32";
VERSION :: "0.1.33";
// Where everything goes: bin/, or bin-headless/ for builds made with no_crash_dialog, so automated
// builds never write over the ones you run by hand.
build_directory := "bin";
@ -54,6 +54,7 @@ build_project :: () {
set_working_directory(#filepath); // so that we load fonts correctly.
make_directory_if_it_does_not_exist(build_directory);
#if OS == .MACOS if !build_macos_shim() return;
// Workspaces compile concurrently once source is added, so every build is
// started before any is waited on.
@ -203,6 +204,27 @@ finish_main_build :: (build: Pending_Build) {
}
#if OS == .MACOS {
// macOS: modules/Screen_Record/macos/capture.m, mcap's ScreenCaptureKit shim, compiled with clang
// into capture.a beside it whenever the source is newer, for the app to link (screen_capture_macos.jai).
// check_targets.jai has the same; keep the two in step.
build_macos_shim :: () -> bool {
FOLDER :: "modules/Screen_Record/macos";
source := tprint("%/capture.m", FOLDER);
object := tprint("%/capture.o", FOLDER);
library := tprint("%/capture.a", FOLDER);
source_time, _, source_ok := file_modtime_and_size(source);
header_time := file_modtime_and_size(tprint("%/capture.h", FOLDER));
library_time, _, library_ok := file_modtime_and_size(library);
if source_ok && library_ok && library_time >= source_time && library_time >= header_time return true;
result := run_command("clang", "-c", "-fobjc-arc", "-O2", "-Wall", "-mmacosx-version-min=14.0", source, "-o", object);
if result.exit_code != 0 { log_error("ERROR: clang could not compile %.", source); return false; }
file_delete(library);
result = run_command("ar", "rcs", library, object);
if result.exit_code != 0 { log_error("ERROR: ar could not make %.", library); return false; }
log("Built %", library);
return true;
}
// The oldest macOS it runs on: ScreenCaptureKit's screenshots need 14, and so does onnxruntime 1.30.
MACOS_MINIMUM_VERSION :: Build_Options.Os_Version.{ major = 14, minor = 0 };

View File

@ -0,0 +1,17 @@
// mcap's ScreenCaptureKit shim (capture.m): plain C, for Jai (src/app/screen_capture_macos.jai).
#pragma once
#include <stdint.h>
// One screen to copy. display_id, width and height (its size in pixels) are given; ScreenCaptureKit's
// own width and height and the pixels (packed RGBA, opaque, sRGB; free with mcap_sck_free) come back.
typedef struct {
uint32_t display_id;
int32_t width, height;
uint8_t *pixels;
} Mcap_Sck_Shot;
// Copies each screen, all at once, and waits for them. 1 when every one was copied; else 0, with why
// in error (pixels already made are left set, to be freed).
int mcap_sck_screenshots(Mcap_Sck_Shot *shots, int count, char *error, int error_size);
void mcap_sck_free(void *pointer);

View File

@ -0,0 +1,107 @@
// ScreenCaptureKit for mcap on macOS, behind a plain C interface (capture.h): ScreenCaptureKit is
// asynchronous, with completion blocks and its own queues, which is simpler to wait on here than from
// Jai. first.jai compiles this into capture.a beside it. Recording (macos-plan, phase 7) comes here too.
#import "capture.h"
#import <ScreenCaptureKit/ScreenCaptureKit.h>
#import <CoreGraphics/CoreGraphics.h>
#include <stdlib.h>
#include <string.h>
static void set_error(char *error, int error_size, NSString *text) {
if (!error || error_size <= 0) return;
const char *utf8 = text.UTF8String ? text.UTF8String : "unknown error";
strncpy(error, utf8, (size_t) error_size - 1);
error[error_size - 1] = 0;
}
// The image as packed RGBA, opaque, in sRGB (which the capture is already in): malloc'd.
static uint8_t *rgba_from_image(CGImageRef image, int32_t width, int32_t height) {
size_t row = (size_t) width * 4;
uint8_t *pixels = malloc(row * (size_t) height);
if (!pixels) return NULL;
CGColorSpaceRef srgb = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
CGContextRef bitmap = CGBitmapContextCreate(pixels, (size_t) width, (size_t) height, 8, row, srgb,
kCGBitmapByteOrder32Big | kCGImageAlphaNoneSkipLast);
CGColorSpaceRelease(srgb);
if (!bitmap) { free(pixels); return NULL; }
CGContextSetBlendMode(bitmap, kCGBlendModeCopy);
CGContextDrawImage(bitmap, CGRectMake(0, 0, width, height), image);
CGContextRelease(bitmap);
for (size_t i = 0; i < (size_t) width * (size_t) height; i++) pixels[i * 4 + 3] = 255;
return pixels;
}
int mcap_sck_screenshots(Mcap_Sck_Shot *shots, int count, char *error, int error_size) {
@autoreleasepool {
for (int i = 0; i < count; i++) shots[i].pixels = NULL;
__block SCShareableContent *content = nil;
__block NSError *content_error = nil;
dispatch_semaphore_t got = dispatch_semaphore_create(0);
[SCShareableContent getShareableContentExcludingDesktopWindows:NO onScreenWindowsOnly:YES
completionHandler:^(SCShareableContent *c, NSError *e) {
content = c;
content_error = e;
dispatch_semaphore_signal(got);
}];
if (dispatch_semaphore_wait(got, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC)) != 0) {
set_error(error, error_size, @"ScreenCaptureKit did not list the screens in time");
return 0;
}
if (!content) {
set_error(error, error_size, content_error ? content_error.localizedDescription : @"ScreenCaptureKit listed no screens");
return 0;
}
// Every screen at once; each completion fills its own entry.
dispatch_group_t group = dispatch_group_create();
__block NSString *failure = nil;
NSLock *lock = [NSLock new];
for (int i = 0; i < count; i++) {
SCDisplay *display = nil;
for (SCDisplay *d in content.displays) if (d.displayID == shots[i].display_id) display = d;
if (!display) {
failure = [NSString stringWithFormat:@"ScreenCaptureKit has no display %u", shots[i].display_id];
continue;
}
SCContentFilter *filter = [[SCContentFilter alloc] initWithDisplay:display excludingWindows:@[]];
SCStreamConfiguration *configuration = [SCStreamConfiguration new];
configuration.width = (size_t) shots[i].width;
configuration.height = (size_t) shots[i].height;
configuration.showsCursor = NO;
configuration.colorSpaceName = kCGColorSpaceSRGB;
configuration.captureResolution = SCCaptureResolutionBest;
Mcap_Sck_Shot *shot = &shots[i];
dispatch_group_enter(group);
[SCScreenshotManager captureImageWithFilter:filter configuration:configuration
completionHandler:^(CGImageRef image, NSError *e) {
if (image) {
int32_t w = (int32_t) CGImageGetWidth(image), h = (int32_t) CGImageGetHeight(image);
shot->width = w;
shot->height = h;
shot->pixels = rgba_from_image(image, w, h);
}
if (!shot->pixels) {
[lock lock];
failure = e ? e.localizedDescription : @"ScreenCaptureKit gave no image";
[lock unlock];
}
dispatch_group_leave(group);
}];
}
if (dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)) != 0) {
failure = @"ScreenCaptureKit did not copy the screens in time";
}
if (failure) {
// Entries still being filled after a timeout are left to leak rather than raced.
set_error(error, error_size, failure);
return 0;
}
return 1;
}
}
void mcap_sck_free(void *pointer) {
free(pointer);
}

View File

@ -31,6 +31,7 @@ USING_WINDOWS :: OS == .WINDOWS;
#load "app/screen_capture.jai";
#load "app/screen_record.jai";
#if OS == .WINDOWS #load "app/screen_capture_win32.jai";
#if OS == .MACOS #load "app/screen_capture_macos.jai";
#if OS == .WINDOWS #load "app/screen_record_win32.jai";
#load "base/circle_buffer.jai";

View File

@ -1,6 +1,6 @@
// Screen capture, as Snagit does it: PrintScreen (the capture_region global hotkey, from any app,
// even with mcap hidden in the tray) freezes every screen, and a rectangle dragged over them is taken
// (app/screen_capture_win32.jai; Windows only so far). What is taken goes onto the clipboard at once,
// Screen capture, as Snagit does it: PrintScreen on Windows, Cmd+Shift+2 on macOS (the capture_region
// global hotkey, from any app, even with mcap hidden in the tray) freezes every screen, and a rectangle
// dragged over them is taken (app/screen_capture_win32.jai, app/screen_capture_macos.jai). What is taken goes onto the clipboard at once,
// and into the library as an .mcap in the folder SCREEN_CAPTURE_FOLDER: the pixels as a PNG, with
// the window it was taken from in metadata.json, its title as WindowName and its program as AppName,
// as Snagit records them, so searches find it the same way. mcap then comes to the front showing
@ -219,7 +219,7 @@ screen_capture_out_path :: (name: string) -> path: string, error: string {
return path, "";
}
#if OS != .WINDOWS {
#if OS == .LINUX {
Screen_Shot :: struct {
pixels: [] u8;
width, height: s32;
@ -237,7 +237,7 @@ screen_capture_out_path :: (name: string) -> path: string, error: string {
return .{}, false, "not available on this system yet";
}
#if OS == .LINUX platform_copy_image_to_clipboard :: (pixels: [] u8, width: s32, height: s32) -> bool {
platform_copy_image_to_clipboard :: (pixels: [] u8, width: s32, height: s32) -> bool {
return false;
}
}

View File

@ -0,0 +1,772 @@
// 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";

View File

@ -349,7 +349,7 @@ GLOBAL_HOTKEY_ACTION_COUNT :: 3; // sizes the platforms' registration a
global_hotkey_actions_build :: (out: *[..] Hotkey_Action) {
array_add(out, .{"bring_to_foreground", *bring_to_foreground_hotkey, false});
array_add(out, .{"capture_region", *capture_region_hotkey, true}); // app/screen_capture.jai
array_add(out, .{"capture_region", *capture_region_hotkey, OS == .LINUX}); // app/screen_capture.jai: Windows and macOS
array_add(out, .{"record_region", *record_region_hotkey, true}); // app/screen_record.jai
}