changeset 116:7bd795bac997

[Postdog] Added scrollable area to inputs and history files, buttons to delete and view.
author June Park <parkjune1995@gmail.com>
date Wed, 07 Jan 2026 04:52:17 -0800
parents 96db6c3f38d6
children b91f2dd6f84d
files .hgignore mrjunejune/src/public/epi_all_colors.png postdog/BUILD postdog/epi_all_colors.png postdog/gui_window_file_dialog.h postdog/main.c postdog/slider_example.c third_party/raylib/custom.c third_party/raylib/include/raygui.h
diffstat 9 files changed, 1105 insertions(+), 65 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore	Tue Jan 06 08:19:07 2026 -0800
+++ b/.hgignore	Wed Jan 07 04:52:17 2026 -0800
@@ -21,6 +21,9 @@
 # Playground
 playground/
 
+# Postdog
+postdog/history
+
 # Node
 **/node_modules/**
 
Binary file mrjunejune/src/public/epi_all_colors.png has changed
--- a/postdog/BUILD	Tue Jan 06 08:19:07 2026 -0800
+++ b/postdog/BUILD	Wed Jan 07 04:52:17 2026 -0800
@@ -2,6 +2,18 @@
 load("//gui_ze:gui_ze.bzl", "macos_app_and_dmg")
 
 raylib_binary(
+  name = "slider_example",
+  srcs = [
+    "slider_example.c",
+    "gui_window_file_dialog.h",
+  ],
+  deps = [
+    "//third_party/raylib:raylib",
+    "//dowa:dowa",
+  ],
+)
+
+raylib_binary(
   name = "postdog",
   srcs = [
     "main.c",
@@ -42,5 +54,6 @@
   srcs = glob([
       "**/*.ttf",
       "**/*.txt",
+      "**/*.png",
   ], allow_empty=True)
 )
