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
@@ -31,7 +31,7 @@ type BreakpointListOptions struct {
|
||||
|
||||
// BreakpointListResult fields hold breakpoint IDs.
|
||||
type BreakpointListResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
Toggled, Edited, Opened string
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ func BreakpointList(c *ui.Context, bps *[]Breakpoint, opts BreakpointListOptions
|
||||
layout.ScrollArea(c, nil, layout.ScrollAreaOptions{}, func() {
|
||||
for i := range *bps {
|
||||
b := &(*bps)[i]
|
||||
ui.Column(c).Key(b.ID).Padding(u, u*2).Gap(u).BorderWidth(0, 0, theme.BorderWidth, 0).BorderColor(k.Border).Children(func() {
|
||||
ui.Column(c.Key(b.ID)).Padding(u, u*2).Gap(u).BorderWidth(0, 0, theme.BorderWidth, 0).BorderColor(k.Border).Children(func() {
|
||||
ui.Row(c).MinHeight(d.ControlHeight()).Gap(u * 2).AlignItems(ui.Center).Children(func() {
|
||||
on := b.Enabled
|
||||
input.Checkbox(c, &on, "", input.CheckboxOptions{Disabled: opts.Disabled}).Label(fmt.Sprintf(core.Msg(c, "breakpoint_list.enable"), path.Base(b.File), b.Line))
|
||||
@@ -86,7 +86,7 @@ func BreakpointList(c *ui.Context, bps *[]Breakpoint, opts BreakpointListOptions
|
||||
switch {
|
||||
case edit.id == b.ID:
|
||||
in := input.TextInput(c, &edit.draft, input.TextInputOptions{Placeholder: core.Msg(c, "breakpoint_list.placeholder"), Label: core.Msg(c, "breakpoint_list.condition")})
|
||||
in.Input.AutoFocus()
|
||||
core.AutoFocus(c, in.Input)
|
||||
if in.Input.Shortcut(0, ui.KeyEscape) {
|
||||
edit.id = ""
|
||||
} else if in.Submitted() && !disabled {
|
||||
@@ -104,7 +104,7 @@ func BreakpointList(c *ui.Context, bps *[]Breakpoint, opts BreakpointListOptions
|
||||
}
|
||||
|
||||
// breakpointLocation is a button opening the location.
|
||||
func breakpointLocation(c *ui.Context, b Breakpoint) *ui.Element {
|
||||
func breakpointLocation(c *ui.Context, b Breakpoint) ui.Element {
|
||||
k := core.Tokens(c)
|
||||
d := core.Density(c)
|
||||
e := ui.ButtonBase(c).Grow(1).MinWidth(0).MinHeight(theme.MinHitSize).Justify(ui.Start).Gap(d.Unit()*2).Padding(0, d.Unit()).
|
||||
|
||||
+5
-5
@@ -20,12 +20,12 @@ func codeFont(c *ui.Context) ui.Font {
|
||||
}
|
||||
|
||||
// CodeMono is a single-line monospace text.
|
||||
func CodeMono(c *ui.Context, s string) *ui.Element {
|
||||
func CodeMono(c *ui.Context, s string) ui.Element {
|
||||
return ui.Text(c, s).Font(theme.MonoFont).FontSize(core.FontSize(c, CodeSize)).SingleLine()
|
||||
}
|
||||
|
||||
// CodeCaption is muted caption text.
|
||||
func CodeCaption(c *ui.Context, s string) *ui.Element {
|
||||
func CodeCaption(c *ui.Context, s string) ui.Element {
|
||||
return ui.Text(c, s).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(core.Tokens(c).TextMuted).SingleLine()
|
||||
}
|
||||
|
||||
@@ -116,13 +116,13 @@ func CodeMatchSpans(c *ui.Context, text, query string) []ui.Span {
|
||||
}
|
||||
|
||||
// CodePanel is a framed surface holding a code tool.
|
||||
func CodePanel(c *ui.Context) *ui.Element {
|
||||
func CodePanel(c *ui.Context) ui.Element {
|
||||
k := core.Tokens(c)
|
||||
return ui.Column(c).MinHeight(0).Background(k.Surface).Border(theme.BorderWidth, k.Border).Radius(theme.ControlRadius).Clip()
|
||||
}
|
||||
|
||||
// CodeHeader is a panel's top strip of tools.
|
||||
func CodeHeader(c *ui.Context, fn func()) *ui.Element {
|
||||
func CodeHeader(c *ui.Context, fn func()) ui.Element {
|
||||
k := core.Tokens(c)
|
||||
u := core.Density(c).Unit()
|
||||
return ui.Row(c).Shrink(0).MinHeight(theme.MinHitSize+2*u).Padding(u, u*2).Gap(u*2).AlignItems(ui.Center).
|
||||
@@ -140,7 +140,7 @@ func CodeSelectMark(c *ui.Context, on bool) {
|
||||
}
|
||||
|
||||
// CodeRows builds a growing virtual list with marks.
|
||||
func CodeRows(c *ui.Context, s *ui.ListState, n int, row func(i int)) *ui.Element {
|
||||
func CodeRows(c *ui.Context, s *ui.ListState, n int, row func(i int)) ui.Element {
|
||||
d := core.Density(c)
|
||||
restore := data.DataSelectTheme(c, 0)
|
||||
defer restore()
|
||||
|
||||
+1
-1
@@ -140,7 +140,7 @@ func CodeViewer(c *ui.Context, lines []string, s *CodeViewerState, opts CodeView
|
||||
}
|
||||
|
||||
// codeViewerKeys copies the selection and folds with Left and Right.
|
||||
func codeViewerKeys(c *ui.Context, list *ui.Element, lines []string, s *CodeViewerState) {
|
||||
func codeViewerKeys(c *ui.Context, list ui.Element, lines []string, s *CodeViewerState) {
|
||||
if list.IsDisabled() {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ type CommandHistoryOptions struct {
|
||||
|
||||
// CommandHistoryResult carries the entry to replay.
|
||||
type CommandHistoryResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
// Replayed is the entry to run again, or nil.
|
||||
Replayed *HistoryEntry
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ type CompletionMenuOptions struct {
|
||||
|
||||
// CompletionMenuResult carries the accepted candidate.
|
||||
type CompletionMenuResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
Accepted *Completion
|
||||
}
|
||||
|
||||
@@ -42,8 +42,8 @@ type CompletionMenuResult struct {
|
||||
const completionRows = 8
|
||||
|
||||
// CompletionMenu shows candidates under the editor anchor.
|
||||
func CompletionMenu(c *ui.Context, anchor *ui.Element, open *bool, items []Completion, s *CompletionMenuState, opts CompletionMenuOptions) CompletionMenuResult {
|
||||
if anchor == nil || open == nil || s == nil {
|
||||
func CompletionMenu(c *ui.Context, anchor ui.Element, open *bool, items []Completion, s *CompletionMenuState, opts CompletionMenuOptions) CompletionMenuResult {
|
||||
if !anchor.Valid() || open == nil || s == nil {
|
||||
panic("mujica: CompletionMenu needs an anchor, open and a state")
|
||||
}
|
||||
for _, it := range items {
|
||||
@@ -70,8 +70,9 @@ func CompletionMenu(c *ui.Context, anchor *ui.Element, open *bool, items []Compl
|
||||
s.rows.ScrollIntoView(s.Selected)
|
||||
}
|
||||
// Take the menu keys before the editor does.
|
||||
disabled := anchor.IsDisabled()
|
||||
anchor.HandleInput(func(ev ui.InputEvent) bool {
|
||||
if ev.Kind != ui.InputKeyDown || ev.Mods != 0 || !*open || anchor.IsDisabled() {
|
||||
if ev.Kind != ui.InputKeyDown || ev.Mods != 0 || !*open || disabled {
|
||||
return false
|
||||
}
|
||||
switch ev.Key {
|
||||
@@ -100,7 +101,7 @@ func CompletionMenu(c *ui.Context, anchor *ui.Element, open *bool, items []Compl
|
||||
d := core.Density(c)
|
||||
b := anchor.Bounds()
|
||||
core.OverlayLayer(c, open)
|
||||
res.Element = ui.PopoverBase(c, anchor, open, func(panel *ui.Element) {
|
||||
res.Element = ui.PopoverBase(c, anchor, open, func(panel ui.Element) {
|
||||
core.OverlayStyle(c, panel, overlay.OverlayAnchored(c, panel, b))
|
||||
panel.Margin(4, 0, 0, 0).Padding(d.Unit()).Width(overlay.PopoverWide(c, 360)).MaxHeight(overlay.PopoverRoom(c, b)).
|
||||
Label(core.Msg(c, "completion_menu.label")).Role(ui.RoleList)
|
||||
|
||||
@@ -42,7 +42,7 @@ type DebugToolbarOptions struct {
|
||||
|
||||
// DebugToolbarResult carries the requested action.
|
||||
type DebugToolbarResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
Action DebugAction
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -50,7 +50,7 @@ type FindWidgetOptions struct {
|
||||
|
||||
// FindWidgetResult is a FindWidget and what happened in this frame.
|
||||
type FindWidgetResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
// Moved reports navigation; Replaced counts replacements.
|
||||
Moved bool
|
||||
Replaced int
|
||||
|
||||
+1
-1
@@ -109,7 +109,7 @@ func Flamegraph(c *ui.Context, root *FlameNode, s *FlamegraphState, opts Flamegr
|
||||
}
|
||||
|
||||
// flameInput zooms, moves focus and tracks hover.
|
||||
func flameInput(c *ui.Context, g *ui.Element, root *FlameNode, s *FlamegraphState, hover *[]int) bool {
|
||||
func flameInput(c *ui.Context, g ui.Element, root *FlameNode, s *FlamegraphState, hover *[]int) bool {
|
||||
b := g.Bounds()
|
||||
x, y, over := g.PointerPosition()
|
||||
*hover = nil
|
||||
|
||||
+1
-1
@@ -128,7 +128,7 @@ func hexColumn(c *ui.Context, s *HexViewerState, data []byte, start, end int, cw
|
||||
}
|
||||
|
||||
// hexKeys moves, extends and copies the selection.
|
||||
func hexKeys(c *ui.Context, frame *ui.Element, data []byte, s *HexViewerState, move func(int, bool)) {
|
||||
func hexKeys(c *ui.Context, frame ui.Element, data []byte, s *HexViewerState, move func(int, bool)) {
|
||||
copyMods := ui.Cmd
|
||||
if runtime.GOOS != "darwin" {
|
||||
copyMods = ui.Ctrl
|
||||
|
||||
@@ -34,7 +34,7 @@ type OutputPanelOptions struct {
|
||||
|
||||
// OutputPanelResult is an OutputPanel and what happened in this frame.
|
||||
type OutputPanelResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
// Cleared names a channel to clear.
|
||||
Changed bool
|
||||
Cleared string
|
||||
|
||||
@@ -58,7 +58,7 @@ type ProblemsPanelOptions struct {
|
||||
|
||||
// ProblemsPanelResult carries the problem opened.
|
||||
type ProblemsPanelResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
Opened *Problem
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ type ProcessListOptions struct {
|
||||
|
||||
// ProcessListResult carries a confirmed end request.
|
||||
type ProcessListResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
// End is the confirmed process to end, or nil.
|
||||
End *Process
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ type SearchOpen struct {
|
||||
|
||||
// SearchPanelResult is a SearchPanel and what happened in this frame.
|
||||
type SearchPanelResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
// Changed asks the caller to search again.
|
||||
Changed bool
|
||||
Opened *SearchOpen
|
||||
|
||||
@@ -63,7 +63,7 @@ type SymbolOutlineOptions struct {
|
||||
|
||||
// SymbolOutlineResult carries the symbol to jump to.
|
||||
type SymbolOutlineResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
Jumped *Symbol
|
||||
}
|
||||
|
||||
|
||||
+21
-8
@@ -49,8 +49,14 @@ func terminalMetrics(f ui.Font) (cw, ch, base float32) {
|
||||
return cw, ch, (ch-m.Ascent-m.Descent)/2 + m.Ascent
|
||||
}
|
||||
|
||||
// terminalPointer is the selection drag and focus target kept across frames.
|
||||
type terminalPointer struct {
|
||||
handle ui.Handle
|
||||
down bool
|
||||
}
|
||||
|
||||
// Terminal draws a TerminalState and sends input to the PTY.
|
||||
func Terminal(c *ui.Context, s *TerminalState, opts TerminalOptions) *ui.Element {
|
||||
func Terminal(c *ui.Context, s *TerminalState, opts TerminalOptions) ui.Element {
|
||||
if s == nil {
|
||||
panic("mujica: Terminal needs a TerminalState")
|
||||
}
|
||||
@@ -94,13 +100,17 @@ func Terminal(c *ui.Context, s *TerminalState, opts TerminalOptions) *ui.Element
|
||||
return TerminalPos{Line: len(s.back) - s.offset + row, Col: col}
|
||||
}
|
||||
anchor := ui.Local(e, "terminal.anchor", func() TerminalPos { return TerminalPos{} })
|
||||
ptr := ui.Local(e, "terminal.pointer", func() terminalPointer { return terminalPointer{} })
|
||||
e.Bind(&ptr.handle)
|
||||
disabled := e.IsDisabled()
|
||||
svc := c.Services()
|
||||
e.HandleInput(func(ev ui.InputEvent) bool {
|
||||
if e.IsDisabled() {
|
||||
if disabled {
|
||||
return false
|
||||
}
|
||||
switch ev.Kind {
|
||||
case ui.InputKeyDown:
|
||||
return terminalKey(c, s, ev, send)
|
||||
return terminalKey(svc, s, ev, send)
|
||||
case ui.InputText:
|
||||
send(ev.Text)
|
||||
return true
|
||||
@@ -117,15 +127,18 @@ func Terminal(c *ui.Context, s *TerminalState, opts TerminalOptions) *ui.Element
|
||||
if ev.Button != 0 {
|
||||
return false
|
||||
}
|
||||
e.Focus()
|
||||
ptr.down = true
|
||||
ptr.handle.Focus()
|
||||
*anchor = cell(ev.X, ev.Y)
|
||||
s.selOn = false
|
||||
return true
|
||||
case ui.InputPointerMove:
|
||||
if e.Pressed() {
|
||||
if ptr.down {
|
||||
s.Select(*anchor, cell(ev.X, ev.Y))
|
||||
}
|
||||
return true
|
||||
case ui.InputPointerUp:
|
||||
ptr.down = false
|
||||
}
|
||||
return false
|
||||
})
|
||||
@@ -144,7 +157,7 @@ func Terminal(c *ui.Context, s *TerminalState, opts TerminalOptions) *ui.Element
|
||||
}
|
||||
|
||||
// terminalKey encodes a key, handling copy, paste and scrollback.
|
||||
func terminalKey(c *ui.Context, s *TerminalState, ev ui.InputEvent, send func(string)) bool {
|
||||
func terminalKey(svc ui.Services, s *TerminalState, ev ui.InputEvent, send func(string)) bool {
|
||||
clip := ui.Super
|
||||
if runtime.GOOS != "darwin" {
|
||||
clip = ui.Ctrl | ui.Shift
|
||||
@@ -152,12 +165,12 @@ func terminalKey(c *ui.Context, s *TerminalState, ev ui.InputEvent, send func(st
|
||||
switch {
|
||||
case ev.Mods == clip && ev.Key == ui.KeyC:
|
||||
if t := s.Selection(); t != "" {
|
||||
c.WriteClipboard(t)
|
||||
svc.WriteClipboard(t)
|
||||
slog.Debug("mujica: terminal copy", "runes", len([]rune(t)))
|
||||
}
|
||||
return true
|
||||
case ev.Mods == clip && ev.Key == ui.KeyV:
|
||||
if t := c.ReadClipboard(); t != "" {
|
||||
if t := svc.ReadClipboard(); t != "" {
|
||||
send(t)
|
||||
}
|
||||
return true
|
||||
|
||||
@@ -30,7 +30,7 @@ type TerminalTabsOptions struct {
|
||||
|
||||
// TerminalTabsResult is TerminalTabs and what happened in this frame.
|
||||
type TerminalTabsResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
// Renamed and Closed hold a tab ID.
|
||||
Changed bool
|
||||
Renamed string
|
||||
@@ -126,7 +126,7 @@ func TerminalTabs(c *ui.Context, tabs *[]TerminalTab, active *string, opts Termi
|
||||
}
|
||||
|
||||
// terminalTabsTab builds one tab with title and close.
|
||||
func terminalTabsTab(c *ui.Context, tab *ui.Element, t TerminalTab, on bool, st *terminalTabsState, rename func(string), askClose func(TerminalTab), disabled bool) {
|
||||
func terminalTabsTab(c *ui.Context, tab ui.Element, t TerminalTab, on bool, st *terminalTabsState, rename func(string), askClose func(TerminalTab), disabled bool) {
|
||||
k := core.Tokens(c)
|
||||
d := core.Density(c)
|
||||
u := d.Unit()
|
||||
@@ -162,7 +162,7 @@ func terminalTabsTab(c *ui.Context, tab *ui.Element, t TerminalTab, on bool, st
|
||||
if st.editing == t.ID {
|
||||
in := input.TextInput(c, &st.draft, input.TextInputOptions{Label: core.Msg(c, "terminal_tabs.rename")})
|
||||
in.Element.Width(u * 36)
|
||||
in.Input.AutoFocus()
|
||||
core.AutoFocus(c, in.Input)
|
||||
switch {
|
||||
case in.Input.Shortcut(0, ui.KeyEscape):
|
||||
st.editing = ""
|
||||
|
||||
@@ -43,7 +43,7 @@ type VariableEdit struct{ Path, Value string }
|
||||
|
||||
// VariablesPanelResult carries a committed edit.
|
||||
type VariablesPanelResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
Edited *VariableEdit
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ func VariablesPanel(c *ui.Context, vars []Variable, s *VariablesPanelState, opts
|
||||
if s.editing == p {
|
||||
in := input.TextInput(c, &s.draft, input.TextInputOptions{Label: core.Msg(c, "variables_panel.value")})
|
||||
in.Element.Grow(1).MinWidth(0)
|
||||
in.Input.AutoFocus()
|
||||
core.AutoFocus(c, in.Input)
|
||||
switch {
|
||||
case in.Input.Shortcut(0, ui.KeyEscape):
|
||||
s.editing = ""
|
||||
|
||||
Reference in new issue
Block a user