Compare commits
5 Commits
11423d86e5
...
2576ea87bc
| Author | SHA1 | Date | |
|---|---|---|---|
| 2576ea87bc | |||
| 7bbfab8359 | |||
| 828ff3561b | |||
| c7acc70dcd | |||
| b506b78c32 |
@@ -1,12 +1,9 @@
|
|||||||
[Button]
|
[Button]
|
||||||
BackgroundColor = "#0f62fe"
|
BackgroundColor = "#0f62fe"
|
||||||
|
CornerRadius = 16.0
|
||||||
TextColor = "#ffffff"
|
TextColor = "#ffffff"
|
||||||
Padding = 16.0
|
Padding = 16.0
|
||||||
|
|
||||||
# Creates an empty rule for Button.Normal.
|
|
||||||
# This will inherit style from Button, this is a temporary workaround.
|
|
||||||
[Button.Normal]
|
|
||||||
|
|
||||||
[Button.Hovered]
|
[Button.Hovered]
|
||||||
BackgroundColor = "#0353e9"
|
BackgroundColor = "#0353e9"
|
||||||
|
|
||||||
@@ -61,3 +58,17 @@ BorderColor = "#161616"
|
|||||||
# Default background color for all Container derived classes.
|
# Default background color for all Container derived classes.
|
||||||
[Container]
|
[Container]
|
||||||
BackgroundColor = "#e0e0e0"
|
BackgroundColor = "#e0e0e0"
|
||||||
|
|
||||||
|
|
||||||
|
[InputField]
|
||||||
|
TextColor = "#161616"
|
||||||
|
BorderColor = "#8d8d8d"
|
||||||
|
BorderSize = [0, 0, 0, 1]
|
||||||
|
Padding = [16, 16, 4, 8]
|
||||||
|
|
||||||
|
[InputField.Focused]
|
||||||
|
BorderColor = "#0f62fe"
|
||||||
|
BorderSize = 2.0
|
||||||
|
|
||||||
|
# [InputField.Hovered]BorderColor = "#0f62fe"
|
||||||
|
# BackgroundColor = "#f4f4f4"
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ public class TestGame : Game
|
|||||||
InitializeSystemsDefault();
|
InitializeSystemsDefault();
|
||||||
|
|
||||||
_uiSystem = new UISystem(Input);
|
_uiSystem = new UISystem(Input);
|
||||||
// _uiSystem.RenderDebugRects = true;
|
_uiSystem.RenderDebugRects = true;
|
||||||
|
|
||||||
ResourceManager.EnableFileWatching();
|
ResourceManager.EnableFileWatching();
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ public class TestGame : Game
|
|||||||
|
|
||||||
_uiSystem.SetStyleSheet(_styleSheet);
|
_uiSystem.SetStyleSheet(_styleSheet);
|
||||||
|
|
||||||
var addButton = new Button("Default button", _defaultFontSet);
|
var addButton = new Button("", _defaultFontSet);
|
||||||
|
|
||||||
var removeButton = new Button("Danger button", _defaultFontSet);
|
var removeButton = new Button("Danger button", _defaultFontSet);
|
||||||
|
|
||||||
@@ -93,10 +93,16 @@ public class TestGame : Game
|
|||||||
Anchor = Anchor.TopCenter
|
Anchor = Anchor.TopCenter
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var inputField = new InputField(string.Empty, _defaultFontSet)
|
||||||
|
{
|
||||||
|
PlaceholderText = "Hello, World!"
|
||||||
|
};
|
||||||
|
|
||||||
c.AddChild(addButton);
|
c.AddChild(addButton);
|
||||||
c.AddChild(removeButton);
|
c.AddChild(removeButton);
|
||||||
c.AddChild(outlineButton);
|
c.AddChild(outlineButton);
|
||||||
c.AddChild(linkButton);
|
c.AddChild(linkButton);
|
||||||
|
c.AddChild(inputField);
|
||||||
|
|
||||||
var vc = new VerticalContainer(0.0f);
|
var vc = new VerticalContainer(0.0f);
|
||||||
vc.AddChild(c);
|
vc.AddChild(c);
|
||||||
|
|||||||
@@ -25,11 +25,8 @@ namespace Voile.Input
|
|||||||
/// Some backends require inputs to be polled once per specific interval. Override this method to implement this behavior.
|
/// Some backends require inputs to be polled once per specific interval. Override this method to implement this behavior.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual void Poll() { }
|
public virtual void Poll() { }
|
||||||
|
|
||||||
public void Shutdown() => Dispose();
|
public void Shutdown() => Dispose();
|
||||||
|
|
||||||
public void Dispose() => GC.SuppressFinalize(this);
|
public void Dispose() => GC.SuppressFinalize(this);
|
||||||
|
|
||||||
public bool Handled { get => _handled; set => _handled = value; }
|
public bool Handled { get => _handled; set => _handled = value; }
|
||||||
|
|
||||||
public bool IsActionDown(string action)
|
public bool IsActionDown(string action)
|
||||||
@@ -137,18 +134,20 @@ namespace Voile.Input
|
|||||||
new KeyInputAction(KeyboardKey.Down),
|
new KeyInputAction(KeyboardKey.Down),
|
||||||
]);
|
]);
|
||||||
AddInputMapping("left", [
|
AddInputMapping("left", [
|
||||||
new KeyInputAction(KeyboardKey.A),
|
|
||||||
new KeyInputAction(KeyboardKey.Left),
|
new KeyInputAction(KeyboardKey.Left),
|
||||||
]);
|
]);
|
||||||
AddInputMapping("right", [
|
AddInputMapping("right", [
|
||||||
new KeyInputAction(KeyboardKey.D),
|
|
||||||
new KeyInputAction(KeyboardKey.Right),
|
new KeyInputAction(KeyboardKey.Right),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
AddInputMapping("accept", [
|
AddInputMapping("accept", [
|
||||||
new KeyInputAction(KeyboardKey.Enter),
|
new KeyInputAction(KeyboardKey.Enter),
|
||||||
]);
|
]);
|
||||||
AddInputMapping("cancel", [
|
AddInputMapping("cancel", [
|
||||||
new KeyInputAction(KeyboardKey.Backspace),
|
new KeyInputAction(KeyboardKey.Escape),
|
||||||
|
]);
|
||||||
|
AddInputMapping("backspace", [
|
||||||
|
new KeyInputAction(KeyboardKey.Backspace)
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,23 +3,61 @@ namespace Voile;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a rectangle. Used to determine widget confines for UI layout.
|
/// Represents a rectangle. Used to determine widget confines for UI layout.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public record Rect(float Width = 0.0f, float Height = 0.0f)
|
public class Rect(float width = 0.0f, float height = 0.0f)
|
||||||
{
|
{
|
||||||
public float Width { get; set; } = Width;
|
public float Width { get; set; } = width;
|
||||||
public float Height { get; set; } = Height;
|
public float Height { get; set; } = height;
|
||||||
public static Rect Zero => new Rect(0.0f, 0.0f);
|
public static Rect Zero => new Rect(0.0f, 0.0f);
|
||||||
public float Area => Width * Height;
|
public float Area => Width * Height;
|
||||||
|
|
||||||
public int CompareTo(Rect? other)
|
public int CompareTo(Rect other) => Area.CompareTo(other.Area);
|
||||||
{
|
|
||||||
if (other is null) return 1;
|
|
||||||
return Area.CompareTo(other.Area);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool operator >(Rect left, Rect right) => left.CompareTo(right) > 0;
|
public static bool operator >(Rect left, Rect right) => left.CompareTo(right) > 0;
|
||||||
public static bool operator <(Rect left, Rect right) => left.CompareTo(right) < 0;
|
public static bool operator <(Rect left, Rect right) => left.CompareTo(right) < 0;
|
||||||
public static bool operator >=(Rect left, Rect right) => left.CompareTo(right) >= 0;
|
public static bool operator >=(Rect left, Rect right) => left.CompareTo(right) >= 0;
|
||||||
public static bool operator <=(Rect left, Rect right) => left.CompareTo(right) <= 0;
|
public static bool operator <=(Rect left, Rect right) => left.CompareTo(right) <= 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the rectangle with the smallest width.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="a"></param>
|
||||||
|
/// <param name="b"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static Rect MinWidth(Rect a, Rect b)
|
||||||
|
{
|
||||||
|
if (a < b)
|
||||||
|
{
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
else if (a > b)
|
||||||
|
{
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Rect MaxWidth(Rect a, Rect b)
|
||||||
|
{
|
||||||
|
if (a > b)
|
||||||
|
{
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a < b)
|
||||||
|
{
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Rect operator +(Rect left, Rect right)
|
||||||
|
{
|
||||||
|
var width = left.Width + right.Width;
|
||||||
|
var height = left.Height + right.Height;
|
||||||
|
return new(width, height);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -32,6 +70,8 @@ public struct Size : IEquatable<Size>
|
|||||||
public float Top;
|
public float Top;
|
||||||
public float Bottom;
|
public float Bottom;
|
||||||
|
|
||||||
|
public static Size Zero => new Size(0.0f);
|
||||||
|
|
||||||
public Size(float uniform)
|
public Size(float uniform)
|
||||||
{
|
{
|
||||||
Left = Right = Top = Bottom = uniform;
|
Left = Right = Top = Bottom = uniform;
|
||||||
@@ -51,8 +91,6 @@ public struct Size : IEquatable<Size>
|
|||||||
Bottom = bottom;
|
Bottom = bottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Size Zero => new Size(0);
|
|
||||||
|
|
||||||
public static Rect operator +(Size margin, Rect rect) =>
|
public static Rect operator +(Size margin, Rect rect) =>
|
||||||
new Rect(rect.Width + margin.Left + margin.Right,
|
new Rect(rect.Width + margin.Left + margin.Right,
|
||||||
rect.Height + margin.Top + margin.Bottom);
|
rect.Height + margin.Top + margin.Bottom);
|
||||||
|
|||||||
@@ -252,7 +252,27 @@ namespace Voile.Rendering
|
|||||||
|
|
||||||
var rayFont = _fontPool[font.Handle];
|
var rayFont = _fontPool[font.Handle];
|
||||||
|
|
||||||
Raylib.DrawTextPro(rayFont, text, transformPosition, transformPivot, transformRotation, font.Size, 0.0f, VoileColorToRaylibColor(color));
|
float x = transformPosition.X;
|
||||||
|
float y = transformPosition.Y;
|
||||||
|
|
||||||
|
for (int i = 0; i < text.Length; i++)
|
||||||
|
{
|
||||||
|
char c = text[i];
|
||||||
|
|
||||||
|
if (i > 0)
|
||||||
|
x += font.GetKerning(text[i - 1], c);
|
||||||
|
|
||||||
|
Raylib.DrawTextCodepoint(
|
||||||
|
rayFont,
|
||||||
|
c,
|
||||||
|
new Vector2(x, y),
|
||||||
|
font.Size,
|
||||||
|
VoileColorToRaylibColor(color)
|
||||||
|
);
|
||||||
|
|
||||||
|
var glyph = font.GetGlyph(c);
|
||||||
|
x += glyph.Advance * font.SpacingScale + font.LetterSpacing;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override int GetMonitorWidth(int monitorId)
|
protected override int GetMonitorWidth(int monitorId)
|
||||||
|
|||||||
@@ -9,9 +9,14 @@ namespace Voile;
|
|||||||
public struct Glyph
|
public struct Glyph
|
||||||
{
|
{
|
||||||
public int TextureId { get; set; } = -1;
|
public int TextureId { get; set; } = -1;
|
||||||
|
public Vector2 Offset;
|
||||||
|
|
||||||
public float Width { get; set; }
|
public float Width { get; set; }
|
||||||
public float Height { get; set; }
|
public float Height { get; set; }
|
||||||
public Vector2 Bearing { get; set; }
|
public Vector2 Bearing { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Glyph's advance in pixels.
|
||||||
|
/// </summary>
|
||||||
public int Advance { get; set; }
|
public int Advance { get; set; }
|
||||||
|
|
||||||
public Glyph() { }
|
public Glyph() { }
|
||||||
@@ -26,7 +31,10 @@ public class Font : Resource, IUpdatableResource, IDisposable
|
|||||||
/// Internal handle for the font. If it got successfully loaded into the GPU, the value will be other than -1.
|
/// Internal handle for the font. If it got successfully loaded into the GPU, the value will be other than -1.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal int Handle { get; set; } = -1;
|
internal int Handle { get; set; } = -1;
|
||||||
public int Size { get; set; } = 16;
|
public int Size { get; set; } = 24;
|
||||||
|
|
||||||
|
public float LetterSpacing { get; set; } = 0f;
|
||||||
|
public float SpacingScale { get; set; } = 0.8f; // TODO: this is a super temporary fix for character spacing. Should be fixed once custom font rendering will be implemented.
|
||||||
|
|
||||||
public byte[]? Buffer { get; private set; }
|
public byte[]? Buffer { get; private set; }
|
||||||
public long BufferSize { get; set; }
|
public long BufferSize { get; set; }
|
||||||
@@ -75,19 +83,29 @@ public class Font : Resource, IUpdatableResource, IDisposable
|
|||||||
/// <returns>A <see cref="Rect"/> with the sizes of a given text using this font.</returns>
|
/// <returns>A <see cref="Rect"/> with the sizes of a given text using this font.</returns>
|
||||||
public Rect Measure(string text)
|
public Rect Measure(string text)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(text))
|
return Measure(text.AsSpan());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Measures a given <see cref="ReadOnlySpan"/> of characters.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="chars"></param>
|
||||||
|
/// <returns>A <see cref="Rect"/> with the sizes of a given text using this font.</returns>
|
||||||
|
public Rect Measure(ReadOnlySpan<char> chars)
|
||||||
|
{
|
||||||
|
if (chars.Length == 0)
|
||||||
return Rect.Zero;
|
return Rect.Zero;
|
||||||
|
|
||||||
float totalWidth = 0;
|
float totalWidth = 0;
|
||||||
float maxAscent = 0;
|
float maxAscent = 0;
|
||||||
float maxDescent = 0;
|
float maxDescent = 0;
|
||||||
|
|
||||||
for (int i = 0; i < text.Length; i++)
|
for (int i = 0; i < chars.Length; i++)
|
||||||
{
|
{
|
||||||
char c = text[i];
|
char c = chars[i];
|
||||||
Glyph glyph = GetGlyph(c);
|
Glyph glyph = GetGlyph(c);
|
||||||
|
|
||||||
totalWidth += glyph.Advance;
|
totalWidth += glyph.Advance * SpacingScale + LetterSpacing;
|
||||||
|
|
||||||
float ascent = glyph.Bearing.Y;
|
float ascent = glyph.Bearing.Y;
|
||||||
float descent = glyph.Height - glyph.Bearing.Y;
|
float descent = glyph.Height - glyph.Bearing.Y;
|
||||||
@@ -99,7 +117,7 @@ public class Font : Resource, IUpdatableResource, IDisposable
|
|||||||
|
|
||||||
if (i > 0)
|
if (i > 0)
|
||||||
{
|
{
|
||||||
char prevChar = text[i - 1];
|
char prevChar = chars[i - 1];
|
||||||
totalWidth += GetKerning(prevChar, c);
|
totalWidth += GetKerning(prevChar, c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -112,10 +130,10 @@ public class Font : Resource, IUpdatableResource, IDisposable
|
|||||||
{
|
{
|
||||||
unsafe
|
unsafe
|
||||||
{
|
{
|
||||||
if (FacePtr == IntPtr.Zero)
|
var face = (FT_FaceRec_*)FacePtr;
|
||||||
return 0;
|
|
||||||
|
|
||||||
FT_FaceRec_* face = (FT_FaceRec_*)FacePtr;
|
if (face == null)
|
||||||
|
return 0;
|
||||||
|
|
||||||
uint leftIndex = FT_Get_Char_Index(face, left);
|
uint leftIndex = FT_Get_Char_Index(face, left);
|
||||||
uint rightIndex = FT_Get_Char_Index(face, right);
|
uint rightIndex = FT_Get_Char_Index(face, right);
|
||||||
@@ -127,7 +145,7 @@ public class Font : Resource, IUpdatableResource, IDisposable
|
|||||||
if (FT_Get_Kerning(face, leftIndex, rightIndex, FT_Kerning_Mode_.FT_KERNING_DEFAULT, &kerning) != 0)
|
if (FT_Get_Kerning(face, leftIndex, rightIndex, FT_Kerning_Mode_.FT_KERNING_DEFAULT, &kerning) != 0)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
return (int)kerning.x;
|
return (int)(kerning.x >> 6);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,31 +172,48 @@ public class Font : Resource, IUpdatableResource, IDisposable
|
|||||||
unsafe
|
unsafe
|
||||||
{
|
{
|
||||||
var face = (FT_FaceRec_*)FacePtr;
|
var face = (FT_FaceRec_*)FacePtr;
|
||||||
|
|
||||||
|
if (FacePtr == IntPtr.Zero)
|
||||||
|
return false;
|
||||||
|
|
||||||
return FT_Get_Char_Index(face, character) != 0;
|
return FT_Get_Char_Index(face, character) != 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal unsafe void InitializeFontSize()
|
||||||
|
{
|
||||||
|
var face = (FT_FaceRec_*)FacePtr;
|
||||||
|
|
||||||
|
if (face == null)
|
||||||
|
throw new Exception("Font face not initialized.");
|
||||||
|
|
||||||
|
FT_Set_Pixel_Sizes(face, 0, (uint)Size);
|
||||||
|
}
|
||||||
|
|
||||||
private unsafe Glyph LoadGlyph(char character)
|
private unsafe Glyph LoadGlyph(char character)
|
||||||
{
|
{
|
||||||
var face = (FT_FaceRec_*)FacePtr;
|
var face = (FT_FaceRec_*)FacePtr;
|
||||||
|
|
||||||
// TODO: for now, we're loading glyphs for metrics, but when implementing WebGPU rendering, we want to somehow render them to SDF or bitmap.
|
var error = FT_Load_Char(face, character, FT_LOAD_RENDER);
|
||||||
FT_Error error = FT_Set_Pixel_Sizes(face, 0, (uint)Size);
|
|
||||||
error = FT_Load_Char(face, character, FT_LOAD_NO_BITMAP);
|
|
||||||
|
|
||||||
if (error != 0)
|
if (error != 0)
|
||||||
throw new Exception($"Failed to load glyph for '{character}'");
|
throw new Exception($"Failed to load glyph for '{character}'");
|
||||||
|
|
||||||
var glyph = face->glyph;
|
var g = face->glyph;
|
||||||
var bitmap = glyph->bitmap;
|
var bitmap = g->bitmap;
|
||||||
var metrics = glyph->metrics;
|
var metrics = g->metrics;
|
||||||
|
|
||||||
return new Glyph
|
return new Glyph
|
||||||
{
|
{
|
||||||
Width = metrics.width >> 6,
|
Width = bitmap.width,
|
||||||
Height = metrics.height >> 6,
|
Height = bitmap.rows,
|
||||||
Bearing = new Vector2(metrics.horiBearingX >> 6, metrics.horiBearingY >> 6),
|
|
||||||
Advance = (int)metrics.horiAdvance >> 6,
|
Bearing = new Vector2(
|
||||||
|
metrics.horiBearingX >> 6,
|
||||||
|
metrics.horiBearingY >> 6
|
||||||
|
),
|
||||||
|
|
||||||
|
Advance = (int)(g->advance.x >> 6)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,11 +21,14 @@ public class FontLoader : ResourceLoader<Font>
|
|||||||
|
|
||||||
byte[] fileBuffer = new byte[stream.Length];
|
byte[] fileBuffer = new byte[stream.Length];
|
||||||
int bytesRead = stream.Read(fileBuffer, 0, fileBuffer.Length);
|
int bytesRead = stream.Read(fileBuffer, 0, fileBuffer.Length);
|
||||||
var result = new Font(path, fileBuffer);
|
|
||||||
|
|
||||||
|
var result = new Font(path, fileBuffer);
|
||||||
result.BufferSize = bytesRead;
|
result.BufferSize = bytesRead;
|
||||||
|
|
||||||
LoadFaceData(result);
|
LoadFaceData(result);
|
||||||
|
|
||||||
|
result.InitializeFontSize();
|
||||||
|
|
||||||
result.LoadAsciiData();
|
result.LoadAsciiData();
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
using Voile.Input;
|
||||||
using Voile.Rendering;
|
using Voile.Rendering;
|
||||||
|
|
||||||
namespace Voile.UI;
|
namespace Voile.UI;
|
||||||
@@ -18,6 +19,19 @@ public interface IElement
|
|||||||
public Rect Size { get; set; }
|
public Rect Size { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a UI element that requires a frame-independent update tick loop.
|
||||||
|
/// </summary>
|
||||||
|
public interface ITickableElement
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Excutes unconditionally on every frame engine loop step.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dt">Elapsed delta frame time in seconds.</param>
|
||||||
|
/// <param name="input">InputSystem that this tickable element should use to poll input events.</param>
|
||||||
|
void Tick(float dt, InputSystem input);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a UI element that can contain child elements.
|
/// Represents a UI element that can contain child elements.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ namespace Voile.UI;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class Style
|
public class Style
|
||||||
{
|
{
|
||||||
public enum AnimationType
|
public enum TransitionType
|
||||||
{
|
{
|
||||||
Linear,
|
Linear,
|
||||||
EaseIn,
|
EaseIn,
|
||||||
@@ -20,7 +20,7 @@ public class Style
|
|||||||
}
|
}
|
||||||
|
|
||||||
public float TransitionDuration = 0f;
|
public float TransitionDuration = 0f;
|
||||||
public AnimationType TransitionType = AnimationType.Linear;
|
public TransitionType Transition = TransitionType.Linear;
|
||||||
|
|
||||||
public Style() { }
|
public Style() { }
|
||||||
|
|
||||||
@@ -35,17 +35,39 @@ public class Style
|
|||||||
/// Merges this <see cref="Style"/> with a different one.<br />
|
/// Merges this <see cref="Style"/> with a different one.<br />
|
||||||
/// Properties that are not set for this <see cref="Style"/> will be inherited from <paramref name="overrideStyle"/>.
|
/// Properties that are not set for this <see cref="Style"/> will be inherited from <paramref name="overrideStyle"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="overrideStyle"></param>
|
/// <param name="other"></param>
|
||||||
/// <returns>A merged <see cref="Style"/>.</returns>
|
/// <returns>A merged <see cref="Style"/>.</returns>
|
||||||
public Style Merge(Style overrideStyle)
|
public Style Merge(Style other)
|
||||||
{
|
{
|
||||||
return new Style
|
return new Style
|
||||||
{
|
{
|
||||||
BackgroundColor = overrideStyle.BackgroundColor != default ? overrideStyle.BackgroundColor : BackgroundColor,
|
Padding =
|
||||||
TextColor = overrideStyle.TextColor != default ? overrideStyle.TextColor : TextColor,
|
other.Padding ?? Padding,
|
||||||
Padding = overrideStyle.Padding != default ? overrideStyle.Padding : Padding,
|
|
||||||
BorderSize = overrideStyle.BorderSize != default ? overrideStyle.BorderSize : BorderSize,
|
BackgroundColor =
|
||||||
BorderColor = overrideStyle.BorderColor != default ? overrideStyle.BorderColor : BorderColor,
|
other.BackgroundColor ?? BackgroundColor,
|
||||||
|
|
||||||
|
BorderSize =
|
||||||
|
other.BorderSize ?? BorderSize,
|
||||||
|
|
||||||
|
BorderColor =
|
||||||
|
other.BorderColor ?? BorderColor,
|
||||||
|
|
||||||
|
TextColor =
|
||||||
|
other.TextColor ?? TextColor,
|
||||||
|
|
||||||
|
CornerRadius =
|
||||||
|
other.CornerRadius != default
|
||||||
|
? other.CornerRadius
|
||||||
|
: CornerRadius,
|
||||||
|
|
||||||
|
TransitionDuration =
|
||||||
|
other.TransitionDuration != default
|
||||||
|
? other.TransitionDuration
|
||||||
|
: TransitionDuration,
|
||||||
|
|
||||||
|
Transition =
|
||||||
|
other.Transition
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -88,12 +110,12 @@ public class StyleSheetLoader : ResourceLoader<StyleSheet>
|
|||||||
{
|
{
|
||||||
var style = new Style();
|
var style = new Style();
|
||||||
|
|
||||||
string easingName = reader.GetString("TransitionType", "Linear");
|
string transitionName = reader.GetString("TransitionType", "Linear");
|
||||||
|
|
||||||
if (!Enum.TryParse<Style.AnimationType>(easingName, true, out var easing))
|
if (!Enum.TryParse<Style.TransitionType>(transitionName, true, out var transition))
|
||||||
easing = Style.AnimationType.Linear;
|
throw new ArgumentException($"\"{transitionName}\" is not a valid TransitionType.");
|
||||||
|
|
||||||
style.TransitionType = easing;
|
style.Transition = transition;
|
||||||
|
|
||||||
|
|
||||||
if (reader.HasKey("BackgroundColor"))
|
if (reader.HasKey("BackgroundColor"))
|
||||||
@@ -147,9 +169,13 @@ public class StyleSheet : Resource
|
|||||||
|
|
||||||
public void Add(string key, Style style) => _styles.Add(key, style);
|
public void Add(string key, Style style) => _styles.Add(key, style);
|
||||||
|
|
||||||
public bool TryGet(string styleName, [NotNullWhen(true)] out Style? style)
|
public bool TryGet(
|
||||||
|
string styleName,
|
||||||
|
[NotNullWhen(true)] out Style? style)
|
||||||
{
|
{
|
||||||
return _styles.TryGetValue(styleName, out style);
|
style = Resolve(styleName);
|
||||||
|
|
||||||
|
return style != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static StyleSheet Default => new(new Dictionary<string, Style>()
|
public static StyleSheet Default => new(new Dictionary<string, Style>()
|
||||||
@@ -226,5 +252,29 @@ public class StyleSheet : Resource
|
|||||||
}},
|
}},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
private Style? Resolve(string fullKey)
|
||||||
|
{
|
||||||
|
var parts = fullKey.Split('.');
|
||||||
|
|
||||||
|
Style? merged = null;
|
||||||
|
|
||||||
|
for (int i = 1; i <= parts.Length; i++)
|
||||||
|
{
|
||||||
|
var subKey = string.Join('.',
|
||||||
|
parts.Take(i));
|
||||||
|
|
||||||
|
if (_styles.TryGetValue(
|
||||||
|
subKey,
|
||||||
|
out var style))
|
||||||
|
{
|
||||||
|
merged ??= new Style();
|
||||||
|
|
||||||
|
merged = merged.Merge(style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
|
||||||
private Dictionary<string, Style> _styles = new();
|
private Dictionary<string, Style> _styles = new();
|
||||||
}
|
}
|
||||||
@@ -13,14 +13,14 @@ public class StyleAnimator
|
|||||||
_elapsed = 0f;
|
_elapsed = 0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static float Ease(float t, Style.AnimationType type)
|
public static float Ease(float t, Style.TransitionType type)
|
||||||
{
|
{
|
||||||
return type switch
|
return type switch
|
||||||
{
|
{
|
||||||
Style.AnimationType.Linear => t,
|
Style.TransitionType.Linear => t,
|
||||||
Style.AnimationType.EaseIn => t * t,
|
Style.TransitionType.EaseIn => t * t,
|
||||||
Style.AnimationType.EaseOut => t * (2 - t),
|
Style.TransitionType.EaseOut => t * (2 - t),
|
||||||
Style.AnimationType.EaseInOut => t < 0.5f
|
Style.TransitionType.EaseInOut => t < 0.5f
|
||||||
? 2 * t * t
|
? 2 * t * t
|
||||||
: -1 + (4 - 2 * t) * t,
|
: -1 + (4 - 2 * t) * t,
|
||||||
_ => t
|
_ => t
|
||||||
@@ -31,7 +31,7 @@ public class StyleAnimator
|
|||||||
{
|
{
|
||||||
_elapsed = MathF.Min(_elapsed + deltaTime, _duration);
|
_elapsed = MathF.Min(_elapsed + deltaTime, _duration);
|
||||||
float t = _duration == 0 ? 1 : _elapsed / _duration;
|
float t = _duration == 0 ? 1 : _elapsed / _duration;
|
||||||
float easedT = Ease(t, _to.TransitionType);
|
float easedT = Ease(t, _to.Transition);
|
||||||
|
|
||||||
return LerpStyle(_from, _to, easedT);
|
return LerpStyle(_from, _to, easedT);
|
||||||
}
|
}
|
||||||
@@ -45,7 +45,7 @@ public class StyleAnimator
|
|||||||
Padding = MathUtils.LerpSize(from.Padding ?? Size.Zero, to.Padding ?? Size.Zero, t),
|
Padding = MathUtils.LerpSize(from.Padding ?? Size.Zero, to.Padding ?? Size.Zero, t),
|
||||||
BorderColor = MathUtils.LerpColor(from.BorderColor ?? Color.Transparent, to.BorderColor ?? Color.Transparent, t),
|
BorderColor = MathUtils.LerpColor(from.BorderColor ?? Color.Transparent, to.BorderColor ?? Color.Transparent, t),
|
||||||
BorderSize = MathUtils.LerpSize(from.BorderSize ?? Size.Zero, to.BorderSize ?? Size.Zero, t),
|
BorderSize = MathUtils.LerpSize(from.BorderSize ?? Size.Zero, to.BorderSize ?? Size.Zero, t),
|
||||||
TransitionType = to.TransitionType
|
Transition = to.Transition
|
||||||
};
|
};
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -42,24 +42,23 @@ public abstract class UIElement : IElement, IRenderableElement, IResizeableEleme
|
|||||||
get => _size;
|
get => _size;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
if (value.Width < MinimumSize.Width)
|
float width = Math.Max(value.Width, MinimumSize.Width);
|
||||||
{
|
float height = Math.Max(value.Height, MinimumSize.Height);
|
||||||
_size.Width = MinimumSize.Width;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value.Height < MinimumSize.Height)
|
bool changed =
|
||||||
{
|
_size.Width != width ||
|
||||||
_size.Height = MinimumSize.Height;
|
_size.Height != height;
|
||||||
}
|
|
||||||
|
if (!changed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_size.Width = width;
|
||||||
|
_size.Height = height;
|
||||||
|
|
||||||
if (_size != value)
|
|
||||||
{
|
|
||||||
MarkDirty();
|
MarkDirty();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_size = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
public Vector2 AnchorOffset { get; set; } = Vector2.Zero;
|
public Vector2 AnchorOffset { get; set; } = Vector2.Zero;
|
||||||
public Anchor Anchor { get; set; } = Anchor.TopLeft;
|
public Anchor Anchor { get; set; } = Anchor.TopLeft;
|
||||||
|
|
||||||
@@ -211,6 +210,7 @@ public abstract class UIElement : IElement, IRenderableElement, IResizeableEleme
|
|||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
foreach (var modifier in modifiers)
|
foreach (var modifier in modifiers)
|
||||||
{
|
{
|
||||||
|
if (string.IsNullOrEmpty(modifier)) continue;
|
||||||
sb.Append($".{modifier}");
|
sb.Append($".{modifier}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,15 +17,15 @@ public class UIInputContext
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public Vector2 MousePosition { get; }
|
public Vector2 MousePosition { get; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Determines if a mouse button was pressed.
|
/// Determines if a left mouse button was pressed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool MousePressed { get; set; }
|
public bool MousePressed { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Determines if a mouse button was released.
|
/// Determines if a left mouse button was released.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool MouseReleased { get; set; }
|
public bool MouseReleased { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Determines if a mouse button is currently held.
|
/// Determines if a left mouse button is currently held.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool MouseDown { get; set; }
|
public bool MouseDown { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -37,6 +37,31 @@ public class UIInputContext
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int CharPressed { get; }
|
public int CharPressed { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines if the current input action corresponds to a cancel operation (ESC on the keyboard, B on the Xbox controller, etc.)
|
||||||
|
/// </summary>
|
||||||
|
public bool IsCancel => ActionName == "cancel";
|
||||||
|
/// <summary>
|
||||||
|
/// Determines if the current input action corresponds to a backspace operation. Used for text inputs.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsBackspace => ActionName == "backspace";
|
||||||
|
/// <summary>
|
||||||
|
/// Determines if the current input action corresponds to an accept/submit operation (ENTER on the keyboard, A on the Xbox controller, etc.)
|
||||||
|
/// </summary>
|
||||||
|
public bool IsAccept => ActionName == "accept";
|
||||||
|
|
||||||
|
public bool IsLeft => ActionName == "left";
|
||||||
|
public bool IsRight => ActionName == "right";
|
||||||
|
public bool IsUp => ActionName == "up";
|
||||||
|
public bool IsDown => ActionName == "down";
|
||||||
|
|
||||||
|
public float DeltaTime { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines if control key is pressed.
|
||||||
|
/// </summary>
|
||||||
|
public bool Control { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Determines if this <see cref="UIInputContext"/> registered any character input from keyboard.
|
/// Determines if this <see cref="UIInputContext"/> registered any character input from keyboard.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -39,21 +39,19 @@ public class UISystem : IUpdatableSystem, IRenderableSystem, IReloadableSystem
|
|||||||
|
|
||||||
public void Update(double deltaTime)
|
public void Update(double deltaTime)
|
||||||
{
|
{
|
||||||
// HandleInput();
|
float dt = (float)deltaTime;
|
||||||
}
|
PropagateTick(_elements, dt);
|
||||||
|
HandleInput((float)deltaTime);
|
||||||
public void Render(RenderSystem renderer)
|
|
||||||
{
|
|
||||||
// Update elements each time UI system is rendered.
|
|
||||||
HandleInput();
|
|
||||||
|
|
||||||
foreach (var element in _elements)
|
foreach (var element in _elements)
|
||||||
{
|
{
|
||||||
if (element is not IUpdatableElement updatable) continue;
|
if (element is not IUpdatableElement updatable) continue;
|
||||||
updatable.Update();
|
updatable.Update();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Render(RenderSystem renderer)
|
||||||
|
{
|
||||||
foreach (var element in _elements)
|
foreach (var element in _elements)
|
||||||
{
|
{
|
||||||
if (element is IRenderableElement renderable)
|
if (element is IRenderableElement renderable)
|
||||||
@@ -104,50 +102,90 @@ public class UISystem : IUpdatableSystem, IRenderableSystem, IReloadableSystem
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleInput()
|
private void HandleInput(float deltaTime)
|
||||||
{
|
{
|
||||||
int charPressed = _input.GetCharPressed();
|
int charPressed = _input.GetCharPressed();
|
||||||
Vector2 mousePos = _input.GetMousePosition();
|
Vector2 mousePos = _input.GetMousePosition();
|
||||||
|
|
||||||
Vector2 currentMousePosition = _input.GetMousePosition();
|
Vector2 currentMousePosition = _input.GetMousePosition();
|
||||||
|
bool inputHandled = false;
|
||||||
|
|
||||||
|
// Process Actions (Pressed or Held)
|
||||||
foreach (var (actionName, mappings) in InputSystem.InputMappings)
|
foreach (var (actionName, mappings) in InputSystem.InputMappings)
|
||||||
{
|
{
|
||||||
foreach (var action in mappings)
|
foreach (var action in mappings)
|
||||||
{
|
{
|
||||||
if (action.IsPressed(_input))
|
bool isPressed = action.IsPressed(_input);
|
||||||
|
bool isDown = action.IsDown(_input);
|
||||||
|
|
||||||
|
if (isPressed)
|
||||||
{
|
{
|
||||||
// TODO: specify which mouse button is used in the context.
|
|
||||||
var context = new UIInputContext(action, mousePos, actionName, charPressed)
|
var context = new UIInputContext(action, mousePos, actionName, charPressed)
|
||||||
{
|
{
|
||||||
MouseDown = _input.IsMouseButtonDown(MouseButton.Left),
|
MouseDown = _input.IsMouseButtonDown(MouseButton.Left),
|
||||||
MouseReleased = _input.IsMouseButtonReleased(MouseButton.Left),
|
MouseReleased = _input.IsMouseButtonReleased(MouseButton.Left),
|
||||||
MousePressed = _input.IsMouseButtonReleased(MouseButton.Left),
|
MousePressed = _input.IsMouseButtonPressed(MouseButton.Left),
|
||||||
|
DeltaTime = deltaTime
|
||||||
};
|
};
|
||||||
|
|
||||||
if (PropagateInput(_elements, context))
|
if (PropagateInput(_elements, context))
|
||||||
|
{
|
||||||
|
inputHandled = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (inputHandled) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputHandled)
|
||||||
|
{
|
||||||
|
_lastMousePosition = currentMousePosition;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (charPressed != 0)
|
if (charPressed != 0)
|
||||||
{
|
{
|
||||||
var context = new UIInputContext(new KeyInputAction(KeyboardKey.Null), mousePos, "", charPressed);
|
var context = new UIInputContext(new KeyInputAction(KeyboardKey.Null), mousePos, "", charPressed)
|
||||||
PropagateInput(_elements, context);
|
{
|
||||||
|
DeltaTime = deltaTime
|
||||||
|
};
|
||||||
|
if (PropagateInput(_elements, context))
|
||||||
|
{
|
||||||
|
_lastMousePosition = currentMousePosition;
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var frameContext = new UIInputContext(new MouseInputAction(MouseButton.Left), mousePos, "", charPressed)
|
||||||
if (currentMousePosition != _lastMousePosition)
|
|
||||||
{
|
|
||||||
// TODO: specify which mouse button is used in the context.
|
|
||||||
var context = new UIInputContext(new MouseInputAction(MouseButton.Left), mousePos, "", charPressed)
|
|
||||||
{
|
{
|
||||||
MouseDown = _input.IsMouseButtonDown(MouseButton.Left),
|
MouseDown = _input.IsMouseButtonDown(MouseButton.Left),
|
||||||
MouseReleased = _input.IsMouseButtonReleased(MouseButton.Left),
|
MouseReleased = _input.IsMouseButtonReleased(MouseButton.Left),
|
||||||
MousePressed = _input.IsMouseButtonPressed(MouseButton.Left),
|
MousePressed = _input.IsMouseButtonPressed(MouseButton.Left),
|
||||||
|
DeltaTime = deltaTime
|
||||||
};
|
};
|
||||||
PropagateInput(_elements, context);
|
|
||||||
|
PropagateInput(_elements, frameContext);
|
||||||
|
|
||||||
|
_lastMousePosition = currentMousePosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PropagateTick(List<UIElement> elements, float dt)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < elements.Count; i++)
|
||||||
|
{
|
||||||
|
var element = elements[i];
|
||||||
|
|
||||||
|
if (!element.Visible) continue;
|
||||||
|
|
||||||
|
if (element is ITickableElement tickable)
|
||||||
|
{
|
||||||
|
tickable.Tick(dt, _input);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element is IParentableElement parentable)
|
||||||
|
{
|
||||||
|
PropagateTick(parentable.Children.ToList(), dt);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,20 +195,19 @@ public class UISystem : IUpdatableSystem, IRenderableSystem, IReloadableSystem
|
|||||||
{
|
{
|
||||||
var element = elements[i];
|
var element = elements[i];
|
||||||
|
|
||||||
// if (!element.ContainsPoint(context.MousePosition)) continue;
|
|
||||||
|
|
||||||
if (element is IInputElement inputElement && !inputElement.IgnoreInput)
|
|
||||||
{
|
|
||||||
inputElement.Input(context);
|
|
||||||
_input.SetAsHandled();
|
|
||||||
// return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (element is IParentableElement parent)
|
if (element is IParentableElement parent)
|
||||||
{
|
{
|
||||||
if (PropagateInput(parent.Children.ToList(), context))
|
if (PropagateInput(parent.Children.ToList(), context))
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (element is IInputElement input && !input.IgnoreInput)
|
||||||
|
{
|
||||||
|
input.Input(context);
|
||||||
|
|
||||||
|
if (context.Handled)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using Voile.Rendering;
|
using Voile.Rendering;
|
||||||
using Voile.Resources;
|
using Voile.Resources;
|
||||||
using Voile.UI.Containers;
|
|
||||||
|
|
||||||
namespace Voile.UI.Widgets;
|
namespace Voile.UI.Widgets;
|
||||||
|
|
||||||
public enum ButtonState
|
|
||||||
{
|
|
||||||
Disabled,
|
|
||||||
Hovered,
|
|
||||||
Pressed,
|
|
||||||
Normal
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A clickable button with a label.
|
/// A clickable button with a label.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class Button : Widget
|
public class Button : Widget
|
||||||
{
|
{
|
||||||
|
public enum ButtonState
|
||||||
|
{
|
||||||
|
Disabled,
|
||||||
|
Hovered,
|
||||||
|
Pressed,
|
||||||
|
Focused,
|
||||||
|
Normal
|
||||||
|
}
|
||||||
|
|
||||||
public string Text
|
public string Text
|
||||||
{
|
{
|
||||||
get => _text; set
|
get => _text; set
|
||||||
@@ -93,8 +93,12 @@ public class Button : Widget
|
|||||||
|
|
||||||
var textPosition = new Vector2(GlobalPosition.X + Padding.Left, GlobalPosition.Y + Padding.Top);
|
var textPosition = new Vector2(GlobalPosition.X + Padding.Left, GlobalPosition.Y + Padding.Top);
|
||||||
renderer.SetTransform(textPosition, Vector2.Zero);
|
renderer.SetTransform(textPosition, Vector2.Zero);
|
||||||
|
|
||||||
|
if (_suitableFont.HasValue)
|
||||||
|
{
|
||||||
renderer.DrawText(_suitableFont, _text, textColor);
|
renderer.DrawText(_suitableFont, _text, textColor);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected virtual void Pressed() { }
|
protected virtual void Pressed() { }
|
||||||
|
|
||||||
@@ -138,6 +142,7 @@ public class Button : Widget
|
|||||||
protected override void OnUpdate()
|
protected override void OnUpdate()
|
||||||
{
|
{
|
||||||
ResourceRef<Font> fontRef = ResourceRef<Font>.Empty();
|
ResourceRef<Font> fontRef = ResourceRef<Font>.Empty();
|
||||||
|
|
||||||
foreach (var c in _text)
|
foreach (var c in _text)
|
||||||
{
|
{
|
||||||
if (FontSet.TryGetFontFor(c, out var fallbackFont))
|
if (FontSet.TryGetFontFor(c, out var fallbackFont))
|
||||||
@@ -149,10 +154,13 @@ public class Button : Widget
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (fontRef.HasValue)
|
||||||
|
{
|
||||||
_suitableFont = fontRef;
|
_suitableFont = fontRef;
|
||||||
|
|
||||||
var font = _suitableFont.Value;
|
var font = _suitableFont.Value;
|
||||||
_textSize = font.Measure(_text);
|
_textSize = font.Measure(_text);
|
||||||
|
}
|
||||||
|
|
||||||
Size = _padding + _textSize;
|
Size = _padding + _textSize;
|
||||||
}
|
}
|
||||||
|
|||||||
289
Voile/Source/UI/Widgets/InputField.cs
Normal file
289
Voile/Source/UI/Widgets/InputField.cs
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
using System.Numerics;
|
||||||
|
using Voile.Input;
|
||||||
|
using Voile.Rendering;
|
||||||
|
using Voile.Resources;
|
||||||
|
|
||||||
|
namespace Voile.UI.Widgets;
|
||||||
|
|
||||||
|
public class InputField : Widget, ITickableElement
|
||||||
|
{
|
||||||
|
enum ActionType { None, Backspace, Left, Right }
|
||||||
|
|
||||||
|
public override Rect MinimumSize => _placeholderSize + _padding;
|
||||||
|
public override string? StyleElementName => nameof(InputField);
|
||||||
|
|
||||||
|
public override string[]? StyleModifiers =>
|
||||||
|
[
|
||||||
|
_isFocused ? "Focused" : string.Empty
|
||||||
|
];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Current text value of this input field.
|
||||||
|
/// </summary>
|
||||||
|
public string Value
|
||||||
|
{
|
||||||
|
get => _input;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_input = value ?? string.Empty;
|
||||||
|
_cursor = Math.Clamp(_cursor, 0, _input.Length);
|
||||||
|
MarkDirty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string PlaceholderText { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public bool IsFocused => _isFocused;
|
||||||
|
|
||||||
|
public FontSet FontSet { get; set; } = new();
|
||||||
|
|
||||||
|
public InputField(string initialText, FontSet fontSet)
|
||||||
|
{
|
||||||
|
_input = initialText ?? string.Empty;
|
||||||
|
FontSet = fontSet;
|
||||||
|
|
||||||
|
MarkDirty();
|
||||||
|
Update();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Tick(float dt, InputSystem input)
|
||||||
|
{
|
||||||
|
if (!_isFocused)
|
||||||
|
return;
|
||||||
|
|
||||||
|
HandleRepeatingActions(dt, input);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnInput(UIInputContext context)
|
||||||
|
{
|
||||||
|
HandleFocus(context);
|
||||||
|
|
||||||
|
if (!_isFocused)
|
||||||
|
{
|
||||||
|
_lastActiveAction = ActionType.None;
|
||||||
|
_repeatTimer = 0.0f;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
HandleTextInput(context);
|
||||||
|
ClampCursor();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void HandleFocus(UIInputContext context)
|
||||||
|
{
|
||||||
|
if (!context.MousePressed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
bool isInside = ContainsPoint(context.MousePosition);
|
||||||
|
|
||||||
|
_isFocused = isInside;
|
||||||
|
|
||||||
|
if (isInside)
|
||||||
|
context.SetHandled();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleTextInput(UIInputContext context)
|
||||||
|
{
|
||||||
|
if (!context.HasCharInput)
|
||||||
|
return;
|
||||||
|
|
||||||
|
char c = (char)context.CharPressed;
|
||||||
|
|
||||||
|
if (!char.IsControl(c))
|
||||||
|
{
|
||||||
|
_input = _input.Insert(_cursor, c.ToString());
|
||||||
|
_cursor++;
|
||||||
|
MarkDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
context.SetHandled();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleRepeatingActions(float dt, InputSystem input)
|
||||||
|
{
|
||||||
|
ActionType currentAction = ActionType.None;
|
||||||
|
|
||||||
|
if (input.IsKeyboardKeyDown(KeyboardKey.Backspace)) currentAction = ActionType.Backspace;
|
||||||
|
else if (input.IsKeyboardKeyDown(KeyboardKey.Left)) currentAction = ActionType.Left;
|
||||||
|
else if (input.IsKeyboardKeyDown(KeyboardKey.Right)) currentAction = ActionType.Right;
|
||||||
|
|
||||||
|
if (currentAction == ActionType.None)
|
||||||
|
{
|
||||||
|
_lastActiveAction = ActionType.None;
|
||||||
|
_repeatTimer = 0.0f;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool shouldExecute = false;
|
||||||
|
|
||||||
|
if (_lastActiveAction != currentAction)
|
||||||
|
{
|
||||||
|
shouldExecute = true;
|
||||||
|
_repeatTimer = 0.0f;
|
||||||
|
_lastActiveAction = currentAction;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_repeatTimer += dt;
|
||||||
|
|
||||||
|
if (_repeatTimer >= INITIAL_DELAY)
|
||||||
|
{
|
||||||
|
shouldExecute = true;
|
||||||
|
_repeatTimer = INITIAL_DELAY - REPEAT_RATE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldExecute)
|
||||||
|
{
|
||||||
|
ExecuteAction(currentAction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ExecuteAction(ActionType action)
|
||||||
|
{
|
||||||
|
switch (action)
|
||||||
|
{
|
||||||
|
case ActionType.Backspace:
|
||||||
|
if (_cursor > 0 && _input.Length > 0)
|
||||||
|
{
|
||||||
|
_input = _input.Remove(_cursor - 1, 1);
|
||||||
|
_cursor--;
|
||||||
|
MarkDirty();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ActionType.Left:
|
||||||
|
if (_cursor > 0)
|
||||||
|
{
|
||||||
|
_cursor--;
|
||||||
|
MarkDirty();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ActionType.Right:
|
||||||
|
if (_cursor < _input.Length)
|
||||||
|
{
|
||||||
|
_cursor++;
|
||||||
|
MarkDirty();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClampCursor()
|
||||||
|
{
|
||||||
|
_cursor = Math.Clamp(_cursor, 0, _input.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnRender(RenderSystem renderer, Style style)
|
||||||
|
{
|
||||||
|
if (_padding != style.Padding)
|
||||||
|
{
|
||||||
|
MarkDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
_padding = style.Padding ?? Voile.Size.Zero;
|
||||||
|
|
||||||
|
var textColor = style.TextColor ?? Color.Black;
|
||||||
|
var caretColor = style.BorderColor ?? Color.Black;
|
||||||
|
|
||||||
|
var textPosition = new Vector2(GlobalPosition.X + _padding.Left, GlobalPosition.Y + _padding.Top);
|
||||||
|
renderer.SetTransform(textPosition, Vector2.Zero);
|
||||||
|
|
||||||
|
// Placeholder
|
||||||
|
// TODO: use a color from the style instead of making it less transparent.
|
||||||
|
if (string.IsNullOrEmpty(_input) && !string.IsNullOrEmpty(PlaceholderText))
|
||||||
|
{
|
||||||
|
var placeholderColor = textColor.Lightened(0.5f);
|
||||||
|
|
||||||
|
if (_suitableFont.HasValue)
|
||||||
|
{
|
||||||
|
renderer.DrawText(_suitableFont, PlaceholderText, placeholderColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (_suitableFont.HasValue)
|
||||||
|
{
|
||||||
|
renderer.DrawText(_suitableFont, _input, textColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Caret
|
||||||
|
if (_isFocused && _blink)
|
||||||
|
{
|
||||||
|
var caretX = MeasureTextWidth(_input.AsSpan(0, _cursor));
|
||||||
|
|
||||||
|
renderer.SetTransform(
|
||||||
|
new Vector2(GlobalPosition.X + caretX + _padding.Left, GlobalPosition.Y + _padding.Top),
|
||||||
|
Vector2.Zero
|
||||||
|
);
|
||||||
|
|
||||||
|
var caretHeight = Math.Max(_placeholderSize.Height, _textSize.Height);
|
||||||
|
|
||||||
|
renderer.DrawRectangle(
|
||||||
|
new Vector2(1, caretHeight),
|
||||||
|
caretColor
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnUpdate()
|
||||||
|
{
|
||||||
|
ResourceRef<Font> fontRef = ResourceRef<Font>.Empty();
|
||||||
|
|
||||||
|
var text = string.IsNullOrEmpty(_input) ? PlaceholderText : _input;
|
||||||
|
|
||||||
|
foreach (var c in text)
|
||||||
|
{
|
||||||
|
if (FontSet.TryGetFontFor(c, out var fallbackFont))
|
||||||
|
{
|
||||||
|
fontRef = fallbackFont;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fontRef.HasValue)
|
||||||
|
{
|
||||||
|
_suitableFont = fontRef;
|
||||||
|
var font = _suitableFont.Value;
|
||||||
|
|
||||||
|
_textSize = font.Measure(_input);
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(_input))
|
||||||
|
_placeholderSize = font.Measure(PlaceholderText);
|
||||||
|
}
|
||||||
|
|
||||||
|
var size = Rect.MaxWidth(_placeholderSize, _textSize);
|
||||||
|
|
||||||
|
Size = _padding + size;
|
||||||
|
}
|
||||||
|
|
||||||
|
private float MeasureTextWidth(ReadOnlySpan<char> chars)
|
||||||
|
{
|
||||||
|
if (!_suitableFont.HasValue)
|
||||||
|
{
|
||||||
|
return 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _suitableFont.Value.Measure(chars).Width;
|
||||||
|
}
|
||||||
|
|
||||||
|
private float _repeatTimer = 0.0f;
|
||||||
|
private ActionType _lastActiveAction = ActionType.None;
|
||||||
|
private const float INITIAL_DELAY = 0.5f;
|
||||||
|
private const float REPEAT_RATE = 0.05f;
|
||||||
|
|
||||||
|
private string _input = string.Empty;
|
||||||
|
private bool _isFocused;
|
||||||
|
|
||||||
|
private int _cursor;
|
||||||
|
|
||||||
|
private bool _blink = true;
|
||||||
|
|
||||||
|
private ResourceRef<Font> _suitableFont = ResourceRef<Font>.Empty();
|
||||||
|
|
||||||
|
private Size _padding = Voile.Size.Zero;
|
||||||
|
private Rect _textSize = Rect.Zero;
|
||||||
|
private Rect _placeholderSize = Rect.Zero;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user