From 4cccd8ad1485ef1be39ddbb767ebaa564452910f Mon Sep 17 00:00:00 2001 From: Andy Date: Thu, 19 Mar 2026 13:58:30 +0300 Subject: [PATCH 1/2] feat: add WindowChrome interface for frameless window support Add WindowChrome interface with HitTestResult enum (13 values), HitTestCallback type, and NullWindowChrome for testing. Enables custom window chrome with drag, resize, and button regions. --- doc.go | 1 + window_chrome.go | 184 ++++++++++++++++++++++++++++++++++++++++++ window_chrome_test.go | 184 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 369 insertions(+) create mode 100644 window_chrome.go create mode 100644 window_chrome_test.go diff --git a/doc.go b/doc.go index baad92e..5c3e32c 100644 --- a/doc.go +++ b/doc.go @@ -9,6 +9,7 @@ // - WindowProvider: Interface for window geometry, DPI, and redraw requests // - PlatformProvider: Interface for clipboard, cursor, dark mode, accessibility // - ScrollEventSource: Interface for detailed scroll events +// - WindowChrome: Interface for custom window chrome (frameless windows) // - Texture: Minimal interface for GPU textures // - TextureDrawer: Interface for drawing textures (2D rendering) // - TextureCreator: Interface for creating textures from pixel data diff --git a/window_chrome.go b/window_chrome.go new file mode 100644 index 0000000..634e7f9 --- /dev/null +++ b/window_chrome.go @@ -0,0 +1,184 @@ +// Copyright 2026 The gogpu Authors +// SPDX-License-Identifier: MIT + +package gpucontext + +// HitTestResult represents what region of the window the cursor is over. +// +// When a window is frameless (no OS title bar), the application must tell +// the OS what part of the window the cursor is in, so the OS can handle +// dragging, resizing, and window button interactions. +// +// These values map directly to platform-specific hit test constants: +// - Windows: WM_NCHITTEST return values (HTCLIENT, HTCAPTION, etc.) +// - macOS: NSWindow regions +// - Linux: xdg-toplevel resize edges +type HitTestResult int + +const ( + // HitTestClient indicates the cursor is over the client (content) area. + // The application handles all input normally. + HitTestClient HitTestResult = iota + + // HitTestCaption indicates the cursor is over the title bar / drag area. + // The OS handles window dragging on mouse down. + HitTestCaption + + // HitTestClose indicates the cursor is over the close button region. + HitTestClose + + // HitTestMaximize indicates the cursor is over the maximize button region. + HitTestMaximize + + // HitTestMinimize indicates the cursor is over the minimize button region. + HitTestMinimize + + // HitTestResizeN indicates the cursor is over the top resize edge. + HitTestResizeN + + // HitTestResizeS indicates the cursor is over the bottom resize edge. + HitTestResizeS + + // HitTestResizeW indicates the cursor is over the left resize edge. + HitTestResizeW + + // HitTestResizeE indicates the cursor is over the right resize edge. + HitTestResizeE + + // HitTestResizeNW indicates the cursor is over the top-left resize corner. + HitTestResizeNW + + // HitTestResizeNE indicates the cursor is over the top-right resize corner. + HitTestResizeNE + + // HitTestResizeSW indicates the cursor is over the bottom-left resize corner. + HitTestResizeSW + + // HitTestResizeSE indicates the cursor is over the bottom-right resize corner. + HitTestResizeSE +) + +// String returns the hit test result name for debugging. +func (h HitTestResult) String() string { + switch h { + case HitTestClient: + return "Client" + case HitTestCaption: + return "Caption" + case HitTestClose: + return "Close" + case HitTestMaximize: + return "Maximize" + case HitTestMinimize: + return "Minimize" + case HitTestResizeN: + return "ResizeN" + case HitTestResizeS: + return "ResizeS" + case HitTestResizeW: + return "ResizeW" + case HitTestResizeE: + return "ResizeE" + case HitTestResizeNW: + return "ResizeNW" + case HitTestResizeNE: + return "ResizeNE" + case HitTestResizeSW: + return "ResizeSW" + case HitTestResizeSE: + return "ResizeSE" + default: + return "Unknown" + } +} + +// HitTestCallback is called by the platform layer to determine what region +// of the window the cursor is in. The coordinates (x, y) are in logical +// points (DIP) relative to the window's top-left corner. +// +// The callback is invoked during mouse move events when the window is frameless. +// It must return quickly to avoid input lag. +type HitTestCallback func(x, y float64) HitTestResult + +// WindowChrome provides control over window chrome (title bar, borders). +// +// This interface enables replacing the OS window chrome with a custom +// GPU-rendered title bar. When frameless mode is enabled, the OS removes +// the title bar and borders, and the application takes responsibility for: +// - Rendering its own title bar +// - Providing hit-test regions (drag area, buttons, resize edges) +// - Handling minimize/maximize/close actions +// +// Implementations: +// - gogpu.App implements WindowChrome via platform-specific code +// - NullWindowChrome provides no-op defaults for testing +// +// WindowChrome is optional. Use type assertion to check availability: +// +// if wc, ok := provider.(gpucontext.WindowChrome); ok { +// wc.SetFrameless(true) +// wc.SetHitTestCallback(myHitTest) +// } +type WindowChrome interface { + // SetFrameless enables or disables frameless (borderless) window mode. + // When true, the OS title bar and borders are removed. + // The application must provide its own title bar via SetHitTestCallback. + SetFrameless(frameless bool) + + // IsFrameless returns true if the window is in frameless mode. + IsFrameless() bool + + // SetHitTestCallback sets the callback that determines what region + // of the window the cursor is over. This is used by the platform layer + // to route mouse events to the OS for dragging, resizing, etc. + // + // Pass nil to clear the callback (all areas become HitTestClient). + SetHitTestCallback(callback HitTestCallback) + + // Minimize minimizes the window to the taskbar/dock. + Minimize() + + // Maximize toggles between maximized and restored window state. + // If the window is currently maximized, it is restored to its previous size. + Maximize() + + // IsMaximized returns true if the window is currently maximized. + IsMaximized() bool + + // Close requests the window to close. + // This triggers the normal close flow (close events, cleanup). + Close() +} + +// NullWindowChrome implements WindowChrome with no-op behavior. +// Used for testing and platforms without window chrome support. +// +// Default return values: +// - IsFrameless: false +// - IsMaximized: false +// - All actions: no-op +type NullWindowChrome struct{} + +// SetFrameless does nothing. +func (NullWindowChrome) SetFrameless(bool) {} + +// IsFrameless returns false. +func (NullWindowChrome) IsFrameless() bool { return false } + +// SetHitTestCallback does nothing. +func (NullWindowChrome) SetHitTestCallback(HitTestCallback) {} + +// Minimize does nothing. +func (NullWindowChrome) Minimize() {} + +// Maximize does nothing. +func (NullWindowChrome) Maximize() {} + +// IsMaximized returns false. +func (NullWindowChrome) IsMaximized() bool { return false } + +// Close does nothing. +func (NullWindowChrome) Close() {} + +// Ensure NullWindowChrome implements WindowChrome. +var _ WindowChrome = NullWindowChrome{} diff --git a/window_chrome_test.go b/window_chrome_test.go new file mode 100644 index 0000000..d9b6470 --- /dev/null +++ b/window_chrome_test.go @@ -0,0 +1,184 @@ +// Copyright 2026 The gogpu Authors +// SPDX-License-Identifier: MIT + +package gpucontext + +import "testing" + +func TestNullWindowChrome_Defaults(t *testing.T) { + var wc WindowChrome = NullWindowChrome{} + + if wc.IsFrameless() { + t.Error("IsFrameless() should return false") + } + if wc.IsMaximized() { + t.Error("IsMaximized() should return false") + } +} + +func TestNullWindowChrome_Actions(t *testing.T) { + var wc WindowChrome = NullWindowChrome{} + + // All actions should succeed without panic + wc.SetFrameless(true) + wc.SetFrameless(false) + wc.SetHitTestCallback(func(x, y float64) HitTestResult { return HitTestClient }) + wc.SetHitTestCallback(nil) + wc.Minimize() + wc.Maximize() + wc.Close() +} + +func TestHitTestResult_String(t *testing.T) { + tests := []struct { + result HitTestResult + want string + }{ + {HitTestClient, "Client"}, + {HitTestCaption, "Caption"}, + {HitTestClose, "Close"}, + {HitTestMaximize, "Maximize"}, + {HitTestMinimize, "Minimize"}, + {HitTestResizeN, "ResizeN"}, + {HitTestResizeS, "ResizeS"}, + {HitTestResizeW, "ResizeW"}, + {HitTestResizeE, "ResizeE"}, + {HitTestResizeNW, "ResizeNW"}, + {HitTestResizeNE, "ResizeNE"}, + {HitTestResizeSW, "ResizeSW"}, + {HitTestResizeSE, "ResizeSE"}, + {HitTestResult(99), "Unknown"}, + } + + for _, tt := range tests { + t.Run(tt.want, func(t *testing.T) { + if got := tt.result.String(); got != tt.want { + t.Errorf("HitTestResult(%d).String() = %q, want %q", tt.result, got, tt.want) + } + }) + } +} + +func TestHitTestResult_Values(t *testing.T) { + // Verify hit test result constants are sequential starting from 0 + if HitTestClient != 0 { + t.Errorf("HitTestClient = %d, want 0", HitTestClient) + } + if HitTestCaption != 1 { + t.Errorf("HitTestCaption = %d, want 1", HitTestCaption) + } + if HitTestClose != 2 { + t.Errorf("HitTestClose = %d, want 2", HitTestClose) + } + if HitTestMaximize != 3 { + t.Errorf("HitTestMaximize = %d, want 3", HitTestMaximize) + } + if HitTestMinimize != 4 { + t.Errorf("HitTestMinimize = %d, want 4", HitTestMinimize) + } + if HitTestResizeN != 5 { + t.Errorf("HitTestResizeN = %d, want 5", HitTestResizeN) + } + if HitTestResizeS != 6 { + t.Errorf("HitTestResizeS = %d, want 6", HitTestResizeS) + } + if HitTestResizeW != 7 { + t.Errorf("HitTestResizeW = %d, want 7", HitTestResizeW) + } + if HitTestResizeE != 8 { + t.Errorf("HitTestResizeE = %d, want 8", HitTestResizeE) + } + if HitTestResizeNW != 9 { + t.Errorf("HitTestResizeNW = %d, want 9", HitTestResizeNW) + } + if HitTestResizeNE != 10 { + t.Errorf("HitTestResizeNE = %d, want 10", HitTestResizeNE) + } + if HitTestResizeSW != 11 { + t.Errorf("HitTestResizeSW = %d, want 11", HitTestResizeSW) + } + if HitTestResizeSE != 12 { + t.Errorf("HitTestResizeSE = %d, want 12", HitTestResizeSE) + } +} + +// mockWindowChrome verifies the interface can be implemented by custom types. +type mockWindowChrome struct { + frameless bool + maximized bool + closed bool + minimized bool + callback HitTestCallback +} + +func (m *mockWindowChrome) SetFrameless(frameless bool) { m.frameless = frameless } +func (m *mockWindowChrome) IsFrameless() bool { return m.frameless } +func (m *mockWindowChrome) SetHitTestCallback(cb HitTestCallback) { m.callback = cb } +func (m *mockWindowChrome) Minimize() { m.minimized = true } +func (m *mockWindowChrome) Maximize() { m.maximized = !m.maximized } +func (m *mockWindowChrome) IsMaximized() bool { return m.maximized } +func (m *mockWindowChrome) Close() { m.closed = true } + +// Ensure mockWindowChrome implements WindowChrome. +var _ WindowChrome = &mockWindowChrome{} + +func TestWindowChrome_CustomImplementation(t *testing.T) { + mock := &mockWindowChrome{} + var wc WindowChrome = mock + + // Test frameless mode + wc.SetFrameless(true) + if !wc.IsFrameless() { + t.Error("IsFrameless() should return true after SetFrameless(true)") + } + wc.SetFrameless(false) + if wc.IsFrameless() { + t.Error("IsFrameless() should return false after SetFrameless(false)") + } + + // Test hit test callback + called := false + wc.SetHitTestCallback(func(x, y float64) HitTestResult { + called = true + if x > 100 { + return HitTestClient + } + return HitTestCaption + }) + if mock.callback == nil { + t.Fatal("callback should be set") + } + result := mock.callback(50, 10) + if !called { + t.Error("callback should have been called") + } + if result != HitTestCaption { + t.Errorf("callback(50, 10) = %v, want Caption", result) + } + result = mock.callback(150, 10) + if result != HitTestClient { + t.Errorf("callback(150, 10) = %v, want Client", result) + } + + // Test minimize + wc.Minimize() + if !mock.minimized { + t.Error("Minimize() should set minimized to true") + } + + // Test maximize toggle + wc.Maximize() + if !wc.IsMaximized() { + t.Error("IsMaximized() should return true after first Maximize()") + } + wc.Maximize() + if wc.IsMaximized() { + t.Error("IsMaximized() should return false after second Maximize()") + } + + // Test close + wc.Close() + if !mock.closed { + t.Error("Close() should set closed to true") + } +} From 2fd519fc180884768823fa29e5985c67734f9ea9 Mon Sep 17 00:00:00 2001 From: Andy Date: Fri, 20 Mar 2026 11:17:17 +0300 Subject: [PATCH 2/2] docs: update CHANGELOG and README for v0.11.0 WindowChrome release Add WindowChrome interface, HitTestResult enum, HitTestCallback, NullWindowChrome to CHANGELOG. Add WindowChrome to README features and usage section with frameless window example. --- CHANGELOG.md | 24 ++++++++++++++++++++++++ README.md | 30 ++++++++++++++++++++++++++++-- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 568cc77..8e0eae2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.11.0] - 2026-03-20 + +### Added + +- **WindowChrome interface** for custom window chrome (frameless windows) + - `SetFrameless(bool)` / `IsFrameless() bool` — enable/disable frameless mode + - `SetHitTestCallback(HitTestCallback)` — custom hit testing for drag, resize, buttons + - `Minimize()` / `Maximize()` / `IsMaximized() bool` / `Close()` — window controls + - Optional interface — use type assertion: + `if wc, ok := provider.(gpucontext.WindowChrome); ok { ... }` + +- **HitTestResult enum** (13 values) for custom window regions + - `HitTestClient` — normal content area + - `HitTestCaption` — title bar drag area + - `HitTestClose` / `HitTestMaximize` / `HitTestMinimize` — window buttons + - `HitTestResizeN/S/W/E/NW/NE/SW/SE` — 8 resize edges/corners + - `String()` method for debugging + +- **HitTestCallback type** — `func(x, y float64) HitTestResult` + +- **NullWindowChrome** — no-op implementation for testing + ## [0.10.0] - 2026-03-15 ### Removed @@ -128,6 +150,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **TouchCancelled → TouchCanceled** — US English spelling (misspell linter) - Removed unused `DeviceHandle` alias +[0.11.0]: https://github.com/gogpu/gpucontext/releases/tag/v0.11.0 +[0.10.0]: https://github.com/gogpu/gpucontext/releases/tag/v0.10.0 [0.9.0]: https://github.com/gogpu/gpucontext/releases/tag/v0.9.0 [0.8.0]: https://github.com/gogpu/gpucontext/releases/tag/v0.8.0 [0.7.0]: https://github.com/gogpu/gpucontext/releases/tag/v0.7.0 diff --git a/README.md b/README.md index 05df2bc..06cd576 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Shared GPU infrastructure for the [gogpu](https://github.com/gogpu) ecosystem. | Package | Purpose | Dependencies | |---------|---------|--------------| | [gputypes](https://github.com/gogpu/gputypes) | WebGPU types (enums, structs, constants) | **ZERO** | -| **gpucontext** | Interfaces (DeviceProvider, EventSource, Texture) | imports gputypes | +| **gpucontext** | Interfaces (DeviceProvider, EventSource, WindowChrome, Texture) | imports gputypes | gpucontext imports gputypes to use shared types in interface signatures, ensuring type compatibility across the ecosystem. @@ -34,6 +34,7 @@ go get github.com/gogpu/gpucontext - **ScrollEventSource** — Scroll/wheel events with pixel/line/page modes - **Texture** — Minimal interface for GPU textures with TextureUpdater/TextureDrawer/TextureCreator - **IME Support** — Input Method Editor for CJK languages (Chinese, Japanese, Korean) +- **WindowChrome** — Custom window chrome for frameless windows (hit testing, minimize/maximize/close) - **Registry[T]** — Generic registry with priority-based backend selection - **WebGPU Interfaces** — Device, Queue, Adapter, Surface interfaces - **WebGPU Types** — Re-exports from [gputypes](https://github.com/gogpu/gputypes) (TextureFormat, etc.) @@ -247,6 +248,31 @@ func (ctx *Context) DrawTexture(tex gpucontext.Texture, x, y float32) error { } ``` +### WindowChrome (frameless windows) + +`WindowChrome` enables custom window chrome for frameless windows with custom title bars: + +```go +// In gogpu/ui - custom title bar with hit testing +func (ui *UI) SetupFramelessWindow(provider gpucontext.WindowProvider) { + if wc, ok := provider.(gpucontext.WindowChrome); ok { + wc.SetFrameless(true) + wc.SetHitTestCallback(func(x, y float64) gpucontext.HitTestResult { + if y < 40 { // title bar height + return gpucontext.HitTestCaption // enables window dragging + } + return gpucontext.HitTestClient + }) + } +} + +// Window controls +wc.Minimize() +wc.Maximize() // toggles maximized/restored +wc.IsMaximized() // for button icon state +wc.Close() +``` + ### Backend Registry The `Registry[T]` provides thread-safe registration with priority-based selection: @@ -289,7 +315,7 @@ names := backends.Available() // ["vulkan", "software"] ▼ gpucontext (imports gputypes) - DeviceProvider, + DeviceProvider, WindowChrome, WindowProvider, PlatformProvider, EventSource, Texture, Registry │