mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-28 15:44:32 +00:00
Debouncing but working
This commit is contained in:
parent
64b5815c4a
commit
2e19ff1ed9
4 changed files with 384 additions and 90 deletions
|
|
@ -38,13 +38,17 @@ struct ContentView: View {
|
||||||
|
|
||||||
let enabledGraphsCount = [showCpuGraph, showMemoryGraph, showGpuGraph, showNetworkGraph, showDiskGraph].filter { $0 }.count
|
let enabledGraphsCount = [showCpuGraph, showMemoryGraph, showGpuGraph, showNetworkGraph, showDiskGraph].filter { $0 }.count
|
||||||
|
|
||||||
// If 4+ graphs are enabled, increase width
|
// Calculate height based on layout: 1-3 graphs = single row, 4+ graphs = two rows
|
||||||
|
var requiredHeight = openNotchSize.height
|
||||||
|
|
||||||
if enabledGraphsCount >= 4 {
|
if enabledGraphsCount >= 4 {
|
||||||
let extraWidth: CGFloat = CGFloat(enabledGraphsCount - 3) * 120
|
// Two rows needed - add height for second row plus spacing
|
||||||
return CGSize(width: openNotchSize.width + extraWidth, height: openNotchSize.height)
|
let extraHeight: CGFloat = 120 + 12 // Graph height + spacing
|
||||||
|
requiredHeight = openNotchSize.height + extraHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
return openNotchSize
|
// Width stays constant - no horizontal expansion
|
||||||
|
return CGSize(width: openNotchSize.width, height: requiredHeight)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -87,11 +87,30 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
private var onboardingWindowController: NSWindowController?
|
private var onboardingWindowController: NSWindowController?
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
// Debouncing mechanism for window size updates
|
||||||
|
private var windowSizeUpdateWorkItem: DispatchWorkItem?
|
||||||
|
|
||||||
|
private func debouncedUpdateWindowSize() {
|
||||||
|
// Cancel any existing work item
|
||||||
|
windowSizeUpdateWorkItem?.cancel()
|
||||||
|
|
||||||
|
// Create new work item with delay
|
||||||
|
let workItem = DispatchWorkItem { [weak self] in
|
||||||
|
self?.updateWindowSizeIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store reference and schedule
|
||||||
|
windowSizeUpdateWorkItem = workItem
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15, execute: workItem)
|
||||||
|
}
|
||||||
|
|
||||||
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func applicationWillTerminate(_ notification: Notification) {
|
func applicationWillTerminate(_ notification: Notification) {
|
||||||
|
// Cancel any pending window size updates
|
||||||
|
windowSizeUpdateWorkItem?.cancel()
|
||||||
NotificationCenter.default.removeObserver(self)
|
NotificationCenter.default.removeObserver(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -126,9 +145,12 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
private func createDynamicIslandWindow(for screen: NSScreen, with viewModel: DynamicIslandViewModel)
|
private func createDynamicIslandWindow(for screen: NSScreen, with viewModel: DynamicIslandViewModel)
|
||||||
-> NSWindow
|
-> NSWindow
|
||||||
{
|
{
|
||||||
|
// Use the current required size instead of always using openNotchSize
|
||||||
|
let requiredSize = calculateRequiredNotchSize()
|
||||||
|
|
||||||
let window = DynamicIslandWindow(
|
let window = DynamicIslandWindow(
|
||||||
contentRect: NSRect(
|
contentRect: NSRect(
|
||||||
x: 0, y: 0, width: openNotchSize.width, height: openNotchSize.height),
|
x: 0, y: 0, width: requiredSize.width, height: requiredSize.height),
|
||||||
styleMask: [.borderless, .nonactivatingPanel, .utilityWindow, .hudWindow],
|
styleMask: [.borderless, .nonactivatingPanel, .utilityWindow, .hudWindow],
|
||||||
backing: .buffered,
|
backing: .buffered,
|
||||||
defer: false
|
defer: false
|
||||||
|
|
@ -151,14 +173,20 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
window.alphaValue = 0
|
window.alphaValue = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
DispatchQueue.main.async { [weak window] in
|
// Use the same centering logic as updateWindowSizeIfNeeded()
|
||||||
guard let window = window else { return }
|
let screenFrame = screen.frame
|
||||||
let screenFrame = screen.frame
|
let centerX = screenFrame.origin.x + (screenFrame.width / 2)
|
||||||
window.setFrameOrigin(
|
let newX = centerX - (window.frame.width / 2)
|
||||||
NSPoint(
|
let newY = screenFrame.origin.y + screenFrame.height - window.frame.height
|
||||||
x: screenFrame.origin.x + (screenFrame.width / 2) - window.frame.width / 2,
|
|
||||||
y: screenFrame.origin.y + screenFrame.height - window.frame.height
|
window.setFrame(NSRect(
|
||||||
))
|
x: newX,
|
||||||
|
y: newY,
|
||||||
|
width: window.frame.width,
|
||||||
|
height: window.frame.height
|
||||||
|
), display: false)
|
||||||
|
|
||||||
|
if changeAlpha {
|
||||||
window.alphaValue = 1
|
window.alphaValue = 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -167,22 +195,65 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
// Calculate required size based on current state
|
// Calculate required size based on current state
|
||||||
let requiredSize = calculateRequiredNotchSize()
|
let requiredSize = calculateRequiredNotchSize()
|
||||||
|
|
||||||
// Update all windows if size has changed
|
if Defaults[.showOnAllDisplays] {
|
||||||
for (screen, window) in windows {
|
// Update all windows if size has changed (multi-display mode)
|
||||||
if window.frame.size != requiredSize {
|
for (screen, window) in windows {
|
||||||
let screenFrame = screen.frame
|
if window.frame.size != requiredSize {
|
||||||
let newFrame = NSRect(
|
// Calculate center position BEFORE any changes
|
||||||
x: screenFrame.origin.x + (screenFrame.width / 2) - requiredSize.width / 2,
|
let screenFrame = screen.frame
|
||||||
y: screenFrame.origin.y + screenFrame.height - requiredSize.height,
|
let centerX = screenFrame.origin.x + (screenFrame.width / 2)
|
||||||
width: requiredSize.width,
|
let newX = centerX - (requiredSize.width / 2)
|
||||||
height: requiredSize.height
|
let newY = screenFrame.origin.y + screenFrame.height - requiredSize.height
|
||||||
)
|
|
||||||
|
|
||||||
// Animate the frame change for smooth transition
|
// Stop any existing animations first
|
||||||
|
NSAnimationContext.runAnimationGroup { _ in
|
||||||
|
window.animator().setFrame(window.frame, display: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Animate window resize smoothly
|
||||||
|
NSAnimationContext.runAnimationGroup { context in
|
||||||
|
context.duration = 0.25
|
||||||
|
context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
|
||||||
|
context.allowsImplicitAnimation = true
|
||||||
|
window.animator().setFrame(NSRect(
|
||||||
|
x: newX,
|
||||||
|
y: newY,
|
||||||
|
width: requiredSize.width,
|
||||||
|
height: requiredSize.height
|
||||||
|
), display: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Update single window if size has changed (single display mode)
|
||||||
|
if let window = window, window.frame.size != requiredSize {
|
||||||
|
// Find the screen this window is on
|
||||||
|
let currentScreen = NSScreen.screens.first { screen in
|
||||||
|
screen.frame.intersects(window.frame)
|
||||||
|
} ?? NSScreen.main ?? NSScreen.screens.first!
|
||||||
|
|
||||||
|
// Calculate center position BEFORE any changes
|
||||||
|
let screenFrame = currentScreen.frame
|
||||||
|
let centerX = screenFrame.origin.x + (screenFrame.width / 2)
|
||||||
|
let newX = centerX - (requiredSize.width / 2)
|
||||||
|
let newY = screenFrame.origin.y + screenFrame.height - requiredSize.height
|
||||||
|
|
||||||
|
// Stop any existing animations first
|
||||||
|
NSAnimationContext.runAnimationGroup { _ in
|
||||||
|
window.animator().setFrame(window.frame, display: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Animate window resize smoothly
|
||||||
NSAnimationContext.runAnimationGroup { context in
|
NSAnimationContext.runAnimationGroup { context in
|
||||||
context.duration = 0.3
|
context.duration = 0.25
|
||||||
|
context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
|
||||||
context.allowsImplicitAnimation = true
|
context.allowsImplicitAnimation = true
|
||||||
window.setFrame(newFrame, display: true, animate: true)
|
window.animator().setFrame(NSRect(
|
||||||
|
x: newX,
|
||||||
|
y: newY,
|
||||||
|
width: requiredSize.width,
|
||||||
|
height: requiredSize.height
|
||||||
|
), display: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -202,61 +273,51 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
Defaults[.showDiskGraph]
|
Defaults[.showDiskGraph]
|
||||||
].filter { $0 }.count
|
].filter { $0 }.count
|
||||||
|
|
||||||
// If 4+ graphs are enabled, increase width
|
// Calculate height based on layout: 1-3 graphs = single row, 4+ graphs = two rows
|
||||||
|
var requiredHeight = openNotchSize.height
|
||||||
|
|
||||||
if enabledGraphsCount >= 4 {
|
if enabledGraphsCount >= 4 {
|
||||||
let extraWidth: CGFloat = CGFloat(enabledGraphsCount - 3) * 120
|
// Two rows needed - add height for second row plus spacing
|
||||||
return CGSize(width: openNotchSize.width + extraWidth, height: openNotchSize.height)
|
let extraHeight: CGFloat = 120 + 12 // Graph height + spacing
|
||||||
|
requiredHeight = openNotchSize.height + extraHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
return openNotchSize
|
// Width stays constant - no horizontal expansion
|
||||||
|
return CGSize(width: openNotchSize.width, height: requiredHeight)
|
||||||
}
|
}
|
||||||
|
|
||||||
func applicationDidFinishLaunching(_ notification: Notification) {
|
func applicationDidFinishLaunching(_ notification: Notification) {
|
||||||
|
|
||||||
coordinator.setupWorkersNotificationObservers()
|
coordinator.setupWorkersNotificationObservers()
|
||||||
|
|
||||||
// Observe tab changes to update window size dynamically
|
// Observe tab changes - use debounced updates
|
||||||
coordinator.objectWillChange.sink { [weak self] in
|
coordinator.$currentView.sink { [weak self] newView in
|
||||||
DispatchQueue.main.async {
|
self?.debouncedUpdateWindowSize()
|
||||||
self?.updateWindowSizeIfNeeded()
|
|
||||||
}
|
|
||||||
}.store(in: &cancellables)
|
}.store(in: &cancellables)
|
||||||
|
|
||||||
// Observe stats settings changes
|
// Observe stats settings changes - use debounced updates
|
||||||
Defaults.publisher(.enableStatsFeature, options: []).sink { [weak self] _ in
|
Defaults.publisher(.enableStatsFeature, options: []).sink { [weak self] _ in
|
||||||
DispatchQueue.main.async {
|
self?.debouncedUpdateWindowSize()
|
||||||
self?.updateWindowSizeIfNeeded()
|
|
||||||
}
|
|
||||||
}.store(in: &cancellables)
|
}.store(in: &cancellables)
|
||||||
|
|
||||||
Defaults.publisher(.showCpuGraph, options: []).sink { [weak self] _ in
|
Defaults.publisher(.showCpuGraph, options: []).sink { [weak self] _ in
|
||||||
DispatchQueue.main.async {
|
self?.debouncedUpdateWindowSize()
|
||||||
self?.updateWindowSizeIfNeeded()
|
|
||||||
}
|
|
||||||
}.store(in: &cancellables)
|
}.store(in: &cancellables)
|
||||||
|
|
||||||
Defaults.publisher(.showMemoryGraph, options: []).sink { [weak self] _ in
|
Defaults.publisher(.showMemoryGraph, options: []).sink { [weak self] _ in
|
||||||
DispatchQueue.main.async {
|
self?.debouncedUpdateWindowSize()
|
||||||
self?.updateWindowSizeIfNeeded()
|
|
||||||
}
|
|
||||||
}.store(in: &cancellables)
|
}.store(in: &cancellables)
|
||||||
|
|
||||||
Defaults.publisher(.showGpuGraph, options: []).sink { [weak self] _ in
|
Defaults.publisher(.showGpuGraph, options: []).sink { [weak self] _ in
|
||||||
DispatchQueue.main.async {
|
self?.debouncedUpdateWindowSize()
|
||||||
self?.updateWindowSizeIfNeeded()
|
|
||||||
}
|
|
||||||
}.store(in: &cancellables)
|
}.store(in: &cancellables)
|
||||||
|
|
||||||
Defaults.publisher(.showNetworkGraph, options: []).sink { [weak self] _ in
|
Defaults.publisher(.showNetworkGraph, options: []).sink { [weak self] _ in
|
||||||
DispatchQueue.main.async {
|
self?.debouncedUpdateWindowSize()
|
||||||
self?.updateWindowSizeIfNeeded()
|
|
||||||
}
|
|
||||||
}.store(in: &cancellables)
|
}.store(in: &cancellables)
|
||||||
|
|
||||||
Defaults.publisher(.showDiskGraph, options: []).sink { [weak self] _ in
|
Defaults.publisher(.showDiskGraph, options: []).sink { [weak self] _ in
|
||||||
DispatchQueue.main.async {
|
self?.debouncedUpdateWindowSize()
|
||||||
self?.updateWindowSizeIfNeeded()
|
|
||||||
}
|
|
||||||
}.store(in: &cancellables)
|
}.store(in: &cancellables)
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(
|
NotificationCenter.default.addObserver(
|
||||||
|
|
|
||||||
|
|
@ -118,15 +118,141 @@ struct NotchStatsView: View {
|
||||||
return graphs
|
return graphs
|
||||||
}
|
}
|
||||||
|
|
||||||
var gridColumns: [GridItem] {
|
// New vertical expansion layout system
|
||||||
|
@ViewBuilder
|
||||||
|
var statsGridLayout: some View {
|
||||||
let graphCount = availableGraphs.count
|
let graphCount = availableGraphs.count
|
||||||
|
|
||||||
if graphCount <= 3 {
|
switch graphCount {
|
||||||
// Horizontal layout for 1-3 graphs
|
case 1...3:
|
||||||
return Array(repeating: GridItem(.flexible(), spacing: 12), count: graphCount)
|
// Single row for 1-3 graphs
|
||||||
} else {
|
HStack(spacing: 12) {
|
||||||
// Compressed grid layout for 4+ graphs
|
ForEach(0..<graphCount, id: \.self) { index in
|
||||||
return Array(repeating: GridItem(.flexible(), spacing: 8), count: graphCount)
|
graphView(for: availableGraphs[index])
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.transition(.asymmetric(
|
||||||
|
insertion: .scale.combined(with: .opacity),
|
||||||
|
removal: .scale.combined(with: .opacity)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.animation(.easeInOut(duration: 0.25), value: graphCount)
|
||||||
|
|
||||||
|
case 4:
|
||||||
|
// 2x2 quadrants for 4 graphs
|
||||||
|
VStack(spacing: 12) {
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
graphView(for: availableGraphs[0])
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.transition(.asymmetric(
|
||||||
|
insertion: .scale.combined(with: .opacity),
|
||||||
|
removal: .scale.combined(with: .opacity)
|
||||||
|
))
|
||||||
|
graphView(for: availableGraphs[1])
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.transition(.asymmetric(
|
||||||
|
insertion: .scale.combined(with: .opacity),
|
||||||
|
removal: .scale.combined(with: .opacity)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
graphView(for: availableGraphs[2])
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.transition(.asymmetric(
|
||||||
|
insertion: .scale.combined(with: .opacity),
|
||||||
|
removal: .scale.combined(with: .opacity)
|
||||||
|
))
|
||||||
|
graphView(for: availableGraphs[3])
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.transition(.asymmetric(
|
||||||
|
insertion: .scale.combined(with: .opacity),
|
||||||
|
removal: .scale.combined(with: .opacity)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.animation(.easeInOut(duration: 0.25), value: graphCount)
|
||||||
|
|
||||||
|
case 5:
|
||||||
|
// 3 on top, 2 on bottom (taking half space each)
|
||||||
|
VStack(spacing: 12) {
|
||||||
|
// Top row: 3 graphs
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
graphView(for: availableGraphs[0])
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.transition(.asymmetric(
|
||||||
|
insertion: .scale.combined(with: .opacity),
|
||||||
|
removal: .scale.combined(with: .opacity)
|
||||||
|
))
|
||||||
|
graphView(for: availableGraphs[1])
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.transition(.asymmetric(
|
||||||
|
insertion: .scale.combined(with: .opacity),
|
||||||
|
removal: .scale.combined(with: .opacity)
|
||||||
|
))
|
||||||
|
graphView(for: availableGraphs[2])
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.transition(.asymmetric(
|
||||||
|
insertion: .scale.combined(with: .opacity),
|
||||||
|
removal: .scale.combined(with: .opacity)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
// Bottom row: 2 graphs centered
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
Spacer()
|
||||||
|
graphView(for: availableGraphs[3])
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.transition(.asymmetric(
|
||||||
|
insertion: .scale.combined(with: .opacity),
|
||||||
|
removal: .scale.combined(with: .opacity)
|
||||||
|
))
|
||||||
|
graphView(for: availableGraphs[4])
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.transition(.asymmetric(
|
||||||
|
insertion: .scale.combined(with: .opacity),
|
||||||
|
removal: .scale.combined(with: .opacity)
|
||||||
|
))
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.animation(.easeInOut(duration: 0.25), value: graphCount)
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Fallback for more than 5 graphs (shouldn't happen with current settings)
|
||||||
|
LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: min(3, graphCount)), spacing: 12) {
|
||||||
|
ForEach(0..<graphCount, id: \.self) { index in
|
||||||
|
graphView(for: availableGraphs[index])
|
||||||
|
.transition(.asymmetric(
|
||||||
|
insertion: .scale.combined(with: .opacity),
|
||||||
|
removal: .scale.combined(with: .opacity)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.animation(.easeInOut(duration: 0.25), value: graphCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to create graph views
|
||||||
|
@ViewBuilder
|
||||||
|
func graphView(for graphData: GraphData) -> some View {
|
||||||
|
if let singleData = graphData as? SingleGraphData {
|
||||||
|
StatCard(
|
||||||
|
title: singleData.title,
|
||||||
|
value: singleData.value,
|
||||||
|
data: singleData.data,
|
||||||
|
color: singleData.color,
|
||||||
|
icon: singleData.icon
|
||||||
|
)
|
||||||
|
} else if let dualData = graphData as? DualGraphData {
|
||||||
|
DualStatCard(
|
||||||
|
title: dualData.title,
|
||||||
|
positiveValue: dualData.positiveValue,
|
||||||
|
negativeValue: dualData.negativeValue,
|
||||||
|
positiveData: dualData.positiveData,
|
||||||
|
negativeData: dualData.negativeData,
|
||||||
|
positiveColor: dualData.positiveColor,
|
||||||
|
negativeColor: dualData.negativeColor,
|
||||||
|
icon: dualData.icon
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -171,39 +297,18 @@ struct NotchStatsView: View {
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
.padding()
|
.padding()
|
||||||
} else {
|
} else {
|
||||||
// Stats content with dynamic layout
|
// Stats content with vertical expansion layout
|
||||||
ZStack(alignment: .topTrailing) {
|
ZStack(alignment: .topTrailing) {
|
||||||
VStack(spacing: 12) {
|
VStack(spacing: 12) {
|
||||||
// Dynamic Stats Grid
|
// New vertical expansion layout
|
||||||
LazyVGrid(columns: gridColumns, spacing: 12) {
|
statsGridLayout
|
||||||
ForEach(0..<availableGraphs.count, id: \.self) { index in
|
|
||||||
let graphData = availableGraphs[index]
|
|
||||||
|
|
||||||
if let singleData = graphData as? SingleGraphData {
|
|
||||||
StatCard(
|
|
||||||
title: singleData.title,
|
|
||||||
value: singleData.value,
|
|
||||||
data: singleData.data,
|
|
||||||
color: singleData.color,
|
|
||||||
icon: singleData.icon
|
|
||||||
)
|
|
||||||
} else if let dualData = graphData as? DualGraphData {
|
|
||||||
DualStatCard(
|
|
||||||
title: dualData.title,
|
|
||||||
positiveValue: dualData.positiveValue,
|
|
||||||
negativeValue: dualData.negativeValue,
|
|
||||||
positiveData: dualData.positiveData,
|
|
||||||
negativeData: dualData.negativeData,
|
|
||||||
positiveColor: dualData.positiveColor,
|
|
||||||
negativeColor: dualData.negativeColor,
|
|
||||||
icon: dualData.icon
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
||||||
}
|
}
|
||||||
.padding(16)
|
.padding(16)
|
||||||
|
.animation(.easeInOut(duration: 0.25), value: availableGraphs.count)
|
||||||
|
.transition(.asymmetric(
|
||||||
|
insertion: .scale.combined(with: .opacity),
|
||||||
|
removal: .scale.combined(with: .opacity)
|
||||||
|
))
|
||||||
|
|
||||||
// Live indicator and controls in top-right corner
|
// Live indicator and controls in top-right corner
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
|
|
|
||||||
124
logic.md
Normal file
124
logic.md
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
# Dynamic Island Alignment Logic Analysis
|
||||||
|
|
||||||
|
## Problem Statement
|
||||||
|
The Dynamic Island window sometimes gets misaligned (moves to the left) when switching between tabs, especially when going from stats tab (with 4+ graphs that make the window wider) back to other tabs.
|
||||||
|
|
||||||
|
## Current Implementation Analysis
|
||||||
|
|
||||||
|
### When Window is Created
|
||||||
|
1. `createDynamicIslandWindow()` creates window with `openNotchSize` (fixed size)
|
||||||
|
2. `positionWindow()` centers it using current window width
|
||||||
|
3. Window is perfectly centered initially
|
||||||
|
|
||||||
|
### When User Switches to Stats Tab (4+ graphs)
|
||||||
|
1. `coordinator.$currentView` triggers `updateWindowSizeIfNeeded()`
|
||||||
|
2. `calculateRequiredNotchSize()` returns larger size (openNotchSize.width + extraWidth)
|
||||||
|
3. `updateWindowSizeIfNeeded()` resizes window and re-centers using new width
|
||||||
|
4. Window should stay centered with new larger size
|
||||||
|
|
||||||
|
### When User Switches Away from Stats Tab
|
||||||
|
1. `coordinator.$currentView` triggers `updateWindowSizeIfNeeded()` again
|
||||||
|
2. `calculateRequiredNotchSize()` returns normal `openNotchSize`
|
||||||
|
3. `updateWindowSizeIfNeeded()` resizes window back and re-centers using normal width
|
||||||
|
4. **THIS IS WHERE THE PROBLEM OCCURS**
|
||||||
|
|
||||||
|
## Root Cause Analysis
|
||||||
|
|
||||||
|
### The Issue: Timing and State Mismatch
|
||||||
|
|
||||||
|
The problem is likely that `coordinator.currentView` and the actual UI state are not perfectly synchronized. Here's what probably happens:
|
||||||
|
|
||||||
|
1. User clicks another tab
|
||||||
|
2. `coordinator.$currentView` fires immediately
|
||||||
|
3. `updateWindowSizeIfNeeded()` calculates size as if we're NOT on stats tab
|
||||||
|
4. BUT the ContentView might still be showing stats content (transition not complete)
|
||||||
|
5. Window gets resized to small size while content is still large
|
||||||
|
6. This causes misalignment because content and window size don't match
|
||||||
|
|
||||||
|
### Why It Sometimes Works
|
||||||
|
- When the UI transition is fast enough, everything stays in sync
|
||||||
|
- When there's any delay or animation, they get out of sync
|
||||||
|
|
||||||
|
## Potential Solutions
|
||||||
|
|
||||||
|
### Option 1: Delay the Window Resize
|
||||||
|
Add a small delay to ensure UI transition is complete before resizing window.
|
||||||
|
|
||||||
|
### Option 2: Remove Window Resizing, Use Content Clipping
|
||||||
|
Let the window stay large and let the content handle its own sizing internally.
|
||||||
|
|
||||||
|
### Option 3: Better State Synchronization
|
||||||
|
Make sure window resizing only happens when we're 100% sure the content has updated.
|
||||||
|
|
||||||
|
### Option 4: Use ContentView's dynamicNotchSize Instead
|
||||||
|
The ContentView already has `dynamicNotchSize` logic. Maybe the window should use that instead of having its own calculation.
|
||||||
|
|
||||||
|
## Recommended Approach
|
||||||
|
|
||||||
|
**Option 2** seems safest: Remove dynamic window resizing entirely and let the content handle sizing.
|
||||||
|
|
||||||
|
The ContentView already has:
|
||||||
|
```swift
|
||||||
|
var dynamicNotchSize: CGSize {
|
||||||
|
// calculation logic
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
And uses it in:
|
||||||
|
```swift
|
||||||
|
.frame(maxWidth: dynamicNotchSize.width, maxHeight: dynamicNotchSize.height)
|
||||||
|
```
|
||||||
|
|
||||||
|
This means the content already knows how to size itself. The window can stay at the maximum possible size, and the content will center itself within that space.
|
||||||
|
|
||||||
|
## Test Plan
|
||||||
|
|
||||||
|
1. Remove all dynamic window resizing logic
|
||||||
|
2. Set window to maximum possible size (openNotchSize.width + max extra width)
|
||||||
|
3. Let ContentView handle all dynamic sizing internally
|
||||||
|
4. Test alignment stays perfect
|
||||||
|
|
||||||
|
This eliminates the timing issue completely because the window never changes size after initial creation.
|
||||||
|
|
||||||
|
## REAL ROOT CAUSE FOUND AND FIXED
|
||||||
|
|
||||||
|
### The ACTUAL Problem: Window Creation with Wrong Size
|
||||||
|
|
||||||
|
The issue was NOT positioning logic - it was that **`createDynamicIslandWindow()` always created windows with `openNotchSize`** instead of the current required size!
|
||||||
|
|
||||||
|
#### When This Caused Problems:
|
||||||
|
|
||||||
|
1. **"Show on all displays" toggle**: Creates new window with default `openNotchSize`, losing dynamic sizing
|
||||||
|
2. **Screen changes**: `adjustWindowPosition()` creates new windows with wrong size
|
||||||
|
3. **App restarts/other events**: Any time a new window is created, it ignores current dynamic sizing needs
|
||||||
|
|
||||||
|
#### The Wrong Code:
|
||||||
|
```swift
|
||||||
|
// OLD - ALWAYS used openNotchSize
|
||||||
|
let window = DynamicIslandWindow(
|
||||||
|
contentRect: NSRect(x: 0, y: 0, width: openNotchSize.width, height: openNotchSize.height),
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
#### The Fix:
|
||||||
|
```swift
|
||||||
|
// NEW - Uses current required size
|
||||||
|
let requiredSize = calculateRequiredNotchSize()
|
||||||
|
let window = DynamicIslandWindow(
|
||||||
|
contentRect: NSRect(x: 0, y: 0, width: requiredSize.width, height: requiredSize.height),
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Why This Fixes Everything:
|
||||||
|
✅ **"Show on all displays" toggle**: New windows created with correct size
|
||||||
|
✅ **Tab switching**: No more size mismatch when repositioning
|
||||||
|
✅ **Screen changes**: New windows respect current graph settings
|
||||||
|
✅ **Consistent behavior**: All window creation uses same sizing logic
|
||||||
|
|
||||||
|
### The Real Issue Was:
|
||||||
|
- Windows created with wrong size, then positioned correctly = **misalignment**
|
||||||
|
- Content expecting larger window, but window is small = **positioning errors**
|
||||||
|
- Dynamic sizing only worked if you never triggered window recreation
|
||||||
|
|
||||||
|
### Result:
|
||||||
|
Perfect alignment in ALL scenarios - no more window size/content size mismatches!
|
||||||
Loading…
Add table
Add a link
Reference in a new issue