Skip to content

Commit 0297a33

Browse files
author
Ricardo Wagemaker
committed
improved MacOS app edges
1 parent 84b5f99 commit 0297a33

10 files changed

Lines changed: 1230 additions & 34 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"specId": "92f0652f-664e-4ea6-907a-577d0e9e4115", "workflowType": "requirements-first", "specType": "bugfix"}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Bugfix Requirements Document
2+
3+
## Introduction
4+
5+
On macOS, the WeatherWidget applies transparency using `NSWindow.setAlphaValue()`, which sets opacity at the window level. This causes all window contents — background, text labels, and weather icons — to fade together, making the widget unreadable at lower opacity levels. The fix must isolate transparency to the background layer only, leaving text and icons at full opacity. A secondary issue is that the widget window has square corners, which is inconsistent with macOS design conventions.
6+
7+
## Bug Analysis
8+
9+
### Current Behavior (Defect)
10+
11+
1.1 WHEN the user sets widget transparency to any value below 100% THEN the system applies `NSWindow.setAlphaValue()`, making the entire window — background, text, and icons — uniformly transparent.
12+
13+
1.2 WHEN the user sets transparency to 25% THEN the system renders all UI elements (temperature, city name, weather icon) at 25% opacity, making the widget content barely visible and unreadable.
14+
15+
1.3 WHEN the widget window is displayed on macOS THEN the system renders it with hard square corners, inconsistent with macOS visual conventions.
16+
17+
### Expected Behavior (Correct)
18+
19+
2.1 WHEN the user sets widget transparency to any value below 100% THEN the system SHALL apply transparency only to the window background layer, leaving text and icon elements at full (100%) opacity.
20+
21+
2.2 WHEN the user sets transparency to 25% THEN the system SHALL render the background at ~25% opacity while all text labels and weather icons remain fully opaque and readable.
22+
23+
2.3 WHEN the widget window is displayed on macOS THEN the system SHALL render it with rounded corners (radius ≈ 12pt) matching macOS design conventions.
24+
25+
### Unchanged Behavior (Regression Prevention)
26+
27+
3.1 WHEN transparency is set to 100% (fully opaque) THEN the system SHALL CONTINUE TO display the widget with a fully opaque dark background and fully visible content.
28+
29+
3.2 WHEN the user changes the opacity setting THEN the system SHALL CONTINUE TO immediately reflect the new transparency level without requiring an app restart.
30+
31+
3.3 WHEN the widget is displayed on Windows THEN the system SHALL CONTINUE TO use the existing Win32 color-key transparency mechanism, unaffected by this change.
32+
33+
3.4 WHEN the widget is displayed on Linux THEN the system SHALL CONTINUE TO use the existing `_NET_WM_WINDOW_OPACITY` transparency mechanism, unaffected by this change.
34+
35+
3.5 WHEN the widget window is moved, resized, or repositioned on macOS THEN the system SHALL CONTINUE TO function correctly (drag, corner snapping, position persistence).
36+
37+
---
38+
39+
## Bug Condition Pseudocode
40+
41+
**Bug Condition Function** — identifies inputs that trigger the bug:
42+
43+
```pascal
44+
FUNCTION isBugCondition(X)
45+
INPUT: X of type OpacityInput { platform: string, opacityPercent: int }
46+
OUTPUT: boolean
47+
48+
// Bug occurs when running on macOS AND transparency is active
49+
RETURN X.platform = "darwin" AND X.opacityPercent < 100
50+
END FUNCTION
51+
```
52+
53+
**Property Specification** — defines correct behavior for buggy inputs:
54+
55+
```pascal
56+
// Property: Fix Checking — Background-Only Transparency
57+
FOR ALL X WHERE isBugCondition(X) DO
58+
result ← setWindowOpacity'(X)
59+
ASSERT backgroundAlpha(result) ≈ X.opacityPercent / 100
60+
ASSERT contentAlpha(result) = 1.0 // text and icons fully opaque
61+
ASSERT windowAlphaValue(result) = 1.0 // NSWindow.alphaValue is NOT used
62+
END FOR
63+
64+
// Property: Fix Checking — Rounded Corners
65+
FOR ALL W WHERE platform(W) = "darwin" DO
66+
ASSERT cornerRadius(contentView(W)) ≥ 12.0
67+
ASSERT contentView(W).wantsLayer = true
68+
END FOR
69+
```
70+
71+
**Preservation Goal:**
72+
73+
```pascal
74+
// Property: Preservation Checking
75+
FOR ALL X WHERE NOT isBugCondition(X) DO
76+
ASSERT setWindowOpacity(X) = setWindowOpacity'(X)
77+
END FOR
78+
```

.kiro/specs/macos-visual-fix/design.md

