Skip to content

GTK3 → GTK4 Migration Assessment #20433

@TurboGit

Description

@TurboGit

For the record here is a document contributed by @masterpiga. The document is an analysis from Claude AI about the migration. Post here in the hope it will help.

GTK3 → GTK4 Migration Assessment: darktable

Assessment date: March 2026


1. Executive Summary

Migrating darktable from GTK3 to GTK4 is a large, high-risk, multi-month undertaking. The codebase has ~506,000 lines of C across 501 source files, of which roughly 205 files touch GTK directly. darktable is not a typical GTK application: it has built an entire second layer of custom widgets on top of GTK (the bauhaus system for sliders and combo boxes, and the dtgtk library of icon buttons and range selectors), and its main canvas relies on hand-crafted event handling for pixel-accurate interactions with images. These layers are precisely what GTK4 changes most radically.

The migration is feasible but requires deliberate phasing. The core GTK API changes are mechanical enough to automate large portions of the work, but the event system rewrite and the bauhaus custom widget system require careful design decisions that go beyond search-and-replace. A realistic estimate with AI assistance is 12–18 weeks of focused effort, of which roughly 65% can be handled autonomously by an AI coding assistant, with the remaining 35% requiring human judgment, visual validation, and platform testing.


2. What Changed Between GTK3 and GTK4 (and Why It Matters)

If you are unfamiliar with the differences between the two toolkit versions, here is what you need to understand. GTK4 is not a minor revision: it is a deliberate modernisation that broke backward compatibility in several fundamental areas.

2.1 The Event System Was Completely Redesigned

In GTK3, user input (mouse clicks, keyboard presses, pointer motion, scrolling) was delivered as raw event structures (GdkEventButton, GdkEventKey, GdkEventMotion, etc.) passed directly to signal callback functions. A handler for a mouse click looked like:

// GTK3
gboolean on_click(GtkWidget *widget, GdkEventButton *event, gpointer data) {
    if (event->button == 1) { ... }
}
g_signal_connect(widget, "button-press-event", G_CALLBACK(on_click), NULL);

In GTK4 this pattern is entirely gone. The "button-press-event", "key-press-event", "motion-notify-event", and similar signals no longer exist. Instead, GTK4 introduces a family of event controller objects (GtkGestureClick, GtkEventControllerKey, GtkEventControllerMotion, GtkEventControllerScroll, etc.) that you attach to widgets:

// GTK4
GtkGestureClick *click = gtk_gesture_click_new();
g_signal_connect(click, "pressed", G_CALLBACK(on_click), NULL);
gtk_widget_add_controller(widget, GTK_EVENT_CONTROLLER(click));

This is not a rename — it is a fundamentally different model. Every single event handler in the application needs to be examined and rewritten individually. darktable has approximately 1,192 g_signal_connect calls across the codebase, a significant fraction of which use the deprecated event signal names.

2.2 GtkContainer and Widget Packing Were Simplified

GTK3 had a GtkContainer base class with methods like gtk_container_add(), and GtkBox had gtk_box_pack_start() / gtk_box_pack_end() with boolean parameters controlling expansion and fill. GTK4 removed GtkContainer entirely and simplified the packing API:

// GTK3
gtk_box_pack_start(GTK_BOX(box), child, TRUE, TRUE, 4);

// GTK4
gtk_box_append(GTK_BOX(box), child);  // expansion/fill handled via child properties

darktable has ~631 gtk_box_pack_start/end calls across 68+ files. These are mechanical to update but numerous.

2.3 GtkEventBox Was Removed

GtkEventBox was a GTK3 trick: a transparent container that added event-handling capability to widgets that otherwise would not receive mouse/keyboard input. GTK4 removed this widget because in GTK4, all widgets can receive events via the new event controller system. The problem for darktable is that GtkEventBox is used as the base class for several custom widgets in src/dtgtk/ (specifically GtkDarktableIcon and GtkDarktableRange). Those widgets need to be rebased onto a different parent.

2.4 GtkMenu and the Entire Menu System Were Removed

GtkMenu, GtkMenuItem, GtkMenuBar, and related types were completely removed in GTK4. They are replaced by a model-based system using GMenu (a data model) rendered via GtkPopoverMenu (the visual component). This is not a trivial rename: the programming model is different enough that context menus and popup menus need to be redesigned, not just ported.

darktable uses menus in ~28 files (~360 occurrences).

2.5 The Drawing Model Changed

GTK3 widgets overrode the "draw" signal to receive a cairo_t * context and paint themselves:

