Window Layout

Every window has a root layout: the thing that turns its children into a picture. It decides which children claim edges, which share the scrolling middle, and what scrolling even means for that window.

The root is chosen per window and can be replaced.


The default: DockRootLayout#

Unless told otherwise, a window uses DockRootLayout: four edges around a scrolling body.

window.DockTop.Add(toolbar);
window.DockLeft.Add(sidebar);
window.DockBottom.Add(statusBar);
window.DockBody.Add(content);     // the scrolling middle

A child added with window.AddControl(...) and no edge goes to the body, which is why ordinary code needs to know none of this. StickyTop() / StickyBottom() map onto the top and bottom edges, so existing applications keep working unchanged.

The edges, the surfaces and the allocation rule are documented in Docking.


Replacing the root#

For one window#

window.Root = () => new DockRootLayout();   // this window only
window.Root = null;                          // back to the application default

For the application#

WindowRenderer.RootLayoutFactory = () => new MyRootLayout();

Root is a factory, not an instance. The renderer rebuilds its root whenever the DOM tree is rebuilt, and a shared instance would carry one window's banding and scroll position into the next rebuild. Assigning takes effect immediately — the window rebuilds its tree.


Writing a root layout#

A root implements IWindowRootLayout:

public interface IWindowRootLayout : ILayoutContainer, IRegionClippingLayout
{
    int ScrollOffset { get; set; }
    int ScrollableContentHeight { get; }
    int MaxScrollOffset { get; }

    void ScrollBy(int delta);
    void ScrollToTop();
    void ScrollToBottom();
    void PageUp();
    void PageDown();
}

Three parts, and only the first is unusual:

Scrolling is part of the contract. A root that bands its children is the only thing that knows which band scrolls — the window cannot work it out from outside. A root with no scrolling region reports a zero range and ignores the verbs; window.ScrollBy() then does nothing, which is the honest answer for a layout that does not scroll.

ILayoutContainer is the ordinary measure/arrange pair every container implements. See DOM layout.

IRegionClippingLayout lets the root clip each child separately. A root with pinned edges needs this: without it, scrolling body content paints straight over a docked toolbar once the body is long enough to reach it.

A minimal example#

A root that stacks children top to bottom and never scrolls:

public sealed class PlainStackRoot : IWindowRootLayout
{
    public int ScrollOffset { get; set; }
    public int ScrollableContentHeight => 0;
    public int MaxScrollOffset => 0;

    public void ScrollBy(int delta) { }
    public void ScrollToTop() { }
    public void ScrollToBottom() { }
    public void PageUp() { }
    public void PageDown() { }

    public LayoutSize MeasureChildren(LayoutNode node, LayoutConstraints constraints)
    {
        int y = 0;
        foreach (var child in node.Children)
        {
            if (!child.IsVisible) continue;
            var size = child.Measure(new LayoutConstraints(
                0, constraints.MaxWidth, 0, Math.Max(0, constraints.MaxHeight - y)));
            y += size.Height;
        }
        return new LayoutSize(constraints.MaxWidth, constraints.MaxHeight);
    }

    public void ArrangeChildren(LayoutNode node, LayoutRect finalRect)
    {
        int y = 0;
        foreach (var child in node.Children)
        {
            if (!child.IsVisible) { child.Arrange(LayoutRect.Empty); continue; }
            int h = child.DesiredSize.Height;
            child.Arrange(new LayoutRect(0, y, finalRect.Width, h));
            y += h;
        }
    }

    public LayoutRect GetPaintClipRect(LayoutNode child, LayoutRect parentClipRect)
        => parentClipRect;
}
window.Root = () => new PlainStackRoot();

Available root layouts#

Root Shape Scrolls
DockRootLayout (default) Four edges around a body the body

One ships today. The seam exists so that is not a permanent answer — a tabbed root, a split root or a fixed-pane root are all expressible against the same interface, and adding one requires no change to Window or the renderer.


How a window reaches its root#

Window
  └── WindowRenderer
        └── IWindowRootLayout          ← window.Root, or RootLayoutFactory
              ├── docked children      (DockRootLayout: edge strips)
              └── body node            (a ScrollLayout over the undocked children)
                    └── your controls

The window's scroll API is a facade over the root:

window.ScrollOffset        // → root.ScrollOffset
window.ScrollBy(5)         // → root.ScrollBy(5)
window.TotalLines          // → root.ScrollableContentHeight

Below the root, every container chooses its own layout by control type through LayoutNodeFactory — grid, scroll, stack, tab, collapsible. The root was the last node that did not, and making it polymorphic is what IWindowRootLayout is for.


Border-delegated scrollbars#

A scrolling region can hand its scrollbar to the window frame, reclaiming the column or row it would otherwise spend drawing one. Covered in Docking.


See also#

  • Docking — the edges, the surfaces, and the allocation rule
  • DOM layout — how measure, arrange and paint work
  • Windows — window lifecycle and the rest of the window API
  • GridControl — docking inside a grid