// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace Aspire.Tray;
/// <summary>
/// The text styles of the panel. Renderers map each role to the system UI font at the
/// same size in DIPs (SF on macOS, Segoe UI Variable Text on Windows) without embedding a font.
/// </summary>
internal enum PanelFontRole
{
/// <summary>15 semibold: the panel title.</summary>
Title,
/// <summary>13 semibold: AppHost names and the empty-state title.</summary>
Name,
/// <summary>12 regular: status lines, subtitles, banner and button labels.</summary>
Body,
/// <summary>11 semibold: uppercase section headers.</summary>
Section,
/// <summary>12 semibold: the labels of text buttons in confirmation strips.</summary>
Button
}
internal static class PanelFonts
{
public static double Size(PanelFontRole role) => role switch
{
PanelFontRole.Title => 15,
PanelFontRole.Name => 13,
PanelFontRole.Section => 11,
_ => 12
};
public static bool IsSemibold(PanelFontRole role) => role is not PanelFontRole.Body;
}
internal enum PanelTextAlignment
{
Leading,
Center,
Trailing
}
internal enum PanelTruncation
{
End,
Middle
}
/// <summary>One drawing instruction. Commands are drawn in order, painter's algorithm.</summary>
internal abstract record PanelCommand;
internal sealed record FillPathCommand(PanelPath Path, PanelColor Color) : PanelCommand;
/// <summary>Strokes a path with round caps and round joins.</summary>
internal sealed record StrokePathCommand(PanelPath Path, PanelColor Color, double Width) : PanelCommand;
/// <summary>
/// Draws text inside fixed bounds. The layout never measures text: a renderer truncates with
/// an ellipsis to fit. Single-line text is centered vertically from the font's ascent and
/// descent; multi-line text wraps at word boundaries and is top-aligned.
/// </summary>
internal sealed record TextCommand(
PanelRect Bounds,
string Text,
PanelFontRole Font,
PanelColor Color,
PanelTextAlignment Alignment = PanelTextAlignment.Leading,
int MaxLines = 1,
PanelTruncation Truncation = PanelTruncation.End) : PanelCommand;
/// <summary>Draws the full-color Aspire logo scaled into the bounds.</summary>
internal sealed record LogoCommand(PanelRect Bounds) : PanelCommand;
/// <summary>
/// Draws the icon of an installed application, one of <see cref="TrayPanel.OpenInApplications"/>,
/// scaled into the bounds. Hosts look the icon up by <see cref="Application"/> title; when they
/// have none, they draw <see cref="Fallback"/> instead, a generic application glyph.
/// </summary>
internal sealed record AppIconCommand(PanelRect Bounds, string Application, IReadOnlyList<PanelCommand> Fallback) : PanelCommand;
/// <summary>Intersects the clip with a rectangle until the matching <see cref="PopClipCommand"/>.</summary>
internal sealed record PushClipCommand(PanelRect Bounds) : PanelCommand;
internal sealed record PopClipCommand : PanelCommand;
internal enum PanelTargetKind
{
Row,
Dashboard,
Stop,
Start,
More,
ToggleRecent,
ClearRecent,
Documentation,
Settings,
Quit,
/// <summary>The Cancel button of an inline confirmation strip.</summary>
ConfirmCancel,
/// <summary>The destructive button of an inline confirmation strip (Clear, Remove).</summary>
ConfirmAccept,
/// <summary>The back arrow in the header of the Settings view.</summary>
Back,
/// <summary>The "Launch at sign-in" toggle row of the Settings view.</summary>
LaunchAtSignIn,
/// <summary>
/// The "Open in" item of a row's action list, which shows or hides the applications under it.
/// </summary>
OpenInMenu,
/// <summary>
/// An application under "Open in". <see cref="PanelTargetKey.Item"/> indexes
/// <see cref="TrayPanel.OpenInApplications"/>.
/// </summary>
OpenIn,
/// <summary>The "Show in Finder" or "Show in File Explorer" item of a row's action list.</summary>
ShowInFolder,
/// <summary>The "Copy path" item of a row's action list.</summary>
CopyPath,
/// <summary>The "Pin" or "Unpin" item of a row's action list.</summary>
TogglePin,
/// <summary>The "Remove from recent" item of a recent row's action list.</summary>
RemoveRecent
}
/// <summary>The inline confirmation a panel is showing in place of a row or section header.</summary>
internal enum PanelConfirmKind
{
/// <summary>Clear the recent list; replaces the Recent section header.</summary>
ClearRecent,
/// <summary>Remove a recent AppHost whose source file is gone; replaces its row.</summary>
RemoveMissing
}
internal enum PanelSection
{
None,
Running,
Pinned,
Recent
}
/// <summary>
/// Identifies a hit target across scene rebuilds, so hover, press and focus survive a
/// refresh even when rows move. The same AppHost can be listed only once per section.
/// <see cref="Item"/> tells apart targets of the same kind for one row, such as the
/// "Open in" items of its action list.
/// </summary>
internal readonly record struct PanelTargetKey(PanelTargetKind Kind, PanelSection Section, AppHostId? Host, int Item = 0);
/// <summary>
/// How a screen reader presents an element. Hosts map it to the platform role: an AXButton or
/// UIA Button, an AXCheckBox with the AXSwitch subrole or a UIA CheckBox with the Toggle pattern,
/// and AXStaticText or a UIA Text element.
/// </summary>
internal enum PanelAccessibleRole
{
Button,
/// <summary>An on/off control. Its state is <see cref="PanelHitTarget.Checked"/>, not part of its name.</summary>
Switch,
/// <summary>Read-only text: the header status, a notice, a section title or a description.</summary>
Text
}
/// <summary>
/// An interactive region. <see cref="Bounds"/> is where it is drawn and <see cref="HitBounds"/>
/// is the part that is visible through the list clip, which is what pointer input tests.
/// Icon-only buttons carry a <see cref="Tooltip"/>, which hosts show as a native tooltip over
/// <see cref="HitBounds"/>; buttons with a visible label have none.
/// </summary>
internal sealed record PanelHitTarget(
PanelTargetKey Key,
PanelRect Bounds,
PanelRect HitBounds,
string AccessibleName,
bool Enabled,
AppHostMenuItem? Host,
PanelAccessibleRole Role = PanelAccessibleRole.Button,
bool? Checked = null,
string? Tooltip = null)
{
public PanelTargetKind Kind => Key.Kind;
}
/// <summary>
/// Identifies an accessible element across scene rebuilds, so a host can keep the same native
/// element (and a screen reader its cursor on it) while the panel redraws. Targets use their
/// <see cref="PanelTargetKey"/>; text uses a stable label such as "header.status".
/// </summary>
internal readonly record struct PanelAccessibleKey(PanelTargetKey? Target, string? Label)
{
public static PanelAccessibleKey For(PanelTargetKey target) => new(target, null);
public static PanelAccessibleKey For(string label) => new(null, label);
}
/// <summary>
/// One element of the panel's accessibility tree, in reading order. Interactive elements carry
/// their <see cref="Target"/>; text has none. <see cref="VisibleBounds"/> is empty when the
/// element is scrolled out of the list, which hosts report as offscreen.
/// </summary>
internal sealed record PanelAccessibleElement(
PanelAccessibleKey Key,
PanelAccessibleRole Role,
string Name,
PanelRect Bounds,
PanelRect VisibleBounds,
PanelHitTarget? Target = null,
string? Description = null)
{
public bool Enabled => Target?.Enabled ?? true;
public bool? Checked => Target?.Checked;
public bool IsOffscreen => VisibleBounds.IsEmpty;
}
/// <summary>
/// The output of one layout pass: the panel size, what to draw, where to click, and what a
/// screen reader reads, in reading order (header, list, footer).
/// </summary>
internal sealed record TrayPanelScene(
double Width,
double Height,
IReadOnlyList<PanelCommand> Commands,
IReadOnlyList<PanelHitTarget> Targets,
IReadOnlyList<PanelAccessibleElement> Accessibility);
internal enum PanelActionKind
{
OpenDashboard,
/// <summary>Stop the AppHost now. Stopping isn't confirmed, as in the dashboard.</summary>
Stop,
/// <summary>
/// Start a pinned or recent AppHost. When its source file is gone, the host calls
/// <see cref="TrayPanel.ConfirmRemoveMissing"/> instead of showing a dialog.
/// </summary>
Start,
/// <summary>
/// Open the AppHost's folder in <see cref="PanelAction.Application"/>, one of
/// <see cref="TrayPanel.OpenInApplications"/>.
/// </summary>
OpenIn,
/// <summary>Reveal the AppHost file in Finder or File Explorer.</summary>
ShowInFolder,
/// <summary>Copy the path of the folder that contains the AppHost.</summary>
CopyPath,
/// <summary>Pin the AppHost, or unpin it when it is already pinned.</summary>
TogglePin,
/// <summary>Clear the recent list. The panel has already confirmed it inline.</summary>
ClearRecent,
/// <summary>
/// Remove one entry from the recent list: picked from its action list, or confirmed inline
/// after its source file was found missing.
/// </summary>
RemoveRecent,
ToggleRecent,
Documentation,
/// <summary>
/// The gear was clicked. The host reads the startup settings and calls
/// <see cref="TrayPanel.ShowSettings"/> to switch the panel to its Settings view.
/// </summary>
Settings,
/// <summary>Turn launch at sign-in on or off, per <see cref="PanelAction.Enabled"/>.</summary>
SetLaunchAtSignIn,
Quit,
Close
}
/// <summary>
/// What the platform host should do in response to input. <see cref="Anchor"/> is the
/// rectangle in panel coordinates of the control that produced it.
/// </summary>
internal sealed record PanelAction(PanelActionKind Kind, AppHostMenuItem? Host = null, PanelRect Anchor = default)
{
/// <summary>True when the action is for an entry of the recent list rather than a current AppHost.</summary>
public bool Recent { get; init; }
/// <summary>For the Settings toggles: the value the user asked for.</summary>
public bool Enabled { get; init; }
/// <summary>For <see cref="PanelActionKind.OpenIn"/>: the title of the application to open the folder in.</summary>
public string? Application { get; init; }
}
/// <summary>The result of an input event: whether the panel must be redrawn, and an action to run.</summary>
internal readonly record struct PanelResponse(bool Redraw, PanelAction? Action = null)
{
public static PanelResponse None => default;
public static PanelResponse Changed => new(true);
}
internal enum PanelKey
{
Up,
Down,
Tab,
ShiftTab,
Enter,
Space,
Escape,
Home,
End
}