Docs

User Interface

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.

Core Concepts

View Hierarchy

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:

ViewGroup extends View with child management:

UIContext

UIContext 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.

Measure / Layout / Draw Pipeline

Views go through three phases each frame:

  1. Measure – determine desired size given constraints (BoxConstraints)
  2. Layout – assign final position and size
  3. Draw – render via UIDrawContext

Override OnMeasure, OnLayout, OnDraw in custom views.

Layout Containers

FlexLayout

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:

Other Layouts

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

SizeSpec.Match            // Fill available space
SizeSpec.Wrap             // Fit content
SizeSpec.Fixed(.Px(200))  // Exact pixels

Controls

Buttons

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");

Text Input

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);

Selection Controls

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) => { });

Labels and Display

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;

Lists and Trees

// 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

Scrolling

let scroll = new ScrollView();
scroll.AddView(contentView, new LayoutParams() {
    Width = .Match, Height = .Wrap
});

Containers

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

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);

Styling and Themes

Themes

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

Style Rules

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.

Theme Extensions

Custom themes can extend the base with additional style rules:

class MyThemeExtension : IThemeExtension
{
    public void Apply(StyleSheet sheet) { /* add custom rules */ }
}

Popups and Dialogs

Context Menu

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);

Dialog

let dialog = Dialog.Confirm("Delete?", "Are you sure?");
dialog.OnClosed.Add(new (dlg, result) => {
    if (result == .OK)
        PerformDelete();
});
dialog.Show(ctx);

Tooltips

view.TooltipText = "Hover text here";
// Or implement ITooltipProvider for custom tooltip content

Drag and Drop

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;
    }
}

Data Binding

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

ListView Adapter Pattern

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]);
    }
}

Screen-Space UI

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);

World-Space UI

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).

Custom Views

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;
    }
}