Skip to content

Commit

Permalink
Merge pull request #2892 from VsVim/dev/nosami/relative-line-numbers
Browse files Browse the repository at this point in the history
[Mac] Add first draft of relative numbers
  • Loading branch information
nosami committed Jun 6, 2021
2 parents f47ea16 + 5ef7ca4 commit d568ef3
Show file tree
Hide file tree
Showing 12 changed files with 941 additions and 1 deletion.
2 changes: 1 addition & 1 deletion Src/VimMac/Properties/AddinInfo.cs
Expand Up @@ -5,7 +5,7 @@
[assembly: Addin(
"VsVim",
Namespace = "Vim.Mac",
Version = "2.8.0.12"
Version = "2.8.0.13"
)]

[assembly: AddinName("VsVim")]
Expand Down
124 changes: 124 additions & 0 deletions Src/VimMac/RelativeLineNumbers/CocoaExtensions.cs
@@ -0,0 +1,124 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Linq;
using System.Windows;
using System.Windows.Media;

using AppKit;
using CoreAnimation;
using CoreGraphics;
using CoreText;
using Foundation;

namespace Vim.UI.Cocoa.Implementation.RelativeLineNumbers
{
public static class CocoaExtensions
{
public static double WidthIncludingTrailingWhitespace(this CTLine line)
=> line.GetBounds(0).Width;

//Before "optimising" this to `new CGColor(color.R / 255f, color.G / 255f, color.B / 255f, color.A / 255f);`
//notice that doesn't handle colorspace correctly
public static CGColor AsCGColor(this Color color)
=> NSColor.FromRgba(color.R, color.G, color.B, color.A).CGColor;

public static bool IsDarkColor(this CGColor color)
{
double red;
double green;
double blue;

var components = color?.Components;

if (components == null || components.Length == 0)
return false;

if (components.Length >= 3)
{
red = components[0];
green = components[1];
blue = components[2];
}
else
{
red = green = blue = components[0];
}

// https://www.w3.org/WAI/ER/WD-AERT/#color-contrast
var brightness = (red * 255 * 299 + green * 255 * 587 + blue * 255 * 114) / 1000;
return brightness <= 155;
}

public static bool IsMouseOver(this NSView view)
{
var mousePoint = NSEvent.CurrentMouseLocation;
return IsMouseOver(view, mousePoint);
}

public static bool IsMouseOver(this NSView view, CGPoint mousePoint)
{
var window = view.Window;
if (window == null)
{
return false;
}
var viewScreenRect = window.ConvertRectToScreen(
view.ConvertRectToView(view.Frame, null));
return viewScreenRect.Contains(mousePoint);
}

public static Rect AsRect(this CGRect rect)
{
return new Rect(rect.X, rect.Y, rect.Width, rect.Height);
}

public static CGPoint AsCGPoint(this Point point)
{
return new CGPoint(point.X, point.Y);
}

public static Point PointToScreen(this NSView view, Point point)
{
var p = view.ConvertPointToView(new CGPoint(point.X, point.Y), null);
if (view.Window == null)
return new Point(p.X, p.Y);
p = view.Window.ConvertPointToScreen(p);
return new Point(p.X, p.Y);
}

public static CGPoint PointToScreen(this NSView view, CGPoint point)
{
var p = view.ConvertPointToView(point, null);
if (view.Window == null)
return p;
p = view.Window.ConvertPointToScreen(p);
return p;
}

private static readonly NSDictionary disableAnimations = new NSDictionary(
"actions", NSNull.Null,
"contents", NSNull.Null,
"hidden", NSNull.Null,
"onLayout", NSNull.Null,
"onOrderIn", NSNull.Null,
"onOrderOut", NSNull.Null,
"position", NSNull.Null,
"sublayers", NSNull.Null,
"transform", NSNull.Null,
"bounds", NSNull.Null);

public static void DisableImplicitAnimations(this CALayer layer)
=> layer.Actions = disableAnimations;
}
}

namespace CoreGraphics
{
internal static class CoreGraphicsExtensions
{
public static CGSize InflateToIntegral(this CGSize size)
=> new CGSize(NMath.Ceiling(size.Width), NMath.Ceiling(size.Height));
}
}
@@ -0,0 +1,94 @@
using System;
using System.Globalization;

using AppKit;
using CoreAnimation;
using CoreGraphics;
using CoreText;
using Foundation;
using Vim.UI.Wpf.Implementation.RelativeLineNumbers;

