shithub: mycel

ref: b61213d33be35446f1fff9efcb83adbb3eaf4d54
dir: /browser/duitx/scroll.go/

View raw version
package duitx

// Original code from github.com/mjl-/duit
//
// Copyright 2018 Mechiel Lukkien [email protected]
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

import (
	"fmt"
	"image"
	"math"
	"time"

	"9fans.net/go/draw"
	"github.com/mjl-/duit"
	"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
	Height int // < 0 means full height, 0 means as much as necessary, >0 means exactly that many lowdpi pixels

	r             image.Rectangle // entire ui
	barR          image.Rectangle
	barActiveR    image.Rectangle
	childR        image.Rectangle
	Offset        int         // current scroll offset in pixels
	img           *draw.Image // for child to draw on
	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(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)

	if self.Layout == duit.Clean && !force {
		return
	}
	self.Layout = duit.Clean
	self.Draw = duit.Dirty
	// todo: be smarter about DirtyKid

	ui.scrollbarSize = dui.Scale(duit.ScrollbarSize)
	scaledHeight := dui.Scale(ui.Height)
	if scaledHeight > 0 && scaledHeight < sizeAvail.Y {
		sizeAvail.Y = scaledHeight
	}
	ui.r = rect(sizeAvail)
	ui.barR = ui.r
	ui.barR.Max.X = ui.barR.Min.X + ui.scrollbarSize
	ui.childR = ui.r
	ui.childR.Min.X = ui.barR.Max.X

	// todo: only force when sizeAvail or childR changed?
	ui.Kid.UI.Layout(dui, &ui.Kid, image.Pt(ui.r.Dx()-ui.barR.Dx(), ui.r.Dy()), force)
	ui.Kid.Layout = duit.Clean
	ui.Kid.Draw = duit.Dirty

	kY := ui.Kid.R.Dy()
	if ui.r.Dy() > kY && ui.Height == 0 {
		ui.barR.Max.Y = kY
		ui.r.Max.Y = kY
		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) {
	debugDraw(dui, self)

	if self.Draw == duit.Clean {
		return
	} else {
		log.Printf("Draw: self.Draw=%v is not clean, force=%v", self.Draw, force)
	}

	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)

	bg := dui.ScrollBGNormal
	vis := dui.ScrollVisibleNormal
	if barHover {
		bg = dui.ScrollBGHover
		vis = dui.ScrollVisibleHover
	}

	h := ui.r.Dy()
	uih := ui.Kid.R.Dy()
	if uih > h {
		barR := ui.barR.Add(orig)
		img.Draw(barR, bg, nil, image.ZP)
		barH := h * h / uih
		barY := ui.Offset * h / uih
		ui.barActiveR = ui.barR
		ui.barActiveR.Min.Y += barY
		ui.barActiveR.Max.Y = ui.barActiveR.Min.Y + barH
		barActiveR := ui.barActiveR.Add(orig)
		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
	}
	
	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] }
	}

	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]
		}
		if i > 1 {
			_, okm2 = ui.tiles[i-2]
		}
		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) }
	}
	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
	}

	predrawCur()

	rTop := draw.Rectangle{
		Min: ui.childR.Min,
		Max: draw.Point{
			X: ui.childR.Max.X,
			Y: ui.childR.Max.Y - of,
		},
	}
	rBtm := draw.Rectangle{
		Min: draw.Point{
			X: ui.childR.Min.X,
			Y: rTop.Max.Y,
		},
		Max: ui.childR.Max,
	}
	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
}

func (ui *Scroll) scroll(delta int) (changed bool) {
	o := ui.Offset
	ui.Offset += delta
	ui.Offset = maximum(0, ui.Offset)
	ui.Offset = minimum(ui.Offset, maximum(0, ui.Kid.R.Dy()-ui.childR.Dy()))
	return o != ui.Offset
}

func (ui *Scroll) scrollKey(k rune) (consumed bool) {
	switch k {
	case draw.KeyUp:
		return ui.scroll(-50)
	case draw.KeyDown:
		return ui.scroll(50)
	case draw.KeyPageUp:
		return ui.scroll(-200)
	case draw.KeyPageDown:
		return ui.scroll(200)
	}
	return false
}

func (ui *Scroll) scrollMouse(m draw.Mouse, scrollOnly bool) (consumed bool) {
	switch m.Buttons {
	case duit.Button4:
		return ui.scroll(-m.Y / 4)
	case duit.Button5:
		return ui.scroll(m.Y / 4)
	}

	if scrollOnly {
		return false
	}
	switch m.Buttons {
	case duit.Button1:
		return ui.scroll(-m.Y)
	case duit.Button2:
		Offset := m.Y * ui.Kid.R.Dy() / ui.barR.Dy()
		OffsetMax := ui.Kid.R.Dy() - ui.childR.Dy()
		Offset = maximum(0, minimum(Offset, OffsetMax))
		o := ui.Offset
		ui.Offset = Offset
		return o != ui.Offset
	case duit.Button3:
		return ui.scroll(m.Y)
	}
	return false
}

func (ui *Scroll) result(dui *duit.DUI, self *duit.Kid, r *duit.Result, scrolled bool) {
	if ui.Kid.Layout != duit.Clean {
		ui.Kid.UI.Layout(dui, &ui.Kid, ui.childR.Size(), false)
		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
		}
	}
}

