1
0
Fork 0
mirror of https://github.com/Ellpeck/MLEM.git synced 2024-11-25 22:18:34 +01:00

Compare commits

..

84 commits

Author SHA1 Message Date
Ell
9b090c954f 6.0.0 2022-07-25 18:44:16 +02:00
Ell
064dc5607e resolved demo update todo 2022-07-25 18:42:52 +02:00
Ell
663d7148fe Revert "restore android workload when building"
This reverts commit 79f7206686.
2022-07-25 18:38:23 +02:00
Ell
79f7206686 restore android workload when building 2022-07-25 18:26:17 +02:00
Ell
711f60a97e Updated demos and templates to MonoGame 3.8.1 2022-07-25 18:23:16 +02:00
Ell
48dfa8f1ee Allow RandomExtensions to operate on any ICollection 2022-07-19 15:20:19 +02:00
Ell
e673ccea61 fixed a missing using directive in MLEM.FNA 2022-07-18 20:46:59 +02:00
Ell
ba1058748e Allow specifying multiple names for a DataTextureAtlas region 2022-07-18 20:41:19 +02:00
Ell
08e28cb95b Allow manually setting a RootElement as CanBeActive 2022-07-18 15:53:26 +02:00
Ell
288b8352af improved the README and some package descriptions slightly 2022-07-04 23:53:48 +02:00
Ell
d32bc0cbfb Added TryGetUpTime, GetUpTime, TryGetTimeSincePress and GetTimeSincePress to InputHandler 2022-06-29 15:57:41 +02:00
Ell
d58c5d8b33 fixed the new TextInput not updating correctly if the Font is set late 2022-06-29 14:34:13 +02:00
Ell
6f05263980 ensure that SimpleEndCode doesn't end formatting codes with shorter names 2022-06-26 17:18:38 +02:00
Ell
6e2c2b3730 Improved the way terminating formatting codes work by introducing SimpleEndCode 2022-06-26 15:08:11 +02:00
Ell
92018eea1e Made the base package's description more descriptive
This reverts commit 2973bd98e2.
This reverts commit 04c6bb5ff8.
2022-06-26 14:25:19 +02:00
Ell
04c6bb5ff8 neaten up the MLEM.Input description 2022-06-25 23:50:25 +02:00
Ell
2973bd98e2 moved MLEM's Input namespace into its own package 2022-06-25 23:46:06 +02:00
Ell
87b575b5c3 bump major version and cleaned up readme 2022-06-24 21:39:53 +02:00
Ell
61055148ef some fixes and doc changes to make MLEM.FNA more in line with MLEM 2022-06-24 21:33:24 +02:00
Ell
334918103f link to the mlem search directly for baget 2022-06-24 17:20:15 +02:00
Ell
42a87471fc fixed UiSystem AutoScaleReferenceSize default value 2022-06-24 15:21:08 +02:00
Ell
9c370f75e8 switch non-core dependencies over to core 2022-06-24 15:04:01 +02:00
Ell
059e26781b fixed submodules not being restored on ci 2022-06-24 14:11:43 +02:00
Ell
5fcdda80dc fixed multiline text inputs not working on FNA 2022-06-24 14:10:24 +02:00
Ell
5d7d238630 added MLEM.FNA 2022-06-24 14:01:26 +02:00
Ell
aff61508c4 Added TextInput class, which is an isolated version of MLEM.Ui's TextField logic 2022-06-19 18:17:46 +02:00
Ell
282a398f3b fixed the text formatting demo being too dark for drop shadows to be visible 2022-06-17 23:17:45 +02:00
Ell
6b08e892f7 added a proper text formatting demo 2022-06-17 21:47:40 +02:00
Ell
aabb1ed5df added .editorconfig 2022-06-17 18:23:47 +02:00
Ell
1795acb30e Added GenericInput support for Buttons.None 2022-06-15 11:44:28 +02:00
Ell
59af00c89a Code cleanup, and marked AStar.InfiniteCost as obsolete 2022-06-15 11:38:11 +02:00
Ell
d0ece92550 return the ElementType along with each Element in Parse 2022-06-14 00:08:05 +02:00
Ell
01fb5288ff finished up UiMarkdownParser 2022-06-14 00:04:57 +02:00
Ell
7b1da2f1a7 fixed image base path not being applied properly 2022-06-13 23:59:56 +02:00
Ell
f53305ce42 Added UiMarkdownParser 2022-06-13 23:52:10 +02:00
Ell
d03116a49a Allow using multiple textures in a StaticSpriteBatch 2022-06-08 11:05:18 +02:00
Ell
7d9633d989 Fixed StaticSpriteBatch not resetting its texture when all items are removed 2022-06-07 11:57:25 +02:00
Ell
42993f1a0b Added LayerPositionF to MLEM.Extended 2022-06-06 23:50:13 +02:00
Ell
0a93fb7da7 Allow using a StaticSpriteBatch to draw an IndividualTiledMapRenderer 2022-06-06 22:56:48 +02:00
Ell
144062fa64 cleaned up sandbox texture atlas 2022-05-29 15:05:23 +02:00
Ell
16053d9d04 further improve runtime texture packer performance by caching the first possible position for a request of a given size 2022-05-28 21:21:25 +02:00
Ell
61770d59b1 further improve RuntimeTexturePacker performance by only checking against already packed textures 2022-05-27 11:41:21 +02:00
Ell
93a82bcf36 cache UniformTextureAtlas texture straight away when checking for transparency 2022-05-27 11:19:29 +02:00
Ell
fdf04a7e77 allow ignoring transparent regions when packing a UniformTextureAtlas 2022-05-27 11:16:16 +02:00
Ell
cb496f613f Improve RuntimeTexturePacker performance by checking against packed textures in the same order as packing 2022-05-27 11:02:33 +02:00
Ell
951f4babd5 Fixed gamepad auto-nav angle being incorrect for some elements 2022-05-26 11:39:55 +02:00
Ell
b9f2de8290 Made RuntimeTexturePacker padding be per request and improve performance by caching texture data 2022-05-25 13:18:25 +02:00
Ell
f0f1d7f8ed added runtime texture region padding and other improvements 2022-05-25 12:37:51 +02:00
Ell
fcca5300ae Fixed elements' OnDeselected events not being raised when CanBeSelected is set to false while selected 2022-05-21 20:42:54 +02:00
Ell
bd9d3f970b Added RandomPitchModifier and GetRandomPitch to SoundEffectInfo 2022-05-20 16:59:28 +02:00
Ell
161d44dbe0 Added an Enum constructor to GenericInput 2022-05-18 21:45:38 +02:00
Ell
30bcdc1710 Allow comparing Keybind and Combination based on the amount of modifiers they have 2022-05-18 18:50:00 +02:00
Ell
15b873a8ad fixed xml doc recursive reference 2022-05-18 16:01:24 +02:00
Ell
6dc4011ef5 Added optional isKeybindAllowed parameter to KeybindButton 2022-05-18 15:54:29 +02:00
Ell
8968e6025d Added IsPressConsumed and IsAnyPressedAvailable to InputHandler 2022-05-17 18:20:31 +02:00
Ell
03accff6ae modify AutoNavGroup behavior to disallow new selections 2022-05-17 16:06:22 +02:00
Ell
5ba550619d Added AndThen to Easings 2022-05-10 21:32:28 +02:00
Ell
7ebbe49786 Added ReverseInput and ReverseOutput to Easings 2022-05-10 20:56:14 +02:00
Ell
61439aa521 made the MLEM.Data description more descriptive 2022-05-10 16:06:45 +02:00
Ell
874be1fd6e Fixed SoundEffectReader incorrectly claiming it could read ogg and mp3 files 2022-05-10 15:58:47 +02:00
Ell
acd15fea14 improved Ui and Data package descriptions 2022-05-10 15:42:20 +02:00
Ell
47b58b1942 Premultiply textures when using RawContentManager 2022-05-07 21:39:36 +02:00
Ell
16b9e26969 Fixed elements sometimes staying hidden when they shouldn't in scrolling panels 2022-05-04 13:54:15 +02:00
Ell
98118e540a Allow manually hiding a paragraph without its text overriding the hidden state 2022-05-04 13:22:24 +02:00
Ell
58b716aabb Don't query a paragraph's text callback in the constructor 2022-05-03 20:26:39 +02:00
Ell
63d2353694 Improved ElementHelper.AddTooltip overloads 2022-05-03 20:10:26 +02:00
Ell
15a57d8db9 Turned Tooltip paragraph styling into style properties 2022-05-03 19:35:44 +02:00
Ell
5a1b31e8a3 Allow adding dropdown and tooltip elements at a specified index 2022-05-03 19:07:53 +02:00
Ell
435042e1f5 Allow Tooltip to manage more than one paragraph and make it easier to add new lines 2022-05-03 18:58:18 +02:00
Ell
4c24284a3f updated templates 2022-04-30 13:14:44 +02:00
Ell
bc0f9d5c0c consume other UI inputs too 2022-04-30 12:26:40 +02:00
Ell
610527374e Make use of the new consuming variants in InputHandler and Keybind to consume UiControls inputs 2022-04-30 12:14:08 +02:00
Ell
4a88cca8bf also added consuming variants of IsPressed to Keybind 2022-04-30 11:38:05 +02:00
Ell
8adee49e55 fixed scroll bars not working with the new InvertPressBehavior 2022-04-30 11:31:40 +02:00
Ell
46c77d2444 Added InputHandler.InvertPressBehavior 2022-04-29 15:34:04 +02:00
Ell
6393d879d9 added SpriteBatchContext 2022-04-25 15:25:58 +02:00
Ell
c78bafd000 Ensure that Element.IsMouseOver is always accurate by making it an auto-property 2022-04-15 14:18:55 +02:00
Ell
783da33107 Fixed elements not being deselected when removed through RemoveChild 2022-04-15 14:16:38 +02:00
Ell
be26a2ebc2 made the new AutoNavGroup also work for gamepad controls 2022-04-14 18:01:30 +02:00
Ell
45afd9ac79 Added Element.AutoNavGroup which allows forming groups for auto-navigation 2022-04-14 17:54:25 +02:00
Ell
ad29b46df3 Fixed radio buttons not unchecking all other radio buttons with the same root element 2022-04-14 17:45:01 +02:00
Ell
f445f59078 Added consuming variants of IsPressed methods to InputHandler 2022-04-11 10:33:41 +02:00
Ell
902391d278 Fixed auto-nav tooltip displaying on the selected element even when not in auto-nav mode 2022-04-09 22:00:21 +02:00
Ell
62d2b28ec0 bump version 2022-04-08 14:50:14 +02:00
225 changed files with 4910 additions and 2353 deletions

113
.editorconfig Normal file
View file

@ -0,0 +1,113 @@
[*]
charset = utf-8
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true
indent_style = space
indent_size = 4
# Microsoft .NET properties
csharp_new_line_before_catch = false
csharp_new_line_before_else = false
csharp_new_line_before_finally = false
csharp_new_line_before_members_in_object_initializers = false
csharp_new_line_before_open_brace = none
csharp_new_line_between_query_expression_clauses = false
csharp_preferred_modifier_order = public, private, protected, internal, new, static, abstract, virtual, sealed, readonly, override, extern, unsafe, volatile, async:suggestion
csharp_space_after_cast = true
csharp_style_var_elsewhere = true:suggestion
csharp_style_var_for_built_in_types = true:suggestion
csharp_style_var_when_type_is_apparent = true:suggestion
dotnet_naming_rule.private_constants_rule.severity = warning
dotnet_naming_rule.private_constants_rule.style = upper_camel_case_style
dotnet_naming_rule.private_constants_rule.symbols = private_constants_symbols
dotnet_naming_rule.private_instance_fields_rule.severity = warning
dotnet_naming_rule.private_instance_fields_rule.style = lower_camel_case_style
dotnet_naming_rule.private_instance_fields_rule.symbols = private_instance_fields_symbols
dotnet_naming_rule.private_static_fields_rule.severity = warning
dotnet_naming_rule.private_static_fields_rule.style = lower_camel_case_style
dotnet_naming_rule.private_static_fields_rule.symbols = private_static_fields_symbols
dotnet_naming_rule.private_static_readonly_rule.severity = warning
dotnet_naming_rule.private_static_readonly_rule.style = upper_camel_case_style
dotnet_naming_rule.private_static_readonly_rule.symbols = private_static_readonly_symbols
dotnet_naming_style.lower_camel_case_style.capitalization = camel_case
dotnet_naming_style.upper_camel_case_style.capitalization = pascal_case
dotnet_naming_symbols.private_constants_symbols.applicable_accessibilities = private
dotnet_naming_symbols.private_constants_symbols.applicable_kinds = field
dotnet_naming_symbols.private_constants_symbols.required_modifiers = const
dotnet_naming_symbols.private_instance_fields_symbols.applicable_accessibilities = private
dotnet_naming_symbols.private_instance_fields_symbols.applicable_kinds = field
dotnet_naming_symbols.private_static_fields_symbols.applicable_accessibilities = private
dotnet_naming_symbols.private_static_fields_symbols.applicable_kinds = field
dotnet_naming_symbols.private_static_fields_symbols.required_modifiers = static
dotnet_naming_symbols.private_static_readonly_symbols.applicable_accessibilities = private
dotnet_naming_symbols.private_static_readonly_symbols.applicable_kinds = field
dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers = static, readonly
dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:none
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:none
dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:none
dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
dotnet_style_predefined_type_for_member_access = true:suggestion
dotnet_style_qualification_for_event = true:suggestion
dotnet_style_qualification_for_field = true:suggestion
dotnet_style_qualification_for_method = true:suggestion
dotnet_style_qualification_for_property = true:suggestion
dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion
# ReSharper properties
resharper_blank_lines_after_block_statements = 0
resharper_blank_lines_around_auto_property = 0
resharper_blank_lines_around_local_method = 0
resharper_blank_lines_around_property = 0
resharper_blank_lines_inside_type = 1
resharper_braces_for_dowhile = required_for_multiline
resharper_braces_for_fixed = required_for_multiline
resharper_braces_for_for = required_for_multiline
resharper_braces_for_foreach = required_for_multiline
resharper_braces_for_ifelse = required_for_multiline
resharper_braces_for_lock = required_for_multiline
resharper_braces_for_using = required_for_multiline
resharper_braces_for_while = required_for_multiline
resharper_braces_redundant = false
resharper_csharp_blank_lines_around_field = 0
resharper_csharp_blank_lines_around_single_line_invocable = 1
resharper_csharp_empty_block_style = together
resharper_csharp_keep_blank_lines_in_code = 1
resharper_csharp_keep_blank_lines_in_declarations = 1
resharper_csharp_max_line_length = unset
resharper_csharp_stick_comment = false
resharper_csharp_wrap_before_ternary_opsigns = false
resharper_csharp_wrap_multiple_declaration_style = wrap_if_long
resharper_csharp_wrap_multiple_type_parameter_constraints_style = wrap_if_long
resharper_csharp_wrap_ternary_expr_style = wrap_if_long
resharper_force_attribute_style = join
resharper_indent_nested_fixed_stmt = true
resharper_indent_nested_foreach_stmt = true
resharper_indent_nested_for_stmt = true
resharper_indent_nested_lock_stmt = true
resharper_indent_nested_usings_stmt = true
resharper_indent_nested_while_stmt = true
resharper_indent_preprocessor_if = usual_indent
resharper_indent_preprocessor_other = usual_indent
resharper_keep_existing_declaration_parens_arrangement = false
resharper_keep_existing_embedded_arrangement = false
resharper_keep_existing_expr_member_arrangement = false
resharper_keep_existing_property_patterns_arrangement = false
resharper_keep_existing_switch_expression_arrangement = false
resharper_parentheses_redundancy_style = remove
resharper_place_attribute_on_same_line = false
resharper_place_expr_accessor_on_single_line = true
resharper_place_expr_method_on_single_line = true
resharper_place_expr_property_on_single_line = true
resharper_place_simple_accessor_on_single_line = false
resharper_place_simple_anonymousmethod_on_single_line = false
resharper_place_simple_embedded_statement_on_same_line = false
resharper_space_around_arrow_op = true
resharper_space_within_empty_braces = false
resharper_space_within_single_line_array_initializer_braces = false
resharper_static_members_qualify_members = field, property, event, method
resharper_wrap_for_stmt_header_style = wrap_if_long
resharper_wrap_object_and_collection_initializer_style = wrap_if_long
resharper_xmldoc_attribute_indent = align_by_first_attribute
resharper_xmldoc_attribute_style = on_single_line
resharper_xmldoc_pi_attribute_style = on_single_line

6
.gitmodules vendored Normal file
View file

@ -0,0 +1,6 @@
[submodule "FNA"]
path = FNA
url = https://github.com/FNA-XNA/FNA
[submodule "FontStashSharp"]
path = FontStashSharp
url = https://github.com/FontStashSharp/FontStashSharp

View file

