[0.1.33] macOS phase 6: screen capture stills with Cmd+Shift+2, through ScreenCaptureKit
This commit is contained in:
parent
91f6cf768f
commit
63a0cdb64e
3
.gitignore
vendored
3
.gitignore
vendored
@ -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/
|
||||
|
||||
22
README.md
22
README.md
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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";
|
||||
|
||||
24
first.jai
24
first.jai
@ -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 };
|
||||
|
||||
|
||||
17
modules/Screen_Record/macos/capture.h
Normal file
17
modules/Screen_Record/macos/capture.h
Normal 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);
|
||||
107
modules/Screen_Record/macos/capture.m
Normal file
107
modules/Screen_Record/macos/capture.m
Normal 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);
|
||||
}
|
||||
@ -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";
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
772
src/app/screen_capture_macos.jai
Normal file
772
src/app/screen_capture_macos.jai
Normal 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";
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user