├── .clang-format ├── .gitignore ├── include ├── dialog.h ├── ribbon.h ├── input.h ├── hr_list.h ├── option_list.h ├── vr_list.h ├── draw.h ├── animation.h ├── fm.h ├── signal.h └── ui.h ├── readme.md ├── src ├── dialog.c ├── signal.c ├── option_list.c ├── input.c ├── hr_list.c ├── animation.c ├── vr_list.c ├── draw.c ├── fm.c ├── ribbon.c ├── main.c └── ui.c └── Makefile /.clang-format: -------------------------------------------------------------------------------- 1 | ColumnLimit: 120 2 | IndentWidth: 4 3 | SortIncludes: false 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | compile_commands.json 2 | app 3 | .cache/ 4 | build/ 5 | libnanovg.a 6 | fonts/ 7 | -------------------------------------------------------------------------------- /include/dialog.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | typedef struct { 4 | float position; 5 | bool is_visible; 6 | const char *content; 7 | int animation_target; 8 | } Dialog; 9 | 10 | void dialog_show(Dialog *dialog); 11 | void dialog_hide(Dialog *dialog); 12 | void dialog_move_cursor_left(Dialog *dialog); 13 | void dialog_move_cursor_right(Dialog *dialog); 14 | -------------------------------------------------------------------------------- /include/ribbon.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | typedef struct { 6 | uint32_t ribbon_program; 7 | uint32_t ribbon_vertex_array; 8 | uint32_t ribbon_vertex_buffer; 9 | 10 | uint32_t bg_vao; 11 | uint32_t bg_vbo; 12 | uint32_t bg_program; 13 | } RibbonState; 14 | 15 | void init_ribbon(); 16 | void draw_ribbon(float width, float height, float time); 17 | void draw_background(float width, float height, int theme); 18 | -------------------------------------------------------------------------------- /include/input.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define INPUT_BUFFER_LEN 60 4 | 5 | typedef struct { 6 | int position; 7 | bool is_visible; 8 | char buffer[INPUT_BUFFER_LEN]; 9 | } Input; 10 | 11 | void show_input(Input *input); 12 | void hide_input(Input *input); 13 | void pop_from_input(Input *input); 14 | void set_buffer_input(Input *input, const char *text); 15 | void append_to_input(Input *input, unsigned int codepoint); 16 | 17 | void move_cursor_left(Input *input); 18 | void move_cursor_right(Input *input); 19 | -------------------------------------------------------------------------------- /include/hr_list.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "signal.h" 4 | 5 | typedef struct { 6 | char title[50]; 7 | char path[50]; 8 | char icon[10]; 9 | } HrItem; 10 | 11 | typedef struct { 12 | int depth; 13 | int selected; 14 | float scroll; 15 | HrItem *items; 16 | int items_count; 17 | } HorizontalList; 18 | 19 | void init_horizontal_list(HorizontalList *hr_list); 20 | void update_horizontal_list(HorizontalList *hr_list); 21 | void horizontal_list_event_handler(EventType type, void *context, void *data); 22 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # XMB File Manager 2 | 3 | an attempt to make a file manager with a UI resembling XMB (XrossMediaBar) more known as PS3 UI. 4 | 5 | it's using OpenGL, FreeType, GLFW 6 | 7 | # Keybindings 8 | 9 | - Arrow Keys: `up` and `down` for vertical menu, `left` & `right` for horizontal menu. 10 | - `Enter`: open directory, open file using `xdg-open` (not fully tested). 11 | - `Backspace`: navigate to parent directory. 12 | - `i`: to open options list 13 | - `p`: preview first 500 char of a file if detected as text file. 14 | - `+`/`-`: switch the themes 15 | - `PAGE_UP`/`PAGE_DOWN`: scroll up and down by 10. 16 | - `HOME`/`END`: scroll to start / end of list. 17 | - `/`: show a search input, after validating `Enter` it will scroll to item that contains keyword. 18 | -------------------------------------------------------------------------------- /include/option_list.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "signal.h" 4 | #include 5 | 6 | #define OPTION_LIST_WIDTH 300 7 | 8 | struct Options; 9 | 10 | typedef struct option { 11 | float y; 12 | char title[60]; 13 | struct Options *submenu; 14 | } Option; 15 | 16 | typedef struct Options { 17 | int selected; 18 | 19 | Option *items; 20 | int items_count; 21 | 22 | struct Options *parent; 23 | } Options; 24 | 25 | typedef struct { 26 | float x; 27 | size_t depth; 28 | Options *root; 29 | Options *current; 30 | 31 | void (*get_screen_size)(unsigned *width, unsigned *height); 32 | void (*on_item_selected)(Option *option); 33 | } OptionList; 34 | 35 | void update_option_list(OptionList *list); 36 | void option_list_event_handler(EventType event, OptionList *list, void *data); 37 | -------------------------------------------------------------------------------- /include/vr_list.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "fm.h" 4 | #include "signal.h" 5 | 6 | typedef struct { 7 | int selected; 8 | bool toggled; 9 | 10 | ///// 11 | int depth; 12 | float icon_spacing_vertical; 13 | 14 | int icon_size; 15 | float margins_screen_top; 16 | 17 | float above_subitem_offset; 18 | float above_item_offset; 19 | float active_item_factor; 20 | float under_item_offset; 21 | 22 | int items_count; 23 | struct file_entry **items; 24 | 25 | unsigned entry_start; 26 | unsigned entry_end; 27 | 28 | void (*get_screen_size)(unsigned *width, unsigned *height); 29 | } VerticalList; 30 | 31 | void init_vertical_list(VerticalList *list); 32 | void update_vertical_list(VerticalList *list); 33 | void vertical_list_event_handler(EventType type, void *context, void *data); 34 | -------------------------------------------------------------------------------- /src/dialog.c: -------------------------------------------------------------------------------- 1 | #include "dialog.h" 2 | #include "animation.h" 3 | 4 | void dialog_show(Dialog *dialog) { 5 | dialog->position = 0; 6 | dialog->is_visible = true; 7 | dialog->animation_target = 0; 8 | } 9 | 10 | void dialog_hide(Dialog *dialog) { 11 | dialog->position = 0; 12 | dialog->is_visible = false; 13 | dialog->animation_target = 0; 14 | } 15 | 16 | void dialog_move_cursor_left(Dialog *dialog) { 17 | if (dialog->animation_target <= -1) 18 | return; 19 | dialog->animation_target -= 1; 20 | animation_push(0.1, dialog->animation_target, &dialog->position, DialogTag); 21 | } 22 | 23 | void dialog_move_cursor_right(Dialog *dialog) { 24 | if (dialog->animation_target >= 0) 25 | return; 26 | dialog->animation_target += 1; 27 | animation_push(0.1, dialog->animation_target, &dialog->position, DialogTag); 28 | } 29 | -------------------------------------------------------------------------------- /include/draw.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "dialog.h" 4 | #include "input.h" 5 | #include "hr_list.h" 6 | #include "vr_list.h" 7 | #include "option_list.h" 8 | 9 | typedef struct { 10 | int theme; 11 | int width; 12 | int height; 13 | bool show_info; 14 | bool show_preview; 15 | char buffer[512]; 16 | } DrawState; 17 | 18 | void draw_folder_path(const HorizontalList *hr_list, const char *path, float x, float y); 19 | void draw_vertical_list(const VerticalList *list, float x); 20 | void draw_horizontal_menu(const HorizontalList *hr_list, int x, int y); 21 | void draw_text_preview(DrawState *state); 22 | void draw_info(const VerticalList *vr_list, float width, float height); 23 | void draw_option_list(OptionList *op_list, DrawState *state); 24 | void draw_input_field(Input *input, const char *title, DrawState *state); 25 | void draw_dialog(Dialog *dialog, DrawState *state, const char *path); 26 | -------------------------------------------------------------------------------- /include/animation.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | typedef enum { 9 | DialogTag, 10 | OptionListTag, 11 | VerticalListTag, 12 | HorizontalListTag, 13 | } AnimationTag; 14 | 15 | struct tween { 16 | float duration; 17 | float initial_value; 18 | float target_value; 19 | float *subject; 20 | AnimationTag tag; 21 | 22 | float start_time; 23 | float running_since; 24 | }; 25 | 26 | struct animations { 27 | struct tween *list; 28 | size_t size; 29 | size_t capacity; 30 | }; 31 | 32 | typedef struct tween tween_t; 33 | typedef struct animations animation_t; 34 | 35 | void animation_push(float duration, float target, float *subject, AnimationTag tag); 36 | void animation_update(float current_time); 37 | void animation_clean(); 38 | void animation_remove_by_tag(AnimationTag tag); 39 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PCKGS = gl freetype2 glew libmagic harfbuzz 2 | CFLAGS=-Wall -std=c23 -pedantic -g -fcolor-diagnostics# -fsanitize=address 3 | LDFLAGS = -lm -lGL -lglfw `pkg-config --libs $(PCKGS)` 4 | INCS=-I include/ `pkg-config --cflags $(PCKGS)` 5 | 6 | COMPILER = clang 7 | 8 | SRC_DIR = src 9 | BUILD_DIR = build 10 | INCLUDE_DIR = . 11 | 12 | # Files 13 | SRC_FILES = $(shell find $(SRC_DIR) -type f -name "*.c") 14 | OBJ_FILES = $(patsubst $(SRC_DIR)/%.c,$(BUILD_DIR)/%.o,$(SRC_FILES)) 15 | 16 | 17 | TARGET = app 18 | 19 | all: $(TARGET) $(BUILD_DIR) 20 | 21 | run: all 22 | @./$(TARGET) 23 | 24 | debug: all 25 | gdb ./$(TARGET) 26 | 27 | $(TARGET): $(OBJ_FILES) 28 | $(COMPILER) $(CFLAGS) $(INCS) -o $@ $^ $(LDFLAGS) 29 | 30 | 31 | $(BUILD_DIR)/%.o: $(SRC_DIR)/%.c 32 | @mkdir -p $(dir $@) 33 | $(COMPILER) $(CFLAGS) $(INCS) -c -o "$@" "$<" 34 | 35 | $(BUILD_DIR): 36 | mkdir -p $(BUILD_DIR) 37 | 38 | clean: 39 | @rm -rf $(BUILD_DIR) $(TARGET) 40 | 41 | .PHONY: all clean 42 | -------------------------------------------------------------------------------- /include/fm.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | typedef enum { 9 | TYPE_FILE = 0, 10 | TYPE_DIRECTORY = 1, 11 | TYPE_SYMLINK = 2, 12 | } FileType; 13 | 14 | typedef enum { 15 | SortByName = 0, 16 | SortByDate = 1, 17 | SortBySize = 2, 18 | } SortMode; 19 | 20 | typedef struct file_entry { 21 | char name[256]; 22 | char path[1024]; 23 | FileType type; 24 | 25 | struct file_entry **children; 26 | size_t child_count; 27 | 28 | struct file_entry *parent; 29 | 30 | size_t size; // File size in bytes 31 | time_t access_time; // Last access timestamp 32 | mode_t permissions; // Unix-style permissions 33 | time_t modified_time; // Last modified timestamp 34 | 35 | float x; 36 | float y; 37 | float zoom; 38 | float alpha; 39 | float label_alpha; 40 | } FileEntry; 41 | 42 | typedef struct file_manager { 43 | int depth; 44 | FileEntry *current_dir; 45 | 46 | // View settings 47 | bool show_hidden; 48 | bool reverse_sort; 49 | SortMode sort_mode; 50 | 51 | int action_target_index; 52 | } FileManager; 53 | 54 | FileManager *create_file_manager(const char *path); 55 | void free_file_manager(FileManager *fm); 56 | 57 | void change_directory(FileManager *fm, const char *path); 58 | int navigate_back(FileManager *fm); 59 | void switch_directory(FileManager *fm, const char *path); 60 | 61 | bool get_mime_type(const char *path, const char *test); 62 | void open_file(const char *path); 63 | void read_file_content(const char *filename, char *buffer, size_t len); 64 | int search_file_name(FileManager *fm, const char *keyword); 65 | 66 | bool fm_rename(FileManager *fm, const char *new_name); 67 | bool fm_create_dir(FileManager *fm, const char *name); 68 | bool fm_delete_entry(FileManager *fm); 69 | -------------------------------------------------------------------------------- /include/signal.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define MAX_CONNECTIONS 100 4 | #define MAX_ARGS 10 5 | 6 | typedef enum { 7 | // Navigation events 8 | EVENT_NAVIGATE_TO_PATH, // Navigate to a specific path 9 | EVENT_NAVIGATE_BACK, // Navigate up one level 10 | 11 | // Selection events 12 | EVENT_HORIZONTAL_SELECTION_CHANGED, // Horizontal list selection changed 13 | EVENT_VERTICAL_SELECTION_CHANGED, // Vertical list selection changed 14 | EVENT_ITEM_ACTIVATED, // Item was activated (enter pressed) 15 | EVENT_SEARCH, // search for next item 16 | EVENT_RENAME, // rename current item 17 | EVENT_CREATE_DIR, // create new directory 18 | EVENT_CONFIRM_DELETE, // delete entry 19 | EVENT_REJECT_DELETE, // reject delete entry 20 | 21 | // State update events 22 | EVENT_DIRECTORY_CONTENT_CHANGED, // Directory contents changed 23 | 24 | OPTION_EVENT_OPEN_MENU, // Open the menu 25 | OPTION_EVENT_CLOSE_MENU, // Close current menu 26 | OPTION_EVENT_MOVE_SELECTION, // Move selection up/down 27 | OPTION_EVENT_SELECT_ITEM, // Select current item 28 | OPTION_EVENT_OPEN_SUBMENU, // Open a submenu 29 | 30 | MAX_EVENT_TYPES, 31 | } EventType; 32 | 33 | typedef struct { 34 | const char *path; 35 | int selected_index; 36 | bool clear_history; 37 | } NavigationData; 38 | 39 | typedef struct { 40 | int index; 41 | } SelectionData; 42 | 43 | typedef struct { 44 | char *current_path; 45 | void *items; 46 | int items_count; 47 | int depth; 48 | int selected; 49 | } DirectoryData; 50 | 51 | typedef struct { 52 | EventType type; 53 | void **data; 54 | } Event; 55 | 56 | typedef void (*EventCallback)(EventType type, void *context, void *data); 57 | 58 | typedef struct { 59 | EventCallback callback; 60 | void *context; 61 | } EventListener; 62 | 63 | // Structure to represent a connection 64 | #define MAX_LISTENERS 5 65 | 66 | typedef struct { 67 | EventListener listeners[MAX_EVENT_TYPES][MAX_LISTENERS]; 68 | int listener_counts[MAX_EVENT_TYPES]; 69 | } EventManager; 70 | 71 | int connect_signal(EventType type, EventCallback callback, void *context); 72 | void emit_signal(EventType type, void *data); 73 | -------------------------------------------------------------------------------- /src/signal.c: -------------------------------------------------------------------------------- 1 | #include "signal.h" 2 | #include 3 | #include 4 | 5 | const char *event_types[] = { 6 | "EVENT_CHANGE_HORIZONTAL_INDEX", 7 | "EVENT_INCREASE_HORIZONTAL_DEPTH", 8 | "EVENT_DECREASE_HORIZONTAL_DEPTH", 9 | 10 | "EVENT_SWITCH_DIRECTORY", 11 | "EVENT_CHANGE_DIRECTORY", 12 | "EVENT_DIRECTORY_CHANGED", 13 | 14 | "EVENT_VERTICAL_LIST_INDEX_POPPED", 15 | "EVENT_VERTICAL_LIST_INDEX_CHANGED", 16 | "EVENT_VERTICAL_LIST_INDEX_VALIDATED", 17 | 18 | "EVENT_FM_OPEN_SELECTED_ITEM", 19 | "EVENT_FM_CLOSE_FOLDER", 20 | }; 21 | 22 | static EventManager manager; 23 | 24 | int connect_signal(EventType type, EventCallback callback, void *context) { 25 | if (!callback || type >= MAX_EVENT_TYPES) 26 | return 0; 27 | 28 | int count = manager.listener_counts[type]; 29 | if (count >= MAX_LISTENERS) 30 | return 1; // No space for more listeners 31 | 32 | // Add new listener 33 | manager.listeners[type][count].callback = callback; 34 | manager.listeners[type][count].context = context; 35 | manager.listener_counts[type]++; 36 | return 1; 37 | } 38 | 39 | int disconnect_signal(EventType type, EventCallback callback) { 40 | if (!callback || type >= MAX_EVENT_TYPES) 41 | return 0; 42 | 43 | int count = manager.listener_counts[type]; 44 | 45 | // Find the listener with the matching callback 46 | for (size_t i = 0; i < count; ++i) { 47 | if (manager.listeners[type][i].callback == callback) { 48 | // Remove by shifting the rest down 49 | for (size_t j = i; j < count - 1; ++j) { 50 | manager.listeners[type][j] = manager.listeners[type][j + 1]; 51 | } 52 | manager.listener_counts[type]--; 53 | break; 54 | } 55 | } 56 | return 1; 57 | } 58 | 59 | void emit_signal(EventType type, void *data) { 60 | if (type >= MAX_EVENT_TYPES) 61 | return; 62 | 63 | // printf("SIGNAL: %s\n", event_types[type]); 64 | 65 | int count = manager.listener_counts[type]; 66 | 67 | // Call all registered callbacks for this event type 68 | for (size_t i = 0; i < count; ++i) { 69 | EventListener *listener = &manager.listeners[type][i]; 70 | listener->callback(type, listener->context, data); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/option_list.c: -------------------------------------------------------------------------------- 1 | #include "option_list.h" 2 | #include "animation.h" 3 | #include 4 | #include 5 | #include 6 | 7 | void option_list_event_handler(EventType event, OptionList *list, void *data) { 8 | switch (event) { 9 | case OPTION_EVENT_OPEN_MENU: 10 | list->depth = 1; 11 | update_option_list(list); 12 | break; 13 | 14 | case OPTION_EVENT_CLOSE_MENU: 15 | if (list->depth <= 0) 16 | return; 17 | 18 | list->depth--; 19 | list->current->selected = 0; 20 | if (list->current->parent) 21 | list->current = list->current->parent; 22 | update_option_list(list); 23 | break; 24 | 25 | case OPTION_EVENT_MOVE_SELECTION: { 26 | int direction = *(int *)data; // +1 for down, -1 for up 27 | 28 | if (direction < 0 && list->current->selected > 0) 29 | list->current->selected--; 30 | else if (direction > 0 && list->current->selected < list->current->items_count - 1) 31 | list->current->selected++; 32 | 33 | update_option_list(list); 34 | break; 35 | } 36 | 37 | case OPTION_EVENT_SELECT_ITEM: { 38 | Option *current = &list->current->items[list->current->selected]; 39 | 40 | if (list->on_item_selected == NULL) 41 | return; 42 | 43 | list->on_item_selected(current); 44 | if (list->current->parent) 45 | list->current = list->current->parent; 46 | list->current->selected = 0; 47 | list->depth--; 48 | update_option_list(list); 49 | break; 50 | } 51 | 52 | case OPTION_EVENT_OPEN_SUBMENU: { 53 | Option *current = &list->current->items[list->current->selected]; 54 | 55 | if (current->submenu && current->submenu->items_count > 0) { 56 | list->depth++; 57 | list->current = current->submenu; 58 | update_option_list(list); 59 | } 60 | break; 61 | } 62 | default: 63 | break; 64 | } 65 | } 66 | 67 | void update_option_list(OptionList *op_list) { 68 | // if (list->items_count == 0) 69 | // return; 70 | 71 | Options *list = op_list->current; 72 | 73 | animation_push(0.1, op_list->depth * -(float)OPTION_LIST_WIDTH, &op_list->x, OptionListTag); 74 | 75 | uint32_t height = 0; 76 | uint32_t end = list->items_count; 77 | if (op_list->get_screen_size != NULL) { 78 | op_list->get_screen_size(NULL, &height); 79 | } 80 | 81 | for (size_t i = 0; i < end; i++) { 82 | Option node = list->items[i]; 83 | 84 | node.y = 40 * i; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/input.c: -------------------------------------------------------------------------------- 1 | #include "input.h" 2 | #include 3 | 4 | void show_input(Input *input) { 5 | input->is_visible = true; // 6 | } 7 | 8 | void hide_input(Input *input) { 9 | memset(input->buffer, 0, INPUT_BUFFER_LEN); 10 | input->is_visible = false; 11 | input->position = 0; 12 | } 13 | 14 | void set_buffer_input(Input *input, const char *text) { 15 | strcpy(input->buffer, text); // 16 | input->position = strlen(text); 17 | } 18 | 19 | void move_cursor_left(Input *input) { 20 | if (input->position > 0) { 21 | input->position--; 22 | } 23 | } 24 | 25 | void move_cursor_right(Input *input) { 26 | if (input->position < strlen(input->buffer)) { 27 | input->position++; 28 | } 29 | } 30 | 31 | void pop_from_input(Input *input) { 32 | if (input->position <= 0) 33 | return; 34 | 35 | size_t len = strlen(input->buffer); 36 | if (len == 0) 37 | return; 38 | 39 | int char_start = input->position - 1; 40 | while (char_start > 0 && (input->buffer[char_start] & 0xC0) == 0x80) { 41 | char_start--; 42 | } 43 | 44 | int char_length = input->position - char_start; 45 | 46 | for (int i = char_start; i <= len - char_length; i++) { 47 | input->buffer[i] = input->buffer[i + char_length]; 48 | } 49 | 50 | input->position = char_start; 51 | } 52 | 53 | void append_to_input(Input *input, unsigned int codepoint) { 54 | char utf8_char[5] = {0}; 55 | int utf8_len = 0; 56 | 57 | if (codepoint < 0x80) { 58 | // ASCII 59 | utf8_char[0] = (char)codepoint; 60 | utf8_len = 1; 61 | } else if (codepoint < 0x800) { 62 | // 2-byte UTF-8 63 | utf8_char[0] = 0xC0 | (codepoint >> 6); 64 | utf8_char[1] = 0x80 | (codepoint & 0x3F); 65 | utf8_len = 2; 66 | } else if (codepoint < 0x10000) { 67 | // 3-byte UTF-8 68 | utf8_char[0] = 0xE0 | (codepoint >> 12); 69 | utf8_char[1] = 0x80 | ((codepoint >> 6) & 0x3F); 70 | utf8_char[2] = 0x80 | (codepoint & 0x3F); 71 | utf8_len = 3; 72 | } else if (codepoint < 0x110000) { 73 | // 4-byte UTF-8 74 | utf8_char[0] = 0xF0 | (codepoint >> 18); 75 | utf8_char[1] = 0x80 | ((codepoint >> 12) & 0x3F); 76 | utf8_char[2] = 0x80 | ((codepoint >> 6) & 0x3F); 77 | utf8_char[3] = 0x80 | (codepoint & 0x3F); 78 | utf8_len = 4; 79 | } else { 80 | // Invalid Unicode code point 81 | return; 82 | } 83 | 84 | size_t len = strlen(input->buffer); 85 | 86 | if (len < INPUT_BUFFER_LEN - 1) { 87 | for (int i = len; i >= input->position; i--) { 88 | input->buffer[i + 1] = input->buffer[i]; 89 | } 90 | input->buffer[input->position] = (unsigned char)codepoint; 91 | input->position++; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/hr_list.c: -------------------------------------------------------------------------------- 1 | #include "hr_list.h" 2 | #include "animation.h" 3 | #include "signal.h" 4 | #include 5 | #include 6 | #include 7 | 8 | HrItem horizontalItems[10]; 9 | 10 | void init_horizontal_list(HorizontalList *hr_list) { 11 | const char *homedir; 12 | 13 | if ((homedir = getenv("HOME")) == NULL) { 14 | homedir = getpwuid(getuid())->pw_dir; 15 | } 16 | 17 | // Users category 18 | strcpy(horizontalItems[0].title, "Home"); 19 | strcpy(horizontalItems[0].path, homedir); 20 | strcpy(horizontalItems[0].icon, "\ue977"); 21 | 22 | // Settings category 23 | strcpy(horizontalItems[1].title, "Desktop"); 24 | sprintf(horizontalItems[1].path, "%s/%s", homedir, "Desktop"); 25 | strcpy(horizontalItems[1].icon, "\ue9b7"); 26 | 27 | // Add more categories like in the Vue code 28 | strcpy(horizontalItems[2].title, "Documents"); 29 | sprintf(horizontalItems[2].path, "%s/%s", homedir, "Documents"); 30 | strcpy(horizontalItems[2].icon, "\ue909"); 31 | 32 | // Songs category 33 | strcpy(horizontalItems[3].title, "Downloads"); 34 | sprintf(horizontalItems[3].path, "%s/%s", homedir, "Downloads"); 35 | strcpy(horizontalItems[3].icon, "\ue95f"); 36 | 37 | // Movies category 38 | strcpy(horizontalItems[4].title, "Pictures"); 39 | sprintf(horizontalItems[4].path, "%s/%s", homedir, "Pictures"); 40 | strcpy(horizontalItems[4].icon, "\ue978"); 41 | 42 | // Games category 43 | strcpy(horizontalItems[5].title, "Public"); 44 | sprintf(horizontalItems[5].path, "%s/%s", homedir, "Public"); 45 | strcpy(horizontalItems[5].icon, "\ueA07"); 46 | 47 | // Network category 48 | strcpy(horizontalItems[6].title, "Videos"); 49 | sprintf(horizontalItems[6].path, "%s/%s", homedir, "Videos"); 50 | strcpy(horizontalItems[6].icon, "\ue94d"); 51 | 52 | // Friends category 53 | strcpy(horizontalItems[7].title, "File System"); 54 | strcpy(horizontalItems[7].path, "/"); 55 | strcpy(horizontalItems[7].icon, "\ue958"); 56 | 57 | // Initialize animation state 58 | hr_list->items = horizontalItems; 59 | hr_list->items_count = 8; 60 | } 61 | 62 | void horizontal_list_event_handler(EventType type, void *context, void *data) { 63 | HorizontalList *list = (HorizontalList *)context; 64 | 65 | if (type == EVENT_HORIZONTAL_SELECTION_CHANGED) { 66 | SelectionData *sel_data = (SelectionData *)data; 67 | 68 | animation_remove_by_tag(HorizontalListTag); 69 | update_horizontal_list(list); 70 | 71 | // Navigate to the selected path 72 | NavigationData nav_data = { 73 | .selected_index = 0, 74 | .clear_history = true, 75 | .path = list->items[sel_data->index].path, 76 | }; 77 | emit_signal(EVENT_NAVIGATE_TO_PATH, &nav_data); 78 | } else if (type == EVENT_DIRECTORY_CONTENT_CHANGED) { 79 | DirectoryData *dir_data = (DirectoryData *)data; 80 | 81 | // Update depth as needed 82 | list->depth = dir_data->depth; 83 | 84 | // Update UI 85 | animation_remove_by_tag(HorizontalListTag); 86 | update_horizontal_list(list); 87 | } 88 | } 89 | 90 | void update_horizontal_list(HorizontalList *hr_list) { 91 | float offset = 150; 92 | float target = hr_list->selected * offset; 93 | 94 | if (hr_list->depth > 0) 95 | target += 50; 96 | 97 | animation_push(0.2, target, &hr_list->scroll, HorizontalListTag); 98 | } 99 | -------------------------------------------------------------------------------- /src/animation.c: -------------------------------------------------------------------------------- 1 | #include "animation.h" 2 | #include 3 | #include 4 | #include 5 | 6 | animation_t anim_st = {0}; 7 | 8 | static float get_current_time() { return glfwGetTime(); } 9 | 10 | void animation_push(float duration, float target, float *subject, AnimationTag tag) { 11 | animation_t *p_anim = &anim_st; 12 | 13 | tween_t t = { 14 | .tag = tag, 15 | .running_since = 0, 16 | .subject = subject, 17 | .duration = duration, 18 | .target_value = target, 19 | .initial_value = *subject, 20 | .start_time = get_current_time(), 21 | }; 22 | 23 | // init animation list 24 | if (p_anim->list == NULL) { 25 | if (p_anim->capacity) 26 | p_anim->capacity = 10; 27 | 28 | p_anim->list = malloc(p_anim->capacity * sizeof(tween_t)); 29 | p_anim->size = 0; 30 | } 31 | 32 | // add animation 33 | if (p_anim->size >= p_anim->capacity) { 34 | p_anim->capacity = p_anim->capacity > 0 ? p_anim->capacity * 2 : 1; 35 | p_anim->list = realloc(p_anim->list, p_anim->capacity * sizeof(tween_t)); 36 | } 37 | 38 | p_anim->list[p_anim->size++] = t; 39 | } 40 | 41 | float easeOutBounce(float x) { 42 | float n1 = 7.5625; 43 | float d1 = 2.75; 44 | if (x < 1 / d1) { 45 | return n1 * x * x; 46 | } else if (x < 2 / d1) { 47 | float v = x - 1.5 / d1; 48 | return n1 * v * x + 0.75; 49 | } else if (x < 2.5 / d1) { 50 | float v = x - 2.25 / d1; 51 | return n1 * v * x + 0.9375; 52 | } else { 53 | float v = x - 2.625 / d1; 54 | return n1 * v * x + 0.984375; 55 | } 56 | } 57 | 58 | float easeInOutExpo(float x) { 59 | return x == 0 ? 0 : x == 1 ? 1 : x < 0.5 ? powf(2.0, 20.0 * x - 10) / 2.0 : (2 - powf(2, -20 * x + 10)) / 2.0; 60 | } 61 | 62 | float ease(float x) { return x * x * (3 - 2 * x); } 63 | 64 | void gfx_remove_animation(animation_t *p_anim, size_t i) { 65 | if (i < 0 || i >= p_anim->size) 66 | return; 67 | 68 | for (int idx = i; idx < p_anim->size - 1; idx++) { 69 | p_anim->list[idx] = p_anim->list[idx + 1]; // Shift elements left 70 | } 71 | 72 | (p_anim->size)--; 73 | } 74 | 75 | void animation_update(float current_time) { 76 | animation_t *p_anim = &anim_st; 77 | 78 | // printf("animations: %zu\n", p_anim->size); 79 | for (int i = 0; i < p_anim->size; ++i) { 80 | tween_t *tween = &p_anim->list[i]; 81 | 82 | float elapsed = current_time - tween->start_time; 83 | tween->running_since = elapsed; 84 | 85 | float t = tween->running_since / tween->duration; 86 | if (t >= 1.0f) { 87 | *tween->subject = tween->target_value; 88 | gfx_remove_animation(p_anim, i); 89 | i--; 90 | } else { 91 | float eased = ease(t); 92 | // float eased = easeInOutExpo(t); 93 | *tween->subject = tween->initial_value + (tween->target_value - tween->initial_value) * eased; 94 | } 95 | } 96 | } 97 | 98 | void animation_remove_by_tag(AnimationTag tag) { 99 | animation_t *p_anim = &anim_st; 100 | 101 | for (int i = 0; i < p_anim->size; ++i) { 102 | tween_t *tween = &p_anim->list[i]; 103 | 104 | if (tween->tag == tag) { 105 | gfx_remove_animation(p_anim, i); 106 | i--; 107 | } 108 | } 109 | } 110 | 111 | void animation_clean() { 112 | animation_t *p_anim = &anim_st; 113 | if (!p_anim) 114 | return; 115 | // clean animation 116 | free(p_anim->list); 117 | memset(p_anim, 0, sizeof(*p_anim)); 118 | } 119 | -------------------------------------------------------------------------------- /include/ui.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define GL_GLEXT_PROTOTYPES 4 | #include 5 | 6 | #include 7 | 8 | #include 9 | #include FT_FREETYPE_H 10 | 11 | #include 12 | #include 13 | 14 | #define GLSL(src) "#version 430\n" #src 15 | 16 | typedef struct { 17 | float r, g, b, a; 18 | } Color; 19 | typedef struct { 20 | float tl, tr, br, bl; 21 | } Radius; 22 | 23 | typedef struct Vertex { 24 | float pos[2]; 25 | float size[2]; 26 | Color color; 27 | float pos_px[2]; 28 | Radius corner_radius; 29 | float texcoord[2]; // for glyphs 30 | float is_text; // 1.0 = text, 0.0 = shape 31 | } Vertex; 32 | 33 | #define BATCHES 10000 34 | 35 | #define FIRST_CHAR 32 36 | #define LAST_CHAR 126 37 | #define MAX_CHARS (LAST_CHAR - FIRST_CHAR + 1) 38 | 39 | // Atlas dimensions 40 | #define ATLAS_WIDTH 1024 41 | #define ATLAS_HEIGHT 1024 42 | 43 | // Maximum number of fonts that can be registered 44 | #define MAX_FONTS 8 45 | 46 | // Maximum number of font sizes per font 47 | #define MAX_FONT_SIZES 16 48 | 49 | typedef struct { 50 | int x, y; // Position in atlas 51 | int width, height; // Glyph dimensions 52 | float xoff, yoff; // Offset for positioning 53 | float xadvance; // Horizontal advance 54 | int codepoint; // harfbuzz Unicode codepoint 55 | int ft_codepoint; // Unicode codepoint 56 | int font_id; // Font identifier 57 | float size; // Font size 58 | bool used; // Is this slot used? 59 | } GlyphInfo; 60 | 61 | typedef struct { 62 | FT_Face face; 63 | char name[64]; 64 | float sizes[MAX_FONT_SIZES]; 65 | int size_count; 66 | hb_font_t *hb_font; 67 | hb_buffer_t *hb_buffer; 68 | } FontInfo; 69 | 70 | typedef struct { 71 | unsigned char *pixels; 72 | int width, height; 73 | FT_Library ft_library; 74 | FontInfo fonts[MAX_FONTS]; 75 | char font_names[MAX_FONTS][60]; 76 | int font_count; 77 | GlyphInfo *glyphs; 78 | int glyph_capacity; 79 | int glyph_count; 80 | int current_x, current_y; 81 | int current_line_height; 82 | GLuint texture_id; 83 | bool dirty; 84 | } FontAtlas; 85 | 86 | typedef struct { 87 | Vertex verts[BATCHES * 4]; 88 | uint32_t vert_count; 89 | uint32_t index_count; 90 | bool shape_in_progress; 91 | 92 | uint32_t program; 93 | uint32_t vertex_buffer; 94 | uint32_t vertex_array; 95 | 96 | FontAtlas *font_atlas; 97 | int current_font; 98 | } RenderState; 99 | 100 | // static const uint32_t indices[BATCHES * 6] = { 101 | // 0, 1, 2, // tri 1 102 | // 2, 3, 0, // tri 2 103 | // }; 104 | 105 | void begin_rect(float x, float y); 106 | void rect_size(float w, float h); 107 | void rect_color(float r, float g, float b, float a); 108 | void rect_gradient4(Color tl, Color tr, Color br, Color bl); 109 | void rect_gradient_topdown(Color top, Color bottom); 110 | void rect_gradient_sides(Color left, Color right); 111 | 112 | void rect_radius(float tl, float tr, float br, float bl); 113 | void rect_radius_all(float value); 114 | void end_rect(); 115 | void ui_create(); 116 | void start_frame(float width, float height); 117 | void render_ribbon(float width, float height, float current_time); 118 | void ui_delete(); 119 | void end_frame(); 120 | 121 | void add_rect(float x, float y, float w, float h, Color color[4]); 122 | 123 | void create_font_atlas(); 124 | void use_font(const char *name); 125 | int register_font(const char *name, const char *filename); 126 | void get_text_bounds(float size, const char *text, float *width, float *height, float *start_x, float *bearing_y); 127 | void draw_text(float size, float x, float y, const char *text, Color color); 128 | float draw_wrapped_text(float size, float x, float y, const char *text, Color color, float max_width); 129 | void draw_single_line(float font_size, float x, float y, const char *text, Color color, float max_width); 130 | void destroy_font_atlas(); 131 | -------------------------------------------------------------------------------- /src/vr_list.c: -------------------------------------------------------------------------------- 1 | #include "animation.h" 2 | #include 3 | #include "vr_list.h" 4 | 5 | void init_vertical_list(VerticalList *vr_list) { 6 | vr_list->above_subitem_offset = 0.0f; 7 | vr_list->above_item_offset = -1.5f; 8 | vr_list->active_item_factor = 1.0f; 9 | vr_list->under_item_offset = 1.0f; 10 | 11 | vr_list->icon_size = 28.0; 12 | vr_list->margins_screen_top = 200; 13 | vr_list->icon_spacing_vertical = 50.0; 14 | } 15 | 16 | void vertical_list_event_handler(EventType type, void *context, void *data) { 17 | VerticalList *list = (VerticalList *)context; 18 | 19 | if (type == EVENT_DIRECTORY_CONTENT_CHANGED) { 20 | DirectoryData *dir_data = (DirectoryData *)data; 21 | 22 | // Update list data 23 | list->items = dir_data->items; 24 | list->selected = dir_data->selected; 25 | list->items_count = dir_data->items_count; 26 | 27 | // Update UI 28 | animation_remove_by_tag(VerticalListTag); 29 | update_vertical_list(list); 30 | } else if (type == EVENT_VERTICAL_SELECTION_CHANGED) { 31 | SelectionData *sel_data = (SelectionData *)data; 32 | 33 | if (sel_data->index == list->selected) 34 | return; 35 | 36 | list->selected = sel_data->index; 37 | update_vertical_list(list); 38 | } 39 | } 40 | 41 | static float item_y(const VerticalList *list, int i, size_t current) { 42 | float icon_spacing_vertical = list->icon_spacing_vertical; 43 | 44 | if (i < (int)current) { 45 | if (list->depth > 1) 46 | return icon_spacing_vertical * (i - (int)current + list->above_subitem_offset); 47 | return icon_spacing_vertical * (i - (int)current + list->above_item_offset); 48 | } else if (i == (int)current) { 49 | return icon_spacing_vertical * list->active_item_factor; 50 | } 51 | 52 | return icon_spacing_vertical * (i - (int)current + list->under_item_offset); 53 | } 54 | 55 | static void find_visible_items(const VerticalList *list, uint32_t screen_height, uint32_t total_items, 56 | uint32_t selected_index, uint32_t *start_index, uint32_t *end_index) { 57 | float y_offset = list->margins_screen_top; 58 | 59 | *start_index = 0; 60 | *end_index = total_items > 0 ? (total_items - 1) : 0; 61 | 62 | unsigned i; 63 | // Look upward from the current index 64 | for (i = selected_index; i-- > 0;) { 65 | float item_bottom = item_y(list, i, selected_index) + y_offset + list->icon_size; 66 | if (item_bottom < 0) 67 | break; 68 | *start_index = i; 69 | } 70 | 71 | // Look downward from the current index 72 | for (i = selected_index + 1; i < total_items; ++i) { 73 | float item_top = item_y(list, i, selected_index) + y_offset; 74 | if (item_top > screen_height) 75 | break; 76 | *end_index = i; 77 | } 78 | } 79 | 80 | void update_vertical_list(VerticalList *list) { 81 | int threshold = 0; 82 | size_t selection = list->selected; 83 | 84 | if (list->items_count == 0) 85 | return; 86 | 87 | uint32_t end = (uint32_t)list->items_count; 88 | threshold = list->icon_size * 10; 89 | 90 | uint32_t height, entry_start, entry_end; 91 | list->get_screen_size(NULL, &height); 92 | find_visible_items(list, height, end, selection, &entry_start, &entry_end); 93 | 94 | list->entry_start = entry_start; 95 | list->entry_end = entry_end + 1; 96 | 97 | float default_zoom = 0.6; 98 | float default_alpha = 0.6; 99 | 100 | for (size_t i = 0; i < end; i++) { 101 | float y_pos, real_y_pos; 102 | float zoom = default_zoom; 103 | float label_alpha = default_alpha; 104 | struct file_entry *node = list->items[i]; 105 | 106 | y_pos = item_y(list, i, selection); 107 | real_y_pos = y_pos + list->margins_screen_top; 108 | 109 | if (i == selection) { 110 | label_alpha = 1.0; 111 | zoom = 1.0; 112 | } 113 | 114 | if (real_y_pos < -threshold || real_y_pos > height + threshold) { 115 | node->y = y_pos; 116 | node->zoom = zoom; 117 | node->alpha = node->label_alpha = label_alpha; 118 | } else { 119 | animation_push(0.2, y_pos, &node->y, VerticalListTag); 120 | animation_push(0.05, zoom, &node->zoom, VerticalListTag); 121 | animation_push(0.2, label_alpha, &node->alpha, VerticalListTag); 122 | animation_push(0.2, label_alpha, &node->label_alpha, VerticalListTag); 123 | 124 | // printf("IY: %f\n", iy); 125 | // node->y = iy; 126 | // node->zoom = iz; 127 | // node->alpha = node->label_alpha = ia; 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/draw.c: -------------------------------------------------------------------------------- 1 | #include "draw.h" 2 | #include "ui.h" 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | void draw_selected_item_title(const HorizontalList *hr_list) { 10 | Color text_color = {1.0, 1.0, 1.0, 0.9}; 11 | 12 | use_font("sans"); 13 | draw_text(24, 30, 40, hr_list->items[hr_list->selected].title, text_color); 14 | 15 | // subtitle 16 | draw_text(16, 30, 70, hr_list->items[hr_list->selected].path, text_color); 17 | } 18 | 19 | void draw_horizontal_item(float x, float y, float size, const char *icon, float scale_factor) { 20 | Color iconColor = {0, 0.07, 0.19, 1}; 21 | begin_rect(x - size / 2.0, y - size / 2.0); 22 | rect_size(size, size); 23 | rect_radius_all(14 * scale_factor); 24 | rect_color(1, 1, 1, 1); 25 | end_rect(); 26 | 27 | // icon 28 | float fsize = 20 * scale_factor; 29 | 30 | float w, h, by; 31 | get_text_bounds(fsize, icon, &w, &h, NULL, &by); 32 | 33 | float ty = y + by / 2.0; 34 | float tx = x - w / 2.0; 35 | 36 | draw_text(fsize, tx, ty, icon, iconColor); 37 | } 38 | 39 | void draw_horizontal_menu(const HorizontalList *hr_list, int x, int y) { 40 | float gap = 150.0f; 41 | float base_x = x - hr_list->scroll; 42 | 43 | // draw title 44 | draw_selected_item_title(hr_list); 45 | float size = 56; 46 | 47 | // icon 48 | use_font("icon"); 49 | 50 | if (hr_list->depth > 0) { 51 | float scale_factor = 1.5f; 52 | float x = base_x + (hr_list->selected * gap); 53 | 54 | draw_horizontal_item(x, y, size * scale_factor, hr_list->items[hr_list->selected].icon, scale_factor); 55 | 56 | return; 57 | } 58 | 59 | for (int i = 0; i < hr_list->items_count; i++) { 60 | float x = base_x + (i * gap); 61 | 62 | // Calculate dynamic scale based on proximity to selected item 63 | float distance = abs(i - hr_list->selected); 64 | float scale_factor = 1.0f; 65 | if (distance == 0) { 66 | scale_factor = 1.5; 67 | } 68 | 69 | draw_horizontal_item(x, y, size * scale_factor, hr_list->items[i].icon, scale_factor); 70 | } 71 | } 72 | 73 | void draw_folder_path(const HorizontalList *hr_list, const char *path, float x, float y) { 74 | if (hr_list->depth == 0) 75 | return; 76 | 77 | use_font("sans"); 78 | draw_single_line(12, x, y, path, (Color){1, 1, 1, 1}, 600); 79 | } 80 | 81 | void draw_vertical_list(const VerticalList *list, float start_x) { 82 | if (list->items_count == 0) 83 | return; 84 | 85 | for (int i = list->entry_start; i < list->entry_end; i++) { 86 | struct file_entry *node = list->items[i]; 87 | 88 | float x = start_x + node->x; 89 | float y = list->margins_screen_top + node->y; 90 | 91 | Color icon_color = {1, 1, 1, node->alpha}; 92 | 93 | float size = list->icon_size + (node->zoom == 1 ? 10 : 0); 94 | 95 | use_font("icon"); 96 | draw_text(size, x - size / 2, y + size / 2 - 2, node->type == TYPE_DIRECTORY ? "\ue950" : "\ue96d", icon_color); 97 | 98 | Color text_color = {1, 1, 1, node->label_alpha}; 99 | 100 | use_font("sans"); 101 | 102 | char name[60]; 103 | if (strlen(node->name) > 59) { 104 | memcpy(name, &node->name, 59); 105 | name[59] = '\0'; 106 | } else { 107 | memcpy(name, &node->name, strlen(node->name)); 108 | name[strlen(node->name)] = '\0'; 109 | } 110 | 111 | float w, h, by, sx; 112 | float fsize = 12 + (node->zoom == 1 ? 4 : 0); 113 | get_text_bounds(fsize, name, &w, &h, &sx, &by); 114 | 115 | float ty = y + by / 2; 116 | float tx = x - sx; 117 | 118 | draw_single_line(fsize, tx + 50, ty, name, text_color, 600); 119 | } 120 | 121 | // selected item always in place 122 | float y = list->margins_screen_top + list->icon_spacing_vertical; 123 | float pulse = 0.5f + 0.3f * sinf(glfwGetTime() * 3.0f); 124 | 125 | begin_rect(start_x + 35, y - 20); 126 | rect_size(600, 40); 127 | rect_radius(10, 10, 10, 10); 128 | rect_color(1, 1, 1, 0.2 * pulse); 129 | end_rect(); 130 | } 131 | 132 | void draw_text_preview(DrawState *state) { 133 | if (!state->show_preview) 134 | return; 135 | 136 | begin_rect(0, 0); 137 | rect_size(state->width, state->height); 138 | rect_color(0, 0, 0, .8); 139 | end_rect(); 140 | 141 | float x = state->width / 10.0; 142 | float y = state->height / 10.0; 143 | 144 | begin_rect(x, y); 145 | rect_size(state->width - 2 * x, state->height - 2 * y); 146 | rect_radius(20, 20, 20, 20); 147 | rect_color(1, 1, 1, 1); 148 | end_rect(); 149 | 150 | float padding = 20; 151 | float content_width = state->width - 2 * x; 152 | 153 | x += padding; 154 | y += padding; 155 | content_width -= padding * 2; 156 | 157 | use_font("sans"); 158 | draw_wrapped_text(16, x, y + 10, state->buffer, (Color){0, 0, 0, 1}, content_width); 159 | } 160 | 161 | static char *readable_fs(double bytes, char *buf) { 162 | int i = 0; 163 | const char *units[] = {"B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"}; 164 | while (bytes > 1024) { 165 | bytes /= 1024; 166 | i++; 167 | } 168 | sprintf(buf, "%.*f %s", i, bytes, units[i]); 169 | return buf; 170 | } 171 | 172 | static float draw_section(float x, float y, const char *key, const char *value, Color color, float max_width) { 173 | draw_text(16, x, y, key, color); 174 | draw_wrapped_text(16, x + 160, y, value, color, max_width); 175 | return 16; 176 | } 177 | 178 | void draw_info(const VerticalList *vr_list, float width, float height) { 179 | struct file_entry *current = vr_list->items[vr_list->selected]; 180 | if (current == NULL) 181 | return; 182 | 183 | use_font("sans"); 184 | Color color = {1, 1, 1, 1}; 185 | 186 | char buf[20]; 187 | struct tm access_lt; 188 | localtime_r(¤t->access_time, &access_lt); 189 | char access_timbuf[80]; 190 | strftime(access_timbuf, sizeof(access_timbuf), "%c", &access_lt); 191 | 192 | struct tm modified_lt; 193 | localtime_r(¤t->modified_time, &modified_lt); 194 | char modified_timbuf[80]; 195 | strftime(modified_timbuf, sizeof(access_timbuf), "%c", &access_lt); 196 | 197 | float x = 100; 198 | float y = 100; 199 | float gap = 20; 200 | float wrap_width = width - 40; 201 | float total_height = 5 * 16 + gap * 4; 202 | y = height / 2 - total_height / 2; 203 | 204 | y += draw_section(x, y, " Name", current->name, color, wrap_width) + gap; 205 | y += draw_section(x, y, " Path", current->path, color, wrap_width) + gap; 206 | y += draw_section(x, y, " Access Time", access_timbuf, color, wrap_width) + gap; 207 | y += draw_section(x, y, "Modified Time", modified_timbuf, color, wrap_width) + gap; 208 | y += draw_section(x, y, " Size", readable_fs(current->size, buf), color, wrap_width); 209 | } 210 | 211 | void draw_option_list_depth(float x, float y, Options *list) { 212 | Color color = {1, 1, 1, 1}; 213 | Color muted_color = {1, 1, 1, .5}; 214 | float rect_w = OPTION_LIST_WIDTH; 215 | 216 | for (size_t i = 0; i < list->items_count; ++i) { 217 | use_font("sans"); 218 | draw_text(16, x + 20, y + 40 * i, list->items[i].title, i == list->selected ? color : muted_color); 219 | if (list->items[i].submenu) { 220 | use_font("icon"); 221 | draw_text(16, x + rect_w - 40, y + 40 * i, "\ue942", i == list->selected ? color : muted_color); 222 | } 223 | } 224 | } 225 | 226 | void draw_option_list(OptionList *op_list, DrawState *state) { 227 | float rect_w = OPTION_LIST_WIDTH; 228 | float x = state->width + op_list->x; 229 | 230 | begin_rect(x, 0); 231 | rect_size(x + state->width, state->height); 232 | rect_gradient_sides((Color){0, 0, 0, .8}, (Color){0, 0, 0, .3}); 233 | end_rect(); 234 | 235 | Options *menus[op_list->depth == 0 ? 1 : op_list->depth]; // Assuming max depth of 10 236 | int depth = 0; 237 | 238 | // Start from current and go to root 239 | Options *current = op_list->current; 240 | while (current) { 241 | menus[depth++] = current; 242 | current = current->parent; 243 | } 244 | 245 | for (int i = depth - 1; i >= 0; i--) { 246 | float total_height = 40. * menus[i]->items_count; 247 | float start_y = state->height / 2.0 - total_height / 2.0; 248 | 249 | draw_option_list_depth(x + (depth - 1 - i) * rect_w, start_y, menus[i]); 250 | } 251 | } 252 | 253 | void draw_input_field(Input *input, const char *title, DrawState *state) { 254 | if (!input->is_visible) 255 | return; 256 | 257 | begin_rect(0, 0); 258 | rect_size(state->width, state->height); 259 | rect_color(0, 0, 0, .3); 260 | end_rect(); 261 | 262 | float w = 400; 263 | float h = 200; 264 | 265 | float x = state->width / 2.0 - w / 2.0; 266 | float y = state->height / 2.0 - h / 2.0; 267 | begin_rect(x, y); 268 | rect_size(w, h); 269 | rect_radius(20, 20, 20, 20); 270 | rect_color(1, 1, 1, 1); 271 | end_rect(); 272 | 273 | float padding = 20; 274 | 275 | x += padding; 276 | y += padding; 277 | 278 | use_font("sans"); 279 | float tw, th; 280 | get_text_bounds(16, title, &tw, &th, NULL, NULL); 281 | draw_text(16, state->width / 2.0 - tw / 2.0, y + padding, title, (Color){0, 0, 0, 1}); 282 | 283 | // draw cursor 284 | float twc = 0, thc = 30; 285 | 286 | if (strlen(input->buffer) > 0) { 287 | char temp[INPUT_BUFFER_LEN]; 288 | strncpy(temp, input->buffer, input->position); 289 | temp[input->position] = '\0'; 290 | 291 | get_text_bounds(20, temp, &twc, NULL, NULL, NULL); 292 | } 293 | 294 | begin_rect(x + twc, y - padding + h / 2); 295 | rect_size(1, thc); 296 | rect_color(1, 0, 0, 1); 297 | end_rect(); 298 | 299 | draw_text(20, x, y + h / 2, input->buffer, (Color){0, 0, 0, 1}); 300 | } 301 | 302 | void draw_dialog(Dialog *dialog, DrawState *state, const char *path) { 303 | begin_rect(0, 0); 304 | rect_size(state->width, state->height); 305 | rect_color(0, 0, 0, .7); 306 | end_rect(); 307 | 308 | begin_rect(0, 100); 309 | rect_size(state->width, 1); 310 | rect_color(1, 1, 1, .7); 311 | end_rect(); 312 | 313 | begin_rect(0, state->height - 100); 314 | rect_size(state->width, 1); 315 | rect_color(1, 1, 1, .7); 316 | end_rect(); 317 | 318 | float mid_width = state->width / 2.0; 319 | float mid_height = state->height / 2.0; 320 | 321 | use_font("sans"); 322 | float tw, th; 323 | get_text_bounds(18, dialog->content, &tw, &th, NULL, NULL); 324 | draw_text(18, state->width / 2.0 - tw / 2.0, mid_height - 80, dialog->content, (Color){1, 1, 1, 1}); 325 | 326 | float pw, ph; 327 | get_text_bounds(18, path, &pw, &ph, NULL, NULL); 328 | draw_text(18, state->width / 2.0 - pw / 2.0, mid_height - 50, path, (Color){1, 0, 0, 1}); 329 | 330 | float gap = 60; 331 | float button_h = 40; 332 | float button_w = 100; 333 | 334 | begin_rect(mid_width + (button_w + gap) * dialog->position + gap / 2, mid_height); 335 | rect_size(button_w, button_h); 336 | 337 | rect_color(1, 1 + dialog->position, 1 + dialog->position, .3); 338 | rect_radius_all(5); 339 | end_rect(); 340 | 341 | float yw, yh; 342 | get_text_bounds(18, "Yes", &yw, &yh, NULL, NULL); 343 | float nw, nh; 344 | get_text_bounds(18, "No", &nw, &nh, NULL, NULL); 345 | 346 | float yx = mid_width - 0.5f * (button_w + gap + yw); 347 | draw_text(18, yx, mid_height + yh, "Yes", (Color){1, 1, 1, 1}); 348 | 349 | float nx = mid_width + 0.5f * (button_w + gap) - 0.5f * nw; 350 | draw_text(18, nx, mid_height + nh, "No", (Color){1, 1, 1, 1}); 351 | } 352 | -------------------------------------------------------------------------------- /src/fm.c: -------------------------------------------------------------------------------- 1 | #include "fm.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | static bool show_file(struct dirent *entry) { 14 | if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) 15 | return false; 16 | if (entry->d_name[0] == '.') 17 | return false; 18 | return true; 19 | } 20 | 21 | static int compare_by_name(const void *a, const void *b) { 22 | FileEntry *entry_a = *(FileEntry **)a; 23 | FileEntry *entry_b = *(FileEntry **)b; 24 | 25 | // Directories first, then files 26 | if (entry_a->type == 1 && entry_b->type != 1) 27 | return -1; 28 | if (entry_a->type != 1 && entry_b->type == 1) 29 | return 1; 30 | 31 | return strcasecmp(entry_a->name, entry_b->name); 32 | } 33 | 34 | static int compare_by_size(const void *a, const void *b) { 35 | FileEntry *entry_a = *(FileEntry **)a; 36 | FileEntry *entry_b = *(FileEntry **)b; 37 | 38 | // Directories first, then files 39 | if (entry_a->type == TYPE_DIRECTORY && entry_b->type != TYPE_DIRECTORY) 40 | return -1; 41 | if (entry_a->type != TYPE_DIRECTORY && entry_b->type == TYPE_DIRECTORY) 42 | return 1; 43 | 44 | if (entry_a->size < entry_b->size) 45 | return -1; 46 | if (entry_a->size > entry_b->size) 47 | return 1; 48 | return 0; 49 | } 50 | 51 | static int compare_by_date(const void *a, const void *b) { 52 | FileEntry *entry_a = *(FileEntry **)a; 53 | FileEntry *entry_b = *(FileEntry **)b; 54 | 55 | if (entry_a->modified_time < entry_b->modified_time) 56 | return -1; 57 | if (entry_a->modified_time > entry_b->modified_time) 58 | return 1; 59 | return 0; 60 | } 61 | 62 | static void sort_entries(FileManager *fm) { 63 | if (!fm || !fm->current_dir || !fm->current_dir->children) 64 | return; 65 | 66 | int (*compare_func)(const void *, const void *); 67 | 68 | switch (fm->sort_mode) { 69 | case SortByDate: 70 | compare_func = compare_by_date; 71 | break; 72 | case SortBySize: 73 | compare_func = compare_by_size; 74 | break; 75 | case SortByName: 76 | default: 77 | compare_func = compare_by_name; 78 | break; 79 | } 80 | 81 | // Sort entries 82 | qsort(fm->current_dir->children, fm->current_dir->child_count, sizeof(FileEntry *), compare_func); 83 | 84 | // Reverse if needed 85 | if (fm->reverse_sort && fm->current_dir->child_count > 1) { 86 | for (size_t i = 0; i < fm->current_dir->child_count / 2; i++) { 87 | FileEntry *temp = fm->current_dir->children[i]; 88 | fm->current_dir->children[i] = fm->current_dir->children[fm->current_dir->child_count - 1 - i]; 89 | fm->current_dir->children[fm->current_dir->child_count - 1 - i] = temp; 90 | } 91 | } 92 | } 93 | 94 | static void load_entry_info(const char *path, FileEntry *entry) { 95 | struct stat st; 96 | if (stat(path, &st) == 0) { 97 | entry->size = st.st_size; 98 | entry->permissions = st.st_mode; 99 | entry->access_time = st.st_atime; 100 | entry->modified_time = st.st_mtime; 101 | 102 | if (S_ISDIR(st.st_mode)) 103 | entry->type = TYPE_DIRECTORY; 104 | else if (S_ISLNK(st.st_mode)) 105 | entry->type = TYPE_SYMLINK; 106 | else 107 | entry->type = TYPE_FILE; 108 | } 109 | } 110 | 111 | static FileEntry *create_file_entry(const char *path) { 112 | FileEntry *entry = (FileEntry *)malloc(sizeof(FileEntry)); 113 | if (!entry) 114 | return NULL; 115 | 116 | memset(entry, 0, sizeof(FileEntry)); 117 | entry->y = 0; 118 | 119 | // Get filename from path 120 | const char *name = strrchr(path, '/'); 121 | if (name) 122 | strncpy(entry->name, name + 1, sizeof(entry->name) - 1); 123 | else 124 | strncpy(entry->name, path, sizeof(entry->name) - 1); 125 | 126 | strncpy(entry->path, path, sizeof(entry->path) - 1); 127 | 128 | load_entry_info(path, entry); 129 | 130 | entry->children = NULL; 131 | entry->child_count = 0; 132 | entry->parent = NULL; 133 | 134 | return entry; 135 | } 136 | 137 | static int read_directory(FileEntry *dir) { 138 | if (!dir) 139 | return -1; 140 | 141 | if (dir->children) { 142 | for (size_t i = 0; i < dir->child_count; ++i) { 143 | free(dir->children[i]); 144 | } 145 | 146 | free(dir->children); 147 | } 148 | 149 | DIR *d = opendir(dir->path); 150 | if (!d) { 151 | fprintf(stderr, "Couldn't open directory: %s\n", dir->path); 152 | return -1; 153 | } 154 | 155 | struct dirent *entry; 156 | size_t count = 0; 157 | 158 | while ((entry = readdir(d)) != NULL) { 159 | // Skip . and .. 160 | if (!show_file(entry)) 161 | continue; 162 | 163 | count++; 164 | } 165 | 166 | dir->children = (FileEntry **)malloc(count * sizeof(FileEntry *)); 167 | if (!dir->children) { 168 | closedir(d); 169 | return -1; 170 | } 171 | 172 | // Rewind and read entries 173 | rewinddir(d); 174 | size_t index = 0; 175 | 176 | while ((entry = readdir(d)) != NULL && index < count) { 177 | if (!show_file(entry)) 178 | continue; 179 | 180 | char full_path[1024]; 181 | if (strcmp(dir->path, "/") == 0) { 182 | snprintf(full_path, sizeof(full_path), "/%s", entry->d_name); 183 | } else { 184 | snprintf(full_path, sizeof(full_path), "%s/%s", dir->path, entry->d_name); 185 | } 186 | 187 | FileEntry *file_entry = create_file_entry(full_path); 188 | if (file_entry) { 189 | file_entry->parent = dir; 190 | dir->children[index++] = file_entry; 191 | } 192 | } 193 | 194 | dir->child_count = index; 195 | closedir(d); 196 | 197 | return 0; 198 | } 199 | 200 | static int find_index_of(FileManager *fm, const char *path, int default_index) { 201 | for (size_t i = 0; i < fm->current_dir->child_count; ++i) { 202 | if (strcmp(fm->current_dir->children[i]->path, path) == 0) { 203 | return i; 204 | } 205 | } 206 | 207 | return default_index; 208 | } 209 | 210 | static void free_file_entry(FileEntry *entry) { 211 | for (size_t i = 0; i < entry->child_count; ++i) { 212 | free(entry->children[i]); 213 | } 214 | 215 | free(entry->children); 216 | free(entry); 217 | } 218 | 219 | static void free_history(FileManager *fm) { 220 | FileEntry *entry = fm->current_dir; 221 | while (entry) { 222 | FileEntry *parent = entry->parent; 223 | free_file_entry(entry); 224 | entry = parent; 225 | } 226 | } 227 | 228 | FileManager *create_file_manager(const char *path) { 229 | FileManager *fm = (FileManager *)malloc(sizeof(FileManager)); 230 | if (!fm) 231 | return NULL; 232 | 233 | memset(fm, 0, sizeof(FileManager)); 234 | 235 | fm->sort_mode = SortByName; 236 | fm->show_hidden = false; // unimplemented 237 | fm->reverse_sort = false; 238 | fm->action_target_index = -1; 239 | 240 | fm->current_dir = create_file_entry(path); 241 | fm->current_dir->parent = NULL; 242 | read_directory(fm->current_dir); 243 | sort_entries(fm); 244 | 245 | return fm; 246 | } 247 | 248 | void change_directory(FileManager *fm, const char *path) { 249 | FileEntry *parent = fm->current_dir; 250 | 251 | fm->current_dir = create_file_entry(path); 252 | fm->current_dir->parent = parent; 253 | fm->depth++; 254 | read_directory(fm->current_dir); 255 | sort_entries(fm); 256 | } 257 | 258 | void switch_directory(FileManager *fm, const char *path) { 259 | free_history(fm); 260 | 261 | fm->current_dir = create_file_entry(path); 262 | fm->current_dir->parent = NULL; 263 | fm->depth = 0; 264 | read_directory(fm->current_dir); 265 | sort_entries(fm); 266 | } 267 | 268 | int navigate_back(FileManager *fm) { 269 | if (fm->current_dir->parent == NULL) 270 | return -1; 271 | 272 | FileEntry *old = fm->current_dir; 273 | 274 | fm->current_dir = fm->current_dir->parent; 275 | read_directory(fm->current_dir); 276 | sort_entries(fm); 277 | fm->depth--; 278 | 279 | int index = find_index_of(fm, old->path, 0); 280 | 281 | free_file_entry(old); 282 | return index; 283 | } 284 | 285 | void free_file_manager(FileManager *fm) { 286 | free_history(fm); 287 | free(fm); 288 | } 289 | 290 | bool get_mime_type(const char *path, const char *test) { 291 | bool ret = false; 292 | const char *mimetype; 293 | magic_t magic; 294 | 295 | if ((magic = magic_open(MAGIC_MIME_TYPE)) == NULL) { 296 | fprintf(stderr, "Error opening libmagic.\n"); 297 | return ret; 298 | } 299 | if (magic_load(magic, NULL) == -1) { 300 | fprintf(stderr, "Error magic_load.\n"); 301 | magic_close(magic); 302 | return ret; 303 | } 304 | if ((mimetype = magic_file(magic, path)) == NULL) { 305 | fprintf(stderr, "Error getting mimetype.\n"); 306 | magic_close(magic); 307 | return ret; 308 | } 309 | 310 | ret = strstr(mimetype, test) != NULL; 311 | 312 | magic_close(magic); 313 | return ret; 314 | } 315 | 316 | static void xdg_open(const char *str) { 317 | if (fork() == 0) { 318 | // redirect stdout and stderr to null 319 | int fd = open("/dev/null", O_WRONLY); 320 | dup2(fd, 1); 321 | dup2(fd, 2); 322 | close(fd); 323 | execl("/usr/bin/xdg-open", "/usr/bin/xdg-open", str, (char *)0); 324 | } 325 | } 326 | 327 | void open_file(const char *path) { 328 | if (!access("/usr/bin/xdg-open", X_OK)) { 329 | xdg_open(path); 330 | } else { 331 | fprintf(stderr, "no xdg-open.\n"); 332 | } 333 | } 334 | 335 | void read_file_content(const char *filename, char *buffer, size_t len) { 336 | FILE *file = fopen(filename, "rb"); 337 | if (!file) { 338 | fprintf(stderr, "File couldn't be opened.\n"); 339 | return; 340 | } 341 | 342 | size_t bytesRead = fread(buffer, 1, len - 1, file); 343 | buffer[bytesRead] = '\0'; 344 | fclose(file); 345 | } 346 | 347 | static int stristr(const char *haystack, const char *needle) { 348 | if (!haystack || !needle) 349 | return 0; 350 | 351 | for (; *haystack; haystack++) { 352 | const char *h = haystack; 353 | const char *n = needle; 354 | 355 | while (*h && *n && tolower((unsigned char)*h) == tolower((unsigned char)*n)) { 356 | h++; 357 | n++; 358 | } 359 | 360 | if (*n == '\0') { 361 | return 1; // Found 362 | } 363 | } 364 | 365 | return 0; // Not found 366 | } 367 | 368 | int search_file_name(FileManager *fm, const char *keyword) { 369 | for (size_t i = 0; i < fm->current_dir->child_count; i++) { 370 | if (stristr(fm->current_dir->children[i]->name, keyword)) { 371 | return i; 372 | } 373 | } 374 | 375 | return -1; 376 | } 377 | 378 | bool fm_rename(FileManager *fm, const char *new_name) { 379 | if (fm->action_target_index == -1) { 380 | fprintf(stderr, "No target index\n"); 381 | return false; 382 | } 383 | 384 | struct file_entry *current = fm->current_dir->children[fm->action_target_index]; 385 | 386 | char target[1024]; 387 | snprintf(target, sizeof(target), "%s/%s", current->parent->path, new_name); 388 | 389 | if (strcmp(current->path, target) == 0) { 390 | fprintf(stderr, "Can't rename file with same name.\n"); 391 | return false; 392 | } 393 | 394 | if (rename(current->path, target) == 0) { 395 | strcpy(current->path, target); 396 | strcpy(current->name, new_name); 397 | load_entry_info(target, current); 398 | return true; 399 | } 400 | return false; 401 | } 402 | 403 | bool fm_create_dir(FileManager *fm, const char *name) { 404 | struct file_entry *current = fm->current_dir; 405 | size_t len = strlen(current->path) + strlen(name) + 2; 406 | char target[len]; 407 | snprintf(target, len, "%s/%s", current->path, name); 408 | target[len] = 0; 409 | 410 | bool ret = mkdir(target, 0700) == 0; 411 | read_directory(current); 412 | return ret; 413 | } 414 | 415 | static bool delete_path(FileManager *fm, const char *path) { 416 | DIR *dir = opendir(path); 417 | if (!dir) { 418 | return remove(path) == 0; // Try to remove if it's not a directory 419 | } 420 | 421 | struct dirent *entry; 422 | char fullpath[1024]; 423 | 424 | while ((entry = readdir(dir))) { 425 | if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) 426 | continue; 427 | 428 | snprintf(fullpath, sizeof(fullpath), "%s/%s", path, entry->d_name); 429 | 430 | struct stat statbuf; 431 | if (stat(fullpath, &statbuf) == 0) { 432 | if (S_ISDIR(statbuf.st_mode)) { 433 | delete_path(fm, fullpath); // Recurse into subdir 434 | } else { 435 | remove(fullpath); // Delete file 436 | } 437 | } 438 | } 439 | 440 | closedir(dir); 441 | return remove(path) == 0; 442 | } 443 | 444 | bool fm_delete_entry(FileManager *fm) { 445 | if (fm->action_target_index == -1) { 446 | fprintf(stderr, "No target index\n"); 447 | return false; 448 | } 449 | 450 | struct file_entry *current = fm->current_dir->children[fm->action_target_index]; 451 | bool ret = delete_path(fm, current->path); 452 | read_directory(fm->current_dir); 453 | return ret; 454 | } 455 | -------------------------------------------------------------------------------- /src/ribbon.c: -------------------------------------------------------------------------------- 1 | #include "ribbon.h" 2 | #include "ui.h" 3 | 4 | RibbonState ribbonState = {}; 5 | 6 | static Color gradient_golden[4] = { 7 | {174 / 255.0, 123 / 255.0, 44 / 255.0, 1.0}, 8 | {205 / 255.0, 174 / 255.0, 84 / 255.0, 1.0}, 9 | {58 / 255.0, 43 / 255.0, 24 / 255.0, 1.0}, 10 | {58 / 255.0, 43 / 255.0, 24 / 255.0, 1.0}, 11 | }; 12 | 13 | static Color gradient_legacy_red[4] = { 14 | {171 / 255.0, 70 / 255.0, 59 / 255.0, 1.0}, 15 | {171 / 255.0, 70 / 255.0, 59 / 255.0, 1.0}, 16 | {190 / 255.0, 80 / 255.0, 69 / 255.0, 1.0}, 17 | {190 / 255.0, 80 / 255.0, 69 / 255.0, 1.0}, 18 | }; 19 | 20 | static Color gradient_electric_blue[4] = { 21 | {1 / 255.0, 2 / 255.0, 67 / 255.0, 1.0}, 22 | {1 / 255.0, 73 / 255.0, 183 / 255.0, 1.0}, 23 | {1 / 255.0, 93 / 255.0, 194 / 255.0, 1.0}, 24 | {3 / 255.0, 162 / 255.0, 254 / 255.0, 1.0}, 25 | }; 26 | 27 | static Color gradient_dark_purple[4] = { 28 | {20 / 255.0, 13 / 255.0, 20 / 255.0, 1.0}, 29 | {20 / 255.0, 13 / 255.0, 20 / 255.0, 1.0}, 30 | {92 / 255.0, 44 / 255.0, 92 / 255.0, 1.0}, 31 | {148 / 255.0, 90 / 255.0, 148 / 255.0, 1.0}, 32 | }; 33 | 34 | static Color gradient_midnight_blue[4] = { 35 | {44 / 255.0, 62 / 255.0, 80 / 255.0, 1.0}, 36 | {44 / 255.0, 62 / 255.0, 80 / 255.0, 1.0}, 37 | {44 / 255.0, 62 / 255.0, 80 / 255.0, 1.0}, 38 | {44 / 255.0, 62 / 255.0, 80 / 255.0, 1.0}, 39 | }; 40 | 41 | static Color gradient_apple_green[4] = { 42 | {102 / 255.0, 134 / 255.0, 58 / 255.0, 1.0}, 43 | {122 / 255.0, 131 / 255.0, 52 / 255.0, 1.0}, 44 | {82 / 255.0, 101 / 255.0, 35 / 255.0, 1.0}, 45 | {63 / 255.0, 95 / 255.0, 30 / 255.0, 1.0}, 46 | }; 47 | 48 | static Color gradient_undersea[4] = { 49 | {23 / 255.0, 18 / 255.0, 41 / 255.0, 1.0}, 50 | {30 / 255.0, 72 / 255.0, 114 / 255.0, 1.0}, 51 | {52 / 255.0, 88 / 255.0, 110 / 255.0, 1.0}, 52 | {69 / 255.0, 125 / 255.0, 140 / 255.0, 1.0}, 53 | }; 54 | 55 | static Color gradient_morning_blue[4] = { 56 | {221 / 255.0, 241 / 255.0, 254 / 255.0, 1.0}, 57 | {135 / 255.0, 206 / 255.0, 250 / 255.0, 1.0}, 58 | {0.7, 0.7, 0.7, 1.0}, 59 | {170 / 255.0, 200 / 255.0, 252 / 255.0, 1.0}, 60 | }; 61 | 62 | static Color gradient_sunbeam[4] = { 63 | {20 / 255.0, 13 / 255.0, 20 / 255.0, 1.0}, 64 | {30 / 255.0, 72 / 255.0, 114 / 255.0, 1.0}, 65 | {0.7, 0.7, 0.7, 1.0}, 66 | {0.1, 0.0, 0.1, 1.0}, 67 | }; 68 | 69 | static Color gradient_lime_green[4] = { 70 | {209 / 255.0, 255 / 255.0, 82 / 255.0, 1.0}, 71 | {146 / 255.0, 232 / 255.0, 66 / 255.0, 1.0}, 72 | {82 / 255.0, 101 / 255.0, 35 / 255.0, 1.0}, 73 | {63 / 255.0, 95 / 255.0, 30 / 255.0, 1.0}, 74 | }; 75 | 76 | static Color gradient_pikachu_yellow[4] = { 77 | {63 / 255.0, 63 / 255.0, 1 / 255.0, 1.0}, 78 | {174 / 255.0, 174 / 255.0, 1 / 255.0, 1.0}, 79 | {191 / 255.0, 194 / 255.0, 1 / 255.0, 1.0}, 80 | {254 / 255.0, 221 / 255.0, 3 / 255.0, 1.0}, 81 | }; 82 | 83 | static Color gradient_gamecube_purple[4] = { 84 | {40 / 255.0, 20 / 255.0, 91 / 255.0, 1.0}, 85 | {160 / 255.0, 140 / 255.0, 211 / 255.0, 1.0}, 86 | {107 / 255.0, 92 / 255.0, 177 / 255.0, 1.0}, 87 | {84 / 255.0, 71 / 255.0, 132 / 255.0, 1.0}, 88 | }; 89 | 90 | static Color gradient_famicom_red[4] = { 91 | {255 / 255.0, 191 / 255.0, 171 / 255.0, 1.0}, 92 | {119 / 255.0, 49 / 255.0, 28 / 255.0, 1.0}, 93 | {148 / 255.0, 10 / 255.0, 36 / 255.0, 1.0}, 94 | {206 / 255.0, 126 / 255.0, 110 / 255.0, 1.0}, 95 | }; 96 | 97 | static Color gradient_flaming_hot[4] = { 98 | {231 / 255.0, 53 / 255.0, 53 / 255.0, 1.0}, 99 | {242 / 255.0, 138 / 255.0, 97 / 255.0, 1.0}, 100 | {236 / 255.0, 97 / 255.0, 76 / 255.0, 1.0}, 101 | {255 / 255.0, 125 / 255.0, 3 / 255.0, 1.0}, 102 | }; 103 | 104 | static Color gradient_ice_cold[4] = { 105 | {66 / 255.0, 183 / 255.0, 229 / 255.0, 1.0}, 106 | {29 / 255.0, 164 / 255.0, 255 / 255.0, 1.0}, 107 | {176 / 255.0, 255 / 255.0, 247 / 255.0, 1.0}, 108 | {174 / 255.0, 240 / 255.0, 255 / 255.0, 1.0}, 109 | }; 110 | 111 | static Color gradient_midgar[4] = { 112 | {255 / 255.0, 0 / 255.0, 0 / 255.0, 1.0}, 113 | {0 / 255.0, 0 / 255.0, 255 / 255.0, 1.0}, 114 | {0 / 255.0, 255 / 255.0, 0 / 255.0, 1.0}, 115 | {32 / 255.0, 32 / 255.0, 32 / 255.0, 1.0}, 116 | }; 117 | 118 | static Color gradient_volcanic_red[4] = { 119 | {1.0, 0.0, 0.1, 1.0}, 120 | {1.0, 0.1, 0.0, 1.0}, 121 | {0.1, 0.0, 0.1, 1.0}, 122 | {0.1, 0.0, 0.1, 1.0}, 123 | }; 124 | 125 | static Color gradient_dark[4] = { 126 | {0.05, 0.05, 0.05, 1.0}, 127 | {0.05, 0.05, 0.05, 1.0}, 128 | {0.05, 0.05, 0.05, 1.0}, 129 | {0.05, 0.05, 0.05, 1.0}, 130 | }; 131 | 132 | static Color gradient_light[4] = { 133 | {0.50, 0.50, 0.50, 1.0}, 134 | {0.50, 0.50, 0.50, 1.0}, 135 | {0.50, 0.50, 0.50, 1.0}, 136 | {0.50, 0.50, 0.50, 1.0}, 137 | }; 138 | 139 | static Color gradient_gray_dark[4] = { 140 | {16 / 255.0, 16 / 255.0, 16 / 255.0, 1.0}, 141 | {16 / 255.0, 16 / 255.0, 16 / 255.0, 1.0}, 142 | {16 / 255.0, 16 / 255.0, 16 / 255.0, 1.0}, 143 | {16 / 255.0, 16 / 255.0, 16 / 255.0, 1.0}, 144 | }; 145 | 146 | static Color gradient_gray_light[4] = { 147 | {32 / 255.0, 32 / 255.0, 32 / 255.0, 1.0}, 148 | {32 / 255.0, 32 / 255.0, 32 / 255.0, 1.0}, 149 | {32 / 255.0, 32 / 255.0, 32 / 255.0, 1.0}, 150 | {32 / 255.0, 32 / 255.0, 32 / 255.0, 1.0}, 151 | }; 152 | 153 | Color *themes[] = { 154 | gradient_golden, // 155 | gradient_legacy_red, // 156 | gradient_electric_blue, // 157 | gradient_dark_purple, // 158 | gradient_midnight_blue, // 159 | gradient_apple_green, // 160 | gradient_undersea, // 161 | gradient_morning_blue, // 162 | gradient_sunbeam, // 163 | gradient_lime_green, // 164 | gradient_pikachu_yellow, // 165 | gradient_gamecube_purple, // 166 | gradient_famicom_red, // 167 | gradient_flaming_hot, // 168 | gradient_ice_cold, // 169 | gradient_midgar, // 170 | gradient_volcanic_red, // 171 | gradient_dark, // 172 | gradient_light, // 173 | gradient_gray_dark, // 174 | gradient_gray_light, // 175 | }; 176 | 177 | static void check_shader_error(uint32_t shader, const char *type) { 178 | GLint success; 179 | glGetShaderiv(shader, GL_COMPILE_STATUS, &success); 180 | if (!success) { 181 | char infoLog[512]; 182 | glGetShaderInfoLog(shader, 512, NULL, infoLog); 183 | fprintf(stderr, "Shader %s Compile Error: %s\n", type, infoLog); 184 | } 185 | } 186 | 187 | void init_ribbon() { 188 | float fs_vertices[8] = {-1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f}; 189 | 190 | glGenVertexArrays(1, &ribbonState.bg_vao); 191 | glGenBuffers(1, &ribbonState.bg_vbo); 192 | 193 | glBindVertexArray(ribbonState.bg_vao); 194 | glBindBuffer(GL_ARRAY_BUFFER, ribbonState.bg_vbo); 195 | glBufferData(GL_ARRAY_BUFFER, sizeof(fs_vertices), fs_vertices, GL_STATIC_DRAW); 196 | glEnableVertexAttribArray(0); 197 | glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void *)0); 198 | 199 | static const char *bg_vertex_shader_text = GLSL( // 200 | layout(location = 0) in vec2 pos; // 201 | void main() { gl_Position = vec4(pos, 0.0, 1.0); }); // 202 | 203 | static const char *bg_fragment_shader_text = GLSL( // 204 | out vec4 FragColor; // 205 | uniform vec2 u_resolution; // 206 | 207 | uniform vec4 color0; // bottom‑left 208 | uniform vec4 color1; // top‑left 209 | uniform vec4 color2; // bottom‑right 210 | uniform vec4 color3; // top‑right 211 | 212 | void main() { 213 | vec2 uv = gl_FragCoord.xy / u_resolution; 214 | float t = uv.y; 215 | vec4 top = mix(color0, color2, t); 216 | vec4 bottom = mix(color1, color3, t); 217 | float s = uv.x; 218 | FragColor = mix(top, bottom, s); 219 | }); 220 | 221 | const GLuint bg_vertex_shader = glCreateShader(GL_VERTEX_SHADER); 222 | glShaderSource(bg_vertex_shader, 1, &bg_vertex_shader_text, NULL); 223 | glCompileShader(bg_vertex_shader); 224 | check_shader_error(bg_vertex_shader, "vertex"); 225 | 226 | const GLuint bg_fragment_shader = glCreateShader(GL_FRAGMENT_SHADER); 227 | glShaderSource(bg_fragment_shader, 1, &bg_fragment_shader_text, NULL); 228 | glCompileShader(bg_fragment_shader); 229 | check_shader_error(bg_fragment_shader, "fragment"); 230 | 231 | ribbonState.bg_program = glCreateProgram(); 232 | glAttachShader(ribbonState.bg_program, bg_vertex_shader); 233 | glAttachShader(ribbonState.bg_program, bg_fragment_shader); 234 | glLinkProgram(ribbonState.bg_program); 235 | 236 | glDeleteShader(bg_vertex_shader); 237 | glDeleteShader(bg_fragment_shader); 238 | 239 | // ribbon 240 | 241 | glGenBuffers(1, &ribbonState.ribbon_vertex_buffer); 242 | glBindBuffer(GL_ARRAY_BUFFER, ribbonState.ribbon_vertex_buffer); 243 | 244 | static const char *ribbon_vertex_shader_text = GLSL( // 245 | uniform float time; // 246 | layout(location = 0) in vec2 VertexCoord; // 247 | out vec3 vEC; // 248 | 249 | float iqhash(float n) { return fract(sin(n) * 43758.5453); } 250 | 251 | float noise(vec3 x) { 252 | vec3 p = floor(x); 253 | vec3 f = fract(x); 254 | f = f * f * (3.0 - 2.0 * f); 255 | float n = p.x + p.y * 57.0 + 113.0 * p.z; 256 | return mix(mix(mix(iqhash(n), iqhash(n + 1.0), f.x), mix(iqhash(n + 57.0), iqhash(n + 58.0), f.x), f.y), 257 | mix(mix(iqhash(n + 113.0), iqhash(n + 114.0), f.x), 258 | mix(iqhash(n + 170.0), iqhash(n + 171.0), f.x), f.y), 259 | f.z); 260 | } 261 | 262 | float noise2(vec3 x) { return cos(x.z * 4.0) * cos(x.z + time / 10.0 + x.x); } 263 | 264 | void main() { 265 | vec3 v = vec3(VertexCoord.x, 0.0, VertexCoord.y); 266 | vec3 v2 = v; 267 | vec3 v3 = v; 268 | 269 | float dt = time / 2.0; 270 | 271 | v.y = noise2(v2) / 8.0; 272 | 273 | v3.x -= dt / 5.0; 274 | v3.x /= 4.0; 275 | 276 | v3.z -= dt / 10.0; 277 | v3.y -= dt / 100.0; 278 | 279 | v.z -= noise(v3 * 7.0) / 15.0; 280 | v.y -= noise(v3 * 7.0) / 15.0 + cos(v.x * 2.0 - dt / 2.0) / 5.0 - 0.3; 281 | v.y = -v.y; 282 | 283 | vEC = v; 284 | gl_Position = vec4(v, 1.0); 285 | }); 286 | 287 | static const char *ribbon_fragment_shader_text = GLSL( // 288 | 289 | uniform float time; 290 | 291 | in vec3 vEC; // 292 | out vec4 FragColor; 293 | 294 | void main() { 295 | const vec3 up = vec3(0.0, 0.0, 1.0); 296 | vec3 x = dFdx(vEC); 297 | vec3 y = dFdy(vEC); 298 | vec3 normal = normalize(cross(x, y)); 299 | float c = 1.0 - dot(normal, up); 300 | c = (1.0 - cos(c * c)) / 3.0; 301 | FragColor = vec4(1.0, 1.0, 1.0, c); 302 | }); 303 | 304 | #define RIBBON_X_SEGMENTS 128 305 | #define RIBBON_Y_SEGMENTS 32 306 | #define RIBBON_VERTEX_COUNT ((RIBBON_X_SEGMENTS + 1) * (RIBBON_Y_SEGMENTS + 1)) 307 | #define RIBBON_INDEX_COUNT (RIBBON_X_SEGMENTS * RIBBON_Y_SEGMENTS * 6) 308 | 309 | float ribbon_vertices[RIBBON_VERTEX_COUNT * 2]; 310 | size_t i = 0; 311 | for (int y = 0; y <= RIBBON_Y_SEGMENTS; ++y) { 312 | for (int x = 0; x <= RIBBON_X_SEGMENTS; ++x) { 313 | float u = (float)x / RIBBON_X_SEGMENTS; 314 | float v = (float)y / RIBBON_Y_SEGMENTS; 315 | float px = u * 2.0f - 1.0f; 316 | float py = v * 2.0f - 1.0f; 317 | ribbon_vertices[i++] = px; 318 | ribbon_vertices[i++] = py; 319 | } 320 | } 321 | 322 | glGenBuffers(1, &ribbonState.ribbon_vertex_buffer); 323 | glBindBuffer(GL_ARRAY_BUFFER, ribbonState.ribbon_vertex_buffer); 324 | glBufferData(GL_ARRAY_BUFFER, sizeof(float) * RIBBON_VERTEX_COUNT * 2, ribbon_vertices, GL_STATIC_DRAW); 325 | 326 | const GLuint ribbon_vertex_shader = glCreateShader(GL_VERTEX_SHADER); 327 | glShaderSource(ribbon_vertex_shader, 1, &ribbon_vertex_shader_text, NULL); 328 | glCompileShader(ribbon_vertex_shader); 329 | check_shader_error(ribbon_vertex_shader, "vertex"); 330 | 331 | const GLuint ribbon_fragment_shader = glCreateShader(GL_FRAGMENT_SHADER); 332 | glShaderSource(ribbon_fragment_shader, 1, &ribbon_fragment_shader_text, NULL); 333 | glCompileShader(ribbon_fragment_shader); 334 | check_shader_error(ribbon_fragment_shader, "fragment"); 335 | 336 | ribbonState.ribbon_program = glCreateProgram(); 337 | glAttachShader(ribbonState.ribbon_program, ribbon_vertex_shader); 338 | glAttachShader(ribbonState.ribbon_program, ribbon_fragment_shader); 339 | glLinkProgram(ribbonState.ribbon_program); 340 | 341 | glDeleteShader(ribbon_vertex_shader); 342 | glDeleteShader(ribbon_fragment_shader); 343 | 344 | glGenVertexArrays(1, &ribbonState.ribbon_vertex_array); 345 | glBindVertexArray(ribbonState.ribbon_vertex_array); 346 | 347 | glEnableVertexAttribArray(0); 348 | glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void *)0); 349 | 350 | uint32_t *ribbon_indices = malloc(sizeof(GLuint) * RIBBON_INDEX_COUNT); 351 | uint32_t ribbon_offset = 0; 352 | for (int y = 0; y < RIBBON_Y_SEGMENTS; ++y) { 353 | for (int x = 0; x < RIBBON_X_SEGMENTS; ++x) { 354 | int i0 = y * (RIBBON_X_SEGMENTS + 1) + x; 355 | int i1 = i0 + 1; 356 | int i2 = i0 + (RIBBON_X_SEGMENTS + 1); 357 | int i3 = i2 + 1; 358 | 359 | // Triangle 1 360 | ribbon_indices[ribbon_offset++] = i0; 361 | ribbon_indices[ribbon_offset++] = i2; 362 | ribbon_indices[ribbon_offset++] = i1; 363 | 364 | // Triangle 2 365 | ribbon_indices[ribbon_offset++] = i1; 366 | ribbon_indices[ribbon_offset++] = i2; 367 | ribbon_indices[ribbon_offset++] = i3; 368 | } 369 | } 370 | 371 | GLuint ribbon_ebo; 372 | glGenBuffers(1, &ribbon_ebo); 373 | glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ribbon_ebo); 374 | glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(uint32_t) * RIBBON_INDEX_COUNT, ribbon_indices, GL_STATIC_DRAW); 375 | 376 | free(ribbon_indices); 377 | } 378 | 379 | void draw_background(float width, float height, int theme) { 380 | glUseProgram(ribbonState.bg_program); 381 | glBindVertexArray(ribbonState.bg_vao); 382 | 383 | Color *colors = themes[theme]; 384 | 385 | GLint locColors[4] = { 386 | glGetUniformLocation(ribbonState.bg_program, "color0"), 387 | glGetUniformLocation(ribbonState.bg_program, "color1"), 388 | glGetUniformLocation(ribbonState.bg_program, "color2"), 389 | glGetUniformLocation(ribbonState.bg_program, "color3"), 390 | }; 391 | 392 | glUniform2f(glGetUniformLocation(ribbonState.bg_program, "u_resolution"), width, height); 393 | 394 | for (size_t i = 0; i < 4; ++i) { 395 | glUniform4f(locColors[i], colors[i].r, colors[i].g, colors[i].b, colors[i].a); 396 | } 397 | 398 | glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); 399 | } 400 | 401 | void draw_ribbon(float width, float height, float time) { 402 | glUseProgram(ribbonState.ribbon_program); 403 | glUniform1f(glGetUniformLocation(ribbonState.ribbon_program, "time"), time); 404 | 405 | glBindVertexArray(ribbonState.ribbon_vertex_array); 406 | glBindBuffer(GL_ARRAY_BUFFER, ribbonState.ribbon_vertex_buffer); 407 | 408 | glDrawElements(GL_TRIANGLES, RIBBON_INDEX_COUNT, GL_UNSIGNED_INT, 0); 409 | } 410 | -------------------------------------------------------------------------------- /src/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #define GL_GLEXT_PROTOTYPES 13 | #include 14 | 15 | #include "dialog.h" 16 | #include "animation.h" 17 | #include "fm.h" 18 | #include "input.h" 19 | #include "option_list.h" 20 | #include "ribbon.h" 21 | #include "signal.h" 22 | #include "ui.h" 23 | #include "draw.h" 24 | #include "hr_list.h" 25 | #include "vr_list.h" 26 | 27 | #define min(a, b) (a > b ? b : a) 28 | #define max(a, b) (a < b ? b : a) 29 | 30 | // Global state 31 | 32 | DrawState state = {0}; 33 | 34 | FileManager *fm; 35 | Input search_input; 36 | Input rename_input; 37 | Input create_dir_input; 38 | 39 | OptionList op_list; 40 | VerticalList vr_list; 41 | HorizontalList hr_list; 42 | 43 | Dialog delete_dialog = { 44 | .content = "Are you sure you want to permanently delete this?", 45 | }; 46 | 47 | Option options_items[] = { 48 | {.title = "Option 1"}, 49 | {.title = "Option 2"}, 50 | {.title = "Option 3"}, 51 | }; 52 | 53 | Options options = { 54 | .selected = 0, 55 | .items = options_items, 56 | .items_count = sizeof(options_items) / sizeof(Option), 57 | .parent = NULL, 58 | }; 59 | 60 | Option root_items[] = { 61 | {.title = "New"}, // 62 | {.title = "Cut"}, // 63 | {.title = "Copy"}, // 64 | {.title = "Rename"}, // 65 | {.title = "Delete"}, // 66 | {.title = "Information"}, // 67 | {.title = "More"}, // 68 | }; 69 | 70 | Options root = { 71 | .items = root_items, 72 | .items_count = sizeof(root_items) / sizeof(Option), 73 | .selected = 0, 74 | .parent = NULL, 75 | }; 76 | 77 | void resize_callback(GLFWwindow *window, int w, int h) { 78 | glViewport(0, 0, w, h); 79 | state.width = w; 80 | state.height = h; 81 | } 82 | 83 | void get_window_size(unsigned *width, unsigned *height) { 84 | if (width) { 85 | *width = state.width; 86 | } 87 | if (height) { 88 | *height = state.height; 89 | } 90 | } 91 | 92 | bool handle_global_key(int key) { 93 | if (key == GLFW_KEY_EQUAL) { 94 | state.theme = min(state.theme + 1, 20); 95 | return true; 96 | } 97 | 98 | if (key == GLFW_KEY_MINUS) { 99 | state.theme = max(state.theme - 1, 0); 100 | return true; 101 | } 102 | 103 | return false; 104 | } 105 | 106 | bool handle_hr_list_key(int key) { 107 | if (hr_list.depth > 0) 108 | return false; 109 | 110 | if (key == GLFW_KEY_RIGHT || key == GLFW_KEY_LEFT) { 111 | int selected = 0; 112 | if (key == GLFW_KEY_RIGHT) 113 | selected = min(hr_list.selected + 1, hr_list.items_count - 1); 114 | else 115 | selected = max(hr_list.selected - 1, 0); 116 | 117 | if (selected == hr_list.selected) 118 | return false; 119 | 120 | hr_list.selected = selected; 121 | 122 | // Emit selection change event 123 | SelectionData sel_data = {.index = selected}; 124 | emit_signal(EVENT_HORIZONTAL_SELECTION_CHANGED, &sel_data); 125 | 126 | return true; 127 | } 128 | 129 | return false; 130 | } 131 | 132 | bool handle_option_list_key(OptionList *list, int key) { 133 | if (list->depth > 0) { 134 | switch (key) { 135 | case GLFW_KEY_I: 136 | case GLFW_KEY_ESCAPE: 137 | option_list_event_handler(OPTION_EVENT_CLOSE_MENU, list, NULL); 138 | return true; 139 | 140 | case GLFW_KEY_UP: { 141 | int direction = -1; 142 | option_list_event_handler(OPTION_EVENT_MOVE_SELECTION, list, &direction); 143 | return true; 144 | } 145 | 146 | case GLFW_KEY_DOWN: { 147 | int direction = 1; 148 | option_list_event_handler(OPTION_EVENT_MOVE_SELECTION, list, &direction); 149 | return true; 150 | } 151 | 152 | case GLFW_KEY_ENTER: { 153 | Option *current = &list->current->items[list->current->selected]; 154 | 155 | if (current->submenu && current->submenu->items_count > 0) { 156 | option_list_event_handler(OPTION_EVENT_OPEN_SUBMENU, list, NULL); 157 | } else { 158 | option_list_event_handler(OPTION_EVENT_SELECT_ITEM, list, NULL); 159 | } 160 | return true; 161 | } 162 | } 163 | return true; 164 | } 165 | 166 | if (key == GLFW_KEY_I) { 167 | option_list_event_handler(OPTION_EVENT_OPEN_MENU, list, NULL); 168 | return true; 169 | } 170 | 171 | return false; 172 | } 173 | 174 | bool handle_vr_list_key(int key) { 175 | int selected = vr_list.selected; 176 | 177 | switch (key) { 178 | case GLFW_KEY_BACKSPACE: 179 | emit_signal(EVENT_NAVIGATE_BACK, NULL); 180 | return true; 181 | case GLFW_KEY_ENTER: 182 | emit_signal(EVENT_ITEM_ACTIVATED, &vr_list.selected); 183 | return true; 184 | case GLFW_KEY_UP: 185 | selected = max(selected - 1, 0); 186 | break; 187 | case GLFW_KEY_DOWN: 188 | selected = min(selected + 1, vr_list.entry_end - 1); 189 | break; 190 | 191 | case GLFW_KEY_PAGE_UP: 192 | selected = max(selected - 10, 0); 193 | break; 194 | 195 | case GLFW_KEY_PAGE_DOWN: 196 | selected = min(selected + 10, vr_list.entry_end - 1); 197 | break; 198 | 199 | case GLFW_KEY_HOME: 200 | selected = 0; 201 | break; 202 | 203 | case GLFW_KEY_END: 204 | selected = vr_list.items_count - 1; 205 | break; 206 | } 207 | 208 | if (selected != vr_list.selected) { 209 | SelectionData sel_data = {.index = selected}; 210 | emit_signal(EVENT_VERTICAL_SELECTION_CHANGED, &sel_data); 211 | return true; 212 | } 213 | 214 | return false; 215 | } 216 | 217 | bool handle_file_entry_key(int key) { 218 | if (state.show_preview) { 219 | if (key == GLFW_KEY_ESCAPE || key == GLFW_KEY_P) { 220 | memset(state.buffer, 0, 512); 221 | state.show_preview = false; 222 | } 223 | return true; 224 | } 225 | 226 | if (key == GLFW_KEY_P) { 227 | struct file_entry *current = fm->current_dir->children[vr_list.selected]; 228 | if (current->type == TYPE_FILE && get_mime_type(current->path, "text/")) { 229 | read_file_content(current->path, state.buffer, 512); 230 | state.show_preview = true; 231 | } 232 | return true; 233 | } 234 | 235 | return false; 236 | } 237 | 238 | void handle_input_key(Input *input, EventType eventType, int key) { 239 | if (key == GLFW_KEY_ESCAPE) { 240 | hide_input(input); 241 | } else if (key == GLFW_KEY_BACKSPACE) { 242 | pop_from_input(input); 243 | } else if (key == GLFW_KEY_ENTER) { 244 | // search current files 245 | emit_signal(eventType, input->buffer); 246 | hide_input(input); 247 | } else if (key == GLFW_KEY_LEFT) { 248 | move_cursor_left(input); 249 | } else if (key == GLFW_KEY_RIGHT) { 250 | move_cursor_right(input); 251 | } 252 | } 253 | 254 | bool handle_input_entry_key(int key) { 255 | if (key == GLFW_KEY_SLASH) { 256 | show_input(&search_input); 257 | return true; 258 | } 259 | 260 | if (search_input.is_visible) { 261 | handle_input_key(&search_input, EVENT_SEARCH, key); 262 | return true; 263 | } 264 | 265 | if (rename_input.is_visible) { 266 | handle_input_key(&rename_input, EVENT_RENAME, key); 267 | return true; 268 | } 269 | 270 | if (create_dir_input.is_visible) { 271 | handle_input_key(&create_dir_input, EVENT_CREATE_DIR, key); 272 | return true; 273 | } 274 | 275 | return false; 276 | } 277 | 278 | bool handle_dialog_entry_key(Dialog *dialog, int key) { 279 | if (key == GLFW_KEY_ESCAPE) { 280 | dialog_hide(dialog); 281 | } else if (key == GLFW_KEY_ENTER) { 282 | if (dialog->animation_target == -1) { 283 | emit_signal(EVENT_CONFIRM_DELETE, NULL); 284 | } else { 285 | emit_signal(EVENT_REJECT_DELETE, NULL); 286 | } 287 | dialog_hide(dialog); 288 | } else if (key == GLFW_KEY_LEFT) { 289 | dialog_move_cursor_left(dialog); 290 | } else if (key == GLFW_KEY_RIGHT) { 291 | dialog_move_cursor_right(dialog); 292 | } 293 | return true; 294 | } 295 | 296 | void character_callback(GLFWwindow *window, unsigned int codepoint) { 297 | if (search_input.is_visible) { 298 | if (codepoint == '/') 299 | return; 300 | append_to_input(&search_input, codepoint); 301 | } else if (rename_input.is_visible) { 302 | if (codepoint == '/') 303 | return; 304 | append_to_input(&rename_input, codepoint); 305 | } else if (create_dir_input.is_visible) { 306 | if (codepoint == '/') 307 | return; 308 | append_to_input(&create_dir_input, codepoint); 309 | } 310 | } 311 | 312 | void handle_key(GLFWwindow *window, int key, int scancode, int action, int mods) { 313 | if (action != GLFW_PRESS) 314 | return; 315 | 316 | // Global ESC to close info or preview 317 | if (state.show_info) { 318 | if (key == GLFW_KEY_ESCAPE) { 319 | state.show_info = false; 320 | } 321 | return; 322 | } 323 | 324 | if (delete_dialog.is_visible) { 325 | handle_dialog_entry_key(&delete_dialog, key); 326 | return; 327 | } 328 | 329 | if (handle_input_entry_key(key)) 330 | return; 331 | 332 | if (search_input.is_visible || rename_input.is_visible || create_dir_input.is_visible) { 333 | return; 334 | } 335 | 336 | if (handle_option_list_key(&op_list, key)) 337 | return; 338 | 339 | if (handle_file_entry_key(key)) 340 | return; 341 | 342 | // Main view mode 343 | if (handle_global_key(key)) 344 | return; 345 | if (handle_hr_list_key(key)) 346 | return; 347 | if (handle_vr_list_key(key)) 348 | return; 349 | 350 | if (key == GLFW_KEY_ESCAPE) 351 | glfwSetWindowShouldClose(window, true); 352 | } 353 | 354 | void op_list_option_selected(Option *option) { 355 | if (strcmp(option->title, "Information") == 0) { 356 | state.show_info = true; 357 | } else if (strcmp(option->title, "Rename") == 0) { 358 | fm->action_target_index = vr_list.selected; // Store target index 359 | set_buffer_input(&rename_input, fm->current_dir->children[vr_list.selected]->name); 360 | show_input(&rename_input); 361 | } else if (strcmp(option->title, "New") == 0) { 362 | show_input(&create_dir_input); 363 | } else if (strcmp(option->title, "Delete") == 0) { 364 | fm->action_target_index = vr_list.selected; // Store target index 365 | dialog_show(&delete_dialog); 366 | } 367 | } 368 | 369 | // Initialization of menu data 370 | void initialize_menu_data() { 371 | init_horizontal_list(&hr_list); 372 | 373 | fm = create_file_manager(hr_list.items[hr_list.selected].path); 374 | 375 | // vr 376 | init_vertical_list(&vr_list); 377 | vr_list.get_screen_size = get_window_size; 378 | 379 | // options 380 | root.items[5].submenu = &options; 381 | options.parent = &root; 382 | 383 | op_list.on_item_selected = op_list_option_selected; 384 | 385 | op_list.root = &root; 386 | op_list.current = &root; 387 | } 388 | 389 | void file_manager_event_handler(EventType type, void *context, void *data) { 390 | FileManager *fm = (FileManager *)context; 391 | 392 | if (type == EVENT_NAVIGATE_TO_PATH) { 393 | NavigationData *nav_data = (NavigationData *)data; 394 | 395 | // Change directory 396 | if (nav_data->clear_history) { 397 | switch_directory(fm, nav_data->path); 398 | } else { 399 | change_directory(fm, nav_data->path); 400 | } 401 | 402 | // Notify about new directory content 403 | DirectoryData dir_data = { 404 | .selected = 0, 405 | .depth = fm->depth, 406 | .items = fm->current_dir->children, 407 | .current_path = fm->current_dir->path, 408 | .items_count = fm->current_dir->child_count, 409 | }; 410 | emit_signal(EVENT_DIRECTORY_CONTENT_CHANGED, &dir_data); 411 | } else if (type == EVENT_ITEM_ACTIVATED) { 412 | int index = *(int *)data; 413 | struct file_entry *current = fm->current_dir->children[index]; 414 | 415 | if (current->type == TYPE_DIRECTORY) { 416 | NavigationData nav_data = {.path = current->path, .selected_index = 0, .clear_history = false}; 417 | emit_signal(EVENT_NAVIGATE_TO_PATH, &nav_data); 418 | } else { 419 | open_file(current->path); 420 | } 421 | } else if (type == EVENT_NAVIGATE_BACK) { 422 | // Only proceed if we can go back 423 | if (fm->depth <= 0) 424 | return; 425 | 426 | int selected = navigate_back(fm); 427 | 428 | DirectoryData dir_data = { 429 | .depth = fm->depth, 430 | .selected = selected, 431 | .items = fm->current_dir->children, 432 | .current_path = fm->current_dir->path, 433 | .items_count = fm->current_dir->child_count, 434 | }; 435 | emit_signal(EVENT_DIRECTORY_CONTENT_CHANGED, &dir_data); 436 | } else if (type == EVENT_SEARCH) { 437 | char *search = (char *)data; 438 | 439 | int index = search_file_name(fm, search); 440 | if (index == -1) 441 | return; 442 | 443 | SelectionData sel_data = {.index = index}; 444 | emit_signal(EVENT_VERTICAL_SELECTION_CHANGED, &sel_data); 445 | } else if (type == EVENT_RENAME) { 446 | char *new_name = (char *)data; 447 | 448 | if (!fm_rename(fm, new_name)) { 449 | printf("Couldn't rename file.\n"); 450 | return; 451 | } 452 | 453 | DirectoryData dir_data = { 454 | .depth = fm->depth, 455 | .items = fm->current_dir->children, 456 | .selected = fm->action_target_index, 457 | .current_path = fm->current_dir->path, 458 | .items_count = fm->current_dir->child_count, 459 | }; 460 | emit_signal(EVENT_DIRECTORY_CONTENT_CHANGED, &dir_data); 461 | fm->action_target_index = -1; 462 | } else if (type == EVENT_CREATE_DIR) { 463 | char *dir_name = (char *)data; 464 | 465 | if (!fm_create_dir(fm, dir_name)) { 466 | printf("Couldn't create directory.\n"); 467 | return; 468 | } 469 | 470 | DirectoryData dir_data = { 471 | .selected = 0, 472 | .depth = fm->depth, 473 | .items = fm->current_dir->children, 474 | .current_path = fm->current_dir->path, 475 | .items_count = fm->current_dir->child_count, 476 | }; 477 | emit_signal(EVENT_DIRECTORY_CONTENT_CHANGED, &dir_data); 478 | } else if (type == EVENT_CONFIRM_DELETE) { 479 | if (!fm_delete_entry(fm)) { 480 | printf("Couldn't delete directory.\n"); 481 | return; 482 | } 483 | 484 | DirectoryData dir_data = { 485 | .selected = 0, 486 | .depth = fm->depth, 487 | .items = fm->current_dir->children, 488 | .current_path = fm->current_dir->path, 489 | .items_count = fm->current_dir->child_count, 490 | }; 491 | emit_signal(EVENT_DIRECTORY_CONTENT_CHANGED, &dir_data); 492 | } else if (type == EVENT_REJECT_DELETE) { 493 | fm->action_target_index = -1; 494 | } 495 | } 496 | 497 | int main() { 498 | // Initialize GLFW 499 | if (!glfwInit()) { 500 | fprintf(stderr, "Failed to initialize GLFW\n"); 501 | return 1; 502 | } 503 | 504 | glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4); 505 | glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 5); 506 | glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); 507 | glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); 508 | 509 | // Create window 510 | GLFWwindow *window = glfwCreateWindow(960, 720, "File Manager", NULL, NULL); 511 | if (!window) { 512 | fprintf(stderr, "Failed to create GLFW window\n"); 513 | glfwTerminate(); 514 | return 1; 515 | } 516 | 517 | glfwSetWindowAttrib(window, GLFW_FLOATING, GL_TRUE); 518 | glfwMakeContextCurrent(window); 519 | glfwSetKeyCallback(window, handle_key); 520 | glfwSetCharCallback(window, character_callback); 521 | glfwSetFramebufferSizeCallback(window, resize_callback); 522 | 523 | glewExperimental = GL_TRUE; 524 | if (glewInit() != GLEW_OK) { 525 | printf("Could not init glew.\n"); 526 | return 1; 527 | } 528 | 529 | glfwSwapInterval(1); 530 | 531 | ui_create(); 532 | 533 | init_ribbon(); 534 | 535 | // Register a font 536 | if (register_font("sans", "./fonts/SpaceMonoNerdFont.ttf") < 0) { 537 | fprintf(stderr, "Failed to register font\n"); 538 | ui_delete(); 539 | return 1; 540 | } 541 | 542 | if (register_font("icon", "./fonts/feather.ttf") < 0) { 543 | fprintf(stderr, "Failed to register font icon\n"); 544 | ui_delete(); 545 | return 1; 546 | } 547 | 548 | state.theme = 2; // electric_blue 549 | srand(time(NULL)); 550 | initialize_menu_data(); 551 | 552 | /////////////////////////////////////////// 553 | 554 | // Horizontal list connections 555 | connect_signal(EVENT_HORIZONTAL_SELECTION_CHANGED, horizontal_list_event_handler, &hr_list); 556 | connect_signal(EVENT_DIRECTORY_CONTENT_CHANGED, horizontal_list_event_handler, &hr_list); 557 | 558 | // Vertical list connections 559 | connect_signal(EVENT_DIRECTORY_CONTENT_CHANGED, vertical_list_event_handler, &vr_list); 560 | connect_signal(EVENT_VERTICAL_SELECTION_CHANGED, vertical_list_event_handler, &vr_list); 561 | 562 | // File manager connections 563 | connect_signal(EVENT_NAVIGATE_TO_PATH, file_manager_event_handler, fm); 564 | connect_signal(EVENT_NAVIGATE_BACK, file_manager_event_handler, fm); 565 | connect_signal(EVENT_ITEM_ACTIVATED, file_manager_event_handler, fm); 566 | connect_signal(EVENT_SEARCH, file_manager_event_handler, fm); 567 | connect_signal(EVENT_RENAME, file_manager_event_handler, fm); 568 | connect_signal(EVENT_CREATE_DIR, file_manager_event_handler, fm); 569 | connect_signal(EVENT_CONFIRM_DELETE, file_manager_event_handler, fm); 570 | connect_signal(EVENT_REJECT_DELETE, file_manager_event_handler, fm); 571 | 572 | /////////////////////////////////////////// 573 | 574 | // init 575 | emit_signal(EVENT_HORIZONTAL_SELECTION_CHANGED, &(SelectionData){.index = 0}); 576 | 577 | while (!glfwWindowShouldClose(window)) { 578 | float current_time = glfwGetTime(); 579 | 580 | animation_update(current_time); 581 | 582 | int width, height; 583 | glfwGetFramebufferSize(window, &width, &height); 584 | 585 | glViewport(0, 0, width, height); 586 | glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); 587 | 588 | int winWidth, winHeight; 589 | glfwGetWindowSize(window, &winWidth, &winHeight); 590 | 591 | draw_background(width, height, state.theme); 592 | 593 | draw_ribbon(width, height, current_time); 594 | 595 | start_frame(width, height); 596 | 597 | if (delete_dialog.is_visible) { 598 | draw_dialog(&delete_dialog, &state, fm->current_dir->children[vr_list.selected]->path); 599 | } else if (!state.show_info) { 600 | draw_folder_path(&hr_list, fm->current_dir->path, 200, 160); 601 | draw_vertical_list(&vr_list, 180); 602 | draw_horizontal_menu(&hr_list, 180, 150); 603 | 604 | draw_text_preview(&state); 605 | 606 | draw_option_list(&op_list, &state); 607 | 608 | draw_input_field(&search_input, "Search", &state); 609 | draw_input_field(&rename_input, "Rename", &state); 610 | draw_input_field(&create_dir_input, "New Dir", &state); 611 | } else { 612 | draw_info(&vr_list, state.width, state.height); 613 | } 614 | 615 | end_frame(); 616 | 617 | // Swap buffers 618 | glfwSwapBuffers(window); 619 | glfwPollEvents(); 620 | } 621 | 622 | free_file_manager(fm); 623 | animation_clean(); 624 | 625 | ui_delete(); 626 | 627 | glfwTerminate(); 628 | 629 | return 0; 630 | } 631 | -------------------------------------------------------------------------------- /src/ui.c: -------------------------------------------------------------------------------- 1 | #include "ui.h" 2 | #include 3 | #include 4 | 5 | #define GL_GLEXT_PROTOTYPES 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | #include 13 | #include FT_FREETYPE_H 14 | 15 | RenderState renderState = { 16 | .vert_count = 0, 17 | .index_count = 0, 18 | }; 19 | 20 | // static const uint32_t indices[BATCHES * 6] = { 21 | // 0, 1, 2, // tri 1 22 | // 2, 3, 0, // tri 2 23 | // }; 24 | 25 | static void check_shader_error(uint32_t shader, const char *type) { 26 | GLint success; 27 | glGetShaderiv(shader, GL_COMPILE_STATUS, &success); 28 | if (!success) { 29 | char infoLog[512]; 30 | glGetShaderInfoLog(shader, 512, NULL, infoLog); 31 | fprintf(stderr, "Shader %s Compile Error: %s\n", type, infoLog); 32 | } 33 | } 34 | 35 | void add_vert(float x, float y, float w, float h, Color color, float px, float py, float t0, float t1, float is_text, 36 | Radius rad) { 37 | renderState.verts[renderState.vert_count++] = (Vertex){ 38 | .pos = {x, y}, 39 | .size = {w, h}, 40 | .color = color, 41 | .pos_px = {px, py}, 42 | .texcoord = {t0, t1}, 43 | .is_text = is_text, 44 | .corner_radius = rad, 45 | }; 46 | } 47 | 48 | void create_font_atlas() { 49 | FontAtlas *atlas = (FontAtlas *)malloc(sizeof(FontAtlas)); 50 | if (!atlas) { 51 | fprintf(stderr, "Error: Could not initialize font.\n"); 52 | exit(1); 53 | } 54 | 55 | // Initialize FreeType 56 | FT_Error error = FT_Init_FreeType(&atlas->ft_library); 57 | if (error) { 58 | fprintf(stderr, "Error: Could not initialize FreeType library\n"); 59 | free(atlas); 60 | exit(1); 61 | } 62 | 63 | // Initialize atlas 64 | atlas->width = ATLAS_WIDTH; 65 | atlas->height = ATLAS_HEIGHT; 66 | atlas->pixels = (unsigned char *)calloc(atlas->width * atlas->height, 1); 67 | atlas->font_count = 0; 68 | 69 | // Initialize glyph storage 70 | atlas->glyph_capacity = 1024; // Start with space for 1024 glyphs 71 | atlas->glyphs = (GlyphInfo *)malloc(atlas->glyph_capacity * sizeof(GlyphInfo)); 72 | atlas->glyph_count = 0; 73 | 74 | // Initialize positioning 75 | atlas->current_x = 0; 76 | atlas->current_y = 0; 77 | atlas->current_line_height = 0; 78 | 79 | // Mark atlas as not needing update yet 80 | atlas->dirty = false; 81 | 82 | // Create OpenGL texture 83 | glGenTextures(1, &atlas->texture_id); 84 | glBindTexture(GL_TEXTURE_2D, atlas->texture_id); 85 | glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, atlas->width, atlas->height, 0, GL_RED, GL_UNSIGNED_BYTE, atlas->pixels); 86 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); 87 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); 88 | 89 | renderState.font_atlas = atlas; 90 | } 91 | 92 | void use_font(const char *name) { 93 | int font_id = -1; 94 | for (size_t i = 0; i < renderState.font_atlas->font_count; ++i) { 95 | if (strcmp(name, renderState.font_atlas->font_names[i]) == 0) { 96 | font_id = i; 97 | break; 98 | } 99 | } 100 | 101 | if (font_id == -1) { 102 | fprintf(stderr, "Font %s not declared.\n", name); 103 | return; 104 | } 105 | 106 | renderState.current_font = font_id; 107 | } 108 | 109 | // Load font from file and register it with the atlas 110 | int register_font(const char *name, const char *filename) { 111 | FontAtlas *atlas = renderState.font_atlas; 112 | if (!atlas) { 113 | fprintf(stderr, "Font atlas not created.\n"); 114 | return -1; 115 | } 116 | 117 | if (atlas->font_count >= MAX_FONTS) { 118 | fprintf(stderr, "Error: Maximum number of fonts reached\n"); 119 | return -1; 120 | } 121 | 122 | // Load font using FreeType 123 | FontInfo *font = &atlas->fonts[atlas->font_count]; 124 | FT_Error error = FT_New_Face(atlas->ft_library, filename, 0, &font->face); 125 | 126 | if (error == FT_Err_Unknown_File_Format) { 127 | fprintf(stderr, "Error: Unsupported font format: %s\n", filename); 128 | return -1; 129 | } else if (error) { 130 | fprintf(stderr, "Error: Could not open font file: %s\n", filename); 131 | return -1; 132 | } 133 | 134 | font->hb_font = hb_ft_font_create(font->face, NULL); 135 | font->hb_buffer = hb_buffer_create(); 136 | hb_ft_font_set_funcs(font->hb_font); 137 | 138 | // Store font information 139 | snprintf(font->name, sizeof(font->name), "%s", filename); 140 | font->size_count = 0; 141 | 142 | if (strlen(name) > 59) { 143 | printf("INFO: use a shorter name for font.\n"); 144 | } 145 | snprintf(atlas->font_names[atlas->font_count], 60, "%s", name); 146 | 147 | return atlas->font_count++; // Return font ID and increment count 148 | } 149 | 150 | int find_glyph(int font_id, float size, int codepoint) { 151 | FontAtlas *atlas = renderState.font_atlas; 152 | if (!atlas) { 153 | fprintf(stderr, "Font atlas not created.\n"); 154 | return -1; 155 | } 156 | 157 | for (int i = 0; i < atlas->glyph_count; i++) { 158 | GlyphInfo *glyph = &atlas->glyphs[i]; 159 | if (glyph->used && glyph->font_id == font_id && glyph->size == size && glyph->codepoint == codepoint) { 160 | return i; 161 | } 162 | } 163 | return -1; 164 | } 165 | 166 | // Add a new line in the atlas when current line is full 167 | bool add_new_line(FontAtlas *atlas) { 168 | atlas->current_x = 0; 169 | atlas->current_y += atlas->current_line_height; 170 | atlas->current_line_height = 0; 171 | 172 | // Check if we've run out of atlas space 173 | if (atlas->current_y >= atlas->height) { 174 | // Could implement atlas expansion or paging here 175 | fprintf(stderr, "Error: Font atlas is full\n"); 176 | return false; 177 | } 178 | 179 | return true; 180 | } 181 | 182 | void copy_bitmap_to_atlas(FT_Bitmap *bitmap, FontAtlas *atlas) { 183 | int x = atlas->current_x; 184 | int y = atlas->current_y; 185 | 186 | // Ensure we don't write outside atlas bounds 187 | if (x < 0 || y < 0 || x + bitmap->width > atlas->width || y + bitmap->rows > atlas->height) { 188 | return; 189 | } 190 | 191 | for (int row = 0; row < bitmap->rows; row++) { 192 | unsigned char *src_row = bitmap->buffer + row * bitmap->pitch; 193 | unsigned char *dst_row = atlas->pixels + (y + row) * atlas->width + x; 194 | memcpy(dst_row, src_row, bitmap->width); 195 | } 196 | } 197 | 198 | int add_glyph_to_atlas(FontAtlas *atlas, int font_id, float size, int codepoint, int ft_point) { 199 | // Check if font_id is valid 200 | if (font_id < 0 || font_id >= atlas->font_count) { 201 | fprintf(stderr, "Error: Invalid font ID: %d\n", font_id); 202 | return -1; 203 | } 204 | 205 | // Check if already in atlas 206 | int existing = find_glyph(font_id, size, codepoint); 207 | if (existing >= 0) { 208 | return existing; 209 | } 210 | 211 | // Get font info 212 | FontInfo *font = &atlas->fonts[font_id]; 213 | 214 | // Check if we've seen this size before 215 | bool size_found = false; 216 | for (int i = 0; i < font->size_count; i++) { 217 | if (font->sizes[i] == size) { 218 | size_found = true; 219 | break; 220 | } 221 | } 222 | 223 | // Add new size if needed 224 | if (!size_found && font->size_count < MAX_FONT_SIZES) { 225 | font->sizes[font->size_count++] = size; 226 | } else if (!size_found) { 227 | fprintf(stderr, "Warning: Maximum number of font sizes reached\n"); 228 | } 229 | 230 | // Set font size (FreeType uses 26.6 fixed-point format, so multiply by 64) 231 | FT_Error error = FT_Set_Char_Size(font->face, 0, (FT_F26Dot6)(size * 64.0), 0, 0); 232 | if (error) { 233 | fprintf(stderr, "Error: Could not set font size\n"); 234 | return -1; 235 | } 236 | 237 | // Load glyph 238 | // FT_UInt glyph_index = FT_Get_Char_Index(font->face, codepoint); 239 | // if (glyph_index == 0) { 240 | // const int fallback_codepoints[] = {'?'}; 241 | // int len = sizeof(fallback_codepoints) / sizeof(int); 242 | // 243 | // for (int i = 0; i < len && glyph_index == 0; i++) { 244 | // int fallback_codepoint = fallback_codepoints[i]; // Use a question mark as fallback 245 | // // First check if we already have the fallback glyph 246 | // int fallback_glyph = find_glyph(font_id, size, fallback_codepoint); 247 | // if (fallback_glyph >= 0) { 248 | // return fallback_glyph; 249 | // } 250 | // 251 | // // Otherwise, load the fallback glyph 252 | // glyph_index = FT_Get_Char_Index(font->face, fallback_codepoint); 253 | // if (glyph_index == 0) { 254 | // // If even the fallback is missing, use a simple box or space 255 | // fprintf(stderr, "Warning: Even fallback glyph missing for codepoint %d\n", codepoint); 256 | // } else { 257 | // // Remember the original codepoint, but use fallback glyph data 258 | // codepoint = fallback_codepoint; 259 | // break; 260 | // } 261 | // } 262 | // } 263 | 264 | error = FT_Load_Glyph(font->face, codepoint, FT_LOAD_DEFAULT | FT_LOAD_RENDER); 265 | if (error) { 266 | fprintf(stderr, "Error: Could not load glyph\n"); 267 | return -1; 268 | } 269 | 270 | if (font->face == NULL) 271 | return -1; 272 | 273 | // Get bitmap and metrics 274 | FT_Bitmap bitmap = font->face->glyph->bitmap; 275 | FT_GlyphSlot slot = font->face->glyph; 276 | 277 | int width = bitmap.width; 278 | int height = bitmap.rows; 279 | 280 | int effective_width = width; 281 | if (slot->bitmap_left < 0) { 282 | effective_width += -slot->bitmap_left; // Extend width to the left 283 | } 284 | 285 | float xadvance = (float)slot->advance.x / 64.0f; 286 | if (xadvance > effective_width) { 287 | effective_width = (int)ceil(xadvance); 288 | } 289 | 290 | // Get the font's global metrics for this size 291 | float ascender = font->face->size->metrics.ascender / 64.0f; 292 | float descender = font->face->size->metrics.descender / 64.0f; 293 | 294 | float padding = 1.0f; 295 | int total_height = (int)ceil(ascender - descender) + (int)padding; 296 | int final_height = height > total_height ? height : total_height; 297 | 298 | if (atlas->glyph_count >= atlas->glyph_capacity) { 299 | atlas->glyph_capacity *= 2; 300 | atlas->glyphs = (GlyphInfo *)realloc(atlas->glyphs, atlas->glyph_capacity * sizeof(GlyphInfo)); 301 | } 302 | 303 | if (atlas->current_x + width > atlas->width) { 304 | if (!add_new_line(atlas)) { 305 | return -1; 306 | } 307 | } 308 | 309 | if (final_height > atlas->current_line_height) { 310 | atlas->current_line_height = final_height; 311 | } 312 | 313 | // Add glyph info 314 | GlyphInfo *glyph = &atlas->glyphs[atlas->glyph_count]; 315 | glyph->x = atlas->current_x; 316 | glyph->y = atlas->current_y; 317 | glyph->width = effective_width; 318 | glyph->height = final_height; 319 | glyph->xoff = (float)slot->bitmap_left; 320 | glyph->yoff = (float)-slot->bitmap_top; 321 | glyph->xadvance = xadvance; 322 | glyph->codepoint = codepoint; 323 | glyph->ft_codepoint = ft_point; 324 | glyph->font_id = font_id; 325 | glyph->size = size; 326 | glyph->used = true; 327 | 328 | // Copy bitmap to atlas 329 | 330 | copy_bitmap_to_atlas(&bitmap, atlas); 331 | 332 | atlas->current_x += effective_width + padding; 333 | atlas->dirty = true; 334 | return atlas->glyph_count++; 335 | } 336 | 337 | void update_atlas_texture(FontAtlas *atlas) { 338 | if (!atlas->dirty) 339 | return; 340 | 341 | glBindTexture(GL_TEXTURE_2D, atlas->texture_id); 342 | glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, atlas->width, atlas->height, GL_RED, GL_UNSIGNED_BYTE, atlas->pixels); 343 | 344 | atlas->dirty = false; 345 | } 346 | 347 | unsigned int decode_utf8(const char **text) { 348 | unsigned int c = (unsigned char)**text; 349 | 350 | if (c < 0x80) { // ASCII 351 | (*text)++; 352 | return c; 353 | } else if (c < 0xE0) { // 2-byte 354 | unsigned int c2 = (unsigned char)*((*text) + 1); 355 | (*text) += 2; 356 | return ((c & 0x1F) << 6) | (c2 & 0x3F); 357 | } else if (c < 0xF0) { // 3-byte 358 | unsigned int c2 = (unsigned char)*((*text) + 1); 359 | unsigned int c3 = (unsigned char)*((*text) + 2); 360 | (*text) += 3; 361 | return ((c & 0x0F) << 12) | ((c2 & 0x3F) << 6) | (c3 & 0x3F); 362 | } else { // 4-byte (or invalid) 363 | unsigned int c2 = (unsigned char)*((*text) + 1); 364 | unsigned int c3 = (unsigned char)*((*text) + 2); 365 | unsigned int c4 = (unsigned char)*((*text) + 3); 366 | (*text) += 4; 367 | return ((c & 0x07) << 18) | ((c2 & 0x3F) << 12) | ((c3 & 0x3F) << 6) | (c4 & 0x3F); 368 | } 369 | } 370 | 371 | void draw_glyph(float cursor_x, float cursor_y, Color color, GlyphInfo *glyph, float aw, float ah) { 372 | // Calculate vertices 373 | float x0 = cursor_x + glyph->xoff; 374 | float y0 = cursor_y + glyph->yoff; 375 | float w = glyph->width; 376 | float h = glyph->height; 377 | 378 | // Calculate texture coordinates 379 | float u0 = (float)glyph->x / aw; 380 | float v0 = (float)glyph->y / ah; 381 | float u1 = (float)(glyph->x + glyph->width) / aw; 382 | float v1 = (float)(glyph->y + glyph->height) / ah; 383 | 384 | // Draw quad 385 | Radius rad = {0, 0, 0, 0}; 386 | 387 | float pos[8] = { 388 | -1.0, -1.0, // Vertex 0: Bottom-left 389 | 1.0, -1.0, // Vertex 1: Bottom-right 390 | 1.0, 1.0, // Vertex 2: Top-right 391 | -1.0, 1.0 // Vertex 3: Top-left 392 | }; 393 | 394 | add_vert(pos[0], pos[1], w, h, color, x0, y0, u0, v0, 1, rad); 395 | add_vert(pos[2], pos[3], w, h, color, x0, y0, u1, v0, 1, rad); 396 | add_vert(pos[4], pos[5], w, h, color, x0, y0, u1, v1, 1, rad); 397 | add_vert(pos[6], pos[7], w, h, color, x0, y0, u0, v1, 1, rad); 398 | renderState.index_count += 6; 399 | } 400 | 401 | // Get glyph positions for a string, adding glyphs to atlas as needed 402 | void get_string_glyphs(FontAtlas *atlas, int font_id, float size, const char *text, GlyphInfo **glyphs, int *count) { 403 | if (!text || !glyphs || !count) 404 | return; 405 | 406 | FontInfo *font = &atlas->fonts[font_id]; 407 | 408 | // Shape text with HarfBuzz 409 | hb_buffer_reset(font->hb_buffer); 410 | hb_buffer_add_utf8(font->hb_buffer, text, -1, 0, -1); 411 | hb_buffer_guess_segment_properties(font->hb_buffer); 412 | 413 | // Temporary scale adjustment 414 | hb_font_set_scale(font->hb_font, (int)(size * 64), (int)(size * 64)); 415 | 416 | hb_feature_t features[] = { 417 | {HB_TAG('c', 'c', 'm', 'p'), 1, 0, -1}, 418 | {HB_TAG('l', 'o', 'c', 'l'), 1, 0, -1}, 419 | }; 420 | 421 | hb_shape(font->hb_font, font->hb_buffer, features, 2); 422 | 423 | // Get shaping results 424 | unsigned int hb_count; 425 | hb_glyph_info_t *info = hb_buffer_get_glyph_infos(font->hb_buffer, &hb_count); 426 | // hb_glyph_position_t *pos = hb_buffer_get_glyph_positions(font->hb_buffer, NULL); 427 | 428 | *glyphs = malloc(hb_count * sizeof(GlyphInfo)); 429 | 430 | for (unsigned i = 0; i < hb_count; i++) { 431 | hb_codepoint_t gid = info[i].codepoint; 432 | 433 | // Find or add glyph to atlas 434 | int atlas_idx = find_glyph(font_id, size, gid); 435 | if (atlas_idx < 0) { 436 | atlas_idx = add_glyph_to_atlas(atlas, font_id, size, gid, text[i]); 437 | } 438 | 439 | if (atlas_idx >= 0) { 440 | (*glyphs)[i] = atlas->glyphs[atlas_idx]; 441 | } 442 | 443 | // Store HarfBuzz positioning 444 | // (*positions)[i] = (GlyphPosition){ 445 | // .x_offset = pos[i].x_offset / 64.0f, 446 | // .y_offset = pos[i].y_offset / 64.0f, 447 | // .x_advance = pos[i].x_advance / 64.0f, 448 | // .y_advance = pos[i].y_advance / 64.0f, 449 | // }; 450 | } 451 | *count = hb_count; 452 | update_atlas_texture(atlas); 453 | } 454 | 455 | // Calculate text bounds 456 | void get_text_bounds(float size, const char *text, float *width, float *height, float *start_x, float *bearing_y) { 457 | GlyphInfo *glyphs; 458 | int glyph_count; 459 | 460 | FontAtlas *atlas = renderState.font_atlas; 461 | if (!atlas) { 462 | fprintf(stderr, "Font atlas not created.\n"); 463 | return; 464 | } 465 | 466 | int font_id = renderState.current_font; 467 | get_string_glyphs(atlas, font_id, size, text, &glyphs, &glyph_count); 468 | 469 | float min_x = 0, min_y = 0, max_x = 0, max_y = 0; 470 | float cursor_x = 0; 471 | 472 | for (int i = 0; i < glyph_count; i++) { 473 | GlyphInfo *glyph = &glyphs[i]; 474 | 475 | float x0 = cursor_x + glyph->xoff; 476 | float y0 = glyph->yoff; 477 | float x1 = x0 + glyph->width; 478 | float y1 = y0 + glyph->height; 479 | 480 | min_x = (i == 0) ? x0 : (x0 < min_x ? x0 : min_x); 481 | min_y = (i == 0) ? y0 : (y0 < min_y ? y0 : min_y); 482 | max_x = (x1 > max_x) ? x1 : max_x; 483 | max_y = (y1 > max_y) ? y1 : max_y; 484 | 485 | cursor_x += glyph->xadvance; 486 | } 487 | 488 | // For empty strings 489 | if (glyph_count == 0) { 490 | min_x = min_y = max_x = max_y = 0; 491 | } 492 | 493 | if (width) 494 | *width = max_x - min_x; 495 | if (height) 496 | *height = max_y - min_y; 497 | if (start_x) 498 | *start_x = min_x; 499 | if (bearing_y) 500 | *bearing_y = -min_y; 501 | 502 | free(glyphs); 503 | } 504 | 505 | static int render_line(FontAtlas *atlas, float x, float y, Color color, GlyphInfo *glyphs, int glyph_count, 506 | float max_width, int *glyphs_consumed, float *rendered_width) { 507 | int end = 0; 508 | float width = 0; 509 | int last_break = -1; 510 | int atlas_w = atlas->width; 511 | int atlas_h = atlas->height; 512 | 513 | while (end < glyph_count) { 514 | GlyphInfo *glyph = &glyphs[end]; 515 | // printf("codepoint: %d\n", glyph->codepoint == '\n'); 516 | 517 | if (glyph->ft_codepoint == '\n') { 518 | end++; // Consume the newline but don't render it 519 | break; 520 | } 521 | 522 | if (glyph->ft_codepoint == ' ' || glyph->ft_codepoint == '\t') { 523 | last_break = end; 524 | } 525 | 526 | // Check if adding this glyph would exceed max_width 527 | if (width + glyph->xadvance > max_width && max_width > 0) { 528 | if (last_break > 0) { 529 | end = last_break; // Break at last space 530 | } 531 | break; 532 | } 533 | 534 | width += glyph->xadvance; 535 | end++; 536 | } 537 | 538 | // If we couldn't fit even one glyph, force at least one 539 | if (end == 0 && glyph_count > 0) { 540 | end = 1; 541 | width = glyphs[0].xadvance; 542 | } 543 | 544 | // Render the line 545 | float current_x = x; 546 | for (int i = 0; i < end; i++) { 547 | if (glyphs[i].ft_codepoint == '\n' || glyphs[i].ft_codepoint == '\t') { 548 | continue; 549 | } 550 | draw_glyph(current_x, y, color, &glyphs[i], atlas_w, atlas_h); 551 | current_x += glyphs[i].xadvance; 552 | } 553 | 554 | *glyphs_consumed = end; 555 | *rendered_width = width; 556 | return end; 557 | } 558 | 559 | float draw_wrapped_text(float size, float x, float y, const char *text, Color color, float max_width) { 560 | if (!text || !text[0] || !renderState.font_atlas) 561 | return 0; 562 | 563 | GlyphInfo *glyphs; 564 | int glyph_count; 565 | 566 | FontAtlas *atlas = renderState.font_atlas; 567 | if (!atlas) { 568 | fprintf(stderr, "Font atlas not created.\n"); 569 | return 0; 570 | } 571 | 572 | int font_id = renderState.current_font; 573 | // Get glyphs for the entire string 574 | get_string_glyphs(atlas, font_id, size, text, &glyphs, &glyph_count); 575 | 576 | // Line tracking variables 577 | float line_height = size * 1.5f; // Adjust line spacing as needed 578 | 579 | int start = 0; 580 | float total_height = 0; 581 | while (start < glyph_count) { 582 | int consumed; 583 | float line_width; 584 | 585 | render_line(atlas, x, y + total_height, color, &glyphs[start], glyph_count - start, max_width, &consumed, 586 | &line_width); 587 | 588 | start += consumed; 589 | total_height += line_height; 590 | } 591 | 592 | free(glyphs); 593 | 594 | return total_height; 595 | } 596 | 597 | void draw_single_line(float font_size, float x, float y, const char *text, Color color, float max_width) { 598 | GlyphInfo *glyphs; 599 | int glyph_count; 600 | get_string_glyphs(renderState.font_atlas, renderState.current_font, font_size, text, &glyphs, &glyph_count); 601 | 602 | int consumed; 603 | float width; 604 | render_line(renderState.font_atlas, x, y, color, glyphs, glyph_count, max_width, &consumed, &width); 605 | 606 | free(glyphs); 607 | } 608 | 609 | void draw_text(float size, float x, float y, const char *text, Color color) { 610 | // Get glyphs for the string 611 | GlyphInfo *glyphs; 612 | int glyph_count; 613 | 614 | FontAtlas *atlas = renderState.font_atlas; 615 | if (!atlas) { 616 | fprintf(stderr, "Font atlas not created.\n"); 617 | return; 618 | } 619 | 620 | int font_id = renderState.current_font; 621 | 622 | get_string_glyphs(atlas, font_id, size, text, &glyphs, &glyph_count); 623 | 624 | float cursor_x = x; 625 | float cursor_y = y; 626 | 627 | FontInfo *font = &atlas->fonts[font_id]; 628 | for (int i = 0; i < glyph_count; i++) { 629 | GlyphInfo *glyph = &glyphs[i]; 630 | if (glyph->codepoint == ' ') { 631 | cursor_x += glyph->xadvance; 632 | continue; 633 | } 634 | 635 | draw_glyph(cursor_x, cursor_y, color, glyph, atlas->width, atlas->height); 636 | 637 | cursor_x += glyph->xadvance; 638 | 639 | if (i < glyph_count - 1) { 640 | FT_Vector kerning; 641 | FT_UInt current = FT_Get_Char_Index(font->face, glyph->codepoint); 642 | FT_UInt next = FT_Get_Char_Index(font->face, glyphs[i + 1].codepoint); 643 | 644 | if (FT_HAS_KERNING(font->face)) { 645 | FT_Get_Kerning(font->face, current, next, FT_KERNING_DEFAULT, &kerning); 646 | cursor_x += kerning.x / 64.0f; // Convert from 26.6 format 647 | } 648 | } 649 | } 650 | 651 | free(glyphs); 652 | } 653 | 654 | // Clean up resources 655 | void destroy_font_atlas() { 656 | FontAtlas *atlas = renderState.font_atlas; 657 | if (!atlas) 658 | return; 659 | 660 | free(atlas->pixels); 661 | 662 | for (int i = 0; i < atlas->font_count; i++) { 663 | FT_Done_Face(atlas->fonts[i].face); 664 | hb_buffer_destroy(atlas->fonts[i].hb_buffer); 665 | hb_font_destroy(atlas->fonts[i].hb_font); 666 | } 667 | 668 | FT_Done_FreeType(atlas->ft_library); 669 | 670 | free(atlas->glyphs); 671 | 672 | glDeleteTextures(1, &atlas->texture_id); 673 | 674 | renderState.current_font = -1; 675 | free(atlas); 676 | } 677 | 678 | void begin_rect(float x, float y) { 679 | if (renderState.shape_in_progress) 680 | return; 681 | 682 | float pos[8] = { 683 | -1.0f, -1.0f, // Vertex 0: Bottom-left 684 | 1.0f, -1.0f, // Vertex 1: Bottom-right 685 | 1.0f, 1.0f, // Vertex 2: Top-right 686 | -1.0f, 1.0f // Vertex 3: Top-left 687 | }; 688 | 689 | renderState.shape_in_progress = true; 690 | int base_vert = renderState.vert_count; 691 | for (size_t i = 0; i < 4; ++i) { 692 | renderState.verts[base_vert + i] = (Vertex){ 693 | .pos = {pos[i * 2], pos[i * 2 + 1]}, 694 | .pos_px = {x, y}, 695 | }; 696 | } 697 | 698 | renderState.vert_count += 4; 699 | } 700 | 701 | void rect_size(float w, float h) { 702 | if (!renderState.shape_in_progress) 703 | return; 704 | 705 | for (size_t i = 4; i > 0; i--) { 706 | renderState.verts[renderState.vert_count - i].size[0] = w; 707 | renderState.verts[renderState.vert_count - i].size[1] = h; 708 | } 709 | } 710 | 711 | void rect_color(float r, float g, float b, float a) { 712 | if (!renderState.shape_in_progress) 713 | return; 714 | for (size_t i = 4; i > 0; i--) 715 | renderState.verts[renderState.vert_count - i].color = (Color){r, g, b, a}; 716 | } 717 | 718 | void rect_gradient_topdown(Color top, Color bottom) { 719 | if (!renderState.shape_in_progress) 720 | return; 721 | 722 | size_t vc = renderState.vert_count; 723 | renderState.verts[vc - 4].color = top; // top-left 724 | renderState.verts[vc - 3].color = top; // top-right 725 | renderState.verts[vc - 2].color = bottom; // bottom-right 726 | renderState.verts[vc - 1].color = bottom; // bottom-left 727 | } 728 | 729 | void rect_gradient_sides(Color left, Color right) { 730 | if (!renderState.shape_in_progress) 731 | return; 732 | 733 | size_t vc = renderState.vert_count; 734 | renderState.verts[vc - 4].color = left; // top-left 735 | renderState.verts[vc - 3].color = right; // top-right 736 | renderState.verts[vc - 2].color = right; // bottom-right 737 | renderState.verts[vc - 1].color = left; // bottom-left 738 | } 739 | 740 | void rect_gradient4(Color tl, Color tr, Color br, Color bl) { 741 | if (!renderState.shape_in_progress) 742 | return; 743 | 744 | size_t vc = renderState.vert_count; 745 | renderState.verts[vc - 4].color = tl; // top-left 746 | renderState.verts[vc - 3].color = tr; // top-right 747 | renderState.verts[vc - 2].color = br; // bottom-right 748 | renderState.verts[vc - 1].color = bl; // bottom-left 749 | } 750 | 751 | void rect_radius(float tl, float tr, float br, float bl) { 752 | if (!renderState.shape_in_progress) 753 | return; 754 | for (size_t i = 4; i > 0; i--) 755 | renderState.verts[renderState.vert_count - i].corner_radius = (Radius){tr, br, tl, bl}; 756 | } 757 | 758 | void rect_radius_all(float v) { rect_radius(v, v, v, v); } 759 | 760 | void end_rect() { 761 | if (!renderState.shape_in_progress) 762 | return; 763 | 764 | renderState.shape_in_progress = false; 765 | renderState.index_count += 6; 766 | } 767 | 768 | void add_rect(float x, float y, float w, float h, Color color[4]) { 769 | Radius rad = {0, 0, 0, 0}; 770 | float pos[8] = { 771 | -1.0f, -1.0f, // Vertex 0: Bottom-left 772 | 1.0f, -1.0f, // Vertex 1: Bottom-right 773 | 1.0f, 1.0f, // Vertex 2: Top-right 774 | -1.0f, 1.0f // Vertex 3: Top-left 775 | }; 776 | add_vert(pos[0], pos[1], w, h, color[0], x, y, 0, 0, 0, rad); 777 | add_vert(pos[2], pos[3], w, h, color[1], x, y, 0, 0, 0, rad); 778 | add_vert(pos[4], pos[5], w, h, color[2], x, y, 0, 0, 0, rad); 779 | add_vert(pos[6], pos[7], w, h, color[3], x, y, 0, 0, 0, rad); 780 | renderState.index_count += 6; 781 | } 782 | 783 | void ui_create() { 784 | static const char *vertex_shader_text = GLSL( // 785 | layout(location = 0) in vec2 vPos; // 786 | layout(location = 1) in vec4 vCol; // 787 | layout(location = 2) in vec2 vSize; // 788 | layout(location = 3) in vec2 aPos; // 789 | layout(location = 4) in vec4 aCornerRadius; // 790 | layout(location = 5) in vec2 vTexCoord; // 791 | layout(location = 6) in float vIsText; // 792 | 793 | out vec2 texCoord; // 794 | flat out vec4 cornerRadius; // 795 | flat out float isText; // 796 | out vec4 color; // 797 | out vec2 frag_local_pos; // this will be used to calculate UV 798 | out vec2 frag_rect_pos; // Global fragment position in screen space 799 | flat out vec2 frag_rect_size; // 800 | uniform vec2 u_resolution; // 801 | 802 | void main() { 803 | vec2 screenPos = aPos + (vPos * 0.5 + 0.5) * vSize; 804 | // Convert to NDC 805 | vec2 ndc = (screenPos / u_resolution) * 2.0 - 1.0; 806 | ndc.y = -ndc.y; // Flip Y 807 | gl_Position = vec4(ndc, 0.0, 1.0); 808 | 809 | frag_local_pos = screenPos; 810 | frag_rect_pos = aPos; 811 | frag_rect_size = vSize; 812 | color = vCol; 813 | cornerRadius = aCornerRadius; 814 | isText = vIsText; 815 | texCoord = vTexCoord; 816 | }); 817 | 818 | static const char *fragment_shader_text = GLSL( // 819 | in vec4 color; // 820 | in vec2 frag_local_pos; // 821 | in vec2 frag_rect_pos; // 822 | flat in vec2 frag_rect_size; // 823 | flat in vec4 cornerRadius; // 824 | // 825 | out vec4 fragment; // 826 | uniform vec2 u_resolution; // 827 | 828 | in vec2 texCoord; // 829 | flat in float isText; // 830 | uniform sampler2D u_glyph_texture; // 831 | 832 | float sdroundedbox(vec2 p, vec2 b, vec4 r) { // 833 | r.xy = (p.x > 0.0) ? r.xy : r.zw; 834 | r.x = (p.y > 0.0) ? r.x : r.y; 835 | vec2 q = abs(p) - b + r.x; 836 | return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r.x; 837 | } 838 | 839 | float roundedBox(vec2 uv, float radius) { 840 | vec2 halfSize = vec2(0.5) - radius; 841 | vec2 d = abs(uv - 0.5) - halfSize; 842 | return length(max(d, 0.0)) - radius; 843 | } 844 | 845 | void main() { 846 | // Check if we're in the rectangle area 847 | if (frag_local_pos.x < frag_rect_pos.x || frag_local_pos.x > frag_rect_pos.x + frag_rect_size.x || 848 | frag_local_pos.y < frag_rect_pos.y || frag_local_pos.y > frag_rect_pos.y + frag_rect_size.y) { 849 | discard; // Early out for pixels outside the rectangle 850 | return; 851 | } 852 | 853 | if (isText > 0.5) { 854 | float glyph_alpha = texture(u_glyph_texture, texCoord).r; // grayscale 855 | fragment = vec4(color.rgb, color.a * glyph_alpha); 856 | return; 857 | } 858 | 859 | // // if circle 860 | // vec2 uv = (frag_local_pos - frag_rect_pos) / frag_rect_size; 861 | // float dist = length(uv - 0.5); 862 | // float alpha = 1.0 - smoothstep(0.49, 0.5, dist); 863 | // fragment = vec4(color.rgb, color.a * alpha); 864 | // return; 865 | 866 | vec2 center = frag_rect_pos + frag_rect_size * 0.5; // Center of rectangle 867 | vec2 p = frag_local_pos - center; // Vector from center to current fragment 868 | float d = sdroundedbox(p, frag_rect_size * 0.5, cornerRadius); // 869 | float alpha = 1.0 - smoothstep(-0.5, 0.5, d); // Smooth edge 870 | fragment = vec4(color.rgb, color.a * alpha); 871 | }); 872 | 873 | glGenBuffers(1, &renderState.vertex_buffer); 874 | glBindBuffer(GL_ARRAY_BUFFER, renderState.vertex_buffer); 875 | glBufferData(GL_ARRAY_BUFFER, sizeof(Vertex) * BATCHES * 4, renderState.verts, GL_DYNAMIC_DRAW); 876 | 877 | const GLuint vertex_shader = glCreateShader(GL_VERTEX_SHADER); 878 | glShaderSource(vertex_shader, 1, &vertex_shader_text, NULL); 879 | glCompileShader(vertex_shader); 880 | check_shader_error(vertex_shader, "vertex"); 881 | 882 | const GLuint fragment_shader = glCreateShader(GL_FRAGMENT_SHADER); 883 | glShaderSource(fragment_shader, 1, &fragment_shader_text, NULL); 884 | glCompileShader(fragment_shader); 885 | check_shader_error(fragment_shader, "fragment"); 886 | 887 | renderState.program = glCreateProgram(); 888 | glAttachShader(renderState.program, vertex_shader); 889 | glAttachShader(renderState.program, fragment_shader); 890 | glLinkProgram(renderState.program); 891 | 892 | glDeleteShader(vertex_shader); 893 | glDeleteShader(fragment_shader); 894 | 895 | glGenVertexArrays(1, &renderState.vertex_array); 896 | glBindVertexArray(renderState.vertex_array); 897 | 898 | glEnableVertexAttribArray(0); 899 | glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void *)offsetof(Vertex, pos)); 900 | 901 | glEnableVertexAttribArray(1); 902 | glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void *)offsetof(Vertex, color)); 903 | 904 | glEnableVertexAttribArray(2); 905 | glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void *)offsetof(Vertex, size)); 906 | 907 | glEnableVertexAttribArray(3); 908 | glVertexAttribPointer(3, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void *)offsetof(Vertex, pos_px)); 909 | 910 | glEnableVertexAttribArray(4); 911 | glVertexAttribPointer(4, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void *)offsetof(Vertex, corner_radius)); 912 | 913 | glEnableVertexAttribArray(5); 914 | glVertexAttribPointer(5, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void *)offsetof(Vertex, texcoord)); 915 | 916 | glEnableVertexAttribArray(6); 917 | glVertexAttribPointer(6, 1, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void *)offsetof(Vertex, is_text)); 918 | 919 | uint32_t *indices = malloc(sizeof(uint32_t) * BATCHES * 6); 920 | uint32_t offset = 0; 921 | for (uint32_t i = 0; i < BATCHES * 6; i += 6) { 922 | // tri 1 923 | indices[i + 0] = offset + 0; 924 | indices[i + 1] = offset + 1; 925 | indices[i + 2] = offset + 2; 926 | 927 | // tri 2 928 | indices[i + 3] = offset + 2; 929 | indices[i + 4] = offset + 3; 930 | indices[i + 5] = offset + 0; 931 | offset += 4; 932 | } 933 | 934 | GLuint ebo; 935 | glGenBuffers(1, &ebo); 936 | glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo); 937 | glBufferData(GL_ELEMENT_ARRAY_BUFFER, 6 * BATCHES * sizeof(uint32_t), indices, GL_STATIC_DRAW); 938 | 939 | free(indices); 940 | 941 | // Create the font atlas 942 | create_font_atlas(); 943 | 944 | glEnable(GL_BLEND); 945 | glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); 946 | } 947 | 948 | void start_frame(float width, float height) { 949 | glUseProgram(renderState.program); 950 | glUniform2f(glGetUniformLocation(renderState.program, "u_resolution"), (float)width, (float)height); 951 | } 952 | 953 | void ui_delete() { 954 | destroy_font_atlas(); 955 | glDeleteProgram(renderState.program); 956 | } 957 | 958 | void end_frame() { 959 | // printf("vert count: %d\n", renderState.vert_count); 960 | // printf("/////////////////\n"); 961 | // for (int i = 0; i < renderState.vert_count; i++) { 962 | // Vertex v = renderState.verts[i]; 963 | // // printf("Vertex %d: pos = (%.2f, %.2f)\n", i, v.pos[0], v.pos[1]); 964 | // printf("Vertex %d: pos_px = (%.2f, %.2f)\n", i, v.pos_px[0], v.pos_px[1]); 965 | // } 966 | // printf("/////////////////\n"); 967 | 968 | glBindVertexArray(renderState.vertex_array); 969 | glBindBuffer(GL_ARRAY_BUFFER, renderState.vertex_buffer); 970 | glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(renderState.verts), renderState.verts); 971 | 972 | glDrawElements(GL_TRIANGLES, renderState.index_count, GL_UNSIGNED_INT, 0); 973 | 974 | renderState.vert_count = 0; 975 | renderState.index_count = 0; 976 | } 977 | --------------------------------------------------------------------------------