<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sai Chandan Kadarla</title>
    <description>The latest articles on DEV Community by Sai Chandan Kadarla (@chan27).</description>
    <link>https://dev.to/chan27</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3971708%2Fe8958e27-fce1-4541-946e-c7c1259c5de5.jpg</url>
      <title>DEV Community: Sai Chandan Kadarla</title>
      <link>https://dev.to/chan27</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/chan27"/>
    <language>en</language>
    <item>
      <title>Building Vel: A Token-Efficient, Compile-to-Native UI Language</title>
      <dc:creator>Sai Chandan Kadarla</dc:creator>
      <pubDate>Sun, 07 Jun 2026 19:42:05 +0000</pubDate>
      <link>https://dev.to/chan27/building-vel-a-token-efficient-compile-to-native-ui-language-275f</link>
      <guid>https://dev.to/chan27/building-vel-a-token-efficient-compile-to-native-ui-language-275f</guid>
      <description>&lt;p&gt;I've been building &lt;strong&gt;Vel&lt;/strong&gt; — a new declarative UI language plus the runtime and component registry to back it. The goal is unapologetically ambitious: write production desktop apps with the expressive density of Tailwind classes, the reactivity model of SolidJS, and the rendering quality of Flutter, while letting you call into any existing C/C++ backend without a binding layer.&lt;/p&gt;

&lt;p&gt;This post walks through the architecture I landed on, why each tier exists, and what the language actually buys you in practice.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three-tier rule
&lt;/h2&gt;

&lt;p&gt;Most UI frameworks blur three concerns: the language, the runtime substrate, and the opinionated widget set. Vel splits them on purpose, with a strict one-way dependency:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;language  →  framework  →  registry  →  application
(velc)       (libvel)       (vel::ui::*)    (.vel + main.cpp)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Language&lt;/strong&gt;: a small, indentation-sensitive DSL (&lt;code&gt;.vel&lt;/code&gt;) and a single-pass compiler (&lt;code&gt;velc&lt;/code&gt;). It parses to an AST, type-checks against a primitive registry, and emits idiomatic modern C++ that consumes the framework.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Framework (&lt;code&gt;libvel&lt;/code&gt;)&lt;/strong&gt;: the substrate. Widget pipeline (measure / place / paint / tick), Skia-backed Painter, an event dispatcher, the reactive primitives (&lt;code&gt;Signal&amp;lt;T&amp;gt;&lt;/code&gt;, &lt;code&gt;Computed&amp;lt;T&amp;gt;&lt;/code&gt;, &lt;code&gt;AsyncSignal&amp;lt;T&amp;gt;&lt;/code&gt;), and infra (router, storage, net shim, IO, time, validate, notify, JSON, typed context). No application policy — no themes, no shortcuts, no business rules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Registry (&lt;code&gt;vel::ui::*&lt;/code&gt;)&lt;/strong&gt;: 43 baseline widgets in the shadcn idiom — Btn, Card, Tabs, Accordion, Dialog, Sheet, Table, TreeView, Stepper, Image, and so on. Built by composing framework primitives. Third parties can ship their own registries as plain &lt;code&gt;.vel&lt;/code&gt; files; the language has cross-file imports as a first-class feature.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Application&lt;/strong&gt;: your &lt;code&gt;.vel&lt;/code&gt; files and a thin &lt;code&gt;main.cpp&lt;/code&gt; shell. Themes, keyboard shortcuts, and routing decisions live here.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Code in a higher tier never reaches into a lower one. The framework never imports an icon set. The runtime never hardcodes a key combo. This isn't religion; it's the only way the language stays composable as third-party registries appear.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a DSL — and why compile to C++?
&lt;/h2&gt;

&lt;p&gt;Two reasons.&lt;/p&gt;

&lt;p&gt;First, &lt;strong&gt;source-token density&lt;/strong&gt;. The same UI in JSX vs. Vel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bg0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;borderRadius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fontWeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;700&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Hello&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;V p=lg g=md bg=bg0 r=md
  T "Hello" font=bold
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's roughly a 4–6× compression on real components without losing the declarative shape. For LLMs writing UI, that's the difference between fitting a feature in context and not. Vel was designed from day one with agent-authored code as a primary user.&lt;/p&gt;

&lt;p&gt;Second, &lt;strong&gt;no VM at runtime&lt;/strong&gt;. Vel compiles directly to typed C++ that calls into a static &lt;code&gt;libvel&lt;/code&gt;. There's no interpreter loop, no JIT, no garbage collector. Layout, paint, and event dispatch are all monomorphic virtual calls on stable widget instances. On idle, the app sits in &lt;code&gt;glfwWaitEventsTimeout&lt;/code&gt; and uses ~0 CPU — a global frame-dirty flag short-circuits the entire pipeline until something changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reactive substrate
&lt;/h2&gt;

&lt;p&gt;The keystone primitive is &lt;code&gt;Signal&amp;lt;T&amp;gt;&lt;/code&gt; — a value plus a listener list. Everything reactive flows from it.&lt;/p&gt;

