Browse Source

Keep the editor zoom separate from the configured font size

Zoom was stored as the font size itself, so the zoom overlay had no way to
tell a Ctrl+Wheel zoom from a font size picked in the options dialog: any
size other than the hard-coded default made the overlay appear, and the
percentage was measured against that default rather than the user's font.
A separate multiplier restores the split the setting always implied - the
options dialog moves the base size, zoom scales it - so 100% means "the
font you configured", whatever that is.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
pull/3983/head
Siegfried Pammer 1 month ago committed by Siegfried Pammer
parent
commit
7465fb1fb0
  1. 71
      ILSpy.Tests/Editor/EditorZoomTests.cs
  2. 1
      ILSpy/Options/DisplaySettingReactions.cs
  3. 8
      ILSpy/Options/DisplaySettings.cs
  4. 5
      ILSpy/TextView/DecompilerTextEditor.cs
  5. 6
      ILSpy/TextView/DecompilerTextView.axaml
  6. 8
      ILSpy/TextView/DecompilerTextView.axaml.cs
  7. 48
      ILSpy/TextView/EditorZoom.cs
  8. 21
      ILSpy/TextView/ZoomButtons.axaml.cs

71
ILSpy.Tests/Editor/EditorZoomTests.cs

@ -16,8 +16,11 @@ @@ -16,8 +16,11 @@
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using Avalonia.Headless.NUnit;
using AwesomeAssertions;
using ICSharpCode.ILSpy.Options;
using ICSharpCode.ILSpy.TextView;
using NUnit.Framework;
@ -30,53 +33,87 @@ public class EditorZoomTests @@ -30,53 +33,87 @@ public class EditorZoomTests
[Test]
public void ZoomIn_Multiplies_By_Factor()
{
EditorZoom.ZoomIn(10.0).Should().BeApproximately(11.0, 0.001);
EditorZoom.ZoomIn(1.0).Should().BeApproximately(1.1, 0.001);
}
[Test]
public void ZoomOut_Divides_By_Factor()
{
EditorZoom.ZoomOut(11.0).Should().BeApproximately(10.0, 0.001);
EditorZoom.ZoomOut(1.1).Should().BeApproximately(1.0, 0.001);
}
[Test]
public void Reset_Returns_Default_Font_Size()
public void Reset_Returns_Default_Zoom()
{
EditorZoom.Reset().Should().Be(EditorZoom.DefaultFontSize);
EditorZoom.Reset().Should().Be(EditorZoom.DefaultZoom);
}
[Test]
public void Zoom_Round_Trip_Returns_To_Default_Without_Floating_Point_Drift()
{
// 1.1 * (1/1.1) is bit-equal-ish but not exactly equal in float; without the
// snap-to-default heuristic, the rounded-trip value would be 13.3333000001
// instead of 13.3333333... and the user would see a sticky "non-100% zoom"
// even after they explicitly returned to default. The Reset path bypasses
// this entirely, but Ctrl+Wheel round trips need the snap.
var step1 = EditorZoom.ZoomIn(EditorZoom.DefaultFontSize);
// snap-to-default heuristic, the round-tripped value would be 1.0000000001
// instead of 1.0 and the user would see a sticky "non-100% zoom" even after
// they explicitly returned to default. The Reset path bypasses this entirely,
// but Ctrl+Wheel round trips need the snap.
var step1 = EditorZoom.ZoomIn(EditorZoom.DefaultZoom);
var step2 = EditorZoom.ZoomOut(step1);
step2.Should().Be(EditorZoom.DefaultFontSize);
step2.Should().Be(EditorZoom.DefaultZoom);
}
[Test]
public void ZoomIn_Clamps_At_Upper_Bound()
{
EditorZoom.ZoomIn(EditorZoom.MaxFontSize).Should().Be(EditorZoom.MaxFontSize);
EditorZoom.ZoomIn(EditorZoom.MaxZoom).Should().Be(EditorZoom.MaxZoom);
}
[Test]
public void ZoomOut_Clamps_At_Lower_Bound()
{
EditorZoom.ZoomOut(EditorZoom.MinFontSize).Should().Be(EditorZoom.MinFontSize);
EditorZoom.ZoomOut(EditorZoom.MinZoom).Should().Be(EditorZoom.MinZoom);
}
[Test]
public void ZoomOut_Of_Slightly_Above_Min_Saturates_Not_Below()
{
// Slightly above MinFontSize zoomed out by the standard factor should land
// AT min, not below. Guards against an off-by-clamp bug where the post-divide
// value is below min but clamping only runs on the multiplier output.
var justAbove = EditorZoom.MinFontSize * 1.05;
EditorZoom.ZoomOut(justAbove).Should().Be(EditorZoom.MinFontSize);
// Slightly above MinZoom zoomed out by the standard factor should land AT min,
// not below. Guards against an off-by-clamp bug where the post-divide value is
// below min but clamping only runs on the multiplier output.
EditorZoom.ZoomOut(EditorZoom.MinZoom * 1.05).Should().Be(EditorZoom.MinZoom);
}
[Test]
public void EffectiveFontSize_Scales_The_Configured_Size()
{
var settings = new DisplaySettings { SelectedFontSize = 20, EditorZoomFactor = 1.5 };
EditorZoom.EffectiveFontSize(settings).Should().BeApproximately(30, 0.001);
}
[AvaloniaTest]
public void Changing_The_Configured_Font_Size_Leaves_The_Zoom_Buttons_Hidden()
{
// The options dialog moves the base font size; that is not a zoom, so the
// overlay must stay hidden and the label must keep reading 100%.
var settings = new DisplaySettings();
var buttons = new ZoomButtons();
buttons.Bind(settings);
settings.SelectedFontSize = 24;
buttons.IsVisible.Should().BeFalse();
buttons.ZoomPercentText.Should().Be("100%");
}
[AvaloniaTest]
public void Zooming_Shows_The_Zoom_Buttons()
{
var settings = new DisplaySettings();
var buttons = new ZoomButtons();
buttons.Bind(settings);
settings.EditorZoomFactor = 1.5;
buttons.IsVisible.Should().BeTrue();
buttons.ZoomPercentText.Should().Be("150%");
}
}

