Desktop SWT
The quarkus-desktop-swt extension makes Eclipse SWT applications work in JVM mode
and as GraalVM native executables: the native widgets of Windows, GTK and macOS (Cocoa), graphics, images, text and
layouts, with an application model built on CDI. The extension starts the user interface once the application started,
runs the event loop of SWT on the main thread, stops the application when its last shell closes, and gives the ways to
reach the user interface thread from any thread (CompletableFuture, Mutiny, CDI events, an interceptor), without
Display.asyncExec boilerplate.
It does not include quarkus-desktop-awt (nor the Quarkus core extension quarkus-awt): SWT draws with the toolkit of
the platform. It covers plain SWT: JFace and the Eclipse Rich Client Platform are not covered. An application may use
AWT or Swing with SWT, see Using AWT or Swing with SWT, and embed JavaFX with Quarkus FX, see Using JavaFX with SWT.
The API is in the io.quarkiverse.desktop.swt package:
| API | What for |
|---|---|
|
Observed to create the user interface, on the user interface thread (see Starting the user interface) |
|
Runs the user interface from a |
|
The |
|
Static helpers: |
|
Runs the methods of a bean (a presenter) on the user interface thread, whatever thread calls them (see @RunOnUiThread) |
|
Destroys a |
|
The quit requests of the |
Installation
<dependency>
<groupId>io.quarkiverse.desktop</groupId>
<artifactId>quarkus-desktop-swt</artifactId>
<version>0.2.1</version>
</dependency>
SWT is a jar per platform. The extension depends on org.eclipse.platform:org.eclipse.swt, whose jar is empty: its pom
has profiles, activated by the operating system and the architecture of the build, that add the SWT jar of the platform,
org.eclipse.platform:org.eclipse.swt.<ws>.<os>.<arch> (for instance org.eclipse.swt.win32.win32.x86_64,
org.eclipse.swt.win32.win32.aarch64, org.eclipse.swt.gtk.linux.x86_64, org.eclipse.swt.gtk.linux.aarch64 or
org.eclipse.swt.cocoa.macosx.aarch64). Maven applies them: nothing else is needed. The architecture of the build is
the one of the JVM running Maven, not of the machine: on Windows on arm64, an arm64 JDK gets
org.eclipse.swt.win32.win32.aarch64, and an x64 JDK, which runs under the x64 emulation of Windows,
org.eclipse.swt.win32.win32.x86_64. Run the application with a JVM of the same architecture (see Starting the user interface).
Gradle does not apply these profiles: add the SWT jar of the platform yourself, with the SWT version of the extension (3.132.0, see SWT versions), for instance chosen from the operating system and the architecture of the build:
build.gradle)def os = System.getProperty("os.name").toLowerCase()
def arch = System.getProperty("os.arch") in ["aarch64", "arm64"] ? "aarch64" : "x86_64"
def swtPlatform = os.contains("win") ? "win32.win32." + arch
: os.contains("mac") ? "cocoa.macosx." + arch
: "gtk.linux." + arch
dependencies {
implementation("io.quarkiverse.desktop:quarkus-desktop-swt:0.2.1")
implementation("org.eclipse.platform:org.eclipse.swt." + swtPlatform + ":3.132.0")
}
build.gradle.kts)val os = System.getProperty("os.name").lowercase()
val arch = if (System.getProperty("os.arch") in listOf("aarch64", "arm64")) "aarch64" else "x86_64"
val swtPlatform = when {
os.contains("win") -> "win32.win32.$arch"
os.contains("mac") -> "cocoa.macosx.$arch"
else -> "gtk.linux.$arch"
}
dependencies {
implementation("io.quarkiverse.desktop:quarkus-desktop-swt:0.2.1")
implementation("org.eclipse.platform:org.eclipse.swt.$swtPlatform:3.132.0")
}
The build logs a warning when the application has no SWT jar of a platform, and a native build fails without the SWT jar of the platform of the executable (see The SWT jar and container builds).
How it works
In JVM mode, SWT needs nothing special: it extracts its native libraries into ~/.swt/lib/<os>/<arch> on the first run,
and loads them from there. The extension adds the application model (see Writing an SWT application), and enables the native
access of SWT: the runner jar declares Enable-Native-Access: ALL-UNNAMED in its manifest, and the descriptor of the
extension gives --enable-native-access=ALL-UNNAMED to the JVM of dev mode, so that the JDK (24 and later) does not
warn about the System.load calls of SWT. On macOS, the JVM needs -XstartOnFirstThread (see macOS).
For native executables, the extension adds:
-
The run time initialization of SWT (the
org.eclipse.swtpackages): its static initializers load its native libraries and call them. -
The run time initialization of the classes whose static initializer uses SWT, in the application and in its libraries (whether they are in the Jandex index or not): a class with a
static final RGB ACCENT = new RGB(0, 150, 201)(or aPoint, aFontData…) is initialized at run time, as in JVM mode, instead of failing the native build. -
The JNI metadata of SWT, computed from the class files of the SWT jar of the application, since it differs between platforms and SWT versions: the struct classes whose fields the native code of SWT reads and writes (
RECT,MSG,GdkRectangle,NSRect…), the callback methods that it calls (Callback, the COM objects on Windows, the callbacks of the Edge WebView2 browser on Windows), and a few fixed JNI members. -
What SWT looks up by name: the
SWTMessagesresource bundle (the messages of SWT, for the locales ofquarkus.locales), the style sheets of SWT for the GTK themes on Linux, and theDisplayclass (with reflection). -
The native libraries of SWT, embedded in the executable or copied next to it (see Native libraries).
-
A check of the SWT jar: it must be the jar of the platform of the executable (see The SWT jar and container builds).
-
On Windows, the subsystem of the executable (see Windows).
-
With
--exact-reachability-metadata, the lookups of SWT expected to fail (see Exact reachability metadata).
The native image needs no configuration from the application for SWT itself.
Writing an SWT application
import io.quarkiverse.desktop.swt.SwtStartupEvent;
@ApplicationScoped (1)
public class Library {
public List<String> titles() {
return List.of("Dune", "Emma", "Ulysses");
}
}
@Singleton (2)
public class MainWindow {
@Inject
Library library; (3)
@ConfigProperty(name = "app.title", defaultValue = "Library")
String title;
private Shell shell;
void open(@Observes SwtStartupEvent event) { (4)
shell = new Shell(event.display()); (5)
shell.setText(title);
shell.setLayout(new FillLayout());
org.eclipse.swt.widgets.List titles = new org.eclipse.swt.widgets.List(shell, SWT.BORDER | SWT.V_SCROLL);
library.titles().forEach(titles::add);
shell.setSize(320, 240);
shell.open(); (6)
}
public Shell shell() {
return shell;
}
}
| 1 | The services of the application are ordinary beans, of any scope. |
| 2 | The bean that creates the main shell. It is not a widget: SWT widgets are created in methods, with their parent (see
Widgets and beans). A @Singleton (or @ApplicationScoped) bean keeps its shell; a @Dependent observer would be
destroyed right after the event. |
| 3 | It gets beans and configuration injected, as any bean. |
| 4 | Called once, on the user interface thread, once the Display is created and before the event loop runs. |
| 5 | The Display of the application (also UiThread.display()). |
| 6 | The application stops when its last shell closes (see Closing shells and exit). |
No @QuarkusMain is needed: without one, the extension gives the application a main that runs the user interface on the
main thread until the application stops. ./mvnw quarkus:dev runs the application in dev mode (on Windows and Linux,
see Dev mode), java -jar target/quarkus-app/quarkus-run.jar runs it in production (on
macOS: java -XstartOnFirstThread -jar target/quarkus-app/quarkus-run.jar), and ./mvnw package -Dnative builds a
native executable (see Native executables).
Starting the user interface
SWT runs its event loop on the thread that creates the Display (on macOS, it must be the first thread of the
process): the extension runs it on the main thread. An application that observes SwtStartupEvent with @Observes is
a user interface application: without a @QuarkusMain, its main (generated by the extension) calls
SwtLifecycle.run(), which:
-
creates the
Displayon the main thread, which becomes the user interface thread, once the application started (after theStartupEventobservers); -
fires
SwtStartupEventthere, synchronously: the event loop starts once its observers return, so that the listeners they add to theDisplayget the first events (for instanceSWT.OpenDocument, the files opened with the application on macOS, the one it was launched with included); -
runs the tasks queued before the start (
UiThreadExecutor,@RunOnUiThread,UiThread.runfrom another thread), in order (when the application stops before the user interface runs, they are rejected, see The user interface thread); -
runs the event loop until the application stops, then disposes the
Display(see Closing shells and exit).
@Singleton
public class OpenedDocuments {
@Inject
Documents documents;
void listen(@Observes SwtStartupEvent event) {
event.display().addListener(SWT.OpenDocument, e -> documents.open(Path.of(e.text)));
event.display().addListener(SWT.OpenUrl, e -> documents.open(URI.create(e.text))); // not on macOS, see below
}
}
On macOS, the URLs opened with the application (a URL scheme of its bundle) never reach the SWT.OpenUrl listeners
with SWT 3.132.0 and 3.135.0, a bug of SWT (see macOS).
@Priority orders several observers. Keep them short, they delay the first shell: load the data on another thread (see
Background work).
When an observer throws (an exception or an error, such as the LinkageError of a native executable missing some
metadata), the error is logged and, in production and in a @QuarkusMainTest, the application stops with exit code 1,
instead of running without a shell. When the Display cannot be created, the error is logged, with a hint on Linux
(GTK 3 and a display), on macOS (the first thread of the process) and when the native libraries of SWT do not load: a
system library missing on Linux, which the hint names (GTK 3 is not installed, mostly), the SWT jar of another
operating system or architecture (the hint names the jar to use), or the native libraries that an executable of
another architecture left in ~/.swt/lib/<os>/<arch> (or in the directory of swt.library.path, when it is set),
which SWT never replaces (the hint names the directory); without one otherwise on Windows; in production and in a
@QuarkusMainTest, the application
stops with exit code 1. In dev mode and in a @QuarkusTest, the errors are only logged. In JVM mode, the extension
compares the SWT-OS and SWT-Arch attributes of the manifest of the SWT jar with the JVM before it creates the
Display, and reports a manifest without them (an uber-jar, see Limitations): SWT would exit the JVM itself (exit
code 1), with its own message only ("Libraries for platform <ws> cannot be loaded because of incompatible
environment"). In a native executable, the error is an UnsatisfiedLinkError.
The build logs a warning for an @ObservesAsync observer of SwtStartupEvent, which is never notified. In tests, the
user interface does not run unless it is enabled (see Testing).
A @QuarkusMain
A @QuarkusMain runs the user interface itself, with SwtLifecycle.run(), once its work before the user interface is
done: the arguments, a single instance lock, a login, or a batch mode without user interface.
import io.quarkiverse.desktop.swt.SwtLifecycle;
@QuarkusMain
public class Main implements QuarkusApplication {
@Inject
SwtLifecycle lifecycle;
@Inject
Exporter exporter;
@Override
public int run(String... args) throws Exception {
if (args.length == 2 && args[0].equals("--export")) { (1)
exporter.export(Path.of(args[1]));
return 0;
}
lifecycle.run(); (2)
return 0; (3)
}
}
| 1 | A batch mode, which may run without display: the user interface never starts (the tasks queued for it are rejected when the application stops, see The user interface thread). |
| 2 | Creates the Display on this thread (the main thread), fires SwtStartupEvent, and runs the event loop until the
application stops: the last shell closed, a quit request, Quarkus.asyncExit(), a signal. It blocks, unlike
DesktopLifecycle.start() of the AWT extension: no Quarkus.waitForExit(). |
| 3 | Runs once the Display is disposed. When the stop comes from Quarkus.asyncExit() (the last shell closed, a quit
request, the application calling it), it runs before the ShutdownEvent observers. On a signal (Ctrl+C, SIGTERM),
Quarkus stops the application on its shutdown thread: the ShutdownEvent observers may run at the same time as this
code. The exit code given to Quarkus.asyncExit(code), when there is one, wins over the returned one. |
SwtLifecycle.run() returns at once when the user interface is disabled (tests, see Testing) and when the
application is stopping (an exit requested before, or the user interface already ran). In a @QuarkusTest, where the
user interface runs on the thread quarkus-desktop-swt-ui of the extension, it blocks until the application stops.
Otherwise, it throws an IllegalStateException when the user interface already runs.
An exit requested before run() (a StartupEvent observer, or the main, calling Quarkus.asyncExit()) makes run()
return without creating the Display. A thread of the extension, started with the application, waits for the exit:
when it only wakes up once run() started, the user interface starts and stops at once.
The build logs a warning when a user interface application (observing SwtStartupEvent) has @QuarkusMain classes and
none of them, nor any QuarkusApplication implementation of the index, mentions SwtLifecycle in its class file: the
user interface would never start. It is a heuristic: it looks for a reference to SwtLifecycle, not for a call to
run().
With another extension that provides the main of the application (Picocli), its main replaces the one of this
extension: the application calls SwtLifecycle.run() itself, on the main thread (for instance from the Picocli
command). Verified with Picocli on Windows, Linux x64 and macOS, in JVM mode and as a native executable. Quarkus FX
does not replace it: the main of this extension runs the user interface, and JavaFX runs embedded in SWT (see
Using JavaFX with SWT).
Closing shells and exit
The application stops (Quarkus.asyncExit()) when its last visible shell is closed or hidden, once a first shell
opened, as the Desktop AWT extension does with its windows. Closing a shell disposes it (the default of SWT), and a
hidden shell (setVisible(false)) counts as closed. The shells are counted in a later task, so a listener may dispose a
shell and open another one. The application also stops when it disposes the Display itself: the event loop ends and
stops it (the shells disposed with the Display are not counted as closed).
Whatever stops the application (Quarkus.asyncExit(), the last shell closed, a quit request, a signal such as Ctrl+C
or SIGTERM, a dev mode restart, the end of a @QuarkusTest), the user interface stops before the ShutdownEvent
observers and the destruction of the beans. On the user interface thread, the event loop ends, then:
-
the shells are disposed: their
SWT.Disposelisteners and the@PreDestroymethods of theWidgetBeansbeans run; -
the events and tasks already handed to the
Displayrun (10,000 at most): the shells are gone, checkisDisposed(); -
the
Displayis disposed.
Then the ShutdownEvent observers run. Their calls to UiThread.run, UiThread.call, UiThreadExecutor and
@RunOnUiThread methods are rejected at once (RejectedExecutionException): a void @RunOnUiThread call is skipped
(with a debug log), and the stage of a CompletionStage one completes exceptionally.
When the application stops on another thread (a signal such as Ctrl+C or SIGTERM, a dev mode restart, the end of a
@QuarkusTest) while the user interface thread is blocked (a long task, a lock), the stop waits for it 10 seconds,
logs a warning, and the application stops without disposing the Display. A stop through Quarkus.asyncExit() (the
last shell closed, a quit request, the application calling it) is carried out by the user interface thread itself,
once it returns to the event loop: nothing waits, and a user interface thread that never returns keeps the application
running (a signal then stops it, with the time-out above). System.exit() called on the user interface thread stops
the application without disposing the Display either, and without waiting: call Quarkus.asyncExit() instead. After
a stop that timed out or a System.exit(), the calls of the ShutdownEvent observers are rejected at once too, as
after any stop. The tasks already handed to the Display and the shells are left as they are: after System.exit()
for good, after a time-out until the user interface thread returns to the event loop, if it does before the process
ends (in dev mode and in tests, where the process goes on), and then at the same time as the ShutdownEvent
observers. Until then the shells are not disposed: their SWT.Dispose listeners do not run, and the @Dependent beans
of WidgetBeans are destroyed by the container at the end of the shutdown, after the ShutdownEvent observers, on the
shutdown thread.
Their @PreDestroy methods run there and must not use the widgets: they are not disposed, and a widget (or the
Display) called from another thread throws an SWTException. Check UiThread.isUiThread() (isDisposed() is not
enough, see Widget beans).
A shell that asks before closing:
@Singleton
public class EditorWindow {
@Inject
Documents documents;
void open(@Observes SwtStartupEvent event) {
Shell shell = new Shell(event.display());
shell.setText("Editor");
shell.addListener(SWT.Close, e -> { (1)
if (documents.hasUnsavedChanges()) {
MessageBox box = new MessageBox(shell, SWT.ICON_QUESTION | SWT.YES | SWT.NO);
box.setText("Close");
box.setMessage("Close without saving?");
e.doit = box.open() == SWT.YES;
}
});
shell.addListener(SWT.Dispose, e -> documents.saveBounds(shell.getBounds())); (2)
shell.setSize(640, 480);
shell.open();
}
}
| 1 | Closing the shell (its close button, Alt-F4) sends SWT.Close: the shell stays open when a listener sets doit to
false, and is disposed otherwise. The last shell closed stops the application. |
| 2 | Save the state of the user interface (bounds, open documents) in SWT.Close or SWT.Dispose listeners, on the user
interface thread: the ShutdownEvent observers and @PreDestroy methods run once the Display is disposed, on other
threads. SWT.Dispose also runs when the application stops for another reason (on macOS, Cmd-Q does not close the
shells, see Quit requests), unless the stop times out or System.exit() is called on the user interface thread. |
An application that stays alive without shells (a tray icon application, or the macOS convention of an application
without window) disables the exit policy, and calls Quarkus.asyncExit() itself (from a Quit menu item, for instance):
quarkus.desktop.swt.exit-on-last-shell-closed=false
The exit policy is also for a splash screen: open the main shell before closing the splash screen, or the application stops in between (or disable the policy). It is never installed in tests.
Quit requests
QuitRequest is fired on the user interface thread when the Display is asked to close (its SWT.Close event): Quit
in the application menu of macOS, Cmd-Q, Quit in the Dock, a logout or a shutdown of the Mac, the end of the session on
Windows and GNOME, and Display.close() on every platform. The application stops (Quarkus.asyncExit()) unless an
observer cancels the request:
import io.quarkiverse.desktop.swt.QuitRequest;
@Singleton
public class QuitConfirmation {
@Inject
MainWindow window;
@Inject
Documents documents;
void confirm(@Observes QuitRequest request) { // on the user interface thread
if (documents.hasUnsavedChanges()) {
MessageBox box = new MessageBox(window.shell(), SWT.ICON_QUESTION | SWT.YES | SWT.NO);
box.setText("Quit");
box.setMessage("Quit without saving?");
if (box.open() != SWT.YES) {
request.cancel(); // the application keeps running
}
}
}
}
-
Closing the shells is not a quit request: the last shell closed stops the application (see Closing shells and exit), ask for confirmation in a
SWT.Closelistener of the shell. -
An observer that throws (any exception or error) cancels the request (the error is logged): an observer guarding unsaved changes never loses them.
request.event()is theSWT.Closeevent of theDisplay. -
Windows and Linux: when no observer cancels the request, SWT disposes the
Displayand the application stops: at once forDisplay.close(), and for the end of the session only once it ends (WM_ENDSESSIONon Windows, theEndSessionof the session manager on Linux), not when it is queried (WM_QUERYENDSESSION,QueryEndSession, when the request is fired): another application may still refuse the end of the session, and this one then keeps running. When an observer cancels the request, the end of the session does not go on: cancel only when the user asked to. Only what runs while SWT disposes theDisplay(theSWT.Disposelisteners, the@PreDestroymethods of the widget beans) is sure to run before the session ends: theShutdownEventobservers run once SWT answered the end of the session, and a real logout may end the process before they are done (not verified). -
macOS: the extension always answers "cancel" to AppKit (
doit = false), and stops the application itself when the request is not cancelled: SWT would otherwise dispose theDisplayand let AppKit exit the process without the shutdown of Quarkus (theShutdownEventobservers and@PreDestroymethods would not run). A logout, a restart or a shutdown of the Mac is then cancelled by the system (it reports that the application canceled logout) while the application stops, as with the Desktop AWT extension in dev mode (see Application menu, Finder and Dock events): the user repeats it. -
macOS, signals: a
SIGTERMor aSIGINT(Ctrl+C) may also make AppKit ask the application to terminate (-[NSApplication terminate:], run from the main run loop), in JVM mode and in native executables, so theQuitRequestobservers may be called while the application stops anyway. It is a race with the shutdown of Quarkus, seen in about one run in ten (in JVM mode, 4 of 34 runs withSIGTERMand 3 of 28 withSIGINT; in native executables, 1 of 19 and 3 of 17). Cancelling the request has no effect then. An observer must not block the user interface thread (a modal dialog), or the exit waits for the stop time-out, 10 seconds, after which the application stops without disposing theDisplay(see Closing shells and exit).
A cancelled Display.close() is verified on Windows, Linux and macOS (see Verification), and so are, in JVM
mode and as native executables, a Display.close() that is not cancelled (the observers run once, the application
stops with exit code 0 once its shells are disposed) and an observer that throws (the error is logged, the request is
cancelled and the application keeps running). On Windows, the end of the session is verified with the messages that
Windows sends (WM_QUERYENDSESSION, then WM_ENDSESSION) sent to the application, without logging out: the query
alone leaves the application running, WM_ENDSESSION then stops it with exit code 0, an end of the session refused by
another application (WM_ENDSESSION with FALSE) leaves it running, and an observer that cancels makes the query
answer that the session may not end. On macOS, Cmd-Q, Quit in the application menu and in the Dock, a quit Apple
Event sent to an application bundle (tell application id "…" to quit) and a quit request cancelled then repeated
are verified too, in JVM mode and as native executables: the observers run once per request, the application stops
through Quarkus with exit code 0, and the sender of the Apple Event gets "User canceled" (-128). Not verified: a real
end of the session (a logout) on every platform.
The user interface thread
SWT widgets are created and used on the user interface thread, the thread that created the Display (SWT throws an
SWTException, "Invalid thread access", on other threads). With the extension, it is the main thread of the
application, the thread calling SwtLifecycle.run() (in @QuarkusTest tests, a thread of the extension, see
Testing). The SwtStartupEvent and QuitRequest observers, the listeners of the widgets and of the Display, the
timers (Display.timerExec), the tasks of Display.asyncExec and the @RunOnUiThread methods run there; everything
else (StartupEvent observers, REST resources, @Scheduled methods, messaging, ShutdownEvent observers) runs on
other threads. The extension gives three ways to get to the user interface thread:
-
UiThreadExecutor, the executor of the user interface thread: continue there after work done on another thread; -
UiThread, static helpers, including the only call that waits for the user interface thread; -
@RunOnUiThread, an interceptor binding: the methods of a bean always run there, whatever thread calls them.
Unlike Display.asyncExec, they work before the user interface starts: the tasks queued then (for instance by a
StartupEvent observer) run once it started, after the SwtStartupEvent observers, in order. When the application
stops before the user interface runs (a @QuarkusMain returning without calling SwtLifecycle.run(), an exit
requested before run(), a Display that cannot be created, a SwtStartupEvent observer disposing the Display, a
@QuarkusTest on macOS), these tasks are rejected: UiThread.call throws a RejectedExecutionException, the stage of
a @RunOnUiThread method returning a CompletionStage completes exceptionally with it, and the void
@RunOnUiThread calls and the tasks of UiThread.run and UiThreadExecutor are dropped. Once the user interface
stopped (before the ShutdownEvent observers, see Closing shells and exit), the calls are rejected at once.
Never block the user interface thread (a database query, an HTTP call, a file read): the user interface freezes.
Background work
Run the work on a Quarkus executor and continue on the user interface thread with UiThreadExecutor, an Executor
bean that always queues the task (Display.asyncExec), in order, even when called on the user interface thread:
import io.quarkiverse.desktop.swt.UiThreadExecutor;
@Singleton
public class BooksWindow {
@Inject
BookRepository repository;
@Inject
ManagedExecutor workers; (1)
@Inject
UiThreadExecutor ui; (2)
private Shell shell;
private org.eclipse.swt.widgets.List books;
private Label status;
void open(@Observes SwtStartupEvent event) {
shell = new Shell(event.display());
shell.setLayout(new GridLayout());
books = new org.eclipse.swt.widgets.List(shell, SWT.BORDER | SWT.V_SCROLL);
books.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
status = new Label(shell, SWT.NONE);
status.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
shell.setSize(400, 300);
shell.open();
refresh();
}
void refresh() { // on the user interface thread
status.setText("Loading...");
CompletableFuture.supplyAsync(repository::titles, workers) (3)
.whenCompleteAsync(this::show, ui); (4)
}
void search(String text) { // on the user interface thread
repository.search(text) // a Uni<List<String>>
.emitOn(ui) (5)
.subscribe().with(titles -> show(titles, null), error -> show(null, error));
}
void reload(@ObservesAsync BooksChanged event) { (6)
refresh();
}
private void show(List<String> titles, Throwable error) {
if (shell.isDisposed()) { (7)
return;
}
if (error != null) {
status.setText("Loading failed: " + error.getMessage());
} else {
books.setItems(titles.toArray(String[]::new));
status.setText(titles.size() + " books");
}
}
}
| 1 | The ManagedExecutor of quarkus-smallrye-context-propagation propagates the contexts (CDI request context,
transactions) to the worker threads. Without it, @Inject ExecutorService gets the worker pool of Quarkus. |
| 2 | The only bean type of UiThreadExecutor is UiThreadExecutor: @Inject Executor still gets the worker pool of
Quarkus. |
| 3 | Loads the data on a worker thread. |
| 4 | Continues on the user interface thread, with the result or the error. Once the user interface stopped (the application is stopping), the executor rejects the task and the continuation does not run. |
| 5 | Mutiny: the items and the failure are emitted on the user interface thread. Use runSubscriptionOn(workers) for a
Uni that blocks when subscribed. |
| 6 | Asynchronous events fired with the UiThreadExecutor are notified on the user interface thread, see below. |
| 7 | The shell may be closed while the work runs: a disposed widget throws an SWTException ("Widget is disposed"). |
An asynchronous CDI event fired with NotificationOptions.ofExecutor(ui) notifies its @ObservesAsync observers on the
user interface thread, from any thread:
@ApplicationScoped
public class BookService {
@Inject
BookRepository repository;
@Inject
Event<BooksChanged> booksChanged;
@Inject
UiThreadExecutor ui;
public void save(String title) { // on any thread
repository.save(title);
booksChanged.fireAsync(new BooksChanged(), NotificationOptions.ofExecutor(ui));
}
}
UiThread
UiThread has static helpers:
-
UiThread.isUiThread(): whether the current thread is the user interface thread. -
UiThread.display(): theDisplayof the application. It throws anIllegalStateExceptionwhen the user interface does not run (beforeSwtLifecycle.run(), or once the application stopped). -
UiThread.run(task): runs the task now when called on the user interface thread, later (Display.asyncExec) otherwise. It throws aRejectedExecutionExceptiononce the user interface stopped, or when it is disabled (tests). -
UiThread.call(callable, timeout): the only call that waits for the user interface thread, for background threads and tests. Called on the user interface thread, it calls the task now. It throws aTimeoutExceptioninstead of waiting for ever, the exception of the task, and aRejectedExecutionExceptionwhen the user interface stopped, or stops (or never runs) before the task ran.
Point size = UiThread.call(() -> table.computeSize(SWT.DEFAULT, SWT.DEFAULT), Duration.ofSeconds(5));
UiThread.run(() -> status.setText("Saved"));
When UiThread.call times out, or the waiting thread is interrupted, a task that did not start yet is skipped; a task
already running goes on, and its result is dropped. Never call it while holding a lock that the user interface thread
may need: a lock of the application, or ArC creating a bean (a constructor, a @PostConstruct method, a producer) while
an observer of that bean runs on the user interface thread. Nor on the thread that will run the user interface, before
it runs (a StartupEvent observer, a @QuarkusMain before SwtLifecycle.run()): it waits until the timeout. Nor
from a ShutdownEvent observer, where the user interface stopped: the call is rejected at once.
@RunOnUiThread
@RunOnUiThread runs the methods of a bean on the user interface thread, whatever thread calls them (a @Scheduled
method, a REST resource, a Kafka consumer, an asynchronous observer). Put it on presenters (beans that are not widgets),
or on methods:
import io.quarkiverse.desktop.swt.RunOnUiThread;
public interface StatusView {
void showStatus(String text);
String selectedBook();
}
@Singleton
public class StatusBar implements StatusView { (1)
private Label label;
private org.eclipse.swt.widgets.List books;
void create(Composite parent, org.eclipse.swt.widgets.List books) { // called by the main window
this.label = new Label(parent, SWT.NONE);
this.books = books;
}
@Override
public void showStatus(String text) {
if (label != null && !label.isDisposed()) {
label.setText(text);
}
}
@Override
public String selectedBook() {
return books.getSelectionCount() == 0 ? null : books.getSelection()[0];
}
}
@ApplicationScoped
public class StatusPresenter {
@Inject
StatusView view; (2)
@RunOnUiThread
public void show(String text) { (3)
view.showStatus(text);
}
@RunOnUiThread
public CompletionStage<String> selectedBook() { (4)
return CompletableFuture.completedFuture(view.selectedBook());
}
}
@ApplicationScoped
public class ServerMonitor {
@Inject
StatusPresenter presenter;
@Inject
ServerClient server;
@Scheduled(every = "10s")
void poll() { // on a worker thread
presenter.show(server.status());
}
}
| 1 | The view: a bean that creates its widgets when the main window calls it, on the user interface thread, and implements an interface that the presenter uses (which makes the presenter testable, see Testing). |
| 2 | The view is not a widget: it can be injected directly, creating it creates no widget. |
| 3 | A void method runs now when called on the user interface thread: its exception goes to the caller. Called on
another thread, it runs later (Display.asyncExec): the call returns at once, and an exception thrown then is logged
(see Uncaught exceptions). A call before the user interface starts runs once it started (it is dropped when the user
interface never runs, see The user interface thread); a call once it stopped is skipped. |
| 4 | A method returning a CompletionStage (or a CompletableFuture) returns a stage completed with its result, or its
exception, once it ran on the user interface thread. Called on the user interface thread, it runs now: an exception
gives a failed stage, and a null stage is replaced with a completed one. Once the user interface stopped, or when it
never runs, the stage completes exceptionally with a RejectedExecutionException. |
The build fails for a @RunOnUiThread method returning another type (there is no blocking mode: use UiThread.call),
and for a bean that is not @ApplicationScoped, @Singleton or @Dependent (a @RequestScoped instance belongs to
the request of the caller, which may be over when the method runs). @RunOnUiThread on a class applies to its business
methods; on a widget class it is a build error, annotate the methods (see Build checks). It works on static methods
too, with the same return types (the build fails for a private static method, which ArC does not intercept).
The interceptor of @RunOnUiThread is the outermost one (priority PLATFORM_BEFORE - 100), so the other interceptors
of the method run on the user interface thread, around the method:
| Interceptor | Where it runs with @RunOnUiThread |
|---|---|
|
On the calling thread: it queues the call |
|
On the user interface thread, around the method: a transaction or a retry then blocks the user interface thread. Call
such methods from a worker thread, and |
Fault tolerance |
On the user interface thread: when the timeout expires, it interrupts the user interface thread. The build logs a warning |
Fault tolerance |
The method runs on another thread, not on the user interface thread. The build logs a warning |
The user interface thread has no active request context: use @ActivateRequestContext on a method that needs one. A
call that the Display already queued when the application stops still runs, once the shells are disposed (see
Closing shells and exit).
Uncaught exceptions
An exception or an error that escapes a listener, a timer, a task (Display.asyncExec, UiThreadExecutor,
UiThread.run) or a queued call of a void @RunOnUiThread method is logged at the ERROR level in the
io.quarkiverse.desktop.swt.ui-thread category, with the name of the thread, and the event loop keeps running. The
extension sets the runtime exception handler and the error handler of the Display for it
(Display.setRuntimeExceptionHandler, Display.setErrorHandler): an application may replace them in a
SwtStartupEvent observer. The event loop also gives them the exceptions of the code that SWT runs without them (on
macOS, a timer that fires while the event loop sleeps), and logs the exception when a handler throws (a handler
rethrowing it, as the default ones of SWT, is then called twice for the exceptions that SWT gave it). A checked
exception thrown without being declared (Kotlin, Groovy, Lombok @SneakyThrows), which SWT lets escape from the
listeners and the timers, goes to the runtime exception handler wrapped in an UndeclaredThrowableException, and the
event loop keeps running too. Once the Display is disposed, an exception that SWT throws itself (the top frame of its
stack trace in SWT: on Windows, the WM_ENDSESSION of the end of the session disposes it during PeekMessage, then
SWT throws a NullPointerException, as without Quarkus) is logged at the DEBUG level only, and the event loop ends;
an exception of the application (a listener or a timer that disposes the Display, then fails) still goes to the
handlers. A @RunOnUiThread method called on the user interface thread runs inline: its exception goes to the caller
instead.
# keep the log (and the uncaught exceptions) of a desktop application in a file
quarkus.log.file.enabled=true
# or do not log them
quarkus.log.category."io.quarkiverse.desktop.swt.ui-thread".level=OFF
The errors of the lifecycle (a Display that cannot be created, a failing SwtStartupEvent or QuitRequest observer,
the stop timeout) are logged in the io.quarkiverse.desktop.swt.SwtLifecycle category: turning off the ui-thread
category does not silence them.
Widgets and beans
SWT widgets are not CDI beans, unlike the components of the AWT extension (see Components as
CDI beans): a widget needs its parent when it is created (new Button(parent, SWT.PUSH)), and SWT forbids subclassing
most widget classes (Shell, Button… throw SWTException: Subclassing not allowed; Composite and Canvas may be
subclassed). Beans create the widgets in their methods, on the user interface thread: in a SwtStartupEvent observer, a
listener, a @RunOnUiThread method, or a method that one of them calls.
-
The bean that creates the main shell is a
@Singleton(or@ApplicationScoped) bean that observesSwtStartupEvent(see Writing an SWT application). -
A dialog or a document window opened several times is a
@Dependentbean created withWidgetBeans(see Widget beans). -
The logic goes to presenters and services, of any scope, which use the widgets on the user interface thread (
@RunOnUiThread,UiThreadExecutor) and are easy to test and mock (see Testing).
These beans are not widgets: they can be injected anywhere, and creating them creates no widget.
Widget beans
A @Dependent bean got from an Instance<T> stays referenced by the Instance until it is destroyed: a dialog opened
many times would leak. WidgetBeans.create(instance, factory) gets a bean on the user interface thread, calls the
factory with it to create its widget, and destroys the bean once the widget is disposed (closing a shell disposes it):
its @PreDestroy methods run (stop its timers, remove its global listeners) and its own dependent beans are destroyed.
import io.quarkiverse.desktop.swt.UiThread;
import io.quarkiverse.desktop.swt.WidgetBeans;
@Dependent
public class InvoiceDialog {
@Inject
InvoiceService service;
private final Runnable tick = this::tick;
private Display display;
private Label age;
private Invoice invoice;
Shell open(Shell parent, Invoice invoice) { (1)
this.invoice = invoice;
display = parent.getDisplay();
Shell shell = new Shell(parent, SWT.DIALOG_TRIM | SWT.APPLICATION_MODAL);
shell.setText("Invoice " + invoice.number());
shell.setLayout(new GridLayout());
age = new Label(shell, SWT.NONE);
// the other widgets of the invoice...
tick();
shell.pack();
shell.open();
return shell;
}
private void tick() { // every minute, on the user interface thread
age.setText("Updated " + service.age(invoice) + " ago");
display.timerExec(60_000, tick);
}
@PreDestroy
void close() { (2)
if (UiThread.isUiThread() && display != null && !display.isDisposed()) {
display.timerExec(-1, tick); // cancels the timer
}
}
}
@Singleton
public class InvoicesWindow {
@Inject
InvoiceService service;
@Inject
Instance<InvoiceDialog> dialogs;
void open(@Observes SwtStartupEvent event) {
Shell shell = new Shell(event.display());
shell.setLayout(new GridLayout());
List<Invoice> all = service.findAll();
org.eclipse.swt.widgets.List invoices = new org.eclipse.swt.widgets.List(shell, SWT.BORDER | SWT.V_SCROLL);
all.forEach(invoice -> invoices.add(invoice.number()));
Button edit = new Button(shell, SWT.PUSH);
edit.setText("Edit");
edit.addListener(SWT.Selection, e -> {
if (invoices.getSelectionIndex() >= 0) {
Invoice selected = all.get(invoices.getSelectionIndex());
WidgetBeans.create(dialogs, dialog -> dialog.open(shell, selected)); (3)
}
});
shell.pack();
shell.open();
}
}
| 1 | Called by WidgetBeans.create, on the user interface thread, with the bean just created. The widget that it
returns decides the life of the bean. |
| 2 | Called on the user interface thread when the shell is disposed: closed, or disposed when the application stops
(before the ShutdownEvent observers). When the stop of a signal, a dev mode restart or the end of a test times out, or
after System.exit(), the shell is not disposed:
the container destroys the bean at the end of the shutdown, on the shutdown thread, where a call to the Display
throws an SWTException, hence the check of UiThread.isUiThread() (see Closing shells and exit). |
| 3 | Gets a new InvoiceDialog, creates its shell, and returns it. WidgetBeans.create throws an
IllegalStateException outside the user interface thread. When the factory throws, or returns no widget (or a disposed
one), the bean is destroyed at once. A bean of another scope than @Dependent is used as is, and never destroyed. |
Build checks
The build checks the beans whose type is an SWT widget or device (a Composite subclass, a producer of widgets, a
Display…) and the @RunOnUiThread methods. Errors for what cannot work, warnings for what may not:
| Found | Result | Instead |
|---|---|---|
A normal scoped bean ( |
Build error |
A bean that creates its widgets in a method (see Widgets and beans), or |
An interceptor binding on a widget class ( |
Build error |
Annotate methods, or move the logic to a presenter that is not a widget |
A |
Build error |
Return |
|
Build error |
One of these scopes |
A widget bean injected directly into a |
Build warning |
|
|
Build warning |
Call |
|
Build warning |
See the message |
Platforms
| Platform | JVM mode | Native executable |
|---|---|---|
Windows x64 |
Yes, verified (Windows 11) |
Yes, verified (Windows 11), also with exact reachability metadata: a native build on Windows, with Visual Studio (see Windows). |
Windows arm64 |
Yes, verified (Windows 11 in Parallels Desktop on an Apple silicon Mac, with arm64 JDKs 17 and 25) |
No arm64 executable: GraalVM has no native image builder for Windows on arm64. An x64 executable, built with an x64 GraalVM under the x64 emulation of Windows, runs there under emulation, verified (see Windows). |
Linux x64 |
Yes, verified (Ubuntu 24.04 in Docker, GTK 3, X11 under Xvfb, and Wayland, a headless Weston) |
Yes, verified with exact reachability metadata, in the same environments: a native build on Linux (see Linux, and The SWT jar and container builds for a build in a container). |
Linux arm64 |
Yes, verified (Ubuntu 24.04 in Docker on an Apple silicon Mac, GTK 3, X11 under Xvfb) |
Yes, verified with exact reachability metadata, in the same environment. A native executable also ran on Wayland (a headless Weston), and one built in a container from macOS ran too (see The SWT jar and container builds). |
macOS (Apple silicon) |
Yes, with |
Yes, verified (macOS 27.0), also with exact reachability metadata: the event loop runs on the first thread of the process (see macOS). |
macOS x86_64 |
Not verified (the SWT jar exists) |
Not verified. |
Verified means the integration tests of the extension, run locally in JVM mode and as native executables (see Verification).
macOS
On macOS (Cocoa), SWT creates the Display and runs its event loop on the first thread of the process. The extension
runs the event loop on the main thread:
-
JVM mode: start the JVM with
-XstartOnFirstThread(java -XstartOnFirstThread -jar target/quarkus-app/quarkus-run.jar), which runsmainon the first thread. Without it, theDisplaycannot be created: the application logs an error with a hint and stops with exit code 1. -
Native executables:
mainruns on the first thread of the process, and so does the event loop. Do not build them with-H:+RunMainInNewThread, which movesmainto another thread (the build logs a warning). When the application also has the Desktop AWT extension and an SWT user interface (aSwtStartupEventobserver), the first thread is not parked for AWT (quarkus.desktop.awt.macos.park-main-threadis ignored): SWT owns it (see Using AWT or Swing with SWT). -
A
@QuarkusMaincallsSwtLifecycle.run()on the thread of itsrunmethod: never on another thread. -
Dev mode does not run the user interface on macOS: it runs the application on another thread than the first one (see Dev mode). Test the user interface with
@QuarkusMainTestand-XstartOnFirstThread(see Testing). -
The name of the application in the menu bar and the Dock is
quarkus.desktop.swt.application-name, given toDisplay.setAppNamebefore theDisplayis created (see Configuration). -
Cmd-Q, Quit in the application menu and in the Dock, a logout and a shutdown fire
QuitRequestand stop the application through Quarkus, and so may aSIGTERMor aSIGINT(see Quit requests). The other items of the application menu (About, Settings) are SWT menu items (Display.getSystemMenu(),SWT.ID_ABOUTandSWT.ID_PREFERENCES: verified on macOS), and the files opened with the application areSWT.OpenDocumentevents of theDisplay(see Starting the user interface), not CDI events. The URLs opened with the application (a URL scheme of the bundle) never reachSWT.OpenUrlwith SWT 3.132.0 and 3.135.0: SWT registers its delegate method asapplication:openUrls:, while AppKit callsapplication:openURLs:(a bug of SWT). -
An application bundle that signs its content keeps the native libraries of SWT next to the executable (
quarkus.desktop.swt.native-libraries=next-to-executable, see Native libraries).A native executable started from an application bundle by Launch Services ( open, the Dock, Finder) may wait about 35 seconds at its first log record: the logging of Quarkus resolves the host name (io.smallrye.common.net.HostName,InetAddress.getLocalHost, an mDNS lookup of<name>.local), and the Local Network privacy of macOS holds the query of an ad hoc signed bundle (it stays pending, and macOS may ask the user to allow it).-Djboss.qualified.host.name=<name>, in the launcher script of the bundle or as an argument of the executable, avoids the lookup: the executable then starts in milliseconds. The bundles running the application in JVM mode did not wait, and the wait was only seen with the screen locked: with the screen unlocked, the same native bundle started at once.
The native executable declares the minimum macOS version and the SDK version of the java launcher of the JDK used for
the native build (otool -l shows them in LC_BUILD_VERSION, for instance minos 11.0 and sdk 14.5): it starts on
the macOS versions the JDK supports, and AppKit gives its windows and controls the same look as in JVM mode (AppKit
chooses the size of the controls, the height of the title bars and the background color of the widgets, for instance,
from the SDK version of the executable). Without it (quarkus.desktop.swt.macos.jdk-build-version=false), the linker
writes the version of the SDK of the Xcode tools as both, for instance 26.5: the executable only starts on that macOS
version and later, and draws the controls of that macOS version. With the Desktop AWT extension, that extension writes
the versions instead (quarkus.desktop.awt.macos.jdk-build-version, see Using AWT or Swing with SWT).
JVM mode and native executables are verified on macOS 27.0 on Apple silicon (see Verification), not on Intel Macs.
Native executables
Build a native executable as usual (./mvnw package -Dnative), on the platform of the executable: the SWT jar of the
application must be the one of that platform (see The SWT jar and container builds).
Windows
The native build needs Visual Studio (the C++ build tools), as any Quarkus native build on Windows, and must run on Windows: a container build produces a Linux executable.
On Windows on arm64, GraalVM has no native image builder: an x64 GraalVM builds an x64 executable, under the x64
emulation of Windows, and the executable runs under emulation too. Run Maven itself with that GraalVM (JAVA_HOME and
GRAALVM_HOME), so that the profiles of org.eclipse.swt add org.eclipse.swt.win32.win32.x86_64, and the C++
build tools for x64 (vcvarsall.bat arm64_amd64, the arm64 compiler for x64, is enough). With an arm64 JDK running
Maven, the profiles add the aarch64 jar and the native build fails (see The SWT jar and container builds); adding the x86_64 jar
as a dependency without excluding the aarch64 one does not help, the build then fails with both jars (excluding it as
for a container build is not verified). Verified with the integration tests of the extension on Windows 11 arm64.
Unlike AWT, the executable needs no application manifest and no Visual C++ runtime: SWT activates the visual styles of Windows and the DPI awareness itself, and its native libraries only link system libraries of Windows.
The executable is a console application by default: set quarkus.desktop.swt.windows.subsystem=windows for an
application without console window (as javaw). Such an executable has no standard output and error streams when it is
not started from a console: log to a file (quarkus.log.file.enabled=true).
Linux
The executable uses GTK 3, as SWT does in JVM mode (libgtk-3-0t64 on Ubuntu 24.04), and the display of the DISPLAY
or WAYLAND_DISPLAY environment variable (X11, XWayland or Wayland; X11 under Xvfb is verified on x64 and arm64, and
Wayland, a headless Weston with GDK_BACKEND=wayland and no DISPLAY, on x64 in JVM mode and with native executables,
and on arm64 with native executables). Tests need a display: use a virtual one (xvfb-run).
GLCanvas needs X11: SWT draws it with GLX only, and on a Wayland display its constructor throws an SWTException
"Unsupported color depth" (in JVM mode as in native executables, an SWT limitation, also in SWT 3.135.0). On a Wayland
desktop, an application using it runs on XWayland with the environment variable GDK_BACKEND=x11 (in its launcher
script or its .desktop file): verified on Linux x64 with the XWayland of a headless Weston, in JVM mode and with
native executables.
The SWT jar and container builds
A native build fails when the SWT jar of the application is not the one of the platform of the executable, with a
message naming the jar to use. A local native build fails too when the architecture of the jar is not the one of the
native image builder (the OS_ARCH of the release file of the GraalVM used for the build, the architecture of the JVM
running the build when it is unknown): the executable could never load the native libraries of SWT, and running it would
leave them in ~/.swt/lib/<os>/<arch>, where SWT never replaces a file, so that the other SWT applications of that
version would load them too. A container build or a native sources build (quarkus.native.sources-only=true) only logs
a warning then: the builder image of a container may run another architecture than the build host, and the sources may
be compiled later on a machine of the architecture of the jar. With several SWT jars on the class path (typically a
container build that does not exclude the SWT jar of the build host), a native build fails, listing them; JVM mode logs
a warning and uses the first one. A native build in a container (-Dquarkus.native.container-build=true) produces a
Linux executable, also from Windows or macOS, where Maven adds the SWT jar of the build host: use the Linux jar in a
Maven profile of the container build instead. The Linux jar depends on org.eclipse.swt, whose profiles would add the
jar of the build host again: exclude it there too (the dependency resolution is verified on Windows; on an Apple silicon
Mac, the container build with this profile, its jar replaced with org.eclipse.swt.gtk.linux.aarch64 as said below,
produced a Linux arm64 executable that runs, and failed without it, naming the jar to use; a container build from
Windows is not verified):
<profile>
<id>container-build</id>
<activation>
<property>
<name>quarkus.native.container-build</name>
<value>true</value>
</property>
</activation>
<dependencies>
<dependency>
<groupId>io.quarkiverse.desktop</groupId>
<artifactId>quarkus-desktop-swt</artifactId>
<version>0.2.1</version>
<exclusions>
<!-- the SWT jar of the build host -->
<exclusion>
<groupId>org.eclipse.platform</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.eclipse.platform</groupId>
<artifactId>org.eclipse.swt.gtk.linux.x86_64</artifactId>
<version>3.132.0</version>
<exclusions>
<!-- org.eclipse.swt, and the SWT jar of the build host that its profiles add -->
<exclusion>
<groupId>org.eclipse.platform</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</profile>
On an Apple silicon Mac, the container builds a Linux executable for arm64: use org.eclipse.swt.gtk.linux.aarch64.
Native libraries
By default (quarkus.desktop.swt.native-libraries=embedded), the native libraries of the SWT jar
(swt-win32-<version>.dll, swt-gdip-win32-<version>.dll… and WebView2Loader.dll on Windows,
libswt-gtk-<version>.so… on Linux, libswt-cocoa-<version>.jnilib and libswt-pi-cocoa-<version>.jnilib on macOS)
are resources of the native executable: a single file to distribute. SWT extracts them on the first run into
~/.swt/lib/<os>/<arch> (or into the directory of the swt.library.path system property, when it is set), as it does
in JVM mode, and loads them from there.
With quarkus.desktop.swt.native-libraries=next-to-executable, the libraries are not embedded in the executable: the
native build copies them next to it, and the executable loads them from its directory (it sets the swt.library.path
system property before the StartupEvent observers; a swt.library.path set by the user wins). Nothing is written to
the home directory, and the libraries can be signed with the executable (a macOS application bundle). Distribute them
with the executable. Only a full native build copies them, not a build of the native sources
(quarkus.native.sources-only=true). When they are missing next to the executable, it logs a warning, and SWT looks
for them in java.library.path and ~/.swt, and fails when it does not find them there.
The library of the SWT_AWT bridge is never included (see Limitations).
Exact reachability metadata
With the GraalVM option --exact-reachability-metadata (see Exact
reachability metadata for what it changes), the extension registers the lookups of SWT expected to fail: the
BrowserInitializer class that the Browser widget looks for, and on Windows the classes of the Java plug-in of
Internet Explorer that it probes. These registrations are added when quarkus.native.additional-build-args or
quarkus.native.additional-build-args-append contains --exact-reachability-metadata (or
-H:ThrowMissingRegistrationErrors).
The application registers its own lookups in a
src/main/resources/META-INF/native-image/<group>/<artifact>/reachability-metadata.json file, as the integration tests
of the extension do for the configuration files that Quarkus looks up (application.properties,
application-prod.properties, META-INF/microprofile-config.properties).
Application resources and locales
The images and other resources that the application loads from the class path
(new Image(display, getClass().getResourceAsStream("/icons/open.png"))) are included with
quarkus.native.resources.includes, as in any Quarkus native executable. The messages of SWT are included for the
locales of the native executable: set quarkus.locales (and quarkus.default-locale) to the languages of the
application.
SWT versions
The extension depends on org.eclipse.platform:org.eclipse.swt 3.132.0, the last SWT release that runs on Java 17, the
Java version of these extensions: SWT 3.133.0 and later need Java 21. An application on Java 21 or later can use a
later SWT release: declare org.eclipse.platform:org.eclipse.swt with that version, as a dependency or in its
dependency management (with Gradle, the version of the SWT jar of the platform too):
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.eclipse.platform</groupId>
<artifactId>org.eclipse.swt</artifactId>
<version>3.135.0</version>
</dependency>
</dependencies>
</dependencyManagement>
Never use a swt.version property for it, neither a system property nor a Maven property (-Dswt.version=…
on the command line of Maven reaches the tests): SWT reads the swt.version system property as the version of its
native libraries (for instance 4971r15), and then fails to load them.
|
The extension computes the JNI metadata of native executables from the SWT jar of the application (see How it works): it follows the SWT version. The extension is verified with SWT 3.132.0, and with SWT 3.135.0 on Linux x64 and macOS, in JVM mode and as native executables, and on Windows arm64 in JVM mode (see Verification).
Dev mode
./mvnw quarkus:dev runs the application in dev mode on Windows and Linux: a restart runs the application again in the
same JVM, on its new main thread, and the user interface with it, once the previous Display is disposed.
SwtStartupEvent is fired again, to the new beans, which open the shells again. Verified on Windows and on Linux x64
(under Xvfb), and on Windows arm64 with an arm64 JDK (without widget beans): [s] disposes the shells (their
SWT.Dispose listeners and the @PreDestroy methods of the widget beans run), runs the ShutdownEvent observers,
disposes the Display, then starts the application again with the code changed meanwhile, and [q] quits. On Linux,
GTK logs a Gdk-CRITICAL message about gdk_threads_set_lock_functions at each restart: SWT sets the lock functions
of GDK each time it creates a Display, GTK takes them once per process, and nothing else changes. Dev mode does not
run the user interface on macOS, where SWT needs the first thread of the process: dev mode runs the application on
another thread (the Display cannot be created, an error is logged). Verified on macOS: the error is logged with its
hint, dev mode keeps running, and q quits it; -Djvm.args=-XstartOnFirstThread changes nothing, since dev mode runs
the application on its Quarkus Main Thread, not on the first thread.
What a restart disposes: the user interface stops as for any other stop, before the ShutdownEvent observers (see
Closing shells and exit): the shells are disposed, and the @Dependent beans of WidgetBeans with them, the tasks already handed to
the Display run, then the Display is disposed (the shutdown waits for the user interface thread 10 seconds at
most).
What stays: SWT and its native libraries (loaded once per JVM), and what the application started outside the
Display: its threads, its executors. Stop them in @PreDestroy methods, or they keep the code of the previous run
alive.
Restarts and exit: [s] in the console forces a restart, and dev mode looks for changes of the sources when the
application gets an HTTP request, with an HTTP extension (see Dev mode of the AWT application
model). When the last shell closes, the application stops (Quarkus.asyncExit()) and dev mode keeps running: press
[space] in the console to start the application again.
Testing
In tests (@QuarkusTest, @QuarkusMainTest), the user interface does not run by default: SwtStartupEvent is not
fired, SwtLifecycle.run() returns at once, and the tasks for the user interface thread are rejected: UiThread.run
(from another thread), UiThread.call and UiThreadExecutor throw a RejectedExecutionException, the void
@RunOnUiThread calls are skipped, and the stage of the CompletionStage ones completes exceptionally with a
RejectedExecutionException. The beans (services, presenters) are tested as in any Quarkus application.
quarkus.arc.test.disable-application-lifecycle-observers=true disables the user interface even when it is enabled.
User interface tests enable it (for all the tests, or in a test profile), and need a display (on Linux, run them on a
virtual display: xvfb-run ./mvnw verify):
%test.quarkus.desktop.swt.startup-event.enabled=true
-
@QuarkusTest: no main runs, so the extension runs the user interface on a thread of its own, namedquarkus-desktop-swt-ui, once the application started. Not on macOS, where theDisplayneeds the first thread of the process (a warning is logged): use@QuarkusMainTestthere. -
@QuarkusMainTestruns the main of the application (the generated one, or the@QuarkusMain), hence the user interface on the thread of the main, as in production. The exit policy is never installed in tests: the application stops by itself (Quarkus.asyncExit()), as the integration tests of the extension do. On macOS, add-XstartOnFirstThreadto theargLineof the Surefire plugin (verified on macOS). -
@QuarkusIntegrationTestand@QuarkusMainIntegrationTestrun the packaged application (a jar or a native executable) as in production: the user interface runs, with the exit policy. On macOS, a jar needsquarkus.test.arg-line=-XstartOnFirstThread(for instance-Dquarkus.test.arg-line=-XstartOnFirstThreadon the command line of Maven), which Quarkus adds to thejavacommand that runs it; a native executable needs nothing (verified on macOS).
import io.quarkiverse.desktop.swt.UiThread;
@QuarkusTest
class MainWindowTest {
@Inject
MainWindow window; (1)
@Test
void opens() throws Exception {
assertEquals("Library", UiThread.call(() -> window.shell().getText(), Duration.ofSeconds(5))); (2)
}
}
| 1 | The bean that creates the main shell is not a widget: it is injected as any bean. |
| 2 | UiThread.call runs after the SwtStartupEvent observers when it is queued before the start, and reads the
widgets on the user interface thread. |
Run the test classes that start the user interface in JVMs of their own: SWT loads its native libraries once per JVM,
and allows one Display at a time. With Surefire, one JVM per test class:
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<reuseForks>false</reuseForks>
</configuration>
</plugin>
After running Maven with a JDK of another architecture (an x64 and an arm64 JDK on Windows on arm64, and likely an
x86_64 JDK under Rosetta on macOS), build with clean: the Quarkus tests reuse the application model that a previous
build resolved (target/quarkus/bootstrap), with the SWT jar of the other architecture, and the Display cannot be
created (verified on Windows arm64, where deleting target/quarkus/bootstrap fixes it too).
Presenters: make them depend on view interfaces rather than on the classes that create the widgets, and test them
with @QuarkusComponentTest (quarkus-junit-component), which mocks the views, without display. A component test runs
the beans without the Quarkus extensions: the interceptor of @RunOnUiThread is not there, and the methods run on the
test thread (see Testing of the AWT application model for an example).
Using AWT or Swing with SWT
An application may depend on quarkus-desktop-swt and on quarkus-desktop-awt (or quarkus-desktop-swing). This
combination is verified with Swing on Windows, Linux x64 and macOS, in JVM mode and as a native executable (on macOS
built with Quarkus 4.0.0.Beta1, as macOS native executables with AWT need, see macOS). Each extension runs
its own user interface thread, with its own API: the event dispatch thread of AWT (DesktopStartupEvent,
EdtExecutor, @RunOnEdt, see Application model and CDI) and the user interface thread of SWT
(SwtStartupEvent, UiThreadExecutor, @RunOnUiThread). On Windows and Linux, the two threads run side by side. On
macOS, SWT owns the first thread of the process: when the application has an SWT user interface (a SwtStartupEvent
observer: the Desktop SWT extension then provides the main), the Desktop AWT extension does not park the first thread
for AWT in native executables. When another extension provides the main (Picocli), the Desktop AWT extension cannot
tell: set quarkus.desktop.awt.macos.park-main-thread=false.
-
Each exit policy counts the windows of its own toolkit (the AWT one the AWT windows of an application observing
DesktopStartupEvent, the SWT one the shells), but either one stops the whole application: closing the last SWT shell stops it even with AWT windows open, and the reverse. Disable one of them (quarkus.desktop.swt.exit-on-last-shell-closed=falseorquarkus.desktop.awt.exit-on-last-window-closed=false). -
On macOS, the event dispatch thread waits for the first thread for some AWT calls (
Window.dispose()asks AppKit for the locale of the input method there), and the first thread runs them only while the SWT event loop runs. Once the loop ended, aShutdownEventobserver running on the first thread (after the last shell closed orQuarkus.asyncExit(), the shutdown runs on the main thread) blocks when it waits for the event dispatch thread: in JVM mode and in a native executable, an observer that disposed aJFrameon the event dispatch thread and waited for it gave up after its 2 second time-out. Dispose the AWT windows while the event loop runs (in aSWT.Closelistener of the last shell, for instance), or do not wait for the event dispatch thread at shutdown. -
Both extensions have a
QuitRequestclass, in different packages (io.quarkiverse.desktop.swt.QuitRequestandio.quarkiverse.desktop.awt.QuitRequest): observe the one of the toolkit that receives the request. -
Both extensions have a
windows.subsystemproperty: either one set towindowsmakes the executable a Windows subsystem one, without console window. -
Both extensions have a
macos.jdk-build-versionproperty: the Desktop AWT extension writes the minimum macOS version and the SDK version of the executable, andquarkus.desktop.awt.macos.jdk-build-versiondecides it (quarkus.desktop.swt.macos.jdk-build-versionis ignored, see macOS).
The SWT_AWT bridge (org.eclipse.swt.awt.SWT_AWT, AWT components in SWT shells) is not supported in native executables
(see Limitations).
Using JavaFX with SWT
With Quarkus FX, JavaFX runs embedded in SWT, in javafx.embed.swt.FXCanvas
widgets: Quarkus FX does not provide its main when io.quarkiverse.desktop.swt.SwtLifecycle is on the class path, and
the user interface is the SWT one, run by the main of this extension (or by a @QuarkusMain that calls
SwtLifecycle.run(), see A @QuarkusMain). The first FXCanvas starts JavaFX on the user interface thread of SWT,
which becomes the JavaFX application thread. Quarkus FX also turns off the implicit exit of JavaFX, and registers the
FXCanvas interop for native executables. See
JavaFX embedded in SWT in the Quarkus FX documentation.
This needs a version of Quarkus FX that includes this support. Earlier versions provide their main too, as an
overridable producer of QuarkusApplicationClassBuildItem like this extension, and Quarkus rejects the application
(ChainBuildException: "Multiple overridable producers").
Limitations
-
The SWT_AWT bridge is not supported in native executables: its native library needs the
jawtlibrary of AWT, and is not embedded in the executable. -
The
Browserwidget is verified with Edge WebView2 on Windows (the defaultBrowserof SWT there, andSWT.EDGE), WebKitGTK on Linux x64 (X11 and Wayland) and WebKit on macOS, andGLCanvason Windows, Linux x64 (X11 only, see Linux) and macOS, in JVM mode and in native executables, also with exact reachability metadata: a page loaded withsetText,evaluate, and aBrowserFunctioncalled from JavaScript; aGLCanvasmade current, its buffers swapped, resized and disposed (no OpenGL drawing: that needs an OpenGL binding library, not covered). Without WebKitGTK on Linux,new Browser(…)throws anSWTError, as in JVM mode. -
JFace and the Eclipse Rich Client Platform are not covered: the extension supports plain SWT.
-
An uber-jar (
quarkus.package.jar.type=uber-jar) drops the manifest of the SWT jar, and SWT would then exit the JVM: it loads its native libraries only from a jar whose manifest declaresSWT-OSandSWT-Arch. The extension reports it and stops the application first (see Starting the user interface). Use the default fast-jar packaging. -
Dev mode does not run the user interface on macOS (see Dev mode), and SWT 3.132.0 and 3.135.0 never fire
SWT.OpenUrlthere (see macOS). -
Classes that the application or its libraries load by name need their own native image metadata, for instance with
@RegisterForReflection. Please report the features of SWT that fail in a native executable. The native image tracing agent of GraalVM gives the missing metadata:$GRAALVM_HOME/bin/java -agentlib:native-image-agent=config-output-dir=src/main/resources/META-INF/native-image/<group>/<artifact> -jar target/quarkus-app/quarkus-run.jar(on macOS, with-XstartOnFirstThreadafterjava), and a native executable built with exact metadata and run with-XX:MissingRegistrationReportingMode=Warnlists them (see Exact reachability metadata).
Verification
The integration tests of the extension run a @QuarkusMain application that calls SwtLifecycle.run() on the main
thread, in JVM mode (@QuarkusMainTest) and as a native executable, with three scenarios:
-
The application model:
SwtStartupEventon the main thread, the tasks queued before the start, the order of theUiThreadExecutortasks,@RunOnUiThreadmethods (voidandCompletionStage, with an exception),UiThread.call,WidgetBeans(a@Dependentbean destroyed with its shell, rejected outside the user interface thread), a cancelled quit request (Display.close()), and the order of the shutdown throughQuarkus.asyncExit()(theDisplaydisposed before theShutdownEventobservers). -
SWT: the environment (platform, DPI, monitors, system font), a class whose static initializer creates SWT objects, a gallery of widgets rendered with
Control.print, and graphics drawn into an image (shapes, antialiasing, alpha, text), without uncaught exceptions nor errors on the standard error. The checks install their own handlers on theDisplay: the logging of the uncaught exceptions by the extension (see Uncaught exceptions) is not exercised. The native executable renders the same pixels as JVM mode. -
The exit policy (the packaged application only: the native executable, or the jar with
-DskipITs=false; the policy is never installed in a@QuarkusMainTest): the application stops when its only shell closes.
The tests of the deployment module check the build, and the user interface in tests without a main (as in a
@QuarkusTest): among others, the shutdown order when the test ends, and the rejection of the tasks queued for a user
interface that never runs.
They were run locally, with GraalVM for JDK 25 (on Windows arm64, which has no GraalVM of its architecture, with the arm64 builds of OpenJDK 17 and 25 of Microsoft, and, under emulation, an x64 Oracle JDK 25 and an x64 GraalVM CE for JDK 25):
-
on Windows 11 x64, in JVM mode and as native executables, also built with exact reachability metadata (
-Dquarkus.native.additional-build-args=--exact-reachability-metadata), with SWT 3.132.0, together with the cycles of the SWT variant of the showcase, plus a probe application run in JVM mode and as native executables (also with exact reachability metadata): quit requests (aDisplay.close()cancelled or not, an observer that throws), the end of the session (WM_QUERYENDSESSIONandWM_ENDSESSIONsent to the application, cancelled or not), theBrowserwidget (Edge WebView2) andGLCanvas, Picocli, Swing with SWT, and dev mode; -
on Linux x64, Ubuntu 24.04 in Docker (GTK 3, X11 under Xvfb), the deployment tests and the integration tests in JVM mode and as native executables built with exact reachability metadata, with SWT 3.132.0 and 3.135.0, the integration tests on Wayland too (a headless Weston), plus the probe application, in JVM mode and as native executables (also with exact reachability metadata): quit requests, the
Browserwidget (WebKitGTK, also on Wayland) andGLCanvas(on X11, and on XWayland withGDK_BACKEND=x11: on Wayland it fails, see Linux), Picocli, Swing with SWT, and dev mode, together with the cycles of the SWT variant of the showcase (JVM mode, native, exact reachability metadata); -
on macOS 27.0 on Apple silicon, the deployment tests and the integration tests in JVM mode (also on Java 17, and against the packaged jar), and as native executables, also built with exact reachability metadata, with SWT 3.132.0 and 3.135.0, together with the cycles of the SWT variant of the showcase (JVM mode, native, exact reachability metadata), plus a probe application run in JVM mode and as native executables: the signals,
System.exit(), a blocked user interface thread at the stop, uncaught exceptions, the exit policy, quit requests (aDisplay.close()cancelled or not, an observer that throws, a quit Apple Event), the native libraries next to the executable in a signed application bundle,SWT.OpenDocument, theBrowserwidget (WebKit) andGLCanvas, Picocli, Swing with SWT, dev mode, and the SWT jar of another architecture; -
on Linux arm64, Ubuntu 24.04 in Docker on that Mac, the deployment tests and the integration tests in JVM mode and as native executables built with exact reachability metadata, a native executable on Wayland (a headless Weston), and a native executable built in a container from macOS (see The SWT jar and container builds);
-
on Windows 11 arm64, in Parallels Desktop on an Apple silicon Mac (a display at 200 %), with the arm64 builds of OpenJDK 17 and 25 of Microsoft, which get
org.eclipse.swt.win32.win32.aarch64: the deployment tests and the integration tests in JVM mode, also against the packaged jar, with SWT 3.132.0, and 3.135.0 on Java 25, together with two JVM runs of the SWT variant of the showcase (they match, with 4 checks failing in both runs on that display, see the showcase README), plus a probe application in JVM mode on Java 25: quit requests (aDisplay.close()cancelled or not), the end of the session (WM_QUERYENDSESSIONandWM_ENDSESSIONsent to the application, cancelled or not), theBrowserwidget (Edge WebView2, also withSWT.EDGE) andGLCanvas, the SWT_AWT bridge, Picocli, Swing with SWT, the exit policy, dev mode, and the SWT jar of another architecture (an x64 JVM, under emulation, with theaarch64jar, and an arm64 JVM with thex86_64jar). With an x64 Oracle JDK 25 under emulation (thex86_64jar), the deployment tests and the integration tests in JVM mode, also against the packaged jar, with SWT 3.132.0, and the probes of quit requests, the end of the session,Browser,GLCanvas, the SWT_AWT bridge and the exit policy, pass too, and so do the integration tests with an x64 native executable, built with an x64 GraalVM under emulation (see Windows), without the comparison with JVM mode (-Dnative.surefire.skip).
The continuous integration runs them on Windows x64 and Linux, in JVM mode and as native executables, and on macOS in
JVM mode and as native executables (a job of its own, also with exact reachability metadata, that builds the SWT
modules alone: the AWT and Swing native executables of macOS need Quarkus 4.0.0.Beta1 or later and GraalVM 25.1 or
later, see macOS). Its native jobs skip the JVM mode tests (-Dnative.surefire.skip): it does not
compare the rendering of the native executable with JVM mode.
Not verified: macOS on Intel Macs, a Retina display and the dark appearance (every macOS run used a display at scale
1, in the light appearance), the name of the application in the Dock as shown on screen (only as reported by
lsappinfo, the menu bar is verified on screen), Windows arm64 on a physical machine (only in a virtual machine) and
with a display at 100 %, on Windows arm64 the probe application, dev mode and the showcase on Java 17, the x64 native
executables there beyond the integration tests (with exact reachability metadata, the probe application, their
rendering compared with JVM mode), on Linux arm64 dev mode, the
Browser widget and GLCanvas, Picocli, Swing with SWT and quit requests, a real end of the session (a logout) on
every platform, the Linux session manager (QueryEndSession, EndSession), AWT (rather than Swing) with SWT, and
Quarkus FX (JavaFX embedded in SWT, see Using JavaFX with SWT).
The Quarkus Desktop showcase has an SWT
variant: -Dswt builds it into target/swt/, and java tools/Cycle.java <label> --swt compares its JVM mode and
native runs (see the showcase README, and Verification).
Configuration
See the overview of the properties. The run time properties
(quarkus.desktop.swt.startup-event.enabled, quarkus.desktop.swt.exit-on-last-shell-closed,
quarkus.desktop.swt.application-name) also apply in JVM mode; quarkus.desktop.swt.native-libraries,
quarkus.desktop.swt.windows.subsystem and quarkus.desktop.swt.macos.jdk-build-version only apply to native
executables.
quarkus.desktop.swt.application-name defaults to quarkus.application.name as it was when the application was built:
overriding quarkus.application.name at run time does not change it, set quarkus.desktop.swt.application-name
instead. The extension also gives quarkus.application.version to Display.setAppVersion. In tests, where
quarkus.application.name and quarkus.application.version are not set, SWT gets neither (unless
quarkus.desktop.swt.application-name is set).
Configuration property fixed at build time - All other configuration properties are overridable at runtime
Configuration property |
Type |
Default |
|---|---|---|
Where the native executable finds the native libraries of SWT (
Environment variable: |
|
|
The subsystem of the native executable : Environment variable: |
|
|
Whether the native executable declares the minimum macOS version and the SDK version of the macOS does not start an executable on a version older than its minimum version, and AppKit chooses the look of the windows and controls (the height of the title bars, the size of the controls, the background color of the widgets for instance) from its SDK version : the executable starts on the macOS versions that the JDK supports, and looks as in JVM mode. When disabled, the linker writes the version of the SDK of the Xcode tools as both : the executable then only starts on that macOS version and later, and gets the look of that version. With the Desktop AWT extension, Environment variable: |
boolean |
|
Whether the user interface runs. By default it does, except in tests ( Environment variable: |
boolean |
|
Whether the application stops ( Environment variable: |
boolean |
|
The name of the application, given to SWT ( Environment variable: |
string |