Files
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

150 lines
4.3 KiB
Go

package display
import (
"log/slog"
"github.com/egoist/mygo/ui"
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/core"
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/internal"
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/theme"
)
// TextOptions configure a Text. The zero value wraps every line.
type TextOptions struct {
// MaxLines cuts with an ellipsis after n lines; zero never.
MaxLines int
// Selectable lets the user select and copy the text.
Selectable bool
}
// Text shows body text wrapping at its width.
func Text(c *ui.Context, s string, opts TextOptions) ui.Element {
if opts.MaxLines < 0 {
panic("mujica: Text MaxLines must not be negative")
}
e := ui.Text(c, s).LineHeight(theme.LineHeight)
switch {
case opts.MaxLines == 1:
e.SingleLine()
case opts.MaxLines > 1:
e.MaxLines(opts.MaxLines)
}
if opts.Selectable {
e.Selectable().Focusable()
internal.Ring(e, core.Tokens(c).Focus, theme.ControlRadius)
}
return e
}
// EditableTextOptions configure the editable mode of Text.
type EditableTextOptions struct {
// Saving shows the caller's save in progress and blocks editing.
Saving bool
// Error is the caller's save failure, shown below the text.
Error string
// Label names the text for assistive technology.
Label string
}
// EditableTextView is a shown EditableText and this frame's outcome.
type EditableTextView struct {
Element ui.Element
// Canceled reports Escape discarded an edit.
Canceled bool
changed bool
}
// Changed reports a confirmed new value, already in *value.
func (v EditableTextView) Changed() bool { return v.changed }
type editableTextState struct {
editing, fresh bool
draft string
}
type editableTextKey struct{}
// EditableText is Text's editable mode with confirm and cancel.
func EditableText(c *ui.Context, value *string, opts EditableTextOptions) EditableTextView {
k := core.Tokens(c)
u := core.Density(c).Unit()
line := k.ControlBorder
if opts.Error != "" {
line = k.Danger
}
var v EditableTextView
ui.Column(c).Gap(u).AlignItems(ui.Start).Children(func() {
ui.Row(c).Gap(u * 2).AlignItems(ui.Center).Children(func() {
v = editableTextField(c, k, value, opts, line)
if opts.Saving {
core.Spinner(c, core.SpinnerOptions{Size: 12, Label: core.Msg(c, "editable_text.saving")})
}
})
if opts.Error != "" {
ui.Text(c, opts.Error).TextColor(k.Danger).FontSize(core.FontSize(c, theme.CaptionSize))
}
})
switch {
case v.changed:
slog.Debug("mujica: editabletext changed", "len", len(*value))
case v.Canceled:
slog.Debug("mujica: editabletext canceled")
}
return v
}
func editableTextField(c *ui.Context, k theme.Tokens, value *string, opts EditableTextOptions, line ui.Color) EditableTextView {
pad := core.Density(c).Unit() / 2
box := ui.Row(c).AlignItems(ui.Center).MinWidth(theme.MinHitSize * 2).MinHeight(theme.MinHitSize).
Radius(theme.ControlRadius).Disabled(opts.Saving)
v := EditableTextView{Element: box}
st := ui.Local(box, editableTextKey{}, func() editableTextState { return editableTextState{} })
if opts.Saving {
st.editing = false
}
if !st.editing {
box.Focusable().Padding(pad+1, pad*2+1).BorderWidth(0, 0, 1, 0).BorderColor(line).BorderStyle(ui.BorderDashed).
Label(opts.Label).Error(opts.Error)
internal.Ring(box, k.Focus, theme.ControlRadius)
if box.Hovered() {
box.Background(k.SurfaceHover)
}
box.Transition(core.ColorFade)
if !box.IsDisabled() && (box.DoubleClicked() || box.Shortcut(0, ui.KeyEnter)) {
st.editing, st.fresh, st.draft = true, true, *value
} else {
box.Children(func() { ui.Text(c, *value).SingleLine() })
return v
}
}
var in ui.Element
box.Children(func() {
in = ui.TextInputBase(c, &st.draft).Grow(1).MinWidth(theme.MinHitSize*3).Padding(pad, pad*2).
Radius(theme.ControlRadius).Background(k.Surface).Border(theme.BorderWidth, k.Accent).TextColor(k.Text).
Label(opts.Label).Error(opts.Error)
internal.Ring(in, k.Focus, theme.ControlRadius)
})
done := func() {
st.editing = false
box.Focus()
}
switch {
case st.fresh:
st.fresh = false
in.Focus()
case in.Submitted():
v.changed = st.draft != *value
*value = st.draft
done()
case in.Shortcut(0, ui.KeyEscape):
v.Canceled = true
done()
case !in.Focused():
v.changed = st.draft != *value
*value = st.draft
st.editing = false
}
return v
}