using System.ComponentModel; using System.Runtime.CompilerServices; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using StardewValley.Menus; namespace StardewUI.Framework; /// /// Public API for StardewUI, abstracting away all implementation details of views and trees. /// public interface IViewEngine { /// /// Creates an from the StarML stored in a game asset, as provided by a mod via SMAPI or /// Content Patcher. /// /// /// The and can be provided after creation. /// /// The name of the StarML view asset in the content pipeline, e.g. /// Mods/MyMod/Views/MyView. /// An for drawing directly to the of a rendering /// event or other draw handler. IViewDrawable CreateDrawableFromAsset(string assetName); /// /// Creates an from arbitrary markup. /// /// /// /// The and can be provided after creation. /// /// /// Warning: 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. /// /// /// The markup in StarML format. /// An for drawing directly to the of a rendering /// event or other draw handler. IViewDrawable CreateDrawableFromMarkup(string markup); /// /// 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. /// /// /// The menu that is created is the same as the result of . The /// menu is not automatically shown; to show it, use or equivalent. /// /// The name of the StarML view asset in the content pipeline, e.g. /// Mods/MyMod/Views/MyView. /// The context, or "model", for the menu's view, which holds any data-dependent values. /// Note: The type must implement in order for any changes to this data /// to be automatically reflected in the UI. /// A controller object whose is the created menu and whose other /// properties can be used to change menu-level behavior. IMenuController CreateMenuControllerFromAsset(string assetName, object? context = null); /// /// Creates a menu from arbitrary markup, and returns a controller for customizing the menu's behavior. /// /// /// Warning: 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. /// /// The markup in StarML format. /// The context, or "model", for the menu's view, which holds any data-dependent values. /// Note: The type must implement in order for any changes to this data /// to be automatically reflected in the UI. /// A controller object whose is the created menu and whose other /// properties can be used to change menu-level behavior. IMenuController CreateMenuControllerFromMarkup(string markup, object? context = null); /// /// Creates a menu from the StarML stored in a game asset, as provided by a mod via SMAPI or Content Patcher. /// /// /// Does not make the menu active. To show it, use or equivalent. /// /// The name of the StarML view asset in the content pipeline, e.g. /// Mods/MyMod/Views/MyView. /// The context, or "model", for the menu's view, which holds any data-dependent values. /// Note: The type must implement in order for any changes to this data /// to be automatically reflected in the UI. /// A menu object which can be shown using the game's standard menu APIs such as /// . IClickableMenu CreateMenuFromAsset(string assetName, object? context = null); /// /// Creates a menu from arbitrary markup. /// /// /// Warning: 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. /// /// The markup in StarML format. /// The context, or "model", for the menu's view, which holds any data-dependent values. /// Note: The type must implement in order for any changes to this data /// to be automatically reflected in the UI. /// A menu object which can be shown using the game's standard menu APIs such as /// . IClickableMenu CreateMenuFromMarkup(string markup, object? context = null); /// /// Starts monitoring this mod's directory for changes to assets managed by any of the Register methods, e.g. /// views and sprites. /// /// /// /// If the 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 /// sourceDirectory 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 .csproj directory allows hot reloading from the /// source files instead of the deployed mod's files. /// /// /// Hot reload may impact game performance and should normally only be used during development and/or in debug mode. /// /// /// 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. void EnableHotReloading(string? sourceDirectory = null); /// /// Begins preloading assets found in this mod's registered asset directories. /// /// /// /// 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. /// /// /// Must be called after asset registration (, and so on) /// in order to be effective, and must not be called more than once per mod otherwise errors or crashes may occur. /// /// void PreloadAssets(); /// /// Declares that the specified context types will be used as either direct arguments or subproperties in one or /// more subsequent CreateMenu or CreateDrawable APIs, and instructs the framework to begin inspecting /// those types and optimizing for later use. /// /// /// 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. /// /// The types that the mod expects to use as context. void PreloadModels(params Type[] types); /// /// Registers a mod directory to be searched for special-purpose mod data, i.e. that is not either views or sprites. /// /// /// Allowed extensions for files in this folder and their corresponding data types are: /// /// .buttonspritemap.json - ButtonSpriteMapData /// /// /// The prefix for all asset names, excluding the category which is deduced from /// the file extension as described in the remarks. For example, given a value of Mods/MyMod, a file named /// foo.buttonspritemap.json would be referenced in views as @Mods/MyMod/ButtonSpriteMaps/Foo. /// The physical directory where the asset files are located, relative to the mod /// directory. Typically a path such as assets/ui or assets/ui/data. void RegisterCustomData(string assetPrefix, string modDirectory); /// /// Registers a mod directory to be searched for sprite (and corresponding texture/sprite sheet data) assets. /// /// The prefix for all asset names, e.g. Mods/MyMod/Sprites. This can be any value /// but the same prefix must be used in @AssetName view bindings. /// The physical directory where the asset files are located, relative to the mod /// directory. Typically a path such as assets/sprites or assets/ui/sprites. void RegisterSprites(string assetPrefix, string modDirectory); /// /// Registers a mod directory to be searched for view (StarML) assets. Uses the .sml extension. /// /// The prefix for all asset names, e.g. Mods/MyMod/Views. This can be any value /// but the same prefix must be used in include elements and in API calls to create views. /// The physical directory where the asset files are located, relative to the mod /// directory. Typically a path such as assets/views or assets/ui/views. public void RegisterViews(string assetPrefix, string modDirectory); } /// /// Provides methods to update and draw a simple, non-interactive UI component, such as a HUD widget. /// public interface IViewDrawable : IDisposable { /// /// The current size required for the content. /// /// /// Use for calculating the correct position for a , especially for elements /// that should be aligned to the center or right edge of the viewport. /// Vector2 ActualSize { get; } /// /// The context, or "model", for the menu's view, which holds any data-dependent values. /// /// /// The type must implement in order for any changes to this data to be /// automatically reflected on the next . /// object? Context { get; set; } /// /// The maximum size, in pixels, allowed for this content. /// /// /// If no value is specified, then the content is allowed to use the entire . /// Vector2? MaxSize { get; set; } /// /// Draws the current contents. /// /// Target sprite batch. /// Position on the screen or viewport to use as the top-left corner. void Draw(SpriteBatch b, Vector2 position); } /// /// Wrapper for a mod-managed that allows further customization of menu-level properties /// not accessible to StarML or data binding. /// public interface IMenuController : IDisposable { /// /// Event raised after the menu has been closed. /// event Action Closed; /// /// Event raised when the menu is about to close. /// /// /// This has the same lifecycle as . /// event Action Closing; /// /// Gets or sets a function that returns whether or not the menu can be closed. /// /// /// This is equivalent to implementing . /// Func? CanClose { get; set; } /// /// Gets or sets an action that replaces the default menu-close behavior. /// /// /// 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 entirely, so the caller is responsible /// for handling all possible scenarios (e.g. child of another menu, or sub-menu of the title menu). /// Action? CloseAction { get; set; } /// /// Offset from the menu view's top-right edge to draw the close button. /// /// /// Only applies when has been called at least once. /// Vector2 CloseButtonOffset { get; set; } /// /// Whether to automatically close the menu when a mouse click is detected outside the bounds of the menu and any /// floating elements. /// /// /// This setting is primarily intended for submenus and makes them behave more like overlays. /// bool CloseOnOutsideClick { get; set; } /// /// Sound to play when closing the menu. /// string CloseSound { get; set; } /// /// How much the menu should dim the entire screen underneath. /// /// /// 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 0) entirely. /// float DimmingAmount { get; set; } /// /// Gets the menu, which can be opened using , or as a child menu. /// IClickableMenu Menu { get; } /// /// Gets or sets a function that returns the top-left position of the menu. /// /// /// Setting any non-null value will disable the auto-centering functionality, and is equivalent to setting the /// and fields. /// Func? PositionSelector { get; set; } /// /// Removes any cursor attachment previously set by . /// void ClearCursorAttachment(); /// /// Closes the menu. /// /// /// 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 is false. /// void Close(); /// /// Configures the menu to display a close button on the upper-right side. /// /// /// If no 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. /// /// The source image/tile sheet containing the button image. /// The location within the where the image is located, or /// null to draw the entire . /// Scale to apply, if the destination size should be different from the size of the /// . void EnableCloseButton(Texture2D? texture = null, Rectangle? sourceRect = null, float scale = 4f); /// /// Begins displaying a cursor attachment, i.e. a sprite that follows the mouse cursor. /// /// /// The cursor is shown in addition to, not instead of, the normal mouse cursor. /// /// The source image/tile sheet containing the cursor image. /// The location within the where the image is located, or /// null to draw the entire . /// Destination size for the cursor sprite, if different from the size of the /// . /// Offset between the actual mouse position and the top-left corner of the drawn /// cursor sprite. /// Optional tint color to apply to the drawn cursor sprite. void SetCursorAttachment( Texture2D texture, Rectangle? sourceRect = null, Point? size = null, Point? offset = null, Color? tint = null ); /// /// Configures the menu's gutter widths/heights. /// /// /// /// Gutters are areas of the screen that the menu should not occupy. These are typically used with a menu whose root /// view uses for one of its dimensions, and allows /// limiting the max width/height relative to the viewport size. /// /// /// The historical reason for gutters is overscan, however /// they are still commonly used for aesthetic reasons. /// /// /// The gutter width on the left side of the viewport. /// The gutter height at the top of the viewport. /// The gutter width on the right side of the viewport. The default value of -1 specifies /// that the value should be mirrored on the right. /// The gutter height at the bottom of the viewport. The default value of -1 specifies /// that the value should be mirrored on the bottom. void SetGutters(int left, int top, int right = -1, int bottom = -1); } /// /// Extensions for the interface. /// internal static class ViewEngineExtensions { /// /// Starts monitoring this mod's directory for changes to assets managed by any of the 's /// Register 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. /// /// /// /// Callers should normally omit the 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. /// /// /// If detection/sync fails due to an unusual project structure, consider providing an exact path directly to /// instead of using this extension. /// /// /// Hot reload may impact game performance and should normally only be used during development and/or in debug mode. /// /// /// The view engine API. /// Do not pass in this argument, so that can /// provide the correct value on build. 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; } }