Sasha Koshka
72f604e819
The repeated bool was removed and instead key release events are *only* sent when the key is actually let go. If an element wants to listen to repeat presses, it can just listen to press events.
349 lines
9.6 KiB
Go
349 lines
9.6 KiB
Go
package basic
|
|
|
|
import "image"
|
|
import "git.tebibyte.media/sashakoshka/tomo"
|
|
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
|
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
|
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
|
|
selected bool
|
|
|
|
child tomo.Scrollable
|
|
childWidth, childHeight int
|
|
|
|
horizontal struct {
|
|
exists bool
|
|
enabled bool
|
|
gutter image.Rectangle
|
|
bar image.Rectangle
|
|
}
|
|
|
|
vertical struct {
|
|
exists bool
|
|
enabled bool
|
|
gutter image.Rectangle
|
|
bar image.Rectangle
|
|
}
|
|
|
|
onSelectionRequest func () (granted bool)
|
|
onSelectionMotionRequest func (tomo.SelectionDirection) (granted bool)
|
|
}
|
|
|
|
// NewScrollContainer creates a new scroll container with the specified scroll
|
|
// bars.
|
|
func NewScrollContainer (horizontal, vertical bool) (element *ScrollContainer) {
|
|
element = &ScrollContainer { }
|
|
element.Core, element.core = core.NewCore(element)
|
|
element.updateMinimumSize()
|
|
element.horizontal.exists = horizontal
|
|
element.vertical.exists = vertical
|
|
return
|
|
}
|
|
|
|
// Resize resizes the scroll box.
|
|
func (element *ScrollContainer) Resize (width, height int) {
|
|
element.core.AllocateCanvas(width, height)
|
|
element.recalculate()
|
|
element.child.Resize (
|
|
element.childWidth,
|
|
element.childHeight)
|
|
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 tomo.Scrollable) {
|
|
// disown previous child if it exists
|
|
if element.child != nil {
|
|
element.clearChildEventHandlers(child)
|
|
}
|
|
|
|
// adopt new child
|
|
element.child = child
|
|
if child != nil {
|
|
child.OnDamage(element.childDamageCallback)
|
|
child.OnMinimumSizeChange(element.updateMinimumSize)
|
|
child.OnScrollBoundsChange(element.childScrollBoundsChangeCallback)
|
|
if newChild, ok := child.(tomo.Selectable); ok {
|
|
newChild.OnSelectionRequest (
|
|
element.childSelectionRequestCallback)
|
|
newChild.OnSelectionMotionRequest (
|
|
element.childSelectionMotionRequestCallback)
|
|
}
|
|
|
|
// TODO: somehow inform the core that we do not in fact want to
|
|
// redraw the element.
|
|
element.updateMinimumSize()
|
|
|
|
element.horizontal.enabled,
|
|
element.vertical.enabled = element.child.ScrollAxes()
|
|
|
|
if element.core.HasImage() {
|
|
element.child.Resize (
|
|
element.childWidth,
|
|
element.childHeight)
|
|
element.core.DamageAll()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (element *ScrollContainer) HandleKeyDown (key tomo.Key, modifiers tomo.Modifiers) {
|
|
if child, ok := element.child.(tomo.KeyboardTarget); ok {
|
|
child.HandleKeyDown(key, modifiers)
|
|
}
|
|
}
|
|
|
|
func (element *ScrollContainer) HandleKeyUp (key tomo.Key, modifiers tomo.Modifiers) {
|
|
if child, ok := element.child.(tomo.KeyboardTarget); ok {
|
|
child.HandleKeyUp(key, modifiers)
|
|
}
|
|
}
|
|
|
|
func (element *ScrollContainer) HandleMouseDown (x, y int, button tomo.Button) {
|
|
if child, ok := element.child.(tomo.MouseTarget); ok {
|
|
child.HandleMouseDown(x, y, button)
|
|
}
|
|
}
|
|
|
|
func (element *ScrollContainer) HandleMouseUp (x, y int, button tomo.Button) {
|
|
if child, ok := element.child.(tomo.MouseTarget); ok {
|
|
child.HandleMouseUp(x, y, button)
|
|
}
|
|
}
|
|
|
|
func (element *ScrollContainer) HandleMouseMove (x, y int) {
|
|
if child, ok := element.child.(tomo.MouseTarget); ok {
|
|
child.HandleMouseMove(x, y)
|
|
}
|
|
}
|
|
|
|
func (element *ScrollContainer) HandleMouseScroll (
|
|
x, y int,
|
|
deltaX, deltaY float64,
|
|
) {
|
|
scrollPoint := element.child.ScrollViewportBounds().Min.Add(image.Pt (
|
|
int(deltaX),
|
|
int(deltaY)))
|
|
element.child.ScrollTo(scrollPoint)
|
|
}
|
|
|
|
func (element *ScrollContainer) Selected () (selected bool) {
|
|
return element.selected
|
|
}
|
|
|
|
func (element *ScrollContainer) Select () {
|
|
if element.onSelectionRequest != nil {
|
|
element.onSelectionRequest()
|
|
}
|
|
}
|
|
|
|
func (element *ScrollContainer) HandleSelection (
|
|
direction tomo.SelectionDirection,
|
|
) (
|
|
accepted bool,
|
|
) {
|
|
if child, ok := element.child.(tomo.Selectable); ok {
|
|
element.selected = true
|
|
return child.HandleSelection(direction)
|
|
} else {
|
|
element.selected = false
|
|
return false
|
|
}
|
|
}
|
|
|
|
func (element *ScrollContainer) HandleDeselection () {
|
|
if child, ok := element.child.(tomo.Selectable); ok {
|
|
child.HandleDeselection()
|
|
}
|
|
element.selected = false
|
|
}
|
|
|
|
func (element *ScrollContainer) OnSelectionRequest (callback func () (granted bool)) {
|
|
element.onSelectionRequest = callback
|
|
}
|
|
|
|
func (element *ScrollContainer) OnSelectionMotionRequest (
|
|
callback func (direction tomo.SelectionDirection) (granted bool),
|
|
) {
|
|
element.onSelectionMotionRequest = callback
|
|
}
|
|
|
|
func (element *ScrollContainer) childDamageCallback (region tomo.Canvas) {
|
|
element.core.DamageRegion(artist.Paste(element, region, image.Point { }))
|
|
}
|
|
|
|
func (element *ScrollContainer) childSelectionRequestCallback () (granted bool) {
|
|
child, ok := element.child.(tomo.Selectable)
|
|
if !ok { return false }
|
|
if element.onSelectionRequest != nil && element.onSelectionRequest() {
|
|
child.HandleSelection(tomo.SelectionDirectionNeutral)
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
func (element *ScrollContainer) childSelectionMotionRequestCallback (
|
|
direction tomo.SelectionDirection,
|
|
) (
|
|
granted bool,
|
|
) {
|
|
if element.onSelectionMotionRequest == nil {
|
|
return
|
|
}
|
|
return element.onSelectionMotionRequest(direction)
|
|
}
|
|
|
|
func (element *ScrollContainer) clearChildEventHandlers (child tomo.Scrollable) {
|
|
child.OnDamage(nil)
|
|
child.OnMinimumSizeChange(nil)
|
|
child.OnScrollBoundsChange(nil)
|
|
if child0, ok := child.(tomo.Selectable); ok {
|
|
child0.OnSelectionRequest(nil)
|
|
child0.OnSelectionMotionRequest(nil)
|
|
if child0.Selected() {
|
|
child0.HandleDeselection()
|
|
}
|
|
}
|
|
if child0, ok := child.(tomo.Flexible); ok {
|
|
child0.OnFlexibleHeightChange(nil)
|
|
}
|
|
}
|
|
|
|
func (element *ScrollContainer) recalculate () {
|
|
horizontal := &element.horizontal
|
|
vertical := &element.vertical
|
|
bounds := element.Bounds()
|
|
thickness := theme.Padding() * 2
|
|
|
|
// 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.Y = bounds.Max.Y - thickness
|
|
horizontal.gutter.Max.X = bounds.Max.X
|
|
horizontal.gutter.Max.Y = bounds.Max.Y
|
|
if vertical.exists {
|
|
horizontal.gutter.Max.X -= thickness
|
|
}
|
|
element.childHeight -= thickness
|
|
}
|
|
if vertical.exists {
|
|
vertical.gutter.Min.X = bounds.Max.X - thickness
|
|
vertical.gutter.Max.X = bounds.Max.X
|
|
vertical.gutter.Max.Y = bounds.Max.Y
|
|
if horizontal.exists {
|
|
vertical.gutter.Max.Y -= thickness
|
|
}
|
|
element.childWidth -= thickness
|
|
}
|
|
|
|
// 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.gutter.Min.Y
|
|
horizontal.bar.Max.Y = horizontal.gutter.Max.Y
|
|
|
|
scale := float64(horizontal.gutter.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.gutter.Min.X
|
|
horizontal.bar.Max.X += horizontal.gutter.Min.X
|
|
}
|
|
if vertical.exists && vertical.enabled {
|
|
vertical.bar.Min.X = vertical.gutter.Min.X
|
|
vertical.bar.Max.X = vertical.gutter.Max.X
|
|
|
|
scale := float64(vertical.gutter.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.gutter.Min.Y
|
|
vertical.bar.Max.Y += vertical.gutter.Min.Y
|
|
}
|
|
|
|
// if the scroll bars are out of bounds, don't display them.
|
|
if horizontal.bar.Dx() >= horizontal.gutter.Dx() {
|
|
horizontal.bar = image.Rectangle { }
|
|
}
|
|
if vertical.bar.Dy() >= vertical.gutter.Dy() {
|
|
vertical.bar = image.Rectangle { }
|
|
}
|
|
}
|
|
|
|
func (element *ScrollContainer) draw () {
|
|
artist.Paste(element.core, element.child, image.Point { })
|
|
artist.FillRectangle (
|
|
element, theme.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 () {
|
|
artist.FillRectangle (
|
|
element,
|
|
theme.ScrollGutterPattern(true, element.horizontal.enabled),
|
|
element.horizontal.gutter)
|
|
artist.FillRectangle (
|
|
element,
|
|
theme.ScrollBarPattern(true, element.horizontal.enabled),
|
|
element.horizontal.bar)
|
|
}
|
|
|
|
func (element *ScrollContainer) drawVerticalBar () {
|
|
artist.FillRectangle (
|
|
element,
|
|
theme.ScrollGutterPattern(false, element.vertical.enabled),
|
|
element.vertical.gutter)
|
|
artist.FillRectangle (
|
|
element,
|
|
theme.ScrollBarPattern(false, element.vertical.enabled),
|
|
element.vertical.bar)
|
|
}
|
|
|
|
func (element *ScrollContainer) updateMinimumSize () {
|
|
width := theme.Padding() * 2
|
|
height := theme.Padding() * 2
|
|
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)
|
|
}
|
|
}
|