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  }