1 /// 2 module nanogui.window; 3 4 /* 5 nanogui/window.d -- Top-level window widget 6 7 NanoGUI was developed by Wenzel Jakob <wenzel.jakob@epfl.ch>. 8 The widget drawing code is based on the NanoVG demo application 9 by Mikko Mononen. 10 11 All rights reserved. Use of this source code is governed by a 12 BSD-style license that can be found in the LICENSE.txt file. 13 */ 14 15 import nanogui.widget; 16 import nanogui.common : Vector2i, Vector2f, MouseButton; 17 18 /** 19 * Top-level window widget. 20 */ 21 class Window : Widget 22 { 23 public: 24 this(Widget parent, string title = "Untitled", bool resizable = false) 25 { 26 super(parent); 27 mTitle = title; 28 mButtonPanel = null; 29 mModal = false; 30 mDrag = false; 31 mResizeDir = Vector2i(); 32 import std.algorithm : max; 33 mMinSize = Vector2i(3*mTheme.mResizeAreaOffset, max(3*mTheme.mResizeAreaOffset, mTheme.mWindowHeaderHeight + mTheme.mResizeAreaOffset)); 34 mResizable = resizable; 35 } 36 37 /// Return the window title 38 final string title() const { return mTitle; } 39 /// Set the window title 40 final void title(string title) { mTitle = title; } 41 42 /// Is this a model dialog? 43 final bool modal() const { return mModal; } 44 /// Set whether or not this is a modal dialog 45 final void modal(bool modal) { mModal = modal; } 46 /// Is this a resizable window? 47 bool resizable() const { return mResizable; } 48 /// Set whether or not this window is resizable 49 void resizable(bool value) { mResizable = value; } 50 51 /// Return the panel used to house window buttons 52 final Widget buttonPanel() 53 { 54 import nanogui.layout : BoxLayout, Orientation, Alignment; 55 if (!mButtonPanel) { 56 mButtonPanel = new Widget(this); 57 mButtonPanel.layout(new BoxLayout(Orientation.Horizontal, Alignment.Middle, 0, 4)); 58 } 59 return mButtonPanel; 60 } 61 62 /// Dispose the window 63 final void dispose(); 64 65 /// Center the window in the current `Screen` 66 final void center(); 67 68 /// Draw the window 69 override void draw(ref NanoContext ctx) 70 { 71 assert (mTheme); 72 int ds = mTheme.mWindowDropShadowSize, cr = mTheme.mWindowCornerRadius; 73 int hh = mTheme.mWindowHeaderHeight; 74 75 /* Draw window */ 76 ctx.save; 77 ctx.beginPath; 78 ctx.roundedRect(mPos.x, mPos.y, mSize.x, mSize.y, cr); 79 80 ctx.fillColor(mMouseFocus ? mTheme.mWindowFillFocused 81 : mTheme.mWindowFillUnfocused); 82 ctx.fill; 83 84 85 /* Draw a drop shadow */ 86 NVGPaint shadowPaint = ctx.boxGradient( 87 mPos.x, mPos.y, mSize.x, mSize.y, cr*2, ds*2, 88 mTheme.mDropShadow, mTheme.mTransparent); 89 90 ctx.save; 91 ctx.resetScissor; 92 ctx.beginPath; 93 ctx.rect(mPos.x-ds,mPos.y-ds, mSize.x+2*ds, mSize.y+2*ds); 94 ctx.roundedRect(mPos.x, mPos.y, mSize.x, mSize.y, cr); 95 ctx.pathWinding(NVGSolidity.Hole); 96 ctx.fillPaint(shadowPaint); 97 ctx.fill; 98 ctx.restore; 99 100 if (mTitle.length) 101 { 102 /* Draw header */ 103 NVGPaint headerPaint = ctx.linearGradient( 104 mPos.x, mPos.y, mPos.x, 105 mPos.y + hh, 106 mTheme.mWindowHeaderGradientTop, 107 mTheme.mWindowHeaderGradientBot); 108 109 ctx.beginPath; 110 ctx.roundedRect(mPos.x, mPos.y, mSize.x, hh, cr); 111 112 ctx.fillPaint(headerPaint); 113 ctx.fill; 114 115 ctx.beginPath; 116 ctx.roundedRect(mPos.x, mPos.y, mSize.x, hh, cr); 117 ctx.strokeColor(mTheme.mWindowHeaderSepTop); 118 119 ctx.save; 120 ctx.intersectScissor(mPos.x, mPos.y, mSize.x, 0.5f); 121 ctx.stroke; 122 ctx.restore; 123 124 ctx.beginPath; 125 ctx.moveTo(mPos.x + 0.5f, mPos.y + hh - 1.5f); 126 ctx.lineTo(mPos.x + mSize.x - 0.5f, mPos.y + hh - 1.5); 127 ctx.strokeColor(mTheme.mWindowHeaderSepBot); 128 ctx.stroke; 129 130 ctx.fontSize(18.0f); 131 ctx.fontFace("sans-bold"); 132 auto algn = NVGTextAlign(); 133 algn.center = true; 134 algn.middle = true; 135 ctx.textAlign(algn); 136 137 ctx.fontBlur(2); 138 ctx.fillColor(mTheme.mDropShadow); 139 ctx.text(mPos.x + mSize.x / 2, 140 mPos.y + hh / 2, mTitle); 141 142 ctx.fontBlur(0); 143 ctx.fillColor(mFocused ? mTheme.mWindowTitleFocused 144 : mTheme.mWindowTitleUnfocused); 145 ctx.text(mPos.x + mSize.x / 2, mPos.y + hh / 2 - 1, 146 mTitle); 147 } 148 149 ctx.restore; 150 const old = ctx.mouse; 151 if (window.contains(ctx.mouse)) 152 ctx.mouse -= window.absolutePosition; 153 else 154 ctx.mouse = Vector2i(-1, -1); 155 scope(exit) ctx.mouse = old; 156 Widget.draw(ctx); 157 } 158 /// Handle window drag events 159 override bool mouseDragEvent(Vector2i p, Vector2i rel, MouseButton button, int modifiers) 160 { 161 import std.algorithm : min, max; 162 import gfm.math : maxByElem; 163 164 if (mDrag && (button & (1 << MouseButton.Left)) != 0) { 165 mPos += rel; 166 { 167 // mPos = mPos.cwiseMax(Vector2i::Zero()); 168 mPos[0] = max(mPos[0], 0); 169 mPos[1] = max(mPos[1], 0); 170 } 171 { 172 // mPos = mPos.cwiseMin(parent()->size() - mSize); 173 auto other = parent.size - mSize; 174 mPos[0] = min(mPos[0], other[0]); 175 mPos[1] = min(mPos[1], other[1]); 176 } 177 return true; 178 } 179 else if (mResizable && mResize && (button & (1 << MouseButton.Left)) != 0) 180 { 181 const lowerRightCorner = mPos + mSize; 182 const upperLeftCorner = mPos; 183 bool resized = false; 184 185 186 if (mResizeDir.x == 1) { 187 if ((rel.x > 0 && p.x >= lowerRightCorner.x) || (rel.x < 0)) { 188 mSize.x += rel.x; 189 resized = true; 190 } 191 } else if (mResizeDir.x == -1) { 192 if ((rel.x < 0 && p.x <= upperLeftCorner.x) || 193 (rel.x > 0)) { 194 mSize.x += -rel.x; 195 mSize = mSize.maxByElem(mMinSize); 196 mPos = lowerRightCorner - mSize; 197 resized = true; 198 } 199 } 200 201 if (mResizeDir.y == 1) { 202 if ((rel.y > 0 && p.y >= lowerRightCorner.y) || (rel.y < 0)) { 203 mSize.y += rel.y; 204 resized = true; 205 } 206 } 207 mSize = mSize.maxByElem(mMinSize); 208 if (resized) 209 screen.needToPerfomLayout = true; 210 return true; 211 } 212 return false; 213 } 214 /// Handle a mouse motion event (default implementation: propagate to children) 215 override bool mouseMotionEvent(const Vector2i p, const Vector2i rel, MouseButton button, int modifiers) 216 { 217 import nanogui.common : Cursor; 218 219 if (Widget.mouseMotionEvent(p, rel, button, modifiers)) 220 return true; 221 222 if (mResizable && mFixedSize.x == 0 && checkHorizontalResize(p) != 0) 223 { 224 mCursor = Cursor.HResize; 225 } 226 else if (mResizable && mFixedSize.y == 0 && checkVerticalResize(p) != 0) 227 { 228 mCursor = Cursor.VResize; 229 } 230 else 231 { 232 mCursor = Cursor.Arrow; 233 } 234 return false; 235 } 236 /// Handle mouse events recursively and bring the current window to the top 237 override bool mouseButtonEvent(Vector2i p, MouseButton button, bool down, int modifiers) 238 { 239 if (super.mouseButtonEvent(p, button, down, modifiers)) 240 return true; 241 if (button == MouseButton.Left) 242 { 243 mDrag = down && (p.y - mPos.y) < mTheme.mWindowHeaderHeight; 244 mResize = false; 245 if (mResizable && !mDrag && down) 246 { 247 mResizeDir.x = (mFixedSize.x == 0) ? checkHorizontalResize(p) : 0; 248 mResizeDir.y = (mFixedSize.y == 0) ? checkVerticalResize(p) : 0; 249 mResize = mResizeDir.x != 0 || mResizeDir.y != 0; 250 } 251 return true; 252 } 253 return false; 254 } 255 /// Accept scroll events and propagate them to the widget under the mouse cursor 256 override bool scrollEvent(Vector2i p, Vector2f rel) 257 { 258 Widget.scrollEvent(p, rel); 259 return true; 260 } 261 262 /// Compute the preferred size of the widget 263 override Vector2i preferredSize(NanoContext ctx) const 264 { 265 if (mResizable) 266 return mSize; 267 268 Vector2i result = Widget.preferredSize(ctx, mButtonPanel); 269 270 ctx.fontSize(18.0f); 271 ctx.fontFace("sans-bold"); 272 float[4] bounds; 273 ctx.textBounds(0, 0, mTitle, bounds); 274 275 if (result.x < bounds[2]-bounds[0] + 20) 276 result.x = cast(int) (bounds[2]-bounds[0] + 20); 277 if (result.y < bounds[3]-bounds[1]) 278 result.y = cast(int) (bounds[3]-bounds[1]); 279 280 return result; 281 } 282 /// Invoke the associated layout generator to properly place child widgets, if any 283 override void performLayout(NanoContext ctx) 284 { 285 if (!mButtonPanel) { 286 Widget.performLayout(ctx); 287 } else { 288 mButtonPanel.visible(false); 289 Widget.performLayout(ctx); 290 foreach (w; mButtonPanel.children) { 291 w.fixedSize(Vector2i(22, 22)); 292 w.fontSize(15); 293 } 294 mButtonPanel.visible(true); 295 mButtonPanel.size(Vector2i(width(), 22)); 296 mButtonPanel.position(Vector2i(width() - (mButtonPanel.preferredSize(ctx).x + 5), 3)); 297 mButtonPanel.performLayout(ctx); 298 } 299 } 300 //override void save(Serializer &s) const; 301 //override bool load(Serializer &s); 302 public: 303 /// Internal helper function to maintain nested window position values; overridden in \ref Popup 304 void refreshRelativePlacement() 305 { 306 /* Overridden in \ref Popup */ 307 } 308 protected: 309 int checkHorizontalResize(const Vector2i mousePos) 310 { 311 const offset = mTheme.mResizeAreaOffset; 312 const lowerRightCorner = absolutePosition + size; 313 const headerLowerLeftCornerY = absolutePosition.y + mTheme.mWindowHeaderHeight; 314 315 if (mousePos.y > headerLowerLeftCornerY && 316 mousePos.x <= absolutePosition.x + offset && 317 mousePos.x >= absolutePosition.x) 318 { 319 return -1; 320 } 321 else if (mousePos.y > headerLowerLeftCornerY && 322 mousePos.x >= lowerRightCorner.x - offset && 323 mousePos.x <= lowerRightCorner.x) 324 { 325 return 1; 326 } 327 328 return 0; 329 } 330 int checkVerticalResize(const Vector2i mousePos) 331 { 332 const offset = mTheme.mResizeAreaOffset; 333 const lowerRightCorner = absolutePosition + size; 334 335 // Do not check for resize area on top of the window. It is to prevent conflict drag and resize event. 336 if (mousePos.y >= lowerRightCorner.y - offset && mousePos.y <= lowerRightCorner.y) 337 { 338 return 1; 339 } 340 341 return 0; 342 } 343 344 string mTitle; 345 Widget mButtonPanel; 346 bool mModal; 347 bool mDrag; 348 bool mResize; 349 Vector2i mResizeDir; 350 Vector2i mMinSize; 351 bool mResizable; 352 }