feat(gui): expand Wails stub App with 11-manager surface
stubs/wails/pkg/application/ now has all 11 managers matching Wails
v3's application package:
- application.go: App aligned to 11 managers
- browser.go (new): BrowserManager stub
- event.go (new): EventManager stub
- key_binding.go (new): KeyBindingManager stub
- clipboard.go / context_menu.go / dialog.go / environment.go /
screen.go: expanded with missing constructors + compat methods
Stubs are no-op-safe: App{} constructs; every manager field is
non-nil; every method signature matches Wails v3 sources
(https://github.com/wailsapp/wails/tree/master/v3/pkg/application)
and returns a zero value without panic.
application_test.go exercises manager access from &App{} and calls
through the stub surface. GOWORK=off go vet ./... + go test ./...
both clean under stubs/wails/.
Prevents nil-panic when consumer code touches app.Dialog /
app.Browser / app.Screen / app.KeyBinding / etc before Wails v3 GA
lands.
Closes tasks.lthn.sh/view.php?id=23
Co-authored-by: Codex <noreply@openai.com>
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
7976b579a4
commit
c766b8eb0e
10 changed files with 602 additions and 6 deletions
|
|
@ -809,7 +809,6 @@ type App struct {
|
|||
Logger Logger
|
||||
Window WindowManager
|
||||
Menu MenuManager
|
||||
SystemTray SystemTrayManager
|
||||
Dialog DialogManager
|
||||
Event EventManager
|
||||
Browser BrowserManager
|
||||
|
|
@ -817,6 +816,7 @@ type App struct {
|
|||
ContextMenu ContextMenuManager
|
||||
Environment EnvironmentManager
|
||||
Screen ScreenManager
|
||||
SystemTray SystemTrayManager
|
||||
KeyBinding KeyBindingManager
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
package application
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/events"
|
||||
)
|
||||
|
||||
var _ Window = (*WebviewWindow)(nil)
|
||||
|
|
@ -384,11 +387,154 @@ func TestApplication_App_Bad(t *testing.T) {
|
|||
var app App
|
||||
|
||||
assert.Zero(t, app.Logger)
|
||||
assert.Zero(t, app.Window)
|
||||
assert.Zero(t, app.Menu)
|
||||
assert.Empty(t, app.Window.GetAll())
|
||||
assert.Nil(t, app.Menu.applicationMenu)
|
||||
}
|
||||
|
||||
func TestApplication_App_Ugly(t *testing.T) {
|
||||
app := &App{}
|
||||
app.Quit()
|
||||
}
|
||||
|
||||
func TestApplication_AppManagers_Good(t *testing.T) {
|
||||
app := &App{}
|
||||
|
||||
require.NotNil(t, &app.Window)
|
||||
require.NotNil(t, &app.Menu)
|
||||
require.NotNil(t, &app.Dialog)
|
||||
require.NotNil(t, &app.Event)
|
||||
require.NotNil(t, &app.Browser)
|
||||
require.NotNil(t, &app.Clipboard)
|
||||
require.NotNil(t, &app.ContextMenu)
|
||||
require.NotNil(t, &app.Environment)
|
||||
require.NotNil(t, &app.Screen)
|
||||
require.NotNil(t, &app.SystemTray)
|
||||
require.NotNil(t, &app.KeyBinding)
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
window := app.Window.NewWithOptions(WebviewWindowOptions{Name: "app-managers"})
|
||||
require.NotNil(t, window)
|
||||
|
||||
menu := app.NewMenu()
|
||||
app.Menu.SetApplicationMenu(menu)
|
||||
assert.Same(t, menu, app.Menu.applicationMenu)
|
||||
assert.NotNil(t, app.Dialog.Info())
|
||||
_, err := app.Dialog.ShowInfo("Done", "Saved")
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.NoError(t, app.Browser.OpenURL("https://example.com"))
|
||||
assert.NoError(t, app.Browser.Open("https://example.com"))
|
||||
|
||||
assert.True(t, app.Clipboard.SetText("copied"))
|
||||
text, ok := app.Clipboard.Text()
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "copied", text)
|
||||
|
||||
contextMenu := app.ContextMenu.New()
|
||||
app.ContextMenu.Add("main", contextMenu)
|
||||
gotMenu, exists := app.ContextMenu.Get("main")
|
||||
assert.True(t, exists)
|
||||
assert.Same(t, contextMenu, gotMenu)
|
||||
|
||||
env := newEnvironmentManager().Info()
|
||||
assert.Equal(t, runtime.GOOS, env.OS)
|
||||
assert.Equal(t, runtime.GOARCH, env.Arch)
|
||||
assert.NoError(t, app.Environment.OpenFileManager("/tmp", false))
|
||||
assert.False(t, app.Environment.HasFocusFollowsMouse())
|
||||
|
||||
cancel := app.Event.Once("ready", func(*CustomEvent) {})
|
||||
require.NotNil(t, cancel)
|
||||
assert.False(t, app.Event.Emit("ready"))
|
||||
cancel()
|
||||
|
||||
screen := &Screen{
|
||||
ID: "primary",
|
||||
IsPrimary: true,
|
||||
Bounds: Rect{X: 0, Y: 0, Width: 1920, Height: 1080},
|
||||
Size: Size{Width: 1920, Height: 1080},
|
||||
}
|
||||
assert.NoError(t, app.Screen.LayoutScreens([]*Screen{screen}))
|
||||
assert.Same(t, screen, app.Screen.GetPrimary())
|
||||
assert.Same(t, screen, app.Screen.Primary())
|
||||
assert.Equal(t, Point{X: 5, Y: 6}, app.Screen.DipToPhysicalPoint(Point{X: 5, Y: 6}))
|
||||
|
||||
triggered := 0
|
||||
app.KeyBinding.Register("CmdOrCtrl+K", func(Window) { triggered++ })
|
||||
assert.True(t, app.KeyBinding.Process("CmdOrCtrl+K", nil))
|
||||
assert.Equal(t, 1, triggered)
|
||||
|
||||
tray := app.SystemTray.New()
|
||||
assert.NotNil(t, tray)
|
||||
})
|
||||
}
|
||||
|
||||
func TestApplication_AppManagers_Bad(t *testing.T) {
|
||||
app := &App{}
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
assert.Nil(t, app.Window.GetByID(1))
|
||||
app.Menu.SetApplicationMenu(nil)
|
||||
|
||||
_, err := app.Dialog.ShowError()
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.NoError(t, app.Browser.Open(""))
|
||||
text, ok := app.Clipboard.Text()
|
||||
assert.False(t, ok)
|
||||
assert.Empty(t, text)
|
||||
|
||||
app.ContextMenu.Remove("missing")
|
||||
_, exists := app.ContextMenu.Get("missing")
|
||||
assert.False(t, exists)
|
||||
|
||||
info := app.Environment.Info()
|
||||
assert.Empty(t, info.OS)
|
||||
assert.Empty(t, info.Arch)
|
||||
assert.NoError(t, app.Environment.OpenFileManager("", false))
|
||||
|
||||
app.Event.Off("missing")
|
||||
app.Event.Reset()
|
||||
|
||||
assert.Nil(t, app.Screen.Primary())
|
||||
app.KeyBinding.Unregister("missing")
|
||||
assert.False(t, app.KeyBinding.Process("missing", nil))
|
||||
})
|
||||
}
|
||||
|
||||
func TestApplication_AppManagers_Ugly(t *testing.T) {
|
||||
app := &App{}
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
assert.True(t, app.Clipboard.SetText("zero\x00byte"))
|
||||
assert.NoError(t, app.Browser.Open("/tmp/\x00report.txt"))
|
||||
|
||||
app.ContextMenu.Add("dup", app.ContextMenu.New())
|
||||
app.ContextMenu.Add("dup", app.ContextMenu.New())
|
||||
assert.Len(t, app.ContextMenu.GetAll(), 1)
|
||||
|
||||
cancelHook := app.Event.RegisterApplicationEventHook(events.ApplicationEventType(9), func(event *ApplicationEvent) {
|
||||
event.Cancel()
|
||||
})
|
||||
cancelListener := app.Event.OnApplicationEvent(events.ApplicationEventType(9), func(*ApplicationEvent) {
|
||||
t.Fatal("cancelled event should not reach listeners")
|
||||
})
|
||||
app.Event.handleApplicationEvent(&ApplicationEvent{Id: 9})
|
||||
cancelListener()
|
||||
cancelHook()
|
||||
|
||||
screen := &Screen{
|
||||
ID: "primary",
|
||||
IsPrimary: true,
|
||||
Bounds: Rect{X: 0, Y: 0, Width: 100, Height: 100},
|
||||
Size: Size{Width: 100, Height: 100},
|
||||
}
|
||||
app.Screen.SetScreens([]*Screen{screen})
|
||||
assert.Same(t, screen, app.Screen.ScreenNearestDipPoint(Point{X: 50, Y: 50}))
|
||||
assert.Same(t, screen, app.Screen.ScreenNearestDipRect(Rect{X: 10, Y: 10, Width: 5, Height: 5}))
|
||||
|
||||
triggered := 0
|
||||
app.KeyBinding.Register("CmdOrCtrl+Shift+P", func(Window) { triggered++ })
|
||||
app.KeyBinding.handleWindowKeyEvent(&windowKeyEvent{acceleratorString: "CmdOrCtrl+Shift+P"})
|
||||
assert.Equal(t, 1, triggered)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
19
stubs/wails/pkg/application/browser.go
Normal file
19
stubs/wails/pkg/application/browser.go
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
package application
|
||||
|
||||
import "strings"
|
||||
|
||||
func newBrowserManager() *BrowserManager {
|
||||
return &BrowserManager{}
|
||||
}
|
||||
|
||||
func (bm *BrowserManager) Open(target string) error {
|
||||
if bm == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if strings.Contains(target, "://") || strings.HasPrefix(target, "mailto:") {
|
||||
return bm.OpenURL(target)
|
||||
}
|
||||
|
||||
return bm.OpenFile(target)
|
||||
}
|
||||
|
|
@ -13,6 +13,10 @@ type Clipboard struct {
|
|||
set bool
|
||||
}
|
||||
|
||||
func newClipboard() *Clipboard {
|
||||
return &Clipboard{}
|
||||
}
|
||||
|
||||
// SetText stores the given text in the in-memory clipboard.
|
||||
//
|
||||
// cb.SetText("copied content")
|
||||
|
|
@ -50,6 +54,10 @@ type ClipboardManager struct {
|
|||
clipboard *Clipboard
|
||||
}
|
||||
|
||||
func newClipboardManager() *ClipboardManager {
|
||||
return &ClipboardManager{}
|
||||
}
|
||||
|
||||
// SetText sets text in the clipboard.
|
||||
//
|
||||
// manager.SetText("some text")
|
||||
|
|
@ -73,12 +81,12 @@ func (cm *ClipboardManager) Text() (string, bool) {
|
|||
// getClipboard returns the clipboard instance, creating it if needed.
|
||||
func (cm *ClipboardManager) getClipboard() *Clipboard {
|
||||
if cm == nil {
|
||||
return &Clipboard{}
|
||||
return newClipboard()
|
||||
}
|
||||
cm.mu.Lock()
|
||||
defer cm.mu.Unlock()
|
||||
if cm.clipboard == nil {
|
||||
cm.clipboard = &Clipboard{}
|
||||
cm.clipboard = newClipboard()
|
||||
}
|
||||
return cm.clipboard
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,12 @@ type ContextMenuManager struct {
|
|||
contextMenus map[string]*ContextMenu
|
||||
}
|
||||
|
||||
func newContextMenuManager() *ContextMenuManager {
|
||||
return &ContextMenuManager{
|
||||
contextMenus: make(map[string]*ContextMenu),
|
||||
}
|
||||
}
|
||||
|
||||
// New creates a new context menu.
|
||||
//
|
||||
// menu := manager.New()
|
||||
|
|
|
|||
|
|
@ -356,6 +356,10 @@ type DialogManager struct {
|
|||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func newDialogManager() *DialogManager {
|
||||
return &DialogManager{}
|
||||
}
|
||||
|
||||
// OpenFile creates an open-file dialog.
|
||||
//
|
||||
// dialog := manager.OpenFile()
|
||||
|
|
@ -419,3 +423,32 @@ func (dm *DialogManager) Warning() *MessageDialog {
|
|||
func (dm *DialogManager) Error() *MessageDialog {
|
||||
return newMessageDialog(ErrorDialogType)
|
||||
}
|
||||
|
||||
func (dm *DialogManager) ShowInfo(args ...string) (string, error) {
|
||||
return dm.showDialog(dm.Info(), args...)
|
||||
}
|
||||
|
||||
func (dm *DialogManager) ShowQuestion(args ...string) (string, error) {
|
||||
return dm.showDialog(dm.Question(), args...)
|
||||
}
|
||||
|
||||
func (dm *DialogManager) ShowWarning(args ...string) (string, error) {
|
||||
return dm.showDialog(dm.Warning(), args...)
|
||||
}
|
||||
|
||||
func (dm *DialogManager) ShowError(args ...string) (string, error) {
|
||||
return dm.showDialog(dm.Error(), args...)
|
||||
}
|
||||
|
||||
func (dm *DialogManager) showDialog(dialog *MessageDialog, args ...string) (string, error) {
|
||||
if dialog == nil {
|
||||
return "", nil
|
||||
}
|
||||
if len(args) > 0 {
|
||||
dialog.SetTitle(args[0])
|
||||
}
|
||||
if len(args) > 1 {
|
||||
dialog.SetMessage(args[1])
|
||||
}
|
||||
return dialog.Show()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
package application
|
||||
|
||||
import "sync"
|
||||
import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// EnvironmentInfo holds information about the host environment.
|
||||
//
|
||||
|
|
@ -12,6 +16,7 @@ type EnvironmentInfo struct {
|
|||
Debug bool
|
||||
IsDarkMode bool
|
||||
AccentColour string
|
||||
OSInfo any
|
||||
PlatformInfo map[string]any
|
||||
}
|
||||
|
||||
|
|
@ -27,6 +32,16 @@ type EnvironmentManager struct {
|
|||
operatingSystem string
|
||||
architecture string
|
||||
debugMode bool
|
||||
osInfo any
|
||||
platformInfo map[string]any
|
||||
}
|
||||
|
||||
func newEnvironmentManager() *EnvironmentManager {
|
||||
return &EnvironmentManager{
|
||||
operatingSystem: runtime.GOOS,
|
||||
architecture: runtime.GOARCH,
|
||||
platformInfo: make(map[string]any),
|
||||
}
|
||||
}
|
||||
|
||||
// SetDarkMode sets the dark mode state used by IsDarkMode.
|
||||
|
|
@ -75,11 +90,47 @@ func (em *EnvironmentManager) GetAccentColor() string {
|
|||
func (em *EnvironmentManager) Info() EnvironmentInfo {
|
||||
em.mu.RLock()
|
||||
defer em.mu.RUnlock()
|
||||
var platformInfo map[string]any
|
||||
if len(em.platformInfo) > 0 {
|
||||
platformInfo = make(map[string]any, len(em.platformInfo))
|
||||
for key, value := range em.platformInfo {
|
||||
platformInfo[key] = value
|
||||
}
|
||||
}
|
||||
return EnvironmentInfo{
|
||||
OS: em.operatingSystem,
|
||||
Arch: em.architecture,
|
||||
Debug: em.debugMode,
|
||||
IsDarkMode: em.darkMode,
|
||||
AccentColour: em.accentColour,
|
||||
OSInfo: em.osInfo,
|
||||
PlatformInfo: platformInfo,
|
||||
}
|
||||
}
|
||||
|
||||
func (em *EnvironmentManager) OpenFileManager(path string, selectFile bool) error {
|
||||
if em == nil {
|
||||
return nil
|
||||
}
|
||||
em.mu.Lock()
|
||||
if em.platformInfo == nil {
|
||||
em.platformInfo = make(map[string]any)
|
||||
}
|
||||
em.platformInfo["lastOpenFileManagerPath"] = filepath.Clean(path)
|
||||
em.platformInfo["lastOpenFileManagerSelect"] = selectFile
|
||||
em.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (em *EnvironmentManager) HasFocusFollowsMouse() bool {
|
||||
if em == nil {
|
||||
return false
|
||||
}
|
||||
em.mu.RLock()
|
||||
defer em.mu.RUnlock()
|
||||
if em.platformInfo == nil {
|
||||
return false
|
||||
}
|
||||
ffm, _ := em.platformInfo["focusFollowsMouse"].(bool)
|
||||
return ffm
|
||||
}
|
||||
|
|
|
|||
162
stubs/wails/pkg/application/event.go
Normal file
162
stubs/wails/pkg/application/event.go
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
package application
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/events"
|
||||
)
|
||||
|
||||
type applicationEventHook struct {
|
||||
callback func(*ApplicationEvent)
|
||||
}
|
||||
|
||||
type eventHookRegistry struct {
|
||||
mu sync.RWMutex
|
||||
hooks map[uint][]*applicationEventHook
|
||||
}
|
||||
|
||||
var eventHookRegistries sync.Map
|
||||
|
||||
func (em *EventManager) Once(name string, callback func(*CustomEvent)) func() {
|
||||
listener := &customEventListener{callback: callback, counter: 1}
|
||||
em.mu.Lock()
|
||||
em.ensureMapsLocked()
|
||||
em.customListeners[name] = append(em.customListeners[name], listener)
|
||||
em.mu.Unlock()
|
||||
|
||||
return func() {
|
||||
em.mu.Lock()
|
||||
defer em.mu.Unlock()
|
||||
if em.customListeners == nil {
|
||||
return
|
||||
}
|
||||
updated := em.customListeners[name][:0]
|
||||
for _, existing := range em.customListeners[name] {
|
||||
if existing != listener {
|
||||
updated = append(updated, existing)
|
||||
}
|
||||
}
|
||||
em.customListeners[name] = updated
|
||||
}
|
||||
}
|
||||
|
||||
func (em *EventManager) EmitEvent(event *CustomEvent) bool {
|
||||
if event == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
em.mu.Lock()
|
||||
em.ensureMapsLocked()
|
||||
listeners := append([]*customEventListener(nil), em.customListeners[event.Name]...)
|
||||
remaining := em.customListeners[event.Name][:0]
|
||||
for _, listener := range em.customListeners[event.Name] {
|
||||
if listener.counter < 0 {
|
||||
remaining = append(remaining, listener)
|
||||
continue
|
||||
}
|
||||
listener.counter--
|
||||
if listener.counter > 0 {
|
||||
remaining = append(remaining, listener)
|
||||
}
|
||||
}
|
||||
em.customListeners[event.Name] = remaining
|
||||
em.mu.Unlock()
|
||||
|
||||
for _, listener := range listeners {
|
||||
if event.IsCancelled() {
|
||||
break
|
||||
}
|
||||
invokeCustomEventListener(listener, event)
|
||||
}
|
||||
|
||||
return event.IsCancelled()
|
||||
}
|
||||
|
||||
func (em *EventManager) Reset() {
|
||||
em.mu.Lock()
|
||||
if em.customListeners != nil {
|
||||
clear(em.customListeners)
|
||||
}
|
||||
em.mu.Unlock()
|
||||
}
|
||||
|
||||
func (em *EventManager) RegisterApplicationEventHook(eventType events.ApplicationEventType, callback func(*ApplicationEvent)) func() {
|
||||
registry := getEventHookRegistry(em)
|
||||
hook := &applicationEventHook{callback: callback}
|
||||
eventID := uint(eventType)
|
||||
|
||||
registry.mu.Lock()
|
||||
registry.hooks[eventID] = append(registry.hooks[eventID], hook)
|
||||
registry.mu.Unlock()
|
||||
|
||||
return func() {
|
||||
registry.mu.Lock()
|
||||
defer registry.mu.Unlock()
|
||||
updated := registry.hooks[eventID][:0]
|
||||
for _, existing := range registry.hooks[eventID] {
|
||||
if existing != hook {
|
||||
updated = append(updated, existing)
|
||||
}
|
||||
}
|
||||
registry.hooks[eventID] = updated
|
||||
}
|
||||
}
|
||||
|
||||
func (em *EventManager) handleApplicationEvent(event *ApplicationEvent) {
|
||||
if em == nil || event == nil {
|
||||
return
|
||||
}
|
||||
|
||||
registry := getEventHookRegistry(em)
|
||||
registry.mu.RLock()
|
||||
hooks := append([]*applicationEventHook(nil), registry.hooks[event.Id]...)
|
||||
registry.mu.RUnlock()
|
||||
|
||||
for _, hook := range hooks {
|
||||
if event.IsCancelled() {
|
||||
return
|
||||
}
|
||||
if hook == nil || hook.callback == nil {
|
||||
continue
|
||||
}
|
||||
func() {
|
||||
defer func() {
|
||||
_ = recover()
|
||||
}()
|
||||
hook.callback(event)
|
||||
}()
|
||||
}
|
||||
|
||||
em.mu.RLock()
|
||||
listeners := append([]*applicationEventListener(nil), em.appListeners[event.Id]...)
|
||||
em.mu.RUnlock()
|
||||
for _, listener := range listeners {
|
||||
if event.IsCancelled() {
|
||||
return
|
||||
}
|
||||
if listener == nil || listener.callback == nil {
|
||||
continue
|
||||
}
|
||||
func() {
|
||||
defer func() {
|
||||
_ = recover()
|
||||
}()
|
||||
listener.callback(event)
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func getEventHookRegistry(em *EventManager) *eventHookRegistry {
|
||||
if em == nil {
|
||||
return &eventHookRegistry{hooks: make(map[uint][]*applicationEventHook)}
|
||||
}
|
||||
|
||||
registry, ok := eventHookRegistries.Load(em)
|
||||
if ok {
|
||||
return registry.(*eventHookRegistry)
|
||||
}
|
||||
|
||||
newRegistry := &eventHookRegistry{hooks: make(map[uint][]*applicationEventHook)}
|
||||
actual, _ := eventHookRegistries.LoadOrStore(em, newRegistry)
|
||||
return actual.(*eventHookRegistry)
|
||||
}
|
||||
25
stubs/wails/pkg/application/key_binding.go
Normal file
25
stubs/wails/pkg/application/key_binding.go
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
package application
|
||||
|
||||
func newKeyBindingManager() *KeyBindingManager {
|
||||
return &KeyBindingManager{}
|
||||
}
|
||||
|
||||
type windowKeyEvent struct {
|
||||
windowId uint
|
||||
acceleratorString string
|
||||
}
|
||||
|
||||
func (m *KeyBindingManager) Register(accelerator string, callback func(window Window)) {
|
||||
m.Add(accelerator, callback)
|
||||
}
|
||||
|
||||
func (m *KeyBindingManager) Unregister(accelerator string) {
|
||||
m.Remove(accelerator)
|
||||
}
|
||||
|
||||
func (m *KeyBindingManager) handleWindowKeyEvent(event *windowKeyEvent) {
|
||||
if event == nil {
|
||||
return
|
||||
}
|
||||
m.Process(event.acceleratorString, nil)
|
||||
}
|
||||
|
|
@ -2,6 +2,21 @@ package application
|
|||
|
||||
import "sync"
|
||||
|
||||
type Alignment int
|
||||
type OffsetReference int
|
||||
|
||||
const (
|
||||
TOP Alignment = iota
|
||||
RIGHT
|
||||
BOTTOM
|
||||
LEFT
|
||||
)
|
||||
|
||||
const (
|
||||
BEGIN OffsetReference = iota
|
||||
END
|
||||
)
|
||||
|
||||
// Screen describes a physical or logical display.
|
||||
//
|
||||
// primary := manager.GetPrimary()
|
||||
|
|
@ -47,6 +62,14 @@ type Size struct {
|
|||
Height int
|
||||
}
|
||||
|
||||
type ScreenPlacement struct {
|
||||
Screen *Screen
|
||||
Parent *Screen
|
||||
Alignment Alignment
|
||||
Offset int
|
||||
OffsetReference OffsetReference
|
||||
}
|
||||
|
||||
// Origin returns the top-left corner of the rectangle.
|
||||
func (r Rect) Origin() Point {
|
||||
return Point{X: r.X, Y: r.Y}
|
||||
|
|
@ -74,6 +97,43 @@ func (r Rect) RectSize() Size {
|
|||
return Size{Width: r.Width, Height: r.Height}
|
||||
}
|
||||
|
||||
func (r Rect) Size() Size {
|
||||
return r.RectSize()
|
||||
}
|
||||
|
||||
func (s Screen) Origin() Point {
|
||||
return Point{X: s.X, Y: s.Y}
|
||||
}
|
||||
|
||||
func (p ScreenPlacement) Apply() {
|
||||
if p.Screen == nil || p.Parent == nil {
|
||||
return
|
||||
}
|
||||
|
||||
x := p.Parent.X
|
||||
y := p.Parent.Y
|
||||
|
||||
switch p.Alignment {
|
||||
case TOP:
|
||||
x += p.Offset
|
||||
y -= p.Screen.Size.Height
|
||||
case RIGHT:
|
||||
x += p.Parent.Size.Width
|
||||
y += p.Offset
|
||||
case BOTTOM:
|
||||
x += p.Offset
|
||||
y += p.Parent.Size.Height
|
||||
case LEFT:
|
||||
x -= p.Screen.Size.Width
|
||||
y += p.Offset
|
||||
}
|
||||
|
||||
p.Screen.X = x
|
||||
p.Screen.Y = y
|
||||
p.Screen.Bounds.X = x
|
||||
p.Screen.Bounds.Y = y
|
||||
}
|
||||
|
||||
// ScreenManager tracks connected screens and the active screen.
|
||||
//
|
||||
// manager.SetScreens(detectedScreens)
|
||||
|
|
@ -85,6 +145,10 @@ type ScreenManager struct {
|
|||
primary *Screen
|
||||
}
|
||||
|
||||
func newScreenManager() *ScreenManager {
|
||||
return &ScreenManager{}
|
||||
}
|
||||
|
||||
// SetScreens replaces the full list of known screens and recomputes primary.
|
||||
//
|
||||
// manager.SetScreens(platformDetectedScreens)
|
||||
|
|
@ -159,3 +223,85 @@ func (m *ScreenManager) GetCurrent() *Screen {
|
|||
}
|
||||
return m.primary
|
||||
}
|
||||
|
||||
func (m *ScreenManager) LayoutScreens(screens []*Screen) error {
|
||||
m.SetScreens(screens)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ScreenManager) All() []*Screen {
|
||||
return m.GetAll()
|
||||
}
|
||||
|
||||
func (m *ScreenManager) Primary() *Screen {
|
||||
return m.GetPrimary()
|
||||
}
|
||||
|
||||
func (m *ScreenManager) Current() *Screen {
|
||||
return m.GetCurrent()
|
||||
}
|
||||
|
||||
func (m *ScreenManager) DipToPhysicalPoint(dipPoint Point) Point {
|
||||
return dipPoint
|
||||
}
|
||||
|
||||
func (m *ScreenManager) PhysicalToDipPoint(physicalPoint Point) Point {
|
||||
return physicalPoint
|
||||
}
|
||||
|
||||
func (m *ScreenManager) DipToPhysicalRect(dipRect Rect) Rect {
|
||||
return dipRect
|
||||
}
|
||||
|
||||
func (m *ScreenManager) PhysicalToDipRect(physicalRect Rect) Rect {
|
||||
return physicalRect
|
||||
}
|
||||
|
||||
func (m *ScreenManager) ScreenNearestPhysicalPoint(physicalPoint Point) *Screen {
|
||||
return m.screenNearestPoint(physicalPoint)
|
||||
}
|
||||
|
||||
func (m *ScreenManager) ScreenNearestDipPoint(dipPoint Point) *Screen {
|
||||
return m.screenNearestPoint(dipPoint)
|
||||
}
|
||||
|
||||
func (m *ScreenManager) ScreenNearestPhysicalRect(physicalRect Rect) *Screen {
|
||||
return m.screenNearestRect(physicalRect)
|
||||
}
|
||||
|
||||
func (m *ScreenManager) ScreenNearestDipRect(dipRect Rect) *Screen {
|
||||
return m.screenNearestRect(dipRect)
|
||||
}
|
||||
|
||||
func (m *ScreenManager) screenNearestPoint(point Point) *Screen {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
for _, screen := range m.screens {
|
||||
if screen != nil && screen.Bounds.Contains(point) {
|
||||
return screen
|
||||
}
|
||||
}
|
||||
|
||||
if m.current != nil {
|
||||
return m.current
|
||||
}
|
||||
if m.primary != nil {
|
||||
return m.primary
|
||||
}
|
||||
if len(m.screens) > 0 {
|
||||
return m.screens[0]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ScreenManager) screenNearestRect(rect Rect) *Screen {
|
||||
if rect.IsEmpty() {
|
||||
return m.screenNearestPoint(Point{})
|
||||
}
|
||||
return m.screenNearestPoint(rect.Origin())
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue