Add SetFontScaleMode(ImFontPtr, FontScaleMode) (#1666)

* Add SetFontScaleMode(ImFontPtr, FontScaleMode)

`IgnoreGlobalScale` was advertised as "excludes the given font from
global scaling", but the intent I had in mind was "excludes the given
font from being scaled in any manner". As the latter functionality is
needed, obsoleted `IgnoreGlobalScale` and added `SetFontScaleMode`.

* Make it correct

* Name consistency
This commit is contained in:
srkizer 2024-02-18 23:08:07 +09:00 committed by GitHub
parent 7dc99c9307
commit 2d8b71c647
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 166 additions and 66 deletions

View file

@ -31,6 +31,8 @@ public interface IFontSpec
/// <param name="atlas">The atlas to bind this font handle to.</param>
/// <param name="callback">Optional callback to be called after creating the font handle.</param>
/// <returns>The new font handle.</returns>
/// <remarks><see cref="IFontAtlasBuildToolkit.Font"/> will be set when <paramref name="callback"/> is invoked.
/// </remarks>
IFontHandle CreateFontHandle(IFontAtlas atlas, FontAtlasBuildStepDelegate? callback = null);
/// <summary>

View file

@ -109,7 +109,9 @@ public record SingleFontSpec : IFontSpec
tk.RegisterPostBuild(
() =>
{
var roundUnit = tk.IsGlobalScaleIgnored(font) ? 1 : 1 / tk.Scale;
// Multiplication by scale will be done with global scale, outside of this handling.
var scale = tk.GetFontScaleMode(font) == FontScaleMode.UndoGlobalScale ? 1 / tk.Scale : 1;
var roundUnit = tk.GetFontScaleMode(font) == FontScaleMode.SkipHandling ? 1 : 1 / tk.Scale;
var newAscent = MathF.Round((font.Ascent * this.LineHeight) / roundUnit) * roundUnit;
var newFontSize = MathF.Round((font.FontSize * this.LineHeight) / roundUnit) * roundUnit;
var shiftDown = MathF.Round((newFontSize - font.FontSize) / 2f / roundUnit) * roundUnit;
@ -129,13 +131,10 @@ public record SingleFontSpec : IFontSpec
}
}
// `/ roundUnit` = `* scale`
var dax = MathF.Round(this.LetterSpacing / roundUnit / roundUnit) * roundUnit;
var dxy0 = this.GlyphOffset / roundUnit;
var dax = MathF.Round((this.LetterSpacing * scale) / roundUnit) * roundUnit;
var dxy0 = this.GlyphOffset * scale;
dxy0 /= roundUnit;
dxy0.X = MathF.Round(dxy0.X);
dxy0.Y = MathF.Round(dxy0.Y);
dxy0 = new(MathF.Round(dxy0.X), MathF.Round(dxy0.Y));
dxy0 *= roundUnit;
dxy0.Y += shiftDown;

View file

@ -342,9 +342,7 @@ public sealed class SingleFontChooserDialog : IDisposable
{
this.fontHandle ??= this.selectedFont.CreateFontHandle(
this.atlas,
tk =>
tk.OnPreBuild(e => e.IgnoreGlobalScale(e.Font))
.OnPostBuild(e => e.Font.AdjustGlyphMetrics(1f / e.Scale)));
tk => tk.OnPreBuild(e => e.SetFontScaleMode(e.Font, FontScaleMode.UndoGlobalScale)));
}
else
{
@ -837,7 +835,7 @@ public sealed class SingleFontChooserDialog : IDisposable
var changed = false;
if (!ImGui.BeginTable("##advancedOptions", 4))
return changed;
return false;
var labelWidth = ImGui.CalcTextSize("Letter Spacing:").X;
labelWidth = Math.Max(labelWidth, ImGui.CalcTextSize("Offset:").X);

View file

@ -25,12 +25,20 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
/// </summary>
internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable
{
private static readonly string[] FontScaleModes =
{
nameof(FontScaleMode.Default),
nameof(FontScaleMode.SkipHandling),
nameof(FontScaleMode.UndoGlobalScale),
};
private ImVectorWrapper<byte> testStringBuffer;
private IFontAtlas? privateAtlas;
private SingleFontSpec fontSpec = new() { FontId = DalamudDefaultFontAndFamilyId.Instance };
private IFontHandle? fontDialogHandle;
private IReadOnlyDictionary<GameFontFamily, (GameFontStyle Size, Lazy<IFontHandle> Handle)[]>? fontHandles;
private bool useGlobalScale;
private bool atlasScaleMode = true;
private int fontScaleMode = (int)FontScaleMode.UndoGlobalScale;
private bool useWordWrap;
private bool useItalic;
private bool useBold;
@ -52,12 +60,14 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable
public unsafe void Draw()
{
ImGui.AlignTextToFramePadding();
fixed (byte* labelPtr = "Global Scale"u8)
if (ImGui.Combo("Global Scale per Font", ref this.fontScaleMode, FontScaleModes, FontScaleModes.Length))
this.ClearAtlas();
fixed (byte* labelPtr = "Global Scale for Atlas"u8)
{
var v = (byte)(this.useGlobalScale ? 1 : 0);
var v = (byte)(this.atlasScaleMode ? 1 : 0);
if (ImGuiNative.igCheckbox(labelPtr, &v) != 0)
{
this.useGlobalScale = v != 0;
this.atlasScaleMode = v != 0;
this.ClearAtlas();
}
}
@ -124,7 +134,7 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable
$"{nameof(GamePrebakedFontsTestWidget)}:EditorFont",
FontAtlasAutoRebuildMode.Async));
fcd.SelectedFont = this.fontSpec;
fcd.IgnorePreviewGlobalScale = !this.useGlobalScale;
fcd.IgnorePreviewGlobalScale = !this.atlasScaleMode;
Service<InterfaceManager>.Get().Draw += fcd.Draw;
fcd.ResultTask.ContinueWith(
r => Service<Framework>.Get().RunOnFrameworkThread(
@ -148,12 +158,14 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable
Service<FontAtlasFactory>.Get().CreateFontAtlas(
nameof(GamePrebakedFontsTestWidget),
FontAtlasAutoRebuildMode.Async,
this.useGlobalScale);
this.fontDialogHandle ??= this.fontSpec.CreateFontHandle(this.privateAtlas);
this.atlasScaleMode);
this.fontDialogHandle ??= this.fontSpec.CreateFontHandle(
this.privateAtlas,
e => e.OnPreBuild(tk => tk.SetFontScaleMode(tk.Font, (FontScaleMode)this.fontScaleMode)));
fixed (byte* labelPtr = "Test Input"u8)
{
if (!this.useGlobalScale)
if (!this.atlasScaleMode)
ImGuiNative.igSetWindowFontScale(1 / ImGuiHelpers.GlobalScale);
using (this.fontDialogHandle.Push())
{
@ -180,7 +192,7 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable
}
}
if (!this.useGlobalScale)
if (!this.atlasScaleMode)
ImGuiNative.igSetWindowFontScale(1);
}
@ -192,17 +204,29 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable
.ToImmutableDictionary(
x => x.Key,
x => x.Select(
y => (y, new Lazy<IFontHandle>(
() => this.useMinimumBuild
? this.privateAtlas.NewDelegateFontHandle(
e =>
e.OnPreBuild(
tk => tk.AddGameGlyphs(
y,
Encoding.UTF8.GetString(
this.testStringBuffer.DataSpan).ToGlyphRange(),
default)))
: this.privateAtlas.NewGameFontHandle(y))))
y =>
{
var range = Encoding.UTF8.GetString(this.testStringBuffer.DataSpan).ToGlyphRange();
Lazy<IFontHandle> l;
if (this.useMinimumBuild
|| (this.atlasScaleMode && this.fontScaleMode != (int)FontScaleMode.Default))
{
l = new(
() => this.privateAtlas!.NewDelegateFontHandle(
e =>
e.OnPreBuild(
tk => tk.SetFontScaleMode(
tk.AddGameGlyphs(y, range, default),
(FontScaleMode)this.fontScaleMode))));
}
else
{
l = new(() => this.privateAtlas!.NewGameFontHandle(y));
}
return (y, l);
})
.ToArray());
var offsetX = ImGui.CalcTextSize("99.9pt").X + (ImGui.GetStyle().FramePadding.X * 2);
@ -230,7 +254,7 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable
}
else
{
if (!this.useGlobalScale)
if (!this.atlasScaleMode)
ImGuiNative.igSetWindowFontScale(1 / ImGuiHelpers.GlobalScale);
if (counter++ % 2 == 0)
{
@ -251,8 +275,8 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable
}
finally
{
ImGuiNative.igPopTextWrapPos();
ImGuiNative.igSetWindowFontScale(1);
ImGuiNative.igPopTextWrapPos();
}
}
}