@ -2,11 +2,98 @@
MLEM tries to adhere to [semantic versioning](https://semver.org/). Breaking changes are written in **bold**.
Jump to version:
- [6.0.0](#600)
- [5.3.0](#530)
- [5.2.0](#520)
- [5.1.0](#510)
- [5.0.0](#500)
## 6.0.0
### MLEM
Additions
- Added consuming variants of IsPressed methods to InputHandler and Keybind
- Added SpriteBatchContext struct and extensions
- Added InputHandler.InvertPressBehavior
- Added ReverseInput, ReverseOutput and AndThen to Easings
- Added an Enum constructor to GenericInput
- Added RandomPitchModifier and GetRandomPitch to SoundEffectInfo
- Added TextInput class, which is an isolated version of MLEM.Ui's TextField logic
- Added MLEM.FNA, which is fully compatible with FNA
- Added TryGetUpTime, GetUpTime, TryGetTimeSincePress and GetTimeSincePress to InputHandler
Improvements
- Allow comparing Keybind and Combination based on the amount of modifiers they have
- Allow using multiple textures in a StaticSpriteBatch
- Added GenericInput support for Buttons.None
- Improved the way terminating formatting codes work by introducing SimpleEndCode
- Allow RandomExtensions to operate on any ICollection
Removals
- Marked AStar.InfiniteCost as obsolete
### MLEM.Ui
Additions
- Added Element.AutoNavGroup which allows forming groups for auto-navigation
- Added UiMarkdownParser
- Added MLEM.Ui.FNA, which is fully compatible with FNA
Improvements
- Ensure that Element.IsMouseOver is always accurate by making it an auto-property
- Started using SpriteBatchContext for Draw and DrawTransformed methods
- Make use of the new consuming variants in InputHandler and Keybind to consume UiControls inputs
- Allow Tooltip to manage more than one paragraph and make it easier to add new lines
- Allow adding dropdown elements at a specified index
- Turned Tooltip paragraph styling into style properties
- Improved ElementHelper.AddTooltip overloads
- Don't query a paragraph's text callback in the constructor
- Allow manually hiding a paragraph without its text overriding the hidden state
- Added optional isKeybindAllowed parameter to KeybindButton
- Allow manually setting a RootElement as CanBeActive
Fixes
- Fixed auto-nav tooltip displaying on the selected element even when not in auto-nav mode
- Fixed radio buttons not unchecking all other radio buttons with the same root element
- Fixed elements not being deselected when removed through RemoveChild
- Fixed elements sometimes staying hidden when they shouldn't in scrolling panels
- Fixed elements' OnDeselected events not being raised when CanBeSelected is set to false while selected
- Fixed gamepad auto-nav angle being incorrect for some elements
Removals
- Marked old Draw and DrawTransformed overloads as obsolete in favor of SpriteBatchContext ones
- Marked Tooltip.Paragraph as obsolete in favor of new Paragraphs collection
### MLEM.Extended
Additions
- Added LayerPositionF
- Added MLEM.Extended.FNA, which is fully compatible with FNA
Improvements
- Allow using a StaticSpriteBatch to render an IndividualTiledMapRenderer
### MLEM.Data
Additions
- Added the ability to add padding to RuntimeTexturePacker texture regions
- Added the ability to pack UniformTextureAtlas and DataTextureAtlas using RuntimeTexturePacker
- Added MLEM.Data.FNA, which is fully compatible with FNA
Improvements
- Premultiply textures when using RawContentManager
- Allow enumerating all region names of a DataTextureAtlas
- Cache RuntimeTexturePacker texture data while packing to improve performance
- Greatly improved RuntimeTexturePacker performance
- Allow specifying multiple names for a DataTextureAtlas region
Fixes
- Fixed SoundEffectReader incorrectly claiming it could read ogg and mp3 files
### MLEM.Startup
Additions
- Added MLEM.Startup.FNA, which is fully compatible with FNA
### MLEM.Templates
Improvements
- Updated to MonoGame 3.8.1
## 5.3.0
### MLEM
Additions

View file

@ -0,0 +1,36 @@
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-mgcb": {
"version": "3.8.1.263",
"commands": [
"mgcb"
]
},
"dotnet-mgcb-editor": {
"version": "3.8.1.263",
"commands": [
"mgcb-editor"
]
},
"dotnet-mgcb-editor-linux": {
"version": "3.8.1.263",
"commands": [
"mgcb-editor-linux"
]
},
"dotnet-mgcb-editor-windows": {
"version": "3.8.1.263",
"commands": [
"mgcb-editor-windows"
]
},
"dotnet-mgcb-editor-mac": {
"version": "3.8.1.263",
"commands": [
"mgcb-editor-mac"
]
}
}
}

View file

@ -1,17 +1,14 @@
using Android.App;
using Android.Content;
using Android.Content.PM;
using Android.Net;
using Android.OS;
using Android.Views;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using MLEM.Extensions;
using MLEM.Misc;
using static Android.Views.SystemUiFlags;
using Uri = Android.Net.Uri;
namespace Demos.Android {
[Activity(
namespace Demos.Android;
[Activity(
Label = "@string/app_name",
MainLauncher = true,
Icon = "@drawable/icon",
@ -19,8 +16,8 @@ namespace Demos.Android {
LaunchMode = LaunchMode.SingleInstance,
ScreenOrientation = ScreenOrientation.UserLandscape,
ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.Keyboard | ConfigChanges.KeyboardHidden | ConfigChanges.ScreenSize
)]
public class Activity1 : AndroidGameActivity {
)]
public class Activity1 : AndroidGameActivity {
private GameImpl game;
private View view;
@ -47,9 +44,12 @@ namespace Demos.Android {
public override void OnWindowFocusChanged(bool hasFocus) {
base.OnWindowFocusChanged(hasFocus);
// hide the status bar
if (hasFocus)
this.Window.DecorView.SystemUiVisibility = (StatusBarVisibility) (ImmersiveSticky | LayoutStable | LayoutHideNavigation | LayoutFullscreen | HideNavigation | Fullscreen);
if (hasFocus) {
#pragma warning disable CS0618
// 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);
#pragma warning restore CS0618
}
}
}
}

View file

@ -0,0 +1,6 @@
<?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">
<uses-sdk android:minSdkVersion="23" android:targetSdkVersion="31" />
<uses-feature android:glEsVersion="0x00020000" android:required="true" />
<application android:label="MLEM Android Demos" />
</manifest>

View file

@ -1,104 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProductVersion>8.0.30703</ProductVersion>
<SchemaVersion>2.0</SchemaVersion>
<ProjectGuid>{410C0262-131C-4D0E-910D-D01B4F7143E0}</ProjectGuid>
<ProjectTypeGuids>{EFBA0AD7-5A72-4C68-AF49-83D382785DCF};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Demos.Android</RootNamespace>
<AssemblyName>Demos.Android</AssemblyName>
<FileAlignment>512</FileAlignment>
<AndroidApplication>true</AndroidApplication>
<AndroidResgenFile>Resources\Resource.Designer.cs</AndroidResgenFile>
<AndroidResgenClass>Resource</AndroidResgenClass>
<GenerateSerializationAssemblies>Off</GenerateSerializationAssemblies>
<AndroidStoreUncompressedFileExtensions>.m4a</AndroidStoreUncompressedFileExtensions>
<TargetFrameworkVersion>v10.0</TargetFrameworkVersion>
<AndroidManifest>Properties\AndroidManifest.xml</AndroidManifest>
<AndroidUseLatestPlatformSdk>false</AndroidUseLatestPlatformSdk>
<MonoAndroidResourcePrefix>Resources</MonoAndroidResourcePrefix>
<MonoAndroidAssetsPrefix>Assets</MonoAndroidAssetsPrefix>
<AndroidEnableSGenConcurrent>true</AndroidEnableSGenConcurrent>
<AndroidHttpClientHandlerType>Xamarin.Android.Net.AndroidClientHandler</AndroidHttpClientHandlerType>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>portable</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\$(MonoGamePlatform)\$(Platform)\$(Configuration)\</OutputPath>
<DefineConstants>DEBUG;TRACE;ANDROID</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<AndroidUseSharedRuntime>True</AndroidUseSharedRuntime>
<AndroidLinkMode>None</AndroidLinkMode>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>portable</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\$(MonoGamePlatform)\$(Platform)\$(Configuration)\</OutputPath>
<DefineConstants>TRACE;ANDROID</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<AndroidUseSharedRuntime>False</AndroidUseSharedRuntime>
<AndroidLinkMode>SdkOnly</AndroidLinkMode>
<AotAssemblies>false</AotAssemblies>
<EnableLLVM>false</EnableLLVM>
<AndroidEnableProfiledAot>false</AndroidEnableProfiledAot>
<BundleAssemblies>false</BundleAssemblies>
<MandroidI18n />
<AndroidPackageFormat>aab</AndroidPackageFormat>
<AndroidUseAapt2>true</AndroidUseAapt2>
<AndroidCreatePackagePerAbi>false</AndroidCreatePackagePerAbi>
<TargetFramework>net6.0-android</TargetFramework>
<SupportedOSPlatformVersion>31</SupportedOSPlatformVersion>
<OutputType>Exe</OutputType>
<ApplicationId>de.ellpeck.mlem.demos.android</ApplicationId>
<ApplicationVersion>1</ApplicationVersion>
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
<ImplicitUsings>true</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml" />
<Reference Include="Mono.Android" />
</ItemGroup>
<ItemGroup>
<Compile Include="Activity1.cs" />
<Compile Include="Resources\Resource.Designer.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\Drawable\Icon.png" />
<AndroidResource Include="Resources\Values\Strings.xml" />
<PackageReference Include="MonoGame.Content.Builder.Task" Version="3.8.1.263" />
<PackageReference Include="MonoGame.Framework.Android" Version="3.8.1.263" />
<ProjectReference Include="..\Demos\Demos.csproj" />
</ItemGroup>
<ItemGroup>
<MonoGameContentReference Include="..\Demos\Content\Content.mgcb" />
<None Include="..\Demos\Content\*\**" />
</ItemGroup>
<ItemGroup>
<None Include="Properties\AndroidManifest.xml" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Coroutine" Version="2.1.3" />
<PackageReference Include="MonoGame.Content.Builder.Task" Version="3.8.0.1641" />
<PackageReference Include="MonoGame.Framework.Android" Version="3.8.0.1641" />
<PackageReference Include="TextCopy" Version="4.3.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Demos\Demos.csproj">
<Project>{1bc4682b-aa14-4937-b5c7-707e20fe88ff}</Project>
<Name>Demos</Name>
</ProjectReference>
<ProjectReference Include="..\MLEM.Startup\MLEM.Startup.csproj">
<Project>{997f4739-7bec-4621-b9ca-68deb2d74412}</Project>
<Name>MLEM.Startup</Name>
</ProjectReference>
<ProjectReference Include="..\MLEM.Ui\MLEM.Ui.csproj">
<Project>{6f00629a-8b87-4264-8896-19983285e32f}</Project>
<Name>MLEM.Ui</Name>
</ProjectReference>
<ProjectReference Include="..\MLEM\MLEM.csproj">
<Project>{1d6ab762-43c4-4775-8924-707c7ec3f142}</Project>
<Name>MLEM</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
</Project>

View file

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

View file

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

View file

@ -1,76 +0,0 @@
#pragma warning disable 1591
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
[assembly: global::Android.Runtime.ResourceDesignerAttribute("Demos.Android.Resource", IsApplication=true)]
namespace Demos.Android
{
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Android.Build.Tasks", "1.0.0.0")]
public partial class Resource
{
static Resource()
{
global::Android.Runtime.ResourceIdManager.UpdateIdValues();
}
public static void UpdateIdValues()
{
}
public partial class Attribute
{
static Attribute()
{
global::Android.Runtime.ResourceIdManager.UpdateIdValues();
}
private Attribute()
{
}
}
public partial class Drawable
{
// aapt resource value: 0x7F010000
public const int Icon = 2130771968;
static Drawable()
{
global::Android.Runtime.ResourceIdManager.UpdateIdValues();
}
private Drawable()
{
}
}
public partial class String
{
// aapt resource value: 0x7F020000
public const int app_name = 2130837504;
static String()
{
global::Android.Runtime.ResourceIdManager.UpdateIdValues();
}
private String()
{
}
}
}
}
#pragma warning restore 1591

View file

@ -0,0 +1,36 @@
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-mgcb": {
"version": "3.8.1.263",
"commands": [
"mgcb"
]
},
"dotnet-mgcb-editor": {
"version": "3.8.1.263",
"commands": [
"mgcb-editor"
]
},
"dotnet-mgcb-editor-linux": {
"version": "3.8.1.263",
"commands": [
"mgcb-editor-linux"
]
},
"dotnet-mgcb-editor-windows": {
"version": "3.8.1.263",
"commands": [
"mgcb-editor-windows"
]
},
"dotnet-mgcb-editor-mac": {
"version": "3.8.1.263",
"commands": [
"mgcb-editor-mac"
]
}
}
}

View file

@ -0,0 +1,37 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
<ApplicationIcon>Icon.ico</ApplicationIcon>
<AssemblyName>MLEM Desktop Demos</AssemblyName>
<RootNamespace>Demos.DesktopGL</RootNamespace>
<DefineConstants>$(DefineConstants);FNA</DefineConstants>
<!-- We still use the MG content builder for ease of compatibility between the MG and FNA demo projects -->
<MonoGamePlatform>DesktopGL</MonoGamePlatform>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Demos\Demos.FNA.csproj" />
<ProjectReference Include="..\MLEM.Startup\MLEM.Startup.FNA.csproj" />
<ProjectReference Include="..\MLEM.Ui\MLEM.Ui.FNA.csproj" />
<ProjectReference Include="..\MLEM\MLEM.FNA.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="MonoGame.Content.Builder.Task" Version="3.8.0.1641" />
<ProjectReference Include="..\FNA\FNA.Core.csproj" />
</ItemGroup>
<ItemGroup>
<MonoGameContentReference Include="..\Demos\Content\Content.mgcb" />
<Content Include="..\Demos\Content\*\**" />
<EmbeddedResource Include="Icon.ico" />
<EmbeddedResource Include="Icon.bmp" />
<Content Include="FnaNative/**">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Link>%(Filename)%(Extension)</Link>
</Content>
</ItemGroup>
</Project>

View file

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -1,11 +1,17 @@
using Microsoft.Xna.Framework;
using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using MLEM.Misc;
namespace Demos.DesktopGL {
public static class Program {
public static void Main() {
#if FNA
MlemPlatform.Current = new MlemPlatform.DesktopFna(a => TextInputEXT.TextInput += a);
#else
MlemPlatform.Current = new MlemPlatform.DesktopGl<TextInputEventArgs>((w, c) => w.TextInput += c);
#endif
using var game = new GameImpl();
game.Run();
}

View file

@ -27,7 +27,7 @@ namespace Demos {
// using it having wrong coordinates and/or sizes
// the regions that are part of the atlas are then referenced by region coordinates rather than texture coordinates
// (as seen below)
var atlas = new UniformTextureAtlas(LoadContent<Texture2D>("Textures/Anim"), 4, 4);
var atlas = new UniformTextureAtlas(Demo.LoadContent<Texture2D>("Textures/Anim"), 4, 4);
// create the four animations by supplying the time per frame, and the four regions used
// note that you don't need to use a texture atlas for this, you can also simply supply the texture and the regions manually here
@ -90,7 +90,7 @@ namespace Demos {
public override void DoDraw(GameTime gameTime) {
this.GraphicsDevice.Clear(Color.CornflowerBlue);
this.SpriteBatch.Begin(SpriteSortMode.Deferred, null, SamplerState.PointClamp, transformMatrix: Matrix.CreateScale(10));
this.SpriteBatch.Begin(SpriteSortMode.Deferred, null, SamplerState.PointClamp, null, null, null, Matrix.CreateScale(10));
// draw the group's current region
// if not using a group, just draw the animation's CurrentRegion here
this.SpriteBatch.Draw(this.group.CurrentRegion, new Vector2(10, 10), Color.White);

View file

@ -20,7 +20,7 @@ namespace Demos {
public override void LoadContent() {
base.LoadContent();
// The layout of the texture is important for auto tiling to work correctly, and is explained in the XML docs for the methods used
this.texture = LoadContent<Texture2D>("Textures/AutoTiling");
this.texture = Demo.LoadContent<Texture2D>("Textures/AutoTiling");
// in this example, a simple string array is used for layout purposes. As the AutoTiling method allows any kind of
// comparison, the actual implementation can vary (for instance, based on a more in-depth tile map)
@ -37,7 +37,7 @@ namespace Demos {
this.GraphicsDevice.Clear(Color.Black);
// drawing the auto tiles
this.SpriteBatch.Begin(SpriteSortMode.Deferred, null, SamplerState.PointClamp, transformMatrix: Matrix.CreateScale(10));
this.SpriteBatch.Begin(SpriteSortMode.Deferred, null, SamplerState.PointClamp, null, null, null, Matrix.CreateScale(10));
for (var x = 0; x < 6; x++) {
for (var y = 0; y < 5; y++) {
// don't draw non-grass tiles ( )
@ -59,12 +59,12 @@ namespace Demos {
}
// the texture region supplied to the AutoTiling method should only encompass the first filler tile's location and size
AutoTiling.DrawAutoTile(this.SpriteBatch, new Vector2(x + 1, y + 1) * TileSize, new TextureRegion(this.texture, 0, 0, TileSize, TileSize), ConnectsTo, Color.White);
AutoTiling.DrawAutoTile(this.SpriteBatch, new Vector2(x + 1, y + 1) * AutoTilingDemo.TileSize, new TextureRegion(this.texture, 0, 0, AutoTilingDemo.TileSize, AutoTilingDemo.TileSize), ConnectsTo, Color.White);
// when drawing extended auto-tiles, the same rules apply, but the source texture layout is different
var background = new TextureRegion(this.texture, 0, TileSize * 2, TileSize, TileSize);
var overlay = new TextureRegion(this.texture, background.Area.OffsetCopy(new Point(TileSize, 0)));
AutoTiling.DrawExtendedAutoTile(this.SpriteBatch, new Vector2(x + 8, y + 1) * TileSize, background, overlay, ConnectsTo, Color.White, Color.White);
var background = new TextureRegion(this.texture, 0, AutoTilingDemo.TileSize * 2, AutoTilingDemo.TileSize, AutoTilingDemo.TileSize);
var overlay = new TextureRegion(this.texture, background.Area.OffsetCopy(new Point(AutoTilingDemo.TileSize, 0)));
AutoTiling.DrawExtendedAutoTile(this.SpriteBatch, new Vector2(x + 8, y + 1) * AutoTilingDemo.TileSize, background, overlay, ConnectsTo, Color.White, Color.White);
}
}
this.SpriteBatch.End();

View file

@ -13,6 +13,13 @@
#---------------------------------- Content ---------------------------------#
#begin Fonts/MonospacedFont.spritefont
/importer:FontDescriptionImporter
/processor:FontDescriptionProcessor
/processorParam:PremultiplyAlpha=True
/processorParam:TextureFormat=Compressed
/build:Fonts/MonospacedFont.spritefont
#begin Fonts/TestFont.spritefont
/importer:FontDescriptionImporter
/processor:FontDescriptionProcessor
@ -34,12 +41,8 @@
/processorParam:TextureFormat=Compressed
/build:Fonts/TestFontItalic.spritefont
#begin Fonts/MonospacedFont.spritefont
/importer:FontDescriptionImporter
/processor:FontDescriptionProcessor
/processorParam:PremultiplyAlpha=True
/processorParam:TextureFormat=Compressed
/build:Fonts/MonospacedFont.spritefont
#begin Markdown.md
/copy:Markdown.md
#begin Textures/Anim.png
/importer:TextureImporter

23
Demos/Content/Markdown.md Normal file
View file

@ -0,0 +1,23 @@
# H1
## H2
### H3
#### H4
##### H5
###### H6
Italics with *asterisks* or _underscores_.
Bold with **asterisks** or __underscores__.
Strikethrough with ~~two tildes~~.
[I'm an inline-style link](https://www.google.com)
<http://www.example.com>
![](https://raw.githubusercontent.com/Ellpeck/MLEM/main/Media/Logo.png)
Some `inline code` right here
```js
function codeBlock() {
}
```

21
Demos/Demos.FNA.csproj Normal file
View file

@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<RootNamespace>Demos</RootNamespace>
<DefineConstants>$(DefineConstants);FNA</DefineConstants>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\MLEM.Startup\MLEM.Startup.FNA.csproj" />
<ProjectReference Include="..\MLEM.Ui\MLEM.Ui.FNA.csproj" />
<ProjectReference Include="..\MLEM\MLEM.FNA.csproj" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\FNA\FNA.Core.csproj">
<PrivateAssets>all</PrivateAssets>
</ProjectReference>
</ItemGroup>
</Project>

View file

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
@ -11,7 +11,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.0.1641">
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.1.263">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

View file

@ -13,7 +13,7 @@ namespace Demos {
private static readonly FieldInfo[] EasingFields = typeof(Easings)
.GetFields(BindingFlags.Public | BindingFlags.Static).ToArray();
private static readonly Easings.Easing[] Easings = EasingFields
private static readonly Easings.Easing[] Easings = EasingsDemo.EasingFields
.Select(f => (Easings.Easing) f.GetValue(null)).ToArray();
private Group group;
private int current;
@ -27,11 +27,11 @@ namespace Demos {
this.group = new Group(Anchor.TopCenter, Vector2.One) {CanBeMoused = false};
this.group.AddChild(new Button(Anchor.AutoCenter, new Vector2(30, 10), "Next") {
OnPressed = e => {
this.current = (this.current + 1) % Easings.Length;
this.current = (this.current + 1) % EasingsDemo.Easings.Length;
this.progress = 0;
}
});
this.group.AddChild(new Paragraph(Anchor.AutoCenter, 1, p => EasingFields[this.current].Name, true));
this.group.AddChild(new Paragraph(Anchor.AutoCenter, 1, p => EasingsDemo.EasingFields[this.current].Name, true));
this.UiRoot.AddChild(this.group);
}
@ -43,11 +43,11 @@ namespace Demos {
this.GraphicsDevice.Clear(Color.CornflowerBlue);
base.DoDraw(time);
this.SpriteBatch.Begin(SpriteSortMode.Deferred, null, SamplerState.PointClamp);
this.SpriteBatch.Begin(SpriteSortMode.Deferred, null, SamplerState.PointClamp, null, null);
var view = this.GraphicsDevice.Viewport;
// graph the easing function
var graphEase = Easings[this.current].ScaleInput(0, view.Width).ScaleOutput(-view.Height / 3, view.Height / 3);
var graphEase = EasingsDemo.Easings[this.current].ScaleInput(0, view.Width).ScaleOutput(-view.Height / 3, view.Height / 3);
for (var x = 0; x < view.Width; x++) {
var area = new RectangleF(x - 2, view.Height / 2 - graphEase(x) - 2, 4, 4);
this.SpriteBatch.Draw(this.SpriteBatch.GetBlankTexture(), area, Color.Green);
@ -55,7 +55,7 @@ namespace Demos {
// draw a little dot to show what it would look like
this.progress = (this.progress + (float) time.ElapsedGameTime.TotalSeconds / 2) % 1;
var dotEase = Easings[this.current].AndReverse().ScaleOutput(0, view.Height / 4);
var dotEase = EasingsDemo.Easings[this.current].AndReverse().ScaleOutput(0, view.Height / 4);
var pos = new RectangleF(view.Width / 2 - 4, view.Height - 20 - dotEase(this.progress), 8, 8);
this.SpriteBatch.Draw(this.SpriteBatch.GetBlankTexture(), pos, Color.Red);

View file

@ -9,7 +9,9 @@ using MLEM.Textures;
using MLEM.Ui;
using MLEM.Ui.Elements;
using MLEM.Ui.Style;
#if !FNA
using MonoGame.Framework.Utilities;
#endif
namespace Demos {
public class GameImpl : MlemGame {
@ -23,11 +25,12 @@ namespace Demos {
private TimeSpan secondCounter;
static GameImpl() {
Demos.Add("Ui", ("An in-depth demonstration of the MLEM.Ui package and its abilities", game => new UiDemo(game)));
Demos.Add("Easings", ("An example of MLEM's Easings class, an adaptation of easings.net", game => new EasingsDemo(game)));
Demos.Add("Pathfinding", ("An example of MLEM's A* pathfinding implementation in 2D", game => new PathfindingDemo(game)));
Demos.Add("Animation and Texture Atlas", ("An example of UniformTextureAtlases, SpriteAnimations and SpriteAnimationGroups", game => new AnimationDemo(game)));
Demos.Add("Auto Tiling", ("A demonstration of the AutoTiling class that MLEM provides", game => new AutoTilingDemo(game)));
GameImpl.Demos.Add("Ui", ("An in-depth demonstration of the MLEM.Ui package and its abilities", game => new UiDemo(game)));
GameImpl.Demos.Add("Text Formatting", ("A demonstration of MLEM's text formatting system", game => new TextFormattingDemo(game)));
GameImpl.Demos.Add("Easings", ("An example of MLEM's Easings class, an adaptation of easings.net", game => new EasingsDemo(game)));
GameImpl.Demos.Add("Pathfinding", ("An example of MLEM's A* pathfinding implementation in 2D", game => new PathfindingDemo(game)));
GameImpl.Demos.Add("Animation and Texture Atlas", ("An example of UniformTextureAtlases, SpriteAnimations and SpriteAnimationGroups", game => new AnimationDemo(game)));
GameImpl.Demos.Add("Auto Tiling", ("A demonstration of the AutoTiling class that MLEM provides", game => new AutoTilingDemo(game)));
}
public GameImpl() {
@ -45,13 +48,6 @@ namespace Demos {
}
protected override void LoadContent() {
// TODO remove with MonoGame 3.8.1 https://github.com/MonoGame/MonoGame/issues/7298
if (PlatformInfo.MonoGamePlatform == MonoGamePlatform.DesktopGL) {
this.GraphicsDeviceManager.PreferredBackBufferWidth = 1280;
this.GraphicsDeviceManager.PreferredBackBufferHeight = 720;
this.GraphicsDeviceManager.ApplyChanges();
}
base.LoadContent();
this.UiSystem.AutoScaleReferenceSize = new Point(1280, 720);
this.UiSystem.AutoScaleWithScreen = true;
@ -74,7 +70,7 @@ namespace Demos {
selection.AddChild(new Paragraph(Anchor.AutoLeft, 1, "Select the demo you want to see below using your mouse, touch input, your keyboard or a controller. Check the demos' <l https://github.com/Ellpeck/MLEM/tree/main/Demos>source code</l> for more in-depth explanations of their functionality or the <l https://mlem.ellpeck.de/>website</l> for tutorials and API documentation."));
selection.AddChild(new VerticalSpace(5));
foreach (var demo in Demos) {
foreach (var demo in GameImpl.Demos) {
selection.AddChild(new Button(Anchor.AutoCenter, new Vector2(1, 10), demo.Key, demo.Value.Item1) {
OnPressed = e => {
selection.IsHidden = true;
@ -92,9 +88,9 @@ namespace Demos {
}
protected override UiStyle InitializeDefaultUiStyle(SpriteBatch batch) {
var tex = LoadContent<Texture2D>("Textures/Test");
var tex = MlemGame.LoadContent<Texture2D>("Textures/Test");
return new UntexturedStyle(this.SpriteBatch) {
Font = new GenericSpriteFont(LoadContent<SpriteFont>("Fonts/TestFont")),
Font = new GenericSpriteFont(MlemGame.LoadContent<SpriteFont>("Fonts/TestFont")),
TextScale = 0.1F,
PanelTexture = new NinePatch(new TextureRegion(tex, 0, 8, 24, 24), 8),
ButtonTexture = new NinePatch(new TextureRegion(tex, 24, 8, 16, 16), 4),

View file

@ -41,8 +41,8 @@ namespace Demos {
// pathfinder's constructor
float Cost(Point pos, Point nextPos) {
if (nextPos.X < 0 || nextPos.Y < 0 || nextPos.X >= 50 || nextPos.Y >= 50)
return AStar2.InfiniteCost;
return this.world[nextPos.X, nextPos.Y] ? 1 : AStar2.InfiniteCost;
return float.PositiveInfinity;
return this.world[nextPos.X, nextPos.Y] ? 1 : float.PositiveInfinity;
}
// Actually initialize the pathfinder with the cost function, as well as specify if moving diagonally between tiles should be
@ -73,7 +73,7 @@ namespace Demos {
public override void DoDraw(GameTime gameTime) {
this.GraphicsDevice.Clear(Color.White);
this.SpriteBatch.Begin(SpriteSortMode.Deferred, null, SamplerState.PointClamp, transformMatrix: Matrix.CreateScale(14));
this.SpriteBatch.Begin(SpriteSortMode.Deferred, null, SamplerState.PointClamp, null, null, null, Matrix.CreateScale(14));
var tex = this.SpriteBatch.GetBlankTexture();
// draw the world with simple shapes
for (var x = 0; x < 50; x++) {
@ -87,9 +87,9 @@ namespace Demos {
// in a real game, you'd obviously make your characters walk along the path instead of drawing it
if (this.path != null) {
for (var i = 1; i < this.path.Count; i++) {
var (firstX, firstY) = this.path[i - 1];
var (secondX, secondY) = this.path[i];
this.SpriteBatch.Draw(tex, RectangleF.FromCorners(new Vector2(firstX + 0.25F, firstY + 0.25F), new Vector2(secondX + 0.75F, secondY + 0.75F)), Color.Blue);
var first = this.path[i - 1];
var second = this.path[i];
this.SpriteBatch.Draw(tex, RectangleF.FromCorners(new Vector2(first.X + 0.25F, first.Y + 0.25F), new Vector2(second.X + 0.75F, second.Y + 0.75F)), Color.Blue);
}
}
this.SpriteBatch.End();

View file

@ -0,0 +1,81 @@
using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using MLEM.Font;
using MLEM.Formatting;
using MLEM.Formatting.Codes;
using MLEM.Startup;
using MLEM.Textures;
namespace Demos {
public class TextFormattingDemo : Demo {
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" +
"You can write in <b>bold</i>, <i>italics</i>, <u>with an underline</u>, <st>strikethrough</st>, with a <s #000000 4>drop shadow</s> whose <s #ff0000 4>color</s> and <s #000000 10>offset</s> you can modify in each application of the code, or with various types of <b>combined <c Pink>formatting</c> codes</b>.\n\n" +
"You can apply <c CornflowerBlue>custom</c> <c Yellow>colors</c> to text, including all default <c Orange>MonoGame colors</c> and <c #aabb00>inline custom colors</c>.\n\n" +
"You can also use animations like <a wobbly>a wobbly one</a>, as well as create custom ones using the <a wobbly>Code class</a>.\n\n" +
"You can also display <i grass> icons in your text!\n\n" +
"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 Width = 0.9F;
private TextFormatter formatter;
private TokenizedString tokenizedText;
private GenericFont font;
public TextFormattingDemo(MlemGame game) : base(game) {}
public override void LoadContent() {
this.Game.Window.ClientSizeChanged += this.OnResize;
// creating a new text formatter as well as a generic font to draw with
this.formatter = new TextFormatter();
// 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
this.font = new GenericSpriteFont(
Demo.LoadContent<SpriteFont>("Fonts/TestFont"),
Demo.LoadContent<SpriteFont>("Fonts/TestFontBold"),
Demo.LoadContent<SpriteFont>("Fonts/TestFontItalic"));
// adding the image code used in the example to it
var testTexture = Demo.LoadContent<Texture2D>("Textures/Test");
this.formatter.AddImage("grass", new TextureRegion(testTexture, 0, 0, 8, 8));
// 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
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);
}
public override void DoDraw(GameTime time) {
this.GraphicsDevice.Clear(Color.DarkSlateGray);
this.SpriteBatch.Begin(SpriteSortMode.Deferred, null, SamplerState.PointClamp, null, null);
// 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
var size = this.tokenizedText.Measure(this.font) * TextFormattingDemo.Scale;
var pos = new Vector2(this.GraphicsDevice.Viewport.Width / 2, (this.GraphicsDevice.Viewport.Height - size.Y) / 2);
this.tokenizedText.Draw(time, this.SpriteBatch, pos, this.font, Color.White, TextFormattingDemo.Scale, 0);
this.SpriteBatch.End();
}
public override void Update(GameTime time) {
// update our tokenized string to animate the animation codes
this.tokenizedText.Update(time);
}
public override void Clear() {
base.Clear();
this.Game.Window.ClientSizeChanged -= this.OnResize;
}
private void OnResize(object sender, EventArgs e) {
// 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.tokenizedText.Split(this.font, this.GraphicsDevice.Viewport.Width * TextFormattingDemo.Width, TextFormattingDemo.Scale, TextAlignment.Center);
}
}
}

View file

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Coroutine;
using Microsoft.Xna.Framework;
@ -7,13 +8,13 @@ using Microsoft.Xna.Framework.Graphics;
using MLEM.Extensions;
using MLEM.Font;
using MLEM.Formatting;
using MLEM.Formatting.Codes;
using MLEM.Input;
using MLEM.Misc;
using MLEM.Startup;
using MLEM.Textures;
using MLEM.Ui;
using MLEM.Ui.Elements;
using MLEM.Ui.Parsers;
using MLEM.Ui.Style;
namespace Demos {
@ -26,7 +27,7 @@ namespace Demos {
public UiDemo(MlemGame game) : base(game) {}
public override void LoadContent() {
this.testTexture = LoadContent<Texture2D>("Textures/Test");
this.testTexture = Demo.LoadContent<Texture2D>("Textures/Test");
this.testPatch = new NinePatch(new TextureRegion(this.testTexture, 0, 8, 24, 24), 8);
base.LoadContent();
@ -36,7 +37,10 @@ namespace Demos {
// when using a SpriteFont, use GenericSpriteFont. When using a MonoGame.Extended BitmapFont, use GenericBitmapFont.
// Wrapping fonts like this allows for both types to be usable within MLEM.Ui easily
// Supplying a bold and an italic version is optional
Font = new GenericSpriteFont(LoadContent<SpriteFont>("Fonts/TestFont"), LoadContent<SpriteFont>("Fonts/TestFontBold"), LoadContent<SpriteFont>("Fonts/TestFontItalic")),
Font = new GenericSpriteFont(
Demo.LoadContent<SpriteFont>("Fonts/TestFont"),
Demo.LoadContent<SpriteFont>("Fonts/TestFontBold"),
Demo.LoadContent<SpriteFont>("Fonts/TestFontItalic")),
TextScale = 0.1F,
PanelTexture = this.testPatch,
ButtonTexture = new NinePatch(new TextureRegion(this.testTexture, 24, 8, 16, 16), 4),
@ -47,7 +51,7 @@ namespace Demos {
CheckboxCheckmark = new TextureRegion(this.testTexture, 24, 0, 8, 8),
RadioTexture = new NinePatch(new TextureRegion(this.testTexture, 16, 0, 8, 8), 3),
RadioCheckmark = new TextureRegion(this.testTexture, 32, 0, 8, 8),
AdditionalFonts = {{"Monospaced", new GenericSpriteFont(LoadContent<SpriteFont>("Fonts/MonospacedFont"))}},
AdditionalFonts = {{"Monospaced", new GenericSpriteFont(Demo.LoadContent<SpriteFont>("Fonts/MonospacedFont"))}},
LinkColor = Color.CornflowerBlue
};
var untexturedStyle = new UntexturedStyle(this.SpriteBatch) {
@ -69,7 +73,7 @@ namespace Demos {
// add the root to the demos' ui
this.UiRoot.AddChild(this.root);
this.root.AddChild(new Paragraph(Anchor.AutoLeft, 1, "This is a small demo for MLEM.Ui, a user interface library that is part of the MLEM Library for Extending MonoGame."));
this.root.AddChild(new Paragraph(Anchor.AutoLeft, 1, "This is a small demo for MLEM.Ui, a user interface library that is part of the MLEM Library for Extending MonoGame and FNA."));
var image = this.root.AddChild(new Image(Anchor.AutoCenter, new Vector2(50, 50), new TextureRegion(this.testTexture, 0, 0, 8, 8)) {IsHidden = true, Padding = new Padding(3)});
// Setting the x or y coordinate of the size to 1 or a lower number causes the width or height to be a percentage of the parent's width or height
// (for example, setting the size's x to 0.75 would make the element's width be 0.75*parentWidth)
@ -88,12 +92,7 @@ namespace Demos {
this.root.AddChild(new VerticalSpace(3));
// a paragraph with formatting codes. To see them all or to add more, check the TextFormatting class
this.root.AddChild(new Paragraph(Anchor.AutoLeft, 1, "Paragraphs can also contain <c Blue>formatting codes</c>, including colors and <i>text styles</i>. The names of all <c Orange>MonoGame Colors</c> can be used, as well as the codes <i>Italic</i>, <b>Bold</b>, <s>Drop Shadow'd</s> and <s><c Pink>mixed formatting</s></c>. You can also add additional fonts for things like\n<f Monospaced>void Code() {\n // Code\n}</f>\n<i>Even <c #ff611f82>inline custom colors</c> work!</i>"));
// adding some custom image formatting codes
this.root.AddChild(new Paragraph(Anchor.AutoLeft, 1, "Additionally, you can create custom formatting codes that contain <i Grass> images and more!"));
this.UiSystem.TextFormatter.AddImage("Grass", image.Texture);
this.root.AddChild(new Paragraph(Anchor.AutoLeft, 1, "Defining text animations as formatting codes is also possible, including <a wobbly>wobbly text</a> at <a wobbly 8 0.25>different intensities</a>. Of course, more animations can be added though."));
this.root.AddChild(new Paragraph(Anchor.AutoLeft, 1, "Paragraphs can also contain <c Blue>formatting codes</c>, including colors and <i>text styles</i>. For more info, check out the <b>text formatting demo</b>!"));
this.root.AddChild(new VerticalSpace(3));
this.root.AddChild(new Paragraph(Anchor.AutoCenter, 1, "Multiline text input:", true));
@ -156,7 +155,7 @@ namespace Demos {
// Check the WobbleButton method for an explanation of how this button works
this.root.AddChild(new Button(Anchor.AutoCenter, new Vector2(0.5F, 10), "Wobble Me", "This button wobbles around visually when clicked, but this doesn't affect its actual size and positioning") {
OnPressed = element => CoroutineHandler.Start(WobbleButton(element)),
OnPressed = element => CoroutineHandler.Start(UiDemo.WobbleButton(element)),
PositionOffset = new Vector2(0, 1)
});
// Another button that shows animations!
@ -189,13 +188,13 @@ namespace Demos {
this.root.AddChild(new VerticalSpace(3));
this.root.AddChild(new Paragraph(Anchor.AutoLeft, 1, "Progress bars!"));
var bar1 = this.root.AddChild(new ProgressBar(Anchor.AutoLeft, new Vector2(1, 8), Direction2.Right, 10) {PositionOffset = new Vector2(0, 1)});
CoroutineHandler.Start(WobbleProgressBar(bar1));
CoroutineHandler.Start(UiDemo.WobbleProgressBar(bar1));
var bar2 = this.root.AddChild(new ProgressBar(Anchor.AutoLeft, new Vector2(1, 8), Direction2.Left, 10) {PositionOffset = new Vector2(0, 1)});
CoroutineHandler.Start(WobbleProgressBar(bar2));
CoroutineHandler.Start(UiDemo.WobbleProgressBar(bar2));
var bar3 = this.root.AddChild(new ProgressBar(Anchor.AutoLeft, new Vector2(8, 30), Direction2.Down, 10) {PositionOffset = new Vector2(0, 1)});
CoroutineHandler.Start(WobbleProgressBar(bar3));
CoroutineHandler.Start(UiDemo.WobbleProgressBar(bar3));
var bar4 = this.root.AddChild(new ProgressBar(Anchor.AutoInline, new Vector2(8, 30), Direction2.Up, 10) {PositionOffset = new Vector2(1, 0)});
CoroutineHandler.Start(WobbleProgressBar(bar4));
CoroutineHandler.Start(UiDemo.WobbleProgressBar(bar4));
this.root.AddChild(new VerticalSpace(3));
var dropdown = this.root.AddChild(new Dropdown(Anchor.AutoLeft, new Vector2(1, 10), "Dropdown Menu"));
@ -223,6 +222,13 @@ namespace Demos {
alignPar.Alignment = alignment;
};
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 VerticalSpace(3));
var parser = new UiMarkdownParser {GraphicsDevice = this.GraphicsDevice};
using (var reader = new StreamReader(TitleContainer.OpenStream("Content/Markdown.md")))
parser.ParseInto(reader.ReadToEnd(), this.root);
this.root.AddChild(new VerticalSpace(3));
this.root.AddChild(new Paragraph(Anchor.AutoLeft, 1, "The code for this demo contains some examples for how to query element data. This is the output of that:"));

View file

@ -1,6 +1,6 @@
# MLEM.Ui
**MLEM.Ui** is a Ui framework for MonoGame that features elements with automatic positioning and sizing. It contains various ready-made element types like buttons, paragraphs, text fields and more, along with the ability to easily create custom controls. It supports **mouse**, **keyboard**, **gamepad** and **touch** input with little to no additional setup work required.
**MLEM.Ui** is a Ui framework for MonoGame and FNA that features elements with automatic positioning and sizing. It contains various ready-made element types like buttons, paragraphs, text fields and more, along with the ability to easily create custom controls. It supports **mouse**, **keyboard**, **gamepad** and **touch** input with little to no additional setup work required.
To see some of what MLEM.Ui can do, you can check out [the demo](https://github.com/Ellpeck/MLEM/blob/main/Demos/UiDemo.cs) as well.
@ -35,15 +35,19 @@ On desktop devices, MonoGame provides the `Window.TextInput` event that gets cal
To make MLEM compatible with all devices without publishing a separate version for each MonoGame platform, you have to set up the `MlemPlatform` class based on the system you're using MLEM.Ui with. This has to be done *before* initializing your `UiSystem`.
DesktopGL:
DesktopGL and WindowsDX using MonoGame:
```cs
MlemPlatform.Current = new MlemPlatform.DesktopGl<TextInputEventArgs>((w, c) => w.TextInput += c);
```
Desktop using FNA:
```cs
MlemPlatform.Current = new MlemPlatform.DesktopFna(a => TextInputEXT.TextInput += a);
```
Mobile devices and consoles:
```cs
MlemPlatform.Current = new MlemPlatform.Mobile(KeyboardInput.Show, l => this.StartActivity(new Intent(Intent.ActionView, Uri.Parse(l))));
```
If you're not using text input, you can just set the platform to a stub one like so:
If you're not using text input, you can leave the platform uninitialized or just set it to a stub one like so:
```cs
MlemPlatform.Current = new MlemPlatform.None();
```

View file

@ -1,27 +1,27 @@
![The MLEM logo](https://raw.githubusercontent.com/Ellpeck/MLEM/release/Media/Banner.png)
**MLEM Library for Extending MonoGame** is an addition to the game framework [MonoGame](https://www.monogame.net/) that provides extension methods, quality of life improvements and additional features like a ui system and easy input handling.
**MLEM Library for Extending MonoGame and FNA** is a set of multipurpose libraries for the game frameworks [MonoGame](https://www.monogame.net/) and [FNA](https://fna-xna.github.io/) that provides abstractions, quality of life improvements and additional features like an extensive ui system and easy input handling.
# What next?
- Get it on [NuGet](https://www.nuget.org/packages?q=mlem)
- Get prerelease builds on [BaGet](https://nuget.ellpeck.de)
- 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 extension methods and additional features for MonoGame
- **MLEM.Ui** features 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 libraries
- **MLEM.Data** provides simple loading and processing of textures and data
- **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))
- [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!
@ -38,9 +38,9 @@ MLEM's [text formatting system](https://mlem.ellpeck.de/articles/text_formatting
![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 and MLEM. Here are some of them:
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 Core applications into several distributable formats
- [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

1
FNA Submodule

@ -0,0 +1 @@
Subproject commit 102990f514f1e5bfac07d33f7c33e2e712946da4

1
FontStashSharp Submodule

@ -0,0 +1 @@
Subproject commit f0774130cad6cec0b790a58bc7c811a186443fb3

5
Jenkinsfile vendored
View file

@ -1,6 +1,11 @@
pipeline {
agent any
stages {
stage('Submodules') {
steps {
sh 'git submodule update --init --recursive --force'
}
}
stage('Cake Build') {
steps {
sh 'dotnet tool restore'

View file

@ -13,13 +13,16 @@ namespace MLEM.Data.Content {
private static List<RawContentReader> readers;
private readonly List<IDisposable> disposableAssets = new List<IDisposable>();
/// <summary>
/// The graphics device that this content manager uses
/// </summary>
public readonly GraphicsDevice GraphicsDevice;
private readonly List<IDisposable> disposableAssets = new List<IDisposable>();
#if FNA
private Dictionary<string, object> LoadedAssets { get; } = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
#endif
/// <summary>
/// Creates a new content manager with an optionally specified root directory.
/// </summary>
@ -50,15 +53,19 @@ namespace MLEM.Data.Content {
/// <param name="originalAssetName">The original name of the asset.</param>
/// <param name="currentAsset">The current asset instance.</param>
/// <typeparam name="T">The asset's type.</typeparam>
protected override void ReloadAsset<T>(string originalAssetName, T currentAsset) {
protected
#if !FNA
override
#endif
void ReloadAsset<T>(string originalAssetName, T currentAsset) {
this.Read(originalAssetName, currentAsset);
}
private T Read<T>(string assetName, T existing) {
var triedFiles = new List<string>();
if (readers == null)
readers = CollectContentReaders();
foreach (var reader in readers) {
if (RawContentManager.readers == null)
RawContentManager.readers = RawContentManager.CollectContentReaders();
foreach (var reader in RawContentManager.readers) {
if (!reader.CanRead(typeof(T)))
continue;
foreach (var ext in reader.GetFileExtensions()) {

View file

@ -12,7 +12,7 @@ namespace MLEM.Data.Content {
/// <inheritdoc />
public override string[] GetFileExtensions() {
return new[] {"ogg", "wav", "mp3"};
return new[] {"wav"};
}
}

View file

@ -1,5 +1,7 @@
using System.IO;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using MLEM.Extensions;
namespace MLEM.Data.Content {
/// <inheritdoc />
@ -7,11 +9,26 @@ namespace MLEM.Data.Content {
/// <inheritdoc />
protected override Texture2D Read(RawContentManager manager, string assetPath, Stream stream, Texture2D existing) {
#if !FNA
if (existing != null) {
existing.Reload(stream);
return existing;
} else {
return Texture2D.FromStream(manager.GraphicsDevice, stream);
} else
#endif
{
// premultiply the texture's color to be in line with the pipeline's texture reader
using (var texture = Texture2D.FromStream(manager.GraphicsDevice, stream)) {
var ret = new Texture2D(manager.GraphicsDevice, texture.Width, texture.Height);
using (var textureData = texture.GetTextureData()) {
using (var retData = ret.GetTextureData()) {
for (var x = 0; x < ret.Width; x++) {
for (var y = 0; y < ret.Height; y++)
retData[x, y] = Color.FromNonPremultiplied(textureData[x, y].ToVector4());
}
}
}
return ret;
}
}
}

View file

@ -21,7 +21,7 @@ namespace MLEM.Data {
/// <param name="content">The content manager to add the json serializer to</param>
/// <param name="serializer">The json serializer to add</param>
public static void SetJsonSerializer(this ContentManager content, JsonSerializer serializer) {
Serializers[content] = serializer;
ContentExtensions.Serializers[content] = serializer;
}
/// <summary>
@ -31,7 +31,7 @@ namespace MLEM.Data {
/// <param name="content">The content manager whose serializer to get</param>
/// <returns>The content manager's serializer</returns>
public static JsonSerializer GetJsonSerializer(this ContentManager content) {
if (!Serializers.TryGetValue(content, out var serializer)) {
if (!ContentExtensions.Serializers.TryGetValue(content, out var serializer)) {
serializer = JsonConverters.AddAll(new JsonSerializer());
content.SetJsonSerializer(serializer);
}
@ -44,7 +44,7 @@ namespace MLEM.Data {
/// <param name="content">The content manager to add the converter to</param>
/// <param name="converter">The converter to add</param>
public static void AddJsonConverter(this ContentManager content, JsonConverter converter) {
var serializer = GetJsonSerializer(content);
var serializer = content.GetJsonSerializer();
serializer.Converters.Add(converter);
}
@ -60,7 +60,7 @@ namespace MLEM.Data {
public static T LoadJson<T>(this ContentManager content, string name, string[] extensions = null, JsonSerializer serializer = null) {
var triedFiles = new List<string>();
var serializerToUse = serializer ?? content.GetJsonSerializer();
foreach (var extension in extensions ?? JsonExtensions) {
foreach (var extension in extensions ?? ContentExtensions.JsonExtensions) {
var file = Path.Combine(content.RootDirectory, name + extension);
triedFiles.Add(file);
try {

View file

@ -23,8 +23,8 @@ namespace MLEM.Data {
/// <typeparam name="T">The type of the object to copy</typeparam>
/// <returns>A shallow copy of the object</returns>
[Obsolete("CopyExtensions has major flaws and insufficient speed compared to other libraries specifically designed for copying objects.")]
public static T Copy<T>(this T obj, BindingFlags flags = DefaultFlags, Predicate<FieldInfo> fieldInclusion = null) {
var copy = (T) Construct(typeof(T), flags);
public static T Copy<T>(this T obj, BindingFlags flags = CopyExtensions.DefaultFlags, Predicate<FieldInfo> fieldInclusion = null) {
var copy = (T) CopyExtensions.Construct(typeof(T), flags);
obj.CopyInto(copy, flags, fieldInclusion);
return copy;
}
@ -39,8 +39,8 @@ namespace MLEM.Data {
/// <typeparam name="T">The type of the object to copy</typeparam>
/// <returns>A deep copy of the object</returns>
[Obsolete("CopyExtensions has major flaws and insufficient speed compared to other libraries specifically designed for copying objects.")]
public static T DeepCopy<T>(this T obj, BindingFlags flags = DefaultFlags, Predicate<FieldInfo> fieldInclusion = null) {
var copy = (T) Construct(typeof(T), flags);
public static T DeepCopy<T>(this T obj, BindingFlags flags = CopyExtensions.DefaultFlags, Predicate<FieldInfo> fieldInclusion = null) {
var copy = (T) CopyExtensions.Construct(typeof(T), flags);
obj.DeepCopyInto(copy, flags, fieldInclusion);
return copy;
}
@ -54,7 +54,7 @@ namespace MLEM.Data {
/// <param name="fieldInclusion">A predicate that determines whether or not the given field should be copied. If null, all fields will be copied.</param>
/// <typeparam name="T">The type of the object to copy</typeparam>
[Obsolete("CopyExtensions has major flaws and insufficient speed compared to other libraries specifically designed for copying objects.")]
public static void CopyInto<T>(this T obj, T otherObj, BindingFlags flags = DefaultFlags, Predicate<FieldInfo> fieldInclusion = null) {
public static void CopyInto<T>(this T obj, T otherObj, BindingFlags flags = CopyExtensions.DefaultFlags, Predicate<FieldInfo> fieldInclusion = null) {
foreach (var field in typeof(T).GetFields(flags)) {
if (fieldInclusion == null || fieldInclusion(field))
field.SetValue(otherObj, field.GetValue(obj));
@ -71,7 +71,7 @@ namespace MLEM.Data {
/// <param name="fieldInclusion">A predicate that determines whether or not the given field should be copied. If null, all fields will be copied.</param>
/// <typeparam name="T">The type of the object to copy</typeparam>
[Obsolete("CopyExtensions has major flaws and insufficient speed compared to other libraries specifically designed for copying objects.")]
public static void DeepCopyInto<T>(this T obj, T otherObj, BindingFlags flags = DefaultFlags, Predicate<FieldInfo> fieldInclusion = null) {
public static void DeepCopyInto<T>(this T obj, T otherObj, BindingFlags flags = CopyExtensions.DefaultFlags, Predicate<FieldInfo> fieldInclusion = null) {
foreach (var field in obj.GetType().GetFields(flags)) {
if (fieldInclusion != null && !fieldInclusion(field))
continue;
@ -83,7 +83,7 @@ namespace MLEM.Data {
var otherVal = field.GetValue(otherObj);
// if the object we want to copy into doesn't have a value yet, we create one
if (otherVal == null) {
otherVal = Construct(field.FieldType, flags);
otherVal = CopyExtensions.Construct(field.FieldType, flags);
field.SetValue(otherObj, otherVal);
}
val.DeepCopyInto(otherVal, flags);
@ -92,7 +92,7 @@ namespace MLEM.Data {
}
private static object Construct(Type t, BindingFlags flags) {
if (!ConstructorCache.TryGetValue(t, out var constructor)) {
if (!CopyExtensions.ConstructorCache.TryGetValue(t, out var constructor)) {
var constructors = t.GetConstructors(flags);
// find a contructor with the correct attribute
constructor = constructors.FirstOrDefault(c => c.GetCustomAttribute<CopyConstructorAttribute>() != null);
@ -104,7 +104,7 @@ namespace MLEM.Data {
constructor = constructors.FirstOrDefault();
if (constructor == null)
throw new NullReferenceException($"Type {t} does not have a constructor with the required visibility");
ConstructorCache.Add(t, constructor);
CopyExtensions.ConstructorCache.Add(t, constructor);
}
return constructor.Invoke(new object[constructor.GetParameters().Length]);
}

View file

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
@ -5,6 +6,7 @@ using System.Text.RegularExpressions;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using MLEM.Extensions;
using MLEM.Textures;
namespace MLEM.Data {
@ -50,9 +52,13 @@ namespace MLEM.Data {
}
}
/// <summary>
/// Returns an enumerable of all of the <see cref="TextureRegion"/>s in this atlas.
/// Returns an enumerable of all of the <see cref="TextureRegion"/> values in this atlas.
/// </summary>
public IEnumerable<TextureRegion> Regions => this.regions.Values;
/// <summary>
/// Returns an enumerable of all of the <see cref="TextureRegion"/> names in this atlas.
/// </summary>
public IEnumerable<string> RegionNames => this.regions.Keys;
private readonly Dictionary<string, TextureRegion> regions = new Dictionary<string, TextureRegion>();
@ -79,9 +85,8 @@ namespace MLEM.Data {
}
var atlas = new DataTextureAtlas(texture);
// parse each texture region: "<name> loc <u> <v> <w> <h> [piv <px> <py>] [off <ox> <oy>]"
// parse each texture region: "<names> loc <u> <v> <w> <h> [piv <px> <py>] [off <ox> <oy>]"
foreach (Match match in Regex.Matches(text, @"(.+)\W+loc\W+([0-9]+)\W+([0-9]+)\W+([0-9]+)\W+([0-9]+)\W*(?:piv\W+([0-9.]+)\W+([0-9.]+))?\W*(?:off\W+([0-9.]+)\W+([0-9.]+))?")) {
var name = match.Groups[1].Value.Trim();
// offset
var off = !match.Groups[8].Success ? Vector2.Zero : new Vector2(
float.Parse(match.Groups[8].Value, CultureInfo.InvariantCulture),
@ -91,18 +96,23 @@ namespace MLEM.Data {
var loc = new Rectangle(
int.Parse(match.Groups[2].Value), int.Parse(match.Groups[3].Value),
int.Parse(match.Groups[4].Value), int.Parse(match.Groups[5].Value));
loc.Offset(off);
loc.Offset(off.ToPoint());
// pivot
var piv = !match.Groups[6].Success ? Vector2.Zero : off + new Vector2(
float.Parse(match.Groups[6].Value, CultureInfo.InvariantCulture) - (pivotRelative ? 0 : loc.X),
float.Parse(match.Groups[7].Value, CultureInfo.InvariantCulture) - (pivotRelative ? 0 : loc.Y));
foreach (var name in Regex.Split(match.Groups[1].Value, @"\W")) {
var trimmed = name.Trim();
if (trimmed.Length <= 0)
continue;
var region = new TextureRegion(texture, loc) {
PivotPixels = piv,
Name = name
Name = trimmed
};
atlas.regions.Add(name, region);
atlas.regions.Add(trimmed, region);
}
}
return atlas;

View file

@ -60,7 +60,7 @@ namespace MLEM.Data {
if (this.allFlagsCache == null)
this.allFlagsCache = new Dictionary<DynamicEnum, bool>();
if (!this.allFlagsCache.TryGetValue(flags, out var ret)) {
ret = (GetValue(this) & GetValue(flags)) == GetValue(flags);
ret = (DynamicEnum.GetValue(this) & DynamicEnum.GetValue(flags)) == DynamicEnum.GetValue(flags);
this.allFlagsCache.Add(flags, ret);
}
return ret;
@ -76,7 +76,7 @@ namespace MLEM.Data {
if (this.anyFlagsCache == null)
this.anyFlagsCache = new Dictionary<DynamicEnum, bool>();
if (!this.anyFlagsCache.TryGetValue(flags, out var ret)) {
ret = (GetValue(this) & GetValue(flags)) != 0;
ret = (DynamicEnum.GetValue(this) & DynamicEnum.GetValue(flags)) != 0;
this.anyFlagsCache.Add(flags, ret);
}
return ret;
@ -87,13 +87,13 @@ namespace MLEM.Data {
public override string ToString() {
if (this.name == null) {
var included = new List<DynamicEnum>();
if (GetValue(this) != 0) {
foreach (var v in GetValues(this.GetType())) {
if (this.HasFlag(v) && GetValue(v) != 0)
if (DynamicEnum.GetValue(this) != 0) {
foreach (var v in DynamicEnum.GetValues(this.GetType())) {
if (this.HasFlag(v) && DynamicEnum.GetValue(v) != 0)
included.Add(v);
}
}
this.name = included.Count > 0 ? string.Join(" | ", included) : GetValue(this).ToString();
this.name = included.Count > 0 ? string.Join(" | ", included) : DynamicEnum.GetValue(this).ToString();
}
return this.name;
}
@ -107,7 +107,7 @@ namespace MLEM.Data {
/// <returns>The newly created enum value</returns>
/// <exception cref="ArgumentException">Thrown if the name or value passed are already present</exception>
public static T Add<T>(string name, BigInteger value) where T : DynamicEnum {
var storage = GetStorage(typeof(T));
var storage = DynamicEnum.GetStorage(typeof(T));
// cached parsed values and names might be incomplete with new values
storage.ClearCaches();
@ -119,7 +119,7 @@ namespace MLEM.Data {
throw new ArgumentException($"Duplicate name {name}", nameof(name));
}
var ret = Construct(typeof(T), name, value);
var ret = DynamicEnum.Construct(typeof(T), name, value);
storage.Values.Add(value, ret);
return (T) ret;
}
@ -134,9 +134,9 @@ namespace MLEM.Data {
/// <returns>The newly created enum value</returns>
public static T AddValue<T>(string name) where T : DynamicEnum {
BigInteger value = 0;
while (GetStorage(typeof(T)).Values.ContainsKey(value))
while (DynamicEnum.GetStorage(typeof(T)).Values.ContainsKey(value))
value++;
return Add<T>(name, value);
return DynamicEnum.Add<T>(name, value);
}
/// <summary>
@ -149,9 +149,9 @@ namespace MLEM.Data {
/// <returns>The newly created enum value</returns>
public static T AddFlag<T>(string name) where T : DynamicEnum {
BigInteger value = 1;
while (GetStorage(typeof(T)).Values.ContainsKey(value))
while (DynamicEnum.GetStorage(typeof(T)).Values.ContainsKey(value))
value <<= 1;
return Add<T>(name, value);
return DynamicEnum.Add<T>(name, value);
}
/// <summary>
@ -161,7 +161,7 @@ namespace MLEM.Data {
/// <typeparam name="T">The type whose values to get</typeparam>
/// <returns>The defined values for the given type</returns>
public static IEnumerable<T> GetValues<T>() where T : DynamicEnum {
return GetValues(typeof(T)).Cast<T>();
return DynamicEnum.GetValues(typeof(T)).Cast<T>();
}
/// <summary>
@ -171,7 +171,7 @@ namespace MLEM.Data {
/// <param name="type">The type whose values to get</param>
/// <returns>The defined values for the given type</returns>
public static IEnumerable<DynamicEnum> GetValues(Type type) {
return GetStorage(type).Values.Values;
return DynamicEnum.GetStorage(type).Values.Values;
}
/// <summary>
@ -182,9 +182,9 @@ namespace MLEM.Data {
/// <typeparam name="T">The type of the values</typeparam>
/// <returns>The bitwise OR (|) combination</returns>
public static T Or<T>(T left, T right) where T : DynamicEnum {
var cache = GetStorage(typeof(T)).OrCache;
var cache = DynamicEnum.GetStorage(typeof(T)).OrCache;
if (!cache.TryGetValue((left, right), out var ret)) {
ret = GetEnumValue<T>(GetValue(left) | GetValue(right));
ret = DynamicEnum.GetEnumValue<T>(DynamicEnum.GetValue(left) | DynamicEnum.GetValue(right));
cache.Add((left, right), ret);
}
return (T) ret;
@ -198,9 +198,9 @@ namespace MLEM.Data {
/// <typeparam name="T">The type of the values</typeparam>
/// <returns>The bitwise AND (&amp;) combination</returns>
public static T And<T>(T left, T right) where T : DynamicEnum {
var cache = GetStorage(typeof(T)).AndCache;
var cache = DynamicEnum.GetStorage(typeof(T)).AndCache;
if (!cache.TryGetValue((left, right), out var ret)) {
ret = GetEnumValue<T>(GetValue(left) & GetValue(right));
ret = DynamicEnum.GetEnumValue<T>(DynamicEnum.GetValue(left) & DynamicEnum.GetValue(right));
cache.Add((left, right), ret);
}
return (T) ret;
@ -214,9 +214,9 @@ namespace MLEM.Data {
/// <typeparam name="T">The type of the values</typeparam>
/// <returns>The bitwise XOR (^) combination</returns>
public static T Xor<T>(T left, T right) where T : DynamicEnum {
var cache = GetStorage(typeof(T)).XorCache;
var cache = DynamicEnum.GetStorage(typeof(T)).XorCache;
if (!cache.TryGetValue((left, right), out var ret)) {
ret = GetEnumValue<T>(GetValue(left) ^ GetValue(right));
ret = DynamicEnum.GetEnumValue<T>(DynamicEnum.GetValue(left) ^ DynamicEnum.GetValue(right));
cache.Add((left, right), ret);
}
return (T) ret;
@ -229,9 +229,9 @@ namespace MLEM.Data {
/// <typeparam name="T">The type of the values</typeparam>
/// <returns>The bitwise NEG (~) value</returns>
public static T Neg<T>(T value) where T : DynamicEnum {
var cache = GetStorage(typeof(T)).NegCache;
var cache = DynamicEnum.GetStorage(typeof(T)).NegCache;
if (!cache.TryGetValue(value, out var ret)) {
ret = GetEnumValue<T>(~GetValue(value));
ret = DynamicEnum.GetEnumValue<T>(~DynamicEnum.GetValue(value));
cache.Add(value, ret);
}
return (T) ret;
@ -253,7 +253,7 @@ namespace MLEM.Data {
/// <typeparam name="T">The type that the returned dynamic enum should have</typeparam>
/// <returns>The defined or combined dynamic enum value</returns>
public static T GetEnumValue<T>(BigInteger value) where T : DynamicEnum {
return (T) GetEnumValue(typeof(T), value);
return (T) DynamicEnum.GetEnumValue(typeof(T), value);
}
/// <summary>
@ -263,7 +263,7 @@ namespace MLEM.Data {
/// <param name="value">The value whose dynamic enum value to get</param>
/// <returns>The defined or combined dynamic enum value</returns>
public static DynamicEnum GetEnumValue(Type type, BigInteger value) {
var storage = GetStorage(type);
var storage = DynamicEnum.GetStorage(type);
// get the defined value if it exists
if (storage.Values.TryGetValue(value, out var defined))
@ -271,7 +271,7 @@ namespace MLEM.Data {
// otherwise, cache the combined value
if (!storage.FlagCache.TryGetValue(value, out var combined)) {
combined = Construct(type, null, value);
combined = DynamicEnum.Construct(type, null, value);
storage.FlagCache.Add(value, combined);
}
return combined;
@ -286,7 +286,7 @@ namespace MLEM.Data {
/// <typeparam name="T">The type of the dynamic enum value to parse</typeparam>
/// <returns>The parsed enum value, or null if parsing fails</returns>
public static T Parse<T>(string strg) where T : DynamicEnum {
return (T) Parse(typeof(T), strg);
return (T) DynamicEnum.Parse(typeof(T), strg);
}
/// <summary>
@ -297,28 +297,28 @@ namespace MLEM.Data {
/// <param name="strg">The string to parse into a dynamic enum value</param>
/// <returns>The parsed enum value, or null if parsing fails</returns>
public static DynamicEnum Parse(Type type, string strg) {
var cache = GetStorage(type).ParseCache;
var cache = DynamicEnum.GetStorage(type).ParseCache;
if (!cache.TryGetValue(strg, out var cached)) {
BigInteger? accum = null;
foreach (var val in strg.Split('|')) {
foreach (var defined in GetValues(type)) {
foreach (var defined in DynamicEnum.GetValues(type)) {
if (defined.name == val.Trim()) {
accum = (accum ?? 0) | GetValue(defined);
accum = (accum ?? 0) | DynamicEnum.GetValue(defined);
break;
}
}
}
if (accum != null)
cached = GetEnumValue(type, accum.Value);
cached = DynamicEnum.GetEnumValue(type, accum.Value);
cache.Add(strg, cached);
}
return cached;
}
private static Storage GetStorage(Type type) {
if (!Storages.TryGetValue(type, out var storage)) {
if (!DynamicEnum.Storages.TryGetValue(type, out var storage)) {
storage = new Storage();
Storages.Add(type, storage);
DynamicEnum.Storages.Add(type, storage);
}
return storage;
}

View file

@ -21,7 +21,7 @@ namespace MLEM.Data.Json {
/// <param name="serializer">The serializer to add the converters to</param>
/// <returns>The given serializer, for chaining</returns>
public static JsonSerializer AddAll(JsonSerializer serializer) {
foreach (var converter in Converters)
foreach (var converter in JsonConverters.Converters)
serializer.Converters.Add(converter);
return serializer;
}

View file

@ -29,7 +29,7 @@ namespace MLEM.Data.Json {
/// </summary>
/// <param name="type">The type that the dictionary is declared in</param>
/// <param name="memberName">The name of the dictionary itself</param>
public StaticJsonConverter(Type type, string memberName) : this(GetEntries(type, memberName)) {}
public StaticJsonConverter(Type type, string memberName) : this(StaticJsonConverter<T>.GetEntries(type, memberName)) {}
/// <summary>Writes the JSON representation of the object.</summary>
/// <param name="writer">The <see cref="T:Newtonsoft.Json.JsonWriter" /> to write to.</param>

View file

@ -0,0 +1,45 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<ProduceReferenceAssembly>true</ProduceReferenceAssembly>
<RootNamespace>MLEM.Data</RootNamespace>
<DefineConstants>$(DefineConstants);FNA</DefineConstants>
<NoWarn>NU1701</NoWarn>
</PropertyGroup>
<PropertyGroup>
<Authors>Ellpeck</Authors>
<Description>Simple loading and processing of textures and other data for FNA, including the ability to load non-XNB content files easily</Description>
<PackageReleaseNotes>See the full changelog at https://mlem.ellpeck.de/CHANGELOG</PackageReleaseNotes>
<PackageTags>fna ellpeck mlem utility extensions data serialize</PackageTags>
<PackageProjectUrl>https://mlem.ellpeck.de/</PackageProjectUrl>
<RepositoryUrl>https://github.com/Ellpeck/MLEM</RepositoryUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageIcon>Logo.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\MLEM\MLEM.FNA.csproj" />
<!--TODO remove lidgren support eventually (methods marked as obsolete since 5.2.0)-->
<PackageReference Include="Lidgren.Network" Version="1.0.2">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Newtonsoft.Json.Bson" Version="1.0.2">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<ProjectReference Include="..\FNA\FNA.Core.csproj">
<PrivateAssets>all</PrivateAssets>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<None Include="../Media/Logo.png" Pack="true" PackagePath="" />
<None Include="../Docs/index.md" Pack="true" PackagePath="README.md" />
</ItemGroup>
</Project>

View file

@ -3,11 +3,12 @@
<TargetFramework>netstandard2.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<ProduceReferenceAssembly>true</ProduceReferenceAssembly>
<NoWarn>NU1701</NoWarn>
</PropertyGroup>
<PropertyGroup>
<Authors>Ellpeck</Authors>
<Description>Simple loading and processing of textures and data for MLEM Library for Extending MonoGame</Description>
<Description>Simple loading and processing of textures and other data for MonoGame, including the ability to load non-XNB content files easily</Description>
<PackageReleaseNotes>See the full changelog at https://mlem.ellpeck.de/CHANGELOG</PackageReleaseNotes>
<PackageTags>monogame ellpeck mlem utility extensions data serialize</PackageTags>
<PackageProjectUrl>https://mlem.ellpeck.de/</PackageProjectUrl>
@ -15,7 +16,6 @@
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageIcon>Logo.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile>
<NoWarn>NU1701</NoWarn>
</PropertyGroup>
<ItemGroup>

View file

@ -6,6 +6,7 @@ using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using MLEM.Extensions;
using MLEM.Textures;
using static MLEM.Extensions.TextureExtensions;
namespace MLEM.Data {
/// <summary>
@ -33,7 +34,10 @@ namespace MLEM.Data {
/// </summary>
public TimeSpan LastTotalTime => this.LastCalculationTime + this.LastPackTime;
private readonly List<Request> textures = new List<Request>();
private readonly List<Request> texturesToPack = new List<Request>();
private readonly List<Request> alreadyPackedTextures = new List<Request>();
private readonly Dictionary<Point, Point> firstPossiblePosForSizeCache = new Dictionary<Point, Point>();
private readonly Dictionary<Texture2D, TextureData> dataCache = new Dictionary<Texture2D, TextureData>();
private readonly bool autoIncreaseMaxWidth;
private readonly bool forcePowerOfTwo;
private readonly bool forceSquare;
@ -42,13 +46,13 @@ namespace MLEM.Data {
private int maxWidth;
/// <summary>
/// Creates a new runtime texture packer with the given settings
/// Creates a new runtime texture packer with the given settings.
/// </summary>
/// <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="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="disposeTextures">Whether the original textures submitted to this texture packer should be disposed after packing</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="disposeTextures">Whether the original textures submitted to this texture packer should be disposed after packing.</param>
public RuntimeTexturePacker(int maxWidth = 2048, bool autoIncreaseMaxWidth = false, bool forcePowerOfTwo = false, bool forceSquare = false, bool disposeTextures = false) {
this.maxWidth = maxWidth;
this.autoIncreaseMaxWidth = autoIncreaseMaxWidth;
@ -58,37 +62,99 @@ namespace MLEM.Data {
}
/// <summary>
/// Adds a new texture to this texture packer to be packed.
/// Adds a new <see cref="UniformTextureAtlas"/> to this texture packer to be packed.
/// The passed <see cref="Action{T}"/> is invoked in <see cref="Pack"/> and provides the caller with the resulting dictionary of texture regions on the <see cref="PackedTexture"/>, mapped to their x and y positions on the original <see cref="UniformTextureAtlas"/>.
/// Note that the resulting data cannot be converted back into a <see cref="UniformTextureAtlas"/>, since the resulting texture regions might be scattered throughout the <see cref="PackedTexture"/>.
/// </summary>
/// <param name="atlas">The texture atlas to pack.</param>
/// <param name="result">The result callback which will receive the resulting texture regions.</param>
/// <param name="padding">The padding that the texture should have around itself. This can be useful if texture bleeding issues occur due to texture coordinate rounding.</param>
/// <param name="padWithPixels">Whether the texture's padding should be filled with a copy of the texture's border, rather than transparent pixels. This value only has an effect if <paramref name="padding"/> is greater than 0.</param>
/// <param name="ignoreTransparent">Whether completely transparent texture regions in the <paramref name="atlas"/> should be ignored. If this is true, they will not be part of the <paramref name="result"/> collection either.</param>
/// <exception cref="InvalidOperationException">Thrown when trying to add data to a packer that has already been packed, or when trying to add a texture width a width greater than the defined max width.</exception>
public void Add(UniformTextureAtlas atlas, Action<Dictionary<Point, TextureRegion>> result, int padding = 0, bool padWithPixels = false, bool ignoreTransparent = false) {
var addedRegions = new List<TextureRegion>();
var resultRegions = new Dictionary<Point, TextureRegion>();
for (var x = 0; x < atlas.RegionAmountX; x++) {
for (var y = 0; y < atlas.RegionAmountY; y++) {
var pos = new Point(x, y);
var region = atlas[pos];
if (ignoreTransparent) {
if (this.IsTransparent(region))
continue;
}
this.Add(region, r => {
resultRegions.Add(pos, r);
if (resultRegions.Count >= addedRegions.Count)
result.Invoke(resultRegions);
}, padding, padWithPixels);
addedRegions.Add(region);
}
}
}
/// <summary>
/// Adds a new <see cref="DataTextureAtlas"/> to this texture packer to be packed.
/// The passed <see cref="Action{T}"/> is invoked in <see cref="Pack"/> and provides the caller with the resulting dictionary of texture regions on the <see cref="PackedTexture"/>, mapped to their name on the original <see cref="DataTextureAtlas"/>.
/// Note that the resulting data cannot be converted back into a <see cref="DataTextureAtlas"/>, since the resulting texture regions might be scattered throughout the <see cref="PackedTexture"/>.
/// </summary>
/// <param name="atlas">The texture atlas to pack.</param>
/// <param name="result">The result callback which will receive the resulting texture regions.</param>
/// <param name="padding">The padding that the texture should have around itself. This can be useful if texture bleeding issues occur due to texture coordinate rounding.</param>
/// <param name="padWithPixels">Whether the texture's padding should be filled with a copy of the texture's border, rather than transparent pixels. This value only has an effect if <paramref name="padding"/> is greater than 0.</param>
/// <exception cref="InvalidOperationException">Thrown when trying to add data to a packer that has already been packed, or when trying to add a texture width a width greater than the defined max width.</exception>
public void Add(DataTextureAtlas atlas, Action<Dictionary<string, TextureRegion>> result, int padding = 0, bool padWithPixels = false) {
var atlasRegions = atlas.RegionNames.ToArray();
var resultRegions = new Dictionary<string, TextureRegion>();
foreach (var region in atlasRegions) {
this.Add(atlas[region], r => {
resultRegions.Add(region, r);
if (resultRegions.Count >= atlasRegions.Length)
result.Invoke(resultRegions);
}, padding, padWithPixels);
}
}
/// <summary>
/// Adds a new <see cref="Texture2D"/> to this texture packer to be packed.
/// The passed <see cref="Action{T}"/> is invoked in <see cref="Pack"/> and provides the caller with the resulting texture region on the <see cref="PackedTexture"/>.
/// </summary>
/// <param name="texture">The texture to pack</param>
/// <param name="result">The result callback which will receive the resulting texture region</param>
public void Add(Texture2D texture, Action<TextureRegion> result) {
this.Add(new TextureRegion(texture), result);
/// <param name="texture">The texture to pack.</param>
/// <param name="result">The result callback which will receive the resulting texture region.</param>
/// <param name="padding">The padding that the texture should have around itself. This can be useful if texture bleeding issues occur due to texture coordinate rounding.</param>
/// <param name="padWithPixels">Whether the texture's padding should be filled with a copy of the texture's border, rather than transparent pixels. This value only has an effect if <paramref name="padding"/> is greater than 0.</param>
/// <exception cref="InvalidOperationException">Thrown when trying to add data to a packer that has already been packed, or when trying to add a texture width a width greater than the defined max width.</exception>
public void Add(Texture2D texture, Action<TextureRegion> result, int padding = 0, bool padWithPixels = false) {
this.Add(new TextureRegion(texture), result, padding, padWithPixels);
}
/// <summary>
/// Adds a new <see cref="TextureRegion"/> to this texture packer to be packed.
/// The passed <see cref="Action{T}"/> is invoked in <see cref="Pack"/> and provides the caller with the resulting texture region on the <see cref="PackedTexture"/>.
/// </summary>
/// <param name="texture">The texture to pack</param>
/// <param name="result">The result callback which will receive the resulting texture region</param>
/// <exception cref="InvalidOperationException">Thrown when trying to add data to a packer that has already been packed, or when trying to add a texture width a width greater than the defined max width</exception>
public void Add(TextureRegion texture, Action<TextureRegion> result) {
/// <param name="texture">The texture region to pack.</param>
/// <param name="result">The result callback which will receive the resulting texture region.</param>
/// <param name="padding">The padding that the texture should have around itself. This can be useful if texture bleeding issues occur due to texture coordinate rounding.</param>
/// <param name="padWithPixels">Whether the texture's padding should be filled with a copy of the texture's border, rather than transparent pixels. This value only has an effect if <paramref name="padding"/> is greater than 0.</param>
/// <exception cref="InvalidOperationException">Thrown when trying to add data to a packer that has already been packed, or when trying to add a texture width a width greater than the defined max width.</exception>
public void Add(TextureRegion texture, Action<TextureRegion> result, int padding = 0, bool padWithPixels = false) {
if (this.PackedTexture != null)
throw new InvalidOperationException("Cannot add texture to a texture packer that is already packed");
if (texture.Width > this.maxWidth) {
var paddedWidth = texture.Width + 2 * padding;
if (paddedWidth > this.maxWidth) {
if (this.autoIncreaseMaxWidth) {
this.maxWidth = texture.Width;
this.maxWidth = paddedWidth;
} else {
throw new InvalidOperationException($"Cannot add texture with width {texture.Width} to a texture packer with max width {this.maxWidth}");
}
}
this.textures.Add(new Request(texture, result));
this.texturesToPack.Add(new Request(texture, result, padding, padWithPixels));
}
/// <summary>
/// Packs all of the textures and texture regions added using <see cref="Add(Microsoft.Xna.Framework.Graphics.Texture2D,System.Action{MLEM.Textures.TextureRegion})"/> into one texture.
/// Packs all of the textures and texture regions added using <see cref="Add(Microsoft.Xna.Framework.Graphics.Texture2D,System.Action{MLEM.Textures.TextureRegion},int,bool)"/> into one texture.
/// The resulting texture will be stored in <see cref="PackedTexture"/>.
/// All of the result callbacks that were added will also be invoked.
/// </summary>
@ -99,20 +165,23 @@ namespace MLEM.Data {
throw new InvalidOperationException("Cannot pack a texture packer that is already packed");
// set pack areas for each request
// we pack larger textures first, so that smaller textures can fit in the gaps that larger ones leave
var stopwatch = Stopwatch.StartNew();
foreach (var request in this.textures.OrderByDescending(t => t.Texture.Width * t.Texture.Height)) {
var area = this.FindFreeArea(new Point(request.Texture.Width, request.Texture.Height));
request.PackedArea = area;
foreach (var request in this.texturesToPack.OrderByDescending(t => t.Texture.Width * t.Texture.Height)) {
request.PackedArea = this.FindFreeArea(request);
// if this is the first position that this request fit in, no other requests of the same size will find a position before it
this.firstPossiblePosForSizeCache[new Point(request.PackedArea.Width, request.PackedArea.Height)] = request.PackedArea.Location;
this.alreadyPackedTextures.Add(request);
}
stopwatch.Stop();
this.LastCalculationTime = stopwatch.Elapsed;
// figure out texture size and generate texture
var width = this.textures.Max(t => t.PackedArea.Right);
var height = this.textures.Max(t => t.PackedArea.Bottom);
var width = this.alreadyPackedTextures.Max(t => t.PackedArea.Right);
var height = this.alreadyPackedTextures.Max(t => t.PackedArea.Bottom);
if (this.forcePowerOfTwo) {
width = ToPowerOfTwo(width);
height = ToPowerOfTwo(height);
width = RuntimeTexturePacker.ToPowerOfTwo(width);
height = RuntimeTexturePacker.ToPowerOfTwo(height);
}
if (this.forceSquare)
width = height = Math.Max(width, height);
@ -121,20 +190,21 @@ namespace MLEM.Data {
// copy texture data onto the packed texture
stopwatch.Restart();
using (var data = this.PackedTexture.GetTextureData()) {
foreach (var request in this.textures)
CopyRegion(data, request);
foreach (var request in this.alreadyPackedTextures)
this.CopyRegion(data, request);
}
stopwatch.Stop();
this.LastPackTime = stopwatch.Elapsed;
// invoke callbacks
foreach (var request in this.textures) {
request.Result.Invoke(new TextureRegion(this.PackedTexture, request.PackedArea));
foreach (var request in this.alreadyPackedTextures) {
var packedArea = request.PackedArea.Shrink(new Point(request.Padding, request.Padding));
request.Result.Invoke(new TextureRegion(this.PackedTexture, packedArea));
if (this.disposeTextures)
request.Texture.Texture.Dispose();
}
this.textures.Clear();
this.ClearTempCollections();
}
/// <summary>
@ -143,9 +213,9 @@ namespace MLEM.Data {
public void Reset() {
this.PackedTexture?.Dispose();
this.PackedTexture = null;
this.textures.Clear();
this.LastCalculationTime = TimeSpan.Zero;
this.LastPackTime = TimeSpan.Zero;
this.ClearTempCollections();
}
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
@ -153,13 +223,17 @@ namespace MLEM.Data {
this.Reset();
}
private Rectangle FindFreeArea(Point size) {
var pos = new Point(0, 0);
private Rectangle FindFreeArea(Request request) {
var size = new Point(request.Texture.Width, request.Texture.Height);
size.X += request.Padding * 2;
size.Y += request.Padding * 2;
var pos = this.firstPossiblePosForSizeCache.TryGetValue(size, out var first) ? first : Point.Zero;
var lowestY = int.MaxValue;
while (true) {
var intersected = false;
var area = new Rectangle(pos, size);
foreach (var tex in this.textures) {
var area = new Rectangle(pos.X, pos.Y, size.X, size.Y);
foreach (var tex in this.alreadyPackedTextures) {
if (tex.PackedArea.Intersects(area)) {
pos.X = tex.PackedArea.Right;
// when we move down, we want to move down by the smallest intersecting texture's height
@ -179,16 +253,51 @@ namespace MLEM.Data {
}
}
private static void CopyRegion(TextureExtensions.TextureData destination, Request request) {
using (var data = request.Texture.Texture.GetTextureData()) {
for (var x = 0; x < request.Texture.Width; x++) {
for (var y = 0; y < request.Texture.Height; y++) {
var dest = request.PackedArea.Location + new Point(x, y);
var src = request.Texture.Position + new Point(x, y);
destination[dest] = data[src];
private void CopyRegion(TextureData destination, Request request) {
var data = this.GetCachedTextureData(request.Texture.Texture);
var location = request.PackedArea.Location + new Point(request.Padding, request.Padding);
for (var x = -request.Padding; x < request.Texture.Width + request.Padding; x++) {
for (var y = -request.Padding; y < request.Texture.Height + request.Padding; y++) {
Color srcColor;
if (!request.PadWithPixels && (x < 0 || y < 0 || x >= request.Texture.Width || y >= request.Texture.Height)) {
// if we're out of bounds and not padding with pixels, we make it transparent
srcColor = Color.Transparent;
} else {
// otherwise, we just use the closest pixel that is actually in bounds, causing the border pixels to be doubled up
var src = new Point((int) MathHelper.Clamp(x, 0, request.Texture.Width - 1), (int) MathHelper.Clamp(y, 0, request.Texture.Height - 1));
srcColor = data[request.Texture.Position + src];
}
destination[location + new Point(x, y)] = srcColor;
}
}
}
private TextureData GetCachedTextureData(Texture2D texture) {
// we cache texture data in case multiple requests use the same underlying texture
// this collection doesn't need to be disposed since we don't actually edit these textures
if (!this.dataCache.TryGetValue(texture, out var data)) {
data = texture.GetTextureData();
this.dataCache.Add(texture, data);
}
return data;
}
private bool IsTransparent(TextureRegion region) {
var data = this.GetCachedTextureData(region.Texture);
for (var rX = 0; rX < region.Width; rX++) {
for (var rY = 0; rY < region.Height; rY++) {
if (data[region.U + rX, region.V + rY] != Color.Transparent)
return false;
}
}
return true;
}
private void ClearTempCollections() {
this.texturesToPack.Clear();
this.alreadyPackedTextures.Clear();
this.firstPossiblePosForSizeCache.Clear();
this.dataCache.Clear();
}
private static int ToPowerOfTwo(int value) {
@ -202,11 +311,15 @@ namespace MLEM.Data {
public readonly TextureRegion Texture;
public readonly Action<TextureRegion> Result;
public readonly int Padding;
public readonly bool PadWithPixels;
public Rectangle PackedArea;
public Request(TextureRegion texture, Action<TextureRegion> result) {
public Request(TextureRegion texture, Action<TextureRegion> result, int padding, bool padWithPixels) {
this.Texture = texture;
this.Result = result;
this.Padding = padding;
this.PadWithPixels = padWithPixels;
}
}

View file

@ -0,0 +1,41 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<ProduceReferenceAssembly>true</ProduceReferenceAssembly>
<RootNamespace>MLEM.Extended</RootNamespace>
<DefineConstants>$(DefineConstants);FNA</DefineConstants>
</PropertyGroup>
<PropertyGroup>
<Authors>Ellpeck</Authors>
<Description>MLEM Library for Extending FNA extension that ties in with other FNA libraries</Description>
<PackageReleaseNotes>See the full changelog at https://mlem.ellpeck.de/CHANGELOG</PackageReleaseNotes>
<PackageTags>fna ellpeck mlem utility extensions extended</PackageTags>
<PackageProjectUrl>https://mlem.ellpeck.de/</PackageProjectUrl>
<RepositoryUrl>https://github.com/Ellpeck/MLEM</RepositoryUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageIcon>Logo.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\MLEM\MLEM.FNA.csproj" />
<ProjectReference Include="..\FontStashSharp\src\XNA\FontStashSharp.FNA.Core.csproj">
<PrivateAssets>all</PrivateAssets>
</ProjectReference>
<ProjectReference Include="..\FNA\FNA.Core.csproj">
<PrivateAssets>all</PrivateAssets>
</ProjectReference>
<Compile Remove="Tiled/**" />
<Compile Remove="Extensions/**" />
<Compile Remove="Font/GenericBitmapFont.cs" />
</ItemGroup>
<ItemGroup>
<None Include="../Media/Logo.png" Pack="true" PackagePath="" />
<None Include="../Docs/index.md" Pack="true" PackagePath="README.md" />
</ItemGroup>
</Project>

View file

@ -26,7 +26,7 @@
<PackageReference Include="MonoGame.Extended.Tiled" Version="3.8.0">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="FontStashSharp.MonoGame" Version="1.0.4">
<PackageReference Include="FontStashSharp.MonoGame" Version="1.1.6">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.0.1641">

View file

@ -4,6 +4,7 @@ using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using MLEM.Cameras;
using MLEM.Extensions;
using MLEM.Graphics;
using MLEM.Misc;
using MonoGame.Extended.Tiled;
using RectangleF = MonoGame.Extended.RectangleF;
@ -92,6 +93,20 @@ namespace MLEM.Extended.Tiled {
}
}
/// <summary>
/// Adds this individual tiled map renderer to the given <see cref="StaticSpriteBatch"/>.
/// Optionally, a frustum can be supplied that determines which positions, in pixel space, are visible at this time. <see cref="Camera"/> provides <see cref="Camera.GetVisibleRectangle"/> for this purpose.
/// </summary>
/// <param name="batch">The static sprite batch to use for drawing.</param>
/// <param name="frustum">The area that is visible, in pixel space.</param>
/// <param name="addFunction">The add function to use, or null to use <see cref="DefaultAdd"/>.</param>
public void Add(StaticSpriteBatch batch, RectangleF? frustum = null, AddDelegate addFunction = null) {
for (var i = 0; i < this.map.TileLayers.Count; i++) {
if (this.map.TileLayers[i].IsVisible)
this.AddLayer(batch, i, frustum, addFunction);
}
}
/// <summary>
/// Draws the given layer of this individual tiled map renderer.
/// Optionally, a frustum can be supplied that determines which positions, in pixel space, are visible at this time. <see cref="Camera"/> provides <see cref="Camera.GetVisibleRectangle"/> for this purpose.
@ -101,12 +116,8 @@ namespace MLEM.Extended.Tiled {
/// <param name="frustum">The area that is visible, in pixel space.</param>
/// <param name="drawFunction">The draw function to use, or null to use <see cref="DefaultDraw"/></param>
public void DrawLayer(SpriteBatch batch, int layerIndex, RectangleF? frustum = null, DrawDelegate drawFunction = null) {
var draw = drawFunction ?? DefaultDraw;
var frust = frustum ?? new RectangleF(0, 0, float.MaxValue, float.MaxValue);
var minX = Math.Max(0, frust.Left / this.map.TileWidth).Floor();
var minY = Math.Max(0, frust.Top / this.map.TileHeight).Floor();
var maxX = Math.Min(this.map.Width, frust.Right / this.map.TileWidth).Ceil();
var maxY = Math.Min(this.map.Height, frust.Bottom / this.map.TileHeight).Ceil();
var draw = drawFunction ?? IndividualTiledMapRenderer.DefaultDraw;
var (minX, minY, maxX, maxY) = this.GetFrustum(frustum);
for (var x = minX; x < maxX; x++) {
for (var y = minY; y < maxY; y++) {
var info = this.drawInfos[layerIndex, x, y];
@ -116,6 +127,26 @@ namespace MLEM.Extended.Tiled {
}
}
/// <summary>
/// Adds the given layer of this individual tiled map renderer to the given <see cref="StaticSpriteBatch"/>.
/// Optionally, a frustum can be supplied that determines which positions, in pixel space, are visible at this time. <see cref="Camera"/> provides <see cref="Camera.GetVisibleRectangle"/> for this purpose.
/// </summary>
/// <param name="batch">The static sprite batch to use for drawing.</param>
/// <param name="layerIndex">The index of the layer in <see cref="TiledMap.TileLayers"/>.</param>
/// <param name="frustum">The area that is visible, in pixel space.</param>
/// <param name="addFunction">The add function to use, or null to use <see cref="DefaultAdd"/>.</param>
public void AddLayer(StaticSpriteBatch batch, int layerIndex, RectangleF? frustum = null, AddDelegate addFunction = null) {
var add = addFunction ?? IndividualTiledMapRenderer.DefaultAdd;
var (minX, minY, maxX, maxY) = this.GetFrustum(frustum);
for (var x = minX; x < maxX; x++) {
for (var y = minY; y < maxY; y++) {
var info = this.drawInfos[layerIndex, x, y];
if (info != null)
add(batch, info);
}
}
}
/// <summary>
/// Update all of the animated tiles in this individual tiled map renderer
/// </summary>
@ -125,8 +156,17 @@ namespace MLEM.Extended.Tiled {
animation.Update(time);
}
private (int MinX, int MinY, int MaxX, int MaxY) GetFrustum(RectangleF? frustum) {
var frust = frustum ?? new RectangleF(0, 0, float.MaxValue, float.MaxValue);
var minX = Math.Max(0, frust.Left / this.map.TileWidth).Floor();
var minY = Math.Max(0, frust.Top / this.map.TileHeight).Floor();
var maxX = Math.Min(this.map.Width, frust.Right / this.map.TileWidth).Ceil();
var maxY = Math.Min(this.map.Height, frust.Bottom / this.map.TileHeight).Ceil();
return (minX, minY, maxX, maxY);
}
/// <summary>
/// The default implementation of <see cref="DrawDelegate"/> that is used by <see cref="SetMap"/> if no custom draw function is passed
/// The default implementation of <see cref="DrawDelegate"/> that is used by <see cref="Draw"/> if no custom draw function is passed.
/// </summary>
/// <param name="batch">The sprite batch to use for drawing</param>
/// <param name="info">The <see cref="TileDrawInfo"/> to draw</param>
@ -137,6 +177,18 @@ namespace MLEM.Extended.Tiled {
batch.Draw(info.Tileset.Texture, drawPos, region, Color.White, 0, Vector2.Zero, 1, effects, info.Depth);
}
/// <summary>
/// The default implementation of <see cref="AddDelegate"/> that is used by <see cref="Add"/> if no custom add function is passed.
/// </summary>
/// <param name="batch">The static sprite batch to use for drawing.</param>
/// <param name="info">The <see cref="TileDrawInfo"/> to add.</param>
public static void DefaultAdd(StaticSpriteBatch batch, TileDrawInfo info) {
var region = info.Tileset.GetTextureRegion(info.TilesetTile);
var effects = info.Tile.GetSpriteEffects();
var drawPos = new Vector2(info.Position.X * info.Renderer.map.TileWidth, info.Position.Y * info.Renderer.map.TileHeight);
batch.Add(info.Tileset.Texture, drawPos, region, Color.White, 0, Vector2.Zero, 1, effects, info.Depth);
}
/// <summary>
/// A delegate method used for <see cref="IndividualTiledMapRenderer.depthFunction"/>.
/// The idea is to return a depth (between 0 and 1) for the given tile that determines where in the sprite batch it should be rendererd.
@ -155,6 +207,13 @@ namespace MLEM.Extended.Tiled {
/// <param name="info">The <see cref="TileDrawInfo"/> to draw</param>
public delegate void DrawDelegate(SpriteBatch batch, TileDrawInfo info);
/// <summary>
/// A delegate method used for adding an <see cref="IndividualTiledMapRenderer"/> to a <see cref="StaticSpriteBatch"/>.
/// </summary>
/// <param name="batch">The static sprite batch to use for drawing.</param>
/// <param name="info">The <see cref="TileDrawInfo"/> to add.</param>
public delegate void AddDelegate(StaticSpriteBatch batch, TileDrawInfo info);
/// <summary>
/// A tile draw info contains information about a tile at a given map location.
/// It caches a lot of data that is required for drawing a tile efficiently.

View file

@ -4,7 +4,8 @@ using MonoGame.Extended.Tiled;
namespace MLEM.Extended.Tiled {
/// <summary>
/// A struct that represents a position on a <see cref="TiledMap"/> with multiple layers.
/// A struct that represents a position on a <see cref="TiledMap"/> with multiple layers, where the <see cref="X"/> and <see cref="Y"/> coordinates are 32-bit integer numbers.
/// See <see cref="LayerPositionF"/> for a floating point position.
/// </summary>
[DataContract]
public struct LayerPosition : IEquatable<LayerPosition> {
@ -53,8 +54,8 @@ namespace MLEM.Extended.Tiled {
/// <returns>A 32-bit signed integer that is the hash code for this instance.</returns>
public override int GetHashCode() {
var hashCode = this.Layer.GetHashCode();
hashCode = (hashCode * 397) ^ this.X;
hashCode = (hashCode * 397) ^ this.Y;
hashCode = hashCode * 397 ^ this.X;
hashCode = hashCode * 397 ^ this.Y;
return hashCode;
}
@ -104,7 +105,7 @@ namespace MLEM.Extended.Tiled {
/// <param name="right">The right position.</param>
/// <returns>The sum of the positions.</returns>
public static LayerPosition operator +(LayerPosition left, LayerPosition right) {
return Add(left, right);
return LayerPosition.Add(left, right);
}
/// <summary>
@ -114,7 +115,16 @@ namespace MLEM.Extended.Tiled {
/// <param name="right">The right position.</param>
/// <returns>The difference of the positions.</returns>
public static LayerPosition operator -(LayerPosition left, LayerPosition right) {
return Add(left, -right);
return LayerPosition.Add(left, -right);
}
/// <summary>
/// Implicitly converts a <see cref="LayerPosition"/> to a <see cref="LayerPositionF"/>.
/// </summary>
/// <param name="position">The position to convert.</param>
/// <returns>The converted position.</returns>
public static implicit operator LayerPositionF(LayerPosition position) {
return new LayerPositionF(position.Layer, position.X, position.Y);
}
}

View file

@ -0,0 +1,132 @@
using System;
using System.Runtime.Serialization;
using MonoGame.Extended.Tiled;
namespace MLEM.Extended.Tiled {
/// <summary>
/// A struct that represents a position on a <see cref="TiledMap"/> with multiple layers, where the <see cref="X"/> and <see cref="Y"/> coordinates are 32-bit floating point numbers.
/// See <see cref="LayerPosition"/> for an integer position.
/// </summary>
[DataContract]
public struct LayerPositionF : IEquatable<LayerPositionF> {
/// <summary>
/// The name of the layer that this position is on
/// </summary>
[DataMember]
public string Layer;
/// <summary>
/// The x coordinate of this position
/// </summary>
[DataMember]
public float X;
/// <summary>
/// The y coordinate of this position
/// </summary>
[DataMember]
public float Y;
/// <summary>
/// Creates a new layer position with the given settings
/// </summary>
/// <param name="layerName">The layer name</param>
/// <param name="x">The x coordinate</param>
/// <param name="y">The y coordinate</param>
public LayerPositionF(string layerName, float x, float y) {
this.Layer = layerName;
this.X = x;
this.Y = y;
}
/// <inheritdoc cref="Equals(object)"/>
public bool Equals(LayerPositionF other) {
return this.Layer == other.Layer && this.X == other.X && this.Y == other.Y;
}
/// <summary>Indicates whether this instance and a specified object are equal.</summary>
/// <param name="obj">The object to compare with the current instance.</param>
/// <returns>true if <paramref name="obj">obj</paramref> and this instance are the same type and represent the same value; otherwise, false.</returns>
public override bool Equals(object obj) {
return obj is LayerPositionF other && this.Equals(other);
}
/// <summary>Returns the hash code for this instance.</summary>
/// <returns>A 32-bit signed integer that is the hash code for this instance.</returns>
public override int GetHashCode() {
var hashCode = this.Layer.GetHashCode();
hashCode = hashCode * 397 ^ this.X.GetHashCode();
hashCode = hashCode * 397 ^ this.Y.GetHashCode();
return hashCode;
}
/// <summary>Returns the fully qualified type name of this instance.</summary>
/// <returns>The fully qualified type name.</returns>
public override string ToString() {
return $"{nameof(this.Layer)}: {this.Layer}, {nameof(this.X)}: {this.X}, {nameof(this.Y)}: {this.Y}";
}
/// <summary>
/// Adds the given layer positions together, returning a new layer position with the sum of their coordinates.
/// If the two layer positions' <see cref="Layer"/> differ, an <see cref="ArgumentException"/> is thrown.
/// </summary>
/// <param name="left">The left position.</param>
/// <param name="right">The right position.</param>
/// <returns>The sum of the positions.</returns>
/// <exception cref="ArgumentException">Thrown if the two positions' <see cref="Layer"/> are not the same.</exception>
public static LayerPositionF Add(LayerPositionF left, LayerPositionF right) {
if (left.Layer != right.Layer)
throw new ArgumentException("Cannot add layer positions on different layers");
return new LayerPositionF(left.Layer, left.X + right.X, left.Y + right.Y);
}
/// <inheritdoc cref="Equals(LayerPositionF)"/>
public static bool operator ==(LayerPositionF left, LayerPositionF right) {
return left.Equals(right);
}
/// <inheritdoc cref="Equals(LayerPositionF)"/>
public static bool operator !=(LayerPositionF left, LayerPositionF right) {
return !left.Equals(right);
}
/// <summary>
/// Returns the negative of the given layer position.
/// </summary>
/// <param name="position">The position to negate.</param>
/// <returns>The negative position.</returns>
public static LayerPositionF operator -(LayerPositionF position) {
return new LayerPositionF(position.Layer, -position.X, -position.Y);
}
/// <summary>
/// Returns the sum of the two layer positions using <see cref="Add"/>.
/// </summary>
/// <param name="left">The left position.</param>
/// <param name="right">The right position.</param>
/// <returns>The sum of the positions.</returns>
public static LayerPositionF operator +(LayerPositionF left, LayerPositionF right) {
return LayerPositionF.Add(left, right);
}
/// <summary>
/// Subtracts the second from the first position using <see cref="Add"/>.
/// </summary>
/// <param name="left">The left position.</param>
/// <param name="right">The right position.</param>
/// <returns>The difference of the positions.</returns>
public static LayerPositionF operator -(LayerPositionF left, LayerPositionF right) {
return LayerPositionF.Add(left, -right);
}
/// <summary>
/// Implicitly converts a <see cref="LayerPositionF"/> to a <see cref="LayerPosition"/>.
/// The coordinates are typecast to 32-bit integers in the process.
/// </summary>
/// <param name="position">The position to convert.</param>
/// <returns>The converted position.</returns>
public static implicit operator LayerPosition(LayerPositionF position) {
return new LayerPosition(position.Layer, (int) position.X, (int) position.Y);
}
}
}

View file

@ -5,7 +5,6 @@ using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using MonoGame.Extended;
using MonoGame.Extended.Tiled;
using static MonoGame.Extended.Tiled.TiledMapTileFlipFlags;
using ColorHelper = MLEM.Extensions.ColorHelper;
namespace MLEM.Extended.Tiled {
@ -144,9 +143,9 @@ namespace MLEM.Extended.Tiled {
public static TiledMapTilesetTile GetTilesetTile(this TiledMapTileset tileset, int localId, bool createStub = true) {
var tilesetTile = tileset.Tiles.FirstOrDefault(t => t.LocalTileIdentifier == localId);
if (tilesetTile == null && createStub) {
if (!StubTilesetTiles.TryGetValue(localId, out tilesetTile)) {
if (!TiledExtensions.StubTilesetTiles.TryGetValue(localId, out tilesetTile)) {
tilesetTile = new TiledMapTilesetTile(localId);
StubTilesetTiles.Add(localId, tilesetTile);
TiledExtensions.StubTilesetTiles.Add(localId, tilesetTile);
}
}
return tilesetTile;
@ -271,12 +270,12 @@ namespace MLEM.Extended.Tiled {
/// <param name="position">The position to add to the object's position</param>
/// <param name="flipFlags">The flipping of the tile that this object belongs to. If set, the returned area will be "flipped" in the tile's space so that it matches the flip flags.</param>
/// <returns>The area that the tile covers</returns>
public static RectangleF GetArea(this TiledMapObject obj, TiledMap map, Vector2? position = null, TiledMapTileFlipFlags flipFlags = None) {
public static RectangleF GetArea(this TiledMapObject obj, TiledMap map, Vector2? position = null, TiledMapTileFlipFlags flipFlags = TiledMapTileFlipFlags.None) {
var tileSize = map.GetTileSize();
var area = new RectangleF(obj.Position / tileSize, obj.Size / tileSize);
if (flipFlags.HasFlag(FlipHorizontally))
if (flipFlags.HasFlag(TiledMapTileFlipFlags.FlipHorizontally))
area.X = 1 - area.X - area.Width;
if (flipFlags.HasFlag(FlipVertically))
if (flipFlags.HasFlag(TiledMapTileFlipFlags.FlipVertically))
area.Y = 1 - area.Y - area.Height;
if (position != null)
area.Offset(position.Value);

View file

@ -36,7 +36,7 @@ namespace MLEM.Extended.Tiled {
/// <param name="collisionFunction">The function used to collect the collision info of a tile, or null to use <see cref="DefaultCollectCollisions"/></param>
public void SetMap(TiledMap map, CollectCollisions collisionFunction = null) {
this.map = map;
this.collisionFunction = collisionFunction ?? DefaultCollectCollisions;
this.collisionFunction = collisionFunction ?? TiledMapCollisions.DefaultCollectCollisions;
this.collisionInfos = new TileCollisionInfo[map.Layers.Count, map.Width, map.Height];
for (var i = 0; i < map.TileLayers.Count; i++) {
for (var x = 0; x < map.Width; x++) {

70
MLEM.FNA.sln Normal file
View file

@ -0,0 +1,70 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MLEM.FNA", "MLEM\MLEM.FNA.csproj", "{C2C88AE6-6274-4395-8B03-52AE898BA070}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MLEM.Ui.FNA", "MLEM.Ui\MLEM.Ui.FNA.csproj", "{1B47A40B-3BF6-4933-A7DB-57EADEBDFB61}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MLEM.Startup.FNA", "MLEM.Startup\MLEM.Startup.FNA.csproj", "{FBE2C9B5-293D-47A7-9BA1-5A4BD1C4E816}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MLEM.Data.FNA", "MLEM.Data\MLEM.Data.FNA.csproj", "{6587BC91-0640-43FB-988A-4F545B8ACFC5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Demos.FNA", "Demos\Demos.FNA.csproj", "{D83D7D24-14CB-4A7C-A80B-BA4FEC66AB55}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Demos.DesktopGL.FNA", "Demos.DesktopGL\Demos.DesktopGL.FNA.csproj", "{AB08FEC7-3AC3-4FDE-B632-226FAAD7F73F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests.FNA", "Tests\Tests.FNA.csproj", "{C74FC4C5-3BE0-42A7-8BA6-4A9AD438A9E2}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MLEM.Extended.FNA", "MLEM.Extended\MLEM.Extended.FNA.csproj", "{A5B22930-DF4B-4A62-93ED-A6549F7B666B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FNA.Core", "FNA\FNA.Core.csproj", "{06459F72-CEAA-4B45-B2B1-708FC28D04F8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FontStashSharp.FNA.Core", "FontStashSharp\src\XNA\FontStashSharp.FNA.Core.csproj", "{0B410591-3AED-4C82-A07A-516FF493709B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{C2C88AE6-6274-4395-8B03-52AE898BA070}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C2C88AE6-6274-4395-8B03-52AE898BA070}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C2C88AE6-6274-4395-8B03-52AE898BA070}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C2C88AE6-6274-4395-8B03-52AE898BA070}.Release|Any CPU.Build.0 = Release|Any CPU
{1B47A40B-3BF6-4933-A7DB-57EADEBDFB61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1B47A40B-3BF6-4933-A7DB-57EADEBDFB61}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1B47A40B-3BF6-4933-A7DB-57EADEBDFB61}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1B47A40B-3BF6-4933-A7DB-57EADEBDFB61}.Release|Any CPU.Build.0 = Release|Any CPU
{FBE2C9B5-293D-47A7-9BA1-5A4BD1C4E816}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FBE2C9B5-293D-47A7-9BA1-5A4BD1C4E816}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FBE2C9B5-293D-47A7-9BA1-5A4BD1C4E816}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FBE2C9B5-293D-47A7-9BA1-5A4BD1C4E816}.Release|Any CPU.Build.0 = Release|Any CPU
{6587BC91-0640-43FB-988A-4F545B8ACFC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6587BC91-0640-43FB-988A-4F545B8ACFC5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6587BC91-0640-43FB-988A-4F545B8ACFC5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6587BC91-0640-43FB-988A-4F545B8ACFC5}.Release|Any CPU.Build.0 = Release|Any CPU
{D83D7D24-14CB-4A7C-A80B-BA4FEC66AB55}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D83D7D24-14CB-4A7C-A80B-BA4FEC66AB55}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D83D7D24-14CB-4A7C-A80B-BA4FEC66AB55}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D83D7D24-14CB-4A7C-A80B-BA4FEC66AB55}.Release|Any CPU.Build.0 = Release|Any CPU
{AB08FEC7-3AC3-4FDE-B632-226FAAD7F73F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AB08FEC7-3AC3-4FDE-B632-226FAAD7F73F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AB08FEC7-3AC3-4FDE-B632-226FAAD7F73F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AB08FEC7-3AC3-4FDE-B632-226FAAD7F73F}.Release|Any CPU.Build.0 = Release|Any CPU
{C74FC4C5-3BE0-42A7-8BA6-4A9AD438A9E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C74FC4C5-3BE0-42A7-8BA6-4A9AD438A9E2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C74FC4C5-3BE0-42A7-8BA6-4A9AD438A9E2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C74FC4C5-3BE0-42A7-8BA6-4A9AD438A9E2}.Release|Any CPU.Build.0 = Release|Any CPU
{A5B22930-DF4B-4A62-93ED-A6549F7B666B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A5B22930-DF4B-4A62-93ED-A6549F7B666B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A5B22930-DF4B-4A62-93ED-A6549F7B666B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A5B22930-DF4B-4A62-93ED-A6549F7B666B}.Release|Any CPU.Build.0 = Release|Any CPU
{06459F72-CEAA-4B45-B2B1-708FC28D04F8}.Debug|Any CPU.ActiveCfg = Debug|x64
{06459F72-CEAA-4B45-B2B1-708FC28D04F8}.Debug|Any CPU.Build.0 = Debug|x64
{06459F72-CEAA-4B45-B2B1-708FC28D04F8}.Release|Any CPU.ActiveCfg = Release|x64
{06459F72-CEAA-4B45-B2B1-708FC28D04F8}.Release|Any CPU.Build.0 = Release|x64
{0B410591-3AED-4C82-A07A-516FF493709B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0B410591-3AED-4C82-A07A-516FF493709B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0B410591-3AED-4C82-A07A-516FF493709B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0B410591-3AED-4C82-A07A-516FF493709B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View file

@ -0,0 +1,37 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<ProduceReferenceAssembly>true</ProduceReferenceAssembly>
<RootNamespace>MLEM.Startup</RootNamespace>
<DefineConstants>$(DefineConstants);FNA</DefineConstants>
</PropertyGroup>
<PropertyGroup>
<Authors>Ellpeck</Authors>
<Description>MLEM Library for Extending FNA combined with some other useful libraries into a quick Game startup class</Description>
<PackageReleaseNotes>See the full changelog at https://mlem.ellpeck.de/CHANGELOG</PackageReleaseNotes>
<PackageTags>fna ellpeck mlem utility extensions</PackageTags>
<PackageProjectUrl>https://mlem.ellpeck.de/</PackageProjectUrl>
<RepositoryUrl>https://github.com/Ellpeck/MLEM</RepositoryUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageIcon>Logo.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Coroutine" Version="2.1.3" />
<ProjectReference Include="..\MLEM.Ui\MLEM.Ui.FNA.csproj" />
<ProjectReference Include="..\MLEM\MLEM.FNA.csproj" />
<ProjectReference Include="..\FNA\FNA.Core.csproj">
<PrivateAssets>all</PrivateAssets>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<None Include="../Media/Logo.png" Pack="true" PackagePath="" />
<None Include="../Docs/index.md" Pack="true" PackagePath="README.md" />
</ItemGroup>
</Project>

View file

@ -18,7 +18,7 @@ namespace MLEM.Startup {
/// <summary>
/// The static game instance's input handler
/// </summary>
public static InputHandler Input => instance.InputHandler;
public static InputHandler Input => MlemGame.instance.InputHandler;
/// <summary>
/// This game's graphics device manager, initialized in the constructor
@ -64,12 +64,14 @@ namespace MLEM.Startup {
/// <param name="windowWidth">The default window width</param>
/// <param name="windowHeight">The default window height</param>
public MlemGame(int windowWidth = 1280, int windowHeight = 720) {
instance = this;
MlemGame.instance = this;
this.GraphicsDeviceManager = new GraphicsDeviceManager(this) {
PreferredBackBufferWidth = windowWidth,
PreferredBackBufferHeight = windowHeight,
#if !FNA
HardwareModeSwitch = false
#endif
};
this.Window.AllowUserResizing = true;
this.Content.RootDirectory = "Content";
@ -160,7 +162,7 @@ namespace MLEM.Startup {
/// <typeparam name="T">The type of content to load</typeparam>
/// <returns>The loaded content</returns>
public static T LoadContent<T>(string name) {
return instance.Content.Load<T>(name);
return MlemGame.instance.Content.Load<T>(name);
}
/// <summary>

View file

@ -0,0 +1,36 @@
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-mgcb": {
"version": "3.8.1.263",
"commands": [
"mgcb"
]
},
"dotnet-mgcb-editor": {
"version": "3.8.1.263",
"commands": [
"mgcb-editor"
]
},
"dotnet-mgcb-editor-linux": {
"version": "3.8.1.263",
"commands": [
"mgcb-editor-linux"
]
},
"dotnet-mgcb-editor-windows": {
"version": "3.8.1.263",
"commands": [
"mgcb-editor-windows"
]
},
"dotnet-mgcb-editor-mac": {
"version": "3.8.1.263",
"commands": [
"mgcb-editor-mac"
]
}
}
}

View file

@ -1,7 +1,8 @@
using MLEM.Startup;
namespace TemplateNamespace {
public class GameImpl : MlemGame {
namespace TemplateNamespace;
public class GameImpl : MlemGame {
public static GameImpl Instance { get; private set; }
@ -9,5 +10,4 @@ namespace TemplateNamespace {
Instance = this;
}
}
}

View file

@ -1,8 +1,9 @@
using Microsoft.Xna.Framework;
using MLEM.Misc;
namespace TemplateNamespace {
public static class Program {
namespace TemplateNamespace;
public static class Program {
public static void Main() {
MlemPlatform.Current = new MlemPlatform.DesktopGl<TextInputEventArgs>((w, c) => w.TextInput += c);
@ -10,5 +11,4 @@ namespace TemplateNamespace {
game.Run();
}
}
}

View file

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<PublishReadyToRun>false</PublishReadyToRun>
<TieredCompilation>false</TieredCompilation>
<ApplicationIcon>Icon.ico</ApplicationIcon>
@ -10,15 +10,18 @@
<ItemGroup>
<PackageReference Include="Contentless" Version="3.*" />
<PackageReference Include="MLEM.Startup" Version="5.*" />
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.*" />
<PackageReference Include="MonoGame.Content.Builder.Task" Version="3.8.*" />
<PackageReference Include="MLEM.Startup" Version="6.*" />
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.1.263" />
<PackageReference Include="MonoGame.Content.Builder.Task" Version="3.8.1.263" />
</ItemGroup>
<ItemGroup>
<MonoGameContentReference Include="Content\Content.mgcb" />
<Content Include="Content\*\**" />
<EmbeddedResource Include="Icon.ico" />
<EmbeddedResource Include="Icon.bmp" />
</ItemGroup>
<Target Name="RestoreDotnetTools" BeforeTargets="Restore">
<Message Text="Restoring dotnet tools" Importance="High" />
<Exec Command="dotnet tool restore" />
</Target>
</Project>

View file

@ -1,7 +1,8 @@
using MLEM.Startup;
namespace TemplateNamespace {
public class GameImpl : MlemGame {
namespace TemplateNamespace;
public class GameImpl : MlemGame {
public static GameImpl Instance { get; private set; }
@ -9,5 +10,4 @@ namespace TemplateNamespace {
Instance = this;
}
}
}

View file

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

View file

@ -1,5 +1,6 @@
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using MLEM.Graphics;
using MLEM.Misc;
using MLEM.Textures;
using MLEM.Ui.Style;
@ -92,7 +93,7 @@ namespace MLEM.Ui.Elements {
}
/// <inheritdoc />
public override void Draw(GameTime time, SpriteBatch batch, float alpha, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, Effect effect, Matrix matrix) {
public override void Draw(GameTime time, SpriteBatch batch, float alpha, SpriteBatchContext context) {
var tex = this.Texture;
var color = (Color) this.NormalColor * alpha;
if (this.IsDisabled) {
@ -103,7 +104,7 @@ namespace MLEM.Ui.Elements {
color = (Color) this.HoveredColor * alpha;
}
batch.Draw(tex, this.DisplayArea, color, this.Scale);
base.Draw(time, batch, alpha, blendState, samplerState, depthStencilState, effect, matrix);
base.Draw(time, batch, alpha, context);
}
/// <inheritdoc />

View file

@ -1,5 +1,6 @@
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using MLEM.Graphics;
using MLEM.Misc;
using MLEM.Textures;
using MLEM.Ui.Style;
@ -108,7 +109,7 @@ namespace MLEM.Ui.Elements {
}
/// <inheritdoc />
public override void Draw(GameTime time, SpriteBatch batch, float alpha, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, Effect effect, Matrix matrix) {
public override void Draw(GameTime time, SpriteBatch batch, float alpha, SpriteBatchContext context) {
var tex = this.Texture;
var color = Color.White * alpha;
if (this.IsDisabled) {
@ -123,7 +124,7 @@ namespace MLEM.Ui.Elements {
batch.Draw(tex, boxDisplayArea, color, this.Scale);
if (this.Checked)
batch.Draw(this.Checkmark, boxDisplayArea, Color.White * alpha);
base.Draw(time, batch, alpha, blendState, samplerState, depthStencilState, effect, matrix);
base.Draw(time, batch, alpha, context);
}
/// <inheritdoc />

View file

@ -55,8 +55,9 @@ namespace MLEM.Ui.Elements {
/// Adds an element to this dropdown's <see cref="Panel"/>
/// </summary>
/// <param name="element">The element to add</param>
public void AddElement(Element element) {
this.Panel.AddChild(element);
/// <param name="index">The index to add the child at, or -1 to add it to the end of the <see cref="Element.Children"/> list</param>
public void AddElement(Element element, int index = -1) {
this.Panel.AddChild(element, index);
// Since the dropdown causes elements to be over each other,
// usual gamepad code doesn't apply
element.GetGamepadNextElement = (dir, usualNext) => {
@ -77,8 +78,9 @@ namespace MLEM.Ui.Elements {
/// </summary>
/// <param name="text">The text to display</param>
/// <param name="pressed">The resulting paragraph's <see cref="Element.OnPressed"/> event</param>
public Element AddElement(string text, GenericCallback pressed = null) {
return this.AddElement(p => text, pressed);
/// <param name="index">The index to add the child at, or -1 to add it to the end of the <see cref="Element.Children"/> list</param>
public Element AddElement(string text, GenericCallback pressed = null, int index = -1) {
return this.AddElement(p => text, pressed, index);
}
/// <summary>
@ -87,7 +89,8 @@ namespace MLEM.Ui.Elements {
/// </summary>
/// <param name="text">The text to display</param>
/// <param name="pressed">The resulting paragraph's <see cref="Element.OnPressed"/> event</param>
public Element AddElement(Paragraph.TextCallback text, GenericCallback pressed = null) {
/// <param name="index">The index to add the child at, or -1 to add it to the end of the <see cref="Element.Children"/> list</param>
public Element AddElement(Paragraph.TextCallback text, GenericCallback pressed = null, int index = -1) {
var paragraph = new Paragraph(Anchor.AutoLeft, 1, text) {
CanBeMoused = true,
CanBeSelected = true,
@ -97,7 +100,7 @@ namespace MLEM.Ui.Elements {
paragraph.OnPressed += pressed;
paragraph.OnMouseEnter += e => paragraph.TextColor = Color.LightGray;
paragraph.OnMouseExit += e => paragraph.TextColor = Color.White;
this.AddElement(paragraph);
this.AddElement(paragraph, index);
return paragraph;
}

View file

@ -7,6 +7,7 @@ using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using MLEM.Extensions;
using MLEM.Graphics;
using MLEM.Input;
using MLEM.Misc;
using MLEM.Textures;
@ -177,12 +178,12 @@ namespace MLEM.Ui.Elements {
/// <summary>
/// This element's transform matrix.
/// Can easily be scaled using <see cref="ScaleTransform"/>.
/// Note that, when this is non-null, a new <see cref="SpriteBatch.Begin"/> call is used for this element.
/// Note that, when this is non-null, a new <c>SpriteBatch.Begin</c> call is used for this element.
/// </summary>
public Matrix Transform = Matrix.Identity;
/// <summary>
/// The call that this element should make to <see cref="SpriteBatch"/> to begin drawing.
/// Note that, when this is non-null, a new <see cref="SpriteBatch.Begin"/> call is used for this element.
/// Note that, when this is non-null, a new <c>SpriteBatch.Begin</c> call is used for this element.
/// </summary>
#pragma warning disable CS0618
[Obsolete("BeginImpl is deprecated. You can create a custom element class and override Draw instead.")]
@ -192,7 +193,14 @@ namespace MLEM.Ui.Elements {
/// Set this field to false to disallow the element from being selected.
/// An unselectable element is skipped by automatic navigation and its <see cref="OnSelected"/> callback will never be called.
/// </summary>
public virtual bool CanBeSelected { get; set; } = true;
public virtual bool CanBeSelected {
get => this.canBeSelected;
set {
this.canBeSelected = value;
if (!this.canBeSelected && this.Root?.SelectedElement == this)
this.Root.SelectElement(null);
}
}
/// <summary>
/// Set this field to false to disallow the element from reacting to being moused over.
/// </summary>
@ -234,13 +242,13 @@ namespace MLEM.Ui.Elements {
public virtual bool PreventParentSpill { get; set; }
/// <summary>
/// The transparency (alpha value) that this element is rendered with.
/// Note that, when <see cref="Draw"/> is called, this alpha value is multiplied with the <see cref="Parent"/>'s alpha value and passed down to this element's <see cref="Children"/>.
/// Note that, when <see cref="Draw(Microsoft.Xna.Framework.GameTime,Microsoft.Xna.Framework.Graphics.SpriteBatch,float,MLEM.Graphics.SpriteBatchContext)"/> is called, this alpha value is multiplied with the <see cref="Parent"/>'s alpha value and passed down to this element's <see cref="Children"/>.
/// </summary>
public virtual float DrawAlpha { get; set; } = 1;
/// <summary>
/// Stores whether this element is currently being moused over or touched.
/// </summary>
public bool IsMouseOver { get; protected set; }
public bool IsMouseOver => this.Controls.MousedElement == this || this.Controls.TouchedElement == this;
/// <summary>
/// Returns whether this element is its <see cref="Root"/>'s <see cref="RootElement.SelectedElement"/>.
/// </summary>
@ -249,6 +257,12 @@ namespace MLEM.Ui.Elements {
/// 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>
public bool AreaDirty { get; private set; }
/// <summary>
/// An optional string that represents a group of elements for automatic (keyboard and gamepad) navigation.
/// All elements that share the same auto-nav group will be able to be navigated between, and all other elements will not be reachable from elements of other groups.
/// Note that, if no element is previously selected and auto-navigation is invoked, this element cannot be chosen as the first element to navigate to if its auto-nav group is non-null.
/// </summary>
public virtual string AutoNavGroup { get; set; }
/// <summary>
/// This Element's current <see cref="UiStyle"/>.
@ -422,6 +436,7 @@ namespace MLEM.Ui.Elements {
private int priority;
private StyleProp<UiStyle> style;
private StyleProp<Padding> childPadding;
private bool canBeSelected = true;
/// <summary>
/// Creates a new element with the given anchor and size and sets up some default event reactions.
@ -433,11 +448,6 @@ namespace MLEM.Ui.Elements {
this.size = size;
this.Children = new ReadOnlyCollection<Element>(this.children);
this.OnMouseEnter += element => this.IsMouseOver = true;
this.OnMouseExit += element => this.IsMouseOver = false;
this.OnTouchEnter += element => this.IsMouseOver = true;
this.OnTouchExit += element => this.IsMouseOver = false;
this.GetTabNextElement += (backward, next) => next;
this.GetGamepadNextElement += (dir, next) => next;
@ -479,6 +489,8 @@ namespace MLEM.Ui.Elements {
/// <param name="element">The child element to remove</param>
public virtual void RemoveChild(Element element) {
this.children.Remove(element);
if (this.Root?.SelectedElement == element)
this.Root.SelectElement(null);
// set area dirty here so that a dirty call is made
// upwards to us if the element is auto-positioned
element.SetAreaDirty();
@ -640,7 +652,7 @@ namespace MLEM.Ui.Elements {
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 + Epsilon) {
if (newX + newSize.X <= parentArea.Right + Element.Epsilon) {
pos.X = newX;
pos.Y = prevArea.Y + this.ScaledOffset.Y;
} else {
@ -703,7 +715,7 @@ namespace MLEM.Ui.Elements {
}
// we want to leave some leeway to prevent float rounding causing an infinite loop
if (!autoSize.Equals(this.UnscrolledArea.Size, Epsilon)) {
if (!autoSize.Equals(this.UnscrolledArea.Size, Element.Epsilon)) {
recursion++;
if (recursion >= 16) {
throw new ArithmeticException($"The area of {this} with root {this.Root.Name} has recursively updated too often. Does its child {foundChild} contain any conflicting auto-sizing settings?");
@ -897,13 +909,14 @@ namespace MLEM.Ui.Elements {
}
/// <summary>
/// Updates this element and all of its <see cref="GetRelevantChildren"/>
/// Updates this element and all of its <see cref="SortedChildren"/>
/// </summary>
/// <param name="time">The game's time</param>
public virtual void Update(GameTime time) {
this.System.InvokeOnElementUpdated(this, time);
foreach (var child in this.GetRelevantChildren()) {
// update all sorted children, not just relevant ones, because they might become relevant or irrelevant through updates
foreach (var child in this.SortedChildren) {
if (child.System != null)
child.Update(time);
}
@ -913,8 +926,8 @@ namespace MLEM.Ui.Elements {
}
/// <summary>
/// Draws this element by calling <see cref="Draw"/> internally.
/// If <see cref="Transform"/> or <see cref="BeginImpl"/> is set, a new <see cref="SpriteBatch.Begin"/> call is also started.
/// Draws this element by calling <see cref="Draw(Microsoft.Xna.Framework.GameTime,Microsoft.Xna.Framework.Graphics.SpriteBatch,float,MLEM.Graphics.SpriteBatchContext)"/> internally.
/// If <see cref="Transform"/> or <see cref="BeginImpl"/> is set, a new <c>SpriteBatch.Begin</c> call is also started.
/// </summary>
/// <param name="time">The game's time</param>
/// <param name="batch">The sprite batch to use for drawing</param>
@ -924,25 +937,37 @@ namespace MLEM.Ui.Elements {
/// <param name="effect">The effect that is used for drawing</param>
/// <param name="depthStencilState">The depth stencil state that is used for drawing</param>
/// <param name="matrix">The transformation matrix that is used for drawing</param>
[Obsolete("Use DrawTransformed that takes a SpriteBatchContext instead")]
public void DrawTransformed(GameTime time, SpriteBatch batch, float alpha, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, Effect effect, Matrix matrix) {
this.DrawTransformed(time, batch, alpha, new SpriteBatchContext(SpriteSortMode.Deferred, blendState, samplerState, depthStencilState, null, effect, matrix));
}
/// <summary>
/// Draws this element by calling <see cref="Draw(Microsoft.Xna.Framework.GameTime,Microsoft.Xna.Framework.Graphics.SpriteBatch,float,MLEM.Graphics.SpriteBatchContext)"/> internally.
/// If <see cref="Transform"/> or <see cref="BeginImpl"/> is set, a new <c>SpriteBatch.Begin</c> call is also started.
/// </summary>
/// <param name="time">The game's time</param>
/// <param name="batch">The sprite batch to use for drawing</param>
/// <param name="alpha">The alpha to draw this element and its children with</param>
/// <param name="context">The sprite batch context to use for drawing</param>
public void DrawTransformed(GameTime time, SpriteBatch batch, float alpha, SpriteBatchContext context) {
#pragma warning disable CS0618
var customDraw = this.BeginImpl != null || this.Transform != Matrix.Identity;
var mat = this.Transform * matrix;
var transformed = context;
transformed.TransformMatrix = this.Transform * transformed.TransformMatrix;
// TODO ending and beginning again when the matrix changes isn't ideal (https://github.com/MonoGame/MonoGame/issues/3156)
if (customDraw) {
// end the usual draw so that we can begin our own
batch.End();
// begin our own draw call
if (this.BeginImpl != null) {
this.BeginImpl(this, time, batch, alpha, blendState, samplerState, depthStencilState, effect, mat);
} else {
batch.Begin(SpriteSortMode.Deferred, blendState, samplerState, depthStencilState, null, effect, mat);
}
batch.Begin(transformed);
}
#pragma warning restore CS0618
// draw content in custom begin call
this.Draw(time, batch, alpha, blendState, samplerState, depthStencilState, effect, mat);
#pragma warning disable CS0618
this.Draw(time, batch, alpha, transformed.BlendState, transformed.SamplerState, transformed.DepthStencilState, transformed.Effect, transformed.TransformMatrix);
#pragma warning restore CS0618
if (this.System != null)
this.System.Metrics.Draws++;
@ -950,13 +975,13 @@ namespace MLEM.Ui.Elements {
// end our draw
batch.End();
// begin the usual draw again for other elements
batch.Begin(SpriteSortMode.Deferred, blendState, samplerState, depthStencilState, null, effect, matrix);
batch.Begin(context);
}
}
/// <summary>
/// Draws this element and all of its children. Override this method to draw the content of custom elements.
/// Note that, when this is called, <see cref="SpriteBatch.Begin"/> has already been called with custom <see cref="Transform"/> etc. applied.
/// Note that, when this is called, <c>SpriteBatch.Begin</c> has already been called with custom <see cref="Transform"/> etc. applied.
/// </summary>
/// <param name="time">The game's time</param>
/// <param name="batch">The sprite batch to use for drawing</param>
@ -966,21 +991,37 @@ namespace MLEM.Ui.Elements {
/// <param name="effect">The effect that is used for drawing</param>
/// <param name="depthStencilState">The depth stencil state that is used for drawing</param>
/// <param name="matrix">The transformation matrix that is used for drawing</param>
[Obsolete("Use Draw that takes a SpriteBatchContext instead")]
public virtual void Draw(GameTime time, SpriteBatch batch, float alpha, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, Effect effect, Matrix matrix) {
this.Draw(time, batch, alpha, new SpriteBatchContext(SpriteSortMode.Deferred, blendState, samplerState, depthStencilState, null, effect, matrix));
}
/// <summary>
/// Draws this element and all of its children. Override this method to draw the content of custom elements.
/// Note that, when this is called, <c>SpriteBatch.Begin</c> has already been called with custom <see cref="Transform"/> etc. applied.
/// </summary>
/// <param name="time">The game's time</param>
/// <param name="batch">The sprite batch to use for drawing</param>
/// <param name="alpha">The alpha to draw this element and its children with</param>
/// <param name="context">The sprite batch context to use for drawing</param>
public virtual void Draw(GameTime time, SpriteBatch batch, float alpha, SpriteBatchContext context) {
this.System.InvokeOnElementDrawn(this, time, batch, alpha);
if (this.IsSelected)
this.System.InvokeOnSelectedElementDrawn(this, time, batch, alpha);
foreach (var child in this.GetRelevantChildren()) {
if (!child.IsHidden)
child.DrawTransformed(time, batch, alpha * child.DrawAlpha, blendState, samplerState, depthStencilState, effect, matrix);
if (!child.IsHidden) {
#pragma warning disable CS0618
child.DrawTransformed(time, batch, alpha * child.DrawAlpha, context.BlendState, context.SamplerState, context.DepthStencilState, context.Effect, context.TransformMatrix);
#pragma warning restore CS0618
}
}
}
/// <summary>
/// Draws this element and all of its <see cref="GetRelevantChildren"/> early.
/// Drawing early involves drawing onto <see cref="RenderTarget2D"/> instances rather than onto the screen.
/// Note that, when this is called, <see cref="SpriteBatch.Begin"/> has not yet been called.
/// Note that, when this is called, <c>SpriteBatch.Begin</c> has not yet been called.
/// </summary>
/// <param name="time">The game's time</param>
/// <param name="batch">The sprite batch to use for drawing</param>
@ -1128,7 +1169,7 @@ namespace MLEM.Ui.Elements {
public delegate void OtherElementCallback(Element thisElement, Element otherElement);
/// <summary>
/// A delegate used inside of <see cref="Element.Draw"/>
/// A delegate used inside of <see cref="Element.Draw(Microsoft.Xna.Framework.GameTime,Microsoft.Xna.Framework.Graphics.SpriteBatch,float,MLEM.Graphics.SpriteBatchContext)"/>
/// </summary>
/// <param name="element">The element that is being drawn</param>
/// <param name="time">The game's time</param>

View file

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using MLEM.Input;
@ -115,9 +116,9 @@ namespace MLEM.Ui.Elements {
return group;
}
/// <inheritdoc cref="KeybindButton(MLEM.Ui.Anchor,Microsoft.Xna.Framework.Vector2,MLEM.Input.Keybind,MLEM.Input.InputHandler,string,MLEM.Input.Keybind,string,System.Func{MLEM.Input.GenericInput,string},int)"/>
public static Button KeybindButton(Anchor anchor, Vector2 size, Keybind keybind, InputHandler inputHandler, string activePlaceholder, GenericInput unbindKey = default, string unboundPlaceholder = "", Func<GenericInput, string> inputName = null, int index = 0) {
return KeybindButton(anchor, size, keybind, inputHandler, activePlaceholder, new Keybind(unbindKey), unboundPlaceholder, inputName, index);
/// <inheritdoc cref="KeybindButton(MLEM.Ui.Anchor,Microsoft.Xna.Framework.Vector2,MLEM.Input.Keybind,MLEM.Input.InputHandler,string,MLEM.Input.Keybind,string,System.Func{MLEM.Input.GenericInput,string},int,System.Func{MLEM.Input.GenericInput,System.Collections.Generic.IEnumerable{MLEM.Input.GenericInput},bool})"/>
public static Button KeybindButton(Anchor anchor, Vector2 size, Keybind keybind, InputHandler inputHandler, string activePlaceholder, GenericInput unbindKey = default, string unboundPlaceholder = "", Func<GenericInput, string> inputName = null, int index = 0, Func<GenericInput, IEnumerable<GenericInput>, bool> isKeybindAllowed = null) {
return ElementHelper.KeybindButton(anchor, size, keybind, inputHandler, activePlaceholder, new Keybind(unbindKey), unboundPlaceholder, inputName, index, isKeybindAllowed);
}
/// <summary>
@ -135,9 +136,12 @@ namespace MLEM.Ui.Elements {
/// <param name="unboundPlaceholder">A placeholder text that is displayed if the keybind is unbound</param>
/// <param name="inputName">An optional function to give each input a display name that is easier to read. If this is null, <see cref="GenericInput.ToString"/> is used.</param>
/// <param name="index">The index in the <paramref name="keybind"/> that the button should display. Note that, if the index is greater than the amount of combinations, combinations entered using this button will be stored at an earlier index.</param>
/// <param name="isKeybindAllowed">A function that can optionally determine whether a given input and modifier combination is allowed. If this is null, all combinations are allowed.</param>
/// <returns>A keybind button with the given settings</returns>
public static Button KeybindButton(Anchor anchor, Vector2 size, Keybind keybind, InputHandler inputHandler, string activePlaceholder, Keybind unbind = default, string unboundPlaceholder = "", Func<GenericInput, string> inputName = null, int index = 0) {
string GetCurrentName() => keybind.TryGetCombination(index, out var combination) ? combination.ToString(" + ", inputName) : unboundPlaceholder;
public static Button KeybindButton(Anchor anchor, Vector2 size, Keybind keybind, InputHandler inputHandler, string activePlaceholder, Keybind unbind = default, string unboundPlaceholder = "", Func<GenericInput, string> inputName = null, int index = 0, Func<GenericInput, IEnumerable<GenericInput>, bool> isKeybindAllowed = null) {
string GetCurrentName() {
return keybind.TryGetCombination(index, out var combination) ? combination.ToString(" + ", inputName) : unboundPlaceholder;
}
var button = new Button(anchor, size, GetCurrentName());
var activeNext = false;
@ -150,19 +154,21 @@ namespace MLEM.Ui.Elements {
button.SetData("Active", true);
activeNext = false;
} else if (button.GetData<bool>("Active")) {
if (unbind != null && unbind.IsPressed(inputHandler)) {
if (unbind != null && unbind.TryConsumePressed(inputHandler)) {
keybind.Remove((c, i) => i == index);
button.Text.Text = unboundPlaceholder;
button.SetData("Active", false);
} else if (inputHandler.InputsPressed.Length > 0) {
var key = inputHandler.InputsPressed.FirstOrDefault(i => !i.IsModifier());
if (key != default) {
var mods = inputHandler.InputsDown.Where(i => i.IsModifier());
keybind.Remove((c, i) => i == index).Insert(index, key, mods.ToArray());
var mods = inputHandler.InputsDown.Where(i => i.IsModifier()).ToArray();
if (isKeybindAllowed == null || isKeybindAllowed(key, mods)) {
keybind.Remove((c, i) => i == index).Insert(index, key, mods);
button.Text.Text = GetCurrentName();
button.SetData("Active", false);
}
}
}
} else {
button.Text.Text = GetCurrentName();
}
@ -184,7 +190,7 @@ namespace MLEM.Ui.Elements {
/// <param name="textCallback">The text to display on the tooltip</param>
/// <returns>The created tooltip instance</returns>
public static Tooltip AddTooltip(this Element element, Paragraph.TextCallback textCallback) {
return new Tooltip(textCallback, element);
return element.AddTooltip(new Tooltip(textCallback));
}
/// <summary>
@ -194,7 +200,18 @@ namespace MLEM.Ui.Elements {
/// <param name="text">The text to display on the tooltip</param>
/// <returns>The created tooltip instance</returns>
public static Tooltip AddTooltip(this Element element, string text) {
return new Tooltip(text, element);
return element.AddTooltip(new Tooltip(text));
}
/// <summary>
/// Adds the given <see cref="Tooltip"/> to the given element
/// </summary>
/// <param name="element">The element to add the tooltip to</param>
/// <param name="tooltip">The tooltip to add</param>
/// <returns>The passed tooltip, for chaining</returns>
public static Tooltip AddTooltip(this Element element, Tooltip tooltip) {
tooltip.AddToElement(element);
return tooltip;
}
}

View file

@ -1,5 +1,6 @@
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using MLEM.Graphics;
namespace MLEM.Ui.Elements {
/// <summary>
@ -20,10 +21,10 @@ namespace MLEM.Ui.Elements {
}
/// <inheritdoc />
public override void Draw(GameTime time, SpriteBatch batch, float alpha, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, Effect effect, Matrix matrix) {
public override void Draw(GameTime time, SpriteBatch batch, float alpha, SpriteBatchContext context) {
// since the group never accesses its own area when drawing, we have to update it manually
this.UpdateAreaIfDirty();
base.Draw(time, batch, alpha, blendState, samplerState, depthStencilState, effect, matrix);
base.Draw(time, batch, alpha, context);
}
}

View file

@ -1,6 +1,8 @@
using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using MLEM.Extensions;
using MLEM.Graphics;
using MLEM.Misc;
using MLEM.Textures;
using MLEM.Ui.Style;
@ -103,7 +105,7 @@ namespace MLEM.Ui.Elements {
}
/// <inheritdoc />
public override void Draw(GameTime time, SpriteBatch batch, float alpha, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, Effect effect, Matrix matrix) {
public override void Draw(GameTime time, SpriteBatch batch, float alpha, SpriteBatchContext context) {
if (this.Texture == null)
return;
var center = new Vector2(this.Texture.Width / 2F, this.Texture.Height / 2F);
@ -116,7 +118,7 @@ namespace MLEM.Ui.Elements {
var scale = new Vector2(1F / this.Texture.Width, 1F / this.Texture.Height) * this.DisplayArea.Size;
batch.Draw(this.Texture, this.DisplayArea.Location + center * scale, color, this.ImageRotation, center, scale * this.ImageScale, this.ImageEffects, 0);
}
base.Draw(time, batch, alpha, blendState, samplerState, depthStencilState, effect, matrix);
base.Draw(time, batch, alpha, context);
}
/// <summary>

View file

@ -4,6 +4,7 @@ using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using MLEM.Extensions;
using MLEM.Graphics;
using MLEM.Misc;
using MLEM.Textures;
using MLEM.Ui.Style;
@ -120,7 +121,7 @@ namespace MLEM.Ui.Elements {
var offset = new Vector2(0, -this.ScrollBar.CurrentValue);
// we ignore false grandchildren so that the children of the scroll bar stay in place
foreach (var child in this.GetChildren(c => c != this.ScrollBar, true, true)) {
if (!child.ScrollOffset.Equals(offset, Epsilon)) {
if (!child.ScrollOffset.Equals(offset, Element.Epsilon)) {
child.ScrollOffset = offset;
this.relevantChildrenDirty = true;
}
@ -167,7 +168,7 @@ namespace MLEM.Ui.Elements {
}
/// <inheritdoc />
public override void Draw(GameTime time, SpriteBatch batch, float alpha, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, Effect effect, Matrix matrix) {
public override void Draw(GameTime time, SpriteBatch batch, float alpha, SpriteBatchContext context) {
// draw children onto the render target if we have one
if (this.scrollOverflow && this.renderTarget != null) {
this.UpdateAreaIfDirty();
@ -179,21 +180,22 @@ namespace MLEM.Ui.Elements {
batch.GraphicsDevice.Clear(Color.Transparent);
// offset children by the render target's location
var area = this.GetRenderTargetArea();
var trans = Matrix.CreateTranslation(-area.X, -area.Y, 0);
// do the usual draw, but within the render target
batch.Begin(SpriteSortMode.Deferred, blendState, samplerState, depthStencilState, null, effect, trans);
base.Draw(time, batch, alpha, blendState, samplerState, depthStencilState, effect, trans);
var trans = context;
trans.TransformMatrix = Matrix.CreateTranslation(-area.X, -area.Y, 0);
batch.Begin(trans);
base.Draw(time, batch, alpha, trans);
batch.End();
}
batch.GraphicsDevice.PresentationParameters.RenderTargetUsage = lastUsage;
batch.Begin(SpriteSortMode.Deferred, blendState, samplerState, depthStencilState, null, effect, matrix);
batch.Begin(context);
}
if (this.Texture.HasValue())
batch.Draw(this.Texture, this.DisplayArea, this.DrawColor.OrDefault(Color.White) * alpha, this.Scale);
// if we handle overflow, draw using the render target in DrawUnbound
if (!this.scrollOverflow || this.renderTarget == null) {
base.Draw(time, batch, alpha, blendState, samplerState, depthStencilState, effect, matrix);
base.Draw(time, batch, alpha, context);
} else {
// draw the actual render target (don't apply the alpha here because it's already drawn onto with alpha)
batch.Draw(this.renderTarget, this.GetRenderTargetArea(), Color.White);
@ -244,13 +246,13 @@ namespace MLEM.Ui.Elements {
// the max value of the scrollbar is the amount of non-scaled pixels taken up by overflowing components
var scrollBarMax = (childrenHeight - this.ChildPaddedArea.Height) / this.Scale;
if (!this.ScrollBar.MaxValue.Equals(scrollBarMax, Epsilon)) {
if (!this.ScrollBar.MaxValue.Equals(scrollBarMax, Element.Epsilon)) {
this.ScrollBar.MaxValue = scrollBarMax;
this.relevantChildrenDirty = true;
// update child padding based on whether the scroll bar is visible
var childOffset = this.ScrollBar.IsHidden ? 0 : this.ScrollerSize.Value.X + this.ScrollBarOffset;
if (!this.scrollBarChildOffset.Equals(childOffset, Epsilon)) {
if (!this.scrollBarChildOffset.Equals(childOffset, Element.Epsilon)) {
this.ChildPadding += new Padding(0, -this.scrollBarChildOffset + childOffset, 0, 0);
this.scrollBarChildOffset = childOffset;
this.SetAreaDirty();

View file

@ -6,6 +6,7 @@ using MLEM.Extensions;
using MLEM.Font;
using MLEM.Formatting;
using MLEM.Formatting.Codes;
using MLEM.Graphics;
using MLEM.Misc;
using MLEM.Ui.Style;
@ -58,8 +59,13 @@ namespace MLEM.Ui.Elements {
set {
if (this.text != value) {
this.text = value;
this.IsHidden = string.IsNullOrWhiteSpace(this.text);
this.SetTextDirty();
var force = string.IsNullOrWhiteSpace(this.text);
if (this.forceHide != force) {
this.forceHide = force;
this.SetAreaDirty();
}
}
}
}
@ -98,9 +104,13 @@ namespace MLEM.Ui.Elements {
}
}
/// <inheritdoc />
public override bool IsHidden => base.IsHidden || this.forceHide;
private string text;
private StyleProp<TextAlignment> alignment;
private StyleProp<GenericFont> regularFont;
private bool forceHide;
/// <summary>
/// Creates a new paragraph with the given settings.
@ -109,19 +119,13 @@ namespace MLEM.Ui.Elements {
/// <param name="width">The paragraph's width. Note that its height is automatically calculated.</param>
/// <param name="textCallback">The paragraph's text</param>
/// <param name="autoAdjustWidth">Whether the paragraph's width should automatically be calculated based on the text within it.</param>
public Paragraph(Anchor anchor, float width, TextCallback textCallback, bool autoAdjustWidth = false)
: this(anchor, width, "", autoAdjustWidth) {
public Paragraph(Anchor anchor, float width, TextCallback textCallback, bool autoAdjustWidth = false) : this(anchor, width, string.Empty, autoAdjustWidth) {
this.GetTextCallback = textCallback;
this.Text = textCallback(this);
if (this.Text == null)
this.IsHidden = true;
}
/// <inheritdoc cref="Paragraph(Anchor,float,TextCallback,bool)"/>
public Paragraph(Anchor anchor, float width, string text, bool autoAdjustWidth = false) : base(anchor, new Vector2(width, 0)) {
this.Text = text;
if (this.Text == null)
this.IsHidden = true;
this.AutoAdjustWidth = autoAdjustWidth;
this.CanBeSelected = false;
this.CanBeMoused = false;
@ -131,8 +135,8 @@ namespace MLEM.Ui.Elements {
protected override Vector2 CalcActualSize(RectangleF parentArea) {
var size = base.CalcActualSize(parentArea);
this.ParseText(size);
var (w, h) = this.TokenizedText.Measure(this.RegularFont) * this.TextScale * this.TextScaleMultiplier * this.Scale;
return new Vector2(this.AutoAdjustWidth ? w + this.ScaledPadding.Width : size.X, h + this.ScaledPadding.Height);
var textSize = this.TokenizedText.Measure(this.RegularFont) * this.TextScale * this.TextScaleMultiplier * this.Scale;
return new Vector2(this.AutoAdjustWidth ? textSize.X + this.ScaledPadding.Width : size.X, textSize.Y + this.ScaledPadding.Height);
}
/// <inheritdoc />
@ -144,12 +148,12 @@ namespace MLEM.Ui.Elements {
}
/// <inheritdoc />
public override void Draw(GameTime time, SpriteBatch batch, float alpha, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, Effect effect, Matrix matrix) {
public override void Draw(GameTime time, SpriteBatch batch, float alpha, SpriteBatchContext context) {
var pos = this.DisplayArea.Location + new Vector2(this.GetAlignmentOffset(), 0);
var sc = this.TextScale * this.TextScaleMultiplier * this.Scale;
var color = this.TextColor.OrDefault(Color.White) * alpha;
this.TokenizedText.Draw(time, batch, pos, this.RegularFont, color, sc, 0);
base.Draw(time, batch, alpha, blendState, samplerState, depthStencilState, effect, matrix);
base.Draw(time, batch, alpha, context);
}
/// <inheritdoc />
@ -193,7 +197,7 @@ namespace MLEM.Ui.Elements {
protected void SetTextDirty() {
this.TokenizedText = null;
// only set our area dirty if our size changed as a result of this action
if (!this.AreaDirty && !this.CalcActualSize(this.ParentArea).Equals(this.DisplayArea.Size, Epsilon))
if (!this.AreaDirty && !this.CalcActualSize(this.ParentArea).Equals(this.DisplayArea.Size, Element.Epsilon))
this.SetAreaDirty();
}

View file

@ -2,6 +2,7 @@ using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using MLEM.Extensions;
using MLEM.Graphics;
using MLEM.Misc;
using MLEM.Textures;
using MLEM.Ui.Style;
@ -73,7 +74,7 @@ namespace MLEM.Ui.Elements {
}
/// <inheritdoc />
public override void Draw(GameTime time, SpriteBatch batch, float alpha, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, Effect effect, Matrix matrix) {
public override void Draw(GameTime time, SpriteBatch batch, float alpha, SpriteBatchContext context) {
batch.Draw(this.Texture, this.DisplayArea, (Color) this.Color * alpha, this.Scale);
var percentage = this.CurrentValue / this.MaxValue;
@ -106,7 +107,7 @@ namespace MLEM.Ui.Elements {
} else {
batch.Draw(batch.GetBlankTexture(), offsetArea, (Color) this.ProgressColor * alpha);
}
base.Draw(time, batch, alpha, blendState, samplerState, depthStencilState, effect, matrix);
base.Draw(time, batch, alpha, context);
}
/// <inheritdoc />

View file

@ -4,7 +4,7 @@ using MLEM.Ui.Style;
namespace MLEM.Ui.Elements {
/// <summary>
/// A radio button element to use inside of a <see cref="UiSystem"/>.
/// A radio button is a variation of a <see cref="Checkbox"/> that causes all other radio buttons in the same <see cref="Group"/> to be deselected upon selection.
/// A radio button is a variation of a <see cref="Checkbox"/> that causes all other radio buttons in the same <see cref="RootElement"/> to be deselected upon selection.
/// </summary>
public class RadioButton : Checkbox {
@ -26,13 +26,13 @@ namespace MLEM.Ui.Elements {
base(anchor, size, label, defaultChecked) {
this.Group = group;
// don't += because we want to override the checking + unchecking behavior of Checkbox
// don't += because we want to override the checking/unchecking behavior of Checkbox
this.OnPressed = element => {
this.Checked = true;
foreach (var sib in this.GetSiblings()) {
if (sib is RadioButton radio && radio.Group == this.Group)
radio.Checked = false;
}
this.Root.Element.AndChildren(e => {
if (e != this && e is RadioButton r && r.Group == this.Group)
r.Checked = false;
});
};
}

View file

@ -3,6 +3,8 @@ using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input.Touch;
using MLEM.Extensions;
using MLEM.Graphics;
using MLEM.Input;
using MLEM.Misc;
using MLEM.Textures;
@ -44,7 +46,7 @@ namespace MLEM.Ui.Elements {
// force current value to be clamped
this.CurrentValue = this.CurrentValue;
// auto-hide if necessary
var shouldHide = this.maxValue <= Epsilon;
var shouldHide = this.maxValue <= Element.Epsilon;
if (this.AutoHideWhenEmpty && this.IsHidden != shouldHide) {
this.IsHidden = shouldHide;
this.OnAutoHide?.Invoke(this);
@ -137,7 +139,7 @@ namespace MLEM.Ui.Elements {
// MOUSE INPUT
var moused = this.Controls.MousedElement;
if (moused == this && this.Input.IsMouseButtonPressed(MouseButton.Left)) {
if (moused == this && this.Input.WasMouseButtonUp(MouseButton.Left) && this.Input.IsMouseButtonDown(MouseButton.Left)) {
this.isMouseHeld = true;
this.scrollStartOffset = this.TransformInverseAll(this.Input.ViewportMousePosition.ToVector2()) - this.ScrollerPosition;
} else if (this.isMouseHeld && !this.Input.IsMouseButtonDown(MouseButton.Left)) {
@ -187,31 +189,31 @@ namespace MLEM.Ui.Elements {
if (this.SmoothScrolling && this.scrollAdded != 0) {
this.scrollAdded *= this.SmoothScrollFactor;
if (Math.Abs(this.scrollAdded) <= Epsilon)
if (Math.Abs(this.scrollAdded) <= Element.Epsilon)
this.scrollAdded = 0;
this.OnValueChanged?.Invoke(this, this.CurrentValue);
}
}
private void ScrollToPos(Vector2 position) {
var (width, height) = this.ScrollerSize * this.Scale;
var size = this.ScrollerSize * this.Scale;
if (this.Horizontal) {
var offset = this.scrollStartOffset.X >= 0 && this.scrollStartOffset.X <= width ? this.scrollStartOffset.X : width / 2;
this.CurrentValue = (position.X - this.Area.X - offset) / (this.Area.Width - width) * this.MaxValue;
var offset = this.scrollStartOffset.X >= 0 && this.scrollStartOffset.X <= size.X ? this.scrollStartOffset.X : size.X / 2;
this.CurrentValue = (position.X - this.Area.X - offset) / (this.Area.Width - size.X) * this.MaxValue;
} else {
var offset = this.scrollStartOffset.Y >= 0 && this.scrollStartOffset.Y <= height ? this.scrollStartOffset.Y : height / 2;
this.CurrentValue = (position.Y - this.Area.Y - offset) / (this.Area.Height - height) * this.MaxValue;
var offset = this.scrollStartOffset.Y >= 0 && this.scrollStartOffset.Y <= size.Y ? this.scrollStartOffset.Y : size.Y / 2;
this.CurrentValue = (position.Y - this.Area.Y - offset) / (this.Area.Height - size.Y) * this.MaxValue;
}
}
/// <inheritdoc />
public override void Draw(GameTime time, SpriteBatch batch, float alpha, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, Effect effect, Matrix matrix) {
public override void Draw(GameTime time, SpriteBatch batch, float alpha, SpriteBatchContext context) {
batch.Draw(this.Background, this.DisplayArea, Color.White * alpha, this.Scale);
if (this.MaxValue > 0) {
var scrollerRect = new RectangleF(this.ScrollerPosition, this.ScrollerSize * this.Scale);
batch.Draw(this.ScrollerTexture, scrollerRect, Color.White * alpha, this.Scale);
}
base.Draw(time, batch, alpha, blendState, samplerState, depthStencilState, effect, matrix);
base.Draw(time, batch, alpha, context);
}
/// <inheritdoc />

View file

@ -30,9 +30,9 @@ namespace MLEM.Ui.Elements {
base.Update(time);
if (this.IsSelected) {
if (this.Controls.LeftButtons.IsPressed(this.Input, this.Controls.GamepadIndex)) {
if (this.CurrentValue > 0 && this.Controls.LeftButtons.TryConsumePressed(this.Input, this.Controls.GamepadIndex)) {
this.CurrentValue -= this.StepPerScroll;
} else if (this.Controls.RightButtons.IsPressed(this.Input, this.Controls.GamepadIndex)) {
} else if (this.CurrentValue < this.MaxValue && this.Controls.RightButtons.TryConsumePressed(this.Input, this.Controls.GamepadIndex)) {
this.CurrentValue += this.StepPerScroll;
}
}

View file

@ -24,7 +24,7 @@ namespace MLEM.Ui.Elements {
// we squish children in order of priority, since auto-anchoring is based on addition order
for (var i = 0; i < this.SortedChildren.Count; i++) {
var child = this.SortedChildren[i];
if (SquishChild(child, out var squished))
if (SquishingGroup.SquishChild(child, out var squished))
child.SetAreaAndUpdateChildren(squished);
}
}
@ -66,7 +66,7 @@ namespace MLEM.Ui.Elements {
}
}
}
if (!pos.Equals(element.Area.Location, Epsilon) || !size.Equals(element.Area.Size, Epsilon)) {
if (!pos.Equals(element.Area.Location, Element.Epsilon) || !size.Equals(element.Area.Size, Element.Epsilon)) {
squishedArea = new RectangleF(pos, size);
return true;
}

View file

@ -1,12 +1,8 @@
using System;
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using MLEM.Extensions;
using MLEM.Font;
using MLEM.Graphics;
using MLEM.Input;
using MLEM.Misc;
using MLEM.Textures;
@ -18,57 +14,22 @@ namespace MLEM.Ui.Elements {
/// A text field element for use inside of a <see cref="UiSystem"/>.
/// A text field is a selectable element that can be typed in, as well as copied and pasted from.
/// If an on-screen keyboard is required, then this text field will automatically open an on-screen keyboard using <see cref="MlemPlatform.OpenOnScreenKeyboard"/>.
/// This class interally uses MLEM's <see cref="TextInput"/>.
/// </summary>
public class TextField : Element {
/// <summary>
/// A <see cref="Rule"/> that allows any visible character and spaces
/// </summary>
public static readonly Rule DefaultRule = (field, add) => {
foreach (var c in add) {
if (char.IsControl(c) && (!field.Multiline || c != '\n'))
return false;
}
return true;
};
/// <summary>
/// A <see cref="Rule"/> that only allows letters
/// </summary>
public static readonly Rule OnlyLetters = (field, add) => {
foreach (var c in add) {
if (!char.IsLetter(c))
return false;
}
return true;
};
/// <summary>
/// A <see cref="Rule"/> that only allows numerals
/// </summary>
public static readonly Rule OnlyNumbers = (field, add) => {
foreach (var c in add) {
if (!char.IsNumber(c))
return false;
}
return true;
};
/// <summary>
/// A <see cref="Rule"/> that only allows letters and numerals
/// </summary>
public static readonly Rule LettersNumbers = (field, add) => {
foreach (var c in add) {
if (!char.IsLetter(c) || !char.IsNumber(c))
return false;
}
return true;
};
/// <summary>
/// A <see cref="Rule"/> that only allows characters not contained in <see cref="Path.GetInvalidPathChars"/>
/// </summary>
public static readonly Rule PathNames = (field, add) => add.IndexOfAny(Path.GetInvalidPathChars()) < 0;
/// <summary>
/// A <see cref="Rule"/> that only allows characters not contained in <see cref="Path.GetInvalidFileNameChars"/>
/// </summary>
public static readonly Rule FileNames = (field, add) => add.IndexOfAny(Path.GetInvalidFileNameChars()) < 0;
/// <inheritdoc cref="TextInput.DefaultRule"/>
public static readonly Rule DefaultRule = (field, add) => TextInput.DefaultRule(field.textInput, add);
/// <inheritdoc cref="TextInput.OnlyLetters"/>
public static readonly Rule OnlyLetters = (field, add) => TextInput.OnlyLetters(field.textInput, add);
/// <inheritdoc cref="TextInput.OnlyNumbers"/>
public static readonly Rule OnlyNumbers = (field, add) => TextInput.OnlyNumbers(field.textInput, add);
/// <inheritdoc cref="TextInput.LettersNumbers"/>
public static readonly Rule LettersNumbers = (field, add) => TextInput.LettersNumbers(field.textInput, add);
/// <inheritdoc cref="TextInput.PathNames"/>
public static readonly Rule PathNames = (field, add) => TextInput.PathNames(field.textInput, add);
/// <inheritdoc cref="TextInput.FileNames"/>
public static readonly Rule FileNames = (field, add) => TextInput.FileNames(field.textInput, add);
/// <summary>
/// The color that this text field's text should display with
@ -93,23 +54,23 @@ namespace MLEM.Ui.Elements {
/// <summary>
/// The scale that this text field should render text with
/// </summary>
public StyleProp<float> TextScale;
public StyleProp<float> TextScale {
get => this.textScale;
set {
this.textScale = value;
this.textInput.TextScale = value;
}
}
/// <summary>
/// The font that this text field should display text with
/// </summary>
public StyleProp<GenericFont> Font;
/// <summary>
/// This text field's current text
/// </summary>
public string Text => this.text.ToString();
/// <summary>
/// The text that displays in this text field if <see cref="Text"/> is empty
/// </summary>
public string PlaceholderText;
/// <summary>
/// An event that gets called when <see cref="Text"/> changes, either through input, or through a manual change.
/// </summary>
public TextChanged OnTextChange;
public StyleProp<GenericFont> Font {
get => this.font;
set {
this.font = value;
this.textInput.Font = value;
}
}
/// <summary>
/// The x position that text should start rendering at, based on the x position of this text field.
/// </summary>
@ -118,11 +79,48 @@ namespace MLEM.Ui.Elements {
/// The width that the caret should render with, in pixels
/// </summary>
public StyleProp<float> CaretWidth;
/// <summary>
/// The rule used for text input.
/// Rules allow only certain characters to be allowed inside of a text field.
/// </summary>
/// <inheritdoc cref="TextInput.Text"/>
public string Text => this.textInput.Text;
/// <inheritdoc cref="TextInput.OnTextChange"/>
public TextChanged OnTextChange;
/// <inheritdoc cref="TextInput.InputRule"/>
public Rule InputRule;
/// <inheritdoc cref="TextInput.CaretPos"/>
public int CaretPos {
get => this.textInput.CaretPos;
set => this.textInput.CaretPos = value;
}
/// <inheritdoc cref="TextInput.CaretLine"/>
public int CaretLine => this.textInput.CaretLine;
/// <inheritdoc cref="TextInput.CaretPosInLine"/>
public int CaretPosInLine => this.textInput.CaretPosInLine;
/// <inheritdoc cref="TextInput.MaskingCharacter"/>
public char? MaskingCharacter {
get => this.textInput.MaskingCharacter;
set => this.textInput.MaskingCharacter = value;
}
/// <inheritdoc cref="TextInput.MaximumCharacters"/>
public int? MaximumCharacters {
get => this.textInput.MaximumCharacters;
set => this.textInput.MaximumCharacters = value;
}
/// <inheritdoc cref="TextInput.Multiline"/>
public bool Multiline {
get => this.textInput.Multiline;
set => this.textInput.Multiline = value;
}
#if FNA
/// <inheritdoc />
// we need to make sure that the enter press doesn't get consumed by our press function so that it still works in TextInput
public override bool CanBePressed => base.CanBePressed && !this.IsSelected;
#endif
/// <summary>
/// The text that displays in this text field if <see cref="Text"/> is empty
/// </summary>
public string PlaceholderText;
/// <summary>
/// The title of the <c>KeyboardInput</c> field on mobile devices and consoles
/// </summary>
@ -131,72 +129,10 @@ namespace MLEM.Ui.Elements {
/// The description of the <c>KeyboardInput</c> field on mobile devices and consoles
/// </summary>
public string MobileDescription;
/// <summary>
/// The position of the caret within the text.
/// This is always between 0 and the <see cref="string.Length"/> of <see cref="Text"/>
/// </summary>
public int CaretPos {
get => this.caretPos;
set {
var val = MathHelper.Clamp(value, 0, this.text.Length);
if (this.caretPos != val) {
this.caretPos = val;
this.caretBlinkTimer = 0;
this.HandleTextChange(false);
}
}
}
/// <summary>
/// The line of text that the caret is currently on.
/// This can only be only non-0 if <see cref="Multiline"/> is true.
/// </summary>
public int CaretLine { get; private set; }
/// <summary>
/// The position in the current <see cref="CaretLine"/> that the caret is currently on.
/// If <see cref="Multiline"/> is false, this value is always equal to <see cref="CaretPos"/>.
/// </summary>
public int CaretPosInLine { get; private set; }
/// <summary>
/// A character that should be displayed instead of this text field's <see cref="Text"/> content.
/// The amount of masking characters displayed will be equal to the <see cref="Text"/>'s length.
/// This behavior is useful for password fields or similar.
/// </summary>
public char? MaskingCharacter {
get => this.maskingCharacter;
set {
this.maskingCharacter = value;
this.HandleTextChange(false);
}
}
/// <summary>
/// The maximum amount of characters that can be input into this text field.
/// If this is set, the length of <see cref="Text"/> will never exceed this value.
/// </summary>
public int? MaximumCharacters;
/// <summary>
/// Whether this text field should support multi-line editing.
/// If this is true, pressing <see cref="Keys.Enter"/> will insert a new line into the <see cref="Text"/> if the <see cref="InputRule"/> allows it.
/// Additionally, text will be rendered with horizontal soft wraps, and lines that are outside of the text field's bounds will be hidden.
/// </summary>
public bool Multiline {
get => this.multiline;
set {
this.multiline = value;
this.HandleTextChange(false);
}
}
private readonly StringBuilder text = new StringBuilder();
private char? maskingCharacter;
private double caretBlinkTimer;
private string displayedText;
private string[] splitText;
private int textOffset;
private int lineOffset;
private int caretPos;
private float caretDrawOffset;
private bool multiline;
private readonly TextInput textInput;
private StyleProp<GenericFont> font;
private StyleProp<float> textScale;
/// <summary>
/// Creates a new text field with the given settings
@ -208,7 +144,12 @@ namespace MLEM.Ui.Elements {
/// <param name="text">The text that the text field should contain by default</param>
/// <param name="multiline">Whether the text field should support multi-line editing</param>
public TextField(Anchor anchor, Vector2 size, Rule rule = null, GenericFont font = null, string text = null, bool multiline = false) : base(anchor, size) {
this.InputRule = rule ?? DefaultRule;
this.textInput = new TextInput(null, Vector2.Zero, 1, null, ClipboardService.SetText, ClipboardService.GetText) {
OnTextChange = (i, s) => this.OnTextChange?.Invoke(this, s),
InputRule = (i, s) => this.InputRule.Invoke(this, s)
};
this.InputRule = rule ?? TextField.DefaultRule;
this.Multiline = multiline;
if (font != null)
this.Font = font;
@ -217,75 +158,36 @@ namespace MLEM.Ui.Elements {
MlemPlatform.EnsureExists();
this.OnPressed += OnPressed;
this.OnTextInput += (element, key, character) => {
if (!this.IsSelected || this.IsHidden)
return;
if (key == Keys.Back) {
if (this.CaretPos > 0) {
this.CaretPos--;
this.RemoveText(this.CaretPos, 1);
}
} else if (key == Keys.Delete) {
this.RemoveText(this.CaretPos, 1);
} else if (this.Multiline && key == Keys.Enter) {
this.InsertText('\n');
} else {
this.InsertText(character);
}
};
this.OnDeselected += e => this.CaretPos = 0;
this.OnSelected += e => this.CaretPos = this.text.Length;
async void OnPressed(Element e) {
this.OnPressed += async e => {
var title = this.MobileTitle ?? this.PlaceholderText;
var result = await MlemPlatform.Current.OpenOnScreenKeyboard(title, this.MobileDescription, this.Text, false);
if (result != null)
this.SetText(this.Multiline ? result : result.Replace('\n', ' '), true);
};
this.OnTextInput += (element, key, character) => {
if (this.IsSelected && !this.IsHidden)
this.textInput.OnTextInput(key, character);
};
this.OnDeselected += e => this.CaretPos = 0;
this.OnSelected += e => this.CaretPos = this.textInput.Length;
}
/// <inheritdoc />
public override void SetAreaAndUpdateChildren(RectangleF area) {
base.SetAreaAndUpdateChildren(area);
this.textInput.Size = this.DisplayArea.Size / this.Scale - new Vector2(2 * this.TextOffsetX);
this.textInput.TextScale = this.TextScale;
}
/// <inheritdoc />
public override void Update(GameTime time) {
base.Update(time);
// handle first initialization if not done
if (this.displayedText == null)
this.HandleTextChange(false);
if (!this.IsSelected || this.IsHidden)
return;
if (this.Input.IsKeyPressed(Keys.Left)) {
this.CaretPos--;
} else if (this.Input.IsKeyPressed(Keys.Right)) {
this.CaretPos++;
} else if (this.Multiline && this.Input.IsKeyPressed(Keys.Up)) {
this.MoveCaretToLine(this.CaretLine - 1);
} else if (this.Multiline && this.Input.IsKeyPressed(Keys.Down)) {
this.MoveCaretToLine(this.CaretLine + 1);
} else if (this.Input.IsKeyPressed(Keys.Home)) {
this.CaretPos = 0;
} else if (this.Input.IsKeyPressed(Keys.End)) {
this.CaretPos = this.text.Length;
} else if (this.Input.IsModifierKeyDown(ModifierKey.Control)) {
if (this.Input.IsKeyPressed(Keys.V)) {
var clip = ClipboardService.GetText();
if (clip != null)
this.InsertText(clip, true);
} else if (this.Input.IsKeyPressed(Keys.C)) {
// until there is text selection, just copy the whole content
ClipboardService.SetText(this.Text);
}
}
this.caretBlinkTimer += time.ElapsedGameTime.TotalSeconds;
if (this.caretBlinkTimer >= 1)
this.caretBlinkTimer = 0;
if (this.IsSelected && !this.IsHidden)
this.textInput.Update(time, this.Input);
}
/// <inheritdoc />
public override void Draw(GameTime time, SpriteBatch batch, float alpha, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, Effect effect, Matrix matrix) {
public override void Draw(GameTime time, SpriteBatch batch, float alpha, SpriteBatchContext context) {
var tex = this.Texture;
var color = Color.White * alpha;
if (this.IsMouseOver) {
@ -294,101 +196,31 @@ namespace MLEM.Ui.Elements {
}
batch.Draw(tex, this.DisplayArea, color, this.Scale);
if (this.displayedText != null) {
var lineHeight = this.Font.Value.LineHeight * this.TextScale * this.Scale;
var offset = new Vector2(
var textPos = this.DisplayArea.Location + new Vector2(
this.TextOffsetX * this.Scale,
this.Multiline ? this.TextOffsetX * this.Scale : this.DisplayArea.Height / 2 - lineHeight / 2);
var textPos = this.DisplayArea.Location + offset;
if (this.text.Length > 0 || this.IsSelected) {
var textColor = this.TextColor.OrDefault(Color.White);
this.Font.Value.DrawString(batch, this.displayedText, textPos, textColor * alpha, 0, Vector2.Zero, this.TextScale * this.Scale, SpriteEffects.None, 0);
if (this.IsSelected && this.caretBlinkTimer < 0.5F) {
var caretDrawPos = textPos + new Vector2(this.caretDrawOffset * this.TextScale * this.Scale, 0);
if (this.Multiline)
caretDrawPos.Y += this.Font.Value.LineHeight * (this.CaretLine - this.lineOffset) * this.TextScale * this.Scale;
batch.Draw(batch.GetBlankTexture(), new RectangleF(caretDrawPos, new Vector2(this.CaretWidth * this.Scale, lineHeight)), null, textColor * alpha);
}
if (this.textInput.Length > 0 || this.IsSelected) {
this.textInput.Draw(batch, textPos, this.Scale, this.IsSelected ? this.CaretWidth : 0, this.TextColor.OrDefault(Color.White) * alpha);
} else if (this.PlaceholderText != null) {
this.Font.Value.DrawString(batch, this.PlaceholderText, textPos, this.PlaceholderColor.OrDefault(Color.Gray) * alpha, 0, Vector2.Zero, this.TextScale * this.Scale, SpriteEffects.None, 0);
}
}
base.Draw(time, batch, alpha, blendState, samplerState, depthStencilState, effect, matrix);
base.Draw(time, batch, alpha, context);
}
/// <summary>
/// Replaces this text field's text with the given text.
/// If the resulting <see cref="Text"/> exceeds <see cref="MaximumCharacters"/>, the end will be cropped to fit.
/// </summary>
/// <param name="text">The new text</param>
/// <param name="removeMismatching">If any characters that don't match the <see cref="InputRule"/> should be left out</param>
/// <inheritdoc cref="TextInput.SetText"/>
public void SetText(object text, bool removeMismatching = false) {
var strg = text?.ToString() ?? string.Empty;
if (!this.FilterText(ref strg, removeMismatching))
return;
if (this.MaximumCharacters != null && strg.Length > this.MaximumCharacters)
strg = strg.Substring(0, this.MaximumCharacters.Value);
this.text.Clear();
this.text.Append(strg);
this.CaretPos = this.text.Length;
this.HandleTextChange();
this.textInput.SetText(text, removeMismatching);
}
/// <summary>
/// Inserts the given text at the <see cref="CaretPos"/>.
/// If the resulting <see cref="Text"/> exceeds <see cref="MaximumCharacters"/>, the end will be cropped to fit.
/// </summary>
/// <param name="text">The text to insert</param>
/// <param name="removeMismatching">If any characters that don't match the <see cref="InputRule"/> should be left out</param>
/// <inheritdoc cref="TextInput.InsertText"/>
public void InsertText(object text, bool removeMismatching = false) {
var strg = text?.ToString() ?? string.Empty;
if (!this.FilterText(ref strg, removeMismatching))
return;
if (this.MaximumCharacters != null && this.text.Length + strg.Length > this.MaximumCharacters)
strg = strg.Substring(0, this.MaximumCharacters.Value - this.text.Length);
this.text.Insert(this.CaretPos, strg);
this.CaretPos += strg.Length;
this.HandleTextChange();
this.textInput.InsertText(text, removeMismatching);
}
/// <summary>
/// Removes the given amount of text at the given index
/// </summary>
/// <param name="index">The index</param>
/// <param name="length">The amount of text to remove</param>
/// <inheritdoc cref="TextInput.RemoveText"/>
public void RemoveText(int index, int length) {
if (index < 0 || index >= this.text.Length)
return;
this.text.Remove(index, length);
// ensure that caret pos is still in bounds
this.CaretPos = this.CaretPos;
this.HandleTextChange();
}
/// <summary>
/// Moves the <see cref="CaretPos"/> to the given line, if it exists.
/// Additionally maintains the <see cref="CaretPosInLine"/> roughly based on the visual distance that the caret has from the left border of the current <see cref="CaretLine"/>.
/// </summary>
/// <param name="line">The line to move the caret to</param>
/// <returns>True if the caret was moved, false if it was not (which indicates that the line with the given <paramref name="line"/> index does not exist)</returns>
public bool MoveCaretToLine(int line) {
var (destStart, destEnd) = this.GetLineBounds(line);
if (destEnd > 0) {
// find the position whose distance from the start is closest to the current distance from the start
var destAccum = "";
while (destAccum.Length < destEnd - destStart) {
if (this.Font.Value.MeasureString(destAccum).X >= this.caretDrawOffset) {
this.CaretPos = destStart + destAccum.Length;
return true;
}
destAccum += this.text[destStart + destAccum.Length];
}
// if we don't find a proper position, just move to the end of the destination line
this.CaretPos = destEnd;
return true;
}
return false;
this.textInput.RemoveText(index, length);
}
/// <inheritdoc />
@ -403,154 +235,6 @@ namespace MLEM.Ui.Elements {
this.CaretWidth = this.CaretWidth.OrStyle(style.TextFieldCaretWidth);
}
private bool FilterText(ref string text, bool removeMismatching) {
if (removeMismatching) {
var result = new StringBuilder();
foreach (var c in text) {
if (this.InputRule(this, c.ToCachedString()))
result.Append(c);
}
text = result.ToString();
} else if (!this.InputRule(this, text))
return false;
return true;
}
private void HandleTextChange(bool textChanged = true) {
// not initialized yet
if (!this.Font.HasValue())
return;
var maxWidth = this.DisplayArea.Width / this.Scale - this.TextOffsetX * 2;
if (this.Multiline) {
// soft wrap if we're multiline
this.splitText = this.Font.Value.SplitStringSeparate(this.text, maxWidth, this.TextScale).ToArray();
this.displayedText = string.Join("\n", this.splitText);
this.UpdateCaretData();
var maxHeight = this.DisplayArea.Height / this.Scale - this.TextOffsetX * 2;
if (this.Font.Value.MeasureString(this.displayedText).Y * this.TextScale > maxHeight) {
var maxLines = (maxHeight / (this.Font.Value.LineHeight * this.TextScale)).Floor();
if (this.lineOffset > this.CaretLine) {
// if we're moving up
this.lineOffset = this.CaretLine;
} else if (this.CaretLine >= maxLines) {
// if we're moving down
var limit = this.CaretLine - (maxLines - 1);
if (limit > this.lineOffset)
this.lineOffset = limit;
}
// calculate resulting string
var ret = new StringBuilder();
var lines = 0;
var originalIndex = 0;
for (var i = 0; i < this.displayedText.Length; i++) {
if (lines >= this.lineOffset) {
if (ret.Length <= 0)
this.textOffset = originalIndex;
ret.Append(this.displayedText[i]);
}
if (this.displayedText[i] == '\n') {
lines++;
if (this.text[originalIndex] == '\n')
originalIndex++;
} else {
originalIndex++;
}
if (lines - this.lineOffset >= maxLines)
break;
}
this.displayedText = ret.ToString();
} else {
this.lineOffset = 0;
this.textOffset = 0;
}
} else {
// not multiline, so scroll horizontally based on caret position
if (this.Font.Value.MeasureString(this.text).X * this.TextScale > maxWidth) {
if (this.textOffset > this.CaretPos) {
// if we're moving the caret to the left
this.textOffset = this.CaretPos;
} else {
// if we're moving the caret to the right
var importantArea = this.text.ToString(this.textOffset, Math.Min(this.CaretPos, this.text.Length) - this.textOffset);
var bound = this.CaretPos - this.Font.Value.TruncateString(importantArea, maxWidth, this.TextScale, true).Length;
if (this.textOffset < bound)
this.textOffset = bound;
}
var visible = this.text.ToString(this.textOffset, this.text.Length - this.textOffset);
this.displayedText = this.Font.Value.TruncateString(visible, maxWidth, this.TextScale);
} else {
this.displayedText = this.Text;
this.textOffset = 0;
}
this.UpdateCaretData();
}
if (this.MaskingCharacter != null)
this.displayedText = new string(this.MaskingCharacter.Value, this.displayedText.Length);
if (textChanged)
this.OnTextChange?.Invoke(this, this.Text);
}
private void UpdateCaretData() {
if (this.splitText != null) {
var line = 0;
var index = 0;
for (var d = 0; d < this.splitText.Length; d++) {
var startOfLine = 0;
var split = this.splitText[d];
for (var i = 0; i <= split.Length; i++) {
if (index == this.CaretPos) {
this.CaretLine = line;
this.CaretPosInLine = i - startOfLine;
this.caretDrawOffset = this.Font.Value.MeasureString(split.Substring(startOfLine, this.CaretPosInLine)).X;
return;
}
if (i < split.Length) {
// manual splits
if (split[i] == '\n') {
startOfLine = i + 1;
line++;
}
index++;
}
}
// max width splits
line++;
}
} else if (this.displayedText != null) {
this.CaretLine = 0;
this.CaretPosInLine = this.CaretPos;
this.caretDrawOffset = this.Font.Value.MeasureString(this.displayedText.Substring(0, this.CaretPos - this.textOffset)).X;
}
}
private (int, int) GetLineBounds(int boundLine) {
if (this.splitText != null) {
var line = 0;
var index = 0;
var startOfLineIndex = 0;
for (var d = 0; d < this.splitText.Length; d++) {
var split = this.splitText[d];
for (var i = 0; i < split.Length; i++) {
index++;
if (split[i] == '\n') {
if (boundLine == line)
return (startOfLineIndex, index - 1);
line++;
startOfLineIndex = index;
}
}
if (boundLine == line)
return (startOfLineIndex, index - 1);
line++;
startOfLineIndex = index;
}
}
return default;
}
/// <summary>
/// A delegate method used for <see cref="TextField.OnTextChange"/>
/// </summary>

View file

@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using MLEM.Extensions;
using MLEM.Input;
using MLEM.Ui.Style;
@ -11,6 +13,13 @@ namespace MLEM.Ui.Elements {
/// </summary>
public class Tooltip : Panel {
/// <summary>
/// A list of <see cref="Elements.Paragraph"/> objects that this tooltip automatically manages.
/// A paragraph that is contained in this list will automatically have the <see cref="UiStyle.TooltipTextWidth"/> and <see cref="UiStyle.TooltipTextColor"/> applied.
/// To add a paragraph to both this list and to <see cref="Element.Children"/>, use <see cref="AddParagraph(Elements.Paragraph,int)"/>.
/// </summary>
public readonly List<Paragraph> Paragraphs = new List<Paragraph>();
/// <summary>
/// The offset that this tooltip's top left corner should have from the mouse position
/// </summary>
@ -23,9 +32,41 @@ namespace MLEM.Ui.Elements {
/// The amount of time that the mouse has to be over an element before it appears
/// </summary>
public StyleProp<TimeSpan> Delay;
/// <summary>
/// The <see cref="Elements.Paragraph.TextColor"/> that this tooltip's <see cref="Paragraphs"/> should have
/// </summary>
public StyleProp<Color> ParagraphTextColor {
get => this.paragraphTextColor;
set {
this.paragraphTextColor = value;
this.UpdateParagraphsStyles();
}
}
/// <summary>
/// The <see cref="Elements.Paragraph.TextScale"/> that this tooltip's <see cref="Paragraphs"/> should have
/// </summary>
public StyleProp<float> ParagraphTextScale {
get => this.paragraphTextScale;
set {
this.paragraphTextScale = value;
this.UpdateParagraphsStyles();
}
}
/// <summary>
/// The width that this tooltip's <see cref="Paragraphs"/> should have
/// </summary>
public StyleProp<float> ParagraphWidth {
get => this.paragraphWidth;
set {
this.paragraphWidth = value;
this.UpdateParagraphsStyles();
}
}
/// <summary>
/// The paragraph of text that this tooltip displays
/// </summary>
[Obsolete("Use Paragraphs instead, which allows for multiple paragraphs to be managed by one tooltip")]
public Paragraph Paragraph;
/// <summary>
/// Determines whether this tooltip should display when <see cref="UiControls.IsAutoNavMode"/> is true, which is when the UI is being controlled using a keyboard or gamepad.
@ -45,6 +86,9 @@ namespace MLEM.Ui.Elements {
private TimeSpan delayCountdown;
private bool autoHidden;
private Element snapElement;
private StyleProp<float> paragraphWidth;
private StyleProp<float> paragraphTextScale;
private StyleProp<Color> paragraphTextColor;
/// <summary>
/// Creates a new tooltip with the given settings
@ -53,8 +97,11 @@ namespace MLEM.Ui.Elements {
/// <param name="elementToHover">The element that should automatically cause the tooltip to appear and disappear when hovered and not hovered, respectively</param>
public Tooltip(string text = null, Element elementToHover = null) :
base(Anchor.TopLeft, Vector2.One, Vector2.Zero) {
if (text != null)
this.Paragraph = this.AddChild(new Paragraph(Anchor.TopLeft, 0, text));
if (text != null) {
#pragma warning disable CS0618
this.Paragraph = this.AddParagraph(text);
#pragma warning restore CS0618
}
this.Init(elementToHover);
}
@ -65,7 +112,9 @@ namespace MLEM.Ui.Elements {
/// <param name="elementToHover">The element that should automatically cause the tooltip to appear and disappear when hovered and not hovered, respectively</param>
public Tooltip(Paragraph.TextCallback textCallback, Element elementToHover = null) :
base(Anchor.TopLeft, Vector2.One, Vector2.Zero) {
this.Paragraph = this.AddChild(new Paragraph(Anchor.TopLeft, 0, textCallback));
#pragma warning disable CS0618
this.Paragraph = this.AddParagraph(textCallback);
#pragma warning restore CS0618
this.Init(elementToHover);
}
@ -101,11 +150,47 @@ namespace MLEM.Ui.Elements {
this.MouseOffset = this.MouseOffset.OrStyle(style.TooltipOffset);
this.AutoNavOffset = this.AutoNavOffset.OrStyle(style.TooltipAutoNavOffset);
this.Delay = this.Delay.OrStyle(style.TooltipDelay);
this.ParagraphTextColor = this.ParagraphTextColor.OrStyle(style.TooltipTextColor);
this.ParagraphTextScale = this.ParagraphTextScale.OrStyle(style.TextScale);
this.ParagraphWidth = this.ParagraphWidth.OrStyle(style.TooltipTextWidth);
this.ChildPadding = this.ChildPadding.OrStyle(style.TooltipChildPadding);
if (this.Paragraph != null) {
this.Paragraph.TextColor = this.Paragraph.TextColor.OrStyle(style.TooltipTextColor, 1);
this.Paragraph.Size = new Vector2(style.TooltipTextWidth, 0);
this.UpdateParagraphsStyles();
}
/// <summary>
/// Adds the given paragraph to this tooltip's managed <see cref="Paragraphs"/> list, as well as to its children using <see cref="Element.AddChild{T}"/>.
/// A paragraph that is contained in the <see cref="Paragraphs"/> list will automatically have the <see cref="UiStyle.TooltipTextWidth"/> and <see cref="UiStyle.TooltipTextColor"/> applied.
/// </summary>
/// <param name="paragraph">The paragraph to add</param>
/// <returns>The added paragraph, for chaining</returns>
/// <param name="index">The index to add the child at, or -1 to add it to the end of the <see cref="Element.Children"/> list</param>
public Paragraph AddParagraph(Paragraph paragraph, int index = -1) {
this.Paragraphs.Add(paragraph);
this.AddChild(paragraph, index);
this.UpdateParagraphStyle(paragraph);
return paragraph;
}
/// <summary>
/// Adds a new paragraph with the given text callback to this tooltip's managed <see cref="Paragraphs"/> list, as well as to its children using <see cref="Element.AddChild{T}"/>.
/// A paragraph that is contained in the <see cref="Paragraphs"/> list will automatically have the <see cref="UiStyle.TooltipTextWidth"/> and <see cref="UiStyle.TooltipTextColor"/> applied.
/// </summary>
/// <param name="text">The text that the paragraph should display</param>
/// <returns>The created paragraph, for chaining</returns>
/// <param name="index">The index to add the child at, or -1 to add it to the end of the <see cref="Element.Children"/> list</param>
public Paragraph AddParagraph(Paragraph.TextCallback text, int index = -1) {
return this.AddParagraph(new Paragraph(Anchor.AutoLeft, 0, text), index);
}
/// <summary>
/// Adds a new paragraph with the given text to this tooltip's managed <see cref="Paragraphs"/> list, as well as to its children using <see cref="Element.AddChild{T}"/>.
/// A paragraph that is contained in the <see cref="Paragraphs"/> list will automatically have the <see cref="UiStyle.TooltipTextWidth"/> and <see cref="UiStyle.TooltipTextColor"/> applied.
/// </summary>
/// <param name="text">The text that the paragraph should display</param>
/// <returns>The created paragraph, for chaining</returns>
/// <param name="index">The index to add the child at, or -1 to add it to the end of the <see cref="Element.Children"/> list</param>
public Paragraph AddParagraph(string text, int index = -1) {
return this.AddParagraph(p => text, index);
}
/// <summary>
@ -171,7 +256,7 @@ namespace MLEM.Ui.Elements {
elementToHover.OnMouseEnter += e => this.Display(e.System, $"{e.GetType().Name}Tooltip");
elementToHover.OnMouseExit += e => this.Remove();
elementToHover.OnSelected += e => {
if (this.DisplayInAutoNavMode) {
if (this.DisplayInAutoNavMode && e.Controls.IsAutoNavMode) {
this.snapElement = e;
this.Display(e.System, $"{e.GetType().Name}Tooltip");
}
@ -185,9 +270,6 @@ namespace MLEM.Ui.Elements {
}
private void Init(Element elementToHover) {
if (this.Paragraph != null)
this.Paragraph.AutoAdjustWidth = true;
this.SetWidthBasedOnChildren = true;
this.SetHeightBasedOnChildren = true;
this.CanBeMoused = false;
@ -210,5 +292,23 @@ namespace MLEM.Ui.Elements {
}
}
private void UpdateParagraphsStyles() {
foreach (var paragraph in this.Paragraphs)
this.UpdateParagraphStyle(paragraph);
#pragma warning disable CS0618
// still set style here in case someone changed the paragraph field manually
if (this.Paragraph != null)
this.UpdateParagraphStyle(this.Paragraph);
#pragma warning restore CS0618
}
private void UpdateParagraphStyle(Paragraph paragraph) {
paragraph.TextColor = paragraph.TextColor.OrStyle(this.ParagraphTextColor, 1);
paragraph.TextScale = paragraph.TextScale.OrStyle(this.ParagraphTextScale, 1);
paragraph.Size = new Vector2(this.ParagraphWidth, 0);
paragraph.AutoAdjustWidth = true;
}
}
}

View file

@ -0,0 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<ProduceReferenceAssembly>true</ProduceReferenceAssembly>
<RootNamespace>MLEM.Ui</RootNamespace>
<DefineConstants>$(DefineConstants);FNA</DefineConstants>
</PropertyGroup>
<PropertyGroup>
<Authors>Ellpeck</Authors>
<Description>A mouse, keyboard, gamepad and touch ready Ui system for FNA that features automatic anchoring, sizing and several ready-to-use element types</Description>
<PackageReleaseNotes>See the full changelog at https://mlem.ellpeck.de/CHANGELOG</PackageReleaseNotes>
<PackageTags>fna ellpeck mlem ui user interface graphical gui system mouse keyboard gamepad touch</PackageTags>
<PackageProjectUrl>https://mlem.ellpeck.de/</PackageProjectUrl>
<RepositoryUrl>https://github.com/Ellpeck/MLEM</RepositoryUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageIcon>Logo.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="TextCopy" Version="6.1.0" />
<ProjectReference Include="..\MLEM\MLEM.FNA.csproj" />
<ProjectReference Include="..\FNA\FNA.Core.csproj">
<PrivateAssets>all</PrivateAssets>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<None Include="../Media/Logo.png" Pack="true" PackagePath="" />
<None Include="../Docs/index.md" Pack="true" PackagePath="README.md" />
</ItemGroup>
</Project>

View file

@ -7,7 +7,7 @@
<PropertyGroup>
<Authors>Ellpeck</Authors>
<Description>A mouse, keyboard, gamepad and touch ready Ui system that features automatic anchoring, sizing and several ready-to-use element types</Description>
<Description>A mouse, keyboard, gamepad and touch ready Ui system for MonoGame that features automatic anchoring, sizing and several ready-to-use element types</Description>
<PackageReleaseNotes>See the full changelog at https://mlem.ellpeck.de/CHANGELOG</PackageReleaseNotes>
<PackageTags>monogame ellpeck mlem ui user interface graphical gui system mouse keyboard gamepad touch</PackageTags>
<PackageProjectUrl>https://mlem.ellpeck.de/</PackageProjectUrl>
@ -18,7 +18,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="TextCopy" Version="4.3.1" />
<PackageReference Include="TextCopy" Version="6.1.0" />
<ProjectReference Include="..\MLEM\MLEM.csproj" />
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.0.1641">

View file

@ -0,0 +1,298 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text.RegularExpressions;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using MLEM.Formatting;
using MLEM.Misc;
using MLEM.Textures;
using MLEM.Ui.Elements;
using MLEM.Ui.Style;
namespace MLEM.Ui.Parsers {
/// <summary>
/// A class for parsing Markdown strings into a set of MLEM.Ui elements with styling for each individual <see cref="ElementType"/>.
/// To parse, use <see cref="Parse"/> or <see cref="ParseInto"/>. To style the parsed output, use <see cref="Style{T}"/> before parsing.
/// </summary>
/// <remarks>
/// Note that this parser is rather rudimentary and doesn't deal well with very complex Markdown documents. Missing features are as follows:
/// <list type="bullet">
/// <item><description>Lines that end without a double space are still converted to distinct lines rather than being merged with the next line</description></item>
/// <item><description>Better list handling, including nested lists</description></item>
/// <item><description>Horizontal rules</description></item>
/// <item><description>Tables</description></item>
/// </list>
/// </remarks>
public class UiMarkdownParser {
private static readonly ElementType[] ElementTypes = EnumHelper.GetValues<ElementType>().ToArray();
/// <summary>
/// The base path for markdown images, which is prepended to the image link.
/// </summary>
public string ImageBasePath;
/// <summary>
/// An action that is invoked when an image fails to load while parsing.
/// This action receives the expected location of the image, as well as the <see cref="Exception"/> that occured.
/// </summary>
public Action<string, Exception> ImageExceptionHandler;
/// <summary>
/// The graphics device that should be used when loading images and other graphics-dependent content.
/// </summary>
public GraphicsDevice GraphicsDevice;
/// <summary>
/// The name of the font used for inline code as well as code blocks.
/// This only has an effect if a font with this name is added to the used <see cref="UiStyle"/>'s <see cref="UiStyle.AdditionalFonts"/>.
/// This defaults to "Monospaced" if default styling is applied in <see cref="UiMarkdownParser(bool)"/>.
/// </summary>
public string CodeFont;
private readonly Dictionary<ElementType, Action<Element>> elementStyles = new Dictionary<ElementType, Action<Element>>();
/// <summary>
/// Creates a new UI markdown parser and optionally initializes some default style settings.
/// </summary>
/// <param name="applyDefaultStyling">Whether default style settings should be applied.</param>
public UiMarkdownParser(bool applyDefaultStyling = true) {
if (applyDefaultStyling) {
this.CodeFont = "Monospaced";
this.Style<VerticalSpace>(ElementType.VerticalSpace, v => v.Size = new Vector2(1, 5));
for (var i = 0; i < 6; i++) {
var level = i;
this.Style<Paragraph>(UiMarkdownParser.ElementTypes[Array.IndexOf(UiMarkdownParser.ElementTypes, ElementType.Header1) + i], p => {
p.Alignment = TextAlignment.Center;
p.TextScaleMultiplier = 2 - level * 0.15F;
});
}
}
}
/// <summary>
/// Parses the given markdown string into a set of elements (using <see cref="Parse"/>) and adds them as children to the givem <paramref name="element"/>.
/// During this process, the element stylings specified using <see cref="Style{T}"/> are also applied.
/// </summary>
/// <param name="markdown">The markdown to parse.</param>
/// <param name="element">The element to add the parsed elements to.</param>
/// <returns>The <paramref name="element"/>, for chaining.</returns>
public Element ParseInto(string markdown, Element element) {
foreach (var (_, e) in this.Parse(markdown))
element.AddChild(e);
return element;
}
/// <summary>
/// Parses the given markdown string into a set of elements and returns them along with their <see cref="ElementType"/>.
/// During this process, the element stylings specified using <see cref="Style{T}"/> are also applied.
/// </summary>
/// <param name="markdown">The markdown to parse.</param>
/// <returns>The parsed elements.</returns>
public IEnumerable<(ElementType, Element)> Parse(string markdown) {
foreach (var (t, e) in this.ParseUnstyled(markdown)) {
if (this.elementStyles.TryGetValue(t, out var style))
style.Invoke(e);
yield return (t, e);
}
}
/// <summary>
/// Specifies an action to be invoked when a new element with the given <see cref="ElementType"/> is parsed in <see cref="Parse"/> or <see cref="ParseInto"/>.
/// These actions can be used to modify the style properties of the created elements.
/// </summary>
/// <param name="types">The element types that should be styled. Can be a combined flag.</param>
/// <param name="style">The action that styles the elements with the given element type.</param>
/// <param name="add">Whether the <paramref name="style"/> function should be added to the existing style settings, or replace them.</param>
/// <typeparam name="T">The type of elements that the given <see cref="ElementType"/> flags are expected to be.</typeparam>
/// <returns>This parser, for chaining.</returns>
public UiMarkdownParser Style<T>(ElementType types, Action<T> style, bool add = false) where T : Element {
foreach (var type in UiMarkdownParser.ElementTypes) {
if (types.HasFlag(type)) {
if (add && this.elementStyles.ContainsKey(type)) {
this.elementStyles[type] += Action;
} else {
this.elementStyles[type] = Action;
}
}
}
return this;
void Action(Element e) {
style.Invoke(e as T ?? throw new ArgumentException($"Expected {typeof(T)} for style action but got {e.GetType()}"));
}
}
private IEnumerable<(ElementType, Element)> ParseUnstyled(string markdown) {
var inCodeBlock = false;
foreach (var line in markdown.Split('\n')) {
// code blocks
if (line.Trim().StartsWith("```")) {
inCodeBlock = !inCodeBlock;
continue;
}
// code block content
if (inCodeBlock) {
yield return (ElementType.CodeBlock, new Paragraph(Anchor.AutoLeft, 1, $"<f {this.CodeFont}>{line}</f>"));
continue;
}
// quotes
if (line.StartsWith(">")) {
yield return (ElementType.Blockquote, new Paragraph(Anchor.AutoLeft, 1, line.Substring(1).Trim()));
continue;
}
// vertical space (empty lines)
if (line.Trim().Length <= 0) {
yield return (ElementType.VerticalSpace, new VerticalSpace(0));
continue;
}
// images
var imageMatch = Regex.Match(line, @"!\[\]\(([^)]+)\)");
if (imageMatch.Success) {
if (this.GraphicsDevice == null)
throw new NullReferenceException("A markdown parser requires a GraphicsDevice for parsing images");
TextureRegion image = null;
LoadImageAsync();
yield return (ElementType.Image, new Image(Anchor.AutoLeft, new Vector2(1, -1), _ => image) {
OnDisposed = e => image?.Texture.Dispose()
});
async void LoadImageAsync() {
var loc = imageMatch.Groups[1].Value;
// only apply the base path for relative files
if (this.ImageBasePath != null && !loc.StartsWith("http") && !Path.IsPathRooted(loc))
loc = $"{this.ImageBasePath}/{loc}";
try {
Texture2D tex;
if (loc.StartsWith("http")) {
using (var client = new HttpClient()) {
using (var src = await client.GetStreamAsync(loc)) {
using (var memory = new MemoryStream()) {
// download the full stream before passing it to texture
await src.CopyToAsync(memory);
tex = Texture2D.FromStream(this.GraphicsDevice, memory);
}
}
}
} else {
using (var stream = Path.IsPathRooted(loc) ? File.OpenRead(loc) : TitleContainer.OpenStream(loc))
tex = Texture2D.FromStream(this.GraphicsDevice, stream);
}
image = new TextureRegion(tex);
} catch (Exception e) {
if (this.ImageExceptionHandler != null) {
this.ImageExceptionHandler.Invoke(loc, e);
} else {
throw new Exception($"Couldn't parse image {loc}, and no ImageExceptionHandler was set", e);
}
}
}
continue;
}
// headers
var parsedHeader = false;
for (var h = 6; h >= 1; h--) {
if (line.StartsWith(new string('#', h))) {
var type = UiMarkdownParser.ElementTypes[Array.IndexOf(UiMarkdownParser.ElementTypes, ElementType.Header1) + h - 1];
yield return (type, new Paragraph(Anchor.AutoLeft, 1, line.Substring(h).Trim()));
parsedHeader = true;
break;
}
}
if (parsedHeader)
continue;
// parse everything else as a paragraph (with formatting)
var par = line;
// replace links
par = Regex.Replace(par, @"<([^>]+)>", "<l $1>$1</l>");
par = Regex.Replace(par, @"\[([^\]]+)\]\(([^)]+)\)", "<l $2>$1</l>");
// replace formatting
par = Regex.Replace(par, @"\*\*([^\*]+)\*\*", "<b>$1</b>");
par = Regex.Replace(par, @"__([^_]+)__", "<b>$1</b>");
par = Regex.Replace(par, @"\*([^\*]+)\*", "<i>$1</i>");
par = Regex.Replace(par, @"_([^_]+)_", "<i>$1</i>");
par = Regex.Replace(par, @"~~([^~]+)~~", "<st>$1</st>");
// replace inline code with custom code font
par = Regex.Replace(par, @"`([^`]+)`", $"<f {this.CodeFont}>$1</f>");
yield return (ElementType.Paragraph, new Paragraph(Anchor.AutoLeft, 1, par));
}
}
/// <summary>
/// A flags enumeration used by <see cref="UiMarkdownParser"/> that contains the types of elements that can be parsed and returned in <see cref="Parse"/> or <see cref="UiMarkdownParser.ParseInto"/>.
/// This is a flags enumeration so that <see cref="UiMarkdownParser.Style{T}"/> can have multiple element types being styled at the same time.
/// </summary>
[Flags]
public enum ElementType {
/// <summary>
/// A blockquote.
/// This element type is a <see cref="Paragraph"/>.
/// </summary>
Blockquote = 1,
/// <summary>
/// A vertical space, which is a gap between multiple markdown paragraphs.
/// This element type is a <see cref="VerticalSpace"/>.
/// </summary>
VerticalSpace = 2,
/// <summary>
/// An image.
/// This element type is an <see cref="Image"/>.
/// </summary>
Image = 4,
/// <summary>
/// A header with header level 1.
/// This element type is a <see cref="Paragraph"/>.
/// </summary>
Header1 = 8,
/// <summary>
/// A header with header level 2.
/// This element type is a <see cref="Paragraph"/>.
/// </summary>
Header2 = 16,
/// <summary>
/// A header with header level 3.
/// This element type is a <see cref="Paragraph"/>.
/// </summary>
Header3 = 32,
/// <summary>
/// A header with header level 4.
/// This element type is a <see cref="Paragraph"/>.
/// </summary>
Header4 = 64,
/// <summary>
/// A header with header level 5.
/// This element type is a <see cref="Paragraph"/>.
/// </summary>
Header5 = 128,
/// <summary>
/// A header with header level 6.
/// This element type is a <see cref="Paragraph"/>.
/// </summary>
Header6 = 256,
/// <summary>
/// A combined flag that contains <see cref="Header1"/> through <see cref="Header6"/>.
/// This element type is a <see cref="Paragraph"/>.
/// </summary>
Headers = ElementType.Header1 | ElementType.Header2 | ElementType.Header3 | ElementType.Header4 | ElementType.Header5 | ElementType.Header6,
/// <summary>
/// A paragraph, which is one line of markdown text.
/// This element type is a <see cref="Paragraph"/>.
/// </summary>
Paragraph = 512,
/// <summary>
/// A single line of a code block.
/// This element type is a <see cref="Paragraph"/>.
/// </summary>
CodeBlock = 1024
}
}
}

View file

@ -57,7 +57,7 @@ namespace MLEM.Ui {
/// <summary>
/// The <see cref="RootElement"/> that is currently active.
/// The active root element is the one with the highest <see cref="RootElement.Priority"/> that whose <see cref="RootElement.CanSelectContent"/> property is true.
/// The active root element is the one with the highest <see cref="RootElement.Priority"/> that <see cref="RootElement.CanBeActive"/>.
/// </summary>
public RootElement ActiveRoot { get; protected set; }
/// <summary>
@ -155,29 +155,33 @@ namespace MLEM.Ui {
public virtual void Update() {
if (this.IsInputOurs)
this.Input.Update();
this.ActiveRoot = this.System.GetRootElements().FirstOrDefault(root => !root.Element.IsHidden && root.CanSelectContent);
this.ActiveRoot = this.System.GetRootElements().FirstOrDefault(root => root.CanBeActive);
// MOUSE INPUT
if (this.HandleMouse) {
var mousedNow = this.GetElementUnderPos(this.Input.ViewportMousePosition.ToVector2());
var mousedNow = this.GetElementUnderPos(new Vector2(this.Input.ViewportMousePosition.X, this.Input.ViewportMousePosition.Y));
this.SetMousedElement(mousedNow);
if (this.Input.IsMouseButtonPressed(MouseButton.Left)) {
if (this.Input.IsMouseButtonPressedAvailable(MouseButton.Left)) {
this.IsAutoNavMode = false;
var selectedNow = mousedNow != null && mousedNow.CanBeSelected ? mousedNow : null;
this.SelectElement(this.ActiveRoot, selectedNow);
if (mousedNow != null && mousedNow.CanBePressed)
if (mousedNow != null && mousedNow.CanBePressed) {
this.System.InvokeOnElementPressed(mousedNow);
} else if (this.Input.IsMouseButtonPressed(MouseButton.Right)) {
this.Input.TryConsumeMouseButtonPressed(MouseButton.Left);
}
} else if (this.Input.IsMouseButtonPressedAvailable(MouseButton.Right)) {
this.IsAutoNavMode = false;
if (mousedNow != null && mousedNow.CanBePressed)
if (mousedNow != null && mousedNow.CanBePressed) {
this.System.InvokeOnElementSecondaryPressed(mousedNow);
this.Input.TryConsumeMouseButtonPressed(MouseButton.Right);
}
}
}
// KEYBOARD INPUT
if (this.HandleKeyboard) {
if (this.KeyboardButtons.IsPressed(this.Input, this.GamepadIndex)) {
if (this.KeyboardButtons.IsPressedAvailable(this.Input, this.GamepadIndex)) {
if (this.SelectedElement?.Root != null && this.SelectedElement.CanBePressed) {
if (this.Input.IsModifierKeyDown(ModifierKey.Shift)) {
// secondary action on element using space or enter
@ -186,15 +190,19 @@ namespace MLEM.Ui {
// first action on element using space or enter
this.System.InvokeOnElementPressed(this.SelectedElement);
}
this.KeyboardButtons.TryConsumePressed(this.Input, this.GamepadIndex);
}
} else if (this.Input.IsKeyPressed(Keys.Tab)) {
} else if (this.Input.IsKeyPressedAvailable(Keys.Tab)) {
this.IsAutoNavMode = true;
// tab or shift-tab to next or previous element
var backward = this.Input.IsModifierKeyDown(ModifierKey.Shift);
var next = this.GetTabNextElement(backward);
if (this.SelectedElement?.Root != null)
next = this.SelectedElement.GetTabNextElement(backward, next);
if (next != this.SelectedElement) {
this.SelectElement(this.ActiveRoot, next);
this.Input.TryConsumeKeyPressed(Keys.Tab);
}
}
}
@ -230,20 +238,28 @@ namespace MLEM.Ui {
// GAMEPAD INPUT
if (this.HandleGamepad) {
if (this.GamepadButtons.IsPressed(this.Input, this.GamepadIndex)) {
if (this.SelectedElement?.Root != null && this.SelectedElement.CanBePressed)
if (this.GamepadButtons.IsPressedAvailable(this.Input, this.GamepadIndex)) {
if (this.SelectedElement?.Root != null && this.SelectedElement.CanBePressed) {
this.System.InvokeOnElementPressed(this.SelectedElement);
} else if (this.SecondaryGamepadButtons.IsPressed(this.Input, this.GamepadIndex)) {
if (this.SelectedElement?.Root != null && this.SelectedElement.CanBePressed)
this.GamepadButtons.TryConsumePressed(this.Input, this.GamepadIndex);
}
} else if (this.SecondaryGamepadButtons.IsPressedAvailable(this.Input, this.GamepadIndex)) {
if (this.SelectedElement?.Root != null && this.SelectedElement.CanBePressed) {
this.System.InvokeOnElementSecondaryPressed(this.SelectedElement);
} else if (this.DownButtons.IsPressed(this.Input, this.GamepadIndex)) {
this.HandleGamepadNextElement(Direction2.Down);
} else if (this.LeftButtons.IsPressed(this.Input, this.GamepadIndex)) {
this.HandleGamepadNextElement(Direction2.Left);
} else if (this.RightButtons.IsPressed(this.Input, this.GamepadIndex)) {
this.HandleGamepadNextElement(Direction2.Right);
} else if (this.UpButtons.IsPressed(this.Input, this.GamepadIndex)) {
this.HandleGamepadNextElement(Direction2.Up);
this.SecondaryGamepadButtons.TryConsumePressed(this.Input, this.GamepadIndex);
}
} else if (this.DownButtons.IsPressedAvailable(this.Input, this.GamepadIndex)) {
if (this.HandleGamepadNextElement(Direction2.Down))
this.DownButtons.TryConsumePressed(this.Input, this.GamepadIndex);
} else if (this.LeftButtons.IsPressedAvailable(this.Input, this.GamepadIndex)) {
if (this.HandleGamepadNextElement(Direction2.Left))
this.LeftButtons.TryConsumePressed(this.Input, this.GamepadIndex);
} else if (this.RightButtons.IsPressedAvailable(this.Input, this.GamepadIndex)) {
if (this.HandleGamepadNextElement(Direction2.Right))
this.RightButtons.TryConsumePressed(this.Input, this.GamepadIndex);
} else if (this.UpButtons.IsPressedAvailable(this.Input, this.GamepadIndex)) {
if (this.HandleGamepadNextElement(Direction2.Up))
this.UpButtons.TryConsumePressed(this.Input, this.GamepadIndex);
}
}
}
@ -339,8 +355,6 @@ namespace MLEM.Ui {
if (root == null)
return null;
this.selectedElements.TryGetValue(root.Name, out var element);
if (element != null && !element.CanBeSelected)
return null;
return element;
}
@ -353,15 +367,17 @@ namespace MLEM.Ui {
protected virtual Element GetTabNextElement(bool backward) {
if (this.ActiveRoot == null)
return null;
var children = this.ActiveRoot.Element.GetChildren(c => !c.IsHidden, true, true).Append(this.ActiveRoot.Element);
var children = this.ActiveRoot.Element.GetChildren(c => !c.IsHidden, true, true).Append(this.ActiveRoot.Element)
// we can't add these checks to GetChildren because it ignores false grandchildren
.Where(c => c.CanBeSelected && c.AutoNavGroup == this.SelectedElement?.AutoNavGroup);
if (this.SelectedElement?.Root != this.ActiveRoot) {
return backward ? children.LastOrDefault(c => c.CanBeSelected) : children.FirstOrDefault(c => c.CanBeSelected);
// if we don't have an element selected in this root, navigate to the first one without a group
var allowed = children.Where(c => c.AutoNavGroup == null);
return backward ? allowed.LastOrDefault() : allowed.FirstOrDefault();
} else {
var foundCurr = false;
Element lastFound = null;
foreach (var child in children) {
if (!child.CanBeSelected)
continue;
if (child == this.SelectedElement) {
// when going backwards, return the last element found before the current one
if (backward)
@ -386,17 +402,20 @@ namespace MLEM.Ui {
protected virtual Element GetGamepadNextElement(Direction2 direction) {
if (this.ActiveRoot == null)
return null;
var children = this.ActiveRoot.Element.GetChildren(c => !c.IsHidden, true, true).Append(this.ActiveRoot.Element);
var children = this.ActiveRoot.Element.GetChildren(c => !c.IsHidden, true, true).Append(this.ActiveRoot.Element)
// we can't add these checks to GetChildren because it ignores false grandchildren
.Where(c => c.CanBeSelected && c.AutoNavGroup == this.SelectedElement?.AutoNavGroup);
if (this.SelectedElement?.Root != this.ActiveRoot) {
return children.FirstOrDefault(c => c.CanBeSelected);
// if we don't have an element selected in this root, navigate to the first one without a group
return children.FirstOrDefault(c => c.AutoNavGroup == null);
} else {
Element closest = null;
float closestPriority = 0;
foreach (var child in children) {
if (!child.CanBeSelected || child == this.SelectedElement)
if (child == this.SelectedElement)
continue;
var (xOffset, yOffset) = child.Area.Center - this.SelectedElement.Area.Center;
var angle = Math.Abs(direction.Angle() - (float) Math.Atan2(yOffset, xOffset));
var offset = child.Area.Center - this.SelectedElement.Area.Center;
var angle = Math.Abs(MathHelper.WrapAngle(direction.Angle() - (float) Math.Atan2(offset.Y, offset.X)));
if (angle >= MathHelper.PiOver2 - Element.Epsilon)
continue;
var distSq = child.Area.DistanceSquared(this.SelectedElement.Area);
@ -411,13 +430,16 @@ namespace MLEM.Ui {
}
}
private void HandleGamepadNextElement(Direction2 dir) {
private bool HandleGamepadNextElement(Direction2 dir) {
this.IsAutoNavMode = true;
var next = this.GetGamepadNextElement(dir);
if (this.SelectedElement != null)
next = this.SelectedElement.GetGamepadNextElement(dir, next);
if (next != null)
if (next != null) {
this.SelectElement(this.ActiveRoot, next);
return true;
}
return false;
}
}

View file

@ -5,7 +5,6 @@ using MLEM.Ui.Elements;
namespace MLEM.Ui {
/// <summary>
/// A snapshot of update and rendering statistics from <see cref="UiSystem.Metrics"/> to be used for runtime debugging and profiling.
/// This metrics struct works similarly to <see cref="GraphicsMetrics"/>.
/// </summary>
public struct UiMetrics {
@ -33,12 +32,12 @@ namespace MLEM.Ui {
public uint Updates { get; internal set; }
/// <summary>
/// The amount of time that <see cref="Element.Draw"/> took.
/// The amount of time that <see cref="Element.Draw(Microsoft.Xna.Framework.GameTime,Microsoft.Xna.Framework.Graphics.SpriteBatch,float,MLEM.Graphics.SpriteBatchContext)"/> took.
/// Can be divided by <see cref="Draws"/> to get an average per draw.
/// </summary>
public TimeSpan DrawTime { get; internal set; }
/// <summary>
/// The amount of times that <see cref="Element.Draw"/> was called.
/// The amount of times that <see cref="Element.Draw(Microsoft.Xna.Framework.GameTime,Microsoft.Xna.Framework.Graphics.SpriteBatch,float,MLEM.Graphics.SpriteBatchContext)"/> was called.
/// </summary>
public uint Draws { get; internal set; }

View file

@ -7,6 +7,7 @@ using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using MLEM.Formatting;
using MLEM.Formatting.Codes;
using MLEM.Graphics;
using MLEM.Input;
using MLEM.Misc;
using MLEM.Textures;
@ -77,23 +78,32 @@ namespace MLEM.Ui {
/// <summary>
/// The blend state that this ui system and all of its elements draw with
/// </summary>
[Obsolete("Set this through SpriteBatchContext instead")]
public BlendState BlendState;
/// <summary>
/// The sampler state that this ui system and all of its elements draw with.
/// The default is <see cref="Microsoft.Xna.Framework.Graphics.SamplerState.PointClamp"/>, as that is the one that works best with pixel graphics.
/// </summary>
public SamplerState SamplerState = SamplerState.PointClamp;
[Obsolete("Set this through SpriteBatchContext instead")]
public SamplerState SamplerState;
/// <summary>
/// The depth stencil state that this ui system and all of its elements draw with.
/// The default is <see cref="Microsoft.Xna.Framework.Graphics.DepthStencilState.None"/>, which is also the default for <see cref="SpriteBatch.Begin"/>.
/// The default is <see cref="Microsoft.Xna.Framework.Graphics.DepthStencilState.None"/>, which is also the default for <c>SpriteBatch.Begin</c>.
/// </summary>
public DepthStencilState DepthStencilState = DepthStencilState.None;
[Obsolete("Set this through SpriteBatchContext instead")]
public DepthStencilState DepthStencilState;
/// <summary>
/// The effect that this ui system and all of its elements draw with.
/// The default is null, which means that no custom effect will be used.
/// </summary>
[Obsolete("Set this through SpriteBatchContext instead")]
public Effect Effect;
/// <summary>
/// The spriteb atch context that this ui system and all of its elements should draw with.
/// The default <see cref="MLEM.Graphics.SpriteBatchContext.SamplerState"/> is <see cref="Microsoft.Xna.Framework.Graphics.SamplerState.PointClamp"/>, as that is the one that works best with pixel graphics.
/// </summary>
public SpriteBatchContext SpriteBatchContext = new SpriteBatchContext(samplerState: SamplerState.PointClamp);
/// <summary>
/// The <see cref="TextFormatter"/> that this ui system's <see cref="Paragraph"/> elements format their text with.
/// To add new formatting codes to the ui system, add them to this formatter.
/// </summary>
@ -231,10 +241,10 @@ namespace MLEM.Ui {
MlemPlatform.Current?.AddTextInputListener(game.Window, (sender, key, character) => this.ApplyToAll(e => e.OnTextInput?.Invoke(e, key, character)));
if (automaticViewport) {
this.Viewport = new Rectangle(Point.Zero, game.Window.ClientBounds.Size);
this.AutoScaleReferenceSize = this.Viewport.Size;
this.Viewport = new Rectangle(0, 0, game.Window.ClientBounds.Width, game.Window.ClientBounds.Height);
this.AutoScaleReferenceSize = new Point(this.Viewport.Width, this.Viewport.Height);
game.Window.ClientSizeChanged += (sender, args) => {
this.Viewport = new Rectangle(Point.Zero, game.Window.ClientBounds.Size);
this.Viewport = new Rectangle(0, 0, game.Window.ClientBounds.Width, game.Window.ClientBounds.Height);
};
}
@ -296,9 +306,24 @@ namespace MLEM.Ui {
foreach (var root in this.rootElements) {
if (root.Element.IsHidden)
continue;
batch.Begin(SpriteSortMode.Deferred, this.BlendState, this.SamplerState, this.DepthStencilState, null, this.Effect, root.Transform);
var alpha = this.DrawAlpha * root.Element.DrawAlpha;
root.Element.DrawTransformed(time, batch, alpha, this.BlendState, this.SamplerState, this.DepthStencilState, this.Effect, root.Transform);
var context = this.SpriteBatchContext;
context.TransformMatrix = root.Transform * context.TransformMatrix;
#pragma warning disable CS0618
if (this.BlendState != null)
context.BlendState = this.BlendState;
if (this.SamplerState != null)
context.SamplerState = this.SamplerState;
if (this.DepthStencilState != null)
context.DepthStencilState = this.DepthStencilState;
if (this.Effect != null)
context.Effect = this.Effect;
#pragma warning restore CS0618
batch.Begin(context);
#pragma warning disable CS0618
root.Element.DrawTransformed(time, batch, this.DrawAlpha * root.Element.DrawAlpha, context.BlendState, context.SamplerState, context.DepthStencilState, context.Effect, context.TransformMatrix);
#pragma warning restore CS0618
batch.End();
}
@ -483,7 +508,6 @@ namespace MLEM.Ui {
/// The <see cref="UiSystem"/> that this root element is a part of.
/// </summary>
public readonly UiSystem System;
private float scale = 1;
/// <summary>
/// The scale of this root element.
/// Note that, to change the scale of every root element, you can use <see cref="UiSystem.GlobalScale"/>
@ -497,7 +521,6 @@ namespace MLEM.Ui {
this.Element.ForceUpdateArea();
}
}
private int priority;
/// <summary>
/// The priority of this root element.
/// A higher priority means the element will be updated first, as well as rendered on top.
@ -514,7 +537,6 @@ namespace MLEM.Ui {
/// This is a combination of this root element's <see cref="Scale"/> as well as the ui system's <see cref="UiSystem.GlobalScale"/>.
/// </summary>
public float ActualScale => this.System.GlobalScale * this.Scale;
/// <summary>
/// The transformation that this root element (and all of its children) has.
/// This transform is applied both to input, as well as to rendering.
@ -524,7 +546,6 @@ namespace MLEM.Ui {
/// An inversion of <see cref="Transform"/>
/// </summary>
public Matrix InvTransform => Matrix.Invert(this.Transform);
/// <summary>
/// The child element of this root element that is currently selected.
/// If there is no selected element in this root, this value will be <c>null</c>.
@ -532,9 +553,17 @@ namespace MLEM.Ui {
public Element SelectedElement => this.System.Controls.GetSelectedElement(this);
/// <summary>
/// Determines whether this root element contains any children that <see cref="Elements.Element.CanBeSelected"/>.
/// This value is automatically calculated.
/// This value is automatically calculated, and used in <see cref="CanBeActive"/>.
/// </summary>
public bool CanSelectContent => this.Element.CanBeSelected || this.Element.GetChildren(c => c.CanBeSelected, true).Any();
/// <summary>
/// Determines whether this root element can become the <see cref="UiControls.ActiveRoot"/>.
/// This property returns <see langword="true"/> if <see cref="CanSelectContent"/> is <see langword="true"/> and the <see cref="Element"/> is not hidden, or if it has been set to <see langword="true"/> manually.
/// </summary>
public bool CanBeActive {
get => this.canBeActive || (!this.Element.IsHidden && this.CanSelectContent);
set => this.canBeActive = value;
}
/// <summary>
/// Event that is invoked when a <see cref="Element"/> is added to this root element or any of its children.
@ -553,6 +582,10 @@ namespace MLEM.Ui {
/// </summary>
public event Action<UiSystem> OnRemovedFromUi;
private float scale = 1;
private bool canBeActive;
private int priority;
internal RootElement(string name, Element element, UiSystem system) {
this.Name = name;
this.Element = element;

View file

@ -58,7 +58,7 @@ namespace MLEM.Cameras {
}
/// <summary>
/// The matrix that this camera "sees", based on its position and scale.
/// Use this in your <see cref="SpriteBatch.Begin"/> calls to render based on the camera's viewport.
/// Use this in your <c>SpriteBatch.Begin</c> calls to render based on the camera's viewport.
/// </summary>
public Matrix ViewMatrix {
get {
@ -105,7 +105,7 @@ namespace MLEM.Cameras {
/// <param name="roundPosition">Whether the camera's <see cref="Position"/> should be rounded to full integers when calculating the <see cref="ViewMatrix"/></param>
public Camera(GraphicsDevice graphicsDevice, bool roundPosition = true) {
this.graphicsDevice = graphicsDevice;
this.AutoScaleReferenceSize = this.Viewport.Size;
this.AutoScaleReferenceSize = new Point(this.Viewport.Width, this.Viewport.Height);
this.RoundPosition = roundPosition;
}
@ -164,7 +164,7 @@ namespace MLEM.Cameras {
if (this.Max.Y > max.Y)
this.Max = new Vector2(this.Max.X, max.Y);
}
return !this.Position.Equals(lastPos, Epsilon);
return !this.Position.Equals(lastPos, Camera.Epsilon);
}
/// <summary>
@ -173,7 +173,7 @@ namespace MLEM.Cameras {
/// <param name="delta">The amount to zoom in or out by</param>
/// <param name="zoomCenter">The position that should be regarded as the zoom's center, in screen space. The default value is the center.</param>
public void Zoom(float delta, Vector2? zoomCenter = null) {
var center = (zoomCenter ?? this.Viewport.Size.ToVector2() / 2) / this.ActualScale;
var center = (zoomCenter ?? new Vector2(this.Viewport.Width, this.Viewport.Height) / 2) / this.ActualScale;
var lastScale = this.Scale;
this.Scale += delta;
this.Position += center * ((this.Scale - lastScale) / this.Scale);

View file

@ -15,9 +15,9 @@ namespace MLEM.Extensions {
/// <param name="c">The character to turn into a string</param>
/// <returns>A string representing the character</returns>
public static string ToCachedString(this char c) {
if (!Cache.TryGetValue(c, out var ret)) {
if (!CharExtensions.Cache.TryGetValue(c, out var ret)) {
ret = c.ToString();
Cache.Add(c, ret);
CharExtensions.Cache.Add(c, ret);
}
return ret;
}

View file

@ -50,7 +50,7 @@ namespace MLEM.Extensions {
/// <param name="value">The number to parse.</param>
/// <returns>The resulting color.</returns>
public static Color FromHexRgba(int value) {
return new Color((value >> 16) & 0xFF, (value >> 8) & 0xFF, (value >> 0) & 0xFF, (value >> 24) & 0xFF);
return new Color(value >> 16 & 0xFF, value >> 8 & 0xFF, value >> 0 & 0xFF, value >> 24 & 0xFF);
}
/// <summary>
@ -60,7 +60,7 @@ namespace MLEM.Extensions {
/// <param name="value">The number to parse.</param>
/// <returns>The resulting color.</returns>
public static Color FromHexRgb(int value) {
return new Color((value >> 16) & 0xFF, (value >> 8) & 0xFF, (value >> 0) & 0xFF);
return new Color(value >> 16 & 0xFF, value >> 8 & 0xFF, value >> 0 & 0xFF);
}
/// <summary>
@ -73,7 +73,7 @@ namespace MLEM.Extensions {
if (value.StartsWith("#"))
value = value.Substring(1);
var val = int.Parse(value, NumberStyles.HexNumber);
return value.Length > 6 ? FromHexRgba(val) : FromHexRgb(val);
return value.Length > 6 ? ColorHelper.FromHexRgba(val) : ColorHelper.FromHexRgb(val);
}
}

View file

@ -21,18 +21,18 @@ namespace MLEM.Extensions {
manager.IsFullScreen = fullscreen;
if (fullscreen) {
var view = manager.GraphicsDevice.Viewport;
lastWidth = view.Width;
lastHeight = view.Height;
GraphicsExtensions.lastWidth = view.Width;
GraphicsExtensions.lastHeight = view.Height;
var curr = GraphicsAdapter.DefaultAdapter.CurrentDisplayMode;
manager.PreferredBackBufferWidth = curr.Width;
manager.PreferredBackBufferHeight = curr.Height;
} else {
if (lastWidth <= 0 || lastHeight <= 0)
if (GraphicsExtensions.lastWidth <= 0 || GraphicsExtensions.lastHeight <= 0)
throw new InvalidOperationException("Can't call SetFullscreen to change out of fullscreen mode without going into fullscreen mode first");
manager.PreferredBackBufferWidth = lastWidth;
manager.PreferredBackBufferHeight = lastHeight;
manager.PreferredBackBufferWidth = GraphicsExtensions.lastWidth;
manager.PreferredBackBufferHeight = GraphicsExtensions.lastHeight;
}
manager.ApplyChanges();
}
@ -57,7 +57,7 @@ namespace MLEM.Extensions {
/// <param name="manager">The graphics device manager</param>
/// <param name="window">The window whose bounds to use</param>
public static void ResetWidthAndHeight(this GraphicsDeviceManager manager, GameWindow window) {
var (_, _, width, height) = window.ClientBounds;
var (width, height) = (window.ClientBounds.Width, window.ClientBounds.Height);
manager.PreferredBackBufferWidth = Math.Max(height, width);
manager.PreferredBackBufferHeight = Math.Min(height, width);
manager.ApplyChanges();
@ -90,7 +90,12 @@ namespace MLEM.Extensions {
/// <param name="target">The target to apply</param>
public TargetContext(GraphicsDevice device, RenderTarget2D target) {
this.device = device;
#if FNA
// RenderTargetCount doesn't exist in FNA but we still want the optimization in MG
this.lastTargets = device.GetRenderTargets();
#else
this.lastTargets = device.RenderTargetCount <= 0 ? null : device.GetRenderTargets();
#endif
device.SetRenderTarget(target);
}

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