Perl 5 GUI Development · FreeBSD 16 via Lima

Beyond Perl/Tk

Four alternative GUI toolkits — each with complete working examples.

Wx — wxPerl Gtk3 Prima IUP FreeBSD 16 · Lima · X11

Toolkit Overview & Comparison

Perl/Tk is not the only game in town. Four mature GUI toolkits offer excellent Perl 5 bindings, each with a distinct philosophy:

wxPerl
use Wx
Native widgets via wxWidgets. Closest to Tk in feel. Best Windows + Linux cross-compat.
Gtk3
use Gtk3
GTK3 binding. Natural on FreeBSD/GNOME/XFCE. CSS theming. Signal/slot model.
Prima
use Prima
Written entirely in Perl+C. Its own rendering engine. No external toolkit dependency.
IUP
use IUP
Tiny, fast, attribute-driven API. PUC-Rio origin. Unique layout engine.

Feature Comparison

FeatureWx (wxPerl)Gtk3PrimaIUP
FreeBSD pkgp5-Wxp5-Gtk3p5-Primacpanm IUP
ArchitecturewxWidgets C++ bindingGObject introspectionPerl + C engineC library binding
LayoutSizers (powerful)Box/Grid containersAbsolute + LayoutAttribute-driven
ThemingSystem nativeCSS stylesheetsBuilt-in skinsMinimal, consistent
Learning curveModerateModerateSteep (inverted Y)Easy
DocumentationExcellentGoodFairGood
OOP styleSubclass framesFunctional/OOPProperty-based OOPFunctional/OOP
Event modelEVT_* macrossignal_connectonEvent callbackscallbacks
Best forDesktop apps, menusLinux/BSD appsCustom graphicsQuick utilities

Installing on FreeBSD 16 via Lima

All four toolkits are available in FreeBSD's ports/packages. Run these commands inside the Lima VM after connecting with ssh -X.

1
wxPerl
bash
pkg install p5-Wx
# Verify:
perl -e 'use Wx; print "wxWidgets ", $Wx::wx_version, "\n"'
2
Gtk3
bash
pkg install p5-Gtk3 p5-Glib
# Verify:
perl -e 'use Gtk3; print "Gtk3 OK\n"'
3
Prima
bash
pkg install p5-Prima
# or build from ports:
cd /usr/ports/x11-toolkits/p5-Prima && make install clean
# Verify:
perl -e 'use Prima; print Prima::Application->new->version, "\n"'
4
IUP
bash
pkg install iup
cpanm IUP
# Verify:
perl -e 'use IUP; print "IUP OK\n"'
Tip: Run echo $DISPLAY before any GUI program. If empty, reconnect with ssh -X -p $PORT -i ~/.lima/_config/user 127.0.0.1. Windows appear in XQuartz on your macOS host.
wxPerl · Wx

Core Concepts

wxPerl wraps the wxWidgets C++ library. The three key ideas to understand are the App/Frame pattern, the Panel+Sizer layout system, and the EVT_* event macros.

The App / Frame Pattern

Every wxPerl program has two mandatory classes: a Wx::App subclass that owns the event loop, and at least one Wx::Frame subclass for each window.

perl — structure
package MyApp;
use parent -norequire, 'Wx::App';
sub OnInit {
    my ($self) = @_;
    my $frame = MyFrame->new;      # Create the main window
    $self->SetTopWindow($frame);   # Tell app which window is "main"
    $frame->Show(1);               # Make it visible
    return 1;                      # MUST return true
}

package MyFrame;
use parent -norequire, 'Wx::Frame';
sub new {
    my ($class) = @_;
    my $self = $class->SUPER::new(
        undef,                    # No parent window
        wxID_ANY,                 # Auto-assign ID
        "My Application",         # Title bar text
        wxDefaultPosition,        # OS decides where to place it
        Wx::Size->new(700, 500),  # Width x Height in pixels
        wxDEFAULT_FRAME_STYLE,    # Standard minimize/maximize/close
    );
    # ... add widgets here ...
    return $self;
}

package main;
MyApp->new->MainLoop;  # Create app and enter event loop

Panels and Sizers

Widgets should always be placed on a Wx::Panel (not directly on the frame). Layout is controlled by Sizers — objects that position and resize widgets automatically when the window changes size.

Wx::BoxSizer

Lays out widgets in a row (H) or column (V). The most-used sizer.

Wx::StaticBoxSizer

Like BoxSizer but draws a labeled border box around the group.

Wx::GridSizer

Equal-sized grid cells. Good for button grids.

Wx::FlexGridSizer

Grid where rows/columns can have different sizes.

perl — sizer example
my $panel = Wx::Panel->new($frame, wxID_ANY);
my $sizer = Wx::BoxSizer->new(wxVERTICAL);  # Vertical column

# $sizer->Add($widget, $proportion, $flags, $border)
#   $proportion: 0 = fixed size,  1+ = stretches proportionally
#   $flags:      wxEXPAND, wxALL, wxALIGN_CENTER, etc.
#   $border:     pixels of padding around the widget

my $label  = Wx::StaticText->new($panel, wxID_ANY, "Name:");
my $entry  = Wx::TextCtrl->new($panel, wxID_ANY, "");
my $button = Wx::Button->new($panel, wxID_ANY, "OK");

$sizer->Add($label,  0, wxALL,              6);   # Fixed, 6px pad
$sizer->Add($entry,  0, wxEXPAND | wxALL,   6);   # Full width, 6px pad
$sizer->Add($button, 0, wxALIGN_RIGHT | wxALL, 6); # Right-aligned

$panel->SetSizer($sizer);
$sizer->SetSizeHints($frame);   # Resize frame to fit contents

Event Binding with EVT_* Macros

perl — events
use Wx::Event qw(EVT_BUTTON EVT_MENU EVT_TEXT EVT_CLOSE EVT_SIZE);

# EVT_BUTTON($handler_object, $button_widget, \&callback_sub)
EVT_BUTTON($frame, $ok_btn, \&on_ok);
EVT_BUTTON($frame, $cancel_btn, sub {
    my ($frame_self, $event) = @_;   # First arg is always the handler object
    $frame_self->Close;
});

# Menu events use the menu item ID
EVT_MENU($frame, wxID_EXIT,  sub { $_[0]->Close(1) });
EVT_MENU($frame, wxID_ABOUT, \&on_about);

# Named callback receives ($self, $event)
sub on_ok {
    my ($self, $event) = @_;
    Wx::MessageBox("OK clicked!", "Info", wxOK | wxICON_INFORMATION, $self);
}

Key Constants

