1
0
Fork 0
mirror of https://github.com/Ellpeck/MLEM.git synced 2024-11-25 05:58:35 +01:00

Compare commits

..

6 commits

6 changed files with 137 additions and 54 deletions

View file

@ -15,6 +15,7 @@ Additions
- Added SoundEffectInstanceHandler.Stop - Added SoundEffectInstanceHandler.Stop
- Added TextureRegion.OffsetCopy - Added TextureRegion.OffsetCopy
- Added RectangleF.DistanceSquared and RectangleF.Distance - Added RectangleF.DistanceSquared and RectangleF.Distance
- Added GamepadExtensions.GetAnalogValue to get the analog value of any gamepad button
Improvements Improvements
- Generify GenericFont's string drawing - Generify GenericFont's string drawing
@ -22,6 +23,7 @@ Improvements
- Added float version of GetRandomWeightedEntry - Added float version of GetRandomWeightedEntry
- Allow LinkCode to specify a color to draw with - Allow LinkCode to specify a color to draw with
- Allow better control over the order and layout of a Keybind's combinations - Allow better control over the order and layout of a Keybind's combinations
- Allow setting a gamepad button deadzone in InputHandler
Fixes Fixes
- **Fixed a formatting Code only knowing about the last Token that it is applied in** - **Fixed a formatting Code only knowing about the last Token that it is applied in**
@ -34,6 +36,7 @@ Removals
### MLEM.Ui ### MLEM.Ui
Additions Additions
- Added Element.OnStyleInit event - Added Element.OnStyleInit event
- Added UiControls.AutoNavModeChanged event
Improvements Improvements
- Allow for checkboxes and radio buttons to be disabled - Allow for checkboxes and radio buttons to be disabled
@ -49,6 +52,7 @@ Improvements
- Automatically select the first element when a dropdown is opened in auto nav mode - Automatically select the first element when a dropdown is opened in auto nav mode
- Improved gamepad navigation by employing angles between elements - Improved gamepad navigation by employing angles between elements
- Prefer elements that have the same parent as the currently selected element when using gamepad navigation - 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
Fixes Fixes
- Fixed paragraph links having incorrect hover locations when using special text alignments - Fixed paragraph links having incorrect hover locations when using special text alignments

View file

