PanGui
PanGui

A cross-platform UI library with a razor sharp focus on performance, simplicity and expressive power

Sign up for news or beta access

PanGui is currently under development and is not yet available to the public. Sign up to the newsletter to stay up to date on PanGui's progress, or sign up to the beta program to help shape the future of PanGui when we begin bringing on beta testers.

There's a problem in the software industry

Most modern applications - composed of barely more than a few images, buttons and lines of text - sit uneasily upon towering tech-stacks comprising millions of lines of code. The industry is plagued by overcomplicated, slow and buggy software that is painful both to use and develop.

PanGui is our attempt to help solve the problem, by massively reducing the complexity of the tech stack required to produce good, modern applications.

PanGui has no dependencies at all, is as small as we could make it, and is extremely portable. It is very easy and simple to use, and is capable of creating sophisticated user interfaces with complex layouts, shapes, effects and animations, all while running at thousands of frames per second and having a small memory footprint.


Credit: XKCD                         

PanGui in a nutshell

Fully self-contained

PanGui has zero dependencies. It does not require a particular runtime, environment or third party library. Memory management, layouting, font processing, input handling and so on is all handled by PanGui.

Easy and fun to use

PanGui lets you solve actual problems with every line of code. There's no need to memorize a massive framework or wrangle tedious and restrictive boilerplate; basic programming skills is all you need.

Immediate and retained-mode

PanGui is immediate-mode first, with an optional retained-mode layer on top that ultimately reduces to immediate-mode calls, bringing you the best of both worlds.

Data-oriented

PanGui doesn't have a single abstract class, virtual method or component factory. There's no trick, really. It's just data, and the code that operates on that data.

Simple

PanGui is non-pessimized. Everything is as simple and straight-forward as possible; we have been merciless when deleting code and complexity that is not absolutely necessary.

Fast

When you stop tangling up a modern processor with spaghetti, it can really go! PanGui processes and renders most interfaces in fractions of a millisecond.

Layout-capable

PanGui boasts an extremely capable and expressive layouting system, providing all the features you'd expect of a top-of-the-line modern UI solution, and then some.

Stable in a single frame

Doing things the easy way is also doing things the right way. Idiomatic PanGui user code does not jitter and take multiple frames to "settle down"; it provides a correct and stable result in the first frame.

Testable

The input-output relationship of PanGui is extremely clear and simple, and every system is exposed and fully controllable by code, making it straight-forward to simulate any setup for testing.

Code-centric

The programmer is a first-class citizen of PanGui; every feature is fully usable and controllable by code. No secondary resources, stylesheets or markup documents are necessary.

Cross Platform

Since PanGui's only job is to turn input into a list of basic rendering commands, it is easily integrated into any potential environment and rendering pipeline.

Language-agnostic

PanGui's simplicity makes it easily transpilable into any target language and environment. It will feel and be native to the environment you're working in. More info in the FAQ section.

Demos

We're about to show you PanGui - in its current state!

PanGui is still under active development and this website currently shows only a subset of features. Many core features like the retained-mode layer and input controls are still in their early stages and are not presented here. It should go without saying that everything here is subject to change.

All these examples are written in C#, and are created using our Win32 platform integration. As such, they are using an API that is idiomatic to C# and made to feel nice to use in specifically C#.

Keep in mind that PanGui is designed to be language-agnostic and will be transpiled to many languages, with C++ being the next target language. Where necessary, the user-facing API will always change to feel natural to use in the target language.

Also note that the UI code itself does not care about the Win32 platform layer and will be identical when targeting other C# platforms.

Hello, world

Where else to start, but with "hello world"?

These examples show just how little is needed to get a basic PanGui application up and running, and then to draw something simple on the screen. They contain the entire user application codebase; no more code is necessary to create a Win32 window and draw a UI until the window is closed.

As you can see, it is extremely simple to draw primitives such as rects, triangles and text. PanGui has many layers of API granularity, with no tools - however sharp! - hidden away from the user. From simple shapes to complicated layouts, PanGui lets you work at exactly the level of complexity you want.


1using PanGui;
2
3public class Program
4{
5 public static void Main()
6 {
7 var gui = new Gui();
8 var win = new GuiWindow(gui);
9
10 win.RunGui(() =>
11 {
12 gui.DrawRect(gui.ScreenRect, 0x292929FF);
13 gui.DrawText("Hello, world!");
14 });
15 }
16}
Hello, world!

1var gui = new Gui();
2var win = new GuiWindow(gui);
3
4win.RunGui(() =>
5{
6 gui.DrawRect(gui.ScreenRect, 0x2A2929FF);
7 gui.DrawWindowTitlebar();
8
9 using (gui.Node().Expand().Gap(40).Margin(40).AlignContent(0.5f).Enter())
10 {
11 gui.DrawBackgroundRect(0x00000088, radius: 20);
12
13 float time = gui.Time.Elapsed * 2;
14 Vector2 center = gui.Node(200, 200).Rect.Center;
15 Vector2 p1 = center + new Vector2(-100 * Cos(time), -100 + Sin(time) * 10);
16 Vector2 p2 = center + new Vector2(+100 * Cos(time), -100 - Sin(time) * 10);
17 Vector2 p3 = center + new Vector2(0, 100);
18
19 gui.DrawTriangle(p1, p2, p3, Color.Red, Color.Green, Color.Blue);
20 gui.DrawText("Hello, Triangle!", color: Color.White, fontSize: 50);
21 }
22});

Audio App Demo

Next up, let's jump right into the deep end with a look at a more substantial example of a complete, slick-looking, polished user interface with animations, complex effects and shapes, and so on.

This is a demo of the user interface (and none of the audio functionality!) of a hypothetical audio app. We chose this "genre" of interface because audio applications often have very nice looking user interfaces with a lot of graphical complexity in the form of various visually styled shapes such as knobs, dials, buttons, keys and wave visualizations.

A bigger example like this of course requires a greater amount of code - approximately ~1500 lines of code (with lots of comments) - though we believe this example contains significantly less code (or text, or specification data, whatever you want to call it) than creating this UI would require using almost any other UI tech in existence. Furthermore, the code is very easy to understand and modify.

There is no cheating here, no hidden stylesheets or textures, no pre-built library of fancy buttons and effects. Every single part of this example UI is built from scratch using only PanGui's layouting, shape and effect primitives.

This is the entire application code, and all necessary secondary resources (IE, none); there is nothing else hidden away. If you were to import PanGui into an empty project and copy paste the example code given here, this user interface would appear on your screen.

This example is a good showcase of the sorts of "dog-fooding" tests we do during development. We're constantly using PanGui ourselves to develop demo applications and hobby projects, to ensure that it is easy to use and that all of our ideas work in practice.

UI Code
Utilities
Data

