mirror of
https://github.com/Ellpeck/MLEM.git
synced 2024-11-26 06:28:35 +01:00
Compare commits
135 commits
9b090c954f
...
30160e8210
Author | SHA1 | Date | |
---|---|---|---|
30160e8210 | |||
eb8a8568e1 | |||
a249331ce9 | |||
500736e20f | |||
2e8a8244a3 | |||
04829f1695 | |||
4bdecf792e | |||
ef0499958d | |||
79a2eaa8d2 | |||
10bea0f820 | |||
f8ebbdacdf | |||
f5be677b83 | |||
d6ab8061f3 | |||
179afbc428 | |||
f5ff96d348 | |||
ee62554fee | |||
f8567cfc99 | |||
73abfb2dc3 | |||
df2b9cc10e | |||
45c668c992 | |||
d5d3297271 | |||
b41341d01e | |||
b3da8d35c0 | |||
14d0b24aa9 | |||
e229432d0f | |||
4189ae6d4d | |||
170b397e02 | |||
f0b65daf68 | |||
066ed9f8f7 | |||
b374d50815 | |||
cf3d0e8e0c | |||
943616e21a | |||
e3b41ebeea | |||
0a62ae0036 | |||
8ffd9ab48a | |||
edcaa84a2b | |||
8555fc2499 | |||
88efc6b41c | |||
b2b4dfbdc9 | |||
81c69041c3 | |||
1a1b2025cd | |||
d72b094a7a | |||
e5cfebef3b | |||
e21729de67 | |||
9919ee4a97 | |||
92353e40e6 | |||
e812dd7802 | |||
3b08b66fa4 | |||
63ea3eba90 | |||
810406fb94 | |||
b5619db55c | |||
a1df35ea05 | |||
db02dfcfde | |||
6ac2ba6151 | |||
5906278091 | |||
8bb62a2ce5 | |||
797a3b2617 | |||
8469185297 | |||
cc749103e0 | |||
2df9d6a3a8 | |||
d138577285 | |||
f58e3c94d5 | |||
791c66b098 | |||
8745a3237e | |||
7ab76d239d | |||
d5e5d1d536 | |||
be7676d37e | |||
17ce7b668d | |||
627350ca31 | |||
d3fade27e5 | |||
8d689952cc | |||
560c797b87 | |||
d6309ce9c1 | |||
d3b153fd45 | |||
0b6e6743cf | |||
3e4c4e566d | |||
bfa4ab4ac2 | |||
92f9164256 | |||
d6a51776e5 | |||
74b3c426b8 | |||
e8710f69e9 | |||
02cf01fcb7 | |||
fe89b28031 | |||
9f60a59706 | |||
d0500bf981 | |||
e6b4bc54ba | |||
b6a626d96e | |||
39a7dd3e97 | |||
53cda02ec4 | |||
48735c3d36 | |||
fc026ad0de | |||
c3c8b132da | |||
7d8b14ee8d | |||
ff92a00e1a | |||
740c65a887 | |||
4918a72760 | |||
914b0d9c2d | |||
38214a66a3 | |||
a6fd2c052e | |||
d0c805cf18 | |||
55735b4c64 | |||
df2d102d8e | |||
b4e1b00c88 | |||
eadabf3919 | |||
856d67b6cf | |||
742bc52437 | |||
d6e7c1086d | |||
e60d3591ff | |||
a274517861 | |||
d94e882c02 | |||
2d3d93c610 | |||
963ea557e8 | |||
5e68bf5d3d | |||
023233a062 | |||
0fe4c940d7 | |||
32dad847a0 | |||
1f2e2a4f38 | |||
0a696941dc | |||
6a271af017 | |||
af0aee6c40 | |||
e50d28ce11 | |||
f0432ab981 | |||
8332f56237 | |||
b7b1490d70 | |||
4d34a2fac1 | |||
b2898a8eae | |||
b012c65990 | |||
72647a2edf | |||
02b4626996 | |||
5aaba0c583 | |||
8044cb59cb | |||
7a0464e8d6 | |||
87d04e1abd | |||
f0cc4b0c80 | |||
b78465c054 |
133 changed files with 24510 additions and 2228 deletions
|
@ -3,7 +3,7 @@
|
||||||
"isRoot": true,
|
"isRoot": true,
|
||||||
"tools": {
|
"tools": {
|
||||||
"cake.tool": {
|
"cake.tool": {
|
||||||
"version": "1.3.0",
|
"version": "3.0.0",
|
||||||
"commands": [
|
"commands": [
|
||||||
"dotnet-cake"
|
"dotnet-cake"
|
||||||
]
|
]
|
||||||
|
|
|
@ -87,8 +87,9 @@ resharper_indent_nested_for_stmt = true
|
||||||
resharper_indent_nested_lock_stmt = true
|
resharper_indent_nested_lock_stmt = true
|
||||||
resharper_indent_nested_usings_stmt = true
|
resharper_indent_nested_usings_stmt = true
|
||||||
resharper_indent_nested_while_stmt = true
|
resharper_indent_nested_while_stmt = true
|
||||||
resharper_indent_preprocessor_if = usual_indent
|
resharper_indent_preprocessor_if = no_indent
|
||||||
resharper_indent_preprocessor_other = usual_indent
|
resharper_indent_preprocessor_other = no_indent
|
||||||
|
resharper_indent_preprocessor_region = no_indent
|
||||||
resharper_keep_existing_declaration_parens_arrangement = false
|
resharper_keep_existing_declaration_parens_arrangement = false
|
||||||
resharper_keep_existing_embedded_arrangement = false
|
resharper_keep_existing_embedded_arrangement = false
|
||||||
resharper_keep_existing_expr_member_arrangement = false
|
resharper_keep_existing_expr_member_arrangement = false
|
||||||
|
@ -111,3 +112,7 @@ resharper_wrap_object_and_collection_initializer_style = wrap_if_long
|
||||||
resharper_xmldoc_attribute_indent = align_by_first_attribute
|
resharper_xmldoc_attribute_indent = align_by_first_attribute
|
||||||
resharper_xmldoc_attribute_style = on_single_line
|
resharper_xmldoc_attribute_style = on_single_line
|
||||||
resharper_xmldoc_pi_attribute_style = on_single_line
|
resharper_xmldoc_pi_attribute_style = on_single_line
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
indent_size = 2
|
||||||
|
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -4,4 +4,4 @@ obj
|
||||||
packages
|
packages
|
||||||
*.user
|
*.user
|
||||||
tools
|
tools
|
||||||
TestResults
|
TestResults*
|
||||||
|
|
127
CHANGELOG.md
127
CHANGELOG.md
|
@ -1,14 +1,135 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
MLEM tries to adhere to [semantic versioning](https://semver.org/). Breaking changes are written in **bold**.
|
MLEM tries to adhere to [semantic versioning](https://semver.org/). Potentially breaking changes are written in **bold**.
|
||||||
|
|
||||||
Jump to version:
|
Jump to version:
|
||||||
|
- [6.1.0](#610)
|
||||||
- [6.0.0](#600)
|
- [6.0.0](#600)
|
||||||
- [5.3.0](#530)
|
- [5.3.0](#530)
|
||||||
- [5.2.0](#520)
|
- [5.2.0](#520)
|
||||||
- [5.1.0](#510)
|
- [5.1.0](#510)
|
||||||
- [5.0.0](#500)
|
- [5.0.0](#500)
|
||||||
|
|
||||||
|
## 6.1.0
|
||||||
|
|
||||||
|
### MLEM
|
||||||
|
Additions
|
||||||
|
- Added TokenizedString.Realign
|
||||||
|
- Added GetFlags and GetUniqueFlags to EnumHelper
|
||||||
|
- Added GetDownTime, GetUpTime, GetTimeSincePress, WasModifierDown and WasDown to Keybind and Combination
|
||||||
|
- Added the ability for UniformTextureAtlases to have padding for each region
|
||||||
|
- Added UniformTextureAtlas methods ToList and ToDictionary
|
||||||
|
- Added SingleRandom and SeedSource
|
||||||
|
- Added TokenizedString.GetArea
|
||||||
|
- Added InputHandler.WasPressedForLess and related methods as well as InputHandler.IsPressedIgnoreRepeats
|
||||||
|
- Added RandomExtensions.NextSingle with minimum and maximum values
|
||||||
|
- Added subscript and superscript formatting codes
|
||||||
|
- **Added the ability to find paths to one of multiple goals using AStar**
|
||||||
|
|
||||||
|
Improvements
|
||||||
|
- Improved EnumHelper.GetValues signature to return an array
|
||||||
|
- Allow using external gesture handling alongside InputHandler through ExternalGestureHandling
|
||||||
|
- Discard old data when updating a StaticSpriteBatch
|
||||||
|
- Multi-target net452, making MLEM compatible with MonoGame for consoles
|
||||||
|
- Allow retrieving the cost of a calculated path when using AStar
|
||||||
|
- Added trimming and AOT annotations and made MLEM trimmable
|
||||||
|
- Allow specifying percentage-based padding for a NinePatch
|
||||||
|
- Improved the way InputHandler down time calculation works
|
||||||
|
- Allow explicitly specifying each region for extended auto tiles
|
||||||
|
- Added a generic version of IGenericDataHolder.SetData
|
||||||
|
- Allow formatting codes to have an arbitrary custom width
|
||||||
|
- Allow initializing text formatters without default codes and macros
|
||||||
|
- **Drastically improved StaticSpriteBatch batching performance**
|
||||||
|
- **Made GenericFont and TokenizedString support UTF-32 characters like emoji**
|
||||||
|
|
||||||
|
Fixes
|
||||||
|
- Fixed TokenizedString handling trailing spaces incorrectly in the last line of non-left aligned text
|
||||||
|
- Fixed some TokenizedString tokens starting with a line break not being split correctly
|
||||||
|
- Fixed InputHandler maintaining old input states when input types are toggled off
|
||||||
|
- Fixed Combination.IsModifierDown querying one of its modifiers instead of all of them
|
||||||
|
|
||||||
|
Removals
|
||||||
|
- Removed DataContract attribute from GenericDataHolder
|
||||||
|
- Marked EnumHelper as obsolete due to its reimplementation in [DynamicEnums](https://www.nuget.org/packages/DynamicEnums)
|
||||||
|
- Marked Code.GetReplacementString as obsolete
|
||||||
|
- Marked TokenizedString.Measure as obsolete in favor of GetArea
|
||||||
|
- Marked non-GenericInput versions of IsDown, IsUp, IsPressed and related methods as obsolete in favor of GenericInput ones
|
||||||
|
|
||||||
|
### MLEM.Ui
|
||||||
|
Additions
|
||||||
|
- Added some extension methods for querying Anchor types
|
||||||
|
- Added Element.AutoSizeAddedAbsolute to allow for more granular control of auto-sizing
|
||||||
|
- Added Element.OnAddedToUi and Element.OnRemovedFromUi
|
||||||
|
- Added ScrollBar.MouseDragScrolling
|
||||||
|
- Added Panel.ScrollToElement
|
||||||
|
- Added ElementHelper.MakeGrid
|
||||||
|
- Added Button.AutoDisableCondition
|
||||||
|
|
||||||
|
Improvements
|
||||||
|
- Allow elements to auto-adjust their size even when their children are aligned oddly
|
||||||
|
- Close other dropdowns when opening a dropdown
|
||||||
|
- Generified UiMarkdownParser by adding abstract UiParser
|
||||||
|
- Multi-target net452, making MLEM compatible with MonoGame for consoles
|
||||||
|
- Added trimming and AOT annotations and made MLEM.Ui trimmable
|
||||||
|
- Ensure paragraphs display up-to-date versions of their text callbacks
|
||||||
|
- Set cornflower blue as the default link color
|
||||||
|
- Added TextField.OnCopyPasteException to allow handling exceptions thrown by TextCopy
|
||||||
|
- Avoid paragraphs splitting or truncating their text unnecessarily
|
||||||
|
- Automatically mark elements dirty when various member values are changed
|
||||||
|
- Allow initializing a ui system's text formatter without default codes and macros
|
||||||
|
|
||||||
|
Fixes
|
||||||
|
- Fixed parents of elements that prevent spill not being notified properly
|
||||||
|
- Fixed paragraphs sometimes not updating their position properly when hidden because they're empty
|
||||||
|
- Fixed panels sometimes not drawing children that came into view when their positions changed unexpectedly
|
||||||
|
- Fixed UiMarkdownParser not parsing formatting in headings and blockquotes
|
||||||
|
- Fixed Element.OnChildAdded and Element.OnChildRemoved being called for grandchildren when a child is added
|
||||||
|
- Fixed an exception when trying to force-update the area of an element without a ui system
|
||||||
|
- Fixed the scroll bar of an empty panel being positioned incorrectly
|
||||||
|
- Fixed UiControls maintaining old input states when input types are toggled off
|
||||||
|
- Fixed an occasional deadlock when a game is disposed with a scrolling Panel present
|
||||||
|
- Fixed UiStyle.LinkColor not being applied to the ui system when changed
|
||||||
|
|
||||||
|
Removals
|
||||||
|
- Marked Element.OnDisposed as obsolete in favor of the more predictable OnRemovedFromUi
|
||||||
|
|
||||||
|
### MLEM.Data
|
||||||
|
Additions
|
||||||
|
- Added data, from, and copy instructions to DataTextureAtlas
|
||||||
|
- Added the ability to add additional regions to a RuntimeTexturePacker after packing
|
||||||
|
- Added GetFlags, GetUniqueFlags and IsDefined to DynamicEnum
|
||||||
|
- Added DataTextureAtlas.ToDictionary
|
||||||
|
|
||||||
|
Improvements
|
||||||
|
- Allow data texture atlas pivots and offsets to be negative
|
||||||
|
- Made RuntimeTexturePacker restore texture region name and pivot when packing
|
||||||
|
- Multi-target net452, making MLEM compatible with MonoGame for consoles
|
||||||
|
- Added trimming and AOT annotations and made MLEM.Data trimmable
|
||||||
|
- Store a RuntimeTexturePacker packed texture region's source region
|
||||||
|
- Use JSON.NET attributes in favor of DataContract and DataMember
|
||||||
|
- Made JsonTypeSafeWrapper.Of generic to potentially avoid reflective instantiation
|
||||||
|
|
||||||
|
Fixes
|
||||||
|
- Fixed data texture atlases not allowing most characters in their region names
|
||||||
|
|
||||||
|
Removals
|
||||||
|
- Marked DynamicEnum as obsolete due to its reimplementation in [DynamicEnums](https://www.nuget.org/packages/DynamicEnums)
|
||||||
|
|
||||||
|
## MLEM.Extended
|
||||||
|
Additions
|
||||||
|
- Added Range extension methods GetPercentage and FromPercentage
|
||||||
|
|
||||||
|
Improvements
|
||||||
|
- Multi-target net452, making MLEM compatible with MonoGame for consoles
|
||||||
|
- Added trimming and AOT annotations and made MLEM.Extended trimmable
|
||||||
|
- **Made GenericBitmapFont and GenericStashFont support UTF-32 characters like emoji**
|
||||||
|
|
||||||
|
## MLEM.Startup
|
||||||
|
Improvements
|
||||||
|
- Multi-target net452, making MLEM compatible with MonoGame for consoles
|
||||||
|
- Added trimming and AOT annotations and made MLEM.Startup trimmable
|
||||||
|
|
||||||
## 6.0.0
|
## 6.0.0
|
||||||
|
|
||||||
### MLEM
|
### MLEM
|
||||||
Additions
|
Additions
|
||||||
- Added consuming variants of IsPressed methods to InputHandler and Keybind
|
- Added consuming variants of IsPressed methods to InputHandler and Keybind
|
||||||
|
@ -95,6 +216,7 @@ Improvements
|
||||||
- Updated to MonoGame 3.8.1
|
- Updated to MonoGame 3.8.1
|
||||||
|
|
||||||
## 5.3.0
|
## 5.3.0
|
||||||
|
|
||||||
### MLEM
|
### MLEM
|
||||||
Additions
|
Additions
|
||||||
- Added StringBuilder overloads to GenericFont
|
- Added StringBuilder overloads to GenericFont
|
||||||
|
@ -179,6 +301,7 @@ Removals
|
||||||
- Marked CopyExtensions as obsolete
|
- Marked CopyExtensions as obsolete
|
||||||
|
|
||||||
## 5.2.0
|
## 5.2.0
|
||||||
|
|
||||||
### MLEM
|
### MLEM
|
||||||
Additions
|
Additions
|
||||||
- Added a strikethrough formatting code
|
- Added a strikethrough formatting code
|
||||||
|
@ -253,6 +376,7 @@ Additions
|
||||||
- Added PreDraw and PreUpdate events and coroutine events
|
- Added PreDraw and PreUpdate events and coroutine events
|
||||||
|
|
||||||
## 5.1.0
|
## 5.1.0
|
||||||
|
|
||||||
### MLEM
|
### MLEM
|
||||||
Additions
|
Additions
|
||||||
- Added RotateBy to Direction2Helper
|
- Added RotateBy to Direction2Helper
|
||||||
|
@ -303,6 +427,7 @@ Fixes
|
||||||
- Fixed DynamicEnum AddFlag going into an infinite loop
|
- Fixed DynamicEnum AddFlag going into an infinite loop
|
||||||
|
|
||||||
## 5.0.0
|
## 5.0.0
|
||||||
|
|
||||||
### MLEM
|
### MLEM
|
||||||
Additions
|
Additions
|
||||||
- Added some Collection extensions, namely for dealing with combinations
|
- Added some Collection extensions, namely for dealing with combinations
|
||||||
|
|
|
@ -2,6 +2,8 @@ using Android.Content;
|
||||||
using Android.Content.PM;
|
using Android.Content.PM;
|
||||||
using Android.OS;
|
using Android.OS;
|
||||||
using Android.Views;
|
using Android.Views;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using Microsoft.Xna.Framework.Input;
|
||||||
using MLEM.Extensions;
|
using MLEM.Extensions;
|
||||||
using MLEM.Misc;
|
using MLEM.Misc;
|
||||||
using Uri = Android.Net.Uri;
|
using Uri = Android.Net.Uri;
|
||||||
|
|
|
@ -7,11 +7,12 @@
|
||||||
<ApplicationVersion>1</ApplicationVersion>
|
<ApplicationVersion>1</ApplicationVersion>
|
||||||
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
|
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
|
||||||
<ImplicitUsings>true</ImplicitUsings>
|
<ImplicitUsings>true</ImplicitUsings>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="MonoGame.Content.Builder.Task" Version="3.8.1.263" />
|
<PackageReference Include="MonoGame.Content.Builder.Task" Version="3.8.1.303" />
|
||||||
<PackageReference Include="MonoGame.Framework.Android" Version="3.8.1.263" />
|
<PackageReference Include="MonoGame.Framework.Android" Version="3.8.1.303" />
|
||||||
|
|
||||||
<ProjectReference Include="..\Demos\Demos.csproj" />
|
<ProjectReference Include="..\Demos\Demos.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
|
@ -2,11 +2,12 @@
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>net5.0</TargetFramework>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
<ApplicationIcon>Icon.ico</ApplicationIcon>
|
<ApplicationIcon>Icon.ico</ApplicationIcon>
|
||||||
<AssemblyName>MLEM Desktop Demos</AssemblyName>
|
<AssemblyName>MLEM Desktop Demos</AssemblyName>
|
||||||
<RootNamespace>Demos.DesktopGL</RootNamespace>
|
<RootNamespace>Demos.DesktopGL</RootNamespace>
|
||||||
<DefineConstants>$(DefineConstants);FNA</DefineConstants>
|
<DefineConstants>$(DefineConstants);FNA</DefineConstants>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
<!-- We still use the MG content builder for ease of compatibility between the MG and FNA demo projects -->
|
<!-- We still use the MG content builder for ease of compatibility between the MG and FNA demo projects -->
|
||||||
<MonoGamePlatform>DesktopGL</MonoGamePlatform>
|
<MonoGamePlatform>DesktopGL</MonoGamePlatform>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
@ -19,7 +20,7 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="MonoGame.Content.Builder.Task" Version="3.8.0.1641" />
|
<PackageReference Include="MonoGame.Content.Builder.Task" Version="3.8.1.303" />
|
||||||
<ProjectReference Include="..\FNA\FNA.Core.csproj" />
|
<ProjectReference Include="..\FNA\FNA.Core.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
|
@ -5,14 +5,15 @@
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
<ApplicationIcon>Icon.ico</ApplicationIcon>
|
<ApplicationIcon>Icon.ico</ApplicationIcon>
|
||||||
<AssemblyName>MLEM Desktop Demos</AssemblyName>
|
<AssemblyName>MLEM Desktop Demos</AssemblyName>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="MonoGame.Content.Builder.Task" Version="3.8.1.263" />
|
<PackageReference Include="MonoGame.Content.Builder.Task" Version="3.8.1.303" />
|
||||||
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.1.263" />
|
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.1.303" />
|
||||||
|
|
||||||
<ProjectReference Include="..\Demos\Demos.csproj" />
|
<ProjectReference Include="..\Demos\Demos.csproj" />
|
||||||
<ProjectReference Include="..\MLEM.Startup\MLEM.Startup.csproj" />
|
<ProjectReference Include="..\MLEM.Startup\MLEM.Startup.csproj" />
|
||||||
|
|
|
@ -1,20 +1,22 @@
|
||||||
using System;
|
using MLEM.Misc;
|
||||||
|
#if !FNA
|
||||||
using Microsoft.Xna.Framework;
|
using Microsoft.Xna.Framework;
|
||||||
|
#else
|
||||||
using Microsoft.Xna.Framework.Input;
|
using Microsoft.Xna.Framework.Input;
|
||||||
using MLEM.Misc;
|
#endif
|
||||||
|
|
||||||
namespace Demos.DesktopGL {
|
namespace Demos.DesktopGL;
|
||||||
public static class Program {
|
|
||||||
|
|
||||||
public static void Main() {
|
public static class Program {
|
||||||
#if FNA
|
|
||||||
MlemPlatform.Current = new MlemPlatform.DesktopFna(a => TextInputEXT.TextInput += a);
|
|
||||||
#else
|
|
||||||
MlemPlatform.Current = new MlemPlatform.DesktopGl<TextInputEventArgs>((w, c) => w.TextInput += c);
|
|
||||||
#endif
|
|
||||||
using var game = new GameImpl();
|
|
||||||
game.Run();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
public static void Main() {
|
||||||
|
#if FNA
|
||||||
|
MlemPlatform.Current = new MlemPlatform.DesktopFna(a => TextInputEXT.TextInput += a);
|
||||||
|
#else
|
||||||
|
MlemPlatform.Current = new MlemPlatform.DesktopGl<TextInputEventArgs>((w, c) => w.TextInput += c);
|
||||||
|
#endif
|
||||||
|
using var game = new GameImpl();
|
||||||
|
game.Run();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
<TargetFramework>netstandard2.0</TargetFramework>
|
<TargetFramework>netstandard2.0</TargetFramework>
|
||||||
<RootNamespace>Demos</RootNamespace>
|
<RootNamespace>Demos</RootNamespace>
|
||||||
<DefineConstants>$(DefineConstants);FNA</DefineConstants>
|
<DefineConstants>$(DefineConstants);FNA</DefineConstants>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
<TargetFramework>netstandard2.0</TargetFramework>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@ -11,7 +12,7 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.1.263">
|
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.0.1641">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
|
@ -43,7 +43,7 @@ namespace Demos {
|
||||||
this.GraphicsDevice.Clear(Color.CornflowerBlue);
|
this.GraphicsDevice.Clear(Color.CornflowerBlue);
|
||||||
base.DoDraw(time);
|
base.DoDraw(time);
|
||||||
|
|
||||||
this.SpriteBatch.Begin(SpriteSortMode.Deferred, null, SamplerState.PointClamp, null, null);
|
this.SpriteBatch.Begin(SpriteSortMode.Deferred, null, SamplerState.PointClamp, DepthStencilState.None, RasterizerState.CullCounterClockwise);
|
||||||
var view = this.GraphicsDevice.Viewport;
|
var view = this.GraphicsDevice.Viewport;
|
||||||
|
|
||||||
// graph the easing function
|
// graph the easing function
|
||||||
|
|
|
@ -9,9 +9,6 @@ using MLEM.Textures;
|
||||||
using MLEM.Ui;
|
using MLEM.Ui;
|
||||||
using MLEM.Ui.Elements;
|
using MLEM.Ui.Elements;
|
||||||
using MLEM.Ui.Style;
|
using MLEM.Ui.Style;
|
||||||
#if !FNA
|
|
||||||
using MonoGame.Framework.Utilities;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
namespace Demos {
|
namespace Demos {
|
||||||
public class GameImpl : MlemGame {
|
public class GameImpl : MlemGame {
|
||||||
|
@ -94,9 +91,8 @@ namespace Demos {
|
||||||
TextScale = 0.1F,
|
TextScale = 0.1F,
|
||||||
PanelTexture = new NinePatch(new TextureRegion(tex, 0, 8, 24, 24), 8),
|
PanelTexture = new NinePatch(new TextureRegion(tex, 0, 8, 24, 24), 8),
|
||||||
ButtonTexture = new NinePatch(new TextureRegion(tex, 24, 8, 16, 16), 4),
|
ButtonTexture = new NinePatch(new TextureRegion(tex, 24, 8, 16, 16), 4),
|
||||||
ScrollBarBackground = new NinePatch(new TextureRegion(tex, 12, 0, 4, 8), 1, 1, 2, 2),
|
ScrollBarBackground = new NinePatch(new TextureRegion(tex, 12, 0, 4, 8), 0.25F, paddingPercent: true),
|
||||||
ScrollBarScrollerTexture = new NinePatch(new TextureRegion(tex, 8, 0, 4, 8), 1, 1, 2, 2),
|
ScrollBarScrollerTexture = new NinePatch(new TextureRegion(tex, 8, 0, 4, 8), 0.25F, paddingPercent: true)
|
||||||
LinkColor = Color.CornflowerBlue
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Xna.Framework;
|
using Microsoft.Xna.Framework;
|
||||||
using Microsoft.Xna.Framework.Graphics;
|
using Microsoft.Xna.Framework.Graphics;
|
||||||
using MLEM.Extensions;
|
using MLEM.Extensions;
|
||||||
|
@ -51,7 +52,7 @@ namespace Demos {
|
||||||
|
|
||||||
// Now find a path from the top left to the bottom right corner and store it in a variable
|
// Now find a path from the top left to the bottom right corner and store it in a variable
|
||||||
// If no path can be found after the maximum amount of tries (10000 by default), the pathfinder will abort and return no path (null)
|
// If no path can be found after the maximum amount of tries (10000 by default), the pathfinder will abort and return no path (null)
|
||||||
var foundPath = await this.pathfinder.FindPathAsync(Point.Zero, new Point(49, 49));
|
var foundPath = await Task.Run(() => this.pathfinder.FindPath(Point.Zero, new Point(49, 49)));
|
||||||
this.path = foundPath != null ? foundPath.ToList() : null;
|
this.path = foundPath != null ? foundPath.ToList() : null;
|
||||||
|
|
||||||
// print out some info
|
// print out some info
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.Xna.Framework;
|
using Microsoft.Xna.Framework;
|
||||||
using Microsoft.Xna.Framework.Graphics;
|
using Microsoft.Xna.Framework.Graphics;
|
||||||
|
using Microsoft.Xna.Framework.Input;
|
||||||
|
using MLEM.Extensions;
|
||||||
using MLEM.Font;
|
using MLEM.Font;
|
||||||
using MLEM.Formatting;
|
using MLEM.Formatting;
|
||||||
using MLEM.Formatting.Codes;
|
using MLEM.Formatting.Codes;
|
||||||
|
using MLEM.Misc;
|
||||||
using MLEM.Startup;
|
using MLEM.Startup;
|
||||||
using MLEM.Textures;
|
using MLEM.Textures;
|
||||||
|
|
||||||
|
@ -15,7 +18,7 @@ namespace Demos {
|
||||||
"You can write in <b>bold</i>, <i>italics</i>, <u>with an underline</u>, <st>strikethrough</st>, with a <s #000000 4>drop shadow</s> whose <s #ff0000 4>color</s> and <s #000000 10>offset</s> you can modify in each application of the code, or with various types of <b>combined <c Pink>formatting</c> codes</b>.\n\n" +
|
"You can write in <b>bold</i>, <i>italics</i>, <u>with an underline</u>, <st>strikethrough</st>, with a <s #000000 4>drop shadow</s> whose <s #ff0000 4>color</s> and <s #000000 10>offset</s> you can modify in each application of the code, or with various types of <b>combined <c Pink>formatting</c> codes</b>.\n\n" +
|
||||||
"You can apply <c CornflowerBlue>custom</c> <c Yellow>colors</c> to text, including all default <c Orange>MonoGame colors</c> and <c #aabb00>inline custom colors</c>.\n\n" +
|
"You can apply <c CornflowerBlue>custom</c> <c Yellow>colors</c> to text, including all default <c Orange>MonoGame colors</c> and <c #aabb00>inline custom colors</c>.\n\n" +
|
||||||
"You can also use animations like <a wobbly>a wobbly one</a>, as well as create custom ones using the <a wobbly>Code class</a>.\n\n" +
|
"You can also use animations like <a wobbly>a wobbly one</a>, as well as create custom ones using the <a wobbly>Code class</a>.\n\n" +
|
||||||
"You can also display <i grass> icons in your text!\n\n" +
|
"You can also display <i grass> icons in your text, and use super<sup>script</sup> or sub<sub>script</sub> formatting!\n\n" +
|
||||||
"Additionally, the text formatter has various methods for interacting with the text, like custom behaviors when hovering over certain parts, and more.";
|
"Additionally, the text formatter has various methods for interacting with the text, like custom behaviors when hovering over certain parts, and more.";
|
||||||
private const float Scale = 0.5F;
|
private const float Scale = 0.5F;
|
||||||
private const float Width = 0.9F;
|
private const float Width = 0.9F;
|
||||||
|
@ -23,6 +26,7 @@ namespace Demos {
|
||||||
private TextFormatter formatter;
|
private TextFormatter formatter;
|
||||||
private TokenizedString tokenizedText;
|
private TokenizedString tokenizedText;
|
||||||
private GenericFont font;
|
private GenericFont font;
|
||||||
|
private bool drawBounds;
|
||||||
|
|
||||||
public TextFormattingDemo(MlemGame game) : base(game) {}
|
public TextFormattingDemo(MlemGame game) : base(game) {}
|
||||||
|
|
||||||
|
@ -50,12 +54,24 @@ namespace Demos {
|
||||||
|
|
||||||
public override void DoDraw(GameTime time) {
|
public override void DoDraw(GameTime time) {
|
||||||
this.GraphicsDevice.Clear(Color.DarkSlateGray);
|
this.GraphicsDevice.Clear(Color.DarkSlateGray);
|
||||||
this.SpriteBatch.Begin(SpriteSortMode.Deferred, null, SamplerState.PointClamp, null, null);
|
this.SpriteBatch.Begin(SpriteSortMode.Deferred, null, SamplerState.PointClamp, DepthStencilState.None, RasterizerState.CullCounterClockwise);
|
||||||
|
|
||||||
// we draw the tokenized text in the center of the screen
|
// we draw the tokenized text in the center of the screen
|
||||||
// since the text is already center-aligned, we only need to align it on the y axis here
|
// since the text is already center-aligned, we only need to align it on the y axis here
|
||||||
var size = this.tokenizedText.Measure(this.font) * TextFormattingDemo.Scale;
|
var size = this.tokenizedText.GetArea(Vector2.Zero, TextFormattingDemo.Scale).Size;
|
||||||
var pos = new Vector2(this.GraphicsDevice.Viewport.Width / 2, (this.GraphicsDevice.Viewport.Height - size.Y) / 2);
|
var pos = new Vector2(this.GraphicsDevice.Viewport.Width / 2, (this.GraphicsDevice.Viewport.Height - size.Y) / 2);
|
||||||
|
|
||||||
|
// draw bounds, which can be toggled with B in this demo
|
||||||
|
if (this.drawBounds) {
|
||||||
|
var blank = this.SpriteBatch.GetBlankTexture();
|
||||||
|
this.SpriteBatch.Draw(blank, new RectangleF(pos - new Vector2(size.X / 2, 0), size), Color.Red * 0.25F);
|
||||||
|
foreach (var token in this.tokenizedText.Tokens) {
|
||||||
|
foreach (var area in token.GetArea(pos, TextFormattingDemo.Scale))
|
||||||
|
this.SpriteBatch.Draw(blank, area, Color.Black * 0.25F);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw the text itself
|
||||||
this.tokenizedText.Draw(time, this.SpriteBatch, pos, this.font, Color.White, TextFormattingDemo.Scale, 0);
|
this.tokenizedText.Draw(time, this.SpriteBatch, pos, this.font, Color.White, TextFormattingDemo.Scale, 0);
|
||||||
|
|
||||||
this.SpriteBatch.End();
|
this.SpriteBatch.End();
|
||||||
|
@ -64,6 +80,8 @@ namespace Demos {
|
||||||
public override void Update(GameTime time) {
|
public override void Update(GameTime time) {
|
||||||
// update our tokenized string to animate the animation codes
|
// update our tokenized string to animate the animation codes
|
||||||
this.tokenizedText.Update(time);
|
this.tokenizedText.Update(time);
|
||||||
|
if (this.InputHandler.IsPressed(Keys.B))
|
||||||
|
this.drawBounds = !this.drawBounds;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Clear() {
|
public override void Clear() {
|
||||||
|
|
|
@ -51,8 +51,7 @@ namespace Demos {
|
||||||
CheckboxCheckmark = new TextureRegion(this.testTexture, 24, 0, 8, 8),
|
CheckboxCheckmark = new TextureRegion(this.testTexture, 24, 0, 8, 8),
|
||||||
RadioTexture = new NinePatch(new TextureRegion(this.testTexture, 16, 0, 8, 8), 3),
|
RadioTexture = new NinePatch(new TextureRegion(this.testTexture, 16, 0, 8, 8), 3),
|
||||||
RadioCheckmark = new TextureRegion(this.testTexture, 32, 0, 8, 8),
|
RadioCheckmark = new TextureRegion(this.testTexture, 32, 0, 8, 8),
|
||||||
AdditionalFonts = {{"Monospaced", new GenericSpriteFont(Demo.LoadContent<SpriteFont>("Fonts/MonospacedFont"))}},
|
AdditionalFonts = {{"Monospaced", new GenericSpriteFont(Demo.LoadContent<SpriteFont>("Fonts/MonospacedFont"))}}
|
||||||
LinkColor = Color.CornflowerBlue
|
|
||||||
};
|
};
|
||||||
var untexturedStyle = new UntexturedStyle(this.SpriteBatch) {
|
var untexturedStyle = new UntexturedStyle(this.SpriteBatch) {
|
||||||
TextScale = style.TextScale,
|
TextScale = style.TextScale,
|
||||||
|
@ -69,7 +68,6 @@ namespace Demos {
|
||||||
|
|
||||||
// create the root panel that all the other components sit on and add it to the ui system
|
// create the root panel that all the other components sit on and add it to the ui system
|
||||||
this.root = new Panel(Anchor.Center, new Vector2(80, 100), Vector2.Zero, false, true);
|
this.root = new Panel(Anchor.Center, new Vector2(80, 100), Vector2.Zero, false, true);
|
||||||
this.root.ScrollBar.SmoothScrolling = true;
|
|
||||||
// add the root to the demos' ui
|
// add the root to the demos' ui
|
||||||
this.UiRoot.AddChild(this.root);
|
this.UiRoot.AddChild(this.root);
|
||||||
|
|
||||||
|
@ -94,11 +92,18 @@ namespace Demos {
|
||||||
// a paragraph with formatting codes. To see them all or to add more, check the TextFormatting class
|
// a paragraph with formatting codes. To see them all or to add more, check the TextFormatting class
|
||||||
this.root.AddChild(new Paragraph(Anchor.AutoLeft, 1, "Paragraphs can also contain <c Blue>formatting codes</c>, including colors and <i>text styles</i>. For more info, check out the <b>text formatting demo</b>!"));
|
this.root.AddChild(new Paragraph(Anchor.AutoLeft, 1, "Paragraphs can also contain <c Blue>formatting codes</c>, including colors and <i>text styles</i>. For more info, check out the <b>text formatting demo</b>!"));
|
||||||
|
|
||||||
|
this.root.AddChild(new VerticalSpace(3));
|
||||||
|
this.root.AddChild(new Paragraph(Anchor.AutoCenter, 1, "Text input:", true));
|
||||||
|
this.root.AddChild(new TextField(Anchor.AutoLeft, new Vector2(1, 10)) {
|
||||||
|
PositionOffset = new Vector2(0, 1),
|
||||||
|
PlaceholderText = "Click here to input text"
|
||||||
|
});
|
||||||
|
|
||||||
this.root.AddChild(new VerticalSpace(3));
|
this.root.AddChild(new VerticalSpace(3));
|
||||||
this.root.AddChild(new Paragraph(Anchor.AutoCenter, 1, "Multiline text input:", true));
|
this.root.AddChild(new Paragraph(Anchor.AutoCenter, 1, "Multiline text input:", true));
|
||||||
this.root.AddChild(new TextField(Anchor.AutoLeft, new Vector2(1, 50), multiline: true) {
|
this.root.AddChild(new TextField(Anchor.AutoLeft, new Vector2(1, 50), multiline: true) {
|
||||||
PositionOffset = new Vector2(0, 1),
|
PositionOffset = new Vector2(0, 1),
|
||||||
PlaceholderText = "Click here to input text"
|
PlaceholderText = "Click here to input a lot"
|
||||||
});
|
});
|
||||||
|
|
||||||
this.root.AddChild(new VerticalSpace(3));
|
this.root.AddChild(new VerticalSpace(3));
|
||||||
|
@ -205,12 +210,24 @@ namespace Demos {
|
||||||
dropdown.AddElement(new Button(Anchor.AutoLeft, new Vector2(1, 10), "Buttons"));
|
dropdown.AddElement(new Button(Anchor.AutoLeft, new Vector2(1, 10), "Buttons"));
|
||||||
|
|
||||||
this.root.AddChild(new VerticalSpace(3));
|
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);
|
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") {
|
||||||
this.root.AddChild(new Checkbox(Anchor.AutoLeft, new Vector2(1, 10), "Disabled checkbox") {IsDisabled = true}).PositionOffset = new Vector2(0, 1);
|
IsDisabled = true,
|
||||||
|
PositionOffset = new Vector2(0, 1)
|
||||||
|
});
|
||||||
|
this.root.AddChild(new Checkbox(Anchor.AutoLeft, new Vector2(1, 10), "Disabled checkbox") {
|
||||||
|
IsDisabled = true,
|
||||||
|
PositionOffset = new Vector2(0, 1)
|
||||||
|
});
|
||||||
this.root.AddChild(new Button(Anchor.AutoLeft, new Vector2(1, 10), "Disabled tooltip button", "This button can't be clicked, but can be moved to using automatic navigation, and will display its tooltip even when done so.") {
|
this.root.AddChild(new Button(Anchor.AutoLeft, new Vector2(1, 10), "Disabled tooltip button", "This button can't be clicked, but can be moved to using automatic navigation, and will display its tooltip even when done so.") {
|
||||||
CanSelectDisabled = true,
|
CanSelectDisabled = true,
|
||||||
IsDisabled = true,
|
IsDisabled = true,
|
||||||
Tooltip = {DisplayInAutoNavMode = true},
|
PositionOffset = new Vector2(0, 1),
|
||||||
|
Tooltip = {
|
||||||
|
DisplayInAutoNavMode = true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.root.AddChild(new Button(Anchor.AutoLeft, new Vector2(1, 10), "Button with <b>far</b> too much text which will automatically be cut off, hi!") {
|
||||||
|
TruncateTextIfLong = true,
|
||||||
PositionOffset = new Vector2(0, 1)
|
PositionOffset = new Vector2(0, 1)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -50,3 +50,8 @@ if (this.InputHandler.GetGesture(GestureType.Tap, out var sample)) {
|
||||||
// The gesture did not happen this frame
|
// The gesture did not happen this frame
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### External gesture handling
|
||||||
|
If your game already handles gestures through some other means, you might notice that one of the gesture handling methods stops working correctly. This is due to the fact that MonoGame's gesture querying system only supports each gesture to be queried once before it is removed from the queue.
|
||||||
|
|
||||||
|
If you want to continue using your own gesture handling, but still allow the `InputHandler` to use gestures (for [MLEM.Ui](ui.md), for example), you can set `GesturesExternal` to true in your `InputHandler`. Then, you can use `AddExternalGesture` to make the input handler aware of a gesture for the duration of the update frame that you added it on.
|
||||||
|
|
|
@ -2,7 +2,8 @@
|
||||||
"metadata": [{
|
"metadata": [{
|
||||||
"src": [{
|
"src": [{
|
||||||
"src": "../",
|
"src": "../",
|
||||||
"files": ["**/MLEM**.csproj"]
|
"files": ["**/MLEM**.csproj"],
|
||||||
|
"exclude": ["**.FNA.**"]
|
||||||
}],
|
}],
|
||||||
"dest": "api"
|
"dest": "api"
|
||||||
}],
|
}],
|
||||||
|
@ -44,8 +45,7 @@
|
||||||
"fileMetadataFiles": [],
|
"fileMetadataFiles": [],
|
||||||
"template": [
|
"template": [
|
||||||
"default",
|
"default",
|
||||||
"templates/darkfx",
|
"templates/darkfx"
|
||||||
"templates/custom"
|
|
||||||
],
|
],
|
||||||
"postProcessors": [],
|
"postProcessors": [],
|
||||||
"markdownEngineName": "markdig",
|
"markdownEngineName": "markdig",
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
**MLEM Library for Extending MonoGame and FNA** is a set of multipurpose libraries for the game frameworks [MonoGame](https://www.monogame.net/) and [FNA](https://fna-xna.github.io/) that provides abstractions, quality of life improvements and additional features like an extensive ui system and easy input handling.
|
**MLEM Library for Extending MonoGame and FNA** is a set of multipurpose libraries for the game frameworks [MonoGame](https://www.monogame.net/) and [FNA](https://fna-xna.github.io/) that provides abstractions, quality of life improvements and additional features like an extensive ui system and easy input handling.
|
||||||
|
|
||||||
|
MLEM is platform-agnostic and multi-targets .NET Standard 2.0, .NET 6.0 and .NET Framework 4.5.2, which makes it compatible with MonoGame and FNA on Desktop, mobile devices and consoles.
|
||||||
|
|
||||||
# What next?
|
# What next?
|
||||||
- Get it on [NuGet](https://www.nuget.org/packages?q=mlem)
|
- Get it on [NuGet](https://www.nuget.org/packages?q=mlem)
|
||||||
- Get prerelease builds on [BaGet](https://nuget.ellpeck.de/?q=mlem)
|
- Get prerelease builds on [BaGet](https://nuget.ellpeck.de/?q=mlem)
|
||||||
|
@ -44,3 +46,4 @@ There are several other libraries and tools that work well in combination with M
|
||||||
- [MonoGame.Extended](https://github.com/craftworkgames/MonoGame.Extended), a package that also provides several additional features for MonoGame
|
- [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
|
- [Coroutine](https://github.com/Ellpeck/Coroutine), a package that implements Unity-style coroutines for any project
|
||||||
- [Illumilib](https://github.com/Ellpeck/Illumilib), a simple keyboard and mouse lighting library with support for Razer, Logitech and Corsair devices
|
- [Illumilib](https://github.com/Ellpeck/Illumilib), a simple keyboard and mouse lighting library with support for Razer, Logitech and Corsair devices
|
||||||
|
- [DynamicEnums](https://github.com/Ellpeck/DynamicEnums), which provides enum-like single-instance values with additional capabilities, including dynamic addition of new arbitrary values and flags
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
{{!Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.}}
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
|
||||||
<title>{{#title}}{{title}}{{/title}}{{^title}}{{>partials/title}}{{/title}} {{#_appTitle}}| {{_appTitle}} {{/_appTitle}}</title>
|
|
||||||
<meta name="viewport" content="width=device-width">
|
|
||||||
<meta name="title" content="{{#title}}{{title}}{{/title}}{{^title}}{{>partials/title}}{{/title}} {{#_appTitle}}| {{_appTitle}} {{/_appTitle}}">
|
|
||||||
<meta name="generator" content="docfx {{_docfxVersion}}">
|
|
||||||
{{#_description}}<meta name="description" content="{{_description}}">{{/_description}}
|
|
||||||
<link rel="shortcut icon" href="{{_rel}}{{{_appFaviconPath}}}{{^_appFaviconPath}}favicon.ico{{/_appFaviconPath}}">
|
|
||||||
<link rel="stylesheet" href="{{_rel}}styles/docfx.vendor.css">
|
|
||||||
<link rel="stylesheet" href="{{_rel}}styles/docfx.css">
|
|
||||||
<link rel="stylesheet" href="{{_rel}}styles/main.css">
|
|
||||||
<meta property="docfx:navrel" content="{{_navRel}}">
|
|
||||||
<meta property="docfx:tocrel" content="{{_tocRel}}">
|
|
||||||
{{#_noindex}}<meta name="searchOption" content="noindex">{{/_noindex}}
|
|
||||||
{{#_enableSearch}}<meta property="docfx:rel" content="{{_rel}}">{{/_enableSearch}}
|
|
||||||
{{#_enableNewTab}}<meta property="docfx:newtab" content="true">{{/_enableNewTab}}
|
|
||||||
|
|
||||||
<!-- Global site tag (gtag.js) - Google Analytics -->
|
|
||||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-85JBDH1P3Z"></script>
|
|
||||||
<script>
|
|
||||||
window.dataLayer = window.dataLayer || [];
|
|
||||||
|
|
||||||
let gtag = function () {
|
|
||||||
dataLayer.push(arguments);
|
|
||||||
}
|
|
||||||
gtag('js', new Date());
|
|
||||||
gtag('config', 'G-85JBDH1P3Z');
|
|
||||||
</script>
|
|
||||||
</head>
|
|
2
FNA
2
FNA
|
@ -1 +1 @@
|
||||||
Subproject commit 102990f514f1e5bfac07d33f7c33e2e712946da4
|
Subproject commit 9029e149358197612509e2ee4893870a3b5d590e
|
15
FNA.Settings.props
Normal file
15
FNA.Settings.props
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<PropertyGroup>
|
||||||
|
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<!-- include reference assemblies so we can build for old framework versions without having to install them -->
|
||||||
|
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!-- dummy pack target to allow for this (non-SDK-style) project to be included in dotnet pack -->
|
||||||
|
<Target Name="Pack" />
|
||||||
|
</Project>
|
|
@ -1 +1 @@
|
||||||
Subproject commit f0774130cad6cec0b790a58bc7c811a186443fb3
|
Subproject commit c50bf544bcfb217b518727a0a38eb71fc5725092
|
|
@ -5,33 +5,52 @@ using Microsoft.Xna.Framework;
|
||||||
using Microsoft.Xna.Framework.Content;
|
using Microsoft.Xna.Framework.Content;
|
||||||
using Microsoft.Xna.Framework.Graphics;
|
using Microsoft.Xna.Framework.Graphics;
|
||||||
|
|
||||||
|
#if NET6_0_OR_GREATER
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace MLEM.Data.Content {
|
namespace MLEM.Data.Content {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a version of <see cref="ContentManager"/> that doesn't load content binary <c>xnb</c> files, but rather as their regular formats.
|
/// Represents a version of <see cref="ContentManager"/> that doesn't load content binary <c>xnb</c> files, but rather as their regular formats.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class RawContentManager : ContentManager, IGameComponent {
|
public class RawContentManager : ContentManager, IGameComponent {
|
||||||
|
|
||||||
private static List<RawContentReader> readers;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The graphics device that this content manager uses
|
/// The graphics device that this content manager uses
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly GraphicsDevice GraphicsDevice;
|
public readonly GraphicsDevice GraphicsDevice;
|
||||||
|
|
||||||
private readonly List<IDisposable> disposableAssets = new List<IDisposable>();
|
private readonly List<IDisposable> disposableAssets = new List<IDisposable>();
|
||||||
#if FNA
|
private readonly List<RawContentReader> readers;
|
||||||
|
|
||||||
|
#if FNA
|
||||||
private Dictionary<string, object> LoadedAssets { get; } = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
private Dictionary<string, object> LoadedAssets { get; } = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new content manager with an optionally specified root directory.
|
/// Creates a new content manager with an optionally specified root directory.
|
||||||
|
/// Each <see cref="RawContentReader"/> required for asset loading is gathered and instantiated automatically from the loaded assemblies.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="serviceProvider">The service provider of your game</param>
|
/// <param name="serviceProvider">The service provider of your game</param>
|
||||||
/// <param name="rootDirectory">The root directory. Defaults to "Content"</param>
|
/// <param name="rootDirectory">The root directory. Defaults to "Content"</param>
|
||||||
|
#if NET6_0_OR_GREATER
|
||||||
|
[RequiresUnreferencedCode("Automatically gathered RawContentReader types might be removed, use other constructor to add readers manually")]
|
||||||
|
#endif
|
||||||
public RawContentManager(IServiceProvider serviceProvider, string rootDirectory = "Content") :
|
public RawContentManager(IServiceProvider serviceProvider, string rootDirectory = "Content") :
|
||||||
|
this(serviceProvider, RawContentManager.CollectContentReaders(), rootDirectory) {}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new content manager with an optionally specified root directory.
|
||||||
|
/// Each <see cref="RawContentReader"/> required for asset loading has to be passed using <paramref name="readers"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serviceProvider">The service provider of your game</param>
|
||||||
|
/// <param name="readers">The raw content readers to use, which can be modified externally afterwards to add additional readers if desired.</param>
|
||||||
|
/// <param name="rootDirectory">The root directory. Defaults to "Content"</param>
|
||||||
|
public RawContentManager(IServiceProvider serviceProvider, List<RawContentReader> readers, string rootDirectory) :
|
||||||
base(serviceProvider, rootDirectory) {
|
base(serviceProvider, rootDirectory) {
|
||||||
if (serviceProvider.GetService(typeof(IGraphicsDeviceService)) is IGraphicsDeviceService s)
|
if (serviceProvider.GetService(typeof(IGraphicsDeviceService)) is IGraphicsDeviceService s)
|
||||||
this.GraphicsDevice = s.GraphicsDevice;
|
this.GraphicsDevice = s.GraphicsDevice;
|
||||||
|
this.readers = readers;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -54,18 +73,16 @@ namespace MLEM.Data.Content {
|
||||||
/// <param name="currentAsset">The current asset instance.</param>
|
/// <param name="currentAsset">The current asset instance.</param>
|
||||||
/// <typeparam name="T">The asset's type.</typeparam>
|
/// <typeparam name="T">The asset's type.</typeparam>
|
||||||
protected
|
protected
|
||||||
#if !FNA
|
#if !FNA
|
||||||
override
|
override
|
||||||
#endif
|
#endif
|
||||||
void ReloadAsset<T>(string originalAssetName, T currentAsset) {
|
void ReloadAsset<T>(string originalAssetName, T currentAsset) {
|
||||||
this.Read(originalAssetName, currentAsset);
|
this.Read(originalAssetName, currentAsset);
|
||||||
}
|
}
|
||||||
|
|
||||||
private T Read<T>(string assetName, T existing) {
|
private T Read<T>(string assetName, T existing) {
|
||||||
var triedFiles = new List<string>();
|
var triedFiles = new List<string>();
|
||||||
if (RawContentManager.readers == null)
|
foreach (var reader in this.readers) {
|
||||||
RawContentManager.readers = RawContentManager.CollectContentReaders();
|
|
||||||
foreach (var reader in RawContentManager.readers) {
|
|
||||||
if (!reader.CanRead(typeof(T)))
|
if (!reader.CanRead(typeof(T)))
|
||||||
continue;
|
continue;
|
||||||
foreach (var ext in reader.GetFileExtensions()) {
|
foreach (var ext in reader.GetFileExtensions()) {
|
||||||
|
@ -104,6 +121,9 @@ namespace MLEM.Data.Content {
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Initialize() {}
|
public void Initialize() {}
|
||||||
|
|
||||||
|
#if NET6_0_OR_GREATER
|
||||||
|
[RequiresUnreferencedCode("Automatically gathered RawContentReader types might be removed, use other constructor to add readers manually")]
|
||||||
|
#endif
|
||||||
private static List<RawContentReader> CollectContentReaders() {
|
private static List<RawContentReader> CollectContentReaders() {
|
||||||
var ret = new List<RawContentReader>();
|
var ret = new List<RawContentReader>();
|
||||||
var assemblyExceptions = new List<Exception>();
|
var assemblyExceptions = new List<Exception>();
|
||||||
|
|
|
@ -9,26 +9,25 @@ namespace MLEM.Data.Content {
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override Texture2D Read(RawContentManager manager, string assetPath, Stream stream, Texture2D existing) {
|
protected override Texture2D Read(RawContentManager manager, string assetPath, Stream stream, Texture2D existing) {
|
||||||
#if !FNA
|
#if !FNA
|
||||||
if (existing != null) {
|
if (existing != null) {
|
||||||
existing.Reload(stream);
|
existing.Reload(stream);
|
||||||
return existing;
|
return existing;
|
||||||
} else
|
}
|
||||||
#endif
|
#endif
|
||||||
{
|
|
||||||
// premultiply the texture's color to be in line with the pipeline's texture reader
|
// premultiply the texture's color to be in line with the pipeline's texture reader
|
||||||
using (var texture = Texture2D.FromStream(manager.GraphicsDevice, stream)) {
|
using (var texture = Texture2D.FromStream(manager.GraphicsDevice, stream)) {
|
||||||
var ret = new Texture2D(manager.GraphicsDevice, texture.Width, texture.Height);
|
var ret = new Texture2D(manager.GraphicsDevice, texture.Width, texture.Height);
|
||||||
using (var textureData = texture.GetTextureData()) {
|
using (var textureData = texture.GetTextureData()) {
|
||||||
using (var retData = ret.GetTextureData()) {
|
using (var retData = ret.GetTextureData()) {
|
||||||
for (var x = 0; x < ret.Width; x++) {
|
for (var x = 0; x < ret.Width; x++) {
|
||||||
for (var y = 0; y < ret.Height; y++)
|
for (var y = 0; y < ret.Height; y++)
|
||||||
retData[x, y] = Color.FromNonPremultiplied(textureData[x, y].ToVector4());
|
retData[x, y] = Color.FromNonPremultiplied(textureData[x, y].ToVector4());
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ret;
|
|
||||||
}
|
}
|
||||||
|
return ret;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,10 @@ namespace MLEM.Data.Content {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
#if NET6_0_OR_GREATER
|
||||||
|
[System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026",
|
||||||
|
Justification = "RawContentManager does not support XmlReader in a trimmed or AOT context, so this method is not expected to be called.")]
|
||||||
|
#endif
|
||||||
public override object Read(RawContentManager manager, string assetPath, Stream stream, Type t, object existing) {
|
public override object Read(RawContentManager manager, string assetPath, Stream stream, Type t, object existing) {
|
||||||
return new XmlSerializer(t).Deserialize(stream);
|
return new XmlSerializer(t).Deserialize(stream);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,11 +3,18 @@ using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
|
||||||
|
#if NET6_0_OR_GREATER
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace MLEM.Data {
|
namespace MLEM.Data {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A set of extensions for dealing with copying objects.
|
/// A set of extensions for dealing with copying objects.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Obsolete("CopyExtensions has major flaws and insufficient speed compared to other libraries specifically designed for copying objects.")]
|
[Obsolete("CopyExtensions has major flaws and insufficient speed compared to other libraries specifically designed for copying objects.")]
|
||||||
|
#if NET6_0_OR_GREATER
|
||||||
|
[UnconditionalSuppressMessage("Aot", "IL3050"), UnconditionalSuppressMessage("Aot", "IL2070"), UnconditionalSuppressMessage("Aot", "IL2090")]
|
||||||
|
#endif
|
||||||
public static class CopyExtensions {
|
public static class CopyExtensions {
|
||||||
|
|
||||||
private const BindingFlags DefaultFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
|
private const BindingFlags DefaultFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
|
||||||
|
|
|
@ -7,6 +7,7 @@ using Microsoft.Xna.Framework;
|
||||||
using Microsoft.Xna.Framework.Content;
|
using Microsoft.Xna.Framework.Content;
|
||||||
using Microsoft.Xna.Framework.Graphics;
|
using Microsoft.Xna.Framework.Graphics;
|
||||||
using MLEM.Extensions;
|
using MLEM.Extensions;
|
||||||
|
using MLEM.Misc;
|
||||||
using MLEM.Textures;
|
using MLEM.Textures;
|
||||||
|
|
||||||
namespace MLEM.Data {
|
namespace MLEM.Data {
|
||||||
|
@ -17,15 +18,20 @@ namespace MLEM.Data {
|
||||||
/// </para>
|
/// </para>
|
||||||
/// <para>
|
/// <para>
|
||||||
/// Data texture atlases are designed to be easy to write by hand. Because of this, their structure is very simple.
|
/// Data texture atlases are designed to be easy to write by hand. Because of this, their structure is very simple.
|
||||||
/// Each texture region defined in the atlas consists of its name, followed by a set of possible keywords and their arguments, separated by spaces.
|
/// Each texture region defined in the atlas consists of its names (where multiple names can be separated by whitespace), followed by a set of possible instructions and their arguments, also separated by whitespace.
|
||||||
/// The <c>loc</c> keyword defines the <see cref="TextureRegion.Area"/> of the texture region as a rectangle whose origin is its top-left corner. It requires four arguments: x, y, width and height of the rectangle.
|
/// <list type="bullet">
|
||||||
/// The (optional) <c>piv</c> keyword defines the <see cref="TextureRegion.PivotPixels"/> of the texture region. It requires two arguments: x and y. If it is not supplied, the pivot defaults to the top-left corner of the texture region.
|
/// <item><description>The <c>loc</c> (or <c>location</c>) instruction defines the <see cref="TextureRegion.Area"/> of the texture region as a rectangle whose origin is its top-left corner. It requires four arguments: x, y, width and height of the rectangle.</description></item>
|
||||||
/// The (optional) <c>off</c> keyword defines an offset that is added onto the location and pivot of this texture region. This is useful when copying and pasting a previously defined texture region to create a second region that has a constant offset. It requires two arguments: The x and y offset.
|
/// <item><description>The (optional) <c>piv</c> (or <c>pivot</c>) instruction defines the <see cref="TextureRegion.PivotPixels"/> of the texture region. It requires two arguments: x and y. If it is not supplied, the pivot defaults to the top-left corner of the texture region.</description></item>
|
||||||
|
/// <item><description>The (optional) <c>off</c> (of <c>offset</c>) instruction defines an offset that is added onto the location and pivot of this texture region. This is useful when duplicating a previously defined texture region to create a second region that has a constant offset. It requires two arguments: The x and y offset.</description></item>
|
||||||
|
/// <item><description>The (optional and repeatable) <c>cpy</c> (or <c>copy</c>) instruction defines an additional texture region that should also be generated from the same data, but with a given offset that will be applied to the location and pivot. It requires three arguments: the copy region's name and the x and y offsets.</description></item>
|
||||||
|
/// <item><description>The (optional and repeatable) <c>dat</c> (or <c>data</c>) instruction defines a custom data point that can be added to the resulting <see cref="TextureRegion"/>'s <see cref="GenericDataHolder"/> data. It requires two arguments: the data point's name and the data point's value, the latter of which is also stored as a string value.</description></item>
|
||||||
|
/// <item><description>The (optional) <c>frm</c> (or <c>from</c>) instruction defines a texture region (defined before the current region) whose data should be copied. All data from the region will be copied, but adding additional instructions afterwards modifies the data. It requires one argument: the name of the region whose data to copy. If this instruction is used, the <c>loc</c> instruction is not required.</description></item>
|
||||||
|
/// </list>
|
||||||
/// </para>
|
/// </para>
|
||||||
/// <example>
|
/// <example>
|
||||||
/// The following entry defines a texture region with the name <c>LongTableRight</c>, whose <see cref="TextureRegion.Area"/> will be a rectangle with X=32, Y=30, Width=64, Height=48, and whose <see cref="TextureRegion.PivotPixels"/> will be a vector with X=80, Y=46.
|
/// The following entry defines a texture region with the names <c>LongTableRight</c> and <c>LongTableUp</c>, whose <see cref="TextureRegion.Area"/> will be a rectangle with X=32, Y=30, Width=64, Height=48, and whose <see cref="TextureRegion.PivotPixels"/> will be a vector with X=80, Y=46.
|
||||||
/// <code>
|
/// <code>
|
||||||
/// LongTableRight
|
/// LongTableRight LongTableUp
|
||||||
/// loc 32 30 64 48
|
/// loc 32 30 64 48
|
||||||
/// piv 80 46
|
/// piv 80 46
|
||||||
/// </code>
|
/// </code>
|
||||||
|
@ -66,8 +72,18 @@ namespace MLEM.Data {
|
||||||
this.Texture = texture;
|
this.Texture = texture;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts this data texture atlas to a <see cref="Dictionary{TKey,TValue}"/> and returns the result.
|
||||||
|
/// The resulting dictionary will contain all named regions that this data texture atlas contains.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The dictionary representation of this data texture atlas.</returns>
|
||||||
|
public Dictionary<string, TextureRegion> ToDictionary() {
|
||||||
|
return new Dictionary<string, TextureRegion>(this.regions);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Loads a <see cref="DataTextureAtlas"/> from the given loaded texture and texture data file.
|
/// Loads a <see cref="DataTextureAtlas"/> from the given loaded texture and texture data file.
|
||||||
|
/// For more information on data texture atlases, see the <see cref="DataTextureAtlas"/> type documentation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="texture">The texture to use for this data texture atlas</param>
|
/// <param name="texture">The texture to use for this data texture atlas</param>
|
||||||
/// <param name="content">The content manager to use for loading</param>
|
/// <param name="content">The content manager to use for loading</param>
|
||||||
|
@ -77,45 +93,124 @@ namespace MLEM.Data {
|
||||||
public static DataTextureAtlas LoadAtlasData(TextureRegion texture, ContentManager content, string infoPath, bool pivotRelative = false) {
|
public static DataTextureAtlas LoadAtlasData(TextureRegion texture, ContentManager content, string infoPath, bool pivotRelative = false) {
|
||||||
var info = Path.Combine(content.RootDirectory, infoPath);
|
var info = Path.Combine(content.RootDirectory, infoPath);
|
||||||
string text;
|
string text;
|
||||||
if (Path.IsPathRooted(info)) {
|
try {
|
||||||
text = File.ReadAllText(info);
|
if (Path.IsPathRooted(info)) {
|
||||||
} else {
|
text = File.ReadAllText(info);
|
||||||
using (var reader = new StreamReader(TitleContainer.OpenStream(info)))
|
} else {
|
||||||
text = reader.ReadToEnd();
|
using (var reader = new StreamReader(TitleContainer.OpenStream(info)))
|
||||||
|
text = reader.ReadToEnd();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new ContentLoadException($"Couldn't load data texture atlas data from {info}", e);
|
||||||
}
|
}
|
||||||
var atlas = new DataTextureAtlas(texture);
|
var atlas = new DataTextureAtlas(texture);
|
||||||
|
var words = Regex.Split(text, @"\s+");
|
||||||
|
|
||||||
// parse each texture region: "<names> loc <u> <v> <w> <h> [piv <px> <py>] [off <ox> <oy>]"
|
var namesOffsets = new List<(string, Vector2)>();
|
||||||
foreach (Match match in Regex.Matches(text, @"(.+)\W+loc\W+([0-9]+)\W+([0-9]+)\W+([0-9]+)\W+([0-9]+)\W*(?:piv\W+([0-9.]+)\W+([0-9.]+))?\W*(?:off\W+([0-9.]+)\W+([0-9.]+))?")) {
|
var customData = new Dictionary<string, string>();
|
||||||
// offset
|
var location = Rectangle.Empty;
|
||||||
var off = !match.Groups[8].Success ? Vector2.Zero : new Vector2(
|
var pivot = Vector2.Zero;
|
||||||
float.Parse(match.Groups[8].Value, CultureInfo.InvariantCulture),
|
var offset = Vector2.Zero;
|
||||||
float.Parse(match.Groups[9].Value, CultureInfo.InvariantCulture));
|
for (var i = 0; i < words.Length; i++) {
|
||||||
|
var word = words[i];
|
||||||
|
try {
|
||||||
|
switch (word) {
|
||||||
|
case "loc":
|
||||||
|
case "location":
|
||||||
|
location = new Rectangle(
|
||||||
|
int.Parse(words[i + 1], CultureInfo.InvariantCulture), int.Parse(words[i + 2], CultureInfo.InvariantCulture),
|
||||||
|
int.Parse(words[i + 3], CultureInfo.InvariantCulture), int.Parse(words[i + 4], CultureInfo.InvariantCulture));
|
||||||
|
i += 4;
|
||||||
|
break;
|
||||||
|
case "piv":
|
||||||
|
case "pivot":
|
||||||
|
pivot = new Vector2(
|
||||||
|
float.Parse(words[i + 1], CultureInfo.InvariantCulture),
|
||||||
|
float.Parse(words[i + 2], CultureInfo.InvariantCulture));
|
||||||
|
i += 2;
|
||||||
|
break;
|
||||||
|
case "off":
|
||||||
|
case "offset":
|
||||||
|
offset = new Vector2(
|
||||||
|
float.Parse(words[i + 1], CultureInfo.InvariantCulture),
|
||||||
|
float.Parse(words[i + 2], CultureInfo.InvariantCulture));
|
||||||
|
i += 2;
|
||||||
|
break;
|
||||||
|
case "cpy":
|
||||||
|
case "copy":
|
||||||
|
var copyOffset = new Vector2(
|
||||||
|
float.Parse(words[i + 2], CultureInfo.InvariantCulture),
|
||||||
|
float.Parse(words[i + 3], CultureInfo.InvariantCulture));
|
||||||
|
namesOffsets.Add((words[i + 1], copyOffset));
|
||||||
|
i += 3;
|
||||||
|
break;
|
||||||
|
case "dat":
|
||||||
|
case "data":
|
||||||
|
customData.Add(words[i + 1], words[i + 2]);
|
||||||
|
i += 2;
|
||||||
|
break;
|
||||||
|
case "frm":
|
||||||
|
case "from":
|
||||||
|
var fromRegion = atlas[words[i + 1]];
|
||||||
|
customData.Clear();
|
||||||
|
foreach (var key in fromRegion.GetDataKeys())
|
||||||
|
customData.Add(key, fromRegion.GetData<string>(key));
|
||||||
|
// our main texture might be a sub-region already, so we have to take that into account
|
||||||
|
location = fromRegion.Area.OffsetCopy(new Point(-texture.U, -texture.V));
|
||||||
|
pivot = fromRegion.PivotPixels;
|
||||||
|
if (pivot != Vector2.Zero && !pivotRelative)
|
||||||
|
pivot += location.Location.ToVector2();
|
||||||
|
offset = Vector2.Zero;
|
||||||
|
i += 1;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// if we have data for the previous regions, they're valid so we add them
|
||||||
|
AddCurrentRegions();
|
||||||
|
|
||||||
// location
|
// we're starting a new region (or adding another name for a new region), so clear old data
|
||||||
var loc = new Rectangle(
|
namesOffsets.Add((word.Trim(), Vector2.Zero));
|
||||||
int.Parse(match.Groups[2].Value), int.Parse(match.Groups[3].Value),
|
customData.Clear();
|
||||||
int.Parse(match.Groups[4].Value), int.Parse(match.Groups[5].Value));
|
location = Rectangle.Empty;
|
||||||
loc.Offset(off.ToPoint());
|
pivot = Vector2.Zero;
|
||||||
|
offset = Vector2.Zero;
|
||||||
// pivot
|
break;
|
||||||
var piv = !match.Groups[6].Success ? Vector2.Zero : off + new Vector2(
|
}
|
||||||
float.Parse(match.Groups[6].Value, CultureInfo.InvariantCulture) - (pivotRelative ? 0 : loc.X),
|
} catch (Exception e) {
|
||||||
float.Parse(match.Groups[7].Value, CultureInfo.InvariantCulture) - (pivotRelative ? 0 : loc.Y));
|
throw new ContentLoadException($"Couldn't parse data texture atlas instruction {word} for region(s) {string.Join(", ", namesOffsets)}", e);
|
||||||
|
|
||||||
foreach (var name in Regex.Split(match.Groups[1].Value, @"\W")) {
|
|
||||||
var trimmed = name.Trim();
|
|
||||||
if (trimmed.Length <= 0)
|
|
||||||
continue;
|
|
||||||
var region = new TextureRegion(texture, loc) {
|
|
||||||
PivotPixels = piv,
|
|
||||||
Name = trimmed
|
|
||||||
};
|
|
||||||
atlas.regions.Add(trimmed, region);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// add the last region that was started on
|
||||||
|
AddCurrentRegions();
|
||||||
return atlas;
|
return atlas;
|
||||||
|
|
||||||
|
void AddCurrentRegions() {
|
||||||
|
// the location is the only mandatory information, which is why we check it here
|
||||||
|
if (location == Rectangle.Empty || namesOffsets.Count <= 0)
|
||||||
|
return;
|
||||||
|
foreach (var (name, addedOff) in namesOffsets) {
|
||||||
|
var loc = location;
|
||||||
|
var piv = pivot;
|
||||||
|
var off = offset + addedOff;
|
||||||
|
|
||||||
|
loc.Offset(off.ToPoint());
|
||||||
|
if (piv != Vector2.Zero) {
|
||||||
|
piv += off;
|
||||||
|
if (!pivotRelative)
|
||||||
|
piv -= loc.Location.ToVector2();
|
||||||
|
}
|
||||||
|
|
||||||
|
var region = new TextureRegion(texture, loc) {
|
||||||
|
PivotPixels = piv,
|
||||||
|
Name = name
|
||||||
|
};
|
||||||
|
foreach (var kv in customData)
|
||||||
|
region.SetData(kv.Key, kv.Value);
|
||||||
|
atlas.regions.Add(name, region);
|
||||||
|
}
|
||||||
|
// we only clear names offsets if the location was valid, otherwise we ignore multiple names for a region
|
||||||
|
namesOffsets.Clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -127,6 +222,7 @@ namespace MLEM.Data {
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Loads a <see cref="DataTextureAtlas"/> from the given texture and texture data file.
|
/// Loads a <see cref="DataTextureAtlas"/> from the given texture and texture data file.
|
||||||
|
/// For more information on data texture atlases, see the <see cref="DataTextureAtlas"/> type documentation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="content">The content manager to use for loading</param>
|
/// <param name="content">The content manager to use for loading</param>
|
||||||
/// <param name="texturePath">The path to the texture file</param>
|
/// <param name="texturePath">The path to the texture file</param>
|
||||||
|
|
|
@ -13,9 +13,6 @@ namespace MLEM.Data {
|
||||||
/// 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.
|
/// 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.
|
/// 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}"/>.
|
/// 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}"/>.
|
||||||
///
|
|
||||||
/// This class, and its entire concept, are extremely terrible. If you intend on using this, there's probably at least one better solution available.
|
|
||||||
/// Though if, for some weird reason, you need a way to have more than 64 distinct flags, this is a pretty good solution.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// To include enum-like operator overloads in a dynamic enum named MyEnum, the following code can be used:
|
/// To include enum-like operator overloads in a dynamic enum named MyEnum, the following code can be used:
|
||||||
|
@ -28,7 +25,10 @@ namespace MLEM.Data {
|
||||||
/// public static MyEnum operator ~(MyEnum value) => Neg(value);
|
/// public static MyEnum operator ~(MyEnum value) => Neg(value);
|
||||||
/// </code>
|
/// </code>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
[JsonConverter(typeof(DynamicEnumConverter))]
|
[Obsolete("DynamicEnum has been moved into the DynamicEnums library: https://www.nuget.org/packages/DynamicEnums"), JsonConverter(typeof(DynamicEnumConverter))]
|
||||||
|
#if NET6_0_OR_GREATER
|
||||||
|
[System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Aot", "IL2067")]
|
||||||
|
#endif
|
||||||
public abstract class DynamicEnum {
|
public abstract class DynamicEnum {
|
||||||
|
|
||||||
private static readonly Dictionary<Type, Storage> Storages = new Dictionary<Type, Storage>();
|
private static readonly Dictionary<Type, Storage> Storages = new Dictionary<Type, Storage>();
|
||||||
|
@ -134,7 +134,7 @@ namespace MLEM.Data {
|
||||||
/// <returns>The newly created enum value</returns>
|
/// <returns>The newly created enum value</returns>
|
||||||
public static T AddValue<T>(string name) where T : DynamicEnum {
|
public static T AddValue<T>(string name) where T : DynamicEnum {
|
||||||
BigInteger value = 0;
|
BigInteger value = 0;
|
||||||
while (DynamicEnum.GetStorage(typeof(T)).Values.ContainsKey(value))
|
while (DynamicEnum.IsDefined(typeof(T), value))
|
||||||
value++;
|
value++;
|
||||||
return DynamicEnum.Add<T>(name, value);
|
return DynamicEnum.Add<T>(name, value);
|
||||||
}
|
}
|
||||||
|
@ -149,7 +149,7 @@ namespace MLEM.Data {
|
||||||
/// <returns>The newly created enum value</returns>
|
/// <returns>The newly created enum value</returns>
|
||||||
public static T AddFlag<T>(string name) where T : DynamicEnum {
|
public static T AddFlag<T>(string name) where T : DynamicEnum {
|
||||||
BigInteger value = 1;
|
BigInteger value = 1;
|
||||||
while (DynamicEnum.GetStorage(typeof(T)).Values.ContainsKey(value))
|
while (DynamicEnum.IsDefined(typeof(T), value))
|
||||||
value <<= 1;
|
value <<= 1;
|
||||||
return DynamicEnum.Add<T>(name, value);
|
return DynamicEnum.Add<T>(name, value);
|
||||||
}
|
}
|
||||||
|
@ -174,6 +174,42 @@ namespace MLEM.Data {
|
||||||
return DynamicEnum.GetStorage(type).Values.Values;
|
return DynamicEnum.GetStorage(type).Values.Values;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns all of the defined values from the given dynamic enum type <typeparamref name="T"/> which are contained in <paramref name="combinedFlag"/>.
|
||||||
|
/// Note that, if combined flags are defined in <typeparamref name="T"/>, and <paramref name="combinedFlag"/> contains them, they will also be returned.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="combinedFlag">The combined flags whose individual flags to return.</param>
|
||||||
|
/// <param name="includeZero">Whether the enum value 0 should also be returned, if <typeparamref name="T"/> contains one.</param>
|
||||||
|
/// <typeparam name="T">The type of enum.</typeparam>
|
||||||
|
/// <returns>All of the flags that make up <paramref name="combinedFlag"/>.</returns>
|
||||||
|
public static IEnumerable<T> GetFlags<T>(T combinedFlag, bool includeZero = true) where T : DynamicEnum {
|
||||||
|
foreach (var flag in DynamicEnum.GetValues<T>()) {
|
||||||
|
if (combinedFlag.HasFlag(flag) && (includeZero || DynamicEnum.GetValue(flag) != BigInteger.Zero))
|
||||||
|
yield return flag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns all of the defined unique flags from the given dynamic enum type <typeparamref name="T"/> which are contained in <paramref name="combinedFlag"/>.
|
||||||
|
/// Any combined flags (flags that aren't powers of two) which are defined in <typeparamref name="T"/> will not be returned.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="combinedFlag">The combined flags whose individual flags to return.</param>
|
||||||
|
/// <typeparam name="T">The type of enum.</typeparam>
|
||||||
|
/// <returns>All of the unique flags that make up <paramref name="combinedFlag"/>.</returns>
|
||||||
|
public static IEnumerable<T> GetUniqueFlags<T>(T combinedFlag) where T : DynamicEnum {
|
||||||
|
// we can't use the same method here as EnumHelper.GetUniqueFlags since DynamicEnum doesn't guarantee sorted values
|
||||||
|
var max = DynamicEnum.GetValues<T>().Max(DynamicEnum.GetValue);
|
||||||
|
var uniqueFlag = BigInteger.One;
|
||||||
|
while (uniqueFlag <= max) {
|
||||||
|
if (DynamicEnum.IsDefined(typeof(T), uniqueFlag)) {
|
||||||
|
var uniqueFlagValue = DynamicEnum.GetEnumValue<T>(uniqueFlag);
|
||||||
|
if (combinedFlag.HasFlag(uniqueFlagValue))
|
||||||
|
yield return uniqueFlagValue;
|
||||||
|
}
|
||||||
|
uniqueFlag <<= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the bitwise OR (|) combination of the two dynamic enum values
|
/// Returns the bitwise OR (|) combination of the two dynamic enum values
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -292,7 +328,8 @@ namespace MLEM.Data {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Parses the given <see cref="string"/> into a dynamic enum value and returns the result.
|
/// 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.
|
/// 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>
|
/// 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="type">The type of the dynamic enum value to parse</param>
|
||||||
/// <param name="strg">The string to parse into a dynamic enum value</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>
|
/// <returns>The parsed enum value, or null if parsing fails</returns>
|
||||||
|
@ -315,6 +352,27 @@ namespace MLEM.Data {
|
||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns whether the given <paramref name="value"/> is defined in the given dynamic enum <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 dynamic enum type to query.</param>
|
||||||
|
/// <param name="value">The value to query.</param>
|
||||||
|
/// <returns>Whether the <paramref name="value"/> is defined.</returns>
|
||||||
|
public static bool IsDefined(Type type, BigInteger value) {
|
||||||
|
return DynamicEnum.GetStorage(type).Values.ContainsKey(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns whether the given <paramref name="value"/> is defined in its dynamic enum 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="value">The value to query.</param>
|
||||||
|
/// <returns>Whether the <paramref name="value"/> is defined.</returns>
|
||||||
|
public static bool IsDefined(DynamicEnum value) {
|
||||||
|
return value != null && DynamicEnum.IsDefined(value.GetType(), DynamicEnum.GetValue(value));
|
||||||
|
}
|
||||||
|
|
||||||
private static Storage GetStorage(Type type) {
|
private static Storage GetStorage(Type type) {
|
||||||
if (!DynamicEnum.Storages.TryGetValue(type, out var storage)) {
|
if (!DynamicEnum.Storages.TryGetValue(type, out var storage)) {
|
||||||
storage = new Storage();
|
storage = new Storage();
|
||||||
|
|
|
@ -5,6 +5,7 @@ namespace MLEM.Data.Json {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Converts a <see cref="DynamicEnum"/> to and from JSON
|
/// Converts a <see cref="DynamicEnum"/> to and from JSON
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[Obsolete("DynamicEnum has been moved into the DynamicEnums library: https://www.nuget.org/packages/DynamicEnums"), JsonConverter(typeof(DynamicEnumConverter))]
|
||||||
public class DynamicEnumConverter : JsonConverter<DynamicEnum> {
|
public class DynamicEnumConverter : JsonConverter<DynamicEnum> {
|
||||||
|
|
||||||
/// <summary>Writes the JSON representation of the object.</summary>
|
/// <summary>Writes the JSON representation of the object.</summary>
|
||||||
|
|
|
@ -11,9 +11,16 @@ namespace MLEM.Data.Json {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// An array of all of the <see cref="JsonConverter"/>s that are part of MLEM.Data
|
/// An array of all of the <see cref="JsonConverter"/>s that are part of MLEM.Data
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static readonly JsonConverter[] Converters = typeof(JsonConverters).Assembly.GetExportedTypes()
|
public static readonly JsonConverter[] Converters = {
|
||||||
.Where(t => t.IsSubclassOf(typeof(JsonConverter)) && !t.IsGenericType)
|
new Direction2Converter(),
|
||||||
.Select(Activator.CreateInstance).Cast<JsonConverter>().ToArray();
|
#pragma warning disable CS0618
|
||||||
|
new DynamicEnumConverter(),
|
||||||
|
#pragma warning restore CS0618
|
||||||
|
new PointConverter(),
|
||||||
|
new RectangleConverter(),
|
||||||
|
new RectangleFConverter(),
|
||||||
|
new Vector2Converter()
|
||||||
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds all of the <see cref="JsonConverter"/> objects that are part of MLEM.Data to the given <see cref="JsonSerializer"/>
|
/// Adds all of the <see cref="JsonConverter"/> objects that are part of MLEM.Data to the given <see cref="JsonSerializer"/>
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Runtime.Serialization;
|
|
||||||
using MLEM.Misc;
|
using MLEM.Misc;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
@ -8,16 +7,27 @@ namespace MLEM.Data.Json {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// An <see cref="IGenericDataHolder"/> represents an object that can hold generic key-value based data.
|
/// An <see cref="IGenericDataHolder"/> represents an object that can hold generic key-value based data.
|
||||||
/// This class uses <see cref="JsonTypeSafeWrapper"/> for each object stored to ensure that objects with a custom <see cref="JsonConverter"/> get deserialized as an instance of their original type if <see cref="JsonSerializer.TypeNameHandling"/> is not set to <see cref="TypeNameHandling.None"/>.
|
/// This class uses <see cref="JsonTypeSafeWrapper"/> for each object stored to ensure that objects with a custom <see cref="JsonConverter"/> get deserialized as an instance of their original type if <see cref="JsonSerializer.TypeNameHandling"/> is not set to <see cref="TypeNameHandling.None"/>.
|
||||||
|
/// Note that, using <see cref="SetData"/>, adding <see cref="JsonTypeSafeWrapper{T}"/> instances directly is also possible.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DataContract]
|
#if NET7_0_OR_GREATER
|
||||||
|
[System.Diagnostics.CodeAnalysis.RequiresDynamicCode("The native code for instantiation of JsonTypeSafeWrapper instances might not be available at runtime.")]
|
||||||
|
#endif
|
||||||
public class JsonTypeSafeGenericDataHolder : IGenericDataHolder {
|
public class JsonTypeSafeGenericDataHolder : IGenericDataHolder {
|
||||||
|
|
||||||
[DataMember(EmitDefaultValue = false)]
|
private static readonly string[] EmptyStrings = new string[0];
|
||||||
|
|
||||||
|
[JsonProperty]
|
||||||
private Dictionary<string, JsonTypeSafeWrapper> data;
|
private Dictionary<string, JsonTypeSafeWrapper> data;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
[Obsolete("This method will be removed in a future update in favor of the generic SetData<T>.")]
|
||||||
public void SetData(string key, object data) {
|
public void SetData(string key, object data) {
|
||||||
if (data == default) {
|
this.SetData<object>(key, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void SetData<T>(string key, T data) {
|
||||||
|
if (EqualityComparer<T>.Default.Equals(data, default)) {
|
||||||
if (this.data != null)
|
if (this.data != null)
|
||||||
this.data.Remove(key);
|
this.data.Remove(key);
|
||||||
} else {
|
} else {
|
||||||
|
@ -35,9 +45,9 @@ namespace MLEM.Data.Json {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IReadOnlyCollection<string> GetDataKeys() {
|
public IEnumerable<string> GetDataKeys() {
|
||||||
if (this.data == null)
|
if (this.data == null)
|
||||||
return Array.Empty<string>();
|
return JsonTypeSafeGenericDataHolder.EmptyStrings;
|
||||||
return this.data.Keys;
|
return this.data.Keys;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Runtime.Serialization;
|
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace MLEM.Data.Json {
|
namespace MLEM.Data.Json {
|
||||||
|
@ -15,6 +14,7 @@ namespace MLEM.Data.Json {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns this json type-safe wrapper's value as an <see cref="object"/>.
|
/// Returns this json type-safe wrapper's value as an <see cref="object"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
public abstract object Value { get; }
|
public abstract object Value { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -29,30 +29,37 @@ namespace MLEM.Data.Json {
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new <see cref="JsonTypeSafeWrapper{T}"/> from the given value.
|
/// Creates a new <see cref="JsonTypeSafeWrapper{T}"/> from the given value.
|
||||||
/// The type parameter of the returned wrapper will be equal to the <see cref="Type"/> of the <paramref name="value"/> passed.
|
/// The type parameter of the returned wrapper will be equal to the <see cref="Type"/> of the <paramref name="value"/> passed, even if it is a subtype of <typeparamref name="T"/>.
|
||||||
/// If a <see cref="JsonTypeSafeWrapper{T}"/> for a specific type, known at comepile type, should be created, you can use <see cref="JsonTypeSafeWrapper{T}(T)"/>.
|
/// If a <see cref="JsonTypeSafeWrapper{T}"/> for a specific type, known at compile type, should be created, you can use <see cref="JsonTypeSafeWrapper{T}(T)"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="value">The value to wrap</param>
|
/// <param name="value">The value to wrap</param>
|
||||||
/// <returns>A <see cref="JsonTypeSafeWrapper{T}"/> with a type matching the type of <paramref name="value"/></returns>
|
/// <returns>A <see cref="JsonTypeSafeWrapper{T}"/> with a type matching the type of <paramref name="value"/></returns>
|
||||||
public static JsonTypeSafeWrapper Of(object value) {
|
#if NET7_0_OR_GREATER
|
||||||
var type = typeof(JsonTypeSafeWrapper<>).MakeGenericType(value.GetType());
|
[System.Diagnostics.CodeAnalysis.RequiresDynamicCode("The native code for this instantiation might not be available at runtime if the value's type is a subtype of T.")]
|
||||||
return (JsonTypeSafeWrapper) Activator.CreateInstance(type, value);
|
#endif
|
||||||
|
public static JsonTypeSafeWrapper Of<T>(T value) {
|
||||||
|
if (value.GetType() == typeof(T)) {
|
||||||
|
return new JsonTypeSafeWrapper<T>(value);
|
||||||
|
} else {
|
||||||
|
var type = typeof(JsonTypeSafeWrapper<>).MakeGenericType(value.GetType());
|
||||||
|
return (JsonTypeSafeWrapper) Activator.CreateInstance(type, value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
[DataContract]
|
|
||||||
public class JsonTypeSafeWrapper<T> : JsonTypeSafeWrapper {
|
public class JsonTypeSafeWrapper<T> : JsonTypeSafeWrapper {
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override object Value => this.value;
|
public override object Value => this.value;
|
||||||
[DataMember]
|
|
||||||
|
[JsonProperty]
|
||||||
private readonly T value;
|
private readonly T value;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new json type-safe wrapper instance that wraps the given <paramref name="value"/>.
|
/// Creates a new json type-safe wrapper instance that wraps the given <paramref name="value"/>.
|
||||||
/// If the type of the value is unknown at compile time, <see cref="JsonTypeSafeWrapper.Of"/> can be used instead.
|
/// If the type of the value is unknown at compile time, <see cref="JsonTypeSafeWrapper.Of{T}"/> can be used instead.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="value">The value to wrap</param>
|
/// <param name="value">The value to wrap</param>
|
||||||
public JsonTypeSafeWrapper(T value) {
|
public JsonTypeSafeWrapper(T value) {
|
||||||
|
|
|
@ -4,6 +4,10 @@ using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
#if NET6_0_OR_GREATER
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace MLEM.Data.Json {
|
namespace MLEM.Data.Json {
|
||||||
/// <summary>
|
/// <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}"/>.
|
/// 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}"/>.
|
||||||
|
@ -29,7 +33,11 @@ namespace MLEM.Data.Json {
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="type">The type that the dictionary is declared in</param>
|
/// <param name="type">The type that the dictionary is declared in</param>
|
||||||
/// <param name="memberName">The name of the dictionary itself</param>
|
/// <param name="memberName">The name of the dictionary itself</param>
|
||||||
public StaticJsonConverter(Type type, string memberName) : this(StaticJsonConverter<T>.GetEntries(type, memberName)) {}
|
public StaticJsonConverter(
|
||||||
|
#if NET6_0_OR_GREATER
|
||||||
|
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties | DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.NonPublicFields)]
|
||||||
|
#endif
|
||||||
|
Type type, string memberName) : this(StaticJsonConverter<T>.GetEntries(type, memberName)) {}
|
||||||
|
|
||||||
/// <summary>Writes the JSON representation of the object.</summary>
|
/// <summary>Writes the JSON representation of the object.</summary>
|
||||||
/// <param name="writer">The <see cref="T:Newtonsoft.Json.JsonWriter" /> to write to.</param>
|
/// <param name="writer">The <see cref="T:Newtonsoft.Json.JsonWriter" /> to write to.</param>
|
||||||
|
@ -56,7 +64,11 @@ namespace MLEM.Data.Json {
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Dictionary<string, T> GetEntries(Type type, string memberName) {
|
private static Dictionary<string, T> GetEntries(
|
||||||
|
#if NET6_0_OR_GREATER
|
||||||
|
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties | DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.NonPublicFields)]
|
||||||
|
#endif
|
||||||
|
Type type, string memberName) {
|
||||||
const BindingFlags flags = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;
|
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);
|
var value = type.GetProperty(memberName, flags)?.GetValue(null) ?? type.GetField(memberName, flags)?.GetValue(null);
|
||||||
if (value == null)
|
if (value == null)
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netstandard2.0</TargetFramework>
|
<TargetFrameworks>net452;netstandard2.0;net6.0</TargetFrameworks>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<ProduceReferenceAssembly>true</ProduceReferenceAssembly>
|
<ProduceReferenceAssembly>true</ProduceReferenceAssembly>
|
||||||
|
<IsTrimmable>true</IsTrimmable>
|
||||||
<RootNamespace>MLEM.Data</RootNamespace>
|
<RootNamespace>MLEM.Data</RootNamespace>
|
||||||
<DefineConstants>$(DefineConstants);FNA</DefineConstants>
|
<DefineConstants>$(DefineConstants);FNA</DefineConstants>
|
||||||
<NoWarn>NU1701</NoWarn>
|
<NoWarn>NU1701</NoWarn>
|
||||||
|
@ -27,13 +28,14 @@
|
||||||
<PackageReference Include="Lidgren.Network" Version="1.0.2">
|
<PackageReference Include="Lidgren.Network" Version="1.0.2">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1">
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.2">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Newtonsoft.Json.Bson" Version="1.0.2">
|
<PackageReference Include="Newtonsoft.Json.Bson" Version="1.0.2">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<ProjectReference Include="..\FNA\FNA.Core.csproj">
|
|
||||||
|
<ProjectReference Include="..\FNA\FNA.csproj">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</ProjectReference>
|
</ProjectReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netstandard2.0</TargetFramework>
|
<TargetFrameworks>net452;netstandard2.0;net6.0</TargetFrameworks>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<ProduceReferenceAssembly>true</ProduceReferenceAssembly>
|
<ProduceReferenceAssembly>true</ProduceReferenceAssembly>
|
||||||
|
<IsTrimmable>true</IsTrimmable>
|
||||||
<NoWarn>NU1701</NoWarn>
|
<NoWarn>NU1701</NoWarn>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
@ -25,7 +26,7 @@
|
||||||
<PackageReference Include="Lidgren.Network" Version="1.0.2">
|
<PackageReference Include="Lidgren.Network" Version="1.0.2">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1">
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.2">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Newtonsoft.Json.Bson" Version="1.0.2">
|
<PackageReference Include="Newtonsoft.Json.Bson" Version="1.0.2">
|
||||||
|
|
|
@ -11,6 +11,9 @@ namespace MLEM.Data {
|
||||||
/// Before serializing and deserializing an object, each of the object's fields has to have a handler. New handlers can be added using <see cref="AddHandler{T}(System.Action{Lidgren.Network.NetBuffer,T},System.Func{Lidgren.Network.NetBuffer,T})"/> or <see cref="AddHandler{T}(Newtonsoft.Json.JsonSerializer)"/>.
|
/// Before serializing and deserializing an object, each of the object's fields has to have a handler. New handlers can be added using <see cref="AddHandler{T}(System.Action{Lidgren.Network.NetBuffer,T},System.Func{Lidgren.Network.NetBuffer,T})"/> or <see cref="AddHandler{T}(Newtonsoft.Json.JsonSerializer)"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Obsolete("Lidgren.Network support is deprecated. Consider using LiteNetLib or a custom implementation instead.")]
|
[Obsolete("Lidgren.Network support is deprecated. Consider using LiteNetLib or a custom implementation instead.")]
|
||||||
|
#if NET6_0_OR_GREATER
|
||||||
|
[System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Aot", "IL2070")]
|
||||||
|
#endif
|
||||||
public class NetBufferSerializer {
|
public class NetBufferSerializer {
|
||||||
|
|
||||||
private readonly Dictionary<Type, Action<NetBuffer, object>> writeFunctions = new Dictionary<Type, Action<NetBuffer, object>>();
|
private readonly Dictionary<Type, Action<NetBuffer, object>> writeFunctions = new Dictionary<Type, Action<NetBuffer, object>>();
|
||||||
|
|
|
@ -35,8 +35,8 @@ namespace MLEM.Data {
|
||||||
public TimeSpan LastTotalTime => this.LastCalculationTime + this.LastPackTime;
|
public TimeSpan LastTotalTime => this.LastCalculationTime + this.LastPackTime;
|
||||||
|
|
||||||
private readonly List<Request> texturesToPack = new List<Request>();
|
private readonly List<Request> texturesToPack = new List<Request>();
|
||||||
private readonly List<Request> alreadyPackedTextures = new List<Request>();
|
private readonly List<Request> packedTextures = new List<Request>();
|
||||||
private readonly Dictionary<Point, Point> firstPossiblePosForSizeCache = new Dictionary<Point, Point>();
|
private readonly Dictionary<Point, Point> firstPossiblePosForSize = new Dictionary<Point, Point>();
|
||||||
private readonly Dictionary<Texture2D, TextureData> dataCache = new Dictionary<Texture2D, TextureData>();
|
private readonly Dictionary<Texture2D, TextureData> dataCache = new Dictionary<Texture2D, TextureData>();
|
||||||
private readonly bool autoIncreaseMaxWidth;
|
private readonly bool autoIncreaseMaxWidth;
|
||||||
private readonly bool forcePowerOfTwo;
|
private readonly bool forcePowerOfTwo;
|
||||||
|
@ -71,7 +71,7 @@ namespace MLEM.Data {
|
||||||
/// <param name="padding">The padding that the texture should have around itself. This can be useful if texture bleeding issues occur due to texture coordinate rounding.</param>
|
/// <param name="padding">The padding that the texture should have around itself. This can be useful if texture bleeding issues occur due to texture coordinate rounding.</param>
|
||||||
/// <param name="padWithPixels">Whether the texture's padding should be filled with a copy of the texture's border, rather than transparent pixels. This value only has an effect if <paramref name="padding"/> is greater than 0.</param>
|
/// <param name="padWithPixels">Whether the texture's padding should be filled with a copy of the texture's border, rather than transparent pixels. This value only has an effect if <paramref name="padding"/> is greater than 0.</param>
|
||||||
/// <param name="ignoreTransparent">Whether completely transparent texture regions in the <paramref name="atlas"/> should be ignored. If this is true, they will not be part of the <paramref name="result"/> collection either.</param>
|
/// <param name="ignoreTransparent">Whether completely transparent texture regions in the <paramref name="atlas"/> should be ignored. If this is true, they will not be part of the <paramref name="result"/> collection either.</param>
|
||||||
/// <exception cref="InvalidOperationException">Thrown when trying to add data to a packer that has already been packed, or when trying to add a texture width a width greater than the defined max width.</exception>
|
/// <exception cref="InvalidOperationException">Thrown when trying to add a texture width a width greater than the defined max width.</exception>
|
||||||
public void Add(UniformTextureAtlas atlas, Action<Dictionary<Point, TextureRegion>> result, int padding = 0, bool padWithPixels = false, bool ignoreTransparent = false) {
|
public void Add(UniformTextureAtlas atlas, Action<Dictionary<Point, TextureRegion>> result, int padding = 0, bool padWithPixels = false, bool ignoreTransparent = false) {
|
||||||
var addedRegions = new List<TextureRegion>();
|
var addedRegions = new List<TextureRegion>();
|
||||||
var resultRegions = new Dictionary<Point, TextureRegion>();
|
var resultRegions = new Dictionary<Point, TextureRegion>();
|
||||||
|
@ -104,7 +104,7 @@ namespace MLEM.Data {
|
||||||
/// <param name="result">The result callback which will receive the resulting texture regions.</param>
|
/// <param name="result">The result callback which will receive the resulting texture regions.</param>
|
||||||
/// <param name="padding">The padding that the texture should have around itself. This can be useful if texture bleeding issues occur due to texture coordinate rounding.</param>
|
/// <param name="padding">The padding that the texture should have around itself. This can be useful if texture bleeding issues occur due to texture coordinate rounding.</param>
|
||||||
/// <param name="padWithPixels">Whether the texture's padding should be filled with a copy of the texture's border, rather than transparent pixels. This value only has an effect if <paramref name="padding"/> is greater than 0.</param>
|
/// <param name="padWithPixels">Whether the texture's padding should be filled with a copy of the texture's border, rather than transparent pixels. This value only has an effect if <paramref name="padding"/> is greater than 0.</param>
|
||||||
/// <exception cref="InvalidOperationException">Thrown when trying to add data to a packer that has already been packed, or when trying to add a texture width a width greater than the defined max width.</exception>
|
/// <exception cref="InvalidOperationException">Thrown when trying to add a texture width a width greater than the defined max width.</exception>
|
||||||
public void Add(DataTextureAtlas atlas, Action<Dictionary<string, TextureRegion>> result, int padding = 0, bool padWithPixels = false) {
|
public void Add(DataTextureAtlas atlas, Action<Dictionary<string, TextureRegion>> result, int padding = 0, bool padWithPixels = false) {
|
||||||
var atlasRegions = atlas.RegionNames.ToArray();
|
var atlasRegions = atlas.RegionNames.ToArray();
|
||||||
var resultRegions = new Dictionary<string, TextureRegion>();
|
var resultRegions = new Dictionary<string, TextureRegion>();
|
||||||
|
@ -125,7 +125,7 @@ namespace MLEM.Data {
|
||||||
/// <param name="result">The result callback which will receive the resulting texture region.</param>
|
/// <param name="result">The result callback which will receive the resulting texture region.</param>
|
||||||
/// <param name="padding">The padding that the texture should have around itself. This can be useful if texture bleeding issues occur due to texture coordinate rounding.</param>
|
/// <param name="padding">The padding that the texture should have around itself. This can be useful if texture bleeding issues occur due to texture coordinate rounding.</param>
|
||||||
/// <param name="padWithPixels">Whether the texture's padding should be filled with a copy of the texture's border, rather than transparent pixels. This value only has an effect if <paramref name="padding"/> is greater than 0.</param>
|
/// <param name="padWithPixels">Whether the texture's padding should be filled with a copy of the texture's border, rather than transparent pixels. This value only has an effect if <paramref name="padding"/> is greater than 0.</param>
|
||||||
/// <exception cref="InvalidOperationException">Thrown when trying to add data to a packer that has already been packed, or when trying to add a texture width a width greater than the defined max width.</exception>
|
/// <exception cref="InvalidOperationException">Thrown when trying to add a texture width a width greater than the defined max width.</exception>
|
||||||
public void Add(Texture2D texture, Action<TextureRegion> result, int padding = 0, bool padWithPixels = false) {
|
public void Add(Texture2D texture, Action<TextureRegion> result, int padding = 0, bool padWithPixels = false) {
|
||||||
this.Add(new TextureRegion(texture), result, padding, padWithPixels);
|
this.Add(new TextureRegion(texture), result, padding, padWithPixels);
|
||||||
}
|
}
|
||||||
|
@ -138,10 +138,8 @@ namespace MLEM.Data {
|
||||||
/// <param name="result">The result callback which will receive the resulting texture region.</param>
|
/// <param name="result">The result callback which will receive the resulting texture region.</param>
|
||||||
/// <param name="padding">The padding that the texture should have around itself. This can be useful if texture bleeding issues occur due to texture coordinate rounding.</param>
|
/// <param name="padding">The padding that the texture should have around itself. This can be useful if texture bleeding issues occur due to texture coordinate rounding.</param>
|
||||||
/// <param name="padWithPixels">Whether the texture's padding should be filled with a copy of the texture's border, rather than transparent pixels. This value only has an effect if <paramref name="padding"/> is greater than 0.</param>
|
/// <param name="padWithPixels">Whether the texture's padding should be filled with a copy of the texture's border, rather than transparent pixels. This value only has an effect if <paramref name="padding"/> is greater than 0.</param>
|
||||||
/// <exception cref="InvalidOperationException">Thrown when trying to add data to a packer that has already been packed, or when trying to add a texture width a width greater than the defined max width.</exception>
|
/// <exception cref="InvalidOperationException">Thrown when trying to add a texture width a width greater than the defined max width.</exception>
|
||||||
public void Add(TextureRegion texture, Action<TextureRegion> result, int padding = 0, bool padWithPixels = false) {
|
public void Add(TextureRegion texture, Action<TextureRegion> result, int padding = 0, bool padWithPixels = false) {
|
||||||
if (this.PackedTexture != null)
|
|
||||||
throw new InvalidOperationException("Cannot add texture to a texture packer that is already packed");
|
|
||||||
var paddedWidth = texture.Width + 2 * padding;
|
var paddedWidth = texture.Width + 2 * padding;
|
||||||
if (paddedWidth > this.maxWidth) {
|
if (paddedWidth > this.maxWidth) {
|
||||||
if (this.autoIncreaseMaxWidth) {
|
if (this.autoIncreaseMaxWidth) {
|
||||||
|
@ -154,68 +152,80 @@ namespace MLEM.Data {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Packs all of the textures and texture regions added using <see cref="Add(Microsoft.Xna.Framework.Graphics.Texture2D,System.Action{MLEM.Textures.TextureRegion},int,bool)"/> into one texture.
|
/// Packs all of the textures and texture regions added using <see cref="Add(Microsoft.Xna.Framework.Graphics.Texture2D,System.Action{MLEM.Textures.TextureRegion},int,bool)"/> into one texture, which can be retrieved using <see cref="PackedTexture"/>.
|
||||||
/// The resulting texture will be stored in <see cref="PackedTexture"/>.
|
|
||||||
/// All of the result callbacks that were added will also be invoked.
|
/// All of the result callbacks that were added will also be invoked.
|
||||||
|
/// This method can be called multiple times if regions are added after <see cref="Pack"/> has already been called. When doing so, result callbacks of previous regions may be invoked again if the resulting <see cref="PackedTexture"/> has to be resized to accommodate newly added regions.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="device">The graphics device to use for texture generation</param>
|
/// <param name="device">The graphics device to use for texture generation</param>
|
||||||
/// <exception cref="InvalidOperationException">Thrown when calling this method on a texture packer that has already been packed</exception>
|
|
||||||
public void Pack(GraphicsDevice device) {
|
public void Pack(GraphicsDevice device) {
|
||||||
if (this.PackedTexture != null)
|
|
||||||
throw new InvalidOperationException("Cannot pack a texture packer that is already packed");
|
|
||||||
|
|
||||||
// set pack areas for each request
|
// set pack areas for each request
|
||||||
// we pack larger textures first, so that smaller textures can fit in the gaps that larger ones leave
|
// we pack larger textures first, so that smaller textures can fit in the gaps that larger ones leave
|
||||||
var stopwatch = Stopwatch.StartNew();
|
var stopwatch = Stopwatch.StartNew();
|
||||||
foreach (var request in this.texturesToPack.OrderByDescending(t => t.Texture.Width * t.Texture.Height)) {
|
foreach (var request in this.texturesToPack.OrderByDescending(t => t.Texture.Width * t.Texture.Height)) {
|
||||||
request.PackedArea = this.FindFreeArea(request);
|
request.PackedArea = this.FindFreeArea(request);
|
||||||
// if this is the first position that this request fit in, no other requests of the same size will find a position before it
|
// if this is the first position that this request fit in, no other requests of the same size will find a position before it
|
||||||
this.firstPossiblePosForSizeCache[new Point(request.PackedArea.Width, request.PackedArea.Height)] = request.PackedArea.Location;
|
this.firstPossiblePosForSize[new Point(request.PackedArea.Width, request.PackedArea.Height)] = request.PackedArea.Location;
|
||||||
this.alreadyPackedTextures.Add(request);
|
this.packedTextures.Add(request);
|
||||||
}
|
}
|
||||||
stopwatch.Stop();
|
stopwatch.Stop();
|
||||||
this.LastCalculationTime = stopwatch.Elapsed;
|
this.LastCalculationTime = stopwatch.Elapsed;
|
||||||
|
|
||||||
// figure out texture size and generate texture
|
// figure out texture size and regenerate texture if necessary
|
||||||
var width = this.alreadyPackedTextures.Max(t => t.PackedArea.Right);
|
var width = this.packedTextures.Max(t => t.PackedArea.Right);
|
||||||
var height = this.alreadyPackedTextures.Max(t => t.PackedArea.Bottom);
|
var height = this.packedTextures.Max(t => t.PackedArea.Bottom);
|
||||||
if (this.forcePowerOfTwo) {
|
if (this.forcePowerOfTwo) {
|
||||||
width = RuntimeTexturePacker.ToPowerOfTwo(width);
|
width = RuntimeTexturePacker.ToPowerOfTwo(width);
|
||||||
height = RuntimeTexturePacker.ToPowerOfTwo(height);
|
height = RuntimeTexturePacker.ToPowerOfTwo(height);
|
||||||
}
|
}
|
||||||
if (this.forceSquare)
|
if (this.forceSquare)
|
||||||
width = height = Math.Max(width, height);
|
width = height = Math.Max(width, height);
|
||||||
this.PackedTexture = new Texture2D(device, width, height);
|
|
||||||
|
// if we don't need to regenerate, we only need to add newly added regions
|
||||||
|
IEnumerable<Request> texturesToCopy = this.texturesToPack;
|
||||||
|
if (this.PackedTexture == null || this.PackedTexture.Width != width || this.PackedTexture.Height != height) {
|
||||||
|
this.PackedTexture?.Dispose();
|
||||||
|
this.PackedTexture = new Texture2D(device, width, height);
|
||||||
|
// if we need to regenerate, we need to copy all regions since the old ones were deleted
|
||||||
|
texturesToCopy = this.packedTextures;
|
||||||
|
}
|
||||||
|
|
||||||
// copy texture data onto the packed texture
|
// copy texture data onto the packed texture
|
||||||
stopwatch.Restart();
|
stopwatch.Restart();
|
||||||
using (var data = this.PackedTexture.GetTextureData()) {
|
using (var data = this.PackedTexture.GetTextureData()) {
|
||||||
foreach (var request in this.alreadyPackedTextures)
|
foreach (var request in texturesToCopy)
|
||||||
this.CopyRegion(data, request);
|
this.CopyRegion(data, request);
|
||||||
}
|
}
|
||||||
stopwatch.Stop();
|
stopwatch.Stop();
|
||||||
this.LastPackTime = stopwatch.Elapsed;
|
this.LastPackTime = stopwatch.Elapsed;
|
||||||
|
|
||||||
// invoke callbacks
|
// invoke callbacks for textures we copied
|
||||||
foreach (var request in this.alreadyPackedTextures) {
|
foreach (var request in texturesToCopy) {
|
||||||
var packedArea = request.PackedArea.Shrink(new Point(request.Padding, request.Padding));
|
var packedArea = request.PackedArea.Shrink(new Point(request.Padding, request.Padding));
|
||||||
request.Result.Invoke(new TextureRegion(this.PackedTexture, packedArea));
|
request.Result.Invoke(new TextureRegion(this.PackedTexture, packedArea) {
|
||||||
|
Pivot = request.Texture.Pivot,
|
||||||
|
Name = request.Texture.Name,
|
||||||
|
Source = request.Texture
|
||||||
|
});
|
||||||
if (this.disposeTextures)
|
if (this.disposeTextures)
|
||||||
request.Texture.Texture.Dispose();
|
request.Texture.Texture.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.ClearTempCollections();
|
this.texturesToPack.Clear();
|
||||||
|
this.dataCache.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resets this texture packer, disposing its <see cref="PackedTexture"/> and readying it to be re-used
|
/// Resets this texture packer entirely, disposing its <see cref="PackedTexture"/>, clearing all previously added requests, and readying it to be re-used.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Reset() {
|
public void Reset() {
|
||||||
this.PackedTexture?.Dispose();
|
this.PackedTexture?.Dispose();
|
||||||
this.PackedTexture = null;
|
this.PackedTexture = null;
|
||||||
this.LastCalculationTime = TimeSpan.Zero;
|
this.LastCalculationTime = TimeSpan.Zero;
|
||||||
this.LastPackTime = TimeSpan.Zero;
|
this.LastPackTime = TimeSpan.Zero;
|
||||||
this.ClearTempCollections();
|
this.texturesToPack.Clear();
|
||||||
|
this.packedTextures.Clear();
|
||||||
|
this.firstPossiblePosForSize.Clear();
|
||||||
|
this.dataCache.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
|
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
|
||||||
|
@ -228,12 +238,12 @@ namespace MLEM.Data {
|
||||||
size.X += request.Padding * 2;
|
size.X += request.Padding * 2;
|
||||||
size.Y += request.Padding * 2;
|
size.Y += request.Padding * 2;
|
||||||
|
|
||||||
var pos = this.firstPossiblePosForSizeCache.TryGetValue(size, out var first) ? first : Point.Zero;
|
var pos = this.firstPossiblePosForSize.TryGetValue(size, out var first) ? first : Point.Zero;
|
||||||
var lowestY = int.MaxValue;
|
var lowestY = int.MaxValue;
|
||||||
while (true) {
|
while (true) {
|
||||||
var intersected = false;
|
var intersected = false;
|
||||||
var area = new Rectangle(pos.X, pos.Y, size.X, size.Y);
|
var area = new Rectangle(pos.X, pos.Y, size.X, size.Y);
|
||||||
foreach (var tex in this.alreadyPackedTextures) {
|
foreach (var tex in this.packedTextures) {
|
||||||
if (tex.PackedArea.Intersects(area)) {
|
if (tex.PackedArea.Intersects(area)) {
|
||||||
pos.X = tex.PackedArea.Right;
|
pos.X = tex.PackedArea.Right;
|
||||||
// when we move down, we want to move down by the smallest intersecting texture's height
|
// when we move down, we want to move down by the smallest intersecting texture's height
|
||||||
|
@ -264,7 +274,7 @@ namespace MLEM.Data {
|
||||||
srcColor = Color.Transparent;
|
srcColor = Color.Transparent;
|
||||||
} else {
|
} else {
|
||||||
// otherwise, we just use the closest pixel that is actually in bounds, causing the border pixels to be doubled up
|
// otherwise, we just use the closest pixel that is actually in bounds, causing the border pixels to be doubled up
|
||||||
var src = new Point((int) MathHelper.Clamp(x, 0, request.Texture.Width - 1), (int) MathHelper.Clamp(y, 0, request.Texture.Height - 1));
|
var src = new Point((int) MathHelper.Clamp(x, 0F, request.Texture.Width - 1), (int) MathHelper.Clamp(y, 0F, request.Texture.Height - 1));
|
||||||
srcColor = data[request.Texture.Position + src];
|
srcColor = data[request.Texture.Position + src];
|
||||||
}
|
}
|
||||||
destination[location + new Point(x, y)] = srcColor;
|
destination[location + new Point(x, y)] = srcColor;
|
||||||
|
@ -293,13 +303,6 @@ namespace MLEM.Data {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ClearTempCollections() {
|
|
||||||
this.texturesToPack.Clear();
|
|
||||||
this.alreadyPackedTextures.Clear();
|
|
||||||
this.firstPossiblePosForSizeCache.Clear();
|
|
||||||
this.dataCache.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int ToPowerOfTwo(int value) {
|
private static int ToPowerOfTwo(int value) {
|
||||||
var ret = 1;
|
var ret = 1;
|
||||||
while (ret < value)
|
while (ret < value)
|
||||||
|
|
|
@ -40,5 +40,53 @@ namespace MLEM.Extended.Extensions {
|
||||||
return rect.ToMlem().Penetrate(other.ToMlem(), out normal, out penetration);
|
return rect.ToMlem().Penetrate(other.ToMlem(), out normal, out penetration);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns how far between the given <paramref name="range"/>'s <see cref="Range{T}.Min"/> and <see cref="Range{T}.Max"/> value the given <paramref name="value"/> is, as a number between 0 and 1.
|
||||||
|
/// Note that, if the <paramref name="value"/> is outside the given <paramref name="range"/>, a correct proportional value outside the 0 to 1 range will still be returned.
|
||||||
|
/// This method is the reverse action of <see cref="FromPercentage(MonoGame.Extended.Range{float},float)"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="range">The range to query.</param>
|
||||||
|
/// <param name="value">The value to query.</param>
|
||||||
|
/// <returns>The percentage.</returns>
|
||||||
|
public static float GetPercentage(this Range<float> range, float value) {
|
||||||
|
return (value - range.Min) / (range.Max - range.Min);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns how far between the given <paramref name="range"/>'s <see cref="Range{T}.Min"/> and <see cref="Range{T}.Max"/> value the given <paramref name="value"/> is, as a number between 0 and 1.
|
||||||
|
/// Note that, if the <paramref name="value"/> is outside the given <paramref name="range"/>, a correct proportional value outside the 0 to 1 range will still be returned.
|
||||||
|
/// This method is the reverse action of <see cref="FromPercentage(MonoGame.Extended.Range{int},float)"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="range">The range to query.</param>
|
||||||
|
/// <param name="value">The value to query.</param>
|
||||||
|
/// <returns>The percentage.</returns>
|
||||||
|
public static float GetPercentage(this Range<int> range, float value) {
|
||||||
|
return (value - range.Min) / (range.Max - range.Min);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a value within the given <paramref name="range"/>'s <see cref="Range{T}.Min"/> and <see cref="Range{T}.Max"/> values based on the given <paramref name="percentage"/> into the range.
|
||||||
|
/// Note that, if the <paramref name="percentage"/> is outside the 0 to 1 range, a correct value outside the <paramref name="range"/> will still be returned.
|
||||||
|
/// This method is the reverse action of <see cref="GetPercentage(MonoGame.Extended.Range{float},float)"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="range">The range to query.</param>
|
||||||
|
/// <param name="percentage">The percentage to query.</param>
|
||||||
|
/// <returns>The value.</returns>
|
||||||
|
public static float FromPercentage(this Range<float> range, float percentage) {
|
||||||
|
return (range.Max - range.Min) * percentage + range.Min;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a value within the given <paramref name="range"/>'s <see cref="Range{T}.Min"/> and <see cref="Range{T}.Max"/> values based on the given <paramref name="percentage"/> into the range.
|
||||||
|
/// Note that, if the <paramref name="percentage"/> is outside the 0 to 1 range, a correct value outside the <paramref name="range"/> will still be returned.
|
||||||
|
/// This method is the reverse action of <see cref="GetPercentage(MonoGame.Extended.Range{int},float)"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="range">The range to query.</param>
|
||||||
|
/// <param name="percentage">The percentage to query.</param>
|
||||||
|
/// <returns>The value.</returns>
|
||||||
|
public static float FromPercentage(this Range<int> range, float percentage) {
|
||||||
|
return (range.Max - range.Min) * percentage + range.Min;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,14 +32,14 @@ namespace MLEM.Extended.Font {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override float MeasureChar(char c) {
|
protected override float MeasureCharacter(int codePoint) {
|
||||||
var region = this.Font.GetCharacterRegion(c);
|
var region = this.Font.GetCharacterRegion(codePoint);
|
||||||
return region != null ? new Vector2(region.XAdvance, region.Height).X : 0;
|
return region != null ? new Vector2(region.XAdvance, region.Height).X : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override void DrawChar(SpriteBatch batch, string cString, Vector2 position, Color color, float rotation, Vector2 scale, SpriteEffects effects, float layerDepth) {
|
protected override void DrawCharacter(SpriteBatch batch, int codePoint, string character, Vector2 position, Color color, float rotation, Vector2 scale, SpriteEffects effects, float layerDepth) {
|
||||||
batch.DrawString(this.Font, cString, position, color, rotation, Vector2.Zero, scale, effects, layerDepth);
|
batch.DrawString(this.Font, character, position, color, rotation, Vector2.Zero, scale, effects, layerDepth);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
using FontStashSharp;
|
using FontStashSharp;
|
||||||
using Microsoft.Xna.Framework;
|
using Microsoft.Xna.Framework;
|
||||||
using Microsoft.Xna.Framework.Graphics;
|
using Microsoft.Xna.Framework.Graphics;
|
||||||
using MLEM.Extensions;
|
|
||||||
using MLEM.Font;
|
using MLEM.Font;
|
||||||
|
|
||||||
namespace MLEM.Extended.Font {
|
namespace MLEM.Extended.Font {
|
||||||
|
@ -33,13 +32,13 @@ namespace MLEM.Extended.Font {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override float MeasureChar(char c) {
|
protected override float MeasureCharacter(int codePoint) {
|
||||||
return this.Font.MeasureString(c.ToCachedString()).X;
|
return this.Font.MeasureString(CodePointSource.ToString(codePoint)).X;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override void DrawChar(SpriteBatch batch, string cString, Vector2 position, Color color, float rotation, Vector2 scale, SpriteEffects effects, float layerDepth) {
|
protected override void DrawCharacter(SpriteBatch batch, int codePoint, string character, Vector2 position, Color color, float rotation, Vector2 scale, SpriteEffects effects, float layerDepth) {
|
||||||
this.Font.DrawText(batch, cString, position, color, scale, rotation, Vector2.Zero, layerDepth);
|
this.Font.DrawText(batch, character, position, color, scale, rotation, Vector2.Zero, layerDepth);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netstandard2.0</TargetFramework>
|
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<ProduceReferenceAssembly>true</ProduceReferenceAssembly>
|
<ProduceReferenceAssembly>true</ProduceReferenceAssembly>
|
||||||
|
<IsTrimmable>true</IsTrimmable>
|
||||||
<RootNamespace>MLEM.Extended</RootNamespace>
|
<RootNamespace>MLEM.Extended</RootNamespace>
|
||||||
<DefineConstants>$(DefineConstants);FNA</DefineConstants>
|
<DefineConstants>$(DefineConstants);FNA</DefineConstants>
|
||||||
|
<NoWarn>NU1702</NoWarn>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
|
@ -22,10 +24,10 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\MLEM\MLEM.FNA.csproj" />
|
<ProjectReference Include="..\MLEM\MLEM.FNA.csproj" />
|
||||||
|
|
||||||
<ProjectReference Include="..\FontStashSharp\src\XNA\FontStashSharp.FNA.Core.csproj">
|
<ProjectReference Include="..\FontStashSharp\src\XNA\FontStashSharp.FNA.csproj">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</ProjectReference>
|
</ProjectReference>
|
||||||
<ProjectReference Include="..\FNA\FNA.Core.csproj">
|
<ProjectReference Include="..\FNA\FNA.csproj">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</ProjectReference>
|
</ProjectReference>
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netstandard2.0</TargetFramework>
|
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<ProduceReferenceAssembly>true</ProduceReferenceAssembly>
|
<ProduceReferenceAssembly>true</ProduceReferenceAssembly>
|
||||||
|
<IsTrimmable>true</IsTrimmable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
|
@ -26,7 +27,7 @@
|
||||||
<PackageReference Include="MonoGame.Extended.Tiled" Version="3.8.0">
|
<PackageReference Include="MonoGame.Extended.Tiled" Version="3.8.0">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="FontStashSharp.MonoGame" Version="1.1.6">
|
<PackageReference Include="FontStashSharp.MonoGame" Version="1.2.8">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.0.1641">
|
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.0.1641">
|
||||||
|
|
26
MLEM.FNA.sln
26
MLEM.FNA.sln
|
@ -16,9 +16,11 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests.FNA", "Tests\Tests.FN
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MLEM.Extended.FNA", "MLEM.Extended\MLEM.Extended.FNA.csproj", "{A5B22930-DF4B-4A62-93ED-A6549F7B666B}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MLEM.Extended.FNA", "MLEM.Extended\MLEM.Extended.FNA.csproj", "{A5B22930-DF4B-4A62-93ED-A6549F7B666B}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FNA.Core", "FNA\FNA.Core.csproj", "{06459F72-CEAA-4B45-B2B1-708FC28D04F8}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FNA", "FNA\FNA.csproj", "{35253CE1-C864-4CD3-8249-4D1319748E8F}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FontStashSharp.FNA.Core", "FontStashSharp\src\XNA\FontStashSharp.FNA.Core.csproj", "{0B410591-3AED-4C82-A07A-516FF493709B}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FontStashSharp.FNA", "FontStashSharp\src\XNA\FontStashSharp.FNA.csproj", "{39249E92-EBF2-4951-A086-AB4951C3CCE1}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FNA.Core", "FNA\FNA.Core.csproj", "{458FFA5E-A1C4-4B23-A5D8-259385FEECED}"
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
@ -58,13 +60,17 @@ Global
|
||||||
{A5B22930-DF4B-4A62-93ED-A6549F7B666B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{A5B22930-DF4B-4A62-93ED-A6549F7B666B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{A5B22930-DF4B-4A62-93ED-A6549F7B666B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{A5B22930-DF4B-4A62-93ED-A6549F7B666B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{A5B22930-DF4B-4A62-93ED-A6549F7B666B}.Release|Any CPU.Build.0 = Release|Any CPU
|
{A5B22930-DF4B-4A62-93ED-A6549F7B666B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{06459F72-CEAA-4B45-B2B1-708FC28D04F8}.Debug|Any CPU.ActiveCfg = Debug|x64
|
{35253CE1-C864-4CD3-8249-4D1319748E8F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{06459F72-CEAA-4B45-B2B1-708FC28D04F8}.Debug|Any CPU.Build.0 = Debug|x64
|
{35253CE1-C864-4CD3-8249-4D1319748E8F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{06459F72-CEAA-4B45-B2B1-708FC28D04F8}.Release|Any CPU.ActiveCfg = Release|x64
|
{35253CE1-C864-4CD3-8249-4D1319748E8F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{06459F72-CEAA-4B45-B2B1-708FC28D04F8}.Release|Any CPU.Build.0 = Release|x64
|
{35253CE1-C864-4CD3-8249-4D1319748E8F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{0B410591-3AED-4C82-A07A-516FF493709B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{39249E92-EBF2-4951-A086-AB4951C3CCE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{0B410591-3AED-4C82-A07A-516FF493709B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{39249E92-EBF2-4951-A086-AB4951C3CCE1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{0B410591-3AED-4C82-A07A-516FF493709B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{39249E92-EBF2-4951-A086-AB4951C3CCE1}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{0B410591-3AED-4C82-A07A-516FF493709B}.Release|Any CPU.Build.0 = Release|Any CPU
|
{39249E92-EBF2-4951-A086-AB4951C3CCE1}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{458FFA5E-A1C4-4B23-A5D8-259385FEECED}.Debug|Any CPU.ActiveCfg = Debug|x64
|
||||||
|
{458FFA5E-A1C4-4B23-A5D8-259385FEECED}.Debug|Any CPU.Build.0 = Debug|x64
|
||||||
|
{458FFA5E-A1C4-4B23-A5D8-259385FEECED}.Release|Any CPU.ActiveCfg = Release|x64
|
||||||
|
{458FFA5E-A1C4-4B23-A5D8-259385FEECED}.Release|Any CPU.Build.0 = Release|x64
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netstandard2.0</TargetFramework>
|
<TargetFrameworks>net452;netstandard2.0;net6.0</TargetFrameworks>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<ProduceReferenceAssembly>true</ProduceReferenceAssembly>
|
<ProduceReferenceAssembly>true</ProduceReferenceAssembly>
|
||||||
|
<IsTrimmable>true</IsTrimmable>
|
||||||
<RootNamespace>MLEM.Startup</RootNamespace>
|
<RootNamespace>MLEM.Startup</RootNamespace>
|
||||||
<DefineConstants>$(DefineConstants);FNA</DefineConstants>
|
<DefineConstants>$(DefineConstants);FNA</DefineConstants>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
@ -21,11 +22,11 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Coroutine" Version="2.1.3" />
|
<PackageReference Include="Coroutine" Version="2.1.4" />
|
||||||
<ProjectReference Include="..\MLEM.Ui\MLEM.Ui.FNA.csproj" />
|
<ProjectReference Include="..\MLEM.Ui\MLEM.Ui.FNA.csproj" />
|
||||||
<ProjectReference Include="..\MLEM\MLEM.FNA.csproj" />
|
<ProjectReference Include="..\MLEM\MLEM.FNA.csproj" />
|
||||||
|
|
||||||
<ProjectReference Include="..\FNA\FNA.Core.csproj">
|
<ProjectReference Include="..\FNA\FNA.csproj">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</ProjectReference>
|
</ProjectReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netstandard2.0</TargetFramework>
|
<TargetFrameworks>net452;netstandard2.0;net6.0</TargetFrameworks>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<ProduceReferenceAssembly>true</ProduceReferenceAssembly>
|
<ProduceReferenceAssembly>true</ProduceReferenceAssembly>
|
||||||
|
<IsTrimmable>true</IsTrimmable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
|
@ -19,7 +20,7 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Coroutine" Version="2.1.3" />
|
<PackageReference Include="Coroutine" Version="2.1.4" />
|
||||||
<ProjectReference Include="..\MLEM.Ui\MLEM.Ui.csproj" />
|
<ProjectReference Include="..\MLEM.Ui\MLEM.Ui.csproj" />
|
||||||
<ProjectReference Include="..\MLEM\MLEM.csproj" />
|
<ProjectReference Include="..\MLEM\MLEM.csproj" />
|
||||||
|
|
||||||
|
|
|
@ -69,9 +69,9 @@ namespace MLEM.Startup {
|
||||||
this.GraphicsDeviceManager = new GraphicsDeviceManager(this) {
|
this.GraphicsDeviceManager = new GraphicsDeviceManager(this) {
|
||||||
PreferredBackBufferWidth = windowWidth,
|
PreferredBackBufferWidth = windowWidth,
|
||||||
PreferredBackBufferHeight = windowHeight,
|
PreferredBackBufferHeight = windowHeight,
|
||||||
#if !FNA
|
#if !FNA
|
||||||
HardwareModeSwitch = false
|
HardwareModeSwitch = false
|
||||||
#endif
|
#endif
|
||||||
};
|
};
|
||||||
this.Window.AllowUserResizing = true;
|
this.Window.AllowUserResizing = true;
|
||||||
this.Content.RootDirectory = "Content";
|
this.Content.RootDirectory = "Content";
|
||||||
|
@ -116,9 +116,9 @@ namespace MLEM.Startup {
|
||||||
this.PreDraw?.Invoke(this, gameTime);
|
this.PreDraw?.Invoke(this, gameTime);
|
||||||
CoroutineHandler.RaiseEvent(CoroutineEvents.PreDraw);
|
CoroutineHandler.RaiseEvent(CoroutineEvents.PreDraw);
|
||||||
|
|
||||||
#pragma warning disable CS0618
|
#pragma warning disable CS0618
|
||||||
this.UiSystem.DrawEarly(gameTime, this.SpriteBatch);
|
this.UiSystem.DrawEarly(gameTime, this.SpriteBatch);
|
||||||
#pragma warning restore CS0618
|
#pragma warning restore CS0618
|
||||||
this.DoDraw(gameTime);
|
this.DoDraw(gameTime);
|
||||||
this.UiSystem.Draw(gameTime, this.SpriteBatch);
|
this.UiSystem.Draw(gameTime, this.SpriteBatch);
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netstandard2.0</TargetFramework>
|
<TargetFrameworks>net452;netstandard2.0;net6.0</TargetFrameworks>
|
||||||
<IncludeContentInPack>true</IncludeContentInPack>
|
<IncludeContentInPack>true</IncludeContentInPack>
|
||||||
<IncludeBuildOutput>false</IncludeBuildOutput>
|
<IncludeBuildOutput>false</IncludeBuildOutput>
|
||||||
<ContentTargetFolders>content</ContentTargetFolders>
|
<ContentTargetFolders>content</ContentTargetFolders>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
|
<IsTrimmable>true</IsTrimmable>
|
||||||
<NoWarn>NU5128</NoWarn>
|
<NoWarn>NU5128</NoWarn>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
using System;
|
||||||
using Microsoft.Xna.Framework;
|
using Microsoft.Xna.Framework;
|
||||||
using Microsoft.Xna.Framework.Graphics;
|
using Microsoft.Xna.Framework.Graphics;
|
||||||
using MLEM.Graphics;
|
using MLEM.Graphics;
|
||||||
|
@ -51,8 +52,12 @@ namespace MLEM.Ui.Elements {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Set this property to true to mark the button as disabled.
|
/// Set this property to true to mark the button as disabled.
|
||||||
/// A disabled button cannot be moused over, selected or pressed.
|
/// A disabled button cannot be moused over, selected or pressed.
|
||||||
|
/// If this value changes often, consider using <see cref="AutoDisableCondition"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual bool IsDisabled { get; set; }
|
public virtual bool IsDisabled {
|
||||||
|
get => this.isDisabled || this.AutoDisableCondition?.Invoke(this) == true;
|
||||||
|
set => this.isDisabled = value;
|
||||||
|
}
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether this button's <see cref="Text"/> should be truncated if it exceeds this button's width.
|
/// Whether this button's <see cref="Text"/> should be truncated if it exceeds this button's width.
|
||||||
/// Defaults to false.
|
/// Defaults to false.
|
||||||
|
@ -69,12 +74,19 @@ namespace MLEM.Ui.Elements {
|
||||||
/// If this is true, <see cref="CanBeSelected"/> will be able to return true even if <see cref="IsDisabled"/> is true.
|
/// If this is true, <see cref="CanBeSelected"/> will be able to return true even if <see cref="IsDisabled"/> is true.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool CanSelectDisabled;
|
public bool CanSelectDisabled;
|
||||||
|
/// <summary>
|
||||||
|
/// An optional function that can be used to modify the result of <see cref="IsDisabled"/> automatically based on a user-defined condition. This removes the need to disable a button based on a condition in <see cref="Element.OnUpdated"/> or manually.
|
||||||
|
/// Note that, if <see cref="IsDisabled"/>'s underlying value is set to <see langword="true"/> using <see cref="set_IsDisabled"/>, this function's result will be ignored.
|
||||||
|
/// </summary>
|
||||||
|
public Func<Button, bool> AutoDisableCondition;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override bool CanBeSelected => base.CanBeSelected && (this.CanSelectDisabled || !this.IsDisabled);
|
public override bool CanBeSelected => base.CanBeSelected && (this.CanSelectDisabled || !this.IsDisabled);
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override bool CanBePressed => base.CanBePressed && !this.IsDisabled;
|
public override bool CanBePressed => base.CanBePressed && !this.IsDisabled;
|
||||||
|
|
||||||
|
private bool isDisabled;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new button with the given settings
|
/// Creates a new button with the given settings
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
@ -46,7 +46,13 @@ namespace MLEM.Ui.Elements {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The width of the space between this checkbox and its <see cref="Label"/>
|
/// The width of the space between this checkbox and its <see cref="Label"/>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public StyleProp<float> TextOffsetX;
|
public StyleProp<float> TextOffsetX {
|
||||||
|
get => this.textOffsetX;
|
||||||
|
set {
|
||||||
|
this.textOffsetX = value;
|
||||||
|
this.SetAreaDirty();
|
||||||
|
}
|
||||||
|
}
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether or not this checkbox is currently checked.
|
/// Whether or not this checkbox is currently checked.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -80,6 +86,7 @@ namespace MLEM.Ui.Elements {
|
||||||
public override bool CanBePressed => base.CanBePressed && !this.IsDisabled;
|
public override bool CanBePressed => base.CanBePressed && !this.IsDisabled;
|
||||||
|
|
||||||
private bool isChecked;
|
private bool isChecked;
|
||||||
|
private StyleProp<float> textOffsetX;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new checkbox with the given settings
|
/// Creates a new checkbox with the given settings
|
||||||
|
|
|
@ -42,7 +42,16 @@ namespace MLEM.Ui.Elements {
|
||||||
});
|
});
|
||||||
this.OnAreaUpdated += e => this.Panel.PositionOffset = new Vector2(0, e.Area.Height / this.Scale);
|
this.OnAreaUpdated += e => this.Panel.PositionOffset = new Vector2(0, e.Area.Height / this.Scale);
|
||||||
this.OnOpenedOrClosed += e => this.Priority = this.IsOpen ? 10000 : 0;
|
this.OnOpenedOrClosed += e => this.Priority = this.IsOpen ? 10000 : 0;
|
||||||
this.OnPressed += e => this.IsOpen = !this.IsOpen;
|
this.OnPressed += e => {
|
||||||
|
this.IsOpen = !this.IsOpen;
|
||||||
|
// close other dropdowns in the same root when we open
|
||||||
|
if (this.IsOpen) {
|
||||||
|
this.Root.Element.AndChildren(o => {
|
||||||
|
if (o != this && o is Dropdown d && d.IsOpen)
|
||||||
|
d.IsOpen = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
this.GetGamepadNextElement = (dir, usualNext) => {
|
this.GetGamepadNextElement = (dir, usualNext) => {
|
||||||
// Force navigate down to our first child if we're open
|
// Force navigate down to our first child if we're open
|
||||||
if (this.IsOpen && dir == Direction2.Down)
|
if (this.IsOpen && dir == Direction2.Down)
|
||||||
|
|
|
@ -10,9 +10,9 @@ using MLEM.Extensions;
|
||||||
using MLEM.Graphics;
|
using MLEM.Graphics;
|
||||||
using MLEM.Input;
|
using MLEM.Input;
|
||||||
using MLEM.Misc;
|
using MLEM.Misc;
|
||||||
|
using MLEM.Sound;
|
||||||
using MLEM.Textures;
|
using MLEM.Textures;
|
||||||
using MLEM.Ui.Style;
|
using MLEM.Ui.Style;
|
||||||
using SoundEffectInfo = MLEM.Sound.SoundEffectInfo;
|
|
||||||
|
|
||||||
namespace MLEM.Ui.Elements {
|
namespace MLEM.Ui.Elements {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -31,7 +31,7 @@ namespace MLEM.Ui.Elements {
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public UiSystem System {
|
public UiSystem System {
|
||||||
get => this.system;
|
get => this.system;
|
||||||
internal set {
|
private set {
|
||||||
this.system = value;
|
this.system = value;
|
||||||
this.Controls = value?.Controls;
|
this.Controls = value?.Controls;
|
||||||
this.Style = this.Style.OrStyle(value?.Style);
|
this.Style = this.Style.OrStyle(value?.Style);
|
||||||
|
@ -50,7 +50,7 @@ namespace MLEM.Ui.Elements {
|
||||||
/// This element's <see cref="RootElement"/>.
|
/// This element's <see cref="RootElement"/>.
|
||||||
/// Note that this value is set even if this element has a <see cref="Parent"/>. To get the element that represents the root element, use <see cref="RootElement.Element"/>.
|
/// Note that this value is set even if this element has a <see cref="Parent"/>. To get the element that represents the root element, use <see cref="RootElement.Element"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public RootElement Root { get; internal set; }
|
public RootElement Root { get; private set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The scale that this ui element renders with
|
/// The scale that this ui element renders with
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -71,9 +71,10 @@ namespace MLEM.Ui.Elements {
|
||||||
/// The size of this element, where X represents the width and Y represents the height.
|
/// 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 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 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).
|
||||||
|
/// Additionally, if auto-sizing is used, <see cref="AutoSizeAddedAbsolute"/> can be set to add or subtract an absolute value from the automatically calculated size.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <example>
|
/// <example>
|
||||||
/// The following example combines both types of percentage-based sizing.
|
/// The following example, ignoring <see cref="Scale"/>, combines both types of percentage-based sizing.
|
||||||
/// If this element is inside of a <see cref="Parent"/> whose width is 20, this element's width will be set to <c>0.5 * 20 = 10</c>, and its height will be set to <c>2.5 * 10 = 25</c>.
|
/// If this element is inside of a <see cref="Parent"/> whose width is 20, this element's width will be set to <c>0.5 * 20 = 10</c>, and its height will be set to <c>2.5 * 10 = 25</c>.
|
||||||
/// <code>
|
/// <code>
|
||||||
/// element.Size = new Vector2(0.5F, -2.5F);
|
/// element.Size = new Vector2(0.5F, -2.5F);
|
||||||
|
@ -93,6 +94,26 @@ namespace MLEM.Ui.Elements {
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Vector2 ScaledSize => this.size * this.Scale;
|
public Vector2 ScaledSize => this.size * this.Scale;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
/// If auto-sizing is used by setting <see cref="Size"/> less than or equal to 1, this property allows adding or subtracting an additional, absolute value from the automatically calculated size.
|
||||||
|
/// If this element is not using auto-sizing, this property is ignored.
|
||||||
|
/// </summary>
|
||||||
|
/// <example>
|
||||||
|
/// Ignoring <see cref="Scale"/>, if this element's <see cref="Size"/> is set to <c>0.5, 0.75</c> and its <see cref="Parent"/> has a size of <c>200, 100</c>, then an added absolute size of <c>-50, 25</c> will result in a final <see cref="Area"/> size of <c>0.5 * 200 - 50, 0.75 * 100 + 25</c>, or <c>50, 100</c>.
|
||||||
|
/// </example>
|
||||||
|
public Vector2 AutoSizeAddedAbsolute {
|
||||||
|
get => this.autoSizeAddedAbsolute;
|
||||||
|
set {
|
||||||
|
if (this.autoSizeAddedAbsolute == value)
|
||||||
|
return;
|
||||||
|
this.autoSizeAddedAbsolute = value;
|
||||||
|
this.SetAreaDirty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// The <see cref="AutoSizeAddedAbsolute"/>, but with <see cref="Scale"/> applied.
|
||||||
|
/// </summary>
|
||||||
|
public Vector2 ScaledAutoSizeAddedAbsolute => this.autoSizeAddedAbsolute * this.Scale;
|
||||||
|
/// <summary>
|
||||||
/// This element's offset from its default position, which is dictated by its <see cref="Anchor"/>.
|
/// This element's offset from its default position, which is dictated by its <see cref="Anchor"/>.
|
||||||
/// Note that, depending on the side that the element is anchored to, this offset moves it in a different direction.
|
/// Note that, depending on the side that the element is anchored to, this offset moves it in a different direction.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -185,10 +206,10 @@ namespace MLEM.Ui.Elements {
|
||||||
/// The call that this element should make to <see cref="SpriteBatch"/> to begin drawing.
|
/// The call that this element should make to <see cref="SpriteBatch"/> to begin drawing.
|
||||||
/// Note that, when this is non-null, a new <c>SpriteBatch.Begin</c> call is used for this element.
|
/// Note that, when this is non-null, a new <c>SpriteBatch.Begin</c> call is used for this element.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
#pragma warning disable CS0618
|
#pragma warning disable CS0618
|
||||||
[Obsolete("BeginImpl is deprecated. You can create a custom element class and override Draw instead.")]
|
[Obsolete("BeginImpl is deprecated. You can create a custom element class and override Draw instead.")]
|
||||||
public BeginDelegate BeginImpl;
|
public BeginDelegate BeginImpl;
|
||||||
#pragma warning restore CS0618
|
#pragma warning restore CS0618
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Set this field to false to disallow the element from being selected.
|
/// Set this field to false to disallow the element from being selected.
|
||||||
/// An unselectable element is skipped by automatic navigation and its <see cref="OnSelected"/> callback will never be called.
|
/// An unselectable element is skipped by automatic navigation and its <see cref="OnSelected"/> callback will never be called.
|
||||||
|
@ -212,34 +233,82 @@ namespace MLEM.Ui.Elements {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Set this field to false to cause auto-anchored siblings to ignore this element as a possible anchor point.
|
/// Set this field to false to cause auto-anchored siblings to ignore this element as a possible anchor point.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual bool CanAutoAnchorsAttach { get; set; } = true;
|
public virtual bool CanAutoAnchorsAttach {
|
||||||
|
get => this.canAutoAnchorsAttach;
|
||||||
|
set {
|
||||||
|
if (this.canAutoAnchorsAttach != value) {
|
||||||
|
this.canAutoAnchorsAttach = value;
|
||||||
|
this.SetAreaDirty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
/// <summary>
|
/// <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.
|
/// 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.
|
/// 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>
|
/// </summary>
|
||||||
public virtual bool SetWidthBasedOnChildren { get; set; }
|
public virtual bool SetWidthBasedOnChildren {
|
||||||
|
get => this.setWidthBasedOnChildren;
|
||||||
|
set {
|
||||||
|
if (this.setWidthBasedOnChildren != value) {
|
||||||
|
this.setWidthBasedOnChildren = value;
|
||||||
|
this.SetAreaDirty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
/// <summary>
|
/// <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.
|
/// 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.
|
/// 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>
|
/// </summary>
|
||||||
public virtual bool SetHeightBasedOnChildren { get; set; }
|
public virtual bool SetHeightBasedOnChildren {
|
||||||
|
get => this.setHeightBasedOnChildren;
|
||||||
|
set {
|
||||||
|
if (this.setHeightBasedOnChildren != value) {
|
||||||
|
this.setHeightBasedOnChildren = value;
|
||||||
|
this.SetAreaDirty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 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"/>.
|
/// 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.
|
/// 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.
|
/// Note that this value only has an effect if <see cref="SetWidthBasedOnChildren"/> or <see cref="SetHeightBasedOnChildren"/> are enabled.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual bool TreatSizeAsMinimum { get; set; }
|
public virtual bool TreatSizeAsMinimum {
|
||||||
|
get => this.treatSizeAsMinimum;
|
||||||
|
set {
|
||||||
|
if (this.treatSizeAsMinimum != value) {
|
||||||
|
this.treatSizeAsMinimum = value;
|
||||||
|
this.SetAreaDirty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
/// <summary>
|
/// <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"/>.
|
/// 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.
|
/// Note that this value only has an effect if <see cref="SetWidthBasedOnChildren"/> or <see cref="SetHeightBasedOnChildren"/> are enabled.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual bool TreatSizeAsMaximum { get; set; }
|
public virtual bool TreatSizeAsMaximum {
|
||||||
|
get => this.treatSizeAsMaximum;
|
||||||
|
set {
|
||||||
|
if (this.treatSizeAsMaximum != value) {
|
||||||
|
this.treatSizeAsMaximum = value;
|
||||||
|
this.SetAreaDirty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Set this field to true to cause this element's final display area to never exceed that of its <see cref="Parent"/>.
|
/// 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.
|
/// If the resulting area is too large, the size of this element is shrunk to fit the target area.
|
||||||
/// This can be useful if an element should fill the remaining area of a parent exactly.
|
/// This can be useful if an element should fill the remaining area of a parent exactly.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual bool PreventParentSpill { get; set; }
|
public virtual bool PreventParentSpill {
|
||||||
|
get => this.preventParentSpill;
|
||||||
|
set {
|
||||||
|
if (this.preventParentSpill != value) {
|
||||||
|
this.preventParentSpill = value;
|
||||||
|
this.SetAreaDirty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The transparency (alpha value) that this element is rendered with.
|
/// The transparency (alpha value) that this element is rendered with.
|
||||||
/// Note that, when <see cref="Draw(Microsoft.Xna.Framework.GameTime,Microsoft.Xna.Framework.Graphics.SpriteBatch,float,MLEM.Graphics.SpriteBatchContext)"/> is called, this alpha value is multiplied with the <see cref="Parent"/>'s alpha value and passed down to this element's <see cref="Children"/>.
|
/// Note that, when <see cref="Draw(Microsoft.Xna.Framework.GameTime,Microsoft.Xna.Framework.Graphics.SpriteBatch,float,MLEM.Graphics.SpriteBatchContext)"/> is called, this alpha value is multiplied with the <see cref="Parent"/>'s alpha value and passed down to this element's <see cref="Children"/>.
|
||||||
|
@ -387,16 +456,27 @@ namespace MLEM.Ui.Elements {
|
||||||
public GamepadNextElementCallback GetGamepadNextElement;
|
public GamepadNextElementCallback GetGamepadNextElement;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Event that is called when a child is added to this element using <see cref="AddChild{T}"/>
|
/// Event that is called when a child is added to this element using <see cref="AddChild{T}"/>
|
||||||
|
/// Note that, while this event is only called for immediate children of this element, <see cref="RootElement.OnElementAdded"/> is called for all children and grandchildren.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public OtherElementCallback OnChildAdded;
|
public OtherElementCallback OnChildAdded;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Event that is called when a child is removed from this element using <see cref="RemoveChild"/>
|
/// Event that is called when a child is removed from this element using <see cref="RemoveChild"/>.
|
||||||
|
/// Note that, while this event is only called for immediate children of this element, <see cref="RootElement.OnElementRemoved"/> is called for all children and grandchildren.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public OtherElementCallback OnChildRemoved;
|
public OtherElementCallback OnChildRemoved;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Event that is called when this element's <see cref="Dispose"/> method is called, which also happens in <see cref="Finalize"/>.
|
/// Event that is called when this element is added to a <see cref="UiSystem"/>, that is, when this element's <see cref="System"/> is set to a non-<see langword="null"/> value.
|
||||||
|
/// </summary>
|
||||||
|
public GenericCallback OnAddedToUi;
|
||||||
|
/// <summary>
|
||||||
|
/// Event that is called when this element is removed from a <see cref="UiSystem"/>, that is, when this element's <see cref="System"/> is set to <see langword="null"/>.
|
||||||
|
/// </summary>
|
||||||
|
public GenericCallback OnRemovedFromUi;
|
||||||
|
/// <summary>
|
||||||
|
/// Event that is called when this element's <see cref="Dispose"/> method is called.
|
||||||
/// This event is useful for unregistering global event handlers when this object should be destroyed.
|
/// This event is useful for unregistering global event handlers when this object should be destroyed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[Obsolete("OnDisposed will be removed in a future update. To unregister custom event handlers, use OnRemovedFromUi instead.")]
|
||||||
public GenericCallback OnDisposed;
|
public GenericCallback OnDisposed;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -421,7 +501,7 @@ namespace MLEM.Ui.Elements {
|
||||||
/// The <see cref="ChildPaddedArea"/> of this element's <see cref="Parent"/>, or the <see cref="UiSystem.Viewport"/> if this element has no parent.
|
/// The <see cref="ChildPaddedArea"/> of this element's <see cref="Parent"/>, or the <see cref="UiSystem.Viewport"/> if this element has no parent.
|
||||||
/// This value is the one that is passed to <see cref="CalcActualSize"/> during <see cref="ForceUpdateArea"/>.
|
/// This value is the one that is passed to <see cref="CalcActualSize"/> during <see cref="ForceUpdateArea"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected RectangleF ParentArea => this.Parent?.ChildPaddedArea ?? (RectangleF) this.system.Viewport;
|
protected RectangleF ParentArea => this.Parent?.ChildPaddedArea ?? (RectangleF) this.System.Viewport;
|
||||||
|
|
||||||
private readonly List<Element> children = new List<Element>();
|
private readonly List<Element> children = new List<Element>();
|
||||||
private readonly Stopwatch stopwatch = new Stopwatch();
|
private readonly Stopwatch stopwatch = new Stopwatch();
|
||||||
|
@ -430,6 +510,7 @@ namespace MLEM.Ui.Elements {
|
||||||
private UiSystem system;
|
private UiSystem system;
|
||||||
private Anchor anchor;
|
private Anchor anchor;
|
||||||
private Vector2 size;
|
private Vector2 size;
|
||||||
|
private Vector2 autoSizeAddedAbsolute;
|
||||||
private Vector2 offset;
|
private Vector2 offset;
|
||||||
private RectangleF area;
|
private RectangleF area;
|
||||||
private bool isHidden;
|
private bool isHidden;
|
||||||
|
@ -437,6 +518,12 @@ namespace MLEM.Ui.Elements {
|
||||||
private StyleProp<UiStyle> style;
|
private StyleProp<UiStyle> style;
|
||||||
private StyleProp<Padding> childPadding;
|
private StyleProp<Padding> childPadding;
|
||||||
private bool canBeSelected = true;
|
private bool canBeSelected = true;
|
||||||
|
private bool canAutoAnchorsAttach = true;
|
||||||
|
private bool setWidthBasedOnChildren;
|
||||||
|
private bool setHeightBasedOnChildren;
|
||||||
|
private bool treatSizeAsMinimum;
|
||||||
|
private bool treatSizeAsMaximum;
|
||||||
|
private bool preventParentSpill;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new element with the given anchor and size and sets up some default event reactions.
|
/// Creates a new element with the given anchor and size and sets up some default event reactions.
|
||||||
|
@ -448,14 +535,15 @@ namespace MLEM.Ui.Elements {
|
||||||
this.size = size;
|
this.size = size;
|
||||||
|
|
||||||
this.Children = new ReadOnlyCollection<Element>(this.children);
|
this.Children = new ReadOnlyCollection<Element>(this.children);
|
||||||
this.GetTabNextElement += (backward, next) => next;
|
this.GetTabNextElement = (backward, next) => next;
|
||||||
this.GetGamepadNextElement += (dir, next) => next;
|
this.GetGamepadNextElement = (dir, next) => next;
|
||||||
|
|
||||||
this.SetAreaDirty();
|
this.SetAreaDirty();
|
||||||
this.SetSortedChildrenDirty();
|
this.SetSortedChildrenDirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
[Obsolete("Dispose will be removed in a future update. To unregister custom event handlers, use OnRemovedFromUi instead.")]
|
||||||
~Element() {
|
~Element() {
|
||||||
this.Dispose();
|
this.Dispose();
|
||||||
}
|
}
|
||||||
|
@ -472,12 +560,8 @@ namespace MLEM.Ui.Elements {
|
||||||
index = this.children.Count;
|
index = this.children.Count;
|
||||||
this.children.Insert(index, element);
|
this.children.Insert(index, element);
|
||||||
element.Parent = this;
|
element.Parent = this;
|
||||||
element.AndChildren(e => {
|
element.AndChildren(e => e.AddedToUi(this.System, this.Root));
|
||||||
e.Root = this.Root;
|
this.OnChildAdded?.Invoke(this, element);
|
||||||
e.System = this.System;
|
|
||||||
this.Root?.InvokeOnElementAdded(e);
|
|
||||||
this.OnChildAdded?.Invoke(this, e);
|
|
||||||
});
|
|
||||||
this.SetSortedChildrenDirty();
|
this.SetSortedChildrenDirty();
|
||||||
element.SetAreaDirty();
|
element.SetAreaDirty();
|
||||||
return element;
|
return element;
|
||||||
|
@ -495,12 +579,8 @@ namespace MLEM.Ui.Elements {
|
||||||
// upwards to us if the element is auto-positioned
|
// upwards to us if the element is auto-positioned
|
||||||
element.SetAreaDirty();
|
element.SetAreaDirty();
|
||||||
element.Parent = null;
|
element.Parent = null;
|
||||||
element.AndChildren(e => {
|
element.AndChildren(e => e.RemovedFromUi());
|
||||||
e.Root = null;
|
this.OnChildRemoved?.Invoke(this, element);
|
||||||
e.System = null;
|
|
||||||
this.Root?.InvokeOnElementRemoved(e);
|
|
||||||
this.OnChildRemoved?.Invoke(this, e);
|
|
||||||
});
|
|
||||||
this.SetSortedChildrenDirty();
|
this.SetSortedChildrenDirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -567,7 +647,7 @@ namespace MLEM.Ui.Elements {
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual void ForceUpdateArea() {
|
public virtual void ForceUpdateArea() {
|
||||||
this.AreaDirty = false;
|
this.AreaDirty = false;
|
||||||
if (this.IsHidden)
|
if (this.IsHidden || this.System == null)
|
||||||
return;
|
return;
|
||||||
// if the parent's area is dirty, it would get updated anyway when querying its ChildPaddedArea,
|
// if the parent's area is dirty, it would get updated anyway when querying its ChildPaddedArea,
|
||||||
// which would cause our ForceUpdateArea code to be run twice, so we only update our parent instead
|
// which would cause our ForceUpdateArea code to be run twice, so we only update our parent instead
|
||||||
|
@ -633,7 +713,7 @@ namespace MLEM.Ui.Elements {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.Anchor >= Anchor.AutoLeft) {
|
if (this.Anchor.IsAuto()) {
|
||||||
Element previousChild;
|
Element previousChild;
|
||||||
if (this.Anchor == Anchor.AutoInline || this.Anchor == Anchor.AutoInlineIgnoreOverflow) {
|
if (this.Anchor == Anchor.AutoInline || this.Anchor == Anchor.AutoInlineIgnoreOverflow) {
|
||||||
previousChild = this.GetOlderSibling(e => !e.IsHidden && e.CanAutoAnchorsAttach);
|
previousChild = this.GetOlderSibling(e => !e.IsHidden && e.CanAutoAnchorsAttach);
|
||||||
|
@ -687,11 +767,13 @@ namespace MLEM.Ui.Elements {
|
||||||
if (this.SetHeightBasedOnChildren) {
|
if (this.SetHeightBasedOnChildren) {
|
||||||
var lowest = this.GetLowestChild(e => !e.IsHidden);
|
var lowest = this.GetLowestChild(e => !e.IsHidden);
|
||||||
if (lowest != null) {
|
if (lowest != null) {
|
||||||
autoSize.Y = lowest.UnscrolledArea.Bottom - pos.Y + this.ScaledChildPadding.Bottom;
|
if (lowest.Anchor.IsTopAligned()) {
|
||||||
|
autoSize.Y = lowest.UnscrolledArea.Bottom - pos.Y + this.ScaledChildPadding.Bottom;
|
||||||
|
} else {
|
||||||
|
autoSize.Y = lowest.UnscrolledArea.Height + this.ScaledChildPadding.Height;
|
||||||
|
}
|
||||||
foundChild = lowest;
|
foundChild = lowest;
|
||||||
} else {
|
} else {
|
||||||
if (this.Children.Any(e => !e.IsHidden))
|
|
||||||
throw new InvalidOperationException($"{this} with root {this.Root.Name} sets its height based on children but it only has visible children anchored too low ({string.Join(", ", this.Children.Where(c => !c.IsHidden).Select(c => c.Anchor))})");
|
|
||||||
autoSize.Y = 0;
|
autoSize.Y = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -699,11 +781,13 @@ namespace MLEM.Ui.Elements {
|
||||||
if (this.SetWidthBasedOnChildren) {
|
if (this.SetWidthBasedOnChildren) {
|
||||||
var rightmost = this.GetRightmostChild(e => !e.IsHidden);
|
var rightmost = this.GetRightmostChild(e => !e.IsHidden);
|
||||||
if (rightmost != null) {
|
if (rightmost != null) {
|
||||||
autoSize.X = rightmost.UnscrolledArea.Right - pos.X + this.ScaledChildPadding.Right;
|
if (rightmost.Anchor.IsLeftAligned()) {
|
||||||
|
autoSize.X = rightmost.UnscrolledArea.Right - pos.X + this.ScaledChildPadding.Right;
|
||||||
|
} else {
|
||||||
|
autoSize.X = rightmost.UnscrolledArea.Width + this.ScaledChildPadding.Width;
|
||||||
|
}
|
||||||
foundChild = rightmost;
|
foundChild = rightmost;
|
||||||
} else {
|
} else {
|
||||||
if (this.Children.Any(e => !e.IsHidden))
|
|
||||||
throw new InvalidOperationException($"{this} with root {this.Root.Name} sets its width based on children but it only has visible children anchored too far right ({string.Join(", ", this.Children.Where(c => !c.IsHidden).Select(c => c.Anchor))})");
|
|
||||||
autoSize.X = 0;
|
autoSize.X = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -717,11 +801,9 @@ namespace MLEM.Ui.Elements {
|
||||||
// we want to leave some leeway to prevent float rounding causing an infinite loop
|
// we want to leave some leeway to prevent float rounding causing an infinite loop
|
||||||
if (!autoSize.Equals(this.UnscrolledArea.Size, Element.Epsilon)) {
|
if (!autoSize.Equals(this.UnscrolledArea.Size, Element.Epsilon)) {
|
||||||
recursion++;
|
recursion++;
|
||||||
if (recursion >= 16) {
|
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?");
|
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);
|
||||||
UpdateDisplayArea(autoSize);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -749,12 +831,12 @@ namespace MLEM.Ui.Elements {
|
||||||
/// <returns>The actual size of this element, taking <see cref="Scale"/> into account</returns>
|
/// <returns>The actual size of this element, taking <see cref="Scale"/> into account</returns>
|
||||||
protected virtual Vector2 CalcActualSize(RectangleF parentArea) {
|
protected virtual Vector2 CalcActualSize(RectangleF parentArea) {
|
||||||
var ret = new Vector2(
|
var ret = new Vector2(
|
||||||
this.size.X > 1 ? this.ScaledSize.X : parentArea.Width * this.size.X,
|
this.size.X > 1 ? this.ScaledSize.X : parentArea.Width * this.size.X + this.ScaledAutoSizeAddedAbsolute.X,
|
||||||
this.size.Y > 1 ? this.ScaledSize.Y : parentArea.Height * this.size.Y);
|
this.size.Y > 1 ? this.ScaledSize.Y : parentArea.Height * this.size.Y + this.ScaledAutoSizeAddedAbsolute.Y);
|
||||||
if (this.size.X < 0)
|
if (this.size.X < 0)
|
||||||
ret.X = -this.size.X * ret.Y;
|
ret.X = -this.size.X * ret.Y + this.ScaledAutoSizeAddedAbsolute.X;
|
||||||
if (this.size.Y < 0)
|
if (this.size.Y < 0)
|
||||||
ret.Y = -this.size.Y * ret.X;
|
ret.Y = -this.size.Y * ret.X + this.ScaledAutoSizeAddedAbsolute.Y;
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -773,13 +855,15 @@ namespace MLEM.Ui.Elements {
|
||||||
/// <returns>The lowest element, or null if no such element exists</returns>
|
/// <returns>The lowest element, or null if no such element exists</returns>
|
||||||
public Element GetLowestChild(Func<Element, bool> condition = null) {
|
public Element GetLowestChild(Func<Element, bool> condition = null) {
|
||||||
Element lowest = null;
|
Element lowest = null;
|
||||||
|
var lowestX = float.MinValue;
|
||||||
foreach (var child in this.Children) {
|
foreach (var child in this.Children) {
|
||||||
if (condition != null && !condition(child))
|
if (condition != null && !condition(child))
|
||||||
continue;
|
continue;
|
||||||
if (child.Anchor > Anchor.TopRight && child.Anchor < Anchor.AutoLeft)
|
var x = !child.Anchor.IsTopAligned() ? child.UnscrolledArea.Height : child.UnscrolledArea.Bottom;
|
||||||
continue;
|
if (x >= lowestX) {
|
||||||
if (lowest == null || child.UnscrolledArea.Bottom >= lowest.UnscrolledArea.Bottom)
|
|
||||||
lowest = child;
|
lowest = child;
|
||||||
|
lowestX = x;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return lowest;
|
return lowest;
|
||||||
}
|
}
|
||||||
|
@ -791,13 +875,15 @@ namespace MLEM.Ui.Elements {
|
||||||
/// <returns>The rightmost element, or null if no such element exists</returns>
|
/// <returns>The rightmost element, or null if no such element exists</returns>
|
||||||
public Element GetRightmostChild(Func<Element, bool> condition = null) {
|
public Element GetRightmostChild(Func<Element, bool> condition = null) {
|
||||||
Element rightmost = null;
|
Element rightmost = null;
|
||||||
|
var rightmostX = float.MinValue;
|
||||||
foreach (var child in this.Children) {
|
foreach (var child in this.Children) {
|
||||||
if (condition != null && !condition(child))
|
if (condition != null && !condition(child))
|
||||||
continue;
|
continue;
|
||||||
if (child.Anchor < Anchor.AutoLeft && child.Anchor != Anchor.TopLeft && child.Anchor != Anchor.CenterLeft && child.Anchor != Anchor.BottomLeft)
|
var x = !child.Anchor.IsLeftAligned() ? child.UnscrolledArea.Width : child.UnscrolledArea.Right;
|
||||||
continue;
|
if (x >= rightmostX) {
|
||||||
if (rightmost == null || child.UnscrolledArea.Right >= rightmost.UnscrolledArea.Right)
|
|
||||||
rightmost = child;
|
rightmost = child;
|
||||||
|
rightmostX = x;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return rightmost;
|
return rightmost;
|
||||||
}
|
}
|
||||||
|
@ -951,7 +1037,7 @@ namespace MLEM.Ui.Elements {
|
||||||
/// <param name="alpha">The alpha to draw this element and its children with</param>
|
/// <param name="alpha">The alpha to draw this element and its children with</param>
|
||||||
/// <param name="context">The sprite batch context to use for drawing</param>
|
/// <param name="context">The sprite batch context to use for drawing</param>
|
||||||
public void DrawTransformed(GameTime time, SpriteBatch batch, float alpha, SpriteBatchContext context) {
|
public void DrawTransformed(GameTime time, SpriteBatch batch, float alpha, SpriteBatchContext context) {
|
||||||
#pragma warning disable CS0618
|
#pragma warning disable CS0618
|
||||||
var customDraw = this.BeginImpl != null || this.Transform != Matrix.Identity;
|
var customDraw = this.BeginImpl != null || this.Transform != Matrix.Identity;
|
||||||
var transformed = context;
|
var transformed = context;
|
||||||
transformed.TransformMatrix = this.Transform * transformed.TransformMatrix;
|
transformed.TransformMatrix = this.Transform * transformed.TransformMatrix;
|
||||||
|
@ -962,12 +1048,12 @@ namespace MLEM.Ui.Elements {
|
||||||
// begin our own draw call
|
// begin our own draw call
|
||||||
batch.Begin(transformed);
|
batch.Begin(transformed);
|
||||||
}
|
}
|
||||||
#pragma warning restore CS0618
|
#pragma warning restore CS0618
|
||||||
|
|
||||||
// draw content in custom begin call
|
// draw content in custom begin call
|
||||||
#pragma warning disable CS0618
|
#pragma warning disable CS0618
|
||||||
this.Draw(time, batch, alpha, transformed.BlendState, transformed.SamplerState, transformed.DepthStencilState, transformed.Effect, transformed.TransformMatrix);
|
this.Draw(time, batch, alpha, transformed.BlendState, transformed.SamplerState, transformed.DepthStencilState, transformed.Effect, transformed.TransformMatrix);
|
||||||
#pragma warning restore CS0618
|
#pragma warning restore CS0618
|
||||||
if (this.System != null)
|
if (this.System != null)
|
||||||
this.System.Metrics.Draws++;
|
this.System.Metrics.Draws++;
|
||||||
|
|
||||||
|
@ -1011,9 +1097,9 @@ namespace MLEM.Ui.Elements {
|
||||||
|
|
||||||
foreach (var child in this.GetRelevantChildren()) {
|
foreach (var child in this.GetRelevantChildren()) {
|
||||||
if (!child.IsHidden) {
|
if (!child.IsHidden) {
|
||||||
#pragma warning disable CS0618
|
#pragma warning disable CS0618
|
||||||
child.DrawTransformed(time, batch, alpha * child.DrawAlpha, context.BlendState, context.SamplerState, context.DepthStencilState, context.Effect, context.TransformMatrix);
|
child.DrawTransformed(time, batch, alpha * child.DrawAlpha, context.BlendState, context.SamplerState, context.DepthStencilState, context.Effect, context.TransformMatrix);
|
||||||
#pragma warning restore CS0618
|
#pragma warning restore CS0618
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1058,6 +1144,7 @@ namespace MLEM.Ui.Elements {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
|
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
|
||||||
|
[Obsolete("Dispose will be removed in a future update. To unregister custom event handlers, use OnRemovedFromUi instead.")]
|
||||||
public virtual void Dispose() {
|
public virtual void Dispose() {
|
||||||
this.OnDisposed?.Invoke(this);
|
this.OnDisposed?.Invoke(this);
|
||||||
GC.SuppressFinalize(this);
|
GC.SuppressFinalize(this);
|
||||||
|
@ -1119,7 +1206,7 @@ namespace MLEM.Ui.Elements {
|
||||||
/// <param name="grandchild">Whether the <paramref name="child"/> is a grandchild of this element, rather than a direct child.</param>
|
/// <param name="grandchild">Whether the <paramref name="child"/> is a grandchild of this element, rather than a direct child.</param>
|
||||||
protected virtual void OnChildAreaDirty(Element child, bool grandchild) {
|
protected virtual void OnChildAreaDirty(Element child, bool grandchild) {
|
||||||
if (!grandchild) {
|
if (!grandchild) {
|
||||||
if (child.Anchor >= Anchor.AutoLeft || this.SetWidthBasedOnChildren || this.SetHeightBasedOnChildren)
|
if (child.Anchor.IsAuto() || child.PreventParentSpill || this.SetWidthBasedOnChildren || this.SetHeightBasedOnChildren)
|
||||||
this.SetAreaDirty();
|
this.SetAreaDirty();
|
||||||
}
|
}
|
||||||
this.Parent?.OnChildAreaDirty(child, true);
|
this.Parent?.OnChildAreaDirty(child, true);
|
||||||
|
@ -1147,6 +1234,31 @@ namespace MLEM.Ui.Elements {
|
||||||
return this.TransformInverse(position);
|
return this.TransformInverse(position);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when this element is added to a <see cref="UiSystem"/> and, optionally, a given <see cref="RootElement"/>.
|
||||||
|
/// This method is called in <see cref="AddChild{T}"/> and <see cref="UiSystem.Add"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="system">The ui system to add to.</param>
|
||||||
|
/// <param name="root">The root element to add to.</param>
|
||||||
|
protected internal virtual void AddedToUi(UiSystem system, RootElement root) {
|
||||||
|
this.Root = root;
|
||||||
|
this.System = system;
|
||||||
|
this.OnAddedToUi?.Invoke(this);
|
||||||
|
root?.InvokeOnElementAdded(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when this element is removed from a <see cref="UiSystem"/> and <see cref="RootElement"/>.
|
||||||
|
/// This method is called in <see cref="RemoveChild"/> and <see cref="UiSystem.Remove"/>.
|
||||||
|
/// </summary>
|
||||||
|
protected internal virtual void RemovedFromUi() {
|
||||||
|
var root = this.Root;
|
||||||
|
this.Root = null;
|
||||||
|
this.System = null;
|
||||||
|
this.OnRemovedFromUi?.Invoke(this);
|
||||||
|
root?.InvokeOnElementRemoved(this);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A delegate used for the <see cref="Element.OnTextInput"/> event.
|
/// A delegate used for the <see cref="Element.OnTextInput"/> event.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
@ -68,12 +68,31 @@ namespace MLEM.Ui.Elements {
|
||||||
for (var i = 0; i < amount; i++) {
|
for (var i = 0; i < amount; i++) {
|
||||||
var anchor = i == amount - 1 ? Anchor.AutoInlineIgnoreOverflow : Anchor.AutoInline;
|
var anchor = i == amount - 1 ? Anchor.AutoInlineIgnoreOverflow : Anchor.AutoInline;
|
||||||
cols[i] = new Group(anchor, new Vector2(totalSize.X / amount, totalSize.Y), setHeightBasedOnChildren);
|
cols[i] = new Group(anchor, new Vector2(totalSize.X / amount, totalSize.Y), setHeightBasedOnChildren);
|
||||||
if (parent != null)
|
parent?.AddChild(cols[i]);
|
||||||
parent.AddChild(cols[i]);
|
|
||||||
}
|
}
|
||||||
return cols;
|
return cols;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an array of groups with a fixed width and height that can be used to create a grid with equally sized boxes.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="parent">The element the groups should be added to, can be <see langword="null"/>.</param>
|
||||||
|
/// <param name="totalSize">The total size that the grid should take up.</param>
|
||||||
|
/// <param name="width">The width of the grid, or the amount of columns it should have.</param>
|
||||||
|
/// <param name="height">The height of the grid, or the amount of rows it should have.</param>
|
||||||
|
/// <returns>The created grid.</returns>
|
||||||
|
public static Group[,] MakeGrid(Element parent, Vector2 totalSize, int width, int height) {
|
||||||
|
var grid = new Group[width, height];
|
||||||
|
for (var y = 0; y < height; y++) {
|
||||||
|
for (var x = 0; x < width; x++) {
|
||||||
|
var anchor = x == 0 ? Anchor.AutoLeft : Anchor.AutoInlineIgnoreOverflow;
|
||||||
|
grid[x, y] = new Group(anchor, new Vector2(totalSize.X / width, totalSize.Y / height), false);
|
||||||
|
parent?.AddChild(grid[x, y]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return grid;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a <see cref="TextField"/> with a + and a - button next to it, to allow for easy number input.
|
/// Creates a <see cref="TextField"/> with a + and a - button next to it, to allow for easy number input.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -214,5 +233,32 @@ namespace MLEM.Ui.Elements {
|
||||||
return tooltip;
|
return tooltip;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns whether the given <see cref="Anchor"/> is automatic. The anchors <see cref="Anchor.AutoLeft"/>, <see cref="Anchor.AutoCenter"/>, <see cref="Anchor.AutoRight"/>, <see cref="Anchor.AutoInline"/> and <see cref="Anchor.AutoInlineIgnoreOverflow"/> will return true.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="anchor">The anchor to query.</param>
|
||||||
|
/// <returns>Whether the given anchor is automatic.</returns>
|
||||||
|
public static bool IsAuto(this Anchor anchor) {
|
||||||
|
return anchor == Anchor.AutoLeft || anchor == Anchor.AutoCenter || anchor == Anchor.AutoRight || anchor == Anchor.AutoInline || anchor == Anchor.AutoInlineIgnoreOverflow;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns whether the given <see cref="Anchor"/> is left-aligned for the purpose of <see cref="Element.GetRightmostChild"/>. The anchors <see cref="Anchor.TopLeft"/>, <see cref="Anchor.CenterLeft"/>, <see cref="Anchor.BottomLeft"/>, <see cref="Anchor.AutoLeft"/>, <see cref="Anchor.AutoInline"/> and <see cref="Anchor.AutoInlineIgnoreOverflow"/> will return true.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="anchor">The anchor to query.</param>
|
||||||
|
/// <returns>Whether the given anchor is left-aligned.</returns>
|
||||||
|
public static bool IsLeftAligned(this Anchor anchor) {
|
||||||
|
return anchor == Anchor.TopLeft || anchor == Anchor.CenterLeft || anchor == Anchor.BottomLeft || anchor == Anchor.AutoLeft || anchor == Anchor.AutoInline || anchor == Anchor.AutoInlineIgnoreOverflow;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns whether the given <see cref="Anchor"/> is top-aligned for the purpose of <see cref="Element.GetLowestChild"/>. The anchors <see cref="Anchor.TopLeft"/>, <see cref="Anchor.TopCenter"/>, <see cref="Anchor.TopRight"/>, <see cref="Anchor.AutoLeft"/>, <see cref="Anchor.AutoCenter"/>, <see cref="Anchor.AutoRight"/>, <see cref="Anchor.AutoInline"/> and <see cref="Anchor.AutoInlineIgnoreOverflow"/> will return true.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="anchor">The anchor to query.</param>
|
||||||
|
/// <returns>Whether the given anchor is top-aligned.</returns>
|
||||||
|
public static bool IsTopAligned(this Anchor anchor) {
|
||||||
|
return anchor == Anchor.TopLeft || anchor == Anchor.TopCenter || anchor == Anchor.TopRight || anchor == Anchor.AutoLeft || anchor == Anchor.AutoCenter || anchor == Anchor.AutoRight || anchor == Anchor.AutoInline || anchor == Anchor.AutoInlineIgnoreOverflow;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.Xna.Framework;
|
using Microsoft.Xna.Framework;
|
||||||
using Microsoft.Xna.Framework.Graphics;
|
using Microsoft.Xna.Framework.Graphics;
|
||||||
using MLEM.Extensions;
|
|
||||||
using MLEM.Graphics;
|
using MLEM.Graphics;
|
||||||
using MLEM.Misc;
|
using MLEM.Misc;
|
||||||
using MLEM.Textures;
|
using MLEM.Textures;
|
||||||
using MLEM.Ui.Style;
|
using MLEM.Ui.Style;
|
||||||
|
#if FNA
|
||||||
|
using MLEM.Extensions;
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace MLEM.Ui.Elements {
|
namespace MLEM.Ui.Elements {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -28,17 +30,13 @@ namespace MLEM.Ui.Elements {
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public TextureRegion Texture {
|
public TextureRegion Texture {
|
||||||
get {
|
get {
|
||||||
if (this.GetTextureCallback != null)
|
var ret = this.GetTextureCallback?.Invoke(this) ?? this.texture;
|
||||||
this.Texture = this.GetTextureCallback(this);
|
this.CheckTextureChange(ret);
|
||||||
return this.texture;
|
return ret;
|
||||||
}
|
}
|
||||||
set {
|
set {
|
||||||
if (this.texture != value) {
|
this.texture = value;
|
||||||
this.texture = value;
|
this.CheckTextureChange(value);
|
||||||
this.IsHidden = this.texture == null;
|
|
||||||
if (this.scaleToImage)
|
|
||||||
this.SetAreaDirty();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -73,8 +71,12 @@ namespace MLEM.Ui.Elements {
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public float ImageRotation;
|
public float ImageRotation;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override bool IsHidden => base.IsHidden || this.Texture == null;
|
||||||
|
|
||||||
private bool scaleToImage;
|
private bool scaleToImage;
|
||||||
private TextureRegion texture;
|
private TextureRegion texture;
|
||||||
|
private TextureRegion lastTexture;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new image with the given settings
|
/// Creates a new image with the given settings
|
||||||
|
@ -121,6 +123,15 @@ namespace MLEM.Ui.Elements {
|
||||||
base.Draw(time, batch, alpha, context);
|
base.Draw(time, batch, alpha, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void CheckTextureChange(TextureRegion newTexture) {
|
||||||
|
if (this.lastTexture == newTexture)
|
||||||
|
return;
|
||||||
|
var nullChanged = this.lastTexture == null != (newTexture == null);
|
||||||
|
this.lastTexture = newTexture;
|
||||||
|
if (nullChanged || this.scaleToImage)
|
||||||
|
this.SetAreaDirty();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A delegate method used for <see cref="Image.GetTextureCallback"/>
|
/// A delegate method used for <see cref="Image.GetTextureCallback"/>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
@ -46,7 +46,13 @@ namespace MLEM.Ui.Elements {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The amount of pixels of room there should be between the <see cref="ScrollBar"/> and the rest of the content
|
/// The amount of pixels of room there should be between the <see cref="ScrollBar"/> and the rest of the content
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public StyleProp<float> ScrollBarOffset;
|
public StyleProp<float> ScrollBarOffset {
|
||||||
|
get => this.scrollBarOffset;
|
||||||
|
set {
|
||||||
|
this.scrollBarOffset = value;
|
||||||
|
this.SetAreaDirty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private readonly List<Element> relevantChildren = new List<Element>();
|
private readonly List<Element> relevantChildren = new List<Element>();
|
||||||
private readonly bool scrollOverflow;
|
private readonly bool scrollOverflow;
|
||||||
|
@ -54,6 +60,7 @@ namespace MLEM.Ui.Elements {
|
||||||
private RenderTarget2D renderTarget;
|
private RenderTarget2D renderTarget;
|
||||||
private bool relevantChildrenDirty;
|
private bool relevantChildrenDirty;
|
||||||
private float scrollBarChildOffset;
|
private float scrollBarChildOffset;
|
||||||
|
private StyleProp<float> scrollBarOffset;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new panel with the given settings.
|
/// Creates a new panel with the given settings.
|
||||||
|
@ -84,8 +91,7 @@ namespace MLEM.Ui.Elements {
|
||||||
return;
|
return;
|
||||||
if (e == null || !e.GetParentTree().Contains(this))
|
if (e == null || !e.GetParentTree().Contains(this))
|
||||||
return;
|
return;
|
||||||
var firstChild = this.Children.First(c => c != this.ScrollBar);
|
this.ScrollToElement(e);
|
||||||
this.ScrollBar.CurrentValue = (e.Area.Center.Y - this.Area.Height / 2 - firstChild.Area.Top) / e.Scale + this.ChildPadding.Value.Height / 2;
|
|
||||||
};
|
};
|
||||||
this.AddChild(this.ScrollBar);
|
this.AddChild(this.ScrollBar);
|
||||||
}
|
}
|
||||||
|
@ -98,7 +104,7 @@ namespace MLEM.Ui.Elements {
|
||||||
if (this.SetHeightBasedOnChildren)
|
if (this.SetHeightBasedOnChildren)
|
||||||
throw new NotSupportedException("A panel can't both set height based on children and scroll overflow");
|
throw new NotSupportedException("A panel can't both set height based on children and scroll overflow");
|
||||||
foreach (var child in this.Children) {
|
foreach (var child in this.Children) {
|
||||||
if (child != this.ScrollBar && child.Anchor < Anchor.AutoLeft)
|
if (child != this.ScrollBar && !child.Anchor.IsAuto())
|
||||||
throw new NotSupportedException($"A panel that handles overflow can't contain non-automatic anchors ({child})");
|
throw new NotSupportedException($"A panel that handles overflow can't contain non-automatic anchors ({child})");
|
||||||
if (child is Panel panel && panel.scrollOverflow)
|
if (child is Panel panel && panel.scrollOverflow)
|
||||||
throw new NotSupportedException($"A panel that scrolls overflow cannot contain another panel that scrolls overflow ({child})");
|
throw new NotSupportedException($"A panel that scrolls overflow cannot contain another panel that scrolls overflow ({child})");
|
||||||
|
@ -115,19 +121,6 @@ namespace MLEM.Ui.Elements {
|
||||||
this.ScrollSetup();
|
this.ScrollSetup();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ScrollChildren() {
|
|
||||||
if (!this.scrollOverflow)
|
|
||||||
return;
|
|
||||||
var offset = new Vector2(0, -this.ScrollBar.CurrentValue);
|
|
||||||
// we ignore false grandchildren so that the children of the scroll bar stay in place
|
|
||||||
foreach (var child in this.GetChildren(c => c != this.ScrollBar, true, true)) {
|
|
||||||
if (!child.ScrollOffset.Equals(offset, Element.Epsilon)) {
|
|
||||||
child.ScrollOffset = offset;
|
|
||||||
this.relevantChildrenDirty = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override void ForceUpdateSortedChildren() {
|
public override void ForceUpdateSortedChildren() {
|
||||||
base.ForceUpdateSortedChildren();
|
base.ForceUpdateSortedChildren();
|
||||||
|
@ -147,26 +140,6 @@ namespace MLEM.Ui.Elements {
|
||||||
base.RemoveChildren(e => e != this.ScrollBar && (condition == null || condition(e)));
|
base.RemoveChildren(e => e != this.ScrollBar && (condition == null || condition(e)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override IList<Element> GetRelevantChildren() {
|
|
||||||
var relevant = base.GetRelevantChildren();
|
|
||||||
if (this.scrollOverflow) {
|
|
||||||
if (this.relevantChildrenDirty)
|
|
||||||
this.ForceUpdateRelevantChildren();
|
|
||||||
relevant = this.relevantChildren;
|
|
||||||
}
|
|
||||||
return relevant;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void OnChildAreaDirty(Element child, bool grandchild) {
|
|
||||||
base.OnChildAreaDirty(child, grandchild);
|
|
||||||
// we only need to scroll when a grandchild changes, since all of our children are forced
|
|
||||||
// to be auto-anchored and so will automatically propagate their changes up to us
|
|
||||||
if (grandchild)
|
|
||||||
this.ScrollChildren();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override void Draw(GameTime time, SpriteBatch batch, float alpha, SpriteBatchContext context) {
|
public override void Draw(GameTime time, SpriteBatch batch, float alpha, SpriteBatchContext context) {
|
||||||
// draw children onto the render target if we have one
|
// draw children onto the render target if we have one
|
||||||
|
@ -211,11 +184,23 @@ namespace MLEM.Ui.Elements {
|
||||||
return base.GetElementUnderPos(position);
|
return base.GetElementUnderPos(position);
|
||||||
}
|
}
|
||||||
|
|
||||||
private RectangleF GetRenderTargetArea() {
|
/// <summary>
|
||||||
var area = this.ChildPaddedArea;
|
/// Scrolls this panel's <see cref="ScrollBar"/> to the given <see cref="Element"/> in such a way that its center is positioned in the center of this panel.
|
||||||
area.X = this.DisplayArea.X;
|
/// </summary>
|
||||||
area.Width = this.DisplayArea.Width;
|
/// <param name="element">The element to scroll to.</param>
|
||||||
return area;
|
public void ScrollToElement(Element element) {
|
||||||
|
this.ScrollToElement(element.Area.Center.Y);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Scrolls this panel's <see cref="ScrollBar"/> to the given <paramref name="elementY"/> coordinate in such a way that the coordinate is positioned in the center of this panel.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="elementY">The y coordinate to scroll to, which should have this element's <see cref="Element.Scale"/> applied.</param>
|
||||||
|
public void ScrollToElement(float elementY) {
|
||||||
|
var firstChild = this.Children.FirstOrDefault(c => c != this.ScrollBar);
|
||||||
|
if (firstChild == null)
|
||||||
|
return;
|
||||||
|
this.ScrollBar.CurrentValue = (elementY - this.Area.Height / 2 - firstChild.Area.Top) / this.Scale + this.ChildPadding.Value.Height / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
@ -229,34 +214,65 @@ namespace MLEM.Ui.Elements {
|
||||||
this.SetScrollBarStyle();
|
this.SetScrollBarStyle();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override IList<Element> GetRelevantChildren() {
|
||||||
|
var relevant = base.GetRelevantChildren();
|
||||||
|
if (this.scrollOverflow) {
|
||||||
|
if (this.relevantChildrenDirty)
|
||||||
|
this.ForceUpdateRelevantChildren();
|
||||||
|
relevant = this.relevantChildren;
|
||||||
|
}
|
||||||
|
return relevant;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void OnChildAreaDirty(Element child, bool grandchild) {
|
||||||
|
base.OnChildAreaDirty(child, grandchild);
|
||||||
|
// we only need to scroll when a grandchild changes, since all of our children are forced
|
||||||
|
// to be auto-anchored and so will automatically propagate their changes up to us
|
||||||
|
if (grandchild)
|
||||||
|
this.ScrollChildren();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected internal override void RemovedFromUi() {
|
||||||
|
base.RemovedFromUi();
|
||||||
|
// we dispose our render target when removing so that it doesn't cause a memory leak
|
||||||
|
// if we're added back afterwards, it'll be recreated in ScrollSetup anyway
|
||||||
|
this.renderTarget?.Dispose();
|
||||||
|
this.renderTarget = null;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Prepares the panel for auto-scrolling, creating the render target and setting up the scroll bar's maximum value.
|
/// Prepares the panel for auto-scrolling, creating the render target and setting up the scroll bar's maximum value.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected virtual void ScrollSetup() {
|
protected virtual void ScrollSetup() {
|
||||||
if (!this.scrollOverflow || this.IsHidden)
|
if (!this.scrollOverflow || this.IsHidden)
|
||||||
return;
|
return;
|
||||||
// if there is only one child, then we have just the scroll bar
|
|
||||||
if (this.Children.Count == 1)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// the "real" first child is the scroll bar, which we want to ignore
|
float childrenHeight;
|
||||||
var firstChild = this.Children.First(c => c != this.ScrollBar);
|
if (this.Children.Count > 1) {
|
||||||
var lowestChild = this.GetLowestChild(c => c != this.ScrollBar && !c.IsHidden);
|
var firstChild = this.Children.FirstOrDefault(c => c != this.ScrollBar);
|
||||||
var childrenHeight = lowestChild.Area.Bottom - firstChild.Area.Top;
|
var lowestChild = this.GetLowestChild(c => c != this.ScrollBar && !c.IsHidden);
|
||||||
|
childrenHeight = lowestChild.Area.Bottom - firstChild.Area.Top;
|
||||||
|
} else {
|
||||||
|
// if we only have one child (the scroll bar), then the children take up no visual height
|
||||||
|
childrenHeight = 0;
|
||||||
|
}
|
||||||
|
|
||||||
// the max value of the scrollbar is the amount of non-scaled pixels taken up by overflowing components
|
// the max value of the scroll bar is the amount of non-scaled pixels taken up by overflowing components
|
||||||
var scrollBarMax = (childrenHeight - this.ChildPaddedArea.Height) / this.Scale;
|
var scrollBarMax = (childrenHeight - this.ChildPaddedArea.Height) / this.Scale;
|
||||||
if (!this.ScrollBar.MaxValue.Equals(scrollBarMax, Element.Epsilon)) {
|
if (!this.ScrollBar.MaxValue.Equals(scrollBarMax, Element.Epsilon)) {
|
||||||
this.ScrollBar.MaxValue = scrollBarMax;
|
this.ScrollBar.MaxValue = scrollBarMax;
|
||||||
this.relevantChildrenDirty = true;
|
this.relevantChildrenDirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
// update child padding based on whether the scroll bar is visible
|
// update child padding based on whether the scroll bar is visible
|
||||||
var childOffset = this.ScrollBar.IsHidden ? 0 : this.ScrollerSize.Value.X + this.ScrollBarOffset;
|
var childOffset = this.ScrollBar.IsHidden ? 0 : this.ScrollerSize.Value.X + this.ScrollBarOffset;
|
||||||
if (!this.scrollBarChildOffset.Equals(childOffset, Element.Epsilon)) {
|
if (!this.scrollBarChildOffset.Equals(childOffset, Element.Epsilon)) {
|
||||||
this.ChildPadding += new Padding(0, -this.scrollBarChildOffset + childOffset, 0, 0);
|
this.ChildPadding += new Padding(0, -this.scrollBarChildOffset + childOffset, 0, 0);
|
||||||
this.scrollBarChildOffset = childOffset;
|
this.scrollBarChildOffset = childOffset;
|
||||||
this.SetAreaDirty();
|
this.SetAreaDirty();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// the scroller height has the same relation to the scroll bar height as the visible area has to the total height of the panel's content
|
// the scroller height has the same relation to the scroll bar height as the visible area has to the total height of the panel's content
|
||||||
|
@ -265,25 +281,18 @@ namespace MLEM.Ui.Elements {
|
||||||
|
|
||||||
// update the render target
|
// update the render target
|
||||||
var targetArea = (Rectangle) this.GetRenderTargetArea();
|
var targetArea = (Rectangle) this.GetRenderTargetArea();
|
||||||
if (targetArea.Width <= 0 || targetArea.Height <= 0)
|
if (targetArea.Width <= 0 || targetArea.Height <= 0) {
|
||||||
|
this.renderTarget?.Dispose();
|
||||||
|
this.renderTarget = null;
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
if (this.renderTarget == null || targetArea.Width != this.renderTarget.Width || targetArea.Height != this.renderTarget.Height) {
|
if (this.renderTarget == null || targetArea.Width != this.renderTarget.Width || targetArea.Height != this.renderTarget.Height) {
|
||||||
if (this.renderTarget != null)
|
this.renderTarget?.Dispose();
|
||||||
this.renderTarget.Dispose();
|
|
||||||
this.renderTarget = targetArea.IsEmpty ? null : new RenderTarget2D(this.System.Game.GraphicsDevice, targetArea.Width, targetArea.Height);
|
this.renderTarget = targetArea.IsEmpty ? null : new RenderTarget2D(this.System.Game.GraphicsDevice, targetArea.Width, targetArea.Height);
|
||||||
this.relevantChildrenDirty = true;
|
this.relevantChildrenDirty = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void Dispose() {
|
|
||||||
if (this.renderTarget != null) {
|
|
||||||
this.renderTarget.Dispose();
|
|
||||||
this.renderTarget = null;
|
|
||||||
}
|
|
||||||
base.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SetScrollBarStyle() {
|
private void SetScrollBarStyle() {
|
||||||
if (this.ScrollBar == null)
|
if (this.ScrollBar == null)
|
||||||
return;
|
return;
|
||||||
|
@ -310,5 +319,21 @@ namespace MLEM.Ui.Elements {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private RectangleF GetRenderTargetArea() {
|
||||||
|
var area = this.ChildPaddedArea;
|
||||||
|
area.X = this.DisplayArea.X;
|
||||||
|
area.Width = this.DisplayArea.Width;
|
||||||
|
return area;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ScrollChildren() {
|
||||||
|
if (!this.scrollOverflow)
|
||||||
|
return;
|
||||||
|
// we ignore false grandchildren so that the children of the scroll bar stay in place
|
||||||
|
foreach (var child in this.GetChildren(c => c != this.ScrollBar, true, true))
|
||||||
|
child.ScrollOffset.Y = -this.ScrollBar.CurrentValue;
|
||||||
|
this.relevantChildrenDirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,13 @@ namespace MLEM.Ui.Elements {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The tokenized version of the <see cref="Text"/>
|
/// The tokenized version of the <see cref="Text"/>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public TokenizedString TokenizedText { get; private set; }
|
public TokenizedString TokenizedText {
|
||||||
|
get {
|
||||||
|
this.CheckTextChange();
|
||||||
|
this.TokenizeIfNecessary();
|
||||||
|
return this.tokenizedText;
|
||||||
|
}
|
||||||
|
}
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The color that the text will be rendered with
|
/// The color that the text will be rendered with
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -41,48 +47,78 @@ namespace MLEM.Ui.Elements {
|
||||||
/// 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"/>.
|
/// To add a multiplier rather than changing the scale directly, use <see cref="TextScaleMultiplier"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public StyleProp<float> TextScale;
|
public StyleProp<float> TextScale {
|
||||||
|
get => this.textScale;
|
||||||
|
set {
|
||||||
|
this.textScale = value;
|
||||||
|
this.SetTextDirty();
|
||||||
|
}
|
||||||
|
}
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A multiplier that will be applied to <see cref="TextScale"/>.
|
/// A multiplier that will be applied to <see cref="TextScale"/>.
|
||||||
/// To change the text scale itself, use <see cref="TextScale"/>.
|
/// To change the text scale itself, use <see cref="TextScale"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public float TextScaleMultiplier = 1;
|
public float TextScaleMultiplier {
|
||||||
|
get => this.textScaleMultiplier;
|
||||||
|
set {
|
||||||
|
if (this.textScaleMultiplier != value) {
|
||||||
|
this.textScaleMultiplier = value;
|
||||||
|
this.SetTextDirty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The text to render inside of this paragraph.
|
/// The text to render inside of this paragraph.
|
||||||
/// Use <see cref="GetTextCallback"/> if the text changes frequently.
|
/// Use <see cref="GetTextCallback"/> if the text changes frequently.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Text {
|
public string Text {
|
||||||
get {
|
get {
|
||||||
this.QueryTextCallback();
|
this.CheckTextChange();
|
||||||
return this.text;
|
return this.displayedText;
|
||||||
}
|
}
|
||||||
set {
|
set {
|
||||||
if (this.text != value) {
|
this.explicitlySetText = value;
|
||||||
this.text = value;
|
this.CheckTextChange();
|
||||||
this.SetTextDirty();
|
|
||||||
|
|
||||||
var force = string.IsNullOrWhiteSpace(this.text);
|
|
||||||
if (this.forceHide != force) {
|
|
||||||
this.forceHide = force;
|
|
||||||
this.SetAreaDirty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// If this paragraph should automatically adjust its width based on the width of the text within it
|
/// If this paragraph should automatically adjust its width based on the width of the text within it
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool AutoAdjustWidth;
|
public bool AutoAdjustWidth {
|
||||||
|
get => this.autoAdjustWidth;
|
||||||
|
set {
|
||||||
|
if (this.autoAdjustWidth != value) {
|
||||||
|
this.autoAdjustWidth = value;
|
||||||
|
this.SetAreaDirty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether this paragraph should be truncated instead of split if the displayed <see cref="Text"/>'s width exceeds the provided width.
|
/// 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.
|
/// When the string is truncated, the <see cref="Ellipsis"/> is added to its end.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool TruncateIfLong;
|
public bool TruncateIfLong {
|
||||||
|
get => this.truncateIfLong;
|
||||||
|
set {
|
||||||
|
if (this.truncateIfLong != value) {
|
||||||
|
this.truncateIfLong = value;
|
||||||
|
this.SetAlignSplitDirty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The ellipsis characters to use if <see cref="TruncateIfLong"/> is enabled and the string is truncated.
|
/// 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.
|
/// If this is set to an empty string, no ellipsis will be attached to the truncated string.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Ellipsis = "...";
|
public string Ellipsis {
|
||||||
|
get => this.ellipsis;
|
||||||
|
set {
|
||||||
|
if (this.ellipsis != value) {
|
||||||
|
this.ellipsis = value;
|
||||||
|
this.SetAlignSplitDirty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// An event that gets called when this paragraph's <see cref="Text"/> is queried.
|
/// 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.
|
/// Use this event for setting this paragraph's text if it changes frequently.
|
||||||
|
@ -105,12 +141,20 @@ namespace MLEM.Ui.Elements {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override bool IsHidden => base.IsHidden || this.forceHide;
|
public override bool IsHidden => base.IsHidden || string.IsNullOrWhiteSpace(this.Text);
|
||||||
|
|
||||||
private string text;
|
private string displayedText;
|
||||||
|
private string explicitlySetText;
|
||||||
private StyleProp<TextAlignment> alignment;
|
private StyleProp<TextAlignment> alignment;
|
||||||
private StyleProp<GenericFont> regularFont;
|
private StyleProp<GenericFont> regularFont;
|
||||||
private bool forceHide;
|
private StyleProp<float> textScale;
|
||||||
|
private TokenizedString tokenizedText;
|
||||||
|
private float? lastAlignSplitWidth;
|
||||||
|
private float? lastAlignSplitScale;
|
||||||
|
private string ellipsis = "...";
|
||||||
|
private bool truncateIfLong;
|
||||||
|
private float textScaleMultiplier = 1;
|
||||||
|
private bool autoAdjustWidth;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new paragraph with the given settings.
|
/// Creates a new paragraph with the given settings.
|
||||||
|
@ -134,17 +178,17 @@ namespace MLEM.Ui.Elements {
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override Vector2 CalcActualSize(RectangleF parentArea) {
|
protected override Vector2 CalcActualSize(RectangleF parentArea) {
|
||||||
var size = base.CalcActualSize(parentArea);
|
var size = base.CalcActualSize(parentArea);
|
||||||
this.ParseText(size);
|
this.CheckTextChange();
|
||||||
var textSize = this.TokenizedText.Measure(this.RegularFont) * this.TextScale * this.TextScaleMultiplier * this.Scale;
|
this.TokenizeIfNecessary();
|
||||||
|
this.AlignAndSplitIfNecessary(size);
|
||||||
|
var textSize = this.tokenizedText.GetArea(Vector2.Zero, this.TextScale * this.TextScaleMultiplier * this.Scale).Size;
|
||||||
return new Vector2(this.AutoAdjustWidth ? textSize.X + this.ScaledPadding.Width : size.X, textSize.Y + this.ScaledPadding.Height);
|
return new Vector2(this.AutoAdjustWidth ? textSize.X + this.ScaledPadding.Width : size.X, textSize.Y + this.ScaledPadding.Height);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override void Update(GameTime time) {
|
public override void Update(GameTime time) {
|
||||||
this.QueryTextCallback();
|
this.TokenizedText?.Update(time);
|
||||||
base.Update(time);
|
base.Update(time);
|
||||||
if (this.TokenizedText != null)
|
|
||||||
this.TokenizedText.Update(time);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
@ -165,45 +209,22 @@ namespace MLEM.Ui.Elements {
|
||||||
this.Alignment = this.Alignment.OrStyle(style.TextAlignment);
|
this.Alignment = this.Alignment.OrStyle(style.TextAlignment);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
private void SetTextDirty() {
|
||||||
/// Parses this paragraph's <see cref="Text"/> into <see cref="TokenizedText"/>.
|
this.tokenizedText = null;
|
||||||
/// Additionally, this method adds any <see cref="Link"/> elements for tokenized links in the text.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="size">The paragraph's default size</param>
|
|
||||||
protected virtual void ParseText(Vector2 size) {
|
|
||||||
if (this.TokenizedText == null) {
|
|
||||||
// tokenize the 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.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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A helper method that causes the <see cref="TokenizedText"/> to be reset.
|
|
||||||
/// Additionally, <see cref="Element.SetAreaDirty"/> if this paragraph's area has changed enough to warrant it, or if it has any <see cref="Link"/> children.
|
|
||||||
/// </summary>
|
|
||||||
protected void SetTextDirty() {
|
|
||||||
this.TokenizedText = null;
|
|
||||||
// only set our area dirty if our size changed as a result of this action
|
// only set our area dirty if our size changed as a result of this action
|
||||||
if (!this.AreaDirty && !this.CalcActualSize(this.ParentArea).Equals(this.DisplayArea.Size, Element.Epsilon))
|
if (!this.AreaDirty && !this.CalcActualSize(this.ParentArea).Equals(this.DisplayArea.Size, Element.Epsilon))
|
||||||
this.SetAreaDirty();
|
this.SetAreaDirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void QueryTextCallback() {
|
private void CheckTextChange() {
|
||||||
if (this.GetTextCallback != null)
|
var newText = this.GetTextCallback?.Invoke(this) ?? this.explicitlySetText;
|
||||||
this.Text = this.GetTextCallback(this);
|
if (this.displayedText == newText)
|
||||||
|
return;
|
||||||
|
var emptyChanged = string.IsNullOrWhiteSpace(this.displayedText) != string.IsNullOrWhiteSpace(newText);
|
||||||
|
this.displayedText = newText;
|
||||||
|
if (emptyChanged)
|
||||||
|
this.SetAreaDirty();
|
||||||
|
this.SetTextDirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
private float GetAlignmentOffset() {
|
private float GetAlignmentOffset() {
|
||||||
|
@ -216,6 +237,41 @@ namespace MLEM.Ui.Elements {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void TokenizeIfNecessary() {
|
||||||
|
if (this.tokenizedText != null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// tokenize the text
|
||||||
|
this.tokenizedText = this.System.TextFormatter.Tokenize(this.RegularFont, this.Text, this.Alignment);
|
||||||
|
this.SetAlignSplitDirty();
|
||||||
|
|
||||||
|
// 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.TextScaleMultiplier));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AlignAndSplitIfNecessary(Vector2 size) {
|
||||||
|
var width = size.X - this.ScaledPadding.Width;
|
||||||
|
var scale = this.TextScale * this.TextScaleMultiplier * this.Scale;
|
||||||
|
|
||||||
|
if (this.lastAlignSplitWidth == width && this.lastAlignSplitScale == scale)
|
||||||
|
return;
|
||||||
|
this.lastAlignSplitWidth = width;
|
||||||
|
this.lastAlignSplitScale = 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetAlignSplitDirty() {
|
||||||
|
this.lastAlignSplitWidth = null;
|
||||||
|
this.lastAlignSplitScale = null;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A delegate method used for <see cref="Paragraph.GetTextCallback"/>
|
/// A delegate method used for <see cref="Paragraph.GetTextCallback"/>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
@ -3,12 +3,14 @@ using System.Linq;
|
||||||
using Microsoft.Xna.Framework;
|
using Microsoft.Xna.Framework;
|
||||||
using Microsoft.Xna.Framework.Graphics;
|
using Microsoft.Xna.Framework.Graphics;
|
||||||
using Microsoft.Xna.Framework.Input.Touch;
|
using Microsoft.Xna.Framework.Input.Touch;
|
||||||
using MLEM.Extensions;
|
|
||||||
using MLEM.Graphics;
|
using MLEM.Graphics;
|
||||||
using MLEM.Input;
|
using MLEM.Input;
|
||||||
using MLEM.Misc;
|
using MLEM.Misc;
|
||||||
using MLEM.Textures;
|
using MLEM.Textures;
|
||||||
using MLEM.Ui.Style;
|
using MLEM.Ui.Style;
|
||||||
|
#if FNA
|
||||||
|
using MLEM.Extensions;
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace MLEM.Ui.Elements {
|
namespace MLEM.Ui.Elements {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -31,6 +33,16 @@ namespace MLEM.Ui.Elements {
|
||||||
/// The texture of this scroll bar's scroller indicator
|
/// The texture of this scroll bar's scroller indicator
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public StyleProp<NinePatch> ScrollerTexture;
|
public StyleProp<NinePatch> ScrollerTexture;
|
||||||
|
/// <summary>
|
||||||
|
/// Whether smooth scrolling should be enabled for this scroll bar.
|
||||||
|
/// Smooth scrolling causes the <see cref="CurrentValue"/> to change gradually rather than instantly when scrolling.
|
||||||
|
/// </summary>
|
||||||
|
public StyleProp<bool> SmoothScrolling;
|
||||||
|
/// <summary>
|
||||||
|
/// The factor with which <see cref="SmoothScrolling"/> happens.
|
||||||
|
/// </summary>
|
||||||
|
public StyleProp<float> SmoothScrollFactor;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The scroller's width and height
|
/// The scroller's width and height
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -84,7 +96,7 @@ namespace MLEM.Ui.Elements {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// This property is true while the user scrolls on the scroll bar using the mouse or touch input
|
/// This property is true while the user scrolls on the scroll bar using the mouse or touch input
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsBeingScrolled => this.isMouseHeld || this.isDragging || this.isTouchHeld;
|
public bool IsBeingScrolled => this.isMouseScrolling || this.isMouseDragging || this.isTouchDragging || this.isTouchScrolling;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// This field determines if this scroll bar should automatically be hidden from a <see cref="Panel"/> if there aren't enough children to allow for scrolling.
|
/// This field determines if this scroll bar should automatically be hidden from a <see cref="Panel"/> if there aren't enough children to allow for scrolling.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -97,18 +109,14 @@ namespace MLEM.Ui.Elements {
|
||||||
!this.Horizontal ? 0 : this.CurrentValue / this.maxValue * (this.DisplayArea.Width - this.ScrollerSize.X * this.Scale),
|
!this.Horizontal ? 0 : this.CurrentValue / this.maxValue * (this.DisplayArea.Width - this.ScrollerSize.X * this.Scale),
|
||||||
this.Horizontal ? 0 : this.CurrentValue / this.maxValue * (this.DisplayArea.Height - this.ScrollerSize.Y * this.Scale));
|
this.Horizontal ? 0 : this.CurrentValue / this.maxValue * (this.DisplayArea.Height - this.ScrollerSize.Y * this.Scale));
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether smooth scrolling should be enabled for this scroll bar.
|
/// Whether this scroll bar should allow dragging the mouse over its attached <see cref="Panel"/>'s content while holding the left mouse button to scroll, similarly to how scrolling using touch input works.
|
||||||
/// Smooth scrolling causes the <see cref="CurrentValue"/> to change gradually rather than instantly when scrolling.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public StyleProp<bool> SmoothScrolling;
|
public bool MouseDragScrolling;
|
||||||
/// <summary>
|
|
||||||
/// The factor with which <see cref="SmoothScrolling"/> happens.
|
|
||||||
/// </summary>
|
|
||||||
public StyleProp<float> SmoothScrollFactor;
|
|
||||||
|
|
||||||
private bool isMouseHeld;
|
private bool isMouseScrolling;
|
||||||
private bool isDragging;
|
private bool isMouseDragging;
|
||||||
private bool isTouchHeld;
|
private bool isTouchScrolling;
|
||||||
|
private bool isTouchDragging;
|
||||||
private float maxValue;
|
private float maxValue;
|
||||||
private float scrollAdded;
|
private float scrollAdded;
|
||||||
private float currValue;
|
private float currValue;
|
||||||
|
@ -139,18 +147,29 @@ namespace MLEM.Ui.Elements {
|
||||||
|
|
||||||
// MOUSE INPUT
|
// MOUSE INPUT
|
||||||
var moused = this.Controls.MousedElement;
|
var moused = this.Controls.MousedElement;
|
||||||
if (moused == this && this.Input.WasMouseButtonUp(MouseButton.Left) && this.Input.IsMouseButtonDown(MouseButton.Left)) {
|
var wasMouseUp = this.Input.WasUp(MouseButton.Left);
|
||||||
this.isMouseHeld = true;
|
var isMouseDown = this.Input.IsDown(MouseButton.Left);
|
||||||
|
if (moused == this && wasMouseUp && isMouseDown) {
|
||||||
|
this.isMouseScrolling = true;
|
||||||
this.scrollStartOffset = this.TransformInverseAll(this.Input.ViewportMousePosition.ToVector2()) - this.ScrollerPosition;
|
this.scrollStartOffset = this.TransformInverseAll(this.Input.ViewportMousePosition.ToVector2()) - this.ScrollerPosition;
|
||||||
} else if (this.isMouseHeld && !this.Input.IsMouseButtonDown(MouseButton.Left)) {
|
} else if (!isMouseDown) {
|
||||||
this.isMouseHeld = false;
|
this.isMouseScrolling = false;
|
||||||
}
|
}
|
||||||
if (this.isMouseHeld)
|
if (this.isMouseScrolling)
|
||||||
this.ScrollToPos(this.TransformInverseAll(this.Input.ViewportMousePosition.ToVector2()));
|
this.ScrollToPos(this.TransformInverseAll(this.Input.ViewportMousePosition.ToVector2()));
|
||||||
if (!this.Horizontal && moused != null && (moused == this.Parent || moused.GetParentTree().Contains(this.Parent))) {
|
if (!this.Horizontal) {
|
||||||
var scroll = this.Input.LastScrollWheel - this.Input.ScrollWheel;
|
if (moused != null && (moused == this.Parent || moused.GetParentTree().Contains(this.Parent))) {
|
||||||
if (scroll != 0)
|
var scroll = this.Input.LastScrollWheel - this.Input.ScrollWheel;
|
||||||
this.CurrentValue += this.StepPerScroll * Math.Sign(scroll);
|
if (scroll != 0)
|
||||||
|
this.CurrentValue += this.StepPerScroll * Math.Sign(scroll);
|
||||||
|
|
||||||
|
if (this.MouseDragScrolling && moused != this && wasMouseUp && isMouseDown)
|
||||||
|
this.isMouseDragging = true;
|
||||||
|
}
|
||||||
|
if (!isMouseDown)
|
||||||
|
this.isMouseDragging = false;
|
||||||
|
if (this.isMouseDragging)
|
||||||
|
this.CurrentValue -= (this.Input.MousePosition.Y - this.Input.LastMousePosition.Y) / this.Scale;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TOUCH INPUT
|
// TOUCH INPUT
|
||||||
|
@ -160,29 +179,29 @@ namespace MLEM.Ui.Elements {
|
||||||
// if the element under the drag's start position is on top of the panel, start dragging
|
// if the element under the drag's start position is on top of the panel, start dragging
|
||||||
var touched = this.Parent.GetElementUnderPos(this.TransformInverseAll(drag.Position));
|
var touched = this.Parent.GetElementUnderPos(this.TransformInverseAll(drag.Position));
|
||||||
if (touched != null && touched != this)
|
if (touched != null && touched != this)
|
||||||
this.isDragging = true;
|
this.isTouchDragging = true;
|
||||||
|
|
||||||
// if we're dragging at all, then move the scroller
|
// if we're dragging at all, then move the scroller
|
||||||
if (this.isDragging)
|
if (this.isTouchDragging)
|
||||||
this.CurrentValue -= drag.Delta.Y / this.Scale;
|
this.CurrentValue -= drag.Delta.Y / this.Scale;
|
||||||
} else {
|
} else {
|
||||||
this.isDragging = false;
|
this.isTouchDragging = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this.Input.ViewportTouchState.Count <= 0) {
|
if (this.Input.ViewportTouchState.Count <= 0) {
|
||||||
// if no touch has occured this tick, then reset the variable
|
// if no touch has occured this tick, then reset the variable
|
||||||
this.isTouchHeld = false;
|
this.isTouchScrolling = false;
|
||||||
} else {
|
} else {
|
||||||
foreach (var loc in this.Input.ViewportTouchState) {
|
foreach (var loc in this.Input.ViewportTouchState) {
|
||||||
var pos = this.TransformInverseAll(loc.Position);
|
var pos = this.TransformInverseAll(loc.Position);
|
||||||
// if we just started touching and are on top of the scroller, then we should start scrolling
|
// if we just started touching and are on top of the scroller, then we should start scrolling
|
||||||
if (this.DisplayArea.Contains(pos) && !loc.TryGetPreviousLocation(out _)) {
|
if (this.DisplayArea.Contains(pos) && !loc.TryGetPreviousLocation(out _)) {
|
||||||
this.isTouchHeld = true;
|
this.isTouchScrolling = true;
|
||||||
this.scrollStartOffset = pos - this.ScrollerPosition;
|
this.scrollStartOffset = pos - this.ScrollerPosition;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// scroll no matter if we're on the scroller right now
|
// scroll no matter if we're on the scroller right now
|
||||||
if (this.isTouchHeld)
|
if (this.isTouchScrolling)
|
||||||
this.ScrollToPos(pos);
|
this.ScrollToPos(pos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,9 @@ using MLEM.Input;
|
||||||
using MLEM.Misc;
|
using MLEM.Misc;
|
||||||
using MLEM.Textures;
|
using MLEM.Textures;
|
||||||
using MLEM.Ui.Style;
|
using MLEM.Ui.Style;
|
||||||
|
#if NETSTANDARD2_0_OR_GREATER || NET6_0_OR_GREATER
|
||||||
using TextCopy;
|
using TextCopy;
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace MLEM.Ui.Elements {
|
namespace MLEM.Ui.Elements {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -31,6 +33,14 @@ namespace MLEM.Ui.Elements {
|
||||||
/// <inheritdoc cref="TextInput.FileNames"/>
|
/// <inheritdoc cref="TextInput.FileNames"/>
|
||||||
public static readonly Rule FileNames = (field, add) => TextInput.FileNames(field.textInput, add);
|
public static readonly Rule FileNames = (field, add) => TextInput.FileNames(field.textInput, add);
|
||||||
|
|
||||||
|
#if NETSTANDARD2_0_OR_GREATER || NET6_0_OR_GREATER
|
||||||
|
/// <summary>
|
||||||
|
/// An event that is raised when an exception is thrown while trying to copy or paste clipboard contents using TextCopy.
|
||||||
|
/// If no event handlers are added, the exception is ignored.
|
||||||
|
/// </summary>
|
||||||
|
public static event Action<Exception> OnCopyPasteException;
|
||||||
|
#endif
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The color that this text field's text should display with
|
/// The color that this text field's text should display with
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -111,11 +121,11 @@ namespace MLEM.Ui.Elements {
|
||||||
set => this.textInput.Multiline = value;
|
set => this.textInput.Multiline = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
#if FNA
|
#if FNA
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
// we need to make sure that the enter press doesn't get consumed by our press function so that it still works in TextInput
|
// we need to make sure that the enter press doesn't get consumed by our press function so that it still works in TextInput
|
||||||
public override bool CanBePressed => base.CanBePressed && !this.IsSelected;
|
public override bool CanBePressed => base.CanBePressed && !this.IsSelected;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The text that displays in this text field if <see cref="Text"/> is empty
|
/// The text that displays in this text field if <see cref="Text"/> is empty
|
||||||
|
@ -144,7 +154,24 @@ namespace MLEM.Ui.Elements {
|
||||||
/// <param name="text">The text that the text field should contain by default</param>
|
/// <param name="text">The text that the text field should contain by default</param>
|
||||||
/// <param name="multiline">Whether the text field should support multi-line editing</param>
|
/// <param name="multiline">Whether the text field should support multi-line editing</param>
|
||||||
public TextField(Anchor anchor, Vector2 size, Rule rule = null, GenericFont font = null, string text = null, bool multiline = false) : base(anchor, size) {
|
public TextField(Anchor anchor, Vector2 size, Rule rule = null, GenericFont font = null, string text = null, bool multiline = false) : base(anchor, size) {
|
||||||
this.textInput = new TextInput(null, Vector2.Zero, 1, null, ClipboardService.SetText, ClipboardService.GetText) {
|
this.textInput = new TextInput(null, Vector2.Zero, 1
|
||||||
|
#if NETSTANDARD2_0_OR_GREATER || NET6_0_OR_GREATER
|
||||||
|
, null, s => {
|
||||||
|
try {
|
||||||
|
ClipboardService.SetText(s);
|
||||||
|
} catch (Exception e) {
|
||||||
|
TextField.OnCopyPasteException?.Invoke(e);
|
||||||
|
}
|
||||||
|
}, () => {
|
||||||
|
try {
|
||||||
|
return ClipboardService.GetText();
|
||||||
|
} catch (Exception e) {
|
||||||
|
TextField.OnCopyPasteException?.Invoke(e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
) {
|
||||||
OnTextChange = (i, s) => this.OnTextChange?.Invoke(this, s),
|
OnTextChange = (i, s) => this.OnTextChange?.Invoke(this, s),
|
||||||
InputRule = (i, s) => this.InputRule.Invoke(this, s)
|
InputRule = (i, s) => this.InputRule.Invoke(this, s)
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Microsoft.Xna.Framework;
|
using Microsoft.Xna.Framework;
|
||||||
using MLEM.Extensions;
|
|
||||||
using MLEM.Input;
|
using MLEM.Input;
|
||||||
using MLEM.Ui.Style;
|
using MLEM.Ui.Style;
|
||||||
|
#if FNA
|
||||||
|
using MLEM.Extensions;
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace MLEM.Ui.Elements {
|
namespace MLEM.Ui.Elements {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -98,9 +100,9 @@ namespace MLEM.Ui.Elements {
|
||||||
public Tooltip(string text = null, Element elementToHover = null) :
|
public Tooltip(string text = null, Element elementToHover = null) :
|
||||||
base(Anchor.TopLeft, Vector2.One, Vector2.Zero) {
|
base(Anchor.TopLeft, Vector2.One, Vector2.Zero) {
|
||||||
if (text != null) {
|
if (text != null) {
|
||||||
#pragma warning disable CS0618
|
#pragma warning disable CS0618
|
||||||
this.Paragraph = this.AddParagraph(text);
|
this.Paragraph = this.AddParagraph(text);
|
||||||
#pragma warning restore CS0618
|
#pragma warning restore CS0618
|
||||||
}
|
}
|
||||||
this.Init(elementToHover);
|
this.Init(elementToHover);
|
||||||
}
|
}
|
||||||
|
@ -112,9 +114,9 @@ namespace MLEM.Ui.Elements {
|
||||||
/// <param name="elementToHover">The element that should automatically cause the tooltip to appear and disappear when hovered and not hovered, respectively</param>
|
/// <param name="elementToHover">The element that should automatically cause the tooltip to appear and disappear when hovered and not hovered, respectively</param>
|
||||||
public Tooltip(Paragraph.TextCallback textCallback, Element elementToHover = null) :
|
public Tooltip(Paragraph.TextCallback textCallback, Element elementToHover = null) :
|
||||||
base(Anchor.TopLeft, Vector2.One, Vector2.Zero) {
|
base(Anchor.TopLeft, Vector2.One, Vector2.Zero) {
|
||||||
#pragma warning disable CS0618
|
#pragma warning disable CS0618
|
||||||
this.Paragraph = this.AddParagraph(textCallback);
|
this.Paragraph = this.AddParagraph(textCallback);
|
||||||
#pragma warning restore CS0618
|
#pragma warning restore CS0618
|
||||||
this.Init(elementToHover);
|
this.Init(elementToHover);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -296,11 +298,11 @@ namespace MLEM.Ui.Elements {
|
||||||
foreach (var paragraph in this.Paragraphs)
|
foreach (var paragraph in this.Paragraphs)
|
||||||
this.UpdateParagraphStyle(paragraph);
|
this.UpdateParagraphStyle(paragraph);
|
||||||
|
|
||||||
#pragma warning disable CS0618
|
#pragma warning disable CS0618
|
||||||
// still set style here in case someone changed the paragraph field manually
|
// still set style here in case someone changed the paragraph field manually
|
||||||
if (this.Paragraph != null)
|
if (this.Paragraph != null)
|
||||||
this.UpdateParagraphStyle(this.Paragraph);
|
this.UpdateParagraphStyle(this.Paragraph);
|
||||||
#pragma warning restore CS0618
|
#pragma warning restore CS0618
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateParagraphStyle(Paragraph paragraph) {
|
private void UpdateParagraphStyle(Paragraph paragraph) {
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netstandard2.0</TargetFramework>
|
<TargetFrameworks>net452;netstandard2.0;net6.0</TargetFrameworks>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<ProduceReferenceAssembly>true</ProduceReferenceAssembly>
|
<ProduceReferenceAssembly>true</ProduceReferenceAssembly>
|
||||||
|
<IsTrimmable>true</IsTrimmable>
|
||||||
<RootNamespace>MLEM.Ui</RootNamespace>
|
<RootNamespace>MLEM.Ui</RootNamespace>
|
||||||
<DefineConstants>$(DefineConstants);FNA</DefineConstants>
|
<DefineConstants>$(DefineConstants);FNA</DefineConstants>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
@ -20,10 +21,10 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="TextCopy" Version="6.1.0" />
|
<PackageReference Include="TextCopy" Version="6.2.0" Condition="'$(TargetFramework)'!='net452'" />
|
||||||
<ProjectReference Include="..\MLEM\MLEM.FNA.csproj" />
|
<ProjectReference Include="..\MLEM\MLEM.FNA.csproj" />
|
||||||
|
|
||||||
<ProjectReference Include="..\FNA\FNA.Core.csproj">
|
<ProjectReference Include="..\FNA\FNA.csproj">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</ProjectReference>
|
</ProjectReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netstandard2.0</TargetFramework>
|
<TargetFrameworks>net452;netstandard2.0;net6.0</TargetFrameworks>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<ProduceReferenceAssembly>true</ProduceReferenceAssembly>
|
<ProduceReferenceAssembly>true</ProduceReferenceAssembly>
|
||||||
|
<IsTrimmable>true</IsTrimmable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
|
@ -18,7 +19,7 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="TextCopy" Version="6.1.0" />
|
<PackageReference Include="TextCopy" Version="6.2.0" Condition="'$(TargetFramework)'!='net452'" />
|
||||||
<ProjectReference Include="..\MLEM\MLEM.csproj" />
|
<ProjectReference Include="..\MLEM\MLEM.csproj" />
|
||||||
|
|
||||||
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.0.1641">
|
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.0.1641">
|
||||||
|
|
|
@ -1,21 +1,12 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Net.Http;
|
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using Microsoft.Xna.Framework;
|
|
||||||
using Microsoft.Xna.Framework.Graphics;
|
|
||||||
using MLEM.Formatting;
|
|
||||||
using MLEM.Misc;
|
|
||||||
using MLEM.Textures;
|
|
||||||
using MLEM.Ui.Elements;
|
using MLEM.Ui.Elements;
|
||||||
using MLEM.Ui.Style;
|
|
||||||
|
|
||||||
namespace MLEM.Ui.Parsers {
|
namespace MLEM.Ui.Parsers {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A class for parsing Markdown strings into a set of MLEM.Ui elements with styling for each individual <see cref="ElementType"/>.
|
/// A class for parsing Markdown strings into a set of MLEM.Ui elements with styling for each individual <see cref="UiParser.ElementType"/>.
|
||||||
/// To parse, use <see cref="Parse"/> or <see cref="ParseInto"/>. To style the parsed output, use <see cref="Style{T}"/> before parsing.
|
/// To parse, use <see cref="UiParser.Parse"/> or <see cref="UiParser.ParseInto"/>. To style the parsed output, use <see cref="UiParser.Style{T}"/> before parsing.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Note that this parser is rather rudimentary and doesn't deal well with very complex Markdown documents. Missing features are as follows:
|
/// Note that this parser is rather rudimentary and doesn't deal well with very complex Markdown documents. Missing features are as follows:
|
||||||
|
@ -26,106 +17,18 @@ namespace MLEM.Ui.Parsers {
|
||||||
/// <item><description>Tables</description></item>
|
/// <item><description>Tables</description></item>
|
||||||
/// </list>
|
/// </list>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public class UiMarkdownParser {
|
public class UiMarkdownParser : UiParser {
|
||||||
|
|
||||||
private static readonly ElementType[] ElementTypes = EnumHelper.GetValues<ElementType>().ToArray();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The base path for markdown images, which is prepended to the image link.
|
|
||||||
/// </summary>
|
|
||||||
public string ImageBasePath;
|
|
||||||
/// <summary>
|
|
||||||
/// An action that is invoked when an image fails to load while parsing.
|
|
||||||
/// This action receives the expected location of the image, as well as the <see cref="Exception"/> that occured.
|
|
||||||
/// </summary>
|
|
||||||
public Action<string, Exception> ImageExceptionHandler;
|
|
||||||
/// <summary>
|
|
||||||
/// The graphics device that should be used when loading images and other graphics-dependent content.
|
|
||||||
/// </summary>
|
|
||||||
public GraphicsDevice GraphicsDevice;
|
|
||||||
/// <summary>
|
|
||||||
/// The name of the font used for inline code as well as code blocks.
|
|
||||||
/// This only has an effect if a font with this name is added to the used <see cref="UiStyle"/>'s <see cref="UiStyle.AdditionalFonts"/>.
|
|
||||||
/// This defaults to "Monospaced" if default styling is applied in <see cref="UiMarkdownParser(bool)"/>.
|
|
||||||
/// </summary>
|
|
||||||
public string CodeFont;
|
|
||||||
|
|
||||||
private readonly Dictionary<ElementType, Action<Element>> elementStyles = new Dictionary<ElementType, Action<Element>>();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new UI markdown parser and optionally initializes some default style settings.
|
/// Creates a new UI markdown parser and optionally initializes some default style settings.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="applyDefaultStyling">Whether default style settings should be applied.</param>
|
/// <param name="applyDefaultStyling">Whether default style settings should be applied.</param>
|
||||||
public UiMarkdownParser(bool applyDefaultStyling = true) {
|
public UiMarkdownParser(bool applyDefaultStyling = true) : base(applyDefaultStyling) {}
|
||||||
if (applyDefaultStyling) {
|
|
||||||
this.CodeFont = "Monospaced";
|
|
||||||
this.Style<VerticalSpace>(ElementType.VerticalSpace, v => v.Size = new Vector2(1, 5));
|
|
||||||
for (var i = 0; i < 6; i++) {
|
|
||||||
var level = i;
|
|
||||||
this.Style<Paragraph>(UiMarkdownParser.ElementTypes[Array.IndexOf(UiMarkdownParser.ElementTypes, ElementType.Header1) + i], p => {
|
|
||||||
p.Alignment = TextAlignment.Center;
|
|
||||||
p.TextScaleMultiplier = 2 - level * 0.15F;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <inheritdoc />
|
||||||
/// Parses the given markdown string into a set of elements (using <see cref="Parse"/>) and adds them as children to the givem <paramref name="element"/>.
|
protected override IEnumerable<(ElementType, Element)> ParseUnstyled(string raw) {
|
||||||
/// During this process, the element stylings specified using <see cref="Style{T}"/> are also applied.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="markdown">The markdown to parse.</param>
|
|
||||||
/// <param name="element">The element to add the parsed elements to.</param>
|
|
||||||
/// <returns>The <paramref name="element"/>, for chaining.</returns>
|
|
||||||
public Element ParseInto(string markdown, Element element) {
|
|
||||||
foreach (var (_, e) in this.Parse(markdown))
|
|
||||||
element.AddChild(e);
|
|
||||||
return element;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Parses the given markdown string into a set of elements and returns them along with their <see cref="ElementType"/>.
|
|
||||||
/// During this process, the element stylings specified using <see cref="Style{T}"/> are also applied.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="markdown">The markdown to parse.</param>
|
|
||||||
/// <returns>The parsed elements.</returns>
|
|
||||||
public IEnumerable<(ElementType, Element)> Parse(string markdown) {
|
|
||||||
foreach (var (t, e) in this.ParseUnstyled(markdown)) {
|
|
||||||
if (this.elementStyles.TryGetValue(t, out var style))
|
|
||||||
style.Invoke(e);
|
|
||||||
yield return (t, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Specifies an action to be invoked when a new element with the given <see cref="ElementType"/> is parsed in <see cref="Parse"/> or <see cref="ParseInto"/>.
|
|
||||||
/// These actions can be used to modify the style properties of the created elements.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="types">The element types that should be styled. Can be a combined flag.</param>
|
|
||||||
/// <param name="style">The action that styles the elements with the given element type.</param>
|
|
||||||
/// <param name="add">Whether the <paramref name="style"/> function should be added to the existing style settings, or replace them.</param>
|
|
||||||
/// <typeparam name="T">The type of elements that the given <see cref="ElementType"/> flags are expected to be.</typeparam>
|
|
||||||
/// <returns>This parser, for chaining.</returns>
|
|
||||||
public UiMarkdownParser Style<T>(ElementType types, Action<T> style, bool add = false) where T : Element {
|
|
||||||
foreach (var type in UiMarkdownParser.ElementTypes) {
|
|
||||||
if (types.HasFlag(type)) {
|
|
||||||
if (add && this.elementStyles.ContainsKey(type)) {
|
|
||||||
this.elementStyles[type] += Action;
|
|
||||||
} else {
|
|
||||||
this.elementStyles[type] = Action;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
|
|
||||||
void Action(Element e) {
|
|
||||||
style.Invoke(e as T ?? throw new ArgumentException($"Expected {typeof(T)} for style action but got {e.GetType()}"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerable<(ElementType, Element)> ParseUnstyled(string markdown) {
|
|
||||||
var inCodeBlock = false;
|
var inCodeBlock = false;
|
||||||
foreach (var line in markdown.Split('\n')) {
|
foreach (var line in raw.Split('\n')) {
|
||||||
// code blocks
|
// code blocks
|
||||||
if (line.Trim().StartsWith("```")) {
|
if (line.Trim().StartsWith("```")) {
|
||||||
inCodeBlock = !inCodeBlock;
|
inCodeBlock = !inCodeBlock;
|
||||||
|
@ -139,7 +42,7 @@ namespace MLEM.Ui.Parsers {
|
||||||
|
|
||||||
// quotes
|
// quotes
|
||||||
if (line.StartsWith(">")) {
|
if (line.StartsWith(">")) {
|
||||||
yield return (ElementType.Blockquote, new Paragraph(Anchor.AutoLeft, 1, line.Substring(1).Trim()));
|
yield return (ElementType.Blockquote, new Paragraph(Anchor.AutoLeft, 1, this.ParseParagraph(line.Substring(1).Trim())));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -152,45 +55,7 @@ namespace MLEM.Ui.Parsers {
|
||||||
// images
|
// images
|
||||||
var imageMatch = Regex.Match(line, @"!\[\]\(([^)]+)\)");
|
var imageMatch = Regex.Match(line, @"!\[\]\(([^)]+)\)");
|
||||||
if (imageMatch.Success) {
|
if (imageMatch.Success) {
|
||||||
if (this.GraphicsDevice == null)
|
yield return (ElementType.Image, this.ParseImage(imageMatch.Groups[1].Value));
|
||||||
throw new NullReferenceException("A markdown parser requires a GraphicsDevice for parsing images");
|
|
||||||
|
|
||||||
TextureRegion image = null;
|
|
||||||
LoadImageAsync();
|
|
||||||
yield return (ElementType.Image, new Image(Anchor.AutoLeft, new Vector2(1, -1), _ => image) {
|
|
||||||
OnDisposed = e => image?.Texture.Dispose()
|
|
||||||
});
|
|
||||||
|
|
||||||
async void LoadImageAsync() {
|
|
||||||
var loc = imageMatch.Groups[1].Value;
|
|
||||||
// only apply the base path for relative files
|
|
||||||
if (this.ImageBasePath != null && !loc.StartsWith("http") && !Path.IsPathRooted(loc))
|
|
||||||
loc = $"{this.ImageBasePath}/{loc}";
|
|
||||||
try {
|
|
||||||
Texture2D tex;
|
|
||||||
if (loc.StartsWith("http")) {
|
|
||||||
using (var client = new HttpClient()) {
|
|
||||||
using (var src = await client.GetStreamAsync(loc)) {
|
|
||||||
using (var memory = new MemoryStream()) {
|
|
||||||
// download the full stream before passing it to texture
|
|
||||||
await src.CopyToAsync(memory);
|
|
||||||
tex = Texture2D.FromStream(this.GraphicsDevice, memory);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
using (var stream = Path.IsPathRooted(loc) ? File.OpenRead(loc) : TitleContainer.OpenStream(loc))
|
|
||||||
tex = Texture2D.FromStream(this.GraphicsDevice, stream);
|
|
||||||
}
|
|
||||||
image = new TextureRegion(tex);
|
|
||||||
} catch (Exception e) {
|
|
||||||
if (this.ImageExceptionHandler != null) {
|
|
||||||
this.ImageExceptionHandler.Invoke(loc, e);
|
|
||||||
} else {
|
|
||||||
throw new Exception($"Couldn't parse image {loc}, and no ImageExceptionHandler was set", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -198,8 +63,8 @@ namespace MLEM.Ui.Parsers {
|
||||||
var parsedHeader = false;
|
var parsedHeader = false;
|
||||||
for (var h = 6; h >= 1; h--) {
|
for (var h = 6; h >= 1; h--) {
|
||||||
if (line.StartsWith(new string('#', h))) {
|
if (line.StartsWith(new string('#', h))) {
|
||||||
var type = UiMarkdownParser.ElementTypes[Array.IndexOf(UiMarkdownParser.ElementTypes, ElementType.Header1) + h - 1];
|
var type = UiParser.ElementTypes[Array.IndexOf(UiParser.ElementTypes, ElementType.Header1) + h - 1];
|
||||||
yield return (type, new Paragraph(Anchor.AutoLeft, 1, line.Substring(h).Trim()));
|
yield return (type, new Paragraph(Anchor.AutoLeft, 1, this.ParseParagraph(line.Substring(h).Trim())));
|
||||||
parsedHeader = true;
|
parsedHeader = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -208,90 +73,23 @@ namespace MLEM.Ui.Parsers {
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
// parse everything else as a paragraph (with formatting)
|
// parse everything else as a paragraph (with formatting)
|
||||||
var par = line;
|
yield return (ElementType.Paragraph, new Paragraph(Anchor.AutoLeft, 1, this.ParseParagraph(line)));
|
||||||
// replace links
|
|
||||||
par = Regex.Replace(par, @"<([^>]+)>", "<l $1>$1</l>");
|
|
||||||
par = Regex.Replace(par, @"\[([^\]]+)\]\(([^)]+)\)", "<l $2>$1</l>");
|
|
||||||
// replace formatting
|
|
||||||
par = Regex.Replace(par, @"\*\*([^\*]+)\*\*", "<b>$1</b>");
|
|
||||||
par = Regex.Replace(par, @"__([^_]+)__", "<b>$1</b>");
|
|
||||||
par = Regex.Replace(par, @"\*([^\*]+)\*", "<i>$1</i>");
|
|
||||||
par = Regex.Replace(par, @"_([^_]+)_", "<i>$1</i>");
|
|
||||||
par = Regex.Replace(par, @"~~([^~]+)~~", "<st>$1</st>");
|
|
||||||
// replace inline code with custom code font
|
|
||||||
par = Regex.Replace(par, @"`([^`]+)`", $"<f {this.CodeFont}>$1</f>");
|
|
||||||
yield return (ElementType.Paragraph, new Paragraph(Anchor.AutoLeft, 1, par));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
private string ParseParagraph(string par) {
|
||||||
/// A flags enumeration used by <see cref="UiMarkdownParser"/> that contains the types of elements that can be parsed and returned in <see cref="Parse"/> or <see cref="UiMarkdownParser.ParseInto"/>.
|
// replace links
|
||||||
/// This is a flags enumeration so that <see cref="UiMarkdownParser.Style{T}"/> can have multiple element types being styled at the same time.
|
par = Regex.Replace(par, @"<([^>]+)>", "<l $1>$1</l>");
|
||||||
/// </summary>
|
par = Regex.Replace(par, @"\[([^\]]+)\]\(([^)]+)\)", "<l $2>$1</l>");
|
||||||
[Flags]
|
// replace formatting
|
||||||
public enum ElementType {
|
par = Regex.Replace(par, @"\*\*([^\*]+)\*\*", "<b>$1</b>");
|
||||||
|
par = Regex.Replace(par, @"__([^_]+)__", "<b>$1</b>");
|
||||||
/// <summary>
|
par = Regex.Replace(par, @"\*([^\*]+)\*", "<i>$1</i>");
|
||||||
/// A blockquote.
|
par = Regex.Replace(par, @"_([^_]+)_", "<i>$1</i>");
|
||||||
/// This element type is a <see cref="Paragraph"/>.
|
par = Regex.Replace(par, @"~~([^~]+)~~", "<st>$1</st>");
|
||||||
/// </summary>
|
// replace inline code with custom code font
|
||||||
Blockquote = 1,
|
par = Regex.Replace(par, @"`([^`]+)`", $"<f {this.CodeFont}>$1</f>");
|
||||||
/// <summary>
|
return par;
|
||||||
/// A vertical space, which is a gap between multiple markdown paragraphs.
|
|
||||||
/// This element type is a <see cref="VerticalSpace"/>.
|
|
||||||
/// </summary>
|
|
||||||
VerticalSpace = 2,
|
|
||||||
/// <summary>
|
|
||||||
/// An image.
|
|
||||||
/// This element type is an <see cref="Image"/>.
|
|
||||||
/// </summary>
|
|
||||||
Image = 4,
|
|
||||||
/// <summary>
|
|
||||||
/// A header with header level 1.
|
|
||||||
/// This element type is a <see cref="Paragraph"/>.
|
|
||||||
/// </summary>
|
|
||||||
Header1 = 8,
|
|
||||||
/// <summary>
|
|
||||||
/// A header with header level 2.
|
|
||||||
/// This element type is a <see cref="Paragraph"/>.
|
|
||||||
/// </summary>
|
|
||||||
Header2 = 16,
|
|
||||||
/// <summary>
|
|
||||||
/// A header with header level 3.
|
|
||||||
/// This element type is a <see cref="Paragraph"/>.
|
|
||||||
/// </summary>
|
|
||||||
Header3 = 32,
|
|
||||||
/// <summary>
|
|
||||||
/// A header with header level 4.
|
|
||||||
/// This element type is a <see cref="Paragraph"/>.
|
|
||||||
/// </summary>
|
|
||||||
Header4 = 64,
|
|
||||||
/// <summary>
|
|
||||||
/// A header with header level 5.
|
|
||||||
/// This element type is a <see cref="Paragraph"/>.
|
|
||||||
/// </summary>
|
|
||||||
Header5 = 128,
|
|
||||||
/// <summary>
|
|
||||||
/// A header with header level 6.
|
|
||||||
/// This element type is a <see cref="Paragraph"/>.
|
|
||||||
/// </summary>
|
|
||||||
Header6 = 256,
|
|
||||||
/// <summary>
|
|
||||||
/// A combined flag that contains <see cref="Header1"/> through <see cref="Header6"/>.
|
|
||||||
/// This element type is a <see cref="Paragraph"/>.
|
|
||||||
/// </summary>
|
|
||||||
Headers = ElementType.Header1 | ElementType.Header2 | ElementType.Header3 | ElementType.Header4 | ElementType.Header5 | ElementType.Header6,
|
|
||||||
/// <summary>
|
|
||||||
/// A paragraph, which is one line of markdown text.
|
|
||||||
/// This element type is a <see cref="Paragraph"/>.
|
|
||||||
/// </summary>
|
|
||||||
Paragraph = 512,
|
|
||||||
/// <summary>
|
|
||||||
/// A single line of a code block.
|
|
||||||
/// This element type is a <see cref="Paragraph"/>.
|
|
||||||
/// </summary>
|
|
||||||
CodeBlock = 1024
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
263
MLEM.Ui/Parsers/UiParser.cs
Normal file
263
MLEM.Ui/Parsers/UiParser.cs
Normal file
|
@ -0,0 +1,263 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using Microsoft.Xna.Framework.Graphics;
|
||||||
|
using MLEM.Formatting;
|
||||||
|
using MLEM.Textures;
|
||||||
|
using MLEM.Ui.Elements;
|
||||||
|
using MLEM.Ui.Style;
|
||||||
|
|
||||||
|
#if NETSTANDARD2_0_OR_GREATER || NET6_0_OR_GREATER
|
||||||
|
using System.Net.Http;
|
||||||
|
|
||||||
|
#else
|
||||||
|
using System.Net;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace MLEM.Ui.Parsers {
|
||||||
|
/// <summary>
|
||||||
|
/// A base class for parsing various types of formatted strings into a set of MLEM.Ui elements with styling for each individual <see cref="ElementType"/>.
|
||||||
|
/// The only parser currently implemented is <see cref="UiMarkdownParser"/>.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class UiParser {
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An array containing all of the <see cref="ElementType"/> enum values.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly ElementType[] ElementTypes =
|
||||||
|
#if NET6_0_OR_GREATER
|
||||||
|
Enum.GetValues<ElementType>();
|
||||||
|
#else
|
||||||
|
(ElementType[]) Enum.GetValues(typeof(ElementType));
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The base path for images, which is prepended to the image link.
|
||||||
|
/// </summary>
|
||||||
|
public string ImageBasePath;
|
||||||
|
/// <summary>
|
||||||
|
/// An action that is invoked when an image fails to load while parsing.
|
||||||
|
/// This action receives the expected location of the image, as well as the <see cref="Exception"/> that occured.
|
||||||
|
/// </summary>
|
||||||
|
public Action<string, Exception> ImageExceptionHandler;
|
||||||
|
/// <summary>
|
||||||
|
/// The graphics device that should be used when loading images and other graphics-dependent content.
|
||||||
|
/// </summary>
|
||||||
|
public GraphicsDevice GraphicsDevice;
|
||||||
|
/// <summary>
|
||||||
|
/// The name of the font used for inline code as well as code blocks.
|
||||||
|
/// This only has an effect if a font with this name is added to the used <see cref="UiStyle"/>'s <see cref="UiStyle.AdditionalFonts"/>.
|
||||||
|
/// This defaults to "Monospaced" if default styling is applied in <see cref="UiParser(bool)"/>.
|
||||||
|
/// </summary>
|
||||||
|
public string CodeFont;
|
||||||
|
|
||||||
|
private readonly Dictionary<ElementType, Action<Element>> elementStyles = new Dictionary<ElementType, Action<Element>>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new UI parser and optionally initializes some default style settings.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="applyDefaultStyling">Whether default style settings should be applied.</param>
|
||||||
|
protected UiParser(bool applyDefaultStyling) {
|
||||||
|
if (applyDefaultStyling) {
|
||||||
|
this.CodeFont = "Monospaced";
|
||||||
|
this.Style<VerticalSpace>(ElementType.VerticalSpace, v => v.Size = new Vector2(1, 5));
|
||||||
|
for (var i = 0; i < 6; i++) {
|
||||||
|
var level = i;
|
||||||
|
this.Style<Paragraph>(UiParser.ElementTypes[Array.IndexOf(UiParser.ElementTypes, UiParser.ElementType.Header1) + i], p => {
|
||||||
|
p.Alignment = TextAlignment.Center;
|
||||||
|
p.TextScaleMultiplier = 2 - level * 0.15F;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parses the given raw formatted string into a set of elements and returns them along with their <see cref="ElementType"/>.
|
||||||
|
/// This method is used by implementors to parse specific text, and it is used by <see cref="Parse"/> and <see cref="ParseInto"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="raw">The raw string to parse.</param>
|
||||||
|
/// <returns>The parsed elements, without styling.</returns>
|
||||||
|
protected abstract IEnumerable<(ElementType, Element)> ParseUnstyled(string raw);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parses the given raw formatted string into a set of elements and returns them along with their <see cref="ElementType"/>.
|
||||||
|
/// During this process, the element stylings specified using <see cref="Style{T}"/> are also applied.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="raw">The raw string to parse.</param>
|
||||||
|
/// <returns>The parsed elements.</returns>
|
||||||
|
public IEnumerable<(ElementType, Element)> Parse(string raw) {
|
||||||
|
foreach (var (t, e) in this.ParseUnstyled(raw)) {
|
||||||
|
if (this.elementStyles.TryGetValue(t, out var style))
|
||||||
|
style.Invoke(e);
|
||||||
|
yield return (t, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parses the given raw formatted string into a set of elements (using <see cref="Parse"/>) and adds them as children to the givem <paramref name="element"/>.
|
||||||
|
/// During this process, the element stylings specified using <see cref="Style{T}"/> are also applied.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="raw">The raw string to parse.</param>
|
||||||
|
/// <param name="element">The element to add the parsed elements to.</param>
|
||||||
|
/// <returns>The <paramref name="element"/>, for chaining.</returns>
|
||||||
|
public Element ParseInto(string raw, Element element) {
|
||||||
|
foreach (var (_, e) in this.Parse(raw))
|
||||||
|
element.AddChild(e);
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Specifies an action to be invoked when a new element with the given <see cref="ElementType"/> is parsed in <see cref="Parse"/> or <see cref="ParseInto"/>.
|
||||||
|
/// These actions can be used to modify the style properties of the created elements.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="types">The element types that should be styled. Can be a combined flag.</param>
|
||||||
|
/// <param name="style">The action that styles the elements with the given element type.</param>
|
||||||
|
/// <param name="add">Whether the <paramref name="style"/> function should be added to the existing style settings, or replace them.</param>
|
||||||
|
/// <typeparam name="T">The type of elements that the given <see cref="ElementType"/> flags are expected to be.</typeparam>
|
||||||
|
/// <returns>This parser, for chaining.</returns>
|
||||||
|
public UiParser Style<T>(ElementType types, Action<T> style, bool add = false) where T : Element {
|
||||||
|
foreach (var type in UiParser.ElementTypes) {
|
||||||
|
if (types.HasFlag(type)) {
|
||||||
|
if (add && this.elementStyles.ContainsKey(type)) {
|
||||||
|
this.elementStyles[type] += Action;
|
||||||
|
} else {
|
||||||
|
this.elementStyles[type] = Action;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
|
||||||
|
void Action(Element e) {
|
||||||
|
style.Invoke(e as T ?? throw new ArgumentException($"Expected {typeof(T)} for style action but got {e.GetType()}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parses the given path into a <see cref="Image"/> element by loading it from disk or downloading it from the internet.
|
||||||
|
/// Note that, for a <paramref name="path"/> that doesn't start with <c>http</c> and isn't rooted, the <see cref="ImageBasePath"/> is prepended automatically.
|
||||||
|
/// This method invokes an asynchronouns action, meaning the <see cref="Image"/>'s <see cref="Image.Texture"/> will likely not have loaded in when this method returns.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path">The absolute, relative or web path to the image.</param>
|
||||||
|
/// <returns>The loaded image.</returns>
|
||||||
|
/// <exception cref="NullReferenceException">Thrown if <see cref="GraphicsDevice"/> is null, or if there is an <see cref="Exception"/> loading the image and <see cref="ImageExceptionHandler"/> is unset.</exception>
|
||||||
|
protected Image ParseImage(string path) {
|
||||||
|
if (this.GraphicsDevice == null)
|
||||||
|
throw new NullReferenceException("A UI parser requires a GraphicsDevice for parsing images");
|
||||||
|
|
||||||
|
TextureRegion image = null;
|
||||||
|
return new Image(Anchor.AutoLeft, new Vector2(1, -1), _ => image) {
|
||||||
|
OnAddedToUi = e => {
|
||||||
|
if (image == null)
|
||||||
|
LoadImageAsync();
|
||||||
|
},
|
||||||
|
OnRemovedFromUi = e => {
|
||||||
|
image?.Texture.Dispose();
|
||||||
|
image = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
async void LoadImageAsync() {
|
||||||
|
// only apply the base path for relative files
|
||||||
|
if (this.ImageBasePath != null && !path.StartsWith("http") && !Path.IsPathRooted(path))
|
||||||
|
path = $"{this.ImageBasePath}/{path}";
|
||||||
|
try {
|
||||||
|
Texture2D tex;
|
||||||
|
if (path.StartsWith("http")) {
|
||||||
|
byte[] src;
|
||||||
|
#if NETSTANDARD2_0_OR_GREATER || NET6_0_OR_GREATER
|
||||||
|
using (var client = new HttpClient())
|
||||||
|
src = await client.GetByteArrayAsync(path);
|
||||||
|
#else
|
||||||
|
using (var client = new WebClient())
|
||||||
|
src = await client.DownloadDataTaskAsync(path);
|
||||||
|
#endif
|
||||||
|
using (var memory = new MemoryStream(src))
|
||||||
|
tex = Texture2D.FromStream(this.GraphicsDevice, memory);
|
||||||
|
} else {
|
||||||
|
using (var stream = Path.IsPathRooted(path) ? File.OpenRead(path) : TitleContainer.OpenStream(path))
|
||||||
|
tex = Texture2D.FromStream(this.GraphicsDevice, stream);
|
||||||
|
}
|
||||||
|
image = new TextureRegion(tex);
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (this.ImageExceptionHandler != null) {
|
||||||
|
this.ImageExceptionHandler.Invoke(path, e);
|
||||||
|
} else {
|
||||||
|
throw new NullReferenceException($"Couldn't parse image {path}, and no ImageExceptionHandler was set", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A flags enumeration used by <see cref="UiParser"/> that contains the types of elements that can be parsed and returned in <see cref="Parse"/> or <see cref="ParseInto"/>.
|
||||||
|
/// This is a flags enumeration so that <see cref="Style{T}"/> can have multiple element types being styled at the same time.
|
||||||
|
/// </summary>
|
||||||
|
[Flags]
|
||||||
|
public enum ElementType {
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A blockquote.
|
||||||
|
/// This element type is a <see cref="Paragraph"/>.
|
||||||
|
/// </summary>
|
||||||
|
Blockquote = 1,
|
||||||
|
/// <summary>
|
||||||
|
/// A vertical space, which is a gap between multiple paragraphs.
|
||||||
|
/// This element type is a <see cref="VerticalSpace"/>.
|
||||||
|
/// </summary>
|
||||||
|
VerticalSpace = 2,
|
||||||
|
/// <summary>
|
||||||
|
/// An image.
|
||||||
|
/// This element type is an <see cref="Image"/>.
|
||||||
|
/// </summary>
|
||||||
|
Image = 4,
|
||||||
|
/// <summary>
|
||||||
|
/// A header with header level 1.
|
||||||
|
/// This element type is a <see cref="Paragraph"/>.
|
||||||
|
/// </summary>
|
||||||
|
Header1 = 8,
|
||||||
|
/// <summary>
|
||||||
|
/// A header with header level 2.
|
||||||
|
/// This element type is a <see cref="Paragraph"/>.
|
||||||
|
/// </summary>
|
||||||
|
Header2 = 16,
|
||||||
|
/// <summary>
|
||||||
|
/// A header with header level 3.
|
||||||
|
/// This element type is a <see cref="Paragraph"/>.
|
||||||
|
/// </summary>
|
||||||
|
Header3 = 32,
|
||||||
|
/// <summary>
|
||||||
|
/// A header with header level 4.
|
||||||
|
/// This element type is a <see cref="Paragraph"/>.
|
||||||
|
/// </summary>
|
||||||
|
Header4 = 64,
|
||||||
|
/// <summary>
|
||||||
|
/// A header with header level 5.
|
||||||
|
/// This element type is a <see cref="Paragraph"/>.
|
||||||
|
/// </summary>
|
||||||
|
Header5 = 128,
|
||||||
|
/// <summary>
|
||||||
|
/// A header with header level 6.
|
||||||
|
/// This element type is a <see cref="Paragraph"/>.
|
||||||
|
/// </summary>
|
||||||
|
Header6 = 256,
|
||||||
|
/// <summary>
|
||||||
|
/// A combined flag that contains <see cref="Header1"/> through <see cref="Header6"/>.
|
||||||
|
/// This element type is a <see cref="Paragraph"/>.
|
||||||
|
/// </summary>
|
||||||
|
Headers = ElementType.Header1 | ElementType.Header2 | ElementType.Header3 | ElementType.Header4 | ElementType.Header5 | ElementType.Header6,
|
||||||
|
/// <summary>
|
||||||
|
/// A paragraph, which is one line (or non-vertically spaced section) of text.
|
||||||
|
/// This element type is a <see cref="Paragraph"/>.
|
||||||
|
/// </summary>
|
||||||
|
Paragraph = 512,
|
||||||
|
/// <summary>
|
||||||
|
/// A single line of a code block.
|
||||||
|
/// This element type is a <see cref="Paragraph"/>.
|
||||||
|
/// </summary>
|
||||||
|
CodeBlock = 1024
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -84,7 +84,7 @@ namespace MLEM.Ui.Style {
|
||||||
/// <param name="obj">The object to compare with the current instance.</param>
|
/// <param name="obj">The object to compare with the current instance.</param>
|
||||||
/// <returns>true if <paramref name="obj">obj</paramref> and this instance are the same type and represent the same value; otherwise, false.</returns>
|
/// <returns>true if <paramref name="obj">obj</paramref> and this instance are the same type and represent the same value; otherwise, false.</returns>
|
||||||
[Obsolete("StyleProp equality is ambiguous as it is not clear whether priority is taken into account. Compare Values instead.")]
|
[Obsolete("StyleProp equality is ambiguous as it is not clear whether priority is taken into account. Compare Values instead.")]
|
||||||
#pragma warning disable CS0809
|
#pragma warning disable CS0809
|
||||||
public override bool Equals(object obj) {
|
public override bool Equals(object obj) {
|
||||||
return obj is StyleProp<T> other && this.Equals(other);
|
return obj is StyleProp<T> other && this.Equals(other);
|
||||||
}
|
}
|
||||||
|
@ -95,7 +95,7 @@ namespace MLEM.Ui.Style {
|
||||||
public override int GetHashCode() {
|
public override int GetHashCode() {
|
||||||
return EqualityComparer<T>.Default.GetHashCode(this.Value);
|
return EqualityComparer<T>.Default.GetHashCode(this.Value);
|
||||||
}
|
}
|
||||||
#pragma warning restore CS0809
|
#pragma warning restore CS0809
|
||||||
|
|
||||||
/// <summary>Returns the fully qualified type name of this instance.</summary>
|
/// <summary>Returns the fully qualified type name of this instance.</summary>
|
||||||
/// <returns>The fully qualified type name.</returns>
|
/// <returns>The fully qualified type name.</returns>
|
||||||
|
|
|
@ -5,9 +5,9 @@ using MLEM.Font;
|
||||||
using MLEM.Formatting;
|
using MLEM.Formatting;
|
||||||
using MLEM.Formatting.Codes;
|
using MLEM.Formatting.Codes;
|
||||||
using MLEM.Misc;
|
using MLEM.Misc;
|
||||||
|
using MLEM.Sound;
|
||||||
using MLEM.Textures;
|
using MLEM.Textures;
|
||||||
using MLEM.Ui.Elements;
|
using MLEM.Ui.Elements;
|
||||||
using SoundEffectInfo = MLEM.Sound.SoundEffectInfo;
|
|
||||||
|
|
||||||
namespace MLEM.Ui.Style {
|
namespace MLEM.Ui.Style {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -216,7 +216,7 @@ namespace MLEM.Ui.Style {
|
||||||
/// The color that a <see cref="Paragraph"/>'s <see cref="Paragraph.Link"/> codes should have.
|
/// The color that a <see cref="Paragraph"/>'s <see cref="Paragraph.Link"/> codes should have.
|
||||||
/// This value is passed to <see cref="LinkCode"/>.
|
/// This value is passed to <see cref="LinkCode"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Color? LinkColor;
|
public Color? LinkColor = Color.CornflowerBlue;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A set of additional fonts that can be used for the <c><f FontName></c> formatting code
|
/// A set of additional fonts that can be used for the <c><f FontName></c> formatting code
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
@ -9,6 +9,10 @@ using MLEM.Misc;
|
||||||
using MLEM.Ui.Elements;
|
using MLEM.Ui.Elements;
|
||||||
using MLEM.Ui.Style;
|
using MLEM.Ui.Style;
|
||||||
|
|
||||||
|
#if NET452
|
||||||
|
using MLEM.Extensions;
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace MLEM.Ui {
|
namespace MLEM.Ui {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// UiControls holds and manages all of the controls for a <see cref="UiSystem"/>.
|
/// UiControls holds and manages all of the controls for a <see cref="UiSystem"/>.
|
||||||
|
@ -157,29 +161,29 @@ namespace MLEM.Ui {
|
||||||
this.Input.Update();
|
this.Input.Update();
|
||||||
this.ActiveRoot = this.System.GetRootElements().FirstOrDefault(root => root.CanBeActive);
|
this.ActiveRoot = this.System.GetRootElements().FirstOrDefault(root => root.CanBeActive);
|
||||||
|
|
||||||
// MOUSE INPUT
|
|
||||||
if (this.HandleMouse) {
|
if (this.HandleMouse) {
|
||||||
var mousedNow = this.GetElementUnderPos(new Vector2(this.Input.ViewportMousePosition.X, this.Input.ViewportMousePosition.Y));
|
var mousedNow = this.GetElementUnderPos(new Vector2(this.Input.ViewportMousePosition.X, this.Input.ViewportMousePosition.Y));
|
||||||
this.SetMousedElement(mousedNow);
|
this.SetMousedElement(mousedNow);
|
||||||
|
|
||||||
if (this.Input.IsMouseButtonPressedAvailable(MouseButton.Left)) {
|
if (this.Input.IsPressedAvailable(MouseButton.Left)) {
|
||||||
this.IsAutoNavMode = false;
|
this.IsAutoNavMode = false;
|
||||||
var selectedNow = mousedNow != null && mousedNow.CanBeSelected ? mousedNow : null;
|
var selectedNow = mousedNow != null && mousedNow.CanBeSelected ? mousedNow : null;
|
||||||
this.SelectElement(this.ActiveRoot, selectedNow);
|
this.SelectElement(this.ActiveRoot, selectedNow);
|
||||||
if (mousedNow != null && mousedNow.CanBePressed) {
|
if (mousedNow != null && mousedNow.CanBePressed) {
|
||||||
this.System.InvokeOnElementPressed(mousedNow);
|
this.System.InvokeOnElementPressed(mousedNow);
|
||||||
this.Input.TryConsumeMouseButtonPressed(MouseButton.Left);
|
this.Input.TryConsumePressed(MouseButton.Left);
|
||||||
}
|
}
|
||||||
} else if (this.Input.IsMouseButtonPressedAvailable(MouseButton.Right)) {
|
} else if (this.Input.IsPressedAvailable(MouseButton.Right)) {
|
||||||
this.IsAutoNavMode = false;
|
this.IsAutoNavMode = false;
|
||||||
if (mousedNow != null && mousedNow.CanBePressed) {
|
if (mousedNow != null && mousedNow.CanBePressed) {
|
||||||
this.System.InvokeOnElementSecondaryPressed(mousedNow);
|
this.System.InvokeOnElementSecondaryPressed(mousedNow);
|
||||||
this.Input.TryConsumeMouseButtonPressed(MouseButton.Right);
|
this.Input.TryConsumePressed(MouseButton.Right);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
this.SetMousedElement(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// KEYBOARD INPUT
|
|
||||||
if (this.HandleKeyboard) {
|
if (this.HandleKeyboard) {
|
||||||
if (this.KeyboardButtons.IsPressedAvailable(this.Input, this.GamepadIndex)) {
|
if (this.KeyboardButtons.IsPressedAvailable(this.Input, this.GamepadIndex)) {
|
||||||
if (this.SelectedElement?.Root != null && this.SelectedElement.CanBePressed) {
|
if (this.SelectedElement?.Root != null && this.SelectedElement.CanBePressed) {
|
||||||
|
@ -192,7 +196,7 @@ namespace MLEM.Ui {
|
||||||
}
|
}
|
||||||
this.KeyboardButtons.TryConsumePressed(this.Input, this.GamepadIndex);
|
this.KeyboardButtons.TryConsumePressed(this.Input, this.GamepadIndex);
|
||||||
}
|
}
|
||||||
} else if (this.Input.IsKeyPressedAvailable(Keys.Tab)) {
|
} else if (this.Input.IsPressedAvailable(Keys.Tab)) {
|
||||||
this.IsAutoNavMode = true;
|
this.IsAutoNavMode = true;
|
||||||
// tab or shift-tab to next or previous element
|
// tab or shift-tab to next or previous element
|
||||||
var backward = this.Input.IsModifierKeyDown(ModifierKey.Shift);
|
var backward = this.Input.IsModifierKeyDown(ModifierKey.Shift);
|
||||||
|
@ -201,12 +205,11 @@ namespace MLEM.Ui {
|
||||||
next = this.SelectedElement.GetTabNextElement(backward, next);
|
next = this.SelectedElement.GetTabNextElement(backward, next);
|
||||||
if (next != this.SelectedElement) {
|
if (next != this.SelectedElement) {
|
||||||
this.SelectElement(this.ActiveRoot, next);
|
this.SelectElement(this.ActiveRoot, next);
|
||||||
this.Input.TryConsumeKeyPressed(Keys.Tab);
|
this.Input.TryConsumePressed(Keys.Tab);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TOUCH INPUT
|
|
||||||
if (this.HandleTouch) {
|
if (this.HandleTouch) {
|
||||||
if (this.Input.GetViewportGesture(GestureType.Tap, out var tap)) {
|
if (this.Input.GetViewportGesture(GestureType.Tap, out var tap)) {
|
||||||
this.IsAutoNavMode = false;
|
this.IsAutoNavMode = false;
|
||||||
|
@ -234,9 +237,10 @@ namespace MLEM.Ui {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
this.SetTouchedElement(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// GAMEPAD INPUT
|
|
||||||
if (this.HandleGamepad) {
|
if (this.HandleGamepad) {
|
||||||
if (this.GamepadButtons.IsPressedAvailable(this.Input, this.GamepadIndex)) {
|
if (this.GamepadButtons.IsPressedAvailable(this.Input, this.GamepadIndex)) {
|
||||||
if (this.SelectedElement?.Root != null && this.SelectedElement.CanBePressed) {
|
if (this.SelectedElement?.Root != null && this.SelectedElement.CanBePressed) {
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.Xna.Framework.Graphics;
|
|
||||||
using MLEM.Ui.Elements;
|
using MLEM.Ui.Elements;
|
||||||
|
|
||||||
namespace MLEM.Ui {
|
namespace MLEM.Ui {
|
||||||
|
|
|
@ -206,7 +206,12 @@ namespace MLEM.Ui {
|
||||||
/// <param name="style">The style settings that this ui should have. Use <see cref="UntexturedStyle"/> for the default, untextured style.</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>
|
/// <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>
|
||||||
/// <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>
|
/// <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) {
|
/// <param name="hasFontModifierFormatting">Whether default font modifier codes should be added to this ui system's <see cref="TextFormatter"/>, including bold, italic, strikethrough, shadow, subscript, and more.</param>
|
||||||
|
/// <param name="hasColorFormatting">Whether default color codes should be added to this ui system's <see cref="TextFormatter"/>, including all <see cref="Color"/> values and the ability to use custom colors.</param>
|
||||||
|
/// <param name="hasAnimationFormatting">Whether default animation codes should be added to this ui system's <see cref="TextFormatter"/>, namely the wobbly animation.</param>
|
||||||
|
/// <param name="hasMacroFormatting">Whether default macros should be added to this ui system's <see cref="TextFormatter"/>, including TeX's ~ non-breaking space and more.</param>
|
||||||
|
/// <param name="hasUiFormatting">Whether <see cref="UiSystem"/>-based formatting codes should be added to this ui system's <see cref="TextFormatter"/>, including <see cref="Paragraph.Link"/> codes and font switching.</param>
|
||||||
|
public UiSystem(Game game, UiStyle style, InputHandler inputHandler = null, bool automaticViewport = true, bool hasFontModifierFormatting = true, bool hasColorFormatting = true, bool hasAnimationFormatting = true, bool hasMacroFormatting = true, bool hasUiFormatting = true) : base(game) {
|
||||||
this.Controls = new UiControls(this, inputHandler);
|
this.Controls = new UiControls(this, inputHandler);
|
||||||
this.style = style;
|
this.style = style;
|
||||||
|
|
||||||
|
@ -248,12 +253,14 @@ namespace MLEM.Ui {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
this.TextFormatter = new TextFormatter();
|
this.TextFormatter = new TextFormatter(hasFontModifierFormatting, hasColorFormatting, hasAnimationFormatting, hasMacroFormatting);
|
||||||
this.TextFormatter.Codes.Add(new Regex("<l(?: ([^>]+))?>"), (f, m, r) => new LinkCode(m, r, 1 / 16F, 0.85F,
|
if (hasUiFormatting) {
|
||||||
t => this.Controls.MousedElement is Paragraph.Link l1 && l1.Token == t || this.Controls.TouchedElement is Paragraph.Link l2 && l2.Token == t,
|
this.TextFormatter.Codes.Add(new Regex("<l(?: ([^>]+))?>"), (f, m, r) => new LinkCode(m, r, 1 / 16F, 0.85F,
|
||||||
this.Style.LinkColor));
|
t => this.Controls.MousedElement is Paragraph.Link l1 && l1.Token == t || this.Controls.TouchedElement is Paragraph.Link l2 && l2.Token == t,
|
||||||
this.TextFormatter.Codes.Add(new Regex("<f ([^>]+)>"), (_, m, r) => new FontCode(m, r,
|
d => this.Style.LinkColor));
|
||||||
f => this.Style.AdditionalFonts != null && this.Style.AdditionalFonts.TryGetValue(m.Groups[1].Value, out var c) ? c : f));
|
this.TextFormatter.Codes.Add(new Regex("<f ([^>]+)>"), (_, m, r) => new FontCode(m, r,
|
||||||
|
f => this.Style.AdditionalFonts != null && this.Style.AdditionalFonts.TryGetValue(m.Groups[1].Value, out var c) ? c : f));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -309,7 +316,7 @@ namespace MLEM.Ui {
|
||||||
var context = this.SpriteBatchContext;
|
var context = this.SpriteBatchContext;
|
||||||
context.TransformMatrix = root.Transform * context.TransformMatrix;
|
context.TransformMatrix = root.Transform * context.TransformMatrix;
|
||||||
|
|
||||||
#pragma warning disable CS0618
|
#pragma warning disable CS0618
|
||||||
if (this.BlendState != null)
|
if (this.BlendState != null)
|
||||||
context.BlendState = this.BlendState;
|
context.BlendState = this.BlendState;
|
||||||
if (this.SamplerState != null)
|
if (this.SamplerState != null)
|
||||||
|
@ -318,12 +325,12 @@ namespace MLEM.Ui {
|
||||||
context.DepthStencilState = this.DepthStencilState;
|
context.DepthStencilState = this.DepthStencilState;
|
||||||
if (this.Effect != null)
|
if (this.Effect != null)
|
||||||
context.Effect = this.Effect;
|
context.Effect = this.Effect;
|
||||||
#pragma warning restore CS0618
|
#pragma warning restore CS0618
|
||||||
|
|
||||||
batch.Begin(context);
|
batch.Begin(context);
|
||||||
#pragma warning disable CS0618
|
#pragma warning disable CS0618
|
||||||
root.Element.DrawTransformed(time, batch, this.DrawAlpha * root.Element.DrawAlpha, context.BlendState, context.SamplerState, context.DepthStencilState, context.Effect, context.TransformMatrix);
|
root.Element.DrawTransformed(time, batch, this.DrawAlpha * root.Element.DrawAlpha, context.BlendState, context.SamplerState, context.DepthStencilState, context.Effect, context.TransformMatrix);
|
||||||
#pragma warning restore CS0618
|
#pragma warning restore CS0618
|
||||||
batch.End();
|
batch.End();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -345,9 +352,7 @@ namespace MLEM.Ui {
|
||||||
var root = new RootElement(name, element, this);
|
var root = new RootElement(name, element, this);
|
||||||
this.rootElements.Add(root);
|
this.rootElements.Add(root);
|
||||||
root.Element.AndChildren(e => {
|
root.Element.AndChildren(e => {
|
||||||
e.Root = root;
|
e.AddedToUi(this, root);
|
||||||
e.System = this;
|
|
||||||
root.InvokeOnElementAdded(e);
|
|
||||||
e.SetAreaDirty();
|
e.SetAreaDirty();
|
||||||
});
|
});
|
||||||
this.OnRootAdded?.Invoke(root);
|
this.OnRootAdded?.Invoke(root);
|
||||||
|
@ -367,9 +372,7 @@ namespace MLEM.Ui {
|
||||||
this.rootElements.Remove(root);
|
this.rootElements.Remove(root);
|
||||||
this.Controls.SelectElement(root, null);
|
this.Controls.SelectElement(root, null);
|
||||||
root.Element.AndChildren(e => {
|
root.Element.AndChildren(e => {
|
||||||
e.Root = null;
|
e.RemovedFromUi();
|
||||||
e.System = null;
|
|
||||||
root.InvokeOnElementRemoved(e);
|
|
||||||
e.SetAreaDirty();
|
e.SetAreaDirty();
|
||||||
});
|
});
|
||||||
this.OnRootRemoved?.Invoke(root);
|
this.OnRootRemoved?.Invoke(root);
|
||||||
|
@ -561,7 +564,7 @@ namespace MLEM.Ui {
|
||||||
/// This property returns <see langword="true"/> if <see cref="CanSelectContent"/> is <see langword="true"/> and the <see cref="Element"/> is not hidden, or if it has been set to <see langword="true"/> manually.
|
/// This property returns <see langword="true"/> if <see cref="CanSelectContent"/> is <see langword="true"/> and the <see cref="Element"/> is not hidden, or if it has been set to <see langword="true"/> manually.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool CanBeActive {
|
public bool CanBeActive {
|
||||||
get => this.canBeActive || (!this.Element.IsHidden && this.CanSelectContent);
|
get => this.canBeActive || !this.Element.IsHidden && this.CanSelectContent;
|
||||||
set => this.canBeActive = value;
|
set => this.canBeActive = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -570,7 +573,7 @@ namespace MLEM.Ui {
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public event Element.GenericCallback OnElementAdded;
|
public event Element.GenericCallback OnElementAdded;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Event that is invoked when a <see cref="Element"/> is removed rom this root element of any of its children.
|
/// Event that is invoked when a <see cref="Element"/> is removed rom this root element or any of its children.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public event Element.GenericCallback OnElementRemoved;
|
public event Element.GenericCallback OnElementRemoved;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
12
MLEM.sln
12
MLEM.sln
|
@ -18,10 +18,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MLEM.Data", "MLEM.Data\MLEM
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MLEM.Templates", "MLEM.Templates\MLEM.Templates.csproj", "{C2A2CFED-C9E8-4675-BD66-EFC3DB210977}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MLEM.Templates", "MLEM.Templates\MLEM.Templates.csproj", "{C2A2CFED-C9E8-4675-BD66-EFC3DB210977}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Demos.Android", "Demos.Android\Demos.Android.csproj", "{410C0262-131C-4D0E-910D-D01B4F7143E0}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{53D52C3F-67FB-4F32-A794-EAB140BBFC11}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{53D52C3F-67FB-4F32-A794-EAB140BBFC11}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Demos.Android", "Demos.Android\Demos.Android.csproj", "{DA234B13-AA46-4C03-8CFC-A969A2AEAEF2}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
@ -64,13 +64,13 @@ Global
|
||||||
{C2A2CFED-C9E8-4675-BD66-EFC3DB210977}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{C2A2CFED-C9E8-4675-BD66-EFC3DB210977}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{C2A2CFED-C9E8-4675-BD66-EFC3DB210977}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{C2A2CFED-C9E8-4675-BD66-EFC3DB210977}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{C2A2CFED-C9E8-4675-BD66-EFC3DB210977}.Release|Any CPU.Build.0 = Release|Any CPU
|
{C2A2CFED-C9E8-4675-BD66-EFC3DB210977}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{410C0262-131C-4D0E-910D-D01B4F7143E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{410C0262-131C-4D0E-910D-D01B4F7143E0}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{410C0262-131C-4D0E-910D-D01B4F7143E0}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{410C0262-131C-4D0E-910D-D01B4F7143E0}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{53D52C3F-67FB-4F32-A794-EAB140BBFC11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{53D52C3F-67FB-4F32-A794-EAB140BBFC11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{53D52C3F-67FB-4F32-A794-EAB140BBFC11}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{53D52C3F-67FB-4F32-A794-EAB140BBFC11}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{53D52C3F-67FB-4F32-A794-EAB140BBFC11}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{53D52C3F-67FB-4F32-A794-EAB140BBFC11}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{53D52C3F-67FB-4F32-A794-EAB140BBFC11}.Release|Any CPU.Build.0 = Release|Any CPU
|
{53D52C3F-67FB-4F32-A794-EAB140BBFC11}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{DA234B13-AA46-4C03-8CFC-A969A2AEAEF2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{DA234B13-AA46-4C03-8CFC-A969A2AEAEF2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{DA234B13-AA46-4C03-8CFC-A969A2AEAEF2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{DA234B13-AA46-4C03-8CFC-A969A2AEAEF2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace MLEM.Extensions {
|
namespace MLEM.Extensions {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A set of extensions for dealing with <see cref="char"/>
|
/// A set of extensions for dealing with <see cref="char"/>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[Obsolete("ToCachedString is deprecated. Consider using a more robust, custom implementation for text caching, or CodePointSource.ToString for UTF-32 caching.")]
|
||||||
public static class CharExtensions {
|
public static class CharExtensions {
|
||||||
|
|
||||||
private static readonly Dictionary<char, string> Cache = new Dictionary<char, string>();
|
private static readonly Dictionary<char, string> Cache = new Dictionary<char, string>();
|
||||||
|
@ -14,6 +16,7 @@ namespace MLEM.Extensions {
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="c">The character to turn into a string</param>
|
/// <param name="c">The character to turn into a string</param>
|
||||||
/// <returns>A string representing the character</returns>
|
/// <returns>A string representing the character</returns>
|
||||||
|
[Obsolete("ToCachedString is deprecated. Consider using a more robust, custom implementation for text caching, or CodePointSource.ToString for UTF-32 caching.")]
|
||||||
public static string ToCachedString(this char c) {
|
public static string ToCachedString(this char c) {
|
||||||
if (!CharExtensions.Cache.TryGetValue(c, out var ret)) {
|
if (!CharExtensions.Cache.TryGetValue(c, out var ret)) {
|
||||||
ret = c.ToString();
|
ret = c.ToString();
|
||||||
|
|
|
@ -51,5 +51,29 @@ namespace MLEM.Extensions {
|
||||||
return combos;
|
return combos;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if NET452
|
||||||
|
/// <summary>Appends a value to the end of the sequence.</summary>
|
||||||
|
/// <param name="source">A sequence of values.</param>
|
||||||
|
/// <param name="element">The value to append to <paramref name="source"/>.</param>
|
||||||
|
/// <typeparam name="T">The type of the elements of <paramref name="source"/>.</typeparam>
|
||||||
|
/// <returns>A new sequence that ends with <paramref name="element"/>.</returns>
|
||||||
|
public static IEnumerable<T> Append<T>(this IEnumerable<T> source, T element) {
|
||||||
|
foreach (var src in source)
|
||||||
|
yield return src;
|
||||||
|
yield return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Prepends a value to the beginning of the sequence.</summary>
|
||||||
|
/// <param name="source">A sequence of values.</param>
|
||||||
|
/// <param name="element">The value to prepend to <paramref name="source"/>.</param>
|
||||||
|
/// <typeparam name="T">The type of the elements of <paramref name="source"/>.</typeparam>
|
||||||
|
/// <returns>A new sequence that begins with <paramref name="element"/>.</returns>
|
||||||
|
public static IEnumerable<T> Prepend<T>(this IEnumerable<T> source, T element) {
|
||||||
|
yield return element;
|
||||||
|
foreach (var src in source)
|
||||||
|
yield return src;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,12 +90,12 @@ namespace MLEM.Extensions {
|
||||||
/// <param name="target">The target to apply</param>
|
/// <param name="target">The target to apply</param>
|
||||||
public TargetContext(GraphicsDevice device, RenderTarget2D target) {
|
public TargetContext(GraphicsDevice device, RenderTarget2D target) {
|
||||||
this.device = device;
|
this.device = device;
|
||||||
#if FNA
|
#if FNA
|
||||||
// RenderTargetCount doesn't exist in FNA but we still want the optimization in MG
|
// RenderTargetCount doesn't exist in FNA but we still want the optimization in MG
|
||||||
this.lastTargets = device.GetRenderTargets();
|
this.lastTargets = device.GetRenderTargets();
|
||||||
#else
|
#else
|
||||||
this.lastTargets = device.RenderTargetCount <= 0 ? null : device.GetRenderTargets();
|
this.lastTargets = device.RenderTargetCount <= 0 ? null : device.GetRenderTargets();
|
||||||
#endif
|
#endif
|
||||||
device.SetRenderTarget(target);
|
device.SetRenderTarget(target);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -288,7 +288,7 @@ namespace MLEM.Extensions {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
#if FNA
|
#if FNA
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a <see cref="Point"/> representation for this object.
|
/// Gets a <see cref="Point"/> representation for this object.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -304,7 +304,42 @@ namespace MLEM.Extensions {
|
||||||
public static Vector2 ToVector2(this Point point) {
|
public static Vector2 ToVector2(this Point point) {
|
||||||
return new Vector2(point.X, point.Y);
|
return new Vector2(point.X, point.Y);
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deconstruction method for <see cref="Point"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="point">The point to deconstruct.</param>
|
||||||
|
/// <param name="x"></param>
|
||||||
|
/// <param name="y"></param>
|
||||||
|
public static void Deconstruct(this Point point, out int x, out int y) {
|
||||||
|
x = point.X;
|
||||||
|
y = point.Y;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deconstruction method for <see cref="Vector2"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="vector">The vector to deconstruct.</param>
|
||||||
|
/// <param name="x"></param>
|
||||||
|
/// <param name="y"></param>
|
||||||
|
public static void Deconstruct(this Vector2 vector, out float x, out float y) {
|
||||||
|
x = vector.X;
|
||||||
|
y = vector.Y;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deconstruction method for <see cref="Vector3"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="vector">The vector to deconstruct.</param>
|
||||||
|
/// <param name="x"></param>
|
||||||
|
/// <param name="y"></param>
|
||||||
|
/// <param name="z"></param>
|
||||||
|
public static void Deconstruct(this Vector3 vector, out float x, out float y, out float z) {
|
||||||
|
x = vector.X;
|
||||||
|
y = vector.Y;
|
||||||
|
z = vector.Z;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,5 +55,37 @@ namespace MLEM.Extensions {
|
||||||
throw new IndexOutOfRangeException();
|
throw new IndexOutOfRangeException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a random floating-point number that is greater than or equal to 0, and less than <paramref name="maxValue"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="random">The random.</param>
|
||||||
|
/// <param name="maxValue">The (exclusive) maximum value.</param>
|
||||||
|
/// <returns>A single-precision floating point number that is greater than or equal to 0, and less than <paramref name="maxValue"/>.</returns>
|
||||||
|
public static float NextSingle(this Random random, float maxValue) {
|
||||||
|
return maxValue * random.NextSingle();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a random floating-point number that is greater than or equal to <paramref name="minValue"/>, and less than <paramref name="maxValue"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="random">The random.</param>
|
||||||
|
/// <param name="minValue">The (inclusive) minimum value.</param>
|
||||||
|
/// <param name="maxValue">The (exclusive) maximum value.</param>
|
||||||
|
/// <returns>A single-precision floating point number that is greater than or equal to <paramref name="minValue"/>, and less than <paramref name="maxValue"/>.</returns>
|
||||||
|
public static float NextSingle(this Random random, float minValue, float maxValue) {
|
||||||
|
return (maxValue - minValue) * random.NextSingle() + minValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if !NET6_0_OR_GREATER
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a random floating-point number that is greater than or equal to 0, and less than 1.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="random">The random.</param>
|
||||||
|
/// <returns>A single-precision floating point number that is greater than or equal to 0, and less than 1.</returns>
|
||||||
|
public static float NextSingle(this Random random) {
|
||||||
|
return (float) random.NextDouble();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
99
MLEM/Font/CodePointSource.cs
Normal file
99
MLEM/Font/CodePointSource.cs
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace MLEM.Font {
|
||||||
|
/// <summary>
|
||||||
|
/// A code point source is a wrapper around a <see cref="string"/> or <see cref="StringBuilder"/> that allows retrieving UTF-32 code points at a given index using <see cref="GetCodePoint"/>. Additionally, it allows enumerating every code point in the underlying <see cref="string"/> or <see cref="StringBuilder"/>. This class also contains <see cref="ToString(int)"/>, which converts a code point into its <see cref="string"/> representation, but caches the result to avoid allocating excess memory.
|
||||||
|
/// </summary>
|
||||||
|
public readonly struct CodePointSource : IEnumerable<int> {
|
||||||
|
|
||||||
|
private static readonly Dictionary<int, string> StringCache = new Dictionary<int, string>();
|
||||||
|
|
||||||
|
private readonly string strg;
|
||||||
|
private readonly StringBuilder builder;
|
||||||
|
private char this[int index] => this.strg?[index] ?? this.builder[index];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The length of this code point, in characters.
|
||||||
|
/// Note that this is not representative of the amount of code points in this source.
|
||||||
|
/// </summary>
|
||||||
|
public int Length => this.strg?.Length ?? this.builder.Length;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new code point source from the given <see cref="string"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="strg">The <see cref="string"/> whose code points to inspect.</param>
|
||||||
|
public CodePointSource(string strg) {
|
||||||
|
this.strg = strg;
|
||||||
|
this.builder = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new code point source from the given <see cref="StringBuilder"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="builder">The <see cref="StringBuilder"/> whose code points to inspect.</param>
|
||||||
|
public CodePointSource(StringBuilder builder) {
|
||||||
|
this.strg = null;
|
||||||
|
this.builder = builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the code point at the given <paramref name="index"/> in this code point source's underlying string, where the index is measured in characters and not code points.
|
||||||
|
/// The resulting code point will either be a single <see cref="char"/> cast to an <see cref="int"/>, at which point the returned length will be 1, or a UTF-32 <see cref="int"/> character made up of two <see cref="char"/> values, at which point the returned length will be 2.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="index">The index at which to return the code point, which is measured in characters.</param>
|
||||||
|
/// <param name="indexLowSurrogate">Whether the <paramref name="index"/> represents a low surrogate. If this is <see langword="false"/>, the <paramref name="index"/> represents a high surrogate and the low surrogate will be looked for in the following character. If this is <see langword="true"/>, the <paramref name="index"/> represents a low surrogate and the high surrogate will be looked for in the previous character.</param>
|
||||||
|
/// <returns>The code point at the given location, as well as its length.</returns>
|
||||||
|
public (int CodePoint, int Length) GetCodePoint(int index, bool indexLowSurrogate = false) {
|
||||||
|
var curr = this[index];
|
||||||
|
if (indexLowSurrogate) {
|
||||||
|
if (index > 0) {
|
||||||
|
var high = this[index - 1];
|
||||||
|
if (char.IsSurrogatePair(high, curr))
|
||||||
|
return (char.ConvertToUtf32(high, curr), 2);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (index < this.Length - 1) {
|
||||||
|
var low = this[index + 1];
|
||||||
|
if (char.IsSurrogatePair(curr, low))
|
||||||
|
return (char.ConvertToUtf32(curr, low), 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (curr, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Returns an enumerator that iterates through the collection.</summary>
|
||||||
|
/// <returns>A <see cref="T:System.Collections.Generic.IEnumerator`1" /> that can be used to iterate through the collection.</returns>
|
||||||
|
/// <filterpriority>1</filterpriority>
|
||||||
|
public IEnumerator<int> GetEnumerator() {
|
||||||
|
var index = 0;
|
||||||
|
while (index < this.Length) {
|
||||||
|
var (codePoint, length) = this.GetCodePoint(index);
|
||||||
|
yield return codePoint;
|
||||||
|
index += length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Returns an enumerator that iterates through a collection.</summary>
|
||||||
|
/// <returns>An <see cref="T:System.Collections.IEnumerator" /> object that can be used to iterate through the collection.</returns>
|
||||||
|
/// <filterpriority>2</filterpriority>
|
||||||
|
IEnumerator IEnumerable.GetEnumerator() {
|
||||||
|
return this.GetEnumerator();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts the given UTF-32 <paramref name="codePoint"/> into a string using <see cref="char.ConvertFromUtf32"/>, but caches the result in a <see cref="Dictionary{TKey,TValue}"/> cache to avoid allocating excess memory.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="codePoint">The UTF-32 code point to convert.</param>
|
||||||
|
/// <returns>The string representation of the code point.</returns>
|
||||||
|
public static string ToString(int codePoint) {
|
||||||
|
if (!CodePointSource.StringCache.TryGetValue(codePoint, out var ret)) {
|
||||||
|
ret = char.ConvertFromUtf32(codePoint);
|
||||||
|
CodePointSource.StringCache.Add(codePoint, ret);
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,9 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Microsoft.Xna.Framework;
|
using Microsoft.Xna.Framework;
|
||||||
using Microsoft.Xna.Framework.Graphics;
|
using Microsoft.Xna.Framework.Graphics;
|
||||||
using MLEM.Extensions;
|
|
||||||
using MLEM.Misc;
|
using MLEM.Misc;
|
||||||
|
|
||||||
namespace MLEM.Font {
|
namespace MLEM.Font {
|
||||||
|
@ -50,35 +50,36 @@ namespace MLEM.Font {
|
||||||
public abstract float LineHeight { get; }
|
public abstract float LineHeight { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Measures the width of the given character with the default scale for use in <see cref="MeasureString(string,bool)"/>.
|
/// Measures the width of the given code point with the default scale for use in <see cref="MeasureString(string,bool)"/>.
|
||||||
/// Note that this method does not support <see cref="Nbsp"/>, <see cref="Zwsp"/> and <see cref="Emsp"/> for most generic fonts, which is why <see cref="MeasureString(string,bool)"/> should be used even for single characters.
|
/// Note that this method does not support <see cref="Nbsp"/>, <see cref="Zwsp"/> and <see cref="Emsp"/> for most generic fonts, which is why <see cref="MeasureString(string,bool)"/> should be used even for single characters.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="c">The character whose width to calculate</param>
|
/// <param name="codePoint">The code point whose width to calculate</param>
|
||||||
/// <returns>The width of the given character with the default scale</returns>
|
/// <returns>The width of the given character with the default scale</returns>
|
||||||
protected abstract float MeasureChar(char c);
|
protected abstract float MeasureCharacter(int codePoint);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Draws the given character with the given data for use in <see cref="DrawString(Microsoft.Xna.Framework.Graphics.SpriteBatch,System.Text.StringBuilder,Microsoft.Xna.Framework.Vector2,Microsoft.Xna.Framework.Color,float,Microsoft.Xna.Framework.Vector2,Microsoft.Xna.Framework.Vector2,Microsoft.Xna.Framework.Graphics.SpriteEffects,float)"/>.
|
/// Draws the given code point with the given data for use in <see cref="DrawString(Microsoft.Xna.Framework.Graphics.SpriteBatch,System.Text.StringBuilder,Microsoft.Xna.Framework.Vector2,Microsoft.Xna.Framework.Color,float,Microsoft.Xna.Framework.Vector2,Microsoft.Xna.Framework.Vector2,Microsoft.Xna.Framework.Graphics.SpriteEffects,float)"/>.
|
||||||
/// Note that this method is only called internally.
|
/// Note that this method is only called internally.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="batch">The sprite batch to draw with.</param>
|
/// <param name="batch">The sprite batch to draw with.</param>
|
||||||
/// <param name="cString">A string representation of the character which will be drawn.</param>
|
/// <param name="codePoint">The code point which will be drawn.</param>
|
||||||
|
/// <param name="character">A string representation of the character which will be drawn.</param>
|
||||||
/// <param name="position">The drawing location on screen.</param>
|
/// <param name="position">The drawing location on screen.</param>
|
||||||
/// <param name="color">A color mask.</param>
|
/// <param name="color">A color mask.</param>
|
||||||
/// <param name="rotation">A rotation of this character.</param>
|
/// <param name="rotation">A rotation of this character.</param>
|
||||||
/// <param name="scale">A scaling of this character.</param>
|
/// <param name="scale">A scaling of this character.</param>
|
||||||
/// <param name="effects">Modificators for drawing. Can be combined.</param>
|
/// <param name="effects">Modificators for drawing. Can be combined.</param>
|
||||||
/// <param name="layerDepth">A depth of the layer of this character.</param>
|
/// <param name="layerDepth">A depth of the layer of this character.</param>
|
||||||
protected abstract void DrawChar(SpriteBatch batch, string cString, Vector2 position, Color color, float rotation, Vector2 scale, SpriteEffects effects, float layerDepth);
|
protected abstract void DrawCharacter(SpriteBatch batch, int codePoint, string character, Vector2 position, Color color, float rotation, Vector2 scale, SpriteEffects effects, float layerDepth);
|
||||||
|
|
||||||
///<inheritdoc cref="SpriteBatch.DrawString(SpriteFont,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/>
|
///<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, Vector2 scale, SpriteEffects effects, float layerDepth) {
|
public void DrawString(SpriteBatch batch, string text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth) {
|
||||||
this.DrawString(batch, new CharSource(text), position, color, rotation, origin, scale, effects, layerDepth);
|
this.DrawString(batch, new CodePointSource(text), position, color, rotation, origin, scale, effects, layerDepth);
|
||||||
}
|
}
|
||||||
|
|
||||||
///<inheritdoc cref="SpriteBatch.DrawString(SpriteFont,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/>
|
///<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, Vector2 scale, SpriteEffects effects, float layerDepth) {
|
public void DrawString(SpriteBatch batch, StringBuilder text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth) {
|
||||||
this.DrawString(batch, new CharSource(text), position, color, rotation, origin, scale, effects, layerDepth);
|
this.DrawString(batch, new CodePointSource(text), position, color, rotation, origin, scale, effects, layerDepth);
|
||||||
}
|
}
|
||||||
|
|
||||||
///<inheritdoc cref="SpriteBatch.DrawString(SpriteFont,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/>
|
///<inheritdoc cref="SpriteBatch.DrawString(SpriteFont,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/>
|
||||||
|
@ -103,19 +104,19 @@ namespace MLEM.Font {
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Measures the width of the given string when drawn with this font's underlying font.
|
/// Measures the width of the given string when drawn with this font's underlying font.
|
||||||
/// This method uses <see cref="MeasureChar"/> internally to calculate the size of known characters and calculates additional characters like <see cref="Nbsp"/>, <see cref="Zwsp"/> and <see cref="Emsp"/>.
|
/// This method uses <see cref="MeasureCharacter"/> internally to calculate the size of known characters and calculates additional characters like <see cref="Nbsp"/>, <see cref="Zwsp"/> and <see cref="Emsp"/>.
|
||||||
/// If the text contains newline characters (\n), the size returned will represent a rectangle that encompasses the width of the longest line and the string's full height.
|
/// If the text contains newline characters (\n), the size returned will represent a rectangle that encompasses the width of the longest line and the string's full height.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="text">The text whose size to calculate</param>
|
/// <param name="text">The text whose size to calculate</param>
|
||||||
/// <param name="ignoreTrailingSpaces">Whether trailing whitespace should be ignored in the returned size, causing the end of each line to be effectively trimmed</param>
|
/// <param name="ignoreTrailingSpaces">Whether trailing whitespace should be ignored in the returned size, causing the end of each line to be effectively trimmed</param>
|
||||||
/// <returns>The size of the string when drawn with this font</returns>
|
/// <returns>The size of the string when drawn with this font</returns>
|
||||||
public Vector2 MeasureString(string text, bool ignoreTrailingSpaces = false) {
|
public Vector2 MeasureString(string text, bool ignoreTrailingSpaces = false) {
|
||||||
return this.MeasureString(new CharSource(text), ignoreTrailingSpaces, null);
|
return this.MeasureString(new CodePointSource(text), ignoreTrailingSpaces);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc cref="MeasureString(string,bool)"/>
|
/// <inheritdoc cref="MeasureString(string,bool)"/>
|
||||||
public Vector2 MeasureString(StringBuilder text, bool ignoreTrailingSpaces = false) {
|
public Vector2 MeasureString(StringBuilder text, bool ignoreTrailingSpaces = false) {
|
||||||
return this.MeasureString(new CharSource(text), ignoreTrailingSpaces, null);
|
return this.MeasureString(new CodePointSource(text), ignoreTrailingSpaces);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -129,12 +130,12 @@ namespace MLEM.Font {
|
||||||
/// <param name="ellipsis">The characters to add to the end of the string if it is too long</param>
|
/// <param name="ellipsis">The characters to add to the end of the string if it is too long</param>
|
||||||
/// <returns>The truncated string, or the same string if it is shorter than the maximum width</returns>
|
/// <returns>The truncated string, or the same string if it is shorter than the maximum width</returns>
|
||||||
public string TruncateString(string text, float width, float scale, bool fromBack = false, string ellipsis = "") {
|
public string TruncateString(string text, float width, float scale, bool fromBack = false, string ellipsis = "") {
|
||||||
return this.TruncateString(new CharSource(text), width, scale, fromBack, ellipsis, null).ToString();
|
return GenericFont.TruncateString(Enumerable.Repeat(new DecoratedCodePointSource(new CodePointSource(text), this, 0), 1), width, scale, fromBack, ellipsis).First().ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc cref="TruncateString(string,float,float,bool,string)"/>
|
/// <inheritdoc cref="TruncateString(string,float,float,bool,string)"/>
|
||||||
public StringBuilder TruncateString(StringBuilder text, float width, float scale, bool fromBack = false, string ellipsis = "") {
|
public StringBuilder TruncateString(StringBuilder text, float width, float scale, bool fromBack = false, string ellipsis = "") {
|
||||||
return this.TruncateString(new CharSource(text), width, scale, fromBack, ellipsis, null);
|
return GenericFont.TruncateString(Enumerable.Repeat(new DecoratedCodePointSource(new CodePointSource(text), this, 0), 1), width, scale, fromBack, ellipsis).First();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -165,22 +166,23 @@ namespace MLEM.Font {
|
||||||
/// <param name="scale">The scale to use for width measurements</param>
|
/// <param name="scale">The scale to use for width measurements</param>
|
||||||
/// <returns>The split string as an enumerable of split sections</returns>
|
/// <returns>The split string as an enumerable of split sections</returns>
|
||||||
public IEnumerable<string> SplitStringSeparate(string text, float width, float scale) {
|
public IEnumerable<string> SplitStringSeparate(string text, float width, float scale) {
|
||||||
return this.SplitStringSeparate(new CharSource(text), width, scale, null);
|
return GenericFont.SplitStringSeparate(Enumerable.Repeat(new DecoratedCodePointSource(new CodePointSource(text), this, 0), 1), width, scale).First();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc cref="SplitStringSeparate(string,float,float)"/>
|
/// <inheritdoc cref="SplitStringSeparate(string,float,float)"/>
|
||||||
public IEnumerable<string> SplitStringSeparate(StringBuilder text, float width, float scale) {
|
public IEnumerable<string> SplitStringSeparate(StringBuilder text, float width, float scale) {
|
||||||
return this.SplitStringSeparate(new CharSource(text), width, scale, null);
|
return GenericFont.SplitStringSeparate(Enumerable.Repeat(new DecoratedCodePointSource(new CodePointSource(text), this, 0), 1), width, scale).First();
|
||||||
}
|
}
|
||||||
|
|
||||||
internal Vector2 MeasureString(CharSource text, bool ignoreTrailingSpaces, Func<int, GenericFont> fontFunction) {
|
private Vector2 MeasureString(CodePointSource text, bool ignoreTrailingSpaces) {
|
||||||
var size = Vector2.Zero;
|
var size = Vector2.Zero;
|
||||||
if (text.Length <= 0)
|
if (text.Length <= 0)
|
||||||
return size;
|
return size;
|
||||||
var xOffset = 0F;
|
var xOffset = 0F;
|
||||||
for (var i = 0; i < text.Length; i++) {
|
var index = 0;
|
||||||
var font = fontFunction?.Invoke(i) ?? this;
|
while (index < text.Length) {
|
||||||
switch (text[i]) {
|
var (codePoint, length) = text.GetCodePoint(index);
|
||||||
|
switch (codePoint) {
|
||||||
case '\n':
|
case '\n':
|
||||||
xOffset = 0;
|
xOffset = 0;
|
||||||
size.Y += this.LineHeight;
|
size.Y += this.LineHeight;
|
||||||
|
@ -189,107 +191,39 @@ namespace MLEM.Font {
|
||||||
xOffset += this.LineHeight;
|
xOffset += this.LineHeight;
|
||||||
break;
|
break;
|
||||||
case GenericFont.Nbsp:
|
case GenericFont.Nbsp:
|
||||||
xOffset += font.MeasureChar(' ');
|
xOffset += this.MeasureCharacter(' ');
|
||||||
break;
|
break;
|
||||||
case GenericFont.Zwsp:
|
case GenericFont.Zwsp:
|
||||||
// don't add width for a zero-width space
|
// don't add width for a zero-width space
|
||||||
break;
|
break;
|
||||||
case ' ':
|
case ' ':
|
||||||
if (ignoreTrailingSpaces && GenericFont.IsTrailingSpace(text, i)) {
|
if (ignoreTrailingSpaces && GenericFont.IsTrailingSpace(text, index)) {
|
||||||
// if this is a trailing space, we can skip remaining spaces too
|
// if this is a trailing space, we can skip remaining spaces too
|
||||||
i = text.Length - 1;
|
index = text.Length - 1;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
xOffset += font.MeasureChar(' ');
|
xOffset += this.MeasureCharacter(' ');
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
xOffset += font.MeasureChar(text[i]);
|
xOffset += this.MeasureCharacter(codePoint);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// increase x size if this line is the longest
|
// increase x size if this line is the longest
|
||||||
if (xOffset > size.X)
|
if (xOffset > size.X)
|
||||||
size.X = xOffset;
|
size.X = xOffset;
|
||||||
|
index += length;
|
||||||
}
|
}
|
||||||
// include the last line's height too!
|
// include the last line's height too!
|
||||||
size.Y += this.LineHeight;
|
size.Y += this.LineHeight;
|
||||||
return size;
|
return size;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal StringBuilder TruncateString(CharSource text, float width, float scale, bool fromBack, string ellipsis, Func<int, GenericFont> fontFunction) {
|
private void DrawString(SpriteBatch batch, CodePointSource text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth) {
|
||||||
var total = new StringBuilder();
|
|
||||||
for (var i = 0; i < text.Length; i++) {
|
|
||||||
if (fromBack) {
|
|
||||||
total.Insert(0, text[text.Length - 1 - i]);
|
|
||||||
} else {
|
|
||||||
total.Append(text[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
var font = fontFunction?.Invoke(i) ?? this;
|
|
||||||
if (font.MeasureString(total + ellipsis).X * scale >= width) {
|
|
||||||
if (fromBack) {
|
|
||||||
return total.Remove(0, 1).Insert(0, ellipsis);
|
|
||||||
} else {
|
|
||||||
return total.Remove(total.Length - 1, 1).Append(ellipsis);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return total;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal IEnumerable<string> SplitStringSeparate(CharSource text, float width, float scale, Func<int, GenericFont> fontFunction) {
|
|
||||||
var currWidth = 0F;
|
|
||||||
var lastSpaceIndex = -1;
|
|
||||||
var widthSinceLastSpace = 0F;
|
|
||||||
var curr = new StringBuilder();
|
|
||||||
for (var i = 0; i < text.Length; i++) {
|
|
||||||
var c = text[i];
|
|
||||||
if (c == '\n') {
|
|
||||||
// fake split at pre-defined new lines
|
|
||||||
curr.Append(c);
|
|
||||||
lastSpaceIndex = -1;
|
|
||||||
widthSinceLastSpace = 0;
|
|
||||||
currWidth = 0;
|
|
||||||
} else {
|
|
||||||
var font = fontFunction?.Invoke(i) ?? this;
|
|
||||||
var cWidth = font.MeasureString(c.ToCachedString()).X * scale;
|
|
||||||
if (c == ' ' || c == GenericFont.Emsp || c == GenericFont.Zwsp) {
|
|
||||||
// remember the location of this (breaking!) space
|
|
||||||
lastSpaceIndex = curr.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
|
|
||||||
yield return curr.ToString();
|
|
||||||
currWidth = 0;
|
|
||||||
curr.Clear();
|
|
||||||
} else {
|
|
||||||
// split after the last space
|
|
||||||
yield return curr.ToString().Substring(0, lastSpaceIndex + 1);
|
|
||||||
curr.Remove(0, lastSpaceIndex + 1);
|
|
||||||
// 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;
|
|
||||||
curr.Append(c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (curr.Length > 0)
|
|
||||||
yield return curr.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawString(SpriteBatch batch, CharSource text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth) {
|
|
||||||
var (flipX, flipY) = (0F, 0F);
|
var (flipX, flipY) = (0F, 0F);
|
||||||
var flippedV = (effects & SpriteEffects.FlipVertically) != 0;
|
var flippedV = (effects & SpriteEffects.FlipVertically) != 0;
|
||||||
var flippedH = (effects & SpriteEffects.FlipHorizontally) != 0;
|
var flippedH = (effects & SpriteEffects.FlipHorizontally) != 0;
|
||||||
if (flippedV || flippedH) {
|
if (flippedV || flippedH) {
|
||||||
var size = this.MeasureString(text, false, null);
|
var size = this.MeasureString(text, false);
|
||||||
if (flippedH) {
|
if (flippedH) {
|
||||||
origin.X *= -1;
|
origin.X *= -1;
|
||||||
flipX = -size.X;
|
flipX = -size.X;
|
||||||
|
@ -318,53 +252,170 @@ namespace MLEM.Font {
|
||||||
}
|
}
|
||||||
|
|
||||||
var offset = Vector2.Zero;
|
var offset = Vector2.Zero;
|
||||||
for (var i = 0; i < text.Length; i++) {
|
var index = 0;
|
||||||
var c = text[i];
|
while (index < text.Length) {
|
||||||
if (c == '\n') {
|
var (codePoint, length) = text.GetCodePoint(index);
|
||||||
|
if (codePoint == '\n') {
|
||||||
offset.X = 0;
|
offset.X = 0;
|
||||||
offset.Y += this.LineHeight;
|
offset.Y += this.LineHeight;
|
||||||
continue;
|
} else {
|
||||||
|
var character = CodePointSource.ToString(codePoint);
|
||||||
|
var charSize = this.MeasureString(character);
|
||||||
|
|
||||||
|
var charPos = offset;
|
||||||
|
if (flippedH)
|
||||||
|
charPos.X += charSize.X;
|
||||||
|
if (flippedV)
|
||||||
|
charPos.Y += charSize.Y - this.LineHeight;
|
||||||
|
Vector2.Transform(ref charPos, ref trans, out charPos);
|
||||||
|
|
||||||
|
this.DrawCharacter(batch, codePoint, character, charPos, color, rotation, scale, effects, layerDepth);
|
||||||
|
offset.X += charSize.X;
|
||||||
}
|
}
|
||||||
|
index += length;
|
||||||
var cString = c.ToCachedString();
|
|
||||||
var cSize = this.MeasureString(cString);
|
|
||||||
|
|
||||||
var charPos = offset;
|
|
||||||
if (flippedH)
|
|
||||||
charPos.X += cSize.X;
|
|
||||||
if (flippedV)
|
|
||||||
charPos.Y += cSize.Y - this.LineHeight;
|
|
||||||
Vector2.Transform(ref charPos, ref trans, out charPos);
|
|
||||||
|
|
||||||
this.DrawChar(batch, cString, charPos, color, rotation, scale, effects, layerDepth);
|
|
||||||
offset.X += cSize.X;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsTrailingSpace(CharSource s, int index) {
|
internal static IEnumerable<IEnumerable<string>> SplitStringSeparate(IEnumerable<DecoratedCodePointSource> text, float maxWidth, float scale) {
|
||||||
for (var i = index + 1; i < s.Length; i++) {
|
var currWidth = 0F;
|
||||||
if (s[i] != ' ')
|
var lastSpacePart = -1;
|
||||||
|
var lastSpaceIndex = -1;
|
||||||
|
var widthSinceLastSpace = 0F;
|
||||||
|
var curr = new StringBuilder();
|
||||||
|
var fullSplit = new List<List<string>>();
|
||||||
|
foreach (var part in text) {
|
||||||
|
var partSplit = new List<string>();
|
||||||
|
AddWidth(partSplit, part.ExtraWidth * scale, true);
|
||||||
|
|
||||||
|
var index = 0;
|
||||||
|
while (index < part.Source.Length) {
|
||||||
|
var (codePoint, length) = part.Source.GetCodePoint(index);
|
||||||
|
if (codePoint == '\n') {
|
||||||
|
// fake split at pre-defined new lines
|
||||||
|
curr.Append('\n');
|
||||||
|
lastSpacePart = -1;
|
||||||
|
lastSpaceIndex = -1;
|
||||||
|
widthSinceLastSpace = 0;
|
||||||
|
currWidth = 0;
|
||||||
|
} else {
|
||||||
|
var character = CodePointSource.ToString(codePoint);
|
||||||
|
var charWidth = part.Font.MeasureString(character).X * scale;
|
||||||
|
if (codePoint == ' ' || codePoint == GenericFont.Emsp || codePoint == GenericFont.Zwsp) {
|
||||||
|
// remember the location of this (breaking!) space
|
||||||
|
lastSpacePart = fullSplit.Count;
|
||||||
|
lastSpaceIndex = curr.Length;
|
||||||
|
widthSinceLastSpace = 0;
|
||||||
|
// we never want to insert a line break before a space!
|
||||||
|
AddWidth(partSplit, charWidth, false);
|
||||||
|
} else {
|
||||||
|
AddWidth(partSplit, charWidth, true);
|
||||||
|
}
|
||||||
|
curr.Append(character);
|
||||||
|
}
|
||||||
|
index += length;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (curr.Length > 0) {
|
||||||
|
partSplit.Add(curr.ToString());
|
||||||
|
curr.Clear();
|
||||||
|
}
|
||||||
|
fullSplit.Add(partSplit);
|
||||||
|
}
|
||||||
|
return fullSplit;
|
||||||
|
|
||||||
|
void AddWidth(ICollection<string> partSplit, float width, bool canBreakHere) {
|
||||||
|
if (canBreakHere && currWidth + width >= maxWidth) {
|
||||||
|
// 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
|
||||||
|
partSplit.Add(curr.ToString());
|
||||||
|
curr.Clear();
|
||||||
|
currWidth = 0;
|
||||||
|
} else {
|
||||||
|
if (lastSpacePart < fullSplit.Count) {
|
||||||
|
// the last space exists, but isn't a part of curr, so we have to backtrack and split the previous token
|
||||||
|
var prevPart = fullSplit[lastSpacePart];
|
||||||
|
var prevCurr = prevPart[prevPart.Count - 1];
|
||||||
|
prevPart[prevPart.Count - 1] = prevCurr.Substring(0, lastSpaceIndex + 1);
|
||||||
|
prevPart.Add(prevCurr.Substring(lastSpaceIndex + 1));
|
||||||
|
} else {
|
||||||
|
// split after the last space
|
||||||
|
partSplit.Add(curr.ToString().Substring(0, lastSpaceIndex + 1));
|
||||||
|
curr.Remove(0, lastSpaceIndex + 1);
|
||||||
|
}
|
||||||
|
// we need to restore the width accumulated since the last space for the new line
|
||||||
|
currWidth = widthSinceLastSpace;
|
||||||
|
}
|
||||||
|
widthSinceLastSpace = 0;
|
||||||
|
lastSpacePart = -1;
|
||||||
|
lastSpaceIndex = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
currWidth += width;
|
||||||
|
widthSinceLastSpace += width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static IEnumerable<StringBuilder> TruncateString(IEnumerable<DecoratedCodePointSource> text, float maxWidth, float scale, bool fromBack, string ellipsis) {
|
||||||
|
var total = new StringBuilder();
|
||||||
|
var extraWidth = 0F;
|
||||||
|
var endReached = false;
|
||||||
|
foreach (var part in fromBack ? text.Reverse() : text) {
|
||||||
|
var curr = new StringBuilder();
|
||||||
|
// if we reached the end previously, all the other parts should just be empty
|
||||||
|
if (!endReached) {
|
||||||
|
extraWidth += part.ExtraWidth * scale;
|
||||||
|
var index = 0;
|
||||||
|
while (index < part.Source.Length) {
|
||||||
|
var innerIndex = fromBack ? part.Source.Length - 1 - index : index;
|
||||||
|
var (codePoint, length) = part.Source.GetCodePoint(innerIndex, fromBack);
|
||||||
|
var character = CodePointSource.ToString(codePoint);
|
||||||
|
if (fromBack) {
|
||||||
|
curr.Insert(0, character);
|
||||||
|
total.Insert(0, character);
|
||||||
|
} else {
|
||||||
|
curr.Append(character);
|
||||||
|
total.Append(character);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (part.Font.MeasureString(new CodePointSource(total + ellipsis), false).X * scale + extraWidth >= maxWidth) {
|
||||||
|
if (fromBack) {
|
||||||
|
curr.Remove(0, length).Insert(0, ellipsis);
|
||||||
|
total.Remove(0, length).Insert(0, ellipsis);
|
||||||
|
} else {
|
||||||
|
curr.Remove(curr.Length - length, length).Append(ellipsis);
|
||||||
|
total.Remove(total.Length - length, length).Append(ellipsis);
|
||||||
|
}
|
||||||
|
endReached = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
index += length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
yield return curr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsTrailingSpace(CodePointSource s, int index) {
|
||||||
|
while (index < s.Length) {
|
||||||
|
var (codePoint, length) = s.GetCodePoint(index);
|
||||||
|
if (codePoint != ' ')
|
||||||
return false;
|
return false;
|
||||||
|
index += length;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal readonly struct CharSource {
|
internal readonly struct DecoratedCodePointSource {
|
||||||
|
|
||||||
private readonly string strg;
|
public readonly CodePointSource Source;
|
||||||
private readonly StringBuilder builder;
|
public readonly GenericFont Font;
|
||||||
|
public readonly float ExtraWidth;
|
||||||
|
|
||||||
public int Length => this.strg?.Length ?? this.builder.Length;
|
public DecoratedCodePointSource(CodePointSource source, GenericFont font, float extraWidth) {
|
||||||
public char this[int index] => this.strg?[index] ?? this.builder[index];
|
this.Source = source;
|
||||||
|
this.Font = font;
|
||||||
public CharSource(string strg) {
|
this.ExtraWidth = extraWidth;
|
||||||
this.strg = strg;
|
|
||||||
this.builder = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CharSource(StringBuilder builder) {
|
|
||||||
this.strg = null;
|
|
||||||
this.builder = builder;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
using System.Linq;
|
|
||||||
using Microsoft.Xna.Framework;
|
using Microsoft.Xna.Framework;
|
||||||
using Microsoft.Xna.Framework.Graphics;
|
using Microsoft.Xna.Framework.Graphics;
|
||||||
using MLEM.Extensions;
|
|
||||||
|
#if !FNA
|
||||||
|
using System.Linq;
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace MLEM.Font {
|
namespace MLEM.Font {
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
|
@ -32,20 +34,20 @@ namespace MLEM.Font {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override float MeasureChar(char c) {
|
protected override float MeasureCharacter(int codePoint) {
|
||||||
return this.Font.MeasureString(c.ToCachedString()).X;
|
return this.Font.MeasureString(CodePointSource.ToString(codePoint)).X;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override void DrawChar(SpriteBatch batch, string cString, Vector2 position, Color color, float rotation, Vector2 scale, SpriteEffects effects, float layerDepth) {
|
protected override void DrawCharacter(SpriteBatch batch, int codePoint, string character, Vector2 position, Color color, float rotation, Vector2 scale, SpriteEffects effects, float layerDepth) {
|
||||||
batch.DrawString(this.Font, cString, position, color, rotation, Vector2.Zero, scale, effects, layerDepth);
|
batch.DrawString(this.Font, character, position, color, rotation, Vector2.Zero, scale, effects, layerDepth);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static SpriteFont SetDefaults(SpriteFont font) {
|
private static SpriteFont SetDefaults(SpriteFont font) {
|
||||||
#if FNA
|
#if FNA
|
||||||
// none of the copying is available with FNA
|
// none of the copying is available with FNA
|
||||||
return font;
|
return font;
|
||||||
#else
|
#else
|
||||||
// we copy the font here to set the default character to a space
|
// we copy the font here to set the default character to a space
|
||||||
return new SpriteFont(
|
return new SpriteFont(
|
||||||
font.Texture,
|
font.Texture,
|
||||||
|
@ -56,7 +58,7 @@ namespace MLEM.Font {
|
||||||
font.Spacing,
|
font.Spacing,
|
||||||
font.Glyphs.Select(g => new Vector3(g.LeftSideBearing, g.Width, g.RightSideBearing)).ToList(),
|
font.Glyphs.Select(g => new Vector3(g.LeftSideBearing, g.Width, g.RightSideBearing)).ToList(),
|
||||||
' ');
|
' ');
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using Microsoft.Xna.Framework;
|
using Microsoft.Xna.Framework;
|
||||||
|
@ -68,6 +69,11 @@ namespace MLEM.Formatting.Codes {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc cref="Token.GetSelfWidth"/>
|
||||||
|
public virtual float GetSelfWidth(GenericFont font) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Update this formatting code's animations etc.
|
/// Update this formatting code's animations etc.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -80,12 +86,13 @@ namespace MLEM.Formatting.Codes {
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="font">The font that is used</param>
|
/// <param name="font">The font that is used</param>
|
||||||
/// <returns>The replacement string for this formatting code</returns>
|
/// <returns>The replacement string for this formatting code</returns>
|
||||||
|
[Obsolete("This method is deprecated. Use GetSelfWidth to add additional width to this code and DrawSelf or DrawCharacter to draw additional items.")]
|
||||||
public virtual string GetReplacementString(GenericFont font) {
|
public virtual string GetReplacementString(GenericFont font) {
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc cref="Formatting.Token.DrawCharacter"/>
|
/// <inheritdoc cref="Formatting.Token.DrawCharacter"/>
|
||||||
public virtual bool DrawCharacter(GameTime time, SpriteBatch batch, char c, string cString, Token token, int indexInToken, ref Vector2 pos, GenericFont font, ref Color color, ref float scale, float depth) {
|
public virtual bool DrawCharacter(GameTime time, SpriteBatch batch, int codePoint, string character, Token token, int indexInToken, ref Vector2 pos, GenericFont font, ref Color color, ref float scale, float depth) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,8 +26,8 @@ namespace MLEM.Formatting.Codes {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string GetReplacementString(GenericFont font) {
|
public override float GetSelfWidth(GenericFont font) {
|
||||||
return GenericFont.Emsp.ToCachedString();
|
return font.LineHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
@ -41,12 +41,6 @@ namespace MLEM.Formatting.Codes {
|
||||||
batch.Draw(this.image.CurrentRegion, new RectangleF(pos, new Vector2(font.LineHeight * scale)), actualColor);
|
batch.Draw(this.image.CurrentRegion, new RectangleF(pos, new Vector2(font.LineHeight * scale)), actualColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override bool DrawCharacter(GameTime time, SpriteBatch batch, char c, string cString, Token token, int indexInToken, ref Vector2 pos, GenericFont font, ref Color color, ref float scale, float depth) {
|
|
||||||
// we don't want to draw the first (space) character (in case it is set to a missing character in FNA)
|
|
||||||
return indexInToken == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
@ -9,14 +9,19 @@ namespace MLEM.Formatting.Codes {
|
||||||
public class LinkCode : UnderlineCode {
|
public class LinkCode : UnderlineCode {
|
||||||
|
|
||||||
private readonly Func<Token, bool> isSelected;
|
private readonly Func<Token, bool> isSelected;
|
||||||
private readonly Color? color;
|
private readonly Func<Color, Color?> color;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public LinkCode(Match match, Regex regex, float thickness, float yOffset, Func<Token, bool> isSelected, Color? color = null) : base(match, regex, thickness, yOffset) {
|
public LinkCode(Match match, Regex regex, float thickness, float yOffset, Func<Token, bool> isSelected, Func<Color, Color?> color) :
|
||||||
|
base(match, regex, thickness, yOffset) {
|
||||||
this.isSelected = isSelected;
|
this.isSelected = isSelected;
|
||||||
this.color = color;
|
this.color = color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public LinkCode(Match match, Regex regex, float thickness, float yOffset, Func<Token, bool> isSelected, Color? color = null) :
|
||||||
|
this(match, regex, thickness, yOffset, isSelected, d => color) {}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns true if this link formatting code is currently selected or hovered over, based on the selection function.
|
/// Returns true if this link formatting code is currently selected or hovered over, based on the selection function.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -31,13 +36,13 @@ namespace MLEM.Formatting.Codes {
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override Color? GetColor(Color defaultPick) {
|
public override Color? GetColor(Color defaultPick) {
|
||||||
return this.color;
|
return this.color.Invoke(defaultPick);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override bool DrawCharacter(GameTime time, SpriteBatch batch, char c, string cString, Token token, int indexInToken, ref Vector2 pos, GenericFont font, ref Color color, ref float scale, float depth) {
|
public override bool DrawCharacter(GameTime time, SpriteBatch batch, int codePoint, string character, Token token, int indexInToken, ref Vector2 pos, GenericFont font, ref Color color, ref float scale, float depth) {
|
||||||
// since we inherit from UnderlineCode, we can just call base if selected
|
// since we inherit from UnderlineCode, we can just call base if selected
|
||||||
return this.IsSelected() && base.DrawCharacter(time, batch, c, cString, token, indexInToken, ref pos, font, ref color, ref scale, depth);
|
return this.IsSelected() && base.DrawCharacter(time, batch, codePoint, character, token, indexInToken, ref pos, font, ref color, ref scale, depth);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,8 +18,8 @@ namespace MLEM.Formatting.Codes {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override bool DrawCharacter(GameTime time, SpriteBatch batch, char c, string cString, Token token, int indexInToken, ref Vector2 pos, GenericFont font, ref Color color, ref float scale, float depth) {
|
public override bool DrawCharacter(GameTime time, SpriteBatch batch, int codePoint, string character, Token token, int indexInToken, ref Vector2 pos, GenericFont font, ref Color color, ref float scale, float depth) {
|
||||||
font.DrawString(batch, cString, pos + this.offset * scale, this.color.CopyAlpha(color), 0, Vector2.Zero, scale, SpriteEffects.None, depth);
|
font.DrawString(batch, character, pos + this.offset * scale, this.color.CopyAlpha(color), 0, Vector2.Zero, scale, SpriteEffects.None, depth);
|
||||||
// we return false since we still want regular drawing to occur
|
// we return false since we still want regular drawing to occur
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
24
MLEM/Formatting/Codes/SubSupCode.cs
Normal file
24
MLEM/Formatting/Codes/SubSupCode.cs
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using Microsoft.Xna.Framework.Graphics;
|
||||||
|
using MLEM.Font;
|
||||||
|
|
||||||
|
namespace MLEM.Formatting.Codes {
|
||||||
|
/// <inheritdoc />
|
||||||
|
public class SubSupCode : Code {
|
||||||
|
|
||||||
|
private readonly float offset;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public SubSupCode(Match match, Regex regex, float offset) : base(match, regex) {
|
||||||
|
this.offset = offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override bool DrawCharacter(GameTime time, SpriteBatch batch, int codePoint, string character, Token token, int indexInToken, ref Vector2 pos, GenericFont font, ref Color color, ref float scale, float depth) {
|
||||||
|
pos.Y += this.offset * font.LineHeight * scale;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,11 +19,11 @@ namespace MLEM.Formatting.Codes {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override bool DrawCharacter(GameTime time, SpriteBatch batch, char c, string cString, Token token, int indexInToken, ref Vector2 pos, GenericFont font, ref Color color, ref float scale, float depth) {
|
public override bool DrawCharacter(GameTime time, SpriteBatch batch, int codePoint, string character, Token token, int indexInToken, ref Vector2 pos, GenericFont font, ref Color color, ref float scale, float depth) {
|
||||||
// don't underline spaces at the end of lines
|
// don't underline spaces at the end of lines
|
||||||
if (c == ' ' && token.DisplayString.Length > indexInToken + 1 && token.DisplayString[indexInToken + 1] == '\n')
|
if (codePoint == ' ' && token.DisplayString.Length > indexInToken + 1 && token.DisplayString[indexInToken + 1] == '\n')
|
||||||
return false;
|
return false;
|
||||||
var size = font.MeasureString(cString) * scale;
|
var size = font.MeasureString(character) * scale;
|
||||||
var t = size.Y * this.thickness;
|
var t = size.Y * this.thickness;
|
||||||
batch.Draw(batch.GetBlankTexture(), new RectangleF(pos.X, pos.Y + this.yOffset * size.Y - t, size.X, t), color);
|
batch.Draw(batch.GetBlankTexture(), new RectangleF(pos.X, pos.Y + this.yOffset * size.Y - t, size.X, t), color);
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -28,7 +28,7 @@ namespace MLEM.Formatting.Codes {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override bool DrawCharacter(GameTime time, SpriteBatch batch, char c, string cString, Token token, int indexInToken, ref Vector2 pos, GenericFont font, ref Color color, ref float scale, float depth) {
|
public override bool DrawCharacter(GameTime time, SpriteBatch batch, int codePoint, string character, Token token, int indexInToken, ref Vector2 pos, GenericFont font, ref Color color, ref float scale, float depth) {
|
||||||
var offset = new Vector2(0, (float) Math.Sin(token.Index + indexInToken + this.TimeIntoAnimation.TotalSeconds * this.modifier) * font.LineHeight * this.heightModifier * scale);
|
var offset = new Vector2(0, (float) Math.Sin(token.Index + indexInToken + this.TimeIntoAnimation.TotalSeconds * this.modifier) * font.LineHeight * this.heightModifier * scale);
|
||||||
pos += offset;
|
pos += offset;
|
||||||
// we return false since we still want regular drawing to occur, we just changed the position
|
// we return false since we still want regular drawing to occur, we just changed the position
|
||||||
|
|
|
@ -29,38 +29,54 @@ namespace MLEM.Formatting {
|
||||||
public readonly Dictionary<Regex, Macro> Macros = new Dictionary<Regex, Macro>();
|
public readonly Dictionary<Regex, Macro> Macros = new Dictionary<Regex, Macro>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new text formatter with a set of default formatting codes.
|
/// Creates a new text formatter with an optional set of default formatting codes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public TextFormatter() {
|
/// <param name="hasFontModifiers">Whether default font modifier codes should be added, including bold, italic, strikethrough, shadow, subscript, and more.</param>
|
||||||
// font codes
|
/// <param name="hasColors">Whether default color codes should be added, including all <see cref="Color"/> values and the ability to use custom colors.</param>
|
||||||
this.Codes.Add(new Regex("<b>"), (f, m, r) => new FontCode(m, r, fnt => fnt.Bold));
|
/// <param name="hasAnimations">Whether default animation codes should be added, namely the wobbly animation.</param>
|
||||||
this.Codes.Add(new Regex("<i>"), (f, m, r) => new FontCode(m, r, fnt => fnt.Italic));
|
/// <param name="hasMacros">Whether default macros should be added, including TeX's ~ non-breaking space and more.</param>
|
||||||
this.Codes.Add(new Regex(@"<s(?: #([0-9\w]{6,8}) (([+-.0-9]*)))?>"), (f, m, r) => new ShadowCode(m, r,
|
public TextFormatter(bool hasFontModifiers = true, bool hasColors = true, bool hasAnimations = true, bool hasMacros = true) {
|
||||||
m.Groups[1].Success ? ColorHelper.FromHexString(m.Groups[1].Value) : Color.Black,
|
// general font modifier codes
|
||||||
new Vector2(float.TryParse(m.Groups[2].Value, NumberStyles.Number, CultureInfo.InvariantCulture, out var offset) ? offset : 2)));
|
if (hasFontModifiers) {
|
||||||
this.Codes.Add(new Regex("<u>"), (f, m, r) => new UnderlineCode(m, r, 1 / 16F, 0.85F));
|
this.Codes.Add(new Regex("<b>"), (f, m, r) => new FontCode(m, r, fnt => fnt.Bold));
|
||||||
this.Codes.Add(new Regex("<st>"), (f, m, r) => new UnderlineCode(m, r, 1 / 16F, 0.55F));
|
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, 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("<st>"), (f, m, r) => new UnderlineCode(m, r, 1 / 16F, 0.55F));
|
||||||
|
this.Codes.Add(new Regex(@"<sub(?: ([+-.0-9]+))?>"), (f, m, r) => new SubSupCode(m, r,
|
||||||
|
float.TryParse(m.Groups[1].Value, NumberStyles.Number, CultureInfo.InvariantCulture, out var off) ? off : 0.15F));
|
||||||
|
this.Codes.Add(new Regex(@"<sup(?: ([+-.0-9]+))?>"), (f, m, r) => new SubSupCode(m, r,
|
||||||
|
float.TryParse(m.Groups[1].Value, NumberStyles.Number, CultureInfo.InvariantCulture, out var off) ? -off : -0.25F));
|
||||||
|
}
|
||||||
|
|
||||||
// color codes
|
// color codes
|
||||||
foreach (var c in typeof(Color).GetProperties()) {
|
if (hasColors) {
|
||||||
if (c.GetGetMethod().IsStatic) {
|
foreach (var c in typeof(Color).GetProperties()) {
|
||||||
var value = (Color) c.GetValue(null);
|
if (c.GetGetMethod().IsStatic) {
|
||||||
this.Codes.Add(new Regex($"<c {c.Name}>"), (f, m, r) => new ColorCode(m, r, value));
|
var value = (Color) c.GetValue(null);
|
||||||
|
this.Codes.Add(new Regex($"<c {c.Name}>"), (f, m, r) => new ColorCode(m, r, value));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
this.Codes.Add(new Regex(@"<c #([0-9\w]{6,8})>"), (f, m, r) => new ColorCode(m, r, ColorHelper.FromHexString(m.Groups[1].Value)));
|
||||||
}
|
}
|
||||||
this.Codes.Add(new Regex(@"<c #([0-9\w]{6,8})>"), (f, m, r) => new ColorCode(m, r, ColorHelper.FromHexString(m.Groups[1].Value)));
|
|
||||||
|
|
||||||
// animation codes
|
// animation codes
|
||||||
this.Codes.Add(new Regex(@"<a wobbly(?: ([+-.0-9]*) ([+-.0-9]*))?>"), (f, m, r) => new WobblyCode(m, r,
|
if (hasAnimations) {
|
||||||
float.TryParse(m.Groups[1].Value, NumberStyles.Number, CultureInfo.InvariantCulture, out var mod) ? mod : 5,
|
this.Codes.Add(new Regex(@"<a wobbly(?: ([+-.0-9]*) ([+-.0-9]*))?>"), (f, m, r) => new WobblyCode(m, r,
|
||||||
float.TryParse(m.Groups[2].Value, NumberStyles.Number, CultureInfo.InvariantCulture, out var heightMod) ? heightMod : 1 / 8F));
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
// control codes
|
// control codes
|
||||||
this.Codes.Add(new Regex(@"</(\w+)>"), (f, m, r) => new SimpleEndCode(m, r, m.Groups[1].Value));
|
this.Codes.Add(new Regex(@"</(\w+)>"), (f, m, r) => new SimpleEndCode(m, r, m.Groups[1].Value));
|
||||||
|
|
||||||
// macros
|
// macros
|
||||||
this.Macros.Add(new Regex("~"), (f, m, r) => GenericFont.Nbsp.ToCachedString());
|
if (hasMacros) {
|
||||||
this.Macros.Add(new Regex("<n>"), (f, m, r) => '\n'.ToCachedString());
|
this.Macros.Add(new Regex("~"), (f, m, r) => GenericFont.Nbsp.ToString());
|
||||||
|
this.Macros.Add(new Regex("<n>"), (f, m, r) => '\n'.ToString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -68,8 +84,8 @@ namespace MLEM.Formatting {
|
||||||
/// </summary>
|
/// </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="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="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>
|
/// <param name="alignment">The text alignment that should be used. This alignment can later be changed using <see cref="TokenizedString.Realign"/>.</param>
|
||||||
/// <returns></returns>
|
/// <returns>The tokenized string.</returns>
|
||||||
public TokenizedString Tokenize(GenericFont font, string s, TextAlignment alignment = TextAlignment.Left) {
|
public TokenizedString Tokenize(GenericFont font, string s, TextAlignment alignment = TextAlignment.Left) {
|
||||||
// resolve macros
|
// resolve macros
|
||||||
s = this.ResolveMacros(s);
|
s = this.ResolveMacros(s);
|
||||||
|
@ -142,8 +158,11 @@ namespace MLEM.Formatting {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string StripFormatting(GenericFont font, string s, IEnumerable<Code> codes) {
|
private static string StripFormatting(GenericFont font, string s, IEnumerable<Code> codes) {
|
||||||
foreach (var code in codes)
|
foreach (var code in codes) {
|
||||||
|
#pragma warning disable CS0618
|
||||||
s = code.Regex.Replace(s, code.GetReplacementString(font));
|
s = code.Regex.Replace(s, code.GetReplacementString(font));
|
||||||
|
#pragma warning restore CS0618
|
||||||
|
}
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -80,6 +80,19 @@ namespace MLEM.Formatting {
|
||||||
return defaultPick;
|
return defaultPick;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the width of the token itself, including all of the <see cref="Code"/> instances that this token contains.
|
||||||
|
/// Note that this method does not return the width of this token's <see cref="DisplayString"/>, but only the width that the codes themselves take up.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="font">The font to use for calculating the width.</param>
|
||||||
|
/// <returns>The width of this token itself.</returns>
|
||||||
|
public float GetSelfWidth(GenericFont font) {
|
||||||
|
var ret = 0F;
|
||||||
|
foreach (var code in this.AppliedCodes)
|
||||||
|
ret += code.GetSelfWidth(font);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Draws the token itself, including all of the <see cref="Code"/> instances that this token contains.
|
/// Draws the token itself, including all of the <see cref="Code"/> instances that this token contains.
|
||||||
/// Note that, to draw the token's actual string, <see cref="DrawCharacter"/> is used.
|
/// Note that, to draw the token's actual string, <see cref="DrawCharacter"/> is used.
|
||||||
|
@ -97,31 +110,30 @@ namespace MLEM.Formatting {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Draws a given character using this token's formatting options.
|
/// Draws a given code point using this token's formatting options.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="time">The time</param>
|
/// <param name="time">The time</param>
|
||||||
/// <param name="batch">The sprite batch to use</param>
|
/// <param name="batch">The sprite batch to use</param>
|
||||||
/// <param name="c">The character to draw</param>
|
/// <param name="codePoint">The code point of the character to draw</param>
|
||||||
/// <param name="cString">A single-character string that contains the character to draw</param>
|
/// <param name="character">The string representation of the character to draw</param>
|
||||||
/// <param name="indexInToken">The index within this token that the character is at</param>
|
/// <param name="indexInToken">The index within this token that the character is at</param>
|
||||||
/// <param name="pos">The position to draw the token at</param>
|
/// <param name="pos">The position to draw the token at</param>
|
||||||
/// <param name="font">The font to use to draw</param>
|
/// <param name="font">The font to use to draw</param>
|
||||||
/// <param name="color">The color to draw with</param>
|
/// <param name="color">The color to draw with</param>
|
||||||
/// <param name="scale">The scale to draw at</param>
|
/// <param name="scale">The scale to draw at</param>
|
||||||
/// <param name="depth">The depth to draw at</param>
|
/// <param name="depth">The depth to draw at</param>
|
||||||
public void DrawCharacter(GameTime time, SpriteBatch batch, char c, string cString, int indexInToken, Vector2 pos, GenericFont font, Color color, float scale, float depth) {
|
public void DrawCharacter(GameTime time, SpriteBatch batch, int codePoint, string character, int indexInToken, Vector2 pos, GenericFont font, Color color, float scale, float depth) {
|
||||||
foreach (var code in this.AppliedCodes) {
|
foreach (var code in this.AppliedCodes) {
|
||||||
if (code.DrawCharacter(time, batch, c, cString, this, indexInToken, ref pos, font, ref color, ref scale, depth))
|
if (code.DrawCharacter(time, batch, codePoint, character, this, indexInToken, ref pos, font, ref color, ref scale, depth))
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if no code drew, we have to do it ourselves
|
// if no code drew, we have to do it ourselves
|
||||||
font.DrawString(batch, cString, pos, color, 0, Vector2.Zero, scale, SpriteEffects.None, depth);
|
font.DrawString(batch, character, pos, color, 0, Vector2.Zero, scale, SpriteEffects.None, depth);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a list of rectangles that encompass this token's area.
|
/// Gets a list of rectangles that encompass this token's area.
|
||||||
/// Note that more than one rectangle is only returned if the string has been split.
|
|
||||||
/// This can be used to invoke events when the mouse is hovered over the token, for example.
|
/// This can be used to invoke events when the mouse is hovered over the token, for example.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="stringPos">The position that the string is drawn at</param>
|
/// <param name="stringPos">The position that the string is drawn at</param>
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
@ -8,7 +9,6 @@ using MLEM.Extensions;
|
||||||
using MLEM.Font;
|
using MLEM.Font;
|
||||||
using MLEM.Formatting.Codes;
|
using MLEM.Formatting.Codes;
|
||||||
using MLEM.Misc;
|
using MLEM.Misc;
|
||||||
using static MLEM.Font.GenericFont;
|
|
||||||
|
|
||||||
namespace MLEM.Formatting {
|
namespace MLEM.Formatting {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -40,6 +40,7 @@ namespace MLEM.Formatting {
|
||||||
public readonly Code[] AllCodes;
|
public readonly Code[] AllCodes;
|
||||||
private string modifiedString;
|
private string modifiedString;
|
||||||
private float initialInnerOffset;
|
private float initialInnerOffset;
|
||||||
|
private RectangleF area;
|
||||||
|
|
||||||
internal TokenizedString(GenericFont font, TextAlignment alignment, string rawString, string strg, Token[] tokens) {
|
internal TokenizedString(GenericFont font, TextAlignment alignment, string rawString, string strg, Token[] tokens) {
|
||||||
this.RawString = rawString;
|
this.RawString = rawString;
|
||||||
|
@ -52,7 +53,7 @@ namespace MLEM.Formatting {
|
||||||
foreach (var code in this.AllCodes)
|
foreach (var code in this.AllCodes)
|
||||||
code.Tokens = new ReadOnlyCollection<Token>(this.Tokens.Where(t => t.AppliedCodes.Contains(code)).ToList());
|
code.Tokens = new ReadOnlyCollection<Token>(this.Tokens.Where(t => t.AppliedCodes.Contains(code)).ToList());
|
||||||
|
|
||||||
this.RecalculateTokenData(font, alignment);
|
this.Realign(font, alignment);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -64,9 +65,16 @@ namespace MLEM.Formatting {
|
||||||
/// <param name="scale">The scale to use for width measurements</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>
|
/// <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) {
|
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
|
var index = 0;
|
||||||
this.modifiedString = string.Join("\n", font.SplitStringSeparate(new CharSource(this.String), width, scale, i => this.GetFontForIndex(font, i)));
|
var modified = new StringBuilder();
|
||||||
this.StoreModifiedSubstrings(font, alignment);
|
foreach (var part in GenericFont.SplitStringSeparate(this.AsDecoratedSources(font), width, scale)) {
|
||||||
|
var joined = string.Join("\n", part);
|
||||||
|
this.Tokens[index].ModifiedSubstring = joined;
|
||||||
|
modified.Append(joined);
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
this.modifiedString = modified.ToString();
|
||||||
|
this.Realign(font, alignment);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -80,13 +88,77 @@ namespace MLEM.Formatting {
|
||||||
/// <param name="ellipsis">The characters to add to the end of the string if it is too long</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>
|
/// <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) {
|
public void Truncate(GenericFont font, float width, float scale, string ellipsis = "", TextAlignment alignment = TextAlignment.Left) {
|
||||||
this.modifiedString = font.TruncateString(new CharSource(this.String), width, scale, false, ellipsis, i => this.GetFontForIndex(font, i)).ToString();
|
var index = 0;
|
||||||
this.StoreModifiedSubstrings(font, alignment);
|
var modified = new StringBuilder();
|
||||||
|
foreach (var part in GenericFont.TruncateString(this.AsDecoratedSources(font), width, scale, false, ellipsis)) {
|
||||||
|
this.Tokens[index].ModifiedSubstring = part.ToString();
|
||||||
|
modified.Append(part);
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
this.modifiedString = modified.ToString();
|
||||||
|
this.Realign(font, alignment);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Realigns this tokenized string using the given <see cref="TextAlignment"/>.
|
||||||
|
/// If the <paramref name="alignment"/> is <see cref="TextAlignment.Right"/>, trailing space characters (but not <see cref="GenericFont.Nbsp"/>) will be removed.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="font">The font to use for width calculations.</param>
|
||||||
|
/// <param name="alignment">The text alignment that should be used for width calculations.</param>
|
||||||
|
public void Realign(GenericFont font, TextAlignment alignment) {
|
||||||
|
// split display strings
|
||||||
|
foreach (var token in this.Tokens)
|
||||||
|
token.SplitDisplayString = token.DisplayString.Split('\n');
|
||||||
|
|
||||||
|
// token areas and inner offsets
|
||||||
|
this.area = RectangleF.Empty;
|
||||||
|
this.initialInnerOffset = this.GetInnerOffsetX(font, 0, 0, alignment);
|
||||||
|
var innerOffset = new Vector2(this.initialInnerOffset, 0);
|
||||||
|
for (var t = 0; t < this.Tokens.Length; t++) {
|
||||||
|
var token = this.Tokens[t];
|
||||||
|
var tokenFont = token.GetFont(font);
|
||||||
|
token.InnerOffsets = new float[token.SplitDisplayString.Length - 1];
|
||||||
|
|
||||||
|
var tokenArea = new List<RectangleF>();
|
||||||
|
var selfRect = new RectangleF(innerOffset, new Vector2(token.GetSelfWidth(tokenFont), tokenFont.LineHeight));
|
||||||
|
if (!selfRect.IsEmpty) {
|
||||||
|
tokenArea.Add(selfRect);
|
||||||
|
this.area = RectangleF.Union(this.area, selfRect);
|
||||||
|
innerOffset.X += selfRect.Width;
|
||||||
|
}
|
||||||
|
for (var l = 0; l < token.SplitDisplayString.Length; l++) {
|
||||||
|
var size = tokenFont.MeasureString(token.SplitDisplayString[l], !this.EndsLater(t, l));
|
||||||
|
var rect = new RectangleF(innerOffset, size);
|
||||||
|
if (!rect.IsEmpty) {
|
||||||
|
tokenArea.Add(rect);
|
||||||
|
this.area = RectangleF.Union(this.area, rect);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (l < token.SplitDisplayString.Length - 1) {
|
||||||
|
innerOffset.X = token.InnerOffsets[l] = this.GetInnerOffsetX(font, t, l + 1, alignment);
|
||||||
|
innerOffset.Y += tokenFont.LineHeight;
|
||||||
|
} else {
|
||||||
|
innerOffset.X += size.X;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
token.Area = tokenArea.ToArray();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc cref="GenericFont.MeasureString(string,bool)"/>
|
/// <inheritdoc cref="GenericFont.MeasureString(string,bool)"/>
|
||||||
|
[Obsolete("Measure is deprecated. Use GetArea, which returns the string's total size measurement, instead.")]
|
||||||
public Vector2 Measure(GenericFont font) {
|
public Vector2 Measure(GenericFont font) {
|
||||||
return font.MeasureString(new CharSource(this.DisplayString), false, i => this.GetFontForIndex(font, i));
|
return this.GetArea(Vector2.Zero, 1).Size;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Measures the area that this entire tokenized string and all of its <see cref="Tokens"/> take up and returns it as a <see cref="RectangleF"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="stringPos">The position that this string is being rendered at, which will offset the resulting <see cref="RectangleF"/>.</param>
|
||||||
|
/// <param name="scale">The scale that this string is being rendered with, which will scale the resulting <see cref="RectangleF"/>.</param>
|
||||||
|
/// <returns>The area that this tokenized string takes up.</returns>
|
||||||
|
public RectangleF GetArea(Vector2 stringPos, float scale) {
|
||||||
|
return new RectangleF(stringPos + this.area.Location * scale, this.area.Size * scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -124,115 +196,49 @@ namespace MLEM.Formatting {
|
||||||
var drawFont = token.GetFont(font);
|
var drawFont = token.GetFont(font);
|
||||||
var drawColor = token.GetColor(color);
|
var drawColor = token.GetColor(color);
|
||||||
|
|
||||||
|
token.DrawSelf(time, batch, pos + innerOffset, drawFont, drawColor, scale, depth);
|
||||||
|
innerOffset.X += token.GetSelfWidth(drawFont) * scale;
|
||||||
|
|
||||||
var indexInToken = 0;
|
var indexInToken = 0;
|
||||||
for (var l = 0; l < token.SplitDisplayString.Length; l++) {
|
for (var l = 0; l < token.SplitDisplayString.Length; l++) {
|
||||||
foreach (var c in token.SplitDisplayString[l]) {
|
var charIndex = 0;
|
||||||
var cString = c.ToCachedString();
|
var line = new CodePointSource(token.SplitDisplayString[l]);
|
||||||
|
while (charIndex < line.Length) {
|
||||||
|
var (codePoint, length) = line.GetCodePoint(charIndex);
|
||||||
|
var character = CodePointSource.ToString(codePoint);
|
||||||
|
|
||||||
if (indexInToken == 0)
|
token.DrawCharacter(time, batch, codePoint, character, indexInToken, pos + innerOffset, drawFont, drawColor, scale, depth);
|
||||||
token.DrawSelf(time, batch, pos + innerOffset, drawFont, color, scale, depth);
|
|
||||||
token.DrawCharacter(time, batch, c, cString, indexInToken, pos + innerOffset, drawFont, drawColor, scale, depth);
|
|
||||||
|
|
||||||
innerOffset.X += drawFont.MeasureString(cString).X * scale;
|
innerOffset.X += drawFont.MeasureString(character).X * scale;
|
||||||
|
charIndex += length;
|
||||||
indexInToken++;
|
indexInToken++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// only split at a new line, not between tokens!
|
// only split at a new line, not between tokens!
|
||||||
if (l < token.SplitDisplayString.Length - 1) {
|
if (l < token.SplitDisplayString.Length - 1) {
|
||||||
innerOffset.X = token.InnerOffsets[l] * scale;
|
innerOffset.X = token.InnerOffsets[l] * scale;
|
||||||
innerOffset.Y += font.LineHeight * scale;
|
innerOffset.Y += drawFont.LineHeight * scale;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 void RecalculateTokenData(GenericFont font, TextAlignment alignment) {
|
|
||||||
// split display strings
|
|
||||||
foreach (var token in this.Tokens)
|
|
||||||
token.SplitDisplayString = token.DisplayString.Split('\n');
|
|
||||||
|
|
||||||
// token areas and inner offsets
|
|
||||||
this.initialInnerOffset = this.GetInnerOffsetX(font, 0, 0, alignment);
|
|
||||||
var innerOffset = new Vector2(this.initialInnerOffset, 0);
|
|
||||||
for (var t = 0; t < this.Tokens.Length; t++) {
|
|
||||||
var token = this.Tokens[t];
|
|
||||||
var tokenFont = token.GetFont(font);
|
|
||||||
token.InnerOffsets = new float[token.SplitDisplayString.Length - 1];
|
|
||||||
var area = new List<RectangleF>();
|
|
||||||
for (var l = 0; l < token.SplitDisplayString.Length; l++) {
|
|
||||||
var size = tokenFont.MeasureString(token.SplitDisplayString[l]);
|
|
||||||
var rect = new RectangleF(innerOffset, size);
|
|
||||||
if (!rect.IsEmpty)
|
|
||||||
area.Add(rect);
|
|
||||||
|
|
||||||
if (l < token.SplitDisplayString.Length - 1) {
|
|
||||||
innerOffset.X = token.InnerOffsets[l] = this.GetInnerOffsetX(font, t, l + 1, alignment);
|
|
||||||
innerOffset.Y += font.LineHeight;
|
|
||||||
} else {
|
|
||||||
innerOffset.X += size.X;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
token.Area = area.ToArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private float GetInnerOffsetX(GenericFont defaultFont, int tokenIndex, int lineIndex, TextAlignment alignment) {
|
private float GetInnerOffsetX(GenericFont defaultFont, int tokenIndex, int lineIndex, TextAlignment alignment) {
|
||||||
if (alignment > TextAlignment.Left) {
|
if (alignment > TextAlignment.Left) {
|
||||||
var token = this.Tokens[tokenIndex];
|
var token = this.Tokens[tokenIndex];
|
||||||
var tokenFont = token.GetFont(defaultFont);
|
var tokenFont = token.GetFont(defaultFont);
|
||||||
// if we're the last line in our line array, then we don't contain a line split, so the line ends in a later token
|
var tokenWidth = lineIndex <= 0 ? token.GetSelfWidth(tokenFont) : 0;
|
||||||
var endsLater = lineIndex >= token.SplitDisplayString.Length - 1;
|
var endsLater = this.EndsLater(tokenIndex, lineIndex);
|
||||||
// if the line ends in our token, we should ignore trailing white space
|
// if the line ends in our token, we should ignore trailing white space
|
||||||
var restOfLine = tokenFont.MeasureString(token.SplitDisplayString[lineIndex], !endsLater).X;
|
var restOfLine = tokenFont.MeasureString(token.SplitDisplayString[lineIndex], !endsLater).X + tokenWidth;
|
||||||
if (endsLater) {
|
if (endsLater) {
|
||||||
for (var i = tokenIndex + 1; i < this.Tokens.Length; i++) {
|
for (var i = tokenIndex + 1; i < this.Tokens.Length; i++) {
|
||||||
var other = this.Tokens[i];
|
var other = this.Tokens[i];
|
||||||
var otherFont = other.GetFont(defaultFont);
|
var otherFont = other.GetFont(defaultFont);
|
||||||
if (other.SplitDisplayString.Length > 1) {
|
restOfLine += otherFont.MeasureString(other.SplitDisplayString[0], !this.EndsLater(i, 0)).X + other.GetSelfWidth(otherFont);
|
||||||
// the line ends in this token (so we also ignore trailing whitespaces)
|
// if the token's split display string has multiple lines, then the line ends in it, which means we can stop
|
||||||
restOfLine += otherFont.MeasureString(other.SplitDisplayString[0], true).X;
|
if (other.SplitDisplayString.Length > 1)
|
||||||
break;
|
break;
|
||||||
} else {
|
|
||||||
// the line doesn't end in this token (or it's the last token), so add it fully
|
|
||||||
var lastToken = i >= this.Tokens.Length - 1;
|
|
||||||
restOfLine += otherFont.MeasureString(other.DisplayString, lastToken).X;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (alignment == TextAlignment.Center)
|
if (alignment == TextAlignment.Center)
|
||||||
|
@ -242,13 +248,16 @@ namespace MLEM.Formatting {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private GenericFont GetFontForIndex(GenericFont font, int index) {
|
private bool EndsLater(int tokenIndex, int lineIndex) {
|
||||||
foreach (var token in this.Tokens) {
|
// if we're the last line in our line array, then we don't contain a line split, so the line ends in a later token
|
||||||
index -= token.Substring.Length;
|
return lineIndex >= this.Tokens[tokenIndex].SplitDisplayString.Length - 1 && tokenIndex < this.Tokens.Length - 1;
|
||||||
if (index <= 0)
|
}
|
||||||
return token.GetFont(font);
|
|
||||||
}
|
private IEnumerable<GenericFont.DecoratedCodePointSource> AsDecoratedSources(GenericFont font) {
|
||||||
return null;
|
return this.Tokens.Select(t => {
|
||||||
|
var tokenFont = t.GetFont(font);
|
||||||
|
return new GenericFont.DecoratedCodePointSource(new CodePointSource(t.Substring), tokenFont, t.GetSelfWidth(tokenFont));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Microsoft.Xna.Framework;
|
using Microsoft.Xna.Framework;
|
||||||
using Microsoft.Xna.Framework.Graphics;
|
using Microsoft.Xna.Framework.Graphics;
|
||||||
|
@ -5,8 +6,8 @@ using MLEM.Textures;
|
||||||
|
|
||||||
namespace MLEM.Graphics {
|
namespace MLEM.Graphics {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// This class contains a <see cref="DrawAutoTile"/> method that allows users to easily draw a tile with automatic connections, as well as a more complex <see cref="DrawExtendedAutoTile"/> method.
|
/// This class contains a <see cref="DrawAutoTile"/> method that allows users to easily draw a tile with automatic connections, as well as a more complex <see cref="DrawExtendedAutoTile(Microsoft.Xna.Framework.Graphics.SpriteBatch,Microsoft.Xna.Framework.Vector2,MLEM.Textures.TextureRegion,MLEM.Textures.TextureRegion,MLEM.Graphics.AutoTiling.ConnectsTo,Microsoft.Xna.Framework.Color,Microsoft.Xna.Framework.Color,System.Nullable{Microsoft.Xna.Framework.Vector2},System.Nullable{Microsoft.Xna.Framework.Vector2},float,float)"/> method.
|
||||||
/// Note that <see cref="StaticSpriteBatch"/> can also be used for drawing by using the <see cref="AddAutoTile"/> and <see cref="AddExtendedAutoTile"/> methods instead.
|
/// Note that <see cref="StaticSpriteBatch"/> can also be used for drawing by using the <see cref="AddAutoTile"/> and <see cref="AddExtendedAutoTile(MLEM.Graphics.StaticSpriteBatch,Microsoft.Xna.Framework.Vector2,MLEM.Textures.TextureRegion,MLEM.Textures.TextureRegion,MLEM.Graphics.AutoTiling.ConnectsTo,Microsoft.Xna.Framework.Color,Microsoft.Xna.Framework.Color,System.Nullable{Microsoft.Xna.Framework.Vector2},System.Nullable{Microsoft.Xna.Framework.Vector2},float,float,System.Collections.Generic.ICollection{MLEM.Graphics.StaticSpriteBatch.Item})"/> methods instead.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class AutoTiling {
|
public static class AutoTiling {
|
||||||
|
|
||||||
|
@ -88,7 +89,7 @@ namespace MLEM.Graphics {
|
||||||
var orig = origin ?? Vector2.Zero;
|
var orig = origin ?? Vector2.Zero;
|
||||||
var sc = scale ?? Vector2.One;
|
var sc = scale ?? Vector2.One;
|
||||||
var od = layerDepth + overlayDepthOffset;
|
var od = layerDepth + overlayDepthOffset;
|
||||||
var (r1, r2, r3, r4) = AutoTiling.CalculateExtendedAutoTile(pos, overlayTexture.Area, connectsTo, sc);
|
var (r1, r2, r3, r4) = AutoTiling.CalculateExtendedAutoTile(overlayTexture.Area, connectsTo);
|
||||||
if (backgroundTexture != null)
|
if (backgroundTexture != null)
|
||||||
batch.Draw(backgroundTexture, pos, backgroundColor, 0, orig, sc, SpriteEffects.None, layerDepth);
|
batch.Draw(backgroundTexture, pos, backgroundColor, 0, orig, sc, SpriteEffects.None, layerDepth);
|
||||||
if (r1 != Rectangle.Empty)
|
if (r1 != Rectangle.Empty)
|
||||||
|
@ -101,12 +102,30 @@ namespace MLEM.Graphics {
|
||||||
batch.Draw(overlayTexture.Texture, pos, r4, overlayColor, 0, orig, sc, SpriteEffects.None, od);
|
batch.Draw(overlayTexture.Texture, pos, r4, overlayColor, 0, orig, sc, SpriteEffects.None, od);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc cref="DrawExtendedAutoTile"/>
|
/// <inheritdoc cref="DrawExtendedAutoTile(Microsoft.Xna.Framework.Graphics.SpriteBatch,Microsoft.Xna.Framework.Vector2,MLEM.Textures.TextureRegion,MLEM.Textures.TextureRegion,MLEM.Graphics.AutoTiling.ConnectsTo,Microsoft.Xna.Framework.Color,Microsoft.Xna.Framework.Color,System.Nullable{Microsoft.Xna.Framework.Vector2},System.Nullable{Microsoft.Xna.Framework.Vector2},float,float)"/>
|
||||||
|
public static void DrawExtendedAutoTile(SpriteBatch batch, Vector2 pos, TextureRegion backgroundTexture, Func<int, TextureRegion> overlayTextures, ConnectsTo connectsTo, Color backgroundColor, Color overlayColor, Vector2? origin = null, Vector2? scale = null, float layerDepth = 0, float overlayDepthOffset = 0) {
|
||||||
|
var orig = origin ?? Vector2.Zero;
|
||||||
|
var sc = scale ?? Vector2.One;
|
||||||
|
var od = layerDepth + overlayDepthOffset;
|
||||||
|
var (xUl, xUr, xDl, xDr) = AutoTiling.CalculateExtendedAutoTileOffsets(connectsTo);
|
||||||
|
if (backgroundTexture != null)
|
||||||
|
batch.Draw(backgroundTexture, pos, backgroundColor, 0, orig, sc, SpriteEffects.None, layerDepth);
|
||||||
|
if (xUl >= 0)
|
||||||
|
batch.Draw(overlayTextures(xUl), pos, overlayColor, 0, orig, sc, SpriteEffects.None, od);
|
||||||
|
if (xUr >= 0)
|
||||||
|
batch.Draw(overlayTextures(xUr), pos, overlayColor, 0, orig, sc, SpriteEffects.None, od);
|
||||||
|
if (xDl >= 0)
|
||||||
|
batch.Draw(overlayTextures(xDl), pos, overlayColor, 0, orig, sc, SpriteEffects.None, od);
|
||||||
|
if (xDr >= 0)
|
||||||
|
batch.Draw(overlayTextures(xDr), pos, overlayColor, 0, orig, sc, SpriteEffects.None, od);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc cref="DrawExtendedAutoTile(Microsoft.Xna.Framework.Graphics.SpriteBatch,Microsoft.Xna.Framework.Vector2,MLEM.Textures.TextureRegion,MLEM.Textures.TextureRegion,MLEM.Graphics.AutoTiling.ConnectsTo,Microsoft.Xna.Framework.Color,Microsoft.Xna.Framework.Color,System.Nullable{Microsoft.Xna.Framework.Vector2},System.Nullable{Microsoft.Xna.Framework.Vector2},float,float)"/>
|
||||||
public static void AddExtendedAutoTile(StaticSpriteBatch batch, Vector2 pos, TextureRegion backgroundTexture, TextureRegion overlayTexture, ConnectsTo connectsTo, Color backgroundColor, Color overlayColor, Vector2? origin = null, Vector2? scale = null, float layerDepth = 0, float overlayDepthOffset = 0, ICollection<StaticSpriteBatch.Item> items = null) {
|
public static void AddExtendedAutoTile(StaticSpriteBatch batch, Vector2 pos, TextureRegion backgroundTexture, TextureRegion overlayTexture, ConnectsTo connectsTo, Color backgroundColor, Color overlayColor, Vector2? origin = null, Vector2? scale = null, float layerDepth = 0, float overlayDepthOffset = 0, ICollection<StaticSpriteBatch.Item> items = null) {
|
||||||
var orig = origin ?? Vector2.Zero;
|
var orig = origin ?? Vector2.Zero;
|
||||||
var sc = scale ?? Vector2.One;
|
var sc = scale ?? Vector2.One;
|
||||||
var od = layerDepth + overlayDepthOffset;
|
var od = layerDepth + overlayDepthOffset;
|
||||||
var (r1, r2, r3, r4) = AutoTiling.CalculateExtendedAutoTile(pos, overlayTexture.Area, connectsTo, sc);
|
var (r1, r2, r3, r4) = AutoTiling.CalculateExtendedAutoTile(overlayTexture.Area, connectsTo);
|
||||||
if (backgroundTexture != null) {
|
if (backgroundTexture != null) {
|
||||||
var background = batch.Add(backgroundTexture, pos, backgroundColor, 0, orig, sc, SpriteEffects.None, layerDepth);
|
var background = batch.Add(backgroundTexture, pos, backgroundColor, 0, orig, sc, SpriteEffects.None, layerDepth);
|
||||||
items?.Add(background);
|
items?.Add(background);
|
||||||
|
@ -129,6 +148,34 @@ namespace MLEM.Graphics {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc cref="DrawExtendedAutoTile(Microsoft.Xna.Framework.Graphics.SpriteBatch,Microsoft.Xna.Framework.Vector2,MLEM.Textures.TextureRegion,Func{int,MLEM.Textures.TextureRegion},MLEM.Graphics.AutoTiling.ConnectsTo,Microsoft.Xna.Framework.Color,Microsoft.Xna.Framework.Color,System.Nullable{Microsoft.Xna.Framework.Vector2},System.Nullable{Microsoft.Xna.Framework.Vector2},float,float)"/>
|
||||||
|
public static void AddExtendedAutoTile(StaticSpriteBatch batch, Vector2 pos, TextureRegion backgroundTexture, Func<int, TextureRegion> overlayTextures, ConnectsTo connectsTo, Color backgroundColor, Color overlayColor, Vector2? origin = null, Vector2? scale = null, float layerDepth = 0, float overlayDepthOffset = 0, ICollection<StaticSpriteBatch.Item> items = null) {
|
||||||
|
var orig = origin ?? Vector2.Zero;
|
||||||
|
var sc = scale ?? Vector2.One;
|
||||||
|
var od = layerDepth + overlayDepthOffset;
|
||||||
|
var (xUl, xUr, xDl, xDr) = AutoTiling.CalculateExtendedAutoTileOffsets(connectsTo);
|
||||||
|
if (backgroundTexture != null) {
|
||||||
|
var background = batch.Add(backgroundTexture, pos, backgroundColor, 0, orig, sc, SpriteEffects.None, layerDepth);
|
||||||
|
items?.Add(background);
|
||||||
|
}
|
||||||
|
if (xUl >= 0) {
|
||||||
|
var o1 = batch.Add(overlayTextures(xUl), pos, overlayColor, 0, orig, sc, SpriteEffects.None, od);
|
||||||
|
items?.Add(o1);
|
||||||
|
}
|
||||||
|
if (xUr >= 0) {
|
||||||
|
var o2 = batch.Add(overlayTextures(xUr), pos, overlayColor, 0, orig, sc, SpriteEffects.None, od);
|
||||||
|
items?.Add(o2);
|
||||||
|
}
|
||||||
|
if (xDl >= 0) {
|
||||||
|
var o3 = batch.Add(overlayTextures(xDl), pos, overlayColor, 0, orig, sc, SpriteEffects.None, od);
|
||||||
|
items?.Add(o3);
|
||||||
|
}
|
||||||
|
if (xDr >= 0) {
|
||||||
|
var o4 = batch.Add(overlayTextures(xDr), pos, overlayColor, 0, orig, sc, SpriteEffects.None, od);
|
||||||
|
items?.Add(o4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static (Vector2, Rectangle, Vector2, Rectangle, Vector2, Rectangle, Vector2, Rectangle) CalculateAutoTile(Vector2 pos, Rectangle textureRegion, ConnectsTo connectsTo, Vector2 scale) {
|
private static (Vector2, Rectangle, Vector2, Rectangle, Vector2, Rectangle, Vector2, Rectangle) CalculateAutoTile(Vector2 pos, Rectangle textureRegion, ConnectsTo connectsTo, Vector2 scale) {
|
||||||
var up = connectsTo(0, -1);
|
var up = connectsTo(0, -1);
|
||||||
var down = connectsTo(0, 1);
|
var down = connectsTo(0, 1);
|
||||||
|
@ -147,15 +194,20 @@ namespace MLEM.Graphics {
|
||||||
new Vector2(pos.X + w2 * scale.X, pos.Y + h2 * scale.Y), new Rectangle(textureRegion.X + w2 + xDr * w, textureRegion.Y + h2, w2, h2));
|
new Vector2(pos.X + w2 * scale.X, pos.Y + h2 * scale.Y), new Rectangle(textureRegion.X + w2 + xDr * w, textureRegion.Y + h2, w2, h2));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static (Rectangle, Rectangle, Rectangle, Rectangle) CalculateExtendedAutoTile(Vector2 pos, Rectangle textureRegion, ConnectsTo connectsTo, Vector2 scale) {
|
private static (int, int, int, int) CalculateExtendedAutoTileOffsets(ConnectsTo connectsTo) {
|
||||||
var up = connectsTo(0, -1);
|
var up = connectsTo(0, -1);
|
||||||
var down = connectsTo(0, 1);
|
var down = connectsTo(0, 1);
|
||||||
var left = connectsTo(-1, 0);
|
var left = connectsTo(-1, 0);
|
||||||
var right = connectsTo(1, 0);
|
var right = connectsTo(1, 0);
|
||||||
var xUl = up && left ? connectsTo(-1, -1) ? -1 : 12 : left ? 0 : up ? 8 : 4;
|
return (
|
||||||
var xUr = up && right ? connectsTo(1, -1) ? -1 : 13 : right ? 1 : up ? 9 : 5;
|
up && left ? connectsTo(-1, -1) ? -1 : 12 : left ? 0 : up ? 8 : 4,
|
||||||
var xDl = down && left ? connectsTo(-1, 1) ? -1 : 14 : left ? 2 : down ? 10 : 6;
|
up && right ? connectsTo(1, -1) ? -1 : 13 : right ? 1 : up ? 9 : 5,
|
||||||
var xDr = down && right ? connectsTo(1, 1) ? -1 : 15 : right ? 3 : down ? 11 : 7;
|
down && left ? connectsTo(-1, 1) ? -1 : 14 : left ? 2 : down ? 10 : 6,
|
||||||
|
down && right ? connectsTo(1, 1) ? -1 : 15 : right ? 3 : down ? 11 : 7);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (Rectangle, Rectangle, Rectangle, Rectangle) CalculateExtendedAutoTile(Rectangle textureRegion, ConnectsTo connectsTo) {
|
||||||
|
var (xUl, xUr, xDl, xDr) = AutoTiling.CalculateExtendedAutoTileOffsets(connectsTo);
|
||||||
var (w, h) = (textureRegion.Width, textureRegion.Height);
|
var (w, h) = (textureRegion.Width, textureRegion.Height);
|
||||||
return (
|
return (
|
||||||
xUl < 0 ? Rectangle.Empty : new Rectangle(textureRegion.X + xUl * w, textureRegion.Y, w, h),
|
xUl < 0 ? Rectangle.Empty : new Rectangle(textureRegion.X + xUl * w, textureRegion.Y, w, h),
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Microsoft.Xna.Framework;
|
using Microsoft.Xna.Framework;
|
||||||
using Microsoft.Xna.Framework.Graphics;
|
using Microsoft.Xna.Framework.Graphics;
|
||||||
|
#if FNA
|
||||||
using MLEM.Extensions;
|
using MLEM.Extensions;
|
||||||
|
using System.IO;
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace MLEM.Graphics {
|
namespace MLEM.Graphics {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A static sprite batch is a variation of <see cref="SpriteBatch"/> that keeps all batched items in a <see cref="VertexBuffer"/>, allowing for them to be drawn multiple times.
|
/// A static sprite batch is a highly optimized variation of <see cref="SpriteBatch"/> that keeps all batched items in a <see cref="VertexBuffer"/>, allowing for them to be drawn multiple times.
|
||||||
/// To add items to a static sprite batch, use <see cref="BeginBatch"/> to begin batching, <see cref="ClearBatch"/> to clear currently batched items, <c>Add</c> and its various overloads to add batch items, <see cref="Remove"/> to remove them again, and <see cref="EndBatch"/> to end batching.
|
/// To add items to a static sprite batch, use <see cref="BeginBatch"/> to begin batching, <see cref="ClearBatch"/> to clear currently batched items, <c>Add</c> and its various overloads to add batch items, <see cref="Remove"/> to remove them again, and <see cref="EndBatch"/> to end batching.
|
||||||
/// To draw the batched items, call <see cref="Draw"/>.
|
/// To draw the batched items, call <see cref="Draw"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -21,7 +23,7 @@ namespace MLEM.Graphics {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The amount of vertices that are currently batched.
|
/// The amount of vertices that are currently batched.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int Vertices => this.items.Count * 4;
|
public int Vertices => this.itemAmount * 4;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The amount of vertex buffers that this static sprite batch has.
|
/// The amount of vertex buffers that this static sprite batch has.
|
||||||
/// To see the amount of buffers that are actually in use, see <see cref="FilledBuffers"/>.
|
/// To see the amount of buffers that are actually in use, see <see cref="FilledBuffers"/>.
|
||||||
|
@ -39,13 +41,15 @@ namespace MLEM.Graphics {
|
||||||
|
|
||||||
private readonly GraphicsDevice graphicsDevice;
|
private readonly GraphicsDevice graphicsDevice;
|
||||||
private readonly SpriteEffect spriteEffect;
|
private readonly SpriteEffect spriteEffect;
|
||||||
|
private readonly List<DynamicVertexBuffer> vertexBuffers = new List<DynamicVertexBuffer>();
|
||||||
private readonly List<VertexBuffer> vertexBuffers = new List<VertexBuffer>();
|
|
||||||
private readonly List<Texture2D> textures = new List<Texture2D>();
|
private readonly List<Texture2D> textures = new List<Texture2D>();
|
||||||
private readonly ISet<Item> items = new HashSet<Item>();
|
private readonly SortedDictionary<float, ItemSet> items = new SortedDictionary<float, ItemSet>();
|
||||||
|
|
||||||
|
private SpriteSortMode sortMode = SpriteSortMode.Texture;
|
||||||
private IndexBuffer indices;
|
private IndexBuffer indices;
|
||||||
private bool batching;
|
private bool batching;
|
||||||
private bool batchChanged;
|
private bool batchChanged;
|
||||||
|
private int itemAmount;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new static sprite batch with the given <see cref="GraphicsDevice"/>
|
/// Creates a new static sprite batch with the given <see cref="GraphicsDevice"/>
|
||||||
|
@ -60,10 +64,27 @@ namespace MLEM.Graphics {
|
||||||
/// Begins batching.
|
/// Begins batching.
|
||||||
/// Call this method before calling <c>Add</c> or any of its overloads.
|
/// Call this method before calling <c>Add</c> or any of its overloads.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="sortMode">The drawing order for sprite drawing. When <see langword="null"/> is passed, the last used sort mode will be used again. The initial sort mode is <see cref="SpriteSortMode.Texture"/>. Note that <see cref="SpriteSortMode.Immediate"/> is not supported.</param>
|
||||||
/// <exception cref="InvalidOperationException">Thrown if this batch is currently batching already</exception>
|
/// <exception cref="InvalidOperationException">Thrown if this batch is currently batching already</exception>
|
||||||
public void BeginBatch() {
|
/// <exception cref="ArgumentOutOfRangeException">Thrown if the <paramref name="sortMode"/> is <see cref="SpriteSortMode.Immediate"/>, which is not supported.</exception>
|
||||||
|
public void BeginBatch(SpriteSortMode? sortMode = null) {
|
||||||
if (this.batching)
|
if (this.batching)
|
||||||
throw new InvalidOperationException("Already batching");
|
throw new InvalidOperationException("Already batching");
|
||||||
|
if (sortMode == SpriteSortMode.Immediate)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(sortMode), "Cannot use sprite sort mode Immediate for static batching");
|
||||||
|
|
||||||
|
// if the sort mode changed (which should be very rare in practice), we have to re-sort our list
|
||||||
|
if (sortMode != null && this.sortMode != sortMode) {
|
||||||
|
this.sortMode = sortMode.Value;
|
||||||
|
if (this.items.Count > 0) {
|
||||||
|
var tempItems = this.items.Values.SelectMany(s => s.Items).ToArray();
|
||||||
|
this.items.Clear();
|
||||||
|
foreach (var item in tempItems)
|
||||||
|
this.AddItemToSet(item);
|
||||||
|
this.batchChanged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.batching = true;
|
this.batching = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,14 +92,10 @@ namespace MLEM.Graphics {
|
||||||
/// Ends batching.
|
/// Ends batching.
|
||||||
/// Call this method after calling <c>Add</c> or any of its overloads the desired number of times to add batched items.
|
/// Call this method after calling <c>Add</c> or any of its overloads the desired number of times to add batched items.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="sortMode">The drawing order for sprite drawing. <see cref="SpriteSortMode.Texture" /> by default, since it is the best in terms of rendering performance. Note that <see cref="SpriteSortMode.Immediate"/> is not supported.</param>
|
|
||||||
/// <exception cref="InvalidOperationException">Thrown if this method is called before <see cref="BeginBatch"/> was called.</exception>
|
/// <exception cref="InvalidOperationException">Thrown if this method is called before <see cref="BeginBatch"/> was called.</exception>
|
||||||
/// <exception cref="ArgumentOutOfRangeException">Thrown if the <paramref name="sortMode"/> is <see cref="SpriteSortMode.Immediate"/>, which is not supported.</exception>
|
public void EndBatch() {
|
||||||
public void EndBatch(SpriteSortMode sortMode = SpriteSortMode.Texture) {
|
|
||||||
if (!this.batching)
|
if (!this.batching)
|
||||||
throw new InvalidOperationException("Not batching");
|
throw new InvalidOperationException("Not batching");
|
||||||
if (sortMode == SpriteSortMode.Immediate)
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(sortMode), "Cannot use sprite sort mode Immediate for static batching");
|
|
||||||
this.batching = false;
|
this.batching = false;
|
||||||
|
|
||||||
// if we didn't add or remove any batch items, we don't have to recalculate anything
|
// if we didn't add or remove any batch items, we don't have to recalculate anything
|
||||||
|
@ -88,41 +105,28 @@ namespace MLEM.Graphics {
|
||||||
this.FilledBuffers = 0;
|
this.FilledBuffers = 0;
|
||||||
this.textures.Clear();
|
this.textures.Clear();
|
||||||
|
|
||||||
// order items according to the sort mode
|
|
||||||
IEnumerable<Item> ordered = this.items;
|
|
||||||
switch (sortMode) {
|
|
||||||
case SpriteSortMode.Texture:
|
|
||||||
// SortingKey is internal, but this will do for batching the same texture together
|
|
||||||
ordered = ordered.OrderBy(i => i.Texture.GetHashCode());
|
|
||||||
break;
|
|
||||||
case SpriteSortMode.BackToFront:
|
|
||||||
ordered = ordered.OrderBy(i => -i.Depth);
|
|
||||||
break;
|
|
||||||
case SpriteSortMode.FrontToBack:
|
|
||||||
ordered = ordered.OrderBy(i => i.Depth);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// fill vertex buffers
|
// fill vertex buffers
|
||||||
var dataIndex = 0;
|
var dataIndex = 0;
|
||||||
Texture2D texture = null;
|
Texture2D texture = null;
|
||||||
foreach (var item in ordered) {
|
foreach (var itemSet in this.items.Values) {
|
||||||
// if the texture changes, we also have to start a new buffer!
|
foreach (var item in itemSet.Items) {
|
||||||
if (dataIndex > 0 && (item.Texture != texture || dataIndex >= StaticSpriteBatch.Data.Length)) {
|
// if the texture changes, we also have to start a new buffer!
|
||||||
this.FillBuffer(this.FilledBuffers++, texture, StaticSpriteBatch.Data);
|
if (dataIndex > 0 && (item.Texture != texture || dataIndex >= StaticSpriteBatch.Data.Length)) {
|
||||||
dataIndex = 0;
|
this.FillBuffer(this.FilledBuffers++, texture, StaticSpriteBatch.Data);
|
||||||
|
dataIndex = 0;
|
||||||
|
}
|
||||||
|
StaticSpriteBatch.Data[dataIndex++] = item.TopLeft;
|
||||||
|
StaticSpriteBatch.Data[dataIndex++] = item.TopRight;
|
||||||
|
StaticSpriteBatch.Data[dataIndex++] = item.BottomLeft;
|
||||||
|
StaticSpriteBatch.Data[dataIndex++] = item.BottomRight;
|
||||||
|
texture = item.Texture;
|
||||||
}
|
}
|
||||||
StaticSpriteBatch.Data[dataIndex++] = item.TopLeft;
|
|
||||||
StaticSpriteBatch.Data[dataIndex++] = item.TopRight;
|
|
||||||
StaticSpriteBatch.Data[dataIndex++] = item.BottomLeft;
|
|
||||||
StaticSpriteBatch.Data[dataIndex++] = item.BottomRight;
|
|
||||||
texture = item.Texture;
|
|
||||||
}
|
}
|
||||||
if (dataIndex > 0)
|
if (dataIndex > 0)
|
||||||
this.FillBuffer(this.FilledBuffers++, texture, StaticSpriteBatch.Data);
|
this.FillBuffer(this.FilledBuffers++, texture, StaticSpriteBatch.Data);
|
||||||
|
|
||||||
// ensure we have enough indices
|
// ensure we have enough indices
|
||||||
var maxItems = Math.Min(this.items.Count, StaticSpriteBatch.MaxBatchItems);
|
var maxItems = Math.Min(this.itemAmount, StaticSpriteBatch.MaxBatchItems);
|
||||||
// each item has 2 triangles which each have 3 indices
|
// each item has 2 triangles which each have 3 indices
|
||||||
if (this.indices == null || this.indices.IndexCount < 6 * maxItems) {
|
if (this.indices == null || this.indices.IndexCount < 6 * maxItems) {
|
||||||
var newIndices = new short[6 * maxItems];
|
var newIndices = new short[6 * maxItems];
|
||||||
|
@ -176,7 +180,7 @@ namespace MLEM.Graphics {
|
||||||
for (var i = 0; i < this.FilledBuffers; i++) {
|
for (var i = 0; i < this.FilledBuffers; i++) {
|
||||||
var buffer = this.vertexBuffers[i];
|
var buffer = this.vertexBuffers[i];
|
||||||
var texture = this.textures[i];
|
var texture = this.textures[i];
|
||||||
var verts = Math.Min(this.items.Count * 4 - totalIndex, buffer.VertexCount);
|
var verts = Math.Min(this.itemAmount * 4 - totalIndex, buffer.VertexCount);
|
||||||
|
|
||||||
this.graphicsDevice.SetVertexBuffer(buffer);
|
this.graphicsDevice.SetVertexBuffer(buffer);
|
||||||
if (effect != null) {
|
if (effect != null) {
|
||||||
|
@ -350,6 +354,21 @@ namespace MLEM.Graphics {
|
||||||
return this.Add(texture, destinationRectangle, null, color);
|
return this.Add(texture, destinationRectangle, null, color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds an item to this batch.
|
||||||
|
/// Note that this batch needs to currently be batching, meaning <see cref="BeginBatch"/> has to have been called previously.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="item">The item to add.</param>
|
||||||
|
/// <returns>The added <paramref name="item"/>, for chaining.</returns>
|
||||||
|
public Item Add(Item item) {
|
||||||
|
if (!this.batching)
|
||||||
|
throw new InvalidOperationException("Not batching");
|
||||||
|
this.AddItemToSet(item);
|
||||||
|
this.itemAmount++;
|
||||||
|
this.batchChanged = true;
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removes the given item from this batch.
|
/// Removes the given item from this batch.
|
||||||
/// Note that this batch needs to currently be batching, meaning <see cref="BeginBatch"/> has to have been called previously.
|
/// Note that this batch needs to currently be batching, meaning <see cref="BeginBatch"/> has to have been called previously.
|
||||||
|
@ -360,7 +379,11 @@ namespace MLEM.Graphics {
|
||||||
public bool Remove(Item item) {
|
public bool Remove(Item item) {
|
||||||
if (!this.batching)
|
if (!this.batching)
|
||||||
throw new InvalidOperationException("Not batching");
|
throw new InvalidOperationException("Not batching");
|
||||||
if (this.items.Remove(item)) {
|
var key = item.GetSortKey(this.sortMode);
|
||||||
|
if (this.items.TryGetValue(key, out var itemSet) && itemSet.Remove(item)) {
|
||||||
|
if (itemSet.IsEmpty)
|
||||||
|
this.items.Remove(key);
|
||||||
|
this.itemAmount--;
|
||||||
this.batchChanged = true;
|
this.batchChanged = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -378,6 +401,7 @@ namespace MLEM.Graphics {
|
||||||
this.items.Clear();
|
this.items.Clear();
|
||||||
this.textures.Clear();
|
this.textures.Clear();
|
||||||
this.FilledBuffers = 0;
|
this.FilledBuffers = 0;
|
||||||
|
this.itemAmount = 0;
|
||||||
this.batchChanged = true;
|
this.batchChanged = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -387,7 +411,6 @@ namespace MLEM.Graphics {
|
||||||
this.indices?.Dispose();
|
this.indices?.Dispose();
|
||||||
foreach (var buffer in this.vertexBuffers)
|
foreach (var buffer in this.vertexBuffers)
|
||||||
buffer.Dispose();
|
buffer.Dispose();
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Item Add(Texture2D texture, Vector2 pos, Vector2 offset, Vector2 size, float sin, float cos, Color color, Vector2 texTl, Vector2 texBr, float depth) {
|
private Item Add(Texture2D texture, Vector2 pos, Vector2 offset, Vector2 size, float sin, float cos, Color color, Vector2 texTl, Vector2 texBr, float depth) {
|
||||||
|
@ -427,27 +450,31 @@ namespace MLEM.Graphics {
|
||||||
}
|
}
|
||||||
|
|
||||||
private Item Add(Texture2D texture, float depth, VertexPositionColorTexture tl, VertexPositionColorTexture tr, VertexPositionColorTexture bl, VertexPositionColorTexture br) {
|
private Item Add(Texture2D texture, float depth, VertexPositionColorTexture tl, VertexPositionColorTexture tr, VertexPositionColorTexture bl, VertexPositionColorTexture br) {
|
||||||
if (!this.batching)
|
return this.Add(new Item(texture, depth, tl, tr, bl, br));
|
||||||
throw new InvalidOperationException("Not batching");
|
|
||||||
var item = new Item(texture, depth, tl, tr, bl, br);
|
|
||||||
this.items.Add(item);
|
|
||||||
this.batchChanged = true;
|
|
||||||
return item;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void FillBuffer(int index, Texture2D texture, VertexPositionColorTexture[] data) {
|
private void FillBuffer(int index, Texture2D texture, VertexPositionColorTexture[] data) {
|
||||||
if (this.vertexBuffers.Count <= index)
|
if (this.vertexBuffers.Count <= index)
|
||||||
this.vertexBuffers.Add(new VertexBuffer(this.graphicsDevice, VertexPositionColorTexture.VertexDeclaration, StaticSpriteBatch.MaxBatchItems * 4, BufferUsage.WriteOnly));
|
this.vertexBuffers.Add(new DynamicVertexBuffer(this.graphicsDevice, VertexPositionColorTexture.VertexDeclaration, StaticSpriteBatch.MaxBatchItems * 4, BufferUsage.WriteOnly));
|
||||||
this.vertexBuffers[index].SetData(data);
|
this.vertexBuffers[index].SetData(data, 0, data.Length, SetDataOptions.Discard);
|
||||||
this.textures.Insert(index, texture);
|
this.textures.Insert(index, texture);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawPrimitives(int vertices) {
|
private void DrawPrimitives(int vertices) {
|
||||||
#if FNA
|
#if FNA
|
||||||
this.graphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, vertices, 0, vertices / 4 * 2);
|
this.graphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, vertices, 0, vertices / 4 * 2);
|
||||||
#else
|
#else
|
||||||
this.graphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, vertices / 4 * 2);
|
this.graphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, vertices / 4 * 2);
|
||||||
#endif
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddItemToSet(Item item) {
|
||||||
|
var sortKey = item.GetSortKey(this.sortMode);
|
||||||
|
if (!this.items.TryGetValue(sortKey, out var itemSet)) {
|
||||||
|
itemSet = new ItemSet();
|
||||||
|
this.items.Add(sortKey, itemSet);
|
||||||
|
}
|
||||||
|
itemSet.Add(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -472,9 +499,68 @@ namespace MLEM.Graphics {
|
||||||
this.BottomRight = bottomRight;
|
this.BottomRight = bottomRight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal float GetSortKey(SpriteSortMode sortMode) {
|
||||||
|
switch (sortMode) {
|
||||||
|
case SpriteSortMode.Texture:
|
||||||
|
return this.Texture.GetHashCode();
|
||||||
|
case SpriteSortMode.BackToFront:
|
||||||
|
return -this.Depth;
|
||||||
|
case SpriteSortMode.FrontToBack:
|
||||||
|
return this.Depth;
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#if FNA
|
private class ItemSet {
|
||||||
|
|
||||||
|
public IEnumerable<Item> Items {
|
||||||
|
get {
|
||||||
|
if (this.items != null)
|
||||||
|
return this.items;
|
||||||
|
if (this.single != null)
|
||||||
|
return Enumerable.Repeat(this.single, 1);
|
||||||
|
return Enumerable.Empty<Item>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public bool IsEmpty => this.items == null && this.single == null;
|
||||||
|
|
||||||
|
private HashSet<Item> items;
|
||||||
|
private Item single;
|
||||||
|
|
||||||
|
public void Add(Item item) {
|
||||||
|
if (this.items != null) {
|
||||||
|
this.items.Add(item);
|
||||||
|
} else if (this.single != null) {
|
||||||
|
this.items = new HashSet<Item>();
|
||||||
|
this.items.Add(this.single);
|
||||||
|
this.items.Add(item);
|
||||||
|
this.single = null;
|
||||||
|
} else {
|
||||||
|
this.single = item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Remove(Item item) {
|
||||||
|
if (this.items != null && this.items.Remove(item)) {
|
||||||
|
if (this.items.Count <= 1) {
|
||||||
|
this.single = this.items.Single();
|
||||||
|
this.items = null;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} else if (this.single == item) {
|
||||||
|
this.single = null;
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#if FNA
|
||||||
private class SpriteEffect : Effect {
|
private class SpriteEffect : Effect {
|
||||||
|
|
||||||
private EffectParameter matrixParam;
|
private EffectParameter matrixParam;
|
||||||
|
@ -525,7 +611,7 @@ namespace MLEM.Graphics {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,20 +6,41 @@ using Microsoft.Xna.Framework;
|
||||||
using Microsoft.Xna.Framework.Graphics;
|
using Microsoft.Xna.Framework.Graphics;
|
||||||
using Microsoft.Xna.Framework.Input;
|
using Microsoft.Xna.Framework.Input;
|
||||||
using Microsoft.Xna.Framework.Input.Touch;
|
using Microsoft.Xna.Framework.Input.Touch;
|
||||||
using MLEM.Misc;
|
using static MLEM.Input.GenericInput;
|
||||||
|
|
||||||
namespace MLEM.Input {
|
namespace MLEM.Input {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// An input handler is a more advanced wrapper around MonoGame's default input system.
|
/// An input handler is a more advanced wrapper around MonoGame's default input system.
|
||||||
/// It includes keyboard, mouse, gamepad and touch states, as well as a new "pressed" state for keys and the ability for keyboard and gamepad repeat events.
|
/// It includes keyboard, mouse, gamepad and touch handling through the <see cref="GenericInput"/> wrapper, as well as a new "pressed" state for inputs, the ability for keyboard and gamepad repeat events, and the ability to track down, up and press times for inputs.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class InputHandler : GameComponent {
|
public class InputHandler : GameComponent {
|
||||||
|
|
||||||
#if FNA
|
/// <summary>
|
||||||
|
/// All values of the <see cref="Buttons"/> enum.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly Buttons[] AllButtons =
|
||||||
|
#if NET6_0_OR_GREATER
|
||||||
|
Enum.GetValues<Buttons>();
|
||||||
|
#else
|
||||||
|
(Buttons[]) Enum.GetValues(typeof(Buttons));
|
||||||
|
#endif
|
||||||
|
/// <summary>
|
||||||
|
/// All values of the <see cref="Keys"/> enum.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly Keys[] AllKeys =
|
||||||
|
#if NET6_0_OR_GREATER
|
||||||
|
Enum.GetValues<Keys>();
|
||||||
|
#else
|
||||||
|
(Keys[]) Enum.GetValues(typeof(Keys));
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if FNA
|
||||||
private const int MaximumGamePadCount = 4;
|
private const int MaximumGamePadCount = 4;
|
||||||
#else
|
#else
|
||||||
private static readonly int MaximumGamePadCount = GamePad.MaximumGamePadCount;
|
private static readonly int MaximumGamePadCount = GamePad.MaximumGamePadCount;
|
||||||
#endif
|
#endif
|
||||||
|
private static readonly TouchLocation[] EmptyTouchLocations = new TouchLocation[0];
|
||||||
|
private static readonly GenericInput[] EmptyGenericInputs = new GenericInput[0];
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Contains all of the gestures that have finished during the last update call.
|
/// Contains all of the gestures that have finished during the last update call.
|
||||||
|
@ -72,25 +93,31 @@ namespace MLEM.Input {
|
||||||
/// Inverted behavior means that, instead of an input counting as pressed when it was up in the last frame and is now down, it will be counted as pressed when it was down in the last frame and is now up.
|
/// Inverted behavior means that, instead of an input counting as pressed when it was up in the last frame and is now down, it will be counted as pressed when it was down in the last frame and is now up.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool InvertPressBehavior;
|
public bool InvertPressBehavior;
|
||||||
|
/// <summary>
|
||||||
|
/// If your project already handles the processing of MonoGame's gestures elsewhere, you can set this field to true to ensure that this input handler's gesture handling does not override your own, since <see cref="GestureSample"/> objects can only be retrieved once and are then removed from the <see cref="TouchPanel"/>'s queue.
|
||||||
|
/// If this value is set to true, but you still want to be able to use <see cref="Gestures"/>, <see cref="GetGesture"/>, and <see cref="GetViewportGesture"/>, you can make this input handler aware of a gesture for the duration of the update frame that you added it on by using <see cref="AddExternalGesture"/>.
|
||||||
|
/// For more info, see https://mlem.ellpeck.de/articles/input.html#external-gesture-handling.
|
||||||
|
/// </summary>
|
||||||
|
public bool ExternalGestureHandling;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// An array of all <see cref="Keys"/>, <see cref="Buttons"/> and <see cref="MouseButton"/> values that are currently down.
|
/// An array of all <see cref="Keys"/>, <see cref="Buttons"/> and <see cref="MouseButton"/> values that are currently down.
|
||||||
/// Additionally, <see cref="TryGetDownTime"/> or <see cref="GetDownTime"/> can be used to determine the amount of time that a given input has been down for.
|
/// Additionally, <see cref="TryGetDownTime"/> or <see cref="GetDownTime"/> can be used to determine the amount of time that a given input has been down for.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public GenericInput[] InputsDown { get; private set; } = Array.Empty<GenericInput>();
|
public GenericInput[] InputsDown { get; private set; } = InputHandler.EmptyGenericInputs;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// An array of all <see cref="Keys"/>, <see cref="Buttons"/> and <see cref="MouseButton"/> that are currently considered pressed.
|
/// An array of all <see cref="Keys"/>, <see cref="Buttons"/> and <see cref="MouseButton"/> that are currently considered pressed.
|
||||||
/// An input is considered pressed if it was up in the last update, and is up in the current one.
|
/// An input is considered pressed if it was up in the last update, and is up in the current one.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public GenericInput[] InputsPressed { get; private set; } = Array.Empty<GenericInput>();
|
public GenericInput[] InputsPressed { get; private set; } = InputHandler.EmptyGenericInputs;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Contains the touch state from the last update call
|
/// Contains the touch state from the last update call
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public TouchCollection LastTouchState { get; private set; } = new TouchCollection(Array.Empty<TouchLocation>());
|
public TouchCollection LastTouchState { get; private set; } = new TouchCollection(InputHandler.EmptyTouchLocations);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Contains the current touch state
|
/// Contains the current touch state
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public TouchCollection TouchState { get; private set; } = new TouchCollection(Array.Empty<TouchLocation>());
|
public TouchCollection TouchState { get; private set; } = new TouchCollection(InputHandler.EmptyTouchLocations);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Contains the <see cref="LastTouchState"/>, but with the <see cref="GraphicsDevice.Viewport"/> taken into account.
|
/// Contains the <see cref="LastTouchState"/>, but with the <see cref="GraphicsDevice.Viewport"/> taken into account.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -100,8 +127,8 @@ namespace MLEM.Input {
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IList<TouchLocation> ViewportTouchState { get; private set; }
|
public IList<TouchLocation> ViewportTouchState { get; private set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Contains the amount of gamepads that are currently connected.
|
/// Contains the amount of gamepads that are currently connected. Note that this value will be set to 0 if <see cref="HandleGamepads"/> is false.
|
||||||
/// This field is automatically updated in <see cref="Update()"/>
|
/// This field is automatically updated in <see cref="Update()"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int ConnectedGamepads { get; private set; }
|
public int ConnectedGamepads { get; private set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -153,6 +180,7 @@ namespace MLEM.Input {
|
||||||
private readonly List<GestureSample> gestures = new List<GestureSample>();
|
private readonly List<GestureSample> gestures = new List<GestureSample>();
|
||||||
private readonly HashSet<(GenericInput, int)> consumedPresses = new HashSet<(GenericInput, int)>();
|
private readonly HashSet<(GenericInput, int)> consumedPresses = new HashSet<(GenericInput, int)>();
|
||||||
private readonly Dictionary<(GenericInput, int), DateTime> inputUpTimes = new Dictionary<(GenericInput, int), DateTime>();
|
private readonly Dictionary<(GenericInput, int), DateTime> inputUpTimes = new Dictionary<(GenericInput, int), DateTime>();
|
||||||
|
private readonly Dictionary<(GenericInput, int), DateTime> inputDownTimes = new Dictionary<(GenericInput, int), DateTime>();
|
||||||
private readonly Dictionary<(GenericInput, int), DateTime> inputPressedTimes = new Dictionary<(GenericInput, int), DateTime>();
|
private readonly Dictionary<(GenericInput, int), DateTime> inputPressedTimes = new Dictionary<(GenericInput, int), DateTime>();
|
||||||
|
|
||||||
private Point ViewportOffset => new Point(-this.Game.GraphicsDevice.Viewport.X, -this.Game.GraphicsDevice.Viewport.Y);
|
private Point ViewportOffset => new Point(-this.Game.GraphicsDevice.Viewport.X, -this.Game.GraphicsDevice.Viewport.Y);
|
||||||
|
@ -188,15 +216,15 @@ namespace MLEM.Input {
|
||||||
|
|
||||||
this.consumedPresses.Clear();
|
this.consumedPresses.Clear();
|
||||||
|
|
||||||
|
this.LastKeyboardState = this.KeyboardState;
|
||||||
if (this.HandleKeyboard) {
|
if (this.HandleKeyboard) {
|
||||||
this.LastKeyboardState = this.KeyboardState;
|
|
||||||
this.KeyboardState = active ? Keyboard.GetState() : default;
|
this.KeyboardState = active ? Keyboard.GetState() : default;
|
||||||
var pressedKeys = this.KeyboardState.GetPressedKeys();
|
var pressedKeys = this.KeyboardState.GetPressedKeys();
|
||||||
foreach (var pressed in pressedKeys)
|
foreach (var pressed in pressedKeys)
|
||||||
this.AccumulateDown(pressed, -1);
|
this.AccumulateDown(pressed, -1);
|
||||||
|
|
||||||
|
this.triggerKeyRepeat = false;
|
||||||
if (this.HandleKeyboardRepeats) {
|
if (this.HandleKeyboardRepeats) {
|
||||||
this.triggerKeyRepeat = false;
|
|
||||||
// the key that started being held most recently should be the one being repeated
|
// the key that started being held most recently should be the one being repeated
|
||||||
this.heldKey = pressedKeys.OrderBy(k => this.GetDownTime(k)).FirstOrDefault();
|
this.heldKey = pressedKeys.OrderBy(k => this.GetDownTime(k)).FirstOrDefault();
|
||||||
if (this.TryGetDownTime(this.heldKey, out var heldTime)) {
|
if (this.TryGetDownTime(this.heldKey, out var heldTime)) {
|
||||||
|
@ -211,11 +239,17 @@ namespace MLEM.Input {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
this.heldKey = Keys.None;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
this.KeyboardState = default;
|
||||||
|
this.triggerKeyRepeat = false;
|
||||||
|
this.heldKey = Keys.None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.LastMouseState = this.MouseState;
|
||||||
if (this.HandleMouse) {
|
if (this.HandleMouse) {
|
||||||
this.LastMouseState = this.MouseState;
|
|
||||||
var state = Mouse.GetState();
|
var state = Mouse.GetState();
|
||||||
if (active && this.Game.GraphicsDevice.Viewport.Bounds.Contains(state.X, state.Y)) {
|
if (active && this.Game.GraphicsDevice.Viewport.Bounds.Contains(state.X, state.Y)) {
|
||||||
this.MouseState = state;
|
this.MouseState = state;
|
||||||
|
@ -225,12 +259,14 @@ namespace MLEM.Input {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// mouse position and scroll wheel value should be preserved when the mouse is out of bounds
|
// mouse position and scroll wheel value should be preserved when the mouse is out of bounds
|
||||||
#if FNA
|
#if FNA
|
||||||
this.MouseState = new MouseState(state.X, state.Y, state.ScrollWheelValue, 0, 0, 0, 0, 0);
|
this.MouseState = new MouseState(state.X, state.Y, state.ScrollWheelValue, 0, 0, 0, 0, 0);
|
||||||
#else
|
#else
|
||||||
this.MouseState = new MouseState(state.X, state.Y, state.ScrollWheelValue, 0, 0, 0, 0, 0, state.HorizontalScrollWheelValue);
|
this.MouseState = new MouseState(state.X, state.Y, state.ScrollWheelValue, 0, 0, 0, 0, 0, state.HorizontalScrollWheelValue);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
this.MouseState = default;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.HandleGamepads) {
|
if (this.HandleGamepads) {
|
||||||
|
@ -241,8 +277,8 @@ namespace MLEM.Input {
|
||||||
if (GamePad.GetCapabilities((PlayerIndex) i).IsConnected) {
|
if (GamePad.GetCapabilities((PlayerIndex) i).IsConnected) {
|
||||||
if (active) {
|
if (active) {
|
||||||
this.gamepads[i] = GamePad.GetState((PlayerIndex) i);
|
this.gamepads[i] = GamePad.GetState((PlayerIndex) i);
|
||||||
foreach (var button in EnumHelper.Buttons) {
|
foreach (var button in InputHandler.AllButtons) {
|
||||||
if (this.IsGamepadButtonDown(button, i))
|
if (this.IsDown(button, i))
|
||||||
this.AccumulateDown(button, i);
|
this.AccumulateDown(button, i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -251,11 +287,11 @@ namespace MLEM.Input {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.HandleGamepadRepeats) {
|
for (var i = 0; i < this.ConnectedGamepads; i++) {
|
||||||
for (var i = 0; i < this.ConnectedGamepads; i++) {
|
this.triggerGamepadButtonRepeat[i] = false;
|
||||||
this.triggerGamepadButtonRepeat[i] = false;
|
if (this.HandleGamepadRepeats) {
|
||||||
this.heldGamepadButtons[i] = EnumHelper.Buttons
|
this.heldGamepadButtons[i] = InputHandler.AllButtons
|
||||||
.Where(b => this.IsGamepadButtonDown(b, i))
|
.Where(b => this.IsDown(b, i))
|
||||||
.OrderBy(b => this.GetDownTime(b, i))
|
.OrderBy(b => this.GetDownTime(b, i))
|
||||||
.Cast<Buttons?>().FirstOrDefault();
|
.Cast<Buttons?>().FirstOrDefault();
|
||||||
if (this.heldGamepadButtons[i].HasValue && this.TryGetDownTime(this.heldGamepadButtons[i].Value, out var heldTime, i)) {
|
if (this.heldGamepadButtons[i].HasValue && this.TryGetDownTime(this.heldGamepadButtons[i].Value, out var heldTime, i)) {
|
||||||
|
@ -267,15 +303,24 @@ namespace MLEM.Input {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
this.heldGamepadButtons[i] = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
this.ConnectedGamepads = 0;
|
||||||
|
for (var i = 0; i < InputHandler.MaximumGamePadCount; i++) {
|
||||||
|
this.lastGamepads[i] = this.gamepads[i];
|
||||||
|
this.gamepads[i] = default;
|
||||||
|
this.triggerGamepadButtonRepeat[i] = false;
|
||||||
|
this.heldGamepadButtons[i] = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.LastTouchState = this.TouchState;
|
||||||
|
this.LastViewportTouchState = this.ViewportTouchState;
|
||||||
if (this.HandleTouch) {
|
if (this.HandleTouch) {
|
||||||
this.LastTouchState = this.TouchState;
|
this.TouchState = active ? TouchPanel.GetState() : new TouchCollection(InputHandler.EmptyTouchLocations);
|
||||||
this.LastViewportTouchState = this.ViewportTouchState;
|
|
||||||
|
|
||||||
this.TouchState = active ? TouchPanel.GetState() : new TouchCollection(Array.Empty<TouchLocation>());
|
|
||||||
if (this.TouchState.Count > 0 && this.ViewportOffset != Point.Zero) {
|
if (this.TouchState.Count > 0 && this.ViewportOffset != Point.Zero) {
|
||||||
this.ViewportTouchState = new List<TouchLocation>();
|
this.ViewportTouchState = new List<TouchLocation>();
|
||||||
foreach (var touch in this.TouchState) {
|
foreach (var touch in this.TouchState) {
|
||||||
|
@ -287,14 +332,20 @@ namespace MLEM.Input {
|
||||||
this.ViewportTouchState = this.TouchState;
|
this.ViewportTouchState = this.TouchState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// we still want to clear gestures when handling externally to maintain the per-frame gesture system
|
||||||
|
this.gestures.Clear();
|
||||||
|
if (active && !this.ExternalGestureHandling) {
|
||||||
|
while (TouchPanel.IsGestureAvailable)
|
||||||
|
this.gestures.Add(TouchPanel.ReadGesture());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.TouchState = new TouchCollection(InputHandler.EmptyTouchLocations);
|
||||||
this.gestures.Clear();
|
this.gestures.Clear();
|
||||||
while (active && TouchPanel.IsGestureAvailable)
|
|
||||||
this.gestures.Add(TouchPanel.ReadGesture());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.inputsDownAccum.Count <= 0 && this.inputsDown.Count <= 0) {
|
if (this.inputsDownAccum.Count <= 0 && this.inputsDown.Count <= 0) {
|
||||||
this.InputsPressed = Array.Empty<GenericInput>();
|
this.InputsPressed = InputHandler.EmptyGenericInputs;
|
||||||
this.InputsDown = Array.Empty<GenericInput>();
|
this.InputsDown = InputHandler.EmptyGenericInputs;
|
||||||
} else {
|
} else {
|
||||||
// handle pressed inputs
|
// handle pressed inputs
|
||||||
var pressed = new List<GenericInput>();
|
var pressed = new List<GenericInput>();
|
||||||
|
@ -307,13 +358,15 @@ namespace MLEM.Input {
|
||||||
}
|
}
|
||||||
this.InputsPressed = pressed.ToArray();
|
this.InputsPressed = pressed.ToArray();
|
||||||
|
|
||||||
// handle inputs that changed to up
|
// handle inputs that changed between down and up
|
||||||
foreach (var key in this.inputsDownAccum.Keys)
|
|
||||||
this.inputUpTimes.Remove(key);
|
|
||||||
foreach (var key in this.inputsDown.Keys) {
|
foreach (var key in this.inputsDown.Keys) {
|
||||||
if (!this.inputsDownAccum.ContainsKey(key))
|
if (!this.inputsDownAccum.ContainsKey(key))
|
||||||
this.inputUpTimes[key] = DateTime.UtcNow;
|
this.inputUpTimes[key] = DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
|
foreach (var key in this.inputsDownAccum.Keys) {
|
||||||
|
if (!this.inputsDown.ContainsKey(key))
|
||||||
|
this.inputDownTimes[key] = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
// handle inputs that are currently down
|
// handle inputs that are currently down
|
||||||
this.InputsDown = this.inputsDownAccum.Keys.Select(key => key.Item1).ToArray();
|
this.InputsDown = this.inputsDownAccum.Keys.Select(key => key.Item1).ToArray();
|
||||||
|
@ -346,22 +399,39 @@ namespace MLEM.Input {
|
||||||
return this.gamepads[index];
|
return this.gamepads[index];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns whether the given modifier key is down.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="modifier">The modifier key</param>
|
||||||
|
/// <returns>If the modifier key is down</returns>
|
||||||
|
public bool IsModifierKeyDown(ModifierKey modifier) {
|
||||||
|
foreach (var key in modifier.GetKeys()) {
|
||||||
|
if (this.IsDown(key))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc cref="Microsoft.Xna.Framework.Input.KeyboardState.IsKeyDown"/>
|
/// <inheritdoc cref="Microsoft.Xna.Framework.Input.KeyboardState.IsKeyDown"/>
|
||||||
|
[Obsolete("This method is deprecated. Use the GenericInput version IsDown instead.")]
|
||||||
public bool IsKeyDown(Keys key) {
|
public bool IsKeyDown(Keys key) {
|
||||||
return this.KeyboardState.IsKeyDown(key);
|
return this.KeyboardState.IsKeyDown(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc cref="Microsoft.Xna.Framework.Input.KeyboardState.IsKeyUp"/>
|
/// <inheritdoc cref="Microsoft.Xna.Framework.Input.KeyboardState.IsKeyUp"/>
|
||||||
|
[Obsolete("This method is deprecated. Use the GenericInput version IsUp instead.")]
|
||||||
public bool IsKeyUp(Keys key) {
|
public bool IsKeyUp(Keys key) {
|
||||||
return this.KeyboardState.IsKeyUp(key);
|
return this.KeyboardState.IsKeyUp(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc cref="Microsoft.Xna.Framework.Input.KeyboardState.IsKeyDown"/>
|
/// <inheritdoc cref="Microsoft.Xna.Framework.Input.KeyboardState.IsKeyDown"/>
|
||||||
|
[Obsolete("This method is deprecated. Use the GenericInput version WasDown instead.")]
|
||||||
public bool WasKeyDown(Keys key) {
|
public bool WasKeyDown(Keys key) {
|
||||||
return this.LastKeyboardState.IsKeyDown(key);
|
return this.LastKeyboardState.IsKeyDown(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc cref="Microsoft.Xna.Framework.Input.KeyboardState.IsKeyUp"/>
|
/// <inheritdoc cref="Microsoft.Xna.Framework.Input.KeyboardState.IsKeyUp"/>
|
||||||
|
[Obsolete("This method is deprecated. Use the GenericInput version WasUp instead.")]
|
||||||
public bool WasKeyUp(Keys key) {
|
public bool WasKeyUp(Keys key) {
|
||||||
return this.LastKeyboardState.IsKeyUp(key);
|
return this.LastKeyboardState.IsKeyUp(key);
|
||||||
}
|
}
|
||||||
|
@ -373,6 +443,7 @@ namespace MLEM.Input {
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="key">The key to query</param>
|
/// <param name="key">The key to query</param>
|
||||||
/// <returns>If the key is pressed</returns>
|
/// <returns>If the key is pressed</returns>
|
||||||
|
[Obsolete("This method is deprecated. Use the GenericInput version IsPressed instead.")]
|
||||||
public bool IsKeyPressed(Keys key) {
|
public bool IsKeyPressed(Keys key) {
|
||||||
// if the queried key is the held key and a repeat should be triggered, return true
|
// if the queried key is the held key and a repeat should be triggered, return true
|
||||||
if (this.HandleKeyboardRepeats && key == this.heldKey && this.triggerKeyRepeat)
|
if (this.HandleKeyboardRepeats && key == this.heldKey && this.triggerKeyRepeat)
|
||||||
|
@ -388,6 +459,7 @@ namespace MLEM.Input {
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="key">The key to query</param>
|
/// <param name="key">The key to query</param>
|
||||||
/// <returns>If the key is pressed</returns>
|
/// <returns>If the key is pressed</returns>
|
||||||
|
[Obsolete("This method is deprecated. Use the GenericInput version IsPressedIgnoreRepeats instead.")]
|
||||||
public bool IsKeyPressedIgnoreRepeats(Keys key) {
|
public bool IsKeyPressedIgnoreRepeats(Keys key) {
|
||||||
if (this.InvertPressBehavior)
|
if (this.InvertPressBehavior)
|
||||||
return this.WasKeyDown(key) && this.IsKeyUp(key);
|
return this.WasKeyDown(key) && this.IsKeyUp(key);
|
||||||
|
@ -399,6 +471,7 @@ namespace MLEM.Input {
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="key">The key to query.</param>
|
/// <param name="key">The key to query.</param>
|
||||||
/// <returns>If the key is pressed and the press is not consumed yet.</returns>
|
/// <returns>If the key is pressed and the press is not consumed yet.</returns>
|
||||||
|
[Obsolete("This method is deprecated. Use the GenericInput version IsPressedAvailable instead.")]
|
||||||
public bool IsKeyPressedAvailable(Keys key) {
|
public bool IsKeyPressedAvailable(Keys key) {
|
||||||
return this.IsKeyPressed(key) && !this.IsPressConsumed(key);
|
return this.IsKeyPressed(key) && !this.IsPressConsumed(key);
|
||||||
}
|
}
|
||||||
|
@ -411,6 +484,7 @@ namespace MLEM.Input {
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="key">The key to query.</param>
|
/// <param name="key">The key to query.</param>
|
||||||
/// <returns>If the key is pressed and the press is not consumed yet.</returns>
|
/// <returns>If the key is pressed and the press is not consumed yet.</returns>
|
||||||
|
[Obsolete("This method is deprecated. Use the GenericInput version TryConsumePressed instead.")]
|
||||||
public bool TryConsumeKeyPressed(Keys key) {
|
public bool TryConsumeKeyPressed(Keys key) {
|
||||||
if (this.IsKeyPressedAvailable(key)) {
|
if (this.IsKeyPressedAvailable(key)) {
|
||||||
this.consumedPresses.Add((key, -1));
|
this.consumedPresses.Add((key, -1));
|
||||||
|
@ -419,24 +493,12 @@ namespace MLEM.Input {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns whether the given modifier key is down.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="modifier">The modifier key</param>
|
|
||||||
/// <returns>If the modifier key is down</returns>
|
|
||||||
public bool IsModifierKeyDown(ModifierKey modifier) {
|
|
||||||
foreach (var key in modifier.GetKeys()) {
|
|
||||||
if (this.IsKeyDown(key))
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns whether the given mouse button is currently down.
|
/// Returns whether the given mouse button is currently down.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="button">The button to query</param>
|
/// <param name="button">The button to query</param>
|
||||||
/// <returns>Whether or not the queried button is down</returns>
|
/// <returns>Whether or not the queried button is down</returns>
|
||||||
|
[Obsolete("This method is deprecated. Use the GenericInput version IsDown instead.")]
|
||||||
public bool IsMouseButtonDown(MouseButton button) {
|
public bool IsMouseButtonDown(MouseButton button) {
|
||||||
return this.MouseState.GetState(button) == ButtonState.Pressed;
|
return this.MouseState.GetState(button) == ButtonState.Pressed;
|
||||||
}
|
}
|
||||||
|
@ -446,6 +508,7 @@ namespace MLEM.Input {
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="button">The button to query</param>
|
/// <param name="button">The button to query</param>
|
||||||
/// <returns>Whether or not the queried button is up</returns>
|
/// <returns>Whether or not the queried button is up</returns>
|
||||||
|
[Obsolete("This method is deprecated. Use the GenericInput version IsUp instead.")]
|
||||||
public bool IsMouseButtonUp(MouseButton button) {
|
public bool IsMouseButtonUp(MouseButton button) {
|
||||||
return this.MouseState.GetState(button) == ButtonState.Released;
|
return this.MouseState.GetState(button) == ButtonState.Released;
|
||||||
}
|
}
|
||||||
|
@ -455,6 +518,7 @@ namespace MLEM.Input {
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="button">The button to query</param>
|
/// <param name="button">The button to query</param>
|
||||||
/// <returns>Whether or not the queried button was down</returns>
|
/// <returns>Whether or not the queried button was down</returns>
|
||||||
|
[Obsolete("This method is deprecated. Use the GenericInput version WasDown instead.")]
|
||||||
public bool WasMouseButtonDown(MouseButton button) {
|
public bool WasMouseButtonDown(MouseButton button) {
|
||||||
return this.LastMouseState.GetState(button) == ButtonState.Pressed;
|
return this.LastMouseState.GetState(button) == ButtonState.Pressed;
|
||||||
}
|
}
|
||||||
|
@ -464,6 +528,7 @@ namespace MLEM.Input {
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="button">The button to query</param>
|
/// <param name="button">The button to query</param>
|
||||||
/// <returns>Whether or not the queried button was up</returns>
|
/// <returns>Whether or not the queried button was up</returns>
|
||||||
|
[Obsolete("This method is deprecated. Use the GenericInput version WasUp instead.")]
|
||||||
public bool WasMouseButtonUp(MouseButton button) {
|
public bool WasMouseButtonUp(MouseButton button) {
|
||||||
return this.LastMouseState.GetState(button) == ButtonState.Released;
|
return this.LastMouseState.GetState(button) == ButtonState.Released;
|
||||||
}
|
}
|
||||||
|
@ -474,6 +539,7 @@ namespace MLEM.Input {
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="button">The button to query</param>
|
/// <param name="button">The button to query</param>
|
||||||
/// <returns>Whether the button is pressed</returns>
|
/// <returns>Whether the button is pressed</returns>
|
||||||
|
[Obsolete("This method is deprecated. Use the GenericInput version IsPressed instead.")]
|
||||||
public bool IsMouseButtonPressed(MouseButton button) {
|
public bool IsMouseButtonPressed(MouseButton button) {
|
||||||
if (this.InvertPressBehavior)
|
if (this.InvertPressBehavior)
|
||||||
return this.WasMouseButtonDown(button) && this.IsMouseButtonUp(button);
|
return this.WasMouseButtonDown(button) && this.IsMouseButtonUp(button);
|
||||||
|
@ -485,6 +551,7 @@ namespace MLEM.Input {
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="button">The button to query.</param>
|
/// <param name="button">The button to query.</param>
|
||||||
/// <returns>If the button is pressed and the press is not consumed yet.</returns>
|
/// <returns>If the button is pressed and the press is not consumed yet.</returns>
|
||||||
|
[Obsolete("This method is deprecated. Use the GenericInput version IsPressedAvailable instead.")]
|
||||||
public bool IsMouseButtonPressedAvailable(MouseButton button) {
|
public bool IsMouseButtonPressedAvailable(MouseButton button) {
|
||||||
return this.IsMouseButtonPressed(button) && !this.IsPressConsumed(button);
|
return this.IsMouseButtonPressed(button) && !this.IsPressConsumed(button);
|
||||||
}
|
}
|
||||||
|
@ -496,6 +563,7 @@ namespace MLEM.Input {
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="button">The button to query.</param>
|
/// <param name="button">The button to query.</param>
|
||||||
/// <returns>If the button is pressed and the press is not consumed yet.</returns>
|
/// <returns>If the button is pressed and the press is not consumed yet.</returns>
|
||||||
|
[Obsolete("This method is deprecated. Use the GenericInput version TryConsumePressed instead.")]
|
||||||
public bool TryConsumeMouseButtonPressed(MouseButton button) {
|
public bool TryConsumeMouseButtonPressed(MouseButton button) {
|
||||||
if (this.IsMouseButtonPressedAvailable(button)) {
|
if (this.IsMouseButtonPressedAvailable(button)) {
|
||||||
this.consumedPresses.Add((button, -1));
|
this.consumedPresses.Add((button, -1));
|
||||||
|
@ -505,6 +573,7 @@ namespace MLEM.Input {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc cref="GamePadState.IsButtonDown"/>
|
/// <inheritdoc cref="GamePadState.IsButtonDown"/>
|
||||||
|
[Obsolete("This method is deprecated. Use the GenericInput version IsDown instead.")]
|
||||||
public bool IsGamepadButtonDown(Buttons button, int index = -1) {
|
public bool IsGamepadButtonDown(Buttons button, int index = -1) {
|
||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
for (var i = 0; i < this.ConnectedGamepads; i++) {
|
for (var i = 0; i < this.ConnectedGamepads; i++) {
|
||||||
|
@ -517,6 +586,7 @@ namespace MLEM.Input {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc cref="GamePadState.IsButtonUp"/>
|
/// <inheritdoc cref="GamePadState.IsButtonUp"/>
|
||||||
|
[Obsolete("This method is deprecated. Use the GenericInput version IsUp instead.")]
|
||||||
public bool IsGamepadButtonUp(Buttons button, int index = -1) {
|
public bool IsGamepadButtonUp(Buttons button, int index = -1) {
|
||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
for (var i = 0; i < this.ConnectedGamepads; i++) {
|
for (var i = 0; i < this.ConnectedGamepads; i++) {
|
||||||
|
@ -529,6 +599,7 @@ namespace MLEM.Input {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc cref="GamePadState.IsButtonDown"/>
|
/// <inheritdoc cref="GamePadState.IsButtonDown"/>
|
||||||
|
[Obsolete("This method is deprecated. Use the GenericInput version WasDown instead.")]
|
||||||
public bool WasGamepadButtonDown(Buttons button, int index = -1) {
|
public bool WasGamepadButtonDown(Buttons button, int index = -1) {
|
||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
for (var i = 0; i < this.ConnectedGamepads; i++) {
|
for (var i = 0; i < this.ConnectedGamepads; i++) {
|
||||||
|
@ -541,6 +612,7 @@ namespace MLEM.Input {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc cref="GamePadState.IsButtonUp"/>
|
/// <inheritdoc cref="GamePadState.IsButtonUp"/>
|
||||||
|
[Obsolete("This method is deprecated. Use the GenericInput version WasUp instead.")]
|
||||||
public bool WasGamepadButtonUp(Buttons button, int index = -1) {
|
public bool WasGamepadButtonUp(Buttons button, int index = -1) {
|
||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
for (var i = 0; i < this.ConnectedGamepads; i++) {
|
for (var i = 0; i < this.ConnectedGamepads; i++) {
|
||||||
|
@ -560,6 +632,7 @@ namespace MLEM.Input {
|
||||||
/// <param name="button">The button to query</param>
|
/// <param name="button">The button to query</param>
|
||||||
/// <param name="index">The zero-based index of the gamepad, or -1 for any gamepad</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>
|
/// <returns>Whether the given button is pressed</returns>
|
||||||
|
[Obsolete("This method is deprecated. Use the GenericInput version IsPressed instead.")]
|
||||||
public bool IsGamepadButtonPressed(Buttons button, int index = -1) {
|
public bool IsGamepadButtonPressed(Buttons button, int index = -1) {
|
||||||
if (this.HandleGamepadRepeats) {
|
if (this.HandleGamepadRepeats) {
|
||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
|
@ -583,6 +656,7 @@ namespace MLEM.Input {
|
||||||
/// <param name="button">The button to query</param>
|
/// <param name="button">The button to query</param>
|
||||||
/// <param name="index">The zero-based index of the gamepad, or -1 for any gamepad</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>
|
/// <returns>Whether the given button is pressed</returns>
|
||||||
|
[Obsolete("This method is deprecated. Use the GenericInput version IsPressedIgnoreRepeats instead.")]
|
||||||
public bool IsGamepadButtonPressedIgnoreRepeats(Buttons button, int index = -1) {
|
public bool IsGamepadButtonPressedIgnoreRepeats(Buttons button, int index = -1) {
|
||||||
if (this.InvertPressBehavior)
|
if (this.InvertPressBehavior)
|
||||||
return this.WasGamepadButtonDown(button, index) && this.IsGamepadButtonUp(button, index);
|
return this.WasGamepadButtonDown(button, index) && this.IsGamepadButtonUp(button, index);
|
||||||
|
@ -595,6 +669,7 @@ namespace MLEM.Input {
|
||||||
/// <param name="button">The button to query.</param>
|
/// <param name="button">The button to query.</param>
|
||||||
/// <param name="index">The zero-based index of the gamepad, or -1 for any gamepad.</param>
|
/// <param name="index">The zero-based index of the gamepad, or -1 for any gamepad.</param>
|
||||||
/// <returns>Whether the given button is pressed and the press is not consumed yet.</returns>
|
/// <returns>Whether the given button is pressed and the press is not consumed yet.</returns>
|
||||||
|
[Obsolete("This method is deprecated. Use the GenericInput version IsPressedAvailable instead.")]
|
||||||
public bool IsGamepadButtonPressedAvailable(Buttons button, int index = -1) {
|
public bool IsGamepadButtonPressedAvailable(Buttons button, int index = -1) {
|
||||||
return this.IsGamepadButtonPressed(button) && !this.IsPressConsumed(button, index) && (index < 0 || !this.IsPressConsumed(button));
|
return this.IsGamepadButtonPressed(button) && !this.IsPressConsumed(button, index) && (index < 0 || !this.IsPressConsumed(button));
|
||||||
}
|
}
|
||||||
|
@ -608,6 +683,7 @@ namespace MLEM.Input {
|
||||||
/// <param name="button">The button to query.</param>
|
/// <param name="button">The button to query.</param>
|
||||||
/// <param name="index">The zero-based index of the gamepad, or -1 for any gamepad.</param>
|
/// <param name="index">The zero-based index of the gamepad, or -1 for any gamepad.</param>
|
||||||
/// <returns>Whether the given button is pressed and the press is not consumed yet.</returns>
|
/// <returns>Whether the given button is pressed and the press is not consumed yet.</returns>
|
||||||
|
[Obsolete("This method is deprecated. Use the GenericInput version TryConsumePressed instead.")]
|
||||||
public bool TryConsumeGamepadButtonPressed(Buttons button, int index = -1) {
|
public bool TryConsumeGamepadButtonPressed(Buttons button, int index = -1) {
|
||||||
if (this.IsGamepadButtonPressedAvailable(button, index)) {
|
if (this.IsGamepadButtonPressedAvailable(button, index)) {
|
||||||
this.consumedPresses.Add((button, index));
|
this.consumedPresses.Add((button, index));
|
||||||
|
@ -650,6 +726,19 @@ namespace MLEM.Input {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a gesture to the <see cref="Gestures"/> collection and allows it to be queried using <see cref="GetGesture"/> and <see cref="GetViewportGesture"/> for the duration of the update frame that it was added on.
|
||||||
|
/// This method should be used when <see cref="ExternalGestureHandling"/> is set to true, but <see cref="GetGesture"/> and <see cref="GetViewportGesture"/> should still be available.
|
||||||
|
/// For more info, see https://mlem.ellpeck.de/articles/input.html#external-gesture-handling.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sample">The gesture sample to add.</param>
|
||||||
|
/// <exception cref="InvalidOperationException">Thrown if <see cref="ExternalGestureHandling"/> is false.</exception>
|
||||||
|
public void AddExternalGesture(GestureSample sample) {
|
||||||
|
if (!this.ExternalGestureHandling)
|
||||||
|
throw new InvalidOperationException($"Cannot add external gestures if {nameof(this.ExternalGestureHandling)} is false");
|
||||||
|
this.gestures.Add(sample);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns if a given control of any kind is down.
|
/// Returns if a given control of any kind is down.
|
||||||
/// This is a helper function that can be passed a <see cref="Keys"/>, <see cref="Buttons"/> or <see cref="MouseButton"/>.
|
/// This is a helper function that can be passed a <see cref="Keys"/>, <see cref="Buttons"/> or <see cref="MouseButton"/>.
|
||||||
|
@ -657,15 +746,21 @@ namespace MLEM.Input {
|
||||||
/// <param name="control">The control whose down state to query</param>
|
/// <param name="control">The control whose down state to query</param>
|
||||||
/// <param name="index">The index of the gamepad to query (if applicable), or -1 for any gamepad</param>
|
/// <param name="index">The index of the gamepad to query (if applicable), or -1 for any gamepad</param>
|
||||||
/// <returns>Whether the given control is down</returns>
|
/// <returns>Whether the given control is down</returns>
|
||||||
/// <exception cref="ArgumentException">If the passed control isn't of a supported type</exception>
|
|
||||||
public bool IsDown(GenericInput control, int index = -1) {
|
public bool IsDown(GenericInput control, int index = -1) {
|
||||||
switch (control.Type) {
|
switch (control.Type) {
|
||||||
case GenericInput.InputType.Keyboard:
|
case InputType.Keyboard:
|
||||||
return this.IsKeyDown(control);
|
return this.KeyboardState.IsKeyDown(control);
|
||||||
case GenericInput.InputType.Gamepad:
|
case InputType.Gamepad:
|
||||||
return this.IsGamepadButtonDown(control, index);
|
if (index < 0) {
|
||||||
case GenericInput.InputType.Mouse:
|
for (var i = 0; i < this.ConnectedGamepads; i++) {
|
||||||
return this.IsMouseButtonDown(control);
|
if (this.GetGamepadState(i).GetAnalogValue(control) > this.GamepadButtonDeadzone)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return this.GetGamepadState(index).GetAnalogValue(control) > this.GamepadButtonDeadzone;
|
||||||
|
case InputType.Mouse:
|
||||||
|
return this.MouseState.GetState(control) == ButtonState.Pressed;
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -678,15 +773,75 @@ namespace MLEM.Input {
|
||||||
/// <param name="control">The control whose up state to query</param>
|
/// <param name="control">The control whose up state to query</param>
|
||||||
/// <param name="index">The index of the gamepad to query (if applicable), or -1 for any gamepad</param>
|
/// <param name="index">The index of the gamepad to query (if applicable), or -1 for any gamepad</param>
|
||||||
/// <returns>Whether the given control is up.</returns>
|
/// <returns>Whether the given control is up.</returns>
|
||||||
/// <exception cref="ArgumentException">If the passed control isn't of a supported type</exception>
|
|
||||||
public bool IsUp(GenericInput control, int index = -1) {
|
public bool IsUp(GenericInput control, int index = -1) {
|
||||||
switch (control.Type) {
|
switch (control.Type) {
|
||||||
case GenericInput.InputType.Keyboard:
|
case InputType.Keyboard:
|
||||||
return this.IsKeyUp(control);
|
return this.KeyboardState.IsKeyUp(control);
|
||||||
case GenericInput.InputType.Gamepad:
|
case InputType.Gamepad:
|
||||||
return this.IsGamepadButtonUp(control, index);
|
if (index < 0) {
|
||||||
case GenericInput.InputType.Mouse:
|
for (var i = 0; i < this.ConnectedGamepads; i++) {
|
||||||
return this.IsMouseButtonUp(control);
|
if (this.GetGamepadState(i).GetAnalogValue(control) <= this.GamepadButtonDeadzone)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return this.GetGamepadState(index).GetAnalogValue(control) <= this.GamepadButtonDeadzone;
|
||||||
|
case InputType.Mouse:
|
||||||
|
return this.MouseState.GetState(control) == ButtonState.Released;
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns if a given control of any kind was down in the last update call.
|
||||||
|
/// This is a helper function that can be passed a <see cref="Keys"/>, <see cref="Buttons"/> or <see cref="MouseButton"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="control">The control whose down state to query</param>
|
||||||
|
/// <param name="index">The index of the gamepad to query (if applicable), or -1 for any gamepad</param>
|
||||||
|
/// <returns>Whether the given control was down</returns>
|
||||||
|
public bool WasDown(GenericInput control, int index = -1) {
|
||||||
|
switch (control.Type) {
|
||||||
|
case InputType.Keyboard:
|
||||||
|
return this.LastKeyboardState.IsKeyDown(control);
|
||||||
|
case InputType.Gamepad:
|
||||||
|
if (index < 0) {
|
||||||
|
for (var i = 0; i < this.ConnectedGamepads; i++) {
|
||||||
|
if (this.GetLastGamepadState(i).GetAnalogValue(control) > this.GamepadButtonDeadzone)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return this.GetLastGamepadState(index).GetAnalogValue(control) > this.GamepadButtonDeadzone;
|
||||||
|
case InputType.Mouse:
|
||||||
|
return this.LastMouseState.GetState(control) == ButtonState.Pressed;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns if a given control of any kind was up in the last update call.
|
||||||
|
/// This is a helper function that can be passed a <see cref="Keys"/>, <see cref="Buttons"/> or <see cref="MouseButton"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="control">The control whose up state to query</param>
|
||||||
|
/// <param name="index">The index of the gamepad to query (if applicable), or -1 for any gamepad</param>
|
||||||
|
/// <returns>Whether the given control was up.</returns>
|
||||||
|
public bool WasUp(GenericInput control, int index = -1) {
|
||||||
|
switch (control.Type) {
|
||||||
|
case InputType.Keyboard:
|
||||||
|
return this.LastKeyboardState.IsKeyUp(control);
|
||||||
|
case InputType.Gamepad:
|
||||||
|
if (index < 0) {
|
||||||
|
for (var i = 0; i < this.ConnectedGamepads; i++) {
|
||||||
|
if (this.GetLastGamepadState(i).GetAnalogValue(control) <= this.GamepadButtonDeadzone)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return this.GetLastGamepadState(index).GetAnalogValue(control) <= this.GamepadButtonDeadzone;
|
||||||
|
case InputType.Mouse:
|
||||||
|
return this.LastMouseState.GetState(control) == ButtonState.Released;
|
||||||
default:
|
default:
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -694,23 +849,47 @@ namespace MLEM.Input {
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns if a given control of any kind is pressed.
|
/// Returns if a given control of any kind is pressed.
|
||||||
/// This is a helper function that can be passed a <see cref="Keys"/>, <see cref="Buttons"/> or <see cref="MouseButton"/>.
|
/// If <see cref="HandleKeyboardRepeats"/> or <see cref="HandleGamepadRepeats"/> are true, this method will also return true to signify a key or gamepad button repeat.
|
||||||
|
/// An input is considered pressed if it was not down the last update call, but is down the current update call. If <see cref="InvertPressBehavior"/> is true, this behavior is inverted.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="control">The control whose pressed state to query</param>
|
/// <param name="control">The control whose pressed state to query</param>
|
||||||
/// <param name="index">The index of the gamepad to query (if applicable), or -1 for any gamepad</param>
|
/// <param name="index">The index of the gamepad to query (if applicable), or -1 for any gamepad</param>
|
||||||
/// <returns>Whether the given control is pressed.</returns>
|
/// <returns>Whether the given control is pressed.</returns>
|
||||||
/// <exception cref="ArgumentException">If the passed control isn't of a supported type</exception>
|
|
||||||
public bool IsPressed(GenericInput control, int index = -1) {
|
public bool IsPressed(GenericInput control, int index = -1) {
|
||||||
|
// handle repeat events for specific inputs, and delegate to default "ignore repeats" behavior otherwise
|
||||||
switch (control.Type) {
|
switch (control.Type) {
|
||||||
case GenericInput.InputType.Keyboard:
|
case InputType.Keyboard:
|
||||||
return this.IsKeyPressed(control);
|
if (this.HandleKeyboardRepeats && (Keys) control == this.heldKey && this.triggerKeyRepeat)
|
||||||
case GenericInput.InputType.Gamepad:
|
return true;
|
||||||
return this.IsGamepadButtonPressed(control, index);
|
break;
|
||||||
case GenericInput.InputType.Mouse:
|
case InputType.Gamepad:
|
||||||
return this.IsMouseButtonPressed(control);
|
if (this.HandleGamepadRepeats) {
|
||||||
default:
|
if (index < 0) {
|
||||||
return false;
|
for (var i = 0; i < this.ConnectedGamepads; i++) {
|
||||||
|
if (this.heldGamepadButtons[i] == (Buttons) control && this.triggerGamepadButtonRepeat[i])
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else if (this.heldGamepadButtons[index] == (Buttons) control && this.triggerGamepadButtonRepeat[index]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
return this.IsPressedIgnoreRepeats(control, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An input is considered pressed if it was not down the last update call, but is down the current update call. If <see cref="InvertPressBehavior"/> is true, this behavior is inverted.
|
||||||
|
/// This has the same behavior as <see cref="IsPressed"/>, but ignores keyboard and gamepad repeat events.
|
||||||
|
/// If <see cref="HandleKeyboardRepeats"/> and <see cref="HandleGamepadRepeats"/> are false, this method does the same as <see cref="IsPressed"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="control">The control whose pressed state to query</param>
|
||||||
|
/// <param name="index">The index of the gamepad to query (if applicable), or -1 for any gamepad</param>
|
||||||
|
/// <returns>Whether the given control is pressed, ignoring repeat events.</returns>
|
||||||
|
public bool IsPressedIgnoreRepeats(GenericInput control, int index = -1) {
|
||||||
|
if (this.InvertPressBehavior)
|
||||||
|
return this.WasDown(control, index) && this.IsUp(control, index);
|
||||||
|
return this.WasUp(control, index) && this.IsDown(control, index);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -720,16 +899,7 @@ namespace MLEM.Input {
|
||||||
/// <param name="index">The index of the gamepad to query (if applicable), or -1 for any gamepad.</param>
|
/// <param name="index">The index of the gamepad to query (if applicable), or -1 for any gamepad.</param>
|
||||||
/// <returns>Whether the given control is pressed and the press is not consumed yet.</returns>
|
/// <returns>Whether the given control is pressed and the press is not consumed yet.</returns>
|
||||||
public bool IsPressedAvailable(GenericInput control, int index = -1) {
|
public bool IsPressedAvailable(GenericInput control, int index = -1) {
|
||||||
switch (control.Type) {
|
return this.IsPressed(control, index) && !this.IsPressConsumed(control, index);
|
||||||
case GenericInput.InputType.Keyboard:
|
|
||||||
return this.IsKeyPressedAvailable(control);
|
|
||||||
case GenericInput.InputType.Gamepad:
|
|
||||||
return this.IsGamepadButtonPressedAvailable(control, index);
|
|
||||||
case GenericInput.InputType.Mouse:
|
|
||||||
return this.IsMouseButtonPressedAvailable(control);
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -740,16 +910,48 @@ namespace MLEM.Input {
|
||||||
/// <param name="index">The index of the gamepad to query (if applicable), or -1 for any gamepad.</param>
|
/// <param name="index">The index of the gamepad to query (if applicable), or -1 for any gamepad.</param>
|
||||||
/// <returns>Whether the given control is pressed and the press is not consumed yet.</returns>
|
/// <returns>Whether the given control is pressed and the press is not consumed yet.</returns>
|
||||||
public bool TryConsumePressed(GenericInput control, int index = -1) {
|
public bool TryConsumePressed(GenericInput control, int index = -1) {
|
||||||
switch (control.Type) {
|
if (this.IsPressedAvailable(control, index)) {
|
||||||
case GenericInput.InputType.Keyboard:
|
this.consumedPresses.Add((control, index));
|
||||||
return this.TryConsumeKeyPressed(control);
|
return true;
|
||||||
case GenericInput.InputType.Gamepad:
|
|
||||||
return this.TryConsumeGamepadButtonPressed(control, index);
|
|
||||||
case GenericInput.InputType.Mouse:
|
|
||||||
return this.TryConsumeMouseButtonPressed(control);
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns if a given control of any kind was just let go this frame, and had been down for less than the given <paramref name="time"/> before that. Essentially, this action signifies a short press action.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="control">The control whose pressed state to query.</param>
|
||||||
|
/// <param name="time">The maximum time that the control should have been down for.</param>
|
||||||
|
/// <param name="index">The index of the gamepad to query (if applicable), or -1 for any gamepad.</param>
|
||||||
|
/// <returns>Whether the given control was pressed for less than the given time.</returns>
|
||||||
|
public bool WasPressedForLess(GenericInput control, TimeSpan time, int index = -1) {
|
||||||
|
return this.WasDown(control, index) && this.IsUp(control, index) && this.GetDownTime(control, index) < time;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns if a given control of any kind was just let go this frame, and had been down for less than the given <paramref name="time"/> before that, and if the press has not been consumed yet using <see cref="TryConsumePressedForLess"/>. Essentially, this action signifies a short press action.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="control">The control whose pressed state to query.</param>
|
||||||
|
/// <param name="time">The maximum time that the control should have been down for.</param>
|
||||||
|
/// <param name="index">The index of the gamepad to query (if applicable), or -1 for any gamepad.</param>
|
||||||
|
/// <returns>Whether the given control was pressed for less than the given time, and the press has not been consumed yet.</returns>
|
||||||
|
public bool WasPressedForLessAvailable(GenericInput control, TimeSpan time, int index = -1) {
|
||||||
|
return this.WasPressedForLess(control, time, index) && !this.IsPressConsumed(control, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns if a given control of any kind was just let go this frame, and had been down for less than the given <paramref name="time"/> before that, and if the press has not been consumed yet using <see cref="TryConsumePressedForLess"/>, and marks the press as consumed if it is. Essentially, this action signifies a short press action.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="control">The control whose pressed state to query.</param>
|
||||||
|
/// <param name="time">The maximum time that the control should have been down for.</param>
|
||||||
|
/// <param name="index">The index of the gamepad to query (if applicable), or -1 for any gamepad.</param>
|
||||||
|
/// <returns>Whether the given control was pressed for less than the given time, and the press was successfully consumed.</returns>
|
||||||
|
public bool TryConsumePressedForLess(GenericInput control, TimeSpan time, int index = -1) {
|
||||||
|
if (this.WasPressedForLessAvailable(control, time, index)) {
|
||||||
|
this.consumedPresses.Add((control, index));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc cref="IsDown"/>
|
/// <inheritdoc cref="IsDown"/>
|
||||||
|
@ -790,23 +992,26 @@ namespace MLEM.Input {
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tries to retrieve the amount of time that a given <see cref="GenericInput"/> has been held down for.
|
/// Tries to retrieve the amount of time that a given <see cref="GenericInput"/> has been held down for.
|
||||||
/// If the input is currently down, this method returns true and the amount of time that it has been down for is stored in <paramref name="downTime"/>.
|
/// If the input is currently down or has been down previously, this method returns true and the amount of time that it has currently or last been down for is stored in <paramref name="downTime"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="input">The input whose down time to query.</param>
|
/// <param name="input">The input whose down time to query.</param>
|
||||||
/// <param name="downTime">The resulting down time, or <see cref="TimeSpan.Zero"/> if the input is not being held.</param>
|
/// <param name="downTime">The resulting down time, or <see cref="TimeSpan.Zero"/> if the input is not being held.</param>
|
||||||
/// <param name="index">The index of the gamepad to query (if applicable), or -1 for any gamepad.</param>
|
/// <param name="index">The index of the gamepad to query (if applicable), or -1 for any gamepad.</param>
|
||||||
/// <returns>Whether the input is currently being held.</returns>
|
/// <returns>Whether the input is currently being held.</returns>
|
||||||
public bool TryGetDownTime(GenericInput input, out TimeSpan downTime, int index = -1) {
|
public bool TryGetDownTime(GenericInput input, out TimeSpan downTime, int index = -1) {
|
||||||
if (this.inputsDown.TryGetValue((input, index), out var start)) {
|
if (this.inputDownTimes.TryGetValue((input, index), out var wentDown)) {
|
||||||
downTime = DateTime.UtcNow - start;
|
// if we're currently down, we return the amount of time we've been down for so far
|
||||||
|
// if we're not currently down, we return the last amount of time we were down for
|
||||||
|
downTime = (this.IsDown(input) || !this.inputUpTimes.TryGetValue((input, index), out var wentUp) ? DateTime.UtcNow : wentUp) - wentDown;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
downTime = default;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the amount of time that a given <see cref="GenericInput"/> has been held down for.
|
/// Returns the current or last amount of time that a given <see cref="GenericInput"/> has been held down for.
|
||||||
/// If this input isn't currently down, this method returns <see cref="TimeSpan.Zero"/>.
|
/// If this input isn't currently down and has not been down previously, this method returns <see cref="TimeSpan.Zero"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="input">The input whose down time to query.</param>
|
/// <param name="input">The input whose down time to query.</param>
|
||||||
/// <param name="index">The index of the gamepad to query (if applicable), or -1 for any gamepad.</param>
|
/// <param name="index">The index of the gamepad to query (if applicable), or -1 for any gamepad.</param>
|
||||||
|
@ -818,23 +1023,26 @@ namespace MLEM.Input {
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tries to retrieve the amount of time that a given <see cref="GenericInput"/> has been up for since the last time it was down.
|
/// Tries to retrieve the amount of time that a given <see cref="GenericInput"/> has been up for since the last time it was down.
|
||||||
/// If the input is currently up, this method returns true and the amount of time that it has been up for is stored in <paramref name="upTime"/>.
|
/// If the input has previously been down, this method returns true and the amount of time that it has been up for is stored in <paramref name="upTime"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="input">The input whose up time to query.</param>
|
/// <param name="input">The input whose up time to query.</param>
|
||||||
/// <param name="upTime">The resulting up time, or <see cref="TimeSpan.Zero"/> if the input is being held.</param>
|
/// <param name="upTime">The resulting up time, or <see cref="TimeSpan.Zero"/> if the input is being held.</param>
|
||||||
/// <param name="index">The index of the gamepad to query (if applicable), or -1 for any gamepad.</param>
|
/// <param name="index">The index of the gamepad to query (if applicable), or -1 for any gamepad.</param>
|
||||||
/// <returns>Whether the input is currently up.</returns>
|
/// <returns>Whether the input is currently up.</returns>
|
||||||
public bool TryGetUpTime(GenericInput input, out TimeSpan upTime, int index = -1) {
|
public bool TryGetUpTime(GenericInput input, out TimeSpan upTime, int index = -1) {
|
||||||
if (this.inputUpTimes.TryGetValue((input, index), out var start)) {
|
if (this.inputUpTimes.TryGetValue((input, index), out var wentUp)) {
|
||||||
upTime = DateTime.UtcNow - start;
|
// if we're currently up, we return the amount of time we've been up for so far
|
||||||
|
// if we're not currently up, we return the last amount of time we were up for
|
||||||
|
upTime = (this.IsUp(input) || !this.inputDownTimes.TryGetValue((input, index), out var wentDown) ? DateTime.UtcNow : wentDown) - wentUp;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
upTime = default;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the amount of time that a given <see cref="GenericInput"/> has been up for since the last time it was down.
|
/// Returns the amount of time that a given <see cref="GenericInput"/> has last been up for since the last time it was down.
|
||||||
/// If this input isn't currently up, this method returns <see cref="TimeSpan.Zero"/>.
|
/// If this input hasn't been down previously, this method returns <see cref="TimeSpan.Zero"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="input">The input whose up time to query.</param>
|
/// <param name="input">The input whose up time to query.</param>
|
||||||
/// <param name="index">The index of the gamepad to query (if applicable), or -1 for any gamepad.</param>
|
/// <param name="index">The index of the gamepad to query (if applicable), or -1 for any gamepad.</param>
|
||||||
|
@ -857,12 +1065,13 @@ namespace MLEM.Input {
|
||||||
lastPressTime = DateTime.UtcNow - start;
|
lastPressTime = DateTime.UtcNow - start;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
lastPressTime = default;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the amount of time that has passed since a given <see cref="GenericInput"/> last counted as pressed.
|
/// Returns the amount of time that has passed since a given <see cref="GenericInput"/> last counted as pressed.
|
||||||
/// If this input hasn't been pressed previously, or is currently pressed, this method returns <see cref="TimeSpan.Zero"/>.
|
/// If this input hasn't been pressed previously, this method returns <see cref="TimeSpan.Zero"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="input">The input whose up time to query.</param>
|
/// <param name="input">The input whose up time to query.</param>
|
||||||
/// <param name="index">The index of the gamepad to query (if applicable), or -1 for any gamepad.</param>
|
/// <param name="index">The index of the gamepad to query (if applicable), or -1 for any gamepad.</param>
|
||||||
|
|
|
@ -3,6 +3,10 @@ using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.Serialization;
|
using System.Runtime.Serialization;
|
||||||
|
|
||||||
|
#if NET452
|
||||||
|
using MLEM.Extensions;
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace MLEM.Input {
|
namespace MLEM.Input {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A keybind represents a generic way to trigger input.
|
/// A keybind represents a generic way to trigger input.
|
||||||
|
@ -13,8 +17,10 @@ namespace MLEM.Input {
|
||||||
[DataContract]
|
[DataContract]
|
||||||
public class Keybind : IComparable<Keybind>, IComparable {
|
public class Keybind : IComparable<Keybind>, IComparable {
|
||||||
|
|
||||||
|
private static readonly Combination[] EmptyCombinations = new Combination[0];
|
||||||
|
|
||||||
[DataMember]
|
[DataMember]
|
||||||
private Combination[] combinations = Array.Empty<Combination>();
|
private Combination[] combinations = Keybind.EmptyCombinations;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new keybind and adds the given key and modifiers using <see cref="Add(MLEM.Input.GenericInput,MLEM.Input.GenericInput[])"/>
|
/// Creates a new keybind and adds the given key and modifiers using <see cref="Add(MLEM.Input.GenericInput,MLEM.Input.GenericInput[])"/>
|
||||||
|
@ -77,7 +83,7 @@ namespace MLEM.Input {
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>This keybind, for chaining</returns>
|
/// <returns>This keybind, for chaining</returns>
|
||||||
public Keybind Clear() {
|
public Keybind Clear() {
|
||||||
this.combinations = Array.Empty<Combination>();
|
this.combinations = Keybind.EmptyCombinations;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,6 +123,21 @@ namespace MLEM.Input {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns whether this keybind was considered to be down in the last update call.
|
||||||
|
/// See <see cref="InputHandler.WasDown"/> 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 this keybind was considered to be down</returns>
|
||||||
|
public bool WasDown(InputHandler handler, int gamepadIndex = -1) {
|
||||||
|
foreach (var combination in this.combinations) {
|
||||||
|
if (combination.WasDown(handler, gamepadIndex))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns whether this keybind is considered to be pressed.
|
/// Returns whether this keybind is considered to be pressed.
|
||||||
/// See <see cref="InputHandler.IsPressed"/> for more information.
|
/// See <see cref="InputHandler.IsPressed"/> for more information.
|
||||||
|
@ -177,6 +198,54 @@ namespace MLEM.Input {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns whether any of this keybind's modifier keys were down in the last update call.
|
||||||
|
/// See <see cref="InputHandler.WasDown"/> 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 were down</returns>
|
||||||
|
public bool WasModifierDown(InputHandler handler, int gamepadIndex = -1) {
|
||||||
|
foreach (var combination in this.combinations) {
|
||||||
|
if (combination.WasModifierDown(handler, gamepadIndex))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the amount of time that this keybind has been held down for.
|
||||||
|
/// If this input isn't currently down, this method returns <see cref="TimeSpan.Zero"/>.
|
||||||
|
/// </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>The resulting down time, or <see cref="TimeSpan.Zero"/> if the input is not being held.</returns>
|
||||||
|
public TimeSpan GetDownTime(InputHandler handler, int gamepadIndex = -1) {
|
||||||
|
return this.combinations.Max(c => c.GetDownTime(handler, gamepadIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the amount of time that this keybind has been up for since the last time it was down.
|
||||||
|
/// If this input isn't currently up, this method returns <see cref="TimeSpan.Zero"/>.
|
||||||
|
/// </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>The resulting up time, or <see cref="TimeSpan.Zero"/> if the input is being held.</returns>
|
||||||
|
public TimeSpan GetUpTime(InputHandler handler, int gamepadIndex = -1) {
|
||||||
|
return this.combinations.Min(c => c.GetUpTime(handler, gamepadIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the amount of time that has passed since this keybind last counted as pressed.
|
||||||
|
/// If this input hasn't been pressed previously, or is currently pressed, this method returns <see cref="TimeSpan.Zero"/>.
|
||||||
|
/// </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>The resulting up time, or <see cref="TimeSpan.Zero"/> if the input has never been pressed, or is currently pressed.</returns>
|
||||||
|
public TimeSpan GetTimeSincePress(InputHandler handler, int gamepadIndex = -1) {
|
||||||
|
return this.combinations.Min(c => c.GetTimeSincePress(handler, gamepadIndex));
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns an enumerable of all of the combinations that this keybind currently contains
|
/// Returns an enumerable of all of the combinations that this keybind currently contains
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -281,7 +350,7 @@ namespace MLEM.Input {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns whether this combination is currently down
|
/// Returns whether this combination is currently down.
|
||||||
/// See <see cref="InputHandler.IsDown"/> for more information.
|
/// See <see cref="InputHandler.IsDown"/> for more information.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="handler">The input handler to query the keys with</param>
|
/// <param name="handler">The input handler to query the keys with</param>
|
||||||
|
@ -291,6 +360,17 @@ namespace MLEM.Input {
|
||||||
return this.IsModifierDown(handler, gamepadIndex) && handler.IsDown(this.Key, gamepadIndex);
|
return this.IsModifierDown(handler, gamepadIndex) && handler.IsDown(this.Key, gamepadIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns whether this combination was down in the last upate call.
|
||||||
|
/// See <see cref="InputHandler.WasDown"/> 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 this combination was down</returns>
|
||||||
|
public bool WasDown(InputHandler handler, int gamepadIndex = -1) {
|
||||||
|
return this.WasModifierDown(handler, gamepadIndex) && handler.WasDown(this.Key, gamepadIndex);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns whether this combination is currently pressed.
|
/// Returns whether this combination is currently pressed.
|
||||||
/// See <see cref="InputHandler.IsPressed"/> for more information.
|
/// See <see cref="InputHandler.IsPressed"/> for more information.
|
||||||
|
@ -326,19 +406,64 @@ namespace MLEM.Input {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns whether this combination's modifier keys are currently down
|
/// Returns whether all of this combination's modifier keys are currently down.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="handler">The input handler to query the keys with</param>
|
/// <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>
|
/// <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>
|
/// <returns>Whether this combination's modifiers are down</returns>
|
||||||
public bool IsModifierDown(InputHandler handler, int gamepadIndex = -1) {
|
public bool IsModifierDown(InputHandler handler, int gamepadIndex = -1) {
|
||||||
if (this.Modifiers.Length <= 0)
|
|
||||||
return true;
|
|
||||||
foreach (var modifier in this.Modifiers) {
|
foreach (var modifier in this.Modifiers) {
|
||||||
if (handler.IsDown(modifier, gamepadIndex))
|
if (!handler.IsDown(modifier, gamepadIndex))
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
return false;
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns whether all of this combination's modifier keys were down in the last update call.
|
||||||
|
/// </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 were down</returns>
|
||||||
|
public bool WasModifierDown(InputHandler handler, int gamepadIndex = -1) {
|
||||||
|
foreach (var modifier in this.Modifiers) {
|
||||||
|
if (!handler.WasDown(modifier, gamepadIndex))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the amount of time that this combination has been held down for.
|
||||||
|
/// If this input isn't currently down, this method returns <see cref="TimeSpan.Zero"/>.
|
||||||
|
/// </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>The resulting down time, or <see cref="TimeSpan.Zero"/> if the input is not being held.</returns>
|
||||||
|
public TimeSpan GetDownTime(InputHandler handler, int gamepadIndex = -1) {
|
||||||
|
return handler.GetDownTime(this.Key, gamepadIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the amount of time that this combination has been up for since the last time it was down.
|
||||||
|
/// If this input isn't currently up, this method returns <see cref="TimeSpan.Zero"/>.
|
||||||
|
/// </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>The resulting up time, or <see cref="TimeSpan.Zero"/> if the input is being held.</returns>
|
||||||
|
public TimeSpan GetUpTime(InputHandler handler, int gamepadIndex = -1) {
|
||||||
|
return handler.GetUpTime(this.Key, gamepadIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the amount of time that has passed since this combination last counted as pressed.
|
||||||
|
/// If this input hasn't been pressed previously, or is currently pressed, this method returns <see cref="TimeSpan.Zero"/>.
|
||||||
|
/// </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>The resulting up time, or <see cref="TimeSpan.Zero"/> if the input has never been pressed, or is currently pressed.</returns>
|
||||||
|
public TimeSpan GetTimeSincePress(InputHandler handler, int gamepadIndex = -1) {
|
||||||
|
return handler.GetTimeSincePress(this.Key, gamepadIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Microsoft.Xna.Framework.Input;
|
using Microsoft.Xna.Framework.Input;
|
||||||
using MLEM.Misc;
|
|
||||||
|
|
||||||
namespace MLEM.Input {
|
namespace MLEM.Input {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -12,7 +12,7 @@ namespace MLEM.Input {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// All enum values of <see cref="ModifierKey"/>
|
/// All enum values of <see cref="ModifierKey"/>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static readonly ModifierKey[] ModifierKeys = EnumHelper.GetValues<ModifierKey>().ToArray();
|
public static readonly ModifierKey[] ModifierKeys = {ModifierKey.None, ModifierKey.Shift, ModifierKey.Control, ModifierKey.Alt};
|
||||||
private static readonly Dictionary<ModifierKey, Keys[]> KeysLookup = new Dictionary<ModifierKey, Keys[]> {
|
private static readonly Dictionary<ModifierKey, Keys[]> KeysLookup = new Dictionary<ModifierKey, Keys[]> {
|
||||||
{ModifierKey.Shift, new[] {Keys.LeftShift, Keys.RightShift}},
|
{ModifierKey.Shift, new[] {Keys.LeftShift, Keys.RightShift}},
|
||||||
{ModifierKey.Control, new[] {Keys.LeftControl, Keys.RightControl}},
|
{ModifierKey.Control, new[] {Keys.LeftControl, Keys.RightControl}},
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
|
||||||
using Microsoft.Xna.Framework.Input;
|
using Microsoft.Xna.Framework.Input;
|
||||||
using MLEM.Misc;
|
|
||||||
|
|
||||||
namespace MLEM.Input {
|
namespace MLEM.Input {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -12,7 +10,7 @@ namespace MLEM.Input {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// All enum values of <see cref="MouseButton"/>
|
/// All enum values of <see cref="MouseButton"/>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static readonly MouseButton[] MouseButtons = EnumHelper.GetValues<MouseButton>().ToArray();
|
public static readonly MouseButton[] MouseButtons = {MouseButton.Left, MouseButton.Middle, MouseButton.Right, MouseButton.Extra1, MouseButton.Extra2};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the <see cref="ButtonState"/> of the given mouse button.
|
/// Returns the <see cref="ButtonState"/> of the given mouse button.
|
||||||
|
|
|
@ -246,7 +246,7 @@ namespace MLEM.Input {
|
||||||
/// <returns>Whether text was successfully input.</returns>
|
/// <returns>Whether text was successfully input.</returns>
|
||||||
public bool OnTextInput(Keys key, char character) {
|
public bool OnTextInput(Keys key, char character) {
|
||||||
// FNA's text input event doesn't supply keys, so we handle this in Update
|
// FNA's text input event doesn't supply keys, so we handle this in Update
|
||||||
#if !FNA
|
#if !FNA
|
||||||
if (key == Keys.Back) {
|
if (key == Keys.Back) {
|
||||||
if (this.CaretPos > 0) {
|
if (this.CaretPos > 0) {
|
||||||
this.CaretPos--;
|
this.CaretPos--;
|
||||||
|
@ -261,9 +261,9 @@ namespace MLEM.Input {
|
||||||
return this.InsertText(character);
|
return this.InsertText(character);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
#else
|
#else
|
||||||
return this.InsertText(character);
|
return this.InsertText(character);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -274,8 +274,8 @@ namespace MLEM.Input {
|
||||||
public void Update(GameTime time, InputHandler input) {
|
public void Update(GameTime time, InputHandler input) {
|
||||||
this.UpdateTextDataIfDirty();
|
this.UpdateTextDataIfDirty();
|
||||||
|
|
||||||
|
#if FNA
|
||||||
// FNA's text input event doesn't supply keys, so we handle this here
|
// FNA's text input event doesn't supply keys, so we handle this here
|
||||||
#if FNA
|
|
||||||
if (this.CaretPos > 0 && input.TryConsumePressed(Keys.Back)) {
|
if (this.CaretPos > 0 && input.TryConsumePressed(Keys.Back)) {
|
||||||
this.CaretPos--;
|
this.CaretPos--;
|
||||||
this.RemoveText(this.CaretPos, 1);
|
this.RemoveText(this.CaretPos, 1);
|
||||||
|
@ -284,21 +284,21 @@ namespace MLEM.Input {
|
||||||
} else if (this.Multiline && input.TryConsumePressed(Keys.Enter)) {
|
} else if (this.Multiline && input.TryConsumePressed(Keys.Enter)) {
|
||||||
this.InsertText('\n');
|
this.InsertText('\n');
|
||||||
} else
|
} else
|
||||||
#endif
|
#endif
|
||||||
if (this.CaretPos > 0 && input.TryConsumePressed(Keys.Left)) {
|
if (this.CaretPos > 0 && input.TryConsumePressed(Keys.Left)) {
|
||||||
this.CaretPos--;
|
this.CaretPos--;
|
||||||
} else if (this.CaretPos < this.text.Length && input.TryConsumePressed(Keys.Right)) {
|
} else if (this.CaretPos < this.text.Length && input.TryConsumePressed(Keys.Right)) {
|
||||||
this.CaretPos++;
|
this.CaretPos++;
|
||||||
} else if (this.Multiline && input.IsKeyPressedAvailable(Keys.Up) && this.MoveCaretToLine(this.CaretLine - 1)) {
|
} else if (this.Multiline && input.IsPressedAvailable(Keys.Up) && this.MoveCaretToLine(this.CaretLine - 1)) {
|
||||||
input.TryConsumePressed(Keys.Up);
|
input.TryConsumePressed(Keys.Up);
|
||||||
} else if (this.Multiline && input.IsKeyPressedAvailable(Keys.Down) && this.MoveCaretToLine(this.CaretLine + 1)) {
|
} else if (this.Multiline && input.IsPressedAvailable(Keys.Down) && this.MoveCaretToLine(this.CaretLine + 1)) {
|
||||||
input.TryConsumePressed(Keys.Down);
|
input.TryConsumePressed(Keys.Down);
|
||||||
} else if (this.CaretPos != 0 && input.TryConsumePressed(Keys.Home)) {
|
} else if (this.CaretPos != 0 && input.TryConsumePressed(Keys.Home)) {
|
||||||
this.CaretPos = 0;
|
this.CaretPos = 0;
|
||||||
} else if (this.CaretPos != this.text.Length && input.TryConsumePressed(Keys.End)) {
|
} else if (this.CaretPos != this.text.Length && input.TryConsumePressed(Keys.End)) {
|
||||||
this.CaretPos = this.text.Length;
|
this.CaretPos = this.text.Length;
|
||||||
} else if (input.IsModifierKeyDown(ModifierKey.Control)) {
|
} else if (input.IsModifierKeyDown(ModifierKey.Control)) {
|
||||||
if (input.IsKeyPressedAvailable(Keys.V)) {
|
if (input.IsPressedAvailable(Keys.V)) {
|
||||||
var clip = this.PasteFromClipboardFunction?.Invoke();
|
var clip = this.PasteFromClipboardFunction?.Invoke();
|
||||||
if (clip != null) {
|
if (clip != null) {
|
||||||
this.InsertText(clip, true);
|
this.InsertText(clip, true);
|
||||||
|
@ -417,9 +417,10 @@ namespace MLEM.Input {
|
||||||
private bool FilterText(ref string text, bool removeMismatching) {
|
private bool FilterText(ref string text, bool removeMismatching) {
|
||||||
if (removeMismatching) {
|
if (removeMismatching) {
|
||||||
var result = new StringBuilder();
|
var result = new StringBuilder();
|
||||||
foreach (var c in text) {
|
foreach (var codePoint in new CodePointSource(text)) {
|
||||||
if (this.InputRule(this, c.ToCachedString()))
|
var character = char.ConvertFromUtf32(codePoint);
|
||||||
result.Append(c);
|
if (this.InputRule(this, character))
|
||||||
|
result.Append(character);
|
||||||
}
|
}
|
||||||
text = result.ToString();
|
text = result.ToString();
|
||||||
} else if (!this.InputRule(this, text))
|
} else if (!this.InputRule(this, text))
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netstandard2.0</TargetFramework>
|
<TargetFrameworks>net452;netstandard2.0;net6.0</TargetFrameworks>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<ProduceReferenceAssembly>true</ProduceReferenceAssembly>
|
<ProduceReferenceAssembly>true</ProduceReferenceAssembly>
|
||||||
|
<IsTrimmable>true</IsTrimmable>
|
||||||
<RootNamespace>MLEM</RootNamespace>
|
<RootNamespace>MLEM</RootNamespace>
|
||||||
<DefineConstants>$(DefineConstants);FNA</DefineConstants>
|
<DefineConstants>$(DefineConstants);FNA</DefineConstants>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
@ -20,9 +21,11 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\FNA\FNA.Core.csproj">
|
<ProjectReference Include="..\FNA\FNA.csproj">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</ProjectReference>
|
</ProjectReference>
|
||||||
|
|
||||||
|
<PackageReference Include="System.ValueTuple" Version="4.5.0" Condition="'$(TargetFramework)'=='net452'" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netstandard2.0</TargetFramework>
|
<TargetFrameworks>net452;netstandard2.0;net6.0</TargetFrameworks>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<ProduceReferenceAssembly>true</ProduceReferenceAssembly>
|
<ProduceReferenceAssembly>true</ProduceReferenceAssembly>
|
||||||
|
<IsTrimmable>true</IsTrimmable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
|
@ -21,6 +22,8 @@
|
||||||
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.0.1641">
|
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.0.1641">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
|
||||||
|
<PackageReference Include="System.ValueTuple" Version="4.5.0" Condition="'$(TargetFramework)'=='net452'" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
@ -71,7 +71,10 @@ namespace MLEM.Misc {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// All <see cref="Direction2"/> enum values
|
/// All <see cref="Direction2"/> enum values
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static readonly Direction2[] All = EnumHelper.GetValues<Direction2>().ToArray();
|
public static readonly Direction2[] All = {
|
||||||
|
Direction2.None, Direction2.Up, Direction2.Right, Direction2.Down, Direction2.Left,
|
||||||
|
Direction2.UpRight, Direction2.DownRight, Direction2.UpLeft, Direction2.DownLeft
|
||||||
|
};
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The <see cref="Direction2.Up"/> through <see cref="Direction2.Left"/> directions
|
/// The <see cref="Direction2.Up"/> through <see cref="Direction2.Left"/> directions
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
@ -1,30 +1,71 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using Microsoft.Xna.Framework.Input;
|
using Microsoft.Xna.Framework.Input;
|
||||||
|
|
||||||
namespace MLEM.Misc {
|
namespace MLEM.Misc {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A helper class that allows easier usage of <see cref="Enum"/> values.
|
/// A helper class that allows easier usage of <see cref="Enum"/> values.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[Obsolete("EnumHelper has been moved into the DynamicEnums library: https://www.nuget.org/packages/DynamicEnums")]
|
||||||
public static class EnumHelper {
|
public static class EnumHelper {
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// All values of the <see cref="Buttons"/> enum.
|
/// All values of the <see cref="Buttons"/> enum.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static readonly Buttons[] Buttons = EnumHelper.GetValues<Buttons>().ToArray();
|
[Obsolete("This field has been moved to InputHandler.AllButtons")]
|
||||||
|
public static readonly Buttons[] Buttons = EnumHelper.GetValues<Buttons>();
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// All values of the <see cref="Keys"/> enum.
|
/// All values of the <see cref="Keys"/> enum.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static readonly Keys[] Keys = EnumHelper.GetValues<Keys>().ToArray();
|
[Obsolete("This field has been moved to InputHandler.AllKeys")]
|
||||||
|
public static readonly Keys[] Keys = EnumHelper.GetValues<Keys>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns all of the values of the given enum type.
|
/// Returns an array containing all of the values of the given enum type.
|
||||||
|
/// Note that this method is a version-independent equivalent of .NET 5's <c>Enum.GetValues<TEnum></c>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T">The type whose enum to get</typeparam>
|
/// <typeparam name="T">The type whose enum to get</typeparam>
|
||||||
/// <returns>An enumerable of the values of the enum, in declaration order.</returns>
|
/// <returns>An enumerable of the values of the enum, in declaration order.</returns>
|
||||||
public static IEnumerable<T> GetValues<T>() {
|
public static T[] GetValues<T>() where T : struct, Enum {
|
||||||
return Enum.GetValues(typeof(T)).Cast<T>();
|
#if NET6_0_OR_GREATER
|
||||||
|
return Enum.GetValues<T>();
|
||||||
|
#else
|
||||||
|
return (T[]) Enum.GetValues(typeof(T));
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns all of the defined values from the given enum type <typeparamref name="T"/> which are contained in <paramref name="combinedFlag"/>.
|
||||||
|
/// Note that, if combined flags are defined in <typeparamref name="T"/>, and <paramref name="combinedFlag"/> contains them, they will also be returned.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="combinedFlag">The combined flags whose individual flags to return.</param>
|
||||||
|
/// <param name="includeZero">Whether the enum value 0 should also be returned, if <typeparamref name="T"/> contains one.</param>
|
||||||
|
/// <typeparam name="T">The type of enum.</typeparam>
|
||||||
|
/// <returns>All of the flags that make up <paramref name="combinedFlag"/>.</returns>
|
||||||
|
public static IEnumerable<T> GetFlags<T>(T combinedFlag, bool includeZero = true) where T : struct, Enum {
|
||||||
|
foreach (var flag in EnumHelper.GetValues<T>()) {
|
||||||
|
if (combinedFlag.HasFlag(flag) && (includeZero || Convert.ToInt64(flag) != 0))
|
||||||
|
yield return flag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns all of the defined unique flags from the given enum type <typeparamref name="T"/> which are contained in <paramref name="combinedFlag"/>.
|
||||||
|
/// Any combined flags (flags that aren't powers of two) which are defined in <typeparamref name="T"/> will not be returned.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="combinedFlag">The combined flags whose individual flags to return.</param>
|
||||||
|
/// <typeparam name="T">The type of enum.</typeparam>
|
||||||
|
/// <returns>All of the unique flags that make up <paramref name="combinedFlag"/>.</returns>
|
||||||
|
public static IEnumerable<T> GetUniqueFlags<T>(T combinedFlag) where T : struct, Enum {
|
||||||
|
var uniqueFlag = 1;
|
||||||
|
foreach (var flag in EnumHelper.GetValues<T>()) {
|
||||||
|
var flagValue = Convert.ToInt64(flag);
|
||||||
|
// GetValues is always ordered by binary value, so we can be sure that the next flag is bigger than the last
|
||||||
|
while (uniqueFlag < flagValue)
|
||||||
|
uniqueFlag <<= 1;
|
||||||
|
if (flagValue == uniqueFlag && combinedFlag.HasFlag(flag))
|
||||||
|
yield return flag;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Runtime.Serialization;
|
|
||||||
|
|
||||||
namespace MLEM.Misc {
|
namespace MLEM.Misc {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -8,15 +7,20 @@ namespace MLEM.Misc {
|
||||||
/// A lot of MLEM components extend this class to allow for users to add additional data to them easily.
|
/// A lot of MLEM components extend this class to allow for users to add additional data to them easily.
|
||||||
/// This <see cref="IGenericDataHolder"/> implemention uses an underlying <see cref="Dictionary{String,Object}"/> that only keeps track of non-default values.
|
/// This <see cref="IGenericDataHolder"/> implemention uses an underlying <see cref="Dictionary{String,Object}"/> that only keeps track of non-default values.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DataContract]
|
|
||||||
public class GenericDataHolder : IGenericDataHolder {
|
public class GenericDataHolder : IGenericDataHolder {
|
||||||
|
|
||||||
[DataMember(EmitDefaultValue = false)]
|
private static readonly string[] EmptyStrings = new string[0];
|
||||||
private Dictionary<string, object> data;
|
private Dictionary<string, object> data;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
[Obsolete("This method will be removed in a future update in favor of the generic SetData<T>.")]
|
||||||
public void SetData(string key, object data) {
|
public void SetData(string key, object data) {
|
||||||
if (data == default) {
|
this.SetData<object>(key, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void SetData<T>(string key, T data) {
|
||||||
|
if (EqualityComparer<T>.Default.Equals(data, default)) {
|
||||||
if (this.data != null)
|
if (this.data != null)
|
||||||
this.data.Remove(key);
|
this.data.Remove(key);
|
||||||
} else {
|
} else {
|
||||||
|
@ -34,9 +38,9 @@ namespace MLEM.Misc {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IReadOnlyCollection<string> GetDataKeys() {
|
public IEnumerable<string> GetDataKeys() {
|
||||||
if (this.data == null)
|
if (this.data == null)
|
||||||
return Array.Empty<string>();
|
return GenericDataHolder.EmptyStrings;
|
||||||
return this.data.Keys;
|
return this.data.Keys;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,8 +57,16 @@ namespace MLEM.Misc {
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="key">The key to store the data by</param>
|
/// <param name="key">The key to store the data by</param>
|
||||||
/// <param name="data">The data to store in the object</param>
|
/// <param name="data">The data to store in the object</param>
|
||||||
|
[Obsolete("This method will be removed in a future update in favor of the generic SetData<T>.")]
|
||||||
void SetData(string key, object data);
|
void SetData(string key, object data);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Store a piece of generic data on this object.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The key to store the data by</param>
|
||||||
|
/// <param name="data">The data to store in the object</param>
|
||||||
|
void SetData<T>(string key, T data);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns a piece of generic data of the given type on this object.
|
/// Returns a piece of generic data of the given type on this object.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -67,7 +79,7 @@ namespace MLEM.Misc {
|
||||||
/// Returns all of the generic data that this object stores.
|
/// Returns all of the generic data that this object stores.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>The generic data on this object</returns>
|
/// <returns>The generic data on this object</returns>
|
||||||
IReadOnlyCollection<string> GetDataKeys();
|
IEnumerable<string> GetDataKeys();
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -95,6 +95,13 @@ namespace MLEM.Misc {
|
||||||
return new Padding(p.Left * scale, p.Right * scale, p.Top * scale, p.Bottom * scale);
|
return new Padding(p.Left * scale, p.Right * scale, p.Top * scale, p.Bottom * scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Scales a padding by a <see cref="Vector2"/>.
|
||||||
|
/// </summary>
|
||||||
|
public static Padding operator *(Padding p, Vector2 scale) {
|
||||||
|
return new Padding(p.Left * scale.X, p.Right * scale.X, p.Top * scale.Y, p.Bottom * scale.Y);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds two paddings together in a memberwise fashion.
|
/// Adds two paddings together in a memberwise fashion.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue