using System.ComponentModel; using System.Runtime.CompilerServices; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using StardewValley.Menus; namespace StardewUI.Framework; /// <summary> /// Public API for StardewUI, abstracting away all implementation details of views and trees. /// </summary> public interface IViewEngine { /// <summary> /// Creates an <see cref="IViewDrawable"/> from the StarML stored in a game asset, as provided by a mod via SMAPI or /// Content Patcher. /// </summary> /// <remarks> /// The <see cref="IViewDrawable.Context"/> and <see cref="IViewDrawable.MaxSize"/> can be provided after creation. /// </remarks> /// <param name="assetName">The name of the StarML view asset in the content pipeline, e.g. /// <c>Mods/MyMod/Views/MyView</c>.</param> /// <returns>An <see cref="IViewDrawable"/> for drawing directly to the <see cref="SpriteBatch"/> of a rendering /// event or other draw handler.</returns> IViewDrawable CreateDrawableFromAsset(string assetName); /// <summary> /// Creates an <see cref="IViewDrawable"/> from arbitrary markup. /// </summary> /// <remarks> /// <para> /// The <see cref="IViewDrawable.Context"/> and <see cref="IViewDrawable.MaxSize"/> can be provided after creation. /// </para> /// <para> /// <b>Warning:</b> Ad-hoc menus created this way cannot be cached, nor patched by other mods. Most mods should not /// use this API except for testing/experimentation. /// </para> /// </remarks> /// <param name="markup">The markup in StarML format.</param> /// <returns>An <see cref="IViewDrawable"/> for drawing directly to the <see cref="SpriteBatch"/> of a rendering /// event or other draw handler.</returns> IViewDrawable CreateDrawableFromMarkup(string markup); /// <summary> /// Creates a menu from the StarML stored in a game asset, as provided by a mod via SMAPI or Content Patcher, and /// returns a controller for customizing the menu's behavior. /// </summary> /// <remarks> /// The menu that is created is the same as the result of <see cref="CreateMenuFromMarkup(string, object?)"/>. The /// menu is not automatically shown; to show it, use <see cref="Game1.activeClickableMenu"/> or equivalent. /// </remarks> /// <param name="assetName">The name of the StarML view asset in the content pipeline, e.g. /// <c>Mods/MyMod/Views/MyView</c>.</param> /// <param name="context">The context, or "model", for the menu's view, which holds any data-dependent values. /// <b>Note:</b> The type must implement <see cref="INotifyPropertyChanged"/> in order for any changes to this data /// to be automatically reflected in the UI.</param> /// <returns>A controller object whose <see cref="IMenuController.Menu"/> is the created menu and whose other /// properties can be used to change menu-level behavior.</returns> IMenuController CreateMenuControllerFromAsset(string assetName, object? context = null); /// <summary> /// Creates a menu from arbitrary markup, and returns a controller for customizing the menu's behavior. /// </summary> /// <remarks> /// <b>Warning:</b> Ad-hoc menus created this way cannot be cached, nor patched by other mods. Most mods should not /// use this API except for testing/experimentation. /// </remarks> /// <param name="markup">The markup in StarML format.</param> /// <param name="context">The context, or "model", for the menu's view, which holds any data-dependent values. /// <b>Note:</b> The type must implement <see cref="INotifyPropertyChanged"/> in order for any changes to this data /// to be automatically reflected in the UI.</param> /// <returns>A controller object whose <see cref="IMenuController.Menu"/> is the created menu and whose other /// properties can be used to change menu-level behavior.</returns> IMenuController CreateMenuControllerFromMarkup(string markup, object? context = null); /// <summary> /// Creates a menu from the StarML stored in a game asset, as provided by a mod via SMAPI or Content Patcher. /// </summary> /// <remarks> /// Does not make the menu active. To show it, use <see cref="Game1.activeClickableMenu"/> or equivalent. /// </remarks> /// <param name="assetName">The name of the StarML view asset in the content pipeline, e.g. /// <c>Mods/MyMod/Views/MyView</c>.</param> /// <param name="context">The context, or "model", for the menu's view, which holds any data-dependent values. /// <b>Note:</b> The type must implement <see cref="INotifyPropertyChanged"/> in order for any changes to this data /// to be automatically reflected in the UI.</param> /// <returns>A menu object which can be shown using the game's standard menu APIs such as /// <see cref="Game1.activeClickableMenu"/>.</returns> IClickableMenu CreateMenuFromAsset(string assetName, object? context = null); /// <summary> /// Creates a menu from arbitrary markup. /// </summary> /// <remarks> /// <b>Warning:</b> Ad-hoc menus created this way cannot be cached, nor patched by other mods. Most mods should not /// use this API except for testing/experimentation. /// </remarks> /// <param name="markup">The markup in StarML format.</param> /// <param name="context">The context, or "model", for the menu's view, which holds any data-dependent values. /// <b>Note:</b> The type must implement <see cref="INotifyPropertyChanged"/> in order for any changes to this data /// to be automatically reflected in the UI.</param> /// <returns>A menu object which can be shown using the game's standard menu APIs such as /// <see cref="Game1.activeClickableMenu"/>.</returns> IClickableMenu CreateMenuFromMarkup(string markup, object? context = null); /// <summary> /// Starts monitoring this mod's directory for changes to assets managed by any of the <c>Register</c> methods, e.g. /// views and sprites. /// </summary> /// <remarks> /// <para> /// If the <paramref name="sourceDirectory"/> argument is specified, and points to a directory with the same asset /// structure as the mod, then an additional sync will be set up such that files modified in the /// <c>sourceDirectory</c> while the game is running will be copied to the active mod directory and subsequently /// reloaded. In other words, pointing this at the mod's <c>.csproj</c> directory allows hot reloading from the /// source files instead of the deployed mod's files. /// </para> /// <para> /// Hot reload may impact game performance and should normally only be used during development and/or in debug mode. /// </para> /// </remarks> /// <param name="sourceDirectory">Optional source directory to watch and sync changes from. If not specified, or not /// a valid source directory, then hot reload will only pick up changes from within the live mod directory.</param> void EnableHotReloading(string? sourceDirectory = null); /// <summary> /// Begins preloading assets found in this mod's registered asset directories. /// </summary> /// <remarks> /// <para> /// Preloading is performed in the background, and can typically help reduce first-time latency for showing menus or /// drawables, without any noticeable lag in game startup. /// </para> /// <para> /// Must be called after asset registration (<see cref="RegisterViews"/>, <see cref="RegisterSprites"/> and so on) /// in order to be effective, and must not be called more than once per mod otherwise errors or crashes may occur. /// </para> /// </remarks> void PreloadAssets(); /// <summary> /// Declares that the specified context types will be used as either direct arguments or subproperties in one or /// more subsequent <c>CreateMenu</c> or <c>CreateDrawable</c> APIs, and instructs the framework to begin inspecting /// those types and optimizing for later use. /// </summary> /// <remarks> /// Data binding to mod-defined types uses reflection, which can become expensive when loading a very complex menu /// and/or binding to a very complex model for the first time. Preloading can perform this work in the background /// instead of causing latency when opening the menu. /// </remarks> /// <param name="types">The types that the mod expects to use as context.</param> void PreloadModels(params Type[] types); /// <summary> /// Registers a mod directory to be searched for special-purpose mod data, i.e. that is not either views or sprites. /// </summary> /// <remarks> /// Allowed extensions for files in this folder and their corresponding data types are: /// <list type="bullet"> /// <item><c>.buttonspritemap.json</c> - <see href="https://focustense.github.io/StardewUI/reference/stardewui/data/buttonspritemapdata/">ButtonSpriteMapData</see></item> /// </list> /// </remarks> /// <param name="assetPrefix">The prefix for all asset names, <b>excluding</b> the category which is deduced from /// the file extension as described in the remarks. For example, given a value of <c>Mods/MyMod</c>, a file named /// <c>foo.buttonspritemap.json</c> would be referenced in views as <c>@Mods/MyMod/ButtonSpriteMaps/Foo</c>.</param> /// <param name="modDirectory">The physical directory where the asset files are located, relative to the mod /// directory. Typically a path such as <c>assets/ui</c> or <c>assets/ui/data</c>.</param> void RegisterCustomData(string assetPrefix, string modDirectory); /// <summary> /// Registers a mod directory to be searched for sprite (and corresponding texture/sprite sheet data) assets. /// </summary> /// <param name="assetPrefix">The prefix for all asset names, e.g. <c>Mods/MyMod/Sprites</c>. This can be any value /// but the same prefix must be used in <c>@AssetName</c> view bindings.</param> /// <param name="modDirectory">The physical directory where the asset files are located, relative to the mod /// directory. Typically a path such as <c>assets/sprites</c> or <c>assets/ui/sprites</c>.</param> void RegisterSprites(string assetPrefix, string modDirectory); /// <summary> /// Registers a mod directory to be searched for view (StarML) assets. Uses the <c>.sml</c> extension. /// </summary> /// <param name="assetPrefix">The prefix for all asset names, e.g. <c>Mods/MyMod/Views</c>. This can be any value /// but the same prefix must be used in <c>include</c> elements and in API calls to create views.</param> /// <param name="modDirectory">The physical directory where the asset files are located, relative to the mod /// directory. Typically a path such as <c>assets/views</c> or <c>assets/ui/views</c>.</param> public void RegisterViews(string assetPrefix, string modDirectory); } /// <summary> /// Provides methods to update and draw a simple, non-interactive UI component, such as a HUD widget. /// </summary> public interface IViewDrawable : IDisposable { /// <summary> /// The current size required for the content. /// </summary> /// <remarks> /// Use for calculating the correct position for a <see cref="Draw(SpriteBatch, Vector2)"/>, especially for elements /// that should be aligned to the center or right edge of the viewport. /// </remarks> Vector2 ActualSize { get; } /// <summary> /// The context, or "model", for the menu's view, which holds any data-dependent values. /// </summary> /// <remarks> /// The type must implement <see cref="INotifyPropertyChanged"/> in order for any changes to this data to be /// automatically reflected on the next <see cref="Draw(SpriteBatch, Vector2)"/>. /// </remarks> object? Context { get; set; } /// <summary> /// The maximum size, in pixels, allowed for this content. /// </summary> /// <remarks> /// If no value is specified, then the content is allowed to use the entire <see cref="Game1.uiViewport"/>. /// </remarks> Vector2? MaxSize { get; set; } /// <summary> /// Draws the current contents. /// </summary> /// <param name="b">Target sprite batch.</param> /// <param name="position">Position on the screen or viewport to use as the top-left corner.</param> void Draw(SpriteBatch b, Vector2 position); } /// <summary> /// Wrapper for a mod-managed <see cref="IClickableMenu"/> that allows further customization of menu-level properties /// not accessible to StarML or data binding. /// </summary> public interface IMenuController : IDisposable { /// <summary> /// Event raised after the menu has been closed. /// </summary> event Action Closed; /// <summary> /// Event raised when the menu is about to close. /// </summary> /// <remarks> /// This has the same lifecycle as <see cref="IClickableMenu.cleanupBeforeExit"/>. /// </remarks> event Action Closing; /// <summary> /// Gets or sets a function that returns whether or not the menu can be closed. /// </summary> /// <remarks> /// This is equivalent to implementing <see cref="IClickableMenu.readyToClose"/>. /// </remarks> Func<bool>? CanClose { get; set; } /// <summary> /// Gets or sets an action that <b>replaces</b> the default menu-close behavior. /// </summary> /// <remarks> /// Most users should leave this property unset. It is intended for use in unusual contexts, such as replacing the /// mod settings in a Generic Mod Config Menu integration. Setting any non-null value to this property will suppress /// the default behavior of <see cref="IClickableMenu.exitThisMenu(bool)"/> entirely, so the caller is responsible /// for handling all possible scenarios (e.g. child of another menu, or sub-menu of the title menu). /// </remarks> Action? CloseAction { get; set; } /// <summary> /// Offset from the menu view's top-right edge to draw the close button. /// </summary> /// <remarks> /// Only applies when <see cref="EnableCloseButton"/> has been called at least once. /// </remarks> Vector2 CloseButtonOffset { get; set; } /// <summary> /// Whether to automatically close the menu when a mouse click is detected outside the bounds of the menu and any /// floating elements. /// </summary> /// <remarks> /// This setting is primarily intended for submenus and makes them behave more like overlays. /// </remarks> bool CloseOnOutsideClick { get; set; } /// <summary> /// Sound to play when closing the menu. /// </summary> string CloseSound { get; set; } /// <summary> /// How much the menu should dim the entire screen underneath. /// </summary> /// <remarks> /// The default dimming is appropriate for most menus, but if the menu is being drawn as a delegate of some other /// macro-menu, then it can be lowered or removed (set to <c>0</c>) entirely. /// </remarks> float DimmingAmount { get; set; } /// <summary> /// Gets the menu, which can be opened using <see cref="Game1.activeClickableMenu"/>, or as a child menu. /// </summary> IClickableMenu Menu { get; } /// <summary> /// Gets or sets a function that returns the top-left position of the menu. /// </summary> /// <remarks> /// Setting any non-null value will disable the auto-centering functionality, and is equivalent to setting the /// <see cref="IClickableMenu.xPositionOnScreen"/> and <see cref="IClickableMenu.yPositionOnScreen"/> fields. /// </remarks> Func<Point>? PositionSelector { get; set; } /// <summary> /// Removes any cursor attachment previously set by <see cref="SetCursorAttachment"/>. /// </summary> void ClearCursorAttachment(); /// <summary> /// Closes the menu. /// </summary> /// <remarks> /// This method allows programmatic closing of the menu. It performs the same action that would be performed by /// pressing one of the configured menu keys (e.g. ESC), clicking the close button, etc., and follows the same /// rules, i.e. will not allow closing if <see cref="CanClose"/> is <c>false</c>. /// </remarks> void Close(); /// <summary> /// Configures the menu to display a close button on the upper-right side. /// </summary> /// <remarks> /// If no <paramref name="texture"/> is specified, then all other parameters are ignored and the default close /// button sprite is drawn. Otherwise, a custom sprite will be drawn using the specified parameters. /// </remarks> /// <param name="texture">The source image/tile sheet containing the button image.</param> /// <param name="sourceRect">The location within the <paramref name="texture"/> where the image is located, or /// <c>null</c> to draw the entire <paramref name="texture"/>.</param> /// <param name="scale">Scale to apply, if the destination size should be different from the size of the /// <paramref name="sourceRect"/>.</param> void EnableCloseButton(Texture2D? texture = null, Rectangle? sourceRect = null, float scale = 4f); /// <summary> /// Begins displaying a cursor attachment, i.e. a sprite that follows the mouse cursor. /// </summary> /// <remarks> /// The cursor is shown in addition to, not instead of, the normal mouse cursor. /// </remarks> /// <param name="texture">The source image/tile sheet containing the cursor image.</param> /// <param name="sourceRect">The location within the <paramref name="texture"/> where the image is located, or /// <c>null</c> to draw the entire <paramref name="texture"/>.</param> /// <param name="size">Destination size for the cursor sprite, if different from the size of the /// <paramref name="sourceRect"/>.</param> /// <param name="offset">Offset between the actual mouse position and the top-left corner of the drawn /// cursor sprite.</param> /// <param name="tint">Optional tint color to apply to the drawn cursor sprite.</param> void SetCursorAttachment( Texture2D texture, Rectangle? sourceRect = null, Point? size = null, Point? offset = null, Color? tint = null ); /// <summary> /// Configures the menu's gutter widths/heights. /// </summary> /// <remarks> /// <para> /// Gutters are areas of the screen that the menu should not occupy. These are typically used with a menu whose root /// view uses <see cref="Layout.Length.Stretch"/> for one of its <see cref="IView.Layout"/> dimensions, and allows /// limiting the max width/height relative to the viewport size. /// </para> /// <para> /// The historical reason for gutters is <see href="https://en.wikipedia.org/wiki/Overscan">overscan</see>, however /// they are still commonly used for aesthetic reasons. /// </para> /// </remarks> /// <param name="left">The gutter width on the left side of the viewport.</param> /// <param name="top">The gutter height at the top of the viewport.</param> /// <param name="right">The gutter width on the right side of the viewport. The default value of <c>-1</c> specifies /// that the <paramref name="left"/> value should be mirrored on the right.</param> /// <param name="bottom">The gutter height at the bottom of the viewport. The default value of <c>-1</c> specifies /// that the <paramref name="top"/> value should be mirrored on the bottom.</param> void SetGutters(int left, int top, int right = -1, int bottom = -1); } /// <summary> /// Extensions for the <see cref="IViewEngine"/> interface. /// </summary> internal static class ViewEngineExtensions { /// <summary> /// Starts monitoring this mod's directory for changes to assets managed by any of the <see cref="IViewEngine"/>'s /// <c>Register</c> methods, e.g. views and sprites, and attempts to set up an additional sync from the mod's /// project (source) directory to the deployed mod directory so that hot reloads can be initiated from the IDE. /// </summary> /// <remarks> /// <para> /// Callers should normally omit the <paramref name="callerFilePath"/> parameter in their call; this will cause it /// to be replaced at compile time with the actual file path of the caller, and used to automatically detect the /// project path. /// </para> /// <para> /// If detection/sync fails due to an unusual project structure, consider providing an exact path directly to /// <see cref="IViewEngine.EnableHotReloading(string)"/> instead of using this extension. /// </para> /// <para> /// Hot reload may impact game performance and should normally only be used during development and/or in debug mode. /// </para> /// </remarks> /// <param name="viewEngine">The view engine API.</param> /// <param name="callerFilePath">Do not pass in this argument, so that <see cref="CallerFilePathAttribute"/> can /// provide the correct value on build.</param> public static void EnableHotReloadingWithSourceSync( this IViewEngine viewEngine, [CallerFilePath] string? callerFilePath = null ) { viewEngine.EnableHotReloading(FindProjectDirectory(callerFilePath)); } // Attempts to determine the project root directory given the path to an arbitrary source file by walking up the // directory tree until it finds a directory containing a file with .csproj extension. private static string? FindProjectDirectory(string? sourceFilePath) { if (string.IsNullOrEmpty(sourceFilePath)) { return null; } for (var dir = Directory.GetParent(sourceFilePath); dir is not null; dir = dir.Parent) { if (dir.EnumerateFiles("*.csproj").Any()) { return dir.FullName; } } return null; } }