1using PanGui;
2using System.Numerics;
3
4// Style constants
5float gap = 8;
6float padding = 10;
7float borderRadius = 5;
8ImFont arial_normal = ImFont.LoadFont("arial");
9ImFont arial_bold = ImFont.LoadFont("arialbd");
10Icons.IconFont = ImFont.LoadFont("fa-regular-400");
11
12// Open a window and run the GUI
13Gui gui = new Gui();
14GuiWindow window = new GuiWindow(gui);
15
16window.RunGui(() =>
17{
18 // Set default font, text color, and size for the window
19 gui.SetTextFont(arial_normal);
20 gui.SetTextColor(0x747273FF);
21 gui.SetTextSize(18);
22
23 // Draw the window background
24 gui.DrawSdBackgroundRect().RadialGradientColor(0x292423FF, 0x322D29FF, 0, gui.ScreenRect.MaxDimension);
25
26 DrawTopToolbar();
27
28 // The root layout node that helps us layout all the main sections of the application
29 using (gui.Node().Expand().Gap(gap).Margin(gap).FlowDir(Axis.X).Enter())
30 {
31 // Section for instruments and snapshots on the left
32 using (gui.Node().Expand().Gap(gap).Enter())
33 {
34 // Draw the instrument controls
35 DrawInstruments();
36 }
37
38 // Section for track list and pads on the right
39 using (gui.Node(400, Size.Expand()).Gap(gap).Enter())
40 {
41 // Draw the library
42 using (gui.Node().Expand().Enter())
43 {
44 DrawLibrary();
45 }
46
47 // Draw the pad player
48 using (gui.Node().ExpandWidth().Enter())
49 {
50 DrawPadPlayer(padPlayer);
51 }
52 }
53 }
54
55 // Section for the piano, mod wheel, and additional controls at the bottom
56 using (gui.Node().ExpandWidth().Enter())
57 {
58 // PanGui lets you pass LayoutNodes around as arguments to functions, which enables a sort of
59 // semi-retained pattern within the immediate mode pattern - which is pretty cool!
60 // In this case, we give the DrawPianoSection function a LayoutNode to put the piano buttons into.
61 LayoutNode pianoButtonsLayoutNode;
62 float radius = 90;
63 SdShapePos s1 = SdShapePos.Rectangle(gui.CurrentNode.LastChild.OuterRect);
64 SdShapePos s2 = SdShapePos.RectangleRounded(gui.CurrentNode.FirstChild.ChildNodes[3].Rect.Expand(30,0).AddHeight(80), radius);
65 SdShapePos pianoBgShape = s1.SmoothUnion(s2, radius);
66
67 // Draw the snapshot controls and prepare layout for piano buttons
68 using (gui.Node().FlowDir(Axis.X).Gap(gap).Margin(gap).MarginTop(0).ExpandWidth().Enter())
69 {
70 // The mask to cut off some of the snapshot buttons with
71 SdShapePos mask = pianoBgShape.Expand(10);
72
73 DrawSnapshotButton(snapshots[0], default);
74 DrawSnapshotButton(snapshots[1], default);
75 DrawSnapshotButton(snapshots[2], mask);
76 pianoButtonsLayoutNode = gui.Node().ExpandHeight();
77 DrawSnapshotButton(snapshots[3], mask);
78 DrawSnapshotButton(snapshots[4], default);
79 DrawSnapshotButton(snapshots[5], default);
80 }
81
82 using (gui.Node().ExpandWidth().Enter())
83 {
84 DrawPianoSection(pianoButtonsLayoutNode, pianoBgShape);
85 }
86 }
87
88 return;
89});
90
91LayoutNode DrawSnapshotButton(Snapshot snapshot, SdShapePos mask)
92{
93 // Draws the horizontal list of buttons below the instruments and volume sliders
94 using (gui.Node(Size.Expand(), 60).Gap(10).Enter())
95 {
96 bool isSelected = snapshot == selectedSnapshot;
97 SdShapePos bgShape = SdShapePos.RectangleRounded(gui.CurrentNode.Rect.Expand(-3), borderRadius);
98
99 if (mask != default)
100 bgShape = bgShape - mask;
101
102 StyleButton(isSelected, bgShape);
103
104 gui.DrawText(snapshot.Name);
105
106 if (gui.CurrentNode.OnClick())
107 selectedSnapshot = snapshot;
108
109 return gui.CurrentNode;
110 }
111}
112
113void DrawInstruments()
114{
115 // You may have noticed that in most places, we enter nodes to add content to them.
116 // However, that is not the only way.
117 //
118 // This:
119 //
120 // using (gui.Node().Enter())
121 // {
122 // LayoutNode a = gui.Node();
123 // LayoutNode b = gui.Node();
124 // }
125 //
126 // Is the same as this:
127 //
128 // LayoutNode container = gui.Node().Enter();
129 // LayoutNode a = gui.Node();
130 // LayoutNode b = gui.Node();
131 // container.Exit();
132 //
133 // And it's also the same as this (less the state management aspects of entering and exiting scopes):
134 //
135 // LayoutNode container = gui.Node();
136 // LayoutNode a = container.AppendNode();
137 // LayoutNode b = container.AppendNode();
138 //
139 // This has many use cases, but in this case we simply use it to reduce the amount of nesting,
140 // and to make the code more readable.
141
142 LayoutNode container = gui.Node().Expand().FlowDir(Axis.X).Gap(gap);
143
144 // Draws the horizontal list of instruments, effects and volume sliders
145 foreach (ref Instrument instrument in selectedSnapshot.Instruments.AsSpan())
146 {
147 using (container.AppendNode().Expand().Enter())
148 {
149 StyleBox();
150
151 float offsetY = container.Rect.Height * 0.5f;
152 gui.DrawSdBackgroundRect(borderRadius)
153 .RadialGradientColor(new Color(instrument.Color, 0.2f), new Color(instrument.Color, 0), 0, 400, 0, -offsetY)
154 .RadialGradientColor(new Color(instrument.Color, 0.2f), new Color(instrument.Color, 0), 0, 200, 0, -offsetY);
155
156 Popup adsrPopup = GetPopup();
157
158 // Draw header
159 using (gui.Node().Enter())
160 {
161 StyleHeader();
162
163 ImRect graphRect = gui.CurrentNode.Rect.AlignRight(100).Padding(8);
164 InteractableElement graphInteractable = gui.GetInteractable(graphRect);
165 float graphHover = gui.AnimateBool01(graphInteractable.OnHover() || adsrPopup.IsVisible(), 0.2f, Easing.SmoothStep);
166 gui.DrawText(instrument.Name);
167 gui.DrawRect(graphRect.Expand(1), 4, 0x00000088);
168
169 DrawADSRGraph(graphRect, instrument.adsr, new Color(instrument.Color, 0.4f + graphHover * 0.4f));
170
171 if (graphInteractable.OnClick())
172 {
173 adsrPopup.Show();
174 }
175
176 if (adsrPopup.IsVisible())
177 {
178 DrawDefaultPopupTitleAndFooter(adsrPopup, "Attack, Decay, Sustain, Release");
179
180 using (adsrPopup.BodyContainer.Enter())
181 {
182 DrawADSRSettings(instrument.adsr);
183 }
184 }
185 }
186
187 // Fancy audio wave:
188 using (gui.Node().ExpandWidth().MarginBottom(padding).Enter())
189 {
190 gui.DrawBackgroundRect(0x00000044);
191
192 Color color = new Color(instrument.Color, 0.7f);
193
194 for (int n = 0; n < 2; n++)
195 {
196 var rect = gui.Node(Size.Expand(), 40).Rect;
197 var count = (int)((rect.Width - 20) / 2);
198
199 // Draw some random audio wave
200 if (count > 2)
201 {
202 unsafe
203 {
204 // Currently, we create the graph by manually modifying vertex data.
205 // Eventually, there will be a slightly higher level and simpler API
206 // for creating graphs from data.
207 ImVertexQuad* quads = gui.DrawList.AddTriangulatedQuads(count);
208 var r = rect.AlignLeft(2).AlignCenterY(2);
209 var center = rect.Center.Y;
210 var t = MathF.PI - gui.Time.Elapsed * 2 + n * 0.2f;
211
212 for (int i = 0; i < count; i++)
213 {
214 float f = i / (count - 1f);
215 f = MathF.Cos(t + f * MathF.PI * 6) * 0.5f + 0.5f;
216 f *= 0.2f + Math.Abs(ImMath.Noise(i * 3.04f, t * 0.4f));
217 r.Height = Math.Max(f * rect.Height * 0.5f, 2);
218 r.Y = center - r.Height * 0.5f;
219 quads[i].Set(r, color);
220 r.X += 2;
221 }
222 }
223 }
224 }
225 }
226
227 // Draw effect buttons
228 using (gui.Node().ExpandWidth().Padding(20).Gap(20).Enter())
229 {
230 foreach (ref var effect in instrument.Effects.AsSpan())
231 {
232 DrawEffectButton(ref effect, instrument.Color);
233 }
234 }
235
236 // Draw Pan
237 using (gui.Node().ExpandWidth().AlignContent(0.5f).MarginBottom(20).Gap(10).Enter())
238 {
239 DrawKnobSlider(0x74B8DBFF, 60, ref instrument.Pan, true);
240 gui.DrawText("Pan");
241 }
242
243 // Draw sliders
244 using (gui.Node().Expand().MarginBottom(40).Gap(5).FlowDir(Axis.X).AlignContent(0.5f).Enter())
245 {
246 int rulerMargin = 8;
247 LayoutNode leftRulerNode = gui.Node(8, Size.Expand()).Margin(0, rulerMargin);
248 LayoutNode sliderNode = DrawVolumeSlider(instrument.Color, ref instrument.Volume).Width(26);
249 LayoutNode rulerNumbersNode = gui.Node(10, Size.Expand()).ContentAlignX(0.5f).Spacing(Spacing.SpaceBetween).Margin(8, rulerMargin);
250 LayoutNode volumnBarNode = gui.Node(26, Size.Expand());
251 LayoutNode rightRulerNode = gui.Node(8, Size.Expand()).Margin(0, rulerMargin);
252
253 int rulerLineCount = 15;
254 using (rulerNumbersNode.Enter())
255 {
256 gui.SetTextSize(13);
257 gui.SetTextColor(0xffffff55);
258 for (int i = 0; i < rulerLineCount; i++)
259 gui.DrawText((i + 1) + "");
260 }
261
262 gui.DrawSdRect(volumnBarNode, 6).SolidColor(0x00000088).InnerShadow(0x00000099, 4);
263 (float left, float right) volume = GetSimulatedOutputVolume();
264 DrawVolumeBar(volumnBarNode.Rect.Padding(6).AlignLeft(5), volume.left * instrument.Volume);
265 DrawVolumeBar(volumnBarNode.Rect.Padding(6).AlignRight(5), volume.right * instrument.Volume);
266 gui.DrawLinesY(leftRulerNode, rulerLineCount, 0xffffff20, 2);
267 gui.DrawLinesY(rightRulerNode, rulerLineCount, 0xffffff20, 2);
268 }
269 }
270 }
271}
272
273unsafe void DrawADSRGraph(ImRect graphRect, ADSR adsr, Color color)
274{
275 Color colGradientTop = new Color(color, color.A * 0.4f);
276 Color colGradientBottom = new Color(color, 0);
277
278 graphRect = graphRect.Padding(5).AddY(5);
279
280 float totalWidth = graphRect.Width;
281 float totalHeight = graphRect.Height;
282
283 float smoothAttack = gui.Smoothdamp(adsr.Attack);
284 float smoothDecay = gui.Smoothdamp(adsr.Decay);
285 float smoothSustain = gui.Smoothdamp(adsr.Sustain);
286 float smoothRelease = gui.Smoothdamp(adsr.Release);
287
288 Vector2 pStart = graphRect.BL;
289 Vector2 pAttack = pStart + new Vector2(smoothAttack * totalWidth, -totalHeight);
290 Vector2 pDecay = pAttack + new Vector2(smoothDecay * totalWidth, (1 - smoothSustain) * totalHeight);
291 Vector2 pSustain = pDecay + new Vector2(0.15f * totalWidth, 0);
292 Vector2 pRelease = new Vector2(pSustain.X + smoothRelease * totalWidth, pStart.Y);
293
294 Span<Vector2> points = [pStart, pAttack, pDecay, pSustain, pRelease];
295 Span<float> curvePowers = [0.7f, 0.5f, 1, 0.5f];
296 Span<int> curveResolutions = [10, 10, 2, 10];
297
298 for (int i = 0; i < points.Length - 1; i++)
299 {
300 float pow = curvePowers[i];
301 Vector2 p1 = points[i];
302 Vector2 p2 = points[i + 1];
303 int nResolution = curveResolutions[i];
304 Vector2 pFrom = p1;
305 for (int j = 1; j < nResolution; j++)
306 {
307 float t = j / (nResolution - 1f);
308 Vector2 pTo = new(p1.X + (p2.X - p1.X) * (1 - MathF.Pow(1 - t, pow)), p1.Y + (p2.Y - p1.Y) * MathF.Pow(t, pow));
309
310 gui.DrawLine(pFrom, pTo, color, 2);
311
312 // Currently, we create the graph by manually modifying vertex data.
313 // Eventually, there will be a slightly higher level and simpler API
314 // for creating graphs from data.
315 ImVertexQuad* quad = gui.DrawList.AddTriangulatedQuad();
316 quad->SetColor(color);
317 quad->TL.Pos = pFrom;
318 quad->BL.Pos = new Vector2(pFrom.X, pStart.Y);
319 quad->TR.Pos = pTo;
320 quad->BR.Pos = new Vector2(pTo.X, pStart.Y);
321 quad->TL.Col = Color.Lerp(colGradientTop, colGradientBottom, 1 - (pStart.Y - pFrom.Y) / totalHeight);
322 quad->TR.Col = Color.Lerp(colGradientTop, colGradientBottom, 1 - (pStart.Y - pTo.Y) / totalHeight);
323 quad->BL.Col = colGradientBottom;
324 quad->BR.Col = colGradientBottom;
325
326 pFrom = pTo;
327 }
328 }
329}
330
331unsafe void DrawADSRSettings(ADSR adsr)
332{
333 // Draws the Attack, Decay, Sustain, Release settings for the selected instrument.
334 ImRect graphRect = gui.Node(700, 300).MarginBottom(gap).Rect;
335
336 gui.DrawLinesX(graphRect.HPadding(20), (int)(graphRect.Width / 20), 0xffffff11);
337 gui.DrawLinesY(graphRect.VPadding(20), (int)(graphRect.Height / 20), 0xffffff11);
338 gui.DrawSdRect(graphRect, borderRadius)
339 .OuterShadow(0xffffff44, new Vector2(0, 1), 1)
340 .InnerShadow(0x000000ff, new Vector2(0, 10), 40, -10)
341 .SolidColor(0x00000099);
342
343 float totalWidth = graphRect.Width - 90;
344 float totalHeight = graphRect.Height - 90;
345
346 Vector2 pStart = graphRect.Padding(20).BL;
347 Vector2 pAttack = pStart + new Vector2(adsr.Attack * totalWidth, -totalHeight);
348 Vector2 pDecay = pAttack + new Vector2(adsr.Decay * totalWidth, (1 - adsr.Sustain) * totalHeight);
349 Vector2 pSustain = pDecay + new Vector2(0.15f * totalWidth, 0);
350 Vector2 pRelease = new Vector2(pSustain.X + adsr.Release * totalWidth, pStart.Y);
351
352 Span<Vector2> points = [pStart, pAttack, pDecay, pSustain, pRelease];
353 Span<Color> colors = [0xF04848FF, 0xF0A948FF, 0x3BBE69FF, 0x5074ecff];
354 Span<float> curvePowers = [0.7f, 0.5f, 1, 0.5f];
355 Span<int> curveResolutions = [30, 30, 2, 30];
356 SdShape circle = SdShape.Circle(6);
357
358 for (int i = 0; i < points.Length - 1; i++)
359 {
360 Color color = colors[i];
361 Color colGradientTop = new Color(color, 0.4f);
362 Color colGradientBottom = new Color(color, 0);
363
364 gui.PushZIndex(gui.State.ZIndex + 1);
365 gui.DrawSdShape(points[i + 1], circle).SolidColor(color);
366 gui.PopZIndex();
367
368 float pow = curvePowers[i];
369 Vector2 p1 = points[i];
370 Vector2 p2 = points[i + 1];
371 int nResolution = curveResolutions[i];
372
373 Vector2 pFrom = p1;
374 for (int j = 1; j < nResolution; j++)
375 {
376 float t = j / (nResolution - 1f);
377 Vector2 pTo = new(p1.X + (p2.X - p1.X) * (1 - MathF.Pow(1 - t, pow)), p1.Y + (p2.Y - p1.Y) * MathF.Pow(t, pow));
378 gui.DrawLine(pFrom, pTo, color, 2);
379 ImVertexQuad* quad = gui.DrawList.AddTriangulatedQuad();
380 quad->SetColor(color);
381 quad->TL.Pos = pFrom;
382 quad->BL.Pos = new Vector2(pFrom.X, pStart.Y);
383 quad->TR.Pos = pTo;
384 quad->BR.Pos = new Vector2(pTo.X, pStart.Y);
385 quad->TL.Col = Color.Lerp(colGradientTop, colGradientBottom, 1 - (pStart.Y - pFrom.Y) / totalHeight);
386 quad->TR.Col = Color.Lerp(colGradientTop, colGradientBottom, 1 - (pStart.Y - pTo.Y) / totalHeight);
387 quad->BL.Col = colGradientBottom;
388 quad->BR.Col = colGradientBottom;
389 pFrom = pTo;
390 }
391 }
392
393 var sustainDragRect = new ImRect(pDecay.X - 10, pDecay.Y - 10, (pSustain.X - pDecay.X) + 20, 20);
394 InteractableElement sustainHandle = gui.GetInteractable(sustainDragRect);
395 var attackHandle = gui.GetInteractable(points[1], SdShape.Circle(20));
396 var decayHandle = gui.GetInteractable(points[2], SdShape.Circle(20));
397 var releaseHandle = gui.GetInteractable(points[4], SdShape.Circle(20));
398
399 if (attackHandle.OnHover() || attackHandle.OnHold()) gui.DrawCircle(points[1], 15, 0xffffff22);
400 if (decayHandle.OnHover() || decayHandle.OnHold()) gui.DrawCircle(points[2], 15, 0xffffff22);
401 if (sustainHandle.OnHover() || sustainHandle.OnHold()) gui.DrawRect(sustainDragRect, 15, 0xffffff22);
402 if (releaseHandle.OnHover() || releaseHandle.OnHold()) gui.DrawCircle(points[4], 15, 0xffffff22);
403
404 var mouseDelta = gui.Input.MouseDelta / graphRect.Size;
405
406 if (attackHandle.OnHold())
407 adsr.Attack = Math.Clamp(adsr.Attack + mouseDelta.X, 0, 1);
408
409 if (decayHandle.OnHold())
410 adsr.Decay = Math.Clamp(adsr.Decay + mouseDelta.X, 0, 1);
411
412 if (sustainHandle.OnHold())
413 adsr.Sustain = 1 - Math.Clamp((1 - adsr.Sustain) + mouseDelta.Y, 0, 1);
414
415 if (releaseHandle.OnHold())
416 adsr.Release = Math.Clamp(adsr.Release + mouseDelta.X, 0, 1);
417
418 using (gui.Node().ExpandWidth().Spacing(Spacing.SpaceEvenly).FlowDir(Axis.X).Enter())
419 {
420 DrawKnobSliderWithLabel(colors[0], ref adsr.Attack, "Attack", $"{adsr.Attack * 1000:0} ms");
421 DrawKnobSliderWithLabel(colors[1], ref adsr.Decay, "Decay", $"{adsr.Decay * 1000:0} ms");
422 DrawKnobSliderWithLabel(colors[2], ref adsr.Sustain, "Sustain", $"{adsr.Sustain * 100:0} %");
423 DrawKnobSliderWithLabel(colors[3], ref adsr.Release, "Release", $"{adsr.Release * 1000:0} ms");
424 }
425}
426
427void DrawEffectSettings(Popup popup, ref Effect effect)
428{
429 gui.CurrentNode
430 .Margin(gap)
431 .Gap(gap)
432 .FlowDir(Axis.X);
433
434 using (gui.Node().Padding(gap).AlignContent(0.5f).Enter())
435 {
436 StyleInnerPopupBox("Settings");
437
438 using (gui.Node().FlowDir(Axis.X).Wrap(3).Enter())
439 {
440 foreach (ref var knob in effect.KnobValues.AsSpan())
441 {
442 using (gui.Node().Padding(gap * 2).Gap(gap * 2).AlignContent(0.5f).Enter())
443 {
444 gui.DrawText("Low cut");
445 DrawKnobSlider(0xE6B455FF, 70, ref knob);
446 gui.DrawText(knob.ToString("0.0"));
447 }
448 }
449 }
450 }
451
452 using (gui.Node().Padding(padding).ExpandHeight().AlignContent(0.5f).Enter())
453 {
454 StyleInnerPopupBox("Output");
455
456 using (gui.Node().ExpandHeight().Padding(gap * 2, gap).FlowDir(Axis.X).Gap(40).Enter())
457 {
458 using (gui.Node().Padding(padding).ExpandHeight().Gap(gap).AlignContent(0.5f).Enter())
459 {
460 gui.DrawText("Dry");
461 DrawVolumeSlider(0xE6B556FF, ref effect.Dry).Width(30);
462 gui.DrawText(effect.Dry.ToString("0.0"));
463 }
464
465 using (gui.Node().Padding(padding).ExpandHeight().Gap(gap).AlignContent(0.5f).Enter())
466 {
467 gui.DrawText("Wet");
468 DrawVolumeSlider(0xE6B556FF, ref effect.Wet).Width(30);
469 gui.DrawText(effect.Wet.ToString("0.0"));
470 }
471 }
472 }
473
474 void StyleInnerPopupBox(string title)
475 {
476 gui.DrawSdBackgroundRect(borderRadius).SolidColor(0xffffff11);
477
478 using (gui.Node().ExpandWidth().AlignChildrenCenter().Margin(gap).MarginBottom(gap).Enter())
479 {
480 gui.DrawText(title);
481 }
482
483 var line = gui.Node(Size.Expand(), 2).MarginBottom(gap);
484 gui.DrawRect(line.Rect.Expand(gap, 0), 0x00000044);
485 }
486}
487
488void DrawPianoSection(LayoutNode pianoButtonsContainer, SdShapePos pianoBgShape)
489{
490 float popupVisibility = GetPopupVisibility();
491
492 // Draw the piano buttons always on top
493 gui.SetZIndex(1000);
494 gui.SetTransform(Matrix3x2.CreateScale(1, 1 + popupVisibility * 0.05f, gui.CurrentNode.Rect.BottomCenter));
495
496 gui.DrawSdShape(pianoBgShape.Pos, pianoBgShape.Shape)
497 .SolidColor(Color.Lerp(0x151313FF, 0x292423FF, popupVisibility))
498 .InnerShadow(new Color(0x00000088, 1 - popupVisibility), new Vector2(0, 40), 80, -40)
499 .InnerShadow(new Color(0x00000088, 1 - popupVisibility), new Vector2(0, 10), 20, -10)
500 .SolidColor(new Color(0x00000044, popupVisibility))
501 .InnerShadow(new Color(0xffffff11, popupVisibility), new Vector2(0, 40), 80, -40)
502 .OuterShadow(new Color(0x000000ff, popupVisibility), 20)
503 .Stroke(0x000000ff, 1, 0);
504
505 // Here we jump into the layout node we were passed, which was created separately.
506 using (pianoButtonsContainer.FlowDir(Axis.X).Enter())
507 {
508 float t = 0.5f;
509 DrawKnobSlider(0x74B8DBFF, 60, ref t).Margin(15);
510 DrawKnobSlider(0xE5B657FF, 60, ref t).Margin(15);
511 DrawKnobSlider(0x3DBF6BFF, 60, ref t).Margin(15);
512 }
513
514 using (gui.Node().ExpandWidth().FlowDir(Axis.X).Height(260).MarginTop(20).Padding(padding).Gap(gap).Enter())
515 {
516 // Draw mod wheel
517 {
518 var leftRuler = gui.Node(10, Size.Expand()).Spacing(Spacing.SpaceBetween);
519 var wheel = gui.Node(60, Size.Expand()).Rect;
520 var rightRuler = gui.Node(10, Size.Expand()).Spacing(Spacing.SpaceBetween);
521 var handle = wheel.Padding(0, 5).AlignTop(30).AddY(modWheel * (wheel.Height - 36));
522 var shadowShape = SdShape.Circle(300).MoveX(-220) * SdShape.Rectangle(400, wheel.Height).MoveX(180);
523 var handleShape = SdShape.RectangleRounded(wheel.Width, 20, 5);
524
525 // shadow
526 gui.DrawSdShape(wheel.Center, shadowShape)
527 .DiamondGradientColor(0x000000cc, 0x00000000, 50);
528
529 // wheel
530 gui.DrawSdRect(wheel, 5)
531 .OuterShadow(0xffffff22, new Vector2(0, 3), 3)
532 .SolidColor(0xffffff11)
533 .Stroke(0x000000ff, 3);
534
535 // handle
536 gui.DrawSdRect(handle)
537 .LinearGradientColor(0x00000088, 0xffffff11, Angle.Turns(-0.25f), 0.54f)
538 .InnerShadow(0x000000ff, new Vector2(0, 1), 1, 1)
539 .InnerShadow(0xffffff22, new Vector2(0, -1), 1, 1);
540
541 // lighting
542 gui.DrawSdRect(wheel, 5)
543 .InnerShadow(0xffffff11, new Vector2(8, 0), 5f, -5) // left light
544 .InnerShadow(0x000000cc, new Vector2(-8, 0), 5f, -5) // right shadow
545 .InnerShadow(0x000000cc, new Vector2(0, 10), 10f, -10) // top shadow
546 .LinearGradientColor(0x000000ee, 0x00000000, MathF.PI * 0.5f, 0.6f, offsetY: -0.5f) // 3d effect
547 .LinearGradientColor(0x00000000, 0x00000044, MathF.PI * 0.5f, 0.8f, offsetY: 0.8f); // 3d effect
548
549 // Draw left and right rulers:
550 {
551 var numLines = 20;
552 for (int i = 0; i < numLines; i++)
553 {
554 float f = i / (float)numLines;
555 var left = leftRuler.AppendNode(Size.Expand(), 2);
556 var right = rightRuler.AppendNode(Size.Expand(), 2);
557 float brightness = gui.Smoothdamp(f >= modWheel ? 1 : 0f, 10);
558 Color color = Color.Lerp(0x000000ff, 0xE6B455FF, brightness);
559 float outerShadow = brightness * 8;
560 gui.DrawSdRect(left.Rect, 1).SolidColor(color).OuterShadow(new Color(color, 0.2f), outerShadow, 2);
561 gui.DrawSdRect(right.Rect, 1).SolidColor(color).OuterShadow(new Color(color, 0.2f), outerShadow, 2);
562 }
563 }
564
565 InteractableElement e = gui.GetInteractable(wheel);
566 if (e.OnHold(out var args))
567 {
568 var slideArea = wheel.Padding(0, handle.Height * 0.5f);
569 modWheel = Math.Clamp(gui.Input.MousePosition.Y - slideArea.Top, 0, slideArea.Height) / slideArea.Height;
570 }
571 }
572
573 gui.Node(gap);
574
575 using (gui.Node().Expand().Gap(4).Enter())
576 {
577 float numKeys = 7 * 12f;
578
579 foreach (var instrument in selectedSnapshot.Instruments)
580 {
581 Color color = instrument.Color;
582 float keyStart = gui.Smoothdamp((1f / numKeys) * instrument.KeyStart);
583 float keyLength = gui.Smoothdamp((1f / numKeys) * instrument.KeyLength);
584
585 ImRect rangeRect = gui.Node(Size.Expand(), 14).Rect;
586 rangeRect.X += keyStart * rangeRect.Width;
587 rangeRect.Width *= keyLength;
588
589 // bg
590 gui.DrawSdRect(rangeRect, 7)
591 .SolidColor(0x00000088)
592 .OuterShadow(0xffffff22, new Vector2(0, 1), 2);
593
594 // glow
595 gui.DrawSdRect(rangeRect.Padding(2), 5)
596 .SolidColor(color)
597 .InnerShadow(0x000000aa, new Vector2(0, 0), 8.2f, -2);
598 }
599
600 // Draw Piano
601 ImRect piano = gui.Node().MarginTop(10).Expand().Rect;
602 DrawKeyboard(piano, keyPressures);
603 }
604 }
605}
606
607void DrawPadPlayer(PadPlayer padPlayer)
608{
609 gui.CurrentNode.Padding(padding).Gap(gap);
610
611 StyleBox();
612
613 using (gui.Node().Gap(gap).FlowDir(Axis.X).ExpandWidth().Enter())
614 {
615 using (gui.Node().ExpandWidth().Gap(gap).Enter())
616 {
617 gui.SetTextSize(22);
618
619 using (gui.Node().ExpandWidth().FlowDir(Axis.X).Gap(gap).Wrap(4).Enter())
620 {
621 for (int i = 0; i < padPlayer.Pads.Length; i++)
622 {
623 using (gui.Node().ExpandWidth().Height(Size.Ratio(1)).Enter())
624 {
625 StyleButton(i == padPlayer.SelectedPad);
626 gui.DrawText(padPlayer.Pads[i]);
627
628 if (gui.CurrentNode.OnClick())
629 {
630 padPlayer.SelectedPad = i;
631 }
632 }
633 }
634 }
635 }
636
637 using (gui.Node().ExpandHeight().Width(80).MarginBottom(20).AlignContent(0.5f).Gap(20).Enter())
638 {
639 DrawVolumeSlider(0xC2984BFF, ref padPlayer.Volume).Width(25);
640 gui.SetTextSize(22);
641 gui.SetTextFont(arial_bold);
642 gui.DrawText("-5 dB");
643 }
644 }
645
646 using (gui.Node().FlowDir(Axis.X).Gap(gap).AlignContent(0.5f).Enter())
647 {
648 DrawKnobSliderWithLabel(0xE5B458FF, ref padPlayer.Brightness, "Brightness", $"{padPlayer.Brightness * 100:0}%");
649 gui.Node(30);
650 DrawKnobSliderWithLabel(0xE5B458FF, ref padPlayer.Shimmer, "Shimmer", $"{padPlayer.Shimmer * 100:0}%");
651 }
652}
653
654void DrawKnobSliderWithLabel(Color color, ref float value, string label, string valueLabel)
655{
656 using (gui.Node().FlowDir(Axis.X).AlignContent(0.5f).Gap(gap).Enter())
657 {
658 DrawKnobSlider(color, 70, ref value);
659
660 using (gui.Node().Gap(10).Enter())
661 {
662 gui.DrawText(label, Color.White);
663 gui.DrawText(valueLabel, (Color)0xffffff88);
664 }
665
666 Color col = 0xF18B46FF;
667 }
668}
669
670LayoutNode DrawKnobSlider(Color color, Size size, ref float value, bool isPan = false)
671{
672 ref float t = ref gui.Smoothdamp(value);
673 LayoutNode node = gui.Node(size);
674 float angleOffset = 0.1f;
675 float arcLength = Angle.Turns(1 - angleOffset * 2);
676 float arcStart = Angle.Turns(0.25f + angleOffset);
677
678 if (node.IsVisible())
679 {
680 Vector2 center = node.Rect.Center;
681 float arcThickness = 3;
682 float arcRadius = node.Rect.Width * 0.5f - arcThickness;
683 float knobRadius = node.Rect.Width * 0.5f - arcThickness - 8;
684
685 SdShape knobShape;
686 SdShape arcSliceShape;
687
688 if (isPan)
689 {
690 knobShape = SdShape.Circle(knobRadius - 4);
691 float panLength = Math.Abs(0.5f - t);
692 float len = arcLength * panLength;
693 float rot = len * 0.5f - Angle.Turns(0.25f);
694
695 if (t < 0.5f)
696 {
697 rot -= len;
698 }
699
700 arcSliceShape = SdShape.Pie(arcRadius * 2, len)
701 .Rotate(rot);
702
703 arcSliceShape *= (knobShape + 10);
704 arcSliceShape -= knobShape;
705 arcSliceShape -= 3;
706 }
707 else
708 {
709 float rotation = arcLength * t * 0.5f + MathF.PI * 0.5f + Angle.Turns(angleOffset);
710
711 knobShape = SdShape.Union(SdShape.Circle(knobRadius - 4), SdShape.EquilateralTriangle(knobRadius).MoveY(-10), smoothness: 10).Rotate(arcStart + arcLength * value + MathF.PI * 0.5f);
712 arcSliceShape = SdShape.Pie(arcRadius * 2, arcLength * t).Rotate(rotation);
713 arcSliceShape *= (knobShape + 10);
714 arcSliceShape -= knobShape;
715 arcSliceShape -= 3;
716 }
717
718 // Bg
719 gui.DrawSdShape(center, knobShape + 10)
720 .InnerShadow(0x00000022, new Vector2(0, 10), 20, -10)
721 .InnerShadow(0x000000ff, new Vector2(0, -10), 20, -10)
722 .SolidColor(0x00000088);
723
724 gui.DrawSdShape(center, arcSliceShape)
725 .SolidColor(color);
726
727 //Handle
728 gui.DrawSdShape(center, knobShape)
729 .LinearGradientColor(0xB9B1AFFF, 0x484443FF, Angle.Turns(-0.25f))
730 .OuterShadow(0x000000cc, new Vector2(0, knobRadius * 0.5f), knobRadius * 1.5f, knobRadius * 0.2f)
731 .InnerShadow(0xffffff22, new Vector2(0, 1), 1, 1);
732 }
733
734 InteractableElement e = gui.GetInteractable(node.Rect);
735 if (e.OnHold())
736 {
737 value += -gui.Input.MouseDelta.Y / 1000;
738 value += gui.Input.MouseDelta.X / 1000;
739 value = Math.Clamp(value, 0, 1f);
740
741 t = value;
742 }
743
744 return node;
745}
746
747void DrawLibrary()
748{
749 StyleBox();
750
751 using (gui.Node().Enter())
752 {
753 StyleHeader();
754 gui.DrawText("Library");
755 }
756
757 using (gui.Node().Expand().Margin(1).Enter())
758 {
759 gui.ScrollY(0x00000055, 0xffffff22);
760
761 foreach (string title in trackLibrary)
762 {
763 bool isSelected = title == selectedTrack;
764 using (gui.Node().ExpandWidth().Padding(padding, 10).Enter())
765 {
766 if (isSelected)
767 {
768 gui.DrawBackgroundRect(0x00000077);
769 gui.SetTextColor(0xffffff99);
770 }
771
772 gui.DrawText(title);
773
774 if (gui.CurrentNode.OnClick())
775 {
776 selectedTrack = title;
777 }
778 }
779 }
780 }
781}
782
783void DrawTopToolbar()
784{
785 using (gui.Node()
786 .ExpandWidth()
787 .AlignContent(0.5f)
788 .Padding(gap)
789 .Gap(20)
790 .FlowDir(Axis.X)
791 .Enter())
792 {
793 gui.SetZIndex(1000);
794
795 gui.DrawSdBackgroundRect()
796 .SolidColor(Color.Lerp(0x252323FF, 0x393433FF, 1))
797 .SolidColor(new Color(0x00000044, 1))
798 .InnerShadow(new Color(0xffffff11, 1), new Vector2(0, 40), 80, -40)
799 .OuterShadow(new Color(0x000000ff, 1), 10)
800 .Stroke(0x000000ff, 1, 0);
801
802 gui.DrawText("Some Logo", Color.White).MarginLeft(10);
803
804 // Spacer
805 gui.Node().Expand();
806
807 {
808 DrawToolbarButtonTemplate(out LayoutNodeScope title, out LayoutNodeScope subTitle, out LayoutNodeScope btn);
809 using (title.Enter()) gui.DrawText("Play in");
810 using (subTitle.Enter()) gui.DrawText("per patch");
811 using (btn.Enter()) gui.DrawText("C");
812 }
813
814 {
815 DrawToolbarButtonTemplate(out LayoutNodeScope title, out LayoutNodeScope subTitle, out LayoutNodeScope btn);
816 using (title.Enter()) gui.DrawText("Hear in");
817 using (subTitle.Enter()) gui.DrawText("per patch");
818 using (btn.Enter()) gui.DrawText("D");
819 }
820
821 {
822 DrawToolbarButtonTemplate(out LayoutNodeScope title, out LayoutNodeScope subTitle, out LayoutNodeScope btn);
823 using (title.Enter()) gui.DrawText("Tempo");
824 using (btn.Enter()) gui.DrawText("122.00 BPM");
825 }
826
827 // Spacer
828 gui.Node().Expand();
829
830 // Menu button placeholder
831 gui.DrawIcon(Icons.MenuBurger, 30).MarginRight(10);
832 }
833}
834
835void DrawEffectButton(ref Effect effect, in Color instrumentColor)
836{
837 LayoutNode container = gui.Node().ExpandWidth().FlowDir(Axis.X);
838 LayoutNode toggleEffectNode = container.AppendNode(Size.Ratio(1), Size.Expand()).AlignContent(0.5f); // Expand height, and make width as wide as it is tall.
839 LayoutNode textNode = container.AppendNode().ExpandWidth()
840 .AlignContent(0, 0.5f)
841 .Gap(7)
842 .PaddingY(10)
843 .PaddingLeft(padding);
844
845 float onOffNodeHover = gui.AnimateBool01(toggleEffectNode.OnHover(), 0.2f, Easing.EaseInOutSine);
846 float textNodeHover = gui.AnimateBool01(textNode.OnHover(), 0.2f, Easing.EaseInOutSine);
847
848 textNode.ContentOffsetX(textNodeHover * 10);
849
850 Popup popup = GetPopup();
851
852 // We could also use an Icon. This is just to showcase the shapes API a little.
853 // Angle is a utility struct that returns a float in radians.
854 SdShape onIconShape = SdShape.Arc(13, 2, Angle.Turns(-0.1f), Angle.Turns(0.7f))
855 + SdShape.RectangleRounded(4, 16, 2).MoveY(-10);
856
857 float onBrightness = gui.AnimateBool01(effect.IsOn, 0.2f, Easing.EaseInOutSine);
858
859 Color col1 = Color.Lerp(0x00000077, instrumentColor, onBrightness);
860 Color col2 = Color.Lerp(0x00000044, instrumentColor, onBrightness);
861
862 // bg
863 gui.DrawSdRect(container.Rect, 10)
864 .LinearGradientColor(0xffffff44, 0xffffff11, Angle.Turns(-0.15f))
865 .SolidColor(new Color(instrumentColor, textNodeHover * 0.1f))
866 .OuterShadow(0x00000055, new Vector2(0, 1), 5);
867
868 // icon bg
869 gui.DrawSdRect(toggleEffectNode.Rect, 10, 0, 0, 10)
870 .LinearGradientColor(col1, col2)
871 .InnerShadow(0xffffff22, new Vector2(0, 0), 1, 0);
872
873 // icon
874 using (toggleEffectNode.Enter())
875 {
876 Color iconColor = effect.IsOn ? 0xffffffff : Color.Lerp(0xffffff33, 0xffffffff, onOffNodeHover);
877 gui.SetTextColor(iconColor);
878 gui.DrawIcon(Icons.PowerOff, 35);
879 }
880
881 using (textNode.Enter())
882 {
883 // This will clip the contents of the node to the node's size, so overflow content is cut off.
884 gui.ClipContent();
885 gui.DrawText(effect.Type, 0xffffff55, 16);
886 gui.DrawText(effect.Name, 0xffffff88, 20);
887 }
888
889 if (popup.IsVisible())
890 {
891 DrawDefaultPopupTitleAndFooter(popup, effect.Name);
892
893 using (popup.BodyContainer.Enter())
894 {
895 DrawEffectSettings(popup, ref effect);
896 }
897 }
898
899 if (toggleEffectNode.OnClick())
900 {
901 effect.IsOn = !effect.IsOn;
902 }
903
904 if (textNode.OnClick())
905 {
906 popup.Show();
907 }
908}
909
910void StyleHeader()
911{
912 gui.CurrentNode
913 .Height(50)
914 .Spacing(Spacing.SpaceBetween)
915 .AlignContent(0, 0.5f)
916 .ExpandWidth()
917 .FlowDir(Axis.X)
918 .Padding(20, 0);
919
920 gui.SetTextColor(0xDCF2F2FF);
921
922 gui.DrawSdBackgroundRect()
923 .SolidColor(0xffffff11)
924 .OuterShadow(0x000000ff, 2);
925}
926
927void StyleBox()
928{
929 var bgShape = SdShape.RectangleRounded(gui.CurrentNode.Rect.Width, gui.CurrentNode.Rect.Height, borderRadius);
930
931 gui.DrawSdShape(gui.CurrentNode.Rect.Center, bgShape)
932 .OuterShadow(0x00000088, new Vector2(0, 0), 80, 20);
933
934 gui.DrawSdShape(gui.CurrentNode.Rect.Center, bgShape)
935 .RadialGradientColor(0xffffff08, 0xffffff05, 0, gui.CurrentNode.Rect.MaxDimension, offsetY: gui.CurrentNode.Rect.Height * 0.5f)
936 .InnerShadow(0xffffff22, 2);
937}
938
939void StyleButton(bool on, SdShapePos bgShape)
940{
941 ImRect rect = gui.CurrentNode
942 .Padding(15)
943 .MinWidth(Size.Ratio(1))
944 .AlignContent(0.5f)
945 .Rect;
946
947 InteractableElement e = gui.CurrentNode.GetInteractable();
948 float tOn = gui.AnimateBool01(on, 0.2f, Easing.EaseInOutSine);
949 float tHover = e.OnHover() ? 1 : 0;
950 float tDown = gui.AnimateBool01(e.OnHold(), 0.1f, Easing.EaseInOutSine);
951 float tUp = 1 - tDown;
952
953 gui.DrawSdShape(bgShape)
954 .OuterShadow((0xffffff22, tUp), new Vector2(0, -4), 6, 3)
955 .OuterShadow((0x000000ff, tUp), new Vector2(0, 2), 6)
956 .InnerShadow((0, 0, 0, tDown * 0.423f), new Vector2(0, 0), 30, -5)
957 .InnerShadow((0, 0, 0, tDown * 0.375f), 20)
958 .LinearGradientColor((0x00000088, tUp), 0x00000000, Angle.Turns(0.25f))
959 .RadialGradientColor((0xF4A73AFF, tOn * tUp), (0xEF753DFF, tOn), 0, rect.Width * 0.5f)
960 .InnerShadow((0xA73317FF, tOn * tUp), 4, 0)
961 .InnerShadow((1, 1, 1, 0.2f * tUp), new Vector2(0, +3), 2, -2)
962 .InnerShadow((1, 1, 1, 0.2f * tDown), new Vector2(0, -3), 2, -2)
963 .SolidColor((1, 1, 1, tHover * tUp * 0.05f))
964 .RadialGradientColor((0, 0, 0, tDown * 0.6f), 0x00000000, 0, rect.Width * 0.7f);
965
966 Color textColor = 0xFDF5F2FF;
967 textColor = Color.Lerp(textColor, 0x130302FF, tOn);
968 textColor = Color.Lerp(textColor, 0xffffff44, tDown * (1 - tOn));
969 gui.SetTextColor(textColor);
970}
971
972void StyleButton(bool on)
973{
974 var rect = gui.CurrentNode.Rect;
975 SdShapePos bgShape = SdShapePos.RectangleRounded(rect.Expand(-3), borderRadius);
976 StyleButton(on, bgShape);
977}
978
979void DrawVolumeBar(ImRect r, float t)
980{
981 Color green = 0x39BE69FF;
982 Color yellow = 0xF0A948FF;
983 Color red = 0xF03E3EFF;
984
985 // Adjust the method of drawing gradients to support vertical gradients
986 VertexPlane q = gui.DrawList.AddTriangulatedPlane(2, 4);
987 float left = r.Left;
988 float right = r.Right;
989 float bottom = r.Bottom;
990 float h = r.Height;
991
992 // Adjustments to make sure the colors blend correctly from bottom to top
993 q[0, 0].Set(new Vector2(left, bottom), Vector2.One, green);
994 q[1, 0].Set(new Vector2(right, bottom), Vector2.One, green);
995 q[0, 1].Set(new Vector2(left, bottom - h * Math.Min(t, 0.3f)), Vector2.One, green);
996 q[1, 1].Set(new Vector2(right, bottom - h * Math.Min(t, 0.3f)), Vector2.One, green);
997 q[0, 2].Set(new Vector2(left, bottom - h * Math.Min(t, 0.5f)), Vector2.One, yellow);
998 q[1, 2].Set(new Vector2(right, bottom - h * Math.Min(t, 0.5f)), Vector2.One, yellow);
999 q[0, 3].Set(new Vector2(left, bottom - h * t), Vector2.One, red);
1000 q[1, 3].Set(new Vector2(right, bottom - h * t), Vector2.One, red);
1001
1002 // Note that this is another example of using a lower-level API to easily do
1003 // things that we have not yet designed nicer, higher-level APIs for, such
1004 // as drawing a multi step gradient.
1005 //
1006 // Since nothing is hidden away, and everything is fully accessible, very few
1007 // things are impossible to do.
1008}
1009
1010LayoutNode DrawVolumeSlider(Color color, ref float volume, float min = 0, float max = 1)
1011{
1012 ref float animatedVolume = ref gui.GetFloat(volume);
1013 animatedVolume = ImMath.Lerp(animatedVolume, volume, gui.Time.DeltaTime * 10);
1014
1015 float tHeight = Math.Clamp((animatedVolume - min) / (max - min), 0, 1);
1016 LayoutNode node = gui.Node().ExpandWidth().ExpandHeight();
1017 SdShape bgShape = SdShape.RectangleRounded(node.Rect.Width, node.Rect.Height, node.Rect.Width * 0.4f);
1018 ImRect laneRect = node.Rect.Padding(0, 10).AlignCenterX(8);
1019 ImRect handleRect = node.Rect.SetHeight(35).SetY(laneRect.Y + laneRect.Height * (1 - tHeight) - 20);
1020 InteractableElement sliderInteractable = gui.GetInteractable(laneRect);
1021 InteractableElement handleInteractable = gui.GetInteractable(handleRect);
1022
1023 float tFocus = gui.AnimateBool01(handleInteractable.OnHover() || handleInteractable.OnHold(), duration: 0.2f, Easing.EaseInOutSine);
1024 color = Color.Lerp(color, Color.White, tFocus * 0.2f);
1025
1026 // Bg
1027 gui.DrawSdShape(node, bgShape)
1028 .LinearGradientColor(0x00000044, 0x00000044);
1029
1030 // Lane
1031 gui.DrawSdRect(laneRect.Expand(1), 4)
1032 .SolidColor(0x00000088)
1033 .OuterShadow(0xffffff22, new Vector2(0, 1), 2);
1034
1035 // Lane glow
1036 gui.DrawSdRect(laneRect.AlignBottom(laneRect.Height * tHeight), 4)
1037 .SolidColor(color)
1038 .InnerShadow(0x000000aa, new Vector2(0, 0), 8.2f, -2);
1039
1040 // Handle
1041 gui.DrawSdRect(handleRect, 3)
1042 .LinearGradientColor(0xB9B1AFFF, 0x484443FF, Angle.Turns(-0.25f))
1043 .OuterShadow(0x000000ff, new Vector2(0, 10), 30, 10)
1044 .InnerShadow(0xffffff22, new Vector2(0, 1), 1, 1);
1045
1046 // Handle whole
1047 gui.DrawSdRect(handleRect.AlignCenterY(8))
1048 .OuterShadow(0xffffff22, new Vector2(0, 2), 1)
1049 .LinearGradientColor(0xB9B1AFFF, 0x484443FF, Angle.Turns(+0.25f));
1050
1051 // Input
1052 if (handleInteractable.OnHold() || sliderInteractable.OnHold())
1053 {
1054 volume -= gui.Input.MouseDelta.Y / laneRect.Height * (max - min);
1055 volume = Math.Clamp(volume, min, max);
1056 animatedVolume = volume;
1057 }
1058
1059 if (sliderInteractable.OnPointerDown())
1060 {
1061 var deltaY = gui.Input.MousePosition.Y - node.Rect.TL.Y;
1062 volume = Math.Clamp(1 - deltaY / laneRect.Height, 0, 1);
1063 animatedVolume = volume;
1064 }
1065
1066 // Tooltip
1067 if (handleInteractable.On(Interactions.Hover | Interactions.Hold))
1068 {
1069 DrawTooltip(handleRect.LeftCenter, volume.ToString("0.00") + " dB");
1070 }
1071
1072 return node;
1073}
1074
1075
1076LayoutNode DrawToolbarButtonTemplate(out LayoutNodeScope titleText, out LayoutNodeScope subTitleText, out LayoutNodeScope buttonContents)
1077{
1078 using (gui.Node().FlowDir(Axis.X).Gap(gap).AlignContent(0.5f).Enter())
1079 {
1080 using (gui.Node().AlignContent(0, 0.5f).Gap(5).Enter())
1081 {
1082 titleText = gui.Node().ToScope();
1083
1084 using (subTitleText = gui.Node().Enter())
1085 {
1086 gui.SetTextSize(15);
1087 gui.SetTextColor(0xffffff44);
1088 }
1089 }
1090
1091 using (buttonContents = gui.Node().Enter())
1092 {
1093 StyleButton(false);
1094 }
1095
1096 return gui.CurrentNode;
1097 }
1098}
1099
1100void DrawKeyboard(ImRect piano, float[] keyPressure)
1101{
1102 int numOctaves = keyPressure.Length / 12;
1103 int numMajorKeys = numOctaves * 7;
1104 int numMinorKeys = numOctaves * 5;
1105 ImRect majorKeyRect = piano.AlignLeft(piano.Width / numMajorKeys);
1106
1107 float minorKeyWidth = majorKeyRect.Width * 0.6f;
1108 ImRect minorKeyRect = piano.AlignLeft(minorKeyWidth);
1109 minorKeyRect.X += majorKeyRect.Width * 0.5f + (majorKeyRect.Width - minorKeyWidth) * 0.5f;
1110 minorKeyRect.Height = majorKeyRect.Height * 0.55f;
1111
1112 bool isPressingPiano = gui.GetInteractable(piano).IgnorePropagation().OnHold(out var args);
1113
1114 // Draw major keys
1115 for (int i = 0; i < numMajorKeys; i++)
1116 {
1117 ref float pressure = ref keyPressure[i];
1118
1119 var darkColor = Color.Lerp(0x00000044, 0x00000088, pressure);
1120
1121 gui.DrawSdRect(majorKeyRect, 5)
1122 .SolidColor(0xffffffff)
1123 .InnerShadow(0x000000ff, 0.1f, 1)
1124 .LinearGradientColor(0x00000000, darkColor, Angle.Turns(-0.25f), 0.5f);
1125
1126 // Simulate key release
1127 pressure = ImMath.Lerp(pressure, 0, gui.Time.DeltaTime * 5);
1128 if (gui.GetInteractable(majorKeyRect).OnHover() && isPressingPiano)
1129 pressure = 1;
1130
1131 majorKeyRect.X += majorKeyRect.Width;
1132 }
1133
1134 // Draw minor keys
1135 for (int i = 0; i < numMinorKeys; i++)
1136 {
1137 ref float pressure = ref keyPressure[i + numMajorKeys];
1138
1139 gui.DrawSdRect(minorKeyRect, 0, 0, 3, 3)
1140 .SolidColor(Color.Lerp(0x2A2A31FF, 0x757984FF, pressure))
1141 .InnerShadow(0x00000088, 1f, 2)
1142 .InnerShadow(0xffffff44, new Vector2(2, 0), 1f);
1143
1144 // Simulate key release
1145 pressure = ImMath.Lerp(pressure, 0, gui.Time.DeltaTime * 5);
1146 if (gui.GetInteractable(minorKeyRect).OnHover() && isPressingPiano)
1147 pressure = 1;
1148
1149 minorKeyRect.X += majorKeyRect.Width;
1150 if (i % 5 == 1 || i % 5 == 4)
1151 minorKeyRect.X += majorKeyRect.Width;
1152 }
1153}

