diff --git a/.gitignore b/.gitignore
index 6e201df0..32b6feb7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -400,3 +400,4 @@ FodyWeavers.xsd
# CnCNet
/Compiled
.idea/
+!/References/*
\ No newline at end of file
diff --git a/FontManagement/FontManager.cs b/FontManagement/FontManager.cs
index c85059c0..a3161f32 100644
--- a/FontManagement/FontManager.cs
+++ b/FontManagement/FontManager.cs
@@ -7,6 +7,10 @@
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using Rampastring.Tools;
+#if !XNA
+using Forme;
+using Forme.MonoGame;
+#endif
namespace Rampastring.XNAUI.FontManagement;
@@ -218,6 +222,12 @@ private static void CreateFontIndexesFromIni(IniFile iniFile, ContentManager con
string sfName = Path.GetFileNameWithoutExtension(config.Path);
LoadSpriteFont(contentManager, searchPath, sfName);
break;
+
+#if !XNA
+ case FontType.Forme:
+ CreateFormeFontIndex(i, config, searchPath);
+ break;
+#endif
}
}
}
@@ -337,6 +347,79 @@ public FontConfig(string path, int size, FontType fontType, int fallback)
}
}
+#if !XNA
+ ///
+ /// Creates a Forme GPU font index from a TTF or .forme file.
+ ///
+ ///
+ ///
+ /// If a pre-processed .forme file (same base name as the TTF, with .forme
+ /// extension) exists in the same directory, it is loaded instead of the TTF to avoid
+ /// reprocessing on every launch. Otherwise the TTF is processed at startup and, if the
+ /// directory is writable, the result is saved as a .forme file for future runs.
+ ///
+ ///
+ /// Glyphs are baked for (Unicode Basic Latin,
+ /// U+0020–U+007F). To support a wider character set, pre-process the font with a custom
+ /// using FormeFont.FromTtf and save it as a
+ /// .forme file next to the TTF.
+ ///
+ ///
+ private static void CreateFormeFontIndex(int fontIndex, FontConfig config, string searchPath)
+ {
+ if (string.IsNullOrEmpty(config.Path))
+ {
+ Logger.Log($"FontManager: Font{fontIndex} - Forme font has no path configured, skipping");
+ return;
+ }
+
+ string ttfFullPath = SafePath.GetFile(searchPath, config.Path).FullName;
+ if (!File.Exists(ttfFullPath))
+ {
+ Logger.Log($"FontManager: Font{fontIndex} - Forme font file not found: {ttfFullPath}");
+ return;
+ }
+
+ try
+ {
+ // Look for a pre-processed .forme file alongside the TTF.
+ string formePath = Path.ChangeExtension(ttfFullPath, ".forme");
+ FormeFont formeFont;
+
+ if (File.Exists(formePath))
+ {
+ formeFont = FormeFont.FromFile(formePath);
+ Logger.Log($"FontManager: Font{fontIndex} - Loaded pre-processed font from {formePath}");
+ }
+ else
+ {
+ byte[] ttfData = File.ReadAllBytes(ttfFullPath);
+ formeFont = FormeFont.FromTtf(ttfData, CharacterSet.BasicLatin);
+ Logger.Log($"FontManager: Font{fontIndex} - Processed TTF font from {ttfFullPath}");
+
+ // Try to save the processed font for faster future loads.
+ try
+ {
+ formeFont.Save(formePath);
+ Logger.Log($"FontManager: Font{fontIndex} - Saved processed font to {formePath}");
+ }
+ catch (Exception ex)
+ {
+ Logger.Log($"FontManager: Font{fontIndex} - Could not save processed font to {formePath}: {ex.Message}");
+ }
+ }
+
+ var fontDevice = new FormeFontDevice(Renderer.GraphicsDevice, formeFont);
+ fonts.Add(new FormeFontWrapper(fontDevice, Renderer.FormeRenderer, config.Size));
+ Logger.Log($"FontManager: Created FontIndex {fonts.Count - 1}: Forme size {config.Size} ({Path.GetFileName(config.Path)})");
+ }
+ catch (Exception ex)
+ {
+ Logger.Log($"FontManager: Font{fontIndex} - Failed to create Forme font from {config.Path}: {ex.Message}");
+ }
+ }
+#endif
+
///
/// Loads a SpriteFont and adds it to the font list.
///
diff --git a/FontManagement/FontType.cs b/FontManagement/FontType.cs
index b518c2fb..3ab0d9da 100644
--- a/FontManagement/FontType.cs
+++ b/FontManagement/FontType.cs
@@ -3,5 +3,6 @@ namespace Rampastring.XNAUI.FontManagement;
public enum FontType
{
SpriteFont,
- TrueType
+ TrueType,
+ Forme
}
diff --git a/FontManagement/FormeFontWrapper.cs b/FontManagement/FormeFontWrapper.cs
new file mode 100644
index 00000000..0832cd26
--- /dev/null
+++ b/FontManagement/FormeFontWrapper.cs
@@ -0,0 +1,131 @@
+#if !XNA
+using FontStashSharp;
+using Forme;
+using Forme.MonoGame;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+using System;
+
+namespace Rampastring.XNAUI.FontManagement;
+
+///
+/// A font wrapper that renders text using the Forme GPU-accelerated Slug algorithm.
+///
+///
+///
+/// Forme renders text directly from quadratic Bezier glyph outlines on the GPU without
+/// precomputed textures, distance fields, or rasterized atlases. Text remains sharp at
+/// any size and scale.
+///
+///
+/// Because Forme uses its own renderer rather than SpriteBatch, each
+/// call
+/// flushes the active SpriteBatch, draws with , then
+/// restarts the SpriteBatch. This preserves correct draw order at the cost of one
+/// extra flush per text call.
+///
+///
+public class FormeFontWrapper : IFont, IDisposable
+{
+ private readonly FormeFontDevice _fontDevice;
+ private readonly FormeRenderer _renderer;
+ private readonly int _sizePixels;
+
+ // Ascent in pixels at size 1.0: multiply by (sizePixels * scale) when drawing.
+ private readonly float _ascentRatio;
+
+ private bool _disposed;
+
+ ///
+ /// Initializes a new .
+ ///
+ /// The GPU font device that owns the curve and band textures.
+ /// The shared used for drawing.
+ /// The nominal em-square height in pixels for this font size.
+ public FormeFontWrapper(FormeFontDevice fontDevice, FormeRenderer renderer, int sizePixels)
+ {
+ _fontDevice = fontDevice ?? throw new ArgumentNullException(nameof(fontDevice));
+ _renderer = renderer ?? throw new ArgumentNullException(nameof(renderer));
+ _sizePixels = sizePixels;
+
+ int unitsPerEm = Math.Max(1, fontDevice.Metrics.UnitsPerEm);
+ _ascentRatio = (float)fontDevice.Metrics.Ascent / unitsPerEm;
+ }
+
+ ///
+ public Vector2 MeasureString(string text)
+ {
+ if (string.IsNullOrEmpty(text))
+ return Vector2.Zero;
+
+ var bounds = _fontDevice.Font.MeasureString(text.AsSpan(), (float)_sizePixels);
+ return new Vector2(bounds.Width, bounds.Height);
+ }
+
+ ///
+ ///
+ /// The is interpreted as the top-left corner of the text,
+ /// consistent with the rest of the XNAUI framework. Internally this is converted to a
+ /// baseline origin as required by .
+ ///
+ public void DrawString(SpriteBatch spriteBatch, string text, Vector2 location, Color color, float scale, float depth)
+ {
+ if (string.IsNullOrEmpty(text))
+ return;
+
+ // Forme expects a baseline origin; convert from the top-left corner used by XNAUI.
+ float ascentPixels = _ascentRatio * _sizePixels * scale;
+ Vector2 baseline = new Vector2(location.X, location.Y + ascentPixels);
+
+ // Flush the active SpriteBatch before using FormeRenderer, then restart it.
+ // FormeRenderer.Begin/End save and restore all GraphicsDevice state, so the
+ // SpriteBatch can resume with Begin() using the same settings.
+ Renderer.EndDraw();
+
+ _renderer.Begin();
+ // Note: explicitly passing through `new TextLayoutOptions()` to workaround the following issue:
+ // '\n' in DrawString doesn't work without TextLayoutOptions #12
+ // https://github.com/AristurtleDev/Forme/issues/12
+ _renderer.DrawString(_fontDevice, text, baseline, _sizePixels * scale, color, options: new TextLayoutOptions());
+ _renderer.End();
+
+ Renderer.BeginDraw();
+ }
+
+ ///
+ public void DrawString(SpriteBatch spriteBatch, StringSegment text, Vector2 location, Color color, float rotation, Vector2 origin, Vector2 scale, float depth)
+ {
+ // StringSegment overload: convert to string and delegate.
+ // Rotation is not supported by FormeRenderer. For non-uniform scale, the larger
+ // axis is used so that text is never clipped.
+ float uniformScale = Math.Max(scale.X, scale.Y);
+ DrawString(spriteBatch, text.ToString(), location, color, uniformScale, depth);
+ }
+
+ ///
+ ///
+ /// Always returns . Forme skips glyphs not present in the baked
+ /// character set rather than substituting a replacement character, so missing characters
+ /// are simply not rendered.
+ ///
+ public bool HasCharacter(char c) => true;
+
+ ///
+ ///
+ /// Returns unchanged. Forme handles missing glyphs silently by
+ /// skipping them, so no character substitution is needed.
+ ///
+ public string GetSafeString(string str) => str;
+
+ ///
+ public void Dispose()
+ {
+ if (_disposed)
+ return;
+
+ _disposed = true;
+ _fontDevice.Dispose();
+ GC.SuppressFinalize(this);
+ }
+}
+#endif
diff --git a/Rampastring.XNAUI.csproj b/Rampastring.XNAUI.csproj
index b07f0c91..40b30758 100644
--- a/Rampastring.XNAUI.csproj
+++ b/Rampastring.XNAUI.csproj
@@ -104,13 +104,11 @@
-
-
+
-
-
+
@@ -196,4 +194,9 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/References/CnCNet.Forme.MonoGame.Content.Pipeline.NET48.0.0.3.1.nupkg b/References/CnCNet.Forme.MonoGame.Content.Pipeline.NET48.0.0.3.1.nupkg
new file mode 100644
index 00000000..e2def9af
Binary files /dev/null and b/References/CnCNet.Forme.MonoGame.Content.Pipeline.NET48.0.0.3.1.nupkg differ
diff --git a/References/CnCNet.Forme.MonoGame.NET48.0.0.3.1.nupkg b/References/CnCNet.Forme.MonoGame.NET48.0.0.3.1.nupkg
new file mode 100644
index 00000000..e17c2359
Binary files /dev/null and b/References/CnCNet.Forme.MonoGame.NET48.0.0.3.1.nupkg differ
diff --git a/References/CnCNet.Forme.NET48.0.0.3.1.nupkg b/References/CnCNet.Forme.NET48.0.0.3.1.nupkg
new file mode 100644
index 00000000..c166c952
Binary files /dev/null and b/References/CnCNet.Forme.NET48.0.0.3.1.nupkg differ
diff --git a/Renderer.cs b/Renderer.cs
index 42e8271e..53832eae 100644
--- a/Renderer.cs
+++ b/Renderer.cs
@@ -5,6 +5,9 @@
using Microsoft.Xna.Framework.Content;
using FontStashSharp;
using Rampastring.XNAUI.FontManagement;
+#if !XNA
+using Forme.MonoGame;
+#endif
#if XNA
using System.Reflection;
#endif
@@ -40,12 +43,31 @@ public static class Renderer
internal static SpriteBatchSettings CurrentSettings;
+#if !XNA
+ ///
+ /// Gets the used for rendering.
+ /// Available after has been called.
+ ///
+ internal static GraphicsDevice GraphicsDevice { get; private set; }
+
+ ///
+ /// Gets the shared used by .
+ /// Available after has been called.
+ ///
+ internal static FormeRenderer FormeRenderer { get; private set; }
+#endif
+
public static SpriteBatchSettings GetCurrentSettings() => CurrentSettings;
public static void Initialize(GraphicsDevice gd, ContentManager content)
{
spriteBatch = new SpriteBatch(gd);
+#if !XNA
+ GraphicsDevice = gd;
+ FormeRenderer = new FormeRenderer(gd);
+#endif
+
FontManager.Initialize();
FontManager.LoadFonts(content);
diff --git a/nuget.config b/nuget.config
index aa5beec8..546b02c0 100644
--- a/nuget.config
+++ b/nuget.config
@@ -1,5 +1,8 @@
+
+
+
-
+
\ No newline at end of file