github.com/jmigpin/editor@v1.6.0/driver/xdriver/window.go (about) 1 package xdriver 2 3 import ( 4 "errors" 5 "fmt" 6 "image" 7 "image/draw" 8 "log" 9 "os" 10 "runtime" 11 "sync" 12 13 "github.com/BurntSushi/xgb" 14 "github.com/BurntSushi/xgb/shm" 15 "github.com/BurntSushi/xgb/xproto" 16 "github.com/jmigpin/editor/driver/xdriver/copypaste" 17 "github.com/jmigpin/editor/driver/xdriver/dragndrop" 18 "github.com/jmigpin/editor/driver/xdriver/wimage" 19 "github.com/jmigpin/editor/driver/xdriver/wmprotocols" 20 "github.com/jmigpin/editor/driver/xdriver/xcursors" 21 "github.com/jmigpin/editor/driver/xdriver/xinput" 22 "github.com/jmigpin/editor/driver/xdriver/xutil" 23 "github.com/jmigpin/editor/util/uiutil/event" 24 ) 25 26 type Window struct { 27 Conn *xgb.Conn 28 Window xproto.Window 29 Screen *xproto.ScreenInfo 30 GCtx xproto.Gcontext 31 32 Paste *copypaste.Paste 33 Copy *copypaste.Copy 34 Cursors *xcursors.Cursors 35 XInput *xinput.XInput 36 Wmp *wmprotocols.WMP 37 Dnd *dragndrop.Dnd 38 39 WImg wimage.WImage 40 41 close struct { 42 sync.RWMutex 43 closing bool 44 closed bool 45 } 46 } 47 48 func NewWindow() (*Window, error) { 49 display := os.Getenv("DISPLAY") 50 51 // help get a display target 52 origDisplay := display 53 if display == "" { 54 switch runtime.GOOS { 55 case "windows": 56 display = "127.0.0.1:0.0" 57 } 58 } 59 60 conn, err := xgb.NewConnDisplay(display) 61 if err != nil { 62 // improve error with hint 63 if origDisplay == "" { 64 err = fmt.Errorf("%w (Hint: is x11 running?)", err) 65 } 66 return nil, fmt.Errorf("x11 conn: %w", err) 67 } 68 69 // initialize extensions early to avoid concurrent map read/write (XGB issue) 70 wimage.Init(conn) 71 72 win := &Window{Conn: conn} 73 74 if err := win.initialize(); err != nil { 75 _ = win.Close() // best effort to close since it was opened 76 return nil, fmt.Errorf("win init: %w", err) 77 } 78 79 return win, nil 80 } 81 82 func (win *Window) initialize() error { 83 // Disable xgb logger that prints to stderr 84 //xgb.Logger = log.New(ioutil.Discard, "", 0) 85 86 si := xproto.Setup(win.Conn) 87 win.Screen = si.DefaultScreen(win.Conn) 88 89 window, err := xproto.NewWindowId(win.Conn) 90 if err != nil { 91 return err 92 } 93 win.Window = window 94 95 // event mask 96 var evMask uint32 = 0 | 97 xproto.EventMaskStructureNotify | 98 xproto.EventMaskExposure | 99 xproto.EventMaskPropertyChange | 100 //xproto.EventMaskPointerMotionHint | 101 //xproto.EventMaskButtonMotion | 102 xproto.EventMaskPointerMotion | 103 xproto.EventMaskButtonPress | 104 xproto.EventMaskButtonRelease | 105 xproto.EventMaskKeyPress | 106 xproto.EventMaskKeyRelease | 107 0 108 // mask/values order is defined by the protocol 109 mask := uint32(xproto.CwEventMask) 110 values := []uint32{evMask} 111 112 _ = xproto.CreateWindow( 113 win.Conn, 114 win.Screen.RootDepth, 115 win.Window, 116 win.Screen.Root, 117 0, 0, 500, 500, 118 0, // border width 119 xproto.WindowClassInputOutput, 120 win.Screen.RootVisual, 121 mask, values) 122 123 _ = xproto.MapWindow(win.Conn, window) 124 125 if err := xutil.LoadAtoms(win.Conn, &Atoms, false); err != nil { 126 return err 127 } 128 129 // graphical context 130 gCtx, err := xproto.NewGcontextId(win.Conn) 131 if err != nil { 132 return err 133 } 134 win.GCtx = gCtx 135 136 gmask := uint32(0) 137 gvalues := []uint32{} 138 c2 := xproto.CreateGCChecked(win.Conn, win.GCtx, xproto.Drawable(win.Window), gmask, gvalues) 139 if err := c2.Check(); err != nil { 140 return err 141 } 142 143 xi, err := xinput.NewXInput(win.Conn) 144 if err != nil { 145 return err 146 } 147 win.XInput = xi 148 149 dnd, err := dragndrop.NewDnd(win.Conn, win.Window) 150 if err != nil { 151 return err 152 } 153 win.Dnd = dnd 154 155 paste, err := copypaste.NewPaste(win.Conn, win.Window) 156 if err != nil { 157 return err 158 } 159 win.Paste = paste 160 161 copy, err := copypaste.NewCopy(win.Conn, win.Window) 162 if err != nil { 163 return err 164 } 165 win.Copy = copy 166 167 c, err := xcursors.NewCursors(win.Conn, win.Window) 168 if err != nil { 169 return err 170 } 171 win.Cursors = c 172 173 opt := &wimage.Options{win.Conn, win.Window, win.Screen, win.GCtx} 174 img, err := wimage.NewWImage(opt) 175 if err != nil { 176 return err 177 } 178 win.WImg = img 179 180 wmp, err := wmprotocols.NewWMP(win.Conn, win.Window) 181 if err != nil { 182 return err 183 } 184 win.Wmp = wmp 185 186 return nil 187 } 188 189 //---------- 190 191 func (win *Window) Close() (rerr error) { 192 win.close.Lock() 193 defer win.close.Unlock() 194 195 // TODO: closing the image may get memory errors from ongoing draws 196 // If a request is called outside the UI loop, using the image will give errors 197 //rerr = win.WImg.Close() 198 199 if !win.close.closed { 200 win.Conn.Close() // conn.WaitForEvent() will return with (nil,nil) 201 win.close.closed = true 202 } 203 204 return nil 205 } 206 207 func (win *Window) closeReqFromWindow() error { 208 win.close.Lock() 209 defer win.close.Unlock() 210 win.close.closing = true // no more requests allowed, speeds up closing 211 return nil 212 } 213 214 func (win *Window) connClosedPossiblyFromServer() { 215 win.close.Lock() 216 defer win.close.Unlock() 217 win.close.closed = true 218 } 219 220 //---------- 221 222 func (win *Window) NextEvent() (event.Event, bool) { 223 win.close.RLock() 224 ok := !win.close.closed 225 win.close.RUnlock() 226 if !ok { 227 return nil, false 228 } 229 230 for { 231 ev := win.nextEvent2() 232 // ev can be nil when the event was consumed internally 233 if ev == nil { 234 continue 235 } 236 return ev, true 237 } 238 } 239 240 func (win *Window) nextEvent2() interface{} { 241 ev, xerr := win.Conn.WaitForEvent() 242 if ev == nil { 243 if xerr != nil { 244 return error(xerr) 245 } 246 // connection closed: ev==nil && xerr==nil 247 win.connClosedPossiblyFromServer() 248 return &event.WindowClose{} 249 } 250 251 switch t := ev.(type) { 252 case xproto.ConfigureNotifyEvent: // structure (position,size,...) 253 //x, y := int(t.X), int(t.Y) // commented: must use (0,0) 254 w, h := int(t.Width), int(t.Height) 255 r := image.Rect(0, 0, w, h) 256 return &event.WindowResize{Rect: r} 257 case xproto.ExposeEvent: // region needs paint 258 //x, y := int(t.X), int(t.Y) // commented: must use (0,0) 259 w, h := int(t.Width), int(t.Height) 260 r := image.Rect(0, 0, w, h) 261 return &event.WindowExpose{Rect: r} 262 case xproto.MapNotifyEvent: // window mapped (created) 263 case xproto.ReparentNotifyEvent: // window rerooted 264 case xproto.MappingNotifyEvent: // keyboard mapping 265 if err := win.XInput.ReadMapTable(); err != nil { 266 return err 267 } 268 269 case xproto.KeyPressEvent: 270 return win.XInput.KeyPress(&t) 271 case xproto.KeyReleaseEvent: 272 return win.XInput.KeyRelease(&t) 273 case xproto.ButtonPressEvent: 274 return win.XInput.ButtonPress(&t) 275 case xproto.ButtonReleaseEvent: 276 return win.XInput.ButtonRelease(&t) 277 case xproto.MotionNotifyEvent: 278 return win.XInput.MotionNotify(&t) 279 280 case xproto.SelectionNotifyEvent: 281 win.Paste.OnSelectionNotify(&t) 282 win.Dnd.OnSelectionNotify(&t) 283 case xproto.SelectionRequestEvent: 284 if err := win.Copy.OnSelectionRequest(&t); err != nil { 285 return err 286 } 287 case xproto.SelectionClearEvent: 288 win.Copy.OnSelectionClear(&t) 289 290 case xproto.ClientMessageEvent: 291 delWin := win.Wmp.OnClientMessageDeleteWindow(&t) 292 if delWin { 293 // TODO: won't allow applications to ignore a close request 294 // speedup close (won't accept more requests) 295 win.closeReqFromWindow() 296 297 return &event.WindowClose{} 298 } 299 if ev2, err, ok := win.Dnd.OnClientMessage(&t); ok { 300 if err != nil { 301 return err 302 } else { 303 return ev2 304 } 305 } 306 307 case xproto.PropertyNotifyEvent: 308 win.Paste.OnPropertyNotify(&t) 309 310 case shm.CompletionEvent: 311 win.WImg.PutImageCompleted() 312 313 default: 314 log.Printf("unhandled event: %#v", ev) 315 } 316 return nil 317 } 318 319 //---------- 320 321 func (win *Window) Request(req event.Request) error { 322 // requests that need write lock 323 switch req.(type) { 324 case *event.ReqClose: 325 return win.Close() 326 } 327 328 win.close.RLock() 329 defer win.close.RUnlock() 330 if win.close.closing || win.close.closed { 331 return errors.New("window closing/closed") 332 } 333 334 switch r := req.(type) { 335 case *event.ReqWindowSetName: 336 return win.setWindowName(r.Name) 337 case *event.ReqImage: 338 r.ReplyImg = win.image() 339 return nil 340 case *event.ReqImagePut: 341 return win.WImg.PutImage(r.Rect) 342 case *event.ReqImageResize: 343 return win.resizeImage(r.Rect) 344 case *event.ReqCursorSet: 345 return win.setCursor(r.Cursor) 346 case *event.ReqPointerQuery: 347 p, err := win.queryPointer() 348 r.ReplyP = p 349 return err 350 case *event.ReqPointerWarp: 351 return win.warpPointer(r.P) 352 case *event.ReqClipboardDataGet: 353 s, err := win.Paste.Get(r.Index) 354 r.ReplyS = s 355 return err 356 case *event.ReqClipboardDataSet: 357 return win.Copy.Set(r.Index, r.Str) 358 default: 359 return fmt.Errorf("todo: %T", r) 360 } 361 } 362 363 //---------- 364 365 func (win *Window) setWindowName(str string) error { 366 c1 := xproto.ChangePropertyChecked( 367 win.Conn, 368 xproto.PropModeReplace, 369 win.Window, // requestor window 370 Atoms.NetWMName, // property 371 Atoms.Utf8String, // target 372 8, // format 373 uint32(len(str)), 374 []byte(str)) 375 return c1.Check() 376 } 377 378 //---------- 379 380 //func (win *Window) getGeometry() (*xproto.GetGeometryReply, error) { 381 // drawable := xproto.Drawable(win.Window) 382 // cookie := xproto.GetGeometry(win.Conn, drawable) 383 // return cookie.Reply() 384 //} 385 386 //---------- 387 388 func (win *Window) image() draw.Image { 389 return win.WImg.Image() 390 } 391 392 func (win *Window) resizeImage(r image.Rectangle) error { 393 ib := win.image().Bounds() 394 if !r.Eq(ib) { 395 err := win.WImg.Resize(r) 396 if err != nil { 397 return err 398 } 399 } 400 return nil 401 } 402 403 //---------- 404 405 func (win *Window) warpPointer(p image.Point) error { 406 // warp pointer only if the window has input focus 407 cookie := xproto.GetInputFocus(win.Conn) 408 reply, err := cookie.Reply() 409 if err != nil { 410 return err 411 } 412 if reply.Focus != win.Window { 413 return fmt.Errorf("window not focused") 414 } 415 c2 := xproto.WarpPointerChecked( 416 win.Conn, 417 xproto.WindowNone, 418 win.Window, 419 0, 0, 0, 0, 420 int16(p.X), int16(p.Y)) 421 return c2.Check() 422 } 423 424 func (win *Window) queryPointer() (image.Point, error) { 425 cookie := xproto.QueryPointer(win.Conn, win.Window) 426 r, err := cookie.Reply() 427 if err != nil { 428 return image.ZP, err 429 } 430 p := image.Point{int(r.WinX), int(r.WinY)} 431 return p, nil 432 } 433 434 //---------- 435 436 func (win *Window) setCursor(c event.Cursor) (rerr error) { 437 sc := func(c2 xcursors.Cursor) { 438 rerr = win.Cursors.SetCursor(c2) 439 } 440 switch c { 441 case event.NoneCursor: 442 sc(xcursors.XCNone) 443 case event.DefaultCursor: 444 sc(xcursors.XCNone) 445 case event.NSResizeCursor: 446 sc(xcursors.SBVDoubleArrow) 447 case event.WEResizeCursor: 448 sc(xcursors.SBHDoubleArrow) 449 case event.CloseCursor: 450 sc(xcursors.XCursor) 451 case event.MoveCursor: 452 sc(xcursors.Fleur) 453 case event.PointerCursor: 454 sc(xcursors.Hand2) 455 case event.BeamCursor: 456 sc(xcursors.XTerm) 457 case event.WaitCursor: 458 sc(xcursors.Watch) 459 } 460 return 461 } 462 463 //---------- 464 465 var Atoms struct { 466 NetWMName xproto.Atom `loadAtoms:"_NET_WM_NAME"` 467 Utf8String xproto.Atom `loadAtoms:"UTF8_STRING"` 468 }