-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathEditableLabel.swift
More file actions
258 lines (201 loc) · 7.64 KB
/
EditableLabel.swift
File metadata and controls
258 lines (201 loc) · 7.64 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
//
// EditableLabel.swift
// Omega
//
// Created by Mark Onyschuk on 2019-06-29.
// Copyright © 2019 Mark Onyschuk. All rights reserved.
//
import SwiftUI
struct EditableLabel : NSViewRepresentable {
@Binding var text: String
var wraps: Bool = false
var minWidth: CGFloat? = 0
var maxWidth: CGFloat? = 144
var didChange: (String)->() = { _ in }
var didEndEditing: (String)->Bool = { _ in true }
func makeCoordinator() -> EditableLabelCoordinator {
return EditableLabelCoordinator(didChange: didChange, didEndEditing: didEndEditing)
}
func makeNSView(context: NSViewRepresentableContext<EditableLabel>) -> EditableLabelView {
let view = EditableLabelView(text: "")
view.wraps = wraps
view.minWidth = minWidth
view.maxWidth = maxWidth
view.delegate = context.coordinator
return view
}
func updateNSView(_ view: EditableLabelView, context: NSViewRepresentableContext<EditableLabel>) {
view.stringValue = text
}
}
#if DEBUG
struct EditableLabel_Previews : PreviewProvider {
static var previews: some View {
EditableLabel(text: .constant("Hello World"))
}
}
#endif
// MARK: - Coordinator
final class EditableLabelCoordinator: NSObject, NSTextFieldDelegate {
var didChange: (String)->()
var didEndEditing: (String)->Bool
init(didChange: @escaping (String)->(), didEndEditing: @escaping (String)->Bool) {
self.didChange = didChange; self.didEndEditing = didEndEditing
}
func controlTextDidChange(_ obj: Notification) {
guard let textField = obj.object as? NSTextField else {
return
}
didChange(textField.stringValue)
}
func controlTextDidEndEditing(_ obj: Notification) {
guard let textField = obj.object as? NSTextField else {
return
}
if didEndEditing(textField.stringValue) {
textField.target = self
textField.action = #selector(endEditing(_:))
}
}
@IBAction func endEditing(_ sender: NSTextField) {
sender.window?.makeFirstResponder(sender.window?.contentView)
sender.target = nil
sender.action = nil
}
}
// MARK: - View
final class EditableLabelView: NSTextField {
var minWidth: CGFloat? {
didSet {
needsUpdate()
}
}
var maxWidth: CGFloat? {
didSet {
needsUpdate()
}
}
lazy var maxConstraint: NSLayoutConstraint = {
return widthAnchor.constraint(lessThanOrEqualToConstant: 1)
}()
lazy var minConstraint: NSLayoutConstraint = {
return widthAnchor.constraint(greaterThanOrEqualToConstant: 1)
}()
@IBInspectable
var wraps: Bool = false {
didSet {
if let cell = cell as? NSTextFieldCell {
cell.wraps = wraps
cell.isScrollable = !wraps
}
needsUpdate()
}
}
private func needsUpdate() {
self.needsUpdateConstraints = true
}
override func updateConstraints() {
super.updateConstraints()
minConstraint.isActive = false
maxConstraint.isActive = false
if wraps {
let minW = minWidth ?? -1
let maxW = maxWidth ?? -1
let width = max(minW, maxW)
if width != -1 {
minConstraint.constant = width
minConstraint.isActive = true
maxConstraint.constant = width
maxConstraint.isActive = true
}
} else {
if let width = minWidth {
minConstraint.constant = width
minConstraint.isActive = true
}
if let width = maxWidth {
maxConstraint.constant = width
maxConstraint.isActive = true
}
}
}
// MARK: - Layout
@objc override func textDidChange(_ notification: Notification) {
super.textDidChange(notification)
invalidateIntrinsicContentSize()
}
override var intrinsicContentSize: NSSize {
var size = CGSize.zero
if let fieldEditor = self.currentEditor() as? NSTextView, let clipView = fieldEditor.superview as? NSClipView {
if wraps {
// the field editor may scroll slightly during edits
// regardless of whether we specify the cell to be scrollable:
// as a result, we fix the field editor's width prior to calculating height
let clipBounds = clipView.bounds
var frame = fieldEditor.frame
if NSWidth(frame) > NSWidth(clipBounds) {
frame.size.width = NSWidth(clipBounds)
fieldEditor.frame = frame
}
}
if let textContainer = fieldEditor.textContainer, let layoutManager = fieldEditor.layoutManager {
let usedRect = layoutManager.usedRect(for: textContainer)
let clipRect = convert(clipView.bounds, from: fieldEditor.superview)
let clipDelta = NSSize(width: NSWidth(bounds) - NSWidth(clipRect), height: NSHeight(bounds) - NSHeight(clipRect))
if wraps {
let minHeight = layoutManager.defaultLineHeight(for: font!)
size = NSSize(width: NSView.noIntrinsicMetric, height: max(NSHeight(usedRect), minHeight) + clipDelta.height)
} else {
size = NSSize(width: ceil(NSWidth(usedRect) + clipDelta.width), height: NSHeight(usedRect) + clipDelta.height)
}
}
} else {
if let cell = cell as? NSTextFieldCell {
if wraps {
// oddly, this sometimes gives incorrect results -
// if anyone has any ideas please issue a pull request
size = cell.cellSize(forBounds: NSMakeRect(0, 0, NSWidth(bounds), CGFloat.greatestFiniteMagnitude))
size.width = NSView.noIntrinsicMetric
size.height = ceil(size.height)
} else {
size = cell.cellSize(forBounds: NSMakeRect(0, 0, CGFloat.greatestFiniteMagnitude, CGFloat.greatestFiniteMagnitude))
size.width = ceil(size.width)
size.height = ceil(size.height)
}
}
}
if let font = self.font {
size.height = max(
EditableLabelView.lm.defaultLineHeight(for: font),
size.height
)
}
return size
}
private static var lm = NSLayoutManager()
// MARK: - Lifecycle
init(text: String) {
super.init(frame: .zero)
configure(text: text)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
configure(text: stringValue)
}
private func configure(text: String?) {
isBordered = false
focusRingType = .none
drawsBackground = false
// FIXME: this doesn't work...
lineBreakMode = .byTruncatingTail
if let stringValue = text {
self.stringValue = stringValue
}
translatesAutoresizingMaskIntoConstraints = false
if let cell = cell as? NSTextFieldCell {
cell.wraps = wraps
cell.isScrollable = !wraps
cell.truncatesLastVisibleLine = true
}
}
}