ScrollLayout added and various improvements
This commit is contained in:
parent
c4a3aa05f9
commit
a20d361871
@ -9,6 +9,7 @@ func drawBuffer(scr tcell.Screen, buf *ViewBuffer) {
|
|||||||
// scr.SetContent(x, y, rn.Rn, nil, rn.Style)
|
// scr.SetContent(x, y, rn.Rn, nil, rn.Style)
|
||||||
// })
|
// })
|
||||||
|
|
||||||
|
// TODO use runewidth.RuneWidth(rn)?
|
||||||
buf.ForEachLine(func(y int, content []Rune) {
|
buf.ForEachLine(func(y int, content []Rune) {
|
||||||
for x := 0; x < buf.Width(); x++ {
|
for x := 0; x < buf.Width(); x++ {
|
||||||
rn := content[x]
|
rn := content[x]
|
||||||
|
@ -2,7 +2,11 @@ package tui
|
|||||||
|
|
||||||
type Events interface {
|
type Events interface {
|
||||||
|
|
||||||
// KeyPressed is called every time a key or key-combination is pressed.
|
// OnKeyPressed is called every time a key or key-combination is pressed.
|
||||||
// If KeyPressed returns true, the event will not be passed onto child views
|
// If OnKeyPressed returns true, the event will not be passed onto child views
|
||||||
OnKeyPressed(event *KeyEvent) (consumed bool)
|
OnKeyPressed(event *KeyEvent) (consumed bool)
|
||||||
|
|
||||||
|
// OnMouseClicked is called every time a mouse button was pressed on the view.
|
||||||
|
// If OnMouseClicked returns true, the event will not be passed onto child views
|
||||||
|
OnMouseClicked(event *MouseEvent) (consumed bool)
|
||||||
}
|
}
|
||||||
|
2
go.mod
2
go.mod
@ -3,7 +3,7 @@ module git.tordarus.net/Tordarus/tui
|
|||||||
go 1.18
|
go 1.18
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.tordarus.net/Tordarus/buf2d v1.1.1
|
git.tordarus.net/Tordarus/buf2d v1.1.2
|
||||||
github.com/gdamore/tcell v1.4.0
|
github.com/gdamore/tcell v1.4.0
|
||||||
golang.org/x/text v0.3.7
|
golang.org/x/text v0.3.7
|
||||||
)
|
)
|
||||||
|
4
go.sum
4
go.sum
@ -1,5 +1,5 @@
|
|||||||
git.tordarus.net/Tordarus/buf2d v1.1.1 h1:rYvQ2YveqogCoKy5andQxuORPusWbUhpnqJhzVkTlRs=
|
git.tordarus.net/Tordarus/buf2d v1.1.2 h1:mmK3tARa30gCh4WaYoSEF5e7qk0C+1ODhxerUcfXN5M=
|
||||||
git.tordarus.net/Tordarus/buf2d v1.1.1/go.mod h1:XXPpS8nQK0gUI0ki7ftV/qlprsGCRWFVSD4ybvDuUL8=
|
git.tordarus.net/Tordarus/buf2d v1.1.2/go.mod h1:XXPpS8nQK0gUI0ki7ftV/qlprsGCRWFVSD4ybvDuUL8=
|
||||||
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
|
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
|
||||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||||
github.com/gdamore/tcell v1.4.0 h1:vUnHwJRvcPQa3tzi+0QI4U9JINXYJlOz9yiaiPQ2wMU=
|
github.com/gdamore/tcell v1.4.0 h1:vUnHwJRvcPQa3tzi+0QI4U9JINXYJlOz9yiaiPQ2wMU=
|
||||||
|
23
screen.go
23
screen.go
@ -16,6 +16,9 @@ type Screen struct {
|
|||||||
|
|
||||||
// KeyPressed is called every time a key or key-combination is pressed.
|
// KeyPressed is called every time a key or key-combination is pressed.
|
||||||
KeyPressed func(event *KeyEvent) (consumed bool)
|
KeyPressed func(event *KeyEvent) (consumed bool)
|
||||||
|
|
||||||
|
// MouseClicked is called every time a mouse button was pressed.
|
||||||
|
MouseClicked func(event *MouseEvent) (consumed bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewScreen(root View) (*Screen, error) {
|
func NewScreen(root View) (*Screen, error) {
|
||||||
@ -42,6 +45,8 @@ func (s *Screen) eventloop() {
|
|||||||
go s.Redraw()
|
go s.Redraw()
|
||||||
case *tcell.EventKey:
|
case *tcell.EventKey:
|
||||||
go s.onKeyPressed(event)
|
go s.onKeyPressed(event)
|
||||||
|
case *tcell.EventMouse:
|
||||||
|
go s.onMouseClicked(convertMouseEvent(event))
|
||||||
default:
|
default:
|
||||||
s.StopWithError(errors.New(fmt.Sprintf("%#v", event)))
|
s.StopWithError(errors.New(fmt.Sprintf("%#v", event)))
|
||||||
}
|
}
|
||||||
@ -56,7 +61,7 @@ func (s *Screen) Start() error {
|
|||||||
}
|
}
|
||||||
defer s.scr.Fini()
|
defer s.scr.Fini()
|
||||||
|
|
||||||
s.Redraw()
|
s.scr.EnableMouse()
|
||||||
return <-s.stopCh
|
return <-s.stopCh
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,6 +80,22 @@ func (s *Screen) onKeyPressed(event *KeyEvent) {
|
|||||||
s.Redraw()
|
s.Redraw()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Screen) onMouseClicked(event *MouseEvent) {
|
||||||
|
if s.MouseClicked == nil || !s.MouseClicked(event) {
|
||||||
|
s.Root.OnMouseClicked(event)
|
||||||
|
}
|
||||||
|
s.Redraw()
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertMouseEvent(original *tcell.EventMouse) *MouseEvent {
|
||||||
|
x, y := original.Position()
|
||||||
|
return &MouseEvent{
|
||||||
|
X: x, Y: y,
|
||||||
|
Button: convertMouseButton(original.Buttons()),
|
||||||
|
Modifiers: original.Modifiers(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Screen) Redraw() {
|
func (s *Screen) Redraw() {
|
||||||
w, h := s.scr.Size()
|
w, h := s.scr.Size()
|
||||||
|
|
||||||
|
@ -3,6 +3,10 @@ package tui
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"math/rand"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.tordarus.net/Tordarus/tui"
|
"git.tordarus.net/Tordarus/tui"
|
||||||
@ -10,6 +14,72 @@ import (
|
|||||||
"github.com/gdamore/tcell"
|
"github.com/gdamore/tcell"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var logger *log.Logger
|
||||||
|
|
||||||
|
func initDebugLogger() func() {
|
||||||
|
out, err := os.Create("output.log")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
logger = log.New(out, "", log.LstdFlags|log.Lmicroseconds)
|
||||||
|
|
||||||
|
return func() {
|
||||||
|
out.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScrollView(t *testing.T) {
|
||||||
|
//defer initDebugLogger()()
|
||||||
|
|
||||||
|
textViews := make([]tui.View, 0, 50)
|
||||||
|
|
||||||
|
for i := 0; i < cap(textViews); i++ {
|
||||||
|
textViews = append(textViews, views.NewTextView(strconv.Itoa(i)))
|
||||||
|
textViews[i].SetStyle(textViews[i].Style().Foreground(tcell.ColorBlack).Background(tcell.Color(rand.Intn(int(tcell.ColorYellowGreen)))))
|
||||||
|
}
|
||||||
|
|
||||||
|
flowLayout := views.NewFlowLayout(tui.Vertical)
|
||||||
|
flowLayout.AppendViews(textViews...)
|
||||||
|
|
||||||
|
scrollView := views.NewScrollView(flowLayout)
|
||||||
|
|
||||||
|
screen, err := tui.NewScreen(scrollView)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
screen.KeyPressed = func(event *tui.KeyEvent) (consumed bool) {
|
||||||
|
switch event.Key() {
|
||||||
|
case tcell.KeyCtrlC:
|
||||||
|
screen.StopWithError(errors.New(fmt.Sprintf("key: %#v | rune: %s", event.Key(), string(event.Rune()))))
|
||||||
|
case tcell.KeyPgDn:
|
||||||
|
scrollView.Scroll(10, 0)
|
||||||
|
case tcell.KeyPgUp:
|
||||||
|
scrollView.Scroll(-10, 0)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
screen.MouseClicked = func(event *tui.MouseEvent) (consumed bool) {
|
||||||
|
//textViews[0].(*views.TextView).Text = fmt.Sprintf("mouse position: %d | %d", event.X, event.Y)
|
||||||
|
//textViews[1].(*views.TextView).Text = fmt.Sprintf("mouse button: %d", event.Button)
|
||||||
|
|
||||||
|
if event.Button == tui.MouseWheelUp {
|
||||||
|
scrollView.Scroll(-1, 0)
|
||||||
|
return true
|
||||||
|
} else if event.Button == tui.MouseWheelDown {
|
||||||
|
scrollView.Scroll(1, 0)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
err = screen.Start()
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
|
||||||
func TestFlowLayout(t *testing.T) {
|
func TestFlowLayout(t *testing.T) {
|
||||||
textView := views.NewTextView("hello world!")
|
textView := views.NewTextView("hello world!")
|
||||||
textView.SetStyle(tui.StyleDefault.Background(tcell.ColorRed).Foreground(tcell.ColorBlack))
|
textView.SetStyle(tui.StyleDefault.Background(tcell.ColorRed).Foreground(tcell.ColorBlack))
|
||||||
|
46
types.go
46
types.go
@ -54,3 +54,49 @@ const (
|
|||||||
AnchorBottom
|
AnchorBottom
|
||||||
AnchorBottomRight
|
AnchorBottomRight
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type MouseEvent struct {
|
||||||
|
X, Y int
|
||||||
|
Button MouseButton
|
||||||
|
Modifiers tcell.ModMask
|
||||||
|
}
|
||||||
|
|
||||||
|
type MouseButton uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
MouseButtonNone MouseButton = iota
|
||||||
|
MouseButtonLeft
|
||||||
|
MouseButtonMiddle
|
||||||
|
MouseButtonRight
|
||||||
|
MouseButtonNext
|
||||||
|
MouseButtonPrev
|
||||||
|
|
||||||
|
MouseWheelUp
|
||||||
|
MouseWheelDown
|
||||||
|
MouseWheelLeft
|
||||||
|
MouseWheelRight
|
||||||
|
)
|
||||||
|
|
||||||
|
func convertMouseButton(mask tcell.ButtonMask) MouseButton {
|
||||||
|
if mask&tcell.Button1 == tcell.Button1 {
|
||||||
|
return MouseButtonLeft
|
||||||
|
} else if mask&tcell.Button2 == tcell.Button2 {
|
||||||
|
return MouseButtonMiddle
|
||||||
|
} else if mask&tcell.Button3 == tcell.Button3 {
|
||||||
|
return MouseButtonRight
|
||||||
|
} else if mask&tcell.Button4 == tcell.Button4 {
|
||||||
|
return MouseButtonNext
|
||||||
|
} else if mask&tcell.Button5 == tcell.Button5 {
|
||||||
|
return MouseButtonPrev
|
||||||
|
} else if mask&tcell.WheelUp == tcell.WheelUp {
|
||||||
|
return MouseWheelUp
|
||||||
|
} else if mask&tcell.WheelDown == tcell.WheelDown {
|
||||||
|
return MouseWheelDown
|
||||||
|
} else if mask&tcell.WheelLeft == tcell.WheelLeft {
|
||||||
|
return MouseWheelLeft
|
||||||
|
} else if mask&tcell.WheelRight == tcell.WheelRight {
|
||||||
|
return MouseWheelRight
|
||||||
|
}
|
||||||
|
|
||||||
|
return MouseButtonNone
|
||||||
|
}
|
||||||
|
19
utils.go
19
utils.go
@ -3,7 +3,7 @@ package tui
|
|||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"golang.org/x/text/width"
|
"github.com/mattn/go-runewidth"
|
||||||
)
|
)
|
||||||
|
|
||||||
// WriteString writes a whole string to the buffer at position (x,y)
|
// WriteString writes a whole string to the buffer at position (x,y)
|
||||||
@ -56,15 +56,16 @@ func MeasureMultiLineString(str string) (maxLineWidth, lineCount int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func runeWidth(r rune) int {
|
func runeWidth(r rune) int {
|
||||||
|
return runewidth.RuneWidth(r)
|
||||||
//fmt.Println(r, width.LookupRune(r).Kind())
|
//fmt.Println(r, width.LookupRune(r).Kind())
|
||||||
switch width.LookupRune(r).Kind() {
|
// switch width.LookupRune(r).Kind() {
|
||||||
case width.EastAsianFullwidth:
|
// case width.EastAsianFullwidth:
|
||||||
fallthrough
|
// fallthrough
|
||||||
case width.EastAsianWide:
|
// case width.EastAsianWide:
|
||||||
return 2
|
// return 2
|
||||||
default:
|
// default:
|
||||||
return 1
|
// return 1
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
func min(x, y int) int {
|
func min(x, y int) int {
|
||||||
|
@ -58,6 +58,9 @@ func (g *FlowLayout) Draw(buf *tui.ViewBuffer) {
|
|||||||
|
|
||||||
if g.Orientation == tui.Horizontal {
|
if g.Orientation == tui.Horizontal {
|
||||||
remainingSpacePerView := buf.Width() - layout.Sum.Width
|
remainingSpacePerView := buf.Width() - layout.Sum.Width
|
||||||
|
if remainingSpacePerView < 0 {
|
||||||
|
remainingSpacePerView = 0
|
||||||
|
}
|
||||||
if layout.HorizontalNegativeCount > 0 {
|
if layout.HorizontalNegativeCount > 0 {
|
||||||
remainingSpacePerView /= layout.HorizontalNegativeCount
|
remainingSpacePerView /= layout.HorizontalNegativeCount
|
||||||
}
|
}
|
||||||
@ -72,10 +75,17 @@ func (g *FlowLayout) Draw(buf *tui.ViewBuffer) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
view.Draw(buf.Sub(x, 0, size.Width, size.Height))
|
view.Draw(buf.Sub(x, 0, size.Width, size.Height))
|
||||||
|
|
||||||
x += size.Width
|
x += size.Width
|
||||||
|
if x >= buf.Width() {
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if g.Orientation == tui.Vertical {
|
} else if g.Orientation == tui.Vertical {
|
||||||
remainingSpacePerView := buf.Height() - layout.Sum.Height
|
remainingSpacePerView := buf.Height() - layout.Sum.Height
|
||||||
|
if remainingSpacePerView < 0 {
|
||||||
|
remainingSpacePerView = 0
|
||||||
|
}
|
||||||
if layout.VerticalNegativeCount > 0 {
|
if layout.VerticalNegativeCount > 0 {
|
||||||
remainingSpacePerView /= layout.VerticalNegativeCount
|
remainingSpacePerView /= layout.VerticalNegativeCount
|
||||||
}
|
}
|
||||||
@ -90,7 +100,11 @@ func (g *FlowLayout) Draw(buf *tui.ViewBuffer) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
view.Draw(buf.Sub(0, y, size.Width, size.Height))
|
view.Draw(buf.Sub(0, y, size.Width, size.Height))
|
||||||
|
|
||||||
y += size.Height
|
y += size.Height
|
||||||
|
if y >= buf.Height() {
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,7 +120,7 @@ func (g *FlowLayout) Layout() (prefWidth, prefHeight int) {
|
|||||||
prefHeight = iff(layout.VerticalNegativeCount == 0, layout.Max.Height, -1)
|
prefHeight = iff(layout.VerticalNegativeCount == 0, layout.Max.Height, -1)
|
||||||
} else if g.Orientation == tui.Vertical {
|
} else if g.Orientation == tui.Vertical {
|
||||||
prefWidth = iff(layout.HorizontalNegativeCount == 0, layout.Max.Width, -1)
|
prefWidth = iff(layout.HorizontalNegativeCount == 0, layout.Max.Width, -1)
|
||||||
prefHeight = iff(layout.VerticalNegativeCount == 0, layout.Sum.Width, -1)
|
prefHeight = iff(layout.VerticalNegativeCount == 0, layout.Sum.Height, -1)
|
||||||
}
|
}
|
||||||
|
|
||||||
layout.Pref = tui.Size{Width: prefWidth, Height: prefHeight}
|
layout.Pref = tui.Size{Width: prefWidth, Height: prefHeight}
|
||||||
|
@ -20,6 +20,10 @@ func max(x, y int) int {
|
|||||||
return y
|
return y
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func limit(v, minv, maxv int) int {
|
||||||
|
return min(max(v, minv), maxv)
|
||||||
|
}
|
||||||
|
|
||||||
func iff[T any](condition bool, trueValue, falseValue T) T {
|
func iff[T any](condition bool, trueValue, falseValue T) T {
|
||||||
if condition {
|
if condition {
|
||||||
return trueValue
|
return trueValue
|
||||||
|
@ -10,7 +10,7 @@ type BorderView struct {
|
|||||||
Border BorderBox
|
Border BorderBox
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ tui.View = &BorderView{}
|
var _ tui.Wrapper = &BorderView{}
|
||||||
|
|
||||||
func NewBorderView(view tui.View) *BorderView {
|
func NewBorderView(view tui.View) *BorderView {
|
||||||
v := new(BorderView)
|
v := new(BorderView)
|
||||||
@ -46,10 +46,6 @@ func (v *BorderView) Layout() (prefWidth, prefHeight int) {
|
|||||||
return w, h
|
return w, h
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *BorderView) Style() tui.Style {
|
|
||||||
return v.ViewTmpl.Style()
|
|
||||||
}
|
|
||||||
|
|
||||||
type BorderBox struct {
|
type BorderBox struct {
|
||||||
TopLeft rune
|
TopLeft rune
|
||||||
TopRight rune
|
TopRight rune
|
||||||
|
@ -11,7 +11,7 @@ type ConstrainView struct {
|
|||||||
MaxHeight int
|
MaxHeight int
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ tui.View = &ConstrainView{}
|
var _ tui.Wrapper = &ConstrainView{}
|
||||||
|
|
||||||
func NewConstrainView(view tui.View) *ConstrainView {
|
func NewConstrainView(view tui.View) *ConstrainView {
|
||||||
v := new(ConstrainView)
|
v := new(ConstrainView)
|
||||||
|
@ -8,7 +8,7 @@ type FrameView struct {
|
|||||||
Anchor tui.Anchor
|
Anchor tui.Anchor
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ tui.View = &FrameView{}
|
var _ tui.Wrapper = &FrameView{}
|
||||||
|
|
||||||
func NewFrameView(view tui.View) *FrameView {
|
func NewFrameView(view tui.View) *FrameView {
|
||||||
v := new(FrameView)
|
v := new(FrameView)
|
||||||
@ -29,7 +29,3 @@ func (g *FrameView) Draw(buf *tui.ViewBuffer) {
|
|||||||
func (v *FrameView) Layout() (prefWidth, prefHeight int) {
|
func (v *FrameView) Layout() (prefWidth, prefHeight int) {
|
||||||
return -1, -1
|
return -1, -1
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *FrameView) Style() tui.Style {
|
|
||||||
return v.ViewTmpl.Style()
|
|
||||||
}
|
|
||||||
|
@ -8,7 +8,7 @@ type MarginView struct {
|
|||||||
Margin map[tui.Side]int
|
Margin map[tui.Side]int
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ tui.View = &MarginView{}
|
var _ tui.Wrapper = &MarginView{}
|
||||||
|
|
||||||
func NewMarginView(view tui.View) *MarginView {
|
func NewMarginView(view tui.View) *MarginView {
|
||||||
v := new(MarginView)
|
v := new(MarginView)
|
||||||
@ -34,10 +34,6 @@ func (v *MarginView) Layout() (prefWidth, prefHeight int) {
|
|||||||
return w, h
|
return w, h
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *MarginView) Style() tui.Style {
|
|
||||||
return v.ViewTmpl.Style()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *MarginView) SetMargin(top, right, bottom, left int) {
|
func (v *MarginView) SetMargin(top, right, bottom, left int) {
|
||||||
v.Margin = map[tui.Side]int{
|
v.Margin = map[tui.Side]int{
|
||||||
tui.SideTop: top,
|
tui.SideTop: top,
|
||||||
|
140
views/view_scroll.go
Normal file
140
views/view_scroll.go
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
package views
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"git.tordarus.net/Tordarus/buf2d"
|
||||||
|
"git.tordarus.net/Tordarus/tui"
|
||||||
|
"github.com/gdamore/tcell"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ScrollView is a tui.View which can hold an arbitrary large view.
|
||||||
|
// If the sub view does not fit into its bounds, scroll bars will be shown
|
||||||
|
type ScrollView struct {
|
||||||
|
tui.WrapperTmpl
|
||||||
|
buf *tui.ViewBuffer
|
||||||
|
|
||||||
|
width, height int
|
||||||
|
|
||||||
|
verticalScrollOffset int
|
||||||
|
horizontalScrollOffset int
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ tui.Wrapper = &ScrollView{}
|
||||||
|
|
||||||
|
func NewScrollView(view tui.View) *ScrollView {
|
||||||
|
v := new(ScrollView)
|
||||||
|
v.SetView(view)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *ScrollView) Draw(buf *tui.ViewBuffer) {
|
||||||
|
w, h := v.View().Layout()
|
||||||
|
|
||||||
|
if v.buf == nil || v.buf.Width() != w || v.buf.Height() != h {
|
||||||
|
v.buf = buf2d.NewBuffer(w, h, tui.DefaultRune)
|
||||||
|
}
|
||||||
|
|
||||||
|
v.Scroll(0, 0) // limit scroll offset boundaries
|
||||||
|
v.View().Draw(v.buf)
|
||||||
|
|
||||||
|
scrollH, scrollV := v.determineViewportSize(buf)
|
||||||
|
copyBufferWidth, copyBufferHeight := 0, 0
|
||||||
|
|
||||||
|
if scrollH {
|
||||||
|
copyBufferWidth = v.width
|
||||||
|
} else {
|
||||||
|
copyBufferWidth = v.buf.Width()
|
||||||
|
}
|
||||||
|
|
||||||
|
if scrollV {
|
||||||
|
copyBufferHeight = v.height
|
||||||
|
} else {
|
||||||
|
copyBufferHeight = v.buf.Height()
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollVHeight := int(float64(buf.Height()) / float64(v.buf.Height()) * float64(v.height))
|
||||||
|
scrollVStart := int(math.Ceil(float64(v.verticalScrollOffset) / float64(v.buf.Height()) * float64(v.height)))
|
||||||
|
|
||||||
|
scrollHStart := int(float64(v.horizontalScrollOffset) / float64(v.buf.Width()) * float64(v.width))
|
||||||
|
scrollHWidth := int(math.Ceil(float64(buf.Width()) / float64(v.buf.Width()) * float64(v.width)))
|
||||||
|
|
||||||
|
// guarantee minimum scroll bar thumb size of 1 TODO inaccurate for small scales
|
||||||
|
if scrollVHeight <= 0 {
|
||||||
|
scrollVHeight = 1
|
||||||
|
if scrollVStart >= v.height {
|
||||||
|
scrollVStart = v.height - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if scrollHWidth <= 0 {
|
||||||
|
scrollHWidth = 1
|
||||||
|
if scrollHStart >= v.width {
|
||||||
|
scrollHStart = v.width - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// copy buffer
|
||||||
|
for x := 0; x < copyBufferWidth; x++ {
|
||||||
|
for y := 0; y < copyBufferHeight; y++ {
|
||||||
|
buf.Set(x, y, v.buf.Get(v.horizontalScrollOffset+x, v.verticalScrollOffset+y))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// vertical scrollbar
|
||||||
|
if scrollV {
|
||||||
|
for y := 0; y < v.height; y++ {
|
||||||
|
var style tcell.Style
|
||||||
|
if y >= scrollVStart && y < scrollVStart+scrollVHeight {
|
||||||
|
style = tui.StyleDefault.Background(tcell.ColorWhite)
|
||||||
|
} else {
|
||||||
|
style = tui.StyleDefault.Background(tcell.ColorDarkSlateGray)
|
||||||
|
}
|
||||||
|
buf.Set(v.width, y, tui.Rune{Rn: ' ', Style: style})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// horizontal scrollbar
|
||||||
|
if scrollH {
|
||||||
|
for x := 0; x < v.width; x++ {
|
||||||
|
var style tcell.Style
|
||||||
|
if x >= scrollHStart && x < scrollHStart+scrollHWidth {
|
||||||
|
style = tui.StyleDefault.Background(tcell.ColorWhite)
|
||||||
|
} else {
|
||||||
|
style = tui.StyleDefault.Background(tcell.ColorDarkSlateGray)
|
||||||
|
}
|
||||||
|
buf.Set(x, v.height, tui.Rune{Rn: ' ', Style: style})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *ScrollView) Layout() (prefWidth, prefHeight int) {
|
||||||
|
return -1, -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *ScrollView) Scroll(verticalOffset, horizontalOffset int) {
|
||||||
|
if v.buf != nil {
|
||||||
|
v.verticalScrollOffset = limit(v.verticalScrollOffset+verticalOffset, 0, max(v.buf.Height()-v.height, 0))
|
||||||
|
v.horizontalScrollOffset = limit(v.horizontalScrollOffset+horizontalOffset, 0, max(v.buf.Width()-v.width, 0))
|
||||||
|
} else {
|
||||||
|
v.verticalScrollOffset = v.verticalScrollOffset + verticalOffset
|
||||||
|
v.horizontalScrollOffset = v.horizontalScrollOffset + horizontalOffset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *ScrollView) determineViewportSize(buf *tui.ViewBuffer) (scrollbarH, scrollbarV bool) {
|
||||||
|
v.width, v.height = buf.Width()-1, buf.Height()-1
|
||||||
|
scrollbarV = v.buf.Height() > v.height
|
||||||
|
scrollbarH = v.buf.Width() > v.width
|
||||||
|
|
||||||
|
if scrollbarV && !scrollbarH {
|
||||||
|
v.height++
|
||||||
|
scrollbarV = v.buf.Height() > v.height
|
||||||
|
}
|
||||||
|
|
||||||
|
if !scrollbarV && scrollbarH {
|
||||||
|
v.width++
|
||||||
|
scrollbarH = v.buf.Width() > v.width
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
@ -12,17 +12,17 @@ type TextView struct {
|
|||||||
|
|
||||||
var _ tui.View = &TextView{}
|
var _ tui.View = &TextView{}
|
||||||
|
|
||||||
func (v *TextView) Draw(buf *tui.ViewBuffer) {
|
|
||||||
v.ViewTmpl.Draw(buf)
|
|
||||||
tui.WriteMultiLineString(buf, v.Text, v.Style(), 0, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewTextView(text string) *TextView {
|
func NewTextView(text string) *TextView {
|
||||||
return &TextView{
|
return &TextView{
|
||||||
Text: text,
|
Text: text,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v *TextView) Draw(buf *tui.ViewBuffer) {
|
||||||
|
v.ViewTmpl.Draw(buf)
|
||||||
|
tui.WriteMultiLineString(buf, v.Text, v.Style(), 0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
func (v *TextView) Layout() (prefWidth, prefHeight int) {
|
func (v *TextView) Layout() (prefWidth, prefHeight int) {
|
||||||
return tui.MeasureMultiLineString(v.Text)
|
return tui.MeasureMultiLineString(v.Text)
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,10 @@ func (v *ViewTmpl) OnKeyPressed(event *KeyEvent) (consumed bool) {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v *ViewTmpl) OnMouseClicked(event *MouseEvent) (consumed bool) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func (v *ViewTmpl) SetStyle(s Style) {
|
func (v *ViewTmpl) SetStyle(s Style) {
|
||||||
v.style = &s
|
v.style = &s
|
||||||
}
|
}
|
||||||
|
@ -30,17 +30,10 @@ func (v *WrapperTmpl) OnKeyPressed(event *KeyEvent) (consumed bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (v *WrapperTmpl) SetStyle(s Style) {
|
func (v *WrapperTmpl) SetStyle(s Style) {
|
||||||
if v.view != nil {
|
|
||||||
v.view.SetStyle(s)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
v.ViewTmpl.SetStyle(s)
|
v.ViewTmpl.SetStyle(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *WrapperTmpl) Style() Style {
|
func (v *WrapperTmpl) Style() Style {
|
||||||
if v.view != nil {
|
|
||||||
return v.view.Style()
|
|
||||||
}
|
|
||||||
return v.ViewTmpl.Style()
|
return v.ViewTmpl.Style()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user