// 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";