app: [macOS] track window state changes initiated by the operating system

Before this change, the window state was explicitly updated whenever
Window.Option was called. However, the system may also change window
state as a result of user gestures, but those changes did not result in
ConfigEvents reflecting them.

Remove the explicit state updates and track them when the system tells
us it has changed.

This is a step towards fixing #600 which require accurate window state
tracking.

References: https://todo.sr.ht/~eliasnaur/gio/600
Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
Elias Naur
2024-07-31 15:42:21 +02:00
parent 042ed4ab49
commit af6dda67a5
2 changed files with 101 additions and 133 deletions
+95 -126
View File
@@ -109,6 +109,14 @@ static void makeKeyAndOrderFront(CFTypeRef windowRef) {
} }
} }
static void makeFirstResponder(CFTypeRef windowRef, CFTypeRef viewRef) {
@autoreleasepool {
NSWindow *window = (__bridge NSWindow *)windowRef;
NSView *view = (__bridge NSView *)viewRef;
[window makeFirstResponder:view];
}
}
static void toggleFullScreen(CFTypeRef windowRef) { static void toggleFullScreen(CFTypeRef windowRef) {
@autoreleasepool { @autoreleasepool {
NSWindow *window = (__bridge NSWindow *)windowRef; NSWindow *window = (__bridge NSWindow *)windowRef;
@@ -238,6 +246,13 @@ static int isWindowZoomed(CFTypeRef windowRef) {
} }
} }
static int isWindowMiniaturized(CFTypeRef windowRef) {
@autoreleasepool {
NSWindow *window = (__bridge NSWindow *)windowRef;
return window.miniaturized ? 1 : 0;
}
}
static void zoomWindow(CFTypeRef windowRef) { static void zoomWindow(CFTypeRef windowRef) {
@autoreleasepool { @autoreleasepool {
NSWindow *window = (__bridge NSWindow *)windowRef; NSWindow *window = (__bridge NSWindow *)windowRef;
@@ -318,7 +333,6 @@ type window struct {
view C.CFTypeRef view C.CFTypeRef
w *callbacks w *callbacks
anim bool anim bool
visible bool
displayLink *displayLink displayLink *displayLink
// redraw is a single entry channel for making sure only one // redraw is a single entry channel for making sure only one
// display link redraw request is in flight. // display link redraw request is in flight.
@@ -367,11 +381,23 @@ func (w *window) WriteClipboard(mime string, s []byte) {
} }
func (w *window) updateWindowMode() { func (w *window) updateWindowMode() {
w.scale = float32(C.getViewBackingScale(w.view))
wf, hf := float32(C.viewWidth(w.view)), float32(C.viewHeight(w.view))
w.config.Size = image.Point{
X: int(wf*w.scale + .5),
Y: int(hf*w.scale + .5),
}
w.config.Mode = Windowed
window := C.windowForView(w.view)
if window == 0 {
return
}
style := int(C.getWindowStyleMask(C.windowForView(w.view))) style := int(C.getWindowStyleMask(C.windowForView(w.view)))
if style&C.NSWindowStyleMaskFullScreen != 0 { switch {
case style&C.NSWindowStyleMaskFullScreen != 0:
w.config.Mode = Fullscreen w.config.Mode = Fullscreen
} else { case C.isWindowZoomed(window) != 0:
w.config.Mode = Windowed w.config.Mode = Maximized
} }
w.config.Decorated = style&C.NSWindowStyleMaskFullSizeContentView == 0 w.config.Decorated = style&C.NSWindowStyleMaskFullSizeContentView == 0
} }
@@ -379,105 +405,82 @@ func (w *window) updateWindowMode() {
func (w *window) Configure(options []Option) { func (w *window) Configure(options []Option) {
screenScale := float32(C.getScreenBackingScale()) screenScale := float32(C.getScreenBackingScale())
cfg := configFor(screenScale) cfg := configFor(screenScale)
prev := w.config
w.updateWindowMode()
cnf := w.config cnf := w.config
cnf.apply(cfg, options) cnf.apply(cfg, options)
window := C.windowForView(w.view) window := C.windowForView(w.view)
mask := C.getWindowStyleMask(window)
fullscreen := mask&C.NSWindowStyleMaskFullScreen != 0
switch cnf.Mode { switch cnf.Mode {
case Fullscreen: case Fullscreen:
switch prev.Mode { if C.isWindowMiniaturized(window) != 0 {
case Fullscreen:
case Minimized:
C.unhideWindow(window) C.unhideWindow(window)
fallthrough }
default: if !fullscreen {
w.config.Mode = Fullscreen
C.toggleFullScreen(window) C.toggleFullScreen(window)
} }
case Minimized: case Minimized:
switch prev.Mode { C.hideWindow(window)
case Minimized, Fullscreen:
default:
w.config.Mode = Minimized
C.hideWindow(window)
}
case Maximized: case Maximized:
switch prev.Mode { if C.isWindowMiniaturized(window) != 0 {
case Fullscreen:
case Minimized:
C.unhideWindow(window) C.unhideWindow(window)
fallthrough }
default: if fullscreen {
w.config.Mode = Maximized C.toggleFullScreen(window)
w.setTitle(prev, cnf) }
if C.isWindowZoomed(window) == 0 { w.setTitle(cnf.Title)
C.zoomWindow(window) if C.isWindowZoomed(window) == 0 {
} C.zoomWindow(window)
} }
case Windowed: case Windowed:
switch prev.Mode { if C.isWindowMiniaturized(window) != 0 {
case Fullscreen:
C.toggleFullScreen(window)
case Minimized:
C.unhideWindow(window) C.unhideWindow(window)
case Maximized:
if C.isWindowZoomed(window) != 0 {
C.zoomWindow(window)
}
} }
w.config.Mode = Windowed if fullscreen {
w.setTitle(prev, cnf) C.toggleFullScreen(window)
if prev.Size != cnf.Size {
w.config.Size = cnf.Size
cnf.Size = cnf.Size.Div(int(screenScale))
C.setSize(window, C.CGFloat(cnf.Size.X), C.CGFloat(cnf.Size.Y))
} }
if prev.MinSize != cnf.MinSize { w.setTitle(cnf.Title)
w.config.MinSize = cnf.MinSize w.config.Size = cnf.Size
cnf.MinSize = cnf.MinSize.Div(int(screenScale)) cnf.Size = cnf.Size.Div(int(screenScale))
C.setMinSize(window, C.CGFloat(cnf.MinSize.X), C.CGFloat(cnf.MinSize.Y)) C.setSize(window, C.CGFloat(cnf.Size.X), C.CGFloat(cnf.Size.Y))
} w.config.MinSize = cnf.MinSize
if prev.MaxSize != cnf.MaxSize { cnf.MinSize = cnf.MinSize.Div(int(screenScale))
w.config.MaxSize = cnf.MaxSize C.setMinSize(window, C.CGFloat(cnf.MinSize.X), C.CGFloat(cnf.MinSize.Y))
cnf.MaxSize = cnf.MaxSize.Div(int(screenScale)) w.config.MaxSize = cnf.MaxSize
cnf.MaxSize = cnf.MaxSize.Div(int(screenScale))
if cnf.MaxSize != (image.Point{}) {
C.setMaxSize(window, C.CGFloat(cnf.MaxSize.X), C.CGFloat(cnf.MaxSize.Y)) C.setMaxSize(window, C.CGFloat(cnf.MaxSize.X), C.CGFloat(cnf.MaxSize.Y))
} }
} if C.isWindowZoomed(window) != 0 {
if cnf.Decorated != prev.Decorated { C.zoomWindow(window)
w.config.Decorated = cnf.Decorated
mask := C.getWindowStyleMask(window)
style := C.NSWindowStyleMask(C.NSWindowStyleMaskTitled | C.NSWindowStyleMaskResizable | C.NSWindowStyleMaskMiniaturizable | C.NSWindowStyleMaskClosable)
style = C.NSWindowStyleMaskFullSizeContentView
mask &^= style
barTrans := C.int(C.NO)
titleVis := C.NSWindowTitleVisibility(C.NSWindowTitleVisible)
if !cnf.Decorated {
mask |= style
barTrans = C.YES
titleVis = C.NSWindowTitleHidden
} }
C.setWindowTitlebarAppearsTransparent(window, barTrans)
C.setWindowTitleVisibility(window, titleVis)
C.setWindowStyleMask(window, mask)
C.setWindowStandardButtonHidden(window, C.NSWindowCloseButton, barTrans)
C.setWindowStandardButtonHidden(window, C.NSWindowMiniaturizeButton, barTrans)
C.setWindowStandardButtonHidden(window, C.NSWindowZoomButton, barTrans)
// When toggling the titlebar, the layer doesn't update its frame
// until the next resize. Force it.
C.resetLayerFrame(w.view)
} }
w.ProcessEvent(ConfigEvent{Config: w.config}) style := C.NSWindowStyleMask(C.NSWindowStyleMaskTitled | C.NSWindowStyleMaskResizable | C.NSWindowStyleMaskMiniaturizable | C.NSWindowStyleMaskClosable)
style = C.NSWindowStyleMaskFullSizeContentView
mask &^= style
barTrans := C.int(C.NO)
titleVis := C.NSWindowTitleVisibility(C.NSWindowTitleVisible)
if !cnf.Decorated {
mask |= style
barTrans = C.YES
titleVis = C.NSWindowTitleHidden
}
C.setWindowTitlebarAppearsTransparent(window, barTrans)
C.setWindowTitleVisibility(window, titleVis)
C.setWindowStyleMask(window, mask)
C.setWindowStandardButtonHidden(window, C.NSWindowCloseButton, barTrans)
C.setWindowStandardButtonHidden(window, C.NSWindowMiniaturizeButton, barTrans)
C.setWindowStandardButtonHidden(window, C.NSWindowZoomButton, barTrans)
// When toggling the titlebar, the layer doesn't update its frame
// until the next resize. Force it.
C.resetLayerFrame(w.view)
} }
func (w *window) setTitle(prev, cnf Config) { func (w *window) setTitle(title string) {
if prev.Title != cnf.Title { w.config.Title = title
w.config.Title = cnf.Title titleC := stringToNSString(title)
title := stringToNSString(cnf.Title) defer C.CFRelease(titleC)
defer C.CFRelease(title) C.setTitle(C.windowForView(w.view), titleC)
C.setTitle(C.windowForView(w.view), title)
}
} }
func (w *window) Perform(acts system.Action) { func (w *window) Perform(acts system.Action) {
@@ -520,7 +523,8 @@ func (w *window) SetInputHint(_ key.InputHint) {}
func (w *window) SetAnimating(anim bool) { func (w *window) SetAnimating(anim bool) {
w.anim = anim w.anim = anim
if w.anim && w.visible { window := C.windowForView(w.view)
if w.anim && window != 0 {
w.displayLink.Start() w.displayLink.Start()
} else { } else {
w.displayLink.Stop() w.displayLink.Stop()
@@ -799,24 +803,19 @@ func gio_firstRectForCharacterRange(h C.uintptr_t, crng C.NSRange, actual C.NSRa
} }
func (w *window) draw() { func (w *window) draw() {
cnf := w.config
w.updateWindowMode()
if w.config != cnf {
w.ProcessEvent(ConfigEvent{Config: w.config})
}
select { select {
case <-w.redraw: case <-w.redraw:
default: default:
} }
w.visible = true
if w.anim { if w.anim {
w.SetAnimating(w.anim) w.SetAnimating(w.anim)
} }
w.scale = float32(C.getViewBackingScale(w.view)) sz := w.config.Size
wf, hf := float32(C.viewWidth(w.view)), float32(C.viewHeight(w.view))
sz := image.Point{
X: int(wf*w.scale + .5),
Y: int(hf*w.scale + .5),
}
if sz != w.config.Size {
w.config.Size = sz
w.ProcessEvent(ConfigEvent{Config: w.config})
}
if sz.X == 0 || sz.Y == 0 { if sz.X == 0 || sz.Y == 0 {
return return
} }
@@ -824,7 +823,7 @@ func (w *window) draw() {
w.ProcessEvent(frameEvent{ w.ProcessEvent(frameEvent{
FrameEvent: FrameEvent{ FrameEvent: FrameEvent{
Now: time.Now(), Now: time.Now(),
Size: w.config.Size, Size: sz,
Metric: cfg, Metric: cfg,
}, },
Sync: true, Sync: true,
@@ -867,7 +866,6 @@ func gio_onAttached(h C.uintptr_t, attached C.int) {
w.ProcessEvent(AppKitViewEvent{View: uintptr(w.view), Layer: uintptr(layer)}) w.ProcessEvent(AppKitViewEvent{View: uintptr(w.view), Layer: uintptr(layer)})
} else { } else {
w.ProcessEvent(AppKitViewEvent{}) w.ProcessEvent(AppKitViewEvent{})
w.visible = false
w.SetAnimating(w.anim) w.SetAnimating(w.anim)
} }
} }
@@ -882,33 +880,6 @@ func gio_onDestroy(h C.uintptr_t) {
w.view = 0 w.view = 0
} }
//export gio_onHide
func gio_onHide(h C.uintptr_t) {
w := windowFor(h)
w.visible = false
w.SetAnimating(w.anim)
}
//export gio_onShow
func gio_onShow(h C.uintptr_t) {
w := windowFor(h)
w.draw()
}
//export gio_onFullscreen
func gio_onFullscreen(h C.uintptr_t) {
w := windowFor(h)
w.config.Mode = Fullscreen
w.ProcessEvent(ConfigEvent{Config: w.config})
}
//export gio_onWindowed
func gio_onWindowed(h C.uintptr_t) {
w := windowFor(h)
w.config.Mode = Windowed
w.ProcessEvent(ConfigEvent{Config: w.config})
}
//export gio_onFinishLaunching //export gio_onFinishLaunching
func gio_onFinishLaunching() { func gio_onFinishLaunching() {
close(launched) close(launched)
@@ -931,10 +902,9 @@ func newWindow(win *callbacks, options []Option) {
w.ProcessEvent(DestroyEvent{Err: err}) w.ProcessEvent(DestroyEvent{Err: err})
return return
} }
window := C.gio_createWindow(w.view, 0, 0, 0, 0, 0, 0) window := C.gio_createWindow(w.view, C.CGFloat(cnf.Size.X), C.CGFloat(cnf.Size.Y), 0, 0, 0, 0)
// Release our reference now that the NSWindow has it. // Release our reference now that the NSWindow has it.
C.CFRelease(w.view) C.CFRelease(w.view)
w.updateWindowMode()
w.Configure(options) w.Configure(options)
if nextTopLeft.x == 0 && nextTopLeft.y == 0 { if nextTopLeft.x == 0 && nextTopLeft.y == 0 {
// cascadeTopLeftFromPoint treats (0, 0) as a no-op, // cascadeTopLeftFromPoint treats (0, 0) as a no-op,
@@ -942,6 +912,7 @@ func newWindow(win *callbacks, options []Option) {
nextTopLeft = C.cascadeTopLeftFromPoint(window, nextTopLeft) nextTopLeft = C.cascadeTopLeftFromPoint(window, nextTopLeft)
} }
nextTopLeft = C.cascadeTopLeftFromPoint(window, nextTopLeft) nextTopLeft = C.cascadeTopLeftFromPoint(window, nextTopLeft)
C.makeFirstResponder(window, w.view)
// makeKeyAndOrderFront assumes ownership of our window reference. // makeKeyAndOrderFront assumes ownership of our window reference.
C.makeKeyAndOrderFront(window) C.makeKeyAndOrderFront(window)
}) })
@@ -966,9 +937,7 @@ func (w *window) init(customRenderer bool) error {
return return
} }
w.runOnMain(func() { w.runOnMain(func() {
if w.visible { C.setNeedsDisplay(w.view)
C.setNeedsDisplay(w.view)
}
}) })
}) })
w.displayLink = dl w.displayLink = dl
+6 -7
View File
@@ -23,22 +23,22 @@ __attribute__ ((visibility ("hidden"))) CALayer *gio_layerFactory(BOOL presentWi
- (void)windowWillMiniaturize:(NSNotification *)notification { - (void)windowWillMiniaturize:(NSNotification *)notification {
NSWindow *window = (NSWindow *)[notification object]; NSWindow *window = (NSWindow *)[notification object];
GioView *view = (GioView *)window.contentView; GioView *view = (GioView *)window.contentView;
gio_onHide(view.handle); gio_onDraw(view.handle);
} }
- (void)windowDidDeminiaturize:(NSNotification *)notification { - (void)windowDidDeminiaturize:(NSNotification *)notification {
NSWindow *window = (NSWindow *)[notification object]; NSWindow *window = (NSWindow *)[notification object];
GioView *view = (GioView *)window.contentView; GioView *view = (GioView *)window.contentView;
gio_onShow(view.handle); gio_onDraw(view.handle);
} }
- (void)windowWillEnterFullScreen:(NSNotification *)notification { - (void)windowWillEnterFullScreen:(NSNotification *)notification {
NSWindow *window = (NSWindow *)[notification object]; NSWindow *window = (NSWindow *)[notification object];
GioView *view = (GioView *)window.contentView; GioView *view = (GioView *)window.contentView;
gio_onFullscreen(view.handle); gio_onDraw(view.handle);
} }
- (void)windowWillExitFullScreen:(NSNotification *)notification { - (void)windowWillExitFullScreen:(NSNotification *)notification {
NSWindow *window = (NSWindow *)[notification object]; NSWindow *window = (NSWindow *)[notification object];
GioView *view = (GioView *)window.contentView; GioView *view = (GioView *)window.contentView;
gio_onWindowed(view.handle); gio_onDraw(view.handle);
} }
- (void)windowDidChangeScreen:(NSNotification *)notification { - (void)windowDidChangeScreen:(NSNotification *)notification {
NSWindow *window = (NSWindow *)[notification object]; NSWindow *window = (NSWindow *)[notification object];
@@ -202,10 +202,10 @@ static void handleMouse(GioView *view, NSEvent *event, int typ, CGFloat dx, CGFl
return [[self window] convertRectToScreen:r]; return [[self window] convertRectToScreen:r];
} }
- (void)applicationWillUnhide:(NSNotification *)notification { - (void)applicationWillUnhide:(NSNotification *)notification {
gio_onShow(self.handle); gio_onDraw(self.handle);
} }
- (void)applicationDidHide:(NSNotification *)notification { - (void)applicationDidHide:(NSNotification *)notification {
gio_onHide(self.handle); gio_onDraw(self.handle);
} }
- (void)dealloc { - (void)dealloc {
gio_onDestroy(self.handle); gio_onDestroy(self.handle);
@@ -387,7 +387,6 @@ CFTypeRef gio_createWindow(CFTypeRef viewRef, CGFloat width, CGFloat height, CGF
[window setAcceptsMouseMovedEvents:YES]; [window setAcceptsMouseMovedEvents:YES];
NSView *view = (__bridge NSView *)viewRef; NSView *view = (__bridge NSView *)viewRef;
[window setContentView:view]; [window setContentView:view];
[window makeFirstResponder:view];
window.delegate = globalWindowDel; window.delegate = globalWindowDel;
return (__bridge_retained CFTypeRef)window; return (__bridge_retained CFTypeRef)window;
} }