Linux GTK Buttons + Graphics Combo Example

Post your FreeBASIC source, examples, tips and tricks here. Please don’t post code without including an explanation.
Post Reply
misterc
Posts: 22
Joined: Sep 04, 2018 18:41

Linux GTK Buttons + Graphics Combo Example

Post by misterc »

I was curious if this was possible: Can FreeBASIC make a dynamic graphics display controlled by GTK buttons?
  • 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.
Image

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()

The vibe-coding was very tricky with this one! I had to ask Q to meta-reflect and offer workarounds many times.

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
'

I tested the reported code, and can confirm that at least its claims as to "works" and "doesn't work" seem valid. Edit: Thanks SARG for pointing out it's just a reserved word issue, don't use "data" in there.
Last edited by misterc on May 13, 2025 18:34, edited 1 time in total.
caseih
Posts: 2199
Joined: Feb 26, 2007 5:32

Re: Linux GTK Buttons + Graphics Combo Example

Post by caseih »

EDIT: deleted post. Totally missed that "data" is a keyword! my bad.
Last edited by caseih on May 13, 2025 14:09, edited 1 time in total.
SARG
Posts: 1888
Joined: May 27, 2005 7:15
Location: FRANCE

Re: Linux GTK Buttons + Graphics Combo Example

Post by SARG »

Instead data try data1 (or any other name)

Code: Select all

 sub broken_callback cdecl(byval widget as GtkWidget ptr, byval data as any ptr)
     print "Clicked"
 end sub
misterc
Posts: 22
Joined: Sep 04, 2018 18:41

Re: Linux GTK Buttons + Graphics Combo Example

Post by misterc »

SARG wrote: May 13, 2025 7:24 Instead data try data1 (or any other name)

Code: Select all

 sub broken_callback cdecl(byval widget as GtkWidget ptr, byval data as any ptr)
     print "Clicked"
 end sub
Oh, of course!

4o had been struggling with reserved words all day...and I think I was blinded by the doc-style-overlay-effect, a common human hallucination (at least for me sometimes). :?

Thank you, will be factoring this into the vibe in the future for sure.
Post Reply