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:
1 parent
bc716d27be
commit
bcce34cda1
603 files changed
+1366
-1277
No files matched your search
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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).
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user