Files
ZacharyZhang-NY b4b4d67b92 Audit keyboard, focus, veils, motion and clocks across all 500
A cross-cutting pass, each fix with a test that failed first.

Veils: MyGo still hands a key to a base that turned off this frame,
so every element key now goes through pixel.Shortcut, which an
archtest enforces. Input handlers record and the build applies them
only while enabled; the editor turns away a veil-frame letter or
copy; value bases get a copy and keep only enabled turns. Popups
give a held focus back to their opener when the veil lifts.

Motion: S5 is enforced at the clock. Every Tick, Since, Step,
Toward and Shot runs at 4-15 fps; blinks keep 1 Hz on 4 fps steps,
fast animations step at 15, and drawn time is quantized to frames.

Clocks stepping back: Since holds at zero without overflow, and
k.Elapsed moves a stored start to now, so nothing goes negative,
runs backward or indexes out of range.

Also: logs count typed text instead of holding it, gallery names
use glyphs the font has, removable rows are keyed, components take
their own Options (archtest), and sweeps over all 508 demos check
veiled keys, clicks, wheels, the clock stepping back and names.
2026-10-12 16:03:04 -04:00

174 lines
6.2 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package gallery
import (
"fmt"
"slices"
"time"
"github.com/egoist/mygo/ui"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/files"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/icon"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/input"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
)
var filesEntries = []Entry{
{406, "FileExplorer", "文件管理器", "files", "Folders on the left, their files on the right. Double click opens.", demoFileExplorer},
{407, "FileGrid", "文件网格", "files", "Big icons over short names. Arrows walk the grid; Enter opens.", demoFileGrid},
{408, "FileIcon", "文件图标", "files", "Six kinds, placed dot by dot: folder, text, image, audio, code, archive.", demoFileIcon},
{409, "PathBar", "路径栏", "files", "Where you are, folder by folder. Click one to climb back up.", demoPathBar},
{410, "MemoryCard", "记忆卡", "files", "Fifteen blocks, like the grey card of old. DELETE frees one.", demoMemoryCard},
{411, "DiskUsage", "磁盘占用", "files", "Where the room went, stacked in one bar.", demoDiskUsage},
{412, "UploadQueue", "上传队列", "files", "Files on their way up. × calls one back.", demoUploadQueue},
{413, "Trash", "回收站", "files", "A bin that looks full when it is. EMPTY takes it out.", demoTrash},
}
// disk is the demo's drive, from C: down.
var disk = files.Dir{Name: "C:", Files: []files.File{{Name: "AUTOEXEC.BAT", Kind: files.Code, Size: 512}}, Dirs: []files.Dir{
{Name: "GAMES", Dirs: []files.Dir{{Name: "HERO QUEST", Files: []files.File{
{Name: "SAVE1.SAV", Kind: files.Archive, Size: 8192},
{Name: "MAP.PNG", Kind: files.Image, Size: 48_000},
{Name: "THEME.MID", Kind: files.Audio, Size: 21_504},
{Name: "CHEATS.TXT", Kind: files.Text, Size: 1_337},
}}}, Files: []files.File{{Name: "README.TXT", Kind: files.Text, Size: 2_048}}},
{Name: "MUSIC", Files: []files.File{
{Name: "BOSS FIGHT.MID", Kind: files.Audio, Size: 30_720},
{Name: "CREDITS.MID", Kind: files.Audio, Size: 12_288},
}},
}}
// path is the demo path's folder names, root first.
var path = []string{"C:", "GAMES", "HERO QUEST", "SAVES"}
type filesDemo struct {
dir string
file, grid, block, trash, at int
saves []files.Save
ups []files.Upload
started time.Time
}
func filesState(c *ui.Context) *filesDemo {
return ui.Local(c.Root(), demoKey("files"), func() filesDemo {
return filesDemo{dir: "0/0/0", file: -1, grid: -1, trash: 3, at: len(path) - 1, saves: []files.Save{
{Title: "HERO QUEST", Icon: icon.Sword},
{Title: "SPACE RACE", Icon: icon.Star, Slot: pixel.Warn},
{Title: "MONSTER FARM", Icon: icon.Heart, Slot: pixel.Bad},
{Title: "PUZZLE TOWER", Icon: icon.Gem, Slot: pixel.Magic},
{Title: "FISHING DAY", Icon: icon.Drop, Slot: pixel.Info},
}, ups: []files.Upload{
{Name: "SPEEDRUN.ZIP", Kind: files.Archive, Size: 9 << 20},
{Name: "BOSS.PNG", Kind: files.Image, Size: 3 << 20},
{Name: "LEVEL 9.MAP", Kind: files.Code, Size: 1 << 20, Sent: 1 << 19, Failed: true},
}}
})
}
func demoFileExplorer(c *ui.Context, log func(string)) {
s := filesState(c)
r := files.FileExplorer(c, "Explorer", disk, &s.dir, &s.file, files.FileExplorerOptions{})
if r.Changed {
log(fmt.Sprintf("folder %s, file %d", s.dir, s.file))
}
if r.Opened {
log(fmt.Sprintf("open file %d", s.file))
}
}
func demoFileGrid(c *ui.Context, log func(string)) {
s := filesState(c)
all := slices.Concat(disk.Dirs[0].Dirs[0].Files, []files.File{{Name: "SECRET LEVEL", Kind: files.Folder}})
r := files.FileGrid(c, "Files", all, &s.grid, files.FileGridOptions{Height: 150})
if r.Changed {
log("chose " + all[s.grid].Name)
}
if r.Opened {
log("open " + all[s.grid].Name)
}
}
func demoFileIcon(c *ui.Context, log func(string)) {
k := pixel.K(c)
kinds := []files.Kind{files.Folder, files.Text, files.Image, files.Audio, files.Code, files.Archive}
ui.Column(c).Gap(k.D(6)).Children(func() {
for _, scale := range []int{1, 2} {
ui.Row(c).Gap(k.D(6)).Children(func() {
for _, kind := range kinds {
files.FileIcon(c, kind, files.FileIconOptions{Scale: scale})
}
})
}
})
}
func demoPathBar(c *ui.Context, log func(string)) {
s := filesState(c)
// Climbing up trims the path; the reset walks back down.
r := files.PathBar(c, "Path", path[:s.at+1], files.PathBarOptions{})
if r.Clicked >= 0 {
s.at = r.Clicked
log("up to " + path[s.at])
}
if s.at < len(path)-1 && input.Button(c, "BACK DOWN", input.ButtonOptions{Size: input.Small}).Clicked {
s.at = len(path) - 1
log("down to " + path[s.at])
}
}
func demoMemoryCard(c *ui.Context, log func(string)) {
s := filesState(c)
r := files.MemoryCard(c, "Memory card 1", s.saves, &s.block, files.MemoryCardOptions{})
if r.Changed {
log(fmt.Sprintf("block %d", s.block+1))
}
if r.Deleted >= 0 {
log("deleted " + s.saves[r.Deleted].Title)
s.saves = append(s.saves[:r.Deleted:r.Deleted], s.saves[r.Deleted+1:]...)
}
}
func demoDiskUsage(c *ui.Context, log func(string)) {
files.DiskUsage(c, "Disk", []files.Use{
{Name: "GAMES", Bytes: 41 << 30}, {Name: "SCREENSHOTS", Bytes: 6 << 30},
{Name: "MUSIC", Bytes: 12 << 30}, {Name: "SAVES", Bytes: 300 << 20},
}, 128<<30, files.DiskUsageOptions{Width: 220})
}
func demoUploadQueue(c *ui.Context, log func(string)) {
k := pixel.K(c)
s := filesState(c)
if s.started.IsZero() {
s.started = k.Now()
}
// The demo sends a megabyte a second, row by row.
sent := int64(k.Since(c, s.started, 4, 60)) << 18
ups := make([]files.Upload, len(s.ups))
for i, u := range s.ups {
if !u.Failed {
u.Sent = min(u.Size, sent)
sent = max(0, sent-u.Size)
}
ups[i] = u
}
if r := files.UploadQueue(c, "Uploads", ups, files.UploadQueueOptions{}); r.Canceled >= 0 {
log("canceled " + s.ups[r.Canceled].Name)
s.ups = append(s.ups[:r.Canceled:r.Canceled], s.ups[r.Canceled+1:]...)
}
}
func demoTrash(c *ui.Context, log func(string)) {
k := pixel.K(c)
s := filesState(c)
ui.Column(c).Gap(k.D(6)).Children(func() {
if files.Trash(c, "Trash", s.trash, files.TrashOptions{}).Emptied {
log(fmt.Sprintf("emptied %d", s.trash))
s.trash = 0
}
if input.Button(c, "THROW AWAY", input.ButtonOptions{Size: input.Small}).Clicked {
s.trash++
log(fmt.Sprintf("threw one away: %d", s.trash))
}
})
}