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