]> Shamusworld >> Repos - apple2/blobdiff - src/gui/diskselector.cpp
Add support for .hdv hard drive images, new "Rob Color TV" palette.
[apple2] / src / gui / diskselector.cpp
index bcc0c239b9609608539326ae9528c0ab7f38cffd..57014b5e3cb1986a6798a9f91058d9bc4d8e14f1 100644 (file)
@@ -3,16 +3,18 @@
 //
 // Floppy disk selector GUI
 // by James Hammons
-// © 2014 Underground Software
+// © 2014-2018 Underground Software
 //
 // JLH = James Hammons <jlhamm@acm.org>
 //
 // WHO  WHEN        WHAT
-// ---  ----------  ------------------------------------------------------------
+// ---  ----------  -----------------------------------------------------------
 // JLH  10/13/2013  Created this file
 //
 // STILL TO DO:
 //
+// - Fix bug where hovering on scroll image causes it to fly across the screen
+//   [DONE]
 //
 
 #include "diskselector.h"
 #include <algorithm>
 #include <string>
 #include <vector>
-#include "apple2.h"
+#include "crc32.h"
+#include "fileio.h"
+#include "floppydrive.h"
 #include "font10pt.h"
+#include "gui.h"
 #include "log.h"
 #include "settings.h"
 #include "video.h"
 
+// Icons, in GIMP "C" format
+#include "gfx/scroll-left.c"
+#include "gfx/scroll-right.c"
 
-enum { DSS_SHOWING, DSS_HIDING, DSS_SHOWN, DSS_HIDDEN };
 
-#define DS_WIDTH       400
-#define DS_HEIGHT      300
+struct Bitmap {
+       unsigned int width;
+       unsigned int height;
+       unsigned int bytesPerPixel;                                     // 3:RGB, 4:RGBA
+       unsigned char pixelData[];
+};
+
+enum { DSS_SHOWING, DSS_HIDING, DSS_SHOWN, DSS_HIDDEN, DSS_LSB_SHOWING, DSS_LSB_SHOWN, DSS_LSB_HIDING, DSS_RSB_SHOWING, DSS_RSB_SHOWN, DSS_RSB_HIDING, DSS_TEXT_SCROLLING };
+
+#define DS_WIDTH                       402
+#define DS_HEIGHT                      322
+#define SCROLL_HOT_WIDTH       48
+#define DS_XPOS        ((VIRTUAL_SCREEN_WIDTH - DS_WIDTH) / 2)
+#define DS_YPOS        ((VIRTUAL_SCREEN_HEIGHT - DS_HEIGHT) / 2)
+
+
+static bool entered = false;
+static int driveNumber;
+static int diskSelectorState = DSS_HIDDEN;
+static int diskSelected = -1;
+static int lastDiskSelected = -1;
+static int numColumns;
+static int colStart = 0;
+static int dxLeft = 0;
+static int dxRight = 0;
+static int rsbPos = DS_WIDTH;
+static int lsbPos = -40;
+static int textScrollCount = 0;
+static bool refresh = false;
 
-bool entered = false;
-int driveNumber;
-int diskSelectorState = DSS_HIDDEN;
-int diskSelected = -1;
-int lastDiskSelected = -1;
+/*
+So, how this will work for multiple columns, where the number of columns is greater than 3, is to have an arrow button pop up on the left or right hand side (putting the mouse on the left or right side of the disk selector activates (shows) the button, if such a move can be made. Button hides when the mouse moves out of the hot zone or when it has no more effect.
+*/
+
+
+// We make provision for sets of 32 or less...
+/*
+The way the manifests are laid out, we make the assumption that the boot disk of a set is always listed first.  Therefore, image[0] will always be the boot disk.
+*/
+struct DiskSet
+{
+       uint8_t num;                    // # of disks in this set
+       std::string name;               // The name of this disk set
+//     std::string fullPath;   // The path to the containing folder
+       std::string image[32];  // List of disk images in this set
+       std::string imgName[32];// List of human readable names of disk images
+       uint32_t crc[32];               // List of CRC32s of the disk images in the set
+       uint32_t crcFound[32];  // List of CRC32s actually discovered on filesystem
+
+       DiskSet(): num(0) {}
+};
 
 
 //
-// Case insensitve string comparison voodoo
+// Struct to hold filenames & full paths to same
 //
-struct ci_char_traits : public std::char_traits<char>
+struct FileStruct
 {
-       static bool eq(char c1, char c2) { return toupper(c1) == toupper(c2); }
-       static bool ne(char c1, char c2) { return toupper(c1) != toupper(c2); }
-       static bool lt(char c1, char c2) { return toupper(c1) <  toupper(c2); }
-       static int compare(const char * s1, const char * s2, size_t n)
-       {
-               while (n-- != 0)
-               {
-                       if (toupper(*s1) < toupper(*s2)) return -1;
-                       if (toupper(*s1) > toupper(*s2)) return 1;
-                       ++s1; ++s2;
-               }
-               return 0;
-       }
-       static const char * find(const char * s, int n, char a)
+       std::string image;
+       std::string fullPath;
+       DiskSet diskSet;
+
+//     FileStruct(): diskSet(NULL) {}
+//     ~FileStruct() { if (diskSet != NULL) delete diskSet; }
+
+       // Functor, to presumably make the std::sort go faster
+       bool operator()(const FileStruct & a, const FileStruct & b) const
        {
-               while (n-- > 0 && toupper(*s) != toupper(a))
-               {
-                       ++s;
-               }
-               return s;
+               return (strcasecmp(a.image.c_str(), b.image.c_str()) < 0 ? true : false);
        }
 };
 
-typedef std::basic_string<char, ci_char_traits> ci_string;
-//
-// END Case insensitve string comparison voodoo
-//
-
 
 static SDL_Texture * window = NULL;
-static SDL_Texture * charStamp = NULL;
 static uint32_t windowPixels[DS_WIDTH * DS_HEIGHT];
-static uint32_t stamp[FONT_WIDTH * FONT_HEIGHT];
+SDL_Texture * scrollLeftIcon = NULL;
+SDL_Texture * scrollRightIcon = NULL;
 bool DiskSelector::showWindow = false;
-std::vector<ci_string> imageList;
+std::vector<FileStruct> fsList;
+std::vector<FileStruct> hdList;
 
 
 void DiskSelector::Init(SDL_Renderer * renderer)
 {
        window = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ABGR8888,
                SDL_TEXTUREACCESS_TARGET, DS_WIDTH, DS_HEIGHT);
-       charStamp = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888,
-               SDL_TEXTUREACCESS_TARGET, FONT_WIDTH, FONT_HEIGHT);
 
        if (!window)
        {
@@ -97,61 +132,303 @@ void DiskSelector::Init(SDL_Renderer * renderer)
        if (SDL_SetTextureBlendMode(window, SDL_BLENDMODE_BLEND) == -1)
                WriteLog("GUI (DiskSelector): Could not set blend mode for window.\n");
 
-       if (SDL_SetTextureBlendMode(charStamp, SDL_BLENDMODE_BLEND) == -1)
-               WriteLog("GUI (DiskSelector): Could not set blend mode for charStamp.\n");
+       scrollLeftIcon  = GUI::CreateTexture(renderer, &scroll_left);
+       scrollRightIcon = GUI::CreateTexture(renderer, &scroll_right);
 
        for(uint32_t i=0; i<DS_WIDTH*DS_HEIGHT; i++)
-               windowPixels[i] = 0xEF00FF00;
+               windowPixels[i] = 0xEF007F00;
 
-       SDL_UpdateTexture(window, NULL, windowPixels, 128 * sizeof(Uint32));
-       FindDisks(NULL);
+       SDL_UpdateTexture(window, NULL, windowPixels, DS_WIDTH * sizeof(Uint32));
+       FindDisks();
+       FindHardDisks();
        DrawFilenames(renderer);
 }
 
 
