Skip to content

Commit

Permalink
Add Text Insets
Browse files Browse the repository at this point in the history
  • Loading branch information
thecoolwinter committed Jan 12, 2025
1 parent 09a3f21 commit f1888f0
Show file tree
Hide file tree
Showing 9 changed files with 175 additions and 93 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@
CODE_SIGN_ENTITLEMENTS = CodeEditTextViewExample/CodeEditTextViewExample.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"CodeEditTextViewExample/Preview Content\"";
DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
Expand Down Expand Up @@ -226,7 +226,7 @@
CODE_SIGN_ENTITLEMENTS = CodeEditTextViewExample/CodeEditTextViewExample.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"CodeEditTextViewExample/Preview Content\"";
DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,18 @@ import SwiftUI

struct ContentView: View {
@Binding var document: CodeEditTextViewExampleDocument
@AppStorage("wraplines") private var wrapLines: Bool = true
@AppStorage("edgeinsets") private var enableEdgeInsets: Bool = false

var body: some View {
SwiftUITextView(text: $document.text)
VStack(spacing: 0) {
HStack {
Toggle("Wrap Lines", isOn: $wrapLines)
Toggle("Inset Edges", isOn: $enableEdgeInsets)
}
Divider()
SwiftUITextView(text: $document.text, wrapLines: $wrapLines, enableEdgeInsets: $enableEdgeInsets)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,20 @@ import CodeEditTextView

struct SwiftUITextView: NSViewControllerRepresentable {
@Binding var text: String
@Binding var wrapLines: Bool
@Binding var enableEdgeInsets: Bool

func makeNSViewController(context: Context) -> TextViewController {
let controller = TextViewController(string: text)
context.coordinator.controller = controller
controller.wrapLines = wrapLines
controller.enableEdgeInsets = enableEdgeInsets
return controller
}

func updateNSViewController(_ nsViewController: TextViewController, context: Context) {
// Do nothing, our binding has to be a one-way binding
nsViewController.wrapLines = wrapLines
nsViewController.enableEdgeInsets = enableEdgeInsets
}

func makeCoordinator() -> Coordinator {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,22 @@ import CodeEditTextView
class TextViewController: NSViewController {
var scrollView: NSScrollView!
var textView: TextView!
var enableEdgeInsets: Bool = false {
didSet {
if enableEdgeInsets {
textView.edgeInsets = .init(left: 20, right: 30)
textView.textInsets = .init(left: 10, right: 30)
} else {
textView.edgeInsets = .zero
textView.textInsets = .zero
}
}
}
var wrapLines: Bool = true {
didSet {
textView.wrapLines = wrapLines
}
}

init(string: String) {
textView = TextView(string: string)
Expand All @@ -24,6 +40,14 @@ class TextViewController: NSViewController {
override func loadView() {
scrollView = NSScrollView()
textView.translatesAutoresizingMaskIntoConstraints = false
textView.wrapLines = wrapLines
if enableEdgeInsets {
textView.edgeInsets = .init(left: 30, right: 30)
textView.textInsets = .init(left: 0, right: 30)
} else {
textView.edgeInsets = .zero
textView.textInsets = .zero
}

scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.documentView = textView
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
//
// File.swift
// CodeEditTextView
//
// Created by Khan Winter on 1/12/25.
//

import AppKit

extension TextSelectionManager {
/// Draws line backgrounds and selection rects for each selection in the given rect.
/// - Parameter rect: The rect to draw in.
func drawSelections(in rect: NSRect) {
guard let context = NSGraphicsContext.current?.cgContext else { return }
context.saveGState()
var highlightedLines: Set<UUID> = []
// For each selection in the rect
for textSelection in textSelections {
if textSelection.range.isEmpty {
drawHighlightedLine(
in: rect,
for: textSelection,
context: context,
highlightedLines: &highlightedLines
)
} else {
drawSelectedRange(in: rect, for: textSelection, context: context)
}
}
context.restoreGState()
}

/// Draws a highlighted line in the given rect.
/// - Parameters:
/// - rect: The rect to draw in.
/// - textSelection: The selection to draw.
/// - context: The context to draw in.
/// - highlightedLines: The set of all lines that have already been highlighted, used to avoid highlighting lines
/// twice and updated if this function comes across a new line id.
private func drawHighlightedLine(
in rect: NSRect,
for textSelection: TextSelection,
context: CGContext,
highlightedLines: inout Set<UUID>
) {
guard let linePosition = layoutManager?.textLineForOffset(textSelection.range.location),
!highlightedLines.contains(linePosition.data.id) else {
return
}
highlightedLines.insert(linePosition.data.id)
context.saveGState()

let insetXPos = max(rect.minX, edgeInsets.left)
let maxWidth = (textView?.frame.width ?? 0) - insetXPos - edgeInsets.right

let selectionRect = CGRect(
x: insetXPos,
y: linePosition.yPos,
width: min(rect.width, maxWidth),
height: linePosition.height
).pixelAligned

if selectionRect.intersects(rect) {
context.setFillColor(selectedLineBackgroundColor.cgColor)
context.fill(selectionRect)
}
context.restoreGState()
}

/// Draws a selected range in the given context.
/// - Parameters:
/// - rect: The rect to draw in.
/// - range: The range to highlight.
/// - context: The context to draw in.
private func drawSelectedRange(in rect: NSRect, for textSelection: TextSelection, context: CGContext) {
context.saveGState()

let fillColor = (textView?.isFirstResponder ?? false)
? selectionBackgroundColor.cgColor
: selectionBackgroundColor.grayscale.cgColor

context.setFillColor(fillColor)

let fillRects = getFillRects(in: rect, for: textSelection)

let minX = fillRects.min(by: { $0.origin.x < $1.origin.x })?.origin.x ?? 0
let minY = fillRects.min(by: { $0.origin.y < $1.origin.y })?.origin.y ?? 0
let max = fillRects.max(by: { $0.maxY < $1.maxY }) ?? .zero
let origin = CGPoint(x: minX, y: minY)
let size = CGSize(width: max.maxX - minX, height: max.maxY - minY)
textSelection.boundingRect = CGRect(origin: origin, size: size)

context.fill(fillRects)
context.restoreGState()
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ extension TextSelectionManager {
return []
}

let insetXPos = max(layoutManager.edgeInsets.left, rect.minX)
let insetWidth = max(0, rect.maxX - insetXPos - layoutManager.edgeInsets.right)
let insetXPos = max(edgeInsets.left, rect.minX)
let insetWidth = max(0, rect.maxX - insetXPos - edgeInsets.right)
let insetRect = NSRect(x: insetXPos, y: rect.origin.y, width: insetWidth, height: rect.height)

// Calculate the first line and any rects selected
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ public class TextSelectionManager: NSObject {
}
}

/// Determines how far inset to draw selection content.
public var edgeInsets: HorizontalEdgeInsets = .zero {
didSet {
delegate?.setNeedsDisplay()
}
}

internal(set) public var textSelections: [TextSelection] = []
weak var layoutManager: TextLayoutManager?
weak var textStorage: NSTextStorage?
Expand Down Expand Up @@ -224,87 +231,4 @@ public class TextSelectionManager: NSObject {
textSelection.view?.removeFromSuperview()
}
}

// MARK: - Draw

/// Draws line backgrounds and selection rects for each selection in the given rect.
/// - Parameter rect: The rect to draw in.
func drawSelections(in rect: NSRect) {
guard let context = NSGraphicsContext.current?.cgContext else { return }
context.saveGState()
var highlightedLines: Set<UUID> = []
// For each selection in the rect
for textSelection in textSelections {
if textSelection.range.isEmpty {
drawHighlightedLine(
in: rect,
for: textSelection,
context: context,
highlightedLines: &highlightedLines
)
} else {
drawSelectedRange(in: rect, for: textSelection, context: context)
}
}
context.restoreGState()
}

/// Draws a highlighted line in the given rect.
/// - Parameters:
/// - rect: The rect to draw in.
/// - textSelection: The selection to draw.
/// - context: The context to draw in.
/// - highlightedLines: The set of all lines that have already been highlighted, used to avoid highlighting lines
/// twice and updated if this function comes across a new line id.
private func drawHighlightedLine(
in rect: NSRect,
for textSelection: TextSelection,
context: CGContext,
highlightedLines: inout Set<UUID>
) {
guard let linePosition = layoutManager?.textLineForOffset(textSelection.range.location),
!highlightedLines.contains(linePosition.data.id) else {
return
}
highlightedLines.insert(linePosition.data.id)
context.saveGState()
let selectionRect = CGRect(
x: rect.minX,
y: linePosition.yPos,
width: rect.width,
height: linePosition.height
)
if selectionRect.intersects(rect) {
context.setFillColor(selectedLineBackgroundColor.cgColor)
context.fill(selectionRect)
}
context.restoreGState()
}

/// Draws a selected range in the given context.
/// - Parameters:
/// - rect: The rect to draw in.
/// - range: The range to highlight.
/// - context: The context to draw in.
private func drawSelectedRange(in rect: NSRect, for textSelection: TextSelection, context: CGContext) {
context.saveGState()

let fillColor = (textView?.isFirstResponder ?? false)
? selectionBackgroundColor.cgColor
: selectionBackgroundColor.grayscale.cgColor

context.setFillColor(fillColor)

let fillRects = getFillRects(in: rect, for: textSelection)

let minX = fillRects.min(by: { $0.origin.x < $1.origin.x })?.origin.x ?? 0
let minY = fillRects.min(by: { $0.origin.y < $1.origin.y })?.origin.y ?? 0
let max = fillRects.max(by: { $0.maxY < $1.maxY }) ?? .zero
let origin = CGPoint(x: minX, y: minY)
let size = CGSize(width: max.maxX - minX, height: max.maxY - minY)
textSelection.boundingRect = CGRect(origin: origin, size: size)

context.fill(fillRects)
context.restoreGState()
}
}
21 changes: 18 additions & 3 deletions Sources/CodeEditTextView/TextView/TextView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -150,13 +150,28 @@ public class TextView: NSView, NSTextContent {
}
}

/// The edge insets for the text view.
/// The edge insets for the text view. This value insets every piece of drawable content in the view, including
/// selection rects.
///
/// To further inset the text from the edge, without modifying how selections are inset, use ``textInsets``
public var edgeInsets: HorizontalEdgeInsets {
get {
layoutManager?.edgeInsets ?? .zero
selectionManager.edgeInsets
}
set {
layoutManager.edgeInsets = newValue + textInsets
selectionManager.edgeInsets = newValue
}
}

/// Insets just drawn text from the horizontal edges. This is in addition to the insets in ``edgeInsets``, but does
/// not apply to other drawn content.
public var textInsets: HorizontalEdgeInsets {
get {
layoutManager.edgeInsets - selectionManager.edgeInsets
}
set {
layoutManager?.edgeInsets = newValue
layoutManager.edgeInsets = edgeInsets + newValue
}
}

Expand Down
10 changes: 9 additions & 1 deletion Sources/CodeEditTextView/Utils/HorizontalEdgeInsets.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import Foundation

public struct HorizontalEdgeInsets: Codable, Sendable, Equatable {
public struct HorizontalEdgeInsets: Codable, Sendable, Equatable, AdditiveArithmetic {
public var left: CGFloat
public var right: CGFloat

Expand All @@ -29,4 +29,12 @@ public struct HorizontalEdgeInsets: Codable, Sendable, Equatable {
public static let zero: HorizontalEdgeInsets = {
HorizontalEdgeInsets(left: 0, right: 0)
}()

public static func + (lhs: HorizontalEdgeInsets, rhs: HorizontalEdgeInsets) -> HorizontalEdgeInsets {
HorizontalEdgeInsets(left: lhs.left + rhs.left, right: lhs.right + rhs.right)
}

public static func - (lhs: HorizontalEdgeInsets, rhs: HorizontalEdgeInsets) -> HorizontalEdgeInsets {
HorizontalEdgeInsets(left: lhs.left - rhs.left, right: lhs.right - rhs.right)
}
}

0 comments on commit f1888f0

Please sign in to comment.