Binary file postdog/epi_all_colors.png has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/postdog/gui_window_file_dialog.h	Wed Jan 07 04:52:17 2026 -0800
@@ -0,0 +1,624 @@
+/*******************************************************************************************
+*
+*   Window File Dialog v1.2 - Modal file dialog to open/save files
+*
+*   MODULE USAGE:
+*       #define GUI_WINDOW_FILE_DIALOG_IMPLEMENTATION
+*       #include "gui_window_file_dialog.h"
+*
+*       INIT: GuiWindowFileDialogState state = GuiInitWindowFileDialog();
+*       DRAW: GuiWindowFileDialog(&state);
+*
+*   NOTE: This module depends on some raylib file system functions:
+*       - LoadDirectoryFiles()
+*       - UnloadDirectoryFiles()
+*       - GetWorkingDirectory()
+*       - DirectoryExists()
+*       - FileExists()
+*
+*   LICENSE: zlib/libpng
+*
+*   Copyright (c) 2019-2024 Ramon Santamaria (@raysan5)
+*
+*   This software is provided "as-is", without any express or implied warranty. In no event
+*   will the authors be held liable for any damages arising from the use of this software.
+*
+*   Permission is granted to anyone to use this software for any purpose, including commercial
+*   applications, and to alter it and redistribute it freely, subject to the following restrictions:
+*
+*     1. The origin of this software must not be misrepresented; you must not claim that you
+*     wrote the original software. If you use this software in a product, an acknowledgment
+*     in the product documentation would be appreciated but is not required.
+*
+*     2. Altered source versions must be plainly marked as such, and must not be misrepresented
+*     as being the original software.
+*
+*     3. This notice may not be removed or altered from any source distribution.
+*
+**********************************************************************************************/
+
+#include "third_party/raylib/include/raylib.h"
+
+#ifndef GUI_WINDOW_FILE_DIALOG_H
+#define GUI_WINDOW_FILE_DIALOG_H
+
+// Gui file dialog context data
+typedef struct {
+
+    // Window management variables
+    bool windowActive;
+    Rectangle windowBounds;
+    Vector2 panOffset;
+    bool dragMode;
+    bool supportDrag;
+
+    // UI variables
+    bool dirPathEditMode;
+    char dirPathText[1024];
+
+    int filesListScrollIndex;
+    bool filesListEditMode;
+    int filesListActive;
+
+    bool fileNameEditMode;
+    char fileNameText[1024];
+    bool SelectFilePressed;
+    bool CancelFilePressed;
+    int fileTypeActive;
+    int itemFocused;
+
+    // Custom state variables
+    FilePathList dirFiles;
+    char filterExt[256];
+    char dirPathTextCopy[1024];
+    char fileNameTextCopy[1024];
+
+    int prevFilesListActive;
+
+    bool saveFileMode;
+
+} GuiWindowFileDialogState;
+
+#ifdef __cplusplus
+extern "C" {            // Prevents name mangling of functions
+#endif
+
+//----------------------------------------------------------------------------------
+// Defines and Macros
+//----------------------------------------------------------------------------------
+//...
+
+//----------------------------------------------------------------------------------
+// Types and Structures Definition
+//----------------------------------------------------------------------------------
+// ...
+
+//----------------------------------------------------------------------------------
+// Global Variables Definition
+//----------------------------------------------------------------------------------
+//...
+
+//----------------------------------------------------------------------------------
+// Module Functions Declaration
+//----------------------------------------------------------------------------------
+GuiWindowFileDialogState InitGuiWindowFileDialog(const char *initPath);
+void GuiWindowFileDialog(GuiWindowFileDialogState *state);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // GUI_WINDOW_FILE_DIALOG_H
+
+/***********************************************************************************
+*
+*   GUI_WINDOW_FILE_DIALOG IMPLEMENTATION
+*
+************************************************************************************/
+#if defined(GUI_WINDOW_FILE_DIALOG_IMPLEMENTATION)
+#include "third_party/raylib/include/raygui.h"
+
+#include <string.h>     // Required for: strcpy()
+
+//----------------------------------------------------------------------------------
+// Defines and Macros
+//----------------------------------------------------------------------------------
+#define MAX_DIRECTORY_FILES    2048
+#define MAX_ICON_PATH_LENGTH    512
+#ifdef _WIN32
+#define PATH_SEPERATOR "\\"
+#else
+#define PATH_SEPERATOR "/"
+#endif
+
+//----------------------------------------------------------------------------------
+// Types and Structures Definition
+//----------------------------------------------------------------------------------
+#if defined(USE_CUSTOM_LISTVIEW_FILEINFO)
+// Detailed file info type
+typedef struct FileInfo {
+    const char *name;
+    int size;
+    int modTime;
+    int type;
+    int icon;
+} FileInfo;
+#else
+// Filename only
+typedef char *FileInfo;             // Files are just a path string
+#endif
+
+//----------------------------------------------------------------------------------
+// Global Variables Definition
+//----------------------------------------------------------------------------------
+FileInfo *dirFilesIcon = NULL;      // Path string + icon (for fancy drawing)
+
+//----------------------------------------------------------------------------------
+// Internal Module Functions Definition
+//----------------------------------------------------------------------------------
+// Read files in new path
+static void ReloadDirectoryFiles(GuiWindowFileDialogState *state);
+
+#if defined(USE_CUSTOM_LISTVIEW_FILEINFO)
+// List View control for files info with extended parameters
+static int GuiListViewFiles(Rectangle bounds, FileInfo *files, int count, int *focus, int *scrollIndex, int active);
+#endif
+
+//----------------------------------------------------------------------------------
+// Module Functions Definition
+//----------------------------------------------------------------------------------
+GuiWindowFileDialogState InitGuiWindowFileDialog(const char *initPath)
+{
+    GuiWindowFileDialogState state = { 0 };
+
+    // Init window data
+    state.windowBounds = (Rectangle){ GetScreenWidth()/2 - 440/2, GetScreenHeight()/2 - 310/2, 440, 310 };
+    state.windowActive = false;
+    state.supportDrag = true;
+    state.dragMode = false;
+    state.panOffset = (Vector2){ 0, 0 };
+
+    // Init path data
+    state.dirPathEditMode = false;
+    state.filesListActive = -1;
+    state.prevFilesListActive = state.filesListActive;
+    state.filesListScrollIndex = 0;
+
+    state.fileNameEditMode = false;
+
+    state.SelectFilePressed = false;
+    state.CancelFilePressed = false;
+
+    state.fileTypeActive = 0;
+
+    strcpy(state.fileNameText, "\0");
+
+    // Custom variables initialization
+    if (initPath && DirectoryExists(initPath))
+    {
+        strcpy(state.dirPathText, initPath);
+    }
+    else if (initPath && FileExists(initPath))
+    {
+        strcpy(state.dirPathText, GetDirectoryPath(initPath));
+        strcpy(state.fileNameText, GetFileName(initPath));
+    }
+    else strcpy(state.dirPathText, GetWorkingDirectory());
+
+    // TODO: Why we keep a copy?
+    strcpy(state.dirPathTextCopy, state.dirPathText);
+    strcpy(state.fileNameTextCopy, state.fileNameText);
+
+    state.filterExt[0] = '\0';
+    //strcpy(state.filterExt, "all");
+
+    state.dirFiles.count = 0;
+
+    return state;
+}
+
+// Update and draw file dialog
+void GuiWindowFileDialog(GuiWindowFileDialogState *state)
+{
+    if (state->windowActive)
+    {
+        // Update window dragging
+        //----------------------------------------------------------------------------------------
+        if (state->supportDrag)
+        {
+            Vector2 mousePosition = GetMousePosition();
+
+            if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
+            {
+                // Window can be dragged from the top window bar
+                if (CheckCollisionPointRec(mousePosition, (Rectangle){ state->windowBounds.x, state->windowBounds.y, (float)state->windowBounds.width, RAYGUI_WINDOWBOX_STATUSBAR_HEIGHT }))
+                {
+                    state->dragMode = true;
+                    state->panOffset.x = mousePosition.x - state->windowBounds.x;
+                    state->panOffset.y = mousePosition.y - state->windowBounds.y;
+                }
+            }
+
+            if (state->dragMode)
+            {
+                state->windowBounds.x = (mousePosition.x - state->panOffset.x);
+                state->windowBounds.y = (mousePosition.y - state->panOffset.y);
+
+                // Check screen limits to avoid moving out of screen
+                if (state->windowBounds.x < 0) state->windowBounds.x = 0;
+                else if (state->windowBounds.x > (GetScreenWidth() - state->windowBounds.width)) state->windowBounds.x = GetScreenWidth() - state->windowBounds.width;
+
+                if (state->windowBounds.y < 0) state->windowBounds.y = 0;
+                else if (state->windowBounds.y > (GetScreenHeight() - state->windowBounds.height)) state->windowBounds.y = GetScreenHeight() - state->windowBounds.height;
+
+                if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) state->dragMode = false;
+            }
+        }
+        //----------------------------------------------------------------------------------------
+
+        // Load dirFilesIcon and state->dirFiles lazily on windows open
+        // NOTE: They are automatically unloaded at fileDialog closing
+        //----------------------------------------------------------------------------------------
+        if (dirFilesIcon == NULL)
+        {
+            dirFilesIcon = (FileInfo *)RL_CALLOC(MAX_DIRECTORY_FILES, sizeof(FileInfo));    // Max files to read
+            for (int i = 0; i < MAX_DIRECTORY_FILES; i++) dirFilesIcon[i] = (char *)RL_CALLOC(MAX_ICON_PATH_LENGTH, 1);    // Max file name length
+        }
+
+        // Load current directory files
+        if (state->dirFiles.paths == NULL) ReloadDirectoryFiles(state);
+        //----------------------------------------------------------------------------------------
+
+        // Draw window and controls
+        //----------------------------------------------------------------------------------------
+        state->windowActive = !GuiWindowBox(state->windowBounds, "#198# Select File Dialog");
+
+        // Draw previous directory button + logic
+        if (GuiButton((Rectangle){ state->windowBounds.x + state->windowBounds.width - 48, state->windowBounds.y + 24 + 12, 40, 24 }, "< .."))
+        {
+            // Move dir path one level up
+            strcpy(state->dirPathText, GetPrevDirectoryPath(state->dirPathText));
+
+            // Reload directory files (frees previous list)
+            ReloadDirectoryFiles(state);
+
+            state->filesListActive = -1;
+            memset(state->fileNameText, 0, 1024);
+            memset(state->fileNameTextCopy, 0, 1024);
+        }
+
+        // Draw current directory text box info + path editing logic
+        if (GuiTextBox((Rectangle){ state->windowBounds.x + 8, state->windowBounds.y + 24 + 12, state->windowBounds.width - 48 - 16, 24 }, state->dirPathText, 1024, state->dirPathEditMode))
+        {
+            if (state->dirPathEditMode)
+            {
+                // Verify if a valid path has been introduced
+                if (DirectoryExists(state->dirPathText))
+                {
+                    // Reload directory files (frees previous list)
+                    ReloadDirectoryFiles(state);
+
+                    strcpy(state->dirPathTextCopy, state->dirPathText);
+                }
+                else strcpy(state->dirPathText, state->dirPathTextCopy);
+            }
+
+            state->dirPathEditMode = !state->dirPathEditMode;
+        }
+
+        // List view elements are aligned left
+        int prevTextAlignment = GuiGetStyle(LISTVIEW, TEXT_ALIGNMENT);
+        int prevElementsHeight = GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT);
+        GuiSetStyle(LISTVIEW, TEXT_ALIGNMENT, TEXT_ALIGN_LEFT);
+        GuiSetStyle(LISTVIEW, LIST_ITEMS_HEIGHT, 24);
+# if defined(USE_CUSTOM_LISTVIEW_FILEINFO)
+        state->filesListActive = GuiListViewFiles((Rectangle){ state->position.x + 8, state->position.y + 48 + 20, state->windowBounds.width - 16, state->windowBounds.height - 60 - 16 - 68 }, fileInfo, state->dirFiles.count, &state->itemFocused, &state->filesListScrollIndex, state->filesListActive);
+# else
+        GuiListViewEx((Rectangle){ state->windowBounds.x + 8, state->windowBounds.y + 48 + 20, state->windowBounds.width - 16, state->windowBounds.height - 60 - 16 - 68 }, 
+                      (const char**)dirFilesIcon, state->dirFiles.count, &state->filesListScrollIndex, &state->filesListActive, &state->itemFocused);
+# endif
+        GuiSetStyle(LISTVIEW, TEXT_ALIGNMENT, prevTextAlignment);
+        GuiSetStyle(LISTVIEW, LIST_ITEMS_HEIGHT, prevElementsHeight);
+
+        // Check if a path has been selected, if it is a directory, move to that directory (and reload paths)
+        if ((state->filesListActive >= 0) && (state->filesListActive != state->prevFilesListActive))
+            //&& (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) || IsKeyPressed(KEY_ENTER) || IsKeyPressed(KEY_DPAD_A)))
+        {
+            strcpy(state->fileNameText, GetFileName(state->dirFiles.paths[state->filesListActive]));
+
+            if (DirectoryExists(TextFormat("%s/%s", state->dirPathText, state->fileNameText)))
+            {
+                if (TextIsEqual(state->fileNameText, "..")) strcpy(state->dirPathText, GetPrevDirectoryPath(state->dirPathText));
+                else strcpy(state->dirPathText, TextFormat("%s/%s", (strcmp(state->dirPathText, "/") == 0)? "" : state->dirPathText, state->fileNameText));
+
+                strcpy(state->dirPathTextCopy, state->dirPathText);
+
+                // Reload directory files (frees previous list)
+                ReloadDirectoryFiles(state);
+
+                strcpy(state->dirPathTextCopy, state->dirPathText);
+
+                state->filesListActive = -1;
+                strcpy(state->fileNameText, "\0");
+                strcpy(state->fileNameTextCopy, state->fileNameText);
+            }
+
+            state->prevFilesListActive = state->filesListActive;
+        }
+
+        // Draw bottom controls
+        //--------------------------------------------------------------------------------------
+        GuiLabel((Rectangle){ state->windowBounds.x + 8, state->windowBounds.y + state->windowBounds.height - 68, 60, 24 }, "File name:");
+        if (GuiTextBox((Rectangle){ state->windowBounds.x + 72, state->windowBounds.y + state->windowBounds.height - 68, state->windowBounds.width - 184, 24 }, state->fileNameText, 128, state->fileNameEditMode))
+        {
+            if (*state->fileNameText)
+            {
+                // Verify if a valid filename has been introduced
+                if (FileExists(TextFormat("%s/%s", state->dirPathText, state->fileNameText)))
+                {
+                    // Select filename from list view
+                    for (unsigned int i = 0; i < state->dirFiles.count; i++)
+                    {
+                        if (TextIsEqual(state->fileNameText, state->dirFiles.paths[i]))
+                        {
+                            state->filesListActive = i;
+                            strcpy(state->fileNameTextCopy, state->fileNameText);
+                            break;
+                        }
+                    }
+                }
+                else if (!state->saveFileMode)
+                {
+                    strcpy(state->fileNameText, state->fileNameTextCopy);
+                }
+            }
+
+            state->fileNameEditMode = !state->fileNameEditMode;
+        }
+
+        GuiLabel((Rectangle){ state->windowBounds.x + 8, state->windowBounds.y + state->windowBounds.height - 24 - 12, 68, 24 }, "File filter:");
+        GuiComboBox((Rectangle){ state->windowBounds.x + 72, state->windowBounds.y + state->windowBounds.height - 24 - 12, state->windowBounds.width - 184, 24 }, "All files", &state->fileTypeActive);
+
+        state->SelectFilePressed = GuiButton((Rectangle){ state->windowBounds.x + state->windowBounds.width - 96 - 8, state->windowBounds.y + state->windowBounds.height - 68, 96, 24 }, "Select");
+
+        if (GuiButton((Rectangle){ state->windowBounds.x + state->windowBounds.width - 96 - 8, state->windowBounds.y + state->windowBounds.height - 24 - 12, 96, 24 }, "Cancel")) state->windowActive = false;
+        //--------------------------------------------------------------------------------------
+
+        // Exit on file selected
+        if (state->SelectFilePressed) state->windowActive = false;
+
+        // File dialog has been closed, free all memory before exit
+        if (!state->windowActive)
+        {
+            // Free dirFilesIcon memory
+            for (int i = 0; i < MAX_DIRECTORY_FILES; i++) RL_FREE(dirFilesIcon[i]);
+
+            RL_FREE(dirFilesIcon);
+            dirFilesIcon = NULL;
+
+            // Unload directory file paths
+            UnloadDirectoryFiles(state->dirFiles);
+
+            // Reset state variables
+            state->dirFiles.count = 0;
+            state->dirFiles.capacity = 0;
+            state->dirFiles.paths = NULL;
+        }
+    }
+}
+
+// Compare two files from a directory
+static inline int FileCompare(const char *d1, const char *d2, const char *dir)
+{
+    const bool b1 = DirectoryExists(TextFormat("%s/%s", dir, d1));
+    const bool b2 = DirectoryExists(TextFormat("%s/%s", dir, d2));
+
+    if (b1 && !b2) return -1;
+    if (!b1 && b2) return 1;
+
+    if (!FileExists(TextFormat("%s/%s", dir, d1))) return 1;
+    if (!FileExists(TextFormat("%s/%s", dir, d2))) return -1;
+
+    return strcmp(d1, d2);
+}
+
+// Read files in new path
+static void ReloadDirectoryFiles(GuiWindowFileDialogState *state)
+{
+    UnloadDirectoryFiles(state->dirFiles);
+
+    state->dirFiles = LoadDirectoryFilesEx(state->dirPathText, (state->filterExt[0] == '\0')? NULL : state->filterExt, false);
+    state->itemFocused = 0;
+
+    // Reset dirFilesIcon memory
+    for (int i = 0; i < MAX_DIRECTORY_FILES; i++) memset(dirFilesIcon[i], 0, MAX_ICON_PATH_LENGTH);
+
+    // Copy paths as icon + fileNames into dirFilesIcon
+    for (unsigned int i = 0; i < state->dirFiles.count; i++)
+    {
+        if (IsPathFile(state->dirFiles.paths[i]))
+        {
+            // Path is a file, a file icon for convenience (for some recognized extensions)
+            if (IsFileExtension(state->dirFiles.paths[i], ".png;.bmp;.tga;.gif;.jpg;.jpeg;.psd;.hdr;.qoi;.dds;.pkm;.ktx;.pvr;.astc"))
+            {
+                strcpy(dirFilesIcon[i], TextFormat("#12#%s", GetFileName(state->dirFiles.paths[i])));
+            }
+            else if (IsFileExtension(state->dirFiles.paths[i], ".wav;.mp3;.ogg;.flac;.xm;.mod;.it;.wma;.aiff"))
+            {
+                strcpy(dirFilesIcon[i], TextFormat("#11#%s", GetFileName(state->dirFiles.paths[i])));
+            }
+            else if (IsFileExtension(state->dirFiles.paths[i], ".txt;.info;.md;.nfo;.xml;.json;.c;.cpp;.cs;.lua;.py;.glsl;.vs;.fs"))
+            {
+                strcpy(dirFilesIcon[i], TextFormat("#10#%s", GetFileName(state->dirFiles.paths[i])));
+            }
+            else if (IsFileExtension(state->dirFiles.paths[i], ".exe;.bin;.raw;.msi"))
+            {
+                strcpy(dirFilesIcon[i], TextFormat("#200#%s", GetFileName(state->dirFiles.paths[i])));
+            }
+            else strcpy(dirFilesIcon[i], TextFormat("#218#%s", GetFileName(state->dirFiles.paths[i])));
+        }
+        else
+        {
+            // Path is a directory, add a directory icon
+            strcpy(dirFilesIcon[i], TextFormat("#1#%s", GetFileName(state->dirFiles.paths[i])));
+        }
+    }
+}
+
+#if defined(USE_CUSTOM_LISTVIEW_FILEINFO)
+// List View control for files info with extended parameters
+static int GuiListViewFiles(Rectangle bounds, FileInfo *files, int count, int *focus, int *scrollIndex, int *active)
+{
+    int result = 0;
+    GuiState state = guiState;
+    int itemFocused = (focus == NULL)? -1 : *focus;
+    int itemSelected = *active;
+
+    // Check if we need a scroll bar
+    bool useScrollBar = false;
+    if ((GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT) + GuiGetStyle(LISTVIEW, LIST_ITEMS_PADDING))*count > bounds.height) useScrollBar = true;
+
+    // Define base item rectangle [0]
+    Rectangle itemBounds = { 0 };
+    itemBounds.x = bounds.x + GuiGetStyle(LISTVIEW, LIST_ITEMS_PADDING);
+    itemBounds.y = bounds.y + GuiGetStyle(LISTVIEW, LIST_ITEMS_PADDING) + GuiGetStyle(DEFAULT, BORDER_WIDTH);
+    itemBounds.width = bounds.width - 2*GuiGetStyle(LISTVIEW, LIST_ITEMS_PADDING) - GuiGetStyle(DEFAULT, BORDER_WIDTH);
+    itemBounds.height = GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT);
+    if (useScrollBar) itemBounds.width -= GuiGetStyle(LISTVIEW, SCROLLBAR_WIDTH);
+
+    // Get items on the list
+    int visibleItems = bounds.height/(GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT) + GuiGetStyle(LISTVIEW, LIST_ITEMS_PADDING));
+    if (visibleItems > count) visibleItems = count;
+
+    int startIndex = (scrollIndex == NULL)? 0 : *scrollIndex;
+    if ((startIndex < 0) || (startIndex > (count - visibleItems))) startIndex = 0;
+    int endIndex = startIndex + visibleItems;
+
+    // Update control
+    //--------------------------------------------------------------------
+    if ((state != GUI_STATE_DISABLED) && !guiLocked)
+    {
+        Vector2 mousePoint = GetMousePosition();
+
+        // Check mouse inside list view
+        if (CheckCollisionPointRec(mousePoint, bounds))
+        {
+            state = GUI_STATE_FOCUSED;
+
+            // Check focused and selected item
+            for (int i = 0; i < visibleItems; i++)
+            {
+                if (CheckCollisionPointRec(mousePoint, itemBounds))
+                {
+                    itemFocused = startIndex + i;
+                    if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) itemSelected = startIndex + i;
+                    break;
+                }
+
+                // Update item rectangle y position for next item
+                itemBounds.y += (GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT) + GuiGetStyle(LISTVIEW, LIST_ITEMS_PADDING));
+            }
+
+            if (useScrollBar)
+            {
+                int wheelMove = GetMouseWheelMove();
+                startIndex -= wheelMove;
+
+                if (startIndex < 0) startIndex = 0;
+                else if (startIndex > (count - visibleItems)) startIndex = count - visibleItems;
+
+                endIndex = startIndex + visibleItems;
+                if (endIndex > count) endIndex = count;
+            }
+        }
+        else itemFocused = -1;
+
+        // Reset item rectangle y to [0]
+        itemBounds.y = bounds.y + GuiGetStyle(LISTVIEW, LIST_ITEMS_PADDING) + GuiGetStyle(DEFAULT, BORDER_WIDTH);
+    }
+    //--------------------------------------------------------------------
+
+    // Draw control
+    //--------------------------------------------------------------------
+    DrawRectangleRec(bounds, GetColor(GuiGetStyle(DEFAULT, BACKGROUND_COLOR)));     // Draw background
+    DrawRectangleLinesEx(bounds, GuiGetStyle(DEFAULT, BORDER_WIDTH), Fade(GetColor(GuiGetStyle(LISTVIEW, BORDER + state*3)), guiAlpha));
+
+    // TODO: Draw list view header with file sections: icon+name | size | type | modTime
+
+    // Draw visible items
+    for (int i = 0; i < visibleItems; i++)
+    {
+        if (state == GUI_STATE_DISABLED)
+        {
+            if ((startIndex + i) == itemSelected)
+            {
+                DrawRectangleRec(itemBounds, Fade(GetColor(GuiGetStyle(LISTVIEW, BASE_COLOR_DISABLED)), guiAlpha));
+                DrawRectangleLinesEx(itemBounds, GuiGetStyle(LISTVIEW, BORDER_WIDTH), Fade(GetColor(GuiGetStyle(LISTVIEW, BORDER_COLOR_DISABLED)), guiAlpha));
+            }
+
+            // TODO: Draw full file info line: icon+name | size | type | modTime
+
+            GuiDrawText(files[startIndex + i].name, GetTextBounds(DEFAULT, itemBounds), GuiGetStyle(LISTVIEW, TEXT_ALIGNMENT), Fade(GetColor(GuiGetStyle(LISTVIEW, TEXT_COLOR_DISABLED)), guiAlpha));
+        }
+        else
+        {
+            if ((startIndex + i) == itemSelected)
+            {
+                // Draw item selected
+                DrawRectangleRec(itemBounds, Fade(GetColor(GuiGetStyle(LISTVIEW, BASE_COLOR_PRESSED)), guiAlpha));
+                DrawRectangleLinesEx(itemBounds, GuiGetStyle(LISTVIEW, BORDER_WIDTH), Fade(GetColor(GuiGetStyle(LISTVIEW, BORDER_COLOR_PRESSED)), guiAlpha));
+
+                GuiDrawText(files[startIndex + i].name, GetTextBounds(DEFAULT, itemBounds), GuiGetStyle(LISTVIEW, TEXT_ALIGNMENT), Fade(GetColor(GuiGetStyle(LISTVIEW, TEXT_COLOR_PRESSED)), guiAlpha));
+            }
+            else if ((startIndex + i) == itemFocused)
+            {
+                // Draw item focused
+                DrawRectangleRec(itemBounds, Fade(GetColor(GuiGetStyle(LISTVIEW, BASE_COLOR_FOCUSED)), guiAlpha));
+                DrawRectangleLinesEx(itemBounds, GuiGetStyle(LISTVIEW, BORDER_WIDTH), Fade(GetColor(GuiGetStyle(LISTVIEW, BORDER_COLOR_FOCUSED)), guiAlpha));
+
+                GuiDrawText(files[startIndex + i].name, GetTextBounds(DEFAULT, itemBounds), GuiGetStyle(LISTVIEW, TEXT_ALIGNMENT), Fade(GetColor(GuiGetStyle(LISTVIEW, TEXT_COLOR_FOCUSED)), guiAlpha));
+            }
+            else
+            {
+                // Draw item normal
+                GuiDrawText(files[startIndex + i].name, GetTextBounds(DEFAULT, itemBounds), GuiGetStyle(LISTVIEW, TEXT_ALIGNMENT), Fade(GetColor(GuiGetStyle(LISTVIEW, TEXT_COLOR_NORMAL)), guiAlpha));
+            }
+        }
+
+        // Update item rectangle y position for next item
+        itemBounds.y += (GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT) + GuiGetStyle(LISTVIEW, LIST_ITEMS_PADDING));
+    }
+
+    if (useScrollBar)
+    {
+        Rectangle scrollBarBounds = {
+            bounds.x + bounds.width - GuiGetStyle(LISTVIEW, BORDER_WIDTH) - GuiGetStyle(LISTVIEW, SCROLLBAR_WIDTH),
+            bounds.y + GuiGetStyle(LISTVIEW, BORDER_WIDTH), (float)GuiGetStyle(LISTVIEW, SCROLLBAR_WIDTH),
+            bounds.height - 2*GuiGetStyle(DEFAULT, BORDER_WIDTH)
+        };
+
+        // Calculate percentage of visible items and apply same percentage to scrollbar
+        float percentVisible = (float)(endIndex - startIndex)/count;
+        float sliderSize = bounds.height*percentVisible;
+
+        int prevSliderSize = GuiGetStyle(SCROLLBAR, SLIDER_WIDTH);  // Save default slider size
+        int prevScrollSpeed = GuiGetStyle(SCROLLBAR, SCROLL_SPEED); // Save default scroll speed
+        GuiSetStyle(SCROLLBAR, SLIDER_WIDTH, sliderSize);           // Change slider size
+        GuiSetStyle(SCROLLBAR, SCROLL_SPEED, count - visibleItems); // Change scroll speed
+
+        startIndex = GuiScrollBar(scrollBarBounds, startIndex, 0, count - visibleItems);
+
+        GuiSetStyle(SCROLLBAR, SCROLL_SPEED, prevScrollSpeed); // Reset scroll speed to default
+        GuiSetStyle(SCROLLBAR, SLIDER_WIDTH, prevSliderSize);  // Reset slider size to default
+    }
+    //--------------------------------------------------------------------
+
+    if (focus != NULL) *focus = itemFocused;
+    if (scrollIndex != NULL) *scrollIndex = startIndex;
+
+    *active = itemSelected;
+    return result;
+}
+#endif // USE_CUSTOM_LISTVIEW_FILEINFO
+
+#endif // GUI_FILE_DIALOG_IMPLEMENTATION
--- a/postdog/main.c	Tue Jan 06 08:19:07 2026 -0800
+++ b/postdog/main.c	Wed Jan 07 04:52:17 2026 -0800
@@ -16,19 +16,13 @@
 
 #define SCREEN_WIDTH 1280
 #define SCREEN_HEIGHT 780