CategoryConstants
OrientationwxHORIZONTAL wxVERTICAL
Sizer flagswxEXPAND wxALL wxTOP wxBOTTOM wxLEFT wxRIGHT wxALIGN_CENTER wxALIGN_RIGHT
IDswxID_ANY wxID_OK wxID_CANCEL wxID_EXIT wxID_ABOUT wxID_NEW wxID_OPEN wxID_SAVE
Dialog buttonswxOK wxCANCEL wxYES wxNO wxYES_NO wxOK_DEFAULT
Dialog iconswxICON_INFORMATION wxICON_WARNING wxICON_ERROR wxICON_QUESTION
Text stylewxTE_MULTILINE wxTE_READONLY wxTE_RICH2 wxTE_PROCESS_ENTER
FontwxFONTFAMILY_DEFAULT wxFONTFAMILY_MODERN wxFONTSTYLE_NORMAL wxFONTWEIGHT_BOLD
wxPerl · Hello World

Hello World

The minimal wxPerl program: an App class, a Frame with a Panel, a label, and a button.

perlwx_hello.pl
#!/usr/local/bin/perl
# =============================================================================
#  wx_hello.pl  —  Minimal wxPerl window
#  Install:  pkg install p5-Wx
#  Run:      perl wx_hello.pl
# =============================================================================
use strict;
use warnings;
use Wx qw(:everything);
use Wx::Event qw(EVT_BUTTON);

# ── Application: owns the event loop ──────────────────────────────────────────
package HelloApp;
use parent -norequire, 'Wx::App';

sub OnInit {
    my ($self) = @_;
    my $frame = HelloFrame->new;
    $self->SetTopWindow($frame);
    $frame->Show(1);
    return 1;   # Must return true or app exits immediately
}

# ── Main window ───────────────────────────────────────────────────────────────
package HelloFrame;
use parent -norequire, 'Wx::Frame';
use Wx qw(:everything);
use Wx::Event qw(EVT_BUTTON);

sub new {
    my ($class) = @_;
    my $self = $class->SUPER::new(
        undef, wxID_ANY,
        'Hello from wxPerl!',       # Window title
        wxDefaultPosition,
        Wx::Size->new(400, 200),    # Width, Height
    );

    # Panel is the correct container for widgets in a Frame
    my $panel = Wx::Panel->new($self, wxID_ANY);
    my $sizer = Wx::BoxSizer->new(wxVERTICAL);

    # Label with large bold font
    my $lbl = Wx::StaticText->new(
        $panel, wxID_ANY,
        'Hello, wxPerl!',
        wxDefaultPosition, wxDefaultSize,
        wxALIGN_CENTRE_HORIZONTAL,
    );
    $lbl->SetFont(Wx::Font->new(
        18,                      # Point size
        wxFONTFAMILY_DEFAULT,    # Family
        wxFONTSTYLE_NORMAL,      # Style
        wxFONTWEIGHT_BOLD,       # Weight
    ));
    $sizer->Add($lbl, 0, wxALL | wxALIGN_CENTER_HORIZONTAL, 24);

    # Button
    my $btn = Wx::Button->new($panel, wxID_ANY, 'Greet Me');
    $sizer->Add($btn, 0, wxBOTTOM | wxALIGN_CENTER_HORIZONTAL, 20);

    $panel->SetSizer($sizer);
    $sizer->SetSizeHints($self);   # Resize frame to fit sizer

    # Bind button click — EVT_BUTTON($handler, $button, $callback)
    EVT_BUTTON($self, $btn, sub {
        Wx::MessageBox(
            'Greetings from wxPerl on FreeBSD 16!',
            'Hello',
            wxOK | wxICON_INFORMATION,
            $self,
        );
    });

    return $self;
}

# ── Entry point ───────────────────────────────────────────────────────────────
package main;
HelloApp->new->MainLoop;
wxPerl · Full Application

wxPerl Greeting Generator

A complete application demonstrating menus, StaticBoxSizer grouped inputs, a Wx::Choice dropdown, a multiline read-only output area, a status bar with two fields, and a confirm-on-close dialog.

perlwx_greeting.pl
#!/usr/local/bin/perl
# =============================================================================
#  wx_greeting.pl  —  wxPerl complete application demo
#  Demonstrates: menus, StaticBoxSizer, Choice, TextCtrl (multi), status bar,
#                EVT_TEXT_ENTER, close intercept, MessageDialog
#  Install:  pkg install p5-Wx
#  Run:      perl wx_greeting.pl
# =============================================================================
use strict;
use warnings;
use Wx qw(:everything);
use Wx::Event qw(EVT_BUTTON EVT_CLOSE EVT_MENU EVT_TEXT_ENTER);

# ══════════════════════════════════════════════════════════════════════
# Application class
# ══════════════════════════════════════════════════════════════════════
package GreetApp;
use parent -norequire, 'Wx::App';

sub OnInit {
    my ($self) = @_;
    my $frame = GreetFrame->new;
    $self->SetTopWindow($frame);
    $frame->Show(1);
    return 1;
}

# ══════════════════════════════════════════════════════════════════════
# Main frame
# ══════════════════════════════════════════════════════════════════════
package GreetFrame;
use parent -norequire, 'Wx::Frame';
use Wx qw(:everything);
use Wx::Event qw(EVT_BUTTON EVT_CLOSE EVT_MENU EVT_TEXT_ENTER);

sub new {
    my ($class) = @_;
    my $self = $class->SUPER::new(
        undef, wxID_ANY,
        'wxPerl Greeting Generator',
        wxDefaultPosition,
        Wx::Size->new(640, 520),
        wxDEFAULT_FRAME_STYLE,
    );

    $self->{count} = 0;           # State: number of greetings generated
    $self->_build_menubar;
    $self->_build_body;
    $self->_build_statusbar;
    EVT_CLOSE($self, \&on_close);
    return $self;
}

# ── Menu bar ──────────────────────────────────────────────────────────
sub _build_menubar {
    my ($self) = @_;
    my $bar = Wx::MenuBar->new;

    my $file = Wx::Menu->new;
    $file->Append(wxID_NEW,  "New\tCtrl+N");
    $file->AppendSeparator;
    $file->Append(wxID_EXIT, "Quit\tCtrl+Q");

    my $help = Wx::Menu->new;
    $help->Append(wxID_ABOUT, "About…");

    $bar->Append($file, "&File");
    $bar->Append($help, "&Help");
    $self->SetMenuBar($bar);

    EVT_MENU($self, wxID_NEW,   \&on_new);
    EVT_MENU($self, wxID_EXIT,  sub { $_[0]->Close(1) });
    EVT_MENU($self, wxID_ABOUT, \&on_about);
}

