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;
}
}