1 /*
2  *  Buttons.c
3  *  Brogue
4  *
5  *  Created by Brian Walker on 11/18/11.
6  *  Copyright 2012. All rights reserved.
7  *
8  *  This file is part of Brogue.
9  *
10  *  This program is free software: you can redistribute it and/or modify
11  *  it under the terms of the GNU Affero General Public License as
12  *  published by the Free Software Foundation, either version 3 of the
13  *  License, or (at your option) any later version.
14  *
15  *  This program is distributed in the hope that it will be useful,
16  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
17  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18  *  GNU Affero General Public License for more details.
19  *
20  *  You should have received a copy of the GNU Affero General Public License
21  *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
22  */
23 
24 #include "Rogue.h"
25 #include "IncludeGlobals.h"
26 #include <math.h>
27 #include <time.h>
28 
29 // Draws the smooth gradient that appears on a button when you hover over or depress it.
30 // Returns the percentage by which the current tile should be averaged toward a hilite color.
smoothHiliteGradient(const short currentXValue,const short maxXValue)31 short smoothHiliteGradient(const short currentXValue, const short maxXValue) {
32     return (short) (100 * sin(3.14159265 * currentXValue / maxXValue));
33 }
34 
35 // Draws the button to the screen, or to a display buffer if one is given.
36 // Button back color fades from -50% intensity at the edges to the back color in the middle.
37 // Text is white, but can use color escapes.
38 //      Hovering highlight augments fore and back colors with buttonHoverColor by 20%.
39 //      Pressed darkens the middle color (or turns it the hover color if the button is black).
drawButton(brogueButton * button,enum buttonDrawStates highlight,cellDisplayBuffer dbuf[COLS][ROWS])40 void drawButton(brogueButton *button, enum buttonDrawStates highlight, cellDisplayBuffer dbuf[COLS][ROWS]) {
41     if (!(button->flags & B_DRAW)) {
42         return;
43     }
44     //assureCosmeticRNG;
45     short oldRNG = rogue.RNG;
46     rogue.RNG = RNG_COSMETIC;
47 
48     const int width = strLenWithoutEscapes(button->text);
49     color bColorBase = button->buttonColor;
50     color fColorBase = ((button->flags & B_ENABLED) ? white : gray);
51 
52     if (highlight == BUTTON_HOVER && (button->flags & B_HOVER_ENABLED)) {
53         //applyColorAugment(&fColorBase, &buttonHoverColor, 20);
54         //applyColorAugment(&bColorBase, &buttonHoverColor, 20);
55         applyColorAverage(&fColorBase, &buttonHoverColor, 25);
56         applyColorAverage(&bColorBase, &buttonHoverColor, 25);
57     }
58 
59     color bColorEdge  = bColorBase;
60     color bColorMid   = bColorBase;
61     applyColorAverage(&bColorEdge, &black, 50);
62 
63     if (highlight == BUTTON_PRESSED) {
64         applyColorAverage(&bColorMid, &black, 75);
65         if (COLOR_DIFF(bColorMid, bColorBase) < 50) {
66             bColorMid   = bColorBase;
67             applyColorAverage(&bColorMid, &buttonHoverColor, 50);
68         }
69     }
70     color bColor = bColorMid;
71 
72     short opacity = button->opacity;
73     if (highlight == BUTTON_HOVER || highlight == BUTTON_PRESSED) {
74         opacity = 100 - ((100 - opacity) * opacity / 100); // Apply the opacity twice.
75     }
76 
77     short symbolNumber = 0;
78 
79     for (int i = 0, textLoc = 0; i < width && i + button->x < COLS; i++, textLoc++) {
80         while (button->text[textLoc] == COLOR_ESCAPE) {
81             textLoc = decodeMessageColor(button->text, textLoc, &fColorBase);
82         }
83 
84         color fColor = fColorBase;
85 
86         if (button->flags & B_GRADIENT) {
87             const int midPercent = smoothHiliteGradient(i, width - 1);
88             bColor = bColorEdge;
89             applyColorAverage(&bColor, &bColorMid, midPercent);
90         }
91 
92         if (highlight == BUTTON_PRESSED) {
93             applyColorAverage(&fColor, &bColor, 30);
94         }
95 
96         if (button->opacity < 100) {
97             applyColorAverage(&fColor, &bColor, 100 - opacity);
98         }
99 
100         bakeColor(&fColor);
101         bakeColor(&bColor);
102         separateColors(&fColor, &bColor);
103 
104         enum displayGlyph displayCharacter = button->text[textLoc];
105         if (button->text[textLoc] == '*') {
106             if (button->symbol[symbolNumber]) {
107                 displayCharacter = button->symbol[symbolNumber];
108             }
109             symbolNumber++;
110         }
111 
112         if (coordinatesAreInWindow(button->x + i, button->y)) {
113             if (dbuf) {
114                 plotCharToBuffer(displayCharacter, button->x + i, button->y, &fColor, &bColor, dbuf);
115                 dbuf[button->x + i][button->y].opacity = opacity;
116             } else {
117                 plotCharWithColor(displayCharacter, button->x + i, button->y, &fColor, &bColor);
118             }
119         }
120     }
121     restoreRNG;
122 }
123 
initializeButton(brogueButton * button)124 void initializeButton(brogueButton *button) {
125     memset((void *) button, 0, sizeof( brogueButton ));
126     button->text[0] = '\0';
127     button->flags |= (B_ENABLED | B_GRADIENT | B_HOVER_ENABLED | B_DRAW | B_KEYPRESS_HIGHLIGHT);
128     button->buttonColor = interfaceButtonColor;
129     button->opacity = 100;
130 }
131 
drawButtonsInState(buttonState * state)132 void drawButtonsInState(buttonState *state) {
133     // Draw the buttons to the dbuf:
134     for (int i=0; i < state->buttonCount; i++) {
135         if (state->buttons[i].flags & B_DRAW) {
136             drawButton(&(state->buttons[i]), BUTTON_NORMAL, state->dbuf);
137         }
138     }
139 }
140 
initializeButtonState(buttonState * state,brogueButton * buttons,short buttonCount,short winX,short winY,short winWidth,short winHeight)141 void initializeButtonState(buttonState *state,
142                            brogueButton *buttons,
143                            short buttonCount,
144                            short winX,
145                            short winY,
146                            short winWidth,
147                            short winHeight) {
148     // Initialize variables for the state struct:
149     state->buttonChosen = state->buttonFocused = state->buttonDepressed = -1;
150     state->buttonCount  = buttonCount;
151     state->winX         = winX;
152     state->winY         = winY;
153     state->winWidth     = winWidth;
154     state->winHeight    = winHeight;
155     for (int i=0; i < state->buttonCount; i++) {
156         state->buttons[i] = buttons[i];
157     }
158     copyDisplayBuffer(state->rbuf, displayBuffer);
159     clearDisplayBuffer(state->dbuf);
160 
161     drawButtonsInState(state);
162 
163     // Clear the rbuf so that it resets only those parts of the screen in which buttons are drawn in the first place:
164     for (int i=0; i<COLS; i++) {
165         for (int j=0; j<ROWS; j++) {
166             state->rbuf[i][j].opacity = (state->dbuf[i][j].opacity ? 100 : 0);
167         }
168     }
169 }
170 
171 // Processes one round of user input, and bakes the necessary graphical changes into state->dbuf.
172 // Does NOT display the buttons or revert the display afterward.
173 // Assumes that the display has already been updated (via overlayDisplayBuffer(state->dbuf, NULL))
174 // and that input has been solicited (via nextBrogueEvent(event, ___, ___, ___)).
175 // Also relies on the buttonState having been initialized with initializeButtonState() or otherwise.
176 // Returns the index of a button if one is chosen.
177 // Otherwise, returns -1. That can be if the user canceled (in which case *canceled is true),
178 // or, more commonly, if the user's input in this particular split-second round was not decisive.
processButtonInput(buttonState * state,boolean * canceled,rogueEvent * event)179 short processButtonInput(buttonState *state, boolean *canceled, rogueEvent *event) {
180     boolean buttonUsed = false;
181 
182     // Mouse event:
183     if (event->eventType == MOUSE_DOWN
184         || event->eventType == MOUSE_UP
185         || event->eventType == MOUSE_ENTERED_CELL) {
186 
187         int x = event->param1;
188         int y = event->param2;
189 
190         // Revert the button with old focus, if any.
191         if (state->buttonFocused >= 0) {
192             drawButton(&(state->buttons[state->buttonFocused]), BUTTON_NORMAL, state->dbuf);
193             state->buttonFocused = -1;
194         }
195 
196         // Find the button with new focus, if any.
197         int focusIndex;
198         for (focusIndex=0; focusIndex < state->buttonCount; focusIndex++) {
199             if ((state->buttons[focusIndex].flags & B_DRAW)
200                 && (state->buttons[focusIndex].flags & B_ENABLED)
201                 && (state->buttons[focusIndex].y == y || ((state->buttons[focusIndex].flags & B_WIDE_CLICK_AREA) && abs(state->buttons[focusIndex].y - y) <= 1))
202                 && x >= state->buttons[focusIndex].x
203                 && x < state->buttons[focusIndex].x + strLenWithoutEscapes(state->buttons[focusIndex].text)) {
204 
205                 state->buttonFocused = focusIndex;
206                 if (event->eventType == MOUSE_DOWN) {
207                     state->buttonDepressed = focusIndex; // Keeps track of which button is down at the moment. Cleared on mouseup.
208                 }
209                 break;
210             }
211         }
212         if (focusIndex == state->buttonCount) { // No focus this round.
213             state->buttonFocused = -1;
214         }
215 
216         if (state->buttonDepressed >= 0) {
217             if (state->buttonDepressed == state->buttonFocused) {
218                 drawButton(&(state->buttons[state->buttonDepressed]), BUTTON_PRESSED, state->dbuf);
219             }
220         } else if (state->buttonFocused >= 0) {
221             // If no button is depressed, then update the appearance of the button with the new focus, if any.
222             drawButton(&(state->buttons[state->buttonFocused]), BUTTON_HOVER, state->dbuf);
223         }
224 
225         // Mouseup:
226         if (event->eventType == MOUSE_UP) {
227             if (state->buttonDepressed == state->buttonFocused && state->buttonFocused >= 0) {
228                 // If a button is depressed, and the mouseup happened on that button, it has been chosen and we're done.
229                 buttonUsed = true;
230             } else {
231                 // Otherwise, no button is depressed. If one was previously depressed, redraw it.
232                 if (state->buttonDepressed >= 0) {
233                     drawButton(&(state->buttons[state->buttonDepressed]), BUTTON_NORMAL, state->dbuf);
234                 } else if (!(x >= state->winX && x < state->winX + state->winWidth
235                              && y >= state->winY && y < state->winY + state->winHeight)) {
236                     // Clicking outside of a button means canceling.
237                     if (canceled) {
238                         *canceled = true;
239                     }
240                 }
241 
242                 if (state->buttonFocused >= 0) {
243                     // Buttons don't hover-highlight when one is depressed, so we have to fix that when the mouse is up.
244                     drawButton(&(state->buttons[state->buttonFocused]), BUTTON_HOVER, state->dbuf);
245                 }
246                 state->buttonDepressed = -1;
247             }
248         }
249     }
250 
251     // Keystroke:
252     if (event->eventType == KEYSTROKE) {
253 
254         // Cycle through all of the hotkeys of all of the buttons.
255         for (int i=0; i < state->buttonCount; i++) {
256             for (int k = 0; k < 10 && state->buttons[i].hotkey[k]; k++) {
257                 if (event->param1 == state->buttons[i].hotkey[k]) {
258                     // This button was chosen.
259 
260                     if (state->buttons[i].flags & B_DRAW) {
261                         // Restore the depressed and focused buttons.
262                         if (state->buttonDepressed >= 0) {
263                             drawButton(&(state->buttons[state->buttonDepressed]), BUTTON_NORMAL, state->dbuf);
264                         }
265                         if (state->buttonFocused >= 0) {
266                             drawButton(&(state->buttons[state->buttonFocused]), BUTTON_NORMAL, state->dbuf);
267                         }
268 
269                         // If the button likes to flash when keypressed:
270                         if (state->buttons[i].flags & B_KEYPRESS_HIGHLIGHT) {
271                             // Depress the chosen button.
272                             drawButton(&(state->buttons[i]), BUTTON_PRESSED, state->dbuf);
273 
274                             // Update the display.
275                             overlayDisplayBuffer(state->rbuf, NULL);
276                             overlayDisplayBuffer(state->dbuf, NULL);
277 
278                             // Wait for a little; then we're done.
279                             pauseBrogue(50);
280                         }
281                     }
282 
283                     state->buttonDepressed = i;
284                     buttonUsed = true;
285                     break;
286                 }
287             }
288         }
289 
290         if (!buttonUsed
291             && (event->param1 == ESCAPE_KEY || event->param1 == ACKNOWLEDGE_KEY)) {
292             // If the player pressed escape, we're done.
293             if (canceled) {
294                 *canceled = true;
295             }
296         }
297     }
298 
299     if (buttonUsed) {
300         state->buttonChosen = state->buttonDepressed;
301         return state->buttonChosen;
302     } else {
303         return -1;
304     }
305 }
306 
307 // Displays a bunch of buttons and collects user input.
308 // Returns the index number of the chosen button, or -1 if the user cancels.
309 // A window region is described by winX, winY, winWidth and winHeight.
310 // Clicking outside of that region will constitute canceling.
buttonInputLoop(brogueButton * buttons,short buttonCount,short winX,short winY,short winWidth,short winHeight,rogueEvent * returnEvent)311 short buttonInputLoop(brogueButton *buttons,
312                       short buttonCount,
313                       short winX,
314                       short winY,
315                       short winWidth,
316                       short winHeight,
317                       rogueEvent *returnEvent) {
318     short button;
319     boolean canceled;
320     rogueEvent theEvent;
321     buttonState state = {0};
322 
323     assureCosmeticRNG;
324 
325     canceled = false;
326     initializeButtonState(&state, buttons, buttonCount, winX, winY, winWidth, winHeight);
327 
328     do {
329         // Update the display.
330         overlayDisplayBuffer(state.dbuf, NULL);
331 
332         // Get input.
333         nextBrogueEvent(&theEvent, true, false, false);
334 
335         // Process the input.
336         button = processButtonInput(&state, &canceled, &theEvent);
337 
338         // Revert the display.
339         overlayDisplayBuffer(state.rbuf, NULL);
340 
341     } while (button == -1 && !canceled);
342 
343     if (returnEvent) {
344         *returnEvent = theEvent;
345     }
346 
347     //overlayDisplayBuffer(dbuf, NULL); // hangs around
348 
349     restoreRNG;
350 
351     return button;
352 }
353