This repository has been archived on 2024-06-03. You can view files and clone it, but cannot push or open issues or pull requests.
x/box.go

523 lines
13 KiB
Go
Raw Permalink Normal View History

2023-07-02 06:52:14 +00:00
package x
import "image"
import "image/color"
import "git.tebibyte.media/tomo/tomo"
2023-08-23 23:21:28 +00:00
import "git.tebibyte.media/tomo/x/canvas"
2023-07-02 06:52:14 +00:00
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"
2024-05-26 19:04:58 +00:00
type textureMode int; const (
textureModeTile textureMode = iota
textureModeCenter
)
2023-07-02 06:52:14 +00:00
type box struct {
backend *Backend
2023-07-04 04:04:00 +00:00
parent parent
2023-07-05 07:25:50 +00:00
outer anyBox
2023-07-02 06:52:14 +00:00
2024-05-26 19:14:40 +00:00
visible bool
2024-05-26 19:04:58 +00:00
bounds image.Rectangle
minSize image.Point
userMinSize image.Point
2023-08-29 19:52:24 +00:00
innerClippingBounds image.Rectangle
minSizeQueued bool
focusQueued *bool
2023-07-02 06:52:14 +00:00
2024-05-26 19:04:58 +00:00
padding tomo.Inset
border []tomo.Border
color color.Color
texture *xcanvas.Texture
textureMode textureMode
2023-07-02 06:52:14 +00:00
dndData data.Data
dndAccept []data.Mime
focused bool
focusable bool
2023-07-04 04:04:00 +00:00
canvas canvas.Canvas
2023-07-02 06:52:14 +00:00
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 (backend *Backend) newBox (outer anyBox) *box {
2023-07-05 07:25:50 +00:00
box := &box {
2023-07-02 06:52:14 +00:00
backend: backend,
color: color.Transparent,
outer: outer,
drawer: outer,
2023-07-02 06:52:14 +00:00
}
if outer == nil {
box.drawer = box
box.outer = box
}
box.invalidateMinimum()
return box
}
func (backend *Backend) NewBox() tomo.Box {
box := backend.newBox(nil)
2023-07-05 07:25:50 +00:00
return box
2023-07-02 06:52:14 +00:00
}
2023-08-12 05:03:34 +00:00
func (this *box) GetBox () tomo.Box {
2023-08-08 16:56:49 +00:00
return this.outer
2023-07-02 06:52:14 +00:00
}
func (this *box) Window () tomo.Window {
2023-07-04 04:04:00 +00:00
return this.parent.window()
2023-07-02 06:52:14 +00:00
}
func (this *box) Bounds () image.Rectangle {
return this.bounds
}
func (this *box) InnerBounds () image.Rectangle {
2023-08-29 19:52:24 +00:00
return this.padding.Apply(this.innerClippingBounds)
2023-07-12 05:17:12 +00:00
}
func (this *box) MinimumSize () image.Point {
return this.minSize
}
2023-07-13 16:48:09 +00:00
func (this *box) borderSum () tomo.Inset {
sum := tomo.Inset { }
2023-07-02 06:52:14 +00:00
for _, border := range this.border {
2023-07-13 16:48:09 +00:00
sum[0] += border.Width[0]
sum[1] += border.Width[1]
sum[2] += border.Width[2]
sum[3] += border.Width[3]
2023-07-02 06:52:14 +00:00
}
2023-07-13 16:48:09 +00:00
return sum
}
2023-07-02 06:52:14 +00:00
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 }
2023-07-02 06:52:14 +00:00
if this.color == c { return }
this.color = c
this.invalidateDraw()
}
2024-05-26 19:04:58 +00:00
func (this *box) SetTextureTile (texture canvas.Texture) {
if this.texture == texture && this.textureMode == textureModeTile { return }
this.textureMode = textureModeTile
this.texture = xcanvas.AssertTexture(texture)
this.invalidateDraw()
}
func (this *box) SetTextureCenter (texture canvas.Texture) {
if this.texture == texture && this.textureMode == textureModeCenter { return }
2023-08-23 23:21:28 +00:00
this.texture = xcanvas.AssertTexture(texture)
2024-05-26 19:04:58 +00:00
this.textureMode = textureModeCenter
2023-07-02 06:52:14 +00:00
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
}
}
2023-07-02 06:52:14 +00:00
}
func (this *box) SetMinimumSize (size image.Point) {
if this.userMinSize == size { return }
this.userMinSize = size
this.invalidateMinimum()
2023-07-02 06:52:14 +00:00
}
func (this *box) SetPadding (padding tomo.Inset) {
if this.padding == padding { return }
this.padding = padding
this.invalidateLayout()
this.invalidateMinimum()
2023-07-02 06:52:14 +00:00
}
2024-05-26 19:14:40 +00:00
func (this *box) SetVisible (visible bool) {
if this.visible == visible { return }
this.visible = visible
this.invalidateMinimum()
}
func (this *box) Visible () bool {
return this.visible
}
2023-07-02 06:52:14 +00:00
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) {
if this.parent == nil || this.parent.window () == nil {
focusedCopy := focused
this.focusQueued = &focusedCopy
return
}
if !this.focusable { return }
2023-07-02 06:52:14 +00:00
if this.Focused () && !focused {
2023-07-04 04:04:00 +00:00
this.parent.window().focus(nil)
2023-07-02 06:52:14 +00:00
} else if !this.Focused() && focused {
2023-07-05 07:25:50 +00:00
this.parent.window().focus(this.outer)
2023-07-02 06:52:14 +00:00
}
}
func (this *box) SetFocusable (focusable bool) {
if this.focusable == focusable { return }
this.focusable = focusable
if !focusable {
this.SetFocused(false)
}
}
func (this *box) Focused () bool {
2023-07-16 04:56:13 +00:00
if this.parent == nil { return false }
parentWindow := this.parent.window()
if parentWindow == nil { return false }
return this.outer == parentWindow.focused
2023-07-02 06:52:14 +00:00
}
func (this *box) Modifiers () input.Modifiers {
2023-07-04 04:04:00 +00:00
return this.parent.window().modifiers
2023-07-02 06:52:14 +00:00
}
func (this *box) MousePosition () image.Point {
2023-07-04 04:04:00 +00:00
return this.parent.window().mousePosition
2023-07-02 06:52:14 +00:00
}
2023-07-05 07:25:50 +00:00
// ----- event handler setters ---------------------------------------------- //
2023-07-02 06:52:14 +00:00
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 {
if this.parent == nil { return }
window := this.parent.window()
if window == nil { return }
window.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)
}
}
2023-09-09 19:05:08 +00:00
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)
}
}
2023-07-02 06:52:14 +00:00
// -------------------------------------------------------------------------- //
func (this *box) Draw (can canvas.Canvas) {
2023-07-12 05:17:12 +00:00
if can == nil { return }
2023-07-02 06:52:14 +00:00
pen := can.Pen()
2024-05-26 19:04:58 +00:00
bounds := this.Bounds()
// background
2023-07-05 07:25:50 +00:00
pen.Fill(this.color)
2024-05-26 19:04:58 +00:00
if this.textureMode == textureModeTile {
pen.Texture(this.texture)
}
if this.transparent() && this.parent != nil {
this.parent.drawBackgroundPart(can)
}
2024-05-26 19:04:58 +00:00
pen.Rectangle(bounds)
// centered texture
if this.textureMode == textureModeCenter {
textureBounds := this.texture.Bounds()
2024-05-26 20:54:00 +00:00
textureOrigin :=
bounds.Min.
Add(image.Pt (
bounds.Dx() / 2,
bounds.Dy() / 2)).
Sub(image.Pt (
textureBounds.Dx() / 2,
textureBounds.Dy() / 2))
2024-05-26 19:04:58 +00:00
pen.Fill(color.Transparent)
pen.Texture(this.texture)
pen.Rectangle(textureBounds.Sub(textureBounds.Min).Add(textureOrigin))
}
2023-07-05 07:25:50 +00:00
}
2023-07-02 06:52:14 +00:00
2023-07-05 07:25:50 +00:00
func (this *box) drawBorders (can canvas.Canvas) {
2023-07-12 05:17:12 +00:00
if can == nil { return }
2023-07-05 07:25:50 +00:00
pen := can.Pen()
2023-07-02 06:52:14 +00:00
bounds := this.bounds
rectangle := func (x0, y0, x1, y1 int, c color.Color) {
area := image.Rect(x0, y0, x1, y1)
if transparent(c) && this.parent != nil {
2024-05-26 19:18:27 +00:00
this.parent.drawBackgroundPart(can.SubCanvas(area))
}
pen.Fill(c)
pen.Rectangle(area)
}
2023-07-02 06:52:14 +00:00
for _, border := range this.border {
rectangle (
2023-07-02 06:52:14 +00:00
bounds.Min.X,
bounds.Min.Y,
bounds.Max.X,
bounds.Min.Y + border.Width[tomo.SideTop],
border.Color[tomo.SideTop])
rectangle (
2023-07-02 06:52:14 +00:00
bounds.Min.X,
bounds.Max.Y - border.Width[tomo.SideBottom],
bounds.Max.X,
bounds.Max.Y,
border.Color[tomo.SideBottom])
rectangle (
2023-07-02 06:52:14 +00:00
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 (
2023-07-02 06:52:14 +00:00
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])
2023-07-02 06:52:14 +00:00
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
2023-07-04 04:04:00 +00:00
func (this *box) doDraw () {
// println("DRAW", drawcnt)
// drawcnt ++
2023-07-04 04:04:00 +00:00
if this.canvas == nil { return }
2023-07-05 07:25:50 +00:00
if this.drawer != nil {
2023-07-12 05:17:12 +00:00
this.drawBorders(this.canvas)
2024-05-26 19:18:27 +00:00
this.drawer.Draw(this.canvas.SubCanvas(this.innerClippingBounds))
2023-07-04 04:04:00 +00:00
}
}
// var laycnt int
2023-07-04 04:04:00 +00:00
func (this *box) doLayout () {
// println("LAYOUT", laycnt)
// laycnt ++
2023-08-29 19:52:24 +00:00
this.innerClippingBounds = this.borderSum().Apply(this.bounds)
2023-07-16 04:42:47 +00:00
if this.parent == nil { this.canvas = nil; return }
parentCanvas := this.parent.canvas()
if parentCanvas == nil { this.canvas = nil; return }
2024-05-26 19:18:27 +00:00
this.canvas = parentCanvas.SubCanvas(this.bounds)
2023-07-04 04:04:00 +00:00
}
func (this *box) setParent (parent parent) {
2023-07-22 03:22:03 +00:00
if this.parent != parent && this.Focused() {
this.SetFocused(false)
}
2023-07-04 04:04:00 +00:00
this.parent = parent
}
2023-09-09 19:05:08 +00:00
func (this *box) getParent () parent {
return this.parent
}
func (this *box) flushActionQueue () {
if this.parent == nil || this.parent.window() == nil { return }
if this.minSizeQueued {
this.invalidateMinimum()
}
if this.focusQueued != nil {
this.SetFocused(*this.focusQueued)
}
}
2023-07-04 04:04:00 +00:00
func (this *box) recursiveRedo () {
this.doLayout()
this.doDraw()
}
2023-07-05 07:25:50 +00:00
func (this *box) invalidateLayout () {
if this.parent == nil || this.parent.window() == nil { return }
2024-05-26 19:14:40 +00:00
this.window().invalidateLayout(this.outer)
2023-07-02 06:52:14 +00:00
}
2023-07-05 07:25:50 +00:00
func (this *box) invalidateDraw () {
if this.parent == nil || this.parent.window() == nil { return }
2024-05-26 19:14:40 +00:00
this.window().invalidateDraw(this.outer)
2023-07-02 06:52:14 +00:00
}
2023-07-13 16:48:09 +00:00
func (this *box) invalidateMinimum () {
if this.parent == nil || this.parent.window() == nil {
this.minSizeQueued = true
return
2023-07-13 16:48:09 +00:00
}
2024-05-26 19:14:40 +00:00
this.window().invalidateMinimum(this.outer)
2023-07-13 16:48:09 +00:00
}
2023-07-16 04:42:47 +00:00
func (this *box) canBeFocused () bool {
return this.focusable
}
2023-09-09 19:05:08 +00:00
func (this *box) boxUnder (point image.Point, category eventCategory) anyBox {
2023-07-16 04:42:47 +00:00
if point.In(this.bounds) {
return this.outer
} else {
return nil
}
}
2023-07-22 03:22:03 +00:00
func (this *box) propagate (callback func (anyBox) bool) bool {
2023-08-12 15:55:25 +00:00
return callback(this.outer)
2023-07-22 03:22:03 +00:00
}
func (this *box) propagateAlt (callback func (anyBox) bool) bool {
2023-08-12 15:55:25 +00:00
return callback(this.outer)
2023-07-22 03:22:03 +00:00
}
func (this *box) transparent () bool {
return transparent(this.color) &&
(this.texture == nil || !this.texture.Opaque())
}
2024-05-26 19:14:40 +00:00
func (this *box) window () *window {
if this.parent == nil { return nil }
return this.parent.window()
}