+//
+// Find all disks images top level call
+//
+void DiskSelector::FindDisks(void)
+{
+       fsList.clear();
+       FindDisks(settings.disksPath);
+       std::sort(fsList.begin(), fsList.end(), FileStruct());
+       // Calculate the number of columns in the file selector...
+       numColumns = (int)ceilf((float)fsList.size() / 27.0f);
+       WriteLog("GUI (DiskSelector)::FindDisks(): # of columns is %i (%i files)\n", numColumns, fsList.size());
+}
+
+
+/*
+OK, so the way that you can determine if a file is a directory in a cross-
+platform way is to do an opendir() call on a discovered filename.  If it
+returns NULL, then it's a regular file and not a directory.  Though I think the
+Linux method is more elegant.  :-P
+*/
+//
+// Find all disks images within path (recursive call does depth first search)
+//
 void DiskSelector::FindDisks(const char * path)
 {
-       DIR * dir = opendir(settings.disksPath);
+       DIR * dir = opendir(path);
 
        if (!dir)
        {
-               WriteLog("GUI (DiskSelector)::FindDisks: Could not open directory \"%s\%!\n", settings.disksPath);
+               WriteLog("GUI (DiskSelector)::FindDisks: Could not open directory \"%s\%!\n", path);
                return;
        }
 
-       imageList.clear();
        dirent * ent;
 
        while ((ent = readdir(dir)) != NULL)
        {
-               if (HasLegalExtension(ent->d_name))
-                       imageList.push_back(ci_string(ent->d_name));
+               char buf[0x10000];
+               sprintf(buf, "%s/%s", path, ent->d_name);
+
+               // Cross-platform way to test if it's a directory...
+               DIR * test = opendir(buf);
+
+//             if ((ent->d_type == DT_REG) && HasLegalExtension(ent->d_name))
+               if (test == NULL)
+               {
+                       if (HasLegalExtension(ent->d_name))
+                       {
+                               FileStruct fs;
+                               fs.image = ent->d_name;
+                               fs.fullPath = buf;
+                               fsList.push_back(fs);
+                       }
+               }
+//             else if (ent->d_type == DT_DIR)
+               else
+               {
+                       // Make sure we close the thing, since it's a bona-fide dir!
+                       closedir(test);
+
+                       // Only recurse if the directory is not one of the special ones...
+                       if ((strcmp(ent->d_name, "..") != 0)
+                               && (strcmp(ent->d_name, ".") != 0))
+                       {
+                               // Check to see if this is a special directory with a manifest
+                               char buf2[0x10000];
+                               sprintf(buf2, "%s/manifest.txt", buf);
+                               FILE * fp = fopen(buf2, "r");
+
+                               // No manifest means it's just a regular directory...
+                               if (fp == NULL)
+                                       FindDisks(buf);
+                               else
+                               {
+                                       // Read the manifest and all that good stuff
+                                       FileStruct fs;
+                                       ReadManifest(fp, &fs.diskSet);
+                                       fclose(fp);
+
+                                       // Finally, check that the stuff in the manifest is
+                                       // actually in the directory...
+                                       if (CheckManifest(buf, &fs.diskSet) == true)
+                                       {
+                                               fs.fullPath = buf;
+                                               fs.image = fs.diskSet.name;
+                                               fsList.push_back(fs);
+                                       }
+                                       else
+                                               WriteLog("Manifest for '%s' failed check phase.\n", fs.diskSet.name.c_str());
+#if 0
+                                       printf("Name found: \"%s\" (%d)\nDisks:\n", fs.diskSet.name.c_str(), fs.diskSet.num);
+                                       for(int i=0; i<fs.diskSet.num; i++)
+                                               printf("%s (CRC: %08X)\n", fs.diskSet.image[i].c_str(), fs.diskSet.crc[i]);
+#endif
+
+                               }
+                       }
+               }
        }
 
        closedir(dir);
-       std::sort(imageList.begin(), imageList.end());
-#if 0
+}
+
+
+void DiskSelector::ReadManifest(FILE * fp, DiskSet * ds)
 {
-       std::vector<ci_string>::iterator i;
-       for(i=imageList.begin(); i!=imageList.end(); i++)
-               printf("GUI::DS::Found \"%s\"\n", (*i).c_str());
+       char line[0x10000];
+       int disksFound = 0;
+       int lineNo = 0;
+
+       while (!feof(fp))
+       {
+               fgets(line, 0x10000, fp);
+               lineNo++;
+
+               if ((line[0] == '#') || (line[0] == '\n'))
+                       ; // Do nothing with comments or blank lines...
+               else
+               {
+                       char buf[1024];
+                       char crcbuf[16];
+                       char altName[1024];
+
+                       if (strncmp(line, "diskset", 7) == 0)
+                       {
+                               sscanf(line, "diskset=\"%[^\"]\"", buf);
+                               ds->name = buf;
+                       }
+                       else if (strncmp(line, "disks", 5) == 0)
+                       {
+                               sscanf(line, "disks=%hhd", &ds->num);
+                       }
+                       else if (strncmp(line, "disk", 4) == 0)
+                       {
+                               int n = sscanf(line, "disk=%s %s (%s)", buf, crcbuf, altName);
+
+                               if ((n == 2) || (n == 3))
+                               {
+                                       ds->image[disksFound] = buf;
+                                       ds->crc[disksFound] = strtoul(crcbuf, NULL, 16);
+                                       disksFound++;
+
+                                       if (n == 3)
+                                               ds->imgName[disksFound] = altName;
+                                       else
+                                       {
+                                               // Find the file's extension, if any
+                                               char * ext = strrchr(buf, '.');
+
+                                               // Kill the disk extension, if it exists
+                                               if (ext != NULL)
+                                                       *ext = 0;
+
+                                               ds->imgName[disksFound] = buf;
+                                       }
+                               }
+                               else
+                                       WriteLog("Malformed disk descriptor in manifest at line %d\n", lineNo);
+                       }
+               }
+       }
+
+       if (disksFound != ds->num)
+               WriteLog("Found only %d entries in manifest, expected %hhd\n", disksFound, ds->num);
 }
