Replaced fmt, strings, sort, os, io, sync, encoding/json, path/filepath, errors, log, reflect with core.Sprintf, core.E, core.Contains, core.Trim, core.Split, core.Join, core.JoinPath, slices.Sort, c.Fs(), c.Lock(), core.JSONMarshal, core.ReadAll and other CoreGO v0.8.0 primitives. Framework boundary exceptions preserved where stdlib types are required by external interfaces (Gin, net/http, CGo, Wails, bubbletea). Co-Authored-By: Virgil <virgil@lethean.io>
339 lines
8.8 KiB
Go
339 lines
8.8 KiB
Go
// pkg/window/window.go
|
|
package window
|
|
|
|
import (
|
|
"math"
|
|
"sync"
|
|
|
|
corego "dappco.re/go/core"
|
|
)
|
|
|
|
// Window is CoreGUI's own window descriptor — NOT a Wails type alias.
|
|
// Use: spec := &window.Window{Name: "editor", URL: "/editor"}
|
|
type Window struct {
|
|
Name string
|
|
Title string
|
|
URL string
|
|
Width, Height int
|
|
X, Y int
|
|
MinWidth, MinHeight int
|
|
MaxWidth, MaxHeight int
|
|
Frameless bool
|
|
Hidden bool
|
|
AlwaysOnTop bool
|
|
BackgroundColour [4]uint8
|
|
DisableResize bool
|
|
EnableFileDrop bool
|
|
}
|
|
|
|
// ToPlatformOptions converts a Window to PlatformWindowOptions for the backend.
|
|
// Use: opts := spec.ToPlatformOptions()
|
|
func (w *Window) ToPlatformOptions() PlatformWindowOptions {
|
|
return PlatformWindowOptions{
|
|
Name: w.Name, Title: w.Title, URL: w.URL,
|
|
Width: w.Width, Height: w.Height, X: w.X, Y: w.Y,
|
|
MinWidth: w.MinWidth, MinHeight: w.MinHeight,
|
|
MaxWidth: w.MaxWidth, MaxHeight: w.MaxHeight,
|
|
Frameless: w.Frameless, Hidden: w.Hidden,
|
|
AlwaysOnTop: w.AlwaysOnTop, BackgroundColour: w.BackgroundColour,
|
|
DisableResize: w.DisableResize, EnableFileDrop: w.EnableFileDrop,
|
|
}
|
|
}
|
|
|
|
// Manager manages window lifecycle through a Platform backend.
|
|
// Use: mgr := window.NewManager(platform)
|
|
type Manager struct {
|
|
platform Platform
|
|
state *StateManager
|
|
layout *LayoutManager
|
|
windows map[string]PlatformWindow
|
|
defaultWidth int
|
|
defaultHeight int
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
// NewManager creates a window Manager with the given platform backend.
|
|
// Use: mgr := window.NewManager(platform)
|
|
func NewManager(platform Platform) *Manager {
|
|
return &Manager{
|
|
platform: platform,
|
|
state: NewStateManager(),
|
|
layout: NewLayoutManager(),
|
|
windows: make(map[string]PlatformWindow),
|
|
}
|
|
}
|
|
|
|
// NewManagerWithDir creates a window Manager with a custom config directory for state/layout persistence.
|
|
// Use: mgr := window.NewManagerWithDir(platform, t.TempDir())
|
|
func NewManagerWithDir(platform Platform, configDir string) *Manager {
|
|
return &Manager{
|
|
platform: platform,
|
|
state: NewStateManagerWithDir(configDir),
|
|
layout: NewLayoutManagerWithDir(configDir),
|
|
windows: make(map[string]PlatformWindow),
|
|
}
|
|
}
|
|
|
|
// SetDefaultWidth overrides the fallback width used when a window is created without one.
|
|
// Use: mgr.SetDefaultWidth(1280)
|
|
func (m *Manager) SetDefaultWidth(width int) {
|
|
if width > 0 {
|
|
m.defaultWidth = width
|
|
}
|
|
}
|
|
|
|
// SetDefaultHeight overrides the fallback height used when a window is created without one.
|
|
// Use: mgr.SetDefaultHeight(800)
|
|
func (m *Manager) SetDefaultHeight(height int) {
|
|
if height > 0 {
|
|
m.defaultHeight = height
|
|
}
|
|
}
|
|
|
|
// Open creates a window using functional options, applies saved state, and tracks it.
|
|
// Use: _, err := mgr.Open(window.WithName("editor"), window.WithURL("/editor"))
|
|
func (m *Manager) Open(opts ...WindowOption) (PlatformWindow, error) {
|
|
w, err := ApplyOptions(opts...)
|
|
if err != nil {
|
|
return nil, corego.Wrap(err, "window.Manager.Open", "failed to apply options")
|
|
}
|
|
return m.Create(w)
|
|
}
|
|
|
|
// Create creates a window from a Window descriptor.
|
|
// Use: _, err := mgr.Create(&window.Window{Name: "editor", URL: "/editor"})
|
|
func (m *Manager) Create(w *Window) (PlatformWindow, error) {
|
|
if w.Name == "" {
|
|
w.Name = "main"
|
|
}
|
|
if w.Title == "" {
|
|
w.Title = "Core"
|
|
}
|
|
if w.Width == 0 {
|
|
if m.defaultWidth > 0 {
|
|
w.Width = m.defaultWidth
|
|
} else {
|
|
w.Width = 1280
|
|
}
|
|
}
|
|
if w.Height == 0 {
|
|
if m.defaultHeight > 0 {
|
|
w.Height = m.defaultHeight
|
|
} else {
|
|
w.Height = 800
|
|
}
|
|
}
|
|
if w.URL == "" {
|
|
w.URL = "/"
|
|
}
|
|
|
|
// Apply saved state if available
|
|
m.state.ApplyState(w)
|
|
|
|
pw := m.platform.CreateWindow(w.ToPlatformOptions())
|
|
|
|
m.mu.Lock()
|
|
m.windows[w.Name] = pw
|
|
m.mu.Unlock()
|
|
|
|
return pw, nil
|
|
}
|
|
|
|
// Get returns a tracked window by name.
|
|
// Use: pw, ok := mgr.Get("editor")
|
|
func (m *Manager) Get(name string) (PlatformWindow, bool) {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
pw, ok := m.windows[name]
|
|
return pw, ok
|
|
}
|
|
|
|
// List returns all tracked window names.
|
|
// Use: names := mgr.List()
|
|
func (m *Manager) List() []string {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
names := make([]string, 0, len(m.windows))
|
|
for name := range m.windows {
|
|
names = append(names, name)
|
|
}
|
|
return names
|
|
}
|
|
|
|
// Remove stops tracking a window by name.
|
|
// Use: mgr.Remove("editor")
|
|
func (m *Manager) Remove(name string) {
|
|
m.mu.Lock()
|
|
delete(m.windows, name)
|
|
m.mu.Unlock()
|
|
}
|
|
|
|
// Platform returns the underlying platform for direct access.
|
|
// Use: platform := mgr.Platform()
|
|
func (m *Manager) Platform() Platform {
|
|
return m.platform
|
|
}
|
|
|
|
// State returns the state manager for window persistence.
|
|
// Use: state := mgr.State()
|
|
func (m *Manager) State() *StateManager {
|
|
return m.state
|
|
}
|
|
|
|
// Layout returns the layout manager.
|
|
// Use: layouts := mgr.Layout()
|
|
func (m *Manager) Layout() *LayoutManager {
|
|
return m.layout
|
|
}
|
|
|
|
// SuggestLayout returns a simple layout recommendation for the given screen.
|
|
// Use: suggestion := mgr.SuggestLayout(1920, 1080, 2)
|
|
func (m *Manager) SuggestLayout(screenW, screenH, windowCount int) LayoutSuggestion {
|
|
if windowCount <= 1 {
|
|
return LayoutSuggestion{
|
|
Mode: "single",
|
|
Columns: 1,
|
|
Rows: 1,
|
|
PrimaryWidth: screenW,
|
|
SecondaryWidth: 0,
|
|
Description: "Focus the primary window and keep the screen uncluttered.",
|
|
}
|
|
}
|
|
|
|
if windowCount == 2 {
|
|
return LayoutSuggestion{
|
|
Mode: "side-by-side",
|
|
Columns: 2,
|
|
Rows: 1,
|
|
PrimaryWidth: screenW / 2,
|
|
SecondaryWidth: screenW - (screenW / 2),
|
|
Description: "Split the screen into two equal panes.",
|
|
}
|
|
}
|
|
|
|
if windowCount <= 4 {
|
|
return LayoutSuggestion{
|
|
Mode: "quadrants",
|
|
Columns: 2,
|
|
Rows: 2,
|
|
PrimaryWidth: screenW / 2,
|
|
SecondaryWidth: screenW / 2,
|
|
Description: "Use a 2x2 grid for the active windows.",
|
|
}
|
|
}
|
|
|
|
cols := 3
|
|
rows := int(math.Ceil(float64(windowCount) / float64(cols)))
|
|
return LayoutSuggestion{
|
|
Mode: "grid",
|
|
Columns: cols,
|
|
Rows: rows,
|
|
PrimaryWidth: screenW / cols,
|
|
SecondaryWidth: screenW / cols,
|
|
Description: "Use a dense grid to keep every window visible.",
|
|
}
|
|
}
|
|
|
|
// FindSpace returns a free placement suggestion for a new window.
|
|
// Use: info := mgr.FindSpace(1920, 1080, 1280, 800)
|
|
func (m *Manager) FindSpace(screenW, screenH, width, height int) SpaceInfo {
|
|
if width <= 0 {
|
|
width = screenW / 2
|
|
}
|
|
if height <= 0 {
|
|
height = screenH / 2
|
|
}
|
|
|
|
occupied := make([]struct {
|
|
x, y, w, h int
|
|
}, 0)
|
|
for _, name := range m.List() {
|
|
pw, ok := m.Get(name)
|
|
if !ok {
|
|
continue
|
|
}
|
|
x, y := pw.Position()
|
|
w, h := pw.Size()
|
|
occupied = append(occupied, struct {
|
|
x, y, w, h int
|
|
}{x: x, y: y, w: w, h: h})
|
|
}
|
|
|
|
step := int(math.Max(40, math.Min(float64(width), float64(height))/6))
|
|
if step < 40 {
|
|
step = 40
|
|
}
|
|
|
|
for y := 0; y+height <= screenH; y += step {
|
|
for x := 0; x+width <= screenW; x += step {
|
|
if !intersectsAny(x, y, width, height, occupied) {
|
|
return SpaceInfo{
|
|
X: x, Y: y, Width: width, Height: height,
|
|
ScreenWidth: screenW, ScreenHeight: screenH,
|
|
Reason: "first available gap",
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return SpaceInfo{
|
|
X: (screenW - width) / 2, Y: (screenH - height) / 2,
|
|
Width: width, Height: height,
|
|
ScreenWidth: screenW, ScreenHeight: screenH,
|
|
Reason: "center fallback",
|
|
}
|
|
}
|
|
|
|
// ArrangePair places two windows side-by-side with a balanced split.
|
|
// Use: _ = mgr.ArrangePair("editor", "terminal", 1920, 1080)
|
|
func (m *Manager) ArrangePair(first, second string, screenW, screenH int) error {
|
|
left, ok := m.Get(first)
|
|
if !ok {
|
|
return corego.E("window.ArrangePair", corego.Sprintf("window %q not found", first), nil)
|
|
}
|
|
right, ok := m.Get(second)
|
|
if !ok {
|
|
return corego.E("window.ArrangePair", corego.Sprintf("window %q not found", second), nil)
|
|
}
|
|
|
|
leftW := screenW / 2
|
|
rightW := screenW - leftW
|
|
left.SetPosition(0, 0)
|
|
left.SetSize(leftW, screenH)
|
|
right.SetPosition(leftW, 0)
|
|
right.SetSize(rightW, screenH)
|
|
return nil
|
|
}
|
|
|
|
// BesideEditor places a target window beside an editor window, using a 70/30 split.
|
|
// Use: _ = mgr.BesideEditor("editor", "terminal", 1920, 1080)
|
|
func (m *Manager) BesideEditor(editorName, windowName string, screenW, screenH int) error {
|
|
editor, ok := m.Get(editorName)
|
|
if !ok {
|
|
return corego.E("window.BesideEditor", corego.Sprintf("window %q not found", editorName), nil)
|
|
}
|
|
target, ok := m.Get(windowName)
|
|
if !ok {
|
|
return corego.E("window.BesideEditor", corego.Sprintf("window %q not found", windowName), nil)
|
|
}
|
|
|
|
editorW := screenW * 70 / 100
|
|
if editorW <= 0 {
|
|
editorW = screenW / 2
|
|
}
|
|
targetW := screenW - editorW
|
|
editor.SetPosition(0, 0)
|
|
editor.SetSize(editorW, screenH)
|
|
target.SetPosition(editorW, 0)
|
|
target.SetSize(targetW, screenH)
|
|
return nil
|
|
}
|
|
|
|
func intersectsAny(x, y, w, h int, occupied []struct{ x, y, w, h int }) bool {
|
|
for _, r := range occupied {
|
|
if x < r.x+r.w && x+w > r.x && y < r.y+r.h && y+h > r.y {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|