// GTK3
g_signal_connect(widget, "draw", G_CALLBACK(my_draw), NULL);
static gboolean my_draw(GtkWidget *w, cairo_t *cr, gpointer data) { ... }

In GTK4, GtkDrawingArea uses a dedicated draw function registered once:

// GTK4
gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(widget), my_draw, NULL, NULL);
static void my_draw(GtkDrawingArea *da, cairo_t *cr, int w, int h, gpointer data) { ... }

Cairo itself still works and the drawing code inside the function is largely unchanged — only the registration mechanism changed. There are ~414 occurrences of custom drawing across ~104 files.

2.6 gtk_main() Was Removed

GTK4 moved to a GApplication-based event loop. gtk_main() and gtk_main_quit() are gone. darktable's main entry point will need to be restructured around g_application_run().

2.7 gtk_widget_show_all() Was Removed

In GTK3, widgets were hidden by default and you had to call gtk_widget_show() or gtk_widget_show_all() (recursive) to make them visible. In GTK4, widgets are visible by default, and gtk_widget_show_all() does not exist. Explicit gtk_widget_set_visible(FALSE) is used to hide things. This is widespread in darktable and produces both compilation errors and invisible widgets if not handled.

2.8 GdkWindow → GdkSurface

GdkWindow was renamed GdkSurface and its API was revised. Most of darktable's direct gdk_window_* usage is incidental (e.g., inside gdk/cairo bridge calls) and lower risk.

2.9 What Did NOT Change (Good News)

  • Cairo is still the primary drawing API. Drawing code inside callbacks is largely unchanged.
  • GObject / GLib (the type system, memory management, signals on non-widget objects) is fully compatible.
  • Pango (text layout) is compatible.
  • CSS theming works in GTK4 with minor selector adjustments.
  • GtkTreeView is deprecated in GTK4 but still compiles and runs. Replacement is not urgent.
  • GtkScrolledWindow, GtkLabel, GtkEntry, GtkCheckButton, GtkSpinButton and most basic widgets are compatible with minor API adjustments.

3. Codebase Audit

3.1 Scale

Metric Value
Total source files 501
Total lines of code ~506,000
Files importing gtk.h 107
Files using GtkWidget / gtk_widget_* 205
g_signal_connect calls ~1,192
Custom widget files (src/dtgtk/) 30
bauhaus system (src/bauhaus/) 4,530 lines
Image operation modules (src/iop/) 105 files
Library modules (src/libs/) 40 files

3.2 The Two Custom Widget Layers

darktable has two internal GTK widget frameworks that are critical to understand before any migration work begins.

The bauhaus system (src/bauhaus/bauhaus.c, 4,098 lines + bauhaus.h, 432 lines) is the workhorse UI component for image processing parameters. It implements three widget types — a slider, a combo box, and a button — all rendered from scratch on a GtkDrawingArea using Cairo. It handles its own mouse, keyboard, and scroll input via raw GdkEvent structures, and manages a custom popup window for the expanded slider/combobox view. Every single image processing module (all 105 of them in src/iop/) uses bauhaus widgets. This system is the single largest migration risk in the entire codebase.

The dtgtk widget library (src/dtgtk/, 30 files, ~17,800 lines) provides icon buttons, gradient sliders, range selectors, thumbnail widgets, and painting utilities. It subclasses GTK base types and adds Cairo-rendered icons via a function-pointer pattern (DTGTKCairoPaintIconFunc). The paint.c file alone defines 80+ icon painting functions. Several dtgtk widgets subclass GtkEventBox (which is removed in GTK4) and must be rebased.

3.3 What Is Surprisingly Unproblematic

No OpenGL-GTK binding. darktable's GPU processing pipeline (OpenCL) does not use GtkGLArea or any direct GTK-OpenGL bridge. The GPU work happens entirely outside GTK. This eliminates one of the most painful GTK4 migration scenarios.

Modern GObject declarations. The custom widgets already use the G_DECLARE_FINAL_TYPE / G_DEFINE_FINAL_TYPE macros introduced to modernise GObject code. These macros are GTK4-compatible.

GtkTreeView is not an emergency. While GTK4 deprecates GtkTreeView in favour of GtkListView/GtkColumnView, it still compiles and runs under GTK4. The ~32 files using it (tagging, filtering, collection browser, metadata, styles, masks) do not block the migration.


4. The Seven Migration Categories

Category 1 — Event System Rewrite (CRITICAL)

Affected files: 73–84
Occurrences: ~499 GdkEvent type references + ~219 deprecated signal connections
Effort share: ~35% of total migration work

