- Display a grid
- Ensure that the grid stretches when the window is resized.
- Show buttons that represent objects placed on the grid.
- Toggle the objects when the buttons are clicked.

OK, I'm happy with the result, glad to know this is possible!
I treated this as a vibe-coding experiment, based on some "can it be done" thinking. Sharing just in case anyone's interested in how one might achieve a similar effect.
I imagine this interactive-graphics pattern could be useful for any number of things.
For now, it is suitable for our purpose in briefing M via datalink.
Code: Select all
' COLOUR TACTICAL DISPLAY X-1
' A simple example of using GTK buttons to control interactive graphics
' in FreeBASIC.
' System target: Linux x64 (Kubuntu; Manjaro)
' Tested and working in FreeBASIC 1.10
'
#include once "gtk/gtk.bi"
' --- Globals ---
' These hold the canvas size and pointer
dim shared as integer draw_w = 0, draw_h = 0
dim shared as GtkWidget ptr canvas
' Flags to show/hide ships — all start hidden
dim shared as boolean show_bond = FALSE
dim shared as boolean show_fortress = FALSE
dim shared as boolean show_dauntless = FALSE
dim shared as boolean show_orkney = FALSE
' --- Toggle Callbacks ---
' These get called when you click a button
' They flip the visibility flag and request a redraw
sub toggle_bond cdecl(byval widget as GtkWidget ptr)
show_bond = not show_bond
gtk_widget_queue_draw(canvas) ' this tells GTK to redraw the canvas
end sub
sub toggle_fortress cdecl(byval widget as GtkWidget ptr)
show_fortress = not show_fortress
gtk_widget_queue_draw(canvas)
end sub
sub toggle_dauntless cdecl(byval widget as GtkWidget ptr)
show_dauntless = not show_dauntless
gtk_widget_queue_draw(canvas)
end sub
sub toggle_orkney cdecl(byval widget as GtkWidget ptr)
show_orkney = not show_orkney
gtk_widget_queue_draw(canvas)
end sub
' --- Resize Handler ---
' Keeps track of canvas size when the window resizes
function on_configure cdecl (byval widget as GtkWidget ptr, byval event as GdkEventConfigure ptr, byval user_data as gpointer) as gboolean
dim as GdkEventConfigure ptr cevent = cast(GdkEventConfigure ptr, event)
draw_w = cevent->width
draw_h = cevent->height
gtk_widget_queue_draw(widget)
return FALSE
end function
' --- Draw Handler ---
' This is where all the drawing magic happens
function on_draw cdecl (byval widget as GtkWidget ptr, byval event as GdkEventExpose ptr, byval user_data as gpointer) as gboolean
dim as GdkWindow ptr win = widget->window
dim as GdkGC ptr gc = gdk_gc_new(win) ' GC = graphics context
' Define some custom colors
dim as GdkColor darkblue = type(0, 0, 0, 20000)
dim as GdkColor blue = type(0, 20000, 20000, 65535)
dim as GdkColor gray = type(0, 40000, 40000, 40000)
dim as GdkColor red = type(0, 65535, 10000, 10000)
dim as GdkColor orkneyGreen = type(0, 10000, 20000, 10000)
dim as GdkColor bondYellow = type(0, 65535, 65535, 0)
' Paint the background
gdk_gc_set_rgb_fg_color(gc, @darkblue)
gdk_draw_rectangle(win, gc, TRUE, 0, 0, draw_w, draw_h)
' Draw a simple grid (10x10 cells)
gdk_gc_set_rgb_fg_color(gc, @blue)
for i as integer = 0 to 10
dim as integer x = i * draw_w \ 10
dim as integer y = i * draw_h \ 10
gdk_draw_line(win, gc, x, 0, x, draw_h) ' vertical
gdk_draw_line(win, gc, 0, y, draw_w, y) ' horizontal
next
' Ship symbol size (little ovals)
dim as integer sw = 30, sh = 15
' Draw each ship / symbol if its flag is set
if show_bond then
gdk_gc_set_rgb_fg_color(gc, @bondYellow)
gdk_draw_arc(win, gc, TRUE, draw_w\6 - sw\2, draw_h\2 - sh\2, sw, sh, 0, 360*64)
end if
if show_fortress then
gdk_gc_set_rgb_fg_color(gc, @red)
gdk_draw_arc(win, gc, TRUE, draw_w\2 - sw\2, draw_h\2 - sh\2, sw, sh, 0, 360*64)
end if
if show_dauntless then
gdk_gc_set_rgb_fg_color(gc, @gray)
gdk_draw_arc(win, gc, TRUE, (4*draw_w)\5 - sw\2, draw_h\4 - sh\2, sw, sh, 0, 360*64)
end if
if show_orkney then
gdk_gc_set_rgb_fg_color(gc, @orkneyGreen)
gdk_draw_arc(win, gc, TRUE, (4*draw_w)\5 - sw\2, (3*draw_h)\4 - sh\2, sw, sh, 0, 360*64)
end if
' Done drawing — clean up
g_object_unref(gc)
return FALSE
end function
' --- Setup ---
gtk_init(0, 0) ' Start up GTK
' Create main window
dim as GtkWidget ptr main_win = gtk_window_new(GTK_WINDOW_TOPLEVEL)
gtk_window_set_title(GTK_WINDOW(main_win), "COLOUR TACTICAL DISPLAY X-1")
gtk_window_set_default_size(GTK_WINDOW(main_win), 600, 400)
g_signal_connect(main_win, "destroy", G_CALLBACK(@gtk_main_quit), NULL)
' Vertical layout container
dim as GtkWidget ptr vbox = gtk_vbox_new(FALSE, 2)
gtk_container_add(GTK_CONTAINER(main_win), vbox)
' Create drawing canvas
canvas = gtk_drawing_area_new()
gtk_widget_set_size_request(canvas, 100, 100)
g_signal_connect(canvas, "expose-event", G_CALLBACK(@on_draw), NULL)
g_signal_connect(canvas, "configure-event", G_CALLBACK(@on_configure), NULL)
gtk_box_pack_start(GTK_BOX(vbox), canvas, TRUE, TRUE, 0)
' Create buttons horizontally
dim as GtkWidget ptr hbox = gtk_hbox_new(TRUE, 2)
dim as GtkWidget ptr btn
' Button: BOND (yellow blip)
btn = gtk_button_new_with_label("BOND - 007")
g_signal_connect(btn, "clicked", cast(GCallback, @toggle_bond), NULL)
gtk_box_pack_start(GTK_BOX(hbox), btn, TRUE, TRUE, 0)
' Button: Fortress (red blip)
btn = gtk_button_new_with_label("Atlantis Fortress")
g_signal_connect(btn, "clicked", cast(GCallback, @toggle_fortress), NULL)
gtk_box_pack_start(GTK_BOX(hbox), btn, TRUE, TRUE, 0)
' Button: Dauntless (gray blip)
btn = gtk_button_new_with_label("HMS Dauntless")
g_signal_connect(btn, "clicked", cast(GCallback, @toggle_dauntless), NULL)
gtk_box_pack_start(GTK_BOX(hbox), btn, TRUE, TRUE, 0)
' Button: Orkney (green blip)
btn = gtk_button_new_with_label("Orkney Isle")
g_signal_connect(btn, "clicked", cast(GCallback, @toggle_orkney), NULL)
gtk_box_pack_start(GTK_BOX(hbox), btn, TRUE, TRUE, 0)
' Add buttons below the canvas
gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 0)
' Show everything and go
gtk_widget_show_all(main_win)
gtk_main()
Eventually, Q apologized for being insistent, but persisted in raising an issue which it claimed is a bug in FB.
I don't know if it's really a bug (Edit: Nope! See below), or if it might be e.g. just some nuance of FB source-lore that Q is not aware of, but I asked Q to reformat the issue as a .bas file:
Code: Select all
'
' FreeBASIC GTK callback bug report: Illegal specification (Error 59)
' Demonstrates how `cdecl` with a second parameter of type `any ptr` fails to compile
'
' Platform: Linux x86_64
' Compiler: FreeBASIC 1.10.1 (likely others affected)
' GTK Version: GTK2 (or GTK3/4 if substituted)
'
' This file can be compiled to show the problem, and includes a workaround.
'
#include once "gtk/gtk.bi"
' ❌ This declaration SHOULD work, but causes:
' error 59: Illegal specification, at parameter 2 (data)
'
' Uncomment this block to reproduce the issue.
'
' sub broken_callback cdecl(byval widget as GtkWidget ptr, byval data as any ptr)
' print "Clicked"
' end sub
' ✅ This version WORKS — we omit the second parameter,
' then cast the function to GCallback anyway.
'
sub working_callback cdecl(byval widget as GtkWidget ptr)
print "Clicked"
end sub
' --- GTK setup ---
gtk_init(0, 0)
dim as GtkWidget ptr win = gtk_window_new(GTK_WINDOW_TOPLEVEL)
gtk_window_set_title(GTK_WINDOW(win), "Callback Bug Repro")
gtk_window_set_default_size(GTK_WINDOW(win), 300, 100)
dim as GtkWidget ptr btn = gtk_button_new_with_label("Click Me")
' ❌ This causes compiler error:
' g_signal_connect(btn, "clicked", G_CALLBACK(@broken_callback), NULL)
' ✅ This works using cast() and the 1-parameter workaround
g_signal_connect(btn, "clicked", cast(GCallback, @working_callback), NULL)
gtk_container_add(GTK_CONTAINER(win), btn)
g_signal_connect(win, "destroy", G_CALLBACK(@gtk_main_quit), NULL)
gtk_widget_show_all(win)
gtk_main()
'
' 📝 Notes:
' - G_CALLBACK expects a function with 2 parameters: (GtkWidget ptr, gpointer)
' - FreeBASIC fails to parse any declaration with `cdecl` and `any ptr` or `gpointer`
' - Casting the callback and ignoring the 2nd param is a workaround, but unsafe
'
' Suggested fix:
' Allow `cdecl sub(..., byval data as any ptr)` to parse correctly
'