1
0
Fork 0
mirror of https://github.com/Ellpeck/MLEM.git synced 2024-05-29 03:23:37 +02:00

Merge remote-tracking branch 'origin/main'

This commit is contained in:
Ell 2022-10-09 20:07:51 +02:00
commit 3e4c4e566d
48 changed files with 715 additions and 308 deletions

View file

@ -3,10 +3,10 @@
"isRoot": true, "isRoot": true,
"tools": { "tools": {
"cake.tool": { "cake.tool": {
"version": "1.3.0", "version": "2.2.0",
"commands": [ "commands": [
"dotnet-cake" "dotnet-cake"
] ]
} }
} }
} }

View file

@ -111,3 +111,7 @@ resharper_wrap_object_and_collection_initializer_style = wrap_if_long
resharper_xmldoc_attribute_indent = align_by_first_attribute resharper_xmldoc_attribute_indent = align_by_first_attribute
resharper_xmldoc_attribute_style = on_single_line resharper_xmldoc_attribute_style = on_single_line
resharper_xmldoc_pi_attribute_style = on_single_line resharper_xmldoc_pi_attribute_style = on_single_line
[*.md]
trim_trailing_whitespace = false
indent_size = 2

2
.gitignore vendored
View file

@ -4,4 +4,4 @@ obj
packages packages
*.user *.user
tools tools
TestResults TestResults*

View file

@ -19,6 +19,9 @@ Additions
Improvements Improvements
- Improved EnumHelper.GetValues signature to return an array - Improved EnumHelper.GetValues signature to return an array
- Allow using external gesture handling alongside InputHandler through ExternalGestureHandling - Allow using external gesture handling alongside InputHandler through ExternalGestureHandling
- Discard old data when updating a StaticSpriteBatch
- **Drastically improved StaticSpriteBatch batching performance**
- Multi-target net452, making MLEM compatible with MonoGame for consoles
Fixes Fixes
- Fixed TokenizedString handling trailing spaces incorrectly in the last line of non-left aligned text - Fixed TokenizedString handling trailing spaces incorrectly in the last line of non-left aligned text
@ -28,26 +31,45 @@ Fixes
Additions Additions
- Added some extension methods for querying Anchor types - Added some extension methods for querying Anchor types
- Added Element.AutoSizeAddedAbsolute to allow for more granular control of auto-sizing - Added Element.AutoSizeAddedAbsolute to allow for more granular control of auto-sizing
- Added Element.OnAddedToUi and Element.OnRemovedFromUi
- Added ScrollBar.MouseDragScrolling
- Added Panel.ScrollToElement
Improvements Improvements
- Allow elements to auto-adjust their size even when their children are aligned oddly - Allow elements to auto-adjust their size even when their children are aligned oddly
- Close other dropdowns when opening a dropdown - Close other dropdowns when opening a dropdown
- Generified UiMarkdownParser by adding abstract UiParser - Generified UiMarkdownParser by adding abstract UiParser
- Multi-target net452, making MLEM compatible with MonoGame for consoles
Fixes Fixes
- Fixed parents of elements that prevent spill not being notified properly - Fixed parents of elements that prevent spill not being notified properly
- Fixed paragraphs sometimes not updating their position properly when hidden because they're empty - Fixed paragraphs sometimes not updating their position properly when hidden because they're empty
- Fixed panels sometimes not drawing children that came into view when their positions changed unexpectedly - Fixed panels sometimes not drawing children that came into view when their positions changed unexpectedly
- Fixed UiMarkdownParser not parsing formatting in headings and blockquotes - Fixed UiMarkdownParser not parsing formatting in headings and blockquotes
- Fixed Element.OnChildAdded and Element.OnChildRemoved being called for grandchildren when a child is added
- Fixed an exception when trying to force-update the area of an element without a ui system
- Fixed the scroll bar of an empty panel being positioned incorrectly
### MLEM.Data ### MLEM.Data
Additions
- Added data, from, and copy instructions to DataTextureAtlas
Improvements Improvements
- Allow data texture atlas pivots and offsets to be negative - Allow data texture atlas pivots and offsets to be negative
- Made RuntimeTexturePacker restore texture region name and pivot when packing - Made RuntimeTexturePacker restore texture region name and pivot when packing
- Multi-target net452, making MLEM compatible with MonoGame for consoles
Fixes Fixes
- Fixed data texture atlases not allowing most characters in their region names - Fixed data texture atlases not allowing most characters in their region names
## MLEM.Extended
Improvements
- Multi-target net452, making MLEM compatible with MonoGame for consoles
## MLEM.Startup
Improvements
- Multi-target net452, making MLEM compatible with MonoGame for consoles
## 6.0.0 ## 6.0.0
### MLEM ### MLEM

View file

@ -7,15 +7,16 @@
<ApplicationVersion>1</ApplicationVersion> <ApplicationVersion>1</ApplicationVersion>
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion> <ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
<ImplicitUsings>true</ImplicitUsings> <ImplicitUsings>true</ImplicitUsings>
<IsPackable>false</IsPackable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="MonoGame.Content.Builder.Task" Version="3.8.1.303" /> <PackageReference Include="MonoGame.Content.Builder.Task" Version="3.8.1.303" />
<PackageReference Include="MonoGame.Framework.Android" Version="3.8.1.303" /> <PackageReference Include="MonoGame.Framework.Android" Version="3.8.1.303" />
<ProjectReference Include="..\Demos\Demos.csproj" /> <ProjectReference Include="..\Demos\Demos.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<MonoGameContentReference Include="..\Demos\Content\Content.mgcb" /> <MonoGameContentReference Include="..\Demos\Content\Content.mgcb" />
</ItemGroup> </ItemGroup>

View file

@ -7,6 +7,7 @@
<AssemblyName>MLEM Desktop Demos</AssemblyName> <AssemblyName>MLEM Desktop Demos</AssemblyName>
<RootNamespace>Demos.DesktopGL</RootNamespace> <RootNamespace>Demos.DesktopGL</RootNamespace>
<DefineConstants>$(DefineConstants);FNA</DefineConstants> <DefineConstants>$(DefineConstants);FNA</DefineConstants>
<IsPackable>false</IsPackable>
<!-- We still use the MG content builder for ease of compatibility between the MG and FNA demo projects --> <!-- We still use the MG content builder for ease of compatibility between the MG and FNA demo projects -->
<MonoGamePlatform>DesktopGL</MonoGamePlatform> <MonoGamePlatform>DesktopGL</MonoGamePlatform>
</PropertyGroup> </PropertyGroup>

View file

@ -1,31 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<ApplicationIcon>Icon.ico</ApplicationIcon> <ApplicationIcon>Icon.ico</ApplicationIcon>
<AssemblyName>MLEM Desktop Demos</AssemblyName> <AssemblyName>MLEM Desktop Demos</AssemblyName>
<IsPackable>false</IsPackable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="MonoGame.Content.Builder.Task" Version="3.8.1.303" /> <PackageReference Include="MonoGame.Content.Builder.Task" Version="3.8.1.303" />
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.1.303" /> <PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.1.303" />
<ProjectReference Include="..\Demos\Demos.csproj" /> <ProjectReference Include="..\Demos\Demos.csproj" />
<ProjectReference Include="..\MLEM.Startup\MLEM.Startup.csproj" /> <ProjectReference Include="..\MLEM.Startup\MLEM.Startup.csproj" />
<ProjectReference Include="..\MLEM.Ui\MLEM.Ui.csproj" /> <ProjectReference Include="..\MLEM.Ui\MLEM.Ui.csproj" />
<ProjectReference Include="..\MLEM\MLEM.csproj" /> <ProjectReference Include="..\MLEM\MLEM.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<MonoGameContentReference Include="..\Demos\Content\Content.mgcb" /> <MonoGameContentReference Include="..\Demos\Content\Content.mgcb" />
<EmbeddedResource Include="Icon.ico" /> <EmbeddedResource Include="Icon.ico" />
<EmbeddedResource Include="Icon.bmp" /> <EmbeddedResource Include="Icon.bmp" />
</ItemGroup> </ItemGroup>
<Target Name="RestoreDotnetTools" BeforeTargets="Restore"> <Target Name="RestoreDotnetTools" BeforeTargets="Restore">
<Message Text="Restoring dotnet tools" Importance="High" /> <Message Text="Restoring dotnet tools" Importance="High" />
<Exec Command="dotnet tool restore" /> <Exec Command="dotnet tool restore" />

View file

@ -4,6 +4,7 @@
<TargetFramework>netstandard2.0</TargetFramework> <TargetFramework>netstandard2.0</TargetFramework>
<RootNamespace>Demos</RootNamespace> <RootNamespace>Demos</RootNamespace>
<DefineConstants>$(DefineConstants);FNA</DefineConstants> <DefineConstants>$(DefineConstants);FNA</DefineConstants>
<IsPackable>false</IsPackable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View file

@ -2,6 +2,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework> <TargetFramework>netstandard2.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View file

