1
0
Fork 0
mirror of https://github.com/Ellpeck/MLEM.git synced 2024-11-24 13:38:34 +01:00

Compare commits

..

115 commits

Author SHA1 Message Date
Ell
5564878c9d release 5.0.0 2021-06-30 00:18:54 +02:00
Ell
5756708010 added quick links to changelog 2021-06-29 00:57:04 +02:00
Ell
fe9b3830f8 (finally) added a changelog 2021-06-28 15:38:30 +02:00
Ell
1377941f1a added TreatSizeAsMaximum to ui elements 2021-06-25 16:48:41 +02:00
Ell
705758090b fixed image tokens drawing themselves too many times for split strings 2021-06-25 16:40:09 +02:00
Ell
14940d39c5 added text alignment options to tokenized strings and paragraphs 2021-06-25 15:23:30 +02:00
Ell
d1ce9412a2 added a test for element auto-area performance 2021-06-25 12:45:00 +02:00
Ell
ef83124cfa allow changing the color of panels 2021-06-22 01:14:06 +02:00
Ell
dca013e551 fixed GetModifier being.. recursive 2021-06-21 01:03:24 +02:00
Ell
91730b1f27 improved GenericInput ToString 2021-06-21 00:57:56 +02:00
Ell
2f16bbdc44 the None input type should always be considered up 2021-06-21 00:51:21 +02:00
Ell
ce920b5219 added an element helper method to create a keybind button 2021-06-21 00:49:09 +02:00
Ell
a5a73af01c improved Keybind constructors 2021-06-20 23:48:02 +02:00
Ell
2118837062 added generic input equality checking 2021-06-20 23:17:39 +02:00
Ell
60c9236cbd added InputsDown and InputsPressed to InputHandler 2021-06-20 23:05:02 +02:00
Ell
664a2a9f11 added a way to access a keybind's combinations 2021-06-20 22:33:24 +02:00
Ell
07eb6ac36f fixed RawContentManager crashing with dynamic assemblies present 2021-06-20 17:35:56 +02:00
Ell
a0357e4dfc improved data-related tests 2021-06-19 21:51:09 +02:00
Ell
dd09f0af25 added HasAnyFlag and improved memory performance of HasFlag in DynamicEnum
Some checks reported errors
Ellpeck/MLEM/pipeline/head Something is wrong with the build of this commit
2021-06-11 20:22:25 +02:00
Ell
fda4097aa5 reverted the last commit, and invalidated DynamicEnum caches when new values are added 2021-06-11 20:05:32 +02:00
Ell
7ed969d1af Moved DynamicEnum to its own library 2021-06-11 19:46:51 +02:00
Ell
cca02b5396 organized UiSystem constructor 2021-06-09 00:37:44 +02:00
Ell
a02334a34c turned some non-event events into events and added Disposing event to ui elements 2021-06-09 00:27:50 +02:00
Ell
2cc77f42cd added events for when a root element is added or removed from a ui 2021-06-08 21:36:42 +02:00
Ell
b0d146849d added dynamic enum json converter 2021-06-08 14:45:46 +02:00
Ell
e0a9971bdb added DynamicEnum to MLEM.Data 2021-06-08 13:32:01 +02:00
Ell
20e2d098ef suppress finalize for manually disposed panels 2021-06-08 00:32:07 +02:00
Ell
0dad4860c1 dispose of the panel's render target 2021-06-08 00:29:51 +02:00
Ell
ed02a83879 fixed panels drawing children early within the render target 2021-06-07 23:14:50 +02:00
Ell
d1fbcb9559 added a sprite batch extension to generate a gradient 2021-06-03 21:33:09 +02:00
Ell
289e0e8597 added a simple way to change the action that is executed when a link is pressed inside a paragraph 2021-05-30 17:57:39 +02:00
Ell
d146e80cf6 updated some outdated dependencies 2021-05-29 18:06:20 +02:00
Ell
3da97fcc83 fixed UnderlineCode being a font code, making it end in the wrong places 2021-05-24 17:12:02 +02:00
Ell
ef45c324f9 fixed a crash with truncated string tokenization 2021-05-20 19:59:37 +02:00
Ell
d385581c25 added formatted string truncation to tokenized strings and ui paragraphs 2021-05-18 16:47:38 +02:00
Ell
e916ddb7a8 allow for underline and shadow codes to be mixed with font codes 2021-05-18 16:19:40 +02:00
Ell
f94d471365 explicitly disallow auto-sizing incompatibilities to make debugging easier 2021-04-27 21:17:06 +02:00
Ell
f60c3b288a also apply auto-sizing if the element has no children at all 2021-04-26 19:21:11 +02:00
Ell
2abc3264a2 simplify auto-size checks 2021-04-26 19:06:54 +02:00
Ell
7792ce99c8 fixed auto-sized elements without children not being updated correctly 2021-04-26 18:55:18 +02:00
Ell
f71f998508 clarify OpenLinkOrFile usage documentation 2021-04-23 14:34:59 +02:00
Ell
b48ed479a0 streamlined TextInputWrapper into MlemPlatform and included link opening 2021-04-23 00:17:46 +02:00
Ell
1123b815b3 updated some package declarations to clean up warnings 2021-04-22 19:59:35 +02:00
Ell
338cf383f4 removed RequiresOnScreenKeyboard 2021-04-22 19:40:14 +02:00
Ell
cf9bcc7ae4 updated to MonoGame 8 and added support for opening the on-screen keyboard to TextInputWrapper 2021-04-22 19:26:07 +02:00
Ell
5e26155ec5 removed redundant dependencies from test project 2021-04-22 02:33:19 +02:00
Ell
f3b3feec9b updated non-netstandard projects to net5.0 2021-04-22 02:21:36 +02:00
Ell
b3759b86c4 possibly fixed weird ci issue because of course 2021-04-22 01:42:42 +02:00
Ell
1759f0ef5b improve performance of TextFormatter tokenization 2021-04-22 01:21:44 +02:00
Ell
455ab59f09 improved performance of TokenizedString splitting massively 2021-04-22 01:14:48 +02:00
Ell
60bc320604 added tests for ensuring genericfont correctness compared to regular font 2021-04-19 14:30:03 +02:00
Ell
8078d41724 simplified GenericFont implementation 2021-04-19 14:02:28 +02:00
Ell
e7ab8fefe8 improved performance of SplitString and re-added Zwsp compatibility 2021-04-14 23:13:19 +02:00
Ell
538fd08d8a improved TokenizedString splitting efficiency 2021-04-14 02:47:41 +02:00
Ell
6e2db418c5 added some more unit tests for string splitting and formatting 2021-04-14 02:38:54 +02:00
Ell
55477c6341 removed Zwsp functionality, which was incomplete and did not work 2021-04-14 00:49:33 +02:00
Ell
d8d29bf10d added a few more tests 2021-04-04 18:31:51 +02:00
Ell
2055c3a6ef convert tests to use graphics 2021-04-02 17:12:27 +02:00
Ell
2287df00af removed unnecessary property 2021-04-01 19:38:03 +02:00
Ell
e7fd026a33 added some UI tests 2021-04-01 19:36:56 +02:00
Ell
64c2ebd4ac updated license year 2021-03-30 00:56:28 +02:00
Ell
82b8c0ab49 fixed a rare stack overflow where scroll bars could get stuck in an auto-hide loop 2021-03-29 08:28:49 +02:00
Ell
b1ff703fe1 reintroduced tolerance for element size equality 2021-03-29 06:56:06 +02:00
Ell
79ba6864e7 fixed stack overflow with nested auto-sized children 2021-03-29 06:41:38 +02:00
Ell
3e20aaf6c5 fixed auto-sized elements doing unnecessarily many area updates 2021-03-29 05:49:09 +02:00
Ell
2741682029 fixed generic stash font line height for static sprite fonts 2021-03-29 02:54:02 +02:00
Ell
e6243b831c remove unnecessary references to GraphicsDevice from UiSystem 2021-03-29 02:26:44 +02:00
Ell
28eafffa32 allow setting a custom viewport for ui systems 2021-03-29 02:15:17 +02:00
Ell
35af9eee25 fixed some number parsing not using invariant culture 2021-03-28 06:20:27 +02:00
Ell
602f19a2a8 some minor code style and format improvements 2021-03-24 22:44:39 +01:00
Ell
281cce8fba updated remaining dependencies 2021-03-24 22:31:58 +01:00
Ell
5f7956a7a3 properly stop a panel's scroll bar from being removed 2021-03-24 22:01:02 +01:00
Ell
be9748e70e fully disallow access to internal children collections 2021-03-24 01:39:41 +01:00
Ell
dcee3c5010 added an option to limit auto-heights in elements 2021-03-24 01:25:39 +01:00
Ell
3d314172d0 avoid recursion in Element.ForceUpdateArea 2021-03-24 01:10:42 +01:00
Ell
a477fef230 always publish test results to jenkins 2021-03-20 21:37:43 +01:00
Ell
23b27cf877 moved friends to README 2021-03-18 22:13:33 +01:00
Ell
c71fb58760 added a few more tests 2021-03-18 18:31:59 +01:00
Ell
10c1cc8905 fixed legacy argument specifier 2021-03-18 17:30:20 +01:00
Ell
702bf94f49 added some tests 2021-03-18 17:28:08 +01:00
Ell
e24c871ecd added SoundExtensions 2021-03-17 22:47:23 +01:00
Ell
3e3f0fc742 updated coroutine dependency 2021-03-17 01:23:30 +01:00
Ell
0ed8ec1268 added a "Made with MLEM" section to the readme 2021-03-14 17:27:10 +01:00
Ell
014b8f90df added repeat-ignoring versions of IsKeyPressed and IsGamepadButtonPressed to InputHandler 2021-03-14 17:09:45 +01:00
Ell
3384f48623 fixed up android demo, updated some dependencies and added icons to templates 2021-03-14 01:03:17 +01:00
Ell
1e485a103c drop .NET Framework support for TextInputWrapper.DesktopGl 2021-03-13 17:09:16 +01:00
Ell
e0263dc943 Removed obsolete ColorExtensions methods 2021-03-13 16:25:08 +01:00
Ell
a0609e66eb use the known type in StaticJsonConverter 2021-03-13 03:20:38 +01:00
Ell
abcdcd21cc added StaticJsonConverter 2021-03-13 03:15:39 +01:00
Ell
37609ade76 fixed DrawString in generic font ignoring text scale for alignment 2021-03-12 20:47:57 +01:00
Ell
0ddb4afc3f allowed the GetArea extension to calculate flipping 2021-03-12 20:22:36 +01:00
Ell
45fc12b0cb added some additional tiled helper methods 2021-03-10 02:13:07 +01:00
Ell
1d80965d24 allow removing a tile using the new SetTile methd 2021-03-09 19:06:06 +01:00
Ell
14e97abf87 some more tiled map extension utility methods 2021-03-09 18:56:55 +01:00
Ell
0411add4d1 added a newline macro to the default text formatter 2021-03-09 17:45:49 +01:00
Ell
b594c271ac expose the viewport of a camera 2021-03-09 02:29:06 +01:00
Ell
0b39928334 minor general cleanup 2021-03-08 15:12:13 +01:00
Ell
dc514815c3 expose GetCollidingAreas 2021-03-07 22:13:24 +01:00
Ell
200058a611 added a method to make sidescrolling collision detection easier with TiledMapCollisions 2021-03-07 22:03:29 +01:00
Ell
6ef9f8ffb1 also switch order of x and y iteration in GetCollidingTiles 2021-03-07 20:59:10 +01:00
Ell
f2df639f9e reverse y loop in GetCollidingTiles to account for gravity usually pointing down 2021-03-07 20:45:00 +01:00
Ell
053aaaf17c removed the array-based GetRandomEntry method as it made lists have to be typecast 2021-03-04 22:53:39 +01:00
Ell
1c8b738555 added some collection extensions, namely Combinations 2021-03-04 22:52:28 +01:00
Ell
5c1e76a0c9 Merge branch 'release' into main 2021-03-02 02:24:03 +01:00
Ell
80f2b55687 combine attributes 2021-02-28 16:44:29 +01:00
Ell
5b4757d3bf made Padding and Direction2 DataContracts 2021-02-28 16:42:51 +01:00
Ell
d73539e41e added a text scale multiplier to Paragraph 2021-02-28 14:43:07 +01:00
Ell
468bee9ca8 Turned Direction2 into a flags enum 2021-02-28 14:37:02 +01:00
Ell
dda827b985 added GenericFont compatibility for FontStashSharp 2021-02-27 16:58:36 +01:00
Ell
6900da9858 allow nine patches to be drawn tiled rather than stretched 2021-02-19 21:24:08 +01:00
Ell
eb3194a0c1 preserve position and scroll wheel value when the mouse is out of bounds 2021-02-19 02:47:32 +01:00
Ell
73eab1d41e only handle mouse input if the mouse is in the window 2021-02-19 02:31:38 +01:00
Ell
01b6168259 fixed the input handler querying input when the window is inactive 2021-02-18 18:36:29 +01:00
Ell
69d81da70c update demos and sandbox to netcore3.1 which was long overdue 2021-02-18 16:12:44 +01:00
Ell
fb4e20f545 bump version (in preparation for updating to net5) 2021-02-18 15:35:59 +01:00
108 changed files with 3110 additions and 1127 deletions

View file

