Beyond Perl/Tk
Four alternative GUI toolkits — each with complete working examples.
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:
Feature Comparison
| Feature | Wx (wxPerl) | Gtk3 | Prima | IUP |
|---|---|---|---|---|
| FreeBSD pkg | p5-Wx | p5-Gtk3 | p5-Prima | cpanm IUP |
| Architecture | wxWidgets C++ binding | GObject introspection | Perl + C engine | C library binding |
| Layout | Sizers (powerful) | Box/Grid containers | Absolute + Layout | Attribute-driven |
| Theming | System native | CSS stylesheets | Built-in skins | Minimal, consistent |
| Learning curve | Moderate | Moderate | Steep (inverted Y) | Easy |
| Documentation | Excellent | Good | Fair | Good |
| OOP style | Subclass frames | Functional/OOP | Property-based OOP | Functional/OOP |
| Event model | EVT_* macros | signal_connect | onEvent callbacks | callbacks |
| Best for | Desktop apps, menus | Linux/BSD apps | Custom graphics | Quick 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.
pkg install p5-Wx
# Verify:
perl -e 'use Wx; print "wxWidgets ", $Wx::wx_version, "\n"'pkg install p5-Gtk3 p5-Glib
# Verify:
perl -e 'use Gtk3; print "Gtk3 OK\n"'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"'pkg install iup
cpanm IUP
# Verify:
perl -e 'use IUP; print "IUP OK\n"'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.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.
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 loopPanels 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.
Lays out widgets in a row (H) or column (V). The most-used sizer.
Like BoxSizer but draws a labeled border box around the group.
Equal-sized grid cells. Good for button grids.
Grid where rows/columns can have different sizes.
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 contentsEvent Binding with EVT_* Macros
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
| Category | Constants |
|---|---|
| Orientation | wxHORIZONTAL wxVERTICAL |
| Sizer flags | wxEXPAND wxALL wxTOP wxBOTTOM wxLEFT wxRIGHT wxALIGN_CENTER wxALIGN_RIGHT |
| IDs | wxID_ANY wxID_OK wxID_CANCEL wxID_EXIT wxID_ABOUT wxID_NEW wxID_OPEN wxID_SAVE |
| Dialog buttons | wxOK wxCANCEL wxYES wxNO wxYES_NO wxOK_DEFAULT |
| Dialog icons | wxICON_INFORMATION wxICON_WARNING wxICON_ERROR wxICON_QUESTION |
| Text style | wxTE_MULTILINE wxTE_READONLY wxTE_RICH2 wxTE_PROCESS_ENTER |
| Font | wxFONTFAMILY_DEFAULT wxFONTFAMILY_MODERN wxFONTSTYLE_NORMAL wxFONTWEIGHT_BOLD |
Hello World
The minimal wxPerl program: an App class, a Frame with a Panel, a label, and a button.
#!/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 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.
#!/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;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
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.
# $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
Horizontal or vertical box. Uses pack_start / pack_end.
Table layout with attach(). Supports row/column spans.
Adds a titled border around one child widget.
Adds scrollbars to TextView, TreeView, etc.
Tabbed panels. Append pages with append_page().
Draggable split panel (HPaned or VPaned).
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 horizontallyHello World
#!/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 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.
#!/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;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.
[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
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 closeCreating 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).
# 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)); # RGBFont and Style Constants
| Category | Constants |
|---|---|
| Font style | fs::Normal fs::Bold fs::Italic fs::Underline fs::StruckOut |
| Text alignment | ta::Left ta::Center ta::Right ta::Top ta::Middle ta::Bottom |
| Colors | cl::Black cl::White cl::Red cl::Green cl::Blue cl::Yellow |
| Message box | mb::Ok mb::OkCancel mb::YesNo mb::YesNoCancel |
Menus
# 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) }
],
]],
]);Hello World
#!/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 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.
#!/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 — 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
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.
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 textIUP Hello World
#!/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
#!/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 Situation | Best Choice | Reason |
|---|---|---|
| Coming from Perl/Tk, want similar feel | Wx (wxPerl) | Closest mental model: App/Frame/Panel maps to MainWindow/Frame/Widget |
| Building a proper Linux/BSD desktop app | Gtk3 | Integrates natively with GNOME/XFCE. CSS theming. Best ecosystem on FreeBSD. |
| Need custom drawing / unique graphics | Prima | Its own rendering engine — you can draw anything. No external dependency. |
| Quick utility, simple forms | IUP | Smallest, fastest, simplest API. Great for internal tools. |
| Maximum cross-platform (Windows too) | Wx or Gtk3 | Both run on Windows/Mac/Linux/BSD without code changes. |
| Want to learn GUI programming concepts | Wx | Sizers and the App/Frame pattern teach transferable concepts (similar to Qt). |
Mental Model Comparison
##############################################################
## 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
# 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