# ── Main UI ───────────────────────────────────────────────────────────
sub _build_body {
    my ($self) = @_;
    my $panel = Wx::Panel->new($self, wxID_ANY);
    my $root  = Wx::BoxSizer->new(wxVERTICAL);

    # ── Title label ──────────────────────────────────────────────────
    my $title = Wx::StaticText->new($panel, wxID_ANY, "wxPerl Greeting Generator");
    $title->SetFont(Wx::Font->new(
        16, wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD
    ));
    $root->Add($title, 0, wxALL | wxALIGN_CENTER_HORIZONTAL, 12);

    # ── Input group — StaticBoxSizer creates a labeled bordered region ──
    my $in_sizer = Wx::StaticBoxSizer->new(
        Wx::StaticBox->new($panel, wxID_ANY, "Your Details"),
        wxVERTICAL,
    );

    # Helper: build a label+control row and add it to $in_sizer
    my $add_row = sub {
        my ($label, $ctrl) = @_;
        my $row = Wx::BoxSizer->new(wxHORIZONTAL);
        my $lbl = Wx::StaticText->new($panel, wxID_ANY, $label);
        $row->Add($lbl,  0, wxRIGHT | wxALIGN_CENTER_VERTICAL, 8);
        $row->Add($ctrl, 1, wxEXPAND);
        $in_sizer->Add($row, 0, wxEXPAND | wxALL, 6);
    };

    # Name entry — process Enter key to trigger generation
    my $name_ctrl = Wx::TextCtrl->new(
        $panel, wxID_ANY, "",
        wxDefaultPosition, Wx::Size->new(260, -1),
        wxTE_PROCESS_ENTER,          # Enable EVT_TEXT_ENTER
    );
    $add_row->("Name:     ", $name_ctrl);

    # Greeting selector dropdown
    my $choice = Wx::Choice->new(
        $panel, wxID_ANY,
        wxDefaultPosition, wxDefaultSize,
        ["Hello", "Hi there", "Greetings", "Good day", "Howdy", "Salutations"],
    );
    $choice->SetSelection(0);
    $add_row->("Greeting: ", $choice);

    $root->Add($in_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, 10);

    # ── Button row ────────────────────────────────────────────────────
    my $btn_row  = Wx::BoxSizer->new(wxHORIZONTAL);
    my $gen_btn  = Wx::Button->new($panel, wxID_ANY, "Generate ▶");
    my $clr_btn  = Wx::Button->new($panel, wxID_ANY, "Clear");
    $gen_btn->SetDefault;   # Pressing Enter anywhere triggers this
    $btn_row->Add($gen_btn, 0, wxRIGHT, 8);
    $btn_row->Add($clr_btn, 0);
    $root->Add($btn_row, 0, wxLEFT | wxBOTTOM, 10);

    # ── Output text area — StaticBoxSizer with multiline TextCtrl ─────
    my $out_sizer = Wx::StaticBoxSizer->new(
        Wx::StaticBox->new($panel, wxID_ANY, "Generated Greetings"),
        wxVERTICAL,
    );
    my $output = Wx::TextCtrl->new(
        $panel, wxID_ANY, "",
        wxDefaultPosition, wxDefaultSize,
        wxTE_MULTILINE | wxTE_READONLY,  # Read-only, multi-line
    );
    $output->SetFont(Wx::Font->new(
        12, wxFONTFAMILY_MODERN, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL
    ));
    $out_sizer->Add($output, 1, wxEXPAND | wxALL, 4);
    $root->Add($out_sizer, 1, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, 10);

    $panel->SetSizer($root);

    # ── Store widget refs in $self for use in callbacks ───────────────
    $self->{name_ctrl} = $name_ctrl;
    $self->{choice}    = $choice;
    $self->{output}    = $output;

    # ── Bind events ───────────────────────────────────────────────────
    EVT_BUTTON($self, $gen_btn, \&on_generate);
    EVT_BUTTON($self, $clr_btn, \&on_new);
    EVT_TEXT_ENTER($self, $name_ctrl, \&on_generate);  # Enter in text field
}

sub _build_statusbar {
    my ($self) = @_;
    $self->CreateStatusBar(2);
    $self->SetStatusWidths(-1, 150);     # Field 0 stretches, field 1 = 150px
    $self->SetStatusText("Ready", 0);
    $self->SetStatusText("wxPerl v$Wx::VERSION", 1);
}

# ── Callbacks ─────────────────────────────────────────────────────────────────
sub on_generate {
    my ($self) = @_;
    my $name  = $self->{name_ctrl}->GetValue;
    $name = "World" unless length $name;
    my $greet = $self->{choice}->GetStringSelection;
    $self->{count}++;
    my $line = sprintf "%3d.  %s, %s!\n", $self->{count}, $greet, $name;
    $self->{output}->AppendText($line);
    $self->SetStatusText("Generated greeting #$self->{count}", 0);
}

sub on_new {
    my ($self) = @_;
    $self->{output}->Clear;
    $self->{name_ctrl}->Clear;
    $self->{count} = 0;
    $self->SetStatusText("Cleared", 0);
}

sub on_about {
    my ($self) = @_;
    Wx::MessageBox(
        "wxPerl Greeting Generator\n\n"
      . "Demonstrates:\n"
      . "  \x{2022}  Wx::Frame / Wx::Panel\n"
      . "  \x{2022}  Wx::BoxSizer / Wx::StaticBoxSizer\n"
      . "  \x{2022}  Wx::TextCtrl, Wx::Choice, Wx::Button\n"
      . "  \x{2022}  Menus, status bar, events\n\n"
      . "Running wxWidgets $Wx::wx_version",
        "About", wxOK | wxICON_INFORMATION, $self,
    );
}

sub on_close {
    my ($self, $event) = @_;
    # Show confirmation only if greetings have been generated
    if ($self->{count} > 0) {
        my $dlg = Wx::MessageDialog->new(
            $self,
            "Exit the application?",
            "Confirm",
            wxYES_NO | wxICON_QUESTION | wxNO_DEFAULT,
        );
        my $answer = $dlg->ShowModal;
        $dlg->Destroy;
        return unless $answer == wxID_YES;
    }
    $self->Destroy;
}

# ══════════════════════════════════════════════════════════════════════
# Entry point
# ══════════════════════════════════════════════════════════════════════
package main;
GreetApp->new->MainLoop;
Gtk3

Core Concepts

The Gtk3 Perl binding uses GObject introspection — it reads the GTK3 library's metadata at runtime to generate Perl bindings automatically. This means the API closely mirrors the C library documentation.

Initialization and Main Loop

perl
use Gtk3 '-init';   # Initializes GTK3 with the display; must be first
use Glib qw(TRUE FALSE);  # Import Glib boolean constants

