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>
296 lines
7.6 KiB
Go
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
|
|
}
|