This repository has been archived on 2023-08-08. You can view files and clone it, but cannot push or open issues or pull requests.
tomo-old/elements/basic/scrollcontainer.go

529 lines
15 KiB
Go

package basicElements
import "image"
import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo/theme"
import "git.tebibyte.media/sashakoshka/tomo/config"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
// ScrollContainer is a container that is capable of holding a scrollable
// element.
type ScrollContainer struct {
*core.Core
core core.CoreControl
focused bool
child elements.Scrollable
childWidth, childHeight int
horizontal struct {
theme theme.Wrapped
exists bool
enabled bool
dragging bool
dragOffset int
gutter image.Rectangle
track image.Rectangle
bar image.Rectangle
}
vertical struct {
theme theme.Wrapped
exists bool
enabled bool
dragging bool
dragOffset int
gutter image.Rectangle
track image.Rectangle
bar image.Rectangle
}
config config.Wrapped
theme theme.Wrapped
onFocusRequest func () (granted bool)
onFocusMotionRequest func (input.KeynavDirection) (granted bool)
}
// NewScrollContainer creates a new scroll container with the specified scroll
// bars.
func NewScrollContainer (horizontal, vertical bool) (element *ScrollContainer) {
element = &ScrollContainer { }
element.theme.Case = theme.C("basic", "scrollContainer")
element.horizontal.theme.Case = theme.C("basic", "scrollBarHorizontal")
element.vertical.theme.Case = theme.C("basic", "scrollBarVertical")
element.Core, element.core = core.NewCore(element.handleResize)
element.horizontal.exists = horizontal
element.vertical.exists = vertical
return
}
func (element *ScrollContainer) handleResize () {
element.recalculate()
element.resizeChildToFit()
element.draw()
}
// Adopt adds a scrollable element to the scroll container. The container can
// only contain one scrollable element at a time, and when a new one is adopted
// it replaces the last one.
func (element *ScrollContainer) Adopt (child elements.Scrollable) {
// disown previous child if it exists
if element.child != nil {
element.clearChildEventHandlers(child)
}
// adopt new child
element.child = child
if child != nil {
if child0, ok := child.(elements.Themeable); ok {
child0.SetTheme(element.theme.Theme)
}
if child0, ok := child.(elements.Configurable); ok {
child0.SetConfig(element.config.Config)
}
child.OnDamage(element.childDamageCallback)
child.OnMinimumSizeChange(element.updateMinimumSize)
child.OnScrollBoundsChange(element.childScrollBoundsChangeCallback)
if newChild, ok := child.(elements.Focusable); ok {
newChild.OnFocusRequest (
element.childFocusRequestCallback)
newChild.OnFocusMotionRequest (
element.childFocusMotionRequestCallback)
}
element.updateMinimumSize()
element.horizontal.enabled,
element.vertical.enabled = element.child.ScrollAxes()
if element.core.HasImage() {
element.resizeChildToFit()
}
}
}
// SetTheme sets the element's theme.
func (element *ScrollContainer) SetTheme (new theme.Theme) {
if new == element.theme.Theme { return }
element.theme.Theme = new
if child, ok := element.child.(elements.Themeable); ok {
child.SetTheme(element.theme.Theme)
}
if element.core.HasImage() {
element.recalculate()
element.resizeChildToFit()
element.draw()
}
}
// SetConfig sets the element's configuration.
func (element *ScrollContainer) SetConfig (new config.Config) {
if new == element.config.Config { return }
element.config.Config = new
if child, ok := element.child.(elements.Configurable); ok {
child.SetConfig(element.config.Config)
}
if element.core.HasImage() {
element.recalculate()
element.resizeChildToFit()
element.draw()
}
}
func (element *ScrollContainer) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
if child, ok := element.child.(elements.KeyboardTarget); ok {
child.HandleKeyDown(key, modifiers)
}
}
func (element *ScrollContainer) HandleKeyUp (key input.Key, modifiers input.Modifiers) {
if child, ok := element.child.(elements.KeyboardTarget); ok {
child.HandleKeyUp(key, modifiers)
}
}
func (element *ScrollContainer) HandleMouseDown (x, y int, button input.Button) {
velocity := element.config.ScrollVelocity()
point := image.Pt(x, y)
if point.In(element.horizontal.bar) {
element.horizontal.dragging = true
element.horizontal.dragOffset =
x - element.horizontal.bar.Min.X +
element.Bounds().Min.X
element.dragHorizontalBar(point)
} else if point.In(element.horizontal.gutter) {
switch button {
case input.ButtonLeft:
element.horizontal.dragging = true
element.horizontal.dragOffset =
element.horizontal.bar.Dx() / 2 +
element.Bounds().Min.X
element.dragHorizontalBar(point)
case input.ButtonMiddle:
viewport := element.child.ScrollViewportBounds().Dx()
if x > element.horizontal.bar.Min.X {
element.scrollChildBy(viewport, 0)
} else {
element.scrollChildBy(-viewport, 0)
}
case input.ButtonRight:
if x > element.horizontal.bar.Min.X {
element.scrollChildBy(velocity, 0)
} else {
element.scrollChildBy(-velocity, 0)
}
}
} else if point.In(element.vertical.bar) {
element.vertical.dragging = true
element.vertical.dragOffset =
y - element.vertical.bar.Min.Y +
element.Bounds().Min.Y
element.dragVerticalBar(point)
} else if point.In(element.vertical.gutter) {
switch button {
case input.ButtonLeft:
element.vertical.dragging = true
element.vertical.dragOffset =
element.vertical.bar.Dy() / 2 +
element.Bounds().Min.Y
element.dragVerticalBar(point)
case input.ButtonMiddle:
viewport := element.child.ScrollViewportBounds().Dy()
if y > element.vertical.bar.Min.Y {
element.scrollChildBy(0, viewport)
} else {
element.scrollChildBy(0, -viewport)
}
case input.ButtonRight:
if y > element.vertical.bar.Min.Y {
element.scrollChildBy(0, velocity)
} else {
element.scrollChildBy(0, -velocity)
}
}
} else if child, ok := element.child.(elements.MouseTarget); ok {
child.HandleMouseDown(x, y, button)
}
}
func (element *ScrollContainer) HandleMouseUp (x, y int, button input.Button) {
if element.horizontal.dragging {
element.horizontal.dragging = false
element.drawHorizontalBar()
element.core.DamageRegion(element.horizontal.bar)
} else if element.vertical.dragging {
element.vertical.dragging = false
element.drawVerticalBar()
element.core.DamageRegion(element.vertical.bar)
} else if child, ok := element.child.(elements.MouseTarget); ok {
child.HandleMouseUp(x, y, button)
}
}
func (element *ScrollContainer) HandleMouseMove (x, y int) {
if element.horizontal.dragging {
element.dragHorizontalBar(image.Pt(x, y))
} else if element.vertical.dragging {
element.dragVerticalBar(image.Pt(x, y))
} else if child, ok := element.child.(elements.MouseTarget); ok {
child.HandleMouseMove(x, y)
}
}
func (element *ScrollContainer) HandleMouseScroll (
x, y int,
deltaX, deltaY float64,
) {
element.scrollChildBy(int(deltaX), int(deltaY))
}
func (element *ScrollContainer) scrollChildBy (x, y int) {
if element.child == nil { return }
scrollPoint :=
element.child.ScrollViewportBounds().Min.
Add(image.Pt(x, y))
element.child.ScrollTo(scrollPoint)
}
func (element *ScrollContainer) Focused () (focused bool) {
return element.focused
}
func (element *ScrollContainer) Focus () {
if element.onFocusRequest != nil {
if element.onFocusRequest() {
element.focused = true
}
}
}
func (element *ScrollContainer) HandleFocus (
direction input.KeynavDirection,
) (
accepted bool,
) {
if child, ok := element.child.(elements.Focusable); ok {
element.focused = child.HandleFocus(direction)
return element.focused
} else {
element.focused = false
return false
}
}
func (element *ScrollContainer) HandleUnfocus () {
if child, ok := element.child.(elements.Focusable); ok {
child.HandleUnfocus()
}
element.focused = false
}
func (element *ScrollContainer) OnFocusRequest (callback func () (granted bool)) {
element.onFocusRequest = callback
}
func (element *ScrollContainer) OnFocusMotionRequest (
callback func (direction input.KeynavDirection) (granted bool),
) {
element.onFocusMotionRequest = callback
}
func (element *ScrollContainer) childDamageCallback (region canvas.Canvas) {
element.core.DamageRegion(region.Bounds())
}
func (element *ScrollContainer) childFocusRequestCallback () (granted bool) {
if element.onFocusRequest != nil {
element.focused = element.onFocusRequest()
return element.focused
} else {
return false
}
}
func (element *ScrollContainer) childFocusMotionRequestCallback (
direction input.KeynavDirection,
) (
granted bool,
) {
if element.onFocusMotionRequest == nil { return }
return element.onFocusMotionRequest(direction)
}
func (element *ScrollContainer) clearChildEventHandlers (child elements.Scrollable) {
child.DrawTo(nil)
child.OnDamage(nil)
child.OnMinimumSizeChange(nil)
child.OnScrollBoundsChange(nil)
if child0, ok := child.(elements.Focusable); ok {
child0.OnFocusRequest(nil)
child0.OnFocusMotionRequest(nil)
if child0.Focused() {
child0.HandleUnfocus()
}
}
if child0, ok := child.(elements.Flexible); ok {
child0.OnFlexibleHeightChange(nil)
}
}
func (element *ScrollContainer) resizeChildToFit () {
childBounds := image.Rect (
0, 0,
element.childWidth,
element.childHeight).Add(element.Bounds().Min)
element.child.DrawTo(canvas.Cut(element.core, childBounds))
}
func (element *ScrollContainer) recalculate () {
horizontal := &element.horizontal
vertical := &element.vertical
gutterInsetHorizontal := horizontal.theme.Padding(theme.PatternGutter)
gutterInsetVertical := vertical.theme.Padding(theme.PatternGutter)
bounds := element.Bounds()
thicknessHorizontal :=
element.config.HandleWidth() +
gutterInsetHorizontal[3] +
gutterInsetHorizontal[1]
thicknessVertical :=
element.config.HandleWidth() +
gutterInsetVertical[3] +
gutterInsetVertical[1]
// calculate child size
element.childWidth = bounds.Dx()
element.childHeight = bounds.Dy()
// reset bounds
horizontal.gutter = image.Rectangle { }
vertical.gutter = image.Rectangle { }
horizontal.bar = image.Rectangle { }
vertical.bar = image.Rectangle { }
// if enabled, give substance to the gutters
if horizontal.exists {
horizontal.gutter.Min.X = bounds.Min.X
horizontal.gutter.Min.Y = bounds.Max.Y - thicknessHorizontal
horizontal.gutter.Max.X = bounds.Max.X
horizontal.gutter.Max.Y = bounds.Max.Y
if vertical.exists {
horizontal.gutter.Max.X -= thicknessVertical
}
element.childHeight -= thicknessHorizontal
horizontal.track = gutterInsetHorizontal.Apply(horizontal.gutter)
}
if vertical.exists {
vertical.gutter.Min.X = bounds.Max.X - thicknessVertical
vertical.gutter.Max.X = bounds.Max.X
vertical.gutter.Min.Y = bounds.Min.Y
vertical.gutter.Max.Y = bounds.Max.Y
if horizontal.exists {
vertical.gutter.Max.Y -= thicknessHorizontal
}
element.childWidth -= thicknessVertical
vertical.track = gutterInsetVertical.Apply(vertical.gutter)
}
// if enabled, calculate the positions of the bars
contentBounds := element.child.ScrollContentBounds()
viewportBounds := element.child.ScrollViewportBounds()
if horizontal.exists && horizontal.enabled {
horizontal.bar.Min.Y = horizontal.track.Min.Y
horizontal.bar.Max.Y = horizontal.track.Max.Y
scale := float64(horizontal.track.Dx()) /
float64(contentBounds.Dx())
horizontal.bar.Min.X = int(float64(viewportBounds.Min.X) * scale)
horizontal.bar.Max.X = int(float64(viewportBounds.Max.X) * scale)
horizontal.bar.Min.X += horizontal.track.Min.X
horizontal.bar.Max.X += horizontal.track.Min.X
}
if vertical.exists && vertical.enabled {
vertical.bar.Min.X = vertical.track.Min.X
vertical.bar.Max.X = vertical.track.Max.X
scale := float64(vertical.track.Dy()) /
float64(contentBounds.Dy())
vertical.bar.Min.Y = int(float64(viewportBounds.Min.Y) * scale)
vertical.bar.Max.Y = int(float64(viewportBounds.Max.Y) * scale)
vertical.bar.Min.Y += vertical.track.Min.Y
vertical.bar.Max.Y += vertical.track.Min.Y
}
// if the scroll bars are out of bounds, don't display them.
if horizontal.bar.Dx() >= horizontal.track.Dx() {
horizontal.bar = image.Rectangle { }
}
if vertical.bar.Dy() >= vertical.track.Dy() {
vertical.bar = image.Rectangle { }
}
}
func (element *ScrollContainer) draw () {
deadPattern := element.theme.Pattern (
theme.PatternDead, theme.State { })
artist.DrawBounds (
element.core, deadPattern,
image.Rect (
element.vertical.gutter.Min.X,
element.horizontal.gutter.Min.Y,
element.vertical.gutter.Max.X,
element.horizontal.gutter.Max.Y))
element.drawHorizontalBar()
element.drawVerticalBar()
}
func (element *ScrollContainer) drawHorizontalBar () {
state := theme.State {
Disabled: !element.horizontal.enabled,
Pressed: element.horizontal.dragging,
}
gutterPattern := element.horizontal.theme.Pattern(theme.PatternGutter, state)
artist.DrawBounds(element.core, gutterPattern, element.horizontal.gutter)
handlePattern := element.horizontal.theme.Pattern(theme.PatternHandle, state)
artist.DrawBounds(element.core, handlePattern, element.horizontal.bar)
}
func (element *ScrollContainer) drawVerticalBar () {
state := theme.State {
Disabled: !element.vertical.enabled,
Pressed: element.vertical.dragging,
}
gutterPattern := element.vertical.theme.Pattern(theme.PatternGutter, state)
artist.DrawBounds(element.core, gutterPattern, element.vertical.gutter)
handlePattern := element.vertical.theme.Pattern(theme.PatternHandle, state)
artist.DrawBounds(element.core, handlePattern, element.vertical.bar)
}
func (element *ScrollContainer) dragHorizontalBar (mousePosition image.Point) {
scrollX :=
float64(element.child.ScrollContentBounds().Dx()) /
float64(element.horizontal.track.Dx()) *
float64(mousePosition.X - element.horizontal.dragOffset)
scrollY := element.child.ScrollViewportBounds().Min.Y
element.child.ScrollTo(image.Pt(int(scrollX), scrollY))
}
func (element *ScrollContainer) dragVerticalBar (mousePosition image.Point) {
scrollY :=
float64(element.child.ScrollContentBounds().Dy()) /
float64(element.vertical.track.Dy()) *
float64(mousePosition.Y - element.vertical.dragOffset)
scrollX := element.child.ScrollViewportBounds().Min.X
element.child.ScrollTo(image.Pt(scrollX, int(scrollY)))
}
func (element *ScrollContainer) updateMinimumSize () {
gutterInsetHorizontal := element.horizontal.theme.Padding(theme.PatternGutter)
gutterInsetVertical := element.vertical.theme.Padding(theme.PatternGutter)
thicknessHorizontal :=
element.config.HandleWidth() +
gutterInsetHorizontal[3] +
gutterInsetHorizontal[1]
thicknessVertical :=
element.config.HandleWidth() +
gutterInsetVertical[3] +
gutterInsetVertical[1]
width := thicknessHorizontal
height := thicknessVertical
if element.child != nil {
childWidth, childHeight := element.child.MinimumSize()
width += childWidth
height += childHeight
}
element.core.SetMinimumSize(width, height)
}
func (element *ScrollContainer) childScrollBoundsChangeCallback () {
element.horizontal.enabled,
element.vertical.enabled = element.child.ScrollAxes()
if element.core.HasImage() {
element.recalculate()
element.drawHorizontalBar()
element.drawVerticalBar()
element.core.DamageRegion(element.horizontal.gutter)
element.core.DamageRegion(element.vertical.gutter)
}
}