5 Commits

16 changed files with 238 additions and 287 deletions

View File

@@ -11,6 +11,7 @@ type Application struct {
backend Backend
config Config
callbackManager CallbackManager
imageManager ImageManager
}
// Run initializes the application, starts it, and then returns a channel that
@@ -33,60 +34,42 @@ func (application *Application) Run () (
return
}
// OnQuit registers an event handler to be called just before the application
// quits. This can happen when the user closes the application, or the backend
// experiences an unrecoverable error.
func (application *Application) OnQuit (
onQuit func (),
) {
application.callbackManager.onQuit = onQuit
}
// OnPress registers an event handler to be called when a key or mouse button
// is pressed.
func (application *Application) OnPress (
onPress func (button Button, modifiers Modifiers),
onPress func (button Button),
) {
application.callbackManager.onPress = onPress
}
// OnPress registers an event handler to be called when a key or mouse button
// is released.
func (application *Application) OnRelease (
onRelease func (button Button),
) {
application.callbackManager.onRelease = onRelease
}
// OnResize registers an event handler to be called when the application window
// is resized. After the event handler is called, any updates it makes will
// automatically be pushed to the screen.
func (application *Application) OnResize (
onResize func (),
) {
application.callbackManager.onResize = onResize
}
// OnMouseMove registers an event handler to be called when mouse motion is
// detected. The coordinates of the cell that the mouse now hovers over are
// given as input.
func (application *Application) OnMouseMove (
onMouseMove func (x, y int),
) {
application.callbackManager.onMouseMove = onMouseMove
}
// OnScroll registers an event handler to be called when the user uses the mouse
// scroll wheel. Horizontal and vertical amounts are given as input.
func (application *Application) OnScroll (
onScroll func (x, y int),
) {
application.callbackManager.onScroll = onScroll
}
// OnStart registers an event handler to be called once when the application
// starts, right before the first time updates are pushed to the screen.
// Anything done in here will be the first thing to appear on screen.
func (application *Application) OnStart (
onStart func (),
) {
@@ -138,3 +121,21 @@ func (application *Application) Config () (config *Config) {
config = &application.config
return
}
// AddImage adds a new image buffer and returns a pointer to it.
func (application *Application) NewImage () (im *ColorImage) {
cellWidth, cellHeight := application.backend.CellMetrics()
im = &ColorImage {
cellWidth: cellWidth,
cellHeight: cellHeight,
}
application.imageManager.Add(im)
return
}
// Remove removes the specified image buffer, if the application has it. If the
// image was found and removed, removed will be true.
func (application *Application) RemoveImage (im *ColorImage) (removed bool) {
removed = application.imageManager.Remove(im)
return
}

View File

@@ -3,54 +3,18 @@ package stone
import "image"
import "errors"
// Backend represents a backend for stone. Backends can be registered for use
// with the RegisterBackend() function. All of the below methods MUST be thread
// safe!
type Backend interface {
// Run is the backend's event loop. It must cleanly exit when the user
// closes the window, but not before calling the OnQuit event. Run
// must call event handlers within its own event loop in a
// non-concurrent fashion.
//
// The OnStart event handler must run after the backend has been fully
// initialized, and right before updates are first pushed to the screen.
// Whatever the application draws from within this event handler must be
// the first thing that appears on-screen.
//
// The OnResize event handler must run whenever the window is resized.
// The backend must push updates to the screen after OnResize has been
// run.
//
// The backend must not push updates to the screen in any other case,
// except when its Draw() method is specifically called.
//
// The OnPress, OnRelease, OnMouseMove, and OnMouseScroll events are to
// be called when such events happen. It is reccommended to compress
// resize, mouse move, and mouse scroll events whenever possible to
// reduce the likelihood of event buildup.
Run ()
// SetTitle sets the application title. This will most often be the
// window title. This method may not always produce an effect, depending
// on the backend.
Run ()
SetTitle (title string) (err error)
// SetIcon takes in a set of images of different sizes and sets the
// window's icon to them. This method may not always produce an effect,
// depending on the backend.
SetIcon (icons []image.Image) (err error)
// Draw pushes all updates made to the application's buffer to the
// screen.
Draw ()
SetIcon (icons []image.Image) (err error)
Draw ()
CellMetrics () (width, height int)
}
// BackendFactory must completely initialize a backend, and return it. If
// anything goes wrong, it must stop, clean up any resources and return an
// error so another backend can be chosen.
type BackendFactory func (
application *Application,
application *Application,
callbackManager *CallbackManager,
imageManager *ImageManager,
) (
backend Backend,
err error,
@@ -58,7 +22,6 @@ type BackendFactory func (
var factories []BackendFactory
// RegisterBackend registers a backend factory.
func RegisterBackend (factory BackendFactory) {
factories = append(factories, factory)
}
@@ -66,7 +29,10 @@ func RegisterBackend (factory BackendFactory) {
func instantiateBackend (application *Application) (backend Backend, err error) {
// find a suitable backend
for _, factory := range factories {
backend, err = factory(application, &application.callbackManager)
backend, err = factory (
application,
&application.callbackManager,
&application.imageManager)
if err == nil && backend != nil { return }
}

View File

@@ -90,16 +90,17 @@ func (backend *Backend) drawRune (
}
if character < 32 { return }
origin := backend.originOfCell(x, y + 1)
destinationRectangle, mask, maskPoint, _, ok := backend.font.face.Glyph (
fixed.Point26_6 {
X: fixed.I(origin.X),
Y: fixed.I(origin.Y),
Y: fixed.I(origin.Y - backend.metrics.descent),
},
character)
if !ok {
println("warning")
strokeRectangle (
&image.Uniform {
C: backend.config.Color(stone.ColorForeground),
@@ -109,7 +110,7 @@ func (backend *Backend) drawRune (
return
}
if backend.drawCellBounds {
if backend.drawCellBounds {
strokeRectangle (
&image.Uniform {
C: backend.config.Color(stone.ColorForeground),
@@ -117,71 +118,17 @@ func (backend *Backend) drawRune (
backend.canvas,
backend.boundsOfCell(x, y))
}
// cue a series of pointless optimizations
alphaMask, isAlpha := mask.(*image.Alpha)
if isAlpha {
backend.sprayRuneMaskAlpha (
alphaMask, destinationRectangle,
maskPoint, backend.colors[runeColor])
} else {
backend.sprayRuneMask (
mask, destinationRectangle,
maskPoint, backend.colors[runeColor])
}
}
func (backend *Backend) sprayRuneMask (
mask image.Image,
bounds image.Rectangle,
maskPoint image.Point,
fill xgraphics.BGRA,
) {
maxX := bounds.Max.X - bounds.Min.X
maxY := bounds.Max.Y - bounds.Min.Y
for y := 0; y < maxY; y ++ {
for x := 0; x < maxX; x ++ {
_, _, _,
alpha := mask.At(x + maskPoint.X, y + maskPoint.Y).RGBA()
backend.canvas.SetBGRA (
x + bounds.Min.X,
y + bounds.Min.Y - backend.metrics.descent,
xgraphics.BlendBGRA (
backend.colors[stone.ColorBackground],
xgraphics.BGRA {
R: fill.R,
G: fill.G,
B: fill.B,
A: uint8(alpha >> 8),
}))
}}
}
func (backend *Backend) sprayRuneMaskAlpha (
mask *image.Alpha,
bounds image.Rectangle,
maskPoint image.Point,
fill xgraphics.BGRA,
) {
maxX := bounds.Max.X - bounds.Min.X
maxY := bounds.Max.Y - bounds.Min.Y
for y := 0; y < maxY; y ++ {
for x := 0; x < maxX; x ++ {
alpha := mask.AlphaAt(x + maskPoint.X, y + maskPoint.Y).A
backend.canvas.SetBGRA (
x + bounds.Min.X,
y + bounds.Min.Y - backend.metrics.descent,
xgraphics.BlendBGRA (
backend.colors[stone.ColorBackground],
xgraphics.BGRA {
R: fill.R,
G: fill.G,
B: fill.B,
A: alpha,
}))
}}
draw.DrawMask (
backend.canvas,
destinationRectangle,
&image.Uniform {
C: backend.config.Color(runeColor),
},
image.Point { },
mask,
maskPoint,
draw.Over)
}
func fillRectangle (

View File

@@ -91,9 +91,7 @@ func (backend *Backend) handleButtonPress (
backend.compressScrollSum(&sum)
backend.callbackManager.RunScroll(sum.x, sum.y)
} else {
backend.callbackManager.RunPress (
stone.Button(buttonEvent.Detail + 127),
stone.Modifiers { })
backend.callbackManager.RunPress(stone.Button(buttonEvent.Detail + 127))
}
}
@@ -112,15 +110,7 @@ func (backend *Backend) handleKeyPress (
) {
keyEvent := *event.KeyPressEvent
button := backend.keycodeToButton(keyEvent.Detail, keyEvent.State)
backend.callbackManager.RunPress (button, stone.Modifiers {
// FIXME these may not be correct in all cases
Shift: (keyEvent.State & xproto.ModMaskShift) > 0,
Control: (keyEvent.State & xproto.ModMaskControl) > 0,
Alt: (keyEvent.State & xproto.ModMask1) > 0,
// Meta: (keyEvent.State & xproto.??) > 0,
Super: (keyEvent.State & xproto.ModMask4) > 0,
// Hyper: (keyEvent.State & xproto.??) > 0,
})
backend.callbackManager.RunPress(button)
}
func (backend *Backend) handleKeyRelease (

View File

@@ -19,8 +19,9 @@ import "github.com/flopp/go-findfont"
// factory instantiates an X backend.
func factory (
application *stone.Application,
application *stone.Application,
callbackManager *stone.CallbackManager,
imageManager *stone.ImageManager,
) (
output stone.Backend,
err error,
@@ -29,6 +30,7 @@ func factory (
application: application,
config: application.Config(),
callbackManager: callbackManager,
imageManager: imageManager,
}
// load font
@@ -133,7 +135,7 @@ func factory (
Connect(backend.connection, backend.window.Id)
// uncomment these to draw debug bounds
// backend.drawCellBounds = true
// backend.drawCellBounds = true
// backend.drawBufferBounds = true
output = backend

View File

@@ -1,6 +1,5 @@
package x
// import "fmt"
import "unicode"
import "github.com/jezek/xgb/xproto"
import "github.com/jezek/xgbutil/keybind"
@@ -37,15 +36,10 @@ var buttonCodeTable = map[xproto.Keysym] stone.Button {
0xFFE2: stone.KeyRightShift,
0xFFE3: stone.KeyLeftControl,
0xFFE4: stone.KeyRightControl,
0xFFE7: stone.KeyLeftMeta,
0xFFE8: stone.KeyRightMeta,
0xFFE9: stone.KeyLeftAlt,
0xFFEA: stone.KeyRightAlt,
0xFFEB: stone.KeyLeftSuper,
0xFFEC: stone.KeyRightSuper,
0xFFED: stone.KeyLeftHyper,
0xFFEE: stone.KeyRightHyper,
0xFFFF: stone.KeyDelete,
@@ -61,14 +55,6 @@ var buttonCodeTable = map[xproto.Keysym] stone.Button {
0xFFC7: stone.KeyF10,
0xFFC8: stone.KeyF11,
0xFFC9: stone.KeyF12,
// TODO: send this whenever a compose key, dead key, etc is pressed,
// and then send the resulting character while witholding the key
// presses that were used to compose it. As far as the program is
// concerned, a magical key with the final character was pressed and the
// KeyDead key is just so that the program might provide some visual
// feedback to the user while input is being waited for.
0xFF20: stone.KeyDead,
}
func (backend *Backend) keycodeToButton (
@@ -98,7 +84,7 @@ func (backend *Backend) keycodeToButton (
symbol3 = symbol1
case symbol3 == 0 && symbol4 == 0:
symbol3 = symbol1
symbol4 = symbol2
symbol2 = symbol2
case symbol4 == 0:
symbol4 = 0
}
@@ -120,8 +106,7 @@ func (backend *Backend) keycodeToButton (
symbol2Rune = unicode.ToUpper(symbol1Rune)
cased = true
} else {
symbol2 = symbol1
symbol2Rune = symbol1Rune
symbol2 = symbol1
}
}
if symbol4 == 0 {
@@ -132,8 +117,7 @@ func (backend *Backend) keycodeToButton (
symbol4Rune = unicode.ToUpper(symbol3Rune)
cased = true
} else {
symbol4 = symbol3
symbol4Rune = symbol3Rune
symbol4 = symbol3
}
}

View File

@@ -17,6 +17,7 @@ type Backend struct {
application *stone.Application
config *stone.Config
callbackManager *stone.CallbackManager
imageManager *stone.ImageManager
connection *xgbutil.XUtil
window *xwindow.Window
canvas *xgraphics.Image
@@ -88,6 +89,12 @@ func (backend *Backend) SetIcon (icons []image.Image) (err error) {
return
}
func (backend *Backend) CellMetrics () (width, height int) {
width = backend.metrics.cellWidth
height = backend.metrics.cellHeight
return
}
// calculateWindowSize calculates window bounds based on the internal buffer
// size.
func (backend *Backend) calculateWindowSize () (x, y int) {

View File

@@ -8,8 +8,8 @@ type Color uint8
const (
ColorBackground Color = 0x0
ColorForeground Color = 0x1
ColorDim Color = 0x2
ColorRed Color = 0x3
ColorRed Color = 0x2
ColorOrange Color = 0x3
ColorYellow Color = 0x4
ColorGreen Color = 0x5
ColorBlue Color = 0x6

View File

@@ -55,10 +55,10 @@ func (config *Config) load () {
color.RGBA { R: 0, G: 0, B: 0, A: 0 },
// foreground
color.RGBA { R: 0xFF, G: 0xFF, B: 0xFF, A: 0xFF },
// dim
color.RGBA { R: 0x80, G: 0x80, B: 0x80, A: 0xFF },
// red
color.RGBA { R: 0xFF, G: 0x00, B: 0x00, A: 0xFF },
// orange
color.RGBA { R: 0xFF, G: 0x80, B: 0x00, A: 0xFF },
// yellow
color.RGBA { R: 0xFF, G: 0xFF, B: 0x00, A: 0xFF },
// green
@@ -154,10 +154,10 @@ func (config *Config) loadFile (path string) {
config.colors[ColorBackground] = valueColor
case "colorForeground":
config.colors[ColorForeground] = valueColor
case "colorDim":
config.colors[ColorDim] = valueColor
case "colorRed":
config.colors[ColorRed] = valueColor
case "colorOrange":
config.colors[ColorOrange] = valueColor
case "colorYellow":
config.colors[ColorYellow] = valueColor
case "colorGreen":

View File

@@ -2,7 +2,7 @@ package stone
type CallbackManager struct {
onQuit func ()
onPress func (button Button, modifiers Modifiers)
onPress func (button Button)
onRelease func (button Button)
onResize func ()
onMouseMove func (x, y int)
@@ -15,9 +15,9 @@ func (manager *CallbackManager) RunQuit () {
manager.onQuit()
}
func (manager *CallbackManager) RunPress (button Button, modifiers Modifiers) {
func (manager *CallbackManager) RunPress (button Button) {
if manager.onPress == nil { return }
manager.onPress(button, modifiers)
manager.onPress(button)
}
func (manager *CallbackManager) RunRelease (button Button) {

View File

@@ -58,7 +58,7 @@ func redraw () {
application.SetRune(0, height - 1, '+')
for x := 0; x < width; x ++ {
application.SetColor(x, height / 2, stone.Color(x % 5 + 3))
application.SetColor(x, height / 2, stone.Color(x % 6 + 2))
}
for x := 1; x < width - 1; x ++ {

View File

@@ -34,7 +34,7 @@ func main () {
if err != nil { panic(err) }
}
func onPress (button stone.Button, modifiers stone.Modifiers) {
func onPress (button stone.Button) {
if button == stone.MouseButtonLeft {
mousePressed = true
application.SetRune(0, 0, '+')

View File

@@ -1,41 +0,0 @@
package main
import "os"
import "image"
import _ "image/png"
import "git.tebibyte.media/sashakoshka/stone"
import _ "git.tebibyte.media/sashakoshka/stone/backends/x"
var application = &stone.Application { }
func main () {
application.SetTitle("press any key")
application.SetSize(8, 1)
iconFile16, err := os.Open("assets/scaffold16.png")
if err != nil { panic(err) }
icon16, _, err := image.Decode(iconFile16)
if err != nil { panic(err) }
iconFile16.Close()
iconFile32, err := os.Open("assets/scaffold32.png")
if err != nil { panic(err) }
icon32, _, err := image.Decode(iconFile32)
if err != nil { panic(err) }
iconFile16.Close()
application.SetIcon([]image.Image { icon16, icon32 })
application.OnPress(onPress)
application.OnRelease(onRelease)
err = application.Run()
if err != nil { panic(err) }
}
func onPress (button stone.Button, modifiers stone.Modifiers) {
println("press", button)
}
func onRelease (button stone.Button) {
println("release", button)
}

View File

@@ -1,20 +1,17 @@
package main
import "os"
import "fmt"
import "image"
import _ "image/png"
import "git.tebibyte.media/sashakoshka/stone"
import _ "git.tebibyte.media/sashakoshka/stone/backends/x"
var application = &stone.Application { }
var caretX = 0
var caretY = 2
var page = 1
var caret = 0
func main () {
application.SetTitle("hellorld")
application.SetSize(32, 28)
application.SetSize(32, 16)
iconFile16, err := os.Open("assets/scaffold16.png")
if err != nil { panic(err) }
@@ -28,57 +25,32 @@ func main () {
iconFile16.Close()
application.SetIcon([]image.Image { icon16, icon32 })
application.OnStart(redraw)
application.OnPress(onPress)
application.OnResize(redraw)
err = application.Run()
channel, err := application.Run()
if err != nil { panic(err) }
application.Draw()
}
func redraw () {
application.Clear()
_, height := application.Size()
application.SetDot(0, 0)
fmt.Fprint(application, "type some text below:")
caretX = 0
caretY = 2
application.SetDot(0, height - 1)
fmt.Fprintf(application, "page %d", page)
drawCaret()
}
for {
event := <- channel
switch event.(type) {
case stone.EventQuit:
os.Exit(0)
func drawCaret () {
application.SetRune(caretX, caretY, '+')
application.SetColor(caretX, caretY, stone.ColorDim)
}
case stone.EventPress:
button := event.(stone.EventPress).Button
if button.Printable() {
application.SetRune(caret, 0, rune(button))
caret ++
width, _ := application.Size()
if caret >= width {
caret = 0
}
application.Draw()
}
func onPress (button stone.Button, modifiers stone.Modifiers) {
width, height := application.Size()
if button == stone.KeyEnter {
application.SetRune(caretX, caretY, 0)
caretX = 0
caretY ++
} else if button.Printable() {
application.SetRune(caretX, caretY, rune(button))
application.SetColor(caretX, caretY, stone.ColorForeground)
caretX ++
if caretX >= width {
caretX = 0
caretY ++
case stone.EventResize:
application.Draw()
}
}
if caretY >= height - 2 {
page ++
redraw()
}
drawCaret()
application.Draw()
}

142
image.go Normal file
View File

@@ -0,0 +1,142 @@
package stone
import "sync"
import "image"
import "image/color"
type ImageManager struct {
lock sync.RWMutex
images []*ColorImage
}
func (manager *ImageManager) For (callback func (im *ColorImage)) {
manager.lock.RLock()
defer manager.lock.RUnlock()
for _, im := range manager.images {
callback(im)
}
}
func (manager *ImageManager) Add (im *ColorImage) {
manager.lock.Lock()
defer manager.lock.Unlock()
manager.images = append(manager.images, im)
}
func (manager *ImageManager) Size () (size int) {
manager.lock.RLock()
defer manager.lock.RUnlock()
size = len(manager.images)
return
}
func (manager *ImageManager) At (index int) (im *ColorImage) {
manager.lock.RLock()
defer manager.lock.RUnlock()
if index < 0 || index > len(manager.images) { return }
im = manager.images[index]
return
}
func (manager *ImageManager) Remove (im *ColorImage) (removed bool) {
manager.lock.Lock()
defer manager.lock.Unlock()
index := 0
for manager.images[index] != im && index < len(manager.images) {
index ++
}
if index >= len(manager.images) { return }
manager.images = append (
manager.images[:index],
manager.images[index + 1:]...)
removed = true
return
}
type ColorImage struct {
x, y int
width, height int
bufferWidth, bufferHeight int
cellWidth, cellHeight int
buffer []color.RGBA
clean bool
}
func (im *ColorImage) Model () (model color.Model) {
model = color.RGBAModel
return
}
func (im *ColorImage) Bounds () (bounds image.Rectangle) {
bounds.Max.X = im.width
bounds.Max.Y = im.height
return
}
func (im *ColorImage) Size () (width, height int) {
width = im.width
height = im.height
return
}
func (im *ColorImage) SetSize (width, height int) {
im.width = width
im.height = height
im.bufferWidth = im.cellWidth * im.width
im.bufferHeight = im.cellHeight * im.height
im.buffer = make([]color.RGBA, im.bufferWidth * im.bufferHeight)
im.clean = false
}
func (im *ColorImage) At (x, y int) (pixel color.Color) {
if im.outOfBounds(x, y) { return }
pixel = im.buffer[x + y * im.width]
return
}
func (im *ColorImage) AtRGBA (x, y int) (pixel color.RGBA) {
if im.outOfBounds(x, y) { return }
pixel = im.buffer[x + y * im.width]
return
}
func (im *ColorImage) Set (x, y int, pixel color.Color) {
if im.outOfBounds(x, y) { return }
r, g, b, a := pixel.RGBA()
im.buffer[x + y * im.width] = color.RGBA {
R: uint8(r >> 8),
G: uint8(g >> 8),
B: uint8(b >> 8),
A: uint8(a >> 8),
}
im.clean = false
return
}
func (im *ColorImage) SetRGBA (x, y int, pixel color.RGBA) {
if im.outOfBounds(x, y) { return }
im.buffer[x + y * im.width] = pixel
return
}
func (im *ColorImage) MarkClean () {
im.clean = true
}
func (im *ColorImage) outOfBounds (x, y int) (outOfBounds bool) {
outOfBounds =
x >= im.width ||
y >= im.height ||
x < 0 ||
y < 0
return
}

View File

@@ -34,13 +34,9 @@ const (
KeyLeftControl Button = 22
KeyRightControl Button = 23
KeyLeftAlt Button = 24
KeyRightAlt Button = 25
KeyLeftMeta Button = 26
KeyRightMeta Button = 27
KeyLeftSuper Button = 28
KeyRightSuper Button = 29
KeyLeftHyper Button = 30
KeyRightHyper Button = 31
KeyRightAlt Button = 25
KeyLeftSuper Button = 26
KeyRightSuper Button = 27
KeyDelete Button = 127
@@ -71,8 +67,6 @@ const (
KeyF10 Button = 153
KeyF11 Button = 154
KeyF12 Button = 155
KeyDead Button = 156
)
// Printable returns whether or not the character could show up on screen. If
@@ -82,16 +76,3 @@ func (button Button) Printable () (printable bool) {
printable = unicode.IsPrint(rune(button))
return
}
// Modifiers lists what modifier keys are being pressed. This is used in
// conjunction with a button code in a button press event. These should be used
// instead of attempting to track the state of the modifier keys, because there
// is no guarantee that one press event will be coupled with one release event.
type Modifiers struct {
Shift bool
Control bool
Alt bool
Meta bool
Super bool
Hyper bool
}