diff --git a/.gitignore b/.gitignore index ed7e4b3..5bd0aad 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/README.md b/README.md index b42ccfa..82c5837 100644 --- a/README.md +++ b/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: diff --git a/assets/data/default_global_hotkeys_macos.config b/assets/data/default_global_hotkeys_macos.config index 2247969..f1dffab 100644 --- a/assets/data/default_global_hotkeys_macos.config +++ b/assets/data/default_global_hotkeys_macos.config @@ -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 diff --git a/check_targets.jai b/check_targets.jai index 42a0990..79e0357 100644 --- a/check_targets.jai +++ b/check_targets.jai @@ -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"; diff --git a/first.jai b/first.jai index 4f728b0..684dfd4 100644 --- a/first.jai +++ b/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 }; diff --git a/modules/Screen_Record/macos/capture.h b/modules/Screen_Record/macos/capture.h new file mode 100644 index 0000000..506a008 --- /dev/null +++ b/modules/Screen_Record/macos/capture.h @@ -0,0 +1,17 @@ +// mcap's ScreenCaptureKit shim (capture.m): plain C, for Jai (src/app/screen_capture_macos.jai). +#pragma once +#include + +// 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); diff --git a/modules/Screen_Record/macos/capture.m b/modules/Screen_Record/macos/capture.m new file mode 100644 index 0000000..81f258c --- /dev/null +++ b/modules/Screen_Record/macos/capture.m @@ -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 +#import +#include +#include + +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); +} diff --git a/src/App.jai b/src/App.jai index b4cc98b..c9acea8 100644 --- a/src/App.jai +++ b/src/App.jai @@ -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"; diff --git a/src/app/screen_capture.jai b/src/app/screen_capture.jai index a458690..4dbae1a 100644 --- a/src/app/screen_capture.jai +++ b/src/app/screen_capture.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; } } diff --git a/src/app/screen_capture_macos.jai b/src/app/screen_capture_macos.jai new file mode 100644 index 0000000..9f4248e --- /dev/null +++ b/src/app/screen_capture_macos.jai @@ -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"; diff --git a/src/ui/imgui_keyboard.jai b/src/ui/imgui_keyboard.jai index b3aa749..90ab29c 100644 --- a/src/ui/imgui_keyboard.jai +++ b/src/ui/imgui_keyboard.jai @@ -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 }