Upgrade MyGo to v0.5.5

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>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-17 01:37:54 -04:00
1 parent bc716d27be
commit bcce34cda1
603 files changed
+1366 -1277

No files matched your search

+2 -2
View File
@@ -55,7 +55,7 @@ func AlertDialog(c *ui.Context, open *bool, opts AlertDialogOptions) AlertDialog
cancelLabel = core.Msg(c, "alert_dialog.cancel")
}
shown, backClicked := true, false
panel := ui.DialogBase(c, &shown, func(back, panel *ui.Element) {
panel := ui.DialogBase(c, &shown, func(back, panel ui.Element) {
backClicked = back.Clicked()
st := ui.Local(panel, "mujica.alert", func() alertDialogState { return alertDialogState{} })
p := core.OverlayEnter(c, panel)
@@ -77,7 +77,7 @@ func AlertDialog(c *ui.Context, open *bool, opts AlertDialogOptions) AlertDialog
}
unlocked := opts.ConfirmText == "" || st.typed == opts.ConfirmText
dialogActions(c, func() {
if input.Button(c, cancelLabel, input.ButtonOptions{Variant: input.Secondary}).AutoFocus().Clicked() {
if core.AutoFocus(c, input.Button(c, cancelLabel, input.ButtonOptions{Variant: input.Secondary})).Clicked() {
res.Canceled = true
}
if input.Button(c, confirmLabel, input.ButtonOptions{Variant: input.Danger, Disabled: !unlocked}).Clicked() {
+13 -8
View File
@@ -13,13 +13,13 @@ type ContextMenuOptions struct {
// contextMenuState is where the menu opened, relative to the target.
type contextMenuState struct {
open bool
x, y float32
open, fresh bool // fresh: opened by a press the menu has not yet seen
x, y float32
}
// ContextMenu gives the container target an in-window menu at the pointer or below the focus.
func ContextMenu(c *ui.Context, target *ui.Element, opts ContextMenuOptions, build func(m *PopupMenu)) *ui.Element {
if target == nil {
func ContextMenu(c *ui.Context, target ui.Element, opts ContextMenuOptions, build func(m *PopupMenu)) ui.Element {
if !target.Valid() {
panic("mujica: ContextMenu needs a target")
}
if build == nil {
@@ -34,7 +34,7 @@ func ContextMenu(c *ui.Context, target *ui.Element, opts ContextMenuOptions, bui
if ev.Kind != ui.InputPointerDown || ev.Button != 1 {
return false
}
st.open, st.x, st.y = true, ev.X, ev.Y
st.open, st.fresh, st.x, st.y = true, true, ev.X, ev.Y
slog.Debug("mujica: context_menu open", "by", "pointer")
return true
})
@@ -45,10 +45,15 @@ func ContextMenu(c *ui.Context, target *ui.Element, opts ContextMenuOptions, bui
if !st.open {
return target
}
var anchor *ui.Element
var anchor ui.Element
target.Children(func() {
anchor = ui.Box(c).Key("mujica.contextmenu.anchor").Absolute().Left(st.x).Top(st.y).Size(1, 1).PassThrough()
anchor = ui.Box(c.Key("mujica.contextmenu.anchor")).Absolute().Left(st.x).Top(st.y).Size(1, 1).PassThrough()
})
PopupMenuOpen(c, anchor, &st.open, build)
panel := PopupMenuOpen(c, anchor, &st.open, build)
// The opening press fell outside the new panel; it must not dismiss it.
if st.fresh && panel.Valid() && panel.Dismissed() {
st.open = true
}
st.fresh = false
return target
}
+4 -4
View File
@@ -30,19 +30,19 @@ type DialogOptions struct {
}
// Dialog shows a modal dialog while *open is true, trapping and then restoring the focus.
func Dialog(c *ui.Context, open *bool, opts DialogOptions, content func()) *ui.Element {
func Dialog(c *ui.Context, open *bool, opts DialogOptions, content func()) ui.Element {
if opts.Title == "" {
panic("mujica: Dialog needs a Title")
}
site, focus := core.OverlaySite(c)
if !*open {
focus.Closed(c, site)
return nil
return ui.Element{}
}
core.OverlayLayer(c, open)
k := core.Tokens(c)
shown, backClicked, closed := true, false, false
panel := ui.DialogBase(c, &shown, func(back, panel *ui.Element) {
panel := ui.DialogBase(c, &shown, func(back, panel ui.Element) {
backClicked = back.Clicked()
p := core.OverlayEnter(c, panel)
back.Background(OverlayScrim(k).Alpha(p)).Padding(16)
@@ -81,7 +81,7 @@ func Dialog(c *ui.Context, open *bool, opts DialogOptions, content func()) *ui.E
}
// DialogPanel styles a centered dialog panel at entrance progress p.
func DialogPanel(c *ui.Context, panel *ui.Element, width float32, decorated bool, p float32) {
func DialogPanel(c *ui.Context, panel ui.Element, width float32, decorated bool, p float32) {
k := core.Tokens(c)
u := core.Density(c).Unit()
if width == 0 {
+8 -8
View File
@@ -32,7 +32,7 @@ type DrawerOptions struct {
}
// Drawer shows a panel along a window edge while *open is true; Escape closes it.
func Drawer(c *ui.Context, open *bool, opts DrawerOptions, content func()) *ui.Element {
func Drawer(c *ui.Context, open *bool, opts DrawerOptions, content func()) ui.Element {
if opts.Title == "" {
panic("mujica: Drawer needs a Title")
}
@@ -45,12 +45,12 @@ func Drawer(c *ui.Context, open *bool, opts DrawerOptions, content func()) *ui.E
site, focus := core.OverlaySite(c)
if !*open {
focus.Closed(c, site)
return nil
return ui.Element{}
}
k := core.Tokens(c)
size := drawerSize(c, opts)
closed := false
body := func(panel *ui.Element, p float32) {
body := func(panel ui.Element, p float32) {
drawerPanel(c, panel, opts.Side, size, p)
panel.Label(opts.Title)
closed = dialogHeader(c, opts.Title, false, true, "drawer.close")
@@ -61,7 +61,7 @@ func Drawer(c *ui.Context, open *bool, opts DrawerOptions, content func()) *ui.E
dialogActions(c, opts.Actions)
}
}
var panel *ui.Element
var panel ui.Element
escaped := false
if opts.NonModal {
ui.Overlay(c, func() {
@@ -74,7 +74,7 @@ func Drawer(c *ui.Context, open *bool, opts DrawerOptions, content func()) *ui.E
} else {
core.OverlayLayer(c, open)
shown := true
panel = ui.DialogBase(c, &shown, func(back, panel *ui.Element) {
panel = ui.DialogBase(c, &shown, func(back, panel ui.Element) {
p := core.OverlayEnter(c, panel)
back.Background(OverlayScrim(k).Alpha(p))
drawerBackdrop(back, opts.Side)
@@ -107,7 +107,7 @@ func drawerSize(c *ui.Context, opts DrawerOptions) float32 {
func drawerHorizontal(s DrawerSide) bool { return s == DrawerLeft || s == DrawerRight }
// drawerBackdrop lines the panel up against its edge inside a modal backdrop.
func drawerBackdrop(back *ui.Element, s DrawerSide) {
func drawerBackdrop(back ui.Element, s DrawerSide) {
if drawerHorizontal(s) {
back.Row()
}
@@ -118,7 +118,7 @@ func drawerBackdrop(back *ui.Element, s DrawerSide) {
}
// drawerPlace puts a non-modal panel against its edge, shown by size.
func drawerPlace(c *ui.Context, panel *ui.Element, s DrawerSide, size float32) {
func drawerPlace(c *ui.Context, panel ui.Element, s DrawerSide, size float32) {
w, h := c.Size()
switch s {
case DrawerRight:
@@ -133,7 +133,7 @@ func drawerPlace(c *ui.Context, panel *ui.Element, s DrawerSide, size float32) {
}
// drawerPanel styles the panel at entrance progress p, sliding in from its edge.
func drawerPanel(c *ui.Context, panel *ui.Element, s DrawerSide, size, p float32) {
func drawerPanel(c *ui.Context, panel ui.Element, s DrawerSide, size, p float32) {
k := core.Tokens(c)
u := core.Density(c).Unit()
off := -(1 - p) * size
+1 -1
View File
@@ -28,7 +28,7 @@ func (a *drawerApp) view(c *ui.Context) {
})
if p := Drawer(c, &a.open, a.opts, func() {
input.Button(c, "Apply", input.ButtonOptions{})
}); p != nil {
}); p.Valid() {
a.panel = p.Bounds()
}
}
+1 -1
View File
@@ -16,7 +16,7 @@ type DropdownMenuOptions struct {
}
// DropdownMenu is a button opening an in-window menu that build fills; it returns the button.
func DropdownMenu(c *ui.Context, label string, opts DropdownMenuOptions, build func(m *PopupMenu)) *ui.Element {
func DropdownMenu(c *ui.Context, label string, opts DropdownMenuOptions, build func(m *PopupMenu)) ui.Element {
if build == nil {
panic("mujica: DropdownMenu needs a build function")
}
+7 -7
View File
@@ -28,8 +28,8 @@ type hoverCardState struct {
}
// HoverCard previews content by a hovered or focused anchor; nil while hidden.
func HoverCard(c *ui.Context, anchor *ui.Element, opts HoverCardOptions, content func()) *ui.Element {
if anchor == nil {
func HoverCard(c *ui.Context, anchor ui.Element, opts HoverCardOptions, content func()) ui.Element {
if !anchor.Valid() {
panic("mujica: HoverCard needs an anchor")
}
if opts.OpenDelay < 0 || opts.CloseDelay < 0 {
@@ -56,7 +56,7 @@ func HoverCard(c *ui.Context, anchor *ui.Element, opts HoverCardOptions, content
st.dismissed = false
}
if st.dismissed {
return nil
return ui.Element{}
}
now := c.Now()
if hot {
@@ -67,7 +67,7 @@ func HoverCard(c *ui.Context, anchor *ui.Element, opts HoverCardOptions, content
}
if wait := openDelay - now.Sub(st.enterAt); wait > 0 {
c.After(wait)
return nil
return ui.Element{}
}
slog.Debug("mujica: hover_card open", "keyboard", keyboard)
st.open = true
@@ -75,7 +75,7 @@ func HoverCard(c *ui.Context, anchor *ui.Element, opts HoverCardOptions, content
} else {
st.enterAt = time.Time{}
if !st.open {
return nil
return ui.Element{}
}
if st.leaveAt.IsZero() {
st.leaveAt = now
@@ -85,7 +85,7 @@ func HoverCard(c *ui.Context, anchor *ui.Element, opts HoverCardOptions, content
} else {
slog.Debug("mujica: hover_card close")
st.open, st.focusIn, st.leaveAt = false, false, time.Time{}
return nil
return ui.Element{}
}
}
width := opts.Width
@@ -95,7 +95,7 @@ func HoverCard(c *ui.Context, anchor *ui.Element, opts HoverCardOptions, content
core.OverlayLayer(c, st)
// A popover keeps the card in the anchor's focus order and modal scope, even inside a Dialog.
shown := true
panel := ui.PopoverBase(c, anchor, &shown, func(panel *ui.Element) {
panel := ui.PopoverBase(c, anchor, &shown, func(panel ui.Element) {
core.OverlayStyle(c, panel, OverlayAnchored(c, panel, b))
panel.Margin(6, 0, 0, 0).Width(PopoverWide(c, width)).Padding(core.Density(c).Unit() * 4).Gap(core.Density(c).Unit() * 2).
Label(opts.Label).MaxHeight(PopoverRoom(c, b))
+1 -2
View File
@@ -48,8 +48,7 @@ func hoverCardShown(tt *ui.Tester) func() bool {
// hoverCardSettle waits past both delays so any pending open or close has happened.
func hoverCardSettle(tt *ui.Tester) {
time.Sleep(hoverCardTestDelay + 30*time.Millisecond)
tt.Frame()
tt.Advance(hoverCardTestDelay + 30*time.Millisecond)
}
func TestHoverCardPointerCanEnterCard(t *testing.T) {
+5 -5
View File
@@ -38,7 +38,7 @@ type OverlayOptions struct {
}
// Overlay shows content in a layer above the window while *open is true; Escape closes it.
func Overlay(c *ui.Context, open *bool, opts OverlayOptions, content func()) *ui.Element {
func Overlay(c *ui.Context, open *bool, opts OverlayOptions, content func()) ui.Element {
if opts.Dim && !opts.Modal {
panic("mujica: Overlay Dim requires Modal")
}
@@ -48,7 +48,7 @@ func Overlay(c *ui.Context, open *bool, opts OverlayOptions, content func()) *ui
site, focus := core.OverlaySite(c)
if !*open {
focus.Closed(c, site)
return nil
return ui.Element{}
}
k := core.Tokens(c)
inset := opts.Inset
@@ -60,7 +60,7 @@ func Overlay(c *ui.Context, open *bool, opts OverlayOptions, content func()) *ui
core.OverlayLayer(c, open)
var backClicked bool
shown := true
panel := ui.DialogBase(c, &shown, func(back, panel *ui.Element) {
panel := ui.DialogBase(c, &shown, func(back, panel ui.Element) {
backClicked = back.Clicked()
p := core.OverlayEnter(c, panel)
back.Padding(inset).Justify(overlayAlign(fy)).AlignItems(overlayAlign(fx))
@@ -79,7 +79,7 @@ func Overlay(c *ui.Context, open *bool, opts OverlayOptions, content func()) *ui
}
return panel
}
var panel *ui.Element
var panel ui.Element
ui.Overlay(c, func() {
w, h := c.Size()
panel = ui.Column(c).Absolute().Role(ui.RoleDialog).Label(opts.Label).Padding(core.Density(c).Unit() * 4).
@@ -143,7 +143,7 @@ func OverlayScrim(k theme.Tokens) ui.Color {
}
// OverlayAnchored is overlayEnter, held at 0 until the anchor b has been laid out.
func OverlayAnchored(c *ui.Context, panel *ui.Element, b ui.Rect) float32 {
func OverlayAnchored(c *ui.Context, panel ui.Element, b ui.Rect) float32 {
if b.W == 0 && b.H == 0 {
c.AnimationFrame()
return 0
+1 -1
View File
@@ -58,7 +58,7 @@ func TestOverlayPanelFitsWindow(t *testing.T) {
if p := Overlay(c, &open, OverlayOptions{Modal: modal}, func() {
ui.Text(c, strings.Repeat(overlayLongText, 6))
input.Button(c, "Done", input.ButtonOptions{})
}); p != nil {
}); p.Valid() {
panel = p.Bounds()
}
}, 600, 400)
+1 -2
View File
@@ -13,8 +13,7 @@ func overlayUntil(tt *ui.Tester, cond func() bool) bool {
if time.Now().After(end) {
return false
}
time.Sleep(2 * time.Millisecond)
tt.Frame()
tt.Advance(2 * time.Millisecond)
}
return true
}
+2 -2
View File
@@ -33,7 +33,7 @@ func (a *overlayApp) view(c *ui.Context) {
if input.Button(c, "Done", input.ButtonOptions{}).Clicked() {
a.open = false
}
}); p != nil {
}); p.Valid() {
a.panel = p.Bounds()
}
}
@@ -178,7 +178,7 @@ func overlayCases() []overlayCase {
tt.Move(r.X+4, r.Y+4)
}
leave := func(tt *ui.Tester) { tt.Move(590, 390) }
anchor := func(c *ui.Context, open *bool) *ui.Element {
anchor := func(c *ui.Context, open *bool) ui.Element {
b := input.Button(c, "Anchor", input.ButtonOptions{})
if b.Clicked() {
*open = true
+5 -5
View File
@@ -28,8 +28,8 @@ type PopconfirmResult struct {
}
// Popconfirm asks for a light confirmation by anchor; use AlertDialog for high-risk actions.
func Popconfirm(c *ui.Context, anchor *ui.Element, open *bool, opts PopconfirmOptions) PopconfirmResult {
if anchor == nil {
func Popconfirm(c *ui.Context, anchor ui.Element, open *bool, opts PopconfirmOptions) PopconfirmResult {
if !anchor.Valid() {
panic("mujica: Popconfirm needs an anchor")
}
if opts.Title == "" {
@@ -54,7 +54,7 @@ func Popconfirm(c *ui.Context, anchor *ui.Element, open *bool, opts PopconfirmOp
}
b := anchor.Bounds()
shown := true
ui.PopoverBase(c, anchor, &shown, func(panel *ui.Element) {
ui.PopoverBase(c, anchor, &shown, func(panel ui.Element) {
core.OverlayStyle(c, panel, OverlayAnchored(c, panel, b))
panel.Margin(6, 0, 0, 0).Padding(u * 3).Gap(u * 3).MaxWidth(PopoverWide(c, 320)).MaxHeight(PopoverRoom(c, b)).Role(ui.RoleAlertDialog).
Label(opts.Title).Description(opts.Description)
@@ -64,14 +64,14 @@ func Popconfirm(c *ui.Context, anchor *ui.Element, open *bool, opts PopconfirmOp
// Danger focuses Cancel, so a second Enter never runs the destructive action.
cb := input.Button(c, cancel, input.ButtonOptions{Variant: input.Secondary})
if opts.Danger {
cb.AutoFocus()
core.AutoFocus(c, cb)
}
if cb.Clicked() {
res.Canceled = true
}
ok := input.Button(c, confirm, input.ButtonOptions{Variant: variant})
if !opts.Danger {
ok.AutoFocus()
core.AutoFocus(c, ok)
}
if ok.Clicked() {
res.Confirmed = true
+4 -4
View File
@@ -20,12 +20,12 @@ type PopoverOptions struct {
}
// Popover shows content below anchor, or above it, inside the window while *open is true.
func Popover(c *ui.Context, anchor *ui.Element, open *bool, opts PopoverOptions, content func()) *ui.Element {
if anchor == nil {
func Popover(c *ui.Context, anchor ui.Element, open *bool, opts PopoverOptions, content func()) ui.Element {
if !anchor.Valid() {
panic("mujica: Popover needs an anchor")
}
if !*open {
return nil
return ui.Element{}
}
core.OverlayLayer(c, open)
u := core.Density(c).Unit()
@@ -34,7 +34,7 @@ func Popover(c *ui.Context, anchor *ui.Element, open *bool, opts PopoverOptions,
if opts.Title != "" {
label = opts.Title
}
panel := ui.PopoverBase(c, anchor, open, func(panel *ui.Element) {
panel := ui.PopoverBase(c, anchor, open, func(panel ui.Element) {
core.OverlayStyle(c, panel, OverlayAnchored(c, panel, b))
panel.Margin(6, 0, 0, 0).Padding(u * 3).Gap(u * 2).MinWidth(PopoverWide(c, b.W)).MaxHeight(PopoverRoom(c, b)).Label(label)
if opts.Width > 0 {
+3 -3
View File
@@ -33,7 +33,7 @@ func (a *popoverApp) view(c *ui.Context) {
a.applied++
a.open = false
}
}); p != nil {
}); p.Valid() {
a.panel = p.Bounds()
}
})
@@ -90,7 +90,7 @@ func TestPopoverFitsWindow(t *testing.T) {
if p := Popover(c, b, &open, PopoverOptions{Width: 1000}, func() {
ui.Text(c, strings.Repeat("Long row of guests. ", 200))
input.Button(c, "Bottom", input.ButtonOptions{})
}); p != nil {
}); p.Valid() {
panel = p.Bounds()
}
})
@@ -110,7 +110,7 @@ func TestPopoverNeedsAnchor(t *testing.T) {
open := true
ui.NewTester(func(c *ui.Context) {
core.Use(c, core.Settings{})
Popover(c, nil, &open, PopoverOptions{}, nil)
Popover(c, ui.Element{}, &open, PopoverOptions{}, nil)
}, 200, 200)
}
+12 -12
View File
@@ -39,7 +39,7 @@ type popupMenuLevel struct {
}
type popupMenuSub struct {
item *ui.Element
item ui.Element
seq int
build func(m *PopupMenu)
}
@@ -59,7 +59,7 @@ func (m *PopupMenu) Item(label string, opts PopupMenuItemOptions) bool {
// Check adds an item toggling *checked and reports whether it changed.
func (m *PopupMenu) Check(label string, checked *bool, opts PopupMenuItemOptions) bool {
// The base toggles as it is built, so a disabled wrapper comes first.
var base *ui.Element
var base ui.Element
ui.Column(m.c).Shrink(0).Disabled(opts.Disabled).Children(func() { base = ui.CheckboxBase(m.c, checked) })
it := m.row(base, opts.Disabled, false)
m.content(it, label, *checked, false)
@@ -109,7 +109,7 @@ func (m *PopupMenu) Separator() {
}
// row styles an item element and runs the hover, focus and highlight logic.
func (m *PopupMenu) row(it *ui.Element, disabled, sub bool) *ui.Element {
func (m *PopupMenu) row(it ui.Element, disabled, sub bool) ui.Element {
c := m.c
k := core.Tokens(c)
d := core.Density(c)
@@ -123,7 +123,7 @@ func (m *PopupMenu) row(it *ui.Element, disabled, sub bool) *ui.Element {
return it
}
if m.autofocus {
it.AutoFocus()
core.AutoFocus(c, it)
m.autofocus = false
}
hovered := it.Hovered()
@@ -146,7 +146,7 @@ func (m *PopupMenu) row(it *ui.Element, disabled, sub bool) *ui.Element {
}
// content lays out the check column, label and submenu chevron of an item.
func (m *PopupMenu) content(it *ui.Element, label string, checked, sub bool) {
func (m *PopupMenu) content(it ui.Element, label string, checked, sub bool) {
c := m.c
k := core.Tokens(c)
it.Children(func() {
@@ -162,14 +162,14 @@ func (m *PopupMenu) content(it *ui.Element, label string, checked, sub bool) {
}
// PopupMenuOpen shows the menu build fills below anchor while *open is true.
func PopupMenuOpen(c *ui.Context, anchor *ui.Element, open *bool, build func(m *PopupMenu)) *ui.Element {
func PopupMenuOpen(c *ui.Context, anchor ui.Element, open *bool, build func(m *PopupMenu)) ui.Element {
if !*open {
return nil
return ui.Element{}
}
core.OverlayLayer(c, open)
b := anchor.Bounds()
root := &popupMenuRoot{}
panel := ui.PopoverBase(c, anchor, open, func(panel *ui.Element) {
panel := ui.PopoverBase(c, anchor, open, func(panel ui.Element) {
core.OverlayStyle(c, panel, OverlayAnchored(c, panel, b))
panel.Margin(4, 0, 0, 0).MinWidth(PopoverWide(c, max(b.W, 200))).MaxWidth(PopoverWide(c, 360)).MaxHeight(PopoverRoom(c, b))
popupMenuLevelBuild(c, panel, root, build, true)
@@ -188,7 +188,7 @@ func PopupMenuOpen(c *ui.Context, anchor *ui.Element, open *bool, build func(m *
}
// popupMenuLevelBuild builds one level's items in outer, then its open submenu.
func popupMenuLevelBuild(c *ui.Context, outer *ui.Element, root *popupMenuRoot, build func(m *PopupMenu), autofocus bool) {
func popupMenuLevelBuild(c *ui.Context, outer ui.Element, root *popupMenuRoot, build func(m *PopupMenu), autofocus bool) {
u := core.Density(c).Unit()
// The padding leaves room for the items' focus rings inside the clipping scroll.
items := ui.Scroll(c).Shrink(1).Padding(u + theme.FocusGap + theme.FocusWidth).FocusGroup(ui.Vertical)
@@ -204,11 +204,11 @@ func popupMenuLevelBuild(c *ui.Context, outer *ui.Element, root *popupMenuRoot,
}
// popupMenuSubmenu builds a submenu beside its item, flipped to stay in the window.
func popupMenuSubmenu(c *ui.Context, outer *ui.Element, root *popupMenuRoot, lv *popupMenuLevel, s popupMenuSub) {
func popupMenuSubmenu(c *ui.Context, outer ui.Element, root *popupMenuRoot, lv *popupMenuLevel, s popupMenuSub) {
u := core.Density(c).Unit()
ob, ib := outer.Bounds(), s.item.Bounds()
ww, wh := c.Size()
box := ui.Column(c).Absolute().Key(s.seq).MinWidth(min(180, ww-8)).MaxWidth(min(360, ww-8))
box := ui.Column(c.Key(s.seq)).Absolute().MinWidth(min(180, ww-8)).MaxWidth(min(360, ww-8))
sb := box.Bounds()
left, top := ib.X+ib.W+u-ob.X, ib.Y-u-ob.Y
if ob.X+left+sb.W > ww-4 {
@@ -232,7 +232,7 @@ func popupMenuSubmenu(c *ui.Context, outer *ui.Element, root *popupMenuRoot, lv
}
// popupMenuTrigger styles a menu trigger button with a trailing chevron.
func popupMenuTrigger(c *ui.Context, b *ui.Element, label string, icon *ui.SVG, open bool) {
func popupMenuTrigger(c *ui.Context, b ui.Element, label string, icon *ui.SVG, open bool) {
k := core.Tokens(c)
d := core.Density(c)
b.MinHeight(d.ControlHeight()).MinWidth(theme.MinHitSize).Padding(0, d.Unit()*3, 0, d.Unit()*4).
+6 -2
View File
@@ -59,7 +59,7 @@ func SplitButton(c *ui.Context, label string, opts SplitButtonOptions, build fun
ui.Box(c).Width(theme.BorderWidth).Background(k.OnAccent.Alpha(0.45)).Role(ui.RoleNone)
}
more := ui.ButtonBase(c).Role(ui.RoleMenuButton).Label(menuName).MinHeight(d.ControlHeight()).Width(theme.MinHitSize+d.Unit()).
Radius(0, theme.ControlRadius, theme.ControlRadius, 0).Tooltip(menuName)
Radius(0, theme.ControlRadius, theme.ControlRadius, 0)
splitButtonLook(c, k, more, opts.Variant, *open)
if opts.Variant == input.Secondary {
more.BorderWidth(theme.BorderWidth, theme.BorderWidth, theme.BorderWidth, 0)
@@ -69,6 +69,10 @@ func SplitButton(c *ui.Context, label string, opts SplitButtonOptions, build fun
*open = true
slog.Debug("mujica: split_button open")
}
// A tip coming and going ahead of the open menu would re-key its items.
if !*open {
more.Tooltip(menuName)
}
PopupMenuOpen(c, group, open, build)
})
if clicked {
@@ -78,7 +82,7 @@ func SplitButton(c *ui.Context, label string, opts SplitButtonOptions, build fun
}
// splitButtonLook colors one half as a Button of variant v would be.
func splitButtonLook(c *ui.Context, k theme.Tokens, b *ui.Element, v input.ButtonVariant, open bool) {
func splitButtonLook(c *ui.Context, k theme.Tokens, b ui.Element, v input.ButtonVariant, open bool) {
internal.Ring(b, k.Focus, theme.ControlRadius)
bg, fg, border := input.ButtonColors(k, v, b.Hovered() && !b.IsDisabled(), b.Pressed() || open)
b.Background(bg).TextColor(fg).Transition(core.ColorFade)
+2 -2
View File
@@ -25,8 +25,8 @@ type TooltipState struct {
const tooltipMaxWidth = 240
// Tooltip shows a non-interactive tip by anchor after hover or keyboard focus rests on it for Delay.
func Tooltip(c *ui.Context, anchor *ui.Element, text string, opts TooltipOptions) *ui.Element {
if anchor == nil {
func Tooltip(c *ui.Context, anchor ui.Element, text string, opts TooltipOptions) ui.Element {
if !anchor.Valid() {
panic("mujica: Tooltip needs an anchor")
}
if opts.Delay < 0 {
+1 -2
View File
@@ -66,8 +66,7 @@ func TestTooltipFocusAndEscape(t *testing.T) {
if tt.HasText("Share with the court") {
t.Fatal("Escape did not hide the tip")
}
time.Sleep(tooltipTestDelay + 30*time.Millisecond)
tt.Frame()
tt.Advance(tooltipTestDelay + 30*time.Millisecond)
if tt.HasText("Share with the court") {
t.Fatal("tip came back while still focused")
}