Skip to content

Commit

Permalink
make it fancy
Browse files Browse the repository at this point in the history
  • Loading branch information
griffi-gh committed Oct 15, 2024
1 parent 85ba747 commit dcfd9d9
Show file tree
Hide file tree
Showing 3 changed files with 246 additions and 26 deletions.
4 changes: 4 additions & 0 deletions src/src/ui_gtk3.hh
Original file line number Diff line number Diff line change
Expand Up @@ -6280,6 +6280,10 @@ int _gtk_loop(void *p)
//Load CSS themes
load_gtk_css();

#ifdef GTK3_LEVEL_BROWSER_ENABLE
init_community_level_browser();
#endif

g_object_set(
gtk_settings_get_default(),
"gtk-application-prefer-dark-theme", true,
Expand Down
261 changes: 235 additions & 26 deletions src/src/ui_gtk3_levelbrowser.cc
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,17 @@
#include "tms/backend/print.h"

#include <string>
#include <vector>
#include <curl/curl.h>
#include <nlohmann/json.hpp>
#include <SDL_mutex.h>
#include <glib.h>
#include <gtk/gtk.h>
#include "pango/pango-layout.h"
#include "gio/gio.h"
#include <curl/easy.h>
#include <curl/curl.h>
#include <thread>

namespace api {
using json = nlohmann::json;
Expand Down Expand Up @@ -68,6 +74,12 @@ namespace api {
return size * nmemb;
}

static size_t WriteCallbackBinary(void *contents, size_t size, size_t nmemb, void *userp) {
std::vector<uint8_t> *vec = (std::vector<uint8_t>*)userp;
vec->insert(vec->end(), (uint8_t*)contents, (uint8_t*)contents + size * nmemb);
return size * nmemb;
}

static std::vector<struct recent_level> get_recent_levels(uint32_t offset, uint32_t limit) {
if (!P.curl) tms_fatalf("curl not initialized");
SDL_LockMutex(P.curl_mutex);
Expand Down Expand Up @@ -109,6 +121,63 @@ namespace api {
static struct level get_level(uint32_t id) {
tms_fatalf("not implemented"); exit(1);
}

static std::vector<uint8_t> get_level_thumbnail(uint32_t id, bool use_global_curl /* = true */) {
CURL* curl;

if (use_global_curl) {
// TODO instad of this use the multi API
curl = curl_easy_init();
} else {
if (!P.curl) tms_fatalf("curl not initialized");
SDL_LockMutex(P.curl_mutex);
curl = P.curl;
}

char url[256];
snprintf(url, 255, "https://%s/thumbs/low/%u.jpg", P.community_host, id);

curl_easy_setopt(curl, CURLOPT_URL, url);

std::vector<uint8_t> response;
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallbackBinary);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

// TODO handle error
CURLcode res = curl_easy_perform(curl);
if (res != CURLE_OK) tms_fatalf("[fuck] curl error");

// TODO handle error
long http_code = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
if (http_code != 200) tms_fatalf("[fuck] HTTP error %ld", http_code);

// Sleep to simulate slow connection
// std::this_thread::sleep_for(std::chrono::seconds(5));

if (use_global_curl) {
curl_easy_cleanup(curl);
} else {
SDL_UnlockMutex(P.curl_mutex);
}

return response;
}
}

void _api_get_level_thumbnail_async(uint32_t id, GAsyncReadyCallback callback, gpointer user_data) {
GTask *task = g_task_new(NULL, NULL, callback, user_data);
g_task_set_source_tag(task, (gpointer)_api_get_level_thumbnail_async);
g_task_set_task_data(task, (gpointer)(size_t)id, NULL);

g_task_run_in_thread(task, [](GTask *task, gpointer source_object, gpointer task_data, GCancellable *cancellable) {
uint32_t id = (size_t)task_data;
std::vector<uint8_t> thumbnail = api::get_level_thumbnail(id, false);
std::vector<uint8_t> *thumbnail_heap = new std::vector<uint8_t>(thumbnail);
tms_infof("Async thumbnail loaded %p", thumbnail_heap);
g_task_return_pointer(task, thumbnail_heap, free);
});
// g_task_return_pointer(task, NULL, g_free);
}

namespace gtk_community {
Expand All @@ -135,23 +204,85 @@ namespace gtk_community {
// TODO this is placeholder
const char *title = level.title.c_str();
const char *username = level.u.name.c_str();
gpointer user_id = (gpointer)(size_t)level.u.id;
gpointer level_id = (gpointer)(size_t)level.id;

// Create a flow child (activatable)
// GtkWidget *child = gtk_flow_box_child_new();
// g_signal_connect(child, "activate", G_CALLBACK(on_level_clicked), (gpointer)level_id);
// g_signal_connect(child, "clicked", G_CALLBACK(on_level_clicked), (gpointer)level_id);

// Create a box to hold the tile elements vertically
GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 5);
GtkStyleContext *context = gtk_widget_get_style_context(GTK_WIDGET(box));
gtk_style_context_add_class(context, "hc-level-tile");

// gtk_container_add(GTK_CONTAINER(child), box);

// Placeholder for the level image (just a blank box for now)
GtkWidget *image = gtk_drawing_area_new();
gtk_widget_set_size_request(image, 100, 100); // Placeholder size
gtk_widget_set_name(image, "level-image");
GtkWidget *image = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_widget_set_size_request(image, 240, 135); // Placeholder size
// Kick off async thumbnail fetch
GAsyncReadyCallback callback = [](GObject *source_object, GAsyncResult *res, gpointer user_data) {
// XXX: some of this may leak memory :<

GTask *task = G_TASK(res);
std::vector<uint8_t> *result = (std::vector<uint8_t>*)g_task_propagate_pointer(task, NULL);
tms_infof("Thumbnail loaded %p", result);
GtkWidget *image = GTK_WIDGET(user_data);

tms_debugf("Image data size: %zu", result->size());

GdkPixbufLoader *loader = gdk_pixbuf_loader_new_with_type("jpeg", NULL);
gdk_pixbuf_loader_write(loader, result->data(), result->size(), NULL);
gdk_pixbuf_loader_close(loader, NULL);
GdkPixbuf *pixbuf = gdk_pixbuf_loader_get_pixbuf(loader);

tms_debugf("pixbuf pointer: %p", pixbuf);
tms_debugf("pixbuf image size: %d x %d", gdk_pixbuf_get_width(pixbuf), gdk_pixbuf_get_height(pixbuf));

GtkWidget *image_widget = gtk_image_new_from_pixbuf(pixbuf);
gtk_container_add(GTK_CONTAINER(image), image_widget);
gtk_widget_show_all(image);

g_object_unref(loader);
delete result;
};
_api_get_level_thumbnail_async(level.id, callback, image);

// Level title (clickable button)
GtkWidget *level_button = gtk_button_new_with_label(title);
gpointer level_id = (gpointer)(size_t)level.id;
GtkWidget *level_label = gtk_label_new(title);
gtk_label_set_xalign(GTK_LABEL(level_label), 0.5);
// gtk_label_set_lines(GTK_LABEL(level_label), 2);
gtk_label_set_ellipsize(GTK_LABEL(level_label), PANGO_ELLIPSIZE_END);
gtk_label_set_width_chars(GTK_LABEL(level_label), 20);
gtk_label_set_max_width_chars(GTK_LABEL(level_label), 20);

GtkWidget *level_button = gtk_button_new();
gtk_button_set_relief(GTK_BUTTON(level_button), GTK_RELIEF_NONE);
gtk_container_add(GTK_CONTAINER(level_button), level_label);
g_signal_connect(level_button, "clicked", G_CALLBACK(on_level_clicked), (gpointer)level_id);

// Username (clickable label)
// TODO set user color
GtkWidget *username_button = gtk_button_new_with_label(username);
gpointer user_id = (gpointer)(size_t)level.u.id;
GtkWidget *username_label = gtk_label_new(username);
gtk_label_set_xalign(GTK_LABEL(username_label), 0.5);
gtk_label_set_lines(GTK_LABEL(username_label), 1);
gtk_label_set_ellipsize(GTK_LABEL(username_label), PANGO_ELLIPSIZE_END);
gtk_label_set_width_chars(GTK_LABEL(username_label), 20);
gtk_label_set_max_width_chars(GTK_LABEL(username_label), 20);
// HACK: override color
const static GdkRGBA rgba_color = {
.red = 109. / 255. ,
.green = 160. / 255.,
.blue = 253 / 255.,
.alpha = 1.0,
};
gtk_widget_override_color(username_label, GTK_STATE_FLAG_NORMAL, &rgba_color);

GtkWidget *username_button = gtk_button_new();
gtk_button_set_relief(GTK_BUTTON(username_button), GTK_RELIEF_NONE);
gtk_container_add(GTK_CONTAINER(username_button), username_label);
g_signal_connect(username_button, "clicked", G_CALLBACK(on_username_clicked), (gpointer)user_id);

// Pack elements into the box
Expand All @@ -162,7 +293,70 @@ namespace gtk_community {
return box;
}

static GtkWidget *create_level_grid(const std::vector<api::recent_level> &levels) {
// Create a grid to hold the level tiles
GtkWidget* flow_box = gtk_flow_box_new();
gtk_flow_box_set_max_children_per_line(GTK_FLOW_BOX(flow_box), 6);
gtk_flow_box_set_min_children_per_line(GTK_FLOW_BOX(flow_box), 4);
gtk_flow_box_set_selection_mode(GTK_FLOW_BOX(flow_box), GTK_SELECTION_NONE);
// gtk_flow_box_unselect_all(GTK_FLOW_BOX(flow_box));
// gtk_flow_box_set_activate_on_single_click(GTK_FLOW_BOX(flow_box), true);
// gtk_container_add(GTK_CONTAINER(content_area), flow_box);

// Add level tiles to the grid
for (auto &level : levels) {
// Create level tile
GtkWidget *level_tile = create_level_tile(level);

// Attach each tile in the grid
gtk_container_add(GTK_CONTAINER(flow_box), level_tile);
}

return flow_box;
}

static GtkWidget* create_shelf(std::string name, GtkWidget *content) {
// Create a box to hold the shelf elements vertically
GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 5);

// Shelf header
GtkWidget *header = gtk_label_new(name.c_str());
// HACK: override font size
PangoFontDescription *font_desc = pango_font_description_new();
pango_font_description_set_size(font_desc, 24 * PANGO_SCALE);
gtk_widget_override_font(header, font_desc);
gtk_container_add(GTK_CONTAINER(box), header);

// Shelf content
gtk_container_add(GTK_CONTAINER(box), content);

return box;
}

// Create top shelf. It contains:
// Input box + button to open level by ID or search
// (If number is entered, open level by ID, otherwise search)
static GtkWidget* create_top_shelf_content() {
GtkWidget *box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5);
gtk_widget_set_halign(GTK_WIDGET(box), GTK_ALIGN_CENTER);
gtk_widget_set_valign(GTK_WIDGET(box), GTK_ALIGN_CENTER);

// Search icon
GtkWidget *search_icon = gtk_image_new_from_icon_name("edit-find-symbolic", GTK_ICON_SIZE_BUTTON);
gtk_container_add(GTK_CONTAINER(box), search_icon);

// Input box
GtkWidget *entry = gtk_entry_new();
gtk_entry_set_placeholder_text(GTK_ENTRY(entry), "Enter level ID or search query");
gtk_entry_set_width_chars(GTK_ENTRY(entry), 40);
gtk_box_pack_start(GTK_BOX(box), entry, FALSE, TRUE, 0);
gtk_box_set_center_widget(GTK_BOX(box), entry);

return box;
}

static GtkWidget* create_dialog(const std::vector<api::recent_level> &levels) {
// Create a dialog window
GtkWidget *dialog = gtk_dialog_new_with_buttons(
"Community Levels",
NULL,
Expand All @@ -179,25 +373,15 @@ namespace gtk_community {
// Get the content area of the dialog
GtkWidget *content_area = gtk_dialog_get_content_area(GTK_DIALOG(dialog));

// Create a grid to hold the level tiles
GtkWidget *grid = gtk_grid_new();
gtk_grid_set_row_spacing(GTK_GRID(grid), 10);
gtk_grid_set_column_spacing(GTK_GRID(grid), 10);
gtk_container_add(GTK_CONTAINER(content_area), grid);
// Add the top shelf
GtkWidget *top_shelf_content = create_top_shelf_content();
GtkWidget *top_shelf = create_shelf("Open level", top_shelf_content);
gtk_container_add(GTK_CONTAINER(content_area), top_shelf);

// Add level tiles to the grid
int num_levels = 6;
int rows = 2; // 2 rows for now
int cols = 3; // 3 columns for now

for (int i = 0; i < num_levels; i++) {
GtkWidget *level_tile = create_level_tile(levels[i]);

// Attach each tile in the grid
int row = i / cols;
int col = i % cols;
gtk_grid_attach(GTK_GRID(grid), level_tile, col, row, 1, 1);
}
// Add the level shelf to the dialog
GtkWidget *level_grid = create_level_grid(levels);
GtkWidget *level_shelf = create_shelf("Recent Levels", level_grid);
gtk_container_add(GTK_CONTAINER(content_area), level_shelf);

return dialog;
}
Expand All @@ -207,7 +391,7 @@ gboolean open_community_level_browser(gpointer _) {
tms_infof("====== Open level browser ======");

tms_infof("Fetching recent levels...");
std::vector<api::recent_level> levels = api::get_recent_levels(0, 6);
std::vector<api::recent_level> levels = api::get_recent_levels(0, 12);

tms_infof("Creating dialog...");
GtkWidget *dialog = gtk_community::create_dialog(levels);
Expand All @@ -216,5 +400,30 @@ gboolean open_community_level_browser(gpointer _) {
return false;
}

void init_community_level_browser() {
const gchar* css_global = R"(
.hc-level-tile {
padding: 5px;
border: 1px solid #444;
border-radius: 4px;
background-color: #060606;
}
.hc-level-tile button {
padding: 0;
}
)";
GtkCssProvider* css_provider = gtk_css_provider_new();
gtk_css_provider_load_from_data(
css_provider,
css_global,
-1, NULL
);
gtk_style_context_add_provider_for_screen(
gdk_screen_get_default(),
GTK_STYLE_PROVIDER(css_provider),
GTK_STYLE_PROVIDER_PRIORITY_APPLICATION
);
}

#endif

7 changes: 7 additions & 0 deletions src/src/ui_gtk3_levelbrowser.hh
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,17 @@ namespace api {

static std::vector<recent_level> get_recent_levels(uint32_t offset, uint32_t limit);
static level get_level(uint32_t id);
static std::vector<uint8_t> get_level_thumbnail(uint32_t id, bool use_global_curl = true);
};

namespace gtk_community {
static void on_level_clicked(GtkWidget *widget, gpointer data);
static void on_username_clicked(GtkWidget *widget, gpointer data);
// Creates a single level tile (image, title, and username)
static GtkWidget *create_level_tile(const api::recent_level &level);
static GtkWidget *create_level_grid(const std::vector<api::recent_level> &levels);
static GtkWidget *create_top_shelf_content();
static GtkWidget *create_shelf(std::string name, GtkWidget *content);
static GtkWidget *create_dialog(const std::vector<api::recent_level> &levels);
}

Expand All @@ -76,6 +80,9 @@ namespace gtk_community {
// (If calling from ui::open_dialog, make sure to wrap it in gdk_threads_add_idle!)
gboolean open_community_level_browser(gpointer _);

// Call in ui::init() after gtk_init()
void init_community_level_browser();

// class gtk_community_state {
// uint16_t cur_page;
// // std::unordered_map<uint32_t, uint8_t*> cache_thumbnails;
Expand Down

0 comments on commit dcfd9d9

Please sign in to comment.