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>
180 lines
5.6 KiB
Go
180 lines
5.6 KiB
Go
package data
|
|
|
|
import (
|
|
"fmt"
|
|
"log/slog"
|
|
"maps"
|
|
"slices"
|
|
|
|
"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/input"
|
|
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/internal"
|
|
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/theme"
|
|
)
|
|
|
|
// tableColumn is what the column menu knows of a column.
|
|
type tableColumn struct {
|
|
ID, Title string
|
|
Width, MinWidth float32
|
|
Sortable bool
|
|
}
|
|
|
|
// tableWidthStep is how far one Narrower or Wider press resizes a column.
|
|
const tableWidthStep float32 = 16
|
|
|
|
// tableColumnMenu is the keyboard path to sort, move, resize and, with hidden, show columns.
|
|
func tableColumnMenu(c *ui.Context, cols []tableColumn, sort *ui.SortOrder, layout *ui.TableLayout, hidden *map[string]bool) {
|
|
k := core.Tokens(c)
|
|
u := core.Density(c).Unit()
|
|
b := input.Button(c, core.Msg(c, "data_table.columns"), input.ButtonOptions{Variant: input.Secondary, Icon: icons.Must("settings")})
|
|
open := ui.Local(b, "open", func() bool { return false })
|
|
if b.Clicked() {
|
|
*open = !*open
|
|
}
|
|
order := tableOrder(cols, *layout)
|
|
visible := len(cols)
|
|
if hidden != nil {
|
|
for _, col := range cols {
|
|
if (*hidden)[col.ID] {
|
|
visible--
|
|
}
|
|
}
|
|
}
|
|
ui.PopoverBase(c, b, open, func(panel ui.Element) {
|
|
panel.Background(k.Surface).Border(theme.BorderWidth, k.ControlBorder).Radius(theme.PopupRadius).
|
|
Padding(u * 2).Gap(u).MinWidth(240).MarginY(u)
|
|
for at, id := range order {
|
|
col := cols[slices.IndexFunc(cols, func(col tableColumn) bool { return col.ID == id })]
|
|
ui.Row(c.Key(id)).Gap(u).AlignItems(ui.Center).Children(func() {
|
|
if hidden != nil {
|
|
tableVisibility(c, col, hidden, visible)
|
|
} else {
|
|
ui.Text(c, col.Title).SingleLine().Grow(1).Padding(0, u*2)
|
|
}
|
|
tableColumnActions(c, col, at, order, sort, layout)
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
// tableColumnActions are one column's sort, move and width buttons.
|
|
func tableColumnActions(c *ui.Context, col tableColumn, at int, order []string, sort *ui.SortOrder, layout *ui.TableLayout) {
|
|
act := func(icon, key string, disabled bool) bool {
|
|
return input.Button(c, "", input.ButtonOptions{Variant: input.Ghost, Icon: icons.Must(icon), Label: fmt.Sprintf(core.Msg(c, key), col.Title), Disabled: disabled}).Clicked()
|
|
}
|
|
if col.Sortable {
|
|
for _, desc := range []bool{false, true} {
|
|
icon, key := "arrow-up", "data_table.sortAsc"
|
|
if desc {
|
|
icon, key = "arrow-down", "data_table.sortDesc"
|
|
}
|
|
if act(icon, key, *sort == ui.SortOrder{Column: col.ID, Descending: desc}) {
|
|
*sort = ui.SortOrder{Column: col.ID, Descending: desc}
|
|
slog.Debug("mujica: table sort", "column", col.ID, "descending", desc)
|
|
}
|
|
}
|
|
}
|
|
for _, step := range []int{-1, 1} {
|
|
icon, key := "chevron-left", "data_table.moveLeft"
|
|
if step > 0 {
|
|
icon, key = "chevron-right", "data_table.moveRight"
|
|
}
|
|
to := at + step
|
|
if act(icon, key, to < 0 || to >= len(order)) {
|
|
ids := slices.Clone(order)
|
|
ids[at], ids[to] = ids[to], ids[at]
|
|
layout.Order = ids
|
|
slog.Debug("mujica: table move", "column", col.ID, "to", to)
|
|
}
|
|
}
|
|
w := tableWidth(col, *layout)
|
|
least := max(col.MinWidth, tableWidthStep)
|
|
if act("minus", "data_table.narrower", w <= least) {
|
|
tableSetWidth(layout, col.ID, max(least, w-tableWidthStep))
|
|
}
|
|
if act("plus", "data_table.wider", false) {
|
|
tableSetWidth(layout, col.ID, w+tableWidthStep)
|
|
}
|
|
}
|
|
|
|
// tableVisibility is a column's show/hide check box; the last visible one cannot hide.
|
|
func tableVisibility(c *ui.Context, col tableColumn, hidden *map[string]bool, visible int) {
|
|
k := core.Tokens(c)
|
|
d := core.Density(c)
|
|
u := d.Unit()
|
|
on := !(*hidden)[col.ID]
|
|
last := on && visible == 1
|
|
cb := ui.CheckboxBase(c, &on).Label(col.Title).Disabled(last).MinHeight(d.ControlHeight()).Grow(1).
|
|
Padding(0, u*2).Gap(u * 2).AlignItems(ui.Center).Radius(theme.ControlRadius)
|
|
internal.Ring(cb, k.Focus, theme.ControlRadius)
|
|
if cb.Hovered() && !last {
|
|
cb.Background(k.SurfaceHover)
|
|
}
|
|
if cb.Changed() && !last {
|
|
m := maps.Clone(*hidden)
|
|
if m == nil {
|
|
m = map[string]bool{}
|
|
}
|
|
m[col.ID] = !on
|
|
*hidden = m
|
|
slog.Debug("mujica: table column", "id", col.ID, "visible", on)
|
|
}
|
|
cb.Children(func() {
|
|
dataTableCheckBox(c, on)
|
|
ui.Text(c, col.Title).SingleLine()
|
|
})
|
|
}
|
|
|
|
// tableOrder lists column IDs as the layout orders them, the rest after.
|
|
func tableOrder(cols []tableColumn, l ui.TableLayout) []string {
|
|
var ids []string
|
|
for _, id := range l.Order {
|
|
if slices.ContainsFunc(cols, func(col tableColumn) bool { return col.ID == id }) && !slices.Contains(ids, id) {
|
|
ids = append(ids, id)
|
|
}
|
|
}
|
|
for _, col := range cols {
|
|
if !slices.Contains(ids, col.ID) {
|
|
ids = append(ids, col.ID)
|
|
}
|
|
}
|
|
return ids
|
|
}
|
|
|
|
// tableWidth is a column's set width; a sharing column starts from its least, at least 120.
|
|
func tableWidth(col tableColumn, l ui.TableLayout) float32 {
|
|
if w, ok := l.Widths[col.ID]; ok {
|
|
return w
|
|
}
|
|
if col.Width > 0 {
|
|
return col.Width
|
|
}
|
|
return max(col.MinWidth, 120)
|
|
}
|
|
|
|
func tableSetWidth(l *ui.TableLayout, id string, w float32) {
|
|
m := maps.Clone(l.Widths)
|
|
if m == nil {
|
|
m = map[string]float32{}
|
|
}
|
|
m[id] = w
|
|
l.Widths = m
|
|
slog.Debug("mujica: table width", "column", id, "width", w)
|
|
}
|
|
|
|
// dataTableCheckBox draws a 16 DIP check box: OnAccent check on Accent when on.
|
|
func dataTableCheckBox(c *ui.Context, on bool) {
|
|
k := core.Tokens(c)
|
|
box := ui.Box(c).Size(16, 16).Shrink(0).Radius(theme.ControlRadius).Center()
|
|
if on {
|
|
box.Background(k.Accent).TextColor(k.OnAccent).Children(func() {
|
|
ui.Icon(c, icons.Must("check")).Size(14, 14)
|
|
})
|
|
} else {
|
|
box.Border(theme.BorderWidth, k.ControlBorder).Background(k.Surface)
|
|
}
|
|
}
|