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 237 /// Handle mouse events recursively and bring the current window to the top 238 override bool mouseButtonEvent(Vector2i p, MouseButton button, bool down, int modifiers) 239 { 240 if (super.mouseButtonEvent(p, button, down, modifiers)) 241 return true; 242 243 if (button == MouseButton.Left) 244 { 245 mDrag = down && (p.y - mPos.y) < mTheme.mWindowHeaderHeight; 246 mResize = false; 247 if (mResizable && !mDrag && down) 248 { 249 mResizeDir.x = (mFixedSize.x == 0) ? checkHorizontalResize(p) : 0; 250 mResizeDir.y = (mFixedSize.y == 0) ? checkVerticalResize(p) : 0; 251 mResize = mResizeDir.x != 0 || mResizeDir.y != 0; 252 } 253 return true; 254 } 255 return false; 256 } 257 /// Accept scroll events and propagate them to the widget under the mouse cursor 258 override bool scrollEvent(Vector2i p, Vector2f rel) 259 { 260 Widget.scrollEvent(p, rel); 261 return true; 262 } 263 264 /// Compute the preferred size of the widget 265 override Vector2i preferredSize(NanoContext ctx) const 266 { 267 if (mResizable) 268 return mSize; 269 270 Vector2i result = Widget.preferredSize(ctx, mButtonPanel); 271 272 ctx.fontSize(18.0f); 273 ctx.fontFace("sans-bold"); 274 float[4] bounds; 275 ctx.textBounds(0, 0, mTitle, bounds); 276 277 if (result.x < bounds[2]-bounds[0] + 20) 278 result.x = cast(int) (bounds[2]-bounds[0] + 20); 279 if (result.y < bounds[3]-bounds[1]) 280 result.y = cast(int) (bounds[3]-bounds[1]); 281 282 return result; 283 } 284 /// Invoke the associated layout generator to properly place child widgets, if any 285 override void performLayout(NanoContext ctx) 286 { 287 if (!mButtonPanel) { 288 Widget.performLayout(ctx); 289 } else { 290 mButtonPanel.visible(false); 291 Widget.performLayout(ctx); 292 foreach (w; mButtonPanel.children) { 293 w.fixedSize(Vector2i(22, 22)); 294 w.fontSize(15); 295 } 296 mButtonPanel.visible(true); 297 mButtonPanel.size(Vector2i(width(), 22)); 298 mButtonPanel.position(Vector2i(width() - (mButtonPanel.preferredSize(ctx).x + 5), 3)); 299 mButtonPanel.performLayout(ctx); 300 } 301 } 302 //override void save(Serializer &s) const; 303 //override bool load(Serializer &s); 304 public: 305 /// Internal helper function to maintain nested window position values; overridden in \ref Popup 306 void refreshRelativePlacement() 307 { 308 /* Overridden in \ref Popup */ 309 } 310 protected: 311 int checkHorizontalResize(const Vector2i mousePos) 312 { 313 const offset = mTheme.mResizeAreaOffset; 314 const lowerRightCorner = absolutePosition + size; 315 const headerLowerLeftCornerY = absolutePosition.y + mTheme.mWindowHeaderHeight; 316 317 if (mousePos.y > headerLowerLeftCornerY && 318 mousePos.x <= absolutePosition.x + offset && 319 mousePos.x >= absolutePosition.x) 320 { 321 return -1; 322 } 323 else if (mousePos.y > headerLowerLeftCornerY && 324 mousePos.x >= lowerRightCorner.x - offset && 325 mousePos.x <= lowerRightCorner.x) 326 { 327 return 1; 328 } 329 330 return 0; 331 } 332 int checkVerticalResize(const Vector2i mousePos) 333 { 334 const offset = mTheme.mResizeAreaOffset; 335 const lowerRightCorner = absolutePosition + size; 336 337 // Do not check for resize area on top of the window. It is to prevent conflict drag and resize event. 338 if (mousePos.y >= lowerRightCorner.y - offset && mousePos.y <= lowerRightCorner.y) 339 { 340 return 1; 341 } 342 343 return 0; 344 } 345 346 string mTitle; 347 Widget mButtonPanel; 348 bool mModal; 349 bool mDrag; 350 bool mResize; 351 Vector2i mResizeDir; 352 Vector2i mMinSize; 353 bool mResizable; 354 }