1 /* 2 nanogui.widget -- Base class of all widgets 3 4 NanoGUI was developed by Wenzel Jakob <wenzel.jakob@epfl.ch> and 5 ported to D by Alexandr Druzhinin <drug2004@bk.ru> 6 The widget drawing code is based on the NanoVG demo application 7 by Mikko Mononen. 8 9 All rights reserved. Use of this source code is governed by a 10 BSD-style license that can be found in the LICENSE.txt file. 11 */ 12 /// 13 module nanogui.widget; 14 15 import std.container.array; 16 17 import nanogui.theme; 18 import nanogui.layout; 19 import nanogui.common : Cursor, Vector2i, Vector2f, MouseButton, KeyAction; 20 import nanogui.screen : Screen; 21 import nanogui.window : Window; 22 public import nanogui.common; 23 24 /** 25 * Base class of all widgets. 26 * 27 * Widget is the base class of all widgets in nanogui. It can 28 * also be used as an panel to arrange an arbitrary number of child 29 * widgets using a layout generator (see `nanogui.layout.Layout`). 30 */ 31 class Widget 32 { 33 public: 34 /// Construct a new widget with the given parent widget 35 this(Widget parent) 36 { 37 mVisible = true; 38 mEnabled = true; 39 mTooltip = ""; 40 mFontSize = -1; 41 mIconExtraScale = 1.0f; 42 mCursor = Cursor.Arrow; 43 if (parent) 44 parent.addChild(this); 45 } 46 47 /// Return the parent widget 48 final Widget parent() { return mParent; } 49 /// Return the parent widget 50 auto parent() const { return mParent; } 51 /// Set the parent widget 52 final void parent(Widget parent) { mParent = parent; } 53 54 /// Return the used `nanogui.layout.Layout` generator 55 final Layout layout() { return mLayout; } 56 /// Return the used `nanogui.layout.Layout` generator 57 auto layout() const { return mLayout; } 58 /// Set the used `nanogui.layout.Layout` generator 59 final void layout(Layout layout) { mLayout = layout; } 60 61 /// Return the `nanogui.theme.Theme` used to draw this widget 62 final const(Theme) theme() const { return mTheme; } 63 /// Set the `nanogui.theme.Theme` used to draw this widget 64 void theme(Theme theme) 65 { 66 if (mTheme is theme) 67 return; 68 mTheme = theme; 69 foreach(child; mChildren) 70 child.theme = theme; 71 } 72 73 /// Return the position relative to the parent widget 74 final Vector2i position() const { return mPos; } 75 /// Set the position relative to the parent widget 76 final void position(Vector2i pos) { mPos = pos; } 77 78 /// Return the absolute position on screen 79 final Vector2i absolutePosition() const 80 { 81 return mParent ? 82 (parent.absolutePosition + mPos) : mPos; 83 } 84 85 /// Return the size of the widget 86 final Vector2i size() const { return mSize; } 87 /// set the size of the widget 88 final void size(Vector2i size) { mSize = size; } 89 90 /// Return the width of the widget 91 final int width() const { return mSize.x; } 92 /// Set the width of the widget 93 final void width(int width) { mSize.x = width; } 94 95 /// Return the height of the widget 96 final int height() const { return mSize.y; } 97 /// Set the height of the widget 98 final void height(int height) { mSize.y = height; } 99 100 /** 101 * Set the fixed size of this widget 102 * 103 * If nonzero, components of the fixed size attribute override any values 104 * computed by a layout generator associated with this widget. Note that 105 * just setting the fixed size alone is not enough to actually change its 106 * size; this is done with a call to `nanogui.widget.Widget.size` or a call to `nanogui.widget.Widget.performLayout` 107 * in the parent widget. 108 */ 109 final void fixedSize(Vector2i fixedSize) { mFixedSize = fixedSize; } 110 111 /// Return the fixed size 112 final Vector2i fixedSize() const { return mFixedSize; } 113 114 /// Return the fixed width (see `fixedSize`) 115 final int fixedWidth() const { return mFixedSize.x; } 116 /// Return the fixed height (see `fixedSize`) 117 final int fixedHeight() const { return mFixedSize.y; } 118 /// Set the fixed width (see `fixedSize`) 119 final void fixedWidth(int width) { mFixedSize.x = width; } 120 /// Set the fixed height (see `fixedSize`) 121 final void fixedHeight(int height) { mFixedSize.y = height; } 122 123 /// Return whether or not the widget is currently visible (assuming all parents are visible) 124 final bool visible() const { return mVisible; } 125 /// Set whether or not the widget is currently visible (assuming all parents are visible) 126 final void visible(bool visible) { mVisible = visible; } 127 128 /// Check if this widget is currently visible, taking parent widgets into account 129 final bool visibleRecursive() const { 130 import std.typecons : Rebindable; 131 bool visible = true; 132 Rebindable!(const Widget) widget = this; 133 while (widget) { 134 visible &= widget.visible; 135 widget = widget.parent; 136 } 137 return visible; 138 } 139 140 /// Return the number of child widgets 141 final int childCount() const 142 { 143 import std.conv : castFrom; 144 return castFrom!size_t.to!int(mChildren.length); 145 } 146 147 /// Return the list of child widgets of the current widget 148 auto children() { return mChildren; } 149 /// ditto 150 auto children() const { return mChildren; } 151 152 /** 153 * Add a child widget to the current widget at 154 * the specified index. 155 * 156 * This function almost never needs to be called by hand, 157 * since the constructor of `Widget` automatically 158 * adds the current widget to its parent 159 */ 160 void addChild(int index, Widget widget) 161 { 162 assert(index <= childCount); 163 mChildren.insertBefore(mChildren[index..$], widget); 164 widget.parent = this; 165 widget.theme = mTheme; 166 } 167 168 /// Convenience function which appends a widget at the end 169 void addChild(Widget widget) 170 { 171 addChild(childCount(), widget); 172 } 173 174 /// Remove a child widget by index 175 void removeChild(int index) 176 { 177 import std.range : takeOne; 178 mChildren.linearRemove(mChildren[index..$].takeOne); 179 } 180 181 /// Remove a child widget by value 182 void removeChild(Widget widget) 183 { 184 import std.algorithm : find; 185 import std.range : takeOne; 186 mChildren.linearRemove(mChildren[].find(widget).takeOne); 187 } 188 189 /// Retrieves the child at the specific position 190 auto childAt(int index) const { return mChildren[index]; } 191 192 /// Retrieves the child at the specific position 193 auto childAt(int index) { return mChildren[index]; } 194 195 /// Returns the index of a specific child or -1 if not found 196 int childIndex(Widget widget) const 197 { 198 import std.algorithm : countUntil; 199 return cast(int) countUntil!(a=>a is widget)(mChildren[]); 200 } 201 202 /// Variadic shorthand notation to construct and add a child widget 203 auto add(W, Args...)(Args args) 204 { 205 return new WidgetClass(this, args); 206 } 207 208 /// Walk up the hierarchy and return the parent window 209 final Window window() 210 { 211 Widget widget = this; 212 while (true) { 213 if (!widget) 214 throw new Exception( 215 "Widget:internal error (could not find parent window)"); 216 Window window = cast(Window)(widget); 217 if (window) 218 return window; 219 widget = widget.parent; 220 } 221 } 222 223 /// Walk up the hierarchy and return the parent screen 224 final Screen screen() 225 { 226 auto widget = this; 227 while (true) { 228 if (!widget) 229 throw new Exception( 230 "Widget:internal error (could not find parent screen)"); 231 auto screen = cast(Screen) widget; 232 if (screen) 233 return screen; 234 widget = widget.parent; 235 } 236 } 237 238 /// Associate this widget with an ID value (optional) 239 void setId(string id) { mId = id; } 240 /// Return the ID value associated with this widget, if any 241 auto id() const { return mId; } 242 243 /// Return whether or not this widget is currently enabled 244 final bool enabled() const { return mEnabled; } 245 /// Set whether or not this widget is currently enabled 246 final void enabled(bool enabled) { mEnabled = enabled; } 247 248 /// Return whether or not this widget is currently focused 249 bool focused() const { return mFocused; } 250 /// Set whether or not this widget is currently focused 251 void focused(bool focused) { mFocused = focused; } 252 /// Request the focus to be moved to this widget 253 void requestFocus() 254 { 255 import nanogui.screen : Screen; 256 Widget widget = this; 257 while (widget.parent()) 258 widget = widget.parent(); 259 if (auto screen = cast(Screen) widget) 260 screen.updateFocus(this); 261 else 262 throw new Exception("Invariant violation"); 263 } 264 265 string tooltip() const { return mTooltip; } 266 void tooltip(string tooltip) { mTooltip = tooltip; } 267 268 /// Return current font size. If not set the default of the current theme will be returned 269 final int fontSize() const 270 { 271 return (mFontSize < 0 && mTheme) ? mTheme.mStandardFontSize : mFontSize; 272 } 273 /// Set the font size of this widget 274 final void fontSize(int fontSize) { mFontSize = fontSize; } 275 /// Return whether the font size is explicitly specified for this widget 276 final bool hasFontSize() const { return mFontSize > 0; } 277 278 /** 279 * The amount of extra scaling applied to *icon* fonts. 280 * See `nanogui.Widget.mIconExtraScale`. 281 */ 282 float iconExtraScale() const { return mIconExtraScale; } 283 284 /** 285 * Sets the amount of extra scaling applied to *icon* fonts. 286 * See `nanogui.Widget.mIconExtraScale`. 287 */ 288 void iconExtraScale(float scale) { mIconExtraScale = scale; } 289 290 /// Return a pointer to the cursor of the widget 291 Cursor cursor() const { return mCursor; } 292 /// Set the cursor of the widget 293 void cursor(Cursor value) { mCursor = value; } 294 295 /// Check if the widget contains a certain position 296 final bool contains(Vector2i p) const { 297 import std.algorithm : all; 298 // the widget contains a position if it more than 299 // the widget position and less than widget position 300 // + widget size 301 auto d = (p-mPos); 302 return d[].all!"a>=0" && (d-mSize)[].all!"a<=0"; 303 } 304 305 /// ditto 306 final bool contains(Vector2f p) const { 307 import std.algorithm : all; 308 // the widget contains a position if it more than 309 // the widget position and less than widget position 310 // + widget size 311 auto d = (p-mPos); 312 return d[].all!"a>=0" && (d-mSize)[].all!"a<=0"; 313 } 314 315 /// Determine the widget located at the given position value (recursive) 316 Widget findWidget(Vector2i p) 317 { 318 foreach_reverse(child; mChildren) 319 { 320 if (child.visible() && child.contains(p - mPos)) 321 return child.findWidget(p - mPos); 322 } 323 return contains(p) ? this : null; 324 } 325 326 /// Handle a mouse button event (default implementation: propagate to children) 327 bool mouseButtonEvent(Vector2i p, MouseButton button, bool down, int modifiers) 328 { 329 foreach_reverse(ch; mChildren) 330 { 331 Widget child = ch; 332 if (child.visible && child.contains(p - mPos) && 333 child.mouseButtonEvent(p - mPos, button, down, modifiers)) 334 return true; 335 } 336 if (button == MouseButton.Left && down && !mFocused) 337 requestFocus(); 338 return false; 339 } 340 341 /// Handle a mouse motion event (default implementation: propagate to children) 342 bool mouseMotionEvent(Vector2i p, Vector2i rel, MouseButton button, int modifiers) 343 { 344 foreach_reverse(it; mChildren) 345 { 346 Widget child = it; 347 if (!child.visible) 348 continue; 349 const contained = child.contains(p - mPos); 350 const prevContained = child.contains(p - mPos - rel); 351 if (contained != prevContained) 352 child.mouseEnterEvent(p, contained); 353 if ((contained || prevContained) && 354 child.mouseMotionEvent(p - mPos, rel, button, modifiers)) 355 return true; 356 } 357 return false; 358 } 359 360 /// Handle a mouse drag event (default implementation: do nothing) 361 bool mouseDragEvent(Vector2i p, Vector2i rel, MouseButton button, int modifiers) 362 { 363 return false; 364 } 365 366 /// Handle a mouse enter/leave event (default implementation: record this fact, but do nothing) 367 bool mouseEnterEvent(Vector2i p, bool enter) 368 { 369 mMouseFocus = enter; 370 return false; 371 } 372 373 /// Handle a mouse scroll event (default implementation: propagate to children) 374 bool scrollEvent(Vector2i p, Vector2f rel) 375 { 376 foreach_reverse (child; mChildren) 377 { 378 if (!child.visible) 379 continue; 380 if (child.contains(p - mPos) && child.scrollEvent(p - mPos, rel)) 381 return true; 382 } 383 return false; 384 } 385 386 /// Handle a focus change event (default implementation: record the focus status, but do nothing) 387 bool focusEvent(bool focused) 388 { 389 mFocused = focused; 390 return false; 391 } 392 393 /// Handle a keyboard event (default implementation: do nothing) 394 bool keyboardEvent(int key, int scancode, KeyAction action, int modifiers) 395 { 396 return false; 397 } 398 399 /// Handle text input (UTF-32 format) (default implementation: do nothing) 400 bool keyboardCharacterEvent(dchar codepoint) 401 { 402 return false; 403 } 404 405 /// Compute the preferred size of the widget 406 Vector2i preferredSize(NanoContext ctx) const 407 { 408 if (mLayout) 409 return mLayout.preferredSize(ctx, this); 410 else 411 return mSize; 412 } 413 414 /// Compute the preferred size of the widget considering its child except 415 /// skipped one (for example button panel of window) 416 final Vector2i preferredSize(NanoContext ctx, const Widget skipped) const 417 { 418 if (mLayout) 419 return mLayout.preferredSize(ctx, this, skipped); 420 else 421 return mSize; 422 } 423 424 /// Invoke the associated layout generator to properly place child widgets, if any 425 void performLayout(NanoContext ctx) 426 { 427 if (mLayout) { 428 mLayout.performLayout(ctx, this); 429 } else { 430 foreach(c; mChildren) { 431 Vector2i pref = c.preferredSize(ctx), fix = c.fixedSize(); 432 c.size(Vector2i( 433 fix[0] ? fix[0] : pref[0], 434 fix[1] ? fix[1] : pref[1] 435 )); 436 c.performLayout(ctx); 437 } 438 } 439 } 440 441 /// Draw the widget (and all child widgets) 442 void draw(ref NanoContext ctx) 443 { 444 version(NANOGUI_SHOW_WIDGET_BOUNDS) 445 { 446 ctx.strokeWidth(1.0f); 447 ctx.beginPath; 448 ctx.rect(mPos.x + 1.0f, mPos.y + 0.0f, mSize.x - 1, mSize.y - 1); 449 ctx.strokeColor(Color(255, 0, 0, 255)); 450 ctx.stroke; 451 } 452 453 if (mChildren.length == 0) 454 return; 455 456 ctx.save; 457 ctx.translate(mPos.x, mPos.y); 458 foreach(child; mChildren) 459 { 460 if (child.visible) 461 { 462 ctx.save; 463 scope(exit) ctx.restore; 464 ctx.intersectScissor(child.mPos.x, child.mPos.y, child.mSize.x, child.mSize.y); 465 child.draw(ctx); 466 } 467 } 468 ctx.restore; 469 } 470 471 // // Save the state of the widget into the given \ref Serializer instance 472 //virtual void save(Serializer &s) const; 473 474 // // Restore the state of the widget from the given \ref Serializer instance 475 //virtual bool load(Serializer &s); 476 477 protected: 478 /// Free all resources used by the widget and any children 479 ~this() 480 { 481 //foreach(child; mChildren) { 482 // if (child) 483 // child.decRef(); 484 //} 485 } 486 487 /** 488 * Convenience definition for subclasses to get the full icon scale for this 489 * class of Widget. It simple returns the value 490 * `mTheme.mIconScale * this.mIconExtraScale`. 491 * 492 * See_also: 493 * `Theme.mIconScale` and `Widget.mIconExtraScale`. This tiered scaling 494 * strategy may not be appropriate with fonts other than `entypo.ttf`. 495 */ 496 pragma(inline, true) 497 float icon_scale() const { return mTheme.mIconScale * mIconExtraScale; } 498 499 Widget mParent; 500 Theme mTheme; 501 Layout mLayout; 502 string mId; 503 Vector2i mPos, mSize, mFixedSize; 504 Array!Widget mChildren; 505 506 /** 507 * Whether or not this Widget is currently visible. When a Widget is not 508 * currently visible, no time is wasted executing its drawing method. 509 */ 510 bool mVisible; 511 512 /** 513 * Whether or not this Widget is currently enabled. Various different kinds 514 * of derived types use this to determine whether or not user input will be 515 * accepted. For example, when ``mEnabled == false``, the state of a 516 * CheckBox cannot be changed, or a TextBox will not allow new input. 517 */ 518 bool mEnabled; 519 bool mFocused, mMouseFocus; 520 string mTooltip; 521 int mFontSize; 522 523 /** 524 * The amount of extra icon scaling used in addition the the theme's 525 * default icon font scale. Default value is ``1.0``, which implies 526 * that `icon_scale` simply returns the value of `nanogui.Theme.mIconScale`. 527 * 528 * Most widgets do not need extra scaling, but some (e.g., `CheckBox`, `TextBox`) 529 * need to adjust the Theme's default icon scaling 530 * `nanogui.Theme.mIconScale` to properly display icons within their 531 * bounds (upscale, or downscale). 532 * 533 * Summary: 534 * 535 * When using `nvgFontSize` for icons in subclasses, make sure to call 536 * the `icon_scale` function. Expected usage when *drawing* icon fonts 537 * is something like: 538 * 539 * --- 540 * 541 * void draw(NanoContext ctx) 542 * { 543 * // fontSize depends on the kind of `Widget`. Search for `FontSize` 544 * // in the `Theme` class (e.g., standard vs button) 545 * float ih = fontSize; 546 * // assuming your Widget has a declared `mIcon` 547 * if (isFontIcon(mIcon)) { 548 * ih *= icon_scale(); 549 * ctx.fontFace("icons"); 550 * ctx.fontSize(ih); 551 * /// remaining drawing code (see button.d for more) 552 * } 553 * } 554 * --- 555 */ 556 float mIconExtraScale; 557 Cursor mCursor; 558 }