1
ILSpy/Options/DisplaySettingReactions.cs

@ -77,6 +77,7 @@ namespace ICSharpCode.ILSpy.Options @@ -77,6 +77,7 @@ namespace ICSharpCode.ILSpy.Options
// Editor-only (DecompilerTextView applies these directly to the AvaloniaEdit control).
[nameof(DisplaySettings.SelectedFont)] = DisplaySettingReaction.EditorLive,
[nameof(DisplaySettings.SelectedFontSize)] = DisplaySettingReaction.EditorLive,
[nameof(DisplaySettings.EditorZoomFactor)] = DisplaySettingReaction.EditorLive,
[nameof(DisplaySettings.ShowLineNumbers)] = DisplaySettingReaction.EditorLive,
[nameof(DisplaySettings.EnableWordWrap)] = DisplaySettingReaction.EditorLive,
[nameof(DisplaySettings.HighlightCurrentLine)] = DisplaySettingReaction.EditorLive,

8
ILSpy/Options/DisplaySettings.cs

@ -39,6 +39,12 @@ namespace ICSharpCode.ILSpy.Options @@ -39,6 +39,12 @@ namespace ICSharpCode.ILSpy.Options
[ObservableProperty]
double selectedFontSize = 10.0 * 4 / 3;
/// <summary>Editor zoom (Ctrl+Wheel / the zoom overlay), as a multiplier on top of
/// <see cref="SelectedFontSize"/>. Kept separate so changing the font size in the
/// options dialog isn't mistaken for a zoom.</summary>
[ObservableProperty]
double editorZoomFactor = 1.0;
[ObservableProperty]
bool showLineNumbers;
@ -111,6 +117,7 @@ namespace ICSharpCode.ILSpy.Options @@ -111,6 +117,7 @@ namespace ICSharpCode.ILSpy.Options
{
SelectedFont = (string?)section.Attribute("Font") ?? "Consolas";
SelectedFontSize = (double?)section.Attribute("FontSize") ?? 10.0 * 4 / 3;
EditorZoomFactor = (double?)section.Attribute(nameof(EditorZoomFactor)) ?? 1.0;
ShowLineNumbers = (bool?)section.Attribute(nameof(ShowLineNumbers)) ?? false;
ShowMetadataTokens = (bool?)section.Attribute(nameof(ShowMetadataTokens)) ?? false;
ShowMetadataTokensInBase10 = (bool?)section.Attribute(nameof(ShowMetadataTokensInBase10)) ?? false;
@ -140,6 +147,7 @@ namespace ICSharpCode.ILSpy.Options @@ -140,6 +147,7 @@ namespace ICSharpCode.ILSpy.Options
var section = new XElement(SectionName);
section.SetAttributeValue("Font", SelectedFont);
section.SetAttributeValue("FontSize", SelectedFontSize);
section.SetAttributeValue(nameof(EditorZoomFactor), EditorZoomFactor);
section.SetAttributeValue(nameof(ShowLineNumbers), ShowLineNumbers);
section.SetAttributeValue(nameof(ShowMetadataTokens), ShowMetadataTokens);
section.SetAttributeValue(nameof(ShowMetadataTokensInBase10), ShowMetadataTokensInBase10);

5
ILSpy/TextView/DecompilerTextEditor.cs

@ -86,7 +86,8 @@ namespace ICSharpCode.ILSpy.TextView @@ -86,7 +86,8 @@ namespace ICSharpCode.ILSpy.TextView
void OnDisplaySettingsChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName is nameof(DisplaySettings.SelectedFont) or nameof(DisplaySettings.SelectedFontSize))
if (e.PropertyName is nameof(DisplaySettings.SelectedFont) or nameof(DisplaySettings.SelectedFontSize)
or nameof(DisplaySettings.EditorZoomFactor))
ApplyFontSettings();
}
@ -97,7 +98,7 @@ namespace ICSharpCode.ILSpy.TextView @@ -97,7 +98,7 @@ namespace ICSharpCode.ILSpy.TextView
if (!string.IsNullOrEmpty(displaySettings.SelectedFont))
FontFamily = new FontFamily(displaySettings.SelectedFont);
if (displaySettings.SelectedFontSize > 0)
FontSize = displaySettings.SelectedFontSize;
FontSize = EditorZoom.EffectiveFontSize(displaySettings);
}
static DisplaySettings? TryGetDisplaySettings()

