microsoft / microsoft/microsoft-ui-reactor

Design proposal: FitParent — container-sized rebuild for charts and other size-baked elements

Open
#174 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

design proposal
Dominant language
C#
Stars
646
Forks
54
Avg merge
1d 3h
Merged PRs (30d)
84

Description

Summary

Reactor charts (and any element with explicit pixel Width/Height baked into a build-time render) can't size to their container — their geometry is computed at render time from _width/_height and frozen. To make these elements responsive we'd need a measure→rebuild loop driven from the layout system. This is a proposal for FitParent, a Reactor-level wrapper that provides exactly that, without any new native control.

Status: design proposal, not landing yet. Filed for archival / discussion.

Motivation

Today this works:

PieChart(data, ...).Width(800).Height(500)

But this doesn't:

PieChart(data, ...).Stretch()

PieChart.BuildElement at src/Reactor/Charting/Charts.cs:531 computes the chart's coordinates (slice arcs, axes, label centroids) from _width/_height at construction time. The Element tree is baked at the size you pass in. WinUI's stretch model can't help — by the time layout knows the container's size, the geometry has already been emitted.

Same shape applies to anything else where pixel coordinates are computed at build time (custom canvases, hand-built shape paths, anything using D3Canvas).

Approach

A wrapper component that:

  1. Mounts a placeholder, observes SizeChanged on its host Border.
  2. Debounces resize events by ~150 ms and only then commits the new size to state, triggering a re-render at the new dimensions.
  3. During the debounce window applies a CompositeTransform to the host so the existing rendered visual scales smoothly with the drag, instead of vanishing and re-popping in.

Net effect: cheap visual feedback during the drag, one real rebuild on idle.

chart = Border(
    FitParent((w, h) => PieChart(data, ...).Width(w).Height(h))
).Flex(grow: 1, basis: 0);

Reference implementation

Tested in a sample app against the PieChart + LabelView use case. Lives in user code today (an instance method on the root Component), but the only reason it's not a top-level helper is that UseElementRef, UseState, and UseRef are protected on Component — wrapping it as a Component.Func(...) would let it be a static factory.

public Element FitParent(Func<double, double, Element> render)
{
    var hostRef    = this.UseElementRef<FrameworkElement>();
    var (committed, setCommitted) = UseState((W: 0.0, H: 0.0));
    var pendingRef = UseRef((W: 0.0, H: 0.0));
    var timerRef   = UseRef<DispatcherQueueTimer?>(null);

    Element child = committed.W > 0
        ? render(committed.W, committed.H)
        : TextBlock("");

    return Border(child)
        .Ref(hostRef)
        .OnSizeChanged((_, e) =>
        {
            var nw = e.NewSize.Width;
            var nh = e.NewSize.Height;
            pendingRef.Current = (nw, nh);

            // Transient: scale the existing rendered chart around its arranged
            // center, then translate so the scaled center lands on the host's
            // new center. The host Border centers its explicit-size child when
            // it fits and top-lefts it when it overflows — independently per
            // axis — so we have to compute the actual arranged center.
            if (committed.W > 0 && committed.H > 0 && hostRef.Current is { } host)
            {
                var s = Math.Min(nw / committed.W, nh / committed.H);
                var canvasX = Math.Max(0, (nw - committed.W) / 2);
                var canvasY = Math.Max(0, (nh - committed.H) / 2);
                var centerX = canvasX + committed.W / 2;
                var centerY = canvasY + committed.H / 2;
                host.RenderTransformOrigin = new Point(0, 0);
                host.RenderTransform = new CompositeTransform
                {
                    CenterX = centerX,
                    CenterY = centerY,
                    ScaleX = s,
                    ScaleY = s,
                    TranslateX = nw / 2 - centerX,
                    TranslateY = nh / 2 - centerY,
                };
            }

            // Commit: rebuild at the real (non-uniform) px when resize stops.
            if (timerRef.Current is null)
            {
                var dq = DispatcherQueue.GetForCurrentThread();
                if (dq is null) { setCommitted((nw, nh)); return; }
                var t = dq.CreateTimer();
                t.Interval = TimeSpan.FromMilliseconds(150);
                t.IsRepeating = false;
                t.Tick += (_, _) =>
                {
                    if (hostRef.Current is { } h) h.RenderTransform = null;
                    setCommitted(pendingRef.Current);
                };
                timerRef.Current = t;
            }
            timerRef.Current.Stop();
            timerRef.Current.Start();
        });
}

Tradeoffs / open questions

  1. Two-pass first render. Initial mount emits a placeholder; SizeChanged fires; state commits; second render produces the real chart. There's a one-frame flash of the placeholder. Acceptable for charts; might feel wrong for tightly-coupled visual transitions. Mitigation: read ActualWidth/Height synchronously on Loaded via the same ref to skip the empty first commit.

  2. Scale-during-drag distorts everything inside the host. Axis labels stretch with the chart, line strokes thicken/thin, font metrics get fuzzy. For a 100–200 ms resize burst this looks fine; sustained it would look bad. The 150 ms debounce is the lever — shorter for crisper rebuilds, longer to bound rebuild churn.

  3. Per-axis arranged-center math is load-bearing. The Border's child centering rule changes between fit and overflow per axis, independently. The reference impl handles this correctly across grow/shrink/asymmetric drags but anyone editing it should keep that in mind — earlier iterations got this wrong and produced "twice as fast" or "drifts toward top-left" behaviors.

  4. Re-render churn at fast resize cadence. SizeChanged fires on every pixel during a window drag. Each fire mutates a transform (cheap) and resets the timer (cheap). Only the final commit re-renders. Should be fine but worth a perf check on dense charts.

  5. No clean "resize done" event for window resizes. WinUI 3's window doesn't expose pointer-up for the system resize handle. Debounce is the only option there. For internal resize affordances (GridSplitter, custom drag handles) callers have DragCompleted/PointerReleased and could commit synchronously — worth a future variant that takes an explicit "commit now" trigger.

  6. First render's placeholder is TextBlock("") because Border(child) is non-nullable. Cosmetic but worth replacing with something the reconciler can recognize as a "transparent stand-in" and skip allocating a real TextBlock for.

  7. Should this live in core or charting? It's generic enough for any size-aware element. Suggested location: src/Reactor/Layout/FitParent.cs as a Component.Func-based factory. Charts could ship a thin wrapper Chart.FitParent(...) for ergonomic chained use.

Why not land it now

  • The reference impl is a Component-instance method on a sample app; promoting it to a public API needs the static-factory refactor and a real shape decision.
  • The two-pass first-render flash deserves polishing first.
  • We'd want at least one selftest covering grow/shrink/asymmetric drag before adding it as documented surface.

Discovery story

Came out of debugging PieChart not respecting its container in a demo app — see PR for the related crash-hardening fixes (degenerate-size guard in PieChartElement.BuildElement, XamlReader.Load path for PathGeometry mounting, and stack-trace-aware ErrorFallback UI). Those land separately; this stays archived until we decide on the API shape.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reading src/Reactor/Charting/Charts.cs around PieChart.BuildElement and the reference Component instance method described in the issue. Review the proposed src/Reactor/Layout/FitParent.cs location and the first-render, debounce, transform, and API-shape tradeoffs. Done would require an agreed public API and selftests for grow, shrink, and asymmetric resizing.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp
Domain
desktop
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Needs clarification
Newbie friendliness
30/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.