Important
AppShell is a starting point, not a finished app. src/app/ is scaffolded with demo screens and a live settings panel so you can see every part of the shell working together — replace it with your own screens. The rendering core (window/, gpu/, ui/) is the part meant to stay.
- What is AppShell?
- Why This Project Exists
- Feature Overview
- Architecture
- Tech Stack & Rationale
- Repository Structure
- Getting Started
- Extending This
- Known Limitations
- Roadmap
- Contributing
AppShell is a small, native, GPU-accelerated UI boilerplate for Windows and Linux desktop apps. There's no Electron, no Chromium, no browser engine, no garbage collector — just C++, one window, and direct GPU calls through WebGPU.
It exists for one specific situation: you want a desktop app with a fully custom look (not the OS's native widget toolkit) and native performance, without paying the cost of an embedded browser to get there.
GLFW window → wgpu-native GPU surface → immediate-mode UI, redrawn every frame
Clone it, build it, and you get a resizable borderless window with a real custom title bar, a dashboard full of working demo widgets (Kanban board, timeline, mind-map canvas, activity heatmap, bento grid), and a settings screen that actually controls the rest of the app — a working reference for how every piece fits together, not just a blank canvas.
If you want a desktop UI that looks nothing like the OS's native widgets, and you don't want to pay Electron's or CEF's memory and binary-size cost to get it, your remaining options are usually "learn a native GUI toolkit's widget model" or "write directly against the GPU." AppShell takes the second path and does the unglamorous groundwork first, so a new project can start from "add my own screen" instead of "get a triangle on screen without crashing on resize."
That groundwork is what most from-scratch attempts stall on: a borderless window with a real custom title bar (drag-to-move, double-click-to-maximize, min/max/close — on the primary window and any window a tab gets detached into), DPI-safe rendering, real TrueType text instead of a bitmap font, image loading, scrolling and clipping regions, drag-and-drop, and multi-window support. AppShell ships all of it working together, plus enough demo screens to prove the shell can carry a real app's worth of UI, not just a static mockup.
Everything below reflects what's implemented in the current source, not a wishlist.
| Feature | Notes |
|---|---|
| Borderless window, custom title bar | Hand-drawn min/max/close, drag-to-move, double-click-to-maximize — identical behavior on the primary window and any detached tab window |
| Multi-window | Detach a tab into its own OS window, with a matching title bar and a "dock" button to bring it back |
| Responsive chrome | Sidebar auto-collapses to an icon rail below ~900px width; the dashboard grid reflows from 4 columns down to 1 as the window narrows |
| Two layout modes | Sidebar or top-nav, swapped live from Settings |
| Feature | Notes |
|---|---|
Immediate-mode 2D renderer (UiRenderer) |
Rects, rounded rects, circles, arcs, lines, and clipped scrolling regions, redrawn from state every frame |
| Real TrueType text | JetBrains Mono baked into a glyph atlas via stb_truetype, with five derived style variants (regular/bold/condensed/oblique/wide) |
| Image loading | Arbitrary PNG/JPEG textures via stb_image, cached by path, drawn as tinted quads or UV sub-rects for sprite sheets and icon atlases |
| 4x MSAA on every draw call | Shape edges, circles, and text are anti-aliased rather than jagged — not free with raw immediate-mode rendering |
| Widget | Notes |
|---|---|
| Scroll panels | Clipped, momentum-free scrolling regions used throughout |
| Tab bar | Rename, add, and detach-to-window, per tab |
| Kanban board | Draggable cards across columns; swimlanes modeled in state (demo renders one lane) |
| Timeline | Pan/zoom with tooltips |
| Heatmap | Activity-grid visualization |
| Node-graph canvas | Add/rename/delete/connect nodes, zoom-to-cursor |
| Bento grid | Responsive card grid |
| Workload chart, countdown timer | Additional dashboard-ready widgets |
Not a static mockup — theme (dark/light), layout (sidebar vs. top nav), density (comfortable vs. compact), font style, and feature toggles (animations, shadows) are wired to real state that every other screen reads on every frame.
Kept as separate, additive subsystems so you can delete what you don't need:
| Subsystem | What it demonstrates |
|---|---|
gpu/renderer3d |
A depth-tested 3D pipeline (indexed mesh + MVP uniform) — starting point for any 3D/model-viewer content |
gpu/instanced_quad_renderer |
One draw call for thousands of quads — particle systems, point clouds, tile grids |
gpu/signal_level_compute |
A real WGSL compute shader (parallel-reduction RMS/peak) — a template for GPU-side DSP or other compute workloads |
flowchart TD
A["GLFW window<br/>(borderless, custom title bar, drag/resize)"] --> B["GpuContext<br/>wgpu-native instance/adapter/device/surface"]
A --> C["Input events<br/>mouse, keyboard, scroll, file-drop"]
C --> D["main.cpp<br/>orchestrator: wiring input into AppState"]
D --> E["UiRenderer<br/>solid shapes · textured text · textured images<br/>shared clip/scissor + batching, 4x MSAA"]
E --> B
D --> F["widgets.h<br/>ScrollPanel · TabBar · KanbanBoard · Timeline ·<br/>GraphCanvas · Heatmap · Bento grid"]
F --> E
D --> G["app/ (AppState + screens.h)<br/>replace this with your app"]
G --> F
D -.optional, delete if unused.-> H["gpu/renderer3d<br/>gpu/instanced_quad_renderer<br/>gpu/signal_level_compute"]
H --> B
style D fill:#0D1526,stroke:#38BDF8,stroke-width:2px,color:#E2E8F0
style E fill:#0D1526,stroke:#4B8BBE,stroke-width:2px,color:#E2E8F0
style F fill:#0D1526,stroke:#10B981,stroke-width:1.5px,color:#E2E8F0
style G fill:#0D1526,stroke:#F59E0B,stroke-width:1.5px,color:#E2E8F0
style H fill:#0D1526,stroke:#6366F1,stroke-width:1.5px,color:#E2E8F0
main.cpp owns the window, the GPU device, and the per-frame render loop; it drives the UI by wiring input into AppState and dispatching to a Draw*Screen function per nav section, keeping the loop itself small and mostly free of app-specific logic. src/gpu/gpu_context.* owns the one wgpu-native instance/adapter/device/surface for the window, plus the MSAA color target and depth target every render pass uses — RenderFrame() is the entire per-frame render path. The optional 3D/instancing/compute subsystems under src/gpu/ are independent of the 2D UI path and of each other; none of them are required for the shell to run.
| Layer | Chosen | Why |
|---|---|---|
| Windowing / input | GLFW 3.4 (fetched from source) | Small, well-understood, cross-platform surface for window creation and raw input, without pulling in a full application framework. |
| GPU / rendering | wgpu-native (WebGPU → D3D12 on Windows, Vulkan on Linux) | One graphics API surface instead of maintaining separate D3D12 and Vulkan backends by hand, while still talking to the GPU directly — no engine, no scene graph, no retained-mode widget toolkit. |
| UI rendering | Hand-written immediate-mode renderer, real TrueType text via stb_truetype |
The entire UI is redrawn from application state every frame, batched into vertex buffers and submitted in one render pass — no DOM, no retained widget tree to keep in sync, and no bitmap-font ceiling on text quality. |
| Images | stb_image |
Single-header, dependency-light decoding for arbitrary PNG/JPEG textures, cached by path. |
| Build system | CMake 3.20+, GLFW fetched at configure time | Nothing to preinstall beyond a compiler, CMake, and the one precompiled dependency (wgpu-native) that CMake can't reasonably build from source itself. |
appshell/
├── CMakeLists.txt
├── src/
│ ├── main.cpp # orchestrator: window/GPU/UI setup, input, main loop
│ ├── window/
│ │ ├── window.cpp/.h # GLFW wrapper + input callbacks
│ ├── gpu/
│ │ ├── gpu_context.cpp/.h # wgpu-native instance/adapter/device/surface
│ │ ├── renderer3d.cpp/.h # optional: depth-tested 3D pipeline
│ │ ├── instanced_quad_renderer.cpp/.h # optional: instanced quad rendering
│ │ ├── signal_level_compute.cpp/.h # optional: WGSL compute shader demo
│ │ └── mat4.h
│ ├── ui/
│ │ ├── ui_renderer.cpp/.h # shapes, text, images - the 2D rendering core
│ │ ├── widgets.h # scroll panels, tabs, kanban, timeline, graph...
│ │ ├── theme.h # dark/light color palette
│ │ ├── anim.h # eased-interpolation helpers
│ │ ├── font_style.h # style variants derived from one TTF
│ │ ├── text_input.h # minimal single-line editor (renaming)
│ │ └── geom.h
│ ├── app/ # <-- replace this with your app
│ │ ├── app_state.h # persistent state, Screen enum, live settings
│ │ ├── screens.h # one draw function per nav section (demo content)
│ │ ├── detached_window.h # hosts a tab in a second OS window
│ │ └── asset_paths.h
│ └── assets/
│ ├── fonts/JetBrainsMono-Regular.ttf
│ └── images/demo_gradient.png
└── .gitignore
- CMake 3.20+
- A C++17 compiler (MSVC on Windows, GCC/Clang on Linux)
- Linux only:
libx11-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libgl1-mesa-dev - The matching wgpu-native binary for your platform — this is the one dependency CMake can't fetch and build from source (it's a precompiled Rust binary). Download it and place it under a shared
Project_Dependency/folder; see the comment block at the top ofCMakeLists.txtfor the exact expected layout, or override the location with-DPROJECT_DEPS_DIR=....
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --config ReleaseGLFW is fetched from source automatically. Fonts and image assets are copied next to the built executable as a post-build step, and main.cpp pins the process's working directory to the executable's own folder at startup, so relative asset paths resolve the same way regardless of how the binary is launched.
- Your app's screens: add a case to the
switchinmain.cpp'sDrawFrameand a matchingDraw*Screenfunction insrc/app/screens.h. Add persistent state toAppState. - Reskinning: edit
theme::Dark()/theme::Light()insrc/ui/theme.h. - A new font style: add an entry to
FontStylePresets()infont_style.h— no new glyph art needed, styles are derived from the one TTF. - A new layout mode: extend the
LayoutModeenum, add a case inChromeSidebarWidth()/ChromeTopBandHeight(), and add aDrawYourNavfunction inscreens.h. - More 2D primitives: add methods to
UiRenderernext toDrawCircle/DrawLine/ etc. - Don't need 3D / compute / instancing? Delete the corresponding file(s) under
src/gpu/, drop the matching lines fromCMakeLists.txt'sadd_executable, and remove their setup calls and screen cases frommain.cpp/screens.h. Each is self-contained and doesn't affect the 2D UI path. - A real UI toolkit instead: swap
UiRendererfor Dear ImGui once you outgrow immediate-mode —GpuContextalready exposes the rawdevice()/queue()/surface_format()its WebGPU backend needs. - macOS: add a
WGPUSurfaceSourceMetalLayerbranch togpu_context.cpp. Everything else (GLFW, CMake, the UI/widget layer) is already platform-generic.
Being direct about scope, so you know what you're picking up:
- Text input is intentionally minimal — no cursor movement, no selection, no copy/paste, ASCII only. Fine for renaming a tab or a node; not a real text field. See
text_input.h's header comment before extending it. - Kanban columns scroll as one region, not independently per column. Swimlanes are modeled (
KanbanSwimlane) but the demo board only renders one lane. SignalLevelCompute::Compute()blocks synchronously on the GPU readback — fine for an occasional demo call, not for a real-time hot loop. A production audio/DSP path should double-buffer and poll instead.- No macOS backend yet —
gpu_context.cpphas Windows (D3D12) and Linux (Vulkan/X11) surface creation only. - No native Wayland surface — GLFW is built with X11 only, which covers XWayland (the common case). Flip
GLFW_BUILD_WAYLANDon inCMakeLists.txtif you specifically need native Wayland.
-
WGPUSurfaceSourceMetalLayerbranch for macOS - Native Wayland surface option, built and documented
- Cursor movement, selection, and copy/paste in
text_input.h - Independent per-column scrolling and multi-swimlane rendering in the Kanban demo
- Double-buffered, non-blocking readback for
SignalLevelCompute
Issues and pull requests are welcome — especially around the rendering core (ui_renderer.cpp, widgets.h) and the optional GPU subsystems, since those are the parts most projects will build directly on top of.