44 Commits

Author SHA1 Message Date
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
22 changed files with 776 additions and 174 deletions

2
go.mod
View File

@@ -3,7 +3,7 @@ module git.tebibyte.media/tomo/backend
go 1.20
require (
git.tebibyte.media/tomo/tomo v0.41.0
git.tebibyte.media/tomo/tomo v0.46.1
git.tebibyte.media/tomo/typeset v0.7.1
git.tebibyte.media/tomo/xgbkb v1.0.1
github.com/jezek/xgb v1.1.1

4
go.sum
View File

@@ -1,6 +1,6 @@
git.tebibyte.media/sashakoshka/xgbkb v1.0.0/go.mod h1:pNcE6TRO93vHd6q42SdwLSTTj25L0Yzggz7yLe0JV6Q=
git.tebibyte.media/tomo/tomo v0.41.0 h1:Z+7FHhbGiKjs+kQNvuJOfz47xIct5qxvSJqyDuoNIOs=
git.tebibyte.media/tomo/tomo v0.41.0/go.mod h1:C9EzepS9wjkTJjnZaPBh22YvVPyA4hbBAJVU20Rdmps=
git.tebibyte.media/tomo/tomo v0.46.1 h1:/8fT6I9l4TK529zokrThbNDHGRvUsNgif1Zs++0PBSQ=
git.tebibyte.media/tomo/tomo v0.46.1/go.mod h1:WrtilgKB1y8O2Yu7X4mYcRiqOlPR8NuUnoA/ynkQWrs=
git.tebibyte.media/tomo/typeset v0.7.1 h1:aZrsHwCG5ZB4f5CruRFsxLv5ezJUCFUFsQJJso2sXQ8=
git.tebibyte.media/tomo/typeset v0.7.1/go.mod h1:PwDpSdBF3l/EzoIsa2ME7QffVVajnTHZN6l3MHEGe1g=
git.tebibyte.media/tomo/xgbkb v1.0.1 h1:b3HDUopjdQp1MZrb5Vpil4bOtk3NnNXtfQW27Blw2kE=

View File

@@ -1,26 +1,48 @@
package system
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/backend/internal/util"
type attrHierarchy [T tomo.Attr] struct {
style T
user T
userExists bool
fallback T
style util.Optional[T]
user util.Optional[T]
}
func (this *attrHierarchy[T]) SetFallback (fallback T) {
this.fallback = fallback
}
func (this *attrHierarchy[T]) SetStyle (style T) (different bool) {
styleEquals := this.style.Equals(style)
this.style = style
return !styleEquals && !this.userExists
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 := this.user.Equals(user)
this.user = user
this.userExists = true
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)
@@ -29,10 +51,20 @@ func (this *attrHierarchy[T]) Set (attr T, user bool) (different bool) {
}
}
func (this *attrHierarchy[T]) Value () T {
if this.userExists {
return this.user
func (this *attrHierarchy[T]) Unset (user bool) (different bool) {
if user {
return this.UnsetUser()
} else {
return this.style
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

@@ -14,11 +14,11 @@ type box struct {
parent parent
outer anyBox
tags util.Set[string]
role tomo.Role
lastStyleNonce int
lastIconsNonce int
styleApplicator *styleApplicator
tags util.Set[string]
role tomo.Role
lastStyleNonce int
lastIconSetNonce int
styleApplicator *styleApplicator
minSize util.Memo[image.Point]
bounds image.Rectangle
@@ -41,7 +41,6 @@ type box struct {
focused bool
pressed bool
canvas util.Memo[canvas.Canvas]
drawer canvas.Drawer
@@ -69,12 +68,15 @@ func (this *System) newBox (outer anyBox) *box {
system: this,
outer: outer,
drawer: outer,
tags: make(util.Set[string]),
}
box.attrColor.SetFallback(tomo.AColor(color.Transparent))
box.canvas = util.NewMemo (func () canvas.Canvas {
if box.parent == nil { return nil }
parentCanvas := box.parent.getCanvas()
if parentCanvas == nil { return nil }
return parentCanvas.SubCanvas(box.bounds)
drawableArea := box.bounds.Intersect(box.parent.getInnerClippingBounds())
return parentCanvas.SubCanvas(drawableArea)
})
if outer == nil {
box.drawer = box
@@ -129,17 +131,25 @@ func (this *box) Tag (tag string) bool {
}
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) {
this.dndData = dat
}
@@ -200,18 +210,7 @@ func (this *box) setAttr (attr tomo.Attr, user bool) {
case tomo.AttrBorder:
previousBorderSum := this.borderSum()
different := this.attrBorder.Set(attr, user)
// only invalidate the layout if the border is sized differently
if this.borderSum() != previousBorderSum {
this.invalidateLayout()
this.invalidateMinimum()
}
// if the border takes up the same amount of space, only invalidate the
// drawing if it looks different
if different {
this.invalidateDraw()
}
this.handleBorderChange(previousBorderSum, different)
case tomo.AttrMinimumSize:
if this.attrMinimumSize.Set(attr, user) {
@@ -219,13 +218,66 @@ func (this *box) setAttr (attr tomo.Attr, user bool) {
}
case tomo.AttrPadding:
if this.attrPadding.Set(attr, true) {
if this.attrPadding.Set(attr, user) {
this.invalidateLayout()
this.invalidateMinimum()
}
}
}
func (this *box) unsetAttr (kind tomo.AttrKind, user bool) {
switch kind {
case tomo.AttrKindColor:
if this.attrColor.Unset(user) {
this.invalidateDraw()
}
case tomo.AttrKindTexture:
if this.attrTexture.Unset(user) {
this.invalidateDraw()
}
case tomo.AttrKindTextureMode:
if this.attrTextureMode.Unset(user) {
this.invalidateDraw()
}
case tomo.AttrKindBorder:
previousBorderSum := this.borderSum()
different := this.attrBorder.Unset(user)
this.handleBorderChange(previousBorderSum, different)
case tomo.AttrKindMinimumSize:
if this.attrMinimumSize.Unset(user) {
this.invalidateMinimum()
}
case tomo.AttrKindPadding:
if this.attrPadding.Unset(user) {
this.invalidateLayout()
this.invalidateMinimum()
}
}
}
func (this *box) unsetAllAttrs (user bool) {
// keep this in sync with tomo.AttrKind!
this.outer.unsetAttr(tomo.AttrKindColor, user)
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
@@ -335,13 +387,6 @@ func (this *box) handleMouseDown (button input.Button) (caught bool) {
this.invalidateStyle()
}
if this.focusable {
this.SetFocused(true)
} else {
hierarchy := this.getHierarchy()
if hierarchy == nil { return }
hierarchy.focus(nil)
}
for _, listener := range this.on.buttonDown.Listeners() {
if listener(button) { caught = true }
}
@@ -401,22 +446,28 @@ func (this *box) Draw (can canvas.Canvas) {
// centered texture
if textureMode == tomo.TextureModeCenter && texture != nil {
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))
this.centeredTexture(can, texture)
}
}
func (this *box) centeredTexture (can canvas.Canvas, texture canvas.Texture) {
pen := can.Pen()
bounds := this.Bounds()
textureBounds := texture.Bounds()
textureOrigin :=
bounds.Min.
Add(image.Pt (
bounds.Dx() / 2,
bounds.Dy() / 2)).
Sub(image.Pt (
textureBounds.Dx() / 2,
textureBounds.Dy() / 2))
pen.Fill(color.Transparent)
pen.Texture(texture)
pen.Rectangle(textureBounds.Sub(textureBounds.Min).Add(textureOrigin))
}
func (this *box) drawBorders (can canvas.Canvas) {
if can == nil { return }
pen := can.Pen()
@@ -483,9 +534,6 @@ func (this *box) calculateMinimumSize () image.Point {
minSize.Y = userMinSize.Y
}
if this.parent != nil {
this.parent.notifyMinimumSizeChange(this)
}
return minSize
}
@@ -512,7 +560,7 @@ func (this *box) doLayout () {
}
func (this *box) doStyle () {
this.styleApplicator.apply(this)
this.styleApplicator.apply(this.outer)
}
func (this *box) setParent (parent parent) {
@@ -540,7 +588,21 @@ func (this *box) recursiveRedo () {
}
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 () {
@@ -563,10 +625,14 @@ func (this *box) invalidateDraw () {
func (this *box) invalidateMinimum () {
this.minSize.Invalidate()
if this.parent != nil {
this.parent.notifyMinimumSizeChange(this)
}
}
func (this *box) recursiveReApply () {
if this.getHierarchy() == nil { return }
hierarchy := this.getHierarchy()
if hierarchy == nil { return }
// re-apply styling, icons *if needed*
@@ -577,16 +643,17 @@ func (this *box) recursiveReApply () {
// 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 = this.getHierarchy().newStyleApplicator()
this.styleApplicator = hierarchy.newStyleApplicator()
this.invalidateStyle()
this.on.styleChange.Broadcast()
}
// icons
hierarchyIconsNonce := this.getIconsNonce()
if this.lastIconsNonce != hierarchyIconsNonce {
this.lastIconsNonce = hierarchyIconsNonce
hierarchyIconSetNonce := this.getIconSetNonce()
if this.lastIconSetNonce != hierarchyIconSetNonce {
this.lastIconSetNonce = hierarchyIconSetNonce
this.on.iconSetChange.Broadcast()
}
}
@@ -614,7 +681,8 @@ func (this *box) propagateAlt (callback func (anyBox) bool) bool {
func (this *box) transparent () bool {
// TODO uncomment once we have
// a way to detect texture transparency
return util.Transparent(this.attrColor.Value()) /*&&
col := this.attrColor.Value().Color
return col == nil || util.Transparent(col) /*&&
(this.texture == nil || !this.texture.Opaque())*/
}
@@ -634,7 +702,7 @@ func (this *box) getStyleNonce () int {
return this.getHierarchy().getStyleNonce()
}
func (this *box) getIconsNonce () int {
func (this *box) getIconSetNonce () int {
// should panic if not in the tree
return this.getHierarchy().getIconsNonce()
return this.getHierarchy().getIconSetNonce()
}

View File

@@ -20,6 +20,7 @@ func (querier boxQuerier) RecommendedWidth (index int, height int) int {
if box, ok := box.(anyContentBox); ok {
return box.recommendedWidth(height)
}
return box.minimumSize().X
}
return 0
}
@@ -29,6 +30,7 @@ func (querier boxQuerier) RecommendedHeight (index int, width int) int {
if box, ok := box.(anyContentBox); ok {
return box.recommendedHeight(width)
}
return box.minimumSize().Y
}
return 0
}

View File

@@ -20,7 +20,6 @@ type containerBox struct {
attrLayout attrHierarchy[tomo.AttrLayout]
children []anyBox
layout tomo.Layout
on struct {
contentBoundsChange event.FuncBroadcaster
@@ -174,6 +173,7 @@ func (this *containerBox) setAttr (attr tomo.Attr, user bool) {
case tomo.AttrOverflow:
if this.attrOverflow.Set(attr, user) {
this.invalidateLayout()
this.invalidateMinimum()
}
case tomo.AttrLayout:
@@ -186,20 +186,69 @@ func (this *containerBox) setAttr (attr tomo.Attr, user bool) {
}
}
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 {
if this.layout == nil || this.attrOverflow.Value().Y {
layout := this.attrLayout.Value().Layout
if layout == nil || !this.attrOverflow.Value().Y {
return this.minSize.Value().Y
} else {
return this.layout.RecommendedHeight(this.layoutHints(), this.boxQuerier(), width) +
return layout.RecommendedHeight(this.layoutHints(), this.boxQuerier(), width) +
this.borderAndPaddingSum().Vertical()
}
}
func (this *containerBox) recommendedWidth (height int) int {
if this.layout == nil || this.attrOverflow.Value().X {
layout := this.attrLayout.Value().Layout
if layout == nil || !this.attrOverflow.Value().X {
return this.minSize.Value().X
} else {
return this.layout.RecommendedWidth(this.layoutHints(), this.boxQuerier(), height) +
return layout.RecommendedWidth(this.layoutHints(), this.boxQuerier(), height) +
this.borderAndPaddingSum().Horizontal()
}
}
@@ -249,6 +298,10 @@ func (this *containerBox) getCanvas () canvas.Canvas {
return this.canvas.Value()
}
func (this *containerBox) getInnerClippingBounds () image.Rectangle {
return this.innerClippingBounds
}
func (this *containerBox) notifyMinimumSizeChange (child anyBox) {
this.invalidateMinimum()
size := child.minimumSize()
@@ -274,8 +327,9 @@ func (this *containerBox) layoutHints () tomo.LayoutHints {
func (this *containerBox) contentMinimum () image.Point {
overflow := this.attrOverflow.Value()
minimum := this.box.contentMinimum()
if this.layout != nil {
layoutMinimum := this.layout.MinimumSize (
layout := this.attrLayout.Value().Layout
if layout != nil {
layoutMinimum := layout.MinimumSize (
this.layoutHints(),
this.boxQuerier())
if overflow.X { layoutMinimum.X = 0 }
@@ -288,12 +342,13 @@ func (this *containerBox) contentMinimum () image.Point {
func (this *containerBox) doLayout () {
this.box.doLayout()
previousContentBounds := this.contentBounds
layout := this.attrLayout.Value().Layout
// by default, use innerBounds (translated to 0, 0) for contentBounds.
// if a direction overflows, use the layout's minimum size for it.
var minimum image.Point
if this.layout != nil {
minimum = this.layout.MinimumSize (
if layout != nil {
minimum = layout.MinimumSize (
this.layoutHints(),
this.boxQuerier())
}
@@ -304,10 +359,10 @@ func (this *containerBox) doLayout () {
if overflow.Y { this.contentBounds.Max.Y = this.contentBounds.Min.Y + minimum.Y }
// arrange children
if this.layout != nil {
if layout != nil {
layoutHints := this.layoutHints()
layoutHints.Bounds = this.contentBounds
this.layout.Arrange(layoutHints, this.boxArranger())
layout.Arrange(layoutHints, this.boxArranger())
}
// build an accurate contentBounds by unioning the bounds of all child
@@ -323,7 +378,7 @@ func (this *containerBox) doLayout () {
// offset children and contentBounds by scroll
for _, box := range this.children {
assertAnyBox(box).setBounds(box.Bounds().Add(this.scroll).Add(innerBounds.Min))
box.setBounds(box.Bounds().Add(this.scroll).Add(innerBounds.Min))
}
this.contentBounds = this.contentBounds.Add(this.scroll)

View File

@@ -3,7 +3,6 @@ package system
import "image"
import "git.tebibyte.media/tomo/tomo/input"
// TODO: redo all of this because there are new event propogation rules
// TODO: once go v1.23 comes out, replace the explicit iterator calls here with
// range loops
@@ -25,21 +24,30 @@ func (this *Hierarchy) HandleModifiers (modifiers input.Modifiers) {
// HandleModifiers must be called *before* HandleKeyDown.
func (this *Hierarchy) HandleKeyDown (key input.Key, numberPad bool) {
caught := false
this.keyboardTargets(func (target anyBox) bool {
if target.handleKeyDown(key, numberPad) {
caught = true
return false
if this.anyFocused() {
this.keyboardTargets(func (target anyBox) bool {
if target.handleKeyDown(key, numberPad) {
caught = true
return false
}
return true
})
} else {
if this.root != nil {
caught = this.root.handleKeyDown(key, numberPad)
}
return true
})
}
if caught { return }
if key == input.KeyTab && this.modifiers.Alt {
switch key {
case input.KeyTab:
if this.modifiers.Shift {
this.focusPrevious()
} else {
this.focusNext()
}
case input.KeyUp: this.focusPrevious()
case input.KeyDown: this.focusNext()
}
}
@@ -47,12 +55,18 @@ func (this *Hierarchy) HandleKeyDown (key input.Key, numberPad bool) {
// which triggers this comes with modifier key information, HandleModifiers must
// be called *before* HandleKeyUp.
func (this *Hierarchy) HandleKeyUp (key input.Key, numberPad bool) {
this.keyboardTargets(func (target anyBox) bool {
if target.handleKeyUp(key, numberPad) {
return false
if this.anyFocused() {
this.keyboardTargets(func (target anyBox) bool {
if target.handleKeyUp(key, numberPad) {
return false
}
return true
})
} else {
if this.root != nil {
this.root.handleKeyUp(key, numberPad)
}
return true
})
}
}
// HandleMouseDown sends a mouse down event to the Boxes positioned underneath
@@ -62,7 +76,16 @@ func (this *Hierarchy) HandleKeyUp (key input.Key, numberPad bool) {
// information, HandleMouseMove must be called *before* HandleMouseDown.
func (this *Hierarchy) HandleMouseDown (button input.Button) {
boxes := []anyBox { }
first := true
this.boxesUnder(this.mousePosition)(func (box anyBox) bool {
if first {
if box.canBeFocused() {
this.focus(box)
} else {
this.focus(nil)
}
first = false
}
boxes = append(boxes, box)
return !box.handleMouseDown(button)
})
@@ -92,17 +115,18 @@ func (this *Hierarchy) HandleMouseMove (position image.Point) {
for _, dragSet := range this.drags {
for _, box := range dragSet {
if box.handleMouseMove() { break }
dragged = true
}
dragged = true
}
if dragged { return }
// TODO we can hover over multiple boxes at once. however, any way of
// detecting this involves several slice allocations every time we
// process a MouseMove event. perhaps we just ought to suck it up and do
// it.
// 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()
}

View File

@@ -4,6 +4,7 @@ import "image"
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/input"
import "git.tebibyte.media/tomo/tomo/canvas"
import "git.tebibyte.media/tomo/backend/style"
import "git.tebibyte.media/tomo/backend/internal/util"
// Hierarchy is coupled to a tomo.Window implementation, and manages a tree of
@@ -168,22 +169,34 @@ func (this *Hierarchy) getWindow () tomo.Window {
return this.link.GetWindow()
}
func (this *Hierarchy) getStyle () *tomo.Style {
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) getIconsNonce () int {
return this.system.iconsNonce
func (this *Hierarchy) getIconSetNonce () int {
return this.system.iconSetNonce
}
func (this *Hierarchy) getCanvas () canvas.Canvas {
return this.canvas
}
func (this *Hierarchy) getInnerClippingBounds () image.Rectangle {
return this.canvas.Bounds()
}
func (this *Hierarchy) getModifiers () input.Modifiers {
return this.modifiers
}
@@ -287,13 +300,26 @@ func (this *Hierarchy) considerMaskingParents (box anyBox) anyBox {
return box
}
func (this *Hierarchy) isMasked (box anyBox) bool {
parent := box.getParent()
for {
parentBox, ok := parent.(anyBox)
if !ok { break }
if parent.masks() {
return true
}
parent = parentBox.getParent()
}
return false
}
func (this *Hierarchy) focusNext () {
found := !this.anyFocused()
focused := false
this.propagateAlt(func (box anyBox) bool {
if found {
// looking for the next box to select
if box.canBeFocused() {
if box.canBeFocused() && !this.isMasked(box) {
// found it
this.focus(box)
focused = true
@@ -318,7 +344,7 @@ func (this *Hierarchy) focusPrevious () {
if box == this.focused {
return false
}
if box.canBeFocused() { behind = box }
if box.canBeFocused() && !this.isMasked(box) { behind = box }
return true
})
this.focus(behind)
@@ -341,15 +367,21 @@ func (this *Hierarchy) drawBackgroundPart (canvas.Canvas) {
// if so, windows should be transparent if the color has transparency
}
// var minimumSizeCount = 0
func (this *Hierarchy) doMinimumSize () {
this.minimumClean = true
// println("doMinimumSize", minimumSizeCount)
// minimumSizeCount ++
previousMinimumSize := this.minimumSize
this.minimumSize = image.Point { }
if this.root != nil {
this.minimumSize = this.root.minimumSize()
}
this.link.NotifyMinimumSizeChange()
if previousMinimumSize != this.minimumSize {
this.link.NotifyMinimumSizeChange()
}
}
func (this *Hierarchy) newStyleApplicator () *styleApplicator {

View File

@@ -12,6 +12,9 @@ type parent interface {
getHierarchy () *Hierarchy
// canvas returns the canvas held by the parent.
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
// one of its children has changed.
notifyMinimumSizeChange (anyBox)
@@ -75,6 +78,9 @@ type anyBox interface {
// setAttr sets an attribute at the user or style level depending
// on the value of user.
setAttr (attr tomo.Attr, user bool)
// unsetAttr unsets an attribute at the user or style level depending
// on the value of user.
unsetAttr (kind tomo.AttrKind, user bool)
// propagate recursively calls a function on this anyBox, and all of its
// children (if applicable) The normal propagate behavior calls the

View File

@@ -1,30 +1,33 @@
package system
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/backend/style"
type styleApplicator struct {
style *tomo.Style
role tomo.Role
rules []*tomo.Rule
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 = make([]*tomo.Rule, 4)
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)
this.rules = append(this.rules, rule)
}
}
}
// compile list of attributes by searching through the cached ruleset
attrs := make(tomo.AttrSet)
attrs := make(style.AttrSet)
for _, rule := range this.rules {
satisifed := true
for _, tag := range rule.Tags {
@@ -39,7 +42,18 @@ func (this *styleApplicator) apply (box anyBox) {
}
}
// 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

@@ -2,8 +2,8 @@ package system
import "io"
import "image"
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/canvas"
import "git.tebibyte.media/tomo/backend/style"
import "git.tebibyte.media/tomo/backend/internal/util"
// System is coupled to a tomo.Backend implementation, and manages Hierarchies
@@ -11,9 +11,11 @@ import "git.tebibyte.media/tomo/backend/internal/util"
type System struct {
link BackendLink
style *tomo.Style
styleNonce int
iconsNonce int
style *style.Style
iconSet style.IconSet
faceSet style.FaceSet
styleNonce int
iconSetNonce int
hierarchies util.Set[*Hierarchy]
}
@@ -43,9 +45,9 @@ func New (link BackendLink) *System {
}
}
// SetStyle sets the tomo.Style that is applied to objects, and notifies them
// SetStyle sets the style that is applied to objects, and notifies them
// that the style has changed.
func (this *System) SetStyle (style *tomo.Style) {
func (this *System) SetStyle (style *style.Style) {
this.style = style
this.styleNonce ++
for hierarchy := range this.hierarchies {
@@ -53,14 +55,21 @@ func (this *System) SetStyle (style *tomo.Style) {
}
}
// SetIconSet notifies objects that the icons have changed.
func (this *System) SetIconSet (iconSet tomo.IconSet) {
this.iconsNonce ++
// 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

@@ -2,6 +2,7 @@ package system
import "image"
import "image/color"
import "golang.org/x/image/font"
import "git.tebibyte.media/tomo/tomo"
import "golang.org/x/image/math/fixed"
import "git.tebibyte.media/tomo/typeset"
@@ -9,6 +10,7 @@ import "git.tebibyte.media/tomo/tomo/text"
import "git.tebibyte.media/tomo/tomo/input"
import "git.tebibyte.media/tomo/tomo/event"
import "git.tebibyte.media/tomo/tomo/canvas"
import "git.tebibyte.media/tomo/backend/internal/util"
type textBox struct {
*box
@@ -24,12 +26,14 @@ type textBox struct {
attrOverflow attrHierarchy[tomo.AttrOverflow]
text string
runes []rune
selectable bool
selecting bool
selectStart int
dot text.Dot
drawer typeset.Drawer
face util.Cycler[font.Face]
on struct {
contentBoundsChange event.FuncBroadcaster
@@ -40,6 +44,8 @@ type textBox struct {
func (this *System) NewTextBox () tomo.TextBox {
box := &textBox { }
box.box = this.newBox(box)
box.attrTextColor.SetFallback(tomo.ATextColor(color.Black))
box.attrDotColor.SetFallback(tomo.ADotColor(color.RGBA { G: 255, B: 255, A: 255}))
return box
}
@@ -70,8 +76,9 @@ func (this *textBox) OnContentBoundsChange (callback func()) event.Cookie {
func (this *textBox) SetText (text string) {
if this.text == text { return }
this.text = text
this.drawer.SetText([]rune(text))
this.text = text
this.runes = []rune(text)
this.drawer.SetText(this.runes)
this.invalidateMinimum()
this.invalidateLayout()
}
@@ -104,15 +111,14 @@ func (this *textBox) OnDotChange (callback func ()) event.Cookie {
func (this *textBox) Draw (can canvas.Canvas) {
if can == nil { return }
texture := this.attrTexture.Value().Texture
col := this.attrColor.Value().Color
if col == nil { col = color.Transparent }
texture := this.attrTexture.Value().Texture
col := this.attrColor.Value().Color
this.drawBorders(can)
pen := can.Pen()
pen.Fill(col)
pen.Texture(texture)
if this.transparent() && this.parent != nil {
this.parent.drawBackgroundPart(can)
}
@@ -122,9 +128,8 @@ func (this *textBox) Draw (can canvas.Canvas) {
this.drawDot(can)
}
if this.attrFace.Value().Face != nil {
if this.face.Value() != nil {
textColor := this.attrTextColor.Value().Color
if textColor == nil { textColor = color.Black }
this.drawer.Draw(can, textColor, this.textOffset())
}
}
@@ -143,24 +148,28 @@ func (this *textBox) setAttr (attr tomo.Attr, user bool) {
case tomo.AttrFace:
if this.attrFace.Set(attr, user) {
this.drawer.SetFace(attr.Face)
this.invalidateMinimum()
this.invalidateLayout()
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()
}
@@ -168,6 +177,49 @@ func (this *textBox) setAttr (attr tomo.Attr, user bool) {
}
}
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 {
return image.Pt(point.X.Round(), point.Y.Round())
}
@@ -177,17 +229,13 @@ func fixPt (point image.Point) fixed.Point26_6 {
}
func (this *textBox) drawDot (can canvas.Canvas) {
if this.attrFace.Value().Face == nil { return }
face := this.face.Value()
if face == nil { return }
face := this.attrFace.Value().Face
textColor := this.attrTextColor.Value().Color
dotColor := this.attrDotColor.Value().Color
if textColor == nil { textColor = color.Transparent }
if dotColor == nil { dotColor = color.RGBA { G: 255, B: 255, A: 255 } }
pen := can.Pen()
pen.Fill(color.Transparent)
pen.Stroke(textColor)
bounds := this.InnerBounds()
metrics := face.Metrics()
@@ -200,6 +248,7 @@ func (this *textBox) drawDot (can canvas.Canvas) {
switch {
case dot.Empty():
pen.Stroke(textColor)
pen.StrokeWeight(1)
pen.Path(roundPt(start.Add(ascent)), roundPt(start.Sub(descent)))
@@ -243,14 +292,18 @@ func (this *textBox) textOffset () image.Point {
Sub(this.drawer.LayoutBoundsSpace().Min)
}
func (this *textBox) handleFocusEnter () {
this.invalidateDraw()
this.box.handleFocusEnter()
}
func (this *textBox) handleFocusLeave () {
this.on.dotChange.Broadcast()
this.invalidateDraw()
this.box.handleFocusLeave()
}
func (this *textBox) handleMouseDown (button input.Button) bool {
if button == input.ButtonLeft {
if this.mouseButtonCanDrag(button) {
index := this.runeUnderMouse()
this.selectStart = index
this.selecting = true
@@ -260,7 +313,7 @@ func (this *textBox) handleMouseDown (button input.Button) bool {
}
func (this *textBox) handleMouseUp (button input.Button) bool {
if button == input.ButtonLeft && this.selecting {
if this.mouseButtonCanDrag(button) && this.selecting {
index := this.runeUnderMouse()
this.selecting = false
this.Select(text.Dot { Start: this.selectStart, End: index })
@@ -268,6 +321,12 @@ func (this *textBox) handleMouseUp (button input.Button) bool {
return this.box.handleMouseUp(button)
}
func (this *textBox) mouseButtonCanDrag (button input.Button) bool {
return button == input.ButtonLeft ||
button == input.ButtonMiddle ||
button == input.ButtonRight
}
func (this *textBox) handleMouseMove () bool {
if this.selecting {
index := this.runeUnderMouse()
@@ -283,6 +342,71 @@ func (this *textBox) runeUnderMouse () int {
return this.drawer.AtPosition(fixPt(position))
}
func (this *textBox) handleKeyDown (key input.Key, numberPad bool) bool {
if this.box.handleKeyDown(key, numberPad) { return true }
if !this.selectable { return false }
// because fuck you thats why!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
modifiers := this.Window().Modifiers()
dot := this.Dot()
sel := modifiers.Shift
word := modifiers.Control
switch {
case key == input.KeyHome || (modifiers.Alt && key == input.KeyLeft):
dot.End = 0
if !sel { dot.Start = dot.End }
this.Select(dot)
return true
case key == input.KeyEnd || (modifiers.Alt && key == input.KeyRight):
dot.End = len(this.text)
if !sel { dot.Start = dot.End }
this.Select(dot)
return true
case key == input.KeyLeft:
if sel {
this.Select(text.SelectLeft(this.runes, dot, word))
} else {
this.Select(text.MoveLeft(this.runes, dot, word))
}
return true
case key == input.KeyRight:
if sel {
this.Select(text.SelectRight(this.runes, dot, word))
} else {
this.Select(text.MoveRight(this.runes, dot, word))
}
return true
case key == input.Key('a') && modifiers.Control:
dot.Start = 0
dot.End = len(this.text)
this.Select(dot)
return true
default:
return false
}
}
func (this *textBox) handleKeyUp (key input.Key, numberPad bool) bool {
if this.box.handleKeyUp(key, numberPad) { return true }
if !this.selectable { return false }
modifiers := this.Window().Modifiers()
switch {
case key == input.KeyHome || (modifiers.Alt && key == input.KeyLeft):
return true
case key == input.KeyEnd || (modifiers.Alt && key == input.KeyRight):
return true
case key == input.KeyLeft:
return true
case key == input.KeyRight:
return true
case key == input.Key('a') && modifiers.Control:
return true
default:
return false
}
}
func (this *textBox) normalizedLayoutBoundsSpace () image.Rectangle {
bounds := this.drawer.LayoutBoundsSpace()
return bounds.Sub(bounds.Min)
@@ -366,3 +490,36 @@ func (this *textBox) scrollToDot () {
this.ScrollTo(scroll)
}
func (this *textBox) handleFaceChange () {
hierarchy := this.getHierarchy()
if hierarchy == nil { return }
faceSet := hierarchy.getFaceSet()
if faceSet == nil { return }
face := faceSet.Face(tomo.Face(this.attrFace.Value()))
this.face.Set(face, face)
this.drawer.SetFace(face)
this.invalidateMinimum()
this.invalidateLayout()
}
func (this *textBox) recursiveReApply () {
this.box.recursiveReApply()
hierarchy := this.getHierarchy()
if hierarchy == nil { return }
previousFace := this.face.Value()
if previousFace == nil {
faceSet := hierarchy.getFaceSet()
if faceSet == nil { return }
face := faceSet.Face(tomo.Face(this.attrFace.Value()))
if face != previousFace {
this.face.Set(face, face)
this.drawer.SetFace(face)
this.invalidateMinimum()
this.invalidateLayout()
}
}
}

View File

@@ -83,6 +83,7 @@ func NewMemo[T any] (update func () T) Memo[T] {
func (this *Memo[T]) Value () T {
if !this.valid {
this.cache = this.update()
this.valid = true
}
return this.cache
}
@@ -90,17 +91,11 @@ func (this *Memo[T]) Value () T {
// Invalidate marks the Memo's value as invalid, which will cause it to be
// updated the next time Value is called.
func (this *Memo[T]) Invalidate () {
var zero T
this.cache = zero
this.valid = false
}
// InvalidateTo invalidates the Memo and sets its value. The new value will be
// entirely inaccessible. This is only intended to be used for setting a
// reference to nil
func (this *Memo[T]) InvalidateTo (value T) {
this.Invalidate()
this.cache = value
}
// Cycler stores a value and an accompanying io.Closer. When the value is set,
// the closer associated with the previous value is closed.
type Cycler[T any] struct {
@@ -129,3 +124,33 @@ func (this *Cycler[T]) Close () error {
this.closer = nil
return err
}
// Optional is an optional value.
type Optional[T any] struct {
value T
exists bool
}
// Value returns the value and true if the value exists. If not, it returns the
// last set value and false.
func (this *Optional[T]) Value () (T, bool) {
return this.value, this.exists
}
// Set sets the value.
func (this *Optional[T]) Set (value T) {
this.value = value
this.exists = true
}
// Unset unsets the value.
func (this *Optional[T]) Unset () {
var zero T
this.value = zero
this.exists = false
}
// Exists returns if the value is currently set.
func (this *Optional[T]) Exists () bool {
return this.exists
}

13
style/faceset.go Normal file
View File

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

28
style/iconset.go Normal file
View File

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

70
style/style.go Normal file
View File

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

View File

@@ -4,7 +4,9 @@ import "image"
import "errors"
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/xgbkb"
import "git.tebibyte.media/tomo/tomo/data"
import "git.tebibyte.media/tomo/tomo/canvas"
import "git.tebibyte.media/tomo/backend/style"
import "git.tebibyte.media/tomo/backend/x/canvas"
import "git.tebibyte.media/tomo/backend/internal/system"
@@ -17,6 +19,9 @@ import "github.com/jezek/xgbutil/mousebind"
type Backend struct {
x *xgbutil.XUtil
system *system.System
style *style.Style
iconSet style.IconSet
doChannel chan func()
windows map[xproto.Window] *window
@@ -126,19 +131,40 @@ func (this *Backend) NewCanvas (bounds image.Rectangle) canvas.CanvasCloser {
return xcanvas.NewCanvas(this.x, bounds)
}
func (this *Backend) SetStyle (style *tomo.Style) {
func (this *Backend) 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 (icons tomo.IconSet) {
this.system.SetIconSet(icons)
func (this *Backend) SetIconSet (iconSet style.IconSet) {
this.iconSet = iconSet
this.system.SetIconSet(iconSet)
}
func (this *Backend) SetFaceSet (faceSet style.FaceSet) {
this.system.SetFaceSet(faceSet)
}
func (this *Backend) assert () {
if this == nil { panic("x: nil backend") }
}
func (this *Backend) afterEvent () {
func (this *Backend) afterEvent () {
for _, window := range this.windows {
window.hierarchy.AfterEvent()
}

View File

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

View File

@@ -46,7 +46,7 @@ func (this *pen) textureRectangleTransparent (bounds image.Rectangle) {
srcPos := pos.Add(offset)
dstIndex := this.image.PixOffset(pos.X, pos.Y)
srcIndex := this.texture.PixOffset(srcPos.X, srcPos.Y)
pixel := xgraphics.BlendBGRA(xgraphics.BGRA {
pixel := blendPremultipliedBGRA(xgraphics.BGRA {
B: dst[dstIndex + 0],
G: dst[dstIndex + 1],
R: dst[dstIndex + 2],
@@ -93,7 +93,7 @@ func (this *pen) fillRectangleTransparent (c xgraphics.BGRA, bounds image.Rectan
for pos.Y = bounds.Min.Y; pos.Y < bounds.Max.Y; pos.Y ++ {
for pos.X = bounds.Min.X; pos.X < bounds.Max.X; pos.X ++ {
index := this.image.PixOffset(pos.X, pos.Y)
pixel := xgraphics.BlendBGRA(xgraphics.BGRA {
pixel := blendPremultipliedBGRA(xgraphics.BGRA {
B: this.image.Pix[index + 0],
G: this.image.Pix[index + 1],
R: this.image.Pix[index + 2],
@@ -256,7 +256,7 @@ func (context *fillingContext) fillPolygonHotTransparent () {
// fill pixels in between
for x := left; x < right; x ++ {
index := context.image.PixOffset(x, context.y)
pixel := xgraphics.BlendBGRA(xgraphics.BGRA {
pixel := blendPremultipliedBGRA(xgraphics.BGRA {
B: context.image.Pix[index + 0],
G: context.image.Pix[index + 1],
R: context.image.Pix[index + 2],

View File

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

View File

@@ -145,12 +145,15 @@ func (window *window) updateBounds () {
// need to sum up all their positions.
decorGeometry, _ := window.xWindow.DecorGeometry()
windowGeometry, _ := window.xWindow.Geometry()
origin := image.Pt(
windowGeometry.X() + decorGeometry.X(),
windowGeometry.Y() + decorGeometry.Y())
window.metrics.bounds = image.Rectangle {
Min: origin,
Max: origin.Add(image.Pt(windowGeometry.Width(), windowGeometry.Height())),
origin := image.Pt (
decorGeometry.X(),
decorGeometry.Y())
innerOrigin := origin.Add(image.Pt (
windowGeometry.X(),
windowGeometry.Y()))
window.metrics.innerBounds = image.Rectangle {
Min: innerOrigin,
Max: innerOrigin.Add(image.Pt(windowGeometry.Width(), windowGeometry.Height())),
}
}
@@ -161,9 +164,9 @@ func (window *window) handleConfigureNotify (
configureEvent := *event.ConfigureNotifyEvent
configureEvent = window.compressConfigureNotify(configureEvent)
oldBounds := window.metrics.bounds
oldBounds := window.metrics.innerBounds
window.updateBounds()
newBounds := window.metrics.bounds
newBounds := window.metrics.innerBounds
sizeChanged :=
oldBounds.Dx() != newBounds.Dx() ||

View File

@@ -37,7 +37,8 @@ type window struct {
resizeY bool
metrics struct {
bounds image.Rectangle
bounds image.Rectangle // bounds, including frame
innerBounds image.Rectangle // bounds of the drawable area
}
onClose event.FuncBroadcaster
@@ -156,7 +157,7 @@ func (this *Backend) newWindow (
// xevent.SelectionRequestFun(window.handleSelectionRequest).
// Connect(this.x, window.xWindow.Id)
window.metrics.bounds = bounds
window.metrics.innerBounds = bounds
window.doMinimumSize()
this.windows[window.xWindow.Id] = window
@@ -165,6 +166,14 @@ func (this *Backend) newWindow (
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) {
if root == nil {
this.hierarchy.SetRoot(nil)
@@ -242,7 +251,7 @@ func (this *window) NewChild (bounds image.Rectangle) (tomo.Window, error) {
leader := this.leader
child, err := this.backend.newWindow (
bounds.Add(this.metrics.bounds.Min), false)
bounds.Add(this.metrics.innerBounds.Min), false)
child.leader = leader
if err != nil { return nil, err }
@@ -260,7 +269,7 @@ func (this *window) NewChild (bounds image.Rectangle) (tomo.Window, error) {
func (this *window) NewMenu (bounds image.Rectangle) (tomo.Window, error) {
menu, err := this.backend.newWindow (
bounds.Add(this.metrics.bounds.Min), true)
bounds.Add(this.metrics.innerBounds.Min), true)
menu.shy = true
icccm.WmTransientForSet (
this.backend.x,
@@ -273,7 +282,7 @@ func (this *window) NewMenu (bounds image.Rectangle) (tomo.Window, error) {
func (this *window) NewModal (bounds image.Rectangle) (tomo.Window, error) {
modal, err := this.backend.newWindow (
bounds.Add(this.metrics.bounds.Min), false)
bounds.Add(this.metrics.innerBounds.Min), false)
icccm.WmTransientForSet (
this.backend.x,
modal.xWindow.Id,
@@ -382,8 +391,8 @@ func (this *window) reallocateCanvas () {
previousHeight = this.xCanvas.Bounds().Dy()
}
newWidth := this.metrics.bounds.Dx()
newHeight := this.metrics.bounds.Dy()
newWidth := this.metrics.innerBounds.Dx()
newHeight := this.metrics.innerBounds.Dy()
larger := newWidth > previousWidth || newHeight > previousHeight
smaller := newWidth < previousWidth / 2 || newHeight < previousHeight / 2
@@ -403,7 +412,7 @@ func (this *window) reallocateCanvas () {
}
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 () {
@@ -458,12 +467,12 @@ func (this *window) doMinimumSize () {
this.backend.x,
this.xWindow.Id,
&hints)
newWidth := this.metrics.bounds.Dx()
newHeight := this.metrics.bounds.Dy()
newWidth := this.metrics.innerBounds.Dx()
newHeight := this.metrics.innerBounds.Dy()
if newWidth < size.X { newWidth = size.X }
if newHeight < size.Y { newHeight = size.Y }
if newWidth != this.metrics.bounds.Dx() ||
newHeight != this.metrics.bounds.Dy() {
if newWidth != this.metrics.innerBounds.Dx() ||
newHeight != this.metrics.innerBounds.Dy() {
this.xWindow.Resize(newWidth, newHeight)
}
}