1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141
|
// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source
// code is governed by the MIT license that can be found in the LICENSE
// file.
// Package scrollabletext provides a text widget that can be placed inside
// withscrollbar.Widget
package scrollabletext
import (
"strings"
"github.com/gcla/gowid"
"github.com/gcla/gowid/gwutil"
"github.com/gcla/gowid/widgets/selectable"
"github.com/gcla/gowid/widgets/text"
"github.com/gdamore/tcell/v2"
)
//======================================================================
// Widget constructs a text widget and allows it to be scrolled. But this widget is limited - it assumes no
// line will wrap. To make this happen it ensures that any lines that are too long are clipped. It makes this
// assumption because my scrollbar APIs are not well designed, and functions like ScrollPosition and
// ScrollLength don't understand the current rendering context. That means if the app is resized, and a line
// now takes two screen lines to render and not one, the scrollbar can't be built accurately. Until I design
// a better scrollbar API, this will work - I'm only using it for limited information dialogs at the moment.
type Widget struct {
*selectable.Widget
splitText []string
linesFromTop int // how many lines down we are
cachedLength int
}
var _ gowid.IWidget = (*Widget)(nil)
func New(txt string) *Widget {
splitText := strings.Split(txt, "\n")
res := &Widget{
splitText: splitText,
cachedLength: len(splitText),
}
res.makeText()
return res
}
func (w *Widget) makeText() {
w.Widget = selectable.New(
text.New(
strings.Join(w.splitText[w.linesFromTop:], "\n"),
text.Options{
Wrap: text.WrapClip,
ClipIndicator: "...",
},
),
)
}
func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool {
handled := true
linesFromTop := w.linesFromTop
switch ev := ev.(type) {
case *tcell.EventKey:
switch ev.Key() {
case tcell.KeyPgUp:
w.UpPage(1, size, app)
case tcell.KeyUp, tcell.KeyCtrlP:
w.Up(1, size, app)
case tcell.KeyDown, tcell.KeyCtrlN:
w.Down(1, size, app)
case tcell.KeyPgDn:
w.DownPage(1, size, app)
default:
handled = false
}
}
if handled && linesFromTop == w.linesFromTop {
handled = false
}
if !handled {
handled = w.Widget.UserInput(ev, size, focus, app)
}
return handled
}
// Implement functions for withscrollbar.Widget
func (w *Widget) ScrollPosition() int {
return w.linesFromTop
}
func (w *Widget) ScrollLength() int {
return w.cachedLength
}
func (w *Widget) Up(lines int, size gowid.IRenderSize, app gowid.IApp) {
pos := w.linesFromTop
w.linesFromTop = gwutil.Max(0, w.linesFromTop-lines)
if pos != w.linesFromTop {
w.makeText()
}
}
func (w *Widget) Down(lines int, size gowid.IRenderSize, app gowid.IApp) {
pos := w.linesFromTop
w.linesFromTop = gwutil.Min(w.cachedLength-1, w.linesFromTop+lines)
if pos != w.linesFromTop {
w.makeText()
}
}
func (w *Widget) UpPage(num int, size gowid.IRenderSize, app gowid.IApp) {
pos := w.linesFromTop
pg := 1
if size, ok := size.(gowid.IRows); ok {
pg = size.Rows()
}
w.linesFromTop = gwutil.Max(0, w.linesFromTop-(pg*num))
if pos != w.linesFromTop {
w.makeText()
}
}
func (w *Widget) DownPage(num int, size gowid.IRenderSize, app gowid.IApp) {
pos := w.linesFromTop
pg := 1
if size, ok := size.(gowid.IRows); ok {
pg = size.Rows()
}
w.linesFromTop = gwutil.Min(w.cachedLength-1, w.linesFromTop+(pg*num))
if pos != w.linesFromTop {
w.makeText()
}
}
//======================================================================
// Local Variables:
// mode: Go
// fill-column: 110
// End:
|