Add feedback components 106-130; inert scope, focus return, two-step deploy

This commit is contained in:
ZacharyZhang-NY committed 2026-10-11 20:58:30 -04:00
1 parent bce416182e
commit bd3877e1b1
87 files changed
+2492 -147

No files matched your search

+1 -1
View File
@@ -30,7 +30,7 @@ type editable struct {
// EditableText edits on double click or Enter; Escape reverts.
func EditableText(c *ui.Context, text string, value *string, o EditableTextOptions) EditableTextResult {
k := pixel.K(c)
w := orDef("EditableText Width", o.Width, 120)
w := pixel.Or("EditableText Width", o.Width, 120)
box := ui.Box(c)
s := ui.Local(box, editableKey{}, func() editable { return editable{} })
res := EditableTextResult{Element: box}
+1 -1
View File
@@ -19,7 +19,7 @@ type FaderResult = SliderResult
func Fader(c *ui.Context, text string, value *float64, o FaderOptions) FaderResult {
k := pixel.K(c)
lo, hi := bounds(o.Min, o.Max)
h := orDef("Fader Height", o.Height, 64)
h := pixel.Or("Fader Height", o.Height, 64)
shell, st, changed := slide(c, value, rail{lo: lo, hi: hi, vertical: true, label: text, disabled: o.Disabled})
shell.Size(k.D(16), k.D(h)).Draw(func(p *ui.Painter, r ui.Rect) {
g := k.Canvas(p, r)
-11
View File
@@ -88,17 +88,6 @@ func finish(g pixel.Canvas, st pixel.State) {
}
}
// orDef is v, or def for zero; negative panics.
func orDef(name string, v, def int) int {
if v < 0 {
panic(fmt.Sprintf("input: %s %d is negative", name, v))
}
if v == 0 {
return def
}
return v
}
// label is s, or the fallback when s is empty.
func label(s, fallback string) string {
if s != "" {
+2 -2
View File
@@ -31,7 +31,7 @@ func MultiSelect(c *ui.Context, text string, chosen *[]string, options []string,
return ui.ButtonBase(c).Role(ui.RolePopUpButton).Label(text).Description(strings.Join(*chosen, ", ")).Disabled(o.Disabled)
})
open := ui.Local(base, multiKey{}, func() bool { return false })
if o.Disabled {
if o.Disabled || pixel.Current(c).Inert {
// Closing on disable must not return focus to it.
if *open {
c.Root().Focus()
@@ -41,7 +41,7 @@ func MultiSelect(c *ui.Context, text string, chosen *[]string, options []string,
if base.Clicked() || !*open && base.Shortcut(0, ui.KeyDown) {
*open = !*open
}
shell.Size(k.D(orDef("MultiSelect Width", o.Width, 160)), k.D(pixel.LineHeight+4)).Draw(func(p *ui.Painter, r ui.Rect) {
shell.Size(k.D(pixel.Or("MultiSelect Width", o.Width, 160)), k.D(pixel.LineHeight+4)).Draw(func(p *ui.Painter, r ui.Rect) {
g := k.Canvas(p, r)
ink, dy := face(g, Normal, st, *open)
if len(*chosen) == 0 {
+32
View File
@@ -132,6 +132,38 @@ func TestBadOptionsPanic(t *testing.T) {
}
}
func TestInertClosesPopups(t *testing.T) {
for name, build := range map[string]func(c *ui.Context){
"Class": func(c *ui.Context) {
class := "A"
input.Select(c, "Class", &class, []string{"A", "B"}, input.SelectOptions{})
},
"Gear": func(c *ui.Context) {
var gear []string
input.MultiSelect(c, "Gear", &gear, []string{"B", "C"}, input.MultiSelectOptions{})
},
} {
inert := false
tt := view(400, 300, nil, func(c *ui.Context) {
s := pixel.Current(c)
s.Inert = inert
pixel.Scope(c, s, func() { build(c) })
})
click(t, tt, name)
find(t, tt, "B")
inert = true
tt.Frame()
if _, ok := tt.Find("B"); ok {
t.Fatalf("%s stayed open under Inert", name)
}
inert = false
tt.Frame()
if _, ok := tt.Find("B"); ok {
t.Fatalf("%s opened again after Inert", name)
}
}
}
func TestMultiSelectChecksAndShowsTags(t *testing.T) {
var gear []string
changes := 0
+1 -1
View File
@@ -27,7 +27,7 @@ const pinBox, pinGap = 12, 4
// PinInput types digits into boxes; Backspace goes back, paste fills.
func PinInput(c *ui.Context, text string, value *string, o PinInputOptions) PinInputResult {
k := pixel.K(c)
n := orDef("PinInput Length", o.Length, 4)
n := pixel.Or("PinInput Length", o.Length, 4)
shell, base, st := pixel.Shell(c, func() *ui.Element {
return ui.Box(c).Focusable().Role(ui.RoleTextField).Label(text).Description(*value).Disabled(o.Disabled)
})
+1 -1
View File
@@ -21,7 +21,7 @@ type RangeSliderResult = SliderResult
func RangeSlider(c *ui.Context, text string, low, high *float64, o RangeSliderOptions) RangeSliderResult {
k := pixel.K(c)
lo, hi := bounds(o.Min, o.Max)
w := orDef("RangeSlider Width", o.Width, 96)
w := pixel.Or("RangeSlider Width", o.Width, 96)
was := [2]float64{*low, *high}
e := ui.Box(c).Size(k.D(w), k.D(12)).Role(ui.RoleGroup).Label(text)
e.Draw(func(p *ui.Painter, r ui.Rect) {
+1 -1
View File
@@ -28,7 +28,7 @@ type ratingKey struct{}
// Rating sets *value by click or arrows; reclicking clears.
func Rating(c *ui.Context, text string, value *int, o RatingOptions) RatingResult {
k := pixel.K(c)
n := orDef("Rating Max", o.Max, 5)
n := pixel.Or("Rating Max", o.Max, 5)
full, empty := icon.Star, icon.StarEmpty
if o.Hearts {
full, empty = icon.Heart, icon.HeartEmpty
+12 -6
View File
@@ -22,19 +22,25 @@ type SelectResult struct {
// Select picks one option from a popup list.
func Select(c *ui.Context, text string, value *string, options []string, o SelectOptions) SelectResult {
k := pixel.K(c)
off := o.Disabled || pixel.Current(c).Inert
var sel *ui.SelectParts[string]
shell, base, st := pixel.Shell(c, func() *ui.Element {
sel = ui.SelectBase(c, value)
return sel.Trigger.Label(text).Disabled(o.Disabled)
var shell, base *ui.Element
var st pixel.State
// Re-keying on each off state forgets an open popup.
ui.Box(c).Key(off).Children(func() {
shell, base, st = pixel.Shell(c, func() *ui.Element {
sel = ui.SelectBase(c, value)
return sel.Trigger.Label(text).Disabled(o.Disabled)
})
})
shell.Size(k.D(orDef("Select Width", o.Width, widest(options)+22)), k.D(pixel.LineHeight+4)).Draw(func(p *ui.Painter, r ui.Rect) {
shell.Size(k.D(pixel.Or("Select Width", o.Width, widest(options)+22)), k.D(pixel.LineHeight+4)).Draw(func(p *ui.Painter, r ui.Rect) {
g := k.Canvas(p, r)
ink, dy := face(g, Normal, st, sel.Open() && !o.Disabled)
ink, dy := face(g, Normal, st, sel.Open() && !off)
g.Text(6, 2+dy, *value, ink, 1)
g.Sprite(g.W-10, (g.H-tinyDown.H)/2+dy, tinyDown, 0, ink, 1)
finish(g, st)
})
if !o.Disabled {
if !off {
sel.Popup(func(panel *ui.Element) {
menu(c, panel, func() {
for _, opt := range options {
+1 -1
View File
@@ -27,7 +27,7 @@ type SliderResult struct {
func Slider(c *ui.Context, text string, value *float64, o SliderOptions) SliderResult {
k := pixel.K(c)
lo, hi := bounds(o.Min, o.Max)
w := orDef("Slider Width", o.Width, 96)
w := pixel.Or("Slider Width", o.Width, 96)
shell, st, changed := slide(c, value, rail{lo: lo, hi: hi, label: text, disabled: o.Disabled})
shell.Size(k.D(w), k.D(12)).Draw(func(p *ui.Painter, r ui.Rect) {
g := k.Canvas(p, r)
+1 -1
View File
@@ -29,7 +29,7 @@ func Stepper(c *ui.Context, text string, value *int, o StepperOptions) StepperRe
panic(fmt.Sprintf("input: Stepper range %d to %d is empty", o.Min, o.Max))
}
k := pixel.K(c)
step := orDef("Stepper Step", o.Step, 1)
step := pixel.Or("Stepper Step", o.Step, 1)
old := *value
set := func(v int) { *value = max(o.Min, min(o.Max, v)) }
w := max(pixel.Measure(strconv.Itoa(o.Min)), pixel.Measure(strconv.Itoa(o.Max))) + 8
+1 -1
View File
@@ -24,7 +24,7 @@ func StepSlider(c *ui.Context, text string, value *float64, o StepSliderOptions)
if step <= 0 {
step = (hi - lo) / 10
}
w := orDef("StepSlider Width", o.Width, 96)
w := pixel.Or("StepSlider Width", o.Width, 96)
var res SliderResult
row := ui.Row(c).Gap(k.D(6)).AlignItems(ui.Center).Children(func() {
shell, st, changed := slide(c, value, rail{lo: lo, hi: hi, step: step, label: text, disabled: o.Disabled})
+2 -2
View File
@@ -33,8 +33,8 @@ type TextAreaResult = TextInputResult
// TextArea wraps lines in its own scroll, following the caret.
func TextArea(c *ui.Context, text string, value *string, o TextAreaOptions) TextAreaResult {
k := pixel.K(c)
lines := orDef("TextArea Lines", o.Lines, 4)
col := ui.Column(c).Width(k.D(orDef("TextArea Width", o.Width, 160))).Gap(k.D(2))
lines := pixel.Or("TextArea Lines", o.Lines, 4)
col := ui.Column(c).Width(k.D(pixel.Or("TextArea Width", o.Width, 160))).Gap(k.D(2))
st := ui.Local(col, areaKey{}, func() areaState { return areaState{line: -1} })
if o.Disabled {
st.scroll.Y = st.held
+1 -1
View File
@@ -32,7 +32,7 @@ func TextInput(c *ui.Context, label string, value *string, o TextInputOptions) T
func field(c *ui.Context, value *string, eo pixel.EditorOptions, width int, bad func() bool, icon *pixel.Sprite, after func()) (pixel.Edit, *ui.Element) {
k := pixel.K(c)
var ed pixel.Edit
e := ui.Row(c).Width(k.D(orDef("Width", width, 120))).Height(k.D(pixel.LineHeight+6)).Padding(0, k.D(4)).Gap(k.D(3)).AlignItems(ui.Center)
e := ui.Row(c).Width(k.D(pixel.Or("Width", width, 120))).Height(k.D(pixel.LineHeight+6)).Padding(0, k.D(4)).Gap(k.D(3)).AlignItems(ui.Center)
e.Children(func() {
if icon != nil {
ui.Box(c).Size(k.D(icon.W), k.D(icon.H)).Draw(func(p *ui.Painter, r ui.Rect) {
+1 -1
View File
@@ -31,7 +31,7 @@ type editorSlot struct{}
// TokenField shows tokens as tags; Enter or comma adds.
func TokenField(c *ui.Context, text string, tokens *[]string, o TokenFieldOptions) TokenFieldResult {
k := pixel.K(c)
e := ui.Row(c).Wrap().Width(k.D(orDef("TokenField Width", o.Width, 200))).Padding(k.D(3), k.D(4)).Gap(k.D(3)).AlignItems(ui.Center)
e := ui.Row(c).Wrap().Width(k.D(pixel.Or("TokenField Width", o.Width, 200))).Padding(k.D(3), k.D(4)).Gap(k.D(3)).AlignItems(ui.Center)
e.Role(ui.RoleGroup).Label(text).Description(strings.Join(*tokens, ", "))
draft := ui.Local(e, tokenKey{}, func() string { return "" })
res := TokenFieldResult{Element: e}