fix: keep overlay attributed colors and auto-resize
This commit is contained in:
@@ -190,13 +190,26 @@ final class VoiceWakeOverlayController: ObservableObject {
|
|||||||
private func measuredHeight() -> CGFloat {
|
private func measuredHeight() -> CGFloat {
|
||||||
let attributed = self.model.attributed.length > 0 ? self.model.attributed : self.makeAttributed(from: self.model.text)
|
let attributed = self.model.attributed.length > 0 ? self.model.attributed : self.makeAttributed(from: self.model.text)
|
||||||
let maxWidth = self.width - (self.padding * 2) - self.spacing - self.buttonWidth
|
let maxWidth = self.width - (self.padding * 2) - self.spacing - self.buttonWidth
|
||||||
let rect = attributed.boundingRect(
|
|
||||||
with: CGSize(width: maxWidth, height: .greatestFiniteMagnitude),
|
let textInset = NSSize(width: 2, height: 6)
|
||||||
options: [.usesLineFragmentOrigin, .usesFontLeading],
|
let lineFragmentPadding: CGFloat = 0
|
||||||
context: nil)
|
let containerWidth = max(1, maxWidth - (textInset.width * 2) - (lineFragmentPadding * 2))
|
||||||
let contentHeight = ceil(rect.height)
|
|
||||||
|
let storage = NSTextStorage(attributedString: attributed)
|
||||||
|
let container = NSTextContainer(containerSize: CGSize(width: containerWidth, height: .greatestFiniteMagnitude))
|
||||||
|
container.lineFragmentPadding = lineFragmentPadding
|
||||||
|
container.lineBreakMode = .byWordWrapping
|
||||||
|
|
||||||
|
let layout = NSLayoutManager()
|
||||||
|
layout.addTextContainer(container)
|
||||||
|
storage.addLayoutManager(layout)
|
||||||
|
|
||||||
|
_ = layout.glyphRange(for: container)
|
||||||
|
let used = layout.usedRect(for: container)
|
||||||
|
|
||||||
|
let contentHeight = ceil(used.height + (textInset.height * 2))
|
||||||
let total = contentHeight + self.verticalPadding * 2
|
let total = contentHeight + self.verticalPadding * 2
|
||||||
return max(42, min(total, 220))
|
return max(48, min(total, 220))
|
||||||
}
|
}
|
||||||
|
|
||||||
private func dismissTargetFrame(for frame: NSRect, reason: DismissReason, outcome: SendOutcome) -> NSRect? {
|
private func dismissTargetFrame(for frame: NSRect, reason: DismissReason, outcome: SendOutcome) -> NSRect? {
|
||||||
@@ -252,7 +265,7 @@ private struct VoiceWakeOverlayView: View {
|
|||||||
self.controller.sendNow()
|
self.controller.sendNow()
|
||||||
})
|
})
|
||||||
.focused(self.$focused)
|
.focused(self.$focused)
|
||||||
.frame(minHeight: 32)
|
.frame(minHeight: 32, maxHeight: .infinity)
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
self.controller.sendNow()
|
self.controller.sendNow()
|
||||||
@@ -280,6 +293,7 @@ private struct VoiceWakeOverlayView: View {
|
|||||||
}
|
}
|
||||||
.padding(.vertical, 8)
|
.padding(.vertical, 8)
|
||||||
.padding(.horizontal, 10)
|
.padding(.horizontal, 10)
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
||||||
.background(.regularMaterial)
|
.background(.regularMaterial)
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
|
.clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
|
||||||
.onAppear { self.focused = false }
|
.onAppear { self.focused = false }
|
||||||
@@ -312,13 +326,24 @@ private struct TranscriptTextView: NSViewRepresentable {
|
|||||||
textView.isAutomaticQuoteSubstitutionEnabled = false
|
textView.isAutomaticQuoteSubstitutionEnabled = false
|
||||||
textView.isAutomaticTextReplacementEnabled = false
|
textView.isAutomaticTextReplacementEnabled = false
|
||||||
textView.font = .systemFont(ofSize: 13, weight: .regular)
|
textView.font = .systemFont(ofSize: 13, weight: .regular)
|
||||||
textView.textContainerInset = NSSize(width: 2, height: 6)
|
|
||||||
textView.textContainer?.lineBreakMode = .byWordWrapping
|
textView.textContainer?.lineBreakMode = .byWordWrapping
|
||||||
|
textView.textContainer?.lineFragmentPadding = 0
|
||||||
|
textView.textContainerInset = NSSize(width: 2, height: 6)
|
||||||
|
|
||||||
|
textView.minSize = .zero
|
||||||
textView.maxSize = NSSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
|
textView.maxSize = NSSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
|
||||||
|
textView.isHorizontallyResizable = false
|
||||||
|
textView.isVerticallyResizable = true
|
||||||
|
textView.autoresizingMask = [.width]
|
||||||
|
|
||||||
|
textView.textContainer?.containerSize = NSSize(width: 0, height: CGFloat.greatestFiniteMagnitude)
|
||||||
textView.textContainer?.widthTracksTextView = true
|
textView.textContainer?.widthTracksTextView = true
|
||||||
textView.textContainer?.containerSize = NSSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
|
|
||||||
textView.string = self.text
|
|
||||||
textView.textStorage?.setAttributedString(self.attributed)
|
textView.textStorage?.setAttributedString(self.attributed)
|
||||||
|
textView.typingAttributes = [
|
||||||
|
.foregroundColor: NSColor.labelColor,
|
||||||
|
.font: NSFont.systemFont(ofSize: 13, weight: .regular),
|
||||||
|
]
|
||||||
textView.focusRingType = .none
|
textView.focusRingType = .none
|
||||||
textView.onSend = { [weak textView] in
|
textView.onSend = { [weak textView] in
|
||||||
textView?.window?.makeFirstResponder(nil)
|
textView?.window?.makeFirstResponder(nil)
|
||||||
@@ -339,16 +364,19 @@ private struct TranscriptTextView: NSViewRepresentable {
|
|||||||
guard let textView = scrollView.documentView as? TranscriptNSTextView else { return }
|
guard let textView = scrollView.documentView as? TranscriptNSTextView else { return }
|
||||||
let isEditing = scrollView.window?.firstResponder == textView
|
let isEditing = scrollView.window?.firstResponder == textView
|
||||||
if isEditing {
|
if isEditing {
|
||||||
if textView.string != self.text {
|
return
|
||||||
textView.string = self.text
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
|
if !textView.attributedString().isEqual(to: self.attributed) {
|
||||||
|
context.coordinator.isProgrammaticUpdate = true
|
||||||
|
defer { context.coordinator.isProgrammaticUpdate = false }
|
||||||
textView.textStorage?.setAttributedString(self.attributed)
|
textView.textStorage?.setAttributedString(self.attributed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final class Coordinator: NSObject, NSTextViewDelegate {
|
final class Coordinator: NSObject, NSTextViewDelegate {
|
||||||
var parent: TranscriptTextView
|
var parent: TranscriptTextView
|
||||||
|
var isProgrammaticUpdate = false
|
||||||
|
|
||||||
init(_ parent: TranscriptTextView) { self.parent = parent }
|
init(_ parent: TranscriptTextView) { self.parent = parent }
|
||||||
|
|
||||||
@@ -357,7 +385,9 @@ private struct TranscriptTextView: NSViewRepresentable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func textDidChange(_ notification: Notification) {
|
func textDidChange(_ notification: Notification) {
|
||||||
|
guard !self.isProgrammaticUpdate else { return }
|
||||||
guard let view = notification.object as? NSTextView else { return }
|
guard let view = notification.object as? NSTextView else { return }
|
||||||
|
guard view.window?.firstResponder === view else { return }
|
||||||
self.parent.text = view.string
|
self.parent.text = view.string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user