diff --git a/pkg/display/display.go b/pkg/display/display.go index d15226b..303b54c 100644 --- a/pkg/display/display.go +++ b/pkg/display/display.go @@ -972,7 +972,8 @@ func (s *Service) ApplyWorkflowLayout(workflow window.WorkflowLayout) error { if ws == nil { return coreerr.E("display.ApplyWorkflowLayout", "window service not available", nil) } - return ws.Manager().ApplyWorkflow(workflow, ws.Manager().List(), 1920, 1080) + screenWidth, screenHeight := s.primaryScreenSize() + return ws.Manager().ApplyWorkflow(workflow, ws.Manager().List(), screenWidth, screenHeight) } // GetEventManager returns the event manager for WebSocket event subscriptions. diff --git a/pkg/window/service.go b/pkg/window/service.go index 1f95a19..ef42b56 100644 --- a/pkg/window/service.go +++ b/pkg/window/service.go @@ -5,6 +5,7 @@ import ( coreerr "forge.lthn.ai/core/go-log" "forge.lthn.ai/core/go/pkg/core" + "forge.lthn.ai/core/gui/pkg/screen" ) type Options struct{} @@ -144,6 +145,33 @@ func (s *Service) handleTask(c *core.Core, t core.Task) (any, bool, error) { } } +func (s *Service) primaryScreenSize() (int, int) { + const fallbackWidth = 1920 + const fallbackHeight = 1080 + + result, handled, err := s.Core().QUERY(screen.QueryPrimary{}) + if err != nil || !handled { + return fallbackWidth, fallbackHeight + } + + primary, ok := result.(*screen.Screen) + if !ok || primary == nil { + return fallbackWidth, fallbackHeight + } + + width := primary.WorkArea.Width + height := primary.WorkArea.Height + if width <= 0 || height <= 0 { + width = primary.Bounds.Width + height = primary.Bounds.Height + } + if width <= 0 || height <= 0 { + return fallbackWidth, fallbackHeight + } + + return width, height +} + func (s *Service) taskOpenWindow(t TaskOpenWindow) (any, bool, error) { pw, err := s.manager.Open(t.Options...) if err != nil { @@ -343,8 +371,8 @@ func (s *Service) taskTileWindows(mode string, names []string) error { if len(names) == 0 { names = s.manager.List() } - // Default screen size — callers can query screen_primary for actual values. - return s.manager.TileWindows(tm, names, 1920, 1080) + screenWidth, screenHeight := s.primaryScreenSize() + return s.manager.TileWindows(tm, names, screenWidth, screenHeight) } var snapPosMap = map[string]SnapPosition{ @@ -360,7 +388,8 @@ func (s *Service) taskSnapWindow(name, position string) error { if !ok { return coreerr.E("window.taskSnapWindow", "unknown snap position: "+position, nil) } - return s.manager.SnapWindow(name, pos, 1920, 1080) + screenWidth, screenHeight := s.primaryScreenSize() + return s.manager.SnapWindow(name, pos, screenWidth, screenHeight) } // Manager returns the underlying window Manager for direct access. diff --git a/pkg/window/service_screen_test.go b/pkg/window/service_screen_test.go new file mode 100644 index 0000000..1541dea --- /dev/null +++ b/pkg/window/service_screen_test.go @@ -0,0 +1,99 @@ +package window + +import ( + "context" + "testing" + + "forge.lthn.ai/core/go/pkg/core" + "forge.lthn.ai/core/gui/pkg/screen" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type mockScreenPlatform struct { + screens []screen.Screen +} + +func (m *mockScreenPlatform) GetAll() []screen.Screen { return m.screens } + +func (m *mockScreenPlatform) GetPrimary() *screen.Screen { + for i := range m.screens { + if m.screens[i].IsPrimary { + return &m.screens[i] + } + } + return nil +} + +func newTestWindowServiceWithScreen(t *testing.T, screens []screen.Screen) (*Service, *core.Core) { + t.Helper() + + c, err := core.New( + core.WithService(screen.Register(&mockScreenPlatform{screens: screens})), + core.WithService(Register(newMockPlatform())), + core.WithServiceLock(), + ) + require.NoError(t, err) + require.NoError(t, c.ServiceStartup(context.Background(), nil)) + + svc := core.MustServiceFor[*Service](c, "window") + return svc, c +} + +func TestTaskTileWindows_UsesPrimaryScreenSize(t *testing.T) { + _, c := newTestWindowServiceWithScreen(t, []screen.Screen{ + { + ID: "1", Name: "Primary", IsPrimary: true, + Bounds: screen.Rect{X: 0, Y: 0, Width: 2000, Height: 1000}, + WorkArea: screen.Rect{X: 0, Y: 0, Width: 2000, Height: 1000}, + }, + }) + + _, _, err := c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("left"), WithSize(400, 400)}}) + require.NoError(t, err) + _, _, err = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("right"), WithSize(400, 400)}}) + require.NoError(t, err) + + _, handled, err := c.PERFORM(TaskTileWindows{Mode: "left-right", Windows: []string{"left", "right"}}) + require.NoError(t, err) + assert.True(t, handled) + + result, _, err := c.QUERY(QueryWindowByName{Name: "left"}) + require.NoError(t, err) + left := result.(*WindowInfo) + assert.Equal(t, 0, left.X) + assert.Equal(t, 1000, left.Width) + assert.Equal(t, 1000, left.Height) + + result, _, err = c.QUERY(QueryWindowByName{Name: "right"}) + require.NoError(t, err) + right := result.(*WindowInfo) + assert.Equal(t, 1000, right.X) + assert.Equal(t, 1000, right.Width) + assert.Equal(t, 1000, right.Height) +} + +func TestTaskSnapWindow_UsesPrimaryScreenSize(t *testing.T) { + _, c := newTestWindowServiceWithScreen(t, []screen.Screen{ + { + ID: "1", Name: "Primary", IsPrimary: true, + Bounds: screen.Rect{X: 0, Y: 0, Width: 2000, Height: 1000}, + WorkArea: screen.Rect{X: 0, Y: 0, Width: 2000, Height: 1000}, + }, + }) + + _, _, err := c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("snap"), WithSize(400, 300)}}) + require.NoError(t, err) + + _, handled, err := c.PERFORM(TaskSnapWindow{Name: "snap", Position: "left"}) + require.NoError(t, err) + assert.True(t, handled) + + result, _, err := c.QUERY(QueryWindowByName{Name: "snap"}) + require.NoError(t, err) + info := result.(*WindowInfo) + assert.Equal(t, 0, info.X) + assert.Equal(t, 0, info.Y) + assert.Equal(t, 1000, info.Width) + assert.Equal(t, 1000, info.Height) +}