# Create window
my $win = Gtk3::Window->new('toplevel');
$win->set_title("My App");
$win->set_default_size(700, 500);
$win->set_border_width(10);

# MUST handle window close — otherwise clicking X does nothing!
$win->signal_connect(destroy => sub { Gtk3->main_quit });

# ... add widgets ...

$win->show_all;    # Show window and ALL child widgets
Gtk3->main;        # Enter event loop (blocks until main_quit)

Signal / Slot Event Model

Unlike Tk's -command or Wx's EVT_* macros, Gtk3 uses signals — named events that widgets emit, and handlers (callbacks) that you connect to them.

perl
# $widget->signal_connect($signal_name => $callback, @extra_args)
$button->signal_connect(clicked => sub {
    my ($btn, $user_data) = @_;   # Widget that emitted + any extra args
    print "Button clicked!\n";
});

$entry->signal_connect('activate' => sub {   # 'activate' = Enter key
    my $text = $entry->get_text;
    print "Entered: $text\n";
});

$win->signal_connect('key-press-event' => sub {
    my ($widget, $event) = @_;
    return FALSE;   # Return TRUE to stop further processing
});

Containers and Layout

Gtk3::Box

Horizontal or vertical box. Uses pack_start / pack_end.

Gtk3::Grid

Table layout with attach(). Supports row/column spans.

Gtk3::Frame

Adds a titled border around one child widget.

Gtk3::ScrolledWindow

Adds scrollbars to TextView, TreeView, etc.

Gtk3::Notebook

Tabbed panels. Append pages with append_page().

Gtk3::Paned

Draggable split panel (HPaned or VPaned).

perl — layout
my $vbox = Gtk3::Box->new('vertical', 6);  # 'vertical', spacing=6
$win->add($vbox);

# pack_start($child, $expand, $fill, $padding)
$vbox->pack_start($label,  FALSE, FALSE, 5);  # Fixed size
$vbox->pack_start($entry,  TRUE,  TRUE,  5);  # Stretches
$vbox->pack_end  ($button, FALSE, FALSE, 5);  # Packed from bottom

# Grid layout
my $grid = Gtk3::Grid->new;
$grid->set_column_spacing(8);
$grid->set_row_spacing(6);
$grid->attach($label, 0, 0, 1, 1);   # col, row, col_span, row_span
$grid->attach($entry, 1, 0, 1, 1);
$entry->set_hexpand(TRUE);            # Entry stretches horizontally
Gtk3 · Hello World

Hello World

perlgtk3_hello.pl
#!/usr/local/bin/perl
# =============================================================================
#  gtk3_hello.pl  —  Minimal Gtk3 window
#  Install:  pkg install p5-Gtk3
#  Run:      perl gtk3_hello.pl
# =============================================================================
use strict;
use warnings;
use Gtk3 '-init';
use Glib qw(TRUE FALSE);

# Main window
my $win = Gtk3::Window->new('toplevel');
$win->set_title('Hello from Gtk3!');
$win->set_default_size(380, 170);
$win->set_border_width(20);
$win->signal_connect(destroy => sub { Gtk3->main_quit });

# Vertical layout box
my $vbox = Gtk3::Box->new('vertical', 12);
$win->add($vbox);

# Title label — use markup for rich text
my $label = Gtk3::Label->new('');
$label->set_markup(
    'Hello, Gtk3!'
);
$vbox->pack_start($label, TRUE, TRUE, 0);

# Button
my $btn = Gtk3::Button->new_with_label('Greet Me');
$btn->signal_connect(clicked => sub {
    my $dlg = Gtk3::MessageDialog->new(
        $win,                  # Parent window (for centering)
        'destroy-with-parent', # Flags
        'info',                # Message type: info warning error question
        'ok',                  # Buttons: none ok ok-cancel yes-no
        'Greetings from Gtk3 on FreeBSD 16!'
    );
    $dlg->run;
    $dlg->destroy;
});
$vbox->pack_start($btn, FALSE, FALSE, 0);

$win->show_all;
Gtk3->main;
Gtk3 · Full Application

Gtk3 RGB Color Mixer

A complete Gtk3 application demonstrating Gtk3::Scale sliders, a live color preview using CSS, a Gtk3::Grid layout, markup labels, preset buttons, and clipboard integration.

perlgtk3_color_mixer.pl
#!/usr/local/bin/perl
# =============================================================================
#  gtk3_color_mixer.pl  —  Gtk3 RGB Color Mixer
#  Demonstrates: Scale, Grid, CSS, markup, clipboard, preset buttons
#  Install:  pkg install p5-Gtk3 p5-Glib
#  Run:      perl gtk3_color_mixer.pl
# =============================================================================
use strict;
use warnings;
use Gtk3 '-init';
use Glib qw(TRUE FALSE);

# ── Main window ────────────────────────────────────────────────────────────────
my $win = Gtk3::Window->new('toplevel');
$win->set_title('Gtk3 RGB Color Mixer');
$win->set_default_size(520, 440);
$win->set_border_width(14);
$win->signal_connect(destroy => sub { Gtk3->main_quit });

my $root = Gtk3::Box->new('vertical', 10);
$win->add($root);

# ── Title ──────────────────────────────────────────────────────────────────────
my $title = Gtk3::Label->new('');
$title->set_markup('RGB Color Mixer');
$root->pack_start($title, FALSE, FALSE, 4);

# ── Color preview: an EventBox so we can change its background ────────────────
my $preview_eb = Gtk3::EventBox->new;
$preview_eb->set_size_request(-1, 90);
my $preview_lbl = Gtk3::Label->new('');
$preview_lbl->set_markup('#808080');
$preview_eb->add($preview_lbl);
$root->pack_start($preview_eb, FALSE, FALSE, 0);

# CSS provider for background color — we update it as sliders move
my $css = Gtk3::CssProvider->new;

# ── Slider grid ───────────────────────────────────────────────────────────────
my $grid = Gtk3::Grid->new;
$grid->set_row_spacing(8);
$grid->set_column_spacing(10);
$grid->set_margin_top(8);
$root->pack_start($grid, FALSE, FALSE, 0);

my (%sliders, %val_labels);
my @channels = (
    ['R', 'Red',   '#cc3333'],
    ['G', 'Green', '#33cc55'],
    ['B', 'Blue',  '#3366cc'],
);