namespace Vim.UI.Cocoa.Implementation.RelativeLineNumbers
{
internal sealed class CocoaLineNumberMarginDrawingVisual : CALayer, ICALayerDelegate
{
private NSStringAttributes stringAttributes;
private CGRect lineBounds;
private nfloat lineAscent;
private CTLine ctLine;

public int LineNumber { get; private set; }
public int DisplayNumber { get; private set; }

public bool InUse
{
get => !Hidden;
set => Hidden = !value;
}

public CocoaLineNumberMarginDrawingVisual()
{
Delegate = this;
NeedsDisplayOnBoundsChange = true;
this.DisableImplicitAnimations();
}

internal void Update(
NSStringAttributes stringAttributes,
Line line,
nfloat lineWidth)
{
// NOTE: keep this in sync with CocoaRenderedLineVisual regarding any font
// metric handling, transforms, etc. Ensure that line numbers are always
// exactly aligned with the actual editor text lines. Test with Fluent
// Calibri and many other fonts at various sizes.

if (DisplayNumber != line.DisplayNumber || this.stringAttributes != stringAttributes)
{
DisplayNumber = line.DisplayNumber;
this.stringAttributes = stringAttributes;

ctLine?.Dispose();
ctLine = new CTLine(new NSAttributedString(
line.DisplayNumber.ToString(CultureInfo.CurrentUICulture.NumberFormat),
stringAttributes));

lineBounds = ctLine.GetBounds(0);
ctLine.GetTypographicBounds(out lineAscent, out _, out _);

SetNeedsDisplay();
}

AffineTransform = new CGAffineTransform(
1, 0,
0, 1,
0, (nfloat)line.TextTop);

var transformRect = AffineTransform.TransformRect(new CGRect(
line.IsCaretLine ? 0 : lineWidth - lineBounds.Width, // right justify
line.Baseline - lineAscent,
lineBounds.Width,
lineBounds.Height));

Frame = new CGRect(
NMath.Floor(transformRect.X),
NMath.Floor(transformRect.Y),
NMath.Ceiling(transformRect.Width),
NMath.Ceiling(transformRect.Height));
}

[Export("drawLayer:inContext:")]
private void Draw(CALayer _, CGContext context)
{
if (ctLine == null)
return;

CocoaTextManager.ConfigureContext(context);

context.TextPosition = new CGPoint(0, -lineAscent);
context.ScaleCTM(1, -1);

ctLine.Draw(context);
}
}
}
64 changes: 64 additions & 0 deletions Src/VimMac/RelativeLineNumbers/CocoaTextManager.cs
@@ -0,0 +1,64 @@
using System;

using CoreGraphics;
using Foundation;

namespace Vim.UI.Cocoa.Implementation.RelativeLineNumbers
{
internal static class CocoaTextManager
{
private static readonly NSString AppleFontSmoothing = new NSString(nameof(AppleFontSmoothing));
private static readonly IDisposable smoothingObserver;
private static int smoothing;

public static event EventHandler DefaultsChanged;

static CocoaTextManager()
{
UpdateSmoothing(NSUserDefaults.StandardUserDefaults.ValueForKey(AppleFontSmoothing));
smoothingObserver = NSUserDefaults.StandardUserDefaults.AddObserver(
AppleFontSmoothing,
NSKeyValueObservingOptions.New,
change => UpdateSmoothing(change?.NewValue));
}

private static void UpdateSmoothing(NSObject value)
{
var newSmoothing = value is NSNumber number
? number.Int32Value
: -1;

if (newSmoothing == smoothing)
return;

smoothing = newSmoothing;

DefaultsChanged?.Invoke(null, EventArgs.Empty);
}

public static void ConfigureContext(CGContext context)
{
if (context == null)
return;

context.SetShouldAntialias(true);
context.SetShouldSubpixelPositionFonts(true);

if (MacRuntimeEnvironment.MojaveOrNewer)
{
context.SetAllowsFontSmoothing(smoothing < 0);
}
else
{
// NOTE: we cannot do proper subpixel AA because the layer/context
// needs a background color which is not available in the text layer.
// Selections and highlights are separate layers in the editor. If
// we had reliable background colors, we could enable subpixel AA
// "smoothing" by setting this to true and ensuring the context
// had a background color (by way of the target layer or calling
// the private CGContextSetFontSmoothingBackgroundColor.
context.SetShouldSmoothFonts(false);
}
}
}
}
27 changes: 27 additions & 0 deletions Src/VimMac/RelativeLineNumbers/Line.cs
@@ -0,0 +1,27 @@
using System.Diagnostics;

namespace Vim.UI.Wpf.Implementation.RelativeLineNumbers
{
[DebuggerDisplay("{LineNumber} {DisplayNumber} {Baseline}")]
internal readonly struct Line
{
public int LineNumber { get; }

public int DisplayNumber { get; }

public double Baseline { get; }

public double TextTop { get; }

public bool IsCaretLine { get; }

public Line(int lineNumber, int displayNumber, double verticalBaseline, double textTop, bool isCaretLine)
{
LineNumber = lineNumber;
DisplayNumber = displayNumber;
Baseline = verticalBaseline;
TextTop = textTop;
IsCaretLine = isCaretLine;
}
}
}

0 comments on commit d568ef3

Please sign in to comment.