From f9ec7adb61c149a3fc9a90b9512e8d6f3664d8a7 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Tue, 25 Aug 2026 09:17:54 +0200 Subject: [PATCH] Draw embedded views as one focused unit A view can only be drawn with the focused frame and title colors while it is the current view, but a panel made of an outer view and an editable field embedded in it has to look focused as a whole, whichever of the two the keyboard is pointed at. Co-authored-by: Claude Opus 5 (1M context) --- pkg/gocui/gui.go | 16 +++++++++++++++- pkg/gocui/parent_view_test.go | 20 ++++++++++++++++++++ pkg/gocui/view.go | 4 +++- 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/pkg/gocui/gui.go b/pkg/gocui/gui.go index 1bc184625..67fbd1aa2 100644 --- a/pkg/gocui/gui.go +++ b/pkg/gocui/gui.go @@ -1615,6 +1615,20 @@ func (g *Gui) ForceFlushViewsContentOnly(views []*View) error { return g.flushContentOnly(views) } +// hasFocus reports whether a view is drawn as focused. Views that are embedded +// in one another (see View.ParentView) form a single unit, so they are all drawn +// as focused while any one of them is the current view. +func (g *Gui) hasFocus(v *View) bool { + return g.currentView != nil && outermostView(v) == outermostView(g.currentView) +} + +func outermostView(v *View) *View { + for v.ParentView != nil { + v = v.ParentView + } + return v +} + // draw manages the cursor and calls the draw function of a view. func (g *Gui) draw(v *View) error { if !v.Visible || v.y1 < v.y0 || v.x1 < v.x0 { @@ -1639,7 +1653,7 @@ func (g *Gui) draw(v *View) error { if v.Frame { var fgColor, bgColor, frameColor Attribute - if g.Highlight && v == g.currentView && g.IsFocused() { + if g.Highlight && g.hasFocus(v) && g.IsFocused() { fgColor = g.SelFgColor bgColor = g.SelBgColor frameColor = g.SelFrameColor diff --git a/pkg/gocui/parent_view_test.go b/pkg/gocui/parent_view_test.go index 510acdbbb..9d56a1c46 100644 --- a/pkg/gocui/parent_view_test.go +++ b/pkg/gocui/parent_view_test.go @@ -56,6 +56,26 @@ func TestFirstMatchingKeybindingOfParentViewWins(t *testing.T) { assert.Equal(t, []string{"first"}, pressed) } +func TestEmbeddedViewsAreFocusedTogether(t *testing.T) { + g := newTestGui(t) + parent, child := setupParentAndChildView(t, g) + sibling, _ := g.SetView("sibling", 0, 12, 20, 14, 0) + sibling.ParentView = parent + unrelated, _ := g.SetView("unrelated", 30, 0, 50, 10, 0) + + assert.True(t, g.hasFocus(child)) + assert.True(t, g.hasFocus(parent)) + assert.True(t, g.hasFocus(sibling)) + assert.False(t, g.hasFocus(unrelated)) + + _, err := g.SetCurrentView(unrelated.Name()) + assert.NoError(t, err) + + assert.True(t, g.hasFocus(unrelated)) + assert.False(t, g.hasFocus(parent)) + assert.False(t, g.hasFocus(child)) +} + func TestPrintableKeysGoToTheFieldBeingTypedIn(t *testing.T) { for _, test := range []struct { name string diff --git a/pkg/gocui/view.go b/pkg/gocui/view.go index dba71ab52..6e7b35520 100644 --- a/pkg/gocui/view.go +++ b/pkg/gocui/view.go @@ -210,7 +210,9 @@ type View struct { // Overlaps describes which edges are overlapping with another view's edges Overlaps byte - // ParentView is the view which catches events bubbled up from the given view if there's no matching handler + // ParentView is the view which catches events bubbled up from the given view if there's no matching handler. + // Views related this way are also drawn as a single focused unit: while one of + // them is the current view, they all get the focused frame and title colors. ParentView *View searcher *searcher