func (ui *Scroll) Mouse(dui *duit.DUI, self *duit.Kid, m draw.Mouse, origM draw.Mouse, orig image.Point) (r duit.Result) {
	nOrigM := origM
	nOrigM.Point = nOrigM.Point.Add(image.Pt(-ui.scrollbarSize, ui.Offset))
	nm := m
	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) // comment this to have no flicker after mouse move and then scroll
		return
	}
	if m.Point.In(ui.barR) {
		r.Hit = ui
		r.Consumed = ui.scrollMouse(m, false)
		self.Draw = duit.Dirty
		return
	} else if m.Point.In(ui.childR) {
		r.Consumed = ui.scrollMouse(m, true)
		if r.Consumed {
			self.Draw = duit.Dirty
			return
		}
		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
}

func (ui *Scroll) Key(dui *duit.DUI, self *duit.Kid, k rune, m draw.Mouse, orig image.Point) (r duit.Result) {
	if m.Point.In(ui.barR) {
		r.Hit = ui
		r.Consumed = ui.scrollKey(k)
		if r.Consumed {
			self.Draw = duit.Dirty
		}
	}
	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))
		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
}

func (ui *Scroll) warpScroll(dui *duit.DUI, self *duit.Kid, warp *image.Point, orig image.Point) {
	if warp == nil {
		return
	}

	Offset := ui.Offset
	if warp.Y < ui.Offset {
		ui.Offset = maximum(0, warp.Y-dui.Scale(40))
	} else if warp.Y > ui.Offset+ui.r.Dy() {
		ui.Offset = minimum(ui.Kid.R.Dy()-ui.r.Dy(), warp.Y+dui.Scale(40)-ui.r.Dy())
	}
	if Offset != ui.Offset {
		if self != nil {
			self.Draw = duit.Dirty
		} else {
			dui.MarkDraw(ui)
		}
	}
	warp.Y -= ui.Offset
	warp.X += orig.X + ui.scrollbarSize
	warp.Y += orig.Y
}

func (ui *Scroll) _focus(dui *duit.DUI, p *image.Point) *image.Point {
	if p == nil {
		return nil
	}
	pp := p.Add(ui.childR.Min)
	p = &pp
	ui.warpScroll(dui, nil, p, image.ZP)
	return p
}

func (ui *Scroll) FirstFocus(dui *duit.DUI, self *duit.Kid) *image.Point {
	p := ui.Kid.UI.FirstFocus(dui, &ui.Kid)
	return ui._focus(dui, p)
}

func (ui *Scroll) Focus(dui *duit.DUI, self *duit.Kid, o duit.UI) *image.Point {
	if o == ui {
		p := image.Pt(minimum(ui.scrollbarSize/2, ui.r.Dx()), minimum(ui.scrollbarSize/2, ui.r.Dy()))
		return &p
	}
	p := ui.Kid.UI.Focus(dui, &ui.Kid, o)
	return ui._focus(dui, p)
}

func (ui *Scroll) Mark(self *duit.Kid, o duit.UI, forLayout bool) (marked bool) {
	if self.Mark(o, forLayout) {
		return true
	}
	marked = ui.Kid.UI.Mark(&ui.Kid, o, forLayout)
	if marked {
		if forLayout {
			if self.Layout == duit.Clean {
				self.Layout = duit.DirtyKid
			}
		} else {
			if self.Layout == duit.Clean {
				self.Draw = duit.DirtyKid
			}
		}
	}
	return
}

func (ui *Scroll) Print(self *duit.Kid, indent int) {
	what := fmt.Sprintf("Scroll Offset=%d childR=%v", ui.Offset, ui.childR)
	duit.PrintUI(what, self, indent)
	ui.Kid.UI.Print(&ui.Kid, indent+1)
}

//////////////////////
//                  //
// helper functions //
//                  //
//////////////////////

func pt(v int) image.Point {
	return image.Point{v, v}
}

func rect(p image.Point) image.Rectangle {
	return image.Rectangle{image.ZP, p}
}

func extendY(r image.Rectangle, dy int) image.Rectangle {
	r.Max.Y += dy
	return r
}

func insetPt(r image.Rectangle, pad image.Point) image.Rectangle {
	r.Min = r.Min.Add(pad)
	r.Max = r.Max.Sub(pad)
	return r
}

func outsetPt(r image.Rectangle, pad image.Point) image.Rectangle {
	r.Min = r.Min.Sub(pad)
	r.Max = r.Max.Add(pad)
	return r
}

func minimum64(a, b int64) int64 {
	if a < b {
		return a
	}
	return b
}

func maximum64(a, b int64) int64 {
	if a > b {
		return a
	}
	return b
}

func minimum(a, b int) int {
	if a < b {
		return a
	}
	return b
}

func maximum(a, b int) int {
	if a > b {
		return a
	}
	return b
}

func debugLayout(d *duit.DUI, self *duit.Kid) {
	if d.DebugLayout > 0 {
		log.Printf("duit: Layout %T %s layout=%d draw=%d\n", self.UI, self.R, self.Layout, self.Draw)
	}
}

func debugDraw(d *duit.DUI, self *duit.Kid) {
	if d.DebugDraw > 0 {
		log.Printf("duit: Draw %T %s layout=%d draw=%d\n", self.UI, self.R, self.Layout, self.Draw)
	}
}

func duitError(d *duit.DUI, err error, msg string) bool {
	if err == nil {
		return false
	}
	go func() {
		d.Error <- fmt.Errorf("%s: %s", msg, err)
	}()
	return true
}