1using PanGui;
2using System.Numerics;
3
4static class Icons
5{
6 public static ImFont IconFont;
7
8 public const char MenuBurger = '\uf0c9';
9 public const char ChartSimpleHorizontal = '\ue474';
10 public const char FloppyDisk = '\uf0c7';
11 public const char LocationCrosshairSlash = '\uf603';
12 public const char LocationSlash = '\uf603';
13 public const char NavIcon = '\uf0c9';
14 public const char PianoKeyboard = '\uf8d5';
15 public const char PlusSquare = '\uf0fe';
16 public const char PowerOff = '\uf011';
17 public const char Save = '\uf0c7';
18 public const char SlidersVSquare = '\uf3f2';
19 public const char SquarePlus = '\uf0fe';
20 public const char SquareSlidersVertical = '\uf3f2';
21 public const char Waveform = '\uf8f1';
22
23 public static LayoutNode DrawIcon(this Gui gui, char icon, float size = 40)
24 {
25 return gui.DrawGlyph(IconFont, icon, size);
26 }
27}
28
29struct Popup
30{
31 public AnimationFloat Visibility;
32 public LayoutNodeScope HeaderContainer;
33 public LayoutNodeScope BodyContainer;
34 public LayoutNodeScope FooterContainer;
35
36 public void Show()
37 {
38 this.Visibility.AnimateTo(1, 0.3f, Easing.SmoothStep);
39 }
40
41 public void Hide()
42 {
43 this.Visibility.AnimateTo(0, 0.3f, Easing.SmoothStep);
44 }
45
46 public bool IsVisible()
47 {
48 return this.Visibility > 0.001f;
49 }
50}
51
52// The user-code for this popup function looks like this:
53//
54// Popup popup = GetPopup();
55//
56// if (gui.Button("Show Popup"))
57// {
58// popup.Show();
59// }
60//
61// if (popup.IsVisible())
62// {
63// using (popup.BodyContainer.Enter())
64// {
65// gui.DrawText("Hello, world!");
66// }
67// }
68Popup GetPopup()
69{
70 var popup = new Popup()
71 {
72 Visibility = gui.GetAnimationFloat()
73 };
74
75 float tVisibility01 = popup.Visibility.GetValue();
76
77 // Calculate popup and background scaling for animation effects
78 Vector2 scalePopupIn = Vector2.Lerp(Vector2.One, new Vector2(0.9f, 0.9f), 1 - tVisibility01);
79 Vector2 scaleBgOut = Vector2.Lerp(Vector2.One, new Vector2(0.95f, 0.95f), tVisibility01);
80
81 if (popup.IsVisible())
82 {
83 // If you're familiar with HTMl / CSS you can think of this layout node as being position:fixed;
84 // We will make that API nicer in the near future! It has all the bells and whistles to be far more capable than HTML/CSS, it's just about making the API nicer.
85 using (gui.Node()
86 .Expand()
87 .SetParent(gui.Systems.LayoutSystem.RootNode)
88 .PositionRelativeTo(gui.Systems.LayoutSystem.RootNode, 0, 0)
89 .AlignContent(0.5f, 0.3f).Enter())
90 {
91 // Resetting the state to 0, is sort of like a "reset" for the GUI state. In a way it jumps us back to the main loop of the GUI.
92 gui.SetState(0);
93
94 // Data scopes are explained further down on the website. Go check it out!
95 gui.SetDataScope("popup");
96
97 // This makes the popup appear on top of everything else.
98 gui.SetZIndex(100);
99
100 // Draw semi-transparent background for popup
101 gui.DrawRect(gui.ScreenRect, new Color(0, 0, 0, 0.8f * tVisibility01));
102
103 // Allow popup to be closed by clicking outside its area
104 if (gui.CurrentNode.OnClick())
105 popup.Hide();
106
107 // The default size for a layout node is "Fit", so the inner popup container will be the size of its content
108 using (gui.Node().Enter())
109 {
110 gui.CurrentNode.PreventAllInputPropagation();
111
112 // Animate the popup.
113 gui.SetOpacity(tVisibility01);
114
115 gui.SetTransform(Matrix3x2.CreateScale(scalePopupIn, gui.ScreenRect.Center));
116 gui.DrawSdBackgroundRect(radius: borderRadius)
117 .RadialGradientColor(0x292423FF, 0x322D29FF, 0, gui.CurrentNode.Rect.MaxDimension)
118 .InnerShadow(0xffffff11, new Vector2(0, 40), 80, -40)
119 .OuterShadow(0x000000ff, 20)
120 .Stroke(0x000000ff, 1, 0);
121
122 using (gui.Node()
123 .Margin(gap)
124 .FlowDir(Axis.X)
125 .ExpandWidth()
126 .Gap(gap)
127 .MinWidth(Size.Fit)
128 .Margin(20)
129 .AlignContent(0.5f).Enter())
130 {
131 gui.SetTextSize(20);
132 gui.SetTextFont(arial_bold);
133 gui.SetTextColor(0xFFFFFFcc);
134
135 // Assigns the header container
136 popup.HeaderContainer = gui.CurrentNode.ToScope();
137 }
138
139 // Draw separator line
140 gui.DrawRect(gui.Node(Size.Expand(), 1), new Color(1, 1, 1, 0.1f));
141 gui.SetTextColor(0xffffff88);
142
143 // Assigns the body container
144 popup.BodyContainer = gui.Node()
145 .Margin(gap)
146 .ToScope();
147
148 // Draw another separator line
149 gui.DrawRect(gui.Node(Size.Expand(), 1), new Color(1, 1, 1, 0.1f));
150
151 // Assigns the footer container
152 popup.FooterContainer = gui.Node()
153 .Margin(gap)
154 .FlowDir(Axis.X)
155 .ExpandWidth()
156 .ContentAlignX(1)
157 .Gap(gap)
158 .ToScope();
159 }
160 }
161 }
162
163 // This API is particularly likely to become something different, or be completely removed.
164 // It's meant to solve the problem of getting computed values that are not yet known during
165 // this pass, but were known or calculated during the previous pass.
166 //
167 // For now, this is kind of a temporary hack; don't worry about it, we will have a much better
168 // solution for this problem before PanGui launches.
169 ref float maxPopupVisibility = ref gui.GetValue<float>("maxPopupVisibility", Pass.CurrentPass, 0f);
170 maxPopupVisibility = Math.Max(popup.Visibility, maxPopupVisibility);
171
172 return popup;
173}
174
175float GetPopupVisibility()
176{
177 // This API is particularly likely to become something different, or be completely removed.
178 // It's meant to solve the problem of getting computed values that are not yet known during
179 // this pass, but were known or calculated during the previous pass.
180 //
181 // For now, this is kind of a temporary hack; don't worry about it, we will have a much better
182 // solution for this problem before PanGui launches.
183 return gui.GetValue<float>("maxPopupVisibility", Pass.PreviousPass, 0f);
184}
185
186LayoutNode DrawTooltip(Vector2 pivot, string text)
187{
188 using (gui.EnterDataScope("tooltip"))
189 {
190 // We can position the tooltip layout node
191 // precisely, and then offset its content based
192 // on its own size, so it always floats to the
193 // left of the pivot point and is vertically
194 // centered.
195 var tooltipPositioner = gui.Node()
196 .PositionRelativeToRoot(pivot)
197 .ContentOffsetX(Offset.Percentage(-1))
198 .ContentOffsetY(Offset.Percentage(-0.5f));
199
200 using (tooltipPositioner.AppendNode().AlignContent(0.5f).Padding(10, 15).MarginRight(20).Enter())
201 {
202 var rect = gui.CurrentNode.Rect;
203 var tooltipBgShape = SdShape.RectangleRounded(gui.CurrentNode.Rect.Width, rect.Height, 5)
204 + SdShape.EquilateralTriangle(20, Angle.Turns(0.25f))
205 .Move(rect.Width * 0.5f, 0);
206
207 gui.SetTextSize(16);
208 gui.SetTextColor(0xffffffcc);
209 gui.SetZIndex(10);
210
211 gui.DrawSdShape(gui.CurrentNode, tooltipBgShape)
212 .SolidColor(0x00000099)
213 .OuterShadow(0x00000055, new Vector2(5, 8), 5)
214 .InnerShadow(0xffffff22, new Vector2(0, 1), 1.2f);
215
216 gui.DrawText(text);
217 }
218
219 return tooltipPositioner;
220 }
221}
222
223
224void DrawDefaultPopupTitleAndFooter(Popup popup, string title)
225{
226 using (popup.HeaderContainer.Enter())
227 {
228 gui.DrawText(title);
229 }
230
231 using (popup.FooterContainer.Enter())
232 {
233 using (gui.Node().Enter())
234 {
235 StyleButton(false);
236 gui.DrawText("Cancel");
237
238 if (gui.CurrentNode.OnClick())
239 popup.Hide();
240 }
241
242 using (gui.Node().Enter())
243 {
244 StyleButton(false);
245 gui.DrawText("Confirm");
246
247 if (gui.CurrentNode.OnClick())
248 popup.Hide();
249 }
250 }
251}

