MyGo v0.5.5 returns ui.Element as a checked value handle instead of a
pointer. Migrated with `mygo migrate-ui`, then fixed what the codemod
leaves to review:
- Element nil checks become .Valid(); absent elements are ui.Element{}.
- Input handlers and paint callbacks run outside the build pass, where
elements expire and Context methods are inert: read IsDisabled into a
local during the build, keep a ui.Handle for Focus, use c.Services()
for clipboard access, create ui.Local state during the build.
- Add core.AutoFocus: v0.5.5 records an overlay's opener after its
content builds, so same-frame autofocus lost focus restore on close.
It focuses one frame later; cascader rows defer their focus likewise.
- core.OverlayLayer identifies build passes by the root element, not
c.Now(), which repeats across passes and under the Tester clock.
- Context menus survive the opening press counted as PressedOutside;
split button tooltip no longer reorders its open menu; disabled
menubar rows pass presses to the panel; Form defers blur-started
async checks so the growing message no longer moves Submit away
from the release; Combobox drops its disabled copy now that MyGo
blocks typing under a disabled wrapper (canary flipped to a guard).
- The Tester clock is virtual: tests use Advance/Now instead of
time.Sleep/time.Now and read element state inside the view. The web
mirror advances the clock to wall time before each frame and input.
- AGENTS.md and docs describe the value Element API and the new pin.
Snapshot goldens are darwin-only and were not re-verified here.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
138 lines
4.1 KiB
Go
138 lines
4.1 KiB
Go
package input
|
|
|
|
import (
|
|
"fmt"
|
|
"log/slog"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/egoist/mygo"
|
|
"github.com/egoist/mygo/ui"
|
|
|
|
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/core"
|
|
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/icons"
|
|
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/theme"
|
|
)
|
|
|
|
// FilePickerOptions configure a FilePicker.
|
|
type FilePickerOptions struct {
|
|
// Directory picks folders instead of files.
|
|
Directory bool
|
|
Multiple bool
|
|
// Filters limit the files offered by extension, without the dot.
|
|
Filters []mygo.FileFilter
|
|
// Title is the system dialog's title.
|
|
Title string
|
|
// Label is the button text; empty uses the built-in "Choose file…" or "Choose folder…".
|
|
Label string
|
|
// Parent attaches the dialog to a window, as a sheet on macOS.
|
|
Parent *mygo.Window
|
|
Disabled bool
|
|
}
|
|
|
|
// FilePickerParts are the parts of a FilePicker.
|
|
type FilePickerParts struct {
|
|
Element ui.Element
|
|
changed bool
|
|
canceled bool
|
|
}
|
|
|
|
// Changed reports whether the user picked new paths this frame.
|
|
func (p FilePickerParts) Changed() bool { return p.changed }
|
|
|
|
// Canceled reports whether the user closed the dialog without picking, which is not an error.
|
|
func (p FilePickerParts) Canceled() bool { return p.canceled }
|
|
|
|
// filePickerOpen shows the system dialog; tests replace it.
|
|
var filePickerOpen = mygo.Dialog.Open
|
|
|
|
type filePickerResult struct {
|
|
paths []string
|
|
err error
|
|
}
|
|
|
|
type filePickerState struct {
|
|
result chan filePickerResult
|
|
err string
|
|
}
|
|
|
|
// FilePicker opens the system file dialog off the UI thread and stores the chosen paths.
|
|
func FilePicker(c *ui.Context, paths *[]string, opts FilePickerOptions) FilePickerParts {
|
|
for _, f := range opts.Filters {
|
|
for _, ext := range f.Extensions {
|
|
if ext == "" || strings.HasPrefix(ext, ".") {
|
|
panic(fmt.Sprintf("mujica: FilePicker extension %q: give it without the dot", ext))
|
|
}
|
|
}
|
|
}
|
|
k := core.Tokens(c)
|
|
d := core.Density(c)
|
|
parts := FilePickerParts{}
|
|
col := ui.Column(c).Gap(d.Unit() * 2).AlignItems(ui.Start)
|
|
parts.Element = col
|
|
st := ui.Local(col, "picker", func() filePickerState { return filePickerState{} })
|
|
if st.result != nil {
|
|
select {
|
|
case r := <-st.result:
|
|
st.result = nil
|
|
switch {
|
|
case r.err != nil:
|
|
st.err = fmt.Sprintf(core.Msg(c, "file_picker.failed"), r.err)
|
|
slog.Debug("mujica: file picker failed", "err", r.err)
|
|
case r.paths == nil:
|
|
parts.canceled = true
|
|
slog.Debug("mujica: file picker canceled")
|
|
default:
|
|
*paths = r.paths
|
|
parts.changed = true
|
|
slog.Debug("mujica: file picker picked", "count", len(r.paths))
|
|
}
|
|
default:
|
|
}
|
|
}
|
|
label := opts.Label
|
|
if label == "" {
|
|
label = core.Msg(c, "file_picker.choose_file")
|
|
if opts.Directory {
|
|
label = core.Msg(c, "file_picker.choose_folder")
|
|
}
|
|
}
|
|
icon := icons.Must("file")
|
|
if opts.Directory {
|
|
icon = icons.Must("folder")
|
|
}
|
|
col.Children(func() {
|
|
b := Button(c, label, ButtonOptions{Variant: Secondary, Icon: icon, Loading: st.result != nil, Disabled: opts.Disabled})
|
|
if b.Clicked() {
|
|
st.err = ""
|
|
ch := make(chan filePickerResult, 1)
|
|
st.result = ch
|
|
dlg := mygo.OpenDialogOptions{Parent: opts.Parent, Title: opts.Title, Filters: opts.Filters,
|
|
Directory: opts.Directory, Multiple: opts.Multiple}
|
|
slog.Debug("mujica: file picker opened", "directory", opts.Directory, "multiple", opts.Multiple)
|
|
go func() {
|
|
p, err := filePickerOpen(dlg)
|
|
ch <- filePickerResult{p, err}
|
|
c.Invalidate()
|
|
}()
|
|
}
|
|
switch {
|
|
case st.err != "":
|
|
ui.Text(c, st.err).TextColor(k.Danger).FontSize(core.FontSize(c, theme.CaptionSize)).Role(ui.RoleStatus)
|
|
case len(*paths) == 0:
|
|
ui.Text(c, core.Msg(c, "file_picker.none")).TextColor(k.TextMuted).FontSize(core.FontSize(c, theme.CaptionSize))
|
|
default:
|
|
ui.Column(c).Gap(d.Unit()).Children(func() {
|
|
for _, p := range *paths {
|
|
ui.Row(c.Key(p)).Gap(d.Unit() * 2).AlignItems(ui.Center).Children(func() {
|
|
ui.Icon(c, icon).Size(14, 14).TextColor(k.TextMuted)
|
|
ui.Text(c, filepath.Base(p)).SingleLine()
|
|
ui.Text(c, filepath.Dir(p)).TextColor(k.TextMuted).FontSize(core.FontSize(c, theme.CaptionSize)).SingleLine().Ellipsis("…").MinWidth(0).Shrink(1)
|
|
})
|
|
}
|
|
})
|
|
}
|
|
})
|
|
return parts
|
|
}
|