-#endif
+
+
+bool DiskSelector::CheckManifest(const char * path, DiskSet * ds)
+{
+       uint8_t found = 0;
+
+       for(int i=0; i<ds->num; i++)
+       {
+               std::string filename = path;
+               filename += "/";
+               filename += ds->image[i];
+               uint32_t size;
+               uint8_t * buf = ReadFile(filename.c_str(), &size);
+
+               if (buf != NULL)
+               {
+                       ds->crcFound[i] = CRC32(buf, size);
+                       free(buf);
+                       found++;
+
+                       if (ds->crc[i] != ds->crcFound[i])
+                       {
+                               WriteLog("Warning: Bad CRC32 for '%s'. Expected: %08X, found: %08X\n", ds->image[i], ds->crc[i], ds->crcFound[i]);
+                       }
+               }
+       }
+
+       return (found == ds->num ? true : false);
 }
 
 
 bool DiskSelector::HasLegalExtension(const char * name)
 {
+       // Find the file's extension, if any
        const char * ext = strrchr(name, '.');
 
-       if ((strcasecmp(ext, ".dsk") == 0) || (strcasecmp(ext, ".do") == 0)
-               || (strcasecmp(ext, ".po") == 0) || (strcasecmp(ext, ".nib") == 0))
+       // No extension, so fuggetaboutit
+       if (ext == NULL)
+               return false;
+
+       // Otherwise, look for a legal extension
+       // We should be smarter than this, and look at headers & file sizes instead
+       if ((strcasecmp(ext, ".dsk") == 0)
+               || (strcasecmp(ext, ".do") == 0)
+               || (strcasecmp(ext, ".po") == 0)
+               || (strcasecmp(ext, ".woz") == 0))
                return true;
 
        return false;
 }
 
 
