using Amphora;
using Annulus;
using Azimuth;
using Clave;
using System;
using System.Collections.Generic;
using Prism;
namespace Glass.Testbed.TestCases.Layout
{
///
/// Hoisting and modality on screen: what escapes an owner's clip, what order overlays land in, and who
/// hears a click first.
///
public class OverlaysTestCase : TestCase
{
///
/// A panel that re-derives its position from its owner every tick, in screen space for as long as it is
/// hoisted.
///
private class Overlay : Panel
{
///
/// Where to sit relative to the owner's CanvasRect.
///
public Vector OffsetFromOwner { get; set; }
public Overlay()
{
// An overlay wants all three: screen space, drawn last, clipped by nothing.
Hoisted = true;
AlwaysOnTop = true;
ClipToAncestors = false;
}
public override void Update(Duration elapsed, UpdateState state)
{
base.Update(elapsed, state);
// Unhoisted, Bounds are in the owner's own space already.
Vector origin = Hoisted ? state.ParentCanvasRectOnScreen.TopLeft : Vector.Zero;
Bounds = Bounds.WithPosition(origin + OffsetFromOwner);
}
}
///
/// An Overlay that also takes first refusal on every click, and counts what it heard.
///
private class ModalOverlay : Overlay
{
public int HeardCount { get; private set; }
///
/// Whether to claim what it hears, which is the only thing that keeps input from the controls below.
///
public bool Swallow { get; set; }
public Action OnHeard;
public ModalOverlay()
{
Modal = true;
}
public override bool MouseDown(in MouseState state, MouseButtonFlags button,
in KeyboardSnapshot held, KeyboardManager keys)
{
base.MouseDown(state, button, held, keys);
++HeardCount;
OnHeard?.Invoke();
return Swallow;
}
public override bool MouseUp(in MouseState state, MouseButtonFlags button,
in KeyboardSnapshot held, KeyboardManager keys)
{
base.MouseUp(state, button, held, keys);
return Swallow;
}
public override bool MouseMoveStart(in MouseState state, in KeyboardSnapshot held, KeyboardManager keys)
{
base.MouseMoveStart(state, held, keys);
return Swallow;
}
public override bool MouseMove(in MouseState state, in KeyboardSnapshot held, KeyboardManager keys)
{
base.MouseMove(state, held, keys);
return Swallow;
}
public override bool MouseMoveEnd(in MouseState state, in KeyboardSnapshot held, KeyboardManager keys)
{
base.MouseMoveEnd(state, held, keys);
return Swallow;
}
public override bool Click(in MouseState state, MouseButtonFlags button, int clickCount,
in KeyboardSnapshot held, KeyboardManager keys)
{
base.Click(state, button, clickCount, held, keys);
return Swallow;
}
}
///
/// The Control flags the grid can flip.
///
[Flags]
private enum ControlFlags
{
Hoisted = 1,
AlwaysOnTop = 2,
ClipToAncestors = 4,
ClipsCanvas = 8,
}
private static readonly Vector OwnerSize = new Vector(150, 80);
private static readonly Vector FlapSize = new Vector(110, 60);
private static readonly Vector NestedFlapSize = new Vector(80, 40);
private static readonly Vector RidingFlapSize = new Vector(90, 40);
// The packsheet only has four colours besides the neutral, and there are seven boxes to tell apart.
private static readonly Material PurpleFlatPanel =
UiPacksheetMaterials.FlatPanel with { TintColor = Colors.MediumPurple };
private static readonly Material OrangeFlatPanel =
UiPacksheetMaterials.FlatPanel with { TintColor = Colors.Orange };
private static readonly ButtonMaterial SmallCheckboxUnchecked =
new ButtonMaterial(UiPacksheetMaterials.Checkbox_Unchecked);
private static readonly ButtonMaterial SmallCheckboxChecked =
new ButtonMaterial(UiPacksheetMaterials.Checkbox_Crossed);
private static readonly ControlFlags[] FlagColumns =
{
ControlFlags.Hoisted, ControlFlags.AlwaysOnTop, ControlFlags.ClipToAncestors, ControlFlags.ClipsCanvas,
};
private static Rect Encompassing(params Rect[] rects)
{
Vector topLeft = rects[0].TopLeft;
Vector bottomRight = rects[0].BottomRight;
for (int i = 1; i < rects.Length; ++i)
{
topLeft = Vector.Min(topLeft, rects[i].TopLeft);
bottomRight = Vector.Max(bottomRight, rects[i].BottomRight);
}
return Rect.FromPositionAndSize(topLeft, bottomRight - topLeft);
}
private static Vector LabelSize(StringView text)
{
return Widgets.ButtonLabel(text).PreferredSize(Constraints.Unbounded);
}
///
/// A button sized to , so its label changing never resizes it.
///
private static Button BuildButton(string debugName, Label label, Vector widestLabelSize)
{
label.Bounds = Rect.FromPositionAndSize(Vector.Zero, widestLabelSize);
Button button = label.WrapIn(UiPacksheetMaterials.Button, ButtonBehavior.Default);
button.DebugName = debugName;
return button;
}
private static bool GetFlag(Control control, ControlFlags flag)
{
switch (flag)
{
case ControlFlags.Hoisted: return control.Hoisted;
case ControlFlags.AlwaysOnTop: return control.AlwaysOnTop;
case ControlFlags.ClipToAncestors: return control.ClipToAncestors;
case ControlFlags.ClipsCanvas: return control.ClipsCanvas;
default: throw new ArgumentOutOfRangeException(nameof(flag), flag, "Expected a single flag");
}
}
private static void SetFlag(Control control, ControlFlags flag, bool value)
{
switch (flag)
{
case ControlFlags.Hoisted: control.Hoisted = value; break;
case ControlFlags.AlwaysOnTop: control.AlwaysOnTop = value; break;
case ControlFlags.ClipToAncestors: control.ClipToAncestors = value; break;
case ControlFlags.ClipsCanvas: control.ClipsCanvas = value; break;
default: throw new ArgumentOutOfRangeException(nameof(flag), flag, "Expected a single flag");
}
}
private static Vector SmallCaptionSize(StringView text)
{
return Widgets.SmallCaption(text).PreferredSize(Constraints.Unbounded);
}
///
/// A checkbox that starts out showing, and then writes, on .
///
private static Checkbox BuildFlagCheckbox(Control control, ControlFlags flag)
{
var checkbox = new Checkbox(ButtonBehavior.Default)
{
DebugName = $"{control.DebugName} {flag}",
UncheckedMaterial = SmallCheckboxUnchecked,
CheckedMaterial = SmallCheckboxChecked,
Checked = GetFlag(control, flag),
};
checkbox.OnCheckedChanged += delegate (bool checkedNow)
{
SetFlag(control, flag, checkedNow);
};
return checkbox;
}
private static RectAggregator NewCells(Rect row)
{
return new RectAggregator(
Rect.FromPositionAndSize(row.TopLeft, new Vector(0, row.Height)), new Vector(Margin, 0));
}
private static void AddCentered(Control parent, Control child, Rect cell)
{
Vector size = child.Bounds.Size;
child.Bounds = Rect.FromPositionAndSize(cell.TopLeft + Control.CenteredIn(cell.Size, size), size);
parent.CanvasChildren.Add(child);
}
///
/// A grid of flag checkboxes, a row per box and a column per flag. Each row is led by a swatch of the box's
/// own material, which is what ties a colour on screen to a name.
///
private static Panel BuildFlagGrid(IReadOnlyList<(Control control, ControlFlags flags)> rows)
{
Vector boxSize = Vector.Max(
SmallCheckboxUnchecked.PreferredSize(Constraints.Unbounded),
SmallCheckboxChecked.PreferredSize(Constraints.Unbounded));
Scalar textHeight = 0;
Scalar nameWidth = 0;
foreach (var row in rows)
{
Vector nameSize = SmallCaptionSize(row.control.DebugName);
nameWidth = Math.Max(nameWidth, nameSize.X);
textHeight = Math.Max(textHeight, nameSize.Y);
}
Scalar rowHeaderWidth = textHeight + Margin + nameWidth;
Scalar rowHeight = Math.Max(textHeight, boxSize.Y);
var columnWidths = new Scalar[FlagColumns.Length];
for (int column = 0; column < FlagColumns.Length; ++column)
{
columnWidths[column] = Math.Max(SmallCaptionSize(FlagColumns[column].ToString()).X, boxSize.X);
}
var grid = new RectAggregator(new Vector(0, CaptionMargin));
var panel = new Panel { DebugName = "flag grid", Material = Material.Invisible };
RectAggregator headerCells = NewCells(grid.NewRow(rowHeight));
headerCells.NewCol(rowHeaderWidth);
for (int column = 0; column < FlagColumns.Length; ++column)
{
var header =
new Label(Widgets.SmallCaptionStyle, FlagColumns[column].ToString(), Constraints.Unbounded);
AddCentered(panel, header, headerCells.NewCol(columnWidths[column]));
}
foreach (var row in rows)
{
RectAggregator cells = NewCells(grid.NewRow(rowHeight));
RectAggregator nameCells = NewCells(cells.NewCol(rowHeaderWidth));
var swatch = new Panel
{
DebugName = $"{row.control.DebugName} swatch",
Material = row.control.Material,
Bounds = Rect.FromPositionAndSize(Vector.Zero, new Vector(textHeight, textHeight)),
};
AddCentered(panel, swatch, nameCells.NewCol(textHeight));
var name = new Label(Widgets.SmallCaptionStyle, row.control.DebugName, Constraints.Unbounded);
AddCentered(panel, name, nameCells.NewCol(nameWidth));
for (int column = 0; column < FlagColumns.Length; ++column)
{
Rect cell = cells.NewCol(columnWidths[column]);
if (row.flags.HasFlag(FlagColumns[column]))
{
Checkbox checkbox = BuildFlagCheckbox(row.control, FlagColumns[column]);
checkbox.Bounds = Rect.FromPositionAndSize(Vector.Zero, boxSize);
AddCentered(panel, checkbox, cell);
}
}
}
panel.Bounds = Rect.FromPositionAndSize(Vector.Zero, new Vector(headerCells.Rect.Width, grid.Rect.Height));
return panel;
}
private void BuildHoistDemo()
{
// Flush with the owner's right edge and halfway down it, so it overhangs the bottom. The clipped flap
// shares the hoisted one's offset, so the two columns compare directly.
Vector flapOffset = new Vector(OwnerSize.X - FlapSize.X, OwnerSize.Y / 2);
// Centred on the flap's corner, so it pokes out past two of its edges.
Vector nestedOffset = FlapSize - NestedFlapSize / 2;
// Wholly outside the owner, so only escaping the owner's clip lets it be seen at all.
Vector ridingOffset = new Vector(-RidingFlapSize.X / 3, OwnerSize.Y + Margin);
Vector laterOffset = OwnerSize / 2;
// What overhangs the hoist owner still counts toward the space its column claims.
Rect hoistFootprint = Encompassing(
Rect.FromPositionAndSize(Vector.Zero, OwnerSize),
Rect.FromPositionAndSize(flapOffset, FlapSize),
Rect.FromPositionAndSize(flapOffset + nestedOffset, NestedFlapSize),
Rect.FromPositionAndSize(ridingOffset, RidingFlapSize),
Rect.FromPositionAndSize(laterOffset, OwnerSize));
var clippedOwner = NewChild("clipped owner");
clippedOwner.Material = UiPacksheetMaterials.GreenFlatPanel;
var clippedFlap = clippedOwner.CanvasChildren.NewChild("clipped flap");
clippedFlap.Material = UiPacksheetMaterials.RedFlatPanel;
clippedFlap.Bounds = Rect.FromPositionAndSize(flapOffset, FlapSize);
var hoistOwner = NewChild("hoisting owner");
hoistOwner.Material = UiPacksheetMaterials.FlatPanel;
var hoistedFlap = new Overlay
{
DebugName = "hoisted flap",
Material = PurpleFlatPanel,
Bounds = Rect.FromPositionAndSize(Vector.Zero, FlapSize),
OffsetFromOwner = flapOffset,
};
hoistOwner.CanvasChildren.Add(hoistedFlap);
// Sticks out past the flap, so it shows the nested hoist escaping a hoist's clip rather than merely
// drawing after a sibling, which any ordinary child would do.
var nestedFlap = new Overlay
{
DebugName = "nested hoist",
Material = UiPacksheetMaterials.YellowFlatPanel,
Bounds = Rect.FromPositionAndSize(Vector.Zero, NestedFlapSize),
OffsetFromOwner = nestedOffset,
};
hoistedFlap.CanvasChildren.Add(nestedFlap);
// AlwaysOnTop without Hoisted: parent space, so it rides along with its owner for free and needs no
// Update override, while still painting after the whole tree.
var ridingFlap = hoistOwner.CanvasChildren.NewChild("riding flap");
ridingFlap.Material = UiPacksheetMaterials.BlueFlatPanel;
ridingFlap.Bounds = Rect.FromPositionAndSize(ridingOffset, RidingFlapSize);
ridingFlap.AlwaysOnTop = true;
ridingFlap.ClipToAncestors = false;
// Added last, so it paints over both owners — and still ends up under the hoists.
var later = NewChild("later sibling");
later.Material = OrangeFlatPanel;
Panel flagGrid = BuildFlagGrid(new List<(Control control, ControlFlags flags)>
{
(clippedOwner, ControlFlags.ClipsCanvas),
(clippedFlap, ControlFlags.ClipToAncestors),
(hoistOwner, ControlFlags.ClipsCanvas),
(hoistedFlap, ControlFlags.Hoisted | ControlFlags.AlwaysOnTop | ControlFlags.ClipToAncestors),
(nestedFlap, ControlFlags.Hoisted | ControlFlags.AlwaysOnTop | ControlFlags.ClipToAncestors),
(ridingFlap, ControlFlags.AlwaysOnTop | ControlFlags.ClipToAncestors),
(later, ControlFlags.AlwaysOnTop),
});
Add(flagGrid);
Rect stage = ContentAggregator.NewRow(Math.Max(hoistFootprint.Height, flagGrid.Bounds.Height));
Rect clippedSlot = stage.NewCol(OwnerSize.X, margin: Gutter);
clippedOwner.Bounds = Rect.FromPositionAndSize(clippedSlot.TopLeft, OwnerSize);
Rect hoistSlot = stage.NewCol(hoistFootprint.Width, margin: Gutter);
hoistOwner.Bounds = Rect.FromPositionAndSize(hoistSlot.TopLeft - hoistFootprint.TopLeft, OwnerSize);
later.Bounds = Rect.FromPositionAndSize(hoistOwner.Bounds.TopLeft + laterOffset, OwnerSize);
flagGrid.Bounds = flagGrid.Bounds.WithPosition(stage.TopLeft);
}
private void BuildModalDemo()
{
int plainClicks = 0;
var plainLabel = new Label(Widgets.ButtonLabelStyle, "clicked 0");
Button plainButton = BuildButton("plain button", plainLabel, LabelSize("clicked 000"));
Add(plainButton);
plainButton.OnActivate += delegate (ButtonBase _, in KeyboardSnapshot __, KeyboardManager ___)
{
++plainClicks;
plainLabel.Text = $"clicked {plainClicks}";
};
var swallowLabel = new Label(Widgets.ButtonLabelStyle, "swallow: off");
Button swallowButton = BuildButton("swallow toggle", swallowLabel,
Vector.Max(LabelSize("swallow: off"), LabelSize("swallow: on")));
Vector heardSize = LabelSize("heard 000");
// Laid out before the modal exists, so the modal is sized to what it holds. The readout spans the width
// so its centred text stays put as the count grows.
Scalar contentWidth = Math.Max(heardSize.X, swallowButton.Bounds.Width);
var content = new RectAggregator(
Rect.FromPositionAndSize(Vector.Zero, new Vector(contentWidth, 0)), new Vector(0, Gutter));
var heardReadout = new Label(Widgets.ButtonLabelStyle, "heard 0")
{
Bounds = content.NewRow(heardSize.Y),
};
Rect swallowRow = content.NewRow(swallowButton.Bounds.Height);
swallowButton.Bounds = swallowButton.Bounds.WithPosition(swallowRow.TopLeft);
Vector modalSize = UiPacksheetMaterials.YellowPanel.BoundsSizeFor(content.Rect.Size);
Rect modalRow = ContentAggregator.NewRow(Math.Max(plainButton.Bounds.Height, modalSize.Y));
Rect buttonSlot = modalRow.NewCol(plainButton.Bounds.Width, margin: Gutter);
plainButton.Bounds = plainButton.Bounds.WithPosition(buttonSlot.TopLeft);
var modal = new ModalOverlay
{
DebugName = "modal overlay",
Material = UiPacksheetMaterials.YellowPanel,
Bounds = Rect.FromPositionAndSize(Vector.Zero, modalSize),
OffsetFromOwner = modalRow.NewCol(modalSize.X).TopLeft,
};
modal.CanvasChildren.Add(heardReadout);
modal.CanvasChildren.Add(swallowButton);
Add(modal);
modal.OnHeard = delegate
{
heardReadout.Text = $"heard {modal.HeardCount}";
};
swallowButton.OnActivate += delegate (ButtonBase _, in KeyboardSnapshot __, KeyboardManager ___)
{
modal.Swallow = !modal.Swallow;
swallowLabel.Text = modal.Swallow ? "swallow: on" : "swallow: off";
};
}
protected override void Populate()
{
AddCaption("Left: a normal child, clipped to its owner");
AddCaption("Right: a hoisted one, escaping it and painting after the whole tree");
AddCaption("Also right: on top but not hoisted, so it rides with its owner in the owner's own space");
BuildHoistDemo();
AddCaption("A modal hears every click wherever it lands");
AddCaption("It will keep input off anything else");
BuildModalDemo();
}
}
}