package x import "image" import "image/color" import "git.tebibyte.media/tomo/tomo" import "git.tebibyte.media/tomo/x/canvas" 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" type textureMode int; const ( textureModeTile textureMode = iota textureModeCenter ) type box struct { backend *Backend 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 *xcanvas.Texture textureMode textureMode dndData data.Data dndAccept []data.Mime focused bool focusable bool canvas 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 (backend *Backend) newBox (outer anyBox) *box { box := &box { backend: backend, color: color.Transparent, outer: outer, drawer: outer, } if outer == nil { box.drawer = box box.outer = box } box.invalidateMinimum() return box } func (backend *Backend) NewBox() tomo.Box { box := backend.newBox(nil) return box } func (this *box) GetBox () tomo.Box { return this.outer } func (this *box) Window () tomo.Window { return this.parent.window() } 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 = xcanvas.AssertTexture(texture) this.invalidateDraw() } func (this *box) SetTextureCenter (texture canvas.Texture) { if this.texture == texture && this.textureMode == textureModeCenter { return } this.texture = xcanvas.AssertTexture(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) { if this.parent == nil || this.parent.window () == nil { focusedCopy := focused this.focusQueued = &focusedCopy return } if !this.focusable { return } if this.Focused () && !focused { this.parent.window().focus(nil) } else if !this.Focused() && focused { this.parent.window().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 { if this.parent == nil { return false } parentWindow := this.parent.window() if parentWindow == nil { return false } return this.outer == parentWindow.focused } func (this *box) Modifiers () input.Modifiers { return this.parent.window().modifiers } func (this *box) MousePosition () image.Point { return this.parent.window().mousePosition } // ----- 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 { 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) } } 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 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 ++ if this.canvas == nil { return } if this.drawer != nil { this.drawBorders(this.canvas) this.drawer.Draw(this.canvas.SubCanvas(this.innerClippingBounds)) } } // var laycnt int func (this *box) doLayout () { // println("LAYOUT", laycnt) // laycnt ++ this.innerClippingBounds = this.borderSum().Apply(this.bounds) if this.parent == nil { this.canvas = nil; return } parentCanvas := this.parent.canvas() if parentCanvas == nil { this.canvas = nil; return } this.canvas = parentCanvas.SubCanvas(this.bounds) } 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.parent == nil || this.parent.window() == 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) invalidateLayout () { if this.parent == nil || this.parent.window() == nil { return } this.window().invalidateLayout(this.outer) } func (this *box) invalidateDraw () { if this.parent == nil || this.parent.window() == nil { return } this.window().invalidateDraw(this.outer) } func (this *box) invalidateMinimum () { if this.parent == nil || this.parent.window() == nil { this.minSizeQueued = true return } this.window().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 { return transparent(this.color) && (this.texture == nil || !this.texture.Opaque()) } func (this *box) window () *window { if this.parent == nil { return nil } return this.parent.window() }