Initial implementation of System
This commit is contained in:
parent
ccf51dfb0f
commit
0a58228773
@ -1,7 +1,535 @@
|
|||||||
package system
|
package system
|
||||||
|
|
||||||
|
import "image"
|
||||||
|
import "image/color"
|
||||||
import "git.tebibyte.media/tomo/tomo"
|
import "git.tebibyte.media/tomo/tomo"
|
||||||
|
import "git.tebibyte.media/tomo/tomo/data"
|
||||||
|
import "git.tebibyte.media/tomo/tomo/input"
|
||||||
|
import "git.tebibyte.media/tomo/tomo/event"
|
||||||
|
import "git.tebibyte.media/tomo/tomo/canvas"
|
||||||
|
import "git.tebibyte.media/tomo/backend/internal/util"
|
||||||
|
|
||||||
func (this *System) NewBox () tomo.Box {
|
type textureMode int; const (
|
||||||
|
textureModeTile textureMode = iota
|
||||||
|
textureModeCenter
|
||||||
|
)
|
||||||
|
|
||||||
|
type box struct {
|
||||||
|
system *System
|
||||||
|
parent parent
|
||||||
|
outer anyBox
|
||||||
|
|
||||||
|
bounds image.Rectangle
|
||||||
|
minSize image.Point
|
||||||
|
userMinSize image.Point
|
||||||
|
innerClippingBounds image.Rectangle
|
||||||
|
|
||||||
|
minSizeQueued bool
|
||||||
|
focusQueued *bool
|
||||||
|
|
||||||
|
padding tomo.Inset
|
||||||
|
border []tomo.Border
|
||||||
|
color color.Color
|
||||||
|
texture canvas.Texture
|
||||||
|
textureMode textureMode
|
||||||
|
|
||||||
|
dndData data.Data
|
||||||
|
dndAccept []data.Mime
|
||||||
|
focused bool
|
||||||
|
focusable bool
|
||||||
|
|
||||||
|
canvas util.Memo[canvas.Canvas]
|
||||||
|
drawer canvas.Drawer
|
||||||
|
|
||||||
|
on struct {
|
||||||
|
focusEnter event.FuncBroadcaster
|
||||||
|
focusLeave event.FuncBroadcaster
|
||||||
|
dndEnter event.FuncBroadcaster
|
||||||
|
dndLeave event.FuncBroadcaster
|
||||||
|
dndDrop event.Broadcaster[func (data.Data)]
|
||||||
|
mouseEnter event.FuncBroadcaster
|
||||||
|
mouseLeave event.FuncBroadcaster
|
||||||
|
mouseMove event.FuncBroadcaster
|
||||||
|
mouseDown event.Broadcaster[func (input.Button)]
|
||||||
|
mouseUp event.Broadcaster[func (input.Button)]
|
||||||
|
scroll event.Broadcaster[func (float64, float64)]
|
||||||
|
keyDown event.Broadcaster[func (input.Key, bool)]
|
||||||
|
keyUp event.Broadcaster[func (input.Key, bool)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *System) newBox (outer anyBox) *box {
|
||||||
|
box := &box {
|
||||||
|
system: this,
|
||||||
|
color: color.Transparent,
|
||||||
|
outer: outer,
|
||||||
|
drawer: outer,
|
||||||
|
}
|
||||||
|
box.canvas = util.NewMemo (func () canvas.Canvas {
|
||||||
|
if box.parent == nil { return nil }
|
||||||
|
parentCanvas := box.parent.getCanvas()
|
||||||
|
if parentCanvas == nil { return nil }
|
||||||
|
return parentCanvas.SubCanvas(box.bounds)
|
||||||
|
})
|
||||||
|
if outer == nil {
|
||||||
|
box.drawer = box
|
||||||
|
box.outer = box
|
||||||
|
}
|
||||||
|
box.invalidateMinimum()
|
||||||
|
return box
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (this *System) NewBox () tomo.Box {
|
||||||
|
return this.newBox(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *box) GetBox () tomo.Box {
|
||||||
|
return this.outer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *box) Window () tomo.Window {
|
||||||
|
hierarchy := this.getHierarchy()
|
||||||
|
if hierarchy == nil { return nil }
|
||||||
|
return hierarchy.getWindow()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *box) Bounds () image.Rectangle {
|
||||||
|
return this.bounds
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *box) InnerBounds () image.Rectangle {
|
||||||
|
return this.padding.Apply(this.innerClippingBounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *box) MinimumSize () image.Point {
|
||||||
|
return this.minSize
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *box) borderSum () tomo.Inset {
|
||||||
|
sum := tomo.Inset { }
|
||||||
|
for _, border := range this.border {
|
||||||
|
sum[0] += border.Width[0]
|
||||||
|
sum[1] += border.Width[1]
|
||||||
|
sum[2] += border.Width[2]
|
||||||
|
sum[3] += border.Width[3]
|
||||||
|
}
|
||||||
|
return sum
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *box) SetBounds (bounds image.Rectangle) {
|
||||||
|
if this.bounds == bounds { return }
|
||||||
|
this.bounds = bounds
|
||||||
|
this.invalidateLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *box) SetColor (c color.Color) {
|
||||||
|
if c == nil { c = color.Transparent }
|
||||||
|
if this.color == c { return }
|
||||||
|
this.color = c
|
||||||
|
this.invalidateDraw()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *box) SetTextureTile (texture canvas.Texture) {
|
||||||
|
if this.texture == texture && this.textureMode == textureModeTile { return }
|
||||||
|
this.textureMode = textureModeTile
|
||||||
|
this.texture = texture
|
||||||
|
this.invalidateDraw()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *box) SetTextureCenter (texture canvas.Texture) {
|
||||||
|
if this.texture == texture && this.textureMode == textureModeCenter { return }
|
||||||
|
this.texture = texture
|
||||||
|
this.textureMode = textureModeCenter
|
||||||
|
this.invalidateDraw()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *box) SetBorder (borders ...tomo.Border) {
|
||||||
|
previousBorderSum := this.borderSum()
|
||||||
|
previousBorders := this.border
|
||||||
|
this.border = borders
|
||||||
|
|
||||||
|
// only invalidate the layout if the border is sized differently
|
||||||
|
if this.borderSum() != previousBorderSum {
|
||||||
|
this.invalidateLayout()
|
||||||
|
this.invalidateMinimum()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the border takes up the same amount of space, only invalidate the
|
||||||
|
// drawing if it looks different
|
||||||
|
for index, newBorder := range this.border {
|
||||||
|
different :=
|
||||||
|
index >= len(previousBorders) ||
|
||||||
|
newBorder != previousBorders[index]
|
||||||
|
if different {
|
||||||
|
this.invalidateDraw()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *box) SetMinimumSize (size image.Point) {
|
||||||
|
if this.userMinSize == size { return }
|
||||||
|
this.userMinSize = size
|
||||||
|
this.invalidateMinimum()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *box) SetPadding (padding tomo.Inset) {
|
||||||
|
if this.padding == padding { return }
|
||||||
|
this.padding = padding
|
||||||
|
this.invalidateLayout()
|
||||||
|
this.invalidateMinimum()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *box) SetDNDData (dat data.Data) {
|
||||||
|
this.dndData = dat
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *box) SetDNDAccept (types ...data.Mime) {
|
||||||
|
this.dndAccept = types
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *box) SetFocused (focused bool) {
|
||||||
|
hierarchy := this.parent.getHierarchy()
|
||||||
|
if hierarchy == nil {
|
||||||
|
focusedCopy := focused
|
||||||
|
this.focusQueued = &focusedCopy
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !this.focusable { return }
|
||||||
|
|
||||||
|
if this.Focused () && !focused {
|
||||||
|
hierarchy.focus(nil)
|
||||||
|
} else if !this.Focused() && focused {
|
||||||
|
hierarchy.focus(this.outer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *box) SetFocusable (focusable bool) {
|
||||||
|
if this.focusable == focusable { return }
|
||||||
|
this.focusable = focusable
|
||||||
|
if !focusable {
|
||||||
|
this.SetFocused(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *box) Focused () bool {
|
||||||
|
hierarchy := this.parent.getHierarchy()
|
||||||
|
if hierarchy == nil { return false }
|
||||||
|
return hierarchy.isFocused(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *box) Modifiers () input.Modifiers {
|
||||||
|
hierarchy := this.getHierarchy()
|
||||||
|
if hierarchy == nil { return input.Modifiers { } }
|
||||||
|
return hierarchy.getModifiers()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *box) MousePosition () image.Point {
|
||||||
|
hierarchy := this.getHierarchy()
|
||||||
|
if hierarchy == nil { return image.Point { } }
|
||||||
|
return hierarchy.getMousePosition()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- event handler setters ---------------------------------------------- //
|
||||||
|
func (this *box) OnFocusEnter (callback func()) event.Cookie {
|
||||||
|
return this.on.focusEnter.Connect(callback)
|
||||||
|
}
|
||||||
|
func (this *box) OnFocusLeave (callback func()) event.Cookie {
|
||||||
|
return this.on.focusLeave.Connect(callback)
|
||||||
|
}
|
||||||
|
func (this *box) OnDNDEnter (callback func()) event.Cookie {
|
||||||
|
return this.on.dndEnter.Connect(callback)
|
||||||
|
}
|
||||||
|
func (this *box) OnDNDLeave (callback func()) event.Cookie {
|
||||||
|
return this.on.dndLeave.Connect(callback)
|
||||||
|
}
|
||||||
|
func (this *box) OnDNDDrop (callback func(data.Data)) event.Cookie {
|
||||||
|
return this.on.dndDrop.Connect(callback)
|
||||||
|
}
|
||||||
|
func (this *box) OnMouseEnter (callback func()) event.Cookie {
|
||||||
|
return this.on.mouseEnter.Connect(callback)
|
||||||
|
}
|
||||||
|
func (this *box) OnMouseLeave (callback func()) event.Cookie {
|
||||||
|
return this.on.mouseLeave.Connect(callback)
|
||||||
|
}
|
||||||
|
func (this *box) OnMouseMove (callback func()) event.Cookie {
|
||||||
|
return this.on.mouseMove.Connect(callback)
|
||||||
|
}
|
||||||
|
func (this *box) OnMouseDown (callback func(input.Button)) event.Cookie {
|
||||||
|
return this.on.mouseDown.Connect(callback)
|
||||||
|
}
|
||||||
|
func (this *box) OnMouseUp (callback func(input.Button)) event.Cookie {
|
||||||
|
return this.on.mouseUp.Connect(callback)
|
||||||
|
}
|
||||||
|
func (this *box) OnScroll (callback func(deltaX, deltaY float64)) event.Cookie {
|
||||||
|
return this.on.scroll.Connect(callback)
|
||||||
|
}
|
||||||
|
func (this *box) OnKeyDown (callback func(key input.Key, numberPad bool)) event.Cookie {
|
||||||
|
return this.on.keyDown.Connect(callback)
|
||||||
|
}
|
||||||
|
func (this *box) OnKeyUp (callback func(key input.Key, numberPad bool)) event.Cookie {
|
||||||
|
return this.on.keyUp.Connect(callback)
|
||||||
|
}
|
||||||
|
func (this *box) handleFocusEnter () {
|
||||||
|
this.on.focusEnter.Broadcast()
|
||||||
|
}
|
||||||
|
func (this *box) handleFocusLeave () {
|
||||||
|
this.on.focusLeave.Broadcast()
|
||||||
|
}
|
||||||
|
func (this *box) handleMouseEnter () {
|
||||||
|
this.on.mouseEnter.Broadcast()
|
||||||
|
}
|
||||||
|
func (this *box) handleMouseLeave () {
|
||||||
|
this.on.mouseLeave.Broadcast()
|
||||||
|
}
|
||||||
|
func (this *box) handleMouseMove () {
|
||||||
|
this.on.mouseMove.Broadcast()
|
||||||
|
}
|
||||||
|
func (this *box) handleMouseDown (button input.Button) {
|
||||||
|
if this.focusable {
|
||||||
|
this.SetFocused(true)
|
||||||
|
} else {
|
||||||
|
hierarchy := this.getHierarchy()
|
||||||
|
if hierarchy == nil { return }
|
||||||
|
hierarchy.focus(nil)
|
||||||
|
}
|
||||||
|
for _, listener := range this.on.mouseDown.Listeners() {
|
||||||
|
listener(button)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (this *box) handleMouseUp (button input.Button) {
|
||||||
|
for _, listener := range this.on.mouseUp.Listeners() {
|
||||||
|
listener(button)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (this *box) handleScroll (x, y float64) {
|
||||||
|
for _, listener := range this.on.scroll.Listeners() {
|
||||||
|
listener(x, y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (this *box) handleKeyDown (key input.Key, numberPad bool) {
|
||||||
|
for _, listener := range this.on.keyDown.Listeners() {
|
||||||
|
listener(key, numberPad)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (this *box) handleKeyUp (key input.Key, numberPad bool) {
|
||||||
|
for _, listener := range this.on.keyUp.Listeners() {
|
||||||
|
listener(key, numberPad)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// -------------------------------------------------------------------------- //
|
||||||
|
|
||||||
|
func (this *box) Draw (can canvas.Canvas) {
|
||||||
|
if can == nil { return }
|
||||||
|
pen := can.Pen()
|
||||||
|
bounds := this.Bounds()
|
||||||
|
|
||||||
|
// background
|
||||||
|
pen.Fill(this.color)
|
||||||
|
if this.textureMode == textureModeTile {
|
||||||
|
pen.Texture(this.texture)
|
||||||
|
}
|
||||||
|
if this.transparent() && this.parent != nil {
|
||||||
|
this.parent.drawBackgroundPart(can)
|
||||||
|
}
|
||||||
|
pen.Rectangle(bounds)
|
||||||
|
|
||||||
|
// centered texture
|
||||||
|
if this.textureMode == textureModeCenter && this.texture != nil {
|
||||||
|
textureBounds := this.texture.Bounds()
|
||||||
|
textureOrigin :=
|
||||||
|
bounds.Min.
|
||||||
|
Add(image.Pt (
|
||||||
|
bounds.Dx() / 2,
|
||||||
|
bounds.Dy() / 2)).
|
||||||
|
Sub(image.Pt (
|
||||||
|
textureBounds.Dx() / 2,
|
||||||
|
textureBounds.Dy() / 2))
|
||||||
|
|
||||||
|
pen.Fill(color.Transparent)
|
||||||
|
pen.Texture(this.texture)
|
||||||
|
pen.Rectangle(textureBounds.Sub(textureBounds.Min).Add(textureOrigin))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *box) drawBorders (can canvas.Canvas) {
|
||||||
|
if can == nil { return }
|
||||||
|
pen := can.Pen()
|
||||||
|
bounds := this.bounds
|
||||||
|
|
||||||
|
rectangle := func (x0, y0, x1, y1 int, c color.Color) {
|
||||||
|
area := image.Rect(x0, y0, x1, y1)
|
||||||
|
if util.Transparent(c) && this.parent != nil {
|
||||||
|
this.parent.drawBackgroundPart(can.SubCanvas(area))
|
||||||
|
}
|
||||||
|
pen.Fill(c)
|
||||||
|
pen.Rectangle(area)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, border := range this.border {
|
||||||
|
rectangle (
|
||||||
|
bounds.Min.X,
|
||||||
|
bounds.Min.Y,
|
||||||
|
bounds.Max.X,
|
||||||
|
bounds.Min.Y + border.Width[tomo.SideTop],
|
||||||
|
border.Color[tomo.SideTop])
|
||||||
|
rectangle (
|
||||||
|
bounds.Min.X,
|
||||||
|
bounds.Max.Y - border.Width[tomo.SideBottom],
|
||||||
|
bounds.Max.X,
|
||||||
|
bounds.Max.Y,
|
||||||
|
border.Color[tomo.SideBottom])
|
||||||
|
rectangle (
|
||||||
|
bounds.Min.X,
|
||||||
|
bounds.Min.Y + border.Width[tomo.SideTop],
|
||||||
|
bounds.Min.X + border.Width[tomo.SideLeft],
|
||||||
|
bounds.Max.Y - border.Width[tomo.SideBottom],
|
||||||
|
border.Color[tomo.SideLeft])
|
||||||
|
rectangle (
|
||||||
|
bounds.Max.X - border.Width[tomo.SideRight],
|
||||||
|
bounds.Min.Y + border.Width[tomo.SideTop],
|
||||||
|
bounds.Max.X,
|
||||||
|
bounds.Max.Y - border.Width[tomo.SideBottom],
|
||||||
|
border.Color[tomo.SideRight])
|
||||||
|
|
||||||
|
bounds = border.Width.Apply(bounds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *box) contentMinimum () image.Point {
|
||||||
|
var minimum image.Point
|
||||||
|
minimum.X += this.padding.Horizontal()
|
||||||
|
minimum.Y += this.padding.Vertical()
|
||||||
|
borderSum := this.borderSum()
|
||||||
|
minimum.X += borderSum.Horizontal()
|
||||||
|
minimum.Y += borderSum.Vertical()
|
||||||
|
return minimum
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *box) doMinimumSize () {
|
||||||
|
this.minSize = this.outer.contentMinimum()
|
||||||
|
if this.minSize.X < this.userMinSize.X {
|
||||||
|
this.minSize.X = this.userMinSize.X
|
||||||
|
}
|
||||||
|
if this.minSize.Y < this.userMinSize.Y {
|
||||||
|
this.minSize.Y = this.userMinSize.Y
|
||||||
|
}
|
||||||
|
|
||||||
|
if this.parent != nil {
|
||||||
|
this.parent.notifyMinimumSizeChange(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// var drawcnt int
|
||||||
|
func (this *box) doDraw () {
|
||||||
|
// println("DRAW", drawcnt)
|
||||||
|
// drawcnt ++
|
||||||
|
|
||||||
|
canvas := this.canvas.Value()
|
||||||
|
if canvas == nil { return }
|
||||||
|
if this.drawer != nil {
|
||||||
|
this.drawBorders(canvas)
|
||||||
|
this.drawer.Draw(canvas.SubCanvas(this.innerClippingBounds))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// var laycnt int
|
||||||
|
func (this *box) doLayout () {
|
||||||
|
// println("LAYOUT", laycnt)
|
||||||
|
// laycnt ++
|
||||||
|
|
||||||
|
this.innerClippingBounds = this.borderSum().Apply(this.bounds)
|
||||||
|
this.loseCanvas()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *box) setParent (parent parent) {
|
||||||
|
if this.parent != parent && this.Focused() {
|
||||||
|
this.SetFocused(false)
|
||||||
|
}
|
||||||
|
this.parent = parent
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *box) getParent () parent {
|
||||||
|
return this.parent
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *box) flushActionQueue () {
|
||||||
|
if this.getHierarchy() == nil { return }
|
||||||
|
|
||||||
|
if this.minSizeQueued {
|
||||||
|
this.invalidateMinimum()
|
||||||
|
}
|
||||||
|
if this.focusQueued != nil {
|
||||||
|
this.SetFocused(*this.focusQueued)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *box) recursiveRedo () {
|
||||||
|
this.doLayout()
|
||||||
|
this.doDraw()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *box) loseCanvas () {
|
||||||
|
this.canvas.InvalidateTo(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *box) invalidateLayout () {
|
||||||
|
hierarchy := this.getHierarchy()
|
||||||
|
if hierarchy == nil { return }
|
||||||
|
hierarchy.invalidateLayout(this.outer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *box) invalidateDraw () {
|
||||||
|
hierarchy := this.getHierarchy()
|
||||||
|
if hierarchy == nil { return }
|
||||||
|
hierarchy.invalidateDraw(this.outer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *box) invalidateMinimum () {
|
||||||
|
hierarchy := this.getHierarchy()
|
||||||
|
if hierarchy == nil {
|
||||||
|
this.minSizeQueued = true
|
||||||
|
} else {
|
||||||
|
hierarchy.invalidateMinimum(this.outer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *box) canBeFocused () bool {
|
||||||
|
return this.focusable
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *box) boxUnder (point image.Point, category eventCategory) anyBox {
|
||||||
|
if point.In(this.bounds) {
|
||||||
|
return this.outer
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *box) propagate (callback func (anyBox) bool) bool {
|
||||||
|
return callback(this.outer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *box) propagateAlt (callback func (anyBox) bool) bool {
|
||||||
|
return callback(this.outer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *box) transparent () bool {
|
||||||
|
// TODO uncomment once we have
|
||||||
|
// a way to detect texture transparency
|
||||||
|
return util.Transparent(this.color) /*&&
|
||||||
|
(this.texture == nil || !this.texture.Opaque())*/
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *box) getWindow () tomo.Window {
|
||||||
|
hierarchy := this.getHierarchy()
|
||||||
|
if hierarchy == nil { return nil }
|
||||||
|
return hierarchy.getWindow()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *box) getHierarchy () *Hierarchy {
|
||||||
|
if this.parent == nil { return nil }
|
||||||
|
return this.parent.getHierarchy()
|
||||||
|
}
|
||||||
|
@ -1,7 +1,35 @@
|
|||||||
package system
|
package system
|
||||||
|
|
||||||
import "git.tebibyte.media/tomo/tomo"
|
import "git.tebibyte.media/tomo/tomo"
|
||||||
|
import "git.tebibyte.media/tomo/tomo/canvas"
|
||||||
|
|
||||||
|
type canvasBox struct {
|
||||||
|
*box
|
||||||
|
userDrawer canvas.Drawer
|
||||||
|
}
|
||||||
|
|
||||||
func (this *System) NewCanvasBox () tomo.CanvasBox {
|
func (this *System) NewCanvasBox () tomo.CanvasBox {
|
||||||
|
box := &canvasBox { }
|
||||||
|
box.box = this.newBox(box)
|
||||||
|
box.drawer = box
|
||||||
|
return box
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *canvasBox) Box () tomo.Box {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *canvasBox) SetDrawer (drawer canvas.Drawer) {
|
||||||
|
this.userDrawer = drawer
|
||||||
|
this.invalidateDraw()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *canvasBox) Invalidate () {
|
||||||
|
this.invalidateDraw()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *canvasBox) Draw (can canvas.Canvas) {
|
||||||
|
this.box.Draw(can)
|
||||||
|
this.userDrawer.Draw (
|
||||||
|
can.SubCanvas(this.padding.Apply(this.innerClippingBounds)))
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,373 @@
|
|||||||
package system
|
package system
|
||||||
|
|
||||||
|
import "image"
|
||||||
|
import "image/color"
|
||||||
import "git.tebibyte.media/tomo/tomo"
|
import "git.tebibyte.media/tomo/tomo"
|
||||||
|
import "git.tebibyte.media/tomo/tomo/event"
|
||||||
|
import "git.tebibyte.media/tomo/tomo/canvas"
|
||||||
|
import "git.tebibyte.media/tomo/backend/internal/util"
|
||||||
|
|
||||||
|
type containerBox struct {
|
||||||
|
*box
|
||||||
|
|
||||||
|
hOverflow, vOverflow bool
|
||||||
|
hAlign, vAlign tomo.Align
|
||||||
|
contentBounds image.Rectangle
|
||||||
|
scroll image.Point
|
||||||
|
capture [4]bool
|
||||||
|
|
||||||
|
gap image.Point
|
||||||
|
children []tomo.Box
|
||||||
|
layout tomo.Layout
|
||||||
|
|
||||||
|
on struct {
|
||||||
|
contentBoundsChange event.FuncBroadcaster
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (this *System) NewContainerBox () tomo.ContainerBox {
|
func (this *System) NewContainerBox () tomo.ContainerBox {
|
||||||
|
box := &containerBox { }
|
||||||
|
box.box = this.newBox(box)
|
||||||
|
return box
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *containerBox) SetColor (c color.Color) {
|
||||||
|
if this.color == c { return }
|
||||||
|
this.box.SetColor(c)
|
||||||
|
this.invalidateTransparentChildren()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *containerBox) SetTextureTile (texture canvas.Texture) {
|
||||||
|
if this.texture == texture { return }
|
||||||
|
this.box.SetTextureTile(texture)
|
||||||
|
this.invalidateTransparentChildren()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *containerBox) SetTextureCenter (texture canvas.Texture) {
|
||||||
|
if this.texture == texture { return }
|
||||||
|
this.box.SetTextureTile(texture)
|
||||||
|
this.invalidateTransparentChildren()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *containerBox) SetOverflow (horizontal, vertical bool) {
|
||||||
|
if this.hOverflow == horizontal && this.vOverflow == vertical { return }
|
||||||
|
this.hOverflow = horizontal
|
||||||
|
this.vOverflow = vertical
|
||||||
|
this.invalidateLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *containerBox) SetAlign (x, y tomo.Align) {
|
||||||
|
if this.hAlign == x && this.vAlign == y { return }
|
||||||
|
this.hAlign = x
|
||||||
|
this.vAlign = y
|
||||||
|
this.invalidateLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *containerBox) ContentBounds () image.Rectangle {
|
||||||
|
return this.contentBounds
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *containerBox) ScrollTo (point image.Point) {
|
||||||
|
if this.scroll == point { return }
|
||||||
|
this.scroll = point
|
||||||
|
this.invalidateLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *containerBox) OnContentBoundsChange (callback func()) event.Cookie {
|
||||||
|
return this.on.contentBoundsChange.Connect(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *containerBox) CaptureDND (capture bool) {
|
||||||
|
this.capture[eventCategoryDND] = capture
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *containerBox) CaptureMouse (capture bool) {
|
||||||
|
this.capture[eventCategoryMouse] = capture
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *containerBox) CaptureScroll (capture bool) {
|
||||||
|
this.capture[eventCategoryScroll] = capture
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *containerBox) CaptureKeyboard (capture bool) {
|
||||||
|
this.capture[eventCategoryKeyboard] = capture
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *containerBox) SetGap (gap image.Point) {
|
||||||
|
if this.gap == gap { return }
|
||||||
|
this.gap = gap
|
||||||
|
this.invalidateLayout()
|
||||||
|
this.invalidateMinimum()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *containerBox) Add (child tomo.Object) {
|
||||||
|
box := assertAnyBox(child.GetBox())
|
||||||
|
if util.IndexOf(this.children, tomo.Box(box)) > -1 { return }
|
||||||
|
|
||||||
|
box.setParent(this)
|
||||||
|
box.flushActionQueue()
|
||||||
|
this.children = append(this.children, box)
|
||||||
|
this.invalidateLayout()
|
||||||
|
this.invalidateMinimum()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *containerBox) Remove (child tomo.Object) {
|
||||||
|
box := assertAnyBox(child.GetBox())
|
||||||
|
index := util.IndexOf(this.children, tomo.Box(box))
|
||||||
|
if index < 0 { return }
|
||||||
|
|
||||||
|
box.setParent(nil)
|
||||||
|
this.children = util.Remove(this.children, index)
|
||||||
|
this.invalidateLayout()
|
||||||
|
this.invalidateMinimum()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *containerBox) Insert (child, before tomo.Object) {
|
||||||
|
box := assertAnyBox(child.GetBox())
|
||||||
|
if util.IndexOf(this.children, tomo.Box(box)) > -1 { return }
|
||||||
|
|
||||||
|
beforeBox := assertAnyBox(before.GetBox())
|
||||||
|
index := util.IndexOf(this.children, tomo.Box(beforeBox))
|
||||||
|
|
||||||
|
if index < 0 {
|
||||||
|
this.children = append(this.children, tomo.Box(box))
|
||||||
|
} else {
|
||||||
|
this.children = util.Insert(this.children, index, tomo.Box(box))
|
||||||
|
}
|
||||||
|
box.setParent(this)
|
||||||
|
|
||||||
|
this.invalidateLayout()
|
||||||
|
this.invalidateMinimum()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *containerBox) Clear () {
|
||||||
|
for _, box := range this.children {
|
||||||
|
box.(anyBox).setParent(nil)
|
||||||
|
}
|
||||||
|
this.children = nil
|
||||||
|
this.invalidateLayout()
|
||||||
|
this.invalidateMinimum()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *containerBox) Length () int {
|
||||||
|
return len(this.children)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *containerBox) At (index int) tomo.Object {
|
||||||
|
if index < 0 || index >= len(this.children) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return this.children[index]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *containerBox) SetLayout (layout tomo.Layout) {
|
||||||
|
this.layout = layout
|
||||||
|
this.invalidateLayout()
|
||||||
|
this.invalidateMinimum()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *containerBox) Draw (can canvas.Canvas) {
|
||||||
|
if can == nil { return }
|
||||||
|
|
||||||
|
rocks := make([]image.Rectangle, len(this.children))
|
||||||
|
for index, box := range this.children {
|
||||||
|
rocks[index] = box.Bounds()
|
||||||
|
}
|
||||||
|
for _, tile := range canvas.Shatter(this.bounds, rocks...) {
|
||||||
|
clipped := can.SubCanvas(tile)
|
||||||
|
if this.transparent() && this.parent != nil {
|
||||||
|
this.parent.drawBackgroundPart(clipped)
|
||||||
|
}
|
||||||
|
if clipped == nil { continue }
|
||||||
|
pen := clipped.Pen()
|
||||||
|
pen.Fill(this.color)
|
||||||
|
pen.Texture(this.texture)
|
||||||
|
pen.Rectangle(this.innerClippingBounds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *containerBox) drawBackgroundPart (can canvas.Canvas) {
|
||||||
|
if can == nil { return }
|
||||||
|
pen := can.Pen()
|
||||||
|
pen.Fill(this.color)
|
||||||
|
pen.Texture(this.texture)
|
||||||
|
|
||||||
|
if this.transparent() && this.parent != nil {
|
||||||
|
this.parent.drawBackgroundPart(can)
|
||||||
|
}
|
||||||
|
pen.Rectangle(this.innerClippingBounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *containerBox) invalidateTransparentChildren () {
|
||||||
|
hierarchy := this.getHierarchy()
|
||||||
|
if hierarchy == nil { return }
|
||||||
|
for _, box := range this.children {
|
||||||
|
box := assertAnyBox(box)
|
||||||
|
if box.transparent() {
|
||||||
|
hierarchy.invalidateDraw(box)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *containerBox) flushActionQueue () {
|
||||||
|
for _, box := range this.children {
|
||||||
|
box.(anyBox).flushActionQueue()
|
||||||
|
}
|
||||||
|
this.box.flushActionQueue()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *containerBox) getHierarchy () *Hierarchy {
|
||||||
|
if this.parent == nil { return nil }
|
||||||
|
return this.parent.getHierarchy()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *containerBox) getCanvas () canvas.Canvas {
|
||||||
|
return this.canvas.Value()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *containerBox) notifyMinimumSizeChange (child anyBox) {
|
||||||
|
this.invalidateMinimum()
|
||||||
|
size := child.MinimumSize()
|
||||||
|
bounds := child.Bounds()
|
||||||
|
if bounds.Dx() < size.X || bounds.Dy() < size.Y {
|
||||||
|
this.invalidateLayout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *containerBox) layoutHints () tomo.LayoutHints {
|
||||||
|
return tomo.LayoutHints {
|
||||||
|
OverflowX: this.hOverflow,
|
||||||
|
OverflowY: this.vOverflow,
|
||||||
|
AlignX: this.hAlign,
|
||||||
|
AlignY: this.vAlign,
|
||||||
|
Gap: this.gap,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *containerBox) contentMinimum () image.Point {
|
||||||
|
minimum := this.box.contentMinimum()
|
||||||
|
if this.layout != nil {
|
||||||
|
layoutMinimum := this.layout.MinimumSize (
|
||||||
|
this.layoutHints(),
|
||||||
|
this.children)
|
||||||
|
if this.hOverflow { layoutMinimum.X = 0 }
|
||||||
|
if this.vOverflow { layoutMinimum.Y = 0 }
|
||||||
|
minimum = minimum.Add(layoutMinimum)
|
||||||
|
}
|
||||||
|
return minimum
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *containerBox) doLayout () {
|
||||||
|
this.box.doLayout()
|
||||||
|
previousContentBounds := this.contentBounds
|
||||||
|
|
||||||
|
// by default, use innerBounds (translated to 0, 0) for contentBounds.
|
||||||
|
// if a direction overflows, use the layout's minimum size for it.
|
||||||
|
var minimum image.Point
|
||||||
|
if this.layout != nil {
|
||||||
|
minimum = this.layout.MinimumSize (
|
||||||
|
this.layoutHints(),
|
||||||
|
this.children)
|
||||||
|
}
|
||||||
|
innerBounds := this.InnerBounds()
|
||||||
|
this.contentBounds = innerBounds.Sub(innerBounds.Min)
|
||||||
|
if this.hOverflow { this.contentBounds.Max.X = this.contentBounds.Min.X + minimum.X }
|
||||||
|
if this.vOverflow { this.contentBounds.Max.Y = this.contentBounds.Min.Y + minimum.Y }
|
||||||
|
|
||||||
|
// arrange children
|
||||||
|
if this.layout != nil {
|
||||||
|
layoutHints := this.layoutHints()
|
||||||
|
layoutHints.Bounds = this.contentBounds
|
||||||
|
this.layout.Arrange(layoutHints, this.children)
|
||||||
|
}
|
||||||
|
|
||||||
|
// build an accurate contentBounds by unioning the bounds of all child
|
||||||
|
// boxes
|
||||||
|
this.contentBounds = image.Rectangle { }
|
||||||
|
for _, box := range this.children {
|
||||||
|
bounds := box.Bounds()
|
||||||
|
this.contentBounds = this.contentBounds.Union(bounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
// constrain the scroll
|
||||||
|
this.constrainScroll()
|
||||||
|
|
||||||
|
// offset children and contentBounds by scroll
|
||||||
|
for _, box := range this.children {
|
||||||
|
box.SetBounds(box.Bounds().Add(this.scroll).Add(innerBounds.Min))
|
||||||
|
}
|
||||||
|
this.contentBounds = this.contentBounds.Add(this.scroll)
|
||||||
|
|
||||||
|
if previousContentBounds != this.contentBounds {
|
||||||
|
this.on.contentBoundsChange.Broadcast()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *containerBox) constrainScroll () {
|
||||||
|
innerBounds := this.InnerBounds()
|
||||||
|
width := this.contentBounds.Dx()
|
||||||
|
height := this.contentBounds.Dy()
|
||||||
|
|
||||||
|
// X
|
||||||
|
if width <= innerBounds.Dx() {
|
||||||
|
this.scroll.X = 0
|
||||||
|
} else if this.scroll.X > 0 {
|
||||||
|
this.scroll.X = 0
|
||||||
|
} else if this.scroll.X < innerBounds.Dx() - width {
|
||||||
|
this.scroll.X = innerBounds.Dx() - width
|
||||||
|
}
|
||||||
|
|
||||||
|
// Y
|
||||||
|
if height <= innerBounds.Dy() {
|
||||||
|
this.scroll.Y = 0
|
||||||
|
} else if this.scroll.Y > 0 {
|
||||||
|
this.scroll.Y = 0
|
||||||
|
} else if this.scroll.Y < innerBounds.Dy() - height {
|
||||||
|
this.scroll.Y = innerBounds.Dy() - height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *containerBox) recursiveRedo () {
|
||||||
|
this.doLayout()
|
||||||
|
this.doDraw()
|
||||||
|
for _, child := range this.children {
|
||||||
|
child.(anyBox).recursiveRedo()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *containerBox) boxUnder (point image.Point, category eventCategory) anyBox {
|
||||||
|
if !point.In(this.bounds) { return nil }
|
||||||
|
|
||||||
|
if !this.capture[category] {
|
||||||
|
for _, box := range this.children {
|
||||||
|
candidate := box.(anyBox).boxUnder(point, category)
|
||||||
|
if candidate != nil { return candidate }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *containerBox) propagate (callback func (anyBox) bool) bool {
|
||||||
|
for _, box := range this.children {
|
||||||
|
box := box.(anyBox)
|
||||||
|
if !box.propagate(callback) { return false }
|
||||||
|
}
|
||||||
|
|
||||||
|
return callback(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *containerBox) propagateAlt (callback func (anyBox) bool) bool {
|
||||||
|
if !callback(this) { return false}
|
||||||
|
|
||||||
|
for _, box := range this.children {
|
||||||
|
box := box.(anyBox)
|
||||||
|
if !box.propagateAlt(callback) { return false }
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *containerBox) captures (category eventCategory) bool {
|
||||||
|
return this.capture[category]
|
||||||
}
|
}
|
||||||
|
97
internal/system/event.go
Normal file
97
internal/system/event.go
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
package system
|
||||||
|
|
||||||
|
import "image"
|
||||||
|
import "git.tebibyte.media/tomo/tomo/input"
|
||||||
|
|
||||||
|
// HandleFocusChange sets whether or not the window containing this Hierarchy
|
||||||
|
// has input focus.
|
||||||
|
func (this *Hierarchy) HandleFocusChange (focused bool) {
|
||||||
|
if this.windowFocused == focused { return }
|
||||||
|
this.windowFocused = focused
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleModifiers sets the modifier keys that are currently being pressed.
|
||||||
|
func (this *Hierarchy) HandleModifiers (modifiers input.Modifiers) {
|
||||||
|
if this.modifiers == modifiers { return }
|
||||||
|
this.modifiers = modifiers
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleKeyDown sends a key down event to the currently focused Box. If the
|
||||||
|
// event which triggers this comes with modifier key information,
|
||||||
|
// HandleModifiers must be called *before* HandleKeyDown.
|
||||||
|
func (this *Hierarchy) HandleKeyDown (key input.Key, numberPad bool) {
|
||||||
|
if key == input.KeyTab && this.modifiers.Alt {
|
||||||
|
if this.modifiers.Shift {
|
||||||
|
this.focusPrevious()
|
||||||
|
} else {
|
||||||
|
this.focusNext()
|
||||||
|
}
|
||||||
|
} else if this.focused != nil {
|
||||||
|
this.keyboardTarget().handleKeyDown(key, numberPad)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleKeyUp sends a key up event to the currently focused Box. If the event
|
||||||
|
// which triggers this comes with modifier key information, HandleModifiers must
|
||||||
|
// be called *before* HandleKeyUp.
|
||||||
|
func (this *Hierarchy) HandleKeyUp (key input.Key, numberPad bool) {
|
||||||
|
if this.focused == nil {
|
||||||
|
this.keyboardTarget().handleKeyUp(key, numberPad)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleMouseDown sends a mouse down event to the Box positioned underneath the
|
||||||
|
// mouse cursor and marks it as being "dragged" by that mouse button. If the
|
||||||
|
// event which triggers this comes with mouse position information,
|
||||||
|
// HandleMouseMove must be called *before* HandleMouseDown.
|
||||||
|
func (this *Hierarchy) HandleMouseDown (button input.Button) {
|
||||||
|
underneath := this.boxUnder(this.mousePosition, eventCategoryMouse)
|
||||||
|
this.drags[button] = underneath
|
||||||
|
if underneath != nil {
|
||||||
|
underneath.handleMouseDown(button)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleMouseUp sends a mouse up event to the Box currently being "dragged" by
|
||||||
|
// the specified mouse button, and marks it as being "not dragged" by that mouse
|
||||||
|
// button. If the event which triggers this comes with mouse position
|
||||||
|
// information, HandleMouseMove must be caleld *before* HandleMouseUp
|
||||||
|
func (this *Hierarchy) HandleMouseUp (button input.Button) {
|
||||||
|
dragging := this.drags[button]
|
||||||
|
this.drags[button] = nil
|
||||||
|
if dragging != nil {
|
||||||
|
dragging.handleMouseUp(button)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleMouseMove sends a mouse move event to any Boxes currently being
|
||||||
|
// "dragged" by a mouse button. If none are, it sends the event to the Box which
|
||||||
|
// is underneath the mouse pointer.
|
||||||
|
func (this *Hierarchy) HandleMouseMove (position image.Point) {
|
||||||
|
if this.mousePosition == position { return }
|
||||||
|
this.mousePosition = position
|
||||||
|
|
||||||
|
handled := false
|
||||||
|
for _, child := range this.drags {
|
||||||
|
if child == nil { continue }
|
||||||
|
child.handleMouseMove()
|
||||||
|
handled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
underneath := this.boxUnder(position, eventCategoryMouse)
|
||||||
|
if underneath != nil {
|
||||||
|
this.hover(underneath)
|
||||||
|
if !handled {
|
||||||
|
underneath.handleMouseMove()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleScroll sends a scroll event to the Box currently underneath the mouse
|
||||||
|
// cursor.
|
||||||
|
func (this *Hierarchy) HandleScroll (x, y float64) {
|
||||||
|
underneath := this.boxUnder(this.mousePosition, eventCategoryScroll)
|
||||||
|
if underneath != nil {
|
||||||
|
underneath.handleScroll(x, y)
|
||||||
|
}
|
||||||
|
}
|
@ -1,15 +1,283 @@
|
|||||||
package system
|
package system
|
||||||
|
|
||||||
|
import "image"
|
||||||
import "git.tebibyte.media/tomo/tomo"
|
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 {
|
type Hierarchy struct {
|
||||||
link WindowLink
|
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 backend implementation
|
||||||
|
// which contains it.
|
||||||
type WindowLink interface {
|
type WindowLink interface {
|
||||||
GetWindow () tomo.Window
|
GetWindow () tomo.Window
|
||||||
|
PushRegion (image.Rectangle)
|
||||||
|
PushAll ()
|
||||||
|
NotifyMinimumSizeChange ()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewHierarchy creates a new Hierarchy.
|
||||||
func (this *System) NewHierarchy (link WindowLink) *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
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCanvas sets the held canvas of the Hierarchy that all boxes within it will
|
||||||
|
// draw to. The Hierarchy will take on the 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
|
||||||
|
}
|
||||||
|
|
||||||
|
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) 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) 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()
|
||||||
}
|
}
|
||||||
|
105
internal/system/internal-iface.go
Normal file
105
internal/system/internal-iface.go
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
package system
|
||||||
|
|
||||||
|
import "image"
|
||||||
|
import "git.tebibyte.media/tomo/tomo"
|
||||||
|
import "git.tebibyte.media/tomo/tomo/input"
|
||||||
|
import "git.tebibyte.media/tomo/tomo/canvas"
|
||||||
|
|
||||||
|
// eventCategory lists kinds of Tomo events.
|
||||||
|
type eventCategory int; const (
|
||||||
|
eventCategoryDND eventCategory = iota
|
||||||
|
eventCategoryMouse
|
||||||
|
eventCategoryScroll
|
||||||
|
eventCategoryKeyboard
|
||||||
|
)
|
||||||
|
|
||||||
|
// parent is any hierarchical type which contains other boxes. This can be a
|
||||||
|
// Hierarchy, containerBox, etc.
|
||||||
|
type parent interface {
|
||||||
|
// hierarchy returns the hierarchy the parent is apart of.
|
||||||
|
getHierarchy () *Hierarchy
|
||||||
|
// canvas returns the canvas held by the parent.
|
||||||
|
getCanvas () canvas.Canvas
|
||||||
|
// notifyMinimumSizeChange informs the parent that the minimum size of
|
||||||
|
// one of its children has changed.
|
||||||
|
notifyMinimumSizeChange (anyBox)
|
||||||
|
// drawBackgroundPart draws a part of the parent's background to the
|
||||||
|
// given Canvas, filling the Canvas's entire bounds. The origin (0, 0)
|
||||||
|
// of the given Canvas is assumed to be the same as the parent's canvas.
|
||||||
|
drawBackgroundPart (canvas.Canvas)
|
||||||
|
// captures returns whether or not this parent captures the given event
|
||||||
|
// category.
|
||||||
|
captures (eventCategory) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// anyBox is any tomo.Box type that is implemented by this package.
|
||||||
|
type anyBox interface {
|
||||||
|
tomo.Box
|
||||||
|
canvas.Drawer
|
||||||
|
|
||||||
|
// setParent sets this anyBox's parent.
|
||||||
|
// getParent returns this anyBox's parent as set by setParent.
|
||||||
|
setParent (parent)
|
||||||
|
getParent () parent
|
||||||
|
|
||||||
|
// doDraw re-paints the anyBox onto its currently held Canvas non-recursively
|
||||||
|
// doLayout re-calculates the layout of the anyBox non-recursively
|
||||||
|
// doMinimumSize re-calculates the minimum size of the anyBox non-recursively
|
||||||
|
doDraw ()
|
||||||
|
doLayout ()
|
||||||
|
doMinimumSize ()
|
||||||
|
|
||||||
|
// flushActionQueue performs any queued actions, like invalidating the
|
||||||
|
// minimum size or grabbing input focus.
|
||||||
|
flushActionQueue ()
|
||||||
|
// recursiveRedo recursively recalculates the minimum size, layout, and
|
||||||
|
// re-paints this anyBox and all of its children.
|
||||||
|
recursiveRedo ()
|
||||||
|
// loseCanvas causes this anyBox and its children (if applicable) to
|
||||||
|
// lose their canvases and re-cut them as needed.
|
||||||
|
loseCanvas ()
|
||||||
|
|
||||||
|
// contentMinimum returns the minimum dimensions of this box's content
|
||||||
|
contentMinimum () image.Point
|
||||||
|
// canBeFocused returns whether or not this anyBox is capable of holding
|
||||||
|
// input focus.
|
||||||
|
canBeFocused () bool
|
||||||
|
// boxUnder returns the anyBox under the mouse pointer. It can be this
|
||||||
|
// anyBox, one of its children (if applicable). It must return nil if
|
||||||
|
// the mouse pointer is outside of this anyBox's bounds.
|
||||||
|
boxUnder (image.Point, eventCategory) anyBox
|
||||||
|
// transparent returns whether or not this anyBox contains transparent
|
||||||
|
// pixels or not, and thus needs its parent's backround to be painted
|
||||||
|
// underneath it.
|
||||||
|
transparent () bool
|
||||||
|
|
||||||
|
// propagate recursively calls a function on this anyBox, and all of its
|
||||||
|
// children (if applicable) The normal propagate behavior calls the
|
||||||
|
// callback on all children before calling it on this anyBox, and
|
||||||
|
// propagateAlt calls the callback on this anyBox before calling it on
|
||||||
|
// its children.
|
||||||
|
propagate (func (anyBox) bool) bool
|
||||||
|
propagateAlt (func (anyBox) bool) bool
|
||||||
|
|
||||||
|
handleFocusEnter ()
|
||||||
|
handleFocusLeave ()
|
||||||
|
// handleDndEnter ()
|
||||||
|
// handleDndLeave ()
|
||||||
|
// handleDndDrop (data.Data)
|
||||||
|
handleMouseEnter ()
|
||||||
|
handleMouseLeave ()
|
||||||
|
handleMouseMove ()
|
||||||
|
handleMouseDown (input.Button)
|
||||||
|
handleMouseUp (input.Button)
|
||||||
|
handleScroll (float64, float64)
|
||||||
|
handleKeyDown (input.Key, bool)
|
||||||
|
handleKeyUp (input.Key, bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertAnyBox (unknown tomo.Box) anyBox {
|
||||||
|
if box, ok := unknown.(anyBox); ok {
|
||||||
|
return box
|
||||||
|
} else {
|
||||||
|
panic("system: foregin box implementation, i did not make this!")
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,9 @@
|
|||||||
package system
|
package system
|
||||||
|
|
||||||
|
import "errors"
|
||||||
import "git.tebibyte.media/tomo/tomo"
|
import "git.tebibyte.media/tomo/tomo"
|
||||||
|
|
||||||
func (this *System) NewSurfaceBox () (tomo.SurfaceBox, error) {
|
func (this *System) NewSurfaceBox () (tomo.SurfaceBox, error) {
|
||||||
|
// TODO
|
||||||
|
return nil, errors.New("system: not implemented yet")
|
||||||
}
|
}
|
||||||
|
@ -1,25 +1,30 @@
|
|||||||
package system
|
package system
|
||||||
|
|
||||||
import "io"
|
|
||||||
import "image"
|
import "image"
|
||||||
import "git.tebibyte.media/tomo/tomo/canvas"
|
import "git.tebibyte.media/tomo/tomo/canvas"
|
||||||
|
|
||||||
|
// System is coupled to a tomo.Backend implementation, and manages Hierarchies
|
||||||
|
// and Boxes.
|
||||||
type System struct {
|
type System struct {
|
||||||
link BackendLink
|
link BackendLink
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BackendLink allows the System to call up into the backend implementation
|
||||||
|
// which contains it in order to do things such as create new textures.
|
||||||
type BackendLink interface {
|
type BackendLink interface {
|
||||||
NewTexture (image.Image) canvas.TextureCloser
|
NewTexture (image.Image) canvas.TextureCloser
|
||||||
NewCanvas (image.Rectangle) canvas.Canvas
|
NewCanvas (image.Rectangle) canvas.Canvas
|
||||||
NewSurface (image.Rectangle) SurfaceLink
|
NewSurface (image.Rectangle) SurfaceLink
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SurfaceLink wraps a Surface created by the backend implementation, allowing
|
||||||
|
// the System a higher level of control over it.
|
||||||
type SurfaceLink interface {
|
type SurfaceLink interface {
|
||||||
io.Closer
|
|
||||||
GetSurface () any
|
GetSurface () any
|
||||||
SetSize (image.Rectangle)
|
SetSize (image.Rectangle)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// New creates a new System.
|
||||||
func New (link BackendLink) *System {
|
func New (link BackendLink) *System {
|
||||||
return &System {
|
return &System {
|
||||||
link: link,
|
link: link,
|
||||||
|
@ -1,7 +1,349 @@
|
|||||||
package system
|
package system
|
||||||
|
|
||||||
|
import "image"
|
||||||
|
import "image/color"
|
||||||
|
import "golang.org/x/image/font"
|
||||||
import "git.tebibyte.media/tomo/tomo"
|
import "git.tebibyte.media/tomo/tomo"
|
||||||
|
import "golang.org/x/image/math/fixed"
|
||||||
|
import "git.tebibyte.media/tomo/typeset"
|
||||||
|
import "git.tebibyte.media/tomo/tomo/text"
|
||||||
|
import "git.tebibyte.media/tomo/tomo/input"
|
||||||
|
import "git.tebibyte.media/tomo/tomo/event"
|
||||||
|
import "git.tebibyte.media/tomo/tomo/canvas"
|
||||||
|
|
||||||
|
type textBox struct {
|
||||||
|
*box
|
||||||
|
|
||||||
|
hOverflow, vOverflow bool
|
||||||
|
contentBounds image.Rectangle
|
||||||
|
scroll image.Point
|
||||||
|
|
||||||
|
text string
|
||||||
|
textColor color.Color
|
||||||
|
face font.Face
|
||||||
|
wrap bool
|
||||||
|
hAlign tomo.Align
|
||||||
|
vAlign tomo.Align
|
||||||
|
|
||||||
|
selectable bool
|
||||||
|
selecting bool
|
||||||
|
selectStart int
|
||||||
|
dot text.Dot
|
||||||
|
dotColor color.Color
|
||||||
|
|
||||||
|
drawer typeset.Drawer
|
||||||
|
|
||||||
|
on struct {
|
||||||
|
contentBoundsChange event.FuncBroadcaster
|
||||||
|
dotChange event.FuncBroadcaster
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (this *System) NewTextBox () tomo.TextBox {
|
func (this *System) NewTextBox () tomo.TextBox {
|
||||||
|
box := &textBox {
|
||||||
|
textColor: color.Black,
|
||||||
|
dotColor: color.RGBA { B: 255, G: 255, A: 255 },
|
||||||
|
}
|
||||||
|
box.box = this.newBox(box)
|
||||||
|
return box
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *textBox) SetOverflow (horizontal, vertical bool) {
|
||||||
|
if this.hOverflow == horizontal && this.vOverflow == vertical { return }
|
||||||
|
this.hOverflow = horizontal
|
||||||
|
this.vOverflow = vertical
|
||||||
|
this.invalidateLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *textBox) ContentBounds () image.Rectangle {
|
||||||
|
return this.contentBounds
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *textBox) ScrollTo (point image.Point) {
|
||||||
|
if this.scroll == point { return }
|
||||||
|
this.scroll = point
|
||||||
|
this.invalidateLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *textBox) OnContentBoundsChange (callback func()) event.Cookie {
|
||||||
|
return this.on.contentBoundsChange.Connect(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *textBox) SetText (text string) {
|
||||||
|
if this.text == text { return }
|
||||||
|
this.text = text
|
||||||
|
this.drawer.SetText([]rune(text))
|
||||||
|
this.invalidateMinimum()
|
||||||
|
this.invalidateLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *textBox) SetTextColor (c color.Color) {
|
||||||
|
if this.textColor == c { return }
|
||||||
|
this.textColor = c
|
||||||
|
this.invalidateDraw()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *textBox) SetFace (face font.Face) {
|
||||||
|
if this.face == face { return }
|
||||||
|
this.face = face
|
||||||
|
this.drawer.SetFace(face)
|
||||||
|
this.invalidateMinimum()
|
||||||
|
this.invalidateLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *textBox) SetWrap (wrap bool) {
|
||||||
|
if this.wrap == wrap { return }
|
||||||
|
this.drawer.SetWrap(wrap)
|
||||||
|
this.invalidateMinimum()
|
||||||
|
this.invalidateLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *textBox) SetSelectable (selectable bool) {
|
||||||
|
if this.selectable == selectable { return }
|
||||||
|
this.selectable = selectable
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *textBox) SetDotColor (c color.Color) {
|
||||||
|
if this.dotColor == c { return }
|
||||||
|
this.dotColor = c
|
||||||
|
if !this.dot.Empty() {
|
||||||
|
this.invalidateDraw()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *textBox) Select (dot text.Dot) {
|
||||||
|
if !this.selectable { return }
|
||||||
|
if this.dot == dot { return }
|
||||||
|
this.SetFocused(true)
|
||||||
|
this.dot = dot
|
||||||
|
this.scrollToDot()
|
||||||
|
this.on.dotChange.Broadcast()
|
||||||
|
this.invalidateDraw()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *textBox) Dot () text.Dot {
|
||||||
|
return this.dot
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *textBox) OnDotChange (callback func ()) event.Cookie {
|
||||||
|
return this.on.dotChange.Connect(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *textBox) SetAlign (x, y tomo.Align) {
|
||||||
|
if this.hAlign == x && this.vAlign == y { return }
|
||||||
|
this.hAlign = x
|
||||||
|
this.vAlign = y
|
||||||
|
this.drawer.SetAlign(typeset.Align(x), typeset.Align(y))
|
||||||
|
this.invalidateDraw()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *textBox) Draw (can canvas.Canvas) {
|
||||||
|
if can == nil { return }
|
||||||
|
this.drawBorders(can)
|
||||||
|
pen := can.Pen()
|
||||||
|
pen.Fill(this.color)
|
||||||
|
pen.Texture(this.texture)
|
||||||
|
|
||||||
|
if this.transparent() && this.parent != nil {
|
||||||
|
this.parent.drawBackgroundPart(can)
|
||||||
|
}
|
||||||
|
pen.Rectangle(can.Bounds())
|
||||||
|
|
||||||
|
if this.selectable && this.Focused() {
|
||||||
|
this.drawDot(can)
|
||||||
|
}
|
||||||
|
|
||||||
|
if this.face == nil { return }
|
||||||
|
this.drawer.Draw(can, this.textColor, this.textOffset())
|
||||||
|
}
|
||||||
|
|
||||||
|
func roundPt (point fixed.Point26_6) image.Point {
|
||||||
|
return image.Pt(point.X.Round(), point.Y.Round())
|
||||||
|
}
|
||||||
|
|
||||||
|
func fixPt (point image.Point) fixed.Point26_6 {
|
||||||
|
return fixed.P(point.X, point.Y)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *textBox) drawDot (can canvas.Canvas) {
|
||||||
|
if this.face == nil { return }
|
||||||
|
|
||||||
|
pen := can.Pen()
|
||||||
|
pen.Fill(color.Transparent)
|
||||||
|
pen.Stroke(this.textColor)
|
||||||
|
|
||||||
|
bounds := this.InnerBounds()
|
||||||
|
metrics := this.face.Metrics()
|
||||||
|
dot := this.dot.Canon()
|
||||||
|
start := this.drawer.PositionAt(dot.Start).Add(fixPt(this.textOffset()))
|
||||||
|
end := this.drawer.PositionAt(dot.End ).Add(fixPt(this.textOffset()))
|
||||||
|
height := this.drawer.LineHeight().Round()
|
||||||
|
ascent := fixed.Point26_6 { Y: metrics.Descent }
|
||||||
|
descent := fixed.Point26_6 { Y: metrics.Ascent }
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case dot.Empty():
|
||||||
|
pen.StrokeWeight(1)
|
||||||
|
pen.Path(roundPt(start.Add(ascent)), roundPt(start.Sub(descent)))
|
||||||
|
|
||||||
|
case start.Y == end.Y:
|
||||||
|
pen.Fill(this.dotColor)
|
||||||
|
pen.StrokeWeight(0)
|
||||||
|
pen.Rectangle(image.Rectangle {
|
||||||
|
Min: roundPt(start.Add(ascent)),
|
||||||
|
Max: roundPt(end.Sub(descent)),
|
||||||
|
})
|
||||||
|
|
||||||
|
default:
|
||||||
|
pen.Fill(this.dotColor)
|
||||||
|
pen.StrokeWeight(0)
|
||||||
|
|
||||||
|
rect := image.Rectangle {
|
||||||
|
Min: roundPt(start.Add(ascent)),
|
||||||
|
Max: roundPt(start.Sub(descent)),
|
||||||
|
}
|
||||||
|
rect.Max.X = bounds.Max.X
|
||||||
|
pen.Rectangle(rect)
|
||||||
|
if end.Y - start.Y > fixed.I(height) {
|
||||||
|
rect.Min.X = bounds.Min.X
|
||||||
|
rect.Min.Y = roundPt(start.Sub(descent)).Y + height
|
||||||
|
rect.Max.X = bounds.Max.X
|
||||||
|
rect.Max.Y = roundPt(end.Add(ascent)).Y - height
|
||||||
|
pen.Rectangle(rect)
|
||||||
|
}
|
||||||
|
rect = image.Rectangle {
|
||||||
|
Min: roundPt(end.Add(ascent)),
|
||||||
|
Max: roundPt(end.Sub(descent)),
|
||||||
|
}
|
||||||
|
rect.Min.X = bounds.Min.X
|
||||||
|
pen.Rectangle(rect)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *textBox) textOffset () image.Point {
|
||||||
|
return this.InnerBounds().Min.
|
||||||
|
Add(this.scroll).
|
||||||
|
Sub(this.drawer.LayoutBoundsSpace().Min)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *textBox) handleFocusLeave () {
|
||||||
|
this.on.dotChange.Broadcast()
|
||||||
|
this.invalidateDraw()
|
||||||
|
this.box.handleFocusLeave()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *textBox) handleMouseDown (button input.Button) {
|
||||||
|
if button == input.ButtonLeft {
|
||||||
|
index := this.runeUnderMouse()
|
||||||
|
this.selectStart = index
|
||||||
|
this.selecting = true
|
||||||
|
this.Select(text.Dot { Start: this.selectStart, End: index })
|
||||||
|
}
|
||||||
|
this.box.handleMouseDown(button)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *textBox) handleMouseUp (button input.Button) {
|
||||||
|
if button == input.ButtonLeft && this.selecting {
|
||||||
|
index := this.runeUnderMouse()
|
||||||
|
this.selecting = false
|
||||||
|
this.Select(text.Dot { Start: this.selectStart, End: index })
|
||||||
|
}
|
||||||
|
this.box.handleMouseUp(button)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *textBox) handleMouseMove () {
|
||||||
|
if this.selecting {
|
||||||
|
index := this.runeUnderMouse()
|
||||||
|
this.Select(text.Dot { Start: this.selectStart, End: index })
|
||||||
|
}
|
||||||
|
this.box.handleMouseMove()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *textBox) runeUnderMouse () int {
|
||||||
|
position := this.MousePosition().Sub(this.textOffset())
|
||||||
|
return this.drawer.AtPosition(fixPt(position))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *textBox) normalizedLayoutBoundsSpace () image.Rectangle {
|
||||||
|
bounds := this.drawer.LayoutBoundsSpace()
|
||||||
|
return bounds.Sub(bounds.Min)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *textBox) contentMinimum () image.Point {
|
||||||
|
minimum := this.drawer.MinimumSize()
|
||||||
|
|
||||||
|
if this.hOverflow || this.wrap {
|
||||||
|
minimum.X = this.drawer.Em().Round()
|
||||||
|
}
|
||||||
|
if this.vOverflow {
|
||||||
|
minimum.Y = this.drawer.LineHeight().Round()
|
||||||
|
}
|
||||||
|
|
||||||
|
return minimum.Add(this.box.contentMinimum())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *textBox) doLayout () {
|
||||||
|
this.box.doLayout()
|
||||||
|
previousContentBounds := this.contentBounds
|
||||||
|
|
||||||
|
innerBounds := this.InnerBounds()
|
||||||
|
this.drawer.SetWidth(innerBounds.Dx())
|
||||||
|
this.drawer.SetHeight(innerBounds.Dy())
|
||||||
|
|
||||||
|
this.contentBounds = this.normalizedLayoutBoundsSpace()
|
||||||
|
this.constrainScroll()
|
||||||
|
this.contentBounds = this.contentBounds.Add(this.scroll)
|
||||||
|
// println(this.InnerBounds().String(), this.contentBounds.String())
|
||||||
|
|
||||||
|
if previousContentBounds != this.contentBounds {
|
||||||
|
this.on.contentBoundsChange.Broadcast()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *textBox) constrainScroll () {
|
||||||
|
innerBounds := this.InnerBounds()
|
||||||
|
width := this.contentBounds.Dx()
|
||||||
|
height := this.contentBounds.Dy()
|
||||||
|
|
||||||
|
// X
|
||||||
|
if width <= innerBounds.Dx() {
|
||||||
|
this.scroll.X = 0
|
||||||
|
} else if this.scroll.X > 0 {
|
||||||
|
this.scroll.X = 0
|
||||||
|
} else if this.scroll.X < innerBounds.Dx() - width {
|
||||||
|
this.scroll.X = innerBounds.Dx() - width
|
||||||
|
}
|
||||||
|
|
||||||
|
// Y
|
||||||
|
if height <= innerBounds.Dy() {
|
||||||
|
this.scroll.Y = 0
|
||||||
|
} else if this.scroll.Y > 0 {
|
||||||
|
this.scroll.Y = 0
|
||||||
|
} else if this.scroll.Y < innerBounds.Dy() - height {
|
||||||
|
this.scroll.Y = innerBounds.Dy() - height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *textBox) scrollToDot () {
|
||||||
|
dot := roundPt(this.drawer.PositionAt(this.dot.End)).Add(this.textOffset())
|
||||||
|
innerBounds := this.InnerBounds()
|
||||||
|
scroll := this.scroll
|
||||||
|
em := this.drawer.Em().Round()
|
||||||
|
lineHeight := this.drawer.LineHeight().Round()
|
||||||
|
|
||||||
|
// X
|
||||||
|
if dot.X < innerBounds.Min.X + em {
|
||||||
|
scroll.X += innerBounds.Min.X - dot.X + em
|
||||||
|
} else if dot.X > innerBounds.Max.X - em {
|
||||||
|
scroll.X -= dot.X - innerBounds.Max.X + em
|
||||||
|
}
|
||||||
|
|
||||||
|
// Y
|
||||||
|
if dot.Y < innerBounds.Min.Y + lineHeight {
|
||||||
|
scroll.Y += innerBounds.Min.Y - dot.Y + lineHeight
|
||||||
|
} else if dot.Y > innerBounds.Max.Y - lineHeight {
|
||||||
|
scroll.Y -= dot.Y - innerBounds.Max.Y + lineHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ScrollTo(scroll)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user