#include #include #include #include #include "gui.h" // Define CSS data for the application's look and feel static const char* css_data = "window {\n" " background-color: #0b090a;\n" " color: #f5f3f4;\n" " font-family: 'Inter', 'Segoe UI', sans-serif;\n" "}\n" "\n" ".title-label {\n" " font-size: 28px;\n" " font-weight: 800;\n" " color: #f5f3f4;\n" " margin-top: 15px;\n" " margin-bottom: 5px;\n" " letter-spacing: 2px;\n" "}\n" "\n" ".subtitle-label {\n" " font-size: 14px;\n" " color: #b1a7a6;\n" " margin-bottom: 15px;\n" "}\n" "\n" ".worry-text-view {\n" " font-family: 'JetBrains Mono', 'Fira Code', 'Courier New', monospace;\n" " font-size: 16px;\n" " background-color: #161a1d;\n" " color: #f5f3f4;\n" " border: 2px solid #3f3f46;\n" " border-radius: 8px;\n" " padding: 15px;\n" "}\n" "\n" ".worry-text-view:focus {\n" " border-color: #7209b7;\n" "}\n" "\n" ".action-button {\n" " font-size: 14px;\n" " font-weight: 600;\n" " padding: 12px 24px;\n" " border-radius: 8px;\n" " border: none;\n" " color: white;\n" " margin-bottom: 10px;\n" "}\n" "\n" ".btn-scream {\n" " background: #7209b7;\n" " box-shadow: 0 4px 12px rgba(114, 9, 183, 0.3);\n" "}\n" "\n" ".btn-scream:hover {\n" " background: #8f20d8;\n" "}\n" "\n" ".btn-burn {\n" " background: #d90429;\n" " box-shadow: 0 4px 12px rgba(217, 4, 41, 0.3);\n" "}\n" "\n" ".btn-burn:hover {\n" " background: #ef233c;\n" "}\n" "\n" ".btn-dissolve {\n" " background: #06d6a0;\n" " color: #0b090a;\n" " box-shadow: 0 4px 12px rgba(6, 214, 160, 0.3);\n" "}\n" "\n" ".btn-dissolve:hover {\n" " background: #1ae8b2;\n" "}\n" "\n" ".text-scream {\n" " color: #b388ff;\n" "}\n" "\n" ".text-burn {\n" " color: #ff9100;\n" "}\n" "\n" ".text-dissolve {\n" " color: #00e5ff;\n" "}\n" "\n" ".consumed-label {\n" " font-size: 24px;\n" " font-weight: 700;\n" " color: #f5f3f4;\n" " margin-bottom: 25px;\n" "}\n" "\n" ".btn-reset {\n" " background-color: #3f3f46;\n" " color: white;\n" " padding: 10px 20px;\n" " border-radius: 6px;\n" " border: 1px solid #52525b;\n" "}\n" "\n" ".btn-reset:hover {\n" " background-color: #52525b;\n" "}\n" ; typedef enum { ANIM_SCREAM, ANIM_BURN, ANIM_DISSOLVE } AnimType; typedef struct { GtkWidget *stack; GtkWidget *text_view; // Input text view GtkWidget *anim_text_view; // Animation text view (read-only) GtkWidget *anim_title; // Title during animation GtkWidget *consumed_label; // Label on the consumed page AnimType type; char *original_text; char *animated_text; int step; int max_steps; guint timer_id; } AppState; static AppState app_state = {NULL, NULL, NULL, NULL, NULL, ANIM_SCREAM, NULL, NULL, 0, 0, 0}; // Forward declarations static gboolean anim_timer_tick(gpointer user_data); static void start_animation(AnimType type, const char* title, int max_steps, int interval_ms); static void on_btn_scream_clicked(GtkButton *btn, gpointer user_data); static void on_btn_burn_clicked(GtkButton *btn, gpointer user_data); static void on_btn_dissolve_clicked(GtkButton *btn, gpointer user_data); static void on_btn_reset_clicked(GtkButton *btn, gpointer user_data); static gboolean anim_timer_tick(gpointer user_data) { AppState *state = (AppState*)user_data; if (state->animated_text == NULL) { state->timer_id = 0; return FALSE; } int len = strlen(state->animated_text); if (state->type == ANIM_SCREAM) { for (int i = 0; i < len; i++) { if (state->animated_text[i] != ' ' && state->animated_text[i] != '\n' && state->animated_text[i] != '.' && (rand() % 100) < (state->step * 7)) { state->animated_text[i] = (rand() % 2 == 0) ? '.' : ' '; } } } else if (state->type == ANIM_BURN) { const char* fire_chars = "@#%*+=-:. "; int fire_len = strlen(fire_chars); for (int i = 0; i < len; i++) { if (state->animated_text[i] != ' ' && state->animated_text[i] != '\n') { int r = rand() % 100; if (r < (state->step * 10)) { int char_idx = (state->step / 3) + (rand() % 3); if (char_idx >= fire_len) state->animated_text[i] = ' '; else state->animated_text[i] = fire_chars[char_idx]; } } } } else if (state->type == ANIM_DISSOLVE) { for (int i = 0; i < len; i++) { if (state->animated_text[i] != ' ' && state->animated_text[i] != '\n') { if ((rand() % 100) < (state->step * 5)) { state->animated_text[i] = (rand() % 5 == 0) ? '~' : (rand() % 2 == 0 ? ',' : ' '); } } } } // Update the animation buffer GtkTextBuffer *buf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(state->anim_text_view)); gtk_text_buffer_set_text(buf, state->animated_text, -1); state->step++; if (state->step > state->max_steps) { state->timer_id = 0; // Transition to consumed page gtk_stack_set_visible_child_name(GTK_STACK(state->stack), "consumed"); if (state->type == ANIM_SCREAM) { gtk_label_set_text(GTK_LABEL(state->consumed_label), "The void has consumed your worries."); } else if (state->type == ANIM_BURN) { gtk_label_set_text(GTK_LABEL(state->consumed_label), "Ashes to ashes. Dust to dust."); } else { gtk_label_set_text(GTK_LABEL(state->consumed_label), "Dissolved into nothingness."); } if (state->original_text) { g_free(state->original_text); state->original_text = NULL; } if (state->animated_text) { g_free(state->animated_text); state->animated_text = NULL; } return FALSE; // Stop the timer } return TRUE; // Continue the timer } static void start_animation(AnimType type, const char* title, int max_steps, int interval_ms) { GtkTextBuffer *buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(app_state.text_view)); GtkTextIter start, end; gtk_text_buffer_get_bounds(buffer, &start, &end); char *text = gtk_text_buffer_get_text(buffer, &start, &end, FALSE); if (text == NULL || strlen(text) == 0) { g_free(text); return; } // Clean up any old timer or strings if (app_state.timer_id != 0) { g_source_remove(app_state.timer_id); app_state.timer_id = 0; } if (app_state.original_text) { g_free(app_state.original_text); app_state.original_text = NULL; } if (app_state.animated_text) { g_free(app_state.animated_text); app_state.animated_text = NULL; } app_state.type = type; app_state.original_text = g_strdup(text); app_state.animated_text = g_strdup(text); app_state.step = 0; app_state.max_steps = max_steps; g_free(text); // Set up css classes for anim text view gtk_widget_remove_css_class(app_state.anim_text_view, "text-scream"); gtk_widget_remove_css_class(app_state.anim_text_view, "text-burn"); gtk_widget_remove_css_class(app_state.anim_text_view, "text-dissolve"); if (type == ANIM_SCREAM) { gtk_widget_add_css_class(app_state.anim_text_view, "text-scream"); } else if (type == ANIM_BURN) { gtk_widget_add_css_class(app_state.anim_text_view, "text-burn"); } else { gtk_widget_add_css_class(app_state.anim_text_view, "text-dissolve"); } gtk_label_set_text(GTK_LABEL(app_state.anim_title), title); // Set initial text on anim view GtkTextBuffer *anim_buf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(app_state.anim_text_view)); gtk_text_buffer_set_text(anim_buf, app_state.animated_text, -1); // Transition to animation screen gtk_stack_set_visible_child_name(GTK_STACK(app_state.stack), "anim"); // Start timer app_state.timer_id = g_timeout_add(interval_ms, anim_timer_tick, &app_state); } static void on_btn_scream_clicked(GtkButton *btn, gpointer user_data) { (void)btn; (void)user_data; start_animation(ANIM_SCREAM, "SCREAMING INTO THE VOID...", 20, 150); } static void on_btn_burn_clicked(GtkButton *btn, gpointer user_data) { (void)btn; (void)user_data; start_animation(ANIM_BURN, "LETTING IT BURN...", 25, 100); } static void on_btn_dissolve_clicked(GtkButton *btn, gpointer user_data) { (void)btn; (void)user_data; start_animation(ANIM_DISSOLVE, "LETTING IT DISSOLVE...", 30, 100); } static void on_btn_reset_clicked(GtkButton *btn, gpointer user_data) { (void)btn; (void)user_data; GtkTextBuffer *buf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(app_state.text_view)); gtk_text_buffer_set_text(buf, "", -1); gtk_stack_set_visible_child_name(GTK_STACK(app_state.stack), "input"); gtk_widget_grab_focus(app_state.text_view); } static void activate(GtkApplication* app, gpointer user_data) { const char* initial_text = (const char*)user_data; // Create main window GtkWidget *window = gtk_application_window_new(app); gtk_window_set_title(GTK_WINDOW(window), "Scream Into The Void"); gtk_window_set_default_size(GTK_WINDOW(window), 800, 600); // Apply CSS styling GtkCssProvider *provider = gtk_css_provider_new(); gtk_css_provider_load_from_string(provider, css_data); gtk_style_context_add_provider_for_display( gdk_display_get_default(), GTK_STYLE_PROVIDER(provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION ); g_object_unref(provider); // Main layout container: Stack app_state.stack = gtk_stack_new(); gtk_stack_set_transition_type(GTK_STACK(app_state.stack), GTK_STACK_TRANSITION_TYPE_CROSSFADE); gtk_stack_set_transition_duration(GTK_STACK(app_state.stack), 400); gtk_window_set_child(GTK_WINDOW(window), app_state.stack); // ---------------------------------------------------- // PAGE 1: Input Page // ---------------------------------------------------- GtkWidget *input_page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10); gtk_widget_set_margin_start(input_page, 40); gtk_widget_set_margin_end(input_page, 40); gtk_widget_set_margin_top(input_page, 20); gtk_widget_set_margin_bottom(input_page, 30); GtkWidget *title_label = gtk_label_new("SCREAM INTO THE VOID"); gtk_widget_add_css_class(title_label, "title-label"); gtk_box_append(GTK_BOX(input_page), title_label); GtkWidget *subtitle_label = gtk_label_new("What's weighing on your soul?"); gtk_widget_add_css_class(subtitle_label, "subtitle-label"); gtk_box_append(GTK_BOX(input_page), subtitle_label); // Scrolled text area for input GtkWidget *scrolled_window = gtk_scrolled_window_new(); gtk_widget_set_vexpand(scrolled_window, TRUE); gtk_widget_set_hexpand(scrolled_window, TRUE); gtk_widget_set_margin_top(scrolled_window, 10); gtk_widget_set_margin_bottom(scrolled_window, 15); app_state.text_view = gtk_text_view_new(); gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(app_state.text_view), GTK_WRAP_WORD_CHAR); gtk_widget_add_css_class(app_state.text_view, "worry-text-view"); // Set initial text if any (e.g. read from file) if (initial_text && strlen(initial_text) > 0) { GtkTextBuffer *buf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(app_state.text_view)); gtk_text_buffer_set_text(buf, initial_text, -1); } gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scrolled_window), app_state.text_view); gtk_box_append(GTK_BOX(input_page), scrolled_window); // Action buttons container GtkWidget *btn_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 20); gtk_widget_set_halign(btn_box, GTK_ALIGN_CENTER); GtkWidget *btn_scream = gtk_button_new_with_label("Scream it into the void"); gtk_widget_add_css_class(btn_scream, "action-button"); gtk_widget_add_css_class(btn_scream, "btn-scream"); g_signal_connect(btn_scream, "clicked", G_CALLBACK(on_btn_scream_clicked), NULL); GtkWidget *btn_burn = gtk_button_new_with_label("Let it burn"); gtk_widget_add_css_class(btn_burn, "action-button"); gtk_widget_add_css_class(btn_burn, "btn-burn"); g_signal_connect(btn_burn, "clicked", G_CALLBACK(on_btn_burn_clicked), NULL); GtkWidget *btn_dissolve = gtk_button_new_with_label("Let it dissolve"); gtk_widget_add_css_class(btn_dissolve, "action-button"); gtk_widget_add_css_class(btn_dissolve, "btn-dissolve"); g_signal_connect(btn_dissolve, "clicked", G_CALLBACK(on_btn_dissolve_clicked), NULL); gtk_box_append(GTK_BOX(btn_box), btn_scream); gtk_box_append(GTK_BOX(btn_box), btn_burn); gtk_box_append(GTK_BOX(btn_box), btn_dissolve); gtk_box_append(GTK_BOX(input_page), btn_box); gtk_stack_add_named(GTK_STACK(app_state.stack), input_page, "input"); // ---------------------------------------------------- // PAGE 2: Animation Page // ---------------------------------------------------- GtkWidget *anim_page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10); gtk_widget_set_margin_start(anim_page, 40); gtk_widget_set_margin_end(anim_page, 40); gtk_widget_set_margin_top(anim_page, 20); gtk_widget_set_margin_bottom(anim_page, 30); app_state.anim_title = gtk_label_new("THE VOID IS CONSUMING..."); gtk_widget_add_css_class(app_state.anim_title, "title-label"); gtk_box_append(GTK_BOX(anim_page), app_state.anim_title); // Subtitle placeholder to keep consistent alignment GtkWidget *anim_subtitle = gtk_label_new("Release your hold on these thoughts."); gtk_widget_add_css_class(anim_subtitle, "subtitle-label"); gtk_box_append(GTK_BOX(anim_page), anim_subtitle); GtkWidget *anim_scrolled = gtk_scrolled_window_new(); gtk_widget_set_vexpand(anim_scrolled, TRUE); gtk_widget_set_hexpand(anim_scrolled, TRUE); gtk_widget_set_margin_top(anim_scrolled, 10); gtk_widget_set_margin_bottom(anim_scrolled, 15); app_state.anim_text_view = gtk_text_view_new(); gtk_text_view_set_editable(GTK_TEXT_VIEW(app_state.anim_text_view), FALSE); gtk_text_view_set_cursor_visible(GTK_TEXT_VIEW(app_state.anim_text_view), FALSE); gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(app_state.anim_text_view), GTK_WRAP_WORD_CHAR); gtk_widget_add_css_class(app_state.anim_text_view, "worry-text-view"); gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(anim_scrolled), app_state.anim_text_view); gtk_box_append(GTK_BOX(anim_page), anim_scrolled); // Add dummy box to maintain layout height equivalent to input page GtkWidget *dummy_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); GtkWidget *dummy_lbl = gtk_label_new(""); gtk_widget_set_size_request(dummy_lbl, -1, 48); // Match button container height approximately gtk_box_append(GTK_BOX(dummy_box), dummy_lbl); gtk_box_append(GTK_BOX(anim_page), dummy_box); gtk_stack_add_named(GTK_STACK(app_state.stack), anim_page, "anim"); // ---------------------------------------------------- // PAGE 3: Consumed Page // ---------------------------------------------------- GtkWidget *consumed_page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 20); gtk_widget_set_valign(consumed_page, GTK_ALIGN_CENTER); gtk_widget_set_halign(consumed_page, GTK_ALIGN_CENTER); gtk_widget_set_vexpand(consumed_page, TRUE); gtk_widget_set_hexpand(consumed_page, TRUE); app_state.consumed_label = gtk_label_new("Consumed."); gtk_widget_add_css_class(app_state.consumed_label, "consumed-label"); gtk_box_append(GTK_BOX(consumed_page), app_state.consumed_label); GtkWidget *btn_reset = gtk_button_new_with_label("Return to the Void"); gtk_widget_add_css_class(btn_reset, "action-button"); gtk_widget_add_css_class(btn_reset, "btn-reset"); g_signal_connect(btn_reset, "clicked", G_CALLBACK(on_btn_reset_clicked), NULL); gtk_box_append(GTK_BOX(consumed_page), btn_reset); gtk_stack_add_named(GTK_STACK(app_state.stack), consumed_page, "consumed"); // Present window gtk_window_present(GTK_WINDOW(window)); } int run_gui(int argc, char* argv[], const char* initial_text) { srand(time(NULL)); GtkApplication *app = gtk_application_new("org.antigravity.ScreamIntoTheVoid", G_APPLICATION_DEFAULT_FLAGS); g_signal_connect(app, "activate", G_CALLBACK(activate), (gpointer)initial_text); // We construct a clean arguments list for GTK to avoid standard GTK warnings // about custom command line arguments. int dummy_argc = 1; char* dummy_argv[] = { argv[0], NULL }; int status = g_application_run(G_APPLICATION(app), dummy_argc, dummy_argv); g_object_unref(app); // Cleanup any lingering app state references if (app_state.timer_id != 0) { g_source_remove(app_state.timer_id); } if (app_state.original_text) { g_free(app_state.original_text); } if (app_state.animated_text) { g_free(app_state.animated_text); } return status; }