Coming from SWT
This page is for someone who already writes SWT, with or without JFace. The overall shape carries over: a window holds a tree of controls, events arrive as listeners, one thread owns the interface, and slow work goes elsewhere. What changes is that Limn draws its own widgets instead of wrapping the system’s, a control is made on its own and added to a container instead of being born inside a parent with style bits, there is nothing to dispose, and a list or a table reads your own objects without content and label providers.
Each section below shows the SWT code first and the Limn code after it, and links to the page of this guide that says the rest.
The ideas, side by side
Section titled “The ideas, side by side”| SWT and JFace | Limn |
|---|---|
Display |
the backend: new LwjglBackend() |
Shell |
a NativeWindow from backend.createWindow(…), and a Scene bound to it |
the readAndDispatch loop |
backend.runEventLoop() |
Composite with a Layout |
a layout widget: Column, Row, Grid, Wrap, Stack |
GridLayout and GridData |
Grid; Expanded and columnAlignment in place of grab and fill |
RowLayout |
Row, or Wrap when it wraps |
FillLayout |
a Column or a Row whose children are each Expanded |
FormLayout, StackLayout |
no counterpart for attachments; scene.setRoot(…) or Stack for stacking |
new Button(parent, SWT.PUSH) |
new Button("…"), added to its container |
SWT.CHECK, SWT.RADIO, SWT.TOGGLE |
Checkbox, RadioButton, Checkbox.Variant.SWITCH |
Label, Text, Text with SWT.PASSWORD, SWT.MULTI |
Label, TextField, PasswordField, TextArea |
Combo, CCombo |
ComboBox, editable or not |
List, Table, Tree |
ListView, Table, Tree |
JFace TableViewer, TreeViewer and their providers |
Table with a column per value, Tree with a Tree.Model |
ScrolledComposite, SashForm, TabFolder, CTabFolder |
ScrollView, SplitPane, TabbedPane |
ToolBar, ProgressBar, Scale, Slider, Spinner |
ToolBar, ProgressBar, Slider, Spinner |
DateTime |
DateField, DatePicker |
ColorDialog |
ColorPicker, ColorPickerButton |
Menu with SWT.BAR, SWT.CASCADE, SWT.POP_UP |
MenuBar, Menu, PopupMenu |
MenuItem.setAccelerator(SWT.MOD1 + 'S') |
MenuItem.setAccelerator(Accelerator.command(Keys.S)) |
MessageBox, JFace MessageDialog |
Dialog |
FileDialog, DirectoryDialog |
window.fileDialogs() |
setToolTipText |
setTooltip |
display.asyncExec |
Ui.post |
display.timerExec |
Ui.postDelayed, which returns a handle that cancels it |
a worker thread, or an Eclipse Job |
Ui.work(…), which returns a Job you can cancel |
SWT.Selection listener |
onAction on a button, onSelect on a choice |
ModifyListener |
onChange for the user’s changes, observeChanges for every change |
Canvas with a PaintListener and GC |
a widget of your own with onPaint(Canvas) |
redraw(), layout() |
invalidate(), markNeedsLayout(); a built-in widget does both for itself |
Color, Font, Image with dispose() |
plain values, collected like any object |
| the system’s look | Theme |
DragSource, DropTarget, Clipboard |
drag and drop and the clipboard (Drag and drop) |
Printer, PrintDialog |
Document: saved as PDF, printed, previewed (Printing and PDF) |
Tray, TrayItem |
Tray |
Program.launch |
window.launcher() |
The differences that matter
Section titled “The differences that matter”Limn draws its own widgets. An SWT button is the system’s button; a Limn button is drawn
by Limn, the same on every system, in the palette of the current Theme. What stays the
system’s own is what a user expects to be: the menu bar on macOS, the file choosers, the print
dialog and the tray. Screen readers are still told everything: Limn speaks to each system’s
accessibility interface itself (Accessibility).
A widget is made on its own. There is no parent argument and no style bits. A widget is
created, configured with setters, and added to a container, and it can be taken out and put
somewhere else later. What a style bit chose becomes a class (PasswordField), a variant
(Checkbox.Variant.SWITCH) or a setter.
Nothing to dispose. Colours, fonts and images are plain Java values, and a window’s widgets
go when the window does. There is no dispose() to forget and no “widget is disposed”
exception to chase.
A handler answers the user. onChange, onAction, onSelect and the other handlers run
when the user acts, not when your code calls setText or setSelectedIndex. An SWT
ModifyListener fires for both. When you do want every change, the code’s included,
observeChanges is the watcher that hears them all.
No providers. A Table takes your List<Order> and a column for each thing to show, each a
function that reads one order. That function is the content provider and the label provider
at once: setRows with a new list, or refresh() after changing the objects in place.
The thread rule is the same. A widget setter called from another thread throws at once, as
SWT’s “Invalid thread access” does. There is no syncExec: hand a result to the UI thread with
Ui.post, or use Ui.work, whose callbacks already run there.
main runs the window. backend.runEventLoop() is the readAndDispatch loop, written once
by the toolkit, and returns when the application ends. On macOS the thread has to be the
process’s first one, which is why the JVM is started with -XstartOnFirstThread there, as for
SWT.
A window
Section titled “A window”In SWT:
public static void main(String[] args) { Display display = new Display(); Shell shell = new Shell(display); shell.setText("Hello, SWT"); shell.setSize(480, 320); GridLayout layout = new GridLayout(1, false); layout.marginWidth = layout.marginHeight = 24; layout.verticalSpacing = 12; shell.setLayout(layout); new Label(shell, SWT.NONE).setText("A window, drawn by SWT."); Button close = new Button(shell, SWT.PUSH); close.setText("Close"); close.addListener(SWT.Selection, e -> shell.close()); shell.open(); while (!shell.isDisposed()) { if (!display.readAndDispatch()) { display.sleep(); } } display.dispose();}In Limn:
import limn.backend.Backend;import limn.backend.NativeWindow;import limn.backend.WindowConfig;import limn.backend.lwjgl.LwjglBackend;import limn.components.Button;import limn.components.Label;import limn.scene.Insets;import limn.scene.Scene;import limn.scene.layout.Column;import limn.scene.layout.Padding;
/** A window with a label and a button that closes it. */public final class HelloWindow {
public static void main(String[] args) { // macOS needs -XstartOnFirstThread on the JVM command line; it is macOS-only and a // JVM elsewhere given it will not start. try (Backend backend = new LwjglBackend()) { NativeWindow window = backend.createWindow( WindowConfig.of("Hello, Limn", 480, 320));
Column column = new Column(); column.gap(12); column.add(new Label("A window, drawn by Limn.")); column.add(new Button("Close").onAction(window::requestClose));
Scene scene = new Scene(new Padding(Insets.all(24), column)); scene.bind(window);
backend.runEventLoop(); } }}Install and first window has the dependency and the command line.
A form
Section titled “A form”In SWT:
Composite form = new Composite(parent, SWT.NONE);GridLayout layout = new GridLayout(2, false);layout.marginWidth = layout.marginHeight = 20;layout.horizontalSpacing = 12;layout.verticalSpacing = 10;form.setLayout(layout);
new Label(form, SWT.NONE).setText("Name");Text name = new Text(form, SWT.BORDER);name.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));new Label(form, SWT.NONE).setText("Email");Text email = new Text(form, SWT.BORDER);email.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
Composite buttons = new Composite(form, SWT.NONE);buttons.setLayoutData(new GridData(SWT.END, SWT.CENTER, true, false, 2, 1));buttons.setLayout(new RowLayout(SWT.HORIZONTAL));new Button(buttons, SWT.PUSH).setText("Cancel");Button save = new Button(buttons, SWT.PUSH);save.setText("Save");In Limn:
static Widget<?> contactForm(TextField name, TextField email, Button save) { Grid fields = new Grid(2); fields.gap(12).rowGap(10).expandedColumn(1) .columnAlignment(1, Flex.CrossAlignment.STRETCH); fields.add(new Label("Name").setLabelFor(name)); fields.add(name); fields.add(new Label("Email").setLabelFor(email)); fields.add(email);
Row buttons = new Row(); buttons.gap(8); buttons.add(Expanded.spacer(1)); // takes the room left: the buttons go to the end buttons.add(new Button("Cancel").setStyle(Button.Style.SECONDARY)); buttons.add(save);
Column form = new Column(); form.gap(16).crossAlignment(Flex.CrossAlignment.STRETCH); form.add(fields); form.add(buttons); return new Padding(Insets.all(20), form);}setLabelFor is what a screen reader reads as the field’s name, the job a label placed before
a text field does on its own in SWT. Layout has every layout widget, and
Forms the Field that puts a label, a hint and a validation message around an
input.
Events
Section titled “Events”In SWT:
save.addListener(SWT.Selection, e -> status.setText("Saved " + name.getText()));name.addModifyListener(e -> save.setEnabled(!name.getText().isBlank()));notify.addListener(SWT.Selection, e -> status.setText(notify.getSelection() ? "You will be told" : "Quietly"));plan.addListener(SWT.Selection, e -> status.setText("Plan: " + plan.getText()));In Limn:
static void wire(Button save, TextField name, Checkbox notify, ComboBox<String> plan, Label status) { save.onAction(() -> status.setText("Saved " + name.text())); // A handler answers the user: it runs when they type, not on setText. name.onChange(text -> save.setEnabled(!text.isBlank())); notify.onChange(on -> status.setText(on ? "You will be told" : "Quietly")); plan.onSelect(() -> status.setText("Plan: " + plan.selectedItem())); // A watcher hears every change, the code's included: the label mirrors the field. name.observeChanges((widget, change) -> status.setText(name.text()));}A widget has one handler of each kind, and setting a second throws: pass null first to
replace it. Watchers, as many as you like, are what observeChanges adds.
Work off the UI thread
Section titled “Work off the UI thread”In SWT:
status.setText("Searching…");int asked = ++generation; // an older search must not land after a newer onenew Thread(() -> { try { List<String> found = repository.search(query); display.asyncExec(() -> { if (asked != generation || results.isDisposed()) return; results.setItems(found.toArray(new String[0])); status.setText(found.size() + " found"); }); } catch (Exception e) { display.asyncExec(() -> status.setText("Failed: " + e.getMessage())); }}).start();In Limn:
static final class Search {
private Job job;
void find(String query, Function<String, List<String>> repository, ListView<String> results, Label status) { if (job != null) { job.cancel(); // the answer to an older question is not wanted any more } status.setText("Searching…"); job = Ui.work(progress -> repository.apply(query)) // on a worker .onSuccess(found -> { // on the UI thread results.setItems(found); status.setText(found.size() + " found"); }) .onFailure(error -> status.setText("Failed: " + error.getMessage())) .start(); }}Every callback runs on the UI thread, so none of them needs asyncExec, and a cancelled job
delivers nothing, which is the generation check written once. Background work
has progress and cancellation.
A timer
Section titled “A timer”In SWT:
Runnable tick = new Runnable() { @Override public void run() { if (time.isDisposed()) return; time.setText(LocalTime.now().withNano(0).toString()); display.timerExec(1000, this); }};display.timerExec(0, tick);// laterdisplay.timerExec(-1, tick);In Limn:
static final class Clock {
private Subscription next;
void start(Label time) { time.setText(LocalTime.now().withNano(0).toString()); next = Ui.postDelayed(() -> start(time), 1000); // re-armed on every tick }
void stop() { if (next != null) { next.cancel(); } }}For something that moves on screen, a transition or a ticker is the better tool: Animations.
A table
Section titled “A table”In SWT:
TableViewer viewer = new TableViewer(parent, SWT.BORDER | SWT.FULL_SELECTION);viewer.getTable().setHeaderVisible(true);viewer.setContentProvider(ArrayContentProvider.getInstance());
TableViewerColumn number = new TableViewerColumn(viewer, SWT.NONE);number.getColumn().setText("Order");number.getColumn().setWidth(90);number.setLabelProvider(new ColumnLabelProvider() { @Override public String getText(Object o) { return ((Order) o).number(); }});TableViewerColumn customer = new TableViewerColumn(viewer, SWT.NONE);customer.getColumn().setText("Customer");customer.getColumn().setWidth(200);customer.setLabelProvider(new ColumnLabelProvider() { @Override public String getText(Object o) { return ((Order) o).customer(); }});TableViewerColumn total = new TableViewerColumn(viewer, SWT.RIGHT);total.getColumn().setText("Total");total.getColumn().setWidth(100);total.setLabelProvider(new ColumnLabelProvider() { @Override public String getText(Object o) { return String.valueOf(((Order) o).total()); }});
viewer.setInput(rows);viewer.addDoubleClickListener(e -> { Order o = (Order) e.getStructuredSelection().getFirstElement(); status.setText("Opened " + o.number());});In Limn:
static Table<Order> orders(List<Order> rows, Label status) { Table<Order> table = new Table<>(List.of( Column.text("Order", Order::number).width(90), Column.text("Customer", Order::customer).weight(1), Column.numeric("Total", Order::total).width(100))); table.setRows(rows); // the list itself; sorting by a header is built in table.onActivate(row -> status.setText("Opened " + table.rows().get(row).number())); return table;}A column is the label provider and the content provider in one function. The table sorts from
its headers with no ViewerComparator, and only builds the rows on screen, so a table over a
million rows costs what one over twenty does, with no SWT.VIRTUAL to ask for.
Lists and scrolling has selection, footers, frozen columns and a
button in a row.
Menus and dialogs
Section titled “Menus and dialogs”In SWT:
Menu bar = new Menu(shell, SWT.BAR);MenuItem fileItem = new MenuItem(bar, SWT.CASCADE);fileItem.setText("&File");Menu file = new Menu(shell, SWT.DROP_DOWN);fileItem.setMenu(file);
MenuItem saveItem = new MenuItem(file, SWT.PUSH);saveItem.setText("&Save\tCtrl+S");saveItem.setAccelerator(SWT.MOD1 + 'S');saveItem.addListener(SWT.Selection, e -> save.run());new MenuItem(file, SWT.SEPARATOR);MenuItem deleteItem = new MenuItem(file, SWT.PUSH);deleteItem.setText("Delete…");deleteItem.addListener(SWT.Selection, e -> { MessageBox box = new MessageBox(shell, SWT.ICON_WARNING | SWT.YES | SWT.NO); box.setText("Delete this order?"); box.setMessage("This cannot be undone."); if (box.open() == SWT.YES) delete.run();});shell.setMenuBar(bar);In Limn:
static MenuBar menuBar(Widget<?> owner, Runnable save, Runnable delete) { Menu file = new Menu() .add(MenuItem.of("Save", save).setAccelerator(Accelerator.command(Keys.S))) .addSeparator() .add(MenuItem.of("Delete…", () -> confirmDelete(owner, delete))); MenuBar bar = new MenuBar(); // on macOS it is shown in the system's menu bar bar.addMenu("File", file); return bar;}
static void confirmDelete(Widget<?> owner, Runnable delete) { new Dialog("Delete this order?", "This cannot be undone.") .addPrimaryButton("Keep", "keep") .addDestructiveButton("Delete", "delete") .setCancelResult("keep") .show(owner) // returns at once .thenAccept(answer -> { // the choice, later, on the UI thread if ("delete".equals(answer)) { delete.run(); } });}show returns at once and the answer arrives later, so the window keeps drawing while the
question is up, where MessageBox.open waits. The buttons are named for what they do rather
than Yes and No. Accelerator.command is SWT.MOD1: Command on macOS, Control elsewhere.
Menus and dialogs has the rest.
Choosing a file
Section titled “Choosing a file”In SWT:
FileDialog dialog = new FileDialog(shell, SWT.OPEN);dialog.setText("Open invoice");dialog.setFilterPath(lastFolder.toString());dialog.setFilterNames(new String[]{"PDF"});dialog.setFilterExtensions(new String[]{"*.pdf"});String chosen = dialog.open();if (chosen != null) { load.accept(Path.of(chosen));}In Limn:
static void open(NativeWindow window, Path lastFolder, Consumer<Path> load) { window.fileDialogs() .openFile("Open invoice", lastFolder, FileDialogs.Filter.of("PDF", "*.pdf")) .thenAccept(chosen -> chosen.ifPresent(load)); // empty means cancelled}Both are the system’s own chooser. Limn’s does not block: the window keeps drawing while the user browses, and an empty answer means cancelled.
Painting your own
Section titled “Painting your own”In SWT:
Canvas meter = new Canvas(parent, SWT.DOUBLE_BUFFERED);meter.setLayoutData(new GridData(160, 12));meter.addPaintListener(e -> { Rectangle area = meter.getClientArea(); e.gc.setBackground(display.getSystemColor(SWT.COLOR_WIDGET_NORMAL_SHADOW)); e.gc.fillRectangle(area); e.gc.setBackground(display.getSystemColor(SWT.COLOR_LIST_SELECTION)); e.gc.fillRectangle(0, 0, Math.round(area.width * level), area.height);});// after a change of levelmeter.redraw();In Limn:
public static final class Meter extends Widget<Meter> {
private float level; // 0 to 1
public Meter setLevel(float newLevel) { Ui.checkUiThread(); level = Math.max(0, Math.min(1, newLevel)); invalidate(); // repaint at the same size; markNeedsLayout() for a new size return this; }
@Override protected Size onMeasure(Constraints constraints) { return constraints.constrain(160, 12); }
@Override protected void onPaint(Canvas canvas) { Theme theme = Theme.of(this); canvas.fillRect(0, 0, width(), height(), theme.outline()); canvas.fillRect(0, 0, width() * level, height(), theme.primary()); }}The widget says how big it wants to be instead of being given layout data, and its colours come from the theme, so it follows a palette change with no code and nothing to dispose. Sizes are in points and the canvas is already scaled for the display. Your own widgets has input, focus and what a screen reader is told.
The look
Section titled “The look”SWT has none of its own to change: its controls are the system’s. Limn’s look is a palette, which you pick from the built-ins or build from your brand:
static void brand() { Theme.builder("Acme", false) .background(Color.WHITE) .primary(Color.rgb(0x0F6E4F)) .deriveAccentStates() // hover and pressed tones solved from the accent .cornerScale(1.6f) .build() .apply(); // every open window repaints in it}Theming has the built-in palettes, density, fonts and following the operating system’s light or dark appearance.
What Limn does not have
Section titled “What Limn does not have”Some of what SWT and the Eclipse platform give you has no Limn counterpart, and it is better to know before you plan a migration than in the middle of one.
- No native controls. That is the design rather than a gap, but it is the first thing an SWT user notices: a Limn window looks like Limn on every system, not like that system’s own controls.
- No workbench. There are no views, editors, perspectives, extension points or plug-ins: Limn is a toolkit, not an application platform. An RCP application moves its screens, and keeps or replaces its own structure around them.
- No data binding. There is no counterpart to JFace data binding; values are set from handlers and watchers.
- No mixing. Limn does not host SWT controls, and a Limn scene cannot be put inside a
Shell. Running both toolkits in one process is not something Limn supports or tests, so a migration moves an application, or a separate program of it, at a time. - No browser and no styled text. There is no
Browserwidget;window.launcher()opens a page in the user’s own. There is noStyledText: aLabelwraps and aTextAreaedits plain text. - No form attachments. There is no
FormLayout; a layout of your own is a widget that places its children (Your own widgets).