Files
MujicaUI/text.go
T
ZacharyZhang-NY 43c96a7a81 Refactor: one colorFade transition replaces internal.Fade
textInputFade becomes colorFade in theme.go and serves AvatarGroup,
Image retry, Tag and EditableText, which used internal.Fade. MyGo already
skips element transitions under ReduceMotion, so dropping the Motion
check keeps behaviour.
2026-10-05 13:50:47 -04:00

149 lines
4.1 KiB
Go

package mujica
import (
"log/slog"
"github.com/egoist/mygo/ui"
"mujicaui/internal"
"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, 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 := Tokens(c)
u := 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 {
Spinner(c, SpinnerOptions{Size: 12, Label: msg(c, "editable_text.saving")})
}
})
if opts.Error != "" {
ui.Text(c, opts.Error).TextColor(k.Danger).FontSize(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 := 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(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
}