@ -2,6 +2,8 @@
**MLEM Library for Extending MonoGame and FNA** is a set of multipurpose libraries for the game frameworks [MonoGame](https://www.monogame.net/) and [FNA](https://fna-xna.github.io/) that provides abstractions, quality of life improvements and additional features like an extensive ui system and easy input handling. **MLEM Library for Extending MonoGame and FNA** is a set of multipurpose libraries for the game frameworks [MonoGame](https://www.monogame.net/) and [FNA](https://fna-xna.github.io/) that provides abstractions, quality of life improvements and additional features like an extensive ui system and easy input handling.
MLEM is platform-agnostic and multi-targets .NET Standard 2.0, .NET 6.0 and .NET Framework 4.5.2, which makes it compatible with MonoGame and FNA on Desktop, mobile devices and consoles.
# What next? # What next?
- Get it on [NuGet](https://www.nuget.org/packages?q=mlem) - Get it on [NuGet](https://www.nuget.org/packages?q=mlem)
- Get prerelease builds on [BaGet](https://nuget.ellpeck.de/?q=mlem) - Get prerelease builds on [BaGet](https://nuget.ellpeck.de/?q=mlem)

15
FNA.Settings.props Normal file
View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<PlatformTarget>AnyCPU</PlatformTarget>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<!-- include reference assemblies so we can build for old framework versions without having to install them -->
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.2" />
</ItemGroup>
<!-- dummy pack target to allow for this (non-SDK-style) project to be included in dotnet pack -->
<Target Name="Pack" />
</Project>

@ -1 +1 @@
Subproject commit 38849f3ac2887c14b8fa1c69c17468032e5233e1 Subproject commit 6e6fc608bee0d4e7b2944f7c686ca0d28896e195

View file

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
@ -5,10 +6,9 @@ using System.Text.RegularExpressions;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Graphics;
using MLEM.Textures;
#if FNA
using MLEM.Extensions; using MLEM.Extensions;
#endif using MLEM.Misc;
using MLEM.Textures;
namespace MLEM.Data { namespace MLEM.Data {
/// <summary> /// <summary>
@ -18,15 +18,20 @@ namespace MLEM.Data {
/// </para> /// </para>
/// <para> /// <para>
/// Data texture atlases are designed to be easy to write by hand. Because of this, their structure is very simple. /// Data texture atlases are designed to be easy to write by hand. Because of this, their structure is very simple.
/// Each texture region defined in the atlas consists of its name, followed by a set of possible keywords and their arguments, separated by spaces. /// Each texture region defined in the atlas consists of its names (where multiple names can be separated by whitespace), followed by a set of possible instructions and their arguments, also separated by whitespace.
/// The <c>loc</c> keyword defines the <see cref="TextureRegion.Area"/> of the texture region as a rectangle whose origin is its top-left corner. It requires four arguments: x, y, width and height of the rectangle. /// <list type="bullet">
/// The (optional) <c>piv</c> keyword defines the <see cref="TextureRegion.PivotPixels"/> of the texture region. It requires two arguments: x and y. If it is not supplied, the pivot defaults to the top-left corner of the texture region. /// <item><description>The <c>loc</c> (or <c>location</c>) instruction defines the <see cref="TextureRegion.Area"/> of the texture region as a rectangle whose origin is its top-left corner. It requires four arguments: x, y, width and height of the rectangle.</description></item>
/// The (optional) <c>off</c> keyword defines an offset that is added onto the location and pivot of this texture region. This is useful when copying and pasting a previously defined texture region to create a second region that has a constant offset. It requires two arguments: The x and y offset. /// <item><description>The (optional) <c>piv</c> (or <c>pivot</c>) instruction defines the <see cref="TextureRegion.PivotPixels"/> of the texture region. It requires two arguments: x and y. If it is not supplied, the pivot defaults to the top-left corner of the texture region.</description></item>
/// <item><description>The (optional) <c>off</c> (of <c>offset</c>) instruction defines an offset that is added onto the location and pivot of this texture region. This is useful when duplicating a previously defined texture region to create a second region that has a constant offset. It requires two arguments: The x and y offset.</description></item>
/// <item><description>The (optional and repeatable) <c>cpy</c> (or <c>copy</c>) instruction defines an additional texture region that should also be generated from the same data, but with a given offset that will be applied to the location and pivot. It requires three arguments: the copy region's name and the x and y offsets.</description></item>
/// <item><description>The (optional and repeatable) <c>dat</c> (or <c>data</c>) instruction defines a custom data point that can be added to the resulting <see cref="TextureRegion"/>'s <see cref="GenericDataHolder"/> data. It requires two arguments: the data point's name and the data point's value, the latter of which is also stored as a string value.</description></item>
/// <item><description>The (optional) <c>frm</c> (or <c>from</c>) instruction defines a texture region (defined before the current region) whose data should be copied. All data from the region will be copied, but adding additional instructions afterwards modifies the data. It requires one argument: the name of the region whose data to copy. If this instruction is used, the <c>loc</c> instruction is not required.</description></item>
/// </list>
/// </para> /// </para>
/// <example> /// <example>
/// The following entry defines a texture region with the name <c>LongTableRight</c>, whose <see cref="TextureRegion.Area"/> will be a rectangle with X=32, Y=30, Width=64, Height=48, and whose <see cref="TextureRegion.PivotPixels"/> will be a vector with X=80, Y=46. /// The following entry defines a texture region with the names <c>LongTableRight</c> and <c>LongTableUp</c>, whose <see cref="TextureRegion.Area"/> will be a rectangle with X=32, Y=30, Width=64, Height=48, and whose <see cref="TextureRegion.PivotPixels"/> will be a vector with X=80, Y=46.
/// <code> /// <code>
/// LongTableRight /// LongTableRight LongTableUp
/// loc 32 30 64 48 /// loc 32 30 64 48
/// piv 80 46 /// piv 80 46
/// </code> /// </code>
@ -69,6 +74,7 @@ namespace MLEM.Data {
/// <summary> /// <summary>
/// Loads a <see cref="DataTextureAtlas"/> from the given loaded texture and texture data file. /// Loads a <see cref="DataTextureAtlas"/> from the given loaded texture and texture data file.
/// For more information on data texture atlases, see the <see cref="DataTextureAtlas"/> type documentation.
/// </summary> /// </summary>
/// <param name="texture">The texture to use for this data texture atlas</param> /// <param name="texture">The texture to use for this data texture atlas</param>
/// <param name="content">The content manager to use for loading</param> /// <param name="content">The content manager to use for loading</param>
@ -78,45 +84,124 @@ namespace MLEM.Data {
public static DataTextureAtlas LoadAtlasData(TextureRegion texture, ContentManager content, string infoPath, bool pivotRelative = false) { public static DataTextureAtlas LoadAtlasData(TextureRegion texture, ContentManager content, string infoPath, bool pivotRelative = false) {
var info = Path.Combine(content.RootDirectory, infoPath); var info = Path.Combine(content.RootDirectory, infoPath);
string text; string text;
if (Path.IsPathRooted(info)) { try {
text = File.ReadAllText(info); if (Path.IsPathRooted(info)) {
} else { text = File.ReadAllText(info);
using (var reader = new StreamReader(TitleContainer.OpenStream(info))) } else {
text = reader.ReadToEnd(); using (var reader = new StreamReader(TitleContainer.OpenStream(info)))
text = reader.ReadToEnd();
}
} catch (Exception e) {
throw new ContentLoadException($"Couldn't load data texture atlas data from {info}", e);
} }
var atlas = new DataTextureAtlas(texture); var atlas = new DataTextureAtlas(texture);
var words = Regex.Split(text, @"\s+");
// parse each texture region: "<names> loc <u> <v> <w> <h> [piv <px> <py>] [off <ox> <oy>]" var namesOffsets = new List<(string, Vector2)>();
foreach (Match match in Regex.Matches(text, @"(.+)\s+loc\s+([0-9+]+)\s+([0-9+]+)\s+([0-9+]+)\s+([0-9+]+)\s*(?:piv\s+([0-9.+-]+)\s+([0-9.+-]+))?\s*(?:off\s+([0-9.+-]+)\s+([0-9.+-]+))?")) { var customData = new Dictionary<string, string>();
// offset var location = Rectangle.Empty;
var off = !match.Groups[8].Success ? Vector2.Zero : new Vector2( var pivot = Vector2.Zero;
float.Parse(match.Groups[8].Value, CultureInfo.InvariantCulture), var offset = Vector2.Zero;
float.Parse(match.Groups[9].Value, CultureInfo.InvariantCulture)); for (var i = 0; i < words.Length; i++) {
var word = words[i];
try {
switch (word) {
case "loc":
case "location":
location = new Rectangle(
int.Parse(words[i + 1], CultureInfo.InvariantCulture), int.Parse(words[i + 2], CultureInfo.InvariantCulture),
int.Parse(words[i + 3], CultureInfo.InvariantCulture), int.Parse(words[i + 4], CultureInfo.InvariantCulture));
i += 4;
break;
case "piv":
case "pivot":
pivot = new Vector2(
float.Parse(words[i + 1], CultureInfo.InvariantCulture),
float.Parse(words[i + 2], CultureInfo.InvariantCulture));
i += 2;
break;
case "off":
case "offset":
offset = new Vector2(
float.Parse(words[i + 1], CultureInfo.InvariantCulture),
float.Parse(words[i + 2], CultureInfo.InvariantCulture));
i += 2;
break;
case "cpy":
case "copy":
var copyOffset = new Vector2(
float.Parse(words[i + 2], CultureInfo.InvariantCulture),
float.Parse(words[i + 3], CultureInfo.InvariantCulture));
namesOffsets.Add((words[i + 1], copyOffset));
i += 3;
break;
case "dat":
case "data":
customData.Add(words[i + 1], words[i + 2]);
i += 2;
break;
case "frm":
case "from":
var fromRegion = atlas[words[i + 1]];
customData.Clear();
foreach (var key in fromRegion.GetDataKeys())
customData.Add(key, fromRegion.GetData<string>(key));
// our main texture might be a sub-region already, so we have to take that into account
location = fromRegion.Area.OffsetCopy(new Point(-texture.U, -texture.V));
pivot = fromRegion.PivotPixels;
if (pivot != Vector2.Zero && !pivotRelative)
pivot += location.Location.ToVector2();
offset = Vector2.Zero;
i += 1;
break;
default:
// if we have data for the previous regions, they're valid so we add them
AddCurrentRegions();
// location // we're starting a new region (or adding another name for a new region), so clear old data
var loc = new Rectangle( namesOffsets.Add((word.Trim(), Vector2.Zero));
int.Parse(match.Groups[2].Value), int.Parse(match.Groups[3].Value), customData.Clear();
int.Parse(match.Groups[4].Value), int.Parse(match.Groups[5].Value)); location = Rectangle.Empty;
loc.Offset(off.ToPoint()); pivot = Vector2.Zero;
offset = Vector2.Zero;
// pivot break;
var piv = !match.Groups[6].Success ? Vector2.Zero : off + new Vector2( }
float.Parse(match.Groups[6].Value, CultureInfo.InvariantCulture) - (pivotRelative ? 0 : loc.X), } catch (Exception e) {
float.Parse(match.Groups[7].Value, CultureInfo.InvariantCulture) - (pivotRelative ? 0 : loc.Y)); throw new ContentLoadException($"Couldn't parse data texture atlas instruction {word} for region(s) {string.Join(", ", namesOffsets)}", e);
foreach (var name in Regex.Split(match.Groups[1].Value, @"\s")) {
var trimmed = name.Trim();
if (trimmed.Length <= 0)
continue;
var region = new TextureRegion(texture, loc) {
PivotPixels = piv,
Name = trimmed
};
atlas.regions.Add(trimmed, region);
} }
} }
// add the last region that was started on
AddCurrentRegions();
return atlas; return atlas;
void AddCurrentRegions() {
// the location is the only mandatory information, which is why we check it here
if (location == Rectangle.Empty || namesOffsets.Count <= 0)
return;
foreach (var (name, addedOff) in namesOffsets) {
var loc = location;
var piv = pivot;
var off = offset + addedOff;
loc.Offset(off.ToPoint());
if (piv != Vector2.Zero) {
piv += off;
if (!pivotRelative)
piv -= loc.Location.ToVector2();
}
var region = new TextureRegion(texture, loc) {
PivotPixels = piv,
Name = name
};
foreach (var kv in customData)
region.SetData(kv.Key, kv.Value);
atlas.regions.Add(name, region);
}
// we only clear names offsets if the location was valid, otherwise we ignore multiple names for a region
namesOffsets.Clear();
}
} }
} }
@ -128,6 +213,7 @@ namespace MLEM.Data {
/// <summary> /// <summary>
/// Loads a <see cref="DataTextureAtlas"/> from the given texture and texture data file. /// Loads a <see cref="DataTextureAtlas"/> from the given texture and texture data file.
/// For more information on data texture atlases, see the <see cref="DataTextureAtlas"/> type documentation.
/// </summary> /// </summary>
/// <param name="content">The content manager to use for loading</param> /// <param name="content">The content manager to use for loading</param>
/// <param name="texturePath">The path to the texture file</param> /// <param name="texturePath">The path to the texture file</param>

View file

@ -12,6 +12,8 @@ namespace MLEM.Data.Json {
[DataContract] [DataContract]
public class JsonTypeSafeGenericDataHolder : IGenericDataHolder { public class JsonTypeSafeGenericDataHolder : IGenericDataHolder {
private static readonly string[] EmptyStrings = new string[0];
[DataMember(EmitDefaultValue = false)] [DataMember(EmitDefaultValue = false)]
private Dictionary<string, JsonTypeSafeWrapper> data; private Dictionary<string, JsonTypeSafeWrapper> data;
@ -35,9 +37,9 @@ namespace MLEM.Data.Json {
} }
/// <inheritdoc /> /// <inheritdoc />
public IReadOnlyCollection<string> GetDataKeys() { public IEnumerable<string> GetDataKeys() {
if (this.data == null) if (this.data == null)
return Array.Empty<string>(); return JsonTypeSafeGenericDataHolder.EmptyStrings;
return this.data.Keys; return this.data.Keys;
} }

View file

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework> <TargetFrameworks>net452;netstandard2.0;net6.0</TargetFrameworks>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<ProduceReferenceAssembly>true</ProduceReferenceAssembly> <ProduceReferenceAssembly>true</ProduceReferenceAssembly>
<RootNamespace>MLEM.Data</RootNamespace> <RootNamespace>MLEM.Data</RootNamespace>
@ -33,7 +33,8 @@
<PackageReference Include="Newtonsoft.Json.Bson" Version="1.0.2"> <PackageReference Include="Newtonsoft.Json.Bson" Version="1.0.2">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>
<ProjectReference Include="..\FNA\FNA.Core.csproj">
<ProjectReference Include="..\FNA\FNA.csproj">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</ProjectReference> </ProjectReference>
</ItemGroup> </ItemGroup>

View file

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework> <TargetFrameworks>net452;netstandard2.0;net6.0</TargetFrameworks>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<ProduceReferenceAssembly>true</ProduceReferenceAssembly> <ProduceReferenceAssembly>true</ProduceReferenceAssembly>
<NoWarn>NU1701</NoWarn> <NoWarn>NU1701</NoWarn>

View file

@ -1,10 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework> <TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<ProduceReferenceAssembly>true</ProduceReferenceAssembly> <ProduceReferenceAssembly>true</ProduceReferenceAssembly>
<RootNamespace>MLEM.Extended</RootNamespace> <RootNamespace>MLEM.Extended</RootNamespace>
<DefineConstants>$(DefineConstants);FNA</DefineConstants> <DefineConstants>$(DefineConstants);FNA</DefineConstants>
<NoWarn>NU1702</NoWarn>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
@ -22,10 +23,10 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\MLEM\MLEM.FNA.csproj" /> <ProjectReference Include="..\MLEM\MLEM.FNA.csproj" />
<ProjectReference Include="..\FontStashSharp\src\XNA\FontStashSharp.FNA.Core.csproj"> <ProjectReference Include="..\FontStashSharp\src\XNA\FontStashSharp.FNA.csproj">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</ProjectReference> </ProjectReference>
<ProjectReference Include="..\FNA\FNA.Core.csproj"> <ProjectReference Include="..\FNA\FNA.csproj">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</ProjectReference> </ProjectReference>

View file

@ -1,10 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework> <TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<ProduceReferenceAssembly>true</ProduceReferenceAssembly> <ProduceReferenceAssembly>true</ProduceReferenceAssembly>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<Authors>Ellpeck</Authors> <Authors>Ellpeck</Authors>
<Description>MLEM Library for Extending MonoGame extension that ties in with MonoGame.Extended and other MonoGame libraries</Description> <Description>MLEM Library for Extending MonoGame extension that ties in with MonoGame.Extended and other MonoGame libraries</Description>
@ -16,10 +16,10 @@
<PackageIcon>Logo.png</PackageIcon> <PackageIcon>Logo.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile> <PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\MLEM\MLEM.csproj" /> <ProjectReference Include="..\MLEM\MLEM.csproj" />
<PackageReference Include="MonoGame.Extended" Version="3.8.0"> <PackageReference Include="MonoGame.Extended" Version="3.8.0">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>
@ -33,9 +33,9 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="../Media/Logo.png" Pack="true" PackagePath="" /> <None Include="../Media/Logo.png" Pack="true" PackagePath="" />
<None Include="../Docs/index.md" Pack="true" PackagePath="README.md" /> <None Include="../Docs/index.md" Pack="true" PackagePath="README.md" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -16,9 +16,11 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests.FNA", "Tests\Tests.FN
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MLEM.Extended.FNA", "MLEM.Extended\MLEM.Extended.FNA.csproj", "{A5B22930-DF4B-4A62-93ED-A6549F7B666B}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MLEM.Extended.FNA", "MLEM.Extended\MLEM.Extended.FNA.csproj", "{A5B22930-DF4B-4A62-93ED-A6549F7B666B}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FNA.Core", "FNA\FNA.Core.csproj", "{06459F72-CEAA-4B45-B2B1-708FC28D04F8}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FNA", "FNA\FNA.csproj", "{35253CE1-C864-4CD3-8249-4D1319748E8F}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FontStashSharp.FNA.Core", "FontStashSharp\src\XNA\FontStashSharp.FNA.Core.csproj", "{0B410591-3AED-4C82-A07A-516FF493709B}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FontStashSharp.FNA", "FontStashSharp\src\XNA\FontStashSharp.FNA.csproj", "{39249E92-EBF2-4951-A086-AB4951C3CCE1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FNA.Core", "FNA\FNA.Core.csproj", "{458FFA5E-A1C4-4B23-A5D8-259385FEECED}"
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -58,13 +60,17 @@ Global
{A5B22930-DF4B-4A62-93ED-A6549F7B666B}.Debug|Any CPU.Build.0 = Debug|Any CPU {A5B22930-DF4B-4A62-93ED-A6549F7B666B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A5B22930-DF4B-4A62-93ED-A6549F7B666B}.Release|Any CPU.ActiveCfg = Release|Any CPU {A5B22930-DF4B-4A62-93ED-A6549F7B666B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A5B22930-DF4B-4A62-93ED-A6549F7B666B}.Release|Any CPU.Build.0 = Release|Any CPU {A5B22930-DF4B-4A62-93ED-A6549F7B666B}.Release|Any CPU.Build.0 = Release|Any CPU
{06459F72-CEAA-4B45-B2B1-708FC28D04F8}.Debug|Any CPU.ActiveCfg = Debug|x64 {35253CE1-C864-4CD3-8249-4D1319748E8F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{06459F72-CEAA-4B45-B2B1-708FC28D04F8}.Debug|Any CPU.Build.0 = Debug|x64 {35253CE1-C864-4CD3-8249-4D1319748E8F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{06459F72-CEAA-4B45-B2B1-708FC28D04F8}.Release|Any CPU.ActiveCfg = Release|x64 {35253CE1-C864-4CD3-8249-4D1319748E8F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{06459F72-CEAA-4B45-B2B1-708FC28D04F8}.Release|Any CPU.Build.0 = Release|x64 {35253CE1-C864-4CD3-8249-4D1319748E8F}.Release|Any CPU.Build.0 = Release|Any CPU
{0B410591-3AED-4C82-A07A-516FF493709B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {39249E92-EBF2-4951-A086-AB4951C3CCE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0B410591-3AED-4C82-A07A-516FF493709B}.Debug|Any CPU.Build.0 = Debug|Any CPU {39249E92-EBF2-4951-A086-AB4951C3CCE1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0B410591-3AED-4C82-A07A-516FF493709B}.Release|Any CPU.ActiveCfg = Release|Any CPU {39249E92-EBF2-4951-A086-AB4951C3CCE1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0B410591-3AED-4C82-A07A-516FF493709B}.Release|Any CPU.Build.0 = Release|Any CPU {39249E92-EBF2-4951-A086-AB4951C3CCE1}.Release|Any CPU.Build.0 = Release|Any CPU
{458FFA5E-A1C4-4B23-A5D8-259385FEECED}.Debug|Any CPU.ActiveCfg = Debug|x64
{458FFA5E-A1C4-4B23-A5D8-259385FEECED}.Debug|Any CPU.Build.0 = Debug|x64
{458FFA5E-A1C4-4B23-A5D8-259385FEECED}.Release|Any CPU.ActiveCfg = Release|x64
{458FFA5E-A1C4-4B23-A5D8-259385FEECED}.Release|Any CPU.Build.0 = Release|x64
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal

View file

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework> <TargetFrameworks>net452;netstandard2.0;net6.0</TargetFrameworks>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<ProduceReferenceAssembly>true</ProduceReferenceAssembly> <ProduceReferenceAssembly>true</ProduceReferenceAssembly>
<RootNamespace>MLEM.Startup</RootNamespace> <RootNamespace>MLEM.Startup</RootNamespace>
@ -21,11 +21,11 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Coroutine" Version="2.1.3" /> <PackageReference Include="Coroutine" Version="2.1.4" />
<ProjectReference Include="..\MLEM.Ui\MLEM.Ui.FNA.csproj" /> <ProjectReference Include="..\MLEM.Ui\MLEM.Ui.FNA.csproj" />
<ProjectReference Include="..\MLEM\MLEM.FNA.csproj" /> <ProjectReference Include="..\MLEM\MLEM.FNA.csproj" />
<ProjectReference Include="..\FNA\FNA.Core.csproj"> <ProjectReference Include="..\FNA\FNA.csproj">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</ProjectReference> </ProjectReference>
</ItemGroup> </ItemGroup>

View file

@ -1,11 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework> <TargetFrameworks>net452;netstandard2.0;net6.0</TargetFrameworks>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<ProduceReferenceAssembly>true</ProduceReferenceAssembly> <ProduceReferenceAssembly>true</ProduceReferenceAssembly>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<Authors>Ellpeck</Authors> <Authors>Ellpeck</Authors>
<Description>MLEM Library for Extending MonoGame combined with some other useful libraries into a quick Game startup class</Description> <Description>MLEM Library for Extending MonoGame combined with some other useful libraries into a quick Game startup class</Description>
@ -17,17 +17,17 @@
<PackageIcon>Logo.png</PackageIcon> <PackageIcon>Logo.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile> <PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Coroutine" Version="2.1.3" /> <PackageReference Include="Coroutine" Version="2.1.4" />
<ProjectReference Include="..\MLEM.Ui\MLEM.Ui.csproj" /> <ProjectReference Include="..\MLEM.Ui\MLEM.Ui.csproj" />
<ProjectReference Include="..\MLEM\MLEM.csproj" /> <ProjectReference Include="..\MLEM\MLEM.csproj" />
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.0.1641"> <PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.0.1641">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="../Media/Logo.png" Pack="true" PackagePath="" /> <None Include="../Media/Logo.png" Pack="true" PackagePath="" />
<None Include="../Docs/index.md" Pack="true" PackagePath="README.md" /> <None Include="../Docs/index.md" Pack="true" PackagePath="README.md" />

View file

@ -1,14 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework> <TargetFrameworks>net452;netstandard2.0;net6.0</TargetFrameworks>
<IncludeContentInPack>true</IncludeContentInPack> <IncludeContentInPack>true</IncludeContentInPack>
<IncludeBuildOutput>false</IncludeBuildOutput> <IncludeBuildOutput>false</IncludeBuildOutput>
<ContentTargetFolders>content</ContentTargetFolders> <ContentTargetFolders>content</ContentTargetFolders>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>NU5128</NoWarn> <NoWarn>NU5128</NoWarn>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<PackageType>Template</PackageType> <PackageType>Template</PackageType>
<Title>MLEM Templates</Title> <Title>MLEM Templates</Title>
@ -21,7 +21,7 @@
<PackageIcon>Logo.png</PackageIcon> <PackageIcon>Logo.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile> <PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Content Include="content\**\*" Exclude="content\**\.DS_Store;content\**\bin;content\**\obj" /> <Content Include="content\**\*" Exclude="content\**\.DS_Store;content\**\bin;content\**\obj" />
<Compile Remove="**\*" /> <Compile Remove="**\*" />
@ -29,4 +29,4 @@
<None Include="../Docs/index.md" Pack="true" PackagePath="README.md" /> <None Include="../Docs/index.md" Pack="true" PackagePath="README.md" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -408,13 +408,23 @@ namespace MLEM.Ui.Elements {
public GamepadNextElementCallback GetGamepadNextElement; public GamepadNextElementCallback GetGamepadNextElement;
/// <summary> /// <summary>
/// Event that is called when a child is added to this element using <see cref="AddChild{T}"/> /// Event that is called when a child is added to this element using <see cref="AddChild{T}"/>
/// Note that, while this event is only called for immediate children of this element, <see cref="RootElement.OnElementAdded"/> is called for all children and grandchildren.
/// </summary> /// </summary>
public OtherElementCallback OnChildAdded; public OtherElementCallback OnChildAdded;
/// <summary> /// <summary>
/// Event that is called when a child is removed from this element using <see cref="RemoveChild"/> /// Event that is called when a child is removed from this element using <see cref="RemoveChild"/>.
/// Note that, while this event is only called for immediate children of this element, <see cref="RootElement.OnElementRemoved"/> is called for all children and grandchildren.
/// </summary> /// </summary>
public OtherElementCallback OnChildRemoved; public OtherElementCallback OnChildRemoved;
/// <summary> /// <summary>
/// Event that is called when this element is added to a <see cref="UiSystem"/>, that is, when this element's <see cref="System"/> is set to a non-<see langword="null"/> value.
/// </summary>
public GenericCallback OnAddedToUi;
/// <summary>
/// Event that is called when this element is removed from a <see cref="UiSystem"/>, that is, when this element's <see cref="System"/> is set to <see langword="null"/>.
/// </summary>
public GenericCallback OnRemovedFromUi;
/// <summary>
/// Event that is called when this element's <see cref="Dispose"/> method is called, which also happens in <see cref="Finalize"/>. /// Event that is called when this element's <see cref="Dispose"/> method is called, which also happens in <see cref="Finalize"/>.
/// This event is useful for unregistering global event handlers when this object should be destroyed. /// This event is useful for unregistering global event handlers when this object should be destroyed.
/// </summary> /// </summary>
@ -442,7 +452,7 @@ namespace MLEM.Ui.Elements {
/// The <see cref="ChildPaddedArea"/> of this element's <see cref="Parent"/>, or the <see cref="UiSystem.Viewport"/> if this element has no parent. /// The <see cref="ChildPaddedArea"/> of this element's <see cref="Parent"/>, or the <see cref="UiSystem.Viewport"/> if this element has no parent.
/// This value is the one that is passed to <see cref="CalcActualSize"/> during <see cref="ForceUpdateArea"/>. /// This value is the one that is passed to <see cref="CalcActualSize"/> during <see cref="ForceUpdateArea"/>.
/// </summary> /// </summary>
protected RectangleF ParentArea => this.Parent?.ChildPaddedArea ?? (RectangleF) this.system.Viewport; protected RectangleF ParentArea => this.Parent?.ChildPaddedArea ?? (RectangleF) this.System.Viewport;
private readonly List<Element> children = new List<Element>(); private readonly List<Element> children = new List<Element>();
private readonly Stopwatch stopwatch = new Stopwatch(); private readonly Stopwatch stopwatch = new Stopwatch();
@ -497,9 +507,10 @@ namespace MLEM.Ui.Elements {
element.AndChildren(e => { element.AndChildren(e => {
e.Root = this.Root; e.Root = this.Root;
e.System = this.System; e.System = this.System;
e.OnAddedToUi?.Invoke(e);
this.Root?.InvokeOnElementAdded(e); this.Root?.InvokeOnElementAdded(e);
this.OnChildAdded?.Invoke(this, e);
}); });
this.OnChildAdded?.Invoke(this, element);
this.SetSortedChildrenDirty(); this.SetSortedChildrenDirty();
element.SetAreaDirty(); element.SetAreaDirty();
return element; return element;
@ -520,9 +531,10 @@ namespace MLEM.Ui.Elements {
element.AndChildren(e => { element.AndChildren(e => {
e.Root = null; e.Root = null;
e.System = null; e.System = null;
e.OnRemovedFromUi?.Invoke(e);
this.Root?.InvokeOnElementRemoved(e); this.Root?.InvokeOnElementRemoved(e);
this.OnChildRemoved?.Invoke(this, e);
}); });
this.OnChildRemoved?.Invoke(this, element);
this.SetSortedChildrenDirty(); this.SetSortedChildrenDirty();
} }
@ -589,7 +601,7 @@ namespace MLEM.Ui.Elements {
/// </summary> /// </summary>
public virtual void ForceUpdateArea() { public virtual void ForceUpdateArea() {
this.AreaDirty = false; this.AreaDirty = false;
if (this.IsHidden) if (this.IsHidden || this.System == null)
return; return;
// if the parent's area is dirty, it would get updated anyway when querying its ChildPaddedArea, // if the parent's area is dirty, it would get updated anyway when querying its ChildPaddedArea,
// which would cause our ForceUpdateArea code to be run twice, so we only update our parent instead // which would cause our ForceUpdateArea code to be run twice, so we only update our parent instead

View file

@ -84,8 +84,7 @@ namespace MLEM.Ui.Elements {
return; return;
if (e == null || !e.GetParentTree().Contains(this)) if (e == null || !e.GetParentTree().Contains(this))
return; return;
var firstChild = this.Children.First(c => c != this.ScrollBar); this.ScrollToElement(e);
this.ScrollBar.CurrentValue = (e.Area.Center.Y - this.Area.Height / 2 - firstChild.Area.Top) / e.Scale + this.ChildPadding.Value.Height / 2;
}; };
this.AddChild(this.ScrollBar); this.AddChild(this.ScrollBar);
} }
@ -115,15 +114,6 @@ namespace MLEM.Ui.Elements {
this.ScrollSetup(); this.ScrollSetup();
} }
private void ScrollChildren() {
if (!this.scrollOverflow)
return;
// we ignore false grandchildren so that the children of the scroll bar stay in place
foreach (var child in this.GetChildren(c => c != this.ScrollBar, true, true))
child.ScrollOffset.Y = -this.ScrollBar.CurrentValue;
this.relevantChildrenDirty = true;
}
/// <inheritdoc /> /// <inheritdoc />
public override void ForceUpdateSortedChildren() { public override void ForceUpdateSortedChildren() {
base.ForceUpdateSortedChildren(); base.ForceUpdateSortedChildren();
@ -143,26 +133,6 @@ namespace MLEM.Ui.Elements {
base.RemoveChildren(e => e != this.ScrollBar && (condition == null || condition(e))); base.RemoveChildren(e => e != this.ScrollBar && (condition == null || condition(e)));
} }
/// <inheritdoc />
protected override IList<Element> GetRelevantChildren() {
var relevant = base.GetRelevantChildren();
if (this.scrollOverflow) {
if (this.relevantChildrenDirty)
this.ForceUpdateRelevantChildren();
relevant = this.relevantChildren;
}
return relevant;
}
/// <inheritdoc />
protected override void OnChildAreaDirty(Element child, bool grandchild) {
base.OnChildAreaDirty(child, grandchild);
// we only need to scroll when a grandchild changes, since all of our children are forced
// to be auto-anchored and so will automatically propagate their changes up to us
if (grandchild)
this.ScrollChildren();
}
/// <inheritdoc /> /// <inheritdoc />
public override void Draw(GameTime time, SpriteBatch batch, float alpha, SpriteBatchContext context) { public override void Draw(GameTime time, SpriteBatch batch, float alpha, SpriteBatchContext context) {
// draw children onto the render target if we have one // draw children onto the render target if we have one
@ -207,11 +177,32 @@ namespace MLEM.Ui.Elements {
return base.GetElementUnderPos(position); return base.GetElementUnderPos(position);
} }
private RectangleF GetRenderTargetArea() { /// <inheritdoc />
var area = this.ChildPaddedArea; public override void Dispose() {
area.X = this.DisplayArea.X; if (this.renderTarget != null) {
area.Width = this.DisplayArea.Width; this.renderTarget.Dispose();
return area; this.renderTarget = null;
}
base.Dispose();
}
/// <summary>
/// Scrolls this panel's <see cref="ScrollBar"/> to the given <see cref="Element"/> in such a way that its center is positioned in the center of this panel.
/// </summary>
/// <param name="element">The element to scroll to.</param>
public void ScrollToElement(Element element) {
this.ScrollToElement(element.Area.Center.Y);
}
/// <summary>
/// Scrolls this panel's <see cref="ScrollBar"/> to the given <paramref name="elementY"/> coordinate in such a way that the coordinate is positioned in the center of this panel.
/// </summary>
/// <param name="elementY">The y coordinate to scroll to, which should have this element's <see cref="Element.Scale"/> applied.</param>
public void ScrollToElement(float elementY) {
var firstChild = this.Children.FirstOrDefault(c => c != this.ScrollBar);
if (firstChild == null)
return;
this.ScrollBar.CurrentValue = (elementY - this.Area.Height / 2 - firstChild.Area.Top) / this.Scale + this.ChildPadding.Value.Height / 2;
} }
/// <inheritdoc /> /// <inheritdoc />
@ -225,34 +216,56 @@ namespace MLEM.Ui.Elements {
this.SetScrollBarStyle(); this.SetScrollBarStyle();
} }
/// <inheritdoc />
protected override IList<Element> GetRelevantChildren() {
var relevant = base.GetRelevantChildren();
if (this.scrollOverflow) {
if (this.relevantChildrenDirty)
this.ForceUpdateRelevantChildren();
relevant = this.relevantChildren;
}
return relevant;
}
/// <inheritdoc />
protected override void OnChildAreaDirty(Element child, bool grandchild) {
base.OnChildAreaDirty(child, grandchild);
// we only need to scroll when a grandchild changes, since all of our children are forced
// to be auto-anchored and so will automatically propagate their changes up to us
if (grandchild)
this.ScrollChildren();
}
/// <summary> /// <summary>
/// Prepares the panel for auto-scrolling, creating the render target and setting up the scroll bar's maximum value. /// Prepares the panel for auto-scrolling, creating the render target and setting up the scroll bar's maximum value.
/// </summary> /// </summary>
protected virtual void ScrollSetup() { protected virtual void ScrollSetup() {
if (!this.scrollOverflow || this.IsHidden) if (!this.scrollOverflow || this.IsHidden)
return; return;
// if there is only one child, then we have just the scroll bar
if (this.Children.Count == 1)
return;
// the "real" first child is the scroll bar, which we want to ignore float childrenHeight;
var firstChild = this.Children.First(c => c != this.ScrollBar); if (this.Children.Count > 1) {
var lowestChild = this.GetLowestChild(c => c != this.ScrollBar && !c.IsHidden); var firstChild = this.Children.FirstOrDefault(c => c != this.ScrollBar);
var childrenHeight = lowestChild.Area.Bottom - firstChild.Area.Top; var lowestChild = this.GetLowestChild(c => c != this.ScrollBar && !c.IsHidden);
childrenHeight = lowestChild.Area.Bottom - firstChild.Area.Top;
} else {
// if we only have one child (the scroll bar), then the children take up no visual height
childrenHeight = 0;
}
// the max value of the scrollbar is the amount of non-scaled pixels taken up by overflowing components // the max value of the scroll bar is the amount of non-scaled pixels taken up by overflowing components
var scrollBarMax = (childrenHeight - this.ChildPaddedArea.Height) / this.Scale; var scrollBarMax = (childrenHeight - this.ChildPaddedArea.Height) / this.Scale;
if (!this.ScrollBar.MaxValue.Equals(scrollBarMax, Element.Epsilon)) { if (!this.ScrollBar.MaxValue.Equals(scrollBarMax, Element.Epsilon)) {
this.ScrollBar.MaxValue = scrollBarMax; this.ScrollBar.MaxValue = scrollBarMax;
this.relevantChildrenDirty = true; this.relevantChildrenDirty = true;
}
// update child padding based on whether the scroll bar is visible // update child padding based on whether the scroll bar is visible
var childOffset = this.ScrollBar.IsHidden ? 0 : this.ScrollerSize.Value.X + this.ScrollBarOffset; var childOffset = this.ScrollBar.IsHidden ? 0 : this.ScrollerSize.Value.X + this.ScrollBarOffset;
if (!this.scrollBarChildOffset.Equals(childOffset, Element.Epsilon)) { if (!this.scrollBarChildOffset.Equals(childOffset, Element.Epsilon)) {
this.ChildPadding += new Padding(0, -this.scrollBarChildOffset + childOffset, 0, 0); this.ChildPadding += new Padding(0, -this.scrollBarChildOffset + childOffset, 0, 0);
this.scrollBarChildOffset = childOffset; this.scrollBarChildOffset = childOffset;
this.SetAreaDirty(); this.SetAreaDirty();
}
} }
// the scroller height has the same relation to the scroll bar height as the visible area has to the total height of the panel's content // the scroller height has the same relation to the scroll bar height as the visible area has to the total height of the panel's content
@ -271,15 +284,6 @@ namespace MLEM.Ui.Elements {
} }
} }
/// <inheritdoc />
public override void Dispose() {
if (this.renderTarget != null) {
this.renderTarget.Dispose();
this.renderTarget = null;
}
base.Dispose();
}
private void SetScrollBarStyle() { private void SetScrollBarStyle() {
if (this.ScrollBar == null) if (this.ScrollBar == null)
return; return;
@ -306,5 +310,21 @@ namespace MLEM.Ui.Elements {
} }
} }
private RectangleF GetRenderTargetArea() {
var area = this.ChildPaddedArea;
area.X = this.DisplayArea.X;
area.Width = this.DisplayArea.Width;
return area;
}
private void ScrollChildren() {
if (!this.scrollOverflow)
return;
// we ignore false grandchildren so that the children of the scroll bar stay in place
foreach (var child in this.GetChildren(c => c != this.ScrollBar, true, true))
child.ScrollOffset.Y = -this.ScrollBar.CurrentValue;
this.relevantChildrenDirty = true;
}
} }
} }

View file

@ -33,6 +33,16 @@ namespace MLEM.Ui.Elements {
/// The texture of this scroll bar's scroller indicator /// The texture of this scroll bar's scroller indicator
/// </summary> /// </summary>
public StyleProp<NinePatch> ScrollerTexture; public StyleProp<NinePatch> ScrollerTexture;
/// <summary>
/// Whether smooth scrolling should be enabled for this scroll bar.
/// Smooth scrolling causes the <see cref="CurrentValue"/> to change gradually rather than instantly when scrolling.
/// </summary>
public StyleProp<bool> SmoothScrolling;
/// <summary>
/// The factor with which <see cref="SmoothScrolling"/> happens.
/// </summary>
public StyleProp<float> SmoothScrollFactor;
/// <summary> /// <summary>
/// The scroller's width and height /// The scroller's width and height
/// </summary> /// </summary>
@ -86,7 +96,7 @@ namespace MLEM.Ui.Elements {
/// <summary> /// <summary>
/// This property is true while the user scrolls on the scroll bar using the mouse or touch input /// This property is true while the user scrolls on the scroll bar using the mouse or touch input
/// </summary> /// </summary>
public bool IsBeingScrolled => this.isMouseHeld || this.isDragging || this.isTouchHeld; public bool IsBeingScrolled => this.isMouseScrolling || this.isMouseDragging || this.isTouchDragging || this.isTouchScrolling;
/// <summary> /// <summary>
/// This field determines if this scroll bar should automatically be hidden from a <see cref="Panel"/> if there aren't enough children to allow for scrolling. /// This field determines if this scroll bar should automatically be hidden from a <see cref="Panel"/> if there aren't enough children to allow for scrolling.
/// </summary> /// </summary>
@ -99,18 +109,14 @@ namespace MLEM.Ui.Elements {
!this.Horizontal ? 0 : this.CurrentValue / this.maxValue * (this.DisplayArea.Width - this.ScrollerSize.X * this.Scale), !this.Horizontal ? 0 : this.CurrentValue / this.maxValue * (this.DisplayArea.Width - this.ScrollerSize.X * this.Scale),
this.Horizontal ? 0 : this.CurrentValue / this.maxValue * (this.DisplayArea.Height - this.ScrollerSize.Y * this.Scale)); this.Horizontal ? 0 : this.CurrentValue / this.maxValue * (this.DisplayArea.Height - this.ScrollerSize.Y * this.Scale));
/// <summary> /// <summary>
/// Whether smooth scrolling should be enabled for this scroll bar. /// Whether this scroll bar should allow dragging the mouse over its attached <see cref="Panel"/>'s content while holding the left mouse button to scroll, similarly to how scrolling using touch input works.
/// Smooth scrolling causes the <see cref="CurrentValue"/> to change gradually rather than instantly when scrolling.
/// </summary> /// </summary>
public StyleProp<bool> SmoothScrolling; public bool MouseDragScrolling;
/// <summary>
/// The factor with which <see cref="SmoothScrolling"/> happens.
/// </summary>
public StyleProp<float> SmoothScrollFactor;
private bool isMouseHeld; private bool isMouseScrolling;
private bool isDragging; private bool isMouseDragging;
private bool isTouchHeld; private bool isTouchScrolling;
private bool isTouchDragging;
private float maxValue; private float maxValue;
private float scrollAdded; private float scrollAdded;
private float currValue; private float currValue;
@ -141,18 +147,29 @@ namespace MLEM.Ui.Elements {
// MOUSE INPUT // MOUSE INPUT
var moused = this.Controls.MousedElement; var moused = this.Controls.MousedElement;
if (moused == this && this.Input.WasMouseButtonUp(MouseButton.Left) && this.Input.IsMouseButtonDown(MouseButton.Left)) { var wasMouseUp = this.Input.WasMouseButtonUp(MouseButton.Left);
this.isMouseHeld = true; var isMouseDown = this.Input.IsMouseButtonDown(MouseButton.Left);
if (moused == this && wasMouseUp && isMouseDown) {
this.isMouseScrolling = true;
this.scrollStartOffset = this.TransformInverseAll(this.Input.ViewportMousePosition.ToVector2()) - this.ScrollerPosition; this.scrollStartOffset = this.TransformInverseAll(this.Input.ViewportMousePosition.ToVector2()) - this.ScrollerPosition;
} else if (this.isMouseHeld && !this.Input.IsMouseButtonDown(MouseButton.Left)) { } else if (!isMouseDown) {
this.isMouseHeld = false; this.isMouseScrolling = false;
} }
if (this.isMouseHeld) if (this.isMouseScrolling)
this.ScrollToPos(this.TransformInverseAll(this.Input.ViewportMousePosition.ToVector2())); this.ScrollToPos(this.TransformInverseAll(this.Input.ViewportMousePosition.ToVector2()));
if (!this.Horizontal && moused != null && (moused == this.Parent || moused.GetParentTree().Contains(this.Parent))) { if (!this.Horizontal) {
var scroll = this.Input.LastScrollWheel - this.Input.ScrollWheel; if (moused != null && (moused == this.Parent || moused.GetParentTree().Contains(this.Parent))) {
if (scroll != 0) var scroll = this.Input.LastScrollWheel - this.Input.ScrollWheel;
this.CurrentValue += this.StepPerScroll * Math.Sign(scroll); if (scroll != 0)
this.CurrentValue += this.StepPerScroll * Math.Sign(scroll);
if (this.MouseDragScrolling && moused != this && wasMouseUp && isMouseDown)
this.isMouseDragging = true;
}
if (!isMouseDown)
this.isMouseDragging = false;
if (this.isMouseDragging)
this.CurrentValue -= (this.Input.MousePosition.Y - this.Input.LastMousePosition.Y) / this.Scale;
} }
// TOUCH INPUT // TOUCH INPUT
@ -162,29 +179,29 @@ namespace MLEM.Ui.Elements {
// if the element under the drag's start position is on top of the panel, start dragging // if the element under the drag's start position is on top of the panel, start dragging
var touched = this.Parent.GetElementUnderPos(this.TransformInverseAll(drag.Position)); var touched = this.Parent.GetElementUnderPos(this.TransformInverseAll(drag.Position));
if (touched != null && touched != this) if (touched != null && touched != this)
this.isDragging = true; this.isTouchDragging = true;
// if we're dragging at all, then move the scroller // if we're dragging at all, then move the scroller
if (this.isDragging) if (this.isTouchDragging)
this.CurrentValue -= drag.Delta.Y / this.Scale; this.CurrentValue -= drag.Delta.Y / this.Scale;
} else { } else {
this.isDragging = false; this.isTouchDragging = false;
} }
} }
if (this.Input.ViewportTouchState.Count <= 0) { if (this.Input.ViewportTouchState.Count <= 0) {
// if no touch has occured this tick, then reset the variable // if no touch has occured this tick, then reset the variable
this.isTouchHeld = false; this.isTouchScrolling = false;
} else { } else {
foreach (var loc in this.Input.ViewportTouchState) { foreach (var loc in this.Input.ViewportTouchState) {
var pos = this.TransformInverseAll(loc.Position); var pos = this.TransformInverseAll(loc.Position);
// if we just started touching and are on top of the scroller, then we should start scrolling // if we just started touching and are on top of the scroller, then we should start scrolling
if (this.DisplayArea.Contains(pos) && !loc.TryGetPreviousLocation(out _)) { if (this.DisplayArea.Contains(pos) && !loc.TryGetPreviousLocation(out _)) {
this.isTouchHeld = true; this.isTouchScrolling = true;
this.scrollStartOffset = pos - this.ScrollerPosition; this.scrollStartOffset = pos - this.ScrollerPosition;
break; break;
} }
// scroll no matter if we're on the scroller right now // scroll no matter if we're on the scroller right now
if (this.isTouchHeld) if (this.isTouchScrolling)
this.ScrollToPos(pos); this.ScrollToPos(pos);
} }
} }

View file

@ -6,7 +6,9 @@ using MLEM.Input;
using MLEM.Misc; using MLEM.Misc;
using MLEM.Textures; using MLEM.Textures;
using MLEM.Ui.Style; using MLEM.Ui.Style;
#if NETSTANDARD2_0_OR_GREATER || NET6_0_OR_GREATER
using TextCopy; using TextCopy;
#endif
namespace MLEM.Ui.Elements { namespace MLEM.Ui.Elements {
/// <summary> /// <summary>
@ -143,7 +145,11 @@ namespace MLEM.Ui.Elements {
/// <param name="text">The text that the text field should contain by default</param> /// <param name="text">The text that the text field should contain by default</param>
/// <param name="multiline">Whether the text field should support multi-line editing</param> /// <param name="multiline">Whether the text field should support multi-line editing</param>
public TextField(Anchor anchor, Vector2 size, Rule rule = null, GenericFont font = null, string text = null, bool multiline = false) : base(anchor, size) { public TextField(Anchor anchor, Vector2 size, Rule rule = null, GenericFont font = null, string text = null, bool multiline = false) : base(anchor, size) {
this.textInput = new TextInput(null, Vector2.Zero, 1, null, ClipboardService.SetText, ClipboardService.GetText) { this.textInput = new TextInput(null, Vector2.Zero, 1
#if NETSTANDARD2_0_OR_GREATER || NET6_0_OR_GREATER
, null, ClipboardService.SetText, ClipboardService.GetText
#endif
) {
OnTextChange = (i, s) => this.OnTextChange?.Invoke(this, s), OnTextChange = (i, s) => this.OnTextChange?.Invoke(this, s),
InputRule = (i, s) => this.InputRule.Invoke(this, s) InputRule = (i, s) => this.InputRule.Invoke(this, s)
}; };

View file

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework> <TargetFrameworks>net452;netstandard2.0;net6.0</TargetFrameworks>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<ProduceReferenceAssembly>true</ProduceReferenceAssembly> <ProduceReferenceAssembly>true</ProduceReferenceAssembly>
<RootNamespace>MLEM.Ui</RootNamespace> <RootNamespace>MLEM.Ui</RootNamespace>
@ -20,10 +20,10 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="TextCopy" Version="6.1.0" /> <PackageReference Include="TextCopy" Version="6.1.0" Condition="'$(TargetFramework)'!='net452'" />
<ProjectReference Include="..\MLEM\MLEM.FNA.csproj" /> <ProjectReference Include="..\MLEM\MLEM.FNA.csproj" />
<ProjectReference Include="..\FNA\FNA.Core.csproj"> <ProjectReference Include="..\FNA\FNA.csproj">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</ProjectReference> </ProjectReference>
</ItemGroup> </ItemGroup>

View file

@ -1,10 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework> <TargetFrameworks>net452;netstandard2.0;net6.0</TargetFrameworks>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<ProduceReferenceAssembly>true</ProduceReferenceAssembly> <ProduceReferenceAssembly>true</ProduceReferenceAssembly>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<Authors>Ellpeck</Authors> <Authors>Ellpeck</Authors>
<Description>A mouse, keyboard, gamepad and touch ready Ui system for MonoGame that features automatic anchoring, sizing and several ready-to-use element types</Description> <Description>A mouse, keyboard, gamepad and touch ready Ui system for MonoGame that features automatic anchoring, sizing and several ready-to-use element types</Description>
@ -16,18 +16,18 @@
<PackageIcon>Logo.png</PackageIcon> <PackageIcon>Logo.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile> <PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="TextCopy" Version="6.1.0" /> <PackageReference Include="TextCopy" Version="6.1.0" Condition="'$(TargetFramework)'!='net452'" />
<ProjectReference Include="..\MLEM\MLEM.csproj" /> <ProjectReference Include="..\MLEM\MLEM.csproj" />
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.0.1641"> <PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.0.1641">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="../Media/Logo.png" Pack="true" PackagePath="" /> <None Include="../Media/Logo.png" Pack="true" PackagePath="" />
<None Include="../Docs/index.md" Pack="true" PackagePath="README.md" /> <None Include="../Docs/index.md" Pack="true" PackagePath="README.md" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -1,7 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Net.Http;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Graphics;
using MLEM.Formatting; using MLEM.Formatting;
@ -10,6 +9,12 @@ using MLEM.Textures;
using MLEM.Ui.Elements; using MLEM.Ui.Elements;
using MLEM.Ui.Style; using MLEM.Ui.Style;
#if NETSTANDARD2_0_OR_GREATER || NET6_0_OR_GREATER
using System.Net.Http;
#else
using System.Net;
#endif
namespace MLEM.Ui.Parsers { namespace MLEM.Ui.Parsers {
/// <summary> /// <summary>
/// A base class for parsing various types of formatted strings into a set of MLEM.Ui elements with styling for each individual <see cref="ElementType"/>. /// A base class for parsing various types of formatted strings into a set of MLEM.Ui elements with styling for each individual <see cref="ElementType"/>.
@ -148,15 +153,16 @@ namespace MLEM.Ui.Parsers {
try { try {
Texture2D tex; Texture2D tex;
if (path.StartsWith("http")) { if (path.StartsWith("http")) {
using (var client = new HttpClient()) { byte[] src;
using (var src = await client.GetStreamAsync(path)) { #if NETSTANDARD2_0_OR_GREATER || NET6_0_OR_GREATER
using (var memory = new MemoryStream()) { using (var client = new HttpClient())
// download the full stream before passing it to texture src = await client.GetByteArrayAsync(path);
await src.CopyToAsync(memory); #else
tex = Texture2D.FromStream(this.GraphicsDevice, memory); using (var client = new WebClient())
} src = await client.DownloadDataTaskAsync(path);
} #endif
} using (var memory = new MemoryStream(src))
tex = Texture2D.FromStream(this.GraphicsDevice, memory);
} else { } else {
using (var stream = Path.IsPathRooted(path) ? File.OpenRead(path) : TitleContainer.OpenStream(path)) using (var stream = Path.IsPathRooted(path) ? File.OpenRead(path) : TitleContainer.OpenStream(path))
tex = Texture2D.FromStream(this.GraphicsDevice, stream); tex = Texture2D.FromStream(this.GraphicsDevice, stream);

View file

@ -9,6 +9,10 @@ using MLEM.Misc;
using MLEM.Ui.Elements; using MLEM.Ui.Elements;
using MLEM.Ui.Style; using MLEM.Ui.Style;
#if NET452
using MLEM.Extensions;
#endif
namespace MLEM.Ui { namespace MLEM.Ui {
/// <summary> /// <summary>
/// UiControls holds and manages all of the controls for a <see cref="UiSystem"/>. /// UiControls holds and manages all of the controls for a <see cref="UiSystem"/>.

View file

@ -347,6 +347,7 @@ namespace MLEM.Ui {
root.Element.AndChildren(e => { root.Element.AndChildren(e => {
e.Root = root; e.Root = root;
e.System = this; e.System = this;
e.OnAddedToUi?.Invoke(e);
root.InvokeOnElementAdded(e); root.InvokeOnElementAdded(e);
e.SetAreaDirty(); e.SetAreaDirty();
}); });
@ -369,6 +370,7 @@ namespace MLEM.Ui {
root.Element.AndChildren(e => { root.Element.AndChildren(e => {
e.Root = null; e.Root = null;
e.System = null; e.System = null;
e.OnRemovedFromUi?.Invoke(e);
root.InvokeOnElementRemoved(e); root.InvokeOnElementRemoved(e);
e.SetAreaDirty(); e.SetAreaDirty();
}); });
@ -570,7 +572,7 @@ namespace MLEM.Ui {
/// </summary> /// </summary>
public event Element.GenericCallback OnElementAdded; public event Element.GenericCallback OnElementAdded;
/// <summary> /// <summary>
/// Event that is invoked when a <see cref="Element"/> is removed rom this root element of any of its children. /// Event that is invoked when a <see cref="Element"/> is removed rom this root element or any of its children.
/// </summary> /// </summary>
public event Element.GenericCallback OnElementRemoved; public event Element.GenericCallback OnElementRemoved;
/// <summary> /// <summary>

View file

@ -18,8 +18,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MLEM.Data", "MLEM.Data\MLEM
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MLEM.Templates", "MLEM.Templates\MLEM.Templates.csproj", "{C2A2CFED-C9E8-4675-BD66-EFC3DB210977}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MLEM.Templates", "MLEM.Templates\MLEM.Templates.csproj", "{C2A2CFED-C9E8-4675-BD66-EFC3DB210977}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Demos.Android", "Demos.Android\Demos.Android.csproj", "{410C0262-131C-4D0E-910D-D01B4F7143E0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{53D52C3F-67FB-4F32-A794-EAB140BBFC11}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{53D52C3F-67FB-4F32-A794-EAB140BBFC11}"
EndProject EndProject
Global Global
@ -64,10 +62,6 @@ Global
{C2A2CFED-C9E8-4675-BD66-EFC3DB210977}.Debug|Any CPU.Build.0 = Debug|Any CPU {C2A2CFED-C9E8-4675-BD66-EFC3DB210977}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C2A2CFED-C9E8-4675-BD66-EFC3DB210977}.Release|Any CPU.ActiveCfg = Release|Any CPU {C2A2CFED-C9E8-4675-BD66-EFC3DB210977}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C2A2CFED-C9E8-4675-BD66-EFC3DB210977}.Release|Any CPU.Build.0 = Release|Any CPU {C2A2CFED-C9E8-4675-BD66-EFC3DB210977}.Release|Any CPU.Build.0 = Release|Any CPU
{410C0262-131C-4D0E-910D-D01B4F7143E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{410C0262-131C-4D0E-910D-D01B4F7143E0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{410C0262-131C-4D0E-910D-D01B4F7143E0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{410C0262-131C-4D0E-910D-D01B4F7143E0}.Release|Any CPU.Build.0 = Release|Any CPU
{53D52C3F-67FB-4F32-A794-EAB140BBFC11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {53D52C3F-67FB-4F32-A794-EAB140BBFC11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{53D52C3F-67FB-4F32-A794-EAB140BBFC11}.Debug|Any CPU.Build.0 = Debug|Any CPU {53D52C3F-67FB-4F32-A794-EAB140BBFC11}.Debug|Any CPU.Build.0 = Debug|Any CPU
{53D52C3F-67FB-4F32-A794-EAB140BBFC11}.Release|Any CPU.ActiveCfg = Release|Any CPU {53D52C3F-67FB-4F32-A794-EAB140BBFC11}.Release|Any CPU.ActiveCfg = Release|Any CPU

View file

@ -51,5 +51,29 @@ namespace MLEM.Extensions {
return combos; return combos;
} }
#if NET452
/// <summary>Appends a value to the end of the sequence.</summary>
/// <param name="source">A sequence of values.</param>
/// <param name="element">The value to append to <paramref name="source"/>.</param>
/// <typeparam name="T">The type of the elements of <paramref name="source"/>.</typeparam>
/// <returns>A new sequence that ends with <paramref name="element"/>.</returns>
public static IEnumerable<T> Append<T>(this IEnumerable<T> source, T element) {
foreach (var src in source)
yield return src;
yield return element;
}
/// <summary>Prepends a value to the beginning of the sequence.</summary>
/// <param name="source">A sequence of values.</param>
/// <param name="element">The value to prepend to <paramref name="source"/>.</param>
/// <typeparam name="T">The type of the elements of <paramref name="source"/>.</typeparam>
/// <returns>A new sequence that begins with <paramref name="element"/>.</returns>
public static IEnumerable<T> Prepend<T>(this IEnumerable<T> source, T element) {
yield return element;
foreach (var src in source)
yield return src;
}
#endif
} }
} }

View file

@ -10,7 +10,7 @@ using System.IO;
namespace MLEM.Graphics { namespace MLEM.Graphics {
/// <summary> /// <summary>
/// A static sprite batch is a variation of <see cref="SpriteBatch"/> that keeps all batched items in a <see cref="VertexBuffer"/>, allowing for them to be drawn multiple times. /// A static sprite batch is a highly optimized variation of <see cref="SpriteBatch"/> that keeps all batched items in a <see cref="VertexBuffer"/>, allowing for them to be drawn multiple times.
/// To add items to a static sprite batch, use <see cref="BeginBatch"/> to begin batching, <see cref="ClearBatch"/> to clear currently batched items, <c>Add</c> and its various overloads to add batch items, <see cref="Remove"/> to remove them again, and <see cref="EndBatch"/> to end batching. /// To add items to a static sprite batch, use <see cref="BeginBatch"/> to begin batching, <see cref="ClearBatch"/> to clear currently batched items, <c>Add</c> and its various overloads to add batch items, <see cref="Remove"/> to remove them again, and <see cref="EndBatch"/> to end batching.
/// To draw the batched items, call <see cref="Draw"/>. /// To draw the batched items, call <see cref="Draw"/>.
/// </summary> /// </summary>
@ -23,7 +23,7 @@ namespace MLEM.Graphics {
/// <summary> /// <summary>
/// The amount of vertices that are currently batched. /// The amount of vertices that are currently batched.
/// </summary> /// </summary>
public int Vertices => this.items.Count * 4; public int Vertices => this.itemAmount * 4;
/// <summary> /// <summary>
/// The amount of vertex buffers that this static sprite batch has. /// The amount of vertex buffers that this static sprite batch has.
/// To see the amount of buffers that are actually in use, see <see cref="FilledBuffers"/>. /// To see the amount of buffers that are actually in use, see <see cref="FilledBuffers"/>.
@ -41,13 +41,15 @@ namespace MLEM.Graphics {
private readonly GraphicsDevice graphicsDevice; private readonly GraphicsDevice graphicsDevice;
private readonly SpriteEffect spriteEffect; private readonly SpriteEffect spriteEffect;
private readonly List<DynamicVertexBuffer> vertexBuffers = new List<DynamicVertexBuffer>();
private readonly List<VertexBuffer> vertexBuffers = new List<VertexBuffer>();
private readonly List<Texture2D> textures = new List<Texture2D>(); private readonly List<Texture2D> textures = new List<Texture2D>();
private readonly ISet<Item> items = new HashSet<Item>(); private readonly SortedDictionary<float, ItemSet> items = new SortedDictionary<float, ItemSet>();
private SpriteSortMode sortMode = SpriteSortMode.Texture;
private IndexBuffer indices; private IndexBuffer indices;
private bool batching; private bool batching;
private bool batchChanged; private bool batchChanged;
private int itemAmount;
/// <summary> /// <summary>
/// Creates a new static sprite batch with the given <see cref="GraphicsDevice"/> /// Creates a new static sprite batch with the given <see cref="GraphicsDevice"/>
@ -62,10 +64,27 @@ namespace MLEM.Graphics {
/// Begins batching. /// Begins batching.
/// Call this method before calling <c>Add</c> or any of its overloads. /// Call this method before calling <c>Add</c> or any of its overloads.
/// </summary> /// </summary>
/// <param name="sortMode">The drawing order for sprite drawing. When <see langword="null"/> is passed, the last used sort mode will be used again. The initial sort mode is <see cref="SpriteSortMode.Texture"/>. Note that <see cref="SpriteSortMode.Immediate"/> is not supported.</param>
/// <exception cref="InvalidOperationException">Thrown if this batch is currently batching already</exception> /// <exception cref="InvalidOperationException">Thrown if this batch is currently batching already</exception>
public void BeginBatch() { /// <exception cref="ArgumentOutOfRangeException">Thrown if the <paramref name="sortMode"/> is <see cref="SpriteSortMode.Immediate"/>, which is not supported.</exception>
public void BeginBatch(SpriteSortMode? sortMode = null) {
if (this.batching) if (this.batching)
throw new InvalidOperationException("Already batching"); throw new InvalidOperationException("Already batching");
if (sortMode == SpriteSortMode.Immediate)
throw new ArgumentOutOfRangeException(nameof(sortMode), "Cannot use sprite sort mode Immediate for static batching");
// if the sort mode changed (which should be very rare in practice), we have to re-sort our list
if (sortMode != null && this.sortMode != sortMode) {
this.sortMode = sortMode.Value;
if (this.items.Count > 0) {
var tempItems = this.items.Values.SelectMany(s => s.Items).ToArray();
this.items.Clear();
foreach (var item in tempItems)
this.AddItemToSet(item);
this.batchChanged = true;
}
}
this.batching = true; this.batching = true;
} }
@ -73,14 +92,10 @@ namespace MLEM.Graphics {
/// Ends batching. /// Ends batching.
/// Call this method after calling <c>Add</c> or any of its overloads the desired number of times to add batched items. /// Call this method after calling <c>Add</c> or any of its overloads the desired number of times to add batched items.
/// </summary> /// </summary>
/// <param name="sortMode">The drawing order for sprite drawing. <see cref="SpriteSortMode.Texture" /> by default, since it is the best in terms of rendering performance. Note that <see cref="SpriteSortMode.Immediate"/> is not supported.</param>
/// <exception cref="InvalidOperationException">Thrown if this method is called before <see cref="BeginBatch"/> was called.</exception> /// <exception cref="InvalidOperationException">Thrown if this method is called before <see cref="BeginBatch"/> was called.</exception>
/// <exception cref="ArgumentOutOfRangeException">Thrown if the <paramref name="sortMode"/> is <see cref="SpriteSortMode.Immediate"/>, which is not supported.</exception> public void EndBatch() {
public void EndBatch(SpriteSortMode sortMode = SpriteSortMode.Texture) {
if (!this.batching) if (!this.batching)
throw new InvalidOperationException("Not batching"); throw new InvalidOperationException("Not batching");
if (sortMode == SpriteSortMode.Immediate)
throw new ArgumentOutOfRangeException(nameof(sortMode), "Cannot use sprite sort mode Immediate for static batching");
this.batching = false; this.batching = false;
// if we didn't add or remove any batch items, we don't have to recalculate anything // if we didn't add or remove any batch items, we don't have to recalculate anything
@ -90,41 +105,28 @@ namespace MLEM.Graphics {
this.FilledBuffers = 0; this.FilledBuffers = 0;
this.textures.Clear(); this.textures.Clear();
// order items according to the sort mode
IEnumerable<Item> ordered = this.items;
switch (sortMode) {
case SpriteSortMode.Texture:
// SortingKey is internal, but this will do for batching the same texture together
ordered = ordered.OrderBy(i => i.Texture.GetHashCode());
break;
case SpriteSortMode.BackToFront:
ordered = ordered.OrderBy(i => -i.Depth);
break;
case SpriteSortMode.FrontToBack:
ordered = ordered.OrderBy(i => i.Depth);
break;
}
// fill vertex buffers // fill vertex buffers
var dataIndex = 0; var dataIndex = 0;
Texture2D texture = null; Texture2D texture = null;
foreach (var item in ordered) { foreach (var itemSet in this.items.Values) {
// if the texture changes, we also have to start a new buffer! foreach (var item in itemSet.Items) {
if (dataIndex > 0 && (item.Texture != texture || dataIndex >= StaticSpriteBatch.Data.Length)) { // if the texture changes, we also have to start a new buffer!
this.FillBuffer(this.FilledBuffers++, texture, StaticSpriteBatch.Data); if (dataIndex > 0 && (item.Texture != texture || dataIndex >= StaticSpriteBatch.Data.Length)) {
dataIndex = 0; this.FillBuffer(this.FilledBuffers++, texture, StaticSpriteBatch.Data);
dataIndex = 0;
}
StaticSpriteBatch.Data[dataIndex++] = item.TopLeft;
StaticSpriteBatch.Data[dataIndex++] = item.TopRight;
StaticSpriteBatch.Data[dataIndex++] = item.BottomLeft;
StaticSpriteBatch.Data[dataIndex++] = item.BottomRight;
texture = item.Texture;
} }
StaticSpriteBatch.Data[dataIndex++] = item.TopLeft;
StaticSpriteBatch.Data[dataIndex++] = item.TopRight;
StaticSpriteBatch.Data[dataIndex++] = item.BottomLeft;
StaticSpriteBatch.Data[dataIndex++] = item.BottomRight;
texture = item.Texture;
} }
if (dataIndex > 0) if (dataIndex > 0)
this.FillBuffer(this.FilledBuffers++, texture, StaticSpriteBatch.Data); this.FillBuffer(this.FilledBuffers++, texture, StaticSpriteBatch.Data);
// ensure we have enough indices // ensure we have enough indices
var maxItems = Math.Min(this.items.Count, StaticSpriteBatch.MaxBatchItems); var maxItems = Math.Min(this.itemAmount, StaticSpriteBatch.MaxBatchItems);
// each item has 2 triangles which each have 3 indices // each item has 2 triangles which each have 3 indices
if (this.indices == null || this.indices.IndexCount < 6 * maxItems) { if (this.indices == null || this.indices.IndexCount < 6 * maxItems) {
var newIndices = new short[6 * maxItems]; var newIndices = new short[6 * maxItems];
@ -178,7 +180,7 @@ namespace MLEM.Graphics {
for (var i = 0; i < this.FilledBuffers; i++) { for (var i = 0; i < this.FilledBuffers; i++) {
var buffer = this.vertexBuffers[i]; var buffer = this.vertexBuffers[i];
var texture = this.textures[i]; var texture = this.textures[i];
var verts = Math.Min(this.items.Count * 4 - totalIndex, buffer.VertexCount); var verts = Math.Min(this.itemAmount * 4 - totalIndex, buffer.VertexCount);
this.graphicsDevice.SetVertexBuffer(buffer); this.graphicsDevice.SetVertexBuffer(buffer);
if (effect != null) { if (effect != null) {
@ -352,6 +354,21 @@ namespace MLEM.Graphics {
return this.Add(texture, destinationRectangle, null, color); return this.Add(texture, destinationRectangle, null, color);
} }
/// <summary>
/// Adds an item to this batch.
/// Note that this batch needs to currently be batching, meaning <see cref="BeginBatch"/> has to have been called previously.
/// </summary>
/// <param name="item">The item to add.</param>
/// <returns>The added <paramref name="item"/>, for chaining.</returns>
public Item Add(Item item) {
if (!this.batching)
throw new InvalidOperationException("Not batching");
this.AddItemToSet(item);
this.itemAmount++;
this.batchChanged = true;
return item;
}
/// <summary> /// <summary>
/// Removes the given item from this batch. /// Removes the given item from this batch.
/// Note that this batch needs to currently be batching, meaning <see cref="BeginBatch"/> has to have been called previously. /// Note that this batch needs to currently be batching, meaning <see cref="BeginBatch"/> has to have been called previously.
@ -362,7 +379,11 @@ namespace MLEM.Graphics {
public bool Remove(Item item) { public bool Remove(Item item) {
if (!this.batching) if (!this.batching)
throw new InvalidOperationException("Not batching"); throw new InvalidOperationException("Not batching");
if (this.items.Remove(item)) { var key = item.GetSortKey(this.sortMode);
if (this.items.TryGetValue(key, out var itemSet) && itemSet.Remove(item)) {
if (itemSet.IsEmpty)
this.items.Remove(key);
this.itemAmount--;
this.batchChanged = true; this.batchChanged = true;
return true; return true;
} }
@ -380,6 +401,7 @@ namespace MLEM.Graphics {
this.items.Clear(); this.items.Clear();
this.textures.Clear(); this.textures.Clear();
this.FilledBuffers = 0; this.FilledBuffers = 0;
this.itemAmount = 0;
this.batchChanged = true; this.batchChanged = true;
} }
@ -429,18 +451,13 @@ namespace MLEM.Graphics {
} }
private Item Add(Texture2D texture, float depth, VertexPositionColorTexture tl, VertexPositionColorTexture tr, VertexPositionColorTexture bl, VertexPositionColorTexture br) { private Item Add(Texture2D texture, float depth, VertexPositionColorTexture tl, VertexPositionColorTexture tr, VertexPositionColorTexture bl, VertexPositionColorTexture br) {
if (!this.batching) return this.Add(new Item(texture, depth, tl, tr, bl, br));
throw new InvalidOperationException("Not batching");
var item = new Item(texture, depth, tl, tr, bl, br);
this.items.Add(item);
this.batchChanged = true;
return item;
} }
private void FillBuffer(int index, Texture2D texture, VertexPositionColorTexture[] data) { private void FillBuffer(int index, Texture2D texture, VertexPositionColorTexture[] data) {
if (this.vertexBuffers.Count <= index) if (this.vertexBuffers.Count <= index)
this.vertexBuffers.Add(new VertexBuffer(this.graphicsDevice, VertexPositionColorTexture.VertexDeclaration, StaticSpriteBatch.MaxBatchItems * 4, BufferUsage.WriteOnly)); this.vertexBuffers.Add(new DynamicVertexBuffer(this.graphicsDevice, VertexPositionColorTexture.VertexDeclaration, StaticSpriteBatch.MaxBatchItems * 4, BufferUsage.WriteOnly));
this.vertexBuffers[index].SetData(data); this.vertexBuffers[index].SetData(data, 0, data.Length, SetDataOptions.Discard);
this.textures.Insert(index, texture); this.textures.Insert(index, texture);
} }
@ -452,6 +469,15 @@ namespace MLEM.Graphics {
#endif #endif
} }
private void AddItemToSet(Item item) {
var sortKey = item.GetSortKey(this.sortMode);
if (!this.items.TryGetValue(sortKey, out var itemSet)) {
itemSet = new ItemSet();
this.items.Add(sortKey, itemSet);
}
itemSet.Add(item);
}
/// <summary> /// <summary>
/// A struct that represents an item added to a <see cref="StaticSpriteBatch"/> using <c>Add</c> or any of its overloads. /// A struct that represents an item added to a <see cref="StaticSpriteBatch"/> using <c>Add</c> or any of its overloads.
/// An item returned after adding can be removed using <see cref="Remove"/>. /// An item returned after adding can be removed using <see cref="Remove"/>.
@ -474,6 +500,65 @@ namespace MLEM.Graphics {
this.BottomRight = bottomRight; this.BottomRight = bottomRight;
} }
internal float GetSortKey(SpriteSortMode sortMode) {
switch (sortMode) {
case SpriteSortMode.Texture:
return this.Texture.GetHashCode();
case SpriteSortMode.BackToFront:
return -this.Depth;
case SpriteSortMode.FrontToBack:
return this.Depth;
default:
return 0;
}
}
}
private class ItemSet {
public IEnumerable<Item> Items {
get {
if (this.items != null)
return this.items;
if (this.single != null)
return Enumerable.Repeat(this.single, 1);
return Enumerable.Empty<Item>();
}
}
public bool IsEmpty => this.items == null && this.single == null;
private HashSet<Item> items;
private Item single;
public void Add(Item item) {
if (this.items != null) {
this.items.Add(item);
} else if (this.single != null) {
this.items = new HashSet<Item>();
this.items.Add(this.single);
this.items.Add(item);
this.single = null;
} else {
this.single = item;
}
}
public bool Remove(Item item) {
if (this.items != null && this.items.Remove(item)) {
if (this.items.Count <= 1) {
this.single = this.items.Single();
this.items = null;
}
return true;
} else if (this.single == item) {
this.single = null;
return true;
} else {
return false;
}
}
} }
#if FNA #if FNA

View file

@ -20,6 +20,8 @@ namespace MLEM.Input {
#else #else
private static readonly int MaximumGamePadCount = GamePad.MaximumGamePadCount; private static readonly int MaximumGamePadCount = GamePad.MaximumGamePadCount;
#endif #endif
private static readonly TouchLocation[] EmptyTouchLocations = new TouchLocation[0];
private static readonly GenericInput[] EmptyGenericInputs = new GenericInput[0];
/// <summary> /// <summary>
/// Contains all of the gestures that have finished during the last update call. /// Contains all of the gestures that have finished during the last update call.
@ -83,20 +85,20 @@ namespace MLEM.Input {
/// An array of all <see cref="Keys"/>, <see cref="Buttons"/> and <see cref="MouseButton"/> values that are currently down. /// An array of all <see cref="Keys"/>, <see cref="Buttons"/> and <see cref="MouseButton"/> values that are currently down.
/// Additionally, <see cref="TryGetDownTime"/> or <see cref="GetDownTime"/> can be used to determine the amount of time that a given input has been down for. /// Additionally, <see cref="TryGetDownTime"/> or <see cref="GetDownTime"/> can be used to determine the amount of time that a given input has been down for.
/// </summary> /// </summary>
public GenericInput[] InputsDown { get; private set; } = Array.Empty<GenericInput>(); public GenericInput[] InputsDown { get; private set; } = InputHandler.EmptyGenericInputs;
/// <summary> /// <summary>
/// An array of all <see cref="Keys"/>, <see cref="Buttons"/> and <see cref="MouseButton"/> that are currently considered pressed. /// An array of all <see cref="Keys"/>, <see cref="Buttons"/> and <see cref="MouseButton"/> that are currently considered pressed.
/// An input is considered pressed if it was up in the last update, and is up in the current one. /// An input is considered pressed if it was up in the last update, and is up in the current one.
/// </summary> /// </summary>
public GenericInput[] InputsPressed { get; private set; } = Array.Empty<GenericInput>(); public GenericInput[] InputsPressed { get; private set; } = InputHandler.EmptyGenericInputs;
/// <summary> /// <summary>
/// Contains the touch state from the last update call /// Contains the touch state from the last update call
/// </summary> /// </summary>
public TouchCollection LastTouchState { get; private set; } = new TouchCollection(Array.Empty<TouchLocation>()); public TouchCollection LastTouchState { get; private set; } = new TouchCollection(InputHandler.EmptyTouchLocations);
/// <summary> /// <summary>
/// Contains the current touch state /// Contains the current touch state
/// </summary> /// </summary>
public TouchCollection TouchState { get; private set; } = new TouchCollection(Array.Empty<TouchLocation>()); public TouchCollection TouchState { get; private set; } = new TouchCollection(InputHandler.EmptyTouchLocations);
/// <summary> /// <summary>
/// Contains the <see cref="LastTouchState"/>, but with the <see cref="GraphicsDevice.Viewport"/> taken into account. /// Contains the <see cref="LastTouchState"/>, but with the <see cref="GraphicsDevice.Viewport"/> taken into account.
/// </summary> /// </summary>
@ -281,7 +283,7 @@ namespace MLEM.Input {
this.LastTouchState = this.TouchState; this.LastTouchState = this.TouchState;
this.LastViewportTouchState = this.ViewportTouchState; this.LastViewportTouchState = this.ViewportTouchState;
this.TouchState = active ? TouchPanel.GetState() : new TouchCollection(Array.Empty<TouchLocation>()); this.TouchState = active ? TouchPanel.GetState() : new TouchCollection(InputHandler.EmptyTouchLocations);
if (this.TouchState.Count > 0 && this.ViewportOffset != Point.Zero) { if (this.TouchState.Count > 0 && this.ViewportOffset != Point.Zero) {
this.ViewportTouchState = new List<TouchLocation>(); this.ViewportTouchState = new List<TouchLocation>();
foreach (var touch in this.TouchState) { foreach (var touch in this.TouchState) {
@ -302,8 +304,8 @@ namespace MLEM.Input {
} }
if (this.inputsDownAccum.Count <= 0 && this.inputsDown.Count <= 0) { if (this.inputsDownAccum.Count <= 0 && this.inputsDown.Count <= 0) {
this.InputsPressed = Array.Empty<GenericInput>(); this.InputsPressed = InputHandler.EmptyGenericInputs;
this.InputsDown = Array.Empty<GenericInput>(); this.InputsDown = InputHandler.EmptyGenericInputs;
} else { } else {
// handle pressed inputs // handle pressed inputs
var pressed = new List<GenericInput>(); var pressed = new List<GenericInput>();
@ -823,6 +825,7 @@ namespace MLEM.Input {
downTime = DateTime.UtcNow - start; downTime = DateTime.UtcNow - start;
return true; return true;
} }
downTime = default;
return false; return false;
} }
@ -851,6 +854,7 @@ namespace MLEM.Input {
upTime = DateTime.UtcNow - start; upTime = DateTime.UtcNow - start;
return true; return true;
} }
upTime = default;
return false; return false;
} }
@ -879,6 +883,7 @@ namespace MLEM.Input {
lastPressTime = DateTime.UtcNow - start; lastPressTime = DateTime.UtcNow - start;
return true; return true;
} }
lastPressTime = default;
return false; return false;
} }

View file

@ -3,6 +3,10 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Runtime.Serialization; using System.Runtime.Serialization;
#if NET452
using MLEM.Extensions;
#endif
namespace MLEM.Input { namespace MLEM.Input {
/// <summary> /// <summary>
/// A keybind represents a generic way to trigger input. /// A keybind represents a generic way to trigger input.
@ -13,8 +17,10 @@ namespace MLEM.Input {
[DataContract] [DataContract]
public class Keybind : IComparable<Keybind>, IComparable { public class Keybind : IComparable<Keybind>, IComparable {
private static readonly Combination[] EmptyCombinations = new Combination[0];
[DataMember] [DataMember]
private Combination[] combinations = Array.Empty<Combination>(); private Combination[] combinations = Keybind.EmptyCombinations;
/// <summary> /// <summary>
/// Creates a new keybind and adds the given key and modifiers using <see cref="Add(MLEM.Input.GenericInput,MLEM.Input.GenericInput[])"/> /// Creates a new keybind and adds the given key and modifiers using <see cref="Add(MLEM.Input.GenericInput,MLEM.Input.GenericInput[])"/>
@ -77,7 +83,7 @@ namespace MLEM.Input {
/// </summary> /// </summary>
/// <returns>This keybind, for chaining</returns> /// <returns>This keybind, for chaining</returns>
public Keybind Clear() { public Keybind Clear() {
this.combinations = Array.Empty<Combination>(); this.combinations = Keybind.EmptyCombinations;
return this; return this;
} }

View file

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework> <TargetFrameworks>net452;netstandard2.0;net6.0</TargetFrameworks>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<ProduceReferenceAssembly>true</ProduceReferenceAssembly> <ProduceReferenceAssembly>true</ProduceReferenceAssembly>
<RootNamespace>MLEM</RootNamespace> <RootNamespace>MLEM</RootNamespace>
@ -20,9 +20,11 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\FNA\FNA.Core.csproj"> <ProjectReference Include="..\FNA\FNA.csproj">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</ProjectReference> </ProjectReference>
<PackageReference Include="System.ValueTuple" Version="4.5.0" Condition="'$(TargetFramework)'=='net452'" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View file

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework> <TargetFrameworks>net452;netstandard2.0;net6.0</TargetFrameworks>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<ProduceReferenceAssembly>true</ProduceReferenceAssembly> <ProduceReferenceAssembly>true</ProduceReferenceAssembly>
</PropertyGroup> </PropertyGroup>
@ -21,6 +21,8 @@
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.0.1641"> <PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.0.1641">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>
<PackageReference Include="System.ValueTuple" Version="4.5.0" Condition="'$(TargetFramework)'=='net452'" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View file

@ -22,8 +22,12 @@ namespace MLEM.Misc {
/// </summary> /// </summary>
/// <typeparam name="T">The type whose enum to get</typeparam> /// <typeparam name="T">The type whose enum to get</typeparam>
/// <returns>An enumerable of the values of the enum, in declaration order.</returns> /// <returns>An enumerable of the values of the enum, in declaration order.</returns>
public static T[] GetValues<T>() { public static T[] GetValues<T>() where T : struct, Enum {
#if NET6_0_OR_GREATER
return Enum.GetValues<T>();
#else
return (T[]) Enum.GetValues(typeof(T)); return (T[]) Enum.GetValues(typeof(T));
#endif
} }
} }

View file

@ -11,6 +11,8 @@ namespace MLEM.Misc {
[DataContract] [DataContract]
public class GenericDataHolder : IGenericDataHolder { public class GenericDataHolder : IGenericDataHolder {
private static readonly string[] EmptyStrings = new string[0];
[DataMember(EmitDefaultValue = false)] [DataMember(EmitDefaultValue = false)]
private Dictionary<string, object> data; private Dictionary<string, object> data;
@ -34,9 +36,9 @@ namespace MLEM.Misc {
} }
/// <inheritdoc /> /// <inheritdoc />
public IReadOnlyCollection<string> GetDataKeys() { public IEnumerable<string> GetDataKeys() {
if (this.data == null) if (this.data == null)
return Array.Empty<string>(); return GenericDataHolder.EmptyStrings;
return this.data.Keys; return this.data.Keys;
} }
@ -67,7 +69,7 @@ namespace MLEM.Misc {
/// Returns all of the generic data that this object stores. /// Returns all of the generic data that this object stores.
/// </summary> /// </summary>
/// <returns>The generic data on this object</returns> /// <returns>The generic data on this object</returns>
IReadOnlyCollection<string> GetDataKeys(); IEnumerable<string> GetDataKeys();
} }
} }

View file

@ -2,6 +2,8 @@
**MLEM Library for Extending MonoGame and FNA** is a set of multipurpose libraries for the game frameworks [MonoGame](https://www.monogame.net/) and [FNA](https://fna-xna.github.io/) that provides abstractions, quality of life improvements and additional features like an extensive ui system and easy input handling. **MLEM Library for Extending MonoGame and FNA** is a set of multipurpose libraries for the game frameworks [MonoGame](https://www.monogame.net/) and [FNA](https://fna-xna.github.io/) that provides abstractions, quality of life improvements and additional features like an extensive ui system and easy input handling.
MLEM is platform-agnostic and multi-targets .NET Standard 2.0, .NET 6.0 and .NET Framework 4.5.2, which makes it compatible with MonoGame and FNA on Desktop, mobile devices and consoles.
# What next? # What next?
- Get it on [NuGet](https://www.nuget.org/packages?q=mlem) - Get it on [NuGet](https://www.nuget.org/packages?q=mlem)
- Get prerelease builds on [BaGet](https://nuget.ellpeck.de/?q=mlem) - Get prerelease builds on [BaGet](https://nuget.ellpeck.de/?q=mlem)

View file

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using FontStashSharp; using FontStashSharp;
@ -13,6 +14,7 @@ using MLEM.Extensions;
using MLEM.Font; using MLEM.Font;
using MLEM.Formatting; using MLEM.Formatting;
using MLEM.Formatting.Codes; using MLEM.Formatting.Codes;
using MLEM.Graphics;
using MLEM.Input; using MLEM.Input;
using MLEM.Misc; using MLEM.Misc;
using MLEM.Startup; using MLEM.Startup;
@ -351,6 +353,35 @@ public class GameImpl : MlemGame {
}); });
} }
this.UiSystem.Add("WidthTest", widthPanel); this.UiSystem.Add("WidthTest", widthPanel);
var batch = new StaticSpriteBatch(this.GraphicsDevice);
batch.BeginBatch();
var depth = 0F;
var items = new List<StaticSpriteBatch.Item>();
foreach (var r in atlas.Regions)
items.Add(batch.Add(r, new Vector2(50 + r.GetHashCode() % 200, 50), ColorHelper.FromHexRgb(r.GetHashCode()), 0, Vector2.Zero, 1, SpriteEffects.None, depth += 0.0001F));
batch.EndBatch();
var sortMode = SpriteSortMode.Deferred;
this.OnUpdate += (_, _) => {
if (MlemGame.Input.IsPressed(Keys.S)) {
do {
sortMode = (SpriteSortMode) (((int) sortMode + 1) % 5);
} while (sortMode == SpriteSortMode.Immediate);
Console.WriteLine(sortMode);
batch.BeginBatch(sortMode);
batch.EndBatch();
} else {
for (var i = 0; i < items.Count; i++) {
if (MlemGame.Input.IsPressed(Keys.D1 + i)) {
batch.BeginBatch();
if (!batch.Remove(items[i]))
batch.Add(items[i]);
batch.EndBatch();
}
}
}
};
this.OnDraw += (_, _) => batch.Draw(null, SamplerState.PointClamp, null, null, null, Matrix.CreateScale(3));
} }
protected override void DoUpdate(GameTime gameTime) { protected override void DoUpdate(GameTime gameTime) {

View file

@ -1,10 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\MLEM.Data\MLEM.Data.csproj" /> <ProjectReference Include="..\MLEM.Data\MLEM.Data.csproj" />
<ProjectReference Include="..\MLEM.Extended\MLEM.Extended.csproj" /> <ProjectReference Include="..\MLEM.Extended\MLEM.Extended.csproj" />
@ -12,7 +13,7 @@
<ProjectReference Include="..\MLEM.Ui\MLEM.Ui.csproj" /> <ProjectReference Include="..\MLEM.Ui\MLEM.Ui.csproj" />
<ProjectReference Include="..\MLEM\MLEM.csproj" /> <ProjectReference Include="..\MLEM\MLEM.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="MonoGame.Content.Builder.Task" Version="3.8.1.303" /> <PackageReference Include="MonoGame.Content.Builder.Task" Version="3.8.1.303" />
<PackageReference Include="MonoGame.Extended.Content.Pipeline" Version="3.8.0" /> <PackageReference Include="MonoGame.Extended.Content.Pipeline" Version="3.8.0" />
@ -21,7 +22,7 @@
<PackageReference Include="FontStashSharp.MonoGame" Version="1.2.4" /> <PackageReference Include="FontStashSharp.MonoGame" Version="1.2.4" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup> </ItemGroup>
<Target Name="RestoreDotnetTools" BeforeTargets="Restore"> <Target Name="RestoreDotnetTools" BeforeTargets="Restore">
<Message Text="Restoring dotnet tools" Importance="High" /> <Message Text="Restoring dotnet tools" Importance="High" />
<Exec Command="dotnet tool restore" /> <Exec Command="dotnet tool restore" />

View file

@ -13,11 +13,21 @@ TestRegionNegativePivot
loc 0 32 +16 16 loc 0 32 +16 16
piv -32 +46 piv -32 +46
DataTest
loc 0 0 16 16
dat DataPoint1 ThisIsSomeData
dat DataPoint2 3.5
dat DataPoint3 ---
LongTableUp LongTableUp
loc 0 32 64 48 piv 16 48 loc 0 32 64 48
piv 16 48 copy Copy1 16 0
cpy Copy2 32 4
Copy3 from
LongTableUp off 2 4
LongTableRight LongTableDown LongTableLeft LongTableRight LongTableDown LongTableLeft
loc 32 30 64 48 location 32 30 64 48
piv 80 46 piv 80 46
off 32 2 offset 32 2

View file

@ -9,26 +9,51 @@ namespace Tests {
public class TestDataTextureAtlas { public class TestDataTextureAtlas {
[Test] [Test]
public void Test() { public void Test([Values(0, 4)] int regionX, [Values(0, 4)] int regionY) {
using var game = TestGame.Create(); using var game = TestGame.Create();
using var texture = new Texture2D(game.GraphicsDevice, 1, 1); using var texture = new Texture2D(game.GraphicsDevice, 1, 1);
var atlas = DataTextureAtlas.LoadAtlasData(new TextureRegion(texture), game.RawContent, "Texture.atlas"); var region = new TextureRegion(texture, regionX, regionY, 1, 1);
Assert.AreEqual(atlas.Regions.Count(), 8); var atlas = DataTextureAtlas.LoadAtlasData(region, game.RawContent, "Texture.atlas");
Assert.AreEqual(12, atlas.Regions.Count());
// no pivot
var plant = atlas["Plant"];
Assert.AreEqual(plant.Area, new Rectangle(96 + regionX, 0 + regionY, 16, 32));
Assert.AreEqual(plant.PivotPixels, Vector2.Zero);
// no added offset // no added offset
var table = atlas["LongTableUp"]; var table = atlas["LongTableUp"];
Assert.AreEqual(table.Area, new Rectangle(0, 32, 64, 48)); Assert.AreEqual(table.Area, new Rectangle(0 + regionX, 32 + regionY, 64, 48));
Assert.AreEqual(table.PivotPixels, new Vector2(16, 48 - 32)); Assert.AreEqual(table.PivotPixels, new Vector2(16, 48 - 32));
// added offset // added offset
var table2 = atlas["LongTableLeft"]; var table2 = atlas["LongTableDown"];
Assert.AreEqual(table2.Area, new Rectangle(64, 32, 64, 48)); Assert.AreEqual(table2.Area, new Rectangle(64 + regionX, 32 + regionY, 64, 48));
Assert.AreEqual(table2.PivotPixels, new Vector2(112 - 64, 48 - 32)); Assert.AreEqual(table2.PivotPixels, new Vector2(112 - 64, 48 - 32));
// negative pivot // negative pivot
var negativePivot = atlas["TestRegionNegativePivot"]; var negativePivot = atlas["TestRegionNegativePivot"];
Assert.AreEqual(negativePivot.Area, new Rectangle(0, 32, 16, 16)); Assert.AreEqual(negativePivot.Area, new Rectangle(0 + regionX, 32 + regionY, 16, 16));
Assert.AreEqual(negativePivot.PivotPixels, new Vector2(-32, 46 - 32)); Assert.AreEqual(negativePivot.PivotPixels, new Vector2(-32, 46 - 32));
// cpy (pivot pixels should be identical to LongTableUp because they're region-internal)
var copy1 = atlas["Copy1"];
Assert.AreEqual(copy1.Area, new Rectangle(0 + 16 + regionX, 32 + regionY, 64, 48));
Assert.AreEqual(copy1.PivotPixels, new Vector2(16, 48 - 32));
var copy2 = atlas["Copy2"];
Assert.AreEqual(copy2.Area, new Rectangle(0 + 32 + regionX, 32 + 4 + regionY, 64, 48));
Assert.AreEqual(copy2.PivotPixels, new Vector2(16, 48 - 32));
// frm
var copy3 = atlas["Copy3"];
Assert.AreEqual(copy3.Area, new Rectangle(0 + 2 + regionX, 32 + 4 + regionY, 64, 48));
Assert.AreEqual(copy3.PivotPixels, new Vector2(16, 48 - 32));
// data
var data = atlas["DataTest"];
Assert.AreEqual("ThisIsSomeData", data.GetData<string>("DataPoint1"));
Assert.AreEqual("3.5", data.GetData<string>("DataPoint2"));
Assert.AreEqual("---", data.GetData<string>("DataPoint3"));
} }
} }

View file

@ -1,9 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<VSTestLogger>nunit</VSTestLogger> <VSTestLogger>nunit</VSTestLogger>
<VSTestResultsDirectory>TestResults.FNA</VSTestResultsDirectory>
<RootNamespace>Tests</RootNamespace> <RootNamespace>Tests</RootNamespace>
<DefineConstants>$(DefineConstants);FNA</DefineConstants> <DefineConstants>$(DefineConstants);FNA</DefineConstants>
<IsPackable>false</IsPackable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View file

@ -1,16 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<VSTestLogger>nunit</VSTestLogger> <VSTestLogger>nunit</VSTestLogger>
<VSTestResultsDirectory>TestResults</VSTestResultsDirectory>
<IsPackable>false</IsPackable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\MLEM.Startup\MLEM.Startup.csproj" /> <ProjectReference Include="..\MLEM.Startup\MLEM.Startup.csproj" />
<ProjectReference Include="..\MLEM.Data\MLEM.Data.csproj" /> <ProjectReference Include="..\MLEM.Data\MLEM.Data.csproj" />
<ProjectReference Include="..\MLEM.Ui\MLEM.Ui.csproj" /> <ProjectReference Include="..\MLEM.Ui\MLEM.Ui.csproj" />
<ProjectReference Include="..\MLEM\MLEM.csproj" /> <ProjectReference Include="..\MLEM\MLEM.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.0.1641" /> <PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.0.1641" />
<PackageReference Include="coverlet.collector" Version="3.1.2"> <PackageReference Include="coverlet.collector" Version="3.1.2">
@ -23,7 +25,7 @@
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" /> <PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
<PackageReference Include="NunitXml.TestLogger" Version="3.0.117" /> <PackageReference Include="NunitXml.TestLogger" Version="3.0.117" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Content Include="Content/**"> <Content Include="Content/**">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>

View file

@ -8,7 +8,8 @@ var branch = Argument("branch", "main");
var config = Argument("configuration", "Release"); var config = Argument("configuration", "Release");
Task("Prepare").Does(() => { Task("Prepare").Does(() => {
DotNetCoreRestore("MLEM.sln"); DotNetRestore("MLEM.sln");
DotNetRestore("MLEM.FNA.sln");
if (branch != "release") { if (branch != "release") {
var buildNum = EnvironmentVariable("BUILD_NUMBER"); var buildNum = EnvironmentVariable("BUILD_NUMBER");
@ -16,36 +17,34 @@ Task("Prepare").Does(() => {
version += "-" + buildNum; version += "-" + buildNum;
} }
DeleteFiles("**/*.nupkg"); DeleteFiles("**/MLEM*.nupkg");
}); });
Task("Build").IsDependentOn("Prepare").Does(() =>{ Task("Build").IsDependentOn("Prepare").Does(() =>{
var settings = new DotNetCoreBuildSettings { var settings = new DotNetBuildSettings {
Configuration = config, Configuration = config,
ArgumentCustomization = args => args.Append($"/p:Version={version}") ArgumentCustomization = args => args.Append($"/p:Version={version}")
}; };
foreach (var project in GetFiles("**/MLEM*.csproj")) DotNetBuild("MLEM.sln", settings);
DotNetCoreBuild(project.FullPath, settings); DotNetBuild("MLEM.FNA.sln", settings);
DotNetCoreBuild("Demos/Demos.csproj", settings);
DotNetCoreBuild("Demos/Demos.FNA.csproj", settings);
}); });
Task("Test").IsDependentOn("Build").Does(() => { Task("Test").IsDependentOn("Build").Does(() => {
var settings = new DotNetCoreTestSettings { var settings = new DotNetTestSettings {
Configuration = config, Configuration = config,
Collectors = {"XPlat Code Coverage"} Collectors = {"XPlat Code Coverage"}
}; };
DotNetCoreTest("Tests/Tests.csproj", settings); DotNetTest("MLEM.sln", settings);
DotNetCoreTest("Tests/Tests.FNA.csproj", settings); DotNetTest("MLEM.FNA.sln", settings);
}); });
Task("Pack").IsDependentOn("Test").Does(() => { Task("Pack").IsDependentOn("Test").Does(() => {
var settings = new DotNetCorePackSettings { var settings = new DotNetPackSettings {
Configuration = config, Configuration = config,
ArgumentCustomization = args => args.Append($"/p:Version={version}") ArgumentCustomization = args => args.Append($"/p:Version={version}")
}; };
foreach (var project in GetFiles("**/MLEM*.csproj")) DotNetPack("MLEM.sln", settings);
DotNetCorePack(project.FullPath, settings); DotNetPack("MLEM.FNA.sln", settings);
}); });
Task("Push").WithCriteria(branch == "main" || branch == "release").IsDependentOn("Pack").Does(() => { Task("Push").WithCriteria(branch == "main" || branch == "release").IsDependentOn("Pack").Does(() => {
@ -62,7 +61,7 @@ Task("Push").WithCriteria(branch == "main" || branch == "release").IsDependentOn
}; };
} }
settings.SkipDuplicate = true; settings.SkipDuplicate = true;
NuGetPush(GetFiles("**/*.nupkg"), settings); NuGetPush(GetFiles("**/MLEM*.nupkg"), settings);
}); });
Task("Document").Does(() => { Task("Document").Does(() => {