Compare commits

..

No commits in common. "a62dff42361fd0d57798d7413129fe04cf1bb7cc" and "5b62c9e16233b81c1e62a35792d4f654929412ff" have entirely different histories.

17 changed files with 560 additions and 830 deletions

3
go.mod
View File

@ -3,9 +3,10 @@ module git.tebibyte.media/tomo/backend
go 1.20
require (
git.tebibyte.media/tomo/tomo v0.41.0
git.tebibyte.media/tomo/tomo v0.40.0
git.tebibyte.media/tomo/typeset v0.7.1
git.tebibyte.media/tomo/xgbkb v1.0.1
git.tebibyte.media/tomo/xgbsel/v2 v2.0.0-alpha.1
github.com/jezek/xgb v1.1.1
github.com/jezek/xgbutil v0.0.0-20231116234834-47f30c120111
golang.org/x/image v0.11.0

6
go.sum
View File

@ -1,10 +1,12 @@
git.tebibyte.media/sashakoshka/xgbkb v1.0.0/go.mod h1:pNcE6TRO93vHd6q42SdwLSTTj25L0Yzggz7yLe0JV6Q=
git.tebibyte.media/tomo/tomo v0.41.0 h1:Z+7FHhbGiKjs+kQNvuJOfz47xIct5qxvSJqyDuoNIOs=
git.tebibyte.media/tomo/tomo v0.41.0/go.mod h1:C9EzepS9wjkTJjnZaPBh22YvVPyA4hbBAJVU20Rdmps=
git.tebibyte.media/tomo/tomo v0.40.0 h1:X1ffSGE+dXI1VkJ97vM+RMfSfchCvSbP0vRbLJ9qoDE=
git.tebibyte.media/tomo/tomo v0.40.0/go.mod h1:C9EzepS9wjkTJjnZaPBh22YvVPyA4hbBAJVU20Rdmps=
git.tebibyte.media/tomo/typeset v0.7.1 h1:aZrsHwCG5ZB4f5CruRFsxLv5ezJUCFUFsQJJso2sXQ8=
git.tebibyte.media/tomo/typeset v0.7.1/go.mod h1:PwDpSdBF3l/EzoIsa2ME7QffVVajnTHZN6l3MHEGe1g=
git.tebibyte.media/tomo/xgbkb v1.0.1 h1:b3HDUopjdQp1MZrb5Vpil4bOtk3NnNXtfQW27Blw2kE=
git.tebibyte.media/tomo/xgbkb v1.0.1/go.mod h1:P5Du0yo5hUsojchW08t+Mds0XPIJXwMi733ZfklzjRw=
git.tebibyte.media/tomo/xgbsel/v2 v2.0.0-alpha.1 h1:B9Z2UZzfHlZZUbKiz3WFOSMUgVO5FvH0bYQ1m0cC0dk=
git.tebibyte.media/tomo/xgbsel/v2 v2.0.0-alpha.1/go.mod h1:NIyIbAPQQbevOKDmkKRgIUizEcPFMYCR4unj/mvvXcA=
github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298 h1:1qlsVAQJXZHsaM8b6OLVo6muQUQd4CwkH/D3fnnbHXA=
github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298/go.mod h1:D+QujdIlUNfa0igpNMk6UIvlb6C252URs4yupRUV4lQ=
github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966 h1:lTG4HQym5oPKjL7nGs+csTgiDna685ZXjxijkne828g=

View File

@ -1,38 +0,0 @@
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,38 +9,39 @@ 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
tags util.Set[string]
role tomo.Role
styleCookie event.Cookie
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
attrColor attrHierarchy[tomo.AttrColor]
attrTexture attrHierarchy[tomo.AttrTexture]
attrTextureMode attrHierarchy[tomo.AttrTextureMode]
attrBorder attrHierarchy[tomo.AttrBorder]
attrMinimumSize attrHierarchy[tomo.AttrMinimumSize]
attrPadding attrHierarchy[tomo.AttrPadding]
padding tomo.Inset
border []tomo.Border
color color.Color
texture canvas.Texture
textureMode textureMode
dndData data.Data
dndAccept []data.Mime
focusable bool
hovered bool
focused bool
pressed bool
focusable bool
canvas util.Memo[canvas.Canvas]
drawer canvas.Drawer
@ -53,20 +54,21 @@ type box struct {
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]
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
iconSetChange event.FuncBroadcaster
iconsChange event.FuncBroadcaster
}
}
func (this *System) newBox (outer anyBox) *box {
box := &box {
system: this,
color: color.Transparent,
outer: outer,
drawer: outer,
}
@ -80,16 +82,15 @@ func (this *System) newBox (outer anyBox) *box {
box.drawer = box
box.outer = box
}
box.minSize = util.NewMemo(box.calculateMinimumSize)
box.invalidateMinimum()
return box
}
func (this *System) NewBox () tomo.Box {
return this.newBox(nil)
}
// ----- public methods ----------------------------------------------------- //
func (this *box) GetBox () tomo.Box {
return this.outer
}
@ -105,13 +106,102 @@ func (this *box) Bounds () image.Rectangle {
}
func (this *box) InnerBounds () image.Rectangle {
return tomo.Inset(this.attrPadding.Value()).Apply(this.innerClippingBounds)
return this.padding.Apply(this.innerClippingBounds)
}
func (this *box) MinimumSize () image.Point {
return this.minSize
}
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
@ -119,27 +209,6 @@ 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
}
@ -164,12 +233,6 @@ 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
@ -178,83 +241,22 @@ func (this *box) SetFocusable (focusable bool) {
}
}
// ----- 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) Focused () bool {
hierarchy := this.getHierarchy()
if hierarchy == nil { return false }
return hierarchy.isFocused(this.outer)
}
func (this *box) setBounds (bounds image.Rectangle) {
if this.bounds == bounds { return }
this.bounds = bounds
this.invalidateLayout()
func (this *box) Modifiers () input.Modifiers {
hierarchy := this.getHierarchy()
if hierarchy == nil { return input.Modifiers { } }
return hierarchy.getModifiers()
}
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
func (this *box) MousePosition () image.Point {
hierarchy := this.getHierarchy()
if hierarchy == nil { return image.Point { } }
return hierarchy.getMousePosition()
}
// ----- event handler setters ---------------------------------------------- //
@ -279,62 +281,46 @@ 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() bool) event.Cookie {
func (this *box) OnMouseMove (callback func()) event.Cookie {
return this.on.mouseMove.Connect(callback)
}
func (this *box) OnButtonDown (callback func(input.Button) bool) event.Cookie {
return this.on.buttonDown.Connect(callback)
func (this *box) OnMouseDown (callback func(input.Button)) event.Cookie {
return this.on.mouseDown.Connect(callback)
}
func (this *box) OnButtonUp (callback func(input.Button) bool) event.Cookie {
return this.on.buttonUp.Connect(callback)
func (this *box) OnMouseUp (callback func(input.Button)) event.Cookie {
return this.on.mouseUp.Connect(callback)
}
func (this *box) OnScroll (callback func(float64, float64) bool) event.Cookie {
func (this *box) OnScroll (callback func(deltaX, deltaY float64)) event.Cookie {
return this.on.scroll.Connect(callback)
}
func (this *box) OnKeyDown (callback func(input.Key, bool) bool) event.Cookie {
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(input.Key, bool) bool) event.Cookie {
func (this *box) OnKeyUp (callback func(key input.Key, numberPad 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) OnIconSetChange (callback func()) event.Cookie {
return this.on.iconSetChange.Connect(callback)
func (this *box) OnIconsChange (callback func()) event.Cookie {
return this.on.iconsChange.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 () (caught bool) {
for _, listener := range this.on.mouseMove.Listeners() {
if listener() { caught = true }
}
return
func (this *box) handleMouseMove () {
this.on.mouseMove.Broadcast()
}
func (this *box) handleMouseDown (button input.Button) (caught bool) {
if button == input.ButtonLeft {
this.pressed = true
this.invalidateStyle()
}
func (this *box) handleMouseDown (button input.Button) {
if this.focusable {
this.SetFocused(true)
} else {
@ -342,39 +328,29 @@ func (this *box) handleMouseDown (button input.Button) (caught bool) {
if hierarchy == nil { return }
hierarchy.focus(nil)
}
for _, listener := range this.on.buttonDown.Listeners() {
if listener(button) { caught = true }
for _, listener := range this.on.mouseDown.Listeners() {
listener(button)
}
return
}
func (this *box) handleMouseUp (button input.Button) (caught bool) {
if button == input.ButtonLeft {
this.pressed = false
this.invalidateStyle()
func (this *box) handleMouseUp (button input.Button) {
for _, listener := range this.on.mouseUp.Listeners() {
listener(button)
}
for _, listener := range this.on.buttonUp.Listeners() {
if listener(button) { caught = true }
}
return
}
func (this *box) handleScroll (x, y float64) (caught bool) {
func (this *box) handleScroll (x, y float64) {
for _, listener := range this.on.scroll.Listeners() {
if listener(x, y) { caught = true }
listener(x, y)
}
return
}
func (this *box) handleKeyDown (key input.Key, numberPad bool) (caught bool) {
func (this *box) handleKeyDown (key input.Key, numberPad bool) {
for _, listener := range this.on.keyDown.Listeners() {
if listener(key, numberPad) { caught = true }
listener(key, numberPad)
}
return
}
func (this *box) handleKeyUp (key input.Key, numberPad bool) (caught bool) {
func (this *box) handleKeyUp (key input.Key, numberPad bool) {
for _, listener := range this.on.keyUp.Listeners() {
if listener(key, numberPad) { caught = true }
listener(key, numberPad)
}
return
}
// -------------------------------------------------------------------------- //
@ -383,25 +359,19 @@ 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 textureMode == tomo.TextureModeCenter && texture != nil {
textureBounds := texture.Bounds()
if this.textureMode == textureModeCenter && this.texture != nil {
textureBounds := this.texture.Bounds()
textureOrigin :=
bounds.Min.
Add(image.Pt (
@ -412,7 +382,7 @@ func (this *box) Draw (can canvas.Canvas) {
textureBounds.Dy() / 2))
pen.Fill(color.Transparent)
pen.Texture(texture)
pen.Texture(this.texture)
pen.Rectangle(textureBounds.Sub(textureBounds.Min).Add(textureOrigin))
}
}
@ -424,7 +394,6 @@ 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))
}
@ -432,7 +401,7 @@ func (this *box) drawBorders (can canvas.Canvas) {
pen.Rectangle(area)
}
for _, border := range this.attrBorder.Value() {
for _, border := range this.border {
rectangle (
bounds.Min.X,
bounds.Min.Y,
@ -464,29 +433,26 @@ func (this *box) drawBorders (can canvas.Canvas) {
func (this *box) contentMinimum () image.Point {
var minimum image.Point
padding := tomo.Inset(this.attrPadding.Value())
minimum.X += padding.Horizontal()
minimum.Y += padding.Vertical()
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) calculateMinimumSize () image.Point {
userMinSize := this.attrMinimumSize.Value()
minSize := this.outer.contentMinimum()
if minSize.X < userMinSize.X {
minSize.X = userMinSize.X
func (this *box) doMinimumSize () {
this.minSize = this.outer.contentMinimum()
if this.minSize.X < this.userMinSize.X {
this.minSize.X = this.userMinSize.X
}
if minSize.Y < userMinSize.Y {
minSize.Y = userMinSize.Y
if this.minSize.Y < this.userMinSize.Y {
this.minSize.Y = this.userMinSize.Y
}
if this.parent != nil {
this.parent.notifyMinimumSizeChange(this)
}
return minSize
}
// var drawcnt int
@ -511,10 +477,6 @@ 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)
@ -529,6 +491,10 @@ 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)
}
@ -543,12 +509,6 @@ 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 }
@ -562,7 +522,12 @@ func (this *box) invalidateDraw () {
}
func (this *box) invalidateMinimum () {
this.minSize.Invalidate()
hierarchy := this.getHierarchy()
if hierarchy == nil {
this.minSizeQueued = true
} else {
hierarchy.invalidateMinimum(this.outer)
}
}
func (this *box) recursiveReApply () {
@ -573,13 +538,17 @@ func (this *box) recursiveReApply () {
// style
hierarchyStyleNonce := this.getStyleNonce()
if this.lastStyleNonce != hierarchyStyleNonce {
// 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).
// remove old style
this.lastStyleNonce = hierarchyStyleNonce
this.styleApplicator = this.getHierarchy().newStyleApplicator()
this.invalidateStyle()
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.on.styleChange.Broadcast()
}
@ -587,7 +556,7 @@ func (this *box) recursiveReApply () {
hierarchyIconsNonce := this.getIconsNonce()
if this.lastIconsNonce != hierarchyIconsNonce {
this.lastIconsNonce = hierarchyIconsNonce
this.on.iconSetChange.Broadcast()
this.on.iconsChange.Broadcast()
}
}
@ -595,7 +564,7 @@ func (this *box) canBeFocused () bool {
return this.focusable
}
func (this *box) boxUnder (point image.Point) anyBox {
func (this *box) boxUnder (point image.Point, category eventCategory) anyBox {
if point.In(this.bounds) {
return this.outer
} else {
@ -614,7 +583,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.attrColor.Value()) /*&&
return util.Transparent(this.color) /*&&
(this.texture == nil || !this.texture.Opaque())*/
}
@ -624,6 +593,12 @@ 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

@ -1,63 +0,0 @@
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,6 +15,10 @@ 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()
@ -28,8 +32,7 @@ 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(padding.Apply(this.innerClippingBounds)))
can.SubCanvas(this.padding.Apply(this.innerClippingBounds)))
}
}

View File

@ -10,16 +10,14 @@ 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
mask bool
capture [4]bool
attrGap attrHierarchy[tomo.AttrGap]
attrAlign attrHierarchy[tomo.AttrAlign]
attrOverflow attrHierarchy[tomo.AttrOverflow]
attrLayout attrHierarchy[tomo.AttrLayout]
children []anyBox
gap image.Point
children []tomo.Box
layout tomo.Layout
on struct {
@ -33,7 +31,37 @@ func (this *System) NewContainerBox () tomo.ContainerBox {
return box
}
// ----- public methods ----------------------------------------------------- //
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
@ -45,13 +73,54 @@ 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, box) > -1 { return }
if util.IndexOf(this.children, tomo.Box(box)) > -1 { return }
box.setParent(this)
box.flushActionQueue()
@ -62,7 +131,7 @@ func (this *containerBox) Add (child tomo.Object) {
func (this *containerBox) Remove (child tomo.Object) {
box := assertAnyBox(child.GetBox())
index := util.IndexOf(this.children, box)
index := util.IndexOf(this.children, tomo.Box(box))
if index < 0 { return }
box.setParent(nil)
@ -73,15 +142,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, box) > -1 { return }
if util.IndexOf(this.children, tomo.Box(box)) > -1 { return }
beforeBox := assertAnyBox(before.GetBox())
index := util.IndexOf(this.children, beforeBox)
index := util.IndexOf(this.children, tomo.Box(beforeBox))
if index < 0 {
this.children = append(this.children, box)
this.children = append(this.children, tomo.Box(box))
} else {
this.children = util.Insert(this.children, index, box)
this.children = util.Insert(this.children, index, tomo.Box(box))
}
box.setParent(this)
@ -98,7 +167,7 @@ func (this *containerBox) Clear () {
this.invalidateMinimum()
}
func (this *containerBox) Len () int {
func (this *containerBox) Length () int {
return len(this.children)
}
@ -109,20 +178,15 @@ func (this *containerBox) At (index int) tomo.Object {
return this.children[index]
}
func (this *containerBox) SetInputMask (mask bool) {
this.mask = mask
func (this *containerBox) SetLayout (layout tomo.Layout) {
this.layout = layout
this.invalidateLayout()
this.invalidateMinimum()
}
// ----- 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 {
rocks[index] = box.Bounds()
@ -134,87 +198,17 @@ func (this *containerBox) Draw (can canvas.Canvas) {
}
if clipped == nil { continue }
pen := clipped.Pen()
pen.Fill(col)
pen.Texture(texture)
pen.Fill(this.color)
pen.Texture(this.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()
// 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)
pen.Fill(this.color)
pen.Texture(this.texture)
if this.transparent() && this.parent != nil {
this.parent.drawBackgroundPart(can)
@ -251,7 +245,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()
@ -259,27 +253,23 @@ 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: overflow.X,
OverflowY: overflow.Y,
AlignX: align.X,
AlignY: align.Y,
Gap: gap,
OverflowX: this.hOverflow,
OverflowY: this.vOverflow,
AlignX: this.hAlign,
AlignY: this.vAlign,
Gap: this.gap,
}
}
func (this *containerBox) contentMinimum () image.Point {
overflow := this.attrOverflow.Value()
minimum := this.box.contentMinimum()
if this.layout != nil {
layoutMinimum := this.layout.MinimumSize (
this.layoutHints(),
this.boxQuerier())
if overflow.X { layoutMinimum.X = 0 }
if overflow.Y { layoutMinimum.Y = 0 }
this.children)
if this.hOverflow { layoutMinimum.X = 0 }
if this.vOverflow { layoutMinimum.Y = 0 }
minimum = minimum.Add(layoutMinimum)
}
return minimum
@ -295,19 +285,18 @@ func (this *containerBox) doLayout () {
if this.layout != nil {
minimum = this.layout.MinimumSize (
this.layoutHints(),
this.boxQuerier())
this.children)
}
innerBounds := this.InnerBounds()
overflow := this.attrOverflow.Value()
this.contentBounds = innerBounds.Sub(innerBounds.Min)
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 }
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.boxArranger())
this.layout.Arrange(layoutHints, this.children)
}
// build an accurate contentBounds by unioning the bounds of all child
@ -323,7 +312,7 @@ func (this *containerBox) doLayout () {
// offset children and contentBounds by scroll
for _, box := range this.children {
assertAnyBox(box).setBounds(box.Bounds().Add(this.scroll).Add(innerBounds.Min))
box.SetBounds(box.Bounds().Add(this.scroll).Add(innerBounds.Min))
}
this.contentBounds = this.contentBounds.Add(this.scroll)
@ -356,14 +345,6 @@ 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()
@ -386,12 +367,12 @@ func (this *containerBox) recursiveReApply () {
}
}
func (this *containerBox) boxUnder (point image.Point) anyBox {
func (this *containerBox) boxUnder (point image.Point, category eventCategory) anyBox {
if !point.In(this.bounds) { return nil }
if !this.mask {
if !this.capture[category] {
for _, box := range this.children {
candidate := box.(anyBox).boxUnder(point)
candidate := box.(anyBox).boxUnder(point, category)
if candidate != nil { return candidate }
}
}
@ -419,6 +400,6 @@ func (this *containerBox) propagateAlt (callback func (anyBox) bool) bool {
return true
}
func (this *containerBox) masks () bool {
return this.mask
func (this *containerBox) captures (category eventCategory) bool {
return this.capture[category]
}

View File

@ -3,10 +3,6 @@ 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) {
@ -24,22 +20,14 @@ 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)
}
}
@ -47,72 +35,63 @@ 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) {
this.keyboardTargets(func (target anyBox) bool {
if target.handleKeyUp(key, numberPad) {
return false
if target := this.keyboardTarget(); target != nil {
target.handleKeyUp(key, numberPad)
}
return true
})
}
// 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.
// 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) {
boxes := []anyBox { }
this.boxesUnder(this.mousePosition)(func (box anyBox) bool {
boxes = append(boxes, box)
return !box.handleMouseDown(button)
})
this.drags[button] = boxes
underneath := this.boxUnder(this.mousePosition, eventCategoryMouse)
this.drags[button] = underneath
if underneath != nil {
underneath.handleMouseDown(button)
}
}
// 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
// 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) {
for _, box := range this.drags[button] {
box.handleMouseUp(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 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.
// "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
dragged := false
for _, dragSet := range this.drags {
for _, box := range dragSet {
if box.handleMouseMove() { break }
handled := false
for _, child := range this.drags {
if child == nil { continue }
child.handleMouseMove()
handled = true
}
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()
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. If the event which triggers this comes with mouse position
// information, HandleMouseMove must be called *before* HandleScroll.
// cursor.
func (this *Hierarchy) HandleScroll (x, y float64) {
this.boxesUnder(this.mousePosition)(func (box anyBox) bool {
return !box.handleScroll(x, y)
})
underneath := this.boxUnder(this.mousePosition, eventCategoryScroll)
if underneath != nil {
underneath.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
needStyle util.Set[anyBox]
needMinimum 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,
needStyle: make(util.Set[anyBox]),
needMinimum: make(util.Set[anyBox]),
needLayout: make(util.Set[anyBox]),
needDraw: make(util.Set[anyBox]),
}
@ -95,16 +95,6 @@ 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 }
@ -114,7 +104,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
@ -126,8 +116,8 @@ func (this *Hierarchy) AfterEvent () {
return
}
for len(this.needStyle) > 0 {
this.needStyle.Pop().doStyle()
for len(this.needMinimum) > 0 {
this.needMinimum.Pop().doMinimumSize()
}
if !this.minimumClean {
this.doMinimumSize()
@ -156,7 +146,7 @@ func (this *Hierarchy) setStyle () {
if this.root != nil { this.root.recursiveReApply() }
}
func (this *Hierarchy) setIconSet () {
func (this *Hierarchy) setIcons () {
if this.root != nil { this.root.recursiveReApply() }
}
@ -168,7 +158,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
}
@ -196,8 +186,8 @@ func (this *Hierarchy) notifyMinimumSizeChange (anyBox) {
this.minimumClean = false
}
func (this *Hierarchy) invalidateStyle (box anyBox) {
this.needStyle.Add(box)
func (this *Hierarchy) invalidateMinimum (box anyBox) {
this.needMinimum.Add(box)
}
func (this *Hierarchy) invalidateDraw (box anyBox) {
@ -245,52 +235,35 @@ func (this *Hierarchy) anyFocused () bool {
return this.focused != nil
}
func (this *Hierarchy) masks () bool {
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) 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) {
func (this *Hierarchy) keyboardTarget () anyBox {
focused := this.focused
if focused == nil { return }
this.parents(this.considerMaskingParents(focused))(yield)
}
func (this *Hierarchy) considerMaskingParents (box anyBox) anyBox {
parent := box.getParent()
if focused == nil { return nil }
parent := focused.getParent()
for {
parentBox, ok := parent.(anyBox)
if !ok { break }
if parent.masks() {
if parent.captures(eventCategoryKeyboard) {
return parentBox
}
parent = parentBox.getParent()
}
return box
return focused
}
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() {
@ -314,7 +287,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
}
@ -346,14 +319,8 @@ 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,6 +5,14 @@ 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 {
@ -19,8 +27,9 @@ 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)
// catches returns whether or not this parent masks events.
masks () bool
// 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.
@ -35,10 +44,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
// doStyle re-applies the box's style non-recursively
// doMinimumSize re-calculates the minimum size of the anyBox non-recursively
doDraw ()
doLayout ()
doStyle ()
doMinimumSize ()
// flushActionQueue performs any queued actions, like invalidating the
// minimum size or grabbing input focus.
@ -54,8 +63,6 @@ type anyBox interface {
// 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
@ -64,18 +71,12 @@ 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) anyBox
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
// 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
@ -91,21 +92,12 @@ type anyBox interface {
// handleDndDrop (data.Data)
handleMouseEnter ()
handleMouseLeave ()
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
handleMouseMove ()
handleMouseDown (input.Button)
handleMouseUp (input.Button)
handleScroll (float64, float64)
handleKeyDown (input.Key, bool)
handleKeyUp (input.Key, bool)
}
func assertAnyBox (unknown tomo.Box) anyBox {

View File

@ -1,46 +0,0 @@
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) {
}
}
// SetIconSet notifies objects that the icons have changed.
func (this *System) SetIconSet (iconSet tomo.IconSet) {
// SetIcons notifies objects that the icons have changed.
func (this *System) SetIcons (icons tomo.Icons) {
this.iconsNonce ++
for hierarchy := range this.hierarchies {
hierarchy.setIconSet()
hierarchy.setIcons()
}
}

View File

@ -2,6 +2,7 @@ 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"
@ -13,21 +14,22 @@ import "git.tebibyte.media/tomo/tomo/canvas"
type textBox struct {
*box
hOverflow, vOverflow bool
contentBounds image.Rectangle
scroll image.Point
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
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
@ -38,12 +40,20 @@ type textBox struct {
}
func (this *System) NewTextBox () tomo.TextBox {
box := &textBox { }
box := &textBox {
textColor: color.Black,
dotColor: color.RGBA { B: 255, G: 255, A: 255 },
}
box.box = this.newBox(box)
return box
}
// ----- public methods ----------------------------------------------------- //
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
@ -61,7 +71,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 {
@ -76,11 +86,40 @@ 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 }
@ -99,19 +138,20 @@ func (this *textBox) OnDotChange (callback func ()) event.Cookie {
return this.on.dotChange.Connect(callback)
}
// ----- private methods ---------------------------------------------------- //
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 }
texture := this.attrTexture.Value().Texture
col := this.attrColor.Value().Color
if col == nil { col = color.Transparent }
this.drawBorders(can)
pen := can.Pen()
pen.Fill(col)
pen.Texture(texture)
pen.Fill(this.color)
pen.Texture(this.texture)
if this.transparent() && this.parent != nil {
this.parent.drawBackgroundPart(can)
@ -122,50 +162,8 @@ func (this *textBox) Draw (can canvas.Canvas) {
this.drawDot(can)
}
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)
}
if this.face == nil { return }
this.drawer.Draw(can, this.textColor, this.textOffset())
}
func roundPt (point fixed.Point26_6) image.Point {
@ -177,20 +175,14 @@ func fixPt (point image.Point) fixed.Point26_6 {
}
func (this *textBox) drawDot (can canvas.Canvas) {
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 } }
if this.face == nil { return }
pen := can.Pen()
pen.Fill(color.Transparent)
pen.Stroke(textColor)
pen.Stroke(this.textColor)
bounds := this.InnerBounds()
metrics := face.Metrics()
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()))
@ -204,7 +196,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(dotColor)
pen.Fill(this.dotColor)
pen.StrokeWeight(0)
pen.Rectangle(image.Rectangle {
Min: roundPt(start.Add(ascent)),
@ -212,7 +204,7 @@ func (this *textBox) drawDot (can canvas.Canvas) {
})
default:
pen.Fill(dotColor)
pen.Fill(this.dotColor)
pen.StrokeWeight(0)
rect := image.Rectangle {
@ -249,37 +241,35 @@ func (this *textBox) handleFocusLeave () {
this.box.handleFocusLeave()
}
func (this *textBox) handleMouseDown (button input.Button) bool {
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 })
}
return this.box.handleMouseDown(button)
this.box.handleMouseDown(button)
}
func (this *textBox) handleMouseUp (button input.Button) bool {
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 })
}
return this.box.handleMouseUp(button)
this.box.handleMouseUp(button)
}
func (this *textBox) handleMouseMove () bool {
func (this *textBox) handleMouseMove () {
if this.selecting {
index := this.runeUnderMouse()
this.Select(text.Dot { Start: this.selectStart, End: index })
}
return this.box.handleMouseMove()
this.box.handleMouseMove()
}
func (this *textBox) runeUnderMouse () int {
window := this.Window()
if window == nil { return 0 }
position := window.MousePosition().Sub(this.textOffset())
position := this.MousePosition().Sub(this.textOffset())
return this.drawer.AtPosition(fixPt(position))
}
@ -291,10 +281,10 @@ func (this *textBox) normalizedLayoutBoundsSpace () image.Rectangle {
func (this *textBox) contentMinimum () image.Point {
minimum := this.drawer.MinimumSize()
if this.attrOverflow.Value().X || bool(this.attrWrap.Value()) {
if this.hOverflow || this.wrap {
minimum.X = this.drawer.Em().Round()
}
if this.attrOverflow.Value().Y {
if this.vOverflow {
minimum.Y = this.drawer.LineHeight().Round()
}

View File

@ -1,6 +1,5 @@
package util
import "io"
import "image/color"
// IndexOf returns the index of needle within haystack. If needle does not exist
@ -100,32 +99,3 @@ 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
}

View File

@ -102,20 +102,20 @@ func (this *Backend) NewBox () tomo.Box {
return this.system.NewBox()
}
func (this *Backend) NewTextBox () tomo.TextBox {
return this.system.NewTextBox()
}
func (this *Backend) NewCanvasBox () tomo.CanvasBox {
return this.system.NewCanvasBox()
}
func (this *Backend) NewContainerBox () tomo.ContainerBox {
return this.system.NewContainerBox()
}
func (this *Backend) NewSurfaceBox () (tomo.SurfaceBox, error) {
return this.system.NewSurfaceBox()
}
func (this *Backend) NewContainerBox () tomo.ContainerBox {
return this.system.NewContainerBox()
func (this *Backend) NewTextBox () tomo.TextBox {
return this.system.NewTextBox()
}
func (this *Backend) NewTexture (source image.Image) canvas.TextureCloser {
@ -126,12 +126,12 @@ func (this *Backend) NewCanvas (bounds image.Rectangle) canvas.CanvasCloser {
return xcanvas.NewCanvas(this.x, bounds)
}
func (this *Backend) SetStyle (style *tomo.Style) {
func (this *Backend) SetStyle (style tomo.Style) {
this.system.SetStyle(style)
}
func (this *Backend) SetIconSet (icons tomo.IconSet) {
this.system.SetIconSet(icons)
func (this *Backend) SetIcons (icons tomo.Icons) {
this.system.SetIcons(icons)
}
func (this *Backend) assert () {

26
x/selection.go Normal file
View File

@ -0,0 +1,26 @@
package x
import "io"
import "strings"
import "git.tebibyte.media/tomo/xgbsel/v2"
import "git.tebibyte.media/tomo/tomo/data"
type selectionData struct {
data data.Data
}
func (this selectionData) Convert (target xgbsel.Target) (io.ReadSeekCloser, bool) {
mimeStr, _ := target.ToMime()
ty, subty, _ := strings.Cut(string(mimeStr), "/")
mime := data.M(ty, subty)
stream, ok := this.data[mime]
stream.Seek(0, io.SeekStart)
return stream, ok
}
func (this selectionData) Supported () (targets []xgbsel.Target) {
for mime := range this.data {
targets = append(targets, xgbsel.MimeToTargets(mime.String())...)
}
return targets
}

View File

@ -4,7 +4,6 @@ import "image"
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/x/canvas"
@ -180,17 +179,12 @@ func (this *window) SetTitle (title string) {
icccm.WmIconNameSet (this.backend.x, this.xWindow.Id, title)
}
func (this *window) SetIcon (icon tomo.Icon) {
textures := []canvas.Texture {
icon.Texture(tomo.IconSizeSmall),
icon.Texture(tomo.IconSizeMedium),
icon.Texture(tomo.IconSizeLarge),
}
func (this *window) SetIcon (sizes ...canvas.Texture) {
wmIcons := []ewmh.WmIcon { }
for _, icon := range textures {
icon := xcanvas.AssertTexture(icon)
for _, icon := range sizes {
icon, ok := icon.(*xcanvas.Texture)
if !ok { continue }
bounds := icon.Bounds()
width := bounds.Dx()
@ -238,26 +232,6 @@ func (this *window) SetBounds (bounds image.Rectangle) {
bounds.Min.Y + bounds.Dy())
}
func (this *window) NewChild (bounds image.Rectangle) (tomo.Window, error) {
leader := this.leader
child, err := this.backend.newWindow (
bounds.Add(this.metrics.bounds.Min), false)
child.leader = leader
if err != nil { return nil, err }
child.setClientLeader(leader)
leader.setClientLeader(leader)
icccm.WmTransientForSet (
this.backend.x,
child.xWindow.Id,
leader.xWindow.Id)
child.setType("UTILITY")
// child.inheritProperties(leader.window)
return child, err
}
func (this *window) NewMenu (bounds image.Rectangle) (tomo.Window, error) {
menu, err := this.backend.newWindow (
bounds.Add(this.metrics.bounds.Min), true)
@ -288,12 +262,29 @@ func (this *window) NewModal (bounds image.Rectangle) (tomo.Window, error) {
return modal, err
}
func (this *window) Modifiers () input.Modifiers {
return this.hierarchy.Modifiers()
func (this *window) NewChild (bounds image.Rectangle) (tomo.Window, error) {
leader := this.leader
child, err := this.backend.newWindow (
bounds.Add(this.metrics.bounds.Min), false)
child.leader = leader
if err != nil { return nil, err }
child.setClientLeader(leader)
leader.setClientLeader(leader)
icccm.WmTransientForSet (
this.backend.x,
child.xWindow.Id,
leader.xWindow.Id)
child.setType("UTILITY")
// child.inheritProperties(leader.window)
return child, err
}
func (this *window) MousePosition () image.Point {
return this.hierarchy.MousePosition()
func (this *window) Widget () (tomo.Window, error) {
// TODO
return nil, nil
}
func (this *window) Copy (data.Data) {