Compare commits

..

No commits in common. "main" and "v0.5.1" have entirely different histories.
main ... v0.5.1

21 changed files with 357 additions and 893 deletions

9
go.mod
View File

@ -1,13 +1,10 @@
module git.tebibyte.media/tomo/backend module git.tebibyte.media/tomo/backend
go 1.21.0 go 1.20
toolchain go1.22.2
require ( require (
git.tebibyte.media/sashakoshka/goutil v0.3.0 git.tebibyte.media/tomo/tomo v0.41.1
git.tebibyte.media/tomo/tomo v0.48.0 git.tebibyte.media/tomo/typeset v0.7.1
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,10 +1,8 @@
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.48.0 h1:AE21ElHwUSPsX82ZWCnoNxJFi9Oswyd3dPDPMbxTueQ= git.tebibyte.media/tomo/tomo v0.41.1 h1:XdbyF3VjsLj1Zppr70gUaufuh49hU32JQo2ENnw4PcA=
git.tebibyte.media/tomo/tomo v0.48.0/go.mod h1:WrtilgKB1y8O2Yu7X4mYcRiqOlPR8NuUnoA/ynkQWrs= git.tebibyte.media/tomo/tomo v0.41.1/go.mod h1:C9EzepS9wjkTJjnZaPBh22YvVPyA4hbBAJVU20Rdmps=
git.tebibyte.media/tomo/typeset v0.8.0 h1:4qA6oW4/3oPHj6/Zrp+JFJ53OmFSDvxs+J6BhO3DW00= git.tebibyte.media/tomo/typeset v0.7.1 h1:aZrsHwCG5ZB4f5CruRFsxLv5ezJUCFUFsQJJso2sXQ8=
git.tebibyte.media/tomo/typeset v0.8.0/go.mod h1:PwDpSdBF3l/EzoIsa2ME7QffVVajnTHZN6l3MHEGe1g= git.tebibyte.media/tomo/typeset v0.7.1/go.mod h1:PwDpSdBF3l/EzoIsa2ME7QffVVajnTHZN6l3MHEGe1g=
git.tebibyte.media/tomo/xgbkb v1.0.1 h1:b3HDUopjdQp1MZrb5Vpil4bOtk3NnNXtfQW27Blw2kE= git.tebibyte.media/tomo/xgbkb v1.0.1 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

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

View File

@ -7,21 +7,20 @@ 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/sashakoshka/goutil/container" import "git.tebibyte.media/tomo/backend/internal/util"
import "git.tebibyte.media/sashakoshka/goutil/image/color"
type box struct { type box struct {
system *System system *System
parent parent parent parent
outer anyBox outer anyBox
tags ucontainer.Set[string] tags util.Set[string]
role tomo.Role role tomo.Role
lastStyleNonce int lastStyleNonce int
lastIconSetNonce int lastIconsNonce int
styleApplicator *styleApplicator styleApplicator *styleApplicator
minSize ucontainer.Memo[image.Point] minSize util.Memo[image.Point]
bounds image.Rectangle bounds image.Rectangle
innerClippingBounds image.Rectangle innerClippingBounds image.Rectangle
@ -42,7 +41,8 @@ type box struct {
focused bool focused bool
pressed bool pressed bool
canvas ucontainer.Memo[canvas.Canvas]
canvas util.Memo[canvas.Canvas]
drawer canvas.Drawer drawer canvas.Drawer
on struct { on struct {
@ -69,21 +69,19 @@ func (this *System) newBox (outer anyBox) *box {
system: this, system: this,
outer: outer, outer: outer,
drawer: outer, drawer: outer,
tags: make(ucontainer.Set[string]), tags: make(util.Set[string]),
} }
box.attrColor.SetFallback(tomo.AColor(color.Transparent)) box.canvas = util.NewMemo (func () canvas.Canvas {
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 }
drawableArea := box.bounds.Intersect(box.parent.getInnerClippingBounds()) return parentCanvas.SubCanvas(box.bounds)
return parentCanvas.SubCanvas(drawableArea)
}) })
if outer == nil { if outer == nil {
box.drawer = box box.drawer = box
box.outer = box box.outer = box
} }
box.minSize = ucontainer.NewMemo(box.calculateMinimumSize) box.minSize = util.NewMemo(box.calculateMinimumSize)
return box return box
} }
@ -132,25 +130,17 @@ func (this *box) Tag (tag string) bool {
} }
func (this *box) SetTag (tag string, on bool) { func (this *box) SetTag (tag string, on bool) {
wasOn := this.tags.Has(tag)
if on { if on {
this.tags.Add(tag) this.tags.Add(tag)
} else { } else {
delete(this.tags, tag) delete(this.tags, tag)
} }
if wasOn != on {
this.invalidateStyle()
}
} }
func (this *box) SetAttr (attr tomo.Attr) { func (this *box) SetAttr (attr tomo.Attr) {
this.outer.setAttr(attr, true) 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) {
this.dndData = dat this.dndData = dat
} }
@ -211,7 +201,18 @@ func (this *box) setAttr (attr tomo.Attr, user bool) {
case tomo.AttrBorder: case tomo.AttrBorder:
previousBorderSum := this.borderSum() previousBorderSum := this.borderSum()
different := this.attrBorder.Set(attr, user) different := this.attrBorder.Set(attr, user)
this.handleBorderChange(previousBorderSum, different)
// only invalidate the layout if the border is sized differently
if this.borderSum() != previousBorderSum {
this.invalidateLayout()
this.invalidateMinimum()
}
// if the border takes up the same amount of space, only invalidate the
// drawing if it looks different
if different {
this.invalidateDraw()
}
case tomo.AttrMinimumSize: case tomo.AttrMinimumSize:
if this.attrMinimumSize.Set(attr, user) { if this.attrMinimumSize.Set(attr, user) {
@ -219,66 +220,13 @@ func (this *box) setAttr (attr tomo.Attr, user bool) {
} }
case tomo.AttrPadding: case tomo.AttrPadding:
if this.attrPadding.Set(attr, user) { if this.attrPadding.Set(attr, true) {
this.invalidateLayout() this.invalidateLayout()
this.invalidateMinimum() 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) { func (this *box) setBounds (bounds image.Rectangle) {
if this.bounds == bounds { return } if this.bounds == bounds { return }
this.bounds = bounds this.bounds = bounds
@ -447,28 +395,22 @@ func (this *box) Draw (can canvas.Canvas) {
// centered texture // centered texture
if textureMode == tomo.TextureModeCenter && texture != nil { if textureMode == tomo.TextureModeCenter && texture != nil {
this.centeredTexture(can, texture) 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) 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()
@ -477,7 +419,7 @@ 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 area.Empty() { return } if area.Empty() { return }
if ucolor.Transparent(c) && this.parent != nil { if util.Transparent(c) && this.parent != nil {
this.parent.drawBackgroundPart(can.SubCanvas(area)) this.parent.drawBackgroundPart(can.SubCanvas(area))
} }
pen.Fill(c) pen.Fill(c)
@ -589,21 +531,7 @@ func (this *box) recursiveRedo () {
} }
func (this *box) recursiveLoseCanvas () { func (this *box) recursiveLoseCanvas () {
this.canvas.Invalidate() this.canvas.InvalidateTo(nil)
}
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 () { func (this *box) invalidateStyle () {
@ -632,8 +560,7 @@ func (this *box) invalidateMinimum () {
} }
func (this *box) recursiveReApply () { func (this *box) recursiveReApply () {
hierarchy := this.getHierarchy() if this.getHierarchy() == nil { return }
if hierarchy == nil { return }
// re-apply styling, icons *if needed* // re-apply styling, icons *if needed*
@ -644,17 +571,16 @@ func (this *box) recursiveReApply () {
// applicator for every box, it's so style applicators can cache // applicator for every box, it's so style applicators can cache
// information about the boxes they're linked to (like all rules // information about the boxes they're linked to (like all rules
// with a matching role). // with a matching role).
this.unsetAllAttrs(false)
this.lastStyleNonce = hierarchyStyleNonce this.lastStyleNonce = hierarchyStyleNonce
this.styleApplicator = hierarchy.newStyleApplicator() this.styleApplicator = this.getHierarchy().newStyleApplicator()
this.invalidateStyle() this.invalidateStyle()
this.on.styleChange.Broadcast() this.on.styleChange.Broadcast()
} }
// icons // icons
hierarchyIconSetNonce := this.getIconSetNonce() hierarchyIconsNonce := this.getIconsNonce()
if this.lastIconSetNonce != hierarchyIconSetNonce { if this.lastIconsNonce != hierarchyIconsNonce {
this.lastIconSetNonce = hierarchyIconSetNonce this.lastIconsNonce = hierarchyIconsNonce
this.on.iconSetChange.Broadcast() this.on.iconSetChange.Broadcast()
} }
} }
@ -683,7 +609,7 @@ 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
col := this.attrColor.Value().Color col := this.attrColor.Value().Color
return col == nil || ucolor.Transparent(col) /*&& return col == nil || util.Transparent(col) /*&&
(this.texture == nil || !this.texture.Opaque())*/ (this.texture == nil || !this.texture.Opaque())*/
} }
@ -703,7 +629,7 @@ func (this *box) getStyleNonce () int {
return this.getHierarchy().getStyleNonce() return this.getHierarchy().getStyleNonce()
} }
func (this *box) getIconSetNonce () int { func (this *box) getIconsNonce () int {
// should panic if not in the tree // should panic if not in the tree
return this.getHierarchy().getIconSetNonce() return this.getHierarchy().getIconsNonce()
} }

View File

@ -1,11 +1,11 @@
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
@ -50,7 +50,7 @@ func (this *containerBox) OnContentBoundsChange (callback func()) event.Cookie {
func (this *containerBox) Add (child tomo.Object) { func (this *containerBox) Add (child tomo.Object) {
box := assertAnyBox(child.GetBox()) box := assertAnyBox(child.GetBox())
if slices.Index(this.children, box) > -1 { return } if util.IndexOf(this.children, box) > -1 { return }
box.setParent(this) box.setParent(this)
box.flushActionQueue() box.flushActionQueue()
@ -61,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 := slices.Index(this.children, box) index := util.IndexOf(this.children, box)
if index < 0 { return } if index < 0 { return }
box.setParent(nil) box.setParent(nil)
this.children = slices.Delete(this.children, index, index + 1) this.children = util.Remove(this.children, index)
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 slices.Index(this.children, box) > -1 { return } if util.IndexOf(this.children, box) > -1 { return }
beforeBox := assertAnyBox(before.GetBox()) beforeBox := assertAnyBox(before.GetBox())
index := slices.Index(this.children, beforeBox) index := util.IndexOf(this.children, beforeBox)
if index < 0 { if index < 0 {
this.children = append(this.children, box) this.children = append(this.children, box)
} else { } else {
this.children = slices.Insert(this.children, index, box) this.children = util.Insert(this.children, index, box)
} }
box.setParent(this) box.setParent(this)
@ -186,53 +186,6 @@ 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 { func (this *containerBox) recommendedHeight (width int) int {
layout := this.attrLayout.Value().Layout layout := this.attrLayout.Value().Layout
if layout == nil || !this.attrOverflow.Value().Y { if layout == nil || !this.attrOverflow.Value().Y {
@ -298,10 +251,6 @@ 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()

View File

@ -2,7 +2,6 @@ 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 // TODO: once go v1.23 comes out, replace the explicit iterator calls here with
// range loops // range loops
@ -40,14 +39,15 @@ func (this *Hierarchy) HandleKeyDown (key input.Key, numberPad bool) {
} }
if caught { return } if caught { return }
switch input.KC(key, this.modifiers) { switch key {
case config.KeyChordFocusNext: this.focusNext() case input.KeyTab:
case config.KeyChordFocusPrevious: this.focusPrevious() if this.modifiers.Shift {
// TODO: up, down, left, and right should find a box to the top, bottom, this.focusPrevious()
// left, and right respectively to move the focus to. we might want to } else {
// have four corresponding key chords in tomo/config. this.focusNext()
case input.KC(input.KeyDown, input.ModNone): this.focusNext() }
case input.KC(input.KeyUp, input.ModNone): this.focusPrevious() case input.KeyUp: this.focusPrevious()
case input.KeyDown: this.focusNext()
} }
} }

View File

@ -4,8 +4,7 @@ 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/style" import "git.tebibyte.media/tomo/backend/internal/util"
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.
@ -25,9 +24,9 @@ type Hierarchy struct {
drags [10][]anyBox drags [10][]anyBox
minimumSize image.Point minimumSize image.Point
needStyle ucontainer.Set[anyBox] needStyle util.Set[anyBox]
needLayout ucontainer.Set[anyBox] needLayout util.Set[anyBox]
needDraw ucontainer.Set[anyBox] needDraw util.Set[anyBox]
needRedo bool needRedo bool
minimumClean bool minimumClean bool
} }
@ -53,9 +52,9 @@ func (this *System) NewHierarchy (link WindowLink) *Hierarchy {
hierarchy := &Hierarchy { hierarchy := &Hierarchy {
system: this, system: this,
link: link, link: link,
needStyle: make(ucontainer.Set[anyBox]), needStyle: make(util.Set[anyBox]),
needLayout: make(ucontainer.Set[anyBox]), needLayout: make(util.Set[anyBox]),
needDraw: make(ucontainer.Set[anyBox]), needDraw: make(util.Set[anyBox]),
} }
this.hierarchies.Add(hierarchy) this.hierarchies.Add(hierarchy)
return hierarchy return hierarchy
@ -169,34 +168,22 @@ func (this *Hierarchy) getWindow () tomo.Window {
return this.link.GetWindow() return this.link.GetWindow()
} }
func (this *Hierarchy) getStyle () *style.Style { func (this *Hierarchy) getStyle () *tomo.Style {
return this.system.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 { func (this *Hierarchy) getStyleNonce () int {
return this.system.styleNonce return this.system.styleNonce
} }
func (this *Hierarchy) getIconSetNonce () int { func (this *Hierarchy) getIconsNonce () int {
return this.system.iconSetNonce return this.system.iconsNonce
} }
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
} }

View File

@ -12,9 +12,6 @@ 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)
@ -78,9 +75,6 @@ type anyBox interface {
// setAttr sets an attribute at the user or style level depending // setAttr sets an attribute at the user or style level depending
// on the value of user. // on the value of user.
setAttr (attr tomo.Attr, user bool) 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

View File

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

View File

@ -2,22 +2,20 @@ package system
import "io" import "io"
import "image" import "image"
import "git.tebibyte.media/tomo/tomo"
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/internal/util"
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 style *tomo.Style
iconSet style.IconSet styleNonce int
faceSet style.FaceSet iconsNonce int
styleNonce int
iconSetNonce int
hierarchies ucontainer.Set[*Hierarchy] hierarchies util.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
@ -41,13 +39,13 @@ type SurfaceLink interface {
func New (link BackendLink) *System { func New (link BackendLink) *System {
return &System { return &System {
link: link, link: link,
hierarchies: make(ucontainer.Set[*Hierarchy]), hierarchies: make(util.Set[*Hierarchy]),
} }
} }
// SetStyle sets the style that is applied to objects, and notifies them // SetStyle sets the tomo.Style that is applied to objects, and notifies them
// that the style has changed. // that the style has changed.
func (this *System) SetStyle (style *style.Style) { func (this *System) SetStyle (style *tomo.Style) {
this.style = style this.style = style
this.styleNonce ++ this.styleNonce ++
for hierarchy := range this.hierarchies { for hierarchy := range this.hierarchies {
@ -55,21 +53,14 @@ func (this *System) SetStyle (style *style.Style) {
} }
} }
// SetIconSet sets the icon set that provides icon textures, and notifies // SetIconSet notifies objects that the icons have changed.
// objects that the icons have changed. func (this *System) SetIconSet (iconSet tomo.IconSet) {
func (this *System) SetIconSet (iconSet style.IconSet) { this.iconsNonce ++
this.iconSet = iconSet
this.iconSetNonce ++
for hierarchy := range this.hierarchies { for hierarchy := range this.hierarchies {
hierarchy.setIconSet() 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) { func (this *System) removeHierarchy (hierarchy *Hierarchy) {
delete(this.hierarchies, hierarchy) delete(this.hierarchies, hierarchy)
} }

View File

@ -1,9 +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 "git.tebibyte.media/tomo/tomo" import "git.tebibyte.media/tomo/tomo"
import "golang.org/x/image/math/fixed" import "golang.org/x/image/math/fixed"
import "git.tebibyte.media/tomo/typeset" import "git.tebibyte.media/tomo/typeset"
@ -11,8 +9,6 @@ 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
@ -33,11 +29,8 @@ type textBox struct {
selecting bool selecting bool
selectStart int selectStart int
dot text.Dot dot text.Dot
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
@ -48,14 +41,6 @@ type textBox struct {
func (this *System) NewTextBox () tomo.TextBox { func (this *System) NewTextBox () tomo.TextBox {
box := &textBox { } box := &textBox { }
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
} }
@ -99,41 +84,13 @@ func (this *textBox) SetSelectable (selectable bool) {
} }
func (this *textBox) Select (dot text.Dot) { func (this *textBox) Select (dot text.Dot) {
this.selec(dot) if !this.selectable { return }
} if this.dot == dot { return }
func (this *textBox) selec (dot text.Dot) bool {
if this.selectWithoutResettingDesiredX(dot) {
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 {
@ -149,8 +106,9 @@ func (this *textBox) OnDotChange (callback func ()) event.Cookie {
func (this *textBox) Draw (can canvas.Canvas) { func (this *textBox) Draw (can canvas.Canvas) {
if can == nil { return } if can == nil { return }
texture := this.attrTexture.Value().Texture texture := this.attrTexture.Value().Texture
col := this.attrColor.Value().Color col := this.attrColor.Value().Color
if col == nil { col = color.Transparent }
this.drawBorders(can) this.drawBorders(can)
@ -166,8 +124,9 @@ func (this *textBox) Draw (can canvas.Canvas) {
this.drawDot(can) this.drawDot(can)
} }
if this.face.Value() != nil { if this.attrFace.Value().Face != nil {
textColor := this.attrTextColor.Value().Color textColor := this.attrTextColor.Value().Color
if textColor == nil { textColor = color.Black }
this.drawer.Draw(can, textColor, this.textOffset()) this.drawer.Draw(can, textColor, this.textOffset())
} }
} }
@ -186,22 +145,23 @@ func (this *textBox) setAttr (attr tomo.Attr, user bool) {
case tomo.AttrFace: case tomo.AttrFace:
if this.attrFace.Set(attr, user) { if this.attrFace.Set(attr, user) {
this.handleFaceChange() this.drawer.SetFace(attr.Face)
this.invalidateMinimum()
this.invalidateLayout()
} }
case tomo.AttrWrap: case tomo.AttrWrap:
if this.attrWrap.Set(attr, user) { if this.attrWrap.Set(attr, user) {
this.drawer.SetWrap(bool(this.attrWrap.Value())) this.drawer.SetWrap(bool(attr))
this.invalidateMinimum() this.invalidateMinimum()
this.invalidateLayout() this.invalidateLayout()
} }
case tomo.AttrAlign: case tomo.AttrAlign:
if this.attrAlign.Set(attr, user) { if this.attrAlign.Set(attr, user) {
align := this.attrAlign.Value()
this.drawer.SetAlign ( this.drawer.SetAlign (
typeset.Align(align.X), typeset.Align(attr.X),
typeset.Align(align.Y)) typeset.Align(attr.Y))
this.invalidateDraw() this.invalidateDraw()
} }
@ -215,49 +175,6 @@ 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 { func roundPt (point fixed.Point26_6) image.Point {
return image.Pt(point.X.Round(), point.Y.Round()) return image.Pt(point.X.Round(), point.Y.Round())
} }
@ -267,27 +184,30 @@ func fixPt (point image.Point) fixed.Point26_6 {
} }
func (this *textBox) drawDot (can canvas.Canvas) { func (this *textBox) drawDot (can canvas.Canvas) {
face := this.face.Value() if this.attrFace.Value().Face == nil { return }
if face == nil { return }
face := this.attrFace.Value().Face
textColor := this.attrTextColor.Value().Color textColor := this.attrTextColor.Value().Color
dotColor := this.attrDotColor.Value().Color dotColor := this.attrDotColor.Value().Color
if textColor == nil { textColor = color.Black }
if dotColor == nil { dotColor = color.RGBA { G: 255, B: 255, A: 255 } }
pen := can.Pen() pen := can.Pen()
bounds := this.InnerBounds() bounds := this.InnerBounds()
metrics := face.Metrics() metrics := face.Metrics()
dot := this.dot dot := this.dot.Canon()
canonDot := dot.Canon() start := this.drawer.PositionAt(dot.Start).Add(fixPt(this.textOffset()))
start := this.drawer.PositionAt(canonDot.Start).Add(fixPt(this.textOffset())) end := this.drawer.PositionAt(dot.End ).Add(fixPt(this.textOffset()))
end := this.drawer.PositionAt(canonDot.End ).Add(fixPt(this.textOffset())) height := this.drawer.LineHeight().Round()
canonEnd := this.drawer.PositionAt(dot.End ).Add(fixPt(this.textOffset())) ascent := fixed.Point26_6 { Y: metrics.Descent }
height := this.drawer.LineHeight().Round() descent := fixed.Point26_6 { Y: metrics.Ascent }
ascent := fixed.Point26_6 { Y: metrics.Descent }
descent := fixed.Point26_6 { Y: metrics.Ascent }
switch { switch {
case canonDot.Empty(): case dot.Empty():
pen.Stroke(textColor)
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(dotColor) pen.Fill(dotColor)
@ -321,19 +241,6 @@ 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 {
@ -357,7 +264,7 @@ func (this *textBox) handleMouseDown (button input.Button) bool {
index := this.runeUnderMouse() index := this.runeUnderMouse()
this.selectStart = index this.selectStart = index
this.selecting = true this.selecting = true
this.userSelect(text.Dot { Start: this.selectStart, End: index }) this.Select(text.Dot { Start: this.selectStart, End: index })
} }
return this.box.handleMouseDown(button) return this.box.handleMouseDown(button)
} }
@ -366,7 +273,7 @@ func (this *textBox) handleMouseUp (button input.Button) bool {
if this.mouseButtonCanDrag(button) && this.selecting { if this.mouseButtonCanDrag(button) && this.selecting {
index := this.runeUnderMouse() index := this.runeUnderMouse()
this.selecting = false this.selecting = false
this.userSelect(text.Dot { Start: this.selectStart, End: index }) this.Select(text.Dot { Start: this.selectStart, End: index })
} }
return this.box.handleMouseUp(button) return this.box.handleMouseUp(button)
} }
@ -380,7 +287,7 @@ func (this *textBox) mouseButtonCanDrag (button input.Button) bool {
func (this *textBox) handleMouseMove () bool { func (this *textBox) handleMouseMove () bool {
if this.selecting { if this.selecting {
index := this.runeUnderMouse() index := this.runeUnderMouse()
this.userSelect(text.Dot { Start: this.selectStart, End: index }) this.Select(text.Dot { Start: this.selectStart, End: index })
} }
return this.box.handleMouseMove() return this.box.handleMouseMove()
} }
@ -392,8 +299,6 @@ func (this *textBox) runeUnderMouse () int {
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 { func (this *textBox) handleKeyDown (key input.Key, numberPad bool) bool {
if this.box.handleKeyDown(key, numberPad) { return true } if this.box.handleKeyDown(key, numberPad) { return true }
if !this.selectable { return false } if !this.selectable { return false }
@ -401,87 +306,37 @@ func (this *textBox) handleKeyDown (key input.Key, numberPad bool) bool {
// because fuck you thats why!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! // because fuck you thats why!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
modifiers := this.Window().Modifiers() modifiers := this.Window().Modifiers()
dot := this.Dot() dot := this.Dot()
sel := modifiers.Shift() sel := modifiers.Shift
word := modifiers.Control() 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 { switch {
case key == input.KeyHome || (modifiers.Alt() && key == input.KeyLeft): case key == input.KeyHome || (modifiers.Alt && key == input.KeyLeft):
if word { dot.End = 0
dot.End = 0
} else {
dot.End = lineHomeSoft(this.runes, dot.End)
}
if !sel { dot.Start = dot.End } if !sel { dot.Start = dot.End }
this.userSelect(dot) this.Select(dot)
return true return true
case key == input.KeyEnd || (modifiers.Alt() && key == input.KeyRight): case key == input.KeyEnd || (modifiers.Alt && key == input.KeyRight):
if word { dot.End = len(this.text)
dot.End = len(this.runes)
} else {
dot.End = lineEnd(this.runes, dot.End)
}
if !sel { dot.Start = dot.End } if !sel { dot.Start = dot.End }
this.userSelect(dot) this.Select(dot)
return true return true
case key == input.KeyLeft: case key == input.KeyLeft:
if sel { if sel {
this.userSelect(text.SelectLeft(this.runes, dot, word)) this.Select(text.SelectLeft(this.runes, dot, word))
} else { } else {
this.userSelect(text.MoveLeft(this.runes, dot, word)) this.Select(text.MoveLeft(this.runes, dot, word))
} }
return true return true
case key == input.KeyRight: case key == input.KeyRight:
if sel { if sel {
this.userSelect(text.SelectRight(this.runes, dot, word)) this.Select(text.SelectRight(this.runes, dot, word))
} else { } else {
this.userSelect(text.MoveRight(this.runes, dot, word)) this.Select(text.MoveRight(this.runes, dot, word))
} }
return true return true
case key == input.KeyUp: case key == input.Key('a') && modifiers.Control:
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.Start = 0
dot.End = len(this.runes) dot.End = len(this.text)
this.userSelect(dot) this.Select(dot)
return true return true
default: default:
return false return false
@ -494,19 +349,15 @@ func (this *textBox) handleKeyUp (key input.Key, numberPad bool) bool {
modifiers := this.Window().Modifiers() modifiers := this.Window().Modifiers()
switch { switch {
case key == input.KeyHome || (modifiers.Alt() && key == input.KeyLeft): case key == input.KeyHome || (modifiers.Alt && key == input.KeyLeft):
return true return true
case key == input.KeyEnd || (modifiers.Alt() && key == input.KeyRight): case key == input.KeyEnd || (modifiers.Alt && key == input.KeyRight):
return true
case key == input.KeyUp:
return true
case key == input.KeyDown:
return true return true
case key == input.KeyLeft: case key == input.KeyLeft:
return true return true
case key == input.KeyRight: case key == input.KeyRight:
return true return true
case key == input.Key('a') && modifiers.Control(): case key == input.Key('a') && modifiers.Control:
return true return true
default: default:
return false return false
@ -596,104 +447,3 @@ 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,6 +1,106 @@
package util package util
import "io" import "io"
import "image/color"
// IndexOf returns the index of needle within haystack. If needle does not exist
// within haystack, it returns -1.
func IndexOf[T comparable] (haystack []T, needle T) int {
for index, test := range haystack {
if test == needle {
return index
}
}
return -1
}
// Remove removes an element from slice at index.
func Remove[T any] (slice []T, index int) []T {
return append(slice[:index], slice[index + 1:]...)
}
// 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()
this.valid = true
}
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
}
// Cycler stores a value and an accompanying io.Closer. When the value is set, // Cycler stores a value and an accompanying io.Closer. When the value is set,
// the closer associated with the previous value is closed. // the closer associated with the previous value is closed.

View File

@ -1,13 +0,0 @@
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
}

View File

@ -1,28 +0,0 @@
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
}

View File

@ -1,70 +0,0 @@
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,9 +4,7 @@ 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"
@ -19,9 +17,6 @@ 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
@ -131,40 +126,19 @@ 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) { func (this *Backend) SetStyle (style *tomo.Style) {
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) this.system.SetStyle(style)
} }
func (this *Backend) SetIconSet (iconSet style.IconSet) { func (this *Backend) SetIconSet (icons tomo.IconSet) {
this.iconSet = iconSet this.system.SetIconSet(icons)
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,32 +132,3 @@ 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

@ -46,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 := blendPremultipliedBGRA(xgraphics.BGRA { pixel := xgraphics.BlendBGRA(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],
@ -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.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 := blendPremultipliedBGRA(xgraphics.BGRA { pixel := xgraphics.BlendBGRA(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],
@ -256,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 := blendPremultipliedBGRA(xgraphics.BGRA { pixel := xgraphics.BlendBGRA(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 := blendPremultipliedBGRA(xgraphics.BGRA { pixel := xgraphics.BlendBGRA(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,7 +7,6 @@ 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
@ -146,15 +145,12 @@ 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(
decorGeometry.X(), windowGeometry.X() + decorGeometry.X(),
decorGeometry.Y()) windowGeometry.Y() + decorGeometry.Y())
innerOrigin := origin.Add(image.Pt ( window.metrics.bounds = image.Rectangle {
windowGeometry.X(), Min: origin,
windowGeometry.Y())) Max: origin.Add(image.Pt(windowGeometry.Width(), windowGeometry.Height())),
window.metrics.innerBounds = image.Rectangle {
Min: innerOrigin,
Max: innerOrigin.Add(image.Pt(windowGeometry.Width(), windowGeometry.Height())),
} }
} }
@ -165,9 +161,9 @@ func (window *window) handleConfigureNotify (
configureEvent := *event.ConfigureNotifyEvent configureEvent := *event.ConfigureNotifyEvent
configureEvent = window.compressConfigureNotify(configureEvent) configureEvent = window.compressConfigureNotify(configureEvent)
oldBounds := window.metrics.innerBounds oldBounds := window.metrics.bounds
window.updateBounds() window.updateBounds()
newBounds := window.metrics.innerBounds newBounds := window.metrics.bounds
sizeChanged := sizeChanged :=
oldBounds.Dx() != newBounds.Dx() || oldBounds.Dx() != newBounds.Dx() ||
@ -227,7 +223,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 config.KeyChordClose.Pressed(key, window.Modifiers()) && window.shy { if key == input.KeyEscape && window.shy {
window.Close() window.Close()
} else { } else {
window.hierarchy.HandleKeyDown(key, numberPad) window.hierarchy.HandleKeyDown(key, numberPad)
@ -434,15 +430,14 @@ func (window *window) compressMotionNotify (
func (window *window) updateModifiers (state uint16) { func (window *window) updateModifiers (state uint16) {
xModifiers := xgbkb.StateToModifiers(state) xModifiers := xgbkb.StateToModifiers(state)
var modifiers input.Modifiers window.hierarchy.HandleModifiers(input.Modifiers {
if xModifiers.Shift { modifiers |= input.ModShift } Shift: xModifiers.Shift || xModifiers.ShiftLock,
if xModifiers.ShiftLock { modifiers |= input.ModShift } Control: xModifiers.Control,
if xModifiers.Control { modifiers |= input.ModControl } Alt: xModifiers.Alt,
if xModifiers.Alt { modifiers |= input.ModAlt } Meta: xModifiers.Meta,
if xModifiers.Meta { modifiers |= input.ModMeta } Super: xModifiers.Super,
if xModifiers.Super { modifiers |= input.ModSuper } Hyper: xModifiers.Hyper,
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,7 +1,6 @@
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"
@ -38,14 +37,10 @@ type window struct {
resizeY bool resizeY bool
metrics struct { metrics struct {
bounds image.Rectangle // bounds, including frame bounds image.Rectangle
innerBounds image.Rectangle // bounds of the drawable area
} }
on struct { onClose event.FuncBroadcaster
close event.FuncBroadcaster
tryClose event.Broadcaster[func () bool]
}
} }
type windowLink struct { type windowLink struct {
@ -68,12 +63,27 @@ func (this *windowLink) NotifyMinimumSizeChange () {
this.window.doMinimumSize() this.window.doMinimumSize()
} }
func (this *Backend) NewWindow (kind tomo.WindowKind, bounds image.Rectangle) (tomo.Window, error) { func (this *Backend) NewWindow (
bounds image.Rectangle,
) (
output tomo.Window,
err error,
) {
this.assert() this.assert()
window, err := this.newWindow(bounds, kind == tomo.WindowKindMenu) return this.newWindow(bounds, false)
if err != nil { return nil, err } }
window.setKind(kind)
return window, nil func (this *Backend) NewPlainWindow (
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 (
@ -120,11 +130,7 @@ 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) {
holdOff := false window.Close()
for _, callback := range window.on.tryClose.Listeners() {
if !callback() { holdOff = true }
}
if !holdOff { window.Close() }
}) })
xevent.ExposeFun(window.handleExpose). xevent.ExposeFun(window.handleExpose).
@ -150,7 +156,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.innerBounds = bounds window.metrics.bounds = bounds
window.doMinimumSize() window.doMinimumSize()
this.windows[window.xWindow.Id] = window this.windows[window.xWindow.Id] = window
@ -159,14 +165,6 @@ 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)
@ -240,29 +238,56 @@ func (this *window) SetBounds (bounds image.Rectangle) {
bounds.Min.Y + bounds.Dy()) bounds.Min.Y + bounds.Dy())
} }
func (this *window) NewChild (kind tomo.WindowKind, bounds image.Rectangle) (tomo.Window, error) { func (this *window) NewChild (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.innerBounds.Min), kind == tomo.WindowKindMenu) bounds.Add(this.metrics.bounds.Min), false)
child.leader = leader
if err != nil { return nil, err } if err != nil { return nil, err }
child.leader = leader child.setClientLeader(leader)
err = child.setKind(kind) leader.setClientLeader(leader)
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) NewMenu (bounds image.Rectangle) (tomo.Window, error) {
menu, err := this.backend.newWindow (
bounds.Add(this.metrics.bounds.Min), true)
menu.shy = true
icccm.WmTransientForSet (
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) {
modal, err := this.backend.newWindow (
bounds.Add(this.metrics.bounds.Min), false)
icccm.WmTransientForSet (
this.backend.x,
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) Modifiers () input.Modifiers { func (this *window) Modifiers () input.Modifiers {
return this.hierarchy.Modifiers() return this.hierarchy.Modifiers()
} }
@ -296,12 +321,12 @@ func (this *window) Visible () bool {
return this.visible return this.visible
} }
func (this *window) Close () error { func (this *window) Close () {
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.on.close.Broadcast() this.onClose.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
@ -311,15 +336,10 @@ func (this *window) Close () error {
delete(this.backend.windows, this.xWindow.Id) delete(this.backend.windows, this.xWindow.Id)
this.xWindow.Destroy() this.xWindow.Destroy()
this.hierarchy.Close() 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.on.close.Connect(callback) return this.onClose.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 () {
@ -335,19 +355,6 @@ 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,
@ -355,13 +362,6 @@ 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 {
@ -382,8 +382,8 @@ func (this *window) reallocateCanvas () {
previousHeight = this.xCanvas.Bounds().Dy() previousHeight = this.xCanvas.Bounds().Dy()
} }
newWidth := this.metrics.innerBounds.Dx() newWidth := this.metrics.bounds.Dx()
newHeight := this.metrics.innerBounds.Dy() newHeight := this.metrics.bounds.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
@ -403,7 +403,7 @@ func (this *window) reallocateCanvas () {
} }
this.hierarchy.SetCanvas(this.xCanvas.SubCanvas ( this.hierarchy.SetCanvas(this.xCanvas.SubCanvas (
this.metrics.innerBounds.Sub(this.metrics.innerBounds.Min))) this.metrics.bounds.Sub(this.metrics.bounds.Min)))
} }
func (this *window) pushAll () { func (this *window) pushAll () {
@ -458,24 +458,12 @@ func (this *window) doMinimumSize () {
this.backend.x, this.backend.x,
this.xWindow.Id, this.xWindow.Id,
&hints) &hints)
newWidth := this.metrics.innerBounds.Dx() newWidth := this.metrics.bounds.Dx()
newHeight := this.metrics.innerBounds.Dy() 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.innerBounds.Dx() || if newWidth != this.metrics.bounds.Dx() ||
newHeight != this.metrics.innerBounds.Dy() { newHeight != this.metrics.bounds.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)), " ", "_")
}
}