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);