1 /// 2 module nanogui.screen; 3 4 import std.algorithm : min; 5 import std.experimental.logger : Logger; 6 7 import arsd.nanovega; 8 public import gfm.math : vec2i; 9 10 import nanogui.widget : Widget; 11 import nanogui.common : Vector2i, Vector2f, MouseButton, MouseAction, KeyAction, Cursor, NanoContext; 12 13 class Screen : Widget 14 { 15 import nanogui.window : Window; 16 17 this(int w, int h, long timestamp) 18 { 19 super(null); 20 size = vec2i(w, h); 21 mNeedToDraw = true; 22 mLastInteraction = mTimestamp = timestamp; 23 mCursor = Cursor.Arrow; 24 mPixelRatio = 1.0; 25 } 26 27 auto currTime() const { return mTimestamp; } 28 void currTime(long value) 29 { 30 mTimestamp = value; 31 auto elapsed = value - mLastInteraction; 32 if (!mTooltipShown && elapsed > 5_000_000) 33 { 34 const widget = findWidget(mMousePos); 35 if (widget && widget.tooltip.length) 36 mNeedToDraw = true; 37 } 38 } 39 40 auto lastInteraction() { return mLastInteraction; } 41 42 override void draw(ref NanoContext ctx) 43 { 44 import arsd.simpledisplay; 45 46 // draw GLCanvas widgets to textures 47 foreach(glcanvas; mGLCanvases[]) 48 { 49 // Resize GLCanvas in non optimal way 50 if (glcanvas.lastWidth != glcanvas.width || 51 glcanvas.lastHeight != glcanvas.height) 52 { 53 // This is resource consuming work that 54 // can/should be avoided but considering 55 // resizing seldom operation 56 // it is appropriate currently 57 glcanvas.lastWidth = glcanvas.width; 58 glcanvas.lastHeight = glcanvas.height; 59 glcanvas.releaseBuffers; 60 glcanvas.initBuffers; 61 } 62 glcanvas.mFbo.use; 63 glViewport(0, 0, glcanvas.width, glcanvas.height); 64 const clr = glcanvas.backgroundColor; 65 glClearColor(clr[0], clr[1], clr[2], clr[3]); 66 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); 67 glcanvas.drawGL; 68 glcanvas.mFbo.unuse(); 69 } 70 71 // draw the rest 72 glViewport(0, 0, size.x, size.y); 73 glClearColor(0., 0., 0., 0); 74 glClear(glNVGClearFlags); // use NanoVega API to get flags for OpenGL call 75 ctx.beginFrame(size.x, size.y); // begin rendering 76 scope(exit) 77 { 78 if (ctx.inFrame) 79 ctx.endFrame(); // and flush render queue on exit 80 } 81 82 if (mNeedToPerfomLayout) 83 { 84 performLayout(ctx); 85 needToPerfomLayout = false; 86 } 87 88 super.draw(ctx); 89 90 mNeedToDraw = false; 91 92 float elapsed = (mTimestamp - mLastInteraction)/10_000_000.0f; 93 94 if (elapsed > 0.5f) 95 { 96 /* Draw tooltips */ 97 const widget = findWidget(mMousePos); 98 if (widget && widget.tooltip.length) { 99 int tooltipWidth = 150; 100 101 float[4] bounds; 102 ctx.fontFace("sans"); 103 ctx.fontSize(15.0f); 104 NVGTextAlign algn; 105 algn.left = true; 106 algn.top = true; 107 ctx.textAlign(algn); 108 ctx.textLineHeight(1.1f); 109 Vector2i pos = widget.absolutePosition() + 110 Vector2i(widget.width() / 2, widget.height() + 10); 111 112 ctx.textBounds(pos.x, pos.y, 113 widget.tooltip, bounds); 114 int h = cast(int) (bounds[2] - bounds[0]) / 2; 115 116 if (h > tooltipWidth / 2) { 117 algn.center = true; 118 algn.top = true; 119 ctx.textAlign(algn); 120 ctx.textBoxBounds(pos.x, pos.y, tooltipWidth, 121 widget.tooltip, bounds); 122 123 h = cast(int)(bounds[2] - bounds[0]) / 2; 124 } 125 enum threshold = 0.8f; 126 auto alpha = min(1.0, 2 * (elapsed - 0.5f)) * threshold; 127 ctx.globalAlpha(alpha); 128 mTooltipShown = (alpha > threshold - 0.01) ? true : false; 129 130 ctx.beginPath; 131 ctx.fillColor(Color(0, 0, 0, 255)); 132 ctx.roundedRect(bounds[0] - 4 - h, bounds[1] - 4, 133 cast(int) (bounds[2] - bounds[0]) + 8, 134 cast(int) (bounds[3] - bounds[1]) + 8, 3); 135 136 int px = cast(int) ((bounds[2] + bounds[0]) / 2) - h; 137 ctx.moveTo(px, bounds[1] - 10); 138 ctx.lineTo(px + 7, bounds[1] + 1); 139 ctx.lineTo(px - 7, bounds[1] + 1); 140 ctx.fill(); 141 142 ctx.fillColor(Color(255, 255, 255, 255)); 143 ctx.fontBlur(0.0f); 144 ctx.textBox(pos.x - h, pos.y, tooltipWidth, 145 widget.tooltip); 146 } 147 } 148 else 149 mTooltipShown = false; 150 } 151 152 bool mouseButtonCallbackEvent(MouseButton button, MouseAction action, int modifiers, long timestamp) 153 { 154 mNeedToDraw = true; 155 mModifiers = modifiers; 156 mLastInteraction = timestamp; 157 try 158 { 159 if (mFocusPath.length > 1) 160 { 161 const window = cast(Window) (mFocusPath[mFocusPath.length - 2]); 162 if (window && window.modal) 163 { 164 if (!window.contains(mMousePos)) 165 return false; 166 } 167 } 168 169 if (action == MouseAction.Press) 170 mMouseState |= 1 << button; 171 else 172 mMouseState &= ~(1 << button); 173 174 const dropWidget = findWidget(mMousePos); 175 if (mDragActive && action == MouseAction.Release && 176 dropWidget !is mDragWidget) 177 mDragWidget.mouseButtonEvent( 178 mMousePos - mDragWidget.parent.absolutePosition, button, 179 false, mModifiers); 180 181 if (dropWidget !is null && dropWidget.cursor != mCursor) 182 cursor = dropWidget.cursor; 183 184 if (action == MouseAction.Press && (button ==MouseButton.Left || button == MouseButton.Right)) { 185 mDragWidget = findWidget(mMousePos); 186 if (mDragWidget is this) 187 mDragWidget = null; 188 mDragActive = mDragWidget !is null; 189 if (!mDragActive) 190 updateFocus(null); 191 } else { 192 mDragActive = false; 193 mDragWidget = null; 194 } 195 196 return mouseButtonEvent(mMousePos, button, action == MouseAction.Press, 197 mModifiers); 198 } 199 catch (Exception e) 200 { 201 import std.stdio : stderr; 202 stderr.writeln("Caught exception in event handler: ", e.msg); 203 return false; 204 } 205 } 206 207 /// Return the last observed mouse position value 208 Vector2i mousePos() const { return mMousePos; } 209 210 final void updateFocus(Widget widget) 211 { 212 mNeedToDraw = true; 213 foreach (w; mFocusPath) 214 { 215 if (!w.focused) 216 continue; 217 w.focusEvent(false); 218 } 219 mFocusPath.clear; 220 Widget window; 221 while (widget) 222 { 223 mFocusPath.insertBack(widget); 224 if (cast(Window)(widget)) 225 window = widget; 226 widget = widget.parent; 227 } 228 foreach_reverse(it; mFocusPath) 229 it.focusEvent(true); 230 231 if (window) 232 moveWindowToFront(cast(Window) window); 233 } 234 235 bool cursorPosCallbackEvent(double x, double y, long last_interaction) 236 { 237 mNeedToDraw = true; 238 auto p = Vector2i(cast(int) x, cast(int) y); 239 240 //#if defined(_WIN32) || defined(__linux__) 241 // p = (p.cast<float>() / mPixelRatio).cast<int>(); 242 //#endif 243 244 bool ret; 245 mLastInteraction = last_interaction; 246 try 247 { 248 p -= Vector2i(1, 2); 249 250 if (!mDragActive) 251 { 252 const widget = findWidget(p); 253 if (widget !is null && widget !is this) 254 { 255 if (widget.cursor != mCursor) 256 cursor = widget.cursor; 257 } 258 else 259 { 260 if (Cursor.Arrow != mCursor) 261 cursor = Cursor.Arrow; 262 } 263 } 264 else 265 { 266 ret = mDragWidget.mouseDragEvent( 267 p - mDragWidget.parent.absolutePosition, p - mMousePos, 268 mMouseState, mModifiers); 269 } 270 271 if (!ret) 272 ret = mouseMotionEvent(p, p - mMousePos, mMouseState, mModifiers); 273 274 mMousePos = p; 275 276 return ret; 277 } 278 catch (Exception e) 279 { 280 import std.stdio : stderr; 281 stderr.writeln("Caught exception in event handler: ", e.msg); 282 return false; 283 } 284 } 285 286 void moveWindowToFront(Window window) { 287 // mChildren.erase(std::remove(mChildren.begin(), mChildren.end(), window), mChildren.end()); 288 { 289 // non-idiomatic way to implement erase-remove idiom in dlang 290 size_t i; 291 foreach(_; mChildren) 292 { 293 if (mChildren[i] is window) 294 break; 295 i++; 296 } 297 if (i < mChildren.length) 298 { 299 foreach(j; i..mChildren.length-1) 300 mChildren[j] = mChildren[j+1]; 301 mChildren.removeBack; 302 } 303 } 304 mChildren.insertBack(window); 305 /* Brute force topological sort (no problem for a few windows..) */ 306 bool changed = false; 307 do { 308 size_t baseIndex = 0; 309 for (size_t index = 0; index < mChildren.length; ++index) 310 if (mChildren[index] == window) 311 baseIndex = index; 312 changed = false; 313 for (size_t index = 0; index < mChildren.length; ++index) 314 { 315 import nanogui.popup : Popup; 316 Popup pw = cast(Popup) mChildren[index]; 317 if (pw && pw.parentWindow is window && index < baseIndex) { 318 moveWindowToFront(pw); 319 changed = true; 320 break; 321 } 322 } 323 } while (changed); 324 mNeedToDraw = true; 325 } 326 327 bool scrollCallbackEvent(double x, double y, long timestamp) 328 { 329 mLastInteraction = timestamp; 330 try 331 { 332 if (mFocusPath.length > 1) 333 { 334 const window = cast(Window) mFocusPath[mFocusPath.length - 2]; 335 if (window && window.modal) 336 { 337 if (!window.contains(mMousePos)) 338 return false; 339 } 340 } 341 return scrollEvent(mMousePos, Vector2f(x, y)); 342 } 343 catch (Exception e) 344 { 345 import std.stdio : stderr; 346 stderr.writeln("Caught exception in event handler: ", e.msg); 347 return false; 348 } 349 } 350 351 override bool keyboardEvent(int key, int scancode, KeyAction action, int modifiers) 352 { 353 if (mFocusPath.length > 0) 354 { 355 foreach_reverse(w; mFocusPath) 356 { 357 if (w is this) 358 continue; 359 if (w.focused && w.keyboardEvent(key, scancode, action, modifiers)) 360 return true; 361 } 362 } 363 364 return false; 365 } 366 367 override bool keyboardCharacterEvent(dchar codepoint) 368 { 369 if (mFocusPath.length) 370 { 371 foreach_reverse(w; mFocusPath) 372 { 373 if (w is this) 374 continue; 375 if (w.focused && w.keyboardCharacterEvent(codepoint)) 376 return true; 377 } 378 } 379 return false; 380 } 381 382 /// Window resize event handler 383 bool resizeEvent(Vector2i size) 384 { 385 if (mResizeCallback) { 386 mResizeCallback(size); 387 mNeedToDraw = true; 388 return true; 389 } 390 return false; 391 } 392 393 Logger logger() { return null; }; 394 395 /// Return the ratio between pixel and device coordinates (e.g. >= 2 on Mac Retina displays) 396 float pixelRatio() const { return mPixelRatio; } 397 398 package bool needToDraw() const pure @safe nothrow { return mNeedToDraw; } 399 // TODO add package qualifier at least but better to remove it completely 400 void needToDraw(bool value) pure @safe nothrow { if (value) mNeedToDraw = value; } 401 package bool needToPerfomLayout() const pure @safe nothrow { return mNeedToPerfomLayout; } 402 package void needToPerfomLayout(bool value) pure @safe nothrow { if (value) mNeedToPerfomLayout = value; } 403 package bool blinkingCursorIsVisibleNow() const pure @safe nothrow { return mBlinkingCursorVisible; } 404 405 package void resetBlinkingCursor() @safe 406 { 407 import std.datetime : Clock; 408 mBlinkingCursorVisible = true; 409 mBlinkingCursorTimestamp = Clock.currTime.stdTime; 410 } 411 412 package: 413 import nanogui.glcanvas : GLCanvas; 414 415 void addGLCanvas(GLCanvas dw) 416 { 417 mGLCanvases.insert(dw); 418 } 419 420 void removeGLCanvas(GLCanvas dw) 421 { 422 import std.algorithm : find; 423 auto r = mGLCanvases[].find!((a,b)=>a is b)(this); 424 mGLCanvases.linearRemove(r); 425 } 426 427 protected: 428 import std.container.array : Array; 429 430 // should the cursor be visible now 431 bool mBlinkingCursorVisible; 432 // the moment in time when the cursor has changed its blinking visibility 433 long mBlinkingCursorTimestamp; 434 Vector2i mMousePos; 435 int mModifiers; 436 MouseButton mMouseState; 437 long mLastInteraction; 438 Array!Widget mFocusPath; 439 bool mDragActive; 440 Widget mDragWidget; 441 bool mNeedToDraw, mNeedToPerfomLayout; 442 long mTimestamp; 443 bool mTooltipShown; 444 Cursor mCursor; 445 float mPixelRatio; 446 void delegate(Vector2i) mResizeCallback; 447 Array!GLCanvas mGLCanvases; 448 }