1using PanGui;
2using System.Numerics;
3
4string[] trackLibrary = ["Into the Wild", "Midnight Serenade", "Echoes of Eternity", "Dreamscape", "Whispering Shadows", "Enchanted Journey", "Mystic Melodies", "Celestial Symphony", "Lost in Time", "Harmony of the Spheres", "Serenity Falls", "Eternal Bliss", "Twilight Reverie", "Melancholy Memories", "Dancing Fireflies", "Starry Night", "Whispers in the Wind", "Moonlit Sonata", "Sunset Serenade", "Ocean's Embrace"];
5string selectedTrack = "Lost in Time";
6float modWheel = 0.5f;
7int numOctaves = 8;
8float[] keyPressures = new float[12 * numOctaves];
9PadPlayer padPlayer = new PadPlayer();
10Snapshot[] snapshots = GenerateSnapshots();
11Snapshot selectedSnapshot = snapshots[0];
12
13(float left, float right) GetSimulatedOutputVolume()
14{
15 float maxPianoKeyPressure = gui.Smoothdamp(keyPressures.Max());
16 float mono = MathF.Cos(gui.Time.Elapsed * 4.5f);
17 float tLeft = mono + MathF.Cos(gui.Time.Elapsed * 7.5f) * 0.5f;
18 float tRight = mono + MathF.Cos(gui.Time.Elapsed * 8.5f) * 0.5f;
19 float left = maxPianoKeyPressure * (0.8f + tLeft * 0.1f);
20 float right = maxPianoKeyPressure * (0.8f + tRight * 0.1f);
21 return (left, right);
22}
23
24Snapshot[] GenerateSnapshots()
25{
26 var rnd = new Random(200);
27
28 return [
29 new Snapshot { Name = "Snapshot 1", Instruments = GenerateInstruments() },
30 new Snapshot { Name = "Snapshot 2", Instruments = GenerateInstruments() },
31 new Snapshot { Name = "Snapshot 3", Instruments = GenerateInstruments() },
32 new Snapshot { Name = "Snapshot 4", Instruments = GenerateInstruments() },
33 new Snapshot { Name = "Snapshot 5", Instruments = GenerateInstruments() },
34 new Snapshot { Name = "Snapshot 6", Instruments = GenerateInstruments() },
35 ];
36
37 Instrument[] GenerateInstruments() => [
38 new Instrument { Name = "Piano", Color = 0xF0A948FF, Volume = (float)rnd.NextDouble(), Effects = GenerateEffects(), KeyStart = rnd.Next(0, 20), KeyLength = rnd.Next(50, 60), adsr = new ADSR(), Pan = 0.5f },
39 new Instrument { Name = "Bass", Color = 0x3BBE69FF, Volume = (float)rnd.NextDouble(), Effects = GenerateEffects(), KeyStart = rnd.Next(0, 20), KeyLength = rnd.Next(50, 60), adsr = new ADSR(), Pan = 0.3f },
40 new Instrument { Name = "Drums", Color = 0x3961E6FF, Volume = (float)rnd.NextDouble(), Effects = GenerateEffects(), KeyStart = rnd.Next(0, 20), KeyLength = rnd.Next(50, 60), adsr = new ADSR(), Pan = 0.6f },
41 new Instrument { Name = "Guitar", Color = 0x8F59E2FF, Volume = (float)rnd.NextDouble(), Effects = GenerateEffects(), KeyStart = rnd.Next(0, 20), KeyLength = rnd.Next(50, 60), adsr = new ADSR(), Pan = 0.5f },
42 new Instrument { Name = "Synth", Color = 0xE673C4FF, Volume = (float)rnd.NextDouble(), Effects = GenerateEffects(), KeyStart = rnd.Next(0, 20), KeyLength = rnd.Next(50, 60), adsr = new ADSR(), Pan = 0.5f },
43 ];
44
45 Effect[] GenerateEffects() => [
46 new Effect { Name = "Reverb", Type = "Midi", IsOn = rnd.NextDouble() > 0.1, KnobValues = [(float)rnd.NextDouble(), (float)rnd.NextDouble(), (float)rnd.NextDouble(), (float)rnd.NextDouble(), (float)rnd.NextDouble(), (float)rnd.NextDouble()] },
47 new Effect { Name = "Delay", Type = "Midi", IsOn = rnd.NextDouble() > 0.1, KnobValues = [(float)rnd.NextDouble(), (float)rnd.NextDouble(), (float)rnd.NextDouble(), (float)rnd.NextDouble(), (float)rnd.NextDouble(), (float)rnd.NextDouble()] },
48 new Effect { Name = "Chorus", Type = "Midi", IsOn = rnd.NextDouble() > 0.1, KnobValues = [(float)rnd.NextDouble(), (float)rnd.NextDouble(), (float)rnd.NextDouble(), (float)rnd.NextDouble(), (float)rnd.NextDouble(), (float)rnd.NextDouble()] },
49 new Effect { Name = "EQ", Type = "Midi", IsOn = rnd.NextDouble() > 0.1, KnobValues = [(float)rnd.NextDouble(), (float)rnd.NextDouble(), (float)rnd.NextDouble(), (float)rnd.NextDouble(), (float)rnd.NextDouble(), (float)rnd.NextDouble()] },
50 new Effect { Name = "Compressor", Type = "Midi", IsOn = rnd.NextDouble() > 0.1, KnobValues = [(float)rnd.NextDouble(), (float)rnd.NextDouble(), (float)rnd.NextDouble(), (float)rnd.NextDouble(), (float)rnd.NextDouble(), (float)rnd.NextDouble()] }
51 ];
52}
53
54
55class Snapshot
56{
57 public string Name;
58 public Instrument[] Instruments;
59}
60
61struct Instrument
62{
63 public string Name;
64 public Color Color;
65 public float Volume;
66 public float Pan;
67 public int KeyStart;
68 public int KeyLength;
69 public Effect[] Effects;
70 public ADSR adsr;
71}
72
73struct Effect
74{
75 public bool IsOn;
76 public string Name;
77 public string Type;
78 public float[] KnobValues;
79 public float Dry, Wet;
80}
81
82class PadPlayer
83{
84 public string[] Pads = ["A", "Bb", "B", "C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab"];
85 public int SelectedPad;
86 public float Volume;
87 public float Brightness;
88 public float Shimmer;
89 public Effect Effect = new Effect { Name = "Reverb", Type = "Midi" };
90}
91
92class ADSR
93{
94 public float Attack = 0.3f;
95 public float Decay = 0.3f;
96 public float Sustain = 0.8f;
97 public float Release = 0.3f;
98}

