Compare commits

...

86 Commits
v0.3.0 ... main

Author SHA1 Message Date
6d071bd1d3 ContainerBox uses slices.Remove correctly 2024-09-12 03:04:45 -04:00
dfc5c4514a Fix window type for tear-off menus 2024-09-12 02:53:12 -04:00
51afca6a3f Fix menu windows not being override redirect 2024-09-12 02:51:20 -04:00
b58932f02b Fix x/window.go 2024-09-12 02:07:57 -04:00
ee64650c19 Fix x/event.go 2024-09-12 02:06:16 -04:00
55215dedc2 Select all in textBox uses the length of the rune slice 2024-09-12 01:15:17 -04:00
eb98d143db Fix textBox 2024-09-12 01:15:01 -04:00
42deb40c2d internal/system/event.go uses key chords in tomo/config 2024-09-12 01:10:52 -04:00
b6850ee702 Update Tomo API 2024-09-12 01:05:32 -04:00
9d67013e33 Replace most functionality in internal/util with goutil and slices 2024-09-11 00:08:21 -04:00
0a8bb39265 Fix unreachable code in textBox 2024-09-10 23:55:38 -04:00
b92308fc80 Textbox does not trigger a DotChange event when using Select
The convention within Tomo is that On*Change events only fire when
the user interacts with something
2024-09-05 23:56:11 -04:00
6f9eca99e7 Text cursor is now an I beam 2024-09-05 16:46:49 -04:00
381f5f88bd Ctrl+Home/End go to the start and end of the box respectively 2024-09-04 12:55:37 -04:00
1c38ed2d87 Add soft line home 2024-09-04 12:40:46 -04:00
9079aca993 Fix segfault in lineHome 2024-09-04 12:30:36 -04:00
70d6759884 Add basic line home/end 2024-09-04 12:27:20 -04:00
2f828b1ae8 Add up/down keynav
Paragraph jumping could be better, but that can be refined later.
Progress on #10
2024-09-04 01:36:31 -04:00
c1c0d2125d Update TypeSet 2024-09-03 21:49:57 -04:00
3b4ab56914 Fix child boxes rendering on top of borders
Closes #4
2024-08-23 15:57:53 -04:00
e7f16645eb Unset all attributes when the style changes
Progress on #4
2024-08-23 12:32:46 -04:00
ccbbb735fd Update Tomo API 2024-08-16 17:58:38 -04:00
ab6bdeaba3 Add Bounds, InnerBounds to x.Window 2024-08-16 17:25:17 -04:00
93d7eed21f Update Tomo API 2024-08-16 17:15:01 -04:00
b18f747f0c Fix tag setting not invalidating style 2024-08-14 19:59:53 -04:00
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
23 changed files with 1943 additions and 762 deletions

9
go.mod
View File

