In this post I'm explaining the creation process for my latest tool rGuiIcons. It took me 7 days from tool design to release and I'm listing here how I lived it.
NOTE: I'm adding some details of my life during those days to better illustrate the time management and development, I think it could be interesting for the readers to see the big picture of it.
Earlier this year, by January-February, I decided to add a new feature to raygui, my immediate-mode gui library, I added icons support. I was in the process of developing some tools and I realized that custom icons could really make a difference and make the tools look way better.
I just created a bunch of 16x16 black & white icons with an image editor and coded a small function to convert that image into an array of bytes for easy embedding with my code, the result was ricons. It worked very good with the tools I developed along the year but I kept thinking about a specific tool for raygui-icons creation, after all, I got rGuiLayout, a raygui layouts editor and rGuiStyler, a raygui styles editor. An icons-creator was the missing piece for my tools creation trilogy.
So, I decided to create rGuiIcons
, a simple and easy-to-use raygui icons editor. After several delays in the past, it was the right time to do so.
It was 11am in the morning, bank holidays in Barcelona, and I just went to one of my favourites cafes in Barcelona, Brooklyn Café, a cozy place with nice ambient music. As always, I ordered a coffee latte and a cured-ham baguette-sandwich and I started designing my new tool.
Tool requirements were apparently quite straightforward: Load icons set, choose icon for edition, edit icon and save results. I started designing a couple of code structures to store icons data:
typedef struct GuiIcon {
Image image;
Color *pixels;
Texture texture;
bool reload;
} GuiIcon;
typedef struct GuiIconSet {
unsigned int count;
GuiIcon *icons;
} GuiIconSet;
I soon realized that this approach was over-complicated for my needs, I felt it could be greatly simplified but, for the moment I would focus on the tool interface while looking for a better approximation to the code part.
I began working in the UI layout, defining a simple and organized layout with all required elements for the user. I tried to keep it as minimal as possible. I always like to design on paper and after a couple of tests, I came out with some idea.
Once I had the basic layout concept, I continued with a mockup, a tool screenshot created with a drawing program. This step could probably be skipped but I really like to get a visual image to better evaluate the user experience and the aesthetics of the tool, I really wanted it to be visually pleasant to the user.
Here there are a couple of mockups I created; for this tool I created 5-6 alternatives:
Working on those designs took me almost the whole day, I just took a break at midday to cook an amazing Spaguetti carbonara
! 😜
Usually at nights I like to work while watching some film/series on TV, sometimes I hardly look at TV and sometimes I get captivated by it. That day I was watching Life Is Beautiful from Roberto Benigni, I had already seen that film before, I really liked it despite being quite sad, actually, at some point I changed to another channel to play something on TV that didn't require much of my attention.
Info | Value |
---|---|
Total dev Time | ~7 hours (~3h paper design + ~4h mockups) |
Lines of Code | 0 loc (some design on paper only) |
I started the day working on the layout creation using my rGuiLayout tool, I used the mockup as a tracemap to build the actual layout over it (that's a rGuiLayout
feature), there are some pixel differences due to snapping but it was quite fast.
With rGuiLayout
I also defined controls drawing order and controls name, used to generate the code variables on code exportation. While doing this process I detected a small bug on rGuiLayout
, on the code generation, and I solved it. I exported the rGuiIcons
tool layout to a plain C file, it used a total of 20 gui controls.
I got a basic tools template C file with all boilerplate code required for new tools, that is:
- Tool info header: description, compilation, code-license
- Include, macros and globals required
- Drag and drop over binary logic
- Drag and drop over GUI logic
- Keyboard shortcuts logic
- Basic program flow and draw logic
- Command line info and logic
- File dialogs (OS-dependant and custom)
- About window
That file is only ~550 loc
but it's a great help to have everything properly organized and avoid missing pieces when creating new tools.
That template requires some renamings and some variables definition for the new tool, also defining possible data structs and related functions to operate on that data. It took me a bit to setup everything and to integrate the GUI related code.
Then I started re-thinking the data structures planned the day before (GuiIcon, GuiIconSet). I realized my design could be improved. I decided to stop coding to better think about it (a process I usually do overnight).
I decided to change context and work on a tech comparison between rTexViewer (a tool I just updated some days ago) and Microsoft.Photos, just for fun.
That night I went out for dinner with a friend from Los Angeles who was visiting Barcelona, we went to La Cratera, a great restaurant with quality food and a complete menu for just 15€, a great deal of a meal!
Info | Value |
---|---|
Total dev Time | ~4 hours (~1h layout creation + ~3h code editing) |
Lines of Code | ~670 loc (550 template + 35 gui layout + 85 structs and funcs) |
Coding day. After some thinking I found a solution, I decided to use a similar implementation for icons than the one used for gui style, it uses a global array of properties, accesible and editable with several functions, that way, rGuiStyler
just changes that global array and automatically changes are reflected in real-time in the tool. I used same aproximation for icons; gui icons are stored in a global array that I could just edit and see changes reflected in real-time in the same tool. It was the simpler solution, the only "difficult" part was creating some functions to properly access those icons bit-data properly... piece of cake!
With this new approximation, I could just use raygui built-in functionality to display icons panel logic with GuiToggleGroup()
function, I had to do some changes to raygui
to support that amount of sub-strings but I just improved the library to allow users customize it with simple defines:
// NOTE: Some raygui elements need to be defined before including raygui
#define TEXTSPLIT_MAX_TEXT_LENGTH 4096
#define TEXTSPLIT_MAX_TEXT_ELEMENTS 256
#define TOGGLEGROUP_MAX_ELEMENTS 256
#define RAYGUI_IMPLEMENTATION
#define RAYGUI_SUPPORT_RICONS // Support raygui icons
#include "external/raygui.h" // Required for: IMGUI controls
I initialized GuiToggleGroup()
text string to contain all icons:
// ToggleGroup() text
// NOTE: Every icon requires 6 text characters: "#001#;"
char toggleIconsText[RICON_MAX_ICONS*6] = { 0 };
for (int i = 0; i < RICON_MAX_ICONS; i++)
{
if ((i + 1)%16 == 0) strncpy(toggleIconsText + 6*i, TextFormat("#%03i#\n", i), 6);
else strncpy(toggleIconsText + 6*i, TextFormat("#%03i#;", i), 6);
}
toggleIconsText[RICON_MAX_ICONS*6 - 1] = '\0';
And icons panel drawing was a breeze:
// Draw icons selection panel
selectedIcon = GuiToggleGroup((Rectangle){ anchor01.x + 15, anchor01.y + 70, 18, 18 }, toggleIconsText, selectedIcon);
Icon zoom logic with mouse and painting logic was also quite simple:
// Icon mouse zoom logic
iconEditScale += GetMouseWheelMove();
if (iconEditScale < 2) iconEditScale = 2;
else if (iconEditScale > 16) iconEditScale = 16;
// Icon mouse painting logic
if ((cell.x >= 0) && (cell.y >= 0) && (cell.x < RICON_SIZE) && (cell.y < RICON_SIZE))
{
if (IsMouseButtonDown(MOUSE_LEFT_BUTTON)) GuiSetIconPixel(selectedIcon, (int)cell.x, (int)cell.y);
else if (IsMouseButtonDown(MOUSE_RIGHT_BUTTON)) GuiClearIconPixel(selectedIcon, (int)cell.x, (int)cell.y);
}
Just note that slider zoom logic is inside GuiSliderBar()
, that's the magic of immediate-mode gui interfaces... 😜
GuiSetIconPixel()
and GuiClearIconPixel()
was done inside ricons, some basic bit-math to set-clear bits.
Here the icons editor drawing logic
// Draw icon zoom scale slider
iconEditScale = (int)GuiSliderBar((Rectangle){ anchor01.x + 410, anchor01.y + 110, 180, 10 }, "ZOOM:", TextFormat("x%i", iconEditScale), (float)iconEditScale, 0, 16);
if (iconEditScale < 2) iconEditScale = 2;
else if (iconEditScale > 16) iconEditScale = 16;
// Draw selected icon at selected scale
DrawRectangle(anchor01.x + 365, anchor01.y + 130, 256, 256, Fade(GetColor(GuiGetStyle(DEFAULT, BASE_COLOR_NORMAL)), 0.3f));
GuiDrawIcon(selectedIcon, (Vector2){ anchor01.x + 365 + 128 - RICON_SIZE*iconEditScale/2, anchor01.y + 130 + 128 - RICON_SIZE*iconEditScale/2 }, iconEditScale, GetColor(GuiGetStyle(DEFAULT, TEXT_COLOR_NORMAL)));
// Draw grid (returns selected cell)
cell = GuiGrid((Rectangle){ anchor01.x + 365 + 128 - RICON_SIZE*iconEditScale/2, anchor01.y + 130 + 128 - RICON_SIZE*iconEditScale/2, RICON_SIZE*iconEditScale, RICON_SIZE*iconEditScale }, iconEditScale, 1);
So, I could call it a day! Icon editing was already working and new solution was quite simple!
In the evening I was attending a Creative Coding Meetup and I gave a small presentation about raylib, raygui and the just released rTexViewer tool, it was very nice, with people from several countries showing their creative projects.
Info | Value |
---|---|
Total dev Time | ~5 hours |
Lines of Code | ~110 locs (80 icons panel-editor + 30 ricons functions) |
After getting a working version with the new approximation, I took the Friday with some relax, I just implemented the tool icons (multiple sizes) and the GuiLoadIcon()
logic. I designed a simple rgi
file format to store icons data:
// Style File Structure (.rgi)
// ------------------------------------------------------
// Offset | Size | Type | Description
// ------------------------------------------------------
// 0 | 4 | char | Signature: "rGI "
// 4 | 2 | short | Version: 100
// 6 | 2 | short | reserved
// 8 | 2 | short | Num icons (N)
// 10 | 2 | short | Icons size (Options: 16, 32, 64) (S)
// Icons name id (32 bytes per name id)
// foreach (icon)
// {
// ... | 32 | char | Icon NameId
// }
// Icons data: One bit per pixel, stored as unsigned int array (depends on icon size)
// S*S pixels/32bit per unsigned int = K unsigned int per icon
// foreach (icon)
// {
// ... | K | unsigned int | Icon Data
// }
I also implemented icons cut-copy-paste logic. Some additional functions were required on ricons module: GuiGetIconData()
and GuiSetIconData()
. Logic was quite straightforward:
// Copy button/shortcut logic
if ((GuiButton((Rectangle){ anchor01.x + 115, anchor01.y + 10, 25, 25 }, "#16#")) ||
(IsKeyDown(KEY_LEFT_CONTROL) && IsKeyPressed(KEY_C)))
{
memcpy(iconData, GuiGetIconData(selectedIcon), RICON_DATA_ELEMENTS*sizeof(unsigned int));
strcpy(iconName, guiIconsName[selectedIcon]);
iconDataToCopy = true;
}
// Cut button/shortcut logic
if ((GuiButton((Rectangle){ anchor01.x + 145, anchor01.y + 10, 25, 25 }, "#17#")) ||
(IsKeyDown(KEY_LEFT_CONTROL) && IsKeyPressed(KEY_X)))
{
memcpy(iconData, GuiGetIconData(selectedIcon), RICON_DATA_ELEMENTS*sizeof(unsigned int));
for (int i = 0; i < RICON_SIZE*RICON_SIZE; i++) GuiClearIconPixel(selectedIcon, i/RICON_SIZE, i%RICON_SIZE);
strcpy(iconName, guiIconsName[selectedIcon]);
memset(guiIconsName[selectedIcon], 0, 32);
iconDataToCopy = true;
}
// Paste button/shortcut logic
if ((GuiButton((Rectangle){ anchor01.x + 175, anchor01.y + 10, 25, 25 }, "#18#")) ||
(IsKeyDown(KEY_LEFT_CONTROL) && IsKeyPressed(KEY_V)))
{
if (iconDataToCopy)
{
GuiSetIconData(selectedIcon, iconData);
strcpy(guiIconsName[selectedIcon], iconName);
}
}
On the afternoon, at 5pm, my old students from CEV Barcelona (where I worked for 6 years until I quit last year to work full time on raylib technologies) were presenting their amazing art projects on an open event and I went to see them. It was really great, I met the students and my other teacher friends, after the event we went out for some beers, the party lasted until 2am. I really enjoyed it!
Info | Value |
---|---|
Total dev Time | ~3 hours |
Lines of Code | ~110 locs (40 cut-copy-paste + 70 ricons.h functions) |
I started the day working on .rgi SaveIcons()
function and later I implemented icon name ids logic.
Similar to icon data, I decided to create a global static char guiIconsName[RICON_MAX_ICONS][32]
to store icon name ids... but only internal to rGuiIcons
and .rgi file. That information could be irrelevant when using icons, actually users only need the icon number and maybe an enumerator name for convenience, so I reviewed GuiLoadIcons()
to explicitely ask for that info if required:
// Load raygui icons file (.rgi)
// NOTE: In case nameIds are required, they can be requested with loadIconsName,
// they are returned as a guiIconsName[iconsCount][RICON_MAX_NAME_LENGTH],
// guiIconsName[]][] memory should be manually freed!
char **GuiLoadIcons(const char *fileName, bool loadIconsName);
Icons name id text edition logic was quite simple (thanks to raygui
):
// NOTE: We show icon name id for selected icon
if (GuiTextBox((Rectangle){ anchor01.x + 365, anchor01.y + 70, 260, 25 }, guiIconsName[selectedIcon], 32, iconNameIdEditMode)) iconNameIdEditMode = !iconNameIdEditMode;
Saturday was done. After lunch, I just found Ghostbusters (1984) starting on TV and I get abducted, I've seen that film hundreds of times and every time I found it on TV, I watch it again! In the evening I was meeting some friends from my home-town that were visiting Barcelona and we went to Ciutadella Park and Born District, it was great!
Later that night I started watching a new TV series in Netflix: Better Than Us, about a robot, not bad. It was playing on TV while reviewing and testing some code.
Info | Value |
---|---|
Total dev Time | ~3 hours |
Lines of Code | ~300 locs (80 SaveIcons() + 200 guiIconsName array) |
Sunday dev started on Brooklyn Cafe, it was time to implement all dialogs logic to load/save/export data and related functions. In the past I used tinyfiledialogs library to manage system OS dialogs, it's a great library with simple functions to manage OS dialogs. I used it for all my tools.
A couple of months ago I realized I needed a second options for platforms without OS file dialogs, for example HTML5, Android native, Raspberry Pi native or custom embbedded platforms, so, I created a custom raygui module to deal with that. Main issue was supporting tinyfiledialogs
, with sync execution and program blocking and also supporting an async solution with an immediate-mode gui like raygui.
After some designs I came out with a nice solution, here it an usage example with file loading dialog:
// GUI: Load File Dialog (and loading logic)
//----------------------------------------------------------------------------------------
if (showLoadFileDialog)
{
#if defined(CUSTOM_MODAL_DIALOGS)
int result = GuiFileDialog(DIALOG_MESSAGE, "Load raygui icons file ...", inFileName, "Ok", "Just drag and drop your .rgi style file!");
#else
int result = GuiFileDialog(DIALOG_OPEN, "Load raygui icons file", inFileName, "*.rgi", "raygui Icons Files (*.rgi)");
#endif
if (result == 1)
{
// Load gui icons data (and gui icon names for the tool)
char **tempIconsName = GuiLoadIcons(inFileName, true);
for (int i = 0; i < RICON_MAX_ICONS; i++)
{
strcpy(guiIconsName[i], tempIconsName[i]);
free(tempIconsName[i]);
}
free(tempIconsName);
SetWindowTitle(FormatText("%s v%s - %s", toolName, toolVersion, GetFileName(inFileName)));
saveChangesRequired = false;
}
if (result >= 0) showLoadFileDialog = false;
}
//----------------------------------------------------------------------------------------
GuiFileDialog()
abstracts sync tinyfiledialogs calls and custom raygui based dialogs, when showLoadFileDialog
is enabled, one mechanism or the other (depending on compilation flags) is executed, waiting for a proper result, blocking or not. It works pretty good. Icons saving and exporting used the same mechanism.
Several function were also required for icons data import/export:
// Export gui icons as code (.h)
static void ExportIconsAsCode(const char *fileName);
// Get GRAYSCALE image from and array of bits stored as int (0-BLANK, 1-WHITE)
static Image ImageFromIconData(unsigned int *icons, int iconsCount, int iconsPerLine, int padding);
// Load icons from image file
void LoadIconsFromImage(Image image, int iconsCount, int iconsSize, int iconsPerLine, int padding);
Note that LoadIconsFromImage()
is implemented but actually never used, it requires some image configuration parameters for proper image loading and consequently an additional window layout that should open on image loading, I left that improvement for a future version.
Implementing all this files functionality took me all day. At midday I took a break of a couple of hours to cook an amazing paella, process I documented on twitter. :P
That night I needed to disconect, no more computer, so I watched a film that was on my list: The Imitation Game, I liked it but I missed some more tech information on the film.
Info | Value |
---|---|
Total dev Time | ~8 hours |
Lines of Code | ~300 locs (150 file dialogs usage + 150 load/export functions) |
Monday morning and tool almost ready, just a couple of features left.
First, I started reviewing command line usage logic, for this tool it was pretty simple, just input to output formats conversion and most of the boilerplate code was ready. Just implemented basic logic:
// Process input file if provided
if (inFileName[0] != '\0')
{
// Set a default name for output in case not provided
if (outFileName[0] == '\0') strcpy(outFileName, "output.rgi");
printf("\nInput file: %s", inFileName);
printf("\nOutput file: %s", outFileName);
// Load input file: icons data and name ids
char **tempIconsName = GuiLoadIcons(inFileName, true);
for (int i = 0; i < RICON_MAX_ICONS; i++)
{
strcpy(guiIconsName[i], tempIconsName[i]);
free(tempIconsName[i]);
}
free(tempIconsName);
// Process input --> output
if (IsFileExtension(outFileName, ".rgi")) SaveIcons(outFileName);
else if (IsFileExtension(outFileName, ".png"))
{
Image image = ImageFromIconData(GuiGetIcons(), RICON_MAX_ICONS, 16, 1);
ExportImage(image, outFileName);
UnloadImage(image);
}
else if (IsFileExtension(outFileName, ".h")) ExportIconsAsCode(outFileName);
}
Second, I decided to implement a simple UNDO/REDO system, I had already implemented one in the past using a ring buffer for rGuiLayout
so I decided to use the same mechanism, probably not the most performant one but it was ok.
Some variables were required and I also created a new GuiIconSet
struct for convenience, actually it can be useful for a future review of the tool. Here the initialization:
// Undo system variables
int currentUndoIndex = 0;
int firstUndoIndex = 0;
int lastUndoIndex = 0;
int undoFrameCounter = 0;
GuiIconSet *undoIconSet = (GuiIconSet *)calloc(MAX_UNDO_LEVELS, sizeof(GuiIconSet));
// Init undo system with current icons set
for (int i = 0; i < MAX_UNDO_LEVELS; i++)
{
undoIconSet[i].count = RICON_MAX_ICONS;
undoIconSet[i].iconSize = RICON_SIZE;
undoIconSet[i].values = (unsigned int *)calloc(RICON_MAX_ICONS*RICON_DATA_ELEMENTS, sizeof(unsigned int));
memcpy(undoIconSet[i].values, GuiGetIcons(), RICON_MAX_ICONS*RICON_DATA_ELEMENTS*sizeof(unsigned int));
}
And here the logic placed in the main loop:
// Undo icons change logic
//----------------------------------------------------------------------------------
// Make sure no windows are open to store changes
if (!windowAboutState.windowAboutActive && !windowExitActive && !showLoadFileDialog &&
!showSaveFileDialog && !showExportFileDialog && !showExportIconImageDialog)
{
undoFrameCounter++;
// Every 120 frames we check if current layout has changed and record a new undo state
if (undoFrameCounter >= 120)
{
if (memcmp(undoIconSet[currentUndoIndex].values, GuiGetIcons(), RICON_MAX_ICONS*RICON_DATA_ELEMENTS*sizeof(unsigned int)) != 0)
{
// Move cursor to next available position to record undo
currentUndoIndex++;
if (currentUndoIndex >= MAX_UNDO_LEVELS) currentUndoIndex = 0;
if (currentUndoIndex == firstUndoIndex) firstUndoIndex++;
if (firstUndoIndex >= MAX_UNDO_LEVELS) firstUndoIndex = 0;
memcpy(undoIconSet[currentUndoIndex].values, GuiGetIcons(), RICON_MAX_ICONS*RICON_DATA_ELEMENTS*sizeof(unsigned int));
lastUndoIndex = currentUndoIndex;
// Set a '*' mark on loaded file name to notice save requirement
if ((inFileName[0] != '\0') && !saveChangesRequired)
{
SetWindowTitle(FormatText("%s v%s - %s*", toolName, toolVersion, GetFileName(inFileName)));
saveChangesRequired = true;
}
}
undoFrameCounter = 0;
}
}
else undoFrameCounter = 120;
// Recover previous layout state from buffer
if (IsKeyDown(KEY_LEFT_CONTROL) && IsKeyPressed(KEY_Z))
{
if (currentUndoIndex != firstUndoIndex)
{
currentUndoIndex--;
if (currentUndoIndex < 0) currentUndoIndex = MAX_UNDO_LEVELS - 1;
if (memcmp(undoIconSet[currentUndoIndex].values, GuiGetIcons(), RICON_MAX_ICONS*RICON_DATA_ELEMENTS*sizeof(unsigned int)) != 0)
{
// Restore previous icons state
// NOTE: GuiGetIcons() returns a pointer to internal data
memcpy(GuiGetIcons(), undoIconSet[currentUndoIndex].values, RICON_MAX_ICONS*RICON_DATA_ELEMENTS*sizeof(unsigned int));
}
}
}
// Recover next layout state from buffer
if (IsKeyDown(KEY_LEFT_CONTROL) && IsKeyPressed(KEY_Y))
{
if (currentUndoIndex != lastUndoIndex)
{
int nextUndoIndex = currentUndoIndex + 1;
if (nextUndoIndex >= MAX_UNDO_LEVELS) nextUndoIndex = 0;
if (nextUndoIndex != firstUndoIndex)
{
currentUndoIndex = nextUndoIndex;
if (memcmp(undoIconSet[currentUndoIndex].values, GuiGetIcons(), RICON_MAX_ICONS*RICON_DATA_ELEMENTS*sizeof(unsigned int)) != 0)
{
memcpy(GuiGetIcons(), undoIconSet[currentUndoIndex].values, RICON_MAX_ICONS*RICON_DATA_ELEMENTS*sizeof(unsigned int));
}
}
}
}
//----------------------------------------------------------------------------------
It worked like a charm!
Implementing all that code for command line and undo/redo took me about 2 hours, I usually test the functions as I'm creating them, I did some test to verify everything was working fine. It was.
Finally I created the README.txt
(also using a template) and the banners/logos for the tool itch.io page. Tool was released by the end of the day.
That night I started watching a new Netflix series: Marianne, a french horror series about a witch, I don't specially like horror series but it was nice, quite scary! 😱
Info | Value |
---|---|
Total dev Time | ~6 hours |
Lines of Code | ~120 locs (30 command line + 90 undo/redo logic) |
Personally I'm very happy with rGuiIcons
and the overall integration with raygui and the other tools for tools development.
It was developed in time record and during the process I also improved raygui
and ricons
, every new tool I create I improve the templates, the tools and the overall process. It let me create more tools faster and more efficiently.
Info | Value |
---|---|
TOTAL DEV TIME | ~36 hours |
TOTAL LINES OF CODE | ~1450 locs (~1300 rGuiIcons + ~150 ricons) |
Please, feel free to comment! Thanks for reading!
This article is licensed as CC BY-SA 4.0, which is one of the licenses in the Creative Commons family.