for my $i (0 .. $#channels) {
    my ($key, $name, $color) = @{ $channels[$i] };

    # Colored name label
    my $name_lbl = Gtk3::Label->new('');
    $name_lbl->set_markup(
        "$name"
    );
    $name_lbl->set_halign('end');
    $grid->attach($name_lbl, 0, $i, 1, 1);

    # Slider (Scale widget)
    my $scale = Gtk3::Scale->new_with_range('horizontal', 0, 255, 1);
    $scale->set_value(128);
    $scale->set_draw_value(FALSE);   # Hide built-in value label (we make our own)
    $scale->set_hexpand(TRUE);
    $sliders{$key} = $scale;
    $grid->attach($scale, 1, $i, 1, 1);

    # Value readout
    my $vl = Gtk3::Label->new('');
    $vl->set_markup('128');
    $vl->set_width_chars(4);
    $vl->set_halign('start');
    $val_labels{$key} = $vl;
    $grid->attach($vl, 2, $i, 1, 1);
}

# ── Separator ──────────────────────────────────────────────────────────────────
$root->pack_start(Gtk3::Separator->new('horizontal'), FALSE, FALSE, 2);

# ── Preset buttons ─────────────────────────────────────────────────────────────
my $preset_outer = Gtk3::Box->new('horizontal', 6);
$preset_outer->set_margin_start(2);
$root->pack_start($preset_outer, FALSE, FALSE, 0);
$preset_outer->pack_start(Gtk3::Label->new('Presets:'), FALSE, FALSE, 0);

my %presets = (
    Amber   => [240, 160, 48],
    Red     => [220,  50, 50],
    Green   => [ 50, 180, 80],
    Blue    => [ 40, 100, 210],
    White   => [255, 255, 255],
    Black   => [  0,   0,   0],
    Violet  => [150,  60, 200],
);

for my $name (sort keys %presets) {
    my @rgb = @{ $presets{$name} };
    my $btn = Gtk3::Button->new_with_label($name);
    $btn->signal_connect(clicked => sub {
        $sliders{R}->set_value($rgb[0]);
        $sliders{G}->set_value($rgb[1]);
        $sliders{B}->set_value($rgb[2]);
        # update_color is called by slider value-changed signals
    });
    $preset_outer->pack_start($btn, FALSE, FALSE, 0);
}

# ── Copy hex button ────────────────────────────────────────────────────────────
my $copy_btn = Gtk3::Button->new_with_label('Copy Hex');
$preset_outer->pack_end($copy_btn, FALSE, FALSE, 0);

# ── Hex label at bottom ────────────────────────────────────────────────────────
my $hex_lbl = Gtk3::Label->new('');
$hex_lbl->set_markup(
    '#808080'
);
$root->pack_start($hex_lbl, FALSE, FALSE, 4);

# ── Update callback: recompute color from slider positions ────────────────────
my $update_color = sub {
    my $r = int($sliders{R}->get_value);
    my $g = int($sliders{G}->get_value);
    my $b = int($sliders{B}->get_value);
    my $hex = sprintf "#%02x%02x%02x", $r, $g, $b;

    # Update value readout labels
    for my $info (['R', $r], ['G', $g], ['B', $b]) {
        my ($key, $val) = @$info;
        $val_labels{$key}->set_markup(
            "$val"
        );
    }
    # Update hex display labels
    my $markup = "$hex";
    $hex_lbl->set_markup($markup);
    $preview_lbl->set_markup(
        "$hex"
    );

    # Update preview background via CSS
    $css->load_from_data("* { background-color: $hex; }");
    $preview_eb->get_style_context->add_provider(
        $css, Gtk3::STYLE_PROVIDER_PRIORITY_APPLICATION
    );
};

# Connect all sliders to the update function
for my $key (qw(R G B)) {
    $sliders{$key}->signal_connect('value-changed' => $update_color);
}

# Copy to clipboard
$copy_btn->signal_connect(clicked => sub {
    my $r = int($sliders{R}->get_value);
    my $g = int($sliders{G}->get_value);
    my $b = int($sliders{B}->get_value);
    my $hex = sprintf "#%02x%02x%02x", $r, $g, $b;
    my $clip = Gtk3::Clipboard::get(Gtk3::Gdk::Atom::intern('CLIPBOARD', FALSE));
    $clip->set_text($hex, -1);
});

$update_color->();   # Set initial display state
$win->show_all;
Gtk3->main;
Prima

Core Concepts

Prima is unique: it is written entirely in Perl and C with its own rendering engine — no external toolkit (no Tk, no GTK, no wxWidgets). Its API is purely property-based OOP.

Coordinate system warning: Prima's origin [0, 0] is at the bottom-left of the parent widget, with Y increasing upward. This is the opposite of most toolkits. A widget at origin => [10, 350] in a 400px-tall window is near the top, not the bottom.

Application and Windows

perl
use Prima qw(Application Buttons Labels InputLine Edit MsgBox);

# Exactly one application object per program
my $app = Prima::Application->new(
    name  => 'MyApp',    # Used in title bar on some platforms
    title => 'MyApp',
);

# Main window — use Prima::MainWindow (not Prima::Window)
# Prima::MainWindow automatically calls $::application->close on destroy
my $win = Prima::MainWindow->new(
    text     => 'My Application',   # Window title
    size     => [700, 500],         # [width, height] in pixels
    centered => 1,                  # Center on screen
    # Optional: override destroy behavior
    onDestroy => sub { $::application->close },
);

$app->run;   # Enter event loop — blocks until all windows close

Creating Widgets

Widgets are inserted into their parent with insert() or directly with WidgetClass->new($owner, %props). All properties are set at creation time (or with accessor methods).

perl
# Method 1: parent->insert('ClassName', props...)
my $btn = $win->insert('Prima::Button',
    text    => 'Click Me',
    origin  => [10, 20],     # [x, y] from bottom-left
    size    => [100, 32],    # [width, height]
    onClick => sub { ... },
);

# Method 2: direct construction
my $lbl = Prima::Label->new($win,
    text      => 'Hello!',
    origin    => [20, 400],  # Near top of 500px window
    size      => [200, 24],
    alignment => ta::Center,
    font      => { size => 14, style => fs::Bold },
);

# Read/write properties after creation
print $btn->text;         # Get
$btn->text('New Label');  # Set

# Colors: use hex-string or Prima color constants
$lbl->color(cl::Blue);           # Foreground
$lbl->backColor(0xF0E0D0);       # Background (hex integer)
$lbl->backColor(Prima::Color::from_rgb(240, 160, 48)); # RGB

Font and Style Constants

CategoryConstants
Font stylefs::Normal fs::Bold fs::Italic fs::Underline fs::StruckOut
Text alignmentta::Left ta::Center ta::Right ta::Top ta::Middle ta::Bottom
Colorscl::Black cl::White cl::Red cl::Green cl::Blue cl::Yellow
Message boxmb::Ok mb::OkCancel mb::YesNo mb::YesNoCancel

