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