Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
45 changes: 45 additions & 0 deletions platform.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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{}
66 changes: 66 additions & 0 deletions platform_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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{}
Expand Down Expand Up @@ -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)
}
})
}
}
Loading