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>
173 lines
5.3 KiB
Go
173 lines
5.3 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"log/slog"
|
|
"time"
|
|
|
|
"github.com/egoist/mygo/ui"
|
|
|
|
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/core"
|
|
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/feedback"
|
|
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/icons"
|
|
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/input"
|
|
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/layout"
|
|
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/navigation"
|
|
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/theme"
|
|
)
|
|
|
|
// event is one line of the gallery's event log.
|
|
type event struct {
|
|
at time.Time
|
|
page string
|
|
text string
|
|
}
|
|
|
|
type gallery struct {
|
|
pages []page
|
|
byID map[string]int
|
|
selected string
|
|
query string
|
|
mode int
|
|
density int
|
|
zh bool
|
|
folded bool
|
|
autoFold bool
|
|
palette bool
|
|
events []event
|
|
scrolls map[string]*ui.ScrollState
|
|
overture overtureState
|
|
// Composition state lives here so it survives page switches.
|
|
cSettings settingsDemo
|
|
cFiles filesDemo
|
|
cForm formSubmitDemo
|
|
}
|
|
|
|
// densities maps the density control's segments to theme densities.
|
|
var densities = []theme.Density{theme.Compact, theme.Default, theme.Comfortable}
|
|
|
|
func newGallery() *gallery {
|
|
g := &gallery{pages: catalog(), byID: map[string]int{}, selected: overtureID, density: 1, scrolls: map[string]*ui.ScrollState{},
|
|
cSettings: newSettingsDemo(), cFiles: newFilesDemo(), cForm: newFormSubmitDemo()}
|
|
for i, p := range g.pages {
|
|
if _, dup := g.byID[p.ID]; dup {
|
|
panic("gallery: duplicate page " + p.ID)
|
|
}
|
|
g.byID[p.ID] = i
|
|
}
|
|
return g
|
|
}
|
|
|
|
// logFor returns the event logger of one page.
|
|
func (g *gallery) logFor(id string) func(string) {
|
|
return func(s string) {
|
|
g.events = append(g.events, event{time.Now(), id, s})
|
|
if len(g.events) > 200 {
|
|
g.events = g.events[len(g.events)-200:]
|
|
}
|
|
slog.Info("gallery: event", "page", id, "event", s)
|
|
}
|
|
}
|
|
|
|
func (g *gallery) settings() core.Settings {
|
|
s := core.Settings{Mode: core.Mode(g.mode), Density: densities[g.density], Locale: core.EnUS}
|
|
if g.zh {
|
|
s.Locale = core.ZhCN
|
|
}
|
|
return s
|
|
}
|
|
|
|
func (g *gallery) view(c *ui.Context) {
|
|
core.Use(c, g.settings())
|
|
w, _ := c.Size()
|
|
if narrow := w < 900; narrow != g.autoFold {
|
|
g.autoFold, g.folded = narrow, narrow
|
|
}
|
|
scroll := g.scrolls[g.selected]
|
|
if scroll == nil {
|
|
scroll = &ui.ScrollState{}
|
|
g.scrolls[g.selected] = scroll
|
|
}
|
|
layout.AppShell(c, &g.folded, layout.AppShellOptions{
|
|
TitleBar: func() { g.titleBar(c) },
|
|
Sidebar: func() { g.sidebar(c) },
|
|
Content: func() { g.content(c) },
|
|
StatusBar: func() { g.statusBar(c) },
|
|
ContentScroll: scroll,
|
|
})
|
|
g.commandPalette(c)
|
|
feedback.Toaster(c)
|
|
}
|
|
|
|
func (g *gallery) titleBar(c *ui.Context) {
|
|
layout.TitleBar(c, "MujicaUI", layout.TitleBarOptions{Trailing: func() {
|
|
ui.Row(c).Gap(8).AlignItems(ui.Center).Children(func() {
|
|
mode := g.mode
|
|
if navigation.SegmentedControl(c, &g.mode, []navigation.Segment{
|
|
{Label: g.t("System", "跟随系统"), Icon: icons.Must("settings"), HideLabel: true},
|
|
{Label: g.t("Light", "浅色"), Icon: icons.Must("sun"), HideLabel: true},
|
|
{Label: g.t("Dark", "深色"), Icon: icons.Must("moon"), HideLabel: true},
|
|
}, navigation.SegmentedControlOptions{Label: g.t("Theme", "主题")}).Changed() && mode != g.mode {
|
|
g.logFor("shell")("theme " + core.Mode(g.mode).String())
|
|
g.overture.switches++
|
|
}
|
|
density := g.density
|
|
if navigation.SegmentedControl(c, &g.density, []navigation.Segment{
|
|
{Label: g.t("Compact", "紧凑")}, {Label: g.t("Default", "默认")}, {Label: g.t("Comfortable", "宽松")},
|
|
}, navigation.SegmentedControlOptions{Label: g.t("Density", "密度")}).Changed() && density != g.density {
|
|
g.logFor("shell")("density " + densities[g.density].String())
|
|
g.overture.switches++
|
|
}
|
|
if input.Button(c, g.t("中文", "English"), input.ButtonOptions{Variant: input.Ghost}).Clicked() {
|
|
g.zh = !g.zh
|
|
g.logFor("shell")(fmt.Sprintf("locale %s", g.settings().Locale))
|
|
}
|
|
if input.IconButton(c, icons.Must("command"), g.t("Command palette", "命令面板"), input.IconButtonOptions{Variant: input.Ghost}).Clicked() {
|
|
g.palette = true
|
|
}
|
|
})
|
|
}})
|
|
}
|
|
|
|
func (g *gallery) statusBar(c *ui.Context) {
|
|
left := []layout.StatusItem{
|
|
{ID: "count", Text: fmt.Sprintf(g.t("%d components", "%d 个组件"), len(g.pages)), Icon: icons.Must("layout-grid")},
|
|
{ID: "look", Text: fmt.Sprintf("%s · %s", core.Mode(g.mode), densities[g.density]), Secondary: true},
|
|
}
|
|
var right []layout.StatusItem
|
|
if n := len(g.events); n > 0 {
|
|
e := g.events[n-1]
|
|
right = append(right, layout.StatusItem{ID: "last", Text: e.page + " · " + e.text, Icon: icons.Must("bell"), Secondary: true})
|
|
}
|
|
layout.StatusBar(c, layout.StatusBarOptions{Left: left, Right: right})
|
|
}
|
|
|
|
func (g *gallery) content(c *ui.Context) {
|
|
ui.Box(c.Key(g.selected)).FillWidth().Children(func() {
|
|
switch g.selected {
|
|
case overtureID:
|
|
g.overturePage(c)
|
|
case settingsID:
|
|
g.compositePage(c, settingsID, g.settingsView)
|
|
case filesID:
|
|
g.compositePage(c, filesID, g.filesView)
|
|
case formID:
|
|
g.compositePage(c, formID, g.formView)
|
|
default:
|
|
i, ok := g.byID[g.selected]
|
|
if !ok {
|
|
panic("gallery: no page " + g.selected)
|
|
}
|
|
g.componentPage(c, g.pages[i])
|
|
}
|
|
})
|
|
}
|
|
|
|
// t picks the English or Chinese copy for the gallery's own text.
|
|
func (g *gallery) t(en, zh string) string {
|
|
if g.zh {
|
|
return zh
|
|
}
|
|
return en
|
|
}
|