&lt;p&gt;In the DSL, &lt;code&gt;@state&lt;/code&gt; declarations become typed &lt;code&gt;Signal&amp;lt;T&amp;gt;&lt;/code&gt; members on the generated component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#Counter
  @count = 0
  @doubled = $count * 2          // detected as derived → Computed&amp;lt;int&amp;gt;
  @users: std::vector&amp;lt;User&amp;gt; = await fetchUsers()  // → AsyncSignal&amp;lt;...&amp;gt;

  V g=md
    T "Count: {$count}, doubled: {$doubled}" font=bold
    Btn "++" -&amp;gt; click =&amp;gt; $count = $count + 1
    if $users.loading
      Spinner
    else
      for u in $users.value
        T "{u.name}"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What velc emits, semantically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Signal&amp;lt;int&amp;gt; count_{0}&lt;/code&gt; — plain reactive cell.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Computed&amp;lt;int&amp;gt; doubled_{ [this]{ return count_.get() * 2; } }&lt;/code&gt; — the codegen walks the init expression's AST, finds &lt;code&gt;$count&lt;/code&gt; as a dependency, and binds the computed to it. Any &lt;code&gt;count_.set(...)&lt;/code&gt; propagates: count → computed → component → &lt;code&gt;needsRebuild_ = true&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AsyncSignal&amp;lt;std::vector&amp;lt;User&amp;gt;&amp;gt; users_&lt;/code&gt; — kicked off with &lt;code&gt;std::async(std::launch::async, ...)&lt;/code&gt;, polled each tick. Exposes &lt;code&gt;.loading&lt;/code&gt; / &lt;code&gt;.ready&lt;/code&gt; / &lt;code&gt;.value&lt;/code&gt; / &lt;code&gt;.error&lt;/code&gt; as method calls in the DSL.&lt;/li&gt;
&lt;li&gt;The component constructor &lt;code&gt;.listen()&lt;/code&gt;s every signal it owns. The next &lt;code&gt;tick()&lt;/code&gt; calls &lt;code&gt;rebuild()&lt;/code&gt;, re-measures, and re-places before the frame paints. Tree rebuilds are coarse-grained — like React's &lt;code&gt;setState&lt;/code&gt;, not Solid's fine-grained reactivity — but the granularity is set per &lt;code&gt;#Component&lt;/code&gt;, so it composes cleanly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Effects (&lt;code&gt;~ $a $b =&amp;gt; body&lt;/code&gt;) compile to per-dep listeners — declarative side effects without polluting event handlers.&lt;/p&gt;

&lt;h2&gt;
  
  
  First-class C/C++ interop
&lt;/h2&gt;

&lt;p&gt;The single most important constraint I set: &lt;strong&gt;you must be able to drop any existing C++ codebase into Vel with one line.&lt;/strong&gt; No bindings, no schemas, no IDL. The language gives you two primitives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;use "myorg/db.h"        // raw C++ include — verbatim
use "ui/card.vel"        // cross-file vel import — pulls in components
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once a header is in scope, qualified names work in every expression. The lexer learned &lt;code&gt;::&lt;/code&gt; so &lt;code&gt;db::fetchUsers()&lt;/code&gt;, &lt;code&gt;std::vector&amp;lt;myorg::User&amp;gt;&lt;/code&gt;, and &lt;code&gt;await myapp::loadAsync()&lt;/code&gt; all parse and codegen straight through to the underlying calls. The framework knows nothing about your backend — velc just emits the include and the call site. Your existing C++ links into the binary like any other translation unit.&lt;/p&gt;

&lt;p&gt;This is the reason Vel exists at all. Every other "new UI language" I've tried makes interop a second-class citizen. Here, the interop is part of the type system from day one — state can be of any C++ type, expressions can call any user namespace, and the DSL's type annotations support templates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture of the rendering pipeline
&lt;/h2&gt;

&lt;p&gt;The render path is Skia all the way down, but Vel adds a few layout patterns worth noting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Constraints model&lt;/strong&gt;: identical to Flutter (min/max W/H), one measure pass returns intrinsic size, one place pass writes final rects. A small but important fix in Vel — when the cross axis is unbounded (e.g. inside a scrollable), &lt;code&gt;align=Stretch&lt;/code&gt; degrades to &lt;code&gt;Start&lt;/code&gt; so children don't get infinite size cascades.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frame-level damage tracking&lt;/strong&gt;: an atomic &lt;code&gt;frameDirty&lt;/code&gt; flag, raised by any &lt;code&gt;Widget::markDirty()&lt;/code&gt; call, controls whether the next frame runs at all. Animating widgets re-arm the flag from their &lt;code&gt;tick()&lt;/code&gt;; static pages don't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Portal layer for overlays&lt;/strong&gt;: tooltips, popovers, dialogs, sheets, menus, and toasts go through an &lt;code&gt;OverlayHost&lt;/code&gt; that owns a per-frame portal queue. Widgets nested inside scrolled containers register their panel rect plus the current scroll offset, so the panel paints in screen space regardless of where in the tree it was declared. This solved the classic "popover gets clipped by a Card with &lt;code&gt;clipContent&lt;/code&gt;" problem cleanly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DPR awareness&lt;/strong&gt;: the GL context is created at framebuffer resolution, the widget tree lays out in logical points, and the canvas matrix is scaled on each paint. Resize is handled by re-creating the Skia surface against the new framebuffer.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The registry as a primitive, not a library
&lt;/h2&gt;

&lt;p&gt;The 43-widget baseline ships in &lt;code&gt;vel/ui/&lt;/code&gt;, but the registry mechanic isn't reserved for the framework. Any &lt;code&gt;.vel&lt;/code&gt; file is a registry artifact. Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// widgets/stat.vel
#Stat label:str value:str hint:str=""
  Card elev=1
    V g=xs
      T $label fg=fgMuted font=uiSm
      T $value font=bold/display
      if $hint != ""
        T $hint fg=fgFaint font=uiSm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// showcase.vel
use "widgets/stat.vel"