View file

@ -0,0 +1,33 @@
using Dalamud.Interface.Utility;
using ImGuiNET;
namespace Dalamud.Interface.ManagedFontAtlas;
/// <summary>
/// Specifies how should global font scale affect a font.
/// </summary>
public enum FontScaleMode
{
/// <summary>
/// Do the default handling. Dalamud will load the sufficienty large font that will accomodate the global scale,
/// and stretch the loaded glyphs so that they look pixel-perfect after applying global scale on drawing.
/// Note that bitmap fonts and game fonts will always look blurry if they're not in their original sizes.
/// </summary>
Default,
/// <summary>
/// Do nothing with the font. Dalamud will load the font with the size that is exactly as specified.
/// On drawing, the font will look blurry due to stretching.
/// Intended for use with custom scale handling.
/// </summary>
SkipHandling,
/// <summary>
/// Stretch the glyphs of the loaded font by the inverse of the global scale.
/// On drawing, the font will always render exactly as the requested size without blurring, as long as
/// <see cref="ImGuiHelpers.GlobalScale"/> and <see cref="ImGui.SetWindowFontScale"/> do not affect the scale any
/// further. Note that bitmap fonts and game fonts will always look blurry if they're not in their original sizes.
/// </summary>
UndoGlobalScale,
}

