stone/backends/x/x.go

468 lines
12 KiB
Go
Raw Normal View History

2022-11-09 13:52:49 -07:00
package x
2022-11-11 13:01:36 -07:00
import "os"
2022-11-12 17:02:24 -07:00
// import "fmt"
import "sync"
2022-11-09 13:52:49 -07:00
import "image"
2022-11-10 19:32:02 -07:00
import "image/draw"
2022-11-10 10:38:02 -07:00
import "golang.org/x/image/font"
2022-11-10 19:32:02 -07:00
import "golang.org/x/image/math/fixed"
2022-11-11 13:01:36 -07:00
import "golang.org/x/image/font/opentype"
2022-11-10 10:38:02 -07:00
import "golang.org/x/image/font/basicfont"
2022-11-09 13:52:49 -07:00
2022-11-12 17:02:24 -07:00
// import "github.com/jezek/xgb"
2022-11-09 13:52:49 -07:00
import "github.com/jezek/xgbutil"
2022-11-09 20:33:18 -07:00
import "github.com/jezek/xgb/xproto"
2022-11-10 00:02:08 -07:00
import "github.com/jezek/xgbutil/ewmh"
2022-11-09 16:53:14 -07:00
import "github.com/jezek/xgbutil/xevent"
2022-11-09 13:52:49 -07:00
import "github.com/jezek/xgbutil/xwindow"
import "github.com/jezek/xgbutil/xgraphics"
import "git.tebibyte.media/sashakoshka/stone"
2022-11-11 13:01:36 -07:00
import "github.com/flopp/go-findfont"
2022-11-09 13:52:49 -07:00
type Backend struct {
2022-11-09 16:53:14 -07:00
application *stone.Application
config *stone.Config
connection *xgbutil.XUtil
window *xwindow.Window
canvas *xgraphics.Image
channel chan(stone.Event)
2022-11-09 13:52:49 -07:00
drawLock sync.Mutex
2022-11-10 10:38:02 -07:00
font struct {
face font.Face
}
2022-11-10 18:43:27 -07:00
colors [4]xgraphics.BGRA
2022-11-09 16:53:14 -07:00
metrics struct {
2022-11-09 20:33:18 -07:00
windowWidth int
windowHeight int
cellWidth int
cellHeight int
padding int
paddingX int
paddingY int
descent int
2022-11-09 16:53:14 -07:00
}
memory struct {
windowWidth int
windowHeight int
}
2022-11-09 13:52:49 -07:00
}
2022-11-09 16:53:14 -07:00
func (backend *Backend) Run (channel chan(stone.Event)) {
backend.channel = channel
2022-11-12 17:02:24 -07:00
xevent.Main(backend.connection)
backend.shutDown()
2022-11-09 13:52:49 -07:00
}
func (backend *Backend) Draw () {
backend.drawLock.Lock()
defer backend.drawLock.Unlock()
boundsChanged :=
backend.memory.windowWidth != backend.metrics.windowWidth ||
backend.memory.windowHeight != backend.metrics.windowHeight
backend.memory.windowWidth = backend.metrics.windowWidth
backend.memory.windowHeight = backend.metrics.windowHeight
if boundsChanged {
backend.reallocateCanvas()
2022-11-11 21:46:46 -07:00
backend.drawCells(true)
backend.canvas.XDraw()
backend.canvas.XPaint(backend.window.Id)
} else {
backend.updateWindowAreas(backend.drawCells(false)...)
}
}
2022-11-10 00:02:08 -07:00
func (backend *Backend) SetTitle (title string) (err error) {
err = ewmh.WmNameSet(backend.connection, backend.window.Id, title)
return
2022-11-09 13:52:49 -07:00
}
2022-11-10 00:02:08 -07:00
func (backend *Backend) SetIcon (icons []image.Image) (err error) {
wmIcons := []ewmh.WmIcon { }
2022-11-09 13:52:49 -07:00
2022-11-10 00:02:08 -07:00
for _, icon := range icons {
width := icon.Bounds().Max.X
height := icon.Bounds().Max.Y
wmIcon := ewmh.WmIcon {
Width: uint(width),
Height: uint(height),
Data: make ([]uint, width * height),
}
// manually convert image data beacuse of course we have to do
// this
index := 0
for y := 0; y < height; y ++ {
for x := 0; x < width; x ++ {
r, g, b, a := icon.At(x, y).RGBA()
r >>= 8
g >>= 8
b >>= 8
a >>= 8
wmIcon.Data[index] =
(uint(a) << 24) |
(uint(r) << 16) |
(uint(g) << 8) |
(uint(b) << 0)
index ++
}}
wmIcons = append(wmIcons, wmIcon)
}
err = ewmh.WmIconSet(backend.connection, backend.window.Id, wmIcons)
return
2022-11-09 13:52:49 -07:00
}
2022-11-09 16:53:14 -07:00
2022-11-12 17:02:24 -07:00
func (backend *Backend) handleConfigureNotify (
connection *xgbutil.XUtil,
event xevent.ConfigureNotifyEvent,
) {
configureEvent := *event.ConfigureNotifyEvent
newWidth := int(configureEvent.Width)
newHeight := int(configureEvent.Height)
sizeChanged :=
backend.metrics.windowWidth != newWidth ||
backend.metrics.windowHeight != newHeight
backend.metrics.windowWidth = newWidth
backend.metrics.windowHeight = newHeight
if sizeChanged {
configureEvent =
backend.compressConfigureNotify(configureEvent)
backend.application.SetSize(backend.calculateBufferSize())
backend.channel <- stone.EventResize { }
2022-11-09 20:33:18 -07:00
}
}
2022-11-12 17:02:24 -07:00
func (backend *Backend) handleButtonPress (
connection *xgbutil.XUtil,
event xevent.ButtonPressEvent,
) {
buttonEvent := *event.ButtonPressEvent
backend.channel <- stone.EventPress(buttonEvent.Detail)
}
func (backend *Backend) handleButtonRelease (
connection *xgbutil.XUtil,
event xevent.ButtonReleaseEvent,
) {
buttonEvent := *event.ButtonReleaseEvent
backend.channel <- stone.EventRelease(buttonEvent.Detail)
}
func (backend *Backend) compressConfigureNotify (
firstEvent xproto.ConfigureNotifyEvent,
) (
lastEvent xproto.ConfigureNotifyEvent,
) {
backend.connection.Sync()
xevent.Read(backend.connection, false)
lastEvent = firstEvent
for index, untypedEvent := range xevent.Peek(backend.connection) {
if untypedEvent.Err != nil { continue }
typedEvent, ok := untypedEvent.Event.(xproto.ConfigureNotifyEvent)
if !ok { continue }
lastEvent = typedEvent
defer func (index int) {
xevent.DequeueAt(backend.connection, index)
} (index)
}
return
}
2022-11-09 16:53:14 -07:00
func (backend *Backend) shutDown () {
backend.channel <- stone.EventQuit { }
}
// calculateWindowSize calculates window bounds based on the internal buffer
// size.
func (backend *Backend) calculateWindowSize () (x, y int) {
width, height := backend.application.Size()
x =
width * backend.metrics.cellWidth +
backend.metrics.padding * 2
y =
height * backend.metrics.cellHeight +
backend.metrics.padding * 2
return
}
func (backend *Backend) calculateBufferSize () (width, height int) {
width =
(backend.metrics.windowWidth - backend.metrics.padding * 2) /
backend.metrics.cellWidth
height =
(backend.metrics.windowHeight - backend.metrics.padding * 2) /
backend.metrics.cellHeight
return
}
2022-11-10 18:43:27 -07:00
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.ColorApplication]
})
2022-11-10 19:32:02 -07:00
2022-11-09 17:07:40 -07:00
backend.canvas.XSurfaceSet(backend.window.Id)
}
2022-11-09 16:53:14 -07:00
2022-11-11 16:14:43 -07:00
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()
2022-11-12 17:02:24 -07:00
if forceRedraw && content < 32 { continue }
2022-11-11 21:46:46 -07:00
areas = append(areas, backend.boundsOfCell(x, y))
2022-11-11 16:14:43 -07:00
backend.drawRune(x, y, content)
}}
return
}
func (backend *Backend) updateWindowAreas (areas ...image.Rectangle) {
backend.canvas.XPaintRects(backend.window.Id, areas...)
}
2022-11-10 10:38:02 -07:00
func (backend *Backend) drawRune (x, y int, character rune) {
// 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.
2022-11-12 17:02:24 -07:00
fillRectangle (
&image.Uniform {
C: backend.config.Color(stone.ColorApplication),
},
backend.canvas,
backend.boundsOfCell(x, y))
if character < 32 { return }
origin := backend.originOfCell(x, y + 1)
destinationRectangle, mask, maskPoint, _, _ := backend.font.face.Glyph (
fixed.Point26_6 {
X: fixed.I(origin.X),
Y: fixed.I(origin.Y - backend.metrics.descent),
},
2022-11-10 19:32:02 -07:00
character)
// strokeRectangle (
// &image.Uniform {
// C: backend.config.Color(stone.ColorForeground),
// },
// backend.canvas,
// backend.boundsOfCell(x, y))
2022-11-10 19:32:02 -07:00
draw.DrawMask (
backend.canvas,
destinationRectangle,
&image.Uniform {
C: backend.config.Color(stone.ColorForeground),
2022-11-10 19:32:02 -07:00
},
image.Point { },
mask,
maskPoint,
draw.Over)
}
func (backend *Backend) cellSubImage (x, y int) (cell *xgraphics.Image) {
cell = backend.canvas.SubImage(backend.boundsOfCell(x, y)).(*xgraphics.Image)
return
}
func (backend *Backend) originOfCell (x, y int) (origin image.Point) {
origin = image.Point {
X: x * backend.metrics.cellWidth + backend.metrics.paddingX,
Y: y * backend.metrics.cellHeight + backend.metrics.paddingY,
}
return
}
func (backend *Backend) boundsOfCell (x, y int) (bounds image.Rectangle) {
bounds = image.Rectangle {
Min: backend.originOfCell(x, y),
Max: backend.originOfCell(x + 1, y + 1),
}
return
2022-11-10 10:38:02 -07:00
}
2022-11-09 16:53:14 -07:00
// factory instantiates an X backend.
func factory (application *stone.Application) (output stone.Backend, err error) {
backend := &Backend {
application: application,
config: application.Config(),
}
2022-11-10 10:38:02 -07:00
// load font
backend.font.face = findAndLoadFont (
backend.config.FontName(),
float64(backend.config.FontSize()))
if backend.font.face == nil {
backend.font.face = basicfont.Face7x13
}
2022-11-10 10:38:02 -07:00
2022-11-10 18:43:27 -07:00
// pre-calculate colors
for index := 0; index < len(backend.colors); index ++ {
color := backend.config.Color(stone.Color(index))
r, g, b, a := color.RGBA()
r >>= 8
g >>= 8
b >>= 8
a >>= 8
backend.colors[index] = xgraphics.BGRA {
R: uint8(r),
G: uint8(g),
B: uint8(b),
A: uint8(a),
}
}
2022-11-09 16:53:14 -07:00
// calculate metrics
2022-11-10 10:38:02 -07:00
metrics := backend.font.face.Metrics()
glyphAdvance, _ := backend.font.face.GlyphAdvance('M')
backend.metrics.cellWidth = glyphAdvance.Round()
backend.metrics.cellHeight = metrics.Height.Round()
backend.metrics.descent = metrics.Descent.Round()
2022-11-09 16:53:14 -07:00
backend.metrics.padding =
backend.config.Padding() *
backend.metrics.cellHeight
backend.metrics.paddingX = backend.metrics.padding
backend.metrics.paddingY = backend.metrics.padding
2022-11-09 20:33:18 -07:00
backend.metrics.windowWidth,
backend.metrics.windowHeight = backend.calculateWindowSize()
2022-11-09 16:53:14 -07:00
// connect to X
backend.connection, err = xgbutil.NewConn()
if err != nil { return }
backend.window, err = xwindow.Generate(backend.connection)
if err != nil { return }
// create the window
backend.window.Create (
backend.connection.RootWin(),
2022-11-09 20:33:18 -07:00
0, 0,
backend.metrics.windowWidth, backend.metrics.windowHeight,
2022-11-09 16:53:14 -07:00
0)
backend.window.Map()
2022-11-12 17:02:24 -07:00
// TODO: also listen to mouse movement (compressed) and mouse and
// keyboard buttons (uncompressed)
err = backend.window.Listen (
xproto.EventMaskStructureNotify,
// xproto.EventMaskPointerMotion,
// xproto.EventMaskKeyPress,
// xproto.EventMaskKeyRelease,
xproto.EventMaskButtonPress,
xproto.EventMaskButtonRelease,
)
2022-11-10 00:02:08 -07:00
backend.SetTitle(application.Title())
backend.SetIcon(application.Icon())
2022-11-09 16:53:14 -07:00
2022-11-09 17:07:40 -07:00
// create a canvas
2022-11-10 18:43:27 -07:00
backend.reallocateCanvas()
2022-11-09 17:07:40 -07:00
2022-11-09 16:53:14 -07:00
// attatch graceful close handler
backend.window.WMGracefulClose (func (window *xwindow.Window) {
backend.window.Destroy()
backend.shutDown()
})
2022-11-12 17:02:24 -07:00
// attatch event handlers
xevent.ConfigureNotifyFun(backend.handleConfigureNotify).
Connect(backend.connection, backend.window.Id)
xevent.ButtonPressFun(backend.handleButtonPress).
Connect(backend.connection, backend.window.Id)
xevent.ButtonReleaseFun(backend.handleButtonRelease).
Connect(backend.connection, backend.window.Id)
2022-11-09 16:53:14 -07:00
output = backend
return
}
func findAndLoadFont (name string, size float64) (face font.Face) {
if name == "" { return }
2022-11-11 13:01:36 -07:00
fontPath, err := findfont.Find(name)
if err != nil { return }
fontFile, err := os.Open(fontPath)
if err != nil { return }
fontObject, err := opentype.ParseReaderAt(fontFile)
if err != nil { return }
face, err = opentype.NewFace (fontObject, &opentype.FaceOptions {
Size: size,
DPI: 96,
Hinting: font.HintingFull,
})
if err != nil { face = nil }
return
}
func fillRectangle (
source image.Image,
destination draw.Image,
bounds image.Rectangle,
) {
for y := bounds.Min.Y; y < bounds.Max.Y; y ++ {
for x := bounds.Min.X; x < bounds.Max.X; x ++ {
destination.Set(x, y, source.At(x, y))
}}
}
func strokeRectangle (
source image.Image,
destination draw.Image,
bounds image.Rectangle,
) {
x := 0
y := bounds.Min.Y
for x = bounds.Min.X; x < bounds.Max.X; x ++ {
destination.Set(x, y, source.At(x, y))
}
y = bounds.Max.Y - 1
for x = bounds.Min.X; x < bounds.Max.X; x ++ {
destination.Set(x, y, source.At(x, y))
}
x = bounds.Min.X
for y = bounds.Min.Y; y < bounds.Max.Y; y ++ {
destination.Set(x, y, source.At(x, y))
}
x = bounds.Max.X - 1
for y = bounds.Min.Y; y < bounds.Max.Y; y ++ {
destination.Set(x, y, source.At(x, y))
}
}
2022-11-09 16:53:14 -07:00
// init registers this backend when the program starts.
func init () {
stone.RegisterBackend(factory)
}