1
0
Fork 0
mirror of https://github.com/Ellpeck/MLEM.git synced 2024-05-15 13:48:46 +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,
"tools": {
"cake.tool": {
"version": "1.3.0",
"version": "2.2.0",
"commands": [
"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_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
*.user
tools
TestResults
TestResults*

View file

@ -19,6 +19,9 @@ Additions
Improvements
- Improved EnumHelper.GetValues signature to return an array
- 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
- Fixed TokenizedString handling trailing spaces incorrectly in the last line of non-left aligned text
@ -28,26 +31,45 @@ Fixes
Additions
- Added some extension methods for querying Anchor types
- 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
- Allow elements to auto-adjust their size even when their children are aligned oddly
- Close other dropdowns when opening a dropdown
- Generified UiMarkdownParser by adding abstract UiParser
- Multi-target net452, making MLEM compatible with MonoGame for consoles
Fixes
- 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 panels sometimes not drawing children that came into view when their positions changed unexpectedly
- 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
Additions
- Added data, from, and copy instructions to DataTextureAtlas
Improvements
- Allow data texture atlas pivots and offsets to be negative
- Made RuntimeTexturePacker restore texture region name and pivot when packing
- Multi-target net452, making MLEM compatible with MonoGame for consoles
Fixes
- 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
### MLEM

View file

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

View file

@ -7,6 +7,7 @@
<AssemblyName>MLEM Desktop Demos</AssemblyName>
<RootNamespace>Demos.DesktopGL</RootNamespace>
<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 -->
<MonoGamePlatform>DesktopGL</MonoGamePlatform>
</PropertyGroup>

View file

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

View file

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

View file

@ -2,6 +2,7 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<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 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?
- Get it on [NuGet](https://www.nuget.org/packages?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.Globalization;
using System.IO;
@ -5,10 +6,9 @@ using System.Text.RegularExpressions;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using MLEM.Textures;
#if FNA
using MLEM.Extensions;
#endif
using MLEM.Misc;
using MLEM.Textures;
namespace MLEM.Data {
/// <summary>
@ -18,15 +18,20 @@ namespace MLEM.Data {
/// </para>
/// <para>
/// 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.
/// 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.
/// 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.
/// 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.
/// 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.
/// <list type="bullet">
/// <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>
/// <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>
/// <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>
/// LongTableRight
/// LongTableRight LongTableUp
/// loc 32 30 64 48
/// piv 80 46
/// </code>
@ -69,6 +74,7 @@ namespace MLEM.Data {
/// <summary>
/// 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>
/// <param name="texture">The texture to use for this data texture atlas</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) {
var info = Path.Combine(content.RootDirectory, infoPath);
string text;
if (Path.IsPathRooted(info)) {
text = File.ReadAllText(info);
} else {
using (var reader = new StreamReader(TitleContainer.OpenStream(info)))
text = reader.ReadToEnd();
try {
if (Path.IsPathRooted(info)) {
text = File.ReadAllText(info);
} else {
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 words = Regex.Split(text, @"\s+");
// parse each texture region: "<names> loc <u> <v> <w> <h> [piv <px> <py>] [off <ox> <oy>]"
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.+-]+))?")) {
// offset
var off = !match.Groups[8].Success ? Vector2.Zero : new Vector2(
float.Parse(match.Groups[8].Value, CultureInfo.InvariantCulture),
float.Parse(match.Groups[9].Value, CultureInfo.InvariantCulture));
var namesOffsets = new List<(string, Vector2)>();
var customData = new Dictionary<string, string>();
var location = Rectangle.Empty;
var pivot = Vector2.Zero;
var offset = Vector2.Zero;
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
var loc = new Rectangle(
int.Parse(match.Groups[2].Value), int.Parse(match.Groups[3].Value),
int.Parse(match.Groups[4].Value), int.Parse(match.Groups[5].Value));
loc.Offset(off.ToPoint());
// pivot
var piv = !match.Groups[6].Success ? Vector2.Zero : off + new Vector2(
float.Parse(match.Groups[6].Value, CultureInfo.InvariantCulture) - (pivotRelative ? 0 : loc.X),
float.Parse(match.Groups[7].Value, CultureInfo.InvariantCulture) - (pivotRelative ? 0 : loc.Y));
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);
// we're starting a new region (or adding another name for a new region), so clear old data
namesOffsets.Add((word.Trim(), Vector2.Zero));
customData.Clear();
location = Rectangle.Empty;
pivot = Vector2.Zero;
offset = Vector2.Zero;
break;
}
} catch (Exception e) {
throw new ContentLoadException($"Couldn't parse data texture atlas instruction {word} for region(s) {string.Join(", ", namesOffsets)}", e);
}
}
// add the last region that was started on
AddCurrentRegions();
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>
/// 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>
/// <param name="content">The content manager to use for loading</param>
/// <param name="texturePath">The path to the texture file</param>

View file

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

View file

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

View file

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

View file

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

View file

@ -1,10 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<ProduceReferenceAssembly>true</ProduceReferenceAssembly>
</PropertyGroup>
<PropertyGroup>
<Authors>Ellpeck</Authors>
<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>
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\MLEM\MLEM.csproj" />
<PackageReference Include="MonoGame.Extended" Version="3.8.0">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
@ -33,9 +33,9 @@
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<None Include="../Media/Logo.png" Pack="true" PackagePath="" />
<None Include="../Docs/index.md" Pack="true" PackagePath="README.md" />
</ItemGroup>
</Project>
</Project>

View file

@ -16,9 +16,11 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests.FNA", "Tests\Tests.FN
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MLEM.Extended.FNA", "MLEM.Extended\MLEM.Extended.FNA.csproj", "{A5B22930-DF4B-4A62-93ED-A6549F7B666B}"
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
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
Global
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}.Release|Any CPU.ActiveCfg = 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
{06459F72-CEAA-4B45-B2B1-708FC28D04F8}.Debug|Any CPU.Build.0 = Debug|x64
{06459F72-CEAA-4B45-B2B1-708FC28D04F8}.Release|Any CPU.ActiveCfg = Release|x64
{06459F72-CEAA-4B45-B2B1-708FC28D04F8}.Release|Any CPU.Build.0 = Release|x64
{0B410591-3AED-4C82-A07A-516FF493709B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0B410591-3AED-4C82-A07A-516FF493709B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0B410591-3AED-4C82-A07A-516FF493709B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0B410591-3AED-4C82-A07A-516FF493709B}.Release|Any CPU.Build.0 = Release|Any CPU
{35253CE1-C864-4CD3-8249-4D1319748E8F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{35253CE1-C864-4CD3-8249-4D1319748E8F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{35253CE1-C864-4CD3-8249-4D1319748E8F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{35253CE1-C864-4CD3-8249-4D1319748E8F}.Release|Any CPU.Build.0 = Release|Any CPU
{39249E92-EBF2-4951-A086-AB4951C3CCE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{39249E92-EBF2-4951-A086-AB4951C3CCE1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{39249E92-EBF2-4951-A086-AB4951C3CCE1}.Release|Any CPU.ActiveCfg = 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
EndGlobal

View file

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

View file

@ -1,11 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFrameworks>net452;netstandard2.0;net6.0</TargetFrameworks>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<ProduceReferenceAssembly>true</ProduceReferenceAssembly>
</PropertyGroup>
<PropertyGroup>
<Authors>Ellpeck</Authors>
<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>
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>
<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\MLEM.csproj" />
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.0.1641">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<None Include="../Media/Logo.png" Pack="true" PackagePath="" />
<None Include="../Docs/index.md" Pack="true" PackagePath="README.md" />

View file

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

View file

@ -408,13 +408,23 @@ namespace MLEM.Ui.Elements {
public GamepadNextElementCallback GetGamepadNextElement;
/// <summary>
/// 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>
public OtherElementCallback OnChildAdded;
/// <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>
public OtherElementCallback OnChildRemoved;
/// <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"/>.
/// This event is useful for unregistering global event handlers when this object should be destroyed.
/// </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.
/// This value is the one that is passed to <see cref="CalcActualSize"/> during <see cref="ForceUpdateArea"/>.
/// </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 Stopwatch stopwatch = new Stopwatch();
@ -497,9 +507,10 @@ namespace MLEM.Ui.Elements {
element.AndChildren(e => {
e.Root = this.Root;
e.System = this.System;
e.OnAddedToUi?.Invoke(e);
this.Root?.InvokeOnElementAdded(e);
this.OnChildAdded?.Invoke(this, e);
});
this.OnChildAdded?.Invoke(this, element);
this.SetSortedChildrenDirty();
element.SetAreaDirty();
return element;
@ -520,9 +531,10 @@ namespace MLEM.Ui.Elements {
element.AndChildren(e => {
e.Root = null;
e.System = null;
e.OnRemovedFromUi?.Invoke(e);
this.Root?.InvokeOnElementRemoved(e);
this.OnChildRemoved?.Invoke(this, e);
});
this.OnChildRemoved?.Invoke(this, element);
this.SetSortedChildrenDirty();
}
@ -589,7 +601,7 @@ namespace MLEM.Ui.Elements {
/// </summary>
public virtual void ForceUpdateArea() {
this.AreaDirty = false;
if (this.IsHidden)
if (this.IsHidden || this.System == null)
return;
// 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

View file

@ -84,8 +84,7 @@ namespace MLEM.Ui.Elements {
return;
if (e == null || !e.GetParentTree().Contains(this))
return;
var firstChild = this.Children.First(c => c != this.ScrollBar);
this.ScrollBar.CurrentValue = (e.Area.Center.Y - this.Area.Height / 2 - firstChild.Area.Top) / e.Scale + this.ChildPadding.Value.Height / 2;
this.ScrollToElement(e);
};
this.AddChild(this.ScrollBar);
}
@ -115,15 +114,6 @@ namespace MLEM.Ui.Elements {
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 />
public override void ForceUpdateSortedChildren() {
base.ForceUpdateSortedChildren();
@ -143,26 +133,6 @@ namespace MLEM.Ui.Elements {
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 />
public override void Draw(GameTime time, SpriteBatch batch, float alpha, SpriteBatchContext context) {
// draw children onto the render target if we have one
@ -207,11 +177,32 @@ namespace MLEM.Ui.Elements {
return base.GetElementUnderPos(position);
}
private RectangleF GetRenderTargetArea() {
var area = this.ChildPaddedArea;
area.X = this.DisplayArea.X;
area.Width = this.DisplayArea.Width;
return area;
/// <inheritdoc />
public override void Dispose() {
if (this.renderTarget != null) {
this.renderTarget.Dispose();
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 />
@ -225,34 +216,56 @@ namespace MLEM.Ui.Elements {
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>
/// Prepares the panel for auto-scrolling, creating the render target and setting up the scroll bar's maximum value.
/// </summary>
protected virtual void ScrollSetup() {
if (!this.scrollOverflow || this.IsHidden)
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
var firstChild = this.Children.First(c => c != this.ScrollBar);
var lowestChild = this.GetLowestChild(c => c != this.ScrollBar && !c.IsHidden);
var childrenHeight = lowestChild.Area.Bottom - firstChild.Area.Top;
float childrenHeight;
if (this.Children.Count > 1) {
var firstChild = this.Children.FirstOrDefault(c => c != this.ScrollBar);
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;
if (!this.ScrollBar.MaxValue.Equals(scrollBarMax, Element.Epsilon)) {
this.ScrollBar.MaxValue = scrollBarMax;
this.relevantChildrenDirty = true;
}
// update child padding based on whether the scroll bar is visible
var childOffset = this.ScrollBar.IsHidden ? 0 : this.ScrollerSize.Value.X + this.ScrollBarOffset;
if (!this.scrollBarChildOffset.Equals(childOffset, Element.Epsilon)) {
this.ChildPadding += new Padding(0, -this.scrollBarChildOffset + childOffset, 0, 0);
this.scrollBarChildOffset = childOffset;
this.SetAreaDirty();
}
// update child padding based on whether the scroll bar is visible
var childOffset = this.ScrollBar.IsHidden ? 0 : this.ScrollerSize.Value.X + this.ScrollBarOffset;
if (!this.scrollBarChildOffset.Equals(childOffset, Element.Epsilon)) {
this.ChildPadding += new Padding(0, -this.scrollBarChildOffset + childOffset, 0, 0);
this.scrollBarChildOffset = childOffset;
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
@ -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() {
if (this.ScrollBar == null)
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
/// </summary>
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>
/// The scroller's width and height
/// </summary>
@ -86,7 +96,7 @@ namespace MLEM.Ui.Elements {
/// <summary>
/// This property is true while the user scrolls on the scroll bar using the mouse or touch input
/// </summary>
public bool IsBeingScrolled => this.isMouseHeld || this.isDragging || this.isTouchHeld;
public bool IsBeingScrolled => this.isMouseScrolling || this.isMouseDragging || this.isTouchDragging || this.isTouchScrolling;
/// <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.
/// </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.Height - this.ScrollerSize.Y * this.Scale));
/// <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.
/// 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.
/// </summary>
public StyleProp<bool> SmoothScrolling;
/// <summary>
/// The factor with which <see cref="SmoothScrolling"/> happens.
/// </summary>
public StyleProp<float> SmoothScrollFactor;
public bool MouseDragScrolling;
private bool isMouseHeld;
private bool isDragging;
private bool isTouchHeld;
private bool isMouseScrolling;
private bool isMouseDragging;
private bool isTouchScrolling;
private bool isTouchDragging;
private float maxValue;
private float scrollAdded;
private float currValue;
@ -141,18 +147,29 @@ namespace MLEM.Ui.Elements {
// MOUSE INPUT
var moused = this.Controls.MousedElement;
if (moused == this && this.Input.WasMouseButtonUp(MouseButton.Left) && this.Input.IsMouseButtonDown(MouseButton.Left)) {
this.isMouseHeld = true;
var wasMouseUp = this.Input.WasMouseButtonUp(MouseButton.Left);
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;
} else if (this.isMouseHeld && !this.Input.IsMouseButtonDown(MouseButton.Left)) {
this.isMouseHeld = false;
} else if (!isMouseDown) {
this.isMouseScrolling = false;
}
if (this.isMouseHeld)
if (this.isMouseScrolling)
this.ScrollToPos(this.TransformInverseAll(this.Input.ViewportMousePosition.ToVector2()));
if (!this.Horizontal && moused != null && (moused == this.Parent || moused.GetParentTree().Contains(this.Parent))) {
var scroll = this.Input.LastScrollWheel - this.Input.ScrollWheel;
if (scroll != 0)
this.CurrentValue += this.StepPerScroll * Math.Sign(scroll);
if (!this.Horizontal) {
if (moused != null && (moused == this.Parent || moused.GetParentTree().Contains(this.Parent))) {
var scroll = this.Input.LastScrollWheel - this.Input.ScrollWheel;
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
@ -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
var touched = this.Parent.GetElementUnderPos(this.TransformInverseAll(drag.Position));
if (touched != null && touched != this)
this.isDragging = true;
this.isTouchDragging = true;
// if we're dragging at all, then move the scroller
if (this.isDragging)
if (this.isTouchDragging)
this.CurrentValue -= drag.Delta.Y / this.Scale;
} else {
this.isDragging = false;
this.isTouchDragging = false;
}
}
if (this.Input.ViewportTouchState.Count <= 0) {
// if no touch has occured this tick, then reset the variable
this.isTouchHeld = false;
this.isTouchScrolling = false;
} else {
foreach (var loc in this.Input.ViewportTouchState) {
var pos = this.TransformInverseAll(loc.Position);
// 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 _)) {
this.isTouchHeld = true;
this.isTouchScrolling = true;
this.scrollStartOffset = pos - this.ScrollerPosition;
break;
}
// scroll no matter if we're on the scroller right now
if (this.isTouchHeld)
if (this.isTouchScrolling)
this.ScrollToPos(pos);
}
}

View file

@ -6,7 +6,9 @@ using MLEM.Input;
using MLEM.Misc;
using MLEM.Textures;
using MLEM.Ui.Style;
#if NETSTANDARD2_0_OR_GREATER || NET6_0_OR_GREATER
using TextCopy;
#endif
namespace MLEM.Ui.Elements {
/// <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="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) {
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),
InputRule = (i, s) => this.InputRule.Invoke(this, s)
};

View file

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFrameworks>net452;netstandard2.0;net6.0</TargetFrameworks>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<ProduceReferenceAssembly>true</ProduceReferenceAssembly>
<RootNamespace>MLEM.Ui</RootNamespace>
@ -20,10 +20,10 @@
</PropertyGroup>
<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="..\FNA\FNA.Core.csproj">
<ProjectReference Include="..\FNA\FNA.csproj">
<PrivateAssets>all</PrivateAssets>
</ProjectReference>
</ItemGroup>

View file

@ -1,10 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFrameworks>net452;netstandard2.0;net6.0</TargetFrameworks>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<ProduceReferenceAssembly>true</ProduceReferenceAssembly>
</PropertyGroup>
<PropertyGroup>
<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>
@ -16,18 +16,18 @@
<PackageIcon>Logo.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="TextCopy" Version="6.1.0" />
<PackageReference Include="TextCopy" Version="6.1.0" Condition="'$(TargetFramework)'!='net452'" />
<ProjectReference Include="..\MLEM\MLEM.csproj" />
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.0.1641">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<None Include="../Media/Logo.png" Pack="true" PackagePath="" />
<None Include="../Docs/index.md" Pack="true" PackagePath="README.md" />
</ItemGroup>
</Project>
</Project>

View file

@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using MLEM.Formatting;
@ -10,6 +9,12 @@ using MLEM.Textures;
using MLEM.Ui.Elements;
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 {
/// <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"/>.
@ -148,15 +153,16 @@ namespace MLEM.Ui.Parsers {
try {
Texture2D tex;
if (path.StartsWith("http")) {
using (var client = new HttpClient()) {
using (var src = await client.GetStreamAsync(path)) {
using (var memory = new MemoryStream()) {
// download the full stream before passing it to texture
await src.CopyToAsync(memory);
tex = Texture2D.FromStream(this.GraphicsDevice, memory);
}
}
}
byte[] src;
#if NETSTANDARD2_0_OR_GREATER || NET6_0_OR_GREATER
using (var client = new HttpClient())
src = await client.GetByteArrayAsync(path);
#else
using (var client = new WebClient())
src = await client.DownloadDataTaskAsync(path);
#endif
using (var memory = new MemoryStream(src))
tex = Texture2D.FromStream(this.GraphicsDevice, memory);
} else {
using (var stream = Path.IsPathRooted(path) ? File.OpenRead(path) : TitleContainer.OpenStream(path))
tex = Texture2D.FromStream(this.GraphicsDevice, stream);

View file

@ -9,6 +9,10 @@ using MLEM.Misc;
using MLEM.Ui.Elements;
using MLEM.Ui.Style;
#if NET452
using MLEM.Extensions;
#endif
namespace MLEM.Ui {
/// <summary>
/// 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 => {
e.Root = root;
e.System = this;
e.OnAddedToUi?.Invoke(e);
root.InvokeOnElementAdded(e);
e.SetAreaDirty();
});
@ -369,6 +370,7 @@ namespace MLEM.Ui {
root.Element.AndChildren(e => {
e.Root = null;
e.System = null;
e.OnRemovedFromUi?.Invoke(e);
root.InvokeOnElementRemoved(e);
e.SetAreaDirty();
});
@ -570,7 +572,7 @@ namespace MLEM.Ui {
/// </summary>
public event Element.GenericCallback OnElementAdded;
/// <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>
public event Element.GenericCallback OnElementRemoved;
/// <summary>

View file

@ -18,8 +18,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MLEM.Data", "MLEM.Data\MLEM
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MLEM.Templates", "MLEM.Templates\MLEM.Templates.csproj", "{C2A2CFED-C9E8-4675-BD66-EFC3DB210977}"
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}"
EndProject
Global
@ -64,10 +62,6 @@ Global
{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.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.Build.0 = Debug|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;
}
#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 {
/// <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 draw the batched items, call <see cref="Draw"/>.
/// </summary>
@ -23,7 +23,7 @@ namespace MLEM.Graphics {
/// <summary>
/// The amount of vertices that are currently batched.
/// </summary>
public int Vertices => this.items.Count * 4;
public int Vertices => this.itemAmount * 4;
/// <summary>
/// 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"/>.
@ -41,13 +41,15 @@ namespace MLEM.Graphics {
private readonly GraphicsDevice graphicsDevice;
private readonly SpriteEffect spriteEffect;
private readonly List<VertexBuffer> vertexBuffers = new List<VertexBuffer>();
private readonly List<DynamicVertexBuffer> vertexBuffers = new List<DynamicVertexBuffer>();
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 bool batching;
private bool batchChanged;
private int itemAmount;
/// <summary>
/// Creates a new static sprite batch with the given <see cref="GraphicsDevice"/>
@ -62,10 +64,27 @@ namespace MLEM.Graphics {
/// Begins batching.
/// Call this method before calling <c>Add</c> or any of its overloads.
/// </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>
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)
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;
}
@ -73,14 +92,10 @@ namespace MLEM.Graphics {
/// Ends batching.
/// Call this method after calling <c>Add</c> or any of its overloads the desired number of times to add batched items.
/// </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="ArgumentOutOfRangeException">Thrown if the <paramref name="sortMode"/> is <see cref="SpriteSortMode.Immediate"/>, which is not supported.</exception>
public void EndBatch(SpriteSortMode sortMode = SpriteSortMode.Texture) {
public void EndBatch() {
if (!this.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;
// 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.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
var dataIndex = 0;
Texture2D texture = null;
foreach (var item in ordered) {
// if the texture changes, we also have to start a new buffer!
if (dataIndex > 0 && (item.Texture != texture || dataIndex >= StaticSpriteBatch.Data.Length)) {
this.FillBuffer(this.FilledBuffers++, texture, StaticSpriteBatch.Data);
dataIndex = 0;
foreach (var itemSet in this.items.Values) {
foreach (var item in itemSet.Items) {
// if the texture changes, we also have to start a new buffer!
if (dataIndex > 0 && (item.Texture != texture || dataIndex >= StaticSpriteBatch.Data.Length)) {
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)
this.FillBuffer(this.FilledBuffers++, texture, StaticSpriteBatch.Data);
// 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
if (this.indices == null || this.indices.IndexCount < 6 * maxItems) {
var newIndices = new short[6 * maxItems];
@ -178,7 +180,7 @@ namespace MLEM.Graphics {
for (var i = 0; i < this.FilledBuffers; i++) {
var buffer = this.vertexBuffers[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);
if (effect != null) {
@ -352,6 +354,21 @@ namespace MLEM.Graphics {
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>
/// 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.
@ -362,7 +379,11 @@ namespace MLEM.Graphics {
public bool Remove(Item item) {
if (!this.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;
return true;
}
@ -380,6 +401,7 @@ namespace MLEM.Graphics {
this.items.Clear();
this.textures.Clear();
this.FilledBuffers = 0;
this.itemAmount = 0;
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) {
if (!this.batching)
throw new InvalidOperationException("Not batching");
var item = new Item(texture, depth, tl, tr, bl, br);
this.items.Add(item);
this.batchChanged = true;
return item;
return this.Add(new Item(texture, depth, tl, tr, bl, br));
}
private void FillBuffer(int index, Texture2D texture, VertexPositionColorTexture[] data) {
if (this.vertexBuffers.Count <= index)
this.vertexBuffers.Add(new VertexBuffer(this.graphicsDevice, VertexPositionColorTexture.VertexDeclaration, StaticSpriteBatch.MaxBatchItems * 4, BufferUsage.WriteOnly));
this.vertexBuffers[index].SetData(data);
this.vertexBuffers.Add(new DynamicVertexBuffer(this.graphicsDevice, VertexPositionColorTexture.VertexDeclaration, StaticSpriteBatch.MaxBatchItems * 4, BufferUsage.WriteOnly));
this.vertexBuffers[index].SetData(data, 0, data.Length, SetDataOptions.Discard);
this.textures.Insert(index, texture);
}
@ -452,6 +469,15 @@ namespace MLEM.Graphics {
#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>
/// 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"/>.
@ -474,6 +500,65 @@ namespace MLEM.Graphics {
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

View file

@ -20,6 +20,8 @@ namespace MLEM.Input {
#else
private static readonly int MaximumGamePadCount = GamePad.MaximumGamePadCount;
#endif
private static readonly TouchLocation[] EmptyTouchLocations = new TouchLocation[0];
private static readonly GenericInput[] EmptyGenericInputs = new GenericInput[0];
/// <summary>
/// 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.
/// 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>
public GenericInput[] InputsDown { get; private set; } = Array.Empty<GenericInput>();
public GenericInput[] InputsDown { get; private set; } = InputHandler.EmptyGenericInputs;
/// <summary>
/// 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.
/// </summary>
public GenericInput[] InputsPressed { get; private set; } = Array.Empty<GenericInput>();
public GenericInput[] InputsPressed { get; private set; } = InputHandler.EmptyGenericInputs;
/// <summary>
/// Contains the touch state from the last update call
/// </summary>
public TouchCollection LastTouchState { get; private set; } = new TouchCollection(Array.Empty<TouchLocation>());
public TouchCollection LastTouchState { get; private set; } = new TouchCollection(InputHandler.EmptyTouchLocations);
/// <summary>
/// Contains the current touch state
/// </summary>
public TouchCollection TouchState { get; private set; } = new TouchCollection(Array.Empty<TouchLocation>());
public TouchCollection TouchState { get; private set; } = new TouchCollection(InputHandler.EmptyTouchLocations);
/// <summary>
/// Contains the <see cref="LastTouchState"/>, but with the <see cref="GraphicsDevice.Viewport"/> taken into account.
/// </summary>
@ -281,7 +283,7 @@ namespace MLEM.Input {
this.LastTouchState = this.TouchState;
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) {
this.ViewportTouchState = new List<TouchLocation>();
foreach (var touch in this.TouchState) {
@ -302,8 +304,8 @@ namespace MLEM.Input {
}
if (this.inputsDownAccum.Count <= 0 && this.inputsDown.Count <= 0) {
this.InputsPressed = Array.Empty<GenericInput>();
this.InputsDown = Array.Empty<GenericInput>();
this.InputsPressed = InputHandler.EmptyGenericInputs;
this.InputsDown = InputHandler.EmptyGenericInputs;
} else {
// handle pressed inputs
var pressed = new List<GenericInput>();
@ -823,6 +825,7 @@ namespace MLEM.Input {
downTime = DateTime.UtcNow - start;
return true;
}
downTime = default;
return false;
}
@ -851,6 +854,7 @@ namespace MLEM.Input {
upTime = DateTime.UtcNow - start;
return true;
}
upTime = default;
return false;
}
@ -879,6 +883,7 @@ namespace MLEM.Input {
lastPressTime = DateTime.UtcNow - start;
return true;
}
lastPressTime = default;
return false;
}

View file

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

View file

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

View file

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

View file

@ -22,8 +22,12 @@ namespace MLEM.Misc {
/// </summary>
/// <typeparam name="T">The type whose enum to get</typeparam>
/// <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));
#endif
}
}

View file

@ -11,6 +11,8 @@ namespace MLEM.Misc {
[DataContract]
public class GenericDataHolder : IGenericDataHolder {
private static readonly string[] EmptyStrings = new string[0];
[DataMember(EmitDefaultValue = false)]
private Dictionary<string, object> data;
@ -34,9 +36,9 @@ namespace MLEM.Misc {
}
/// <inheritdoc />
public IReadOnlyCollection<string> GetDataKeys() {
public IEnumerable<string> GetDataKeys() {
if (this.data == null)
return Array.Empty<string>();
return GenericDataHolder.EmptyStrings;
return this.data.Keys;
}
@ -67,7 +69,7 @@ namespace MLEM.Misc {
/// Returns all of the generic data that this object stores.
/// </summary>
/// <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 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?
- Get it on [NuGet](https://www.nuget.org/packages?q=mlem)
- Get prerelease builds on [BaGet](https://nuget.ellpeck.de/?q=mlem)

View file

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using FontStashSharp;
@ -13,6 +14,7 @@ using MLEM.Extensions;
using MLEM.Font;
using MLEM.Formatting;
using MLEM.Formatting.Codes;
using MLEM.Graphics;
using MLEM.Input;
using MLEM.Misc;
using MLEM.Startup;
@ -351,6 +353,35 @@ public class GameImpl : MlemGame {
});
}
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) {

View file

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

View file

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

View file

@ -9,26 +9,51 @@ namespace Tests {
public class TestDataTextureAtlas {
[Test]
public void Test() {
public void Test([Values(0, 4)] int regionX, [Values(0, 4)] int regionY) {
using var game = TestGame.Create();
using var texture = new Texture2D(game.GraphicsDevice, 1, 1);
var atlas = DataTextureAtlas.LoadAtlasData(new TextureRegion(texture), game.RawContent, "Texture.atlas");
Assert.AreEqual(atlas.Regions.Count(), 8);
var region = new TextureRegion(texture, regionX, regionY, 1, 1);
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
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));
// added offset
var table2 = atlas["LongTableLeft"];
Assert.AreEqual(table2.Area, new Rectangle(64, 32, 64, 48));
var table2 = atlas["LongTableDown"];
Assert.AreEqual(table2.Area, new Rectangle(64 + regionX, 32 + regionY, 64, 48));
Assert.AreEqual(table2.PivotPixels, new Vector2(112 - 64, 48 - 32));
// negative pivot
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));
// 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">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<VSTestLogger>nunit</VSTestLogger>
<VSTestResultsDirectory>TestResults.FNA</VSTestResultsDirectory>
<RootNamespace>Tests</RootNamespace>
<DefineConstants>$(DefineConstants);FNA</DefineConstants>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>

View file

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

View file

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