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