From 222505da05a4112b69f5abf97889e08f7cfca0ec Mon Sep 17 00:00:00 2001 From: Sergey Fedorov Date: Mon, 20 Jul 2026 05:33:03 +0000 Subject: [PATCH 62/71] feat(macos): traditional full screen (View > Toggle Full Screen) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add borderless-window full screen, the 10.6-compatible approach (iTerm2 2014 does the same) — NOT 10.7 toggleFullScreen: nor NSView enterFullScreenMode:. TerminalView toggleTraditionalFullScreen: - enter: save the window frame + style mask, set NSBorderlessWindowMask, size the window to [[window screen] frame], and hide the dock + menu bar via [NSApp setPresentationOptions:AutoHideDock|AutoHideMenuBar]; make the window key and the view first responder. - exit: restore presentation options (Default), the saved style mask, and the saved frame. Backend-agnostic: the window resize drives setFrameSize:, which already updates the GL context; the CPU blit follows the view bounds. A ContourWindow (NSWindow subclass) returns YES from canBecomeKeyWindow/ canBecomeMainWindow so the borderless full-screen window can still take focus (a plain NSWindow refuses key status when borderless). openTerminalWindow uses it. Named toggleTraditionalFullScreen: (not toggleFullScreen:) to avoid the 10.7 NSWindow selector on newer systems. Menu: View > Toggle Full Screen, Cmd+Ctrl+F, targeting the first responder. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01AeG2jwYMX5gdvvtUnx3PJa --- src/contour_macos/TerminalView.h | 6 +++++ src/contour_macos/TerminalView.mm | 39 +++++++++++++++++++++++++++++++ src/contour_macos/main.mm | 33 ++++++++++++++++++++++---- 3 files changed, 74 insertions(+), 4 deletions(-) diff --git a/src/contour_macos/TerminalView.h b/src/contour_macos/TerminalView.h index b99d65b9..fcfced9d 100644 --- a/src/contour_macos/TerminalView.h +++ b/src/contour_macos/TerminalView.h @@ -18,6 +18,9 @@ NSOpenGLContext* _glContext; // non-nil only under the OpenGL backend int _renderMode; // BridgeRenderMode, cached from the session BOOL _glInitialized; // GL program/atlas created (once the context is current) + BOOL _fullScreen; // in traditional (borderless) full-screen + NSRect _savedWindowFrame; // window frame to restore on exit + NSUInteger _savedStyleMask; // window style mask to restore on exit } // Creates the session (spawning a shell) sized to the given frame, using the given render backend @@ -35,4 +38,7 @@ // Requests a repaint on the main thread (safe to call from any thread). - (void)requestRedraw; +// Toggles traditional (borderless-window) full screen. Menu action (View > Toggle Full Screen). +- (void)toggleTraditionalFullScreen:(id)sender; + @end diff --git a/src/contour_macos/TerminalView.mm b/src/contour_macos/TerminalView.mm index 08f2870f..c82a1acd 100644 --- a/src/contour_macos/TerminalView.mm +++ b/src/contour_macos/TerminalView.mm @@ -755,4 +755,43 @@ int cbVerifySshHostkey(void* userData, char const* host, int port, char const* f return NO; } +// --- full screen (traditional / borderless-window, 10.6-compatible) --- + +// The iTerm2 (2014) approach, minimized: no 10.7 toggleTraditionalFullScreen:, no NSView enterFullScreenMode:. +// Enter: save the window frame + style mask, switch the window to a borderless mask sized to the +// whole screen, and auto-hide the dock + menu bar via presentation options. Exit: restore all three. +// Backend-agnostic — the window resize drives setFrameSize:, which already updates the GL context. +- (void)toggleTraditionalFullScreen:(id)sender +{ + (void) sender; + NSWindow* window = [self window]; + if (!window) + return; + + if (!_fullScreen) + { + _savedWindowFrame = [window frame]; + _savedStyleMask = [window styleMask]; + + [window setStyleMask:NSBorderlessWindowMask]; + [window setFrame:[[window screen] frame] display:YES]; + [NSApp setPresentationOptions:(NSApplicationPresentationAutoHideDock + | NSApplicationPresentationAutoHideMenuBar)]; + // A borderless window is not key by default; make ours key and the view first responder so + // input keeps working. + [window makeKeyAndOrderFront:nil]; + [window makeFirstResponder:self]; + _fullScreen = YES; + } + else + { + [NSApp setPresentationOptions:NSApplicationPresentationDefault]; + [window setStyleMask:_savedStyleMask]; + [window setFrame:_savedWindowFrame display:YES]; + [window makeKeyAndOrderFront:nil]; + [window makeFirstResponder:self]; + _fullScreen = NO; + } +} + @end diff --git a/src/contour_macos/main.mm b/src/contour_macos/main.mm index 17aefcf2..696ed66f 100644 --- a/src/contour_macos/main.mm +++ b/src/contour_macos/main.mm @@ -11,6 +11,17 @@ #include #include +// A window that can still become key/main while borderless, which is required for traditional +// full screen (the window switches to NSBorderlessWindowMask). A plain NSWindow refuses key status +// when borderless, which would make the full-screen window unfocusable. +@interface ContourWindow: NSWindow +@end + +@implementation ContourWindow +- (BOOL)canBecomeKeyWindow { return YES; } +- (BOOL)canBecomeMainWindow { return YES; } +@end + // Application delegate: owns the terminal windows and the menu bar, quits on last-window-close. @interface ContourAppDelegate: NSObject - (void)newWindow:(id)sender; @@ -24,10 +35,10 @@ NSRect const frame = NSMakeRect(0, 0, 720, 432); NSUInteger const style = NSTitledWindowMask | NSClosableWindowMask | NSMiniaturizableWindowMask | NSResizableWindowMask; - NSWindow* window = [[NSWindow alloc] initWithContentRect:frame - styleMask:style - backing:NSBackingStoreBuffered - defer:NO]; + NSWindow* window = [[ContourWindow alloc] initWithContentRect:frame + styleMask:style + backing:NSBackingStoreBuffered + defer:NO]; // The initial title is set by the view from the working directory once it is placed in the // window (viewDidMoveToWindow); this is just a placeholder for the brief moment before that. [window setTitle:@"contour"]; @@ -200,6 +211,20 @@ static void installMainMenu(NSApplication* app, ContourAppDelegate* delegate) [editMenu release]; [editItem release]; + // View menu. Toggle Full Screen targets the first responder (the focused TerminalView, which + // implements toggleTraditionalFullScreen:). Cmd+Ctrl+F is the conventional macOS shortcut; it is set as + // Cmd+F with the Control modifier mask. + NSMenuItem* viewItem = [[NSMenuItem alloc] init]; + [menubar addItem:viewItem]; + NSMenu* viewMenu = [[NSMenu alloc] initWithTitle:@"View"]; + [viewItem setSubmenu:viewMenu]; + NSMenuItem* fsItem = [viewMenu addItemWithTitle:@"Toggle Full Screen" + action:@selector(toggleTraditionalFullScreen:) + keyEquivalent:@"f"]; + [fsItem setKeyEquivalentModifierMask:(NSCommandKeyMask | NSControlKeyMask)]; + [viewMenu release]; + [viewItem release]; + // Window menu. setWindowsMenu: hands AppKit ownership: it appends an item per open window, // keeps the list and its checkmark in sync as windows open/close/gain focus, and orders them — // we only provide the standard Minimize/Zoom actions (first-responder/window selectors).