mirror of
https://github.com/wailsapp/wails.git
synced 2025-05-04 09:21:44 +08:00

* [v3-Windows] New DIP system for Enhanced High DPI Monitor Support * Update changelog * Remove asset middleware * Remove SetThreadDpiAwarenessContext() * Fix macOS build. * Fill missing screens fields (linux, darwin) * Skip DPI transformation on unsupported platforms * Simplify distanceFromRectSquared() * Update v3/pkg/application/screenmanager.go Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --------- Co-authored-by: Lea Anthony <lea.anthony@gmail.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
717 lines
24 KiB
Go
717 lines
24 KiB
Go
package application_test
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"slices"
|
|
"strconv"
|
|
"testing"
|
|
|
|
"github.com/matryer/is"
|
|
"github.com/wailsapp/wails/v3/pkg/application"
|
|
)
|
|
|
|
type ScreenDef struct {
|
|
id int
|
|
w, h int
|
|
s float32
|
|
parent ScreenDefParent
|
|
name string
|
|
}
|
|
|
|
type ScreenDefParent struct {
|
|
id int
|
|
align string
|
|
offset int
|
|
}
|
|
|
|
type ScreensLayout struct {
|
|
name string
|
|
screens []ScreenDef
|
|
}
|
|
|
|
type ParsedLayout struct {
|
|
name string
|
|
screens []*application.Screen
|
|
}
|
|
|
|
func exampleLayouts() []ParsedLayout {
|
|
layouts := [][]ScreensLayout{
|
|
{
|
|
// Normal examples (demonstrate real life scenarios)
|
|
{
|
|
name: "Single 4k monitor",
|
|
screens: []ScreenDef{
|
|
{id: 1, w: 3840, h: 2160, s: 163.0 / 96, name: `27" 4K UHD 163DPI`},
|
|
},
|
|
},
|
|
{
|
|
name: "Two monitors",
|
|
screens: []ScreenDef{
|
|
{id: 1, w: 3840, h: 2160, s: 163.0 / 96, name: `27" 4K UHD 163DPI`},
|
|
{id: 2, w: 1920, h: 1080, s: 1, parent: ScreenDefParent{id: 1, align: "r", offset: 0}, name: `23" FHD 96DPI`},
|
|
},
|
|
},
|
|
{
|
|
name: "Two monitors (2)",
|
|
screens: []ScreenDef{
|
|
{id: 1, w: 1920, h: 1080, s: 1, name: `23" FHD 96DPI`},
|
|
{id: 2, w: 1920, h: 1080, s: 1.25, parent: ScreenDefParent{id: 1, align: "r", offset: 0}, name: `23" FHD 96DPI (125%)`},
|
|
},
|
|
},
|
|
{
|
|
name: "Three monitors",
|
|
screens: []ScreenDef{
|
|
{id: 1, w: 3840, h: 2160, s: 163.0 / 96, name: `27" 4K UHD 163DPI`},
|
|
{id: 2, w: 1920, h: 1080, s: 1, parent: ScreenDefParent{id: 1, align: "r", offset: 0}, name: `23" FHD 96DPI`},
|
|
{id: 3, w: 1920, h: 1080, s: 1.25, parent: ScreenDefParent{id: 1, align: "l", offset: 0}, name: `23" FHD 96DPI (125%)`},
|
|
},
|
|
},
|
|
{
|
|
name: "Four monitors",
|
|
screens: []ScreenDef{
|
|
{id: 1, w: 3840, h: 2160, s: 163.0 / 96, name: `27" 4K UHD 163DPI`},
|
|
{id: 2, w: 1920, h: 1080, s: 1, parent: ScreenDefParent{id: 1, align: "r", offset: 0}, name: `23" FHD 96DPI`},
|
|
{id: 3, w: 1920, h: 1080, s: 1.25, parent: ScreenDefParent{id: 2, align: "b", offset: 0}, name: `23" FHD 96DPI (125%)`},
|
|
{id: 4, w: 1080, h: 1920, s: 1, parent: ScreenDefParent{id: 1, align: "l", offset: 0}, name: `23" FHD (90deg)`},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
// Test cases examples (demonstrate the algorithm basics)
|
|
{
|
|
name: "Child scaled, Start offset",
|
|
screens: []ScreenDef{
|
|
{id: 1, w: 1200, h: 1200, s: 1, name: "Parent"},
|
|
{id: 2, w: 1200, h: 1200, s: 1.5, parent: ScreenDefParent{id: 1, align: "r", offset: 600}, name: "Child"},
|
|
},
|
|
},
|
|
{
|
|
name: "Child scaled, End offset",
|
|
screens: []ScreenDef{
|
|
{id: 1, w: 1200, h: 1200, s: 1, name: "Parent"},
|
|
{id: 2, w: 1200, h: 1200, s: 1.5, parent: ScreenDefParent{id: 1, align: "r", offset: -600}, name: "Child"},
|
|
},
|
|
},
|
|
{
|
|
name: "Parent scaled, Start offset percent",
|
|
screens: []ScreenDef{
|
|
{id: 1, w: 1200, h: 1200, s: 1.5, name: "Parent"},
|
|
{id: 2, w: 1200, h: 1200, s: 1, parent: ScreenDefParent{id: 1, align: "r", offset: 600}, name: "Child"},
|
|
},
|
|
},
|
|
{
|
|
name: "Parent scaled, End offset percent",
|
|
screens: []ScreenDef{
|
|
{id: 1, w: 1200, h: 1200, s: 1.5, name: "Parent"},
|
|
{id: 2, w: 1200, h: 1200, s: 1, parent: ScreenDefParent{id: 1, align: "r", offset: -600}, name: "Child"},
|
|
},
|
|
},
|
|
{
|
|
name: "Parent scaled, Start align",
|
|
screens: []ScreenDef{
|
|
{id: 1, w: 1200, h: 1200, s: 1.5, name: "Parent"},
|
|
{id: 2, w: 1200, h: 1100, s: 1, parent: ScreenDefParent{id: 1, align: "r", offset: 0}, name: "Child"},
|
|
},
|
|
},
|
|
{
|
|
name: "Parent scaled, End align",
|
|
screens: []ScreenDef{
|
|
{id: 1, w: 1200, h: 1200, s: 1.5, name: "Parent"},
|
|
{id: 2, w: 1200, h: 1200, s: 1, parent: ScreenDefParent{id: 1, align: "r", offset: 0}, name: "Child"},
|
|
},
|
|
},
|
|
{
|
|
name: "Parent scaled, in-between",
|
|
screens: []ScreenDef{
|
|
{id: 1, w: 1200, h: 1200, s: 1.5, name: "Parent"},
|
|
{id: 2, w: 1200, h: 1500, s: 1, parent: ScreenDefParent{id: 1, align: "r", offset: -250}, name: "Child"},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
// Edge cases examples
|
|
{
|
|
name: "Parent order (5 is parent of 4)",
|
|
screens: []ScreenDef{
|
|
{id: 1, w: 1920, h: 1080, s: 1},
|
|
{id: 2, w: 1024, h: 600, s: 1.25, parent: ScreenDefParent{id: 1, align: "r", offset: -200}},
|
|
{id: 3, w: 800, h: 800, s: 1.25, parent: ScreenDefParent{id: 2, align: "b", offset: 0}},
|
|
{id: 4, w: 800, h: 1080, s: 1.5, parent: ScreenDefParent{id: 2, align: "re", offset: 100}},
|
|
{id: 5, w: 600, h: 600, s: 1, parent: ScreenDefParent{id: 3, align: "r", offset: 100}},
|
|
},
|
|
},
|
|
{
|
|
name: "de-intersection reparent",
|
|
screens: []ScreenDef{
|
|
{id: 1, w: 1920, h: 1080, s: 1},
|
|
{id: 2, w: 1680, h: 1050, s: 1.25, parent: ScreenDefParent{id: 1, align: "r", offset: 10}},
|
|
{id: 3, w: 1440, h: 900, s: 1.5, parent: ScreenDefParent{id: 1, align: "le", offset: 150}},
|
|
{id: 4, w: 1024, h: 768, s: 1, parent: ScreenDefParent{id: 3, align: "bc", offset: -200}},
|
|
{id: 5, w: 1024, h: 768, s: 1.25, parent: ScreenDefParent{id: 4, align: "r", offset: 400}},
|
|
},
|
|
},
|
|
{
|
|
name: "de-intersection (unattached child)",
|
|
screens: []ScreenDef{
|
|
{id: 1, w: 1920, h: 1080, s: 1},
|
|
{id: 2, w: 1024, h: 768, s: 1.5, parent: ScreenDefParent{id: 1, align: "le", offset: 10}},
|
|
{id: 3, w: 1024, h: 768, s: 1.25, parent: ScreenDefParent{id: 2, align: "b", offset: 100}},
|
|
{id: 4, w: 1024, h: 768, s: 1, parent: ScreenDefParent{id: 3, align: "r", offset: 500}},
|
|
},
|
|
},
|
|
{
|
|
name: "Multiple de-intersection",
|
|
screens: []ScreenDef{
|
|
{id: 1, w: 1920, h: 1080, s: 1},
|
|
{id: 2, w: 1024, h: 768, s: 1, parent: ScreenDefParent{id: 1, align: "be", offset: 0}},
|
|
{id: 3, w: 1024, h: 768, s: 1, parent: ScreenDefParent{id: 2, align: "b", offset: 300}},
|
|
{id: 4, w: 1024, h: 768, s: 1.5, parent: ScreenDefParent{id: 2, align: "le", offset: 100}},
|
|
{id: 5, w: 1024, h: 768, s: 1, parent: ScreenDefParent{id: 4, align: "be", offset: 100}},
|
|
},
|
|
},
|
|
{
|
|
name: "Multiple de-intersection (left-side)",
|
|
screens: []ScreenDef{
|
|
{id: 1, w: 1920, h: 1080, s: 1},
|
|
{id: 2, w: 1024, h: 768, s: 1, parent: ScreenDefParent{id: 1, align: "le", offset: 0}},
|
|
{id: 3, w: 1024, h: 768, s: 1, parent: ScreenDefParent{id: 2, align: "b", offset: 300}},
|
|
{id: 4, w: 1024, h: 768, s: 1.5, parent: ScreenDefParent{id: 2, align: "le", offset: 100}},
|
|
{id: 5, w: 1024, h: 768, s: 1, parent: ScreenDefParent{id: 4, align: "be", offset: 100}},
|
|
},
|
|
},
|
|
{
|
|
name: "Parent de-intersection child offset",
|
|
screens: []ScreenDef{
|
|
{id: 1, w: 1600, h: 1600, s: 1.5},
|
|
{id: 2, w: 800, h: 800, s: 1, parent: ScreenDefParent{id: 1, align: "r", offset: 0}},
|
|
{id: 3, w: 800, h: 800, s: 1, parent: ScreenDefParent{id: 1, align: "r", offset: 800}},
|
|
{id: 4, w: 800, h: 1600, s: 1, parent: ScreenDefParent{id: 2, align: "r", offset: 0}},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
parsedLayouts := []ParsedLayout{}
|
|
|
|
for _, section := range layouts {
|
|
for _, layout := range section {
|
|
parsedLayouts = append(parsedLayouts, parseLayout(layout))
|
|
}
|
|
}
|
|
|
|
return parsedLayouts
|
|
}
|
|
|
|
// Parse screens layout from easy-to-define ScreenDef for testing to actual Screens layout
|
|
func parseLayout(layout ScreensLayout) ParsedLayout {
|
|
screens := []*application.Screen{}
|
|
|
|
for _, screen := range layout.screens {
|
|
var x, y int
|
|
w := screen.w
|
|
h := screen.h
|
|
|
|
if screen.parent.id > 0 {
|
|
idx := slices.IndexFunc(screens, func(s *application.Screen) bool { return s.ID == strconv.Itoa(screen.parent.id) })
|
|
parent := screens[idx].Bounds
|
|
offset := screen.parent.offset
|
|
align := screen.parent.align
|
|
align2 := ""
|
|
|
|
if len(align) == 2 {
|
|
align2 = string(align[1])
|
|
align = string(align[0])
|
|
}
|
|
|
|
x = parent.X
|
|
y = parent.Y
|
|
// t: top, b: bottom, l: left, r: right, e: edge, c: corner
|
|
if align == "t" || align == "b" {
|
|
x += offset
|
|
if align2 == "e" || align2 == "c" {
|
|
x += parent.Width
|
|
}
|
|
if align2 == "e" {
|
|
x -= w
|
|
}
|
|
if align == "t" {
|
|
y -= h
|
|
} else {
|
|
y += parent.Height
|
|
}
|
|
} else {
|
|
y += offset
|
|
if align2 == "e" || align2 == "c" {
|
|
y += parent.Height
|
|
}
|
|
if align2 == "e" {
|
|
y -= h
|
|
}
|
|
if align == "l" {
|
|
x -= w
|
|
} else {
|
|
x += parent.Width
|
|
}
|
|
}
|
|
}
|
|
name := screen.name
|
|
if name == "" {
|
|
name = "Display" + strconv.Itoa(screen.id)
|
|
}
|
|
screens = append(screens, &application.Screen{
|
|
ID: strconv.Itoa(screen.id),
|
|
Name: name,
|
|
ScaleFactor: float32(math.Round(float64(screen.s)*100) / 100),
|
|
X: x,
|
|
Y: y,
|
|
Size: application.Size{Width: w, Height: h},
|
|
Bounds: application.Rect{X: x, Y: y, Width: w, Height: h},
|
|
PhysicalBounds: application.Rect{X: x, Y: y, Width: w, Height: h},
|
|
WorkArea: application.Rect{X: x, Y: y, Width: w, Height: h - int(40*screen.s)},
|
|
PhysicalWorkArea: application.Rect{X: x, Y: y, Width: w, Height: h - int(40*screen.s)},
|
|
IsPrimary: screen.id == 1,
|
|
Rotation: 0,
|
|
})
|
|
}
|
|
return ParsedLayout{
|
|
name: layout.name,
|
|
screens: screens,
|
|
}
|
|
}
|
|
|
|
func matchRects(r1, r2 application.Rect) error {
|
|
threshold := 1.0
|
|
if math.Abs(float64(r1.X-r2.X)) > threshold ||
|
|
math.Abs(float64(r1.Y-r2.Y)) > threshold ||
|
|
math.Abs(float64(r1.Width-r2.Width)) > threshold ||
|
|
math.Abs(float64(r1.Height-r2.Height)) > threshold {
|
|
return fmt.Errorf("%v != %v", r1, r2)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Test screens layout (DPI transformation)
|
|
func TestScreenManager_ScreensLayout(t *testing.T) {
|
|
sm := application.ScreenManager{}
|
|
|
|
t.Run("Child scaled", func(t *testing.T) {
|
|
is := is.New(t)
|
|
|
|
layout := parseLayout(ScreensLayout{screens: []ScreenDef{
|
|
{id: 1, w: 1200, h: 1200, s: 1},
|
|
{id: 2, w: 1200, h: 1200, s: 1.5, parent: ScreenDefParent{id: 1, align: "r", offset: 600}},
|
|
}})
|
|
err := sm.LayoutScreens(layout.screens)
|
|
is.NoErr(err)
|
|
|
|
screens := sm.Screens()
|
|
is.Equal(len(screens), 2) // 2 screens
|
|
is.Equal(screens[0].PhysicalBounds, application.Rect{X: 0, Y: 0, Width: 1200, Height: 1200}) // Parent physical bounds
|
|
is.Equal(screens[0].Bounds, screens[0].PhysicalBounds) // Parent no scaling
|
|
is.Equal(screens[1].PhysicalBounds, application.Rect{X: 1200, Y: 600, Width: 1200, Height: 1200}) // Child physical bounds
|
|
is.Equal(screens[1].Bounds, application.Rect{X: 1200, Y: 600, Width: 800, Height: 800}) // Child DIP bounds
|
|
})
|
|
|
|
t.Run("Parent scaled", func(t *testing.T) {
|
|
is := is.New(t)
|
|
|
|
layout := parseLayout(ScreensLayout{screens: []ScreenDef{
|
|
{id: 1, w: 1200, h: 1200, s: 1.5},
|
|
{id: 2, w: 1200, h: 1200, s: 1, parent: ScreenDefParent{id: 1, align: "r", offset: 600}},
|
|
}})
|
|
|
|
err := sm.LayoutScreens(layout.screens)
|
|
is.NoErr(err)
|
|
|
|
screens := sm.Screens()
|
|
is.Equal(len(screens), 2) // 2 screens
|
|
is.Equal(screens[0].PhysicalBounds, application.Rect{X: 0, Y: 0, Width: 1200, Height: 1200}) // Parent physical bounds
|
|
is.Equal(screens[0].Bounds, application.Rect{X: 0, Y: 0, Width: 800, Height: 800}) // Parent DIP bounds
|
|
is.Equal(screens[1].PhysicalBounds, application.Rect{X: 1200, Y: 600, Width: 1200, Height: 1200}) // Child physical bounds
|
|
is.Equal(screens[1].Bounds, application.Rect{X: 800, Y: 400, Width: 1200, Height: 1200}) // Child DIP bounds
|
|
})
|
|
}
|
|
|
|
// Test basic transformation between physical and DIP coordinates
|
|
func TestScreenManager_BasicTranformation(t *testing.T) {
|
|
sm := application.ScreenManager{}
|
|
is := is.New(t)
|
|
layout := parseLayout(ScreensLayout{screens: []ScreenDef{
|
|
{id: 1, w: 1200, h: 1200, s: 1},
|
|
{id: 2, w: 1200, h: 1200, s: 1.5, parent: ScreenDefParent{id: 1, align: "r", offset: 600}},
|
|
}})
|
|
|
|
err := sm.LayoutScreens(layout.screens)
|
|
is.NoErr(err)
|
|
|
|
pt := application.Point{X: 100, Y: 100}
|
|
is.Equal(sm.DipToPhysicalPoint(pt), pt) // DipToPhysicalPoint screen1
|
|
is.Equal(sm.PhysicalToDipPoint(pt), pt) // PhysicalToDipPoint screen1
|
|
|
|
ptDip := application.Point{X: 1300, Y: 700}
|
|
ptPhysical := application.Point{X: 1350, Y: 750}
|
|
is.Equal(sm.DipToPhysicalPoint(ptDip), ptPhysical) // DipToPhysicalPoint screen2
|
|
is.Equal(sm.PhysicalToDipPoint(ptPhysical), ptDip) // PhysicalToDipPoint screen2
|
|
|
|
rect := application.Rect{X: 100, Y: 100, Width: 200, Height: 300}
|
|
is.Equal(sm.DipToPhysicalRect(rect), rect) // DipToPhysicalRect screen1
|
|
is.Equal(sm.PhysicalToDipRect(rect), rect) // DipToPhysicalRect screen1
|
|
|
|
rectDip := application.Rect{X: 1300, Y: 700, Width: 200, Height: 300}
|
|
rectPhysical := application.Rect{X: 1350, Y: 750, Width: 300, Height: 450}
|
|
is.Equal(sm.DipToPhysicalRect(rectDip), rectPhysical) // DipToPhysicalRect screen2
|
|
is.Equal(sm.PhysicalToDipRect(rectPhysical), rectDip) // DipToPhysicalRect screen2
|
|
|
|
rectDip = application.Rect{X: 2200, Y: 250, Width: 200, Height: 300}
|
|
rectPhysical = application.Rect{X: 2700, Y: 75, Width: 300, Height: 450}
|
|
is.Equal(sm.DipToPhysicalRect(rectDip), rectPhysical) // DipToPhysicalRect outside screen2
|
|
is.Equal(sm.PhysicalToDipRect(rectPhysical), rectDip) // DipToPhysicalRect outside screen2
|
|
}
|
|
|
|
func TestScreenManager_PrimaryScreen(t *testing.T) {
|
|
sm := application.ScreenManager{}
|
|
is := is.New(t)
|
|
|
|
for _, layout := range exampleLayouts() {
|
|
err := sm.LayoutScreens(layout.screens)
|
|
is.NoErr(err)
|
|
is.Equal(sm.PrimaryScreen(), layout.screens[0]) // Primary screen
|
|
}
|
|
|
|
layout := parseLayout(ScreensLayout{screens: []ScreenDef{
|
|
{id: 1, w: 1200, h: 1200, s: 1.5},
|
|
{id: 2, w: 1200, h: 1200, s: 1, parent: ScreenDefParent{id: 1, align: "r", offset: 600}},
|
|
}})
|
|
|
|
layout.screens[0], layout.screens[1] = layout.screens[1], layout.screens[0]
|
|
err := sm.LayoutScreens(layout.screens)
|
|
is.NoErr(err)
|
|
is.Equal(sm.PrimaryScreen(), layout.screens[1]) // Primary screen
|
|
|
|
layout.screens[1].IsPrimary = false
|
|
err = sm.LayoutScreens(layout.screens)
|
|
is.True(err != nil) // Should error when no primary screen found
|
|
}
|
|
|
|
// Test edge alignment between transformation
|
|
// (points and rects on the screen edge should transform to the same precise edge position)
|
|
func TestScreenManager_EdgeAlign(t *testing.T) {
|
|
sm := application.ScreenManager{}
|
|
is := is.New(t)
|
|
|
|
for _, layout := range exampleLayouts() {
|
|
err := sm.LayoutScreens(layout.screens)
|
|
is.NoErr(err)
|
|
for _, screen := range sm.Screens() {
|
|
ptOriginDip := screen.Bounds.Origin()
|
|
ptOriginPhysical := screen.PhysicalBounds.Origin()
|
|
ptCornerDip := screen.Bounds.InsideCorner()
|
|
ptCornerPhysical := screen.PhysicalBounds.InsideCorner()
|
|
|
|
is.Equal(sm.DipToPhysicalPoint(ptOriginDip), ptOriginPhysical) // DipToPhysicalPoint Origin
|
|
is.Equal(sm.PhysicalToDipPoint(ptOriginPhysical), ptOriginDip) // PhysicalToDipPoint Origin
|
|
is.Equal(sm.DipToPhysicalPoint(ptCornerDip), ptCornerPhysical) // DipToPhysicalPoint Corner
|
|
is.Equal(sm.PhysicalToDipPoint(ptCornerPhysical), ptCornerDip) // PhysicalToDipPoint Corner
|
|
|
|
rectOriginDip := application.Rect{X: ptOriginDip.X, Y: ptOriginDip.Y, Width: 100, Height: 100}
|
|
rectOriginPhysical := application.Rect{X: ptOriginPhysical.X, Y: ptOriginPhysical.Y, Width: 100, Height: 100}
|
|
rectCornerDip := application.Rect{X: ptCornerDip.X - 99, Y: ptCornerDip.Y - 99, Width: 100, Height: 100}
|
|
rectCornerPhysical := application.Rect{X: ptCornerPhysical.X - 99, Y: ptCornerPhysical.Y - 99, Width: 100, Height: 100}
|
|
|
|
is.Equal(sm.DipToPhysicalRect(rectOriginDip).Origin(), rectOriginPhysical.Origin()) // DipToPhysicalRect Origin
|
|
is.Equal(sm.PhysicalToDipRect(rectOriginPhysical).Origin(), rectOriginDip.Origin()) // PhysicalToDipRect Origin
|
|
is.Equal(sm.DipToPhysicalRect(rectCornerDip).Corner(), rectCornerPhysical.Corner()) // DipToPhysicalRect Corner
|
|
is.Equal(sm.PhysicalToDipRect(rectCornerPhysical).Corner(), rectCornerDip.Corner()) // PhysicalToDipRect Corner
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestScreenManager_ProbePoints(t *testing.T) {
|
|
sm := application.ScreenManager{}
|
|
is := is.New(t)
|
|
threshold := 1.0
|
|
steps := 3
|
|
|
|
for _, layout := range exampleLayouts() {
|
|
err := sm.LayoutScreens(layout.screens)
|
|
is.NoErr(err)
|
|
for _, screen := range sm.Screens() {
|
|
for i := 0; i <= 1; i++ {
|
|
isDip := (i == 0)
|
|
|
|
var b application.Rect
|
|
if isDip {
|
|
b = screen.Bounds
|
|
} else {
|
|
b = screen.PhysicalBounds
|
|
}
|
|
|
|
xStep := b.Width / steps
|
|
yStep := b.Height / steps
|
|
if xStep < 1 {
|
|
xStep = 1
|
|
}
|
|
if yStep < 1 {
|
|
yStep = 1
|
|
}
|
|
pt := b.Origin()
|
|
xDone := false
|
|
yDone := false
|
|
|
|
for !yDone {
|
|
if pt.Y > b.InsideCorner().Y {
|
|
pt.Y = b.InsideCorner().Y
|
|
yDone = true
|
|
}
|
|
|
|
pt.X = b.X
|
|
xDone = false
|
|
|
|
for !xDone {
|
|
if pt.X > b.InsideCorner().X {
|
|
pt.X = b.InsideCorner().X
|
|
xDone = true
|
|
}
|
|
var ptDblTransformed application.Point
|
|
|
|
if isDip {
|
|
ptDblTransformed = sm.PhysicalToDipPoint(sm.DipToPhysicalPoint(pt))
|
|
} else {
|
|
ptDblTransformed = sm.DipToPhysicalPoint(sm.PhysicalToDipPoint(pt))
|
|
}
|
|
|
|
is.True(math.Abs(float64(ptDblTransformed.X-pt.X)) <= threshold)
|
|
is.True(math.Abs(float64(ptDblTransformed.Y-pt.Y)) <= threshold)
|
|
pt.X += xStep
|
|
}
|
|
pt.Y += yStep
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Test transformation drift over time
|
|
func TestScreenManager_TransformationDrift(t *testing.T) {
|
|
sm := application.ScreenManager{}
|
|
is := is.New(t)
|
|
|
|
for _, layout := range exampleLayouts() {
|
|
err := sm.LayoutScreens(layout.screens)
|
|
is.NoErr(err)
|
|
for _, screen := range sm.Screens() {
|
|
rectPhysicalOriginal := application.Rect{
|
|
X: screen.PhysicalBounds.X + 100,
|
|
Y: screen.PhysicalBounds.Y + 100,
|
|
Width: 123,
|
|
Height: 123,
|
|
}
|
|
|
|
// Slide the position to catch any rounding errors
|
|
for i := 0; i < 10; i++ {
|
|
rectPhysicalOriginal.X++
|
|
rectPhysicalOriginal.Y++
|
|
rectPhysical := rectPhysicalOriginal
|
|
// Transform back and forth several times to make sure no drift is introduced over time
|
|
for j := 0; j < 10; j++ {
|
|
rectDip := sm.PhysicalToDipRect(rectPhysical)
|
|
rectPhysical = sm.DipToPhysicalRect(rectDip)
|
|
}
|
|
is.NoErr(matchRects(rectPhysical, rectPhysicalOriginal))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestScreenManager_ScreenNearestRect(t *testing.T) {
|
|
sm := application.ScreenManager{}
|
|
is := is.New(t)
|
|
|
|
layout := parseLayout(ScreensLayout{screens: []ScreenDef{
|
|
{id: 1, w: 3840, h: 2160, s: 163.0 / 96, name: `27" 4K UHD 163DPI`},
|
|
{id: 2, w: 1920, h: 1080, s: 1, parent: ScreenDefParent{id: 1, align: "r", offset: 0}, name: `23" FHD 96DPI`},
|
|
{id: 3, w: 1920, h: 1080, s: 1.25, parent: ScreenDefParent{id: 1, align: "l", offset: 0}, name: `23" FHD 96DPI (125%)`},
|
|
}})
|
|
err := sm.LayoutScreens(layout.screens)
|
|
is.NoErr(err)
|
|
|
|
type Rects map[string][]application.Rect
|
|
|
|
t.Run("DIP rects", func(t *testing.T) {
|
|
is := is.New(t)
|
|
rects := Rects{
|
|
"1": []application.Rect{
|
|
{X: -150, Y: 260, Width: 400, Height: 300},
|
|
{X: -250, Y: 750, Width: 400, Height: 300},
|
|
{X: -450, Y: 950, Width: 400, Height: 300},
|
|
{X: 800, Y: 1350, Width: 400, Height: 300},
|
|
{X: 2000, Y: 100, Width: 400, Height: 300},
|
|
{X: 2100, Y: 950, Width: 400, Height: 300},
|
|
{X: 2350, Y: 1200, Width: 400, Height: 300},
|
|
},
|
|
"2": []application.Rect{
|
|
{X: 2100, Y: 50, Width: 400, Height: 300},
|
|
{X: 2150, Y: 950, Width: 400, Height: 300},
|
|
{X: 2450, Y: 1150, Width: 400, Height: 300},
|
|
{X: 4300, Y: 400, Width: 400, Height: 300},
|
|
},
|
|
"3": []application.Rect{
|
|
{X: -2000, Y: 100, Width: 400, Height: 300},
|
|
{X: -220, Y: 200, Width: 400, Height: 300},
|
|
{X: -300, Y: 750, Width: 400, Height: 300},
|
|
{X: -500, Y: 900, Width: 400, Height: 300},
|
|
},
|
|
}
|
|
|
|
for screenID, screenRects := range rects {
|
|
for _, rect := range screenRects {
|
|
screen := sm.ScreenNearestDipRect(rect)
|
|
is.Equal(screen.ID, screenID)
|
|
}
|
|
}
|
|
})
|
|
t.Run("Physical rects", func(t *testing.T) {
|
|
is := is.New(t)
|
|
rects := Rects{
|
|
"1": []application.Rect{
|
|
{X: -150, Y: 100, Width: 400, Height: 300},
|
|
{X: -250, Y: 1500, Width: 400, Height: 300},
|
|
{X: 3600, Y: 100, Width: 400, Height: 300},
|
|
},
|
|
"2": []application.Rect{
|
|
{X: 3700, Y: 100, Width: 400, Height: 300},
|
|
{X: 4000, Y: 1150, Width: 400, Height: 300},
|
|
},
|
|
"3": []application.Rect{
|
|
{X: -250, Y: 100, Width: 400, Height: 300},
|
|
{X: -300, Y: 950, Width: 400, Height: 300},
|
|
{X: -1000, Y: 1000, Width: 400, Height: 300},
|
|
},
|
|
}
|
|
|
|
for screenID, screenRects := range rects {
|
|
for _, rect := range screenRects {
|
|
screen := sm.ScreenNearestPhysicalRect(rect)
|
|
is.Equal(screen.ID, screenID)
|
|
}
|
|
}
|
|
})
|
|
|
|
// DIP rect is near screen1 but when transformed becomes near screen2.
|
|
// To have a consistent transformation back & forth, screen nearest physical rect
|
|
// should be the one given by ScreenNearestDipRect
|
|
t.Run("Edge case 1", func(t *testing.T) {
|
|
is := is.New(t)
|
|
layout := parseLayout(ScreensLayout{screens: []ScreenDef{
|
|
{id: 1, w: 1200, h: 1200, s: 1},
|
|
{id: 2, w: 1200, h: 1300, s: 1.5, parent: ScreenDefParent{id: 1, align: "r", offset: -20}},
|
|
}})
|
|
err := sm.LayoutScreens(layout.screens)
|
|
is.NoErr(err)
|
|
|
|
rectDip := application.Rect{X: 1020, Y: 800, Width: 400, Height: 300}
|
|
rectPhysical := sm.DipToPhysicalRect(rectDip)
|
|
|
|
screenDip := sm.ScreenNearestDipRect(rectDip)
|
|
screenPhysical := sm.ScreenNearestPhysicalRect(rectPhysical)
|
|
is.Equal(screenDip.ID, "2") // screenDip
|
|
is.Equal(screenPhysical.ID, "2") // screenPhysical
|
|
|
|
rectDblTransformed := sm.PhysicalToDipRect(rectPhysical)
|
|
is.NoErr(matchRects(rectDblTransformed, rectDip)) // double transformation
|
|
})
|
|
}
|
|
|
|
// Unsolved edge cases
|
|
func TestScreenManager_UnsolvedEdgeCases(t *testing.T) {
|
|
sm := application.ScreenManager{}
|
|
is := is.New(t)
|
|
|
|
// Edge case 1: invalid DIP rect location
|
|
// there could be a setup where some dip rects locations are invalid, meaning that there's no
|
|
// physical rect that could produce that dip rect at this location
|
|
// Not sure how to solve this scenario
|
|
t.Run("Edge case 1: invalid dip rect", func(t *testing.T) {
|
|
t.Skip("Unsolved edge case")
|
|
is := is.New(t)
|
|
layout := parseLayout(ScreensLayout{screens: []ScreenDef{
|
|
{id: 1, w: 1200, h: 1200, s: 1},
|
|
{id: 2, w: 1200, h: 1100, s: 1.5, parent: ScreenDefParent{id: 1, align: "r", offset: 0}},
|
|
}})
|
|
err := sm.LayoutScreens(layout.screens)
|
|
is.NoErr(err)
|
|
|
|
rectDip := application.Rect{X: 1050, Y: 700, Width: 400, Height: 300}
|
|
rectPhysical := sm.DipToPhysicalRect(rectDip)
|
|
|
|
screenDip := sm.ScreenNearestDipRect(rectDip)
|
|
screenPhysical := sm.ScreenNearestPhysicalRect(rectPhysical)
|
|
is.Equal(screenDip.ID, screenPhysical.ID)
|
|
|
|
rectDblTransformed := sm.PhysicalToDipRect(rectPhysical)
|
|
is.NoErr(matchRects(rectDblTransformed, rectDip)) // double transformation
|
|
})
|
|
|
|
// Edge case 2: physical rect that changes when double transformed
|
|
// there could be a setup where a dip rect at some locations could be produced by two different physical rects
|
|
// causing one of these physical rects to be changed to the other when double transformed
|
|
// Not sure how to solve this scenario
|
|
t.Run("Edge case 2: changed physical rect", func(t *testing.T) {
|
|
t.Skip("Unsolved edge case")
|
|
is := is.New(t)
|
|
layout := parseLayout(ScreensLayout{screens: []ScreenDef{
|
|
{id: 1, w: 1200, h: 1200, s: 1.5},
|
|
{id: 2, w: 1200, h: 900, s: 1, parent: ScreenDefParent{id: 1, align: "r", offset: 0}},
|
|
}})
|
|
err := sm.LayoutScreens(layout.screens)
|
|
is.NoErr(err)
|
|
|
|
rectPhysical := application.Rect{X: 1050, Y: 890, Width: 400, Height: 300}
|
|
rectDblTransformed := sm.DipToPhysicalRect(sm.PhysicalToDipRect(rectPhysical))
|
|
is.NoErr(matchRects(rectDblTransformed, rectPhysical)) // double transformation
|
|
})
|
|
}
|
|
|
|
func BenchmarkScreenManager_LayoutScreens(b *testing.B) {
|
|
sm := application.ScreenManager{}
|
|
layouts := exampleLayouts()
|
|
screens := layouts[3].screens
|
|
|
|
b.ResetTimer()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
sm.LayoutScreens(screens)
|
|
}
|
|
}
|
|
|
|
func BenchmarkScreenManager_TransformPoint(b *testing.B) {
|
|
sm := application.ScreenManager{}
|
|
layouts := exampleLayouts()
|
|
screens := layouts[3].screens
|
|
sm.LayoutScreens(screens)
|
|
|
|
pt := application.Point{X: 500, Y: 500}
|
|
|
|
b.ResetTimer()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
sm.DipToPhysicalPoint(pt)
|
|
}
|
|
}
|
|
|
|
func BenchmarkScreenManager_TransformRect(b *testing.B) {
|
|
sm := application.ScreenManager{}
|
|
layouts := exampleLayouts()
|
|
screens := layouts[3].screens
|
|
sm.LayoutScreens(screens)
|
|
|
|
rect := application.Rect{X: 500, Y: 500, Width: 800, Height: 600}
|
|
|
|
b.ResetTimer()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
sm.DipToPhysicalRect(rect)
|
|
}
|
|
}
|