diff --git a/stubs/wails/internal/operatingsystem/operatingsystem.go b/stubs/wails/internal/operatingsystem/operatingsystem.go new file mode 100644 index 00000000..42996014 --- /dev/null +++ b/stubs/wails/internal/operatingsystem/operatingsystem.go @@ -0,0 +1,8 @@ +package operatingsystem + +// OS is the minimal host metadata exposed by the Wails environment API. +type OS struct { + Name string + Version string + Build string +} diff --git a/stubs/wails/pkg/application/application.go b/stubs/wails/pkg/application/application.go index d54d525f..b77c217a 100644 --- a/stubs/wails/pkg/application/application.go +++ b/stubs/wails/pkg/application/application.go @@ -1,6 +1,7 @@ package application import ( + "log/slog" "sync" "unsafe" @@ -216,9 +217,13 @@ func (c *WindowEventContext) DropTargetDetails() *DropTargetDetails { return &details } -// DropTargetDetails mirrors the fields consumed by the GUI wrappers. +// DropTargetDetails mirrors the Wails drop-target payload. type DropTargetDetails struct { - ElementID string + X int `json:"x"` + Y int `json:"y"` + ElementID string `json:"id"` + ClassList []string `json:"classList"` + Attributes map[string]string `json:"attributes,omitempty"` } // WindowEvent mirrors the event object passed to window callbacks. @@ -873,18 +878,19 @@ func (wm *WindowManager) RemoveByName(name string) bool { // app := &application.App{} // win := app.Window.NewWithOptions(application.WebviewWindowOptions{Title: "Main"}) type App struct { - Logger Logger - Window WindowManager - Menu MenuManager - SystemTray SystemTrayManager - Dialog DialogManager - Event EventManager - Browser BrowserManager - Clipboard ClipboardManager - ContextMenu ContextMenuManager - Environment EnvironmentManager - Screen ScreenManager - KeyBinding KeyBindingManager + Logger *slog.Logger + Window *WindowManager + Menu *MenuManager + SystemTray *SystemTrayManager + Dialog *DialogManager + Event *EventManager + Browser *BrowserManager + Clipboard *ClipboardManager + ContextMenu *ContextMenuManager + Env *EnvironmentManager + Environment *EnvironmentManager + Screen *ScreenManager + KeyBinding *KeyBindingManager } // NewApp creates a zero-config in-memory application stub. diff --git a/stubs/wails/pkg/application/application_options.go b/stubs/wails/pkg/application/application_options.go index fca6f5cd..08362c6b 100644 --- a/stubs/wails/pkg/application/application_options.go +++ b/stubs/wails/pkg/application/application_options.go @@ -1,6 +1,8 @@ package application import ( + "context" + "log/slog" "net/http" "time" ) @@ -43,6 +45,12 @@ type Options struct { // Example: map[uint32]uint32{1: 1411160069} BindAliases map[uint32]uint32 + // Logger is the Wails system logger used by the runtime. + Logger *slog.Logger + + // LogLevel defines the desired system log level when Logger is nil. + LogLevel slog.Level + // Assets configures the embedded asset server. Assets AssetOptions @@ -85,6 +93,9 @@ type Options struct { // SingleInstance configures single-instance enforcement. SingleInstance *SingleInstanceOptions + // Transport configures the IPC transport implementation. + Transport Transport + // Server configures the headless HTTP server (enabled via the "server" build tag). Server ServerOptions } @@ -141,6 +152,27 @@ type AssetOptions struct { // type Middleware func(next http.Handler) http.Handler type Middleware func(next http.Handler) http.Handler +// MessageProcessor is the placeholder type passed to Transport.Start. +// The stub does not perform runtime IPC processing, but the type exists so +// callers can satisfy the same API surface as real Wails. +type MessageProcessor struct{} + +// Transport is the custom IPC transport contract supported by Wails. +// +// type MyTransport struct{} +// func (t *MyTransport) Start(ctx context.Context, processor *application.MessageProcessor) error { return nil } +// func (t *MyTransport) Stop() error { return nil } +type Transport interface { + Start(ctx context.Context, processor *MessageProcessor) error + Stop() error +} + +// AssetServerTransport optionally allows a custom transport to serve app assets. +type AssetServerTransport interface { + Transport + ServeAssets(assetHandler http.Handler) error +} + // ChainMiddleware composes multiple Middleware values into a single Middleware. // chain := application.ChainMiddleware(authMiddleware, loggingMiddleware) func ChainMiddleware(middleware ...Middleware) Middleware { diff --git a/stubs/wails/pkg/application/compat_state.go b/stubs/wails/pkg/application/compat_state.go index 12785681..4145a8e4 100644 --- a/stubs/wails/pkg/application/compat_state.go +++ b/stubs/wails/pkg/application/compat_state.go @@ -2,6 +2,8 @@ package application import ( "context" + "io" + "log/slog" "sync" "time" ) @@ -53,19 +55,18 @@ func ensureAppCompatState(app *App) *appCompatState { if app == nil { return &appCompatState{ctx: context.Background()} } + initialiseAppManagers(app, Options{}) if state, ok := appCompatStates.Load(app); ok { return state.(*appCompatState) } state := &appCompatState{ctx: context.Background()} actual, _ := appCompatStates.LoadOrStore(app, state) - if app.KeyBinding.bindings == nil { - app.KeyBinding = *NewKeyBindingManager() - } return actual.(*appCompatState) } func newStubApp(options Options) *App { app := &App{} + initialiseAppManagers(app, options) state := ensureAppCompatState(app) state.mu.Lock() state.options = options @@ -77,6 +78,59 @@ func newStubApp(options Options) *App { return app } +func initialiseAppManagers(app *App, options Options) { + if app == nil { + return + } + if app.Logger == nil { + if options.Logger != nil { + app.Logger = options.Logger + } else { + handlerOptions := &slog.HandlerOptions{Level: options.LogLevel} + app.Logger = slog.New(slog.NewTextHandler(io.Discard, handlerOptions)) + } + } + if app.Window == nil { + app.Window = &WindowManager{} + } + if app.Menu == nil { + app.Menu = &MenuManager{} + } + if app.SystemTray == nil { + app.SystemTray = &SystemTrayManager{} + } + if app.Dialog == nil { + app.Dialog = &DialogManager{} + } + if app.Event == nil { + app.Event = &EventManager{} + } + if app.Browser == nil { + app.Browser = &BrowserManager{} + } + if app.Clipboard == nil { + app.Clipboard = &ClipboardManager{} + } + if app.ContextMenu == nil { + app.ContextMenu = &ContextMenuManager{} + } + if app.Env == nil && app.Environment != nil { + app.Env = app.Environment + } + if app.Env == nil { + app.Env = &EnvironmentManager{} + } + if app.Environment == nil { + app.Environment = app.Env + } + if app.Screen == nil { + app.Screen = NewScreenManager() + } + if app.KeyBinding == nil { + app.KeyBinding = NewKeyBindingManager() + } +} + func ensureWebviewCompatState(window *WebviewWindow) *webviewCompatState { if state, ok := webviewCompatStates.Load(window); ok { return state.(*webviewCompatState) @@ -107,7 +161,7 @@ func ensureTrayCompatState(tray *SystemTray) *trayCompatState { func appScreens() *ScreenManager { if globalApplication != nil { - return &globalApplication.Screen + return globalApplication.Screen } return defaultScreenManager } diff --git a/stubs/wails/pkg/application/environment.go b/stubs/wails/pkg/application/environment.go index fe9f982e..f3c40a3d 100644 --- a/stubs/wails/pkg/application/environment.go +++ b/stubs/wails/pkg/application/environment.go @@ -1,5 +1,11 @@ package application +import ( + "runtime" + + "github.com/wailsapp/wails/v3/internal/operatingsystem" +) + // EnvironmentInfo holds runtime information about the host OS and build. // // info := manager.Info() @@ -8,6 +14,7 @@ type EnvironmentInfo struct { OS string Arch string Debug bool + OSInfo *operatingsystem.OS PlatformInfo map[string]any } @@ -42,6 +49,9 @@ func (em *EnvironmentManager) GetAccentColor() string { // info := manager.Info() func (em *EnvironmentManager) Info() EnvironmentInfo { return EnvironmentInfo{ + OS: runtime.GOOS, + Arch: runtime.GOARCH, + OSInfo: &operatingsystem.OS{Name: runtime.GOOS, Version: "stub"}, PlatformInfo: make(map[string]any), } } diff --git a/stubs/wails/pkg/application/parity_test.go b/stubs/wails/pkg/application/parity_test.go index 9e5a4eaf..904eeac9 100644 --- a/stubs/wails/pkg/application/parity_test.go +++ b/stubs/wails/pkg/application/parity_test.go @@ -1,16 +1,50 @@ package application -import "testing" +import ( + "context" + "log/slog" + "testing" +) + +type noopTransport struct{} + +func (noopTransport) Start(context.Context, *MessageProcessor) error { return nil } +func (noopTransport) Stop() error { return nil } func TestNewAndGetExposeSingletonState(t *testing.T) { globalApplication = nil - app := New(Options{Name: "Parity"}) + logger := slog.Default() + transport := noopTransport{} + app := New(Options{ + Name: "Parity", + Logger: logger, + LogLevel: slog.LevelDebug, + Transport: transport, + }) if Get() != app { t.Fatalf("Get() did not return singleton app") } if app.Config().Name != "Parity" { t.Fatalf("unexpected app name: %q", app.Config().Name) } + if app.Config().Logger != logger { + t.Fatalf("expected logger to round-trip through config") + } + if app.Config().LogLevel != slog.LevelDebug { + t.Fatalf("expected log level to round-trip through config") + } + if app.Config().Transport == nil { + t.Fatalf("expected transport to round-trip through config") + } + if app.Logger == nil { + t.Fatalf("expected app logger to be initialised") + } + if app.Env == nil { + t.Fatalf("expected Env manager to be initialised") + } + if app.Environment != app.Env { + t.Fatalf("expected Environment alias to point at Env manager") + } if err := app.Run(); err != nil { t.Fatalf("Run() failed: %v", err) } @@ -61,3 +95,35 @@ func TestMenuAndWindowParityHelpers(t *testing.T) { t.Fatalf("expected devtools to be marked closed") } } + +func TestEnvironmentAndDropTargetParity(t *testing.T) { + info := (&EnvironmentManager{}).Info() + if info.OS == "" || info.Arch == "" { + t.Fatalf("expected runtime environment metadata, got %+v", info) + } + if info.OSInfo == nil { + t.Fatalf("expected OSInfo to be populated") + } + + ctx := &WindowEventContext{ + dropDetails: &DropTargetDetails{ + X: 10, + Y: 20, + ElementID: "dropzone", + ClassList: []string{"primary", "drop-target"}, + Attributes: map[string]string{ + "data-file-drop-target": "true", + }, + }, + } + details := ctx.DropTargetDetails() + if details == nil { + t.Fatalf("expected drop target details") + } + if details.X != 10 || details.Y != 20 || details.ElementID != "dropzone" { + t.Fatalf("unexpected drop details: %+v", details) + } + if len(details.ClassList) != 2 || details.Attributes["data-file-drop-target"] != "true" { + t.Fatalf("drop target metadata was not preserved: %+v", details) + } +}