Twenty pieces of a retro desktop: Desktop, DesktopIcon, Taskbar, StartMenu, AppWindow, Calculator, Notepad, Minesweeper, ControlPanel, TrayIcons, ShutdownScreen, BootScreen, Screensaver, Snake, Dock, RunDialog, DiskDefrag, WallpaperPicker, ClipboardPanel and LockScreen. The calculator really adds up and calls division by zero GAME OVER; Minesweeper and Snake are playable, first click always safe; START cascades its submenus without moving the rows already open; windows drag by whole dots; the screensaver's logo changes color at each wall and sparkles in a corner; the defragmenter is oddly soothing. Clocks that step back hold at zero, and typed or copied text never reaches the log. icon.Bomb is the new 12x12 mine.
97 lines
2.6 KiB
Go
97 lines
2.6 KiB
Go
package desk
|
|
|
|
import (
|
|
"fmt"
|
|
"log/slog"
|
|
|
|
"github.com/egoist/mygo/ui"
|
|
|
|
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
|
)
|
|
|
|
// DockApp is an app on the dock.
|
|
type DockApp struct {
|
|
Name string
|
|
Icon *pixel.Sprite
|
|
}
|
|
|
|
type DockOptions struct {
|
|
Disabled bool
|
|
}
|
|
|
|
type DockResult struct {
|
|
Element *ui.Element
|
|
// Launched is the app pressed this frame, else -1.
|
|
Launched int
|
|
}
|
|
|
|
type dockKey struct{}
|
|
|
|
const dockCell = 30
|
|
|
|
// Dock: a shelf of apps; the pointed or focused rises.
|
|
func Dock(c *ui.Context, text string, apps []DockApp, o DockOptions) DockResult {
|
|
k := pixel.K(c)
|
|
if len(apps) == 0 {
|
|
panic("desk: Dock has no apps")
|
|
}
|
|
seen := map[string]bool{}
|
|
for i, a := range apps {
|
|
sprite("Dock", a.Icon)
|
|
if a.Name == "" || seen[a.Name] {
|
|
panic(fmt.Sprintf("desk: Dock app %d %q is nameless or twice", i, a.Name))
|
|
}
|
|
seen[a.Name] = true
|
|
}
|
|
w := len(apps) * dockCell
|
|
col := ui.Column(c).Width(k.D(w + 8)).AlignItems(ui.Center)
|
|
// lit is the risen app, read when the names paint.
|
|
lit := ui.Local(col, dockKey{}, func() int { return -1 })
|
|
*lit = -1
|
|
res := DockResult{Launched: -1}
|
|
res.Element = col.Children(func() {
|
|
ui.Box(c).Size(k.D(w+8), k.D(pixel.LineHeight)).Role(ui.RoleNone).Draw(func(p *ui.Painter, r ui.Rect) {
|
|
if *lit < 0 {
|
|
return
|
|
}
|
|
g := k.Canvas(p, r)
|
|
name := pixel.Wrap(apps[*lit].Name, w+8, 1)[0]
|
|
x := max(0, min(g.W-pixel.Measure(name), 4+*lit*dockCell+dockCell/2-pixel.Measure(name)/2))
|
|
g.Text(x, 0, name, pixel.Ink, 1)
|
|
})
|
|
ui.Row(c).Padding(0, k.D(4)).FocusGroup(ui.Horizontal).Role(ui.RoleToolbar).Label(text).Draw(func(p *ui.Painter, r ui.Rect) {
|
|
g := k.Canvas(p, r)
|
|
g.Rect(0, g.H-6, g.W, 6, pixel.Face)
|
|
g.HLine(0, g.H-6, g.W, pixel.Lit)
|
|
g.HLine(0, g.H-1, g.W, pixel.Edge)
|
|
}).Children(func() {
|
|
for i, a := range apps {
|
|
// Keyed by name: apps come and go.
|
|
ui.Box(c).Key(a.Name).Children(func() {
|
|
shell, base, st := pixel.Shell(c, func() *ui.Element {
|
|
return ui.Box(c).Focusable().Role(ui.RoleButton).Label(a.Name).Disabled(o.Disabled)
|
|
})
|
|
up := st.Hover || st.Focus
|
|
if up {
|
|
*lit = i
|
|
}
|
|
// It rises eight dots in four steps, and sinks back.
|
|
rise := 2 * k.Toward(c, shell, up, 15, 4)
|
|
if !base.IsDisabled() && base.Clicked() {
|
|
res.Launched = i
|
|
}
|
|
shell.Size(k.D(dockCell), k.D(40)).Draw(func(p *ui.Painter, r ui.Rect) {
|
|
g := k.Canvas(p, r)
|
|
g.Sprite((dockCell-2*a.Icon.W)/2, 10-rise+2*(12-a.Icon.H), a.Icon, 0, pixel.Ink, 2)
|
|
g.Finish(st)
|
|
})
|
|
})
|
|
}
|
|
})
|
|
})
|
|
if res.Launched >= 0 {
|
|
slog.Debug("8bit: dock launched", "label", text, "app", apps[res.Launched].Name)
|
|
}
|
|
return res
|
|
}
|