This document specifies a toast/notification system for XenoAtom.Terminal.UI:
Toast — the individual notification visualToastHost — the container that manages toast positioning and lifecycleToastService — the API for showing toasts from anywhere in the appThe goal is to provide non-blocking, auto-dismissing feedback messages that don't interrupt user workflow — a modern UX pattern missing from most terminal UI frameworks.
Design goals:
[Bindable] properties, routed events, automatic dependency tracking.WindowLayer)In fullscreen mode, TerminalApp wraps the user root into a WindowLayer (unless the root already is a WindowLayer).
That gives the framework a place to host:
Toasts should be layered as:
The most idiomatic way to achieve this in the current architecture is to place the toast overlay inside
WindowLayer.Content (i.e. within the normal visual tree), rather than adding each toast as a window. That ensures:
In practice, this is implemented by a ToastHost that wraps the app content and overlays a toast layer above it
(typically via a ZStack internally).
IAnimatedVisual and TerminalApp.AdvanceAnimations provide tick-based animation. Toasts MAY use this for entrance/exit effects and progress indicators.
Toasts MUST use the existing Theme system for colors (Primary, Success, Warning, Error) and should follow the established style record pattern.
public enum ToastSeverity
{
/// <summary>Neutral informational message.</summary>
Info,
/// <summary>Successful operation feedback.</summary>
Success,
/// <summary>Warning that doesn't block operation.</summary>
Warning,
/// <summary>Error notification (non-fatal).</summary>
Error,
}
public enum ToastPosition
{
/// <summary>Top-right corner (default).</summary>
TopRight,
/// <summary>Top-left corner.</summary>
TopLeft,
/// <summary>Top-center.</summary>
TopCenter,
/// <summary>Bottom-right corner.</summary>
BottomRight,
/// <summary>Bottom-left corner.</summary>
BottomLeft,
/// <summary>Bottom-center.</summary>
BottomCenter,
}
public sealed partial class Toast : Visual
The Toast visual represents a single notification. It is typically created and managed by ToastHost, but MAY be used standalone in custom scenarios.
| Property | Type | Default | Description |
|---|---|---|---|
Title |
Visual? |
null |
Optional title/header visual |
Content |
Visual? |
null |
Main message content (required) |
Severity |
ToastSeverity |
Info |
Determines styling and icon |
Duration |
TimeSpan? |
3 seconds |
Auto-dismiss delay; null = persistent |
ShowIcon |
bool |
true |
Whether to show severity icon |
ShowCloseButton |
bool |
true |
Whether to show manual dismiss button |
ShowProgress |
bool |
false |
Whether to show countdown progress bar |
Action |
Visual? |
null |
Optional action button/link |
[RoutedEvent(RoutingStrategy.Bubble)]
protected virtual void OnDismissed(ToastDismissedEventArgs e);
[RoutedEvent(RoutingStrategy.Bubble)]
protected virtual void OnActionInvoked(ToastActionEventArgs e);
/// <summary>Dismisses the toast programmatically.</summary>
public void Dismiss();
/// <summary>Resets the auto-dismiss timer (e.g., on hover).</summary>
public void ResetTimer();
/// <summary>Pauses the auto-dismiss timer.</summary>
public void PauseTimer();
/// <summary>Resumes the auto-dismiss timer.</summary>
public void ResumeTimer();
public sealed partial class ToastHost : ContentVisual, IAnimatedVisual
The ToastHost is a container that manages the lifecycle and positioning of multiple toasts.
It wraps a normal visual tree via Content (inherited from ContentVisual) and overlays a toast layer above it.
A typical app has a single ToastHost at the root level:
var root = new ToastHost(
new DockLayout()
.Top(new Header().Left("My App"))
.Content(mainContent)
.Bottom(new Footer()));
Terminal.Run(root);
| Property | Type | Default | Description |
|---|---|---|---|
Position |
ToastPosition |
TopRight |
Where toasts appear |
MaxVisible |
int |
5 |
Maximum simultaneous toasts |
Spacing |
int |
1 |
Gap between stacked toasts |
Inset |
Thickness |
(1,1,1,1) |
Inset from viewport edges (applies to the toast overlay) |
DefaultDuration |
TimeSpan |
3 seconds |
Default duration for toasts |
PauseOnHover |
bool |
true |
Pause timer when mouse hovers |
/// <summary>Shows a toast and returns it for further manipulation.</summary>
public Toast Show(Visual content, ToastSeverity severity = ToastSeverity.Info);
/// <summary>Shows a toast with full configuration.</summary>
public Toast Show(Func<Toast> build);
/// <summary>Dismisses all visible toasts.</summary>
public void DismissAll();
/// <summary>Dismisses toasts matching a predicate.</summary>
public void Dismiss(Func<Toast, bool> predicate);
This framework already provides fluent APIs via [Bindable] properties and source generation, so a dedicated
ToastBuilder type is not required.
Prefer:
toastHost.Show(() => new Toast()... )ToastService.Show(() => new Toast()... )For convenience, provide a static service that resolves the ToastHost from the current app context:
public static class ToastService
{
/// <summary>Shows a simple text toast.</summary>
public static Toast? Show(string message, ToastSeverity severity = ToastSeverity.Info);
/// <summary>Shows a toast with configuration.</summary>
public static Toast? Show(Func<Toast> build);
/// <summary>Convenience methods for common severities.</summary>
public static Toast? Info(string message);
public static Toast? Success(string message);
public static Toast? Warning(string message);
public static Toast? Error(string message);
}
The service resolves ToastHost via:
Dispatcher.Current.AttachedApp (fullscreen or inline host)ToastHost (typically there is exactly one at the root)null if no host is found (no-op in inline mode or if host not configured)A toast is composed of:
┌─────────────────────────────────────┐
│ [Icon] [Title................] [X] │ ← Header row (optional)
│ [Content.........................] │ ← Main content
│ [Action button] │ ← Action row (optional)
│ [▓▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░░░░░░░░░░] │ ← Progress bar (optional)
└─────────────────────────────────────┘
Layout SHOULD use existing controls internally:
HStack for header rowVStack for overall structureProgressBar (thin variant) for countdownButton for close/actionToastHost is a content wrapper. Internally it contains two children:
Content (normal app UI)The internal structure is typically:
ToastHost
└─ ZStack
├─ Content
└─ ToastLayer (sized to visible toasts, aligned to corner)
The current hit testing model is:
Visual.HitTest returns this when the point is within bounds and no child hit succeeds.IsHitTestVisible = false disables hit testing for both a visual and its children.That means a fullscreen “overlay” visual that is arranged to the entire viewport will steal pointer input even if it renders nothing in most of that area.
To keep toasts non-intrusive, the toast overlay layer MUST be arranged only to the bounding rectangle of its visible
toasts (plus Inset). It should not cover the full viewport unless the framework adds a “hit-test pass-through”
feature in the future.
The ToastHost MUST:
Content normally.Position + Inset.TopRight/TopLeft/TopCenter: stack downward (newest on top or bottom, configurable)BottomRight/BottomLeft/BottomCenter: stack upwardToastHost MUST ensure the toast overlay is measured so it can compute the toast stack size, but the host should
return size hints compatible with the wrapped Content (the toast overlay must not make the app “bigger”).
In practice:
Content with the provided constraints (normal behavior),SizeHints derived from Content.ToastHost arranges Content to the full available rectangle, then arranges the toast overlay.
The toast overlay should:
DesiredSize from visible toasts,Inset by shrinking the alignment slot before placing the overlay.Avoid positioning toasts using raw terminal coordinates unless strictly needed; prefer the normal layout protocol so the behavior stays consistent across fullscreen and inline hosts.
// Pseudocode sketch (ToastLayer.ArrangeCore)
// - ToastLayer has an arranged rect that is already aligned to the chosen corner.
// - Then it stacks children inside that rect.
Toasts SHOULD have:
MinWidth: style-defined (e.g., 30 cells)MaxWidth: style-defined (e.g., 60 cells) or percentage of viewport[Created] → [Entering] → [Visible] → [Exiting] → [Dismissed]
↑ │
└────────────┘ (timer reset)
State transitions:
Visible statePauseOnHover enabled)ResetTimer() calledDuration is null (persistent toast)Terminal animation is limited, but simple effects are possible:
For v1, instant appear/disappear is acceptable. Animation can be added in v1.1.
When visibleToasts.Count >= MaxVisible:
Option A (recommended): Dismiss oldest — auto-dismiss the oldest toast to make room
Option B: Queue — new toasts wait until a slot opens
Option C: Reject — Show() returns null, toast not displayed
Default SHOULD be Option A for best UX.
Toasts MUST NOT steal focus from the current control. They are informational overlays.
Toast.Focusable = false (default)PauseOnHover)When toast (or its buttons) is focused:
Escape: Dismiss toastEnter/Space on action: Invoke actionEnter/Space on close: DismissGlobal shortcuts (optional, app-configurable):
Ctrl+Shift+N: Focus toast area / cycle through toastsEscape (when toast focused): Dismiss and return focuspublic sealed record ToastStyle : IStyle<ToastStyle>
{
public static StyleKey<ToastStyle> Key { get; }
public static ToastStyle Default { get; }
// Dimensions
public int MinWidth { get; init; } = 30;
public int MaxWidth { get; init; } = 60;
public Thickness Padding { get; init; } = new(1);
public int IconSpacing { get; init; } = 1;
// Container appearance
// Prefer reusing Border/Group styles rather than duplicating border rendering.
public BorderStyle BorderStyle { get; init; } = BorderStyle.Rounded;
// Icons per severity
public Rune InfoIcon { get; init; } = new('ℹ');
public Rune SuccessIcon { get; init; } = new('✓');
public Rune WarningIcon { get; init; } = new('⚠');
public Rune ErrorIcon { get; init; } = new('✗');
// Close button
public Rune CloseIcon { get; init; } = new('×');
// Progress bar variant
public ProgressBarStyle ProgressStyle { get; init; }
// Resolve colors from theme
public Style ResolveStyle(Theme theme, ToastSeverity severity);
public Style ResolveTitleStyle(Theme theme, ToastSeverity severity);
public Style ResolveIconStyle(Theme theme, ToastSeverity severity);
}
| Severity | Background | Border | Icon Color |
|---|---|---|---|
| Info | Surface | Border | Accent |
| Success | Surface | Success | Success |
| Warning | Surface | Warning | Warning |
| Error | Surface | Error | Error |
The exact colors come from Theme semantic tokens.
var root = new ToastHost(
new DockLayout()
.Top(new Header().Left("My App"))
.Content(mainContent)
.Bottom(new Footer()))
.Position(ToastPosition.TopRight)
.MaxVisible(3);
Terminal.Run(root);
// Simple API
ToastService.Success("File saved successfully!");
ToastService.Error("Connection failed. Retrying...");
// Full configuration
ToastService.Show(() => new Toast()
.Title("Update Available")
.Content("Version 2.0 is ready to install.")
.Severity(ToastSeverity.Info)
.Duration(TimeSpan.FromSeconds(10))
.ShowProgress(true)
.Action(new Button("Install Now").Click(StartUpdate)));
// Direct host access
var toast = toastHost.Show("Processing...", ToastSeverity.Info);
// Later...
toast.Dismiss();
// For ongoing operations
var toast = ToastService.Show(() => new Toast()
.Content(new HStack(new Spinner(), "Uploading...").Spacing(1))
.Duration(null)
.ShowCloseButton(false));
// When complete
await UploadAsync();
toast.Dismiss();
ToastService.Success("Upload complete!");
public enum ToastDismissReason
{
/// <summary>Auto-dismissed after duration elapsed.</summary>
Timeout,
/// <summary>User clicked close button.</summary>
UserClosed,
/// <summary>Dismissed programmatically via Dismiss().</summary>
Programmatic,
/// <summary>Dismissed to make room for new toasts.</summary>
Overflow,
/// <summary>Action button invoked (if configured to dismiss).</summary>
ActionInvoked,
}
public sealed class ToastDismissedEventArgs : RoutedEventArgs
{
public ToastDismissReason Reason { get; }
}
public sealed class ToastActionEventArgs : RoutedEventArgs
{
/// <summary>Set to true to prevent auto-dismiss after action.</summary>
public bool KeepOpen { get; set; }
}
Toast notifications are primarily designed for fullscreen apps. In inline mode:
ToastService.Show() returns null if no ToastHost is foundTerminal.WriteMarkupLine() for inline feedbackInlineToastHost could print toasts as flow output (lower priority)ShowProgress provides visual indication of remaining timeinternal sealed class ToastEntry
{
public Toast Visual { get; }
public ToastState State { get; set; }
public long CreatedTick { get; }
public long? DismissAtTick { get; set; }
public bool TimerPaused { get; set; }
}
ToastHost SHOULD implement IAnimatedVisual to:
Toasts render in stack order:
Toast instances for high-frequency scenariosToastService.Success("Deleted", undo: () => Restore()))