74 Commits

Author SHA1 Message Date
fa2ef954b2 Backend unsets style attributes if they are no longer specified 2024-08-12 20:36:19 -04:00
e4fdde3da1 Use premultiplied alpha for X canvas 2024-08-12 18:15:15 -04:00
d166d88388 Remove AttrIcon from Box implementation 2024-08-11 22:29:08 -04:00
74025aac97 Update Tomo API 2024-08-11 22:29:01 -04:00
e1cf524c57 TextBox tries to get a type face when parented if its face is nil 2024-08-11 11:55:13 -04:00
919f000073 Update Tomo API 2024-08-10 21:14:06 -04:00
8aa8dc9570 Add support for AttrIcon 2024-08-10 21:07:31 -04:00
a60a729ad9 Update X backend 2024-08-10 20:47:36 -04:00
2af42a3568 Update internal system 2024-08-10 20:24:25 -04:00
e2b3b84993 Add Exists method to Optional 2024-08-10 01:55:24 -04:00
925e011465 Add IconSet, FaceSet to style package 2024-08-10 01:39:23 -04:00
d4c08a0f8c Add an Optional type to util 2024-08-09 23:41:38 -04:00
38054a95bb Update Tomo API 2024-08-09 23:27:40 -04:00
2ae5e2e30f Move style into this repostiory 2024-08-09 23:27:12 -04:00
bacdd81f60 Propagate mouse motion events to boxes 2024-07-27 15:13:49 -04:00
d944f6016f Update Tomo API to v0.41.1 2024-07-27 15:04:11 -04:00
01582d4ad1 Same for TextBox 2024-07-27 13:47:22 -04:00
3941dae44a Invalidate container minimum size when overflow is changed 2024-07-27 13:46:52 -04:00
85b8536925 Propagate keyboard events to root if nothing is focused
This makes window-level keybinds possible. Exciting!
2024-07-27 02:20:06 -04:00
6ff5dea308 ContainerBox correctly checks for overflow when reporting recommended size 2024-07-27 02:18:52 -04:00
33969f45e9 BoxQuerier returns box minimum size as a fallback for reccomended sizes 2024-07-27 02:17:56 -04:00
832d7e02ef TextBox can be selected with left, middle, and right buttons 2024-07-26 21:17:30 -04:00
fd6297b4fb And arrow keys! Because, why not! 2024-07-26 20:55:34 -04:00
4deb581667 Use tab for keynav instead of alt-tab 2024-07-26 20:52:49 -04:00
192e6c6235 Keynav skips masked boxes 2024-07-26 20:49:10 -04:00
9729e3dfda Fix selectable detection when using keys on TextBox 2024-07-26 20:48:44 -04:00
fad46eafd3 All selectable TextBoxes have keyboard controls 2024-07-26 18:43:12 -04:00
ddde2a79a8 TextBox defaults to black for a cursor color 2024-07-26 17:34:33 -04:00
180a5eb8d1 Hierarchy is now responsible for focusing boxes when they are clicked 2024-07-26 17:34:14 -04:00
a92951f891 Remove debug message 2024-07-26 00:29:07 -04:00
37ec962d1f TextBox properly gives attributes values to the typeset drawer 2024-07-26 00:27:32 -04:00
4f89b11799 Box applies the style to the outer box (oops!) 2024-07-26 00:22:10 -04:00
7809aac72f Actually use layouts 2024-07-25 21:05:03 -04:00
bb082d3989 Change when the parent is notified of a child's minimum size change 2024-07-25 21:04:32 -04:00
fbb6d61cfc Fix style application 2024-07-25 21:04:21 -04:00
e4cba4a7c9 Add check while calculating min size to prevent goofy situations 2024-07-25 20:37:38 -04:00
6192a1e9cc Fixed util.Memo 2024-07-25 20:37:09 -04:00
5864c74691 Fix some segfaults 2024-07-25 18:17:43 -04:00
a62dff4236 Update code for x backend 2024-07-25 13:01:24 -04:00
196afbc2f3 Update code for internal system 2024-07-25 13:01:15 -04:00
9b61600f31 Update Tomo API to v0.41.0 2024-07-25 13:00:37 -04:00
5b62c9e162 Update Tomo API 2024-07-21 13:06:30 -04:00
19ca3e821a Add xgbsel as dependency 2024-07-21 13:06:09 -04:00
39a591e732 Fix boundary detection for polygons 2024-06-24 19:11:21 -04:00
07865dc85f Polygon boundary detection now rounds properly 2024-06-24 19:04:08 -04:00
868b6fdfe9 Fix crash when CanvasBox draws with nil canvas 2024-06-24 18:43:54 -04:00
1c803ff9c1 Fix out of bounds panic with polygon filling 2024-06-24 18:42:43 -04:00
fdcf254891 Box re-applies theme on role change 2024-06-20 16:44:24 -04:00
e23e794730 CanvasBox no longer crashes on nil drawer 2024-06-19 12:13:31 -04:00
caa261665f Remove obsolete TODO 2024-06-15 23:35:44 -04:00
e21b57a915 X backend properly converts image data 2024-06-15 23:33:59 -04:00
727a801243 Attempt to fix strange issue with overflowing 2024-06-14 02:30:59 -04:00
76701d4383 Fix style application part 2 2024-06-12 02:12:24 -04:00
6619987b5a Fixed style application 2024-06-12 00:39:00 -04:00
02de78c997 Window is resizable by default (lol) 2024-06-12 00:29:07 -04:00
95b1d033a9 SetResizable has been implemented 2024-06-11 23:58:19 -04:00
1951b6e408 Partially implement new stuff for X 2024-06-11 23:45:49 -04:00
c7f09c7894 Add recommended sizes and all that jazz 2024-06-11 22:45:40 -04:00
80f60b42de I lied 2024-06-11 18:35:40 -04:00
995e6fd624 Add theme setting nonsense 2024-06-11 18:12:47 -04:00
26b69d3e21 Update Tomo API 2024-06-11 17:18:30 -04:00
52a0136e60 Fixed Window.NewChild returning the parent (oops) 2024-06-07 01:47:24 -04:00
5657f85c83 Update Window.SetIcon 2024-06-07 01:12:04 -04:00
a9ac9f6600 Change scrolling speed from 16 to 32 2024-06-07 01:07:48 -04:00
6eff6887e7 Unify window and mainWindow 2024-06-07 01:07:28 -04:00
adf0ef3a89 Update Tomo API 2024-06-07 01:07:15 -04:00
07bcd119d7 X backend uses Close method instead of Destroy to free canvas 2024-06-03 20:54:45 -04:00
c51ce65c88 System no longer requires a NewCanvas method 2024-06-03 20:44:58 -04:00
0d93d73c32 Backend returns CanvasCloser 2024-06-03 20:43:05 -04:00
006921d690 Store Role in Box 2024-06-03 20:42:54 -04:00
f2bcc217a4 Update Tomo API 2024-06-03 20:42:08 -04:00
a4e067cacb HandleKeyUp causes a... key up event. As it should. 2024-06-03 02:20:37 -04:00
5a2b4cc2f5 Boxes are able to check if they are focused 2024-06-03 02:18:30 -04:00
250c3076fb Fixed another segfault. Do not write code at 4 am. 2024-06-03 02:11:47 -04:00
23 changed files with 1657 additions and 579 deletions

5
go.mod
View File

