Files
8bitMyGoUI/files/fileexplorer.go
T
ZacharyZhang-NY 552b073e81 Add files components 406-420; Tree opens to a known choice
Fifteen file components: FileExplorer, FileGrid, FileIcon, PathBar,
MemoryCard, DiskUsage, UploadQueue, Trash, RecentFiles, FilePreview,
ArchiveViewer, FileDetails, SharePanel, SyncStatus and QuotaMeter.

FileIcon draws six kinds as hand-placed 12x12 pages and a folder. The
browsers build on data.Tree, List, GridView and Table; ArchiveViewer
owns its sort and keeps the caller's index on its entry. UploadQueue
keys rows by name so focus stays on its row. SharePanel never logs the
link. SyncStatus turns its arrows on Busy steps, so syncing moves under
Reduce Motion.

data.Tree now opens the closed branches above a chosen path it knows,
so a caller's starting path survives the first frame.
2026-10-12 09:27:26 -04:00

102 lines
2.9 KiB
Go

package files
import (
"fmt"
"log/slog"
"strconv"
"strings"
"github.com/egoist/mygo/ui"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/data"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
)
// Dir is a named folder of folders and files.
type Dir struct {
Name string
Dirs []Dir
Files []File
}
type FileExplorerOptions struct {
// Height is the file list's in dots; zero means 120.
Height int
Disabled bool
}
type FileExplorerResult struct {
Element *ui.Element
// Opened is Enter or a double click on a file.
Changed, Opened bool
}
// FileExplorer sets the folder tree beside the chosen folder's files.
func FileExplorer(c *ui.Context, text string, root Dir, path *string, file *int, o FileExplorerOptions) FileExplorerResult {
k := pixel.K(c)
dir, ok := folderAt(root, *path)
if !ok {
panic(fmt.Sprintf("files: FileExplorer has no folder at %q", *path))
}
pick("FileExplorer", file, len(dir.Files))
h := pixel.Or("FileExplorer Height", o.Height, 120)
tree, tw := node(root, 0)
var res FileExplorerResult
res.Element = ui.Row(c).FillWidth().Gap(k.D(4)).AlignItems(ui.Start).Role(ui.RoleGroup).Label(text).Children(func() {
old := *path
data.Tree(c, k.Msg("folders"), []data.TreeNode{tree}, path, data.TreeOptions{Width: tw, Disabled: o.Disabled})
if *path != old {
// A new folder starts with no file chosen.
dir, _ = folderAt(root, *path)
*file, res.Changed = -1, true
slog.Debug("8bit: file explorer folder", "label", text, "path", *path)
}
files := dir.Files
for _, f := range files {
check("FileExplorer", f)
}
l := data.List(c, dir.Name, len(files), file, data.ListOptions{Height: h, Disabled: o.Disabled}, func(i int) string { return files[i].Name }, func(i int) {
row(c, k, files[i], func() { pixel.Text(c, size(files[i].Size), pixel.TextOptions{Slot: pixel.Faint}) })
})
l.Element.Grow(1)
res.Changed = res.Changed || l.Changed
res.Opened = l.Submitted && *file >= 0
})
if res.Opened {
slog.Debug("8bit: file explorer open", "label", text, "file", *file)
}
return res
}
// node makes a folder's tree and the width it needs.
func node(f Dir, depth int) (data.TreeNode, int) {
if f.Name == "" {
panic("files: FileExplorer has a folder with no name")
}
// data.Tree indents ten dots a level; the arrow takes twelve.
n, w := data.TreeNode{Label: f.Name}, depth*10+16+pixel.Measure(f.Name)
for _, sub := range f.Dirs {
child, cw := node(sub, depth+1)
n.Children, w = append(n.Children, child), max(w, cw)
}
return n, w
}
// folderAt follows a tree path, "0/1", down from root.
func folderAt(root Dir, path string) (Dir, bool) {
parts := strings.Split(path, "/")
if parts[0] != "0" {
return Dir{}, false
}
f := root
for _, p := range parts[1:] {
// Only the plain form, as data.Tree writes it, is known.
i, err := strconv.Atoi(p)
if err != nil || strconv.Itoa(i) != p || i < 0 || i >= len(f.Dirs) {
return Dir{}, false
}
f = f.Dirs[i]
}
return f, true
}