-#define TEXT_SIZE 10
-
-#define JSON_INPUT_BUFFER_LEN 8192
-#define HEADER_INPUT_BUFFER_LEN 4096
-#define PARAM_INPUT_BUFFER_LEN 4096
-#define MAX_HISTORY_ITEMS 100
+#define MAX_SCROLL_HEIGHT 10000
 
 #define HEADER_BUFFER_LENGTH 1024 * 4
 #define DEFAULT_TEXT_BUFFER_LENGTH 1024 * 4
-#define URL_TEXT_BUFFER 1024 * 10
-#define BODY_BUFFER_LENGTH 1024 * 1025 * 5
-#define RESULT_BUFFER_LENGTH 1024 * 1025 * 5
-#define AREANA_BUFFER_LENGTH 1024 * 1025 * 15
+#define URL_TEXT_BUFFER_LENGTH 1024 * 10
+#define BODY_BUFFER_LENGTH 1024 * 1024 * 5
+#define RESULT_BUFFER_LENGTH 1024 * 1024 * 5
 
 
 #ifdef _WIN32
@@ -171,6 +165,13 @@
   return 0;
 }
 
+char *PostDog_Construct_URL(char *filename)
+{
+  char full_file_path[512] = {0};
+  snprintf(full_file_path, 512, "%s/%s", POSTDOG_PATHS, filename);
+  return &full_file_path;
+}
+
 void PostDog_History_CreateFile(char *filename, char* values)
 {
   char full_file_path[512] = {0};
@@ -263,7 +264,7 @@
     const char *method,
     const char *headers,
     const char *body, 
-    char *response, size_t responseSize)
+    char *response, size_t response_size)
 {
   CURL *curl;
   CURLcode res;
@@ -273,7 +274,7 @@
 
   if (buffer.data == NULL)
   {
-    snprintf(response, responseSize, "Error: Failed to allocate memory");
+    snprintf(response, response_size, "Error: Failed to allocate memory");
     return -1;
   }
   buffer.data[0] = '\0';
@@ -335,22 +336,25 @@
     res = curl_easy_perform(curl);
 
     if (res != CURLE_OK)
-      snprintf(response, responseSize, "Error: %s\n", curl_easy_strerror(res));
+      snprintf(response, response_size, "Error: %s\n", curl_easy_strerror(res));
     else
     {
       long response_code;
       curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code);
 
-      snprintf(response, responseSize, "HTTP Status: %ld\n\n%s",
+      if (buffer.size > response_size)
+        printf("TODO: Realloc\n");
+
+      snprintf(response, response_size, "HTTP Status: %ld\n\n%s",
                response_code, buffer.data ? buffer.data : "");
     }
 
     // Cleanup
     if (headerList) curl_slist_free_all(headerList);
     curl_easy_cleanup(curl);
-  } else {
-    snprintf(response, responseSize, "Error: Failed to initialize curl");
   }