Lines changed: 333 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
# Implementation Plan
2+
3+
## Overview
4+
5+
Fix two macOS-only visual defects: (1) replace `NSWindow.setAlphaValue()` with a `WWidgetBackgroundView` NSView subclass so transparency applies only to the background layer, and (2) configure `contentView.layer.cornerRadius = 12` for rounded corners. All changes are confined to darwin build-tagged files.
6+
7+
## Tasks
8+
9+
- [x] 1. Write bug condition exploration test
10+
- **Property 1: Bug Condition** - Whole-Window Transparency & Square Corners
11+
- **CRITICAL**: This test MUST FAIL on unfixed code — failure confirms the bug exists
12+
- **DO NOT attempt to fix the test or the code when it fails**
13+
- **NOTE**: This test encodes the expected behavior — it will validate the fix when it passes after implementation
14+
- **GOAL**: Surface counterexamples that demonstrate both bugs exist on unfixed code
15+
- **Scoped PBT Approach**: Scope to concrete failing cases: `opacityPercent ∈ {25, 50, 75}` (bug condition: darwin + opacity < 100)
16+
- Create `internal/ui/win32_darwin_test.go` (build tag `//go:build darwin`)
17+
- Test 1a — Whole-window alpha: call `setNSWindowAlpha(handle, 50)` on an off-screen NSWindow; assert `[w alphaValue] == 0.5` AND `[w contentView].layer.backgroundColor alpha == 1.0` — this demonstrates the entire window (not just background) is being composited at 50%, confirming Bug 1
18+
- Test 1b — Square corners: create widget window, inspect `contentView.layer`; assert `cornerRadius == 0` and `masksToBounds == NO` — confirms Bug 2
19+
- For the property-based variant: generate `opacityPercent ∈ [1, 99]`, for each assert that the FIXED behavior (backgroundAlpha ≈ opacityPercent/100, NSWindow.alphaValue == 1.0) does NOT hold on unfixed code
20+
- Run test on UNFIXED code
21+
- **EXPECTED OUTCOME**: Test FAILS (this is correct — it proves both bugs exist)
22+
- Document counterexamples found, e.g. `"setNSWindowAlpha(handle, 50) → [w alphaValue]=0.5; contentView.layer.cornerRadius=0"`
23+
- Mark task complete when test is written, run, and failure is documented
24+
- _Requirements: 1.1, 1.2, 1.3_
25+
26+
- [x] 2. Write preservation property tests (BEFORE implementing fix)
27+
- **Property 2: Preservation** - Non-Buggy Input Behavior
28+
- **IMPORTANT**: Follow observation-first methodology — observe UNFIXED code behavior for inputs where `isBugCondition` does NOT hold, then encode as property tests
29+
- Create `internal/ui/win32_darwin_test.go` (or extend it); create `internal/ui/theme_darwin_test.go`
30+
- **Observation phase (on unfixed code):**
31+
- Observe: `setWindowOpacity(100)` on darwin — `[w alphaValue] == 1.0`, dark background visible, content fully opaque
32+
- Observe: no darwin-specific code is invoked when `GOOS != darwin` (build-tag isolation)
33+
- Observe: drag/move calls (`moveNSWindowTo`) are unaffected by opacity changes
34+
- **Preservation property tests to write:**
35+
- Property 2a — 100% opacity: for `opacityPercent = 100`, assert `[w alphaValue] == 1.0` and background appears fully opaque dark (`layerAlpha == 1.0`) — must hold on BOTH unfixed and fixed code
36+
- Property 2b — Mouse passthrough: after `setupDarwinWindow` (once written), assert `WWidgetBackgroundView.hitTest:` returns `nil` for any point in its bounds — background does not capture mouse events
37+
- Property 2c — Window positioning unaffected: `moveNSWindowTo` still repositions the window correctly after the fix; frame origin matches expected values
38+
- Property 2d — Platform isolation: confirm the darwin code path is not compiled/called on non-darwin builds (build tag check — no direct assertion needed beyond compilation)
39+
- Write property-based test: for `opacityPercent ∈ [1, 100]`, for all values assert `[w alphaValue] == 1.0` (NSWindow alpha is never changed — this tests the preservation invariant)
40+
- Run tests on UNFIXED code
41+
- **EXPECTED OUTCOME**: Tests PASS (confirms baseline behaviors to preserve)
42+
- Mark task complete when tests are written, run, and passing on unfixed code
43+
- _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5_
44+
45+
- [x] 3. Fix macOS visual bugs (background-only transparency + rounded corners)
46+
47+
- [x] 3.1 Add `WWidgetBackgroundView` NSView subclass and new CGo functions in `internal/ui/win32_darwin.go`
48+
- Remove the `setNSWindowAlpha` Objective-C CGo function entirely
49+
- Add `WWidgetBackgroundView` NSView subclass in the CGo block:
50+
- `wantsLayer` returns `YES`
51+
- `acceptsFirstResponder` returns `NO`
52+
- `hitTest:withEvent:` returns `nil` so all mouse events pass through to the Fyne surface
53+
- Add `setupDarwinWindow(uintptr_t winHandle)` CGo function:
54+
- `[w setOpaque:NO]` and `[w setBackgroundColor:[NSColor clearColor]]`
55+
- `contentView.wantsLayer = YES`, `cornerRadius = 12.0`, `masksToBounds = YES`
56+
- Create and insert `WWidgetBackgroundView` as bottom-most subview of `contentView` with `NSViewWidthSizable | NSViewHeightSizable` autoresizing
57+
- Set initial layer background color `rgba(0.12, 0.12, 0.12, 1.0)`
58+
- Add `setDarwinBackgroundAlpha(uintptr_t winHandle, int opacityPercent)` CGo function:
59+
- Walk `contentView.subviews`, find the `WWidgetBackgroundView` instance
60+
- Set `sub.layer.backgroundColor` to `rgba(0.12, 0.12, 0.12, opacityPercent/100.0)`
61+
- Add Go wrapper `applyDarwinWindowSetup()` that calls `getNSWindowHandle()` + `C.setupDarwinWindow(handle)`; use a retry pattern (same as `moveWindow`) if handle is not yet available
62+
- Update Go `setWindowOpacity` function:
63+
- Remove `SetDarwinBackgroundShade(opacityPercent)` call
64+
- Remove `C.setNSWindowAlpha(handle, ...)` call
65+
- Call `C.setDarwinBackgroundAlpha(handle, C.int(opacityPercent))` instead
66+
- _Bug_Condition: isBugCondition(X) where X.platform = "darwin" AND X.opacityPercent < 100 (transparency bug); X.platform = "darwin" unconditionally (corners bug)_
67+
- _Expected_Behavior: backgroundAlpha(result) ≈ opacityPercent/100.0; contentAlpha(result) = 1.0; windowAlphaValue(result) = 1.0; cornerRadius(contentView) ≥ 12.0_
68+
- _Preservation: NSWindow.alphaValue is never set below 1.0; WWidgetBackgroundView.hitTest returns nil; moveNSWindowTo is unaffected; Windows/Linux code paths unchanged_
69+
- _Requirements: 2.1, 2.2, 2.3, 3.1, 3.2, 3.5_
70+
71+
- [x] 3.2 Update `internal/ui/platform_darwin.go` to call `applyDarwinWindowSetup`
72+
- In `initPlatformWindow`, call `applyDarwinWindowSetup()` after `registerDarwinWindow(w)`:
73+
```go
74+
func initPlatformWindow(w fyne.Window) {
75+
registerDarwinWindow(w)
76+
applyDarwinWindowSetup()
77+
}
78+
```
79+
- _Requirements: 2.1, 2.3_
80+
81+
- [x] 3.3 Update `internal/ui/theme.go` to return transparent background on macOS
82+
- In the darwin branch of `widgetTheme.Color`, change `ColorNameBackground` and `ColorNameOverlayBackground` to return `color.NRGBA{A: 0}` (fully transparent) so the Fyne GL surface paints nothing over the native `WWidgetBackgroundView`
83+
- Remove `SetDarwinBackgroundShade` function
84+
- Remove `darwinBgShade` atomic variable
85+
- Remove the `darwinBgShade.Store(30)` line from `init()`
86+
- _Bug_Condition: darwin theme returning opaque background color was masking the native background view_
87+
- _Expected_Behavior: theme.Color returns color.NRGBA{A:0} for ColorNameBackground on darwin_
88+
- _Preservation: Linux and Windows theme paths are unchanged; foreground/disabled/separator colors on darwin are unchanged_
89+
- _Requirements: 2.1, 3.3, 3.4_
90+
91+
- [x] 3.4 Verify bug condition exploration test now passes
92+
- **Property 1: Expected Behavior** - Whole-Window Transparency & Square Corners
93+
- **IMPORTANT**: Re-run the SAME test from task 1 — do NOT write a new test
94+
- The test from task 1 encodes the expected behavior (backgroundAlpha ≈ opacityPercent/100, NSWindow.alphaValue = 1.0, cornerRadius ≥ 12.0)
95+
- When this test passes, it confirms the expected behavior is satisfied for all darwin inputs where `isBugCondition` is true
96+
- Run bug condition exploration test from step 1
97+
- **EXPECTED OUTCOME**: Test PASSES (confirms both bugs are fixed)
98+
- _Requirements: 2.1, 2.2, 2.3_
99+
100+
- [x] 3.5 Verify preservation tests still pass
101+
- **Property 2: Preservation** - Non-Buggy Input Behavior
102+
- **IMPORTANT**: Re-run the SAME tests from task 2 — do NOT write new tests
103+
- Run all preservation property tests from step 2
104+
- **EXPECTED OUTCOME**: Tests PASS (confirms no regressions in 100% opacity, mouse passthrough, window positioning, and platform isolation)
105+
- Confirm all tests still pass after fix (no regressions)
106+
107+
- [x] 4. Write targeted unit tests for the new implementation
108+
- Create/extend `internal/ui/win32_darwin_test.go` with unit tests (all under `//go:build darwin`):
109+
- `TestSetupDarwinWindow_NonOpaque`: assert `[w isOpaque] == false` and `[w backgroundColor] == clearColor` after `setupDarwinWindow`
110+
- `TestSetupDarwinWindow_BgViewInserted`: assert `WWidgetBackgroundView` exists as the first (bottom-most) subview of `contentView`
111+
- `TestSetupDarwinWindow_CornerRadius`: assert `contentView.layer.cornerRadius >= 12.0` and `masksToBounds == YES`
112+
- `TestSetDarwinBackgroundAlpha_BoundaryValues`: call `setDarwinBackgroundAlpha` with `opacityPercent ∈ {1, 25, 50, 75, 99, 100}`; for each assert `layerAlpha(bgView) ≈ opacityPercent / 100.0` (within 0.001 tolerance)
113+
- `TestWWidgetBackgroundView_HitTestReturnsNil`: create a `WWidgetBackgroundView`, call `hitTest:` with a point inside its bounds, assert return value is `nil`
114+
- Create `internal/ui/theme_darwin_test.go` with:
115+
- `TestDarwinThemeBackground_Transparent`: assert `widgetTheme.Color(ColorNameBackground, VariantDark)` returns `color.NRGBA{A: 0}` on darwin
116+
- `TestDarwinThemeOverlayBackground_Transparent`: same assertion for `ColorNameOverlayBackground`
117+
- _Requirements: 2.1, 2.2, 2.3_
118+
119+
- [x] 5. CheckpointEnsure all tests pass
120+
- Run full darwin test suite: `go test ./internal/ui/... -tags darwin`
121+
- Ensure all property-based tests (Property 1 and Property 2) pass
122+
- Ensure all unit tests added in task 4 pass
123+
- Ensure no compilation errors on darwin build (`go build -o /dev/null ./...`)
124+
- Ensure no compilation errors on non-darwin builds (Windows/Linux) — darwin-specific code is gated by `//go:build darwin`
125+
- Ask the user if any questions arise or manual visual verification is needed
126+
127+
## Task Dependency Graph
128+
129+
```json
130+
{
131+
"waves": [
132+
{ "wave": 1, "tasks": ["1", "2"] },
133+
{ "wave": 2, "tasks": ["3.1"] },
134+
{ "wave": 3, "tasks": ["3.2", "3.3"] },
135+
{ "wave": 4, "tasks": ["3.4", "3.5"] },
136+
{ "wave": 5, "tasks": ["4"] },
137+
{ "wave": 6, "tasks": ["5"] }
138+
]
139+
}
140+
```
141+
142+
- Task 1 and Task 2 are independent and can be written in parallel (wave 1)
143+
- Task 3 depends on Tasks 1 and 2 being complete (counterexamples documented, baselines established)
144+
- Tasks 3.1, 3.2, 3.3 can be worked sequentially (each file change is independent but 3.1 must precede 3.2/3.3)
145+
- Tasks 3.4 and 3.5 depend on all of 3.13.3 being complete
146+
- Task 4 depends on 3.1 (functions must exist to unit-test)
147+
- Task 5 depends on all prior tasks
148+
149+
## Notes
150+
151+
- All new code is darwin-only: files must carry `//go:build darwin` tags; CGo functions must be in the existing CGo block in `win32_darwin.go`
152+
- Off-screen NSWindow creation for tests: use `NSWindow` with `NSWindowStyleMaskBorderless` and `NSBackingStoreNonretained` initialized to a zero-sized rect off-screen; no display connection is required for unit tests on macOS
153+
- `applyDarwinWindowSetup` should use the same retry-after-delay pattern as `moveWindow` (150 ms, 400 ms, 900 ms) because the NSWindow handle may not be populated until after `Show()` is called
154+
- `SetDarwinBackgroundShade` removal: grep for all call sites before deleting; the only call site is in the old `setWindowOpacity` function in `win32_darwin.go`
155+
- Property-based testing library: use `gopkg.in/check.v1` or `pgregory.net/rapid` (whichever is already in go.mod); if neither is present, add `pgregory.net/rapid` as it integrates cleanly with `testing.T`

internal/ui/platform_darwin.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ import "fyne.io/fyne/v2"
99
// retrieved later for positioning.
1010
func initPlatformWindow(w fyne.Window) {
1111
registerDarwinWindow(w)
12+
applyDarwinWindowSetup()
1213
}

0 commit comments

Comments
 (0)