gui/pkg/window/tiling.go
Snider 62ec735c10
Some checks failed
Security Scan / security (push) Has been cancelled
Test / test (push) Has been cancelled
refactor: AX compliance sweep — replace banned stdlib imports with core primitives
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>
2026-04-13 09:32:01 +01:00

296 lines
7.6 KiB
Go

// pkg/window/tiling.go
package window
import corego "dappco.re/go/core"
// normalizeWindowForLayout clears transient maximise/minimise state before
// applying a new geometry. This keeps layout helpers effective even when a
// window was previously maximised.
func normalizeWindowForLayout(pw PlatformWindow) {
if pw == nil {
return
}
if pw.IsMaximised() || pw.IsMinimised() {
pw.Restore()
}
}
// TileMode defines how windows are arranged.
// Use: mode := window.TileModeLeftRight
type TileMode int
const (
TileModeLeftHalf TileMode = iota
TileModeRightHalf
TileModeTopHalf
TileModeBottomHalf
TileModeTopLeft
TileModeTopRight
TileModeBottomLeft
TileModeBottomRight
TileModeLeftRight
TileModeGrid
)
var tileModeNames = map[TileMode]string{
TileModeLeftHalf: "left-half", TileModeRightHalf: "right-half",
TileModeTopHalf: "top-half", TileModeBottomHalf: "bottom-half",
TileModeTopLeft: "top-left", TileModeTopRight: "top-right",
TileModeBottomLeft: "bottom-left", TileModeBottomRight: "bottom-right",
TileModeLeftRight: "left-right", TileModeGrid: "grid",
}
// String returns the canonical layout name for the tile mode.
// Use: label := window.TileModeGrid.String()
func (m TileMode) String() string { return tileModeNames[m] }
// SnapPosition defines where a window snaps to.
// Use: pos := window.SnapRight
type SnapPosition int
const (
SnapLeft SnapPosition = iota
SnapRight
SnapTop
SnapBottom
SnapTopLeft
SnapTopRight
SnapBottomLeft
SnapBottomRight
SnapCenter
)
// WorkflowLayout is a predefined arrangement for common tasks.
// Use: workflow := window.WorkflowCoding
type WorkflowLayout int
const (
WorkflowCoding WorkflowLayout = iota // 70/30 split
WorkflowDebugging // 60/40 split
WorkflowPresenting // maximised
WorkflowSideBySide // 50/50 split
)
var workflowNames = map[WorkflowLayout]string{
WorkflowCoding: "coding", WorkflowDebugging: "debugging",
WorkflowPresenting: "presenting", WorkflowSideBySide: "side-by-side",
}
// String returns the canonical workflow name.
// Use: label := window.WorkflowCoding.String()
func (w WorkflowLayout) String() string { return workflowNames[w] }
// ParseWorkflowLayout converts a workflow name into its enum value.
// Use: workflow, ok := window.ParseWorkflowLayout("coding")
func ParseWorkflowLayout(name string) (WorkflowLayout, bool) {
for workflow, workflowName := range workflowNames {
if workflowName == name {
return workflow, true
}
}
return WorkflowCoding, false
}
// TileWindows arranges the named windows in the given mode across the screen area.
func (m *Manager) TileWindows(mode TileMode, names []string, screenW, screenH int) error {
windows := make([]PlatformWindow, 0, len(names))
for _, name := range names {
pw, ok := m.Get(name)
if !ok {
return corego.E("window.tiling", corego.Sprintf("window %q not found", name), nil)
}
windows = append(windows, pw)
}
if len(windows) == 0 {
return corego.E("window.tiling", "no windows to tile", nil)
}
halfW, halfH := screenW/2, screenH/2
switch mode {
case TileModeLeftRight:
w := screenW / len(windows)
for i, pw := range windows {
normalizeWindowForLayout(pw)
pw.SetPosition(i*w, 0)
pw.SetSize(w, screenH)
}
case TileModeGrid:
cols := 2
if len(windows) > 4 {
cols = 3
}
cellW := screenW / cols
for i, pw := range windows {
normalizeWindowForLayout(pw)
row := i / cols
col := i % cols
rows := (len(windows) + cols - 1) / cols
cellH := screenH / rows
pw.SetPosition(col*cellW, row*cellH)
pw.SetSize(cellW, cellH)
}
case TileModeLeftHalf:
for _, pw := range windows {
normalizeWindowForLayout(pw)
pw.SetPosition(0, 0)
pw.SetSize(halfW, screenH)
}
case TileModeRightHalf:
for _, pw := range windows {
normalizeWindowForLayout(pw)
pw.SetPosition(halfW, 0)
pw.SetSize(halfW, screenH)
}
case TileModeTopHalf:
for _, pw := range windows {
normalizeWindowForLayout(pw)
pw.SetPosition(0, 0)
pw.SetSize(screenW, halfH)
}
case TileModeBottomHalf:
for _, pw := range windows {
normalizeWindowForLayout(pw)
pw.SetPosition(0, halfH)
pw.SetSize(screenW, halfH)
}
case TileModeTopLeft:
for _, pw := range windows {
normalizeWindowForLayout(pw)
pw.SetPosition(0, 0)
pw.SetSize(halfW, halfH)
}
case TileModeTopRight:
for _, pw := range windows {
normalizeWindowForLayout(pw)
pw.SetPosition(halfW, 0)
pw.SetSize(halfW, halfH)
}
case TileModeBottomLeft:
for _, pw := range windows {
normalizeWindowForLayout(pw)
pw.SetPosition(0, halfH)
pw.SetSize(halfW, halfH)
}
case TileModeBottomRight:
for _, pw := range windows {
normalizeWindowForLayout(pw)
pw.SetPosition(halfW, halfH)
pw.SetSize(halfW, halfH)
}
}
return nil
}
// SnapWindow snaps a window to a screen edge/corner/centre.
func (m *Manager) SnapWindow(name string, pos SnapPosition, screenW, screenH int) error {
pw, ok := m.Get(name)
if !ok {
return corego.E("window.tiling", corego.Sprintf("window %q not found", name), nil)
}
halfW, halfH := screenW/2, screenH/2
switch pos {
case SnapLeft:
normalizeWindowForLayout(pw)
pw.SetPosition(0, 0)
pw.SetSize(halfW, screenH)
case SnapRight:
normalizeWindowForLayout(pw)
pw.SetPosition(halfW, 0)
pw.SetSize(halfW, screenH)
case SnapTop:
normalizeWindowForLayout(pw)
pw.SetPosition(0, 0)
pw.SetSize(screenW, halfH)
case SnapBottom:
normalizeWindowForLayout(pw)
pw.SetPosition(0, halfH)
pw.SetSize(screenW, halfH)
case SnapTopLeft:
normalizeWindowForLayout(pw)
pw.SetPosition(0, 0)
pw.SetSize(halfW, halfH)
case SnapTopRight:
normalizeWindowForLayout(pw)
pw.SetPosition(halfW, 0)
pw.SetSize(halfW, halfH)
case SnapBottomLeft:
normalizeWindowForLayout(pw)
pw.SetPosition(0, halfH)
pw.SetSize(halfW, halfH)
case SnapBottomRight:
normalizeWindowForLayout(pw)
pw.SetPosition(halfW, halfH)
pw.SetSize(halfW, halfH)
case SnapCenter:
normalizeWindowForLayout(pw)
cw, ch := pw.Size()
pw.SetPosition((screenW-cw)/2, (screenH-ch)/2)
}
return nil
}
// StackWindows cascades windows with an offset.
func (m *Manager) StackWindows(names []string, offsetX, offsetY int) error {
for i, name := range names {
pw, ok := m.Get(name)
if !ok {
return corego.E("window.tiling", corego.Sprintf("window %q not found", name), nil)
}
normalizeWindowForLayout(pw)
pw.SetPosition(i*offsetX, i*offsetY)
}
return nil
}
// ApplyWorkflow arranges windows in a predefined workflow layout.
func (m *Manager) ApplyWorkflow(workflow WorkflowLayout, names []string, screenW, screenH int) error {
if len(names) == 0 {
return corego.E("window.tiling", "no windows for workflow", nil)
}
switch workflow {
case WorkflowCoding:
// 70/30 split — main editor + terminal
mainW := screenW * 70 / 100
if pw, ok := m.Get(names[0]); ok {
normalizeWindowForLayout(pw)
pw.SetPosition(0, 0)
pw.SetSize(mainW, screenH)
}
if len(names) > 1 {
if pw, ok := m.Get(names[1]); ok {
normalizeWindowForLayout(pw)
pw.SetPosition(mainW, 0)
pw.SetSize(screenW-mainW, screenH)
}
}
case WorkflowDebugging:
// 60/40 split
mainW := screenW * 60 / 100
if pw, ok := m.Get(names[0]); ok {
normalizeWindowForLayout(pw)
pw.SetPosition(0, 0)
pw.SetSize(mainW, screenH)
}
if len(names) > 1 {
if pw, ok := m.Get(names[1]); ok {
normalizeWindowForLayout(pw)
pw.SetPosition(mainW, 0)
pw.SetSize(screenW-mainW, screenH)
}
}
case WorkflowPresenting:
// Maximise first window
if pw, ok := m.Get(names[0]); ok {
normalizeWindowForLayout(pw)
pw.SetPosition(0, 0)
pw.SetSize(screenW, screenH)
}
case WorkflowSideBySide:
return m.TileWindows(TileModeLeftRight, names, screenW, screenH)
}
return nil
}