AirBnB slider demo



Next up, let's look at how straight-forward it is to create even very complicated UI widgets. This is a recreation of AirBnb's circular month slider as of February 2024, which you can (or could) find here (click on 'When', then select 'Months'). If it's not there any more when you're reading this, well, it looked like the demo here.

We'll be honest, we had a pretty hard time sorting out the precise mechanics of how AirBnb built that slider using HTML, CSS and JavaScript, as it is extremely complicated. We gave up after finding many dozens (if not hundreds) of layered and interweaving HTML elements and SVG paths seemingly managed by enormous amounts of JavaScript.

In the previous demo there are plenty of small implementation details that show how many things that are complicated in HTML and CSS are very simple in PanGui. But this example, we think, really brings it home. Making this in HTML and CSS would be a nightmare - and clearly was a nightmare for some poor developers working for AirBnb.

The fact that they managed to create this at all is extremely impressive! But despite their best efforts, it still has several bugs. For example, lots of jittering artifacts and the fact that it completely breaks when a scroll view appears in its containing box.

We're perfectly aware that this is an unfair comparison. HTML was not designed to be good at this sort of thing. It, like most other UI systems, is a very structured and abstracted way of creating a UI, forcing the developer to be far away from what is drawn on the screen. However, that is the point - HTML and CSS are just not good enough, and have forced millions of UI designers and developers into the box of only doing the limited sorts of things that are easily achievable in their working environment.

