From 241b00de4291cac514d15d2b0165358787a28998 Mon Sep 17 00:00:00 2001 From: Andrey Kolkov Date: Sat, 1 Aug 2026 15:56:12 +0300 Subject: [PATCH 1/2] feat: ScaleChangedEvent in InputEvent (ADR-059, #409) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Runtime DPI/scale factor change event. Emitted when window moves between monitors with different DPI, or OS DPI settings change. Carries ScaleFactor, Width, Height. Ordering: ScaleChangedEvent before ResizeEvent (winit pattern). Enterprise research: winit, Qt6, SDL3, Flutter — all emit separate DPI event. 2 new tests. --- input_event.go | 20 ++++++++++++++++++++ input_event_test.go | 31 ++++++++++++++++++++++++++++--- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/input_event.go b/input_event.go index 97d3d6c..0b5fad6 100644 --- a/input_event.go +++ b/input_event.go @@ -86,3 +86,23 @@ type ResizeEvent struct { } func (ResizeEvent) inputEventTag() {} + +// ScaleChangedEvent represents a DPI scale factor change. +// +// Emitted when a window moves between monitors with different DPI (e.g., +// Retina 2x → FullHD 1x), or when the OS DPI setting changes at runtime. +// +// ScaleFactor is the new display scale (1.0 = standard, 2.0 = Retina/HiDPI). +// Width and Height are the new logical size in DIP — may change during DPI +// transition because the OS adjusts the window to preserve visual size. +// +// Event ordering: ScaleChangedEvent is emitted BEFORE ResizeEvent (cause +// before effect, winit pattern). Consumers should update their rendering +// scale before processing the new dimensions. +type ScaleChangedEvent struct { + ScaleFactor float64 + Width int + Height int +} + +func (ScaleChangedEvent) inputEventTag() {} diff --git a/input_event_test.go b/input_event_test.go index 0c2ad91..fa9e214 100644 --- a/input_event_test.go +++ b/input_event_test.go @@ -13,13 +13,14 @@ func TestInputEventInterface(t *testing.T) { FocusEvent{Focused: true}, FocusEvent{Focused: false}, ResizeEvent{Width: 800, Height: 600}, + ScaleChangedEvent{ScaleFactor: 2.0, Width: 800, Height: 600}, PointerEvent{Type: PointerDown, X: 100, Y: 200}, PointerEvent{Type: PointerMove, X: 150, Y: 250}, ScrollEvent{DeltaY: -3.0, DeltaMode: ScrollDeltaLine}, } - if len(events) != 9 { - t.Fatalf("expected 9 events, got %d", len(events)) + if len(events) != 10 { + t.Fatalf("expected 10 events, got %d", len(events)) } for i, ev := range events { @@ -37,6 +38,7 @@ func TestInputEventTypeSwitch(t *testing.T) { CharEvent{Char: 'x'}, FocusEvent{Focused: true}, ResizeEvent{Width: 1024, Height: 768}, + ScaleChangedEvent{ScaleFactor: 2.0, Width: 800, Height: 600}, } counts := map[string]int{} @@ -54,12 +56,14 @@ func TestInputEventTypeSwitch(t *testing.T) { counts["focus"]++ case ResizeEvent: counts["resize"]++ + case ScaleChangedEvent: + counts["scale"]++ default: t.Errorf("unexpected event type: %T", ev) } } - for _, name := range []string{"key", "pointer", "scroll", "char", "focus", "resize"} { + for _, name := range []string{"key", "pointer", "scroll", "char", "focus", "resize", "scale"} { if counts[name] != 1 { t.Errorf("expected 1 %s event, got %d", name, counts[name]) } @@ -107,6 +111,27 @@ func TestResizeEventFields(t *testing.T) { } } +func TestScaleChangedEventFields(t *testing.T) { + ev := ScaleChangedEvent{ScaleFactor: 2.0, Width: 800, Height: 600} + if ev.ScaleFactor != 2.0 { + t.Errorf("ScaleFactor = %v, want 2.0", ev.ScaleFactor) + } + if ev.Width != 800 || ev.Height != 600 { + t.Errorf("got %dx%d, want 800x600", ev.Width, ev.Height) + } +} + +func TestScaleChangedEventImplementsInputEvent(t *testing.T) { + var ev InputEvent = ScaleChangedEvent{ScaleFactor: 1.5, Width: 1024, Height: 768} + se, ok := ev.(ScaleChangedEvent) + if !ok { + t.Fatal("ScaleChangedEvent does not implement InputEvent") + } + if se.ScaleFactor != 1.5 { + t.Errorf("ScaleFactor = %v, want 1.5", se.ScaleFactor) + } +} + func TestPointerEventImplementsInputEvent(t *testing.T) { var ev InputEvent = PointerEvent{Type: PointerDown, X: 50, Y: 100} pe, ok := ev.(PointerEvent) From 6b7c08d7b530bfa53d497deea68e1f5f86183877 Mon Sep 17 00:00:00 2001 From: Andrey Kolkov Date: Sat, 1 Aug 2026 16:17:53 +0300 Subject: [PATCH 2/2] =?UTF-8?q?docs:=20CHANGELOG=20v0.24.0=20=E2=80=94=20S?= =?UTF-8?q?caleChangedEvent=20(ADR-059)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c7219d2..b52ef77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.24.0] - 2026-08-01 + +### Added + +- **`ScaleChangedEvent`** (ADR-059, gogpu#409) — runtime DPI/scale factor change event. Emitted when window moves between monitors with different DPI or OS DPI settings change. Carries `ScaleFactor`, `Width`, `Height`. Event ordering: `ScaleChangedEvent` before `ResizeEvent` (cause before effect, winit pattern). Enterprise research: winit, Qt6, SDL3, Flutter — all emit separate DPI event. + ## [0.23.0] - 2026-07-30 ### Added