+//
+// Find all disks images top level call
+//
+void DiskSelector::FindHardDisks(void)
+{
+       hdList.clear();
+       FindHardDisks(settings.disksPath);
+       std::sort(hdList.begin(), hdList.end(), FileStruct());
+       WriteLog("GUI (DiskSelector)::FindHardDisks(): # of HDs is %i\n", hdList.size());
+}
+
+
+//
+// Find all hard disk images within path (recursive call does depth first search)
+//
+void DiskSelector::FindHardDisks(const char * path)
+{
+       DIR * dir = opendir(path);
+
+       if (!dir)
+       {
+               WriteLog("GUI (DiskSelector)::FindHardDisks: Could not open directory \"%s\%!\n", path);
+               return;
+       }
+
+       dirent * ent;
+
+       while ((ent = readdir(dir)) != NULL)
+       {
+               char buf[0x10000];
+               sprintf(buf, "%s/%s", path, ent->d_name);
+
+               // Cross-platform way to test if it's a directory (test = NULL -> file)
+               DIR * test = opendir(buf);
+
+               if (test == NULL)
+               {
+                       const char * ext = strrchr(ent->d_name, '.');
+
+                       if ((ext != NULL)
+                               && ((strcasecmp(ext, ".2mg") == 0)
+                                       || (strcasecmp(ext, ".hdv") == 0)))
+                       {
+                               FileStruct fs;
+                               fs.image = ent->d_name;
+                               fs.fullPath = buf;
+                               hdList.push_back(fs);
+                       }
+               }
+               else
+               {
+                       // Make sure we close the thing, since it's a bona-fide dir!
+                       closedir(test);
+
+                       // Only recurse if the directory is not one of the special ones...
+                       if ((strcmp(ent->d_name, "..") != 0)
+                               && (strcmp(ent->d_name, ".") != 0))
+                       {
+                               FindHardDisks(buf);
+                       }
+               }
+       }
+
+       closedir(dir);
+}
+
+
 void DiskSelector::DrawFilenames(SDL_Renderer * renderer)
 {
        if (SDL_SetRenderTarget(renderer, window) < 0)
@@ -165,8 +442,46 @@ void DiskSelector::DrawFilenames(SDL_Renderer * renderer)
        // 3 columns of 21 chars apiece (with 6X11 font), 27 rows
 
        unsigned int count = 0;
+       unsigned int fsStart = colStart * 27;
+       int offset = 0;
+
+       // Draw partial columns (for scrolling left/right)
+       // [could probably combine these...]
+       if (textScrollCount < 0)
+       {
+               int partialColStart = (colStart - 1) * 27;
+               offset = -1 * textScrollCount;
+
+               for(unsigned int y=0; y<27; y++)
+               {
+                       for(unsigned int i=22+textScrollCount, x=0; i<21; i++, x++)
+                       {
+                               if (i >= fsList[partialColStart + y].image.length())
+                                       break;
 
-       while (count < imageList.size())
+                               GUI::DrawCharacter(renderer, x + 1, y + 1, fsList[partialColStart + y].image[i], false);
+                       }
+               }
+       }
+       else if (textScrollCount > 0)
+       {
+               offset = 22 - textScrollCount;
+
+               for(unsigned int y=0; y<27; y++)
+               {
+                       for(unsigned int i=textScrollCount, x=0; i<21; i++, x++)
+                       {
+                               if (i >= fsList[fsStart + y].image.length())
+                                       break;
+
+                               GUI::DrawCharacter(renderer, x + 1, y + 1, fsList[fsStart + y].image[i], false);
+                       }
+               }
+
+               fsStart += 27;
+       }
+
+       while (fsStart < fsList.size())
        {
 //             int currentX = (count / 18) * 17;
 //             int currentY = (count % 18);
@@ -179,14 +494,15 @@ void DiskSelector::DrawFilenames(SDL_Renderer * renderer)
 //             for(unsigned int i=0; i<18; i++)
                for(unsigned int i=0; i<21; i++)
                {
-                       if (i >= imageList[count].length())
+                       if (i >= fsList[fsStart].image.length())
                                break;
 
-                       bool invert = (diskSelected == (int)count ? true : false);
-                       DrawCharacter(renderer, currentX + i, currentY, imageList[count][i], invert);
+                       bool invert = (diskSelected == (int)fsStart ? true : false);
+                       GUI::DrawCharacter(renderer, currentX + i + 1 + offset, currentY + 1, fsList[fsStart].image[i], invert);
                }
 
                count++;
+               fsStart++;
 
 //             if (count >= (18 * 3))
 //             if (count >= (24 * 3))
@@ -194,73 +510,80 @@ void DiskSelector::DrawFilenames(SDL_Renderer * renderer)
                        break;
        }
 
-       // Set render target back to default
-       SDL_SetRenderTarget(renderer, NULL);
-}
-
-
-void DiskSelector::DrawCharacter(SDL_Renderer * renderer, int x, int y, uint8_t c,
-       bool invert/*=false*/)
-{
-#if 0
-//     uint32_t pixel = 0xFF7F0000;
-       uint8_t * ptr = (uint8_t *)&font2[(c - 0x20) * FONT_WIDTH * FONT_HEIGHT];
-
-       for(int j=0; j<FONT_HEIGHT; j++)
+       // If a disk is selected, show it on the top line in inverse video
+       if (diskSelected > -1)
        {
-               for(int i=0; i<FONT_WIDTH; i++)
+               for(unsigned int i=0; i<65; i++)
                {
-                       SDL_SetRenderDrawColor(renderer, 0xFF, 0x7F, 0x00, ptr[(j * FONT_WIDTH) + i]);
-                       SDL_RenderDrawPoint(renderer, (x * FONT_WIDTH) + i, (y * FONT_HEIGHT) + j);
+                       if (i >= fsList[diskSelected].image.length())
+                               break;
+
+                       GUI::DrawCharacter(renderer, i + 1, 0, fsList[diskSelected].image[i], true);
                }
        }
 
-       SDL_SetRenderDrawColor(renderer, 0x00, 0x00, 0x00, 0x00);
-#else
-       uint32_t inv = (invert ? 0x000000FF : 0x00000000);
-       uint32_t pixel = 0xFFFFC000;    // RRGGBBAA
-       uint8_t * ptr = (uint8_t *)&font10pt[(c - 0x20) * FONT_WIDTH * FONT_HEIGHT];
-       SDL_Rect dst;
-       dst.x = x * FONT_WIDTH, dst.y = y * FONT_HEIGHT, dst.w = FONT_WIDTH, dst.h = FONT_HEIGHT;
-
-       for(int i=0; i<FONT_WIDTH*FONT_HEIGHT; i++)
-               stamp[i] = (pixel | ptr[i]) ^ inv;
-
-       SDL_UpdateTexture(charStamp, NULL, stamp, FONT_WIDTH * sizeof(Uint32));
-       SDL_RenderCopy(renderer, charStamp, NULL, &dst);
-#endif
-}
-
-
-/*
-void DiskSelector::()
-{
+       // Set render target back to default
+       SDL_SetRenderTarget(renderer, NULL);
 }
-*/
 
 
 void DiskSelector::ShowWindow(int drive)
 {
+       diskSelectorState = DSS_SHOWN;
        entered = false;
        showWindow = true;
        driveNumber = drive;
 }
 
 