Note that the PanGui code for this will probably change a little bit once the input controls feature is introduced, as you would likely want to be able to interact with the widget using more than just a pointer device (for example, with keyboard navigation, etc).


1void DrawAirBnBMonthSlider(ref int month)
2{
3 ref float t = ref gui.GetFloat(month / 12.0f);
4
5 float innerRadius = 90;
6 float outerRadius = 150;
7
8 using (gui.Node(500, 500).AlignContent(0.5f).Gap(10).Enter())
9 {
10 gui.DrawText(month.ToString(), 100f);
11 gui.DrawText(month == 1 ? "month" : "months", 20f);
12
13 float halfThickness = (outerRadius - innerRadius) * 0.5f;
14 Vector2 handlePos = gui.CurrentNode.Rect.Center + Angle.Turns(t - 0.25f).GetDirectionVector() * (innerRadius + halfThickness);
15 Vector2 center = gui.CurrentNode.Rect.Center;
16
17 SdShape arcLaneShape = SdShape.Circle(outerRadius) - SdShape.Circle(innerRadius);
18 SdShape arcShape = SdShape.Arc(innerRadius + halfThickness, halfThickness, Angle.Turns(-0.25f), Angle.Turns(t)).Expand(-3);
19 SdShape handleShape = SdShape.Circle(halfThickness - 10);
20
21 InteractableElement handleElement = gui.GetInteractable(handlePos, handleShape);
22
23 if (handleElement.OnHold())
24 {
25 Vector2 delta = center - gui.Input.MousePosition;
26 t = 1 - 0.5f + MathF.Atan2(delta.X, -delta.Y) / MathF.Tau;
27 t = Math.Clamp(t, 1 / 12f, 1);
28 month = (int)Math.Round(t * 12);
29 }
30 else
31 {
32 t = ImMath.Lerp(t, month / 12.0f, gui.Time.DeltaTime * 10);
33 }
34
35 gui.DrawSdShape(center, arcLaneShape)
36 .LinearGradientColor(0x00000022, 0x00000005, scale: 0.8f)
37 .InnerShadow(0x00000066, new Vector2(0, 20), 50, -30)
38 .OuterShadow(0x00000066, new Vector2(0, -5), 10, 5);
39
40 for (int i = 0; i < 12; i++)
41 gui.DrawCircle(center + Angle.Turns(i / 12.0f).GetDirectionVector() * (innerRadius + halfThickness), 2, 0x00000099);
42
43 gui.SetClipArea(gui.CurrentNode, arcLaneShape); // Clips the shadow to the lane.
44
45 gui.DrawSdShape(center, arcShape)
46 .RadialGradientColor(0xBA0057FF, 0xF91E50FF, innerRadius, outerRadius)
47 .RadialGradientColor(0xDC4682FF, 0xCF2D6C00, innerRadius - halfThickness, innerRadius + halfThickness, offsetY: halfThickness)
48 .InnerShadow(0xFA144BFF, new Vector2(0, 5), 25, -8)
49 .OuterShadow(0xEA1C5Acc, 90)
50 .OuterShadow(0x000000822, new Vector2(0, 3), 10, 3)
51 .OuterShadow(0x000000811, 5, 0)
52 .OuterShadow(0x22222244, 2);
53
54 gui.DrawSdShape(handlePos, handleShape.Expand(handleElement.On(Interactions.Hover | Interactions.Hold) ? 4 : 0))
55 .LinearGradientColor(0xD4D1D5FF, 0xFFFCFFFF)
56 .InnerShadow(0xffffffff, 1, 2)
57 .OuterShadow(0x00000066, 4);
58 }
59}

Overview of (some) current features

Layouting

We think we've managed to create one of the most powerful, flexible and expressive layout systems around,
and certainly by far the most capable IMGUI layouting system we've ever heard of.

Use layouting only where and how you want

PanGui's layouting system is a tool for you to use as and where you please, not a box you are forced to fit into. Layouting is not coupled to any other features. In the end, all the layouting system really does is give you rects to use for other things. As such, it is trivial to mix and match layouting with "manual" calculations, using layouting only for the parts of the interface where it actually helps you. Or if you want, you can even forgo it entirely, without losing access to any of PanGui's other features.

Thousands of nodes in fractions of a millisecond

PanGui's layouting is blazingly fast, resolving thousands of layout nodes in fractions of a millisecond. And we haven't even really optimized it yet! We've merely non-pessimized it.

We've tried to run comparisons against for example Yoga, a popular C++ layouting library used by, for example, React Native and the Unity game engine. In very simple cases, PanGui typically outperformed Yoga by a factor of 10-30, and in more complicated situations, PanGui outperformed it by a factor of 100-5000. And note that this is the C# version of PanGui compared to a C++ library.

We realize these are loose numbers and not remotely a stringent benchmark, but they give some sense of the final expected performance. Once PanGui nears completion, we will provide more concrete performance comparisons with popular alternatives.

Retained-like mutability

Work with the immediate-mode layouting data with retained-like patterns

One convenient advantage of many retained mode systems is that you have a hierarchical data model of the UI that can be manipulated. One part of the code can affect parts of the interface created by another part of the code, adding to it, styling it, removing it, and so on.

