diff --git a/CHANGELOG.md b/CHANGELOG.md index b87ae60..9d20c2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ 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.22.0] - 2026-07-29 + +### Added + +- **`FontSmoothing` type** (gogpu#396, ADR-057) — three-state enum (`None`, `Grayscale`, `Subpixel`) for OS text anti-aliasing mode. Separate from `SubpixelLayout` — answers "how is text AA'd?" vs "what is the display's pixel arrangement?". `PlatformProvider.FontSmoothing()` method added. `NullPlatformProvider` defaults to `FontSmoothingGrayscale`. `String()` method for debugging. +- **Coordinate space documentation** (gogpu#398) — explicit "logical DIP, do NOT divide by ScaleFactor" godoc on `PointerEvent.X/Y`, `ScrollEvent.X/Y`, and all `OnMouse*` callbacks. + ## [0.21.1] - 2026-07-12 ### Changed diff --git a/platform.go b/platform.go index 509c548..2d43761 100644 --- a/platform.go +++ b/platform.go @@ -56,6 +56,12 @@ type PlatformProvider interface { // Returns SubpixelNone when subpixel info is unavailable or on HiDPI displays // where subpixels are too small to be visible. SubpixelLayout() SubpixelLayout + + // FontSmoothing returns the OS text anti-aliasing mode. + // Used by text rendering pipelines to select between aliased, grayscale, and + // subpixel anti-aliasing. The result reflects the system-wide user preference + // (ClearType on Windows, font smoothing on macOS/Linux). + FontSmoothing() FontSmoothing } // SubpixelLayout describes the physical arrangement of RGB subpixels on a display. @@ -101,6 +107,42 @@ func (s SubpixelLayout) String() string { } } +// FontSmoothing describes the OS text anti-aliasing mode. +// This is distinct from SubpixelLayout: FontSmoothing tells you HOW the text +// is anti-aliased (aliased / grayscale / subpixel), while SubpixelLayout tells +// you the physical subpixel arrangement (RGB / BGR / etc.) when subpixel AA +// is active. +// +// Detection sources by platform: +// - Windows: SPI_GETFONTSMOOTHING + SPI_GETFONTSMOOTHINGTYPE +// - macOS: always Grayscale (Mojave+ disabled subpixel AA system-wide) +// - Linux: Xft.antialias + Xft.rgba from RESOURCE_MANAGER / fontconfig +// - Browser: always Grayscale (browser controls text rendering) +type FontSmoothing int + +const ( + // FontSmoothingNone means smoothing is disabled — render aliased (bitmap-style) text. + FontSmoothingNone FontSmoothing = iota + // FontSmoothingGrayscale means grayscale anti-aliasing (no subpixel exploitation). + FontSmoothingGrayscale + // FontSmoothingSubpixel means LCD subpixel anti-aliasing (ClearType on Windows). + FontSmoothingSubpixel +) + +// String returns the font smoothing mode name for debugging. +func (f FontSmoothing) String() string { + switch f { + case FontSmoothingNone: + return stringNone + case FontSmoothingGrayscale: + return "Grayscale" + case FontSmoothingSubpixel: + return "Subpixel" + default: + return "Unknown" + } +} + // CursorShape represents the mouse cursor shape. // // These values cover the most common cursor shapes across platforms @@ -257,5 +299,8 @@ func (NullPlatformProvider) FontScale() float32 { return 1.0 } // SubpixelLayout returns SubpixelNone (grayscale AA). func (NullPlatformProvider) SubpixelLayout() SubpixelLayout { return SubpixelNone } +// FontSmoothing returns FontSmoothingGrayscale (safe default). +func (NullPlatformProvider) FontSmoothing() FontSmoothing { return FontSmoothingGrayscale } + // Ensure NullPlatformProvider implements PlatformProvider. var _ PlatformProvider = NullPlatformProvider{} diff --git a/platform_test.go b/platform_test.go index 75f7272..eff85ea 100644 --- a/platform_test.go +++ b/platform_test.go @@ -55,6 +55,12 @@ func TestNullPlatformProvider_Defaults(t *testing.T) { if got := pp.FontScale(); got != 1.0 { t.Errorf("FontScale() = %f, want 1.0", got) } + if got := pp.SubpixelLayout(); got != SubpixelNone { + t.Errorf("SubpixelLayout() = %v, want SubpixelNone", got) + } + if got := pp.FontSmoothing(); got != FontSmoothingGrayscale { + t.Errorf("FontSmoothing() = %v, want FontSmoothingGrayscale", got) + } } func TestCursorShape_String(t *testing.T) { @@ -147,6 +153,7 @@ func (m *mockPlatformProvider) ReduceMotion() bool { return m.reduce func (m *mockPlatformProvider) HighContrast() bool { return m.highContrast } func (m *mockPlatformProvider) FontScale() float32 { return m.fontScale } func (m *mockPlatformProvider) SubpixelLayout() SubpixelLayout { return SubpixelRGB } +func (m *mockPlatformProvider) FontSmoothing() FontSmoothing { return FontSmoothingSubpixel } // Ensure mockPlatformProvider implements PlatformProvider. var _ PlatformProvider = &mockPlatformProvider{} @@ -192,4 +199,63 @@ func TestPlatformProvider_CustomImplementation(t *testing.T) { if got := pp.FontScale(); got != 1.5 { t.Errorf("FontScale() = %f, want 1.5", got) } + + // Test font smoothing delegation + if got := pp.FontSmoothing(); got != FontSmoothingSubpixel { + t.Errorf("FontSmoothing() = %v, want FontSmoothingSubpixel", got) + } +} + +func TestFontSmoothing_String(t *testing.T) { + tests := []struct { + fs FontSmoothing + want string + }{ + {FontSmoothingNone, "None"}, + {FontSmoothingGrayscale, "Grayscale"}, + {FontSmoothingSubpixel, "Subpixel"}, + {FontSmoothing(99), "Unknown"}, + } + + for _, tt := range tests { + t.Run(tt.want, func(t *testing.T) { + if got := tt.fs.String(); got != tt.want { + t.Errorf("FontSmoothing(%d).String() = %q, want %q", tt.fs, got, tt.want) + } + }) + } +} + +func TestFontSmoothing_Values(t *testing.T) { + if FontSmoothingNone != 0 { + t.Errorf("FontSmoothingNone = %d, want 0", FontSmoothingNone) + } + if FontSmoothingGrayscale != 1 { + t.Errorf("FontSmoothingGrayscale = %d, want 1", FontSmoothingGrayscale) + } + if FontSmoothingSubpixel != 2 { + t.Errorf("FontSmoothingSubpixel = %d, want 2", FontSmoothingSubpixel) + } +} + +func TestSubpixelLayout_String(t *testing.T) { + tests := []struct { + sl SubpixelLayout + want string + }{ + {SubpixelNone, "None"}, + {SubpixelRGB, "RGB"}, + {SubpixelBGR, "BGR"}, + {SubpixelVRGB, "VRGB"}, + {SubpixelVBGR, "VBGR"}, + {SubpixelLayout(99), "Unknown"}, + } + + for _, tt := range tests { + t.Run(tt.want, func(t *testing.T) { + if got := tt.sl.String(); got != tt.want { + t.Errorf("SubpixelLayout(%d).String() = %q, want %q", tt.sl, got, tt.want) + } + }) + } }