Basic support in X backend for new API

This commit is contained in:
Sasha Koshka 2023-04-14 00:25:05 -04:00
parent bb9c5df088
commit e931717241
7 changed files with 229 additions and 176 deletions

View File

@ -10,18 +10,25 @@ type entity struct {
children []*entity children []*entity
element tomo.Element element tomo.Element
drawDirty bool bounds image.Rectangle
layoutDirty bool clippedBounds image.Rectangle
minWidth int
bounds image.Rectangle minHeight int
minWidth int
minHeight int layoutInvalid bool
isContainer bool
} }
func bind (element tomo.Element) *entity { func bind (parent *entity, window *window, element tomo.Element) *entity {
entity := &entity { drawDirty: true } entity := &entity {
window: window,
parent: parent,
element: element,
}
entity.Invalidate()
if _, ok := element.(tomo.Container); ok { if _, ok := element.(tomo.Container); ok {
entity.layoutDirty = true entity.isContainer = true
entity.InvalidateLayout()
} }
element.Bind(entity) element.Bind(entity)
@ -38,7 +45,8 @@ func (entity *entity) unbind () {
// ----------- Entity ----------- // // ----------- Entity ----------- //
func (entity *entity) Invalidate () { func (entity *entity) Invalidate () {
entity.drawDirty = true if entity.window.system.invalidateIgnore { return }
entity.window.drawingInvalid.Add(entity)
} }
func (entity *entity) Bounds () image.Rectangle { func (entity *entity) Bounds () image.Rectangle {
@ -64,18 +72,20 @@ func (entity *entity) DrawBackground (destination canvas.Canvas, bounds image.Re
// ----------- ContainerEntity ----------- // // ----------- ContainerEntity ----------- //
func (entity *entity) InvalidateLayout () { func (entity *entity) InvalidateLayout () {
entity.layoutDirty = true if !entity.isContainer { return }
entity.layoutInvalid = true
entity.window.system.anyLayoutInvalid = true
} }
func (entity *entity) Adopt (child tomo.Element) { func (entity *entity) Adopt (child tomo.Element) {
entity.children = append(entity.children, bind(child)) entity.children = append(entity.children, bind(entity, entity.window, child))
} }
func (entity *entity) Insert (index int, child tomo.Element) { func (entity *entity) Insert (index int, child tomo.Element) {
entity.children = append ( entity.children = append (
entity.children[:index + 1], entity.children[:index + 1],
entity.children[index:]...) entity.children[index:]...)
entity.children[index] = bind(child) entity.children[index] = bind(entity, entity.window, child)
} }
func (entity *entity) Disown (index int) { func (entity *entity) Disown (index int) {
@ -104,7 +114,13 @@ func (entity *entity) CountChildren () int {
} }
func (entity *entity) PlaceChild (index int, bounds image.Rectangle) { func (entity *entity) PlaceChild (index int, bounds image.Rectangle) {
entity.children[index].bounds = bounds child := entity.children[index]
child.bounds = bounds
child.clippedBounds = entity.bounds.Intersect(bounds)
child.Invalidate()
if child.isContainer {
child.InvalidateLayout()
}
} }
func (entity *entity) ChildMinimumSize (index int) (width, height int) { func (entity *entity) ChildMinimumSize (index int) (width, height int) {

View File

@ -41,7 +41,6 @@ func (sum *scrollSum) add (button xproto.Button, window *window, state uint16) {
sum.x += scrollDistance sum.x += scrollDistance
} }
} }
} }
func (window *window) handleExpose ( func (window *window) handleExpose (
@ -49,6 +48,7 @@ func (window *window) handleExpose (
event xevent.ExposeEvent, event xevent.ExposeEvent,
) { ) {
_, region := window.compressExpose(*event.ExposeEvent) _, region := window.compressExpose(*event.ExposeEvent)
window.system.afterEvent()
window.pushRegion(region) window.pushRegion(region)
} }
@ -74,7 +74,6 @@ func (window *window) handleConfigureNotify (
window.updateBounds ( window.updateBounds (
configureEvent.X, configureEvent.Y, configureEvent.X, configureEvent.Y,
configureEvent.Width, configureEvent.Height) configureEvent.Width, configureEvent.Height)
if sizeChanged { if sizeChanged {
configureEvent = window.compressConfigureNotify(configureEvent) configureEvent = window.compressConfigureNotify(configureEvent)
@ -85,8 +84,11 @@ func (window *window) handleConfigureNotify (
window.resizeChildToFit() window.resizeChildToFit()
if !window.exposeEventFollows(configureEvent) { if !window.exposeEventFollows(configureEvent) {
window.redrawChildEntirely() window.child.Invalidate()
window.child.InvalidateLayout()
} }
window.system.afterEvent()
} }
} }
@ -136,21 +138,21 @@ func (window *window) handleKeyPress (
modifiers.NumberPad = numberPad modifiers.NumberPad = numberPad
if key == input.KeyTab && modifiers.Alt { if key == input.KeyTab && modifiers.Alt {
if child, ok := window.child.(tomo.Focusable); ok { // if child, ok := window.child.element.(tomo.Focusable); ok {
direction := input.KeynavDirectionForward // direction := input.KeynavDirectionForward
if modifiers.Shift { // if modifiers.Shift {
direction = input.KeynavDirectionBackward // direction = input.KeynavDirectionBackward
} // }
//
if !child.HandleFocus(direction) { // // TODO
child.HandleUnfocus() // }
}
}
} else if key == input.KeyEscape && window.shy { } else if key == input.KeyEscape && window.shy {
window.Close() window.Close()
} else if child, ok := window.child.(tomo.KeyboardTarget); ok { } else if child, ok := window.child.element.(tomo.KeyboardTarget); ok {
child.HandleKeyDown(key, modifiers) child.HandleKeyDown(key, modifiers)
} }
window.system.afterEvent()
} }
func (window *window) handleKeyRelease ( func (window *window) handleKeyRelease (
@ -182,9 +184,11 @@ func (window *window) handleKeyRelease (
modifiers := window.modifiersFromState(keyEvent.State) modifiers := window.modifiersFromState(keyEvent.State)
modifiers.NumberPad = numberPad modifiers.NumberPad = numberPad
if child, ok := window.child.(tomo.KeyboardTarget); ok { if child, ok := window.child.element.(tomo.KeyboardTarget); ok {
child.HandleKeyUp(key, modifiers) child.HandleKeyUp(key, modifiers)
} }
window.system.afterEvent()
} }
func (window *window) handleButtonPress ( func (window *window) handleButtonPress (
@ -205,7 +209,7 @@ func (window *window) handleButtonPress (
if !insideWindow && window.shy && !scrolling { if !insideWindow && window.shy && !scrolling {
window.Close() window.Close()
} else if scrolling { } else if scrolling {
if child, ok := window.child.(tomo.ScrollTarget); ok { if child, ok := window.child.element.(tomo.ScrollTarget); ok {
sum := scrollSum { } sum := scrollSum { }
sum.add(buttonEvent.Detail, window, buttonEvent.State) sum.add(buttonEvent.Detail, window, buttonEvent.State)
window.compressScrollSum(buttonEvent, &sum) window.compressScrollSum(buttonEvent, &sum)
@ -215,7 +219,7 @@ func (window *window) handleButtonPress (
float64(sum.x), float64(sum.y)) float64(sum.x), float64(sum.y))
} }
} else { } else {
if child, ok := window.child.(tomo.MouseTarget); ok { if child, ok := window.child.element.(tomo.MouseTarget); ok {
child.HandleMouseDown ( child.HandleMouseDown (
int(buttonEvent.EventX), int(buttonEvent.EventX),
int(buttonEvent.EventY), int(buttonEvent.EventY),
@ -223,6 +227,7 @@ func (window *window) handleButtonPress (
} }
} }
window.system.afterEvent()
} }
func (window *window) handleButtonRelease ( func (window *window) handleButtonRelease (
@ -231,7 +236,7 @@ func (window *window) handleButtonRelease (
) { ) {
if window.child == nil { return } if window.child == nil { return }
if child, ok := window.child.(tomo.MouseTarget); ok { if child, ok := window.child.element.(tomo.MouseTarget); ok {
buttonEvent := *event.ButtonReleaseEvent buttonEvent := *event.ButtonReleaseEvent
if buttonEvent.Detail >= 4 && buttonEvent.Detail <= 7 { return } if buttonEvent.Detail >= 4 && buttonEvent.Detail <= 7 { return }
child.HandleMouseUp ( child.HandleMouseUp (
@ -239,6 +244,8 @@ func (window *window) handleButtonRelease (
int(buttonEvent.EventY), int(buttonEvent.EventY),
input.Button(buttonEvent.Detail)) input.Button(buttonEvent.Detail))
} }
window.system.afterEvent()
} }
func (window *window) handleMotionNotify ( func (window *window) handleMotionNotify (
@ -247,12 +254,14 @@ func (window *window) handleMotionNotify (
) { ) {
if window.child == nil { return } if window.child == nil { return }
if child, ok := window.child.(tomo.MotionTarget); ok { if child, ok := window.child.element.(tomo.MotionTarget); ok {
motionEvent := window.compressMotionNotify(*event.MotionNotifyEvent) motionEvent := window.compressMotionNotify(*event.MotionNotifyEvent)
child.HandleMotion ( child.HandleMotion (
int(motionEvent.EventX), int(motionEvent.EventX),
int(motionEvent.EventY)) int(motionEvent.EventY))
} }
window.system.afterEvent()
} }
func (window *window) handleSelectionNotify ( func (window *window) handleSelectionNotify (

109
backends/x/system.go Normal file
View File

@ -0,0 +1,109 @@
package x
import "image"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
type entitySet map[*entity] struct { }
func (set entitySet) Empty () bool {
return len(set) == 0
}
func (set entitySet) Has (entity *entity) bool {
_, ok := set[entity]
return ok
}
func (set entitySet) Add (entity *entity) {
set[entity] = struct { } { }
}
type system struct {
child *entity
focused *entity
canvas canvas.BasicCanvas
theme tomo.Theme
config tomo.Config
invalidateIgnore bool
drawingInvalid entitySet
anyLayoutInvalid bool
pushFunc func (image.Rectangle)
}
func (system *system) initialize () {
system.drawingInvalid = make(entitySet)
}
func (system *system) SetTheme (theme tomo.Theme) {
system.theme = theme
if system.child == nil { return }
if child, ok := system.child.element.(tomo.Themeable); ok {
child.SetTheme(theme)
}
}
func (system *system) SetConfig (config tomo.Config) {
system.config = config
if system.child == nil { return }
if child, ok := system.child.element.(tomo.Configurable); ok {
child.SetConfig(config)
}
}
func (system *system) resizeChildToFit () {
system.child.bounds = system.canvas.Bounds()
system.child.clippedBounds = system.child.bounds
system.child.Invalidate()
if system.child.isContainer {
system.child.InvalidateLayout()
}
}
func (system *system) afterEvent () {
if system.anyLayoutInvalid {
system.layout(system.child, false)
system.anyLayoutInvalid = false
}
system.draw()
}
func (system *system) layout (entity *entity, force bool) {
if entity == nil { return }
if entity.layoutInvalid == true || force {
entity.element.(tomo.Container).Layout()
entity.layoutInvalid = false
force = true
}
for _, child := range entity.children {
system.layout(child, force)
}
}
func (system *system) draw () {
finalBounds := image.Rectangle { }
// ignore invalidations that result from drawing elements, because if an
// element decides to do that it really needs to rethink its life
// choices.
system.invalidateIgnore = true
defer func () { system.invalidateIgnore = false } ()
for entity := range system.drawingInvalid {
entity.element.Draw (canvas.Cut (
system.canvas,
entity.clippedBounds))
finalBounds = finalBounds.Union(entity.clippedBounds)
}
system.drawingInvalid = make(entitySet)
// TODO: don't just union all the bounds together, we can definetly
// consolidateupdated regions more efficiently than this.
if !finalBounds.Empty() {
system.pushFunc(finalBounds)
}
}

View File

@ -13,20 +13,16 @@ import "github.com/jezek/xgbutil/mousebind"
import "github.com/jezek/xgbutil/xgraphics" import "github.com/jezek/xgbutil/xgraphics"
import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/data" import "git.tebibyte.media/sashakoshka/tomo/data"
import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/canvas"
// import "runtime/debug"
type mainWindow struct { *window } type mainWindow struct { *window }
type menuWindow struct { *window } type menuWindow struct { *window }
type window struct { type window struct {
system
backend *Backend backend *Backend
xWindow *xwindow.Window xWindow *xwindow.Window
xCanvas *xgraphics.Image xCanvas *xgraphics.Image
canvas canvas.BasicCanvas
child *entity
focused *entity
onClose func ()
title, application string title, application string
@ -34,15 +30,14 @@ type window struct {
hasModal bool hasModal bool
shy bool shy bool
theme tomo.Theme
config tomo.Config
selectionRequest *selectionRequest selectionRequest *selectionRequest
selectionClaim *selectionClaim selectionClaim *selectionClaim
metrics struct { metrics struct {
bounds image.Rectangle bounds image.Rectangle
} }
onClose func ()
} }
func (backend *Backend) NewWindow ( func (backend *Backend) NewWindow (
@ -53,6 +48,10 @@ func (backend *Backend) NewWindow (
) { ) {
if backend == nil { panic("nil backend") } if backend == nil { panic("nil backend") }
window, err := backend.newWindow(bounds, false) window, err := backend.newWindow(bounds, false)
window.system.initialize()
window.system.pushFunc = window.paste
output = mainWindow { window } output = mainWindow { window }
return output, err return output, err
} }
@ -136,69 +135,24 @@ func (backend *Backend) newWindow (
return return
} }
func (window *window) NotifyMinimumSizeChange (child tomo.Element) {
window.childMinimumSizeChangeCallback(child.MinimumSize())
}
func (window *window) Window () tomo.Window { func (window *window) Window () tomo.Window {
return window return window
} }
func (window *window) RequestFocus (
child tomo.Focusable,
) (
granted bool,
) {
return true
}
func (window *window) RequestFocusNext (child tomo.Focusable) {
if child, ok := window.child.(tomo.Focusable); ok {
if !child.HandleFocus(input.KeynavDirectionForward) {
child.HandleUnfocus()
}
}
}
func (window *window) RequestFocusPrevious (child tomo.Focusable) {
if child, ok := window.child.(tomo.Focusable); ok {
if !child.HandleFocus(input.KeynavDirectionBackward) {
child.HandleUnfocus()
}
}
}
func (window *window) Adopt (child tomo.Element) { func (window *window) Adopt (child tomo.Element) {
// disown previous child // disown previous child
if window.child != nil { if window.child != nil {
window.child.SetParent(nil) window.child.unbind()
window.child.DrawTo(nil, image.Rectangle { }, nil) window.child = nil
} }
// adopt new child
if child != nil { if child != nil {
// adopt new child window.child = bind(nil, window, child)
window.child = child window.resizeChildToFit()
child.SetParent(window)
if newChild, ok := child.(tomo.Themeable); ok {
newChild.SetTheme(window.theme)
}
if newChild, ok := child.(tomo.Configurable); ok {
newChild.SetConfig(window.config)
}
if child != nil {
if !window.childMinimumSizeChangeCallback(child.MinimumSize()) {
window.resizeChildToFit()
window.redrawChildEntirely()
}
}
} }
} }
func (window *window) Child () (child tomo.Element) {
child = window.child
return
}
func (window *window) SetTitle (title string) { func (window *window) SetTitle (title string) {
window.title = title window.title = title
ewmh.WmNameSet ( ewmh.WmNameSet (
@ -317,43 +271,6 @@ func (window menuWindow) Pin () {
// TODO iungrab keyboard and mouse // TODO iungrab keyboard and mouse
} }
func (window *window) grabInput () {
keybind.GrabKeyboard(window.backend.connection, window.xWindow.Id)
mousebind.GrabPointer (
window.backend.connection,
window.xWindow.Id,
window.backend.connection.RootWin(), 0)
}
func (window *window) ungrabInput () {
keybind.UngrabKeyboard(window.backend.connection)
mousebind.UngrabPointer(window.backend.connection)
}
func (window *window) inheritProperties (parent *window) {
window.SetApplicationName(parent.application)
}
func (window *window) setType (ty string) error {
return ewmh.WmWindowTypeSet (
window.backend.connection,
window.xWindow.Id,
[]string { "_NET_WM_WINDOW_TYPE_" + ty })
}
func (window *window) setClientLeader (leader *window) error {
hints, _ := icccm.WmHintsGet(window.backend.connection, window.xWindow.Id)
if hints == nil {
hints = &icccm.Hints { }
}
hints.Flags |= icccm.HintWindowGroup
hints.WindowGroup = leader.xWindow.Id
return icccm.WmHintsSet (
window.backend.connection,
window.xWindow.Id,
hints)
}
func (window *window) Show () { func (window *window) Show () {
if window.child == nil { if window.child == nil {
window.xCanvas.For (func (x, y int) xgraphics.BGRA { window.xCanvas.For (func (x, y int) xgraphics.BGRA {
@ -362,7 +279,7 @@ func (window *window) Show () {
window.pushRegion(window.xCanvas.Bounds()) window.pushRegion(window.xCanvas.Bounds())
} }
window.xWindow.Map() window.xWindow.Map()
if window.shy { window.grabInput() } if window.shy { window.grabInput() }
} }
@ -417,18 +334,41 @@ func (window *window) OnClose (callback func ()) {
window.onClose = callback window.onClose = callback
} }
func (window *window) SetTheme (theme tomo.Theme) { func (window *window) grabInput () {
window.theme = theme keybind.GrabKeyboard(window.backend.connection, window.xWindow.Id)
if child, ok := window.child.(tomo.Themeable); ok { mousebind.GrabPointer (
child.SetTheme(theme) window.backend.connection,
} window.xWindow.Id,
window.backend.connection.RootWin(), 0)
} }
func (window *window) SetConfig (config tomo.Config) { func (window *window) ungrabInput () {
window.config = config keybind.UngrabKeyboard(window.backend.connection)
if child, ok := window.child.(tomo.Configurable); ok { mousebind.UngrabPointer(window.backend.connection)
child.SetConfig(config) }
func (window *window) inheritProperties (parent *window) {
window.SetApplicationName(parent.application)
}
func (window *window) setType (ty string) error {
return ewmh.WmWindowTypeSet (
window.backend.connection,
window.xWindow.Id,
[]string { "_NET_WM_WINDOW_TYPE_" + ty })
}
func (window *window) setClientLeader (leader *window) error {
hints, _ := icccm.WmHintsGet(window.backend.connection, window.xWindow.Id)
if hints == nil {
hints = &icccm.Hints { }
} }
hints.Flags |= icccm.HintWindowGroup
hints.WindowGroup = leader.xWindow.Id
return icccm.WmHintsSet (
window.backend.connection,
window.xWindow.Id,
hints)
} }
func (window *window) reallocateCanvas () { func (window *window) reallocateCanvas () {
@ -464,26 +404,6 @@ func (window *window) reallocateCanvas () {
} }
func (window *window) redrawChildEntirely () {
window.paste(window.canvas.Bounds())
window.pushRegion(window.canvas.Bounds())
}
func (window *window) resizeChildToFit () {
window.skipChildDrawCallback = true
window.child.DrawTo (
window.canvas,
window.canvas.Bounds(),
window.childDrawCallback)
window.skipChildDrawCallback = false
}
func (window *window) childDrawCallback (region image.Rectangle) {
if window.skipChildDrawCallback { return }
window.paste(region)
window.pushRegion(region)
}
func (window *window) paste (region image.Rectangle) { func (window *window) paste (region image.Rectangle) {
canvas := canvas.Cut(window.canvas, region) canvas := canvas.Cut(window.canvas, region)
data, stride := canvas.Buffer() data, stride := canvas.Buffer()
@ -492,7 +412,6 @@ func (window *window) paste (region image.Rectangle) {
dstStride := window.xCanvas.Stride dstStride := window.xCanvas.Stride
dstData := window.xCanvas.Pix dstData := window.xCanvas.Pix
// debug.PrintStack()
for y := bounds.Min.Y; y < bounds.Max.Y; y ++ { for y := bounds.Min.Y; y < bounds.Max.Y; y ++ {
srcYComponent := y * stride srcYComponent := y * stride
dstYComponent := y * dstStride dstYComponent := y * dstStride
@ -507,6 +426,18 @@ func (window *window) paste (region image.Rectangle) {
} }
} }
func (window *window) pushRegion (region image.Rectangle) {
if window.xCanvas == nil { panic("whoopsie!!!!!!!!!!!!!!") }
image, ok := window.xCanvas.SubImage(region).(*xgraphics.Image)
if ok {
image.XDraw()
image.XExpPaint (
window.xWindow.Id,
image.Bounds().Min.X,
image.Bounds().Min.Y)
}
}
func (window *window) childMinimumSizeChangeCallback (width, height int) (resized bool) { func (window *window) childMinimumSizeChangeCallback (width, height int) (resized bool) {
icccm.WmNormalHintsSet ( icccm.WmNormalHintsSet (
window.backend.connection, window.backend.connection,
@ -528,15 +459,3 @@ func (window *window) childMinimumSizeChangeCallback (width, height int) (resize
return false return false
} }
func (window *window) pushRegion (region image.Rectangle) {
if window.xCanvas == nil { panic("whoopsie!!!!!!!!!!!!!!") }
image, ok := window.xCanvas.SubImage(region).(*xgraphics.Image)
if ok {
image.XDraw()
image.XExpPaint (
window.xWindow.Id,
image.Bounds().Min.X,
image.Bounds().Min.Y)
}
}

View File

@ -67,6 +67,9 @@ func (backend *Backend) Run () (err error) {
<- pingAfter <- pingAfter
case callback := <- backend.doChannel: case callback := <- backend.doChannel:
callback() callback()
for _, window := range backend.windows {
window.system.afterEvent()
}
case <- pingQuit: case <- pingQuit:
return return
} }

View File

@ -26,7 +26,7 @@ func NewArtist () *Artist {
func (element *Artist) Bind (entity tomo.Entity) { func (element *Artist) Bind (entity tomo.Entity) {
element.entity = entity element.entity = entity
entity.SetMinimumSize(240, 240) if entity != nil { entity.SetMinimumSize(240, 240) }
} }
func (element *Artist) Draw (destination canvas.Canvas) { func (element *Artist) Draw (destination canvas.Canvas) {

View File

@ -15,9 +15,6 @@ type Window interface {
// these at one time. // these at one time.
Adopt (Element) Adopt (Element)
// Child returns the root element of the window.
Child () Element
// SetTitle sets the title that appears on the window's title bar. This // SetTitle sets the title that appears on the window's title bar. This
// method might have no effect with some backends. // method might have no effect with some backends.
SetTitle (string) SetTitle (string)