Every callback connected to "button-press-event", "button-release-event", "motion-notify-event", "key-press-event", "key-release-event", "scroll-event", and similar signals must be refactored. The bauhaus system is the epicentre: it bypasses GTK's signal system and polls GdkEvent structures directly in its own event loop. The darkroom canvas (src/views/darkroom.c) is the second largest problem, as it implements pixel-accurate tablet/stylus interactions that depend on event field access.

Each widget that previously handled raw events needs one or more GtkEventController objects created, connected, and added via gtk_widget_add_controller(). This is not a mechanical transformation — the new gesture objects have different semantics (e.g., GtkGestureDrag encapsulates a full press-move-release sequence that was previously three separate handlers).

Category 2 — bauhaus Custom Widget System (CRITICAL)

Files: src/bauhaus/bauhaus.c (4,098 lines), src/bauhaus/bauhaus.h
Effort share: ~25% of total migration work

The bauhaus system needs a near-complete rewrite of its input handling layer. Its draw callbacks need updating from the "draw" signal pattern to gtk_drawing_area_set_draw_func(). Its popup window lifecycle interacts with the window system in ways that changed in GTK4 (GdkWindow → GdkSurface, window hints API). The global dt_bauhaus_t state struct uses GTK3 types that need updating. Because all 105 IOP modules depend on bauhaus, any bauhaus bug will break the entire darkroom.

Category 3 — Container and Packing API (HIGH)

Affected files: 68+
Occurrences: ~631
Effort share: ~10% of total migration work

gtk_box_pack_start() and gtk_box_pack_end()gtk_box_append() / gtk_box_prepend(). gtk_container_add() → widget-specific gtk_xxx_set_child() or append calls. gtk_widget_show_all() → explicit visibility management. These are largely mechanical changes but are spread across the entire codebase. src/libs/modulegroups.c alone has 119 packing calls.

Category 4 — GtkEventBox Removal (HIGH)

Affected files: src/dtgtk/range.c, src/dtgtk/icon.c, parts of src/gui/gtk.c
Effort share: ~5% of total migration work

Widgets subclassing GtkEventBox need to be rebased. In GTK4, the simplest replacement is GtkBox (which now receives events natively via controllers) or GtkWidget itself (for truly custom widgets). The range selector and icon widgets need their base class changed and their event handling migrated to controllers simultaneously.

Category 5 — Menu System (HIGH)

Affected files: ~28
Occurrences: ~360
Effort share: ~10% of total migration work

All context menus and popup menus must be rebuilt using GtkPopoverMenu with GMenu models. This is a conceptual change: GTK3 menus were widget trees you built imperatively; GTK4 menus are data models (like XML action maps) rendered by the framework. The migration for each menu involves defining a GMenu, attaching GAction objects to the widget's action group, and replacing the old construction code. Files most affected: src/libs/tagging.c, src/libs/masks.c, src/libs/collect.c, src/develop/blend_gui.c.

Category 6 — Drawing Callbacks (MEDIUM)

Affected files: ~104 (Cairo usage), ~74 (explicit queue_draw)
Effort share: ~7% of total migration work

The "draw" signal pattern needs updating to gtk_drawing_area_set_draw_func(). The cairo drawing code inside the callbacks is largely unchanged — only the registration mechanism differs. The function signature changes slightly (receives explicit width/height). gtk_widget_queue_draw() and gtk_widget_queue_draw_area() still exist and work.

Category 7 — Application Lifecycle and Miscellaneous (MEDIUM)

Affected files: primarily src/gui/gtk.c (4,787 lines), src/main.c
Effort share: ~8% of total migration work

  • gtk_main()g_application_run() / GtkApplication
  • gtk_widget_destroy()gtk_window_destroy() (for windows) or g_object_unref() (for non-windows)
  • gtk_dialog_* changes (GtkDialog is deprecated but still works)
  • GdkWindow references → GdkSurface
  • Build system: FindGTK3.cmake → GTK4 pkg-config or cmake module
  • CSS themes: selector/property audit for GTK4 compatibility

5. Migration Strategy and Phasing

Guiding Principles

  1. Keep it working at every phase. The codebase should compile and run (even if imperfectly) after each phase. Do not attempt a "big bang" replacement.
  2. Start at the foundation, not the leaves. Fix infrastructure (build system, lifecycle, widget base classes) before touching the application logic.
  3. Defer non-blocking deprecations. GtkTreeView, GtkDialog, and GtkFileChooser are deprecated but functional in GTK4. Leave them for a later pass.
  4. Test bauhaus in isolation first. Build a minimal test harness for the bauhaus widget system before attempting full integration.