+void DiskSelector::HideWindow(void)
+{
+       diskSelectorState = DSS_HIDDEN;
+       dxLeft = 0;
+       dxRight = 0;
+       rsbPos = DS_WIDTH;
+       lsbPos = -40;
+       showWindow = false;
+       refresh = true;
+}
+
+
 void DiskSelector::MouseDown(int32_t x, int32_t y, uint32_t buttons)
 {
-       if (!showWindow)
+       if (!showWindow || !entered)
                return;
 
-       if (!entered)
+       if ((diskSelectorState == DSS_LSB_SHOWING) || (diskSelectorState == DSS_LSB_SHOWN))
+       {
+               colStart--;
+               textScrollCount = 21;
+
+               if (colStart == 0)
+               {
+                       diskSelectorState = DSS_LSB_HIDING;
+                       dxLeft = -8;
+               }
+
+               return;
+       }
+
+       if ((diskSelectorState == DSS_RSB_SHOWING) || (diskSelectorState == DSS_RSB_SHOWN))
+       {
+               colStart++;
+               textScrollCount = -21;
+
+               if ((colStart + 3) == numColumns)
+               {
+                       diskSelectorState = DSS_RSB_HIDING;
+                       dxRight = 8;
+               }
+
                return;
+       }
 
        if (diskSelected != -1)
        {
-               char buffer[2048];
-               sprintf(buffer, "%s/%s", settings.disksPath, &imageList[diskSelected][0]);
-//             floppyDrive.LoadImage(&imageList[diskSelected][0], driveNumber);
-               floppyDrive.LoadImage(buffer, driveNumber);
+               floppyDrive[0].LoadImage(fsList[diskSelected].fullPath.c_str(), driveNumber);
        }
 
        showWindow = false;
@@ -275,32 +598,94 @@ void DiskSelector::MouseUp(int32_t x, int32_t y, uint32_t buttons)
 }
 
 