Menus

perl
# menuItems is an arrayref of [label => [items...]]
$win->menuItems([
    ['~File' => [
        # Each item: [id, label, hotkey_display, keystroke, callback]
        ['new_item',  '~New',  'Ctrl+N', '^N', sub { ... }],
        ['*'],                            # Separator
        ['exit_item', 'E~xit', 'Ctrl+Q', '^Q',
            sub { $::application->close }
        ],
    ]],
    ['~Help' => [
        ['about_item', '~About', '', '',
            sub { Prima::MsgBox::message("About MyApp", mb::Ok) }
        ],
    ]],
]);
Prima · Hello World

Hello World

perlprima_hello.pl
#!/usr/local/bin/perl
# =============================================================================
#  prima_hello.pl  —  Minimal Prima window
#  Install:  pkg install p5-Prima   OR   cpanm Prima
#  Run:      perl prima_hello.pl
# =============================================================================
use strict;
use warnings;
use Prima qw(Application Buttons Labels MsgBox);

# One application object
my $app = Prima::Application->new(name => 'Hello');

# Main window (800px wide, 200px tall)
my $win = Prima::MainWindow->new(
    text     => 'Hello from Prima!',
    size     => [400, 200],
    centered => 1,
    # REMEMBER: y=0 is bottom-left; y=170 is near the top of a 200px window
    onDestroy => sub { $::application->close },
);

# Label — placed near the top (y=140 in a 200px window)
$win->insert('Prima::Label',
    text      => 'Hello, Prima!',
    origin    => [50, 130],     # 50px from left, 130px from bottom
    size      => [300, 36],
    alignment => ta::Center,
    font      => { size => 18, style => fs::Bold },
);

# Explanation label
$win->insert('Prima::Label',
    text   => "Y=0 is bottom-left in Prima's coordinate system",
    origin => [20, 90],
    size   => [360, 24],
    alignment => ta::Center,
    font   => { size => 10 },
    color  => cl::DarkGray,
);

# Button
$win->insert('Prima::Button',
    text   => 'Click Me',
    origin => [150, 30],         # 150px from left, 30px from bottom
    size   => [100, 40],
    onClick => sub {
        Prima::MsgBox::message("Greetings from Prima!", mb::Ok);
    },
);

$app->run;
Prima · Full Application

Prima Simple Notepad

A complete Prima application: menubar, toolbar-style buttons, a GroupBox with InputLine, a full-height Edit area, and status text — all using Prima's absolute-coordinate layout.

perlprima_notepad.pl
#!/usr/local/bin/perl
# =============================================================================
#  prima_notepad.pl  —  Prima simple notepad
#  Demonstrates: MainWindow, menus, GroupBox, InputLine, Edit, Button,
#                onResize to keep layout responsive, MsgBox
#  Install:  pkg install p5-Prima   OR   cpanm Prima
#  Run:      perl prima_notepad.pl
# =============================================================================
use strict;
use warnings;
use Prima qw(Application Buttons Labels InputLine Edit MsgBox StdDlg GroupBox);

# ── State ──────────────────────────────────────────────────────────────────────
my $current_file = '';
my $count        = 0;

# ── Application ────────────────────────────────────────────────────────────────
my $app = Prima::Application->new(name => 'PrimaNotepad');

# ── Main window ────────────────────────────────────────────────────────────────
my $win = Prima::MainWindow->new(
    text     => 'Prima Notepad',
    size     => [680, 520],
    centered => 1,
    onDestroy => sub { $::application->close },
);

# ── Menus ──────────────────────────────────────────────────────────────────────
$win->menuItems([
    ['~File' => [
        ['new_m',  '~New',     'Ctrl+N', '^N', \&on_new ],
        ['open_m', '~Open…',   'Ctrl+O', '^O', \&on_open],
        ['save_m', '~Save As…','Ctrl+S', '^S', \&on_save],
        ['*'],
        ['exit_m', 'E~xit',    'Ctrl+Q', '^Q', sub { $::application->close }],
    ]],
    ['~Edit' => [
        ['clear_m', 'Clear ~All', '', '', \&on_clear],
    ]],
    ['~Help' => [
        ['about_m', '~About', '', '', \&on_about],
    ]],
]);

# ── Toolbar area (near top — remember y=0 is bottom!) ─────────────────────────
# In a 520px window:
#   y=480 = very top
#   y=460 = toolbar row
#   y=350 = search group top
#   y=  0 = very bottom

# Toolbar buttons along the top
for my $info (
    ['New',    \&on_new,   10],
    ['Open',   \&on_open,  70],
    ['Save',   \&on_save, 130],
    ['Clear',  \&on_clear,190],
) {
    my ($label, $cb, $x) = @$info;
    $win->insert('Prima::Button',
        text   => $label,
        origin => [$x, 476],   # 476px from bottom = near top of 520px window
        size   => [55, 28],
        onClick => $cb,
    );
}

# ── Search / Input group ───────────────────────────────────────────────────────
my $grp = $win->insert('Prima::GroupBox',
    text   => 'Add Entry',
    origin => [8, 390],    # y=390 in 520px window
    size   => [660, 80],
);

$grp->insert('Prima::Label',
    text   => 'Text:',
    origin => [10, 36],
    size   => [40, 22],
);

my $input = $grp->insert('Prima::InputLine',
    origin => [56, 33],
    size   => [480, 26],
    text   => '',
);

my $add_btn = $grp->insert('Prima::Button',
    text    => 'Add ▶',
    origin  => [545, 28],
    size    => [100, 36],
    onClick => \&on_add_entry,
);

# ── Main edit area ─────────────────────────────────────────────────────────────
my $edit = $win->insert('Prima::Edit',
    origin   => [8, 30],     # 30px from bottom (leaves room for status)
    size     => [660, 354],  # Fills most of the window
    readOnly => 1,
    wordWrap => 1,
    font     => { name => 'Courier', size => 11 },
    backColor => cl::White,
);

# ── Status label at the very bottom ────────────────────────────────────────────
my $status = $win->insert('Prima::Label',
    text   => 'Ready — type text above and click Add',
    origin => [8, 6],
    size   => [660, 20],
    font   => { size => 10 },
    color  => cl::DarkGray,
);

# ── Resize handler — keep widgets proportional when window resizes ─────────────
$win->onResize(sub {
    my ($self) = @_;
    my ($w, $h) = ($self->width, $self->height);
    # Reposition toolbar buttons
    $add_btn->origin($add_btn->left, $h - 44);
    # Move group box
    $grp->origin(8, $h - 130);
    # Resize edit area
    $edit->size($w - 20, $h - 180);
    # Keep status at very bottom
    $status->size($w - 20, 20);
});

# ── Callbacks ──────────────────────────────────────────────────────────────────
sub on_add_entry {
    my $text = $input->text;
    return unless length $text;
    $count++;
    $edit->text(
        $edit->text . sprintf("%3d.  %s\n", $count, $text)
    );
    $input->text('');
    $status->text("Added entry #$count");
}

sub on_new {
    $edit->text('');
    $current_file = '';
    $count = 0;
    $win->text('Prima Notepad');
    $status->text('New document');
}

sub on_clear {
    $edit->text('');
    $count = 0;
    $status->text('Cleared');
}

sub on_open {
    my $dlg = Prima::OpenDialog->new(
        filter => [['Text files' => '*.txt'], ['All files' => '*']],
    );
    if ($dlg->execute) {
        my $file = $dlg->fileName;
        if (open my $fh, '<', $file) {
            local $/;
            $edit->text(<$fh>);
            close $fh;
            $current_file = $file;
            $win->text("Prima Notepad — $file");
            $status->text("Opened: $file");
        } else {
            Prima::MsgBox::message("Could not open: $!", mb::Ok);
        }
    }
    $dlg->destroy;
}

sub on_save {
    my $dlg = Prima::SaveDialog->new(
        fileName => $current_file || 'untitled.txt',
        filter   => [['Text files' => '*.txt'], ['All files' => '*']],
    );
    if ($dlg->execute) {
        my $file = $dlg->fileName;
        if (open my $fh, '>', $file) {
            print $fh $edit->text;
            close $fh;
            $current_file = $file;
            $win->text("Prima Notepad — $file");
            $status->text("Saved: $file");
        } else {
            Prima::MsgBox::message("Could not save: $!", mb::Ok);
        }
    }
    $dlg->destroy;
}

sub on_about {
    Prima::MsgBox::message(
        "Prima Notepad\n\n"
      . "Demonstrates:\n"
      . "  \x{2022}  Prima::MainWindow\n"
      . "  \x{2022}  Menus (menuItems)\n"
      . "  \x{2022}  Prima::GroupBox layout\n"
      . "  \x{2022}  Prima::InputLine, Prima::Edit\n"
      . "  \x{2022}  Prima::OpenDialog, SaveDialog\n"
      . "  \x{2022}  Bottom-left coordinate system",
        mb::Ok,
    );
}

$app->run;
IUP

IUP — Lightweight Cross-Platform GUI

IUP (Portable User Interface) was created at PUC-Rio (Brazil). It uses a unique attribute-driven API — every widget property is set and read as a named string attribute, and the layout uses abstract containers rather than pixel positioning.

Installation on FreeBSD 16

bash
pkg install iup    # The C library
cpanm IUP          # The Perl binding
perl -e 'use IUP; print "IUP OK\n"'

The Attribute System

Unlike other toolkits, IUP uses string attributes for everything — sizes, colors, fonts, and state are all set as "ATTRIBUTE" => "value" pairs.

perl — IUP concepts
use IUP ':all';

# Widgets are created with attributes as a hash
my $btn = IUP::Button->new(TITLE => "Click Me", SIZE => "80x30");

# Attributes can be read and set at any time
$btn->TITLE("New Text");           # Set
my $txt = $btn->TITLE;             # Get

# Sizes in IUP use a special format:
#   "WxH"      absolute pixels
#   "HALFxHALF"  half the container
#   ""         natural (auto) size
# Colors: "R G B" (0-255) or color names
$btn->BGCOLOR("255 128 0");        # Orange background
$btn->FGCOLOR("255 255 255");      # White text

IUP Hello World

perliup_hello.pl
#!/usr/local/bin/perl
# =============================================================================
#  iup_hello.pl  —  Minimal IUP window
#  Install:  pkg install iup && cpanm IUP
#  Run:      perl iup_hello.pl
# =============================================================================
use strict;
use warnings;
use IUP ':all';

IUP->Open() or die "Cannot init IUP\n";

my $lbl = IUP::Label->new(
    TITLE     => "Hello from IUP!",
    FONT      => "Helvetica, Bold 18",
    ALIGNMENT => "ACENTER:ACENTER",
);

my $btn = IUP::Button->new(
    TITLE   => "Click Me",
    SIZE    => "80x28",
    ACTION  => sub {
        IUP->Message("Hello", "Greetings from IUP on FreeBSD!");
        return IUP_DEFAULT;
    },
);

my $vbox = IUP::Vbox->new(
    MARGIN    => "20x20",
    GAP       => "10",
    ALIGNMENT => "ACENTER",
    $lbl, $btn,
);

my $dlg = IUP::Dialog->new(
    TITLE  => "IUP Hello World",
    $vbox,
);

$dlg->ShowXY(IUP_CENTER, IUP_CENTER);
IUP->MainLoop;
IUP->Close;

IUP Complete App — Unit Converter

perliup_converter.pl
#!/usr/local/bin/perl
# =============================================================================
#  iup_converter.pl  —  IUP Unit Converter
#  Demonstrates: Vbox/Hbox layout, Text, List, Label, menus, timer
#  Install:  pkg install iup && cpanm IUP
#  Run:      perl iup_converter.pl
# =============================================================================
use strict;
use warnings;
use IUP ':all';

IUP->Open() or die "Cannot init IUP\n";

# ── Conversion table ───────────────────────────────────────────────────────────
my %conversions = (
    'km  → miles'   => sub { $_[0] * 0.621371 },
    'miles → km'    => sub { $_[0] * 1.60934  },
    'kg  → pounds'  => sub { $_[0] * 2.20462  },
    'pounds → kg'   => sub { $_[0] * 0.453592 },
    'C°  → F°'      => sub { $_[0] * 9/5 + 32 },
    'F°  → C°'      => sub { ($_[0] - 32) * 5/9 },
    'cm  → inches'  => sub { $_[0] * 0.393701 },
    'inches → cm'   => sub { $_[0] * 2.54     },
    'liters → gal'  => sub { $_[0] * 0.264172 },
    'gal → liters'  => sub { $_[0] * 3.78541  },
);

# ── Widgets ────────────────────────────────────────────────────────────────────
my $input_lbl = IUP::Label->new(TITLE => "Input value:");

my $input = IUP::Text->new(
    SIZE    => "100x",
    VALUE   => "0",
    SPIN    => "YES",        # Add spinner buttons
);

my $conv_lbl = IUP::Label->new(TITLE => "Conversion:");

