wm/vend/xgbutil/xgraphics/xsurface.go

294 lines
9.6 KiB
Go
Raw Permalink Normal View History

2023-06-11 14:21:08 +00:00
package xgraphics
import (
"fmt"
"image"
"github.com/jezek/xgb/xproto"
"github.com/jezek/xgbutil"
"github.com/jezek/xgbutil/ewmh"
"github.com/jezek/xgbutil/icccm"
"github.com/jezek/xgbutil/keybind"
"github.com/jezek/xgbutil/mousebind"
"github.com/jezek/xgbutil/xevent"
"github.com/jezek/xgbutil/xwindow"
)
/*
xgraphics/xsurface.go contains methods for the Image type that perform X
related requests. Namely, methods that send image data start with 'X'.
*/
// XSurfaceSet will set the given window's background to this image's pixmap.
// Note that an image can have multiple surfaces, which is why the window
// id still needs to be passed to XPaint. A call to XSurfaceSet simply tells
// X that the window specified should use the pixmap in Image as its
// background image.
// Note that XSurfaceSet cannot be called on a sub-image. (An error will be
// returned if you do.)
// XSurfaceSet will also allocate an X pixmap if one hasn't been created for
// this image yet.
// (Generating a pixmap id can cause an error, so this call could return
// an error.)
func (im *Image) XSurfaceSet(wid xproto.Window) error {
if im.Subimg {
return fmt.Errorf("XSurfaceSet cannot be called on sub-images." +
"Please set the surface using the original parent image.")
}
if im.Pixmap == 0 {
if err := im.CreatePixmap(); err != nil {
return err
}
}
// Tell the surface (window) to use this pixmap.
xproto.ChangeWindowAttributes(im.X.Conn(), wid,
xproto.CwBackPixmap, []uint32{uint32(im.Pixmap)})
return nil
}
// CreatePixmap allocates an X resource identifier for a pixmap. (It does not
// do any drawing.) You only need to call this if you're using XDraw/XExpPaint.
// If you're using XSurfaceSet/XDraw/XPaint, then CreatePixmap is called for
// you automatically.
func (im *Image) CreatePixmap() error {
// Generate the pixmap id.
pid, err := xproto.NewPixmapId(im.X.Conn())
if err != nil {
return err
}
// Now actually create the pixmap.
err = xproto.CreatePixmapChecked(im.X.Conn(), im.X.Screen().RootDepth,
pid, xproto.Drawable(im.X.RootWin()),
uint16(im.Bounds().Dx()), uint16(im.Bounds().Dy())).Check()
if err != nil {
return err
}
// Now give it to the image.
im.Pixmap = pid
return nil
}
// XPaint will write the contents of the pixmap to a window.
// Note that painting will do nothing if XDraw hasn't been called.
// XPaint is what switches the buffer (drawn to using XDraw) into the window
// to be visible. That is, multiple calls to XDraw can be made, and the screen
// will only be updated once with a call to XPaint.
func (im *Image) XPaint(wid xproto.Window) {
// We clear the whole window here because sometimes we rely on the tiling
// of a background pixmap. If anyone knows if this is a significant
// performance problem, please let me know. (It seems like the whole area
// of the window is cleared when it is resized anyway.)
xproto.ClearArea(im.X.Conn(), false, wid, 0, 0, 0, 0)
}
// XExpPaint achieves a similar result as XPaint and XSurfaceSet, but
// uses CopyArea instead of setting a background pixmap and using ClearArea.
// CreatePixmap must be called before using XExpPaint.
// XExpPaint can be called on sub-images.
// x and y correspond to the destination x and y to copy the image to.
//
// This should not be used on the same image with XSurfaceSet and XPaint.
func (im *Image) XExpPaint(wid xproto.Window, x, y int) {
if im.Pixmap == 0 {
return
}
xproto.CopyArea(im.X.Conn(),
xproto.Drawable(im.Pixmap), xproto.Drawable(wid), im.X.GC(),
int16(im.Rect.Min.X), int16(im.Rect.Min.Y),
int16(x), int16(y),
uint16(im.Rect.Dx()), uint16(im.Rect.Dy()))
}
// XPaintRects is a convenience function for issuing XDraw requests on
// each sub-image generated by the rects in the slice provided, and then
// painting the updated pixmap all at once to the window provided.
// This is efficient because no pixels are copied when taking a SubImage,
// and each XDraw call on a sub-image updates the pixels represented by that
// sub-image and only that sub-image.
func (im *Image) XPaintRects(wid xproto.Window, rects ...image.Rectangle) {
for _, rect := range rects {
if si := im.SubImage(rect).(*Image); si != nil {
si.XDraw()
}
}
im.XPaint(wid)
}
// XDraw will write the contents of Image to a pixmap.
// Note that this is more like a buffer. Drawing does not put the contents
// on the screen.
// After drawing, it is necessary to call XPaint to put the contents somewhere.
// Draw may return an X error if something has gone horribly wrong.
//
// XSurfaceSet should be called before XDraw. (If not, X will yell at you.)
// More specifically, CreatePixmap needs to be called before XDraw, but it is
// done automatically in XSurfaceSet.
//
// If you're using sub-images to update a particular region of the image, XDraw
// is where you'll see the performance benefit (not XPaint).
func (im *Image) XDraw() {
im.xdraw(false)
}
// XDrawChecked is the same as XDraw, but issues PutImageChecked requests
// instead. This should *only* be used for debugging purposes, as each
// PutImageChecked request blocks for a round trip to the X server.
func (im *Image) XDrawChecked() error {
return im.xdraw(true)
}
func (im *Image) xdraw(checked bool) error {
width, height := im.Rect.Dx(), im.Rect.Dy()
// Put the raw image data into its own slice.
// If this isn't a sub-image, then skip because it isn't necessary.
var data []uint8
if !im.Subimg {
data = im.Pix
} else {
data = make([]uint8, width*height*4)
for y := im.Rect.Min.Y; y < im.Rect.Max.Y; y++ {
i := (y - im.Rect.Min.Y) * width * 4
copy(data[i:i+4*width], im.Pix[im.PixOffset(im.Rect.Min.X, y):])
}
}
// X's max request size (by default) is (2^16) * 4 = 262144 bytes, which
// corresponds precisely to a 256x256 sized image with 32 bits per pixel.
// Thus, we check the size of the image data and calculate the number of
// rows of the image we'll send in each request. If a single row of an
// image exceeds the max request length, we're in trouble. N.B. The
// constant 28 comes from the fixed size part of a PutImage request.
rowsPer := (xgbutil.MaxReqSize - 28) / (width * 4)
bytesPer := rowsPer * width * 4
// The start x position of what we're sending. Doesn't change.
xpos := im.Rect.Min.X
// The start y position of what we're sending. Increases based on the
// number of rows of the image we send in each request.
ypos := im.Rect.Min.Y
// The height of each PutImage request. It's always rowsPer, unless its
// the last request and we're not sending the maximum number of bytes.
heightPer := 0
// The start and end positions of the raw bytes being sent.
start, end := 0, 0
// The sliced data we're sending, for convenience.
var toSend []byte
for end < len(data) {
end = start + bytesPer
if end > len(data) { // make sure end doesn't extend beyond data
end = len(data)
}
toSend = data[start:end]
heightPer = len(toSend) / 4 / width
if checked {
err := xproto.PutImageChecked(
im.X.Conn(), xproto.ImageFormatZPixmap,
xproto.Drawable(im.Pixmap), im.X.GC(),
uint16(width), uint16(heightPer), int16(xpos), int16(ypos),
0, 24, toSend).Check()
if err != nil {
return err
}
} else {
xproto.PutImage(im.X.Conn(), xproto.ImageFormatZPixmap,
xproto.Drawable(im.Pixmap), im.X.GC(),
uint16(width), uint16(heightPer), int16(xpos), int16(ypos),
0, 24, toSend)
}
start = end
ypos += rowsPer
}
return nil
}
// XShow creates a new window and paints the image to the window.
// This is useful for debugging, or if you're creating an image viewer.
// XShow also returns the xwindow.Window value, in case you want to do
// further processing. (Like attach event handlers.)
func (im *Image) XShow() *xwindow.Window {
return im.XShowExtra("", false)
}
// XShowName is just like XShow, except it sets the name of the window to the
// name provided, and will quit the current event loop if 'quit' is true when
// the window is closed.
// If name is empty and quit is false, then the behavior is precisely the same
// as XShow.
func (im *Image) XShowExtra(name string, quit bool) *xwindow.Window {
if len(name) == 0 {
name = "xgbutil Image Window"
}
w, h := im.Rect.Dx(), im.Rect.Dy()
win, err := xwindow.Generate(im.X)
if err != nil {
xgbutil.Logger.Printf("Could not generate new window id: %s", err)
return nil
}
// Create a very simple window with dimensions equal to the image.
win.Create(im.X.RootWin(), 0, 0, w, h, 0)
// Make this window close gracefully.
win.WMGracefulClose(func(w *xwindow.Window) {
xevent.Detach(w.X, w.Id)
keybind.Detach(w.X, w.Id)
mousebind.Detach(w.X, w.Id)
w.Destroy()
if quit {
xevent.Quit(w.X)
}
})
// Set WM_STATE so it is interpreted as a top-level window.
err = icccm.WmStateSet(im.X, win.Id, &icccm.WmState{
State: icccm.StateNormal,
})
if err != nil { // not a fatal error
xgbutil.Logger.Printf("Could not set WM_STATE: %s", err)
}
// Set WM_NORMAL_HINTS so the window can't be resized.
err = icccm.WmNormalHintsSet(im.X, win.Id, &icccm.NormalHints{
Flags: icccm.SizeHintPMinSize | icccm.SizeHintPMaxSize,
MinWidth: uint(w),
MinHeight: uint(h),
MaxWidth: uint(w),
MaxHeight: uint(h),
})
if err != nil { // not a fatal error
xgbutil.Logger.Printf("Could not set WM_NORMAL_HINTS: %s", err)
}
// Set _NET_WM_NAME so it looks nice.
err = ewmh.WmNameSet(im.X, win.Id, name)
if err != nil { // not a fatal error
xgbutil.Logger.Printf("Could not set _NET_WM_NAME: %s", err)
}
// Paint our image before mapping.
im.XSurfaceSet(win.Id)
im.XDraw()
im.XPaint(win.Id)
// Now we can map, since we've set all our properties.
// (The initial map is when the window manager starts managing.)
win.Map()
return win
}