4 // Graphical User Interface support
6 // © 2014-2019 Underground Software
8 // JLH = James Hammons <jlhamm@acm.org>
11 // --- ---------- -----------------------------------------------------------
12 // JLH 02/03/2006 Created this file
13 // JLH 03/13/2006 Added functions to allow shutting down GUI externally
14 // JLH 03/22/2006 Finalized basic multiple window support
15 // JLH 03/03/2014 Refactored GUI to use SDL 2, more modern approach as well
19 // - Memory leak on quitting with a window active [DONE]
20 // - Multiple window handling [DONE]
27 #include "diskselector.h"
29 #include "floppydrive.h"
34 // Icons, in GIMP "C" format
35 #include "gfx/icon-selection.c"
36 #include "gfx/disk-icon.c"
37 #include "gfx/power-off-icon.c"
38 #include "gfx/power-on-icon.c"
39 #include "gfx/disk-swap-icon.c"
40 #include "gfx/disk-door-open.c"
41 #include "gfx/disk-door-closed.c"
42 #include "gfx/save-state-icon.c"
43 #include "gfx/load-state-icon.c"
44 #include "gfx/config-icon.c"
47 // Okay, this is ugly but works and I can't think of any better way to handle
48 // this. So what we do when we pass the GIMP bitmaps into a function is pass
49 // them as a (void *) and then cast them as type (Bitmap *) in order to use
50 // them. Yes, it's ugly. Come up with something better!
55 unsigned int bytesPerPixel; // 3:RGB, 4:RGBA
56 unsigned char pixelData[];
60 const char numeralOne[(7 * 7) + 1] =
69 const char numeralTwo[(7 * 7) + 1] =
78 const char ejectIcon[(8 * 7) + 1] =
87 const char newDiskIcon[(30 * 6) + 1] =
88 "@@ @@ @@@@@ @@ @@ @@ "
89 "@@@ @@ @@ @@ @@ @@@@ "
90 "@@@@ @@ @@@@ @@ @ @@ @@@@@@"
91 "@@ @@@@ @@ @@@@@@@ @@ "
92 "@@ @@@ @@ @@@@@@@ @@@@@ "
93 "@@ @@ @@@@@ @@ @@ @@@@ ";
95 const char driveLight[(5 * 5) + 1] =
103 SDL_Texture * GUI::overlay = NULL;
105 int GUI::sidebarState = SBS_HIDDEN;
107 int32_t GUI::iconSelected = -1;
108 bool GUI::hasKeyboardFocus = false;
109 bool GUI::powerOnState = true;
111 int32_t lastIconSelected = -1;
112 SDL_Texture * iconSelection = NULL;
113 SDL_Texture * diskIcon = NULL;
114 SDL_Texture * disk1Icon = NULL;
115 SDL_Texture * disk2Icon = NULL;
116 SDL_Texture * powerOnIcon = NULL;
117 SDL_Texture * powerOffIcon = NULL;
118 SDL_Texture * diskSwapIcon = NULL;
119 SDL_Texture * stateSaveIcon = NULL;
120 SDL_Texture * stateLoadIcon = NULL;
121 SDL_Texture * configIcon = NULL;
122 SDL_Texture * doorOpen = NULL;
123 SDL_Texture * doorClosed = NULL;
124 uint32_t texturePointer[128 * 380];
125 const char iconHelp[7][80] = { "Turn emulated Apple off/on",
126 "Insert floppy image into drive #1", "Insert floppy image into drive #2",
127 "Swap disks", "Save emulator state", "Load emulator state",
128 "Configure Apple2" };
129 bool disk1EjectHovered = false;
130 bool disk2EjectHovered = false;
131 bool disk1NewDiskHovered = false;
132 bool disk2NewDiskHovered = false;
134 SDL_Texture * GUI::charStamp = NULL;
135 uint32_t GUI::stamp[FONT_WIDTH * FONT_HEIGHT];
137 #define SIDEBAR_X_POS (VIRTUAL_SCREEN_WIDTH - 80)
139 //std::vector<void *> objList;
152 void GUI::Init(SDL_Renderer * renderer)
154 overlay = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ABGR8888,
155 SDL_TEXTUREACCESS_TARGET, 128, 380);
156 charStamp = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888,
157 SDL_TEXTUREACCESS_TARGET, FONT_WIDTH, FONT_HEIGHT);
161 WriteLog("GUI: Could not create overlay!\n");
165 if (SDL_SetTextureBlendMode(overlay, SDL_BLENDMODE_BLEND) == -1)
166 WriteLog("GUI: Could not set blend mode for overlay.\n");
168 if (SDL_SetTextureBlendMode(charStamp, SDL_BLENDMODE_BLEND) == -1)
169 WriteLog("GUI: Could not set blend mode for charStamp.\n");
171 for(uint32_t i=0; i<128*380; i++)
172 texturePointer[i] = 0xB0A000A0;
174 SDL_UpdateTexture(overlay, NULL, texturePointer, 128 * sizeof(Uint32));
176 olDst.x = VIRTUAL_SCREEN_WIDTH;
181 iconSelection = CreateTexture(renderer, &icon_selection);
182 diskIcon = CreateTexture(renderer, &disk_icon);
183 doorOpen = CreateTexture(renderer, &door_open);
184 doorClosed = CreateTexture(renderer, &door_closed);
185 disk1Icon = CreateTexture(renderer, &disk_icon);
186 disk2Icon = CreateTexture(renderer, &disk_icon);
187 powerOffIcon = CreateTexture(renderer, &power_off);
188 powerOnIcon = CreateTexture(renderer, &power_on);
189 diskSwapIcon = CreateTexture(renderer, &disk_swap);
190 stateSaveIcon = CreateTexture(renderer, &save_state);
191 stateLoadIcon = CreateTexture(renderer, &load_state);
192 configIcon = CreateTexture(renderer, &config);
194 // Set up drive icons in their current states
195 // AssembleDriveIcon(renderer, 0);
196 // AssembleDriveIcon(renderer, 1);
198 if (SDL_SetRenderTarget(renderer, overlay) < 0)
200 WriteLog("GUI: Could not set Render Target to overlay... (%s)\n", SDL_GetError());
204 DrawSidebarIcons(renderer);
205 // Set render target back to default
206 SDL_SetRenderTarget(renderer, NULL);
209 DiskSelector::Init(renderer);
210 Config::Init(renderer);
211 WriteLog("GUI: Successfully initialized.\n");
215 SDL_Texture * GUI::CreateTexture(SDL_Renderer * renderer, const void * source)
217 Bitmap * bitmap = (Bitmap *)source;
218 SDL_Texture * texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ABGR8888,
219 // SDL_TEXTUREACCESS_STATIC, bitmap->width, bitmap->height);
220 SDL_TEXTUREACCESS_TARGET, bitmap->width, bitmap->height);
221 SDL_SetTextureBlendMode(texture, SDL_BLENDMODE_BLEND);
222 SDL_UpdateTexture(texture, NULL, (Uint32 *)bitmap->pixelData,
223 bitmap->width * sizeof(Uint32));
229 void GUI::MouseDown(int32_t x, int32_t y, uint32_t buttons)
231 DiskSelector::MouseDown(x, y, buttons);
232 Config::MouseDown(x, y, buttons);
234 if (sidebarState != SBS_SHOWN)
237 switch (iconSelected)
241 powerOnState = !powerOnState;
245 SpawnMessage("*** POWER OFF ***");
250 SpawnMessage("*** DISK #1 ***");
252 if (disk1EjectHovered && !floppyDrive[0].IsEmpty(0))
254 floppyDrive[0].EjectImage(0);
255 SpawnMessage("*** DISK #1 EJECTED ***");
258 if (!disk1EjectHovered && !disk1NewDiskHovered)
260 // Load the disk selector
261 Config::HideWindow();
262 DiskSelector::ShowWindow(0);
268 SpawnMessage("*** DISK #2 ***");
270 if (disk2EjectHovered && !floppyDrive[0].IsEmpty(1))
272 floppyDrive[0].EjectImage(1);
273 SpawnMessage("*** DISK #2 EJECTED ***");
276 if (!disk2EjectHovered && !disk2NewDiskHovered)
278 // Load the disk selector
279 Config::HideWindow();
280 DiskSelector::ShowWindow(1);
286 floppyDrive[0].SwapImages();
287 SpawnMessage("*** DISKS SWAPPED ***");
291 SpawnMessage("*** SAVE STATE ***");
295 SpawnMessage("*** LOAD STATE ***");
299 SpawnMessage("*** CONFIGURATION ***");
300 // Load the configuration window
301 DiskSelector::HideWindow();
302 Config::ShowWindow();
308 void GUI::MouseUp(int32_t x, int32_t y, uint32_t buttons)
310 DiskSelector::MouseUp(x, y, buttons);
311 Config::MouseUp(x, y, buttons);
315 void GUI::MouseMove(int32_t x, int32_t y, uint32_t buttons)
317 DiskSelector::MouseMove(x, y, buttons);
318 Config::MouseMove(x, y, buttons);
320 if (sidebarState != SBS_SHOWN)
324 if (x > SIDEBAR_X_POS)
326 //printf("GUI: sidebar showing (x = %i)...\n", x);
327 sidebarState = SBS_SHOWING;
332 //printf("GUI: sidebar hiding[1] (x = %i)...\n", x);
333 sidebarState = SBS_HIDING;
339 if (x < SIDEBAR_X_POS)
341 iconSelected = lastIconSelected = -1;
342 HandleIconSelection(sdlRenderer);
343 //printf("GUI: sidebar hiding[2] (x = %i)...\n", x);
344 sidebarState = SBS_HIDING;
347 // We're in the right zone, and the sidebar is shown, so let's select
351 if (y < 4 || y > 383)
356 iconSelected = (y - 4) / 54;
358 // It's y+2 because the sidebar sits at (SIDEBAR_X_POS, 2)
359 disk1EjectHovered = ((x >= (SIDEBAR_X_POS + 24 + 29))
360 && (x < (SIDEBAR_X_POS + 24 + 29 + 8))
361 && (y >= (63 + 31 + 2))
362 && (y < (63 + 31 + 2 + 7)) ? true : false);
364 disk2EjectHovered = ((x >= (SIDEBAR_X_POS + 24 + 29))
365 && (x < (SIDEBAR_X_POS + 24 + 29 + 8))
366 && (y >= (117 + 31 + 2))
367 && (y < (117 + 31 + 2 + 7)) ? true : false);
369 disk1NewDiskHovered = ((x >= (SIDEBAR_X_POS + 24 + 6))
370 && (x < (SIDEBAR_X_POS + 24 + 6 + 30))
371 && (y >= (63 + 31 + 2))
372 && (y < (63 + 31 + 2 + 6)) ? true : false);
374 disk2NewDiskHovered = ((x >= (SIDEBAR_X_POS + 24 + 6))
375 && (x < (SIDEBAR_X_POS + 24 + 6 + 30))
376 && (y >= (117 + 31 + 2))
377 && (y < (117 + 31 + 2 + 6)) ? true : false);
379 if (iconSelected != lastIconSelected)
381 HandleIconSelection(sdlRenderer);
382 lastIconSelected = iconSelected;
384 if ((iconSelected >= 0) && (iconSelected <= 6))
385 SpawnMessage("%s", iconHelp[iconSelected]);
387 // Show what's in the selected drive
388 if (iconSelected >= 1 && iconSelected <= 2)
390 if (!floppyDrive[0].IsEmpty(iconSelected - 1))
391 SpawnMessage("\"%s\"", floppyDrive[0].ImageName(iconSelected - 1));
399 bool GUI::KeyDown(uint32_t key)
401 return Config::KeyDown(key);
405 void GUI::HandleIconSelection(SDL_Renderer * renderer)
407 // Set up drive icons in their current states
408 AssembleDriveIcon(renderer, 0);
409 AssembleDriveIcon(renderer, 1);
411 // Reload the background...
412 SDL_UpdateTexture(overlay, NULL, texturePointer, 128 * sizeof(Uint32));
414 if (SDL_SetRenderTarget(renderer, overlay) < 0)
416 WriteLog("GUI: Could not set Render Target to overlay... (%s)\n", SDL_GetError());
420 // Draw the icon selector, if an icon is selected
421 if (iconSelected >= 0)
423 SDL_Rect dst;// = { 54, 54, 24 - 7, 2 };
424 dst.w = dst.h = 54, dst.x = 24 - 7, dst.y = 2 + (iconSelected * 54);
425 SDL_RenderCopy(renderer, iconSelection, NULL, &dst);
428 DrawSidebarIcons(renderer);
430 // Set render target back to default
431 SDL_SetRenderTarget(renderer, NULL);
435 void GUI::AssembleDriveIcon(SDL_Renderer * renderer, int driveNumber)
437 SDL_Texture * drive[2] = { disk1Icon, disk2Icon };
438 const char * number[2] = { numeralOne, numeralTwo };
440 if (SDL_SetRenderTarget(renderer, drive[driveNumber]) < 0)
442 WriteLog("GUI: Could not set Render Target to overlay... (%s)\n", SDL_GetError());
446 SDL_RenderClear(renderer);
447 SDL_RenderCopy(renderer, diskIcon, NULL, NULL);
449 // Drive door @ (16, 7)
451 dst.w = 8, dst.h = 10, dst.x = 16, dst.y = 7;
452 SDL_RenderCopy(renderer, (floppyDrive[0].IsEmpty(driveNumber) ?
453 doorOpen : doorClosed), NULL, &dst);
455 // Numeral @ (30, 20)
456 DrawCharArray(renderer, number[driveNumber], 30, 20, 7, 7, 0xD0, 0xE0, 0xF0);
457 DrawDriveLight(renderer, driveNumber);
458 DrawEjectButton(renderer, driveNumber);
459 DrawNewDiskButton(renderer, driveNumber);
461 // Set render target back to default
462 SDL_SetRenderTarget(renderer, NULL);
466 void GUI::DrawEjectButton(SDL_Renderer * renderer, int driveNumber)
468 if (floppyDrive[0].IsEmpty(driveNumber))
471 uint8_t r = 0x00, g = 0xAA, b = 0x00;
473 if ((driveNumber == 0 && disk1EjectHovered)
474 || (driveNumber == 1 && disk2EjectHovered))
475 r = 0x20, g = 0xFF, b = 0x20;
477 DrawCharArray(renderer, ejectIcon, 29, 31, 8, 7, r, g, b);
481 void GUI::DrawNewDiskButton(SDL_Renderer * renderer, int driveNumber)
483 if (!floppyDrive[0].IsEmpty(driveNumber))
486 uint8_t r = 0x00, g = 0xAA, b = 0x00;
488 if ((driveNumber == 0 && disk1NewDiskHovered)
489 || (driveNumber == 1 && disk2NewDiskHovered))
490 r = 0x20, g = 0xFF, b = 0x20;
492 DrawCharArray(renderer, newDiskIcon, 6, 31, 30, 6, r, g, b);
496 void GUI::DrawDriveLight(SDL_Renderer * renderer, int driveNumber)
498 int lightState = floppyDrive[0].DriveLightStatus(driveNumber);
499 int r = 0x77, g = 0x00, b = 0x00;
501 if (lightState == DLS_READ)
502 r = 0x20, g = 0xFF, b = 0x20;
503 else if (lightState == DLS_WRITE)
504 r = 0xFF, g = 0x30, b = 0x30;
506 // Drive light @ (8, 21)
507 DrawCharArray(renderer, driveLight, 8, 21, 5, 5, r, g, b);
511 void GUI::DrawCharArray(SDL_Renderer * renderer, const char * array, int x,
512 int y, int w, int h, int r, int g, int b)
514 SDL_SetRenderDrawColor(renderer, r, g, b, 0xFF);
516 for(int j=0; j<h; j++)
518 for(int i=0; i<w; i++)
520 if (array[(j * w) + i] != ' ')
521 SDL_RenderDrawPoint(renderer, x + i, y + j);
525 SDL_SetRenderDrawColor(renderer, 0x00, 0x00, 0x00, 0x00);
529 void GUI::DrawCharacter(SDL_Renderer * renderer, int x, int y, uint8_t c, bool invert/*= false*/)
531 uint32_t inv = (invert ? 0x000000FF : 0x00000000);
532 uint32_t pixel = 0xFFFFC000; // RRGGBBAA
533 uint8_t * ptr = (uint8_t *)&font10pt[(c - 0x20) * FONT_WIDTH * FONT_HEIGHT];
535 dst.x = x * FONT_WIDTH, dst.y = y * FONT_HEIGHT, dst.w = FONT_WIDTH, dst.h = FONT_HEIGHT;
537 for(int i=0; i<FONT_WIDTH*FONT_HEIGHT; i++)
538 stamp[i] = (pixel | ptr[i]) ^ inv;
540 SDL_UpdateTexture(charStamp, NULL, stamp, FONT_WIDTH * sizeof(Uint32));
541 SDL_RenderCopy(renderer, charStamp, NULL, &dst);
546 // N.B.: This draws a char at an abosulte X/Y position, not on a grid
548 void GUI::DrawCharacterVert(SDL_Renderer * renderer, int x, int y, uint8_t c, bool invert/*= false*/)
550 uint32_t inv = (invert ? 0x000000FF : 0x00000000);
551 uint32_t pixel = 0xFFFFC000; // RRGGBBAA
552 uint8_t * ptr = (uint8_t *)&font10pt[(c - 0x20) * FONT_WIDTH * FONT_HEIGHT];
554 dst.x = x, dst.y = y, dst.w = FONT_WIDTH, dst.h = FONT_HEIGHT;
555 SDL_Point pt = { FONT_WIDTH - 1, FONT_HEIGHT - 1 };
557 for(int i=0; i<FONT_WIDTH*FONT_HEIGHT; i++)
558 stamp[i] = (pixel | ptr[i]) ^ inv;
560 SDL_SetTextureBlendMode(charStamp, SDL_BLENDMODE_NONE);
561 SDL_UpdateTexture(charStamp, NULL, stamp, FONT_WIDTH * sizeof(Uint32));
562 SDL_RenderCopyEx(renderer, charStamp, NULL, &dst, 270.0, &pt, SDL_FLIP_NONE);
563 SDL_SetTextureBlendMode(charStamp, SDL_BLENDMODE_BLEND);
567 void GUI::DrawString(SDL_Renderer * renderer, int x, int y, const char * s, bool invert/*= false*/)
571 for(int i=0; i<len; i++)
572 DrawCharacter(renderer, x + i, y, s[i], invert);
577 // N.B.: This draws a char at an abosulte X/Y position, not on a grid
579 void GUI::DrawStringVert(SDL_Renderer * renderer, int x, int y, const char * s, bool invert/*= false*/)
583 for(int i=0; i<len; i++)
584 DrawCharacterVert(renderer, x, y - (FONT_WIDTH * i), s[i], invert);
588 void GUI::DrawBox(SDL_Renderer * renderer, int x, int y, int w, int h, int r/*= 0x00*/, int g/*= 0xAA*/, int b/*= 0x00*/)
590 SDL_SetRenderDrawColor(renderer, r, g, b, 0xFF);
592 for(int i=0; i<w; i++)
594 SDL_RenderDrawPoint(renderer, x + i, y);
595 SDL_RenderDrawPoint(renderer, x + i, y + h - 1);
598 for(int i=0; i<h; i++)
600 SDL_RenderDrawPoint(renderer, x, y + i);
601 SDL_RenderDrawPoint(renderer, x + w - 1, y + i);
604 SDL_SetRenderDrawColor(renderer, 0x00, 0x00, 0x00, 0x00);
608 void GUI::HandleGUIState(void)
612 if (olDst.x < SIDEBAR_X_POS && sidebarState == SBS_SHOWING)
614 olDst.x = SIDEBAR_X_POS;
615 sidebarState = SBS_SHOWN;
618 else if (olDst.x > VIRTUAL_SCREEN_WIDTH && sidebarState == SBS_HIDING)
620 olDst.x = VIRTUAL_SCREEN_WIDTH;
621 sidebarState = SBS_HIDDEN;
627 void GUI::DrawSidebarIcons(SDL_Renderer * renderer)
629 SDL_Texture * icons[7] = { powerOnIcon, disk1Icon, disk2Icon, diskSwapIcon,
630 stateSaveIcon, stateLoadIcon, configIcon };
632 icons[0] = (powerOnState ? powerOnIcon : powerOffIcon);
633 SDL_Rect dst = { 24, 2 + 7, 40, 40 };
635 for(int i=0; i<7; i++)
637 SDL_RenderCopy(renderer, icons[i], NULL, &dst);
643 void GUI::Render(SDL_Renderer * renderer)
651 if (sidebarState != SBS_HIDDEN)
652 HandleIconSelection(renderer);
654 SDL_RenderCopy(renderer, overlay, NULL, &olDst);
657 DiskSelector::Render(renderer);
658 Config::Render(renderer);
667 cut into 7 pieces give ~54 pix per piece
668 So, let's try 40x40 icons, and see if that's good enough...
671 drive proportions: 1.62 : 1
684 | ^| <-- eject button
705 maybe state save/load