refactor(ax): standardise display and window contracts
This commit is contained in:
parent
552b0751b9
commit
7deb91ac0b
14 changed files with 258 additions and 246 deletions
|
|
@ -3,8 +3,8 @@ package contextmenu
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
"forge.lthn.ai/core/go/pkg/core"
|
||||
)
|
||||
|
||||
|
|
@ -84,7 +84,7 @@ func (s *Service) taskAdd(t TaskAdd) error {
|
|||
})
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("contextmenu: platform add failed: %w", err)
|
||||
return coreerr.E("contextmenu.taskAdd", "platform add failed", err)
|
||||
}
|
||||
|
||||
s.registeredMenus[t.Name] = t.Menu
|
||||
|
|
@ -98,7 +98,7 @@ func (s *Service) taskRemove(t TaskRemove) error {
|
|||
|
||||
err := s.platform.Remove(t.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("contextmenu: platform remove failed: %w", err)
|
||||
return coreerr.E("contextmenu.taskRemove", "platform remove failed", err)
|
||||
}
|
||||
|
||||
delete(s.registeredMenus, t.Name)
|
||||
|
|
|
|||
|
|
@ -1,43 +1,30 @@
|
|||
# Display
|
||||
|
||||
This repository is a display module for the core web3 framework. It includes a Go backend, an Angular custom element, and a full release cycle configuration.
|
||||
`pkg/display` is the Core GUI display service. It owns window orchestration, layouts, menus, system tray state, dialogs, notifications, and the IPC bridge to the frontend.
|
||||
|
||||
## Getting Started
|
||||
## Working Locally
|
||||
|
||||
1. **Clone the repository:**
|
||||
```bash
|
||||
git clone https://github.com/Snider/display.git
|
||||
```
|
||||
1. Run the backend tests:
|
||||
```bash
|
||||
go test ./pkg/display/...
|
||||
```
|
||||
2. Run the full workspace tests when you touch IPC contracts:
|
||||
```bash
|
||||
go test ./...
|
||||
```
|
||||
3. Build the Angular frontend:
|
||||
```bash
|
||||
cd ui
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
2. **Install the dependencies:**
|
||||
```bash
|
||||
cd display
|
||||
go mod tidy
|
||||
cd ui
|
||||
npm install
|
||||
```
|
||||
## Declarative Windows
|
||||
|
||||
3. **Run the development server:**
|
||||
```bash
|
||||
go run ./cmd/demo-cli serve
|
||||
```
|
||||
This will start the Go backend and serve the Angular custom element.
|
||||
Windows are created from a `window.Window` spec instead of a fluent option chain:
|
||||
|
||||
## Building the Custom Element
|
||||
|
||||
To build the Angular custom element, run the following command:
|
||||
|
||||
```bash
|
||||
cd ui
|
||||
npm run build
|
||||
```go
|
||||
svc.OpenWindow(window.Window{Name: "editor", Title: "Editor", URL: "/#/editor"})
|
||||
```
|
||||
|
||||
This will create a single JavaScript file in the `dist` directory that you can use in any HTML page.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please feel free to submit a Pull Request.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the EUPL-1.2 License - see the [LICENSE](LICENSE) file for details.
|
||||
The same spec is used by `CreateWindow`, layout restore, tiling, snapping, and workflow presets.
|
||||
|
|
|
|||
|
|
@ -578,11 +578,22 @@ func (s *Service) persistSection(key string, value map[string]any) {
|
|||
|
||||
// --- Service accessors ---
|
||||
|
||||
func (s *Service) performWindowTask(operation string, task core.Task) (any, error) {
|
||||
result, handled, err := s.Core().PERFORM(task)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !handled {
|
||||
return nil, coreerr.E(operation, "window service not available", nil)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// --- Window Management (delegates via IPC) ---
|
||||
|
||||
// OpenWindow creates a new window from a declarative Window spec.
|
||||
func (s *Service) OpenWindow(spec window.Window) error {
|
||||
_, _, err := s.Core().PERFORM(window.TaskOpenWindow{Window: spec})
|
||||
_, err := s.performWindowTask("display.OpenWindow", window.TaskOpenWindow{Window: spec})
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -603,7 +614,7 @@ func (s *Service) GetWindowInfo(name string) (*window.WindowInfo, error) {
|
|||
func (s *Service) ListWindowInfos() []window.WindowInfo {
|
||||
result, handled, _ := s.Core().QUERY(window.QueryWindowList{})
|
||||
if !handled {
|
||||
return nil
|
||||
return []window.WindowInfo{}
|
||||
}
|
||||
list, _ := result.([]window.WindowInfo)
|
||||
return list
|
||||
|
|
@ -611,82 +622,82 @@ func (s *Service) ListWindowInfos() []window.WindowInfo {
|
|||
|
||||
// SetWindowPosition moves a window via IPC.
|
||||
func (s *Service) SetWindowPosition(name string, x, y int) error {
|
||||
_, _, err := s.Core().PERFORM(window.TaskSetPosition{Name: name, X: x, Y: y})
|
||||
_, err := s.performWindowTask("display.SetWindowPosition", window.TaskSetPosition{Name: name, X: x, Y: y})
|
||||
return err
|
||||
}
|
||||
|
||||
// SetWindowSize resizes a window via IPC.
|
||||
func (s *Service) SetWindowSize(name string, width, height int) error {
|
||||
_, _, err := s.Core().PERFORM(window.TaskSetSize{Name: name, Width: width, Height: height})
|
||||
_, err := s.performWindowTask("display.SetWindowSize", window.TaskSetSize{Name: name, Width: width, Height: height})
|
||||
return err
|
||||
}
|
||||
|
||||
// SetWindowBounds sets both position and size of a window via IPC.
|
||||
func (s *Service) SetWindowBounds(name string, x, y, width, height int) error {
|
||||
if _, _, err := s.Core().PERFORM(window.TaskSetPosition{Name: name, X: x, Y: y}); err != nil {
|
||||
if _, err := s.performWindowTask("display.SetWindowBounds", window.TaskSetPosition{Name: name, X: x, Y: y}); err != nil {
|
||||
return err
|
||||
}
|
||||
_, _, err := s.Core().PERFORM(window.TaskSetSize{Name: name, Width: width, Height: height})
|
||||
_, err := s.performWindowTask("display.SetWindowBounds", window.TaskSetSize{Name: name, Width: width, Height: height})
|
||||
return err
|
||||
}
|
||||
|
||||
// MaximizeWindow maximizes a window via IPC.
|
||||
func (s *Service) MaximizeWindow(name string) error {
|
||||
_, _, err := s.Core().PERFORM(window.TaskMaximise{Name: name})
|
||||
_, err := s.performWindowTask("display.MaximizeWindow", window.TaskMaximise{Name: name})
|
||||
return err
|
||||
}
|
||||
|
||||
// MinimizeWindow minimizes a window via IPC.
|
||||
func (s *Service) MinimizeWindow(name string) error {
|
||||
_, _, err := s.Core().PERFORM(window.TaskMinimise{Name: name})
|
||||
_, err := s.performWindowTask("display.MinimizeWindow", window.TaskMinimise{Name: name})
|
||||
return err
|
||||
}
|
||||
|
||||
// FocusWindow brings a window to the front via IPC.
|
||||
func (s *Service) FocusWindow(name string) error {
|
||||
_, _, err := s.Core().PERFORM(window.TaskFocus{Name: name})
|
||||
_, err := s.performWindowTask("display.FocusWindow", window.TaskFocus{Name: name})
|
||||
return err
|
||||
}
|
||||
|
||||
// CloseWindow closes a window via IPC.
|
||||
func (s *Service) CloseWindow(name string) error {
|
||||
_, _, err := s.Core().PERFORM(window.TaskCloseWindow{Name: name})
|
||||
_, err := s.performWindowTask("display.CloseWindow", window.TaskCloseWindow{Name: name})
|
||||
return err
|
||||
}
|
||||
|
||||
// RestoreWindow restores a maximized/minimized window.
|
||||
func (s *Service) RestoreWindow(name string) error {
|
||||
_, _, err := s.Core().PERFORM(window.TaskRestore{Name: name})
|
||||
_, err := s.performWindowTask("display.RestoreWindow", window.TaskRestore{Name: name})
|
||||
return err
|
||||
}
|
||||
|
||||
// SetWindowVisibility shows or hides a window.
|
||||
func (s *Service) SetWindowVisibility(name string, visible bool) error {
|
||||
_, _, err := s.Core().PERFORM(window.TaskSetVisibility{Name: name, Visible: visible})
|
||||
_, err := s.performWindowTask("display.SetWindowVisibility", window.TaskSetVisibility{Name: name, Visible: visible})
|
||||
return err
|
||||
}
|
||||
|
||||
// SetWindowAlwaysOnTop sets whether a window stays on top.
|
||||
func (s *Service) SetWindowAlwaysOnTop(name string, alwaysOnTop bool) error {
|
||||
_, _, err := s.Core().PERFORM(window.TaskSetAlwaysOnTop{Name: name, AlwaysOnTop: alwaysOnTop})
|
||||
_, err := s.performWindowTask("display.SetWindowAlwaysOnTop", window.TaskSetAlwaysOnTop{Name: name, AlwaysOnTop: alwaysOnTop})
|
||||
return err
|
||||
}
|
||||
|
||||
// SetWindowTitle changes a window's title.
|
||||
func (s *Service) SetWindowTitle(name string, title string) error {
|
||||
_, _, err := s.Core().PERFORM(window.TaskSetTitle{Name: name, Title: title})
|
||||
_, err := s.performWindowTask("display.SetWindowTitle", window.TaskSetTitle{Name: name, Title: title})
|
||||
return err
|
||||
}
|
||||
|
||||
// SetWindowFullscreen sets a window to fullscreen mode.
|
||||
func (s *Service) SetWindowFullscreen(name string, fullscreen bool) error {
|
||||
_, _, err := s.Core().PERFORM(window.TaskFullscreen{Name: name, Fullscreen: fullscreen})
|
||||
_, err := s.performWindowTask("display.SetWindowFullscreen", window.TaskFullscreen{Name: name, Fullscreen: fullscreen})
|
||||
return err
|
||||
}
|
||||
|
||||
// SetWindowBackgroundColour sets the background colour of a window.
|
||||
func (s *Service) SetWindowBackgroundColour(name string, r, g, b, a uint8) error {
|
||||
_, _, err := s.Core().PERFORM(window.TaskSetBackgroundColour{
|
||||
_, err := s.performWindowTask("display.SetWindowBackgroundColour", window.TaskSetBackgroundColour{
|
||||
Name: name, Red: r, Green: g, Blue: b, Alpha: a,
|
||||
})
|
||||
return err
|
||||
|
|
@ -717,19 +728,19 @@ func (s *Service) GetWindowTitle(name string) (string, error) {
|
|||
|
||||
// ResetWindowState clears saved window positions.
|
||||
func (s *Service) ResetWindowState() error {
|
||||
_, _, _ = s.Core().PERFORM(window.TaskResetWindowState{})
|
||||
return nil
|
||||
_, err := s.performWindowTask("display.ResetWindowState", window.TaskResetWindowState{})
|
||||
return err
|
||||
}
|
||||
|
||||
// GetSavedWindowStates returns all saved window states.
|
||||
func (s *Service) GetSavedWindowStates() map[string]window.WindowState {
|
||||
result, handled, _ := s.Core().QUERY(window.QuerySavedWindowStates{})
|
||||
if !handled {
|
||||
return nil
|
||||
return map[string]window.WindowState{}
|
||||
}
|
||||
saved, _ := result.(map[string]window.WindowState)
|
||||
if saved == nil {
|
||||
return nil
|
||||
return map[string]window.WindowState{}
|
||||
}
|
||||
out := make(map[string]window.WindowState, len(saved))
|
||||
for name, state := range saved {
|
||||
|
|
@ -743,13 +754,16 @@ func (s *Service) CreateWindow(spec window.Window) (*window.WindowInfo, error) {
|
|||
if spec.Name == "" {
|
||||
return nil, coreerr.E("display.CreateWindow", "window name is required", nil)
|
||||
}
|
||||
result, _, err := s.Core().PERFORM(window.TaskOpenWindow{
|
||||
result, err := s.performWindowTask("display.CreateWindow", window.TaskOpenWindow{
|
||||
Window: spec,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
info := result.(window.WindowInfo)
|
||||
info, ok := result.(window.WindowInfo)
|
||||
if !ok {
|
||||
return nil, coreerr.E("display.CreateWindow", "unexpected result type from window create task", nil)
|
||||
}
|
||||
return &info, nil
|
||||
}
|
||||
|
||||
|
|
@ -757,13 +771,13 @@ func (s *Service) CreateWindow(spec window.Window) (*window.WindowInfo, error) {
|
|||
|
||||
// SaveLayout saves the current window arrangement as a named layout.
|
||||
func (s *Service) SaveLayout(name string) error {
|
||||
_, _, err := s.Core().PERFORM(window.TaskSaveLayout{Name: name})
|
||||
_, err := s.performWindowTask("display.SaveLayout", window.TaskSaveLayout{Name: name})
|
||||
return err
|
||||
}
|
||||
|
||||
// RestoreLayout applies a saved layout.
|
||||
func (s *Service) RestoreLayout(name string) error {
|
||||
_, _, err := s.Core().PERFORM(window.TaskRestoreLayout{Name: name})
|
||||
_, err := s.performWindowTask("display.RestoreLayout", window.TaskRestoreLayout{Name: name})
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -771,7 +785,7 @@ func (s *Service) RestoreLayout(name string) error {
|
|||
func (s *Service) ListLayouts() []window.LayoutInfo {
|
||||
result, handled, _ := s.Core().QUERY(window.QueryLayoutList{})
|
||||
if !handled {
|
||||
return nil
|
||||
return []window.LayoutInfo{}
|
||||
}
|
||||
layouts, _ := result.([]window.LayoutInfo)
|
||||
return layouts
|
||||
|
|
@ -779,7 +793,7 @@ func (s *Service) ListLayouts() []window.LayoutInfo {
|
|||
|
||||
// DeleteLayout removes a saved layout by name.
|
||||
func (s *Service) DeleteLayout(name string) error {
|
||||
_, _, err := s.Core().PERFORM(window.TaskDeleteLayout{Name: name})
|
||||
_, err := s.performWindowTask("display.DeleteLayout", window.TaskDeleteLayout{Name: name})
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -797,25 +811,25 @@ func (s *Service) GetLayout(name string) *window.Layout {
|
|||
|
||||
// TileWindows arranges windows in a tiled layout.
|
||||
func (s *Service) TileWindows(mode window.TileMode, windowNames []string) error {
|
||||
_, _, err := s.Core().PERFORM(window.TaskTileWindows{Mode: mode.String(), Windows: windowNames})
|
||||
_, err := s.performWindowTask("display.TileWindows", window.TaskTileWindows{Mode: mode.String(), Windows: windowNames})
|
||||
return err
|
||||
}
|
||||
|
||||
// SnapWindow snaps a window to a screen edge or corner.
|
||||
func (s *Service) SnapWindow(name string, position window.SnapPosition) error {
|
||||
_, _, err := s.Core().PERFORM(window.TaskSnapWindow{Name: name, Position: position.String()})
|
||||
_, err := s.performWindowTask("display.SnapWindow", window.TaskSnapWindow{Name: name, Position: position.String()})
|
||||
return err
|
||||
}
|
||||
|
||||
// StackWindows arranges windows in a cascade pattern.
|
||||
func (s *Service) StackWindows(windowNames []string, offsetX, offsetY int) error {
|
||||
_, _, err := s.Core().PERFORM(window.TaskStackWindows{Windows: windowNames, OffsetX: offsetX, OffsetY: offsetY})
|
||||
_, err := s.performWindowTask("display.StackWindows", window.TaskStackWindows{Windows: windowNames, OffsetX: offsetX, OffsetY: offsetY})
|
||||
return err
|
||||
}
|
||||
|
||||
// ApplyWorkflowLayout applies a predefined layout for a specific workflow.
|
||||
func (s *Service) ApplyWorkflowLayout(workflow window.WorkflowLayout) error {
|
||||
_, _, err := s.Core().PERFORM(window.TaskApplyWorkflow{
|
||||
_, err := s.performWindowTask("display.ApplyWorkflowLayout", window.TaskApplyWorkflow{
|
||||
Workflow: workflow.String(),
|
||||
})
|
||||
return err
|
||||
|
|
@ -878,15 +892,15 @@ func (s *Service) handleNewWorkspace() {
|
|||
}
|
||||
|
||||
func (s *Service) handleListWorkspaces() {
|
||||
ws := s.Core().Service("workspace")
|
||||
if ws == nil {
|
||||
workspaceService := s.Core().Service("workspace")
|
||||
if workspaceService == nil {
|
||||
return
|
||||
}
|
||||
lister, ok := ws.(interface{ ListWorkspaces() []string })
|
||||
workspaceLister, ok := workspaceService.(interface{ ListWorkspaces() []string })
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
_ = lister.ListWorkspaces()
|
||||
_ = workspaceLister.ListWorkspaces()
|
||||
}
|
||||
|
||||
func (s *Service) handleNewFile() {
|
||||
|
|
|
|||
|
|
@ -389,6 +389,14 @@ func TestCreateWindow_Bad(t *testing.T) {
|
|||
assert.Contains(t, err.Error(), "window name is required")
|
||||
}
|
||||
|
||||
func TestCreateWindow_Bad_NoWindowService(t *testing.T) {
|
||||
svc, _ := newTestDisplayService(t)
|
||||
|
||||
_, err := svc.CreateWindow(window.Window{Name: "orphan-window"})
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "window service not available")
|
||||
}
|
||||
|
||||
func TestResetWindowState_Good(t *testing.T) {
|
||||
c := newTestConclave(t)
|
||||
svc := core.MustServiceFor[*Service](c, "display")
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Backend Documentation
|
||||
|
||||
The backend is written in Go and uses the `github.com/Snider/display` package. It utilizes the Wails v3 framework to bridge Go and the web frontend.
|
||||
The backend is written in Go and uses the `forge.lthn.ai/core/gui/pkg/display` package. It uses Wails v3 to bridge Go and the web frontend.
|
||||
|
||||
## Core Types
|
||||
|
||||
|
|
@ -9,46 +9,53 @@ The `Service` struct is the main entry point for the display logic.
|
|||
|
||||
- **Initialization:**
|
||||
- `New() (*Service, error)`: Creates a new instance of the service.
|
||||
- `Startup(ctx context.Context) error`: Initializes the Wails application, builds the menu, sets up the system tray, and opens the main window.
|
||||
- `Register(wailsApp *application.App) func(*core.Core) (any, error)`: Captures the Wails app and registers the service with Core.
|
||||
- `OnStartup(ctx context.Context) error`: Loads config and registers IPC handlers.
|
||||
|
||||
- **Window Management:**
|
||||
- `OpenWindow(opts ...WindowOption) error`: Opens a new window with the specified options.
|
||||
- `OpenWindow(spec window.Window) error`: Opens a new window from a declarative spec.
|
||||
- `CreateWindow(spec window.Window) (*window.WindowInfo, error)`: Opens a window and returns its info.
|
||||
- `GetWindowInfo(name string) (*window.WindowInfo, error)`: Queries a single window.
|
||||
- `ListWindowInfos() []window.WindowInfo`: Queries all tracked windows.
|
||||
- `SetWindowPosition(name string, x, y int) error`
|
||||
- `SetWindowSize(name string, width, height int) error`
|
||||
- `SetWindowBounds(name string, x, y, width, height int) error`
|
||||
- `MaximizeWindow(name string) error`
|
||||
- `MinimizeWindow(name string) error`
|
||||
- `RestoreWindow(name string) error`
|
||||
- `FocusWindow(name string) error`
|
||||
- `SetWindowFullscreen(name string, fullscreen bool) error`
|
||||
- `SetWindowAlwaysOnTop(name string, alwaysOnTop bool) error`
|
||||
- `SetWindowVisibility(name string, visible bool) error`
|
||||
- `SetWindowTitle(name, title string) error`
|
||||
- `SetWindowBackgroundColour(name string, r, g, b, a uint8) error`
|
||||
- `GetFocusedWindow() string`
|
||||
- `ResetWindowState() error`
|
||||
- `GetSavedWindowStates() map[string]window.WindowState`
|
||||
|
||||
- **Dialogs:**
|
||||
- `ShowEnvironmentDialog()`: Displays a native dialog containing information about the runtime environment (OS, Arch, Debug mode, etc.).
|
||||
Example:
|
||||
|
||||
### `WindowConfig` & `WindowOption`
|
||||
Window configuration is handled using the Functional Options pattern. The `WindowConfig` struct holds parameters like:
|
||||
- `Name`, `Title`
|
||||
- `Width`, `Height`
|
||||
- `URL`
|
||||
- `AlwaysOnTop`, `Hidden`, `Frameless`
|
||||
- Window button states (`MinimiseButtonState`, `MaximiseButtonState`, `CloseButtonState`)
|
||||
|
||||
**Available Options:**
|
||||
- `WithName(name string)`
|
||||
- `WithTitle(title string)`
|
||||
- `WithWidth(width int)`
|
||||
- `WithHeight(height int)`
|
||||
- `WithURL(url string)`
|
||||
- `WithAlwaysOnTop(bool)`
|
||||
- `WithHidden(bool)`
|
||||
- `WithFrameless(bool)`
|
||||
- `WithMinimiseButtonState(state)`
|
||||
- `WithMaximiseButtonState(state)`
|
||||
- `WithCloseButtonState(state)`
|
||||
```go
|
||||
svc.OpenWindow(window.Window{
|
||||
Name: "editor",
|
||||
Title: "Editor",
|
||||
URL: "/#/editor",
|
||||
Width: 1200,
|
||||
Height: 800,
|
||||
})
|
||||
```
|
||||
|
||||
## Subsystems
|
||||
|
||||
### Menu (`menu.go`)
|
||||
The `buildMenu` method constructs the application's main menu, adding standard roles like File, Edit, Window, and Help. It allows for platform-specific adjustments (e.g., AppMenu on macOS).
|
||||
The `buildMenu()` method constructs the application's main menu, adding standard roles like File, Edit, Window, and Help. It also dispatches the app-specific developer actions used by the frontend.
|
||||
|
||||
### System Tray (`tray.go`)
|
||||
The `systemTray` method initializes the system tray icon and its context menu. It supports:
|
||||
The `setupTray()` method initializes the system tray icon and its context menu. It supports:
|
||||
- Showing/Hiding all windows.
|
||||
- Displaying environment info.
|
||||
- Quitting the application.
|
||||
- Attaching a hidden window for advanced tray interactions.
|
||||
- Attaching tray actions that are broadcast as IPC events.
|
||||
|
||||
### Actions (`actions.go`)
|
||||
Defines structured messages for Inter-Process Communication (IPC) or internal event handling, such as `ActionOpenWindow` which wraps `application.WebviewWindowOptions`.
|
||||
### Actions (`messages.go`)
|
||||
Defines structured messages for Inter-Process Communication (IPC) and internal event handling, including `window.ActionWindowOpened`, `window.ActionWindowClosed`, and `display.ActionIDECommand`.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Development Guide
|
||||
|
||||
This guide covers how to set up the development environment, build the project, and run the demo.
|
||||
This guide covers how to set up the development environment, build the project, and run the tests.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
|
|
@ -12,11 +12,7 @@ This guide covers how to set up the development environment, build the project,
|
|||
|
||||
## Setup
|
||||
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
git clone https://github.com/Snider/display.git
|
||||
cd display
|
||||
```
|
||||
1. Clone the repository and enter the workspace.
|
||||
|
||||
2. Install Go dependencies:
|
||||
```bash
|
||||
|
|
@ -30,23 +26,6 @@ This guide covers how to set up the development environment, build the project,
|
|||
cd ..
|
||||
```
|
||||
|
||||
## Running the Demo
|
||||
|
||||
The project includes a CLI to facilitate development.
|
||||
|
||||
### Serve Mode (Web Preview)
|
||||
To start a simple HTTP server that serves the frontend and a mock API:
|
||||
|
||||
1. Build the frontend first:
|
||||
```bash
|
||||
cd ui && npm run build && cd ..
|
||||
```
|
||||
2. Run the serve command:
|
||||
```bash
|
||||
go run ./cmd/demo-cli serve
|
||||
```
|
||||
Access the app at `http://localhost:8080`.
|
||||
|
||||
## Building the Project
|
||||
|
||||
### Frontend
|
||||
|
|
@ -56,9 +35,9 @@ npm run build
|
|||
```
|
||||
|
||||
### Backend / Application
|
||||
This project is a library/module. However, it can be tested via the demo CLI or by integrating it into a Wails application entry point.
|
||||
This package is exercised through Go tests and the host application that embeds it.
|
||||
|
||||
To run the tests:
|
||||
```bash
|
||||
go test ./...
|
||||
go test ./pkg/display/...
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,25 +1,24 @@
|
|||
# Overview
|
||||
|
||||
The `display` module is a core component responsible for the visual presentation and system integration of the application. It leverages **Wails v3** to create a desktop application backend in Go and **Angular** for the frontend user interface.
|
||||
The `display` module is the Core GUI surface. It coordinates windows, menus, trays, dialogs, notifications, and WebSocket events through **Wails v3** on the Go side and **Angular** in `ui/` on the frontend side.
|
||||
|
||||
## Architecture
|
||||
|
||||
The project consists of two main parts:
|
||||
|
||||
1. **Backend (Go):** Handles window management, system tray integration, application menus, and communication with the frontend. It is located in the root directory and packaged as a Go module.
|
||||
2. **Frontend (Angular):** Provides the user interface. It is located in the `ui/` directory and is built as a custom element that interacts with the backend.
|
||||
1. **Backend (Go):** Handles window management, tray/menu setup, dialogs, notifications, layout persistence, and IPC dispatch.
|
||||
2. **Frontend (Angular):** Provides the user interface. It lives in `ui/` and is built as a custom element that talks to the backend through the Wails runtime.
|
||||
|
||||
## Key Components
|
||||
|
||||
### Display Service (`display`)
|
||||
The core service that manages the application lifecycle. It wraps the Wails application instance and exposes methods to:
|
||||
- Open and configure windows.
|
||||
- Manage the system tray.
|
||||
- Show system dialogs (e.g., environment info).
|
||||
The core service manages the application lifecycle and exposes declarative operations such as:
|
||||
- `OpenWindow(window.Window{Name: "editor", URL: "/#/editor"})`
|
||||
- `SaveLayout("coding")`
|
||||
- `TileWindows(window.TileModeLeftRight, []string{"editor", "terminal"})`
|
||||
- `ApplyWorkflowLayout(window.WorkflowCoding)`
|
||||
|
||||
### System Integration
|
||||
- **Menu:** A standard application menu (File, Edit, View, etc.) is constructed in `menu.go`.
|
||||
- **System Tray:** A system tray icon and menu are configured in `tray.go`, allowing quick access to common actions like showing/hiding windows or viewing environment info.
|
||||
|
||||
### Demo CLI
|
||||
A command-line interface (`cmd/demo-cli`) is provided to run and test the display module. It includes a `serve` command for web-based development.
|
||||
- **Menu:** The application menu is constructed in `buildMenu()` and dispatched through IPC.
|
||||
- **System Tray:** The tray menu is configured in `setupTray()` and keeps the desktop surface in sync with the runtime.
|
||||
- **Events:** Window, theme, screen, lifecycle, and tray actions are broadcast as WebSocket events for the frontend.
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ package mcp
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
"forge.lthn.ai/core/gui/pkg/notification"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
|
@ -46,7 +46,7 @@ func (s *Subsystem) notificationPermissionRequest(_ context.Context, _ *mcp.Call
|
|||
}
|
||||
granted, ok := result.(bool)
|
||||
if !ok {
|
||||
return nil, NotificationPermissionRequestOutput{}, fmt.Errorf("unexpected result type from notification permission request")
|
||||
return nil, NotificationPermissionRequestOutput{}, coreerr.E("mcp.notificationPermissionRequest", "unexpected result type from notification permission request", nil)
|
||||
}
|
||||
return nil, NotificationPermissionRequestOutput{Granted: granted}, nil
|
||||
}
|
||||
|
|
@ -65,7 +65,7 @@ func (s *Subsystem) notificationPermissionCheck(_ context.Context, _ *mcp.CallTo
|
|||
}
|
||||
status, ok := result.(notification.PermissionStatus)
|
||||
if !ok {
|
||||
return nil, NotificationPermissionCheckOutput{}, fmt.Errorf("unexpected result type from notification permission check")
|
||||
return nil, NotificationPermissionCheckOutput{}, coreerr.E("mcp.notificationPermissionCheck", "unexpected result type from notification permission check", nil)
|
||||
}
|
||||
return nil, NotificationPermissionCheckOutput{Granted: status.Granted}, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ package mcp
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
"forge.lthn.ai/core/gui/pkg/window"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
|
@ -23,7 +23,7 @@ func (s *Subsystem) windowList(_ context.Context, _ *mcp.CallToolRequest, _ Wind
|
|||
}
|
||||
windows, ok := result.([]window.WindowInfo)
|
||||
if !ok {
|
||||
return nil, WindowListOutput{}, fmt.Errorf("unexpected result type from window list query")
|
||||
return nil, WindowListOutput{}, coreerr.E("mcp.windowList", "unexpected result type from window list query", nil)
|
||||
}
|
||||
return nil, WindowListOutput{Windows: windows}, nil
|
||||
}
|
||||
|
|
@ -44,7 +44,7 @@ func (s *Subsystem) windowGet(_ context.Context, _ *mcp.CallToolRequest, input W
|
|||
}
|
||||
info, ok := result.(*window.WindowInfo)
|
||||
if !ok {
|
||||
return nil, WindowGetOutput{}, fmt.Errorf("unexpected result type from window get query")
|
||||
return nil, WindowGetOutput{}, coreerr.E("mcp.windowGet", "unexpected result type from window get query", nil)
|
||||
}
|
||||
return nil, WindowGetOutput{Window: info}, nil
|
||||
}
|
||||
|
|
@ -63,7 +63,7 @@ func (s *Subsystem) windowFocused(_ context.Context, _ *mcp.CallToolRequest, _ W
|
|||
}
|
||||
windows, ok := result.([]window.WindowInfo)
|
||||
if !ok {
|
||||
return nil, WindowFocusedOutput{}, fmt.Errorf("unexpected result type from window list query")
|
||||
return nil, WindowFocusedOutput{}, coreerr.E("mcp.windowFocused", "unexpected result type from window list query", nil)
|
||||
}
|
||||
for _, w := range windows {
|
||||
if w.Focused {
|
||||
|
|
@ -88,7 +88,7 @@ func (s *Subsystem) windowCreate(_ context.Context, _ *mcp.CallToolRequest, inpu
|
|||
}
|
||||
info, ok := result.(window.WindowInfo)
|
||||
if !ok {
|
||||
return nil, WindowCreateOutput{}, fmt.Errorf("unexpected result type from window create task")
|
||||
return nil, WindowCreateOutput{}, coreerr.E("mcp.windowCreate", "unexpected result type from window create task", nil)
|
||||
}
|
||||
return nil, WindowCreateOutput{Window: info}, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
// pkg/systray/menu.go
|
||||
package systray
|
||||
|
||||
import "fmt"
|
||||
import coreerr "forge.lthn.ai/core/go-log"
|
||||
|
||||
// SetMenu sets a dynamic menu on the tray from TrayMenuItem descriptors.
|
||||
func (m *Manager) SetMenu(items []TrayMenuItem) error {
|
||||
if m.tray == nil {
|
||||
return fmt.Errorf("tray not initialised")
|
||||
return coreerr.E("systray.Manager.SetMenu", "tray not initialised", nil)
|
||||
}
|
||||
menu := m.platform.NewMenu()
|
||||
m.buildMenu(menu, items)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
|
|
@ -133,6 +134,9 @@ func (lm *LayoutManager) ListLayouts() []LayoutInfo {
|
|||
CreatedAt: l.CreatedAt, UpdatedAt: l.UpdatedAt,
|
||||
})
|
||||
}
|
||||
sort.Slice(infos, func(i, j int) bool {
|
||||
return infos[i].Name < infos[j].Name
|
||||
})
|
||||
return infos
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ package window
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
"forge.lthn.ai/core/go/pkg/core"
|
||||
"forge.lthn.ai/core/gui/pkg/screen"
|
||||
)
|
||||
|
|
@ -49,6 +49,14 @@ func (s *Service) applyConfig(configData map[string]any) {
|
|||
}
|
||||
}
|
||||
|
||||
func (s *Service) requireWindow(name string, operation string) (PlatformWindow, error) {
|
||||
platformWindow, ok := s.manager.Get(name)
|
||||
if !ok {
|
||||
return nil, coreerr.E(operation, "window not found: "+name, nil)
|
||||
}
|
||||
return platformWindow, nil
|
||||
}
|
||||
|
||||
func (s *Service) HandleIPCEvents(c *core.Core, msg core.Message) error {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -78,13 +86,13 @@ func (s *Service) queryWindowList() []WindowInfo {
|
|||
names := s.manager.List()
|
||||
result := make([]WindowInfo, 0, len(names))
|
||||
for _, name := range names {
|
||||
if pw, ok := s.manager.Get(name); ok {
|
||||
x, y := pw.Position()
|
||||
w, h := pw.Size()
|
||||
if platformWindow, ok := s.manager.Get(name); ok {
|
||||
x, y := platformWindow.Position()
|
||||
width, height := platformWindow.Size()
|
||||
result = append(result, WindowInfo{
|
||||
Name: name, Title: pw.Title(), X: x, Y: y, Width: w, Height: h,
|
||||
Maximized: pw.IsMaximised(),
|
||||
Focused: pw.IsFocused(),
|
||||
Name: name, Title: platformWindow.Title(), X: x, Y: y, Width: width, Height: height,
|
||||
Maximized: platformWindow.IsMaximised(),
|
||||
Focused: platformWindow.IsFocused(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -92,16 +100,16 @@ func (s *Service) queryWindowList() []WindowInfo {
|
|||
}
|
||||
|
||||
func (s *Service) queryWindowByName(name string) *WindowInfo {
|
||||
pw, ok := s.manager.Get(name)
|
||||
platformWindow, ok := s.manager.Get(name)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
x, y := pw.Position()
|
||||
w, h := pw.Size()
|
||||
x, y := platformWindow.Position()
|
||||
width, height := platformWindow.Size()
|
||||
return &WindowInfo{
|
||||
Name: name, Title: pw.Title(), X: x, Y: y, Width: w, Height: h,
|
||||
Maximized: pw.IsMaximised(),
|
||||
Focused: pw.IsFocused(),
|
||||
Name: name, Title: platformWindow.Title(), X: x, Y: y, Width: width, Height: height,
|
||||
Maximized: platformWindow.IsMaximised(),
|
||||
Focused: platformWindow.IsFocused(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -203,55 +211,55 @@ func (s *Service) primaryScreenArea() (int, int, int, int) {
|
|||
}
|
||||
|
||||
func (s *Service) taskOpenWindow(t TaskOpenWindow) (any, bool, error) {
|
||||
pw, err := s.manager.Create(t.Window)
|
||||
platformWindow, err := s.manager.Create(t.Window)
|
||||
if err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
x, y := pw.Position()
|
||||
w, h := pw.Size()
|
||||
info := WindowInfo{Name: pw.Name(), Title: pw.Title(), X: x, Y: y, Width: w, Height: h}
|
||||
x, y := platformWindow.Position()
|
||||
width, height := platformWindow.Size()
|
||||
info := WindowInfo{Name: platformWindow.Name(), Title: platformWindow.Title(), X: x, Y: y, Width: width, Height: height}
|
||||
|
||||
// Attach platform event listeners that convert to IPC actions
|
||||
s.trackWindow(pw)
|
||||
s.trackWindow(platformWindow)
|
||||
|
||||
// Broadcast to all listeners
|
||||
_ = s.Core().ACTION(ActionWindowOpened{Name: pw.Name()})
|
||||
_ = s.Core().ACTION(ActionWindowOpened{Name: platformWindow.Name()})
|
||||
return info, true, nil
|
||||
}
|
||||
|
||||
// trackWindow attaches platform event listeners that emit IPC actions.
|
||||
func (s *Service) trackWindow(pw PlatformWindow) {
|
||||
pw.OnWindowEvent(func(e WindowEvent) {
|
||||
switch e.Type {
|
||||
func (s *Service) trackWindow(platformWindow PlatformWindow) {
|
||||
platformWindow.OnWindowEvent(func(event WindowEvent) {
|
||||
switch event.Type {
|
||||
case "focus":
|
||||
_ = s.Core().ACTION(ActionWindowFocused{Name: e.Name})
|
||||
_ = s.Core().ACTION(ActionWindowFocused{Name: event.Name})
|
||||
case "blur":
|
||||
_ = s.Core().ACTION(ActionWindowBlurred{Name: e.Name})
|
||||
_ = s.Core().ACTION(ActionWindowBlurred{Name: event.Name})
|
||||
case "move":
|
||||
if data := e.Data; data != nil {
|
||||
if data := event.Data; data != nil {
|
||||
x, _ := data["x"].(int)
|
||||
y, _ := data["y"].(int)
|
||||
_ = s.Core().ACTION(ActionWindowMoved{Name: e.Name, X: x, Y: y})
|
||||
_ = s.Core().ACTION(ActionWindowMoved{Name: event.Name, X: x, Y: y})
|
||||
}
|
||||
case "resize":
|
||||
if data := e.Data; data != nil {
|
||||
w, _ := data["width"].(int)
|
||||
if w == 0 {
|
||||
w, _ = data["w"].(int)
|
||||
if data := event.Data; data != nil {
|
||||
width, _ := data["width"].(int)
|
||||
if width == 0 {
|
||||
width, _ = data["w"].(int)
|
||||
}
|
||||
h, _ := data["height"].(int)
|
||||
if h == 0 {
|
||||
h, _ = data["h"].(int)
|
||||
height, _ := data["height"].(int)
|
||||
if height == 0 {
|
||||
height, _ = data["h"].(int)
|
||||
}
|
||||
_ = s.Core().ACTION(ActionWindowResized{Name: e.Name, Width: w, Height: h})
|
||||
_ = s.Core().ACTION(ActionWindowResized{Name: event.Name, Width: width, Height: height})
|
||||
}
|
||||
case "close":
|
||||
_ = s.Core().ACTION(ActionWindowClosed{Name: e.Name})
|
||||
_ = s.Core().ACTION(ActionWindowClosed{Name: event.Name})
|
||||
}
|
||||
})
|
||||
pw.OnFileDrop(func(paths []string, targetID string) {
|
||||
platformWindow.OnFileDrop(func(paths []string, targetID string) {
|
||||
_ = s.Core().ACTION(ActionFilesDropped{
|
||||
Name: pw.Name(),
|
||||
Name: platformWindow.Name(),
|
||||
Paths: paths,
|
||||
TargetID: targetID,
|
||||
})
|
||||
|
|
@ -259,121 +267,121 @@ func (s *Service) trackWindow(pw PlatformWindow) {
|
|||
}
|
||||
|
||||
func (s *Service) taskCloseWindow(name string) error {
|
||||
pw, ok := s.manager.Get(name)
|
||||
if !ok {
|
||||
return fmt.Errorf("window not found: %s", name)
|
||||
platformWindow, err := s.requireWindow(name, "window.Service.taskCloseWindow")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Persist state BEFORE closing (spec requirement)
|
||||
s.manager.State().CaptureState(pw)
|
||||
pw.Close()
|
||||
s.manager.State().CaptureState(platformWindow)
|
||||
platformWindow.Close()
|
||||
s.manager.Remove(name)
|
||||
_ = s.Core().ACTION(ActionWindowClosed{Name: name})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) taskSetPosition(name string, x, y int) error {
|
||||
pw, ok := s.manager.Get(name)
|
||||
if !ok {
|
||||
return fmt.Errorf("window not found: %s", name)
|
||||
platformWindow, err := s.requireWindow(name, "window.Service.taskSetPosition")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pw.SetPosition(x, y)
|
||||
platformWindow.SetPosition(x, y)
|
||||
s.manager.State().UpdatePosition(name, x, y)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) taskSetSize(name string, width, height int) error {
|
||||
pw, ok := s.manager.Get(name)
|
||||
if !ok {
|
||||
return fmt.Errorf("window not found: %s", name)
|
||||
platformWindow, err := s.requireWindow(name, "window.Service.taskSetSize")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pw.SetSize(width, height)
|
||||
platformWindow.SetSize(width, height)
|
||||
s.manager.State().UpdateSize(name, width, height)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) taskMaximise(name string) error {
|
||||
pw, ok := s.manager.Get(name)
|
||||
if !ok {
|
||||
return fmt.Errorf("window not found: %s", name)
|
||||
platformWindow, err := s.requireWindow(name, "window.Service.taskMaximise")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pw.Maximise()
|
||||
platformWindow.Maximise()
|
||||
s.manager.State().UpdateMaximized(name, true)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) taskMinimise(name string) error {
|
||||
pw, ok := s.manager.Get(name)
|
||||
if !ok {
|
||||
return fmt.Errorf("window not found: %s", name)
|
||||
platformWindow, err := s.requireWindow(name, "window.Service.taskMinimise")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pw.Minimise()
|
||||
platformWindow.Minimise()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) taskFocus(name string) error {
|
||||
pw, ok := s.manager.Get(name)
|
||||
if !ok {
|
||||
return fmt.Errorf("window not found: %s", name)
|
||||
platformWindow, err := s.requireWindow(name, "window.Service.taskFocus")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pw.Focus()
|
||||
platformWindow.Focus()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) taskRestore(name string) error {
|
||||
pw, ok := s.manager.Get(name)
|
||||
if !ok {
|
||||
return fmt.Errorf("window not found: %s", name)
|
||||
platformWindow, err := s.requireWindow(name, "window.Service.taskRestore")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pw.Restore()
|
||||
platformWindow.Restore()
|
||||
s.manager.State().UpdateMaximized(name, false)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) taskSetTitle(name, title string) error {
|
||||
pw, ok := s.manager.Get(name)
|
||||
if !ok {
|
||||
return fmt.Errorf("window not found: %s", name)
|
||||
platformWindow, err := s.requireWindow(name, "window.Service.taskSetTitle")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pw.SetTitle(title)
|
||||
platformWindow.SetTitle(title)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) taskSetAlwaysOnTop(name string, alwaysOnTop bool) error {
|
||||
pw, ok := s.manager.Get(name)
|
||||
if !ok {
|
||||
return fmt.Errorf("window not found: %s", name)
|
||||
platformWindow, err := s.requireWindow(name, "window.Service.taskSetAlwaysOnTop")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pw.SetAlwaysOnTop(alwaysOnTop)
|
||||
platformWindow.SetAlwaysOnTop(alwaysOnTop)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) taskSetBackgroundColour(name string, red, green, blue, alpha uint8) error {
|
||||
pw, ok := s.manager.Get(name)
|
||||
if !ok {
|
||||
return fmt.Errorf("window not found: %s", name)
|
||||
platformWindow, err := s.requireWindow(name, "window.Service.taskSetBackgroundColour")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pw.SetBackgroundColour(red, green, blue, alpha)
|
||||
platformWindow.SetBackgroundColour(red, green, blue, alpha)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) taskSetVisibility(name string, visible bool) error {
|
||||
pw, ok := s.manager.Get(name)
|
||||
if !ok {
|
||||
return fmt.Errorf("window not found: %s", name)
|
||||
platformWindow, err := s.requireWindow(name, "window.Service.taskSetVisibility")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pw.SetVisibility(visible)
|
||||
platformWindow.SetVisibility(visible)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) taskFullscreen(name string, fullscreen bool) error {
|
||||
pw, ok := s.manager.Get(name)
|
||||
if !ok {
|
||||
return fmt.Errorf("window not found: %s", name)
|
||||
platformWindow, err := s.requireWindow(name, "window.Service.taskFullscreen")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if fullscreen {
|
||||
pw.Fullscreen()
|
||||
platformWindow.Fullscreen()
|
||||
} else {
|
||||
pw.UnFullscreen()
|
||||
platformWindow.UnFullscreen()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -393,21 +401,21 @@ func (s *Service) taskSaveLayout(name string) error {
|
|||
func (s *Service) taskRestoreLayout(name string) error {
|
||||
layout, ok := s.manager.Layout().GetLayout(name)
|
||||
if !ok {
|
||||
return fmt.Errorf("layout not found: %s", name)
|
||||
return coreerr.E("window.Service.taskRestoreLayout", "layout not found: "+name, nil)
|
||||
}
|
||||
for winName, state := range layout.Windows {
|
||||
pw, found := s.manager.Get(winName)
|
||||
platformWindow, found := s.manager.Get(winName)
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
pw.SetPosition(state.X, state.Y)
|
||||
pw.SetSize(state.Width, state.Height)
|
||||
platformWindow.SetPosition(state.X, state.Y)
|
||||
platformWindow.SetSize(state.Width, state.Height)
|
||||
if state.Maximized {
|
||||
pw.Maximise()
|
||||
platformWindow.Maximise()
|
||||
} else {
|
||||
pw.Restore()
|
||||
platformWindow.Restore()
|
||||
}
|
||||
s.manager.State().CaptureState(pw)
|
||||
s.manager.State().CaptureState(platformWindow)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -423,7 +431,7 @@ var tileModeMap = map[string]TileMode{
|
|||
func (s *Service) taskTileWindows(mode string, names []string) error {
|
||||
tm, ok := tileModeMap[mode]
|
||||
if !ok {
|
||||
return fmt.Errorf("unknown tile mode: %s", mode)
|
||||
return coreerr.E("window.Service.taskTileWindows", "unknown tile mode: "+mode, nil)
|
||||
}
|
||||
if len(names) == 0 {
|
||||
names = s.manager.List()
|
||||
|
|
@ -451,7 +459,7 @@ var snapPosMap = map[string]SnapPosition{
|
|||
func (s *Service) taskSnapWindow(name, position string) error {
|
||||
pos, ok := snapPosMap[position]
|
||||
if !ok {
|
||||
return fmt.Errorf("unknown snap position: %s", position)
|
||||
return coreerr.E("window.Service.taskSnapWindow", "unknown snap position: "+position, nil)
|
||||
}
|
||||
originX, originY, screenWidth, screenHeight := s.primaryScreenArea()
|
||||
return s.manager.SnapWindow(name, pos, screenWidth, screenHeight, originX, originY)
|
||||
|
|
@ -467,7 +475,7 @@ var workflowLayoutMap = map[string]WorkflowLayout{
|
|||
func (s *Service) taskApplyWorkflow(workflow string, names []string) error {
|
||||
layout, ok := workflowLayoutMap[workflow]
|
||||
if !ok {
|
||||
return fmt.Errorf("unknown workflow layout: %s", workflow)
|
||||
return coreerr.E("window.Service.taskApplyWorkflow", "unknown workflow layout: "+workflow, nil)
|
||||
}
|
||||
if len(names) == 0 {
|
||||
names = s.manager.List()
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
|
|
@ -208,6 +209,7 @@ func (sm *StateManager) ListStates() []string {
|
|||
for name := range sm.states {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
return names
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
// pkg/window/window.go
|
||||
package window
|
||||
|
||||
import "sync"
|
||||
import (
|
||||
"sort"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Window is CoreGUI's own window descriptor — NOT a Wails type alias.
|
||||
type Window struct {
|
||||
|
|
@ -142,6 +145,7 @@ func (m *Manager) List() []string {
|
|||
for name := range m.windows {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
return names
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue