backend/internal/system/hierarchy.go

392 lines
8.9 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/style"
import "git.tebibyte.media/sashakoshka/goutil/container"
// 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
needStyle ucontainer.Set[anyBox]
needLayout ucontainer.Set[anyBox]
needDraw ucontainer.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,
needStyle: make(ucontainer.Set[anyBox]),
needLayout: make(ucontainer.Set[anyBox]),
needDraw: make(ucontainer.Set[anyBox]),
}
this.hierarchies.Add(hierarchy)
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.recursiveLoseCanvas() }
this.needRedo = true
}
// MinimumSize returns the minimum size of the Hierarchy.
func (this *Hierarchy) MinimumSize () image.Point {
return this.minimumSize
}
// Modifiers returns the current modifier keys being held.
func (this *Hierarchy) Modifiers () input.Modifiers {
return this.modifiers
}
// MousePosition returns the current mouse position.
func (this *Hierarchy) MousePosition () image.Point {
return this.mousePosition
}
// 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.needStyle) > 0 {
this.needStyle.Pop().doStyle()
}
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)
}
}
// Close closes the Hierarchy. This should be called when the Window that
// contains it has been closed.
func (this *Hierarchy) Close () {
this.system.removeHierarchy(this)
}
func (this *Hierarchy) setStyle () {
if this.root != nil { this.root.recursiveReApply() }
}
func (this *Hierarchy) setIconSet () {
if this.root != nil { this.root.recursiveReApply() }
}
func (this *Hierarchy) getHierarchy () *Hierarchy {
return this
}
func (this *Hierarchy) getWindow () tomo.Window {
return this.link.GetWindow()
}
func (this *Hierarchy) getStyle () *style.Style {
return this.system.style
}
func (this *Hierarchy) getIconSet () style.IconSet {
return this.system.iconSet
}
func (this *Hierarchy) getFaceSet () style.FaceSet {
return this.system.faceSet
}
func (this *Hierarchy) getStyleNonce () int {
return this.system.styleNonce
}
func (this *Hierarchy) getIconSetNonce () int {
return this.system.iconSetNonce
}
func (this *Hierarchy) getCanvas () canvas.Canvas {
return this.canvas
}
func (this *Hierarchy) getInnerClippingBounds () image.Rectangle {
return this.canvas.Bounds()
}
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) invalidateStyle (box anyBox) {
this.needStyle.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) masks () bool {
return false
}
func (this *Hierarchy) boxUnder (point image.Point) anyBox {
if this.root == nil { return nil }
return this.root.boxUnder(point)
}
func (this *Hierarchy) parents (box anyBox) func (func (anyBox) bool) {
return func (yield func (anyBox) bool) {
for box != nil && yield(box) {
parent, ok := box.getParent().(anyBox)
if !ok { break }
box = parent
}
}
}
func (this *Hierarchy) boxesUnder (point image.Point) func (func (anyBox) bool) {
return this.parents(this.boxUnder(point))
}
func (this *Hierarchy) keyboardTargets (yield func (anyBox) bool) {
focused := this.focused
if focused == nil { return }
this.parents(this.considerMaskingParents(focused))(yield)
}
func (this *Hierarchy) considerMaskingParents (box anyBox) anyBox {
parent := box.getParent()
for {
parentBox, ok := parent.(anyBox)
if !ok { break }
if parent.masks() {
return parentBox
}
parent = parentBox.getParent()
}
return box
}
func (this *Hierarchy) isMasked (box anyBox) bool {
parent := box.getParent()
for {
parentBox, ok := parent.(anyBox)
if !ok { break }
if parent.masks() {
return true
}
parent = parentBox.getParent()
}
return false
}
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() && !this.isMasked(box) {
// 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() && !this.isMasked(box) { 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
}
// var minimumSizeCount = 0
func (this *Hierarchy) doMinimumSize () {
this.minimumClean = true
// println("doMinimumSize", minimumSizeCount)
// minimumSizeCount ++
previousMinimumSize := this.minimumSize
this.minimumSize = image.Point { }
if this.root != nil {
this.minimumSize = this.root.minimumSize()
}
if previousMinimumSize != this.minimumSize {
this.link.NotifyMinimumSizeChange()
}
}
func (this *Hierarchy) newStyleApplicator () *styleApplicator {
return &styleApplicator {
style: this.getStyle(),
}
}