Files
MujicaUI/code/completion_menu.go
T
ZacharyZhang-NYandClaude Opus 5.5 bcce34cda1 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>
2026-10-17 01:37:54 -04:00

134 lines
3.9 KiB
Go

package code
import (
"log/slog"
"github.com/egoist/mygo/ui"
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/core"
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/data"
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/overlay"
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/theme"
)
// Completion is one candidate of a CompletionMenu.
type Completion struct {
Label string
Kind SymbolKind
Detail string
// Doc describes the chosen candidate below the list.
Doc string
}
// CompletionMenuState is the highlighted candidate and place of a CompletionMenu.
type CompletionMenuState struct {
Selected int
rows ui.ListState
pending ui.Key
}
// CompletionMenuOptions configure a CompletionMenu.
type CompletionMenuOptions struct {
Disabled bool
}
// CompletionMenuResult carries the accepted candidate.
type CompletionMenuResult struct {
Element ui.Element
Accepted *Completion
}
// completionRows is how many candidates show before the list scrolls.
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.Valid() || open == nil || s == nil {
panic("mujica: CompletionMenu needs an anchor, open and a state")
}
for _, it := range items {
if it.Kind < SymbolFunction || it.Kind > SymbolModule {
panic("mujica: Completion kind out of range")
}
}
var res CompletionMenuResult
if opts.Disabled || anchor.IsDisabled() || len(items) == 0 {
*open = false
}
if !*open {
return res
}
s.Selected = max(0, min(s.Selected, len(items)-1))
accept := func(i int) {
it := items[i]
res.Accepted = &it
*open = false
slog.Debug("mujica: completion_menu accept", "index", i)
}
move := func(d int) {
s.Selected = (s.Selected + d + len(items)) % len(items)
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 || disabled {
return false
}
switch ev.Key {
case ui.KeyDown, ui.KeyUp, ui.KeyTab, ui.KeyEnter, ui.KeyEscape:
s.pending = ev.Key
return true
}
return false
})
key := s.pending
s.pending = 0
switch key {
case ui.KeyDown:
move(1)
case ui.KeyUp:
move(-1)
case ui.KeyTab, ui.KeyEnter:
accept(s.Selected)
return res
case ui.KeyEscape:
*open = false
slog.Debug("mujica: completion_menu close")
return res
}
k := core.Tokens(c)
d := core.Density(c)
b := anchor.Bounds()
core.OverlayLayer(c, open)
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)
chosen := s.Selected
s.rows.Selected = &chosen
s.rows.Label = func(i int) string { return items[i].Label }
restore := data.DataSelectTheme(c, 0)
list := ui.List(c, &s.rows, len(items), func(i int) {
it := items[i]
ui.Row(c).MinHeight(d.ControlHeight()).Padding(0, d.Unit()*2).Gap(d.Unit() * 2).AlignItems(ui.Center).Children(func() {
CodeSelectMark(c, i == s.Selected)
CodeBadge(c, symbolBadges[it.Kind], k.AccentText)
CodeMono(c, it.Label).Grow(1).MinWidth(0)
if it.Detail != "" {
CodeCaption(c, it.Detail).MinWidth(0).Shrink(1)
}
})
}).MaxHeight(d.ControlHeight() * completionRows).Shrink(1)
restore()
if list.Changed() && chosen >= 0 {
accept(chosen)
}
if doc := items[s.Selected].Doc; doc != "" {
ui.Text(c, doc).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Padding(d.Unit()*2).
BorderWidth(theme.BorderWidth, 0, 0, 0).BorderColor(k.Border).Shrink(0)
}
})
return res
}