Compare commits

...

25 Commits
v0.6.1 ... main

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

9
go.mod
View File

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

10
go.sum
View File

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

View File

@ -1,12 +1,12 @@
package system
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/backend/internal/util"
import "git.tebibyte.media/sashakoshka/goutil/container"
type attrHierarchy [T tomo.Attr] struct {
fallback T
style util.Optional[T]
user util.Optional[T]
style ucontainer.Optional[T]
user ucontainer.Optional[T]
}
func (this *attrHierarchy[T]) SetFallback (fallback T) {

View File

@ -7,20 +7,21 @@ import "git.tebibyte.media/tomo/tomo/data"
import "git.tebibyte.media/tomo/tomo/input"
import "git.tebibyte.media/tomo/tomo/event"
import "git.tebibyte.media/tomo/tomo/canvas"
import "git.tebibyte.media/tomo/backend/internal/util"
import "git.tebibyte.media/sashakoshka/goutil/container"
import "git.tebibyte.media/sashakoshka/goutil/image/color"
type box struct {
system *System
parent parent
outer anyBox
tags util.Set[string]
tags ucontainer.Set[string]
role tomo.Role
lastStyleNonce int
lastIconSetNonce int
styleApplicator *styleApplicator
minSize util.Memo[image.Point]
minSize ucontainer.Memo[image.Point]
bounds image.Rectangle
innerClippingBounds image.Rectangle
@ -41,7 +42,7 @@ type box struct {
focused bool
pressed bool
canvas util.Memo[canvas.Canvas]
canvas ucontainer.Memo[canvas.Canvas]
drawer canvas.Drawer
on struct {
@ -68,20 +69,21 @@ func (this *System) newBox (outer anyBox) *box {
system: this,
outer: outer,
drawer: outer,
tags: make(util.Set[string]),
tags: make(ucontainer.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 }
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
box.outer = box
}
box.minSize = util.NewMemo(box.calculateMinimumSize)
box.minSize = ucontainer.NewMemo(box.calculateMinimumSize)
return box
}
@ -130,11 +132,15 @@ 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) {
@ -255,6 +261,24 @@ func (this *box) unsetAttr (kind tomo.AttrKind, user bool) {
}
}
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
@ -453,7 +477,7 @@ func (this *box) drawBorders (can canvas.Canvas) {
rectangle := func (x0, y0, x1, y1 int, c color.Color) {
area := image.Rect(x0, y0, x1, y1)
if area.Empty() { return }
if util.Transparent(c) && this.parent != nil {
if ucolor.Transparent(c) && this.parent != nil {
this.parent.drawBackgroundPart(can.SubCanvas(area))
}
pen.Fill(c)
@ -620,6 +644,7 @@ 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 = hierarchy.newStyleApplicator()
this.invalidateStyle()
@ -658,7 +683,7 @@ func (this *box) transparent () bool {
// TODO uncomment once we have
// a way to detect texture transparency
col := this.attrColor.Value().Color
return col == nil || util.Transparent(col) /*&&
return col == nil || ucolor.Transparent(col) /*&&
(this.texture == nil || !this.texture.Opaque())*/
}

View File

@ -1,11 +1,11 @@
package system
import "image"
import "slices"
import "image/color"
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/event"
import "git.tebibyte.media/tomo/tomo/canvas"
import "git.tebibyte.media/tomo/backend/internal/util"
type containerBox struct {
*box
@ -50,7 +50,7 @@ func (this *containerBox) OnContentBoundsChange (callback func()) event.Cookie {
func (this *containerBox) Add (child tomo.Object) {
box := assertAnyBox(child.GetBox())
if util.IndexOf(this.children, box) > -1 { return }
if slices.Index(this.children, box) > -1 { return }
box.setParent(this)
box.flushActionQueue()
@ -61,26 +61,26 @@ func (this *containerBox) Add (child tomo.Object) {
func (this *containerBox) Remove (child tomo.Object) {
box := assertAnyBox(child.GetBox())
index := util.IndexOf(this.children, box)
index := slices.Index(this.children, box)
if index < 0 { return }
box.setParent(nil)
this.children = util.Remove(this.children, index)
this.children = slices.Delete(this.children, index, index + 1)
this.invalidateLayout()
this.invalidateMinimum()
}
func (this *containerBox) Insert (child, before tomo.Object) {
box := assertAnyBox(child.GetBox())
if util.IndexOf(this.children, box) > -1 { return }
if slices.Index(this.children, box) > -1 { return }
beforeBox := assertAnyBox(before.GetBox())
index := util.IndexOf(this.children, beforeBox)
index := slices.Index(this.children, beforeBox)
if index < 0 {
this.children = append(this.children, box)
} else {
this.children = util.Insert(this.children, index, box)
this.children = slices.Insert(this.children, index, box)
}
box.setParent(this)
@ -298,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()

View File

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

View File

@ -5,7 +5,7 @@ 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"
import "git.tebibyte.media/sashakoshka/goutil/container"
// Hierarchy is coupled to a tomo.Window implementation, and manages a tree of
// Boxes.
@ -25,9 +25,9 @@ type Hierarchy struct {
drags [10][]anyBox
minimumSize image.Point
needStyle util.Set[anyBox]
needLayout util.Set[anyBox]
needDraw util.Set[anyBox]
needStyle ucontainer.Set[anyBox]
needLayout ucontainer.Set[anyBox]
needDraw ucontainer.Set[anyBox]
needRedo bool
minimumClean bool
}
@ -53,9 +53,9 @@ func (this *System) NewHierarchy (link WindowLink) *Hierarchy {
hierarchy := &Hierarchy {
system: this,
link: link,
needStyle: make(util.Set[anyBox]),
needLayout: make(util.Set[anyBox]),
needDraw: make(util.Set[anyBox]),
needStyle: make(ucontainer.Set[anyBox]),
needLayout: make(ucontainer.Set[anyBox]),
needDraw: make(ucontainer.Set[anyBox]),
}
this.hierarchies.Add(hierarchy)
return hierarchy
@ -193,6 +193,10 @@ 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
}

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)

View File

@ -4,7 +4,7 @@ import "io"
import "image"
import "git.tebibyte.media/tomo/tomo/canvas"
import "git.tebibyte.media/tomo/backend/style"
import "git.tebibyte.media/tomo/backend/internal/util"
import "git.tebibyte.media/sashakoshka/goutil/container"
// System is coupled to a tomo.Backend implementation, and manages Hierarchies
// and Boxes.
@ -17,7 +17,7 @@ type System struct {
styleNonce int
iconSetNonce int
hierarchies util.Set[*Hierarchy]
hierarchies ucontainer.Set[*Hierarchy]
}
// BackendLink allows the System to call up into the tomo.Backend implementation
@ -41,7 +41,7 @@ type SurfaceLink interface {
func New (link BackendLink) *System {
return &System {
link: link,
hierarchies: make(util.Set[*Hierarchy]),
hierarchies: make(ucontainer.Set[*Hierarchy]),
}
}

View File

@ -1,6 +1,7 @@
package system
import "image"
import "unicode"
import "image/color"
import "golang.org/x/image/font"
import "git.tebibyte.media/tomo/tomo"
@ -11,6 +12,7 @@ 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"
import "git.tebibyte.media/sashakoshka/goutil/container"
type textBox struct {
*box
@ -31,9 +33,11 @@ type textBox struct {
selecting bool
selectStart int
dot text.Dot
desiredX fixed.Int26_6
drawer typeset.Drawer
face util.Cycler[font.Face]
lineHeight ucontainer.Memo[fixed.Int26_6]
on struct {
contentBoundsChange event.FuncBroadcaster
@ -46,6 +50,12 @@ func (this *System) NewTextBox () tomo.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}))
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
}
@ -89,13 +99,41 @@ func (this *textBox) SetSelectable (selectable bool) {
}
func (this *textBox) Select (dot text.Dot) {
if !this.selectable { return }
if this.dot == dot { return }
this.selec(dot)
}
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.dot = dot
this.scrollToDot()
this.on.dotChange.Broadcast()
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 {
@ -239,18 +277,17 @@ func (this *textBox) drawDot (can canvas.Canvas) {
bounds := this.InnerBounds()
metrics := face.Metrics()
dot := this.dot.Canon()
start := this.drawer.PositionAt(dot.Start).Add(fixPt(this.textOffset()))
end := this.drawer.PositionAt(dot.End ).Add(fixPt(this.textOffset()))
dot := this.dot
canonDot := dot.Canon()
start := this.drawer.PositionAt(canonDot.Start).Add(fixPt(this.textOffset()))
end := this.drawer.PositionAt(canonDot.End ).Add(fixPt(this.textOffset()))
canonEnd := this.drawer.PositionAt(dot.End ).Add(fixPt(this.textOffset()))
height := this.drawer.LineHeight().Round()
ascent := fixed.Point26_6 { Y: metrics.Descent }
descent := fixed.Point26_6 { Y: metrics.Ascent }
switch {
case dot.Empty():
pen.Stroke(textColor)
pen.StrokeWeight(1)
pen.Path(roundPt(start.Add(ascent)), roundPt(start.Sub(descent)))
case canonDot.Empty():
case start.Y == end.Y:
pen.Fill(dotColor)
@ -284,6 +321,19 @@ func (this *textBox) drawDot (can canvas.Canvas) {
rect.Min.X = bounds.Min.X
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 {
@ -307,7 +357,7 @@ func (this *textBox) handleMouseDown (button input.Button) bool {
index := this.runeUnderMouse()
this.selectStart = index
this.selecting = true
this.Select(text.Dot { Start: this.selectStart, End: index })
this.userSelect(text.Dot { Start: this.selectStart, End: index })
}
return this.box.handleMouseDown(button)
}
@ -316,7 +366,7 @@ func (this *textBox) handleMouseUp (button input.Button) bool {
if this.mouseButtonCanDrag(button) && this.selecting {
index := this.runeUnderMouse()
this.selecting = false
this.Select(text.Dot { Start: this.selectStart, End: index })
this.userSelect(text.Dot { Start: this.selectStart, End: index })
}
return this.box.handleMouseUp(button)
}
@ -330,7 +380,7 @@ func (this *textBox) mouseButtonCanDrag (button input.Button) bool {
func (this *textBox) handleMouseMove () bool {
if this.selecting {
index := this.runeUnderMouse()
this.Select(text.Dot { Start: this.selectStart, End: index })
this.userSelect(text.Dot { Start: this.selectStart, End: index })
}
return this.box.handleMouseMove()
}
@ -342,6 +392,8 @@ func (this *textBox) runeUnderMouse () int {
return this.drawer.AtPosition(fixPt(position))
}
// TODO the keynav here should make better use of input key chords.
func (this *textBox) handleKeyDown (key input.Key, numberPad bool) bool {
if this.box.handleKeyDown(key, numberPad) { return true }
if !this.selectable { return false }
@ -349,37 +401,87 @@ func (this *textBox) handleKeyDown (key input.Key, numberPad bool) bool {
// because fuck you thats why!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
modifiers := this.Window().Modifiers()
dot := this.Dot()
sel := modifiers.Shift
word := modifiers.Control
sel := modifiers.Shift()
word := modifiers.Control()
moveVertically := func (delta fixed.Int26_6) {
currentDot := 0
if sel {
currentDot = dot.End
} else {
currentDot = dot.Canon().Start
if delta > fixed.I(0) { currentDot = dot.Canon().End }
}
nextDot := 0
if word {
if delta > fixed.I(0) {
nextDot = nextParagraph(this.runes, currentDot)
} else {
nextDot = previousParagraph(this.runes, currentDot)
}
} else {
currentPosition := this.drawer.PositionAt(currentDot)
if this.desiredX != fixed.I(0) {
currentPosition.X = this.desiredX
}
nextPosition := currentPosition
nextPosition.Y += this.lineHeight.Value().Mul(delta)
this.desiredX = nextPosition.X
nextDot = this.drawer.AtPosition(nextPosition)
}
if sel {
dot.End = nextDot
this.userSelectWithoutResettingDesiredX(dot)
} else {
this.userSelectWithoutResettingDesiredX(text.EmptyDot(nextDot))
}
}
switch {
case key == input.KeyHome || (modifiers.Alt && key == input.KeyLeft):
case key == input.KeyHome || (modifiers.Alt() && key == input.KeyLeft):
if word {
dot.End = 0
} else {
dot.End = lineHomeSoft(this.runes, dot.End)
}
if !sel { dot.Start = dot.End }
this.Select(dot)
this.userSelect(dot)
return true
case key == input.KeyEnd || (modifiers.Alt && key == input.KeyRight):
dot.End = len(this.text)
case key == input.KeyEnd || (modifiers.Alt() && key == input.KeyRight):
if word {
dot.End = len(this.runes)
} else {
dot.End = lineEnd(this.runes, dot.End)
}
if !sel { dot.Start = dot.End }
this.Select(dot)
this.userSelect(dot)
return true
case key == input.KeyLeft:
if sel {
this.Select(text.SelectLeft(this.runes, dot, word))
this.userSelect(text.SelectLeft(this.runes, dot, word))
} else {
this.Select(text.MoveLeft(this.runes, dot, word))
this.userSelect(text.MoveLeft(this.runes, dot, word))
}
return true
case key == input.KeyRight:
if sel {
this.Select(text.SelectRight(this.runes, dot, word))
this.userSelect(text.SelectRight(this.runes, dot, word))
} else {
this.Select(text.MoveRight(this.runes, dot, word))
this.userSelect(text.MoveRight(this.runes, dot, word))
}
return true
case key == input.Key('a') && modifiers.Control:
case key == input.KeyUp:
moveVertically(fixed.I(-1))
return true
case key == input.KeyDown:
moveVertically(fixed.I(1))
return true
case key == input.Key('a') && modifiers.Control():
dot.Start = 0
dot.End = len(this.text)
this.Select(dot)
dot.End = len(this.runes)
this.userSelect(dot)
return true
default:
return false
@ -392,15 +494,19 @@ func (this *textBox) handleKeyUp (key input.Key, numberPad bool) bool {
modifiers := this.Window().Modifiers()
switch {
case key == input.KeyHome || (modifiers.Alt && key == input.KeyLeft):
case key == input.KeyHome || (modifiers.Alt() && key == input.KeyLeft):
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
case key == input.KeyLeft:
return true
case key == input.KeyRight:
return true
case key == input.Key('a') && modifiers.Control:
case key == input.Key('a') && modifiers.Control():
return true
default:
return false
@ -502,6 +608,7 @@ func (this *textBox) handleFaceChange () {
this.drawer.SetFace(face)
this.invalidateMinimum()
this.invalidateLayout()
this.lineHeight.Invalidate()
}
func (this *textBox) recursiveReApply () {
@ -523,3 +630,70 @@ func (this *textBox) recursiveReApply () {
}
}
}
// 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,100 +1,6 @@
package util
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 () {
var zero T
this.cache = zero
this.valid = false
}
// Cycler stores a value and an accompanying io.Closer. When the value is set,
// the closer associated with the previous value is closed.
@ -124,33 +30,3 @@ 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
}

View File

@ -7,6 +7,7 @@ import "github.com/jezek/xgb/xproto"
import "git.tebibyte.media/tomo/xgbkb"
import "github.com/jezek/xgbutil/xevent"
import "git.tebibyte.media/tomo/tomo/input"
import "git.tebibyte.media/tomo/tomo/config"
type scrollSum struct {
x, y int
@ -146,11 +147,14 @@ func (window *window) updateBounds () {
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())),
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 +165,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() ||
@ -223,7 +227,7 @@ func (window *window) handleKeyPress (
key, numberPad := keycodeToKey(keyEvent.Detail, keyEvent.State)
window.updateModifiers(keyEvent.State)
if key == input.KeyEscape && window.shy {
if config.KeyChordClose.Pressed(key, window.Modifiers()) && window.shy {
window.Close()
} else {
window.hierarchy.HandleKeyDown(key, numberPad)
@ -430,14 +434,15 @@ func (window *window) compressMotionNotify (
func (window *window) updateModifiers (state uint16) {
xModifiers := xgbkb.StateToModifiers(state)
window.hierarchy.HandleModifiers(input.Modifiers {
Shift: xModifiers.Shift || xModifiers.ShiftLock,
Control: xModifiers.Control,
Alt: xModifiers.Alt,
Meta: xModifiers.Meta,
Super: xModifiers.Super,
Hyper: xModifiers.Hyper,
})
var modifiers input.Modifiers
if xModifiers.Shift { modifiers |= input.ModShift }
if xModifiers.ShiftLock { modifiers |= input.ModShift }
if xModifiers.Control { modifiers |= input.ModControl }
if xModifiers.Alt { modifiers |= input.ModAlt }
if xModifiers.Meta { modifiers |= input.ModMeta }
if xModifiers.Super { modifiers |= input.ModSuper }
if xModifiers.Hyper { modifiers |= input.ModHyper }
window.hierarchy.HandleModifiers(modifiers)
}
func (window *window) updateMousePosition (x, y int16) {

View File

@ -1,6 +1,7 @@
package x
import "image"
import "strings"
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/data"
@ -37,10 +38,14 @@ 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
on struct {
close event.FuncBroadcaster
tryClose event.Broadcaster[func () bool]
}
}
type windowLink struct {
@ -63,27 +68,12 @@ func (this *windowLink) NotifyMinimumSizeChange () {
this.window.doMinimumSize()
}
func (this *Backend) NewWindow (
bounds image.Rectangle,
) (
output tomo.Window,
err error,
) {
func (this *Backend) NewWindow (kind tomo.WindowKind, bounds image.Rectangle) (tomo.Window, error) {
this.assert()
return this.newWindow(bounds, false)
}
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
window, err := this.newWindow(bounds, kind == tomo.WindowKindMenu)
if err != nil { return nil, err }
window.setKind(kind)
return window, nil
}
func (this *Backend) newWindow (
@ -130,7 +120,11 @@ func (this *Backend) newWindow (
if err != nil { return }
window.xWindow.WMGracefulClose (func (xWindow *xwindow.Window) {
window.Close()
holdOff := false
for _, callback := range window.on.tryClose.Listeners() {
if !callback() { holdOff = true }
}
if !holdOff { window.Close() }
})
xevent.ExposeFun(window.handleExpose).
@ -156,7 +150,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 +159,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)
@ -238,56 +240,29 @@ func (this *window) SetBounds (bounds image.Rectangle) {
bounds.Min.Y + bounds.Dy())
}
func (this *window) NewChild (bounds image.Rectangle) (tomo.Window, error) {
func (this *window) NewChild (kind tomo.WindowKind, bounds image.Rectangle) (tomo.Window, error) {
leader := this.leader
child, err := this.backend.newWindow (
bounds.Add(this.metrics.bounds.Min), false)
child.leader = leader
bounds.Add(this.metrics.innerBounds.Min), kind == tomo.WindowKindMenu)
if err != nil { return nil, err }
child.setClientLeader(leader)
leader.setClientLeader(leader)
child.leader = leader
err = child.setKind(kind)
if err != nil { return nil, err }
if kind == tomo.WindowKindModal {
this.hasModal = true
child.modalParent = this
}
icccm.WmTransientForSet (
this.backend.x,
child.xWindow.Id,
leader.xWindow.Id)
child.setType("UTILITY")
// child.inheritProperties(leader.window)
return child, err
}
func (this *window) NewMenu (bounds image.Rectangle) (tomo.Window, error) {
menu, err := this.backend.newWindow (
bounds.Add(this.metrics.bounds.Min), true)
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 {
return this.hierarchy.Modifiers()
}
@ -321,12 +296,12 @@ func (this *window) Visible () bool {
return this.visible
}
func (this *window) Close () {
func (this *window) Close () error {
xevent .Detach(this.backend.x, this.xWindow.Id)
keybind .Detach(this.backend.x, this.xWindow.Id)
mousebind.Detach(this.backend.x, this.xWindow.Id)
this.onClose.Broadcast()
this.on.close.Broadcast()
if this.modalParent != nil {
// we are a modal dialog, so unlock the parent
this.modalParent.hasModal = false
@ -336,10 +311,15 @@ func (this *window) Close () {
delete(this.backend.windows, this.xWindow.Id)
this.xWindow.Destroy()
this.hierarchy.Close()
return nil // TODO maybe return an error? maybe join them?
}
func (this *window) OnClose (callback func ()) event.Cookie {
return this.onClose.Connect(callback)
return this.on.close.Connect(callback)
}
func (this *window) OnTryClose (callback func () bool) event.Cookie {
return this.on.tryClose.Connect(callback)
}
func (this *window) grabInput () {
@ -355,6 +335,19 @@ func (this *window) ungrabInput () {
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 {
return ewmh.WmWindowTypeSet (
this.backend.x,
@ -362,6 +355,13 @@ func (this *window) setType (ty string) error {
[]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 {
hints, _ := icccm.WmHintsGet(this.backend.x, this.xWindow.Id)
if hints == nil {
@ -382,8 +382,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 +403,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 +458,24 @@ 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)
}
}
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)), " ", "_")
}
}