mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-23 06:35:51 +00:00
Code highlight improvements
This commit is contained in:
@@ -13,6 +13,9 @@ import Pasteboard
|
||||
import ChatTextLinkEditUI
|
||||
import MobileCoreServices
|
||||
import ImageTransparency
|
||||
import ChatInputTextNode
|
||||
import TextInputMenu
|
||||
import ObjCRuntimeUtils
|
||||
|
||||
public final class EmptyInputView: UIView, UIInputViewAudioFeedback {
|
||||
public var enableInputClicksWhenVisible: Bool {
|
||||
@@ -180,28 +183,9 @@ public final class TextFieldComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
final class TextView: UITextView {
|
||||
var onPaste: () -> Bool = { return true }
|
||||
|
||||
override func paste(_ sender: Any?) {
|
||||
if self.onPaste() {
|
||||
super.paste(sender)
|
||||
}
|
||||
}
|
||||
|
||||
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
|
||||
if action == #selector(self.paste(_:)) {
|
||||
return true
|
||||
}
|
||||
return super.canPerformAction(action, withSender: sender)
|
||||
}
|
||||
}
|
||||
|
||||
public final class View: UIView, UITextViewDelegate, UIScrollViewDelegate {
|
||||
private let textContainer: NSTextContainer
|
||||
private let textStorage: NSTextStorage
|
||||
private let layoutManager: NSLayoutManager
|
||||
private let textView: TextView
|
||||
public final class View: UIView, UIScrollViewDelegate, ChatInputTextNodeDelegate {
|
||||
private let textView: ChatInputTextView
|
||||
private let inputMenu: TextInputMenu
|
||||
|
||||
private var spoilerView: InvisibleInkDustView?
|
||||
private var customEmojiContainerView: CustomEmojiContainerView?
|
||||
@@ -216,22 +200,10 @@ public final class TextFieldComponent: Component {
|
||||
|
||||
private var component: TextFieldComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
private var isUpdating: Bool = false
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.textContainer = NSTextContainer(size: CGSize())
|
||||
self.textContainer.widthTracksTextView = false
|
||||
self.textContainer.heightTracksTextView = false
|
||||
self.textContainer.lineBreakMode = .byWordWrapping
|
||||
self.textContainer.lineFragmentPadding = 8.0
|
||||
|
||||
self.textStorage = NSTextStorage()
|
||||
|
||||
self.layoutManager = NSLayoutManager()
|
||||
self.layoutManager.allowsNonContiguousLayout = false
|
||||
self.layoutManager.addTextContainer(self.textContainer)
|
||||
self.textStorage.addLayoutManager(self.layoutManager)
|
||||
|
||||
self.textView = TextView(frame: CGRect(), textContainer: self.textContainer)
|
||||
self.textView = ChatInputTextView(disableTiling: false)
|
||||
self.textView.translatesAutoresizingMaskIntoConstraints = false
|
||||
self.textView.backgroundColor = nil
|
||||
self.textView.layer.isOpaque = false
|
||||
@@ -239,16 +211,15 @@ public final class TextFieldComponent: Component {
|
||||
self.textView.indicatorStyle = .white
|
||||
self.textView.scrollIndicatorInsets = UIEdgeInsets(top: 9.0, left: 0.0, bottom: 9.0, right: 0.0)
|
||||
|
||||
self.inputMenu = TextInputMenu(hasSpoilers: true, hasQuotes: true)
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.clipsToBounds = true
|
||||
|
||||
self.textView.delegate = self
|
||||
self.textView.customDelegate = self
|
||||
self.addSubview(self.textView)
|
||||
|
||||
self.textContainer.widthTracksTextView = false
|
||||
self.textContainer.heightTracksTextView = false
|
||||
|
||||
if #available(iOS 13.0, *) {
|
||||
self.textView.overrideUserInterfaceStyle = .dark
|
||||
}
|
||||
@@ -257,10 +228,6 @@ public final class TextFieldComponent: Component {
|
||||
NSAttributedString.Key.font: Font.regular(17.0),
|
||||
NSAttributedString.Key.foregroundColor: UIColor.white
|
||||
]
|
||||
|
||||
self.textView.onPaste = { [weak self] in
|
||||
return self?.onPaste() ?? false
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
@@ -274,12 +241,20 @@ public final class TextFieldComponent: Component {
|
||||
|
||||
let inputState = f(self.inputState)
|
||||
|
||||
self.textView.attributedText = textAttributedStringForStateText(inputState.inputText, fontSize: component.fontSize, textColor: component.textColor, accentTextColor: component.textColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(component.context.animatedEmojiStickers.keys), emojiViewProvider: self.emojiViewProvider)
|
||||
let currentAttributedText = self.textView.attributedText
|
||||
let updatedAttributedText = textAttributedStringForStateText(inputState.inputText, fontSize: component.fontSize, textColor: component.textColor, accentTextColor: component.textColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(component.context.animatedEmojiStickers.keys), emojiViewProvider: self.emojiViewProvider)
|
||||
if currentAttributedText != updatedAttributedText {
|
||||
self.textView.attributedText = updatedAttributedText
|
||||
}
|
||||
self.textView.selectedRange = NSMakeRange(inputState.selectionRange.lowerBound, inputState.selectionRange.count)
|
||||
|
||||
refreshChatTextInputAttributes(textView: self.textView, primaryTextColor: component.textColor, accentTextColor: component.textColor, baseFontSize: component.fontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(component.context.animatedEmojiStickers.keys), emojiViewProvider: self.emojiViewProvider)
|
||||
|
||||
self.updateEntities()
|
||||
|
||||
if currentAttributedText != updatedAttributedText && !self.isUpdating {
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(AnimationHint(kind: .textChanged)))
|
||||
}
|
||||
}
|
||||
|
||||
public func hasFirstResponder() -> Bool {
|
||||
@@ -290,7 +265,9 @@ public final class TextFieldComponent: Component {
|
||||
self.updateInputState { state in
|
||||
return state.insertText(text)
|
||||
}
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(AnimationHint(kind: .textChanged)))
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(AnimationHint(kind: .textChanged)))
|
||||
}
|
||||
}
|
||||
|
||||
public func deleteBackward() {
|
||||
@@ -301,7 +278,9 @@ public final class TextFieldComponent: Component {
|
||||
self.updateInputState { _ in
|
||||
return TextFieldComponent.InputState(inputText: text, selectionRange: selectionRange)
|
||||
}
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(AnimationHint(kind: .textChanged)))
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(AnimationHint(kind: .textChanged)))
|
||||
}
|
||||
}
|
||||
|
||||
private func onPaste() -> Bool {
|
||||
@@ -327,7 +306,9 @@ public final class TextFieldComponent: Component {
|
||||
return InputState(inputText: attributedString)
|
||||
}
|
||||
}
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(AnimationHint(kind: .textChanged)))
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(AnimationHint(kind: .textChanged)))
|
||||
}
|
||||
component.paste(.text)
|
||||
return false
|
||||
}
|
||||
@@ -382,12 +363,13 @@ public final class TextFieldComponent: Component {
|
||||
return true
|
||||
}
|
||||
|
||||
public func textViewDidChange(_ textView: UITextView) {
|
||||
public func chatInputTextNodeDidUpdateText() {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
refreshChatTextInputAttributes(textView: self.textView, primaryTextColor: component.textColor, accentTextColor: component.textColor, baseFontSize: component.fontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(component.context.animatedEmojiStickers.keys), emojiViewProvider: self.emojiViewProvider)
|
||||
refreshChatTextInputTypingAttributes(self.textView, textColor: component.textColor, baseFontSize: component.fontSize)
|
||||
self.textView.updateTextContainerInset()
|
||||
|
||||
if self.spoilerIsDisappearing {
|
||||
self.spoilerIsDisappearing = false
|
||||
@@ -395,11 +377,16 @@ public final class TextFieldComponent: Component {
|
||||
}
|
||||
|
||||
self.updateEntities()
|
||||
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(AnimationHint(kind: .textChanged)))
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(AnimationHint(kind: .textChanged)))
|
||||
}
|
||||
}
|
||||
|
||||
public func textViewDidChangeSelection(_ textView: UITextView) {
|
||||
public func chatInputTextNodeShouldReturn() -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
public func chatInputTextNodeDidChangeSelection(dueToEditing: Bool) {
|
||||
guard let _ = self.component else {
|
||||
return
|
||||
}
|
||||
@@ -408,11 +395,13 @@ public final class TextFieldComponent: Component {
|
||||
self.updateEmojiSuggestion(transition: .immediate)
|
||||
}
|
||||
|
||||
public func textViewDidBeginEditing(_ textView: UITextView) {
|
||||
public func chatInputTextNodeDidBeginEditing() {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.5, curve: .spring)).withUserData(AnimationHint(kind: .textFocusChanged)))
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.5, curve: .spring)).withUserData(AnimationHint(kind: .textFocusChanged)))
|
||||
}
|
||||
if component.isOneLineWhenUnfocused {
|
||||
Queue.mainQueue().justDispatch {
|
||||
self.textView.selectedTextRange = self.textView.textRange(from: self.textView.endOfDocument, to: self.textView.endOfDocument)
|
||||
@@ -420,12 +409,17 @@ public final class TextFieldComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
public func textViewDidEndEditing(_ textView: UITextView) {
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.5, curve: .spring)).withUserData(AnimationHint(kind: .textFocusChanged)))
|
||||
public func chatInputTextNodeDidFinishEditing() {
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.5, curve: .spring)).withUserData(AnimationHint(kind: .textFocusChanged)))
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 16.0, *)
|
||||
public func textView(_ textView: UITextView, editMenuForTextIn range: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu? {
|
||||
|
||||
public func chatInputTextNodeBackspaceWhileEmpty() {
|
||||
}
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
public func chatInputTextNodeMenu(forTextRange textRange: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu {
|
||||
let filteredActions: Set<String> = Set([
|
||||
"com.apple.menu.format",
|
||||
"com.apple.menu.replace"
|
||||
@@ -437,7 +431,7 @@ public final class TextFieldComponent: Component {
|
||||
return true
|
||||
}
|
||||
}
|
||||
guard let component = self.component, !textView.attributedText.string.isEmpty && textView.selectedRange.length > 0 else {
|
||||
guard let component = self.component, let attributedText = self.textView.attributedText, !attributedText.string.isEmpty, self.textView.selectedRange.length > 0 else {
|
||||
return UIMenu(children: suggestedActions)
|
||||
}
|
||||
let strings = component.strings
|
||||
@@ -504,6 +498,36 @@ public final class TextFieldComponent: Component {
|
||||
self.updateSpoilersRevealed(animated: animated)
|
||||
}
|
||||
})
|
||||
actions.insert(UIAction(title: strings.TextFormat_Quote, image: nil) { [weak self] action in
|
||||
if let self {
|
||||
var animated = false
|
||||
let attributedText = self.inputState.inputText
|
||||
attributedText.enumerateAttributes(in: NSMakeRange(0, attributedText.length), options: [], using: { attributes, _, _ in
|
||||
if let _ = attributes[ChatTextInputAttributes.block] {
|
||||
animated = true
|
||||
}
|
||||
})
|
||||
|
||||
self.toggleAttribute(key: ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: .quote))
|
||||
|
||||
self.updateSpoilersRevealed(animated: animated)
|
||||
}
|
||||
}, at: 0)
|
||||
actions.append(UIAction(title: strings.TextFormat_Code, image: nil) { [weak self] action in
|
||||
if let self {
|
||||
var animated = false
|
||||
let attributedText = self.inputState.inputText
|
||||
attributedText.enumerateAttributes(in: NSMakeRange(0, attributedText.length), options: [], using: { attributes, _, _ in
|
||||
if let _ = attributes[ChatTextInputAttributes.block] {
|
||||
animated = true
|
||||
}
|
||||
})
|
||||
|
||||
self.toggleAttribute(key: ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: .code(language: nil)))
|
||||
|
||||
self.updateSpoilersRevealed(animated: animated)
|
||||
}
|
||||
})
|
||||
|
||||
var updatedActions = suggestedActions
|
||||
let formatMenu = UIMenu(title: strings.TextFormat_Format, image: nil, children: actions)
|
||||
@@ -512,9 +536,148 @@ public final class TextFieldComponent: Component {
|
||||
return UIMenu(children: updatedActions)
|
||||
}
|
||||
|
||||
private func toggleAttribute(key: NSAttributedString.Key) {
|
||||
public func chatInputTextNode(shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
public func chatInputTextNodeShouldCopy() -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
public func chatInputTextNodeShouldPaste() -> Bool {
|
||||
return self.onPaste()
|
||||
}
|
||||
|
||||
public func chatInputTextNodeShouldRespondToAction(action: Selector) -> Bool {
|
||||
if action == #selector(self.paste(_:)) {
|
||||
return true
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public func chatInputTextNodeTargetForAction(action: Selector) -> ChatInputTextNode.TargetForAction? {
|
||||
if action == makeSelectorFromString("_accessibilitySpeak:") {
|
||||
if case .format = self.inputMenu.state {
|
||||
return ChatInputTextNode.TargetForAction(target: nil)
|
||||
} else if self.textView.selectedRange.length > 0 {
|
||||
return ChatInputTextNode.TargetForAction(target: self)
|
||||
} else {
|
||||
return ChatInputTextNode.TargetForAction(target: nil)
|
||||
}
|
||||
} else if action == makeSelectorFromString("_accessibilitySpeakSpellOut:") {
|
||||
if case .format = self.inputMenu.state {
|
||||
return ChatInputTextNode.TargetForAction(target: nil)
|
||||
} else if self.textView.selectedRange.length > 0 {
|
||||
return nil
|
||||
} else {
|
||||
return ChatInputTextNode.TargetForAction(target: nil)
|
||||
}
|
||||
}
|
||||
else if action == makeSelectorFromString("_accessibilitySpeakLanguageSelection:") || action == makeSelectorFromString("_accessibilityPauseSpeaking:") || action == makeSelectorFromString("_accessibilitySpeakSentence:") {
|
||||
return ChatInputTextNode.TargetForAction(target: nil)
|
||||
} else if action == makeSelectorFromString("_showTextStyleOptions:") {
|
||||
if #available(iOS 16.0, *) {
|
||||
return ChatInputTextNode.TargetForAction(target: nil)
|
||||
} else {
|
||||
if case .general = self.inputMenu.state {
|
||||
if self.textView.attributedText == nil || self.textView.attributedText!.length == 0 {
|
||||
return ChatInputTextNode.TargetForAction(target: nil)
|
||||
}
|
||||
return ChatInputTextNode.TargetForAction(target: self)
|
||||
} else {
|
||||
return ChatInputTextNode.TargetForAction(target: nil)
|
||||
}
|
||||
}
|
||||
} else if action == #selector(self.formatAttributesBold(_:)) || action == #selector(self.formatAttributesItalic(_:)) || action == #selector(self.formatAttributesMonospace(_:)) || action == #selector(self.formatAttributesStrikethrough(_:)) || action == #selector(self.formatAttributesUnderline(_:)) || action == #selector(self.formatAttributesSpoiler(_:)) || action == #selector(self.formatAttributesQuote(_:)) || action == #selector(self.formatAttributesCodeBlock(_:)) {
|
||||
if case .format = self.inputMenu.state {
|
||||
if action == #selector(self.formatAttributesSpoiler(_:)) {
|
||||
let selectedRange = self.textView.selectedRange
|
||||
var intersectsMonospace = false
|
||||
self.inputState.inputText.enumerateAttributes(in: selectedRange, options: [], using: { attributes, _, _ in
|
||||
if let _ = attributes[ChatTextInputAttributes.monospace] {
|
||||
intersectsMonospace = true
|
||||
}
|
||||
})
|
||||
if !intersectsMonospace {
|
||||
return ChatInputTextNode.TargetForAction(target: self)
|
||||
} else {
|
||||
return ChatInputTextNode.TargetForAction(target: nil)
|
||||
}
|
||||
} else if action == #selector(self.formatAttributesQuote(_:)) {
|
||||
return ChatInputTextNode.TargetForAction(target: self)
|
||||
} else if action == #selector(self.formatAttributesCodeBlock(_:)) {
|
||||
return ChatInputTextNode.TargetForAction(target: self)
|
||||
} else if action == #selector(self.formatAttributesMonospace(_:)) {
|
||||
var intersectsSpoiler = false
|
||||
self.inputState.inputText.enumerateAttributes(in: self.textView.selectedRange, options: [], using: { attributes, _, _ in
|
||||
if let _ = attributes[ChatTextInputAttributes.spoiler] {
|
||||
intersectsSpoiler = true
|
||||
}
|
||||
})
|
||||
if !intersectsSpoiler {
|
||||
return ChatInputTextNode.TargetForAction(target: self)
|
||||
} else {
|
||||
return ChatInputTextNode.TargetForAction(target: nil)
|
||||
}
|
||||
} else {
|
||||
return ChatInputTextNode.TargetForAction(target: self)
|
||||
}
|
||||
} else {
|
||||
return ChatInputTextNode.TargetForAction(target: nil)
|
||||
}
|
||||
}
|
||||
if case .format = self.inputMenu.state {
|
||||
return ChatInputTextNode.TargetForAction(target: nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@objc func _showTextStyleOptions(_ sender: Any) {
|
||||
let selectionRect: CGRect
|
||||
if let selectedTextRange = self.textView.selectedTextRange {
|
||||
selectionRect = self.textView.firstRect(for: selectedTextRange)
|
||||
} else {
|
||||
selectionRect = self.textView.bounds
|
||||
}
|
||||
|
||||
self.inputMenu.format(view: self.textView, rect: selectionRect.offsetBy(dx: 0.0, dy: -self.textView.contentOffset.y).insetBy(dx: 0.0, dy: -1.0))
|
||||
}
|
||||
|
||||
@objc func formatAttributesBold(_ sender: Any) {
|
||||
self.inputMenu.back()
|
||||
}
|
||||
|
||||
@objc func formatAttributesItalic(_ sender: Any) {
|
||||
self.inputMenu.back()
|
||||
}
|
||||
|
||||
@objc func formatAttributesMonospace(_ sender: Any) {
|
||||
self.inputMenu.back()
|
||||
}
|
||||
|
||||
@objc func formatAttributesStrikethrough(_ sender: Any) {
|
||||
self.inputMenu.back()
|
||||
}
|
||||
|
||||
@objc func formatAttributesUnderline(_ sender: Any) {
|
||||
self.inputMenu.back()
|
||||
}
|
||||
|
||||
@objc func formatAttributesQuote(_ sender: Any) {
|
||||
self.inputMenu.back()
|
||||
}
|
||||
|
||||
@objc func formatAttributesCodeBlock(_ sender: Any) {
|
||||
self.inputMenu.back()
|
||||
}
|
||||
|
||||
@objc func formatAttributesSpoiler(_ sender: Any) {
|
||||
self.inputMenu.back()
|
||||
}
|
||||
|
||||
private func toggleAttribute(key: NSAttributedString.Key, value: Any? = nil) {
|
||||
self.updateInputState { state in
|
||||
return state.addFormattingAttribute(attribute: key)
|
||||
return state.addFormattingAttribute(attribute: key, value: value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -563,7 +726,7 @@ public final class TextFieldComponent: Component {
|
||||
self.updateInputState { _ in
|
||||
return TextFieldComponent.InputState(inputText: string, selectionRange: string.length ..< string.length)
|
||||
}
|
||||
if updateState {
|
||||
if updateState && !self.isUpdating {
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(AnimationHint(kind: .textChanged)))
|
||||
}
|
||||
}
|
||||
@@ -769,8 +932,8 @@ public final class TextFieldComponent: Component {
|
||||
|
||||
var hasTracking = false
|
||||
var hasTrackingView = false
|
||||
if self.textView.selectedRange.length == 0 && self.textView.selectedRange.location > 0 {
|
||||
let selectedSubstring = self.textView.attributedText.attributedSubstring(from: NSRange(location: 0, length: self.textView.selectedRange.location))
|
||||
if let attributedText = self.textView.attributedText, self.textView.selectedRange.length == 0, self.textView.selectedRange.location > 0 {
|
||||
let selectedSubstring = attributedText.attributedSubstring(from: NSRange(location: 0, length: self.textView.selectedRange.location))
|
||||
if let lastCharacter = selectedSubstring.string.last, String(lastCharacter).isSingleEmoji {
|
||||
let queryLength = (String(lastCharacter) as NSString).length
|
||||
if selectedSubstring.attribute(ChatTextInputAttributes.customEmoji, at: selectedSubstring.length - queryLength, effectiveRange: nil) == nil {
|
||||
@@ -817,7 +980,7 @@ public final class TextFieldComponent: Component {
|
||||
}
|
||||
|
||||
func rightmostPositionOfFirstLine() -> CGPoint? {
|
||||
let glyphRange = self.layoutManager.glyphRange(for: self.textContainer)
|
||||
let glyphRange = self.textView.layoutManager.glyphRange(for: self.textView.textContainer)
|
||||
|
||||
if glyphRange.length == 0 { return nil }
|
||||
|
||||
@@ -826,16 +989,16 @@ public final class TextFieldComponent: Component {
|
||||
var lineRange: NSRange = NSRange()
|
||||
|
||||
repeat {
|
||||
lineRect = self.layoutManager.lineFragmentUsedRect(forGlyphAt: glyphIndexForStringStart, effectiveRange: &lineRange)
|
||||
lineRect = self.textView.layoutManager.lineFragmentUsedRect(forGlyphAt: glyphIndexForStringStart, effectiveRange: &lineRange)
|
||||
if NSMaxRange(lineRange) > glyphRange.length {
|
||||
lineRange.length = glyphRange.length - lineRange.location
|
||||
}
|
||||
glyphIndexForStringStart = NSMaxRange(lineRange)
|
||||
} while glyphIndexForStringStart < NSMaxRange(glyphRange) && !NSLocationInRange(glyphRange.location, lineRange)
|
||||
|
||||
let padding = self.textView.textContainerInset.left
|
||||
let padding = self.textView.defaultTextContainerInset.left
|
||||
var rightmostX = lineRect.maxX + padding
|
||||
let rightmostY = lineRect.minY + self.textView.textContainerInset.top
|
||||
let rightmostY = lineRect.minY + self.textView.defaultTextContainerInset.top
|
||||
|
||||
let nsString = (self.textView.text as NSString)
|
||||
let firstLineEndRange = NSMakeRange(lineRange.location + lineRange.length - 1, 1)
|
||||
@@ -850,6 +1013,11 @@ public final class TextFieldComponent: Component {
|
||||
}
|
||||
|
||||
func update(component: TextFieldComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
self.isUpdating = true
|
||||
defer {
|
||||
self.isUpdating = false
|
||||
}
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
@@ -877,8 +1045,13 @@ public final class TextFieldComponent: Component {
|
||||
let wasEditing = component.externalState.isEditing
|
||||
let isEditing = self.textView.isFirstResponder
|
||||
|
||||
if self.textView.textContainerInset != component.insets {
|
||||
self.textView.textContainerInset = component.insets
|
||||
var innerTextInsets = component.insets
|
||||
innerTextInsets.left = 0.0
|
||||
|
||||
let textLeftInset = component.insets.left + 8.0
|
||||
|
||||
if self.textView.defaultTextContainerInset != innerTextInsets {
|
||||
self.textView.defaultTextContainerInset = innerTextInsets
|
||||
}
|
||||
|
||||
var availableSize = availableSize
|
||||
@@ -886,17 +1059,28 @@ public final class TextFieldComponent: Component {
|
||||
availableSize.width += 32.0
|
||||
}
|
||||
|
||||
self.textContainer.size = CGSize(width: availableSize.width - self.textView.textContainerInset.left - self.textView.textContainerInset.right, height: 10000000.0)
|
||||
self.layoutManager.ensureLayout(for: self.textContainer)
|
||||
let textHeight = self.textView.textHeightForWidth(availableSize.width - textLeftInset, rightInset: innerTextInsets.right)
|
||||
let size = CGSize(width: availableSize.width, height: min(textHeight, availableSize.height))
|
||||
|
||||
let boundingRect = self.layoutManager.boundingRect(forGlyphRange: NSRange(location: 0, length: self.textStorage.length), in: self.textContainer)
|
||||
let size = CGSize(width: availableSize.width, height: min(availableSize.height, ceil(boundingRect.height) + self.textView.textContainerInset.top + self.textView.textContainerInset.bottom))
|
||||
let textFrame = CGRect(origin: CGPoint(x: textLeftInset, y: 0.0), size: CGSize(width: size.width - component.insets.left, height: size.height))
|
||||
|
||||
var refreshScrolling = self.textView.bounds.size != size
|
||||
var refreshScrolling = self.textView.bounds.size != textFrame.size
|
||||
if component.isOneLineWhenUnfocused && !isEditing && isEditing != wasEditing {
|
||||
refreshScrolling = true
|
||||
}
|
||||
self.textView.frame = CGRect(origin: CGPoint(), size: size)
|
||||
|
||||
self.textView.theme = ChatInputTextView.Theme(
|
||||
quote: ChatInputTextView.Theme.Quote(
|
||||
background: component.textColor.withMultipliedAlpha(0.1),
|
||||
foreground: component.textColor,
|
||||
lineStyle: .solid(color: component.textColor),
|
||||
codeBackground: component.textColor.withMultipliedAlpha(0.1),
|
||||
codeForeground: component.textColor
|
||||
)
|
||||
)
|
||||
|
||||
self.textView.frame = textFrame
|
||||
self.textView.updateLayout(size: textFrame.size)
|
||||
self.textView.panGestureRecognizer.isEnabled = isEditing
|
||||
|
||||
self.updateEmojiSuggestion(transition: .immediate)
|
||||
@@ -911,9 +1095,9 @@ public final class TextFieldComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
component.externalState.hasText = self.textStorage.length != 0
|
||||
component.externalState.hasText = self.textView.textStorage.length != 0
|
||||
component.externalState.isEditing = isEditing
|
||||
component.externalState.textLength = self.textStorage.string.count
|
||||
component.externalState.textLength = self.textView.textStorage.string.count
|
||||
|
||||
if let inputView = component.customInputView {
|
||||
if self.textView.inputView == nil {
|
||||
@@ -1011,28 +1195,95 @@ extension TextFieldComponent.InputState {
|
||||
return TextFieldComponent.InputState(inputText: inputText, selectionRange: selectionPosition ..< selectionPosition)
|
||||
}
|
||||
|
||||
public func addFormattingAttribute(attribute: NSAttributedString.Key) -> TextFieldComponent.InputState {
|
||||
public func addFormattingAttribute(attribute: NSAttributedString.Key, value: Any? = nil) -> TextFieldComponent.InputState {
|
||||
if !self.selectionRange.isEmpty {
|
||||
let nsRange = NSRange(location: self.selectionRange.lowerBound, length: self.selectionRange.count)
|
||||
var addAttribute = true
|
||||
var attributesToRemove: [NSAttributedString.Key] = []
|
||||
self.inputText.enumerateAttributes(in: nsRange, options: .longestEffectiveRangeNotRequired) { attributes, range, stop in
|
||||
self.inputText.enumerateAttributes(in: nsRange, options: .longestEffectiveRangeNotRequired) { attributes, range, _ in
|
||||
for (key, _) in attributes {
|
||||
if key == attribute && range == nsRange {
|
||||
if key == attribute {
|
||||
addAttribute = false
|
||||
attributesToRemove.append(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var selectionRange = self.selectionRange
|
||||
|
||||
let result = NSMutableAttributedString(attributedString: self.inputText)
|
||||
for attribute in attributesToRemove {
|
||||
result.removeAttribute(attribute, range: nsRange)
|
||||
if attribute == ChatTextInputAttributes.block {
|
||||
var removeRange = nsRange
|
||||
|
||||
var selectionIndex = nsRange.upperBound
|
||||
if nsRange.upperBound != result.length && (result.string as NSString).character(at: nsRange.upperBound) != 0x0a {
|
||||
result.insert(NSAttributedString(string: "\n"), at: nsRange.upperBound)
|
||||
selectionIndex += 1
|
||||
removeRange.length += 1
|
||||
}
|
||||
if nsRange.lowerBound != 0 && (result.string as NSString).character(at: nsRange.lowerBound - 1) != 0x0a {
|
||||
result.insert(NSAttributedString(string: "\n"), at: nsRange.lowerBound)
|
||||
selectionIndex += 1
|
||||
removeRange.location += 1
|
||||
} else if nsRange.lowerBound != 0 {
|
||||
removeRange.location -= 1
|
||||
removeRange.length += 1
|
||||
}
|
||||
|
||||
if removeRange.lowerBound > result.length {
|
||||
removeRange = NSRange(location: result.length, length: 0)
|
||||
} else if removeRange.upperBound > result.length {
|
||||
removeRange = NSRange(location: removeRange.lowerBound, length: result.length - removeRange.lowerBound)
|
||||
}
|
||||
result.removeAttribute(attribute, range: removeRange)
|
||||
|
||||
if selectionRange.lowerBound > result.length {
|
||||
selectionRange = result.length ..< result.length
|
||||
} else if selectionRange.upperBound > result.length {
|
||||
selectionRange = selectionRange.lowerBound ..< result.length
|
||||
}
|
||||
|
||||
// Prevent merge back
|
||||
result.enumerateAttributes(in: NSRange(location: selectionIndex, length: result.length - selectionIndex), options: .longestEffectiveRangeNotRequired) { attributes, range, _ in
|
||||
for (key, value) in attributes {
|
||||
if let value = value as? ChatTextInputTextQuoteAttribute {
|
||||
result.removeAttribute(key, range: range)
|
||||
result.addAttribute(key, value: ChatTextInputTextQuoteAttribute(kind: value.kind), range: range)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
selectionRange = selectionIndex ..< selectionIndex
|
||||
} else {
|
||||
result.removeAttribute(attribute, range: nsRange)
|
||||
}
|
||||
}
|
||||
|
||||
if addAttribute {
|
||||
result.addAttribute(attribute, value: true as Bool, range: nsRange)
|
||||
if attribute == ChatTextInputAttributes.block {
|
||||
result.addAttribute(attribute, value: value ?? ChatTextInputTextQuoteAttribute(kind: .quote), range: nsRange)
|
||||
var selectionIndex = nsRange.upperBound
|
||||
if nsRange.upperBound != result.length && (result.string as NSString).character(at: nsRange.upperBound) != 0x0a {
|
||||
result.insert(NSAttributedString(string: "\n"), at: nsRange.upperBound)
|
||||
selectionIndex += 1
|
||||
}
|
||||
if nsRange.lowerBound != 0 && (result.string as NSString).character(at: nsRange.lowerBound - 1) != 0x0a {
|
||||
result.insert(NSAttributedString(string: "\n"), at: nsRange.lowerBound)
|
||||
selectionIndex += 1
|
||||
}
|
||||
selectionRange = selectionIndex ..< selectionIndex
|
||||
} else {
|
||||
result.addAttribute(attribute, value: true as Bool, range: nsRange)
|
||||
}
|
||||
}
|
||||
return TextFieldComponent.InputState(inputText: result, selectionRange: self.selectionRange)
|
||||
if selectionRange.lowerBound > result.length {
|
||||
selectionRange = result.length ..< result.length
|
||||
} else if selectionRange.upperBound > result.length {
|
||||
selectionRange = selectionRange.lowerBound ..< result.length
|
||||
}
|
||||
|
||||
return TextFieldComponent.InputState(inputText: result, selectionRange: selectionRange)
|
||||
} else {
|
||||
return self
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user