Merge pull request '(Mostly) under the hoood improvements' (#3) from fix-x-concurrency into main

Reviewed-on: #3
This commit is contained in:
Sasha Koshka 2022-11-17 18:33:36 +00:00
commit 19b744250f
12 changed files with 396 additions and 322 deletions

View File

@ -10,12 +10,12 @@ type Application struct {
icons []image.Image icons []image.Image
backend Backend backend Backend
config Config config Config
callbackManager CallbackManager
} }
// Run initializes the application, starts it, and then returns a channel that // 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. // broadcasts events. If no suitable backend can be found, an error is returned.
func (application *Application) Run () ( func (application *Application) Run () (
channel chan(Event),
err error, err error,
) { ) {
// default values for certain parameters // default values for certain parameters
@ -29,12 +29,46 @@ func (application *Application) Run () (
application.backend, err = instantiateBackend(application) application.backend, err = instantiateBackend(application)
if err != nil { return } if err != nil { return }
channel = make(chan(Event)) application.backend.Run()
go application.backend.Run(channel)
return 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. // Draw "commits" changes made in the buffer to the display.
func (application *Application) Draw () { func (application *Application) Draw () {
application.backend.Draw() application.backend.Draw()

View File

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

View File

@ -3,55 +3,113 @@ package x
import "image" import "image"
import "image/draw" import "image/draw"
import "golang.org/x/image/math/fixed" import "golang.org/x/image/math/fixed"
import "github.com/jezek/xgbutil/xgraphics"
import "git.tebibyte.media/sashakoshka/stone" import "git.tebibyte.media/sashakoshka/stone"
func (backend *Backend) Draw () { func (backend *Backend) Draw () {
backend.drawLock.Lock() backend.lock.Lock()
defer backend.drawLock.Unlock() defer backend.lock.Unlock()
boundsChanged := if backend.windowBoundsClean {
backend.memory.windowWidth != backend.metrics.windowWidth || backend.canvas.XPaintRects (
backend.memory.windowHeight != backend.metrics.windowHeight backend.window.Id,
backend.memory.windowWidth = backend.metrics.windowWidth backend.drawCells(false)...)
backend.memory.windowHeight = backend.metrics.windowHeight } else {
if boundsChanged {
backend.reallocateCanvas() backend.reallocateCanvas()
backend.drawCells(true) backend.drawCells(true)
backend.canvas.XDraw() backend.canvas.XDraw()
backend.canvas.XPaint(backend.window.Id) backend.canvas.XPaint(backend.window.Id)
} else { backend.windowBoundsClean = true
backend.updateWindowAreas(backend.drawCells(false)...)
} }
} }
func (backend *Backend) updateWindowAreas (areas ...image.Rectangle) { func (backend *Backend) reallocateCanvas () {
backend.canvas.XPaintRects(backend.window.Id, areas...) 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 }
cell := backend.application.GetForRendering(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 // TODO: cache these draws as non-transparent buffers with the
// application background color as the background. that way, we won't // application background color as the background. that way, we won't
// need to redraw the characters *or* composite them. // need to redraw the characters *or* composite them.
fillRectangle ( if drawBackground {
&image.Uniform { fillRectangle (
C: backend.config.Color(stone.ColorBackground), &image.Uniform {
}, C: backend.config.Color(stone.ColorBackground),
backend.canvas, },
backend.boundsOfCell(x, y)) backend.canvas,
backend.boundsOfCell(x, y))
}
if character < 32 { return } if character < 32 { return }
origin := backend.originOfCell(x, y + 1) origin := backend.originOfCell(x, y + 1)
destinationRectangle, mask, maskPoint, _, _ := backend.font.face.Glyph ( destinationRectangle, mask, maskPoint, _, ok := backend.font.face.Glyph (
fixed.Point26_6 { fixed.Point26_6 {
X: fixed.I(origin.X), X: fixed.I(origin.X),
Y: fixed.I(origin.Y - backend.metrics.descent), Y: fixed.I(origin.Y - backend.metrics.descent),
}, },
character) character)
if !ok {
println("warning")
strokeRectangle (
&image.Uniform {
C: backend.config.Color(stone.ColorForeground),
},
backend.canvas,
backend.boundsOfCell(x, y))
return
}
if backend.drawCellBounds { if backend.drawCellBounds {
strokeRectangle ( strokeRectangle (
&image.Uniform { &image.Uniform {
@ -73,36 +131,6 @@ func (backend *Backend) drawRune (x, y int, character rune, runeColor stone.Colo
draw.Over) 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 ( func fillRectangle (
source image.Image, source image.Image,
destination draw.Image, destination draw.Image,

View File

@ -8,17 +8,19 @@ import "github.com/jezek/xgbutil/xevent"
import "git.tebibyte.media/sashakoshka/stone" import "git.tebibyte.media/sashakoshka/stone"
func (backend *Backend) Run (channel chan(stone.Event)) { func (backend *Backend) Run () {
backend.channel = channel backend.callbackManager.RunStart()
backend.Draw()
xevent.Main(backend.connection) xevent.Main(backend.connection)
backend.shutDown() backend.callbackManager.RunQuit()
} }
func (backend *Backend) handleConfigureNotify ( func (backend *Backend) handleConfigureNotify (
connection *xgbutil.XUtil, connection *xgbutil.XUtil,
event xevent.ConfigureNotifyEvent, event xevent.ConfigureNotifyEvent,
) { ) {
backend.lock.Lock()
configureEvent := *event.ConfigureNotifyEvent configureEvent := *event.ConfigureNotifyEvent
newWidth := int(configureEvent.Width) newWidth := int(configureEvent.Width)
@ -33,23 +35,31 @@ func (backend *Backend) handleConfigureNotify (
configureEvent = configureEvent =
backend.compressConfigureNotify(configureEvent) backend.compressConfigureNotify(configureEvent)
// we should not resize the canvas while drawing is taking place
backend.drawLock.Lock()
defer backend.drawLock.Unlock()
// resize buffer // resize buffer
width, height := backend.calculateBufferSize() width, height := backend.calculateBufferSize()
backend.application.SetSize(width, height) backend.application.SetSize(width, height)
// position buffer in the center of the screen if backend.config.Center() {
frameWidth := width * backend.metrics.cellWidth // position buffer in the center of the screen
frameHeight := height * backend.metrics.cellHeight frameWidth := width * backend.metrics.cellWidth
backend.metrics.paddingX = frameHeight := height * backend.metrics.cellHeight
(backend.metrics.windowWidth - frameWidth) / 2 backend.metrics.paddingX =
backend.metrics.paddingY = (backend.metrics.windowWidth - frameWidth) / 2
(backend.metrics.windowHeight - frameHeight) / 2 backend.metrics.paddingY =
(backend.metrics.windowHeight - frameHeight) / 2
} else {
backend.metrics.paddingX = backend.metrics.padding
backend.metrics.paddingY = backend.metrics.padding
}
backend.channel <- stone.EventResize { } backend.windowBoundsClean = false
}
backend.lock.Unlock()
if sizeChanged {
backend.callbackManager.RunResize()
backend.Draw()
} }
} }
@ -58,9 +68,7 @@ func (backend *Backend) handleButtonPress (
event xevent.ButtonPressEvent, event xevent.ButtonPressEvent,
) { ) {
buttonEvent := *event.ButtonPressEvent buttonEvent := *event.ButtonPressEvent
backend.channel <- stone.EventPress { backend.callbackManager.RunPress(stone.Button(buttonEvent.Detail + 127))
Button: stone.Button(buttonEvent.Detail + 127),
}
} }
func (backend *Backend) handleButtonRelease ( func (backend *Backend) handleButtonRelease (
@ -68,9 +76,7 @@ func (backend *Backend) handleButtonRelease (
event xevent.ButtonReleaseEvent, event xevent.ButtonReleaseEvent,
) { ) {
buttonEvent := *event.ButtonReleaseEvent buttonEvent := *event.ButtonReleaseEvent
backend.channel <- stone.EventRelease { backend.callbackManager.RunRelease(stone.Button(buttonEvent.Detail + 127))
Button: stone.Button(buttonEvent.Detail + 127),
}
} }
func (backend *Backend) handleKeyPress ( func (backend *Backend) handleKeyPress (
@ -79,7 +85,7 @@ func (backend *Backend) handleKeyPress (
) { ) {
keyEvent := *event.KeyPressEvent keyEvent := *event.KeyPressEvent
button := backend.keycodeToButton(keyEvent.Detail, keyEvent.State) button := backend.keycodeToButton(keyEvent.Detail, keyEvent.State)
backend.channel <- stone.EventPress { Button: button } backend.callbackManager.RunPress(button)
} }
func (backend *Backend) handleKeyRelease ( func (backend *Backend) handleKeyRelease (
@ -88,7 +94,7 @@ func (backend *Backend) handleKeyRelease (
) { ) {
keyEvent := *event.KeyReleaseEvent keyEvent := *event.KeyReleaseEvent
button := backend.keycodeToButton(keyEvent.Detail, keyEvent.State) button := backend.keycodeToButton(keyEvent.Detail, keyEvent.State)
backend.channel <- stone.EventRelease { Button: button } backend.callbackManager.RunRelease(button)
} }
func (backend *Backend) handleMotionNotify ( func (backend *Backend) handleMotionNotify (
@ -100,10 +106,7 @@ func (backend *Backend) handleMotionNotify (
X: int(motionEvent.EventX), X: int(motionEvent.EventX),
Y: int(motionEvent.EventY), Y: int(motionEvent.EventY),
}) })
backend.channel <- stone.EventMouseMove { backend.callbackManager.RunMouseMove(x, y)
X: x,
Y: y,
}
} }
func (backend *Backend) compressConfigureNotify ( func (backend *Backend) compressConfigureNotify (
@ -129,7 +132,3 @@ func (backend *Backend) compressConfigureNotify (
return return
} }
func (backend *Backend) shutDown () {
backend.channel <- stone.EventQuit { }
}

View File

@ -18,10 +18,17 @@ import "git.tebibyte.media/sashakoshka/stone"
import "github.com/flopp/go-findfont" import "github.com/flopp/go-findfont"
// factory instantiates an X backend. // 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 { backend := &Backend {
application: application, application: application,
config: application.Config(), config: application.Config(),
callbackManager: callbackManager,
} }
// load font // load font
@ -76,8 +83,6 @@ func factory (application *stone.Application) (output stone.Backend, err error)
backend.metrics.windowWidth, backend.metrics.windowHeight, backend.metrics.windowWidth, backend.metrics.windowHeight,
0) 0)
backend.window.Map() backend.window.Map()
// TODO: also listen to mouse movement (compressed) and mouse and
// keyboard buttons (uncompressed)
err = backend.window.Listen ( err = backend.window.Listen (
xproto.EventMaskStructureNotify, xproto.EventMaskStructureNotify,
xproto.EventMaskPointerMotion, xproto.EventMaskPointerMotion,
@ -110,7 +115,7 @@ func factory (application *stone.Application) (output stone.Backend, err error)
// attatch graceful close handler // attatch graceful close handler
backend.window.WMGracefulClose (func (window *xwindow.Window) { backend.window.WMGracefulClose (func (window *xwindow.Window) {
backend.window.Destroy() backend.window.Destroy()
backend.shutDown() xevent.Quit(backend.connection)
}) })
// attatch event handlers // attatch event handlers

View File

@ -14,17 +14,17 @@ import "github.com/jezek/xgbutil/xgraphics"
import "git.tebibyte.media/sashakoshka/stone" import "git.tebibyte.media/sashakoshka/stone"
type Backend struct { type Backend struct {
application *stone.Application application *stone.Application
config *stone.Config config *stone.Config
connection *xgbutil.XUtil callbackManager *stone.CallbackManager
window *xwindow.Window connection *xgbutil.XUtil
canvas *xgraphics.Image window *xwindow.Window
channel chan(stone.Event) canvas *xgraphics.Image
drawCellBounds bool drawCellBounds bool
drawBufferBounds bool drawBufferBounds bool
drawLock sync.Mutex lock sync.Mutex
font struct { font struct {
face font.Face face font.Face
@ -43,10 +43,7 @@ type Backend struct {
descent int descent int
} }
memory struct { windowBoundsClean bool
windowWidth int
windowHeight int
}
} }
func (backend *Backend) SetTitle (title string) (err error) { func (backend *Backend) SetTitle (title string) (err error) {
@ -114,23 +111,6 @@ func (backend *Backend) calculateBufferSize () (width, height int) {
return 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) { func (backend *Backend) cellAt (onScreen image.Point) (x, y int) {
x = (onScreen.X - backend.metrics.paddingX) / backend.metrics.cellWidth x = (onScreen.X - backend.metrics.paddingX) / backend.metrics.cellWidth
y = (onScreen.Y - backend.metrics.paddingY) / backend.metrics.cellHeight y = (onScreen.Y - backend.metrics.paddingY) / backend.metrics.cellHeight

206
buffer.go
View File

@ -1,5 +1,7 @@
package stone package stone
import "sync"
// Color represents all the different colors a cell can be. // Color represents all the different colors a cell can be.
type Color uint8 type Color uint8
@ -22,6 +24,8 @@ const (
StyleNormal Style = iota StyleNormal Style = iota
StyleBold Style = iota >> 1 StyleBold Style = iota >> 1
StyleItalic StyleItalic
StyleUnderline
StyleHighlight
StyleBoldItalic Style = StyleBold | StyleItalic StyleBoldItalic Style = StyleBold | StyleItalic
) )
@ -51,18 +55,38 @@ func (cell Cell) Rune () (content rune) {
return return
} }
// Buffer is a basic grid of cells. // Buffer represents a two dimensional text buffer.
type Buffer struct { type Buffer interface {
content []Cell Size () (with, height int)
width int Cell (x, y int) (cell Cell)
height int SetColor (x, y int, color Color)
Dot struct { SetSize (with, height int)
X int SetStyle (x, y int, style Style)
Y int SetRune (x, y int, content rune)
} Clear ()
} }
func (buffer *Buffer) isOutOfBounds (x, y int) (outOfBounds bool) { // DamageBuffer is a two dimensional text buffer that stores a grid of cells, as
// well as information stating whether each cell is clean or dirty. Cells are
// dirty by default, are only clean when marked as clean, and become dirty again
// when they are altered in some way.
type DamageBuffer struct {
content []Cell
onScreen []Cell
width int
height int
dot struct {
x int
y int
}
// This should be write locked when resizing the buffer, and read locked
// when writing to cells or reading information about the buffer.
lock sync.RWMutex
}
func (buffer *DamageBuffer) isOutOfBounds (x, y int) (outOfBounds bool) {
outOfBounds = outOfBounds =
x < 0 || x < 0 ||
y < 0 || y < 0 ||
@ -72,122 +96,105 @@ func (buffer *Buffer) isOutOfBounds (x, y int) (outOfBounds bool) {
} }
// Size returns the width and height of the buffer. // Size returns the width and height of the buffer.
func (buffer *Buffer) Size () (width, height int) { func (buffer *DamageBuffer) Size () (width, height int) {
buffer.lock.RLock()
defer buffer.lock.RUnlock()
width = buffer.width width = buffer.width
height = buffer.height height = buffer.height
return return
} }
// SetSize sets the width and height of the buffer. This clears all data in the // SetDot sets the buffer's text insertion position relative to the buffer
// buffer. If the width or height is negative, this method does nothing. // origin point (0, 0).
func (buffer *Buffer) SetSize (width, height int) { func (buffer *DamageBuffer) SetDot (x, y int) {
if width < 0 || height < 0 { return } buffer.dot.x = x
buffer.width = width buffer.dot.y = y
buffer.height = height
buffer.content = make([]Cell, width * height)
for index := 0; index < len(buffer.content); index ++ {
buffer.content[index].color = ColorForeground
}
} }
// Cell returns the cell at the specified x and y coordinates. If the // Cell returns the cell at the specified x and y coordinates. If the
// coordinates are out of bounds, this method will return a blank cell. // coordinates are out of bounds, this method will return a blank cell.
func (buffer *Buffer) Cell (x, y int) (cell Cell) { func (buffer *DamageBuffer) Cell (x, y int) (cell Cell) {
buffer.lock.RLock()
defer buffer.lock.RUnlock()
if buffer.isOutOfBounds(x, y) { return } if buffer.isOutOfBounds(x, y) { return }
cell = buffer.content[x + y * buffer.width] cell = buffer.content[x + y * buffer.width]
return return
} }
// SetColor sets the color of the cell at the specified x and y coordinates. // SetColor sets the color of the cell at the specified x and y coordinates.
func (buffer *Buffer) SetColor (x, y int, color Color) { func (buffer *DamageBuffer) SetColor (x, y int, color Color) {
buffer.lock.RLock()
defer buffer.lock.RUnlock()
if buffer.isOutOfBounds(x, y) { return } if buffer.isOutOfBounds(x, y) { return }
buffer.content[x + y * buffer.width].color = color buffer.content[x + y * buffer.width].color = color
} }
// SetSize sets the width and height of the buffer. This clears all data in the
// buffer. If the width or height is negative, this method does nothing.
func (buffer *DamageBuffer) SetSize (width, height int) {
buffer.lock.Lock()
defer buffer.lock.Unlock()
if width < 0 || height < 0 { return }
buffer.width = width
buffer.height = height
buffer.content = make([]Cell, width * height)
buffer.onScreen = make([]Cell, width * height)
for index := 0; index < len(buffer.content); index ++ {
buffer.content[index].color = ColorForeground
}
}
// SetStyle sets the style of the cell at the specified x and y coordinates. // SetStyle sets the style of the cell at the specified x and y coordinates.
func (buffer *Buffer) SetStyle (x, y int, style Style) { func (buffer *DamageBuffer) SetStyle (x, y int, style Style) {
buffer.lock.RLock()
defer buffer.lock.RUnlock()
if buffer.isOutOfBounds(x, y) { return } if buffer.isOutOfBounds(x, y) { return }
buffer.content[x + y * buffer.width].style = style buffer.content[x + y * buffer.width].style = style
} }
// SetRune sets the rune of the cell at the specified x and y coordinates. // SetRune sets the rune of the cell at the specified x and y coordinates.
func (buffer *Buffer) SetRune (x, y int, content rune) { func (buffer *DamageBuffer) SetRune (x, y int, content rune) {
buffer.lock.RLock()
defer buffer.lock.RUnlock()
buffer.setRune(x, y, content)
}
// Clear resets the entire buffer.
func (buffer *DamageBuffer) Clear () {
buffer.lock.RLock()
defer buffer.lock.RUnlock()
for index := 0; index < len(buffer.content); index ++ {
buffer.content[index] = Cell {
color: ColorForeground,
}
}
}
func (buffer *DamageBuffer) setRune (x, y int, content rune) {
if buffer.isOutOfBounds(x, y) { return } if buffer.isOutOfBounds(x, y) { return }
buffer.content[x + y * buffer.width].content = content buffer.content[x + y * buffer.width].content = content
} }
// Write writes data stored in a byte slice to the buffer at the current dot // Write writes data stored in a byte slice to the buffer at the current dot
// position. This makes Buffer an io.Writer. // position. This makes Buffer an io.Writer.
func (buffer *Buffer) Write (bytes []byte) (bytesWritten int, err error) {
text := string(bytes)
bytesWritten = len(bytes)
for _, character := range text {
buffer.SetRune(buffer.Dot.X, buffer.Dot.Y, character)
buffer.Dot.X ++
if buffer.Dot.X > buffer.width { break }
}
return
}
// ResetDot is a convenience method to reset the dot to the buffer origin point
// (0, 0).
func (buffer *Buffer) ResetDot () {
buffer.Dot.X = 0
buffer.Dot.Y = 0
}
// DamageBuffer is a special buffer that keeps track of damage information.
// Cells are dirty by default, are only clean when marked as clean, and become
// dirty again when they are altered in some way.
type DamageBuffer struct {
Buffer
clean []bool
}
// SetSize sets the width and height of the buffer. This clears all data in the
// buffer. If the width or height is negative, this method does nothing.
func (buffer *DamageBuffer) SetSize (width, height int) {
if width < 0 || height < 0 { return }
buffer.Buffer.SetSize(width, height)
buffer.clean = make([]bool, width * height)
}
// SetColor sets the color of the cell at the specified x and y coordinates.
func (buffer *DamageBuffer) SetColor (x, y int, color Color) {
if buffer.isOutOfBounds(x, y) { return }
index := x + y * buffer.width
buffer.clean[index] = buffer.content[index].color == color
buffer.Buffer.SetColor(x, y, color)
}
// SetStyle sets the style of the cell at the specified x and y coordinates.
func (buffer *DamageBuffer) SetStyle (x, y int, style Style) {
if buffer.isOutOfBounds(x, y) { return }
index := x + y * buffer.width
buffer.clean[index] = buffer.content[index].style == style
buffer.Buffer.SetStyle(x, y, style)
}
// SetRune sets the rune of the cell at the specified x and y coordinates.
func (buffer *DamageBuffer) 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
buffer.Buffer.SetRune(x, y, content)
}
// Write writes data stored in a byte slice to the buffer at the current dot
// position. This makes DamageBuffer an io.Writer.
func (buffer *DamageBuffer) Write (bytes []byte) (bytesWritten int, err error) { func (buffer *DamageBuffer) Write (bytes []byte) (bytesWritten int, err error) {
buffer.lock.RLock()
defer buffer.lock.RUnlock()
text := string(bytes) text := string(bytes)
bytesWritten = len(bytes) bytesWritten = len(bytes)
for _, character := range text { 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 ++ buffer.dot.x ++
if buffer.Dot.X > buffer.width { break } if buffer.dot.x > buffer.width { break }
} }
return return
@ -196,13 +203,24 @@ func (buffer *DamageBuffer) Write (bytes []byte) (bytesWritten int, err error) {
// Clean returns whether or not the cell at the specified x and y coordinates is // Clean returns whether or not the cell at the specified x and y coordinates is
// clean. // clean.
func (buffer *DamageBuffer) Clean (x, y int) (clean bool) { func (buffer *DamageBuffer) Clean (x, y int) (clean bool) {
buffer.lock.RLock()
defer buffer.lock.RUnlock()
if buffer.isOutOfBounds(x, y) { return } if buffer.isOutOfBounds(x, y) { return }
clean = buffer.clean[x + y * buffer.width] index := x + y * buffer.width
clean = buffer.content[index] == buffer.onScreen[index]
return return
} }
// MarkClean marks the cell at the specified x and y coordinates as clean. // GetForRendering returns the cell at the specified x and y coordinates and
func (buffer *DamageBuffer) MarkClean (x, y int) { // marks it as clean.
func (buffer *DamageBuffer) GetForRendering (x, y int) (cell Cell) {
buffer.lock.RLock()
defer buffer.lock.RUnlock()
if buffer.isOutOfBounds(x, y) { return } if buffer.isOutOfBounds(x, y) { return }
buffer.clean[x + y * buffer.width] = true index := x + y * buffer.width
buffer.onScreen[index] = buffer.content[index]
cell = buffer.content[index]
return
} }

View File

@ -12,6 +12,7 @@ import "path/filepath"
type Config struct { type Config struct {
colors [8]color.Color colors [8]color.Color
padding int padding int
center bool
fontSize int fontSize int
fontName string fontName string
} }
@ -29,6 +30,13 @@ func (config *Config) Padding () (padding int) {
return return
} }
// Center returns whether the buffer should be displayed in the center of the
// window like in kitty, or aligned to one corner like in gnome-terminal.
func (config *Config) Center () (center bool) {
center = config.center
return
}
// FontSize specifies how big the font should be. // FontSize specifies how big the font should be.
func (config *Config) FontSize () (fontSize int) { func (config *Config) FontSize () (fontSize int) {
fontSize = config.fontSize fontSize = config.fontSize
@ -42,7 +50,6 @@ func (config *Config) FontName () (fontName string) {
} }
func (config *Config) load () { func (config *Config) load () {
// TODO: load these from a file
config.colors = [8]color.Color { config.colors = [8]color.Color {
// background // background
color.RGBA { R: 0, G: 0, B: 0, A: 0 }, color.RGBA { R: 0, G: 0, B: 0, A: 0 },
@ -57,9 +64,9 @@ func (config *Config) load () {
// green // green
color.RGBA { R: 0x00, G: 0xFF, B: 0x00, A: 0xFF }, color.RGBA { R: 0x00, G: 0xFF, B: 0x00, A: 0xFF },
// blue // blue
color.RGBA { R: 0x00, G: 0x00, B: 0xFF, A: 0xFF }, color.RGBA { R: 0x00, G: 0x80, B: 0xFF, A: 0xFF },
// purple // purple
color.RGBA { R: 0x80, G: 0x00, B: 0xFF, A: 0xFF }, color.RGBA { R: 0x80, G: 0x40, B: 0xFF, A: 0xFF },
} }
config.fontName = "" config.fontName = ""
config.fontSize = 11 config.fontSize = 11
@ -99,8 +106,14 @@ func (config *Config) loadFile (path string) {
} }
key = strings.TrimSpace(key) key = strings.TrimSpace(key)
value = strings.TrimSpace(value) value = strings.TrimSpace(value)
var valueInt int var valueInt int
var valueColor color.Color var valueColor color.Color
var valueBoolean bool
if value == "true" {
valueBoolean = true
}
if value[0] == '#' { if value[0] == '#' {
if len(value) != 7 { if len(value) != 7 {
@ -135,6 +148,8 @@ func (config *Config) loadFile (path string) {
config.fontSize = valueInt config.fontSize = valueInt
case "padding": case "padding":
config.padding = valueInt config.padding = valueInt
case "center":
config.center = valueBoolean
case "colorBackground": case "colorBackground":
config.colors[ColorBackground] = valueColor config.colors[ColorBackground] = valueColor
case "colorForeground": case "colorForeground":

View File

@ -1,27 +1,40 @@
package stone package stone
// Event can be any event. type CallbackManager struct {
type Event interface { } onQuit func ()
onPress func (button Button)
// EventQuit is sent when the backend shuts down due to a window close, error, onRelease func (button Button)
// or something else. onResize func ()
type EventQuit struct { } onMouseMove func (x, y int)
onStart func ()
// EventPress is sent when a button is pressed, or a key repeat event is }
// triggered.
type EventPress struct { Button } func (manager *CallbackManager) RunQuit () {
if manager.onQuit == nil { return }
// Release is sent when a button is released. manager.onQuit()
type EventRelease struct { Button } }
// Resize is sent when the application window is resized by the user. This event func (manager *CallbackManager) RunPress (button Button) {
// must be handled, as it implies that the buffer has been resized and therefore if manager.onPress == nil { return }
// cleared. Application.Draw() must be called after this event is recieved. manager.onPress(button)
type EventResize struct { } }
// EventMouseMove is sent when the mouse changes position. It contains the X and func (manager *CallbackManager) RunRelease (button Button) {
// Y position of the mouse. if manager.onRelease == nil { return }
type EventMouseMove struct { manager.onRelease(button)
X int }
Y int
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 @@ import _ "git.tebibyte.media/sashakoshka/stone/backends/x"
var application = &stone.Application { } var application = &stone.Application { }
func main () { func main () {
application.SetTitle("hellorld") application.SetTitle("color demo")
application.SetSize(12, 7) application.SetSize(12, 7)
iconFile16, err := os.Open("assets/scaffold16.png") iconFile16, err := os.Open("assets/scaffold16.png")
@ -26,29 +26,26 @@ func main () {
application.SetIcon([]image.Image { icon16, icon32 }) application.SetIcon([]image.Image { icon16, icon32 })
channel, err := application.Run() application.OnStart(onStart)
application.OnResize(onResize)
err = application.Run()
if err != nil { panic(err) } if err != nil { panic(err) }
}
func onStart () {
redraw() redraw()
}
for { func onResize () {
event := <- channel redraw()
switch event.(type) {
case stone.EventQuit:
os.Exit(0)
case stone.EventResize:
redraw()
}
}
} }
func redraw () { func redraw () {
text := "RAINBOW :D" text := "RAINBOW :D"
width, height := application.Size() width, height := application.Size()
application.Dot.X = (width - len(text)) / 2 application.SetDot((width - len(text)) / 2, height / 2)
application.Dot.Y = height / 2
fmt.Fprintln(application, text) fmt.Fprintln(application, text)
application.SetColor(0, 0, stone.ColorYellow) application.SetColor(0, 0, stone.ColorYellow)
@ -70,6 +67,4 @@ func redraw () {
application.SetRune(x, height - 1, '=') application.SetRune(x, height - 1, '=')
application.SetColor(x, height - 1, stone.ColorRed) application.SetColor(x, height - 1, stone.ColorRed)
} }
application.Draw()
} }

View File

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

View File

@ -10,7 +10,6 @@ import _ "git.tebibyte.media/sashakoshka/stone/backends/x"
var application = &stone.Application { } var application = &stone.Application { }
var currentTime = time.Time { } var currentTime = time.Time { }
var tickPing = make(chan(struct { }))
func main () { func main () {
application.SetTitle("hellorld") application.SetTitle("hellorld")
@ -29,33 +28,26 @@ func main () {
application.SetIcon([]image.Image { icon16, icon32 }) application.SetIcon([]image.Image { icon16, icon32 })
channel, err := application.Run() application.OnStart(onStart)
if err != nil { panic(err) } application.OnResize(onResize)
err = application.Run()
if err != nil { panic(err) }
}
func onStart () {
redraw() redraw()
go tick() go tick()
}
for { func onResize () {
select { redraw()
case <- tickPing:
redraw()
case event := <- channel:
switch event.(type) {
case stone.EventQuit:
os.Exit(0)
case stone.EventResize:
redraw()
}
}
}
} }
func redraw () { func redraw () {
currentTime = time.Now() currentTime = time.Now()
application.ResetDot() application.SetDot(0, 0)
fmt.Fprintln(application, "hellorld!") fmt.Fprintln(application, "hellorld!")
hour := currentTime.Hour() hour := currentTime.Hour()
@ -70,13 +62,12 @@ func redraw () {
application.SetRune(5, 1, ':') application.SetRune(5, 1, ':')
application.SetRune(6, 1, rune(second / 10 + 48)) application.SetRune(6, 1, rune(second / 10 + 48))
application.SetRune(7, 1, rune(second % 10 + 48)) application.SetRune(7, 1, rune(second % 10 + 48))
application.Draw()
} }
func tick () { func tick () {
for { for {
tickPing <- struct { } { } redraw()
application.Draw()
time.Sleep(time.Second) time.Sleep(time.Second)
} }
} }