+  else
+    snprintf(response, response_size, "Error: Failed to initialize curl");
 
   free(buffer.data);
   curl_global_cleanup();
@@ -461,8 +465,10 @@
   Dowa_Arena *split_arena = Dowa_Arena_Create(file_size * 2);
   char *file_buffer = Dowa_Arena_Allocate(init_arena, file_size+1);
   fread(file_buffer, 1, file_size, file);
+  fclose(file);
+
   char **values = Dowa_String_Split(file_buffer, "---\n", file_size, 4, split_arena);
-  Dowa_Arena_Free(init_arena);
+
   for (int i = 0; i < Dowa_Array_Length(values); i++)
   {
     if (i == 0)
@@ -487,6 +493,9 @@
     else
       snprintf(url_result_text, strlen(values[i]) + 1, "%s", values[i]);
   }
+
+  Dowa_Arena_Free(init_arena);
+  Dowa_Arena_Free(split_arena);
 }
 
 Rectangle AddPadding(Rectangle rect, float padding)
@@ -499,6 +508,26 @@
   };
 }
 
+Rectangle AddPaddingHorizontal(Rectangle rect, float padding)
+{
+  return (Rectangle){
+    rect.x + padding,
+    rect.y,
+    rect.width - (2 * padding),
+    rect.height
+  };
+}
+
+Rectangle AddPaddingVertical(Rectangle rect, float padding)
+{
+  return (Rectangle){
+    rect.x,
+    rect.y + padding,
+    rect.width,
+    rect.height - (2 * padding)
+  };
+}
+
 // Layout helper functions
 Rectangle RightOf(Rectangle ref, float padding)
 {
@@ -557,20 +586,20 @@
   SetWindowState(FLAG_WINDOW_RESIZABLE);
   SetTargetFPS(60);
 
-  Dowa_Arena *arena = Dowa_Arena_Create(AREANA_BUFFER_LENGTH);
-
   Font customFont = LoadFontEx("postdog/Roboto-Regular.ttf", 20, 0, 0);
   GuiSetFont(customFont);
-  GuiSetStyle(DEFAULT, TEXT_SIZE, 20);
+  GuiSetStyle(DEFAULT, TEXT_SIZE, 10);
+  Image logo_original = LoadImage("postdog/epi_all_colors.png");
+  ImageResize(&logo_original, 60, 60);
+  Texture2D logo_texture = LoadTextureFromImage(logo_original); 
+  UnloadImage(logo_original);
 
   // -- Starting pos ---//
-  // Everyhting is relative to sidebar at this point lol
-  float side_bar_x = 10;
-  float side_bar_y = 10;
-  Rectangle history_sidebar = { .x = side_bar_x, .y = side_bar_y, .width = 0, .height = 0 };
+  Rectangle history_sidebar = { 0 };
   Dowa_Array_Reserve(history_items, 10);
   Dowa_Array_Reserve(new_history_items, 10);
   PostDog_History_Load(&history_items);
+  int32 *history_deleted_items = NULL;
 
   Rectangle url_area = { 0 };
   Rectangle textBounds = { 0 };
@@ -581,19 +610,19 @@
   Rectangle method_dropdown = { 0 };
 
 
-  char *url_input_text = (char *)Dowa_Arena_Allocate(arena, URL_TEXT_BUFFER);
-  snprintf(url_input_text, URL_TEXT_BUFFER, "https://httpbin.org/get");
+  char *url_input_text = (char *)malloc(sizeof(char) * URL_TEXT_BUFFER_LENGTH);
+  snprintf(url_input_text, URL_TEXT_BUFFER_LENGTH, "https://httpbin.org/get");
 
   char **url_body_map = NULL;
-  Dowa_Array_Push_Arena(url_body_map, (char *)Dowa_Arena_Allocate(arena, HEADER_BUFFER_LENGTH), arena);
-  Dowa_Array_Push_Arena(url_body_map, (char *)Dowa_Arena_Allocate(arena, BODY_BUFFER_LENGTH), arena);
-  Dowa_Array_Push_Arena(url_body_map, (char *)Dowa_Arena_Allocate(arena, DEFAULT_TEXT_BUFFER_LENGTH), arena);
-  Dowa_Array_Push_Arena(url_body_map, (char *)Dowa_Arena_Allocate(arena, DEFAULT_TEXT_BUFFER_LENGTH), arena);
+  Dowa_Array_Push(url_body_map, (char *)malloc(sizeof(char) * HEADER_BUFFER_LENGTH));
+  Dowa_Array_Push(url_body_map, (char *)malloc(sizeof(char) * BODY_BUFFER_LENGTH));
+  Dowa_Array_Push(url_body_map, (char *)malloc(sizeof(char) * DEFAULT_TEXT_BUFFER_LENGTH));
+  Dowa_Array_Push(url_body_map, (char *)malloc(sizeof(char) * DEFAULT_TEXT_BUFFER_LENGTH));
 
   snprintf(url_body_map[TAB_HEADER], HEADER_BUFFER_LENGTH, "Content-Type: application/json");
   snprintf(url_body_map[TAB_BODY], HEADER_BUFFER_LENGTH, "");
 
-  char *url_result_text = (char *)Dowa_Arena_Allocate(arena, RESULT_BUFFER_LENGTH);
+  char *url_result_text = (char *)malloc(sizeof(char) * RESULT_BUFFER_LENGTH);
 
   int active_method_dropdown = 0;
   bool method_edit = false;
@@ -614,39 +643,73 @@
   // General styling.
   float padding = 10; // TODO make it % based?
   int active_input_tab = 0;
+  int prev_input_tab = 0;
+
+  // Scroll offsets
+  float history_scroll_offset = 0;
+  float input_body_scroll_offset = 0;
+  float result_body_scroll_offset = 0;
 
   while (!WindowShouldClose())
   {
     int screen_width = GetScreenWidth();
     int screen_height = GetScreenHeight();
 
-    // Define main screen container
+    if ((IsKeyDown(KEY_LEFT_SUPER) || IsKeyDown(KEY_LEFT_CONTROL)) && IsKeyDown(KEY_EQUAL))
+      GuiSetStyle(DEFAULT, TEXT_SIZE, GuiGetStyle(DEFAULT, TEXT_SIZE) + 1);
+
+    if ((IsKeyDown(KEY_LEFT_SUPER) || IsKeyDown(KEY_LEFT_CONTROL)) && IsKeyDown(KEY_MINUS))
+      GuiSetStyle(DEFAULT, TEXT_SIZE, GuiGetStyle(DEFAULT, TEXT_SIZE) - 1);
+
     Rectangle screen = { 0, 0, screen_width, screen_height };
 
-    // Layout: Sidebar (left 15%) and Content Area (right 85%)
+    // -- Side bar --//
     history_sidebar = LeftColumn(screen, 0.15, padding);
     Rectangle content_area = RightColumn(screen, history_sidebar, padding);
+    Rectangle logo_area = (Rectangle){
+      .x = history_sidebar.x,
+      .y = history_sidebar.y,
+      .width = history_sidebar.width,
+      .height = 80 
+    };
 
-    // History items inside sidebar
+    Rectangle history_list_area = Below(logo_area, padding);
+    history_list_area.width = history_sidebar.width;
+    history_list_area.height = history_sidebar.height - logo_area.height - padding;
+
     int32 new_history_items_length = Dowa_Array_Length(new_history_items);
     int32 history_item_length = Dowa_Array_Length(history_items);
     int32 total = new_history_items_length + history_item_length;
-    float item_height = history_sidebar.height * 0.05;
+    float item_height = history_list_area.height * 0.10;
 
+    int32 number_of_skipped_items = 0;
     for (int i = 0; i < total; i++)
     {
+      boolean skip = FALSE;
+      for (int32 j = 0; j < Dowa_Array_Length(history_deleted_items); j++)
+      {
+        if (i == j)
+        {
+          skip = TRUE;
+          number_of_skipped_items++;
+          break;
+        }
+      }
+      if (skip)
+        continue;
+
       HistoryItem *curr_history_items = i < new_history_items_length ?
         &new_history_items[i] : &history_items[i - new_history_items_length];
 
       curr_history_items->rect = (Rectangle){
-        .x = history_sidebar.x,
-        .y = history_sidebar.y + (padding + item_height) * i,
-        .width = history_sidebar.width,
+        .x = history_list_area.x,
+        .y = history_list_area.y + (padding + item_height) * (i - number_of_skipped_items) + history_scroll_offset,
+        .width = history_list_area.width,
         .height = item_height
       };
     }
 
-    // Content area: split into URL bar (top 10%) and body (bottom 90%)
+    // --- URL --- //
     url_area = (Rectangle){
       .x = content_area.x,
       .y = content_area.y,
@@ -654,7 +717,6 @@
       .height = content_area.height * 0.1
     };
 
-    // URL bar elements laid out horizontally
     float url_control_y = url_area.y + (url_area.height - TEXT_SIZE * 2) / 2;
 
     url_text_bounds = (Rectangle){
@@ -676,7 +738,7 @@
     method_dropdown.width = url_area.width * 0.1;
     method_dropdown.height = TEXT_SIZE * 2;
 
-    // Body area: split into input (left 50%) and result (right 50%)
+    // -- Body -- //
     Rectangle body_area = Below(url_area, 0);
     body_area.height = content_area.height - url_area.height;
 
@@ -684,7 +746,6 @@
     result_area = RightOf(input_area, 0);
     result_area.width = body_area.width - input_area.width;
 
-    // Input area: tabs at top (10%) and text box below (90%)
     input_tab = (Rectangle){
       .x = input_area.x + padding,
       .y = input_area.y + padding,
@@ -699,7 +760,7 @@
     input_body.width = input_tab.width;
     input_body.height = input_area.height - input_tab.height - (2 * padding);
 
-    // Result area: aligned with input tabs
+    // -- Result -- /
     result_body = (Rectangle){
       .x = result_area.x + padding,
       .y = input_body.y,
@@ -708,16 +769,122 @@
     };
 
     Vector2 mouse_position = GetMousePosition();
+    float mouse_wheel = GetMouseWheelMove();
+
+    // Reset input body scroll when tab changes
+    if (prev_input_tab != active_input_tab) {
+      input_body_scroll_offset = 0;
+      prev_input_tab = active_input_tab;
+    }
+
+    // Handle scroll wheel for history
+    if (InArea(mouse_position, history_list_area) && mouse_wheel != 0) {
+      history_scroll_offset += mouse_wheel * 30;  // 30 pixels per wheel tick
+      // Clamp scroll offset
+      float max_scroll = (total * (item_height + padding)) - history_list_area.height;
+      if (history_scroll_offset > 0) history_scroll_offset = 0;
+      if (history_scroll_offset < -max_scroll && max_scroll > 0) history_scroll_offset = -max_scroll;
+    }
+
+    // Handle scroll wheel for input body
+    if (InArea(mouse_position, input_body) && mouse_wheel != 0) {
+      input_body_scroll_offset += mouse_wheel * 30;
+      if (input_body_scroll_offset > 0) input_body_scroll_offset = 0;
+    }
+
+    // Handle scroll wheel for result body
+    if (InArea(mouse_position, result_body) && mouse_wheel != 0) {
+      result_body_scroll_offset += mouse_wheel * 30;
+      if (result_body_scroll_offset > 0) result_body_scroll_offset = 0;
+    }
 
     BeginDrawing();
       ClearBackground(GetColor(GuiGetStyle(DEFAULT, BACKGROUND_COLOR)));
 
       DrawRectangleRec(history_sidebar, Fade(GRAY, 0.1f));
-      for (int i = 0; i < total; i++)
+
+      // DrawRectangleRec(logo_area, Fade(BLUE, 0.2f));
+      Rectangle logo_image_rect = AddPadding(logo_area, padding);
+      // Fit logo to area while maintaining aspect ratio
+      float logo_size = logo_image_rect.height < logo_image_rect.width ? logo_image_rect.height : logo_image_rect.width;
+      Rectangle dest = {
+        .x = logo_image_rect.x + (logo_image_rect.width - logo_size) / 2,  // Center horizontally
+        .y = logo_image_rect.y,
+        .width = logo_size,
+        .height = logo_size
+      };
+
+      Rectangle source = { 0, 0, logo_texture.width, logo_texture.height };
+      DrawTexturePro(logo_texture, source, dest, (Vector2){0, 0}, 0.0f, WHITE);
+
+      // Draw history items with scissor mode for clipping
+      BeginScissorMode(history_list_area.x, history_list_area.y, history_list_area.width, history_list_area.height);
+        for (int i = 0; i < total; i++)
+        {
+          boolean skip = FALSE;
+          for (int32 j = 0; j < Dowa_Array_Length(history_deleted_items); j++)
+          {
+            if (i == j)
+            {
+              skip = TRUE;
+              break;
+            }
+          }
+          if (skip)
+            continue;
+          HistoryItem *curr_history_items = i < new_history_items_length ? &new_history_items[i] : &history_items[i - new_history_items_length];
+
+          float diff = curr_history_items->rect.height*0.3;
+          // DrawRectangleRec(curr_history_items->rect, Fade(RED, 0.1f));
+          Rectangle filename_area = curr_history_items->rect;
+
+          filename_area.height -= diff;
+          Rectangle icon_area = Below(filename_area, 0);
+          icon_area.height = diff;
+
+          DrawRectangleRec(filename_area, Fade(BLUE, 0.1f));
+          DrawRectangleRec(icon_area, Fade(YELLOW, 0.1f));
+
+          Rectangle icon_area_left_column = LeftColumn(icon_area, 0.5, 0);
+          Rectangle icon_area_right_column = RightColumn(icon_area, icon_area_left_column, 0);
+
+          GuiDrawText(curr_history_items->filename, AddPadding(filename_area, padding), TEXT_ALIGN_CENTER, RED);
+          if (GuiButton(AddPaddingHorizontal(icon_area_left_column, padding), "view"))
+            PostDog_Load_File(
+                curr_history_items->filename,
+                &url_input_text,
+                &active_method_dropdown,
+                url_body_map,
+                &url_result_text
+            );
+          if (GuiButton(AddPaddingHorizontal(icon_area_right_column,padding), "delete"))
+          {
+            if (!remove(PostDog_Construct_URL(curr_history_items->filename)))
+               Dowa_Array_Push(history_deleted_items, i);
+            else
+            {
+              fprintf(stderr, "Wasn't able to delete file: %s \n", curr_history_items->filename);
+            }
+          }
+        }
+      EndScissorMode();
+
+      // Draw scroll indicator for history
+      if (total > 0)
       {
-        HistoryItem *curr_history_items = i < new_history_items_length ? &new_history_items[i] : &history_items[i - new_history_items_length];
-        DrawRectangleRec(curr_history_items->rect, Fade(RED, 0.1f)); 
-        GuiDrawText(curr_history_items->filename, AddPadding(curr_history_items->rect, padding), TEXT_ALIGN_CENTER, RED);
+        float content_height = total * (item_height + padding);
+        if (content_height > history_list_area.height)
+        {
+          float scrollbar_height = (history_list_area.height / content_height) * history_list_area.height;
+          float scrollbar_y = history_list_area.y - (history_scroll_offset / content_height) * history_list_area.height;
+          Rectangle scrollbar = {
+            history_list_area.x + history_list_area.width - 5,
+            scrollbar_y,
+            5,
+            scrollbar_height
+          };
+          DrawRectangleRec(scrollbar, Fade(WHITE, 0.5f));
+        }
       }
 
       // URL area Rect
@@ -743,8 +910,31 @@
       DrawRectangleRec(input_area, Fade(BLUE, 0.1f));
       DrawRectangleRec(input_tab,  Fade(DARKBLUE, 0.1f));
       GuiSetStyle(TOGGLE, GROUP_PADDING, 0);
-      if (JUNE_GuiTextBox(input_body, url_body_map[active_input_tab], DEFAULT_TEXT_BUFFER_LENGTH, input_body_bool))
-        input_body_bool = !input_body_bool;
+      GuiDrawRectangle(input_body, 1, GetColor(GuiGetStyle(TEXTBOX, BORDER)), GetColor(GuiGetStyle(TEXTBOX, BASE_COLOR_PRESSED)));
+
+      // Create scrollable input body with offset
+      BeginScissorMode(input_body.x, input_body.y, input_body.width, input_body.height);
+        Rectangle scrolled_input = AddPadding(input_body, padding * 2);
+        scrolled_input.y += input_body_scroll_offset;
+        scrolled_input.height = MAX_SCROLL_HEIGHT;
+        if (JUNE_GuiTextBox(scrolled_input, url_body_map[active_input_tab], DEFAULT_TEXT_BUFFER_LENGTH, input_body_bool))
+          input_body_bool = !input_body_bool;
+      EndScissorMode();
+
+      // Draw scroll indicator for input body
+      if (input_body_scroll_offset < 0) {
+        float scrollbar_height = 50; // Fixed size indicator
+        float max_scroll = 1000; // Estimated max scroll
+        float scrollbar_y = input_body.y - (input_body_scroll_offset / max_scroll) * (input_body.height - scrollbar_height);
+        Rectangle scrollbar = {
+          input_body.x + input_body.width - 5,
+          scrollbar_y,
+          5,
+          scrollbar_height
+        };
+        DrawRectangleRec(scrollbar, Fade(BLUE, 0.5f));
+      }
+
       GuiToggleGroup(input_tab_item, "Header;Body;Get Param;Bar", &active_input_tab);
 
       PostDog_Update_URL(&url_input_text, url_body_map[TAB_GET_PARAMS]);
@@ -752,7 +942,28 @@
       // Result Rect
       DrawRectangleRec(result_area, Fade(GREEN, 0.1f));
       DrawRectangleRec(result_body, Fade(DARKGREEN, 0.1f));
-      GuiTextBoxMulti(result_body, url_result_text, RESULT_BUFFER_LENGTH, false);
+
+      // Create scrollable result body with offset
+      BeginScissorMode(result_body.x, result_body.y, result_body.width, result_body.height);
+        Rectangle scrolled_result = result_body;
+        scrolled_result.y += result_body_scroll_offset;
+        scrolled_result.height = MAX_SCROLL_HEIGHT;
+        GuiTextBoxMulti(scrolled_result, url_result_text, RESULT_BUFFER_LENGTH, false);
+      EndScissorMode();
+
+      // Draw scroll indicator for result body
+      if (result_body_scroll_offset < 0) {
+        float scrollbar_height = 50; // Fixed size indicator
+        float max_scroll = 1000; // Estimated max scroll
+        float scrollbar_y = result_body.y - (result_body_scroll_offset / max_scroll) * (result_body.height - scrollbar_height);
+        Rectangle scrollbar = {
+          result_body.x + result_body.width - 5,
+          scrollbar_y,
+          5,
+          scrollbar_height
+        };
+        DrawRectangleRec(scrollbar, Fade(GREEN, 0.5f));
+      }
 
       if (url_input_edit && (IsKeyDown(KEY_LEFT_SUPER) || IsKeyDown(KEY_LEFT_CONTROL)) && IsKeyPressed(KEY_C))
       {
@@ -768,18 +979,6 @@
         if ((IsKeyDown(KEY_LEFT_SUPER) || IsKeyDown(KEY_LEFT_CONTROL)) && IsKeyPressed(KEY_C))
           SetClipboardText(url_result_text);
       }
-
-      for (int i = 0; i < Dowa_Array_Length(history_items); i++)
-      {
-        if (Clicked(mouse_position, history_items[i].rect))
-          PostDog_Load_File(
-              history_items[i].filename,
-              &url_input_text,
-              &active_method_dropdown,
-              url_body_map,
-              &url_result_text
-          );
-      }
     EndDrawing();
   }
   CloseWindow();
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/postdog/slider_example.c	Wed Jan 07 04:52:17 2026 -0800
@@ -0,0 +1,115 @@
+
+/*******************************************************************************************
+*
+*   raygui - custom file dialog to load image
+*
+*   DEPENDENCIES:
+*       raylib 4.0  - Windowing/input management and drawing.
+*       raygui 3.0  - Immediate-mode GUI controls.
+*
+*   COMPILATION (Windows - MinGW):
+*       gcc -o $(NAME_PART).exe $(FILE_NAME) -I../../src -lraylib -lopengl32 -lgdi32 -std=c99
+*
+*   LICENSE: zlib/libpng
+*
+*   Copyright (c) 2016-2024 Ramon Santamaria (@raysan5)
+*
+**********************************************************************************************/
+
+#include "third_party/raylib/include/raylib.h"
+
+#define RAYGUI_IMPLEMENTATION
+#include "third_party/raylib/include/raygui.h"
+
+#undef RAYGUI_IMPLEMENTATION            // Avoid including raygui implementation again
+
+#define GUI_WINDOW_FILE_DIALOG_IMPLEMENTATION
+#include "postdog/gui_window_file_dialog.h"
+#define POSTDOG_PATHS "/Users/mrjunejune/zenbu/postdog/history"
+
+
+//------------------------------------------------------------------------------------
+// Program main entry point
+//------------------------------------------------------------------------------------
+int main()
+{
+    // Initialization
+    //---------------------------------------------------------------------------------------
+    int screenWidth = 800;
+    int screenHeight = 560;
+
+    InitWindow(screenWidth, screenHeight, "raygui - custom modal dialog");
+    SetExitKey(0);
+
+    // Custom file dialog
+    GuiWindowFileDialogState fileDialogState = InitGuiWindowFileDialog(POSTDOG_PATHS);
+
+    bool exitWindow = false;
+
+    char fileNameToLoad[512] = { 0 };
+
+    Texture texture = { 0 };
+
+    SetTargetFPS(60);
+    //--------------------------------------------------------------------------------------
+
+    // Main game loop
+    while (!exitWindow)    // Detect window close button or ESC key
+    {
+        // Update
+        //----------------------------------------------------------------------------------
+        exitWindow = WindowShouldClose();
+
+        if (fileDialogState.SelectFilePressed)
+        {
+            // Load image file (if supported extension)
+            if (IsFileExtension(fileDialogState.fileNameText, ".png"))
+            {
+                strcpy(fileNameToLoad, TextFormat("%s" PATH_SEPERATOR "%s", fileDialogState.dirPathText, fileDialogState.fileNameText));
+                UnloadTexture(texture);
+                texture = LoadTexture(fileNameToLoad);
+            }
+
+            fileDialogState.SelectFilePressed = false;
+        }
+        //----------------------------------------------------------------------------------
+
+        // Draw
+        //----------------------------------------------------------------------------------
+        BeginDrawing();
+
+            ClearBackground(GetColor(GuiGetStyle(DEFAULT, BACKGROUND_COLOR)));
+
+            DrawTexture(texture, GetScreenWidth()/2 - texture.width/2, GetScreenHeight()/2 - texture.height/2 - 5, WHITE);
+            DrawRectangleLines(GetScreenWidth()/2 - texture.width/2, GetScreenHeight()/2 - texture.height/2 - 5, texture.width, texture.height, BLACK);
+
+            DrawText(fileNameToLoad, 208, GetScreenHeight() - 20, 10, GRAY);
+
+            // raygui: controls drawing
+            //----------------------------------------------------------------------------------
+            if (fileDialogState.windowActive) GuiLock();
+
+            if (GuiButton((Rectangle){ 20, 20, 140, 30 }, GuiIconText(ICON_FILE_OPEN, "Open Image"))) fileDialogState.windowActive = true;
+
+            GuiUnlock();
+
+            // GUI: Dialog Window
+            //--------------------------------------------------------------------------------
+            GuiWindowFileDialog(&fileDialogState);
+            //--------------------------------------------------------------------------------
+
+            //----------------------------------------------------------------------------------
+
+        EndDrawing();
+        //----------------------------------------------------------------------------------
+    }
+
+    // De-Initialization
+    //--------------------------------------------------------------------------------------
+    UnloadTexture(texture);     // Unload texture
+
+    CloseWindow();              // Close window and OpenGL context
+    //--------------------------------------------------------------------------------------
+
+    return 0;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/third_party/raylib/custom.c	Wed Jan 07 04:52:17 2026 -0800
@@ -0,0 +1,86 @@
+#include "third_party/raylib/include/raylib.h"
+#define RAYGUI_IMPLEMENTATION
+#include "third_party/raylib/include/raygui.h"
+
+// -- forward declarations --//
+
+// --- Default Behaviour that should be part of all raylib gui ---/
+void DefaultBehaviours();
+// --- Increase Font Sizes on key press --- //
+void IncreaseFontSize();
+// --- Decrease Font Sizes on key press --- //
+void DecreaseFontSize();
+
+
+void DefaultBehaviours()
+{
+  // Font sizes 
+  IncreaseFontSize();
+  DecreaseFontSize();
+}
+
+void IncreaseFontSize()
+{
+  if ((IsKeyDown(KEY_LEFT_SUPER) || IsKeyDown(KEY_LEFT_CONTROL)) && IsKeyDown(KEY_EQUAL))
+    GuiSetStyle(DEFAULT, TEXT_SIZE, GuiGetStyle(DEFAULT, TEXT_SIZE) + 1);
+}
+
+void DecreaseFontSize()
+{
+  if ((IsKeyDown(KEY_LEFT_SUPER) || IsKeyDown(KEY_LEFT_CONTROL)) && IsKeyDown(KEY_MINUS))
+    GuiSetStyle(DEFAULT, TEXT_SIZE, GuiGetStyle(DEFAULT, TEXT_SIZE) - 1);
+}
+
+
+// --- Layout helper --- //
+Rectangle RightOf(Rectangle ref, float padding)
+{
+  return (Rectangle){
+    .x = ref.x + ref.width + padding,
+    .y = ref.y,
+    .width = 0,
+    .height = ref.height
+  };
+}
+
+Rectangle Below(Rectangle ref, float padding)
+{
+  return (Rectangle){
+    .x = ref.x,
+    .y = ref.y + ref.height + padding,
+    .width = ref.width,
+    .height = 0
+  };
+}
+
+Rectangle LeftColumn(Rectangle container, float ratio, float padding)
+{
+  return (Rectangle){
+    .x = container.x + padding,
+    .y = container.y + padding,
+    .width = (container.width * ratio) - padding,
+    .height = container.height - (2 * padding)
+  };
+}
+
+Rectangle RightColumn(Rectangle container, Rectangle leftCol, float padding)
+{
+  return (Rectangle){
+    .x = leftCol.x + leftCol.width + padding,
+    .y = container.y + padding,
+    .width = container.width - leftCol.width - (3 * padding),
+    .height = container.height - (2 * padding)
+  };
+}
+
+Rectangle HorizontalSplit(Rectangle container, float ratio)
+{
+  return (Rectangle){
+    .x = container.x,
+    .y = container.y,
+    .width = container.width * ratio,
+    .height = container.height
+  };
+}
+
+
--- a/third_party/raylib/include/raygui.h	Tue Jan 06 08:19:07 2026 -0800
+++ b/third_party/raylib/include/raygui.h	Wed Jan 07 04:52:17 2026 -0800
@@ -6588,13 +6588,13 @@
     //--------------------------------------------------------------------
     if (state == STATE_PRESSED)
     {
-        GuiDrawRectangle(bounds, GuiGetStyle(TEXTBOX, BORDER_WIDTH), GetColor(GuiGetStyle(TEXTBOX, BORDER + (state*3))), GetColor(GuiGetStyle(TEXTBOX, BASE_COLOR_PRESSED)));
+        GuiDrawRectangle(bounds, 0, GetColor(GuiGetStyle(TEXTBOX, BORDER + (state*3))), GetColor(GuiGetStyle(TEXTBOX, BASE_COLOR_PRESSED)));
     }
     else if (state == STATE_DISABLED)
     {
-        GuiDrawRectangle(bounds, GuiGetStyle(TEXTBOX, BORDER_WIDTH), GetColor(GuiGetStyle(TEXTBOX, BORDER + (state*3))), GetColor(GuiGetStyle(TEXTBOX, BASE_COLOR_DISABLED)));
+        GuiDrawRectangle(bounds, 0, GetColor(GuiGetStyle(TEXTBOX, BORDER + (state*3))), GetColor(GuiGetStyle(TEXTBOX, BASE_COLOR_DISABLED)));
     }
-    else GuiDrawRectangle(bounds, GuiGetStyle(TEXTBOX, BORDER_WIDTH), GetColor(GuiGetStyle(TEXTBOX, BORDER + (state*3))),  GetColor(GuiGetStyle(TEXTBOX, BASE_COLOR_PRESSED)));
+    else GuiDrawRectangle(bounds, 0, GetColor(GuiGetStyle(TEXTBOX, BORDER + (state*3))),  GetColor(GuiGetStyle(TEXTBOX, BASE_COLOR_PRESSED)));
 
     // Draw text considering index offset if required
     // NOTE: Text index offset depends on cursor position