Recommended Phases

Phase 0 — Environment Setup (1 week)

  • Add GTK4 as a build dependency alongside GTK3
  • Create a feature-flagged build path (e.g., -DUSE_GTK4=ON) that compiles against GTK4 headers
  • Fix all compilation errors without changing behavior
  • Identify the exact GTK4 version floor (GTK4 4.10+ recommended for stability)
  • Deliverable: codebase compiles against GTK4 with errors/warnings documented

Phase 1 — Mechanical Fixes (2–3 weeks)

  • gtk_box_pack_start/endgtk_box_append/prepend (automated, 631 calls)
  • gtk_container_add → widget-specific append/set_child (automated)
  • gtk_widget_show_all → explicit gtk_widget_set_visible (requires semantic check: is "show all" actually needed?)
  • gtk_widget_destroy → correct GTK4 destructor per context
  • gtk_main → GtkApplication structure in src/gui/gtk.c
  • Build system update: CMakeLists.txt, FindGTK4.cmake
  • Deliverable: all Phase 1 patterns replaced; application starts up (UI may be broken)

Phase 2 — Custom Widget Base Classes (2–3 weeks)

  • Rebase GtkEventBox-derived widgets in src/dtgtk/ onto GTK4-compatible parents
  • Update G_DEFINE_TYPE patterns where base class was removed
  • Migrate draw signal registration to gtk_drawing_area_set_draw_func() across all dtgtk widgets
  • Deliverable: dtgtk widgets compile and render correctly; events still not working

Phase 3 — Event System Migration (4–5 weeks)

  • Rewrite bauhaus event handling: press/release/motion/scroll → GtkGesture controllers
  • Rewrite darkroom canvas event handling (src/views/darkroom.c) — most complex file
  • Migrate all other views: lighttable, map, slideshow
  • Migrate dtgtk range selector, thumbnail, and culling event handlers
  • Deliverable: basic interaction works; tablet/pressure events tested separately

Phase 4 — Menu System Replacement (2 weeks)

  • Define GMenu models for all context menus
  • Wire GAction implementations for each menu item
  • Replace GtkMenu construction with GtkPopoverMenu
  • Deliverable: all context menus functional

Phase 5 — Integration Testing and Hardening (3–4 weeks)

  • Full functional testing across all views (darkroom, lighttable, map, print, slideshow)
  • CSS theme audit: fix any broken selectors or properties
  • Tablet/stylus testing
  • macOS-specific testing (GTK4 on macOS has historically lagged behind Linux)
  • Performance profiling: GTK4's rendering pipeline differs and some drawing paths may need optimisation
  • Deliverable: release-quality GTK4 build

Total Timeline: 14–18 weeks


6. Effort Estimate with AI Assistance

The estimate below assumes working with an AI coding assistant (such as myself) on a dedicated branch, with the human developer providing feedback, running builds, and doing visual/functional testing.

Phase Duration Primary Actor
Phase 0 — Environment setup 1 week AI (build scripts) + Human (testing)
Phase 1 — Mechanical fixes 2–3 weeks AI (can automate ~90%)
Phase 2 — Custom widget bases 2–3 weeks AI (~70%) + Human review
Phase 3 — Event system 4–5 weeks AI + Human (design decisions, testing)
Phase 4 — Menu system 2 weeks AI (~60%) + Human review
Phase 5 — Integration testing 3–4 weeks Human primary, AI support
Total 14–18 weeks

Without AI assistance, double the estimate (28–36 weeks for an experienced GTK developer).


7. What I Can Do Autonomously vs. What Requires You

I Can Do Fully (or Near-Fully) Autonomously

  • Build system migration: Update CMakeLists.txt, find/replace GTK3 package with GTK4, update compiler flags. This is deterministic.
  • Mechanical API replacements: gtk_box_pack_startgtk_box_append, gtk_container_add → appropriate append, gtk_widget_destroy variants, gdk_window_get_*gdk_surface_get_*. I can write scripts to do these globally and verify each file.
  • Draw callback migration: Changing g_signal_connect(w, "draw", ...) to gtk_drawing_area_set_draw_func(...) is pattern-matching work I can do file by file.
  • dtgtk widget base class changes: Given clear GTK4 documentation, I can rebase GtkEventBox subclasses onto GtkBox or GtkWidget and add the appropriate controllers.
  • CSS theme audit and fixes: GTK4 CSS changes are documented and finite; I can go through each theme file systematically.
  • GtkMenu → GtkPopoverMenu: For straightforward context menus with fixed items, I can write the GMenu model and GAction wiring.
  • Application lifecycle (gtk_mainGtkApplication): This is a well-documented, one-time structural change I can do in src/gui/gtk.c.
  • gtk_widget_show_all elimination: Replace with explicit per-widget visibility, with appropriate default-visible logic.