#Showcase
  H g=md
    Stat label="MAU" value="42,103" hint="last 30 days"
    Stat label="Latency" value="84ms" hint="p99"
    Stat label="Errors" value="0.02%" hint="↓ 18%"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;velc resolves the import, parses the imported file to extract the component's param signature, and at the call site emits &lt;code&gt;std::make_unique&amp;lt;vel::gen::Stat&amp;gt;("MAU", "42,103", "last 30 days")&lt;/code&gt; — positional constructor args, type-checked statically by the C++ compiler downstream. Third parties can ship registries as bare &lt;code&gt;.vel&lt;/code&gt; files via git, vendoring, or eventually a package manifest.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's working today
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Compiler with lexer, parser (forward-progress-guarded), type checker, codegen, and &lt;code&gt;--watch&lt;/code&gt; mode.&lt;/li&gt;
&lt;li&gt;Reactive substrate: &lt;code&gt;Signal&amp;lt;T&amp;gt;&lt;/code&gt;, &lt;code&gt;Computed&amp;lt;T&amp;gt;&lt;/code&gt;, &lt;code&gt;AsyncSignal&amp;lt;T&amp;gt;&lt;/code&gt;, &lt;code&gt;PersistentSignal&amp;lt;T&amp;gt;&lt;/code&gt; (auto-syncs to disk), effects, typed &lt;code&gt;ctx&amp;lt;T&amp;gt;(key)&lt;/code&gt; Provider substrate.&lt;/li&gt;
&lt;li&gt;Framework primitives: &lt;code&gt;Router&lt;/code&gt; (path matching, history), &lt;code&gt;storage&lt;/code&gt; (JSON-backed KV), &lt;code&gt;net&lt;/code&gt; (pluggable HTTP shim), &lt;code&gt;io&lt;/code&gt; (file ops + watcher), &lt;code&gt;time&lt;/code&gt; (debounce / throttle / interval / timeout), &lt;code&gt;validate&lt;/code&gt; (form rules), &lt;code&gt;notify&lt;/code&gt; (toast bus), &lt;code&gt;json&lt;/code&gt; (wraps nlohmann), per-widget cursor states, theme switching with full tree rebuild.&lt;/li&gt;
&lt;li&gt;Registry: 43 widgets including Table (virtualized, sortable, multi-select), TreeView, Stepper, Image (Skia-decoded with LRU cache), and the full shadcn-style overlay set.&lt;/li&gt;
&lt;li&gt;DX: agent-readable &lt;code&gt;widgets.json&lt;/code&gt; regenerated on every build; cross-file imports; &lt;code&gt;--watch&lt;/code&gt; for hot iteration.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;The big open items are hot reload (file watch is in; &lt;code&gt;dlopen&lt;/code&gt; swap of the generated &lt;code&gt;.so&lt;/code&gt; is not), a real &lt;code&gt;vel.json&lt;/code&gt; package manifest, per-widget damage rectangles (right now damage is frame-level), and a few more registry primitives — Video, SVG, devtools overlay, a syntax-highlighted code editor.&lt;/p&gt;

&lt;p&gt;If the three-tier story holds — and so far it does — Vel ends up being the smallest source surface area you can use to ship a production native desktop app, with the deepest backend interop story of any framework I'm aware of.&lt;/p&gt;

&lt;p&gt;I'll keep posting as the registry grows.&lt;/p&gt;

</description>
      <category>uilang</category>
      <category>dsl</category>
      <category>cpp</category>
      <category>skia</category>
    </item>
    <item>
      <title>From Skia to Lume: writing my own 2D rendering engine for Vel</title>
      <dc:creator>Sai Chandan Kadarla</dc:creator>
      <pubDate>Sun, 07 Jun 2026 19:21:02 +0000</pubDate>
      <link>https://dev.to/chan27/from-skia-to-lume-writing-my-own-2d-rendering-engine-for-vel-1h37</link>
      <guid>https://dev.to/chan27/from-skia-to-lume-writing-my-own-2d-rendering-engine-for-vel-1h37</guid>
      <description>&lt;p&gt;Vel is about a week old. I started it as a DSL plus framework experiment, and from day one the rendering substrate was Skia. That wasn't an accident. Skia is the most complete 2D rendering API you can drop into a C++ project today. Clean canvas surface, built-in text with system font fallback, image decode, a GPU backend that already works on every desktop OS. If you want a UI framework drawing pixels by the end of the week, Skia is what you reach for.&lt;/p&gt;

&lt;p&gt;But the plan was always to replace it. Skia is a brilliant CPU-rasterization library bolted to a GPU backend, and as soon as you push it hard, the bolts show. Flutter publicly battled this same class of problems for years before they shipped &lt;a href="https://docs.flutter.dev/perf/impeller" rel="noopener noreferrer"&gt;Impeller&lt;/a&gt; and finally got rid of the runtime shader-compilation jank that made early Flutter apps stutter. I'd rather not repeat their story. So once the DSL was working and the framework was responding to my changes the way I wanted, I started writing the renderer I actually needed.&lt;/p&gt;

&lt;p&gt;The new engine is called &lt;strong&gt;Lume&lt;/strong&gt;. It lives in &lt;a href="https://github.com/chan27-2/Vel/tree/main/engine" rel="noopener noreferrer"&gt;&lt;code&gt;engine/&lt;/code&gt;&lt;/a&gt; of the Vel repo. This post is about why I started with Skia, why I'm replacing it now, and what Lume does differently.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fft1tyln4pkbe5ot91hnq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fft1tyln4pkbe5ot91hnq.png" alt="Vel's showcase app, every pixel painted by Lume." width="800" height="582"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Skia first
&lt;/h2&gt;

