From 57c1d5a7521f51da570e16b20ceaadb426291484 Mon Sep 17 00:00:00 2001 From: "Randall C. O'Reilly" Date: Wed, 30 Jul 2025 00:27:30 -0700 Subject: [PATCH 1/7] fieldvalue: FieldValuer interface for CoreFieldValue method for custom widgets for fields. --- core/form.go | 2 +- core/form_test.go | 13 +++++++++++++ core/valuer.go | 28 ++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/core/form.go b/core/form.go index e5f8875709..06e3a39982 100644 --- a/core/form.go +++ b/core/form.go @@ -240,7 +240,7 @@ func (fm *Form) Init() { }) tree.AddNew(p, valnm, func() Value { - return NewValue(reflectx.UnderlyingPointer(f.value).Interface(), f.field.Tag) + return NewFieldValue(f.field.Name, reflectx.UnderlyingPointer(f.parent).Interface(), reflectx.UnderlyingPointer(f.value).Interface(), f.field.Tag) }, func(w Value) { valueWidget = w wb := w.AsWidget() diff --git a/core/form_test.go b/core/form_test.go index 74b5f0a985..8ef01f1502 100644 --- a/core/form_test.go +++ b/core/form_test.go @@ -28,6 +28,13 @@ type morePerson struct { LikesPython bool } +func (mp *morePerson) CoreFieldValue(field string) Value { + if field == "Job" { + return NewChooser().SetStrings("plumber", "dentist", "other") + } + return nil +} + func TestForm(t *testing.T) { b := NewBody() NewForm(b).SetStruct(&person{Name: "Go", Age: 35}) @@ -46,6 +53,12 @@ func TestFormReadOnly(t *testing.T) { b.AssertRender(t, "form/read-only") } +func TestFormFieldValue(t *testing.T) { + b := NewBody() + NewForm(b).SetStruct(&morePerson{Name: "Go", Age: 35}) + b.AssertRender(t, "form/field-value") +} + func TestFormChange(t *testing.T) { b := NewBody() p := person{Name: "Go", Age: 35} diff --git a/core/valuer.go b/core/valuer.go index c6901a1395..afc6e4a297 100644 --- a/core/valuer.go +++ b/core/valuer.go @@ -155,6 +155,34 @@ func toValue(value any, tags reflect.StructTag) Value { return NewTextField() // final fallback } +// FieldValuer is an interface that struct types can implement to specify the +// [Value] that should be used to represent specific fields in the GUI, +// via the CoreFieldValue method. For Form and Table widgets. +type FieldValuer interface { + + // CoreFieldValue returns the [Value] that should be used to represent + // the field of given name in the GUI. If it returns nil, then the default is used + // based on display tags, type, etc. This function must NOT call [Bind]. + CoreFieldValue(field string) Value +} + +// NewFieldValue converts the given value into an appropriate [Value] +// whose associated value is bound to the given value. The given value must +// be a pointer. It uses the given optional struct tags for additional context +// and to determine styling properties via [styleFromTags]. It also adds the +// resulting [Value] to the given optional parent if it specified. The specifics +// on how it determines what type of [Value] to make are further +// documented on [toValue]. +func NewFieldValue(field string, stru any, value any, tags reflect.StructTag, parent ...tree.Node) Value { + if fv, ok := stru.(FieldValuer); ok { + v := fv.CoreFieldValue(field) + if v != nil { + return v + } + } + return NewValue(value, tags, parent...) +} + func init() { AddValueType[icons.Icon, IconButton]() AddValueType[time.Time, TimeInput]() From 0717ba52762a3a09524312dfcbd5b3d1d787d8d6 Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Thu, 31 Jul 2025 15:44:26 -0700 Subject: [PATCH 2/7] svg: use b.String() instead of string(b.Bytes()) --- svg/io.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/svg/io.go b/svg/io.go index a727ffe211..54e4a28234 100644 --- a/svg/io.go +++ b/svg/io.go @@ -874,7 +874,7 @@ func (sv *SVG) UnmarshalXML(decoder *xml.Decoder, se xml.StartElement) error { func (sv *SVG) XMLString() string { var b bytes.Buffer sv.WriteXML(&b, false) - return string(b.Bytes()) + return b.String() } // SaveXML saves the svg to a XML-encoded file, using WriteXML From 411d62a00865ce15c2b2f9d8744264e41bd25247 Mon Sep 17 00:00:00 2001 From: "Randall C. O'Reilly" Date: Sat, 2 Aug 2025 01:06:40 -0700 Subject: [PATCH 3/7] replace CoreFieldValue -> FieldWidget for method name --- core/form_test.go | 2 +- core/valuer.go | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core/form_test.go b/core/form_test.go index 8ef01f1502..3ff9b67b42 100644 --- a/core/form_test.go +++ b/core/form_test.go @@ -28,7 +28,7 @@ type morePerson struct { LikesPython bool } -func (mp *morePerson) CoreFieldValue(field string) Value { +func (mp *morePerson) FieldWidget(field string) Value { if field == "Job" { return NewChooser().SetStrings("plumber", "dentist", "other") } diff --git a/core/valuer.go b/core/valuer.go index afc6e4a297..5fdb1da061 100644 --- a/core/valuer.go +++ b/core/valuer.go @@ -157,13 +157,13 @@ func toValue(value any, tags reflect.StructTag) Value { // FieldValuer is an interface that struct types can implement to specify the // [Value] that should be used to represent specific fields in the GUI, -// via the CoreFieldValue method. For Form and Table widgets. +// via the FieldWidget method. For Form and Table widgets. type FieldValuer interface { - // CoreFieldValue returns the [Value] that should be used to represent + // FieldWidget returns the [Value] that should be used to represent // the field of given name in the GUI. If it returns nil, then the default is used // based on display tags, type, etc. This function must NOT call [Bind]. - CoreFieldValue(field string) Value + FieldWidget(field string) Value } // NewFieldValue converts the given value into an appropriate [Value] @@ -175,7 +175,7 @@ type FieldValuer interface { // documented on [toValue]. func NewFieldValue(field string, stru any, value any, tags reflect.StructTag, parent ...tree.Node) Value { if fv, ok := stru.(FieldValuer); ok { - v := fv.CoreFieldValue(field) + v := fv.FieldWidget(field) if v != nil { return v } From 5dd13da5b24a360e69831e02bed51dbe8361bd3e Mon Sep 17 00:00:00 2001 From: "Randall C. O'Reilly" Date: Tue, 5 Aug 2025 17:45:41 -0700 Subject: [PATCH 4/7] fieldvalue: working in table now -- need pointer to overall slice, so that was a bit of a PITA, but otherwise fine. --- core/table.go | 9 +++++++-- core/table_test.go | 7 +++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/core/table.go b/core/table.go index e95ff0db7c..a67d391967 100644 --- a/core/table.go +++ b/core/table.go @@ -292,7 +292,12 @@ func (tb *Table) MakeRow(p *tree.Plan, i int) { si, _, invis := svi.SliceIndex(i) itxt := strconv.Itoa(i) val := tb.sliceElementValue(si) - // stru := val.Interface() + var valPtr any + if si < tb.SliceSize { + valPtr = reflectx.UnderlyingPointer(tb.sliceUnderlying.Index(si)).Interface() + } else { + valPtr = reflectx.UnderlyingPointer(reflectx.SliceElementValue(tb.Slice)).Interface() + } if tb.ShowIndexes { tb.MakeGridIndex(p, i, si, itxt, invis) @@ -315,7 +320,7 @@ func (tb *Table) MakeRow(p *tree.Plan, i int) { readOnlyTag := tags.Get("edit") == "-" tree.AddNew(p, valnm, func() Value { - return NewValue(uvp.Interface(), tags) + return NewFieldValue(field.Name, valPtr, uvp.Interface(), tags) }, func(w Value) { wb := w.AsWidget() tb.MakeValue(w, i) diff --git a/core/table_test.go b/core/table_test.go index 9855ab0567..31fc8efcb0 100644 --- a/core/table_test.go +++ b/core/table_test.go @@ -13,6 +13,13 @@ type language struct { Rating int } +func (l *language) FieldWidget(field string) Value { + if field == "Rating" { + return NewSlider().SetMin(1).SetMax(10).SetStep(1) + } + return nil +} + func TestTable(t *testing.T) { b := NewBody() NewTable(b).SetSlice(&[]language{{"Go", 10}, {"Python", 5}}) From 0cef15deb2bfe436548d3a33e9899007f5dedafc Mon Sep 17 00:00:00 2001 From: "Randall C. O'Reilly" Date: Wed, 6 Aug 2025 21:39:21 -0700 Subject: [PATCH 5/7] fieldvalue: rename interface to FieldWidgeter and Valuer method to Widget to be more specific to core Widgets vs any kind of value --- core/colormapbutton.go | 2 +- core/valuer.go | 14 +++++++------- xyz/xyzcore/values.go | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/core/colormapbutton.go b/core/colormapbutton.go index 557012db07..aa80669fbb 100644 --- a/core/colormapbutton.go +++ b/core/colormapbutton.go @@ -19,7 +19,7 @@ import ( // which can be edited using a [ColorMapButton]. type ColorMapName string -func (cm ColorMapName) Value() Value { return NewColorMapButton() } +func (cm ColorMapName) Widget() Value { return NewColorMapButton() } // ColorMapButton displays a [colormap.Map] and can be clicked on // to display a dialog for selecting different color map options. diff --git a/core/valuer.go b/core/valuer.go index 5fdb1da061..fee2256f8f 100644 --- a/core/valuer.go +++ b/core/valuer.go @@ -20,13 +20,13 @@ import ( ) // Valuer is an interface that types can implement to specify the -// [Value] that should be used to represent them in the GUI. +// [Value] Widget that should be used to represent them in the GUI. type Valuer interface { - // Value returns the [Value] that should be used to represent + // Widget returns the [Value] Widget that should be used to represent // the value in the GUI. If it returns nil, then [ToValue] will // fall back onto the next step. This function must NOT call [Bind]. - Value() Value + Widget() Value } // ValueTypes is a map of functions that return a [Value] @@ -76,7 +76,7 @@ func NewValue(value any, tags reflect.StructTag, parent ...tree.Node) Value { // it falls back on the next step. func toValue(value any, tags reflect.StructTag) Value { if vwr, ok := value.(Valuer); ok { - if vw := vwr.Value(); vw != nil { + if vw := vwr.Widget(); vw != nil { return vw } } @@ -155,10 +155,10 @@ func toValue(value any, tags reflect.StructTag) Value { return NewTextField() // final fallback } -// FieldValuer is an interface that struct types can implement to specify the +// FieldWidgeter is an interface that struct types can implement to specify the // [Value] that should be used to represent specific fields in the GUI, // via the FieldWidget method. For Form and Table widgets. -type FieldValuer interface { +type FieldWidgeter interface { // FieldWidget returns the [Value] that should be used to represent // the field of given name in the GUI. If it returns nil, then the default is used @@ -174,7 +174,7 @@ type FieldValuer interface { // on how it determines what type of [Value] to make are further // documented on [toValue]. func NewFieldValue(field string, stru any, value any, tags reflect.StructTag, parent ...tree.Node) Value { - if fv, ok := stru.(FieldValuer); ok { + if fv, ok := stru.(FieldWidgeter); ok { v := fv.FieldWidget(field) if v != nil { return v diff --git a/xyz/xyzcore/values.go b/xyz/xyzcore/values.go index 857ff2b6f4..8453d3c933 100644 --- a/xyz/xyzcore/values.go +++ b/xyz/xyzcore/values.go @@ -70,7 +70,7 @@ func (mb *MeshButton) Init() { TODO: This doesn't work because texture is on Material which doesn't have a pointer to the Scene! (https://github.com/cogentcore/core/issues/1023) // Value restylesers TexValue as the viewer of TexName -func (mn TexName) Value() core.Value { +func (mn TexName) Widget() core.Value { vv := TexValue{} vv.Init(&vv) return &vv From 217e77db4ff8d4fdfbc7b6c8b3f4d90ff48ddbb4 Mon Sep 17 00:00:00 2001 From: Kai O'Reilly Date: Thu, 7 Aug 2025 11:28:44 -0700 Subject: [PATCH 6/7] core: improve docs for FieldWidgeter and NewFieldValue --- core/valuer.go | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/core/valuer.go b/core/valuer.go index fee2256f8f..ef983fdc90 100644 --- a/core/valuer.go +++ b/core/valuer.go @@ -156,26 +156,30 @@ func toValue(value any, tags reflect.StructTag) Value { } // FieldWidgeter is an interface that struct types can implement to specify the -// [Value] that should be used to represent specific fields in the GUI, -// via the FieldWidget method. For Form and Table widgets. +// [Value] Widget that should be used to represent specific fields in the GUI, +// via the FieldWidget method. For [Form] and [Table] widgets. type FieldWidgeter interface { // FieldWidget returns the [Value] that should be used to represent - // the field of given name in the GUI. If it returns nil, then the default is used - // based on display tags, type, etc. This function must NOT call [Bind]. + // the field with the given name in the GUI. If it returns nil, then + // the default is used based on display tags, type, etc. This function + // must NOT call [Bind]. FieldWidget(field string) Value } -// NewFieldValue converts the given value into an appropriate [Value] +// NewFieldValue converts the given value into an appropriate [Value] widget // whose associated value is bound to the given value. The given value must -// be a pointer. It uses the given optional struct tags for additional context +// be a pointer. It uses the given field name and parent struct pointer for +// context, such as if the parent struct implements [FieldWidgeter]. This +// function is used in [Form] and [Table] and generally should not be called +// by end users directly. +// +// It uses the given optional struct tags for additional context // and to determine styling properties via [styleFromTags]. It also adds the -// resulting [Value] to the given optional parent if it specified. The specifics -// on how it determines what type of [Value] to make are further -// documented on [toValue]. -func NewFieldValue(field string, stru any, value any, tags reflect.StructTag, parent ...tree.Node) Value { - if fv, ok := stru.(FieldWidgeter); ok { - v := fv.FieldWidget(field) +// resulting [Value] to the given optional parent if it specified. +func NewFieldValue(field string, str any, value any, tags reflect.StructTag, parent ...tree.Node) Value { + if fw, ok := str.(FieldWidgeter); ok { + v := fw.FieldWidget(field) if v != nil { return v } From b4575c0e4bab56707417fb3f8daa545d9b040443 Mon Sep 17 00:00:00 2001 From: "Randall C. O'Reilly" Date: Thu, 7 Aug 2025 11:39:13 -0700 Subject: [PATCH 7/7] fieldvalue: use UnderlyingPointer directly on val --- core/table.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/core/table.go b/core/table.go index a67d391967..7be49f8e87 100644 --- a/core/table.go +++ b/core/table.go @@ -292,12 +292,7 @@ func (tb *Table) MakeRow(p *tree.Plan, i int) { si, _, invis := svi.SliceIndex(i) itxt := strconv.Itoa(i) val := tb.sliceElementValue(si) - var valPtr any - if si < tb.SliceSize { - valPtr = reflectx.UnderlyingPointer(tb.sliceUnderlying.Index(si)).Interface() - } else { - valPtr = reflectx.UnderlyingPointer(reflectx.SliceElementValue(tb.Slice)).Interface() - } + valPtr := reflectx.UnderlyingPointer(val).Interface() if tb.ShowIndexes { tb.MakeGridIndex(p, i, si, itxt, invis)