@@ -3,11 +3,8 @@ module git.tebibyte.media/tomo/backend
go 1.20
require (
git.tebibyte.media/tomo/tomo v0.34.0
git.tebibyte.media/tomo/tomo v0.45.0
git.tebibyte.media/tomo/typeset v0.7.1
)
require (
git.tebibyte.media/tomo/xgbkb v1.0.1
github.com/jezek/xgb v1.1.1
github.com/jezek/xgbutil v0.0.0-20231116234834-47f30c120111

4
go.sum
View File

@@ -1,6 +1,6 @@
git.tebibyte.media/sashakoshka/xgbkb v1.0.0/go.mod h1:pNcE6TRO93vHd6q42SdwLSTTj25L0Yzggz7yLe0JV6Q=
git.tebibyte.media/tomo/tomo v0.34.0 h1:r5yJPks9rtzdDI2RyAUdqa1qb6BebG0QFe2cTmcFi+0=
git.tebibyte.media/tomo/tomo v0.34.0/go.mod h1:C9EzepS9wjkTJjnZaPBh22YvVPyA4hbBAJVU20Rdmps=
git.tebibyte.media/tomo/tomo v0.45.0 h1:fQH0WIPidW275hOq9dE6R7p064xG1RGx2QU68Avlr84=
git.tebibyte.media/tomo/tomo v0.45.0/go.mod h1:WrtilgKB1y8O2Yu7X4mYcRiqOlPR8NuUnoA/ynkQWrs=
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=

View File

@@ -0,0 +1,70 @@
package system
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/backend/internal/util"
type attrHierarchy [T tomo.Attr] struct {
fallback T
style util.Optional[T]
user util.Optional[T]
}
func (this *attrHierarchy[T]) SetFallback (fallback T) {
this.fallback = fallback
}
func (this *attrHierarchy[T]) SetStyle (style T) (different bool) {
styleEquals := false
if previous, ok := this.style.Value(); ok {
styleEquals = previous.Equals(style)
}
this.style.Set(style)
return !styleEquals && !this.user.Exists()
}
func (this *attrHierarchy[T]) UnsetStyle () (different bool) {
different = this.style.Exists()
this.style.Unset()
return different
}
func (this *attrHierarchy[T]) SetUser (user T) (different bool) {
userEquals := false
if previous, ok := this.user.Value(); ok {
userEquals = previous.Equals(user)
}
this.user.Set(user)
return !userEquals
}
func (this *attrHierarchy[T]) UnsetUser () (different bool) {
different = this.user.Exists()
this.user.Unset()
return different
}
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]) Unset (user bool) (different bool) {
if user {
return this.UnsetUser()
} else {
return this.UnsetStyle()
}
}
func (this *attrHierarchy[T]) Value () T {
if user, ok := this.user.Value(); ok {
return user
} else if style, ok := this.style.Value(); ok{
return style
} else {
return this.fallback
}
}

View File

