diff --git a/Cauldron.Macos/Main.storyboard b/Cauldron.Macos/Main.storyboard index 0634e53..5eb2ba2 100644 --- a/Cauldron.Macos/Main.storyboard +++ b/Cauldron.Macos/Main.storyboard @@ -684,7 +684,7 @@ diff --git a/Cauldron.Macos/SourceWriter/LineNumberRuler.cs b/Cauldron.Macos/SourceWriter/LineNumberRuler.cs new file mode 100644 index 0000000..5ed9366 --- /dev/null +++ b/Cauldron.Macos/SourceWriter/LineNumberRuler.cs @@ -0,0 +1,126 @@ +using System; +using AppKit; +using CoreGraphics; +using Foundation; + +namespace Cauldron.Macos.SourceWriter; + +public class LineNumberRuler : NSRulerView +{ + private NSColor _foregroundColor = NSColor.DisabledControlText; + private NSColor _backgroundColor = NSColor.TextBackground; + + public float GutterWidth { get; private set; } = 40f; + public NSColor ForegroundColor + { + get => this._foregroundColor; + set + { + this._foregroundColor = value; + this.NeedsDisplay = true; + } + } + public NSColor BackgroundColor + { + get => this._backgroundColor; + set + { + this._backgroundColor = value; + this.NeedsDisplay = true; + } + } + + public LineNumberRuler(NSTextView textView) + : base(textView.EnclosingScrollView, NSRulerOrientation.Vertical) + { + this.ClientView = textView; + this.RuleThickness = this.GutterWidth; + } + + public LineNumberRuler(NSTextView textView, NSColor foregroundColor, NSColor backgroundColor) + : base(textView.EnclosingScrollView, NSRulerOrientation.Vertical) + { + this.ClientView = textView; + this.ForegroundColor = foregroundColor; + this.BackgroundColor = backgroundColor; + this.RuleThickness = this.GutterWidth; + } + + public override void DrawHashMarksAndLabels(CGRect rect) + { + this.BackgroundColor.Set(); + NSGraphicsContext.CurrentContext.CGContext.FillRect(rect); + + if (this.ClientView is not NSTextView textView) + return; + + NSLayoutManager layoutManager = textView.LayoutManager; + NSTextContainer textContainer = textView.TextContainer; + + if (layoutManager is null || textContainer is null) + return; + + NSString content = new NSString(textView.Value); + NSRange visibleGlyphsRange = layoutManager + .GetGlyphRangeForBoundingRect(textView.VisibleRect(), textContainer); + + int lineNumber = 1; + + NSRegularExpression newlineRegex = new NSRegularExpression(new NSString("\n"), + new NSRegularExpressionOptions(), out NSError error); + if (error is not null) + return; + + lineNumber += (int)newlineRegex.GetNumberOfMatches(content, new NSMatchingOptions(), + new NSRange(0, visibleGlyphsRange.Location)); + + nint firstGlyphOfLineIndex = visibleGlyphsRange.Location; + + while (firstGlyphOfLineIndex < visibleGlyphsRange.Location + visibleGlyphsRange.Length) + { + NSRange charRangeOfLine = content.LineRangeForRange( + new NSRange((nint)layoutManager.GetCharacterIndex((nuint)firstGlyphOfLineIndex), 0)); + NSRange glyphRangeOfLine = layoutManager.GetGlyphRange(charRangeOfLine); + + nint firstGlyphOfRowIndex = firstGlyphOfLineIndex; + int lineWrapCount = 0; + + while (firstGlyphOfRowIndex < glyphRangeOfLine.Location + glyphRangeOfLine.Length) + { + CGRect lineRect = layoutManager.GetLineFragmentRect((nuint)firstGlyphOfRowIndex, + out NSRange effectiveRange, true); + + if (lineWrapCount == 0) + this.DrawLineNumber(lineNumber, (float)lineRect.GetMinY() + + (float)textView.TextContainerInset.Height); + else + break; + + // Move to next row + firstGlyphOfRowIndex = effectiveRange.Location + effectiveRange.Length; + } + + firstGlyphOfLineIndex = glyphRangeOfLine.Location + glyphRangeOfLine.Length; + lineNumber += 1; + } + + if (layoutManager.ExtraLineFragmentTextContainer != null) + this.DrawLineNumber(lineNumber, (float)layoutManager.ExtraLineFragmentRect.GetMinY() + + (float)textView.TextContainerInset.Height); + } + + private void DrawLineNumber(int lineNumber, float yPosition) + { + if (this.ClientView is not NSTextView textView) + return; + + NSDictionary attributes = new(NSStringAttributeKey.Font, textView.Font, + NSStringAttributeKey.ForegroundColor, this.ForegroundColor); + NSAttributedString attributedLineNumber = new(lineNumber.ToString(), attributes); + CGPoint relativePoint = this.ConvertPointFromView(CGPoint.Empty, textView); + nfloat xPosition = this.GutterWidth - (attributedLineNumber.Size.Width + 8); + + attributedLineNumber.DrawAtPoint(new CGPoint(xPosition, relativePoint.Y + yPosition)); + } +} + diff --git a/Cauldron.Macos/SourceWriter/SourceTextView.cs b/Cauldron.Macos/SourceWriter/SourceTextView.cs index acfd397..3bbcb47 100644 --- a/Cauldron.Macos/SourceWriter/SourceTextView.cs +++ b/Cauldron.Macos/SourceWriter/SourceTextView.cs @@ -52,6 +52,8 @@ public class SourceTextView : NSTextView /// Should the editor only use default words if the keyword list is empty. private bool _defaultWordsOnlyIfKeywordsEmpty = true; + private LineNumberRuler LineNumberRuler; + #endregion #region Computed Properties @@ -245,6 +247,28 @@ public class SourceTextView : NSTextView this.UsesAdaptiveColorMappingForDarkAppearance = true; } + public override void AwakeFromNib() + { + base.AwakeFromNib(); + + this.LineNumberRuler = new LineNumberRuler(this); + + this.EnclosingScrollView.VerticalRulerView = this.LineNumberRuler; + this.EnclosingScrollView.HasVerticalRuler = true; + this.EnclosingScrollView.RulersVisible = true; + + this.PostsFrameChangedNotifications = true; + NSView.Notifications.ObserveBoundsChanged((_, _) => this.DrawGutter()); + this.OnTextChanged += (_, _) => this.DrawGutter(); + } + + [Export("drawGutter")] + public void DrawGutter() + { + if (this.LineNumberRuler is not null) + this.LineNumberRuler.NeedsDisplay = true; + } + #endregion #region Private Methods @@ -678,6 +702,7 @@ public class SourceTextView : NSTextView // Console.WriteLine ("Read selection from pasteboard"); bool result = base.ReadSelectionFromPasteboard(pboard); Formatter?.Reformat(); + this.OnTextChanged?.Invoke(this, null); return result; } @@ -695,6 +720,7 @@ public class SourceTextView : NSTextView // Console.WriteLine ("Read selection from pasteboard also"); var result = base.ReadSelectionFromPasteboard(pboard, type); Formatter?.Reformat(); + this.OnTextChanged?.Invoke(this, null); return result; }