-#define DS_XPOS        ((VIRTUAL_SCREEN_WIDTH - DS_WIDTH) / 2)
-#define DS_YPOS        ((VIRTUAL_SCREEN_HEIGHT - DS_HEIGHT) / 2)
 void DiskSelector::MouseMove(int32_t x, int32_t y, uint32_t buttons)
 {
        if (!showWindow)
                return;
 
+       // Check to see if DS has been hovered yet, and, if so, set a flag to show
+       // that it has
        if (!entered && ((x >= DS_XPOS) && (x <= (DS_XPOS + DS_WIDTH))
                && (y >= DS_YPOS) && (y <= (DS_YPOS + DS_HEIGHT))))
                entered = true;
 
+       // Check to see if the DS, since being hovered, is now no longer being
+       // hovered
+//N.B.: Should probably make like a 1/2 to 1 second timeout to allow for overshooting the edge of the thing, maybe have the window fade out gradually and let it come back if you enter before it leaves...
        if (entered && ((x < DS_XPOS) || (x > (DS_XPOS + DS_WIDTH))
                || (y < DS_YPOS) || (y > (DS_YPOS + DS_HEIGHT))))
        {
+               diskSelectorState = DSS_HIDDEN;
+               dxLeft = 0;
+               dxRight = 0;
+               rsbPos = DS_WIDTH;
+               lsbPos = -40;
                showWindow = false;
+               refresh = true;
                return;
        }
 
-//     prevDiskSelected = diskSelected;
-       int xChar = (x - DS_XPOS) / FONT_WIDTH;
-       int yChar = (y - DS_YPOS) / FONT_HEIGHT;
-//             int currentX = (count / 27) * 22;
-//             int currentY = (count % 27);
-       diskSelected = ((xChar / 22) * 27) + yChar;
+       // Bail out if the DS hasn't been entered yet
+       if (!entered)
+               return;
+
+/*
+states:
++-----+---------------------+-----+
+|     |                     |     |
+|     |                     |     |
++-----+---------------------+-----+
+ ^           ^                ^
+ |           |                x is here and state is DSS_SHOWN
+ |           x is here and state is DSS_LSB_SHOWING or DSS_RSB_SHOWING
+ x is here and state is DSS_SHOWN
+
+*/
+       if (x < (DS_XPOS + SCROLL_HOT_WIDTH))
+       {
+               if ((colStart > 0) && (diskSelectorState == DSS_SHOWN))
+               {
+                       diskSelectorState = DSS_LSB_SHOWING;
+                       dxLeft = 8;
+               }
+       }
+       else if (x > (DS_XPOS + DS_WIDTH - SCROLL_HOT_WIDTH))
+       {
+               if (((colStart + 3) < numColumns) && (diskSelectorState == DSS_SHOWN))
+               {
+                       diskSelectorState = DSS_RSB_SHOWING;
+                       dxRight = -8;
+               }
+       }
+       else
+       {
+               // Handle the excluded middle  :-P
+               if ((diskSelectorState == DSS_LSB_SHOWING)
+                       || (diskSelectorState == DSS_LSB_SHOWN))
+               {
+                       diskSelectorState = DSS_LSB_HIDING;
+                       dxLeft = -8;
+               }
+               else if ((diskSelectorState == DSS_RSB_SHOWING)
+                       || (diskSelectorState == DSS_RSB_SHOWN))
+               {
+                       diskSelectorState = DSS_RSB_HIDING;
+                       dxRight = 8;
+               }
+       }
 
-       if ((yChar >= 27) || (diskSelected >= (int)imageList.size()))
+       // The -1 terms move the origin to the upper left corner (from 1 in, and 1
+       // down)
+       int xChar = ((x - DS_XPOS) / FONT_WIDTH) - 1;
+       int yChar = ((y - DS_YPOS) / FONT_HEIGHT) - 1;
+       diskSelected = ((xChar / 22) * 27) + yChar + (colStart * 27);
+
+       if ((yChar < 0) || (yChar >= 27)
+               || (diskSelected >= (int)fsList.size())
+               || (diskSelectorState == DSS_LSB_SHOWING)
+               || (diskSelectorState == DSS_LSB_SHOWN)
+               || (diskSelectorState == DSS_RSB_SHOWING)
+               || (diskSelectorState == DSS_RSB_SHOWN))
                diskSelected = -1;
 
        if (diskSelected != lastDiskSelected)
@@ -311,13 +696,64 @@ void DiskSelector::MouseMove(int32_t x, int32_t y, uint32_t buttons)
 }
 
 
