Retro Nim UI – Windows 3.1 style UI toolkit Sultan Al Isaiee (@foxoman) released retroui, a Windows 3.1-style UI toolkit for the Nim language, installable via `nimble install retroui`, that builds apps for Windows, macOS, Linux, Android 7+, iOS and the web from one code base. A hello-world app compiles to a single 1.9 MB file (about 1 MB of it fonts) that runs in 7 MB of memory on one thread, versus 15-22 MB files and 15-120 MB of memory for the same Hello in egui, iced or Slint, and the toolkit requires no dependencies beyond Nim and a C compiler. The companion `retrokit` tool packages apps for Google Play (APK, App Bundle), the Microsoft Store (MSIX, Authenticode-signed program), the App Store (.ipa) and the web, and a Retro Designer tool edits the same `.nim` file directly. Developed by Sultan Al Isaiee @foxoman https://github.com/foxoman -- www.foxoman.net https://www.foxoman.net -- sultan@foxoman.net mailto:sultan@foxoman.net Co-author: Claude Anthropic A Windows 3.1 style UI toolkit for Nim that builds business and desktop apps for Windows, macOS, Linux, Android, iOS and the web from one code base, and packs them for the stores itself. The package and its module are called retroui : nimble install retroui , import retroui . Learn the syntax: docs/syntax.md https://github.com/foxoman/retroui/blob/main/docs/syntax.md -- the whole way of writing an app, every form of it as a small program with a picture of what it draws: what row 50, -1 does with the 50 and the -1, binding, events, containers, dialogs, how it is built and why it is easy to follow. Documentation: docs/controls.md https://github.com/foxoman/retroui/blob/main/docs/controls.md -- every control with pictures: getting started, layout, each control's example and API, the looks. Design it: Retro Designer https://github.com/foxoman/retroui/blob/main/docs/designer.md -- retrokit designer app.nim shows your file's windows drawn with the real controls: drop controls, drag them, set their properties, add their events, and every change is written into the same .nim file. - Small and light. A "hello" app is a single 1.9 MB file Linux x86-64, built with retrokit small ; about 1 MB of it the fonts that runs in 7 MB of memory on one thread -- the same Hello in egui, iced or Slint is a 15-22 MB file in 15-120 MB see Speed and size speed-and-size ; the full file manager is 4.3 MB, and the gallery with every control, the street map, 3D models, the canvas and PDF 5.7 MB. Fonts, icons, the PDF writer, the world map, the 3D view and the decoders are all built in, and only what an app calls is linked. - No dependencies. You need Nim and a C compiler — nothing else to install, now or on the user's machine: no GTK, Qt, SDL, Electron, browser engine or Java toolchain. On Linux, libX11 is the one library an app needs, loaded when it starts Xft, XRender, EGL and the rest are used when the system has them , so building needs no -dev packages; sound, video and printing use whatever the system offers, loaded only when used. - Runs anywhere. One code base for Windows, macOS, Linux/BSD X11, and Wayland with a driver of its own , Termux:X11, Android 7+, iOS, a web page WebAssembly made by clang alone, no Emscripten and a screen with no windowing system at all a kiosk, a device: the Linux framebuffer or DRM . Touch, mouse and keyboard all work everywhere, and the layout adapts to phones, tablets, laptops and 4K screens. - Ships itself. retrokit makes what each store takes, in Nim, on any system: an APK and an App Bundle for Google Play, an MSIX and an Authenticode-signed program for Windows and the Microsoft Store, a signed macOS app notarized on a Mac , an .ipa for the App Store, a web page that installs and works offline. - Fast. Immediate mode with a software renderer that redraws only the 32-pixel bands that changed: a full 800×600 window takes about 0.25 ms, an unchanged frame costs nothing and is not sent to the screen, and the app uses no CPU while it waits. A 50,000-file folder or a 100,000-row grid scrolls in well under a millisecond per frame. - For everyone. Screen readers read every control and act through it on every platform -- Orca, NVDA, JAWS, Narrator, VoiceOver, TalkBack -- with nothing for an app to do but put a label before a box; input methods Chinese, Japanese, Korean compose in the text box; the window takes the scale of the monitor it is on, 125 % or 175 % alike. - Reaches any OS API. retroui/native calls any Android Java API without writing Java, any macOS/iOS API through the Objective-C runtime, Win32 and COM, and any C library through dlopen , with ready-made cross-platform calls battery, vibrate, dark mode, device name… . - Business ready. Data grids with check boxes, colour status cells, drop-downs, buttons, per-cell colours and totals; forms with masked fields; record navigators; property grids; tree grids; 21 chart types; PDF reports from HTML templates, with printing; kanban, Gantt, agenda and org charts; maps; MDI windows; wizards. - Looks the part. The 3.1 look down to the pixel — black outlines, the two-step bevels, the default button's ring, navy and white title bars, dithered scroll tracks, etched group boxes, hard menu shadows — in FreeSans GNU FreeFont's Helvetica-like sans, with its bold, oblique and bold oblique , its heights on whole pixels so it is crisp at 100 %, at 125 % and 150 %, and on HD, 2K and 4K screens, with Arabic everywhere FreeSerif's . FreeSerif , FreeMono or a font file are the other choices setUiFont , or the user in appearanceSettings ; see "The fonts" below . 255 pixel icons in the manner of the period. Lists and pages glide when scrolled, and a flicked finger keeps them moving. - Safe. Every decoder and parser pictures, zip and ZIP64, CSV, Markdown, HTML, OBJ, STL and glTF models, vector tiles, the map services' answers, certificates, the programs it signs is fuzzed with hostile input, and the tests also run under AddressSanitizer and as 32-bit programs. See SECURITY.md https://github.com/foxoman/retroui/blob/main/SECURITY.md . Every control, its API and how it looks in each state normal, keyboard focus, mouse over, pressed, disabled, open, touch screen, dark scheme is in docs/controls.md https://github.com/foxoman/retroui/blob/main/docs/controls.md ; the generated API reference of every module is in docs/api/ open docs/api/retro.html . The syntax, studied https://github.com/foxoman/retroui/blob/main/docs/syntax.md · Install install · Your first app your-first-app · Retro Designer retro-designer · How it works how-it-works · Controls controls · Layout layout · Text and icons text-and-icons · Screen readers and input methods screen-readers-and-input-methods · Business apps business-apps · Charts charts · Reports, PDF and printing reports-pdf-and-printing · Planning views planning-views · Desktop apps desktop-apps · Files files · Maps maps · 3D 3d-view · Canvas canvas · Whiteboard whiteboard · What the computer is doing what-the-computer-is-doing · Image editing image-editing · Web and HTML web-and-html · Sound and video sound-and-video · Camera and microphone camera-and-microphone · More windows more-windows · The system's features the-systems-features · The system's own APIs the-systems-own-apis · Updates inside the app updates-inside-the-app · Platform services platform-services · Android android · iOS ios · Apps as web pages apps-as-web-pages · Screens without a desktop screens-without-a-desktop · Shipping to the stores shipping-to-the-stores · Platforms platforms · Speed and size speed-and-size · Safety safety · Tests tests · Examples examples · FAQ faq · Limits limits · License license You need Nim 2.0 or newer and a C compiler : | System | Get them | Notes | |---|---|---| | Windows | choosenim brings MinGW-w64 | GDI, nothing else | | macOS | brew install nim , xcode-select --install | Cocoa | | Linux / BSD | your package manager: nim , gcc or clang | X11 is loaded at run time, Wayland spoken by the kit itself: no -dev packages | | Android, on the device | Termux: pkg install nim clang ndk-sysroot git | builds APKs right on the phone | | Android, from a desktop | a clang with lld LLVM on Windows, brew install llvm lld , clang lld packages or zig; with neither, the first APK build fetches the NDK into ~/.retro/ndk | no NDK, no Java, no Gradle, no SDK, no OpenSSL | | iOS | a Mac with Xcode | | | The web | clang and lld apt install clang lld , LLVM on Windows, brew install llvm lld | WebAssembly without Emscripten | Then: nimble install retroui the library and its tools or the repository's URL retrokit doctor checks what this machine has retrokit new myapp --kind:business a ready app: hello, form, business, files, charts cd myapp && nimble run retrokit is the helper that comes with the package: | Command | What it does | |---|---| | retrokit new NAME --kind:K | a folder with NAME.nim , NAME.nimble tasks run, small, apk and an icon | | retrokit doctor | checks Nim, the C compiler, the screen libraries, the Android tools, what Retro Designer needs, and says how to fix what is missing | | retrokit run app.nim | builds in release mode and runs it | | retrokit small app.nim | the smallest build optimized for size, stripped , and its size | | retrokit apk app.nim | an Android APK the retroapk tool, below | | retrokit aab app.nim | an Android App Bundle for Google Play, signed with your upload key | | retrokit web app.nim | the app as a web page: installable, offline Apps as web pages apps-as-web-pages ; retrokit run --web --watch serves it and reloads on every save | | retrokit sign app.exe | Windows' Authenticode signature, made here, with a time stamp | | retrokit msix app.exe | an MSIX for the Microsoft Store and Windows' installer, signed | | retrokit mac app | a macOS .app , signed ad hoc here or with your Developer ID, notarized and stapled on a Mac | | retrokit ipa App.app | an iOS app as the .ipa TestFlight and the App Store take | | retrokit model car.blend | a 3D model Blender's, glTF, OBJ, STL as the .glb an app carries, its parts and clips named | | retrokit designer app.nim | Retro Designer retro-designer : the file's windows to design, every change written into the file built once for this computer | From a clone of this repository the examples are built only when you ask, one at a time; nimble install retroui installs only the toolkit and its tools : nimble gallery every control in one window nimble files Retro Files, the file manager a folder after it: opens there nimble ledger a small business app: invoices, customers, reports nimble invoice an invoice from an HTML template: preview, PDF, printing nimble charts Chart Viewer: a CSV file as charts, saved as PNG nimble photo Retro Photo, the image editor nimble board Retro Board, the whiteboard nimble taskman Retro Task Manager: the programs, the processor, the memory, the network nimble mail Retro Mail: Microsoft 365, Outlook.com, Gmail, any IMAP mailexe, mailapk nimble hrbook HR Book: employee records of several hotels, sealed with a PIN nimble apk Retro Files as an Android APK build/RetroFiles.apk nimble photoapk Retro Photo as an APK photoinstall: build and adb install nimble galleryapk the gallery as an APK nimble boardapk Retro Board as an APK boardinstall: build and adb install nimble taskmanapk Retro Task Manager as an APK taskmanexe: for Windows nimble photoexe Retro Photo for Windows, with its icon from Linux or macOS: MinGW; build/win/ nimble filesexe Retro Files for Windows galleryexe, boardexe: the others nimble photoappimage Retro Photo as a Linux AppImage, with its icon and menu entry one file, runs anywhere: nimble filesappimage Retro Files as an AppImage built against glibc 2.31 with zig when it is there RETRO APP=hrbook nimble exe any one app of the table in retroui.nimble: exe, appapk, RETRO APP=hrbook nimble appapk appimage, macapp nimble apks every app as an APK the apps: the table in retroui.nimble nimble exes every app for Windows nimble appimages every app as an AppImage nimble macapps every app for macOS, zipped .app on a Mac: build/mac/ nimble apps every example for this computer, into build/ nimble dist the apps and all the APKs, into build/ nimble test the headless tests nimble fuzz the fuzzers nimble docs the API reference docs/api and docs/controls.md Step by step -- an APK from Windows, macOS, Linux or a BSD without the Android SDK, an iOS app on a Mac, signing and installing: see BUILDING.md https://github.com/foxoman/retroui/blob/main/BUILDING.md . Every task builds with what nimble install brought: the APK tasks need no Android NDK -- retroapk carries Android's headers and libraries 1.2 MB, unpacked once into ~/.retro/android-sysroot and builds with a clang that has lld, or zig; with neither they fetch the NDK once about 700 MB, into ~/.retro/ndk ; nimble ndk does it now, ANDROID NDK HOME uses yours , sign with a key made for you in ~/.retro/android RETRO KEYS another folder , and need no Java, Gradle, SDK, OpenSSL or apksigner. In Termux the compiler is installed with pkg the first time. GitHub builds them too: CI runs the tests on each push and compiles the tools on Linux, Windows and macOS the examples when run by hand ; Release -- a v... tag pushed, or by hand -- makes the apps I use every day, Retro Gallery, Retro Files, Retro Photo, Retro Board and Retro Mail, as an AppImage, a Windows .exe, an APK and a macOS .app, and puts them on the GitHub release of that tag. Its APKs are signed with the key in the secrets ANDROID KEY PEM and ANDROID CERT DER base64 of the files in ~/.retro/android , so a new one installs over the one before. A desktop app has the system's own frame, or the one Windows 3.1 had: the gray sizing border with dark lines at its corners, the navy title bar white while another window is in front with the control-menu box at its left and the minimize and maximize buttons at its right. The app draws that one itself, in a window without the system's frame, and the system still does the rest: drag the title bar to move the window Aero Snap on Windows , the border or a corner to size it; a double click or the maximize button maximizes it, down to the taskbar; Alt+Space opens the control menu Restore, Move, Size, Minimize, Maximize, Close, the UI's size . The menu bar, tool bars and the content move down under it as under a phone's title bar, so an app needs no change. A size grip sits in the bottom right corner inside the frame in the status bar's end when there is one; the bottom left, right to left : drag it to size the window, with the mouse or a finger, on every platform the 3.1 frame is drawn. The frame takes the theme's colors, and follows when the user picks another theme. A window that keeps its size: app "Calculator", 260, 320, resizable = false: or setResizable false . The 3.1 frame then has no sizing border to drag, no maximize button and no grip its border is the plain band 3.1's dialogs had , and the system's frame is told the same: Windows drops the sizing border and the maximize box, macOS the resize edges and the zoom button, X11 window managers get the same least and largest size, GTK 4 and SDL make the window fixed. RETRO FRAME=classic nimble mailexe Retro Mail for Windows with the 3.1 frame RETRO FRAME=classic nimble apps every example exes and appimages the same way nim c -d:retroFrame=classic app.nim by hand system: the default RETRO FRAME=system ./app when it starts: the system's frame after all The user can always switch. In every app, with nothing to write: the 3.1 frame's control menu has Windows' Own Frame elsewhere Desktop's Own Frame ; on Windows the system's frame gets Windows 3.1 Frame in its system menu the title bar's icon, Alt+Space , and on macOS the menu bar gets it in a Window menu checked while that frame is on . The choice is kept for the app window.ini in its data folder and wins over the frame it was built with at the next start; appearanceSettings offers it too, as Window . On X11 and Wayland the window manager keeps its menu Alt+Space, a right click on the title bar to itself: there appearanceSettings is the way back to the 3.1 frame, so offer it. Where: every desktop. Windows; macOS; Linux and the BSDs on X11, with any window manager EWMH: GNOME, KDE, Xfce, openbox... -- snapping and tiling as for other windows or none an X server that shows one app: the app's window is then moved, sized and maximized by the driver ; Wayland desktops with the kit's own Wayland driver the default build takes it when there is no X11, RETRO DRIVER=wayland asks for it: the system's frame where the compositor draws one, else the 3.1 frame by itself , through XWayland, or with -d:gtk4 or -d:sdl3 . The other drivers have it too: GTK 4, SDL 3 and SDL 2 where a double click on the title bar maximizes on Windows only, and the control menu's Move and Size do nothing , Siwin not on macOS; on Windows the frame chosen is the next window's . Only the phones keep their own. For the developer: app 's frame wfAsBuilt , wfSystem , wfClassic is the frame the window opens with, userFrame = false takes the user's choice away; setClassicFrame on changes it while the app runs, chooseClassicFrame on changes it as the user would and keeps it , classicFrame says which one is on. A window manager's own keys stay its own: openbox, say, opens its window menu for Alt+Space before the app sees it the control-menu box still opens the app's . python import retroui var name = "World" var greeting = "" app "Hello", 320, 120: row 50, -1 : label "&Name:" edit name row: if button "&Greet", isDefault = true : greeting = "Hello, " & name & " " if button "E&xit" : quitApp label greeting nim c -r -d:release -d:useMalloc hello.nim or: retrokit run hello.nim retrokit small hello.nim optimized for size, LTO, stripped: about 1.9 MB, one file retroapk hello.nim --install on a phone in Termux In Termux the same nim c -r runs it in a window on Termux:X11 see BUILDING.md, "A desktop app in Termux" . Working on an app in Termux. Without a window manager, Termux:X11 gives an app the whole screen, as Android does. To see it as a desktop window while you work on it: retrokit run --window app.nim its own size, in its 3.1 frame, in the middle retrokit run --live app.nim the same, built again and shown again on every save retrokit run --window --scale:125 app.nim everything and the window a quarter larger retrokit run --window --theme:Dark --size:420x840 app.nim The toolkit draws a desktop the 3.1 teal over the screen and puts the window on it at the size the app asked for, grown by the screen's own scale its density, so a 2K tablet shows it at its real size , with the title bar to move it, the border and the grip to size it, the maximize button and the control menu. Keys go to the window, also after a tap on the desktop. When the screen changes size the tablet turns, Termux:X11's window is made floating or sized the desktop follows, and the window stays in view: made smaller while the screen is, its size again when there is room. The line it prints says what it came to RETRO WINDOW: 660 x 304 pixels on a 2560 x 1600 screen; the screen's scale 200 % ; --scale:N makes it larger or smaller. retrokit run passes anything else to the compiler. Live: see each change as you save , on any desktop. retrokit run --watch app.nim builds the app again whenever a .nim file in its folder or two levels below is saved, starts the new one where the window was -- the place and the size you gave it -- and then closes the one before, so the window never goes blank. It is the same on Windows, macOS, Linux any window manager, or none and Termux, where --live is --watch with --window . Only what changed is compiled again: a few seconds, most of it Nim reading the kit. Code that does not build leaves the window running what it had, with the compiler's message in the terminal; an app that crashes starts again with the next save. The app starts fresh each time its variables are new ; closing its window, or Ctrl+C, ends it. The window's place is kept by the X11, Windows, macOS, SDL 2 and SDL 3 drivers RETRO WINDOW STATE , keptWindowPlace ; GTK 4 and Siwin cannot place a window, so theirs opens where the system puts it. retrokit run --watch app.nim Windows, macOS, a Linux desktop retrokit run --live app.nim Termux:X11: --window and --watch On Windows from Linux: nim c -d:release -d:useMalloc -d:lto --opt:size --passL:-s -d:mingw --cpu:amd64 --app:gui hello.nim . Build with -d:useMalloc or put switch "define", "useMalloc" in your config.nims ; retroapk adds it by itself : with Nim's own allocator a program never gives memory back to the system, and one that shows many pictures only grows. The toolkit warns at compile time without it, and retroui/memwatch gives back what the program lets go of trimHeap , by itself when it rests and when it goes to the background . app opens the window and runs its body once per frame. Each widget draws itself and returns what happened: button is true when it is clicked, and edit name edits name in place and returns true when it changes. app takes options by name after the size, all optional: js const ledgerIcon = staticRead "ledger.png" app "Ledger", 900, 600, theme = Dark, the colors builtinThemes, or your own Theme frame = wfClassic, wfAsBuilt default , wfSystem, wfClassic userFrame = true, the user may switch it kept per app gpu = gmAuto, gmAuto, gmGpu, gmCpu scale = 125, the size of everything, in percent font = ufSans, the UI's font ufSerif, ufMono zoom = 0, whole-pixel magnification 0: the display's id = "ledger", the app's name for what the toolkit keeps icon = ledgerIcon, the window's picture a PNG's bytes resizable = true, false: the window keeps its size look = "ledger": keep the look the user picks appearanceSettings label "Hello" A misspelled option is a compile error that lists them all. $RETRO FRAME, $RETRO GPU, $RETRO SCALE, $RETRO THEME, $RETRO SIZE and $RETRO ZOOM win over the options when the app starts tests, screenshots ; $RETRO SCALE makes the window larger with everything in it. $RETRO WINDOW=1 opens the app as a window in its 3.1 frame, in the middle of a desktop the toolkit draws, where the screen has no window manager see "Working on an app in Termux" below . retrokit designer app.nim opens your .nim file in Retro Designer , the kit's visual designer, in the manner of Qt Designer: its windows drawn with the real controls, the Widget Box with every control of the kit, the Object Tree, the properties, events and variables of what you choose. Drop and drag controls, put them in rows and groups, set their properties, add their events -- each change is written into the same file as Nim, touching only the lines that changed: no form file, no generated code. It is purely a designer: Edit Code opens your editor at the control's code, and while one of them has the file the other leaves it alone a lock beside it, the file read-only for the others . Dialogs and menus open over their window when you choose something in them; F5 shows the window working, in each look. It comes with the package and builds itself the first time you run it, for your computer and your Nim: Windows, macOS, Linux and BSD, and Termux on Android Termux:X11 . The whole guide: docs/designer.md https://github.com/foxoman/retroui/blob/main/docs/designer.md . retrokit designer app.nim app.nim's windows a new file when there is none - Immediate mode. The UI is plain code that runs every frame, and widgets are procs bound to your own variables. There are no callbacks and no widget objects to create, keep or free; whatever your code draws is what the screen shows. A widget is known by the variable it is bound to or by its label, so you never manage ids use pushId / withId for rows of the same widgets . - Frames only when needed. A frame runs when input arrives, when a timer asked for one wakeIn ms or when work on a background thread finished. Between frames the app sleeps. Drawing commands carry hashes: an unchanged frame is skipped, and a changed one redraws only the bands that changed. A minimized window draws nothing at all ui.hidden : its frames and timers go on, so it can read less often . - Input, the same everywhere. Mouse: click, double-click, the wheel Shift for sideways, Ctrl to zoom where it makes sense , drag, the right button for menus, the middle and side buttons. Keyboard: Tab and Shift+Tab move the focus shown as the dotted 3.1 rectangle , Space and Enter activate, arrows/Home/End/PageUp/PageDown move, Esc closes, Alt+letter for & mnemonics, the menu key or Shift+F10 for context menus. Fingers: a tap activates a drag cancels it , dragging scrolls lists and pages, a long press is the right click, and a held-then-moved finger drags items. Hover effects appear only for a real mouse. - Looks. The 3.1 look bevels, X check boxes, navy selections, group boxes, bold menus in eight colour schemes builtinThemes : Classic, Gray, Dark, Ocean, Desert, Plum, Hot Dog, Paper , or your own Theme ui.theme = Dark . Text is FreeSans the default , FreeSerif or FreeMono -- GNU FreeFont, built in, each in its four styles, Arabic everywhere -- or a font file setUiFont , loadUiFontFile ; icons are the 3.1-style pixel icons or the Nerd Fonts symbols. - Sizes. Layout uses design pixels that follow the screen's density Xft.dpi, GDK SCALE, the monitor's real size, Android's density, Windows per-monitor DPI , in whole pixels so lines stay sharp. Users can make everything larger Ctrl + / − / 0, setUiScale or just the text setTextScale . Touch screens get finger-sized metrics. - Content larger than its place scrolls , always with the standard 3.1 scroll bar on touch screens too, where a finger can also drag the content : the window's page both ways , dialogs, panes, lists, grids, MDI windows, the details pane. The full catalogue, with every signature, a usage example and pictures of each state, is docs/controls.md https://github.com/foxoman/retroui/blob/main/docs/controls.md . In short: | Group | Controls | |---|---| | Text | label , paragraph , heading , styledText , link , badge , icon , iconLabel , tooltip | | Buttons | button also with a picture: icon only, text only, text below or beside the icon -- ButtonLook , barButton tool bars; setToolbarLook , buttonAt , toolButton , menuButton , splitButton , segmented , buttonRow | | Choices | checkbox , radio , toggle 3.1 slide switch , chips , tagInput , rating , knob | | Text input | edit , editArea , searchBox , autocomplete , maskedEdit , comboEdit , hotkeyEdit | | Numbers, dates, colours | spin , intEdit , numberEdit , slider , rangeSlider , progress , datePicker , calendar , dateRange , timePicker , colorPicker | | Lists | listbox , combo , checkList , grid with cell kinds , tree , treeGrid , iconView , lineView , dualList , propertyGrid | | Status | progressRing , spinner , sparkline , usageBar , banner , toast , led , pill , timeline , emptyState , busyState , statusbar | | Containers | group , card , tabs , tabStrip , expander , accordionHeader , form / field , split , vsplit , sidePane , scrollPanel , adaptive , row , col | | Menus and bars | menubar , menu , menuItem , submenu , contextMenu , popupMenu , toolbar , statusbar , breadcrumb , pathBar , driveBar , pager , stepper , recordNav , findBar | | Dialogs | dialog , msgBox , inputBox , progressBox , wizard , aboutBox , fileDialog 3.1 , filePicker modern , propertiesDialog | | Desktop | mdiArea / mdiWindow , mdiWindowMenu , hotkeyPressed , findIn , replaceAllIn | | App parts | settingToggle , settingChoice , settingSlider , settingButtons , settingCombo , settingLink , appearanceSettings , avatar , factList , bottomNav , fab , captionBar | | Business views | chart 21 kinds , gauge , kanbanBoard , ganttView , agendaView , kpiTile , statCard , orgChart , reportView | | Files | fileList , filePreview , folderTree , placesView , trashList , driveBar , pathBar , textFileView , hexFileView , runJobDialog | | Media and more | mapView , view3d , canvas2d , htmlView , webView , markdownView , imageView , animation , videoView | Buttons with a picture show it four ways -- icon only the label becomes the tooltip , text only, the text below the icon, or beside it -- and a tool bar shows all of its barButton s one way, which the app can offer as a setting Explorer 98's "Text Labels" : setToolbarLook blTextBelow labels under the icons; the bar grows toolbar: if barButton pxBack, "Back" : goBack if barButton pxCopy, "Copy", tip = "Copy Ctrl+C " : copy if button "Open", pxOpen, blTextBeside : openIt if button "Save", glyphPic nfSave , blIconOnly : saveIt Drawing your own: canvas w, h gives a rectangle to draw into with fill , box , hline , vline , line , text , image , drawBitmap and the smooth shapes disc , ring , roundRect , polygon , thickLine , pieSlice ; canvas2d adds the whole HTML5 Canvas API. row 120, -1, -2 : fixed, one share, two shares label "Name:" edit name edit notes row: label "Total: 120" spring the buttons go to the right end button "OK" button "Cancel" adaptive 260 : 1 column on a phone, 3 on a desktop card "Sales": ... card "Stock": ... card "Orders": ... if isCompact : ... sizeClass : scCompact, scMedium, scExpanded let cols = responsive 1, 2, 3 Widths in rows, col w , dialogs and grid columns are design pixels: they grow with the screen and the user's size. dp 13 scales your own. Content larger than the window scrolls both ways under the fixed menu, tool and status bars, always with the standard scroll bars on touch screens too : the wheel Shift + the wheel goes sideways , a finger, PageUp / PageDown, Ctrl+Home / End, and Tab brings the focused control into view. A dialog taller than the screen scrolls inside; scrollPanel scrolls a part of a page scrollPanel -1: down to the bottom . Smooth scrolling. Lists, grids, trees, text areas and pages glide a short way per wheel notch or key, follow a finger pixel for pixel and fly on when it is flicked; a finger landing on a flying list stops it without picking an item. setSmoothScrolling false or the user, in appearanceSettings makes every scroll jump at once. Views of your own get the same with scrollWheel , scrollPan , scrollTo / scrollBy / scrollShow and one scrollTick a frame. Sizes. The UI follows the display's density: whole pixels keep lines sharp 2x, 3x and the text and controls make up the rest a 1.33x tablet gets 1x lines and 1.33x text . setUiScale 125 and Ctrl + / - / 0, and the phone title bar's menu scales everything; setTextScale 120 only the text controls grow with it ; setBaseTextSize 14 changes the 100% size 12 pixels per em on desktops and tablets, 15 on phones . docs/syntax.md https://github.com/foxoman/retroui/blob/main/docs/syntax.md widths-in-a-row shows each kind of width with the pixels it came to. Try layouts on a desktop with RETRO SIZE=420x840 , RETRO TOUCH=1 , RETRO CAPTION=1 the phone title bar , RETRO SCALE=150 , RETRO ZOOM=2 . Controls for a mouse or for fingers. setControlSizes chooses: csAuto finger-sized on a phone, desktop-sized on a tablet and a desktop, whose screens hold a desktop layout , csDesktop or csTouch finger-sized everywhere . The user chooses in Appearance appearanceSettings , Controls , kept with the look. RETRO TOUCH=1 is a finger on any screen: finger-sized controls, no hover, tooltips above the finger; RETRO CONTROLS=touch , desktop or auto sets the sizes alone retrokit run --touch , --controls:touch . Both win over what the user kept. text x, y, "Plain", c text x, y, "Big bold", c, style bold = true, size = 2.0 text x, y, "مرحبا بالعالم", c joined, right to left label nfFolderOpen & " Documents" an icon inside text toolButton nfCopy, "Copy", size = 1.3 image x, y, glyphIcon nfFolder, color 222, 170, 40 , 24 as an Icon TextStyle has bold , italic the font's own italic: FreeSans Oblique, not a slanted copy , mono FreeMono , serif FreeSerif , underline , strike , size times the UI's text size and rtl . textWidth , lineHeight and FontH measure. The icon constants nfFolder , nfCog , nfGithub ..., nerdIconNames lists them all come from Font Awesome, Material Design, Seti and Devicons via Nerd Fonts. Characters in no built-in font CJK, emoji are drawn by the platform. Drawing. Apps draw into one software framebuffer cut in tiles: only the tiles whose commands changed are drawn on all cores and sent, and a picture changed in part changed bitmap, rect only there; nothing at all when nothing changed. The framebuffer is the window's own memory where the system lends it -- shared with the X server MIT-SHM on X11, a DIB section on Windows -- so a frame is never copied on its way to the screen RETRO SURFACE=0 copies it, as before 0.26 . Where the platform has OpenGL ES 2 / OpenGL 2.1 EGL on Linux and Android, WGL on Windows -- loaded at run time, nothing to link the frames are drawn by the GPU instead: the same commands as rectangles from textures the CPU makes once text, icons, shapes , pictures as textures sent only where they changed. A start-up self-test, a fall back to the CPU and a guard against a crash at start keep a bad driver from hurting; setGpuMode gmAuto/gmGpu/gmCpu or "Drawing" in appearanceSettings chooses. The text is drawn at the screen's own resolution on every screen; on HD and 4K screens pixel icons are scaled with Scale2x and Scale3x, and small marks are drawn at the screen's resolution setHdText false : the pixels of 1998, magnified . There is a true-black OLED theme; in it and in Dark the pixel icons are drawn as their line twins -- Nerd Fonts symbols in the text's color, crisp at any size setIconStyle . Big images. The photo engine retroui/photo shows a change being made with the mouse -- a layer moved, transformed, adjusted -- before it is done LivePart : the view's pixels are composited straight from the layers at the view's size, and the full-size pixels change once, when it is put down touchWhole hands the old ones to the undo step without a copy . On a 70-megapixel photo, dragging a layer costs about 8 ms a frame and Free Transform 25 ms. The fonts. Every app's text is FreeSans unless it chooses otherwise: GNU FreeFont's sans in the manner of Helvetica and Arial , built into the app in its regular, bold, oblique and bold oblique. setUiFont ufSerif gives FreeSerif in the manner of Times , setUiFont ufMono FreeMono in the manner of Courier ; monospaced text code blocks, editors, hex is FreeMono whatever the UI's font, and serif families in HTML, on the canvas and in documents are FreeSerif. Italic and bold italic are the fonts' own italics, not slanted copies. Arabic is everywhere: FreeSans has none of its own, so its Arabic -- in every style -- is FreeSerif's FreeSerif Bold's in bold text , joined, lam-alef as one letter, right to left among Latin text, mirrored brackets; FreeMono draws its own in its regular and FreeSerif's in its other styles; a font of the user's without Arabic gets FreeSerif's the same way. The same holds in text boxes, the rich editor, htmlView , the canvas, PDF files and reports, and in documents retroui/textengine : FreeSerif's OpenType tables set the Arabic, as Amiri did before . Characters no built-in font has CJK, emoji are drawn by the platform. Right to left is the whole Unicode bidirectional algorithm UAX 9 : numbers and brackets, the direction marks, and the explicit embeddings, overrides and isolates U+202A..202E, U+2066..2069 -- every case of Unicode's conformance tests passes. The zero width non-joiner and joiner steer the joining Persian میخواهم and never cut a word in two; text is kept in its letters joined shapes pasted in, or read from an old PDF, come back as letters: plainLetters . In text boxes and the rich editor the caret goes through a line as it reads -- Left goes on in an Arabic line, a letter and its vowels are one step, Backspace takes a vowel off and Delete a letter with its vowels -- and stands where the next letter typed goes when Latin and Arabic meet; a selection of both is drawn where its parts are. PDF files carry Arabic as it is written /ActualText , the letters themselves and say their language lang ; an Arabic sheet written to Excel opens right to left XlsxSheet.direction . The Arabic interface setUiLanguage "ar", @toolkitArabic mirrors the frame, menus, lists, scroll bars and back and forward arrows; text boxes, documents and Markdown keep their own direction, and charts, maps, the media controls play, the time bar and pictures their sides. The fonts are subsets of GNU FreeFont 0412 made by tools/genfreefont.py from the fonts GNU publishes: Latin with Latin-1, Extended-A and -B, IPA , Greek, Cyrillic, Hebrew, Arabic FreeSerif, FreeMono , punctuation, currency, arrows, mathematical operators, box drawing, geometric shapes and the symbols the kit's own text uses -- the bold and italic styles the letters, their symbols coming from the regular -- with their OpenType kerning, marks, ligatures and Arabic forms, without the TrueType hinting. The twelve files take 0.94 MB in every app compressed, unpacked when a face is first used; the fonts before them took 0.96 MB, and apps with retroui/textengine carried Amiri's 0.21 MB more . Crisp at every scale. Glyphs are drawn by the kit's own TrueType engine retroui/ttf , at the screen's own resolution, on the CPU and the same masks on the GPU: the coverage of each pixel computed exactly, in gray, the baseline, x-height, cap height and ascender set on whole pixels at small sizes below 26 pixels to the em , the strokes as the font draws them FreeSans' stems, a little over a pixel at 12, keep their weight , letters placed at quarter pixels without drift; FreeMono's thin strokes half a pixel at 12 are darkened to a whole pixel there; small text is drawn with a light gamma 1.2 . Lines take the font's own line gap. Sharp text setSubpixelText true , or "Text edges: Sharp" in appearanceSettings ; RETRO TEXT=sharp draws the letters in the screen's red, green and blue strips, three times as fine across, as Windows' ClearType does, the stems then fitted to the pixels: off by default on every platform, used on screens of one device pixel per pixel it follows Windows' strip order, RGB or BGR . loadUiFontFile path gives a font of the user's a .ttf, or the first face of a .ttc; its bold, italic and bold italic beside it when they are, "X-Bold.ttf", "X-Italic.ttf"... ; an app built with -d:retroAppFont=path/X-Regular.ttf carries its own and starts with it useAppFont . Choose before app for one app's default, or at any time; uiFont tells which is on; appearanceSettings puts the choice in an app's Settings -- FreeSans, FreeSerif, FreeMono, a font file -- and storeAppearance / restoreAppearance keep it a choice stored before FreeFont is FreeSans now, a font file stays one . builtinUiFonts lists the built-in fonts for a menu of an app's own, uiFontKey / uiFontOfKey name them in its settings "sans", "serif", "mono"; the words of before -- "w98", "kawkab"... -- still read . The fonts' names of before compile for one release as deprecated aliases: ufW98 , ufRetro , ufSmooth , ufOpenSans are ufSans , ufKawkab , ufFixedsys ufMono . Licences. GNU FreeFont is under the GNU GPL v3 or later with the font exception "if you create a document which uses this font, and embed this font or unaltered portions of this font into the document, this font does not by itself cause the resulting document to be covered by the GNU GPL" , src/retroui/fonts/LICENSE-FreeFont.txt ; the subsets keep that text, and the files GNU publishes and their FontForge sources are where the licence says. The rest of the kit is MIT; the icon font is the Nerd Fonts symbols MIT and SIL OFL, src/retroui/fonts/LICENSE-NerdFonts.txt . An app that hands out its binaries hands out the fonts in it: keep the licence text with it the About box's credit is RetroFontCredit . Pixel icons retroui/pixelicons are used the same way, as text: toolButton pxCopy, "Copy", size = 1.3 a 3.1-style tool bar icon iconLabel pxFolder, "Documents" image x, y, glyphIcon pxComputer, black, 32 the 32x32 art at 32 label retroOr pxSave, nfSave & " Save" pixel icon, or the Nerd one There are 255 pixelIconNames lists them : folders and places, drives, file kinds, the tool bar's actions, the message box signs, the hourglass, charts, maps, mail, calendar, clock, media keys, an image editor's tools and panels, and more. Each has 16x16 and 32x32 art and is drawn at a whole number of screen pixels per art pixel; disabled ones look embossed. Set retroIcons = false to have retroOr and the toolkit's own parts use the Nerd icons. The art is original, drawn in the manner of 3.1, made by tools/genicons.py . Every control tells screen readers what it is, its name, its value and its state as it is drawn: Orca on Linux and BSD AT-SPI, over D-Bus, which the kit speaks itself , NVDA, JAWS and Narrator on Windows UI Automation , VoiceOver on macOS and iOS, TalkBack on Android. A label names the control after it, so a form needs nothing more: row 70, -1 : label "&Name:" read as the text box's name edit name if button "&Save" : save The screen reader moves the focus, presses buttons and menu items, checks boxes, picks list and grid items, tree nodes and calendar days, puts text in a box and moves sliders and spin boxes: each comes into the next frame as the click or the typing would. The grid is read as a table its headers, the rows in view, their cells , the tree with each item's depth and whether it is open, a chart by its title, kind and series and the point the keys are on. The tree is made only while a screen reader listens; RETRO ACCESS=1 makes it without one, to try it. What an app draws itself is not read until it says what it is: js let r = canvas dp 300 , dp 200 drawSales r accessNode arImage, "Sales by month", id = 0, r = r a picture, read by its name accessName "Delete" an icon-only button's name if toolButton nfTrash : deleteIt accessBegin / accessEnd group nodes a list of your own, its items accessNode arListItem, ... with accessItem , accessRange gives a value a range, accessible says whether a screen reader listens. A custom control takes a screen reader's press through interact , as a click. Input methods. What the system's input method is composing pinyin, kana, hangul, a phone keyboard's word is drawn underlined in the text box at the caret, and typed when it is settled: XIM on X11, IMM on Windows, marked text on macOS, GTK 4's preedit, the kit's own input connection on Android. ui.composing holds it for a text box of your own. Scaling. The window takes the scale of the screen it is on -- on X11 the monitor's DPI RandR , on Windows and macOS the system's -- and again when it moves to another monitor; at 125 %, 150 % or 175 % controls and text are the size asked for, and lines stay on whole pixels. grid shows rows that come from a proc, so 100,000 rows cost only what the visible ones cost. Click a header to sort, drag its edge to resize; arrows, Home, End, PageUp/PageDown; F2, a double click or typing edits an editable cell, Enter commits, Esc cancels. Pass marked for multi-select Ctrl/Shift click, Ctrl+A, a rectangle, check boxes on phones . Columns can hold more than text — the controls of a business screen: | Column | Cells show | Interaction | |---|---|---| | column , numberColumn | text, numbers right aligned | edited in place when editable | | checkColumn | a check box "1", "yes", "x"… | a click or Space flips it: ev.toggled | | colorColumn | a block of colour " RRGGBB", "green", "orange"… | status lights | | progressColumn | a bar "0.62" or "62%" | | | choiceColumn | text picked from a list | F4 or a click drops the list: ev.edited | | buttonColumn , linkColumn | a push button, a link | ev.clicked | | iconColumn | a pixel icon or icon-font glyph | | | customColumn | anything you draw in paint row, rect, selected | | style row, col gives any cell its own text and background colours and bold text; rowHeaders adds the record indicator column an arrow at the current row, a star while it is edited ; footer col adds a totals row. js var cols = @ column "Order", 160 , numberColumn "Amount", 90 , colorColumn "Status" , checkColumn "Paid" , choiceColumn "Store", "Kitchen", "Engineering" , 110 , buttonColumn "", 60 let ev = grid cols, orders.len, proc r, c: int : string = orders r c , sel, height = -1, rowHeaders = true, style = proc r, c: int : CellStyle = if c == 1 and isLate r : cellStyle fg = color 200, 0, 0 , bold = true else: CellStyle , footer = proc c: int : string = if c == 1: total else: "" if ev.edited: orders ev.row ev.col = ev.text typed, picked or a check box flipped if ev.clicked: openOrder ev.row its button form 0: field "&Customer:": comboEdit draft.customer, customers suggests as you type field "&Phone:": maskedEdit draft.phone, "+ " field "&Date:": datePicker draft.date Alt+Down or F4 drops it field "&Amount:": numberEdit draft.amount, lo = 0 field "&Period:": dateRange fromDay, toDay with This month, Last year... field "Status:": checkbox "&Paid", draft.paid case recordNav pos, customers.len, dirty |< < 3 of 20 | New Delete Save of raNew: addCustomer of raSave: saveCustomer else: discard - maskedEdit types into a mask digit, A letter, U upper case, X letter or digit, anything; Arabic-Indic digits count as digits ; maskFormat , maskFull , maskRaw read the value. - dualList available, chosen moves items between two lists buttons, double click, Enter, up/down order . - propertyGrid props, sel edits properties by category: text, numbers, yes/no, choices, colours and dates, with a help line. - treeGrid cols, rows, sel is a tree with columns a chart of accounts . - wizard open, title, step, steps, width : body is a dialog of steps with Back / Next / Finish; stepDone false keeps Next disabled. Date is a plain year, month, day with today , addDays , addMonths , weekday , parseDate , $ in dateStyle , presetRange . formatNumber / parseNumber read and write "1,234.50". examples/ledger.nim puts it together: a tree, a splitter, a sortable, editable grid, an invoice form, CSV import and export, reports, settings. chart draws 21 kinds of business chart from the same few lines: | Kind | | Kind | | |---|---|---|---| | chBar , chBarH | bars, up or sideways | chScatter , chBubble | points, sized points | | chStacked , chStacked100 | stacked, as shares | chWaterfall | changes adding up to a total | | chLine , chArea , chStackedArea | lines and areas smooth | chFunnel | the stages of a pipeline | | chCombo | bars with a line on a second axis | chGauge gauge | one value on a dial, a target, bands | | chPie , chDonut | shares the total in the hole | chCandle | open, high, low, close | | chHistogram | how values spread | chHeatmap | a table as colours | | chRadar | series around the labels | chTreemap | shares as rectangles | | chBullet | values against targets | | | js let months = "Jan", "Feb", "Mar", "Apr" chart "Sales", chBar, months, series "2025", 12.0, 15, 9, 20 , series "2026", 14.0, 18, 11, 24 chart "Revenue and margin", chCombo, months, series "Revenue", rev , lineSeries "Margin %", margin , opts = ChartOpts format: NumFormat prefix: "$", short: true , format2: NumFormat percent: true gauge "Target", 0.82, lo = 0, hi = 1, target = 0.9, format = NumFormat percent: true Straight from a CSV file comma, semicolon or tab; quotes; a BOM : let t = readCsv "sales.csv" let ev = chart "By region", chDonut, t, "Region", "Amount" , height = -1 if ev.clicked: showRegion ev.index a bar, point or slice or Enter exportPng ui.lastRect, "chart.png" the chart as a picture discard writeCsv "sales-copy.csv", t and a table back to CSV quoted where needed Every chart has round axis numbers, a legend click an entry to hide that series , a tooltip the mouse, a tap, or the arrow keys once it has the focus and numbers written as money, shares, or 1.2K / 3.4M NumFormat . ChartOpts sets the titles, axis names, legend place, data labels, axis range, bands and palette. Empty, missing NaN , negative and huge values are drawn as well as they can be, never a crash. A chart app in a minute. examples/charts.nim is a ready Chart Viewer: open a CSV, pick the kind, the label column and the value columns, and save the PNG. retrokit new sales --kind:charts starts your own. Invoices, quotes, statements and lists are laid out on pages and come out as PDF files, PNG pictures, an on-screen preview, or paper — all in Nim, with nothing to install, and linked only into apps that use them PDF alone adds about 45 KB . The fonts are embedded, the text in the PDF can be selected and searched, and Arabic is joined, runs right to left and is copied out of the file as it reads. From an HTML template and data — the way most business apps do it. Templates are Mustache style: {{name}} escaped , {{{raw}}} , {{ items}}…{{/items}} repeats, {{^items}} for "none", {{a.b}} , {{price|n2}} formats a number: python import retroui, std/json const tpl = """