github.com/jmigpin/editor@v1.6.0/util/uiutil/basicui.go (about)

     1  package uiutil
     2  
     3  import (
     4  	"fmt"
     5  	"image"
     6  	"image/draw"
     7  	"log"
     8  	"sync"
     9  	"time"
    10  
    11  	"github.com/jmigpin/editor/driver"
    12  	"github.com/jmigpin/editor/util/syncutil"
    13  	"github.com/jmigpin/editor/util/uiutil/event"
    14  	"github.com/jmigpin/editor/util/uiutil/mousefilter"
    15  	"github.com/jmigpin/editor/util/uiutil/widget"
    16  )
    17  
    18  type BasicUI struct {
    19  	DrawFrameRate int // frames per second
    20  	RootNode      widget.Node
    21  	Win           driver.Window
    22  
    23  	curCursor event.Cursor
    24  
    25  	closeOnce sync.Once
    26  
    27  	eventsQ *syncutil.SyncedQ // linked list queue (unlimited length)
    28  	applyEv *widget.ApplyEvent
    29  	movef   *mousefilter.MoveFilter
    30  	clickf  *mousefilter.ClickFilter
    31  	dragf   *mousefilter.DragFilter
    32  
    33  	pendingPaint   bool
    34  	lastPaintStart time.Time
    35  }
    36  
    37  func NewBasicUI(winName string, root widget.Node) (*BasicUI, error) {
    38  	win, err := driver.NewWindow()
    39  	if err != nil {
    40  		return nil, err
    41  	}
    42  
    43  	req := &event.ReqWindowSetName{winName}
    44  	if err := win.Request(req); err != nil {
    45  		return nil, err
    46  	}
    47  
    48  	ui := &BasicUI{
    49  		DrawFrameRate: 37,
    50  		Win:           win,
    51  	}
    52  
    53  	ui.eventsQ = syncutil.NewSyncedQ()
    54  	ui.applyEv = widget.NewApplyEvent(ui)
    55  	ui.initMouseFilters()
    56  
    57  	// Embed nodes have their wrapper nodes set when they are appended to another node. The root node is not appended to any other node, therefore it needs to be set here.
    58  	ui.RootNode = root
    59  	root.Embed().SetWrapperForRoot(root)
    60  
    61  	go ui.eventLoop()
    62  
    63  	return ui, nil
    64  }
    65  
    66  func (ui *BasicUI) initMouseFilters() {
    67  	// move filter
    68  	isMouseMoveEv := func(ev interface{}) bool {
    69  		if wi, ok := ev.(*event.WindowInput); ok {
    70  			if _, ok := wi.Event.(*event.MouseMove); ok {
    71  				return true
    72  			}
    73  		}
    74  		return false
    75  	}
    76  	ui.movef = mousefilter.NewMoveFilter(ui.DrawFrameRate, ui.eventsQ.PushBack, isMouseMoveEv)
    77  
    78  	// click/drag filters
    79  	emitFn := func(ev interface{}, p image.Point) {
    80  		ui.handleWidgetEv(ev, p)
    81  	}
    82  	ui.clickf = mousefilter.NewClickFilter(emitFn)
    83  	ui.dragf = mousefilter.NewDragFilter(emitFn)
    84  }
    85  
    86  //----------
    87  
    88  func (ui *BasicUI) Close() {
    89  	ui.closeOnce.Do(func() {
    90  		req := &event.ReqClose{}
    91  		if err := ui.Win.Request(req); err != nil {
    92  			log.Println(err)
    93  		}
    94  	})
    95  }
    96  
    97  //----------
    98  
    99  func (ui *BasicUI) eventLoop() {
   100  	for {
   101  		//ui.eventsQ.PushBack(ui.Win.NextEvent()) // slow UI
   102  
   103  		ev, ok := ui.Win.NextEvent()
   104  		if !ok {
   105  			break
   106  		}
   107  		ui.movef.Filter(ev) // sends events to ui.eventsQ.In()
   108  	}
   109  }
   110  
   111  //----------
   112  
   113  // How to use NextEvent():
   114  //
   115  //func SampleEventLoop() {
   116  //	defer ui.Close()
   117  //	for {
   118  //		ev := ui.NextEvent()
   119  //		switch t := ev.(type) {
   120  //		case error:
   121  //			fmt.Println(err)
   122  //		case *event.WindowClose:
   123  //			return
   124  //		default:
   125  //			ui.HandleEvent(ev)
   126  //		}
   127  //		ui.LayoutMarkedAndSchedulePaint()
   128  //	}
   129  //}
   130  func (ui *BasicUI) NextEvent() interface{} {
   131  	return ui.eventsQ.PopFront()
   132  }
   133  
   134  //----------
   135  
   136  func (ui *BasicUI) AppendEvent(ev interface{}) {
   137  	ui.eventsQ.PushBack(ev)
   138  }
   139  
   140  //----------
   141  
   142  func (ui *BasicUI) HandleEvent(ev interface{}) (handled bool) {
   143  	switch t := ev.(type) {
   144  	case *event.WindowResize:
   145  		ui.resizeImage(t.Rect)
   146  	case *event.WindowExpose:
   147  		ui.RootNode.Embed().MarkNeedsPaint()
   148  	case *event.WindowInput:
   149  		ui.handleWindowInput(t)
   150  	case *UIRunFuncEvent:
   151  		t.Func()
   152  	case *UIPaintTime:
   153  		ui.paint()
   154  	case struct{}:
   155  		// no op, allow layout/schedule funcs to run
   156  	default:
   157  		return false
   158  	}
   159  	return true
   160  }
   161  
   162  func (ui *BasicUI) handleWindowInput(wi *event.WindowInput) {
   163  	ui.handleWidgetEv(wi.Event, wi.Point)
   164  	ui.clickf.Filter(wi.Event) // emit events; set on initMouseFilters()
   165  	ui.dragf.Filter(wi.Event)  // emit events; set on initMouseFilters()
   166  }
   167  func (ui *BasicUI) handleWidgetEv(ev interface{}, p image.Point) {
   168  	ui.applyEv.Apply(ui.RootNode, ev, p)
   169  }
   170  
   171  //----------
   172  
   173  func (ui *BasicUI) LayoutMarkedAndSchedulePaint() {
   174  	ui.RootNode.LayoutMarked()
   175  	ui.schedulePaintMarked()
   176  }
   177  
   178  //----------
   179  
   180  func (ui *BasicUI) resizeImage(r image.Rectangle) {
   181  	req := &event.ReqImageResize{r}
   182  	if err := ui.Win.Request(req); err != nil {
   183  		log.Println(err)
   184  		return
   185  	}
   186  
   187  	req2 := &event.ReqImage{}
   188  	if err := ui.Win.Request(req2); err != nil {
   189  		log.Println(err)
   190  		return
   191  	}
   192  	img := req2.ReplyImg
   193  
   194  	ib := img.Bounds()
   195  	en := ui.RootNode.Embed()
   196  	if !en.Bounds.Eq(ib) {
   197  		en.Bounds = ib
   198  		en.MarkNeedsLayout()
   199  		en.MarkNeedsPaint()
   200  	}
   201  }
   202  
   203  //----------
   204  
   205  func (ui *BasicUI) schedulePaintMarked() {
   206  	if ui.RootNode.Embed().TreeNeedsPaint() {
   207  		ui.schedulePaint()
   208  	}
   209  }
   210  func (ui *BasicUI) schedulePaint() {
   211  	if ui.pendingPaint {
   212  		return
   213  	}
   214  	ui.pendingPaint = true
   215  	// schedule
   216  	go func() {
   217  		d := ui.durationToNextPaint()
   218  		if d > 0 {
   219  			time.Sleep(d)
   220  		}
   221  		ui.AppendEvent(&UIPaintTime{})
   222  	}()
   223  }
   224  
   225  func (ui *BasicUI) durationToNextPaint() time.Duration {
   226  	now := time.Now()
   227  	frameDur := time.Second / time.Duration(ui.DrawFrameRate)
   228  	d := now.Sub(ui.lastPaintStart)
   229  	return frameDur - d
   230  }
   231  
   232  //----------
   233  
   234  func (ui *BasicUI) paint() {
   235  	// DEBUG: print fps
   236  	now := time.Now()
   237  	//d := now.Sub(ui.lastPaintStart)
   238  	//fmt.Printf("paint: fps %v\n", int(time.Second/d))
   239  	ui.lastPaintStart = now
   240  
   241  	ui.paintMarked()
   242  }
   243  
   244  func (ui *BasicUI) paintMarked() {
   245  	ui.pendingPaint = false
   246  	u := ui.RootNode.PaintMarked()
   247  	r := u.Intersect(ui.Image().Bounds())
   248  	if !r.Empty() {
   249  		ui.putImage(r)
   250  	}
   251  }
   252  
   253  func (ui *BasicUI) putImage(r image.Rectangle) {
   254  	req := &event.ReqImagePut{r}
   255  	if err := ui.Win.Request(req); err != nil {
   256  		log.Println(err)
   257  		return
   258  	}
   259  }
   260  
   261  //----------
   262  
   263  func (ui *BasicUI) EnqueueNoOpEvent() {
   264  	ui.AppendEvent(struct{}{})
   265  }
   266  
   267  func (ui *BasicUI) Image() draw.Image {
   268  	req := &event.ReqImage{}
   269  	if err := ui.Win.Request(req); err != nil {
   270  		// dummy img to avoid errors
   271  		return image.NewRGBA(image.Rect(0, 0, 1, 1))
   272  	}
   273  	return req.ReplyImg
   274  }
   275  
   276  func (ui *BasicUI) WarpPointer(p image.Point) {
   277  	req := &event.ReqPointerWarp{p}
   278  	if err := ui.Win.Request(req); err != nil {
   279  		log.Println(err)
   280  		return
   281  	}
   282  }
   283  
   284  func (ui *BasicUI) QueryPointer() (image.Point, error) {
   285  	req := &event.ReqPointerQuery{}
   286  	err := ui.Win.Request(req)
   287  	return req.ReplyP, err
   288  }
   289  
   290  //----------
   291  
   292  // Implements widget.CursorContext
   293  func (ui *BasicUI) SetCursor(c event.Cursor) {
   294  	if ui.curCursor == c {
   295  		return
   296  	}
   297  	ui.curCursor = c
   298  
   299  	req := &event.ReqCursorSet{c}
   300  	if err := ui.Win.Request(req); err != nil {
   301  		log.Println(err)
   302  		return
   303  	}
   304  }
   305  
   306  //----------
   307  
   308  func (ui *BasicUI) GetClipboardData(i event.ClipboardIndex, fn func(string, error)) {
   309  	go func() {
   310  		req := &event.ReqClipboardDataGet{Index: i}
   311  		err := ui.Win.Request(req)
   312  		if err != nil {
   313  			ui.AppendEvent(fmt.Errorf("getclipboarddata: %w", err))
   314  		}
   315  		fn(req.ReplyS, err)
   316  	}()
   317  }
   318  func (ui *BasicUI) SetClipboardData(i event.ClipboardIndex, s string) {
   319  	req := &event.ReqClipboardDataSet{Index: i, Str: s}
   320  	if err := ui.Win.Request(req); err != nil {
   321  		ui.AppendEvent(fmt.Errorf("setclipboarddata: %w", err))
   322  		return
   323  	}
   324  }
   325  
   326  //----------
   327  
   328  func (ui *BasicUI) RunOnUIGoRoutine(f func()) {
   329  	ui.AppendEvent(&UIRunFuncEvent{f})
   330  }
   331  
   332  // Use with care to avoid UI deadlock (waiting within another wait).
   333  func (ui *BasicUI) WaitRunOnUIGoRoutine(f func()) {
   334  	ch := make(chan struct{}, 1)
   335  	ui.RunOnUIGoRoutine(func() {
   336  		f()
   337  		ch <- struct{}{}
   338  	})
   339  	<-ch
   340  }
   341  
   342  // Allows triggering a run of applyevent (ex: useful for cursor update, needs point or it won't work).
   343  func (ui *BasicUI) QueueEmptyWindowInputEvent() {
   344  	p, err := ui.QueryPointer()
   345  	if err != nil {
   346  		return
   347  	}
   348  	ui.AppendEvent(&event.WindowInput{Point: p})
   349  }
   350  
   351  //----------
   352  
   353  type UIPaintTime struct{}
   354  
   355  type UIRunFuncEvent struct {
   356  	Func func()
   357  }