github.com/jmigpin/editor@v1.6.0/util/uiutil/widget/scrollbar.go (about)

     1  package widget
     2  
     3  import (
     4  	"image"
     5  	"math"
     6  
     7  	"github.com/jmigpin/editor/util/imageutil"
     8  	"github.com/jmigpin/editor/util/mathutil"
     9  	"github.com/jmigpin/editor/util/uiutil/event"
    10  )
    11  
    12  // Used by ScrollArea. Parent of ScrollHandle.
    13  type ScrollBar struct {
    14  	ENode
    15  	Handle     *ScrollHandle
    16  	Horizontal bool
    17  
    18  	positionPercent float64
    19  	sizePercent     float64
    20  
    21  	pressPad image.Point
    22  	clicking bool
    23  	dragging bool
    24  
    25  	sa *ScrollArea
    26  
    27  	ctx ImageContext
    28  }
    29  
    30  func NewScrollBar(ctx ImageContext, sa *ScrollArea) *ScrollBar {
    31  	sb := &ScrollBar{ctx: ctx, sa: sa}
    32  	sb.positionPercent = 0.0
    33  	sb.sizePercent = 1.0
    34  
    35  	sb.Handle = NewScrollHandle(ctx, sb)
    36  	sb.Append(sb.Handle)
    37  	return sb
    38  }
    39  
    40  //----------
    41  
    42  func (sb *ScrollBar) scrollPage(up bool) {
    43  	o := sb.sa.scrollable.ScrollOffset()
    44  	sy := sb.sa.scrollable.ScrollPageSizeY(up)
    45  	o = o.Add(image.Point{0, sy})
    46  	sb.sa.scrollable.SetScrollOffset(o)
    47  }
    48  
    49  func (sb *ScrollBar) scrollWheel(up bool) {
    50  	o := sb.sa.scrollable.ScrollOffset()
    51  	sy := sb.sa.scrollable.ScrollWheelSizeY(up)
    52  	o = o.Add(image.Point{0, sy})
    53  	sb.sa.scrollable.SetScrollOffset(o)
    54  }
    55  
    56  //----------
    57  
    58  func (sb *ScrollBar) yBoundsSizePad() (int, int, int) {
    59  	min := 5
    60  	d := sb.yaxis(sb.Bounds.Size())
    61  	dpad := mathutil.Max(d-min, 0)
    62  	return d, dpad, min
    63  }
    64  
    65  //----------
    66  
    67  func (sb *ScrollBar) scrollToPoint(p *image.Point) {
    68  	py := float64(sb.yaxis(p.Sub(sb.pressPad).Sub(sb.Bounds.Min)))
    69  	_, dpad, _ := sb.yBoundsSizePad()
    70  	o := py / float64(dpad)
    71  	sb.scrollToPositionPercent(o)
    72  }
    73  
    74  func (sb *ScrollBar) scrollToPositionPercent(offsetPerc float64) {
    75  	size := sb.sa.scrollable.ScrollSize()
    76  	offset := sb.sa.scrollable.ScrollOffset()
    77  	offsetPerc = mathutil.LimitFloat64(offsetPerc, 0, 1)
    78  	*sb.yaxisPtr(&offset) = int(offsetPerc*float64(sb.yaxis(size)) + 0.5)
    79  	sb.sa.scrollable.SetScrollOffset(offset)
    80  }
    81  
    82  //----------
    83  
    84  func (sb *ScrollBar) calcPositionAndSize() {
    85  	pos := sb.sa.scrollable.ScrollOffset()
    86  	size := sb.sa.scrollable.ScrollSize()
    87  	vsize := sb.sa.scrollable.ScrollViewSize()
    88  
    89  	var pp, sp float64
    90  
    91  	sizey0 := sb.yaxis(size)
    92  
    93  	if sizey0 == 0 {
    94  		pp = 0
    95  		sp = 1
    96  	} else {
    97  		posy := float64(sb.yaxis(pos))
    98  		sizey := float64(sizey0)
    99  		vsizey := float64(sb.yaxis(vsize))
   100  		pp = posy / sizey
   101  		sp = vsizey / sizey
   102  	}
   103  
   104  	pp = mathutil.LimitFloat64(pp, 0, 1)
   105  	sp = mathutil.LimitFloat64(pp+sp, 0, 1) // add pp
   106  
   107  	sb.positionPercent = pp
   108  	sb.sizePercent = sp
   109  }
   110  
   111  //----------
   112  
   113  func (sb *ScrollBar) OnChildMarked(child Node, newMarks Marks) {
   114  	// paint scrollbar background if the handle is getting painted
   115  	if child == sb.Handle {
   116  		if newMarks.HasAny(MarkNeedsPaint) {
   117  			sb.MarkNeedsPaint()
   118  		}
   119  	}
   120  }
   121  
   122  //----------
   123  
   124  func (sb *ScrollBar) Layout() {
   125  	r := sb.Bounds
   126  	d, dpad, min := sb.yBoundsSizePad()
   127  
   128  	sb.calcPositionAndSize()
   129  
   130  	p := int(math.Ceil(float64(dpad) * sb.positionPercent))
   131  	s := int(math.Ceil(float64(d) * sb.sizePercent))
   132  	s = mathutil.Max(s, p+min) // minimum bar size (stay visible)
   133  
   134  	*sb.yaxisPtr(&r.Min) = sb.yaxis(sb.Bounds.Min) + p
   135  	*sb.yaxisPtr(&r.Max) = sb.yaxis(sb.Bounds.Min) + s
   136  	r = r.Intersect(sb.Bounds)
   137  
   138  	sb.Handle.Bounds = r
   139  }
   140  
   141  func (sb *ScrollBar) Paint() {
   142  	c := sb.TreeThemePaletteColor("scrollbar_bg")
   143  	imageutil.FillRectangle(sb.ctx.Image(), sb.Bounds, c)
   144  }
   145  
   146  //----------
   147  
   148  func (sb *ScrollBar) OnInputEvent(ev interface{}, p image.Point) event.Handled {
   149  	switch evt := ev.(type) {
   150  	case *event.MouseDown:
   151  		switch evt.Button {
   152  		case event.ButtonLeft:
   153  			sb.clicking = true
   154  			sb.setPressPad(&evt.Point)
   155  			sb.scrollToPoint(&evt.Point)
   156  			sb.MarkNeedsPaint() // in case it didn't move
   157  		case event.ButtonWheelUp:
   158  			sb.scrollPage(true)
   159  		case event.ButtonWheelDown:
   160  			sb.scrollPage(false)
   161  		}
   162  	case *event.MouseMove:
   163  		if sb.clicking {
   164  			sb.scrollToPoint(&evt.Point)
   165  		}
   166  	case *event.MouseUp:
   167  		if sb.clicking {
   168  			sb.clicking = false
   169  			sb.scrollToPoint(&evt.Point)
   170  			sb.MarkNeedsPaint() // in case it didn't move
   171  		}
   172  
   173  	case *event.MouseDragStart:
   174  		// take over from down/move/up to allow dragging outside bounds
   175  		sb.clicking = false
   176  
   177  		sb.dragging = true
   178  		sb.setPressPad(&evt.Point2)
   179  		sb.scrollToPoint(&evt.Point2)
   180  	case *event.MouseDragMove:
   181  		sb.scrollToPoint(&evt.Point)
   182  	case *event.MouseDragEnd:
   183  		sb.dragging = false
   184  		sb.scrollToPoint(&evt.Point)
   185  		sb.MarkNeedsPaint() // in case it didn't move
   186  	}
   187  	return false
   188  }
   189  
   190  func (sb *ScrollBar) setPressPad(p *image.Point) {
   191  	b := sb.Handle.Bounds
   192  	if p.In(b) {
   193  		// set position relative to the bar top-left
   194  		sb.pressPad.X = p.X - b.Min.X
   195  		sb.pressPad.Y = p.Y - b.Min.Y
   196  	} else {
   197  		// set position in the middle of the bar
   198  		sb.pressPad.X = b.Dx() / 2
   199  		sb.pressPad.Y = b.Dy() / 2
   200  	}
   201  }
   202  
   203  //----------
   204  
   205  func (sb *ScrollBar) yaxis(p image.Point) int {
   206  	if sb.Horizontal {
   207  		return p.X
   208  	} else {
   209  		return p.Y
   210  	}
   211  }
   212  func (sb *ScrollBar) yaxisPtr(p *image.Point) *int {
   213  	if sb.Horizontal {
   214  		return &p.X
   215  	} else {
   216  		return &p.Y
   217  	}
   218  }