Sedulous includes a UI framework inspired by Android’s view system. It provides layout containers, controls, theming, data binding, drag-drop, and both screen-space and world-space rendering.
Every UI element is a View. Views form a tree rooted at a RootView, which maps to a window or viewport.
RootView (owns viewport size + DPI)
FlexLayout (vertical)
MenuBar
FlexLayout (horizontal)
TreeView (hierarchy)
ViewportView (3D scene)
PropertyGrid (inspector)
StatusBar
View is the base class. Key properties:
Width, Height, Bounds – layout resultsVisibility – .Visible, .Invisible (takes space), .Gone (no space)IsInteractionEnabled – enables/disables inputOpacity – visual opacity (0-1)Cursor – mouse cursor typeStyleId – for CSS-like stylingPadding – inner spacingViewGroup extends View with child management:
AddView(child, layoutParams)RemoveView(child, deleteChild)GetChildAt(index), ChildCountUIContext is the central coordinator. It manages:
let ctx = new UIContext();
let root = new RootView();
root.ViewportSize = .(1280, 720);
ctx.AddRootView(root);
// Each frame:
ctx.BeginFrame(deltaTime);
root.Measure(BoxConstraints.Tight(1280, 720));
root.Layout(0, 0, 1280, 720);
ctx.ProcessInput(inputManager);
// Draw...
ctx.EndFrame();
In practice, EngineUISubsystem or EditorApplication handles the frame loop.
Views go through three phases each frame:
BoxConstraints)UIDrawContextOverride OnMeasure, OnLayout, OnDraw in custom views.
The most versatile layout, inspired by CSS Flexbox:
let container = new FlexLayout();
container.Direction = .Vertical; // or .Horizontal
container.Spacing = 8;
container.Padding = .(16);
// Child with fixed height
container.AddView(header, new FlexLayout.LayoutParams() {
Width = .Match, Height = .Fixed(.Px(40))
});
// Child that fills remaining space
container.AddView(content, new FlexLayout.LayoutParams() {
Width = .Match, Grow = 1
});
// Child that wraps its content
container.AddView(footer, new FlexLayout.LayoutParams() {
Width = .Match, Height = .Wrap
});
LayoutParams properties:
Width, Height – SizeSpec: .Match (fill parent), .Wrap (fit content), .Fixed(.Px(n))Grow – flex grow factor (distribute remaining space)Shrink – flex shrink factorMargin – outer spacing| Layout | Description |
|---|---|
DockLayout |
Dock children to edges: .Top, .Bottom, .Left, .Right, .Fill |
GridLayout |
Row/column grid with cell spanning |
FlowLayout |
Flowing items with line wrapping |
FrameLayout |
Overlapping children (stacked) |
AbsoluteLayout |
Positioned by exact coordinates |
SizeSpec.Match // Fill available space
SizeSpec.Wrap // Fit content
SizeSpec.Fixed(.Px(200)) // Exact pixels
let btn = new Button("Click Me");
btn.OnClick.Add(new (b) => { DoSomething(); });
let toggle = new ToggleButton("Toggle");
toggle.OnCheckedChanged.Add(new (t, checked) => { });
let check = new CheckBox("Enable", true);
let radio = new RadioButton("Option A");
let edit = new EditText();
edit.SetText("Initial value");
edit.OnTextChanged.Add(new (e, text) => { });
let password = new PasswordBox();
let numeric = new NumericField(min: 0, max: 100, value: 50);
let combo = new ComboBox();
combo.AddItem("Option 1");
combo.AddItem("Option 2");
combo.OnSelectionChanged.Add(new (c, index) => { });
let slider = new Slider(min: 0, max: 1, value: 0.5f);
slider.OnValueChanged.Add(new (s, val) => { });
let label = new Label();
label.SetText("Hello World");
label.FontSize = 14;
label.TextColor = .(255, 255, 255);
label.HAlign = .Center;
label.Ellipsis = true; // Truncate with "..."
let image = new ImageView(imageData);
image.ScaleType = .FitCenter; // .None, .FillBounds, .CenterCrop
let progress = new ProgressBar();
progress.Value = 0.75f;
// ListView with adapter pattern
let list = new ListView();
list.SetAdapter(myAdapter); // IListAdapter implementation
list.Selection.OnSelectionChanged.Add(new () => { });
// TreeView with hierarchical adapter
let tree = new TreeView();
tree.SetAdapter(myTreeAdapter); // ITreeAdapter implementation
let scroll = new ScrollView();
scroll.AddView(contentView, new LayoutParams() {
Width = .Match, Height = .Wrap
});
let panel = new Panel();
panel.Background = new ColorDrawable(.(40, 40, 50, 255));
let expander = new Expander("Details", expanded: false);
expander.AddView(detailContent);
let tabs = new TabView();
tabs.AddTab("Tab 1", content1);
tabs.AddTab("Tab 2", content2);
Drawables are reusable visual elements for backgrounds, borders, and decorations:
// Solid color
view.Background = new ColorDrawable(.(30, 30, 35, 255));
// Gradient
view.Background = new GradientDrawable(topColor, bottomColor, .Vertical);
// 9-slice (stretchable border)
view.Background = new NineSliceDrawable(imageData, .(10, 10, 10, 10));
// Rounded rectangle
view.Background = new RoundedRectDrawable(.(50, 55, 65, 255), 8);
// State-dependent (changes on hover/press)
let states = new StateListDrawable();
states.AddState(.Normal, new ColorDrawable(.(50, 55, 65)));
states.AddState(.Hovered, new ColorDrawable(.(60, 65, 80)));
states.AddState(.Pressed, new ColorDrawable(.(70, 80, 100)));
view.Background = states;
// Layered (composited)
view.Background = new LayerDrawable(borderDrawable, fillDrawable);
The framework ships with built-in themes:
ctx.SetTheme(new DarkTheme()); // Dark color scheme
ctx.SetTheme(new LightTheme()); // Light color scheme
ctx.SetTheme(new RoundedDarkTheme()); // Rounded corners
Type-based styling via StyleSheet. Rules match by view type, style class, and control state:
let sheet = new StyleSheet();
// Match all Button views
sheet.ForType(typeof(Button))
.Set(.Background, new ColorDrawable(.(60, 65, 80)))
.Set(.TextColor, Color(220, 225, 235));
// Match Button views in Hovered state
sheet.ForTypeState(typeof(Button), .Hovered)
.Set(.Background, new ColorDrawable(.(70, 80, 100)));
// Match by style class (any view type)
sheet.ForClass("toolbar-button")
.Set(.Background, new ColorDrawable(.(50, 55, 65)));
ctx.SetStyleSheet(sheet);
Views resolve style properties via ResolveStyleColor, ResolveStyleDrawable.
Custom themes can extend the base with additional style rules:
class MyThemeExtension : IThemeExtension
{
public void Apply(StyleSheet sheet) { /* add custom rules */ }
}
let menu = new ContextMenu();
menu.AddItem("Cut", new () => Cut());
menu.AddItem("Copy", new () => Copy());
menu.AddSeparator();
let sub = menu.AddSubmenu("More");
sub.Submenu.AddItem("Details...", new () => ShowDetails());
menu.Show(ctx, screenX, screenY);
let dialog = Dialog.Confirm("Delete?", "Are you sure?");
dialog.OnClosed.Add(new (dlg, result) => {
if (result == .OK)
PerformDelete();
});
dialog.Show(ctx);
view.TooltipText = "Hover text here";
// Or implement ITooltipProvider for custom tooltip content
Implement IDragSource on the source view and IDropTarget on the target:
// Source
class MyDragSource : View, IDragSource
{
public DragData CreateDragData() => new MyDragData(mItem);
public View CreateDragVisual(DragData data) => new Label("Dragging...");
public void OnDragStarted(DragData data) { Opacity = 0.5f; }
public void OnDragCompleted(DragData data, DragDropEffects effect, bool cancelled)
{
Opacity = 1.0f;
}
}
// Target
class MyDropTarget : View, IDropTarget
{
public DragDropEffects CanAcceptDrop(DragData data, float x, float y)
=> (data is MyDragData) ? .Move : .None;
public DragDropEffects OnDrop(DragData data, float x, float y)
{
// Handle the drop
return .Move;
}
}
Property<T> provides observable values with change notifications:
let health = new Property<float>(100);
health.Changed.Add(new (oldVal, newVal) => {
healthBar.Value = newVal / maxHealth;
});
health.Value = 75; // Triggers Changed event
For large data sets, ListView uses an adapter pattern with view recycling:
class MyAdapter : ListAdapterBase
{
public override int32 ItemCount => (int32)mItems.Count;
public override View CreateView(int32 position)
{
let label = new Label();
label.SetText(mItems[position]);
return label;
}
public override void BindView(View view, int32 position)
{
if (let label = view as Label)
label.SetText(mItems[position]);
}
}
For in-game UI rendered on screen (HUD, menus, debug overlays):
// EngineUISubsystem is registered automatically by EngineApplication
let uiSub = Context.GetSubsystem<EngineUISubsystem>();
// Add views to the screen overlay
let hud = new FlexLayout();
hud.Direction = .Vertical;
// ... build HUD views
uiSub.ScreenView.AddView(hud);
UI panels rendered in 3D space (in-world menus, interactive terminals, signage):
let uiMgr = scene.GetModule<UIComponentManager>();
if (let uiComp = uiMgr.Get(uiMgr.CreateComponent(entity)))
{
uiComp.Width = 200;
uiComp.Height = 100;
uiComp.PixelsPerUnit = 100; // UI pixels per world unit
// Add views to uiComp.RootView
}
World-space UI panels are rendered via WorldUIPass and respond to input via raycasting.
Note: Each world-space UI component has its own UIContext, so they are relatively heavyweight. Use them for interactive panels, not for many small elements like per-entity health bars. For large numbers of simple indicators, use sprites, screen-space overlays, or
DebugDraw(which supports 3D-positioned lines and screen-space text/rectangles – see Rendering).
Create custom views by extending View:
class MyWidget : View
{
protected override void OnMeasure(BoxConstraints constraints)
{
MeasuredSize = .(constraints.ConstrainWidth(100), constraints.ConstrainHeight(50));
}
public override void OnDraw(UIDrawContext ctx)
{
ctx.VG.FillRect(.(0, 0, Width, Height), .(80, 100, 200, 255));
if (let font = ctx.FontService?.GetFont(14))
ctx.VG.DrawText("Custom!", font, .(0, 0, Width, Height), .Center, .Middle, .White);
}
public override void OnMouseDown(MouseEventArgs e)
{
// Handle click
e.Handled = true;
}
}