Table of Contents

Toasts, tooltips and key capture

Three small services sit above the views: toasts report what happened, tooltips say what the pointer rests on, and key capture takes the next key the player presses, for rebinding. All three are registered by AddUI(), and are available by constructor injection or from GetService<T>().

Toasts

A toast is a short message near the bottom of the window, above every view. It goes away by itself unless it is still needed. Use one for a one-line outcome, such as "Saved" or "Could not connect", instead of a status label inside a form.

IToastService shows them:

using BSGameLib.UI.Toasts;

public class ConnectionStatus
{
  private readonly IToastService _toasts;

  public ConnectionStatus(IToastService toasts) => _toasts = toasts;

  public void Saved() => _toasts.Show("Saved", ToastKind.Success);

  public void Connecting() => _toasts.Show("Connecting...", ToastKind.Progress, "connection");

  public void Failed() => _toasts.Show("Could not connect", ToastKind.Error, "connection");

  public void Connected() => _toasts.Show("Connected", ToastKind.Success, "connection");

  public void Cancelled() => _toasts.Dismiss("connection");
}
Method What it does
Show(message, kind, channel) Shows a toast. kind defaults to Info, and channel to none.
Dismiss(channel) Takes down the toast on a channel, if there is one.
Clear() Takes down every toast.

A channel groups the toasts from one source. A new toast on a channel replaces the one showing there, so "Connecting..." turns into "Could not connect" and does not stack under it. A toast with no channel stacks with the others, newest at the bottom.

The kind decides the accent colour and how long the toast stays:

ToastKind How long it stays
Info A few seconds.
Success A few seconds.
Error A little longer, to be read.
Progress Until its channel is given another toast or dismissed. Use it for work whose length is not known.

Looks

Set ToastStyle on the service to change the colours, the font and the size. Colours take anything a view's colour attributes take, including {Resource} references, and they are read when the toast is drawn:

toasts.Style = new ToastStyle
{
  FontSize = 14,
  ErrorColor = "#ff6b6b",
  MaxWidth = 360,
};
Property Default What it does
Font null A .ttf, .otf or BMFont path under the content root. null uses the built-in font.
FontSize 13 The point size.
BackgroundColor, BorderColor, TextColor #14141c, #2a2a38, #e6e6ee The box and its text.
InfoColor, SuccessColor, ErrorColor, ProgressColor The accent for each kind.
MaxWidth 420 The longest a toast may be before its text wraps, at a UI scale of 1.
BottomMargin 28 The distance from the bottom of the window, at a UI scale of 1.

Sizes grow with the UI scale.

Tooltips

A tooltip is a short box beside the pointer, drawn over every view and panel. Only one is shown at a time. The game decides when to show one and when to hide it, and gives the lines to show:

using BSGameLib.Game.Drawing;
using BSGameLib.UI.Tooltips;

public class ItemTooltips
{
  private readonly ITooltipService _tooltips;

  public ItemTooltips(ITooltipService tooltips) => _tooltips = tooltips;

  public void Show(string name, int gold) =>
    _tooltips.Show(new[]
    {
      new TooltipLine(name),
      new TooltipLine($"{gold} gold", Color.Translate("#e0c060")),
    });

  public void Hide() => _tooltips.Hide();
}
Member What it does
Show(lines) Shows the lines beside the pointer, in place of any tooltip showing. An empty list hides it.
Hide() Hides the tooltip.
IsShowing true while lines are showing.
Style A TooltipStyle, set in the same way as the toast style.

A TooltipLine is its text, and its colour. A line with no colour uses the style's text colour.

TooltipStyle has the same colour and font properties as ToastStyle, with FontSize of 12, MaxWidth of 280, and Offset, the distance of the box from the pointer, of 16. Sizes are at a UI scale of 1 and grow with it.

Key capture

Rebinding a key needs the next key the player presses, rather than the key that is acted on. The IKeyCapture service waits for it:

using BSGameLib.UI;

public class KeyBindingsViewModel : ViewModel
{
  private readonly IKeyCapture _keys;
  private string _jumpKeyName = "Space";

  public KeyBindingsViewModel(IViewManager viewManager, IKeyCapture keys) : base(viewManager)
  {
    _keys = keys;
    RebindJumpCommand = new DelegateCommand(BeginRebindJump);
  }

  public ICommand RebindJumpCommand { get; }

  public string JumpKeyName
  {
    get => _jumpKeyName;
    set => Set(ref _jumpKeyName, value);
  }

  private void BeginRebindJump() =>
    _keys.Begin(
      captured => JumpKeyName = captured.Key.ToString(),
      () => { /* keep the old binding */ });
}

Begin waits for one key. captured is called with the key and the modifiers held with it, and cancelled is called if the capture is cancelled. Both run on the game thread, once. A capture ends after one of them runs.

While a capture waits, the UI does not act on keys: the focused control gets none, Enter does not press the default button, and Tab does not move focus. Escape cancels the capture rather than being captured, and a click anywhere cancels it too. Beginning a new capture cancels one already waiting. Cancel() stops the wait, and IsCapturing tells whether one is waiting.

A modifier pressed and released on its own comes back as that key with no modifiers, so Shift on its own is a key that can be bound.

What the pointer rests on

Give a control a HoverKey, a name for what it shows, and the game can ask each frame what the pointer is resting on:

<Button Id="slot3" Text="Sword" Size="{120,32}" HoverKey="inventory.3" />
var hover = viewManager.HoverUnderPointer;
if (hover != null)
  ShowItemTooltip(hover.Key);

IViewManager.HoverUnderPointer is the HoverKey of the innermost control under the pointer that has one, with the view it is in, or null. It looks in the front panel under the pointer, or, over no panel, the active view. View.HoverKeyAt asks one view about any point.