From 677dca1dbf52cef0b59cc94d380678e04c5940c4 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sat, 11 Mar 2023 00:21:54 -0500 Subject: [PATCH] ScrollContainer uses ScrollBar for scrolling --- elements/basic/scrollbar.go | 4 +- elements/basic/scrollcontainer.go | 664 +++++++++++------------------- 2 files changed, 234 insertions(+), 434 deletions(-) diff --git a/elements/basic/scrollbar.go b/elements/basic/scrollbar.go index 74ede7e..3c2c16d 100644 --- a/elements/basic/scrollbar.go +++ b/elements/basic/scrollbar.go @@ -274,8 +274,8 @@ func (element *ScrollBar) recalculateHorizontal () { element.bar.Max.Y = element.track.Max.Y ratio := - float64(element.track.Dy()) / - float64(contentBounds.Dy()) + float64(element.track.Dx()) / + float64(contentBounds.Dx()) element.bar.Min.X = int(float64(viewportBounds.Min.X) * ratio) element.bar.Max.X = int(float64(viewportBounds.Max.X) * ratio) diff --git a/elements/basic/scrollcontainer.go b/elements/basic/scrollcontainer.go index c686878..cbc53ce 100644 --- a/elements/basic/scrollcontainer.go +++ b/elements/basic/scrollcontainer.go @@ -5,7 +5,6 @@ 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" @@ -13,33 +12,12 @@ import "git.tebibyte.media/sashakoshka/tomo/elements/core" // element. type ScrollContainer struct { *core.Core + *core.Propagator 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 - } + child elements.Scrollable + horizontal *ScrollBar + vertical *ScrollBar config config.Wrapped theme theme.Wrapped @@ -53,20 +31,40 @@ type ScrollContainer struct { 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 + element.Core, element.core = core.NewCore(element.redoAll) + element.Propagator = core.NewPropagator(element) + + if horizontal { + element.horizontal = NewScrollBar(false) + element.setChildEventHandlers(element.horizontal) + element.horizontal.OnScroll (func (viewport image.Point) { + if element.child != nil { + element.child.ScrollTo(viewport) + } + if element.vertical != nil { + element.vertical.SetBounds ( + element.child.ScrollContentBounds(), + element.child.ScrollViewportBounds()) + } + }) + } + if vertical { + element.vertical = NewScrollBar(true) + element.setChildEventHandlers(element.vertical) + element.vertical.OnScroll (func (viewport image.Point) { + if element.child != nil { + element.child.ScrollTo(viewport) + } + if element.horizontal != nil { + element.horizontal.SetBounds ( + element.child.ScrollContentBounds(), + element.child.ScrollViewportBounds()) + } + }) + } 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 @@ -80,247 +78,45 @@ func (element *ScrollContainer) Adopt (child elements.Scrollable) { // 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.setChildEventHandlers(child) + } + + element.updateEnabled() + element.updateMinimumSize() + if element.core.HasImage() { + element.redoAll() + element.core.DamageAll() + } +} +func (element *ScrollContainer) setChildEventHandlers (child elements.Element) { + 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 (func (region canvas.Canvas) { + element.core.DamageRegion(region.Bounds()) + }) + child.OnMinimumSizeChange (func () { element.updateMinimumSize() - - element.horizontal.enabled, - element.vertical.enabled = element.child.ScrollAxes() - - if element.core.HasImage() { - element.resizeChildToFit() - } + element.redoAll() + element.core.DamageAll() + }) + if child0, ok := child.(elements.Focusable); ok { + child0.OnFocusRequest (func () (granted bool) { + return element.childFocusRequestCallback(child0) + }) + child0.OnFocusMotionRequest ( + func (direction input.KeynavDirection) (granted bool) { + if element.onFocusMotionRequest == nil { return } + return element.onFocusMotionRequest(direction) + }) } -} - -// 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 child0, ok := child.(elements.Scrollable); ok { + child0.OnScrollBoundsChange(element.childScrollBoundsChangeCallback) } - 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) { @@ -340,189 +136,193 @@ func (element *ScrollContainer) clearChildEventHandlers (child elements.Scrollab } } -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)) +// SetTheme sets the element's theme. +func (element *ScrollContainer) SetTheme (new theme.Theme) { + if new == element.theme.Theme { return } + element.theme.Theme = new + element.Propagator.SetTheme(new) + element.updateMinimumSize() + element.redoAll() } -func (element *ScrollContainer) recalculate () { - horizontal := &element.horizontal - vertical := &element.vertical +// SetConfig sets the element's configuration. +func (element *ScrollContainer) SetConfig (new config.Config) { + if new == element.config.Config { return } + element.Propagator.SetConfig(new) + element.updateMinimumSize() + element.redoAll() +} + +func (element *ScrollContainer) HandleMouseScroll ( + x, y int, + deltaX, deltaY float64, +) { + element.scrollChildBy(int(deltaX), int(deltaY)) +} + +func (element *ScrollContainer) OnFocusRequest (callback func () (granted bool)) { + element.onFocusRequest = callback + element.Propagator.OnFocusRequest(callback) +} + +func (element *ScrollContainer) OnFocusMotionRequest ( + callback func (direction input.KeynavDirection) (granted bool), +) { + element.onFocusMotionRequest = callback + element.Propagator.OnFocusMotionRequest(callback) +} + +// CountChildren returns the amount of children contained within this element. +func (element *ScrollContainer) CountChildren () (count int) { + return 3 +} + +// Child returns the child at the specified index. If the index is out of +// bounds, this method will return nil. +func (element *ScrollContainer) Child (index int) (child elements.Element) { + switch index { + case 0: return element.child + case 1: + if element.horizontal == nil { + return nil + } else { + return element.horizontal + } + case 2: + if element.vertical == nil { + return nil + } else { + return element.vertical + } + default: return nil + } +} + +func (element *ScrollContainer) redoAll () { + if !element.core.HasImage() { return } + + if element.child != nil { element.child.DrawTo(nil) } + if element.horizontal != nil { element.horizontal.DrawTo(nil) } + if element.vertical != nil { element.vertical.DrawTo(nil) } - 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) + childBounds, horizontalBounds, verticalBounds := element.layout() + if element.child != nil { + element.child.DrawTo(canvas.Cut(element.core, childBounds)) } - 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 element.horizontal != nil { + element.horizontal.DrawTo(canvas.Cut(element.core, horizontalBounds)) } + if element.vertical != nil { + element.vertical.DrawTo(canvas.Cut(element.core, verticalBounds)) + } + element.draw() +} - // 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 +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) +} - 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 +func (element *ScrollContainer) childFocusRequestCallback ( + child elements.Focusable, +) ( + granted bool, +) { + if element.onFocusRequest != nil && element.onFocusRequest() { + element.Propagator.HandleUnfocus() + element.Propagator.HandleFocus(input.KeynavDirectionNeutral) + return true + } else { + return false } - 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 - } +func (element *ScrollContainer) layout () ( + child image.Rectangle, + horizontal image.Rectangle, + vertical image.Rectangle, +) { + bounds := element.Bounds() + child = bounds - // if the scroll bars are out of bounds, don't display them. - if horizontal.bar.Dx() >= horizontal.track.Dx() { - horizontal.bar = image.Rectangle { } + if element.horizontal != nil { + _, hMinHeight := element.horizontal.MinimumSize() + child.Max.Y -= hMinHeight } - if vertical.bar.Dy() >= vertical.track.Dy() { - vertical.bar = image.Rectangle { } + if element.vertical != nil { + vMinWidth, _ := element.vertical.MinimumSize() + child.Max.X -= vMinWidth } + + vertical.Min.X = child.Max.X + vertical.Max.X = bounds.Max.X + vertical.Min.Y = bounds.Min.Y + vertical.Max.Y = child.Max.Y + + horizontal.Min.X = bounds.Min.X + horizontal.Max.X = child.Max.X + horizontal.Min.Y = child.Max.Y + horizontal.Max.Y = bounds.Max.Y + return } 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, + // XOR + if element.horizontal != nil && element.vertical != nil { + bounds := element.Bounds() + bounds.Min = image.Pt ( + bounds.Max.X - element.vertical.Bounds().Dx(), + bounds.Max.Y - element.horizontal.Bounds().Dy()) + state := theme.State { } + deadArea := element.theme.Pattern(theme.PatternDead, state) + deadArea.Draw(canvas.Cut(element.core, bounds), bounds) } - 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) + var width, height int - 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 + width, height = element.child.MinimumSize() + } + if element.horizontal != nil { + hMinWidth, hMinHeight := element.horizontal.MinimumSize() + height += hMinHeight + if hMinWidth > width { + width = hMinWidth + } + } + if element.vertical != nil { + vMinWidth, vMinHeight := element.vertical.MinimumSize() + width += vMinWidth + if vMinHeight > height { + height = vMinHeight + } } 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) + element.updateEnabled() + viewportBounds := element.child.ScrollViewportBounds() + contentBounds := element.child.ScrollContentBounds() + if element.horizontal != nil { + element.horizontal.SetBounds(contentBounds, viewportBounds) + } + if element.vertical != nil { + element.vertical.SetBounds(contentBounds, viewportBounds) + } +} + +func (element *ScrollContainer) updateEnabled () { + horizontal, vertical := element.child.ScrollAxes() + if element.horizontal != nil { + element.horizontal.SetEnabled(horizontal) + } + if element.vertical != nil { + element.vertical.SetEnabled(vertical) } }