1 /// 2 module nanogui.button; 3 /* 4 NanoGUI was developed by Wenzel Jakob <wenzel.jakob@epfl.ch>. 5 The widget drawing code is based on the NanoVG demo application 6 by Mikko Mononen. 7 8 All rights reserved. Use of this source code is governed by a 9 BSD-style license that can be found in the LICENSE.txt file. 10 */ 11 12 import std.container.array : Array; 13 import std.typecons : RefCounted; 14 15 import nanogui.widget; 16 import nanogui.common; 17 18 /** 19 * Defines the [Normal/Toggle/Radio/Popup] `nanogui.Button` widget. 20 */ 21 class Button : Widget 22 { 23 public: 24 /// Flags to specify the button behavior (can be combined with binary OR) 25 enum Flags 26 { 27 NormalButton = (1 << 0), ///< A normal Button. 28 RadioButton = (1 << 1), ///< A radio Button. 29 ToggleButton = (1 << 2), ///< A toggle Button. 30 PopupButton = (1 << 3) ///< A popup Button. 31 } 32 33 /// The available icon positions. 34 enum IconPosition 35 { 36 Left, ///< Button icon on the far left. 37 LeftCentered, ///< Button icon on the left, centered (depends on caption text length). 38 RightCentered,///< Button icon on the right, centered (depends on caption text length). 39 Right ///< Button icon on the far right. 40 } 41 42 /** 43 * Creates a button attached to the specified parent. 44 * 45 * Params: 46 * parent = The `nanogui.Widget` this Button will be attached to. 47 * caption = The name of the button (default `"Untitled"`). 48 */ 49 this(Widget parent, string caption = "Untitled") 50 { 51 super(parent); 52 mCaption = caption; 53 mIcon = 0; 54 mImage = NVGImage(); 55 mIconPosition = IconPosition.LeftCentered; 56 mPushed = false; 57 mFlags = Flags.NormalButton; 58 mBackgroundColor = Color(0, 0, 0, 0); 59 mTextColor = Color(0, 0, 0, 0); 60 } 61 62 /** 63 * Creates a button attached to the specified parent. 64 * 65 * Params: 66 * parent = The `nanogui.Widget` this Button will be attached to. 67 * caption = The name of the button (default `"Untitled"`). 68 * icon = The icon to display with this Button. See `nanogui.Button.mIcon`. 69 */ 70 this(Widget parent, string caption, dchar icon) 71 { 72 this(parent, caption); 73 mIcon = icon; 74 } 75 76 /** 77 * Creates a button attached to the specified parent. 78 * 79 * Params: 80 * parent = The `nanogui.Widget` this Button will be attached to. 81 * caption = The name of the button (default `"Untitled"`). 82 * image = The image to display with this Button. See `nanogui.Button.mImage`. 83 */ 84 this(Widget parent, string caption, ref const(NVGImage) image) 85 { 86 this(parent, caption); 87 mImage = NVGImage(image); 88 } 89 90 /// Returns the caption of this Button. 91 final string caption() const { return mCaption; } 92 93 /// Sets the caption of this Button. 94 final void caption(string caption) { mCaption = caption; } 95 96 /// Returns the background color of this Button. 97 final Color backgroundColor() const { return mBackgroundColor; } 98 99 /// Sets the background color of this Button. 100 final void backgroundColor(const Color backgroundColor) { mBackgroundColor = backgroundColor; } 101 102 /// Returns the text color of the caption of this Button. 103 final Color textColor() const { return mTextColor; } 104 105 /// Sets the text color of the caption of this Button. 106 final void textColor(const Color textColor) { mTextColor = textColor; } 107 108 /// Returns the icon of this Button. See `nanogui.Button.mIcon`. 109 final dchar icon() const { return mIcon; } 110 111 /// Sets the icon of this Button. See `nanogui.Button.mIcon`. 112 final void icon(int icon) { mIcon = icon; } 113 114 /// The current flags of this Button (see `nanogui.Button.Flags` for options). 115 final int flags() const { return mFlags; } 116 117 /// Sets the flags of this Button (see `nanogui.Button.Flags` for options). 118 final void flags(int buttonFlags) { mFlags = buttonFlags; } 119 120 /// The position of the icon for this Button. 121 final IconPosition iconPosition() const { return mIconPosition; } 122 123 /// Sets the position of the icon for this Button. 124 final void iconPosition(IconPosition iconPosition) { mIconPosition = iconPosition; } 125 126 /// Whether or not this Button is currently pushed. 127 final bool pushed() const { return mPushed; } 128 129 /// Sets whether or not this Button is currently pushed. 130 final void pushed(bool pushed) { mPushed = pushed; } 131 132 /// The current callback to execute (for any type of button). 133 final void delegate() callback() const { return mCallback; } 134 135 /// Set the push callback (for any type of button). 136 final void callback(void delegate() callback) { mCallback = callback; } 137 138 /// The current callback to execute (for toggle buttons). 139 final void delegate(bool) changeCallback() const { return mChangeCallback; } 140 141 /// Set the change callback (for toggle buttons). 142 final void changeCallback(void delegate(bool) callback) { mChangeCallback = callback; } 143 144 /// Set the button group (for radio buttons). 145 final void buttonGroup(ButtonGroup buttonGroup) { mButtonGroup = buttonGroup; } 146 147 /// The current button group (for radio buttons). 148 final buttonGroup() { return mButtonGroup; } 149 150 /// The preferred size of this Button. 151 override Vector2i preferredSize(NanoContext ctx) const 152 { 153 int fontSize = mFontSize == -1 ? mTheme.mButtonFontSize : mFontSize; 154 ctx.fontSize(fontSize); 155 ctx.fontFace("sans-bold"); 156 const tw = ctx.textBounds(0,0, mCaption, null); 157 float iw = 0.0f, ih = fontSize; 158 159 if (mIcon) 160 { 161 ih *= icon_scale(); 162 ctx.fontFace("icons"); 163 ctx.fontSize(ih); 164 iw = ctx.textBounds(0, 0, [mIcon], null) 165 + mSize.y * 0.15f; 166 } 167 else if (mImage.valid) 168 { 169 int w, h; 170 ih *= 0.9f; 171 ctx.imageSize(mImage, w, h); 172 iw = w * ih / h; 173 } 174 return Vector2i(cast(int)(tw + iw) + 20, fontSize + 10); 175 } 176 177 /// The callback that is called when any type of mouse button event is issued to this Button. 178 override bool mouseButtonEvent(Vector2i p, MouseButton button, bool down, int modifiers) 179 { 180 Widget.mouseButtonEvent(p, button, down, modifiers); 181 /* Temporarily increase the reference count of the button in case the 182 button causes the parent window to be destructed */ 183 auto self = this; 184 185 if (button == MouseButton.Left && mEnabled) 186 { 187 bool pushedBackup = mPushed; 188 if (down) 189 { 190 if (mFlags & Flags.RadioButton) 191 { 192 if (mButtonGroup.empty) 193 { 194 foreach (widget; parent.children) 195 { 196 auto b = cast(Button) widget; 197 if (b != this && b && (b.flags & Flags.RadioButton) && b.mPushed) 198 { 199 b.mPushed = false; 200 if (b.mChangeCallback) 201 b.mChangeCallback(false); 202 } 203 } 204 } else { 205 foreach (b; mButtonGroup) 206 { 207 if (b != this && (b.flags & Flags.RadioButton) && b.mPushed) 208 { 209 b.mPushed = false; 210 if (b.mChangeCallback) 211 b.mChangeCallback(false); 212 } 213 } 214 } 215 } 216 if (mFlags & Flags.PopupButton) 217 { 218 foreach (widget; parent.children) 219 { 220 auto b = cast(Button) widget; 221 if (b != this && b && (b.flags & Flags.PopupButton) && b.mPushed) 222 { 223 b.mPushed = false; 224 if (b.mChangeCallback) 225 b.mChangeCallback(false); 226 } 227 } 228 } 229 if (mFlags & Flags.ToggleButton) 230 mPushed = !mPushed; 231 else 232 mPushed = true; 233 } else if (mPushed) 234 { 235 if (contains(p) && mCallback) 236 mCallback(); 237 if (mFlags & Flags.NormalButton) 238 mPushed = false; 239 } 240 if (pushedBackup != mPushed && mChangeCallback) 241 mChangeCallback(mPushed); 242 243 return true; 244 } 245 return false; 246 } 247 248 /// Responsible for drawing the Button. 249 override void draw(ref NanoContext ctx) 250 { 251 super.draw(ctx); 252 253 auto gradTop = mTheme.mButtonGradientTopUnfocused; 254 auto gradBot = mTheme.mButtonGradientBotUnfocused; 255 256 if (mPushed) 257 { 258 gradTop = mTheme.mButtonGradientTopPushed; 259 gradBot = mTheme.mButtonGradientBotPushed; 260 } 261 else if (mMouseFocus && mEnabled) 262 { 263 gradTop = mTheme.mButtonGradientTopFocused; 264 gradBot = mTheme.mButtonGradientBotFocused; 265 } 266 267 ctx.beginPath; 268 269 ctx.roundedRect(mPos.x + 1, mPos.y + 1.0f, mSize.x - 2, 270 mSize.y - 2, mTheme.mButtonCornerRadius - 1); 271 272 if (mBackgroundColor.w != 0) 273 { 274 ctx.fillColor(Color(mBackgroundColor.rgb, 1.0f)); 275 ctx.fill; 276 if (mPushed) 277 { 278 gradTop.a = gradBot.a = 0.8f; 279 } 280 else 281 { 282 const v = 1 - mBackgroundColor.w; 283 gradTop.a = gradBot.a = mEnabled ? v : v * .5f + .5f; 284 } 285 } 286 287 NVGPaint bg = ctx.linearGradient(mPos.x, mPos.y, mPos.x, 288 mPos.y + mSize.y, gradTop, gradBot); 289 290 ctx.fillPaint(bg); 291 ctx.fill; 292 293 ctx.beginPath; 294 ctx.strokeWidth(1.0f); 295 ctx.roundedRect(mPos.x + 0.5f, mPos.y + (mPushed ? 0.5f : 1.5f), mSize.x - 1, 296 mSize.y - 1 - (mPushed ? 0.0f : 1.0f), mTheme.mButtonCornerRadius); 297 ctx.strokeColor(mTheme.mBorderLight); 298 ctx.stroke; 299 300 ctx.beginPath; 301 ctx.roundedRect(mPos.x + 0.5f, mPos.y + 0.5f, mSize.x - 1, 302 mSize.y - 2, mTheme.mButtonCornerRadius); 303 ctx.strokeColor(mTheme.mBorderDark); 304 ctx.stroke; 305 306 int fontSize = mFontSize == -1 ? mTheme.mButtonFontSize : mFontSize; 307 ctx.fontSize(fontSize); 308 ctx.fontFace("sans-bold"); 309 const tw = ctx.textBounds(0,0, mCaption, null); 310 311 Vector2f center = mPos + cast(Vector2f) mSize * 0.5f; 312 auto textPos = Vector2f(center.x - tw * 0.5f, center.y - 1); 313 auto textColor = 314 mTextColor.w == 0 ? mTheme.mTextColor : mTextColor; 315 if (!mEnabled) 316 textColor = mTheme.mDisabledTextColor; 317 318 float iw, ih; 319 float d = (mPushed ? 1.0f : 0.0f); 320 if (mIcon) 321 { 322 ih = fontSize*icon_scale; 323 ctx.fontSize(ih); 324 ctx.fontFace("icons"); 325 iw = ctx.textBounds(0, 0, [mIcon], null); 326 } else if (mImage.valid) 327 { 328 int w, h; 329 ctx.imageSize(mImage, w, h); 330 import std.algorithm : min; 331 ih = min(h*0.9f, height); 332 iw = w * ih / h; 333 } 334 import std.math : isNaN; 335 if (!iw.isNaN) 336 { 337 if (mCaption != "") 338 iw += mSize.y * 0.15f; 339 ctx.fillColor(textColor); 340 NVGTextAlign algn; 341 algn.left = true; 342 algn.middle = true; 343 ctx.textAlign(algn); 344 Vector2f iconPos = center; 345 iconPos.y -= 1; 346 347 if (mIconPosition == IconPosition.LeftCentered) 348 { 349 iconPos.x -= (tw + iw) * 0.5f; 350 textPos.x += iw * 0.5f; 351 } 352 else if (mIconPosition == IconPosition.RightCentered) 353 { 354 textPos.x -= iw * 0.5f; 355 iconPos.x += tw * 0.5f; 356 } 357 else if (mIconPosition == IconPosition.Left) 358 { 359 iconPos.x = mPos.x + 8; 360 } 361 else if (mIconPosition == IconPosition.Right) 362 { 363 iconPos.x = mPos.x + mSize.x - iw - 8; 364 } 365 366 if (mIcon) 367 { 368 ctx.text(iconPos.x, iconPos.y + d + 1, [mIcon]); 369 } 370 else 371 { 372 NVGPaint imgPaint = ctx.imagePattern( 373 iconPos.x, iconPos.y + d - ih/2, iw, ih, 0, mImage, mEnabled ? 0.5f : 0.25f); 374 375 ctx.fillPaint(imgPaint); 376 ctx.fill; 377 } 378 } 379 380 ctx.fontSize(fontSize); 381 ctx.fontFace("sans-bold"); 382 NVGTextAlign algn; 383 algn.left = true; 384 algn.middle = true; 385 ctx.textAlign(algn); 386 ctx.fillColor(mTheme.mTextColorShadow); 387 ctx.text(textPos.x, textPos.y + d, mCaption,); 388 ctx.fillColor(textColor); 389 ctx.text(textPos.x, textPos.y + d + 1, mCaption); 390 } 391 392 // // Saves the state of this Button provided the given Serializer. 393 //override void save(Serializer &s) const; 394 395 // // Sets the state of this Button provided the given Serializer. 396 //override bool load(Serializer &s); 397 398 protected: 399 /// The caption of this Button. 400 string mCaption; 401 402 /// The icon to display with this Button (`0` means icon is represented by mImage). 403 dchar mIcon; 404 /// The icon to display with this Button (it's used if mIcon is `0` and mImage.valid 405 /// returns `true`). 406 NVGImage mImage; 407 408 /// The position to draw the icon at. 409 IconPosition mIconPosition; 410 411 /// Whether or not this Button is currently pushed. 412 bool mPushed; 413 414 /// The current flags of this button (see `nanogui.Button.Flags` for options). 415 int mFlags; 416 417 /// The background color of this Button. 418 Color mBackgroundColor; 419 420 /// The color of the caption text of this Button. 421 Color mTextColor; 422 423 /// The callback issued for all types of buttons. 424 void delegate() mCallback; 425 426 /// The callback issued for toggle buttons. 427 void delegate(bool) mChangeCallback; 428 429 /// The button group for radio buttons. 430 ButtonGroup mButtonGroup; 431 } 432 433 alias ButtonGroup = RefCounted!(Array!Button);