-void DiskSelector::HandleSelection(SDL_Renderer * renderer)
+void DiskSelector::HandleGUIState(void)
 {
-//     if (diskSelected == prevDiskSelected)
-//             return;
+       lsbPos += dxLeft;
+       rsbPos += dxRight;
 
-       SDL_UpdateTexture(window, NULL, windowPixels, 128 * sizeof(Uint32));
+       if ((lsbPos > (SCROLL_HOT_WIDTH - 40)) && (diskSelectorState == DSS_LSB_SHOWING))
+       {
+               diskSelectorState = DSS_LSB_SHOWN;
+               lsbPos = SCROLL_HOT_WIDTH - 40;
+               dxLeft = 0;
+       }
+       else if ((lsbPos < -40) && (diskSelectorState == DSS_LSB_HIDING))
+       {
+               diskSelectorState = DSS_SHOWN;
+               lsbPos = -40;
+               dxLeft = 0;
+       }
+       else if ((rsbPos < (DS_WIDTH - SCROLL_HOT_WIDTH)) && (diskSelectorState == DSS_RSB_SHOWING))
+       {
+               diskSelectorState = DSS_RSB_SHOWN;
+               rsbPos = DS_WIDTH - SCROLL_HOT_WIDTH;
+               dxRight = 0;
+       }
+       else if ((rsbPos > DS_WIDTH) && (diskSelectorState == DSS_RSB_HIDING))
+       {
+               diskSelectorState = DSS_SHOWN;
+               rsbPos = DS_WIDTH;
+               dxRight = 0;
+       }
+
+       if (textScrollCount < 0)
+       {
+               textScrollCount += 2;
+
+               if (textScrollCount > 0)
+               {
+                       textScrollCount = 0;
+                       refresh = true;
+               }
+       }
+       else if (textScrollCount > 0)
+       {
+               textScrollCount -= 2;
+
+               if (textScrollCount < 0)
+               {
+                       textScrollCount = 0;
+                       refresh = true;
+               }
+       }
+}
+
+
+void DiskSelector::HandleSelection(SDL_Renderer * renderer)
+{
+       SDL_UpdateTexture(window, NULL, windowPixels, DS_WIDTH * sizeof(Uint32));
        DrawFilenames(renderer);
+       refresh = false;
 }
 
 
@@ -326,12 +762,25 @@ void DiskSelector::Render(SDL_Renderer * renderer)
        if (!(window && showWindow))
                return;
 
-//     HandleSelection(renderer);
-
-       SDL_Rect dst;
-//     dst.x = (VIRTUAL_SCREEN_WIDTH - DS_WIDTH) / 2, dst.y = (VIRTUAL_SCREEN_HEIGHT - DS_HEIGHT) / 2, dst.w = DS_WIDTH, dst.h = DS_HEIGHT;
-       dst.x = DS_XPOS, dst.y = DS_YPOS, dst.w = DS_WIDTH, dst.h = DS_HEIGHT;
+       HandleGUIState();
+
+       if (((diskSelectorState != DSS_LSB_SHOWN)
+               && (diskSelectorState != DSS_RSB_SHOWN)
+               && (diskSelectorState != DSS_SHOWN))
+               || (textScrollCount != 0) || refresh)
+               HandleSelection(renderer);
+
+       // Render scroll arrows (need to figure out why no alpha!)
+       SDL_SetRenderTarget(renderer, window);
+       SDL_Rect dst2 = { 0, ((DS_HEIGHT - 40) / 2), 40, 40 };
+       dst2.x = lsbPos;
+       SDL_RenderCopy(renderer, scrollLeftIcon, NULL, &dst2);
+       SDL_Rect dst3 = { 0, ((DS_HEIGHT - 40) / 2), 40, 40 };
+       dst3.x = rsbPos;
+       SDL_RenderCopy(renderer, scrollRightIcon, NULL, &dst3);
+       SDL_SetRenderTarget(renderer, NULL);
 
+       SDL_Rect dst = { DS_XPOS, DS_YPOS, DS_WIDTH, DS_HEIGHT };
        SDL_RenderCopy(renderer, window, NULL, &dst);
 }