@ -1,10 +1,13 @@
module git.tebibyte.media/tomo/backend module git.tebibyte.media/tomo/backend
go 1.20 go 1.21.0
toolchain go1.22.2
require ( require (
git.tebibyte.media/tomo/tomo v0.36.0 git.tebibyte.media/sashakoshka/goutil v0.3.0
git.tebibyte.media/tomo/typeset v0.7.1 git.tebibyte.media/tomo/tomo v0.48.0
git.tebibyte.media/tomo/typeset v0.8.0
git.tebibyte.media/tomo/xgbkb v1.0.1 git.tebibyte.media/tomo/xgbkb v1.0.1
github.com/jezek/xgb v1.1.1 github.com/jezek/xgb v1.1.1
github.com/jezek/xgbutil v0.0.0-20231116234834-47f30c120111 github.com/jezek/xgbutil v0.0.0-20231116234834-47f30c120111

10
go.sum
View File

@ -1,8 +1,10 @@
git.tebibyte.media/sashakoshka/goutil v0.3.0 h1:dcZ/9/or7m8eTpf2B1Pu4CscplXh2INTXFartz+ExwE=
git.tebibyte.media/sashakoshka/goutil v0.3.0/go.mod h1:e1OXLa+wX7x/F8n8gyxz2hnfVCEkWzGrZNX8/k/lR/M=
git.tebibyte.media/sashakoshka/xgbkb v1.0.0/go.mod h1:pNcE6TRO93vHd6q42SdwLSTTj25L0Yzggz7yLe0JV6Q= git.tebibyte.media/sashakoshka/xgbkb v1.0.0/go.mod h1:pNcE6TRO93vHd6q42SdwLSTTj25L0Yzggz7yLe0JV6Q=
git.tebibyte.media/tomo/tomo v0.36.0 h1:V9vyPYb4kpUceBhcDF/XyLDACzE5lY8kYEGHAkIsqs0= git.tebibyte.media/tomo/tomo v0.48.0 h1:AE21ElHwUSPsX82ZWCnoNxJFi9Oswyd3dPDPMbxTueQ=
git.tebibyte.media/tomo/tomo v0.36.0/go.mod h1:C9EzepS9wjkTJjnZaPBh22YvVPyA4hbBAJVU20Rdmps= git.tebibyte.media/tomo/tomo v0.48.0/go.mod h1:WrtilgKB1y8O2Yu7X4mYcRiqOlPR8NuUnoA/ynkQWrs=
git.tebibyte.media/tomo/typeset v0.7.1 h1:aZrsHwCG5ZB4f5CruRFsxLv5ezJUCFUFsQJJso2sXQ8= git.tebibyte.media/tomo/typeset v0.8.0 h1:4qA6oW4/3oPHj6/Zrp+JFJ53OmFSDvxs+J6BhO3DW00=
git.tebibyte.media/tomo/typeset v0.7.1/go.mod h1:PwDpSdBF3l/EzoIsa2ME7QffVVajnTHZN6l3MHEGe1g= git.tebibyte.media/tomo/typeset v0.8.0/go.mod h1:PwDpSdBF3l/EzoIsa2ME7QffVVajnTHZN6l3MHEGe1g=
git.tebibyte.media/tomo/xgbkb v1.0.1 h1:b3HDUopjdQp1MZrb5Vpil4bOtk3NnNXtfQW27Blw2kE= git.tebibyte.media/tomo/xgbkb v1.0.1 h1:b3HDUopjdQp1MZrb5Vpil4bOtk3NnNXtfQW27Blw2kE=
git.tebibyte.media/tomo/xgbkb v1.0.1/go.mod h1:P5Du0yo5hUsojchW08t+Mds0XPIJXwMi733ZfklzjRw= git.tebibyte.media/tomo/xgbkb v1.0.1/go.mod h1:P5Du0yo5hUsojchW08t+Mds0XPIJXwMi733ZfklzjRw=
github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298 h1:1qlsVAQJXZHsaM8b6OLVo6muQUQd4CwkH/D3fnnbHXA= github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298 h1:1qlsVAQJXZHsaM8b6OLVo6muQUQd4CwkH/D3fnnbHXA=

View File

@ -0,0 +1,70 @@
package system
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/sashakoshka/goutil/container"
type attrHierarchy [T tomo.Attr] struct {
fallback T
style ucontainer.Optional[T]
user ucontainer.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

@ -7,85 +7,92 @@ import "git.tebibyte.media/tomo/tomo/data"
import "git.tebibyte.media/tomo/tomo/input" import "git.tebibyte.media/tomo/tomo/input"
import "git.tebibyte.media/tomo/tomo/event" import "git.tebibyte.media/tomo/tomo/event"
import "git.tebibyte.media/tomo/tomo/canvas" import "git.tebibyte.media/tomo/tomo/canvas"
import "git.tebibyte.media/tomo/backend/internal/util" import "git.tebibyte.media/sashakoshka/goutil/container"
import "git.tebibyte.media/sashakoshka/goutil/image/color"
type textureMode int; const (
textureModeTile textureMode = iota
textureModeCenter
)
type box struct { type box struct {
system *System system *System
parent parent parent parent
outer anyBox outer anyBox
role tomo.Role tags ucontainer.Set[string]
role tomo.Role
lastStyleNonce int
lastIconSetNonce int
styleApplicator *styleApplicator
minSize ucontainer.Memo[image.Point]
bounds image.Rectangle bounds image.Rectangle
minSize image.Point
userMinSize image.Point
innerClippingBounds image.Rectangle innerClippingBounds image.Rectangle
minSizeQueued bool focusQueued *bool
focusQueued *bool
padding tomo.Inset attrColor attrHierarchy[tomo.AttrColor]
border []tomo.Border attrTexture attrHierarchy[tomo.AttrTexture]
color color.Color attrTextureMode attrHierarchy[tomo.AttrTextureMode]
texture canvas.Texture attrBorder attrHierarchy[tomo.AttrBorder]
textureMode textureMode attrMinimumSize attrHierarchy[tomo.AttrMinimumSize]
attrPadding attrHierarchy[tomo.AttrPadding]
dndData data.Data dndData data.Data
dndAccept []data.Mime dndAccept []data.Mime
focused bool
focusable bool focusable bool
canvas util.Memo[canvas.Canvas] hovered bool
focused bool
pressed bool
canvas ucontainer.Memo[canvas.Canvas]
drawer canvas.Drawer drawer canvas.Drawer
on struct { on struct {
focusEnter event.FuncBroadcaster focusEnter event.FuncBroadcaster
focusLeave event.FuncBroadcaster focusLeave event.FuncBroadcaster
dndEnter event.FuncBroadcaster dndEnter event.FuncBroadcaster
dndLeave event.FuncBroadcaster dndLeave event.FuncBroadcaster
dndDrop event.Broadcaster[func (data.Data)] dndDrop event.Broadcaster[func (data.Data)]
mouseEnter event.FuncBroadcaster mouseEnter event.FuncBroadcaster
mouseLeave event.FuncBroadcaster mouseLeave event.FuncBroadcaster
mouseMove event.FuncBroadcaster mouseMove event.Broadcaster[func () bool]
mouseDown event.Broadcaster[func (input.Button)] buttonDown event.Broadcaster[func (input.Button) bool]
mouseUp event.Broadcaster[func (input.Button)] buttonUp event.Broadcaster[func (input.Button) bool]
scroll event.Broadcaster[func (float64, float64)] scroll event.Broadcaster[func (float64, float64) bool]
keyDown event.Broadcaster[func (input.Key, bool)] keyDown event.Broadcaster[func (input.Key, bool) bool]
keyUp event.Broadcaster[func (input.Key, bool)] keyUp event.Broadcaster[func (input.Key, bool) bool]
styleChange event.FuncBroadcaster
iconSetChange event.FuncBroadcaster
} }
} }
func (this *System) newBox (outer anyBox) *box { func (this *System) newBox (outer anyBox) *box {
box := &box { box := &box {
system: this, system: this,
color: color.Transparent,
outer: outer, outer: outer,
drawer: outer, drawer: outer,
tags: make(ucontainer.Set[string]),
} }
box.canvas = util.NewMemo (func () canvas.Canvas { box.attrColor.SetFallback(tomo.AColor(color.Transparent))
box.canvas = ucontainer.NewMemo (func () canvas.Canvas {
if box.parent == nil { return nil } if box.parent == nil { return nil }
parentCanvas := box.parent.getCanvas() parentCanvas := box.parent.getCanvas()
if parentCanvas == nil { return nil } if parentCanvas == nil { return nil }
return parentCanvas.SubCanvas(box.bounds) drawableArea := box.bounds.Intersect(box.parent.getInnerClippingBounds())
return parentCanvas.SubCanvas(drawableArea)
}) })
if outer == nil { if outer == nil {
box.drawer = box box.drawer = box
box.outer = box box.outer = box
} }
box.invalidateMinimum() box.minSize = ucontainer.NewMemo(box.calculateMinimumSize)
return box return box
} }
func (this *System) NewBox () tomo.Box { func (this *System) NewBox () tomo.Box {
return this.newBox(nil) return this.newBox(nil)
} }
// ----- public methods ----------------------------------------------------- //
func (this *box) GetBox () tomo.Box { func (this *box) GetBox () tomo.Box {
return this.outer return this.outer
} }
@ -101,95 +108,47 @@ func (this *box) Bounds () image.Rectangle {
} }
func (this *box) InnerBounds () 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 { func (this *box) Role () tomo.Role {
return this.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) SetBounds (bounds image.Rectangle) {
if this.bounds == bounds { return }
this.bounds = bounds
this.invalidateLayout()
}
func (this *box) SetColor (c color.Color) {
if c == nil { c = color.Transparent }
if this.color == c { return }
this.color = c
this.invalidateDraw()
}
func (this *box) SetTextureTile (texture canvas.Texture) {
if this.texture == texture && this.textureMode == textureModeTile { return }
this.textureMode = textureModeTile
this.texture = texture
this.invalidateDraw()
}
func (this *box) SetTextureCenter (texture canvas.Texture) {
if this.texture == texture && this.textureMode == textureModeCenter { return }
this.texture = texture
this.textureMode = textureModeCenter
this.invalidateDraw()
}
func (this *box) SetBorder (borders ...tomo.Border) {
previousBorderSum := this.borderSum()
previousBorders := this.border
this.border = borders
// only invalidate the layout if the border is sized differently
if this.borderSum() != previousBorderSum {
this.invalidateLayout()
this.invalidateMinimum()
return
}
// if the border takes up the same amount of space, only invalidate the
// drawing if it looks different
for index, newBorder := range this.border {
different :=
index >= len(previousBorders) ||
newBorder != previousBorders[index]
if different {
this.invalidateDraw()
return
}
}
}
func (this *box) SetMinimumSize (size image.Point) {
if this.userMinSize == size { return }
this.userMinSize = size
this.invalidateMinimum()
}
func (this *box) SetPadding (padding tomo.Inset) {
if this.padding == padding { return }
this.padding = padding
this.invalidateLayout()
this.invalidateMinimum()
}
func (this *box) SetRole (role tomo.Role) { func (this *box) SetRole (role tomo.Role) {
if this.role == role { return }
this.role = role this.role = role
this.lastStyleNonce = -1
this.outer.recursiveReApply()
}
func (this *box) Tag (tag string) bool {
switch tag {
case "hovered": return this.hovered
case "focused": return this.focused
case "pressed": return this.pressed
default: return this.tags.Has(tag)
}
}
func (this *box) SetTag (tag string, on bool) {
wasOn := this.tags.Has(tag)
if on {
this.tags.Add(tag)
} else {
delete(this.tags, tag)
}
if wasOn != on {
this.invalidateStyle()
}
}
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) { func (this *box) SetDNDData (dat data.Data) {
@ -216,6 +175,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) { func (this *box) SetFocusable (focusable bool) {
if this.focusable == focusable { return } if this.focusable == focusable { return }
this.focusable = focusable this.focusable = focusable
@ -224,22 +189,125 @@ func (this *box) SetFocusable (focusable bool) {
} }
} }
func (this *box) Focused () bool { // ----- private methods ---------------------------------------------------- //
hierarchy := this.getHierarchy()
if hierarchy == nil { return false } func (this *box) setAttr (attr tomo.Attr, user bool) {
return hierarchy.isFocused(this.outer) 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 { func (this *box) unsetAttr (kind tomo.AttrKind, user bool) {
hierarchy := this.getHierarchy() switch kind {
if hierarchy == nil { return input.Modifiers { } } case tomo.AttrKindColor:
return hierarchy.getModifiers() 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 { func (this *box) unsetAllAttrs (user bool) {
hierarchy := this.getHierarchy() // keep this in sync with tomo.AttrKind!
if hierarchy == nil { return image.Point { } } this.outer.unsetAttr(tomo.AttrKindColor, user)
return hierarchy.getMousePosition() this.outer.unsetAttr(tomo.AttrKindTexture, user)
this.outer.unsetAttr(tomo.AttrKindTextureMode, user)
this.outer.unsetAttr(tomo.AttrKindBorder, user)
this.outer.unsetAttr(tomo.AttrKindMinimumSize, user)
this.outer.unsetAttr(tomo.AttrKindPadding, user)
this.outer.unsetAttr(tomo.AttrKindGap, user)
this.outer.unsetAttr(tomo.AttrKindTextColor, user)
this.outer.unsetAttr(tomo.AttrKindDotColor, user)
this.outer.unsetAttr(tomo.AttrKindFace, user)
this.outer.unsetAttr(tomo.AttrKindWrap, user)
this.outer.unsetAttr(tomo.AttrKindAlign, user)
this.outer.unsetAttr(tomo.AttrKindOverflow, user)
this.outer.unsetAttr(tomo.AttrKindLayout, user)
}
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 ---------------------------------------------- // // ----- event handler setters ---------------------------------------------- //
@ -264,70 +332,95 @@ func (this *box) OnMouseEnter (callback func()) event.Cookie {
func (this *box) OnMouseLeave (callback func()) event.Cookie { func (this *box) OnMouseLeave (callback func()) event.Cookie {
return this.on.mouseLeave.Connect(callback) 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) return this.on.mouseMove.Connect(callback)
} }
func (this *box) OnMouseDown (callback func(input.Button)) event.Cookie { func (this *box) OnButtonDown (callback func(input.Button) bool) event.Cookie {
return this.on.mouseDown.Connect(callback) return this.on.buttonDown.Connect(callback)
} }
func (this *box) OnMouseUp (callback func(input.Button)) event.Cookie { func (this *box) OnButtonUp (callback func(input.Button) bool) event.Cookie {
return this.on.mouseUp.Connect(callback) 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) 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) 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) 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 () { func (this *box) handleFocusEnter () {
this.focused = true
this.invalidateStyle()
this.on.focusEnter.Broadcast() this.on.focusEnter.Broadcast()
} }
func (this *box) handleFocusLeave () { func (this *box) handleFocusLeave () {
this.focused = false
this.invalidateStyle()
this.on.focusLeave.Broadcast() this.on.focusLeave.Broadcast()
} }
func (this *box) handleMouseEnter () { func (this *box) handleMouseEnter () {
this.hovered = true
this.invalidateStyle()
this.on.mouseEnter.Broadcast() this.on.mouseEnter.Broadcast()
} }
func (this *box) handleMouseLeave () { func (this *box) handleMouseLeave () {
this.hovered = false
this.invalidateStyle()
this.on.mouseLeave.Broadcast() this.on.mouseLeave.Broadcast()
} }
func (this *box) handleMouseMove () { func (this *box) handleMouseMove () (caught bool) {
this.on.mouseMove.Broadcast() for _, listener := range this.on.mouseMove.Listeners() {
} if listener() { caught = true }
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)
} }
return
} }
func (this *box) handleMouseUp (button input.Button) { func (this *box) handleMouseDown (button input.Button) (caught bool) {
for _, listener := range this.on.mouseUp.Listeners() { if button == input.ButtonLeft {
listener(button) 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() { 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() { 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() { for _, listener := range this.on.keyUp.Listeners() {
listener(key, numberPad) if listener(key, numberPad) { caught = true }
} }
return
} }
// -------------------------------------------------------------------------- // // -------------------------------------------------------------------------- //
@ -336,34 +429,46 @@ func (this *box) Draw (can canvas.Canvas) {
pen := can.Pen() pen := can.Pen()
bounds := this.Bounds() 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 // background
pen.Fill(this.color)
if this.textureMode == textureModeTile {
pen.Texture(this.texture)
}
if this.transparent() && this.parent != nil { if this.transparent() && this.parent != nil {
this.parent.drawBackgroundPart(can) this.parent.drawBackgroundPart(can)
} }
pen.Fill(col)
if textureMode == tomo.TextureModeTile && texture != nil {
pen.Texture(texture)
}
pen.Rectangle(bounds) pen.Rectangle(bounds)
// centered texture // centered texture
if this.textureMode == textureModeCenter && this.texture != nil { if textureMode == tomo.TextureModeCenter && texture != nil {
textureBounds := this.texture.Bounds() this.centeredTexture(can, texture)
textureOrigin :=
bounds.Min.
Add(image.Pt (
bounds.Dx() / 2,
bounds.Dy() / 2)).
Sub(image.Pt (
textureBounds.Dx() / 2,
textureBounds.Dy() / 2))
pen.Fill(color.Transparent)
pen.Texture(this.texture)
pen.Rectangle(textureBounds.Sub(textureBounds.Min).Add(textureOrigin))
} }
} }
func (this *box) 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) { func (this *box) drawBorders (can canvas.Canvas) {
if can == nil { return } if can == nil { return }
pen := can.Pen() pen := can.Pen()
@ -371,14 +476,15 @@ func (this *box) drawBorders (can canvas.Canvas) {
rectangle := func (x0, y0, x1, y1 int, c color.Color) { rectangle := func (x0, y0, x1, y1 int, c color.Color) {
area := image.Rect(x0, y0, x1, y1) area := image.Rect(x0, y0, x1, y1)
if util.Transparent(c) && this.parent != nil { if area.Empty() { return }
if ucolor.Transparent(c) && this.parent != nil {
this.parent.drawBackgroundPart(can.SubCanvas(area)) this.parent.drawBackgroundPart(can.SubCanvas(area))
} }
pen.Fill(c) pen.Fill(c)
pen.Rectangle(area) pen.Rectangle(area)
} }
for _, border := range this.border { for _, border := range this.attrBorder.Value() {
rectangle ( rectangle (
bounds.Min.X, bounds.Min.X,
bounds.Min.Y, bounds.Min.Y,
@ -410,26 +516,26 @@ func (this *box) drawBorders (can canvas.Canvas) {
func (this *box) contentMinimum () image.Point { func (this *box) contentMinimum () image.Point {
var minimum image.Point var minimum image.Point
minimum.X += this.padding.Horizontal() padding := tomo.Inset(this.attrPadding.Value())
minimum.Y += this.padding.Vertical() minimum.X += padding.Horizontal()
minimum.Y += padding.Vertical()
borderSum := this.borderSum() borderSum := this.borderSum()
minimum.X += borderSum.Horizontal() minimum.X += borderSum.Horizontal()
minimum.Y += borderSum.Vertical() minimum.Y += borderSum.Vertical()
return minimum return minimum
} }
func (this *box) doMinimumSize () { func (this *box) calculateMinimumSize () image.Point {
this.minSize = this.outer.contentMinimum() userMinSize := this.attrMinimumSize.Value()
if this.minSize.X < this.userMinSize.X { minSize := this.outer.contentMinimum()
this.minSize.X = this.userMinSize.X if minSize.X < userMinSize.X {
minSize.X = userMinSize.X
} }
if this.minSize.Y < this.userMinSize.Y { if minSize.Y < userMinSize.Y {
this.minSize.Y = this.userMinSize.Y minSize.Y = userMinSize.Y
} }
if this.parent != nil { return minSize
this.parent.notifyMinimumSizeChange(this)
}
} }
// var drawcnt int // var drawcnt int
@ -451,7 +557,11 @@ func (this *box) doLayout () {
// laycnt ++ // laycnt ++
this.innerClippingBounds = this.borderSum().Apply(this.bounds) 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) { func (this *box) setParent (parent parent) {
@ -459,6 +569,7 @@ func (this *box) setParent (parent parent) {
this.SetFocused(false) this.SetFocused(false)
} }
this.parent = parent this.parent = parent
this.outer.recursiveReApply()
} }
func (this *box) getParent () parent { func (this *box) getParent () parent {
@ -467,10 +578,6 @@ func (this *box) getParent () parent {
func (this *box) flushActionQueue () { func (this *box) flushActionQueue () {
if this.getHierarchy() == nil { return } if this.getHierarchy() == nil { return }
if this.minSizeQueued {
this.invalidateMinimum()
}
if this.focusQueued != nil { if this.focusQueued != nil {
this.SetFocused(*this.focusQueued) this.SetFocused(*this.focusQueued)
} }
@ -481,8 +588,28 @@ func (this *box) recursiveRedo () {
this.doDraw() this.doDraw()
} }
func (this *box) loseCanvas () { func (this *box) recursiveLoseCanvas () {
this.canvas.InvalidateTo(nil) 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 () { func (this *box) invalidateLayout () {
@ -498,11 +625,37 @@ func (this *box) invalidateDraw () {
} }
func (this *box) invalidateMinimum () { func (this *box) invalidateMinimum () {
this.minSize.Invalidate()
if this.parent != nil {
this.parent.notifyMinimumSizeChange(this)
}
}
func (this *box) recursiveReApply () {
hierarchy := this.getHierarchy() hierarchy := this.getHierarchy()
if hierarchy == nil { if hierarchy == nil { return }
this.minSizeQueued = true
} else { // re-apply styling, icons *if needed*
hierarchy.invalidateMinimum(this.outer)
// 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.unsetAllAttrs(false)
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()
} }
} }
@ -510,7 +663,7 @@ func (this *box) canBeFocused () bool {
return this.focusable 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) { if point.In(this.bounds) {
return this.outer return this.outer
} else { } else {
@ -529,7 +682,8 @@ func (this *box) propagateAlt (callback func (anyBox) bool) bool {
func (this *box) transparent () bool { func (this *box) transparent () bool {
// TODO uncomment once we have // TODO uncomment once we have
// a way to detect texture transparency // a way to detect texture transparency
return util.Transparent(this.color) /*&& col := this.attrColor.Value().Color
return col == nil || ucolor.Transparent(col) /*&&
(this.texture == nil || !this.texture.Opaque())*/ (this.texture == nil || !this.texture.Opaque())*/
} }
@ -543,3 +697,13 @@ func (this *box) getHierarchy () *Hierarchy {
if this.parent == nil { return nil } if this.parent == nil { return nil }
return this.parent.getHierarchy() 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 return box
} }
func (this *canvasBox) Box () tomo.Box {
return this
}
func (this *canvasBox) SetDrawer (drawer canvas.Drawer) { func (this *canvasBox) SetDrawer (drawer canvas.Drawer) {
this.userDrawer = drawer this.userDrawer = drawer
this.invalidateDraw() this.invalidateDraw()
@ -29,7 +25,11 @@ func (this *canvasBox) Invalidate () {
} }
func (this *canvasBox) Draw (can canvas.Canvas) { func (this *canvasBox) Draw (can canvas.Canvas) {
if can == nil { return }
this.box.Draw(can) this.box.Draw(can)
this.userDrawer.Draw ( if this.userDrawer != nil {
can.SubCanvas(this.padding.Apply(this.innerClippingBounds))) padding := tomo.Inset(this.attrPadding.Value())
this.userDrawer.Draw (
can.SubCanvas(padding.Apply(this.innerClippingBounds)))
}
} }

View File

@ -1,24 +1,25 @@
package system package system
import "image" import "image"
import "slices"
import "image/color" import "image/color"
import "git.tebibyte.media/tomo/tomo" import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/event" import "git.tebibyte.media/tomo/tomo/event"
import "git.tebibyte.media/tomo/tomo/canvas" import "git.tebibyte.media/tomo/tomo/canvas"
import "git.tebibyte.media/tomo/backend/internal/util"
type containerBox struct { type containerBox struct {
*box *box
hOverflow, vOverflow bool contentBounds image.Rectangle
hAlign, vAlign tomo.Align scroll image.Point
contentBounds image.Rectangle mask bool
scroll image.Point
capture [4]bool
gap image.Point attrGap attrHierarchy[tomo.AttrGap]
children []tomo.Box attrAlign attrHierarchy[tomo.AttrAlign]
layout tomo.Layout attrOverflow attrHierarchy[tomo.AttrOverflow]
attrLayout attrHierarchy[tomo.AttrLayout]
children []anyBox
on struct { on struct {
contentBoundsChange event.FuncBroadcaster contentBoundsChange event.FuncBroadcaster
@ -31,37 +32,7 @@ func (this *System) NewContainerBox () tomo.ContainerBox {
return box return box
} }
func (this *containerBox) SetColor (c color.Color) { // ----- public methods ----------------------------------------------------- //
if this.color == c { return }
this.box.SetColor(c)
this.invalidateTransparentChildren()
}
func (this *containerBox) SetTextureTile (texture canvas.Texture) {
if this.texture == texture { return }
this.box.SetTextureTile(texture)
this.invalidateTransparentChildren()
}
func (this *containerBox) SetTextureCenter (texture canvas.Texture) {
if this.texture == texture { return }
this.box.SetTextureTile(texture)
this.invalidateTransparentChildren()
}
func (this *containerBox) SetOverflow (horizontal, vertical bool) {
if this.hOverflow == horizontal && this.vOverflow == vertical { return }
this.hOverflow = horizontal
this.vOverflow = vertical
this.invalidateLayout()
}
func (this *containerBox) SetAlign (x, y tomo.Align) {
if this.hAlign == x && this.vAlign == y { return }
this.hAlign = x
this.vAlign = y
this.invalidateLayout()
}
func (this *containerBox) ContentBounds () image.Rectangle { func (this *containerBox) ContentBounds () image.Rectangle {
return this.contentBounds return this.contentBounds
@ -77,32 +48,9 @@ func (this *containerBox) OnContentBoundsChange (callback func()) event.Cookie {
return this.on.contentBoundsChange.Connect(callback) 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) { func (this *containerBox) Add (child tomo.Object) {
box := assertAnyBox(child.GetBox()) box := assertAnyBox(child.GetBox())
if util.IndexOf(this.children, tomo.Box(box)) > -1 { return } if slices.Index(this.children, box) > -1 { return }
box.setParent(this) box.setParent(this)
box.flushActionQueue() box.flushActionQueue()
@ -113,26 +61,26 @@ func (this *containerBox) Add (child tomo.Object) {
func (this *containerBox) Remove (child tomo.Object) { func (this *containerBox) Remove (child tomo.Object) {
box := assertAnyBox(child.GetBox()) box := assertAnyBox(child.GetBox())
index := util.IndexOf(this.children, tomo.Box(box)) index := slices.Index(this.children, box)
if index < 0 { return } if index < 0 { return }
box.setParent(nil) box.setParent(nil)
this.children = util.Remove(this.children, index) this.children = slices.Delete(this.children, index, index + 1)
this.invalidateLayout() this.invalidateLayout()
this.invalidateMinimum() this.invalidateMinimum()
} }
func (this *containerBox) Insert (child, before tomo.Object) { func (this *containerBox) Insert (child, before tomo.Object) {
box := assertAnyBox(child.GetBox()) box := assertAnyBox(child.GetBox())
if util.IndexOf(this.children, tomo.Box(box)) > -1 { return } if slices.Index(this.children, box) > -1 { return }
beforeBox := assertAnyBox(before.GetBox()) beforeBox := assertAnyBox(before.GetBox())
index := util.IndexOf(this.children, tomo.Box(beforeBox)) index := slices.Index(this.children, beforeBox)
if index < 0 { if index < 0 {
this.children = append(this.children, tomo.Box(box)) this.children = append(this.children, box)
} else { } else {
this.children = util.Insert(this.children, index, tomo.Box(box)) this.children = slices.Insert(this.children, index, box)
} }
box.setParent(this) box.setParent(this)
@ -149,7 +97,7 @@ func (this *containerBox) Clear () {
this.invalidateMinimum() this.invalidateMinimum()
} }
func (this *containerBox) Length () int { func (this *containerBox) Len () int {
return len(this.children) return len(this.children)
} }
@ -160,14 +108,19 @@ func (this *containerBox) At (index int) tomo.Object {
return this.children[index] return this.children[index]
} }
func (this *containerBox) SetLayout (layout tomo.Layout) { func (this *containerBox) SetInputMask (mask bool) {
this.layout = layout this.mask = mask
this.invalidateLayout()
this.invalidateMinimum()
} }
// ----- private methods ---------------------------------------------------- //
func (this *containerBox) Draw (can canvas.Canvas) { func (this *containerBox) Draw (can canvas.Canvas) {
if can == nil { return } 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)) rocks := make([]image.Rectangle, len(this.children))
for index, box := range this.children { for index, box := range this.children {
@ -180,17 +133,137 @@ func (this *containerBox) Draw (can canvas.Canvas) {
} }
if clipped == nil { continue } if clipped == nil { continue }
pen := clipped.Pen() pen := clipped.Pen()
pen.Fill(this.color) pen.Fill(col)
pen.Texture(this.texture) pen.Texture(texture)
pen.Rectangle(this.innerClippingBounds) 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) { func (this *containerBox) drawBackgroundPart (can canvas.Canvas) {
if can == nil { return } if can == nil { return }
pen := can.Pen() 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 { if this.transparent() && this.parent != nil {
this.parent.drawBackgroundPart(can) this.parent.drawBackgroundPart(can)
@ -225,9 +298,13 @@ func (this *containerBox) getCanvas () canvas.Canvas {
return this.canvas.Value() return this.canvas.Value()
} }
func (this *containerBox) getInnerClippingBounds () image.Rectangle {
return this.innerClippingBounds
}
func (this *containerBox) notifyMinimumSizeChange (child anyBox) { func (this *containerBox) notifyMinimumSizeChange (child anyBox) {
this.invalidateMinimum() this.invalidateMinimum()
size := child.MinimumSize() size := child.minimumSize()
bounds := child.Bounds() bounds := child.Bounds()
if bounds.Dx() < size.X || bounds.Dy() < size.Y { if bounds.Dx() < size.X || bounds.Dy() < size.Y {
this.invalidateLayout() this.invalidateLayout()
@ -235,23 +312,28 @@ func (this *containerBox) notifyMinimumSizeChange (child anyBox) {
} }
func (this *containerBox) layoutHints () tomo.LayoutHints { func (this *containerBox) layoutHints () tomo.LayoutHints {
overflow := this.attrOverflow.Value()
align := this.attrAlign.Value()
gap := image.Point(this.attrGap.Value())
return tomo.LayoutHints { return tomo.LayoutHints {
OverflowX: this.hOverflow, OverflowX: overflow.X,
OverflowY: this.vOverflow, OverflowY: overflow.Y,
AlignX: this.hAlign, AlignX: align.X,
AlignY: this.vAlign, AlignY: align.Y,
Gap: this.gap, Gap: gap,
} }
} }
func (this *containerBox) contentMinimum () image.Point { func (this *containerBox) contentMinimum () image.Point {
minimum := this.box.contentMinimum() overflow := this.attrOverflow.Value()
if this.layout != nil { minimum := this.box.contentMinimum()
layoutMinimum := this.layout.MinimumSize ( layout := this.attrLayout.Value().Layout
if layout != nil {
layoutMinimum := layout.MinimumSize (
this.layoutHints(), this.layoutHints(),
this.children) this.boxQuerier())
if this.hOverflow { layoutMinimum.X = 0 } if overflow.X { layoutMinimum.X = 0 }
if this.vOverflow { layoutMinimum.Y = 0 } if overflow.Y { layoutMinimum.Y = 0 }
minimum = minimum.Add(layoutMinimum) minimum = minimum.Add(layoutMinimum)
} }
return minimum return minimum
@ -260,25 +342,27 @@ func (this *containerBox) contentMinimum () image.Point {
func (this *containerBox) doLayout () { func (this *containerBox) doLayout () {
this.box.doLayout() this.box.doLayout()
previousContentBounds := this.contentBounds previousContentBounds := this.contentBounds
layout := this.attrLayout.Value().Layout
// by default, use innerBounds (translated to 0, 0) for contentBounds. // by default, use innerBounds (translated to 0, 0) for contentBounds.
// if a direction overflows, use the layout's minimum size for it. // if a direction overflows, use the layout's minimum size for it.
var minimum image.Point var minimum image.Point
if this.layout != nil { if layout != nil {
minimum = this.layout.MinimumSize ( minimum = layout.MinimumSize (
this.layoutHints(), this.layoutHints(),
this.children) this.boxQuerier())
} }
innerBounds := this.InnerBounds() innerBounds := this.InnerBounds()
overflow := this.attrOverflow.Value()
this.contentBounds = innerBounds.Sub(innerBounds.Min) this.contentBounds = innerBounds.Sub(innerBounds.Min)
if this.hOverflow { this.contentBounds.Max.X = this.contentBounds.Min.X + minimum.X } if overflow.X { 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.Y { this.contentBounds.Max.Y = this.contentBounds.Min.Y + minimum.Y }
// arrange children // arrange children
if this.layout != nil { if layout != nil {
layoutHints := this.layoutHints() layoutHints := this.layoutHints()
layoutHints.Bounds = this.contentBounds 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 // build an accurate contentBounds by unioning the bounds of all child
@ -294,7 +378,7 @@ func (this *containerBox) doLayout () {
// offset children and contentBounds by scroll // offset children and contentBounds by scroll
for _, box := range this.children { 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) this.contentBounds = this.contentBounds.Add(this.scroll)
@ -327,6 +411,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 () { func (this *containerBox) recursiveRedo () {
this.doLayout() this.doLayout()
this.doDraw() this.doDraw()
@ -335,12 +427,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 !point.In(this.bounds) { return nil }
if !this.capture[category] { if !this.mask {
for _, box := range this.children { for _, box := range this.children {
candidate := box.(anyBox).boxUnder(point, category) candidate := box.(anyBox).boxUnder(point)
if candidate != nil { return candidate } if candidate != nil { return candidate }
} }
} }
@ -368,6 +474,6 @@ func (this *containerBox) propagateAlt (callback func (anyBox) bool) bool {
return true return true
} }
func (this *containerBox) captures (category eventCategory) bool { func (this *containerBox) masks () bool {
return this.capture[category] return this.mask
} }

View File

@ -2,6 +2,10 @@ package system
import "image" import "image"
import "git.tebibyte.media/tomo/tomo/input" import "git.tebibyte.media/tomo/tomo/input"
import "git.tebibyte.media/tomo/tomo/config"
// 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 // HandleFocusChange sets whether or not the window containing this Hierarchy
// has input focus. // has input focus.
@ -20,14 +24,30 @@ func (this *Hierarchy) HandleModifiers (modifiers input.Modifiers) {
// event which triggers this comes with modifier key information, // event which triggers this comes with modifier key information,
// HandleModifiers must be called *before* HandleKeyDown. // HandleModifiers must be called *before* HandleKeyDown.
func (this *Hierarchy) HandleKeyDown (key input.Key, numberPad bool) { func (this *Hierarchy) HandleKeyDown (key input.Key, numberPad bool) {
if key == input.KeyTab && this.modifiers.Alt { caught := false
if this.modifiers.Shift { if this.anyFocused() {
this.focusPrevious() this.keyboardTargets(func (target anyBox) bool {
} else { if target.handleKeyDown(key, numberPad) {
this.focusNext() caught = true
return false
}
return true
})
} else {
if this.root != nil {
caught = this.root.handleKeyDown(key, numberPad)
} }
} else if target := this.keyboardTarget(); target != nil { }
target.handleKeyDown(key, numberPad) if caught { return }
switch input.KC(key, this.modifiers) {
case config.KeyChordFocusNext: this.focusNext()
case config.KeyChordFocusPrevious: this.focusPrevious()
// TODO: up, down, left, and right should find a box to the top, bottom,
// left, and right respectively to move the focus to. we might want to
// have four corresponding key chords in tomo/config.
case input.KC(input.KeyDown, input.ModNone): this.focusNext()
case input.KC(input.KeyUp, input.ModNone): this.focusPrevious()
} }
} }
@ -35,63 +55,88 @@ func (this *Hierarchy) HandleKeyDown (key input.Key, numberPad bool) {
// which triggers this comes with modifier key information, HandleModifiers must // which triggers this comes with modifier key information, HandleModifiers must
// be called *before* HandleKeyUp. // be called *before* HandleKeyUp.
func (this *Hierarchy) HandleKeyUp (key input.Key, numberPad bool) { func (this *Hierarchy) HandleKeyUp (key input.Key, numberPad bool) {
if target := this.keyboardTarget(); target != nil { if this.anyFocused() {
target.handleKeyUp(key, numberPad) this.keyboardTargets(func (target anyBox) bool {
} if target.handleKeyUp(key, numberPad) {
} return false
}
// HandleMouseDown sends a mouse down event to the Box positioned underneath the return true
// mouse cursor and marks it as being "dragged" by that mouse button. If the })
// event which triggers this comes with mouse position information, } else {
// HandleMouseMove must be called *before* HandleMouseDown. if this.root != nil {
func (this *Hierarchy) HandleMouseDown (button input.Button) { this.root.handleKeyUp(key, numberPad)
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()
} }
} }
} }
// HandleScroll sends a scroll event to the Box currently underneath the mouse // HandleMouseDown sends a mouse down event to the Boxes positioned underneath
// cursor. // the mouse cursor and marks them as being "dragged" by that mouse button,
func (this *Hierarchy) HandleScroll (x, y float64) { // starting at the first Box to mask events and ending at the first box to
underneath := this.boxUnder(this.mousePosition, eventCategoryScroll) // catch the event. If the event which triggers this comes with mouse position
if underneath != nil { // information, HandleMouseMove must be called *before* HandleMouseDown.
underneath.handleScroll(x, y) 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,7 +4,8 @@ import "image"
import "git.tebibyte.media/tomo/tomo" import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/input" import "git.tebibyte.media/tomo/tomo/input"
import "git.tebibyte.media/tomo/tomo/canvas" import "git.tebibyte.media/tomo/tomo/canvas"
import "git.tebibyte.media/tomo/backend/internal/util" import "git.tebibyte.media/tomo/backend/style"
import "git.tebibyte.media/sashakoshka/goutil/container"
// Hierarchy is coupled to a tomo.Window implementation, and manages a tree of // Hierarchy is coupled to a tomo.Window implementation, and manages a tree of
// Boxes. // Boxes.
@ -21,12 +22,12 @@ type Hierarchy struct {
modifiers input.Modifiers modifiers input.Modifiers
mousePosition image.Point mousePosition image.Point
drags [10]anyBox drags [10][]anyBox
minimumSize image.Point minimumSize image.Point
needMinimum util.Set[anyBox] needStyle ucontainer.Set[anyBox]
needLayout util.Set[anyBox] needLayout ucontainer.Set[anyBox]
needDraw util.Set[anyBox] needDraw ucontainer.Set[anyBox]
needRedo bool needRedo bool
minimumClean bool minimumClean bool
} }
@ -52,10 +53,11 @@ func (this *System) NewHierarchy (link WindowLink) *Hierarchy {
hierarchy := &Hierarchy { hierarchy := &Hierarchy {
system: this, system: this,
link: link, link: link,
needMinimum: make(util.Set[anyBox]), needStyle: make(ucontainer.Set[anyBox]),
needLayout: make(util.Set[anyBox]), needLayout: make(ucontainer.Set[anyBox]),
needDraw: make(util.Set[anyBox]), needDraw: make(ucontainer.Set[anyBox]),
} }
this.hierarchies.Add(hierarchy)
return 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. // draw to. The Hierarchy will use the canvas.Canvas's bounds to lay itself out.
func (this *Hierarchy) SetCanvas (can canvas.Canvas) { func (this *Hierarchy) SetCanvas (can canvas.Canvas) {
this.canvas = can this.canvas = can
if this.root != nil { this.root.loseCanvas() } if this.root != nil { this.root.recursiveLoseCanvas() }
this.needRedo = true this.needRedo = true
} }
@ -94,6 +96,16 @@ func (this *Hierarchy) MinimumSize () image.Point {
return this.minimumSize 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. // AfterEvent should be called at the end of every event cycle.
func (this *Hierarchy) AfterEvent () { func (this *Hierarchy) AfterEvent () {
if this.canvas == nil { return } if this.canvas == nil { return }
@ -103,7 +115,7 @@ func (this *Hierarchy) AfterEvent () {
childBounds := this.canvas.Bounds() childBounds := this.canvas.Bounds()
childBounds = childBounds.Sub(childBounds.Min) childBounds = childBounds.Sub(childBounds.Min)
if this.root != nil { if this.root != nil {
this.root.SetBounds(childBounds) this.root.setBounds(childBounds)
} }
// full relayout/redraw // full relayout/redraw
@ -115,8 +127,8 @@ func (this *Hierarchy) AfterEvent () {
return return
} }
for len(this.needMinimum) > 0 { for len(this.needStyle) > 0 {
this.needMinimum.Pop().doMinimumSize() this.needStyle.Pop().doStyle()
} }
if !this.minimumClean { if !this.minimumClean {
this.doMinimumSize() 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 { func (this *Hierarchy) getHierarchy () *Hierarchy {
return this return this
} }
@ -143,10 +169,34 @@ func (this *Hierarchy) getWindow () tomo.Window {
return this.link.GetWindow() 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 { func (this *Hierarchy) getCanvas () canvas.Canvas {
return this.canvas return this.canvas
} }
func (this *Hierarchy) getInnerClippingBounds () image.Rectangle {
return this.canvas.Bounds()
}
func (this *Hierarchy) getModifiers () input.Modifiers { func (this *Hierarchy) getModifiers () input.Modifiers {
return this.modifiers return this.modifiers
} }
@ -159,8 +209,8 @@ func (this *Hierarchy) notifyMinimumSizeChange (anyBox) {
this.minimumClean = false this.minimumClean = false
} }
func (this *Hierarchy) invalidateMinimum (box anyBox) { func (this *Hierarchy) invalidateStyle (box anyBox) {
this.needMinimum.Add(box) this.needStyle.Add(box)
} }
func (this *Hierarchy) invalidateDraw (box anyBox) { func (this *Hierarchy) invalidateDraw (box anyBox) {
@ -208,38 +258,68 @@ func (this *Hierarchy) anyFocused () bool {
return this.focused != nil return this.focused != nil
} }
func (this *Hierarchy) boxUnder (point image.Point, category eventCategory) anyBox { func (this *Hierarchy) masks () bool {
if this.root == nil { return nil }
return this.root.boxUnder(point, category)
}
func (this *Hierarchy) captures (eventCategory) bool {
return false 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 focused := this.focused
if focused == nil { return nil } if focused == nil { return }
parent := focused.getParent() this.parents(this.considerMaskingParents(focused))(yield)
}
func (this *Hierarchy) considerMaskingParents (box anyBox) anyBox {
parent := box.getParent()
for { for {
parentBox, ok := parent.(anyBox) parentBox, ok := parent.(anyBox)
if !ok { break } if !ok { break }
if parent.captures(eventCategoryKeyboard) { if parent.masks() {
return parentBox return parentBox
} }
parent = parentBox.getParent() 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 () { func (this *Hierarchy) focusNext () {
found := !this.anyFocused() found := !this.anyFocused()
focused := false focused := false
this.propagateAlt (func (box anyBox) bool { this.propagateAlt(func (box anyBox) bool {
if found { if found {
// looking for the next box to select // looking for the next box to select
if box.canBeFocused() { if box.canBeFocused() && !this.isMasked(box) {
// found it // found it
this.focus(box) this.focus(box)
focused = true focused = true
@ -260,11 +340,11 @@ func (this *Hierarchy) focusNext () {
func (this *Hierarchy) focusPrevious () { func (this *Hierarchy) focusPrevious () {
var behind anyBox var behind anyBox
this.propagate (func (box anyBox) bool { this.propagate(func (box anyBox) bool {
if box == this.focused { if box == this.focused {
return false return false
} }
if box.canBeFocused() { behind = box } if box.canBeFocused() && !this.isMasked(box) { behind = box }
return true return true
}) })
this.focus(behind) this.focus(behind)
@ -287,13 +367,25 @@ func (this *Hierarchy) drawBackgroundPart (canvas.Canvas) {
// if so, windows should be transparent if the color has transparency // if so, windows should be transparent if the color has transparency
} }
// var minimumSizeCount = 0
func (this *Hierarchy) doMinimumSize () { func (this *Hierarchy) doMinimumSize () {
this.minimumClean = true this.minimumClean = true
// println("doMinimumSize", minimumSizeCount)
// minimumSizeCount ++
previousMinimumSize := this.minimumSize
this.minimumSize = image.Point { } this.minimumSize = image.Point { }
if this.root != nil { 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/input"
import "git.tebibyte.media/tomo/tomo/canvas" 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 // parent is any hierarchical type which contains other boxes. This can be a
// Hierarchy, containerBox, etc. // Hierarchy, containerBox, etc.
type parent interface { type parent interface {
@ -20,6 +12,9 @@ type parent interface {
getHierarchy () *Hierarchy getHierarchy () *Hierarchy
// canvas returns the canvas held by the parent. // canvas returns the canvas held by the parent.
getCanvas () canvas.Canvas getCanvas () canvas.Canvas
// getInnerClippingBounds returns the area of the canvas that children
// can draw to.
getInnerClippingBounds () image.Rectangle
// notifyMinimumSizeChange informs the parent that the minimum size of // notifyMinimumSizeChange informs the parent that the minimum size of
// one of its children has changed. // one of its children has changed.
notifyMinimumSizeChange (anyBox) notifyMinimumSizeChange (anyBox)
@ -27,9 +22,8 @@ type parent interface {
// given Canvas, filling the Canvas's entire bounds. The origin (0, 0) // 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. // of the given Canvas is assumed to be the same as the parent's canvas.
drawBackgroundPart (canvas.Canvas) drawBackgroundPart (canvas.Canvas)
// captures returns whether or not this parent captures the given event // catches returns whether or not this parent masks events.
// category. masks () bool
captures (eventCategory) bool
} }
// anyBox is any tomo.Box type that is implemented by this package. // anyBox is any tomo.Box type that is implemented by this package.
@ -44,21 +38,27 @@ type anyBox interface {
// doDraw re-paints the anyBox onto its currently held Canvas non-recursively // doDraw re-paints the anyBox onto its currently held Canvas non-recursively
// doLayout re-calculates the layout of the anyBox non-recursively // doLayout re-calculates the layout of the anyBox non-recursively
// doMinimumSize re-calculates the minimum size of the anyBox non-recursively // doStyle re-applies the box's style non-recursively
doDraw () doDraw ()
doLayout () doLayout ()
doMinimumSize () doStyle ()
// flushActionQueue performs any queued actions, like invalidating the // flushActionQueue performs any queued actions, like invalidating the
// minimum size or grabbing input focus. // minimum size or grabbing input focus.
flushActionQueue () flushActionQueue ()
// recursiveRedo recursively recalculates the minimum size, layout, and // recursiveRedo recursively recalculates the minimum size, layout, and
// re-paints this anyBox and all of its children. // re-paints this anyBox and all of its children.
recursiveRedo () recursiveRedo ()
// loseCanvas causes this anyBox and its children (if applicable) to // loseCanvas causes this anyBox and its children (if applicable) to
// lose their canvases and re-cut them as needed. // 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 returns the minimum dimensions of this box's content
contentMinimum () image.Point contentMinimum () image.Point
// canBeFocused returns whether or not this anyBox is capable of holding // canBeFocused returns whether or not this anyBox is capable of holding
@ -67,12 +67,21 @@ type anyBox interface {
// boxUnder returns the anyBox under the mouse pointer. It can be this // boxUnder returns the anyBox under the mouse pointer. It can be this
// anyBox, one of its children (if applicable). It must return nil if // anyBox, one of its children (if applicable). It must return nil if
// the mouse pointer is outside of this anyBox's bounds. // 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 // transparent returns whether or not this anyBox contains transparent
// pixels or not, and thus needs its parent's backround to be painted // pixels or not, and thus needs its parent's backround to be painted
// underneath it. // underneath it.
transparent () bool 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 // propagate recursively calls a function on this anyBox, and all of its
// children (if applicable) The normal propagate behavior calls the // children (if applicable) The normal propagate behavior calls the
// callback on all children before calling it on this anyBox, and // callback on all children before calling it on this anyBox, and
@ -81,19 +90,28 @@ type anyBox interface {
propagate (func (anyBox) bool) bool propagate (func (anyBox) bool) bool
propagateAlt (func (anyBox) bool) bool propagateAlt (func (anyBox) bool) bool
handleFocusEnter () handleFocusEnter ()
handleFocusLeave () handleFocusLeave ()
// handleDndEnter () // handleDndEnter ()
// handleDndLeave () // handleDndLeave ()
// handleDndDrop (data.Data) // handleDndDrop (data.Data)
handleMouseEnter () handleMouseEnter ()
handleMouseLeave () handleMouseLeave ()
handleMouseMove () handleMouseMove () bool
handleMouseDown (input.Button) handleMouseDown (input.Button) bool
handleMouseUp (input.Button) handleMouseUp (input.Button) bool
handleScroll (float64, float64) handleScroll (float64, float64) bool
handleKeyDown (input.Key, bool) handleKeyDown (input.Key, bool) bool
handleKeyUp (input.Key, 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 { 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 "io"
import "image" import "image"
import "git.tebibyte.media/tomo/tomo/canvas" import "git.tebibyte.media/tomo/tomo/canvas"
import "git.tebibyte.media/tomo/backend/style"
import "git.tebibyte.media/sashakoshka/goutil/container"
// System is coupled to a tomo.Backend implementation, and manages Hierarchies // System is coupled to a tomo.Backend implementation, and manages Hierarchies
// and Boxes. // and Boxes.
type System struct { type System struct {
link BackendLink link BackendLink
style *style.Style
iconSet style.IconSet
faceSet style.FaceSet
styleNonce int
iconSetNonce int
hierarchies ucontainer.Set[*Hierarchy]
} }
// BackendLink allows the System to call up into the tomo.Backend implementation // BackendLink allows the System to call up into the tomo.Backend implementation
@ -30,6 +40,36 @@ type SurfaceLink interface {
// New creates a new System. // New creates a new System.
func New (link BackendLink) *System { func New (link BackendLink) *System {
return &System { return &System {
link: link, link: link,
hierarchies: make(ucontainer.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

@ -1,6 +1,7 @@
package system package system
import "image" import "image"
import "unicode"
import "image/color" import "image/color"
import "golang.org/x/image/font" import "golang.org/x/image/font"
import "git.tebibyte.media/tomo/tomo" import "git.tebibyte.media/tomo/tomo"
@ -10,28 +11,33 @@ import "git.tebibyte.media/tomo/tomo/text"
import "git.tebibyte.media/tomo/tomo/input" import "git.tebibyte.media/tomo/tomo/input"
import "git.tebibyte.media/tomo/tomo/event" import "git.tebibyte.media/tomo/tomo/event"
import "git.tebibyte.media/tomo/tomo/canvas" import "git.tebibyte.media/tomo/tomo/canvas"
import "git.tebibyte.media/tomo/backend/internal/util"
import "git.tebibyte.media/sashakoshka/goutil/container"
type textBox struct { type textBox struct {
*box *box
hOverflow, vOverflow bool
contentBounds image.Rectangle contentBounds image.Rectangle
scroll image.Point scroll image.Point
text string attrTextColor attrHierarchy[tomo.AttrTextColor]
textColor color.Color attrDotColor attrHierarchy[tomo.AttrDotColor]
face font.Face attrFace attrHierarchy[tomo.AttrFace]
wrap bool attrWrap attrHierarchy[tomo.AttrWrap]
hAlign tomo.Align attrAlign attrHierarchy[tomo.AttrAlign]
vAlign tomo.Align attrOverflow attrHierarchy[tomo.AttrOverflow]
text string
runes []rune
selectable bool selectable bool
selecting bool selecting bool
selectStart int selectStart int
dot text.Dot dot text.Dot
dotColor color.Color desiredX fixed.Int26_6
drawer typeset.Drawer drawer typeset.Drawer
face util.Cycler[font.Face]
lineHeight ucontainer.Memo[fixed.Int26_6]
on struct { on struct {
contentBoundsChange event.FuncBroadcaster contentBoundsChange event.FuncBroadcaster
@ -40,20 +46,20 @@ type textBox struct {
} }
func (this *System) NewTextBox () tomo.TextBox { func (this *System) NewTextBox () tomo.TextBox {
box := &textBox { box := &textBox { }
textColor: color.Black,
dotColor: color.RGBA { B: 255, G: 255, A: 255 },
}
box.box = this.newBox(box) 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}))
box.lineHeight = ucontainer.NewMemo(func () fixed.Int26_6 {
face := box.face.Value()
if face == nil { return 0 }
metrics := face.Metrics()
return metrics.Height
})
return box return box
} }
func (this *textBox) SetOverflow (horizontal, vertical bool) { // ----- public methods ----------------------------------------------------- //
if this.hOverflow == horizontal && this.vOverflow == vertical { return }
this.hOverflow = horizontal
this.vOverflow = vertical
this.invalidateLayout()
}
func (this *textBox) ContentBounds () image.Rectangle { func (this *textBox) ContentBounds () image.Rectangle {
return this.contentBounds return this.contentBounds
@ -65,35 +71,24 @@ func (this *textBox) ScrollTo (point image.Point) {
this.invalidateLayout() 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 { func (this *textBox) OnContentBoundsChange (callback func()) event.Cookie {
return this.on.contentBoundsChange.Connect(callback) return this.on.contentBoundsChange.Connect(callback)
} }
func (this *textBox) SetText (text string) { func (this *textBox) SetText (text string) {
if this.text == text { return } if this.text == text { return }
this.text = text this.text = text
this.drawer.SetText([]rune(text)) this.runes = []rune(text)
this.invalidateMinimum() this.drawer.SetText(this.runes)
this.invalidateLayout()
}
func (this *textBox) SetTextColor (c color.Color) {
if this.textColor == c { return }
this.textColor = c
this.invalidateDraw()
}
func (this *textBox) SetFace (face font.Face) {
if this.face == face { return }
this.face = face
this.drawer.SetFace(face)
this.invalidateMinimum()
this.invalidateLayout()
}
func (this *textBox) SetWrap (wrap bool) {
if this.wrap == wrap { return }
this.drawer.SetWrap(wrap)
this.invalidateMinimum() this.invalidateMinimum()
this.invalidateLayout() this.invalidateLayout()
} }
@ -103,22 +98,42 @@ func (this *textBox) SetSelectable (selectable bool) {
this.selectable = selectable this.selectable = selectable
} }
func (this *textBox) SetDotColor (c color.Color) { func (this *textBox) Select (dot text.Dot) {
if this.dotColor == c { return } this.selec(dot)
this.dotColor = c
if !this.dot.Empty() {
this.invalidateDraw()
}
} }
func (this *textBox) Select (dot text.Dot) { func (this *textBox) selec (dot text.Dot) bool {
if !this.selectable { return } if this.selectWithoutResettingDesiredX(dot) {
if this.dot == dot { return } this.desiredX = fixed.I(0)
return true
}
return false
}
func (this *textBox) selectWithoutResettingDesiredX (dot text.Dot) bool {
if !this.selectable { return false }
if this.dot == dot { return false }
this.SetFocused(true) this.SetFocused(true)
this.dot = dot this.dot = dot
this.scrollToDot() this.scrollToDot()
this.on.dotChange.Broadcast()
this.invalidateDraw() this.invalidateDraw()
return true
}
func (this *textBox) userSelect (dot text.Dot) bool {
if this.selec(dot) {
this.on.dotChange.Broadcast()
return true
}
return false
}
func (this *textBox) userSelectWithoutResettingDesiredX (dot text.Dot) bool {
if this.selectWithoutResettingDesiredX(dot) {
this.on.dotChange.Broadcast()
return true
}
return false
} }
func (this *textBox) Dot () text.Dot { func (this *textBox) Dot () text.Dot {
@ -129,21 +144,19 @@ func (this *textBox) OnDotChange (callback func ()) event.Cookie {
return this.on.dotChange.Connect(callback) return this.on.dotChange.Connect(callback)
} }
func (this *textBox) SetAlign (x, y tomo.Align) { // ----- private methods ---------------------------------------------------- //
if this.hAlign == x && this.vAlign == y { return }
this.hAlign = x
this.vAlign = y
this.drawer.SetAlign(typeset.Align(x), typeset.Align(y))
this.invalidateDraw()
}
func (this *textBox) Draw (can canvas.Canvas) { func (this *textBox) Draw (can canvas.Canvas) {
if can == nil { return } if can == nil { return }
this.drawBorders(can)
pen := can.Pen() texture := this.attrTexture.Value().Texture
pen.Fill(this.color) col := this.attrColor.Value().Color
pen.Texture(this.texture)
this.drawBorders(can)
pen := can.Pen()
pen.Fill(col)
pen.Texture(texture)
if this.transparent() && this.parent != nil { if this.transparent() && this.parent != nil {
this.parent.drawBackgroundPart(can) this.parent.drawBackgroundPart(can)
} }
@ -153,8 +166,96 @@ func (this *textBox) Draw (can canvas.Canvas) {
this.drawDot(can) this.drawDot(can)
} }
if this.face == nil { return } if this.face.Value() != nil {
this.drawer.Draw(can, this.textColor, this.textOffset()) 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 { func roundPt (point fixed.Point26_6) image.Point {
@ -166,28 +267,30 @@ func fixPt (point image.Point) fixed.Point26_6 {
} }
func (this *textBox) drawDot (can canvas.Canvas) { 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 := can.Pen()
pen.Fill(color.Transparent)
pen.Stroke(this.textColor)
bounds := this.InnerBounds() bounds := this.InnerBounds()
metrics := this.face.Metrics() metrics := face.Metrics()
dot := this.dot.Canon() dot := this.dot
start := this.drawer.PositionAt(dot.Start).Add(fixPt(this.textOffset())) canonDot := dot.Canon()
end := this.drawer.PositionAt(dot.End ).Add(fixPt(this.textOffset())) start := this.drawer.PositionAt(canonDot.Start).Add(fixPt(this.textOffset()))
height := this.drawer.LineHeight().Round() end := this.drawer.PositionAt(canonDot.End ).Add(fixPt(this.textOffset()))
ascent := fixed.Point26_6 { Y: metrics.Descent } canonEnd := this.drawer.PositionAt(dot.End ).Add(fixPt(this.textOffset()))
descent := fixed.Point26_6 { Y: metrics.Ascent } height := this.drawer.LineHeight().Round()
ascent := fixed.Point26_6 { Y: metrics.Descent }
descent := fixed.Point26_6 { Y: metrics.Ascent }
switch { switch {
case dot.Empty(): case canonDot.Empty():
pen.StrokeWeight(1)
pen.Path(roundPt(start.Add(ascent)), roundPt(start.Sub(descent)))
case start.Y == end.Y: case start.Y == end.Y:
pen.Fill(this.dotColor) pen.Fill(dotColor)
pen.StrokeWeight(0) pen.StrokeWeight(0)
pen.Rectangle(image.Rectangle { pen.Rectangle(image.Rectangle {
Min: roundPt(start.Add(ascent)), Min: roundPt(start.Add(ascent)),
@ -195,7 +298,7 @@ func (this *textBox) drawDot (can canvas.Canvas) {
}) })
default: default:
pen.Fill(this.dotColor) pen.Fill(dotColor)
pen.StrokeWeight(0) pen.StrokeWeight(0)
rect := image.Rectangle { rect := image.Rectangle {
@ -218,6 +321,19 @@ func (this *textBox) drawDot (can canvas.Canvas) {
rect.Min.X = bounds.Min.X rect.Min.X = bounds.Min.X
pen.Rectangle(rect) pen.Rectangle(rect)
} }
pen.Stroke(textColor)
pen.StrokeWeight(1)
beamTop := roundPt(canonEnd.Add(ascent)).Sub(image.Pt(0, 1))
beamBottom := roundPt(canonEnd.Sub(descent))
beamSerif := 3
pen.Path(beamTop, beamBottom)
pen.Path (
beamTop.Sub(image.Pt(beamSerif - 1, 0)),
beamTop.Add(image.Pt(beamSerif, 0)))
pen.Path (
beamBottom.Sub(image.Pt(beamSerif - 1, 0)),
beamBottom.Add(image.Pt(beamSerif, 0)))
} }
func (this *textBox) textOffset () image.Point { func (this *textBox) textOffset () image.Point {
@ -226,44 +342,177 @@ func (this *textBox) textOffset () image.Point {
Sub(this.drawer.LayoutBoundsSpace().Min) Sub(this.drawer.LayoutBoundsSpace().Min)
} }
func (this *textBox) handleFocusEnter () {
this.invalidateDraw()
this.box.handleFocusEnter()
}
func (this *textBox) handleFocusLeave () { func (this *textBox) handleFocusLeave () {
this.on.dotChange.Broadcast()
this.invalidateDraw() this.invalidateDraw()
this.box.handleFocusLeave() this.box.handleFocusLeave()
} }
func (this *textBox) handleMouseDown (button input.Button) { func (this *textBox) handleMouseDown (button input.Button) bool {
if button == input.ButtonLeft { if this.mouseButtonCanDrag(button) {
index := this.runeUnderMouse() index := this.runeUnderMouse()
this.selectStart = index this.selectStart = index
this.selecting = true this.selecting = true
this.Select(text.Dot { Start: this.selectStart, End: index }) this.userSelect(text.Dot { Start: this.selectStart, End: index })
} }
this.box.handleMouseDown(button) return this.box.handleMouseDown(button)
} }
func (this *textBox) handleMouseUp (button input.Button) { func (this *textBox) handleMouseUp (button input.Button) bool {
if button == input.ButtonLeft && this.selecting { if this.mouseButtonCanDrag(button) && this.selecting {
index := this.runeUnderMouse() index := this.runeUnderMouse()
this.selecting = false this.selecting = false
this.Select(text.Dot { Start: this.selectStart, End: index }) this.userSelect(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 { if this.selecting {
index := this.runeUnderMouse() index := this.runeUnderMouse()
this.Select(text.Dot { Start: this.selectStart, End: index }) this.userSelect(text.Dot { Start: this.selectStart, End: index })
} }
this.box.handleMouseMove() return this.box.handleMouseMove()
} }
func (this *textBox) runeUnderMouse () int { 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)) return this.drawer.AtPosition(fixPt(position))
} }
// TODO the keynav here should make better use of input key chords.
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()
moveVertically := func (delta fixed.Int26_6) {
currentDot := 0
if sel {
currentDot = dot.End
} else {
currentDot = dot.Canon().Start
if delta > fixed.I(0) { currentDot = dot.Canon().End }
}
nextDot := 0
if word {
if delta > fixed.I(0) {
nextDot = nextParagraph(this.runes, currentDot)
} else {
nextDot = previousParagraph(this.runes, currentDot)
}
} else {
currentPosition := this.drawer.PositionAt(currentDot)
if this.desiredX != fixed.I(0) {
currentPosition.X = this.desiredX
}
nextPosition := currentPosition
nextPosition.Y += this.lineHeight.Value().Mul(delta)
this.desiredX = nextPosition.X
nextDot = this.drawer.AtPosition(nextPosition)
}
if sel {
dot.End = nextDot
this.userSelectWithoutResettingDesiredX(dot)
} else {
this.userSelectWithoutResettingDesiredX(text.EmptyDot(nextDot))
}
}
switch {
case key == input.KeyHome || (modifiers.Alt() && key == input.KeyLeft):
if word {
dot.End = 0
} else {
dot.End = lineHomeSoft(this.runes, dot.End)
}
if !sel { dot.Start = dot.End }
this.userSelect(dot)
return true
case key == input.KeyEnd || (modifiers.Alt() && key == input.KeyRight):
if word {
dot.End = len(this.runes)
} else {
dot.End = lineEnd(this.runes, dot.End)
}
if !sel { dot.Start = dot.End }
this.userSelect(dot)
return true
case key == input.KeyLeft:
if sel {
this.userSelect(text.SelectLeft(this.runes, dot, word))
} else {
this.userSelect(text.MoveLeft(this.runes, dot, word))
}
return true
case key == input.KeyRight:
if sel {
this.userSelect(text.SelectRight(this.runes, dot, word))
} else {
this.userSelect(text.MoveRight(this.runes, dot, word))
}
return true
case key == input.KeyUp:
moveVertically(fixed.I(-1))
return true
case key == input.KeyDown:
moveVertically(fixed.I(1))
return true
case key == input.Key('a') && modifiers.Control():
dot.Start = 0
dot.End = len(this.runes)
this.userSelect(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.KeyUp:
return true
case key == input.KeyDown:
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 { func (this *textBox) normalizedLayoutBoundsSpace () image.Rectangle {
bounds := this.drawer.LayoutBoundsSpace() bounds := this.drawer.LayoutBoundsSpace()
return bounds.Sub(bounds.Min) return bounds.Sub(bounds.Min)
@ -272,10 +521,10 @@ func (this *textBox) normalizedLayoutBoundsSpace () image.Rectangle {
func (this *textBox) contentMinimum () image.Point { func (this *textBox) contentMinimum () image.Point {
minimum := this.drawer.MinimumSize() minimum := this.drawer.MinimumSize()
if this.hOverflow || this.wrap { if this.attrOverflow.Value().X || bool(this.attrWrap.Value()) {
minimum.X = this.drawer.Em().Round() minimum.X = this.drawer.Em().Round()
} }
if this.vOverflow { if this.attrOverflow.Value().Y {
minimum.Y = this.drawer.LineHeight().Round() minimum.Y = this.drawer.LineHeight().Round()
} }
@ -347,3 +596,104 @@ func (this *textBox) scrollToDot () {
this.ScrollTo(scroll) 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()
this.lineHeight.Invalidate()
}
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()
}
}
}
// TODO: these two functions really could be better.
func previousParagraph (text []rune, index int) int {
consecLF := 0
if index >= len(text) { index = len(text) - 1 }
for ; index > 0; index -- {
char := text[index]
if char == '\n' {
consecLF ++
} else if !unicode.IsSpace(char) {
if consecLF >= 2 { return index + 1 }
consecLF = 0
}
}
return index
}
func nextParagraph (text []rune, index int) int {
consecLF := 0
for ; index < len(text); index ++ {
char := text[index]
if char == '\n' {
consecLF ++
} else if !unicode.IsSpace(char) {
if consecLF >= 2 { return index }
consecLF = 0
}
}
return index
}
func lineHome (text []rune, index int) int {
liminal := index < len(text) && text[index] == '\n'
if index >= len(text) { index = len(text) - 1 }
for index := index; index >= 0; index -- {
char := text[index]
if char == '\n' && !liminal {
return index + 1
}
liminal = false
}
return 0
}
func lineHomeSoft (text []rune, index int) int {
home := lineHome(text, index)
start := home
for start < len(text) && unicode.IsSpace(text[start]) {
start ++
}
if index == start {
return home
} else {
return start
}
}
func lineEnd (text []rune, index int) int {
for ; index < len(text); index ++ {
char := text[index]
if char == '\n' {
return index
}
}
return index
}

View File

@ -1,101 +1,32 @@
package util package util
import "image/color" import "io"
// IndexOf returns the index of needle within haystack. If needle does not exist // Cycler stores a value and an accompanying io.Closer. When the value is set,
// within haystack, it returns -1. // the closer associated with the previous value is closed.
func IndexOf[T comparable] (haystack []T, needle T) int { type Cycler[T any] struct {
for index, test := range haystack { value T
if test == needle { closer io.Closer
return index }
}
// 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()
} }
return -1 this.value = value
this.closer = closer
return err
} }
// Remove removes an element from slice at index. // Close closes the associated closer early.
func Remove[T any] (slice []T, index int) []T { func (this *Cycler[T]) Close () error {
return append(slice[:index], slice[index + 1:]...) err := this.closer.Close()
} this.closer = nil
return err
// Insert inserts an element into slice at index.
func Insert[T any] (slice []T, index int, element T) []T {
slice = append(slice[:index + 1], slice[index:]...)
slice[index] = element
return slice
}
// Transparent returns whether or not a color has transparency.
func Transparent (c color.Color) bool {
_, _, _, a := c.RGBA()
return a != 0xFFFF
}
// Set is a set of unique items, built on top of map.
type Set[T comparable] map[T] struct { }
// Empty returns true if there are no items in the set.
func (set Set[T]) Empty () bool {
return set == nil || len(set) == 0
}
// Has returns true if the set contains item.
func (set Set[T]) Has (item T) bool {
if set == nil {
return false
}
_, ok := set[item]
return ok
}
// Add adds an item to the set.
func (set Set[T]) Add (item T) {
set[item] = struct { } { }
}
// Pop removes the first accessible item from the set and returns it.
func (set Set[T]) Pop () (item T) {
for item := range set {
delete(set, item)
return item
}
return
}
// Memo holds a cached value.
type Memo[T any] struct {
cache T
valid bool
update func () T
}
// NewMemo creates a new Memo which will take its value from the specified
// update callback.
func NewMemo[T any] (update func () T) Memo[T] {
return Memo[T] {
update: update,
}
}
// Value returns the Memo's value, updating it if the current cached value is
// invalid.
func (this *Memo[T]) Value () T {
if !this.valid {
this.cache = this.update()
}
return this.cache
}
// 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 () {
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
} }

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 "errors"
import "git.tebibyte.media/tomo/tomo" import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/xgbkb" 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/tomo/canvas"
import "git.tebibyte.media/tomo/backend/style"
import "git.tebibyte.media/tomo/backend/x/canvas" import "git.tebibyte.media/tomo/backend/x/canvas"
import "git.tebibyte.media/tomo/backend/internal/system" import "git.tebibyte.media/tomo/backend/internal/system"
@ -17,6 +19,9 @@ import "github.com/jezek/xgbutil/mousebind"
type Backend struct { type Backend struct {
x *xgbutil.XUtil x *xgbutil.XUtil
system *system.System system *system.System
style *style.Style
iconSet style.IconSet
doChannel chan func() doChannel chan func()
windows map[xproto.Window] *window windows map[xproto.Window] *window
@ -102,20 +107,20 @@ func (this *Backend) NewBox () tomo.Box {
return this.system.NewBox() return this.system.NewBox()
} }
func (this *Backend) NewCanvasBox () tomo.CanvasBox { func (this *Backend) NewTextBox () tomo.TextBox {
return this.system.NewCanvasBox() return this.system.NewTextBox()
} }
func (this *Backend) NewContainerBox () tomo.ContainerBox { func (this *Backend) NewCanvasBox () tomo.CanvasBox {
return this.system.NewContainerBox() return this.system.NewCanvasBox()
} }
func (this *Backend) NewSurfaceBox () (tomo.SurfaceBox, error) { func (this *Backend) NewSurfaceBox () (tomo.SurfaceBox, error) {
return this.system.NewSurfaceBox() return this.system.NewSurfaceBox()
} }
func (this *Backend) NewTextBox () tomo.TextBox { func (this *Backend) NewContainerBox () tomo.ContainerBox {
return this.system.NewTextBox() return this.system.NewContainerBox()
} }
func (this *Backend) NewTexture (source image.Image) canvas.TextureCloser { func (this *Backend) NewTexture (source image.Image) canvas.TextureCloser {
@ -126,11 +131,40 @@ func (this *Backend) NewCanvas (bounds image.Rectangle) canvas.CanvasCloser {
return xcanvas.NewCanvas(this.x, bounds) 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 () { func (this *Backend) assert () {
if this == nil { panic("x: nil backend") } if this == nil { panic("x: nil backend") }
} }
func (this *Backend) afterEvent () { func (this *Backend) afterEvent () {
for _, window := range this.windows { for _, window := range this.windows {
window.hierarchy.AfterEvent() window.hierarchy.AfterEvent()
} }

View File

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

View File

@ -7,6 +7,7 @@ import "github.com/jezek/xgb/xproto"
import "git.tebibyte.media/tomo/xgbkb" import "git.tebibyte.media/tomo/xgbkb"
import "github.com/jezek/xgbutil/xevent" import "github.com/jezek/xgbutil/xevent"
import "git.tebibyte.media/tomo/tomo/input" import "git.tebibyte.media/tomo/tomo/input"
import "git.tebibyte.media/tomo/tomo/config"
type scrollSum struct { type scrollSum struct {
x, y int x, y int
@ -145,12 +146,15 @@ func (window *window) updateBounds () {
// need to sum up all their positions. // need to sum up all their positions.
decorGeometry, _ := window.xWindow.DecorGeometry() decorGeometry, _ := window.xWindow.DecorGeometry()
windowGeometry, _ := window.xWindow.Geometry() windowGeometry, _ := window.xWindow.Geometry()
origin := image.Pt( origin := image.Pt (
windowGeometry.X() + decorGeometry.X(), decorGeometry.X(),
windowGeometry.Y() + decorGeometry.Y()) decorGeometry.Y())
window.metrics.bounds = image.Rectangle { innerOrigin := origin.Add(image.Pt (
Min: origin, windowGeometry.X(),
Max: origin.Add(image.Pt(windowGeometry.Width(), windowGeometry.Height())), windowGeometry.Y()))
window.metrics.innerBounds = image.Rectangle {
Min: innerOrigin,
Max: innerOrigin.Add(image.Pt(windowGeometry.Width(), windowGeometry.Height())),
} }
} }
@ -161,9 +165,9 @@ func (window *window) handleConfigureNotify (
configureEvent := *event.ConfigureNotifyEvent configureEvent := *event.ConfigureNotifyEvent
configureEvent = window.compressConfigureNotify(configureEvent) configureEvent = window.compressConfigureNotify(configureEvent)
oldBounds := window.metrics.bounds oldBounds := window.metrics.innerBounds
window.updateBounds() window.updateBounds()
newBounds := window.metrics.bounds newBounds := window.metrics.innerBounds
sizeChanged := sizeChanged :=
oldBounds.Dx() != newBounds.Dx() || oldBounds.Dx() != newBounds.Dx() ||
@ -223,7 +227,7 @@ func (window *window) handleKeyPress (
key, numberPad := keycodeToKey(keyEvent.Detail, keyEvent.State) key, numberPad := keycodeToKey(keyEvent.Detail, keyEvent.State)
window.updateModifiers(keyEvent.State) window.updateModifiers(keyEvent.State)
if key == input.KeyEscape && window.shy { if config.KeyChordClose.Pressed(key, window.Modifiers()) && window.shy {
window.Close() window.Close()
} else { } else {
window.hierarchy.HandleKeyDown(key, numberPad) window.hierarchy.HandleKeyDown(key, numberPad)
@ -430,14 +434,15 @@ func (window *window) compressMotionNotify (
func (window *window) updateModifiers (state uint16) { func (window *window) updateModifiers (state uint16) {
xModifiers := xgbkb.StateToModifiers(state) xModifiers := xgbkb.StateToModifiers(state)
window.hierarchy.HandleModifiers(input.Modifiers { var modifiers input.Modifiers
Shift: xModifiers.Shift || xModifiers.ShiftLock, if xModifiers.Shift { modifiers |= input.ModShift }
Control: xModifiers.Control, if xModifiers.ShiftLock { modifiers |= input.ModShift }
Alt: xModifiers.Alt, if xModifiers.Control { modifiers |= input.ModControl }
Meta: xModifiers.Meta, if xModifiers.Alt { modifiers |= input.ModAlt }
Super: xModifiers.Super, if xModifiers.Meta { modifiers |= input.ModMeta }
Hyper: xModifiers.Hyper, if xModifiers.Super { modifiers |= input.ModSuper }
}) if xModifiers.Hyper { modifiers |= input.ModHyper }
window.hierarchy.HandleModifiers(modifiers)
} }
func (window *window) updateMousePosition (x, y int16) { func (window *window) updateMousePosition (x, y int16) {

View File

@ -1,9 +1,11 @@
package x package x
import "image" import "image"
import "strings"
import "git.tebibyte.media/tomo/tomo" import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/data" 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/event"
import "git.tebibyte.media/tomo/tomo/canvas" import "git.tebibyte.media/tomo/tomo/canvas"
import "git.tebibyte.media/tomo/backend/x/canvas" import "git.tebibyte.media/tomo/backend/x/canvas"
@ -32,12 +34,18 @@ type window struct {
hasModal bool hasModal bool
shy bool shy bool
visible bool visible bool
resizeX bool
resizeY bool
metrics struct { metrics struct {
bounds image.Rectangle bounds image.Rectangle // bounds, including frame
innerBounds image.Rectangle // bounds of the drawable area
} }
onClose event.FuncBroadcaster on struct {
close event.FuncBroadcaster
tryClose event.Broadcaster[func () bool]
}
} }
type windowLink struct { type windowLink struct {
@ -60,27 +68,12 @@ func (this *windowLink) NotifyMinimumSizeChange () {
this.window.doMinimumSize() this.window.doMinimumSize()
} }
func (this *Backend) NewWindow ( func (this *Backend) NewWindow (kind tomo.WindowKind, bounds image.Rectangle) (tomo.Window, error) {
bounds image.Rectangle,
) (
output tomo.Window,
err error,
) {
this.assert() this.assert()
return this.newWindow(bounds, false) window, err := this.newWindow(bounds, kind == tomo.WindowKindMenu)
} if err != nil { return nil, err }
window.setKind(kind)
func (this *Backend) NewPlainWindow ( return window, nil
bounds image.Rectangle,
) (
output tomo.Window,
err error,
) {
this.assert()
window, err := this.newWindow(bounds, false)
window.setType("dock")
return window, err
} }
func (this *Backend) newWindow ( func (this *Backend) newWindow (
@ -97,6 +90,8 @@ func (this *Backend) newWindow (
link := &windowLink { window: window } link := &windowLink { window: window }
window.hierarchy = this.system.NewHierarchy(link) window.hierarchy = this.system.NewHierarchy(link)
window.leader = window window.leader = window
window.resizeX = true
window.resizeY = true
window.xWindow, err = xwindow.Generate(this.x) window.xWindow, err = xwindow.Generate(this.x)
if err != nil { return } if err != nil { return }
@ -125,7 +120,11 @@ func (this *Backend) newWindow (
if err != nil { return } if err != nil { return }
window.xWindow.WMGracefulClose (func (xWindow *xwindow.Window) { window.xWindow.WMGracefulClose (func (xWindow *xwindow.Window) {
window.Close() holdOff := false
for _, callback := range window.on.tryClose.Listeners() {
if !callback() { holdOff = true }
}
if !holdOff { window.Close() }
}) })
xevent.ExposeFun(window.handleExpose). xevent.ExposeFun(window.handleExpose).
@ -151,7 +150,7 @@ func (this *Backend) newWindow (
// xevent.SelectionRequestFun(window.handleSelectionRequest). // xevent.SelectionRequestFun(window.handleSelectionRequest).
// Connect(this.x, window.xWindow.Id) // Connect(this.x, window.xWindow.Id)
window.metrics.bounds = bounds window.metrics.innerBounds = bounds
window.doMinimumSize() window.doMinimumSize()
this.windows[window.xWindow.Id] = window this.windows[window.xWindow.Id] = window
@ -160,6 +159,14 @@ func (this *Backend) newWindow (
return return
} }
func (this *window) Bounds () image.Rectangle {
return this.metrics.bounds.Sub(this.metrics.innerBounds.Min)
}
func (this *window) InnerBounds () image.Rectangle {
return this.metrics.innerBounds.Sub(this.metrics.innerBounds.Min)
}
func (this *window) SetRoot (root tomo.Object) { func (this *window) SetRoot (root tomo.Object) {
if root == nil { if root == nil {
this.hierarchy.SetRoot(nil) this.hierarchy.SetRoot(nil)
@ -175,13 +182,21 @@ func (this *window) SetTitle (title string) {
icccm.WmIconNameSet (this.backend.x, this.xWindow.Id, title) icccm.WmIconNameSet (this.backend.x, this.xWindow.Id, title)
} }
func (this *window) SetIcon (sizes ...canvas.Texture) { 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 { } wmIcons := []ewmh.WmIcon { }
for _, icon := range sizes { for _, icon := range textures {
// TODO we use textures now. make this better icon := xcanvas.AssertTexture(icon)
width := icon.Bounds().Max.X
height := icon.Bounds().Max.Y bounds := icon.Bounds()
width := bounds.Dx()
height := bounds.Dy()
wmIcon := ewmh.WmIcon { wmIcon := ewmh.WmIcon {
Width: uint(width), Width: uint(width),
Height: uint(height), Height: uint(height),
@ -191,18 +206,14 @@ func (this *window) SetIcon (sizes ...canvas.Texture) {
// manually convert image data beacuse of course we have to do // manually convert image data beacuse of course we have to do
// this // this
index := 0 index := 0
for y := 0; y < height; y ++ { for y := bounds.Min.Y; y < bounds.Max.Y; y ++ {
for x := 0; x < width; x ++ { for x := bounds.Min.X; x < bounds.Max.X; x ++ {
r, g, b, a := icon.At(x, y).RGBA() pixel := icon.BGRAAt(x, y)
r >>= 8
g >>= 8
b >>= 8
a >>= 8
wmIcon.Data[index] = wmIcon.Data[index] =
(uint(a) << 24) | (uint(pixel.A) << 24) |
(uint(r) << 16) | (uint(pixel.R) << 16) |
(uint(g) << 8) | (uint(pixel.G) << 8) |
(uint(b) << 0) (uint(pixel.B) << 0)
index ++ index ++
}} }}
@ -215,59 +226,49 @@ func (this *window) SetIcon (sizes ...canvas.Texture) {
wmIcons) wmIcons)
} }
func (this *window) NewMenu (bounds image.Rectangle) (tomo.Window, error) { func (this *window) SetResizable (x, y bool) {
menu, err := this.backend.newWindow ( if this.resizeX == x && this.resizeY == y { return }
bounds.Add(this.metrics.bounds.Min), true) this.resizeX = x
menu.shy = true this.resizeY = y
icccm.WmTransientForSet ( this.doMinimumSize()
this.backend.x,
menu.xWindow.Id,
this.xWindow.Id)
menu.setType("POPUP_MENU")
// menu.inheritProperties(this)
return menu, err
} }
func (this *window) NewModal (bounds image.Rectangle) (tomo.Window, error) { func (this *window) SetBounds (bounds image.Rectangle) {
modal, err := this.backend.newWindow ( this.xWindow.WMMoveResize (
bounds.Add(this.metrics.bounds.Min), false) bounds.Min.X, bounds.Min.Y,
icccm.WmTransientForSet ( bounds.Min.X + bounds.Dx(),
this.backend.x, bounds.Min.Y + bounds.Dy())
modal.xWindow.Id,
this.xWindow.Id)
ewmh.WmStateSet (
this.backend.x,
modal.xWindow.Id,
[]string { "_NET_WM_STATE_MODAL" })
modal.modalParent = this
this.hasModal = true
// modal.inheritProperties(window)
return modal, err
} }
func (this *window) NewChild (bounds image.Rectangle) (tomo.Window, error) { func (this *window) NewChild (kind tomo.WindowKind, bounds image.Rectangle) (tomo.Window, error) {
leader := this.leader leader := this.leader
child, err := this.backend.newWindow ( child, err := this.backend.newWindow (
bounds.Add(this.metrics.bounds.Min), false) bounds.Add(this.metrics.innerBounds.Min), kind == tomo.WindowKindMenu)
child.leader = leader
if err != nil { return nil, err } if err != nil { return nil, err }
child.setClientLeader(leader) child.leader = leader
leader.setClientLeader(leader) err = child.setKind(kind)
if err != nil { return nil, err }
if kind == tomo.WindowKindModal {
this.hasModal = true
child.modalParent = this
}
icccm.WmTransientForSet ( icccm.WmTransientForSet (
this.backend.x, this.backend.x,
child.xWindow.Id, child.xWindow.Id,
leader.xWindow.Id) leader.xWindow.Id)
child.setType("UTILITY")
// child.inheritProperties(leader.window) // child.inheritProperties(leader.window)
return child, err return child, err
} }
func (this *window) Widget () (tomo.Window, error) { func (this *window) Modifiers () input.Modifiers {
// TODO return this.hierarchy.Modifiers()
return nil, nil }
func (this *window) MousePosition () image.Point {
return this.hierarchy.MousePosition()
} }
func (this *window) Copy (data.Data) { func (this *window) Copy (data.Data) {
@ -295,12 +296,12 @@ func (this *window) Visible () bool {
return this.visible return this.visible
} }
func (this *window) Close () { func (this *window) Close () error {
xevent .Detach(this.backend.x, this.xWindow.Id) xevent .Detach(this.backend.x, this.xWindow.Id)
keybind .Detach(this.backend.x, this.xWindow.Id) keybind .Detach(this.backend.x, this.xWindow.Id)
mousebind.Detach(this.backend.x, this.xWindow.Id) mousebind.Detach(this.backend.x, this.xWindow.Id)
this.onClose.Broadcast() this.on.close.Broadcast()
if this.modalParent != nil { if this.modalParent != nil {
// we are a modal dialog, so unlock the parent // we are a modal dialog, so unlock the parent
this.modalParent.hasModal = false this.modalParent.hasModal = false
@ -309,10 +310,16 @@ func (this *window) Close () {
this.SetRoot(nil) this.SetRoot(nil)
delete(this.backend.windows, this.xWindow.Id) delete(this.backend.windows, this.xWindow.Id)
this.xWindow.Destroy() this.xWindow.Destroy()
this.hierarchy.Close()
return nil // TODO maybe return an error? maybe join them?
} }
func (this *window) OnClose (callback func ()) event.Cookie { func (this *window) OnClose (callback func ()) event.Cookie {
return this.onClose.Connect(callback) return this.on.close.Connect(callback)
}
func (this *window) OnTryClose (callback func () bool) event.Cookie {
return this.on.tryClose.Connect(callback)
} }
func (this *window) grabInput () { func (this *window) grabInput () {
@ -328,6 +335,19 @@ func (this *window) ungrabInput () {
mousebind.UngrabPointer(this.backend.x) mousebind.UngrabPointer(this.backend.x)
} }
func (this *window) setKind (kind tomo.WindowKind) error {
err := this.setType(windowKindToType(kind))
if err != nil { return err }
if kind == tomo.WindowKindModal {
err = this.setState("MODAL")
if err != nil { return err }
}
if kind == tomo.WindowKindMenu {
this.shy = true
}
return nil
}
func (this *window) setType (ty string) error { func (this *window) setType (ty string) error {
return ewmh.WmWindowTypeSet ( return ewmh.WmWindowTypeSet (
this.backend.x, this.backend.x,
@ -335,6 +355,13 @@ func (this *window) setType (ty string) error {
[]string { "_NET_WM_WINDOW_TYPE_" + ty }) []string { "_NET_WM_WINDOW_TYPE_" + ty })
} }
func (this *window) setState (state string) error {
return ewmh.WmStateSet (
this.backend.x,
this.xWindow.Id,
[]string { "_NET_WM_STATE_" + state })
}
func (this *window) setClientLeader (leader *window) error { func (this *window) setClientLeader (leader *window) error {
hints, _ := icccm.WmHintsGet(this.backend.x, this.xWindow.Id) hints, _ := icccm.WmHintsGet(this.backend.x, this.xWindow.Id)
if hints == nil { if hints == nil {
@ -355,8 +382,8 @@ func (this *window) reallocateCanvas () {
previousHeight = this.xCanvas.Bounds().Dy() previousHeight = this.xCanvas.Bounds().Dy()
} }
newWidth := this.metrics.bounds.Dx() newWidth := this.metrics.innerBounds.Dx()
newHeight := this.metrics.bounds.Dy() newHeight := this.metrics.innerBounds.Dy()
larger := newWidth > previousWidth || newHeight > previousHeight larger := newWidth > previousWidth || newHeight > previousHeight
smaller := newWidth < previousWidth / 2 || newHeight < previousHeight / 2 smaller := newWidth < previousWidth / 2 || newHeight < previousHeight / 2
@ -376,7 +403,7 @@ func (this *window) reallocateCanvas () {
} }
this.hierarchy.SetCanvas(this.xCanvas.SubCanvas ( this.hierarchy.SetCanvas(this.xCanvas.SubCanvas (
this.metrics.bounds.Sub(this.metrics.bounds.Min))) this.metrics.innerBounds.Sub(this.metrics.innerBounds.Min)))
} }
func (this *window) pushAll () { func (this *window) pushAll () {
@ -407,20 +434,48 @@ func (this *window) doMinimumSize () {
if size.X < 8 { size.X = 8 } if size.X < 8 { size.X = 8 }
if size.Y < 8 { size.Y = 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 ( icccm.WmNormalHintsSet (
this.backend.x, this.backend.x,
this.xWindow.Id, this.xWindow.Id,
&icccm.NormalHints { &hints)
Flags: icccm.SizeHintPMinSize, newWidth := this.metrics.innerBounds.Dx()
MinWidth: uint(size.X), newHeight := this.metrics.innerBounds.Dy()
MinHeight: uint(size.Y),
})
newWidth := this.metrics.bounds.Dx()
newHeight := this.metrics.bounds.Dy()
if newWidth < size.X { newWidth = size.X } if newWidth < size.X { newWidth = size.X }
if newHeight < size.Y { newHeight = size.Y } if newHeight < size.Y { newHeight = size.Y }
if newWidth != this.metrics.bounds.Dx() || if newWidth != this.metrics.innerBounds.Dx() ||
newHeight != this.metrics.bounds.Dy() { newHeight != this.metrics.innerBounds.Dy() {
this.xWindow.Resize(newWidth, newHeight) this.xWindow.Resize(newWidth, newHeight)
} }
} }
func windowKindToType (kind tomo.WindowKind) string {
switch kind {
case tomo.WindowKindNormal: return "NORMAL"
case tomo.WindowKindPlain: return "DOCK"
case tomo.WindowKindUtility: return "UTILITY"
case tomo.WindowKindToolbar: return "MENU"
case tomo.WindowKindMenu: return "POPUP_MENU"
case tomo.WindowKindModal: return "NORMAL"
default: return strings.ReplaceAll(strings.ToUpper(string(kind)), " ", "_")
}
}