What Requires Your Involvement

  • Visual regression testing: I cannot see the rendered UI. After each phase, you need to run the application and verify that panels, buttons, sliders, icons, and menus look and behave correctly. GTK4 changes subtleties in layout, spacing, and theming that only visual inspection reveals.

  • Tablet and stylus pressure handling: darktable has specific support for Wacom and other pressure-sensitive input. GTK4's input device model changed. I can write the code using GtkEventControllerLegacy or GdkDeviceTool, but I cannot verify it works without a physical device and a Linux/Windows test environment.

  • bauhaus design decisions: The bauhaus popup system (the floating window that appears when you click a slider) interacts with the window manager in non-trivial ways. GTK4's surface and subsurface model changed how transient windows work. There will be edge cases (popup positioning, grab behaviour, keyboard focus) that require interactive testing and design decisions about acceptable tradeoffs.

  • Darkroom canvas interactions: The main editing canvas implements pixel-precise interactions (mask drawing, crop handles, color picking). These are the most complex event handling scenarios in the codebase. I can port the code, but only you can verify that the interactions feel right.

  • macOS-specific issues: GTK4 on macOS (via the Quartz backend or via XWayland) has historically been less stable than on Linux. You will need to test on your target macOS versions, and some issues may require platform-specific workarounds that are hard to anticipate without a running build.

  • Performance regression identification: GTK4 renders differently (it uses GSK, a scene graph, which can be better or worse than GTK3's direct Cairo rendering depending on the use case). If the UI becomes sluggish — especially the thumbnail view and the darkroom canvas — profiling and optimisation require running the application.

  • Final acceptance testing: Regression testing across all modules, all views, all export paths, all shortcuts. I can write a test plan, but execution requires you.


8. Necessary Compromises

A pragmatic GTK4 migration for darktable will need to accept some deliberate deferments and imperfections. These are not failures — they are engineering choices that keep the project moving.

8.1 Keep GtkTreeView (for now)

GTK4 deprecated GtkTreeView but did not remove it. The replacement (GtkListView + GtkColumnView with factory-based item rendering) is a complete conceptual overhaul. Replacing GtkTreeView across the 32 files that use it (tagging, filtering, collection browser, metadata, export, styles, masks) would add 4–6 weeks to the migration and risks introducing regressions in complex list interactions. The pragmatic choice is to leave GtkTreeView in place for the initial GTK4 release and plan a separate follow-up.

8.2 GtkDialog and GtkFileChooser Remain as-is

These are deprecated in GTK4 (the replacements are GtkAlertDialog and GtkFileDialog, which use async patterns). They still work. Replace them in a subsequent release.

8.3 CSS Visual Regression Acceptance

Some of the 31 CSS files use GTK3-specific selectors or properties that may not map perfectly to GTK4. The safe approach is to accept minor visual differences (spacing, border radii, colour accuracy) in the first GTK4 release and refine the themes iteratively based on user feedback. A full CSS visual-parity effort is a separate task from the migration itself.

8.4 bauhaus May Need a Simplified Intermediate State

The bauhaus popup positioning and grab logic was written to exploit GTK3 internal behaviour. A pragmatic first port may need to simplify the popup (e.g., use a GtkPopover instead of a raw GtkWindow) even if the UX is slightly different, in order to ship a working release. Full fidelity can be restored incrementally.

8.5 No New GTK4 Features in v1

GTK4 offers genuinely better primitives: GtkListView for large lists, native drag-and-drop with content providers, accessibility improvements, Vulkan rendering. None of these should be pursued during the migration itself — the goal of the migration pass is functional parity, not improvement. Improvements come after the codebase is stable on GTK4.


9. Appendix: Key Files by Category

Foundation (start here)

bauhaus System (highest risk)

Custom Widget Library

Views (complex event handling)

Libraries with Heavy Menu/Tree Usage

IOP Modules (uniform pattern, bauhaus-dependent)

Themes


This assessment was produced by static analysis of the darktable source tree. All line and file counts are approximate. The effort estimates assume a single developer working with an AI assistant; parallel team effort would reduce calendar time proportionally.

Metadata

Metadata

Assignees

No one assigned

    Labels

    scope: UIuser interface and interactions

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions