1
0
Fork 0
mirror of https://github.com/Ellpeck/MLEM.git synced 2024-11-27 06:58:34 +01:00

Compare commits

...

75 commits

Author SHA1 Message Date
Ell
40a697a96c release 6.2.0 2023-06-28 13:37:05 +02:00
Ell
d85d6e8968 Added a copy constructor to UiStyle 2023-06-28 13:35:32 +02:00
Ell
e4e7191d8d Include control characters in TextInput FileNames and PathNames rules 2023-06-21 10:37:48 +02:00
Ell
04050b9144 Added TextField.EnterReceiver 2023-06-21 00:10:52 +02:00
Ell
d81efe1d44 fixed multiline text fields not accepting newline characters (since aef6f7b) 2023-06-21 00:07:26 +02:00
Ell
7b9b177453 Added UiControls.PressElement 2023-06-20 23:23:38 +02:00
Ell
f652854c1d Added AddCustomStyle and ApplyCustomStyle to UiStyle to allow for easy custom styling of elements 2023-06-14 14:54:49 +02:00
Ell
d48b7e2e71 Added UiAnimation system 2023-06-14 10:21:32 +02:00
Ell
985dc74376 Made elements' ui styles be inherited by their children 2023-06-14 09:33:08 +02:00
Ell
d69cd80b72 updated docfx and added serve argument to cake script 2023-06-09 12:59:12 +02:00
Ell
f5f925fab3 fixed an exception when a panel that is not currently part of a ui has a child removed (since 3127ad5) 2023-05-26 23:08:45 +02:00
Ell
34cb5210b5 improved test separation for MG and FNA projects 2023-05-25 09:22:38 +02:00
Ell
2627736283 exclude non-MLEM projects from coverage 2023-05-24 23:57:53 +02:00
Ell
5266d00796 Fixed removing and later adding children to a scrolling panel showing the scroll bar erroneously 2023-05-23 11:18:10 +02:00
Ell
2463c27a5d Remove all elements from a UiSystem when it is disposed 2023-05-22 11:29:41 +02:00
Ell
b2f457088d fixed panels unnecessarily trying to ScrollSetup (regression 3127ad5) 2023-05-21 11:28:07 +02:00
Ell
30432e43d4 Fixed dropdown menu panels not updating their width when the dropdown's width changes 2023-05-21 11:19:24 +02:00
Ell
3127ad5b74 Allow elements with larger children to influence a panel's scrollable area 2023-05-21 11:11:52 +02:00
Ell
f1740b7b32 Added TextFormatter.StripAllFormatting 2023-05-19 19:30:45 +02:00
Ell
0766220a8e fixed runtime texture packer tests not working on some machines due to a texture being generated that is too tall 2023-05-19 19:25:11 +02:00
Ell
a1064984ec Allow specifying start and end indices when drawing a TokenizedString or Paragraph 2023-05-18 21:41:36 +02:00
Ell
a45a6adabd updated license years 2023-05-15 19:54:50 +02:00
Ell
bccef1f7f4 fixed FNA compatibility 2023-05-15 18:41:45 +02:00
Ell
e71450366b additional runtime texture packer improvements 2023-05-15 18:36:23 +02:00
Ell
4863b5504b restored FNA compatibility 2023-05-15 18:05:47 +02:00
Ell
99b45b09d9 Improved RuntimeTexturePacker performance for differently sized textures 2023-05-15 17:50:41 +02:00
Ell
e623eff02d added additional texture packer tests 2023-05-15 17:35:29 +02:00
Ell
1d4a2ebdf7 reduce header levels in README to allow for TOC navigation on the docs site 2023-05-10 23:06:34 +02:00
Ell
e09484cbe7 Fixed GetRandomWeightedEntry distribution not being equal for equal weights 2023-04-26 21:49:43 +02:00
Ell
230f2e954c Fixed TextInput and Slider still reacting to input when they are selected, but not part of the active root 2023-04-24 11:15:16 +02:00
Ell
b289bbd98e skip checkout when publishing docs 2023-04-16 13:40:27 +02:00
Ell
c77ec3765c include correct readme file in packages 2023-04-16 13:37:43 +02:00
Ell
a8e5c93fe4 prepend ci. to prerelease build numbers to be more in line with semver spec 2023-04-16 13:30:49 +02:00
Ell
39ade19d47 publish tests and coverage correctly 2023-04-15 17:38:58 +02:00
Ell
08931b49ac ensure all stages run on the same node 2023-04-15 17:19:26 +02:00
Ell
1f315def2d Merge remote-tracking branch 'origin/main' 2023-04-15 15:12:02 +02:00
Ell
1e16c6fdc5 some docs website improvements with new docfx version 2023-04-15 15:11:50 +02:00
Ell
1b2cbb6afd ensure docs are published on a web agent 2023-04-12 22:19:57 +02:00
Ell
5f02e701d9 updated to the new docfx modern template 2023-04-08 15:30:42 +02:00
Ell
2265af3fae Improved the SquishingGroup algorithm by prioritizing each element's final size 2023-04-06 17:15:57 +02:00
Ell
1a7cb65cf2 Fixed Paragraph and Checkbox not reacting to SquishingGroup sizing properly 2023-04-06 15:54:24 +02:00
Ell
4994bb3d5d updated monogame .net tools 2023-04-06 15:25:20 +02:00
Ell
6607a5f48c net7.0 2023-03-29 23:31:30 +02:00
Ell
eda9531566 updated fnalibs for demos and tests 2023-03-29 22:17:09 +02:00
Ell
da2fab9b57 dependency and submodule update 2023-03-29 21:54:30 +02:00
Ell
e0d4bb3472 Ensure auto anchors keep their initial values if no siblings can be found (94a54c3) 2023-03-29 21:00:17 +02:00
Ell
db454ebd71 Fixed AutoInline elements overflowing into their parent if it's taller 2023-03-29 20:56:56 +02:00
Ell
94a54c336e Added AutoInlineCenter and AutoInlineBottom anchors 2023-03-29 20:51:34 +02:00
Ell
bef670c09b cleaned up Friends of MLEM section 2023-03-28 22:55:09 +02:00
Ell
a4f00c9eed added Touchy Tickets to Made with MLEM section 2023-03-07 11:08:52 +01:00
Ell
24a4c23be5 additional documentation article improvements 2023-03-05 20:31:09 +01:00
Ell
12af816a90 improved various documentation articles 2023-03-05 18:42:21 +01:00
Ell
5086101bad Allow setting ExternalGestureHandling through the InputHandler constructor 2023-03-04 23:41:25 +01:00
Ell
8a4dc11072 Marked GetDownTime, GetUpTime and GetTimeSincePress in Keybind and Combination as obsolete 2023-03-04 23:05:17 +01:00
Ell
5c8b535fe4 fixed issues in the android demo's release mode 2023-03-04 12:10:16 +01:00
Ell
7a54e4aa2d added a default constructor to Combination 2023-03-03 14:24:35 +01:00
Ell
a84fd764c5 some GenericInput and Keybind improvements 2023-03-03 14:02:05 +01:00
Ell
3968f7dfae Added a simple outline formatting code 2023-02-23 15:18:07 +01:00
Ell
d8314877c8 improved text formatting demo 2023-02-23 14:33:03 +01:00
Ell
b69a2c4755 Allow changing the default values used by default TextFormatter codes 2023-02-22 18:48:12 +01:00
Ell
fd642637a1 fixed changelog formatting error 2023-02-21 17:21:19 +01:00
Ell
c4836eedd6 Increased some recursion limits, and added useful Element ToString 2023-02-20 11:01:15 +01:00
Ell
dbd7f66c89 slightly improved A* documentation 2023-02-18 12:32:32 +01:00
Ell
695fba59a4 updated changelog 2023-02-17 20:38:06 +01:00
Ell
4029adb4bf Fixed TextInput drawing characters with the wrong width if a masking character is used 2023-02-17 18:33:56 +01:00
Ell
a47d3f50cb Fixed TextInputs behaving incorrectly when switching between multiline and single-line modes 2023-02-17 13:16:49 +01:00
Ell
f6beaff43a Fixed a multiline text field's cursor not returning to the default position when the last character is removed 2023-02-13 14:37:20 +01:00
Ell
aef6f7bd58 Fixed control characters being included in TextInput and TextField 2023-02-13 14:27:39 +01:00
Ell
7b2306f58f fixed docfx cake tool 2023-02-09 23:19:42 +01:00
Ell
1d26cf017d updated docfx and improved website style slightly 2023-02-09 22:52:31 +01:00
Ell
6be4143331 Fixed images not updating their hidden state properly when the displayed texture changes 2023-02-03 11:31:18 +01:00
Ell
7f7a9c6415 Revert "Improved RuntimeTexturePacker speed when using many distinct texture sizes"
This reverts commit ca9c8e6cfd.
2023-01-30 22:51:15 +01:00
Ell
ca9c8e6cfd Improved RuntimeTexturePacker speed when using many distinct texture sizes 2023-01-30 22:38:15 +01:00
Ell
d450ec3082 make sure the NuGet link only lists MLEM packages 2023-01-25 19:26:39 +01:00
Ell
4cfa105f75 bump upcoming version 2023-01-19 21:03:13 +01:00
130 changed files with 1556 additions and 1062 deletions

View file