PanGui's layouting boasts a similar feature, despite being purely immediate mode. Within a given frame, layout nodes can be referenced and passed around, and are fully mutable. They can be modified, moved, added or removed at will, enabling the sorts of modular usage patterns that you'd typically only find in retained mode systems.

Powerful size primitives

PanGui provides a simple set of size primitives that can be specified for width and height and - this is key - can also be used as min and max constraints. This makes it easy to specify layouts that are very challenging in, for example, HTML and CSS.


1Size.FitContent(percentageOfContent) // 1 by default
2Size.Expand(weight) // 1 by default
3Size.Ratio(ratio) // Other axis multiplied by ratio
4Size.Percentage(percentage) // 1 is 100%
5Size.Pixels(pixels)
610f // In C# floats and ints implicitly convert to Size.

Position and size anything relative to anything else

In PanGui, you're not limited to a fixed set of options like relative, absolute or fixed. You can position anything relative to anything else...


1gui.Node()
2 .PositionRelativeTo(otherNode, x: 0.5f, y: Offset.PercentageOfSelf(0.5f))
3 .SizeRelativeTo(otherNode);

...or even completely move a node to another node.


1gui.Node().SetNewParent(otherNode);

Advanced layout blending

Layout nodes can exist in a blended state between many different possible configurations. This makes transitions between different computed or derived values very easy to create, and is very handy for animations. Certain concepts, such as animating from fit content to a constrained expand, are extremely difficult or even impossible to express in, for example, HTML and CSS.


1gui.Node().Width(
2 Size.Lerp(Size.FitContent(), Size.Expand(), t)
3);

Alignment

In PanGui, alignment is not based on left, center, right, etc., but rather is just a float where 0 is "left", 1 is "right" and 0.5 is "center". You can even align < 0 and > 1.


1float align = cos(time) * 0.5f + 0.5f;
2gui.Node().AlignContent(x: align, y: align);

It's "just data"

The LayoutNode struct simply contains a pointer to the layout data, which you can access and modify at will, but we've made the most common layouting data easily accessible.


1LayoutNode node = gui.Node();
2DrawRect(node.Rect, col); // With padding
3DrawRect(node.OuterRect, col); // With padding and margin
4DrawRect(node.InnerRect, col); // Without padding and margin
5DrawRect(node.ContentRect, col); // Bounds of the content
6DrawRect(node.Parent.Rect, col); // Parent rect

You can easily traverse the entire layout tree:


1int i = 0;
2LayoutNode someNode = gui.CurrentNode;
3foreach (LayoutNode node in someNode.ChildNodes)
4{
5 Color color = i % 2 == 0 ? Color.Red : Color.Green;
6 gui.DrawRect(node.Rect, color);
7}

All of the layout specification methods such as .MaxWidth(), .AlignContent(), .Gap(), etc., are in fact just extremely small, inlinable functions updating a tiny amount of data. For example, setting the max width of a layout node to 100 pixels is just:


1// Specify that we're using pixels
2node.Properties.MaxWidth.Type = SizeMode.Pixels;
3// Give the value in pixels
4node.Properties.MaxWidth.Value = 100;
5// Register that the node now has a max value
6node.Properties.PropertyFlags |= PropertyFlags.MaxWidth;

Flexible

Pick the method of using nodes that best fits your needs. You can assign nodes to variables, use the fluent API, or use the using statement.


1var node = gui.Node();
2node.Position(100, 100);
3node.Size(200, 200);
4SomeOtherFunction(node);

You can enter and exit nodes with the using statement.


1using (gui.Node().Position(100, 100).Size(200, 200).Enter())
2{
3 // Do stuff
4}

This is essentially equivalent to:


1LayoutNodeScope scope = gui.Node().Position(100, 100).Size(200, 200).Enter();
2// Do stuff
3scope.Exit();

Useful shortcuts for working with other PanGui systems

The layout system itself is very isolated from the rest of the codebase, but because of how frequently it is used, we've made some shortcuts to make it easier to work with other PanGui systems through the layouting nodes. Note that none of the following features are part of the layouting system itself, but are rather just convenient shortcuts.

Entering nodes helps you manage state


1gui.SetZIndex(1);
2gui.SetFontSize(13);
3
4using (gui.Node().Enter())
5{
6 gui.SetZIndex(30);
7 gui.SetFontSize(40);
8}
9
10// Font size is back to 13, and z-index is back to 1.

Shortcuts to the interactable system


1LayoutNode node = gui.CurrentNode;
2
3if (node.OnHold(out HoldArgs args))
4 node.SelfOffset(args.DeltaPosition);
5
6if (node.OnHover())
7 DrawRect(node.Rect, Color.Red);
8
9// Same as:
10if (gui.GetInteractable(node.Rect).OnHover())
11 DrawRect(node.Rect, Color.Red);

And so much more

Covering all the features of PanGui's layouting system would take a lot of space, but here are a few we look forward to showing you in the future:

Tables
Wrapping of flowing elements
Pivoting
Margins and Paddings
Self Offset
Content Offset
Flow direction

Shapes

User interfaces are made out of various shapes, so easily being able to define, draw and use complex shapes is important.

Signed distance field based shapes

PanGui has a capable Shapes API based on composing signed distance fields. It is very fast, it has a tiny memory footprint per shape, and, like vectors, it is analytical, meaning it scales perfectly to any resolution.

Define a shape

Note that a shape is not tied to a specific position - it's just a shape. It can be drawn in any position, used and reused as many times as you like.


1Vector2 position = gui.ScreenRect.Center;
2SdShape shape = SdShape.Circle(300);

Use shapes as clipping masks

This will affect everything, including text. This is, for example, how you would make sure content in a rounded rectangle doesn't visually overflow:


1gui.SetClipShape(position, shape);

Shapes can also be used as input elements

Interactable elements are most often defined by a simple rectangle, but you can also define them as a shape, and then use the shape as the interactable area.


1var interactable = gui.GetInteractable(position, shape);
2bool isHovering = interactable.OnHover();
3bool isDragging = interactable.OnHold(out HoldArgs args);

Drawn with as many effects as you like

Shapes can be drawn with various effects, such as gradients, textures and shadows. All effects are easily animatable, and gradients look good and don't have the banding artifacts that are common in other systems.

Also note that all the effects will actually be drawn on top of each other in the given order, all in a single draw call. In fact, all shapes in a given z-index are included in the same single draw call that draws the rest of the geometry.


1gui.DrawShape(position, shape)
2 .SolidColor(Color.HSVLerp(a, b, tHover))
3 .VariousTypesOfGradients(...)
4 .BackgroundTextures(...)
5 .OuterShadow(...)
6 .OuterShadow(...)
7 .InnerShadow(...)
8 .AndSoOn(...);

Pros and cons, SVGs and future plans

Signed distance shapes have a lot of upsides, but there are also some downsides to this approach:

First, GPU rendering performance scales poorly with complexity. It is as fast as rendering a texture for relatively simple shapes, but combining many shapes together can quickly become a performance problem when you're also stacking multiple effects on it, as effects such as shadows require the entire SDF shape to be evaluated once per shadow per fragment, effectively multiplying the render cost of the shape.

It also scales poorly with very thin shapes whose bounding boxes cover a large area, since SDF drawing is bounding box based. Such shapes that cover a very small proportion of their total bounding box will have a lot of potentially expensive overdraw, as the cost of evaluating every pixel in the bounding box is the same whether it is in the shape or not. This is why we also intend to introduce a vector-based graphics API for loading and generating such shapes easily. This API would of course include .svg support.

Another con is that using the signed distance field as a way of rendering shadows often leads to undesirable sharp edges in the shadows for certain shapes or combinations of shapes, due to the way the math works out. It is possible to improve the shadows, but only by sampling the SDF function to a cost-prohibitive degree. This is something we may be able to improve in the future, though; we have some ideas...

Graphics

In the end, all PanGui really does is produce a list of graphics commands. So, of course, it should be good at it. PanGui's drawing is very fast and efficient, without sacrificing ease of use, expressive power or user control.

Graphics Drawing and Manipulation

Often, in UI libraries, it is very hard and tedious to just draw a simple rectangle to the screen. We've done our best to make sure PanGui never gets in your way when you just want to put pixels on the screen:


1// Specify how graphics should be drawn. 
2// (Note that our state system makes it so you don't have
3// to undo all of your state changes manually.)
4gui.SetTransform(matrix);
5gui.SetBlendMode(BlendMode.Additive);
6gui.SetBlendColor(color);
7gui.SetTexture(texture);
8gui.SetZIndex(30);
9
10// Draw a rect the easy way
11gui.DrawRect(new Rect(0, 0, 100, 100), Color.Red);
12
13// Or simply draw a quad
14ImQuad quad = gui.DrawList.AddTriangulatedQuad();
15quad.V1.Position = new Vector2(0, 0);
16quad.V2.Position = new Vector2(100, 0);
17quad.SetRect(rect);
18
19// Or manually mutate the vertex and index buffers.
20gui.DrawList.Indices;
21gui.DrawList.Vertices;
22

Integrating it into your own pipeline

PanGui's only job is to turn input into a set of optimized GPU buffers and simple rendering commands, to be injected anywhere in any arbitrary rendering pipeline. As such, PanGui integrates easily into any potential environment.

The graphics commands themselves are very simple and efficient, the vertex structure is straight-forward, and all you really need to implement the rendering spec is one shader file which, as of the time of writing, is less than 1000 lines of code, making PanGui very easy to introduce into any potential rendering pipeline.


1using System;
2using PanGui;
3
4Gui gui = new Gui();
5Rect rect = new Rect(0, 0, 500, 500);
6Action guiFunction = (gui) =>
7{
8 gui.DrawRect(gui.ScreenRect, 0xFF0000FF);
9};
10
11CommandList commands = gui.ProcessFrame(rect, guiFunction);
12
13foreach (Command command in commands)
14{
15 // Issue command to rendering pipeline
16}

Input

PanGui's input handling is written to make it easy to handle input exactly in the way most useful to solve a given problem, providing the user with an array of options at different levels of abstraction: raw input handling, interactables, and controls.

Raw input handling

Raw input handling is just that: it's asking direct questions about the current input state: where are the cursors, which keys are currently down, what is the axis value of a joystick, and so on.


1// Do something if the user presses space
2if (gui.Input.OnKeyDown(Key.Space))
3{
4 DoSomething();
5}

Interactables

