|
|
@@ -0,0 +1,315 @@
|
|
|
+#include <gtk-3.0/gtk/gtk.h>
|
|
|
+#include <stdbool.h>
|
|
|
+#include <math.h>
|
|
|
+#include <stdio.h>
|
|
|
+
|
|
|
+#define NUM_TRACKS 8
|
|
|
+#define NUM_SAMPLES 60
|
|
|
+#define TRACK_THICKNESS 50.0
|
|
|
+#define DRAW_THICKNESS 3.0
|
|
|
+#define SAMPLE_THRESHOLD (TRACK_THICKNESS * 0.75)
|
|
|
+#define TEST_TIME_LIMIT 150
|
|
|
+#define EXIT_DELAY_MS 2000
|
|
|
+
|
|
|
+typedef struct {
|
|
|
+ double x, y;
|
|
|
+ bool active;
|
|
|
+} SamplePoint;
|
|
|
+
|
|
|
+typedef struct {
|
|
|
+ SamplePoint samples[NUM_SAMPLES];
|
|
|
+ bool completed;
|
|
|
+ double x1, y1, x2, y2;
|
|
|
+} Track;
|
|
|
+
|
|
|
+static cairo_surface_t *surface = NULL;
|
|
|
+static double last_x = -1.0;
|
|
|
+static double last_y = -1.0;
|
|
|
+static bool is_drawing = false;
|
|
|
+static Track tracks[NUM_TRACKS];
|
|
|
+
|
|
|
+static bool test_passed = false;
|
|
|
+static bool test_failed = false;
|
|
|
+static int time_remaining = TEST_TIME_LIMIT;
|
|
|
+static guint timer_id = 0;
|
|
|
+
|
|
|
+static void redraw_all(GtkWidget *widget);
|
|
|
+
|
|
|
+static gboolean auto_exit_callback(gpointer data) {
|
|
|
+ gtk_main_quit();
|
|
|
+ return FALSE;
|
|
|
+}
|
|
|
+
|
|
|
+static void handle_test_end(GtkWidget *widget, bool passed) {
|
|
|
+ if (passed) { test_passed = true; printf("PASS\n"); }
|
|
|
+ else { test_failed = true; printf("FAIL\n"); }
|
|
|
+ fflush(stdout);
|
|
|
+ if (timer_id > 0) { g_source_remove(timer_id); timer_id = 0; }
|
|
|
+ g_timeout_add(EXIT_DELAY_MS, auto_exit_callback, NULL);
|
|
|
+ redraw_all(widget);
|
|
|
+}
|
|
|
+
|
|
|
+static void init_tracks(GtkWidget *widget) {
|
|
|
+ int w = gtk_widget_get_allocated_width(widget);
|
|
|
+ int h = gtk_widget_get_allocated_height(widget);
|
|
|
+ if (w <= 0 || h <= 0) return;
|
|
|
+ double dw = (double)w, dh = (double)h;
|
|
|
+
|
|
|
+ // 加宽顶部、底部、左侧和右侧的待画线区域(通过调整坐标)
|
|
|
+ // 顶部和底部调整 y,左侧和右侧调整 x
|
|
|
+ double offset = 25.0; // 向内平移量(厚度一半)
|
|
|
+
|
|
|
+ double top_y = offset;
|
|
|
+ double bottom_y = dh - offset;
|
|
|
+ double left_x = offset;
|
|
|
+ double right_x = dw - offset;
|
|
|
+
|
|
|
+ double coords[NUM_TRACKS][4] = {
|
|
|
+ {0.0, top_y, dw, top_y}, // 顶部横线
|
|
|
+ {0.0, bottom_y, dw, bottom_y}, // 底部横线
|
|
|
+ {left_x, 0.0, left_x, dh}, // 左侧竖线(原为 0.0)
|
|
|
+ {right_x, 0.0, right_x, dh}, // 右侧竖线(原为 dw)
|
|
|
+ {0.0, dh/2.0, dw, dh/2.0}, // 中间横线
|
|
|
+ {dw/2.0, 0.0, dw/2.0, dh}, // 中间竖线
|
|
|
+ {0.0, 0.0, dw, dh}, // 对角线 1
|
|
|
+ {dw, 0.0, 0.0, dh} // 对角线 2
|
|
|
+ };
|
|
|
+ for (int t = 0; t < NUM_TRACKS; t++) {
|
|
|
+ tracks[t].x1 = coords[t][0]; tracks[t].y1 = coords[t][1];
|
|
|
+ tracks[t].x2 = coords[t][2]; tracks[t].y2 = coords[t][3];
|
|
|
+ tracks[t].completed = false;
|
|
|
+ for (int i = 0; i < NUM_SAMPLES; i++) {
|
|
|
+ double ratio = (double)i / (NUM_SAMPLES - 1);
|
|
|
+ tracks[t].samples[i].x = tracks[t].x1 + (tracks[t].x2 - tracks[t].x1) * ratio;
|
|
|
+ tracks[t].samples[i].y = tracks[t].y1 + (tracks[t].y2 - tracks[t].y1) * ratio;
|
|
|
+ tracks[t].samples[i].active = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ test_passed = false; test_failed = false; time_remaining = TEST_TIME_LIMIT;
|
|
|
+}
|
|
|
+
|
|
|
+static gboolean timer_callback(gpointer data) {
|
|
|
+ if (test_passed || test_failed) return FALSE;
|
|
|
+ time_remaining--;
|
|
|
+ if (time_remaining <= 0) { handle_test_end(GTK_WIDGET(data), false); return FALSE; }
|
|
|
+ redraw_all(GTK_WIDGET(data));
|
|
|
+ return TRUE;
|
|
|
+}
|
|
|
+
|
|
|
+static void draw_ui_elements(cairo_t *cr, int w, int h) {
|
|
|
+ cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);
|
|
|
+ for (int t = 0; t < NUM_TRACKS; t++) {
|
|
|
+ cairo_set_line_width(cr, TRACK_THICKNESS);
|
|
|
+ if (tracks[t].completed) cairo_set_source_rgba(cr, 0.0, 1.0, 0.0, 0.2);
|
|
|
+ else cairo_set_source_rgba(cr, 0.8, 0.8, 0.8, 0.3);
|
|
|
+ cairo_move_to(cr, tracks[t].x1, tracks[t].y1);
|
|
|
+ cairo_line_to(cr, tracks[t].x2, tracks[t].y2);
|
|
|
+ cairo_stroke(cr);
|
|
|
+ }
|
|
|
+ cairo_select_font_face(cr, "Sans", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_BOLD);
|
|
|
+ cairo_set_font_size(cr, 30.0);
|
|
|
+ char time_buf[32]; snprintf(time_buf, sizeof(time_buf), "Time: %ds", time_remaining);
|
|
|
+ cairo_set_source_rgb(cr, time_remaining < 10 ? 1.0 : 0.0, 0, 0);
|
|
|
+ cairo_move_to(cr, w - 180, 60);
|
|
|
+ cairo_show_text(cr, time_buf);
|
|
|
+ if (test_passed) {
|
|
|
+ cairo_set_source_rgb(cr, 0, 0.6, 0); cairo_move_to(cr, 50, 60); cairo_show_text(cr, "PASS: Closing...");
|
|
|
+ } else if (test_failed) {
|
|
|
+ cairo_set_source_rgb(cr, 0.8, 0, 0); cairo_move_to(cr, 50, 60); cairo_show_text(cr, "FAIL: Time Out!");
|
|
|
+ } else {
|
|
|
+ cairo_set_source_rgb(cr, 0.2, 0.2, 0.2); cairo_move_to(cr, 50, 60);
|
|
|
+ int done = 0; for(int i=0; i<NUM_TRACKS; i++) if(tracks[i].completed) done++;
|
|
|
+ char buf[64]; snprintf(buf, sizeof(buf), "Progress: %d/%d", done, NUM_TRACKS);
|
|
|
+ cairo_show_text(cr, buf);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+static void redraw_all(GtkWidget *widget) {
|
|
|
+ if (!surface) return;
|
|
|
+ gtk_widget_queue_draw(widget);
|
|
|
+}
|
|
|
+
|
|
|
+static void check_touch_logic_interp(GtkWidget *widget, double x1, double y1, double x2, double y2) {
|
|
|
+ if (test_passed || test_failed) return;
|
|
|
+ bool state_changed = false;
|
|
|
+ double dist = sqrt(pow(x2-x1, 2) + pow(y2-y1, 2));
|
|
|
+ int steps = (dist < 1.0) ? 1 : (int)(dist / 2.0) + 1;
|
|
|
+ if (steps > 50) steps = 50;
|
|
|
+
|
|
|
+ for (int step = 0; step <= steps; step++) {
|
|
|
+ double t_ratio = (double)step / steps;
|
|
|
+ double cur_x = x1 + (x2 - x1) * t_ratio;
|
|
|
+ double cur_y = y1 + (y2 - y1) * t_ratio;
|
|
|
+ for (int t = 0; t < NUM_TRACKS; t++) {
|
|
|
+ if (tracks[t].completed) continue;
|
|
|
+ int active_count = 0;
|
|
|
+ for (int s = 0; s < NUM_SAMPLES; s++) {
|
|
|
+ if (!tracks[t].samples[s].active) {
|
|
|
+ double dx = cur_x - tracks[t].samples[s].x, dy = cur_y - tracks[t].samples[s].y;
|
|
|
+ if (sqrt(dx*dx + dy*dy) < SAMPLE_THRESHOLD) tracks[t].samples[s].active = true;
|
|
|
+ }
|
|
|
+ if (tracks[t].samples[s].active) active_count++;
|
|
|
+ }
|
|
|
+ if (active_count == NUM_SAMPLES) { tracks[t].completed = true; state_changed = true; }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (state_changed) {
|
|
|
+ bool all_done = true; for(int i=0; i<NUM_TRACKS; i++) if(!tracks[i].completed) all_done = false;
|
|
|
+ if (all_done) handle_test_end(widget, true);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+static void draw_line_to_surface(GtkWidget *widget, double x1, double y1, double x2, double y2) {
|
|
|
+ if (!surface || test_passed || test_failed) return;
|
|
|
+ cairo_t *cr = cairo_create(surface);
|
|
|
+ cairo_set_source_rgb(cr, 0.1, 0.3, 0.9);
|
|
|
+ cairo_set_line_width(cr, DRAW_THICKNESS);
|
|
|
+ cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);
|
|
|
+ cairo_set_line_join(cr, CAIRO_LINE_JOIN_ROUND);
|
|
|
+
|
|
|
+ cairo_move_to(cr, x1, y1);
|
|
|
+ cairo_line_to(cr, x2, y2);
|
|
|
+ cairo_stroke(cr);
|
|
|
+ cairo_destroy(cr);
|
|
|
+
|
|
|
+ gtk_widget_queue_draw(widget);
|
|
|
+ check_touch_logic_interp(widget, x1, y1, x2, y2);
|
|
|
+}
|
|
|
+
|
|
|
+static gboolean draw_cb(GtkWidget *widget, cairo_t *cr, gpointer data) {
|
|
|
+ int w = gtk_widget_get_allocated_width(widget);
|
|
|
+ int h = gtk_widget_get_allocated_height(widget);
|
|
|
+ cairo_set_source_rgb(cr, 1, 1, 1);
|
|
|
+ cairo_paint(cr);
|
|
|
+ draw_ui_elements(cr, w, h);
|
|
|
+ if (surface) {
|
|
|
+ cairo_set_source_surface(cr, surface, 0, 0);
|
|
|
+ cairo_paint(cr);
|
|
|
+ }
|
|
|
+ return FALSE;
|
|
|
+}
|
|
|
+
|
|
|
+static gboolean configure_event_cb(GtkWidget *widget, GdkEventConfigure *event, gpointer data) {
|
|
|
+ if (surface) cairo_surface_destroy(surface);
|
|
|
+ surface = gdk_window_create_similar_surface(gtk_widget_get_window(widget), CAIRO_CONTENT_COLOR_ALPHA,
|
|
|
+ gtk_widget_get_allocated_width(widget),
|
|
|
+ gtk_widget_get_allocated_height(widget));
|
|
|
+ cairo_t *cr = cairo_create(surface);
|
|
|
+ cairo_set_source_rgba(cr, 0, 0, 0, 0);
|
|
|
+ cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE);
|
|
|
+ cairo_paint(cr);
|
|
|
+ cairo_destroy(cr);
|
|
|
+
|
|
|
+ init_tracks(widget);
|
|
|
+ if (timer_id > 0) g_source_remove(timer_id);
|
|
|
+ timer_id = g_timeout_add_seconds(1, timer_callback, widget);
|
|
|
+ return TRUE;
|
|
|
+}
|
|
|
+
|
|
|
+static gboolean button_press_event_cb(GtkWidget *widget, GdkEventButton *event, gpointer data) {
|
|
|
+ if (test_passed || test_failed) return FALSE;
|
|
|
+ if (event->button == GDK_BUTTON_PRIMARY) {
|
|
|
+ is_drawing = true;
|
|
|
+ last_x = event->x; last_y = event->y;
|
|
|
+ draw_line_to_surface(widget, last_x, last_y, event->x, event->y);
|
|
|
+ } else if (event->button == GDK_BUTTON_SECONDARY) {
|
|
|
+ cairo_t *cr = cairo_create(surface);
|
|
|
+ cairo_set_source_rgba(cr, 0, 0, 0, 0);
|
|
|
+ cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE);
|
|
|
+ cairo_paint(cr);
|
|
|
+ cairo_destroy(cr);
|
|
|
+ init_tracks(widget);
|
|
|
+ gtk_widget_queue_draw(widget);
|
|
|
+ }
|
|
|
+ return TRUE;
|
|
|
+}
|
|
|
+
|
|
|
+static gboolean motion_notify_event_cb(GtkWidget *widget, GdkEventMotion *event, gpointer data) {
|
|
|
+ if (test_passed || test_failed || !is_drawing) return FALSE;
|
|
|
+ draw_line_to_surface(widget, last_x, last_y, event->x, event->y);
|
|
|
+ last_x = event->x; last_y = event->y;
|
|
|
+ return TRUE;
|
|
|
+}
|
|
|
+
|
|
|
+static gboolean button_release_event_cb(GtkWidget *widget, GdkEventButton *event, gpointer data) {
|
|
|
+ if (event->button == GDK_BUTTON_PRIMARY) {
|
|
|
+ if (is_drawing) draw_line_to_surface(widget, last_x, last_y, event->x, event->y);
|
|
|
+ is_drawing = false; last_x = -1.0; last_y = -1.0;
|
|
|
+ }
|
|
|
+ return TRUE;
|
|
|
+}
|
|
|
+
|
|
|
+static gboolean window_state_event_cb(GtkWidget *widget, GdkEventWindowState *event, gpointer data) {
|
|
|
+ if (!(event->new_window_state & GDK_WINDOW_STATE_FULLSCREEN)) {
|
|
|
+ gtk_window_fullscreen(GTK_WINDOW(widget));
|
|
|
+ }
|
|
|
+ if (event->new_window_state & GDK_WINDOW_STATE_ICONIFIED) {
|
|
|
+ gtk_window_deiconify(GTK_WINDOW(widget));
|
|
|
+ gtk_window_fullscreen(GTK_WINDOW(widget));
|
|
|
+ }
|
|
|
+ return FALSE;
|
|
|
+}
|
|
|
+
|
|
|
+static gboolean window_focus_out_event_cb(GtkWidget *widget, GdkEventFocus *event, gpointer data) {
|
|
|
+ if (!test_passed && !test_failed) {
|
|
|
+ gtk_window_present(GTK_WINDOW(widget));
|
|
|
+ gtk_window_fullscreen(GTK_WINDOW(widget));
|
|
|
+ }
|
|
|
+ return FALSE;
|
|
|
+}
|
|
|
+
|
|
|
+static gboolean force_fullscreen_timer(gpointer data) {
|
|
|
+ GtkWindow *window = GTK_WINDOW(data);
|
|
|
+ if (!test_passed && !test_failed) {
|
|
|
+ gtk_window_fullscreen(window);
|
|
|
+ gtk_window_set_keep_above(window, TRUE);
|
|
|
+ }
|
|
|
+ return TRUE;
|
|
|
+}
|
|
|
+
|
|
|
+int main(int argc, char *argv[]) {
|
|
|
+ gtk_init(&argc, &argv);
|
|
|
+ GtkWidget *window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
|
|
|
+
|
|
|
+ GdkScreen *screen = gdk_screen_get_default();
|
|
|
+ gint screen_w = gdk_screen_get_width(screen);
|
|
|
+ gint screen_h = gdk_screen_get_height(screen);
|
|
|
+
|
|
|
+ GdkGeometry geometry;
|
|
|
+ geometry.min_width = screen_w;
|
|
|
+ geometry.min_height = screen_h;
|
|
|
+ geometry.max_width = screen_w;
|
|
|
+ geometry.max_height = screen_h;
|
|
|
+ gtk_window_set_geometry_hints(GTK_WINDOW(window), NULL, &geometry, (GdkWindowHints)(GDK_HINT_MIN_SIZE | GDK_HINT_MAX_SIZE));
|
|
|
+
|
|
|
+ gtk_window_set_keep_above(GTK_WINDOW(window), TRUE);
|
|
|
+ gtk_window_set_resizable(GTK_WINDOW(window), FALSE);
|
|
|
+ gtk_window_set_decorated(GTK_WINDOW(window), FALSE);
|
|
|
+
|
|
|
+ gtk_window_set_type_hint(GTK_WINDOW(window), GDK_WINDOW_TYPE_HINT_DIALOG);
|
|
|
+
|
|
|
+ g_signal_connect(window, "destroy", G_CALLBACK(gtk_main_quit), NULL);
|
|
|
+ g_signal_connect(window, "window-state-event", G_CALLBACK(window_state_event_cb), NULL);
|
|
|
+ g_signal_connect(window, "focus-out-event", G_CALLBACK(window_focus_out_event_cb), NULL);
|
|
|
+
|
|
|
+ GtkWidget *da = gtk_drawing_area_new();
|
|
|
+ gtk_container_add(GTK_CONTAINER(window), da);
|
|
|
+ g_signal_connect(da, "draw", G_CALLBACK(draw_cb), NULL);
|
|
|
+ g_signal_connect(da, "configure-event", G_CALLBACK(configure_event_cb), NULL);
|
|
|
+ g_signal_connect(da, "motion-notify-event", G_CALLBACK(motion_notify_event_cb), NULL);
|
|
|
+ g_signal_connect(da, "button-press-event", G_CALLBACK(button_press_event_cb), NULL);
|
|
|
+ g_signal_connect(da, "button-release-event", G_CALLBACK(button_release_event_cb), NULL);
|
|
|
+
|
|
|
+ gtk_widget_set_events(da, GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_POINTER_MOTION_MASK);
|
|
|
+
|
|
|
+ gtk_window_fullscreen(GTK_WINDOW(window));
|
|
|
+ gtk_widget_show_all(window);
|
|
|
+
|
|
|
+ gtk_window_present(GTK_WINDOW(window));
|
|
|
+ gtk_window_fullscreen(GTK_WINDOW(window));
|
|
|
+
|
|
|
+ g_timeout_add(300, force_fullscreen_timer, window);
|
|
|
+
|
|
|
+ gtk_main();
|
|
|
+ return 0;
|
|
|
+}
|