Skip to content

Repository files navigation

ZeppelinForms

Logo

ZeppelinForms (ZF) is an experimental project aimed at creating a simple, platform-independent UI framework with hardware acceleration (on Windows) and straightforward code-behind UI development.

βš™οΈ Current Status

CI

The project is under active development.

# Name Status
1 Headless βœ…
2 Windows βœ…
3 Linux (X11) βœ…
4 WebAssembly βœ…*
5 Android βš™οΈ
6 macOS πŸ’‘

* - WebAssembly: dialogs are async-only, and system drag and drop is not supported β€” see the browser section below.

🧠 Philosophy

In short: combine the simplicity of WinForms with selected ideas from WPF and Flutter, the cross-platform capabilities of Avalonia, and get rid of tons of legacy baggage along the way.

  • No dependency on a specific platform
  • No dependency on a specific graphics stack

And if the project is modern, why not take advantage of the full capabilities of .NET 10 and C# 14?

πŸ–ŒοΈ Rendering

# Name Status
1 SkiaSharp βœ…
2 DirectX πŸ’‘

ZeppelinForms itself knows nothing about Skia, because it is implemented in a separate project, ZeppelinForms.Skia. This means that the graphics layer is completely decoupled from the framework logic.

As a result, the framework can be integrated with DirectX or virtually any other graphics stack.

If hardware acceleration is unavailable, the framework falls back to software rendering.

πŸ“Ÿ Forms

Forms are the only type of window in ZeppelinForms, just like in WinForms.

# Name Status
1 Debugger βœ…*
2 Overlays βœ…**
3 Toast Notifications βœ…***
4 ToolTips βœ…
5 Dialog Windows βœ…
6 Open/Save File Dialog βœ…****
7 MessageBox βœ…
8 InputBox βœ…
9 Clipboard βœ…
10 Folder Dialog βœ…****
11 Drag&Drop (internal) βœ…

* β€” the inspector currently works only with certain types
** β€” the API will be extended further
*** β€” some limitations and unfinished parts remain
**** - managed implementation by default; a platform may supply a system picker through IFilePicker, as the browser backend does

πŸ›£οΈ Layout

  • Supports Measure and Arrange
  • All controls support docking
  • Supports horizontal and vertical alignment, including content alignment

🧩 Controls

All controls must inherit from UIElement, either directly or indirectly through UnitControl, PanelControl, or WrapControl.

There are no WinForms-style components that are not considered actual controls, such as Timer or BackgroundWorker.

UIElement

The common base type for all controls. A form is not a UIElement, but its Content can be any UIElement.

Unlike WinForms, all controls support:

  • Internal padding
  • Transparency
  • Shadows (box-shadow)
  • Scaling

Base class hierarchy

UIElement defines geometry, input and painting entry points. Decoration β€” background, corner radius, border β€” lives one level down. Inherit the closest base that already does what you need:

Draw is sealed in every Decorated* class: it fills the background, calls your content, then draws the border. Override these instead:

Base Override Sealed
DecoratedControl DrawContent (required), DrawDecoration Draw
DecoratedPanel DrawContent, DrawDecoration, MeasureContentOverride, ArrangeContentOverride Draw, MeasureOverride, ArrangeOverride
DecoratedWrapControl DrawContent, DrawDecoration Draw

DrawContent runs before children, DrawDecoration after them and outside their clip β€” that is where selection outlines, resize handles and drop indicators go. For state-dependent colors override CurrentBackground and CurrentBorderColor rather than painting the background yourself.

Shape is the one deliberate exception: shapes have their own Fill and Stroke, so an inherited Background would only confuse.

Styled Properties

Any property a theme may set must be declared as a styled property. A plain auto-property gets overwritten by the theme, is invisible to PropertyGrid and does not trigger a repaint. A source generator expands three lines into the registration, the backing field and the accessors:

[Styled(Category = "Menu")]
public partial Color HoverColor { get; set; }

private static Color HoverColorDefault => new(255, 232, 240, 254);

Requirements: the property is partial with a getter and a setter, its type is a partial descendant of UIElement. The default comes from a static property named <Name>Default; omit it when default(T) will do. It must be a property, not a field: static field initializers run in declaration order, and partial declarations are split across files, so a field could be read before it is computed.

Flags: AffectsLayout = true when the value changes measurement β€” the setter then invalidates layout instead of only repainting. Inherits = true when the value cascades down the tree, as TextColor does.

Value precedence, highest first:

  1. set from user code
  2. set from user code on any ancestor, for inherited properties
  3. a binding
  4. theme or style
  5. control default β€” SetControlDefault from a constructor
  6. DefaultValue from the registration

A constructor must use SetControlDefault: a plain assignment there would mark the value as user-set and lock the theme out for good. ZF0006 reports this.

External = true when the value lives in another object, as TextBox.Text does in its TextDocument. The generator then emits only the registration; the control writes the accessors itself, routing the setter through SetValue(Property, value) and exposing a Write<Name> method for the registration to call directly.

Unit Controls

UIElement β†’ UnitControl

Unit controls are similar to Control in WinForms and can be thought of as regular controls. They cannot contain child controls.

