backend/internal/system/hierarchy.go

297 lines
6.6 KiB
Go

package system
import "image"
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/input"
import "git.tebibyte.media/tomo/tomo/canvas"
import "git.tebibyte.media/tomo/backend/internal/util"
// Hierarchy is coupled to a tomo.Window implementation, and manages a tree of
// Boxes.
type Hierarchy struct {
link WindowLink
system *System
canvas canvas.Canvas
root anyBox
focused anyBox
hovered anyBox
windowFocused bool
modifiers input.Modifiers
mousePosition image.Point
drags [10]anyBox
minimumSize image.Point
needMinimum util.Set[anyBox]
needLayout util.Set[anyBox]
needDraw util.Set[anyBox]
needRedo bool
minimumClean bool
}
// WindowLink allows the Hierarchy to call up into the tomo.Window
// implementation which contains it. This should be a separate entity from the
// tomo.Window.
type WindowLink interface {
// GetWindow returns the tomo.Window containing the Hierarchy.
GetWindow () tomo.Window
// PushRegion pushes a region of the held canvas.Canvas to the screen.
PushRegion (image.Rectangle)
// PushAll pushes the entire canvas.Canvas to the screen.
PushAll ()
// NotifyMinimumSizeChange notifies the tomo.Window that the minimum
// size of the Hierarchy has changed, and if necessary, a resize should
// be requested.
NotifyMinimumSizeChange ()
}
// NewHierarchy creates a new Hierarchy.
func (this *System) NewHierarchy (link WindowLink) *Hierarchy {
hierarchy := &Hierarchy {
system: this,
link: link,
}
return hierarchy
}
// SetRoot sets the root Box of the hierarchy.
func (this *Hierarchy) SetRoot (root tomo.Box) {
if this.root != nil {
this.root.setParent(nil)
}
if root == nil {
this.root = nil
} else {
box := assertAnyBox(root.GetBox())
box.setParent(this)
box.flushActionQueue()
this.invalidateLayout(box)
this.root = box
}
this.minimumClean = false
}
// Empty returns whether or not the root of the Hierarchy is nil.
func (this *Hierarchy) Empty () bool {
return this.root == nil
}
// SetCanvas sets the held canvas of the Hierarchy that all boxes within it will
// draw to. The Hierarchy will use the canvas.Canvas's bounds to lay itself out.
func (this *Hierarchy) SetCanvas (can canvas.Canvas) {
this.canvas = can
if this.root != nil { this.root.loseCanvas() }
this.needRedo = true
}
// MinimumSize returns the minimum size of the Hierarchy.
func (this *Hierarchy) MinimumSize () image.Point {
return this.minimumSize
}
// AfterEvent should be called at the end of every event cycle.
func (this *Hierarchy) AfterEvent () {
if this.canvas == nil { return }
if this.needRedo {
// set child bounds
childBounds := this.canvas.Bounds()
childBounds = childBounds.Sub(childBounds.Min)
if this.root != nil {
this.root.SetBounds(childBounds)
}
// full relayout/redraw
if this.root != nil {
this.root.recursiveRedo()
}
this.link.PushAll()
this.needRedo = false
return
}
for len(this.needMinimum) > 0 {
this.needMinimum.Pop().doMinimumSize()
}
if !this.minimumClean {
this.doMinimumSize()
}
for len(this.needLayout) > 0 {
this.needLayout.Pop().doLayout()
}
var toPush image.Rectangle
for len(this.needDraw) > 0 {
box := this.needDraw.Pop()
box.doDraw()
toPush = toPush.Union(box.Bounds())
}
if !toPush.Empty() {
this.link.PushRegion(toPush)
}
}
func (this *Hierarchy) getHierarchy () *Hierarchy {
return this
}
func (this *Hierarchy) getWindow () tomo.Window {
return this.link.GetWindow()
}
func (this *Hierarchy) getCanvas () canvas.Canvas {
return this.canvas
}
func (this *Hierarchy) getModifiers () input.Modifiers {
return this.modifiers
}
func (this *Hierarchy) getMousePosition () image.Point {
return this.mousePosition
}
func (this *Hierarchy) notifyMinimumSizeChange (anyBox) {
this.minimumClean = false
}
func (this *Hierarchy) invalidateMinimum (box anyBox) {
this.needMinimum.Add(box)
}
func (this *Hierarchy) invalidateDraw (box anyBox) {
this.needDraw.Add(box)
}
func (this *Hierarchy) invalidateLayout (box anyBox) {
this.needLayout.Add(box)
this.invalidateDraw(box)
}
func (this *Hierarchy) focus (box anyBox) {
if this.focused == box { return }
previous := this.focused
this.focused = box
if previous != nil {
previous.handleFocusLeave()
}
if box != nil && box.canBeFocused() {
box.handleFocusEnter()
}
}
func (this *Hierarchy) isFocused (box anyBox) bool {
return this.focused == box
}
func (this *Hierarchy) hover (box anyBox) {
if this.hovered == box { return }
previous := this.hovered
this.hovered = box
if previous != nil {
previous.handleMouseLeave()
}
if box != nil {
box.handleMouseEnter()
}
}
func (this *Hierarchy) anyFocused () bool {
return this.focused != nil
}
func (this *Hierarchy) boxUnder (point image.Point, category eventCategory) anyBox {
if this.root == nil { return nil }
return this.root.boxUnder(point, category)
}
func (this *Hierarchy) captures (eventCategory) bool {
return false
}
func (this *Hierarchy) keyboardTarget () anyBox {
focused := this.focused
if focused == nil { return nil }
parent := focused.getParent()
for {
parentBox, ok := parent.(anyBox)
if !ok { break }
if parent.captures(eventCategoryKeyboard) {
return parentBox
}
parent = parentBox.getParent()
}
return focused
}
func (this *Hierarchy) focusNext () {
found := !this.anyFocused()
focused := false
this.propagateAlt (func (box anyBox) bool {
if found {
// looking for the next box to select
if box.canBeFocused() {
// found it
this.focus(box)
focused = true
return false
}
} else {
// looking for the current focused element
if box == this.focused {
// found it
found = true
}
}
return true
})
if !focused { this.focus(nil) }
}
func (this *Hierarchy) focusPrevious () {
var behind anyBox
this.propagate (func (box anyBox) bool {
if box == this.focused {
return false
}
if box.canBeFocused() { behind = box }
return true
})
this.focus(behind)
}
func (this *Hierarchy) propagate (callback func (box anyBox) bool) {
if this.root == nil { return }
this.root.propagate(callback)
}
func (this *Hierarchy) propagateAlt (callback func (box anyBox) bool) {
if this.root == nil { return }
this.root.propagateAlt(callback)
}
func (this *Hierarchy) drawBackgroundPart (canvas.Canvas) {
// TODO
// no-op for now? maybe eventually windows will be able to have a
// background
// if so, windows should be transparent if the color has transparency
}
func (this *Hierarchy) doMinimumSize () {
this.minimumClean = true
this.minimumSize = image.Point { }
if this.root != nil {
this.minimumSize = this.root.MinimumSize()
}
this.link.NotifyMinimumSizeChange()
}