6
ILSpy/TextView/DecompilerTextView.axaml

@ -68,9 +68,9 @@ @@ -68,9 +68,9 @@
</StackPanel>
</Grid>
</Border>
<!-- Zoom-buttons overlay: bottom-right corner of the editor, auto-hides at default
font size unless DisplaySettings.AlwaysShowZoomButtons is on. Bound at code-
behind time once DisplaySettings is resolvable from composition. -->
<!-- Zoom-buttons overlay: bottom-right corner of the editor, auto-hides at 100% zoom
unless AlwaysShowZoomButtons is on. Bound at code-behind time once DisplaySettings
is resolvable from composition. -->
<textView:ZoomButtons Name="ZoomButtons" />
<!-- Hover documentation popup (configured in SetupRichPopup). It must be declared
here rather than constructed detached: with overlay popups (see

8
ILSpy/TextView/DecompilerTextView.axaml.cs

@ -422,7 +422,7 @@ namespace ICSharpCode.ILSpy.TextView @@ -422,7 +422,7 @@ namespace ICSharpCode.ILSpy.TextView
if (currentDisplaySettings == null)
return;
var step = e.Delta.Y > 0 ? (System.Func<double, double>)EditorZoom.ZoomIn : EditorZoom.ZoomOut;
currentDisplaySettings.SelectedFontSize = step(currentDisplaySettings.SelectedFontSize);
currentDisplaySettings.EditorZoomFactor = step(currentDisplaySettings.EditorZoomFactor);
e.Handled = true;
}
@ -556,17 +556,17 @@ namespace ICSharpCode.ILSpy.TextView @@ -556,17 +556,17 @@ namespace ICSharpCode.ILSpy.TextView
{
case Key.OemPlus:
case Key.Add:
currentDisplaySettings.SelectedFontSize = EditorZoom.ZoomIn(currentDisplaySettings.SelectedFontSize);
currentDisplaySettings.EditorZoomFactor = EditorZoom.ZoomIn(currentDisplaySettings.EditorZoomFactor);
e.Handled = true;
break;
case Key.OemMinus:
case Key.Subtract:
currentDisplaySettings.SelectedFontSize = EditorZoom.ZoomOut(currentDisplaySettings.SelectedFontSize);
currentDisplaySettings.EditorZoomFactor = EditorZoom.ZoomOut(currentDisplaySettings.EditorZoomFactor);
e.Handled = true;
break;
case Key.D0:
case Key.NumPad0:
currentDisplaySettings.SelectedFontSize = EditorZoom.Reset();
currentDisplaySettings.EditorZoomFactor = EditorZoom.Reset();
e.Handled = true;
break;
}

