termdash/widgets/text/scroll.go

166 lines
5.4 KiB
Go

// Copyright 2018 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package text
// scroll.go contains code that tracks the current scrolling position.
import "math"
// scrollTracker tracks the current scrolling position for the Text widget.
//
// The text widget displays the contained text buffer as lines of text that fit
// the widget's canvas. The main goal of this object is to inform the text
// widget which should be the first drawn line from the buffer. This depends on
// two things, the scrolling position based on user inputs and whether the text
// widget is configured to roll the content up as new text is added by the
// client.
//
// The rolling Vs. scrolling state is tracked in an FSM implemented in this
// file.
//
// The client can scroll the content by either a keyboard or a mouse event. The
// widget receives these events concurrently with requests to redraw the
// content, so this objects keeps a track of all the scrolling events that
// happened since the last redraw and consumes them when calculating which is
// the first drawn line on the next redraw event.
//
// This is not thread safe.
type scrollTracker struct {
// scroll stores user requests to scroll up (negative) or down (positive).
// E.g. -1 means up by one line and 2 means down by two lines.
scroll int
// scrollPage stores user requests to scroll up (negative) or down
// (positive) by a page of content. E.g. -1 means up by one page and 2
// means down by two pages.
scrollPage int
// first tracks the first line that will be printed.
first int
// state is the state of the scrolling FSM.
state rollState
}
// newScrollTracker returns a new scroll tracker.
func newScrollTracker(opts *options) *scrollTracker {
if opts.rollContent {
return &scrollTracker{state: rollToEnd}
}
return &scrollTracker{state: rollingDisabled}
}
// upOneLine processes a user request to scroll up by one line.
func (st *scrollTracker) upOneLine() {
st.scroll--
}
// downOneLine processes a user request to scroll down by one line.
func (st *scrollTracker) downOneLine() {
st.scroll++
}
// upOnePage processes a user request to scroll up by one page.
func (st *scrollTracker) upOnePage() {
st.scrollPage--
}
// downOnePage processes a user request to scroll down by one page.
func (st *scrollTracker) downOnePage() {
st.scrollPage++
}
// doScroll processes any outstanding scroll requests and calculates the
// resulting first line.
func (st *scrollTracker) doScroll(lines, height int) int {
first := st.first + st.scroll + st.scrollPage*height
st.scroll = 0
st.scrollPage = 0
return normalizeScroll(first, lines, height)
}
// firstLine returns the number of the first line that should be drawn on a
// canvas of the specified height if there is the provided number of lines of
// text.
func (st *scrollTracker) firstLine(lines, height int) int {
// Execute the scrolling FSM.
st.state = st.state(st, lines, height)
return st.first
}
// rollState is a state in the scrolling FSM.
type rollState func(st *scrollTracker, lines, height int) rollState
// rollingDisabled is a state where content rolling was disabled by the
// configuration of the Text widget.
func rollingDisabled(st *scrollTracker, lines, height int) rollState {
st.first = st.doScroll(lines, height)
return rollingDisabled
}
// rollToEnd is a state in which the last line of the content is always
// visible. When new content arrives, it is rolled upwards.
func rollToEnd(st *scrollTracker, lines, height int) rollState {
// If the user didn't scroll, just roll the content so that the last line
// is visible.
if st.scroll == 0 && st.scrollPage == 0 {
st.first = normalizeScroll(math.MaxUint32, lines, height)
return rollToEnd
}
st.first = st.doScroll(lines, height)
if lastLineVisible(st.first, lines, height) {
return rollToEnd
}
return rollingPaused
}
// rollingPaused is a state in which the user scrolled up and made the last
// line scroll out of the view, so the content rolling is paused.
func rollingPaused(st *scrollTracker, lines, height int) rollState {
st.first = st.doScroll(lines, height)
if lastLineVisible(st.first, lines, height) {
return rollToEnd
}
return rollingPaused
}
// lastLineVisible returns true if the last text line from within the buffer of
// the text widget is visible on the canvas when drawing of the text starts
// from the specified start line, there is the provided total amount of lines
// and the canvas has the height.
func lastLineVisible(start, lines, height int) bool {
return lines-start <= height
}
// normalizeScroll returns normalized position of the first line that should be
// drawn when drawing the specified number of lines on a canvas with the
// provided height.
func normalizeScroll(first, lines, height int) int {
if first < 0 || lines <= 0 || height <= 0 {
return 0
}
if lines <= height {
return 0 // Scrolling not necessary if the content fits.
}
max := lines - height
if first > max {
return max
}
return first
}