mirror of
https://github.com/Ellpeck/MLEM.git
synced 2024-11-25 05:58:35 +01:00
Compare commits
82 commits
c1d1c03063
...
a1c5b8e2d6
Author | SHA1 | Date | |
---|---|---|---|
a1c5b8e2d6 | |||
a7c6230434 | |||
4854d420e0 | |||
393bd9ffe5 | |||
30b4d5fc43 | |||
df0ad68837 | |||
37f0470e4f | |||
94dec34470 | |||
6a3c797eba | |||
3ad024b95a | |||
ae6ce6e7d5 | |||
54ca580dd3 | |||
4e122175b2 | |||
c6fe72bdc9 | |||
bb7192b3cc | |||
c5b2b8798e | |||
1f4f0cfa44 | |||
4aff5a2875 | |||
bb22bbdf75 | |||
fa34258bbe | |||
8fa94f1186 | |||
d5f3453c71 | |||
35e3896dc1 | |||
5d97ab033f | |||
b77edd80d5 | |||
f166c3d256 | |||
2c90ca9b89 | |||
cd32372994 | |||
0f4e67f20f | |||
96f0c51757 | |||
180f82d2c7 | |||
|
2d3e291431 | ||
|
9e8ff82579 | ||
7c18aad8f7 | |||
a14a37cb91 | |||
45955bb5e8 | |||
cb8fed87e5 | |||
67388c106b | |||
48a4aa0588 | |||
d5ec0b8001 | |||
b260aa6901 | |||
af7c341d83 | |||
c360c90f28 | |||
856d69b7db | |||
3a275a1820 | |||
2dcd5c3fa7 | |||
0918e1700b | |||
48b96a10a4 | |||
ad2784a67e | |||
ab76ea5ba8 | |||
ed88862194 | |||
c880c3e011 | |||
a143aef67c | |||
dc6c472b84 | |||
3c4567e4a1 | |||
f7cf9460d6 | |||
c7f021e62d | |||
f3e6df6862 | |||
94b6aa0d1b | |||
faa400c4e6 | |||
dbf370c968 | |||
58a0f8915a | |||
3edd593886 | |||
9ccb3536a0 | |||
6b81054159 | |||
80a6c6b5e2 | |||
c28f6d858c | |||
68fc02b170 | |||
6e75e9ebb4 | |||
b46975391b | |||
a61d7a9722 | |||
2699d0e1c2 | |||
04fab568f8 | |||
b2b34abcd0 | |||
29bbd61f8b | |||
b4f79f0753 | |||
7e49eaef10 | |||
5d9cccc9fd | |||
c060d78010 | |||
f5adf50823 | |||
17ed82fc3c | |||
7f3abdada5 |
94 changed files with 2433 additions and 1164 deletions
85
CHANGELOG.md
85
CHANGELOG.md
|
@ -2,10 +2,95 @@
|
|||
MLEM tries to adhere to [semantic versioning](https://semver.org/). Breaking changes are written in **bold**.
|
||||
|
||||
Jump to version:
|
||||
- [5.3.0](#530)
|
||||
- [5.2.0](#520)
|
||||
- [5.1.0](#510)
|
||||
- [5.0.0](#500)
|
||||
|
||||
## 5.3.0
|
||||
### MLEM
|
||||
Additions
|
||||
- Added StringBuilder overloads to GenericFont
|
||||
- Added ColorExtensions.Multiply
|
||||
- Added SoundEffectInstanceHandler.Stop
|
||||
- Added TextureRegion.OffsetCopy
|
||||
- Added RectangleF.DistanceSquared and RectangleF.Distance
|
||||
- Added GamepadExtensions.GetAnalogValue to get the analog value of any gamepad button
|
||||
- Added InputHandler.TryGetDownTime
|
||||
|
||||
Improvements
|
||||
- Generify GenericFont's string drawing
|
||||
- Added InputHandler mouse and touch position querying that preserves the game's viewport
|
||||
- Added float version of GetRandomWeightedEntry
|
||||
- Allow LinkCode to specify a color to draw with
|
||||
- Allow better control over the order and layout of a Keybind's combinations
|
||||
- Allow setting a gamepad button deadzone in InputHandler
|
||||
- Trigger InputHandler key and gamepad repeats for the most recently pressed input
|
||||
- Added properties and constructors for existing operator overloads to GenericInput
|
||||
|
||||
Fixes
|
||||
- **Fixed a formatting Code only knowing about the last Token that it is applied in**
|
||||
- Fixed Code.Draw receiving the index in the current line rather than the current token
|
||||
- Fixed StaticSpriteBatch handling rotated sprites incorrectly
|
||||
- Fixed InputHandler.InputsPressed ignoring repeat events for keyboards and gamepads
|
||||
|
||||
Removals
|
||||
- **Removed InputHandler.StoreAllActiveInputs and always store all active inputs**
|
||||
- Renamed GenericFont.OneEmSpace to Emsp (and marked OneEmSpace as obsolete)
|
||||
|
||||
### MLEM.Ui
|
||||
Additions
|
||||
- Added Element.OnStyleInit event
|
||||
- Added UiControls.AutoNavModeChanged event
|
||||
|
||||
Improvements
|
||||
- Allow for checkboxes and radio buttons to be disabled
|
||||
- Only set a paragraph's area dirty when a text change would cause it to change size
|
||||
- Ensure that a panel gets notified of all relevant changes by calling OnChildAreaDirty for all grandchildren
|
||||
- Avoid unnecessary panel updates by using an Epsilon comparison when scrolling children
|
||||
- Allow setting a default text alignment for paragraphs in UiStyle
|
||||
- Made custom values of Element.Style persist when a new ui style is set
|
||||
- Update elements less aggressively when changing a ui system's style
|
||||
- Automatically update all elements when changing a ui system's viewport
|
||||
- Allow setting a default color for clickable links in UiStyle
|
||||
- Allow ElementHelper's KeybindButton to query a combination at a given index
|
||||
- Allow ElementHelper's KeybindButton to accept a Keybind for clearing a combination
|
||||
- Automatically select the first element when a dropdown is opened in auto nav mode
|
||||
- Improved gamepad navigation by employing angles between elements
|
||||
- Prefer elements that have the same parent as the currently selected element when using gamepad navigation
|
||||
- Allow specifying a custom position for a tooltip to snap to
|
||||
- Allow tooltips to display for elements when selected in auto-nav mode
|
||||
|
||||
Fixes
|
||||
- Fixed paragraph links having incorrect hover locations when using special text alignments
|
||||
- Fixed the graphics device's viewport being ignored for mouse and touch queries
|
||||
- Fixed auto-navigating panels not scrolling to the center of elements properly
|
||||
- Fixed UiControls allowing for non-selectable or non-mouseable elements to be marked as selected or moused
|
||||
- Fixed buttons and checkboxes changing their CanBeSelected and CanBePressed values when being disabled
|
||||
- Fixed children of Panel scroll bars also being scrolled
|
||||
- Fixed RootElement.CanSelectContent and Element.IsSelected returning incorrect results when CanBeSelected changes
|
||||
- Fixed dropdowns with some non-selectable children failing to navigate when using gamepad controls
|
||||
- Fixed UiMetrics.ForceAreaUpdateTime being inaccurate for nested elements
|
||||
- Fixed tooltips sometimes ignoring manually set IsHidden values
|
||||
- Fixed delayed tooltips sometimes displaying in the wrong location for one frame
|
||||
|
||||
Removals
|
||||
- Marked StyleProp equality members as obsolete
|
||||
- Marked Element.BeginDelegate and Element.BeginImpl as obsolete
|
||||
- Marked Element.DrawEarly and UiSystem.DrawEarly as obsolete
|
||||
|
||||
### MLEM.Extended
|
||||
Improvements
|
||||
- Preserve texture region names when converting between MLEM and MG.Extended
|
||||
|
||||
### MLEM.Data
|
||||
Improvements
|
||||
- Rethrow exceptions when no RawContentManager readers could be constructed
|
||||
- Make Newtonsoft.Json dependency optional
|
||||
|
||||
Removals
|
||||
- Marked CopyExtensions as obsolete
|
||||
|
||||
## 5.2.0
|
||||
### MLEM
|
||||
Additions
|
||||
|
|
|
@ -8,6 +8,7 @@ using Microsoft.Xna.Framework;
|
|||
using Microsoft.Xna.Framework.Input;
|
||||
using MLEM.Extensions;
|
||||
using MLEM.Misc;
|
||||
using static Android.Views.SystemUiFlags;
|
||||
|
||||
namespace Demos.Android {
|
||||
[Activity(
|
||||
|
@ -46,7 +47,8 @@ namespace Demos.Android {
|
|||
public override void OnWindowFocusChanged(bool hasFocus) {
|
||||
base.OnWindowFocusChanged(hasFocus);
|
||||
// hide the status bar
|
||||
this.view.SystemUiVisibility = (StatusBarVisibility) (SystemUiFlags.LayoutStable | SystemUiFlags.LayoutHideNavigation | SystemUiFlags.LayoutFullscreen | SystemUiFlags.HideNavigation | SystemUiFlags.Fullscreen | SystemUiFlags.ImmersiveSticky);
|
||||
if (hasFocus)
|
||||
this.Window.DecorView.SystemUiVisibility = (StatusBarVisibility) (ImmersiveSticky | LayoutStable | LayoutHideNavigation | LayoutFullscreen | HideNavigation | Fullscreen);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -45,6 +45,14 @@
|
|||
<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>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="System" />
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="de.ellpeck.mlem.demos.android" android:versionCode="1" android:versionName="1.0">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" 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" />
|
||||
<application android:label="MLEM Android Demos" android:resizeableActivity="true" />
|
||||
</manifest>
|
|
@ -13,16 +13,3 @@ using System.Runtime.InteropServices;
|
|||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
[assembly: ComVisible(false)]
|
||||
|
||||
// Version information for an assembly consists of the following four values:
|
||||
//
|
||||
// Major Version
|
||||
// Minor Version
|
||||
// Build Number
|
||||
// Revision
|
||||
//
|
||||
// You can specify all the values or you can default the Build and Revision Numbers
|
||||
// by using the '*' as shown below:
|
||||
// [assembly: AssemblyVersion("1.0.*")]
|
||||
[assembly: AssemblyVersion("1.0.0.0")]
|
||||
[assembly: AssemblyFileVersion("1.0.0.0")]
|
|
@ -16,8 +16,7 @@ namespace Demos {
|
|||
private Group buttons;
|
||||
private bool stop;
|
||||
|
||||
public AnimationDemo(MlemGame game) : base(game) {
|
||||
}
|
||||
public AnimationDemo(MlemGame game) : base(game) {}
|
||||
|
||||
public override void LoadContent() {
|
||||
base.LoadContent();
|
||||
|
|
|
@ -15,8 +15,7 @@ namespace Demos {
|
|||
private Texture2D texture;
|
||||
private string[] layout;
|
||||
|
||||
public AutoTilingDemo(MlemGame game) : base(game) {
|
||||
}
|
||||
public AutoTilingDemo(MlemGame game) : base(game) {}
|
||||
|
||||
public override void LoadContent() {
|
||||
base.LoadContent();
|
||||
|
|
|
@ -19,17 +19,13 @@ namespace Demos {
|
|||
this.Game = game;
|
||||
}
|
||||
|
||||
public virtual void LoadContent() {
|
||||
}
|
||||
public virtual void LoadContent() {}
|
||||
|
||||
public virtual void Update(GameTime time) {
|
||||
}
|
||||
public virtual void Update(GameTime time) {}
|
||||
|
||||
public virtual void DoDraw(GameTime time) {
|
||||
}
|
||||
public virtual void DoDraw(GameTime time) {}
|
||||
|
||||
public virtual void Clear() {
|
||||
}
|
||||
public virtual void Clear() {}
|
||||
|
||||
public static T LoadContent<T>(string name) {
|
||||
return MlemGame.LoadContent<T>(name);
|
||||
|
|
|
@ -19,8 +19,7 @@ namespace Demos {
|
|||
private int current;
|
||||
private float progress;
|
||||
|
||||
public EasingsDemo(MlemGame game) : base(game) {
|
||||
}
|
||||
public EasingsDemo(MlemGame game) : base(game) {}
|
||||
|
||||
public override void LoadContent() {
|
||||
base.LoadContent();
|
||||
|
|
|
@ -9,6 +9,7 @@ using MLEM.Textures;
|
|||
using MLEM.Ui;
|
||||
using MLEM.Ui.Elements;
|
||||
using MLEM.Ui.Style;
|
||||
using MonoGame.Framework.Utilities;
|
||||
|
||||
namespace Demos {
|
||||
public class GameImpl : MlemGame {
|
||||
|
@ -45,9 +46,12 @@ 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;
|
||||
|
@ -68,7 +72,7 @@ namespace Demos {
|
|||
IsHidden = true
|
||||
});
|
||||
|
||||
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' <c CornflowerBlue><l https://github.com/Ellpeck/MLEM/tree/main/Demos>source code</l></c> for more in-depth explanations of their functionality or the <c CornflowerBlue><l https://mlem.ellpeck.de/>website</l></c> for tutorials and API documentation."));
|
||||
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) {
|
||||
selection.AddChild(new Button(Anchor.AutoCenter, new Vector2(1, 10), demo.Key, demo.Value.Item1) {
|
||||
|
@ -95,7 +99,8 @@ namespace Demos {
|
|||
PanelTexture = new NinePatch(new TextureRegion(tex, 0, 8, 24, 24), 8),
|
||||
ButtonTexture = new NinePatch(new TextureRegion(tex, 24, 8, 16, 16), 4),
|
||||
ScrollBarBackground = new NinePatch(new TextureRegion(tex, 12, 0, 4, 8), 1, 1, 2, 2),
|
||||
ScrollBarScrollerTexture = new NinePatch(new TextureRegion(tex, 8, 0, 4, 8), 1, 1, 2, 2)
|
||||
ScrollBarScrollerTexture = new NinePatch(new TextureRegion(tex, 8, 0, 4, 8), 1, 1, 2, 2),
|
||||
LinkColor = Color.CornflowerBlue
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -18,8 +18,7 @@ namespace Demos {
|
|||
private List<Point> path;
|
||||
private Button regenerateButton;
|
||||
|
||||
public PathfindingDemo(MlemGame game) : base(game) {
|
||||
}
|
||||
public PathfindingDemo(MlemGame game) : base(game) {}
|
||||
|
||||
private async void Init() {
|
||||
this.path = null;
|
||||
|
|
|
@ -23,8 +23,7 @@ namespace Demos {
|
|||
private NinePatch testPatch;
|
||||
private Panel root;
|
||||
|
||||
public UiDemo(MlemGame game) : base(game) {
|
||||
}
|
||||
public UiDemo(MlemGame game) : base(game) {}
|
||||
|
||||
public override void LoadContent() {
|
||||
this.testTexture = LoadContent<Texture2D>("Textures/Test");
|
||||
|
@ -48,7 +47,8 @@ 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(LoadContent<SpriteFont>("Fonts/MonospacedFont"))}},
|
||||
LinkColor = Color.CornflowerBlue
|
||||
};
|
||||
var untexturedStyle = new UntexturedStyle(this.SpriteBatch) {
|
||||
TextScale = style.TextScale,
|
||||
|
@ -82,7 +82,7 @@ namespace Demos {
|
|||
this.root.AddChild(new Button(Anchor.AutoCenter, new Vector2(1, 10), "Change Style") {
|
||||
OnPressed = element => this.UiSystem.Style = this.UiSystem.Style == untexturedStyle ? style : untexturedStyle,
|
||||
PositionOffset = new Vector2(0, 1),
|
||||
Texture = this.testPatch
|
||||
Style = untexturedStyle
|
||||
});
|
||||
|
||||
this.root.AddChild(new VerticalSpace(3));
|
||||
|
@ -207,8 +207,15 @@ namespace Demos {
|
|||
|
||||
this.root.AddChild(new VerticalSpace(3));
|
||||
this.root.AddChild(new Button(Anchor.AutoLeft, new Vector2(1, 10), "Disabled button", "This button can't be clicked or moved to using automatic navigation") {IsDisabled = true}).PositionOffset = new Vector2(0, 1);
|
||||
this.root.AddChild(new Checkbox(Anchor.AutoLeft, new Vector2(1, 10), "Disabled checkbox") {IsDisabled = true}).PositionOffset = new Vector2(0, 1);
|
||||
this.root.AddChild(new Button(Anchor.AutoLeft, new Vector2(1, 10), "Disabled tooltip button", "This button can't be clicked, but can be moved to using automatic navigation, and will display its tooltip even when done so.") {
|
||||
CanSelectDisabled = true,
|
||||
IsDisabled = true,
|
||||
Tooltip = {DisplayInAutoNavMode = true},
|
||||
PositionOffset = new Vector2(0, 1)
|
||||
});
|
||||
|
||||
const string alignText = "Paragraphs can have <c CornflowerBlue><l Left>left</l></c> aligned text, <c CornflowerBlue><l Right>right</l></c> aligned text and <c CornflowerBlue><l Center>center</l></c> aligned text.";
|
||||
const string alignText = "Paragraphs can have <l Left>left</l> aligned text, <l Right>right</l> aligned text and <l Center>center</l> aligned text.";
|
||||
this.root.AddChild(new VerticalSpace(3));
|
||||
var alignPar = this.root.AddChild(new Paragraph(Anchor.AutoLeft, 1, alignText));
|
||||
alignPar.LinkAction = (l, c) => {
|
||||
|
|
|
@ -22,9 +22,6 @@ protected override void Update(GameTime gameTime) {
|
|||
}
|
||||
|
||||
protected override void Draw(GameTime gameTime) {
|
||||
// DrawEarly needs to be called before clearing your graphics context
|
||||
this.UiSystem.DrawEarly(gameTime, this.SpriteBatch);
|
||||
|
||||
this.GraphicsDevice.Clear(Color.CornflowerBlue);
|
||||
// Do your regular game drawing here
|
||||
|
||||
|
|
|
@ -1,18 +1,13 @@
|
|||
{
|
||||
"metadata": [
|
||||
{
|
||||
"src": [
|
||||
{
|
||||
"metadata": [{
|
||||
"src": [{
|
||||
"src": "../",
|
||||
"files": ["**/MLEM**.csproj"]
|
||||
}
|
||||
],
|
||||
}],
|
||||
"dest": "api"
|
||||
}
|
||||
],
|
||||
}],
|
||||
"build": {
|
||||
"content": [
|
||||
{
|
||||
"content": [{
|
||||
"files": [
|
||||
"articles/**.md",
|
||||
"articles/**/toc.yml",
|
||||
|
@ -28,8 +23,7 @@
|
|||
"src": ".."
|
||||
}
|
||||
],
|
||||
"resource": [
|
||||
{
|
||||
"resource": [{
|
||||
"files": [
|
||||
"favicon.ico"
|
||||
]
|
||||
|
@ -42,7 +36,7 @@
|
|||
"globalMetadata": {
|
||||
"_appTitle": "MLEM Documentation",
|
||||
"_appLogoPath": "Logo.svg",
|
||||
"_appFooter": "<a href=\"https://github.com/Ellpeck/MLEM\">© 2019-2021 Ellpeck</a> – <a href=\"https://ellpeck.de/impressum\">Impressum</a> – <a href=\"https://ellpeck.de/privacy\">Privacy</a>",
|
||||
"_appFooter": "<a href=\"https://github.com/Ellpeck/MLEM\">© 2019-2021 Ellpeck</a> – <a href=\"https://ellpeck.de/impressum\">Impressum</a> – <a href=\"https://ellpeck.de/privacy\">Privacy</a> – <a href=\"https://status.ellpeck.de\">Status</a>",
|
||||
"_enableSearch": true
|
||||
},
|
||||
"dest": "_site",
|
||||
|
@ -50,7 +44,8 @@
|
|||
"fileMetadataFiles": [],
|
||||
"template": [
|
||||
"default",
|
||||
"template"
|
||||
"templates/darkfx",
|
||||
"templates/custom"
|
||||
],
|
||||
"postProcessors": [],
|
||||
"markdownEngineName": "markdig",
|
||||
|
|
|
@ -14,13 +14,14 @@
|
|||
- **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 data handling
|
||||
- **MLEM.Data** provides simple loading and processing of textures and data
|
||||
- **MLEM.Startup** combines MLEM with some other useful libraries into a quick Game startup class
|
||||
- **MLEM.Templates** contains cross-platform project templates
|
||||
|
||||
# Made with MLEM
|
||||
- [A Breath of Spring Air](https://ellpeck.itch.io/a-breath-of-spring-air), a short platformer ([Source](https://git.ellpeck.de/Ellpeck/GreatSpringGameJam))
|
||||
- [Don't Wake Up](https://ellpeck.itch.io/dont-wake-up), a short puzzle game ([Source](https://github.com/Ellpeck/DontLetGo))
|
||||
- [Pong clone](https://github.com/luanfagu/pong), a very simple pong clone ([Source](https://github.com/luanfagu/pong))
|
||||
- [Tiny Life](https://tinylifegame.com), an isometric life simulation game ([Modding API](https://github.com/Ellpeck/TinyLifeExampleMod))
|
||||
|
||||
If you created a game with the help of MLEM, you can get it added to this list by submitting it on the [issue tracker](https://github.com/Ellpeck/MLEM/issues). If its source is public, other people will be able to use your project as an example, too!
|
||||
|
@ -37,8 +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 NuGet packages 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 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
|
||||
- [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
|
||||
|
|
40
Docs/templates/darkfx/partials/affix.tmpl.partial
Normal file
40
Docs/templates/darkfx/partials/affix.tmpl.partial
Normal file
|
@ -0,0 +1,40 @@
|
|||
{{!Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.}}
|
||||
|
||||
<div class="hidden-sm col-md-2" role="complementary">
|
||||
<div class="sideaffix">
|
||||
{{^_disableContribution}}
|
||||
<div class="contribution">
|
||||
<ul class="nav">
|
||||
{{#docurl}}
|
||||
<li>
|
||||
<a href="{{docurl}}" class="contribution-link">{{__global.improveThisDoc}}</a>
|
||||
</li>
|
||||
{{/docurl}}
|
||||
{{#sourceurl}}
|
||||
<li>
|
||||
<a href="{{sourceurl}}" class="contribution-link">{{__global.viewSource}}</a>
|
||||
</li>
|
||||
{{/sourceurl}}
|
||||
</ul>
|
||||
</div>
|
||||
{{/_disableContribution}}
|
||||
<div class="toggle-mode">
|
||||
<div class="icon">
|
||||
<i aria-hidden="true">☀</i>
|
||||
</div>
|
||||
<label class="switch">
|
||||
<input type="checkbox" id="switch-style">
|
||||
<span class="slider round"></span>
|
||||
</label>
|
||||
<div class="icon">
|
||||
<i aria-hidden="true">☾</i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="bs-docs-sidebar hidden-print hidden-xs hidden-sm affix" id="affix">
|
||||
<h5>{{__global.inThisArticle}}</h5>
|
||||
<div></div>
|
||||
<!-- <p><a class="back-to-top" href="#top">Back to top</a><p> -->
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
29
Docs/templates/darkfx/partials/footer.tmpl.partial
Normal file
29
Docs/templates/darkfx/partials/footer.tmpl.partial
Normal file
|
@ -0,0 +1,29 @@
|
|||
{{!Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.}}
|
||||
|
||||
<footer>
|
||||
<div class="grad-bottom"></div>
|
||||
<div class="footer">
|
||||
<div class="container">
|
||||
<span class="pull-right">
|
||||
<a href="#top">Back to top</a>
|
||||
</span>
|
||||
<div class="pull-left">
|
||||
{{{_appFooter}}}
|
||||
{{^_appFooter}}<span>Generated by <strong>DocFX</strong></span>{{/_appFooter}}
|
||||
</div>
|
||||
<div class="toggle-mode pull-right visible-sm visible-xs">
|
||||
<div class="icon">
|
||||
<i aria-hidden="true">☀</i>
|
||||
</div>
|
||||
<label class="switch">
|
||||
<input type="checkbox" id="switch-style-m">
|
||||
<span class="slider round"></span>
|
||||
</label>
|
||||
<div class="icon">
|
||||
<i aria-hidden="true">☾</i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript" src="{{_rel}}styles/toggle-theme.js"></script>
|
||||
</footer>
|
20
Docs/templates/darkfx/partials/head.tmpl.partial
Normal file
20
Docs/templates/darkfx/partials/head.tmpl.partial
Normal file
|
@ -0,0 +1,20 @@
|
|||
{{!Copyright (c) Oscar Vasquez. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.}}
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<title>{{#title}}{{title}}{{/title}}{{^title}}{{>partials/title}}{{/title}} {{#_appTitle}}| {{_appTitle}} {{/_appTitle}}</title>
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<meta name="title" content="{{#title}}{{title}}{{/title}}{{^title}}{{>partials/title}}{{/title}} {{#_appTitle}}| {{_appTitle}} {{/_appTitle}}">
|
||||
<meta name="generator" content="docfx {{_docfxVersion}}">
|
||||
{{#_description}}<meta name="description" content="{{_description}}">{{/_description}}
|
||||
<link rel="shortcut icon" href="{{_rel}}{{{_appFaviconPath}}}{{^_appFaviconPath}}favicon.ico{{/_appFaviconPath}}">
|
||||
<link rel="stylesheet" href="{{_rel}}styles/docfx.vendor.css">
|
||||
<link rel="stylesheet" href="{{_rel}}styles/docfx.css">
|
||||
<link rel="stylesheet" href="{{_rel}}styles/main.css">
|
||||
<meta property="docfx:navrel" content="{{_navRel}}">
|
||||
<meta property="docfx:tocrel" content="{{_tocRel}}">
|
||||
{{#_noindex}}<meta name="searchOption" content="noindex">{{/_noindex}}
|
||||
{{#_enableSearch}}<meta property="docfx:rel" content="{{_rel}}">{{/_enableSearch}}
|
||||
{{#_enableNewTab}}<meta property="docfx:newtab" content="true">{{/_enableNewTab}}
|
||||
</head>
|
470
Docs/templates/darkfx/styles/main.css
Normal file
470
Docs/templates/darkfx/styles/main.css
Normal file
|
@ -0,0 +1,470 @@
|
|||
:root, body.dark-theme {
|
||||
--color-foreground: #ccd5dc;
|
||||
--color-navbar: #66666d;
|
||||
--color-breadcrumb: #999;
|
||||
--color-underline: #ddd;
|
||||
--color-toc-hover: #fff;
|
||||
--color-background: #2d2d30;
|
||||
--color-background-subnav: #333337;
|
||||
--color-background-dark: #1e1e1e;
|
||||
--color-background-table-alt: #212123;
|
||||
--color-background-quote: #69696e;
|
||||
}
|
||||
|
||||
body.light-theme {
|
||||
--color-foreground: #171717;
|
||||
--color-breadcrumb: #4a4a4a;
|
||||
--color-toc-hover: #4c4c4c;
|
||||
--color-background: #ffffff;
|
||||
--color-background-subnav: #f5f5f5;
|
||||
--color-background-dark: #ddd;
|
||||
--color-background-table-alt: #f9f9f9;
|
||||
}
|
||||
|
||||
body {
|
||||
color: var(--color-foreground);
|
||||
line-height: 1.5;
|
||||
font-size: 14px;
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
word-wrap: break-word;
|
||||
background-color: var(--color-background);
|
||||
}
|
||||
|
||||
.btn.focus, .btn:focus, .btn:hover {
|
||||
color: var(--color-foreground);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-weight: 600;
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-weight: 600;
|
||||
font-size: 24px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-weight: 600;
|
||||
font-size: 20px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 14px;
|
||||
padding: 10px 0px;
|
||||
}
|
||||
|
||||
article h1, article h2, article h3, article h4 {
|
||||
margin-top: 35px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
article h4 {
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 2px solid var(--color-underline);
|
||||
}
|
||||
|
||||
.navbar-brand>img {
|
||||
color: var(--color-background);
|
||||
}
|
||||
|
||||
.navbar {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.subnav {
|
||||
border-top: 1px solid var(--color-underline);
|
||||
background-color: var(--color-background-subnav);
|
||||
}
|
||||
|
||||
.sidenav, .fixed_header, .toc {
|
||||
background-color: var(--color-background);
|
||||
}
|
||||
|
||||
.navbar-inverse {
|
||||
background-color: var(--color-background-dark);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.navbar-inverse .navbar-nav>li>a, .navbar-inverse .navbar-text {
|
||||
color: var(--color-navbar);
|
||||
background-color: var(--color-background-dark);
|
||||
border-bottom: 3px solid transparent;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.navbar-inverse .navbar-nav>li>a:focus, .navbar-inverse .navbar-nav>li>a:hover {
|
||||
color: var(--color-foreground);
|
||||
background-color: var(--color-background-dark);
|
||||
border-bottom: 3px solid var(--color-background-subnav);
|
||||
transition: all ease 0.25s;
|
||||
}
|
||||
|
||||
.navbar-inverse .navbar-nav>.active>a, .navbar-inverse .navbar-nav>.active>a:focus, .navbar-inverse .navbar-nav>.active>a:hover {
|
||||
color: var(--color-foreground);
|
||||
background-color: var(--color-background-dark);
|
||||
border-bottom: 3px solid var(--color-foreground);
|
||||
transition: all ease 0.25s;
|
||||
}
|
||||
|
||||
.navbar-form .form-control {
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.light-theme .navbar-brand svg {
|
||||
filter: brightness(20%);
|
||||
}
|
||||
|
||||
.toc .level1>li {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.toc .nav>li>a {
|
||||
color: var(--color-foreground);
|
||||
}
|
||||
|
||||
.sidefilter {
|
||||
background-color: var(--color-background);
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.sidefilter {
|
||||
background-color: var(--color-background);
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.toc-filter {
|
||||
padding: 10px;
|
||||
margin: 0;
|
||||
background-color: var(--color-background);
|
||||
}
|
||||
|
||||
.toc-filter>input {
|
||||
border: none;
|
||||
border-radius: unset;
|
||||
background-color: var(--color-background-subnav);
|
||||
padding: 5px 0 5px 20px;
|
||||
font-size: 90%
|
||||
}
|
||||
|
||||
.toc-filter>.clear-icon {
|
||||
position: absolute;
|
||||
top: 17px;
|
||||
right: 15px;
|
||||
}
|
||||
|
||||
.toc-filter>input:focus {
|
||||
color: var(--color-foreground);
|
||||
transition: all ease 0.25s;
|
||||
}
|
||||
|
||||
.toc-filter>.filter-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidetoc>.toc {
|
||||
background-color: var(--color-background);
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.sidetoc {
|
||||
background-color: var(--color-background);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.alert {
|
||||
background-color: inherit;
|
||||
border: none;
|
||||
padding: 10px 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.alert>p {
|
||||
margin-bottom: 0;
|
||||
padding: 5px 10px;
|
||||
border-bottom: 1px solid;
|
||||
background-color: var(--color-background-dark);
|
||||
}
|
||||
|
||||
.alert>h5 {
|
||||
padding: 10px 15px;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
border-top: 2px solid;
|
||||
background-color: var(--color-background-dark);
|
||||
border-radius: none;
|
||||
}
|
||||
|
||||
.alert>ul {
|
||||
margin-bottom: 0;
|
||||
padding: 5px 40px;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
color: #f57f17;
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
color: #d32f2f;
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: 9.5px;
|
||||
margin: 0 0 10px;
|
||||
font-size: 13px;
|
||||
word-break: break-all;
|
||||
word-wrap: break-word;
|
||||
background-color: var(--color-background-dark);
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
code {
|
||||
background: var(--color-background-dark) !important;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.hljs {
|
||||
color: var(--color-foreground);
|
||||
}
|
||||
|
||||
.toc .nav>li.active>.expand-stub::before, .toc .nav>li.in>.expand-stub::before, .toc .nav>li.in.active>.expand-stub::before, .toc .nav>li.filtered>.expand-stub::before {
|
||||
content: "▾";
|
||||
}
|
||||
|
||||
.toc .nav>li>.expand-stub::before, .toc .nav>li.active>.expand-stub::before {
|
||||
content: "▸";
|
||||
}
|
||||
|
||||
.affix ul ul>li>a:before {
|
||||
content: "|";
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
background-color: var(--color-background-subnav);
|
||||
}
|
||||
|
||||
.breadcrumb .label.label-primary {
|
||||
background: #444;
|
||||
border-radius: 0;
|
||||
font-weight: normal;
|
||||
font-size: 100%;
|
||||
}
|
||||
|
||||
#breadcrumb .breadcrumb>li a {
|
||||
border-radius: 0;
|
||||
font-weight: normal;
|
||||
font-size: 85%;
|
||||
display: inline;
|
||||
padding: 0 .6em 0;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
vertical-align: baseline;
|
||||
color: var(--color-breadcrumb);
|
||||
}
|
||||
|
||||
#breadcrumb .breadcrumb>li a:hover {
|
||||
color: var(--color-foreground);
|
||||
transition: all ease 0.25s;
|
||||
}
|
||||
|
||||
.breadcrumb>li+li:before {
|
||||
content: "⯈";
|
||||
font-size: 75%;
|
||||
color: var(--color-background-dark);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.light-theme .breadcrumb>li+li:before {
|
||||
color: var(--color-foreground)
|
||||
}
|
||||
|
||||
.toc .level1>li {
|
||||
font-weight: 600;
|
||||
font-size: 130%;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
border-top: none;
|
||||
background-color: var(--color-background-dark);
|
||||
padding: 15px 0;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
.toc .nav>li>a:hover, .toc .nav>li>a:focus {
|
||||
color: var(--color-toc-hover);
|
||||
transition: all ease 0.1s;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
background-color: var(--color-background-subnav);
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
-webkit-box-shadow: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: #66afe9;
|
||||
outline: 0;
|
||||
-webkit-box-shadow: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
input#search-query:focus {
|
||||
color: var(--color-foreground);
|
||||
}
|
||||
|
||||
.table-bordered, .table-bordered>tbody>tr>td, .table-bordered>tbody>tr>th, .table-bordered>tfoot>tr>td, .table-bordered>tfoot>tr>th, .table-bordered>thead>tr>td, .table-bordered>thead>tr>th {
|
||||
border: 1px solid var(--color-background-dark);
|
||||
}
|
||||
|
||||
.table-striped>tbody>tr:nth-of-type(odd) {
|
||||
background-color: var(--color-background-table-alt);
|
||||
}
|
||||
|
||||
blockquote {
|
||||
padding: 10px 20px;
|
||||
margin: 0 0 10px;
|
||||
font-size: 110%;
|
||||
border-left: 5px solid var(--color-background-quote);
|
||||
color: var(--color-background-quote);
|
||||
}
|
||||
|
||||
.pagination>.disabled>a, .pagination>.disabled>a:focus, .pagination>.disabled>a:hover, .pagination>.disabled>span, .pagination>.disabled>span:focus, .pagination>.disabled>span:hover {
|
||||
background-color: var(--color-background-subnav);
|
||||
border-color: var(--color-background-subnav);
|
||||
}
|
||||
|
||||
.breadcrumb>li, .pagination {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.tabGroup a[role="tab"] {
|
||||
border-bottom: 2px solid var(--color-background-dark);
|
||||
}
|
||||
|
||||
.tabGroup a[role="tab"][aria-selected="true"] {
|
||||
color: var(--color-foreground);
|
||||
}
|
||||
|
||||
.tabGroup section[role="tabpanel"] {
|
||||
border: 1px solid var(--color-background-dark);
|
||||
}
|
||||
|
||||
.sideaffix > div.contribution > ul > li > a.contribution-link:hover {
|
||||
background-color: var(--color-background);
|
||||
}
|
||||
|
||||
.switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 40px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #ccc;
|
||||
-webkit-transition: .4s;
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
left: 4px;
|
||||
bottom: 3px;
|
||||
background-color: white;
|
||||
-webkit-transition: .4s;
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
background-color: #337ab7;
|
||||
}
|
||||
|
||||
input:focus + .slider {
|
||||
box-shadow: 0 0 1px #337ab7;
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
-webkit-transform: translateX(19px);
|
||||
-ms-transform: translateX(19px);
|
||||
transform: translateX(19px);
|
||||
}
|
||||
|
||||
/* Rounded sliders */
|
||||
.slider.round {
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.slider.round:before {
|
||||
border-radius: 50%;
|
||||
}
|
||||
.toggle-mode .icon {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.toggle-mode .icon i {
|
||||
font-style: normal;
|
||||
font-size: 17px;
|
||||
display: inline-block;
|
||||
padding-right: 7px;
|
||||
padding-left: 7px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
@media (min-width: 1600px) {
|
||||
.container {
|
||||
width: 100%;
|
||||
}
|
||||
.sidefilter {
|
||||
width: 18%;
|
||||
}
|
||||
.sidetoc {
|
||||
width: 18%;
|
||||
}
|
||||
.article.grid-right {
|
||||
margin-left: 19%;
|
||||
}
|
||||
.sideaffix {
|
||||
width: 11.5%;
|
||||
}
|
||||
.affix ul>li.active>a {
|
||||
white-space: initial;
|
||||
}
|
||||
.affix ul>li>a {
|
||||
width: 99%;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
35
Docs/templates/darkfx/styles/toggle-theme.js
Normal file
35
Docs/templates/darkfx/styles/toggle-theme.js
Normal file
|
@ -0,0 +1,35 @@
|
|||
const sw = document.getElementById("switch-style"), sw_mobile = document.getElementById("switch-style-m"), b = document.body;
|
||||
if (b) {
|
||||
function toggleTheme(target, dark) {
|
||||
target.classList.toggle("dark-theme", dark)
|
||||
target.classList.toggle("light-theme", !dark)
|
||||
}
|
||||
|
||||
function switchEventListener() {
|
||||
toggleTheme(b, this.checked);
|
||||
if (window.localStorage) {
|
||||
this.checked ? localStorage.setItem("theme", "dark-theme") : localStorage.setItem("theme", "light-theme")
|
||||
}
|
||||
}
|
||||
|
||||
var isDarkTheme = !window.localStorage || !window.localStorage.getItem("theme") || window.localStorage && localStorage.getItem("theme") === "dark-theme";
|
||||
|
||||
if(sw && sw_mobile){
|
||||
sw.checked = isDarkTheme;
|
||||
sw_mobile.checked = isDarkTheme;
|
||||
|
||||
sw.addEventListener("change", switchEventListener);
|
||||
sw_mobile.addEventListener("change", switchEventListener);
|
||||
|
||||
// sync state between switches
|
||||
sw.addEventListener("change", function() {
|
||||
sw_mobile.checked = this.checked;
|
||||
});
|
||||
|
||||
sw_mobile.addEventListener("change", function() {
|
||||
sw.checked = this.checked;
|
||||
});
|
||||
}
|
||||
|
||||
toggleTheme(b, isDarkTheme);
|
||||
}
|
|
@ -76,8 +76,7 @@ namespace MLEM.Data.Content {
|
|||
r.Name = assetName;
|
||||
return t;
|
||||
}
|
||||
} catch (FileNotFoundException) {
|
||||
}
|
||||
} catch (FileNotFoundException) {}
|
||||
}
|
||||
}
|
||||
throw new ContentLoadException($"Asset {assetName} not found. Tried files {string.Join(", ", triedFiles)}");
|
||||
|
@ -96,11 +95,11 @@ namespace MLEM.Data.Content {
|
|||
/// <summary>
|
||||
/// Initializes the component. Used to load non-graphical resources.
|
||||
/// </summary>
|
||||
public void Initialize() {
|
||||
}
|
||||
public void Initialize() {}
|
||||
|
||||
private static List<RawContentReader> CollectContentReaders() {
|
||||
var ret = new List<RawContentReader>();
|
||||
var assemblyExceptions = new List<Exception>();
|
||||
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) {
|
||||
try {
|
||||
if (assembly.IsDynamic)
|
||||
|
@ -116,10 +115,12 @@ namespace MLEM.Data.Content {
|
|||
throw new NotSupportedException($"The type {type} cannot be constructed by a RawContentManager. Does it have a visible parameterless constructor?", e);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignored
|
||||
} catch (Exception e) {
|
||||
assemblyExceptions.Add(e);
|
||||
}
|
||||
}
|
||||
if (ret.Count <= 0)
|
||||
throw new AggregateException("Failed to construct any RawContentReader instances", assemblyExceptions);
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
|
|
@ -68,8 +68,7 @@ namespace MLEM.Data {
|
|||
using (var reader = new JsonTextReader(stream))
|
||||
return serializerToUse.Deserialize<T>(reader);
|
||||
}
|
||||
} catch (FileNotFoundException) {
|
||||
}
|
||||
} catch (FileNotFoundException) {}
|
||||
}
|
||||
throw new ContentLoadException($"Asset {name} not found. Tried files {string.Join(", ", triedFiles)}");
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ namespace MLEM.Data {
|
|||
/// <summary>
|
||||
/// A set of extensions for dealing with copying objects.
|
||||
/// </summary>
|
||||
[Obsolete("CopyExtensions has major flaws and insufficient speed compared to other libraries specifically designed for copying objects.")]
|
||||
public static class CopyExtensions {
|
||||
|
||||
private const BindingFlags DefaultFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
|
||||
|
@ -21,6 +22,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>
|
||||
/// <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);
|
||||
obj.CopyInto(copy, flags, fieldInclusion);
|
||||
|
@ -36,6 +38,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>
|
||||
/// <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);
|
||||
obj.DeepCopyInto(copy, flags, fieldInclusion);
|
||||
|
@ -50,6 +53,7 @@ namespace MLEM.Data {
|
|||
/// <param name="flags">The binding flags for field searching</param>
|
||||
/// <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) {
|
||||
foreach (var field in typeof(T).GetFields(flags)) {
|
||||
if (fieldInclusion == null || fieldInclusion(field))
|
||||
|
@ -66,6 +70,7 @@ namespace MLEM.Data {
|
|||
/// <param name="flags">The binding flags for field searching</param>
|
||||
/// <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) {
|
||||
foreach (var field in obj.GetType().GetFields(flags)) {
|
||||
if (fieldInclusion != null && !fieldInclusion(field))
|
||||
|
@ -109,8 +114,6 @@ namespace MLEM.Data {
|
|||
/// <summary>
|
||||
/// An attribute that, when added to a constructor, will make that constructor the one used by <see cref="CopyExtensions.Copy{T}"/>, <see cref="CopyExtensions.DeepCopy{T}"/> and <see cref="CopyExtensions.DeepCopyInto{T}"/>.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Constructor)]
|
||||
public class CopyConstructorAttribute : Attribute {
|
||||
|
||||
}
|
||||
[AttributeUsage(AttributeTargets.Constructor), Obsolete("CopyExtensions has major flaws and insufficient speed compared to other libraries specifically designed for copying objects.")]
|
||||
public class CopyConstructorAttribute : Attribute {}
|
||||
}
|
|
@ -29,9 +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(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>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
<PropertyGroup>
|
||||
<Authors>Ellpeck</Authors>
|
||||
<Description>Simple data handling for MLEM Library for Extending MonoGame</Description>
|
||||
<Description>Simple loading and processing of textures and data for MLEM Library for Extending MonoGame</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>
|
||||
|
@ -19,13 +19,15 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<ProjectReference Include="..\MLEM\MLEM.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>
|
||||
|
|
|
@ -23,7 +23,7 @@ namespace MLEM.Extended.Extensions {
|
|||
/// <param name="region">The nine patch to convert</param>
|
||||
/// <returns>The converted nine patch</returns>
|
||||
public static TextureRegion2D ToExtended(this TextureRegion region) {
|
||||
return new TextureRegion2D(region.Texture, region.U, region.V, region.Width, region.Height);
|
||||
return new TextureRegion2D(region.Name, region.Texture, region.U, region.V, region.Width, region.Height);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -32,7 +32,7 @@ namespace MLEM.Extended.Extensions {
|
|||
/// <param name="patch">The nine patch to convert</param>
|
||||
/// <returns>The converted nine patch</returns>
|
||||
public static NinePatch ToMlem(this NinePatchRegion2D patch) {
|
||||
return new NinePatch(new TextureRegion(patch.Texture, patch.Bounds), patch.LeftPadding, patch.RightPadding, patch.TopPadding, patch.BottomPadding);
|
||||
return new NinePatch(((TextureRegion2D) patch).ToMlem(), patch.LeftPadding, patch.RightPadding, patch.TopPadding, patch.BottomPadding);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -41,7 +41,7 @@ namespace MLEM.Extended.Extensions {
|
|||
/// <param name="region">The nine patch to convert</param>
|
||||
/// <returns>The converted nine patch</returns>
|
||||
public static TextureRegion ToMlem(this TextureRegion2D region) {
|
||||
return new TextureRegion(region.Texture, region.Bounds);
|
||||
return new TextureRegion(region.Texture, region.Bounds) {Name = region.Name};
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
using System.Text;
|
||||
using Microsoft.Xna.Framework;
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
using MLEM.Font;
|
||||
|
@ -39,13 +38,8 @@ namespace MLEM.Extended.Font {
|
|||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void DrawString(SpriteBatch batch, string text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth) {
|
||||
batch.DrawString(this.Font, text, position, color, rotation, origin, scale, effects, layerDepth);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void DrawString(SpriteBatch batch, StringBuilder text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth) {
|
||||
batch.DrawString(this.Font, text, position, color, rotation, origin, scale, effects, layerDepth);
|
||||
protected override void DrawChar(SpriteBatch batch, string cString, Vector2 position, Color color, float rotation, Vector2 scale, SpriteEffects effects, float layerDepth) {
|
||||
batch.DrawString(this.Font, cString, position, color, rotation, Vector2.Zero, scale, effects, layerDepth);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
using System.Text;
|
||||
using FontStashSharp;
|
||||
using Microsoft.Xna.Framework;
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
|
@ -33,20 +32,15 @@ namespace MLEM.Extended.Font {
|
|||
this.Italic = italic != null ? new GenericStashFont(italic) : this;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void DrawString(SpriteBatch batch, string text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth) {
|
||||
this.Font.DrawText(batch, text, position, color, scale, rotation, origin, layerDepth);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void DrawString(SpriteBatch batch, StringBuilder text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth) {
|
||||
this.Font.DrawText(batch, text, position, color, scale, rotation, origin, layerDepth);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override float MeasureChar(char c) {
|
||||
return this.Font.MeasureString(c.ToCachedString()).X;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void DrawChar(SpriteBatch batch, string cString, Vector2 position, Color color, float rotation, Vector2 scale, SpriteEffects effects, float layerDepth) {
|
||||
this.Font.DrawText(batch, cString, position, color, scale, rotation, Vector2.Zero, layerDepth);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -114,7 +114,9 @@ namespace MLEM.Startup {
|
|||
this.PreDraw?.Invoke(this, gameTime);
|
||||
CoroutineHandler.RaiseEvent(CoroutineEvents.PreDraw);
|
||||
|
||||
#pragma warning disable CS0618
|
||||
this.UiSystem.DrawEarly(gameTime, this.SpriteBatch);
|
||||
#pragma warning restore CS0618
|
||||
this.DoDraw(gameTime);
|
||||
this.UiSystem.Draw(gameTime, this.SpriteBatch);
|
||||
|
||||
|
|
|
@ -9,10 +9,10 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Contentless" Version="3.0.6" />
|
||||
<PackageReference Include="MLEM.Startup" Version="5.1.0" />
|
||||
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.0.1641" />
|
||||
<PackageReference Include="MonoGame.Content.Builder.Task" Version="3.8.0.1641" />
|
||||
<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.*" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
@ -5,8 +5,8 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MLEM.Startup" Version="5.1.0" />
|
||||
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.0.1641">
|
||||
<PackageReference Include="MLEM.Startup" Version="5.*" />
|
||||
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.*">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
|
|
@ -51,14 +51,7 @@ namespace MLEM.Ui.Elements {
|
|||
/// Set this property to true to mark the button as disabled.
|
||||
/// A disabled button cannot be moused over, selected or pressed.
|
||||
/// </summary>
|
||||
public bool IsDisabled {
|
||||
get => this.isDisabled;
|
||||
set {
|
||||
this.isDisabled = value;
|
||||
this.CanBePressed = !value;
|
||||
this.CanBeSelected = !value;
|
||||
}
|
||||
}
|
||||
public virtual bool IsDisabled { get; set; }
|
||||
/// <summary>
|
||||
/// Whether this button's <see cref="Text"/> should be truncated if it exceeds this button's width.
|
||||
/// Defaults to false.
|
||||
|
@ -70,8 +63,16 @@ namespace MLEM.Ui.Elements {
|
|||
this.Text.TruncateIfLong = value;
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Whether this button should be able to be selected even if it <see cref="IsDisabled"/>.
|
||||
/// If this is true, <see cref="CanBeSelected"/> will be able to return true even if <see cref="IsDisabled"/> is true.
|
||||
/// </summary>
|
||||
public bool CanSelectDisabled;
|
||||
|
||||
private bool isDisabled;
|
||||
/// <inheritdoc />
|
||||
public override bool CanBeSelected => base.CanBeSelected && (this.CanSelectDisabled || !this.IsDisabled);
|
||||
/// <inheritdoc />
|
||||
public override bool CanBePressed => base.CanBePressed && !this.IsDisabled;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new button with the given settings
|
||||
|
|
|
@ -13,7 +13,7 @@ namespace MLEM.Ui.Elements {
|
|||
public class Checkbox : Element {
|
||||
|
||||
/// <summary>
|
||||
/// The texture that this checkbox uses for drawing
|
||||
/// The texture that this checkbox uses for drawing.
|
||||
/// </summary>
|
||||
public StyleProp<NinePatch> Texture;
|
||||
/// <summary>
|
||||
|
@ -26,6 +26,15 @@ namespace MLEM.Ui.Elements {
|
|||
/// </summary>
|
||||
public StyleProp<Color> HoveredColor;
|
||||
/// <summary>
|
||||
/// The texture that the checkbox uses when it <see cref="IsDisabled"/>.
|
||||
/// If this is null, it uses its default <see cref="Texture"/>.
|
||||
/// </summary>
|
||||
public StyleProp<NinePatch> DisabledTexture;
|
||||
/// <summary>
|
||||
/// The color that the checkbox uses for drawing when it <see cref="IsDisabled"/>.
|
||||
/// </summary>
|
||||
public StyleProp<Color> DisabledColor;
|
||||
/// <summary>
|
||||
/// The texture that is rendered on top of this checkbox when it is <see cref="Checked"/>.
|
||||
/// </summary>
|
||||
public StyleProp<TextureRegion> Checkmark;
|
||||
|
@ -41,20 +50,35 @@ namespace MLEM.Ui.Elements {
|
|||
/// Whether or not this checkbox is currently checked.
|
||||
/// </summary>
|
||||
public bool Checked {
|
||||
get => this.checced;
|
||||
get => this.isChecked;
|
||||
set {
|
||||
if (this.checced != value) {
|
||||
this.checced = value;
|
||||
this.OnCheckStateChange?.Invoke(this, this.checced);
|
||||
if (this.isChecked != value) {
|
||||
this.isChecked = value;
|
||||
this.OnCheckStateChange?.Invoke(this, this.isChecked);
|
||||
}
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Set this property to true to mark the checkbox as disabled.
|
||||
/// A disabled checkbox cannot be moused over, selected or toggled.
|
||||
/// </summary>
|
||||
public virtual bool IsDisabled { get; set; }
|
||||
/// <summary>
|
||||
/// An event that is invoked when this checkbox's <see cref="Checked"/> property changes
|
||||
/// </summary>
|
||||
public CheckStateChange OnCheckStateChange;
|
||||
/// <summary>
|
||||
/// Whether this checkbox should be able to be selected even if it <see cref="IsDisabled"/>.
|
||||
/// If this is true, <see cref="CanBeSelected"/> will be able to return true even if <see cref="IsDisabled"/> is true.
|
||||
/// </summary>
|
||||
public bool CanSelectDisabled;
|
||||
|
||||
private bool checced;
|
||||
/// <inheritdoc />
|
||||
public override bool CanBeSelected => base.CanBeSelected && (this.CanSelectDisabled || !this.IsDisabled);
|
||||
/// <inheritdoc />
|
||||
public override bool CanBePressed => base.CanBePressed && !this.IsDisabled;
|
||||
|
||||
private bool isChecked;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new checkbox with the given settings
|
||||
|
@ -64,7 +88,7 @@ namespace MLEM.Ui.Elements {
|
|||
/// <param name="label">The checkbox's label text</param>
|
||||
/// <param name="defaultChecked">The default value of <see cref="Checked"/></param>
|
||||
public Checkbox(Anchor anchor, Vector2 size, string label, bool defaultChecked = false) : base(anchor, size) {
|
||||
this.checced = defaultChecked;
|
||||
this.isChecked = defaultChecked;
|
||||
this.OnPressed += element => this.Checked = !this.Checked;
|
||||
|
||||
if (label != null) {
|
||||
|
@ -87,7 +111,10 @@ namespace MLEM.Ui.Elements {
|
|||
public override void Draw(GameTime time, SpriteBatch batch, float alpha, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, Effect effect, Matrix matrix) {
|
||||
var tex = this.Texture;
|
||||
var color = Color.White * alpha;
|
||||
if (this.IsMouseOver) {
|
||||
if (this.IsDisabled) {
|
||||
tex = this.DisabledTexture.OrDefault(tex);
|
||||
color = (Color) this.DisabledColor * alpha;
|
||||
} else if (this.IsMouseOver) {
|
||||
tex = this.HoveredTexture.OrDefault(tex);
|
||||
color = (Color) this.HoveredColor * alpha;
|
||||
}
|
||||
|
@ -105,6 +132,8 @@ namespace MLEM.Ui.Elements {
|
|||
this.Texture = this.Texture.OrStyle(style.CheckboxTexture);
|
||||
this.HoveredTexture = this.HoveredTexture.OrStyle(style.CheckboxHoveredTexture);
|
||||
this.HoveredColor = this.HoveredColor.OrStyle(style.CheckboxHoveredColor);
|
||||
this.DisabledTexture = this.DisabledTexture.OrStyle(style.CheckboxDisabledTexture);
|
||||
this.DisabledColor = this.DisabledColor.OrStyle(style.CheckboxDisabledColor);
|
||||
this.Checkmark = this.Checkmark.OrStyle(style.CheckboxCheckmark);
|
||||
this.TextOffsetX = this.TextOffsetX.OrStyle(style.CheckboxTextOffsetX);
|
||||
}
|
||||
|
|
|
@ -43,6 +43,12 @@ namespace MLEM.Ui.Elements {
|
|||
this.OnAreaUpdated += e => this.Panel.PositionOffset = new Vector2(0, e.Area.Height / this.Scale);
|
||||
this.OnOpenedOrClosed += e => this.Priority = this.IsOpen ? 10000 : 0;
|
||||
this.OnPressed += e => this.IsOpen = !this.IsOpen;
|
||||
this.GetGamepadNextElement = (dir, usualNext) => {
|
||||
// Force navigate down to our first child if we're open
|
||||
if (this.IsOpen && dir == Direction2.Down)
|
||||
return this.Panel.GetChildren().FirstOrDefault(c => c.CanBeSelected) ?? usualNext;
|
||||
return usualNext;
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -54,11 +60,13 @@ namespace MLEM.Ui.Elements {
|
|||
// Since the dropdown causes elements to be over each other,
|
||||
// usual gamepad code doesn't apply
|
||||
element.GetGamepadNextElement = (dir, usualNext) => {
|
||||
if (dir == Direction2.Left || dir == Direction2.Right)
|
||||
return null;
|
||||
if (dir == Direction2.Up) {
|
||||
var prev = element.GetOlderSibling();
|
||||
var prev = element.GetOlderSibling(e => e.CanBeSelected);
|
||||
return prev ?? this;
|
||||
} else if (dir == Direction2.Down) {
|
||||
return element.GetSiblings(e => e.GetOlderSibling() == element).FirstOrDefault();
|
||||
return element.GetSiblings(e => e.CanBeSelected && e.GetOlderSibling(s => s.CanBeSelected) == element).FirstOrDefault();
|
||||
}
|
||||
return usualNext;
|
||||
};
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using Microsoft.Xna.Framework;
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
|
@ -32,22 +33,7 @@ namespace MLEM.Ui.Elements {
|
|||
internal set {
|
||||
this.system = value;
|
||||
this.Controls = value?.Controls;
|
||||
this.Style = value?.Style;
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// This Element's current <see cref="UiStyle"/>.
|
||||
/// When this value is changed, <see cref="InitStyle"/> is called.
|
||||
/// Note that this value is automatically set to the <see cref="UiSystem"/>'s ui style when it is changed.
|
||||
/// </summary>
|
||||
public UiStyle Style {
|
||||
get => this.style;
|
||||
set {
|
||||
if (this.style != value) {
|
||||
this.style = value;
|
||||
if (value != null)
|
||||
this.InitStyle(value);
|
||||
}
|
||||
this.Style = this.Style.OrStyle(value?.Style);
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
|
@ -165,7 +151,7 @@ namespace MLEM.Ui.Elements {
|
|||
/// Set this property to <c>true</c> to cause this element to be hidden.
|
||||
/// Hidden elements don't receive input events, aren't rendered and don't factor into auto-anchoring.
|
||||
/// </summary>
|
||||
public bool IsHidden {
|
||||
public virtual bool IsHidden {
|
||||
get => this.isHidden;
|
||||
set {
|
||||
if (this.isHidden == value)
|
||||
|
@ -198,69 +184,84 @@ namespace MLEM.Ui.Elements {
|
|||
/// 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.
|
||||
/// </summary>
|
||||
#pragma warning disable CS0618
|
||||
[Obsolete("BeginImpl is deprecated. You can create a custom element class and override Draw instead.")]
|
||||
public BeginDelegate BeginImpl;
|
||||
#pragma warning restore CS0618
|
||||
/// <summary>
|
||||
/// 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 bool CanBeSelected = true;
|
||||
public virtual bool CanBeSelected { get; set; } = true;
|
||||
/// <summary>
|
||||
/// Set this field to false to disallow the element from reacting to being moused over.
|
||||
/// </summary>
|
||||
public bool CanBeMoused = true;
|
||||
public virtual bool CanBeMoused { get; set; } = true;
|
||||
/// <summary>
|
||||
/// Set this field to false to disallow this element's <see cref="OnPressed"/> and <see cref="OnSecondaryPressed"/> events to be called.
|
||||
/// </summary>
|
||||
public bool CanBePressed = true;
|
||||
public virtual bool CanBePressed { get; set; } = true;
|
||||
/// <summary>
|
||||
/// Set this field to false to cause auto-anchored siblings to ignore this element as a possible anchor point.
|
||||
/// </summary>
|
||||
public bool CanAutoAnchorsAttach = true;
|
||||
public virtual bool CanAutoAnchorsAttach { get; set; } = true;
|
||||
/// <summary>
|
||||
/// Set this field to true to cause this element's width to be automatically calculated based on the area that its <see cref="Children"/> take up.
|
||||
/// To use this element's <see cref="Size"/>'s X coordinate as a minimum or maximum width rather than ignoring it, set <see cref="TreatSizeAsMinimum"/> or <see cref="TreatSizeAsMaximum"/> to true.
|
||||
/// </summary>
|
||||
public bool SetWidthBasedOnChildren;
|
||||
public virtual bool SetWidthBasedOnChildren { get; set; }
|
||||
/// <summary>
|
||||
/// Set this field to true to cause this element's height to be automatically calculated based on the area that its <see cref="Children"/> take up.
|
||||
/// To use this element's <see cref="Size"/>'s Y coordinate as a minimum or maximum height rather than ignoring it, set <see cref="TreatSizeAsMinimum"/> or <see cref="TreatSizeAsMaximum"/> to true.
|
||||
/// </summary>
|
||||
public bool SetHeightBasedOnChildren;
|
||||
public virtual bool SetHeightBasedOnChildren { get; set; }
|
||||
/// <summary>
|
||||
/// If this field is set to true, and <see cref="SetWidthBasedOnChildren"/> or <see cref="SetHeightBasedOnChildren"/> are enabled, the resulting width or height will always be greather than or equal to this element's <see cref="Size"/>.
|
||||
/// For example, if an element's <see cref="Size"/>'s Y coordinate is set to 20, but there is only one child with a height of 10 in it, the element's height would be shrunk to 10 if this value was false, but would remain at 20 if it was true.
|
||||
/// Note that this value only has an effect if <see cref="SetWidthBasedOnChildren"/> or <see cref="SetHeightBasedOnChildren"/> are enabled.
|
||||
/// </summary>
|
||||
public bool TreatSizeAsMinimum;
|
||||
public virtual bool TreatSizeAsMinimum { get; set; }
|
||||
/// <summary>
|
||||
/// If this field is set to true, and <see cref="SetWidthBasedOnChildren"/> or <see cref="SetHeightBasedOnChildren"/>are enabled, the resulting width or height weill always be less than or equal to this element's <see cref="Size"/>.
|
||||
/// Note that this value only has an effect if <see cref="SetWidthBasedOnChildren"/> or <see cref="SetHeightBasedOnChildren"/> are enabled.
|
||||
/// </summary>
|
||||
public bool TreatSizeAsMaximum;
|
||||
public virtual bool TreatSizeAsMaximum { get; set; }
|
||||
/// <summary>
|
||||
/// Set this field to true to cause this element's final display area to never exceed that of its <see cref="Parent"/>.
|
||||
/// If the resulting area is too large, the size of this element is shrunk to fit the target area.
|
||||
/// This can be useful if an element should fill the remaining area of a parent exactly.
|
||||
/// </summary>
|
||||
public bool PreventParentSpill;
|
||||
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"/>.
|
||||
/// </summary>
|
||||
public float DrawAlpha = 1;
|
||||
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; }
|
||||
/// <summary>
|
||||
/// Stores whether this element is its <see cref="Root"/>'s <see cref="RootElement.SelectedElement"/>.
|
||||
/// Returns whether this element is its <see cref="Root"/>'s <see cref="RootElement.SelectedElement"/>.
|
||||
/// </summary>
|
||||
public bool IsSelected { get; protected set; }
|
||||
public bool IsSelected => this.Root.SelectedElement == this;
|
||||
/// <summary>
|
||||
/// Returns whether this element's <see cref="SetAreaDirty"/> method has been recently called and its area has not been updated since then using <see cref="UpdateAreaIfDirty"/> or <see cref="ForceUpdateArea"/>.
|
||||
/// </summary>
|
||||
public bool AreaDirty { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// This Element's current <see cref="UiStyle"/>.
|
||||
/// When this property is set, <see cref="InitStyle"/> is called.
|
||||
/// </summary>
|
||||
public StyleProp<UiStyle> Style {
|
||||
get => this.style;
|
||||
set {
|
||||
this.style = value;
|
||||
if (this.style.HasValue())
|
||||
this.InitStyle(this.style);
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// A style property that contains the selection indicator that is displayed on this element if it is the <see cref="RootElement.SelectedElement"/>
|
||||
/// </summary>
|
||||
|
@ -342,6 +343,10 @@ namespace MLEM.Ui.Elements {
|
|||
/// </summary>
|
||||
public GenericCallback OnAreaUpdated;
|
||||
/// <summary>
|
||||
/// Event that is called when this element's <see cref="InitStyle"/> method is called while setting the <see cref="Style"/>.
|
||||
/// </summary>
|
||||
public GenericCallback OnStyleInit;
|
||||
/// <summary>
|
||||
/// Event that is called when the element that is currently being moused changes within the ui system.
|
||||
/// Note that the event fired doesn't necessarily correlate to this specific element.
|
||||
/// </summary>
|
||||
|
@ -398,8 +403,14 @@ namespace MLEM.Ui.Elements {
|
|||
/// The input handler that this element's <see cref="Controls"/> use
|
||||
/// </summary>
|
||||
protected InputHandler Input => this.Controls.Input;
|
||||
/// <summary>
|
||||
/// The <see cref="ChildPaddedArea"/> of this element's <see cref="Parent"/>, or the <see cref="UiSystem.Viewport"/> if this element has no parent.
|
||||
/// This value is the one that is passed to <see cref="CalcActualSize"/> during <see cref="ForceUpdateArea"/>.
|
||||
/// </summary>
|
||||
protected RectangleF ParentArea => this.Parent?.ChildPaddedArea ?? (RectangleF) this.system.Viewport;
|
||||
|
||||
private readonly List<Element> children = new List<Element>();
|
||||
private readonly Stopwatch stopwatch = new Stopwatch();
|
||||
private bool sortedChildrenDirty;
|
||||
private IList<Element> sortedChildren;
|
||||
private UiSystem system;
|
||||
|
@ -409,7 +420,7 @@ namespace MLEM.Ui.Elements {
|
|||
private RectangleF area;
|
||||
private bool isHidden;
|
||||
private int priority;
|
||||
private UiStyle style;
|
||||
private StyleProp<UiStyle> style;
|
||||
private StyleProp<Padding> childPadding;
|
||||
|
||||
/// <summary>
|
||||
|
@ -427,8 +438,6 @@ namespace MLEM.Ui.Elements {
|
|||
this.OnMouseExit += element => this.IsMouseOver = false;
|
||||
this.OnTouchEnter += element => this.IsMouseOver = true;
|
||||
this.OnTouchExit += element => this.IsMouseOver = false;
|
||||
this.OnSelected += element => this.IsSelected = true;
|
||||
this.OnDeselected += element => this.IsSelected = false;
|
||||
this.GetTabNextElement += (backward, next) => next;
|
||||
this.GetGamepadNextElement += (dir, next) => next;
|
||||
|
||||
|
@ -525,7 +534,7 @@ namespace MLEM.Ui.Elements {
|
|||
/// </summary>
|
||||
public void SetAreaDirty() {
|
||||
this.AreaDirty = true;
|
||||
this.Parent?.OnChildAreaDirty(this);
|
||||
this.Parent?.OnChildAreaDirty(this, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -552,9 +561,9 @@ namespace MLEM.Ui.Elements {
|
|||
// which would cause our ForceUpdateArea code to be run twice, so we only update our parent instead
|
||||
if (this.Parent != null && this.Parent.UpdateAreaIfDirty())
|
||||
return;
|
||||
this.System.Stopwatch.Restart();
|
||||
this.stopwatch.Restart();
|
||||
|
||||
var parentArea = this.Parent != null ? this.Parent.ChildPaddedArea : (RectangleF) this.system.Viewport;
|
||||
var parentArea = this.ParentArea;
|
||||
var parentCenterX = parentArea.X + parentArea.Width / 2;
|
||||
var parentCenterY = parentArea.Y + parentArea.Height / 2;
|
||||
var actualSize = this.CalcActualSize(parentArea);
|
||||
|
@ -562,8 +571,8 @@ namespace MLEM.Ui.Elements {
|
|||
var recursion = 0;
|
||||
UpdateDisplayArea(actualSize);
|
||||
|
||||
this.System.Stopwatch.Stop();
|
||||
this.System.Metrics.ForceAreaUpdateTime += this.System.Stopwatch.Elapsed;
|
||||
this.stopwatch.Stop();
|
||||
this.System.Metrics.ForceAreaUpdateTime += this.stopwatch.Elapsed;
|
||||
this.System.Metrics.ForceAreaUpdates++;
|
||||
|
||||
void UpdateDisplayArea(Vector2 newSize) {
|
||||
|
@ -916,8 +925,10 @@ namespace MLEM.Ui.Elements {
|
|||
/// <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>
|
||||
public void DrawTransformed(GameTime time, SpriteBatch batch, float alpha, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, Effect effect, Matrix matrix) {
|
||||
#pragma warning disable CS0618
|
||||
var customDraw = this.BeginImpl != null || this.Transform != Matrix.Identity;
|
||||
var mat = this.Transform * matrix;
|
||||
// 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();
|
||||
|
@ -928,6 +939,7 @@ namespace MLEM.Ui.Elements {
|
|||
batch.Begin(SpriteSortMode.Deferred, blendState, samplerState, depthStencilState, null, effect, mat);
|
||||
}
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
|
||||
// draw content in custom begin call
|
||||
this.Draw(time, batch, alpha, blendState, samplerState, depthStencilState, effect, mat);
|
||||
|
@ -978,6 +990,7 @@ 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("DrawEarly is deprecated. For custom implementations, see Panel.Draw for how to replace this method.")]
|
||||
public virtual void DrawEarly(GameTime time, SpriteBatch batch, float alpha, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, Effect effect, Matrix matrix) {
|
||||
foreach (var child in this.GetRelevantChildren()) {
|
||||
if (!child.IsHidden)
|
||||
|
@ -1053,17 +1066,23 @@ namespace MLEM.Ui.Elements {
|
|||
this.SelectionIndicator = this.SelectionIndicator.OrStyle(style.SelectionIndicator);
|
||||
this.ActionSound = this.ActionSound.OrStyle(style.ActionSound);
|
||||
this.SecondActionSound = this.SecondActionSound.OrStyle(style.ActionSound);
|
||||
|
||||
this.System?.InvokeOnElementStyleInit(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A method that gets called by this element's <see cref="Children"/> when their <see cref="SetAreaDirty"/> methods get called.
|
||||
/// A method that gets called by this element's <see cref="Children"/> or any of its grandchildren when their <see cref="SetAreaDirty"/> methods get called.
|
||||
/// Note that the element's area might already be dirty, which will not stop this method from being called.
|
||||
/// </summary>
|
||||
/// <param name="child">The child whose area is being set dirty</param>
|
||||
protected virtual void OnChildAreaDirty(Element child) {
|
||||
/// <param name="child">The child whose area is being set dirty.</param>
|
||||
/// <param name="grandchild">Whether the <paramref name="child"/> is a grandchild of this element, rather than a direct child.</param>
|
||||
protected virtual void OnChildAreaDirty(Element child, bool grandchild) {
|
||||
if (!grandchild) {
|
||||
if (child.Anchor >= Anchor.AutoLeft || this.SetWidthBasedOnChildren || this.SetHeightBasedOnChildren)
|
||||
this.SetAreaDirty();
|
||||
}
|
||||
this.Parent?.OnChildAreaDirty(child, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transforms the given <paramref name="position"/> by the inverse of this element's <see cref="Transform"/> matrix.
|
||||
|
@ -1150,6 +1169,7 @@ namespace MLEM.Ui.Elements {
|
|||
/// <param name="effect">The effect used for drawing</param>
|
||||
/// <param name="depthStencilState">The depth stencil state used for drawing</param>
|
||||
/// <param name="matrix">The transform matrix used for drawing</param>
|
||||
[Obsolete("BeginDelegate is deprecated. You can create a custom element class and override Draw instead.")]
|
||||
public delegate void BeginDelegate(Element element, GameTime time, SpriteBatch batch, float alpha, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, Effect effect, Matrix matrix);
|
||||
|
||||
}
|
||||
|
|
|
@ -115,28 +115,31 @@ 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="Button"/> that acts as a way to input a custom value for a <see cref="Keybind"/>.
|
||||
/// Note that only the first <see cref="Keybind.Combination"/> of the given keybind is displayed and edited, all others are ignored. The exception is that, if <paramref name="unbindKey"/> is set, unbinding the keybind clears all combinations.
|
||||
/// Note that only the <see cref="Keybind.Combination"/> at index <paramref name="index"/> of the given keybind is displayed and edited, all others are ignored.
|
||||
/// Inputting custom keybinds using this element supports <see cref="ModifierKey"/>-based modifiers and any <see cref="GenericInput"/>-based keys.
|
||||
/// The keybind button's current state can be retrieved using the "Active" <see cref="GenericDataHolder.GetData{T}"/> key, which stores a bool that indicates whether the button is currently waiting for a new value to be assigned.
|
||||
/// </summary>
|
||||
/// <param name="anchor">The button's anchor</param>
|
||||
/// <param name="size">The button's size</param>
|
||||
/// <param name="keybind">The keybind that this button should represent</param>
|
||||
/// <param name="inputHandler">The input handler to query inputs with</param>
|
||||
/// <param name="activePlaceholder">A placeholder text that is displayed while the keybind is being edited</param>
|
||||
/// <param name="unbindKey">An optional generic input that allows the keybind value to be unbound, clearing all combinations</param>
|
||||
/// <param name="unbind">An optional keybind to press that allows the keybind value to be unbound, clearing the combination</param>
|
||||
/// <param name="unboundPlaceholder">A placeholder text that is displayed if the keybind is unbound</param>
|
||||
/// <param name="inputName">An optional function to give each input a display name that is easier to read. If this is null, <see cref="GenericInput.ToString"/> is used.</param>
|
||||
/// <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>
|
||||
/// <returns>A keybind button with the given settings</returns>
|
||||
public static Button KeybindButton(Anchor anchor, Vector2 size, Keybind keybind, InputHandler inputHandler, string activePlaceholder, GenericInput unbindKey = default, string unboundPlaceholder = "", Func<GenericInput, string> inputName = null) {
|
||||
string GetCurrentName() {
|
||||
var combination = keybind.GetCombinations().FirstOrDefault();
|
||||
return 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) {
|
||||
string GetCurrentName() => keybind.TryGetCombination(index, out var combination) ? combination.ToString(" + ", inputName) : unboundPlaceholder;
|
||||
|
||||
var button = new Button(anchor, size, GetCurrentName());
|
||||
var active = false;
|
||||
var activeNext = false;
|
||||
button.OnPressed = e => {
|
||||
button.Text.Text = activePlaceholder;
|
||||
|
@ -144,22 +147,24 @@ namespace MLEM.Ui.Elements {
|
|||
};
|
||||
button.OnUpdated = (e, time) => {
|
||||
if (activeNext) {
|
||||
active = true;
|
||||
button.SetData("Active", true);
|
||||
activeNext = false;
|
||||
} else if (active) {
|
||||
if (unbindKey != default && inputHandler.IsPressed(unbindKey)) {
|
||||
keybind.Clear();
|
||||
} else if (button.GetData<bool>("Active")) {
|
||||
if (unbind != null && unbind.IsPressed(inputHandler)) {
|
||||
keybind.Remove((c, i) => i == index);
|
||||
button.Text.Text = unboundPlaceholder;
|
||||
active = false;
|
||||
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 == 0).Add(key, mods.ToArray());
|
||||
keybind.Remove((c, i) => i == index).Insert(index, key, mods.ToArray());
|
||||
button.Text.Text = GetCurrentName();
|
||||
active = false;
|
||||
button.SetData("Active", false);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
button.Text.Text = GetCurrentName();
|
||||
}
|
||||
};
|
||||
return button;
|
||||
|
|
|
@ -13,7 +13,6 @@ namespace MLEM.Ui.Elements {
|
|||
/// A panel element to be used inside of a <see cref="UiSystem"/>.
|
||||
/// The panel is a complex element that displays a box as a background to all of its child elements.
|
||||
/// Additionally, a panel can be set to <see cref="scrollOverflow"/> on construction, which causes all elements that don't fit into the panel to be hidden until scrolled to using a <see cref="ScrollBar"/>.
|
||||
/// As this behavior is accomplished using a <see cref="RenderTarget2D"/>, scrolling panels need to have their <see cref="DrawEarly"/> methods called using <see cref="UiSystem.DrawEarly"/>.
|
||||
/// </summary>
|
||||
public class Panel : Element {
|
||||
|
||||
|
@ -79,13 +78,13 @@ namespace MLEM.Ui.Elements {
|
|||
};
|
||||
|
||||
// handle automatic element selection, the scroller needs to scroll to the right location
|
||||
this.OnSelectedElementChanged += (element, otherElement) => {
|
||||
this.OnSelectedElementChanged += (_, e) => {
|
||||
if (!this.Controls.IsAutoNavMode)
|
||||
return;
|
||||
if (otherElement == null || !otherElement.GetParentTree().Contains(this))
|
||||
if (e == null || !e.GetParentTree().Contains(this))
|
||||
return;
|
||||
var firstChild = this.Children.First(c => c != this.ScrollBar);
|
||||
this.ScrollBar.CurrentValue = (otherElement.Area.Bottom - firstChild.Area.Top - this.Area.Height / 2) / this.Scale;
|
||||
this.ScrollBar.CurrentValue = (e.Area.Center.Y - this.Area.Height / 2 - firstChild.Area.Top) / e.Scale + this.ChildPadding.Value.Height / 2;
|
||||
};
|
||||
this.AddChild(this.ScrollBar);
|
||||
}
|
||||
|
@ -119,8 +118,9 @@ namespace MLEM.Ui.Elements {
|
|||
if (!this.scrollOverflow)
|
||||
return;
|
||||
var offset = new Vector2(0, -this.ScrollBar.CurrentValue);
|
||||
foreach (var child in this.GetChildren(c => c != this.ScrollBar, true)) {
|
||||
if (child.ScrollOffset != offset) {
|
||||
// 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)) {
|
||||
child.ScrollOffset = offset;
|
||||
this.relevantChildrenDirty = true;
|
||||
}
|
||||
|
@ -158,23 +158,23 @@ 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) {
|
||||
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);
|
||||
} 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);
|
||||
}
|
||||
protected override void OnChildAreaDirty(Element child, bool grandchild) {
|
||||
base.OnChildAreaDirty(child, grandchild);
|
||||
// we only need to scroll when a grandchild changes, since all of our children are forced
|
||||
// to be auto-anchored and so will automatically propagate their changes up to us
|
||||
if (grandchild)
|
||||
this.ScrollChildren();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void DrawEarly(GameTime time, SpriteBatch batch, float alpha, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, Effect effect, Matrix matrix) {
|
||||
this.UpdateAreaIfDirty();
|
||||
public override void Draw(GameTime time, SpriteBatch batch, float alpha, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, Effect effect, Matrix matrix) {
|
||||
// draw children onto the render target if we have one
|
||||
if (this.scrollOverflow && this.renderTarget != null) {
|
||||
// draw children onto the render target
|
||||
this.UpdateAreaIfDirty();
|
||||
batch.End();
|
||||
// force render target usage to preserve so that previous content isn't cleared
|
||||
var lastUsage = batch.GraphicsDevice.PresentationParameters.RenderTargetUsage;
|
||||
batch.GraphicsDevice.PresentationParameters.RenderTargetUsage = RenderTargetUsage.PreserveContents;
|
||||
using (batch.GraphicsDevice.WithRenderTarget(this.renderTarget)) {
|
||||
batch.GraphicsDevice.Clear(Color.Transparent);
|
||||
// offset children by the render target's location
|
||||
|
@ -185,8 +185,19 @@ namespace MLEM.Ui.Elements {
|
|||
base.Draw(time, batch, alpha, blendState, samplerState, depthStencilState, effect, trans);
|
||||
batch.End();
|
||||
}
|
||||
batch.GraphicsDevice.PresentationParameters.RenderTargetUsage = lastUsage;
|
||||
batch.Begin(SpriteSortMode.Deferred, blendState, samplerState, depthStencilState, null, effect, matrix);
|
||||
}
|
||||
|
||||
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);
|
||||
} 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);
|
||||
}
|
||||
base.DrawEarly(time, batch, alpha, blendState, samplerState, depthStencilState, effect, matrix);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
|
@ -2,6 +2,7 @@ using System;
|
|||
using System.Linq;
|
||||
using Microsoft.Xna.Framework;
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
using MLEM.Extensions;
|
||||
using MLEM.Font;
|
||||
using MLEM.Formatting;
|
||||
using MLEM.Formatting.Codes;
|
||||
|
@ -24,8 +25,7 @@ namespace MLEM.Ui.Elements {
|
|||
get => this.regularFont;
|
||||
set {
|
||||
this.regularFont = value;
|
||||
this.SetAreaDirty();
|
||||
this.TokenizedText = null;
|
||||
this.SetTextDirty();
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
|
@ -59,8 +59,7 @@ namespace MLEM.Ui.Elements {
|
|||
if (this.text != value) {
|
||||
this.text = value;
|
||||
this.IsHidden = string.IsNullOrWhiteSpace(this.text);
|
||||
this.SetAreaDirty();
|
||||
this.TokenizedText = null;
|
||||
this.SetTextDirty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -91,17 +90,16 @@ namespace MLEM.Ui.Elements {
|
|||
/// <summary>
|
||||
/// The <see cref="TextAlignment"/> that this paragraph's text should be rendered with
|
||||
/// </summary>
|
||||
public TextAlignment Alignment {
|
||||
public StyleProp<TextAlignment> Alignment {
|
||||
get => this.alignment;
|
||||
set {
|
||||
this.alignment = value;
|
||||
this.SetAreaDirty();
|
||||
this.TokenizedText = null;
|
||||
this.SetTextDirty();
|
||||
}
|
||||
}
|
||||
|
||||
private string text;
|
||||
private TextAlignment alignment;
|
||||
private StyleProp<TextAlignment> alignment;
|
||||
private StyleProp<GenericFont> regularFont;
|
||||
|
||||
/// <summary>
|
||||
|
@ -160,6 +158,7 @@ namespace MLEM.Ui.Elements {
|
|||
this.RegularFont = this.RegularFont.OrStyle(style.Font ?? throw new NotSupportedException("Paragraphs cannot use ui styles that don't have a font. Please supply a custom font by setting UiStyle.Font."));
|
||||
this.TextScale = this.TextScale.OrStyle(style.TextScale);
|
||||
this.TextColor = this.TextColor.OrStyle(style.TextColor);
|
||||
this.Alignment = this.Alignment.OrStyle(style.TextAlignment);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -187,13 +186,24 @@ namespace MLEM.Ui.Elements {
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A helper method that causes the <see cref="TokenizedText"/> to be reset.
|
||||
/// Additionally, <see cref="Element.SetAreaDirty"/> if this paragraph's area has changed enough to warrant it, or if it has any <see cref="Link"/> children.
|
||||
/// </summary>
|
||||
protected void SetTextDirty() {
|
||||
this.TokenizedText = null;
|
||||
// only set our area dirty if our size changed as a result of this action
|
||||
if (!this.AreaDirty && !this.CalcActualSize(this.ParentArea).Equals(this.DisplayArea.Size, Epsilon))
|
||||
this.SetAreaDirty();
|
||||
}
|
||||
|
||||
private void QueryTextCallback() {
|
||||
if (this.GetTextCallback != null)
|
||||
this.Text = this.GetTextCallback(this);
|
||||
}
|
||||
|
||||
private float GetAlignmentOffset() {
|
||||
switch (this.Alignment) {
|
||||
switch (this.Alignment.Value) {
|
||||
case TextAlignment.Center:
|
||||
return this.DisplayArea.Width / 2;
|
||||
case TextAlignment.Right:
|
||||
|
@ -257,7 +267,10 @@ namespace MLEM.Ui.Elements {
|
|||
if (ret != null)
|
||||
return ret;
|
||||
// check if any of our token's parts are hovered
|
||||
foreach (var rect in this.Token.GetArea(this.Parent.DisplayArea.Location, this.Scale * this.textScale)) {
|
||||
var location = this.Parent.DisplayArea.Location;
|
||||
if (this.Parent is Paragraph p)
|
||||
location.X += p.GetAlignmentOffset();
|
||||
foreach (var rect in this.Token.GetArea(location, this.Scale * this.textScale)) {
|
||||
if (rect.Contains(this.TransformInverse(position)))
|
||||
return this;
|
||||
}
|
||||
|
|
|
@ -137,14 +137,14 @@ namespace MLEM.Ui.Elements {
|
|||
|
||||
// MOUSE INPUT
|
||||
var moused = this.Controls.MousedElement;
|
||||
if (moused == this && this.Controls.Input.IsMouseButtonPressed(MouseButton.Left)) {
|
||||
if (moused == this && this.Input.IsMouseButtonPressed(MouseButton.Left)) {
|
||||
this.isMouseHeld = true;
|
||||
this.scrollStartOffset = this.TransformInverseAll(this.Input.MousePosition.ToVector2()) - this.ScrollerPosition;
|
||||
} else if (this.isMouseHeld && !this.Controls.Input.IsMouseButtonDown(MouseButton.Left)) {
|
||||
this.scrollStartOffset = this.TransformInverseAll(this.Input.ViewportMousePosition.ToVector2()) - this.ScrollerPosition;
|
||||
} else if (this.isMouseHeld && !this.Input.IsMouseButtonDown(MouseButton.Left)) {
|
||||
this.isMouseHeld = false;
|
||||
}
|
||||
if (this.isMouseHeld)
|
||||
this.ScrollToPos(this.TransformInverseAll(this.Input.MousePosition.ToVector2()));
|
||||
this.ScrollToPos(this.TransformInverseAll(this.Input.ViewportMousePosition.ToVector2()));
|
||||
if (!this.Horizontal && moused != null && (moused == this.Parent || moused.GetParentTree().Contains(this.Parent))) {
|
||||
var scroll = this.Input.LastScrollWheel - this.Input.ScrollWheel;
|
||||
if (scroll != 0)
|
||||
|
@ -154,7 +154,7 @@ namespace MLEM.Ui.Elements {
|
|||
// TOUCH INPUT
|
||||
if (!this.Horizontal) {
|
||||
// are we dragging on top of the panel?
|
||||
if (this.Input.GetGesture(GestureType.VerticalDrag, out var drag)) {
|
||||
if (this.Input.GetViewportGesture(GestureType.VerticalDrag, out var drag)) {
|
||||
// if the element under the drag's start position is on top of the panel, start dragging
|
||||
var touched = this.Parent.GetElementUnderPos(this.TransformInverseAll(drag.Position));
|
||||
if (touched != null && touched != this)
|
||||
|
@ -167,11 +167,11 @@ namespace MLEM.Ui.Elements {
|
|||
this.isDragging = false;
|
||||
}
|
||||
}
|
||||
if (this.Input.TouchState.Count <= 0) {
|
||||
if (this.Input.ViewportTouchState.Count <= 0) {
|
||||
// if no touch has occured this tick, then reset the variable
|
||||
this.isTouchHeld = false;
|
||||
} else {
|
||||
foreach (var loc in this.Input.TouchState) {
|
||||
foreach (var loc in this.Input.ViewportTouchState) {
|
||||
var pos = this.TransformInverseAll(loc.Position);
|
||||
// if we just started touching and are on top of the scroller, then we should start scrolling
|
||||
if (this.DisplayArea.Contains(pos) && !loc.TryGetPreviousLocation(out _)) {
|
||||
|
|
|
@ -32,9 +32,7 @@ namespace MLEM.Ui.Elements {
|
|||
/// <param name="size">The image's size</param>
|
||||
/// <param name="animation">The sprite group to display</param>
|
||||
/// <param name="scaleToImage">Whether this image element should scale to the texture</param>
|
||||
public SpriteAnimationImage(Anchor anchor, Vector2 size, SpriteAnimation animation, bool scaleToImage = false) :
|
||||
this(anchor, size, new SpriteAnimationGroup().Add(animation, () => true), scaleToImage) {
|
||||
}
|
||||
public SpriteAnimationImage(Anchor anchor, Vector2 size, SpriteAnimation animation, bool scaleToImage = false) : this(anchor, size, new SpriteAnimationGroup().Add(animation, () => true), scaleToImage) {}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Update(GameTime time) {
|
||||
|
|
|
@ -16,8 +16,7 @@ namespace MLEM.Ui.Elements {
|
|||
/// </summary>
|
||||
/// <param name="anchor">The group's anchor.</param>
|
||||
/// <param name="size">The group's size.</param>
|
||||
public SquishingGroup(Anchor anchor, Vector2 size) : base(anchor, size, false) {
|
||||
}
|
||||
public SquishingGroup(Anchor anchor, Vector2 size) : base(anchor, size, false) {}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void SetAreaAndUpdateChildren(RectangleF area) {
|
||||
|
@ -31,8 +30,9 @@ namespace MLEM.Ui.Elements {
|
|||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnChildAreaDirty(Element child) {
|
||||
base.OnChildAreaDirty(child);
|
||||
protected override void OnChildAreaDirty(Element child, bool grandchild) {
|
||||
base.OnChildAreaDirty(child, grandchild);
|
||||
if (!grandchild)
|
||||
this.SetAreaDirty();
|
||||
}
|
||||
|
||||
|
|
|
@ -423,7 +423,7 @@ namespace MLEM.Ui.Elements {
|
|||
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.ToString(), maxWidth, this.TextScale).ToArray();
|
||||
this.splitText = this.Font.Value.SplitStringSeparate(this.text, maxWidth, this.TextScale).ToArray();
|
||||
this.displayedText = string.Join("\n", this.splitText);
|
||||
this.UpdateCaretData();
|
||||
|
||||
|
@ -466,7 +466,7 @@ namespace MLEM.Ui.Elements {
|
|||
}
|
||||
} else {
|
||||
// not multiline, so scroll horizontally based on caret position
|
||||
if (this.Font.Value.MeasureString(this.text.ToString()).X * this.TextScale > maxWidth) {
|
||||
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;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MLEM.Input;
|
||||
using MLEM.Ui.Style;
|
||||
|
||||
namespace MLEM.Ui.Elements {
|
||||
|
@ -15,6 +16,10 @@ namespace MLEM.Ui.Elements {
|
|||
/// </summary>
|
||||
public StyleProp<Vector2> MouseOffset;
|
||||
/// <summary>
|
||||
/// The offset that this tooltip's top center coordinate should have from the bottom center of the element snapped to when <see cref="DisplayInAutoNavMode"/> is true.
|
||||
/// </summary>
|
||||
public StyleProp<Vector2> AutoNavOffset;
|
||||
/// <summary>
|
||||
/// The amount of time that the mouse has to be over an element before it appears
|
||||
/// </summary>
|
||||
public StyleProp<TimeSpan> Delay;
|
||||
|
@ -22,9 +27,24 @@ namespace MLEM.Ui.Elements {
|
|||
/// The paragraph of text that this tooltip displays
|
||||
/// </summary>
|
||||
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.
|
||||
/// If this tooltip is displayed in auto-nav mode, it will display below the selected element with the <see cref="AutoNavOffset"/> applied.
|
||||
/// </summary>
|
||||
public bool DisplayInAutoNavMode;
|
||||
/// <summary>
|
||||
/// The position that this tooltip should be following (or snapped to) instead of the <see cref="InputHandler.ViewportMousePosition"/>.
|
||||
/// If this value is unset, <see cref="InputHandler.ViewportMousePosition"/> will be used as the snap position.
|
||||
/// Note that <see cref="MouseOffset"/> is still applied with this value set.
|
||||
/// </summary>
|
||||
public virtual Vector2? SnapPosition { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool IsHidden => this.autoHidden || base.IsHidden;
|
||||
|
||||
private TimeSpan delayCountdown;
|
||||
private bool autoHidden;
|
||||
private Element snapElement;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new tooltip with the given settings
|
||||
|
@ -59,6 +79,7 @@ namespace MLEM.Ui.Elements {
|
|||
if (this.delayCountdown <= TimeSpan.Zero) {
|
||||
this.IsHidden = false;
|
||||
this.UpdateAutoHidden();
|
||||
this.SnapPositionToMouse();
|
||||
}
|
||||
} else {
|
||||
this.UpdateAutoHidden();
|
||||
|
@ -78,6 +99,7 @@ namespace MLEM.Ui.Elements {
|
|||
base.InitStyle(style);
|
||||
this.Texture = this.Texture.OrStyle(style.TooltipBackground);
|
||||
this.MouseOffset = this.MouseOffset.OrStyle(style.TooltipOffset);
|
||||
this.AutoNavOffset = this.AutoNavOffset.OrStyle(style.TooltipAutoNavOffset);
|
||||
this.Delay = this.Delay.OrStyle(style.TooltipDelay);
|
||||
this.ChildPadding = this.ChildPadding.OrStyle(style.TooltipChildPadding);
|
||||
if (this.Paragraph != null) {
|
||||
|
@ -87,11 +109,20 @@ namespace MLEM.Ui.Elements {
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Causes this tooltip's position to be snapped to the mouse position.
|
||||
/// Causes this tooltip's position to be snapped to the mouse position, or the <see cref="snapElement"/> if <see cref="DisplayInAutoNavMode"/> is true, or the <see cref="SnapPosition"/> if set.
|
||||
/// </summary>
|
||||
public void SnapPositionToMouse() {
|
||||
Vector2 snapPosition;
|
||||
if (this.snapElement != null) {
|
||||
// center our snap position below the snap element
|
||||
snapPosition = new Vector2(this.snapElement.DisplayArea.Center.X, this.snapElement.DisplayArea.Bottom) + this.AutoNavOffset;
|
||||
snapPosition.X -= this.DisplayArea.Width / 2F;
|
||||
} else {
|
||||
snapPosition = (this.SnapPosition ?? this.Input.ViewportMousePosition.ToVector2()) + this.MouseOffset.Value;
|
||||
}
|
||||
|
||||
var viewport = this.System.Viewport;
|
||||
var offset = (this.Input.MousePosition.ToVector2() + this.MouseOffset.Value) / this.Scale;
|
||||
var offset = snapPosition / this.Scale;
|
||||
if (offset.X < viewport.X)
|
||||
offset.X = viewport.X;
|
||||
if (offset.Y < viewport.Y)
|
||||
|
@ -108,8 +139,10 @@ namespace MLEM.Ui.Elements {
|
|||
/// </summary>
|
||||
/// <param name="system">The system to add this tooltip to</param>
|
||||
/// <param name="name">The name that this tooltip should use</param>
|
||||
public void Display(UiSystem system, string name) {
|
||||
system.Add(name, this);
|
||||
/// <returns>Whether this tooltip was successfully added, which is not the case if it is already being displayed currently.</returns>
|
||||
public bool Display(UiSystem system, string name) {
|
||||
if (system.Add(name, this) == null)
|
||||
return false;
|
||||
if (this.Delay <= TimeSpan.Zero) {
|
||||
this.IsHidden = false;
|
||||
this.SnapPositionToMouse();
|
||||
|
@ -118,6 +151,7 @@ namespace MLEM.Ui.Elements {
|
|||
this.delayCountdown = this.Delay;
|
||||
}
|
||||
this.autoHidden = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -134,8 +168,20 @@ namespace MLEM.Ui.Elements {
|
|||
/// </summary>
|
||||
/// <param name="elementToHover">The element that should automatically cause the tooltip to appear and disappear when hovered and not hovered, respectively</param>
|
||||
public void AddToElement(Element elementToHover) {
|
||||
elementToHover.OnMouseEnter += element => this.Display(element.System, element.GetType().Name + "Tooltip");
|
||||
elementToHover.OnMouseExit += element => this.Remove();
|
||||
elementToHover.OnMouseEnter += e => this.Display(e.System, $"{e.GetType().Name}Tooltip");
|
||||
elementToHover.OnMouseExit += e => this.Remove();
|
||||
elementToHover.OnSelected += e => {
|
||||
if (this.DisplayInAutoNavMode) {
|
||||
this.snapElement = e;
|
||||
this.Display(e.System, $"{e.GetType().Name}Tooltip");
|
||||
}
|
||||
};
|
||||
elementToHover.OnDeselected += e => {
|
||||
if (this.DisplayInAutoNavMode) {
|
||||
this.Remove();
|
||||
this.snapElement = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void Init(Element elementToHover) {
|
||||
|
@ -159,10 +205,8 @@ namespace MLEM.Ui.Elements {
|
|||
}
|
||||
}
|
||||
if (this.autoHidden != shouldBeHidden) {
|
||||
// only auto-hide if IsHidden wasn't changed manually
|
||||
if (this.IsHidden == this.autoHidden)
|
||||
this.IsHidden = shouldBeHidden;
|
||||
this.autoHidden = shouldBeHidden;
|
||||
this.SetAreaDirty();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -27,8 +27,7 @@ namespace MLEM.Ui.Style {
|
|||
/// To create a style property with a lower priority, use <see cref="OrStyle(T,byte)"/> on an existing priority, or use <see cref="None"/>.
|
||||
/// </summary>
|
||||
/// <param name="value">The custom style to apply</param>
|
||||
public StyleProp(T value) : this(value, byte.MaxValue) {
|
||||
}
|
||||
public StyleProp(T value) : this(value, byte.MaxValue) {}
|
||||
|
||||
private StyleProp(T value, byte priority) {
|
||||
this.Value = value;
|
||||
|
@ -76,6 +75,7 @@ namespace MLEM.Ui.Style {
|
|||
/// <summary>Indicates whether the current object is equal to another object of the same type.</summary>
|
||||
/// <param name="other">An object to compare with this object.</param>
|
||||
/// <returns>true if the current object is equal to the <paramref name="other">other</paramref> parameter; otherwise, false.</returns>
|
||||
[Obsolete("StyleProp equality is ambiguous as it is not clear whether priority is taken into account. Compare Values instead.")]
|
||||
public bool Equals(StyleProp<T> other) {
|
||||
return EqualityComparer<T>.Default.Equals(this.Value, other.Value);
|
||||
}
|
||||
|
@ -83,15 +83,19 @@ namespace MLEM.Ui.Style {
|
|||
/// <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>
|
||||
[Obsolete("StyleProp equality is ambiguous as it is not clear whether priority is taken into account. Compare Values instead.")]
|
||||
#pragma warning disable CS0809
|
||||
public override bool Equals(object obj) {
|
||||
return obj is StyleProp<T> 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>
|
||||
[Obsolete("StyleProp equality is ambiguous as it is not clear whether priority is taken into account. Compare Values instead.")]
|
||||
public override int GetHashCode() {
|
||||
return EqualityComparer<T>.Default.GetHashCode(this.Value);
|
||||
}
|
||||
#pragma warning restore CS0809
|
||||
|
||||
/// <summary>Returns the fully qualified type name of this instance.</summary>
|
||||
/// <returns>The fully qualified type name.</returns>
|
||||
|
@ -123,6 +127,7 @@ namespace MLEM.Ui.Style {
|
|||
/// <param name="left">The left style property.</param>
|
||||
/// <param name="right">The right style property.</param>
|
||||
/// <returns>Whether the two style properties are equal.</returns>
|
||||
[Obsolete("StyleProp equality is ambiguous as it is not clear whether priority is taken into account. Compare Values instead.")]
|
||||
public static bool operator ==(StyleProp<T> left, StyleProp<T> right) {
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
@ -133,6 +138,7 @@ namespace MLEM.Ui.Style {
|
|||
/// <param name="left">The left style property.</param>
|
||||
/// <param name="right">The right style property.</param>
|
||||
/// <returns>Whether the two style properties are not equal.</returns>
|
||||
[Obsolete("StyleProp equality is ambiguous as it is not clear whether priority is taken into account. Compare Values instead.")]
|
||||
public static bool operator !=(StyleProp<T> left, StyleProp<T> right) {
|
||||
return !left.Equals(right);
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||
using Microsoft.Xna.Framework;
|
||||
using MLEM.Font;
|
||||
using MLEM.Formatting;
|
||||
using MLEM.Formatting.Codes;
|
||||
using MLEM.Misc;
|
||||
using MLEM.Textures;
|
||||
using MLEM.Ui.Elements;
|
||||
|
@ -110,6 +111,14 @@ namespace MLEM.Ui.Style {
|
|||
/// </summary>
|
||||
public Color CheckboxHoveredColor = Color.LightGray;
|
||||
/// <summary>
|
||||
/// The texture that the <see cref="Checkbox"/> element uses when it <see cref="Checkbox.IsDisabled"/>.
|
||||
/// </summary>
|
||||
public NinePatch CheckboxDisabledTexture;
|
||||
/// <summary>
|
||||
/// The color that the <see cref="Checkbox"/> element uses when it <see cref="Checkbox.IsDisabled"/>.
|
||||
/// </summary>
|
||||
public Color CheckboxDisabledColor = Color.Gray;
|
||||
/// <summary>
|
||||
/// The texture that the <see cref="Checkbox"/> element renders on top of its regular texture when it is <see cref="Checkbox.Checked"/>
|
||||
/// </summary>
|
||||
public TextureRegion CheckboxCheckmark;
|
||||
|
@ -142,6 +151,10 @@ namespace MLEM.Ui.Style {
|
|||
/// </summary>
|
||||
public Vector2 TooltipOffset = new Vector2(8, 16);
|
||||
/// <summary>
|
||||
/// The offset of the <see cref="Tooltip"/> element's top center coordinate from the bottom center of the element snapped to when <see cref="Tooltip.DisplayInAutoNavMode"/> is true.
|
||||
/// </summary>
|
||||
public Vector2 TooltipAutoNavOffset = new Vector2(0, 8);
|
||||
/// <summary>
|
||||
/// The color that the text of a <see cref="Tooltip"/> should have
|
||||
/// </summary>
|
||||
public Color TooltipTextColor = Color.White;
|
||||
|
@ -191,11 +204,20 @@ namespace MLEM.Ui.Style {
|
|||
/// </summary>
|
||||
public Color TextColor = Color.White;
|
||||
/// <summary>
|
||||
/// The <see cref="TextAlignment"/> that a <see cref="Paragraph"/> should use by default.
|
||||
/// </summary>
|
||||
public TextAlignment TextAlignment;
|
||||
/// <summary>
|
||||
/// The <see cref="SoundEffectInfo"/> that should be played when an element's <see cref="Element.OnPressed"/> and <see cref="Element.OnSecondaryPressed"/> events are called.
|
||||
/// Note that this sound is only played if the callbacks have any subscribers.
|
||||
/// </summary>
|
||||
public SoundEffectInfo ActionSound;
|
||||
/// <summary>
|
||||
/// The color that a <see cref="Paragraph"/>'s <see cref="Paragraph.Link"/> codes should have.
|
||||
/// This value is passed to <see cref="LinkCode"/>.
|
||||
/// </summary>
|
||||
public Color? LinkColor;
|
||||
/// <summary>
|
||||
/// A set of additional fonts that can be used for the <c><f FontName></c> formatting code
|
||||
/// </summary>
|
||||
public Dictionary<string, GenericFont> AdditionalFonts = new Dictionary<string, GenericFont>();
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Xna.Framework;
|
||||
|
@ -19,36 +20,6 @@ namespace MLEM.Ui {
|
|||
/// The input handler that is used for querying input
|
||||
/// </summary>
|
||||
public readonly InputHandler Input;
|
||||
/// <summary>
|
||||
/// This value ist true if the <see cref="InputHandler"/> was created by this ui controls instance, or if it was passed in.
|
||||
/// If the input handler was created by this instance, its <see cref="InputHandler.Update()"/> method should be called by us.
|
||||
/// </summary>
|
||||
protected readonly bool IsInputOurs;
|
||||
/// <summary>
|
||||
/// The <see cref="UiSystem"/> that this ui controls instance is controlling
|
||||
/// </summary>
|
||||
protected readonly UiSystem System;
|
||||
|
||||
/// <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.
|
||||
/// </summary>
|
||||
public RootElement ActiveRoot { get; protected set; }
|
||||
/// <summary>
|
||||
/// The <see cref="Element"/> that the mouse is currently over.
|
||||
/// </summary>
|
||||
public Element MousedElement { get; protected set; }
|
||||
/// <summary>
|
||||
/// The <see cref="Element"/> that is currently touched.
|
||||
/// </summary>
|
||||
public Element TouchedElement { get; protected set; }
|
||||
private readonly Dictionary<string, Element> selectedElements = new Dictionary<string, Element>();
|
||||
/// <summary>
|
||||
/// The element that is currently selected.
|
||||
/// This is the <see cref="RootElement.SelectedElement"/> of the <see cref="ActiveRoot"/>.
|
||||
/// </summary>
|
||||
public Element SelectedElement => this.GetSelectedElement(this.ActiveRoot);
|
||||
|
||||
/// <summary>
|
||||
/// A list of <see cref="Keys"/>, <see cref="Buttons"/> and/or <see cref="MouseButton"/> that act as the buttons on the keyboard which perform the <see cref="Element.OnPressed"/> action.
|
||||
/// If the <see cref="ModifierKey.Shift"/> is held, these buttons perform <see cref="Element.OnSecondaryPressed"/>.
|
||||
|
@ -83,6 +54,25 @@ namespace MLEM.Ui {
|
|||
/// This can be used to easily serialize and deserialize all ui keybinds.
|
||||
/// </summary>
|
||||
public readonly Keybind[] Keybinds;
|
||||
|
||||
/// <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.
|
||||
/// </summary>
|
||||
public RootElement ActiveRoot { get; protected set; }
|
||||
/// <summary>
|
||||
/// The <see cref="Element"/> that the mouse is currently over.
|
||||
/// </summary>
|
||||
public Element MousedElement { get; protected set; }
|
||||
/// <summary>
|
||||
/// The <see cref="Element"/> that is currently touched.
|
||||
/// </summary>
|
||||
public Element TouchedElement { get; protected set; }
|
||||
/// <summary>
|
||||
/// The element that is currently selected.
|
||||
/// This is the <see cref="RootElement.SelectedElement"/> of the <see cref="ActiveRoot"/>.
|
||||
/// </summary>
|
||||
public Element SelectedElement => this.GetSelectedElement(this.ActiveRoot);
|
||||
/// <summary>
|
||||
/// The zero-based index of the <see cref="GamePad"/> used for gamepad input.
|
||||
/// If this index is lower than 0, every connected gamepad will trigger input.
|
||||
|
@ -111,9 +101,35 @@ namespace MLEM.Ui {
|
|||
/// <summary>
|
||||
/// If this value is true, the ui controls are in automatic navigation mode.
|
||||
/// This means that the <see cref="UiStyle.SelectionIndicator"/> will be drawn around the <see cref="SelectedElement"/>.
|
||||
/// To set this value, use <see cref="SelectElement"/> or <see cref="RootElement.SelectElement"/>
|
||||
/// </summary>
|
||||
public bool IsAutoNavMode { get; internal set; }
|
||||
public bool IsAutoNavMode {
|
||||
get => this.isAutoNavMode;
|
||||
set {
|
||||
if (this.isAutoNavMode != value) {
|
||||
this.isAutoNavMode = value;
|
||||
this.AutoNavModeChanged?.Invoke(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An event that is raised when <see cref="IsAutoNavMode"/> is changed.
|
||||
/// This can be used for custom actions like hiding the mouse cursor when automatic navigation is enabled.
|
||||
/// </summary>
|
||||
public event Action<bool> AutoNavModeChanged;
|
||||
|
||||
/// <summary>
|
||||
/// This value ist true if the <see cref="InputHandler"/> was created by this ui controls instance, or if it was passed in.
|
||||
/// If the input handler was created by this instance, its <see cref="InputHandler.Update()"/> method should be called by us.
|
||||
/// </summary>
|
||||
protected readonly bool IsInputOurs;
|
||||
/// <summary>
|
||||
/// The <see cref="UiSystem"/> that this ui controls instance is controlling
|
||||
/// </summary>
|
||||
protected readonly UiSystem System;
|
||||
|
||||
private readonly Dictionary<string, Element> selectedElements = new Dictionary<string, Element>();
|
||||
private bool isAutoNavMode;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the ui controls.
|
||||
|
@ -139,11 +155,11 @@ namespace MLEM.Ui {
|
|||
public virtual void Update() {
|
||||
if (this.IsInputOurs)
|
||||
this.Input.Update();
|
||||
this.ActiveRoot = this.System.GetRootElements().FirstOrDefault(root => root.CanSelectContent && !root.Element.IsHidden);
|
||||
this.ActiveRoot = this.System.GetRootElements().FirstOrDefault(root => !root.Element.IsHidden && root.CanSelectContent);
|
||||
|
||||
// MOUSE INPUT
|
||||
if (this.HandleMouse) {
|
||||
var mousedNow = this.GetElementUnderPos(this.Input.MousePosition.ToVector2());
|
||||
var mousedNow = this.GetElementUnderPos(this.Input.ViewportMousePosition.ToVector2());
|
||||
this.SetMousedElement(mousedNow);
|
||||
|
||||
if (this.Input.IsMouseButtonPressed(MouseButton.Left)) {
|
||||
|
@ -184,22 +200,22 @@ namespace MLEM.Ui {
|
|||
|
||||
// TOUCH INPUT
|
||||
if (this.HandleTouch) {
|
||||
if (this.Input.GetGesture(GestureType.Tap, out var tap)) {
|
||||
if (this.Input.GetViewportGesture(GestureType.Tap, out var tap)) {
|
||||
this.IsAutoNavMode = false;
|
||||
var tapped = this.GetElementUnderPos(tap.Position);
|
||||
this.SelectElement(this.ActiveRoot, tapped);
|
||||
if (tapped != null && tapped.CanBePressed)
|
||||
this.System.InvokeOnElementPressed(tapped);
|
||||
} else if (this.Input.GetGesture(GestureType.Hold, out var hold)) {
|
||||
} else if (this.Input.GetViewportGesture(GestureType.Hold, out var hold)) {
|
||||
this.IsAutoNavMode = false;
|
||||
var held = this.GetElementUnderPos(hold.Position);
|
||||
this.SelectElement(this.ActiveRoot, held);
|
||||
if (held != null && held.CanBePressed)
|
||||
this.System.InvokeOnElementSecondaryPressed(held);
|
||||
} else if (this.Input.TouchState.Count <= 0) {
|
||||
} else if (this.Input.ViewportTouchState.Count <= 0) {
|
||||
this.SetTouchedElement(null);
|
||||
} else {
|
||||
foreach (var location in this.Input.TouchState) {
|
||||
foreach (var location in this.Input.ViewportTouchState) {
|
||||
var element = this.GetElementUnderPos(location.Position);
|
||||
if (location.State == TouchLocationState.Pressed) {
|
||||
// start touching an element if we just touched down on it
|
||||
|
@ -259,6 +275,8 @@ namespace MLEM.Ui {
|
|||
public void SelectElement(RootElement root, Element element, bool? autoNav = null) {
|
||||
if (root == null)
|
||||
return;
|
||||
if (element != null && !element.CanBeSelected)
|
||||
return;
|
||||
var selected = this.GetSelectedElement(root);
|
||||
if (selected == element)
|
||||
return;
|
||||
|
@ -282,6 +300,8 @@ namespace MLEM.Ui {
|
|||
/// </summary>
|
||||
/// <param name="element">The element to set as moused</param>
|
||||
public void SetMousedElement(Element element) {
|
||||
if (element != null && !element.CanBeMoused)
|
||||
return;
|
||||
if (element != this.MousedElement) {
|
||||
if (this.MousedElement != null)
|
||||
this.System.InvokeOnElementMouseExit(this.MousedElement);
|
||||
|
@ -297,6 +317,8 @@ namespace MLEM.Ui {
|
|||
/// </summary>
|
||||
/// <param name="element">The element to set as touched</param>
|
||||
public void SetTouchedElement(Element element) {
|
||||
if (element != null && !element.CanBeMoused)
|
||||
return;
|
||||
if (element != this.TouchedElement) {
|
||||
if (this.TouchedElement != null)
|
||||
this.System.InvokeOnElementTouchExit(this.TouchedElement);
|
||||
|
@ -317,6 +339,8 @@ 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;
|
||||
}
|
||||
|
||||
|
@ -355,11 +379,11 @@ namespace MLEM.Ui {
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the next element that should be selected during gamepad navigation, based on the <see cref="RectangleF"/> that we're looking for elements in.
|
||||
/// Returns the next element that should be selected during gamepad navigation, based on the <see cref="Direction2"/> that we're looking for elements in.
|
||||
/// </summary>
|
||||
/// <param name="searchArea">The area that we're looking for next elements in</param>
|
||||
/// <param name="direction">The direction that we're looking for next elements in</param>
|
||||
/// <returns>The first element found in that area</returns>
|
||||
protected virtual Element GetGamepadNextElement(RectangleF searchArea) {
|
||||
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);
|
||||
|
@ -367,14 +391,20 @@ namespace MLEM.Ui {
|
|||
return children.FirstOrDefault(c => c.CanBeSelected);
|
||||
} else {
|
||||
Element closest = null;
|
||||
float closestDist = 0;
|
||||
float closestPriority = 0;
|
||||
foreach (var child in children) {
|
||||
if (!child.CanBeSelected || child == this.SelectedElement || !searchArea.Intersects(child.Area))
|
||||
if (!child.CanBeSelected || child == this.SelectedElement)
|
||||
continue;
|
||||
var dist = Vector2.Distance(child.Area.Center, this.SelectedElement.Area.Center);
|
||||
if (closest == null || dist < closestDist) {
|
||||
var (xOffset, yOffset) = child.Area.Center - this.SelectedElement.Area.Center;
|
||||
var angle = Math.Abs(direction.Angle() - (float) Math.Atan2(yOffset, xOffset));
|
||||
if (angle >= MathHelper.PiOver2 - Element.Epsilon)
|
||||
continue;
|
||||
var distSq = child.Area.DistanceSquared(this.SelectedElement.Area);
|
||||
// both distance and angle play a role in a destination button's priority, so we combine them
|
||||
var priority = (distSq + 1) * (angle / MathHelper.PiOver2 + 1);
|
||||
if (closest == null || priority < closestPriority) {
|
||||
closest = child;
|
||||
closestDist = dist;
|
||||
closestPriority = priority;
|
||||
}
|
||||
}
|
||||
return closest;
|
||||
|
@ -383,28 +413,7 @@ namespace MLEM.Ui {
|
|||
|
||||
private void HandleGamepadNextElement(Direction2 dir) {
|
||||
this.IsAutoNavMode = true;
|
||||
RectangleF searchArea = default;
|
||||
if (this.SelectedElement?.Root != null) {
|
||||
searchArea = this.SelectedElement.Area;
|
||||
var (_, _, width, height) = this.System.Viewport;
|
||||
switch (dir) {
|
||||
case Direction2.Down:
|
||||
searchArea.Height += height;
|
||||
break;
|
||||
case Direction2.Left:
|
||||
searchArea.X -= width;
|
||||
searchArea.Width += width;
|
||||
break;
|
||||
case Direction2.Right:
|
||||
searchArea.Width += width;
|
||||
break;
|
||||
case Direction2.Up:
|
||||
searchArea.Y -= height;
|
||||
searchArea.Height += height;
|
||||
break;
|
||||
}
|
||||
}
|
||||
var next = this.GetGamepadNextElement(searchArea);
|
||||
var next = this.GetGamepadNextElement(dir);
|
||||
if (this.SelectedElement != null)
|
||||
next = this.SelectedElement.GetGamepadNextElement(dir, next);
|
||||
if (next != null)
|
||||
|
|
|
@ -23,9 +23,16 @@ namespace MLEM.Ui {
|
|||
|
||||
/// <summary>
|
||||
/// The viewport that this ui system is rendering inside of.
|
||||
/// This is automatically updated during <see cref="GameWindow.ClientSizeChanged"/>
|
||||
/// This is automatically updated during <see cref="GameWindow.ClientSizeChanged"/> by default.
|
||||
/// </summary>
|
||||
public Rectangle Viewport;
|
||||
public Rectangle Viewport {
|
||||
get => this.viewport;
|
||||
set {
|
||||
this.viewport = value;
|
||||
foreach (var root in this.rootElements)
|
||||
root.Element.ForceUpdateArea();
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Set this field to true to cause the ui system and all of its elements to automatically scale up or down with greater and lower resolution, respectively.
|
||||
/// If this field is true, <see cref="AutoScaleReferenceSize"/> is used as the size that uses default <see cref="GlobalScale"/>
|
||||
|
@ -59,10 +66,8 @@ namespace MLEM.Ui {
|
|||
get => this.style;
|
||||
set {
|
||||
this.style = value;
|
||||
foreach (var root in this.rootElements) {
|
||||
root.Element.AndChildren(e => e.System = this);
|
||||
root.Element.ForceUpdateArea();
|
||||
}
|
||||
foreach (var root in this.rootElements)
|
||||
root.Element.AndChildren(e => e.Style = e.Style.OrStyle(value));
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
|
@ -100,7 +105,7 @@ namespace MLEM.Ui {
|
|||
public UiControls Controls;
|
||||
/// <summary>
|
||||
/// The update and rendering statistics to be used for runtime debugging and profiling.
|
||||
/// The metrics are reset accordingly every frame: <see cref="UiMetrics.ResetUpdates"/> is called at the start of <see cref="Update"/>, and <see cref="UiMetrics.ResetDraws"/> is called at the start of <see cref="DrawEarly"/>, or at the start of <see cref="Draw"/> if <see cref="DrawEarly"/> was not called.
|
||||
/// The metrics are reset accordingly every frame: <see cref="UiMetrics.ResetUpdates"/> is called at the start of <see cref="Update"/>, and <see cref="UiMetrics.ResetDraws"/> is called at the start of <see cref="Draw"/>.
|
||||
/// </summary>
|
||||
public UiMetrics Metrics;
|
||||
|
||||
|
@ -153,6 +158,10 @@ namespace MLEM.Ui {
|
|||
/// </summary>
|
||||
public event Element.GenericCallback OnElementAreaUpdated;
|
||||
/// <summary>
|
||||
/// Event that is called when an <see cref="Element"/>'s <see cref="Element.InitStyle"/> method is called while setting its <see cref="Element.Style"/>.
|
||||
/// </summary>
|
||||
public event Element.GenericCallback OnElementStyleInit;
|
||||
/// <summary>
|
||||
/// Event that is invoked when the <see cref="Element"/> that the mouse is currently over changes
|
||||
/// </summary>
|
||||
public event Element.GenericCallback OnMousedElementChanged;
|
||||
|
@ -173,12 +182,12 @@ namespace MLEM.Ui {
|
|||
/// </summary>
|
||||
public event RootCallback OnRootRemoved;
|
||||
|
||||
internal readonly Stopwatch Stopwatch = new Stopwatch();
|
||||
|
||||
private readonly List<RootElement> rootElements = new List<RootElement>();
|
||||
private readonly Stopwatch stopwatch = new Stopwatch();
|
||||
private float globalScale = 1;
|
||||
private bool drewEarly;
|
||||
private UiStyle style;
|
||||
private Rectangle viewport;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new ui system with the given settings.
|
||||
|
@ -190,6 +199,7 @@ namespace MLEM.Ui {
|
|||
public UiSystem(Game game, UiStyle style, InputHandler inputHandler = null, bool automaticViewport = true) : base(game) {
|
||||
this.Controls = new UiControls(this, inputHandler);
|
||||
this.style = style;
|
||||
|
||||
this.OnElementDrawn += (e, time, batch, alpha) => e.OnDrawn?.Invoke(e, time, batch, alpha);
|
||||
this.OnElementUpdated += (e, time) => e.OnUpdated?.Invoke(e, time);
|
||||
this.OnElementPressed += e => e.OnPressed?.Invoke(e);
|
||||
|
@ -201,6 +211,7 @@ namespace MLEM.Ui {
|
|||
this.OnElementTouchEnter += e => e.OnTouchEnter?.Invoke(e);
|
||||
this.OnElementTouchExit += e => e.OnTouchExit?.Invoke(e);
|
||||
this.OnElementAreaUpdated += e => e.OnAreaUpdated?.Invoke(e);
|
||||
this.OnElementStyleInit += e => e.OnStyleInit?.Invoke(e);
|
||||
this.OnMousedElementChanged += e => this.ApplyToAll(t => t.OnMousedElementChanged?.Invoke(t, e));
|
||||
this.OnTouchedElementChanged += e => this.ApplyToAll(t => t.OnTouchedElementChanged?.Invoke(t, e));
|
||||
this.OnSelectedElementChanged += e => this.ApplyToAll(t => t.OnSelectedElementChanged?.Invoke(t, e));
|
||||
|
@ -216,6 +227,7 @@ namespace MLEM.Ui {
|
|||
if (e.OnSecondaryPressed != null)
|
||||
e.SecondActionSound.Value?.Play();
|
||||
};
|
||||
|
||||
MlemPlatform.Current?.AddTextInputListener(game.Window, (sender, key, character) => this.ApplyToAll(e => e.OnTextInput?.Invoke(e, key, character)));
|
||||
|
||||
if (automaticViewport) {
|
||||
|
@ -223,14 +235,13 @@ namespace MLEM.Ui {
|
|||
this.AutoScaleReferenceSize = this.Viewport.Size;
|
||||
game.Window.ClientSizeChanged += (sender, args) => {
|
||||
this.Viewport = new Rectangle(Point.Zero, game.Window.ClientBounds.Size);
|
||||
foreach (var root in this.rootElements)
|
||||
root.Element.ForceUpdateArea();
|
||||
};
|
||||
}
|
||||
|
||||
this.TextFormatter = new TextFormatter();
|
||||
this.TextFormatter.Codes.Add(new Regex("<l(?: ([^>]+))?>"), (f, m, r) => new LinkCode(m, r, 1 / 16F, 0.85F,
|
||||
t => this.Controls.MousedElement is Paragraph.Link l1 && l1.Token == t || this.Controls.TouchedElement is Paragraph.Link l2 && l2.Token == t));
|
||||
t => this.Controls.MousedElement is Paragraph.Link l1 && l1.Token == t || this.Controls.TouchedElement is Paragraph.Link l2 && l2.Token == t,
|
||||
this.Style.LinkColor));
|
||||
this.TextFormatter.Codes.Add(new Regex("<f ([^>]+)>"), (_, m, r) => new FontCode(m, r,
|
||||
f => this.Style.AdditionalFonts != null && this.Style.AdditionalFonts.TryGetValue(m.Groups[1].Value, out var c) ? c : f));
|
||||
}
|
||||
|
@ -241,14 +252,14 @@ namespace MLEM.Ui {
|
|||
/// <param name="time">The game's time</param>
|
||||
public override void Update(GameTime time) {
|
||||
this.Metrics.ResetUpdates();
|
||||
this.Stopwatch.Restart();
|
||||
this.stopwatch.Restart();
|
||||
|
||||
this.Controls.Update();
|
||||
for (var i = this.rootElements.Count - 1; i >= 0; i--)
|
||||
this.rootElements[i].Element.Update(time);
|
||||
|
||||
this.Stopwatch.Stop();
|
||||
this.Metrics.UpdateTime += this.Stopwatch.Elapsed;
|
||||
this.stopwatch.Stop();
|
||||
this.Metrics.UpdateTime += this.stopwatch.Elapsed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -257,30 +268,30 @@ namespace MLEM.Ui {
|
|||
/// </summary>
|
||||
/// <param name="time">The game's time</param>
|
||||
/// <param name="batch">The sprite batch to use for drawing</param>
|
||||
[Obsolete("DrawEarly is deprecated. Calling it is not required anymore, and there is no replacement.")]
|
||||
public void DrawEarly(GameTime time, SpriteBatch batch) {
|
||||
this.Metrics.ResetDraws();
|
||||
this.Stopwatch.Restart();
|
||||
this.stopwatch.Restart();
|
||||
|
||||
foreach (var root in this.rootElements) {
|
||||
if (!root.Element.IsHidden)
|
||||
root.Element.DrawEarly(time, batch, this.DrawAlpha * root.Element.DrawAlpha, this.BlendState, this.SamplerState, this.DepthStencilState, this.Effect, root.Transform);
|
||||
}
|
||||
|
||||
this.Stopwatch.Stop();
|
||||
this.Metrics.DrawTime += this.Stopwatch.Elapsed;
|
||||
this.stopwatch.Stop();
|
||||
this.Metrics.DrawTime += this.stopwatch.Elapsed;
|
||||
this.drewEarly = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws any <see cref="Element"/>s onto the screen.
|
||||
/// Note that, when using <see cref="Panel"/>s with a scroll bar, <see cref="DrawEarly"/> needs to be called as well.
|
||||
/// </summary>
|
||||
/// <param name="time">The game's time</param>
|
||||
/// <param name="batch">The sprite batch to use for drawing</param>
|
||||
public void Draw(GameTime time, SpriteBatch batch) {
|
||||
if (!this.drewEarly)
|
||||
this.Metrics.ResetDraws();
|
||||
this.Stopwatch.Restart();
|
||||
this.stopwatch.Restart();
|
||||
|
||||
foreach (var root in this.rootElements) {
|
||||
if (root.Element.IsHidden)
|
||||
|
@ -291,8 +302,8 @@ namespace MLEM.Ui {
|
|||
batch.End();
|
||||
}
|
||||
|
||||
this.Stopwatch.Stop();
|
||||
this.Metrics.DrawTime += this.Stopwatch.Elapsed;
|
||||
this.stopwatch.Stop();
|
||||
this.Metrics.DrawTime += this.stopwatch.Elapsed;
|
||||
this.drewEarly = false;
|
||||
}
|
||||
|
||||
|
@ -380,21 +391,69 @@ namespace MLEM.Ui {
|
|||
root.Element.AndChildren(action);
|
||||
}
|
||||
|
||||
internal void InvokeOnElementDrawn(Element element, GameTime time, SpriteBatch batch, float alpha) => this.OnElementDrawn?.Invoke(element, time, batch, alpha);
|
||||
internal void InvokeOnSelectedElementDrawn(Element element, GameTime time, SpriteBatch batch, float alpha) => this.OnSelectedElementDrawn?.Invoke(element, time, batch, alpha);
|
||||
internal void InvokeOnElementUpdated(Element element, GameTime time) => this.OnElementUpdated?.Invoke(element, time);
|
||||
internal void InvokeOnElementAreaUpdated(Element element) => this.OnElementAreaUpdated?.Invoke(element);
|
||||
internal void InvokeOnElementPressed(Element element) => this.OnElementPressed?.Invoke(element);
|
||||
internal void InvokeOnElementSecondaryPressed(Element element) => this.OnElementSecondaryPressed?.Invoke(element);
|
||||
internal void InvokeOnElementSelected(Element element) => this.OnElementSelected?.Invoke(element);
|
||||
internal void InvokeOnElementDeselected(Element element) => this.OnElementDeselected?.Invoke(element);
|
||||
internal void InvokeOnSelectedElementChanged(Element element) => this.OnSelectedElementChanged?.Invoke(element);
|
||||
internal void InvokeOnElementMouseExit(Element element) => this.OnElementMouseExit?.Invoke(element);
|
||||
internal void InvokeOnElementMouseEnter(Element element) => this.OnElementMouseEnter?.Invoke(element);
|
||||
internal void InvokeOnMousedElementChanged(Element element) => this.OnMousedElementChanged?.Invoke(element);
|
||||
internal void InvokeOnElementTouchExit(Element element) => this.OnElementTouchExit?.Invoke(element);
|
||||
internal void InvokeOnElementTouchEnter(Element element) => this.OnElementTouchEnter?.Invoke(element);
|
||||
internal void InvokeOnTouchedElementChanged(Element element) => this.OnTouchedElementChanged?.Invoke(element);
|
||||
internal void InvokeOnElementDrawn(Element element, GameTime time, SpriteBatch batch, float alpha) {
|
||||
this.OnElementDrawn?.Invoke(element, time, batch, alpha);
|
||||
}
|
||||
|
||||
internal void InvokeOnSelectedElementDrawn(Element element, GameTime time, SpriteBatch batch, float alpha) {
|
||||
this.OnSelectedElementDrawn?.Invoke(element, time, batch, alpha);
|
||||
}
|
||||
|
||||
internal void InvokeOnElementUpdated(Element element, GameTime time) {
|
||||
this.OnElementUpdated?.Invoke(element, time);
|
||||
}
|
||||
|
||||
internal void InvokeOnElementAreaUpdated(Element element) {
|
||||
this.OnElementAreaUpdated?.Invoke(element);
|
||||
}
|
||||
|
||||
internal void InvokeOnElementStyleInit(Element element) {
|
||||
this.OnElementStyleInit?.Invoke(element);
|
||||
}
|
||||
|
||||
internal void InvokeOnElementPressed(Element element) {
|
||||
this.OnElementPressed?.Invoke(element);
|
||||
}
|
||||
|
||||
internal void InvokeOnElementSecondaryPressed(Element element) {
|
||||
this.OnElementSecondaryPressed?.Invoke(element);
|
||||
}
|
||||
|
||||
internal void InvokeOnElementSelected(Element element) {
|
||||
this.OnElementSelected?.Invoke(element);
|
||||
}
|
||||
|
||||
internal void InvokeOnElementDeselected(Element element) {
|
||||
this.OnElementDeselected?.Invoke(element);
|
||||
}
|
||||
|
||||
internal void InvokeOnSelectedElementChanged(Element element) {
|
||||
this.OnSelectedElementChanged?.Invoke(element);
|
||||
}
|
||||
|
||||
internal void InvokeOnElementMouseExit(Element element) {
|
||||
this.OnElementMouseExit?.Invoke(element);
|
||||
}
|
||||
|
||||
internal void InvokeOnElementMouseEnter(Element element) {
|
||||
this.OnElementMouseEnter?.Invoke(element);
|
||||
}
|
||||
|
||||
internal void InvokeOnMousedElementChanged(Element element) {
|
||||
this.OnMousedElementChanged?.Invoke(element);
|
||||
}
|
||||
|
||||
internal void InvokeOnElementTouchExit(Element element) {
|
||||
this.OnElementTouchExit?.Invoke(element);
|
||||
}
|
||||
|
||||
internal void InvokeOnElementTouchEnter(Element element) {
|
||||
this.OnElementTouchEnter?.Invoke(element);
|
||||
}
|
||||
|
||||
internal void InvokeOnTouchedElementChanged(Element element) {
|
||||
this.OnTouchedElementChanged?.Invoke(element);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A delegate used for callbacks that involve a <see cref="RootElement"/>
|
||||
|
@ -475,7 +534,7 @@ namespace MLEM.Ui {
|
|||
/// Determines whether this root element contains any children that <see cref="Elements.Element.CanBeSelected"/>.
|
||||
/// This value is automatically calculated.
|
||||
/// </summary>
|
||||
public bool CanSelectContent { get; private set; }
|
||||
public bool CanSelectContent => this.Element.CanBeSelected || this.Element.GetChildren(c => c.CanBeSelected, true).Any();
|
||||
|
||||
/// <summary>
|
||||
/// Event that is invoked when a <see cref="Element"/> is added to this root element or any of its children.
|
||||
|
@ -498,21 +557,6 @@ namespace MLEM.Ui {
|
|||
this.Name = name;
|
||||
this.Element = element;
|
||||
this.System = system;
|
||||
|
||||
this.OnElementAdded += e => {
|
||||
if (e.CanBeSelected)
|
||||
this.CanSelectContent = true;
|
||||
};
|
||||
this.OnElementRemoved += e => {
|
||||
if (e.CanBeSelected) {
|
||||
// check if removing this element removed all other selectable elements
|
||||
foreach (var c in this.Element.GetChildren(regardGrandchildren: true)) {
|
||||
if (c.CanBeSelected)
|
||||
return;
|
||||
}
|
||||
this.CanSelectContent = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -534,10 +578,21 @@ namespace MLEM.Ui {
|
|||
this.Transform = Matrix.CreateScale(scale, scale, 1) * Matrix.CreateTranslation(new Vector3((1 - scale) * (origin ?? this.Element.DisplayArea.Center), 0));
|
||||
}
|
||||
|
||||
internal void InvokeOnElementAdded(Element element) => this.OnElementAdded?.Invoke(element);
|
||||
internal void InvokeOnElementRemoved(Element element) => this.OnElementRemoved?.Invoke(element);
|
||||
internal void InvokeOnAddedToUi(UiSystem system) => this.OnAddedToUi?.Invoke(system);
|
||||
internal void InvokeOnRemovedFromUi(UiSystem system) => this.OnRemovedFromUi?.Invoke(system);
|
||||
internal void InvokeOnElementAdded(Element element) {
|
||||
this.OnElementAdded?.Invoke(element);
|
||||
}
|
||||
|
||||
internal void InvokeOnElementRemoved(Element element) {
|
||||
this.OnElementRemoved?.Invoke(element);
|
||||
}
|
||||
|
||||
internal void InvokeOnAddedToUi(UiSystem system) {
|
||||
this.OnAddedToUi?.Invoke(system);
|
||||
}
|
||||
|
||||
internal void InvokeOnRemovedFromUi(UiSystem system) {
|
||||
this.OnRemovedFromUi?.Invoke(system);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -103,18 +103,14 @@ namespace MLEM.Animations {
|
|||
/// </summary>
|
||||
/// <param name="timePerFrame">The amount of time that each frame should last for</param>
|
||||
/// <param name="regions">The texture regions that should make up this animation</param>
|
||||
public SpriteAnimation(double timePerFrame, params TextureRegion[] regions) :
|
||||
this(Array.ConvertAll(regions, r => new AnimationFrame(timePerFrame, r))) {
|
||||
}
|
||||
public SpriteAnimation(double timePerFrame, params TextureRegion[] regions) : this(Array.ConvertAll(regions, r => new AnimationFrame(timePerFrame, r))) {}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new sprite animation that contains the given texture region arrays as frames, where each frame represents one set of texture regions.
|
||||
/// </summary>
|
||||
/// <param name="timePerFrame">The amount of time that each frame should last for</param>
|
||||
/// <param name="regions">The texture regions that should make up this animation</param>
|
||||
public SpriteAnimation(double timePerFrame, params TextureRegion[][] regions) :
|
||||
this(Array.ConvertAll(regions, r => new AnimationFrame(timePerFrame, r))) {
|
||||
}
|
||||
public SpriteAnimation(double timePerFrame, params TextureRegion[][] regions) : this(Array.ConvertAll(regions, r => new AnimationFrame(timePerFrame, r))) {}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new sprite animation based on the given texture regions in rectangle-based format.
|
||||
|
@ -122,9 +118,7 @@ namespace MLEM.Animations {
|
|||
/// <param name="timePerFrame">The amount of time that each frame should last for</param>
|
||||
/// <param name="texture">The texture that the regions should come from</param>
|
||||
/// <param name="regions">The texture regions that should make up this animation</param>
|
||||
public SpriteAnimation(double timePerFrame, Texture2D texture, params Rectangle[] regions) :
|
||||
this(timePerFrame, Array.ConvertAll(regions, r => new TextureRegion(texture, r))) {
|
||||
}
|
||||
public SpriteAnimation(double timePerFrame, Texture2D texture, params Rectangle[] regions) : this(timePerFrame, Array.ConvertAll(regions, r => new TextureRegion(texture, r))) {}
|
||||
|
||||
/// <summary>
|
||||
/// Updates this animation, causing <see cref="TimeIntoAnimation"/> to be increased and the <see cref="CurrentFrame"/> to be updated.
|
||||
|
|
|
@ -3,16 +3,16 @@ using Microsoft.Xna.Framework;
|
|||
|
||||
namespace MLEM.Extensions {
|
||||
/// <summary>
|
||||
/// A set of extensions for dealing with <see cref="Color"/> objects
|
||||
/// A set of extensions for dealing with <see cref="Color"/> objects.
|
||||
/// </summary>
|
||||
public static class ColorExtensions {
|
||||
|
||||
/// <summary>
|
||||
/// Copies the alpha value from another color into this color.
|
||||
/// Copies the alpha value from another color into this color and returns the result.
|
||||
/// </summary>
|
||||
/// <param name="color">The color</param>
|
||||
/// <param name="other">The color to copy the alpha from</param>
|
||||
/// <returns>The first color with the second color's alpha value</returns>
|
||||
/// <param name="color">The color.</param>
|
||||
/// <param name="other">The color to copy the alpha from.</param>
|
||||
/// <returns>The first color with the second color's alpha value.</returns>
|
||||
public static Color CopyAlpha(this Color color, Color other) {
|
||||
return color * (other.A / 255F);
|
||||
}
|
||||
|
@ -20,16 +20,26 @@ namespace MLEM.Extensions {
|
|||
/// <summary>
|
||||
/// Returns an inverted version of this color.
|
||||
/// </summary>
|
||||
/// <param name="color">The color to invert</param>
|
||||
/// <returns>The inverted color</returns>
|
||||
/// <param name="color">The color to invert.</param>
|
||||
/// <returns>The inverted color.</returns>
|
||||
public static Color Invert(this Color color) {
|
||||
return new Color(255 - color.R, 255 - color.G, 255 - color.B, color.A);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Multiplies this color with another color and returns the result.
|
||||
/// </summary>
|
||||
/// <param name="color">The first color.</param>
|
||||
/// <param name="other">The second color.</param>
|
||||
/// <returns>The two colors multiplied together.</returns>
|
||||
public static Color Multiply(this Color color, Color other) {
|
||||
return new Color(color.ToVector4() * other.ToVector4());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A set of utility methods for dealing with <see cref="Color"/> objects
|
||||
/// A set of utility methods for dealing with <see cref="Color"/> objects.
|
||||
/// </summary>
|
||||
public static class ColorHelper {
|
||||
|
||||
|
@ -37,28 +47,28 @@ namespace MLEM.Extensions {
|
|||
/// Parses a hexadecimal number into an rgba color.
|
||||
/// The number should be in the format <c>0xaarrggbb</c>.
|
||||
/// </summary>
|
||||
/// <param name="value">The number to parse</param>
|
||||
/// <returns>The resulting color</returns>
|
||||
/// <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>
|
||||
/// Parses a hexadecimal number into an rgb color with 100% alpha.
|
||||
/// The number should be in the format <c>0xrrggbb</c>.
|
||||
/// </summary>
|
||||
/// <param name="value">The number to parse</param>
|
||||
/// <returns>The resulting color</returns>
|
||||
/// <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>
|
||||
/// Parses a hexadecimal string into a color.
|
||||
/// The string can either be formatted as RRGGBB or AARRGGBB and can optionally start with a <c>#</c>.
|
||||
/// </summary>
|
||||
/// <param name="value">The string to parse</param>
|
||||
/// <returns>The resulting color</returns>
|
||||
/// <param name="value">The string to parse.</param>
|
||||
/// <returns>The resulting color.</returns>
|
||||
public static Color FromHexString(string value) {
|
||||
if (value.StartsWith("#"))
|
||||
value = value.Substring(1);
|
||||
|
|
|
@ -41,5 +41,18 @@ namespace MLEM.Extensions {
|
|||
throw new IndexOutOfRangeException();
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="GetRandomWeightedEntry{T}(System.Random,System.Collections.Generic.IList{T},System.Func{T,int})"/>
|
||||
public static T GetRandomWeightedEntry<T>(this Random random, IList<T> entries, Func<T, float> weightFunc) {
|
||||
var totalWeight = entries.Sum(weightFunc);
|
||||
var goalWeight = random.NextDouble() * totalWeight;
|
||||
var currWeight = 0F;
|
||||
foreach (var entry in entries) {
|
||||
currWeight += weightFunc(entry);
|
||||
if (currWeight >= goalWeight)
|
||||
return entry;
|
||||
}
|
||||
throw new IndexOutOfRangeException();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -18,7 +18,10 @@ namespace MLEM.Font {
|
|||
/// This is a character that isn't drawn, but has the same width as <see cref="LineHeight"/>.
|
||||
/// Whereas a regular <see cref="SpriteFont"/> would have to explicitly support this character for width calculations, generic fonts implicitly support it in <see cref="MeasureString(string,bool)"/>.
|
||||
/// </summary>
|
||||
public const char OneEmSpace = '\u2003';
|
||||
public const char Emsp = '\u2003';
|
||||
/// <inheritdoc cref="Emsp"/>
|
||||
[Obsolete("Use the Emsp field instead.")]
|
||||
public const char OneEmSpace = Emsp;
|
||||
/// <summary>
|
||||
/// This field holds the unicode representation of a non-breaking space.
|
||||
/// Whereas a regular <see cref="SpriteFont"/> would have to explicitly support this character for width calculations, generic fonts implicitly support it in <see cref="MeasureString(string,bool)"/>.
|
||||
|
@ -26,7 +29,7 @@ namespace MLEM.Font {
|
|||
public const char Nbsp = '\u00A0';
|
||||
/// <summary>
|
||||
/// This field holds the unicode representation of a zero-width space.
|
||||
/// Whereas a regular <see cref="SpriteFont"/> would have to explicitly support this character for width calculations and string splitting, generic fonts implicitly support it in <see cref="MeasureString(string,bool)"/> and <see cref="SplitString"/>.
|
||||
/// Whereas a regular <see cref="SpriteFont"/> would have to explicitly support this character for width calculations and string splitting, generic fonts implicitly support it in <see cref="MeasureString(string,bool)"/> and <see cref="SplitString(string,float,float)"/>.
|
||||
/// </summary>
|
||||
public const char Zwsp = '\u200B';
|
||||
|
||||
|
@ -48,17 +51,35 @@ namespace MLEM.Font {
|
|||
|
||||
/// <summary>
|
||||
/// Measures the width of the given character with the default scale for use in <see cref="MeasureString(string,bool)"/>.
|
||||
/// Note that this method does not support <see cref="Nbsp"/>, <see cref="Zwsp"/> and <see cref="OneEmSpace"/> for most generic fonts, which is why <see cref="MeasureString(string,bool)"/> should be used even for single characters.
|
||||
/// Note that this method does not support <see cref="Nbsp"/>, <see cref="Zwsp"/> and <see cref="Emsp"/> for most generic fonts, which is why <see cref="MeasureString(string,bool)"/> should be used even for single characters.
|
||||
/// </summary>
|
||||
/// <param name="c">The character whose width to calculate</param>
|
||||
/// <returns>The width of the given character with the default scale</returns>
|
||||
protected abstract float MeasureChar(char c);
|
||||
|
||||
///<inheritdoc cref="SpriteBatch.DrawString(SpriteFont,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/>
|
||||
public abstract void DrawString(SpriteBatch batch, string text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth);
|
||||
/// <summary>
|
||||
/// Draws the given character with the given data for use in <see cref="DrawString(Microsoft.Xna.Framework.Graphics.SpriteBatch,System.Text.StringBuilder,Microsoft.Xna.Framework.Vector2,Microsoft.Xna.Framework.Color,float,Microsoft.Xna.Framework.Vector2,Microsoft.Xna.Framework.Vector2,Microsoft.Xna.Framework.Graphics.SpriteEffects,float)"/>.
|
||||
/// Note that this method is only called internally.
|
||||
/// </summary>
|
||||
/// <param name="batch">The sprite batch to draw with.</param>
|
||||
/// <param name="cString">A string representation of the character which will be drawn.</param>
|
||||
/// <param name="position">The drawing location on screen.</param>
|
||||
/// <param name="color">A color mask.</param>
|
||||
/// <param name="rotation">A rotation of this character.</param>
|
||||
/// <param name="scale">A scaling of this character.</param>
|
||||
/// <param name="effects">Modificators for drawing. Can be combined.</param>
|
||||
/// <param name="layerDepth">A depth of the layer of this character.</param>
|
||||
protected abstract void DrawChar(SpriteBatch batch, string cString, Vector2 position, Color color, float rotation, Vector2 scale, SpriteEffects effects, float layerDepth);
|
||||
|
||||
///<inheritdoc cref="SpriteBatch.DrawString(SpriteFont,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/>
|
||||
public abstract void DrawString(SpriteBatch batch, StringBuilder text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth);
|
||||
public void DrawString(SpriteBatch batch, string text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth) {
|
||||
this.DrawString(batch, new CharSource(text), position, color, rotation, origin, scale, effects, layerDepth);
|
||||
}
|
||||
|
||||
///<inheritdoc cref="SpriteBatch.DrawString(SpriteFont,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/>
|
||||
public void DrawString(SpriteBatch batch, StringBuilder text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth) {
|
||||
this.DrawString(batch, new CharSource(text), position, color, rotation, origin, scale, effects, layerDepth);
|
||||
}
|
||||
|
||||
///<inheritdoc cref="SpriteBatch.DrawString(SpriteFont,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/>
|
||||
public void DrawString(SpriteBatch batch, string text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects effects, float layerDepth) {
|
||||
|
@ -82,14 +103,19 @@ namespace MLEM.Font {
|
|||
|
||||
/// <summary>
|
||||
/// Measures the width of the given string when drawn with this font's underlying font.
|
||||
/// This method uses <see cref="MeasureChar"/> internally to calculate the size of known characters and calculates additional characters like <see cref="Nbsp"/>, <see cref="Zwsp"/> and <see cref="OneEmSpace"/>.
|
||||
/// This method uses <see cref="MeasureChar"/> internally to calculate the size of known characters and calculates additional characters like <see cref="Nbsp"/>, <see cref="Zwsp"/> and <see cref="Emsp"/>.
|
||||
/// If the text contains newline characters (\n), the size returned will represent a rectangle that encompasses the width of the longest line and the string's full height.
|
||||
/// </summary>
|
||||
/// <param name="text">The text whose size to calculate</param>
|
||||
/// <param name="ignoreTrailingSpaces">Whether trailing whitespace should be ignored in the returned size, causing the end of each line to be effectively trimmed</param>
|
||||
/// <returns>The size of the string when drawn with this font</returns>
|
||||
public Vector2 MeasureString(string text, bool ignoreTrailingSpaces = false) {
|
||||
return this.MeasureString(text, ignoreTrailingSpaces, null);
|
||||
return this.MeasureString(new CharSource(text), ignoreTrailingSpaces, null);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="MeasureString(string,bool)"/>
|
||||
public Vector2 MeasureString(StringBuilder text, bool ignoreTrailingSpaces = false) {
|
||||
return this.MeasureString(new CharSource(text), ignoreTrailingSpaces, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -103,7 +129,12 @@ namespace MLEM.Font {
|
|||
/// <param name="ellipsis">The characters to add to the end of the string if it is too long</param>
|
||||
/// <returns>The truncated string, or the same string if it is shorter than the maximum width</returns>
|
||||
public string TruncateString(string text, float width, float scale, bool fromBack = false, string ellipsis = "") {
|
||||
return this.TruncateString(text, width, scale, fromBack, ellipsis, null);
|
||||
return this.TruncateString(new CharSource(text), width, scale, fromBack, ellipsis, null).ToString();
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="TruncateString(string,float,float,bool,string)"/>
|
||||
public StringBuilder TruncateString(StringBuilder text, float width, float scale, bool fromBack = false, string ellipsis = "") {
|
||||
return this.TruncateString(new CharSource(text), width, scale, fromBack, ellipsis, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -119,20 +150,30 @@ namespace MLEM.Font {
|
|||
return string.Join("\n", this.SplitStringSeparate(text, width, scale));
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="SplitString(string,float,float)"/>
|
||||
public string SplitString(StringBuilder text, float width, float scale) {
|
||||
return string.Join("\n", this.SplitStringSeparate(text, width, scale));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Splits a string to a given maximum width and returns each split section as a separate string.
|
||||
/// Note that existing new lines are taken into account for line length, but not split in the resulting strings.
|
||||
/// This method differs from <see cref="SplitString"/> in that it differentiates between pre-existing newline characters and splits due to maximum width.
|
||||
/// This method differs from <see cref="SplitString(string,float,float)"/> in that it differentiates between pre-existing newline characters and splits due to maximum width.
|
||||
/// </summary>
|
||||
/// <param name="text">The text to split into multiple lines</param>
|
||||
/// <param name="width">The maximum width that each line should have</param>
|
||||
/// <param name="scale">The scale to use for width measurements</param>
|
||||
/// <returns>The split string as an enumerable of split sections</returns>
|
||||
public IEnumerable<string> SplitStringSeparate(string text, float width, float scale) {
|
||||
return this.SplitStringSeparate(text, width, scale, null);
|
||||
return this.SplitStringSeparate(new CharSource(text), width, scale, null);
|
||||
}
|
||||
|
||||
internal Vector2 MeasureString(string text, bool ignoreTrailingSpaces, Func<int, GenericFont> fontFunction) {
|
||||
/// <inheritdoc cref="SplitStringSeparate(string,float,float)"/>
|
||||
public IEnumerable<string> SplitStringSeparate(StringBuilder text, float width, float scale) {
|
||||
return this.SplitStringSeparate(new CharSource(text), width, scale, null);
|
||||
}
|
||||
|
||||
internal Vector2 MeasureString(CharSource text, bool ignoreTrailingSpaces, Func<int, GenericFont> fontFunction) {
|
||||
var size = Vector2.Zero;
|
||||
if (text.Length <= 0)
|
||||
return size;
|
||||
|
@ -144,7 +185,7 @@ namespace MLEM.Font {
|
|||
xOffset = 0;
|
||||
size.Y += this.LineHeight;
|
||||
break;
|
||||
case OneEmSpace:
|
||||
case Emsp:
|
||||
xOffset += this.LineHeight;
|
||||
break;
|
||||
case Nbsp:
|
||||
|
@ -174,7 +215,7 @@ namespace MLEM.Font {
|
|||
return size;
|
||||
}
|
||||
|
||||
internal string TruncateString(string text, float width, float scale, bool fromBack, string ellipsis, Func<int, GenericFont> fontFunction) {
|
||||
internal StringBuilder TruncateString(CharSource text, float width, float scale, bool fromBack, string ellipsis, Func<int, GenericFont> fontFunction) {
|
||||
var total = new StringBuilder();
|
||||
for (var i = 0; i < text.Length; i++) {
|
||||
if (fromBack) {
|
||||
|
@ -186,16 +227,16 @@ namespace MLEM.Font {
|
|||
var font = fontFunction?.Invoke(i) ?? this;
|
||||
if (font.MeasureString(total + ellipsis).X * scale >= width) {
|
||||
if (fromBack) {
|
||||
return total.Remove(0, 1).Insert(0, ellipsis).ToString();
|
||||
return total.Remove(0, 1).Insert(0, ellipsis);
|
||||
} else {
|
||||
return total.Remove(total.Length - 1, 1).Append(ellipsis).ToString();
|
||||
return total.Remove(total.Length - 1, 1).Append(ellipsis);
|
||||
}
|
||||
}
|
||||
}
|
||||
return total.ToString();
|
||||
return total;
|
||||
}
|
||||
|
||||
internal IEnumerable<string> SplitStringSeparate(string text, float width, float scale, Func<int, GenericFont> fontFunction) {
|
||||
internal IEnumerable<string> SplitStringSeparate(CharSource text, float width, float scale, Func<int, GenericFont> fontFunction) {
|
||||
var currWidth = 0F;
|
||||
var lastSpaceIndex = -1;
|
||||
var widthSinceLastSpace = 0F;
|
||||
|
@ -211,7 +252,7 @@ namespace MLEM.Font {
|
|||
} else {
|
||||
var font = fontFunction?.Invoke(i) ?? this;
|
||||
var cWidth = font.MeasureString(c.ToCachedString()).X * scale;
|
||||
if (c == ' ' || c == OneEmSpace || c == Zwsp) {
|
||||
if (c == ' ' || c == Emsp || c == Zwsp) {
|
||||
// remember the location of this (breaking!) space
|
||||
lastSpaceIndex = curr.Length;
|
||||
widthSinceLastSpace = 0;
|
||||
|
@ -243,7 +284,64 @@ namespace MLEM.Font {
|
|||
yield return curr.ToString();
|
||||
}
|
||||
|
||||
private static bool IsTrailingSpace(string s, int index) {
|
||||
private void DrawString(SpriteBatch batch, CharSource text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth) {
|
||||
var (flipX, flipY) = Vector2.Zero;
|
||||
var flippedV = (effects & SpriteEffects.FlipVertically) != 0;
|
||||
var flippedH = (effects & SpriteEffects.FlipHorizontally) != 0;
|
||||
if (flippedV || flippedH) {
|
||||
var (w, h) = this.MeasureString(text, false, null);
|
||||
if (flippedH) {
|
||||
origin.X *= -1;
|
||||
flipX = -w;
|
||||
}
|
||||
if (flippedV) {
|
||||
origin.Y *= -1;
|
||||
flipY = this.LineHeight - h;
|
||||
}
|
||||
}
|
||||
|
||||
var trans = Matrix.Identity;
|
||||
if (rotation == 0) {
|
||||
trans.M11 = flippedH ? -scale.X : scale.X;
|
||||
trans.M22 = flippedV ? -scale.Y : scale.Y;
|
||||
trans.M41 = (flipX - origin.X) * trans.M11 + position.X;
|
||||
trans.M42 = (flipY - origin.Y) * trans.M22 + position.Y;
|
||||
} else {
|
||||
var sin = (float) Math.Sin(rotation);
|
||||
var cos = (float) Math.Cos(rotation);
|
||||
trans.M11 = (flippedH ? -scale.X : scale.X) * cos;
|
||||
trans.M12 = (flippedH ? -scale.X : scale.X) * sin;
|
||||
trans.M21 = (flippedV ? -scale.Y : scale.Y) * -sin;
|
||||
trans.M22 = (flippedV ? -scale.Y : scale.Y) * cos;
|
||||
trans.M41 = (flipX - origin.X) * trans.M11 + (flipY - origin.Y) * trans.M21 + position.X;
|
||||
trans.M42 = (flipX - origin.X) * trans.M12 + (flipY - origin.Y) * trans.M22 + position.Y;
|
||||
}
|
||||
|
||||
var offset = Vector2.Zero;
|
||||
for (var i = 0; i < text.Length; i++) {
|
||||
var c = text[i];
|
||||
if (c == '\n') {
|
||||
offset.X = 0;
|
||||
offset.Y += this.LineHeight;
|
||||
continue;
|
||||
}
|
||||
|
||||
var cString = c.ToCachedString();
|
||||
var (cW, cH) = this.MeasureString(cString);
|
||||
|
||||
var charPos = offset;
|
||||
if (flippedH)
|
||||
charPos.X += cW;
|
||||
if (flippedV)
|
||||
charPos.Y += cH - this.LineHeight;
|
||||
Vector2.Transform(ref charPos, ref trans, out charPos);
|
||||
|
||||
this.DrawChar(batch, cString, charPos, color, rotation, scale, effects, layerDepth);
|
||||
offset.X += cW;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsTrailingSpace(CharSource s, int index) {
|
||||
for (var i = index + 1; i < s.Length; i++) {
|
||||
if (s[i] != ' ')
|
||||
return false;
|
||||
|
@ -251,5 +349,25 @@ namespace MLEM.Font {
|
|||
return true;
|
||||
}
|
||||
|
||||
internal readonly struct CharSource {
|
||||
|
||||
private readonly string strg;
|
||||
private readonly StringBuilder builder;
|
||||
|
||||
public int Length => this.strg?.Length ?? this.builder.Length;
|
||||
public char this[int index] => this.strg?[index] ?? this.builder[index];
|
||||
|
||||
public CharSource(string strg) {
|
||||
this.strg = strg;
|
||||
this.builder = null;
|
||||
}
|
||||
|
||||
public CharSource(StringBuilder builder) {
|
||||
this.strg = null;
|
||||
this.builder = builder;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.Xna.Framework;
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
using MLEM.Extensions;
|
||||
|
@ -38,13 +37,8 @@ namespace MLEM.Font {
|
|||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void DrawString(SpriteBatch batch, string text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth) {
|
||||
batch.DrawString(this.Font, text, position, color, rotation, origin, scale, effects, layerDepth);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void DrawString(SpriteBatch batch, StringBuilder text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth) {
|
||||
batch.DrawString(this.Font, text, position, color, rotation, origin, scale, effects, layerDepth);
|
||||
protected override void DrawChar(SpriteBatch batch, string cString, Vector2 position, Color color, float rotation, Vector2 scale, SpriteEffects effects, float layerDepth) {
|
||||
batch.DrawString(this.Font, cString, position, color, rotation, Vector2.Zero, scale, effects, layerDepth);
|
||||
}
|
||||
|
||||
private static SpriteFont SetDefaults(SpriteFont font) {
|
||||
|
|
|
@ -5,8 +5,7 @@ namespace MLEM.Formatting.Codes {
|
|||
public class AnimatedCode : Code {
|
||||
|
||||
/// <inheritdoc />
|
||||
public AnimatedCode(Match match, Regex regex) : base(match, regex) {
|
||||
}
|
||||
public AnimatedCode(Match match, Regex regex) : base(match, regex) {}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool EndsHere(Code other) {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.Xna.Framework;
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
|
@ -20,9 +21,10 @@ namespace MLEM.Formatting.Codes {
|
|||
/// </summary>
|
||||
public readonly Match Match;
|
||||
/// <summary>
|
||||
/// The token that this formatting code is a part of
|
||||
/// The tokens that this formatting code is a part of.
|
||||
/// Note that this array only has multiple entries if additional tokens have to be started while this code is still applied.
|
||||
/// </summary>
|
||||
public Token Token { get; internal set; }
|
||||
public IList<Token> Tokens { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new formatting code based on a formatting code regex and its match.
|
||||
|
@ -58,8 +60,7 @@ namespace MLEM.Formatting.Codes {
|
|||
/// Update this formatting code's animations etc.
|
||||
/// </summary>
|
||||
/// <param name="time">The game's time</param>
|
||||
public virtual void Update(GameTime time) {
|
||||
}
|
||||
public virtual void Update(GameTime time) {}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the string that this formatting code should be replaced with.
|
||||
|
@ -72,13 +73,12 @@ namespace MLEM.Formatting.Codes {
|
|||
}
|
||||
|
||||
/// <inheritdoc cref="Formatting.Token.DrawCharacter"/>
|
||||
public virtual bool DrawCharacter(GameTime time, SpriteBatch batch, char c, string cString, int indexInToken, ref Vector2 pos, GenericFont font, ref Color color, ref float scale, float depth) {
|
||||
public virtual bool DrawCharacter(GameTime time, SpriteBatch batch, char c, string cString, Token token, int indexInToken, ref Vector2 pos, GenericFont font, ref Color color, ref float scale, float depth) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Formatting.Token.DrawSelf"/>
|
||||
public virtual void DrawSelf(GameTime time, SpriteBatch batch, Vector2 pos, GenericFont font, Color color, float scale, float depth) {
|
||||
}
|
||||
public virtual void DrawSelf(GameTime time, SpriteBatch batch, Token token, Vector2 pos, GenericFont font, Color color, float scale, float depth) {}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new formatting code from the given regex and regex match.
|
||||
|
|
|
@ -27,7 +27,7 @@ namespace MLEM.Formatting.Codes {
|
|||
|
||||
/// <inheritdoc />
|
||||
public override string GetReplacementString(GenericFont font) {
|
||||
return GenericFont.OneEmSpace.ToCachedString();
|
||||
return GenericFont.Emsp.ToCachedString();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
@ -36,7 +36,7 @@ namespace MLEM.Formatting.Codes {
|
|||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void DrawSelf(GameTime time, SpriteBatch batch, Vector2 pos, GenericFont font, Color color, float scale, float depth) {
|
||||
public override void DrawSelf(GameTime time, SpriteBatch batch, Token token, Vector2 pos, GenericFont font, Color color, float scale, float depth) {
|
||||
var actualColor = this.copyTextColor ? color : Color.White.CopyAlpha(color);
|
||||
batch.Draw(this.image.CurrentRegion, new RectangleF(pos, new Vector2(font.LineHeight * scale)), actualColor);
|
||||
}
|
||||
|
|
|
@ -9,10 +9,12 @@ namespace MLEM.Formatting.Codes {
|
|||
public class LinkCode : UnderlineCode {
|
||||
|
||||
private readonly Func<Token, bool> isSelected;
|
||||
private readonly Color? color;
|
||||
|
||||
/// <inheritdoc />
|
||||
public LinkCode(Match match, Regex regex, float thickness, float yOffset, Func<Token, bool> isSelected) : base(match, regex, thickness, yOffset) {
|
||||
public LinkCode(Match match, Regex regex, float thickness, float yOffset, Func<Token, bool> isSelected, Color? color = null) : base(match, regex, thickness, yOffset) {
|
||||
this.isSelected = isSelected;
|
||||
this.color = color;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -20,13 +22,22 @@ namespace MLEM.Formatting.Codes {
|
|||
/// </summary>
|
||||
/// <returns>True if this code is currently selected</returns>
|
||||
public virtual bool IsSelected() {
|
||||
return this.isSelected(this.Token);
|
||||
foreach (var token in this.Tokens) {
|
||||
if (this.isSelected(token))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool DrawCharacter(GameTime time, SpriteBatch batch, char c, string cString, int indexInToken, ref Vector2 pos, GenericFont font, ref Color color, ref float scale, float depth) {
|
||||
public override Color? GetColor(Color defaultPick) {
|
||||
return this.color;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool DrawCharacter(GameTime time, SpriteBatch batch, char c, string cString, Token token, int indexInToken, ref Vector2 pos, GenericFont font, ref Color color, ref float scale, float depth) {
|
||||
// since we inherit from UnderlineCode, we can just call base if selected
|
||||
return this.IsSelected() && base.DrawCharacter(time, batch, c, cString, indexInToken, ref pos, font, ref color, ref scale, depth);
|
||||
return this.IsSelected() && base.DrawCharacter(time, batch, c, cString, token, indexInToken, ref pos, font, ref color, ref scale, depth);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -5,8 +5,7 @@ namespace MLEM.Formatting.Codes {
|
|||
public class ResetFormattingCode : Code {
|
||||
|
||||
/// <inheritdoc />
|
||||
public ResetFormattingCode(Match match, Regex regex) : base(match, regex) {
|
||||
}
|
||||
public ResetFormattingCode(Match match, Regex regex) : base(match, regex) {}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool EndsHere(Code other) {
|
||||
|
|
|
@ -18,7 +18,7 @@ namespace MLEM.Formatting.Codes {
|
|||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool DrawCharacter(GameTime time, SpriteBatch batch, char c, string cString, int indexInToken, ref Vector2 pos, GenericFont font, ref Color color, ref float scale, float depth) {
|
||||
public override bool DrawCharacter(GameTime time, SpriteBatch batch, char c, string cString, Token token, int indexInToken, ref Vector2 pos, GenericFont font, ref Color color, ref float scale, float depth) {
|
||||
font.DrawString(batch, cString, pos + this.offset * scale, this.color.CopyAlpha(color), 0, Vector2.Zero, scale, SpriteEffects.None, depth);
|
||||
// we return false since we still want regular drawing to occur
|
||||
return false;
|
||||
|
|
|
@ -19,9 +19,9 @@ namespace MLEM.Formatting.Codes {
|
|||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool DrawCharacter(GameTime time, SpriteBatch batch, char c, string cString, int indexInToken, ref Vector2 pos, GenericFont font, ref Color color, ref float scale, float depth) {
|
||||
public override bool DrawCharacter(GameTime time, SpriteBatch batch, char c, string cString, Token token, int indexInToken, ref Vector2 pos, GenericFont font, ref Color color, ref float scale, float depth) {
|
||||
// don't underline spaces at the end of lines
|
||||
if (c == ' ' && this.Token.DisplayString.Length > indexInToken + 1 && this.Token.DisplayString[indexInToken + 1] == '\n')
|
||||
if (c == ' ' && token.DisplayString.Length > indexInToken + 1 && token.DisplayString[indexInToken + 1] == '\n')
|
||||
return false;
|
||||
var (w, h) = font.MeasureString(cString) * scale;
|
||||
var t = h * this.thickness;
|
||||
|
|
|
@ -28,8 +28,8 @@ namespace MLEM.Formatting.Codes {
|
|||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool DrawCharacter(GameTime time, SpriteBatch batch, char c, string cString, int indexInToken, ref Vector2 pos, GenericFont font, ref Color color, ref float scale, float depth) {
|
||||
var offset = new Vector2(0, (float) Math.Sin(this.Token.Index + indexInToken + this.TimeIntoAnimation.TotalSeconds * this.modifier) * font.LineHeight * this.heightModifier * scale);
|
||||
public override bool DrawCharacter(GameTime time, SpriteBatch batch, char c, string cString, Token token, int indexInToken, ref Vector2 pos, GenericFont font, ref Color color, ref float scale, float depth) {
|
||||
var offset = new Vector2(0, (float) Math.Sin(token.Index + indexInToken + this.TimeIntoAnimation.TotalSeconds * this.modifier) * font.LineHeight * this.heightModifier * scale);
|
||||
pos += offset;
|
||||
// we return false since we still want regular drawing to occur, we just changed the position
|
||||
return false;
|
||||
|
|
|
@ -50,9 +50,6 @@ namespace MLEM.Formatting {
|
|||
this.RawIndex = rawIndex;
|
||||
this.Substring = substring;
|
||||
this.RawSubstring = rawSubstring;
|
||||
|
||||
foreach (var code in appliedCodes)
|
||||
code.Token = this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -96,7 +93,7 @@ namespace MLEM.Formatting {
|
|||
/// <param name="depth">The depth to draw at</param>
|
||||
public void DrawSelf(GameTime time, SpriteBatch batch, Vector2 pos, GenericFont font, Color color, float scale, float depth) {
|
||||
foreach (var code in this.AppliedCodes)
|
||||
code.DrawSelf(time, batch, pos, font, color, scale, depth);
|
||||
code.DrawSelf(time, batch, this, pos, font, color, scale, depth);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -114,7 +111,7 @@ namespace MLEM.Formatting {
|
|||
/// <param name="depth">The depth to draw at</param>
|
||||
public void DrawCharacter(GameTime time, SpriteBatch batch, char c, string cString, int indexInToken, Vector2 pos, GenericFont font, Color color, float scale, float depth) {
|
||||
foreach (var code in this.AppliedCodes) {
|
||||
if (code.DrawCharacter(time, batch, c, cString, indexInToken, ref pos, font, ref color, ref scale, depth))
|
||||
if (code.DrawCharacter(time, batch, c, cString, this, indexInToken, ref pos, font, ref color, ref scale, depth))
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.Xna.Framework;
|
||||
|
@ -7,6 +8,7 @@ using MLEM.Extensions;
|
|||
using MLEM.Font;
|
||||
using MLEM.Formatting.Codes;
|
||||
using MLEM.Misc;
|
||||
using static MLEM.Font.GenericFont;
|
||||
|
||||
namespace MLEM.Formatting {
|
||||
/// <summary>
|
||||
|
@ -43,15 +45,19 @@ namespace MLEM.Formatting {
|
|||
this.RawString = rawString;
|
||||
this.String = strg;
|
||||
this.Tokens = tokens;
|
||||
|
||||
// since a code can be present in multiple tokens, we use Distinct here
|
||||
this.AllCodes = tokens.SelectMany(t => t.AppliedCodes).Distinct().ToArray();
|
||||
// TODO this can probably be optimized by keeping track of a code's tokens while tokenizing
|
||||
foreach (var code in this.AllCodes)
|
||||
code.Tokens = new ReadOnlyCollection<Token>(this.Tokens.Where(t => t.AppliedCodes.Contains(code)).ToList());
|
||||
|
||||
this.RecalculateTokenData(font, alignment);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Splits this tokenized string, inserting newline characters if the width of the string is bigger than the maximum width.
|
||||
/// Note that a tokenized string can be re-split without losing any of its actual data, as this operation merely modifies the <see cref="DisplayString"/>.
|
||||
/// <seealso cref="GenericFont.SplitString"/>
|
||||
/// </summary>
|
||||
/// <param name="font">The font to use for width calculations</param>
|
||||
/// <param name="width">The maximum width, in display pixels based on the font and scale</param>
|
||||
|
@ -59,7 +65,7 @@ namespace MLEM.Formatting {
|
|||
/// <param name="alignment">The text alignment that should be used for width calculations</param>
|
||||
public void Split(GenericFont font, float width, float scale, TextAlignment alignment = TextAlignment.Left) {
|
||||
// a split string has the same character count as the input string but with newline characters added
|
||||
this.modifiedString = string.Join("\n", font.SplitStringSeparate(this.String, width, scale, i => this.GetFontForIndex(font, i)));
|
||||
this.modifiedString = string.Join("\n", font.SplitStringSeparate(new CharSource(this.String), width, scale, i => this.GetFontForIndex(font, i)));
|
||||
this.StoreModifiedSubstrings(font, alignment);
|
||||
}
|
||||
|
||||
|
@ -74,13 +80,13 @@ namespace MLEM.Formatting {
|
|||
/// <param name="ellipsis">The characters to add to the end of the string if it is too long</param>
|
||||
/// <param name="alignment">The text alignment that should be used for width calculations</param>
|
||||
public void Truncate(GenericFont font, float width, float scale, string ellipsis = "", TextAlignment alignment = TextAlignment.Left) {
|
||||
this.modifiedString = font.TruncateString(this.String, width, scale, false, ellipsis, i => this.GetFontForIndex(font, i));
|
||||
this.modifiedString = font.TruncateString(new CharSource(this.String), width, scale, false, ellipsis, i => this.GetFontForIndex(font, i)).ToString();
|
||||
this.StoreModifiedSubstrings(font, alignment);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="GenericFont.MeasureString(string,bool)"/>
|
||||
public Vector2 Measure(GenericFont font) {
|
||||
return font.MeasureString(this.DisplayString, false, i => this.GetFontForIndex(font, i));
|
||||
return font.MeasureString(new CharSource(this.DisplayString), false, i => this.GetFontForIndex(font, i));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -117,17 +123,20 @@ namespace MLEM.Formatting {
|
|||
var token = this.Tokens[t];
|
||||
var drawFont = token.GetFont(font);
|
||||
var drawColor = token.GetColor(color);
|
||||
for (var l = 0; l < token.SplitDisplayString.Length; l++) {
|
||||
var line = token.SplitDisplayString[l];
|
||||
for (var i = 0; i < line.Length; i++) {
|
||||
var c = line[i];
|
||||
if (l == 0 && i == 0)
|
||||
token.DrawSelf(time, batch, pos + innerOffset, drawFont, color, scale, depth);
|
||||
|
||||
var indexInToken = 0;
|
||||
for (var l = 0; l < token.SplitDisplayString.Length; l++) {
|
||||
foreach (var c in token.SplitDisplayString[l]) {
|
||||
var cString = c.ToCachedString();
|
||||
token.DrawCharacter(time, batch, c, cString, i, pos + innerOffset, drawFont, drawColor, scale, depth);
|
||||
|
||||
if (indexInToken == 0)
|
||||
token.DrawSelf(time, batch, pos + innerOffset, drawFont, color, scale, depth);
|
||||
token.DrawCharacter(time, batch, c, cString, indexInToken, pos + innerOffset, drawFont, drawColor, scale, depth);
|
||||
|
||||
innerOffset.X += drawFont.MeasureString(cString).X * scale;
|
||||
indexInToken++;
|
||||
}
|
||||
|
||||
// only split at a new line, not between tokens!
|
||||
if (l < token.SplitDisplayString.Length - 1) {
|
||||
innerOffset.X = token.InnerOffsets[l] * scale;
|
||||
|
|
|
@ -367,18 +367,38 @@ namespace MLEM.Graphics {
|
|||
|
||||
private Item Add(Texture2D texture, Vector2 pos, Vector2 offset, Vector2 size, float sin, float cos, Color color, Vector2 texTl, Vector2 texBr, float depth) {
|
||||
return this.Add(texture, depth,
|
||||
new VertexPositionColorTexture(new Vector3(pos.X + offset.X * cos - offset.Y * sin, pos.Y + offset.X * sin + offset.Y * cos, depth), color, texTl),
|
||||
new VertexPositionColorTexture(new Vector3(pos.X + (offset.X + size.X) * cos - offset.Y * sin, pos.Y + (offset.X + size.X) + offset.Y * cos, depth), color, new Vector2(texBr.X, texTl.Y)),
|
||||
new VertexPositionColorTexture(new Vector3(pos.X + offset.X * cos - (offset.Y + size.Y) * sin, pos.Y + offset.X * sin + (offset.Y + size.Y) * cos, depth), color, new Vector2(texTl.X, texBr.Y)),
|
||||
new VertexPositionColorTexture(new Vector3(pos.X + (offset.X + size.X) * cos - (offset.Y + size.Y) * sin, pos.Y + (offset.X + size.X) * sin + (offset.Y + size.Y) * cos, depth), color, texBr));
|
||||
new VertexPositionColorTexture(new Vector3(
|
||||
pos.X + offset.X * cos - offset.Y * sin,
|
||||
pos.Y + offset.X * sin + offset.Y * cos, depth),
|
||||
color, texTl),
|
||||
new VertexPositionColorTexture(new Vector3(
|
||||
pos.X + (offset.X + size.X) * cos - offset.Y * sin,
|
||||
pos.Y + (offset.X + size.X) * sin + offset.Y * cos, depth),
|
||||
color, new Vector2(texBr.X, texTl.Y)),
|
||||
new VertexPositionColorTexture(new Vector3(
|
||||
pos.X + offset.X * cos - (offset.Y + size.Y) * sin,
|
||||
pos.Y + offset.X * sin + (offset.Y + size.Y) * cos, depth),
|
||||
color, new Vector2(texTl.X, texBr.Y)),
|
||||
new VertexPositionColorTexture(new Vector3(
|
||||
pos.X + (offset.X + size.X) * cos - (offset.Y + size.Y) * sin,
|
||||
pos.Y + (offset.X + size.X) * sin + (offset.Y + size.Y) * cos, depth),
|
||||
color, texBr));
|
||||
}
|
||||
|
||||
private Item Add(Texture2D texture, Vector2 pos, Vector2 size, Color color, Vector2 texTl, Vector2 texBr, float depth) {
|
||||
return this.Add(texture, depth,
|
||||
new VertexPositionColorTexture(new Vector3(pos, depth), color, texTl),
|
||||
new VertexPositionColorTexture(new Vector3(pos.X + size.X, pos.Y, depth), color, new Vector2(texBr.X, texTl.Y)),
|
||||
new VertexPositionColorTexture(new Vector3(pos.X, pos.Y + size.Y, depth), color, new Vector2(texTl.X, texBr.Y)),
|
||||
new VertexPositionColorTexture(new Vector3(pos.X + size.X, pos.Y + size.Y, depth), color, texBr));
|
||||
new VertexPositionColorTexture(
|
||||
new Vector3(pos, depth),
|
||||
color, texTl),
|
||||
new VertexPositionColorTexture(
|
||||
new Vector3(pos.X + size.X, pos.Y, depth),
|
||||
color, new Vector2(texBr.X, texTl.Y)),
|
||||
new VertexPositionColorTexture(
|
||||
new Vector3(pos.X, pos.Y + size.Y, depth),
|
||||
color, new Vector2(texTl.X, texBr.Y)),
|
||||
new VertexPositionColorTexture(
|
||||
new Vector3(pos.X + size.X, pos.Y + size.Y, depth),
|
||||
color, texBr));
|
||||
}
|
||||
|
||||
private Item Add(Texture2D texture, float depth, VertexPositionColorTexture tl, VertexPositionColorTexture tr, VertexPositionColorTexture bl, VertexPositionColorTexture br) {
|
||||
|
|
45
MLEM/Input/GamepadExtensions.cs
Normal file
45
MLEM/Input/GamepadExtensions.cs
Normal file
|
@ -0,0 +1,45 @@
|
|||
using Microsoft.Xna.Framework;
|
||||
using Microsoft.Xna.Framework.Input;
|
||||
|
||||
namespace MLEM.Input {
|
||||
/// <summary>
|
||||
/// A set of extension methods for dealing with <see cref="GamePad"/>, <see cref="GamePadState"/> and <see cref="Buttons"/>.
|
||||
/// </summary>
|
||||
public static class GamepadExtensions {
|
||||
|
||||
/// <summary>
|
||||
/// Returns the given <see cref="Buttons"/>'s value as an analog value between 0 and 1, where 1 is fully down and 0 is not down at all.
|
||||
/// For non-analog buttons, like <see cref="Buttons.X"/> or <see cref="Buttons.Start"/>, only 0 and 1 will be returned and no inbetween values are possible.
|
||||
/// </summary>
|
||||
/// <param name="state">The gamepad state to query.</param>
|
||||
/// <param name="button">The button to query.</param>
|
||||
/// <returns>The button's state as an analog value.</returns>
|
||||
public static float GetAnalogValue(this GamePadState state, Buttons button) {
|
||||
switch (button) {
|
||||
case Buttons.LeftThumbstickDown:
|
||||
return -MathHelper.Clamp(state.ThumbSticks.Left.Y, -1, 0);
|
||||
case Buttons.LeftThumbstickUp:
|
||||
return MathHelper.Clamp(state.ThumbSticks.Left.Y, 0, 1);
|
||||
case Buttons.LeftThumbstickLeft:
|
||||
return -MathHelper.Clamp(state.ThumbSticks.Left.X, -1, 0);
|
||||
case Buttons.LeftThumbstickRight:
|
||||
return MathHelper.Clamp(state.ThumbSticks.Left.X, 0, 1);
|
||||
case Buttons.RightTrigger:
|
||||
return state.Triggers.Right;
|
||||
case Buttons.LeftTrigger:
|
||||
return state.Triggers.Left;
|
||||
case Buttons.RightThumbstickDown:
|
||||
return -MathHelper.Clamp(state.ThumbSticks.Right.Y, -1, 0);
|
||||
case Buttons.RightThumbstickUp:
|
||||
return MathHelper.Clamp(state.ThumbSticks.Right.Y, 0, 1);
|
||||
case Buttons.RightThumbstickLeft:
|
||||
return -MathHelper.Clamp(state.ThumbSticks.Right.X, -1, 0);
|
||||
case Buttons.RightThumbstickRight:
|
||||
return MathHelper.Clamp(state.ThumbSticks.Right.X, 0, 1);
|
||||
default:
|
||||
return state.IsButtonDown(button) ? 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -6,7 +6,7 @@ namespace MLEM.Input {
|
|||
/// <summary>
|
||||
/// A generic input represents any kind of input key.
|
||||
/// This includes <see cref="Keys"/> for keyboard keys, <see cref="MouseButton"/> for mouse buttons and <see cref="Buttons"/> for gamepad buttons.
|
||||
/// For creating and extracting inputs from a generic input, the implicit operators and <see cref="Type"/> can be used.
|
||||
/// For creating and extracting inputs from a generic input, the implicit operators and <see cref="Type"/> can additionally be used.
|
||||
/// Note that this type is serializable using <see cref="DataContractAttribute"/>.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
|
@ -20,6 +20,46 @@ namespace MLEM.Input {
|
|||
[DataMember]
|
||||
private readonly int value;
|
||||
|
||||
/// <summary>
|
||||
/// Returns this generic input's <see cref="Keys"/>.
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">If this generic input's <see cref="Type"/> is not <see cref="InputType.Keyboard"/> or <see cref="InputType.None"/>.</exception>
|
||||
public Keys Key {
|
||||
get {
|
||||
if (this.Type == InputType.None)
|
||||
return Keys.None;
|
||||
return this.Type == InputType.Keyboard ? (Keys) this.value : throw new InvalidOperationException();
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Returns this generic input's <see cref="MouseButton"/>.
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">If this generic input's <see cref="Type"/> is not <see cref="InputType.Mouse"/>.</exception>
|
||||
public MouseButton MouseButton => this.Type == InputType.Mouse ? (MouseButton) this.value : throw new InvalidOperationException();
|
||||
/// <summary>
|
||||
/// Returns this generic input's <see cref="Buttons"/>.
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">If this generic input's <see cref="Type"/> is not <see cref="InputType.Gamepad"/>.</exception>
|
||||
public Buttons Button => this.Type == InputType.Gamepad ? (Buttons) this.value : throw new InvalidOperationException();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new generic input from the given keyboard <see cref="Keys"/>.
|
||||
/// </summary>
|
||||
/// <param name="key">The key to convert.</param>
|
||||
public GenericInput(Keys key) : this(InputType.Keyboard, (int) key) {}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new generic input from the given <see cref="MouseButton"/>.
|
||||
/// </summary>
|
||||
/// <param name="button">The button to convert.</param>
|
||||
public GenericInput(MouseButton button) : this(InputType.Mouse, (int) button) {}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new generic input from the given gamepad <see cref="Buttons"/>.
|
||||
/// </summary>
|
||||
/// <param name="button">The button to convert.</param>
|
||||
public GenericInput(Buttons button) : this(InputType.Gamepad, (int) button) {}
|
||||
|
||||
private GenericInput(InputType type, int value) {
|
||||
this.Type = type;
|
||||
this.value = value;
|
||||
|
@ -83,10 +123,10 @@ namespace MLEM.Input {
|
|||
/// <summary>
|
||||
/// Converts a <see cref="Keys"/> to a generic input.
|
||||
/// </summary>
|
||||
/// <param name="keys">The keys to convert</param>
|
||||
/// <param name="key">The keys to convert</param>
|
||||
/// <returns>The resulting generic input</returns>
|
||||
public static implicit operator GenericInput(Keys keys) {
|
||||
return new GenericInput(InputType.Keyboard, (int) keys);
|
||||
public static implicit operator GenericInput(Keys key) {
|
||||
return new GenericInput(key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -95,16 +135,16 @@ namespace MLEM.Input {
|
|||
/// <param name="button">The button to convert</param>
|
||||
/// <returns>The resulting generic input</returns>
|
||||
public static implicit operator GenericInput(MouseButton button) {
|
||||
return new GenericInput(InputType.Mouse, (int) button);
|
||||
return new GenericInput(button);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a <see cref="Buttons"/> to a generic input.
|
||||
/// </summary>
|
||||
/// <param name="buttons">The buttons to convert</param>
|
||||
/// <param name="button">The buttons to convert</param>
|
||||
/// <returns>The resulting generic input</returns>
|
||||
public static implicit operator GenericInput(Buttons buttons) {
|
||||
return new GenericInput(InputType.Gamepad, (int) buttons);
|
||||
public static implicit operator GenericInput(Buttons button) {
|
||||
return new GenericInput(button);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -112,11 +152,9 @@ namespace MLEM.Input {
|
|||
/// </summary>
|
||||
/// <param name="input">The input to convert</param>
|
||||
/// <returns>The resulting keys</returns>
|
||||
/// <exception cref="ArgumentException">If the given generic input's <see cref="Type"/> is not <see cref="InputType.Keyboard"/> or <see cref="InputType.None"/></exception>
|
||||
/// <exception cref="InvalidOperationException">If the given generic input's <see cref="Type"/> is not <see cref="InputType.Keyboard"/> or <see cref="InputType.None"/></exception>
|
||||
public static implicit operator Keys(GenericInput input) {
|
||||
if (input.Type == InputType.None)
|
||||
return Keys.None;
|
||||
return input.Type == InputType.Keyboard ? (Keys) input.value : throw new ArgumentException();
|
||||
return input.Key;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -124,9 +162,9 @@ namespace MLEM.Input {
|
|||
/// </summary>
|
||||
/// <param name="input">The input to convert</param>
|
||||
/// <returns>The resulting button</returns>
|
||||
/// <exception cref="ArgumentException">If the given generic input's <see cref="Type"/> is not <see cref="InputType.Mouse"/></exception>
|
||||
/// <exception cref="InvalidOperationException">If the given generic input's <see cref="Type"/> is not <see cref="InputType.Mouse"/></exception>
|
||||
public static implicit operator MouseButton(GenericInput input) {
|
||||
return input.Type == InputType.Mouse ? (MouseButton) input.value : throw new ArgumentException();
|
||||
return input.MouseButton;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -134,9 +172,9 @@ namespace MLEM.Input {
|
|||
/// </summary>
|
||||
/// <param name="input">The input to convert</param>
|
||||
/// <returns>The resulting buttons</returns>
|
||||
/// <exception cref="ArgumentException">If the given generic input's <see cref="Type"/> is not <see cref="InputType.Gamepad"/></exception>
|
||||
/// <exception cref="InvalidOperationException">If the given generic input's <see cref="Type"/> is not <see cref="InputType.Gamepad"/></exception>
|
||||
public static implicit operator Buttons(GenericInput input) {
|
||||
return input.Type == InputType.Gamepad ? (Buttons) input.value : throw new ArgumentException();
|
||||
return input.Button;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using Microsoft.Xna.Framework;
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
using Microsoft.Xna.Framework.Input;
|
||||
using Microsoft.Xna.Framework.Input.Touch;
|
||||
using MLEM.Misc;
|
||||
|
@ -15,78 +16,27 @@ namespace MLEM.Input {
|
|||
public class InputHandler : GameComponent {
|
||||
|
||||
/// <summary>
|
||||
/// Contains the keyboard state from the last update call
|
||||
/// Contains all of the gestures that have finished during the last update call.
|
||||
/// To easily query these gestures, use <see cref="GetGesture"/> or <see cref="GetViewportGesture"/>.
|
||||
/// </summary>
|
||||
public KeyboardState LastKeyboardState { get; private set; }
|
||||
/// <summary>
|
||||
/// Contains the current keyboard state
|
||||
/// </summary>
|
||||
public KeyboardState KeyboardState { get; private set; }
|
||||
public readonly ReadOnlyCollection<GestureSample> Gestures;
|
||||
|
||||
/// <summary>
|
||||
/// Set this field to false to disable keyboard handling for this input handler.
|
||||
/// </summary>
|
||||
public bool HandleKeyboard;
|
||||
|
||||
/// <summary>
|
||||
/// Contains the mouse state from the last update call
|
||||
/// </summary>
|
||||
public MouseState LastMouseState { get; private set; }
|
||||
/// <summary>
|
||||
/// Contains the current mouse state
|
||||
/// </summary>
|
||||
public MouseState MouseState { get; private set; }
|
||||
/// <summary>
|
||||
/// Contains the current position of the mouse, extracted from <see cref="MouseState"/>
|
||||
/// </summary>
|
||||
public Point MousePosition => this.MouseState.Position;
|
||||
/// <summary>
|
||||
/// Contains the position of the mouse from the last update call, extracted from <see cref="LastMouseState"/>
|
||||
/// </summary>
|
||||
public Point LastMousePosition => this.LastMouseState.Position;
|
||||
/// <summary>
|
||||
/// Contains the current scroll wheel value, in increments of 120
|
||||
/// </summary>
|
||||
public int ScrollWheel => this.MouseState.ScrollWheelValue;
|
||||
/// <summary>
|
||||
/// Contains the scroll wheel value from the last update call, in increments of 120
|
||||
/// </summary>
|
||||
public int LastScrollWheel => this.LastMouseState.ScrollWheelValue;
|
||||
/// <summary>
|
||||
/// Set this field to false to disable mouse handling for this input handler.
|
||||
/// </summary>
|
||||
public bool HandleMouse;
|
||||
|
||||
private readonly GamePadState[] lastGamepads = new GamePadState[GamePad.MaximumGamePadCount];
|
||||
private readonly GamePadState[] gamepads = new GamePadState[GamePad.MaximumGamePadCount];
|
||||
/// <summary>
|
||||
/// Contains the amount of gamepads that are currently connected.
|
||||
/// This field is automatically updated in <see cref="Update()"/>
|
||||
/// </summary>
|
||||
public int ConnectedGamepads { get; private set; }
|
||||
/// <summary>
|
||||
/// Set this field to false to disable keyboard handling for this input handler.
|
||||
/// </summary>
|
||||
public bool HandleGamepads;
|
||||
|
||||
/// <summary>
|
||||
/// Contains the touch state from the last update call
|
||||
/// </summary>
|
||||
public TouchCollection LastTouchState { get; private set; }
|
||||
/// <summary>
|
||||
/// Contains the current touch state
|
||||
/// </summary>
|
||||
public TouchCollection TouchState { get; private set; }
|
||||
/// <summary>
|
||||
/// Contains all of the gestures that have finished during the last update call.
|
||||
/// To easily query these gestures, use <see cref="GetGesture"/>
|
||||
/// </summary>
|
||||
public readonly ReadOnlyCollection<GestureSample> Gestures;
|
||||
private readonly List<GestureSample> gestures = new List<GestureSample>();
|
||||
/// <summary>
|
||||
/// Set this field to false to disable touch handling for this input handler.
|
||||
/// </summary>
|
||||
public bool HandleTouch;
|
||||
|
||||
/// <summary>
|
||||
/// This is the amount of time that has to pass before the first keyboard repeat event is triggered.
|
||||
/// <seealso cref="KeyRepeatRate"/>
|
||||
|
@ -97,41 +47,106 @@ namespace MLEM.Input {
|
|||
/// <seealso cref="KeyRepeatDelay"/>
|
||||
/// </summary>
|
||||
public TimeSpan KeyRepeatRate = TimeSpan.FromSeconds(0.05);
|
||||
|
||||
/// <summary>
|
||||
/// Set this field to false to disable keyboard repeat event handling.
|
||||
/// </summary>
|
||||
public bool HandleKeyboardRepeats = true;
|
||||
private DateTime heldKeyStart;
|
||||
private DateTime lastKeyRepeat;
|
||||
private bool triggerKeyRepeat;
|
||||
private Keys heldKey;
|
||||
|
||||
/// <summary>
|
||||
/// Set this field to false to disable gamepad repeat event handling.
|
||||
/// </summary>
|
||||
public bool HandleGamepadRepeats = true;
|
||||
private readonly DateTime[] heldGamepadButtonStarts = new DateTime[GamePad.MaximumGamePadCount];
|
||||
private readonly DateTime[] lastGamepadButtonRepeats = new DateTime[GamePad.MaximumGamePadCount];
|
||||
private readonly bool[] triggerGamepadButtonRepeat = new bool[GamePad.MaximumGamePadCount];
|
||||
private readonly Buttons?[] heldGamepadButtons = new Buttons?[GamePad.MaximumGamePadCount];
|
||||
/// <summary>
|
||||
/// This field represents the deadzone that gamepad <see cref="Buttons"/> have when input is queried for them using this input handler.
|
||||
/// A deadzone is the percentage (between 0 and 1) that an analog value has to exceed for it to be considered down (<see cref="IsGamepadButtonDown"/>) or pressed (<see cref="IsGamepadButtonPressed"/>).
|
||||
/// Querying of analog values is done using <see cref="GamepadExtensions.GetAnalogValue"/>.
|
||||
/// </summary>
|
||||
public float GamepadButtonDeadzone;
|
||||
|
||||
/// <summary>
|
||||
/// An array of all <see cref="Keys"/>, <see cref="Buttons"/> and <see cref="MouseButton"/> values that are currently down.
|
||||
/// Note that this value only gets set if <see cref="StoreAllActiveInputs"/> is true.
|
||||
/// Additionally, <see cref="TryGetDownTime"/> or <see cref="GetDownTime"/> can be used to determine the amount of time that a given input has been down for.
|
||||
/// </summary>
|
||||
public GenericInput[] InputsDown { get; private set; } = Array.Empty<GenericInput>();
|
||||
/// <summary>
|
||||
/// An array of all <see cref="Keys"/>, <see cref="Buttons"/> and <see cref="MouseButton"/> that are currently considered pressed.
|
||||
/// An input is considered pressed if it was up in the last update, and is up in the current one.
|
||||
/// Note that this value only gets set if <see cref="StoreAllActiveInputs"/> is true.
|
||||
/// </summary>
|
||||
public GenericInput[] InputsPressed { get; private set; } = Array.Empty<GenericInput>();
|
||||
private readonly List<GenericInput> inputsDownAccum = new List<GenericInput>();
|
||||
/// <summary>
|
||||
/// Set this field to false to enable <see cref="InputsDown"/> and <see cref="InputsPressed"/> being calculated.
|
||||
/// Contains the touch state from the last update call
|
||||
/// </summary>
|
||||
public bool StoreAllActiveInputs;
|
||||
public TouchCollection LastTouchState { get; private set; }
|
||||
/// <summary>
|
||||
/// Contains the current touch state
|
||||
/// </summary>
|
||||
public TouchCollection TouchState { get; private set; }
|
||||
/// <summary>
|
||||
/// Contains the <see cref="LastTouchState"/>, but with the <see cref="GraphicsDevice.Viewport"/> taken into account.
|
||||
/// </summary>
|
||||
public IList<TouchLocation> LastViewportTouchState { get; private set; }
|
||||
/// <summary>
|
||||
/// Contains the <see cref="TouchState"/>, but with the <see cref="GraphicsDevice.Viewport"/> taken into account.
|
||||
/// </summary>
|
||||
public IList<TouchLocation> ViewportTouchState { get; private set; }
|
||||
/// <summary>
|
||||
/// Contains the amount of gamepads that are currently connected.
|
||||
/// This field is automatically updated in <see cref="Update()"/>
|
||||
/// </summary>
|
||||
public int ConnectedGamepads { get; private set; }
|
||||
/// <summary>
|
||||
/// Contains the mouse state from the last update call
|
||||
/// </summary>
|
||||
public MouseState LastMouseState { get; private set; }
|
||||
/// <summary>
|
||||
/// Contains the current mouse state
|
||||
/// </summary>
|
||||
public MouseState MouseState { get; private set; }
|
||||
/// <summary>
|
||||
/// Contains the position of the mouse from the last update call, extracted from <see cref="LastMouseState"/>
|
||||
/// </summary>
|
||||
public Point LastMousePosition => this.LastMouseState.Position;
|
||||
/// <summary>
|
||||
/// Contains the <see cref="LastMousePosition"/>, but with the <see cref="GraphicsDevice.Viewport"/> taken into account.
|
||||
/// </summary>
|
||||
public Point LastViewportMousePosition => this.LastMousePosition + this.ViewportOffset;
|
||||
/// <summary>
|
||||
/// Contains the current position of the mouse, extracted from <see cref="MouseState"/>
|
||||
/// </summary>
|
||||
public Point MousePosition => this.MouseState.Position;
|
||||
/// <summary>
|
||||
/// Contains the <see cref="MousePosition"/>, but with the <see cref="GraphicsDevice.Viewport"/> taken into account.
|
||||
/// </summary>
|
||||
public Point ViewportMousePosition => this.MousePosition + this.ViewportOffset;
|
||||
/// <summary>
|
||||
/// Contains the current scroll wheel value, in increments of 120
|
||||
/// </summary>
|
||||
public int ScrollWheel => this.MouseState.ScrollWheelValue;
|
||||
/// <summary>
|
||||
/// Contains the scroll wheel value from the last update call, in increments of 120
|
||||
/// </summary>
|
||||
public int LastScrollWheel => this.LastMouseState.ScrollWheelValue;
|
||||
/// <summary>
|
||||
/// Contains the keyboard state from the last update call
|
||||
/// </summary>
|
||||
public KeyboardState LastKeyboardState { get; private set; }
|
||||
/// <summary>
|
||||
/// Contains the current keyboard state
|
||||
/// </summary>
|
||||
public KeyboardState KeyboardState { get; private set; }
|
||||
|
||||
private readonly GamePadState[] lastGamepads = new GamePadState[GamePad.MaximumGamePadCount];
|
||||
private readonly GamePadState[] gamepads = new GamePadState[GamePad.MaximumGamePadCount];
|
||||
private readonly DateTime[] lastGamepadButtonRepeats = new DateTime[GamePad.MaximumGamePadCount];
|
||||
private readonly bool[] triggerGamepadButtonRepeat = new bool[GamePad.MaximumGamePadCount];
|
||||
private readonly Buttons?[] heldGamepadButtons = new Buttons?[GamePad.MaximumGamePadCount];
|
||||
private readonly List<GestureSample> gestures = new List<GestureSample>();
|
||||
|
||||
private Point ViewportOffset => new Point(-this.Game.GraphicsDevice.Viewport.X, -this.Game.GraphicsDevice.Viewport.Y);
|
||||
private Dictionary<(GenericInput, int), DateTime> inputsDownAccum = new Dictionary<(GenericInput, int), DateTime>();
|
||||
private Dictionary<(GenericInput, int), DateTime> inputsDown = new Dictionary<(GenericInput, int), DateTime>();
|
||||
private DateTime lastKeyRepeat;
|
||||
private bool triggerKeyRepeat;
|
||||
private Keys heldKey;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new input handler with optional initial values.
|
||||
|
@ -141,13 +156,11 @@ namespace MLEM.Input {
|
|||
/// <param name="handleMouse">If mouse input should be handled</param>
|
||||
/// <param name="handleGamepads">If gamepad input should be handled</param>
|
||||
/// <param name="handleTouch">If touch input should be handled</param>
|
||||
/// <param name="storeAllActiveInputs">Whether all inputs that are currently down and pressed should be calculated each update</param>
|
||||
public InputHandler(Game game, bool handleKeyboard = true, bool handleMouse = true, bool handleGamepads = true, bool handleTouch = true, bool storeAllActiveInputs = true) : base(game) {
|
||||
public InputHandler(Game game, bool handleKeyboard = true, bool handleMouse = true, bool handleGamepads = true, bool handleTouch = true) : base(game) {
|
||||
this.HandleKeyboard = handleKeyboard;
|
||||
this.HandleMouse = handleMouse;
|
||||
this.HandleGamepads = handleGamepads;
|
||||
this.HandleTouch = handleTouch;
|
||||
this.StoreAllActiveInputs = storeAllActiveInputs;
|
||||
this.Gestures = this.gestures.AsReadOnly();
|
||||
}
|
||||
|
||||
|
@ -156,35 +169,22 @@ namespace MLEM.Input {
|
|||
/// Call this in your <see cref="Game.Update"/> method.
|
||||
/// </summary>
|
||||
public void Update() {
|
||||
var now = DateTime.UtcNow;
|
||||
var active = this.Game.IsActive;
|
||||
if (this.HandleKeyboard) {
|
||||
this.LastKeyboardState = this.KeyboardState;
|
||||
this.KeyboardState = active ? Keyboard.GetState() : default;
|
||||
var pressedKeys = this.KeyboardState.GetPressedKeys();
|
||||
if (this.StoreAllActiveInputs) {
|
||||
foreach (var pressed in pressedKeys)
|
||||
this.inputsDownAccum.Add(pressed);
|
||||
}
|
||||
this.AccumulateDown(pressed, -1);
|
||||
|
||||
if (this.HandleKeyboardRepeats) {
|
||||
this.triggerKeyRepeat = false;
|
||||
if (this.heldKey == Keys.None) {
|
||||
// if we're not repeating a key, set the first key being held to the repeat key
|
||||
// note that modifier keys don't count as that wouldn't really make sense
|
||||
var key = pressedKeys.FirstOrDefault(k => !k.IsModifier());
|
||||
if (key != Keys.None) {
|
||||
this.heldKey = key;
|
||||
this.heldKeyStart = DateTime.UtcNow;
|
||||
}
|
||||
} else {
|
||||
// if the repeating key isn't being held anymore, reset
|
||||
if (!this.IsKeyDown(this.heldKey)) {
|
||||
this.heldKey = Keys.None;
|
||||
} else {
|
||||
var now = DateTime.UtcNow;
|
||||
var holdTime = now - this.heldKeyStart;
|
||||
// the key that started being held most recently should be the one being repeated
|
||||
this.heldKey = pressedKeys.OrderBy(k => this.GetDownTime(k)).FirstOrDefault();
|
||||
if (this.TryGetDownTime(this.heldKey, out var heldTime)) {
|
||||
// if we've been holding the key longer than the initial delay...
|
||||
if (holdTime >= this.KeyRepeatDelay) {
|
||||
if (heldTime >= this.KeyRepeatDelay) {
|
||||
var diff = now - this.lastKeyRepeat;
|
||||
// and we've been holding it for longer than a repeat...
|
||||
if (diff >= this.KeyRepeatRate) {
|
||||
|
@ -196,18 +196,15 @@ namespace MLEM.Input {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.HandleMouse) {
|
||||
this.LastMouseState = this.MouseState;
|
||||
var state = Mouse.GetState();
|
||||
if (active && this.Game.GraphicsDevice.Viewport.Bounds.Contains(state.Position)) {
|
||||
this.MouseState = state;
|
||||
if (this.StoreAllActiveInputs) {
|
||||
foreach (var button in MouseExtensions.MouseButtons) {
|
||||
if (state.GetState(button) == ButtonState.Pressed)
|
||||
this.inputsDownAccum.Add(button);
|
||||
}
|
||||
this.AccumulateDown(button, -1);
|
||||
}
|
||||
} else {
|
||||
// mouse position and scroll wheel value should be preserved when the mouse is out of bounds
|
||||
|
@ -219,43 +216,29 @@ namespace MLEM.Input {
|
|||
this.ConnectedGamepads = GamePad.MaximumGamePadCount;
|
||||
for (var i = 0; i < GamePad.MaximumGamePadCount; i++) {
|
||||
this.lastGamepads[i] = this.gamepads[i];
|
||||
var state = GamePadState.Default;
|
||||
this.gamepads[i] = GamePadState.Default;
|
||||
if (GamePad.GetCapabilities(i).IsConnected) {
|
||||
if (active) {
|
||||
state = GamePad.GetState(i);
|
||||
if (this.StoreAllActiveInputs) {
|
||||
this.gamepads[i] = GamePad.GetState(i);
|
||||
foreach (var button in EnumHelper.Buttons) {
|
||||
if (state.IsButtonDown(button))
|
||||
this.inputsDownAccum.Add(button);
|
||||
if (this.IsGamepadButtonDown(button, i))
|
||||
this.AccumulateDown(button, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (this.ConnectedGamepads > i)
|
||||
} else if (this.ConnectedGamepads > i) {
|
||||
this.ConnectedGamepads = i;
|
||||
}
|
||||
this.gamepads[i] = state;
|
||||
}
|
||||
|
||||
if (this.HandleGamepadRepeats) {
|
||||
for (var i = 0; i < this.ConnectedGamepads; i++) {
|
||||
this.triggerGamepadButtonRepeat[i] = false;
|
||||
|
||||
if (!this.heldGamepadButtons[i].HasValue) {
|
||||
foreach (var b in EnumHelper.Buttons) {
|
||||
if (this.IsGamepadButtonDown(b, i)) {
|
||||
this.heldGamepadButtons[i] = b;
|
||||
this.heldGamepadButtonStarts[i] = DateTime.UtcNow;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!this.IsGamepadButtonDown(this.heldGamepadButtons[i].Value, i)) {
|
||||
this.heldGamepadButtons[i] = null;
|
||||
} else {
|
||||
var now = DateTime.UtcNow;
|
||||
var holdTime = now - this.heldGamepadButtonStarts[i];
|
||||
if (holdTime >= this.KeyRepeatDelay) {
|
||||
this.heldGamepadButtons[i] = EnumHelper.Buttons
|
||||
.Where(b => this.IsGamepadButtonDown(b, i))
|
||||
.OrderBy(b => this.GetDownTime(b, i))
|
||||
.Cast<Buttons?>().FirstOrDefault();
|
||||
if (this.heldGamepadButtons[i].HasValue && this.TryGetDownTime(this.heldGamepadButtons[i].Value, out var heldTime, i)) {
|
||||
if (heldTime >= this.KeyRepeatDelay) {
|
||||
var diff = now - this.lastGamepadButtonRepeats[i];
|
||||
if (diff >= this.KeyRepeatRate) {
|
||||
this.lastGamepadButtonRepeats[i] = now;
|
||||
|
@ -266,28 +249,39 @@ namespace MLEM.Input {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.HandleTouch) {
|
||||
this.LastTouchState = this.TouchState;
|
||||
this.LastViewportTouchState = this.ViewportTouchState;
|
||||
|
||||
this.TouchState = active ? TouchPanel.GetState() : default;
|
||||
if (this.TouchState.Count > 0 && this.ViewportOffset != Point.Zero) {
|
||||
this.ViewportTouchState = new List<TouchLocation>();
|
||||
foreach (var touch in this.TouchState) {
|
||||
touch.TryGetPreviousLocation(out var previous);
|
||||
this.ViewportTouchState.Add(new TouchLocation(touch.Id, touch.State, touch.Position + this.ViewportOffset.ToVector2(), previous.State, previous.Position + this.ViewportOffset.ToVector2()));
|
||||
}
|
||||
} else {
|
||||
this.ViewportTouchState = this.TouchState;
|
||||
}
|
||||
|
||||
this.gestures.Clear();
|
||||
while (active && TouchPanel.IsGestureAvailable)
|
||||
this.gestures.Add(TouchPanel.ReadGesture());
|
||||
}
|
||||
|
||||
if (this.StoreAllActiveInputs) {
|
||||
if (this.inputsDownAccum.Count <= 0) {
|
||||
this.InputsPressed = Array.Empty<GenericInput>();
|
||||
this.InputsDown = Array.Empty<GenericInput>();
|
||||
this.inputsDown.Clear();
|
||||
} else {
|
||||
this.InputsPressed = this.inputsDownAccum.Where(i => !this.InputsDown.Contains(i)).ToArray();
|
||||
this.InputsDown = this.inputsDownAccum.ToArray();
|
||||
this.InputsPressed = this.inputsDownAccum.Keys.Where(kv => this.IsPressed(kv.Item1, kv.Item2)).Select(kv => kv.Item1).ToArray();
|
||||
this.InputsDown = this.inputsDownAccum.Keys.Select(kv => kv.Item1).ToArray();
|
||||
// swapping these collections means that we don't have to keep moving entries between them
|
||||
(this.inputsDown, this.inputsDownAccum) = (this.inputsDownAccum, this.inputsDown);
|
||||
this.inputsDownAccum.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Update()"/>
|
||||
public override void Update(GameTime gameTime) {
|
||||
|
@ -419,45 +413,49 @@ namespace MLEM.Input {
|
|||
/// <inheritdoc cref="GamePadState.IsButtonDown"/>
|
||||
public bool IsGamepadButtonDown(Buttons button, int index = -1) {
|
||||
if (index < 0) {
|
||||
for (var i = 0; i < this.ConnectedGamepads; i++)
|
||||
if (this.GetGamepadState(i).IsButtonDown(button))
|
||||
for (var i = 0; i < this.ConnectedGamepads; i++) {
|
||||
if (this.GetGamepadState(i).GetAnalogValue(button) > this.GamepadButtonDeadzone)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return this.GetGamepadState(index).IsButtonDown(button);
|
||||
return this.GetGamepadState(index).GetAnalogValue(button) > this.GamepadButtonDeadzone;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="GamePadState.IsButtonUp"/>
|
||||
public bool IsGamepadButtonUp(Buttons button, int index = -1) {
|
||||
if (index < 0) {
|
||||
for (var i = 0; i < this.ConnectedGamepads; i++)
|
||||
if (this.GetGamepadState(i).IsButtonUp(button))
|
||||
for (var i = 0; i < this.ConnectedGamepads; i++) {
|
||||
if (this.GetGamepadState(i).GetAnalogValue(button) <= this.GamepadButtonDeadzone)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return this.GetGamepadState(index).IsButtonUp(button);
|
||||
return this.GetGamepadState(index).GetAnalogValue(button) <= this.GamepadButtonDeadzone;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="GamePadState.IsButtonDown"/>
|
||||
public bool WasGamepadButtonDown(Buttons button, int index = -1) {
|
||||
if (index < 0) {
|
||||
for (var i = 0; i < this.ConnectedGamepads; i++)
|
||||
if (this.GetLastGamepadState(i).IsButtonDown(button))
|
||||
for (var i = 0; i < this.ConnectedGamepads; i++) {
|
||||
if (this.GetLastGamepadState(i).GetAnalogValue(button) > this.GamepadButtonDeadzone)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return this.GetLastGamepadState(index).IsButtonDown(button);
|
||||
return this.GetLastGamepadState(index).GetAnalogValue(button) > this.GamepadButtonDeadzone;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="GamePadState.IsButtonUp"/>
|
||||
public bool WasGamepadButtonUp(Buttons button, int index = -1) {
|
||||
if (index < 0) {
|
||||
for (var i = 0; i < this.ConnectedGamepads; i++)
|
||||
if (this.GetLastGamepadState(i).IsButtonUp(button))
|
||||
for (var i = 0; i < this.ConnectedGamepads; i++) {
|
||||
if (this.GetLastGamepadState(i).GetAnalogValue(button) <= this.GamepadButtonDeadzone)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return this.GetLastGamepadState(index).IsButtonUp(button);
|
||||
return this.GetLastGamepadState(index).GetAnalogValue(button) <= this.GamepadButtonDeadzone;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -471,9 +469,10 @@ namespace MLEM.Input {
|
|||
public bool IsGamepadButtonPressed(Buttons button, int index = -1) {
|
||||
if (this.HandleGamepadRepeats) {
|
||||
if (index < 0) {
|
||||
for (var i = 0; i < this.ConnectedGamepads; i++)
|
||||
for (var i = 0; i < this.ConnectedGamepads; i++) {
|
||||
if (this.heldGamepadButtons[i] == button && this.triggerGamepadButtonRepeat[i])
|
||||
return true;
|
||||
}
|
||||
} else if (this.heldGamepadButtons[index] == button && this.triggerGamepadButtonRepeat[index]) {
|
||||
return true;
|
||||
}
|
||||
|
@ -510,6 +509,22 @@ namespace MLEM.Input {
|
|||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queries for a gesture of the given type that finished during the current update call.
|
||||
/// Unlike <see cref="GetGesture"/>, the return value of this method takes the <see cref="GraphicsDevice.Viewport"/> into account.
|
||||
/// </summary>
|
||||
/// <param name="type">The type of gesture to query for</param>
|
||||
/// <param name="sample">The resulting gesture sample with the <see cref="GraphicsDevice.Viewport"/> taken into account, or default if there isn't one</param>
|
||||
/// <returns>True if a gesture of the type was found, otherwise false</returns>
|
||||
public bool GetViewportGesture(GestureType type, out GestureSample sample) {
|
||||
if (this.GetGesture(type, out var original)) {
|
||||
sample = new GestureSample(original.GestureType, original.Timestamp, original.Position + this.ViewportOffset.ToVector2(), original.Position2 + this.ViewportOffset.ToVector2(), original.Delta, original.Delta2);
|
||||
return true;
|
||||
}
|
||||
sample = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if a given control of any kind is down.
|
||||
/// This is a helper function that can be passed a <see cref="Keys"/>, <see cref="Buttons"/> or <see cref="MouseButton"/>.
|
||||
|
@ -600,6 +615,38 @@ namespace MLEM.Input {
|
|||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to retrieve the amount of time that a given <see cref="GenericInput"/> has been held down for.
|
||||
/// If the input is currently down, this method returns true and the amount of time that it has been down for is stored in <paramref name="downTime"/>.
|
||||
/// </summary>
|
||||
/// <param name="input">The input whose down time to query.</param>
|
||||
/// <param name="downTime">The resulting down time, or <see cref="TimeSpan.Zero"/> if the input is not being held.</param>
|
||||
/// <param name="index">The index of the gamepad to query (if applicable), or -1 for any gamepad.</param>
|
||||
/// <returns>Whether the input is currently being held.</returns>
|
||||
public bool TryGetDownTime(GenericInput input, out TimeSpan downTime, int index = -1) {
|
||||
if (this.inputsDown.TryGetValue((input, index), out var start)) {
|
||||
downTime = DateTime.UtcNow - start;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the amount of time that a given <see cref="GenericInput"/> has been held down for.
|
||||
/// If this input isn't currently own, this method returns <see cref="TimeSpan.Zero"/>.
|
||||
/// </summary>
|
||||
/// <param name="input">The input whose down time to query.</param>
|
||||
/// <param name="index">The index of the gamepad to query (if applicable), or -1 for any gamepad.</param>
|
||||
/// <returns>The resulting down time, or <see cref="TimeSpan.Zero"/> if the input is not being held.</returns>
|
||||
public TimeSpan GetDownTime(GenericInput input, int index = -1) {
|
||||
this.TryGetDownTime(input, out var time, index);
|
||||
return time;
|
||||
}
|
||||
|
||||
private void AccumulateDown(GenericInput input, int index) {
|
||||
this.inputsDownAccum.Add((input, index), this.inputsDown.TryGetValue((input, index), out var start) ? start : DateTime.UtcNow);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper function to enable gestures for a <see cref="TouchPanel"/> easily.
|
||||
/// Note that, if other gestures were previously enabled, they will not get overridden.
|
||||
|
|
|
@ -32,8 +32,7 @@ namespace MLEM.Input {
|
|||
/// <summary>
|
||||
/// Creates a new keybind with no default combinations
|
||||
/// </summary>
|
||||
public Keybind() {
|
||||
}
|
||||
public Keybind() {}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new key combination to this keybind that can optionally be pressed for the keybind to trigger.
|
||||
|
@ -48,7 +47,28 @@ namespace MLEM.Input {
|
|||
|
||||
/// <inheritdoc cref="Add(MLEM.Input.GenericInput,MLEM.Input.GenericInput[])"/>
|
||||
public Keybind Add(GenericInput key, ModifierKey modifier) {
|
||||
return this.Add(key, modifier.GetKeys().Select(m => (GenericInput) m).ToArray());
|
||||
foreach (var mod in modifier.GetKeys())
|
||||
this.Add(key, mod);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inserts a new key combination into the given <paramref name="index"/> of this keybind's combinations that can optionally be pressed for the keybind to trigger.
|
||||
/// </summary>
|
||||
/// <param name="index">The index to insert this combination into.</param>
|
||||
/// <param name="key">The key to be pressed.</param>
|
||||
/// <param name="modifiers">The modifier keys that have to be held down.</param>
|
||||
/// <returns>This keybind, for chaining.</returns>
|
||||
public Keybind Insert(int index, GenericInput key, params GenericInput[] modifiers) {
|
||||
this.combinations = this.combinations.Take(index).Append(new Combination(key, modifiers)).Concat(this.combinations.Skip(index)).ToArray();
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Insert(int,MLEM.Input.GenericInput,MLEM.Input.GenericInput[])"/>
|
||||
public Keybind Insert(int index, GenericInput key, ModifierKey modifier) {
|
||||
foreach (var mod in modifier.GetKeys().Reverse())
|
||||
this.Insert(index, key, mod);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -135,6 +155,22 @@ namespace MLEM.Input {
|
|||
yield return combination;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to retrieve the combination at the given <paramref name="index"/> within this keybind.
|
||||
/// </summary>
|
||||
/// <param name="index">The index of the combination to retrieve.</param>
|
||||
/// <param name="combination">The combination, or default if this method returns false.</param>
|
||||
/// <returns>Whether the combination could be successfully retrieved or the index was out of bounds of this keybind's combination collection.</returns>
|
||||
public bool TryGetCombination(int index, out Combination combination) {
|
||||
if (index >= 0 && index < this.combinations.Length) {
|
||||
combination = this.combinations[index];
|
||||
return true;
|
||||
} else {
|
||||
combination = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts this keybind into an easily human-readable string.
|
||||
/// When using <see cref="ToString()"/>, this method is used with <paramref name="joiner"/> set to ", ".
|
||||
|
|
|
@ -140,8 +140,7 @@ namespace MLEM.Misc {
|
|||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void AddTextInputListener(GameWindow window, TextInputCallback callback) {
|
||||
}
|
||||
public override void AddTextInputListener(GameWindow window, TextInputCallback callback) {}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OpenLinkOrFile(string link) {
|
||||
|
@ -172,12 +171,10 @@ namespace MLEM.Misc {
|
|||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void AddTextInputListener(GameWindow window, TextInputCallback callback) {
|
||||
}
|
||||
public override void AddTextInputListener(GameWindow window, TextInputCallback callback) {}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OpenLinkOrFile(string link) {
|
||||
}
|
||||
public override void OpenLinkOrFile(string link) {}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -62,27 +62,21 @@ namespace MLEM.Misc {
|
|||
/// Creates a new padding with the specified value, which will be applied to each edge.
|
||||
/// </summary>
|
||||
/// <param name="value">The padding to apply to each edge</param>
|
||||
public Padding(float value) :
|
||||
this(value, value) {
|
||||
}
|
||||
public Padding(float value) : this(value, value) {}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new padding with the specified x and y values, applying them to both edges.
|
||||
/// </summary>
|
||||
/// <param name="x">The x padding, which will turn into the left and right padding</param>
|
||||
/// <param name="y">The y padding, which till turn into the top and bottom padding</param>
|
||||
public Padding(float x, float y) :
|
||||
this(x, x, y, y) {
|
||||
}
|
||||
public Padding(float x, float y) : this(x, x, y, y) {}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new padding from an existing padding, modifying it by growing or shrinking it.
|
||||
/// </summary>
|
||||
/// <param name="padding">The padding whose initial values to use</param>
|
||||
/// <param name="growth">The amount to grow each border by. Negative values will shrink the padding.</param>
|
||||
public Padding(Padding padding, float growth) :
|
||||
this(padding.Left + growth, padding.Right + growth, padding.Top + growth, padding.Bottom + growth) {
|
||||
}
|
||||
public Padding(Padding padding, float growth) : this(padding.Left + growth, padding.Right + growth, padding.Top + growth, padding.Bottom + growth) {}
|
||||
|
||||
/// <summary>
|
||||
/// Implicitly creates a padding from the given two-dimensional vector.
|
||||
|
|
|
@ -105,49 +105,6 @@ namespace MLEM.Misc {
|
|||
this.Height = size.Y;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new rectangle based on two corners that form a bounding box.
|
||||
/// The resulting rectangle will encompass both corners as well as all of the space between them.
|
||||
/// </summary>
|
||||
/// <param name="corner1">The first corner to use</param>
|
||||
/// <param name="corner2">The second corner to use</param>
|
||||
/// <returns></returns>
|
||||
public static RectangleF FromCorners(Vector2 corner1, Vector2 corner2) {
|
||||
var minX = Math.Min(corner1.X, corner2.X);
|
||||
var minY = Math.Min(corner1.Y, corner2.Y);
|
||||
var maxX = Math.Max(corner1.X, corner2.X);
|
||||
var maxY = Math.Max(corner1.Y, corner2.Y);
|
||||
return new RectangleF(minX, minY, maxX - minX, maxY - minY);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a float-based rectangle to an int-based rectangle, flooring each value in the process.
|
||||
/// </summary>
|
||||
/// <param name="rect">The rectangle to convert</param>
|
||||
/// <returns>The resulting rectangle</returns>
|
||||
public static explicit operator Rectangle(RectangleF rect) {
|
||||
return new Rectangle(rect.X.Floor(), rect.Y.Floor(), rect.Width.Floor(), rect.Height.Floor());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts an int-based rectangle to a float-based rectangle.
|
||||
/// </summary>
|
||||
/// <param name="rect">The rectangle to convert</param>
|
||||
/// <returns>The resulting rectangle</returns>
|
||||
public static explicit operator RectangleF(Rectangle rect) {
|
||||
return new RectangleF(rect.X, rect.Y, rect.Width, rect.Height);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Equals(RectangleF)"/>
|
||||
public static bool operator ==(RectangleF a, RectangleF b) {
|
||||
return a.X == b.X && a.Y == b.Y && a.Width == b.Width && a.Height == b.Height;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Equals(RectangleF)"/>
|
||||
public static bool operator !=(RectangleF a, RectangleF b) {
|
||||
return !(a == b);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Rectangle.Contains(float, float)"/>
|
||||
public bool Contains(float x, float y) {
|
||||
return this.X <= x && x < this.X + this.Width && this.Y <= y && y < this.Y + this.Height;
|
||||
|
@ -196,6 +153,92 @@ namespace MLEM.Misc {
|
|||
return value.Left < this.Right && this.Left < value.Right && value.Top < this.Bottom && this.Top < value.Bottom;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Rectangle.Offset(float, float)"/>
|
||||
public void Offset(float offsetX, float offsetY) {
|
||||
this.X += offsetX;
|
||||
this.Y += offsetY;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Rectangle.Offset(Vector2)"/>
|
||||
public void Offset(Vector2 amount) {
|
||||
this.X += amount.X;
|
||||
this.Y += amount.Y;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the suqared distance between this rectangle and the <paramref name="value"/>.
|
||||
/// The returned value is the smallest squared distance between any two edges or corners of the two rectangles.
|
||||
/// </summary>
|
||||
/// <param name="value">The rectangle to calculate the squared distance to.</param>
|
||||
/// <returns>The squared distance between the two rectangles.</returns>
|
||||
public float DistanceSquared(RectangleF value) {
|
||||
// we calculate the distance based on the quadrants that the other rectangle is in using 8 cases:
|
||||
// 1 7 4
|
||||
// 3 T 6
|
||||
// 2 8 5
|
||||
var valueIsAbove = value.Bottom < this.Top;
|
||||
var valueIsBelow = value.Top > this.Bottom;
|
||||
if (value.Right < this.Left) {
|
||||
if (valueIsAbove) // 1
|
||||
return Vector2.DistanceSquared(new Vector2(value.Right, value.Bottom), new Vector2(this.Left, this.Top));
|
||||
if (valueIsBelow) // 2
|
||||
return Vector2.DistanceSquared(new Vector2(value.Right, value.Top), new Vector2(this.Left, this.Bottom));
|
||||
return (this.Left - value.Right) * (this.Left - value.Right); // 3
|
||||
} else if (value.Left > this.Right) {
|
||||
if (valueIsAbove) // 4
|
||||
return Vector2.DistanceSquared(new Vector2(value.Left, value.Bottom), new Vector2(this.Right, this.Top));
|
||||
if (valueIsBelow) // 5
|
||||
return Vector2.DistanceSquared(new Vector2(value.Left, value.Top), new Vector2(this.Right, this.Bottom));
|
||||
return (value.Left - this.Right) * (value.Left - this.Right); // 6
|
||||
} else if (valueIsAbove) {
|
||||
return (this.Top - value.Bottom) * (this.Top - value.Bottom); // 7
|
||||
} else if (valueIsBelow) {
|
||||
return (value.Top - this.Bottom) * (value.Top - this.Bottom); // 8
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the distance between this rectangle and the <paramref name="value"/>.
|
||||
/// The returned value is the smallest distance between any two edges or corners of the two rectangles.
|
||||
/// </summary>
|
||||
/// <param name="value">The rectangle to calculate the distance to.</param>
|
||||
/// <returns>The distance between the two rectangles.</returns>
|
||||
public float Distance(RectangleF value) {
|
||||
return (float) Math.Sqrt(this.DistanceSquared(value));
|
||||
}
|
||||
|
||||
/// <summary>Returns a string that represents the current object.</summary>
|
||||
/// <returns>A string that represents the current object.</returns>
|
||||
public override string ToString() {
|
||||
return "{X:" + this.X + " Y:" + this.Y + " Width:" + this.Width + " Height:" + this.Height + "}";
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Rectangle.Deconstruct"/>
|
||||
public void Deconstruct(out float x, out float y, out float width, out float height) {
|
||||
x = this.X;
|
||||
y = this.Y;
|
||||
width = this.Width;
|
||||
height = this.Height;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a float-based rectangle to an int-based rectangle, flooring each value in the process.
|
||||
/// </summary>
|
||||
/// <param name="rect">The rectangle to convert</param>
|
||||
/// <returns>The resulting rectangle</returns>
|
||||
public static explicit operator Rectangle(RectangleF rect) {
|
||||
return new Rectangle(rect.X.Floor(), rect.Y.Floor(), rect.Width.Floor(), rect.Height.Floor());
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Rectangle.Union(Rectangle, Rectangle)"/>
|
||||
public static RectangleF Union(RectangleF value1, RectangleF value2) {
|
||||
var x = Math.Min(value1.X, value2.X);
|
||||
var y = Math.Min(value1.Y, value2.Y);
|
||||
return new RectangleF(x, y, Math.Max(value1.Right, value2.Right) - x, Math.Max(value1.Bottom, value2.Bottom) - y);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Rectangle.Intersect(Rectangle, Rectangle)"/>
|
||||
public static RectangleF Intersect(RectangleF value1, RectangleF value2) {
|
||||
if (value1.Intersects(value2)) {
|
||||
|
@ -209,37 +252,38 @@ namespace MLEM.Misc {
|
|||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Rectangle.Offset(float, float)"/>
|
||||
public void Offset(float offsetX, float offsetY) {
|
||||
this.X += offsetX;
|
||||
this.Y += offsetY;
|
||||
/// <summary>
|
||||
/// Creates a new rectangle based on two corners that form a bounding box.
|
||||
/// The resulting rectangle will encompass both corners as well as all of the space between them.
|
||||
/// </summary>
|
||||
/// <param name="corner1">The first corner to use</param>
|
||||
/// <param name="corner2">The second corner to use</param>
|
||||
/// <returns></returns>
|
||||
public static RectangleF FromCorners(Vector2 corner1, Vector2 corner2) {
|
||||
var minX = Math.Min(corner1.X, corner2.X);
|
||||
var minY = Math.Min(corner1.Y, corner2.Y);
|
||||
var maxX = Math.Max(corner1.X, corner2.X);
|
||||
var maxY = Math.Max(corner1.Y, corner2.Y);
|
||||
return new RectangleF(minX, minY, maxX - minX, maxY - minY);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Rectangle.Offset(Vector2)"/>
|
||||
public void Offset(Vector2 amount) {
|
||||
this.X += amount.X;
|
||||
this.Y += amount.Y;
|
||||
/// <summary>
|
||||
/// Converts an int-based rectangle to a float-based rectangle.
|
||||
/// </summary>
|
||||
/// <param name="rect">The rectangle to convert</param>
|
||||
/// <returns>The resulting rectangle</returns>
|
||||
public static explicit operator RectangleF(Rectangle rect) {
|
||||
return new RectangleF(rect.X, rect.Y, rect.Width, rect.Height);
|
||||
}
|
||||
|
||||
/// <summary>Returns a string that represents the current object.</summary>
|
||||
/// <returns>A string that represents the current object.</returns>
|
||||
public override string ToString() {
|
||||
return "{X:" + this.X + " Y:" + this.Y + " Width:" + this.Width + " Height:" + this.Height + "}";
|
||||
/// <inheritdoc cref="Equals(RectangleF)"/>
|
||||
public static bool operator ==(RectangleF a, RectangleF b) {
|
||||
return a.X == b.X && a.Y == b.Y && a.Width == b.Width && a.Height == b.Height;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Rectangle.Union(Rectangle, Rectangle)"/>
|
||||
public static RectangleF Union(RectangleF value1, RectangleF value2) {
|
||||
var x = Math.Min(value1.X, value2.X);
|
||||
var y = Math.Min(value1.Y, value2.Y);
|
||||
return new RectangleF(x, y, Math.Max(value1.Right, value2.Right) - x, Math.Max(value1.Bottom, value2.Bottom) - y);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Rectangle.Deconstruct"/>
|
||||
public void Deconstruct(out float x, out float y, out float width, out float height) {
|
||||
x = this.X;
|
||||
y = this.Y;
|
||||
width = this.Width;
|
||||
height = this.Height;
|
||||
/// <inheritdoc cref="Equals(RectangleF)"/>
|
||||
public static bool operator !=(RectangleF a, RectangleF b) {
|
||||
return !(a == b);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -14,8 +14,7 @@ namespace MLEM.Pathfinding {
|
|||
|
||||
/// <inheritdoc />
|
||||
public AStar2(GetCost defaultCostFunction, bool defaultAllowDiagonals, float defaultCost = 1, int defaultMaxTries = 10000) :
|
||||
base(AllDirs, AdjacentDirs, defaultCostFunction, defaultAllowDiagonals, defaultCost, defaultMaxTries) {
|
||||
}
|
||||
base(AllDirs, AdjacentDirs, defaultCostFunction, defaultAllowDiagonals, defaultCost, defaultMaxTries) {}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Point AddPositions(Point first, Point second) {
|
||||
|
|
|
@ -35,8 +35,7 @@ namespace MLEM.Pathfinding {
|
|||
|
||||
/// <inheritdoc />
|
||||
public AStar3(GetCost defaultCostFunction, bool defaultAllowDiagonals, float defaultCost = 1, int defaultMaxTries = 10000) :
|
||||
base(AllDirs, AdjacentDirs, defaultCostFunction, defaultAllowDiagonals, defaultCost, defaultMaxTries) {
|
||||
}
|
||||
base(AllDirs, AdjacentDirs, defaultCostFunction, defaultAllowDiagonals, defaultCost, defaultMaxTries) {}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Vector3 AddPositions(Vector3 first, Vector3 second) {
|
||||
|
|
|
@ -19,8 +19,7 @@ namespace MLEM.Sound {
|
|||
/// Creates a new sound effect instance handler with the given settings
|
||||
/// </summary>
|
||||
/// <param name="game">The game instance</param>
|
||||
public SoundEffectInstanceHandler(Game game) : base(game) {
|
||||
}
|
||||
public SoundEffectInstanceHandler(Game game) : base(game) {}
|
||||
|
||||
/// <inheritdoc cref="Update()"/>
|
||||
public override void Update(GameTime gameTime) {
|
||||
|
@ -36,8 +35,7 @@ namespace MLEM.Sound {
|
|||
for (var i = this.playingSounds.Count - 1; i >= 0; i--) {
|
||||
var entry = this.playingSounds[i];
|
||||
if (entry.Instance.IsDisposed || entry.Instance.State == SoundState.Stopped) {
|
||||
entry.Instance.Stop(true);
|
||||
entry.OnStopped?.Invoke(entry.Instance);
|
||||
entry.StopAndNotify();
|
||||
this.playingSounds.RemoveAt(i);
|
||||
} else {
|
||||
entry.TryApply3D(this.listeners);
|
||||
|
@ -69,6 +67,16 @@ namespace MLEM.Sound {
|
|||
entry.Instance.Resume();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops all of the sound effect instances in this handler
|
||||
/// </summary>
|
||||
public void Stop() {
|
||||
this.playingSounds.RemoveAll(e => {
|
||||
e.StopAndNotify();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new <see cref="SoundEffectInstance"/> to this handler.
|
||||
/// This also starts playing the instance.
|
||||
|
@ -132,7 +140,7 @@ namespace MLEM.Sound {
|
|||
/// </summary>
|
||||
public readonly SoundEffectInstance Instance;
|
||||
/// <summary>
|
||||
/// An action that is invoked when this entry's <see cref="Instance"/> is stopped.
|
||||
/// An action that is invoked when this entry's <see cref="Instance"/> is stopped or after it finishes naturally.
|
||||
/// This action is invoked in <see cref="SoundEffectInstanceHandler.Update()"/>.
|
||||
/// </summary>
|
||||
public readonly Action<SoundEffectInstance> OnStopped;
|
||||
|
@ -153,6 +161,11 @@ namespace MLEM.Sound {
|
|||
this.Instance.Apply3D(listeners, this.Emitter);
|
||||
}
|
||||
|
||||
internal void StopAndNotify() {
|
||||
this.Instance.Stop(true);
|
||||
this.OnStopped?.Invoke(this.Instance);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -53,14 +53,10 @@ namespace MLEM.Textures {
|
|||
/// <param name="paddingTop">The padding on the top edge</param>
|
||||
/// <param name="paddingBottom">The padding on the bottom edge</param>
|
||||
/// <param name="mode">The mode to use for drawing this nine patch, defaults to <see cref="NinePatchMode.Stretch"/></param>
|
||||
public NinePatch(TextureRegion texture, int paddingLeft, int paddingRight, int paddingTop, int paddingBottom, NinePatchMode mode = NinePatchMode.Stretch) :
|
||||
this(texture, new Padding(paddingLeft, paddingRight, paddingTop, paddingBottom), mode) {
|
||||
}
|
||||
public NinePatch(TextureRegion texture, int paddingLeft, int paddingRight, int paddingTop, int paddingBottom, NinePatchMode mode = NinePatchMode.Stretch) : this(texture, new Padding(paddingLeft, paddingRight, paddingTop, paddingBottom), mode) {}
|
||||
|
||||
/// <inheritdoc cref="NinePatch(TextureRegion, int, int, int, int, NinePatchMode)"/>
|
||||
public NinePatch(Texture2D texture, int paddingLeft, int paddingRight, int paddingTop, int paddingBottom, NinePatchMode mode = NinePatchMode.Stretch) :
|
||||
this(new TextureRegion(texture), paddingLeft, paddingRight, paddingTop, paddingBottom, mode) {
|
||||
}
|
||||
public NinePatch(Texture2D texture, int paddingLeft, int paddingRight, int paddingTop, int paddingBottom, NinePatchMode mode = NinePatchMode.Stretch) : this(new TextureRegion(texture), paddingLeft, paddingRight, paddingTop, paddingBottom, mode) {}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new nine patch from a texture and a uniform padding
|
||||
|
@ -68,14 +64,10 @@ namespace MLEM.Textures {
|
|||
/// <param name="texture">The texture to use</param>
|
||||
/// <param name="padding">The padding that each edge should have</param>
|
||||
/// <param name="mode">The mode to use for drawing this nine patch, defaults to <see cref="NinePatchMode.Stretch"/></param>
|
||||
public NinePatch(Texture2D texture, int padding, NinePatchMode mode = NinePatchMode.Stretch) :
|
||||
this(new TextureRegion(texture), padding, mode) {
|
||||
}
|
||||
public NinePatch(Texture2D texture, int padding, NinePatchMode mode = NinePatchMode.Stretch) : this(new TextureRegion(texture), padding, mode) {}
|
||||
|
||||
/// <inheritdoc cref="NinePatch(TextureRegion, int, NinePatchMode)"/>
|
||||
public NinePatch(TextureRegion texture, int padding, NinePatchMode mode = NinePatchMode.Stretch) :
|
||||
this(texture, padding, padding, padding, padding, mode) {
|
||||
}
|
||||
public NinePatch(TextureRegion texture, int padding, NinePatchMode mode = NinePatchMode.Stretch) : this(texture, padding, padding, padding, padding, mode) {}
|
||||
|
||||
internal RectangleF GetRectangleForIndex(RectangleF area, int index, float patchScale = 1) {
|
||||
var pl = this.Padding.Left * patchScale;
|
||||
|
|
|
@ -73,9 +73,7 @@ namespace MLEM.Textures {
|
|||
/// Creates a new texture region that spans the entire texture
|
||||
/// </summary>
|
||||
/// <param name="texture">The texture to use</param>
|
||||
public TextureRegion(Texture2D texture) :
|
||||
this(texture, new Rectangle(0, 0, texture.Width, texture.Height)) {
|
||||
}
|
||||
public TextureRegion(Texture2D texture) : this(texture, new Rectangle(0, 0, texture.Width, texture.Height)) {}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new texture region based on a texture and area coordinates
|
||||
|
@ -85,9 +83,7 @@ namespace MLEM.Textures {
|
|||
/// <param name="v">The y coordinate of the top left corner of this area</param>
|
||||
/// <param name="width">The width of this area</param>
|
||||
/// <param name="height">The height of this area</param>
|
||||
public TextureRegion(Texture2D texture, int u, int v, int width, int height) :
|
||||
this(texture, new Rectangle(u, v, width, height)) {
|
||||
}
|
||||
public TextureRegion(Texture2D texture, int u, int v, int width, int height) : this(texture, new Rectangle(u, v, width, height)) {}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new texture region based on a texture, a position and a size
|
||||
|
@ -95,18 +91,14 @@ namespace MLEM.Textures {
|
|||
/// <param name="texture">The texture to use</param>
|
||||
/// <param name="uv">The top left corner of this area</param>
|
||||
/// <param name="size">The size of this area</param>
|
||||
public TextureRegion(Texture2D texture, Point uv, Point size) :
|
||||
this(texture, new Rectangle(uv, size)) {
|
||||
}
|
||||
public TextureRegion(Texture2D texture, Point uv, Point size) : this(texture, new Rectangle(uv, size)) {}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new texture region which is a sub-region of the given texture region
|
||||
/// </summary>
|
||||
/// <param name="region">The texture region to create a sub-region of</param>
|
||||
/// <param name="area">The new texture region area</param>
|
||||
public TextureRegion(TextureRegion region, Rectangle area) :
|
||||
this(region, area.Location, area.Size) {
|
||||
}
|
||||
public TextureRegion(TextureRegion region, Rectangle area) : this(region, area.Location, area.Size) {}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new texture region which is a sub-region of the given texture region
|
||||
|
@ -116,9 +108,7 @@ namespace MLEM.Textures {
|
|||
/// <param name="v">The y coordinate of the top left corner of this area</param>
|
||||
/// <param name="width">The width of this area</param>
|
||||
/// <param name="height">The height of this area</param>
|
||||
public TextureRegion(TextureRegion region, int u, int v, int width, int height) :
|
||||
this(region, new Point(u, v), new Point(width, height)) {
|
||||
}
|
||||
public TextureRegion(TextureRegion region, int u, int v, int width, int height) : this(region, new Point(u, v), new Point(width, height)) {}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new texture region which is a sub-region of the given texture region
|
||||
|
@ -126,8 +116,16 @@ namespace MLEM.Textures {
|
|||
/// <param name="region">The texture region to create a sub-region of</param>
|
||||
/// <param name="uv">The top left corner of this area</param>
|
||||
/// <param name="size">The size of this area</param>
|
||||
public TextureRegion(TextureRegion region, Point uv, Point size) :
|
||||
this(region.Texture, region.Position + uv, size) {
|
||||
public TextureRegion(TextureRegion region, Point uv, Point size) : this(region.Texture, region.Position + uv, size) {}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a new <see cref="TextureRegion"/> that has the same <see cref="Texture"/>, <see cref="Pivot"/> and <see cref="Size"/> as this texture, but the returned region's <see cref="Position"/> will be offset by <paramref name="offset"/>.
|
||||
/// Note that the <see cref="Name"/> is not preserved in the copy.
|
||||
/// </summary>
|
||||
/// <param name="offset">The offset to apply to the <see cref="Position"/></param>
|
||||
/// <returns>An offset copy of this texture region</returns>
|
||||
public TextureRegion OffsetCopy(Point offset) {
|
||||
return new TextureRegion(this.Texture, this.Position + offset, this.Size) {Pivot = this.Pivot};
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -82,9 +82,7 @@ namespace MLEM.Textures {
|
|||
/// <param name="texture">The texture to use for this atlas</param>
|
||||
/// <param name="regionAmountX">The amount of texture regions in the x direction</param>
|
||||
/// <param name="regionAmountY">The amount of texture regions in the y direction</param>
|
||||
public UniformTextureAtlas(Texture2D texture, int regionAmountX, int regionAmountY) :
|
||||
this(new TextureRegion(texture), regionAmountX, regionAmountY) {
|
||||
}
|
||||
public UniformTextureAtlas(Texture2D texture, int regionAmountX, int regionAmountY) : this(new TextureRegion(texture), regionAmountX, regionAmountY) {}
|
||||
|
||||
private TextureRegion GetOrAddRegion(Rectangle rect) {
|
||||
if (this.regions.TryGetValue(rect, out var region))
|
||||
|
|
|
@ -14,13 +14,14 @@
|
|||
- **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 data handling
|
||||
- **MLEM.Data** provides simple loading and processing of textures and data
|
||||
- **MLEM.Startup** combines MLEM with some other useful libraries into a quick Game startup class
|
||||
- **MLEM.Templates** contains cross-platform project templates
|
||||
|
||||
# Made with MLEM
|
||||
- [A Breath of Spring Air](https://ellpeck.itch.io/a-breath-of-spring-air), a short platformer ([Source](https://git.ellpeck.de/Ellpeck/GreatSpringGameJam))
|
||||
- [Don't Wake Up](https://ellpeck.itch.io/dont-wake-up), a short puzzle game ([Source](https://github.com/Ellpeck/DontLetGo))
|
||||
- [Pong clone](https://github.com/luanfagu/pong), a very simple pong clone ([Source](https://github.com/luanfagu/pong))
|
||||
- [Tiny Life](https://tinylifegame.com), an isometric life simulation game ([Modding API](https://github.com/Ellpeck/TinyLifeExampleMod))
|
||||
|
||||
If you created a game with the help of MLEM, you can get it added to this list by submitting it on the [issue tracker](https://github.com/Ellpeck/MLEM/issues). If its source is public, other people will be able to use your project as an example, too!
|
||||
|
@ -37,8 +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/main/Media/Formatting.png)
|
||||
|
||||
# Friends of MLEM
|
||||
There are several other NuGet packages and tools that work well in combination with MonoGame and MLEM. Here are some of them:
|
||||
There are several other libraries and tools that work well in combination with MonoGame and MLEM. Here are some of them:
|
||||
- [Contentless](https://github.com/Ellpeck/Contentless), a tool that removes the need to add assets to the MonoGame Content Pipeline manually
|
||||
- [GameBundle](https://github.com/Ellpeck/GameBundle), a tool that packages MonoGame and other .NET Core applications into several distributable formats
|
||||
- [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
|
||||
|
|
|
@ -22,6 +22,7 @@ using MLEM.Ui;
|
|||
using MLEM.Ui.Elements;
|
||||
using MLEM.Ui.Style;
|
||||
using MonoGame.Extended.Tiled;
|
||||
using MonoGame.Extended.ViewportAdapters;
|
||||
|
||||
namespace Sandbox {
|
||||
public class GameImpl : MlemGame {
|
||||
|
@ -119,13 +120,6 @@ namespace Sandbox {
|
|||
Console.WriteLine(vec + " -> " + dir);
|
||||
}
|
||||
|
||||
var copy = obj.DeepCopy();
|
||||
Console.WriteLine(copy);
|
||||
|
||||
var intoCopy = new Test(Vector2.One, "test") {OtherTest = new Test(Vector2.One, "other")};
|
||||
obj.DeepCopyInto(intoCopy);
|
||||
Console.WriteLine(intoCopy);
|
||||
|
||||
var writer = new StringWriter();
|
||||
this.Content.GetJsonSerializer().Serialize(writer, obj);
|
||||
//Console.WriteLine(writer.ToString());
|
||||
|
@ -239,7 +233,7 @@ namespace Sandbox {
|
|||
par.OnDrawn = (e, time, batch, a) => batch.DrawRectangle(e.DisplayArea.ToExtended(), Color.Red);
|
||||
this.UiSystem.Add("Load", loadGroup);*/
|
||||
|
||||
var spillPanel = new Panel(Anchor.Center, new Vector2(100), Vector2.Zero);
|
||||
/*var spillPanel = new Panel(Anchor.Center, new Vector2(100), Vector2.Zero);
|
||||
var squishingGroup = spillPanel.AddChild(new SquishingGroup(Anchor.TopLeft, Vector2.One));
|
||||
squishingGroup.AddChild(new Button(Anchor.TopLeft, new Vector2(30), "TL") {
|
||||
OnUpdated = (e, time) => e.IsHidden = Input.IsKeyDown(Keys.D1),
|
||||
|
@ -266,7 +260,60 @@ namespace Sandbox {
|
|||
e.SetAreaDirty();
|
||||
}
|
||||
}).SetData("Ref", "Main");
|
||||
this.UiSystem.Add("SpillTest", spillPanel);
|
||||
this.UiSystem.Add("SpillTest", spillPanel);*/
|
||||
|
||||
var regularFont = spriteFont.Font;
|
||||
var genericFont = spriteFont;
|
||||
|
||||
var index = 0;
|
||||
var pos = new Vector2(100, 20);
|
||||
var scale = 1F;
|
||||
var origin = Vector2.Zero;
|
||||
var rotation = 0F;
|
||||
var effects = SpriteEffects.None;
|
||||
|
||||
this.OnDraw += (g, time) => {
|
||||
const string testString = "This is a\ntest string\n\twith long lines.\nLet's write some more stuff. Let's\r\nsplit lines weirdly.";
|
||||
if (Input.IsKeyPressed(Keys.I)) {
|
||||
index++;
|
||||
if (index == 1) {
|
||||
scale = 2;
|
||||
} else if (index == 2) {
|
||||
origin = new Vector2(15, 15);
|
||||
} else if (index == 3) {
|
||||
rotation = 0.25F;
|
||||
} else if (index == 4) {
|
||||
effects = SpriteEffects.FlipHorizontally;
|
||||
} else if (index == 5) {
|
||||
effects = SpriteEffects.FlipVertically;
|
||||
} else if (index == 6) {
|
||||
effects = SpriteEffects.FlipHorizontally | SpriteEffects.FlipVertically;
|
||||
}
|
||||
}
|
||||
|
||||
this.SpriteBatch.Begin();
|
||||
if (Input.IsKeyDown(Keys.LeftShift)) {
|
||||
this.SpriteBatch.DrawString(regularFont, testString, pos, Color.Red, rotation, origin, scale, effects, 0);
|
||||
} else {
|
||||
genericFont.DrawString(this.SpriteBatch, testString, pos, Color.Green, rotation, origin, scale, effects, 0);
|
||||
}
|
||||
this.SpriteBatch.End();
|
||||
};
|
||||
|
||||
var viewport = new BoxingViewportAdapter(this.Window, this.GraphicsDevice, 1280, 720);
|
||||
var newPanel = new Panel(Anchor.TopLeft, new Vector2(200, 100), new Vector2(10, 10));
|
||||
newPanel.AddChild(new Button(Anchor.TopLeft, new Vector2(100, 20), "Text", "Tooltip text"));
|
||||
this.UiSystem.Add("Panel", newPanel);
|
||||
|
||||
var keybindPanel = new Panel(Anchor.BottomRight, new Vector2(130, 150), new Vector2(5));
|
||||
for (var i = 0; i < 15; i++) {
|
||||
var button = keybindPanel.AddChild(new Button(default, default, i.ToString()));
|
||||
button.Anchor = Anchor.AutoInline;
|
||||
button.Padding = new Padding(0.5F);
|
||||
button.SetHeightBasedOnChildren = false;
|
||||
button.Size = new Vector2(30, 50);
|
||||
}
|
||||
this.UiSystem.Add("Keybinds", keybindPanel);
|
||||
}
|
||||
|
||||
protected override void DoUpdate(GameTime gameTime) {
|
||||
|
@ -276,13 +323,13 @@ namespace Sandbox {
|
|||
|
||||
var delta = this.InputHandler.ScrollWheel - this.InputHandler.LastScrollWheel;
|
||||
if (delta != 0) {
|
||||
this.camera.Zoom(0.1F * Math.Sign(delta), this.InputHandler.MousePosition.ToVector2());
|
||||
this.camera.Zoom(0.1F * Math.Sign(delta), this.InputHandler.ViewportMousePosition.ToVector2());
|
||||
}
|
||||
|
||||
/*if (Input.InputsDown.Length > 0)
|
||||
Console.WriteLine("Down: " + string.Join(", ", Input.InputsDown));
|
||||
Console.WriteLine("Down: " + string.Join(", ", Input.InputsDown));*/
|
||||
if (Input.InputsPressed.Length > 0)
|
||||
Console.WriteLine("Pressed: " + string.Join(", ", Input.InputsPressed));*/
|
||||
Console.WriteLine("Pressed: " + string.Join(", ", Input.InputsPressed));
|
||||
}
|
||||
|
||||
protected override void DoDraw(GameTime gameTime) {
|
||||
|
@ -307,7 +354,6 @@ namespace Sandbox {
|
|||
public Direction2 Dir { get; set; }
|
||||
public Test OtherTest;
|
||||
|
||||
[CopyConstructor]
|
||||
public Test(Vector2 test, string test2) {
|
||||
Console.WriteLine("Constructed with " + test + ", " + test2);
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
<PackageReference Include="MonoGame.Extended.Tiled" Version="3.8.0" />
|
||||
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.0.1641" />
|
||||
<PackageReference Include="FontStashSharp.MonoGame" Version="1.0.4" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Numerics;
|
||||
using Microsoft.Xna.Framework;
|
||||
|
@ -38,33 +37,6 @@ namespace Tests {
|
|||
Assert.AreEqual(this.testObject, read);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCopy() {
|
||||
var copy = this.testObject.Copy();
|
||||
Assert.AreEqual(this.testObject, copy);
|
||||
Assert.AreSame(this.testObject.OtherTest, copy.OtherTest);
|
||||
|
||||
var deepCopy = this.testObject.DeepCopy();
|
||||
Assert.AreEqual(this.testObject, deepCopy);
|
||||
Assert.AreNotSame(this.testObject.OtherTest, deepCopy.OtherTest);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCopySpeed() {
|
||||
const int count = 1000000;
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
for (var i = 0; i < count; i++)
|
||||
this.testObject.Copy();
|
||||
stopwatch.Stop();
|
||||
TestContext.WriteLine($"Copy took {stopwatch.Elapsed.TotalMilliseconds / count * 1000000}ns on average");
|
||||
|
||||
stopwatch.Restart();
|
||||
for (var i = 0; i < count; i++)
|
||||
this.testObject.DeepCopy();
|
||||
stopwatch.Stop();
|
||||
TestContext.WriteLine($"DeepCopy took {stopwatch.Elapsed.TotalMilliseconds / count * 1000000}ns on average");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDynamicEnum() {
|
||||
var flags = new TestEnum[100];
|
||||
|
@ -124,8 +96,7 @@ namespace Tests {
|
|||
public Direction2 Dir { get; set; }
|
||||
public TestObject OtherTest;
|
||||
|
||||
public TestObject(Vector2 test, string test2) {
|
||||
}
|
||||
public TestObject(Vector2 test, string test2) {}
|
||||
|
||||
protected bool Equals(TestObject other) {
|
||||
return this.Vec.Equals(other.Vec) && this.Point.Equals(other.Point) && Equals(this.OtherTest, other.OtherTest) && this.Dir == other.Dir;
|
||||
|
@ -143,8 +114,7 @@ namespace Tests {
|
|||
|
||||
private class TestEnum : DynamicEnum {
|
||||
|
||||
public TestEnum(string name, BigInteger value) : base(name, value) {
|
||||
}
|
||||
public TestEnum(string name, BigInteger value) : base(name, value) {}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -147,10 +147,10 @@ namespace Tests {
|
|||
}
|
||||
|
||||
CompareSizes($"This is a very simple{GenericFont.Nbsp}test string");
|
||||
CompareSizes($"This is a very simple{GenericFont.OneEmSpace}test string");
|
||||
CompareSizes($"This is a very simple{GenericFont.Emsp}test string");
|
||||
CompareSizes($"This is a very simple{GenericFont.Zwsp}test string");
|
||||
|
||||
Assert.AreEqual(new Vector2(this.font.LineHeight, this.font.LineHeight), this.font.MeasureString(GenericFont.OneEmSpace.ToCachedString()));
|
||||
Assert.AreEqual(new Vector2(this.font.LineHeight, this.font.LineHeight), this.font.MeasureString(GenericFont.Emsp.ToCachedString()));
|
||||
Assert.AreEqual(new Vector2(0, this.font.LineHeight), this.font.MeasureString(GenericFont.Zwsp.ToCachedString()));
|
||||
}
|
||||
|
||||
|
|
|
@ -8,8 +8,7 @@ namespace Tests {
|
|||
|
||||
public RawContentManager RawContent { get; private set; }
|
||||
|
||||
private TestGame() {
|
||||
}
|
||||
private TestGame() {}
|
||||
|
||||
protected override void LoadContent() {
|
||||
base.LoadContent();
|
||||
|
|
|
@ -18,8 +18,9 @@
|
|||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.2.0" />
|
||||
<PackageReference Include="NunitXml.TestLogger" Version="3.0.117" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
@ -80,8 +80,7 @@ namespace Tests {
|
|||
Assert.AreEqual(170, packer4.PackedTexture.Height);
|
||||
}
|
||||
|
||||
private static void StubResult(TextureRegion region) {
|
||||
}
|
||||
private static void StubResult(TextureRegion region) {}
|
||||
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
#tool docfx.console&version=2.58.9
|
||||
|
||||
// this is the upcoming version, for prereleases
|
||||
var version = Argument("version", "5.2.0");
|
||||
var version = Argument("version", "5.3.0");
|
||||
var target = Argument("target", "Default");
|
||||
var branch = Argument("branch", "main");
|
||||
var config = Argument("configuration", "Release");
|
||||
|
|
Loading…
Reference in a new issue