Interactables are elements that can be interacted with: they can have shapes, and provide a more comprehensive and easy way of handling common device-agnostic input operations, such as clicking things, dragging things, respecting z-indices and draw ordering (IE, being capable of blocking each other's input), and so on.


1// Make a clickable circle in the center of the screen
2Interactable e = gui.GetInteractable(gui.ScreenRect.Center, SdShape.Circle(100));
3
4if (e.OnClick())
5{
6 DoSomething();
7}

Controls

Controls are fully fledged "input elements": they can have focus, support very stateful interactions (such as being a text field with one or more inner cursors and selections), they can provide contextual/spatial navigation with keyboard or joysticks, and so on. Controls are an in progress feature, so we will not go into them in detail yet.

Straight-forward event propagation

Since PanGui fully supports z-indices, all input elements (whether interactables or controls) have a clear order of priority. Additionally, event propagation can be controlled very precisely: each input element can decide which events to "eat" or pass on, and elements can even decide whether to respect propagation or not in order to receive events regardless of their propagation state.

Controllable, testable and low-latency input

PanGui does not read input by itself, but is fed input events from the platform integration. This approach also makes it extremely easy to simulate input for testing or other purposes. Great effort has been invested in ensuring that input latency (the time between a user pressing a key and pixels changing on the screen) is as close to instant as possible on all platforms.

Straight-forward state management

The IMGUI pattern overall greatly reduces the amount of UI-related state you need to manage, as the UI code is typically directly tied to the data it represents. PanGui even further simplifies managing the little amount of state that is left - things like the current color, transform matrix, blend mode, text font, etc.

State snapshots

This is achieved with a highly-optimized, low-overhead, delta-based state snapshot system. At any time, a state snapshot can be requested, and then later used to restore the state back to what it was at the time of the snapshot.


1gui.SetZIndex(1);
2gui.SetFontSize(13);
3int state = gui.State.GetState(); // Get state snapshot
4
5gui.SetZIndex(30);
6gui.SetFontSize(40);
7// UI code here draws with a large font and goes on top
8
9gui.State.SetState(state); // Restore state snapshot

Working with layout nodes

Taking a snapshot is very cheap and fast: so cheap and fast that when you enter a layout node, it always takes a state snapshot, such that it can restore the prior state when it later exits.


1gui.SetZIndex(1);
2gui.SetFontSize(13);
3
4using (gui.Node().Enter())
5{
6 gui.SetZIndex(30);
7 gui.SetFontSize(40);
8
9 // UI code here draws with a large font and goes on top
10}
11
12// Font size is back to 13, and z-index is back to 1.
13// This works at any level of scope nesting.

"Jump" to anywhere in the layout from anywhere else

When a layout node is exited, it takes a second state snapshot, such that when you re-enter a layout node, the state will be reset to what it was when you left it.

One of the super awesome features this gives us, as a sort of side-effect, is the ability to go back and revisit, modify and add to previously created UI elements. One such example is the popup in the audio app demo: the popup layout node is created, then passed back out of the method to be filled out by the code that called the GetPopup function.

There is no need to resort to solutions such as splitting functions up into tedious begin and end calls with complicated ways of passing state between them, and no need to clutter your code with push and pop calls. (Though, of course, if you want to, you still can.)

Popup example

1LayoutNodeScope GetPopup() 
2{
3 LayoutNodeScope popupContainer;
4
5 // Potential UI code for popup graphics.
6
7 using (gui.Node().Padding(30).Enter())
8 {
9 gui.SetTransform(matrix);
10 gui.SetFont(font);
11 gui.SetFontSize(16);
12 gui.SetZIndex(1000);
13
14 // Create a container for the calling code
15 // to fill out with whatever it wants.
16 popupContainer = gui.Node().ExpandWidth().ToScope();
17 }
18
19 // More potential UI code for popup graphics.
20
21 return popupContainer;
22}
Use case, somewhere else:

1LayoutNodeScope popup = GetPopup();
2
3using (popup.Enter())
4{
5 // Fill out the popup with whatever you want - it will be on top, it will animate in nicely,
6 // and so on; the popup part has been completely handled for you.
7 //
8 // The GUI state here is now set to what it was when the popup's scope was created:
9 // the font size is 16, the transform matrix is the same, we will be drawing into the correct
10 // z-index, etc.
11
12 // Oh, and this is possible, too:
13 popup.Width(700); // Modify the originally declared popup
14}

Cross-frame/temporal data coherence

Most of the features we've talked about so far are only possible to provide if you know the full context of the frame, which we can only really know if we look at what happened in the previous frame, or rather, the previous "pass". This brings with it a whole host of advantages and challenges, mostly related to matching up data correctly across multiple frames.

IMGUI is, funnily enough, "immediate mode": you are always building the frame as you go, declaring things one at a time. However, sometimes, elements declared later should logically have effects on elements declared earlier in the frame.

For example, perhaps a button should not be clickable because a UI element with a higher z-index is later drawn on top of it. Or an element might be positioned differently based on the size of another UI element that is declared later.

Solving such problems requires the full context of the frame to be known, so PanGui will run such computations at the end of the pass when all information is known. Layouts are calculated, z-indices are sorted, interactables resolve requested inputs, and so on, and then this computed information is passed back to the UI code in the next pass.

So in short, being multi-pass lets PanGui provide features such as a very powerful layouting system, full input and drawing support for z-indices, and so on. All these features are absolutely crucial for a library with PanGui's mission statement, and very much worth the added complexity of managing multiple passes.

That said, the problem of cross-frame data coherence still exists; data needs to be matched to specific locations in the code across frames, to make sure that the code that receives a computed value such as layouting information is the same piece of code that requested it in the last pass.

This sort of setup traditionally makes for an extraordinarily fragile and frustrating development experience. For example, should part of the user code suddenly request more or fewer layout nodes than in the last pass, everything after that change is now suspect - code will receive back the wrong layout nodes, and run based on faulty data.

The error will cascade, and often, bugs in one spot in the code are in fact caused by a totally different part of the code in a far removed part of the user interface that introduced some offset to the data access patterns compared to the last pass. This makes such code very difficult and opaque to debug, as every pass relies on state from the previous pass that might no longer match up correctly.

To solve this problem, PanGui has the concept of data scopes. Think of data scopes as a keyed hierarchy of data contexts unable to affect or leak into each other. Each scope has its own regions of cross-frame memory that it advances through independently of other data scopes.

As such, data can generally always be matched very precisely to the code that requested it, and any piece of code can always very easily and cheaply insulate itself from the behaviour of other parts of the code.

At the same time, the scope system is highly optimized: all data ends up in the same shared, linear memory buffers, and entering and exiting a scope is dirt cheap. Each scope has a very low overhead, and the system is written to gracefully handle potentially tens of thousands of data scopes.

And much more

PanGui has a lot of features that we haven't talked about yet, many of which are still in development.

Controls

Stateful input elements that can handle focus and selection, accommodate selection navigation, and so on. Controls will make it trivial to have proper cross-platform user experiences that adapt well to available input devices.

Retained workflow

Declare and bind a hierarchy of styled UI nodes, or load them from specification files. Retained nodes ultimately resolve to immediate mode calls and as such fully interoperate with pure immediate mode code.

Render Effects

It should be trivial to apply render effects such as blurring everything behind a popup, or adding a frosted glass filter, or bloom, or anything else like it. This will be a pre-built library of out-of-the-box supported effects, as well as a method of easily adding your own without needing to modify the platform layer too much.

Vector Graphics API

SDF shapes are amazing for many things, but for some shapes, generating meshes from vector definitions is a more appropriate solution. PanGui will include a comprehensive vector graphics API for loading and generating vector shapes, including support for loading .svg files and the like.

Animations

A simple and powerful animation system for animating any arbitrary value over time using a variety of behaviours, transitions and easing functions.

Platform layers

As development progresses towards beta and release, we will continually be adding more platform layers to PanGui, and further build out our tooling for this process.

Foreseeably Asked Questions

Who are you guys?

We're a small team of passionate developers who care deeply about the quality of software.

You may or may not be familiar with names like Casey Muratori and Jonathan Blow. Say what you will about them, but we think they've basically got it right. The software industry has a problem, and it needs to be fixed.

Some may know us as the developers of the Odin Inspector & Serializer and Odin Validator plugins for the Unity game engine. For those who don't know: Odin Inspector is a plugin that makes it really easy to create and customize editor tools to work with your game's data. That is, we've spent the past many years working on trying to make it easy to create complicated user interfaces for editor tools in Unity. Odin, of course, is very limited in scope, made for producing only very utilitarian tooling UIs, and is built on Unity's rendering tech stack.

However, we have in essence been smashing our faces against some version of The Problem every day since 2017. From creating websites to trying to figure out how to bring Odin (or something like it) to other platforms than Unity, we've constantly been confronted with the lack of something like PanGui, the existence of which would have made our (and everybody else's) lives significantly easier. Which is, of course, why we're making it now.

How will PanGui affect Odin in the future?

You'll just have to wait and see ;)

What's the licensing and pricing going to be like?

Look, we're going to level with you. We really don't know. We have two overriding priorities:

First, we want to put PanGui into the hands of as many people as possible and make it as accessible as possible. Second, we would prefer not to starve to death.

The extreme version of the first priority is making PanGui fully free and open source for anyone and everyone. The extreme version of the second priority is creating some pricing scheme where people have to pay us exorbitant fees just to glance briefly at an impressionist painting of the source code.

Clearly, neither will work out, so our goal is to strike some balance between these two extremes, preferably getting as close to the first one as possible. If you have any opinions or ideas, we would love to hear them.

As of this moment, we think PanGui is very likely to be some sort of open source, and hopefully the vast majority of people using it will never have to pay a dime. If we can make that work, we will.

What in the world does language-agnostic mean? Will there be wrappers?!

PanGui is explicitly written with the goal in mind of being a very easily transpilable library. The code is written in a simple and direct style, using practically no language or runtime features and mostly working directly with unmanaged memory and its own collection implementations. As long as you can allocate and free memory, have pointers to that memory, and do standard bitwise and arithmetic operations on that memory, PanGui will translate easily.

It is our goal that PanGui can be transpiled from its original source language to any target language, using language-specific templates that will add idiomatic APIs and usage patterns to any given language distribution so it feels like PanGui was written in and for that language to begin with.

Some of you are probably asking: why not just write it in something like C/C++, and create wrappers?

Because using a library through a wrapper API is typically not very nice. It does not feel native or natural to the language, and often forces you into code patterns that do not fit in. Oftentimes, such wrapper APIs also have significant performance penalties - especially for managed languages, transitioning through a wrapper API call into an unmanaged context comes with a considerable overhead.

Translating PanGui's codebase allows users to work directly with the source code in their native language environment. Users will be able to use all of the tools and methods they're used to: stepping into code, attaching debuggers and profilers, and so on and so forth. Additionally, the compiler will have a far greater understanding of the codebase, letting it do a far better job of things like inlining and optimizing many of the PanGui API calls.

That said, for certain languages, wrappers are the most sensible approach. We don't propose translating PanGui into languages like Python or JavaScript. Wrappers around a native library are clearly the best solution, there, since such languages are so incredibly slow that any wrapper overhead pales in comparison to running the library code "natively" in the language itself.

Second, you might ask: why write it in C# first?

It's a good question. First off, we know C# and its tooling extremely well, so the development process has been easier for us than it otherwise would have been. Second, one of our obvious first target environments is the Unity game engine, since we already have a presence in that community through our Odin Inspector and Validator plugins. Finally, C# can be written in a very C-like style. Most of PanGui looks a great deal like C++ or even just C source code, which makes it easy to ultimately convert to something else.

However, we doubt C# will remain the base language of PanGui for very long. It is a great place for us to start and get this project off the ground, but in the long term, as we need to start adding more transpile targets, there are other languages that are more suitable. Luckily, given the way PanGui is written, even manually converting the core library to another language would not be a particularly difficult or time-consuming task.

As for which language we see ultimately becoming the base language of PanGui, it is as yet undecided. However, we see something like Jai as a good candidate, assuming it is ever released. Jai's meta-programming features in particular are tempting, as they would hand us the solution to a significant and tedious part of the transpilation problem out of the box.

Who is PanGui for?

More seriously, we see PanGui ultimately being usable by practically anyone wanting to create a user interface or application practically anywhere, in any context. To start with, however, the primary audience is most likely going to be game developers.

The core library itself is very easy to integrate into a custom engine, and as we (and perhaps, hopefully, other people!) begin adding platform integrations with various common game engines or for application development on various platforms and in various languages, PanGui will grow increasingly accessible and applicable regardless of which language or environment you happen to be working in.

What are the differences between PanGui and Dear ImGui?

First off, a huge shout-out to Omar Cornut and everyone else who has contributed to Dear ImGui. It's a fantastic library and has been a huge inspiration to us.

To answer the question, here's a quote from Dear ImGui's own description: Dear ImGui is designed to enable fast iterations and to empower programmers to create content creation tools and visualization/debug tools (as opposed to UI for the average end-user). It favors simplicity and productivity toward this goal and lacks certain features commonly found in more high-level libraries.

PanGui is a more "high-level library"; it is aimed at a broader spectrum of use-cases, to create any kind of conceivable user interface in general. While it excels in tooling, just like Dear ImGui, PanGui accepts some increased complexity - such as being a multi-pass IMGUI implementation - in exchange for being able to have a far richer set of features like advanced layouting; the ability to easily generate complex, styled visual shapes; and generally emphasizing making it really easy for the developer to create more sophisticated user interfaces.

We think PanGui's features compare very favorably to any other UI technology, particularly to the web trio of HTML, CSS and JavaScript, being both far easier to use and capable of expressing more complicated and useful concepts in terms of layouting and visuals. And it doesn't hurt that it's a lot faster as well.

Is PanGui multi-pass? Isn't that bad and tedious to reason about?

PanGui is a multi-pass IMGUI implementation, meaning that sometimes, the UI code will be executed several times in a row, such that the library can gather required information such as layouting requests, calculate the layout in between passes, and then pass the results back to the requesting code in the next pass.

There are many common pitfalls or drawbacks of multi-pass IMGUI implementations. We believe we've avoided or solved most of them. Let's talk about a few of them:

Complicated and tedious event and input handling

In PanGui, most frames are in fact single-pass; so long as nothing significant has changed since the last frame, the UI code runs only once, relying on results from the last frame.

There are no "event passes", as you might find in, for example, Unity's version of IMGUI. Most frames where something changes will have only two passes: a pass that invalidates the last frame (likely due to input), and a pass after everything that was invalidated (such as the layout) has been recalculated based on the new state.

Input handling is very simple, and is typically done through the interactable system, which does the work of figuring out whether something is a drag or not, and so on. PanGui has no concept of a global "hot control" or any other extremely fragile state like that.

Passes with invalid computed data, or "data pass-back shifting/leaking"

We're not sure whether there's a name for this problem. The multi-pass approach fundamentally relies on the UI code doing the same things in the same order every pass, such that it is possible to "hand back" requested values in later passes to the correct "place in the code" that requested them.

This is an extraordinarily fragile and frustrating pattern. For example, should the user code suddenly request more or fewer layout nodes than last pass, everything after that change is now suspect - code will receive back the wrong layout nodes, and run based on faulty data. The error will cascade, and soon chaos ensues. Often, bugs in one spot in the code are in fact caused by a totally different part of the code in a far removed part of the user interface. This makes such code very difficult and opaque to debug, as every pass relies on state from the previous pass that might no longer match up correctly.

PanGui does several things to resolve this problem. First, there are fewer passes in general, each handling more things at once, leaving fewer chances for such errors to creep in. There is no concept of global state such as control ID's or hot controls, and as mentioned above, no concept of event passes at all. Finally, PanGui introduces the concept of the "data scope".

Think of data scopes as a keyed hierarchy of scopes that cross-pass retained/computed values such as layout info, interactables, and so on cannot leak into or out of. Each data scope is its own unique, cross-pass data context that can very reliably be matched to the requesting code in each pass. This lets any region of code easily and cheaply guard itself against changes happening in other parts of the code.

Will there be an "XML / CSS"-like way of working with PanGui?

At first, we've focused on creating the best fully-featured pure code API for creating UI that we could. Still, loading resource files that define the structure, style and possibly even behaviour of a user interface is definitely a useful solution to several problems, and so something like that will absolutely be a feature.

You will note, however, that we write 'something like that'. We're going to try to boil such features down to the purest form of the problems they are actually meant to solve: 'hot-loading', widget templates, designer friendliness, creating interface specs in other applications, swapping styles, etc, etc. Then we'll work our way towards good solutions from there, hopefully refining the solution to a composable and easily controllable or customizable feature set, rather than the sort of monolithic solutions that are more common around the industry.

Contact

If you have any questions, comments, or feedback, we would love to hear from you. Please don't hesitate to reach out to us.

©2024 Sirenix ApS
PANGUI™ is a registered trademark of Sirenix ApS in the EU and elsewhere. Other names or brands are trademarks of their respective owners.