From 3e011fa47fe455fb30bcf1b427fa7b2df3193017 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Mon, 5 Oct 2026 14:10:53 -0400 Subject: [PATCH] FormItem takes *ui.Context first (PRD 6.1); drop dead internal.Rule --- datetime_review2_test.go | 2 +- docs/components/041-form.md | 2 +- example_form_test.go | 2 +- examples/gallery/composite_form.go | 10 +++++----- examples/gallery/pages_form.go | 8 ++++---- form.go | 7 +++---- form_test.go | 14 +++++++------- internal/display.go | 6 ------ 8 files changed, 22 insertions(+), 29 deletions(-) diff --git a/datetime_review2_test.go b/datetime_review2_test.go index 5654eb2..3de77a7 100644 --- a/datetime_review2_test.go +++ b/datetime_review2_test.go @@ -169,7 +169,7 @@ func TestFormFocusesDatePickerInput(t *testing.T) { tt := ui.NewTester(func(c *ui.Context) { Use(c, Settings{}) Form(c, &st, FormOptions{}, func(f *FormScope) { - FormItem(f, "debut", &day, FormItemOptions[Date]{Label: "Debut date", Required: true}, func() *ui.Element { + FormItem(c, f, "debut", &day, FormItemOptions[Date]{Label: "Debut date", Required: true}, func() *ui.Element { r := DatePicker(c, &day, DatePickerOptions{Today: calendarToday}) input = r.Input return r.Input diff --git a/docs/components/041-form.md b/docs/components/041-form.md index f2efe30..5258920 100644 --- a/docs/components/041-form.md +++ b/docs/components/041-form.md @@ -14,7 +14,7 @@ name := "" view := func(c *ui.Context) { mujica.Use(c, mujica.Settings{}) res := mujica.Form(c, &state, mujica.FormOptions{}, func(f *mujica.FormScope) { - mujica.FormItem(f, "name", &name, mujica.FormItemOptions[string]{Label: "Name", Required: true}, func() *ui.Element { + mujica.FormItem(c, f, "name", &name, mujica.FormItemOptions[string]{Label: "Name", Required: true}, func() *ui.Element { return mujica.InputGroup(c, &name, mujica.InputGroupOptions{Invalid: state.Error("name") != ""}).Input }) }) diff --git a/example_form_test.go b/example_form_test.go index e580dcf..d555109 100644 --- a/example_form_test.go +++ b/example_form_test.go @@ -15,7 +15,7 @@ func ExampleForm() { view := func(c *ui.Context) { mujica.Use(c, mujica.Settings{}) res := mujica.Form(c, &state, mujica.FormOptions{}, func(f *mujica.FormScope) { - mujica.FormItem(f, "name", &name, mujica.FormItemOptions[string]{Label: "Name", Required: true}, func() *ui.Element { + mujica.FormItem(c, f, "name", &name, mujica.FormItemOptions[string]{Label: "Name", Required: true}, func() *ui.Element { return mujica.InputGroup(c, &name, mujica.InputGroupOptions{Invalid: state.Error("name") != ""}).Input }) }) diff --git a/examples/gallery/composite_form.go b/examples/gallery/composite_form.go index cb66bd3..7f71a4b 100644 --- a/examples/gallery/composite_form.go +++ b/examples/gallery/composite_form.go @@ -110,7 +110,7 @@ func (g *gallery) formEdit(c *ui.Context, log func(string)) { func (g *gallery) formFields(c *ui.Context, f *mujica.FormScope, log func(string)) { s := &g.cForm - mujica.FormItem(f, "name", &s.name, mujica.FormItemOptions[string]{Label: g.t("Stage name", "艺名"), Required: true, + mujica.FormItem(c, f, "name", &s.name, mujica.FormItemOptions[string]{Label: g.t("Stage name", "艺名"), Required: true, Description: g.t("Checked against the archive; “Oblivionis” is taken.", "将与档案核对;“Oblivionis”已被占用。"), Validate: func(v string) string { if len([]rune(strings.TrimSpace(v))) < 2 { @@ -134,7 +134,7 @@ func (g *gallery) formFields(c *ui.Context, f *mujica.FormScope, log func(string }}, func() *ui.Element { return mujica.TextInput(c, &s.name, mujica.TextInputOptions{}).Input }) - mujica.FormItem(f, "password", &s.password, mujica.FormItemOptions[string]{Label: g.t("Password", "密码"), Required: true, + mujica.FormItem(c, f, "password", &s.password, mujica.FormItemOptions[string]{Label: g.t("Password", "密码"), Required: true, Validate: func(v string) string { if len([]rune(v)) < 8 { return g.t("Use at least 8 characters.", "至少 8 个字符。") @@ -147,14 +147,14 @@ func (g *gallery) formFields(c *ui.Context, f *mujica.FormScope, log func(string } return r.Input }) - mujica.FormItem(f, "debut", &s.debut, mujica.FormItemOptions[mujica.Date]{Label: g.t("Debut date", "首演日期"), Required: true}, func() *ui.Element { + mujica.FormItem(c, f, "debut", &s.debut, mujica.FormItemOptions[mujica.Date]{Label: g.t("Debut date", "首演日期"), Required: true}, func() *ui.Element { r := mujica.DatePicker(c, &s.debut, mujica.DatePickerOptions{Clearable: true}) if r.Changed() { log("debut " + s.debut.Format("2006-01-02")) } return r.Input }) - mujica.FormItem(f, "roles", &s.tags, mujica.FormItemOptions[[]string]{Label: g.t("Roles", "担当"), Required: true, + mujica.FormItem(c, f, "roles", &s.tags, mujica.FormItemOptions[[]string]{Label: g.t("Roles", "担当"), Required: true, Description: g.t("Up to 4; press Enter after each.", "最多 4 个;每个后按 Enter。")}, func() *ui.Element { r := mujica.TagsInput(c, &s.tags, mujica.TagsInputOptions{Max: 4, Invalid: s.st.Error("roles") != "", Placeholder: g.t("keyboard, bass…", "键盘、贝斯…")}) if r.Changed() { @@ -162,7 +162,7 @@ func (g *gallery) formFields(c *ui.Context, f *mujica.FormScope, log func(string } return r.Input }) - mujica.FormItem(f, "terms", &s.terms, mujica.FormItemOptions[bool]{Label: g.t("Terms", "条款"), + mujica.FormItem(c, f, "terms", &s.terms, mujica.FormItemOptions[bool]{Label: g.t("Terms", "条款"), Validate: func(v bool) string { if !v { return g.t("Accept the terms to continue.", "请先同意条款。") diff --git a/examples/gallery/pages_form.go b/examples/gallery/pages_form.go index 9adba88..2d8aef9 100644 --- a/examples/gallery/pages_form.go +++ b/examples/gallery/pages_form.go @@ -118,7 +118,7 @@ var formPages = []page{ } formToggles(c, []string{"Labels beside fields", "Server rejects the submit"}, &d.horizontal, &d.slowServer) res := mujica.Form(c, &d.st, mujica.FormOptions{Horizontal: d.horizontal}, func(f *mujica.FormScope) { - mujica.FormItem(f, "name", &d.name, mujica.FormItemOptions[string]{Label: "Stage name", Required: true, Initial: "Sakiko", + mujica.FormItem(c, f, "name", &d.name, mujica.FormItemOptions[string]{Label: "Stage name", Required: true, Initial: "Sakiko", Description: "Checked against the archive; \"Oblivionis\" is taken.", Async: func(v string, version uint64) { log(fmt.Sprintf("async check started v%d", version)) @@ -134,7 +134,7 @@ var formPages = []page{ }}, func() *ui.Element { return mujica.InputGroup(c, &d.name, mujica.InputGroupOptions{Invalid: d.st.Error("name") != ""}).Input }) - mujica.FormItem(f, "email", &d.email, mujica.FormItemOptions[string]{Label: "Email", Required: true, + mujica.FormItem(c, f, "email", &d.email, mujica.FormItemOptions[string]{Label: "Email", Required: true, Validate: func(v string) string { if !strings.Contains(v, "@") { return "Enter an email address." @@ -147,7 +147,7 @@ var formPages = []page{ } return g.Input }) - mujica.FormItem(f, "tags", &d.tags, mujica.FormItemOptions[[]string]{Label: "Roles", Required: true, Initial: []string{"keyboard"}}, func() *ui.Element { + mujica.FormItem(c, f, "tags", &d.tags, mujica.FormItemOptions[[]string]{Label: "Roles", Required: true, Initial: []string{"keyboard"}}, func() *ui.Element { return mujica.TagsInput(c, &d.tags, mujica.TagsInputOptions{Max: 4, Invalid: d.st.Error("tags") != ""}).Input }) }) @@ -336,7 +336,7 @@ name := "" view := func(c *ui.Context) { mujica.Use(c, mujica.Settings{}) res := mujica.Form(c, &state, mujica.FormOptions{}, func(f *mujica.FormScope) { - mujica.FormItem(f, "name", &name, mujica.FormItemOptions[string]{Label: "Name", Required: true}, func() *ui.Element { + mujica.FormItem(c, f, "name", &name, mujica.FormItemOptions[string]{Label: "Name", Required: true}, func() *ui.Element { return mujica.InputGroup(c, &name, mujica.InputGroupOptions{Invalid: state.Error("name") != ""}).Input }) }) diff --git a/form.go b/form.go index acee219..4c6314c 100644 --- a/form.go +++ b/form.go @@ -99,7 +99,6 @@ type FormOptions struct { // FormScope registers a Form's fields with FormItem. type FormScope struct { - c *ui.Context st *FormState horizontal bool } @@ -140,7 +139,7 @@ func Form(c *ui.Context, st *FormState, opts FormOptions, fields func(f *FormSco parts := FormParts{} col := ui.Column(c).Gap(d.Unit() * 5) parts.Element = col - scope := &FormScope{c: c, st: st, horizontal: opts.Horizontal} + scope := &FormScope{st: st, horizontal: opts.Horizontal} col.Children(func() { // A new key after Reset drops the controls' drafts and errors. ui.Box(c).Key(st.generation).Children(func() { @@ -242,8 +241,8 @@ func formReset(st *FormState) { } // FormItem registers field name around the focusable element control returns. -func FormItem[T any](f *FormScope, name string, value *T, opts FormItemOptions[T], control func() *ui.Element) *ui.Element { - c, st := f.c, f.st +func FormItem[T any](c *ui.Context, f *FormScope, name string, value *T, opts FormItemOptions[T], control func() *ui.Element) *ui.Element { + st := f.st if name == "" || opts.Label == "" { panic("mujica: FormItem needs a name and a Label") } diff --git a/form_test.go b/form_test.go index 0e895ba..64ce79b 100644 --- a/form_test.go +++ b/form_test.go @@ -25,7 +25,7 @@ func (p *formProbe) run(c *ui.Context) { func (p *formProbe) view(c *ui.Context) { res := Form(c, &p.st, FormOptions{Horizontal: p.horizontal}, func(f *FormScope) { - FormItem(f, "name", &p.name, FormItemOptions[string]{Label: "Name", Required: true, Initial: "Sakiko"}, func() *ui.Element { + FormItem(c, f, "name", &p.name, FormItemOptions[string]{Label: "Name", Required: true, Initial: "Sakiko"}, func() *ui.Element { return InputGroup(c, &p.name, InputGroupOptions{Invalid: p.st.Error("name") != ""}).Input }) opts := FormItemOptions[string]{Label: "Email", Validate: func(v string) string { @@ -40,10 +40,10 @@ func (p *formProbe) view(c *ui.Context) { p.asyncValues = append(p.asyncValues, v) } } - FormItem(f, "email", &p.email, opts, func() *ui.Element { + FormItem(c, f, "email", &p.email, opts, func() *ui.Element { return InputGroup(c, &p.email, InputGroupOptions{Invalid: p.st.Error("email") != ""}).Input }) - FormItem(f, "tags", &p.tags, FormItemOptions[[]string]{Label: "Tags", Required: true, Initial: []string{"rose"}}, func() *ui.Element { + FormItem(c, f, "tags", &p.tags, FormItemOptions[[]string]{Label: "Tags", Required: true, Initial: []string{"rose"}}, func() *ui.Element { return TagsInput(c, &p.tags, TagsInputOptions{}).Input }) }) @@ -222,7 +222,7 @@ func TestFormResetClearsControlState(t *testing.T) { tt := ui.NewTester(func(c *ui.Context) { Use(c, Settings{}) Form(c, &st, FormOptions{}, func(f *FormScope) { - FormItem(f, "tags", &tags, FormItemOptions[[]string]{Label: "Tags"}, func() *ui.Element { + FormItem(c, f, "tags", &tags, FormItemOptions[[]string]{Label: "Tags"}, func() *ui.Element { return TagsInput(c, &tags, TagsInputOptions{Validate: func(s string) string { seen = s return "Invalid tag" @@ -289,13 +289,13 @@ func TestFormCrossFieldRuleAtSettle(t *testing.T) { tt := ui.NewTester(func(c *ui.Context) { Use(c, Settings{}) res := Form(c, &st, FormOptions{}, func(f *FormScope) { - FormItem(f, "user", &user, FormItemOptions[string]{Label: "User", Async: func(_ string, v uint64) { version = v }}, func() *ui.Element { + FormItem(c, f, "user", &user, FormItemOptions[string]{Label: "User", Async: func(_ string, v uint64) { version = v }}, func() *ui.Element { return InputGroup(c, &user, InputGroupOptions{}).Input }) - FormItem(f, "pw", &pw, FormItemOptions[string]{Label: "Password"}, func() *ui.Element { + FormItem(c, f, "pw", &pw, FormItemOptions[string]{Label: "Password"}, func() *ui.Element { return InputGroup(c, &pw, InputGroupOptions{}).Input }) - FormItem(f, "confirm", &confirm, FormItemOptions[string]{Label: "Confirm", Validate: func(v string) string { + FormItem(c, f, "confirm", &confirm, FormItemOptions[string]{Label: "Confirm", Validate: func(v string) string { if v != pw { return "Passwords differ." } diff --git a/internal/display.go b/internal/display.go index 76a35ab..3a00828 100644 --- a/internal/display.go +++ b/internal/display.go @@ -6,12 +6,6 @@ import ( "github.com/egoist/mygo/ui" ) -// Rule draws a pixel-aligned 1 DIP horizontal line at y. -func Rule(p *ui.Painter, x0, x1, y float32, c ui.Color) { - yy := ruleY(p, y) - p.Line(x0, yy, x1, yy, 1, c) -} - // ruleY moves y to where a 1 DIP line fills whole device pixels. func ruleY(p *ui.Painter, y float32) float32 { s := float64(p.Scale())