gui/pkg/screen/platform.go
Claude ab0722d19e
feat(screen): expand with QueryCurrent, ScreenPlacement, Rect geometry
- Platform.GetCurrent() for active screen query
- ScreenPlacement with Apply() for window positioning rules
- Rect geometry: Origin, Corner, InsideCorner, IsEmpty, Contains, RectSize, Intersect
- Point type, Alignment/OffsetReference constants
- 18 new tests with Good/Bad/Ugly coverage

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 14:54:17 +01:00

201 lines
5.9 KiB
Go

// pkg/screen/platform.go
package screen
// Platform abstracts the screen/display backend.
//
// core.WithService(screen.Register(wailsPlatform))
type Platform interface {
GetAll() []Screen
GetPrimary() *Screen
// GetCurrent returns the most recently active screen, or the primary if unset.
// current := platform.GetCurrent()
GetCurrent() *Screen
}
// Screen describes a display/monitor.
type Screen struct {
ID string `json:"id"`
Name string `json:"name"`
ScaleFactor float64 `json:"scaleFactor"`
Size Size `json:"size"`
Bounds Rect `json:"bounds"`
PhysicalBounds Rect `json:"physicalBounds"`
WorkArea Rect `json:"workArea"`
PhysicalWorkArea Rect `json:"physicalWorkArea"`
IsPrimary bool `json:"isPrimary"`
Rotation float64 `json:"rotation"`
}
// Rect represents a rectangle with position and dimensions.
//
// if bounds.Contains(Point{X: cursor.X, Y: cursor.Y}) { highlightWindow() }
type Rect struct {
X int `json:"x"`
Y int `json:"y"`
Width int `json:"width"`
Height int `json:"height"`
}
// Origin returns the top-left corner of the rectangle.
//
// pt := bounds.Origin() // Point{X: bounds.X, Y: bounds.Y}
func (r Rect) Origin() Point {
return Point{X: r.X, Y: r.Y}
}
// Corner returns the exclusive bottom-right corner (X+Width, Y+Height).
//
// end := bounds.Corner() // Point{X: bounds.X+bounds.Width, Y: bounds.Y+bounds.Height}
func (r Rect) Corner() Point {
return Point{X: r.X + r.Width, Y: r.Y + r.Height}
}
// InsideCorner returns the inclusive bottom-right corner (X+Width-1, Y+Height-1).
//
// last := bounds.InsideCorner()
func (r Rect) InsideCorner() Point {
return Point{X: r.X + r.Width - 1, Y: r.Y + r.Height - 1}
}
// IsEmpty reports whether the rectangle has non-positive area.
//
// if r.IsEmpty() { return }
func (r Rect) IsEmpty() bool {
return r.Width <= 0 || r.Height <= 0
}
// Contains reports whether point pt lies within the rectangle.
//
// if workArea.Contains(windowOrigin) { snapToScreen() }
func (r Rect) Contains(pt Point) bool {
return pt.X >= r.X && pt.X < r.X+r.Width && pt.Y >= r.Y && pt.Y < r.Y+r.Height
}
// RectSize returns the dimensions of the rectangle as a Size value.
//
// sz := bounds.RectSize() // Size{Width: bounds.Width, Height: bounds.Height}
func (r Rect) RectSize() Size {
return Size{Width: r.Width, Height: r.Height}
}
// Intersect returns the overlapping region of r and other, or an empty Rect if they do not overlap.
//
// overlap := a.Intersect(b)
// if !overlap.IsEmpty() { handleOverlap(overlap) }
func (r Rect) Intersect(other Rect) Rect {
if r.IsEmpty() || other.IsEmpty() {
return Rect{}
}
maxLeft := max(r.X, other.X)
maxTop := max(r.Y, other.Y)
minRight := min(r.X+r.Width, other.X+other.Width)
minBottom := min(r.Y+r.Height, other.Y+other.Height)
if minRight > maxLeft && minBottom > maxTop {
return Rect{X: maxLeft, Y: maxTop, Width: minRight - maxLeft, Height: minBottom - maxTop}
}
return Rect{}
}
// Point is a two-dimensional coordinate.
//
// centre := Point{X: bounds.X + bounds.Width/2, Y: bounds.Y + bounds.Height/2}
type Point struct {
X int `json:"x"`
Y int `json:"y"`
}
// Size represents dimensions.
type Size struct {
Width int `json:"width"`
Height int `json:"height"`
}
// Alignment describes which edge of a parent screen a child screen is placed against.
type Alignment int
const (
AlignTop Alignment = iota // child is above parent
AlignRight // child is to the right of parent
AlignBottom // child is below parent
AlignLeft // child is to the left of parent
)
// OffsetReference specifies whether the placement offset is measured from the
// beginning (top/left) or end (bottom/right) of the parent edge.
type OffsetReference int
const (
OffsetBegin OffsetReference = iota // offset from top or left
OffsetEnd // offset from bottom or right
)
// ScreenPlacement positions a screen relative to a parent screen.
//
// placement := screen.NewPlacement(parent, AlignRight, 0, OffsetBegin)
// placement.Apply()
type ScreenPlacement struct {
screen *Screen
parent *Screen
alignment Alignment
offset int
offsetReference OffsetReference
}
// NewPlacement creates a ScreenPlacement that positions screen relative to parent.
//
// p := NewPlacement(secondary, primary, AlignRight, 0, OffsetBegin)
// p.Apply()
func NewPlacement(screen, parent *Screen, alignment Alignment, offset int, reference OffsetReference) ScreenPlacement {
return ScreenPlacement{
screen: screen,
parent: parent,
alignment: alignment,
offset: offset,
offsetReference: reference,
}
}
// Apply moves screen.Bounds so that it sits against the specified edge of parent.
//
// NewPlacement(s, p, AlignRight, 0, OffsetBegin).Apply()
func (p ScreenPlacement) Apply() {
parentBounds := p.parent.Bounds
screenBounds := p.screen.Bounds
newX := parentBounds.X
newY := parentBounds.Y
offset := p.offset
if p.alignment == AlignTop || p.alignment == AlignBottom {
if p.offsetReference == OffsetEnd {
offset = parentBounds.Width - offset - screenBounds.Width
}
offset = min(offset, parentBounds.Width)
offset = max(offset, -screenBounds.Width)
newX += offset
if p.alignment == AlignTop {
newY -= screenBounds.Height
} else {
newY += parentBounds.Height
}
} else {
if p.offsetReference == OffsetEnd {
offset = parentBounds.Height - offset - screenBounds.Height
}
offset = min(offset, parentBounds.Height)
offset = max(offset, -screenBounds.Height)
newY += offset
if p.alignment == AlignLeft {
newX -= screenBounds.Width
} else {
newX += parentBounds.Width
}
}
workAreaOffsetX := p.screen.WorkArea.X - p.screen.Bounds.X
workAreaOffsetY := p.screen.WorkArea.Y - p.screen.Bounds.Y
p.screen.Bounds.X = newX
p.screen.Bounds.Y = newY
p.screen.WorkArea.X = newX + workAreaOffsetX
p.screen.WorkArea.Y = newY + workAreaOffsetY
}