ref: 2cd616e4664283680141a3a014caa7253fc76445
parent: 88687b3cae7a352e9606d775b130b8b0d3d6d9c4
author: Philip Silva <[email protected]>
date: Mon Aug 16 12:49:40 EDT 2021
Scrolling with tiles - less flickering - no more blurry texts on hover - can use custom cursor again
--- a/browser/browser.go
+++ b/browser/browser.go
@@ -586,7 +586,7 @@
if n.Css("height") == "" {
n.SetCss("height", fmt.Sprintf("%vpx", 4 * n.Font().Height))
}
- return NewElement(duitx.NewScroll(l), n)
+ return NewElement(duit.NewScroll(l), n)
}
func NewTextArea(n *nodes.Node) *Element {
@@ -673,11 +673,11 @@
maxX := self.R.Dx()
maxY := self.R.Dy()
if 5 <= x && x <= (maxX-5) && 5 <= y && y <= (maxY-5) && el.IsLink {
- //dui.Display.SwitchCursor(&draw.Cursor{
- // Set: cursor,
- //})
+ dui.Display.SwitchCursor(&draw.Cursor{
+ Black: cursor,
+ })
if m.Buttons == 0 {
- r.Consumed = true
+ //r.Consumed = true
return r
}
} else {
--- a/browser/duitx/scroll.go
+++ b/browser/duitx/scroll.go
@@ -24,6 +24,7 @@
"fmt"
"image"
"math"
+ "time"
"9fans.net/go/draw"
"github.com/mjl-/duit"
@@ -30,6 +31,8 @@
"github.com/psilva261/opossum/logger"
)
+const maxAge = time.Minute
+
// Scroll shows a part of its single child, typically a box, and lets you scroll the content.
type Scroll struct {
Kid duit.Kid
@@ -44,15 +47,106 @@
scrollbarSize int
lastMouseUI duit.UI
drawOffset int
+
+ tiles map[int]*draw.Image
+ last map[int]time.Time
+ tilesChanged bool
}
var _ duit.UI = &Scroll{}
// NewScroll returns a full-height scroll bar containing ui.
-func NewScroll(ui duit.UI) *Scroll {
- return &Scroll{Height: -1, Kid: duit.Kid{UI: ui}}
+func NewScroll(dui *duit.DUI, ui duit.UI) *Scroll {
+ s := &Scroll{
+ Height: -1,
+ Kid: duit.Kid{UI: ui},
+ tiles: make(map[int]*draw.Image),
+ last: make(map[int]time.Time),
+ }
+ return s
}
+func (ui *Scroll) Free() {
+ for _, tl := range ui.tiles {
+ tl.Free()
+ }
+ ui.tiles = make(map[int]*draw.Image)
+ ui.last = make(map[int]time.Time)
+}
+
+func (ui *Scroll) freeCur() {
+ i, of := ui.pos()
+ tl, ok := ui.tiles[i]
+ tl1, ok1 := ui.tiles[i+1]
+ if !ui.tilesChanged && (!ok || ui.sizeOk(tl)) && (of == 0 || !ok1 || ui.sizeOk(tl1)) {
+ return
+ }
+ if ui.tiles[i] != nil {
+ ui.tiles[i].Free()
+ delete(ui.tiles, i)
+ delete(ui.last, i)
+ }
+ if of > 0 {
+ if ui.tiles[i+1] != nil {
+ ui.tiles[i+1].Free()
+ delete(ui.tiles, i+1)
+ delete(ui.last, i+1)
+ }
+ }
+ ui.tilesChanged = false
+}
+
+func (ui *Scroll) sizeOk(tl *draw.Image) bool {
+ return tl != nil && tl.R.Dx() == ui.r.Dx() && tl.R.Dy() == ui.r.Dy()
+}
+
+func (ui *Scroll) ensure(dui *duit.DUI, i int) {
+ log.Printf("ensure(dui, %v)", i)
+ last, ok := ui.last[i]
+ tl, _ := ui.tiles[i]
+ if ok && time.Since(last) < maxAge && ui.sizeOk(tl) {
+ return
+ }
+
+ log.Printf("ensure(dui, %v): draw", i)
+ r := ui.r.Add(image.Point{X: 0, Y: i*ui.r.Dy()})
+ img, err := dui.Display.AllocImage(r, draw.ARGB32, false, dui.BackgroundColor)
+ if duitError(dui, err, "allocimage") {
+ return
+ }
+ ui.Kid.UI.Draw(dui, &ui.Kid, img, image.ZP, draw.Mouse{}, true)
+
+ if ui.tiles[i] != nil {
+ ui.tiles[i].Free()
+ ui.tiles[i] = nil
+ }
+ log.Printf("ensure: ui.tiles[%d] = img(R=%+v, ...)", i, img.R)
+ ui.tiles[i] = img
+ ui.last[i] = time.Now()
+
+ for j, t := range ui.tiles {
+ if math.Abs(float64(i-j)) > 5 {
+ t.Free()
+ delete(ui.tiles, j)
+ delete(ui.last, j)
+ }
+ }
+}
+
+func (ui *Scroll) pos() (t, of int) {
+ t = ui.Offset / ui.r.Dy()
+ of = ui.Offset % ui.r.Dy()
+ return
+}
+
+func (ui *Scroll) tlR(i int) (r image.Rectangle) {
+ r.Min.X = ui.r.Min.X
+ r.Max.X = ui.r.Max.X
+ r.Min.Y = ui.r.Min.Y+i*ui.r.Dy()
+ r.Max.Y = r.Min.Y+ui.r.Dy()
+ return
+}
+
func (ui *Scroll) Layout(dui *duit.DUI, self *duit.Kid, sizeAvail image.Point, force bool) {
debugLayout(dui, self)
@@ -86,6 +180,7 @@
ui.childR.Max.Y = kY
}
self.R = rect(ui.r.Size())
+ ui.Free()
}
func (ui *Scroll) Draw(dui *duit.DUI, self *duit.Kid, img *draw.Image, orig image.Point, m draw.Mouse, force bool) {
@@ -93,13 +188,21 @@
if self.Draw == duit.Clean {
return
+ } else {
+ log.Printf("Draw: self.Draw=%v is not clean, force=%v", self.Draw, force)
}
- self.Draw = duit.Clean
if ui.r.Empty() {
+ self.Draw = duit.Clean
return
}
+ ui.drawBar(dui, self, img, orig, m, force)
+ ui.drawChild(dui, self, img, orig, m, force)
+ self.Draw = duit.Clean
+}
+
+func (ui *Scroll) drawBar(dui *duit.DUI, self *duit.Kid, img *draw.Image, orig image.Point, m draw.Mouse, force bool) {
// ui.scroll(0)
barHover := m.In(ui.barR)
@@ -124,65 +227,89 @@
barActiveR.Max.X -= 1 // unscaled
img.Draw(barActiveR, vis, nil, image.ZP)
}
+}
+func (ui *Scroll) drawChild(dui *duit.DUI, self *duit.Kid, img *draw.Image, orig image.Point, m draw.Mouse, force bool) {
// draw child ui
if ui.childR.Empty() {
return
}
- d := math.Abs(float64(ui.drawOffset - ui.Offset))
- if d > float64(ui.r.Max.Y) {
- ui.Kid.Draw = duit.Dirty
+
+ var i, of int
+ var tl, tl1 *draw.Image
+ var ok, ok1, ok2, ok3, okm1, okm2 bool
+ p := draw.Point{X: 0, Y: ui.Offset}
+ n := draw.Point{X: 0, Y: -ui.Offset}
+
+ predrawCur := func() {
+ // tile draw
+ i, of = ui.pos()
+ tl, ok = ui.tiles[i]
+ tl1, ok1 = ui.tiles[i+1]
+ if !ok { ui.ensure(dui, i) }
+ if !ok1 { ui.ensure(dui, i+1) }
+ if !ok { tl, _ = ui.tiles[i] }
+ if !ok1 && of > 0 { tl1, _ = ui.tiles[i+1] }
}
- if ui.img == nil || ui.drawRect().Size() != ui.img.R.Size() || ui.Kid.Draw == duit.Dirty {
- var err error
- if ui.img != nil {
- ui.img.Free()
- ui.img = nil
+
+ predrawFut := func() {
+ // tile draw
+ i, of = ui.pos()
+ tl1, ok1 = ui.tiles[i+1]
+ _, ok2 = ui.tiles[i+2]
+ _, ok3 = ui.tiles[i+3]
+ if i > 0 {
+ _, okm1 = ui.tiles[i-1]
}
- ui.Kid.Draw = duit.Dirty
- if ui.Kid.R.Dx() == 0 || ui.Kid.R.Dy() == 0 {
- return
+ if i > 1 {
+ _, okm2 = ui.tiles[i-2]
}
- ui.img, err = dui.Display.AllocImage(ui.drawRect(), draw.ARGB32, false, dui.BackgroundColor)
- if duitError(dui, err, "allocimage") {
- return
- }
- ui.drawOffset = ui.Offset
- } else if ui.Kid.Draw == duit.Dirty {
- ui.img.Draw(ui.img.R, dui.Background, nil, image.ZP)
+ if of == 0 && !ok1 { ui.ensure(dui, i+1) }
+ if ok1 && !ok2 { ui.ensure(dui, i+2) }
+ if ok2 && !ok3 { ui.ensure(dui, i+3) }
+ if i > 0 && !okm1 { ui.ensure(dui, i-1) }
+ if i > 1 && okm1 && !okm2 { ui.ensure(dui, i-2) }
}
- m.Point = m.Point.Add(image.Pt(-ui.childR.Min.X, ui.Offset))
- if ui.Kid.Draw != duit.Clean {
- if force {
- ui.Kid.Draw = duit.Dirty
- }
- ui.Kid.UI.Draw(dui, &ui.Kid, ui.img, image.ZP, m, ui.Kid.Draw == duit.Dirty)
+ defer predrawFut()
+
+ if self.Draw == duit.DirtyKid {
+ ui.freeCur()
ui.Kid.Draw = duit.Clean
+ } else if ui.Kid.Draw != duit.Clean || force {
+ log.Printf("drawChild: refresh: ui.Kid.Draw=%v force=%v", ui.Kid.Draw, force)
+ log.Flush()
+ ui.freeCur()
+ tmp := img.Clipr
+ img.ReplClipr(false, ui.childR.Add(orig))
+ ui.Kid.UI.Draw(dui, &ui.Kid, img, orig.Add(ui.childR.Min).Add(n), draw.Mouse{}, true)
+ img.ReplClipr(false, tmp)
+ ui.Kid.Draw = duit.Clean
+ return
}
- img.Draw(ui.childR.Add(orig), ui.img, nil, image.Pt(0, ui.Offset))
-}
-// Allocate only an image buffer of view size ui.r
-// - which is translated by scroll offset ui.Offset - instead
-// of whole Kid view size ui.Kid.R which leads to much
-// faster render times for large pages. Add same size rectangles
-// above/below to decrease flickering.
-func (ui *Scroll) drawRect() image.Rectangle {
- if 2*ui.r.Dy() > ui.Kid.R.Dy() {
- return ui.Kid.R
+ predrawCur()
+
+ rTop := draw.Rectangle{
+ Min: ui.childR.Min,
+ Max: draw.Point{
+ X: ui.childR.Max.X,
+ Y: ui.childR.Max.Y - of,
+ },
}
- r := image.Rectangle{
- Min: ui.r.Min,
- Max: image.Point{
- ui.r.Max.X,
- 3*ui.r.Max.Y,
+ rBtm := draw.Rectangle{
+ Min: draw.Point{
+ X: ui.childR.Min.X,
+ Y: rTop.Max.Y,
},
+ Max: ui.childR.Max,
}
- r = r.Add(image.Point{X:0, Y:ui.Offset-ui.r.Max.Y})
- if r.Min.Y > ui.Offset {
- r.Min.Y -= ui.Offset
+ pOf := draw.Point{X: 0, Y: ui.Offset+rTop.Dy()}
+ img.Draw(rTop.Add(orig), tl, nil, p)
+ if of > 0 {
+ img.Draw(rBtm.Add(orig), tl1, nil, pOf)
}
- return r
+
+ return
}
func (ui *Scroll) scroll(delta int) (changed bool) {
@@ -240,8 +367,14 @@
ui.Kid.Layout = duit.Clean
ui.Kid.Draw = duit.Dirty
self.Draw = duit.Dirty
+ if r.Consumed && !scrolled {
+ ui.tilesChanged = true
+ }
} else if ui.Kid.Draw != duit.Clean || scrolled {
self.Draw = duit.Dirty
+ if r.Consumed && !scrolled {
+ ui.tilesChanged = true
+ }
}
}
@@ -252,7 +385,7 @@
nm.Point = nm.Point.Add(image.Pt(-ui.scrollbarSize, ui.Offset))
if m.Buttons == 0 {
- ui.Kid.UI.Mouse(dui, &ui.Kid, nm, nOrigM, image.ZP)
+ ui.Kid.UI.Mouse(dui, &ui.Kid, nm, nOrigM, image.ZP) // comment this to have no flicker after mouse move and then scroll
return
}
if m.Point.In(ui.barR) {
@@ -269,6 +402,8 @@
r = ui.Kid.UI.Mouse(dui, &ui.Kid, nm, nOrigM, image.ZP)
if r.Consumed {
self.Draw = duit.Dirty
+ ui.tilesChanged = true
+ log.Printf("Mouse: set ui.tilesChanged = true")
}
}
return
@@ -283,15 +418,18 @@
}
}
if m.Point.In(ui.childR) {
+ log.Printf("Key: in ui.childR (self.Draw=%v)", self.Draw)
m.Point = m.Point.Add(image.Pt(-ui.scrollbarSize, ui.Offset))
- r = ui.Kid.UI.Key(dui, &ui.Kid, k, m, image.ZP)
- ui.warpScroll(dui, self, r.Warp, orig)
- scrolled := false
- if !r.Consumed {
- scrolled = ui.scrollKey(k)
+ scrolled := ui.scrollKey(k)
+ if scrolled {
+ self.Draw = duit.Dirty
r.Consumed = scrolled
+ return
}
+ r = ui.Kid.UI.Key(dui, &ui.Kid, k, m, image.ZP)
+ ui.warpScroll(dui, self, r.Warp, orig)
ui.result(dui, self, &r, scrolled)
+ log.Printf("Key: in ui.childR (self.Draw'=%v)", self.Draw)
}
return
}
--- /dev/null
+++ b/browser/duitx/scroll_test.go
@@ -1,0 +1,38 @@
+package duitx
+
+import (
+ "image"
+ "testing"
+)
+
+func TestPos(t *testing.T) {
+ s := &Scroll{
+ r: image.Rectangle{
+ Min: image.Point{0, 0},
+ Max: image.Point{1600, 1200},
+ },
+ Offset: 0,
+ }
+ if i, of := s.pos(); i != 0 || of != 0 {
+ t.Fatalf("%v %v", i, of)
+ }
+ s.Offset = 3400
+ if i, of := s.pos(); i != 2 || of != 1000 {
+ t.Fatalf("%v %v", i, of)
+ }
+}
+
+func TestTlR(t *testing.T) {
+ s := &Scroll{
+ r: image.Rectangle{
+ Min: image.Point{0, 0},
+ Max: image.Point{1600, 1200},
+ },
+ }
+ if r := s.tlR(0); r.Min.X != 0 || r.Min.Y != 0 || r.Dx() != 1600 || r.Dy() != 1200 {
+ t.Fatalf("%v", r)
+ }
+ if r := s.tlR(2); r.Min.X != 0 || r.Min.Y != 2400 || r.Dx() != 1600 || r.Dy() != 1200 {
+ t.Fatalf("%v", r)
+ }
+}
--- a/browser/website.go
+++ b/browser/website.go
@@ -133,9 +133,11 @@
log.Printf("Layout website...")
nt := nodes.NewNodeTree(body, style.Map{}, nodeMap, &nodes.Node{})
- scroller = duitx.NewScroll(
- NodeToBox(0, browser, nt),
- )
+ if scroller != nil {
+ scroller.Free()
+ scroller = nil
+ }
+ scroller = duitx.NewScroll(dui, NodeToBox(0, browser, nt))
numElements := 0
TraverseTree(scroller, func(ui duit.UI) {
numElements++
@@ -145,9 +147,7 @@
if numElements < 10 {
log.Errorf("Less than 10 elements layouted, seems css processing failed. Will layout without css")
nt = nodes.NewNodeTree(body, style.Map{}, make(map[*html.Node]style.Map), nil)
- scroller = duitx.NewScroll(
- NodeToBox(0, browser, nt),
- )
+ scroller = duitx.NewScroll(dui, NodeToBox(0, browser, nt))
w.UI = scroller
}