From e45e391f6d50ee8446ef2382738f1c3b034e10e2 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 15 Sep 2023 01:47:58 -0400 Subject: [PATCH] Added ScrollContainer --- scrollbar.go | 8 ++- scrollcontainer.go | 167 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 scrollcontainer.go diff --git a/scrollbar.go b/scrollbar.go index 2f71248..826837f 100644 --- a/scrollbar.go +++ b/scrollbar.go @@ -278,7 +278,13 @@ func (this scrollbarLayout) Arrange (hints tomo.LayoutHints, boxes []tomo.Box) { // calculate handle length handleLength := gutterLength * this.viewportContentRatio() if handleLength < handleMin { handleLength = handleMin } - if handleLength > gutterLength { handleLength = gutterLength } + if handleLength >= gutterLength { + // just hide the handle if it isn't needed. + // TODO: we need a way to hide and show boxes, this is janky as + // fuck + boxes[0].SetBounds(image.Rect(-16, -16, 0, 0)) + return + } if this.vertical { handle.Max.Y = int(handleLength) } else { diff --git a/scrollcontainer.go b/scrollcontainer.go new file mode 100644 index 0000000..4255f61 --- /dev/null +++ b/scrollcontainer.go @@ -0,0 +1,167 @@ +package objects + +import "image" +import "git.tebibyte.media/tomo/tomo" +import "git.tebibyte.media/tomo/tomo/event" +import "git.tebibyte.media/tomo/tomo/theme" + +// ScrollSide determines which Scrollbars are active in a ScrollContainer. +type ScrollSide int; const ( + ScrollVertical ScrollSide = 1 << iota + ScrollHorizontal + ScrollBoth = ScrollVertical | ScrollHorizontal +) + +// Horizontal returns true if the side includes a horizontal bar. +func (sides ScrollSide) Horizontal () bool { + return sides & ScrollHorizontal > 0 +} + +// Vertical returns true if the side includes a vertical bar. +func (sides ScrollSide) Vertical () bool { + return sides & ScrollVertical > 0 +} + +// String returns one of: +// - both +// - horizontal +// - vertical +// - none +func (sides ScrollSide) String () string { + switch { + case sides.Horizontal() && sides.Vertical(): return "both" + case sides.Horizontal(): return "horizontal" + case sides.Vertical(): return "vertical" + default: return "none" + } +} + +// ScrollContainer couples a ContentBox with one or two Scrollbars. +type ScrollContainer struct { + tomo.ContainerBox + layout *scrollContainerLayout + + horizontalCookie event.Cookie + verticalCookie event.Cookie +} + +// NewScrollContainer creates a new scroll container. +func NewScrollContainer (sides ScrollSide) *ScrollContainer { + this := &ScrollContainer { + ContainerBox: tomo.NewContainerBox(), + layout: &scrollContainerLayout { }, + } + if sides.Vertical() { + this.layout.vertical = NewVerticalScrollbar() + this.Add(this.layout.vertical) + } + if sides.Horizontal() { + this.layout.horizontal = NewHorizontalScrollbar() + this.Add(this.layout.horizontal) + } + this.CaptureScroll(true) + this.OnScroll(this.handleScroll) + theme.Apply(this, theme.R("objects", "ScrollContainer", sides.String())) + this.SetLayout(this.layout) + return this +} + +// SetRoot sets the root child of the ScrollContainer. There can only be one at +// a time, and setting it will remove and unlink the current child if there is +// one. +func (this *ScrollContainer) SetRoot (root tomo.ContentBox) { + if this.layout.root != nil { + // delete root and close cookies + this.Delete(this.layout.root) + if this.horizontalCookie != nil { + this.horizontalCookie.Close() + this.horizontalCookie = nil + } + if this.verticalCookie != nil { + this.verticalCookie.Close() + this.verticalCookie = nil + } + } + this.layout.root = root + if root != nil { + // insert root at the beginning (for keynav) + switch { + case this.layout.vertical != nil: + this.Insert(root, this.layout.vertical) + case this.layout.horizontal != nil: + this.Insert(root, this.layout.horizontal) + default: + this.Add(root) + } + + // link root and remember cookies + if this.layout.horizontal != nil { + this.horizontalCookie = this.layout.horizontal.Link(root) + } + if this.layout.vertical != nil { + this.verticalCookie = this.layout.vertical.Link(root) + } + } +} + +func (this *ScrollContainer) handleScroll (x, y float64) { + if this.layout.root == nil { return } + this.layout.root.ScrollTo ( + this.layout.root.ContentBounds().Min. + Add(image.Pt(int(x), int(y)))) +} + +type scrollContainerLayout struct { + root tomo.ContentBox + horizontal *Scrollbar + vertical *Scrollbar +} + +func (this *scrollContainerLayout) MinimumSize (hints tomo.LayoutHints, boxes []tomo.Box) image.Point { + var minimum image.Point; if this.root != nil { + minimum = this.root.MinimumSize() + } + if this.horizontal != nil { + minimum.Y += this.horizontal.MinimumSize().Y + } + if this.vertical != nil { + minimum.X += this.vertical.MinimumSize().X + minimum.Y = max(minimum.Y, this.vertical.MinimumSize().Y) + } + if this.horizontal != nil { + minimum.X = max(minimum.X, this.horizontal.MinimumSize().X) + } + return minimum +} + +func (this *scrollContainerLayout) Arrange (hints tomo.LayoutHints, boxes []tomo.Box) { + rootBounds := hints.Bounds + if this.horizontal != nil { + rootBounds.Max.Y -= this.horizontal.MinimumSize().Y + } + if this.vertical != nil { + rootBounds.Max.X -= this.vertical.MinimumSize().X + } + if this.root != nil { + this.root.SetBounds(rootBounds) + } + if this.horizontal != nil { + this.horizontal.SetBounds(image.Rect ( + hints.Bounds.Min.X, + rootBounds.Max.Y, + rootBounds.Max.X, + hints.Bounds.Max.Y)) + } + if this.vertical != nil { + this.vertical.SetBounds(image.Rect ( + rootBounds.Max.X, + hints.Bounds.Min.Y, + hints.Bounds.Max.X, + rootBounds.Max.Y)) + } +} + +func max (x, y int) int { + if x > y { return x } + return y +}