@ -1,5 +1,6 @@
using System; using System;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using MLEM.Input;
using MLEM.Ui.Style; using MLEM.Ui.Style;
namespace MLEM.Ui.Elements { namespace MLEM.Ui.Elements {
@ -22,6 +23,12 @@ namespace MLEM.Ui.Elements {
/// The paragraph of text that this tooltip displays /// The paragraph of text that this tooltip displays
/// </summary> /// </summary>
public Paragraph Paragraph; public Paragraph Paragraph;
/// <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; }
private TimeSpan delayCountdown; private TimeSpan delayCountdown;
private bool autoHidden; private bool autoHidden;
@ -91,7 +98,8 @@ namespace MLEM.Ui.Elements {
/// </summary> /// </summary>
public void SnapPositionToMouse() { public void SnapPositionToMouse() {
var viewport = this.System.Viewport; var viewport = this.System.Viewport;
var offset = (this.Input.ViewportMousePosition.ToVector2() + this.MouseOffset.Value) / this.Scale; var snapPosition = this.SnapPosition ?? this.Input.ViewportMousePosition.ToVector2();
var offset = (snapPosition + this.MouseOffset.Value) / this.Scale;
if (offset.X < viewport.X) if (offset.X < viewport.X)
offset.X = viewport.X; offset.X = viewport.X;
if (offset.Y < viewport.Y) if (offset.Y < viewport.Y)

View file

@ -21,36 +21,6 @@ namespace MLEM.Ui {
/// The input handler that is used for querying input /// The input handler that is used for querying input
/// </summary> /// </summary>
public readonly InputHandler Input; 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> /// <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. /// 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"/>. /// If the <see cref="ModifierKey.Shift"/> is held, these buttons perform <see cref="Element.OnSecondaryPressed"/>.
@ -85,6 +55,25 @@ namespace MLEM.Ui {
/// This can be used to easily serialize and deserialize all ui keybinds. /// This can be used to easily serialize and deserialize all ui keybinds.
/// </summary> /// </summary>
public readonly Keybind[] Keybinds; 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> /// <summary>
/// The zero-based index of the <see cref="GamePad"/> used for gamepad input. /// 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. /// If this index is lower than 0, every connected gamepad will trigger input.
@ -113,9 +102,35 @@ namespace MLEM.Ui {
/// <summary> /// <summary>
/// If this value is true, the ui controls are in automatic navigation mode. /// 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"/>. /// 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> /// </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> /// <summary>
/// Creates a new instance of the ui controls. /// Creates a new instance of the ui controls.
@ -377,17 +392,20 @@ namespace MLEM.Ui {
return children.FirstOrDefault(c => c.CanBeSelected); return children.FirstOrDefault(c => c.CanBeSelected);
} else { } else {
Element closest = null; Element closest = null;
float closestDistSq = 0; float closestPriority = 0;
foreach (var child in children) { foreach (var child in children) {
if (!child.CanBeSelected || child == this.SelectedElement) if (!child.CanBeSelected || child == this.SelectedElement)
continue; continue;
var (xOffset, yOffset) = child.Area.Center - this.SelectedElement.Area.Center; var (xOffset, yOffset) = child.Area.Center - this.SelectedElement.Area.Center;
if (Math.Abs(direction.Angle() - Math.Atan2(yOffset, xOffset)) >= MathHelper.PiOver2 - Element.Epsilon) var angle = Math.Abs(direction.Angle() - (float) Math.Atan2(yOffset, xOffset));
if (angle >= MathHelper.PiOver2 - Element.Epsilon)
continue; continue;
var distSq = child.Area.DistanceSquared(this.SelectedElement.Area); var distSq = child.Area.DistanceSquared(this.SelectedElement.Area);
if (closest == null || distSq < closestDistSq) { // 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; closest = child;
closestDistSq = distSq; closestPriority = priority;
} }
} }
return closest; return closest;

View file

@ -0,0 +1,46 @@
using System;
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;
}
}
}
}

View file

@ -59,6 +59,12 @@ namespace MLEM.Input {
/// Set this field to false to enable <see cref="InputsDown"/> and <see cref="InputsPressed"/> being calculated. /// Set this field to false to enable <see cref="InputsDown"/> and <see cref="InputsPressed"/> being calculated.
/// </summary> /// </summary>
public bool StoreAllActiveInputs; public bool StoreAllActiveInputs;
/// <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> /// <summary>
/// An array of all <see cref="Keys"/>, <see cref="Buttons"/> and <see cref="MouseButton"/> values that are currently down. /// An array of all <see cref="Keys"/>, <see cref="Buttons"/> and <see cref="MouseButton"/> values that are currently down.
@ -234,13 +240,13 @@ namespace MLEM.Input {
this.ConnectedGamepads = GamePad.MaximumGamePadCount; this.ConnectedGamepads = GamePad.MaximumGamePadCount;
for (var i = 0; i < GamePad.MaximumGamePadCount; i++) { for (var i = 0; i < GamePad.MaximumGamePadCount; i++) {
this.lastGamepads[i] = this.gamepads[i]; this.lastGamepads[i] = this.gamepads[i];
var state = GamePadState.Default; this.gamepads[i] = GamePadState.Default;
if (GamePad.GetCapabilities(i).IsConnected) { if (GamePad.GetCapabilities(i).IsConnected) {
if (active) { if (active) {
state = GamePad.GetState(i); this.gamepads[i] = GamePad.GetState(i);
if (this.StoreAllActiveInputs) { if (this.StoreAllActiveInputs) {
foreach (var button in EnumHelper.Buttons) { foreach (var button in EnumHelper.Buttons) {
if (state.IsButtonDown(button)) if (this.IsGamepadButtonDown(button, i))
this.inputsDownAccum.Add(button); this.inputsDownAccum.Add(button);
} }
} }
@ -249,7 +255,6 @@ namespace MLEM.Input {
if (this.ConnectedGamepads > i) if (this.ConnectedGamepads > i)
this.ConnectedGamepads = i; this.ConnectedGamepads = i;
} }
this.gamepads[i] = state;
} }
if (this.HandleGamepadRepeats) { if (this.HandleGamepadRepeats) {
@ -446,48 +451,48 @@ namespace MLEM.Input {
public bool IsGamepadButtonDown(Buttons button, int index = -1) { public bool IsGamepadButtonDown(Buttons button, int index = -1) {
if (index < 0) { if (index < 0) {
for (var i = 0; i < this.ConnectedGamepads; i++) { for (var i = 0; i < this.ConnectedGamepads; i++) {
if (this.GetGamepadState(i).IsButtonDown(button)) if (this.GetGamepadState(i).GetAnalogValue(button) > this.GamepadButtonDeadzone)
return true; return true;
} }
return false; return false;
} }
return this.GetGamepadState(index).IsButtonDown(button); return this.GetGamepadState(index).GetAnalogValue(button) > this.GamepadButtonDeadzone;
} }
/// <inheritdoc cref="GamePadState.IsButtonUp"/> /// <inheritdoc cref="GamePadState.IsButtonUp"/>
public bool IsGamepadButtonUp(Buttons button, int index = -1) { public bool IsGamepadButtonUp(Buttons button, int index = -1) {
if (index < 0) { if (index < 0) {
for (var i = 0; i < this.ConnectedGamepads; i++) { for (var i = 0; i < this.ConnectedGamepads; i++) {
if (this.GetGamepadState(i).IsButtonUp(button)) if (this.GetGamepadState(i).GetAnalogValue(button) <= this.GamepadButtonDeadzone)
return true; return true;
} }
return false; return false;
} }
return this.GetGamepadState(index).IsButtonUp(button); return this.GetGamepadState(index).GetAnalogValue(button) <= this.GamepadButtonDeadzone;
} }
/// <inheritdoc cref="GamePadState.IsButtonDown"/> /// <inheritdoc cref="GamePadState.IsButtonDown"/>
public bool WasGamepadButtonDown(Buttons button, int index = -1) { public bool WasGamepadButtonDown(Buttons button, int index = -1) {
if (index < 0) { if (index < 0) {
for (var i = 0; i < this.ConnectedGamepads; i++) { for (var i = 0; i < this.ConnectedGamepads; i++) {
if (this.GetLastGamepadState(i).IsButtonDown(button)) if (this.GetLastGamepadState(i).GetAnalogValue(button) > this.GamepadButtonDeadzone)
return true; return true;
} }
return false; return false;
} }
return this.GetLastGamepadState(index).IsButtonDown(button); return this.GetLastGamepadState(index).GetAnalogValue(button) > this.GamepadButtonDeadzone;
} }
/// <inheritdoc cref="GamePadState.IsButtonUp"/> /// <inheritdoc cref="GamePadState.IsButtonUp"/>
public bool WasGamepadButtonUp(Buttons button, int index = -1) { public bool WasGamepadButtonUp(Buttons button, int index = -1) {
if (index < 0) { if (index < 0) {
for (var i = 0; i < this.ConnectedGamepads; i++) { for (var i = 0; i < this.ConnectedGamepads; i++) {
if (this.GetLastGamepadState(i).IsButtonUp(button)) if (this.GetLastGamepadState(i).GetAnalogValue(button) <= this.GamepadButtonDeadzone)
return true; return true;
} }
return false; return false;
} }
return this.GetLastGamepadState(index).IsButtonUp(button); return this.GetLastGamepadState(index).GetAnalogValue(button) <= this.GamepadButtonDeadzone;
} }
/// <summary> /// <summary>

View file

@ -305,11 +305,13 @@ namespace Sandbox {
newPanel.AddChild(new Button(Anchor.TopLeft, new Vector2(100, 20), "Text", "Tooltip text")); newPanel.AddChild(new Button(Anchor.TopLeft, new Vector2(100, 20), "Text", "Tooltip text"));
this.UiSystem.Add("Panel", newPanel); this.UiSystem.Add("Panel", newPanel);
var keybind = new Keybind(MouseButton.Left, ModifierKey.Shift); var keybindPanel = new Panel(Anchor.BottomRight, new Vector2(130, 150), new Vector2(5));
var keybindPanel = new Panel(Anchor.BottomRight, new Vector2(100, 100), new Vector2(5)); for (var i = 0; i < 15; i++) {
for (var i = 0; i < 3; i++) { var button = keybindPanel.AddChild(new Button(default, default, i.ToString()));
var button = keybindPanel.AddChild(ElementHelper.KeybindButton(Anchor.AutoLeft, new Vector2(0.5F, 12), keybind, Input, "Press", Keys.Escape, index: i)); button.Anchor = Anchor.AutoInline;
button.Text.TextScale = 0.1F; button.Padding = new Padding(0.5F);
button.SetHeightBasedOnChildren = false;
button.Size = new Vector2(30, 50);
} }
this.UiSystem.Add("Keybinds", keybindPanel); this.UiSystem.Add("Keybinds", keybindPanel);
} }