summaryrefslogtreecommitdiffstats
path: root/pkg
diff options
context:
space:
mode:
authorStefan Haller <stefan@haller-berlin.de>2023-03-30 18:17:50 +0200
committerStefan Haller <stefan@haller-berlin.de>2024-06-23 11:54:21 +0200
commit44160ef8448239e397c7bddbbd78bc9cd38327a3 (patch)
treecfba440a269fa079f6d2eeb83f601bd9079ebe87 /pkg
parentdd2bffc278dc3879e112caa3effd9c9c426489af (diff)
Only render visible portion of the screen for commits view
Diffstat (limited to 'pkg')
-rw-r--r--pkg/gui/context/base_context.go59
-rw-r--r--pkg/gui/context/list_context_trait.go27
-rw-r--r--pkg/gui/context/local_commits_context.go14
-rw-r--r--pkg/gui/context/remote_branches_context.go15
-rw-r--r--pkg/gui/context/sub_commits_context.go16
-rw-r--r--pkg/gui/context/view_trait.go9
-rw-r--r--pkg/gui/layout.go14
-rw-r--r--pkg/gui/types/context.go5
-rw-r--r--pkg/integration/components/view_driver.go33
9 files changed, 130 insertions, 62 deletions
diff --git a/pkg/gui/context/base_context.go b/pkg/gui/context/base_context.go
index acece1994..beaa61446 100644
--- a/pkg/gui/context/base_context.go
+++ b/pkg/gui/context/base_context.go
@@ -20,11 +20,12 @@ type BaseContext struct {
onFocusFn onFocusFn
onFocusLostFn onFocusLostFn
- focusable bool
- transient bool
- hasControlledBounds bool
- needsRerenderOnWidthChange bool
- highlightOnFocus bool
+ focusable bool
+ transient bool
+ hasControlledBounds bool
+ needsRerenderOnWidthChange bool
+ needsRerenderOnHeightChange bool
+ highlightOnFocus bool
*ParentContextMgr
}
@@ -37,15 +38,16 @@ type (
var _ types.IBaseContext = &BaseContext{}
type NewBaseContextOpts struct {
- Kind types.ContextKind
- Key types.ContextKey
- View *gocui.View
- WindowName string
- Focusable bool
- Transient bool
- HasUncontrolledBounds bool // negating for the sake of making false the default
- HighlightOnFocus bool
- NeedsRerenderOnWidthChange bool
+ Kind types.ContextKind
+ Key types.ContextKey
+ View *gocui.View
+ WindowName string
+ Focusable bool
+ Transient bool
+ HasUncontrolledBounds bool // negating for the sake of making false the default
+ HighlightOnFocus bool
+ NeedsRerenderOnWidthChange bool
+ NeedsRerenderOnHeightChange bool
OnGetOptionsMap func() map[string]string
}
@@ -56,18 +58,19 @@ func NewBaseContext(opts NewBaseContextOpts) *BaseContext {
hasControlledBounds := !opts.HasUncontrolledBounds
return &BaseContext{
- kind: opts.Kind,
- key: opts.Key,
- view: opts.View,
- windowName: opts.WindowName,
- onGetOptionsMap: opts.OnGetOptionsMap,
- focusable: opts.Focusable,
- transient: opts.Transient,
- hasControlledBounds: hasControlledBounds,
- highlightOnFocus: opts.HighlightOnFocus,
- needsRerenderOnWidthChange: opts.NeedsRerenderOnWidthChange,
- ParentContextMgr: &ParentContextMgr{},
- viewTrait: viewTrait,
+ kind: opts.Kind,
+ key: opts.Key,
+ view: opts.View,
+ windowName: opts.WindowName,
+ onGetOptionsMap: opts.OnGetOptionsMap,
+ focusable: opts.Focusable,
+ transient: opts.Transient,
+ hasControlledBounds: hasControlledBounds,
+ highlightOnFocus: opts.HighlightOnFocus,
+ needsRerenderOnWidthChange: opts.NeedsRerenderOnWidthChange,
+ needsRerenderOnHeightChange: opts.NeedsRerenderOnHeightChange,
+ ParentContextMgr: &ParentContextMgr{},
+ viewTrait: viewTrait,
}
}
@@ -197,6 +200,10 @@ func (self *BaseContext) NeedsRerenderOnWidthChange() bool {
return self.needsRerenderOnWidthChange
}
+func (self *BaseContext) NeedsRerenderOnHeightChange() bool {
+ return self.needsRerenderOnHeightChange
+}
+
func (self *BaseContext) Title() string {
return ""
}
diff --git a/pkg/gui/context/list_context_trait.go b/pkg/gui/context/list_context_trait.go
index 78d524bb2..aca33cbd0 100644
--- a/pkg/gui/context/list_context_trait.go
+++ b/pkg/gui/context/list_context_trait.go
@@ -18,6 +18,9 @@ type ListContextTrait struct {
// we should find out exactly which lines are now part of the path and refresh those.
// We should also keep track of the previous path and refresh those lines too.
refreshViewportOnChange bool
+ // If this is true, we only render the visible lines of the list. Useful for lists that can
+ // get very long, because it can save a lot of memory
+ renderOnlyVisibleLines bool
}
func (self *ListContextTrait) IsListContext() {}
@@ -28,6 +31,8 @@ func (self *ListContextTrait) FocusLine() {
// the view could be squashed and won't how to adjust the cursor/origin.
// Also, refreshing the viewport needs to happen after the view has been resized.
self.c.AfterLayout(func() error {
+ oldOrigin, _ := self.GetViewTrait().ViewPortYBounds()
+
self.GetViewTrait().FocusPoint(
self.ModelIndexToViewIndex(self.list.GetSelectedLineIdx()))
@@ -41,6 +46,11 @@ func (self *ListContextTrait) FocusLine() {
if self.refreshViewportOnChange {
self.refreshViewport()
+ } else if self.renderOnlyVisibleLines {
+ newOrigin, _ := self.GetViewTrait().ViewPortYBounds()
+ if oldOrigin != newOrigin {
+ return self.HandleRender()
+ }
}
return nil
})
@@ -83,8 +93,21 @@ func (self *ListContextTrait) HandleFocusLost(opts types.OnFocusLostOpts) error
// OnFocus assumes that the content of the context has already been rendered to the view. OnRender is the function which actually renders the content to the view
func (self *ListContextTrait) HandleRender() error {
self.list.ClampSelection()
- content := self.renderLines(-1, -1)
- self.GetViewTrait().SetContent(content)
+ if self.renderOnlyVisibleLines {
+ // Rendering only the visible area can save a lot of cell memory for
+ // those views that support it.
+ totalLength := self.list.Len()
+ if self.getNonModelItems != nil {
+ totalLength += len(self.getNonModelItems())
+ }
+ self.GetViewTrait().SetContentLineCount(totalLength)
+ startIdx, length := self.GetViewTrait().ViewPortYBounds()
+ content := self.renderLines(startIdx, startIdx+length)
+ self.GetViewTrait().SetViewPortContentAndClearEverythingElse(content)
+ } else {
+ content := self.renderLines(-1, -1)
+ self.GetViewTrait().SetContent(content)
+ }
self.c.Render()
self.setFooter()
diff --git a/pkg/gui/context/local_commits_context.go b/pkg/gui/context/local_commits_context.go
index c02fa3fe6..ab42cfb70 100644
--- a/pkg/gui/context/local_commits_context.go
+++ b/pkg/gui/context/local_commits_context.go
@@ -72,12 +72,13 @@ func NewLocalCommitsContext(c *ContextCommon) *LocalCommitsContext {
SearchTrait: NewSearchTrait(c),
ListContextTrait: &ListContextTrait{
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
- View: c.Views().Commits,
- WindowName: "commits",
- Key: LOCAL_COMMITS_CONTEXT_KEY,
- Kind: types.SIDE_CONTEXT,
- Focusable: true,
- NeedsRerenderOnWidthChange: true,
+ View: c.Views().Commits,
+ WindowName: "commits",
+ Key: LOCAL_COMMITS_CONTEXT_KEY,
+ Kind: types.SIDE_CONTEXT,
+ Focusable: true,
+ NeedsRerenderOnWidthChange: true,
+ NeedsRerenderOnHeightChange: true,
})),
ListRenderer: ListRenderer{
list: viewModel,
@@ -85,6 +86,7 @@ func NewLocalCommitsContext(c *ContextCommon) *LocalCommitsContext {
},
c: c,
refreshViewportOnChange: true,
+ renderOnlyVisibleLines: true,
},
}
diff --git a/pkg/gui/context/remote_branches_context.go b/pkg/gui/context/remote_branches_context.go
index 884d3debb..fff80e076 100644
--- a/pkg/gui/context/remote_branches_context.go
+++ b/pkg/gui/context/remote_branches_context.go
@@ -37,13 +37,14 @@ func NewRemoteBranchesContext(
DynamicTitleBuilder: NewDynamicTitleBuilder(c.Tr.RemoteBranchesDynamicTitle),
ListContextTrait: &ListContextTrait{
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
- View: c.Views().RemoteBranches,
- WindowName: "branches",
- Key: REMOTE_BRANCHES_CONTEXT_KEY,
- Kind: types.SIDE_CONTEXT,
- Focusable: true,
- Transient: true,
- NeedsRerenderOnWidthChange: true,
+ View: c.Views().RemoteBranches,
+ WindowName: "branches",
+ Key: REMOTE_BRANCHES_CONTEXT_KEY,
+ Kind: types.SIDE_CONTEXT,
+ Focusable: true,
+ Transient: true,
+ NeedsRerenderOnWidthChange: true,
+ NeedsRerenderOnHeightChange: true,
})),
ListRenderer: ListRenderer{
list: viewModel,
diff --git a/pkg/gui/context/sub_commits_context.go b/pkg/gui/context/sub_commits_context.go
index 842b364b3..ab0d2784a 100644
--- a/pkg/gui/context/sub_commits_context.go
+++ b/pkg/gui/context/sub_commits_context.go
@@ -115,13 +115,14 @@ func NewSubCommitsContext(
DynamicTitleBuilder: NewDynamicTitleBuilder(c.Tr.SubCommitsDynamicTitle),
ListContextTrait: &ListContextTrait{
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
- View: c.Views().SubCommits,
- WindowName: "branches",
- Key: SUB_COMMITS_CONTEXT_KEY,
- Kind: types.SIDE_CONTEXT,
- Focusable: true,
- Transient: true,
- NeedsRerenderOnWidthChange: true,
+ View: c.Views().SubCommits,
+ WindowName: "branches",
+ Key: SUB_COMMITS_CONTEXT_KEY,
+ Kind: types.SIDE_CONTEXT,
+ Focusable: true,
+ Transient: true,
+ NeedsRerenderOnWidthChange: true,
+ NeedsRerenderOnHeightChange: true,
})),
ListRenderer: ListRenderer{
list: viewModel,
@@ -130,6 +131,7 @@ func NewSubCommitsContext(
},
c: c,
refreshViewportOnChange: true,
+ renderOnlyVisibleLines: true,
},
}
diff --git a/pkg/gui/context/view_trait.go b/pkg/gui/context/view_trait.go
index 1179a8b14..191419897 100644
--- a/pkg/gui/context/view_trait.go
+++ b/pkg/gui/context/view_trait.go
@@ -34,6 +34,15 @@ func (self *ViewTrait) SetViewPortContent(content string) {
self.view.OverwriteLines(y, content)
}
+func (self *ViewTrait) SetViewPortContentAndClearEverythingElse(content string) {
+ _, y := self.view.Origin()
+ self.view.OverwriteLinesAndClearEverythingElse(y, content)
+}
+
+func (self *ViewTrait) SetContentLineCount(lineCount int) {
+ self.view.SetContentLineCount(lineCount)
+}
+
func (self *ViewTrait) SetContent(content string) {
self.view.SetContent(content)
}
diff --git a/pkg/gui/layout.go b/pkg/gui/layout.go
index 01c397628..4e2b49477 100644
--- a/pkg/gui/layout.go
+++ b/pkg/gui/layout.go
@@ -72,14 +72,26 @@ func (gui *Gui) layout(g *gocui.Gui) error {
frameOffset = 0
}
+ mustRerender := false
if context.NeedsRerenderOnWidthChange() {
// view.Width() returns the width -1 for some reason
oldWidth := view.Width() + 1
newWidth := dimensionsObj.X1 - dimensionsObj.X0 + 2*frameOffset
if oldWidth != newWidth {
- contextsToRerender = append(contextsToRerender, context)
+ mustRerender = true
}
}
+ if context.NeedsRerenderOnHeightChange() {
+ // view.Height() returns the height -1 for some reason
+ oldHeight := view.Height() + 1
+ newHeight := dimensionsObj.Y1 - dimensionsObj.Y0 + 2*frameOffset
+ if oldHeight != newHeight {
+ mustRerender = true
+ }
+ }
+ if mustRerender {
+ contextsToRerender = append(contextsToRerender, context)
+ }
_, err = g.SetView(
viewName,
diff --git a/pkg/gui/types/context.go b/pkg/gui/types/context.go
index b0e312c97..29b53b9eb 100644
--- a/pkg/gui/types/context.go
+++ b/pkg/gui/types/context.go
@@ -63,6 +63,9 @@ type IBaseContext interface {
// true if the view needs to be rerendered when its width changes
NeedsRerenderOnWidthChange() bool
+ // true if the view needs to be rerendered when its height changes
+ NeedsRerenderOnHeightChange() bool
+
// returns the desired title for the view upon activation. If there is no desired title (returns empty string), then
// no title will be set
Title() string
@@ -172,6 +175,8 @@ type IViewTrait interface {
SetRangeSelectStart(yIdx int)
CancelRangeSelect()
SetViewPortContent(content string)
+ SetViewPortContentAndClearEverythingElse(content string)
+ SetContentLineCount(lineCount int)
SetContent(content string)
SetFooter(value string)
SetOriginX(value int)
diff --git a/pkg/integration/components/view_driver.go b/pkg/integration/components/view_driver.go
index b6f917603..3abd63a9e 100644
--- a/pkg/integration/components/view_driver.go
+++ b/pkg/integration/components/view_driver.go
@@ -431,6 +431,11 @@ func (self *ViewDriver) SelectPreviousItem() *ViewDriver {
return self.PressFast(self.t.keys.Universal.PrevItem)
}
+// i.e. pressing '<'
+func (self *ViewDriver) GotoTop() *ViewDriver {
+ return self.PressFast(self.t.keys.Universal.GotoTop)
+}
+
// i.e. pressing space
func (self *ViewDriver) PressPrimaryAction() *ViewDriver {
return self.Press(self.t.keys.Universal.Select)
@@ -457,21 +462,15 @@ func (self *ViewDriver) PressEscape() *ViewDriver {
// - the user is not in a list item
// - no list item is found containing the given text
// - multiple list items are found containing the given text in the initial page of items
-//
-// NOTE: this currently assumes that BufferLines returns all the lines that can be accessed.
-// If this changes in future, we'll need to update this code to first attempt to find the item
-// in the current page and failing that, jump to the top of the view and iterate through all of it,
-// looking for the item.
func (self *ViewDriver) NavigateToLine(matcher *TextMatcher) *ViewDriver {
self.IsFocused()
view := self.getView()
lines := view.BufferLines()
- var matchIndex int
+ matchIndex := -1
self.t.assertWithRetries(func() (bool, string) {
- matchIndex = -1
var matches []string
// first we look for a duplicate on the current screen. We won't bother looking beyond that though.
for i, line := range lines {
@@ -483,13 +482,19 @@ func (self *ViewDriver) NavigateToLine(matcher *TextMatcher) *ViewDriver {
}
if len(matches) > 1 {
return false, fmt.Sprintf("Found %d matches for `%s`, expected only a single match. Matching lines:\n%s", len(matches), matcher.name(), strings.Join(matches, "\n"))
- } else if len(matches) == 0 {
- return false, fmt.Sprintf("Could not find item matching: %s. Lines:\n%s", matcher.name(), strings.Join(lines, "\n"))
- } else {
- return true, ""
}
+ return true, ""
})
+ // If no match was found, it could be that this is a view that renders only
+ // the visible lines. In that case, we jump to the top and then press
+ // down-arrow until we found the match. We simply return the first match we
+ // find, so we have no way to assert that there are no duplicates.
+ if matchIndex == -1 {
+ self.GotoTop()
+ matchIndex = len(lines)
+ }
+
selectedLineIdx := self.getSelectedLineIdx()
if selectedLineIdx == matchIndex {
return self.SelectedLine(matcher)
@@ -514,12 +519,14 @@ func (self *ViewDriver) NavigateToLine(matcher *TextMatcher) *ViewDriver {
for i := 0; i < maxNumKeyPresses; i++ {
keyPress()
idx := self.getSelectedLineIdx()
- if ok, _ := matcher.test(lines[idx]); ok {
+ // It is important to use view.BufferLines() here and not lines, because it
+ // could change with every keypress.
+ if ok, _ := matcher.test(view.BufferLines()[idx]); ok {
return self
}
}
- self.t.fail(fmt.Sprintf("Could not navigate to item matching: %s. Lines:\n%s", matcher.name(), strings.Join(lines, "\n")))
+ self.t.fail(fmt.Sprintf("Could not navigate to item matching: %s. Lines:\n%s", matcher.name(), strings.Join(view.BufferLines(), "\n")))
return self
}