Update code for internal system

This commit is contained in:
Sasha Koshka 2024-07-25 13:01:15 -04:00
parent 9b61600f31
commit 196afbc2f3
12 changed files with 785 additions and 495 deletions

View File

@ -0,0 +1,38 @@
package system
import "git.tebibyte.media/tomo/tomo"
type attrHierarchy [T tomo.Attr] struct {
style T
user T
userExists bool
}
func (this *attrHierarchy[T]) SetStyle (style T) (different bool) {
styleEquals := this.style.Equals(style)
this.style = style
return !styleEquals && !this.userExists
}
func (this *attrHierarchy[T]) SetUser (user T) (different bool) {
userEquals := this.user.Equals(user)
this.user = user
this.userExists = true
return !userEquals
}
func (this *attrHierarchy[T]) Set (attr T, user bool) (different bool) {
if user {
return this.SetUser(attr)
} else {
return this.SetStyle(attr)
}
}
func (this *attrHierarchy[T]) Value () T {
if this.userExists {
return this.user
} else {
return this.style
}
}

View File

@ -9,66 +9,64 @@ import "git.tebibyte.media/tomo/tomo/event"
import "git.tebibyte.media/tomo/tomo/canvas"
import "git.tebibyte.media/tomo/backend/internal/util"
type textureMode int; const (
textureModeTile textureMode = iota
textureModeCenter
)
type box struct {
system *System
parent parent
outer anyBox
role tomo.Role
styleCookie event.Cookie
lastStyleNonce int
lastIconsNonce int
tags util.Set[string]
role tomo.Role
lastStyleNonce int
lastIconsNonce int
styleApplicator *styleApplicator
minSize util.Memo[image.Point]
bounds image.Rectangle
minSize image.Point
userMinSize image.Point
innerClippingBounds image.Rectangle
minSizeQueued bool
focusQueued *bool
focusQueued *bool
padding tomo.Inset
border []tomo.Border
color color.Color
texture canvas.Texture
textureMode textureMode
attrColor attrHierarchy[tomo.AttrColor]
attrTexture attrHierarchy[tomo.AttrTexture]
attrTextureMode attrHierarchy[tomo.AttrTextureMode]
attrBorder attrHierarchy[tomo.AttrBorder]
attrMinimumSize attrHierarchy[tomo.AttrMinimumSize]
attrPadding attrHierarchy[tomo.AttrPadding]
dndData data.Data
dndAccept []data.Mime
focused bool
focusable bool
hovered bool
focused bool
pressed 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)]
styleChange event.FuncBroadcaster
iconsChange event.FuncBroadcaster
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.Broadcaster[func () bool]
buttonDown event.Broadcaster[func (input.Button) bool]
buttonUp event.Broadcaster[func (input.Button) bool]
scroll event.Broadcaster[func (float64, float64) bool]
keyDown event.Broadcaster[func (input.Key, bool) bool]
keyUp event.Broadcaster[func (input.Key, bool) bool]
styleChange event.FuncBroadcaster
iconSetChange event.FuncBroadcaster
}
}
func (this *System) newBox (outer anyBox) *box {
box := &box {
system: this,
color: color.Transparent,
outer: outer,
drawer: outer,
}
@ -82,15 +80,16 @@ func (this *System) newBox (outer anyBox) *box {
box.drawer = box
box.outer = box
}
box.invalidateMinimum()
box.minSize = util.NewMemo(box.calculateMinimumSize)
return box
}
func (this *System) NewBox () tomo.Box {
return this.newBox(nil)
}
// ----- public methods ----------------------------------------------------- //
func (this *box) GetBox () tomo.Box {
return this.outer
}
@ -106,102 +105,13 @@ func (this *box) Bounds () image.Rectangle {
}
func (this *box) InnerBounds () image.Rectangle {
return this.padding.Apply(this.innerClippingBounds)
}
func (this *box) MinimumSize () image.Point {
return this.minSize
return tomo.Inset(this.attrPadding.Value()).Apply(this.innerClippingBounds)
}
func (this *box) Role () tomo.Role {
return this.role
}
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) borderAndPaddingSum () tomo.Inset {
sum := this.borderSum()
sum[0] += this.padding[0]
sum[1] += this.padding[1]
sum[2] += this.padding[2]
sum[3] += this.padding[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) SetRole (role tomo.Role) {
if this.role == role { return }
this.role = role
@ -209,6 +119,27 @@ func (this *box) SetRole (role tomo.Role) {
this.outer.recursiveReApply()
}
func (this *box) Tag (tag string) bool {
switch tag {
case "hovered": return this.hovered
case "focused": return this.focused
case "pressed": return this.pressed
default: return this.tags.Has(tag)
}
}
func (this *box) SetTag (tag string, on bool) {
if on {
this.tags.Add(tag)
} else {
delete(this.tags, tag)
}
}
func (this *box) SetAttr (attr tomo.Attr) {
this.outer.setAttr(attr, true)
}
func (this *box) SetDNDData (dat data.Data) {
this.dndData = dat
}
@ -233,6 +164,12 @@ func (this *box) SetFocused (focused bool) {
}
}
func (this *box) Focused () bool {
hierarchy := this.getHierarchy()
if hierarchy == nil { return false }
return hierarchy.isFocused(this.outer)
}
func (this *box) SetFocusable (focusable bool) {
if this.focusable == focusable { return }
this.focusable = focusable
@ -241,22 +178,83 @@ func (this *box) SetFocusable (focusable bool) {
}
}
func (this *box) Focused () bool {
hierarchy := this.getHierarchy()
if hierarchy == nil { return false }
return hierarchy.isFocused(this.outer)
// ----- private methods ---------------------------------------------------- //
func (this *box) setAttr (attr tomo.Attr, user bool) {
switch attr := attr.(type) {
case tomo.AttrColor:
if this.attrColor.Set(attr, user) {
this.invalidateDraw()
}
case tomo.AttrTexture:
if this.attrTexture.Set(attr, user) {
this.invalidateDraw()
}
case tomo.AttrTextureMode:
if this.attrTextureMode.Set(attr, user) {
this.invalidateDraw()
}
case tomo.AttrBorder:
previousBorderSum := this.borderSum()
different := this.attrBorder.Set(attr, user)
// only invalidate the layout if the border is sized differently
if this.borderSum() != previousBorderSum {
this.invalidateLayout()
this.invalidateMinimum()
}
// if the border takes up the same amount of space, only invalidate the
// drawing if it looks different
if different {
this.invalidateDraw()
}
case tomo.AttrMinimumSize:
if this.attrMinimumSize.Set(attr, user) {
this.invalidateMinimum()
}
case tomo.AttrPadding:
if this.attrPadding.Set(attr, true) {
this.invalidateLayout()
this.invalidateMinimum()
}
}
}
func (this *box) Modifiers () input.Modifiers {
hierarchy := this.getHierarchy()
if hierarchy == nil { return input.Modifiers { } }
return hierarchy.getModifiers()
func (this *box) setBounds (bounds image.Rectangle) {
if this.bounds == bounds { return }
this.bounds = bounds
this.invalidateLayout()
}
func (this *box) MousePosition () image.Point {
hierarchy := this.getHierarchy()
if hierarchy == nil { return image.Point { } }
return hierarchy.getMousePosition()
func (this *box) minimumSize () image.Point {
return this.minSize.Value()
}
func (this *box) borderSum () tomo.Inset {
sum := tomo.Inset { }
for _, border := range this.attrBorder.Value() {
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) borderAndPaddingSum () tomo.Inset {
sum := this.borderSum()
padding := this.attrPadding.Value()
sum[0] += padding[0]
sum[1] += padding[1]
sum[2] += padding[2]
sum[3] += padding[3]
return sum
}
// ----- event handler setters ---------------------------------------------- //
@ -281,46 +279,62 @@ func (this *box) OnMouseEnter (callback func()) event.Cookie {
func (this *box) OnMouseLeave (callback func()) event.Cookie {
return this.on.mouseLeave.Connect(callback)
}
func (this *box) OnMouseMove (callback func()) event.Cookie {
func (this *box) OnMouseMove (callback func() bool) 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) OnButtonDown (callback func(input.Button) bool) event.Cookie {
return this.on.buttonDown.Connect(callback)
}
func (this *box) OnMouseUp (callback func(input.Button)) event.Cookie {
return this.on.mouseUp.Connect(callback)
func (this *box) OnButtonUp (callback func(input.Button) bool) event.Cookie {
return this.on.buttonUp.Connect(callback)
}
func (this *box) OnScroll (callback func(deltaX, deltaY float64)) event.Cookie {
func (this *box) OnScroll (callback func(float64, float64) bool) event.Cookie {
return this.on.scroll.Connect(callback)
}
func (this *box) OnKeyDown (callback func(key input.Key, numberPad bool)) event.Cookie {
func (this *box) OnKeyDown (callback func(input.Key, bool) bool) event.Cookie {
return this.on.keyDown.Connect(callback)
}
func (this *box) OnKeyUp (callback func(key input.Key, numberPad bool)) event.Cookie {
func (this *box) OnKeyUp (callback func(input.Key, bool) bool) event.Cookie {
return this.on.keyUp.Connect(callback)
}
func (this *box) OnStyleChange (callback func()) event.Cookie {
return this.on.styleChange.Connect(callback)
}
func (this *box) OnIconsChange (callback func()) event.Cookie {
return this.on.iconsChange.Connect(callback)
func (this *box) OnIconSetChange (callback func()) event.Cookie {
return this.on.iconSetChange.Connect(callback)
}
func (this *box) handleFocusEnter () {
this.focused = true
this.invalidateStyle()
this.on.focusEnter.Broadcast()
}
func (this *box) handleFocusLeave () {
this.focused = false
this.invalidateStyle()
this.on.focusLeave.Broadcast()
}
func (this *box) handleMouseEnter () {
this.hovered = true
this.invalidateStyle()
this.on.mouseEnter.Broadcast()
}
func (this *box) handleMouseLeave () {
this.hovered = false
this.invalidateStyle()
this.on.mouseLeave.Broadcast()
}
func (this *box) handleMouseMove () {
this.on.mouseMove.Broadcast()
func (this *box) handleMouseMove () (caught bool) {
for _, listener := range this.on.mouseMove.Listeners() {
if listener() { caught = true }
}
return
}
func (this *box) handleMouseDown (button input.Button) {
func (this *box) handleMouseDown (button input.Button) (caught bool) {
if button == input.ButtonLeft {
this.pressed = true
this.invalidateStyle()
}
if this.focusable {
this.SetFocused(true)
} else {
@ -328,29 +342,39 @@ func (this *box) handleMouseDown (button input.Button) {
if hierarchy == nil { return }
hierarchy.focus(nil)
}
for _, listener := range this.on.mouseDown.Listeners() {
listener(button)
for _, listener := range this.on.buttonDown.Listeners() {
if listener(button) { caught = true }
}
return
}
func (this *box) handleMouseUp (button input.Button) {
for _, listener := range this.on.mouseUp.Listeners() {
listener(button)
func (this *box) handleMouseUp (button input.Button) (caught bool) {
if button == input.ButtonLeft {
this.pressed = false
this.invalidateStyle()
}
for _, listener := range this.on.buttonUp.Listeners() {
if listener(button) { caught = true }
}
return
}
func (this *box) handleScroll (x, y float64) {
func (this *box) handleScroll (x, y float64) (caught bool) {
for _, listener := range this.on.scroll.Listeners() {
listener(x, y)
if listener(x, y) { caught = true }
}
return
}
func (this *box) handleKeyDown (key input.Key, numberPad bool) {
func (this *box) handleKeyDown (key input.Key, numberPad bool) (caught bool) {
for _, listener := range this.on.keyDown.Listeners() {
listener(key, numberPad)
if listener(key, numberPad) { caught = true }
}
return
}
func (this *box) handleKeyUp (key input.Key, numberPad bool) {
func (this *box) handleKeyUp (key input.Key, numberPad bool) (caught bool) {
for _, listener := range this.on.keyUp.Listeners() {
listener(key, numberPad)
if listener(key, numberPad) { caught = true }
}
return
}
// -------------------------------------------------------------------------- //
@ -359,19 +383,25 @@ func (this *box) Draw (can canvas.Canvas) {
pen := can.Pen()
bounds := this.Bounds()
// get values
textureMode := tomo.TextureMode(this.attrTextureMode.Value())
texture := this.attrTexture.Value().Texture
col := this.attrColor.Value().Color
if col == nil { col = color.Transparent }
// background
pen.Fill(this.color)
if this.textureMode == textureModeTile {
pen.Texture(this.texture)
}
if this.transparent() && this.parent != nil {
this.parent.drawBackgroundPart(can)
}
pen.Fill(col)
if textureMode == tomo.TextureModeTile && texture != nil {
pen.Texture(texture)
}
pen.Rectangle(bounds)
// centered texture
if this.textureMode == textureModeCenter && this.texture != nil {
textureBounds := this.texture.Bounds()
if textureMode == tomo.TextureModeCenter && texture != nil {
textureBounds := texture.Bounds()
textureOrigin :=
bounds.Min.
Add(image.Pt (
@ -382,7 +412,7 @@ func (this *box) Draw (can canvas.Canvas) {
textureBounds.Dy() / 2))
pen.Fill(color.Transparent)
pen.Texture(this.texture)
pen.Texture(texture)
pen.Rectangle(textureBounds.Sub(textureBounds.Min).Add(textureOrigin))
}
}
@ -394,6 +424,7 @@ func (this *box) drawBorders (can canvas.Canvas) {
rectangle := func (x0, y0, x1, y1 int, c color.Color) {
area := image.Rect(x0, y0, x1, y1)
if area.Empty() { return }
if util.Transparent(c) && this.parent != nil {
this.parent.drawBackgroundPart(can.SubCanvas(area))
}
@ -401,7 +432,7 @@ func (this *box) drawBorders (can canvas.Canvas) {
pen.Rectangle(area)
}
for _, border := range this.border {
for _, border := range this.attrBorder.Value() {
rectangle (
bounds.Min.X,
bounds.Min.Y,
@ -433,26 +464,29 @@ func (this *box) drawBorders (can canvas.Canvas) {
func (this *box) contentMinimum () image.Point {
var minimum image.Point
minimum.X += this.padding.Horizontal()
minimum.Y += this.padding.Vertical()
padding := tomo.Inset(this.attrPadding.Value())
minimum.X += padding.Horizontal()
minimum.Y += 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
func (this *box) calculateMinimumSize () image.Point {
userMinSize := this.attrMinimumSize.Value()
minSize := this.outer.contentMinimum()
if minSize.X < userMinSize.X {
minSize.X = userMinSize.X
}
if this.minSize.Y < this.userMinSize.Y {
this.minSize.Y = this.userMinSize.Y
if minSize.Y < userMinSize.Y {
minSize.Y = userMinSize.Y
}
if this.parent != nil {
this.parent.notifyMinimumSizeChange(this)
}
return minSize
}
// var drawcnt int
@ -477,6 +511,10 @@ func (this *box) doLayout () {
this.outer.recursiveLoseCanvas()
}
func (this *box) doStyle () {
this.styleApplicator.apply(this)
}
func (this *box) setParent (parent parent) {
if this.parent != parent && this.Focused() {
this.SetFocused(false)
@ -491,10 +529,6 @@ func (this *box) getParent () parent {
func (this *box) flushActionQueue () {
if this.getHierarchy() == nil { return }
if this.minSizeQueued {
this.invalidateMinimum()
}
if this.focusQueued != nil {
this.SetFocused(*this.focusQueued)
}
@ -509,6 +543,12 @@ func (this *box) recursiveLoseCanvas () {
this.canvas.InvalidateTo(nil)
}
func (this *box) invalidateStyle () {
hierarchy := this.getHierarchy()
if hierarchy == nil { return }
hierarchy.invalidateStyle(this.outer)
}
func (this *box) invalidateLayout () {
hierarchy := this.getHierarchy()
if hierarchy == nil { return }
@ -522,12 +562,7 @@ func (this *box) invalidateDraw () {
}
func (this *box) invalidateMinimum () {
hierarchy := this.getHierarchy()
if hierarchy == nil {
this.minSizeQueued = true
} else {
hierarchy.invalidateMinimum(this.outer)
}
this.minSize.Invalidate()
}
func (this *box) recursiveReApply () {
@ -538,17 +573,13 @@ func (this *box) recursiveReApply () {
// style
hierarchyStyleNonce := this.getStyleNonce()
if this.lastStyleNonce != hierarchyStyleNonce {
// remove old style
// i should probably explain why we have a specific style
// applicator for every box, it's so style applicators can cache
// information about the boxes they're linked to (like all rules
// with a matching role).
this.lastStyleNonce = hierarchyStyleNonce
if this.styleCookie != nil {
this.styleCookie.Close()
this.styleCookie = nil
}
// apply new one
if style := this.getStyle(); style != nil {
this.styleCookie = style.Apply(this.outer)
}
this.styleApplicator = this.getHierarchy().newStyleApplicator()
this.invalidateStyle()
this.on.styleChange.Broadcast()
}
@ -556,7 +587,7 @@ func (this *box) recursiveReApply () {
hierarchyIconsNonce := this.getIconsNonce()
if this.lastIconsNonce != hierarchyIconsNonce {
this.lastIconsNonce = hierarchyIconsNonce
this.on.iconsChange.Broadcast()
this.on.iconSetChange.Broadcast()
}
}
@ -564,7 +595,7 @@ func (this *box) canBeFocused () bool {
return this.focusable
}
func (this *box) boxUnder (point image.Point, category eventCategory) anyBox {
func (this *box) boxUnder (point image.Point) anyBox {
if point.In(this.bounds) {
return this.outer
} else {
@ -583,7 +614,7 @@ func (this *box) propagateAlt (callback func (anyBox) bool) bool {
func (this *box) transparent () bool {
// TODO uncomment once we have
// a way to detect texture transparency
return util.Transparent(this.color) /*&&
return util.Transparent(this.attrColor.Value()) /*&&
(this.texture == nil || !this.texture.Opaque())*/
}
@ -593,12 +624,6 @@ func (this *box) getWindow () tomo.Window {
return hierarchy.getWindow()
}
func (this *box) getStyle () tomo.Style {
hierarchy := this.getHierarchy()
if hierarchy == nil { return nil }
return hierarchy.getStyle()
}
func (this *box) getHierarchy () *Hierarchy {
if this.parent == nil { return nil }
return this.parent.getHierarchy()

View File

@ -0,0 +1,63 @@
package system
import "image"
type boxQuerier []anyBox
func (querier boxQuerier) Len () int {
return len(querier)
}
func (querier boxQuerier) MinimumSize (index int) image.Point {
if box, ok := querier.box(index); ok {
return box.minimumSize()
}
return image.Point { }
}
func (querier boxQuerier) RecommendedWidth (index int, height int) int {
if box, ok := querier.box(index); ok {
if box, ok := box.(anyContentBox); ok {
return box.recommendedWidth(height)
}
}
return 0
}
func (querier boxQuerier) RecommendedHeight (index int, width int) int {
if box, ok := querier.box(index); ok {
if box, ok := box.(anyContentBox); ok {
return box.recommendedHeight(width)
}
}
return 0
}
func (querier boxQuerier) box (index int) (anyBox, bool) {
if index < 0 || index >= len(querier) { return nil, false }
return querier[index], true
}
type boxArranger []anyBox
func (arranger boxArranger) Len () int {
return boxQuerier(arranger).Len()
}
func (arranger boxArranger) MinimumSize (index int) image.Point {
return boxQuerier(arranger).MinimumSize(index)
}
func (arranger boxArranger) RecommendedWidth (index int, height int) int {
return boxQuerier(arranger).RecommendedWidth(index, height)
}
func (arranger boxArranger) RecommendedHeight (index int, width int) int {
return boxQuerier(arranger).RecommendedHeight(index, width)
}
func (arranger boxArranger) SetBounds (index int, bounds image.Rectangle) {
if box, ok := boxQuerier(arranger).box(index); ok {
box.setBounds(bounds)
}
}

View File

@ -15,10 +15,6 @@ func (this *System) NewCanvasBox () tomo.CanvasBox {
return box
}
func (this *canvasBox) Box () tomo.Box {
return this
}
func (this *canvasBox) SetDrawer (drawer canvas.Drawer) {
this.userDrawer = drawer
this.invalidateDraw()
@ -32,7 +28,8 @@ func (this *canvasBox) Draw (can canvas.Canvas) {
if can == nil { return }
this.box.Draw(can)
if this.userDrawer != nil {
padding := tomo.Inset(this.attrPadding.Value())
this.userDrawer.Draw (
can.SubCanvas(this.padding.Apply(this.innerClippingBounds)))
can.SubCanvas(padding.Apply(this.innerClippingBounds)))
}
}

View File

@ -10,14 +10,16 @@ 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
contentBounds image.Rectangle
scroll image.Point
mask bool
gap image.Point
children []tomo.Box
attrGap attrHierarchy[tomo.AttrGap]
attrAlign attrHierarchy[tomo.AttrAlign]
attrOverflow attrHierarchy[tomo.AttrOverflow]
attrLayout attrHierarchy[tomo.AttrLayout]
children []anyBox
layout tomo.Layout
on struct {
@ -31,37 +33,7 @@ func (this *System) NewContainerBox () tomo.ContainerBox {
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()
}
// ----- public methods ----------------------------------------------------- //
func (this *containerBox) ContentBounds () image.Rectangle {
return this.contentBounds
@ -73,54 +45,13 @@ func (this *containerBox) ScrollTo (point image.Point) {
this.invalidateLayout()
}
func (this *containerBox) RecommendedHeight (width int) int {
if this.layout == nil || this.vOverflow {
return this.MinimumSize().Y
} else {
return this.layout.RecommendedHeight(this.layoutHints(), this.children, width) +
this.borderAndPaddingSum().Vertical()
}
}
func (this *containerBox) RecommendedWidth (height int) int {
if this.layout == nil || this.hOverflow {
return this.MinimumSize().X
} else {
return this.layout.RecommendedWidth(this.layoutHints(), this.children, height) +
this.borderAndPaddingSum().Horizontal()
}
}
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 }
if util.IndexOf(this.children, box) > -1 { return }
box.setParent(this)
box.flushActionQueue()
@ -131,7 +62,7 @@ func (this *containerBox) Add (child tomo.Object) {
func (this *containerBox) Remove (child tomo.Object) {
box := assertAnyBox(child.GetBox())
index := util.IndexOf(this.children, tomo.Box(box))
index := util.IndexOf(this.children, box)
if index < 0 { return }
box.setParent(nil)
@ -142,15 +73,15 @@ func (this *containerBox) Remove (child tomo.Object) {
func (this *containerBox) Insert (child, before tomo.Object) {
box := assertAnyBox(child.GetBox())
if util.IndexOf(this.children, tomo.Box(box)) > -1 { return }
if util.IndexOf(this.children, box) > -1 { return }
beforeBox := assertAnyBox(before.GetBox())
index := util.IndexOf(this.children, tomo.Box(beforeBox))
index := util.IndexOf(this.children, beforeBox)
if index < 0 {
this.children = append(this.children, tomo.Box(box))
this.children = append(this.children, box)
} else {
this.children = util.Insert(this.children, index, tomo.Box(box))
this.children = util.Insert(this.children, index, box)
}
box.setParent(this)
@ -167,7 +98,7 @@ func (this *containerBox) Clear () {
this.invalidateMinimum()
}
func (this *containerBox) Length () int {
func (this *containerBox) Len () int {
return len(this.children)
}
@ -178,14 +109,19 @@ func (this *containerBox) At (index int) tomo.Object {
return this.children[index]
}
func (this *containerBox) SetLayout (layout tomo.Layout) {
this.layout = layout
this.invalidateLayout()
this.invalidateMinimum()
func (this *containerBox) SetInputMask (mask bool) {
this.mask = mask
}
// ----- private methods ---------------------------------------------------- //
func (this *containerBox) Draw (can canvas.Canvas) {
if can == nil { return }
// textureMode := tomo.TextureMode(this.attrTextureMode.Value())
texture := this.attrTexture.Value().Texture
col := this.attrColor.Value().Color
if col == nil { col = color.Transparent }
rocks := make([]image.Rectangle, len(this.children))
for index, box := range this.children {
@ -198,17 +134,87 @@ func (this *containerBox) Draw (can canvas.Canvas) {
}
if clipped == nil { continue }
pen := clipped.Pen()
pen.Fill(this.color)
pen.Texture(this.texture)
pen.Fill(col)
pen.Texture(texture)
pen.Rectangle(this.innerClippingBounds)
}
}
func (this *containerBox) setAttr (attr tomo.Attr, user bool) {
switch attr := attr.(type) {
case tomo.AttrColor:
if this.attrColor.Set(attr, user) {
this.invalidateTransparentChildren()
this.invalidateDraw()
}
case tomo.AttrTexture:
if this.attrTexture.Set(attr, user) {
this.invalidateTransparentChildren()
this.invalidateDraw()
}
case tomo.AttrTextureMode:
if this.attrTextureMode.Set(attr, user) {
this.invalidateTransparentChildren()
this.invalidateDraw()
}
case tomo.AttrGap:
if this.attrGap.Set(attr, user) {
this.invalidateLayout()
this.invalidateMinimum()
}
case tomo.AttrAlign:
if this.attrAlign.Set(attr, user) {
this.invalidateLayout()
}
case tomo.AttrOverflow:
if this.attrOverflow.Set(attr, user) {
this.invalidateLayout()
}
case tomo.AttrLayout:
if this.attrLayout.Set(attr, user) {
this.invalidateLayout()
this.invalidateMinimum()
}
default: this.box.setAttr(attr, user)
}
}
func (this *containerBox) recommendedHeight (width int) int {
if this.layout == nil || this.attrOverflow.Value().Y {
return this.minSize.Value().Y
} else {
return this.layout.RecommendedHeight(this.layoutHints(), this.boxQuerier(), width) +
this.borderAndPaddingSum().Vertical()
}
}
func (this *containerBox) recommendedWidth (height int) int {
if this.layout == nil || this.attrOverflow.Value().X {
return this.minSize.Value().X
} else {
return this.layout.RecommendedWidth(this.layoutHints(), this.boxQuerier(), height) +
this.borderAndPaddingSum().Horizontal()
}
}
func (this *containerBox) drawBackgroundPart (can canvas.Canvas) {
if can == nil { return }
pen := can.Pen()
pen.Fill(this.color)
pen.Texture(this.texture)
// textureMode := tomo.TextureMode(this.attrTextureMode.Value())
texture := this.attrTexture.Value().Texture
col := this.attrColor.Value().Color
if col == nil { col = color.Transparent }
pen.Fill(col)
pen.Texture(texture)
if this.transparent() && this.parent != nil {
this.parent.drawBackgroundPart(can)
@ -245,7 +251,7 @@ func (this *containerBox) getCanvas () canvas.Canvas {
func (this *containerBox) notifyMinimumSizeChange (child anyBox) {
this.invalidateMinimum()
size := child.MinimumSize()
size := child.minimumSize()
bounds := child.Bounds()
if bounds.Dx() < size.X || bounds.Dy() < size.Y {
this.invalidateLayout()
@ -253,23 +259,27 @@ func (this *containerBox) notifyMinimumSizeChange (child anyBox) {
}
func (this *containerBox) layoutHints () tomo.LayoutHints {
overflow := this.attrOverflow.Value()
align := this.attrAlign.Value()
gap := image.Point(this.attrGap.Value())
return tomo.LayoutHints {
OverflowX: this.hOverflow,
OverflowY: this.vOverflow,
AlignX: this.hAlign,
AlignY: this.vAlign,
Gap: this.gap,
OverflowX: overflow.X,
OverflowY: overflow.Y,
AlignX: align.X,
AlignY: align.Y,
Gap: gap,
}
}
func (this *containerBox) contentMinimum () image.Point {
minimum := this.box.contentMinimum()
overflow := this.attrOverflow.Value()
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 }
this.boxQuerier())
if overflow.X { layoutMinimum.X = 0 }
if overflow.Y { layoutMinimum.Y = 0 }
minimum = minimum.Add(layoutMinimum)
}
return minimum
@ -285,18 +295,19 @@ func (this *containerBox) doLayout () {
if this.layout != nil {
minimum = this.layout.MinimumSize (
this.layoutHints(),
this.children)
this.boxQuerier())
}
innerBounds := this.InnerBounds()
overflow := this.attrOverflow.Value()
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 }
if overflow.X { this.contentBounds.Max.X = this.contentBounds.Min.X + minimum.X }
if overflow.Y { 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)
this.layout.Arrange(layoutHints, this.boxArranger())
}
// build an accurate contentBounds by unioning the bounds of all child
@ -312,7 +323,7 @@ func (this *containerBox) doLayout () {
// offset children and contentBounds by scroll
for _, box := range this.children {
box.SetBounds(box.Bounds().Add(this.scroll).Add(innerBounds.Min))
assertAnyBox(box).setBounds(box.Bounds().Add(this.scroll).Add(innerBounds.Min))
}
this.contentBounds = this.contentBounds.Add(this.scroll)
@ -345,6 +356,14 @@ func (this *containerBox) constrainScroll () {
}
}
func (this *containerBox) boxQuerier () boxQuerier {
return boxQuerier(this.children)
}
func (this *containerBox) boxArranger () boxArranger {
return boxArranger(this.children)
}
func (this *containerBox) recursiveRedo () {
this.doLayout()
this.doDraw()
@ -367,12 +386,12 @@ func (this *containerBox) recursiveReApply () {
}
}
func (this *containerBox) boxUnder (point image.Point, category eventCategory) anyBox {
func (this *containerBox) boxUnder (point image.Point) anyBox {
if !point.In(this.bounds) { return nil }
if !this.capture[category] {
if !this.mask {
for _, box := range this.children {
candidate := box.(anyBox).boxUnder(point, category)
candidate := box.(anyBox).boxUnder(point)
if candidate != nil { return candidate }
}
}
@ -400,6 +419,6 @@ func (this *containerBox) propagateAlt (callback func (anyBox) bool) bool {
return true
}
func (this *containerBox) captures (category eventCategory) bool {
return this.capture[category]
func (this *containerBox) masks () bool {
return this.mask
}

View File

@ -3,6 +3,10 @@ package system
import "image"
import "git.tebibyte.media/tomo/tomo/input"
// TODO: redo all of this because there are new event propogation rules
// TODO: once go v1.23 comes out, replace the explicit iterator calls here with
// range loops
// HandleFocusChange sets whether or not the window containing this Hierarchy
// has input focus.
func (this *Hierarchy) HandleFocusChange (focused bool) {
@ -20,14 +24,22 @@ func (this *Hierarchy) HandleModifiers (modifiers input.Modifiers) {
// event which triggers this comes with modifier key information,
// HandleModifiers must be called *before* HandleKeyDown.
func (this *Hierarchy) HandleKeyDown (key input.Key, numberPad bool) {
caught := false
this.keyboardTargets(func (target anyBox) bool {
if target.handleKeyDown(key, numberPad) {
caught = true
return false
}
return true
})
if caught { return }
if key == input.KeyTab && this.modifiers.Alt {
if this.modifiers.Shift {
this.focusPrevious()
} else {
this.focusNext()
}
} else if target := this.keyboardTarget(); target != nil {
target.handleKeyDown(key, numberPad)
}
}
@ -35,63 +47,72 @@ func (this *Hierarchy) HandleKeyDown (key input.Key, numberPad bool) {
// which triggers this comes with modifier key information, HandleModifiers must
// be called *before* HandleKeyUp.
func (this *Hierarchy) HandleKeyUp (key input.Key, numberPad bool) {
if target := this.keyboardTarget(); target != nil {
target.handleKeyUp(key, numberPad)
}
this.keyboardTargets(func (target anyBox) bool {
if target.handleKeyUp(key, numberPad) {
return false
}
return true
})
}
// 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.
// HandleMouseDown sends a mouse down event to the Boxes positioned underneath
// the mouse cursor and marks them as being "dragged" by that mouse button,
// starting at the first Box to mask events and ending at the first box to
// catch the event. 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)
}
boxes := []anyBox { }
this.boxesUnder(this.mousePosition)(func (box anyBox) bool {
boxes = append(boxes, box)
return !box.handleMouseDown(button)
})
this.drags[button] = boxes
}
// 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
// HandleMouseUp sends a mouse up event to the Boxes currently being "dragged"
// by the specified mouse button, and marks them 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)
for _, box := range this.drags[button] {
box.handleMouseUp(button)
}
this.drags[button] = nil
}
// 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.
// "dragged" by a mouse button. If none are, it sends the event to the Boxes
// which are underneath the mouse pointer, starting at the first Box to mask
// events and ending at the first box to catch the event.
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()
dragged := false
for _, dragSet := range this.drags {
for _, box := range dragSet {
if box.handleMouseMove() { break }
}
dragged = true
}
if dragged { return }
// TODO we can hover over multiple boxes at once. however, any way of
// detecting this involves several slice allocations every time we
// process a MouseMove event. perhaps we just ought to suck it up and do
// it.
box := this.boxUnder(position)
if box != nil {
this.hover(box)
box.handleMouseMove()
}
}
// HandleScroll sends a scroll event to the Box currently underneath the mouse
// cursor.
// cursor. If the event which triggers this comes with mouse position
// information, HandleMouseMove must be called *before* HandleScroll.
func (this *Hierarchy) HandleScroll (x, y float64) {
underneath := this.boxUnder(this.mousePosition, eventCategoryScroll)
if underneath != nil {
underneath.handleScroll(x, y)
}
this.boxesUnder(this.mousePosition)(func (box anyBox) bool {
return !box.handleScroll(x, y)
})
}

View File

@ -21,10 +21,10 @@ type Hierarchy struct {
modifiers input.Modifiers
mousePosition image.Point
drags [10]anyBox
drags [10][]anyBox
minimumSize image.Point
needMinimum util.Set[anyBox]
needStyle util.Set[anyBox]
needLayout util.Set[anyBox]
needDraw util.Set[anyBox]
needRedo bool
@ -52,7 +52,7 @@ func (this *System) NewHierarchy (link WindowLink) *Hierarchy {
hierarchy := &Hierarchy {
system: this,
link: link,
needMinimum: make(util.Set[anyBox]),
needStyle: make(util.Set[anyBox]),
needLayout: make(util.Set[anyBox]),
needDraw: make(util.Set[anyBox]),
}
@ -95,6 +95,16 @@ func (this *Hierarchy) MinimumSize () image.Point {
return this.minimumSize
}
// Modifiers returns the current modifier keys being held.
func (this *Hierarchy) Modifiers () input.Modifiers {
return this.modifiers
}
// MousePosition returns the current mouse position.
func (this *Hierarchy) MousePosition () image.Point {
return this.mousePosition
}
// AfterEvent should be called at the end of every event cycle.
func (this *Hierarchy) AfterEvent () {
if this.canvas == nil { return }
@ -104,7 +114,7 @@ func (this *Hierarchy) AfterEvent () {
childBounds := this.canvas.Bounds()
childBounds = childBounds.Sub(childBounds.Min)
if this.root != nil {
this.root.SetBounds(childBounds)
this.root.setBounds(childBounds)
}
// full relayout/redraw
@ -116,8 +126,8 @@ func (this *Hierarchy) AfterEvent () {
return
}
for len(this.needMinimum) > 0 {
this.needMinimum.Pop().doMinimumSize()
for len(this.needStyle) > 0 {
this.needStyle.Pop().doStyle()
}
if !this.minimumClean {
this.doMinimumSize()
@ -146,7 +156,7 @@ func (this *Hierarchy) setStyle () {
if this.root != nil { this.root.recursiveReApply() }
}
func (this *Hierarchy) setIcons () {
func (this *Hierarchy) setIconSet () {
if this.root != nil { this.root.recursiveReApply() }
}
@ -158,7 +168,7 @@ func (this *Hierarchy) getWindow () tomo.Window {
return this.link.GetWindow()
}
func (this *Hierarchy) getStyle () tomo.Style {
func (this *Hierarchy) getStyle () *tomo.Style {
return this.system.style
}
@ -186,8 +196,8 @@ func (this *Hierarchy) notifyMinimumSizeChange (anyBox) {
this.minimumClean = false
}
func (this *Hierarchy) invalidateMinimum (box anyBox) {
this.needMinimum.Add(box)
func (this *Hierarchy) invalidateStyle (box anyBox) {
this.needStyle.Add(box)
}
func (this *Hierarchy) invalidateDraw (box anyBox) {
@ -235,35 +245,52 @@ 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 {
func (this *Hierarchy) masks () bool {
return false
}
func (this *Hierarchy) keyboardTarget () anyBox {
func (this *Hierarchy) boxUnder (point image.Point) anyBox {
if this.root == nil { return nil }
return this.root.boxUnder(point)
}
func (this *Hierarchy) parents (box anyBox) func (func (anyBox) bool) {
return func (yield func (anyBox) bool) {
for box != nil && yield(box) {
parent, ok := box.getParent().(anyBox)
if !ok { break }
box = parent
}
}
}
func (this *Hierarchy) boxesUnder (point image.Point) func (func (anyBox) bool) {
return this.parents(this.boxUnder(point))
}
func (this *Hierarchy) keyboardTargets (yield func (anyBox) bool) {
focused := this.focused
if focused == nil { return nil }
parent := focused.getParent()
if focused == nil { return }
this.parents(this.considerMaskingParents(focused))(yield)
}
func (this *Hierarchy) considerMaskingParents (box anyBox) anyBox {
parent := box.getParent()
for {
parentBox, ok := parent.(anyBox)
if !ok { break }
if parent.captures(eventCategoryKeyboard) {
if parent.masks() {
return parentBox
}
parent = parentBox.getParent()
}
return focused
return box
}
func (this *Hierarchy) focusNext () {
found := !this.anyFocused()
focused := false
this.propagateAlt (func (box anyBox) bool {
this.propagateAlt(func (box anyBox) bool {
if found {
// looking for the next box to select
if box.canBeFocused() {
@ -287,7 +314,7 @@ func (this *Hierarchy) focusNext () {
func (this *Hierarchy) focusPrevious () {
var behind anyBox
this.propagate (func (box anyBox) bool {
this.propagate(func (box anyBox) bool {
if box == this.focused {
return false
}
@ -319,8 +346,14 @@ func (this *Hierarchy) doMinimumSize () {
this.minimumSize = image.Point { }
if this.root != nil {
this.minimumSize = this.root.MinimumSize()
this.minimumSize = this.root.minimumSize()
}
this.link.NotifyMinimumSizeChange()
}
func (this *Hierarchy) newStyleApplicator () *styleApplicator {
return &styleApplicator {
style: this.getStyle(),
}
}

View File

@ -5,14 +5,6 @@ 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 {
@ -27,9 +19,8 @@ type parent interface {
// 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
// catches returns whether or not this parent masks events.
masks () bool
}
// anyBox is any tomo.Box type that is implemented by this package.
@ -44,10 +35,10 @@ type anyBox interface {
// 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 ()
// doStyle re-applies the box's style non-recursively
doDraw ()
doLayout ()
doStyle ()
// flushActionQueue performs any queued actions, like invalidating the
// minimum size or grabbing input focus.
@ -62,7 +53,9 @@ type anyBox interface {
// to check whether they have an outdated style or icon set, and if so,
// update it and trigger the appropriate event broadcasters.
recursiveReApply ()
// minimumSize returns the box's minimum size
minimumSize () image.Point
// contentMinimum returns the minimum dimensions of this box's content
contentMinimum () image.Point
// canBeFocused returns whether or not this anyBox is capable of holding
@ -71,12 +64,18 @@ type anyBox interface {
// 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
boxUnder (image.Point) 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
// setBounds sets the box's bounds.
setBounds (image.Rectangle)
// setAttr sets an attribute at the user or style level depending
// on the value of user.
setAttr (attr tomo.Attr, user 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
@ -92,12 +91,21 @@ type anyBox interface {
// handleDndDrop (data.Data)
handleMouseEnter ()
handleMouseLeave ()
handleMouseMove ()
handleMouseDown (input.Button)
handleMouseUp (input.Button)
handleScroll (float64, float64)
handleKeyDown (input.Key, bool)
handleKeyUp (input.Key, bool)
handleMouseMove () bool
handleMouseDown (input.Button) bool
handleMouseUp (input.Button) bool
handleScroll (float64, float64) bool
handleKeyDown (input.Key, bool) bool
handleKeyUp (input.Key, bool) bool
}
type anyContentBox interface {
anyBox
// recommendedWidth returns the recommended width for a given height.
recommendedWidth (int) int
// recommendedHeight returns the recommended height for a given height.
recommendedHeight (int) int
}
func assertAnyBox (unknown tomo.Box) anyBox {

46
internal/system/style.go Normal file
View File

@ -0,0 +1,46 @@
package system
import "git.tebibyte.media/tomo/tomo"
type styleApplicator struct {
style *tomo.Style
role tomo.Role
rules []*tomo.Rule
}
func (this *styleApplicator) apply (box anyBox) {
if box.Role() != this.role {
// the role has changed, so re-cache the list of rules
this.rules = make([]*tomo.Rule, 4)
for _, rule := range this.style.Rules {
role := box.Role()
// blank fields match anything
if rule.Role.Package == "" { role.Package = "" }
if rule.Role.Object == "" { role.Object = "" }
if rule.Role == role {
this.rules = append(this.rules, &rule)
}
}
}
// compile list of attributes by searching through the cached ruleset
attrs := make(tomo.AttrSet)
for _, rule := range this.rules {
satisifed := true
for _, tag := range rule.Tags {
if !box.Tag(tag) {
satisifed = false
break
}
}
if satisifed {
attrs.MergeOver(rule.Set)
}
}
// apply that list of attributes
for _, attr := range attrs {
box.setAttr(attr, false)
}
}

View File

@ -11,7 +11,7 @@ import "git.tebibyte.media/tomo/backend/internal/util"
type System struct {
link BackendLink
style tomo.Style
style *tomo.Style
styleNonce int
iconsNonce int
@ -45,7 +45,7 @@ func New (link BackendLink) *System {
// SetStyle sets the tomo.Style that is applied to objects, and notifies them
// that the style has changed.
func (this *System) SetStyle (style tomo.Style) {
func (this *System) SetStyle (style *tomo.Style) {
this.style = style
this.styleNonce ++
for hierarchy := range this.hierarchies {
@ -53,11 +53,11 @@ func (this *System) SetStyle (style tomo.Style) {
}
}
// SetIcons notifies objects that the icons have changed.
func (this *System) SetIcons (icons tomo.Icons) {
// SetIconSet notifies objects that the icons have changed.
func (this *System) SetIconSet (iconSet tomo.IconSet) {
this.iconsNonce ++
for hierarchy := range this.hierarchies {
hierarchy.setIcons()
hierarchy.setIconSet()
}
}

View File

@ -2,7 +2,6 @@ package system
import "image"
import "image/color"
import "golang.org/x/image/font"
import "git.tebibyte.media/tomo/tomo"
import "golang.org/x/image/math/fixed"
import "git.tebibyte.media/tomo/typeset"
@ -14,22 +13,21 @@ 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
attrTextColor attrHierarchy[tomo.AttrTextColor]
attrDotColor attrHierarchy[tomo.AttrDotColor]
attrFace attrHierarchy[tomo.AttrFace]
attrWrap attrHierarchy[tomo.AttrWrap]
attrAlign attrHierarchy[tomo.AttrAlign]
attrOverflow attrHierarchy[tomo.AttrOverflow]
text string
selectable bool
selecting bool
selectStart int
dot text.Dot
dotColor color.Color
drawer typeset.Drawer
@ -40,20 +38,12 @@ type textBox struct {
}
func (this *System) NewTextBox () tomo.TextBox {
box := &textBox {
textColor: color.Black,
dotColor: color.RGBA { B: 255, G: 255, A: 255 },
}
box := &textBox { }
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()
}
// ----- public methods ----------------------------------------------------- //
func (this *textBox) ContentBounds () image.Rectangle {
return this.contentBounds
@ -71,7 +61,7 @@ func (this *textBox) RecommendedHeight (width int) int {
func (this *textBox) RecommendedWidth (height int) int {
// TODO maybe not the best idea?
return this.MinimumSize().X
return this.minimumSize().X
}
func (this *textBox) OnContentBoundsChange (callback func()) event.Cookie {
@ -86,40 +76,11 @@ func (this *textBox) SetText (text string) {
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 }
@ -138,20 +99,19 @@ 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()
}
// ----- private methods ---------------------------------------------------- //
func (this *textBox) Draw (can canvas.Canvas) {
if can == nil { return }
texture := this.attrTexture.Value().Texture
col := this.attrColor.Value().Color
if col == nil { col = color.Transparent }
this.drawBorders(can)
pen := can.Pen()
pen.Fill(this.color)
pen.Texture(this.texture)
pen.Fill(col)
pen.Texture(texture)
if this.transparent() && this.parent != nil {
this.parent.drawBackgroundPart(can)
@ -162,8 +122,50 @@ func (this *textBox) Draw (can canvas.Canvas) {
this.drawDot(can)
}
if this.face == nil { return }
this.drawer.Draw(can, this.textColor, this.textOffset())
if this.attrFace.Value().Face != nil {
textColor := this.attrTextColor.Value().Color
if textColor == nil { textColor = color.Black }
this.drawer.Draw(can, textColor, this.textOffset())
}
}
func (this *textBox) setAttr (attr tomo.Attr, user bool) {
switch attr := attr.(type) {
case tomo.AttrTextColor:
if this.attrTextColor.Set(attr, user) && !this.dot.Empty() {
this.invalidateDraw()
}
case tomo.AttrDotColor:
if this.attrDotColor.Set(attr, user) && !this.dot.Empty() {
this.invalidateDraw()
}
case tomo.AttrFace:
if this.attrFace.Set(attr, user) {
this.drawer.SetFace(attr.Face)
this.invalidateMinimum()
this.invalidateLayout()
}
case tomo.AttrWrap:
if this.attrWrap.Set(attr, user) {
this.invalidateMinimum()
this.invalidateLayout()
}
case tomo.AttrAlign:
if this.attrAlign.Set(attr, user) {
this.invalidateDraw()
}
case tomo.AttrOverflow:
if this.attrOverflow.Set(attr, user) {
this.invalidateLayout()
}
default: this.box.setAttr(attr, user)
}
}
func roundPt (point fixed.Point26_6) image.Point {
@ -175,14 +177,20 @@ func fixPt (point image.Point) fixed.Point26_6 {
}
func (this *textBox) drawDot (can canvas.Canvas) {
if this.face == nil { return }
if this.attrFace.Value().Face == nil { return }
face := this.attrFace.Value().Face
textColor := this.attrTextColor.Value().Color
dotColor := this.attrDotColor.Value().Color
if textColor == nil { textColor = color.Transparent }
if dotColor == nil { dotColor = color.RGBA { G: 255, B: 255, A: 255 } }
pen := can.Pen()
pen.Fill(color.Transparent)
pen.Stroke(this.textColor)
pen.Stroke(textColor)
bounds := this.InnerBounds()
metrics := this.face.Metrics()
metrics := 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()))
@ -196,7 +204,7 @@ func (this *textBox) drawDot (can canvas.Canvas) {
pen.Path(roundPt(start.Add(ascent)), roundPt(start.Sub(descent)))
case start.Y == end.Y:
pen.Fill(this.dotColor)
pen.Fill(dotColor)
pen.StrokeWeight(0)
pen.Rectangle(image.Rectangle {
Min: roundPt(start.Add(ascent)),
@ -204,7 +212,7 @@ func (this *textBox) drawDot (can canvas.Canvas) {
})
default:
pen.Fill(this.dotColor)
pen.Fill(dotColor)
pen.StrokeWeight(0)
rect := image.Rectangle {
@ -241,35 +249,37 @@ func (this *textBox) handleFocusLeave () {
this.box.handleFocusLeave()
}
func (this *textBox) handleMouseDown (button input.Button) {
func (this *textBox) handleMouseDown (button input.Button) bool {
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)
return this.box.handleMouseDown(button)
}
func (this *textBox) handleMouseUp (button input.Button) {
func (this *textBox) handleMouseUp (button input.Button) bool {
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)
return this.box.handleMouseUp(button)
}
func (this *textBox) handleMouseMove () {
func (this *textBox) handleMouseMove () bool {
if this.selecting {
index := this.runeUnderMouse()
this.Select(text.Dot { Start: this.selectStart, End: index })
}
this.box.handleMouseMove()
return this.box.handleMouseMove()
}
func (this *textBox) runeUnderMouse () int {
position := this.MousePosition().Sub(this.textOffset())
window := this.Window()
if window == nil { return 0 }
position := window.MousePosition().Sub(this.textOffset())
return this.drawer.AtPosition(fixPt(position))
}
@ -281,10 +291,10 @@ func (this *textBox) normalizedLayoutBoundsSpace () image.Rectangle {
func (this *textBox) contentMinimum () image.Point {
minimum := this.drawer.MinimumSize()
if this.hOverflow || this.wrap {
if this.attrOverflow.Value().X || bool(this.attrWrap.Value()) {
minimum.X = this.drawer.Em().Round()
}
if this.vOverflow {
if this.attrOverflow.Value().Y {
minimum.Y = this.drawer.LineHeight().Round()
}

View File

@ -1,5 +1,6 @@
package util
import "io"
import "image/color"
// IndexOf returns the index of needle within haystack. If needle does not exist
@ -99,3 +100,32 @@ func (this *Memo[T]) InvalidateTo (value T) {
this.Invalidate()
this.cache = value
}
// Cycler stores a value and an accompanying io.Closer. When the value is set,
// the closer associated with the previous value is closed.
type Cycler[T any] struct {
value T
closer io.Closer
}
// Value returns the cycler's value.
func (this *Cycler[T]) Value () T {
return this.value
}
// Set sets the value and associated closer, closing the previous one.
func (this *Cycler[T]) Set (value T, closer io.Closer) (err error) {
if this.closer != nil {
err = this.closer.Close()
}
this.value = value
this.closer = closer
return err
}
// Close closes the associated closer early.
func (this *Cycler[T]) Close () error {
err := this.closer.Close()
this.closer = nil
return err
}