UI Scaling Support. (#809)

Not quite perfect, but quite usable.

Adds the ability to scale the UI. Scaling is controlled via a cvar,
and can be changed at runtime.
Fractional scaling is supported.

Some controls could be better (SpriteView, TextureRect),
but for now it's a good start.
This commit is contained in:
Pieter-Jan Briers
2019-05-11 16:04:14 +02:00
committed by GitHub
parent c4fb96b5e8
commit 17ebeac107
31 changed files with 611 additions and 376 deletions
+58 -25
View File
@@ -15,30 +15,39 @@ namespace Robust.Client.Graphics
public abstract class Font
{
/// <summary>
/// The maximum amount a glyph goes above the baseline.
/// The maximum amount a glyph goes above the baseline, in pixels.
/// </summary>
public virtual int Ascent => default;
public abstract int GetAscent(float scale);
/// <summary>
/// The maximum glyph height of a line of text, not relative to the baseline.
/// The maximum glyph height of a line of text in pixels, not relative to the baseline.
/// </summary>
public virtual int Height => default;
public abstract int GetHeight(float scale);
/// <summary>
/// The maximum amount a glyph drops below the baseline.
/// The maximum amount a glyph drops below the baseline, in pixels.
/// </summary>
public virtual int Descent => default;
public abstract int GetDescent(float scale);
/// <summary>
/// The distance between the baselines of two consecutive lines.
/// The distance between the baselines of two consecutive lines, in pixels.
/// Basically, if you encounter a new line, this is how much you need to move down the cursor.
/// </summary>
public virtual int LineHeight => Height;
public abstract int GetLineHeight(float scale);
/// <summary>
/// The distance between the edges of two consecutive lines.
/// The distance between the edges of two consecutive lines, in pixels.
/// </summary>
public int LineSeparation => LineHeight - Height;
public int GetLineSeparation(float scale)
{
return GetLineHeight(scale) - GetHeight(scale);
}
[Obsolete("Use GetAscent")] public int Ascent => GetAscent(1);
[Obsolete("Use GetHeight")] public int Height => GetHeight(1);
[Obsolete("Use GetDescent")] public int Descent => GetDescent(1);
[Obsolete("Use GetLineHeight")] public int LineHeight => GetLineHeight(1);
[Obsolete("Use GetLineSeparation")] public int LineSeparation => GetLineSeparation(1);
// Yes, I am aware that using char is bad.
// At the same time the font system is nowhere close to rendering Unicode so...
@@ -53,7 +62,14 @@ namespace Robust.Client.Graphics
/// <param name="baseline">The baseline from which to draw the character.</param>
/// <param name="color">The color of the character to draw.</param>
/// <returns>How much to advance the cursor to draw the next character.</returns>
public abstract float DrawChar(DrawingHandleScreen handle, char chr, Vector2 baseline, Color color);
[Obsolete("Use DrawChar with scale support.")]
public float DrawChar(DrawingHandleScreen handle, char chr, Vector2 baseline, Color color)
{
return DrawChar(handle, chr, baseline, 1, color);
}
public abstract float DrawChar(DrawingHandleScreen handle, char chr, Vector2 baseline, float scale,
Color color);
/// <summary>
/// Gets metrics describing the dimensions and positioning of a single glyph in the font.
@@ -64,14 +80,26 @@ namespace Robust.Client.Graphics
/// otherwise the metrics you asked for.
/// </returns>
/// <seealso cref="TryGetCharMetrics"/>
public abstract CharMetrics? GetCharMetrics(char chr);
[Obsolete("Use GetCharMetrics with scale support.")]
public CharMetrics? GetCharMetrics(char chr)
{
return GetCharMetrics(chr, 1);
}
public abstract CharMetrics? GetCharMetrics(char chr, float scale);
/// <summary>
/// Try-pattern version of <see cref="GetCharMetrics"/>.
/// </summary>
[Obsolete("Use TryGetCharMetrics with scale support.")]
public bool TryGetCharMetrics(char chr, out CharMetrics metrics)
{
var maybe = GetCharMetrics(chr);
return TryGetCharMetrics(chr, 1, out metrics);
}
public bool TryGetCharMetrics(char chr, float scale, out CharMetrics metrics)
{
var maybe = GetCharMetrics(chr, scale);
if (maybe.HasValue)
{
metrics = maybe.Value;
@@ -92,26 +120,26 @@ namespace Robust.Client.Graphics
internal IFontInstanceHandle Handle { get; }
public override int Ascent => Handle?.Ascent ?? base.Ascent;
public override int Descent => Handle?.Descent ?? base.Descent;
public override int Height => Handle?.Height ?? base.Height;
public override int LineHeight => Handle?.LineHeight ?? base.LineHeight;
public VectorFont(FontResource res, int size)
{
Size = size;
Handle = IoCManager.Resolve<IFontManagerInternal>().MakeInstance(res.FontFaceHandle, size);
}
public override float DrawChar(DrawingHandleScreen handle, char chr, Vector2 baseline, Color color)
public override int GetAscent(float scale) => Handle.GetAscent(scale);
public override int GetHeight(float scale) => Handle.GetHeight(scale);
public override int GetDescent(float scale) => Handle.GetDescent(scale);
public override int GetLineHeight(float scale) => Handle.GetLineHeight(scale);
public override float DrawChar(DrawingHandleScreen handle, char chr, Vector2 baseline, float scale, Color color)
{
var metrics = Handle.GetCharMetrics(chr);
var metrics = Handle.GetCharMetrics(chr, scale);
if (!metrics.HasValue)
{
return 0;
}
var texture = Handle.GetCharTexture(chr);
var texture = Handle.GetCharTexture(chr, scale);
if (texture == null)
{
return metrics.Value.Advance;
@@ -122,21 +150,26 @@ namespace Robust.Client.Graphics
return metrics.Value.Advance;
}
public override CharMetrics? GetCharMetrics(char chr)
public override CharMetrics? GetCharMetrics(char chr, float scale)
{
return Handle.GetCharMetrics(chr);
return Handle.GetCharMetrics(chr, scale);
}
}
public sealed class DummyFont : Font
{
public override float DrawChar(DrawingHandleScreen handle, char chr, Vector2 baseline, Color color)
public override int GetAscent(float scale) => default;
public override int GetHeight(float scale) => default;
public override int GetDescent(float scale) => default;
public override int GetLineHeight(float scale) => default;
public override float DrawChar(DrawingHandleScreen handle, char chr, Vector2 baseline, float scale, Color color)
{
// Nada, it's a dummy after all.
return 0;
}
public override CharMetrics? GetCharMetrics(char chr)
public override CharMetrics? GetCharMetrics(char chr, float scale)
{
// Nada, it's a dummy after all.
return null;
+153 -87
View File
@@ -18,7 +18,7 @@ namespace Robust.Client.Graphics
{
[Dependency] private readonly IConfigurationManager _configuration;
private uint FontDPI;
private uint BaseFontDPI;
private readonly Library _library;
@@ -32,12 +32,14 @@ namespace Robust.Client.Graphics
public IFontFaceHandle Load(ReadOnlySpan<byte> data)
{
unsafe
{
var face = new Face(_library, data.ToArray(), 0);
var handle = new FontFaceHandle(face);
return handle;
}
var face = new Face(_library, data.ToArray(), 0);
var handle = new FontFaceHandle(face);
return handle;
}
void IFontManagerInternal.Initialize()
{
BaseFontDPI = (uint) _configuration.GetCVar<int>("display.fontdpi");
}
public IFontInstanceHandle MakeInstance(IFontFaceHandle handle, int size)
@@ -48,50 +50,63 @@ namespace Robust.Client.Graphics
return instance;
}
var face = fontFaceHandle.Face;
var (atlasData, glyphMap, metricsMap) = _generateAtlas(face, size);
var ascent = face.Size.Metrics.Ascender.ToInt32();
var descent = -face.Size.Metrics.Descender.ToInt32();
var height = face.Size.Metrics.Height.ToInt32();
var instanceHandle = new FontInstanceHandle(this, atlasData, size, fontFaceHandle.Face, glyphMap, ascent,
descent, height, metricsMap);
_loadedInstances.Add((fontFaceHandle, size), instanceHandle);
return instanceHandle;
var glyphMap = _generateGlyphMap(fontFaceHandle.Face);
instance = new FontInstanceHandle(this, size, glyphMap, fontFaceHandle);
_loadedInstances.Add((fontFaceHandle, size), instance);
return instance;
}
void IFontManagerInternal.Initialize()
private ScaledFontData _generateScaledDatum(FontInstanceHandle instance, float scale)
{
FontDPI = (uint) _configuration.GetCVar<int>("display.fontdpi");
var ftFace = instance.FaceHandle.Face;
ftFace.SetCharSize(0, instance.Size, 0, (uint) (BaseFontDPI * scale));
var ascent = ftFace.Size.Metrics.Ascender.ToInt32();
var descent = -ftFace.Size.Metrics.Descender.ToInt32();
var lineHeight = ftFace.Size.Metrics.Height.ToInt32();
var (atlas, metricsMap) = _generateAtlas(instance, scale);
return new ScaledFontData(metricsMap, ascent, descent, ascent + descent, lineHeight, atlas);
}
private (FontTextureAtlas, Dictionary<char, uint> glyphMap, Dictionary<uint, CharMetrics> metricsMap)
_generateAtlas(Face face, int size)
private (FontTextureAtlas, Dictionary<uint, CharMetrics> metricsMap)
_generateAtlas(FontInstanceHandle instance, float scale)
{
// TODO: This could use a better box packing algorithm.
// Right now we treat each glyph bitmap as having the max size among all glyphs.
// So we can divide the atlas into equal-size rectangles.
// This wastes a lot of space though because there's a lot of tiny glyphs.
face.SetCharSize(0, size, 0, FontDPI);
var face = instance.FaceHandle.Face;
var maxGlyphSize = Vector2i.Zero;
var count = 0;
// TODO: Render more than extended ASCII, somehow. Does it make sense to just render every glyph in the font?
// Render all the extended ASCII characters.
const uint startIndex = 32;
const uint endIndex = 255;
for (var i = startIndex; i <= endIndex; i++)
var metricsMap = new Dictionary<uint, CharMetrics>();
foreach (var glyph in instance.GlyphMap.Values)
{
var glyphIndex = face.GetCharIndex(i);
if (glyphIndex == 0)
if (metricsMap.ContainsKey(glyph))
{
continue;
}
face.LoadChar(i, LoadFlags.Default, LoadTarget.Normal);
face.LoadGlyph(glyph, LoadFlags.Default, LoadTarget.Normal);
face.Glyph.RenderGlyph(RenderMode.Normal);
var glyphMetrics = face.Glyph.Metrics;
var metrics = new CharMetrics(glyphMetrics.HorizontalBearingX.ToInt32(),
glyphMetrics.HorizontalBearingY.ToInt32(),
glyphMetrics.HorizontalAdvance.ToInt32(),
glyphMetrics.Width.ToInt32(),
glyphMetrics.Height.ToInt32());
metricsMap.Add(glyph, metrics);
maxGlyphSize = Vector2i.ComponentMax(maxGlyphSize,
new Vector2i(face.Glyph.Bitmap.Width, face.Glyph.Bitmap.Rows));
count += 1;
}
@@ -107,33 +122,12 @@ namespace Robust.Client.Graphics
(int) Math.Round(atlasEntriesVertical * maxGlyphSize.Y / 4f, MidpointRounding.AwayFromZero) * 4;
var atlas = new Image<Alpha8>(atlasDimX, atlasDimY);
var glyphMap = new Dictionary<char, uint>();
var metricsMap = new Dictionary<uint, CharMetrics>();
var atlasRegions = new Dictionary<uint, UIBox2>();
count = 0;
for (var i = startIndex; i <= endIndex; i++)
foreach (var glyph in metricsMap.Keys)
{
var glyphIndex = face.GetCharIndex(i);
if (glyphIndex == 0)
{
continue;
}
glyphMap.Add((char) i, glyphIndex);
if (metricsMap.ContainsKey(glyphIndex))
{
continue;
}
face.LoadGlyph(glyphIndex, LoadFlags.Default, LoadTarget.Normal);
face.LoadGlyph(glyph, LoadFlags.Default, LoadTarget.Normal);
face.Glyph.RenderGlyph(RenderMode.Normal);
var glyphMetrics = face.Glyph.Metrics;
var metrics = new CharMetrics(glyphMetrics.HorizontalBearingX.ToInt32(),
glyphMetrics.HorizontalBearingY.ToInt32(),
glyphMetrics.HorizontalAdvance.ToInt32(),
glyphMetrics.Width.ToInt32(),
glyphMetrics.Height.ToInt32());
metricsMap.Add(glyphIndex, metrics);
var bitmap = face.Glyph.Bitmap;
if (bitmap.Pitch == 0)
@@ -185,18 +179,19 @@ namespace Robust.Client.Graphics
atlas.Mutate(x => x.DrawImage(bitmapImage, new Point(column * maxGlyphSize.X, row * maxGlyphSize.Y),
PixelColorBlendingMode.Overlay, 1));
count += 1;
atlasRegions.Add(glyphIndex, UIBox2i.FromDimensions(offsetX, offsetY, bitmap.Width, bitmap.Rows));
atlasRegions.Add(glyph, UIBox2i.FromDimensions(offsetX, offsetY, bitmap.Width, bitmap.Rows));
}
var atlasDictionary = new Dictionary<uint, AtlasTexture>();
var texture = Texture.LoadFromImage(atlas, $"font-{face.FamilyName}-{size}");
var texture = Texture.LoadFromImage(atlas,
$"font-{face.FamilyName}-{instance.Size}-{(uint) (BaseFontDPI * scale)}");
foreach (var (glyph, region) in atlasRegions)
{
atlasDictionary.Add(glyph, new AtlasTexture(texture, region));
}
return (new FontTextureAtlas(texture, atlasDictionary), glyphMap, metricsMap);
return (new FontTextureAtlas(texture, atlasDictionary), metricsMap);
}
private static Image<Alpha8> MonoBitMapToImage(FTBitmap bitmap)
@@ -226,6 +221,27 @@ namespace Robust.Client.Graphics
return bitmapImage;
}
private Dictionary<char, uint> _generateGlyphMap(Face face)
{
var map = new Dictionary<char, uint>();
// TODO: Render more than extended ASCII, somehow. Does it make sense to just render every glyph in the font?
// Render all the extended ASCII characters.
const uint startIndex = 32;
const uint endIndex = 255;
for (var i = startIndex; i <= endIndex; i++)
{
var glyphIndex = face.GetCharIndex(i);
if (glyphIndex == 0)
{
continue;
}
map.Add((char) i, glyphIndex);
}
return map;
}
private class FontFaceHandle : IFontFaceHandle
{
public Face Face { get; }
@@ -239,42 +255,22 @@ namespace Robust.Client.Graphics
[PublicAPI]
private class FontInstanceHandle : IFontInstanceHandle
{
public Face Face { get; }
public FontFaceHandle FaceHandle { get; }
public int Size { get; }
private readonly Dictionary<char, uint> _glyphMap;
private readonly Dictionary<uint, CharMetrics> _metricsMap;
public int Ascent { get; }
public int Descent { get; }
public int Height { get; }
public int LineHeight { get; }
private readonly Dictionary<float, ScaledFontData> _scaledData = new Dictionary<float, ScaledFontData>();
public readonly IReadOnlyDictionary<char, uint> GlyphMap;
private readonly FontManager _fontManager;
public FontInstanceHandle(FontManager manager, FontTextureAtlas atlas, int size, Face face,
Dictionary<char, uint> glyphMap,
int ascent, int descent, int lineHeight, Dictionary<uint, CharMetrics> metricsMap)
public FontInstanceHandle(FontManager fontManager, int size, IReadOnlyDictionary<char, uint> glyphMap,
FontFaceHandle faceHandle)
{
_fontManager = manager;
Atlas = atlas;
_fontManager = fontManager;
Size = size;
Face = face;
_glyphMap = glyphMap;
Ascent = ascent;
Descent = descent;
LineHeight = lineHeight;
Height = ascent + descent;
_metricsMap = metricsMap;
GlyphMap = glyphMap;
FaceHandle = faceHandle;
}
public FontTextureAtlas Atlas { get; }
public Texture GetCharTexture(char chr)
{
var glyph = _getGlyph(chr);
Atlas.AtlasData.TryGetValue(glyph, out var ret);
return ret;
}
public CharMetrics? GetCharMetrics(char chr)
public Texture GetCharTexture(char chr, float scale)
{
var glyph = _getGlyph(chr);
if (glyph == 0)
@@ -282,19 +278,89 @@ namespace Robust.Client.Graphics
return null;
}
_metricsMap.TryGetValue(glyph, out var metrics);
return metrics;
var scaled = _getScaleDatum(scale);
scaled.Atlas.AtlasData.TryGetValue(glyph, out var texture);
return texture;
}
public CharMetrics? GetCharMetrics(char chr, float scale)
{
var glyph = _getGlyph(chr);
if (glyph == 0)
{
return null;
}
var scaled = _getScaleDatum(scale);
return scaled.MetricsMap[glyph];
}
public int GetAscent(float scale)
{
var scaled = _getScaleDatum(scale);
return scaled.Ascent;
}
public int GetDescent(float scale)
{
var scaled = _getScaleDatum(scale);
return scaled.Descent;
}
public int GetHeight(float scale)
{
var scaled = _getScaleDatum(scale);
return scaled.Height;
}
public int GetLineHeight(float scale)
{
var scaled = _getScaleDatum(scale);
return scaled.LineHeight;
}
private uint _getGlyph(char chr)
{
if (_glyphMap.TryGetValue(chr, out var glyph))
if (GlyphMap.TryGetValue(chr, out var glyph))
{
return glyph;
}
return 0;
}
private ScaledFontData _getScaleDatum(float scale)
{
if (_scaledData.TryGetValue(scale, out var datum))
{
return datum;
}
datum = _fontManager._generateScaledDatum(this, scale);
_scaledData.Add(scale, datum);
return datum;
}
}
private class ScaledFontData
{
public ScaledFontData(IReadOnlyDictionary<uint, CharMetrics> metricsMap, int ascent, int descent,
int height, int lineHeight, FontTextureAtlas atlas)
{
MetricsMap = metricsMap;
Ascent = ascent;
Descent = descent;
Height = height;
LineHeight = lineHeight;
Atlas = atlas;
}
public IReadOnlyDictionary<uint, CharMetrics> MetricsMap { get; }
public int Ascent { get; }
public int Descent { get; }
public int Height { get; }
public int LineHeight { get; }
public FontTextureAtlas Atlas { get; }
}
private class FontTextureAtlas