//go:build linux
package wayland
import (
"context"
"encoding/binary"
"errors"
"fmt"
"io"
"log"
"math"
"net"
"os"
"os/exec"
"strings"
"sync"
"sync/atomic"
"time"
"chirami/internal/platform"
"golang.org/x/sys/unix"
)
var debug = os.Getenv("CHIRAMI_DEBUG") != ""
func debugf(format string, args ...any) {
if debug {
log.Printf(format, args...)
}
}
type waylandMsg struct {
objID uint32
opcode uint32
fd int
data []byte
err error
}
type locateReq struct {
resp chan locateResp
}
type locateResp struct {
anchor platform.Anchor
err error
}
type showReq struct {
popup platform.Popup
resp chan error
}
type hideReq struct {
resp chan struct{}
}
type outputInfo struct {
id uint32
globalName uint32
name string
physX, physY int32
physW, physH int32
outputScale int32
xdgOutputID uint32
hasLogical bool
logicalX int32
logicalY int32
logicalW int32
logicalH int32
preferredScale float64
locateSurfID uint32
locateLayerSurfID uint32
locateViewportID uint32
locateFracScaleID uint32
locateW, locateH int32
locateMapped bool
locateCustomBufID uint32
}
func (o *outputInfo) LogicalWidth() float64 {
if o.hasLogical && o.logicalW > 0 {
return float64(o.logicalW)
}
s := o.Scale()
if s <= 0 {
s = 1.0
}
if o.physW > 0 {
return float64(o.physW) / s
}
return 1920.0
}
func (o *outputInfo) LogicalHeight() float64 {
if o.hasLogical && o.logicalH > 0 {
return float64(o.logicalH)
}
s := o.Scale()
if s <= 0 {
s = 1.0
}
if o.physH > 0 {
return float64(o.physH) / s
}
return 1080.0
}
func (o *outputInfo) Scale() float64 {
if o.preferredScale > 0 {
return o.preferredScale
}
if o.outputScale > 0 {
return float64(o.outputScale)
}
return 1.0
}
type poolBufferSlot struct {
bufID uint32
offset int
w, h int
stride int
busy bool
released bool
}
type shmPool struct {
shmID uint32
poolID uint32
memfd int
capacity int
data []byte
slots [2]poolBufferSlot
}
func newShmPool(shmID uint32) *shmPool {
return &shmPool{
shmID: shmID,
memfd: -1,
}
}
func (p *shmPool) ensureCapacity(conn *net.UnixConn, capNeeded int, allocID func() uint32, sendMsg func(uint32, uint32, []byte, ...any) error) error {
if capNeeded <= p.capacity {
return nil
}
aligned := (capNeeded + 0xffff) &^ 0xffff
if p.memfd < 0 {
mfd, err := unix.MemfdCreate("chirami-card", unix.MFD_CLOEXEC)
if err != nil {
return fmt.Errorf("memfd_create: %w", err)
}
if err := unix.Ftruncate(mfd, int64(aligned)); err != nil {
_ = unix.Close(mfd)
return fmt.Errorf("ftruncate: %w", err)
}
data, err := unix.Mmap(mfd, 0, aligned, unix.PROT_READ|unix.PROT_WRITE, unix.MAP_SHARED)
if err != nil {
_ = unix.Close(mfd)
return fmt.Errorf("mmap: %w", err)
}
p.poolID = allocID()
oob := unix.UnixRights(mfd)
var cpReq [16]byte
binary.LittleEndian.PutUint32(cpReq[0:4], p.shmID)
binary.LittleEndian.PutUint32(cpReq[4:8], (16<<16)|0) // create_pool opcode 0
binary.LittleEndian.PutUint32(cpReq[8:12], p.poolID)
binary.LittleEndian.PutUint32(cpReq[12:16], uint32(aligned))
if _, _, err := conn.WriteMsgUnix(cpReq[:], oob, nil); err != nil {
_ = unix.Munmap(data)
_ = unix.Close(mfd)
return fmt.Errorf("write create_pool: %w", err)
}
p.memfd = mfd
p.capacity = aligned
p.data = data
return nil
}
if err := unix.Ftruncate(p.memfd, int64(aligned)); err != nil {
return fmt.Errorf("ftruncate resize: %w", err)
}
_ = unix.Munmap(p.data)
data, err := unix.Mmap(p.memfd, 0, aligned, unix.PROT_READ|unix.PROT_WRITE, unix.MAP_SHARED)
if err != nil {
return fmt.Errorf("remmap: %w", err)
}
p.data = data
p.capacity = aligned
var rReq [12]byte
binary.LittleEndian.PutUint32(rReq[0:4], p.poolID)
binary.LittleEndian.PutUint32(rReq[4:8], (12<<16)|2) // wl_shm_pool.resize is opcode 2 (1 is destroy)
binary.LittleEndian.PutUint32(rReq[8:12], uint32(aligned))
_, err = conn.Write(rReq[:])
return err
}
func (p *shmPool) getBuffer(conn *net.UnixConn, w, h int, allocID func() uint32, sendMsg func(uint32, uint32, []byte, ...any) error) (uint32, []byte, int, error) {
stride := w * 4
size := stride * h
capNeeded := size * 2
if err := p.ensureCapacity(conn, capNeeded, allocID, sendMsg); err != nil {
return 0, nil, 0, err
}
slotIdx := 0
if p.slots[0].busy && !p.slots[1].busy {
slotIdx = 1
}
slot := &p.slots[slotIdx]
slot.offset = slotIdx * size
if slot.bufID != 0 && (slot.w != w || slot.h != h) && !slot.busy {
var dReq [8]byte
binary.LittleEndian.PutUint32(dReq[0:4], slot.bufID)
binary.LittleEndian.PutUint32(dReq[4:8], (8<<16)|0)
_, _ = conn.Write(dReq[:])
slot.bufID = 0
}
if slot.bufID == 0 {
slot.bufID = allocID()
var bReq [32]byte
binary.LittleEndian.PutUint32(bReq[0:4], p.poolID)
binary.LittleEndian.PutUint32(bReq[4:8], (32<<16)|0) // create_buffer opcode 0
binary.LittleEndian.PutUint32(bReq[8:12], slot.bufID)
binary.LittleEndian.PutUint32(bReq[12:16], uint32(slot.offset))
binary.LittleEndian.PutUint32(bReq[16:20], uint32(w))
binary.LittleEndian.PutUint32(bReq[20:24], uint32(h))
binary.LittleEndian.PutUint32(bReq[24:28], uint32(stride))
binary.LittleEndian.PutUint32(bReq[28:32], uint32(0)) // ARGB8888
if _, err := conn.Write(bReq[:]); err != nil {
return 0, nil, 0, err
}
slot.w = w
slot.h = h
slot.stride = stride
}
slot.busy = true
slot.released = false
dst := p.data[slot.offset : slot.offset+size]
return slot.bufID, dst, stride, nil
}
func (p *shmPool) handleRelease(bufID uint32) {
if p.slots[0].bufID == bufID {
p.slots[0].busy = false
p.slots[0].released = true
}
if p.slots[1].bufID == bufID {
p.slots[1].busy = false
p.slots[1].released = true
}
}
func (p *shmPool) adviseIdle() {
if len(p.data) > 0 {
_ = unix.Madvise(p.data, unix.MADV_DONTNEED)
}
}
func (p *shmPool) destroy(conn *net.UnixConn) {
for i := range p.slots {
if p.slots[i].bufID != 0 {
var dReq [8]byte
binary.LittleEndian.PutUint32(dReq[0:4], p.slots[i].bufID)
binary.LittleEndian.PutUint32(dReq[4:8], (8<<16)|0)
_, _ = conn.Write(dReq[:])
p.slots[i].bufID = 0
}
}
if p.poolID != 0 {
var dReq [8]byte
binary.LittleEndian.PutUint32(dReq[0:4], p.poolID)
binary.LittleEndian.PutUint32(dReq[4:8], (8<<16)|1)
_, _ = conn.Write(dReq[:])
p.poolID = 0
}
if len(p.data) > 0 {
_ = unix.Munmap(p.data)
p.data = nil
}
if p.memfd >= 0 {
_ = unix.Close(p.memfd)
p.memfd = -1
}
}
type clipImgReq struct {
resp chan []byte
}
type backend struct {
conn *net.UnixConn
selections chan string
events chan platform.Event
locateCh chan locateReq
showCh chan showReq
hideCh chan hideReq
shots chan []byte
shotsOnce sync.Once
ctx context.Context
clipImgCh chan clipImgReq
clipboardNotify chan struct{}
lastClipOfferNanos atomic.Int64
}
var (
_ platform.Backend = (*backend)(nil)
_ platform.Screenshotter = (*backend)(nil)
)
func (b *backend) Selections() <-chan string { return b.selections }
func (b *backend) Events() <-chan platform.Event { return b.events }
func (b *backend) Screenshots() <-chan []byte {
b.shotsOnce.Do(func() {
if sock := os.Getenv("NIRI_SOCKET"); sock != "" {
go b.watchNiri(b.ctx, sock)
}
})
return b.shots
}
// ReadClipboardImage returns the current image/png on the Wayland clipboard,
// using data-control selection (or wl-paste fallback).
func (b *backend) ReadClipboardImage(ctx context.Context) ([]byte, error) {
resp := make(chan []byte, 1)
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-b.ctx.Done():
return nil, b.ctx.Err()
case b.clipImgCh <- clipImgReq{resp: resp}:
}
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-b.ctx.Done():
return nil, b.ctx.Err()
case data := <-resp:
if len(data) == 0 {
return nil, errors.New("no image/png on clipboard")
}
return data, nil
}
}
// Pointer is a cheap current pointer position in Anchor space; ok is false where the platform cannot tell (Wayland).
func (b *backend) Pointer() (x, y float64, ok bool) {
return 0, 0, false
}
func (b *backend) Locate() (platform.Anchor, error) {
resp := make(chan locateResp, 1)
b.locateCh <- locateReq{resp: resp}
r := <-resp
return r.anchor, r.err
}
func (b *backend) Show(p platform.Popup) error {
resp := make(chan error, 1)
b.showCh <- showReq{popup: p, resp: resp}
return <-resp
}
func (b *backend) Hide() {
resp := make(chan struct{}, 1)
b.hideCh <- hideReq{resp: resp}
<-resp
}
type pendingShowState struct {
req showReq
bufID uint32
devW int
devH int
logW float64
logH float64
scale float64
hit platform.Rect
}
// Run connects to Wayland, initializes backend globals, and runs app.
func Run(app func(platform.Backend) error) error {
runtimeDir := os.Getenv("XDG_RUNTIME_DIR")
if runtimeDir == "" {
return errors.New("XDG_RUNTIME_DIR not set")
}
disp := os.Getenv("WAYLAND_DISPLAY")
if disp == "" {
disp = "wayland-0"
}
addr := runtimeDir + "/" + disp
conn, err := net.DialUnix("unix", nil, &net.UnixAddr{Name: addr, Net: "unix"})
if err != nil {
return fmt.Errorf("connect to wayland socket %q: %w", addr, err)
}
defer conn.Close()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
b := &backend{
conn: conn,
selections: make(chan string, 16),
events: make(chan platform.Event, 64),
locateCh: make(chan locateReq),
showCh: make(chan showReq),
hideCh: make(chan hideReq),
shots: make(chan []byte, 16),
ctx: ctx,
clipImgCh: make(chan clipImgReq),
clipboardNotify: make(chan struct{}, 1),
}
var nextID uint32 = 2
var freeIDs []uint32
allocID := func() uint32 {
if n := len(freeIDs); n > 0 {
id := freeIDs[n-1]
freeIDs = freeIDs[:n-1]
return id
}
id := nextID
nextID++
return id
}
sendMsg := func(objID, opcode uint32, oob []byte, args ...any) error {
var payload []byte
for _, arg := range args {
switch v := arg.(type) {
case uint32:
var buf [4]byte
binary.LittleEndian.PutUint32(buf[:], v)
payload = append(payload, buf[:]...)
case int32:
var buf [4]byte
binary.LittleEndian.PutUint32(buf[:], uint32(v))
payload = append(payload, buf[:]...)
case float64:
fixed := int32(math.Round(v * 256.0))
var buf [4]byte
binary.LittleEndian.PutUint32(buf[:], uint32(fixed))
payload = append(payload, buf[:]...)
case string:
payload = append(payload, encodeString(v)...)
case []byte:
payload = append(payload, v...)
}
}
totalLen := 8 + len(payload)
hdr := make([]byte, 8)
binary.LittleEndian.PutUint32(hdr[0:4], objID)
binary.LittleEndian.PutUint32(hdr[4:8], uint32((totalLen<<16)|int(opcode)))
msg := append(hdr, payload...)
if len(oob) > 0 {
_, _, err := conn.WriteMsgUnix(msg, oob, nil)
return err
}
_, err := conn.Write(msg)
return err
}
// recent requests, so a protocol error can say what we last sent to the
// object it names
type request struct {
obj, opcode uint32
args []any
}
var sent [128]request
var nsent int
send := func(objID, opcode uint32, args ...any) error {
sent[nsent%len(sent)] = request{objID, opcode, args}
nsent++
return sendMsg(objID, opcode, nil, args...)
}
history := func(obj uint32) string {
var ops []string
for i := max(0, nsent-len(sent)); i < nsent; i++ {
s := sent[i%len(sent)]
mentions := s.obj == obj
for _, a := range s.args {
mentions = mentions || a == any(obj)
}
if mentions {
ops = append(ops, fmt.Sprintf("%d.%d%v", s.obj, s.opcode, s.args))
}
}
return strings.Join(ops, " ")
}
// Reader goroutine for Wayland wire protocol with persistent framing buffer.
// The only event carrying a file descriptor is wl_keyboard.keymap.
var keyboardID atomic.Uint32
msgCh := make(chan waylandMsg, 128)
go func() {
buf := make([]byte, 0, 16384)
tmp := make([]byte, 8192)
oob := make([]byte, unix.CmsgSpace(4)*8)
var pendingFds []int
for {
for len(buf) >= 8 {
objID := binary.LittleEndian.Uint32(buf[0:4])
opAndSize := binary.LittleEndian.Uint32(buf[4:8])
opcode := opAndSize & 0xffff
sz := int(opAndSize >> 16)
if sz < 8 {
msgCh <- waylandMsg{err: fmt.Errorf("invalid message length: %d (less than 8)", sz)}
return
}
if len(buf) < sz {
break
}
recFd := -1
if len(pendingFds) > 0 && objID != 0 && objID == keyboardID.Load() && opcode == 0 {
recFd = pendingFds[0]
pendingFds = pendingFds[1:]
}
msgData := make([]byte, sz-8)
copy(msgData, buf[8:sz])
buf = buf[sz:]
msgCh <- waylandMsg{objID: objID, opcode: opcode, fd: recFd, data: msgData}
}
n, oobn, _, _, err := conn.ReadMsgUnix(tmp, oob)
if err != nil {
msgCh <- waylandMsg{err: err}
return
}
if n == 0 && oobn == 0 {
msgCh <- waylandMsg{err: io.EOF}
return
}
if oobn > 0 {
scms, err := unix.ParseSocketControlMessage(oob[:oobn])
if err == nil {
for _, scm := range scms {
fds, err := unix.ParseUnixRights(&scm)
if err == nil {
pendingFds = append(pendingFds, fds...)
}
}
}
}
if n > 0 {
buf = append(buf, tmp[:n]...)
}
}
}()
// 1. Get registry (ID 2)
registryID := allocID()
if err := send(1, 1, registryID); err != nil {
return err
}
// Initial sync to discover globals
syncID := allocID()
if err := send(1, 0, syncID); err != nil {
return err
}
var compName, shmName, seatName, layerShellName, layerShellVersion uint32
var extDataName, wlrDataName, viewporterName, xdgOutputMgrName, fracScaleMgrName uint32
initialOutputs := make(map[uint32]struct{})
for {
m := <-msgCh
if m.err != nil {
return fmt.Errorf("reading wayland globals: %w", m.err)
}
if m.objID == registryID && m.opcode == 0 { // global
if len(m.data) >= 8 {
name := binary.LittleEndian.Uint32(m.data[0:4])
iface, _, err := decodeString(m.data, 4)
if err == nil {
switch iface {
case "wl_compositor":
compName = name
case "wl_shm":
shmName = name
case "wl_seat":
seatName = name
case "zwlr_layer_shell_v1":
layerShellName = name
if _, next, err := decodeString(m.data, 4); err == nil && len(m.data) >= next+4 {
layerShellVersion = binary.LittleEndian.Uint32(m.data[next:])
}
case "ext_data_control_manager_v1":
extDataName = name
case "zwlr_data_control_manager_v1":
wlrDataName = name
case "wp_viewporter":
viewporterName = name
case "zxdg_output_manager_v1":
xdgOutputMgrName = name
case "wp_fractional_scale_manager_v1":
fracScaleMgrName = name
case "wl_output":
initialOutputs[name] = struct{}{}
}
}
}
}
if m.objID == syncID && m.opcode == 0 {
break
}
}
if compName == 0 || shmName == 0 || seatName == 0 || layerShellName == 0 {
return fmt.Errorf("missing required wayland globals: comp=%d shm=%d seat=%d layer=%d",
compName, shmName, seatName, layerShellName)
}
compID := allocID()
if err := send(registryID, 0, compName, "wl_compositor", uint32(4), compID); err != nil {
return err
}
shmID := allocID()
if err := send(registryID, 0, shmName, "wl_shm", uint32(1), shmID); err != nil {
return err
}
seatID := allocID()
if err := send(registryID, 0, seatName, "wl_seat", uint32(2), seatID); err != nil {
return err
}
// version 4 lets the card take keyboard focus when clicked (on_demand),
// so Escape and clicking elsewhere can close it
layerShellVersion = min(max(layerShellVersion, 1), 4)
layerShellID := allocID()
if err := send(registryID, 0, layerShellName, "zwlr_layer_shell_v1", layerShellVersion, layerShellID); err != nil {
return err
}
var viewporterID uint32
if viewporterName != 0 {
viewporterID = allocID()
_ = send(registryID, 0, viewporterName, "wp_viewporter", uint32(1), viewporterID)
}
var xdgOutputMgrID uint32
if xdgOutputMgrName != 0 {
xdgOutputMgrID = allocID()
_ = send(registryID, 0, xdgOutputMgrName, "zxdg_output_manager_v1", uint32(3), xdgOutputMgrID)
}
var fracScaleMgrID uint32
if fracScaleMgrName != 0 {
fracScaleMgrID = allocID()
_ = send(registryID, 0, fracScaleMgrName, "wp_fractional_scale_manager_v1", uint32(1), fracScaleMgrID)
}
// Track per-output state
boundOutputs := make(map[uint32]*outputInfo) // keyed by outID
boundXdgOutputs := make(map[uint32]*outputInfo) // keyed by xdgOutputID
boundFracScales := make(map[uint32]*outputInfo) // keyed by fracScaleID
boundLocateSurfs := make(map[uint32]*outputInfo) // keyed by locateSurfID
boundLocateLayers := make(map[uint32]*outputInfo) // keyed by locateLayerSurfID
// 1x1 transparent shm buffer for locate overlays (when viewporter is present)
locate1x1Memfd, err := unix.MemfdCreate("chirami-locate-1x1", unix.MFD_CLOEXEC)
if err != nil {
return err
}
defer unix.Close(locate1x1Memfd)
_ = unix.Ftruncate(locate1x1Memfd, 4)
locate1x1PoolID := allocID()
oob := unix.UnixRights(locate1x1Memfd)
var cpReq [16]byte
binary.LittleEndian.PutUint32(cpReq[0:4], shmID)
binary.LittleEndian.PutUint32(cpReq[4:8], (16<<16)|0)
binary.LittleEndian.PutUint32(cpReq[8:12], locate1x1PoolID)
binary.LittleEndian.PutUint32(cpReq[12:16], 4)
_, _, _ = conn.WriteMsgUnix(cpReq[:], oob, nil)
locate1x1BufID := allocID()
_ = send(locate1x1PoolID, 0, locate1x1BufID, int32(0), int32(1), int32(1), int32(4), uint32(0))
// Helper to bind an output and set up its locate layer surface
bindOutput := func(globalName uint32) *outputInfo {
outID := allocID()
_ = send(registryID, 0, globalName, "wl_output", uint32(2), outID)
out := &outputInfo{
id: outID,
globalName: globalName,
outputScale: 1,
physW: 1920,
physH: 1080,
}
boundOutputs[outID] = out
if xdgOutputMgrID != 0 {
xdgOutID := allocID()
_ = send(xdgOutputMgrID, 1, xdgOutID, outID) // get_xdg_output opcode 1
out.xdgOutputID = xdgOutID
boundXdgOutputs[xdgOutID] = out
}
// Locate surfaces are created on-demand during Locate() and unmapped/destroyed immediately after
return out
}
for name := range initialOutputs {
bindOutput(name)
}
// Data control binding: prefer ext_data_control_manager_v1, fall back to zwlr_data_control_manager_v1 v2
var dataDeviceID uint32
if extDataName != 0 {
extDataID := allocID()
if err := send(registryID, 0, extDataName, "ext_data_control_manager_v1", uint32(1), extDataID); err == nil {
dataDeviceID = allocID()
_ = send(extDataID, 1, dataDeviceID, seatID)
}
} else if wlrDataName != 0 {
wlrDataID := allocID()
if err := send(registryID, 0, wlrDataName, "zwlr_data_control_manager_v1", uint32(2), wlrDataID); err == nil {
dataDeviceID = allocID()
_ = send(wlrDataID, 1, dataDeviceID, seatID)
}
}
// Fallback polling if no data control protocol is supported
if dataDeviceID == 0 {
go func() {
var lastPolled string
ticker := time.NewTicker(300 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
cmd := exec.CommandContext(ctx, "wl-paste", "--primary", "--no-newline")
out, err := cmd.Output()
if err == nil && len(out) > 0 {
s := string(out)
if s != lastPolled {
lastPolled = s
select {
case b.selections <- s:
default:
}
}
}
}
}
}()
}
var pointerID uint32
// seatEvent asks for the pointer once the seat says it has one; the
// capabilities event usually arrives during the initial sync below
seatEvent := func(m waylandMsg) bool {
if m.objID != seatID || m.opcode != 0 || len(m.data) < 4 {
return false
}
caps := binary.LittleEndian.Uint32(m.data[0:4])
if caps&1 != 0 && pointerID == 0 {
pointerID = allocID()
_ = send(seatID, 0, pointerID) // seat.get_pointer
}
if caps&2 != 0 && keyboardID.Load() == 0 && layerShellVersion >= 4 {
id := allocID()
keyboardID.Store(id)
_ = send(seatID, 1, id) // seat.get_keyboard
}
return true
}
// keymap closes the keymap's descriptor: the card only needs Escape,
// whose evdev code needs no keymap
keymap := func(m waylandMsg) bool {
if m.objID == 0 || m.objID != keyboardID.Load() || m.opcode != 0 {
return false
}
if m.fd >= 0 {
_ = unix.Close(m.fd)
}
return true
}
offers := make(map[uint32][]string)
var primaryOffer, clipboardOffer uint32
handleDataControl := func(m waylandMsg) bool {
if dataDeviceID != 0 && m.objID == dataDeviceID {
if m.opcode == 0 { // data_offer(new_id)
offID := binary.LittleEndian.Uint32(m.data[0:4])
offers[offID] = nil
} else if m.opcode == 1 { // selection(id)
offID := binary.LittleEndian.Uint32(m.data[0:4])
clipboardOffer = offID
b.lastClipOfferNanos.Store(time.Now().UnixNano())
if offID != 0 && offerHasImage(offers[offID]) {
select {
case b.clipboardNotify <- struct{}{}:
default:
}
}
for oid := range offers {
if oid != primaryOffer && oid != clipboardOffer {
_ = send(oid, 1)
delete(offers, oid)
}
}
} else if m.opcode == 3 { // primary_selection(id)
offID := binary.LittleEndian.Uint32(m.data[0:4])
primaryOffer = offID
if offID != 0 {
mimes := offers[offID]
pick := ""
for _, cand := range []string{"text/plain;charset=utf-8", "UTF8_STRING", "text/plain"} {
for _, m := range mimes {
if strings.EqualFold(m, cand) {
pick = m
break
}
}
if pick != "" {
break
}
}
if pick == "" && len(mimes) > 0 {
for _, m := range mimes {
if strings.HasPrefix(m, "text/") {
pick = m
break
}
}
}
if pick != "" {
pr, pw, err := os.Pipe()
if err == nil {
strEnc := encodeString(pick)
recLen := 8 + len(strEnc)
recReq := make([]byte, recLen)
binary.LittleEndian.PutUint32(recReq[0:4], offID)
binary.LittleEndian.PutUint32(recReq[4:8], uint32((recLen<<16)|0))
copy(recReq[8:], strEnc)
oob := unix.UnixRights(int(pw.Fd()))
_, _, _ = conn.WriteMsgUnix(recReq, oob, nil)
_ = pw.Close()
go func(r *os.File) {
defer r.Close()
_ = r.SetReadDeadline(time.Now().Add(500 * time.Millisecond))
data, err := io.ReadAll(io.LimitReader(r, 65536))
if err == nil && len(data) > 0 {
text := string(data)
select {
case b.selections <- text:
default:
}
}
}(pr)
}
}
}
for oid := range offers {
if oid != primaryOffer && oid != clipboardOffer {
_ = send(oid, 1)
delete(offers, oid)
}
}
}
return true
}
if _, exists := offers[m.objID]; exists && m.opcode == 0 { // offer(mime_type)
mime, _, err := decodeString(m.data, 0)
if err == nil && mime != "" {
offers[m.objID] = append(offers[m.objID], mime)
if m.objID == clipboardOffer && strings.EqualFold(mime, "image/png") {
select {
case b.clipboardNotify <- struct{}{}:
default:
}
}
}
return true
}
return false
}
// Sync after binding initial globals
syncID = allocID()
if err := send(1, 0, syncID); err != nil {
return err
}
for {
m := <-msgCh
if m.err != nil {
return m.err
}
if handleDataControl(m) || seatEvent(m) || keymap(m) {
continue
}
if m.objID == 1 && m.opcode == 0 {
errStr, _, _ := decodeString(m.data, 8)
return fmt.Errorf("display error: %s", errStr)
}
if m.objID == 1 && m.opcode == 1 && len(m.data) >= 4 {
delID := binary.LittleEndian.Uint32(m.data[0:4])
if delID > 0 {
freeIDs = append(freeIDs, delID)
}
}
if out, ok := boundOutputs[m.objID]; ok {
switch m.opcode {
case 0:
if len(m.data) >= 8 {
out.physX = int32(binary.LittleEndian.Uint32(m.data[0:4]))
out.physY = int32(binary.LittleEndian.Uint32(m.data[4:8]))
}
case 1:
if len(m.data) >= 12 {
flags := binary.LittleEndian.Uint32(m.data[0:4])
if (flags & 1) != 0 {
out.physW = int32(binary.LittleEndian.Uint32(m.data[4:8]))
out.physH = int32(binary.LittleEndian.Uint32(m.data[8:12]))
}
}
case 3:
if len(m.data) >= 4 {
out.outputScale = int32(binary.LittleEndian.Uint32(m.data[0:4]))
}
case 4:
name, _, _ := decodeString(m.data, 0)
if name != "" {
out.name = name
}
}
}
if out, ok := boundXdgOutputs[m.objID]; ok {
switch m.opcode {
case 0:
if len(m.data) >= 8 {
out.logicalX = int32(binary.LittleEndian.Uint32(m.data[0:4]))
out.logicalY = int32(binary.LittleEndian.Uint32(m.data[4:8]))
}
case 1:
if len(m.data) >= 8 {
out.logicalW = int32(binary.LittleEndian.Uint32(m.data[0:4]))
out.logicalH = int32(binary.LittleEndian.Uint32(m.data[4:8]))
out.hasLogical = true
}
case 3:
name, _, _ := decodeString(m.data, 0)
if name != "" {
out.name = name
}
}
}
if out, ok := boundFracScales[m.objID]; ok {
if m.opcode == 0 && len(m.data) >= 4 {
scaleVal := binary.LittleEndian.Uint32(m.data[0:4])
if scaleVal > 0 {
out.preferredScale = float64(scaleVal) / 120.0
}
}
}
if m.objID == syncID && m.opcode == 0 {
break
}
}
// State inside event loop
var (
lastKnownOutput *outputInfo
lastKnownX = 960.0
lastKnownY = 540.0
showing = false
pointerInPopup = false
keyboardOnPopup = false // the card has keyboard focus: it was clicked
pointerX = 0.0
pointerY = 0.0
// Popup surface state
popupSurfID uint32
popupLayerSurfID uint32
popupViewportID uint32
popupOutID uint32
pendingShow *pendingShowState
// Offers map is managed by handleDataControl
// Locate synchronization
locateWaiters []chan locateResp
locateStart time.Time
locateTimer *time.Timer
locateTimerCh <-chan time.Time
)
// on_demand (2, layer shell v4) gives the card keyboard focus only when
// it is clicked; older versions get none
popupKeyboard := uint32(0)
if layerShellVersion >= 4 {
popupKeyboard = 2
}
shm := newShmPool(shmID)
defer shm.destroy(conn)
finishLocate := func(targetOut *outputInfo, x, y float64) {
dur := time.Since(locateStart)
if locateTimer != nil {
if !locateTimer.Stop() {
select {
case <-locateTimer.C:
default:
}
}
locateTimer = nil
locateTimerCh = nil
}
if targetOut == nil {
for _, o := range boundOutputs {
targetOut = o
break
}
}
if targetOut == nil {
targetOut = &outputInfo{physW: 1920, physH: 1080, outputScale: 1}
}
lastKnownOutput = targetOut
lastKnownX = x
lastKnownY = y
logW := targetOut.LogicalWidth()
logH := targetOut.LogicalHeight()
scale := targetOut.Scale()
debugf("[wayland] locate complete in %v: output=%q pos=(%.1f, %.1f) screen=(0, 0, %.0f, %.0f) scale=%.1f (unmapping all %d locate overlays)",
dur, targetOut.name, x, y, logW, logH, scale, len(boundOutputs))
// Unmap and destroy ALL locate surfaces immediately
// This ensures direct scanout for fullscreen games is never blocked and zero compositor overhead while idle
for _, out := range boundOutputs {
if out.locateSurfID != 0 {
_ = send(out.locateSurfID, 1, uint32(0), int32(0), int32(0)) // attach nil buffer to unmap
_ = send(out.locateSurfID, 6) // commit
if out.locateLayerSurfID != 0 {
_ = send(out.locateLayerSurfID, 7) // destroy layer surface
delete(boundLocateLayers, out.locateLayerSurfID)
out.locateLayerSurfID = 0
}
if out.locateViewportID != 0 {
_ = send(out.locateViewportID, 0) // destroy wp_viewport
}
_ = send(out.locateSurfID, 0) // destroy wl_surface
delete(boundLocateSurfs, out.locateSurfID)
out.locateSurfID = 0
out.locateViewportID = 0
out.locateMapped = false
}
}
anc := platform.Anchor{
X: x,
Y: y,
Screen: platform.Rect{X: 0, Y: 0, W: logW, H: logH},
Scale: scale,
}
for _, w := range locateWaiters {
w <- locateResp{anchor: anc, err: nil}
}
locateWaiters = nil
}
appErrCh := make(chan error, 1)
go func() {
appErrCh <- app(b)
}()
for {
select {
case err := <-appErrCh:
return err
case <-locateTimerCh:
locateTimer = nil
locateTimerCh = nil
fallbackOut := lastKnownOutput
if fallbackOut == nil {
for _, o := range boundOutputs {
fallbackOut = o
break
}
}
fbX := lastKnownX
fbY := lastKnownY
if fallbackOut != nil && (fbX <= 0 && fbY <= 0) {
fbX = fallbackOut.LogicalWidth() / 2.0
fbY = fallbackOut.LogicalHeight() / 2.0
}
finishLocate(fallbackOut, fbX, fbY)
case req := <-b.locateCh:
showing = false
pointerInPopup = false
locateWaiters = append(locateWaiters, req.resp)
if len(locateWaiters) == 1 {
locateStart = time.Now()
// Map locate overlays across all outputs
for _, out := range boundOutputs {
surfID := allocID()
_ = send(compID, 0, surfID)
out.locateSurfID = surfID
boundLocateSurfs[surfID] = out
if viewporterID != 0 {
vpID := allocID()
_ = send(viewporterID, 1, vpID, surfID)
out.locateViewportID = vpID
}
layerSurfID := allocID()
_ = send(layerShellID, 0, layerSurfID, surfID, out.id, uint32(3), "chirami-locate")
out.locateLayerSurfID = layerSurfID
boundLocateLayers[layerSurfID] = out
_ = send(layerSurfID, 0, uint32(0), uint32(0))
_ = send(layerSurfID, 1, uint32(15))
_ = send(layerSurfID, 3, int32(0), int32(0), int32(0), int32(0))
_ = send(layerSurfID, 2, uint32(0xffffffff))
_ = send(layerSurfID, 4, uint32(0))
_ = send(surfID, 6) // initial commit without buffer
}
if locateTimer == nil {
locateTimer = time.NewTimer(150 * time.Millisecond)
locateTimerCh = locateTimer.C
}
}
case req := <-b.showCh:
p := req.popup
targetOut := lastKnownOutput
if targetOut == nil {
for _, o := range boundOutputs {
targetOut = o
break
}
}
if targetOut == nil {
targetOut = &outputInfo{physW: 1920, physH: 1080, outputScale: 1}
}
scale := targetOut.Scale()
if scale <= 0 {
scale = 1.0
}
bounds := p.Image.Bounds()
devW := bounds.Dx()
devH := bounds.Dy()
logW := float64(devW) / scale
logH := float64(devH) / scale
bufID, dst, stride, err := shm.getBuffer(conn, devW, devH, allocID, sendMsg)
if err != nil {
req.resp <- err
continue
}
// Copy pixels converting premultiplied RGBA to BGRA (PixOffset-correct)
for y := 0; y < devH; y++ {
for x := 0; x < devW; x++ {
srcIdx := p.Image.PixOffset(bounds.Min.X+x, bounds.Min.Y+y)
dstIdx := y*stride + x*4
r := p.Image.Pix[srcIdx+0]
g := p.Image.Pix[srcIdx+1]
b := p.Image.Pix[srcIdx+2]
a := p.Image.Pix[srcIdx+3]
dst[dstIdx+0] = b
dst[dstIdx+1] = g
dst[dstIdx+2] = r
dst[dstIdx+3] = a
}
}
// If popup already showing on this same output, update in-place
if popupSurfID != 0 && popupOutID == targetOut.id {
_ = send(popupLayerSurfID, 0, uint32(math.Round(logW)), uint32(math.Round(logH)))
_ = send(popupLayerSurfID, 1, uint32(5))
_ = send(popupLayerSurfID, 3, int32(math.Round(p.Y)), int32(0), int32(0), int32(math.Round(p.X)))
if popupViewportID != 0 {
_ = send(popupViewportID, 2, int32(math.Round(logW)), int32(math.Round(logH)))
} else {
_ = send(popupSurfID, 8, int32(math.Round(scale)))
}
regID := allocID()
_ = send(compID, 1, regID)
_ = send(regID, 1, int32(p.Hit.X), int32(p.Hit.Y), int32(p.Hit.W), int32(p.Hit.H))
_ = send(popupSurfID, 5, regID)
_ = send(regID, 0)
_ = send(popupSurfID, 1, bufID, int32(0), int32(0))
_ = send(popupSurfID, 9, int32(0), int32(0), int32(devW), int32(devH))
_ = send(popupSurfID, 6)
showing = true
req.resp <- nil
continue
}
// Popup was hidden or on a different output: destroy old surface if any
if popupSurfID != 0 {
keyboardOnPopup = false
_ = send(popupLayerSurfID, 7) // zwlr_layer_surface_v1.destroy
if popupViewportID != 0 {
_ = send(popupViewportID, 0) // wp_viewport.destroy
}
_ = send(popupSurfID, 0) // wl_surface.destroy
popupSurfID = 0
popupLayerSurfID = 0
popupViewportID = 0
}
// Create popup layer surface on target output
popupSurfID = allocID()
_ = send(compID, 0, popupSurfID)
popupOutID = targetOut.id
if viewporterID != 0 {
popupViewportID = allocID()
_ = send(viewporterID, 1, popupViewportID, popupSurfID)
}
popupLayerSurfID = allocID()
_ = send(layerShellID, 0, popupLayerSurfID, popupSurfID, targetOut.id, uint32(3), "chirami-popup")
_ = send(popupLayerSurfID, 0, uint32(math.Round(logW)), uint32(math.Round(logH)))
_ = send(popupLayerSurfID, 1, uint32(5)) // anchor top|left
_ = send(popupLayerSurfID, 3, int32(math.Round(p.Y)), int32(0), int32(0), int32(math.Round(p.X)))
_ = send(popupLayerSurfID, 2, uint32(0xffffffff)) // exclusive -1
_ = send(popupLayerSurfID, 4, popupKeyboard) // focus when clicked
_ = send(popupSurfID, 6) // initial commit without buffer
pendingShow = &pendingShowState{
req: req,
bufID: bufID,
devW: devW,
devH: devH,
logW: logW,
logH: logH,
scale: scale,
hit: p.Hit,
}
case req := <-b.hideCh:
showing = false
pointerInPopup = false
keyboardOnPopup = false
if pendingShow != nil {
pendingShow.req.resp <- errors.New("popup hidden before configure")
pendingShow = nil
}
if popupSurfID != 0 {
_ = send(popupLayerSurfID, 7) // destroy layer surface
if popupViewportID != 0 {
_ = send(popupViewportID, 0) // destroy wp_viewport
}
_ = send(popupSurfID, 0) // destroy wl_surface
popupSurfID = 0
popupLayerSurfID = 0
popupViewportID = 0
popupOutID = 0
}
shm.adviseIdle()
req.resp <- struct{}{}
case req := <-b.clipImgCh:
if clipboardOffer != 0 && offerHasImage(offers[clipboardOffer]) {
pr, pw, err := os.Pipe()
if err != nil {
req.resp <- nil
continue
}
strEnc := encodeString("image/png")
recLen := 8 + len(strEnc)
recReq := make([]byte, recLen)
binary.LittleEndian.PutUint32(recReq[0:4], clipboardOffer)
binary.LittleEndian.PutUint32(recReq[4:8], uint32((recLen<<16)|0))
copy(recReq[8:], strEnc)
oob := unix.UnixRights(int(pw.Fd()))
_, _, _ = conn.WriteMsgUnix(recReq, oob, nil)
_ = pw.Close()
go func(r *os.File, resp chan []byte) {
defer r.Close()
_ = r.SetReadDeadline(time.Now().Add(2 * time.Second))
data, err := io.ReadAll(io.LimitReader(r, 64*1024*1024))
if err == nil && len(data) > 0 {
resp <- data
} else {
resp <- nil
}
}(pr, req.resp)
} else if dataDeviceID == 0 {
go func(resp chan []byte) {
cmd := exec.CommandContext(b.ctx, "wl-paste", "--type", "image/png", "--no-newline")
out, err := cmd.Output()
if err == nil && len(out) > 0 {
resp <- out
} else {
resp <- nil
}
}(req.resp)
} else {
req.resp <- nil
}
case m := <-msgCh:
if m.err != nil {
return m.err
}
// Display error
if m.objID == 1 && m.opcode == 0 {
failedObj := binary.LittleEndian.Uint32(m.data[0:4])
code := binary.LittleEndian.Uint32(m.data[4:8])
errStr, _, _ := decodeString(m.data, 8)
// "invalid object N" names an object we sent but the compositor
// no longer knows, usually as an argument
var bad uint32
if _, err := fmt.Sscanf(errStr, "invalid object %d", &bad); err != nil {
bad = failedObj
}
if os.Getenv("CHIRAMI_WL_TRACE") != "" {
for i := max(0, nsent-len(sent)); i < nsent; i++ {
s := sent[i%len(sent)]
log.Printf("sent %d.%d%v", s.obj, s.opcode, s.args)
}
}
return fmt.Errorf("wayland display error (obj %d, code %d): %s (recent requests involving %d: %s)", failedObj, code, errStr, bad, history(bad))
}
// Finding 2.1: wl_display.delete_id
if m.objID == 1 && m.opcode == 1 {
if len(m.data) >= 4 {
delID := binary.LittleEndian.Uint32(m.data[0:4])
if delID > 0 {
freeIDs = append(freeIDs, delID)
}
}
continue
}
// Finding 2.2: runtime wl_registry events (global / global_remove)
if m.objID == registryID {
if m.opcode == 0 { // global
name := binary.LittleEndian.Uint32(m.data[0:4])
iface, _, err := decodeString(m.data, 4)
if err == nil && iface == "wl_output" {
bindOutput(name)
}
} else if m.opcode == 1 { // global_remove
name := binary.LittleEndian.Uint32(m.data[0:4])
for outID, out := range boundOutputs {
if out.globalName == name {
if out.locateLayerSurfID != 0 {
_ = send(out.locateLayerSurfID, 7)
_ = send(out.locateSurfID, 0)
}
delete(boundOutputs, outID)
if out.xdgOutputID != 0 {
delete(boundXdgOutputs, out.xdgOutputID)
}
if out.locateFracScaleID != 0 {
delete(boundFracScales, out.locateFracScaleID)
}
delete(boundLocateSurfs, out.locateSurfID)
delete(boundLocateLayers, out.locateLayerSurfID)
if lastKnownOutput == out {
lastKnownOutput = nil
}
break
}
}
}
continue
}
// Output events
if out, ok := boundOutputs[m.objID]; ok {
switch m.opcode {
case 0: // geometry
if len(m.data) >= 8 {
out.physX = int32(binary.LittleEndian.Uint32(m.data[0:4]))
out.physY = int32(binary.LittleEndian.Uint32(m.data[4:8]))
}
case 1: // mode
if len(m.data) >= 12 {
flags := binary.LittleEndian.Uint32(m.data[0:4])
if (flags & 1) != 0 {
out.physW = int32(binary.LittleEndian.Uint32(m.data[4:8]))
out.physH = int32(binary.LittleEndian.Uint32(m.data[8:12]))
}
}
case 3: // scale
if len(m.data) >= 4 {
out.outputScale = int32(binary.LittleEndian.Uint32(m.data[0:4]))
}
case 4: // name
name, _, _ := decodeString(m.data, 0)
if name != "" {
out.name = name
}
}
continue
}
// XDG output events
if out, ok := boundXdgOutputs[m.objID]; ok {
switch m.opcode {
case 0: // logical_position
if len(m.data) >= 8 {
out.logicalX = int32(binary.LittleEndian.Uint32(m.data[0:4]))
out.logicalY = int32(binary.LittleEndian.Uint32(m.data[4:8]))
}
case 1: // logical_size
if len(m.data) >= 8 {
out.logicalW = int32(binary.LittleEndian.Uint32(m.data[0:4]))
out.logicalH = int32(binary.LittleEndian.Uint32(m.data[4:8]))
out.hasLogical = true
}
case 3: // name
name, _, _ := decodeString(m.data, 0)
if name != "" {
out.name = name
}
}
continue
}
// Fractional scale events
if out, ok := boundFracScales[m.objID]; ok {
if m.opcode == 0 && len(m.data) >= 4 { // preferred_scale
scaleVal := binary.LittleEndian.Uint32(m.data[0:4])
if scaleVal > 0 {
out.preferredScale = float64(scaleVal) / 120.0
}
}
continue
}
// Locate layer surface configure events (Finding 1 & 6.2)
if out, ok := boundLocateLayers[m.objID]; ok && m.opcode == 0 {
serial := binary.LittleEndian.Uint32(m.data[0:4])
w := binary.LittleEndian.Uint32(m.data[4:8])
h := binary.LittleEndian.Uint32(m.data[8:12])
out.locateW = int32(w)
out.locateH = int32(h)
_ = send(out.locateLayerSurfID, 6, serial) // ack_configure
if len(locateWaiters) > 0 {
if out.locateViewportID != 0 {
_ = send(out.locateViewportID, 2, int32(w), int32(h))
_ = send(out.locateSurfID, 1, locate1x1BufID, int32(0), int32(0))
_ = send(out.locateSurfID, 9, int32(0), int32(0), int32(1), int32(1))
} else {
if out.locateCustomBufID == 0 {
stride := int(w) * 4
bufSize := stride * int(h)
tMfd, err := unix.MemfdCreate("chirami-locate-trans", unix.MFD_CLOEXEC)
if err == nil {
_ = unix.Ftruncate(tMfd, int64(bufSize))
tPoolID := allocID()
tOob := unix.UnixRights(tMfd)
var tpReq [16]byte
binary.LittleEndian.PutUint32(tpReq[0:4], shmID)
binary.LittleEndian.PutUint32(tpReq[4:8], (16<<16)|0)
binary.LittleEndian.PutUint32(tpReq[8:12], tPoolID)
binary.LittleEndian.PutUint32(tpReq[12:16], uint32(bufSize))
_, _, _ = conn.WriteMsgUnix(tpReq[:], tOob, nil)
out.locateCustomBufID = allocID()
_ = send(tPoolID, 0, out.locateCustomBufID, int32(0), int32(w), int32(h), int32(stride), uint32(0))
_ = unix.Close(tMfd)
}
}
if out.locateCustomBufID != 0 {
_ = send(out.locateSurfID, 1, out.locateCustomBufID, int32(0), int32(0))
_ = send(out.locateSurfID, 9, int32(0), int32(0), int32(w), int32(h))
}
}
// Fullscreen input region to capture pointer
regID := allocID()
_ = send(compID, 1, regID)
_ = send(regID, 1, int32(0), int32(0), int32(w), int32(h))
_ = send(out.locateSurfID, 5, regID)
_ = send(regID, 0)
_ = send(out.locateSurfID, 6) // commit to map with input region
out.locateMapped = true
}
continue
}
// Popup layer surface configure events (Finding 6.3)
if popupLayerSurfID != 0 && m.objID == popupLayerSurfID && m.opcode == 0 {
serial := binary.LittleEndian.Uint32(m.data[0:4])
_ = send(popupLayerSurfID, 6, serial) // ack_configure
if pendingShow != nil {
ps := pendingShow
pendingShow = nil
if popupViewportID != 0 {
_ = send(popupViewportID, 2, int32(math.Round(ps.logW)), int32(math.Round(ps.logH)))
} else {
_ = send(popupSurfID, 8, int32(math.Round(ps.scale)))
}
regID := allocID()
_ = send(compID, 1, regID)
_ = send(regID, 1, int32(ps.hit.X), int32(ps.hit.Y), int32(ps.hit.W), int32(ps.hit.H))
_ = send(popupSurfID, 5, regID)
_ = send(regID, 0)
_ = send(popupSurfID, 1, ps.bufID, int32(0), int32(0))
_ = send(popupSurfID, 9, int32(0), int32(0), int32(ps.devW), int32(ps.devH))
_ = send(popupSurfID, 6)
showing = true
pointerInPopup = false
ps.req.resp <- nil
}
continue
}
// Buffer release events (Finding 3.1)
if m.opcode == 0 {
shm.handleRelease(m.objID)
}
if seatEvent(m) || keymap(m) {
continue
}
// Keyboard events: once clicked, the card holds keyboard focus
// (on_demand), so Escape reaches it. Focus moving elsewhere is
// ignored: clicking outside leaves the card up.
if kb := keyboardID.Load(); kb != 0 && m.objID == kb {
dismiss := false
switch m.opcode {
case 1: // enter(serial, surface, keys)
if len(m.data) >= 8 && binary.LittleEndian.Uint32(m.data[4:8]) == popupSurfID && showing {
keyboardOnPopup = true
}
case 2: // leave(serial, surface)
keyboardOnPopup = false
case 3: // key(serial, time, key, state)
const esc = 1 // evdev KEY_ESC
dismiss = len(m.data) >= 16 && keyboardOnPopup && showing &&
binary.LittleEndian.Uint32(m.data[8:12]) == esc && binary.LittleEndian.Uint32(m.data[12:16]) == 1
}
if dismiss {
select {
case b.events <- platform.Event{Kind: platform.Dismiss}:
default:
}
}
continue
}
// Data control events
if handleDataControl(m) {
continue
}
// Pointer events
if m.objID == pointerID {
switch m.opcode {
case 0: // enter(serial, surface, sx, sy)
if len(m.data) >= 16 {
surf := binary.LittleEndian.Uint32(m.data[4:8])
rawX := int32(binary.LittleEndian.Uint32(m.data[8:12]))
rawY := int32(binary.LittleEndian.Uint32(m.data[12:16]))
fx := float64(rawX) / 256.0
fy := float64(rawY) / 256.0
if out, ok := boundLocateSurfs[surf]; ok {
if len(locateWaiters) > 0 {
finishLocate(out, fx, fy)
}
} else if surf == popupSurfID && showing {
pointerInPopup = true
pointerX = fx
pointerY = fy
select {
case b.events <- platform.Event{Kind: platform.Motion, X: fx, Y: fy}:
default:
}
}
}
case 1: // leave(serial, surface)
if len(m.data) >= 8 {
surf := binary.LittleEndian.Uint32(m.data[4:8])
if surf == popupSurfID && showing && pointerInPopup {
pointerInPopup = false
select {
case b.events <- platform.Event{Kind: platform.Leave, X: pointerX, Y: pointerY}:
default:
}
}
}
case 2: // motion(time, sx, sy)
if len(m.data) >= 12 {
rawX := int32(binary.LittleEndian.Uint32(m.data[4:8]))
rawY := int32(binary.LittleEndian.Uint32(m.data[8:12]))
fx := float64(rawX) / 256.0
fy := float64(rawY) / 256.0
if showing && pointerInPopup {
pointerX = fx
pointerY = fy
select {
case b.events <- platform.Event{Kind: platform.Motion, X: fx, Y: fy}:
default:
}
}
}
case 3: // button(serial, time, button, state)
if len(m.data) >= 16 {
btn := binary.LittleEndian.Uint32(m.data[8:12])
state := binary.LittleEndian.Uint32(m.data[12:16])
if showing && pointerInPopup && btn == 272 && state == 0 {
select {
case b.events <- platform.Event{Kind: platform.Click, X: pointerX, Y: pointerY}:
default:
}
}
}
}
}
}
}
}
func offerHasImage(mimes []string) bool {
for _, m := range mimes {
if strings.EqualFold(m, "image/png") {
return true
}
}
return false
}