diff --git a/sources/SquirrelPanel.swift b/sources/SquirrelPanel.swift index c520d78c5..93287b7ef 100644 --- a/sources/SquirrelPanel.swift +++ b/sources/SquirrelPanel.swift @@ -223,7 +223,11 @@ final class SquirrelPanel: NSPanel { let line = NSMutableAttributedString(string: theme.candidateFormat, attributes: labelAttrs) for range in line.string.ranges(of: /\[candidate\]/) { - line.addAttributes(attrs, range: convert(range: range, in: line.string)) + let convertedRange = convert(range: range, in: line.string) + line.addAttributes(attrs, range: convertedRange) + if candidate.count <= 5 { + line.addAttribute(.noBreak, value: true, range: NSMakeRange(convertedRange.location+1, convertedRange.length-1)) + } } for range in line.string.ranges(of: /\[comment\]/) { line.addAttributes(commentAttrs, range: convert(range: range, in: line.string)) @@ -233,6 +237,10 @@ final class SquirrelPanel: NSPanel { line.mutableString.replaceOccurrences(of: "[candidate]", with: candidate, range: NSMakeRange(0, line.length)) line.mutableString.replaceOccurrences(of: "[comment]", with: comment, range: NSMakeRange(0, line.length)) + if line.length <= 10 { + line.addAttribute(.noBreak, value: true, range: NSMakeRange(1, line.length-1)) + } + let lineSeparator = NSAttributedString(string: linear ? " " : "\n", attributes: attrs) if i > 0 { text.append(lineSeparator) diff --git a/sources/SquirrelView.swift b/sources/SquirrelView.swift index 5d17df6d6..43b6bcb47 100644 --- a/sources/SquirrelView.swift +++ b/sources/SquirrelView.swift @@ -10,6 +10,7 @@ import AppKit final class SquirrelView: NSView { let textView: NSTextView + private let squirrelLayoutDelegate: SquirrelLayoutDelegate var candidateRanges: [NSRange] = [] var hilightedIndex = 0 var preeditRange = NSMakeRange(NSNotFound, 0) @@ -33,10 +34,12 @@ final class SquirrelView: NSView { } override init(frame frameRect: NSRect) { + squirrelLayoutDelegate = SquirrelLayoutDelegate() textView = NSTextView(frame: frameRect) textView.drawsBackground = false textView.isEditable = false textView.isSelectable = false + textView.textLayoutManager?.delegate = squirrelLayoutDelegate super.init(frame: frameRect) textContainer.lineFragmentPadding = 0 self.wantsLayer = true @@ -57,7 +60,7 @@ final class SquirrelView: NSView { guard range.location != NSNotFound else { return nil } guard let startLocation = textLayoutManager.location(textLayoutManager.documentRange.location, offsetBy: range.location) else { return nil } guard let endLocation = textLayoutManager.location(startLocation, offsetBy: range.length) else { return nil } - return NSTextRange(location: startLocation, end: endLocation) ?? textLayoutManager.documentRange + return NSTextRange(location: startLocation, end: endLocation) } // Get the rectangle containing entire contents, expensive to calculate @@ -679,3 +682,17 @@ private extension SquirrelView { return newRect } } + +fileprivate class SquirrelLayoutDelegate: NSObject, NSTextLayoutManagerDelegate { + func textLayoutManager(_ textLayoutManager: NSTextLayoutManager, shouldBreakLineBefore location: any NSTextLocation, hyphenating: Bool) -> Bool { + let index = textLayoutManager.offset(from: textLayoutManager.documentRange.location, to: location) + if let attributes = textLayoutManager.textContainer?.textView?.textContentStorage?.attributedString?.attributes(at: index, effectiveRange: nil), let noBreak = attributes[.noBreak] as? Bool, noBreak { + return false + } + return true + } +} + +extension NSAttributedString.Key { + static let noBreak = NSAttributedString.Key("noBreak") +}