&lt;p&gt;Skia gave Vel three things I needed in the first few days of having a framework at all:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A clean &lt;code&gt;SkCanvas&lt;/code&gt; API the widget pipeline could draw into without me writing any GPU code.&lt;/li&gt;
&lt;li&gt;A working text renderer (CoreText on macOS, FreeType elsewhere, all behind the same API) with system font fallback.&lt;/li&gt;
&lt;li&gt;Image decode plus GPU upload as table-stakes, so Image widgets just worked.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That let me focus on the actual hard problem of the framework, the DSL and the reactive substrate. Layout, signals, hot-reload, event dispatch, the widget registry. The rendering substrate didn't need to be mine yet. Skia was a load-bearing dependency for exactly the amount of time it took the rest of the system to stop being the bottleneck.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I'm replacing it now
&lt;/h2&gt;

&lt;p&gt;Once the DSL and framework were in shape, I had a clear view of what the renderer was actually doing for me, and what it was going to cost as the surface area grew. Three things, all well-known to anyone who's tried to ship a Skia-based UI runtime at scale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Shader compilation jank.&lt;/strong&gt; Skia compiles shaders the first time it sees a new primitive &lt;em&gt;during the frame that wants to draw it&lt;/em&gt;. The first time you open a dialog with a blur, you pay 40 to 120 ms while Skia builds the right shader for the GPU. Flutter spent years trying to predict and pre-warm these (the infamous "skp shader cache") and never fully won. The Impeller team's own postmortem describes this as the engine's defining flaw.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Tessellation on the CPU.&lt;/strong&gt; Skia turns rounded rectangles, strokes, and curves into triangle meshes on the CPU, then ships them to the GPU. For one card it's free. For a table of 200 rows with rounded corners and hover highlights, the CPU is doing a lot of work that a fragment shader could do once and for all with &lt;a href="https://iquilezles.org/articles/distfunctions2d/" rel="noopener noreferrer"&gt;an analytic SDF&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The framework didn't own its render path.&lt;/strong&gt; This was the real one. Every cross-cutting question I expected to hit later (popovers clipping inside scroll views, text positioning in tight cells, atlas eviction policy, draw order across overlays, HiDPI handling) was eventually going to bottom out in Skia's behavior, and the answer was always going to be "work around it." When you don't control the rendering substrate, every one of those concerns is something you negotiate with a library that doesn't know what your widgets are.&lt;/p&gt;

&lt;p&gt;I'm not the first person to land here. Flutter, Servo, Bevy's UI work, Slint: every team building a rendering-heavy UI runtime has eventually concluded that owning the engine is the only way to make the rest of the system answer to one design. The cheaper time to do it is before you have a year of code depending on someone else's render path.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I borrowed from Flutter
&lt;/h2&gt;

&lt;p&gt;Impeller's defining decision is &lt;em&gt;ahead-of-time shader compilation&lt;/em&gt;. Every shader the engine could ever need is compiled at build time into Metal or Vulkan IR and bundled with the binary. The "first render is slow" problem goes away because there is no first render. Every shader has already been seen.&lt;/p&gt;

&lt;p&gt;That insight was the foundation. The other thing I borrowed: keep the pipeline list small. Impeller has on the order of a dozen pipelines, not hundreds. The way you do that is by reducing every primitive you draw to a small set of canonical shapes (rounded rects with optional ring strokes, textured quads, line segments) and varying their behavior through &lt;em&gt;uniforms&lt;/em&gt;, not new shaders.&lt;/p&gt;

&lt;p&gt;Lume's pipeline count today is four:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Shape&lt;/strong&gt;: analytic SDF rounded-rect. Fills, strokes, circles, lines, soft shadows all collapse to this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Line&lt;/strong&gt;: per-segment rotated quad with butt caps for polylines.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Text&lt;/strong&gt;: textured quad sampling an R8 glyph atlas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image&lt;/strong&gt;: textured quad sampling RGBA8 with corner-radius mask.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Every shape in the Vel showcase is one of those four primitives. A &lt;code&gt;roundedFill&lt;/code&gt; is the shape pipeline with &lt;code&gt;strokeWidth=0, radius=R&lt;/code&gt;. A &lt;code&gt;shadowRect&lt;/code&gt; is the same pipeline with &lt;code&gt;blur&amp;gt;0&lt;/code&gt;, which switches the fragment shader to a &lt;code&gt;smoothstep&lt;/code&gt; falloff instead of the AA clamp. A &lt;code&gt;circleStroke&lt;/code&gt; is a shape with &lt;code&gt;radius=w/2&lt;/code&gt;. The instance attributes do the heavy lifting; the GPU just rasterizes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Lume actually is
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fci36wcawc7jpvz7hfsgj.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fci36wcawc7jpvz7hfsgj.jpg" alt="Lume, a 2D rendering engine for Vel." width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The architecture is four layers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;L1  platform/   → CAMetalLayer attach (macOS). Future: ANativeWindow, HWND, canvas.
L2  gpu::Device → Dawn instance + adapter + device + queue (singleton).
L2  gpu::Surface → wgpu::Surface bound to the window's native layer.
L3  paint/      → DawnPainterImpl: four WGSL pipelines, glyph atlas,
                  per-instance state for shape/line/text/image, submission-
                  order draw segments.
L4  Painter API → public surface: fill, roundedFill, stroke, polyline,
                  arc, image, text, pushClip, pushTransform, and so on.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The whole stack is &lt;code&gt;engine/include/vel/&lt;/code&gt; (public headers) plus &lt;code&gt;engine/src/&lt;/code&gt; (about 3,000 lines of implementation). The framework calls into the Painter API and never sees a WebGPU type.&lt;/p&gt;