View file

@ -1,4 +1,5 @@
using Dalamud.Interface.Internal;
using Dalamud.Utility;
using ImGuiNET;
@ -10,12 +11,13 @@ namespace Dalamud.Interface.ManagedFontAtlas;
/// </summary>
public interface IFontAtlasBuildToolkitPostBuild : IFontAtlasBuildToolkit
{
/// <summary>
/// Gets whether global scaling is ignored for the given font.
/// </summary>
/// <param name="fontPtr">The font.</param>
/// <returns>True if ignored.</returns>
bool IsGlobalScaleIgnored(ImFontPtr fontPtr);
/// <inheritdoc cref="IFontAtlasBuildToolkitPreBuild.IsGlobalScaleIgnored"/>
[Obsolete($"Use {nameof(this.GetFontScaleMode)}")]
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
bool IsGlobalScaleIgnored(ImFontPtr fontPtr) => this.GetFontScaleMode(fontPtr) == FontScaleMode.UndoGlobalScale;
/// <inheritdoc cref="IFontAtlasBuildToolkitPreBuild.GetFontScaleMode"/>
FontScaleMode GetFontScaleMode(ImFontPtr fontPtr);
/// <summary>
/// Stores a texture to be managed with the atlas.

View file

@ -4,6 +4,7 @@ using System.Runtime.InteropServices;
using Dalamud.Interface.FontIdentifier;
using Dalamud.Interface.GameFonts;
using Dalamud.Interface.Utility;
using Dalamud.Utility;
using ImGuiNET;
@ -45,14 +46,37 @@ public interface IFontAtlasBuildToolkitPreBuild : IFontAtlasBuildToolkit
/// </summary>
/// <param name="fontPtr">The font.</param>
/// <returns>Same <see cref="ImFontPtr"/> with <paramref name="fontPtr"/>.</returns>
ImFontPtr IgnoreGlobalScale(ImFontPtr fontPtr);
[Obsolete(
$"Use {nameof(this.SetFontScaleMode)} with {nameof(FontScaleMode)}.{nameof(FontScaleMode.UndoGlobalScale)}")]
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
ImFontPtr IgnoreGlobalScale(ImFontPtr fontPtr) => this.SetFontScaleMode(fontPtr, FontScaleMode.UndoGlobalScale);
/// <summary>
/// Gets whether global scaling is ignored for the given font.
/// </summary>
/// <param name="fontPtr">The font.</param>
/// <returns>True if ignored.</returns>
bool IsGlobalScaleIgnored(ImFontPtr fontPtr);
[Obsolete($"Use {nameof(this.GetFontScaleMode)}")]
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
bool IsGlobalScaleIgnored(ImFontPtr fontPtr) => this.GetFontScaleMode(fontPtr) == FontScaleMode.UndoGlobalScale;
/// <summary>
/// Sets the scaling mode for the given font.
/// </summary>
/// <param name="fontPtr">The font, returned from <see cref="AddFontFromFile"/> and alike.
/// Note that <see cref="IFontAtlasBuildToolkit.Font"/> property is not guaranteed to be automatically updated upon
/// calling font adding functions. Pass the return value from font adding functions, not
/// <see cref="IFontAtlasBuildToolkit.Font"/> property.</param>
/// <param name="mode">The scaling mode.</param>
/// <returns><paramref name="fontPtr"/>.</returns>
ImFontPtr SetFontScaleMode(ImFontPtr fontPtr, FontScaleMode mode);
/// <summary>
/// Gets the scaling mode for the given font.
/// </summary>
/// <param name="fontPtr">The font.</param>
/// <returns>The scaling mode.</returns>
FontScaleMode GetFontScaleMode(ImFontPtr fontPtr);
/// <summary>
/// Registers a function to be run after build.

View file

@ -83,9 +83,9 @@ internal sealed partial class FontAtlasFactory
public ImVectorWrapper<ImFontPtr> Fonts => this.data.Fonts;
/// <summary>
/// Gets the list of fonts to ignore global scale.
/// Gets the font scale modes.
/// </summary>
public List<ImFontPtr> GlobalScaleExclusions { get; } = new();
private Dictionary<ImFontPtr, FontScaleMode> FontScaleModes { get; } = new();
/// <inheritdoc/>
public void Dispose() => this.disposeAfterBuild.Dispose();
@ -151,15 +151,15 @@ internal sealed partial class FontAtlasFactory
}
/// <inheritdoc/>
public ImFontPtr IgnoreGlobalScale(ImFontPtr fontPtr)
public ImFontPtr SetFontScaleMode(ImFontPtr fontPtr, FontScaleMode scaleMode)
{
this.GlobalScaleExclusions.Add(fontPtr);
this.FontScaleModes[fontPtr] = scaleMode;
return fontPtr;
}
/// <inheritdoc cref="IFontAtlasBuildToolkitPreBuild.IsGlobalScaleIgnored"/>
public bool IsGlobalScaleIgnored(ImFontPtr fontPtr) =>
this.GlobalScaleExclusions.Contains(fontPtr);
/// <inheritdoc cref="IFontAtlasBuildToolkitPreBuild.GetFontScaleMode"/>
public FontScaleMode GetFontScaleMode(ImFontPtr fontPtr) =>
this.FontScaleModes.GetValueOrDefault(fontPtr, FontScaleMode.Default);
/// <inheritdoc/>
public int StoreTexture(IDalamudTextureWrap textureWrap, bool disposeOnError) =>
@ -496,17 +496,17 @@ internal sealed partial class FontAtlasFactory
var configData = this.data.ConfigData;
foreach (ref var config in configData.DataSpan)
{
if (this.GlobalScaleExclusions.Contains(new(config.DstFont)))
if (this.GetFontScaleMode(config.DstFont) != FontScaleMode.Default)
continue;
config.SizePixels *= this.Scale;
config.GlyphMaxAdvanceX *= this.Scale;
if (float.IsInfinity(config.GlyphMaxAdvanceX))
if (float.IsInfinity(config.GlyphMaxAdvanceX) || float.IsNaN(config.GlyphMaxAdvanceX))
config.GlyphMaxAdvanceX = config.GlyphMaxAdvanceX > 0 ? float.MaxValue : -float.MaxValue;
config.GlyphMinAdvanceX *= this.Scale;
if (float.IsInfinity(config.GlyphMinAdvanceX))
if (float.IsInfinity(config.GlyphMinAdvanceX) || float.IsNaN(config.GlyphMinAdvanceX))
config.GlyphMinAdvanceX = config.GlyphMinAdvanceX > 0 ? float.MaxValue : -float.MaxValue;
config.GlyphOffset *= this.Scale;
@ -536,7 +536,7 @@ internal sealed partial class FontAtlasFactory
var scale = this.Scale;
foreach (ref var font in this.Fonts.DataSpan)
{
if (!this.GlobalScaleExclusions.Contains(font))
if (this.GetFontScaleMode(font) != FontScaleMode.SkipHandling)
font.AdjustGlyphMetrics(1 / scale, 1 / scale);
foreach (var c in FallbackCodepoints)

View file

@ -345,17 +345,36 @@ internal class GamePrebakedFontHandle : FontHandle
{
foreach (var (font, style, ranges) in this.attachments)
{
var effectiveStyle =
toolkitPreBuild.IsGlobalScaleIgnored(font)
? style.Scale(1 / toolkitPreBuild.Scale)
: style;
if (!this.fonts.TryGetValue(style, out var plan))
{
plan = new(
effectiveStyle,
toolkitPreBuild.Scale,
this.handleManager.GameFontTextureProvider,
this.CreateTemplateFont(toolkitPreBuild, style.SizePx));
switch (toolkitPreBuild.GetFontScaleMode(font))
{
case FontScaleMode.Default:
default:
plan = new(
style,
toolkitPreBuild.Scale,
this.handleManager.GameFontTextureProvider,
this.CreateTemplateFont(toolkitPreBuild, style.SizePx));
break;
case FontScaleMode.SkipHandling:
plan = new(
style,
1f,
this.handleManager.GameFontTextureProvider,
this.CreateTemplateFont(toolkitPreBuild, style.SizePx));
break;
case FontScaleMode.UndoGlobalScale:
plan = new(
style.Scale(1 / toolkitPreBuild.Scale),
toolkitPreBuild.Scale,
this.handleManager.GameFontTextureProvider,
this.CreateTemplateFont(toolkitPreBuild, style.SizePx));
break;
}
this.fonts[style] = plan;
}
@ -620,15 +639,14 @@ internal class GamePrebakedFontHandle : FontHandle
public unsafe void CopyGlyphsToRanges(IFontAtlasBuildToolkitPostBuild toolkitPostBuild)
{
var scale = this.Style.SizePt / this.Fdt.FontHeader.Size;
var atlasScale = toolkitPostBuild.Scale;
var round = 1 / atlasScale;
foreach (var (font, rangeBits) in this.Ranges)
{
if (font.NativePtr == this.FullRangeFont.NativePtr)
continue;
var noGlobalScale = toolkitPostBuild.IsGlobalScaleIgnored(font);
var fontScaleMode = toolkitPostBuild.GetFontScaleMode(font);
var round = fontScaleMode == FontScaleMode.SkipHandling ? 1 : 1 / toolkitPostBuild.Scale;
var lookup = font.IndexLookupWrapped();
var glyphs = font.GlyphsWrapped();
@ -649,7 +667,7 @@ internal class GamePrebakedFontHandle : FontHandle
ref var g = ref glyphs[glyphIndex];
g = sourceGlyph;
if (noGlobalScale)
if (fontScaleMode == FontScaleMode.SkipHandling)
{
g.XY *= scale;
g.AdvanceX *= scale;
@ -673,7 +691,7 @@ internal class GamePrebakedFontHandle : FontHandle
continue;
if (!rangeBits[leftInt] || !rangeBits[rightInt])
continue;
if (noGlobalScale)
if (fontScaleMode == FontScaleMode.SkipHandling)
{
font.AddKerningPair((ushort)leftInt, (ushort)rightInt, k.RightOffset * scale);
}