From 174c468da844e91a1cb8bd4c80ff2e005de1672b Mon Sep 17 00:00:00 2001 From: Kei Kamikawa Date: Tue, 10 Feb 2026 02:47:26 +0900 Subject: [PATCH 1/2] Implement liquid glass layout for window content and enhance zoom functionality --- virtualization_view.m | 101 +++++++++++++++++++++++++++++++++--------- 1 file changed, 81 insertions(+), 20 deletions(-) diff --git a/virtualization_view.m b/virtualization_view.m index 136781da..6d967627 100644 --- a/virtualization_view.m +++ b/virtualization_view.m @@ -169,9 +169,12 @@ @implementation AppDelegate { VZVirtualMachine *_virtualMachine; dispatch_queue_t _queue; VZVirtualMachineView *_virtualMachineView; + NSScrollView *_scrollView; + NSView *_liquidGlassBaseView; NSWindow *_window; NSToolbar *_toolbar; BOOL _enableController; + BOOL _useWindowCornerSafeLayout; // Overlay for pause mode. NSVisualEffectView *_pauseOverlayView; // Zoom function properties. @@ -210,6 +213,7 @@ - (instancetype)initWithVirtualMachine:(VZVirtualMachine *)virtualMachine _window = [self createMainWindowWithTitle:windowTitle width:windowWidth height:windowHeight]; _toolbar = [self createCustomToolbar]; _enableController = enableController; + _useWindowCornerSafeLayout = [self shouldUseWindowCornerSafeLayout]; [_virtualMachine addObserver:self forKeyPath:@"state" options:NSKeyValueObservingOptionNew @@ -234,6 +238,8 @@ - (void)dealloc if (_virtualMachine) { [_virtualMachine removeObserver:self forKeyPath:@"state"]; } + _scrollView = nil; + _liquidGlassBaseView = nil; _virtualMachineView = nil; _virtualMachine = nil; _queue = nil; @@ -405,6 +411,53 @@ - (void)windowWillClose:(NSNotification *)notification [NSApp performSelectorOnMainThread:@selector(terminate:) withObject:self waitUntilDone:NO]; } +- (BOOL)shouldUseWindowCornerSafeLayout +{ + if (@available(macOS 26.0, *)) { + return YES; + } + return NO; +} + +- (NSView *)createLiquidGlassBaseView +{ + NSView *baseView = [[[NSView alloc] initWithFrame:NSZeroRect] autorelease]; + baseView.translatesAutoresizingMaskIntoConstraints = NO; + baseView.wantsLayer = YES; + baseView.layer.cornerRadius = 6.0; + baseView.layer.masksToBounds = YES; + baseView.layer.backgroundColor = [[NSColor colorWithCalibratedWhite:1.0 alpha:0.06] CGColor]; + baseView.layer.borderWidth = 1.0; + baseView.layer.borderColor = [[NSColor colorWithCalibratedWhite:1.0 alpha:0.16] CGColor]; + return baseView; +} + +- (NSView *)createWindowContentViewForLiquidGlassLayoutWithScrollView:(NSScrollView *)scrollView +{ + NSView *contentView = [[[NSView alloc] initWithFrame:_window.contentView.bounds] autorelease]; + contentView.translatesAutoresizingMaskIntoConstraints = NO; + _liquidGlassBaseView = [self createLiquidGlassBaseView]; + [contentView addSubview:_liquidGlassBaseView]; + [_liquidGlassBaseView addSubview:scrollView]; + + [scrollView setTranslatesAutoresizingMaskIntoConstraints:NO]; + const CGFloat basePadding = 12.0; + [NSLayoutConstraint activateConstraints:@[ + [_liquidGlassBaseView.leadingAnchor constraintEqualToAnchor:contentView.leadingAnchor + constant:basePadding], + [_liquidGlassBaseView.trailingAnchor constraintEqualToAnchor:contentView.trailingAnchor + constant:-basePadding], + [_liquidGlassBaseView.topAnchor constraintEqualToAnchor:contentView.topAnchor], + [_liquidGlassBaseView.bottomAnchor constraintEqualToAnchor:contentView.bottomAnchor + constant:-basePadding], + [scrollView.leadingAnchor constraintEqualToAnchor:_liquidGlassBaseView.leadingAnchor], + [scrollView.trailingAnchor constraintEqualToAnchor:_liquidGlassBaseView.trailingAnchor], + [scrollView.topAnchor constraintEqualToAnchor:_liquidGlassBaseView.topAnchor], + [scrollView.bottomAnchor constraintEqualToAnchor:_liquidGlassBaseView.bottomAnchor] + ]]; + return contentView; +} + - (void)setupGraphicWindow { // Set custom title bar @@ -423,22 +476,28 @@ - (void)setupGraphicWindow // Add scroll wheel event monitor for zoom functionality _scrollWheelMonitor = [NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskScrollWheel - handler:^NSEvent *(NSEvent *event) { - [self handleScrollWheel:event]; - return event; - }]; + handler:^NSEvent *(NSEvent *event) { + [self handleScrollWheel:event]; + return event; + }]; // Create scroll view for the virtual machine view - NSScrollView *scrollView = [self createScrollViewForVirtualMachineView:_virtualMachineView]; - [_window setContentView:scrollView]; + _scrollView = [self createScrollViewForVirtualMachineView:_virtualMachineView]; + if (_useWindowCornerSafeLayout) { + NSView *contentView = [self createWindowContentViewForLiquidGlassLayoutWithScrollView:_scrollView]; + [_window setContentView:contentView]; + } else { + [_window setContentView:_scrollView]; + } - // Configure Auto Layout constraints for VirtualMachineView to resize with the window + // Configure Auto Layout constraints for VirtualMachineView to resize with the window. [_virtualMachineView setTranslatesAutoresizingMaskIntoConstraints:NO]; + NSClipView *clipView = _scrollView.contentView; [NSLayoutConstraint activateConstraints:@[ - [_virtualMachineView.leadingAnchor constraintEqualToAnchor:_window.contentView.leadingAnchor], - [_virtualMachineView.trailingAnchor constraintEqualToAnchor:_window.contentView.trailingAnchor], - [_virtualMachineView.topAnchor constraintEqualToAnchor:_window.contentView.topAnchor], - [_virtualMachineView.bottomAnchor constraintEqualToAnchor:_window.contentView.bottomAnchor] + [_virtualMachineView.leadingAnchor constraintEqualToAnchor:clipView.leadingAnchor], + [_virtualMachineView.trailingAnchor constraintEqualToAnchor:clipView.trailingAnchor], + [_virtualMachineView.topAnchor constraintEqualToAnchor:clipView.topAnchor], + [_virtualMachineView.bottomAnchor constraintEqualToAnchor:clipView.bottomAnchor] ]]; NSSize sizeInPixels = [self getVirtualMachineSizeInPixels]; @@ -649,15 +708,18 @@ - (void)showErrorAlertWithMessage:(NSString *)message error:(NSError *)error - (void)toggleZoomMode:(id)sender { + if (_scrollView == nil) { + return; + } _isZoomEnabled = !_isZoomEnabled; - NSScrollView *scrollView = (NSScrollView *)_window.contentView; + NSScrollView *scrollView = _scrollView; // Reset zoom when zoom mode is disabled. if (!_isZoomEnabled) { [NSAnimationContext runAnimationGroup:^(NSAnimationContext *context) { [context setDuration:0.3]; - [[_window.contentView animator] setMagnification:1.0]; + [[scrollView animator] setMagnification:1.0]; } completionHandler:^{ // Hide scrollers when zoom is disabled @@ -733,12 +795,11 @@ - (void)handleScrollWheel:(NSEvent *)event } // Only zoom if Command or Option key is held - if (!(event.modifierFlags & NSEventModifierFlagCommand) && - !(event.modifierFlags & NSEventModifierFlagOption)) { + if (!(event.modifierFlags & NSEventModifierFlagCommand) && !(event.modifierFlags & NSEventModifierFlagOption)) { return; } - NSScrollView *scrollView = (NSScrollView *)_window.contentView; + NSScrollView *scrollView = _scrollView; if (![scrollView isKindOfClass:[NSScrollView class]]) { return; } @@ -752,8 +813,8 @@ - (void)handleScrollWheel:(NSEvent *)event newMagnification = MIN(scrollView.maxMagnification, MAX(scrollView.minMagnification, newMagnification)); // Get mouse location for centered zooming - NSPoint mouseLocation = [_window.contentView convertPoint:event.locationInWindow fromView:nil]; - NSPoint centeredPoint = [scrollView.contentView convertPoint:mouseLocation fromView:_window.contentView]; + NSPoint mouseLocation = [scrollView convertPoint:event.locationInWindow fromView:nil]; + NSPoint centeredPoint = [scrollView.contentView convertPoint:mouseLocation fromView:scrollView]; [scrollView setMagnification:newMagnification centeredAtPoint:centeredPoint]; } @@ -767,7 +828,7 @@ - (void)handleMouseMovement:(NSEvent *)event return; } - NSScrollView *scrollView = (NSScrollView *)_window.contentView; + NSScrollView *scrollView = _scrollView; if (![scrollView isKindOfClass:[NSScrollView class]]) { [self stopScrollTimer]; return; @@ -829,7 +890,7 @@ - (void)stopScrollTimer - (void)scrollTick:(NSTimer *)timer { - NSScrollView *scrollView = (NSScrollView *)_window.contentView; + NSScrollView *scrollView = _scrollView; if (![scrollView isKindOfClass:[NSScrollView class]]) { [self stopScrollTimer]; return; From be7467b16d71bfa006c53b05a2d88d0534dd98b2 Mon Sep 17 00:00:00 2001 From: Kei Kamikawa Date: Thu, 12 Feb 2026 02:06:45 +0900 Subject: [PATCH 2/2] Refactor window layout to use titlebar window and update toolbar item management --- virtualization_view.m | 108 ++++++++++++++++++++++++++---------------- 1 file changed, 67 insertions(+), 41 deletions(-) diff --git a/virtualization_view.m b/virtualization_view.m index 6d967627..ae1e22a4 100644 --- a/virtualization_view.m +++ b/virtualization_view.m @@ -170,11 +170,10 @@ @implementation AppDelegate { dispatch_queue_t _queue; VZVirtualMachineView *_virtualMachineView; NSScrollView *_scrollView; - NSView *_liquidGlassBaseView; NSWindow *_window; NSToolbar *_toolbar; BOOL _enableController; - BOOL _useWindowCornerSafeLayout; + BOOL _useTitlebarWindow; // Overlay for pause mode. NSVisualEffectView *_pauseOverlayView; // Zoom function properties. @@ -213,7 +212,7 @@ - (instancetype)initWithVirtualMachine:(VZVirtualMachine *)virtualMachine _window = [self createMainWindowWithTitle:windowTitle width:windowWidth height:windowHeight]; _toolbar = [self createCustomToolbar]; _enableController = enableController; - _useWindowCornerSafeLayout = [self shouldUseWindowCornerSafeLayout]; + _useTitlebarWindow = [self shouldUseTitlebarWindow]; [_virtualMachine addObserver:self forKeyPath:@"state" options:NSKeyValueObservingOptionNew @@ -239,7 +238,6 @@ - (void)dealloc [_virtualMachine removeObserver:self forKeyPath:@"state"]; } _scrollView = nil; - _liquidGlassBaseView = nil; _virtualMachineView = nil; _virtualMachine = nil; _queue = nil; @@ -340,26 +338,42 @@ - (void)hideOverlay static NSString *const PowerToolbarIdentifier = @"Power"; static NSString *const SpaceToolbarIdentifier = @"Space"; static NSString *const Space2ToolbarIdentifier = @"Space2"; +static NSString *const TitleToolbarIdentifier = @"Title"; - (NSArray *)setupToolbarItemIdentifiers { NSMutableArray *toolbarItems = [NSMutableArray array]; + if (_useTitlebarWindow) { + [toolbarItems addObject:TitleToolbarIdentifier]; + // macOS 26+ titlebar window: keep controls aligned to the right. + [toolbarItems addObject:NSToolbarFlexibleSpaceItemIdentifier]; + } if (_enableController) { if ([self canPauseVirtualMachine]) { [toolbarItems addObject:PauseToolbarIdentifier]; } if ([self canResumeVirtualMachine]) { - [toolbarItems addObject:SpaceToolbarIdentifier]; + if (_useTitlebarWindow) { + [toolbarItems addObject:NSToolbarSpaceItemIdentifier]; + } else { + [toolbarItems addObject:SpaceToolbarIdentifier]; + } [toolbarItems addObject:PlayToolbarIdentifier]; } if ([self canStopVirtualMachine] || [self canStartVirtualMachine]) { - [toolbarItems addObject:Space2ToolbarIdentifier]; + if (_useTitlebarWindow) { + [toolbarItems addObject:NSToolbarSpaceItemIdentifier]; + } else { + [toolbarItems addObject:Space2ToolbarIdentifier]; + } [toolbarItems addObject:PowerToolbarIdentifier]; } } [toolbarItems addObject:NSToolbarSpaceItemIdentifier]; [toolbarItems addObject:ZoomToolbarIdentifier]; - [toolbarItems addObject:NSToolbarFlexibleSpaceItemIdentifier]; + if (!_useTitlebarWindow) { + [toolbarItems addObject:NSToolbarFlexibleSpaceItemIdentifier]; + } return [toolbarItems copy]; } @@ -411,7 +425,10 @@ - (void)windowWillClose:(NSNotification *)notification [NSApp performSelectorOnMainThread:@selector(terminate:) withObject:self waitUntilDone:NO]; } -- (BOOL)shouldUseWindowCornerSafeLayout +// On macOS 26+, a toolbar window uses a 26pt corner radius, while a titlebar window uses a 16pt radius. +// Using a titlebar window keeps corner UI controls operable inside the guest display. +// See: https://github.com/Code-Hex/vz/issues/210 +- (BOOL)shouldUseTitlebarWindow { if (@available(macOS 26.0, *)) { return YES; @@ -419,51 +436,37 @@ - (BOOL)shouldUseWindowCornerSafeLayout return NO; } -- (NSView *)createLiquidGlassBaseView -{ - NSView *baseView = [[[NSView alloc] initWithFrame:NSZeroRect] autorelease]; - baseView.translatesAutoresizingMaskIntoConstraints = NO; - baseView.wantsLayer = YES; - baseView.layer.cornerRadius = 6.0; - baseView.layer.masksToBounds = YES; - baseView.layer.backgroundColor = [[NSColor colorWithCalibratedWhite:1.0 alpha:0.06] CGColor]; - baseView.layer.borderWidth = 1.0; - baseView.layer.borderColor = [[NSColor colorWithCalibratedWhite:1.0 alpha:0.16] CGColor]; - return baseView; -} - - (NSView *)createWindowContentViewForLiquidGlassLayoutWithScrollView:(NSScrollView *)scrollView { NSView *contentView = [[[NSView alloc] initWithFrame:_window.contentView.bounds] autorelease]; contentView.translatesAutoresizingMaskIntoConstraints = NO; - _liquidGlassBaseView = [self createLiquidGlassBaseView]; - [contentView addSubview:_liquidGlassBaseView]; - [_liquidGlassBaseView addSubview:scrollView]; + [contentView addSubview:scrollView]; [scrollView setTranslatesAutoresizingMaskIntoConstraints:NO]; - const CGFloat basePadding = 12.0; [NSLayoutConstraint activateConstraints:@[ - [_liquidGlassBaseView.leadingAnchor constraintEqualToAnchor:contentView.leadingAnchor - constant:basePadding], - [_liquidGlassBaseView.trailingAnchor constraintEqualToAnchor:contentView.trailingAnchor - constant:-basePadding], - [_liquidGlassBaseView.topAnchor constraintEqualToAnchor:contentView.topAnchor], - [_liquidGlassBaseView.bottomAnchor constraintEqualToAnchor:contentView.bottomAnchor - constant:-basePadding], - [scrollView.leadingAnchor constraintEqualToAnchor:_liquidGlassBaseView.leadingAnchor], - [scrollView.trailingAnchor constraintEqualToAnchor:_liquidGlassBaseView.trailingAnchor], - [scrollView.topAnchor constraintEqualToAnchor:_liquidGlassBaseView.topAnchor], - [scrollView.bottomAnchor constraintEqualToAnchor:_liquidGlassBaseView.bottomAnchor] + [scrollView.leadingAnchor constraintEqualToAnchor:contentView.leadingAnchor], + [scrollView.trailingAnchor constraintEqualToAnchor:contentView.trailingAnchor], + [scrollView.topAnchor constraintEqualToAnchor:contentView.topAnchor], + [scrollView.bottomAnchor constraintEqualToAnchor:contentView.bottomAnchor] ]]; return contentView; } - (void)setupGraphicWindow { - // Set custom title bar - [_window setTitlebarAppearsTransparent:YES]; + if (_useTitlebarWindow) { + [_window setTitlebarAppearsTransparent:NO]; + [_window setTitleVisibility:NSWindowTitleHidden]; + [_window setToolbarStyle:NSWindowToolbarStyleExpanded]; + [_window setOpaque:YES]; + [_window setBackgroundColor:[NSColor windowBackgroundColor]]; + [_toolbar setShowsBaselineSeparator:YES]; + } else { + [_window setTitlebarAppearsTransparent:YES]; + [_window setOpaque:NO]; + [_toolbar setShowsBaselineSeparator:NO]; + } [_window setToolbar:_toolbar]; - [_window setOpaque:NO]; [_window center]; // Monitoring mouse movement events to control auto-scrolling behavior @@ -483,7 +486,7 @@ - (void)setupGraphicWindow // Create scroll view for the virtual machine view _scrollView = [self createScrollViewForVirtualMachineView:_virtualMachineView]; - if (_useWindowCornerSafeLayout) { + if (_useTitlebarWindow) { NSView *contentView = [self createWindowContentViewForLiquidGlassLayoutWithScrollView:_scrollView]; [_window setContentView:contentView]; } else { @@ -563,6 +566,7 @@ - (NSWindow *)createMainWindowWithTitle:(NSString *)title - (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar *)toolbar { return @[ + TitleToolbarIdentifier, ZoomToolbarIdentifier, PlayToolbarIdentifier, PauseToolbarIdentifier, @@ -578,7 +582,29 @@ - (NSToolbarItem *)toolbar:(NSToolbar *)toolbar itemForItemIdentifier:(NSToolbar { NSToolbarItem *item = [[[NSToolbarItem alloc] initWithItemIdentifier:itemIdentifier] autorelease]; - if ([itemIdentifier isEqualToString:PauseToolbarIdentifier]) { + if ([itemIdentifier isEqualToString:TitleToolbarIdentifier]) { + NSTextField *titleLabel = [NSTextField labelWithString:_window.title ?: @""]; + titleLabel.font = [NSFont systemFontOfSize:16 weight:NSFontWeightSemibold]; + titleLabel.textColor = [NSColor labelColor]; + titleLabel.lineBreakMode = NSLineBreakByTruncatingTail; + titleLabel.maximumNumberOfLines = 1; + NSView *titleContainer = [[[NSView alloc] initWithFrame:NSMakeRect(0, 0, 420, 20)] autorelease]; + titleContainer.translatesAutoresizingMaskIntoConstraints = NO; + titleLabel.translatesAutoresizingMaskIntoConstraints = NO; + const CGFloat paddingLeft = 26.0; + [titleContainer addSubview:titleLabel]; + [NSLayoutConstraint activateConstraints:@[ + [titleLabel.leadingAnchor constraintEqualToAnchor:titleContainer.leadingAnchor + constant:paddingLeft], + [titleLabel.trailingAnchor constraintLessThanOrEqualToAnchor:titleContainer.trailingAnchor], + [titleLabel.centerYAnchor constraintEqualToAnchor:titleContainer.centerYAnchor] + ]]; + item.view = titleContainer; + item.minSize = NSMakeSize(80, 20); + item.maxSize = NSMakeSize(420, 20); + [item setBordered:NO]; + [item setLabel:@"Title"]; + } else if ([itemIdentifier isEqualToString:PauseToolbarIdentifier]) { [item setImage:[NSImage imageWithSystemSymbolName:@"pause.fill" accessibilityDescription:nil]]; [item setLabel:@"Pause"]; [item setTarget:self];