Overhauled event system

This commit is contained in:
Sasha Koshka 2022-11-16 00:29:23 -05:00
parent 3a3fb66db8
commit e030f8632b
9 changed files with 221 additions and 158 deletions

View File

@ -10,12 +10,12 @@ type Application struct {
icons []image.Image
backend Backend
config Config
callbackManager CallbackManager
}
// Run initializes the application, starts it, and then returns a channel that
// broadcasts events. If no suitable backend can be found, an error is returned.
func (application *Application) Run () (
channel chan(Event),
err error,
) {
// default values for certain parameters
@ -29,12 +29,46 @@ func (application *Application) Run () (
application.backend, err = instantiateBackend(application)
if err != nil { return }
channel = make(chan(Event))
go application.backend.Run(channel)
application.backend.Run()
return
}
func (application *Application) OnQuit (
onQuit func (),
) {
application.callbackManager.onQuit = onQuit
}
func (application *Application) OnPress (
onPress func (button Button),
) {
application.callbackManager.onPress = onPress
}
func (application *Application) OnRelease (
onRelease func (button Button),
) {
application.callbackManager.onRelease = onRelease
}
func (application *Application) OnResize (
onResize func (),
) {
application.callbackManager.onResize = onResize
}
func (application *Application) OnMouseMove (
onMouseMove func (x, y int),
) {
application.callbackManager.onMouseMove = onMouseMove
}
func (application *Application) OnStart (
onStart func (),
) {
application.callbackManager.onStart = onStart
}
// Draw "commits" changes made in the buffer to the display.
func (application *Application) Draw () {
application.backend.Draw()

View File

@ -4,13 +4,19 @@ import "image"
import "errors"
type Backend interface {
Run (channel chan(Event))
Run ()
SetTitle (title string) (err error)
SetIcon (icons []image.Image) (err error)
Draw ()
}
type BackendFactory func (application *Application) (backend Backend, err error)
type BackendFactory func (
application *Application,
callbackManager *CallbackManager,
) (
backend Backend,
err error,
)
var factories []BackendFactory
@ -21,7 +27,7 @@ func RegisterBackend (factory BackendFactory) {
func instantiateBackend (application *Application) (backend Backend, err error) {
// find a suitable backend
for _, factory := range factories {
backend, err = factory(application)
backend, err = factory(application, &application.callbackManager)
if err == nil && backend != nil { return }
}

View File

@ -3,12 +3,13 @@ package x
import "image"
import "image/draw"
import "golang.org/x/image/math/fixed"
import "github.com/jezek/xgbutil/xgraphics"
import "git.tebibyte.media/sashakoshka/stone"
func (backend *Backend) Draw () {
backend.drawLock.Lock()
defer backend.drawLock.Unlock()
backend.lock.Lock()
defer backend.lock.Unlock()
boundsChanged :=
backend.memory.windowWidth != backend.metrics.windowWidth ||
@ -22,25 +23,77 @@ func (backend *Backend) Draw () {
backend.canvas.XDraw()
backend.canvas.XPaint(backend.window.Id)
} else {
backend.updateWindowAreas(backend.drawCells(false)...)
backend.canvas.XPaintRects (
backend.window.Id,
backend.drawCells(false)...)
}
}
func (backend *Backend) updateWindowAreas (areas ...image.Rectangle) {
backend.canvas.XPaintRects(backend.window.Id, areas...)
func (backend *Backend) reallocateCanvas () {
if backend.canvas != nil {
backend.canvas.Destroy()
}
backend.canvas = xgraphics.New (
backend.connection,
image.Rect (
0, 0,
backend.metrics.windowWidth,
backend.metrics.windowHeight))
backend.canvas.For (func (x, y int) xgraphics.BGRA {
return backend.colors[stone.ColorBackground]
})
backend.canvas.XSurfaceSet(backend.window.Id)
}
func (backend *Backend) drawRune (x, y int, character rune, runeColor stone.Color) {
func (backend *Backend) drawCells (forceRedraw bool) (areas []image.Rectangle) {
width, height := backend.application.Size()
for y := 0; y < height; y ++ {
for x := 0; x < width; x ++ {
if !forceRedraw && backend.application.Clean(x, y) { continue }
backend.application.MarkClean(x, y)
cell := backend.application.Cell(x, y)
content := cell.Rune()
if forceRedraw && content < 32 { continue }
areas = append(areas, backend.boundsOfCell(x, y))
backend.drawRune(x, y, content, cell.Color(), !forceRedraw)
}}
if backend.drawBufferBounds && forceRedraw {
strokeRectangle (
&image.Uniform {
C: backend.config.Color(stone.ColorForeground),
},
backend.canvas,
image.Rectangle {
Min: backend.originOfCell(0, 0),
Max: backend.originOfCell(width, height),
})
}
return
}
func (backend *Backend) drawRune (
x, y int,
character rune,
runeColor stone.Color,
drawBackground bool,
) {
// TODO: cache these draws as non-transparent buffers with the
// application background color as the background. that way, we won't
// need to redraw the characters *or* composite them.
fillRectangle (
&image.Uniform {
C: backend.config.Color(stone.ColorBackground),
},
backend.canvas,
backend.boundsOfCell(x, y))
if drawBackground {
fillRectangle (
&image.Uniform {
C: backend.config.Color(stone.ColorBackground),
},
backend.canvas,
backend.boundsOfCell(x, y))
}
if character < 32 { return }
@ -73,36 +126,6 @@ func (backend *Backend) drawRune (x, y int, character rune, runeColor stone.Colo
draw.Over)
}
func (backend *Backend) drawCells (forceRedraw bool) (areas []image.Rectangle) {
width, height := backend.application.Size()
for y := 0; y < height; y ++ {
for x := 0; x < width; x ++ {
if !forceRedraw && backend.application.Clean(x, y) { continue }
backend.application.MarkClean(x, y)
cell := backend.application.Cell(x, y)
content := cell.Rune()
if forceRedraw && content < 32 { continue }
areas = append(areas, backend.boundsOfCell(x, y))
backend.drawRune(x, y, content, cell.Color())
}}
if backend.drawBufferBounds && forceRedraw {
strokeRectangle (
&image.Uniform {
C: backend.config.Color(stone.ColorForeground),
},
backend.canvas,
image.Rectangle {
Min: backend.originOfCell(0, 0),
Max: backend.originOfCell(width, height),
})
}
return
}
func fillRectangle (
source image.Image,
destination draw.Image,

View File

@ -8,17 +8,19 @@ import "github.com/jezek/xgbutil/xevent"
import "git.tebibyte.media/sashakoshka/stone"
func (backend *Backend) Run (channel chan(stone.Event)) {
backend.channel = channel
func (backend *Backend) Run () {
backend.callbackManager.RunStart()
backend.Draw()
xevent.Main(backend.connection)
backend.shutDown()
}
func (backend *Backend) handleConfigureNotify (
connection *xgbutil.XUtil,
event xevent.ConfigureNotifyEvent,
) {
backend.lock.Lock()
configureEvent := *event.ConfigureNotifyEvent
newWidth := int(configureEvent.Width)
@ -44,8 +46,13 @@ func (backend *Backend) handleConfigureNotify (
(backend.metrics.windowWidth - frameWidth) / 2
backend.metrics.paddingY =
(backend.metrics.windowHeight - frameHeight) / 2
backend.channel <- stone.EventResize { }
}
backend.lock.Unlock()
if sizeChanged {
backend.callbackManager.RunResize()
backend.Draw()
}
}
@ -54,9 +61,7 @@ func (backend *Backend) handleButtonPress (
event xevent.ButtonPressEvent,
) {
buttonEvent := *event.ButtonPressEvent
backend.channel <- stone.EventPress {
Button: stone.Button(buttonEvent.Detail + 127),
}
backend.callbackManager.RunPress(stone.Button(buttonEvent.Detail + 127))
}
func (backend *Backend) handleButtonRelease (
@ -64,9 +69,7 @@ func (backend *Backend) handleButtonRelease (
event xevent.ButtonReleaseEvent,
) {
buttonEvent := *event.ButtonReleaseEvent
backend.channel <- stone.EventRelease {
Button: stone.Button(buttonEvent.Detail + 127),
}
backend.callbackManager.RunRelease(stone.Button(buttonEvent.Detail + 127))
}
func (backend *Backend) handleKeyPress (
@ -75,7 +78,7 @@ func (backend *Backend) handleKeyPress (
) {
keyEvent := *event.KeyPressEvent
button := backend.keycodeToButton(keyEvent.Detail, keyEvent.State)
backend.channel <- stone.EventPress { Button: button }
backend.callbackManager.RunPress(button)
}
func (backend *Backend) handleKeyRelease (
@ -84,7 +87,7 @@ func (backend *Backend) handleKeyRelease (
) {
keyEvent := *event.KeyReleaseEvent
button := backend.keycodeToButton(keyEvent.Detail, keyEvent.State)
backend.channel <- stone.EventRelease { Button: button }
backend.callbackManager.RunRelease(button)
}
func (backend *Backend) handleMotionNotify (
@ -96,10 +99,7 @@ func (backend *Backend) handleMotionNotify (
X: int(motionEvent.EventX),
Y: int(motionEvent.EventY),
})
backend.channel <- stone.EventMouseMove {
X: x,
Y: y,
}
backend.callbackManager.RunMouseMove(x, y)
}
func (backend *Backend) compressConfigureNotify (
@ -127,5 +127,5 @@ func (backend *Backend) compressConfigureNotify (
}
func (backend *Backend) shutDown () {
backend.channel <- stone.EventQuit { }
backend.callbackManager.RunQuit()
}

View File

@ -18,10 +18,17 @@ import "git.tebibyte.media/sashakoshka/stone"
import "github.com/flopp/go-findfont"
// factory instantiates an X backend.
func factory (application *stone.Application) (output stone.Backend, err error) {
func factory (
application *stone.Application,
callbackManager *stone.CallbackManager,
) (
output stone.Backend,
err error,
) {
backend := &Backend {
application: application,
config: application.Config(),
application: application,
config: application.Config(),
callbackManager: callbackManager,
}
// load font
@ -76,8 +83,6 @@ func factory (application *stone.Application) (output stone.Backend, err error)
backend.metrics.windowWidth, backend.metrics.windowHeight,
0)
backend.window.Map()
// TODO: also listen to mouse movement (compressed) and mouse and
// keyboard buttons (uncompressed)
err = backend.window.Listen (
xproto.EventMaskStructureNotify,
xproto.EventMaskPointerMotion,

View File

@ -14,17 +14,17 @@ import "github.com/jezek/xgbutil/xgraphics"
import "git.tebibyte.media/sashakoshka/stone"
type Backend struct {
application *stone.Application
config *stone.Config
connection *xgbutil.XUtil
window *xwindow.Window
canvas *xgraphics.Image
channel chan(stone.Event)
application *stone.Application
config *stone.Config
callbackManager *stone.CallbackManager
connection *xgbutil.XUtil
window *xwindow.Window
canvas *xgraphics.Image
drawCellBounds bool
drawBufferBounds bool
drawLock sync.Mutex
lock sync.Mutex
font struct {
face font.Face
@ -114,23 +114,6 @@ func (backend *Backend) calculateBufferSize () (width, height int) {
return
}
func (backend *Backend) reallocateCanvas () {
if backend.canvas != nil {
backend.canvas.Destroy()
}
backend.canvas = xgraphics.New (
backend.connection,
image.Rect (
0, 0,
backend.metrics.windowWidth,
backend.metrics.windowHeight))
backend.canvas.For (func (x, y int) xgraphics.BGRA {
return backend.colors[stone.ColorBackground]
})
backend.canvas.XSurfaceSet(backend.window.Id)
}
func (backend *Backend) cellAt (onScreen image.Point) (x, y int) {
x = (onScreen.X - backend.metrics.paddingX) / backend.metrics.cellWidth
y = (onScreen.Y - backend.metrics.paddingY) / backend.metrics.cellHeight

View File

@ -153,6 +153,10 @@ func (buffer *Buffer) SetRune (x, y int, content rune) {
buffer.lock.RLock()
defer buffer.lock.RUnlock()
buffer.setRune(x, y, content)
}
func (buffer *Buffer) setRune (x, y int, content rune) {
if buffer.isOutOfBounds(x, y) { return }
index := x + y * buffer.width
buffer.clean[index] = buffer.content[index].content == content
@ -169,7 +173,7 @@ func (buffer *Buffer) Write (bytes []byte) (bytesWritten int, err error) {
bytesWritten = len(bytes)
for _, character := range text {
buffer.SetRune(buffer.dot.x, buffer.dot.y, character)
buffer.setRune(buffer.dot.x, buffer.dot.y, character)
buffer.dot.x ++
if buffer.dot.x > buffer.width { break }
}

View File

@ -1,27 +1,40 @@
package stone
// Event can be any event.
type Event interface { }
// EventQuit is sent when the backend shuts down due to a window close, error,
// or something else.
type EventQuit struct { }
// EventPress is sent when a button is pressed, or a key repeat event is
// triggered.
type EventPress struct { Button }
// Release is sent when a button is released.
type EventRelease struct { Button }
// Resize is sent when the application window is resized by the user. This event
// must be handled, as it implies that the buffer has been resized and therefore
// cleared. Application.Draw() must be called after this event is recieved.
type EventResize struct { }
// EventMouseMove is sent when the mouse changes position. It contains the X and
// Y position of the mouse.
type EventMouseMove struct {
X int
Y int
type CallbackManager struct {
onQuit func ()
onPress func (button Button)
onRelease func (button Button)
onResize func ()
onMouseMove func (x, y int)
onStart func ()
}
func (manager *CallbackManager) RunQuit () {
if manager.onQuit == nil { return }
manager.onQuit()
}
func (manager *CallbackManager) RunPress (button Button) {
if manager.onPress == nil { return }
manager.onPress(button)
}
func (manager *CallbackManager) RunRelease (button Button) {
if manager.onRelease == nil { return }
manager.onRelease(button)
}
func (manager *CallbackManager) RunResize () {
if manager.onResize == nil { return }
manager.onResize()
}
func (manager *CallbackManager) RunMouseMove (x, y int) {
if manager.onMouseMove == nil { return }
manager.onMouseMove(x, y)
}
func (manager *CallbackManager) RunStart () {
if manager.onStart == nil { return }
manager.onStart()
}

View File

@ -10,7 +10,7 @@ var application = &stone.Application { }
var mousePressed bool
func main () {
application.SetTitle("hellorld")
application.SetTitle("drawing canvas")
application.SetSize(32, 16)
iconFile16, err := os.Open("assets/scaffold16.png")
@ -26,42 +26,37 @@ func main () {
application.SetIcon([]image.Image { icon16, icon32 })
channel, err := application.Run()
if err != nil { panic(err) }
application.OnPress(onPress)
application.OnRelease(onRelease)
application.OnMouseMove(onMouseMove)
application.OnQuit(onQuit)
application.Draw()
err = application.Run()
if err != nil { panic(err) }
}
for {
event := <- channel
switch event.(type) {
case stone.EventQuit:
os.Exit(0)
case stone.EventPress:
button := event.(stone.EventPress).Button
if button == stone.MouseButtonLeft {
mousePressed = true
application.SetRune(0, 0, '+')
application.Draw()
}
case stone.EventRelease:
button := event.(stone.EventRelease).Button
if button == stone.MouseButtonLeft {
mousePressed = false
application.SetRune(0, 0, 0)
application.Draw()
}
case stone.EventMouseMove:
event := event.(stone.EventMouseMove)
if mousePressed {
application.SetRune(event.X, event.Y, '#')
application.Draw()
}
case stone.EventResize:
application.Draw()
}
func onPress (button stone.Button) {
if button == stone.MouseButtonLeft {
mousePressed = true
application.SetRune(0, 0, '+')
application.Draw()
}
}
func onRelease (button stone.Button) {
if button == stone.MouseButtonLeft {
mousePressed = false
application.SetRune(0, 0, 0)
application.Draw()
}
}
func onMouseMove (x, y int) { if mousePressed {
application.SetRune(x, y, '#')
application.Draw()
}
}
func onQuit () {
os.Exit(0)
}