Overlay a Loading Spinner

Reflex gives you two ways to show a loading spinner:

  • a float placed on top of the layout with AddFloat
  • a blocking overlay that covers its parent and swallows input with AcquireOverlay

Pick by whether the load actually stops the user from interacting.

  • Inline or passive spinner. Work continuing in the background. No overlay; just AddFloat or AddInline the spinner into a container.
  • Modal busy spinner. A network call, a file save, anything where clicking elsewhere would break things. AcquireOverlay for blocking and full-bleed coverage, with the spinner drawn by the overlay's style or floated in as a child.

Put things on top with AddFloat

GLX::AddFloat is the simple way to place an object on top of others:

GLX::AddFloat(m_panel, m_spinner, GLX::kAlignmentCenter);
  • The child is taken out of the flow.
  • It is positioned independently inside the parent's rectangle, by one of the nine kAlignment values from kAlignmentTopLeft to kAlignmentBottomRight.
  • It does not consume flow space and does not push siblings; it can overlap them.

Whatever the float covers sits behind it and cannot be accessed, but input everywhere else stays live. That makes it right for a passive indicator. The spinner shows work is continuing while the user keeps using the rest of the UI. Remove it with m_spinner.Detach() when the work completes.

If you want the spinner to take its own place in the layout instead of overlapping anything, use GLX::AddInline(parent, m_spinner); the child then participates in the flow like any sibling.


Block the user with AcquireOverlay

If the user must not interact at all during the load, a float is not enough. GLX::AcquireOverlay mounts an overlay object over a parent, covering it completely, children included.

parent, with inline children underneath m_a m_b m_c loading overlay AcquireOverlay stretches over the parent and everything inline inside it

It takes six arguments, and the style name is the fifth:

GLX::AcquireOverlay(m_content, "Loading", true, true, "BusyOverlay",
	[this](GLX::Object & overlay)
{
	// build the spinner into the overlay, covered below
});

The six arguments, in order.

  • m_content is the parent the overlay stretches over.
  • "Loading" is the key that identifies the overlay so you can dismiss it later.
  • The first boolean blocks input to everything underneath.
  • The second boolean blocks drag-and-drop underneath.
  • "BusyOverlay" names the style the overlay is drawn with.
  • The lambda runs once, when the overlay is first created.

Calling AcquireOverlay again with the same key while the overlay is already up is safe and does nothing new, so you can call it from a per-frame update without guarding.

Pass all six arguments. The overload with five compiles fine but drops the style name, and the overlay renders unstyled. If your spinner shows up invisible, count your arguments first.

When the load finishes, dismiss it with the same parent and key pair:

GLX::DiscardOverlay(m_content, "Loading");

DiscardOverlay returns whether an overlay was actually removed, and calling it when nothing is mounted is harmless. Always pair every acquire with a discard on every exit path, or the overlay stays up forever.


Build the spinner itself

Reflex has no built-in spinner widget. The standard pattern is a style whose arc length is bound to a property, driven from C++ by an animation clock. In your stylesheet:

BusyOverlay:
{
	progress:
	Circle(width: 8; color: 50,58,62,32),
	Circle(width: 8; sweep: &value; color: 50,58,62; round_cap: true),
	Rotate(angle: 0.5; content: Circle(width: 8; sweep: &value; color: 50,58,62; round_cap: true));

	bg_colour: 235,238,240,160;

	bg:
	Align
	(
		position: center;
		size: 128;
		content: Render(pad: 16; density: 8; content: Align(content: progress))
	);
};

progress is a named slot holding three layers. A faint full circle is the track (the same grey at alpha 32), a solid arc has its sweep bound to the object's value property, and a copy of that arc rotated half a turn makes the spinner read as two arcs chasing each other. The translucent light grey bg_colour frosts the covered content, and Align centres a 128 pixel render of the slot inside the overlay.

On the C++ side, hold a heap-allocated property as a member and initialise it in the constructor's init list:

Reference<Data::Float32Property> m_progress;

// in the constructor init list:
m_progress(New<Data::Float32Property>())

Then wire it up inside the AcquireOverlay init lambda:

GLX::AcquireOverlay(m_content, "Loading", true, true, "BusyOverlay",
	[this](GLX::Object & overlay)
{
	m_progress->value = 0.0f;

	GLX::AttachAnimationClock(overlay, "clock", [this, &overlay](Float delta)
	{
		m_progress->value = Modulo(m_progress->value + (delta * 0.5f), 1.0f);
		overlay.Realign();
	});

	overlay.SetProperty(GLX::kvalue, m_progress);
});

Three details matter here. delta is seconds since the last frame, and GLX angles are turns rather than radians, so delta * 0.5f with a Modulo wrap spins the arc half a turn per second. overlay.Realign() is what triggers the repaint; without it the property changes but the spinner never redraws. And SetProperty(GLX::kvalue, m_progress) is the binding that lets &value in the stylesheet read your property.

There is no cleanup step. The clock is attached to the overlay object, so DiscardOverlay tears both down together.

The style route keeps the whole visual in the stylesheet, but the init lambda receives the overlay object, so you can also AddFloat a ready-made spinner child into it there instead.


Where to go next