@@ -9,62 +9,68 @@ 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
lastStyleNonce int
lastIconSetNonce 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)]
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,
tags: make(util.Set[string]),
}
box.attrColor.SetFallback(tomo.AColor(color.Transparent))
box.canvas = util.NewMemo (func () canvas.Canvas {
if box.parent == nil { return nil }
parentCanvas := box.parent.getCanvas()
@@ -75,15 +81,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
}
@@ -99,87 +106,43 @@ func (this *box) Bounds () image.Rectangle {
}
func (this *box) InnerBounds () image.Rectangle {
return this.padding.Apply(this.innerClippingBounds)
return tomo.Inset(this.attrPadding.Value()).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) SetRole (role tomo.Role) {
if this.role == role { return }
this.role = role
this.lastStyleNonce = -1
this.outer.recursiveReApply()
}
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) 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) SetMinimumSize (size image.Point) {
if this.userMinSize == size { return }
this.userMinSize = size
this.invalidateMinimum()
func (this *box) SetTag (tag string, on bool) {
if on {
this.tags.Add(tag)
} else {
delete(this.tags, tag)
}
}
func (this *box) SetPadding (padding tomo.Inset) {
if this.padding == padding { return }
this.padding = padding
this.invalidateLayout()
this.invalidateMinimum()
func (this *box) SetAttr (attr tomo.Attr) {
this.outer.setAttr(attr, true)
}
func (this *box) UnsetAttr (kind tomo.AttrKind) {
this.outer.unsetAttr(kind, true)
}
func (this *box) SetDNDData (dat data.Data) {
@@ -191,7 +154,7 @@ func (this *box) SetDNDAccept (types ...data.Mime) {
}
func (this *box) SetFocused (focused bool) {
hierarchy := this.parent.getHierarchy()
hierarchy := this.getHierarchy()
if hierarchy == nil {
focusedCopy := focused
this.focusQueued = &focusedCopy
@@ -206,6 +169,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
@@ -214,22 +183,107 @@ func (this *box) SetFocusable (focusable bool) {
}
}
func (this *box) Focused () bool {
hierarchy := this.getHierarchy()
if hierarchy == nil { return false }
return hierarchy.isFocused(this)
// ----- 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)
this.handleBorderChange(previousBorderSum, different)
case tomo.AttrMinimumSize:
if this.attrMinimumSize.Set(attr, user) {
this.invalidateMinimum()
}
case tomo.AttrPadding:
if this.attrPadding.Set(attr, user) {
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) unsetAttr (kind tomo.AttrKind, user bool) {
switch kind {
case tomo.AttrKindColor:
if this.attrColor.Unset(user) {
this.invalidateDraw()
}
case tomo.AttrKindTexture:
if this.attrTexture.Unset(user) {
this.invalidateDraw()
}
case tomo.AttrKindTextureMode:
if this.attrTextureMode.Unset(user) {
this.invalidateDraw()
}
case tomo.AttrKindBorder:
previousBorderSum := this.borderSum()
different := this.attrBorder.Unset(user)
this.handleBorderChange(previousBorderSum, different)
case tomo.AttrKindMinimumSize:
if this.attrMinimumSize.Unset(user) {
this.invalidateMinimum()
}
case tomo.AttrKindPadding:
if this.attrPadding.Unset(user) {
this.invalidateLayout()
this.invalidateMinimum()
}
}
}
func (this *box) MousePosition () image.Point {
hierarchy := this.getHierarchy()
if hierarchy == nil { return image.Point { } }
return hierarchy.getMousePosition()
func (this *box) setBounds (bounds image.Rectangle) {
if this.bounds == bounds { return }
this.bounds = bounds
this.invalidateLayout()
}
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 ---------------------------------------------- //
@@ -254,70 +308,95 @@ 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) 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) handleMouseDown (button input.Button) {
if this.focusable {
this.SetFocused(true)
} else {
hierarchy := this.getHierarchy()
if hierarchy == nil { return }
hierarchy.focus(nil)
}
for _, listener := range this.on.mouseDown.Listeners() {
listener(button)
func (this *box) handleMouseMove () (caught bool) {
for _, listener := range this.on.mouseMove.Listeners() {
if listener() { caught = true }
}
return
}
func (this *box) handleMouseUp (button input.Button) {
for _, listener := range this.on.mouseUp.Listeners() {
listener(button)
func (this *box) handleMouseDown (button input.Button) (caught bool) {
if button == input.ButtonLeft {
this.pressed = true
this.invalidateStyle()
}
for _, listener := range this.on.buttonDown.Listeners() {
if listener(button) { caught = true }
}
return
}
func (this *box) handleScroll (x, y float64) {
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) (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
}
// -------------------------------------------------------------------------- //
@@ -326,34 +405,46 @@ 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()
textureOrigin :=
bounds.Min.
Add(image.Pt (
bounds.Dx() / 2,
bounds.Dy() / 2)).
Sub(image.Pt (
textureBounds.Dx() / 2,
textureBounds.Dy() / 2))
pen.Fill(color.Transparent)
pen.Texture(this.texture)
pen.Rectangle(textureBounds.Sub(textureBounds.Min).Add(textureOrigin))
if textureMode == tomo.TextureModeCenter && texture != nil {
this.centeredTexture(can, texture)
}
}
func (this *box) centeredTexture (can canvas.Canvas, texture canvas.Texture) {
pen := can.Pen()
bounds := this.Bounds()
textureBounds := texture.Bounds()
textureOrigin :=
bounds.Min.
Add(image.Pt (
bounds.Dx() / 2,
bounds.Dy() / 2)).
Sub(image.Pt (
textureBounds.Dx() / 2,
textureBounds.Dy() / 2))
pen.Fill(color.Transparent)
pen.Texture(texture)
pen.Rectangle(textureBounds.Sub(textureBounds.Min).Add(textureOrigin))
}
func (this *box) drawBorders (can canvas.Canvas) {
if can == nil { return }
pen := can.Pen()
@@ -361,6 +452,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))
}
@@ -368,7 +460,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,
@@ -400,26 +492,26 @@ 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
@@ -441,7 +533,11 @@ func (this *box) doLayout () {
// laycnt ++
this.innerClippingBounds = this.borderSum().Apply(this.bounds)
this.loseCanvas()
this.outer.recursiveLoseCanvas()
}
func (this *box) doStyle () {
this.styleApplicator.apply(this.outer)
}
func (this *box) setParent (parent parent) {
@@ -449,6 +545,7 @@ func (this *box) setParent (parent parent) {
this.SetFocused(false)
}
this.parent = parent
this.outer.recursiveReApply()
}
func (this *box) getParent () parent {
@@ -457,10 +554,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)
}
@@ -471,8 +564,28 @@ func (this *box) recursiveRedo () {
this.doDraw()
}
func (this *box) loseCanvas () {
this.canvas.InvalidateTo(nil)
func (this *box) recursiveLoseCanvas () {
this.canvas.Invalidate()
}
func (this *box) handleBorderChange (previousBorderSum tomo.Inset, different bool) {
// 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()
}
}
func (this *box) invalidateStyle () {
hierarchy := this.getHierarchy()
if hierarchy == nil { return }
hierarchy.invalidateStyle(this.outer)
}
func (this *box) invalidateLayout () {
@@ -488,11 +601,36 @@ func (this *box) invalidateDraw () {
}
func (this *box) invalidateMinimum () {
this.minSize.Invalidate()
if this.parent != nil {
this.parent.notifyMinimumSizeChange(this)
}
}
func (this *box) recursiveReApply () {
hierarchy := this.getHierarchy()
if hierarchy == nil {
this.minSizeQueued = true
} else {
hierarchy.invalidateMinimum(this.outer)
if hierarchy == nil { return }
// re-apply styling, icons *if needed*
// 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).
this.lastStyleNonce = hierarchyStyleNonce
this.styleApplicator = hierarchy.newStyleApplicator()
this.invalidateStyle()
this.on.styleChange.Broadcast()
}
// icons
hierarchyIconSetNonce := this.getIconSetNonce()
if this.lastIconSetNonce != hierarchyIconSetNonce {
this.lastIconSetNonce = hierarchyIconSetNonce
this.on.iconSetChange.Broadcast()
}
}
@@ -500,7 +638,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 {
@@ -519,7 +657,8 @@ 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) /*&&
col := this.attrColor.Value().Color
return col == nil || util.Transparent(col) /*&&
(this.texture == nil || !this.texture.Opaque())*/
}
@@ -533,3 +672,13 @@ func (this *box) getHierarchy () *Hierarchy {
if this.parent == nil { return nil }
return this.parent.getHierarchy()
}
func (this *box) getStyleNonce () int {
// should panic if not in the tree
return this.getHierarchy().getStyleNonce()
}
func (this *box) getIconSetNonce () int {
// should panic if not in the tree
return this.getHierarchy().getIconSetNonce()
}

View File

@@ -0,0 +1,65 @@
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 box.minimumSize().X
}
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 box.minimumSize().Y
}
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()
@@ -29,7 +25,11 @@ func (this *canvasBox) Invalidate () {
}
func (this *canvasBox) Draw (can canvas.Canvas) {
if can == nil { return }
this.box.Draw(can)
this.userDrawer.Draw (
can.SubCanvas(this.padding.Apply(this.innerClippingBounds)))
if this.userDrawer != nil {
padding := tomo.Inset(this.attrPadding.Value())
this.userDrawer.Draw (
can.SubCanvas(padding.Apply(this.innerClippingBounds)))
}
}

View File

@@ -10,15 +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
layout tomo.Layout
attrGap attrHierarchy[tomo.AttrGap]
attrAlign attrHierarchy[tomo.AttrAlign]
attrOverflow attrHierarchy[tomo.AttrOverflow]
attrLayout attrHierarchy[tomo.AttrLayout]
children []anyBox
on struct {
contentBoundsChange event.FuncBroadcaster
@@ -31,37 +32,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
@@ -77,32 +48,9 @@ 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()
@@ -113,7 +61,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)
@@ -124,15 +72,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)
@@ -149,7 +97,7 @@ func (this *containerBox) Clear () {
this.invalidateMinimum()
}
func (this *containerBox) Length () int {
func (this *containerBox) Len () int {
return len(this.children)
}
@@ -160,15 +108,20 @@ 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 {
rocks[index] = box.Bounds()
@@ -180,17 +133,137 @@ 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()
this.invalidateMinimum()
}
case tomo.AttrLayout:
if this.attrLayout.Set(attr, user) {
this.invalidateLayout()
this.invalidateMinimum()
}
default: this.box.setAttr(attr, user)
}
}
func (this *containerBox) unsetAttr (kind tomo.AttrKind, user bool) {
switch kind {
case tomo.AttrKindColor:
if this.attrColor.Unset(user) {
this.invalidateTransparentChildren()
this.invalidateDraw()
}
case tomo.AttrKindTexture:
if this.attrTexture.Unset(user) {
this.invalidateTransparentChildren()
this.invalidateDraw()
}
case tomo.AttrKindTextureMode:
if this.attrTextureMode.Unset(user) {
this.invalidateTransparentChildren()
this.invalidateDraw()
}
case tomo.AttrKindGap:
if this.attrGap.Unset(user) {
this.invalidateLayout()
this.invalidateMinimum()
}
case tomo.AttrKindAlign:
if this.attrAlign.Unset(user) {
this.invalidateLayout()
}
case tomo.AttrKindOverflow:
if this.attrOverflow.Unset(user) {
this.invalidateLayout()
this.invalidateMinimum()
}
case tomo.AttrKindLayout:
if this.attrLayout.Unset(user) {
this.invalidateLayout()
this.invalidateMinimum()
}
default: this.box.unsetAttr(kind, user)
}
}
func (this *containerBox) recommendedHeight (width int) int {
layout := this.attrLayout.Value().Layout
if layout == nil || !this.attrOverflow.Value().Y {
return this.minSize.Value().Y
} else {
return layout.RecommendedHeight(this.layoutHints(), this.boxQuerier(), width) +
this.borderAndPaddingSum().Vertical()
}
}
func (this *containerBox) recommendedWidth (height int) int {
layout := this.attrLayout.Value().Layout
if layout == nil || !this.attrOverflow.Value().X {
return this.minSize.Value().X
} else {
return 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)
@@ -227,7 +300,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()
@@ -235,23 +308,28 @@ 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()
if this.layout != nil {
layoutMinimum := this.layout.MinimumSize (
overflow := this.attrOverflow.Value()
minimum := this.box.contentMinimum()
layout := this.attrLayout.Value().Layout
if layout != nil {
layoutMinimum := 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
@@ -260,25 +338,27 @@ func (this *containerBox) contentMinimum () image.Point {
func (this *containerBox) doLayout () {
this.box.doLayout()
previousContentBounds := this.contentBounds
layout := this.attrLayout.Value().Layout
// by default, use innerBounds (translated to 0, 0) for contentBounds.
// if a direction overflows, use the layout's minimum size for it.
var minimum image.Point
if this.layout != nil {
minimum = this.layout.MinimumSize (
if layout != nil {
minimum = 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 {
if layout != nil {
layoutHints := this.layoutHints()
layoutHints.Bounds = this.contentBounds
this.layout.Arrange(layoutHints, this.children)
layout.Arrange(layoutHints, this.boxArranger())
}
// build an accurate contentBounds by unioning the bounds of all child
@@ -294,7 +374,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))
box.setBounds(box.Bounds().Add(this.scroll).Add(innerBounds.Min))
}
this.contentBounds = this.contentBounds.Add(this.scroll)
@@ -327,6 +407,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()
@@ -335,12 +423,26 @@ func (this *containerBox) recursiveRedo () {
}
}
func (this *containerBox) boxUnder (point image.Point, category eventCategory) anyBox {
func (this *containerBox) recursiveLoseCanvas () {
this.box.recursiveLoseCanvas()
for _, child := range this.children {
child.(anyBox).recursiveLoseCanvas()
}
}
func (this *containerBox) recursiveReApply () {
this.box.recursiveReApply()
for _, child := range this.children {
child.(anyBox).recursiveReApply()
}
}
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 }
}
}
@@ -368,6 +470,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,9 @@ package system
import "image"
import "git.tebibyte.media/tomo/tomo/input"
// 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 +23,31 @@ 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) {
if key == input.KeyTab && this.modifiers.Alt {
caught := false
if this.anyFocused() {
this.keyboardTargets(func (target anyBox) bool {
if target.handleKeyDown(key, numberPad) {
caught = true
return false
}
return true
})
} else {
if this.root != nil {
caught = this.root.handleKeyDown(key, numberPad)
}
}
if caught { return }
switch key {
case input.KeyTab:
if this.modifiers.Shift {
this.focusPrevious()
} else {
this.focusNext()
}
} else if target := this.keyboardTarget(); target != nil {
target.handleKeyDown(key, numberPad)
case input.KeyUp: this.focusPrevious()
case input.KeyDown: this.focusNext()
}
}
@@ -35,63 +55,88 @@ 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.handleKeyDown(key, numberPad)
}
}
// HandleMouseDown sends a mouse down event to the Box positioned underneath the
// mouse cursor and marks it as being "dragged" by that mouse button. If the
// event which triggers this comes with mouse position information,
// HandleMouseMove must be called *before* HandleMouseDown.
func (this *Hierarchy) HandleMouseDown (button input.Button) {
underneath := this.boxUnder(this.mousePosition, eventCategoryMouse)
this.drags[button] = underneath
if underneath != nil {
underneath.handleMouseDown(button)
}
}
// HandleMouseUp sends a mouse up event to the Box currently being "dragged" by
// the specified mouse button, and marks it as being "not dragged" by that mouse
// button. If the event which triggers this comes with mouse position
// information, HandleMouseMove must be caleld *before* HandleMouseUp
func (this *Hierarchy) HandleMouseUp (button input.Button) {
dragging := this.drags[button]
this.drags[button] = nil
if dragging != nil {
dragging.handleMouseUp(button)
}
}
// HandleMouseMove sends a mouse move event to any Boxes currently being
// "dragged" by a mouse button. If none are, it sends the event to the Box which
// is underneath the mouse pointer.
func (this *Hierarchy) HandleMouseMove (position image.Point) {
if this.mousePosition == position { return }
this.mousePosition = position
handled := false
for _, child := range this.drags {
if child == nil { continue }
child.handleMouseMove()
handled = true
}
underneath := this.boxUnder(position, eventCategoryMouse)
if underneath != nil {
this.hover(underneath)
if !handled {
underneath.handleMouseMove()
if this.anyFocused() {
this.keyboardTargets(func (target anyBox) bool {
if target.handleKeyUp(key, numberPad) {
return false
}
return true
})
} else {
if this.root != nil {
this.root.handleKeyUp(key, numberPad)
}
}
}
// HandleScroll sends a scroll event to the Box currently underneath the mouse
// cursor.
func (this *Hierarchy) HandleScroll (x, y float64) {
underneath := this.boxUnder(this.mousePosition, eventCategoryScroll)
if underneath != nil {
underneath.handleScroll(x, y)
// 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) {
boxes := []anyBox { }
first := true
this.boxesUnder(this.mousePosition)(func (box anyBox) bool {
if first {
if box.canBeFocused() {
this.focus(box)
} else {
this.focus(nil)
}
first = false
}
boxes = append(boxes, box)
return !box.handleMouseDown(button)
})
this.drags[button] = boxes
}
// 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) {
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 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
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. or perhaps doing *this* is the better way? we may never know.
box := this.boxUnder(position)
if box != nil {
box := this.considerMaskingParents(box)
this.hover(box)
box.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.
func (this *Hierarchy) HandleScroll (x, y float64) {
this.boxesUnder(this.mousePosition)(func (box anyBox) bool {
return !box.handleScroll(x, y)
})
}

View File

@@ -4,6 +4,7 @@ import "image"
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/input"
import "git.tebibyte.media/tomo/tomo/canvas"
import "git.tebibyte.media/tomo/backend/style"
import "git.tebibyte.media/tomo/backend/internal/util"
// Hierarchy is coupled to a tomo.Window implementation, and manages a tree of
@@ -21,10 +22,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,10 +53,11 @@ 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]),
}
this.hierarchies.Add(hierarchy)
return hierarchy
}
@@ -85,7 +87,7 @@ func (this *Hierarchy) Empty () bool {
// draw to. The Hierarchy will use the canvas.Canvas's bounds to lay itself out.
func (this *Hierarchy) SetCanvas (can canvas.Canvas) {
this.canvas = can
if this.root != nil { this.root.loseCanvas() }
if this.root != nil { this.root.recursiveLoseCanvas() }
this.needRedo = true
}
@@ -94,6 +96,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 }
@@ -103,7 +115,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
@@ -115,8 +127,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()
@@ -135,6 +147,20 @@ func (this *Hierarchy) AfterEvent () {
}
}
// Close closes the Hierarchy. This should be called when the Window that
// contains it has been closed.
func (this *Hierarchy) Close () {
this.system.removeHierarchy(this)
}
func (this *Hierarchy) setStyle () {
if this.root != nil { this.root.recursiveReApply() }
}
func (this *Hierarchy) setIconSet () {
if this.root != nil { this.root.recursiveReApply() }
}
func (this *Hierarchy) getHierarchy () *Hierarchy {
return this
}
@@ -143,6 +169,26 @@ func (this *Hierarchy) getWindow () tomo.Window {
return this.link.GetWindow()
}
func (this *Hierarchy) getStyle () *style.Style {
return this.system.style
}
func (this *Hierarchy) getIconSet () style.IconSet {
return this.system.iconSet
}
func (this *Hierarchy) getFaceSet () style.FaceSet {
return this.system.faceSet
}
func (this *Hierarchy) getStyleNonce () int {
return this.system.styleNonce
}
func (this *Hierarchy) getIconSetNonce () int {
return this.system.iconSetNonce
}
func (this *Hierarchy) getCanvas () canvas.Canvas {
return this.canvas
}
@@ -159,8 +205,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) {
@@ -208,38 +254,68 @@ 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 box
}
return focused
func (this *Hierarchy) isMasked (box anyBox) bool {
parent := box.getParent()
for {
parentBox, ok := parent.(anyBox)
if !ok { break }
if parent.masks() {
return true
}
parent = parentBox.getParent()
}
return false
}
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() {
if box.canBeFocused() && !this.isMasked(box) {
// found it
this.focus(box)
focused = true
@@ -260,11 +336,11 @@ 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
}
if box.canBeFocused() { behind = box }
if box.canBeFocused() && !this.isMasked(box) { behind = box }
return true
})
this.focus(behind)
@@ -287,13 +363,25 @@ func (this *Hierarchy) drawBackgroundPart (canvas.Canvas) {
// if so, windows should be transparent if the color has transparency
}
// var minimumSizeCount = 0
func (this *Hierarchy) doMinimumSize () {
this.minimumClean = true
// println("doMinimumSize", minimumSizeCount)
// minimumSizeCount ++
previousMinimumSize := this.minimumSize
this.minimumSize = image.Point { }
if this.root != nil {
this.minimumSize = this.root.MinimumSize()
this.minimumSize = this.root.minimumSize()
}
this.link.NotifyMinimumSizeChange()
if previousMinimumSize != this.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,21 +35,27 @@ 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.
flushActionQueue ()
flushActionQueue ()
// recursiveRedo recursively recalculates the minimum size, layout, and
// re-paints this anyBox and all of its children.
recursiveRedo ()
recursiveRedo ()
// loseCanvas causes this anyBox and its children (if applicable) to
// lose their canvases and re-cut them as needed.
loseCanvas ()
recursiveLoseCanvas ()
// recursiveReAppply causes this anyBox and its children (if applicable)
// 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
@@ -67,12 +64,21 @@ 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)
// unsetAttr unsets an attribute at the user or style level depending
// on the value of user.
unsetAttr (kind tomo.AttrKind, 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
@@ -81,19 +87,28 @@ type anyBox interface {
propagate (func (anyBox) bool) bool
propagateAlt (func (anyBox) bool) bool
handleFocusEnter ()
handleFocusLeave ()
// handleDndEnter ()
// handleDndLeave ()
// handleDndDrop (data.Data)
handleMouseEnter ()
handleMouseLeave ()
handleMouseMove ()
handleMouseDown (input.Button)
handleMouseUp (input.Button)
handleScroll (float64, float64)
handleKeyDown (input.Key, bool)
handleKeyUp (input.Key, bool)
handleFocusEnter ()
handleFocusLeave ()
// handleDndEnter ()
// handleDndLeave ()
// 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
}
func assertAnyBox (unknown tomo.Box) anyBox {

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

@@ -0,0 +1,60 @@
package system
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/backend/style"
type styleApplicator struct {
style *style.Style
role tomo.Role
rules []style.Rule
currentSet style.AttrSet
}
func (this *styleApplicator) apply (box anyBox) {
if box.Role() != this.role {
this.role = box.Role()
// the role has changed, so re-cache the list of rules
this.rules = nil
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(style.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)
}
}
// reset an attribute if it is no longer specified
if this.currentSet != nil {
for kind := range this.currentSet {
_, exists := attrs[kind]
if !exists {
box.unsetAttr(kind, false)
}
}
}
// apply that list of attributes
this.currentSet = attrs
for _, attr := range attrs {
box.setAttr(attr, false)
}
}

View File

@@ -3,11 +3,21 @@ package system
import "io"
import "image"
import "git.tebibyte.media/tomo/tomo/canvas"
import "git.tebibyte.media/tomo/backend/style"
import "git.tebibyte.media/tomo/backend/internal/util"
// System is coupled to a tomo.Backend implementation, and manages Hierarchies
// and Boxes.
type System struct {
link BackendLink
style *style.Style
iconSet style.IconSet
faceSet style.FaceSet
styleNonce int
iconSetNonce int
hierarchies util.Set[*Hierarchy]
}
// BackendLink allows the System to call up into the tomo.Backend implementation
@@ -15,8 +25,6 @@ type System struct {
type BackendLink interface {
// NewTexture creates a new texture from an image.
NewTexture (image.Image) canvas.TextureCloser
// NewCanvas creates a new blank canvas with the specified bounds.
NewCanvas (image.Rectangle) canvas.Canvas
// NewSurface creates a new surface with the specified bounds.
NewSurface (image.Rectangle) (SurfaceLink, error)
}
@@ -32,6 +40,36 @@ type SurfaceLink interface {
// New creates a new System.
func New (link BackendLink) *System {
return &System {
link: link,
link: link,
hierarchies: make(util.Set[*Hierarchy]),
}
}
// SetStyle sets the style that is applied to objects, and notifies them
// that the style has changed.
func (this *System) SetStyle (style *style.Style) {
this.style = style
this.styleNonce ++
for hierarchy := range this.hierarchies {
hierarchy.setStyle()
}
}
// SetIconSet sets the icon set that provides icon textures, and notifies
// objects that the icons have changed.
func (this *System) SetIconSet (iconSet style.IconSet) {
this.iconSet = iconSet
this.iconSetNonce ++
for hierarchy := range this.hierarchies {
hierarchy.setIconSet()
}
}
// SetFaceSet sets the face set that provides font faces.
func (this *System) SetFaceSet (faceSet style.FaceSet) {
this.faceSet = faceSet
}
func (this *System) removeHierarchy (hierarchy *Hierarchy) {
delete(this.hierarchies, hierarchy)
}

View File

@@ -10,28 +10,30 @@ import "git.tebibyte.media/tomo/tomo/text"
import "git.tebibyte.media/tomo/tomo/input"
import "git.tebibyte.media/tomo/tomo/event"
import "git.tebibyte.media/tomo/tomo/canvas"
import "git.tebibyte.media/tomo/backend/internal/util"
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
runes []rune
selectable bool
selecting bool
selectStart int
dot text.Dot
dotColor color.Color
drawer typeset.Drawer
face util.Cycler[font.Face]
on struct {
contentBoundsChange event.FuncBroadcaster
@@ -40,20 +42,14 @@ 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)
box.attrTextColor.SetFallback(tomo.ATextColor(color.Black))
box.attrDotColor.SetFallback(tomo.ADotColor(color.RGBA { G: 255, B: 255, A: 255}))
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
@@ -65,35 +61,24 @@ func (this *textBox) ScrollTo (point image.Point) {
this.invalidateLayout()
}
func (this *textBox) RecommendedHeight (width int) int {
return this.drawer.ReccomendedHeightFor(width) + this.borderAndPaddingSum().Vertical()
}
func (this *textBox) RecommendedWidth (height int) int {
// TODO maybe not the best idea?
return this.minimumSize().X
}
func (this *textBox) OnContentBoundsChange (callback func()) event.Cookie {
return this.on.contentBoundsChange.Connect(callback)
}
func (this *textBox) SetText (text string) {
if this.text == text { return }
this.text = text
this.drawer.SetText([]rune(text))
this.invalidateMinimum()
this.invalidateLayout()
}
func (this *textBox) SetTextColor (c color.Color) {
if this.textColor == c { return }
this.textColor = c
this.invalidateDraw()
}
func (this *textBox) SetFace (face font.Face) {
if this.face == face { return }
this.face = face
this.drawer.SetFace(face)
this.invalidateMinimum()
this.invalidateLayout()
}
func (this *textBox) SetWrap (wrap bool) {
if this.wrap == wrap { return }
this.drawer.SetWrap(wrap)
this.text = text
this.runes = []rune(text)
this.drawer.SetText(this.runes)
this.invalidateMinimum()
this.invalidateLayout()
}
@@ -103,14 +88,6 @@ func (this *textBox) SetSelectable (selectable bool) {
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 }
@@ -129,21 +106,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 }
this.drawBorders(can)
pen := can.Pen()
pen.Fill(this.color)
pen.Texture(this.texture)
texture := this.attrTexture.Value().Texture
col := this.attrColor.Value().Color
this.drawBorders(can)
pen := can.Pen()
pen.Fill(col)
pen.Texture(texture)
if this.transparent() && this.parent != nil {
this.parent.drawBackgroundPart(can)
}
@@ -153,8 +128,96 @@ 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.face.Value() != nil {
textColor := this.attrTextColor.Value().Color
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.handleFaceChange()
}
case tomo.AttrWrap:
if this.attrWrap.Set(attr, user) {
this.drawer.SetWrap(bool(this.attrWrap.Value()))
this.invalidateMinimum()
this.invalidateLayout()
}
case tomo.AttrAlign:
if this.attrAlign.Set(attr, user) {
align := this.attrAlign.Value()
this.drawer.SetAlign (
typeset.Align(align.X),
typeset.Align(align.Y))
this.invalidateDraw()
}
case tomo.AttrOverflow:
if this.attrOverflow.Set(attr, user) {
this.invalidateMinimum()
this.invalidateLayout()
}
default: this.box.setAttr(attr, user)
}
}
func (this *textBox) unsetAttr (kind tomo.AttrKind, user bool) {
switch kind {
case tomo.AttrKindTextColor:
if this.attrTextColor.Unset(user) && !this.dot.Empty() {
this.invalidateDraw()
}
case tomo.AttrKindDotColor:
if this.attrDotColor.Unset(user) && !this.dot.Empty() {
this.invalidateDraw()
}
case tomo.AttrKindFace:
if this.attrFace.Unset(user) {
this.handleFaceChange()
}
case tomo.AttrKindWrap:
if this.attrWrap.Unset(user) {
this.drawer.SetWrap(bool(this.attrWrap.Value()))
this.invalidateMinimum()
this.invalidateLayout()
}
case tomo.AttrKindAlign:
if this.attrAlign.Unset(user) {
align := this.attrAlign.Value()
this.drawer.SetAlign (
typeset.Align(align.X),
typeset.Align(align.Y))
this.invalidateDraw()
}
case tomo.AttrKindOverflow:
if this.attrOverflow.Unset(user) {
this.invalidateMinimum()
this.invalidateLayout()
}
default: this.box.unsetAttr(kind, user)
}
}
func roundPt (point fixed.Point26_6) image.Point {
@@ -166,14 +229,16 @@ func fixPt (point image.Point) fixed.Point26_6 {
}
func (this *textBox) drawDot (can canvas.Canvas) {
if this.face == nil { return }
face := this.face.Value()
if face == nil { return }
textColor := this.attrTextColor.Value().Color
dotColor := this.attrDotColor.Value().Color
pen := can.Pen()
pen.Fill(color.Transparent)
pen.Stroke(this.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()))
@@ -183,11 +248,12 @@ func (this *textBox) drawDot (can canvas.Canvas) {
switch {
case dot.Empty():
pen.Stroke(textColor)
pen.StrokeWeight(1)
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)),
@@ -195,7 +261,7 @@ func (this *textBox) drawDot (can canvas.Canvas) {
})
default:
pen.Fill(this.dotColor)
pen.Fill(dotColor)
pen.StrokeWeight(0)
rect := image.Rectangle {
@@ -226,44 +292,121 @@ func (this *textBox) textOffset () image.Point {
Sub(this.drawer.LayoutBoundsSpace().Min)
}
func (this *textBox) handleFocusEnter () {
this.invalidateDraw()
this.box.handleFocusEnter()
}
func (this *textBox) handleFocusLeave () {
this.on.dotChange.Broadcast()
this.invalidateDraw()
this.box.handleFocusLeave()
}
func (this *textBox) handleMouseDown (button input.Button) {
if button == input.ButtonLeft {
func (this *textBox) handleMouseDown (button input.Button) bool {
if this.mouseButtonCanDrag(button) {
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) {
if button == input.ButtonLeft && this.selecting {
func (this *textBox) handleMouseUp (button input.Button) bool {
if this.mouseButtonCanDrag(button) && 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) mouseButtonCanDrag (button input.Button) bool {
return button == input.ButtonLeft ||
button == input.ButtonMiddle ||
button == input.ButtonRight
}
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))
}
func (this *textBox) handleKeyDown (key input.Key, numberPad bool) bool {
if this.box.handleKeyDown(key, numberPad) { return true }
if !this.selectable { return false }
// because fuck you thats why!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
modifiers := this.Window().Modifiers()
dot := this.Dot()
sel := modifiers.Shift
word := modifiers.Control
switch {
case key == input.KeyHome || (modifiers.Alt && key == input.KeyLeft):
dot.End = 0
if !sel { dot.Start = dot.End }
this.Select(dot)
return true
case key == input.KeyEnd || (modifiers.Alt && key == input.KeyRight):
dot.End = len(this.text)
if !sel { dot.Start = dot.End }
this.Select(dot)
return true
case key == input.KeyLeft:
if sel {
this.Select(text.SelectLeft(this.runes, dot, word))
} else {
this.Select(text.MoveLeft(this.runes, dot, word))
}
return true
case key == input.KeyRight:
if sel {
this.Select(text.SelectRight(this.runes, dot, word))
} else {
this.Select(text.MoveRight(this.runes, dot, word))
}
return true
case key == input.Key('a') && modifiers.Control:
dot.Start = 0
dot.End = len(this.text)
this.Select(dot)
return true
default:
return false
}
}
func (this *textBox) handleKeyUp (key input.Key, numberPad bool) bool {
if this.box.handleKeyUp(key, numberPad) { return true }
if !this.selectable { return false }
modifiers := this.Window().Modifiers()
switch {
case key == input.KeyHome || (modifiers.Alt && key == input.KeyLeft):
return true
case key == input.KeyEnd || (modifiers.Alt && key == input.KeyRight):
return true
case key == input.KeyLeft:
return true
case key == input.KeyRight:
return true
case key == input.Key('a') && modifiers.Control:
return true
default:
return false
}
}
func (this *textBox) normalizedLayoutBoundsSpace () image.Rectangle {
bounds := this.drawer.LayoutBoundsSpace()
return bounds.Sub(bounds.Min)
@@ -272,10 +415,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()
}
@@ -347,3 +490,36 @@ func (this *textBox) scrollToDot () {
this.ScrollTo(scroll)
}
func (this *textBox) handleFaceChange () {
hierarchy := this.getHierarchy()
if hierarchy == nil { return }
faceSet := hierarchy.getFaceSet()
if faceSet == nil { return }
face := faceSet.Face(tomo.Face(this.attrFace.Value()))
this.face.Set(face, face)
this.drawer.SetFace(face)
this.invalidateMinimum()
this.invalidateLayout()
}
func (this *textBox) recursiveReApply () {
this.box.recursiveReApply()
hierarchy := this.getHierarchy()
if hierarchy == nil { return }
previousFace := this.face.Value()
if previousFace == nil {
faceSet := hierarchy.getFaceSet()
if faceSet == nil { return }
face := faceSet.Face(tomo.Face(this.attrFace.Value()))
if face != previousFace {
this.face.Set(face, face)
this.drawer.SetFace(face)
this.invalidateMinimum()
this.invalidateLayout()
}
}
}

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
@@ -82,6 +83,7 @@ func NewMemo[T any] (update func () T) Memo[T] {
func (this *Memo[T]) Value () T {
if !this.valid {
this.cache = this.update()
this.valid = true
}
return this.cache
}
@@ -89,13 +91,66 @@ func (this *Memo[T]) Value () T {
// Invalidate marks the Memo's value as invalid, which will cause it to be
// updated the next time Value is called.
func (this *Memo[T]) Invalidate () {
var zero T
this.cache = zero
this.valid = false
}
// InvalidateTo invalidates the Memo and sets its value. The new value will be
// entirely inaccessible. This is only intended to be used for setting a
// reference to nil
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
}
// Optional is an optional value.
type Optional[T any] struct {
value T
exists bool
}
// Value returns the value and true if the value exists. If not, it returns the
// last set value and false.
func (this *Optional[T]) Value () (T, bool) {
return this.value, this.exists
}
// Set sets the value.
func (this *Optional[T]) Set (value T) {
this.value = value
this.exists = true
}
// Unset unsets the value.
func (this *Optional[T]) Unset () {
var zero T
this.value = zero
this.exists = false
}
// Exists returns if the value is currently set.
func (this *Optional[T]) Exists () bool {
return this.exists
}

13
style/faceset.go Normal file
View File

@@ -0,0 +1,13 @@
package style
import "golang.org/x/image/font"
import "git.tebibyte.media/tomo/tomo"
// FaceSet holds a set of font faces.
type FaceSet interface {
// Face returns the font face which most closely matches the given
// tomo.Face. The face must be closed when it is done being used. If no
// suitable face could be found, This behavior must return a fallback
// face (such as basicfont.Face7x13) instead of nil.
Face (tomo.Face) font.Face
}

28
style/iconset.go Normal file
View File

@@ -0,0 +1,28 @@
package style
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/data"
import "git.tebibyte.media/tomo/tomo/canvas"
// IconSet holds a set of icon textures.
type IconSet interface {
// A word on textures:
//
// Because textures can be linked to some resource that is outside of
// the control of Go's garbage collector, methods of IconSet must not
// allocate new copies of a texture each time they are called. It is
// fine to lazily load textures and save them for later use, but the
// same texture must never be allocated multiple times as this could
// cause a memory leak.
//
// As such, textures returned by these methods must be protected.
// Icon returns a texture of the corresponding icon ID. If there is no
// suitable option, it should return nil.
Icon (tomo.Icon, tomo.IconSize) canvas.Texture
// MimeIcon returns a texture of an icon corresponding to a MIME type.
// If there is no suitable specific option, it should return a more
// generic icon or a plain file icon.
MimeIcon (data.Mime, tomo.IconSize) canvas.Texture
}

70
style/style.go Normal file
View File

@@ -0,0 +1,70 @@
package style
import "image/color"
import "git.tebibyte.media/tomo/tomo"
// Style can apply a visual style to different objects.
type Style struct {
// Rules determines which styles get applied to which Objects.
Rules []Rule
// Colors maps tomo.Color values to color.RGBA values.
Colors map[tomo.Color] color.Color
}
// Rule describes under what circumstances should certain style attributes be
// active.
type Rule struct {
Role tomo.Role
Tags []string
Set AttrSet
}
// Ru is shorthand for creating a rule structure
func Ru (set AttrSet, role tomo.Role, tags ...string) Rule {
return Rule {
Role: role,
Tags: tags,
Set: set,
}
}
// AttrSet is a set of attributes wherein only one/zero of each attribute type
// can exist. It is keyed by the AttrKind of each attribute and must not be
// modified directly.
type AttrSet map[tomo.AttrKind] tomo.Attr
// AS builds an AttrSet out of a vararg list of Attr values. If multiple Attrs
// of the same kind are specified, the last one will override the others.
func AS (attrs ...tomo.Attr) AttrSet {
set := AttrSet { }
set.Add(attrs...)
return set
}
// Add adds attributes to the set.
func (this AttrSet) Add (attrs ...tomo.Attr) {
for _, attr := range attrs {
this[attr.Kind()] = attr
}
}
// MergeUnder takes attributes from another set and adds them if they don't
// already exist in this one.
func (this AttrSet) MergeUnder (other AttrSet) {
if other == nil { return }
for _, attr := range other {
if _, exists := this[attr.Kind()]; !exists {
this.Add(attr)
}
}
}
// MergeOver takes attributes from another set and adds them, overriding this
// one.
func (this AttrSet) MergeOver (other AttrSet) {
if other == nil { return }
for _, attr := range other {
this.Add(attr)
}
}

View File

@@ -4,7 +4,9 @@ import "image"
import "errors"
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/xgbkb"
import "git.tebibyte.media/tomo/tomo/data"
import "git.tebibyte.media/tomo/tomo/canvas"
import "git.tebibyte.media/tomo/backend/style"
import "git.tebibyte.media/tomo/backend/x/canvas"
import "git.tebibyte.media/tomo/backend/internal/system"
@@ -18,6 +20,9 @@ type Backend struct {
x *xgbutil.XUtil
system *system.System
style *style.Style
iconSet style.IconSet
doChannel chan func()
windows map[xproto.Window] *window
open bool
@@ -31,10 +36,6 @@ func (this *backendLink) NewTexture (source image.Image) canvas.TextureCloser {
return this.backend.NewTexture(source)
}
func (this *backendLink) NewCanvas (bounds image.Rectangle) canvas.Canvas {
return this.backend.NewCanvas(bounds)
}
func (this *backendLink) NewSurface (bounds image.Rectangle) (system.SurfaceLink, error) {
// TODO
return nil, errors.New("x: not implemented")
@@ -106,30 +107,59 @@ func (this *Backend) NewBox () tomo.Box {
return this.system.NewBox()
}
func (this *Backend) NewCanvasBox () tomo.CanvasBox {
return this.system.NewCanvasBox()
func (this *Backend) NewTextBox () tomo.TextBox {
return this.system.NewTextBox()
}
func (this *Backend) NewContainerBox () tomo.ContainerBox {
return this.system.NewContainerBox()
func (this *Backend) NewCanvasBox () tomo.CanvasBox {
return this.system.NewCanvasBox()
}
func (this *Backend) NewSurfaceBox () (tomo.SurfaceBox, error) {
return this.system.NewSurfaceBox()
}
func (this *Backend) NewTextBox () tomo.TextBox {
return this.system.NewTextBox()
func (this *Backend) NewContainerBox () tomo.ContainerBox {
return this.system.NewContainerBox()
}
func (this *Backend) NewTexture (source image.Image) canvas.TextureCloser {
return xcanvas.NewTextureFrom(source)
}
func (this *Backend) NewCanvas (bounds image.Rectangle) canvas.Canvas {
func (this *Backend) NewCanvas (bounds image.Rectangle) canvas.CanvasCloser {
return xcanvas.NewCanvas(this.x, bounds)
}
func (this *Backend) ColorRGBA (id tomo.Color) (r, g, b, a uint32) {
if col, ok := this.style.Colors[id]; ok {
return col.RGBA()
}
return 0xFFFF, 0, 0xFFFF, 0xFFFF // punish bad styles
}
func (this *Backend) IconTexture (id tomo.Icon, size tomo.IconSize) canvas.Texture {
return this.iconSet.Icon(id, size)
}
func (this *Backend) MimeIconTexture (mime data.Mime, size tomo.IconSize) canvas.Texture {
return this.iconSet.MimeIcon(mime, size)
}
func (this *Backend) SetStyle (style *style.Style) {
this.style = style
this.system.SetStyle(style)
}
func (this *Backend) SetIconSet (iconSet style.IconSet) {
this.iconSet = iconSet
this.system.SetIconSet(iconSet)
}
func (this *Backend) SetFaceSet (faceSet style.FaceSet) {
this.system.SetFaceSet(faceSet)
}
func (this *Backend) assert () {
if this == nil { panic("x: nil backend") }
}

View File

@@ -49,9 +49,10 @@ func (this *Canvas) Push (window xproto.Window) {
}
// Close frees this canvas from the X server.
func (this *Canvas) Close () {
func (this *Canvas) Close () error {
this.assert()
this.Image.Destroy()
return nil
}
func (this *Canvas) assert () {
@@ -131,3 +132,32 @@ func convertColor (c color.Color) xgraphics.BGRA {
A: uint8(a >> 8),
}
}
// For some reason, xgraphics.BGRA does not specify whether or not it uses
// premultiplied alpha, and information regarding this is contradictory.
// Basically:
// - BGRAModel just takes the result of c.RGBA and bit shifts it, without
// un-doing the aplha premultiplication that is required by Color.RGBA,
// suggesting that xgraphics.BGRA stores alpha-premultiplied color.
// - xgraphics.BlendBGRA lerps between dest and src using only the alpha of
// src (temporarily converting the colors to fucking floats for some reason)
// which seems to suggest that xgraphics.BGRA *does not* store alpha-
// premultiplied color.
// There is no issues page on xgbutil so we may never get an answer to this
// question. However, in this package we just use xgraphics.BGRA to store alpha-
// premultiplied color anyway because its way faster, and I would sooner eat
// spaghetti with a spoon than convert to and from float64 to blend pixels.
func blendPremultipliedBGRA (dst, src xgraphics.BGRA) xgraphics.BGRA {
// https://en.wikipedia.org/wiki/Alpha_compositing
return xgraphics.BGRA {
B: blendPremultipliedChannel(dst.B, src.B, src.A),
G: blendPremultipliedChannel(dst.G, src.G, src.A),
R: blendPremultipliedChannel(dst.R, src.R, src.A),
A: blendPremultipliedChannel(dst.A, src.A, src.A),
}
}
func blendPremultipliedChannel (dst, src, a uint8) uint8 {
dst16, src16, a16 := uint16(dst), uint16(src), uint16(a)
return uint8(src16 + ((dst16 * (255 - a16)) >> 8))
}

View File

@@ -1,6 +1,7 @@
package xcanvas
import "sort"
import "math"
import "image"
import "github.com/jezek/xgbutil/xgraphics"
@@ -45,7 +46,7 @@ func (this *pen) textureRectangleTransparent (bounds image.Rectangle) {
srcPos := pos.Add(offset)
dstIndex := this.image.PixOffset(pos.X, pos.Y)
srcIndex := this.texture.PixOffset(srcPos.X, srcPos.Y)
pixel := xgraphics.BlendBGRA(xgraphics.BGRA {
pixel := blendPremultipliedBGRA(xgraphics.BGRA {
B: dst[dstIndex + 0],
G: dst[dstIndex + 1],
R: dst[dstIndex + 2],
@@ -92,7 +93,7 @@ func (this *pen) fillRectangleTransparent (c xgraphics.BGRA, bounds image.Rectan
for pos.Y = bounds.Min.Y; pos.Y < bounds.Max.Y; pos.Y ++ {
for pos.X = bounds.Min.X; pos.X < bounds.Max.X; pos.X ++ {
index := this.image.PixOffset(pos.X, pos.Y)
pixel := xgraphics.BlendBGRA(xgraphics.BGRA {
pixel := blendPremultipliedBGRA(xgraphics.BGRA {
B: this.image.Pix[index + 0],
G: this.image.Pix[index + 1],
R: this.image.Pix[index + 2],
@@ -158,12 +159,12 @@ func (this *pen) fillPolygon (c xgraphics.BGRA, points ...image.Point) {
area = this.image.Bounds().Intersect(area)
if area.Empty() { return }
boundaries := make([]int, len(points))
context := fillingContext {
image: this.image,
color: this.fill,
min: area.Min.X,
max: area.Max.X,
boundaries: make([]int, len(points)),
points: points,
}
@@ -181,19 +182,19 @@ func (this *pen) fillPolygon (c xgraphics.BGRA, points ...image.Point) {
(fPointY < fy && fPrevY >= fy) ||
(fPrevY < fy && fPointY >= fy)
if addboundary {
context.boundaries[boundaryCount] = int (
boundaries[boundaryCount] = int(math.Round (
fPointX +
(fy - fPointY) /
(fPrevY - fPointY) *
(fPrevX - fPointX))
(fPrevX - fPointX)))
boundaryCount ++
}
prevPoint = point
}
// sort boundary list
cutBoundaries := context.boundaries[:boundaryCount]
sort.Ints(cutBoundaries)
context.boundaries = boundaries[:boundaryCount]
sort.Ints(context.boundaries)
// fill pixels between boundary pairs
if c.A == 255 {
@@ -215,7 +216,7 @@ type fillingContext struct {
}
func (context *fillingContext) fillPolygonHotOpaque () {
for index := 0; index < len(context.boundaries); index += 2 {
for index := 0; index < len(context.boundaries) - 1; index += 2 {
left := context.boundaries[index]
right := context.boundaries[index + 1]
@@ -240,7 +241,7 @@ func (context *fillingContext) fillPolygonHotOpaque () {
}
func (context *fillingContext) fillPolygonHotTransparent () {
for index := 0; index < len(context.boundaries); index += 2 {
for index := 0; index < len(context.boundaries) - 1; index += 2 {
left := context.boundaries[index]
right := context.boundaries[index + 1]
@@ -255,7 +256,7 @@ func (context *fillingContext) fillPolygonHotTransparent () {
// fill pixels in between
for x := left; x < right; x ++ {
index := context.image.PixOffset(x, context.y)
pixel := xgraphics.BlendBGRA(xgraphics.BGRA {
pixel := blendPremultipliedBGRA(xgraphics.BGRA {
B: context.image.Pix[index + 0],
G: context.image.Pix[index + 1],
R: context.image.Pix[index + 2],

View File

@@ -32,7 +32,7 @@ func (context plottingContext) plot (center image.Point) {
for y := square.Min.Y; y < square.Max.Y; y ++ {
for x := square.Min.X; x < square.Max.X; x ++ {
index := context.image.PixOffset(x, y)
pixel := xgraphics.BlendBGRA(xgraphics.BGRA {
pixel := blendPremultipliedBGRA(xgraphics.BGRA {
B: context.image.Pix[index + 0],
G: context.image.Pix[index + 1],
R: context.image.Pix[index + 2],

View File

@@ -13,7 +13,7 @@ type scrollSum struct {
}
// TODO: this needs to be configurable, we need a config api
const scrollDistance = 16
const scrollDistance = 32
func (sum *scrollSum) add (button xproto.Button, window *window, state uint16) {
if xgbkb.StateToModifiers(state).Shift {

View File

@@ -4,6 +4,7 @@ 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"
@@ -18,7 +19,6 @@ import "github.com/jezek/xgbutil/keybind"
import "github.com/jezek/xgbutil/mousebind"
import "github.com/jezek/xgbutil/xgraphics"
type mainWindow struct { *window }
type window struct {
backend *Backend
hierarchy *system.Hierarchy
@@ -28,10 +28,13 @@ type window struct {
title string
leader *window
modalParent *window
hasModal bool
shy bool
visible bool
resizeX bool
resizeY bool
metrics struct {
bounds image.Rectangle
@@ -63,28 +66,24 @@ func (this *windowLink) NotifyMinimumSizeChange () {
func (this *Backend) NewWindow (
bounds image.Rectangle,
) (
output tomo.MainWindow,
output tomo.Window,
err error,
) {
this.assert()
window, err := this.newWindow(bounds, false)
output = mainWindow { window: window }
return output, err
return this.newWindow(bounds, false)
}
func (this *Backend) NewPlainWindow (
bounds image.Rectangle,
) (
output tomo.MainWindow,
output tomo.Window,
err error,
) {
this.assert()
window, err := this.newWindow(bounds, false)
window.setType("dock")
output = mainWindow { window: window }
return output, err
return window, err
}
func (this *Backend) newWindow (
@@ -100,6 +99,9 @@ func (this *Backend) newWindow (
window := &window { backend: this }
link := &windowLink { window: window }
window.hierarchy = this.system.NewHierarchy(link)
window.leader = window
window.resizeX = true
window.resizeY = true
window.xWindow, err = xwindow.Generate(this.x)
if err != nil { return }
@@ -178,12 +180,21 @@ func (this *window) SetTitle (title string) {
icccm.WmIconNameSet (this.backend.x, this.xWindow.Id, title)
}
func (this *window) SetIcon (sizes ...image.Image) {
func (this *window) SetIcon (icon tomo.Icon) {
textures := []canvas.Texture {
icon.Texture(tomo.IconSizeSmall),
icon.Texture(tomo.IconSizeMedium),
icon.Texture(tomo.IconSizeLarge),
}
wmIcons := []ewmh.WmIcon { }
for _, icon := range sizes {
width := icon.Bounds().Max.X
height := icon.Bounds().Max.Y
for _, icon := range textures {
icon := xcanvas.AssertTexture(icon)
bounds := icon.Bounds()
width := bounds.Dx()
height := bounds.Dy()
wmIcon := ewmh.WmIcon {
Width: uint(width),
Height: uint(height),
@@ -193,18 +204,14 @@ func (this *window) SetIcon (sizes ...image.Image) {
// manually convert image data beacuse of course we have to do
// this
index := 0
for y := 0; y < height; y ++ {
for x := 0; x < width; x ++ {
r, g, b, a := icon.At(x, y).RGBA()
r >>= 8
g >>= 8
b >>= 8
a >>= 8
for y := bounds.Min.Y; y < bounds.Max.Y; y ++ {
for x := bounds.Min.X; x < bounds.Max.X; x ++ {
pixel := icon.BGRAAt(x, y)
wmIcon.Data[index] =
(uint(a) << 24) |
(uint(r) << 16) |
(uint(g) << 8) |
(uint(b) << 0)
(uint(pixel.A) << 24) |
(uint(pixel.R) << 16) |
(uint(pixel.G) << 8) |
(uint(pixel.B) << 0)
index ++
}}
@@ -217,6 +224,40 @@ func (this *window) SetIcon (sizes ...image.Image) {
wmIcons)
}
func (this *window) SetResizable (x, y bool) {
if this.resizeX == x && this.resizeY == y { return }
this.resizeX = x
this.resizeY = y
this.doMinimumSize()
}
func (this *window) SetBounds (bounds image.Rectangle) {
this.xWindow.WMMoveResize (
bounds.Min.X, bounds.Min.Y,
bounds.Min.X + bounds.Dx(),
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)
@@ -247,24 +288,12 @@ func (this *window) NewModal (bounds image.Rectangle) (tomo.Window, error) {
return modal, err
}
func (this mainWindow) NewChild (bounds image.Rectangle) (tomo.Window, error) {
child, err := this.backend.newWindow (
bounds.Add(this.metrics.bounds.Min), false)
if err != nil { return nil, err }
child.setClientLeader(this.window)
this.setClientLeader(this.window)
icccm.WmTransientForSet (
this.backend.x,
this.xWindow.Id,
this.xWindow.Id)
this.setType("UTILITY")
// this.inheritProperties(this.window)
return this, err
func (this *window) Modifiers () input.Modifiers {
return this.hierarchy.Modifiers()
}
func (this *window) Widget () (tomo.Window, error) {
// TODO
return nil, nil
func (this *window) MousePosition () image.Point {
return this.hierarchy.MousePosition()
}
func (this *window) Copy (data.Data) {
@@ -306,6 +335,7 @@ func (this *window) Close () {
this.SetRoot(nil)
delete(this.backend.windows, this.xWindow.Id)
this.xWindow.Destroy()
this.hierarchy.Close()
}
func (this *window) OnClose (callback func ()) event.Cookie {
@@ -361,7 +391,7 @@ func (this *window) reallocateCanvas () {
if larger || smaller {
if this.xCanvas != nil {
this.xCanvas.Destroy()
this.xCanvas.Close()
}
this.xCanvas = xcanvas.NewCanvasFrom(xgraphics.New (
this.backend.x,
@@ -404,14 +434,30 @@ func (this *window) doMinimumSize () {
if size.X < 8 { size.X = 8 }
if size.Y < 8 { size.Y = 8 }
hints := icccm.NormalHints {
Flags: icccm.SizeHintPMinSize,
MinWidth: uint(size.X),
MinHeight: uint(size.Y),
// now you can tell your friends that the max size of a Tomo
// window under X when one of the dimensions is constrained is
// 99999999999
MaxWidth: uint(99999999999),
MaxHeight: uint(99999999999),
}
if !this.resizeX {
hints.Flags |= icccm.SizeHintPMaxSize
hints.MaxWidth = uint(size.X)
}
if !this.resizeY {
hints.Flags |= icccm.SizeHintPMaxSize
hints.MaxHeight = uint(size.Y)
}
icccm.WmNormalHintsSet (
this.backend.x,
this.xWindow.Id,
&icccm.NormalHints {
Flags: icccm.SizeHintPMinSize,
MinWidth: uint(size.X),
MinHeight: uint(size.Y),
})
&hints)
newWidth := this.metrics.bounds.Dx()
newHeight := this.metrics.bounds.Dy()
if newWidth < size.X { newWidth = size.X }