Repository: cogentcore/core
Pull request #976 (2024-06-12 02:25:30)

Remove methods from core.Widget interface that are not overridden
Under the same logic as #975, this PR removes unnecessary methods from the `core.Widget` interface that are not overridden by higher-level types, thereby simplifying the codebase and reducing unnecessary repetition.


Patch:
diff --git a/core/completer.go b/core/completer.go
index e2097d445..3f5d0e752 100644
--- a/core/completer.go
+++ b/core/completer.go
@@ -176,7 +176,7 @@ func (c *Complete) ShowNowImpl(ctx Widget, pos image.Point, text string) bool {
 			OnKeyChord(func(e events.Event) {
 				kf := keymap.Of(e.KeyChord())
 				if e.KeyRune() == ' ' {
-					ctx.HandleEvent(e) // bypass button handler!
+					ctx.AsWidget().HandleEvent(e) // bypass button handler!
 				}
 				if kf == keymap.Enter {
 					e.SetHandled()
diff --git a/core/events.go b/core/events.go
index 757f86b82..6433584a1 100644
--- a/core/events.go
+++ b/core/events.go
@@ -210,7 +210,7 @@ func (em *Events) HandleFocusEvent(e events.Event) {
 			return !e.IsHandled()
 		})
 		if !e.IsHandled() {
-			em.Focus.HandleEvent(e)
+			em.Focus.AsWidget().HandleEvent(e)
 		}
 		if !e.IsHandled() {
 			em.Focus.AsTree().WalkUpParent(func(k tree.Node) bool {
@@ -240,7 +240,7 @@ func (em *Events) ResetOnMouseDown() {
 	// if we have sent a long hover start event, we send an end
 	// event (non-nil widget plus nil timer means we already sent)
 	if em.LongHoverWidget != nil && em.LongHoverTimer == nil {
-		em.LongHoverWidget.Send(events.LongHoverEnd)
+		em.LongHoverWidget.AsWidget().Send(events.LongHoverEnd)
 	}
 	em.LongHoverWidget = nil
 	em.LongHoverPos = image.Point{}
@@ -265,13 +265,13 @@ func (em *Events) HandlePosEvent(e events.Event) {
 			return
 		}
 		if em.Slide != nil {
-			em.Slide.HandleEvent(e)
-			em.Slide.Send(events.SlideMove, e)
+			em.Slide.AsWidget().HandleEvent(e)
+			em.Slide.AsWidget().Send(events.SlideMove, e)
 			return
 		}
 	case events.Scroll:
 		if em.Scroll != nil {
-			em.Scroll.HandleEvent(e)
+			em.Scroll.AsWidget().HandleEvent(e)
 			return
 		}
 	}
@@ -310,7 +310,7 @@ func (em *Events) HandlePosEvent(e events.Event) {
 			continue
 		}
 
-		w.HandleEvent(e) // everyone gets the primary event who is in scope, deepest first
+		w.AsWidget().HandleEvent(e) // everyone gets the primary event who is in scope, deepest first
 		switch et {
 		case events.MouseMove:
 			if move == nil && wb.Styles.Abilities.IsHoverable() {
@@ -390,19 +390,19 @@ func (em *Events) HandlePosEvent(e events.Event) {
 				}
 			}
 			em.DragHovers = em.UpdateHovers(hovs, em.DragHovers, e, events.DragEnter, events.DragLeave)
-			em.DragMove(e)                   // updates sprite position
-			em.Drag.HandleEvent(e)           // raw drag
-			em.Drag.Send(events.DragMove, e) // usually ignored
+			em.DragMove(e)                              // updates sprite position
+			em.Drag.AsWidget().HandleEvent(e)           // raw drag
+			em.Drag.AsWidget().Send(events.DragMove, e) // usually ignored
 		} else {
 			if em.DragPress != nil && em.DragStartCheck(e, DeviceSettings.DragStartTime, DeviceSettings.DragStartDistance) {
 				em.CancelRepeatClick()
 				em.CancelLongPress()
-				em.DragPress.Send(events.DragStart, e)
+				em.DragPress.AsWidget().Send(events.DragStart, e)
 			} else if em.SlidePress != nil && em.DragStartCheck(e, DeviceSettings.SlideStartTime, DeviceSettings.DragStartDistance) {
 				em.CancelRepeatClick()
 				em.CancelLongPress()
 				em.Slide = em.SlidePress
-				em.Slide.Send(events.SlideStart, e)
+				em.Slide.AsWidget().Send(events.SlideStart, e)
 			}
 		}
 		// if we already have a long press widget, we update it based on our dragging movement
@@ -412,7 +412,7 @@ func (em *Events) HandlePosEvent(e events.Event) {
 	case events.MouseUp:
 		em.CancelRepeatClick()
 		if em.Slide != nil {
-			em.Slide.Send(events.SlideStop, e)
+			em.Slide.AsWidget().Send(events.SlideStop, e)
 			em.Slide = nil
 			em.Press = nil
 		}
@@ -441,7 +441,7 @@ func (em *Events) HandlePosEvent(e events.Event) {
 						wb := w.AsWidget()
 						if !wb.StateIs(states.Disabled) && wb.AbilityIs(abilities.TripleClickable) {
 							sentMulti = true
-							w.HandleEvent(tce)
+							w.AsWidget().HandleEvent(tce)
 							break
 						}
 					}
@@ -453,7 +453,7 @@ func (em *Events) HandlePosEvent(e events.Event) {
 						if !wb.StateIs(states.Disabled) && wb.AbilityIs(abilities.DoubleClickable) {
 							em.LastDoubleClickWidget = up // not actually who gets the event
 							sentMulti = true
-							w.HandleEvent(dce)
+							w.AsWidget().HandleEvent(dce)
 							break
 						}
 					}
@@ -461,40 +461,40 @@ func (em *Events) HandlePosEvent(e events.Event) {
 				if !sentMulti {
 					em.LastDoubleClickWidget = nil
 					em.LastClickWidget = up
-					up.Send(events.Click, e)
+					up.AsWidget().Send(events.Click, e)
 				}
 			case events.Right: // note: automatically gets Control+Left
-				up.Send(events.ContextMenu, e)
+				up.AsWidget().Send(events.ContextMenu, e)
 			}
 		}
 		// if our original pressed widget is different from the one we are
 		// going up on, then it has not gotten a mouse up event yet, so
 		// we need to send it one
 		if em.Press != up && em.Press != nil {
-			em.Press.HandleEvent(e)
+			em.Press.AsWidget().HandleEvent(e)
 		}
 		em.Press = nil
 
 		// if we have sent a long press start event, we send an end
 		// event (non-nil widget plus nil timer means we already sent)
 		if em.LongPressWidget != nil && em.LongPressTimer == nil {
-			em.LongPressWidget.Send(events.LongPressEnd, e)
+			em.LongPressWidget.AsWidget().Send(events.LongPressEnd, e)
 		}
 		em.CancelLongPress()
 		// a mouse up event acts also acts as a mouse leave
 		// event on mobile, as that is needed to clear any
 		// hovered state
 		if up != nil && TheApp.Platform().IsMobile() {
-			up.Send(events.MouseLeave, e)
+			up.AsWidget().Send(events.MouseLeave, e)
 		}
 	case events.Scroll:
 		switch {
 		case em.Slide != nil:
-			em.Slide.HandleEvent(e)
+			em.Slide.AsWidget().HandleEvent(e)
 		case em.Drag != nil:
-			em.Drag.HandleEvent(e)
+			em.Drag.AsWidget().HandleEvent(e)
 		case em.Press != nil:
-			em.Press.HandleEvent(e)
+			em.Press.AsWidget().HandleEvent(e)
 		default:
 			em.Scene.HandleEvent(e)
 		}
@@ -526,7 +526,7 @@ func (em *Events) UpdateHovers(hov, prev []Widget, e events.Event, enter, leave
 			}
 		}
 		if !stillIn && prv.AsTree().This != nil {
-			prv.Send(leave, e)
+			prv.AsWidget().Send(leave, e)
 		}
 	}
 
@@ -539,7 +539,7 @@ func (em *Events) UpdateHovers(hov, prev []Widget, e events.Event, enter, leave
 			}
 		}
 		if !wasIn {
-			cur.Send(enter, e)
+			cur.AsWidget().Send(enter, e)
 		}
 	}
 	// todo: detect change in top one, use to update cursor
@@ -551,7 +551,7 @@ func (em *Events) TopLongHover() Widget {
 	var deep Widget
 	for i := len(em.Hovers) - 1; i >= 0; i-- {
 		h := em.Hovers[i]
-		if h.AbilityIs(abilities.LongHoverable) {
+		if h.AsWidget().AbilityIs(abilities.LongHoverable) {
 			deep = h
 			break
 		}
@@ -604,7 +604,7 @@ func (em *Events) HandleLong(e events.Event, deep Widget, w *Widget, pos *image.
 		// if we have already finished the timer, then we have already
 		// sent the start event, so we have to send the end one
 		if *t == nil {
-			(*w).Send(etyp, e)
+			(*w).AsWidget().Send(etyp, e)
 		}
 		clearLong()
 		// fmt.Println("cleared")
@@ -625,7 +625,7 @@ func (em *Events) HandleLong(e events.Event, deep Widget, w *Widget, pos *image.
 		// up not getting another mouse move event, so we will be on the
 		// element with no tooltip, which is a bug. Not returning here is
 		// the solution to https://github.com/cogentcore/core/issues/553
-		(*w).Send(etyp, e)
+		(*w).AsWidget().Send(etyp, e)
 		clearLong()
 		// fmt.Println("fallthrough after clear")
 	}
@@ -641,7 +641,7 @@ func (em *Events) HandleLong(e events.Event, deep Widget, w *Widget, pos *image.
 	// we now know we don't have the timer and thus sent the start
 	// event already, so we need to send a end event
 	if *w != nil {
-		(*w).Send(etyp, e)
+		(*w).AsWidget().Send(etyp, e)
 		clearLong()
 		// fmt.Println("lhw, send end, cleared")
 		return
@@ -665,7 +665,7 @@ func (em *Events) HandleLong(e events.Event, deep Widget, w *Widget, pos *image.
 		if *w == nil {
 			return
 		}
-		(*w).Send(styp, e)
+		(*w).AsWidget().Send(styp, e)
 		// we are done with the timer, and this indicates that
 		// we have sent a start event
 		*t = nil
@@ -729,7 +729,7 @@ func (em *Events) StartRepeatClickTimer() {
 		if em.RepeatClick == nil || !em.RepeatClick.IsVisible() {
 			return
 		}
-		em.RepeatClick.Send(events.Click)
+		em.RepeatClick.AsWidget().Send(events.Click)
 		em.StartRepeatClickTimer()
 	})
 }
@@ -810,7 +810,7 @@ func (em *Events) DragDrop(drag Widget, e events.Event) {
 		return
 	}
 	for _, dwi := range em.DragHovers {
-		dwi.SetState(false, states.DragHovered)
+		dwi.AsWidget().SetState(false, states.DragHovered)
 	}
 	targ := em.DragHovers[len(em.DragHovers)-1]
 	de := events.NewDragDrop(events.Drop, e.(*events.Mouse)) // gets the actual mod at this point
@@ -820,7 +820,7 @@ func (em *Events) DragDrop(drag Widget, e events.Event) {
 	if DebugSettings.EventTrace {
 		fmt.Println(targ, "Drop with mod:", de.DropMod, "source:", de.Source)
 	}
-	targ.HandleEvent(de)
+	targ.AsWidget().HandleEvent(de)
 }
 
 // DropFinalize should be called as the last step in the Drop event processing,
@@ -832,7 +832,7 @@ func (em *Events) DropFinalize(de *events.DragDrop) {
 	}
 	de.Typ = events.DropDeleteSource
 	de.ClearHandled()
-	de.Source.(Widget).HandleEvent(de)
+	de.Source.(Widget).AsWidget().HandleEvent(de)
 }
 
 // Clipboard returns the system system.Clipboard, supplying the window context
@@ -932,11 +932,11 @@ func (em *Events) SetFocusImpl(w Widget, sendEvent bool) bool {
 		if DebugSettings.FocusTrace {
 			fmt.Println(em.Scene, "Losing focus:", cfoc)
 		}
-		cfoc.Send(events.FocusLost)
+		cfoc.AsWidget().Send(events.FocusLost)
 	}
 	em.Focus = w
 	if sendEvent && w != nil {
-		w.Send(events.Focus)
+		w.AsWidget().Send(events.Focus)
 	}
 	return true
 }
@@ -956,7 +956,7 @@ func (em *Events) FocusNext() bool {
 // returns true if a focus item found.
 func (em *Events) FocusNextFrom(from Widget) bool {
 	next := WidgetNextFunc(from, func(w Widget) bool {
-		return w.IsVisible() && !w.StateIs(states.Disabled) && w.AbilityIs(abilities.Focusable)
+		return w.IsVisible() && !w.AsWidget().StateIs(states.Disabled) && w.AsWidget().AbilityIs(abilities.Focusable)
 	})
 	em.SetFocusEvent(next)
 	return next != nil
@@ -1011,7 +1011,7 @@ func (em *Events) FocusPrev() bool {
 // (can be nil).
 func (em *Events) FocusPrevFrom(from Widget) bool {
 	prev := WidgetPrevFunc(from, func(w Widget) bool {
-		return w.IsVisible() && !w.StateIs(states.Disabled) && w.AbilityIs(abilities.Focusable)
+		return w.IsVisible() && !w.AsWidget().StateIs(states.Disabled) && w.AsWidget().AbilityIs(abilities.Focusable)
 	})
 	em.SetFocusEvent(prev)
 	return prev != nil
@@ -1054,7 +1054,7 @@ func (em *Events) ClearNonFocus(foc Widget) {
 			if DebugSettings.EventTrace {
 				fmt.Printf("ClearNonFocus: had focus: %v\n", wb.Path())
 			}
-			wi.Send(events.FocusLost)
+			wi.AsWidget().Send(events.FocusLost)
 		}
 		return tree.Continue
 	})
diff --git a/core/layout.go b/core/layout.go
index bf501a761..96705a180 100644
--- a/core/layout.go
+++ b/core/layout.go
@@ -90,8 +90,8 @@ const (
 ///////////////////////////////////////////////////////////////////
 // Layouter
 
-// Layouter is the interface for layout functions, called by Layout
-// widget type during the various Layout passes.
+// Layouter is the interface for layout functions, called by [Frame]
+// during the various layout passes.
 type Layouter interface {
 	Widget
 
diff --git a/core/menu.go b/core/menu.go
index f10feb225..199a334a6 100644
--- a/core/menu.go
+++ b/core/menu.go
@@ -170,7 +170,7 @@ func (wb *WidgetBase) HandleWidgetContextMenu() {
 func (wb *WidgetBase) ShowContextMenu(e events.Event) {
 	e.SetHandled() // always
 	wi := wb.This.(Widget)
-	nm := NewMenu(wi.ApplyContextMenus, wi, wi.ContextMenuPos(e))
+	nm := NewMenu(wi.AsWidget().ApplyContextMenus, wi, wi.ContextMenuPos(e))
 	if nm == nil { // no items
 		return
 	}
diff --git a/core/scene.go b/core/scene.go
index 190cd925d..06f8407ee 100644
--- a/core/scene.go
+++ b/core/scene.go
@@ -270,7 +270,7 @@ func (sc *Scene) Close() bool {
 	e := &events.Base{Typ: events.Close}
 	e.Init()
 	sc.WidgetWalkDown(func(kwi Widget, kwb *WidgetBase) bool {
-		kwi.HandleEvent(e)
+		kwi.AsWidget().HandleEvent(e)
 		return tree.Continue
 	})
 	// if they set the event as handled, we do not close the scene
diff --git a/core/spinner.go b/core/spinner.go
index 175a5f727..b0c5349a7 100644
--- a/core/spinner.go
+++ b/core/spinner.go
@@ -104,7 +104,7 @@ func (sp *Spinner) Init() {
 	sp.OnWidgetAdded(func(w Widget) {
 		switch w.AsTree().PathFrom(sp) {
 		case "lead-icon", "trail-icon": // TODO(config)
-			w.Styler(func(s *styles.Style) {
+			w.AsWidget().Styler(func(s *styles.Style) {
 				// icons do not get separate focus, as people can
 				// use the arrow keys to get the same effect
 				s.SetAbilities(false, abilities.Focusable)
diff --git a/core/splits.go b/core/splits.go
index 32e745c73..15ac2ea2f 100644
--- a/core/splits.go
+++ b/core/splits.go
@@ -49,7 +49,7 @@ func (sl *Splits) Init() {
 	})
 	sl.OnWidgetAdded(func(w Widget) {
 		if w.AsTree().Parent == sl.This { // TODO(config): need some way to do this with the new config paradigm
-			w.Styler(func(s *styles.Style) {
+			w.AsWidget().Styler(func(s *styles.Style) {
 				// splits elements must scroll independently and grow
 				s.Overflow.Set(styles.OverflowAuto)
 				s.Grow.Set(1, 1)
diff --git a/core/stages.go b/core/stages.go
index b599b1665..1e0d8038a 100644
--- a/core/stages.go
+++ b/core/stages.go
@@ -307,7 +307,7 @@ func (sm *Stages) SendShowEvents() {
 				// pr := profile.Start("send show")
 				sc.Events.GetShortcuts()
 				sc.WidgetWalkDown(func(kwi Widget, kwb *WidgetBase) bool {
-					kwi.Send(events.Show)
+					kwi.AsWidget().Send(events.Show)
 					return tree.Continue
 				})
 				// pr.End()
diff --git a/core/style.go b/core/style.go
index cddeb6e50..f8d472e96 100644
--- a/core/style.go
+++ b/core/style.go
@@ -223,7 +223,7 @@ func StyleFromTags(w Widget, tags reflect.StructTag) {
 			}
 		}
 	}
-	w.Styler(func(s *styles.Style) {
+	w.AsWidget().Styler(func(s *styles.Style) {
 		style("width", s.Min.X.Ch)
 		style("max-width", s.Max.X.Ch)
 		style("height", s.Min.Y.Em)
diff --git a/core/tabs.go b/core/tabs.go
index da379ee4b..134490623 100644
--- a/core/tabs.go
+++ b/core/tabs.go
@@ -126,7 +126,7 @@ func (ts *Tabs) Init() {
 	})
 	ts.OnWidgetAdded(func(w Widget) {
 		if w.AsTree().Parent == ts.ChildByName("frame") { // TODO(config): figure out how to get this to work with new config paradigm
-			w.Styler(func(s *styles.Style) {
+			w.AsWidget().Styler(func(s *styles.Style) {
 				// tab frames must scroll independently and grow
 				s.Overflow.Set(styles.OverflowAuto)
 				s.Grow.Set(1, 1)
diff --git a/core/textfield.go b/core/textfield.go
index e923dae00..295142bc5 100644
--- a/core/textfield.go
+++ b/core/textfield.go
@@ -1287,7 +1287,7 @@ func init() {
 			return
 		}
 		tf := AsTextField(w)
-		if !w.StateIs(states.Focused) || !w.IsVisible() {
+		if !w.AsWidget().StateIs(states.Focused) || !w.IsVisible() {
 			tf.BlinkOn = false
 			tf.RenderCursor(false)
 		} else {
diff --git a/core/toolbar.go b/core/toolbar.go
index 4947e0dca..e71666638 100644
--- a/core/toolbar.go
+++ b/core/toolbar.go
@@ -218,7 +218,7 @@ func (tb *Toolbar) AddOverflowMenu(fun func(m *Scene)) *Toolbar {
 
 // ToolbarStyles styles the given widget to have standard toolbar styling.
 func ToolbarStyles(w Widget) {
-	w.Styler(func(s *styles.Style) {
+	w.AsWidget().Styler(func(s *styles.Style) {
 		s.Border.Radius = styles.BorderRadiusFull
 		s.Background = colors.C(colors.Scheme.SurfaceContainer)
 		s.Gap.Zero()
@@ -233,7 +233,7 @@ func ToolbarStyles(w Widget) {
 			s.Padding.SetVertical(units.Dp(16))
 		}
 	})
-	w.OnWidgetAdded(func(w Widget) { // TODO(config)
+	w.AsWidget().OnWidgetAdded(func(w Widget) { // TODO(config)
 		if bt := AsButton(w); bt != nil {
 			bt.Type = ButtonAction
 			return
diff --git a/core/value.go b/core/value.go
index 1dc07ab79..72dc34bfb 100644
--- a/core/value.go
+++ b/core/value.go
@@ -89,7 +89,7 @@ func InitValueButton(v Value, allowReadOnly bool, make func(d *Body), after ...f
 	if !TheApp.Platform().IsMobile() {
 		wb.SetTooltip(shiftNewWindow)
 	}
-	v.OnClick(func(e events.Event) {
+	wb.OnClick(func(e events.Event) {
 		if allowReadOnly || !wb.IsReadOnly() {
 			v.AsTree().SetFlag(e.HasAnyModifier(key.Shift), ValueDialogNewWindow)
 			OpenValueDialog(v, v.AsWidget(), make, after...)
@@ -122,7 +122,7 @@ func OpenValueDialog(v Value, ctx Widget, make func(d *Body), after ...func()) {
 	if len(after) == 0 {
 		d.OnClose(func(e events.Event) {
 			wb.SendChange()
-			v.Update()
+			wb.Update()
 		})
 	} else {
 		// otherwise, we have to make the bottom bar
@@ -131,7 +131,7 @@ func OpenValueDialog(v Value, ctx Widget, make func(d *Body), after ...func()) {
 			d.AddOK(parent).OnClick(func(e events.Event) {
 				after[0]()
 				wb.SendChange()
-				v.Update()
+				wb.Update()
 			})
 		})
 	}
diff --git a/core/widget.go b/core/widget.go
index c410c9374..d94e20534 100644
--- a/core/widget.go
+++ b/core/widget.go
@@ -8,7 +8,6 @@ package core
 //go:generate core generate
 
 import (
-	"fmt"
 	"image"
 	"log/slog"
 
@@ -25,37 +24,22 @@ import (
 	"cogentcore.org/core/types"
 )
 
-// Widget is the interface for all Cogent Core widgets.
+// Widget is the interface that all Cogent Core satisfy.
+// The core widget functionality is defined on [WidgetBase],
+// and all higher-level widget types must embed it. This
+// interface only contains the methods that higher-level
+// widget types may need to override. You can call
+// [Widget.AsWidget] to get the [WidgetBase] of a Widget
+// and access the core widget functionality.
 type Widget interface {
 	tree.Node
 
-	// AsWidget returns the [WidgetBase] embedded field.
+	// AsWidget returns the [WidgetBase] of this Widget. Most
+	// core widget functionality is implemented on [WidgetBase].
 	AsWidget() *WidgetBase
 
-	// OnWidgetAdded adds a function to call when a widget is added
-	// as a child to the widget or any of its children.
-	OnWidgetAdded(f func(w Widget)) *WidgetBase
-
-	// See [WidgetBase.Styler].
-	Styler(s func(s *styles.Style)) *WidgetBase
-
-	// See [WidgetBase.Update].
-	Update()
-
-	// StateIs returns whether the widget has the given [states.States] flag set
-	StateIs(flag states.States) bool
-
-	// AbilityIs returns whether the widget has the given [abilities.Abilities] flag set
-	AbilityIs(flag abilities.Abilities) bool
-
-	// SetState sets the given [states.State] flags to the given value
-	SetState(on bool, state ...states.States) *WidgetBase
-
-	// SetAbilities sets the given [abilities.Abilities] flags to the given value
-	SetAbilities(on bool, able ...abilities.Abilities) *WidgetBase
-
 	// See [WidgetBase.Style].
-	Style()
+	Style() // TODO(config): remove
 
 	// SizeUp (bottom-up) gathers Actual sizes from our Children & Parts,
 	// based on Styles.Min / Max sizes and actual content sizing
@@ -107,26 +91,6 @@ type Widget interface {
 	// for widget-specific rendering.
 	RenderWidget()
 
-	// On adds an event listener function for the given event type
-	On(etype events.Types, fun func(e events.Event)) *WidgetBase
-
-	// OnClick adds an event listener function for [events.Click] events
-	OnClick(fun func(e events.Event)) *WidgetBase
-
-	// HandleEvent sends the given event to all Listeners for that event type.
-	// It also checks if the State has changed and calls ApplyStyle if so.
-	// If more significant Config level changes are needed due to an event,
-	// the event handler must do this itself.
-	HandleEvent(e events.Event)
-
-	// Send sends an NEW event of given type to this widget,
-	// optionally starting from values in the given original event
-	// (recommended to include where possible).
-	// Do NOT send an existing event using this method if you
-	// want the Handled state to persist throughout the call chain;
-	// call HandleEvent directly for any existing events.
-	Send(e events.Types, orig ...events.Event)
-
 	// WidgetTooltip returns the tooltip text that should be used for this
 	// widget, and the window-relative position to use for the upper-left corner
 	// of the tooltip. The current mouse position in scene-local coordinates
@@ -142,15 +106,6 @@ type Widget interface {
 	// shortcut to the tooltip here.
 	WidgetTooltip(pos image.Point) (string, image.Point)
 
-	// AddContextMenu adds the given context menu to [WidgetBase.ContextMenus].
-	// It is the main way that code should modify a widget's context menus.
-	// Context menu functions are run in reverse order.
-	AddContextMenu(menu func(m *Scene)) *WidgetBase
-
-	// ApplyContextMenus adds the [Widget.ContextMenus] to the given menu scene
-	// in reverse order.
-	ApplyContextMenus(m *Scene)
-
 	// ContextMenuPos returns the default position for popup menus;
 	// by default in the middle its Bounding Box, but can be adapted as
 	// appropriate for different widgets.
@@ -191,8 +146,10 @@ type Widget interface {
 	DirectRenderDraw(drw system.Drawer, idx int, flipY bool)
 }
 
-// WidgetBase is the base type for all [Widget]s. It renders the
-// standard box model, but does not layout or render any children.
+// WidgetBase implements the [Widget] interface and provides the core functionality
+// of a widget. You must use WidgetBase as an embedded struct in all higher-level
+// widget types. It renders the standard box model, but does not layout or render
+// any children; see [Frame] for that.
 type WidgetBase struct {
 	tree.NodeBase
 
@@ -272,7 +229,7 @@ type WidgetBase struct {
 	FinalListeners events.Listeners `copier:"-" json:"-" xml:"-" set:"-"`
 
 	// A slice of functions to call on all widgets that are added as children
-	// to this widget or its children.  These functions are called in sequential
+	// to this widget or its children. These functions are called in sequential
 	// ascending order, so the last added one is called last and thus can
 	// override anything set by the other ones. These should be set using
 	// OnWidgetAdded, which can be called by both end-user and internal code.
@@ -282,7 +239,7 @@ type WidgetBase struct {
 	// the widget's context menu on an [events.ContextMenu]. The
 	// functions are called in reverse order such that the elements
 	// added in the last function are the first in the menu.
-	// Context menus should be added through [Widget.AddContextMenu].
+	// Context menus should be added through [WidgetBase.AddContextMenu].
 	// Separators will be added between each context menu function.
 	ContextMenus []func(m *Scene) `copier:"-" json:"-" xml:"-" set:"-"`
 
@@ -295,7 +252,7 @@ type WidgetBase struct {
 	ValueUpdate func() `copier:"-" json:"-" xml:"-" set:"-"`
 
 	// ValueOnChange is a function set by [Bind] that is called when
-	// the widget receives an [event.Change] to update the bound value
+	// the widget receives an [events.Change] event to update the bound value
 	// from the widget's value.
 	ValueOnChange func() `copier:"-" json:"-" xml:"-" set:"-"`
 
@@ -508,23 +465,13 @@ func (wb *WidgetBase) IsVisible() bool {
 // DirectRenderImage uploads image directly into given system.Drawer at given index
 // Typically this is a drw.SetGoImage call with an [image.RGBA], or
 // drw.SetFrameImage with a [vgpu.FrameBuffer]
-func (wb *WidgetBase) DirectRenderImage(drw system.Drawer, idx int) {
-}
+func (wb *WidgetBase) DirectRenderImage(drw system.Drawer, idx int) {}
 
 // DirectRenderDraw draws the current image at index onto the RenderWindow window,
 // typically using drw.Copy, drw.Scale, or drw.Fill.
 // flipY is the default setting for whether the Y axis needs to be flipped during drawing,
 // which is typically passed along to the Copy or Scale methods.
-func (wb *WidgetBase) DirectRenderDraw(drw system.Drawer, idx int, flipY bool) {
-}
-
-// FieldByName allows [tree.Node.FindPath] to go through parts.
-func (wb *WidgetBase) FieldByName(field string) (tree.Node, error) {
-	if field == "parts" {
-		return wb.Parts, nil
-	}
-	return nil, fmt.Errorf("no field %q for %v; only parts", field, wb)
-}
+func (wb *WidgetBase) DirectRenderDraw(drw system.Drawer, idx int, flipY bool) {}
 
 // NodeWalkDown extends [tree.Node.WalkDown] to [WidgetBase.Parts],
 // which is key for getting full tree traversal to work when updating,
diff --git a/core/widgetevents.go b/core/widgetevents.go
index 64527342b..b9bcaf3c1 100644
--- a/core/widgetevents.go
+++ b/core/widgetevents.go
@@ -145,7 +145,7 @@ func (wb *WidgetBase) AddCloseDialog(config func(d *Body) bool) *WidgetBase {
 				inClose = false
 				canClose = false
 			})
-			parent.OnWidgetAdded(func(w Widget) {
+			parent.AsWidget().OnWidgetAdded(func(w Widget) { // TODO(config)
 				if bt := AsButton(w); bt != nil {
 					bt.OnFirst(events.Click, func(e events.Event) {
 						// any button click gives us permission to close
@@ -180,11 +180,7 @@ func (wb *WidgetBase) Send(typ events.Types, original ...events.Event) {
 		e = &events.Base{Typ: typ}
 		e.Init()
 	}
-	w, ok := wb.This.(Widget)
-	if !ok {
-		return
-	}
-	w.HandleEvent(e)
+	wb.HandleEvent(e)
 }
 
 // SendChange sends the [events.Change] event, which is widely used to signal
@@ -221,11 +217,7 @@ func (wb *WidgetBase) SendKeyChordRune(r rune, code key.Codes, mods key.Modifier
 		ke.Init()
 	}
 	ke.Typ = events.KeyChord
-	w, ok := wb.This.(Widget)
-	if !ok {
-		return
-	}
-	w.HandleEvent(ke)
+	wb.HandleEvent(ke)
 }
 
 // HandleEvent sends the given event to all Listeners for that event type.
@@ -495,7 +487,7 @@ func (wb *WidgetBase) HandleClickOnEnterSpace() {
 // the widget being styled as focused.  See [SetFocusEvent] for one that does.
 func (wb *WidgetBase) SetFocus() {
 	foc := wb.This.(Widget)
-	if !foc.AbilityIs(abilities.Focusable) {
+	if !wb.AbilityIs(abilities.Focusable) {
 		foc = wb.FocusableInMe()
 		if foc == nil {
 			foc = wb.This.(Widget)
@@ -514,7 +506,7 @@ func (wb *WidgetBase) SetFocus() {
 // the widget being styled as focused.  See [SetFocus] for one that does not.
 func (wb *WidgetBase) SetFocusEvent() {
 	foc := wb.This.(Widget)
-	if !foc.AbilityIs(abilities.Focusable) {
+	if !wb.AbilityIs(abilities.Focusable) {
 		foc = wb.FocusableInMe()
 		if foc == nil {
 			foc = wb.This.(Widget)
diff --git a/docs/content/advanced/styling.md b/docs/content/advanced/styling.md
index 9b8197893..db5eec1cc 100644
--- a/docs/content/advanced/styling.md
+++ b/docs/content/advanced/styling.md
@@ -12,8 +12,8 @@ You can style all widgets within a certain container at once using [[core.Widget
 
 ```Go
 fr := core.NewFrame(parent)
-fr.OnWidgetAdded(func(w core.Widget) {
-    w.Styler(func(s *styles.Style) {
+fr.OnWidgetAdded(func(w core.Widget) { // TODO(config)
+    w.AsWidget().Styler(func(s *styles.Style) {
         s.Color = colors.C(colors.Scheme.Error.Base)
     })
 })
diff --git a/docs/pagegen.go b/docs/pagegen.go
index 01762b46b..c46fee8b8 100644
--- a/docs/pagegen.go
+++ b/docs/pagegen.go
@@ -1023,8 +1023,8 @@ func main() {
 	},
 	"advanced/styling-0": func(parent core.Widget) {
 		fr := core.NewFrame(parent)
-		fr.OnWidgetAdded(func(w core.Widget) {
-			w.Styler(func(s *styles.Style) {
+		fr.OnWidgetAdded(func(w core.Widget) { // TODO(config)
+			w.AsWidget().Styler(func(s *styles.Style) {
 				s.Color = colors.C(colors.Scheme.Error.Base)
 			})
 		})
diff --git a/examples/filetree/filetree.go b/examples/filetree/filetree.go
index 60ec92b6c..48bdaf076 100644
--- a/examples/filetree/filetree.go
+++ b/examples/filetree/filetree.go
@@ -68,17 +68,17 @@ func (fb *FileBrowse) Init() {
 		case "title":
 			title := w.(*core.Text)
 			title.Type = core.TextHeadlineSmall
-			w.Styler(func(s *styles.Style) {
+			w.AsWidget().Styler(func(s *styles.Style) {
 				s.Justify.Content = styles.Center
 			})
 		}
 		if w.AsTree().Parent.AsTree().PathFrom(fb) == "splits" {
 			if w.AsTree().IndexInParent() == 0 {
-				w.Styler(func(s *styles.Style) {
+				w.AsWidget().Styler(func(s *styles.Style) {
 					s.Grow.Set(1, 1)
 				})
 			} else {
-				w.Styler(func(s *styles.Style) {
+				w.AsWidget().Styler(func(s *styles.Style) {
 					s.Grow.Set(1, 1)
 					s.Min.X.Ch(20)
 					s.Min.Y.Ch(10)
diff --git a/htmlview/context.go b/htmlview/context.go
index 17d2e15b1..4d1ac7106 100644
--- a/htmlview/context.go
+++ b/htmlview/context.go
@@ -121,7 +121,7 @@ func (c *Context) Config(w core.Widget) {
 	}
 	wb.SetProperty("tag", c.Node.Data)
 	rules := c.Styles[c.Node]
-	w.Styler(func(s *styles.Style) {
+	wb.Styler(func(s *styles.Style) {
 		for _, rule := range rules {
 			for _, decl := range rule.Declarations {
 				// TODO(kai/styproperties): parent style and context
@@ -140,7 +140,7 @@ func (c *Context) InlineParent() core.Widget {
 	c.InlinePw = core.NewFrame(c.BlockParent)
 	c.InlinePw.AsTree().SetName("inline-container")
 	tree.SetUniqueName(c.InlinePw)
-	c.InlinePw.Styler(func(s *styles.Style) {
+	c.InlinePw.AsWidget().Styler(func(s *styles.Style) {
 		s.Grow.Set(1, 0)
 	})
 	return c.InlinePw
diff --git a/htmlview/handler.go b/htmlview/handler.go
index 08c68a2c4..a6186214a 100644
--- a/htmlview/handler.go
+++ b/htmlview/handler.go
@@ -88,7 +88,7 @@ func HandleElement(ctx *Context) {
 	case "body", "main", "div", "section", "nav", "footer", "header", "ol", "ul":
 		ctx.NewParent = New[*core.Frame](ctx)
 		if tag == "body" {
-			ctx.NewParent.Styler(func(s *styles.Style) {
+			ctx.NewParent.AsWidget().Styler(func(s *styles.Style) {
 				s.Grow.Set(1, 1)
 			})
 		}
diff --git a/tensor/tensorview/tableview.go b/tensor/tensorview/tableview.go
index 89a36e87b..268b440fa 100644
--- a/tensor/tensorview/tableview.go
+++ b/tensor/tensorview/tableview.go
@@ -333,7 +333,7 @@ func (tv *TableView) MakeRow(p *core.Plan, i int) {
 						}
 					}
 					wb.SetReadOnly(tv.IsReadOnly())
-					w.SetState(invis, states.Invisible)
+					wb.SetState(invis, states.Invisible)
 					if svi.HasStyleFunc() {
 						w.Style()
 					}
diff --git a/texteditor/cursor.go b/texteditor/cursor.go
index a8a0bfaee..da9c81804 100644
--- a/texteditor/cursor.go
+++ b/texteditor/cursor.go
@@ -31,7 +31,7 @@ func init() {
 			return
 		}
 		ed := AsEditor(w)
-		if !w.StateIs(states.Focused) || !w.IsVisible() {
+		if !w.AsWidget().StateIs(states.Focused) || !w.IsVisible() {
 			ed.BlinkOn = false
 			ed.RenderCursor(false)
 		} else {
diff --git a/tree/node.go b/tree/node.go
index 0ead46113..abaff9159 100644
--- a/tree/node.go
+++ b/tree/node.go
@@ -67,12 +67,6 @@ type Node interface {
 	// This is used in the GUI for determining what types of children can be created.
 	BaseType() *types.Type
 
-	// FieldByName returns the node that is a direct field with the given name.
-	// This must be implemented for any types that have Node fields that
-	// are processed as part of the overall Node tree. This is only used
-	// by [Node.FindPath]. Returns error if not found.
-	FieldByName(field string) (Node, error)
-
 	// Destroy recursively deletes and destroys the node, all of its children,
 	// and all of its children's children, etc. Node types can implement this
 	// to do additional necessary destruction; if they do, they should call
diff --git a/tree/nodebase.go b/tree/nodebase.go
index ace309e87..800ba7fd6 100644
--- a/tree/nodebase.go
+++ b/tree/nodebase.go
@@ -5,11 +5,9 @@
 package tree
 
 import (
-	"errors"
 	"log/slog"
 	"maps"
 	"slices"
-	"strconv"
 	"strings"
 
 	"github.com/jinzhu/copier"
@@ -222,46 +220,37 @@ func (n *NodeBase) ChildByType(t *types.Type, embeds bool, startIndex ...int) No
 
 // TODO: is this the best way to escape paths?
 
-// EscapePathName returns a name that replaces any path delimiter symbols
-// . or / with \, and \\ escaped versions.
+// EscapePathName returns a name that replaces any / with \\
 func EscapePathName(name string) string {
-	return strings.ReplaceAll(strings.ReplaceAll(name, ".", `\,`), "/", `\\`)
+	return strings.ReplaceAll(name, "/", `\\`)
 }
 
-// UnescapePathName returns a name that replaces any escaped path delimiter symbols
-// \, or \\ with . and / unescaped versions.
+// UnescapePathName returns a name that replaces any \\ with /
 func UnescapePathName(name string) string {
-	return strings.ReplaceAll(strings.ReplaceAll(name, `\,`, "."), `\\`, "/")
+	return strings.ReplaceAll(name, `\\`, "/")
 }
 
 // Path returns the path to this node from the tree root,
-// using [Node.Name]s separated by / and fields by .
-// Path is only valid for finding items when child names
-// are unique. Any existing / and . characters in names
-// are escaped to \\ and \,
+// using [Node.Name]s separated by / delimeters. Any
+// existing / characters in names are escaped to \\
 func (n *NodeBase) Path() string {
 	if n.Parent != nil {
-		if n.Is(Field) {
-			return n.Parent.AsTree().Path() + "." + EscapePathName(n.Name)
-		}
 		return n.Parent.AsTree().Path() + "/" + EscapePathName(n.Name)
 	}
 	return "/" + EscapePathName(n.Name)
 }
 
-// PathFrom returns path to this node from the given parent node, using
-// [Node.Name]s separated by / and fields by .
-// Path is only valid for finding items when child names
-// are unique. Any existing / and . characters in names
-// are escaped to \\ and \,
+// PathFrom returns the path to this node from the given parent node,
+// using [Node.Name]s separated by / delimeters. Any
+// existing / characters in names are escaped to \\
 //
 // The paths that it returns exclude the
 // name of the parent and the leading slash; for example, in the tree
 // a/b/c/d/e, the result of d.PathFrom(b) would be c/d. PathFrom
-// automatically gets the [Node.This] version of the given parent,
-// so a base type can be passed in without manually calling [Node.This].
+// automatically gets the [NodeBase.This] version of the given parent,
+// so a base type can be passed in without manually accessing [NodeBase.This].
 func (n *NodeBase) PathFrom(parent Node) string {
-	// critical to get "This"
+	// critical to get `This`
 	parent = parent.AsTree().This
 	// we bail a level below the parent so it isn't in the path
 	if n.Parent == nil || n.Parent == parent {
@@ -273,38 +262,15 @@ func (n *NodeBase) PathFrom(parent Node) string {
 	} else {
 		ppath = n.Parent.AsTree().PathFrom(parent)
 	}
-	if n.Is(Field) {
-		return ppath + "." + EscapePathName(n.Name)
-	}
 	return ppath + "/" + EscapePathName(n.Name)
 
 }
 
-// findPathChild finds the child on the path.
-func findPathChild(n Node, child string) int {
-	if len(child) == 0 {
-		return -1
-	}
-	if child[0] == '[' && child[len(child)-1] == ']' {
-		idx, err := strconv.Atoi(child[1 : len(child)-1])
-		if err != nil {
-			return idx
-		}
-		if idx < 0 { // from end
-			idx = len(n.AsTree().Children) + idx
-		}
-		return idx
-	}
-	return IndexByName(n.AsTree().Children, child)
-}
-
 // FindPath returns the node at the given path from this node.
 // FindPath only works correctly when names are unique.
-// Path has [Node.Name]s separated by / and fields by .
-// Node names escape any existing / and . characters to \\ and \,
-// There is also support for [idx] index-based access for any given path
-// element, for cases when indexes are more useful than names.
-// Returns nil if not found.
+// The given path must be consistent with the format produced
+// by [NodeBase.PathFrom]. It returns nil if no node is found
+// at the given path.
 func (n *NodeBase) FindPath(path string) Node {
 	curn := n.This
 	pels := strings.Split(strings.Trim(strings.TrimSpace(path), "\""), "/")
@@ -312,40 +278,15 @@ func (n *NodeBase) FindPath(path string) Node {
 		if len(pe) == 0 {
 			continue
 		}
-		if strings.Contains(pe, ".") { // has fields
-			fels := strings.Split(pe, ".")
-			// find the child first, then the fields
-			idx := findPathChild(curn, UnescapePathName(fels[0]))
-			if idx < 0 {
-				return nil
-			}
-			curn = curn.AsTree().Children[idx]
-			for i := 1; i < len(fels); i++ {
-				fe := UnescapePathName(fels[i])
-				fk, err := curn.FieldByName(fe)
-				if err != nil {
-					slog.Debug("tree.FindPath: %v", err)
-					return nil
-				}
-				curn = fk
-			}
-		} else {
-			idx := findPathChild(curn, UnescapePathName(pe))
-			if idx < 0 {
-				return nil
-			}
-			curn = curn.AsTree().Children[idx]
+		idx := IndexByName(curn.AsTree().Children, UnescapePathName(pe))
+		if idx < 0 {
+			return nil
 		}
+		curn = curn.AsTree().Children[idx]
 	}
 	return curn
 }
 
-// FieldByName is a placeholder implementation of [Node.FieldByName]
-// that returns an error.
-func (n *NodeBase) FieldByName(field string) (Node, error) {
-	return nil, errors.New("tree.NodeBase.FieldByName: no tree fields defined for this node")
-}
-
 // Adding and Inserting Children:
 
 // AddChild adds given child at end of children list.
diff --git a/views/mapview.go b/views/mapview.go
index 72b0ae454..8488adfba 100644
--- a/views/mapview.go
+++ b/views/mapview.go
@@ -77,7 +77,7 @@ func (mv *MapView) Init() {
 				BindMapKey(mapv, key, w)
 				wb := w.AsWidget()
 				wb.SetReadOnly(mv.IsReadOnly())
-				w.Styler(func(s *styles.Style) {
+				wb.Styler(func(s *styles.Style) {
 					s.SetReadOnly(mv.IsReadOnly())
 					s.SetTextWrap(false)
 				})
@@ -88,7 +88,7 @@ func (mv *MapView) Init() {
 				wb.SetReadOnly(mv.IsReadOnly())
 				wb.OnInput(mv.HandleEvent)
 				if !mv.IsReadOnly() {
-					w.AddContextMenu(func(m *core.Scene) {
+					wb.AddContextMenu(func(m *core.Scene) {
 						mv.ContextMenu(m, key)
 					})
 				}
@@ -106,12 +106,12 @@ func (mv *MapView) Init() {
 				wb.SetReadOnly(mv.IsReadOnly())
 				wb.OnChange(func(e events.Event) { mv.SendChange(e) })
 				wb.OnInput(mv.HandleEvent)
-				w.Styler(func(s *styles.Style) {
+				wb.Styler(func(s *styles.Style) {
 					s.SetReadOnly(mv.IsReadOnly())
 					s.SetTextWrap(false)
 				})
 				if !mv.IsReadOnly() {
-					w.AddContextMenu(func(m *core.Scene) {
+					wb.AddContextMenu(func(m *core.Scene) {
 						mv.ContextMenu(m, key)
 					})
 				}
diff --git a/views/sliceview.go b/views/sliceview.go
index 16ee8c4ad..ace4ce645 100644
--- a/views/sliceview.go
+++ b/views/sliceview.go
@@ -599,8 +599,8 @@ func (sv *SliceViewBase) MakeGrid(p *core.Plan, maker func(p *core.Plan)) {
 func (sv *SliceViewBase) MakeValue(w core.Value, i int) {
 	svi := sv.This.(SliceViewer)
 	wb := w.AsWidget()
-	w.AsTree().SetProperty(SliceViewRowProperty, i)
-	w.Styler(func(s *styles.Style) {
+	wb.SetProperty(SliceViewRowProperty, i)
+	wb.Styler(func(s *styles.Style) {
 		if sv.IsReadOnly() {
 			s.SetAbilities(true, abilities.DoubleClickable)
 			s.SetAbilities(false, abilities.Hoverable, abilities.Focusable, abilities.Activatable, abilities.TripleClickable)
@@ -620,7 +620,7 @@ func (sv *SliceViewBase) MakeValue(w core.Value, i int) {
 		sv.LastClick = row + sv.StartIndex
 	})
 	wb.OnDoubleClick(sv.HandleEvent)
-	w.On(events.ContextMenu, sv.HandleEvent)
+	wb.On(events.ContextMenu, sv.HandleEvent)
 	if !sv.IsReadOnly() {
 		wb.OnInput(sv.HandleEvent)
 	}
@@ -653,7 +653,7 @@ func (sv *SliceViewBase) MakeRow(p *core.Plan, i int) {
 			val := sv.SliceElementValue(vi)
 			core.Bind(val.Addr().Interface(), w)
 			wb.SetReadOnly(sv.IsReadOnly())
-			w.SetState(invis, states.Invisible)
+			wb.SetState(invis, states.Invisible)
 			if sv.This.(SliceViewer).HasStyleFunc() {
 				w.Style()
 			}
diff --git a/views/structview.go b/views/structview.go
index 71e0b424d..ad031dc22 100644
--- a/views/structview.go
+++ b/views/structview.go
@@ -174,7 +174,7 @@ func (sv *StructView) Init() {
 							core.ErrorSnackbar(w, err, "Error setting default value")
 						} else {
 							w.Update()
-							valueWidget.Update()
+							valueWidget.AsWidget().Update()
 							valueWidget.AsWidget().SendChange(e)
 						}
 					})
diff --git a/views/tableview.go b/views/tableview.go
index a3a878f5a..bada92114 100644
--- a/views/tableview.go
+++ b/views/tableview.go
@@ -336,7 +336,7 @@ func (tv *TableView) MakeRow(p *core.Plan, i int) {
 				}
 				wb.ValueTitle = vc + " (" + wb.ValueTitle + ")"
 				wb.SetReadOnly(tv.IsReadOnly() || readOnlyTag)
-				w.SetState(invis, states.Invisible)
+				wb.SetState(invis, states.Invisible)
 				if svi.HasStyleFunc() {
 					w.Style()
 				}


Test patch:
diff --git a/core/layout_test.go b/core/layout_test.go
index b069116cb..0136e36e5 100644
--- a/core/layout_test.go
+++ b/core/layout_test.go
@@ -151,7 +151,7 @@ func TestLayoutFramesJustifySelf(t *testing.T) {
 			s.Justify.Items = align
 		})
 		PlainFrames(b, math32.Vec2(0, 0))
-		b.Child(2).(Widget).Styler(func(s *styles.Style) {
+		b.Child(2).(Widget).AsWidget().Styler(func(s *styles.Style) {
 			s.Justify.Self = aligns[(ai+1)%len(aligns)]
 		})
 		b.AssertRender(t, tdir+tnm)
@@ -172,7 +172,7 @@ func TestLayoutFramesAlignSelf(t *testing.T) {
 			s.Align.Items = align
 		})
 		PlainFrames(b, math32.Vec2(0, 0))
-		b.Child(2).(Widget).Styler(func(s *styles.Style) {
+		b.Child(2).(Widget).AsWidget().Styler(func(s *styles.Style) {
 			s.Align.Self = aligns[(ai+1)%len(aligns)]
 		})
 		b.AssertRender(t, tdir+tnm)
diff --git a/core/switches_test.go b/core/switches_test.go
index f79c13977..d9935d15a 100644
--- a/core/switches_test.go
+++ b/core/switches_test.go
@@ -33,8 +33,8 @@ func TestSwitchesMutex(t *testing.T) {
 	b := NewBody()
 	sw := NewSwitches(b).SetMutex(true).SetStrings("Go", "Python", "C++")
 	b.AssertRender(t, "switches/mutex", func() {
-		sw.Child(0).(Widget).Send(events.Click)
-		sw.Child(1).(Widget).Send(events.Click)
+		sw.Child(0).(Widget).AsWidget().Send(events.Click)
+		sw.Child(1).(Widget).AsWidget().Send(events.Click)
 		assert.Equal(t, "Python", sw.SelectedItem().Value)
 		assert.Equal(t, "Python", sw.SelectedItem().GetText())
 		assert.Equal(t, []SwitchItem{{Value: "Python"}}, sw.SelectedItems())
@@ -73,8 +73,8 @@ func TestSwitchesChange(t *testing.T) {
 		selected = sw.SelectedItems()
 	})
 	b.AssertRender(t, "switches/change", func() {
-		sw.Child(0).(Widget).Send(events.Click)
-		sw.Child(2).(Widget).Send(events.Click)
+		sw.Child(0).(Widget).AsWidget().Send(events.Click)
+		sw.Child(2).(Widget).AsWidget().Send(events.Click)
 		assert.Equal(t, []SwitchItem{{Value: "Go"}, {Value: "C++"}}, selected)
 	})
 }
diff --git a/core/tabs_test.go b/core/tabs_test.go
index a66b05ded..21110c190 100644
--- a/core/tabs_test.go
+++ b/core/tabs_test.go
@@ -38,7 +38,7 @@ func TestTabsClick(t *testing.T) {
 	second := ts.NewTab("Second")
 	NewText(second).SetText("I am second!")
 	b.AssertRender(t, "tabs/click", func() {
-		ts.Tabs().Child(1).(Widget).Send(events.Click)
+		ts.Tabs().Child(1).(Widget).AsWidget().Send(events.Click)
 	})
 }
 
@@ -93,7 +93,7 @@ func TestTabsNewClick(t *testing.T) {
 	ts.NewTab("First")
 	ts.NewTab("Second")
 	b.AssertRender(t, "tabs/new-click", func() {
-		ts.Tabs().ChildByName("new-tab").(Widget).Send(events.Click)
+		ts.Tabs().ChildByName("new-tab").(Widget).AsWidget().Send(events.Click)
 		assert.Equal(t, ts.NumTabs(), 3)
 	})
 }
diff --git a/tree/node_test.go b/tree/node_test.go
index c97b0d1ed..4d3ee36a7 100644
--- a/tree/node_test.go
+++ b/tree/node_test.go
@@ -69,10 +69,10 @@ func TestNodeEscapePaths(t *testing.T) {
 	schild2 := NewNodeBase(child2)
 	schild2.SetName("subchild1")
 	assert.Len(t, parent.Children, 3)
-	assert.Equal(t, `/node-base/child1\,go`, child1.Path())
-	assert.Equal(t, `child1\,go`, child1.PathFrom(parent))
+	assert.Equal(t, `/node-base/child1.go`, child1.Path())
+	assert.Equal(t, `child1.go`, child1.PathFrom(parent))
 	assert.Equal(t, `/node-base/child1\\child1`, child2.Path())
-	assert.Equal(t, `/node-base/child1\\child1\,go`, child3.Path())
+	assert.Equal(t, `/node-base/child1\\child1.go`, child3.Path())
 	assert.Equal(t, `/node-base/child1\\child1/subchild1`, schild2.Path())
 	assert.Equal(t, child1, parent.FindPath(child1.PathFrom(parent)))
 	assert.Equal(t, child3, parent.FindPath(child3.PathFrom(parent)))
diff --git a/tree/testdata/testnodes.go b/tree/testdata/testnodes.go
index 731a95732..584080e9a 100644
--- a/tree/testdata/testnodes.go
+++ b/tree/testdata/testnodes.go
@@ -8,12 +8,8 @@ package testdata
 
 import "cogentcore.org/core/tree"
 
-type TestNode struct {
-	tree.NodeBase
-}
-
 // NodeEmbed embeds tree.Node and adds a couple of fields.
-// Also has a directive processed by typegen.
+// It also has a directive processed by typegen.
 //
 //direct:value
 type NodeEmbed struct {
@@ -21,27 +17,3 @@ type NodeEmbed struct {
 	Mbr1 string
 	Mbr2 int
 }
-
-type NodeField struct {
-	NodeEmbed
-	Field1 NodeEmbed
-}
-
-func (nf *NodeField) FieldByName(field string) (tree.Node, error) {
-	if field == "Field1" {
-		return &nf.Field1, nil
-	}
-	return nf.NodeEmbed.FieldByName(field)
-}
-
-type NodeField2 struct {
-	NodeField
-	Field2 NodeEmbed
-}
-
-func (nf *NodeField2) FieldByName(field string) (tree.Node, error) {
-	if field == "Field2" {
-		return &nf.Field2, nil
-	}
-	return nf.NodeField.FieldByName(field)
-}
diff --git a/tree/testdata/typegen.go b/tree/testdata/typegen.go
index cafa92bda..8b411dc10 100644
--- a/tree/testdata/typegen.go
+++ b/tree/testdata/typegen.go
@@ -7,24 +7,12 @@ import (
 	"cogentcore.org/core/types"
 )
 
-// TestNodeType is the [types.Type] for [TestNode]
-var TestNodeType = types.AddType(&types.Type{Name: "cogentcore.org/core/tree/testdata.TestNode", IDName: "test-node", Embeds: []types.Field{{Name: "NodeBase"}}, Instance: &TestNode{}})
-
-// NewTestNode returns a new [TestNode] with the given optional parent:
-func NewTestNode(parent ...tree.Node) *TestNode { return tree.New[*TestNode](parent...) }
-
-// NodeType returns the [*types.Type] of [TestNode]
-func (t *TestNode) NodeType() *types.Type { return TestNodeType }
-
-// New returns a new [*TestNode] value
-func (t *TestNode) New() tree.Node { return &TestNode{} }
-
 // NodeEmbedType is the [types.Type] for [NodeEmbed]
-var NodeEmbedType = types.AddType(&types.Type{Name: "cogentcore.org/core/tree/testdata.NodeEmbed", IDName: "node-embed", Doc: "NodeEmbed embeds tree.Node and adds a couple of fields.\nAlso has a directive processed by typegen.", Directives: []types.Directive{{Tool: "direct", Directive: "value"}}, Embeds: []types.Field{{Name: "NodeBase"}}, Fields: []types.Field{{Name: "Mbr1"}, {Name: "Mbr2"}}, Instance: &NodeEmbed{}})
+var NodeEmbedType = types.AddType(&types.Type{Name: "cogentcore.org/core/tree/testdata.NodeEmbed", IDName: "node-embed", Doc: "NodeEmbed embeds tree.Node and adds a couple of fields.\nIt also has a directive processed by typegen.", Directives: []types.Directive{{Tool: "direct", Directive: "value"}}, Embeds: []types.Field{{Name: "NodeBase"}}, Fields: []types.Field{{Name: "Mbr1"}, {Name: "Mbr2"}}, Instance: &NodeEmbed{}})
 
 // NewNodeEmbed returns a new [NodeEmbed] with the given optional parent:
 // NodeEmbed embeds tree.Node and adds a couple of fields.
-// Also has a directive processed by typegen.
+// It also has a directive processed by typegen.
 func NewNodeEmbed(parent ...tree.Node) *NodeEmbed { return tree.New[*NodeEmbed](parent...) }
 
 // NodeType returns the [*types.Type] of [NodeEmbed]
@@ -38,33 +26,3 @@ func (t *NodeEmbed) SetMbr1(v string) *NodeEmbed { t.Mbr1 = v; return t }
 
 // SetMbr2 sets the [NodeEmbed.Mbr2]
 func (t *NodeEmbed) SetMbr2(v int) *NodeEmbed { t.Mbr2 = v; return t }
-
-// NodeFieldType is the [types.Type] for [NodeField]
-var NodeFieldType = types.AddType(&types.Type{Name: "cogentcore.org/core/tree/testdata.NodeField", IDName: "node-field", Embeds: []types.Field{{Name: "NodeEmbed"}}, Fields: []types.Field{{Name: "Field1"}}, Instance: &NodeField{}})
-
-// NewNodeField returns a new [NodeField] with the given optional parent:
-func NewNodeField(parent ...tree.Node) *NodeField { return tree.New[*NodeField](parent...) }
-
-// NodeType returns the [*types.Type] of [NodeField]
-func (t *NodeField) NodeType() *types.Type { return NodeFieldType }
-
-// New returns a new [*NodeField] value
-func (t *NodeField) New() tree.Node { return &NodeField{} }
-
-// SetField1 sets the [NodeField.Field1]
-func (t *NodeField) SetField1(v NodeEmbed) *NodeField { t.Field1 = v; return t }
-
-// NodeField2Type is the [types.Type] for [NodeField2]
-var NodeField2Type = types.AddType(&types.Type{Name: "cogentcore.org/core/tree/testdata.NodeField2", IDName: "node-field2", Embeds: []types.Field{{Name: "NodeField"}}, Fields: []types.Field{{Name: "Field2"}}, Instance: &NodeField2{}})
-
-// NewNodeField2 returns a new [NodeField2] with the given optional parent:
-func NewNodeField2(parent ...tree.Node) *NodeField2 { return tree.New[*NodeField2](parent...) }
-
-// NodeType returns the [*types.Type] of [NodeField2]
-func (t *NodeField2) NodeType() *types.Type { return NodeField2Type }
-
-// New returns a new [*NodeField2] value
-func (t *NodeField2) New() tree.Node { return &NodeField2{} }
-
-// SetField2 sets the [NodeField2.Field2]
-func (t *NodeField2) SetField2(v NodeEmbed) *NodeField2 { t.Field2 = v; return t }