|
| 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. Checkpoint — Ensure 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.1–3.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` |
0 commit comments