@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"cake.tool": {
"version": "0.38.5",
"version": "1.1.0",
"commands": [
"dotnet-cake"
]

3
.gitignore vendored
View file

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

81
CHANGELOG.md Normal file
View file

@ -0,0 +1,81 @@
# Changelog
MLEM uses [semantic versioning](https://semver.org/).
This changelog also contains information on versions that have yet to be released. The changelog for unreleased versions might be edited over time as new features get added, changed or removed. To see the newest released version's code, check out the [release branch](https://github.com/Ellpeck/MLEM/tree/release).
Jump to version:
- [5.0.0](#500)
## 5.0.0
### MLEM
Additions
- Added some Collection extensions, namely for dealing with combinations
- Added repeat-ignoring versions of IsKeyPressed and IsGamepadButtonPressed
- Added SoundExtensions
- Added string truncation to TokenizedString
- Added a sprite batch extension to generate a gradient
- Added InputsDown and InputsPressed properties to InputHandler
- Added text alignment options to tokenized strings
Improvements
- Allow NinePatches to be drawn tiled rather than stretched
- Added the ability for Direction2 to be used as flags
- Made Padding and Direction2 DataContracts
- Expose the viewport of cameras
- Greatly improved the efficiency of line splitting for GenericFont and TokenizedString
- Improved performance of TextFormatter tokenization
- Replaced TextInputWrapper with a more refined MlemPlatform that includes the ability to open links on various platforms
- Allow for underline and shadow formatting codes to be mixed with font changing codes
- Exposed Keybind Combinations
Fixes
- Fixed the input handler querying input when the window is inactive
- Fixed UnderlineCode ending in the wrong places because it was marked as a font-changing code
Removals
- Removed the array-based GetRandomEntry method
- Removed obsolete ColorExtension methods
### MLEM.Ui
Additions
- Added a text scale multiplier value to Paragraph
- Added an option to limit auto-height and auto-width in elements to a maximum and minimum size
- Added the ability to set a custom viewport for ui systems
- Added string truncation to Paragraph
- Added a simple way to change the action that is executed when a link is pressed in a paragraph
- Added events for when a root element is added or removed
- Added an ElementHelper method to create a keybind button
- Added text alignment options to paragraphs
Improvements
- Stop a panel's scroll bar from being removed from its children list automatically
- Removed unnecessary GraphicsDevice references from UiSystem
- Dispose of panels' render targets to avoid memory leaks
- Allow changing the color that a panel renders its texture with
Fixes
- Fixed auto-sized elements doing too many area update calculations
- Fixed a rare stack overflow where scroll bars could get stuck in an auto-hide loop
- Fixed auto-sized elements without children not updating their size correctly
- Fixed panels drawing children early within the render target (instead of regularly)
### MLEM.Extended
Additions
- Added GenericFont compatibility for FontStashSharp
- Added a method to make sidescrolling collision detection easier with TiledMapCollisions
- Added some more TiledMapExtension utility methods
Improvements
- Reversed the y loop in GetCollidingTiles to account for gravity which is usually more important
Fixes
- Fixed some number parsing not using the invariant culture
### MLEM.Data
Additions
- Added StaticJsonConverter
- Added DynamicEnum, a cursed custom enumeration class that supports arbitrarily many values
Fixes
- Fixed some number parsing not using the invariant culture
- Fixed RawContentManager crashing with dynamic assemblies present

View file

@ -5,6 +5,7 @@ using Android.Net;
using Android.OS;
using Android.Views;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using MLEM.Extensions;
using MLEM.Misc;
@ -29,16 +30,12 @@ namespace Demos.Android {
if (Build.VERSION.SdkInt >= BuildVersionCodes.P)
this.Window.Attributes.LayoutInDisplayCutoutMode = LayoutInDisplayCutoutMode.ShortEdges;
TextInputWrapper.Current = new TextInputWrapper.Mobile();
MlemPlatform.Current = new MlemPlatform.Mobile(KeyboardInput.Show, l => this.StartActivity(new Intent(Intent.ActionView, Uri.Parse(l))));
this.game = new GameImpl();
// reset MlemGame width and height to use device's aspect ratio
this.game.GraphicsDeviceManager.ResetWidthAndHeight(this.game.Window);
this.game.OnLoadContent += game => {
// disable mouse handling for android to make emulator behavior more coherent
game.InputHandler.HandleMouse = false;
// make text links be opened properly
game.UiSystem.LinkBehavior = l => this.StartActivity(new Intent(Intent.ActionView, Uri.Parse(l.Match.Groups[1].Value)));
};
// disable mouse handling for android to make emulator behavior more coherent
this.game.OnLoadContent += game => game.InputHandler.HandleMouse = false;
// set the game to fullscreen to cause the status bar to be hidden
this.game.GraphicsDeviceManager.IsFullScreen = true;
this.view = this.game.Services.GetService(typeof(View)) as View;

View file

@ -17,7 +17,7 @@
<AndroidResgenClass>Resource</AndroidResgenClass>
<GenerateSerializationAssemblies>Off</GenerateSerializationAssemblies>
<AndroidStoreUncompressedFileExtensions>.m4a</AndroidStoreUncompressedFileExtensions>
<TargetFrameworkVersion>v9.0</TargetFrameworkVersion>
<TargetFrameworkVersion>v10.0</TargetFrameworkVersion>
<AndroidManifest>Properties\AndroidManifest.xml</AndroidManifest>
<AndroidUseLatestPlatformSdk>false</AndroidUseLatestPlatformSdk>
<MonoAndroidResourcePrefix>Resources</MonoAndroidResourcePrefix>
@ -27,7 +27,7 @@
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<DebugType>portable</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\$(MonoGamePlatform)\$(Platform)\$(Configuration)\</OutputPath>
<DefineConstants>DEBUG;TRACE;ANDROID</DefineConstants>
@ -37,7 +37,7 @@
<AndroidLinkMode>None</AndroidLinkMode>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<DebugType>portable</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\$(MonoGamePlatform)\$(Platform)\$(Configuration)\</OutputPath>
<DefineConstants>TRACE;ANDROID</DefineConstants>
@ -69,10 +69,10 @@
<None Include="Properties\AndroidManifest.xml" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Coroutine" Version="2.0.0" />
<PackageReference Include="MonoGame.Content.Builder" Version="3.7.0.9" />
<PackageReference Include="MonoGame.Framework.Android" Version="3.7.1.189" />
<PackageReference Include="TextCopy" Version="3.0.2" />
<PackageReference Include="Coroutine" Version="2.1.1" />
<PackageReference Include="MonoGame.Content.Builder.Task" Version="3.8.0.1641" />
<PackageReference Include="MonoGame.Framework.Android" Version="3.8.0.1641" />
<PackageReference Include="TextCopy" Version="4.3.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Demos\Demos.csproj">

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="Demos.Android.Demos.Android" android:versionCode="1" android:versionName="1.0">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="28" />
<application android:label="Demos.Android"></application>
</manifest>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="de.ellpeck.mlem.demos.android" android:versionCode="1" android:versionName="1.0">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="29" />
<application android:label="MLEM Android Demos" />
</manifest>

View file

@ -4,12 +4,12 @@ using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("Demos.Android")]
[assembly: AssemblyTitle("MLEM Android Demos")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("Demos.Android")]
[assembly: AssemblyCopyright("Copyright © 2018")]
[assembly: AssemblyProduct("MLEM Android Demos")]
[assembly: AssemblyCopyright("Copyright © 2018")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: ComVisible(false)]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

View file

@ -2,7 +2,6 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
@ -15,7 +14,7 @@ namespace Demos.Android
{
[System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Android.Build.Tasks", "1.0.0.0")]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Android.Build.Tasks", "1.0.0.0")]
public partial class Resource
{

View file

@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Demos.Android</string>
</resources>
<string name="app_name">MLEM Android Demos</string>
</resources>

View file

@ -2,7 +2,9 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net462</TargetFramework>
<TargetFramework>net5.0</TargetFramework>
<ApplicationIcon>Icon.ico</ApplicationIcon>
<AssemblyName>MLEM Desktop Demos</AssemblyName>
</PropertyGroup>
<ItemGroup>
@ -13,13 +15,15 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="MonoGame.Content.Builder" Version="3.7.0.9" />
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.7.0.1708" />
<PackageReference Include="MonoGame.Content.Builder.Task" Version="3.8.0.1641" />
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.0.1641" />
</ItemGroup>
<ItemGroup>
<ItemGroup>
<MonoGameContentReference Include="..\Demos\Content\Content.mgcb" />
<Content Include="..\Demos\Content\*\**" />
<EmbeddedResource Include="Icon.ico" />
<EmbeddedResource Include="Icon.bmp" />
</ItemGroup>
</Project>

BIN
Demos.DesktopGL/Icon.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

BIN
Demos.DesktopGL/Icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

View file

@ -5,9 +5,9 @@ namespace Demos.DesktopGL {
public static class Program {
public static void Main() {
TextInputWrapper.Current = new TextInputWrapper.DesktopGl<TextInputEventArgs>((w, c) => w.TextInput += c);
using (var game = new GameImpl())
game.Run();
MlemPlatform.Current = new MlemPlatform.DesktopGl<TextInputEventArgs>((w, c) => w.TextInput += c);
using var game = new GameImpl();
game.Run();
}
}

View file

@ -11,7 +11,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="MonoGame.Framework.Portable" Version="3.7.1.189">
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.0.1641">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

View file

@ -25,7 +25,7 @@ namespace Demos {
public override void LoadContent() {
base.LoadContent();
this.group = new Group(Anchor.TopCenter, Vector2.One) {SetWidthBasedOnChildren = true};
this.group = new Group(Anchor.TopCenter, Vector2.One) {CanBeMoused = false};
this.group.AddChild(new Button(Anchor.AutoCenter, new Vector2(30, 10), "Next") {
OnPressed = e => {
this.current = (this.current + 1) % Easings.Length;

View file

@ -32,6 +32,10 @@ namespace Demos {
}
protected override void LoadContent() {
// TODO remove with MonoGame 3.8.1 https://github.com/MonoGame/MonoGame/issues/7298
this.GraphicsDeviceManager.PreferredBackBufferWidth = 1280;
this.GraphicsDeviceManager.PreferredBackBufferHeight = 720;
this.GraphicsDeviceManager.ApplyChanges();
base.LoadContent();
var tex = LoadContent<Texture2D>("Textures/Test");

View file

@ -6,6 +6,7 @@ using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using MLEM.Extensions;
using MLEM.Font;
using MLEM.Formatting;
using MLEM.Formatting.Codes;
using MLEM.Misc;
using MLEM.Startup;
@ -191,6 +192,14 @@ namespace Demos {
this.root.AddChild(new VerticalSpace(3));
this.root.AddChild(new Button(Anchor.AutoLeft, new Vector2(1, 10), "Disabled button", "This button can't be clicked or moved to using automatic navigation") {IsDisabled = true}).PositionOffset = new Vector2(0, 1);
const string alignText = "Paragraphs can have <c CornflowerBlue><l Left>left</l></c> aligned text, <c CornflowerBlue><l Right>right</l></c> aligned text and <c CornflowerBlue><l Center>center</l></c> aligned text.";
this.root.AddChild(new VerticalSpace(3));
var alignPar = this.root.AddChild(new Paragraph(Anchor.AutoLeft, 1, alignText));
alignPar.LinkAction = (l, c) => {
if (Enum.TryParse<TextAlignment>(c.Match.Groups[1].Value, out var alignment))
alignPar.Alignment = alignment;
};
this.root.AddChild(new VerticalSpace(3));
this.root.AddChild(new Paragraph(Anchor.AutoLeft, 1, "The code for this demo contains some examples for how to query element data. This is the output of that:"));

View file

@ -1,7 +0,0 @@
# Friends of MLEM
There are several other NuGet packages that work well in combination with MonoGame and MLEM. Here are some of them:
- [Contentless](https://github.com/Ellpeck/Contentless), a tool by Ellpeck that removes the need to add assets to the MonoGame Content Pipeline manually
- [GameBundle](https://github.com/Ellpeck/GameBundle), a tool by Ellpeck that packages MonoGame and other .NET Core applications into several distributable formats
- [Coroutine](https://github.com/Ellpeck/Coroutine), a package by Ellpeck that implements Unity-style coroutines for any project
- [MonoGame.Extended](https://github.com/craftworkgames/MonoGame.Extended), a package by craftworkgames that also provides several additional features for MonoGame

View file

@ -13,6 +13,4 @@
- name: Tiled Extensions
href: tiled_extensions.md
- name: MLEM.Startup
href: startup.md
- name: Friends of MLEM
href: friends.md
href: startup.md

View file

@ -34,24 +34,25 @@ protected override void Draw(GameTime gameTime) {
```
### Text Input
Text input is a bit weird in MonoGame. On Desktop devices, you have the `Window.TextInput` event that gets called automatically with the correct characters for the keys that you're pressing, even for non-American keyboards. However, this function doesn't just *not work* on other devices, it doesn't exist there at all. So, to make MLEM.Ui compatible with all devices without publishing a separate version for each MonoGame system, you have to set up the text input wrapper yourself, based on the system you're using MLEM.Ui with. This has to be done *before* initializing your `UiSystem`.
On desktop devices, MonoGame provides the `Window.TextInput` event that gets called automatically with the correct characters for the keys that you're pressing, even for non-American keyboards. However, this function doesn't exist on other devices. Similarly, MonoGame provides the `KeyboardInput` class for showing an on-screen keyboard on mobile devices and consoles, but not on desktop.
To make MLEM compatible with all devices without publishing a separate version for each MonoGame platform, you have to set up the `MlemPlatform` class based on the system you're using MLEM.Ui with. This has to be done *before* initializing your `UiSystem`.
DesktopGL:
```cs
TextInputWrapper.Current = new TextInputWrapper.DesktopGl<TextInputEventArgs>((w, c) => w.TextInput += c);
MlemPlatform.Current = new MlemPlatform.DesktopGl<TextInputEventArgs>((w, c) => w.TextInput += c);
```
Mobile devices and consoles:
```cs
TextInputWrapper.Current = new TextInputWrapper.Mobile();
MlemPlatform.Current = new MlemPlatform.Mobile(KeyboardInput.Show, l => this.StartActivity(new Intent(Intent.ActionView, Uri.Parse(l))));
```
Other systems. Note that, for this implementation, its `Update()` method also has to be called every game update tick. It only supports an American keyboard layout due to the way that it is implemented:
If you're not using text input, you can just set the platform to a stub one like so:
```cs
TextInputWrapper.Current = new TextInputWrapper.Primitive();
```
If you're not using text input, you can just set the wrapper to a stub one like so:
```cs
TextInputWrapper.Current = new TextInputWrapper.None();
MlemPlatform.Current = new MlemPlatform.None();
```
Initializing the platform in this way also allows for links in paragraphs to be clickable, causing a browser or explorer window to be opened on desktop or mobile devices.
For more info on MLEM's platform-related code, you can also check out MlemPlatform's [documentation](https://mlem.ellpeck.de/api/MLEM.Misc.MlemPlatform).
## Setting the style
By default, MLEM.Ui's controls look pretty bland, since it doesn't ship with any fonts or textures for any of its controls. To change the style of your ui, simply expand your `new UntexturedStyle(this.SpriteBatch)` call to include fonts and textures of your choosing, for example:

View file

@ -8,6 +8,14 @@
- See the source code on [GitHub](https://github.com/Ellpeck/MLEM)
- See tutorials and API documentation on this website
- Check out [the demos](https://github.com/Ellpeck/MLEM/tree/main/Demos) on [Desktop](https://github.com/Ellpeck/MLEM/tree/main/Demos.DesktopGL) or [Android](https://github.com/Ellpeck/MLEM/tree/main/Demos.Android)
- See [the changelog](https://github.com/Ellpeck/MLEM/blob/main/CHANGELOG.md) for information on updates
# Made with MLEM
- [A Breath of Spring Air](https://ellpeck.itch.io/a-breath-of-spring-air), a short platformer ([Source](https://git.ellpeck.de/Ellpeck/GreatSpringGameJam))
- [Don't Wake Up](https://ellpeck.itch.io/dont-wake-up), a short puzzle game ([Source](https://github.com/Ellpeck/DontLetGo))
- [Tiny Life](https://tinylifegame.com), an isometric life simulation game ([Modding API](https://github.com/Ellpeck/TinyLifeExampleMod))
If you created a game with the help of MLEM, you can get it added to this list by submitting it on the [issue tracker](https://github.com/Ellpeck/MLEM/issues). If its source is public, other people will be able to use your project as an example, too!
# Gallery
Here are some images that show a couple of MLEM's features.
@ -16,4 +24,12 @@ MLEM.Ui in action:
<img src="Ui.gif">
MLEM's text formatting system:
<img src="Formatting.png">
<img src="Formatting.png">
# Friends of MLEM
There are several other NuGet packages and tools that work well in combination with MonoGame and MLEM. Here are some of them:
- [Contentless](https://github.com/Ellpeck/Contentless), a tool that removes the need to add assets to the MonoGame Content Pipeline manually
- [GameBundle](https://github.com/Ellpeck/GameBundle), a tool that packages MonoGame and other .NET Core applications into several distributable formats
- [ButlerDotNet](https://github.com/Ellpeck/ButlerDotNet), a tool that automatically downloads and invokes itch.io's butler
- [MonoGame.Extended](https://github.com/craftworkgames/MonoGame.Extended), a package that also provides several additional features for MonoGame
- [Coroutine](https://github.com/Ellpeck/Coroutine), a package that implements Unity-style coroutines for any project

11
Jenkinsfile vendored
View file

@ -4,7 +4,8 @@ pipeline {
stage('Cake Build') {
steps {
sh 'dotnet tool restore'
sh 'dotnet dotnet-cake -Target=Publish -Branch=' + env.BRANCH_NAME
// we use xvfb to allow for graphics-dependent tests
sh 'xvfb-run -a dotnet dotnet-cake --Target=Publish --Branch=' + env.BRANCH_NAME
}
}
stage('Document') {
@ -12,11 +13,17 @@ pipeline {
branch 'release'
}
steps {
sh 'dotnet dotnet-cake -Target=Document'
sh 'dotnet dotnet-cake --Target=Document'
sh 'cp Docs/_site/** /var/www/MLEM/ -r'
}
}
}
post {
always {
nunit testResultsPattern: '**/TestResults.xml'
cobertura coberturaReportFile: '**/coverage.cobertura.xml'
}
}
environment {
BAGET = credentials('3db850d0-e6b5-43d5-b607-d180f4eab676')
NUGET = credentials('e1bf7f6c-6047-4f7e-b639-15240a8f8351')

View file

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2019 Ellpeck
Copyright (c) 2019-2021 Ellpeck
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View file

@ -13,6 +13,7 @@ namespace MLEM.Data.Content {
public class RawContentManager : ContentManager, IGameComponent {
private static readonly RawContentReader[] Readers = AppDomain.CurrentDomain.GetAssemblies()
.Where(a => !a.IsDynamic)
.SelectMany(a => a.GetExportedTypes())
.Where(t => t.IsSubclassOf(typeof(RawContentReader)) && !t.IsAbstract)
.Select(t => t.GetConstructor(Type.EmptyTypes).Invoke(null))

View file

@ -78,8 +78,8 @@ namespace MLEM.Data {
if (match.Groups[8].Success) {
for (var i = 0; i < match.Groups[8].Captures.Count; i++) {
region.SetData(match.Groups[8].Captures[i].Value, new Vector2(
float.Parse(match.Groups[9].Captures[i].Value) - (pivotRelative ? 0 : loc.X),
float.Parse(match.Groups[10].Captures[i].Value) - (pivotRelative ? 0 : loc.Y)));
float.Parse(match.Groups[9].Captures[i].Value, CultureInfo.InvariantCulture) - (pivotRelative ? 0 : loc.X),
float.Parse(match.Groups[10].Captures[i].Value, CultureInfo.InvariantCulture) - (pivotRelative ? 0 : loc.Y)));
}
}
atlas.regions.Add(name, region);

315
MLEM.Data/DynamicEnum.cs Normal file
View file

@ -0,0 +1,315 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Reflection;
using MLEM.Data.Json;
using Newtonsoft.Json;
namespace MLEM.Data {
/// <summary>
/// A dynamic enum is a class that represents enum-like single-instance value behavior with additional capabilities, including dynamic addition of new arbitrary values.
/// A dynamic enum uses <see cref="BigInteger"/> as its underlying type, allowing for an arbitrary number of enum values to be created, even when a <see cref="FlagsAttribute"/>-like structure is used that would only allow for up to 64 values in a regular enum.
/// All enum operations including <see cref="And{T}"/>, <see cref="Or{T}"/>, <see cref="Xor{T}"/> and <see cref="Neg{T}"/> are supported and can be implemented in derived classes using operator overloads.
/// To create a custom dynamic enum, simply create a class that extends <see cref="DynamicEnum"/>. New values can then be added using <see cref="Add{T}"/>, <see cref="AddValue{T}"/> or <see cref="AddFlag{T}"/>.
/// </summary>
/// <remarks>
/// To include enum-like operator overloads in a dynamic enum named MyEnum, the following code can be used:
/// <code>
/// public static implicit operator BigInteger(MyEnum value) => GetValue(value);
/// public static implicit operator MyEnum(BigInteger value) => GetEnumValue&lt;MyEnum&gt;(value);
/// public static MyEnum operator |(MyEnum left, MyEnum right) => Or(left, right);
/// public static MyEnum operator &amp;(MyEnum left, MyEnum right) => And(left, right);
/// public static MyEnum operator ^(MyEnum left, MyEnum right) => Xor(left, right);
/// public static MyEnum operator ~(MyEnum value) => Neg(value);
/// </code>
/// </remarks>
[JsonConverter(typeof(DynamicEnumConverter))]
public abstract class DynamicEnum {
private static readonly Dictionary<Type, Dictionary<BigInteger, DynamicEnum>> Values = new Dictionary<Type, Dictionary<BigInteger, DynamicEnum>>();
private static readonly Dictionary<Type, Dictionary<BigInteger, DynamicEnum>> FlagCache = new Dictionary<Type, Dictionary<BigInteger, DynamicEnum>>();
private static readonly Dictionary<Type, Dictionary<string, DynamicEnum>> ParseCache = new Dictionary<Type, Dictionary<string, DynamicEnum>>();
private readonly BigInteger value;
private Dictionary<DynamicEnum, bool> allFlagsCache;
private Dictionary<DynamicEnum, bool> anyFlagsCache;
private string name;
/// <summary>
/// Creates a new dynamic enum instance.
/// This constructor is protected as it is only invoked via reflection.
/// </summary>
/// <param name="name">The name of the enum value</param>
/// <param name="value">The value</param>
protected DynamicEnum(string name, BigInteger value) {
this.value = value;
this.name = name;
}
/// <summary>
/// Returns true if this enum value has ALL of the given <see cref="DynamicEnum"/> flags on it.
/// This operation is equivalent to <see cref="Enum.HasFlag"/>.
/// </summary>
/// <seealso cref="HasAnyFlag"/>
/// <param name="flags">The flags to query</param>
/// <returns>True if all of the flags are present, false otherwise</returns>
public bool HasFlag(DynamicEnum flags) {
if (this.allFlagsCache == null)
this.allFlagsCache = new Dictionary<DynamicEnum, bool>();
if (!this.allFlagsCache.TryGetValue(flags, out var ret)) {
// & is very memory-intensive, so we cache the return value
ret = (GetValue(this) & GetValue(flags)) == GetValue(flags);
this.allFlagsCache.Add(flags, ret);
}
return ret;
}
/// <summary>
/// Returns true if this enum value has ANY of the given <see cref="DynamicEnum"/> flags on it
/// </summary>
/// <seealso cref="HasFlag"/>
/// <param name="flags">The flags to query</param>
/// <returns>True if one of the flags is present, false otherwise</returns>
public bool HasAnyFlag(DynamicEnum flags) {
if (this.anyFlagsCache == null)
this.anyFlagsCache = new Dictionary<DynamicEnum, bool>();
if (!this.anyFlagsCache.TryGetValue(flags, out var ret)) {
// & is very memory-intensive, so we cache the return value
ret = (GetValue(this) & GetValue(flags)) != 0;
this.anyFlagsCache.Add(flags, ret);
}
return ret;
}
/// <inheritdoc />
public override string ToString() {
if (this.name == null) {
var included = new List<DynamicEnum>();
if (GetValue(this) != 0) {
foreach (var v in GetValues(this.GetType())) {
if (this.HasFlag(v) && GetValue(v) != 0)
included.Add(v);
}
}
this.name = included.Count > 0 ? string.Join(" | ", included) : GetValue(this).ToString();
}
return this.name;
}
/// <summary>
/// Adds a new enum value to the given enum type <typeparamref name="T"/>
/// </summary>
/// <param name="name">The name of the enum value to add</param>
/// <param name="value">The value to add</param>
/// <typeparam name="T">The type to add this value to</typeparam>
/// <returns>The newly created enum value</returns>
/// <exception cref="ArgumentException">Thrown if the name or value passed are already present</exception>
public static T Add<T>(string name, BigInteger value) where T : DynamicEnum {
if (!Values.TryGetValue(typeof(T), out var dict)) {
dict = new Dictionary<BigInteger, DynamicEnum>();
Values.Add(typeof(T), dict);
}
// cached parsed values and names might be incomplete with new values
FlagCache.Remove(typeof(T));
ParseCache.Remove(typeof(T));
if (dict.ContainsKey(value))
throw new ArgumentException($"Duplicate value {value}", nameof(value));
if (dict.Values.Any(v => v.name == name))
throw new ArgumentException($"Duplicate name {name}", nameof(name));
var ret = Construct(typeof(T), name, value);
dict.Add(value, ret);
return (T) ret;
}
/// <summary>
/// Adds a new enum value to the given enum type <typeparamref name="T"/>.
/// This method differs from <see cref="Add{T}"/> in that it automatically determines a value.
/// The value determined will be the next free number in a sequence, which represents the default behavior in an enum if enum values are not explicitly numbered.
/// </summary>
/// <param name="name">The name of the enum value to add</param>
/// <typeparam name="T">The type to add this value to</typeparam>
/// <returns>The newly created enum value</returns>
public static T AddValue<T>(string name) where T : DynamicEnum {
BigInteger value = 0;
if (Values.TryGetValue(typeof(T), out var defined)) {
while (defined.ContainsKey(value))
value++;
}
return Add<T>(name, value);
}
/// <summary>
/// Adds a new flag enum value to the given enum type <typeparamref name="T"/>.
/// This method differs from <see cref="Add{T}"/> in that it automatically determines a value.
/// The value determined will be the next free power of two, allowing enum values to be combined using bitwise operations to create <see cref="FlagsAttribute"/>-like behavior.
/// </summary>
/// <param name="name">The name of the enum value to add</param>
/// <typeparam name="T">The type to add this value to</typeparam>
/// <returns>The newly created enum value</returns>
public static T AddFlag<T>(string name) where T : DynamicEnum {
BigInteger value = 0;
if (Values.TryGetValue(typeof(T), out var defined)) {
while (defined.ContainsKey(value))
value <<= 1;
}
return Add<T>(name, value);
}
/// <summary>
/// Returns a collection of all of the enum values that are explicitly defined for the given dynamic enum type <typeparamref name="T"/>.
/// A value counts as explicitly defined if it has been added using <see cref="Add{T}"/>, <see cref="AddValue{T}"/> or <see cref="AddFlag{T}"/>.
/// </summary>
/// <typeparam name="T">The type whose values to get</typeparam>
/// <returns>The defined values for the given type</returns>
public static IEnumerable<T> GetValues<T>() where T : DynamicEnum {
return GetValues(typeof(T)).Cast<T>();
}
/// <summary>
/// Returns a collection of all of the enum values that are explicitly defined for the given dynamic enum type <paramref name="type"/>.
/// A value counts as explicitly defined if it has been added using <see cref="Add{T}"/>, <see cref="AddValue{T}"/> or <see cref="AddFlag{T}"/>.
/// </summary>
/// <param name="type">The type whose values to get</param>
/// <returns>The defined values for the given type</returns>
public static IEnumerable<DynamicEnum> GetValues(Type type) {
return Values.TryGetValue(type, out var ret) ? ret.Values : Enumerable.Empty<DynamicEnum>();
}
/// <summary>
/// Returns the bitwise OR (|) combination of the two dynamic enum values
/// </summary>
/// <param name="left">The left value</param>
/// <param name="right">The right value</param>
/// <typeparam name="T">The type of the values</typeparam>
/// <returns>The bitwise OR (|) combination</returns>
public static T Or<T>(T left, T right) where T : DynamicEnum {
return GetEnumValue<T>(GetValue(left) | GetValue(right));
}
/// <summary>
/// Returns the bitwise AND (&amp;) combination of the two dynamic enum values
/// </summary>
/// <param name="left">The left value</param>
/// <param name="right">The right value</param>
/// <typeparam name="T">The type of the values</typeparam>
/// <returns>The bitwise AND (&amp;) combination</returns>
public static T And<T>(T left, T right) where T : DynamicEnum {
return GetEnumValue<T>(GetValue(left) & GetValue(right));
}
/// <summary>
/// Returns the bitwise XOR (^) combination of the two dynamic enum values
/// </summary>
/// <param name="left">The left value</param>
/// <param name="right">The right value</param>
/// <typeparam name="T">The type of the values</typeparam>
/// <returns>The bitwise XOR (^) combination</returns>
public static T Xor<T>(T left, T right) where T : DynamicEnum {
return GetEnumValue<T>(GetValue(left) ^ GetValue(right));
}
/// <summary>
/// Returns the bitwise NEG (~) combination of the dynamic enum value
/// </summary>
/// <param name="value">The value</param>
/// <typeparam name="T">The type of the values</typeparam>
/// <returns>The bitwise NEG (~) value</returns>
public static T Neg<T>(T value) where T : DynamicEnum {
return GetEnumValue<T>(~GetValue(value));
}
/// <summary>
/// Returns the <see cref="BigInteger"/> representation of the given dynamic enum value
/// </summary>
/// <param name="value">The value whose number representation to get</param>
/// <returns>The value's number representation</returns>
public static BigInteger GetValue(DynamicEnum value) {
return value?.value ?? 0;
}
/// <summary>
/// Returns the defined or combined dynamic enum value for the given <see cref="BigInteger"/> representation
/// </summary>
/// <param name="value">The value whose dynamic enum value to get</param>
/// <typeparam name="T">The type that the returned dynamic enum should have</typeparam>
/// <returns>The defined or combined dynamic enum value</returns>
public static T GetEnumValue<T>(BigInteger value) where T : DynamicEnum {
return (T) GetEnumValue(typeof(T), value);
}
/// <summary>
/// Returns the defined or combined dynamic enum value for the given <see cref="BigInteger"/> representation
/// </summary>
/// <param name="type">The type that the returned dynamic enum should have</param>
/// <param name="value">The value whose dynamic enum value to get</param>
/// <returns>The defined or combined dynamic enum value</returns>
public static DynamicEnum GetEnumValue(Type type, BigInteger value) {
// get the defined value if it exists
if (Values.TryGetValue(type, out var values) && values.TryGetValue(value, out var defined))
return defined;
// otherwise, cache the combined value
if (!FlagCache.TryGetValue(type, out var cache)) {
cache = new Dictionary<BigInteger, DynamicEnum>();
FlagCache.Add(type, cache);
}
if (!cache.TryGetValue(value, out var combined)) {
combined = Construct(type, null, value);
cache.Add(value, combined);
}
return combined;
}
/// <summary>
/// Parses the given <see cref="string"/> into a dynamic enum value and returns the result.
/// This method supports defined enum values as well as values combined using the pipe (|) character and any number of spaces.
/// If no enum value can be parsed, null is returned.
/// </summary>
/// <param name="strg">The string to parse into a dynamic enum value</param>
/// <typeparam name="T">The type of the dynamic enum value to parse</typeparam>
/// <returns>The parsed enum value, or null if parsing fails</returns>
public static T Parse<T>(string strg) where T : DynamicEnum {
return (T) Parse(typeof(T), strg);
}
/// <summary>
/// Parses the given <see cref="string"/> into a dynamic enum value and returns the result.
/// This method supports defined enum values as well as values combined using the pipe (|) character and any number of spaces.
/// If no enum value can be parsed, null is returned. /// </summary>
/// <param name="type">The type of the dynamic enum value to parse</param>
/// <param name="strg">The string to parse into a dynamic enum value</param>
/// <returns>The parsed enum value, or null if parsing fails</returns>
public static DynamicEnum Parse(Type type, string strg) {
if (!ParseCache.TryGetValue(type, out var cache)) {
cache = new Dictionary<string, DynamicEnum>();
ParseCache.Add(type, cache);
}
if (!cache.TryGetValue(strg, out var cached)) {
BigInteger? accum = null;
foreach (var val in strg.Split('|')) {
foreach (var defined in GetValues(type)) {
if (defined.name == val.Trim()) {
accum = (accum ?? 0) | GetValue(defined);
break;
}
}
}
if (accum != null)
cached = GetEnumValue(type, accum.Value);
cache.Add(strg, cached);
}
return cached;
}
private static DynamicEnum Construct(Type type, string name, BigInteger value) {
var constructor = type.GetConstructor(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new[] {typeof(string), typeof(BigInteger)}, null);
return (DynamicEnum) constructor.Invoke(new object[] {name, value});
}
}
}

View file

@ -0,0 +1,19 @@
using System;
using Newtonsoft.Json;
namespace MLEM.Data.Json {
/// <inheritdoc />
public class DynamicEnumConverter : JsonConverter<DynamicEnum> {
/// <inheritdoc />
public override void WriteJson(JsonWriter writer, DynamicEnum value, JsonSerializer serializer) {
writer.WriteValue(value.ToString());
}
/// <inheritdoc />
public override DynamicEnum ReadJson(JsonReader reader, Type objectType, DynamicEnum existingValue, bool hasExistingValue, JsonSerializer serializer) {
return DynamicEnum.Parse(objectType, reader.Value.ToString());
}
}
}

View file

@ -12,7 +12,7 @@ namespace MLEM.Data.Json {
/// An array of all of the <see cref="JsonConverter"/>s that are part of MLEM.Data
/// </summary>
public static readonly JsonConverter[] Converters = typeof(JsonConverters).Assembly.GetExportedTypes()
.Where(t => t.IsSubclassOf(typeof(JsonConverter)))
.Where(t => t.IsSubclassOf(typeof(JsonConverter)) && !t.IsGenericType)
.Select(t => t.GetConstructor(Type.EmptyTypes).Invoke(null)).Cast<JsonConverter>().ToArray();
/// <summary>

View file

@ -0,0 +1,61 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Newtonsoft.Json;
namespace MLEM.Data.Json {
/// <summary>
/// A <see cref="JsonConverter{T}"/> that doesn't actually serialize the object, but instead serializes the name given to it by the underlying <see cref="Dictionary{T,T}"/>.
/// Optionally, the name of a <see cref="Dictionary{TKey,TValue}"/> can be passed to this converter when used in the <see cref="JsonConverterAttribute"/> by passing the arguments for the <see cref="StaticJsonConverter{T}(Type,string)"/> constructor as <see cref="JsonConverterAttribute.ConverterParameters"/>.
/// </summary>
/// <typeparam name="T">The type of the object to convert</typeparam>
public class StaticJsonConverter<T> : JsonConverter<T> {
private readonly Dictionary<string, T> entries;
private readonly Dictionary<T, string> inverse;
/// <summary>
/// Creates a new static json converter using the given underlying <see cref="Dictionary{T,T}"/>.
/// </summary>
/// <param name="entries">The dictionary to use</param>
public StaticJsonConverter(Dictionary<string, T> entries) {
this.entries = entries;
this.inverse = entries.ToDictionary(kv => kv.Value, kv => kv.Key);
}
/// <summary>
/// Creates a new static json converter by finding the underlying <see cref="Dictionary{TKey,TValue}"/> from the given type and member name
/// </summary>
/// <param name="type">The type that the dictionary is declared in</param>
/// <param name="memberName">The name of the dictionary itself</param>
public StaticJsonConverter(Type type, string memberName) :
this(GetEntries(type, memberName)) {
}
/// <inheritdoc />
public override void WriteJson(JsonWriter writer, T value, JsonSerializer serializer) {
if (!this.inverse.TryGetValue(value, out var key))
throw new InvalidOperationException($"Cannot write {value} that is not a registered entry");
writer.WriteValue(key);
}
/// <inheritdoc />
public override T ReadJson(JsonReader reader, Type objectType, T existingValue, bool hasExistingValue, JsonSerializer serializer) {
var val = reader.Value?.ToString();
if (val == null)
return default;
this.entries.TryGetValue(val, out var ret);
return ret;
}
private static Dictionary<string, T> GetEntries(Type type, string memberName) {
const BindingFlags flags = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;
var value = type.GetProperty(memberName, flags)?.GetValue(null) ?? type.GetField(memberName, flags)?.GetValue(null);
if (value == null)
throw new ArgumentException($"There is no property or field value for name {memberName}", nameof(memberName));
return value as Dictionary<string, T> ?? throw new InvalidCastException($"{value} is not of expected type {typeof(T)}");
}
}
}

View file

@ -7,27 +7,30 @@
<PropertyGroup>
<Authors>Ellpeck</Authors>
<Description>Simple data and network handling for MLEM Library for Extending MonoGame</Description>
<PackageReleaseNotes>See the full changelog at https://github.com/Ellpeck/MLEM/blob/main/CHANGELOG.md</PackageReleaseNotes>
<PackageTags>monogame ellpeck mlem utility extensions data network serialize</PackageTags>
<PackageProjectUrl>https://mlem.ellpeck.de/</PackageProjectUrl>
<RepositoryUrl>https://github.com/Ellpeck/MLEM</RepositoryUrl>
<PackageLicenseUrl>https://github.com/Ellpeck/MLEM/blob/main/LICENSE</PackageLicenseUrl>
<PackageIconUrl>https://raw.githubusercontent.com/Ellpeck/MLEM/main/Media/Logo.png</PackageIconUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageIcon>Logo.png</PackageIcon>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<ProjectReference Include="..\MLEM\MLEM.csproj" />
<PackageReference Include="Lidgren.Network" Version="1.0.2">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="MonoGame.Framework.Portable" Version="3.7.1.189">
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.0.1641">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="Newtonsoft.Json.Bson" Version="1.0.2">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MLEM\MLEM.csproj" />
<None Include="../Media/Logo.png" Pack="true" PackagePath="" />
</ItemGroup>
</Project>

View file

@ -113,7 +113,7 @@ namespace MLEM.Data {
if (this.forceSquare)
width = height = Math.Max(width, height);
this.PackedTexture = new Texture2D(device, width, height);
// copy texture data onto the packed texture
stopwatch.Restart();
using (var data = this.PackedTexture.GetTextureData()) {

View file

@ -1,3 +1,5 @@
using Microsoft.Xna.Framework;
using MLEM.Extensions;
using MonoGame.Extended;
namespace MLEM.Extended.Extensions {
@ -24,5 +26,10 @@ namespace MLEM.Extended.Extensions {
return new Misc.RectangleF(rect.X, rect.Y, rect.Width, rect.Height);
}
/// <inheritdoc cref="MLEM.Extensions.NumberExtensions.Penetrate"/>
public static bool Penetrate(this RectangleF rect, RectangleF other, out Vector2 normal, out float penetration) {
return rect.ToMlem().Penetrate(other.ToMlem(), out normal, out penetration);
}
}
}

View file

@ -38,31 +38,11 @@ namespace MLEM.Extended.Font {
return region != null ? new Vector2(region.XAdvance, region.Height) : Vector2.Zero;
}
/// <inheritdoc/>
public override void DrawString(SpriteBatch batch, string text, Vector2 position, Color color) {
batch.DrawString(this.Font, text, position, color);
}
/// <inheritdoc/>
public override void DrawString(SpriteBatch batch, string text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects effects, float layerDepth) {
batch.DrawString(this.Font, text, position, color, rotation, origin, scale, effects, layerDepth);
}
/// <inheritdoc/>
public override void DrawString(SpriteBatch batch, string text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth) {
batch.DrawString(this.Font, text, position, color, rotation, origin, scale, effects, layerDepth);
}
/// <inheritdoc/>
public override void DrawString(SpriteBatch batch, StringBuilder text, Vector2 position, Color color) {
batch.DrawString(this.Font, text, position, color);
}
/// <inheritdoc/>
public override void DrawString(SpriteBatch batch, StringBuilder text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects effects, float layerDepth) {
batch.DrawString(this.Font, text, position, color, rotation, origin, scale, effects, layerDepth);
}
/// <inheritdoc/>
public override void DrawString(SpriteBatch batch, StringBuilder text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth) {
batch.DrawString(this.Font, text, position, color, rotation, origin, scale, effects, layerDepth);

View file

@ -0,0 +1,55 @@
using System.Text;
using FontStashSharp;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using MLEM.Extensions;
using MLEM.Font;
namespace MLEM.Extended.Font {
/// <inheritdoc/>
public class GenericStashFont : GenericFont {
/// <summary>
/// The <see cref="SpriteFontBase"/> that is being wrapped by this generic font
/// </summary>
public readonly SpriteFontBase Font;
/// <inheritdoc />
public override GenericFont Bold { get; }
/// <inheritdoc />
public override GenericFont Italic { get; }
/// <inheritdoc />
public override float LineHeight { get; }
/// <summary>
/// Creates a new generic font using <see cref="SpriteFontBase"/>.
/// Optionally, a bold and italic version of the font can be supplied.
/// </summary>
/// <param name="font">The font to wrap</param>
/// <param name="bold">A bold version of the font</param>
/// <param name="italic">An italic version of the font</param>
public GenericStashFont(SpriteFontBase font, SpriteFontBase bold = null, SpriteFontBase italic = null) {
this.Font = font;
// SpriteFontBase provides no line height, so we measure the height of a new line for most fonts
// This doesn't work with static sprite fonts, but their size is always the one we calculate here
this.LineHeight = font is StaticSpriteFont s ? s.FontSize + s.LineSpacing : font.MeasureString("\n").Y;
this.Bold = bold != null ? new GenericStashFont(bold) : this;
this.Italic = italic != null ? new GenericStashFont(italic) : this;
}
/// <inheritdoc />
public override void DrawString(SpriteBatch batch, string text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth) {
this.Font.DrawText(batch, text, position, color, scale, rotation, origin, layerDepth);
}
/// <inheritdoc />
public override void DrawString(SpriteBatch batch, StringBuilder text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth) {
this.Font.DrawText(batch, text, position, color, scale, rotation, origin, layerDepth);
}
/// <inheritdoc />
protected override Vector2 MeasureChar(char c) {
return this.Font.MeasureString(c.ToCachedString());
}
}
}

View file

@ -6,27 +6,33 @@
<PropertyGroup>
<Authors>Ellpeck</Authors>
<Description>MLEM Library for Extending MonoGame extension that ties in with MonoGame.Extended</Description>
<Description>MLEM Library for Extending MonoGame extension that ties in with MonoGame.Extended and other MonoGame libraries</Description>
<PackageReleaseNotes>See the full changelog at https://github.com/Ellpeck/MLEM/blob/main/CHANGELOG.md</PackageReleaseNotes>
<PackageTags>monogame ellpeck mlem utility extensions monogame.extended extended</PackageTags>
<PackageProjectUrl>https://mlem.ellpeck.de/</PackageProjectUrl>
<RepositoryUrl>https://github.com/Ellpeck/MLEM</RepositoryUrl>
<PackageLicenseUrl>https://github.com/Ellpeck/MLEM/blob/main/LICENSE</PackageLicenseUrl>
<PackageIconUrl>https://raw.githubusercontent.com/Ellpeck/MLEM/main/Media/Logo.png</PackageIconUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageIcon>Logo.png</PackageIcon>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\MLEM\MLEM.csproj" />
<PackageReference Include="MonoGame.Extended" Version="3.8.0">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="MonoGame.Extended.Tiled" Version="3.8.0">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="MonoGame.Framework.Portable" Version="3.7.1.189">
<PackageReference Include="FontStashSharp.MonoGame" Version="0.9.2">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.0.1641">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MLEM\MLEM.csproj" />
<None Include="../Media/Logo.png" Pack="true" PackagePath=""/>
</ItemGroup>
</Project>

View file

@ -24,7 +24,7 @@ namespace MLEM.Extended.Tiled {
/// Creates a new individual tiled map renderer using the given map and depth function
/// </summary>
/// <param name="map">The map to use</param>
/// <param name="depthFunction">The depth function to use</param>
/// <param name="depthFunction">The depth function to use. Defaults to a function that assigns a depth of 0 to every tile.</param>
public IndividualTiledMapRenderer(TiledMap map = null, GetDepth depthFunction = null) {
if (map != null)
this.SetMap(map, depthFunction);
@ -34,7 +34,7 @@ namespace MLEM.Extended.Tiled {
/// Sets this individual tiled map renderer's map and depth function
/// </summary>
/// <param name="map">The map to use</param>
/// <param name="depthFunction">The depth function to use</param>
/// <param name="depthFunction">The depth function to use. Defaults to a function that assigns a depth of 0 to every tile.</param>
public void SetMap(TiledMap map, GetDepth depthFunction = null) {
this.map = map;
this.depthFunction = depthFunction ?? ((tile, layer, layerIndex, position) => 0);
@ -84,7 +84,7 @@ namespace MLEM.Extended.Tiled {
/// </summary>
/// <param name="batch">The sprite batch to use</param>
/// <param name="frustum">The area that is visible, in pixel space.</param>
/// <param name="drawFunction">The draw function to use, or null for default</param>
/// <param name="drawFunction">The draw function to use, or null to use <see cref="DefaultDraw"/></param>
public void Draw(SpriteBatch batch, RectangleF? frustum = null, DrawDelegate drawFunction = null) {
for (var i = 0; i < this.map.TileLayers.Count; i++) {
if (this.map.TileLayers[i].IsVisible)
@ -99,8 +99,9 @@ namespace MLEM.Extended.Tiled {
/// <param name="batch">The sprite batch to use</param>
/// <param name="layerIndex">The index of the layer in <see cref="TiledMap.TileLayers"/></param>
/// <param name="frustum">The area that is visible, in pixel space.</param>
/// <param name="drawFunction">The draw function to use, or null for default</param>
/// <param name="drawFunction">The draw function to use, or null to use <see cref="DefaultDraw"/></param>
public void DrawLayer(SpriteBatch batch, int layerIndex, RectangleF? frustum = null, DrawDelegate drawFunction = null) {
var draw = drawFunction ?? DefaultDraw;
var frust = frustum ?? new RectangleF(0, 0, float.MaxValue, float.MaxValue);
var minX = Math.Max(0, frust.Left / this.map.TileWidth).Floor();
var minY = Math.Max(0, frust.Top / this.map.TileHeight).Floor();
@ -110,7 +111,7 @@ namespace MLEM.Extended.Tiled {
for (var y = minY; y < maxY; y++) {
var info = this.drawInfos[layerIndex, x, y];
if (info != null)
info.Draw(batch, drawFunction);
draw(batch, info);
}
}
}
@ -124,6 +125,18 @@ namespace MLEM.Extended.Tiled {
animation.Update(time);
}
/// <summary>
/// The default implementation of <see cref="DrawDelegate"/> that is used by <see cref="SetMap"/> if no custom draw function is passed
/// </summary>
/// <param name="batch">The sprite batch to use for drawing</param>
/// <param name="info">The <see cref="TileDrawInfo"/> to draw</param>
public static void DefaultDraw(SpriteBatch batch, TileDrawInfo info) {
var region = info.Tileset.GetTextureRegion(info.TilesetTile);
var effects = info.Tile.GetSpriteEffects();
var drawPos = new Vector2(info.Position.X * info.Renderer.map.TileWidth, info.Position.Y * info.Renderer.map.TileHeight);
batch.Draw(info.Tileset.Texture, drawPos, region, Color.White, 0, Vector2.Zero, 1, effects, info.Depth);
}
/// <summary>
/// A delegate method used for <see cref="IndividualTiledMapRenderer.depthFunction"/>.
/// The idea is to return a depth (between 0 and 1) for the given tile that determines where in the sprite batch it should be rendererd.
@ -182,22 +195,6 @@ namespace MLEM.Extended.Tiled {
this.Depth = depth;
}
/// <summary>
/// Draws this tile draw info with the default settings.
/// </summary>
/// <param name="batch">The sprite batch to use for drawing</param>
/// <param name="drawFunction">The draw function used to draw, or null if there is no override</param>
public void Draw(SpriteBatch batch, DrawDelegate drawFunction) {
if (drawFunction == null) {
var region = this.Tileset.GetTextureRegion(this.TilesetTile);
var effects = this.Tile.GetSpriteEffects();
var drawPos = new Vector2(this.Position.X * this.Renderer.map.TileWidth, this.Position.Y * this.Renderer.map.TileHeight);
batch.Draw(this.Tileset.Texture, drawPos, region, Color.White, 0, Vector2.Zero, 1, effects, this.Depth);
} else {
drawFunction(batch, this);
}
}
}
}

View file

@ -5,6 +5,7 @@ using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using MonoGame.Extended;
using MonoGame.Extended.Tiled;
using static MonoGame.Extended.Tiled.TiledMapTileFlipFlags;
using ColorHelper = MLEM.Extensions.ColorHelper;
namespace MLEM.Extended.Tiled {
@ -54,7 +55,7 @@ namespace MLEM.Extended.Tiled {
/// <param name="key">The key by which to get a property</param>
/// <returns>The float property, or 0 if there is none</returns>
public static float GetFloat(this TiledMapProperties properties, string key) {
float.TryParse(properties.Get(key), NumberStyles.Number, NumberFormatInfo.InvariantInfo, out var val);
float.TryParse(properties.Get(key), NumberStyles.Number, CultureInfo.InvariantCulture, out var val);
return val;
}
@ -65,7 +66,7 @@ namespace MLEM.Extended.Tiled {
/// <param name="key">The key by which to get a property</param>
/// <returns>The int property, or 0 if there is none</returns>
public static int GetInt(this TiledMapProperties properties, string key) {
int.TryParse(properties.Get(key), NumberStyles.Number, NumberFormatInfo.InvariantInfo, out var val);
int.TryParse(properties.Get(key), NumberStyles.Number, CultureInfo.InvariantCulture, out var val);
return val;
}
@ -115,15 +116,7 @@ namespace MLEM.Extended.Tiled {
if (tile.IsBlank)
return null;
var localId = tile.GetLocalIdentifier(tileset, map);
var tilesetTile = tileset.Tiles.FirstOrDefault(t => t.LocalTileIdentifier == localId);
if (tilesetTile == null && createStub) {
var id = tile.GetLocalIdentifier(tileset, map);
if (!StubTilesetTiles.TryGetValue(id, out tilesetTile)) {
tilesetTile = new TiledMapTilesetTile(id);
StubTilesetTiles.Add(id, tilesetTile);
}
}
return tilesetTile;
return tileset.GetTilesetTile(localId, createStub);
}
/// <summary>
@ -141,6 +134,24 @@ namespace MLEM.Extended.Tiled {
return tileset.GetTilesetTile(tile, map, createStub);
}
/// <summary>
/// Gets the tileset tile on the given tileset for the given local id.
/// </summary>
/// <param name="tileset">The tileset</param>
/// <param name="localId">The tile's local id</param>
/// <param name="createStub">If a tileset tile has no special properties, there is no pre-made object for it. If this boolean is true, a stub object with no extra data will be created instead of returning null.</param>
/// <returns>null if the tile is blank or the tileset tile if there is one or createStub is true</returns>
public static TiledMapTilesetTile GetTilesetTile(this TiledMapTileset tileset, int localId, bool createStub = true) {
var tilesetTile = tileset.Tiles.FirstOrDefault(t => t.LocalTileIdentifier == localId);
if (tilesetTile == null && createStub) {
if (!StubTilesetTiles.TryGetValue(localId, out tilesetTile)) {
tilesetTile = new TiledMapTilesetTile(localId);
StubTilesetTiles.Add(localId, tilesetTile);
}
}
return tilesetTile;
}
/// <summary>
/// Gets the layer index of the layer with the given name in the <see cref="TiledMap.Layers"/> array.
/// </summary>
@ -165,6 +176,16 @@ namespace MLEM.Extended.Tiled {
return layer != null ? layer.GetTile(x, y) : default;
}
/// <summary>
/// Returns the tiled map tile at the given location on the layer with the given name.
/// </summary>
/// <param name="map">The map</param>
/// <param name="pos">The layer position to get the tile at</param>
/// <returns>The tile at the given location, or default if the layer does not exist</returns>
public static TiledMapTile GetTile(this TiledMap map, LayerPosition pos) {
return map.GetTile(pos.Layer, pos.X, pos.Y);
}
/// <summary>
/// Sets the tiled map tile at the given location to the given global tile identifier.
/// </summary>
@ -179,6 +200,42 @@ namespace MLEM.Extended.Tiled {
layer.SetTile((ushort) x, (ushort) y, (uint) globalTile);
}
/// <summary>
/// Sets the tiled map tile at the given location to the given tile from the given tileset.
/// If the passed <paramref name="tileset"/> or <paramref name="tile"/> is null, the tile at the location is removed instead.
/// </summary>
/// <param name="map">The map</param>
/// <param name="layerName">The name of the layer</param>
/// <param name="x">The x coordinate</param>
/// <param name="y">The y coordinate</param>
/// <param name="tileset">The tileset to use, or null to remove the tile</param>
/// <param name="tile">The tile to place, from the given tileset, or null to remove the tile</param>
public static void SetTile(this TiledMap map, string layerName, int x, int y, TiledMapTileset tileset, TiledMapTilesetTile tile) {
map.SetTile(layerName, x, y, tileset != null && tile != null ? tile.GetGlobalIdentifier(tileset, map) : 0);
}
/// <summary>
/// Sets the tiled map tile at the given location to the given global tile identifier.
/// </summary>
/// <param name="map">The map</param>
/// <param name="pos">The layer position</param>
/// <param name="globalTile">The tile's global identifier to set</param>
public static void SetTile(this TiledMap map, LayerPosition pos, int globalTile) {
map.SetTile(pos.Layer, pos.X, pos.Y, globalTile);
}
/// <summary>
/// Sets the tiled map tile at the given location to the given tile from the given tileset.
/// If the passed <paramref name="tileset"/> or <paramref name="tile"/> is null, the tile at the location is removed instead.
/// </summary>
/// <param name="map">The map</param>
/// <param name="pos">The layer position</param>
/// <param name="tileset">The tileset to use, or null to remove the tile</param>
/// <param name="tile">The tile to place, from the given tileset, or null to remove the tile</param>
public static void SetTile(this TiledMap map, LayerPosition pos, TiledMapTileset tileset, TiledMapTilesetTile tile) {
map.SetTile(pos.Layer, pos.X, pos.Y, tileset, tile);
}
/// <summary>
/// For an x and y coordinate, returns an enumerable of all of the tiles on each of the map's <see cref="TiledMap.TileLayers"/>.
/// </summary>
@ -212,11 +269,18 @@ namespace MLEM.Extended.Tiled {
/// <param name="obj">The object whose area to get</param>
/// <param name="map">The map</param>
/// <param name="position">The position to add to the object's position</param>
/// <param name="flipFlags">The flipping of the tile that this object belongs to. If set, the returned area will be "flipped" in the tile's space so that it matches the flip flags.</param>
/// <returns>The area that the tile covers</returns>
public static RectangleF GetArea(this TiledMapObject obj, TiledMap map, Vector2? position = null) {
public static RectangleF GetArea(this TiledMapObject obj, TiledMap map, Vector2? position = null, TiledMapTileFlipFlags flipFlags = None) {
var tileSize = map.GetTileSize();
var pos = position ?? Vector2.Zero;
return new RectangleF(obj.Position / tileSize + pos, obj.Size / tileSize);
var area = new RectangleF(obj.Position / tileSize, obj.Size / tileSize);
if (flipFlags.HasFlag(FlipHorizontally))
area.X = 1 - area.X - area.Width;
if (flipFlags.HasFlag(FlipVertically))
area.Y = 1 - area.Y - area.Height;
if (position != null)
area.Offset(position.Value);
return area;
}
/// <summary>

View file

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using MLEM.Extended.Extensions;
using MLEM.Extensions;
using MLEM.Misc;
using MonoGame.Extended.Tiled;
@ -22,30 +23,20 @@ namespace MLEM.Extended.Tiled {
/// Creates a new tiled map collision handler for the given map
/// </summary>
/// <param name="map">The map</param>
public TiledMapCollisions(TiledMap map = null) {
/// <param name="collisionFunction">The function used to collect the collision info of a tile, or null to use <see cref="DefaultCollectCollisions"/></param>
public TiledMapCollisions(TiledMap map = null, CollectCollisions collisionFunction = null) {
if (map != null)
this.SetMap(map);
this.SetMap(map, collisionFunction);
}
/// <summary>
/// Sets this collision handler's handled map
/// </summary>
/// <param name="map">The map</param>
/// <param name="collisionFunction">The function used to collect the collision info of a tile, or null for the default handling</param>
/// <param name="collisionFunction">The function used to collect the collision info of a tile, or null to use <see cref="DefaultCollectCollisions"/></param>
public void SetMap(TiledMap map, CollectCollisions collisionFunction = null) {
this.map = map;
this.collisionFunction = collisionFunction ?? ((collisions, tile) => {
foreach (var obj in tile.TilesetTile.Objects) {
var area = obj.GetArea(tile.Map);
if (tile.Tile.IsFlippedHorizontally)
area.X = 1 - area.X - area.Width;
if (tile.Tile.IsFlippedVertically)
area.Y = 1 - area.Y - area.Height;
area.Offset(tile.Position);
collisions.Add(area);
}
});
this.collisionFunction = collisionFunction ?? DefaultCollectCollisions;
this.collisionInfos = new TileCollisionInfo[map.Layers.Count, map.Width, map.Height];
for (var i = 0; i < map.TileLayers.Count; i++) {
for (var x = 0; x < map.Width; x++) {
@ -83,12 +74,12 @@ namespace MLEM.Extended.Tiled {
public IEnumerable<TileCollisionInfo> GetCollidingTiles(RectangleF area, Func<TileCollisionInfo, bool> included = null) {
var inclusionFunc = included ?? (tile => tile.Collisions.Any(c => c.Intersects(area)));
var minX = Math.Max(0, area.Left.Floor());
var maxX = Math.Min(this.map.Width - 1, area.Right);
var maxX = Math.Min(this.map.Width - 1, area.Right.Floor());
var minY = Math.Max(0, area.Top.Floor());
var maxY = Math.Min(this.map.Height - 1, area.Bottom);
var maxY = Math.Min(this.map.Height - 1, area.Bottom.Floor());
for (var i = 0; i < this.map.TileLayers.Count; i++) {
for (var x = minX; x <= maxX; x++) {
for (var y = minY; y <= maxY; y++) {
for (var y = maxY; y >= minY; y--) {
for (var x = minX; x <= maxX; x++) {
var tile = this.collisionInfos[i, x, y];
if (tile == null)
continue;
@ -110,6 +101,50 @@ namespace MLEM.Extended.Tiled {
return this.GetCollidingTiles(area, included).Any();
}
/// <summary>
/// Returns an enumerable of all of the <see cref="TileCollisionInfo.Collisions"/> of the colliding tiles in the given area.
/// This method is a convenience method based on <see cref="GetCollidingTiles"/>.
/// </summary>
/// <param name="area">The area to check for collisions in</param>
/// <param name="included">A function that determines if a certain info should be included or not</param>
/// <returns>An enumerable of collision rectangles for that area</returns>
public IEnumerable<RectangleF> GetCollidingAreas(RectangleF area, Func<TileCollisionInfo, bool> included = null) {
foreach (var tile in this.GetCollidingTiles(area, included)) {
foreach (var col in tile.Collisions)
yield return col;
}
}
/// <summary>
/// Returns an enumerable of normals and penetration amounts for each <see cref="TileCollisionInfo"/> that intersects with the given <see cref="RectangleF"/> area.
/// The normals and penetration amounts are based on <see cref="MLEM.Extensions.NumberExtensions.Penetrate"/>.
/// Note that all x penetrations are returned before all y penetrations, which improves collision detection in sidescrolling games with gravity. Note that this behavior can be inverted using <paramref name="prioritizeX"/>.
/// </summary>
/// <param name="getArea">The area to penetrate</param>
/// <param name="included">A function that determines if a certain info should be included or not</param>
/// <param name="prioritizeX">Whether all x penetrations should be prioritized (returned first). If this is false, all y penetrations are prioritized instead.</param>
/// <returns>A set of normals and penetration amounts</returns>
public IEnumerable<(Vector2, float)> GetPenetrations(Func<RectangleF> getArea, Func<TileCollisionInfo, bool> included = null, bool prioritizeX = true) {
foreach (var col in this.GetCollidingAreas(getArea(), included)) {
if (getArea().Penetrate(col, out var normal, out var penetration) && normal.X != 0 == prioritizeX)
yield return (normal, penetration);
}
foreach (var col in this.GetCollidingAreas(getArea(), included)) {
if (getArea().Penetrate(col, out var normal, out var penetration) && normal.X == 0 == prioritizeX)
yield return (normal, penetration);
}
}
/// <summary>
/// The default implementation of <see cref="CollectCollisions"/> which is used by <see cref="SetMap"/> if no custom collision collection function is passed
/// </summary>
/// <param name="collisions">The list of collisions to add to</param>
/// <param name="tile">The tile's collision information</param>
public static void DefaultCollectCollisions(List<RectangleF> collisions, TileCollisionInfo tile) {
foreach (var obj in tile.TilesetTile.Objects)
collisions.Add(obj.GetArea(tile.Map, tile.Position, tile.Tile.Flags));
}
/// <summary>
/// A delegate method used to override the default collision checking behavior.
/// </summary>

View file

@ -8,22 +8,25 @@
<PropertyGroup>
<Authors>Ellpeck</Authors>
<Description>MLEM Library for Extending MonoGame combined with some other useful libraries into a quick Game startup class</Description>
<PackageReleaseNotes>See the full changelog at https://github.com/Ellpeck/MLEM/blob/main/CHANGELOG.md</PackageReleaseNotes>
<PackageTags>monogame ellpeck mlem utility extensions</PackageTags>
<PackageProjectUrl>https://mlem.ellpeck.de/</PackageProjectUrl>
<RepositoryUrl>https://github.com/Ellpeck/MLEM</RepositoryUrl>
<PackageLicenseUrl>https://github.com/Ellpeck/MLEM/blob/main/LICENSE</PackageLicenseUrl>
<PackageIconUrl>https://raw.githubusercontent.com/Ellpeck/MLEM/main/Media/Logo.png</PackageIconUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageIcon>Logo.png</PackageIcon>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Coroutine" Version="2.0.0" />
<PackageReference Include="MonoGame.Framework.Portable" Version="3.7.1.189">
<PackageReference Include="Coroutine" Version="2.1.1" />
<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>
<ProjectReference Include="..\MLEM.Ui\MLEM.Ui.csproj" />
<ProjectReference Include="..\MLEM\MLEM.csproj" />
<None Include="../Media/Logo.png" Pack="true" PackagePath="" />
</ItemGroup>
</Project>

View file

@ -40,15 +40,15 @@ namespace MLEM.Startup {
/// <summary>
/// An event that is invoked in <see cref="LoadContent"/>
/// </summary>
public GenericCallback OnLoadContent;
public event GenericCallback OnLoadContent;
/// <summary>
/// An event that is invoked in <see cref="Update"/>
/// </summary>
public TimeCallback OnUpdate;
public event TimeCallback OnUpdate;
/// <summary>
/// An event that is invoked in <see cref="Draw"/>
/// </summary>
public TimeCallback OnDraw;
public event TimeCallback OnDraw;
/// <summary>
/// Creates a new MlemGame instance with some default settings
@ -70,9 +70,9 @@ namespace MLEM.Startup {
/// <inheritdoc />
protected override void LoadContent() {
this.SpriteBatch = new SpriteBatch(this.GraphicsDevice);
this.InputHandler = new InputHandler();
this.InputHandler = new InputHandler(this);
this.Components.Add(this.InputHandler);
this.UiSystem = new UiSystem(this.Window, this.GraphicsDevice, new UntexturedStyle(this.SpriteBatch), this.InputHandler);
this.UiSystem = new UiSystem(this, new UntexturedStyle(this.SpriteBatch), this.InputHandler);
this.Components.Add(this.UiSystem);
this.OnLoadContent?.Invoke(this);
}

View file

@ -6,6 +6,7 @@
<IncludeBuildOutput>false</IncludeBuildOutput>
<ContentTargetFolders>content</ContentTargetFolders>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>NU5128</NoWarn>
</PropertyGroup>
<PropertyGroup>
@ -13,15 +14,17 @@
<Title>MLEM Templates</Title>
<Authors>Ellpeck</Authors>
<Description>MLEM Library for Extending MonoGame cross-platform project templates</Description>
<PackageReleaseNotes>See the full changelog at https://github.com/Ellpeck/MLEM/blob/main/CHANGELOG.md</PackageReleaseNotes>
<PackageTags>dotnet-new templates monogame ellpeck mlem utility extensions</PackageTags>
<PackageProjectUrl>https://mlem.ellpeck.de/</PackageProjectUrl>
<RepositoryUrl>https://github.com/Ellpeck/MLEM</RepositoryUrl>
<PackageIconUrl>https://raw.githubusercontent.com/Ellpeck/MLEM/main/Media/Logo.png</PackageIconUrl>
<PackageIcon>Logo.png</PackageIcon>
</PropertyGroup>
<ItemGroup>
<Content Include="content\**\*" Exclude="content\**\.DS_Store;content\**\bin;content\**\obj" />
<Compile Remove="**\*" />
<None Include="../Media/Logo.png" Pack="true" PackagePath=""/>
</ItemGroup>
</Project>

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

View file

@ -5,9 +5,9 @@ namespace TemplateNamespace {
public static class Program {
public static void Main() {
TextInputWrapper.Current = new TextInputWrapper.DesktopGl<TextInputEventArgs>((w, c) => w.TextInput += c);
using (var game = new GameImpl())
game.Run();
MlemPlatform.Current = new MlemPlatform.DesktopGl<TextInputEventArgs>((w, c) => w.TextInput += c);
using var game = new GameImpl();
game.Run();
}
}

View file

@ -2,26 +2,23 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.0</TargetFramework>
<TargetFramework>net5.0</TargetFramework>
<PublishReadyToRun>false</PublishReadyToRun>
<TieredCompilation>false</TieredCompilation>
<ApplicationIcon>Icon.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Contentless" Version="3.0.*" />
<PackageReference Include="MLEM.Startup" Version="4.0.*" />
<PackageReference Include="MonoGame.Content.Builder.Task" Version="3.8.0.1641" />
<PackageReference Include="Contentless" Version="3.0.5" />
<PackageReference Include="MLEM.Startup" Version="4.3.0" />
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.0.1641" />
<PackageReference Include="MonoGame.Content.Builder.Task" Version="3.8.0.1641" />
</ItemGroup>
<ItemGroup>
<MonoGameContentReference Include="Content\Content.mgcb" />
<Content Include="Content\*\**" />
</ItemGroup>
<ItemGroup>
<TrimmerRootAssembly Include="MonoGame.Framework" Visible="false" />
<TrimmerRootAssembly Include="MLEM" Visible="false" />
<TrimmerRootAssembly Include="MLEM.Ui" Visible="false" />
<EmbeddedResource Include="Icon.ico" />
<EmbeddedResource Include="Icon.bmp" />
</ItemGroup>
</Project>

View file

@ -5,15 +5,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MLEM.Startup" Version="4.0.*" />
<PackageReference Include="MonoGame.Framework.Portable" Version="3.7.1.189">
<PackageReference Include="MLEM.Startup" Version="4.3.0" />
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.0.1641">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<TrimmerRootAssembly Include="MonoGame.Framework" Visible="false" />
<TrimmerRootAssembly Include="MLEM" Visible="false" />
<TrimmerRootAssembly Include="MLEM.Ui" Visible="false" />
</ItemGroup>
</Project>

View file

@ -60,6 +60,17 @@ namespace MLEM.Ui.Elements {
this.CanBeSelected = !value;
}
}
/// <summary>
/// Whether this button's <see cref="Text"/> should be truncated if it exceeds this button's width.
/// Defaults to false.
/// </summary>
public bool TruncateTextIfLong {
get => this.Text?.TruncateIfLong ?? false;
set {
if (this.Text != null)
this.Text.TruncateIfLong = value;
}
}
/// <summary>
/// Creates a new button with the given settings
@ -71,7 +82,7 @@ namespace MLEM.Ui.Elements {
/// <param name="tooltipWidth">The width of this button's <see cref="Tooltip"/>, or 50 by default</param>
public Button(Anchor anchor, Vector2 size, string text = null, string tooltipText = null, float tooltipWidth = 50) : base(anchor, size) {
if (text != null) {
this.Text = new Paragraph(Anchor.Center, 1, text, true);
this.Text = new Paragraph(Anchor.Center, 1, text, true) {Padding = new Vector2(1)};
this.AddChild(this.Text);
}
if (tooltipText != null)

View file

@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
@ -13,24 +15,25 @@ namespace MLEM.Ui.Elements {
/// <summary>
/// This class represents a generic base class for ui elements of a <see cref="UiSystem"/>.
/// </summary>
public abstract class Element : GenericDataHolder {
public abstract class Element : GenericDataHolder, IDisposable {
/// <summary>
/// A list of all of this element's direct children.
/// Use <see cref="AddChild{T}"/> or <see cref="RemoveChild"/> to manipulate this list while calling all of the necessary callbacks.
/// </summary>
protected readonly List<Element> Children = new List<Element>();
private readonly List<Element> sortedChildren = new List<Element>();
protected readonly IList<Element> Children;
private readonly List<Element> children = new List<Element>();
/// <summary>
/// A sorted version of <see cref="Children"/>. The children are sorted by their <see cref="Priority"/>.
/// </summary>
protected List<Element> SortedChildren {
protected IList<Element> SortedChildren {
get {
this.UpdateSortedChildrenIfDirty();
return this.sortedChildren;
}
}
private bool sortedChildrenDirty;
private IList<Element> sortedChildren;
private UiSystem system;
/// <summary>
@ -87,7 +90,6 @@ namespace MLEM.Ui.Elements {
/// The size of this element, where X represents the width and Y represents the height.
/// If the x or y value of the size is between 0 and 1, the size will be seen as a percentage of its parent's size rather than as an absolute value.
/// If the x (or y) value of the size is negative, the width (or height) is seen as a percentage of the element's resulting height (or width).
/// If <see cref="SetWidthBasedOnChildren"/> is true, this property's X value is ignored and overridden. If <see cref="SetHeightBasedOnChildren"/> is true, this property's Y value is ignored and overridden.
/// </summary>
/// <example>
/// The following example combines both types of percentage-based sizing.
@ -174,7 +176,6 @@ namespace MLEM.Ui.Elements {
}
}
private bool areaDirty;
private int areaUpdateRecursionCount;
/// <summary>
/// The <see cref="UnscrolledArea"/> of this element, but with <see cref="ScaledScrollOffset"/> applied.
/// </summary>
@ -255,13 +256,26 @@ namespace MLEM.Ui.Elements {
/// </summary>
public bool CanAutoAnchorsAttach = true;
/// <summary>
/// Set this field to true to cause this element's width to be automatically calculated based on the area that its <see cref="Children"/> take up.
/// To use this element's <see cref="Size"/>'s X coordinate as a minimum or maximum width rather than ignoring it, set <see cref="TreatSizeAsMinimum"/> or <see cref="TreatSizeAsMaximum"/> to true.
/// </summary>
public bool SetWidthBasedOnChildren;
/// <summary>
/// Set this field to true to cause this element's height to be automatically calculated based on the area that its <see cref="Children"/> take up.
/// To use this element's <see cref="Size"/>'s Y coordinate as a minimum or maximum height rather than ignoring it, set <see cref="TreatSizeAsMinimum"/> or <see cref="TreatSizeAsMaximum"/> to true.
/// </summary>
public bool SetHeightBasedOnChildren;
/// <summary>
/// Set this field to true to cause this element's width to be automatically calculated based on the area that is <see cref="Children"/> take up.
/// If this field is set to true, and <see cref="SetWidthBasedOnChildren"/> or <see cref="SetHeightBasedOnChildren"/> are enabled, the resulting width or height will always be greather than or equal to this element's <see cref="Size"/>.
/// For example, if an element's <see cref="Size"/>'s Y coordinate is set to 20, but there is only one child with a height of 10 in it, the element's height would be shrunk to 10 if this value was false, but would remain at 20 if it was true.
/// Note that this value only has an effect if <see cref="SetWidthBasedOnChildren"/> or <see cref="SetHeightBasedOnChildren"/> are enabled.
/// </summary>
public bool SetWidthBasedOnChildren;
public bool TreatSizeAsMinimum;
/// <summary>
/// If this field is set to true, and <see cref="SetWidthBasedOnChildren"/> or <see cref="SetHeightBasedOnChildren"/>are enabled, the resulting width or height weill always be less than or equal to this element's <see cref="Size"/>.
/// Note that this value only has an effect if <see cref="SetWidthBasedOnChildren"/> or <see cref="SetHeightBasedOnChildren"/> are enabled.
/// </summary>
public bool TreatSizeAsMaximum;
/// <summary>
/// Set this field to true to cause this element's final display area to never exceed that of its <see cref="Parent"/>.
/// If the resulting area is too large, the size of this element is shrunk to fit the target area.
@ -325,10 +339,10 @@ namespace MLEM.Ui.Elements {
public GenericCallback OnTouchExit;
/// <summary>
/// Event that is called when text input is made.
/// When an element uses this event, it should call <see cref="TextInputWrapper.EnsureExists"/> on construction to ensure that a text input wrapper was set.
/// When an element uses this event, it should call <see cref="MlemPlatform.EnsureExists"/> on construction to ensure that the MLEM platform is initialized.
///
/// Note that this event is called for every element, even if it is not selected.
/// Also note that if <see cref="TextInputWrapper.RequiresOnScreenKeyboard"/> is true, this event is never called.
/// Also note that if the active <see cref="MlemPlatform"/> uses an on-screen keyboard, this event is never called.
/// </summary>
public TextInputCallback OnTextInput;
/// <summary>
@ -368,6 +382,11 @@ namespace MLEM.Ui.Elements {
/// Event that is called when a child is removed from this element using <see cref="RemoveChild"/>
/// </summary>
public OtherElementCallback OnChildRemoved;
/// <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>
public GenericCallback OnDisposed;
/// <summary>
/// A style property that contains the selection indicator that is displayed on this element if it is the <see cref="RootElement.SelectedElement"/>
@ -391,6 +410,8 @@ namespace MLEM.Ui.Elements {
this.anchor = anchor;
this.size = size;
this.Children = new ReadOnlyCollection<Element>(this.children);
this.OnMouseEnter += element => this.IsMouseOver = true;
this.OnMouseExit += element => this.IsMouseOver = false;
this.OnTouchEnter += element => this.IsMouseOver = true;
@ -401,6 +422,12 @@ namespace MLEM.Ui.Elements {
this.GetGamepadNextElement += (dir, next) => next;
this.SetAreaDirty();
this.SetSortedChildrenDirty();
}
/// <inheritdoc />
~Element() {
this.Dispose();
}
/// <summary>
@ -410,15 +437,15 @@ namespace MLEM.Ui.Elements {
/// <param name="index">The index to add the child at, or -1 to add it to the end of the <see cref="Children"/> list</param>
/// <typeparam name="T">The type of child to add</typeparam>
/// <returns>This element, for chaining</returns>
public T AddChild<T>(T element, int index = -1) where T : Element {
if (index < 0 || index > this.Children.Count)
index = this.Children.Count;
this.Children.Insert(index, element);
public virtual T AddChild<T>(T element, int index = -1) where T : Element {
if (index < 0 || index > this.children.Count)
index = this.children.Count;
this.children.Insert(index, element);
element.Parent = this;
element.AndChildren(e => {
e.Root = this.Root;
e.System = this.System;
this.Root?.OnElementAdded(e);
this.Root?.InvokeOnElementAdded(e);
this.OnChildAdded?.Invoke(this, e);
});
this.SetSortedChildrenDirty();
@ -430,8 +457,8 @@ namespace MLEM.Ui.Elements {
/// Removes the given child from this element.
/// </summary>
/// <param name="element">The child element to remove</param>
public void RemoveChild(Element element) {
this.Children.Remove(element);
public virtual void RemoveChild(Element element) {
this.children.Remove(element);
// set area dirty here so that a dirty call is made
// upwards to us if the element is auto-positioned
element.SetAreaDirty();
@ -439,7 +466,7 @@ namespace MLEM.Ui.Elements {
element.AndChildren(e => {
e.Root = null;
e.System = null;
this.Root?.OnElementRemoved(e);
this.Root?.InvokeOnElementRemoved(e);
this.OnChildRemoved?.Invoke(this, e);
});
this.SetSortedChildrenDirty();
@ -449,7 +476,7 @@ namespace MLEM.Ui.Elements {
/// Removes all children from this element that match the given condition.
/// </summary>
/// <param name="condition">The condition that determines if a child should be removed</param>
public void RemoveChildren(Func<Element, bool> condition = null) {
public virtual void RemoveChildren(Func<Element, bool> condition = null) {
for (var i = this.Children.Count - 1; i >= 0; i--) {
var child = this.Children[i];
if (condition == null || condition(child)) {
@ -478,10 +505,7 @@ namespace MLEM.Ui.Elements {
/// </summary>
public virtual void ForceUpdateSortedChildren() {
this.sortedChildrenDirty = false;
this.sortedChildren.Clear();
this.sortedChildren.AddRange(this.Children);
this.sortedChildren.Sort((e1, e2) => e1.Priority.CompareTo(e2.Priority));
this.sortedChildren = new ReadOnlyCollection<Element>(this.Children.OrderBy(e => e.Priority).ToArray());
}
/// <summary>
@ -514,134 +538,148 @@ namespace MLEM.Ui.Elements {
var parentArea = this.Parent != null ? this.Parent.ChildPaddedArea : (RectangleF) this.system.Viewport;
var parentCenterX = parentArea.X + parentArea.Width / 2;
var parentCenterY = parentArea.Y + parentArea.Height / 2;
var actualSize = this.CalcActualSize(parentArea);
var pos = new Vector2();
switch (this.anchor) {
case Anchor.TopLeft:
case Anchor.AutoLeft:
case Anchor.AutoInline:
case Anchor.AutoInlineIgnoreOverflow:
pos.X = parentArea.X + this.ScaledOffset.X;
pos.Y = parentArea.Y + this.ScaledOffset.Y;
break;
case Anchor.TopCenter:
case Anchor.AutoCenter:
pos.X = parentCenterX - actualSize.X / 2 + this.ScaledOffset.X;
pos.Y = parentArea.Y + this.ScaledOffset.Y;
break;
case Anchor.TopRight:
case Anchor.AutoRight:
pos.X = parentArea.Right - actualSize.X - this.ScaledOffset.X;
pos.Y = parentArea.Y + this.ScaledOffset.Y;
break;
case Anchor.CenterLeft:
pos.X = parentArea.X + this.ScaledOffset.X;
pos.Y = parentCenterY - actualSize.Y / 2 + this.ScaledOffset.Y;
break;
case Anchor.Center:
pos.X = parentCenterX - actualSize.X / 2 + this.ScaledOffset.X;
pos.Y = parentCenterY - actualSize.Y / 2 + this.ScaledOffset.Y;
break;
case Anchor.CenterRight:
pos.X = parentArea.Right - actualSize.X - this.ScaledOffset.X;
pos.Y = parentCenterY - actualSize.Y / 2 + this.ScaledOffset.Y;
break;
case Anchor.BottomLeft:
pos.X = parentArea.X + this.ScaledOffset.X;
pos.Y = parentArea.Bottom - actualSize.Y - this.ScaledOffset.Y;
break;
case Anchor.BottomCenter:
pos.X = parentCenterX - actualSize.X / 2 + this.ScaledOffset.X;
pos.Y = parentArea.Bottom - actualSize.Y - this.ScaledOffset.Y;
break;
case Anchor.BottomRight:
pos.X = parentArea.Right - actualSize.X - this.ScaledOffset.X;
pos.Y = parentArea.Bottom - actualSize.Y - this.ScaledOffset.Y;
break;
}
var recursion = 0;
UpdateDisplayArea(actualSize);
if (this.Anchor >= Anchor.AutoLeft) {
Element previousChild;
if (this.Anchor == Anchor.AutoInline || this.Anchor == Anchor.AutoInlineIgnoreOverflow) {
previousChild = this.GetOlderSibling(e => !e.IsHidden && e.CanAutoAnchorsAttach);
} else {
previousChild = this.GetLowestOlderSibling(e => !e.IsHidden && e.CanAutoAnchorsAttach);
void UpdateDisplayArea(Vector2 newSize) {
var pos = new Vector2();
switch (this.anchor) {
case Anchor.TopLeft:
case Anchor.AutoLeft:
case Anchor.AutoInline:
case Anchor.AutoInlineIgnoreOverflow:
pos.X = parentArea.X + this.ScaledOffset.X;
pos.Y = parentArea.Y + this.ScaledOffset.Y;
break;
case Anchor.TopCenter:
case Anchor.AutoCenter:
pos.X = parentCenterX - newSize.X / 2 + this.ScaledOffset.X;
pos.Y = parentArea.Y + this.ScaledOffset.Y;
break;
case Anchor.TopRight:
case Anchor.AutoRight:
pos.X = parentArea.Right - newSize.X - this.ScaledOffset.X;
pos.Y = parentArea.Y + this.ScaledOffset.Y;
break;
case Anchor.CenterLeft:
pos.X = parentArea.X + this.ScaledOffset.X;
pos.Y = parentCenterY - newSize.Y / 2 + this.ScaledOffset.Y;
break;
case Anchor.Center:
pos.X = parentCenterX - newSize.X / 2 + this.ScaledOffset.X;
pos.Y = parentCenterY - newSize.Y / 2 + this.ScaledOffset.Y;
break;
case Anchor.CenterRight:
pos.X = parentArea.Right - newSize.X - this.ScaledOffset.X;
pos.Y = parentCenterY - newSize.Y / 2 + this.ScaledOffset.Y;
break;
case Anchor.BottomLeft:
pos.X = parentArea.X + this.ScaledOffset.X;
pos.Y = parentArea.Bottom - newSize.Y - this.ScaledOffset.Y;
break;
case Anchor.BottomCenter:
pos.X = parentCenterX - newSize.X / 2 + this.ScaledOffset.X;
pos.Y = parentArea.Bottom - newSize.Y - this.ScaledOffset.Y;
break;
case Anchor.BottomRight:
pos.X = parentArea.Right - newSize.X - this.ScaledOffset.X;
pos.Y = parentArea.Bottom - newSize.Y - this.ScaledOffset.Y;
break;
}
if (previousChild != null) {
var prevArea = previousChild.GetAreaForAutoAnchors();
switch (this.Anchor) {
case Anchor.AutoLeft:
case Anchor.AutoCenter:
case Anchor.AutoRight:
pos.Y = prevArea.Bottom + this.ScaledOffset.Y;
break;
case Anchor.AutoInline:
var newX = prevArea.Right + this.ScaledOffset.X;
if (newX + actualSize.X <= parentArea.Right) {
pos.X = newX;
pos.Y = prevArea.Y + this.ScaledOffset.Y;
} else {
pos.Y = prevArea.Bottom + this.ScaledOffset.Y;
}
break;
case Anchor.AutoInlineIgnoreOverflow:
pos.X = prevArea.Right + this.ScaledOffset.X;
pos.Y = prevArea.Y + this.ScaledOffset.Y;
break;
}
}
}
if (this.PreventParentSpill) {
if (pos.X < parentArea.X)
pos.X = parentArea.X;
if (pos.Y < parentArea.Y)
pos.Y = parentArea.Y;
if (pos.X + actualSize.X > parentArea.Right)
actualSize.X = parentArea.Right - pos.X;
if (pos.Y + actualSize.Y > parentArea.Bottom)
actualSize.Y = parentArea.Bottom - pos.Y;
}
this.area = new RectangleF(pos, actualSize);
this.System.OnElementAreaUpdated?.Invoke(this);
foreach (var child in this.Children)
child.ForceUpdateArea();
if (this.Children.Count > 0) {
Element foundChild = null;
if (this.SetHeightBasedOnChildren) {
var lowest = this.GetLowestChild(e => !e.IsHidden);
if (lowest != null) {
var newHeight = (lowest.UnscrolledArea.Bottom - pos.Y + this.ScaledChildPadding.Bottom) / this.Scale;
if (!newHeight.Equals(this.size.Y, 0.01F)) {
this.size.Y = newHeight;
foundChild = lowest;
}
}
}
if (this.SetWidthBasedOnChildren) {
var rightmost = this.GetRightmostChild(e => !e.IsHidden);
if (rightmost != null) {
var newWidth = (rightmost.UnscrolledArea.Right - pos.X + this.ScaledChildPadding.Right) / this.Scale;
if (!newWidth.Equals(this.size.X, 0.01F)) {
this.size.X = newWidth;
foundChild = rightmost;
}
}
}
if (foundChild != null) {
this.areaUpdateRecursionCount++;
if (this.areaUpdateRecursionCount >= 16) {
throw new ArithmeticException($"The area of {this} with root {this.Root?.Name} has recursively updated too often. Does its child {foundChild} contain any conflicting auto-sizing settings?");
if (this.Anchor >= Anchor.AutoLeft) {
Element previousChild;
if (this.Anchor == Anchor.AutoInline || this.Anchor == Anchor.AutoInlineIgnoreOverflow) {
previousChild = this.GetOlderSibling(e => !e.IsHidden && e.CanAutoAnchorsAttach);
} else {
this.ForceUpdateArea();
previousChild = this.GetLowestOlderSibling(e => !e.IsHidden && e.CanAutoAnchorsAttach);
}
if (previousChild != null) {
var prevArea = previousChild.GetAreaForAutoAnchors();
switch (this.Anchor) {
case Anchor.AutoLeft:
case Anchor.AutoCenter:
case Anchor.AutoRight:
pos.Y = prevArea.Bottom + this.ScaledOffset.Y;
break;
case Anchor.AutoInline:
var newX = prevArea.Right + this.ScaledOffset.X;
if (newX + newSize.X <= parentArea.Right) {
pos.X = newX;
pos.Y = prevArea.Y + this.ScaledOffset.Y;
} else {
pos.Y = prevArea.Bottom + this.ScaledOffset.Y;
}
break;
case Anchor.AutoInlineIgnoreOverflow:
pos.X = prevArea.Right + this.ScaledOffset.X;
pos.Y = prevArea.Y + this.ScaledOffset.Y;
break;
}
}
}
if (this.PreventParentSpill) {
if (pos.X < parentArea.X)
pos.X = parentArea.X;
if (pos.Y < parentArea.Y)
pos.Y = parentArea.Y;
if (pos.X + newSize.X > parentArea.Right)
newSize.X = parentArea.Right - pos.X;
if (pos.Y + newSize.Y > parentArea.Bottom)
newSize.Y = parentArea.Bottom - pos.Y;
}
this.area = new RectangleF(pos, newSize);
this.System.InvokeOnElementAreaUpdated(this);
foreach (var child in this.Children)
child.ForceUpdateArea();
if (this.SetWidthBasedOnChildren || this.SetHeightBasedOnChildren) {
Element foundChild = null;
var autoSize = this.UnscrolledArea.Size;
if (this.SetHeightBasedOnChildren) {
var lowest = this.GetLowestChild(e => !e.IsHidden);
if (lowest != null) {
autoSize.Y = lowest.UnscrolledArea.Bottom - pos.Y + this.ScaledChildPadding.Bottom;
foundChild = lowest;
} else {
if (this.Children.Count > 0)
throw new InvalidOperationException($"{this} with root {this.Root.Name} sets its height based on children but it only has children anchored too low ({string.Join(", ", this.Children.Select(c => c.Anchor))})");
autoSize.Y = 0;
}
}
if (this.SetWidthBasedOnChildren) {
var rightmost = this.GetRightmostChild(e => !e.IsHidden);
if (rightmost != null) {
autoSize.X = rightmost.UnscrolledArea.Right - pos.X + this.ScaledChildPadding.Right;
foundChild = rightmost;
} else {
if (this.Children.Count > 0)
throw new InvalidOperationException($"{this} with root {this.Root.Name} sets its width based on children but it only has children anchored too far right ({string.Join(", ", this.Children.Select(c => c.Anchor))})");
autoSize.X = 0;
}
}
if (this.TreatSizeAsMinimum) {
autoSize = Vector2.Max(autoSize, actualSize);
} else if (this.TreatSizeAsMaximum) {
autoSize = Vector2.Min(autoSize, actualSize);
}
if (!autoSize.Equals(this.UnscrolledArea.Size, 0.01F)) {
recursion++;
if (recursion >= 16) {
throw new ArithmeticException($"The area of {this} with root {this.Root.Name} has recursively updated too often. Does its child {foundChild} contain any conflicting auto-sizing settings?");
} else {
UpdateDisplayArea(autoSize);
}
}
} else {
this.areaUpdateRecursionCount = 0;
}
}
}
@ -809,7 +847,7 @@ namespace MLEM.Ui.Elements {
/// A <see cref="Panel"/> only returns elements that are currently in view here.
/// </summary>
/// <returns>This element's relevant children</returns>
protected virtual List<Element> GetRelevantChildren() {
protected virtual IList<Element> GetRelevantChildren() {
return this.SortedChildren;
}
@ -818,7 +856,7 @@ namespace MLEM.Ui.Elements {
/// </summary>
/// <param name="time">The game's time</param>
public virtual void Update(GameTime time) {
this.System.OnElementUpdated?.Invoke(this, time);
this.System.InvokeOnElementUpdated(this, time);
foreach (var child in this.GetRelevantChildren())
if (child.System != null)
@ -869,9 +907,9 @@ namespace MLEM.Ui.Elements {
/// <param name="samplerState">The sampler state that is used for drawing</param>
/// <param name="matrix">The transformation matrix that is used for drawing</param>
public virtual void Draw(GameTime time, SpriteBatch batch, float alpha, BlendState blendState, SamplerState samplerState, Matrix matrix) {
this.System.OnElementDrawn?.Invoke(this, time, batch, alpha);
this.System.InvokeOnElementDrawn(this, time, batch, alpha);
if (this.IsSelected)
this.System.OnSelectedElementDrawn?.Invoke(this, time, batch, alpha);
this.System.InvokeOnSelectedElementDrawn(this, time, batch, alpha);
foreach (var child in this.GetRelevantChildren()) {
if (!child.IsHidden)
@ -907,15 +945,21 @@ namespace MLEM.Ui.Elements {
return null;
if (this.Transform != Matrix.Identity)
position = Vector2.Transform(position, Matrix.Invert(this.Transform));
var children = this.GetRelevantChildren();
for (var i = children.Count - 1; i >= 0; i--) {
var element = children[i].GetElementUnderPos(position);
var relevant = this.GetRelevantChildren();
for (var i = relevant.Count - 1; i >= 0; i--) {
var element = relevant[i].GetElementUnderPos(position);
if (element != null)
return element;
}
return this.CanBeMoused && this.DisplayArea.Contains(position) ? this : null;
}
/// <inheritdoc />
public virtual void Dispose() {
this.OnDisposed?.Invoke(this);
GC.SuppressFinalize(this);
}
/// <summary>
/// Performs the specified action on this element and all of its <see cref="Children"/>
/// </summary>
@ -931,7 +975,7 @@ namespace MLEM.Ui.Elements {
/// </summary>
/// <param name="comparison">The comparison to sort by</param>
public void ReorderChildren(Comparison<Element> comparison) {
this.Children.Sort(comparison);
this.children.Sort(comparison);
}
/// <summary>
@ -940,7 +984,7 @@ namespace MLEM.Ui.Elements {
/// <param name="index">The index to start reversing at</param>
/// <param name="count">The amount of elements to reverse</param>
public void ReverseChildren(int index = 0, int? count = null) {
this.Children.Reverse(index, count ?? this.Children.Count);
this.children.Reverse(index, count ?? this.Children.Count);
}
/// <summary>

View file

@ -1,4 +1,7 @@
using System;
using System.Linq;
using Microsoft.Xna.Framework;
using MLEM.Input;
using MLEM.Textures;
namespace MLEM.Ui.Elements {
@ -111,6 +114,60 @@ namespace MLEM.Ui.Elements {
return group;
}
/// <summary>
/// Creates a <see cref="Button"/> that acts as a way to input a custom value for a <see cref="Keybind"/>.
/// Note that only the first <see cref="Keybind.Combination"/> of the given keybind is displayed and edited, all others are ignored. The exception is that, if <paramref name="unbindKey"/> is set, unbinding the keybind clears all combinations.
/// Inputting custom keybinds using this element supports <see cref="ModifierKey"/>-based modifiers and any <see cref="GenericInput"/>-based keys.
/// </summary>
/// <param name="anchor">The button's anchor</param>
/// <param name="size">The button's size</param>
/// <param name="keybind">The keybind that this button should represent</param>
/// <param name="inputHandler">The input handler to query inputs with</param>
/// <param name="activePlaceholder">A placeholder text that is displayed while the keybind is being edited</param>
/// <param name="unbindKey">An optional generic input that allows the keybind value to be unbound, clearing all combinations</param>
/// <param name="unboundPlaceholder">A placeholder text that is displayed if the keybind is unbound</param>
/// <param name="inputName">An optional function to give each input a display name that is easier to read. If this is null, <see cref="GenericInput.ToString"/> is used.</param>
/// <returns>A keybind button with the given settings</returns>
public static Button KeybindButton(Anchor anchor, Vector2 size, Keybind keybind, InputHandler inputHandler, string activePlaceholder, GenericInput unbindKey = default, string unboundPlaceholder = "", Func<GenericInput, string> inputName = null) {
string GetCurrentName() {
var combination = keybind.GetCombinations().FirstOrDefault();
if (combination == null)
return unboundPlaceholder;
return string.Join(" + ", combination.Modifiers
.Append(combination.Key)
.Select(i => inputName?.Invoke(i) ?? i.ToString()));
}
var button = new Button(anchor, size, GetCurrentName());
var active = false;
var activeNext = false;
button.OnPressed = e => {
button.Text.Text = activePlaceholder;
activeNext = true;
};
button.OnUpdated = (e, time) => {
if (activeNext) {
active = true;
activeNext = false;
} else if (active) {
if (unbindKey != default && inputHandler.IsPressed(unbindKey)) {
keybind.Clear();
button.Text.Text = unboundPlaceholder;
active = false;
} else if (inputHandler.InputsPressed.Length > 0) {
var key = inputHandler.InputsPressed.FirstOrDefault(i => !i.IsModifier());
if (key != default) {
var mods = inputHandler.InputsDown.Where(i => i.IsModifier());
keybind.Remove((c, i) => i == 0).Add(key, mods.ToArray());
button.Text.Text = GetCurrentName();
active = false;
}
}
}
};
return button;
}
}
/// <summary>

View file

@ -22,6 +22,11 @@ namespace MLEM.Ui.Elements {
/// </summary>
public StyleProp<NinePatch> Texture;
/// <summary>
/// The color that this panel's <see cref="Texture"/> should be drawn with.
/// If this style property has no value, <see cref="Color.White"/> is used.
/// </summary>
public StyleProp<Color> DrawColor;
/// <summary>
/// The scroll bar that this panel contains.
/// This is only nonnull if <see cref="scrollOverflow"/> is true.
/// </summary>
@ -49,8 +54,8 @@ namespace MLEM.Ui.Elements {
this.CanBeSelected = false;
if (scrollOverflow) {
var scrollSize = scrollerSize ?? Point.Zero;
this.ScrollBar = new ScrollBar(Anchor.TopRight, new Vector2(scrollSize.X, 1), scrollSize.Y, 0) {
var (w, h) = scrollerSize ?? Point.Zero;
this.ScrollBar = new ScrollBar(Anchor.TopRight, new Vector2(w, 1), h, 0) {
StepPerScroll = 10,
OnValueChanged = (element, value) => this.ScrollChildren(),
CanAutoAnchorsAttach = false,
@ -59,9 +64,9 @@ namespace MLEM.Ui.Elements {
};
// modify the padding so that the scroll bar isn't over top of something else
this.ScrollBar.PositionOffset -= new Vector2(scrollSize.X + 1, 0);
this.ScrollBar.PositionOffset -= new Vector2(w + 1, 0);
if (autoHideScrollbar)
this.ScrollBar.OnAutoHide += e => this.ChildPadding += new Padding(0, scrollSize.X, 0, 0) * (e.IsHidden ? -1 : 1);
this.ScrollBar.OnAutoHide += e => this.ChildPadding += new Padding(0, w, 0, 0) * (e.IsHidden ? -1 : 1);
// handle automatic element selection, the scroller needs to scroll to the right location
this.OnSelectedElementChanged += (element, otherElement) => {
@ -108,15 +113,24 @@ namespace MLEM.Ui.Elements {
/// <inheritdoc />
public override void ForceUpdateSortedChildren() {
base.ForceUpdateSortedChildren();
if (this.scrollOverflow) {
if (this.ScrollBar.Parent != this)
throw new NotSupportedException("A panel that scrolls overflow cannot have its scroll bar removed from its list of children");
if (this.scrollOverflow)
this.relevantChildrenDirty = true;
}
}
/// <inheritdoc />
protected override List<Element> GetRelevantChildren() {
public override void RemoveChild(Element element) {
if (element == this.ScrollBar)
throw new NotSupportedException("A panel that scrolls overflow cannot have its scroll bar removed from its list of children");
base.RemoveChild(element);
}
/// <inheritdoc />
public override void RemoveChildren(Func<Element, bool> condition = null) {
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) {
@ -145,7 +159,7 @@ namespace MLEM.Ui.Elements {
/// <inheritdoc />
public override void Draw(GameTime time, SpriteBatch batch, float alpha, BlendState blendState, SamplerState samplerState, Matrix matrix) {
if (this.Texture.HasValue())
batch.Draw(this.Texture, this.DisplayArea, Color.White * alpha, this.Scale);
batch.Draw(this.Texture, this.DisplayArea, this.DrawColor.OrDefault(Color.White) * alpha, this.Scale);
// if we handle overflow, draw using the render target in DrawUnbound
if (!this.scrollOverflow || this.renderTarget == null) {
base.Draw(time, batch, alpha, blendState, samplerState, matrix);
@ -169,12 +183,9 @@ namespace MLEM.Ui.Elements {
batch.Begin(SpriteSortMode.Deferred, blendState, samplerState, null, null, null, trans);
base.Draw(time, batch, alpha, blendState, samplerState, trans);
batch.End();
// also draw any children early within the render target with the translation applied
base.DrawEarly(time, batch, alpha, blendState, samplerState, trans);
}
} else {
base.DrawEarly(time, batch, alpha, blendState, samplerState, matrix);
}
base.DrawEarly(time, batch, alpha, blendState, samplerState, matrix);
}
/// <inheritdoc />
@ -209,7 +220,7 @@ namespace MLEM.Ui.Elements {
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(e => !e.IsHidden);
var lowestChild = this.GetLowestChild(c => c != this.ScrollBar && !c.IsHidden);
// the max value of the scrollbar is the amount of non-scaled pixels taken up by overflowing components
var childrenHeight = lowestChild.Area.Bottom - firstChild.Area.Top;
this.ScrollBar.MaxValue = (childrenHeight - this.Area.Height) / this.Scale + this.ChildPadding.Height;
@ -221,9 +232,18 @@ namespace MLEM.Ui.Elements {
if (this.renderTarget == null || targetArea.Width != this.renderTarget.Width || targetArea.Height != this.renderTarget.Height) {
if (this.renderTarget != null)
this.renderTarget.Dispose();
this.renderTarget = targetArea.IsEmpty ? null : new RenderTarget2D(this.System.GraphicsDevice, targetArea.Width, targetArea.Height);
this.renderTarget = targetArea.IsEmpty ? null : new RenderTarget2D(this.System.Game.GraphicsDevice, targetArea.Width, targetArea.Height);
}
}
/// <inheritdoc />
public override void Dispose() {
if (this.renderTarget != null) {
this.renderTarget.Dispose();
this.renderTarget = null;
}
base.Dispose();
}
}
}

View file

@ -1,3 +1,4 @@
using System;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
@ -15,7 +16,6 @@ namespace MLEM.Ui.Elements {
/// </summary>
public class Paragraph : Element {
private string text;
/// <summary>
/// The font that this paragraph draws text with.
/// To set its bold and italic font, use <see cref="GenericFont.Bold"/> and <see cref="GenericFont.Italic"/>.
@ -31,10 +31,16 @@ namespace MLEM.Ui.Elements {
/// </summary>
public StyleProp<Color> TextColor;
/// <summary>
/// The scale that the text will be rendered with
/// The scale that the text will be rendered with.
/// To add a multiplier rather than changing the scale directly, use <see cref="TextScaleMultiplier"/>.
/// </summary>
public StyleProp<float> TextScale;
/// <summary>
/// A multiplier that will be applied to <see cref="TextScale"/>.
/// To change the text scale itself, use <see cref="TextScale"/>.
/// </summary>
public float TextScaleMultiplier = 1;
/// <summary>
/// The text to render inside of this paragraph.
/// Use <see cref="GetTextCallback"/> if the text changes frequently.
/// </summary>
@ -58,10 +64,39 @@ namespace MLEM.Ui.Elements {
/// </summary>
public bool AutoAdjustWidth;
/// <summary>
/// Whether this paragraph should be truncated instead of split if the displayed <see cref="Text"/>'s width exceeds the provided width.
/// When the string is truncated, the <see cref="Ellipsis"/> is added to its end.
/// </summary>
public bool TruncateIfLong;
/// <summary>
/// The ellipsis characters to use if <see cref="TruncateIfLong"/> is enabled and the string is truncated.
/// If this is set to an empty string, no ellipsis will be attached to the truncated string.
/// </summary>
public string Ellipsis = "...";
/// <summary>
/// An event that gets called when this paragraph's <see cref="Text"/> is queried.
/// Use this event for setting this paragraph's text if it changes frequently.
/// </summary>
public TextCallback GetTextCallback;
/// <summary>
/// The action that is executed if <see cref="Link"/> objects inside of this paragraph are pressed.
/// By default, <see cref="MlemPlatform.OpenLinkOrFile"/> is executed.
/// </summary>
public Action<Link, LinkCode> LinkAction;
/// <summary>
/// The <see cref="TextAlignment"/> that this paragraph's text should be rendered with
/// </summary>
public TextAlignment Alignment {
get => this.alignment;
set {
this.alignment = value;
this.SetAreaDirty();
this.TokenizedText = null;
}
}
private string text;
private TextAlignment alignment;
/// <summary>
/// Creates a new paragraph with the given settings.
@ -92,8 +127,8 @@ namespace MLEM.Ui.Elements {
protected override Vector2 CalcActualSize(RectangleF parentArea) {
var size = base.CalcActualSize(parentArea);
this.ParseText(size);
var dims = this.TokenizedText.Measure(this.RegularFont) * this.TextScale * this.Scale;
return new Vector2(this.AutoAdjustWidth ? dims.X + this.ScaledPadding.Width : size.X, dims.Y + this.ScaledPadding.Height);
var (w, h) = this.TokenizedText.Measure(this.RegularFont) * this.TextScale * this.TextScaleMultiplier * this.Scale;
return new Vector2(this.AutoAdjustWidth ? w + this.ScaledPadding.Width : size.X, h + this.ScaledPadding.Height);
}
/// <inheritdoc />
@ -106,10 +141,10 @@ namespace MLEM.Ui.Elements {
/// <inheritdoc />
public override void Draw(GameTime time, SpriteBatch batch, float alpha, BlendState blendState, SamplerState samplerState, Matrix matrix) {
var pos = this.DisplayArea.Location;
var sc = this.TextScale * this.Scale;
var pos = this.DisplayArea.Location + new Vector2(GetAlignmentOffset(), 0);
var sc = this.TextScale * this.TextScaleMultiplier * this.Scale;
var color = this.TextColor.OrDefault(Color.White) * alpha;
this.TokenizedText.Draw(time, batch, pos, this.RegularFont, color, sc, 0);
this.TokenizedText.Draw(time, batch, pos, this.RegularFont, color, sc, 0, this.Alignment);
base.Draw(time, batch, alpha, blendState, samplerState, matrix);
}
@ -129,14 +164,21 @@ namespace MLEM.Ui.Elements {
protected virtual void ParseText(Vector2 size) {
if (this.TokenizedText == null) {
// tokenize the text
this.TokenizedText = this.System.TextFormatter.Tokenize(this.RegularFont, this.Text);
this.TokenizedText = this.System.TextFormatter.Tokenize(this.RegularFont, this.Text, this.Alignment);
// add links to the paragraph
this.RemoveChildren(c => c is Link);
foreach (var link in this.TokenizedText.Tokens.Where(t => t.AppliedCodes.Any(c => c is LinkCode)))
this.AddChild(new Link(Anchor.TopLeft, link, this.TextScale));
this.AddChild(new Link(Anchor.TopLeft, link, this.TextScale * this.TextScaleMultiplier));
}
var width = size.X - this.ScaledPadding.Width;
var scale = this.TextScale * this.TextScaleMultiplier * this.Scale;
if (this.TruncateIfLong) {
this.TokenizedText.Truncate(this.RegularFont, width, scale, this.Ellipsis, this.Alignment);
} else {
this.TokenizedText.Split(this.RegularFont, width, scale, this.Alignment);
}
this.TokenizedText.Split(this.RegularFont, size.X - this.ScaledPadding.Width, this.TextScale * this.Scale);
}
private void QueryTextCallback() {
@ -144,6 +186,16 @@ namespace MLEM.Ui.Elements {
this.Text = this.GetTextCallback(this);
}
private float GetAlignmentOffset() {
switch (this.Alignment) {
case TextAlignment.Center:
return this.DisplayArea.Width / 2;
case TextAlignment.Right:
return this.DisplayArea.Width;
}
return 0;
}
/// <summary>
/// A delegate method used for <see cref="Paragraph.GetTextCallback"/>
/// </summary>
@ -171,8 +223,13 @@ namespace MLEM.Ui.Elements {
this.Token = token;
this.textScale = textScale;
this.OnPressed += e => {
foreach (var code in token.AppliedCodes.OfType<LinkCode>())
this.System?.LinkBehavior?.Invoke(code);
foreach (var code in token.AppliedCodes.OfType<LinkCode>()) {
if (this.Parent is Paragraph p && p.LinkAction != null) {
p.LinkAction.Invoke(this, code);
} else {
MlemPlatform.Current.OpenLinkOrFile(code.Match.Groups[1].Value);
}
}
};
}
@ -180,7 +237,7 @@ namespace MLEM.Ui.Elements {
public override void ForceUpdateArea() {
// set the position offset and size to the token's first area
var area = this.Token.GetArea(Vector2.Zero, this.textScale).First();
this.PositionOffset = area.Location;
this.PositionOffset = area.Location + new Vector2(((Paragraph) this.Parent).GetAlignmentOffset() / this.Parent.Scale, 0);
this.Size = area.Size;
base.ForceUpdateArea();
}

View file

@ -44,8 +44,10 @@ namespace MLEM.Ui.Elements {
this.maxValue = Math.Max(0, value);
// force current value to be clamped
this.CurrentValue = this.CurrentValue;
if (this.AutoHideWhenEmpty && this.IsHidden != this.maxValue <= 0) {
this.IsHidden = this.maxValue <= 0;
// auto-hide if necessary
var shouldHide = this.maxValue <= 0.01F;
if (this.AutoHideWhenEmpty && this.IsHidden != shouldHide) {
this.IsHidden = shouldHide;
this.OnAutoHide?.Invoke(this);
}
}

View file

@ -17,7 +17,7 @@ namespace MLEM.Ui.Elements {
/// <summary>
/// A text field element for use inside of a <see cref="UiSystem"/>.
/// A text field is a selectable element that can be typed in, as well as copied and pasted from.
/// If <see cref="TextInputWrapper.RequiresOnScreenKeyboard"/> is enabled, then this text field will automatically open an on-screen keyboard when pressed using <see cref="KeyboardInput"/>.
/// If an on-screen keyboard is required, then this text field will automatically open an on-screen keyboard using <see cref="MlemPlatform.OpenOnScreenKeyboard"/>.
/// </summary>
public class TextField : Element {
@ -104,11 +104,11 @@ namespace MLEM.Ui.Elements {
/// </summary>
public Rule InputRule;
/// <summary>
/// The title of the <see cref="KeyboardInput"/> field on mobile devices and consoles
/// The title of the <c>KeyboardInput</c> field on mobile devices and consoles
/// </summary>
public string MobileTitle;
/// <summary>
/// The description of the <see cref="KeyboardInput"/> field on mobile devices and consoles
/// The description of the <c>KeyboardInput</c> field on mobile devices and consoles
/// </summary>
public string MobileDescription;
private int caretPos;
@ -141,17 +141,13 @@ namespace MLEM.Ui.Elements {
if (font != null)
this.Font.Set(font);
TextInputWrapper.EnsureExists();
if (TextInputWrapper.Current.RequiresOnScreenKeyboard()) {
this.OnPressed += async e => {
if (!KeyboardInput.IsVisible) {
var title = this.MobileTitle ?? this.PlaceholderText;
var result = await KeyboardInput.Show(title, this.MobileDescription, this.Text);
if (result != null)
this.SetText(result.Replace('\n', ' '), true);
}
};
}
MlemPlatform.EnsureExists();
this.OnPressed += async e => {
var title = this.MobileTitle ?? this.PlaceholderText;
var result = await MlemPlatform.Current.OpenOnScreenKeyboard(title, this.MobileDescription, this.Text, false);
if (result != null)
this.SetText(result.Replace('\n', ' '), true);
};
this.OnTextInput += (element, key, character) => {
if (!this.IsSelected || this.IsHidden)
return;
@ -220,12 +216,12 @@ namespace MLEM.Ui.Elements {
this.CaretPos = this.text.Length;
} else if (this.Input.IsModifierKeyDown(ModifierKey.Control)) {
if (this.Input.IsKeyPressed(Keys.V)) {
var clip = Clipboard.GetText();
var clip = ClipboardService.GetText();
if (clip != null)
this.InsertText(clip);
} else if (this.Input.IsKeyPressed(Keys.C)) {
// until there is text selection, just copy the whole content
Clipboard.SetText(this.Text);
ClipboardService.SetText(this.Text);
}
}

View file

@ -84,16 +84,16 @@ namespace MLEM.Ui.Elements {
/// Causes this tooltip's position to be snapped to the mouse position.
/// </summary>
public void SnapPositionToMouse() {
var viewport = this.System.Viewport.Size;
var viewport = this.System.Viewport;
var offset = (this.Input.MousePosition.ToVector2() + this.MouseOffset.Value) / this.Scale;
if (offset.X < 0)
offset.X = 0;
if (offset.Y < 0)
offset.Y = 0;
if (offset.X * this.Scale + this.Area.Width >= viewport.X)
offset.X = (viewport.X - this.Area.Width) / this.Scale;
if (offset.Y * this.Scale + this.Area.Height >= viewport.Y)
offset.Y = (viewport.Y - this.Area.Height) / this.Scale;
if (offset.X < viewport.X)
offset.X = viewport.X;
if (offset.Y < viewport.Y)
offset.Y = viewport.Y;
if (offset.X * this.Scale + this.Area.Width >= viewport.Right)
offset.X = (viewport.Right - this.Area.Width) / this.Scale;
if (offset.Y * this.Scale + this.Area.Height >= viewport.Bottom)
offset.Y = (viewport.Bottom - this.Area.Height) / this.Scale;
this.PositionOffset = offset;
}

View file

@ -7,21 +7,24 @@
<PropertyGroup>
<Authors>Ellpeck</Authors>
<Description>A mouse, keyboard, gamepad and touch ready Ui system that features automatic anchoring, sizing and several ready-to-use element types.</Description>
<PackageReleaseNotes>See the full changelog at https://github.com/Ellpeck/MLEM/blob/main/CHANGELOG.md</PackageReleaseNotes>
<PackageTags>monogame ellpeck mlem ui user interface graphical gui system mouse keyboard gamepad touch</PackageTags>
<PackageProjectUrl>https://mlem.ellpeck.de/</PackageProjectUrl>
<RepositoryUrl>https://github.com/Ellpeck/MLEM</RepositoryUrl>
<PackageLicenseUrl>https://github.com/Ellpeck/MLEM/blob/main/LICENSE</PackageLicenseUrl>
<PackageIconUrl>https://raw.githubusercontent.com/Ellpeck/MLEM/main/Media/Logo.png</PackageIconUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageIcon>Logo.png</PackageIcon>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MonoGame.Framework.Portable" Version="3.7.1.189">
<PackageReference Include="TextCopy" Version="4.3.0" />
<ProjectReference Include="..\MLEM\MLEM.csproj" />
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.0.1641">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="TextCopy" Version="3.0.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MLEM\MLEM.csproj" />
<None Include="../Media/Logo.png" Pack="true" PackagePath=""/>
</ItemGroup>
</Project>

View file

@ -50,21 +50,9 @@ namespace MLEM.Ui.Style {
return Vector2.Zero;
}
public override void DrawString(SpriteBatch batch, string text, Vector2 position, Color color) {
}
public override void DrawString(SpriteBatch batch, string text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects effects, float layerDepth) {
}
public override void DrawString(SpriteBatch batch, string text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth) {
}
public override void DrawString(SpriteBatch batch, StringBuilder text, Vector2 position, Color color) {
}
public override void DrawString(SpriteBatch batch, StringBuilder text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects effects, float layerDepth) {
}
public override void DrawString(SpriteBatch batch, StringBuilder text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth) {
}

View file

@ -57,11 +57,11 @@ namespace MLEM.Ui {
/// <summary>
/// AA <see cref="Keybind"/> that acts as the buttons on a gamepad that perform the <see cref="Element.OnPressed"/> action.
/// </summary>
public readonly Keybind GamepadButtons = new Keybind().Add(Buttons.A);
public readonly Keybind GamepadButtons = new Keybind(Buttons.A);
/// <summary>
/// A <see cref="Keybind"/> that acts as the buttons on a gamepad that perform the <see cref="Element.OnSecondaryPressed"/> action.
/// </summary>
public readonly Keybind SecondaryGamepadButtons = new Keybind().Add(Buttons.X);
public readonly Keybind SecondaryGamepadButtons = new Keybind(Buttons.X);
/// <summary>
/// A <see cref="Keybind"/> that acts as the buttons that select a <see cref="Element"/> that is above the currently selected element.
/// </summary>
@ -123,7 +123,7 @@ namespace MLEM.Ui {
/// <param name="inputHandler">The input handler to use for controlling, or null to create a new one.</param>
public UiControls(UiSystem system, InputHandler inputHandler = null) {
this.System = system;
this.Input = inputHandler ?? new InputHandler();
this.Input = inputHandler ?? new InputHandler(system.Game);
this.IsInputOurs = inputHandler == null;
this.Keybinds = typeof(UiControls).GetFields()
.Where(f => f.FieldType == typeof(Keybind))
@ -151,11 +151,11 @@ namespace MLEM.Ui {
var selectedNow = mousedNow != null && mousedNow.CanBeSelected ? mousedNow : null;
this.SelectElement(this.ActiveRoot, selectedNow);
if (mousedNow != null && mousedNow.CanBePressed)
this.System.OnElementPressed?.Invoke(mousedNow);
this.System.InvokeOnElementPressed(mousedNow);
} else if (this.Input.IsMouseButtonPressed(MouseButton.Right)) {
this.IsAutoNavMode = false;
if (mousedNow != null && mousedNow.CanBePressed)
this.System.OnElementSecondaryPressed?.Invoke(mousedNow);
this.System.InvokeOnElementSecondaryPressed(mousedNow);
}
}
@ -165,10 +165,10 @@ namespace MLEM.Ui {
if (this.SelectedElement?.Root != null && this.SelectedElement.CanBePressed) {
if (this.Input.IsModifierKeyDown(ModifierKey.Shift)) {
// secondary action on element using space or enter
this.System.OnElementSecondaryPressed?.Invoke(this.SelectedElement);
this.System.InvokeOnElementSecondaryPressed(this.SelectedElement);
} else {
// first action on element using space or enter
this.System.OnElementPressed?.Invoke(this.SelectedElement);
this.System.InvokeOnElementPressed(this.SelectedElement);
}
}
} else if (this.Input.IsKeyPressed(Keys.Tab)) {
@ -189,13 +189,13 @@ namespace MLEM.Ui {
var tapped = this.GetElementUnderPos(tap.Position);
this.SelectElement(this.ActiveRoot, tapped);
if (tapped != null && tapped.CanBePressed)
this.System.OnElementPressed?.Invoke(tapped);
this.System.InvokeOnElementPressed(tapped);
} else if (this.Input.GetGesture(GestureType.Hold, out var hold)) {
this.IsAutoNavMode = false;
var held = this.GetElementUnderPos(hold.Position);
this.SelectElement(this.ActiveRoot, held);
if (held != null && held.CanBePressed)
this.System.OnElementSecondaryPressed?.Invoke(held);
this.System.InvokeOnElementSecondaryPressed(held);
} else if (this.Input.TouchState.Count <= 0) {
this.SetTouchedElement(null);
} else {
@ -216,10 +216,10 @@ namespace MLEM.Ui {
if (this.HandleGamepad) {
if (this.GamepadButtons.IsPressed(this.Input, this.GamepadIndex)) {
if (this.SelectedElement?.Root != null && this.SelectedElement.CanBePressed)
this.System.OnElementPressed?.Invoke(this.SelectedElement);
this.System.InvokeOnElementPressed(this.SelectedElement);
} else if (this.SecondaryGamepadButtons.IsPressed(this.Input, this.GamepadIndex)) {
if (this.SelectedElement?.Root != null && this.SelectedElement.CanBePressed)
this.System.OnElementSecondaryPressed?.Invoke(this.SelectedElement);
this.System.InvokeOnElementSecondaryPressed(this.SelectedElement);
} else if (this.DownButtons.IsPressed(this.Input, this.GamepadIndex)) {
this.HandleGamepadNextElement(Direction2.Down);
} else if (this.LeftButtons.IsPressed(this.Input, this.GamepadIndex)) {
@ -265,14 +265,14 @@ namespace MLEM.Ui {
return;
if (selected != null)
this.System.OnElementDeselected?.Invoke(selected);
this.System.InvokeOnElementDeselected(selected);
if (element != null) {
this.System.OnElementSelected?.Invoke(element);
this.System.InvokeOnElementSelected(element);
this.selectedElements[root.Name] = element;
} else {
this.selectedElements.Remove(root.Name);
}
this.System.OnSelectedElementChanged?.Invoke(element);
this.System.InvokeOnSelectedElementChanged(element);
if (autoNav != null)
this.IsAutoNavMode = autoNav.Value;
@ -285,11 +285,11 @@ namespace MLEM.Ui {
public void SetMousedElement(Element element) {
if (element != this.MousedElement) {
if (this.MousedElement != null)
this.System.OnElementMouseExit?.Invoke(this.MousedElement);
this.System.InvokeOnElementMouseExit(this.MousedElement);
if (element != null)
this.System.OnElementMouseEnter?.Invoke(element);
this.System.InvokeOnElementMouseEnter(element);
this.MousedElement = element;
this.System.OnMousedElementChanged?.Invoke(element);
this.System.InvokeOnMousedElementChanged(element);
}
}
@ -300,11 +300,11 @@ namespace MLEM.Ui {
public void SetTouchedElement(Element element) {
if (element != this.TouchedElement) {
if (this.TouchedElement != null)
this.System.OnElementTouchExit?.Invoke(this.TouchedElement);
this.System.InvokeOnElementTouchExit(this.TouchedElement);
if (element != null)
this.System.OnElementTouchEnter?.Invoke(element);
this.System.InvokeOnElementTouchEnter(element);
this.TouchedElement = element;
this.System.OnTouchedElementChanged?.Invoke(element);
this.System.InvokeOnTouchedElementChanged(element);
}
}

View file

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text.RegularExpressions;
using Microsoft.Xna.Framework;
@ -21,21 +20,13 @@ namespace MLEM.Ui {
/// </summary>
public class UiSystem : GameComponent {
/// <summary>
/// The graphics device that this ui system uses for its size calculations
/// </summary>
public readonly GraphicsDevice GraphicsDevice;
/// <summary>
/// The game window that this ui system renders within
/// </summary>
public readonly GameWindow Window;
private readonly List<RootElement> rootElements = new List<RootElement>();
/// <summary>
/// The viewport that this ui system is rendering inside of.
/// This is automatically updated during <see cref="GameWindow.ClientSizeChanged"/>
/// </summary>
public Rectangle Viewport { get; private set; }
public Rectangle Viewport;
/// <summary>
/// Set this field to true to cause the ui system and all of its elements to automatically scale up or down with greater and lower resolution, respectively.
/// If this field is true, <see cref="AutoScaleReferenceSize"/> is used as the size that uses default <see cref="GlobalScale"/>
@ -97,12 +88,6 @@ namespace MLEM.Ui {
/// </summary>
public TextFormatter TextFormatter;
/// <summary>
/// The action that should be executed when a <see cref="LinkCode"/> in a paragraph's <see cref="Paragraph.TokenizedText"/> is pressed.
/// The actual link stored in the link code is stored in its <see cref="Code.Match"/>'s 1st group.
/// By default, the browser is opened with the given link's address.
/// </summary>
public Action<LinkCode> LinkBehavior = l => Process.Start(new ProcessStartInfo(l.Match.Groups[1].Value) {UseShellExecute = true});
/// <summary>
/// The <see cref="UiControls"/> that this ui system is controlled by.
/// The ui controls are also the place to change bindings for controller and keyboard input.
/// </summary>
@ -111,102 +96,99 @@ namespace MLEM.Ui {
/// <summary>
/// Event that is invoked after an <see cref="Element"/> is drawn, but before its children are drawn.
/// </summary>
public Element.DrawCallback OnElementDrawn = (e, time, batch, alpha) => e.OnDrawn?.Invoke(e, time, batch, alpha);
public event Element.DrawCallback OnElementDrawn;
/// <summary>
/// Event that is invoked after the <see cref="RootElement.SelectedElement"/> for each root element is drawn, but before its children are drawn.
/// </summary>
public Element.DrawCallback OnSelectedElementDrawn;
public event Element.DrawCallback OnSelectedElementDrawn;
/// <summary>
/// Event that is invoked when an <see cref="Element"/> is updated
/// </summary>
public Element.TimeCallback OnElementUpdated = (e, time) => e.OnUpdated?.Invoke(e, time);
public event Element.TimeCallback OnElementUpdated;
/// <summary>
/// Event that is invoked when an <see cref="Element"/> is pressed with the primary action key
/// </summary>
public Element.GenericCallback OnElementPressed = e => e.OnPressed?.Invoke(e);
public event Element.GenericCallback OnElementPressed;
/// <summary>
/// Event that is invoked when an <see cref="Element"/> is pressed with the secondary action key
/// </summary>
public Element.GenericCallback OnElementSecondaryPressed = e => e.OnSecondaryPressed?.Invoke(e);
public event Element.GenericCallback OnElementSecondaryPressed;
/// <summary>
/// Event that is invoked when an <see cref="Element"/> is newly selected using automatic navigation, or after it has been pressed with the mouse.
/// </summary>
public Element.GenericCallback OnElementSelected = e => e.OnSelected?.Invoke(e);
public event Element.GenericCallback OnElementSelected;
/// <summary>
/// Event that is invoked when an <see cref="Element"/> is deselected during the selection of a new element.
/// </summary>
public Element.GenericCallback OnElementDeselected = e => e.OnDeselected?.Invoke(e);
public event Element.GenericCallback OnElementDeselected;
/// <summary>
/// Event that is invoked when the mouse enters an <see cref="Element"/>
/// </summary>
public Element.GenericCallback OnElementMouseEnter = e => e.OnMouseEnter?.Invoke(e);
public event Element.GenericCallback OnElementMouseEnter;
/// <summary>
/// Event that is invoked when the mouse exits an <see cref="Element"/>
/// </summary>
public Element.GenericCallback OnElementMouseExit = e => e.OnMouseExit?.Invoke(e);
public event Element.GenericCallback OnElementMouseExit;
/// <summary>
/// Event that is invoked when an <see cref="Element"/> starts being touched
/// </summary>
public Element.GenericCallback OnElementTouchEnter = e => e.OnTouchEnter?.Invoke(e);
public event Element.GenericCallback OnElementTouchEnter;
/// <summary>
/// Event that is invoked when an <see cref="Element"/> stops being touched
/// </summary>
public Element.GenericCallback OnElementTouchExit = e => e.OnTouchExit?.Invoke(e);
public event Element.GenericCallback OnElementTouchExit;
/// <summary>
/// Event that is invoked when an <see cref="Element"/>'s display area changes
/// </summary>
public Element.GenericCallback OnElementAreaUpdated = e => e.OnAreaUpdated?.Invoke(e);
public event Element.GenericCallback OnElementAreaUpdated;
/// <summary>
/// Event that is invoked when the <see cref="Element"/> that the mouse is currently over changes
/// </summary>
public Element.GenericCallback OnMousedElementChanged;
public event Element.GenericCallback OnMousedElementChanged;
/// <summary>
/// Event that is invoked when the <see cref="Element"/> that is being touched changes
/// </summary>
public Element.GenericCallback OnTouchedElementChanged;
public event Element.GenericCallback OnTouchedElementChanged;
/// <summary>
/// Event that is invoked when the selected <see cref="Element"/> changes, either through automatic navigation, or by pressing on an element with the mouse
/// </summary>
public Element.GenericCallback OnSelectedElementChanged;
public event Element.GenericCallback OnSelectedElementChanged;
/// <summary>
/// Event that is invoked when a new <see cref="RootElement"/> is added to this ui system
/// </summary>
public RootCallback OnRootAdded;
public event RootCallback OnRootAdded;
/// <summary>
/// Event that is invoked when a <see cref="RootElement"/> is removed from this ui system
/// </summary>
public RootCallback OnRootRemoved;
public event RootCallback OnRootRemoved;
/// <summary>
/// Creates a new ui system with the given settings.
/// </summary>
/// <param name="window">The game's window</param>
/// <param name="device">The graphics device that should be used for viewport calculations</param>
/// <param name="game">The game</param>
/// <param name="style">The style settings that this ui should have. Use <see cref="UntexturedStyle"/> for the default, untextured style.</param>
/// <param name="inputHandler">The input handler that this ui's <see cref="UiControls"/> should use. If none is supplied, a new input handler is created for this ui.</param>
public UiSystem(GameWindow window, GraphicsDevice device, UiStyle style, InputHandler inputHandler = null) : base(null) {
/// <param name="automaticViewport">If this value is set to true, the ui system's <see cref="Viewport"/> will be set automatically based on the <see cref="GameWindow"/>'s size. Defaults to true.</param>
public UiSystem(Game game, UiStyle style, InputHandler inputHandler = null, bool automaticViewport = true) : base(game) {
this.Controls = new UiControls(this, inputHandler);
this.GraphicsDevice = device;
this.Window = window;
this.style = style;
this.Viewport = new Rectangle(Point.Zero, window.ClientBounds.Size);
this.AutoScaleReferenceSize = this.Viewport.Size;
window.ClientSizeChanged += (sender, args) => {
this.Viewport = new Rectangle(Point.Zero, window.ClientBounds.Size);
foreach (var root in this.rootElements)
root.Element.ForceUpdateArea();
};
if (TextInputWrapper.Current != null)
TextInputWrapper.Current.AddListener(window, (sender, key, character) => this.ApplyToAll(e => e.OnTextInput?.Invoke(e, key, character)));
this.OnMousedElementChanged = e => this.ApplyToAll(t => t.OnMousedElementChanged?.Invoke(t, e));
this.OnTouchedElementChanged = e => this.ApplyToAll(t => t.OnTouchedElementChanged?.Invoke(t, e));
this.OnSelectedElementChanged = e => this.ApplyToAll(t => t.OnSelectedElementChanged?.Invoke(t, e));
this.OnSelectedElementDrawn = (element, time, batch, alpha) => {
if (this.Controls.IsAutoNavMode && element.SelectionIndicator.HasValue()) {
this.OnElementDrawn += (e, time, batch, alpha) => e.OnDrawn?.Invoke(e, time, batch, alpha);
this.OnElementUpdated += (e, time) => e.OnUpdated?.Invoke(e, time);
this.OnElementPressed += e => e.OnPressed?.Invoke(e);
this.OnElementSecondaryPressed += e => e.OnSecondaryPressed?.Invoke(e);
this.OnElementSelected += e => e.OnSelected?.Invoke(e);
this.OnElementDeselected += e => e.OnDeselected?.Invoke(e);
this.OnElementMouseEnter += e => e.OnMouseEnter?.Invoke(e);
this.OnElementMouseExit += e => e.OnMouseExit?.Invoke(e);
this.OnElementTouchEnter += e => e.OnTouchEnter?.Invoke(e);
this.OnElementTouchExit += e => e.OnTouchExit?.Invoke(e);
this.OnElementAreaUpdated += e => e.OnAreaUpdated?.Invoke(e);
this.OnMousedElementChanged += e => this.ApplyToAll(t => t.OnMousedElementChanged?.Invoke(t, e));
this.OnTouchedElementChanged += e => this.ApplyToAll(t => t.OnTouchedElementChanged?.Invoke(t, e));
this.OnSelectedElementChanged += e => this.ApplyToAll(t => t.OnSelectedElementChanged?.Invoke(t, e));
this.OnSelectedElementDrawn += (element, time, batch, alpha) => {
if (this.Controls.IsAutoNavMode && element.SelectionIndicator.HasValue())
batch.Draw(element.SelectionIndicator, element.DisplayArea, Color.White * alpha, element.Scale / 2);
}
};
this.OnElementPressed += e => {
if (e.OnPressed != null)
@ -216,6 +198,17 @@ namespace MLEM.Ui {
if (e.OnSecondaryPressed != null)
e.SecondActionSound.Value?.Play();
};
MlemPlatform.Current?.AddTextInputListener(game.Window, (sender, key, character) => this.ApplyToAll(e => e.OnTextInput?.Invoke(e, key, character)));
if (automaticViewport) {
this.Viewport = new Rectangle(Point.Zero, game.Window.ClientBounds.Size);
this.AutoScaleReferenceSize = this.Viewport.Size;
game.Window.ClientSizeChanged += (sender, args) => {
this.Viewport = new Rectangle(Point.Zero, game.Window.ClientBounds.Size);
foreach (var root in this.rootElements)
root.Element.ForceUpdateArea();
};
}
this.TextFormatter = new TextFormatter();
this.TextFormatter.Codes.Add(new Regex("<l(?: ([^>]+))?>"), (f, m, r) => new LinkCode(m, r, 1 / 16F, 0.85F,
@ -279,10 +272,11 @@ namespace MLEM.Ui {
root.Element.AndChildren(e => {
e.Root = root;
e.System = this;
root.OnElementAdded(e);
root.InvokeOnElementAdded(e);
e.SetAreaDirty();
});
this.OnRootAdded?.Invoke(root);
root.InvokeOnAddedToUi(this);
this.SortRoots();
return root;
}
@ -300,10 +294,11 @@ namespace MLEM.Ui {
root.Element.AndChildren(e => {
e.Root = null;
e.System = null;
root.OnElementRemoved(e);
root.InvokeOnElementRemoved(e);
e.SetAreaDirty();
});
this.OnRootRemoved?.Invoke(root);
root.InvokeOnRemovedFromUi(this);
}
/// <summary>
@ -346,6 +341,22 @@ namespace MLEM.Ui {
root.Element.AndChildren(action);
}
internal void InvokeOnElementDrawn(Element element, GameTime time, SpriteBatch batch, float alpha) => this.OnElementDrawn?.Invoke(element, time, batch, alpha);
internal void InvokeOnSelectedElementDrawn(Element element, GameTime time, SpriteBatch batch, float alpha) => this.OnSelectedElementDrawn?.Invoke(element, time, batch, alpha);
internal void InvokeOnElementUpdated(Element element, GameTime time) => this.OnElementUpdated?.Invoke(element, time);
internal void InvokeOnElementAreaUpdated(Element element) => this.OnElementAreaUpdated?.Invoke(element);
internal void InvokeOnElementPressed(Element element) => this.OnElementPressed?.Invoke(element);
internal void InvokeOnElementSecondaryPressed(Element element) => this.OnElementSecondaryPressed?.Invoke(element);
internal void InvokeOnElementSelected(Element element) => this.OnElementSelected?.Invoke(element);
internal void InvokeOnElementDeselected(Element element) => this.OnElementDeselected?.Invoke(element);
internal void InvokeOnSelectedElementChanged(Element element) => this.OnSelectedElementChanged?.Invoke(element);
internal void InvokeOnElementMouseExit(Element element) => this.OnElementMouseExit?.Invoke(element);
internal void InvokeOnElementMouseEnter(Element element) => this.OnElementMouseEnter?.Invoke(element);
internal void InvokeOnMousedElementChanged(Element element) => this.OnMousedElementChanged?.Invoke(element);
internal void InvokeOnElementTouchExit(Element element) => this.OnElementTouchExit?.Invoke(element);
internal void InvokeOnElementTouchEnter(Element element) => this.OnElementTouchEnter?.Invoke(element);
internal void InvokeOnTouchedElementChanged(Element element) => this.OnTouchedElementChanged?.Invoke(element);
/// <summary>
/// A delegate used for callbacks that involve a <see cref="RootElement"/>
/// </summary>
@ -359,7 +370,7 @@ namespace MLEM.Ui {
/// Root elements are only used for the element in each element tree that doesn't have a <see cref="MLEM.Ui.Elements.Element.Parent"/>
/// To create a new root element, use <see cref="UiSystem.Add"/>
/// </summary>
public class RootElement {
public class RootElement : GenericDataHolder {
/// <summary>
/// The name of this root element
@ -430,11 +441,19 @@ namespace MLEM.Ui {
/// <summary>
/// Event that is invoked when a <see cref="Element"/> is added to this root element or any of its children.
/// </summary>
public Element.GenericCallback OnElementAdded;
public event Element.GenericCallback OnElementAdded;
/// <summary>
/// Even 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 of any of its children.
/// </summary>
public Element.GenericCallback OnElementRemoved;
public event Element.GenericCallback OnElementRemoved;
/// <summary>
/// Event that is invoked when this <see cref="RootElement"/> gets added to a <see cref="UiSystem"/> in <see cref="UiSystem.Add"/>
/// </summary>
public event Action<UiSystem> OnAddedToUi;
/// <summary>
/// Event that is invoked when this <see cref="RootElement"/> gets removed from a <see cref="UiSystem"/> in <see cref="UiSystem.Remove"/>
/// </summary>
public event Action<UiSystem> OnRemovedFromUi;
internal RootElement(string name, Element element, UiSystem system) {
this.Name = name;
@ -470,5 +489,10 @@ namespace MLEM.Ui {
this.Transform = Matrix.CreateScale(scale, scale, 1) * Matrix.CreateTranslation(new Vector3((1 - scale) * (origin ?? this.Element.DisplayArea.Center), 0));
}
internal void InvokeOnElementAdded(Element element) => this.OnElementAdded?.Invoke(element);
internal void InvokeOnElementRemoved(Element element) => this.OnElementRemoved?.Invoke(element);
internal void InvokeOnAddedToUi(UiSystem system) => this.OnAddedToUi?.Invoke(system);
internal void InvokeOnRemovedFromUi(UiSystem system) => this.OnRemovedFromUi?.Invoke(system);
}
}

View file

@ -20,6 +20,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MLEM.Templates", "MLEM.Temp
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
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -66,5 +68,9 @@ Global
{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
{53D52C3F-67FB-4F32-A794-EAB140BBFC11}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View file

@ -23,7 +23,6 @@ namespace MLEM.Cameras {
get => this.scale;
set => this.scale = MathHelper.Clamp(value, this.MinScale, this.MaxScale);
}
private float scale = 1;
/// <summary>
/// The minimum <see cref="Scale"/> that the camera can have
/// </summary>
@ -79,11 +78,15 @@ namespace MLEM.Cameras {
get => this.Position + this.ScaledViewport / 2;
set => this.Position = value - this.ScaledViewport / 2;
}
private Rectangle Viewport => this.graphicsDevice.Viewport.Bounds;
private Vector2 ScaledViewport => new Vector2(this.Viewport.Width, this.Viewport.Height) / this.ActualScale;
/// <summary>
/// The viewport of this camera, based on the game's <see cref="GraphicsDevice.Viewport"/> and this camera's <see cref="ActualScale"/>
/// </summary>
public Vector2 ScaledViewport => new Vector2(this.Viewport.Width, this.Viewport.Height) / this.ActualScale;
private Rectangle Viewport => this.graphicsDevice.Viewport.Bounds;
private readonly bool roundPosition;
private readonly GraphicsDevice graphicsDevice;
private float scale = 1;
/// <summary>
/// Creates a new camera.

View file

@ -0,0 +1,55 @@
using System.Collections.Generic;
using System.Linq;
namespace MLEM.Extensions {
/// <summary>
/// A set of extensions for dealing with collections of various kinds
/// </summary>
public static class CollectionExtensions {
/// <summary>
/// This method returns a set of possible combinations of n items from n different sets, where the order of the items in each combination is based on the order of the input sets.
/// For a version of this method that returns indices rather than entries, see <see cref="IndexCombinations{T}"/>.
/// <example>
/// Given the input set <c>{{1, 2, 3}, {A, B}, {+, -}}</c>, the returned set would contain the following sets:
/// <code>
/// {1, A, +}, {1, A, -}, {1, B, +}, {1, B, -},
/// {2, A, +}, {2, A, -}, {2, B, +}, {2, B, -},
/// {3, A, +}, {3, A, -}, {3, B, +}, {3, B, -}
/// </code>
/// </example>
/// </summary>
/// <param name="things">The different sets to be combined</param>
/// <typeparam name="T">The type of the items in the sets</typeparam>
/// <returns>All combinations of set items as described</returns>
public static IEnumerable<IEnumerable<T>> Combinations<T>(this IEnumerable<IEnumerable<T>> things) {
var combos = Enumerable.Repeat(Enumerable.Empty<T>(), 1);
foreach (var t in things)
combos = combos.SelectMany(c => t.Select(c.Append));
return combos;
}
/// <summary>
/// This method returns a set of possible combinations of n indices of items from n different sets, where the order of the items' indices in each combination is based on the order of the input sets.
/// For a version of this method that returns entries rather than indices, see <see cref="Combinations{T}"/>.
/// <example>
/// Given the input set <c>{{1, 2, 3}, {A, B}, {+, -}}</c>, the returned set would contain the following sets:
/// <code>
/// {0, 0, 0}, {0, 0, 1}, {0, 1, 0}, {0, 1, 1},
/// {1, 0, 0}, {1, 0, 1}, {1, 1, 0}, {1, 1, 1},
/// {2, 0, 0}, {2, 0, 1}, {2, 1, 0}, {2, 1, 1}
/// </code>
/// </example>
/// </summary>
/// <param name="things">The different sets to be combined</param>
/// <typeparam name="T">The type of the items in the sets</typeparam>
/// <returns>All combinations of set items as described</returns>
public static IEnumerable<IEnumerable<int>> IndexCombinations<T>(this IEnumerable<IEnumerable<T>> things) {
var combos = Enumerable.Repeat(Enumerable.Empty<int>(), 1);
foreach (var t in things)
combos = combos.SelectMany(c => t.Select((o, i) => c.Append(i)));
return combos;
}
}
}

View file

@ -8,32 +8,6 @@ namespace MLEM.Extensions {
/// </summary>
public static class ColorExtensions {
/// <summary>
/// Returns an inverted version of the color.
/// </summary>
/// <param name="color">The color to invert</param>
/// <returns>The inverted color</returns>
[Obsolete("Use ColorHelper.Invert instead")]
public static Color Invert(this Color color) => ColorHelper.Invert(color);
/// <summary>
/// Parses a hexadecimal number into a color.
/// The number should be in the format <c>0xaarrggbb</c>.
/// </summary>
/// <param name="value">The number to parse</param>
/// <returns>The resulting color</returns>
[Obsolete("Use ColorHelper.FromHexRgba instead")]
public static Color FromHex(uint value) => ColorHelper.FromHexRgba((int) value);
/// <summary>
/// Parses a hexadecimal string into a color.
/// The string can optionally start with a <c>#</c>.
/// </summary>
/// <param name="value">The string to parse</param>
/// <returns>The resulting color</returns>
[Obsolete("Use ColorHelper.FromHexString instead")]
public static Color FromHex(string value) => ColorHelper.FromHexString(value);
/// <summary>
/// Copies the alpha value from another color into this color.
/// </summary>

View file

@ -15,11 +15,6 @@ namespace MLEM.Extensions {
/// <param name="entries">The entries to choose from</param>
/// <typeparam name="T">The entries' type</typeparam>
/// <returns>A random entry</returns>
public static T GetRandomEntry<T>(this Random random, params T[] entries) {
return entries[random.Next(entries.Length)];
}
/// <inheritdoc cref="GetRandomEntry{T}(System.Random,T[])"/>
public static T GetRandomEntry<T>(this Random random, IList<T> entries) {
return entries[random.Next(entries.Count)];
}

View file

@ -0,0 +1,28 @@
using Microsoft.Xna.Framework.Audio;
namespace MLEM.Extensions {
/// <summary>
/// A set of extensions for dealing with <see cref="SoundEffect"/> and <see cref="SoundEffectInstance"/>
/// </summary>
public static class SoundExtensions {
/// <summary>
/// Creates a new <see cref="SoundEffectInstance"/> from the given <see cref="SoundEffect"/>, allowing optional instance data to be supplied as part of the method call
/// </summary>
/// <param name="effect">The sound effect to create an instance from</param>
/// <param name="volume">The value to set the returned instance's <see cref="SoundEffectInstance.Volume"/> to. Defaults to 1.</param>
/// <param name="pitch">The value to set the returned instance's <see cref="SoundEffectInstance.Pitch"/> to. Defaults to 0.</param>
/// <param name="pan">The value to set the returned instance's <see cref="SoundEffectInstance.Pan"/> to. Defaults to 0.</param>
/// <param name="isLooped">The value to set the returned instance's <see cref="SoundEffectInstance.IsLooped"/> to. Defaults to false.</param>
/// <returns></returns>
public static SoundEffectInstance CreateInstance(this SoundEffect effect, float volume = 1, float pitch = 0, float pan = 0, bool isLooped = false) {
var instance = effect.CreateInstance();
instance.Volume = volume;
instance.Pitch = pitch;
instance.Pan = pan;
instance.IsLooped = isLooped;
return instance;
}
}
}

View file

@ -83,6 +83,33 @@ namespace MLEM.Extensions {
return tex;
}
/// <summary>
/// Generates a texture with the given size that contains a gradient between the four specified corner colors.
/// If the same color is specified for two pairs of corners, a horizontal, vertical or diagonal gradient can be achieved.
/// This texture is automatically disposed of when the batch is disposed.
/// </summary>
/// <param name="batch">The sprite batch</param>
/// <param name="topLeft">The color of the texture's top left corner</param>
/// <param name="topRight">The color of the texture's top right corner</param>
/// <param name="bottomLeft">The color of the texture's bottom left corner</param>
/// <param name="bottomRight">The color of the texture's bottom right corner</param>
/// <param name="width">The width of the resulting texture, or 256 by default</param>
/// <param name="height">The height of the resulting texture, or 256 by default</param>
/// <returns>A new texture with the given data</returns>
public static Texture2D GenerateGradientTexture(this SpriteBatch batch, Color topLeft, Color topRight, Color bottomLeft, Color bottomRight, int width = 256, int height = 256) {
var tex = new Texture2D(batch.GraphicsDevice, width, height);
using (var data = tex.GetTextureData()) {
for (var x = 0; x < width; x++) {
var top = Color.Lerp(topLeft, topRight, x / (float) width);
var btm = Color.Lerp(bottomLeft, bottomRight, x / (float) width);
for (var y = 0; y < height; y++)
data[x, y] = Color.Lerp(top, btm, y / (float) height);
}
}
AutoDispose(batch, tex);
return tex;
}
/// <inheritdoc cref="SpriteBatch.Draw(Texture2D,Rectangle,Rectangle?,Color,float,Vector2,SpriteEffects,float)"/>
public static void Draw(this SpriteBatch batch, Texture2D texture, RectangleF destinationRectangle, Rectangle? sourceRectangle, Color color, float rotation, Vector2 origin, SpriteEffects effects, float layerDepth) {
var source = sourceRectangle ?? new Rectangle(0, 0, texture.Width, texture.Height);

View file

@ -25,7 +25,7 @@ namespace MLEM.Font {
/// This field holds the unicode representation of a zero-width space.
/// Whereas a regular <see cref="SpriteFont"/> would have to explicitly support this character for width calculations and string splitting, generic fonts implicitly support it in <see cref="MeasureString"/> and <see cref="SplitString"/>.
/// </summary>
public const char Zwsp = '\u8203';
public const char Zwsp = '\u200B';
/// <summary>
/// The bold version of this font.
@ -40,69 +40,43 @@ namespace MLEM.Font {
///<inheritdoc cref="SpriteFont.LineSpacing"/>
public abstract float LineHeight { get; }
///<inheritdoc cref="SpriteBatch.DrawString(SpriteFont,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/>
public abstract void DrawString(SpriteBatch batch, string text, Vector2 position, Color color);
///<inheritdoc cref="SpriteBatch.DrawString(SpriteFont,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/>
public abstract void DrawString(SpriteBatch batch, string text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects effects, float layerDepth);
///<inheritdoc cref="SpriteFont.MeasureString(string)"/>
protected abstract Vector2 MeasureChar(char c);
///<inheritdoc cref="SpriteBatch.DrawString(SpriteFont,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/>
public abstract void DrawString(SpriteBatch batch, string text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth);
///<inheritdoc cref="SpriteBatch.DrawString(SpriteFont,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/>
public abstract void DrawString(SpriteBatch batch, StringBuilder text, Vector2 position, Color color);
///<inheritdoc cref="SpriteBatch.DrawString(SpriteFont,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/>
public abstract void DrawString(SpriteBatch batch, StringBuilder text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects effects, float layerDepth);
///<inheritdoc cref="SpriteBatch.DrawString(SpriteFont,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/>
public abstract void DrawString(SpriteBatch batch, StringBuilder text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth);
///<inheritdoc cref="SpriteFont.MeasureString(string)"/>
protected abstract Vector2 MeasureChar(char c);
/// <summary>
/// Draws a string with the given text alignment.
/// </summary>
/// <param name="batch">The sprite batch to use</param>
/// <param name="text">The string to draw</param>
/// <param name="position">The position of the top left corner of the string</param>
/// <param name="align">The alignment to use</param>
/// <param name="color">The color to use</param>
public void DrawString(SpriteBatch batch, string text, Vector2 position, TextAlign align, Color color) {
this.DrawString(batch, text, position, align, color, 0, Vector2.Zero, Vector2.One, SpriteEffects.None, 0);
///<inheritdoc cref="SpriteBatch.DrawString(SpriteFont,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/>
public void DrawString(SpriteBatch batch, string text, Vector2 position, Color color) {
this.DrawString(batch, text, position, color, 0, Vector2.Zero, Vector2.One, SpriteEffects.None, 0);
}
///<inheritdoc cref="DrawString(SpriteBatch,string,Vector2,TextAlign,Color)"/>
public void DrawString(SpriteBatch batch, string text, Vector2 position, TextAlign align, Color color, float rotation, Vector2 origin, float scale, SpriteEffects effects, float layerDepth) {
this.DrawString(batch, text, position, align, color, rotation, origin, new Vector2(scale), effects, layerDepth);
///<inheritdoc cref="SpriteBatch.DrawString(SpriteFont,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/>
public void DrawString(SpriteBatch batch, string text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects effects, float layerDepth) {
this.DrawString(batch, text, position, color, rotation, origin, new Vector2(scale), effects, layerDepth);
}
///<inheritdoc cref="DrawString(SpriteBatch,string,Vector2,TextAlign,Color)"/>
public void DrawString(SpriteBatch batch, string text, Vector2 position, TextAlign align, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth) {
switch (align) {
case TextAlign.Center:
case TextAlign.CenterBothAxes:
var (w, h) = this.MeasureString(text);
position.X -= w / 2;
if (align == TextAlign.CenterBothAxes)
position.Y -= h / 2;
break;
case TextAlign.Right:
position.X -= this.MeasureString(text).X;
break;
}
this.DrawString(batch, text, position, color, rotation, origin, scale, effects, layerDepth);
///<inheritdoc cref="SpriteBatch.DrawString(SpriteFont,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/>
public void DrawString(SpriteBatch batch, StringBuilder text, Vector2 position, Color color) {
this.DrawString(batch, text, position, color, 0, Vector2.Zero, Vector2.One, SpriteEffects.None, 0);
}
///<inheritdoc cref="SpriteBatch.DrawString(SpriteFont,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/>
public void DrawString(SpriteBatch batch, StringBuilder text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects effects, float layerDepth) {
this.DrawString(batch, text, position, color, rotation, origin, new Vector2(scale), effects, layerDepth);
}
///<inheritdoc cref="SpriteFont.MeasureString(string)"/>
public Vector2 MeasureString(string text) {
public Vector2 MeasureString(string text, bool ignoreTrailingSpaces = false) {
var size = Vector2.Zero;
if (text.Length <= 0)
return size;
var xOffset = 0F;
foreach (var c in text) {
switch (c) {
for (var i = 0; i < text.Length; i++) {
switch (text[i]) {
case '\n':
xOffset = 0;
size.Y += this.LineHeight;
@ -116,8 +90,16 @@ namespace MLEM.Font {
case Zwsp:
// don't add width for a zero-width space
break;
case ' ':
if (ignoreTrailingSpaces && IsTrailingSpace(text, i)) {
// if this is a trailing space, we can skip remaining spaces too
i = text.Length - 1;
break;
}
xOffset += this.MeasureChar(' ').X;
break;
default:
xOffset += this.MeasureChar(c).X;
xOffset += this.MeasureChar(text[i]).X;
break;
}
// increase x size if this line is the longest
@ -169,63 +151,56 @@ namespace MLEM.Font {
/// <param name="scale">The scale to use for width measurements</param>
/// <returns>The split string, containing newline characters at each new line</returns>
public string SplitString(string text, float width, float scale) {
var total = new StringBuilder();
foreach (var line in text.Split('\n')) {
var curr = new StringBuilder();
foreach (var word in line.Split(' ', Zwsp)) {
if (this.MeasureString(word).X * scale >= width) {
if (curr.Length > 0) {
total.Append(curr).Append('\n');
curr.Clear();
}
var wordBuilder = new StringBuilder();
for (var i = 0; i < word.Length; i++) {
wordBuilder.Append(word[i]);
if (this.MeasureString(wordBuilder.ToString()).X * scale >= width) {
total.Append(wordBuilder.ToString(0, wordBuilder.Length - 1)).Append('\n');
wordBuilder.Remove(0, wordBuilder.Length - 1);
}
}
curr.Append(wordBuilder).Append(' ');
} else {
curr.Append(word).Append(' ');
if (this.MeasureString(curr.ToString()).X * scale >= width) {
var len = curr.Length - word.Length - 1;
if (len > 0) {
total.Append(curr.ToString(0, len)).Append('\n');
curr.Remove(0, len);
}
var ret = new StringBuilder();
var currWidth = 0F;
var lastSpaceIndex = -1;
var widthSinceLastSpace = 0F;
for (var i = 0; i < text.Length; i++) {
var c = text[i];
if (c == '\n') {
// split at pre-defined new lines
ret.Append(c);
lastSpaceIndex = -1;
widthSinceLastSpace = 0;
currWidth = 0;
} else {
var cWidth = this.MeasureChar(c).X * scale;
if (c == ' ' || c == OneEmSpace || c == Zwsp) {
// remember the location of this space
lastSpaceIndex = ret.Length;
widthSinceLastSpace = 0;
} else if (currWidth + cWidth >= width) {
// check if this line contains a space
if (lastSpaceIndex < 0) {
// if there is no last space, the word is longer than a line so we split here
ret.Append('\n');
currWidth = 0;
} else {
// split after the last space
ret.Insert(lastSpaceIndex + 1, '\n');
// we need to restore the width accumulated since the last space for the new line
currWidth = widthSinceLastSpace;
}
widthSinceLastSpace = 0;
lastSpaceIndex = -1;
}
// add current character
currWidth += cWidth;
widthSinceLastSpace += cWidth;
ret.Append(c);
}
total.Append(curr).Append('\n');
}
return total.ToString(0, total.Length - 2);
return ret.ToString();
}
private static bool IsTrailingSpace(string s, int index) {
for (var i = index + 1; i < s.Length; i++) {
if (s[i] != ' ')
return false;
}
return true;
}
}
/// <summary>
/// An enum that represents the text alignment options for <see cref="GenericFont.DrawString(SpriteBatch,string,Vector2,TextAlign,Color)"/>
/// </summary>
public enum TextAlign {
/// <summary>
/// The text is aligned as normal
/// </summary>
Left,
/// <summary>
/// The position passed represents the center of the resulting string in the x axis
/// </summary>
Center,
/// <summary>
/// The position passed represents the right edge of the resulting string
/// </summary>
Right,
/// <summary>
/// The position passed represents the center of the resulting string, both in the x and y axes
/// </summary>
CenterBothAxes
}
}

View file

@ -37,31 +37,11 @@ namespace MLEM.Font {
return this.Font.MeasureString(c.ToCachedString());
}
/// <inheritdoc/>
public override void DrawString(SpriteBatch batch, string text, Vector2 position, Color color) {
batch.DrawString(this.Font, text, position, color);
}
/// <inheritdoc/>
public override void DrawString(SpriteBatch batch, string text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects effects, float layerDepth) {
batch.DrawString(this.Font, text, position, color, rotation, origin, scale, effects, layerDepth);
}
/// <inheritdoc/>
public override void DrawString(SpriteBatch batch, string text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth) {
batch.DrawString(this.Font, text, position, color, rotation, origin, scale, effects, layerDepth);
}
/// <inheritdoc/>
public override void DrawString(SpriteBatch batch, StringBuilder text, Vector2 position, Color color) {
batch.DrawString(this.Font, text, position, color);
}
/// <inheritdoc/>
public override void DrawString(SpriteBatch batch, StringBuilder text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects effects, float layerDepth) {
batch.DrawString(this.Font, text, position, color, rotation, origin, scale, effects, layerDepth);
}
/// <inheritdoc/>
public override void DrawString(SpriteBatch batch, StringBuilder text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth) {
batch.DrawString(this.Font, text, position, color, rotation, origin, scale, effects, layerDepth);

View file

@ -0,0 +1,17 @@
using System.Text.RegularExpressions;
namespace MLEM.Formatting.Codes {
/// <inheritdoc />
public class ResetFormattingCode : Code {
/// <inheritdoc />
public ResetFormattingCode(Match match, Regex regex) : base(match, regex) {
}
/// <inheritdoc />
public override bool EndsHere(Code other) {
return true;
}
}
}

View file

@ -6,13 +6,13 @@ using MLEM.Font;
namespace MLEM.Formatting.Codes {
/// <inheritdoc />
public class ShadowCode : FontCode {
public class ShadowCode : Code {
private readonly Color color;
private readonly Vector2 offset;
/// <inheritdoc />
public ShadowCode(Match match, Regex regex, Color color, Vector2 offset) : base(match, regex, null) {
public ShadowCode(Match match, Regex regex, Color color, Vector2 offset) : base(match, regex) {
this.color = color;
this.offset = offset;
}
@ -24,5 +24,10 @@ namespace MLEM.Formatting.Codes {
return false;
}
/// <inheritdoc />
public override bool EndsHere(Code other) {
return other is ShadowCode || other is ResetFormattingCode;
}
}
}

View file

@ -7,13 +7,13 @@ using MLEM.Misc;
namespace MLEM.Formatting.Codes {
/// <inheritdoc />
public class UnderlineCode : FontCode {
public class UnderlineCode : Code {
private readonly float thickness;
private readonly float yOffset;
/// <inheritdoc />
public UnderlineCode(Match match, Regex regex, float thickness, float yOffset) : base(match, regex, null) {
public UnderlineCode(Match match, Regex regex, float thickness, float yOffset) : base(match, regex) {
this.thickness = thickness;
this.yOffset = yOffset;
}
@ -23,11 +23,16 @@ namespace MLEM.Formatting.Codes {
// don't underline spaces at the end of lines
if (c == ' ' && this.Token.DisplayString.Length > indexInToken + 1 && this.Token.DisplayString[indexInToken + 1] == '\n')
return false;
var size = font.MeasureString(cString) * scale;
var thicc = size.Y * this.thickness;
batch.Draw(batch.GetBlankTexture(), new RectangleF(pos.X, pos.Y + this.yOffset * size.Y - thicc, size.X, thicc), color);
var (w, h) = font.MeasureString(cString) * scale;
var t = h * this.thickness;
batch.Draw(batch.GetBlankTexture(), new RectangleF(pos.X, pos.Y + this.yOffset * h - t, w, t), color);
return false;
}
/// <inheritdoc />
public override bool EndsHere(Code other) {
return other is UnderlineCode || other is ResetFormattingCode;
}
}
}

View file

@ -0,0 +1,22 @@
namespace MLEM.Formatting {
/// <summary>
/// An enumeration that represents a set of alignment options for <see cref="TokenizedString"/> objects and MLEM.Ui paragraphs.
/// </summary>
public enum TextAlignment {
/// <summary>
/// Left alignment, which is also the default value
/// </summary>
Left,
/// <summary>
/// Center alignment
/// </summary>
Center,
/// <summary>
/// Right alignment.
/// In this alignment option, trailing spaces are ignored to ensure that visual alignment is consistent.
/// </summary>
Right
}
}

View file

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using Microsoft.Xna.Framework;
@ -33,9 +34,12 @@ namespace MLEM.Formatting {
// font codes
this.Codes.Add(new Regex("<b>"), (f, m, r) => new FontCode(m, r, fnt => fnt.Bold));
this.Codes.Add(new Regex("<i>"), (f, m, r) => new FontCode(m, r, fnt => fnt.Italic));
this.Codes.Add(new Regex(@"<s(?: #([0-9\w]{6,8}) (([+-.0-9]*)))?>"), (f, m, r) => new ShadowCode(m, r, m.Groups[1].Success ? ColorHelper.FromHexString(m.Groups[1].Value) : Color.Black, new Vector2(float.TryParse(m.Groups[2].Value, out var offset) ? offset : 2)));
this.Codes.Add(new Regex(@"<s(?: #([0-9\w]{6,8}) (([+-.0-9]*)))?>"), (f, m, r) => new ShadowCode(m, r,
m.Groups[1].Success ? ColorHelper.FromHexString(m.Groups[1].Value) : Color.Black,
new Vector2(float.TryParse(m.Groups[2].Value, NumberStyles.Number, CultureInfo.InvariantCulture, out var offset) ? offset : 2)));
this.Codes.Add(new Regex("<u>"), (f, m, r) => new UnderlineCode(m, r, 1 / 16F, 0.85F));
this.Codes.Add(new Regex("</(b|i|s|u|l)>"), (f, m, r) => new FontCode(m, r, null));
this.Codes.Add(new Regex("</(s|u|l)>"), (f, m, r) => new ResetFormattingCode(m, r));
this.Codes.Add(new Regex("</(b|i)>"), (f, m, r) => new FontCode(m, r, null));
// color codes
foreach (var c in typeof(Color).GetProperties()) {
@ -48,11 +52,14 @@ namespace MLEM.Formatting {
this.Codes.Add(new Regex("</c>"), (f, m, r) => new ColorCode(m, r, null));
// animation codes
this.Codes.Add(new Regex(@"<a wobbly(?: ([+-.0-9]*) ([+-.0-9]*))?>"), (f, m, r) => new WobblyCode(m, r, float.TryParse(m.Groups[1].Value, out var mod) ? mod : 5, float.TryParse(m.Groups[2].Value, out var heightMod) ? heightMod : 1 / 8F));
this.Codes.Add(new Regex(@"<a wobbly(?: ([+-.0-9]*) ([+-.0-9]*))?>"), (f, m, r) => new WobblyCode(m, r,
float.TryParse(m.Groups[1].Value, NumberStyles.Number, CultureInfo.InvariantCulture, out var mod) ? mod : 5,
float.TryParse(m.Groups[2].Value, NumberStyles.Number, CultureInfo.InvariantCulture, out var heightMod) ? heightMod : 1 / 8F));
this.Codes.Add(new Regex("</a>"), (f, m, r) => new AnimatedCode(m, r));
// macros
this.Macros.Add(new Regex("~"), (f, m, r) => GenericFont.Nbsp.ToCachedString());
this.Macros.Add(new Regex("<n>"), (f, m, r) => '\n'.ToCachedString());
}
/// <summary>
@ -60,8 +67,9 @@ namespace MLEM.Formatting {
/// </summary>
/// <param name="font">The font to use for tokenization. Note that this font needs to be the same that will later be used for splitting, measuring and/or drawing.</param>
/// <param name="s">The string to tokenize</param>
/// <param name="alignment">The text alignment that should be used. Note that this alignment needs to be the same that will later be used for splitting, measuring and/or drawing.</param>
/// <returns></returns>
public TokenizedString Tokenize(GenericFont font, string s) {
public TokenizedString Tokenize(GenericFont font, string s, TextAlignment alignment = TextAlignment.Left) {
// resolve macros
s = this.ResolveMacros(s);
var tokens = new List<Token>();
@ -70,9 +78,9 @@ namespace MLEM.Formatting {
var firstCode = this.GetNextCode(s, 0, 0);
if (firstCode != null)
codes.Add(firstCode);
var index = 0;
var rawIndex = 0;
while (rawIndex < s.Length) {
var index = StripFormatting(font, s.Substring(0, rawIndex), tokens.SelectMany(t => t.AppliedCodes)).Length;
var next = this.GetNextCode(s, rawIndex + 1);
// if we've reached the end of the string
if (next == null) {
@ -83,16 +91,18 @@ namespace MLEM.Formatting {
// create a new token for the content up to the next code
var ret = s.Substring(rawIndex, next.Match.Index - rawIndex);
tokens.Add(new Token(codes.ToArray(), index, rawIndex, StripFormatting(font, ret, codes), ret));
var strippedRet = StripFormatting(font, ret, codes);
tokens.Add(new Token(codes.ToArray(), index, rawIndex, strippedRet, ret));
// move to the start of the next code
rawIndex = next.Match.Index;
index += strippedRet.Length;
// remove all codes that are incompatible with the next one and apply it
codes.RemoveAll(c => c.EndsHere(next));
codes.Add(next);
}
return new TokenizedString(font, s, StripFormatting(font, s, tokens.SelectMany(t => t.AppliedCodes)), tokens.ToArray());
return new TokenizedString(font, alignment, s, StripFormatting(font, s, tokens.SelectMany(t => t.AppliedCodes)), tokens.ToArray());
}
/// <summary>

View file

@ -29,15 +29,19 @@ namespace MLEM.Formatting {
/// </summary>
public readonly string Substring;
/// <summary>
/// The string that is displayed by this token. If the tokenized string has been split, this string will contain the newline characters.
/// The string that is displayed by this token. If the tokenized string has been <see cref="TokenizedString.Split"/> or <see cref="TokenizedString.Truncate"/> has been used, this string will contain the newline characters.
/// </summary>
public string DisplayString => this.SplitSubstring ?? this.Substring;
public string DisplayString => this.ModifiedSubstring ?? this.Substring;
/// <summary>
/// The <see cref="DisplayString"/>, but split at newline characters
/// </summary>
public string[] SplitDisplayString { get; internal set; }
/// <summary>
/// The substring that this token contains, without the formatting codes removed.
/// </summary>
public readonly string RawSubstring;
internal RectangleF[] Area;
internal string SplitSubstring;
internal string ModifiedSubstring;
internal Token(Code[] appliedCodes, int index, int rawIndex, string substring, string rawSubstring) {
this.AppliedCodes = appliedCodes;

View file

@ -24,9 +24,9 @@ namespace MLEM.Formatting {
public readonly string String;
/// <summary>
/// The string that is actually displayed by this tokenized string.
/// If this string has been <see cref="Split"/>, this string will contain the newline characters.
/// If this string has been <see cref="Split"/> or <see cref="Truncate"/> has been used, this string will contain the newline characters.
/// </summary>
public string DisplayString => this.splitString ?? this.String;
public string DisplayString => this.modifiedString ?? this.String;
/// <summary>
/// The tokens that this tokenized string contains.
/// </summary>
@ -36,55 +36,48 @@ namespace MLEM.Formatting {
/// Note that, to get a formatting code for a certain token, use <see cref="Token.AppliedCodes"/>
/// </summary>
public readonly Code[] AllCodes;
private string splitString;
private string modifiedString;
internal TokenizedString(GenericFont font, string rawString, string strg, Token[] tokens) {
internal TokenizedString(GenericFont font, TextAlignment alignment, string rawString, string strg, Token[] tokens) {
this.RawString = rawString;
this.String = strg;
this.Tokens = tokens;
// since a code can be present in multiple tokens, we use Distinct here
this.AllCodes = tokens.SelectMany(t => t.AppliedCodes).Distinct().ToArray();
this.CalculateTokenAreas(font);
this.RecalculateTokenData(font, alignment);
}
/// <summary>
/// Splits this tokenized string, inserting newline characters if the width of the string is bigger than the maximum width.
/// Note that a tokenized string can be re-split without losing any of its actual data, as this operation merely modifies the <see cref="DisplayString"/>.
/// <seealso cref="GenericFont.SplitString"/>
/// </summary>
/// <param name="font">The font to use for width calculations</param>
/// <param name="width">The maximum width</param>
/// <param name="scale">The scale to use fr width calculations</param>
public void Split(GenericFont font, float width, float scale) {
// a split string has the same character count as the input string
// but with newline characters added
this.splitString = font.SplitString(this.String, width, scale);
// skip splitting logic for unformatted text
if (this.Tokens.Length == 1) {
this.Tokens[0].SplitSubstring = this.splitString;
return;
}
foreach (var token in this.Tokens) {
var index = 0;
var length = 0;
var ret = new StringBuilder();
// this is basically a substring function that ignores newlines for indexing
for (var i = 0; i < this.splitString.Length; i++) {
// if we're within the bounds of the token's substring, append to the new substring
if (index >= token.Index && length < token.Substring.Length)
ret.Append(this.splitString[i]);
// if the current char is not a newline, we simulate length increase
if (this.splitString[i] != '\n') {
if (index >= token.Index)
length++;
index++;
}
}
token.SplitSubstring = ret.ToString();
}
this.CalculateTokenAreas(font);
/// <param name="width">The maximum width, in display pixels based on the font and scale</param>
/// <param name="scale">The scale to use for width measurements</param>
/// <param name="alignment">The text alignment that should be used for width calculations</param>
public void Split(GenericFont font, float width, float scale, TextAlignment alignment = TextAlignment.Left) {
// a split string has the same character count as the input string but with newline characters added
this.modifiedString = font.SplitString(this.String, width, scale);
this.StoreModifiedSubstrings(font, alignment);
}
/// <inheritdoc cref="GenericFont.MeasureString(string)"/>
/// <summary>
/// Truncates this tokenized string, removing any additional characters that exceed the length from the displayed string.
/// Note that a tokenized string can be re-truncated without losing any of its actual data, as this operation merely modifies the <see cref="DisplayString"/>.
/// <seealso cref="GenericFont.TruncateString"/>
/// </summary>
/// <param name="font">The font to use for width calculations</param>
/// <param name="width">The maximum width, in display pixels based on the font and scale</param>
/// <param name="scale">The scale to use for width measurements</param>
/// <param name="ellipsis">The characters to add to the end of the string if it is too long</param>
/// <param name="alignment">The text alignment that should be used for width calculations</param>
public void Truncate(GenericFont font, float width, float scale, string ellipsis = "", TextAlignment alignment = TextAlignment.Left) {
this.modifiedString = font.TruncateString(this.String, width, scale, false, ellipsis);
this.StoreModifiedSubstrings(font, alignment);
}
/// <inheritdoc cref="GenericFont.MeasureString(string,bool)"/>
public Vector2 Measure(GenericFont font) {
return font.MeasureString(this.DisplayString);
}
@ -111,40 +104,116 @@ namespace MLEM.Formatting {
}
/// <inheritdoc cref="GenericFont.DrawString(SpriteBatch,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/>
public void Draw(GameTime time, SpriteBatch batch, Vector2 pos, GenericFont font, Color color, float scale, float depth) {
var innerOffset = new Vector2();
foreach (var token in this.Tokens) {
public void Draw(GameTime time, SpriteBatch batch, Vector2 pos, GenericFont font, Color color, float scale, float depth, TextAlignment alignment = TextAlignment.Left) {
var innerOffset = new Vector2(this.GetInnerOffsetX(font, 0, 0, scale, alignment), 0);
for (var t = 0; t < this.Tokens.Length; t++) {
var token = this.Tokens[t];
var drawFont = token.GetFont(font) ?? font;
var drawColor = token.GetColor(color) ?? color;
for (var i = 0; i < token.DisplayString.Length; i++) {
var c = token.DisplayString[i];
if (c == '\n') {
innerOffset.X = 0;
for (var l = 0; l < token.SplitDisplayString.Length; l++) {
var line = token.SplitDisplayString[l];
for (var i = 0; i < line.Length; i++) {
var c = line[i];
if (l == 0 && i == 0)
token.DrawSelf(time, batch, pos + innerOffset, font, color, scale, depth);
var cString = c.ToCachedString();
token.DrawCharacter(time, batch, c, cString, i, pos + innerOffset, drawFont, drawColor, scale, depth);
innerOffset.X += font.MeasureString(cString).X * scale;
}
// only split at a new line, not between tokens!
if (l < token.SplitDisplayString.Length - 1) {
innerOffset.X = this.GetInnerOffsetX(font, t, l + 1, scale, alignment);
innerOffset.Y += font.LineHeight * scale;
}
if (i == 0)
token.DrawSelf(time, batch, pos + innerOffset, font, color, scale, depth);
var cString = c.ToCachedString();
token.DrawCharacter(time, batch, c, cString, i, pos + innerOffset, drawFont, drawColor, scale, depth);
innerOffset.X += font.MeasureString(cString).X * scale;
}
}
}
private void CalculateTokenAreas(GenericFont font) {
var innerOffset = new Vector2();
foreach (var token in this.Tokens) {
private void StoreModifiedSubstrings(GenericFont font, TextAlignment alignment) {
if (this.Tokens.Length == 1) {
// skip substring logic for unformatted text
this.Tokens[0].ModifiedSubstring = this.modifiedString;
} else {
// this is basically a substring function that ignores added newlines for indexing
var index = 0;
var currToken = 0;
var splitIndex = 0;
var ret = new StringBuilder();
while (splitIndex < this.modifiedString.Length && currToken < this.Tokens.Length) {
var token = this.Tokens[currToken];
if (token.Substring.Length > 0) {
ret.Append(this.modifiedString[splitIndex]);
// if the current char is not an added newline, we simulate length increase
if (this.modifiedString[splitIndex] != '\n' || this.String[index] == '\n')
index++;
splitIndex++;
}
// move on to the next token if we reached its end
if (index >= token.Index + token.Substring.Length) {
token.ModifiedSubstring = ret.ToString();
ret.Clear();
currToken++;
}
}
// set additional token contents beyond our string in case we truncated
if (ret.Length > 0)
this.Tokens[currToken++].ModifiedSubstring = ret.ToString();
while (currToken < this.Tokens.Length)
this.Tokens[currToken++].ModifiedSubstring = string.Empty;
}
this.RecalculateTokenData(font, alignment);
}
private float GetInnerOffsetX(GenericFont font, int tokenIndex, int lineIndex, float scale, TextAlignment alignment) {
if (alignment > TextAlignment.Left) {
var rest = this.GetRestOfLineLength(font, tokenIndex, lineIndex) * scale;
if (alignment == TextAlignment.Center)
rest /= 2;
return -rest;
}
return 0;
}
private float GetRestOfLineLength(GenericFont font, int tokenIndex, int lineIndex) {
var token = this.Tokens[tokenIndex];
var ret = font.MeasureString(token.SplitDisplayString[lineIndex], true).X;
if (lineIndex >= token.SplitDisplayString.Length - 1) {
// the line ends somewhere in or after the next token
for (var i = tokenIndex + 1; i < this.Tokens.Length; i++) {
var other = this.Tokens[i];
if (other.SplitDisplayString.Length > 1) {
// the line ends in this token
ret += font.MeasureString(other.SplitDisplayString[0]).X;
break;
} else {
// the line doesn't end in this token, so add it fully
ret += font.MeasureString(other.DisplayString).X;
}
}
}
return ret;
}
private void RecalculateTokenData(GenericFont font, TextAlignment alignment) {
// split display strings
foreach (var token in this.Tokens)
token.SplitDisplayString = token.DisplayString.Split('\n');
// token areas
var innerOffset = new Vector2(this.GetInnerOffsetX(font, 0, 0, 1, alignment), 0);
for (var t = 0; t < this.Tokens.Length; t++) {
var token = this.Tokens[t];
var area = new List<RectangleF>();
var split = token.DisplayString.Split('\n');
for (var i = 0; i < split.Length; i++) {
var size = font.MeasureString(split[i]);
for (var l = 0; l < token.SplitDisplayString.Length; l++) {
var size = font.MeasureString(token.SplitDisplayString[l]);
var rect = new RectangleF(innerOffset, size);
if (!rect.IsEmpty)
area.Add(rect);
if (i < split.Length - 1) {
innerOffset.X = 0;
if (l < token.SplitDisplayString.Length - 1) {
innerOffset.X = this.GetInnerOffsetX(font, t, l + 1, 1, alignment);
innerOffset.Y += font.LineHeight;
} else {
innerOffset.X += size.X;

View file

@ -25,6 +25,53 @@ namespace MLEM.Input {
this.value = value;
}
/// <inheritdoc />
public override string ToString() {
var ret = this.Type.ToString();
switch (this.Type) {
case InputType.Mouse:
ret += ((MouseButton) this).ToString();
break;
case InputType.Keyboard:
ret += ((Keys) this).ToString();
break;
case InputType.Gamepad:
ret += ((Buttons) this).ToString();
break;
}
return ret;
}
/// <inheritdoc />
public override bool Equals(object obj) {
return obj is GenericInput o && this.Type == o.Type && this.value == o.value;
}
/// <inheritdoc />
public override int GetHashCode() {
return ((int) this.Type * 397) ^ this.value;
}
/// <summary>
/// Compares the two generic input instances for equality using <see cref="Equals"/>
/// </summary>
/// <param name="left">The left input</param>
/// <param name="right">The right input</param>
/// <returns>Whether the two generic inputs are equal</returns>
public static bool operator ==(GenericInput left, GenericInput right) {
return left.Equals(right);
}
/// <summary>
/// Compares the two generic input instances for inequality using <see cref="Equals"/>
/// </summary>
/// <param name="left">The left input</param>
/// <param name="right">The right input</param>
/// <returns>Whether the two generic inputs are not equal</returns>
public static bool operator !=(GenericInput left, GenericInput right) {
return !left.Equals(right);
}
/// <summary>
/// Converts a <see cref="Keys"/> to a generic input.
/// </summary>
@ -57,11 +104,11 @@ namespace MLEM.Input {
/// </summary>
/// <param name="input">The input to convert</param>
/// <returns>The resulting keys</returns>
/// <exception cref="ArgumentException">If the given generic input's <see cref="Type"/> is not <see cref="InputType.Keyboard"/></exception>
/// <exception cref="ArgumentException">If the given generic input's <see cref="Type"/> is not <see cref="InputType.Keyboard"/> or <see cref="InputType.None"/></exception>
public static implicit operator Keys(GenericInput input) {
if (input.Type != InputType.Keyboard)
throw new ArgumentException();
return (Keys) input.value;
if (input.Type == InputType.None)
return Keys.None;
return input.Type == InputType.Keyboard ? (Keys) input.value : throw new ArgumentException();
}
/// <summary>
@ -71,9 +118,7 @@ namespace MLEM.Input {
/// <returns>The resulting button</returns>
/// <exception cref="ArgumentException">If the given generic input's <see cref="Type"/> is not <see cref="InputType.Mouse"/></exception>
public static implicit operator MouseButton(GenericInput input) {
if (input.Type != InputType.Mouse)
throw new ArgumentException();
return (MouseButton) input.value;
return input.Type == InputType.Mouse ? (MouseButton) input.value : throw new ArgumentException();
}
/// <summary>
@ -83,9 +128,7 @@ namespace MLEM.Input {
/// <returns>The resulting buttons</returns>
/// <exception cref="ArgumentException">If the given generic input's <see cref="Type"/> is not <see cref="InputType.Gamepad"/></exception>
public static implicit operator Buttons(GenericInput input) {
if (input.Type != InputType.Gamepad)
throw new ArgumentException();
return (Buttons) input.value;
return input.Type == InputType.Gamepad ? (Buttons) input.value : throw new ArgumentException();
}
/// <summary>
@ -94,6 +137,11 @@ namespace MLEM.Input {
[DataContract]
public enum InputType {
/// <summary>
/// A type representing no value
/// </summary>
[EnumMember]
None,
/// <summary>
/// A type representing <see cref="MouseButton"/>
/// </summary>

View file

@ -23,11 +23,7 @@ namespace MLEM.Input {
/// </summary>
public KeyboardState KeyboardState { get; private set; }
/// <summary>
/// Contains the keyboard keys that are currently being pressed
/// </summary>
public Keys[] PressedKeys { get; private set; }
/// <summary>
/// Set this property to false to disable keyboard handling for this input handler.
/// Set this field to false to disable keyboard handling for this input handler.
/// </summary>
public bool HandleKeyboard;
@ -56,7 +52,7 @@ namespace MLEM.Input {
/// </summary>
public int LastScrollWheel => this.LastMouseState.ScrollWheelValue;
/// <summary>
/// Set this property to false to disable mouse handling for this input handler.
/// Set this field to false to disable mouse handling for this input handler.
/// </summary>
public bool HandleMouse;
@ -64,11 +60,11 @@ namespace MLEM.Input {
private readonly GamePadState[] gamepads = new GamePadState[GamePad.MaximumGamePadCount];
/// <summary>
/// Contains the amount of gamepads that are currently connected.
/// This property is automatically updated in <see cref="Update()"/>
/// This field is automatically updated in <see cref="Update()"/>
/// </summary>
public int ConnectedGamepads { get; private set; }
/// <summary>
/// Set this property to false to disable keyboard handling for this input handler.
/// Set this field to false to disable keyboard handling for this input handler.
/// </summary>
public bool HandleGamepads;
@ -87,7 +83,7 @@ namespace MLEM.Input {
public readonly ReadOnlyCollection<GestureSample> Gestures;
private readonly List<GestureSample> gestures = new List<GestureSample>();
/// <summary>
/// Set this property to false to disable touch handling for this input handler.
/// Set this field to false to disable touch handling for this input handler.
/// </summary>
public bool HandleTouch;
@ -103,7 +99,7 @@ namespace MLEM.Input {
public TimeSpan KeyRepeatRate = TimeSpan.FromSeconds(0.05);
/// <summary>
/// Set this property to false to disable keyboard repeat event handling.
/// Set this field to false to disable keyboard repeat event handling.
/// </summary>
public bool HandleKeyboardRepeats = true;
private DateTime heldKeyStart;
@ -112,7 +108,7 @@ namespace MLEM.Input {
private Keys heldKey;
/// <summary>
/// Set this property to false to disable gamepad repeat event handling.
/// Set this field to false to disable gamepad repeat event handling.
/// </summary>
public bool HandleGamepadRepeats = true;
private readonly DateTime[] heldGamepadButtonStarts = new DateTime[GamePad.MaximumGamePadCount];
@ -120,18 +116,38 @@ namespace MLEM.Input {
private readonly bool[] triggerGamepadButtonRepeat = new bool[GamePad.MaximumGamePadCount];
private readonly Buttons?[] heldGamepadButtons = new Buttons?[GamePad.MaximumGamePadCount];
/// <summary>
/// An array of all <see cref="Keys"/>, <see cref="Buttons"/> and <see cref="MouseButton"/> values that are currently down.
/// Note that this value only gets set if <see cref="StoreAllActiveInputs"/> is true.
/// </summary>
public GenericInput[] InputsDown { get; private set; }
/// <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.
/// Note that this value only gets set if <see cref="StoreAllActiveInputs"/> is true.
/// </summary>
public GenericInput[] InputsPressed { get; private set; }
private readonly List<GenericInput> inputsDownAccum = new List<GenericInput>();
/// <summary>
/// Set this field to false to enable <see cref="InputsDown"/> and <see cref="InputsPressed"/> being calculated.
/// </summary>
public bool StoreAllActiveInputs;
/// <summary>
/// Creates a new input handler with optional initial values.
/// </summary>
/// <param name="game">The game instance that this input handler belongs to</param>
/// <param name="handleKeyboard">If keyboard input should be handled</param>
/// <param name="handleMouse">If mouse input should be handled</param>
/// <param name="handleGamepads">If gamepad input should be handled</param>
/// <param name="handleTouch">If touch input should be handled</param>
public InputHandler(bool handleKeyboard = true, bool handleMouse = true, bool handleGamepads = true, bool handleTouch = true) : base(null) {
/// <param name="storeAllActiveInputs">Whether all inputs that are currently down and pressed should be calculated each update</param>
public InputHandler(Game game, bool handleKeyboard = true, bool handleMouse = true, bool handleGamepads = true, bool handleTouch = true, bool storeAllActiveInputs = true) : base(game) {
this.HandleKeyboard = handleKeyboard;
this.HandleMouse = handleMouse;
this.HandleGamepads = handleGamepads;
this.HandleTouch = handleTouch;
this.StoreAllActiveInputs = storeAllActiveInputs;
this.Gestures = this.gestures.AsReadOnly();
}
@ -140,17 +156,22 @@ namespace MLEM.Input {
/// Call this in your <see cref="Game.Update"/> method.
/// </summary>
public void Update() {
var active = this.Game.IsActive;
if (this.HandleKeyboard) {
this.LastKeyboardState = this.KeyboardState;
this.KeyboardState = Keyboard.GetState();
this.PressedKeys = this.KeyboardState.GetPressedKeys();
this.KeyboardState = active ? Keyboard.GetState() : default;
var pressedKeys = this.KeyboardState.GetPressedKeys();
if (this.StoreAllActiveInputs) {
foreach (var pressed in pressedKeys)
this.inputsDownAccum.Add(pressed);
}
if (this.HandleKeyboardRepeats) {
this.triggerKeyRepeat = false;
if (this.heldKey == Keys.None) {
// if we're not repeating a key, set the first key being held to the repeat key
// note that modifier keys don't count as that wouldn't really make sense
var key = this.PressedKeys.FirstOrDefault(k => !k.IsModifier());
var key = pressedKeys.FirstOrDefault(k => !k.IsModifier());
if (key != Keys.None) {
this.heldKey = key;
this.heldKeyStart = DateTime.UtcNow;
@ -179,16 +200,41 @@ namespace MLEM.Input {
if (this.HandleMouse) {
this.LastMouseState = this.MouseState;
this.MouseState = Mouse.GetState();
var state = Mouse.GetState();
if (active && this.Game.GraphicsDevice.Viewport.Bounds.Contains(state.Position)) {
this.MouseState = state;
if (this.StoreAllActiveInputs) {
foreach (var button in MouseExtensions.MouseButtons) {
if (state.GetState(button) == ButtonState.Pressed)
this.inputsDownAccum.Add(button);
}
}
} else {
// mouse position and scroll wheel value should be preserved when the mouse is out of bounds
this.MouseState = new MouseState(state.X, state.Y, state.ScrollWheelValue, 0, 0, 0, 0, 0, state.HorizontalScrollWheelValue);
}
}
if (this.HandleGamepads) {
this.ConnectedGamepads = GamePad.MaximumGamePadCount;
for (var i = 0; i < GamePad.MaximumGamePadCount; i++) {
this.lastGamepads[i] = this.gamepads[i];
this.gamepads[i] = GamePad.GetState(i);
if (this.ConnectedGamepads > i && !this.gamepads[i].IsConnected)
this.ConnectedGamepads = i;
var state = GamePadState.Default;
if (GamePad.GetCapabilities(i).IsConnected) {
if (active) {
state = GamePad.GetState(i);
if (this.StoreAllActiveInputs) {
foreach (var button in EnumHelper.Buttons) {
if (state.IsButtonDown(button))
this.inputsDownAccum.Add(button);
}
}
}
} else {
if (this.ConnectedGamepads > i)
this.ConnectedGamepads = i;
}
this.gamepads[i] = state;
}
if (this.HandleGamepadRepeats) {
@ -224,12 +270,23 @@ namespace MLEM.Input {
if (this.HandleTouch) {
this.LastTouchState = this.TouchState;
this.TouchState = TouchPanel.GetState();
this.TouchState = active ? TouchPanel.GetState() : default;
this.gestures.Clear();
while (TouchPanel.IsGestureAvailable)
while (active && TouchPanel.IsGestureAvailable)
this.gestures.Add(TouchPanel.ReadGesture());
}
if (this.StoreAllActiveInputs) {
if (this.inputsDownAccum.Count <= 0) {
this.InputsPressed = Array.Empty<GenericInput>();
this.InputsDown = Array.Empty<GenericInput>();
} else {
this.InputsPressed = this.inputsDownAccum.Where(i => !this.InputsDown.Contains(i)).ToArray();
this.InputsDown = this.inputsDownAccum.ToArray();
this.inputsDownAccum.Clear();
}
}
}
/// <inheritdoc cref="Update()"/>
@ -278,6 +335,7 @@ namespace MLEM.Input {
/// <summary>
/// Returns whether the given key is considered pressed.
/// A key is considered pressed if it was not down the last update call, but is down the current update call.
/// If <see cref="HandleKeyboardRepeats"/> is true, this method will also return true to signify a key repeat.
/// </summary>
/// <param name="key">The key to query</param>
/// <returns>If the key is pressed</returns>
@ -285,6 +343,17 @@ namespace MLEM.Input {
// if the queried key is the held key and a repeat should be triggered, return true
if (this.HandleKeyboardRepeats && key == this.heldKey && this.triggerKeyRepeat)
return true;
return this.IsKeyPressedIgnoreRepeats(key);
}
/// <summary>
/// Returns whether the given key is considered pressed.
/// This has the same behavior as <see cref="IsKeyPressed"/>, but ignores keyboard repeat events.
/// If <see cref="HandleKeyboardRepeats"/> is false, this method does the same as <see cref="IsKeyPressed"/>.
/// </summary>
/// <param name="key">The key to query</param>
/// <returns>If the key is pressed</returns>
public bool IsKeyPressedIgnoreRepeats(Keys key) {
return this.WasKeyUp(key) && this.IsKeyDown(key);
}
@ -390,6 +459,7 @@ namespace MLEM.Input {
/// <summary>
/// Returns whether the given gamepad button on the given index is considered pressed.
/// A gamepad button is considered pressed if it was down the last update call, and is up the current update call.
/// If <see cref="HandleGamepadRepeats"/> is true, this method will also return true to signify a gamepad button repeat.
/// </summary>
/// <param name="button">The button to query</param>
/// <param name="index">The zero-based index of the gamepad, or -1 for any gamepad</param>
@ -404,6 +474,18 @@ namespace MLEM.Input {
return true;
}
}
return this.IsGamepadButtonPressedIgnoreRepeats(button, index);
}
/// <summary>
/// Returns whether the given key is considered pressed.
/// This has the same behavior as <see cref="IsGamepadButtonPressed"/>, but ignores gamepad repeat events.
/// If <see cref="HandleGamepadRepeats"/> is false, this method does the same as <see cref="IsGamepadButtonPressed"/>.
/// </summary>
/// <param name="button">The button to query</param>
/// <param name="index">The zero-based index of the gamepad, or -1 for any gamepad</param>
/// <returns>Whether the given button is pressed</returns>
public bool IsGamepadButtonPressedIgnoreRepeats(Buttons button, int index = -1) {
return this.WasGamepadButtonUp(button, index) && this.IsGamepadButtonDown(button, index);
}
@ -420,6 +502,7 @@ namespace MLEM.Input {
return true;
}
}
sample = default;
return false;
}
@ -440,7 +523,7 @@ namespace MLEM.Input {
case GenericInput.InputType.Mouse:
return this.IsMouseButtonDown(control);
default:
throw new ArgumentException(nameof(control));
return false;
}
}
@ -461,7 +544,7 @@ namespace MLEM.Input {
case GenericInput.InputType.Mouse:
return this.IsMouseButtonUp(control);
default:
throw new ArgumentException(nameof(control));
return true;
}
}
@ -482,7 +565,7 @@ namespace MLEM.Input {
case GenericInput.InputType.Mouse:
return this.IsMouseButtonPressed(control);
default:
throw new ArgumentException(nameof(control));
return false;
}
}

View file

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
@ -12,7 +13,27 @@ namespace MLEM.Input {
public class Keybind {
[DataMember]
private readonly List<Combination> combinations = new List<Combination>();
private Combination[] combinations = Array.Empty<Combination>();
/// <summary>
/// Creates a new keybind and adds the given key and modifiers using <see cref="Add(MLEM.Input.GenericInput,MLEM.Input.GenericInput[])"/>
/// </summary>
/// <param name="key">The key to be pressed.</param>
/// <param name="modifiers">The modifier keys that have to be held down.</param>
public Keybind(GenericInput key, params GenericInput[] modifiers) {
this.Add(key, modifiers);
}
/// <inheritdoc cref="Keybind(GenericInput, GenericInput[])"/>
public Keybind(GenericInput key, ModifierKey modifier) {
this.Add(key, modifier);
}
/// <summary>
/// Creates a new keybind with no default combinations
/// </summary>
public Keybind() {
}
/// <summary>
/// Adds a new key combination to this keybind that can optionally be pressed for the keybind to trigger.
@ -21,7 +42,7 @@ namespace MLEM.Input {
/// <param name="modifiers">The modifier keys that have to be held down.</param>
/// <returns>This keybind, for chaining</returns>
public Keybind Add(GenericInput key, params GenericInput[] modifiers) {
this.combinations.Add(new Combination(key, modifiers));
this.combinations = this.combinations.Append(new Combination(key, modifiers)).ToArray();
return this;
}
@ -35,7 +56,17 @@ namespace MLEM.Input {
/// </summary>
/// <returns>This keybind, for chaining</returns>
public Keybind Clear() {
this.combinations.Clear();
this.combinations = Array.Empty<Combination>();
return this;
}
/// <summary>
/// Removes all combinations that match the given predicate
/// </summary>
/// <param name="predicate">The predicate to match against</param>
/// <returns>This keybind, for chaining</returns>
public Keybind Remove(Func<Combination, int, bool> predicate) {
this.combinations = this.combinations.Where((c, i) => !predicate(c, i)).ToArray();
return this;
}
@ -46,7 +77,7 @@ namespace MLEM.Input {
/// <param name="other">The keybind to copy from</param>
/// <returns>This keybind, for chaining</returns>
public Keybind CopyFrom(Keybind other) {
this.combinations.AddRange(other.combinations);
this.combinations = this.combinations.Concat(other.combinations).ToArray();
return this;
}
@ -72,29 +103,85 @@ namespace MLEM.Input {
return this.combinations.Any(c => c.IsPressed(handler, gamepadIndex));
}
/// <summary>
/// Returns whether any of this keybind's modifier keys are currently down.
/// See <see cref="InputHandler.IsDown"/> for more information.
/// </summary>
/// <param name="handler">The input handler to query the keys with</param>
/// <param name="gamepadIndex">The index of the gamepad to query, or -1 to query all gamepads</param>
/// <returns>Whether any of this keyboard's modifier keys are down</returns>
public bool IsModifierDown(InputHandler handler, int gamepadIndex = -1) {
return this.combinations.Any(c => c.IsModifierDown(handler, gamepadIndex));
}
/// <summary>
/// Returns an enumerable of all of the combinations that this keybind currently contains
/// </summary>
/// <returns>This keybind's combinations</returns>
public IEnumerable<Combination> GetCombinations() {
foreach (var combination in this.combinations)
yield return combination;
}
/// <summary>
/// A key combination is a combination of a set of modifier keys and a key.
/// All of the keys are <see cref="GenericInput"/> instances, so they can be keyboard-, mouse- or gamepad-based.
/// </summary>
[DataContract]
private class Combination {
public class Combination {
/// <summary>
/// The inputs that have to be held down for this combination to be valid.
/// If this collection is empty, there are no required modifier keys.
/// </summary>
[DataMember]
private readonly GenericInput[] modifiers;
public readonly GenericInput[] Modifiers;
/// <summary>
/// The input that has to be down (or pressed) for this combination to be considered down (or pressed).
/// Note that <see cref="Modifiers"/> needs to be empty, or all of its values need to be down, as well.
/// </summary>
[DataMember]
private readonly GenericInput key;
public readonly GenericInput Key;
/// <summary>
/// Creates a new combination with the given settings.
/// To add a combination to a <see cref="Keybind"/>, use <see cref="Keybind.Add(MLEM.Input.GenericInput,MLEM.Input.GenericInput[])"/> instead.
/// </summary>
/// <param name="key">The key</param>
/// <param name="modifiers">The modifiers</param>
public Combination(GenericInput key, GenericInput[] modifiers) {
this.modifiers = modifiers;
this.key = key;
this.Modifiers = modifiers;
this.Key = key;
}
internal bool IsDown(InputHandler handler, int gamepadIndex = -1) {
return this.IsModifierDown(handler, gamepadIndex) && handler.IsDown(this.key, gamepadIndex);
/// <summary>
/// Returns whether this combination is currently down
/// </summary>
/// <param name="handler">The input handler to query the keys with</param>
/// <param name="gamepadIndex">The index of the gamepad to query, or -1 to query all gamepads</param>
/// <returns>Whether this combination is down</returns>
public bool IsDown(InputHandler handler, int gamepadIndex = -1) {
return this.IsModifierDown(handler, gamepadIndex) && handler.IsDown(this.Key, gamepadIndex);
}
internal bool IsPressed(InputHandler handler, int gamepadIndex = -1) {
return this.IsModifierDown(handler, gamepadIndex) && handler.IsPressed(this.key, gamepadIndex);
/// <summary>
/// Returns whether this combination is currently pressed
/// </summary>
/// <param name="handler">The input handler to query the keys with</param>
/// <param name="gamepadIndex">The index of the gamepad to query, or -1 to query all gamepads</param>
/// <returns>Whether this combination is pressed</returns>
public bool IsPressed(InputHandler handler, int gamepadIndex = -1) {
return this.IsModifierDown(handler, gamepadIndex) && handler.IsPressed(this.Key, gamepadIndex);
}
private bool IsModifierDown(InputHandler handler, int gamepadIndex = -1) {
return this.modifiers.Length <= 0 || this.modifiers.Any(m => handler.IsDown(m, gamepadIndex));
/// <summary>
/// Returns whether this combination's modifier keys are currently down
/// </summary>
/// <param name="handler">The input handler to query the keys with</param>
/// <param name="gamepadIndex">The index of the gamepad to query, or -1 to query all gamepads</param>
/// <returns>Whether this combination's modifiers are down</returns>
public bool IsModifierDown(InputHandler handler, int gamepadIndex = -1) {
return this.Modifiers.Length <= 0 || this.Modifiers.Any(m => handler.IsDown(m, gamepadIndex));
}
}

View file

@ -50,6 +50,11 @@ namespace MLEM.Input {
return ModifierKey.None;
}
/// <inheritdoc cref="GetModifier(Microsoft.Xna.Framework.Input.Keys)"/>
public static ModifierKey GetModifier(this GenericInput input) {
return input.Type == GenericInput.InputType.Keyboard ? GetModifier((Keys) input) : ModifierKey.None;
}
/// <summary>
/// Returns whether the given key is a modifier key or not.
/// </summary>
@ -59,6 +64,11 @@ namespace MLEM.Input {
return GetModifier(key) != ModifierKey.None;
}
/// <inheritdoc cref="IsModifier(Microsoft.Xna.Framework.Input.Keys)"/>
public static bool IsModifier(this GenericInput input) {
return GetModifier(input) != ModifierKey.None;
}
}
/// <summary>
@ -68,7 +78,7 @@ namespace MLEM.Input {
public enum ModifierKey {
/// <summary>
/// No modifier key. Only used for <see cref="KeysExtensions.GetModifier"/>
/// No modifier key. Only used for <see cref="KeysExtensions.GetModifier(Keys)"/>
/// </summary>
None,
/// <summary>

View file

@ -7,16 +7,21 @@
<PropertyGroup>
<Authors>Ellpeck</Authors>
<Description>MLEM Library for Extending MonoGame provides extension methods and additional features for MonoGame</Description>
<PackageReleaseNotes>See the full changelog at https://github.com/Ellpeck/MLEM/blob/main/CHANGELOG.md</PackageReleaseNotes>
<PackageTags>monogame ellpeck mlem utility extensions</PackageTags>
<PackageProjectUrl>https://mlem.ellpeck.de/</PackageProjectUrl>
<RepositoryUrl>https://github.com/Ellpeck/MLEM</RepositoryUrl>
<PackageLicenseUrl>https://github.com/Ellpeck/MLEM/blob/main/LICENSE</PackageLicenseUrl>
<PackageIconUrl>https://raw.githubusercontent.com/Ellpeck/MLEM/main/Media/Logo.png</PackageIconUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageIcon>Logo.png</PackageIcon>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MonoGame.Framework.Portable" Version="3.7.1.189">
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.0.1641">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<None Include="../Media/Logo.png" Pack="true" PackagePath="" />
</ItemGroup>
</Project>

View file

@ -6,7 +6,7 @@ namespace MLEM.Misc {
/// This class contains a <see cref="DrawAutoTile"/> method that allows users to easily draw a tile with automatic connections.
/// For auto-tiling in this manner to work, auto-tiled textures have to be laid out in a format described in <see cref="DrawAutoTile"/>.
/// </summary>
public class AutoTiling {
public static class AutoTiling {
/// <summary>
/// This method allows for a tiled texture to be drawn in an auto-tiling mode.
@ -39,12 +39,12 @@ namespace MLEM.Misc {
var xDl = down && left ? connectsTo(-1, 1) ? 0 : 4 : left ? 1 : down ? 3 : 2;
var xDr = down && right ? connectsTo(1, 1) ? 0 : 4 : right ? 1 : down ? 3 : 2;
var size = textureRegion.Size;
var halfSize = new Point(size.X / 2, size.Y / 2);
batch.Draw(texture, new Vector2(pos.X, pos.Y), new Rectangle(textureRegion.X + 0 + xUl * size.X, textureRegion.Y + 0, halfSize.X, halfSize.Y), color, rotation, org, sc, SpriteEffects.None, layerDepth);
batch.Draw(texture, new Vector2(pos.X + 0.5F * size.X * sc.X, pos.Y), new Rectangle(textureRegion.X + halfSize.X + xUr * size.X, textureRegion.Y + 0, halfSize.X, halfSize.Y), color, rotation, org, sc, SpriteEffects.None, layerDepth);
batch.Draw(texture, new Vector2(pos.X, pos.Y + 0.5F * size.Y * sc.Y), new Rectangle(textureRegion.X + xDl * size.X, textureRegion.Y + halfSize.Y, halfSize.X, halfSize.Y), color, rotation, org, sc, SpriteEffects.None, layerDepth);
batch.Draw(texture, new Vector2(pos.X + 0.5F * size.X * sc.X, pos.Y + 0.5F * size.Y * sc.Y), new Rectangle(textureRegion.X + halfSize.X + xDr * size.X, textureRegion.Y + halfSize.Y, halfSize.X, halfSize.Y), color, rotation, org, sc, SpriteEffects.None, layerDepth);
var (w, h) = textureRegion.Size;
var (w2, h2) = new Point(w / 2, h / 2);
batch.Draw(texture, new Vector2(pos.X, pos.Y), new Rectangle(textureRegion.X + 0 + xUl * w, textureRegion.Y + 0, w2, h2), color, rotation, org, sc, SpriteEffects.None, layerDepth);
batch.Draw(texture, new Vector2(pos.X + 0.5F * w * sc.X, pos.Y), new Rectangle(textureRegion.X + w2 + xUr * w, textureRegion.Y + 0, w2, h2), color, rotation, org, sc, SpriteEffects.None, layerDepth);
batch.Draw(texture, new Vector2(pos.X, pos.Y + 0.5F * h * sc.Y), new Rectangle(textureRegion.X + xDl * w, textureRegion.Y + h2, w2, h2), color, rotation, org, sc, SpriteEffects.None, layerDepth);
batch.Draw(texture, new Vector2(pos.X + 0.5F * w * sc.X, pos.Y + 0.5F * h * sc.Y), new Rectangle(textureRegion.X + w2 + xDr * w, textureRegion.Y + h2, w2, h2), color, rotation, org, sc, SpriteEffects.None, layerDepth);
}
/// <summary>

View file

@ -1,53 +1,65 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using Microsoft.Xna.Framework;
namespace MLEM.Misc {
/// <summary>
/// An enum that represents two-dimensional directions.
/// Both straight and diagonal directions are supported.
/// There are several extension methods and arrays available in <see cref="Direction2Helper"/>.
/// </summary>
[Flags, DataContract]
public enum Direction2 {
/// <summary>
/// The up direction, or -y.
/// </summary>
Up,
/// <summary>
/// The right direction, or +x.
/// </summary>
Right,
/// <summary>
/// The down direction, or +y.
/// </summary>
Down,
/// <summary>
/// The left direction, or -x.
/// </summary>
Left,
/// <summary>
/// The up and right direction, or +x, -y.
/// </summary>
UpRight,
/// <summary>
/// The down and right direction, or +x, +y.
/// </summary>
DownRight,
/// <summary>
/// The down and left direction, or -x, +y.
/// </summary>
DownLeft,
/// <summary>
/// The up and left direction, or -x, -y.
/// </summary>
UpLeft,
/// <summary>
/// No direction.
/// </summary>
None
[EnumMember]
None = 0,
/// <summary>
/// The up direction, or -y.
/// </summary>
[EnumMember]
Up = 1,
/// <summary>
/// The right direction, or +x.
/// </summary>
[EnumMember]
Right = 2,
/// <summary>
/// The down direction, or +y.
/// </summary>
[EnumMember]
Down = 4,
/// <summary>
/// The left direction, or -x.
/// </summary>
[EnumMember]
Left = 8,
/// <summary>
/// The up and right direction, or +x, -y.
/// </summary>
[EnumMember]
UpRight = Up | Right,
/// <summary>
/// The down and right direction, or +x, +y.
/// </summary>
[EnumMember]
DownRight = Down | Right,
/// <summary>
/// The up and left direction, or -x, -y.
/// </summary>
[EnumMember]
UpLeft = Up | Left,
/// <summary>
/// The down and left direction, or -x, +y.
/// </summary>
[EnumMember]
DownLeft = Down | Left
}
@ -80,7 +92,7 @@ namespace MLEM.Misc {
/// <param name="dir">The direction to query</param>
/// <returns>Whether the direction is adjacent</returns>
public static bool IsAdjacent(this Direction2 dir) {
return dir <= Direction2.Left;
return dir == Direction2.Up || dir == Direction2.Right || dir == Direction2.Down || dir == Direction2.Left;
}
/// <summary>
@ -89,7 +101,7 @@ namespace MLEM.Misc {
/// <param name="dir">The direction to query</param>
/// <returns>Whether the direction is diagonal</returns>
public static bool IsDiagonal(this Direction2 dir) {
return dir >= Direction2.UpRight && dir <= Direction2.UpLeft;
return dir == Direction2.UpRight || dir == Direction2.DownRight || dir == Direction2.UpLeft || dir == Direction2.DownLeft;
}
/// <summary>
@ -167,8 +179,8 @@ namespace MLEM.Misc {
/// <param name="dir">The direction whose angle to get</param>
/// <returns>The direction's angle</returns>
public static float Angle(this Direction2 dir) {
var offset = dir.Offset();
return (float) Math.Atan2(offset.Y, offset.X);
var (x, y) = dir.Offset();
return (float) Math.Atan2(y, x);
}
/// <summary>

190
MLEM/Misc/MlemPlatform.cs Normal file
View file

@ -0,0 +1,190 @@
using System;
using System.Diagnostics;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using MLEM.Formatting.Codes;
namespace MLEM.Misc {
/// <summary>
/// MlemPlatform is a wrapper around some of MonoGame's platform-dependent behavior to allow for MLEM to stay platform-independent.
/// See <see cref="DesktopGl{T}"/>, <see cref="Mobile"/> and <see cref="None"/> for information on the specific platforms.
/// The MLEM demos' main classes also make use of this functionality: <see href="https://github.com/Ellpeck/MLEM/blob/main/Demos.DesktopGL/Program.cs#L8"/> and <see href="https://github.com/Ellpeck/MLEM/blob/main/Demos.Android/Activity1.cs#L33"/>.
/// </summary>
public abstract class MlemPlatform {
/// <summary>
/// The current MLEM platform
/// Set this value before starting your game if you want to use platform-dependent MLEM features.
/// </summary>
/// <exception cref="InvalidOperationException"></exception>
public static MlemPlatform Current;
/// <summary>
/// Opens the on-screen keyboard if one is required by the platform.
/// Note that, if no on-screen keyboard is required, a null string should be returned.
/// </summary>
/// <param name="title">Title of the dialog box.</param>
/// <param name="description">Description of the dialog box.</param>
/// <param name="defaultText">Default text displayed in the input area.</param>
/// <param name="usePasswordMode">If password mode is enabled, the characters entered are not displayed.</param>
/// <returns>Text entered by the player. Null if back was used.</returns>
public abstract Task<string> OpenOnScreenKeyboard(string title, string description, string defaultText, bool usePasswordMode);
/// <summary>
/// Adds a text input listener to this platform, if supported.
/// The supplied listener will be called whenever a character is input.
/// </summary>
/// <param name="window">The game's window</param>
/// <param name="callback">The callback that should be called whenever a character is pressed</param>
public abstract void AddTextInputListener(GameWindow window, TextInputCallback callback);
/// <summary>
/// A method that should be executed to open a link in the browser or a file explorer.
/// This method is currently used only by MLEM.Ui's implementation of the <see cref="LinkCode"/> formatting code.
/// </summary>
public abstract void OpenLinkOrFile(string link);
/// <summary>
/// Ensures that <see cref="Current"/> is set to a valid <see cref="MlemPlatform"/> value by throwing an <see cref="InvalidOperationException"/> exception if <see cref="Current"/> is null.
/// </summary>
/// <exception cref="InvalidOperationException">If <see cref="Current"/> is null</exception>
public static void EnsureExists() {
if (Current == null)
throw new InvalidOperationException("MlemPlatform was not initialized. For more information, see the MlemPlatform class or https://mlem.ellpeck.de/api/MLEM.Misc.MlemPlatform");
}
/// <summary>
/// A delegate method that can be used for <see cref="MlemPlatform.AddTextInputListener"/>
/// </summary>
/// <param name="sender">The object that sent the event. The <see cref="MlemPlatform"/> used in most cases.</param>
/// <param name="key">The key that was pressed</param>
/// <param name="character">The character that corresponds to that key</param>
public delegate void TextInputCallback(object sender, Keys key, char character);
/// <summary>
/// The MLEM DesktopGL platform.
/// This platform uses the built-in MonoGame TextInput event, which makes this listener work with any keyboard localization natively.
/// </summary>
/// <example>
/// This platform is initialized as follows:
/// <code>
/// new MlemPlatform.DesktopGl{TextInputEventArgs}((w, c) => w.TextInput += c)
/// </code>
/// </example>
/// <typeparam name="T"></typeparam>
public class DesktopGl<T> : MlemPlatform {
private FieldInfo key;
private FieldInfo character;
private readonly Action<GameWindow, EventHandler<T>> addListener;
/// <summary>
/// Creates a new DesktopGL-based platform
/// See <see cref="MlemPlatform.DesktopGl{T}"/> class documentation for more detailed information.
/// </summary>
/// <param name="addListener">The function that is used to add a text input listener</param>
public DesktopGl(Action<GameWindow, EventHandler<T>> addListener) {
this.addListener = addListener;
}
/// <inheritdoc />
public override Task<string> OpenOnScreenKeyboard(string title, string description, string defaultText, bool usePasswordMode) {
return Task.FromResult<string>(null);
}
/// <inheritdoc />
public override void AddTextInputListener(GameWindow window, TextInputCallback callback) {
this.addListener(window, (sender, args) => {
if (this.key == null)
this.key = args.GetType().GetField("Key");
if (this.character == null)
this.character = args.GetType().GetField("Character");
callback.Invoke(sender, (Keys) this.key.GetValue(args), (char) this.character.GetValue(args));
});
}
/// <inheritdoc />
public override void OpenLinkOrFile(string link) {
Process.Start(new ProcessStartInfo(link) {UseShellExecute = true});
}
}
/// <summary>
/// The MLEM platform for mobile platforms as well as consoles.
/// This platform opens an on-screen keyboard using the <see cref="Microsoft.Xna.Framework.Input"/> <c>KeyboardInput</c> class on mobile devices.
/// Additionally, it starts a new activity whenever <see cref="OpenLinkOrFile"/> is called.
/// </summary>
/// <example>
/// This listener is initialized as follows in the game's <c>Activity</c> class:
/// <code>
/// new MlemPlatform.Mobile(KeyboardInput.Show, l =&gt; this.StartActivity(new Intent(Intent.ActionView, Uri.Parse(l))))
/// </code>
/// </example>
public class Mobile : MlemPlatform {
private readonly OpenOnScreenKeyboardDelegate openOnScreenKeyboard;
private readonly Action<string> openLink;
/// <summary>
/// Creates a new mobile- and console-based platform.
/// See <see cref="MlemPlatform.Mobile"/> class documentation for more detailed information.
/// </summary>
/// <param name="openOnScreenKeyboard">The function that is used to display the on-screen keyboard</param>
/// <param name="openLink">The action that is invoked to open a link in the browser, which is used for <see cref="LinkCode"/></param>
public Mobile(OpenOnScreenKeyboardDelegate openOnScreenKeyboard, Action<string> openLink = null) {
this.openOnScreenKeyboard = openOnScreenKeyboard;
this.openLink = openLink;
}
/// <inheritdoc />
public override Task<string> OpenOnScreenKeyboard(string title, string description, string defaultText, bool usePasswordMode) {
return this.openOnScreenKeyboard(title, description, defaultText, usePasswordMode);
}
/// <inheritdoc />
public override void AddTextInputListener(GameWindow window, TextInputCallback callback) {
}
/// <inheritdoc />
public override void OpenLinkOrFile(string link) {
this.openLink?.Invoke(link);
}
/// <summary>
/// A delegate method used for <see cref="Mobile.OpenOnScreenKeyboard"/>
/// </summary>
/// <param name="title">Title of the dialog box.</param>
/// <param name="description">Description of the dialog box.</param>
/// <param name="defaultText">Default text displayed in the input area.</param>
/// <param name="usePasswordMode">If password mode is enabled, the characters entered are not displayed.</param>
/// <returns>Text entered by the player. Null if back was used.</returns>
public delegate Task<string> OpenOnScreenKeyboardDelegate(string title, string description, string defaultText, bool usePasswordMode);
}
/// <summary>
/// A MLEM platform implementation that does nothing.
/// This can be used if no platform-dependent code is required for the game.
/// </summary>
public class None : MlemPlatform {
/// <inheritdoc />
public override Task<string> OpenOnScreenKeyboard(string title, string description, string defaultText, bool usePasswordMode) {
return Task.FromResult<string>(null);
}
/// <inheritdoc />
public override void AddTextInputListener(GameWindow window, TextInputCallback callback) {
}
/// <inheritdoc />
public override void OpenLinkOrFile(string link) {
}
}
}
}

View file

@ -1,3 +1,4 @@
using System.Runtime.Serialization;
using Microsoft.Xna.Framework;
namespace MLEM.Misc {
@ -5,23 +6,28 @@ namespace MLEM.Misc {
/// Represents a generic padding.
/// A padding is an object of data that stores an offset from each side of a rectangle or square.
/// </summary>
[DataContract]
public struct Padding {
/// <summary>
/// The amount of padding on the left side
/// </summary>
[DataMember]
public float Left;
/// <summary>
/// The amount of padding on the right side
/// </summary>
[DataMember]
public float Right;
/// <summary>
/// The amount of padding on the top side
/// </summary>
[DataMember]
public float Top;
/// <summary>
/// The amount of padding on the bottom side
/// </summary>
[DataMember]
public float Bottom;
/// <summary>
/// The total width of this padding, a sum of the left and right padding.

View file

@ -1,4 +1,5 @@
using Microsoft.Xna.Framework.Audio;
using MLEM.Extensions;
namespace MLEM.Misc {
/// <summary>
@ -49,13 +50,10 @@ namespace MLEM.Misc {
/// <summary>
/// Creates a new <see cref="SoundEffectInstance"/> with this sound effect info's data.
/// </summary>
/// <param name="isLooped">The value to set the returned instance's <see cref="SoundEffectInstance.IsLooped"/> to. Defaults to false.</param>
/// <returns>A new sound effect instance, with this info's data applied</returns>
public SoundEffectInstance CreateInstance() {
var instance = this.Sound.CreateInstance();
instance.Volume = this.Volume;
instance.Pitch = this.Pitch;
instance.Pan = this.Pan;
return instance;
public SoundEffectInstance CreateInstance(bool isLooped = false) {
return this.Sound.CreateInstance(this.Volume, this.Pitch, this.Pan, isLooped);
}
}

View file

@ -1,267 +0,0 @@
using System;
using System.Linq;
using System.Reflection;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using MLEM.Input;
namespace MLEM.Misc {
/// <summary>
/// A text input wrapper is a wrapper around MonoGame's built-in text input event.
/// Since said text input event does not exist on non-Desktop devices, we want to wrap it in a wrapper that is platform-independent for MLEM.
/// See subclasses of this wrapper or <see href="https://mlem.ellpeck.de/articles/ui.html#text-input"/> for more info.
/// </summary>
public abstract class TextInputWrapper {
/// <summary>
/// The current text input wrapper.
/// Set this value before starting your game if you want to use text input wrapping.
/// </summary>
/// <exception cref="InvalidOperationException"></exception>
public static TextInputWrapper Current;
/// <summary>
/// Ensures that <see cref="Current"/> is set to a valid <see cref="TextInputWrapper"/> value by throwing an <see cref="InvalidOperationException"/> exception if <see cref="Current"/> is null.
/// </summary>
/// <exception cref="InvalidOperationException">If <see cref="Current"/> is null</exception>
public static void EnsureExists() {
if (Current == null)
throw new InvalidOperationException("The TextInputWrapper was not initialized. For more information, see https://mlem.ellpeck.de/articles/ui.html#text-input");
}
/// <summary>
/// Determines if this text input wrapper requires an on-screen keyboard.
/// </summary>
/// <returns>If this text input wrapper requires an on-screen keyboard</returns>
public abstract bool RequiresOnScreenKeyboard();
/// <summary>
/// Adds a text input listener to this text input wrapper.
/// The supplied listener will be called whenever a character is input.
/// </summary>
/// <param name="window">The game's window</param>
/// <param name="callback">The callback that should be called whenever a character is pressed</param>
public abstract void AddListener(GameWindow window, TextInputCallback callback);
/// <summary>
/// A delegate method that can be used for <see cref="TextInputWrapper.AddListener"/>
/// </summary>
/// <param name="sender">The object that sent the event. The <see cref="TextInputWrapper"/> used in most cases.</param>
/// <param name="key">The key that was pressed</param>
/// <param name="character">The character that corresponds to that key</param>
public delegate void TextInputCallback(object sender, Keys key, char character);
/// <summary>
/// A text input wrapper for DesktopGL devices.
/// This wrapper uses the built-in MonoGame TextInput event, which makes this listener work with any keyboard localization natively.
/// </summary>
/// <example>
/// This listener is initialized as follows:
/// <code>
/// new TextInputWrapper.DesktopGl{TextInputEventArgs}((w, c) => w.TextInput += c);
/// </code>
/// </example>
/// <typeparam name="T"></typeparam>
public class DesktopGl<T> : TextInputWrapper {
private MemberInfo key;
private MemberInfo character;
private readonly Action<GameWindow, EventHandler<T>> addListener;
/// <summary>
/// Creates a new DesktopGL-based text input wrapper
/// </summary>
/// <param name="addListener">The function that is used to add a text input listener</param>
public DesktopGl(Action<GameWindow, EventHandler<T>> addListener) {
this.addListener = addListener;
}
/// <inheritdoc />
public override bool RequiresOnScreenKeyboard() {
return false;
}
/// <inheritdoc />
public override void AddListener(GameWindow window, TextInputCallback callback) {
this.addListener(window, (sender, args) => {
// the old versions of DesktopGL use a property here, while the
// core version uses a field. So much for "no breaking changes"
if (this.key == null)
this.key = GetMember(args, "Key");
if (this.character == null)
this.character = GetMember(args, "Character");
callback.Invoke(sender, GetValue<Keys>(this.key, args), GetValue<char>(this.character, args));
});
}
private static MemberInfo GetMember(object args, string name) {
var ret = args.GetType().GetProperty(name);
if (ret != null)
return ret;
return args.GetType().GetField(name);
}
private static U GetValue<U>(MemberInfo member, object args) {
switch (member) {
case PropertyInfo p:
return (U) p.GetValue(args);
case FieldInfo f:
return (U) f.GetValue(args);
}
throw new ArgumentException();
}
}
/// <summary>
/// A text input wrapper for mobile platforms as well as consoles.
/// This text input wrapper performs no actions itself, as it signals that an on-screen keyboard is required.
/// </summary>
public class Mobile : TextInputWrapper {
/// <inheritdoc />
public override bool RequiresOnScreenKeyboard() {
return true;
}
/// <inheritdoc />
public override void AddListener(GameWindow window, TextInputCallback callback) {
}
}
/// <summary>
/// A text input wrapper that does nothing.
/// This can be used if no text input is required for the game.
/// </summary>
public class None : TextInputWrapper {
/// <inheritdoc />
public override bool RequiresOnScreenKeyboard() {
return false;
}
/// <inheritdoc />
public override void AddListener(GameWindow window, TextInputCallback callback) {
}
}
/// <summary>
/// A primitive text input wrapper that is locked to the American keyboard localization.
/// Only use this text input wrapper if <see cref="TextInputWrapper.DesktopGl{T}"/> is unavailable for some reason.
///
/// Note that, when using this text input wrapper, its <see cref="Update"/> method has to be called periodically.
/// </summary>
public class Primitive : TextInputWrapper {
private TextInputCallback callback;
/// <summary>
/// Updates this text input wrapper by querying pressed keys and sending corresponding input events.
/// </summary>
/// <param name="handler">The input handler to use for text input querying</param>
public void Update(InputHandler handler) {
var pressed = handler.KeyboardState.GetPressedKeys().Except(handler.LastKeyboardState.GetPressedKeys());
var shift = handler.IsModifierKeyDown(ModifierKey.Shift);
foreach (var key in pressed) {
var c = GetChar(key, shift);
if (c.HasValue)
this.callback?.Invoke(this, key, c.Value);
}
}
/// <inheritdoc />
public override bool RequiresOnScreenKeyboard() {
return false;
}
/// <inheritdoc />
public override void AddListener(GameWindow window, TextInputCallback callback) {
this.callback += callback;
}
private static char? GetChar(Keys key, bool shift) {
if (key == Keys.A) return shift ? 'A' : 'a';
if (key == Keys.B) return shift ? 'B' : 'b';
if (key == Keys.C) return shift ? 'C' : 'c';
if (key == Keys.D) return shift ? 'D' : 'd';
if (key == Keys.E) return shift ? 'E' : 'e';
if (key == Keys.F) return shift ? 'F' : 'f';
if (key == Keys.G) return shift ? 'G' : 'g';
if (key == Keys.H) return shift ? 'H' : 'h';
if (key == Keys.I) return shift ? 'I' : 'i';
if (key == Keys.J) return shift ? 'J' : 'j';
if (key == Keys.K) return shift ? 'K' : 'k';
if (key == Keys.L) return shift ? 'L' : 'l';
if (key == Keys.M) return shift ? 'M' : 'm';
if (key == Keys.N) return shift ? 'N' : 'n';
if (key == Keys.O) return shift ? 'O' : 'o';
if (key == Keys.P) return shift ? 'P' : 'p';
if (key == Keys.Q) return shift ? 'Q' : 'q';
if (key == Keys.R) return shift ? 'R' : 'r';
if (key == Keys.S) return shift ? 'S' : 's';
if (key == Keys.T) return shift ? 'T' : 't';
if (key == Keys.U) return shift ? 'U' : 'u';
if (key == Keys.V) return shift ? 'V' : 'v';
if (key == Keys.W) return shift ? 'W' : 'w';
if (key == Keys.X) return shift ? 'X' : 'x';
if (key == Keys.Y) return shift ? 'Y' : 'y';
if (key == Keys.Z) return shift ? 'Z' : 'z';
if (key == Keys.D0 && !shift || key == Keys.NumPad0) return '0';
if (key == Keys.D1 && !shift || key == Keys.NumPad1) return '1';
if (key == Keys.D2 && !shift || key == Keys.NumPad2) return '2';
if (key == Keys.D3 && !shift || key == Keys.NumPad3) return '3';
if (key == Keys.D4 && !shift || key == Keys.NumPad4) return '4';
if (key == Keys.D5 && !shift || key == Keys.NumPad5) return '5';
if (key == Keys.D6 && !shift || key == Keys.NumPad6) return '6';
if (key == Keys.D7 && !shift || key == Keys.NumPad7) return '7';
if (key == Keys.D8 && !shift || key == Keys.NumPad8) return '8';
if (key == Keys.D9 && !shift || key == Keys.NumPad9) return '9';
if (key == Keys.D0 && shift) return ')';
if (key == Keys.D1 && shift) return '!';
if (key == Keys.D2 && shift) return '@';
if (key == Keys.D3 && shift) return '#';
if (key == Keys.D4 && shift) return '$';
if (key == Keys.D5 && shift) return '%';
if (key == Keys.D6 && shift) return '^';
if (key == Keys.D7 && shift) return '&';
if (key == Keys.D8 && shift) return '*';
if (key == Keys.D9 && shift) return '(';
if (key == Keys.Space) return ' ';
if (key == Keys.Tab) return '\t';
if (key == Keys.Add) return '+';
if (key == Keys.Decimal) return '.';
if (key == Keys.Divide) return '/';
if (key == Keys.Multiply) return '*';
if (key == Keys.OemBackslash) return '\\';
if (key == Keys.OemComma && !shift) return ',';
if (key == Keys.OemComma && shift) return '<';
if (key == Keys.OemOpenBrackets && !shift) return '[';
if (key == Keys.OemOpenBrackets && shift) return '{';
if (key == Keys.OemCloseBrackets && !shift) return ']';
if (key == Keys.OemCloseBrackets && shift) return '}';
if (key == Keys.OemPeriod && !shift) return '.';
if (key == Keys.OemPeriod && shift) return '>';
if (key == Keys.OemPipe && !shift) return '\\';
if (key == Keys.OemPipe && shift) return '|';
if (key == Keys.OemPlus && !shift) return '=';
if (key == Keys.OemPlus && shift) return '+';
if (key == Keys.OemMinus && !shift) return '-';
if (key == Keys.OemMinus && shift) return '_';
if (key == Keys.OemQuestion && !shift) return '/';
if (key == Keys.OemQuestion && shift) return '?';
if (key == Keys.OemQuotes && !shift) return '\'';
if (key == Keys.OemQuotes && shift) return '"';
if (key == Keys.OemSemicolon && !shift) return ';';
if (key == Keys.OemSemicolon && shift) return ':';
if (key == Keys.OemTilde && !shift) return '`';
if (key == Keys.OemTilde && shift) return '~';
if (key == Keys.Subtract) return '-';
return null;
}
}
}
}

View file

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
@ -8,7 +9,8 @@ using MLEM.Misc;
namespace MLEM.Textures {
/// <summary>
/// This class represents a texture with nine areas.
/// A nine patch texture is useful if a big area should be covered by a small texture that has a specific outline, like a gui panel texture. The center of the texture will be stretched, while the outline of the texture will remain at its original size, keeping aspect ratios alive.
/// A nine patch texture is useful if a big area should be covered by a small texture that has a specific outline, like a gui panel texture. The center of the texture will be stretched or tiled, while the outline of the texture will remain at its original size, keeping aspect ratios alive.
/// The nine patch can then be drawn using <see cref="NinePatchExtensions"/>.
/// </summary>
public class NinePatch : GenericDataHolder {
@ -21,6 +23,10 @@ namespace MLEM.Textures {
/// </summary>
public readonly Padding Padding;
/// <summary>
/// The <see cref="NinePatchMode"/> that this nine patch should use for drawing
/// </summary>
public readonly NinePatchMode Mode;
/// <summary>
/// The nine patches that result from the <see cref="Padding"/>
/// </summary>
public readonly Rectangle[] SourceRectangles;
@ -30,9 +36,11 @@ namespace MLEM.Textures {
/// </summary>
/// <param name="texture">The texture to use</param>
/// <param name="padding">The padding that marks where the outline area stops</param>
public NinePatch(TextureRegion texture, Padding padding) {
/// <param name="mode">The mode to use for drawing this nine patch, defaults to <see cref="NinePatchMode.Stretch"/></param>
public NinePatch(TextureRegion texture, Padding padding, NinePatchMode mode = NinePatchMode.Stretch) {
this.Region = texture;
this.Padding = padding;
this.Mode = mode;
this.SourceRectangles = this.CreateRectangles(this.Region.Area).ToArray();
}
@ -44,13 +52,14 @@ namespace MLEM.Textures {
/// <param name="paddingRight">The padding on the right edge</param>
/// <param name="paddingTop">The padding on the top edge</param>
/// <param name="paddingBottom">The padding on the bottom edge</param>
public NinePatch(TextureRegion texture, int paddingLeft, int paddingRight, int paddingTop, int paddingBottom) :
this(texture, new Padding(paddingLeft, paddingRight, paddingTop, paddingBottom)) {
/// <param name="mode">The mode to use for drawing this nine patch, defaults to <see cref="NinePatchMode.Stretch"/></param>
public NinePatch(TextureRegion texture, int paddingLeft, int paddingRight, int paddingTop, int paddingBottom, NinePatchMode mode = NinePatchMode.Stretch) :
this(texture, new Padding(paddingLeft, paddingRight, paddingTop, paddingBottom), mode) {
}
/// <inheritdoc cref="NinePatch(TextureRegion, int, int, int, int)"/>
public NinePatch(Texture2D texture, int paddingLeft, int paddingRight, int paddingTop, int paddingBottom) :
this(new TextureRegion(texture), paddingLeft, paddingRight, paddingTop, paddingBottom) {
/// <inheritdoc cref="NinePatch(TextureRegion, int, int, int, int, NinePatchMode)"/>
public NinePatch(Texture2D texture, int paddingLeft, int paddingRight, int paddingTop, int paddingBottom, NinePatchMode mode = NinePatchMode.Stretch) :
this(new TextureRegion(texture), paddingLeft, paddingRight, paddingTop, paddingBottom, mode) {
}
/// <summary>
@ -58,15 +67,14 @@ namespace MLEM.Textures {
/// </summary>
/// <param name="texture">The texture to use</param>
/// <param name="padding">The padding that each edge should have</param>
public NinePatch(Texture2D texture, int padding) : this(new TextureRegion(texture), padding) {
/// <param name="mode">The mode to use for drawing this nine patch, defaults to <see cref="NinePatchMode.Stretch"/></param>
public NinePatch(Texture2D texture, int padding, NinePatchMode mode = NinePatchMode.Stretch) :
this(new TextureRegion(texture), padding, mode) {
}
/// <inheritdoc cref="NinePatch(TextureRegion, int)"/>
public NinePatch(TextureRegion texture, int padding) : this(texture, padding, padding, padding, padding) {
}
private IEnumerable<Rectangle> CreateRectangles(Rectangle area, float patchScale = 1) {
return this.CreateRectangles((RectangleF) area, patchScale).Select(r => (Rectangle) r);
/// <inheritdoc cref="NinePatch(TextureRegion, int, NinePatchMode)"/>
public NinePatch(TextureRegion texture, int padding, NinePatchMode mode = NinePatchMode.Stretch) :
this(texture, padding, padding, padding, padding, mode) {
}
internal IEnumerable<RectangleF> CreateRectangles(RectangleF area, float patchScale = 1) {
@ -93,6 +101,28 @@ namespace MLEM.Textures {
yield return new RectangleF(rightX, bottomY, pr, pb);
}
private IEnumerable<Rectangle> CreateRectangles(Rectangle area, float patchScale = 1) {
return this.CreateRectangles((RectangleF) area, patchScale).Select(r => (Rectangle) r);
}
}
/// <summary>
/// An enumeration that represents the modes that a <see cref="NinePatch"/> uses to be drawn
/// </summary>
public enum NinePatchMode {
/// <summary>
/// The nine resulting patches will each be stretched.
/// This mode is fitting for textures that don't have an intricate design on their edges.
/// </summary>
Stretch,
/// <summary>
/// The nine resulting paches will be tiled, repeating the texture multiple times.
/// This mode is fitting for textures that have a more complex design on their edges.
/// </summary>
Tile
}
/// <summary>
@ -113,11 +143,27 @@ namespace MLEM.Textures {
/// <param name="layerDepth">The depth</param>
/// <param name="patchScale">The scale of each area of the nine patch</param>
public static void Draw(this SpriteBatch batch, NinePatch texture, RectangleF destinationRectangle, Color color, float rotation, Vector2 origin, SpriteEffects effects, float layerDepth, float patchScale = 1) {
var dest = texture.CreateRectangles(destinationRectangle, patchScale);
var destinations = texture.CreateRectangles(destinationRectangle, patchScale);
var count = 0;
foreach (var rect in dest) {
if (!rect.IsEmpty)
batch.Draw(texture.Region.Texture, rect, texture.SourceRectangles[count], color, rotation, origin, effects, layerDepth);
foreach (var rect in destinations) {
if (!rect.IsEmpty) {
var src = texture.SourceRectangles[count];
switch (texture.Mode) {
case NinePatchMode.Stretch:
batch.Draw(texture.Region.Texture, rect, src, color, rotation, origin, effects, layerDepth);
break;
case NinePatchMode.Tile:
var width = src.Width * patchScale;
var height = src.Height * patchScale;
for (var x = 0F; x < rect.Width; x += width) {
for (var y = 0F; y < rect.Height; y += height) {
var size = new Vector2(Math.Min(rect.Width - x, width), Math.Min(rect.Height - y, height));
batch.Draw(texture.Region.Texture, new RectangleF(rect.Location + new Vector2(x, y), size), new Rectangle(src.Location, (size / patchScale).ToPoint()), color, rotation, origin, effects, layerDepth);
}
}
break;
}
}
count++;
}
}

5
NuGet.config Normal file
View file

@ -0,0 +1,5 @@
<configuration>
<config>
<add key="globalPackagesFolder" value="./packages" />
</config>
</configuration>

View file

@ -8,6 +8,14 @@
- See the source code in this repository
- See tutorials and API documentation on [the website](https://mlem.ellpeck.de/)
- Check out [the demos](https://github.com/Ellpeck/MLEM/tree/main/Demos) on [Desktop](https://github.com/Ellpeck/MLEM/tree/main/Demos.DesktopGL) or [Android](https://github.com/Ellpeck/MLEM/tree/main/Demos.Android)
- See [the changelog](https://github.com/Ellpeck/MLEM/blob/main/CHANGELOG.md) for information on updates
# Made with MLEM
- [A Breath of Spring Air](https://ellpeck.itch.io/a-breath-of-spring-air), a short platformer ([Source](https://git.ellpeck.de/Ellpeck/GreatSpringGameJam))
- [Don't Wake Up](https://ellpeck.itch.io/dont-wake-up), a short puzzle game ([Source](https://github.com/Ellpeck/DontLetGo))
- [Tiny Life](https://tinylifegame.com), an isometric life simulation game ([Modding API](https://github.com/Ellpeck/TinyLifeExampleMod))
If you created a game with the help of MLEM, you can get it added to this list by submitting it on the [issue tracker](https://github.com/Ellpeck/MLEM/issues). If its source is public, other people will be able to use your project as an example, too!
# Gallery
Here are some images that show a couple of MLEM's features.
@ -16,4 +24,12 @@ MLEM.Ui in action:
<img src="Media/Ui.gif">
MLEM's text formatting system:
<img src="Media/Formatting.png">
<img src="Media/Formatting.png">
# Friends of MLEM
There are several other NuGet packages and tools that work well in combination with MonoGame and MLEM. Here are some of them:
- [Contentless](https://github.com/Ellpeck/Contentless), a tool that removes the need to add assets to the MonoGame Content Pipeline manually
- [GameBundle](https://github.com/Ellpeck/GameBundle), a tool that packages MonoGame and other .NET Core applications into several distributable formats
- [ButlerDotNet](https://github.com/Ellpeck/ButlerDotNet), a tool that automatically downloads and invokes itch.io's butler
- [MonoGame.Extended](https://github.com/craftworkgames/MonoGame.Extended), a package that also provides several additional features for MonoGame
- [Coroutine](https://github.com/Ellpeck/Coroutine), a package that implements Unity-style coroutines for any project

View file

@ -3,17 +3,20 @@
/outputDir:bin
/intermediateDir:obj
/platform:Windows
/platform:DesktopGL
/config:
/profile:Reach
/compress:False
#-------------------------------- References --------------------------------#
/reference:..\bin\Debug\net462\MonoGame.Extended.Content.Pipeline.dll
/reference:..\..\packages\monogame.extended.content.pipeline\3.8.0\tools\MonoGame.Extended.Content.Pipeline.dll
#---------------------------------- Content ---------------------------------#
#begin Fonts/Cadman_Roman.otf
/copy:Fonts/Cadman_Roman.otf
#begin Fonts/Regular.fnt
/importer:BitmapFontImporter
/processor:BitmapFontProcessor

Binary file not shown.

View file

@ -1,6 +1,7 @@
using System;
using System.IO;
using System.Text.RegularExpressions;
using FontStashSharp;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
@ -19,8 +20,8 @@ using MLEM.Textures;
using MLEM.Ui;
using MLEM.Ui.Elements;
using MLEM.Ui.Style;
using MonoGame.Extended.BitmapFonts;
using MonoGame.Extended.Tiled;
using Group = MLEM.Ui.Elements.Group;
namespace Sandbox {
public class GameImpl : MlemGame {
@ -40,6 +41,11 @@ namespace Sandbox {
}
protected override void LoadContent() {
// TODO remove with MonoGame 3.8.1 https://github.com/MonoGame/MonoGame/issues/7298
this.GraphicsDeviceManager.PreferredBackBufferWidth = 1280;
this.GraphicsDeviceManager.PreferredBackBufferHeight = 720;
this.GraphicsDeviceManager.ApplyChanges();
base.LoadContent();
this.Components.Add(this.rawContent = new RawContentManager(this.Services));
@ -63,8 +69,11 @@ namespace Sandbox {
textureData[textureData.FromIndex(textureData.ToIndex(25, 9))] = Color.Yellow;
}
var system = new FontSystem(this.GraphicsDevice, 1024, 1024);
system.AddFont(File.ReadAllBytes("Content/Fonts/Cadman_Roman.otf"));
//var font = new GenericSpriteFont(LoadContent<SpriteFont>("Fonts/TestFont"));
var font = new GenericBitmapFont(LoadContent<BitmapFont>("Fonts/Regular"));
//var font = new GenericBitmapFont(LoadContent<BitmapFont>("Fonts/Regular"));
var font = new GenericStashFont(system.GetFont(32));
this.UiSystem.Style = new UntexturedStyle(this.SpriteBatch) {
Font = font,
TextScale = 0.1F,
@ -119,12 +128,9 @@ namespace Sandbox {
var res = this.Content.LoadJson<Test>("Test");
Console.WriteLine("The res is " + res);
/*this.OnDraw += (game, time) => {
var gradient = this.SpriteBatch.GenerateGradientTexture(Color.Green, Color.Red, Color.Blue, Color.Yellow);
this.OnDraw += (game, time) => {
this.SpriteBatch.Begin();
font.DrawString(this.SpriteBatch, "Left Aligned\nover multiple lines", new Vector2(640, 0), TextAlign.Left, Color.White);
font.DrawString(this.SpriteBatch, "Center Aligned\nover multiple lines", new Vector2(640, 100), TextAlign.Center, Color.White);
font.DrawString(this.SpriteBatch, "Right Aligned\nover multiple lines", new Vector2(640, 200), TextAlign.Right, Color.White);
font.DrawString(this.SpriteBatch, "Center Aligned on both axes", new Vector2(640, 360), TextAlign.CenterBothAxes, Color.White);
this.SpriteBatch.Draw(this.SpriteBatch.GetBlankTexture(), new Rectangle(640 - 4, 360 - 4, 8, 8), Color.Green);
this.SpriteBatch.Draw(this.SpriteBatch.GetBlankTexture(), new Rectangle(200, 400, 200, 400), Color.Green);
@ -132,8 +138,10 @@ namespace Sandbox {
font.DrawString(this.SpriteBatch, font.TruncateString("This is a very long string", 200, 1, ellipsis: "..."), new Vector2(200, 450), Color.White);
font.DrawString(this.SpriteBatch, font.TruncateString("This is a very long string", 200, 1, true), new Vector2(200, 500), Color.White);
font.DrawString(this.SpriteBatch, font.TruncateString("This is a very long string", 200, 1, true, "..."), new Vector2(200, 550), Color.White);
this.SpriteBatch.Draw(gradient, new Rectangle(300, 100, 200, 200), Color.White);
this.SpriteBatch.End();
};*/
};
var sc = 4;
var formatter = new TextFormatter();
@ -195,6 +203,22 @@ namespace Sandbox {
invalidPanel.AddChild(new Paragraph(Anchor.AutoRight, 1, "This is some test text!", true));
invalidPanel.AddChild(new VerticalSpace(1));
this.UiSystem.Add("Invalid", invalidPanel);*/
var loadGroup = new Group(Anchor.TopLeft, Vector2.One, false);
var loadPanel = loadGroup.AddChild(new Panel(Anchor.Center, new Vector2(150, 150), Vector2.Zero, false, true, new Point(5, 10), false) {
ChildPadding = new Padding(5, 10, 5, 5)
});
for (var i = 0; i < 1; i++) {
var button = loadPanel.AddChild(new Button(Anchor.AutoLeft, new Vector2(1)) {
SetHeightBasedOnChildren = true,
Padding = new Padding(0, 0, 0, 1),
ChildPadding = new Vector2(3)
});
button.AddChild(new Group(Anchor.AutoLeft, new Vector2(0.5F, 30), false) {
CanBeMoused = false
});
}
this.UiSystem.Add("Load", loadGroup);
}
protected override void DoUpdate(GameTime gameTime) {
@ -206,6 +230,11 @@ namespace Sandbox {
if (delta != 0) {
this.camera.Zoom(0.1F * Math.Sign(delta), this.InputHandler.MousePosition.ToVector2());
}
if (Input.InputsDown.Length > 0)
Console.WriteLine("Down: " + string.Join(", ", Input.InputsDown));
if (Input.InputsPressed.Length > 0)
Console.WriteLine("Pressed: " + string.Join(", ", Input.InputsPressed));
}
protected override void DoDraw(GameTime gameTime) {

View file

@ -5,9 +5,9 @@ namespace Sandbox {
internal static class Program {
private static void Main() {
TextInputWrapper.Current = new TextInputWrapper.DesktopGl<TextInputEventArgs>((w, c) => w.TextInput += c);
using (var game = new GameImpl())
game.Run();
MlemPlatform.Current = new MlemPlatform.DesktopGl<TextInputEventArgs>((w, c) => w.TextInput += c);
using var game = new GameImpl();
game.Run();
}
}

View file

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net462</TargetFramework>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
@ -14,10 +14,11 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="MonoGame.Content.Builder" Version="3.7.0.9" />
<PackageReference Include="MonoGame.Extended.Content.Pipeline" Version="3.7.0" />
<PackageReference Include="MonoGame.Extended.Tiled" Version="3.7.0" />
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.7.0.1708" />
<PackageReference Include="MonoGame.Content.Builder.Task" Version="3.8.0.1641" />
<PackageReference Include="MonoGame.Extended.Content.Pipeline" Version="3.8.0" />
<PackageReference Include="MonoGame.Extended.Tiled" Version="3.8.0" />
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.0.1641" />
<PackageReference Include="FontStashSharp.MonoGame" Version="0.9.2" />
</ItemGroup>
<ItemGroup>

30
Tests/CameraTests.cs Normal file
View file

@ -0,0 +1,30 @@
using Microsoft.Xna.Framework;
using MLEM.Cameras;
using NUnit.Framework;
namespace Tests {
public class CameraTests {
private TestGame game;
[SetUp]
public void SetUp() {
this.game = TestGame.Create();
}
[TearDown]
public void TearDown() {
this.game?.Dispose();
}
[Test]
public void TestConversions([Range(-4, 4, 4F)] float x, [Range(-4, 4, 4F)] float y) {
var camera = new Camera(this.game.GraphicsDevice);
var pos = new Vector2(x, y);
var cam = camera.ToCameraPos(pos);
var ret = camera.ToWorldPos(cam);
Assert.AreEqual(pos, ret);
}
}
}

31
Tests/CollectionTests.cs Normal file
View file

@ -0,0 +1,31 @@
using MLEM.Extensions;
using NUnit.Framework;
namespace Tests {
public class CollectionTests {
[Test]
public void TestCombinations() {
var things = new[] {
new[] {'1', '2', '3'},
new[] {'A', 'B'},
new[] {'+', '-'}
};
var expected = new[] {
new[] {'1', 'A', '+'}, new[] {'1', 'A', '-'}, new[] {'1', 'B', '+'}, new[] {'1', 'B', '-'},
new[] {'2', 'A', '+'}, new[] {'2', 'A', '-'}, new[] {'2', 'B', '+'}, new[] {'2', 'B', '-'},
new[] {'3', 'A', '+'}, new[] {'3', 'A', '-'}, new[] {'3', 'B', '+'}, new[] {'3', 'B', '-'}
};
Assert.AreEqual(things.Combinations(), expected);
var indices = new[] {
new[] {0, 0, 0}, new[] {0, 0, 1}, new[] {0, 1, 0}, new[] {0, 1, 1},
new[] {1, 0, 0}, new[] {1, 0, 1}, new[] {1, 1, 0}, new[] {1, 1, 1},
new[] {2, 0, 0}, new[] {2, 0, 1}, new[] {2, 1, 0}, new[] {2, 1, 1}
};
Assert.AreEqual(things.IndexCombinations(), indices);
}
}
}

BIN
Tests/Content/TestFont.xnb Normal file

Binary file not shown.

View file

@ -0,0 +1,14 @@
SimpleDeskUp
loc 0 0 48 32
piv 16 16
SimpleDeskRight
loc 48 0 48 32
piv 80 16
Plant
loc 96 0 16 32
LongTableUp
loc 0 32 64 48
piv 16 48
LongTableRight
loc 64 32 64 48
piv 112 48

90
Tests/DataTests.cs Normal file
View file

@ -0,0 +1,90 @@
using System;
using System.Diagnostics;
using System.IO;
using Microsoft.Xna.Framework;
using MLEM.Data;
using MLEM.Data.Json;
using MLEM.Misc;
using Newtonsoft.Json;
using NUnit.Framework;
namespace Tests {
public class DataTests {
private readonly TestObject testObject = new(Vector2.One, "test") {
Vec = new Vector2(10, 20),
Point = new Point(20, 30),
Dir = Direction2.Left,
OtherTest = new TestObject(Vector2.One, "other") {
Vec = new Vector2(70, 30),
Dir = Direction2.Right
}
};
[Test]
public void TestJsonSerializers() {
var serializer = JsonConverters.AddAll(new JsonSerializer());
var writer = new StringWriter();
serializer.Serialize(writer, this.testObject);
var ret = writer.ToString();
Assert.AreEqual(ret, "{\"Vec\":\"10 20\",\"Point\":\"20 30\",\"OtherTest\":{\"Vec\":\"70 30\",\"Point\":\"0 0\",\"OtherTest\":null,\"Dir\":\"Right\"},\"Dir\":\"Left\"}");
var read = serializer.Deserialize<TestObject>(new JsonTextReader(new StringReader(ret)));
Assert.AreEqual(this.testObject, read);
}
[Test]
public void TestCopy() {
var copy = this.testObject.Copy();
Assert.AreEqual(this.testObject, copy);
Assert.AreSame(this.testObject.OtherTest, copy.OtherTest);
var deepCopy = this.testObject.DeepCopy();
Assert.AreEqual(this.testObject, deepCopy);
Assert.AreNotSame(this.testObject.OtherTest, deepCopy.OtherTest);
}
[Test]
public void TestCopySpeed() {
const int count = 1000000;
var stopwatch = Stopwatch.StartNew();
for (var i = 0; i < count; i++)
this.testObject.Copy();
stopwatch.Stop();
TestContext.WriteLine($"Copy took {stopwatch.Elapsed.TotalMilliseconds / count * 1000000}ns on average");
stopwatch.Restart();
for (var i = 0; i < count; i++)
this.testObject.DeepCopy();
stopwatch.Stop();
TestContext.WriteLine($"DeepCopy took {stopwatch.Elapsed.TotalMilliseconds / count * 1000000}ns on average");
}
private class TestObject {
public Vector2 Vec;
public Point Point;
public Direction2 Dir { get; set; }
public TestObject OtherTest;
public TestObject(Vector2 test, string test2) {
}
protected bool Equals(TestObject other) {
return this.Vec.Equals(other.Vec) && this.Point.Equals(other.Point) && Equals(this.OtherTest, other.OtherTest) && this.Dir == other.Dir;
}
public override bool Equals(object obj) {
return ReferenceEquals(this, obj) || obj is TestObject other && this.Equals(other);
}
public override int GetHashCode() {
return HashCode.Combine(this.Vec, this.Point, this.OtherTest, (int) this.Dir);
}
}
}
}

View file

@ -0,0 +1,24 @@
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using MLEM.Data;
using MLEM.Textures;
using NUnit.Framework;
namespace Tests {
public class TestDataTextureAtlas {
[Test]
public void Test() {
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(), 5);
var table = atlas["LongTableUp"];
Assert.AreEqual(table.Area, new Rectangle(0, 32, 64, 48));
Assert.AreEqual(table.PivotPixels, new Vector2(16, 48 - 32));
}
}
}

Some files were not shown because too many files have changed in this diff Show more