# Build the list string (IUP uses |-separated values for list items)
my @conv_names = sort keys %conversions;
my $list = IUP::List->new(
    SIZE      => "150x130",
    VISIBLELINES => "8",
    VALUE        => "1",         # Select first item
);
$list->$_(1 + $_, $conv_names[$_]) for 0..$#conv_names;

my $result_lbl = IUP::Label->new(
    TITLE  => "Result: —",
    FONT   => "Helvetica, Bold 14",
    SIZE   => "200x",
);

my $convert_btn = IUP::Button->new(
    TITLE  => "Convert  ▶",
    SIZE   => "80x28",
    BGCOLOR => "0 100 200",
    FGCOLOR => "255 255 255",
);

# ── Layout ─────────────────────────────────────────────────────────────────────
my $top_hbox = IUP::Hbox->new(
    MARGIN    => "0x0",
    GAP       => "8",
    ALIGNMENT => "ACENTER",
    $input_lbl, $input,
);

my $main_vbox = IUP::Vbox->new(
    MARGIN    => "16x16",
    GAP       => "10",
    ALIGNMENT => "ACENTER",
    $top_hbox,
    IUP::Label->new(TITLE => ""),  # Spacer
    $conv_lbl,
    $list,
    $convert_btn,
    IUP::Fill->new,               # Flexible space
    $result_lbl,
);

# ── Dialog ─────────────────────────────────────────────────────────────────────
my $dlg = IUP::Dialog->new(
    TITLE   => "IUP Unit Converter",
    SIZE    => "240x340",
    MINSIZE => "220x300",
    $main_vbox,
);

# ── Events ─────────────────────────────────────────────────────────────────────
$convert_btn->ACTION(sub {
    my $raw = $input->VALUE;
    unless ($raw =~ /^-?\d+\.?\d*$/) {
        $result_lbl->TITLE("Error: not a number");
        return IUP_DEFAULT;
    }
    my $value   = $raw + 0;
    my $sel_idx = $list->VALUE - 1;   # IUP list index is 1-based
    my $name    = $conv_names[$sel_idx];
    my $func    = $conversions{$name};
    my $result  = $func->($value);
    $result_lbl->TITLE(sprintf "%.6g  %s", $result, $name);
    return IUP_DEFAULT;
});

# Allow Enter key in input field to trigger conversion
$input->ACTION(sub {
    $convert_btn->ACTION->();
    return IUP_DEFAULT;
});

$dlg->ShowXY(IUP_CENTER, IUP_CENTER);
IUP->MainLoop;
IUP->Close;

Which Toolkit Should You Choose?

Your SituationBest ChoiceReason
Coming from Perl/Tk, want similar feelWx (wxPerl)Closest mental model: App/Frame/Panel maps to MainWindow/Frame/Widget
Building a proper Linux/BSD desktop appGtk3Integrates natively with GNOME/XFCE. CSS theming. Best ecosystem on FreeBSD.
Need custom drawing / unique graphicsPrimaIts own rendering engine — you can draw anything. No external dependency.
Quick utility, simple formsIUPSmallest, fastest, simplest API. Great for internal tools.
Maximum cross-platform (Windows too)Wx or Gtk3Both run on Windows/Mac/Linux/BSD without code changes.
Want to learn GUI programming conceptsWxSizers and the App/Frame pattern teach transferable concepts (similar to Qt).

Mental Model Comparison

comparison — the same "label + button" in all four toolkits
##############################################################
## Perl/Tk (reference)
use Tk;
my $mw  = MainWindow->new;
my $lbl = $mw->Label(-text => "Hello")->pack;
my $btn = $mw->Button(-text => "Click", -command => sub {...})->pack;
MainLoop;

##############################################################
## wxPerl
use Wx qw(:everything); use Wx::Event qw(EVT_BUTTON);
package App; use parent -norequire, 'Wx::App';
sub OnInit {
    my $f = Wx::Frame->new(undef, wxID_ANY, "Title");
    my $p = Wx::Panel->new($f, wxID_ANY);
    my $s = Wx::BoxSizer->new(wxVERTICAL);
    my $l = Wx::StaticText->new($p, wxID_ANY, "Hello");
    my $b = Wx::Button->new($p, wxID_ANY, "Click");
    EVT_BUTTON($f, $b, sub {...});
    $s->Add($l, 0, wxALL, 5); $s->Add($b, 0, wxALL, 5);
    $p->SetSizer($s); $f->Show(1); return 1;
}
package main; App->new->MainLoop;

##############################################################
## Gtk3
use Gtk3 '-init';
my $w = Gtk3::Window->new('toplevel');
my $v = Gtk3::Box->new('vertical', 6);
my $l = Gtk3::Label->new("Hello");
my $b = Gtk3::Button->new_with_label("Click");
$b->signal_connect(clicked => sub {...});
$v->pack_start($l, 0,0,5); $v->pack_start($b, 0,0,5);
$w->add($v); $w->show_all; Gtk3->main;

##############################################################
## Prima
use Prima qw(Application Buttons Labels);
my $app = Prima::Application->new;
my $win = Prima::MainWindow->new(size => [300, 150]);
$win->insert('Prima::Label',  text => "Hello", origin => [20,100], size => [200,24]);
$win->insert('Prima::Button', text => "Click", origin => [100,30], size => [80,32],
    onClick => sub {...});
$app->run;

##############################################################
## IUP
use IUP ':all'; IUP->Open;
my $l = IUP::Label->new(TITLE => "Hello");
my $b = IUP::Button->new(TITLE => "Click", ACTION => sub {...; IUP_DEFAULT});
my $v = IUP::Vbox->new($l, $b);
IUP::Dialog->new(TITLE => "Title", $v)->ShowXY(IUP_CENTER, IUP_CENTER);
IUP->MainLoop; IUP->Close;

Running Your Scripts via Lima

bash — daily workflow
# On your macOS host — get Lima VM SSH port
PORT=$(limactl list --format '{{.SSHLocalPort}}' experimental/freebsd-16)
KEY=~/.lima/_config/user

# Copy script(s) to the FreeBSD VM
scp -P $PORT -i $KEY wx_greeting.pl 127.0.0.1:~/

# Connect with X11 forwarding and run
ssh -X -p $PORT -i $KEY -o StrictHostKeyChecking=no 127.0.0.1

# Inside the VM:
perl wx_greeting.pl        # Window opens in XQuartz on your Mac desktop
perl gtk3_color_mixer.pl
perl prima_notepad.pl
perl iup_converter.pl
Comparison summary: For a beginner coming from Perl/Tk, wxPerl is the gentlest transition. For building something that looks native on FreeBSD desktops, Gtk3 is the best choice. For unusual graphics needs, Prima is uniquely capable. For the simplest possible code, IUP is hardest to beat.