48
ILSpy/TextView/EditorZoom.cs

@ -18,41 +18,49 @@ @@ -18,41 +18,49 @@
using System;
using ICSharpCode.ILSpy.Options;
namespace ICSharpCode.ILSpy.TextView
{
/// <summary>
/// Pure-math helper for editor zoom (font-size scaling). The actual editor wiring
/// reads/writes <c>DisplaySettings.SelectedFontSize</c>; this class encapsulates
/// the step calculation and clamping so it's unit-testable without an editor.
/// Pure-math helper for editor zoom. Zoom is a multiplier on top of the configured
/// <see cref="DisplaySettings.SelectedFontSize"/>, kept in
/// <see cref="DisplaySettings.EditorZoomFactor"/>: picking a different font size in the
/// options dialog moves the base size without counting as a zoom. The step calculation
/// and clamping live here so they're unit-testable without an editor.
/// </summary>
public static class EditorZoom
{
/// <summary>Multiplicative step per wheel tick / button press.</summary>
public const double Factor = 1.1;
/// <summary>Lower bound in points. Below ~8 pt the gutter glyphs collapse.</summary>
public const double MinFontSize = 8.0;
/// <summary>Lower zoom bound; below 20% the gutter glyphs collapse.</summary>
public const double MinZoom = 0.2;
/// <summary>Upper zoom bound; above 500% one glyph fills the viewport.</summary>
public const double MaxZoom = 5.0;
/// <summary>Upper bound in points. Above ~72 pt one glyph fills the viewport.</summary>
public const double MaxFontSize = 72.0;
/// <summary>Unzoomed state: the configured font size is used as-is.</summary>
public const double DefaultZoom = 1.0;
/// <summary>Default font size in points, matching <c>DisplaySettings</c>'s initial value.</summary>
public const double DefaultFontSize = 10.0 * 4 / 3;
public static double ZoomIn(double currentZoom)
=> Clamp(RoundToDefaultIfClose(currentZoom * Factor));
public static double ZoomIn(double currentFontSize)
=> Clamp(RoundToDefaultIfClose(currentFontSize * Factor));
public static double ZoomOut(double currentZoom)
=> Clamp(RoundToDefaultIfClose(currentZoom / Factor));
public static double ZoomOut(double currentFontSize)
=> Clamp(RoundToDefaultIfClose(currentFontSize / Factor));
public static double Reset() => DefaultZoom;
public static double Reset() => DefaultFontSize;
/// <summary>The font size an editor should render at: configured size times zoom.</summary>
public static double EffectiveFontSize(DisplaySettings settings)
=> settings.SelectedFontSize * Clamp(settings.EditorZoomFactor);
/// <summary>Snap to <see cref="DefaultFontSize"/> when within 0.001 pt — avoids
/// floating-point drift after zoom-in followed by zoom-out leaving the size stuck
/// at 13.3333000001 instead of the canonical 13.3333333.</summary>
static double RoundToDefaultIfClose(double size)
=> Math.Abs(size - DefaultFontSize) < 0.001 ? DefaultFontSize : size;
/// <summary>Snap to <see cref="DefaultZoom"/> when within 0.001 — avoids floating-point
/// drift after zoom-in followed by zoom-out leaving the factor stuck at 1.0000000001,
/// which would keep the zoom overlay visible at an apparent 100%.</summary>
static double RoundToDefaultIfClose(double zoom)
=> Math.Abs(zoom - DefaultZoom) < 0.001 ? DefaultZoom : zoom;
static double Clamp(double size) => Math.Max(MinFontSize, Math.Min(MaxFontSize, size));
static double Clamp(double zoom) => Math.Max(MinZoom, Math.Min(MaxZoom, zoom));
}
}

