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 }