diff options
author | Stefan Haller <stefan@haller-berlin.de> | 2023-08-09 18:34:43 +0200 |
---|---|---|
committer | Stefan Haller <stefan@haller-berlin.de> | 2023-08-15 11:40:40 +0200 |
commit | 341b9725d4a91471ea4e5e9c515572abd9e66821 (patch) | |
tree | f3de36d9b7acf9d3fb6657f3a2f57195f1870b98 /pkg | |
parent | 8f164f7bc536ca711e531d49f96348fbae54b860 (diff) |
Add ScrollOffMargin user config
When set to a non-zero value, views will scroll when the selection gets this
close to the top or bottom of the view.
Diffstat (limited to 'pkg')
-rw-r--r-- | pkg/config/user_config.go | 2 | ||||
-rw-r--r-- | pkg/gui/controllers/list_controller.go | 6 | ||||
-rw-r--r-- | pkg/gui/controllers/patch_explorer_controller.go | 12 | ||||
-rw-r--r-- | pkg/gui/controllers/scroll_off_margin.go | 70 | ||||
-rw-r--r-- | pkg/gui/controllers/scroll_off_margin_test.go | 171 |
5 files changed, 261 insertions, 0 deletions
diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go index 9eb8e028a..a217d8099 100644 --- a/pkg/config/user_config.go +++ b/pkg/config/user_config.go @@ -31,6 +31,7 @@ type GuiConfig struct { BranchColors map[string]string `yaml:"branchColors"` ScrollHeight int `yaml:"scrollHeight"` ScrollPastBottom bool `yaml:"scrollPastBottom"` + ScrollOffMargin int `yaml:"scrollOffMargin"` MouseEvents bool `yaml:"mouseEvents"` SkipDiscardChangeWarning bool `yaml:"skipDiscardChangeWarning"` SkipStashWarning bool `yaml:"skipStashWarning"` @@ -418,6 +419,7 @@ func GetDefaultConfig() *UserConfig { Gui: GuiConfig{ ScrollHeight: 2, ScrollPastBottom: true, + ScrollOffMargin: 2, MouseEvents: true, SkipDiscardChangeWarning: false, SkipStashWarning: false, diff --git a/pkg/gui/controllers/list_controller.go b/pkg/gui/controllers/list_controller.go index fb6d8736a..cdaea413a 100644 --- a/pkg/gui/controllers/list_controller.go +++ b/pkg/gui/controllers/list_controller.go @@ -82,6 +82,12 @@ func (self *ListController) handleLineChange(change int) error { // doing this check so that if we're holding the up key at the start of the list // we're not constantly re-rendering the main view. if before != after { + if change == -1 { + checkScrollUp(self.context.GetViewTrait(), self.c.UserConfig.Gui.ScrollOffMargin, before, after) + } else if change == 1 { + checkScrollDown(self.context.GetViewTrait(), self.c.UserConfig.Gui.ScrollOffMargin, before, after) + } + return self.context.HandleFocus(types.OnFocusOpts{}) } diff --git a/pkg/gui/controllers/patch_explorer_controller.go b/pkg/gui/controllers/patch_explorer_controller.go index dd19d08db..83c8633a7 100644 --- a/pkg/gui/controllers/patch_explorer_controller.go +++ b/pkg/gui/controllers/patch_explorer_controller.go @@ -159,13 +159,25 @@ func (self *PatchExplorerController) GetMouseKeybindings(opts types.KeybindingsO } func (self *PatchExplorerController) HandlePrevLine() error { + before := self.context.GetState().GetSelectedLineIdx() self.context.GetState().CycleSelection(false) + after := self.context.GetState().GetSelectedLineIdx() + + if self.context.GetState().SelectingLine() { + checkScrollUp(self.context.GetViewTrait(), self.c.UserConfig.Gui.ScrollOffMargin, before, after) + } return nil } func (self *PatchExplorerController) HandleNextLine() error { + before := self.context.GetState().GetSelectedLineIdx() self.context.GetState().CycleSelection(true) + after := self.context.GetState().GetSelectedLineIdx() + + if self.context.GetState().SelectingLine() { + checkScrollDown(self.context.GetViewTrait(), self.c.UserConfig.Gui.ScrollOffMargin, before, after) + } return nil } diff --git a/pkg/gui/controllers/scroll_off_margin.go b/pkg/gui/controllers/scroll_off_margin.go new file mode 100644 index 000000000..119f30090 --- /dev/null +++ b/pkg/gui/controllers/scroll_off_margin.go @@ -0,0 +1,70 @@ +package controllers + +import ( + "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/jesseduffield/lazygit/pkg/utils" +) + +// To be called after pressing up-arrow; checks whether the cursor entered the +// top scroll-off margin, and so the view needs to be scrolled up one line +func checkScrollUp(view types.IViewTrait, scrollOffMargin int, lineIdxBefore int, lineIdxAfter int) { + viewPortStart, viewPortHeight := view.ViewPortYBounds() + + linesToScroll := calculateLinesToScrollUp( + viewPortStart, viewPortHeight, scrollOffMargin, lineIdxBefore, lineIdxAfter) + if linesToScroll != 0 { + view.ScrollUp(linesToScroll) + } +} + +// To be called after pressing down-arrow; checks whether the cursor entered the +// bottom scroll-off margin, and so the view needs to be scrolled down one line +func checkScrollDown(view types.IViewTrait, scrollOffMargin int, lineIdxBefore int, lineIdxAfter int) { + viewPortStart, viewPortHeight := view.ViewPortYBounds() + + linesToScroll := calculateLinesToScrollDown( + viewPortStart, viewPortHeight, scrollOffMargin, lineIdxBefore, lineIdxAfter) + if linesToScroll != 0 { + view.ScrollDown(linesToScroll) + } +} + +func calculateLinesToScrollUp(viewPortStart int, viewPortHeight int, scrollOffMargin int, lineIdxBefore int, lineIdxAfter int) int { + // Cap the margin to half the view height. This allows setting the config to + // a very large value to keep the cursor always in the middle of the screen. + // Use +.5 so that if the height is even, the top margin is one line higher + // than the bottom margin. + scrollOffMargin = utils.Min(scrollOffMargin, int((float64(viewPortHeight)+.5)/2)) + + // Scroll only if the "before" position was visible (this could be false if + // the scroll wheel was used to scroll the selected line out of view) ... + if lineIdxBefore >= viewPortStart && lineIdxBefore < viewPortStart+viewPortHeight { + marginEnd := viewPortStart + scrollOffMargin + // ... and the "after" position is within the top margin (or before it) + if lineIdxAfter < marginEnd { + return marginEnd - lineIdxAfter + } + } + + return 0 +} + +func calculateLinesToScrollDown(viewPortStart int, viewPortHeight int, scrollOffMargin int, lineIdxBefore int, lineIdxAfter int) int { + // Cap the margin to half the view height. This allows setting the config to + // a very large value to keep the cursor always in the middle of the screen. + // Use -.5 so that if the height is even, the bottom margin is one line lower + // than the top margin. + scrollOffMargin = utils.Min(scrollOffMargin, int((float64(viewPortHeight)-.5)/2)) + + // Scroll only if the "before" position was visible (this could be false if + // the scroll wheel was used to scroll the selected line out of view) ... + if lineIdxBefore >= viewPortStart && lineIdxBefore < viewPortStart+viewPortHeight { + marginStart := viewPortStart + viewPortHeight - scrollOffMargin - 1 + // ... and the "after" position is within the bottom margin (or after it) + if lineIdxAfter > marginStart { + return lineIdxAfter - marginStart + } + } + + return 0 +} diff --git a/pkg/gui/controllers/scroll_off_margin_test.go b/pkg/gui/controllers/scroll_off_margin_test.go new file mode 100644 index 000000000..099da299f --- /dev/null +++ b/pkg/gui/controllers/scroll_off_margin_test.go @@ -0,0 +1,171 @@ +package controllers + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_calculateLinesToScrollUp(t *testing.T) { + scenarios := []struct { + name string + viewPortStart int + viewPortHeight int + scrollOffMargin int + lineIdxBefore int + lineIdxAfter int + expectedLinesToScroll int + }{ + { + name: "before position is above viewport - don't scroll", + viewPortStart: 10, + viewPortHeight: 10, + scrollOffMargin: 3, + lineIdxBefore: 9, + lineIdxAfter: 8, + expectedLinesToScroll: 0, + }, + { + name: "before position is below viewport - don't scroll", + viewPortStart: 10, + viewPortHeight: 10, + scrollOffMargin: 3, + lineIdxBefore: 20, + lineIdxAfter: 19, + expectedLinesToScroll: 0, + }, + { + name: "before and after positions are outside scroll-off margin - don't scroll", + viewPortStart: 10, + viewPortHeight: 10, + scrollOffMargin: 3, + lineIdxBefore: 14, + lineIdxAfter: 13, + expectedLinesToScroll: 0, + }, + { + name: "before outside, after inside scroll-off margin - scroll by 1", + viewPortStart: 10, + viewPortHeight: 10, + scrollOffMargin: 3, + lineIdxBefore: 13, + lineIdxAfter: 12, + expectedLinesToScroll: 1, + }, + { + name: "before inside scroll-off margin - scroll by more than 1", + viewPortStart: 10, + viewPortHeight: 10, + scrollOffMargin: 3, + lineIdxBefore: 11, + lineIdxAfter: 10, + expectedLinesToScroll: 3, + }, + { + name: "very large scroll-off margin - keep view centered (even viewport height)", + viewPortStart: 10, + viewPortHeight: 10, + scrollOffMargin: 999, + lineIdxBefore: 15, + lineIdxAfter: 14, + expectedLinesToScroll: 1, + }, + { + name: "very large scroll-off margin - keep view centered (odd viewport height)", + viewPortStart: 10, + viewPortHeight: 9, + scrollOffMargin: 999, + lineIdxBefore: 14, + lineIdxAfter: 13, + expectedLinesToScroll: 1, + }, + } + for _, scenario := range scenarios { + t.Run(scenario.name, func(t *testing.T) { + linesToScroll := calculateLinesToScrollUp(scenario.viewPortStart, scenario.viewPortHeight, scenario.scrollOffMargin, scenario.lineIdxBefore, scenario.lineIdxAfter) + assert.Equal(t, scenario.expectedLinesToScroll, linesToScroll) + }) + } +} + +func Test_calculateLinesToScrollDown(t *testing.T) { + scenarios := []struct { + name string + viewPortStart int + viewPortHeight int + scrollOffMargin int + lineIdxBefore int + lineIdxAfter int + expectedLinesToScroll int + }{ + { + name: "before position is above viewport - don't scroll", + viewPortStart: 10, + viewPortHeight: 10, + scrollOffMargin: 3, + lineIdxBefore: 9, + lineIdxAfter: 10, + expectedLinesToScroll: 0, + }, + { + name: "before position is below viewport - don't scroll", + viewPortStart: 10, + viewPortHeight: 10, + scrollOffMargin: 3, + lineIdxBefore: 20, + lineIdxAfter: 21, + expectedLinesToScroll: 0, + }, + { + name: "before and after positions are outside scroll-off margin - don't scroll", + viewPortStart: 10, + viewPortHeight: 10, + scrollOffMargin: 3, + lineIdxBefore: 15, + lineIdxAfter: 16, + expectedLinesToScroll: 0, + }, + { + name: "before outside, after inside scroll-off margin - scroll by 1", + viewPortStart: 10, + viewPortHeight: 10, + scrollOffMargin: 3, + lineIdxBefore: 16, + lineIdxAfter: 17, + expectedLinesToScroll: 1, + }, + { + name: "before inside scroll-off margin - scroll by more than 1", + viewPortStart: 10, + viewPortHeight: 10, + scrollOffMargin: 3, + lineIdxBefore: 18, + lineIdxAfter: 19, + expectedLinesToScroll: 3, + }, + { + name: "very large scroll-off margin - keep view centered (even viewport height)", + viewPortStart: 10, + viewPortHeight: 10, + scrollOffMargin: 999, + lineIdxBefore: 15, + lineIdxAfter: 16, + expectedLinesToScroll: 1, + }, + { + name: "very large scroll-off margin - keep view centered (odd viewport height)", + viewPortStart: 10, + viewPortHeight: 9, + scrollOffMargin: 999, + lineIdxBefore: 14, + lineIdxAfter: 15, + expectedLinesToScroll: 1, + }, + } + for _, scenario := range scenarios { + t.Run(scenario.name, func(t *testing.T) { + linesToScroll := calculateLinesToScrollDown(scenario.viewPortStart, scenario.viewPortHeight, scenario.scrollOffMargin, scenario.lineIdxBefore, scenario.lineIdxAfter) + assert.Equal(t, scenario.expectedLinesToScroll, linesToScroll) + }) + } +} |