21
ILSpy/TextView/ZoomButtons.axaml.cs

@ -29,8 +29,9 @@ namespace ICSharpCode.ILSpy.TextView @@ -29,8 +29,9 @@ namespace ICSharpCode.ILSpy.TextView
{
/// <summary>
/// Editor-corner overlay with zoom in / out / reset buttons plus a live "133%" label.
/// Bound to <see cref="DisplaySettings.SelectedFontSize"/>; the overlay auto-hides at
/// the default font size unless <see cref="AlwaysShowZoomButtons"/> is set to <c>true</c>.
/// Bound to <see cref="DisplaySettings.EditorZoomFactor"/>; the overlay auto-hides at
/// 100% zoom unless <see cref="AlwaysShowZoomButtons"/> is set to <c>true</c>. Changing
/// the configured font size is not a zoom and leaves the overlay hidden.
/// </summary>
public partial class ZoomButtons : UserControl
{
@ -79,7 +80,7 @@ namespace ICSharpCode.ILSpy.TextView @@ -79,7 +80,7 @@ namespace ICSharpCode.ILSpy.TextView
/// <summary>
/// Binds this widget to a live <see cref="DisplaySettings"/>. Re-renders the
/// percent label and visibility on each <c>SelectedFontSize</c> change. Safe to
/// percent label and visibility on each <c>EditorZoomFactor</c> change. Safe to
/// call multiple times — the previous subscription is dropped before the new one
/// is wired.
/// </summary>
@ -96,7 +97,7 @@ namespace ICSharpCode.ILSpy.TextView @@ -96,7 +97,7 @@ namespace ICSharpCode.ILSpy.TextView
void OnSettingsChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(DisplaySettings.SelectedFontSize))
if (e.PropertyName == nameof(DisplaySettings.EditorZoomFactor))
{
RefreshLabel();
RefreshVisibility();
@ -114,22 +115,26 @@ namespace ICSharpCode.ILSpy.TextView @@ -114,22 +115,26 @@ namespace ICSharpCode.ILSpy.TextView
{
if (settings == null)
return;
settings.SelectedFontSize = step(settings.SelectedFontSize);
settings.EditorZoomFactor = step(settings.EditorZoomFactor);
}
/// <summary>The rendered zoom label, e.g. "150%" (test observation point).</summary>
internal string ZoomPercentText => settings == null
? string.Empty
: (int)Math.Round(settings.EditorZoomFactor * 100.0) + "%";
void RefreshLabel()
{
if (settings == null || this.FindControl<TextBlock>("PercentLabel") is not { } label)
return;
var pct = (int)Math.Round(settings.SelectedFontSize / EditorZoom.DefaultFontSize * 100.0);
label.Text = pct + "%";
label.Text = ZoomPercentText;
}
void RefreshVisibility()
{
IsVisible = settings != null
&& (AlwaysShowZoomButtons
|| Math.Abs(settings.SelectedFontSize - EditorZoom.DefaultFontSize) > 0.001);
|| Math.Abs(settings.EditorZoomFactor - EditorZoom.DefaultZoom) > 0.001);
}
}
}

Loading…
Cancel
Save