β„– Name Status β„– Name Status
1 Label βœ… 21 ToggleButton βœ…
2 Button βœ… 22 BarChart βœ…
3 CheckBox βœ… 23 LineChart βœ…
4 PictureBox βœ… 24 PieChart βœ…
5 RadioButton βœ… 25 RichLabel βœ…
6 TextBox βœ…* 26 LinkLabel βœ…
7 ToggleSwitch βœ… 27 LineShape βœ…
8 DateTimePicker βœ… 28 RectangleShape βœ…
9 TimePicker βœ… 29 EllipseShape βœ…
10 ColorPicker βœ… 30 PolygonShape βœ…
11 ScrollBar βœ… 31 CheckedComboBox βœ…
12 SvgIcon βœ… 32 ComboBox βœ…
13 NumericUpDown βœ… 33 GridSplitter βœ…
14 ProgressBar βœ… 34 MaskedTextBox βœ…
15 CircularProgressBar βœ… 35 HintLabel βœ…
16 TrackBar βœ… 36 MapControl βœ…
17 Calendar βœ… 37 Loader βœ…
18 MenuBar βœ… 38 PageIndicator βœ…
19 MenuList βœ…
20 SplitButton βœ…

* β€” contains bugs and is missing part of its API

Panels

UIElement β†’ PanelControl

Panels are controls that can contain other controls, including other panels.

# Name Status
1 Panel βœ…
2 StackPanel βœ…
3 Grid βœ…
4 DockPanel βœ…
5 TabControl βœ…
6 UniformGrid βœ…
7 VirtualizingStackPanel βœ…
8 SplitContainer βœ…
9 PageControl βœ…
10 WrapPanel βœ…
11 Table βœ…
12 AttachButton βœ…
13 PropertyGrid βœ…
Items Panels

UIElement β†’ PanelControl β†’ ItemsControl

A specialized type of panel capable of working with collections of elements.

# Name Status
1 ListBox βœ…
2 CheckedListBox βœ…
3 DragList βœ…
4 TreeView πŸ’‘
5 DataGrid πŸ’‘

⭐ All panels can display a scrollbar when their content overflows.

Wrapper Controls

UIElement β†’ WrapControl

Wrapper controls are controls that can contain a single child control. This concept is unusual in the WinForms world, but familiar from XAML-based frameworks.

# Name Status
1 Border βœ…
2 Spoiler βœ…
3 ZoomBox βœ…
4 GroupBox βœ…
5 LayoutBuilder βœ…
6 Page βœ…
7 GradientBorder βœ…
8 GripBox βœ…

πŸŽ„ Themes

Built-in light and dark themes are included. A theme is a set of appliers matched by control type; they are applied from the base type down, so a specific applier extends the base one instead of replacing it.

A theme never overwrites a value set from user code β€” see Styled Properties above for the full precedence. ClearValue gives a property back to the theme.

πŸ”— Bindings

Any styled property can be bound to a property of any object:

nameBox.Bind(TextBox.TextProperty, user, nameof(User.Name), BindingMode.TwoWay);
statusLabel.Bind(Label.TextProperty, user, nameof(User.Status));
saveButton.Bind(UIElement.IsEnabledProperty, user, nameof(User.HasChanges));

OneWay follows the source; TwoWay also writes back. A source implementing INotifyPropertyChanged pushes its changes to the target β€” without it a binding only reads the source once, when it is created.

A binding holds its target weakly, so a model outliving a window does not keep that window alive. Unbind drops one property, UnbindAll drops them all, and ClearValue removes the binding together with the value.

Assigning a bound property from code wins over the binding: in TwoWay the value goes to the source, in OneWay the binding is broken β€” otherwise the next source change would silently overwrite what was just written.

πŸ› οΈ Code Examples

Creating an application in Windows:

public class Program
{
    static void Main()
    {
        WindowsPlatform windowsPlatform = new();
        App myApp = new(windowsPlatform)
        {
            MainForm = new MainForm()
        };
        myApp.Run();
    }
}

Creating an application in Linux (X11):

public class Program
{
    static void Main()
    {
        X11Platform linuxPlatform = new();
        App myApp = new(linuxPlatform)
        {
            MainForm = new MainForm()
        };
        myApp.Run();
    }
}

Creating an application in the browser (WebAssembly):

public class Program
{
    static Task Main() => BrowserApp.RunAsync(
        () => new MainForm(),
        font: "/fonts/Inter-Regular.ttf",
        preload: ["/Assets/Logo.png"]);
}

A factory is passed rather than a form instance: a form measures text in its constructor, and the text measurer only exists once the platform has been created.

The browser has no system fonts and no local file system, so font and preload name files served from wwwroot. BrowserApp downloads them into the virtual file system before the first form is built, after which Image.LoadAsset and Font.WithFile work exactly as they do on the desktop.

Two host files are needed alongside the application. wwwroot/index.html:

<body>
    <canvas id="zf-canvas"></canvas>
    <script type="module" src="main.js"></script>
</body>

and wwwroot/main.js, which loads the runtime and wires up the JavaScript module:

import { dotnet } from "./_framework/dotnet.js";
import * as zf from "./zf.js";

const runtime = await dotnet.create();
runtime.setModuleImports("zf", { ...zf });

const exports = await runtime.getAssemblyExports("ZeppelinForms.Browser");
globalThis.zfExports = exports.ZeppelinForms.Browser.Interop;

await runtime.runMain();

zf.js ships with ZeppelinForms.Browser and has to be copied into the application's wwwroot. See examples/ZF Wasm for a complete project.

πŸ§ͺ Snapshot Tests

Reference snapshots are stored in:

tests/ZeppelinForms.UnitTests/Snapshots/Expected/{win,linux}

Text rendering differs between platforms, so separate snapshot sets are maintained for Windows and Linux.

Local snapshot update:

  • bash: ZF_UPDATE_SNAPSHOTS=true dotnet test
  • PowerShell: $env:ZF_UPDATE_SNAPSHOTS='true'; dotnet test

About

UI-framework with ideas WinForms, WPF, Avalonia and Flutter.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages