// While recording (app/screen_record.jai), on Windows: a thin red border just outside the region, and a // toolbar beside it with the time, the size, frames dropped, and Mic, Pause, Stop and Discard. Both are // plain GDI windows, as the overlay is, so they work while mcap is hidden in the tray, and both are // kept out of every capture (WDA_EXCLUDEFROMCAPTURE), the recording's own included. The border is // outside the region anyway, so it could never be in the video. // // Neither takes the focus: the window being recorded keeps it. The border lets clicks through. A // toolbar button only asks (record_ui.asked); screen_record_frame does it, outside the window's own // message, as Stop and Discard close the toolbar. Discard asks first, in the toolbar itself: a // message box would be in the recording. Record_UI_Ask :: enum u8 { NONE; MICROPHONE; PAUSE; STOP; DISCARD; } record_ui: struct { border, toolbar: HWND; classes_registered: bool; scale: float; font: HFONT; asked: Record_UI_Ask; // a button pressed, for screen_record_frame confirming: bool; // Discard pressed once: "Discard it? Yes No" hovered: s32 = -1; // the button under the mouse buttons: [4] Record_UI_Button; button_count: s32; started: Apollo_Time; size_text: string; // "1920 × 1080"; heap } Record_UI_Button :: struct { rect: RECT; label: string; ask: Record_UI_Ask; confirm: bool; } // Shows the border around region (on the virtual desktop) and the toolbar beside it. record_ui_open :: (region: RECT, whole_screen: bool, width: s32, height: s32) { record_ui_close(); instance := GetModuleHandleW(null); if !record_ui.classes_registered { wc: WNDCLASSEXW; wc.cbSize = size_of(WNDCLASSEXW); wc.lpfnWndProc = xx DefWindowProcW; wc.hInstance = instance; wc.hbrBackground = CreateSolidBrush(RECORD_UI_RED); wc.lpszClassName = utf8_to_wide(RECORD_BORDER_CLASS,, context.default_allocator); RegisterClassExW(*wc); wc.lpfnWndProc = xx record_toolbar_proc; wc.hbrBackground = null; wc.hCursor = LoadCursorW(null, IDC_ARROW); wc.lpszClassName = utf8_to_wide(RECORD_TOOLBAR_CLASS,, context.default_allocator); RegisterClassExW(*wc); record_ui.classes_registered = true; } record_ui.started = current_time_monotonic(); record_ui.asked = .NONE; record_ui.confirming = false; record_ui.hovered = -1; free(record_ui.size_text,, context.default_allocator); record_ui.size_text = sprint("% × %", width, height,, context.default_allocator); monitor := MonitorFromPoint(.{region.left, region.top}, MONITOR_DEFAULTTONEAREST); info: MONITORINFO; info.cbSize = size_of(MONITORINFO); GetMonitorInfoW(monitor, *info); screen := info.rcMonitor; // The toolbar first, to know the screen's DPI. ex_style :: WS_EX_TOPMOST | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE; record_ui.toolbar = CreateWindowExW(ex_style, utf8_to_wide(RECORD_TOOLBAR_CLASS,, temp), utf8_to_wide("Recording",, temp), WS_POPUP, screen.left, screen.top, 10, 10, null, null, instance, null); if !record_ui.toolbar return; dpi := GetDpiForWindow(record_ui.toolbar); record_ui.scale = ifx dpi then dpi / 96.0 else 1.0; font_height := cast(s32) (14 * record_ui.scale + 0.5); record_ui.font = CreateFontW(-font_height, 0, 0, 0, 600, 0, 0, 0, 1, 0, 0, CLEARTYPE_QUALITY, 0, utf8_to_wide("Segoe UI",, temp)); // Below the region, at its right; else above; else inside its top right (kept out of the recording). toolbar_width, toolbar_height := record_toolbar_size(); border := record_ui_px(3); gap := record_ui_px(6); x := region.right - toolbar_width; y := region.bottom + border + gap; if y + toolbar_height > screen.bottom y = region.top - border - gap - toolbar_height; if y < screen.top || whole_screen { y = region.top + gap; x = region.right - toolbar_width - gap; } x = clamp(x, screen.left, screen.right - toolbar_width); SetWindowPos(record_ui.toolbar, HWND_TOPMOST, x, y, toolbar_width, toolbar_height, SWP_NOACTIVATE); visible := array_find(app.args, "-record-ui-visible"); // to look at them in a screenshot if !visible SetWindowDisplayAffinity(record_ui.toolbar, WDA_EXCLUDEFROMCAPTURE); ShowWindow(record_ui.toolbar, SW_SHOWNOACTIVATE); SetTimer(record_ui.toolbar, RECORD_UI_TIMER, 250, null); // The border: a frame border pixels wide just outside the region, which lets clicks through. Not // for a whole screen, where it would be on the screen beside it. if !whole_screen { outer := RECT.{region.left - border, region.top - border, region.right + border, region.bottom + border}; record_ui.border = CreateWindowExW(ex_style | WS_EX_LAYERED | WS_EX_TRANSPARENT, utf8_to_wide(RECORD_BORDER_CLASS,, temp), utf8_to_wide("Recording",, temp), WS_POPUP, outer.left, outer.top, outer.right - outer.left, outer.bottom - outer.top, null, null, instance, null); if record_ui.border { SetLayeredWindowAttributes(record_ui.border, 0, 255, LWA_ALPHA); frame := CreateRectRgn(0, 0, outer.right - outer.left, outer.bottom - outer.top); inside := CreateRectRgn(border, border, outer.right - outer.left - border, outer.bottom - outer.top - border); CombineRgn(frame, frame, inside, RGN_DIFF); DeleteObject(inside); SetWindowRgn(record_ui.border, frame, .TRUE); // the window owns the region from here if !visible SetWindowDisplayAffinity(record_ui.border, WDA_EXCLUDEFROMCAPTURE); ShowWindow(record_ui.border, SW_SHOWNOACTIVATE); } } } record_ui_close :: () { if record_ui.toolbar { KillTimer(record_ui.toolbar, RECORD_UI_TIMER); DestroyWindow(record_ui.toolbar); record_ui.toolbar = null; } if record_ui.border { DestroyWindow(record_ui.border); record_ui.border = null; } if record_ui.font { DeleteObject(record_ui.font); record_ui.font = null; } } // From another thread (the shutdown's): hidden, not destroyed, which only the main thread may do. record_ui_hide :: () { if record_ui.toolbar ShowWindowAsync(record_ui.toolbar, SW_HIDE); if record_ui.border ShowWindowAsync(record_ui.border, SW_HIDE); } // The button pressed since the last call, once. record_ui_take_ask :: () -> Record_UI_Ask { ask := record_ui.asked; record_ui.asked = .NONE; return ask; } record_ui_refresh :: () { if record_ui.toolbar InvalidateRect(record_ui.toolbar, null, .FALSE); } #scope_file RECORD_BORDER_CLASS :: "mcapRecordBorder"; RECORD_TOOLBAR_CLASS :: "mcapRecordToolbar"; RECORD_UI_TIMER :: 1; RECORD_UI_RED :: 0x002828E8; // COLORREF 0x00BBGGRR, as the overlay's video mode RECORD_UI_GREY :: 0x00909090; RECORD_UI_AMBER :: 0x0000B4FF; RECORD_UI_BACK :: 0x00202020; RECORD_UI_BUTTON :: 0x00383838; RECORD_UI_HOVER :: 0x00505050; RECORD_UI_TEXT :: 0x00F0F0F0; record_ui_px :: (pixels: float) -> s32 { return max(cast(s32) (pixels * record_ui.scale + 0.5), 1); } // What the toolbar shows now, left to right: the status, then the buttons. record_toolbar_texts :: () -> status: string, dropped: string { stats := record_stats(*screen_record.recorder); paused := record_paused(*screen_record.recorder); seconds := max(to_float64_seconds(current_time_monotonic() - record_ui.started) - stats.paused_seconds, 0.0); whole := cast(s64) seconds; two :: (n: s64) -> string { return tprint("%", formatInt(n, minimum_digits = 2)); } time := ifx whole >= 3600 then tprint("%:%:%", whole / 3600, two(whole / 60 % 60), two(whole % 60)) else tprint("%:%", two(whole / 60), two(whole % 60)); status := tprint("% % %", ifx paused then "PAUSED" else "REC", time, record_ui.size_text); dropped := ifx stats.dropped then tprint("% dropped", stats.dropped) else ""; if stats.lost { lost := tprint("screen lost % time%", stats.lost, ifx stats.lost == 1 then "" else "s"); dropped = ifx dropped then tprint("%, %", dropped, lost) else lost; } return status, dropped; } // The buttons for the state the toolbar is in. record_toolbar_buttons :: () -> [] Record_UI_Button { if record_ui.confirming { record_ui.buttons[0] = .{label = "Yes, discard", ask = .DISCARD}; record_ui.buttons[1] = .{label = "No", confirm = true}; record_ui.button_count = 2; } else { record_ui.buttons[0] = .{label = ifx record_microphone(*screen_record.recorder) then "Mic: on" else "Mic: off", ask = .MICROPHONE}; record_ui.buttons[1] = .{label = ifx record_paused(*screen_record.recorder) then "Resume" else "Pause", ask = .PAUSE}; record_ui.buttons[2] = .{label = "Stop", ask = .STOP}; record_ui.buttons[3] = .{label = "Discard", confirm = true}; record_ui.button_count = 4; } return array_view(record_ui.buttons, 0, record_ui.button_count); } text_width :: (dc: HDC, text: string) -> s32 { wide := utf8_to_wide(text,, temp); count: s32; while wide[count] count += 1; size: SIZE; GetTextExtentPoint32W(dc, wide, count, *size); return size.cx; } // The toolbar's size, and where its buttons are, for the longest it is likely to be. record_toolbar_size :: () -> width: s32, height: s32 { dc := GetDC(record_ui.toolbar); defer ReleaseDC(record_ui.toolbar, dc); old := SelectObject(dc, record_ui.font); defer SelectObject(dc, old); padding, gap := record_ui_px(10), record_ui_px(8); status_width := text_width(dc, tprint("PAUSED 00:00:00 %", record_ui.size_text)); dropped_width := text_width(dc, "999 dropped, screen lost 9 times"); buttons_width := text_width(dc, "Mic: off") + text_width(dc, "Resume") + text_width(dc, "Stop") + text_width(dc, "Yes, discard") + 4 * 2 * padding + 3 * gap; height := record_ui_px(34); width := padding + record_ui_px(12) + gap + status_width + gap + dropped_width + gap + buttons_width + padding; return width, height; } record_toolbar_proc :: (hwnd: HWND, message: u32, wParam: WPARAM, lParam: LPARAM) -> LRESULT #c_call { if !platform.main_context return DefWindowProcW(hwnd, message, wParam, lParam); push_context platform.main_context.* { if message == { case WM_MOUSEACTIVATE; return MA_NOACTIVATE; // the window being recorded keeps the focus case WM_ERASEBKGND; return 1; case WM_TIMER; // The time moves on, and a mouse that left the toolbar no longer lights a button. cursor: POINT; GetCursorPos(*cursor); window: RECT; GetWindowRect(hwnd, *window); if cursor.x < window.left || cursor.x >= window.right || cursor.y < window.top || cursor.y >= window.bottom record_ui.hovered = -1; InvalidateRect(hwnd, null, .FALSE); return 0; case WM_MOUSEMOVE; hovered := record_toolbar_hit(.{cast(s32) LOWORD(lParam), cast(s32) HIWORD(lParam)}); if hovered != record_ui.hovered { record_ui.hovered = hovered; InvalidateRect(hwnd, null, .FALSE); } return 0; case WM_LBUTTONDOWN; hit := record_toolbar_hit(.{cast(s32) LOWORD(lParam), cast(s32) HIWORD(lParam)}); if hit >= 0 { button := record_ui.buttons[hit]; if button.confirm record_ui.confirming = !record_ui.confirming; else if button.ask != .NONE { record_ui.asked = button.ask; record_ui.confirming = false; } InvalidateRect(hwnd, null, .FALSE); } return 0; case WM_PAINT; record_toolbar_paint(hwnd); return 0; } return DefWindowProcW(hwnd, message, wParam, lParam); } } record_toolbar_hit :: (p: POINT) -> s32 { for array_view(record_ui.buttons, 0, record_ui.button_count) { r := it.rect; if p.x >= r.left && p.x < r.right && p.y >= r.top && p.y < r.bottom return cast(s32) it_index; } return -1; } record_toolbar_paint :: (hwnd: HWND) { paint: PAINTSTRUCT; window_dc := BeginPaint(hwnd, *paint); defer EndPaint(hwnd, *paint); client: RECT; GetClientRect(hwnd, *client); width, height := client.right, client.bottom; // Drawn whole into a bitmap, then put on screen: no flicker as the time changes. dc := CreateCompatibleDC(window_dc); defer DeleteDC(dc); bitmap := CreateCompatibleBitmap(window_dc, width, height); defer DeleteObject(bitmap); old_bitmap := SelectObject(dc, bitmap); defer SelectObject(dc, old_bitmap); old_font := SelectObject(dc, record_ui.font); defer SelectObject(dc, old_font); fill :: (r: RECT, colour: u32) #expand { brush := CreateSolidBrush(colour); rect := r; FillRect(`dc, *rect, brush); DeleteObject(brush); } text_at :: (x: s32, text: string, colour: u32) #expand { SetTextColor(`dc, colour); wide := utf8_to_wide(text,, temp); count: s32; while wide[count] count += 1; size: SIZE; GetTextExtentPoint32W(`dc, wide, count, *size); TextOutW(`dc, x, (`height - size.cy) / 2, wide, count); } fill(.{0, 0, width, height}, RECORD_UI_BACK); SetBkMode(dc, TRANSPARENT); padding, gap := record_ui_px(10), record_ui_px(8); paused := record_paused(*screen_record.recorder); // The dot: red while recording, blinking each half second; grey while paused. dot := record_ui_px(12); blink := (cast(s64) (to_float64_seconds(current_time_monotonic() - record_ui.started) * 2)) % 2 == 0; dot_colour: u32 = ifx paused then cast(u32) RECORD_UI_GREY else ifx blink then cast(u32) RECORD_UI_RED else cast(u32) 0x00602020; brush := CreateSolidBrush(dot_colour); old_brush := SelectObject(dc, brush); old_pen := SelectObject(dc, GetStockObject(NULL_PEN)); Ellipse(dc, padding, (height - dot) / 2, padding + dot, (height - dot) / 2 + dot); SelectObject(dc, old_pen); SelectObject(dc, old_brush); DeleteObject(brush); status, dropped := record_toolbar_texts(); x := padding + dot + gap; text_at(x, status, RECORD_UI_TEXT); x += text_width(dc, status) + gap; if dropped text_at(x, dropped, RECORD_UI_AMBER); // The buttons, from the right. buttons := record_toolbar_buttons(); button_height := height - 2 * record_ui_px(5); right := width - padding; for < buttons { label_width := text_width(dc, it.label); r := RECT.{right - label_width - 2 * padding, (height - button_height) / 2, right, (height - button_height) / 2 + button_height}; record_ui.buttons[it_index].rect = r; lit := it.ask == .MICROPHONE && record_microphone(*screen_record.recorder); // on: in the record red fill(r, ifx lit then cast(u32) RECORD_UI_RED else ifx it_index == record_ui.hovered then cast(u32) RECORD_UI_HOVER else cast(u32) RECORD_UI_BUTTON); SetTextColor(dc, ifx it.ask == .DISCARD then cast(u32) RECORD_UI_AMBER else cast(u32) RECORD_UI_TEXT); wide := utf8_to_wide(it.label,, temp); count: s32; while wide[count] count += 1; size: SIZE; GetTextExtentPoint32W(dc, wide, count, *size); TextOutW(dc, r.left + padding, r.top + (button_height - size.cy) / 2, wide, count); right = r.left - gap; } if record_ui.confirming text_at(right - text_width(dc, "Discard it?"), "Discard it?", RECORD_UI_AMBER); BitBlt(window_dc, 0, 0, width, height, dc, 0, 0, .SRCCOPY); }