&lt;p&gt;Three details that took real effort:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The glyph atlas is keyed on physical pixel size.&lt;/strong&gt; When you ask for 14 px text on a 2× DPR display, FreeType rasterizes at 28 px. Lume's atlas cache key includes that physical size, so a window dragged to a 1× external monitor doesn't render upsampled-blurry text. It just rasterizes a second 14 px entry and uses that. The dst rect stays in logical pixels; the GPU samples the physical atlas 1:1.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Submission-order draw segments.&lt;/strong&gt; Originally Lume batched all-shapes, then all-lines, then all-text per frame. This broke the Table widget's sticky header: the header background was drawn before the row text, so row text overdrew the header bg, and rows became visible &lt;em&gt;through&lt;/em&gt; the header during scroll. The fix was to track a small &lt;code&gt;DrawCmd&lt;/code&gt; list (&lt;code&gt;{kind, firstInstance, count}&lt;/code&gt;) in submission order and emit one Draw call per segment. Same-kind cmds fuse. The Table works, and any widget that depends on draw order ("this card needs to be on top of those cards") works for the same reason.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Drag capture survives reactive rebuilds.&lt;/strong&gt; Vel is signal-driven. When the user drags a Slider, the slider writes to a signal, which triggers a re-render, which replaces the Slider widget instance. The new instance has &lt;code&gt;dragging_=false&lt;/code&gt;. The drag dies after one mouse-move event. The fix wasn't in Lume; it was in the framework's &lt;code&gt;EventDispatcher&lt;/code&gt;. &lt;code&gt;captureDrag(handler)&lt;/code&gt; registers a callable that closes over the slider's geometry plus its &lt;code&gt;onChange&lt;/code&gt; (whose own closure captures the long-lived owning component's &lt;code&gt;this&lt;/code&gt;). Mouse-move and mouse-up route to the captured handler directly, bypassing the widget tree. Drag continues across any number of rebuilds.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4pyh3vgzsfer6hmgdnao.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4pyh3vgzsfer6hmgdnao.png" alt="Cards, forms, and overlays from the showcase, all going through Lume's four pipelines." width="800" height="582"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Skia / Impeller / Lume comparison
&lt;/h2&gt;

&lt;p&gt;The dimensions that matter for a 2D UI runtime:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Skia (Vel v1)&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Impeller (Flutter)&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Lume (Vel today)&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Shader compilation&lt;/td&gt;
&lt;td&gt;JIT, at first-draw time&lt;/td&gt;
&lt;td&gt;AOT, build-time&lt;/td&gt;
&lt;td&gt;WGSL precompiled by Dawn at device init&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shape rendering&lt;/td&gt;
&lt;td&gt;CPU tessellation → GPU triangles&lt;/td&gt;
&lt;td&gt;Compute + tessellation hybrid&lt;/td&gt;
&lt;td&gt;Analytic SDF in the fragment shader&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pipeline count&lt;/td&gt;
&lt;td&gt;hundreds (one per primitive + state combo)&lt;/td&gt;
&lt;td&gt;~12&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Text&lt;/td&gt;
&lt;td&gt;CoreText / FreeType per platform&lt;/td&gt;
&lt;td&gt;Manual rasterizer → MTLTexture atlas&lt;/td&gt;
&lt;td&gt;FreeType → R8 atlas, OS/2 typo metrics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Idle frame cost&lt;/td&gt;
&lt;td&gt;Always paints&lt;/td&gt;
&lt;td&gt;Always paints&lt;/td&gt;
&lt;td&gt;~0 (frame-dirty flag short-circuits the whole pipeline)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HiDPI&lt;/td&gt;
&lt;td&gt;Surface scaled in canvas&lt;/td&gt;
&lt;td&gt;Per-pass DPR awareness&lt;/td&gt;
&lt;td&gt;Atlas keyed on physical px; dst rect in logical px&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-platform reach&lt;/td&gt;
&lt;td&gt;GL/Vulkan/Metal/D3D11&lt;/td&gt;
&lt;td&gt;Metal + Vulkan (+ work-in-progress)&lt;/td&gt;
&lt;td&gt;Dawn handles Metal/Vulkan/D3D12/WebGPU from one WGSL source&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Library code in libvel&lt;/td&gt;
&lt;td&gt;Skia + image codecs (~25 MB linked)&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;libvel.dylib&lt;/code&gt; size (macOS arm64)&lt;/td&gt;
&lt;td&gt;~30 MB&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;11 MB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hot-reload safety&lt;/td&gt;
&lt;td&gt;Crashes if plugin link drops Skia symbols&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;Plugin links the same &lt;code&gt;libvel.dylib&lt;/code&gt;; nothing else to share&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The single most useful number on that table is the bottom one. With Skia gone, the hot-reload plugin no longer needs to think about which graphics symbols it shares with the host. &lt;code&gt;libvel.dylib&lt;/code&gt; is the sole boundary. A hot reload re-emits a &lt;code&gt;.vel.cpp&lt;/code&gt;, recompiles 200 lines, and &lt;code&gt;dlopen&lt;/code&gt;s the new dylib in under a second.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2relz9oyy5auyx4mjqiu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2relz9oyy5auyx4mjqiu.png" alt="A virtualized Table with a sticky header, the test case that forced submission-order draw segments into existence." width="800" height="582"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What Lume doesn't do yet (the honest section)
&lt;/h2&gt;

&lt;p&gt;This is the first usable version of the engine, and I'd be lying if I said it was at parity with Skia for every workload. Three real gaps:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compute-shader Gaussian blur.&lt;/strong&gt; Lume's shadow is currently a &lt;code&gt;smoothstep&lt;/code&gt; outer falloff applied to the rounded-rect SDF. For small blur radii (4 to 16 px, which covers most UI shadows) it's perceptually identical to a Gaussian. For larger radii it reads as "the rect got bigger and softer at the edges" rather than a true Gaussian. A two-pass separable Gaussian in a compute pipeline is next; for now the cheap approximation is honest about what it is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Complex-script text shaping.&lt;/strong&gt; I link HarfBuzz; I don't drive it yet. Latin, Cyrillic, and Greek render correctly. Arabic ligatures, Devanagari conjuncts, vertical text: those are next. The FreeType path is in; the HarfBuzz shaping pass on top of it isn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The platform surface is macOS-only.&lt;/strong&gt; Dawn supports Vulkan and D3D12, so the underlying portability is real. The part missing is the window-to-surface glue. Lume has a &lt;code&gt;SurfaceMac.mm&lt;/code&gt; that attaches a &lt;code&gt;CAMetalLayer&lt;/code&gt; to a GLFW window's &lt;code&gt;NSView&lt;/code&gt;; the Windows and Linux equivalents are file-shaped holes today. CI builds compile against the abstraction, but the surface code is the actual port.&lt;/p&gt;

&lt;p&gt;The roadmap continues from here: native arcs and dashed strokes via additional pipelines, then HarfBuzz, then compute blur, then a Web target via Dawn plus Emscripten, then Windows and Linux surface layers, then partial-repaint damage rects. Owning the engine means the work is real, but at least it's bounded.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9jwllrxkgu58qjtf7ubs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9jwllrxkgu58qjtf7ubs.png" alt="Images, icons, and inputs in the showcase, all decoded through ImageIO and uploaded as  raw `wgpu::Texture` endraw ." width="800" height="582"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The journey is in the git log
&lt;/h2&gt;

&lt;p&gt;The proof that Lume isn't a paper exercise is the diff. The Skia removal was commit &lt;code&gt;0d7a8f4&lt;/code&gt;. The reorganized four-tier repo (Lume in &lt;code&gt;engine/&lt;/code&gt;, the framework in &lt;code&gt;framework/&lt;/code&gt;, the component registry in &lt;code&gt;registry/&lt;/code&gt;, and the .vel compiler in &lt;code&gt;velc/&lt;/code&gt;) is &lt;code&gt;f5b86af&lt;/code&gt;. &lt;code&gt;grep -rE 'Sk[A-Z]|sk_sp' engine framework registry velc&lt;/code&gt; returns zero hits. The dependency list in &lt;code&gt;vcpkg.json&lt;/code&gt; is six lines now, none of them Skia.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dawn          : GPU abstraction (Metal/Vulkan/D3D12/WebGPU)
freetype      : glyph rasterization
harfbuzz      : complex-script shaping (next)
glfw3         : windowing
spdlog        : logging
nlohmann-json : JSON for the framework
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want to read it, the code is at &lt;a href="https://github.com/chan27-2/Vel" rel="noopener noreferrer"&gt;github.com/chan27-2/Vel&lt;/a&gt;. The README's &lt;a href="https://github.com/chan27-2/Vel#lume--the-rendering-engine" rel="noopener noreferrer"&gt;Lume section&lt;/a&gt; walks the engine specifically; the &lt;code&gt;engine/&lt;/code&gt; tree on &lt;code&gt;main&lt;/code&gt; is the smallest version of "a 2D GPU rendering engine you can actually run" I know how to write.&lt;/p&gt;

&lt;p&gt;The lesson I'd take from this, and I'm saying this because I want to remember it later, is that &lt;em&gt;the rendering substrate is not a library decision&lt;/em&gt;. It's an architecture decision. The moment your framework needs to answer cross-cutting questions about hit-testing, atlas eviction, draw order, and HiDPI all at once, you can either keep negotiating with someone else's library or you can write your own. Flutter eventually came to the same conclusion. So did I, just earlier. The work is bigger than it looks. The result is that everything downstream of the renderer stops feeling like it's fighting the renderer.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpx40qo3dbzg0a30jfplz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpx40qo3dbzg0a30jfplz.png" alt="Lume v1, running the full Vel showcase on macOS." width="800" height="582"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>vel</category>
      <category>gpu</category>
      <category>rendering</category>
      <category>webgpu</category>
    </item>
    <item>
      <title>What's new in Vel: hot reload, an SDK, and a real editor</title>
      <dc:creator>Sai Chandan Kadarla</dc:creator>
      <pubDate>Sat, 06 Jun 2026 20:10:03 +0000</pubDate>
      <link>https://dev.to/chan27/whats-new-in-vel-hot-reload-an-sdk-and-a-real-editor-1kfd</link>
      <guid>https://dev.to/chan27/whats-new-in-vel-hot-reload-an-sdk-and-a-real-editor-1kfd</guid>
      <description>&lt;p&gt;When I posted &lt;a href="https://dev.to/blog/building-vel"&gt;Building Vel&lt;/a&gt; a month ago, the language and runtime worked end-to-end — but everything around them was painful. You had to clone the repo, wait 30–60 minutes for vcpkg to build Skia, restart the binary on every change, and write &lt;code&gt;.vel&lt;/code&gt; files in a plain text editor with no help.&lt;/p&gt;

&lt;p&gt;Here's what shipped to fix that. None of it changed what Vel &lt;em&gt;is&lt;/em&gt;. All of it changed what using Vel &lt;em&gt;feels like&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Flutter-style SDK
&lt;/h2&gt;

&lt;p&gt;The install path is now one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/chan27-2/vel/main/scripts/install-vel.sh | bash
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'eval "$(vel shell-init bash)"'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; ~/.zshrc
vel doctor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script downloads a prebuilt SDK archive for your OS and arch — &lt;code&gt;vel-sdk-&amp;lt;ver&amp;gt;-darwin-arm64.tar.gz&lt;/code&gt;, &lt;code&gt;linux-x64.tar.gz&lt;/code&gt;, &lt;code&gt;windows-x64.zip&lt;/code&gt; — drops it at &lt;code&gt;~/.vel/sdk&lt;/code&gt;, and adds &lt;code&gt;vel&lt;/code&gt; + &lt;code&gt;velc&lt;/code&gt; to your &lt;code&gt;PATH&lt;/code&gt;. No Skia compile. No vcpkg bootstrap. First run to working compiler: seconds, not hours.&lt;/p&gt;

&lt;p&gt;Windows users get the equivalent PowerShell line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;irm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;https://raw.githubusercontent.com/chan27-2/vel/main/scripts/install-vel.ps1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;iex&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model is Flutter's, deliberately:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Flutter&lt;/th&gt;
&lt;th&gt;Vel&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;~/flutter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;~/.vel/sdk&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;stable&lt;/code&gt; / &lt;code&gt;beta&lt;/code&gt; channels&lt;/td&gt;
&lt;td&gt;`vel channel stable\&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;{% raw %}&lt;code&gt;flutter upgrade&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;vel upgrade&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;flutter doctor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;vel doctor&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The piece Vel doesn't ship: your final binary still links Skia/GLFW via vcpkg, same as Flutter's platform embedders. The SDK gets you the compiler and the framework — your CMake handles the application link.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hot reload via dynamic plugins
&lt;/h2&gt;

&lt;p&gt;The biggest architectural change since the introduction post. &lt;code&gt;libvel&lt;/code&gt; is now a shared library, and the app shell can &lt;code&gt;dlopen&lt;/code&gt; a plugin — your widget tree compiled into its own dylib.&lt;/p&gt;

&lt;p&gt;The plugin ABI is one symbol:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="k"&gt;extern&lt;/span&gt; &lt;span class="s"&gt;"C"&lt;/span&gt; &lt;span class="n"&gt;vel&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Widget&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;vel_create_root&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The host watches the dylib's mtime every 50ms. When you save a &lt;code&gt;.vel&lt;/code&gt; file, a watch loop runs &lt;code&gt;cmake --build build --target showcase_plugin&lt;/code&gt; (typically 1–2s for a small edit). The dylib mtime ticks. The host destroys the current root, &lt;code&gt;dlclose&lt;/code&gt;s the old handle, &lt;code&gt;dlopen&lt;/code&gt;s the new one, and calls &lt;code&gt;vel_create_root()&lt;/code&gt; again. The window updates in place.&lt;/p&gt;

&lt;p&gt;One script wires it all together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./scripts/dev-hot.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the dev loop now: edit, save, see the UI change in under two seconds without restarting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it costs.&lt;/strong&gt; Signal state is reset on reload. If you had a form half-filled, those values are gone after the dylib swap — the new root is a fresh tree. This is a deliberate tradeoff: serializing state across an ABI boundary is the kind of feature that ends up dictating every widget's API. For a UI dev loop, "edit structure → see new structure" is what matters; the state comes back when you fill the form once.&lt;/p&gt;

&lt;p&gt;The other cost: &lt;code&gt;RTLD_LOCAL&lt;/code&gt; matters. Plugins are loaded with &lt;code&gt;dlopen(path, RTLD_NOW | RTLD_LOCAL)&lt;/code&gt; so symbol tables don't leak across reloads. Without it, the second load picks up stale vtables from the first plugin and crashes on the first virtual call. The teardown order also matters — destroy the old root &lt;em&gt;before&lt;/em&gt; &lt;code&gt;dlclose&lt;/code&gt;, because the destructors run vtables that live in the plugin's text segment.&lt;/p&gt;

&lt;h2&gt;
  
  
  A VS Code extension with real diagnostics
&lt;/h2&gt;

&lt;p&gt;Vel has &lt;a href="https://github.com/chan27-2/vel/tree/main/editors/vscode" rel="noopener noreferrer"&gt;a VS Code extension&lt;/a&gt; now — syntax highlighting, completion, hover, live diagnostics.&lt;/p&gt;

&lt;p&gt;The interesting part is where the data comes from. Two things were already true about &lt;code&gt;velc&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;velc --diagnostics-json file.vel&lt;/code&gt; emits structured parse + typecheck errors.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;velc --docs-json&lt;/code&gt; emits the complete widget registry — every widget, every prop, every event, every example — as JSON.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The extension is mostly a thin TypeScript shim over those two outputs. The language server runs &lt;code&gt;velc --diagnostics-json&lt;/code&gt; on save and surfaces the errors. Completion and hover read from &lt;code&gt;widgets.json&lt;/code&gt; — the &lt;em&gt;same&lt;/em&gt; file the docs site is built from. There is one source of truth for "what widgets and props exist," and the compiler owns it.&lt;/p&gt;

&lt;p&gt;This is the part I'm proudest of. The compiler doing double duty as the registry source means the editor experience never drifts from the language. Add a widget; the editor knows about it on the next sync. Rename a prop; the diagnostics flag every old call site.&lt;/p&gt;

&lt;h2&gt;
  
  
  A live theme configurator
&lt;/h2&gt;

&lt;p&gt;The framework now ships a &lt;code&gt;ThemeConfig&lt;/code&gt; global — accent, radius scale, dark/light — that any widget can flip:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Btn "Rose"   variant=outline -&amp;gt; click =&amp;gt; vel::useAccentRose()
Btn "Lg"     variant=outline -&amp;gt; click =&amp;gt; vel::useRadiusLg()
Btn "Light"  variant=outline -&amp;gt; click =&amp;gt; vel::useLightTheme()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are 7 accents (Indigo, Blue, Violet, Rose, Emerald, Amber, Slate), 6 radius scales (None → Full), and dark/light. A single setter call rebuilds the &lt;code&gt;Theme&lt;/code&gt; from the config and re-applies it across the tree. The whole UI re-tints in one frame.&lt;/p&gt;

&lt;p&gt;The detail that took thought: this used to crash. The old path called &lt;code&gt;root-&amp;gt;onThemeChanged()&lt;/code&gt; synchronously from inside the click dispatch, which rebuilt the entire widget tree — freeing the button that was still executing its own &lt;code&gt;onClick&lt;/code&gt; handler. Use-after-free, every time.&lt;/p&gt;

&lt;p&gt;The fix is deferred rebuild: set a &lt;code&gt;pendingThemeChange_&lt;/code&gt; flag and apply the theme at the start of the &lt;em&gt;next&lt;/em&gt; frame, after the current event dispatch has unwound. It's now the pattern for any widget action that mutates global state — locale switching will use it next.&lt;/p&gt;

&lt;h2&gt;
  
  
  EditableText: one widget, two surfaces
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Input&lt;/code&gt; and &lt;code&gt;Textarea&lt;/code&gt; shared ~95% of their code — UTF-8 cursor math, blink, key handling, focus sync, char input. They were drifting. Both have been refactored onto a shared &lt;code&gt;EditableText&lt;/code&gt; base:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EditableText&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Interactable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="nl"&gt;public:&lt;/span&gt;
    &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="n"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;function&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;function&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;onSubmit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;onMouseDown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Point&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MouseButton&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="c1"&gt;// final&lt;/span&gt;
    &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;onKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;               &lt;span class="c1"&gt;// final&lt;/span&gt;
    &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;onChar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                &lt;span class="c1"&gt;// final&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;tick&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                    &lt;span class="c1"&gt;// final&lt;/span&gt;

&lt;span class="nl"&gt;protected:&lt;/span&gt;
    &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;  &lt;span class="n"&gt;byteIndexAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Point&lt;/span&gt; &lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;         &lt;span class="c1"&gt;// override per layout&lt;/span&gt;
    &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;onEnterPressed&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;                     &lt;span class="c1"&gt;// Input submits, Textarea inserts \n&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Subclasses now only own their rendering and the click-to-cursor mapping. One place to fix backspace-at-position-zero. One place to keep the cursor on a UTF-8 boundary. Adding a &lt;code&gt;PasswordInput&lt;/code&gt; or a syntax-highlighted code editor is much smaller now — the base class owns the hard parts.&lt;/p&gt;

&lt;h2&gt;
  
  
  CI on Linux, macOS, and Windows
&lt;/h2&gt;

&lt;p&gt;Less glamorous, but it matters: GitHub Actions builds Vel on Ubuntu, macOS, and Windows on every push and PR. Each platform builds &lt;code&gt;velc&lt;/code&gt;, builds &lt;code&gt;libvel&lt;/code&gt;, runs CTest, and runs the showcase smoke test where a display exists.&lt;/p&gt;

&lt;p&gt;Two things were painful to get right:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;vcpkg caching.&lt;/strong&gt; Earlier iterations cached only &lt;code&gt;installed/&lt;/code&gt;, which left the cache unable to bootstrap on restore — the &lt;code&gt;.git&lt;/code&gt; directory and &lt;code&gt;scripts/&lt;/code&gt; were missing. Now the entire &lt;code&gt;vcpkg-root&lt;/code&gt; is cached, keyed on the manifest hash. CI cold start dropped from a full Skia build to a tar extract.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Headless runners.&lt;/strong&gt; macOS CI has no display; Windows Server runners ship without a usable OpenGL driver, so &lt;code&gt;glfwCreateWindow&lt;/code&gt; aborts the showcase. The &lt;code&gt;vel run&lt;/code&gt; script now detects &lt;code&gt;MINGW*/MSYS*/CYGWIN*&lt;/code&gt; and macOS CI and skips the GUI launch — compile + CTest still cover the toolchain.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Multi-platform CI is the kind of thing you don't notice until it breaks. It took three commits to get right, and I hope to never touch it again.&lt;/p&gt;

&lt;h2&gt;
  
  
  The showcase, reorganized
&lt;/h2&gt;

&lt;p&gt;A small one but worth flagging: &lt;code&gt;examples/showcase.vel&lt;/code&gt; used to be one long scrolling page exercising every primitive. It's now a multi-page demo with a sidebar:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Form Controls&lt;/li&gt;
&lt;li&gt;Buttons &amp;amp; Cards&lt;/li&gt;
&lt;li&gt;Data&lt;/li&gt;
&lt;li&gt;Overlays&lt;/li&gt;
&lt;li&gt;Theme&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Build, then &lt;code&gt;./scripts/vel run ./build/showcase&lt;/code&gt; — that's the first thing you see. It's also the integration test that exercises every widget, every prop, and every event.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Per-widget damage rectangles.&lt;/strong&gt; Frame-level damage tracking is great for idle apps; per-widget would let animations not repaint the whole screen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;vel.json&lt;/code&gt; package manifest&lt;/strong&gt;, so third-party registries become installable instead of vendored.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A &lt;code&gt;vel new&lt;/code&gt; template generator.&lt;/strong&gt; The boilerplate to start a Vel project is small but not zero.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More registry primitives&lt;/strong&gt; — Video, SVG, a devtools overlay, a syntax-highlighted code editor (which the &lt;code&gt;EditableText&lt;/code&gt; refactor now makes practical).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you tried Vel a month ago and bounced off the install, give it another go. &lt;code&gt;curl -fsSL .../install-vel.sh | bash&lt;/code&gt;, run &lt;code&gt;vel doctor&lt;/code&gt;, open the showcase. The first ten minutes look very different now.&lt;/p&gt;

</description>
      <category>uilang</category>
      <category>cpp</category>
      <category>dsl</category>
      <category>tooling</category>
    </item>
  </channel>
</rss>
