Skip to content

Commit 77e51db

Browse files
Add Letter Spacing (#174)
### Description Adds a `letterSpacing` parameter that determines the amount of space between characters. The parameter is a multiplier of the font's character width so `1.0` indicates no space, `2.0` indicates a full character width's space. This is exactly as described in #153. In detail: - The letter spacing parameter modifies a stored `kern` variable that is used for all default attributes. This PR also adds documentation for a couple undocumented parameters in the initializer and moves `lineHeight` into the `STTextViewController` initializer for a cleaner `CodeEditTextView`. ### Related Issues * closes #153 ### Checklist - [x] I read and understood the [contributing guide](https://github.com/CodeEditApp/CodeEdit/blob/main/CONTRIBUTING.md) as well as the [code of conduct](https://github.com/CodeEditApp/CodeEdit/blob/main/CODE_OF_CONDUCT.md) - [x] The issues this PR addresses are related to each other - [x] My changes generate no new warnings - [x] My code builds and runs on my machine - [x] My changes are all related to the related issue above - [x] I documented my code ### Screenshots The below recording shows a placeholder settings adjusting the letter spacing, as well as the spacing being applied while typing. https://user-images.githubusercontent.com/35942988/230160550-c540523c-4fb9-4984-8564-5bdebb7fbcb3.mov
1 parent cfceb13 commit 77e51db

File tree

4 files changed

+64
-11
lines changed

4 files changed

+64
-11
lines changed

Sources/CodeEditTextView/CodeEditTextView.swift

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,16 @@ public struct CodeEditTextView: NSViewControllerRepresentable {
2323
/// - lineHeight: The line height multiplier (e.g. `1.2`)
2424
/// - wrapLines: Whether lines wrap to the width of the editor
2525
/// - editorOverscroll: The percentage for overscroll, between 0-1 (default: `0.0`)
26+
/// - cursorPosition: The cursor's position in the editor, measured in `(lineNum, columnNum)`
27+
/// - useThemeBackground: Determines whether the editor uses the theme's background color, or a transparent
28+
/// background color
2629
/// - highlightProvider: A class you provide to perform syntax highlighting. Leave this as `nil` to use the
2730
/// built-in `TreeSitterClient` highlighter.
2831
/// - contentInsets: Insets to use to offset the content in the enclosing scroll view. Leave as `nil` to let the
2932
/// scroll view automatically adjust content insets.
3033
/// - isEditable: A Boolean value that controls whether the text view allows the user to edit text.
34+
/// - letterSpacing: The amount of space to use between letters, as a percent. Eg: `1.0` = no space, `1.5` = 1/2 a
35+
/// character's width between characters, etc. Defaults to `1.0`
3136
public init(
3237
_ text: Binding<String>,
3338
language: CodeLanguage,
@@ -42,7 +47,8 @@ public struct CodeEditTextView: NSViewControllerRepresentable {
4247
useThemeBackground: Bool = true,
4348
highlightProvider: HighlightProviding? = nil,
4449
contentInsets: NSEdgeInsets? = nil,
45-
isEditable: Bool = true
50+
isEditable: Bool = true,
51+
letterSpacing: Double = 1.0
4652
) {
4753
self._text = text
4854
self.language = language
@@ -58,6 +64,7 @@ public struct CodeEditTextView: NSViewControllerRepresentable {
5864
self.highlightProvider = highlightProvider
5965
self.contentInsets = contentInsets
6066
self.isEditable = isEditable
67+
self.letterSpacing = letterSpacing
6168
}
6269

6370
@Binding private var text: String
@@ -74,6 +81,7 @@ public struct CodeEditTextView: NSViewControllerRepresentable {
7481
private var highlightProvider: HighlightProviding?
7582
private var contentInsets: NSEdgeInsets?
7683
private var isEditable: Bool
84+
private var letterSpacing: Double
7785

7886
public typealias NSViewControllerType = STTextViewController
7987

@@ -85,15 +93,16 @@ public struct CodeEditTextView: NSViewControllerRepresentable {
8593
theme: theme,
8694
tabWidth: tabWidth,
8795
indentOption: indentOption,
96+
lineHeight: lineHeight,
8897
wrapLines: wrapLines,
8998
cursorPosition: $cursorPosition,
9099
editorOverscroll: editorOverscroll,
91100
useThemeBackground: useThemeBackground,
92101
highlightProvider: highlightProvider,
93102
contentInsets: contentInsets,
94-
isEditable: isEditable
103+
isEditable: isEditable,
104+
letterSpacing: letterSpacing
95105
)
96-
controller.lineHeightMultiple = lineHeight
97106
return controller
98107
}
99108

@@ -118,6 +127,9 @@ public struct CodeEditTextView: NSViewControllerRepresentable {
118127
if controller.tabWidth != tabWidth {
119128
controller.tabWidth = tabWidth
120129
}
130+
if controller.letterSpacing != letterSpacing {
131+
controller.letterSpacing = letterSpacing
132+
}
121133

122134
controller.reloadUI()
123135
return

Sources/CodeEditTextView/Controller/STTextViewController.swift

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,22 @@ public class STTextViewController: NSViewController, STTextViewDelegate, ThemeAt
7676
/// Optional insets to offset the text view in the scroll view by.
7777
public var contentInsets: NSEdgeInsets?
7878

79+
/// A multiplier that determines the amount of space between characters. `1.0` indicates no space,
80+
/// `2.0` indicates one character of space between other characters.
81+
public var letterSpacing: Double = 1.0 {
82+
didSet {
83+
kern = fontCharWidth * (letterSpacing - 1.0)
84+
reloadUI()
85+
}
86+
}
87+
88+
/// The kern to use for characters. Defaults to `0.0` and is updated when `letterSpacing` is set.
89+
private var kern: CGFloat = 0.0
90+
91+
private var fontCharWidth: CGFloat {
92+
(" " as NSString).size(withAttributes: [.font: font]).width
93+
}
94+
7995
// MARK: - Highlighting
8096

8197
internal var highlighter: Highlighter?
@@ -92,20 +108,23 @@ public class STTextViewController: NSViewController, STTextViewDelegate, ThemeAt
92108
theme: EditorTheme,
93109
tabWidth: Int,
94110
indentOption: IndentOption,
111+
lineHeight: Double,
95112
wrapLines: Bool,
96113
cursorPosition: Binding<(Int, Int)>,
97114
editorOverscroll: Double,
98115
useThemeBackground: Bool,
99116
highlightProvider: HighlightProviding? = nil,
100117
contentInsets: NSEdgeInsets? = nil,
101-
isEditable: Bool
118+
isEditable: Bool,
119+
letterSpacing: Double
102120
) {
103121
self.text = text
104122
self.language = language
105123
self.font = font
106124
self.theme = theme
107125
self.tabWidth = tabWidth
108126
self.indentOption = indentOption
127+
self.lineHeightMultiple = lineHeight
109128
self.wrapLines = wrapLines
110129
self.cursorPosition = cursorPosition
111130
self.editorOverscroll = editorOverscroll
@@ -122,7 +141,7 @@ public class STTextViewController: NSViewController, STTextViewDelegate, ThemeAt
122141

123142
// MARK: VC Lifecycle
124143

125-
// swiftlint:disable function_body_length
144+
// swiftlint:disable:next function_body_length
126145
public override func loadView() {
127146
textView = STTextView()
128147

@@ -234,7 +253,7 @@ public class STTextViewController: NSViewController, STTextViewDelegate, ThemeAt
234253
paragraph.minimumLineHeight = lineHeight
235254
paragraph.maximumLineHeight = lineHeight
236255
paragraph.tabStops.removeAll()
237-
paragraph.defaultTabInterval = CGFloat(tabWidth) * (" " as NSString).size(withAttributes: [.font: font]).width
256+
paragraph.defaultTabInterval = CGFloat(tabWidth) * fontCharWidth
238257
return paragraph
239258
}
240259

@@ -252,9 +271,6 @@ public class STTextViewController: NSViewController, STTextViewDelegate, ThemeAt
252271

253272
/// Reloads the UI to apply changes to ``STTextViewController/font``, ``STTextViewController/theme``, ...
254273
internal func reloadUI() {
255-
// if font or baseline has been modified, set the hasSetStandardAttributesFlag
256-
// to false to ensure attributes are updated. This allows live UI updates when changing preferences.
257-
258274
textView?.textColor = theme.text
259275
textView.backgroundColor = useThemeBackground ? theme.background : .clear
260276
textView?.insertionPointColor = theme.insertionPoint
@@ -293,7 +309,8 @@ public class STTextViewController: NSViewController, STTextViewDelegate, ThemeAt
293309
.font: font,
294310
.foregroundColor: theme.colorFor(capture),
295311
.baselineOffset: baselineOffset,
296-
.paragraphStyle: paragraphStyle
312+
.paragraphStyle: paragraphStyle,
313+
.kern: kern
297314
]
298315
}
299316

Tests/CodeEditTextViewTests/CodeEditTextViewTests.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,4 @@ final class CodeEditTextViewTests: XCTestCase {
4040
XCTAssertEqual(result, expected)
4141
}
4242
}
43+
// swiftlint:enable all

Tests/CodeEditTextViewTests/STTextViewControllerTests.swift

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import SwiftTreeSitter
44
import AppKit
55
import TextStory
66

7+
// swiftlint:disable all
78
final class STTextViewControllerTests: XCTestCase {
89

910
var controller: STTextViewController!
@@ -35,11 +36,13 @@ final class STTextViewControllerTests: XCTestCase {
3536
theme: theme,
3637
tabWidth: 4,
3738
indentOption: .spaces(count: 4),
39+
lineHeight: 1.0,
3840
wrapLines: true,
3941
cursorPosition: .constant((1, 1)),
4042
editorOverscroll: 0.5,
4143
useThemeBackground: true,
42-
isEditable: true
44+
isEditable: true,
45+
letterSpacing: 1.0
4346
)
4447

4548
controller.loadView()
@@ -193,4 +196,24 @@ final class STTextViewControllerTests: XCTestCase {
193196
controller.textView.insertText("\t", replacementRange: .zero)
194197
XCTAssertEqual(controller.textView.string, String(repeating: " ", count: 1000))
195198
}
199+
200+
func test_letterSpacing() {
201+
let font: NSFont = .monospacedSystemFont(ofSize: 11, weight: .medium)
202+
203+
controller.letterSpacing = 1.0
204+
205+
XCTAssertEqual(
206+
controller.attributesFor(nil)[.kern]! as! CGFloat,
207+
(" " as NSString).size(withAttributes: [.font: font]).width * 0.0
208+
)
209+
210+
controller.letterSpacing = 2.0
211+
XCTAssertEqual(
212+
controller.attributesFor(nil)[.kern]! as! CGFloat,
213+
(" " as NSString).size(withAttributes: [.font: font]).width * 1.0
214+
)
215+
216+
controller.letterSpacing = 1.0
217+
}
196218
}
219+
// swiftlint:enable all

0 commit comments

Comments
 (0)