@ -2,6 +2,7 @@
MLEM tries to adhere to [semantic versioning](https://semver.org/). Potentially 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.2.0](#620)
- [6.1.0](#610) - [6.1.0](#610)
- [6.0.0](#600) - [6.0.0](#600)
- [5.3.0](#530) - [5.3.0](#530)
@ -9,6 +10,62 @@ Jump to version:
- [5.1.0](#510) - [5.1.0](#510)
- [5.0.0](#500) - [5.0.0](#500)
## 6.2.0
### MLEM
Additions
- Added a simple outline formatting code
- Added the ability to add inverse modifiers to a Keybind
- Added GenericInput collections AllKeys, AllMouseButtons, AllButtons and AllInputs
- Added TextFormatter.StripAllFormatting
Improvements
- Increased TextFormatter macro recursion limit to 64
- Allow changing the default values used by default TextFormatter codes
- Allow setting ExternalGestureHandling through the InputHandler constructor
- Allow specifying start and end indices when drawing a TokenizedString
- Include control characters in TextInput FileNames and PathNames rules
Fixes
- Fixed control characters being included in TextInput
- Fixed TextInputs behaving incorrectly when switching between multiline and single-line modes
- Fixed TextInput drawing characters with the wrong width if a masking character is used
- Fixed a multiline TextInput's cursor not returning to the default position when the last character is removed
- Fixed GetRandomWeightedEntry distribution not being equal for equal weights
Removals
- Marked GetDownTime, GetUpTime and GetTimeSincePress in Keybind and Combination as obsolete
### MLEM.Ui
Additions
- Added AutoInlineCenter and AutoInlineBottom anchors
- Added UiAnimation system
- Added AddCustomStyle and ApplyCustomStyle to UiStyle to allow for easy custom styling of elements
- Added UiControls.PressElement
- Added TextField.EnterReceiver
- Added a copy constructor to UiStyle
Improvements
- Increased Element area calculation recursion limit to 64
- Improved the SquishingGroup algorithm by prioritizing each element's final size
- Allow specifying start and end indices when drawing a Paragraph
- Allow elements with larger children to influence a panel's scrollable area
- Remove all elements from a UiSystem when it is disposed
- Made elements' ui styles be inherited by their children
Fixes
- Fixed images not updating their hidden state properly when the displayed texture changes
- Fixed AutoInline elements overflowing into their parent if it's taller
- Fixed Paragraph and Checkbox not reacting to SquishingGroup sizing properly
- Fixed TextInput and Slider still reacting to input when they are selected, but not part of the active root
- Fixed dropdown menu panels not updating their width when the dropdown's width changes
- Fixed removing and later adding children to a scrolling panel showing the scroll bar erroneously
### MLEM.Data
Improvements
- Improved RuntimeTexturePacker performance for differently sized textures
- Allow querying the amount of RuntimeTexturePacker regions
## 6.1.0 ## 6.1.0
### MLEM ### MLEM
@ -114,7 +171,7 @@ Fixes
Removals Removals
- Marked DynamicEnum as obsolete due to its reimplementation in [DynamicEnums](https://www.nuget.org/packages/DynamicEnums) - Marked DynamicEnum as obsolete due to its reimplementation in [DynamicEnums](https://www.nuget.org/packages/DynamicEnums)
## MLEM.Extended ### MLEM.Extended
Additions Additions
- Added Range extension methods GetPercentage and FromPercentage - Added Range extension methods GetPercentage and FromPercentage
@ -123,7 +180,7 @@ Improvements
- Added trimming and AOT annotations and made MLEM.Extended trimmable - Added trimming and AOT annotations and made MLEM.Extended trimmable
- **Made GenericBitmapFont and GenericStashFont support UTF-32 characters like emoji** - **Made GenericBitmapFont and GenericStashFont support UTF-32 characters like emoji**
## MLEM.Startup ### MLEM.Startup
Improvements Improvements
- Multi-target net452, making MLEM compatible with MonoGame for consoles - Multi-target net452, making MLEM compatible with MonoGame for consoles
- Added trimming and AOT annotations and made MLEM.Startup trimmable - Added trimming and AOT annotations and made MLEM.Startup trimmable

View file

@ -3,31 +3,31 @@
"isRoot": true, "isRoot": true,
"tools": { "tools": {
"dotnet-mgcb": { "dotnet-mgcb": {
"version": "3.8.1.263", "version": "3.8.1.303",
"commands": [ "commands": [
"mgcb" "mgcb"
] ]
}, },
"dotnet-mgcb-editor": { "dotnet-mgcb-editor": {
"version": "3.8.1.263", "version": "3.8.1.303",
"commands": [ "commands": [
"mgcb-editor" "mgcb-editor"
] ]
}, },
"dotnet-mgcb-editor-linux": { "dotnet-mgcb-editor-linux": {
"version": "3.8.1.263", "version": "3.8.1.303",
"commands": [ "commands": [
"mgcb-editor-linux" "mgcb-editor-linux"
] ]
}, },
"dotnet-mgcb-editor-windows": { "dotnet-mgcb-editor-windows": {
"version": "3.8.1.263", "version": "3.8.1.303",
"commands": [ "commands": [
"mgcb-editor-windows" "mgcb-editor-windows"
] ]
}, },
"dotnet-mgcb-editor-mac": { "dotnet-mgcb-editor-mac": {
"version": "3.8.1.263", "version": "3.8.1.303",
"commands": [ "commands": [
"mgcb-editor-mac" "mgcb-editor-mac"
] ]

View file

@ -1,3 +1,4 @@
using Android.App;
using Android.Content; using Android.Content;
using Android.Content.PM; using Android.Content.PM;
using Android.OS; using Android.OS;
@ -47,10 +48,10 @@ public class Activity1 : AndroidGameActivity {
base.OnWindowFocusChanged(hasFocus); base.OnWindowFocusChanged(hasFocus);
// hide the status bar // hide the status bar
if (hasFocus) { if (hasFocus) {
#pragma warning disable CS0618 #pragma warning disable CA1422
// TODO this is deprecated, find out how to replace it // TODO this is deprecated, find out how to replace it
this.Window.DecorView.SystemUiVisibility = (StatusBarVisibility) (SystemUiFlags.ImmersiveSticky | SystemUiFlags.LayoutStable | SystemUiFlags.LayoutHideNavigation | SystemUiFlags.LayoutFullscreen | SystemUiFlags.HideNavigation | SystemUiFlags.Fullscreen); this.Window.DecorView.SystemUiVisibility = (StatusBarVisibility) (SystemUiFlags.ImmersiveSticky | SystemUiFlags.LayoutStable | SystemUiFlags.LayoutHideNavigation | SystemUiFlags.LayoutFullscreen | SystemUiFlags.HideNavigation | SystemUiFlags.Fullscreen);
#pragma warning restore CS0618 #pragma warning restore CA1422
} }
} }

View file

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="de.ellpeck.mlem.demos.android" android:versionCode="1" android:versionName="1.0"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-sdk android:minSdkVersion="23" android:targetSdkVersion="31" /> <application android:label="MLEM Android Demos"/>
<uses-feature android:glEsVersion="0x00020000" android:required="true" /> <uses-permission android:name="android.permission.INTERNET" />
<application android:label="MLEM Android Demos" />
</manifest> </manifest>

View file

@ -1,12 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0-android</TargetFramework> <TargetFramework>net7.0-android</TargetFramework>
<SupportedOSPlatformVersion>31</SupportedOSPlatformVersion> <SupportedOSPlatformVersion>31</SupportedOSPlatformVersion>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<ApplicationId>de.ellpeck.mlem.demos.android</ApplicationId> <ApplicationId>de.ellpeck.mlem.demos.android</ApplicationId>
<ApplicationVersion>1</ApplicationVersion> <ApplicationVersion>100</ApplicationVersion>
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion> <ApplicationDisplayVersion>1.0.0</ApplicationDisplayVersion>
<ImplicitUsings>true</ImplicitUsings> <RunAOTCompilation>false</RunAOTCompilation>
<PublishTrimmed>false</PublishTrimmed>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
</PropertyGroup> </PropertyGroup>

View file

@ -3,31 +3,31 @@
"isRoot": true, "isRoot": true,
"tools": { "tools": {
"dotnet-mgcb": { "dotnet-mgcb": {
"version": "3.8.1.263", "version": "3.8.1.303",
"commands": [ "commands": [
"mgcb" "mgcb"
] ]
}, },
"dotnet-mgcb-editor": { "dotnet-mgcb-editor": {
"version": "3.8.1.263", "version": "3.8.1.303",
"commands": [ "commands": [
"mgcb-editor" "mgcb-editor"
] ]
}, },
"dotnet-mgcb-editor-linux": { "dotnet-mgcb-editor-linux": {
"version": "3.8.1.263", "version": "3.8.1.303",
"commands": [ "commands": [
"mgcb-editor-linux" "mgcb-editor-linux"
] ]
}, },
"dotnet-mgcb-editor-windows": { "dotnet-mgcb-editor-windows": {
"version": "3.8.1.263", "version": "3.8.1.303",
"commands": [ "commands": [
"mgcb-editor-windows" "mgcb-editor-windows"
] ]
}, },
"dotnet-mgcb-editor-mac": { "dotnet-mgcb-editor-mac": {
"version": "3.8.1.263", "version": "3.8.1.303",
"commands": [ "commands": [
"mgcb-editor-mac" "mgcb-editor-mac"
] ]

View file

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net7.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>
@ -29,7 +29,7 @@
<Content Include="..\Demos\Content\*\**" /> <Content Include="..\Demos\Content\*\**" />
<EmbeddedResource Include="Icon.ico" /> <EmbeddedResource Include="Icon.ico" />
<EmbeddedResource Include="Icon.bmp" /> <EmbeddedResource Include="Icon.bmp" />
<Content Include="FnaNative/**"> <Content Include="../FnaNative/**">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Link>%(Filename)%(Extension)</Link> <Link>%(Filename)%(Extension)</Link>
</Content> </Content>

View file

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net7.0</TargetFramework>
<ApplicationIcon>Icon.ico</ApplicationIcon> <ApplicationIcon>Icon.ico</ApplicationIcon>
<AssemblyName>MLEM Desktop Demos</AssemblyName> <AssemblyName>MLEM Desktop Demos</AssemblyName>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -41,6 +41,27 @@
/processorParam:TextureFormat=Compressed /processorParam:TextureFormat=Compressed
/build:Fonts/TestFontItalic.spritefont /build:Fonts/TestFontItalic.spritefont
#begin Fonts/Roboto.spritefont
/importer:FontDescriptionImporter
/processor:FontDescriptionProcessor
/processorParam:PremultiplyAlpha=True
/processorParam:TextureFormat=Compressed
/build:Fonts/Roboto.spritefont
#begin Fonts/RobotoBold.spritefont
/importer:FontDescriptionImporter
/processor:FontDescriptionProcessor
/processorParam:PremultiplyAlpha=True
/processorParam:TextureFormat=Compressed
/build:Fonts/RobotoBold.spritefont
#begin Fonts/RobotoItalic.spritefont
/importer:FontDescriptionImporter
/processor:FontDescriptionProcessor
/processorParam:PremultiplyAlpha=True
/processorParam:TextureFormat=Compressed
/build:Fonts/RobotoItalic.spritefont
#begin Markdown.md #begin Markdown.md
/copy:Markdown.md /copy:Markdown.md

View file

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
This file contains an xml description of a font, and will be read by the XNA
Framework Content Pipeline. Follow the comments to customize the appearance
of the font in your game, and to change the characters which are available to draw
with.
-->
<XnaContent xmlns:Graphics="Microsoft.Xna.Framework.Content.Pipeline.Graphics">
<Asset Type="Graphics:FontDescription">
<!--
Modify this string to change the font that will be imported.
-->
<FontName>RobotoRegular.ttf</FontName>
<!--
Size is a float value, measured in points. Modify this value to change
the size of the font.
-->
<Size>32</Size>
<!--
Spacing is a float value, measured in pixels. Modify this value to change
the amount of spacing in between characters.
-->
<Spacing>0</Spacing>
<!--
UseKerning controls the layout of the font. If this value is true, kerning information
will be used when placing characters.
-->
<UseKerning>true</UseKerning>
<!--
Style controls the style of the font. Valid entries are "Regular", "Bold", "Italic",
and "Bold, Italic", and are case sensitive.
-->
<Style>Regular</Style>
<!--
If you uncomment this line, the default character will be substituted if you draw
or measure text that contains characters which were not included in the font.
-->
<DefaultCharacter>*</DefaultCharacter>
<!--
CharacterRegions control what letters are available in the font. Every
character from Start to End will be built and made available for drawing. The
default range is from 32, (ASCII space), to 126, ('~'), covering the basic Latin
character set. The characters are ordered according to the Unicode standard.
See the documentation for more information.
-->
<CharacterRegions>
<CharacterRegion>
<Start>&#32;</Start>
<End>&#591;</End>
</CharacterRegion>
</CharacterRegions>
</Asset>
</XnaContent>

View file

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
This file contains an xml description of a font, and will be read by the XNA
Framework Content Pipeline. Follow the comments to customize the appearance
of the font in your game, and to change the characters which are available to draw
with.
-->
<XnaContent xmlns:Graphics="Microsoft.Xna.Framework.Content.Pipeline.Graphics">
<Asset Type="Graphics:FontDescription">
<!--
Modify this string to change the font that will be imported.
-->
<FontName>RobotoBold.ttf</FontName>
<!--
Size is a float value, measured in points. Modify this value to change
the size of the font.
-->
<Size>32</Size>
<!--
Spacing is a float value, measured in pixels. Modify this value to change
the amount of spacing in between characters.
-->
<Spacing>0</Spacing>
<!--
UseKerning controls the layout of the font. If this value is true, kerning information
will be used when placing characters.
-->
<UseKerning>true</UseKerning>
<!--
Style controls the style of the font. Valid entries are "Regular", "Bold", "Italic",
and "Bold, Italic", and are case sensitive.
-->
<Style>Regular</Style>
<!--
If you uncomment this line, the default character will be substituted if you draw
or measure text that contains characters which were not included in the font.
-->
<DefaultCharacter>*</DefaultCharacter>
<!--
CharacterRegions control what letters are available in the font. Every
character from Start to End will be built and made available for drawing. The
default range is from 32, (ASCII space), to 126, ('~'), covering the basic Latin
character set. The characters are ordered according to the Unicode standard.
See the documentation for more information.
-->
<CharacterRegions>
<CharacterRegion>
<Start>&#32;</Start>
<End>&#591;</End>
</CharacterRegion>
</CharacterRegions>
</Asset>
</XnaContent>

Binary file not shown.

View file

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
This file contains an xml description of a font, and will be read by the XNA
Framework Content Pipeline. Follow the comments to customize the appearance
of the font in your game, and to change the characters which are available to draw
with.
-->
<XnaContent xmlns:Graphics="Microsoft.Xna.Framework.Content.Pipeline.Graphics">
<Asset Type="Graphics:FontDescription">
<!--
Modify this string to change the font that will be imported.
-->
<FontName>RobotoItalic.ttf</FontName>
<!--
Size is a float value, measured in points. Modify this value to change
the size of the font.
-->
<Size>32</Size>
<!--
Spacing is a float value, measured in pixels. Modify this value to change
the amount of spacing in between characters.
-->
<Spacing>0</Spacing>
<!--
UseKerning controls the layout of the font. If this value is true, kerning information
will be used when placing characters.
-->
<UseKerning>true</UseKerning>
<!--
Style controls the style of the font. Valid entries are "Regular", "Bold", "Italic",
and "Bold, Italic", and are case sensitive.
-->
<Style>Regular</Style>
<!--
If you uncomment this line, the default character will be substituted if you draw
or measure text that contains characters which were not included in the font.
-->
<DefaultCharacter>*</DefaultCharacter>
<!--
CharacterRegions control what letters are available in the font. Every
character from Start to End will be built and made available for drawing. The
default range is from 32, (ASCII space), to 126, ('~'), covering the basic Latin
character set. The characters are ordered according to the Unicode standard.
See the documentation for more information.
-->
<CharacterRegions>
<CharacterRegion>
<Start>&#32;</Start>
<End>&#591;</End>
</CharacterRegion>
</CharacterRegions>
</Asset>
</XnaContent>

Binary file not shown.

Binary file not shown.

View file

@ -15,18 +15,27 @@ namespace Demos {
private const string Text = private const string Text =
"MLEM's text formatting system allows for various <b>formatting codes</b> to be applied in the middle of a string. Here's a demonstration of some of them.\n\n" + "MLEM's text formatting system allows for various <b>formatting codes</b> to be applied in the middle of a string. Here's a demonstration of some of them.\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 write in <b>bold</i>, <i>italics</i>, <u>with an underline</u>, <st>strikethrough</st>, with a <s>drop shadow</s> whose <s #ff0000 4>color</s> and <s #000000 10>offset</s> you can modify in each application of the code, with an <o>outline</o> that you can also <o #ff0000 4>modify</o> <o #ff00ff 2>dynamically</o>, 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, and use super<sup>script</sup> or sub<sub>script</sub> formatting!\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 DefaultScale = 0.5F;
private const float Width = 0.9F; private const float WidthMultiplier = 0.9F;
private TextFormatter formatter; private TextFormatter formatter;
private TokenizedString tokenizedText; private TokenizedString tokenizedText;
private GenericFont font; private GenericFont font;
private bool drawBounds; private bool drawBounds;
private float Scale {
get {
// calculate our scale based on how much larger the window is, so that the text scales with the window
var viewport = new Rectangle(0, 0, this.Game.Window.ClientBounds.Width, this.Game.Window.ClientBounds.Height);
return TextFormattingDemo.DefaultScale * Math.Min(viewport.Width / 1280F, viewport.Height / 720F);
}
}
private int startIndex;
private int endIndex;
public TextFormattingDemo(MlemGame game) : base(game) {} public TextFormattingDemo(MlemGame game) : base(game) {}
@ -34,13 +43,16 @@ namespace Demos {
this.Game.Window.ClientSizeChanged += this.OnResize; this.Game.Window.ClientSizeChanged += this.OnResize;
// creating a new text formatter as well as a generic font to draw with // creating a new text formatter as well as a generic font to draw with
this.formatter = new TextFormatter(); this.formatter = new TextFormatter {
DefaultShadowOffset = new Vector2(4),
DefaultOutlineThickness = 4
};
// GenericFont and its subtypes are wrappers around various font classes, including SpriteFont, MonoGame.Extended's BitmapFont and FontStashSharp // GenericFont and its subtypes are wrappers around various font classes, including SpriteFont, MonoGame.Extended's BitmapFont and FontStashSharp
// supplying a bold and italic version of the font here allows for the bold and italic formatting codes to be used // supplying a bold and italic version of the font here allows for the bold and italic formatting codes to be used
this.font = new GenericSpriteFont( this.font = new GenericSpriteFont(
Demo.LoadContent<SpriteFont>("Fonts/TestFont"), Demo.LoadContent<SpriteFont>("Fonts/Roboto"),
Demo.LoadContent<SpriteFont>("Fonts/TestFontBold"), Demo.LoadContent<SpriteFont>("Fonts/RobotoBold"),
Demo.LoadContent<SpriteFont>("Fonts/TestFontItalic")); Demo.LoadContent<SpriteFont>("Fonts/RobotoItalic"));
// adding the image code used in the example to it // adding the image code used in the example to it
var testTexture = Demo.LoadContent<Texture2D>("Textures/Test"); var testTexture = Demo.LoadContent<Texture2D>("Textures/Test");
@ -49,7 +61,8 @@ namespace Demos {
// tokenizing our text and splitting it to fit the screen // tokenizing our text and splitting it to fit the screen
// we specify our text alignment here too, so that all data is cached correctly for display // we specify our text alignment here too, so that all data is cached correctly for display
this.tokenizedText = this.formatter.Tokenize(this.font, TextFormattingDemo.Text, TextAlignment.Center); this.tokenizedText = this.formatter.Tokenize(this.font, TextFormattingDemo.Text, TextAlignment.Center);
this.tokenizedText.Split(this.font, this.GraphicsDevice.Viewport.Width * TextFormattingDemo.Width, TextFormattingDemo.Scale, TextAlignment.Center); this.tokenizedText.Split(this.font, this.GraphicsDevice.Viewport.Width * TextFormattingDemo.WidthMultiplier, this.Scale, TextAlignment.Center);
this.endIndex = this.tokenizedText.String.Length;
} }
public override void DoDraw(GameTime time) { public override void DoDraw(GameTime time) {
@ -58,7 +71,7 @@ namespace Demos {
// 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.GetArea(Vector2.Zero, TextFormattingDemo.Scale).Size; var size = this.tokenizedText.GetArea(Vector2.Zero, this.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 // draw bounds, which can be toggled with B in this demo
@ -66,13 +79,13 @@ namespace Demos {
var blank = this.SpriteBatch.GetBlankTexture(); var blank = this.SpriteBatch.GetBlankTexture();
this.SpriteBatch.Draw(blank, new RectangleF(pos - new Vector2(size.X / 2, 0), size), Color.Red * 0.25F); 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 token in this.tokenizedText.Tokens) {
foreach (var area in token.GetArea(pos, TextFormattingDemo.Scale)) foreach (var area in token.GetArea(pos, this.Scale))
this.SpriteBatch.Draw(blank, area, Color.Black * 0.25F); this.SpriteBatch.Draw(blank, area, Color.Black * 0.25F);
} }
} }
// draw the text itself // draw the text itself (start and end indices are optional)
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, this.Scale, 0, this.startIndex, this.endIndex);
this.SpriteBatch.End(); this.SpriteBatch.End();
} }
@ -80,8 +93,18 @@ 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);
// change some demo showcase info based on keybinds
if (this.InputHandler.IsPressed(Keys.B)) if (this.InputHandler.IsPressed(Keys.B))
this.drawBounds = !this.drawBounds; this.drawBounds = !this.drawBounds;
if (this.startIndex > 0 && this.InputHandler.IsDown(Keys.Left))
this.startIndex--;
if (this.startIndex < this.tokenizedText.String.Length && this.InputHandler.IsDown(Keys.Right))
this.startIndex++;
if (this.endIndex > 0 && this.InputHandler.IsDown(Keys.Down))
this.endIndex--;
if (this.endIndex < this.tokenizedText.String.Length && this.InputHandler.IsDown(Keys.Up))
this.endIndex++;
} }
public override void Clear() { public override void Clear() {
@ -92,7 +115,7 @@ namespace Demos {
private void OnResize(object sender, EventArgs e) { private void OnResize(object sender, EventArgs e) {
// re-split our text if the window resizes, since it depends on the window size // re-split our text if the window resizes, since it depends on the window size
// this doesn't require re-tokenization of the text, since TokenizedString also stores the un-split version // this doesn't require re-tokenization of the text, since TokenizedString also stores the un-split version
this.tokenizedText.Split(this.font, this.GraphicsDevice.Viewport.Width * TextFormattingDemo.Width, TextFormattingDemo.Scale, TextAlignment.Center); this.tokenizedText.Split(this.font, this.GraphicsDevice.Viewport.Width * TextFormattingDemo.WidthMultiplier, this.Scale, TextAlignment.Center);
} }
} }

View file

@ -164,21 +164,13 @@ namespace Demos {
PositionOffset = new Vector2(0, 1) PositionOffset = new Vector2(0, 1)
}); });
// Another button that shows animations! // Another button that shows animations!
var fancyHoverTimer = 0D; this.root.AddChild(new Button(Anchor.AutoCenter, new Vector2(0.5F, 10), "Fancy Hover") {
var fancyButton = this.root.AddChild(new Button(Anchor.AutoCenter, new Vector2(0.5F, 10), "Fancy Hover") {
PositionOffset = new Vector2(0, 1), PositionOffset = new Vector2(0, 1),
OnUpdated = (e, time) => { MouseEnterAnimation = new UiAnimation(0.15, (a, e, p) => e.ScaleTransform(1 + Easings.OutSine(p) * 0.05F)),
if (e.IsMouseOver && fancyHoverTimer <= 0.5F) MouseExitAnimation = new UiAnimation(0.15, (a, e, p) => e.ScaleTransform(1 + Easings.OutSine.ReverseOutput()(p) * 0.05F)) {
return; Finished = (a, e) => e.Transform = Matrix.Identity
if (fancyHoverTimer > 0) {
fancyHoverTimer -= time.ElapsedGameTime.TotalSeconds * 3;
e.ScaleTransform(1 + (float) Math.Sin(fancyHoverTimer * MathHelper.Pi) * 0.05F);
} else {
e.Transform = Matrix.Identity;
}
} }
}); });
fancyButton.OnMouseEnter += e => fancyHoverTimer = 1;
this.root.AddChild(new Button(Anchor.AutoCenter, new Vector2(0.5F, 10), "Transform Ui", "This button causes the entire ui to be transformed (both in positioning, rotation and scale)") { this.root.AddChild(new Button(Anchor.AutoCenter, new Vector2(0.5F, 10), "Transform Ui", "This button causes the entire ui to be transformed (both in positioning, rotation and scale)") {
OnPressed = element => { OnPressed = element => {
if (element.Root.Transform == Matrix.Identity) { if (element.Root.Transform == Matrix.Identity) {
@ -242,7 +234,10 @@ namespace Demos {
this.root.AddChild(new VerticalSpace(3)); this.root.AddChild(new VerticalSpace(3));
this.root.AddChild(new Paragraph(Anchor.AutoLeft, 1, "MLEM.Ui also contains a simple Markdown parser, which can be useful for displaying things like changelogs in your game.")); this.root.AddChild(new Paragraph(Anchor.AutoLeft, 1, "MLEM.Ui also contains a simple Markdown parser, which can be useful for displaying things like changelogs in your game."));
this.root.AddChild(new VerticalSpace(3)); this.root.AddChild(new VerticalSpace(3));
var parser = new UiMarkdownParser {GraphicsDevice = this.GraphicsDevice}; var parser = new UiMarkdownParser {
GraphicsDevice = this.GraphicsDevice,
ImageExceptionHandler = (s, e) => Console.Error.WriteLine($"Couldn't load image {s}: {e}")
};
using (var reader = new StreamReader(TitleContainer.OpenStream("Content/Markdown.md"))) using (var reader = new StreamReader(TitleContainer.OpenStream("Content/Markdown.md")))
parser.ParseInto(reader.ReadToEnd(), this.root); parser.ParseInto(reader.ReadToEnd(), this.root);

View file

@ -23,6 +23,12 @@ var split = spriteFont.SplitString("This is a really long line of text [...]", w
spriteFont.DrawString(this.SpriteBatch, split, new Vector2(10, 10), Color.White); spriteFont.DrawString(this.SpriteBatch, split, new Vector2(10, 10), Color.White);
``` ```
Alternatively, the `SplitStringSeparate` method returns a collection of strings, where each entry represents a place where a split has been introduced. Using this method, you can differentiate between pre-existing newline characters and newly introduced ones.
```cs
var split = spriteFont.SplitStringSeparate("This is a line of text that contains\nnewline characters!", width: 10, scale: 1);
// returns something like ["This is a line of ", "text that contains\nnewline characters!"]
```
## Truncating ## Truncating
Using generic fonts, a long line of text can also be truncated to fit a certain width in pixels. The remaining text that doesn't fit will simply be chopped off of the end (or start) of the string. Using generic fonts, a long line of text can also be truncated to fit a certain width in pixels. The remaining text that doesn't fit will simply be chopped off of the end (or start) of the string.
```cs ```cs

View file

@ -4,10 +4,12 @@ The **MLEM** base package features an extended `InputHandler` class that allows
Rather than using an event-based structure, the MLEM input handler relies on the game's `Update` frames: To query input through the input handler, you have to query it every Update frame, and input information will only be available for a single update frame in most situations. Rather than using an event-based structure, the MLEM input handler relies on the game's `Update` frames: To query input through the input handler, you have to query it every Update frame, and input information will only be available for a single update frame in most situations.
The input handler makes use of the `GenericInput` struct, which is a MLEM wrapper around the three main types of input that MonoGame and FNA provide: `Keys`, `Buttons` and `MouseButton` (the latter of which is a MLEM abstraction of mouse buttons). Values of all of these types can be converted into `GenericInput` implicitly, and a `GenericInput` can be converted back implicitly as well, so you will rarely ever have to interact with the `GenericInput` type manually.
## Setting it up ## Setting it up
To set it up, all you have to do is create a new instance. The constructor optionally accepts parameters to enable or disable certain kinds of input. To set it up, all you have to do is create a new instance. The constructor optionally accepts parameters to enable or disable certain kinds of input.
```cs ```cs
this.InputHandler = new InputHandler(); this.InputHandler = new InputHandler(gameInstance);
``` ```
Additionally, you will have to call the input handler's `Update` method each update call of your game: Additionally, you will have to call the input handler's `Update` method each update call of your game:
```cs ```cs
@ -15,7 +17,7 @@ this.InputHandler.Update();
``` ```
## Querying pressed keys ## Querying pressed keys
A *pressed* key is a key that wasn't down the last update but is held down the current update. This behavior can be useful for things like ui buttons, where holding down the mouse button shouldn't constantly keep triggering the button. A *pressed* key is a key that wasn't down the last update but is held down the current update, which is essentially a positive-edge-triggered press. This behavior can be useful for things like ui buttons, where holding down the mouse button shouldn't constantly keep triggering the button.
You can query if any key, mouse button or gamepad button is pressed as follows: You can query if any key, mouse button or gamepad button is pressed as follows:
```cs ```cs
@ -28,20 +30,52 @@ var gamepad = this.InputHandler.IsPressed(Buttons.A);
var gamepad2 = this.InputHandler.IsPressed(Buttons.A, 2); var gamepad2 = this.InputHandler.IsPressed(Buttons.A, 2);
``` ```
Using the `InvertPressBehavior` flag, you can invert this behavior: If it is set to `true`, a key is considered pressed if it was down the last update, but is up in the current update. This is essentially a negative-edge-triggered press.
### Repeat events ### Repeat events
Keyboard and gamepad repeat events can be enabled or disabled through the `HandleKeyboardRepeats` and `HandleGamepadRepeats` properties in the input handler. Additionally, you can configure the time that it takes until the first repeat is triggered through the `KeyRepeatDelay` property, and you can configure the delay between repeat events through the `KeyRepeatRate` property. Keyboard and gamepad repeat events can be enabled or disabled through the `HandleKeyboardRepeats` and `HandleGamepadRepeats` properties in the input handler. Additionally, you can configure the time that it takes until the first repeat is triggered through the `KeyRepeatDelay` property, and you can configure the delay between repeat events through the `KeyRepeatRate` property.
When enabled, repeat events for *pressing* are automatically triggered. This means that calling `IsPressed` every update call would return `true` for a control that is being held down every `KeyRepeatRate` seconds after `KeyRepeatDelay` seconds have passed once. When enabled, repeat events for *pressing* are automatically triggered. This means that calling `IsPressed` every update call would return `true` for a control that is being held down every `KeyRepeatRate` seconds after `KeyRepeatDelay` seconds have passed once.
## Consuming inputs
Due to the fact that the input handler is query-based (rather than event-based), multiple pieces of code might query the same input each update, causing a single press to be misconstrued as multiple distinct inputs.
The input handler provides the methods `IsPressConsumed`, `IsPressedAvailable`, and `TryConsumePressed`. Calling `TryConsumePressed` on an input means that subsequent calls to `IsPressConsumed` and `IsPressedAvailable` will not return `true` until the next update frame:
```cs
// is this update frame's Up press consumed yet?
var consumed = this.InputHandler.IsPressConsumed(Keys.Up);
// is the Up key pressed, and its press not consumed yet?
var available = this.InputHandler.IsPressedAvailable(Keys.Up);
// check whether the Up key is pressed and its press is available, and consume it
if (this.InputHandler.TryConsumePressed(Keys.Up)) {
// the press has been consumed by us, now do something with the press!
}
```
## Input metrics
The input handler tracks additional data related to keyboard, gamepad, and mouse inputs, such as the amount of times that they have been down for. These metrics can be useful for implementing short-press and long-press behavior.
```cs
// how long has the A key been up (or down) for the last time it was up (or down)?
var upTime = this.InputHandler.GetUpTime(Keys.A);
var downTime = this.InputHandler.GetDownTime(Keys.A);
// how long has it been since the A key was pressed?
var timeSincePress = this.InputHandler.TryGetTimeSincePress(Keys.A);
```
## Gesture handling ## Gesture handling
MonoGame's default touch handling can be a bit wonky to deal with, so the input handler also provides a much better user experience for touch gesture input. MonoGame's default gesture handling (which is inherited from XNA) can be a little difficult to deal with. This is mainly due to the fact that gestures stay in the queue until they are queried (so they might be very old), and the fact that they can't be queried without being permanently removed from the queue.
Because of this, MLEM's input handler also provides a much more streamlined user experience for touch gesture input.
To enable touch input, the gestures you want to use first have to be enabled: To enable touch input, the gestures you want to use first have to be enabled:
```cs ```cs
InputHandler.EnableGestures(GestureType.Tap); InputHandler.EnableGestures(GestureType.Tap);
``` ```
When enabled, a `GestureSample` will be available for the requested gesture type *the frame it is finished*. It can be accessed like so: When enabled, a `GestureSample` will be available for the requested gesture type *the update frame it is finished*. It can be accessed like so:
```cs ```cs
if (this.InputHandler.GetGesture(GestureType.Tap, out var sample)) { if (this.InputHandler.GetGesture(GestureType.Tap, out var sample)) {
// The gesture happened this frame // The gesture happened this frame
@ -52,6 +86,21 @@ if (this.InputHandler.GetGesture(GestureType.Tap, out var sample)) {
``` ```
### External gesture handling ### 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 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 being queried once before it is removed from the queue, which causes any additional queries for that gesture to fail.
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. The input handler's gesture handling does not have this problem, since gestures are kept around for an entire update frame no matter how many times they are queried, and gestures can be queried from multiple sources based on the expected gesture type. Because of this, it's generally recommended that you use the input handler's gesture system instead of the default one.
However, if you want to continue using your own gesture handling, but still allow the `InputHandler` to have access to gestures (for [MLEM.Ui](ui.md), for example), you can set `ExternalGestureHandling` 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. As an example, you could modify your game's existing gesture handling like this:
```cs
while (TouchPanel.IsGestureAvailable) {
var gesture = TouchPanel.ReadGesture();
// your game's existing gesture handling ...
bool gestureConsumed = this.HandleGestureSomeWay(gesture);
if (gestureConsumed)
continue;
// pass the gesture onto the input handler if we didn't make use of it
this.InputHandler.AddExternalGesture(gesture);
}
```

View file

@ -57,3 +57,17 @@ namespace Test {
} }
``` ```
As `RawContentManager` automatically collects all raw content readers in the loaded assemblies, you don't have to register your custom reader anywhere. As `RawContentManager` automatically collects all raw content readers in the loaded assemblies, you don't have to register your custom reader anywhere.
## Environments without reflection or with trimming
By default, the `RawContentManager` finds all types that extend `RawContentReader` in all loaded assemblies, so they don't have to be added manually. This won't work in environments like NativeAOT, where reflection isn't as readily available, or in assemblies that get trimmed.
If you're in an environment with this restriction, you can manually collect all the content readers that you plan on using and call the constructor that accepts a list of content readers instead:
```csharp
protected override void LoadContent() {
var neededReaders = new List<RawContentReader> {
new Texture2DReader(), new JsonReader() // ...
};
this.rawContent = new RawContentManager(this.Services, neededReaders);
this.Components.Add(this.rawContent);
}
```

View file

@ -2,24 +2,28 @@
The **MLEM** package contains a simple text formatting system that supports coloring, bold and italic font modifiers, in-text icons and text animations. The **MLEM** package contains a simple text formatting system that supports coloring, bold and italic font modifiers, in-text icons and text animations.
Text formatting makes use of [generic fonts](font_extensions.md). Text formatting makes use of [generic fonts](font_extensions.md), and [MLEM.Ui](ui.md)'s `Paragraph` supports text formatting out of the box, but using it for your own text rendering is very simple.
It should also be noted that [MLEM.Ui](ui.md)'s `Paragraph` supports text formatting out of the box. [The demo](https://github.com/Ellpeck/MLEM/blob/main/Demos/TextFormattingDemo.cs) features plenty of examples of the formatting codes that are available by default, as well as examples of the ability to add custom codes and interact with formatted text.
## Formatting codes ## Formatting codes
To format your text, you can insert *formatting codes* into it. Almost all of these codes are single letters surrounded by `<>`, and some formatting codes can accept additional parameters after their letter representation. To format your text, you can insert *formatting codes* into it. Almost all of these codes are single letters surrounded by `<>`, and some formatting codes can accept additional parameters after their letter representation.
By default, the following formatting options are available: By default, the following formatting options are available:
- Colors using `<c ColorName>`. All default MonoGame colors are supported, for example `<c CornflowerBlue>`. Reset using `</c>`. - **Colors** using `<c ColorName>`. All default MonoGame colors are supported, for example `<c CornflowerBlue>`. Reset using `</c>`.
- Bold and italic text using `<b>` and `<i>`, respectively. Reset using `</b>` and `</i>`. - **Bold** and *italic* text using `<b>` and `<i>`, respectively. Reset using `</b>` and `</i>`.
- Drop shadows using `<s>`. Optional parameters for the shadow's color and positional offset are accepted: `<s #AARRGGBB 2.5>`. Reset using `</s>`. - **Drop shadows** using `<s>`. Optional parameters for the shadow's color and positional offset are accepted: `<s #AARRGGBB 2.5>`. Reset using `</s>`.
- Underlined and strikethrough text using `<u>` and `<st>`, respectively. Reset using `</u>` and `</st>`. - **Underlined** and **strikethrough** text using `<u>` and `<st>`, respectively. Reset using `</u>` and `</st>`.
- A wobbly sine wave animation using `<a wobbly>`. Optional parameters for the wobble's intensity and height are accepted: `<a wobbly 10 0.25>`. Reset using `</a>`. - **Subscript** and **superscript** text using `<sub>` and `<sup>`, respectively. Reset using `</sub>` and `</sup>`.
- **Text outlines** using `<o>`. Optional parameters for the outlines' color and thickness are accepted as well: `<o #ff0000 4>`. Reset using `</o>`.
- A wobbly sine wave **animation** using `<a wobbly>`. Optional parameters for the wobble's intensity and height are accepted: `<a wobbly 10 0.25>`. Reset using `</a>`.
When using [MLEM.Ui](ui.md)'s `Paragraph`, these additional formatting options are available by default: When using [MLEM.Ui](ui.md)'s `Paragraph`, these additional formatting options are available by default:
- Hoverable and clickable links using `<l Url>`. Note that this code does not automatically change the color of the text. Reset using `</l>`. - Hoverable and clickable links using `<l Url>`. Reset using `</l>`.
- Inline font changes using `<f FontName>`, with custom fonts gathered from `UiStyle.AdditionalFonts`. Reset using `</f>`. - Inline font changes using `<f FontName>`, with custom fonts gathered from `UiStyle.AdditionalFonts`. Reset using `</f>`.
If you only want to use your own formatting codes in your text formatter, the constructor allows disabling some or all of the default ones.
## Getting your text ready ## Getting your text ready
To get your text ready for rendering with formatting codes, it has to be tokenized. For that, you need to create a new text formatter first. Additionally, you need to have a [generic font](font_extensions.md) ready: To get your text ready for rendering with formatting codes, it has to be tokenized. For that, you need to create a new text formatter first. Additionally, you need to have a [generic font](font_extensions.md) ready:
```cs ```cs
@ -34,15 +38,29 @@ Additionally, if you want your tokenized string to be split based on a certain m
```cs ```cs
tokenizedString.Split(font, maxWidth, scale); tokenizedString.Split(font, maxWidth, scale);
``` ```
## Drawing the formatted text ## Drawing the formatted text
To draw your tokenized text, all you have to do is call its `Draw` method like so: To draw your tokenized text, all you have to do is call its `Draw` method like so:
```cs ```cs
tokenizedString.Draw(gameTime, spriteBatch, position, font, color, scale, depth); tokenizedString.Draw(gameTime, spriteBatch, position, font, color, scale, depth);
``` ```
Note that, if your tokenized text contains any animations, you have to updated the tokenized string every `Update` call like so: Note that, if your tokenized text contains any animations, you have to update the tokenized string every `Update` call like so:
```cs ```cs
tokenizedString.Update(gameTime); tokenizedString.Update(gameTime);
``` ```
## Interacting with formatted text
The `TokenizedString` class also features several methods for querying and interacting with the drawing of formatted text:
```cs
// the token that is under queryPosition if the string is drawn at position
var tokenUnderPos = tokenizedString.GetTokenUnderPos(position, queryPosition, scale);
foreach (var token in tokenizedString.Tokens) {
// the area that the given token takes up
var area = token.GetArea(position, scale);
}
```
## Adding custom codes ## Adding custom codes
Adding custom formatting codes is easy! There are two things that a custom formatting code requires: Adding custom formatting codes is easy! There are two things that a custom formatting code requires:
- A class that extends `Code` that does what your formatting code should do (we'll use `MyCustomCode` in this case) - A class that extends `Code` that does what your formatting code should do (we'll use `MyCustomCode` in this case)
@ -61,10 +79,11 @@ formatter.AddImage("ImageName", new TextureRegion(texture, 0, 0, 8, 8));
After doing so, the image can be displayed using the code `<i ImageName>`. After doing so, the image can be displayed using the code `<i ImageName>`.
## Macros ## Macros
The text formatting system additionally supports macros: Regular expressions that cause the matched text to expand into a different string. Macros are resolved recursively, meaning that you can have macros that resolve into other macros, and so on. The text formatting system additionally supports macros: Regular expressions that cause the matched text to expand into a different string. Macros are resolved recursively (up to 64 times), meaning that you can have macros that resolve into other macros as well.
By default, the following macros are available: By default, the following macros are available:
- `~` expands into a non-breaking space, much like in LaTeX. - `~` expands into a non-breaking space, much like in LaTeX.
- `<n>` expands into a newline character, if you like visual consistency with the other codes.
Adding custom macros is very similar to adding custom formatting codes: Adding custom macros is very similar to adding custom formatting codes:
```cs ```cs

View file

@ -32,7 +32,7 @@ This means that, to check if the tile at tile coordinate `6, 10` contains any co
```cs ```cs
var tiles = collisions.GetCollidingTiles(new RectangleF(6, 10, 1, 1)); var tiles = collisions.GetCollidingTiles(new RectangleF(6, 10, 1, 1));
``` ```
If the tile at that location is `16x16` pixels big and it has a single collision box at pixels `4, 4` that is `8x8` pixels big, then the following code prints out its percentaged coordinates: `X: 0.25, Y: 0.25, Width: 0.5, Height: 0.5`. If the tile at that location is `16x16` pixels big, and it has a single collision box at pixels `4, 4` that is `8x8` pixels big, then the following code prints out its percentaged coordinates: `X: 0.25, Y: 0.25, Width: 0.5, Height: 0.5`.
```cs ```cs
foreach (var tile in tiles) foreach (var tile in tiles)
Console.WriteLine(tile.Collisions[0]); Console.WriteLine(tile.Collisions[0]);

View file

@ -1,16 +1,25 @@
- name: MLEM.Ui - name: MLEM
href: ui.md
- name: Font Extensions - name: Font Extensions
href: font_extensions.md href: font_extensions.md
- name: Text Formatting - name: Text Formatting
href: text_formatting.md href: text_formatting.md
- name: Input Handler - name: Input Handler
href: input.md href: input.md
- name: Raw Content Manager
href: raw_content.md
- name: Sprite Animations - name: Sprite Animations
href: sprite_animations.md href: sprite_animations.md
- name: MLEM.Ui
- name: MLEM.Ui
href: ui.md
- name: MLEM.Extended
- name: Tiled Extensions - name: Tiled Extensions
href: tiled_extensions.md href: tiled_extensions.md
- name: MLEM.Data
- name: Raw Content Manager
href: raw_content.md
- name: MLEM.Startup
- name: MLEM.Startup - name: MLEM.Startup
href: startup.md href: startup.md

View file

@ -53,7 +53,7 @@ MlemPlatform.Current = new MlemPlatform.None();
``` ```
Initializing the platform in this way also allows for links in paragraphs to be clickable, causing a browser or explorer window to be opened on desktop or mobile devices. Initializing the platform in this way also allows for links in paragraphs to be clickable, causing a browser or explorer window to be opened on desktop or mobile devices.
For more info on MLEM's platform-related code, you can also check out MlemPlatform's [documentation](https://mlem.ellpeck.de/api/MLEM.Misc.MlemPlatform). For more info on MLEM's platform-related code, you can also check out MlemPlatform's [documentation](xref:MLEM.Misc.MlemPlatform).
## Setting the style ## Setting the style
By default, MLEM.Ui's controls look pretty bland, since it doesn't ship with any fonts or textures for any of its controls. To change the style of your ui, simply expand your `new UntexturedStyle(this.SpriteBatch)` call to include fonts and textures of your choosing, for example: By default, MLEM.Ui's controls look pretty bland, since it doesn't ship with any fonts or textures for any of its controls. To change the style of your ui, simply expand your `new UntexturedStyle(this.SpriteBatch)` call to include fonts and textures of your choosing, for example:
@ -92,5 +92,7 @@ this.UiSystem.Add("InfoBox", box);
### About sizing ### About sizing
Note that, when setting the width and height of any element, there are some things to note: Note that, when setting the width and height of any element, there are some things to note:
- Each element has a `SetWidthBasedOnChildren` and a `SetHeightBasedOnChildren` property, which allow them to change their size automatically based on their content - Each element has a `SetWidthBasedOnChildren` and a `SetHeightBasedOnChildren` property, which allow them to change their size automatically based on their content
- When specifying a width or height *lower than or equal to 1*, it is seen as a percentage based on the parent's size instead. For example, a paragraph with a width of `0.5F` inside of a panel width a width of `200` will be `100` units wide. - When specifying a width or height *lower than or equal to 1*, it is seen as a percentage based on the parent's size instead. For example, a paragraph with a width of `0.5F` inside a panel width a width of `200` will be `100` units wide.
- When specifying a width *lower than 0*, it is seen as a percentage based on the element's height, and vice versa. For example, a panel with a width of `200` and a height of `-2` will be `400` units tall. - When specifying a width *lower than 0*, it is seen as a percentage based on the element's height, and vice versa. For example, a panel with a width of `200` and a height of `-2` will be `400` units tall.
A lot of other ways to modify the size of an object are available as well, including `TreatSizeAsMaximum`, `TreatSizeAsMinimum`, `PreventParentSpill` and more. For more information, the `Element` [documentation](xref:MLEM.Ui.Elements.Element) contains descriptions of all fields, properties, and methods.

View file

@ -1,14 +1,23 @@
{ {
"metadata": [{ "metadata": [
"src": [{ {
"src": "../", "src": [
"files": ["**/MLEM**.csproj"], {
"exclude": ["**.FNA.**"] "src": "../",
}], "files": [
"dest": "api" "**/MLEM**.csproj"
}], ],
"exclude": [
"**.FNA.**"
]
}
],
"dest": "api"
}
],
"build": { "build": {
"content": [{ "content": [
{
"files": [ "files": [
"articles/**.md", "articles/**.md",
"articles/**/toc.yml", "articles/**/toc.yml",
@ -24,31 +33,30 @@
"src": ".." "src": ".."
} }
], ],
"resource": [{ "resource": [
{
"files": [ "files": [
"favicon.ico" "favicon.ico"
] ]
}, },
{ {
"files": ["*"], "files": [
"*"
],
"src": "../Media" "src": "../Media"
} }
], ],
"globalMetadata": { "globalMetadata": {
"_appTitle": "MLEM Documentation", "_appTitle": "MLEM Documentation",
"_appLogoPath": "Logo.svg", "_appLogoPath": "Logo.svg",
"_appFooter": "<a href=\"https://github.com/Ellpeck/MLEM\">&copy; 2019-2021 Ellpeck</a> &ndash; <a href=\"https://ellpeck.de/impressum\">Impressum</a> &ndash; <a href=\"https://ellpeck.de/privacy\">Privacy</a> &ndash; <a href=\"https://status.ellpeck.de\">Status</a>", "_appFooter": "<a href=\"https://github.com/Ellpeck/MLEM\">&copy; 2019-2023 Ellpeck</a> &ndash; <a href=\"https://ellpeck.de/impressum\">Impressum</a> &ndash; <a href=\"https://ellpeck.de/privacy\">Privacy</a> &ndash; <a href=\"https://status.ellpeck.de\">Status</a>",
"_enableSearch": true "_enableSearch": true
}, },
"dest": "_site", "dest": "_site",
"globalMetadataFiles": [],
"fileMetadataFiles": [],
"template": [ "template": [
"default", "default",
"templates/darkfx" "modern",
], "overrides"
"postProcessors": [], ]
"markdownEngineName": "markdig",
"noLangKeyword": false
} }
} }

View file

@ -1,49 +1 @@
![The MLEM logo](https://raw.githubusercontent.com/Ellpeck/MLEM/release/Media/Banner.png) [!INCLUDE [](../README.md)]
**MLEM Library for Extending MonoGame and FNA** is a set of multipurpose libraries for the game frameworks [MonoGame](https://www.monogame.net/) and [FNA](https://fna-xna.github.io/) that provides abstractions, quality of life improvements and additional features like an extensive ui system and easy input handling.
MLEM is platform-agnostic and multi-targets .NET Standard 2.0, .NET 6.0 and .NET Framework 4.5.2, which makes it compatible with MonoGame and FNA on Desktop, mobile devices and consoles.
# What next?
- Get it on [NuGet](https://www.nuget.org/packages?q=mlem)
- Get prerelease builds on [BaGet](https://nuget.ellpeck.de/?q=mlem)
- See the source code on [GitHub](https://github.com/Ellpeck/MLEM)
- See tutorials and API documentation on [the website](https://mlem.ellpeck.de/)
- Check out [the demos](https://github.com/Ellpeck/MLEM/tree/release/Demos) on [Desktop](https://github.com/Ellpeck/MLEM/tree/release/Demos.DesktopGL) or [Android](https://github.com/Ellpeck/MLEM/tree/release/Demos.Android)
- See [the changelog](https://mlem.ellpeck.de/CHANGELOG.html) for information on updates
# Packages
- **MLEM** is the base package, which provides various small addons and abstractions for MonoGame and FNA, including a text formatting system and simple input handling
- **MLEM.Ui** provides a mouse, keyboard, gamepad and touch ready Ui system that features automatic anchoring, sizing and several ready-to-use element types
- **MLEM.Extended** ties in with MonoGame.Extended and other MonoGame and FNA libraries
- **MLEM.Data** provides simple loading and processing of textures and other data, including the ability to load non-XNB content files easily
- **MLEM.Startup** combines MLEM with some other useful libraries into a quick Game startup class
- **MLEM.Templates** contains cross-platform project templates
# Made with MLEM
- [A Breath of Spring Air](https://ellpeck.itch.io/a-breath-of-spring-air), a short platformer ([Source](https://git.ellpeck.de/Ellpeck/GreatSpringGameJam))
- [Don't Wake Up](https://ellpeck.itch.io/dont-wake-up), a short puzzle game ([Source](https://github.com/Ellpeck/DontLetGo))
- [Pong Clone](https://github.com/luanfagu/pong), a very simple pong clone ([Source](https://github.com/luanfagu/pong))
- [Tiny Life](https://tinylifegame.com), an isometric life simulation game ([Modding API](https://github.com/Ellpeck/TinyLifeExampleMod))
If you created a game with the help of MLEM, you can get it added to this list by submitting it on the [issue tracker](https://github.com/Ellpeck/MLEM/issues). If its source is public, other people will be able to use your project as an example, too!
# Gallery
Here are some images that show a couple of MLEM's features.
The [MLEM.Ui](https://mlem.ellpeck.de/articles/ui) demo in action:
![A gif showing various user interface elements from the MLEM.Ui demo](https://raw.githubusercontent.com/Ellpeck/MLEM/release/Media/Ui.gif)
MLEM's [text formatting system](https://mlem.ellpeck.de/articles/text_formatting), which is compatible with both MLEM.Ui and regular sprite batch rendering:
![An image showing text with various colors and other formatting](https://raw.githubusercontent.com/Ellpeck/MLEM/release/Media/Formatting.png)
# Friends of MLEM
There are several other libraries and tools that work well in combination with MonoGame, FNA and MLEM. Here are some of them:
- [Contentless](https://github.com/Ellpeck/Contentless), a tool that removes the need to add assets to the MonoGame Content Pipeline manually
- [GameBundle](https://github.com/Ellpeck/GameBundle), a tool that packages MonoGame and other .NET applications into several distributable formats
- [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
- [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

View file

@ -0,0 +1,11 @@
exports.preTransform = function (model) {
if (model._path.includes("index")) {
// point to the release branch in the readme
model.conceptual = model.conceptual.replaceAll(/\/MLEM(\/[^/]+)?\/main\//g, "/MLEM$1/release/");
// reduce header levels by 1 to allow for TOC navigation
for (let i = 5; i >= 1; i--)
model.conceptual = model.conceptual.replaceAll(`<h${i}`, `<h${i + 1}`).replaceAll(`</h${i}`, `</h${i + 1}`);
}
return model;
};

View file

@ -1,40 +0,0 @@
{{!Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.}}
<div class="hidden-sm col-md-2" role="complementary">
<div class="sideaffix">
{{^_disableContribution}}
<div class="contribution">
<ul class="nav">
{{#docurl}}
<li>
<a href="{{docurl}}" class="contribution-link">{{__global.improveThisDoc}}</a>
</li>
{{/docurl}}
{{#sourceurl}}
<li>
<a href="{{sourceurl}}" class="contribution-link">{{__global.viewSource}}</a>
</li>
{{/sourceurl}}
</ul>
</div>
{{/_disableContribution}}
<div class="toggle-mode">
<div class="icon">
<i aria-hidden="true">☀</i>
</div>
<label class="switch">
<input type="checkbox" id="switch-style">
<span class="slider round"></span>
</label>
<div class="icon">
<i aria-hidden="true">☾</i>
</div>
</div>
<nav class="bs-docs-sidebar hidden-print hidden-xs hidden-sm affix" id="affix">
<h5>{{__global.inThisArticle}}</h5>
<div></div>
<!-- <p><a class="back-to-top" href="#top">Back to top</a><p> -->
</nav>
</div>
</div>

View file

@ -1,29 +0,0 @@
{{!Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.}}
<footer>
<div class="grad-bottom"></div>
<div class="footer">
<div class="container">
<span class="pull-right">
<a href="#top">Back to top</a>
</span>
<div class="pull-left">
{{{_appFooter}}}
{{^_appFooter}}<span>Generated by <strong>DocFX</strong></span>{{/_appFooter}}
</div>
<div class="toggle-mode pull-right visible-sm visible-xs">
<div class="icon">
<i aria-hidden="true">☀</i>
</div>
<label class="switch">
<input type="checkbox" id="switch-style-m">
<span class="slider round"></span>
</label>
<div class="icon">
<i aria-hidden="true">☾</i>
</div>
</div>
</div>
</div>
<script type="text/javascript" src="{{_rel}}styles/toggle-theme.js"></script>
</footer>

View file

@ -1,20 +0,0 @@
{{!Copyright (c) Oscar Vasquez. 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}}
</head>

View file

@ -1,470 +0,0 @@
:root, body.dark-theme {
--color-foreground: #ccd5dc;
--color-navbar: #66666d;
--color-breadcrumb: #999;
--color-underline: #ddd;
--color-toc-hover: #fff;
--color-background: #2d2d30;
--color-background-subnav: #333337;
--color-background-dark: #1e1e1e;
--color-background-table-alt: #212123;
--color-background-quote: #69696e;
}
body.light-theme {
--color-foreground: #171717;
--color-breadcrumb: #4a4a4a;
--color-toc-hover: #4c4c4c;
--color-background: #ffffff;
--color-background-subnav: #f5f5f5;
--color-background-dark: #ddd;
--color-background-table-alt: #f9f9f9;
}
body {
color: var(--color-foreground);
line-height: 1.5;
font-size: 14px;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
word-wrap: break-word;
background-color: var(--color-background);
}
.btn.focus, .btn:focus, .btn:hover {
color: var(--color-foreground);
}
h1 {
font-weight: 600;
font-size: 32px;
}
h2 {
font-weight: 600;
font-size: 24px;
line-height: 1.8;
}
h3 {
font-weight: 600;
font-size: 20px;
line-height: 1.8;
}
h5 {
font-size: 14px;
padding: 10px 0px;
}
article h1, article h2, article h3, article h4 {
margin-top: 35px;
margin-bottom: 15px;
}
article h4 {
padding-bottom: 8px;
border-bottom: 2px solid var(--color-underline);
}
.navbar-brand>img {
color: var(--color-background);
}
.navbar {
border: none;
}
.subnav {
border-top: 1px solid var(--color-underline);
background-color: var(--color-background-subnav);
}
.sidenav, .fixed_header, .toc {
background-color: var(--color-background);
}
.navbar-inverse {
background-color: var(--color-background-dark);
z-index: 100;
}
.navbar-inverse .navbar-nav>li>a, .navbar-inverse .navbar-text {
color: var(--color-navbar);
background-color: var(--color-background-dark);
border-bottom: 3px solid transparent;
padding-bottom: 12px;
}
.navbar-inverse .navbar-nav>li>a:focus, .navbar-inverse .navbar-nav>li>a:hover {
color: var(--color-foreground);
background-color: var(--color-background-dark);
border-bottom: 3px solid var(--color-background-subnav);
transition: all ease 0.25s;
}
.navbar-inverse .navbar-nav>.active>a, .navbar-inverse .navbar-nav>.active>a:focus, .navbar-inverse .navbar-nav>.active>a:hover {
color: var(--color-foreground);
background-color: var(--color-background-dark);
border-bottom: 3px solid var(--color-foreground);
transition: all ease 0.25s;
}
.navbar-form .form-control {
border: none;
border-radius: 0;
}
.light-theme .navbar-brand svg {
filter: brightness(20%);
}
.toc .level1>li {
font-weight: 400;
}
.toc .nav>li>a {
color: var(--color-foreground);
}
.sidefilter {
background-color: var(--color-background);
border-left: none;
border-right: none;
}
.sidefilter {
background-color: var(--color-background);
border-left: none;
border-right: none;
}
.toc-filter {
padding: 10px;
margin: 0;
background-color: var(--color-background);
}
.toc-filter>input {
border: none;
border-radius: unset;
background-color: var(--color-background-subnav);
padding: 5px 0 5px 20px;
font-size: 90%
}
.toc-filter>.clear-icon {
position: absolute;
top: 17px;
right: 15px;
}
.toc-filter>input:focus {
color: var(--color-foreground);
transition: all ease 0.25s;
}
.toc-filter>.filter-icon {
display: none;
}
.sidetoc>.toc {
background-color: var(--color-background);
overflow-x: hidden;
}
.sidetoc {
background-color: var(--color-background);
border: none;
}
.alert {
background-color: inherit;
border: none;
padding: 10px 0;
border-radius: 0;
}
.alert>p {
margin-bottom: 0;
padding: 5px 10px;
border-bottom: 1px solid;
background-color: var(--color-background-dark);
}
.alert>h5 {
padding: 10px 15px;
margin-top: 0;
margin-bottom: 0;
text-transform: uppercase;
font-weight: bold;
border-top: 2px solid;
background-color: var(--color-background-dark);
border-radius: none;
}
.alert>ul {
margin-bottom: 0;
padding: 5px 40px;
}
.alert-info {
color: #1976d2;
}
.alert-warning {
color: #f57f17;
}
.alert-danger {
color: #d32f2f;
}
pre {
padding: 9.5px;
margin: 0 0 10px;
font-size: 13px;
word-break: break-all;
word-wrap: break-word;
background-color: var(--color-background-dark);
border-radius: 0;
border: none;
}
code {
background: var(--color-background-dark) !important;
border-radius: 2px;
}
.hljs {
color: var(--color-foreground);
}
.toc .nav>li.active>.expand-stub::before, .toc .nav>li.in>.expand-stub::before, .toc .nav>li.in.active>.expand-stub::before, .toc .nav>li.filtered>.expand-stub::before {
content: "▾";
}
.toc .nav>li>.expand-stub::before, .toc .nav>li.active>.expand-stub::before {
content: "▸";
}
.affix ul ul>li>a:before {
content: "|";
}
.breadcrumb {
background-color: var(--color-background-subnav);
}
.breadcrumb .label.label-primary {
background: #444;
border-radius: 0;
font-weight: normal;
font-size: 100%;
}
#breadcrumb .breadcrumb>li a {
border-radius: 0;
font-weight: normal;
font-size: 85%;
display: inline;
padding: 0 .6em 0;
line-height: 1;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
color: var(--color-breadcrumb);
}
#breadcrumb .breadcrumb>li a:hover {
color: var(--color-foreground);
transition: all ease 0.25s;
}
.breadcrumb>li+li:before {
content: "⯈";
font-size: 75%;
color: var(--color-background-dark);
padding: 0;
}
.light-theme .breadcrumb>li+li:before {
color: var(--color-foreground)
}
.toc .level1>li {
font-weight: 600;
font-size: 130%;
padding-left: 5px;
}
.footer {
border-top: none;
background-color: var(--color-background-dark);
padding: 15px 0;
font-size: 90%;
}
.toc .nav>li>a:hover, .toc .nav>li>a:focus {
color: var(--color-toc-hover);
transition: all ease 0.1s;
}
.form-control {
background-color: var(--color-background-subnav);
border: none;
border-radius: 0;
-webkit-box-shadow: none;
box-shadow: none;
}
.form-control:focus {
border-color: #66afe9;
outline: 0;
-webkit-box-shadow: none;
box-shadow: none;
}
input#search-query:focus {
color: var(--color-foreground);
}
.table-bordered, .table-bordered>tbody>tr>td, .table-bordered>tbody>tr>th, .table-bordered>tfoot>tr>td, .table-bordered>tfoot>tr>th, .table-bordered>thead>tr>td, .table-bordered>thead>tr>th {
border: 1px solid var(--color-background-dark);
}
.table-striped>tbody>tr:nth-of-type(odd) {
background-color: var(--color-background-table-alt);
}
blockquote {
padding: 10px 20px;
margin: 0 0 10px;
font-size: 110%;
border-left: 5px solid var(--color-background-quote);
color: var(--color-background-quote);
}
.pagination>.disabled>a, .pagination>.disabled>a:focus, .pagination>.disabled>a:hover, .pagination>.disabled>span, .pagination>.disabled>span:focus, .pagination>.disabled>span:hover {
background-color: var(--color-background-subnav);
border-color: var(--color-background-subnav);
}
.breadcrumb>li, .pagination {
display: inline;
}
.tabGroup a[role="tab"] {
border-bottom: 2px solid var(--color-background-dark);
}
.tabGroup a[role="tab"][aria-selected="true"] {
color: var(--color-foreground);
}
.tabGroup section[role="tabpanel"] {
border: 1px solid var(--color-background-dark);
}
.sideaffix > div.contribution > ul > li > a.contribution-link:hover {
background-color: var(--color-background);
}
.switch {
position: relative;
display: inline-block;
width: 40px;
height: 20px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
-webkit-transition: .4s;
transition: .4s;
}
.slider:before {
position: absolute;
content: "";
height: 14px;
width: 14px;
left: 4px;
bottom: 3px;
background-color: white;
-webkit-transition: .4s;
transition: .4s;
}
input:checked + .slider {
background-color: #337ab7;
}
input:focus + .slider {
box-shadow: 0 0 1px #337ab7;
}
input:checked + .slider:before {
-webkit-transform: translateX(19px);
-ms-transform: translateX(19px);
transform: translateX(19px);
}
/* Rounded sliders */
.slider.round {
border-radius: 20px;
}
.slider.round:before {
border-radius: 50%;
}
.toggle-mode .icon {
display: inline-block;
}
.toggle-mode .icon i {
font-style: normal;
font-size: 17px;
display: inline-block;
padding-right: 7px;
padding-left: 7px;
vertical-align: middle;
}
@media (min-width: 1600px) {
.container {
width: 100%;
}
.sidefilter {
width: 18%;
}
.sidetoc {
width: 18%;
}
.article.grid-right {
margin-left: 19%;
}
.sideaffix {
width: 11.5%;
}
.affix ul>li.active>a {
white-space: initial;
}
.affix ul>li>a {
width: 99%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}

View file

@ -1,35 +0,0 @@
const sw = document.getElementById("switch-style"), sw_mobile = document.getElementById("switch-style-m"), b = document.body;
if (b) {
function toggleTheme(target, dark) {
target.classList.toggle("dark-theme", dark)
target.classList.toggle("light-theme", !dark)
}
function switchEventListener() {
toggleTheme(b, this.checked);
if (window.localStorage) {
this.checked ? localStorage.setItem("theme", "dark-theme") : localStorage.setItem("theme", "light-theme")
}
}
var isDarkTheme = !window.localStorage || !window.localStorage.getItem("theme") || window.localStorage && localStorage.getItem("theme") === "dark-theme";
if(sw && sw_mobile){
sw.checked = isDarkTheme;
sw_mobile.checked = isDarkTheme;
sw.addEventListener("change", switchEventListener);
sw_mobile.addEventListener("change", switchEventListener);
// sync state between switches
sw.addEventListener("change", function() {
sw_mobile.checked = this.checked;
});
sw_mobile.addEventListener("change", function() {
sw.checked = this.checked;
});
}
toggleTheme(b, isDarkTheme);
}

2
FNA

@ -1 +1 @@
Subproject commit 9029e149358197612509e2ee4893870a3b5d590e Subproject commit 697cc63662914c0dc26c500bc9b8498b5ca8a68f

BIN
FnaNative/FAudio.dll Normal file

Binary file not shown.

BIN
FnaNative/FNA3D.dll Normal file

Binary file not shown.

BIN
FnaNative/SDL2.dll Normal file

Binary file not shown.

BIN
FnaNative/libFAudio.so.0 Normal file

Binary file not shown.

BIN
FnaNative/libFNA3D.0.dylib Normal file

Binary file not shown.

BIN
FnaNative/libFNA3D.so.0 Normal file

Binary file not shown.

Binary file not shown.

BIN
FnaNative/libSDL2-2.0.so.0 Normal file

Binary file not shown.

BIN
FnaNative/libvulkan.1.dylib Normal file

Binary file not shown.

@ -1 +1 @@
Subproject commit c50bf544bcfb217b518727a0a38eb71fc5725092 Subproject commit f11f97b709e50960dd8ce1f727974744c4f8a0dd

55
Jenkinsfile vendored
View file

@ -1,39 +1,46 @@
pipeline { pipeline {
agent any agent none
stages { stages {
stage('Submodules') { stage('Cake') {
steps { agent any
sh 'git submodule update --init --recursive --force' stages {
stage('Submodules') {
steps {
sh 'git submodule update --init --recursive --force'
}
}
stage('Build') {
steps {
sh 'dotnet tool restore'
// we use xvfb to allow for graphics-dependent tests
sh 'xvfb-run -a dotnet cake --target Publish --branch ' + env.BRANCH_NAME
}
}
stage('Document') {
steps {
sh 'dotnet cake --target Document --branch ' + env.BRANCH_NAME
stash includes: 'Docs/_site/**', name: 'site'
}
}
} }
} post {
stage('Cake Build') { always {
steps { nunit testResultsPattern: '**/TestResults.xml'
sh 'dotnet tool restore' cobertura coberturaReportFile: '**/coverage.cobertura.xml'
// we use xvfb to allow for graphics-dependent tests }
sh 'xvfb-run -a dotnet cake --target Publish --branch ' + env.BRANCH_NAME
}
}
stage('Document') {
steps {
sh 'dotnet cake --target Document --branch ' + env.BRANCH_NAME
} }
} }
stage('Publish Docs') { stage('Publish Docs') {
when { when { branch 'release' }
branch 'release' agent { label 'web' }
} options { skipDefaultCheckout() }
steps { steps {
unstash 'site'
sh 'rm -rf /var/www/MLEM/*' sh 'rm -rf /var/www/MLEM/*'
sh 'cp Docs/_site/** /var/www/MLEM/ -r' sh 'cp Docs/_site/** /var/www/MLEM/ -r'
} }
} }
} }
post {
always {
nunit testResultsPattern: '**/TestResults.xml'
cobertura coberturaReportFile: '**/coverage.cobertura.xml'
}
}
environment { environment {
BAGET = credentials('3db850d0-e6b5-43d5-b607-d180f4eab676') BAGET = credentials('3db850d0-e6b5-43d5-b607-d180f4eab676')
NUGET = credentials('e1bf7f6c-6047-4f7e-b639-15240a8f8351') NUGET = credentials('e1bf7f6c-6047-4f7e-b639-15240a8f8351')

View file

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

View file

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>net452;netstandard2.0;net6.0</TargetFrameworks> <TargetFrameworks>net452;netstandard2.0;net7.0</TargetFrameworks>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<ProduceReferenceAssembly>true</ProduceReferenceAssembly> <ProduceReferenceAssembly>true</ProduceReferenceAssembly>
<IsTrimmable>true</IsTrimmable> <IsTrimmable>true</IsTrimmable>
@ -42,6 +42,6 @@
<ItemGroup> <ItemGroup>
<None Include="../Media/Logo.png" Pack="true" PackagePath="" /> <None Include="../Media/Logo.png" Pack="true" PackagePath="" />
<None Include="../Docs/index.md" Pack="true" PackagePath="README.md" /> <None Include="../README.md" Pack="true" PackagePath="" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>net452;netstandard2.0;net6.0</TargetFrameworks> <TargetFrameworks>net452;netstandard2.0;net7.0</TargetFrameworks>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<ProduceReferenceAssembly>true</ProduceReferenceAssembly> <ProduceReferenceAssembly>true</ProduceReferenceAssembly>
<IsTrimmable>true</IsTrimmable> <IsTrimmable>true</IsTrimmable>
@ -26,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.2"> <PackageReference Include="Newtonsoft.Json" Version="13.0.3">
<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">
@ -39,6 +39,6 @@
<ItemGroup> <ItemGroup>
<None Include="../Media/Logo.png" Pack="true" PackagePath="" /> <None Include="../Media/Logo.png" Pack="true" PackagePath="" />
<None Include="../Docs/index.md" Pack="true" PackagePath="README.md" /> <None Include="../README.md" Pack="true" PackagePath="" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -33,10 +33,15 @@ namespace MLEM.Data {
/// The time that <see cref="Pack"/> took the last time it was called /// The time that <see cref="Pack"/> took the last time it was called
/// </summary> /// </summary>
public TimeSpan LastTotalTime => this.LastCalculationTime + this.LastPackTime; public TimeSpan LastTotalTime => this.LastCalculationTime + this.LastPackTime;
/// <summary>
/// The amount of currently packed texture regions.
/// </summary>
public int PackedTextures => this.packedTextures.Count;
private readonly List<Request> texturesToPack = new List<Request>(); private readonly List<Request> texturesToPack = new List<Request>();
private readonly List<Request> packedTextures = new List<Request>(); private readonly List<Request> packedTextures = new List<Request>();
private readonly Dictionary<Point, Point> firstPossiblePosForSize = new Dictionary<Point, Point>(); private readonly Dictionary<Point, Request> occupiedPositions = new Dictionary<Point, Request>();
private readonly Dictionary<Point, Point> initialPositions = 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;
@ -49,7 +54,7 @@ namespace MLEM.Data {
/// Creates a new runtime texture packer with the given settings. /// Creates a new runtime texture packer with the given settings.
/// </summary> /// </summary>
/// <param name="maxWidth">The maximum width that the packed texture can have. Defaults to 2048.</param> /// <param name="maxWidth">The maximum width that the packed texture can have. Defaults to 2048.</param>
/// <param name="autoIncreaseMaxWidth">Whether the maximum width should be increased if there is a texture to be packed that is wider than <see cref="maxWidth"/>. Defaults to false.</param> /// <param name="autoIncreaseMaxWidth">Whether the maximum width should be increased if there is a texture to be packed that is wider than the maximum width specified in the constructor. Defaults to false.</param>
/// <param name="forcePowerOfTwo">Whether the resulting <see cref="PackedTexture"/> should have a width and height that is a power of two.</param> /// <param name="forcePowerOfTwo">Whether the resulting <see cref="PackedTexture"/> should have a width and height that is a power of two.</param>
/// <param name="forceSquare">Whether the resulting <see cref="PackedTexture"/> should be square regardless of required size.</param> /// <param name="forceSquare">Whether the resulting <see cref="PackedTexture"/> should be square regardless of required size.</param>
/// <param name="disposeTextures">Whether the original textures submitted to this texture packer should be disposed after packing.</param> /// <param name="disposeTextures">Whether the original textures submitted to this texture packer should be disposed after packing.</param>
@ -162,9 +167,7 @@ namespace MLEM.Data {
// 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.OccupyFreeArea(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
this.firstPossiblePosForSize[new Point(request.PackedArea.Width, request.PackedArea.Height)] = request.PackedArea.Location;
this.packedTextures.Add(request); this.packedTextures.Add(request);
} }
stopwatch.Stop(); stopwatch.Stop();
@ -224,7 +227,8 @@ namespace MLEM.Data {
this.LastPackTime = TimeSpan.Zero; this.LastPackTime = TimeSpan.Zero;
this.texturesToPack.Clear(); this.texturesToPack.Clear();
this.packedTextures.Clear(); this.packedTextures.Clear();
this.firstPossiblePosForSize.Clear(); this.initialPositions.Clear();
this.occupiedPositions.Clear();
this.dataCache.Clear(); this.dataCache.Clear();
} }
@ -233,31 +237,45 @@ namespace MLEM.Data {
this.Reset(); this.Reset();
} }
private Rectangle FindFreeArea(Request request) { private Rectangle OccupyFreeArea(Request request) {
var size = new Point(request.Texture.Width, request.Texture.Height); var size = new Point(request.Texture.Width, request.Texture.Height);
size.X += request.Padding * 2; size.X += request.Padding * 2;
size.Y += request.Padding * 2; size.Y += request.Padding * 2;
var pos = this.firstPossiblePosForSize.TryGetValue(size, out var first) ? first : Point.Zero; // exit early if the texture doesn't need to find a free location
if (size.X <= 0 || size.Y <= 0)
return Rectangle.Empty;
var pos = this.initialPositions.TryGetValue(size, out var first) ? first : Point.Zero;
var area = new Rectangle(pos.X, pos.Y, size.X, size.Y);
var lowestY = int.MaxValue; var lowestY = int.MaxValue;
while (true) { while (true) {
var intersected = false; // check if the current area is already occupied
var area = new Rectangle(pos.X, pos.Y, size.X, size.Y); if (!this.occupiedPositions.TryGetValue(area.Location, out var existing)) {
foreach (var tex in this.packedTextures) { existing = this.packedTextures.FirstOrDefault(t => t.PackedArea.Intersects(area));
if (tex.PackedArea.Intersects(area)) { // if no texture is occupying this space, we have found a free area
pos.X = tex.PackedArea.Right; if (existing == null) {
// when we move down, we want to move down by the smallest intersecting texture's height // 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 (lowestY > tex.PackedArea.Bottom) this.initialPositions[new Point(area.Width, area.Height)] = area.Location;
lowestY = tex.PackedArea.Bottom; this.occupiedPositions.Add(area.Location, request);
intersected = true; return area;
break;
} }
// also cache the existing texture for this position, in case we check it again in the future
this.occupiedPositions.Add(area.Location, existing);
} }
if (!intersected)
return area; // move to the right by the existing texture's width
if (pos.X + size.X > this.maxWidth) { area.X = existing.PackedArea.Right;
pos.X = 0;
pos.Y = lowestY; // remember the smallest intersecting texture's height for when we move down
if (lowestY > existing.PackedArea.Bottom)
lowestY = existing.PackedArea.Bottom;
// move down a row if we exceed our maximum width
if (area.Right > this.maxWidth) {
area.X = 0;
area.Y = lowestY;
lowestY = int.MaxValue; lowestY = int.MaxValue;
} }
} }

View file

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks> <TargetFrameworks>netstandard2.0;net7.0</TargetFrameworks>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<ProduceReferenceAssembly>true</ProduceReferenceAssembly> <ProduceReferenceAssembly>true</ProduceReferenceAssembly>
<IsTrimmable>true</IsTrimmable> <IsTrimmable>true</IsTrimmable>
@ -38,6 +38,6 @@
<ItemGroup> <ItemGroup>
<None Include="../Media/Logo.png" Pack="true" PackagePath="" /> <None Include="../Media/Logo.png" Pack="true" PackagePath="" />
<None Include="../Docs/index.md" Pack="true" PackagePath="README.md" /> <None Include="../README.md" Pack="true" PackagePath="" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks> <TargetFrameworks>netstandard2.0;net7.0</TargetFrameworks>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<ProduceReferenceAssembly>true</ProduceReferenceAssembly> <ProduceReferenceAssembly>true</ProduceReferenceAssembly>
<IsTrimmable>true</IsTrimmable> <IsTrimmable>true</IsTrimmable>
@ -37,6 +37,6 @@
<ItemGroup> <ItemGroup>
<None Include="../Media/Logo.png" Pack="true" PackagePath="" /> <None Include="../Media/Logo.png" Pack="true" PackagePath="" />
<None Include="../Docs/index.md" Pack="true" PackagePath="README.md" /> <None Include="../README.md" Pack="true" PackagePath="" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -190,7 +190,7 @@ namespace MLEM.Extended.Tiled {
} }
/// <summary> /// <summary>
/// A delegate method used for <see cref="IndividualTiledMapRenderer.depthFunction"/>. /// A delegate method used for an <see cref="IndividualTiledMapRenderer"/>'s depth function.
/// The idea is to return a depth (between 0 and 1) for the given tile that determines where in the sprite batch it should be rendererd. /// The idea is to return a depth (between 0 and 1) for the given tile that determines where in the sprite batch it should be rendererd.
/// Note that, for this depth function to take effect, the sprite batch needs to begin with <see cref="SpriteSortMode.FrontToBack"/> or <see cref="SpriteSortMode.BackToFront"/>. /// Note that, for this depth function to take effect, the sprite batch needs to begin with <see cref="SpriteSortMode.FrontToBack"/> or <see cref="SpriteSortMode.BackToFront"/>.
/// </summary> /// </summary>

View file

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>net452;netstandard2.0;net6.0</TargetFrameworks> <TargetFrameworks>net452;netstandard2.0;net7.0</TargetFrameworks>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<ProduceReferenceAssembly>true</ProduceReferenceAssembly> <ProduceReferenceAssembly>true</ProduceReferenceAssembly>
<IsTrimmable>true</IsTrimmable> <IsTrimmable>true</IsTrimmable>
@ -33,6 +33,6 @@
<ItemGroup> <ItemGroup>
<None Include="../Media/Logo.png" Pack="true" PackagePath="" /> <None Include="../Media/Logo.png" Pack="true" PackagePath="" />
<None Include="../Docs/index.md" Pack="true" PackagePath="README.md" /> <None Include="../README.md" Pack="true" PackagePath="" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>net452;netstandard2.0;net6.0</TargetFrameworks> <TargetFrameworks>net452;netstandard2.0;net7.0</TargetFrameworks>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<ProduceReferenceAssembly>true</ProduceReferenceAssembly> <ProduceReferenceAssembly>true</ProduceReferenceAssembly>
<IsTrimmable>true</IsTrimmable> <IsTrimmable>true</IsTrimmable>
@ -20,7 +20,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Coroutine" Version="2.1.4" /> <PackageReference Include="Coroutine" Version="2.1.5" />
<ProjectReference Include="..\MLEM.Ui\MLEM.Ui.csproj" /> <ProjectReference Include="..\MLEM.Ui\MLEM.Ui.csproj" />
<ProjectReference Include="..\MLEM\MLEM.csproj" /> <ProjectReference Include="..\MLEM\MLEM.csproj" />
@ -31,6 +31,6 @@
<ItemGroup> <ItemGroup>
<None Include="../Media/Logo.png" Pack="true" PackagePath="" /> <None Include="../Media/Logo.png" Pack="true" PackagePath="" />
<None Include="../Docs/index.md" Pack="true" PackagePath="README.md" /> <None Include="../README.md" Pack="true" PackagePath="" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>net452;netstandard2.0;net6.0</TargetFrameworks> <TargetFrameworks>net452;netstandard2.0;net7.0</TargetFrameworks>
<IncludeContentInPack>true</IncludeContentInPack> <IncludeContentInPack>true</IncludeContentInPack>
<IncludeBuildOutput>false</IncludeBuildOutput> <IncludeBuildOutput>false</IncludeBuildOutput>
<ContentTargetFolders>content</ContentTargetFolders> <ContentTargetFolders>content</ContentTargetFolders>
@ -27,7 +27,7 @@
<Content Include="content\**\*" Exclude="content\**\.DS_Store;content\**\bin;content\**\obj" /> <Content Include="content\**\*" Exclude="content\**\.DS_Store;content\**\bin;content\**\obj" />
<Compile Remove="**\*" /> <Compile Remove="**\*" />
<None Include="../Media/Logo.png" Pack="true" PackagePath="" /> <None Include="../Media/Logo.png" Pack="true" PackagePath="" />
<None Include="../Docs/index.md" Pack="true" PackagePath="README.md" /> <None Include="../README.md" Pack="true" PackagePath="" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -3,31 +3,31 @@
"isRoot": true, "isRoot": true,
"tools": { "tools": {
"dotnet-mgcb": { "dotnet-mgcb": {
"version": "3.8.1.263", "version": "3.8.1.303",
"commands": [ "commands": [
"mgcb" "mgcb"
] ]
}, },
"dotnet-mgcb-editor": { "dotnet-mgcb-editor": {
"version": "3.8.1.263", "version": "3.8.1.303",
"commands": [ "commands": [
"mgcb-editor" "mgcb-editor"
] ]
}, },
"dotnet-mgcb-editor-linux": { "dotnet-mgcb-editor-linux": {
"version": "3.8.1.263", "version": "3.8.1.303",
"commands": [ "commands": [
"mgcb-editor-linux" "mgcb-editor-linux"
] ]
}, },
"dotnet-mgcb-editor-windows": { "dotnet-mgcb-editor-windows": {
"version": "3.8.1.263", "version": "3.8.1.303",
"commands": [ "commands": [
"mgcb-editor-windows" "mgcb-editor-windows"
] ]
}, },
"dotnet-mgcb-editor-mac": { "dotnet-mgcb-editor-mac": {
"version": "3.8.1.263", "version": "3.8.1.303",
"commands": [ "commands": [
"mgcb-editor-mac" "mgcb-editor-mac"
] ]

View file

@ -11,8 +11,8 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Contentless" Version="3.*" /> <PackageReference Include="Contentless" Version="3.*" />
<PackageReference Include="MLEM.Startup" Version="6.*" /> <PackageReference Include="MLEM.Startup" Version="6.*" />
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.1.263" /> <PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.1.303" />
<PackageReference Include="MonoGame.Content.Builder.Task" Version="3.8.1.263" /> <PackageReference Include="MonoGame.Content.Builder.Task" Version="3.8.1.303" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View file

@ -6,7 +6,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="MLEM.Startup" Version="6.*" /> <PackageReference Include="MLEM.Startup" Version="6.*" />
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.1.263"> <PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.1.303">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>
</ItemGroup> </ItemGroup>

View file

@ -60,7 +60,7 @@ namespace MLEM.Ui {
AutoRight, AutoRight,
/// <summary> /// <summary>
/// This is an auto-anchoring value. /// This is an auto-anchoring value.
/// This anchor will cause an element to be placed in the same line as its older sibling, or at the start of the next line if there is no space to the right of its older sibling. /// This anchor will cause an element to be placed at the top right of its older sibling, or at the start of the next line if there is no space to the right of its older sibling.
/// </summary> /// </summary>
AutoInline, AutoInline,
/// <summary> /// <summary>
@ -68,7 +68,29 @@ namespace MLEM.Ui {
/// This anchor is an overflow-ignoring version of <see cref="AutoInline"/>, meaning that the element will never be forced into the next line. /// This anchor is an overflow-ignoring version of <see cref="AutoInline"/>, meaning that the element will never be forced into the next line.
/// Note that, when using this property, it is very easy to cause an element to overflow out of its parent container. /// Note that, when using this property, it is very easy to cause an element to overflow out of its parent container.
/// </summary> /// </summary>
AutoInlineIgnoreOverflow AutoInlineIgnoreOverflow,
/// <summary>
/// This is an auto-anchoring value.
/// This anchor will cause an element to be placed at the center right of its older sibling, or at the start of the next line if there is no space to the right of its older sibling.
/// </summary>
AutoInlineCenter,
/// <summary>
/// This is an auto-anchoring value.
/// This anchor is an overflow-ignoring version of <see cref="AutoInlineCenter"/>, meaning that the element will never be forced into the next line.
/// Note that, when using this property, it is very easy to cause an element to overflow out of its parent container.
/// </summary>
AutoInlineCenterIgnoreOverflow,
/// <summary>
/// This is an auto-anchoring value.
/// This anchor will cause an element to be placed at the bottom right of its older sibling, or at the start of the next line if there is no space to the right of its older sibling.
/// </summary>
AutoInlineBottom,
/// <summary>
/// This is an auto-anchoring value.
/// This anchor is an overflow-ignoring version of <see cref="AutoInlineBottom"/>, meaning that the element will never be forced into the next line.
/// Note that, when using this property, it is very easy to cause an element to overflow out of its parent container.
/// </summary>
AutoInlineBottomIgnoreOverflow
} }
} }

View file

@ -76,7 +76,7 @@ namespace MLEM.Ui.Elements {
public bool CanSelectDisabled; public bool CanSelectDisabled;
/// <summary> /// <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. /// 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. /// Note that, if <see cref="IsDisabled"/>'s underlying value is set to <see langword="true"/> using <see cref="IsDisabled"/>, this function's result will be ignored.
/// </summary> /// </summary>
public Func<Button, bool> AutoDisableCondition; public Func<Button, bool> AutoDisableCondition;

View file

@ -106,13 +106,12 @@ namespace MLEM.Ui.Elements {
} }
/// <inheritdoc /> /// <inheritdoc />
protected override Vector2 CalcActualSize(RectangleF parentArea) { public override void SetAreaAndUpdateChildren(RectangleF area) {
var size = base.CalcActualSize(parentArea); base.SetAreaAndUpdateChildren(area);
if (this.Label != null) { if (this.Label != null) {
this.Label.Size = new Vector2((size.X - size.Y) / this.Scale - this.TextOffsetX, 1); this.Label.Size = new Vector2((area.Width - area.Height) / this.Scale - this.TextOffsetX, 1);
this.Label.PositionOffset = new Vector2(size.Y / this.Scale + this.TextOffsetX, 0); this.Label.PositionOffset = new Vector2(area.Height / this.Scale + this.TextOffsetX, 0);
} }
return size;
} }
/// <inheritdoc /> /// <inheritdoc />

View file

@ -37,10 +37,13 @@ namespace MLEM.Ui.Elements {
/// <param name="text">The text displayed on the dropdown button</param> /// <param name="text">The text displayed on the dropdown button</param>
/// <param name="tooltipText">The text displayed as a tooltip when hovering over the dropdown button</param> /// <param name="tooltipText">The text displayed as a tooltip when hovering over the dropdown button</param>
public Dropdown(Anchor anchor, Vector2 size, string text = null, string tooltipText = null) : base(anchor, size, text, tooltipText) { public Dropdown(Anchor anchor, Vector2 size, string text = null, string tooltipText = null) : base(anchor, size, text, tooltipText) {
this.Panel = this.AddChild(new Panel(Anchor.TopCenter, size, Vector2.Zero, true) { this.Panel = this.AddChild(new Panel(Anchor.TopCenter, Vector2.Zero, Vector2.Zero, true) {
IsHidden = true IsHidden = true
}); });
this.OnAreaUpdated += e => this.Panel.PositionOffset = new Vector2(0, e.Area.Height / this.Scale); this.OnAreaUpdated += e => {
this.Panel.Size = new Vector2(e.Area.Width / e.Scale, 0);
this.Panel.PositionOffset = new Vector2(0, e.Area.Height / e.Scale);
};
this.OnOpenedOrClosed += e => this.Priority = this.IsOpen ? 10000 : 0; this.OnOpenedOrClosed += e => this.Priority = this.IsOpen ? 10000 : 0;
this.OnPressed += e => { this.OnPressed += e => {
this.IsOpen = !this.IsOpen; this.IsOpen = !this.IsOpen;

View file

@ -34,7 +34,7 @@ namespace MLEM.Ui.Elements {
private 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.AndChildren(e => e.Style = e.Style.OrStyle(value?.Style));
} }
} }
/// <summary> /// <summary>
@ -320,9 +320,15 @@ namespace MLEM.Ui.Elements {
public bool IsMouseOver => this.Controls.MousedElement == this || this.Controls.TouchedElement == this; public bool IsMouseOver => this.Controls.MousedElement == this || this.Controls.TouchedElement == this;
/// <summary> /// <summary>
/// Returns whether this element is its <see cref="Root"/>'s <see cref="RootElement.SelectedElement"/>. /// Returns whether this element is its <see cref="Root"/>'s <see cref="RootElement.SelectedElement"/>.
/// Note that, unlike <see cref="IsSelectedActive"/>, this property will be <see langword="true"/> even if this element's <see cref="Root"/> is not the <see cref="UiControls.ActiveRoot"/>.
/// </summary> /// </summary>
public bool IsSelected => this.Root.SelectedElement == this; public bool IsSelected => this.Root.SelectedElement == this;
/// <summary> /// <summary>
/// Returns whether this element is its <see cref="Controls"/>'s <see cref="UiControls.SelectedElement"/>.
/// Note that <see cref="IsSelected"/> can be used to query whether this element is its <see cref="Root"/>'s <see cref="RootElement.SelectedElement"/> instead.
/// </summary>
public bool IsSelectedActive => this.Controls.SelectedElement == this;
/// <summary>
/// Returns whether this element's <see cref="SetAreaDirty"/> method has been recently called and its area has not been updated since then using <see cref="UpdateAreaIfDirty"/> or <see cref="ForceUpdateArea"/>. /// Returns whether this element's <see cref="SetAreaDirty"/> method has been recently called and its area has not been updated since then using <see cref="UpdateAreaIfDirty"/> or <see cref="ForceUpdateArea"/>.
/// </summary> /// </summary>
public bool AreaDirty { get; private set; } public bool AreaDirty { get; private set; }
@ -373,6 +379,14 @@ namespace MLEM.Ui.Elements {
this.SetAreaDirty(); this.SetAreaDirty();
} }
} }
/// <summary>
/// A <see cref="UiAnimation"/> that is played when the mouse enters this element, in <see cref="OnMouseEnter"/>.
/// </summary>
public StyleProp<UiAnimation> MouseEnterAnimation;
/// <summary>
/// A <see cref="UiAnimation"/> that is played when the mouse exits this element, in <see cref="OnMouseExit"/>.
/// </summary>
public StyleProp<UiAnimation> MouseExitAnimation;
/// <summary> /// <summary>
/// Event that is called after this element is drawn, but before its children are drawn /// Event that is called after this element is drawn, but before its children are drawn
@ -484,6 +498,12 @@ namespace MLEM.Ui.Elements {
/// Use <see cref="AddChild{T}"/> or <see cref="RemoveChild"/> to manipulate this list while calling all of the necessary callbacks. /// Use <see cref="AddChild{T}"/> or <see cref="RemoveChild"/> to manipulate this list while calling all of the necessary callbacks.
/// </summary> /// </summary>
protected readonly IList<Element> Children; protected readonly IList<Element> Children;
/// <summary>
/// A list of all of the <see cref="UiAnimation"/> instances that are currently playing.
/// You can modify this collection through <see cref="PlayAnimation"/> and <see cref="StopAnimation"/>.
/// </summary>
protected readonly List<UiAnimation> PlayingAnimations = new List<UiAnimation>();
/// <summary> /// <summary>
/// A sorted version of <see cref="Children"/>. The children are sorted by their <see cref="Priority"/>. /// A sorted version of <see cref="Children"/>. The children are sorted by their <see cref="Priority"/>.
/// </summary> /// </summary>
@ -535,8 +555,16 @@ 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.OnMouseEnter += e => {
if (e.MouseEnterAnimation.HasValue())
e.PlayAnimation(e.MouseEnterAnimation);
};
this.OnMouseExit += e => {
if (e.MouseExitAnimation.HasValue())
e.PlayAnimation(e.MouseExitAnimation);
};
this.SetAreaDirty(); this.SetAreaDirty();
this.SetSortedChildrenDirty(); this.SetSortedChildrenDirty();
@ -673,7 +701,11 @@ namespace MLEM.Ui.Elements {
case Anchor.TopLeft: case Anchor.TopLeft:
case Anchor.AutoLeft: case Anchor.AutoLeft:
case Anchor.AutoInline: case Anchor.AutoInline:
case Anchor.AutoInlineCenter:
case Anchor.AutoInlineBottom:
case Anchor.AutoInlineIgnoreOverflow: case Anchor.AutoInlineIgnoreOverflow:
case Anchor.AutoInlineCenterIgnoreOverflow:
case Anchor.AutoInlineBottomIgnoreOverflow:
pos.X = parentArea.X + this.ScaledOffset.X; pos.X = parentArea.X + this.ScaledOffset.X;
pos.Y = parentArea.Y + this.ScaledOffset.Y; pos.Y = parentArea.Y + this.ScaledOffset.Y;
break; break;
@ -714,36 +746,33 @@ namespace MLEM.Ui.Elements {
} }
if (this.Anchor.IsAuto()) { if (this.Anchor.IsAuto()) {
Element previousChild; if (this.Anchor.IsInline()) {
if (this.Anchor == Anchor.AutoInline || this.Anchor == Anchor.AutoInlineIgnoreOverflow) { var anchorEl = this.GetOlderSibling(e => !e.IsHidden && e.CanAutoAnchorsAttach);
previousChild = this.GetOlderSibling(e => !e.IsHidden && e.CanAutoAnchorsAttach); if (anchorEl != null) {
} else { var anchorElArea = anchorEl.GetAreaForAutoAnchors();
previousChild = this.GetLowestOlderSibling(e => !e.IsHidden && e.CanAutoAnchorsAttach); var newX = anchorElArea.Right + this.ScaledOffset.X;
} // with awkward ui scale values, floating point rounding can cause an element that would usually
if (previousChild != null) { // be positioned correctly to be pushed into the next line due to a very small deviation
var prevArea = previousChild.GetAreaForAutoAnchors(); if (this.Anchor.IsIgnoreOverflow() || newX + newSize.X <= parentArea.Right + Element.Epsilon) {
switch (this.Anchor) { pos.X = newX;
case Anchor.AutoLeft: pos.Y = anchorElArea.Y + this.ScaledOffset.Y;
case Anchor.AutoCenter: if (this.Anchor == Anchor.AutoInlineCenter || this.Anchor == Anchor.AutoInlineCenterIgnoreOverflow) {
case Anchor.AutoRight: pos.Y += (anchorElArea.Height - newSize.Y) / 2;
pos.Y = prevArea.Bottom + this.ScaledOffset.Y; } else if (this.Anchor == Anchor.AutoInlineBottom || this.Anchor == Anchor.AutoInlineBottomIgnoreOverflow) {
break; pos.Y += anchorElArea.Height - newSize.Y;
case Anchor.AutoInline:
var newX = prevArea.Right + this.ScaledOffset.X;
// with awkward ui scale values, floating point rounding can cause an element that would usually
// be positioned correctly to be pushed into the next line due to a very small deviation
if (newX + newSize.X <= parentArea.Right + Element.Epsilon) {
pos.X = newX;
pos.Y = prevArea.Y + this.ScaledOffset.Y;
} else {
pos.Y = prevArea.Bottom + this.ScaledOffset.Y;
} }
break; } else {
case Anchor.AutoInlineIgnoreOverflow: // inline anchors that overflow into the next line act like AutoLeft
pos.X = prevArea.Right + this.ScaledOffset.X; var newlineAnchorEl = this.GetLowestOlderSibling(e => !e.IsHidden && e.CanAutoAnchorsAttach);
pos.Y = prevArea.Y + this.ScaledOffset.Y; if (newlineAnchorEl != null)
break; pos.Y = newlineAnchorEl.GetAreaForAutoAnchors().Bottom + this.ScaledOffset.Y;
}
} }
} else {
// auto anchors keep their x coordinates from the switch above
var anchorEl = this.GetLowestOlderSibling(e => !e.IsHidden && e.CanAutoAnchorsAttach);
if (anchorEl != null)
pos.Y = anchorEl.GetAreaForAutoAnchors().Bottom + this.ScaledOffset.Y;
} }
} }
@ -801,8 +830,8 @@ 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 >= 64)
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} has recursively updated too often. Does its child {foundChild} contain any conflicting auto-sizing settings?");
UpdateDisplayArea(autoSize); UpdateDisplayArea(autoSize);
} }
} }
@ -852,14 +881,16 @@ namespace MLEM.Ui.Elements {
/// Returns this element's lowest child element (in terms of y position) that matches the given condition. /// Returns this element's lowest child element (in terms of y position) that matches the given condition.
/// </summary> /// </summary>
/// <param name="condition">The condition to match</param> /// <param name="condition">The condition to match</param>
/// <param name="total">Whether to evaluate based on the child's <see cref="GetTotalCoveredArea"/>, rather than its <see cref="UnscrolledArea"/>.</param>
/// <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, bool total = false) {
Element lowest = null; Element lowest = null;
var lowestX = float.MinValue; 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;
var x = !child.Anchor.IsTopAligned() ? child.UnscrolledArea.Height : child.UnscrolledArea.Bottom; var covered = total ? child.GetTotalCoveredArea(true) : child.UnscrolledArea;
var x = !child.Anchor.IsTopAligned() ? covered.Height : covered.Bottom;
if (x >= lowestX) { if (x >= lowestX) {
lowest = child; lowest = child;
lowestX = x; lowestX = x;
@ -872,14 +903,16 @@ namespace MLEM.Ui.Elements {
/// Returns this element's rightmost child (in terms of x position) that matches the given condition. /// Returns this element's rightmost child (in terms of x position) that matches the given condition.
/// </summary> /// </summary>
/// <param name="condition">The condition to match</param> /// <param name="condition">The condition to match</param>
/// <param name="total">Whether to evaluate based on the child's <see cref="GetTotalCoveredArea"/>, rather than its <see cref="UnscrolledArea"/>.</param>
/// <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, bool total = false) {
Element rightmost = null; Element rightmost = null;
var rightmostX = float.MinValue; 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;
var x = !child.Anchor.IsLeftAligned() ? child.UnscrolledArea.Width : child.UnscrolledArea.Right; var covered = total ? child.GetTotalCoveredArea(true) : child.UnscrolledArea;
var x = !child.Anchor.IsLeftAligned() ? covered.Width : covered.Right;
if (x >= rightmostX) { if (x >= rightmostX) {
rightmost = child; rightmost = child;
rightmostX = x; rightmostX = x;
@ -893,8 +926,9 @@ namespace MLEM.Ui.Elements {
/// The returned element's <see cref="Parent"/> will always be equal to this element's <see cref="Parent"/>. /// The returned element's <see cref="Parent"/> will always be equal to this element's <see cref="Parent"/>.
/// </summary> /// </summary>
/// <param name="condition">The condition to match</param> /// <param name="condition">The condition to match</param>
/// <param name="total">Whether to evaluate based on the child's <see cref="GetTotalCoveredArea"/>, rather than its <see cref="UnscrolledArea"/>.</param>
/// <returns>The lowest older sibling of this element, or null if no such element exists</returns> /// <returns>The lowest older sibling of this element, or null if no such element exists</returns>
public Element GetLowestOlderSibling(Func<Element, bool> condition = null) { public Element GetLowestOlderSibling(Func<Element, bool> condition = null, bool total = false) {
if (this.Parent == null) if (this.Parent == null)
return null; return null;
Element lowest = null; Element lowest = null;
@ -903,7 +937,7 @@ namespace MLEM.Ui.Elements {
break; break;
if (condition != null && !condition(child)) if (condition != null && !condition(child))
continue; continue;
if (lowest == null || child.UnscrolledArea.Bottom >= lowest.UnscrolledArea.Bottom) if (lowest == null || (total ? child.GetTotalCoveredArea(true) : child.UnscrolledArea).Bottom >= lowest.UnscrolledArea.Bottom)
lowest = child; lowest = child;
} }
return lowest; return lowest;
@ -985,6 +1019,21 @@ namespace MLEM.Ui.Elements {
yield return parent; yield return parent;
} }
/// <summary>
/// Returns the total covered area of this element, which is its <see cref="Area"/> (or <see cref="UnscrolledArea"/>), unioned with all of the total covered areas of its <see cref="Children"/>.
/// The returned area is only different from this element's <see cref="Area"/> (or <see cref="UnscrolledArea"/>) if it has any <see cref="Children"/> that are outside of this element's area, or are bigger than this element.
/// </summary>
/// <param name="unscrolled">Whether to use elements' <see cref="UnscrolledArea"/> (instead of their <see cref="Area"/>).</param>
/// <returns>This element's total covered area.</returns>
public RectangleF GetTotalCoveredArea(bool unscrolled) {
var ret = unscrolled ? this.UnscrolledArea : this.Area;
foreach (var child in this.Children) {
if (!child.IsHidden)
ret = RectangleF.Union(ret, child.GetTotalCoveredArea(unscrolled));
}
return ret;
}
/// <summary> /// <summary>
/// Returns a subset of <see cref="Children"/> that are currently relevant in terms of drawing and input querying. /// Returns a subset of <see cref="Children"/> that are currently relevant in terms of drawing and input querying.
/// A <see cref="Panel"/> only returns elements that are currently in view here. /// A <see cref="Panel"/> only returns elements that are currently in view here.
@ -1001,6 +1050,14 @@ namespace MLEM.Ui.Elements {
public virtual void Update(GameTime time) { public virtual void Update(GameTime time) {
this.System.InvokeOnElementUpdated(this, time); this.System.InvokeOnElementUpdated(this, time);
for (var i = this.PlayingAnimations.Count - 1; i >= 0; i--) {
var anim = this.PlayingAnimations[i];
if (anim.Update(this, time)) {
anim.OnFinished(this);
this.PlayingAnimations.RemoveAt(i);
}
}
// update all sorted children, not just relevant ones, because they might become relevant or irrelevant through updates // update all sorted children, not just relevant ones, because they might become relevant or irrelevant through updates
foreach (var child in this.SortedChildren) { foreach (var child in this.SortedChildren) {
if (child.System != null) if (child.System != null)
@ -1143,6 +1200,33 @@ namespace MLEM.Ui.Elements {
return this.CanBeMoused && this.DisplayArea.Contains(position) ? this : null; return this.CanBeMoused && this.DisplayArea.Contains(position) ? this : null;
} }
/// <summary>
/// Plays the given <see cref="UiAnimation"/> on this element, causing it to be added to the <see cref="PlayingAnimations"/> and updated in <see cref="Update"/>.
/// If the given <paramref name="animation"/> is already playing on this element, it will be restarted.
/// </summary>
/// <param name="animation">The animation to play.</param>
public virtual void PlayAnimation(UiAnimation animation) {
if (this.PlayingAnimations.Contains(animation)) {
// if we're already playing this animation, just restart it
animation.OnFinished(this);
} else {
this.PlayingAnimations.Add(animation);
}
}
/// <summary>
/// Stops the given <see cref="UiAnimation"/> on this element, causing it to be removed from the <see cref="PlayingAnimations"/> and <see cref="UiAnimation.OnFinished"/> to be invoked.
/// </summary>
/// <param name="animation">The animation to stop.</param>
/// <returns>Whether the animation was present in this element's <see cref="PlayingAnimations"/>.</returns>
public virtual bool StopAnimation(UiAnimation animation) {
if (this.PlayingAnimations.Remove(animation)) {
animation.OnFinished(this);
return true;
}
return false;
}
/// <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.")] [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() {
@ -1150,6 +1234,18 @@ namespace MLEM.Ui.Elements {
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
} }
/// <inheritdoc />
public override string ToString() {
var ret = this.GetType().ToString();
// elements will contain their path up to the root (Paragraph@Panel@...@RootName)
if (this.Parent != null) {
ret += $"@{this.Parent}";
} else if (this.Root?.Element == this) {
ret += $"@{this.Root.Name}";
}
return ret;
}
/// <summary> /// <summary>
/// Performs the specified action on this element and all of its <see cref="Children"/> /// Performs the specified action on this element and all of its <see cref="Children"/>
/// </summary> /// </summary>
@ -1194,8 +1290,11 @@ namespace MLEM.Ui.Elements {
this.SelectionIndicator = this.SelectionIndicator.OrStyle(style.SelectionIndicator); this.SelectionIndicator = this.SelectionIndicator.OrStyle(style.SelectionIndicator);
this.ActionSound = this.ActionSound.OrStyle(style.ActionSound); this.ActionSound = this.ActionSound.OrStyle(style.ActionSound);
this.SecondActionSound = this.SecondActionSound.OrStyle(style.ActionSound); this.SecondActionSound = this.SecondActionSound.OrStyle(style.ActionSound);
this.MouseEnterAnimation = this.MouseEnterAnimation.OrStyle(style.MouseEnterAnimation);
this.MouseExitAnimation = this.MouseExitAnimation.OrStyle(style.MouseExitAnimation);
this.System?.InvokeOnElementStyleInit(this); this.System?.InvokeOnElementStyleInit(this);
style.ApplyCustomStyle(this);
} }
/// <summary> /// <summary>

View file

@ -234,21 +234,39 @@ namespace MLEM.Ui.Elements {
} }
/// <summary> /// <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. /// Returns whether the given <see cref="Anchor"/> is automatic. The anchors <see cref="Anchor.AutoLeft"/>, <see cref="Anchor.AutoCenter"/>, <see cref="Anchor.AutoRight"/>, and any anchor that <see cref="IsInline"/> will return true.
/// </summary> /// </summary>
/// <param name="anchor">The anchor to query.</param> /// <param name="anchor">The anchor to query.</param>
/// <returns>Whether the given anchor is automatic.</returns> /// <returns>Whether the given anchor is automatic.</returns>
public static bool IsAuto(this Anchor anchor) { public static bool IsAuto(this Anchor anchor) {
return anchor == Anchor.AutoLeft || anchor == Anchor.AutoCenter || anchor == Anchor.AutoRight || anchor == Anchor.AutoInline || anchor == Anchor.AutoInlineIgnoreOverflow; return anchor == Anchor.AutoLeft || anchor == Anchor.AutoCenter || anchor == Anchor.AutoRight || anchor.IsInline();
} }
/// <summary> /// <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. /// Returns whether the given <see cref="Anchor"/> is inline. The anchors <see cref="Anchor.AutoInline"/>, <see cref="Anchor.AutoInlineCenter"/>, <see cref="Anchor.AutoInlineBottom"/>, and any anchor that <see cref="IsIgnoreOverflow"/> will return true.
/// </summary>
/// <param name="anchor">The anchor to query.</param>
/// <returns>Whether the given anchor is inline.</returns>
public static bool IsInline(this Anchor anchor) {
return anchor == Anchor.AutoInline || anchor == Anchor.AutoInlineCenter || anchor == Anchor.AutoInlineBottom || anchor.IsIgnoreOverflow();
}
/// <summary>
/// Returns whether the given <see cref="Anchor"/> ignores overflow. The anchors <see cref="Anchor.AutoInlineIgnoreOverflow"/>, <see cref="Anchor.AutoInlineCenterIgnoreOverflow"/>, and <see cref="Anchor.AutoInlineBottomIgnoreOverflow"/> will return true.
/// </summary>
/// <param name="anchor">The anchor to query.</param>
/// <returns>Whether the given anchor ignores overflow.</returns>
public static bool IsIgnoreOverflow(this Anchor anchor) {
return anchor == Anchor.AutoInlineIgnoreOverflow || anchor == Anchor.AutoInlineCenterIgnoreOverflow || anchor == Anchor.AutoInlineBottomIgnoreOverflow;
}
/// <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"/>, and any anchor that <see cref="IsInline"/> will return true.
/// </summary> /// </summary>
/// <param name="anchor">The anchor to query.</param> /// <param name="anchor">The anchor to query.</param>
/// <returns>Whether the given anchor is left-aligned.</returns> /// <returns>Whether the given anchor is left-aligned.</returns>
public static bool IsLeftAligned(this Anchor anchor) { 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; return anchor == Anchor.TopLeft || anchor == Anchor.CenterLeft || anchor == Anchor.BottomLeft || anchor == Anchor.AutoLeft || anchor.IsInline();
} }
/// <summary> /// <summary>

View file

@ -30,13 +30,12 @@ namespace MLEM.Ui.Elements {
/// </summary> /// </summary>
public TextureRegion Texture { public TextureRegion Texture {
get { get {
var ret = this.GetTextureCallback?.Invoke(this) ?? this.texture; this.CheckTextureChange();
this.CheckTextureChange(ret); return this.displayedTexture;
return ret;
} }
set { set {
this.texture = value; this.explicitlySetTexture = value;
this.CheckTextureChange(value); this.CheckTextureChange();
} }
} }
/// <summary> /// <summary>
@ -75,8 +74,8 @@ namespace MLEM.Ui.Elements {
public override bool IsHidden => base.IsHidden || this.Texture == null; public override bool IsHidden => base.IsHidden || this.Texture == null;
private bool scaleToImage; private bool scaleToImage;
private TextureRegion texture; private TextureRegion explicitlySetTexture;
private TextureRegion lastTexture; private TextureRegion displayedTexture;
/// <summary> /// <summary>
/// Creates a new image with the given settings /// Creates a new image with the given settings
@ -106,6 +105,12 @@ namespace MLEM.Ui.Elements {
return this.Texture != null && this.scaleToImage ? this.Texture.Size.ToVector2() * this.Scale : base.CalcActualSize(parentArea); return this.Texture != null && this.scaleToImage ? this.Texture.Size.ToVector2() * this.Scale : base.CalcActualSize(parentArea);
} }
/// <inheritdoc />
public override void Update(GameTime time) {
this.CheckTextureChange();
base.Update(time);
}
/// <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) {
if (this.Texture == null) if (this.Texture == null)
@ -123,11 +128,12 @@ namespace MLEM.Ui.Elements {
base.Draw(time, batch, alpha, context); base.Draw(time, batch, alpha, context);
} }
private void CheckTextureChange(TextureRegion newTexture) { private void CheckTextureChange() {
if (this.lastTexture == newTexture) var newTexture = this.GetTextureCallback?.Invoke(this) ?? this.explicitlySetTexture;
if (this.displayedTexture == newTexture)
return; return;
var nullChanged = this.lastTexture == null != (newTexture == null); var nullChanged = this.displayedTexture == null != (newTexture == null);
this.lastTexture = newTexture; this.displayedTexture = newTexture;
if (nullChanged || this.scaleToImage) if (nullChanged || this.scaleToImage)
this.SetAreaDirty(); this.SetAreaDirty();
} }

View file

@ -13,13 +13,13 @@ namespace MLEM.Ui.Elements {
/// <summary> /// <summary>
/// A panel element to be used inside of a <see cref="UiSystem"/>. /// A panel element to be used inside of a <see cref="UiSystem"/>.
/// The panel is a complex element that displays a box as a background to all of its child elements. /// The panel is a complex element that displays a box as a background to all of its child elements.
/// Additionally, a panel can be set to <see cref="scrollOverflow"/> on construction, which causes all elements that don't fit into the panel to be hidden until scrolled to using a <see cref="ScrollBar"/>. /// Additionally, a panel can be set to scroll overflowing elements on construction, which causes all elements that don't fit into the panel to be hidden until scrolled to using a <see cref="ScrollBar"/>.
/// </summary> /// </summary>
public class Panel : Element { public class Panel : Element {
/// <summary> /// <summary>
/// The scroll bar that this panel contains. /// The scroll bar that this panel contains.
/// This is only nonnull if <see cref="scrollOverflow"/> is true. /// This is only nonnull if scrolling overflow was enabled in the constructor.
/// Note that some scroll bar styling is controlled by this panel, namely <see cref="StepPerScroll"/> and <see cref="ScrollerSize"/>. /// Note that some scroll bar styling is controlled by this panel, namely <see cref="StepPerScroll"/> and <see cref="ScrollerSize"/>.
/// </summary> /// </summary>
public readonly ScrollBar ScrollBar; public readonly ScrollBar ScrollBar;
@ -133,6 +133,11 @@ namespace MLEM.Ui.Elements {
if (element == this.ScrollBar) if (element == this.ScrollBar)
throw new NotSupportedException("A panel that scrolls overflow cannot have its scroll bar removed from its list of children"); throw new NotSupportedException("A panel that scrolls overflow cannot have its scroll bar removed from its list of children");
base.RemoveChild(element); base.RemoveChild(element);
// when removing children, our scroll bar might have to be hidden
// if we don't do this before adding children again, they might incorrectly assume that the scroll bar will still be visible and adjust their size accordingly
if (this.System != null)
this.ScrollSetup();
} }
/// <inheritdoc /> /// <inheritdoc />
@ -207,6 +212,7 @@ namespace MLEM.Ui.Elements {
protected override void InitStyle(UiStyle style) { protected override void InitStyle(UiStyle style) {
base.InitStyle(style); base.InitStyle(style);
this.Texture = this.Texture.OrStyle(style.PanelTexture); this.Texture = this.Texture.OrStyle(style.PanelTexture);
this.DrawColor = this.DrawColor.OrStyle(style.PanelColor);
this.StepPerScroll = this.StepPerScroll.OrStyle(style.PanelStepPerScroll); this.StepPerScroll = this.StepPerScroll.OrStyle(style.PanelStepPerScroll);
this.ScrollerSize = this.ScrollerSize.OrStyle(style.PanelScrollerSize); this.ScrollerSize = this.ScrollerSize.OrStyle(style.PanelScrollerSize);
this.ScrollBarOffset = this.ScrollBarOffset.OrStyle(style.PanelScrollBarOffset); this.ScrollBarOffset = this.ScrollBarOffset.OrStyle(style.PanelScrollBarOffset);
@ -230,8 +236,12 @@ namespace MLEM.Ui.Elements {
base.OnChildAreaDirty(child, grandchild); base.OnChildAreaDirty(child, grandchild);
// we only need to scroll when a grandchild changes, since all of our children are forced // 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 // to be auto-anchored and so will automatically propagate their changes up to us
if (grandchild) if (grandchild) {
this.ScrollChildren(); this.ScrollChildren();
// we also need to re-setup here in case the child is involved in a special GetTotalCoveredArea
if (!this.AreaDirty)
this.ScrollSetup();
}
} }
/// <inheritdoc /> /// <inheritdoc />
@ -253,8 +263,8 @@ namespace MLEM.Ui.Elements {
float childrenHeight; float childrenHeight;
if (this.Children.Count > 1) { if (this.Children.Count > 1) {
var firstChild = this.Children.FirstOrDefault(c => c != this.ScrollBar); var firstChild = this.Children.FirstOrDefault(c => c != this.ScrollBar);
var lowestChild = this.GetLowestChild(c => c != this.ScrollBar && !c.IsHidden); var lowestChild = this.GetLowestChild(c => c != this.ScrollBar && !c.IsHidden, true);
childrenHeight = lowestChild.Area.Bottom - firstChild.Area.Top; childrenHeight = lowestChild.GetTotalCoveredArea(false).Bottom - firstChild.Area.Top;
} else { } else {
// if we only have one child (the scroll bar), then the children take up no visual height // if we only have one child (the scroll bar), then the children take up no visual height
childrenHeight = 0; childrenHeight = 0;

View file

@ -139,6 +139,16 @@ namespace MLEM.Ui.Elements {
this.SetTextDirty(); this.SetTextDirty();
} }
} }
/// <summary>
/// The inclusive index in this paragraph's <see cref="Text"/> to start drawing at.
/// This value is passed to <see cref="TokenizedString.Draw"/>.
/// </summary>
public int? DrawStartIndex;
/// <summary>
/// The exclusive index in this paragraph's <see cref="Text"/> to stop drawing at.
/// This value is passed to <see cref="TokenizedString.Draw"/>.
/// </summary>
public int? DrawEndIndex;
/// <inheritdoc /> /// <inheritdoc />
public override bool IsHidden => base.IsHidden || string.IsNullOrWhiteSpace(this.Text); public override bool IsHidden => base.IsHidden || string.IsNullOrWhiteSpace(this.Text);
@ -175,6 +185,13 @@ namespace MLEM.Ui.Elements {
this.CanBeMoused = false; this.CanBeMoused = false;
} }
/// <inheritdoc />
public override void SetAreaAndUpdateChildren(RectangleF area) {
base.SetAreaAndUpdateChildren(area);
// in case an outside source sets our area, we still want to display our text correctly
this.AlignAndSplitIfNecessary(area.Size);
}
/// <inheritdoc /> /// <inheritdoc />
protected override Vector2 CalcActualSize(RectangleF parentArea) { protected override Vector2 CalcActualSize(RectangleF parentArea) {
var size = base.CalcActualSize(parentArea); var size = base.CalcActualSize(parentArea);
@ -182,6 +199,9 @@ namespace MLEM.Ui.Elements {
this.TokenizeIfNecessary(); this.TokenizeIfNecessary();
this.AlignAndSplitIfNecessary(size); this.AlignAndSplitIfNecessary(size);
var textSize = this.tokenizedText.GetArea(Vector2.Zero, this.TextScale * this.TextScaleMultiplier * this.Scale).Size; var textSize = this.tokenizedText.GetArea(Vector2.Zero, this.TextScale * this.TextScaleMultiplier * this.Scale).Size;
// if we auto-adjust our width, then we would also split the same way with our adjusted width, so cache that
if (this.AutoAdjustWidth)
this.lastAlignSplitWidth = textSize.X;
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);
} }
@ -196,7 +216,7 @@ namespace MLEM.Ui.Elements {
var pos = this.DisplayArea.Location + new Vector2(this.GetAlignmentOffset(), 0); var pos = this.DisplayArea.Location + new Vector2(this.GetAlignmentOffset(), 0);
var sc = this.TextScale * this.TextScaleMultiplier * this.Scale; var sc = this.TextScale * this.TextScaleMultiplier * this.Scale;
var color = this.TextColor.OrDefault(Color.White) * alpha; var color = this.TextColor.OrDefault(Color.White) * alpha;
this.TokenizedText.Draw(time, batch, pos, this.RegularFont, color, sc, 0); this.TokenizedText.Draw(time, batch, pos, this.RegularFont, color, sc, 0, this.DrawStartIndex, this.DrawEndIndex);
base.Draw(time, batch, alpha, context); base.Draw(time, batch, alpha, context);
} }
@ -255,7 +275,7 @@ namespace MLEM.Ui.Elements {
var width = size.X - this.ScaledPadding.Width; var width = size.X - this.ScaledPadding.Width;
var scale = this.TextScale * this.TextScaleMultiplier * this.Scale; var scale = this.TextScale * this.TextScaleMultiplier * this.Scale;
if (this.lastAlignSplitWidth == width && this.lastAlignSplitScale == scale) if (this.lastAlignSplitWidth?.Equals(width, Element.Epsilon) == true && this.lastAlignSplitScale?.Equals(scale, Element.Epsilon) == true)
return; return;
this.lastAlignSplitWidth = width; this.lastAlignSplitWidth = width;
this.lastAlignSplitScale = scale; this.lastAlignSplitScale = scale;

View file

@ -10,7 +10,7 @@ using MLEM.Ui.Style;
namespace MLEM.Ui.Elements { namespace MLEM.Ui.Elements {
/// <summary> /// <summary>
/// A progress bar element to use inside of a <see cref="UiSystem"/>. /// A progress bar element to use inside of a <see cref="UiSystem"/>.
/// A progress bar is an element that fills up a bar based on a given <see cref="currentValue"/> percentage. /// A progress bar is an element that fills up a bar based on a given <see cref="CurrentValue"/> percentage.
/// </summary> /// </summary>
public class ProgressBar : Element { public class ProgressBar : Element {

View file

@ -29,7 +29,7 @@ namespace MLEM.Ui.Elements {
public override void Update(GameTime time) { public override void Update(GameTime time) {
base.Update(time); base.Update(time);
if (this.IsSelected) { if (this.IsSelectedActive) {
if (this.CurrentValue > 0 && this.Controls.LeftButtons.TryConsumePressed(this.Input, this.Controls.GamepadIndex)) { if (this.CurrentValue > 0 && this.Controls.LeftButtons.TryConsumePressed(this.Input, this.Controls.GamepadIndex)) {
this.CurrentValue -= this.StepPerScroll; this.CurrentValue -= this.StepPerScroll;
} else if (this.CurrentValue < this.MaxValue && this.Controls.RightButtons.TryConsumePressed(this.Input, this.Controls.GamepadIndex)) { } else if (this.CurrentValue < this.MaxValue && this.Controls.RightButtons.TryConsumePressed(this.Input, this.Controls.GamepadIndex)) {

View file

@ -1,13 +1,13 @@
using System; using System;
using System.Linq;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using MLEM.Extensions; using MLEM.Extensions;
using MLEM.Misc; using MLEM.Misc;
namespace MLEM.Ui.Elements { namespace MLEM.Ui.Elements {
/// <summary> /// <summary>
/// A squishing group is a <see cref="Group"/> whose <see cref="Element.Children"/> automatically get resized so that they do not overlap each other. /// A squishing group is a <see cref="Group"/> whose <see cref="Element.Children"/> automatically get resized so that they do not overlap each other. Elements are squished in a way that maximizes the final area that each element retains compared to its original area.
/// The order in which elements are squished depends on their <see cref="Element.Priority"/>, where elements with a lower priority will move out of the way of elements with a higher priority. /// The order in which elements are squished depends on their <see cref="Element.Priority"/>, where elements with a lower priority will move out of the way of elements with a higher priority. If all elements have the same priority, their addition order (their order in <see cref="Element.Children"/>) determines squish order.
/// If all elements have the same priority, their addition order (their order in <see cref="Element.Children"/>) determines squish order.
/// </summary> /// </summary>
public class SquishingGroup : Group { public class SquishingGroup : Group {
@ -43,27 +43,17 @@ namespace MLEM.Ui.Elements {
var pos = element.Area.Location; var pos = element.Area.Location;
var size = element.Area.Size; var size = element.Area.Size;
foreach (var sibling in element.GetSiblings(e => !e.IsHidden)) { foreach (var sibling in element.GetSiblings(e => !e.IsHidden)) {
var siblingArea = sibling.Area; var sibArea = sibling.Area;
var leftIntersect = siblingArea.Right - pos.X; if (pos.X < sibArea.Right && sibArea.Left < pos.X + size.X && pos.Y < sibArea.Bottom && sibArea.Top < pos.Y + size.Y) {
var rightIntersect = pos.X + size.X - siblingArea.Left; var possible = new[] {
var bottomIntersect = siblingArea.Bottom - pos.Y; new RectangleF(Math.Max(pos.X, sibArea.Right), pos.Y, size.X - (sibArea.Right - pos.X), size.Y),
var topIntersect = pos.Y + size.Y - siblingArea.Top; new RectangleF(pos.X, pos.Y, Math.Min(pos.X + size.X, sibArea.Left) - pos.X, size.Y),
if (leftIntersect > 0 && rightIntersect > 0 && bottomIntersect > 0 && topIntersect > 0) { new RectangleF(pos.X, Math.Max(pos.Y, sibArea.Bottom), size.X, size.Y - (sibArea.Bottom - pos.Y)),
if (rightIntersect + leftIntersect < topIntersect + bottomIntersect) { new RectangleF(pos.X, pos.Y, size.X, Math.Min(pos.Y + size.Y, sibArea.Top) - pos.Y)
if (rightIntersect > leftIntersect) { };
size.X -= siblingArea.Right - pos.X; var biggest = possible.OrderByDescending(r => r.Width * r.Height).First();
pos.X = Math.Max(pos.X, siblingArea.Right); pos = biggest.Location;
} else { size = biggest.Size;
size.X = Math.Min(pos.X + size.X, siblingArea.Left) - pos.X;
}
} else {
if (topIntersect > bottomIntersect) {
size.Y -= siblingArea.Bottom - pos.Y;
pos.Y = Math.Max(pos.Y, siblingArea.Bottom);
} else {
size.Y = Math.Min(pos.Y + size.Y, siblingArea.Top) - pos.Y;
}
}
} }
} }
if (!pos.Equals(element.Area.Location, Element.Epsilon) || !size.Equals(element.Area.Size, Element.Epsilon)) { if (!pos.Equals(element.Area.Location, Element.Epsilon) || !size.Equals(element.Area.Size, Element.Epsilon)) {

View file

@ -1,6 +1,7 @@
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.Font; using MLEM.Font;
using MLEM.Graphics; using MLEM.Graphics;
using MLEM.Input; using MLEM.Input;
@ -139,6 +140,12 @@ namespace MLEM.Ui.Elements {
/// The description of the <c>KeyboardInput</c> field on mobile devices and consoles /// The description of the <c>KeyboardInput</c> field on mobile devices and consoles
/// </summary> /// </summary>
public string MobileDescription; public string MobileDescription;
/// <summary>
/// An element that should be pressed (using <see cref="UiControls.PressElement"/>) if <see cref="Keys.Enter"/> is pressed while this text field is active.
/// Note that, for text fields that are <see cref="Multiline"/>, this is ignored.
/// This also occurs once the text input window is successfully closed on a mobile device.
/// </summary>
public Element EnterReceiver;
private readonly TextInput textInput; private readonly TextInput textInput;
private StyleProp<GenericFont> font; private StyleProp<GenericFont> font;
@ -188,12 +195,14 @@ namespace MLEM.Ui.Elements {
this.OnPressed += async e => { this.OnPressed += async e => {
var title = this.MobileTitle ?? this.PlaceholderText; var title = this.MobileTitle ?? this.PlaceholderText;
var result = await MlemPlatform.Current.OpenOnScreenKeyboard(title, this.MobileDescription, this.Text, false); var result = await MlemPlatform.Current.OpenOnScreenKeyboard(title, this.MobileDescription, this.Text, false);
if (result != null) if (result != null) {
this.SetText(this.Multiline ? result : result.Replace('\n', ' '), true); this.SetText(this.Multiline ? result : result.Replace('\n', ' '), true);
this.EnterReceiver?.Controls?.PressElement(this.EnterReceiver);
}
}; };
this.OnTextInput += (element, key, character) => { this.OnTextInput += (element, key, character) => {
if (this.IsSelected && !this.IsHidden) if (this.IsSelectedActive && !this.IsHidden && !this.textInput.OnTextInput(key, character) && key == Keys.Enter && !this.Multiline)
this.textInput.OnTextInput(key, character); this.EnterReceiver?.Controls?.PressElement(this.EnterReceiver);
}; };
this.OnDeselected += e => this.CaretPos = 0; this.OnDeselected += e => this.CaretPos = 0;
this.OnSelected += e => this.CaretPos = this.textInput.Length; this.OnSelected += e => this.CaretPos = this.textInput.Length;
@ -209,8 +218,14 @@ namespace MLEM.Ui.Elements {
/// <inheritdoc /> /// <inheritdoc />
public override void Update(GameTime time) { public override void Update(GameTime time) {
base.Update(time); base.Update(time);
if (this.IsSelected && !this.IsHidden) if (this.IsSelectedActive && !this.IsHidden) {
this.textInput.Update(time, this.Input); this.textInput.Update(time, this.Input);
#if FNA
// this occurs in OnTextInput outside FNA, where special keys are also counted as text input
if (this.EnterReceiver != null && !this.Multiline && this.Input.TryConsumePressed(Keys.Enter))
this.EnterReceiver.Controls?.PressElement(this.EnterReceiver);
#endif
}
} }
/// <inheritdoc /> /// <inheritdoc />

View file

@ -196,7 +196,7 @@ namespace MLEM.Ui.Elements {
} }
/// <summary> /// <summary>
/// Causes this tooltip's position to be snapped to the mouse position, or the <see cref="snapElement"/> if <see cref="DisplayInAutoNavMode"/> is true, or the <see cref="SnapPosition"/> if set. /// Causes this tooltip's position to be snapped to the mouse position, or the element to snap to if <see cref="DisplayInAutoNavMode"/> is true, or the <see cref="SnapPosition"/> if set.
/// </summary> /// </summary>
public void SnapPositionToMouse() { public void SnapPositionToMouse() {
Vector2 snapPosition; Vector2 snapPosition;

View file

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>net452;netstandard2.0;net6.0</TargetFrameworks> <TargetFrameworks>net452;netstandard2.0;net7.0</TargetFrameworks>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<ProduceReferenceAssembly>true</ProduceReferenceAssembly> <ProduceReferenceAssembly>true</ProduceReferenceAssembly>
<IsTrimmable>true</IsTrimmable> <IsTrimmable>true</IsTrimmable>
@ -31,6 +31,6 @@
<ItemGroup> <ItemGroup>
<None Include="../Media/Logo.png" Pack="true" PackagePath="" /> <None Include="../Media/Logo.png" Pack="true" PackagePath="" />
<None Include="../Docs/index.md" Pack="true" PackagePath="README.md" /> <None Include="../README.md" Pack="true" PackagePath="" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>net452;netstandard2.0;net6.0</TargetFrameworks> <TargetFrameworks>net452;netstandard2.0;net7.0</TargetFrameworks>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<ProduceReferenceAssembly>true</ProduceReferenceAssembly> <ProduceReferenceAssembly>true</ProduceReferenceAssembly>
<IsTrimmable>true</IsTrimmable> <IsTrimmable>true</IsTrimmable>
@ -29,6 +29,6 @@
<ItemGroup> <ItemGroup>
<None Include="../Media/Logo.png" Pack="true" PackagePath="" /> <None Include="../Media/Logo.png" Pack="true" PackagePath="" />
<None Include="../Docs/index.md" Pack="true" PackagePath="README.md" /> <None Include="../README.md" Pack="true" PackagePath="" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -109,11 +109,11 @@ namespace MLEM.Ui.Parsers {
/// <summary> /// <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"/>. /// 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. /// These actions can be used to modify the style properties of the created elements similarly to <see cref="UiStyle.AddCustomStyle{T}"/>.
/// </summary> /// </summary>
/// <param name="types">The element types that should be styled. Can be a combined flag.</param> /// <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="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> /// <param name="add">Whether the <paramref name="style"/> function should be added to the existing style settings rather than replacing them.</param>
/// <typeparam name="T">The type of elements that the given <see cref="ElementType"/> flags are expected to be.</typeparam> /// <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> /// <returns>This parser, for chaining.</returns>
public UiParser Style<T>(ElementType types, Action<T> style, bool add = false) where T : Element { public UiParser Style<T>(ElementType types, Action<T> style, bool add = false) where T : Element {

View file

@ -13,7 +13,7 @@ namespace MLEM.Ui.Style {
/// <summary> /// <summary>
/// The style settings for a <see cref="UiSystem"/>. /// The style settings for a <see cref="UiSystem"/>.
/// Each <see cref="Element"/> uses these style settings by default, however you can also change these settings per element using the elements' individual style settings. /// Each <see cref="Element"/> uses these style settings by default, however you can also change these settings per element using the elements' individual style settings.
/// Note that this class is a <see cref="GenericDataHolder"/>, meaning additional styles for custom components can easily be added using <see cref="GenericDataHolder.SetData"/> /// Additional styles for built-in or custom element types can easily be added using <see cref="AddCustomStyle{T}"/>.
/// </summary> /// </summary>
public class UiStyle : GenericDataHolder { public class UiStyle : GenericDataHolder {
@ -22,6 +22,14 @@ namespace MLEM.Ui.Style {
/// </summary> /// </summary>
public NinePatch SelectionIndicator; public NinePatch SelectionIndicator;
/// <summary> /// <summary>
/// A <see cref="UiAnimation"/> that is played when the mouse enters an element.
/// </summary>
public UiAnimation MouseEnterAnimation;
/// <summary>
/// A <see cref="UiAnimation"/> that is played when the mouse exists an element.
/// </summary>
public UiAnimation MouseExitAnimation;
/// <summary>
/// The texture that the <see cref="Button"/> element uses /// The texture that the <see cref="Button"/> element uses
/// </summary> /// </summary>
public NinePatch ButtonTexture; public NinePatch ButtonTexture;
@ -47,6 +55,10 @@ namespace MLEM.Ui.Style {
/// </summary> /// </summary>
public NinePatch PanelTexture; public NinePatch PanelTexture;
/// <summary> /// <summary>
/// The color that the <see cref="Panel"/> element draws with.
/// </summary>
public Color PanelColor = Color.White;
/// <summary>
/// The <see cref="Element.ChildPadding"/> to apply to a <see cref="Panel"/> by default /// The <see cref="Element.ChildPadding"/> to apply to a <see cref="Panel"/> by default
/// </summary> /// </summary>
public Padding PanelChildPadding = new Vector2(5); public Padding PanelChildPadding = new Vector2(5);
@ -222,5 +234,106 @@ namespace MLEM.Ui.Style {
/// </summary> /// </summary>
public Dictionary<string, GenericFont> AdditionalFonts = new Dictionary<string, GenericFont>(); public Dictionary<string, GenericFont> AdditionalFonts = new Dictionary<string, GenericFont>();
private readonly Dictionary<Type, Action<Element>> elementStyles = new Dictionary<Type, Action<Element>>();
/// <summary>
/// Creates a new set of style settings with the default values.
/// </summary>
public UiStyle() {}
/// <summary>
/// Creates a new set of style settings with values inherited from the given <paramref name="original"/> style settings.
/// </summary>
/// <param name="original">The original style settings, to copy into the new instance.</param>
public UiStyle(UiStyle original) {
this.SelectionIndicator = original.SelectionIndicator;
this.MouseEnterAnimation = original.MouseEnterAnimation;
this.MouseExitAnimation = original.MouseExitAnimation;
this.ButtonTexture = original.ButtonTexture;
this.ButtonHoveredTexture = original.ButtonHoveredTexture;
this.ButtonHoveredColor = original.ButtonHoveredColor;
this.ButtonDisabledTexture = original.ButtonDisabledTexture;
this.ButtonDisabledColor = original.ButtonDisabledColor;
this.PanelTexture = original.PanelTexture;
this.PanelColor = original.PanelColor;
this.PanelChildPadding = original.PanelChildPadding;
this.PanelStepPerScroll = original.PanelStepPerScroll;
this.PanelScrollerSize = original.PanelScrollerSize;
this.PanelScrollBarOffset = original.PanelScrollBarOffset;
this.TextFieldTexture = original.TextFieldTexture;
this.TextFieldHoveredTexture = original.TextFieldHoveredTexture;
this.TextFieldHoveredColor = original.TextFieldHoveredColor;
this.TextFieldTextOffsetX = original.TextFieldTextOffsetX;
this.TextFieldCaretWidth = original.TextFieldCaretWidth;
this.ScrollBarBackground = original.ScrollBarBackground;
this.ScrollBarScrollerTexture = original.ScrollBarScrollerTexture;
this.ScrollBarSmoothScrolling = original.ScrollBarSmoothScrolling;
this.ScrollBarSmoothScrollFactor = original.ScrollBarSmoothScrollFactor;
this.CheckboxTexture = original.CheckboxTexture;
this.CheckboxHoveredTexture = original.CheckboxHoveredTexture;
this.CheckboxHoveredColor = original.CheckboxHoveredColor;
this.CheckboxDisabledTexture = original.CheckboxDisabledTexture;
this.CheckboxDisabledColor = original.CheckboxDisabledColor;
this.CheckboxCheckmark = original.CheckboxCheckmark;
this.CheckboxTextOffsetX = original.CheckboxTextOffsetX;
this.RadioTexture = original.RadioTexture;
this.RadioHoveredTexture = original.RadioHoveredTexture;
this.RadioHoveredColor = original.RadioHoveredColor;
this.RadioCheckmark = original.RadioCheckmark;
this.TooltipBackground = original.TooltipBackground;
this.TooltipOffset = original.TooltipOffset;
this.TooltipAutoNavOffset = original.TooltipAutoNavOffset;
this.TooltipTextColor = original.TooltipTextColor;
this.TooltipDelay = original.TooltipDelay;
this.TooltipTextWidth = original.TooltipTextWidth;
this.TooltipChildPadding = original.TooltipChildPadding;
this.ProgressBarTexture = original.ProgressBarTexture;
this.ProgressBarColor = original.ProgressBarColor;
this.ProgressBarProgressPadding = original.ProgressBarProgressPadding;
this.ProgressBarProgressTexture = original.ProgressBarProgressTexture;
this.ProgressBarProgressColor = original.ProgressBarProgressColor;
this.Font = original.Font;
this.TextScale = original.TextScale;
this.TextColor = original.TextColor;
this.TextAlignment = original.TextAlignment;
this.ActionSound = original.ActionSound;
this.LinkColor = original.LinkColor;
this.AdditionalFonts = new Dictionary<string, GenericFont>(original.AdditionalFonts);
this.elementStyles = new Dictionary<Type, Action<Element>>(original.elementStyles);
}
/// <summary>
/// Adds an action to the given <see cref="Element"/> type <typeparamref name="T"/> that allows applying any kind of custom styling or behavior to it.
/// Custom styles added in this manner can be applied to an element using <see cref="ApplyCustomStyle"/>.
/// </summary>
/// <param name="style">The style action to add.</param>
/// <param name="add">Whether the <paramref name="style"/> function should be added to the existing style settings rather than replacing them.</param>
/// <typeparam name="T">The <see cref="Element"/> type that the <paramref name="style"/> should apply to.</typeparam>
public void AddCustomStyle<T>(Action<T> style, bool add = false) where T : Element {
if (add && this.elementStyles.ContainsKey(typeof(T))) {
this.elementStyles[typeof(T)] += Action;
} else {
this.elementStyles[typeof(T)] = Action;
}
void Action(Element e) {
style.Invoke((T) e);
}
}
/// <summary>
/// Applies a set of custom styling actions to the given <paramref name="element"/> which were added through <see cref="AddCustomStyle{T}"/>.
/// This method is automatically invoked in <see cref="Element.InitStyle"/>.
/// </summary>
/// <param name="element">The element to apply custom styling to.</param>
/// <returns>Whether any custom styling exists for the given <paramref name="element"/>.</returns>
public bool ApplyCustomStyle(Element element) {
if (this.elementStyles.TryGetValue(element.GetType(), out var style)) {
style?.Invoke(element);
return true;
}
return false;
}
} }
} }

86
MLEM.Ui/UiAnimation.cs Normal file
View file

@ -0,0 +1,86 @@
using System;
using Microsoft.Xna.Framework;
using MLEM.Misc;
using MLEM.Ui.Elements;
namespace MLEM.Ui {
/// <summary>
/// A ui animation is a simple timed event that an <see cref="Element"/> in a <see cref="UiSystem"/> can use to play a visual or other type of animation.
/// To use ui animations, you can use <see cref="Element.PlayAnimation"/>, or one of the built-in style properties like <see cref="Element.MouseEnterAnimation"/> or <see cref="Element.MouseExitAnimation"/>.
/// </summary>
public class UiAnimation : GenericDataHolder {
/// <summary>
/// The total time that this ui animation plays for.
/// </summary>
public readonly TimeSpan TotalTime;
/// <summary>
/// The <see cref="AnimationFunction"/> that is invoked every <see cref="Update"/>.
/// </summary>
public readonly AnimationFunction Function;
/// <summary>
/// An event that is raised when this ui animation is (re)started in <see cref="Update"/>.
/// </summary>
public Action<UiAnimation, Element> Started;
/// <summary>
/// An event that is raised when this ui animation is stopped or finished through <see cref="OnFinished"/>.
/// </summary>
public Action<UiAnimation, Element> Finished;
/// <summary>
/// The current time that this ui animation has been playing for, out of the <see cref="TotalTime"/>.
/// </summary>
public TimeSpan CurrentTime { get; private set; }
/// <summary>
/// Creates a new ui animation with the given settings.
/// </summary>
/// <param name="seconds">The amount of seconds that this ui animation should play for.</param>
/// <param name="function">The <see cref="AnimationFunction"/> that is invoked every <see cref="Update"/>.</param>
public UiAnimation(double seconds, AnimationFunction function) : this(TimeSpan.FromSeconds(seconds), function) {}
/// <summary>
/// Creates a new ui animation with the given settings.
/// </summary>
/// <param name="totalTime">The <see cref="TotalTime"/> that this ui animation should play for.</param>
/// <param name="function">The <see cref="AnimationFunction"/> that is invoked every <see cref="Update"/>.</param>
public UiAnimation(TimeSpan totalTime, AnimationFunction function) {
this.TotalTime = totalTime;
this.Function = function;
}
/// <summary>
/// Updates this ui animation, invoking its <see cref="Started"/> event if necessary, increasing its <see cref="CurrentTime"/> and invoking its <see cref="Function"/>.
/// This method is called by an <see cref="Element"/> in <see cref="Element.Update"/>.
/// </summary>
/// <param name="element">The element that this ui animation is attached to.</param>
/// <param name="time">The game's current time.</param>
/// <returns>Whether this animation is ready to finish, that is, if its <see cref="CurrentTime"/> is greater than or equal to its <see cref="TotalTime"/>.</returns>
public virtual bool Update(Element element, GameTime time) {
if (this.CurrentTime <= TimeSpan.Zero)
this.Started?.Invoke(this, element);
this.CurrentTime += time.ElapsedGameTime;
this.Function?.Invoke(this, element, this.CurrentTime.Ticks / (float) this.TotalTime.Ticks);
return this.CurrentTime >= this.TotalTime;
}
/// <summary>
/// Causes this ui animation's <see cref="Finished"/> event to be raised, and sets the <see cref="CurrentTime"/> to <see cref="TimeSpan.Zero"/>.
/// This allows the animation to play from the start again.
/// This method is invoked automatically when <see cref="Update"/> returns <see langword="true"/> in <see cref="Element.Update"/>, as well as in <see cref="Element.StopAnimation"/>.
/// </summary>
/// <param name="element"></param>
public virtual void OnFinished(Element element) {
this.Finished?.Invoke(this, element);
this.CurrentTime = TimeSpan.Zero;
}
/// <summary>
/// A delegate method used by <see cref="UiAnimation.Function"/>.
/// </summary>
public delegate void AnimationFunction(UiAnimation animation, Element element, float timePercentage);
}
}

View file

@ -170,13 +170,13 @@ namespace MLEM.Ui {
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.PressElement(mousedNow);
this.Input.TryConsumePressed(MouseButton.Left); this.Input.TryConsumePressed(MouseButton.Left);
} }
} else if (this.Input.IsPressedAvailable(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.PressElement(mousedNow, true);
this.Input.TryConsumePressed(MouseButton.Right); this.Input.TryConsumePressed(MouseButton.Right);
} }
} }
@ -187,13 +187,8 @@ namespace MLEM.Ui {
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) {
if (this.Input.IsModifierKeyDown(ModifierKey.Shift)) { // primary or secondary action on element using space or enter
// secondary action on element using space or enter this.PressElement(this.SelectedElement, this.Input.IsModifierKeyDown(ModifierKey.Shift));
this.System.InvokeOnElementSecondaryPressed(this.SelectedElement);
} else {
// first action on element using space or enter
this.System.InvokeOnElementPressed(this.SelectedElement);
}
this.KeyboardButtons.TryConsumePressed(this.Input, this.GamepadIndex); this.KeyboardButtons.TryConsumePressed(this.Input, this.GamepadIndex);
} }
} else if (this.Input.IsPressedAvailable(Keys.Tab)) { } else if (this.Input.IsPressedAvailable(Keys.Tab)) {
@ -216,13 +211,13 @@ namespace MLEM.Ui {
var tapped = this.GetElementUnderPos(tap.Position); var tapped = this.GetElementUnderPos(tap.Position);
this.SelectElement(this.ActiveRoot, tapped); this.SelectElement(this.ActiveRoot, tapped);
if (tapped != null && tapped.CanBePressed) if (tapped != null && tapped.CanBePressed)
this.System.InvokeOnElementPressed(tapped); this.PressElement(tapped);
} else if (this.Input.GetViewportGesture(GestureType.Hold, out var hold)) { } else if (this.Input.GetViewportGesture(GestureType.Hold, out var hold)) {
this.IsAutoNavMode = false; this.IsAutoNavMode = false;
var held = this.GetElementUnderPos(hold.Position); var held = this.GetElementUnderPos(hold.Position);
this.SelectElement(this.ActiveRoot, held); this.SelectElement(this.ActiveRoot, held);
if (held != null && held.CanBePressed) if (held != null && held.CanBePressed)
this.System.InvokeOnElementSecondaryPressed(held); this.PressElement(held, true);
} else if (this.Input.ViewportTouchState.Count <= 0) { } else if (this.Input.ViewportTouchState.Count <= 0) {
this.SetTouchedElement(null); this.SetTouchedElement(null);
} else { } else {
@ -244,12 +239,12 @@ namespace MLEM.Ui {
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) {
this.System.InvokeOnElementPressed(this.SelectedElement); this.PressElement(this.SelectedElement);
this.GamepadButtons.TryConsumePressed(this.Input, this.GamepadIndex); this.GamepadButtons.TryConsumePressed(this.Input, this.GamepadIndex);
} }
} else if (this.SecondaryGamepadButtons.IsPressedAvailable(this.Input, this.GamepadIndex)) { } else if (this.SecondaryGamepadButtons.IsPressedAvailable(this.Input, this.GamepadIndex)) {
if (this.SelectedElement?.Root != null && this.SelectedElement.CanBePressed) { if (this.SelectedElement?.Root != null && this.SelectedElement.CanBePressed) {
this.System.InvokeOnElementSecondaryPressed(this.SelectedElement); this.PressElement(this.SelectedElement, true);
this.SecondaryGamepadButtons.TryConsumePressed(this.Input, this.GamepadIndex); this.SecondaryGamepadButtons.TryConsumePressed(this.Input, this.GamepadIndex);
} }
} else if (this.DownButtons.IsPressedAvailable(this.Input, this.GamepadIndex)) { } else if (this.DownButtons.IsPressedAvailable(this.Input, this.GamepadIndex)) {
@ -362,6 +357,19 @@ namespace MLEM.Ui {
return element; return element;
} }
/// <summary>
/// Causes the passed element to be pressed, invoking its <see cref="Element.OnPressed"/> or <see cref="Element.OnSecondaryPressed"/> through <see cref="UiSystem.OnElementPressed"/> or <see cref="UiSystem.OnElementSecondaryPressed"/>.
/// </summary>
/// <param name="element">The element to press.</param>
/// <param name="secondary">Whether the secondary action should be invoked, rather than the primary one.</param>
public void PressElement(Element element, bool secondary = false) {
if (secondary) {
this.System.InvokeOnElementSecondaryPressed(element);
} else {
this.System.InvokeOnElementPressed(element);
}
}
/// <summary> /// <summary>
/// Returns the next element to select when pressing the <see cref="Keys.Tab"/> key during keyboard navigation. /// Returns the next element to select when pressing the <see cref="Keys.Tab"/> key during keyboard navigation.
/// If the <c>backward</c> boolean is true, the previous element should be returned instead. /// If the <c>backward</c> boolean is true, the previous element should be returned instead.

View file

@ -419,6 +419,14 @@ namespace MLEM.Ui {
root.Element.AndChildren(action); root.Element.AndChildren(action);
} }
/// <inheritdoc />
protected override void Dispose(bool disposing) {
if (disposing) {
while (this.rootElements.Count > 0)
this.Remove(this.rootElements[0].Name);
}
}
internal void InvokeOnElementDrawn(Element element, GameTime time, SpriteBatch batch, float alpha) { internal void InvokeOnElementDrawn(Element element, GameTime time, SpriteBatch batch, float alpha) {
this.OnElementDrawn?.Invoke(element, time, batch, alpha); this.OnElementDrawn?.Invoke(element, time, batch, alpha);
} }

View file

@ -13,7 +13,7 @@ namespace MLEM.Cameras {
/// <summary> /// <summary>
/// This field holds an epsilon value used in some camera calculations to mitigate floating point rounding inaccuracies. /// This field holds an epsilon value used in some camera calculations to mitigate floating point rounding inaccuracies.
/// If camera <see cref="Position"/> or <see cref="Viewport"/> size are extremely small or extremely big, this value can be reduced or increased. /// If camera <see cref="Position"/> or <see cref="ScaledViewport"/> size are extremely small or extremely big, this value can be reduced or increased.
/// </summary> /// </summary>
public static float Epsilon = 0.01F; public static float Epsilon = 0.01F;

View file

@ -36,7 +36,7 @@ namespace MLEM.Extensions {
var currWeight = 0; var currWeight = 0;
foreach (var entry in entries) { foreach (var entry in entries) {
currWeight += weightFunc(entry); currWeight += weightFunc(entry);
if (currWeight >= goalWeight) if (currWeight > goalWeight)
return entry; return entry;
} }
throw new IndexOutOfRangeException(); throw new IndexOutOfRangeException();
@ -49,7 +49,7 @@ namespace MLEM.Extensions {
var currWeight = 0F; var currWeight = 0F;
foreach (var entry in entries) { foreach (var entry in entries) {
currWeight += weightFunc(entry); currWeight += weightFunc(entry);
if (currWeight >= goalWeight) if (currWeight > goalWeight)
return entry; return entry;
} }
throw new IndexOutOfRangeException(); throw new IndexOutOfRangeException();

View file

@ -0,0 +1,34 @@
using System;
using System.Text.RegularExpressions;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using MLEM.Extensions;
using MLEM.Font;
using MLEM.Misc;
namespace MLEM.Formatting.Codes {
/// <inheritdoc />
public class OutlineCode : Code {
private readonly Color color;
private readonly float thickness;
private readonly bool diagonals;
/// <inheritdoc />
public OutlineCode(Match match, Regex regex, Color color, float thickness, bool diagonals) : base(match, regex) {
this.color = color;
this.thickness = thickness;
this.diagonals = diagonals;
}
/// <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) {
foreach (var dir in this.diagonals ? Direction2Helper.AllExceptNone : Direction2Helper.Adjacent) {
var offset = Vector2.Normalize(dir.Offset().ToVector2()) * (this.thickness * scale);
font.DrawString(batch, character, pos + offset, this.color.CopyAlpha(color), 0, Vector2.Zero, scale, SpriteEffects.None, depth);
}
return false;
}
}
}

View file

@ -28,6 +28,67 @@ namespace MLEM.Formatting {
/// </summary> /// </summary>
public readonly Dictionary<Regex, Macro> Macros = new Dictionary<Regex, Macro>(); public readonly Dictionary<Regex, Macro> Macros = new Dictionary<Regex, Macro>();
/// <summary>
/// The line thickness used by this text formatter, which determines how the default <see cref="UnderlineCode"/>-based formatting codes are drawn.
/// Note that this value only has an effect on the default formatting codes created through the <see cref="TextFormatter(bool, bool, bool, bool)"/> constructor.
/// </summary>
public float LineThickness = 1 / 16F;
/// <summary>
/// The underline offset used by this text formatter, which determines how the default <see cref="UnderlineCode"/> is drawn.
/// Note that this value only has an effect on the default formatting codes created through the <see cref="TextFormatter(bool, bool, bool, bool)"/> constructor.
/// </summary>
public float UnderlineOffset = 0.85F;
/// <summary>
/// The strikethrough offset used by this text formatter, which determines how the default <see cref="UnderlineCode"/>'s strikethrough variant is drawn.
/// Note that this value only has an effect on the default formatting codes created through the <see cref="TextFormatter(bool, bool, bool, bool)"/> constructor.
/// </summary>
public float StrikethroughOffset = 0.55F;
/// <summary>
/// The default subscript offset used by this text formatter, which determines how the default <see cref="SubSupCode"/> is drawn if no custom value is used.
/// Note that this value only has an effect on the default formatting codes created through the <see cref="TextFormatter(bool, bool, bool, bool)"/> constructor.
/// </summary>
public float DefaultSubOffset = 0.15F;
/// <summary>
/// The default superscript offset used by this text formatter, which determines how the default <see cref="SubSupCode"/> is drawn if no custom value is used.
/// Note that this value only has an effect on the default formatting codes created through the <see cref="TextFormatter(bool, bool, bool, bool)"/> constructor.
/// </summary>
public float DefaultSupOffset = -0.25F;
/// <summary>
/// The default shadow color used by this text formatter, which determines how the default <see cref="ShadowCode"/> is drawn if no custom value is used.
/// Note that this value only has an effect on the default formatting codes created through the <see cref="TextFormatter(bool, bool, bool, bool)"/> constructor.
/// </summary>
public Color DefaultShadowColor = Color.Black;
/// <summary>
/// The default shadow offset used by this text formatter, which determines how the default <see cref="ShadowCode"/> is drawn if no custom value is used.
/// Note that this value only has an effect on the default formatting codes created through the <see cref="TextFormatter(bool, bool, bool, bool)"/> constructor.
/// </summary>
public Vector2 DefaultShadowOffset = new Vector2(2);
/// <summary>
/// The default wobbly modifier used by this text formatter, which determines how the default <see cref="WobblyCode"/> is drawn if no custom value is used.
/// Note that this value only has an effect on the default formatting codes created through the <see cref="TextFormatter(bool, bool, bool, bool)"/> constructor.
/// </summary>
public float DefaultWobblyModifier = 5;
/// <summary>
/// The default wobbly modifier used by this text formatter, which determines how the default <see cref="WobblyCode"/> is drawn if no custom value is used.
/// Note that this value only has an effect on the default formatting codes created through the <see cref="TextFormatter(bool, bool, bool, bool)"/> constructor.
/// </summary>
public float DefaultWobblyHeight = 1 / 8F;
/// <summary>
/// The default outline thickness used by this text formatter, which determines how the default <see cref="OutlineCode"/> is drawn if no custom value is used.
/// Note that this value only has an effect on the default formatting codes created through the <see cref="TextFormatter(bool, bool, bool, bool)"/> constructor.
/// </summary>
public float DefaultOutlineThickness = 2;
/// <summary>
/// The default outline color used by this text formatter, which determines how the default <see cref="OutlineCode"/> is drawn if no custom value is used.
/// Note that this value only has an effect on the default formatting codes created through the <see cref="TextFormatter(bool, bool, bool, bool)"/> constructor.
/// </summary>
public Color DefaultOutlineColor = Color.Black;
/// <summary>
/// Whether the default outline used by this text formatter should also draw outlines diagonally, which determines how the default <see cref="OutlineCode"/> is drawn if no custom value is used. Non-diagonally drawn outlines might generally look better when using a pixelart font.
/// Note that this value only has an effect on the default formatting codes created through the <see cref="TextFormatter(bool, bool, bool, bool)"/> constructor.
/// </summary>
public bool OutlineDiagonals = true;
/// <summary> /// <summary>
/// Creates a new text formatter with an optional set of default formatting codes. /// Creates a new text formatter with an optional set of default formatting codes.
/// </summary> /// </summary>
@ -41,14 +102,18 @@ namespace MLEM.Formatting {
this.Codes.Add(new Regex("<b>"), (f, m, r) => new FontCode(m, r, fnt => fnt.Bold)); this.Codes.Add(new Regex("<b>"), (f, m, r) => new FontCode(m, r, fnt => fnt.Bold));
this.Codes.Add(new Regex("<i>"), (f, m, r) => new FontCode(m, r, fnt => fnt.Italic)); this.Codes.Add(new Regex("<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, 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, m.Groups[1].Success ? ColorHelper.FromHexString(m.Groups[1].Value) : this.DefaultShadowColor,
new Vector2(float.TryParse(m.Groups[2].Value, NumberStyles.Number, CultureInfo.InvariantCulture, out var offset) ? offset : 2))); float.TryParse(m.Groups[2].Value, NumberStyles.Number, CultureInfo.InvariantCulture, out var offset) ? new Vector2(offset) : this.DefaultShadowOffset));
this.Codes.Add(new Regex("<u>"), (f, m, r) => new UnderlineCode(m, r, 1 / 16F, 0.85F)); this.Codes.Add(new Regex("<u>"), (f, m, r) => new UnderlineCode(m, r, this.LineThickness, this.UnderlineOffset));
this.Codes.Add(new Regex("<st>"), (f, m, r) => new UnderlineCode(m, r, 1 / 16F, 0.55F)); this.Codes.Add(new Regex("<st>"), (f, m, r) => new UnderlineCode(m, r, this.LineThickness, this.StrikethroughOffset));
this.Codes.Add(new Regex(@"<sub(?: ([+-.0-9]+))?>"), (f, m, r) => new SubSupCode(m, r, 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)); float.TryParse(m.Groups[1].Value, NumberStyles.Number, CultureInfo.InvariantCulture, out var off) ? off : this.DefaultSubOffset));
this.Codes.Add(new Regex(@"<sup(?: ([+-.0-9]+))?>"), (f, m, r) => new SubSupCode(m, r, 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)); float.TryParse(m.Groups[1].Value, NumberStyles.Number, CultureInfo.InvariantCulture, out var off) ? -off : this.DefaultSupOffset));
this.Codes.Add(new Regex(@"<o(?: #([0-9\w]{6,8}) (([+-.0-9]*)))?>"), (f, m, r) => new OutlineCode(m, r,
m.Groups[1].Success ? ColorHelper.FromHexString(m.Groups[1].Value) : this.DefaultOutlineColor,
float.TryParse(m.Groups[2].Value, NumberStyles.Number, CultureInfo.InvariantCulture, out var thickness) ? thickness : this.DefaultOutlineThickness,
this.OutlineDiagonals));
} }
// color codes // color codes
@ -65,8 +130,8 @@ namespace MLEM.Formatting {
// animation codes // animation codes
if (hasAnimations) { if (hasAnimations) {
this.Codes.Add(new Regex(@"<a wobbly(?: ([+-.0-9]*) ([+-.0-9]*))?>"), (f, m, r) => new WobblyCode(m, r, this.Codes.Add(new Regex(@"<a wobbly(?: ([+-.0-9]*) ([+-.0-9]*))?>"), (f, m, r) => new WobblyCode(m, r,
float.TryParse(m.Groups[1].Value, NumberStyles.Number, CultureInfo.InvariantCulture, out var mod) ? mod : 5, float.TryParse(m.Groups[1].Value, NumberStyles.Number, CultureInfo.InvariantCulture, out var mod) ? mod : this.DefaultWobblyModifier,
float.TryParse(m.Groups[2].Value, NumberStyles.Number, CultureInfo.InvariantCulture, out var heightMod) ? heightMod : 1 / 8F)); float.TryParse(m.Groups[2].Value, NumberStyles.Number, CultureInfo.InvariantCulture, out var heightMod) ? heightMod : this.DefaultWobblyHeight));
} }
// control codes // control codes
@ -131,35 +196,50 @@ namespace MLEM.Formatting {
public string ResolveMacros(string s) { public string ResolveMacros(string s) {
// resolve macros that resolve into macros // resolve macros that resolve into macros
var rec = 0; var rec = 0;
var ret = s;
bool matched; bool matched;
do { do {
matched = false; matched = false;
foreach (var macro in this.Macros) { foreach (var macro in this.Macros) {
s = macro.Key.Replace(s, m => { ret = macro.Key.Replace(ret, m => {
// if the match evaluator was queried, then we know we matched something // if the match evaluator was queried, then we know we matched something
matched = true; matched = true;
return macro.Value(this, m, macro.Key); return macro.Value(this, m, macro.Key);
}); });
} }
rec++; rec++;
if (rec >= 16) if (rec >= 64)
throw new ArithmeticException($"A string resolved macros recursively too many times. Does it contain any conflicting macros?\n{s}"); throw new ArithmeticException($"A string resolved macros recursively too many times. Does it contain any conflicting macros?\nOriginal: {s}\nCurrent: {ret}");
} while (matched); } while (matched);
return ret;
}
/// <summary>
/// Strips all formatting codes from the given string, causing a string without any formatting codes to be returned.
/// Note that, if a <see cref="TokenizedString"/> has already been created using <see cref="Tokenize"/>, it is more efficient to use <see cref="TokenizedString.String"/> or <see cref="TokenizedString.DisplayString"/>.
/// </summary>
/// <param name="s">The string to strip formatting codes from.</param>
/// <returns>The stripped string.</returns>
public string StripAllFormatting(string s) {
foreach (var regex in this.Codes.Keys)
s = regex.Replace(s, string.Empty);
return s; return s;
} }
private Code GetNextCode(string s, int index, int maxIndex = int.MaxValue) { private Code GetNextCode(string s, int index, int maxIndex = int.MaxValue) {
var (c, m, r) = this.Codes var (constructor, match, regex) = this.Codes
.Select(kv => (c: kv.Value, m: kv.Key.Match(s, index), r: kv.Key)) .Select(kv => (Constructor: kv.Value, Match: kv.Key.Match(s, index), Regex: kv.Key))
.Where(kv => kv.m.Success && kv.m.Index <= maxIndex) .Where(kv => kv.Match.Success && kv.Match.Index <= maxIndex)
.OrderBy(kv => kv.m.Index) .OrderBy(kv => kv.Match.Index)
.FirstOrDefault(); .FirstOrDefault();
return c?.Invoke(this, m, r); return constructor?.Invoke(this, match, regex);
} }
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 #pragma warning disable CS0618
// this can be combined with StripAllFormatting (which was added after GetReplacementString was deprecated) once GetReplacementString is removed
// (just make this method accept a set of regular expressions, and then call it with all code keys in StripAllFormatting, and the applied codes' regexes in Tokenize)
s = code.Regex.Replace(s, code.GetReplacementString(font)); s = code.Regex.Replace(s, code.GetReplacementString(font));
#pragma warning restore CS0618 #pragma warning restore CS0618
} }

View file

@ -189,29 +189,37 @@ namespace MLEM.Formatting {
} }
/// <inheritdoc cref="GenericFont.DrawString(SpriteBatch,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/> /// <inheritdoc cref="GenericFont.DrawString(SpriteBatch,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/>
public void Draw(GameTime time, SpriteBatch batch, Vector2 pos, GenericFont font, Color color, float scale, float depth) { public void Draw(GameTime time, SpriteBatch batch, Vector2 pos, GenericFont font, Color color, float scale, float depth, int? startIndex = null, int? endIndex = null) {
var innerOffset = new Vector2(this.initialInnerOffset * scale, 0); var innerOffset = new Vector2(this.initialInnerOffset * scale, 0);
for (var t = 0; t < this.Tokens.Length; t++) { for (var t = 0; t < this.Tokens.Length; t++) {
var token = this.Tokens[t]; var token = this.Tokens[t];
if (endIndex != null && token.Index >= endIndex)
return;
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); if (startIndex == null || token.Index >= startIndex)
token.DrawSelf(time, batch, pos + innerOffset, drawFont, drawColor, scale, depth);
innerOffset.X += token.GetSelfWidth(drawFont) * scale; 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++) {
var charIndex = 0; var cpsIndex = 0;
var line = new CodePointSource(token.SplitDisplayString[l]); var line = new CodePointSource(token.SplitDisplayString[l]);
while (charIndex < line.Length) { while (cpsIndex < line.Length) {
var (codePoint, length) = line.GetCodePoint(charIndex); if (endIndex != null && token.Index + indexInToken >= endIndex)
return;
var (codePoint, length) = line.GetCodePoint(cpsIndex);
var character = CodePointSource.ToString(codePoint); var character = CodePointSource.ToString(codePoint);
token.DrawCharacter(time, batch, codePoint, character, indexInToken, pos + innerOffset, drawFont, drawColor, scale, depth); if (startIndex == null || token.Index + indexInToken >= startIndex)
token.DrawCharacter(time, batch, codePoint, character, indexInToken, pos + innerOffset, drawFont, drawColor, scale, depth);
innerOffset.X += drawFont.MeasureString(character).X * scale; innerOffset.X += drawFont.MeasureString(character).X * scale;
charIndex += length; indexInToken += length;
indexInToken++; cpsIndex += length;
} }
// only split at a new line, not between tokens! // only split at a new line, not between tokens!

View file

@ -1,4 +1,5 @@
using System; using System;
using System.Linq;
using System.Runtime.Serialization; using System.Runtime.Serialization;
using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Input;
@ -13,7 +14,25 @@ namespace MLEM.Input {
public readonly struct GenericInput : IEquatable<GenericInput> { public readonly struct GenericInput : IEquatable<GenericInput> {
/// <summary> /// <summary>
/// The <see cref="InputType"/> of this generic input's current <see cref="value"/>. /// All <see cref="GenericInput"/> values created from all values of the <see cref="Keys"/> enum.
/// </summary>
public static readonly GenericInput[] AllKeys = InputHandler.AllKeys.Select(k => (GenericInput) k).ToArray();
/// <summary>
/// All <see cref="GenericInput"/> values created from all values of the <see cref="Input.MouseButton"/> enum.
/// </summary>
public static readonly GenericInput[] AllMouseButtons = MouseExtensions.MouseButtons.Select(k => (GenericInput) k).ToArray();
/// <summary>
/// All <see cref="GenericInput"/> values created from all values of the <see cref="Buttons"/> enum.
/// </summary>
public static readonly GenericInput[] AllButtons = InputHandler.AllButtons.Select(k => (GenericInput) k).ToArray();
/// <summary>
/// All <see cref="GenericInput"/> values created from all values of the <see cref="Keys"/>, <see cref="Input.MouseButton"/> and <see cref="Buttons"/> enums.
/// This collection represents all possible valid, non-default <see cref="GenericInput"/> values.
/// </summary>
public static readonly GenericInput[] AllInputs = GenericInput.AllKeys.Concat(GenericInput.AllMouseButtons).Concat(GenericInput.AllButtons).ToArray();
/// <summary>
/// The <see cref="InputType"/> of this generic input's current value.
/// </summary> /// </summary>
[DataMember] [DataMember]
public readonly InputType Type; public readonly InputType Type;

View file

@ -193,16 +193,18 @@ namespace MLEM.Input {
/// <summary> /// <summary>
/// Creates a new input handler with optional initial values. /// Creates a new input handler with optional initial values.
/// </summary> /// </summary>
/// <param name="game">The game instance that this input handler belongs to</param> /// <param name="game">The game instance that this input handler belongs to.</param>
/// <param name="handleKeyboard">If keyboard input should be handled</param> /// <param name="handleKeyboard">The initial value for <see cref="HandleKeyboard"/>, which determines whether this input handler handles keyboard inputs.</param>
/// <param name="handleMouse">If mouse input should be handled</param> /// <param name="handleMouse">The initial value for <see cref="HandleMouse"/>, which determines whether this input handler handles mouse inputs.</param>
/// <param name="handleGamepads">If gamepad input should be handled</param> /// <param name="handleGamepads">The initial value for <see cref="HandleGamepads"/>, which determines whether this input handler handles gamepad inputs.</param>
/// <param name="handleTouch">If touch input should be handled</param> /// <param name="handleTouch">The initial value for <see cref="HandleTouch"/>, which determines whether this input handler handles touch inputs.</param>
public InputHandler(Game game, bool handleKeyboard = true, bool handleMouse = true, bool handleGamepads = true, bool handleTouch = true) : base(game) { /// <param name="externalGestureHandling">The initial value for <see cref="ExternalGestureHandling"/>, which determines whether gestures will be supplied using <see cref="AddExternalGesture"/> (or this input handler should handle gestures itself).</param>
public InputHandler(Game game, bool handleKeyboard = true, bool handleMouse = true, bool handleGamepads = true, bool handleTouch = true, bool externalGestureHandling = false) : base(game) {
this.HandleKeyboard = handleKeyboard; this.HandleKeyboard = handleKeyboard;
this.HandleMouse = handleMouse; this.HandleMouse = handleMouse;
this.HandleGamepads = handleGamepads; this.HandleGamepads = handleGamepads;
this.HandleTouch = handleTouch; this.HandleTouch = handleTouch;
this.ExternalGestureHandling = externalGestureHandling;
this.Gestures = this.gestures.AsReadOnly(); this.Gestures = this.gestures.AsReadOnly();
} }

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