Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 18 additions & 25 deletions service/values_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -501,40 +501,33 @@ func (s *ValuesService) CompareWithBaseline(
return changes, nil
}

// ApplyCollapseFilter removes entries whose key has any collapsed ancestor,
// i.e. the entry sits inside a user-collapsed section. The collapsed section
// header itself is kept visible so the user can click its chevron to expand
// again. Reuses `out` (truncated) to avoid per-frame allocation; `out` and
// `indices` may safely alias — the write pointer never overtakes the read
// pointer. When `collapsed` is empty the function short-circuits and returns
// `indices` unchanged (no copy into `out`).
func ApplyCollapseFilter(
entries []FlatValueEntry,
indices []int,
collapsed map[string]bool,
out []int,
) []int {
// VisibleRows returns the entry indices that render as table rows: every entry
// except those hidden inside a user-collapsed section (the collapsed section
// header itself stays visible so its chevron can re-expand it). When `collapsed`
// is empty this is simply 0..len(entries)-1. Reuses `out` (truncated) to avoid
// per-frame allocation.
func VisibleRows(entries []FlatValueEntry, collapsed map[string]bool, out []int) []int {
out = out[:0]

if len(collapsed) == 0 {
return indices
}
for i := range entries {
out = append(out, i)
}

out = out[:0]
return out
}

// Comment rows have no flat key of their own — visibility piggy-backs on the
// most recently seen non-comment row so a foot comment under a collapsed
// section disappears with that section.
lastNonCommentVisible := true

for _, idx := range indices {
if idx >= len(entries) {
continue
}

entry := entries[idx]
for i := range entries {
entry := entries[i]

if entry.IsComment() {
if lastNonCommentVisible {
out = append(out, idx)
out = append(out, i)
}

continue
Expand All @@ -546,7 +539,7 @@ func ApplyCollapseFilter(
if collapsed[key] {
lastNonCommentVisible = true

out = append(out, idx)
out = append(out, i)

continue
}
Expand All @@ -564,7 +557,7 @@ func ApplyCollapseFilter(
lastNonCommentVisible = !hidden

if !hidden {
out = append(out, idx)
out = append(out, i)
}
}

Expand Down
5 changes: 5 additions & 0 deletions ui/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -670,6 +670,11 @@ func (a *Application) handleKeyEvents(gtx layout.Context) {
a.valuesState.AnchorOpName = ""
case a.valuesState.AnchorMenuOpen:
a.valuesState.AnchorMenuOpen = false
case a.navState.CurrentPage == state.PageValues && a.valuesState.SearchFocused(gtx):
// Escape clears a non-empty search field rather than leaving the page.
a.valuesState.SearchEditor.SetText("")
gtx.Execute(key.FocusCmd{Tag: &a.valuesState.SearchEditor})
gtx.Execute(op.InvalidateCmd{})
default:
a.navigateBack()
}
Expand Down
1 change: 1 addition & 0 deletions ui/page/values_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,7 @@ func (vc *ValuesController) ResetState() {
vc.State.CollapsedPreSearch = nil
vc.State.SearchCollapseActive = false
vc.State.SearchEditor.SetText("")
vc.State.CurrentMatch = -1
vc.State.OverrideList.Position.First = 0
vc.State.OverrideList.Position.Offset = 0

Expand Down
65 changes: 33 additions & 32 deletions ui/page/values_page.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,9 @@ type ValuesPage struct {
lastFocusedRow int
lastFocusedCol int

// lastSearchQuery caches the previous frame's query
lastSearchQuery string

// AnchorDialog and AnchorMenu are the widget instances driving the
// anchor/alias mutation UI. They live on the page (not state) so state
// types don't need to import ui/widget. The page's Layout wires them to
Expand Down Expand Up @@ -245,14 +248,17 @@ type ValuesPage struct {

func NewValuesPage(th *material.Theme, st *state.ValuesPageState, cb ValuesPageCallbacks) *ValuesPage {
st.SearchEditor.SingleLine = true
st.SearchEditor.Submit = true // Enter jumps to the next match
st.CurrentMatch = -1

p := &ValuesPage{
Theme: th,
State: st,
Table: customwidget.OverrideTable{
Theme: th,
List: &st.OverrideList,
HoveredRow: -1,
Theme: th,
List: &st.OverrideList,
HoveredRow: -1,
CurrentMatchEntry: -1,
},
Search: customwidget.SearchBar{Editor: &st.SearchEditor},
ValuesPageCallbacks: cb,
Expand Down Expand Up @@ -374,54 +380,45 @@ func (p *ValuesPage) Layout(gtx layout.Context) layout.Dimensions {
p.Table.OnCellNullify = p.OnCellNullify
p.Table.SearchQuery = p.Search.Editor.Text()

// Build columnEditors slice for search filter.
// Build columnEditors slice for the search match scan.
for c := range p.State.ColumnCount {
p.columnEditorSlices[c] = p.State.Columns[c].OverrideEditors
}

// Recompute filtered indices.
p.State.FilteredIndices = p.Search.FilterEntriesWithMultiOverrides(
// Search highlights and jumps rather than filtering: SearchMatches holds the
// matching entry indices; FilteredIndices (every visible row, built below
// after any jump-induced uncollapse) stays independent of the query.
p.State.SearchMatches = p.Search.MatchingEntries(
p.State.Entries,
p.columnEditorSlices[:p.State.ColumnCount],
p.State.FilteredIndices,
p.State.SearchMatches,
)

// Snapshot the user's collapsed set when search becomes active, so that
// any search-induced auto-uncollapse below can be reverted when the user
// clears the search. Restore on the reverse transition.
inSearch := p.Search.Editor.Text() != ""
wasActive := p.State.SearchCollapseActive
p.syncSearchCollapseSnapshot(inSearch)

// Reset scroll to the top on the empty→non-empty search transition so the
// first match is visible instead of hidden below the prior scroll offset.
if !wasActive && p.State.SearchCollapseActive {
p.State.OverrideList.Position.First = 0
p.State.OverrideList.Position.Offset = 0
}

// Auto-expand any collapsed ancestors of search matches so results are
// never hidden. Mutates CollapsedKeys only — CollapsedPreSearch retains
// the user's intent and is what the controller persists while
// SearchCollapseActive is true, so nothing is lost on search clear.
if inSearch && len(p.State.CollapsedKeys) > 0 {
service.UncollapseMatchAncestors(
p.State.Entries,
p.State.FilteredIndices,
p.State.SearchMatches,
p.State.CollapsedKeys,
)
}

// Hide entries inside collapsed sections. Skipped during an active search
// so matches inside (previously) collapsed sections stay visible.
if !inSearch && len(p.State.CollapsedKeys) > 0 {
p.State.FilteredIndices = service.ApplyCollapseFilter(
p.State.Entries,
p.State.FilteredIndices,
p.State.CollapsedKeys,
p.State.FilteredIndices,
)
}
p.handleSearchNavigation(gtx)

// Build the visible-row list (all entries minus collapsed sections). Done
// after navigation so a jump's uncollapse of its target's ancestors is
// reflected this frame.
p.State.FilteredIndices = service.VisibleRows(
p.State.Entries,
p.State.CollapsedKeys,
p.State.FilteredIndices,
)

// Resolve a pending restored focus key to a filtered row. If the key is no
// longer visible (filtered out or removed from the chart), drop the
Expand Down Expand Up @@ -480,6 +477,8 @@ func (p *ValuesPage) Layout(gtx layout.Context) layout.Dimensions {

p.Table.FocusedRow = p.State.FocusedRow
p.Table.FocusedCol = p.State.FocusedCol
p.Table.CurrentMatchEntry = p.currentMatchEntry()
p.Table.SearchMatches = p.State.SearchMatches

// When the controller signals a pending focus highlight (fires once per
// chart load after the async UI-state load completes), focus the
Expand Down Expand Up @@ -518,13 +517,15 @@ func (p *ValuesPage) Layout(gtx layout.Context) layout.Dimensions {
editors := p.State.Columns[p.State.FocusedCol].OverrideEditors

if entryIdx < len(editors) {
searchBusy := gtx.Focused(p.Search.Editor) && p.Search.Editor.Text() != ""
// Don't steal focus into the cell while the user is working the
// search bar (field or a nav button)
searchBusy := p.State.SearchFocused(gtx)

switch {
case gtx.Focused(&editors[entryIdx]):
// Focus landed — stop retrying.
case searchBusy:
// User is actively typing in search; don't steal focus.
// User is actively working search; don't steal focus.
default:
gtx.Execute(key.FocusCmd{Tag: &editors[entryIdx]})
gtx.Execute(op.InvalidateCmd{})
Expand Down Expand Up @@ -583,7 +584,7 @@ func (p *ValuesPage) Layout(gtx layout.Context) layout.Dimensions {
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
searchHint := platform.ShortcutLabel("\u2318+F", "Ctrl+F")

dims := p.Search.Layout(gtx, p.Theme, "Search values... ("+searchHint+")")
dims := p.Search.Layout(gtx, p.Theme, "Search values... ("+searchHint+")", p.layoutSearchNav)

totalRigidH += dims.Size.Y

Expand Down
5 changes: 5 additions & 0 deletions ui/page/values_page_events.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,11 @@ func collapsedEqual(a, b map[string]bool) bool {
}

func (p *ValuesPage) handleKeyEvents(gtx layout.Context) {
// Intercept the search field's Ctrl/Cmd word chords before the editor's own
// Update runs later this frame (a consumed key event is removed from the
// queue), working around Gio swallowing them on Windows/Linux.
p.handleSearchEditorKeys(gtx)

area := clip.Rect{Max: gtx.Constraints.Max}.Push(gtx.Ops)
event.Op(gtx.Ops, p)
area.Pop()
Expand Down
Loading
Loading