diff --git a/Telegram/BUILD b/Telegram/BUILD
index bdff4b5f74..22655f3a9b 100644
--- a/Telegram/BUILD
+++ b/Telegram/BUILD
@@ -1635,7 +1635,6 @@ plist_fragment(
UIAppFonts
- SFCompactRounded-Semibold.otf
AremacFS-Regular.otf
AremacFS-Semibold.otf
diff --git a/Telegram/Telegram-iOS/Resources/IntroCall.tgs b/Telegram/Telegram-iOS/Resources/IntroCall.tgs
deleted file mode 100644
index b819c8302f..0000000000
Binary files a/Telegram/Telegram-iOS/Resources/IntroCall.tgs and /dev/null differ
diff --git a/Telegram/Telegram-iOS/Resources/intro/start_arrow@2x.png b/Telegram/Telegram-iOS/Resources/intro/start_arrow@2x.png
deleted file mode 100644
index fe59b3e2b5..0000000000
Binary files a/Telegram/Telegram-iOS/Resources/intro/start_arrow@2x.png and /dev/null differ
diff --git a/Telegram/Telegram-iOS/Resources/intro/start_arrow_ipad.png b/Telegram/Telegram-iOS/Resources/intro/start_arrow_ipad.png
deleted file mode 100644
index eaa2cd6dd1..0000000000
Binary files a/Telegram/Telegram-iOS/Resources/intro/start_arrow_ipad.png and /dev/null differ
diff --git a/Telegram/Telegram-iOS/Resources/intro/start_arrow_ipad@2x.png b/Telegram/Telegram-iOS/Resources/intro/start_arrow_ipad@2x.png
deleted file mode 100644
index 08a47e44ac..0000000000
Binary files a/Telegram/Telegram-iOS/Resources/intro/start_arrow_ipad@2x.png and /dev/null differ
diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift
index 7e723704f1..7b1781e32a 100644
--- a/submodules/AccountContext/Sources/ChatController.swift
+++ b/submodules/AccountContext/Sources/ChatController.swift
@@ -67,6 +67,8 @@ public final class ChatMessageItemAssociatedData: Equatable {
public let isSuspiciousPeer: Bool
public let showTextAsPlaceholder: Bool
public let accountCountry: String?
+ public let isParticipant: Bool
+ public let invitedOn: Int32?
public init(
automaticDownloadPeerType: MediaAutoDownloadPeerType,
@@ -104,7 +106,9 @@ public final class ChatMessageItemAssociatedData: Equatable {
showSensitiveContent: Bool = false,
isSuspiciousPeer: Bool = false,
showTextAsPlaceholder: Bool = false,
- accountCountry: String? = nil
+ accountCountry: String? = nil,
+ isParticipant: Bool = false,
+ invitedOn: Int32? = nil
) {
self.automaticDownloadPeerType = automaticDownloadPeerType
self.automaticDownloadPeerId = automaticDownloadPeerId
@@ -142,6 +146,8 @@ public final class ChatMessageItemAssociatedData: Equatable {
self.isSuspiciousPeer = isSuspiciousPeer
self.showTextAsPlaceholder = showTextAsPlaceholder
self.accountCountry = accountCountry
+ self.isParticipant = isParticipant
+ self.invitedOn = invitedOn
}
public static func == (lhs: ChatMessageItemAssociatedData, rhs: ChatMessageItemAssociatedData) -> Bool {
@@ -241,6 +247,12 @@ public final class ChatMessageItemAssociatedData: Equatable {
if lhs.accountCountry != rhs.accountCountry {
return false
}
+ if lhs.isParticipant != rhs.isParticipant {
+ return false
+ }
+ if lhs.invitedOn != rhs.invitedOn {
+ return false
+ }
return true
}
}
diff --git a/submodules/BrowserUI/Sources/BrowserMarkdown.swift b/submodules/BrowserUI/Sources/BrowserMarkdown.swift
index c6f23b1907..17dbb5dc35 100644
--- a/submodules/BrowserUI/Sources/BrowserMarkdown.swift
+++ b/submodules/BrowserUI/Sources/BrowserMarkdown.swift
@@ -22,14 +22,221 @@ private let markdownDefaultBlockImageDimensions = PixelDimensions(width: 1200, h
private let markdownDefaultInlineImageDimensions = PixelDimensions(width: 18, height: 18)
private let markdownTaskListUncheckedNumber = "\u{001f}tg-md-task:unchecked"
private let markdownTaskListCheckedNumber = "\u{001f}tg-md-task:checked"
+private let markdownRawHTMLTagRegex = try! NSRegularExpression(pattern: #"?([A-Za-z][A-Za-z0-9:-]*)\b[^>]*?>"#)
+private let markdownFormulaPlaceholderRegex = try! NSRegularExpression(pattern: #"TGMDMATH\d+TGMD"#)
+private let markdownVoidHTMLTags: Set = [
+ "area",
+ "base",
+ "br",
+ "col",
+ "embed",
+ "hr",
+ "img",
+ "input",
+ "link",
+ "meta",
+ "param",
+ "source",
+ "track",
+ "wbr"
+]
+
+private struct MarkdownSafetyLimits {
+ let maxFileSize = 2_097_152
+ let maxLineLength = 32_768
+ let maxBlockquoteDepth = 64
+ let maxListIndent = 96
+ let maxRawHTMLTagCount = 8_000
+ let maxRawHTMLNestingDepth = 64
+ let maxAttributedStringLength = 400_000
+ let maxAttributeRuns = 20_000
+ let maxPresentationIntentDepth = 128
+ let maxIntentNodes = 10_000
+ let maxEmittedBlocks = 5_000
+ let maxTableColumns = 32
+ let maxTableCells = 2_000
+ let maxMediaItems = 100
+ let maxInlineHTMLStyleDepth = 32
+ let maxDataImageBytes = 2_097_152
+ let maxDataImagePixelCount = 12_000_000
+ let maxFormulas = 200
+ let maxFormulaSourceCharacters = 20_000
+ let maxInlineFormulaLength = 256
+ let maxBlockFormulaLength = 4_096
+}
+
+private let markdownSafetyLimits = MarkdownSafetyLimits()
+
+private final class MarkdownConversionBudget {
+ let limits: MarkdownSafetyLimits
+
+ private(set) var isExceeded = false
+
+ private var attributeRunCount = 0
+ private var intentNodeCount = 0
+ private var tableCellCount = 0
+ private var mediaItemCount = 0
+
+ init(limits: MarkdownSafetyLimits) {
+ self.limits = limits
+ }
+
+ @discardableResult
+ func fail() -> Bool {
+ self.isExceeded = true
+ return false
+ }
+
+ @discardableResult
+ func registerAttributedStringLength(_ length: Int) -> Bool {
+ guard length <= self.limits.maxAttributedStringLength else {
+ return self.fail()
+ }
+ return true
+ }
+
+ @discardableResult
+ func registerAttributeRun() -> Bool {
+ self.attributeRunCount += 1
+ guard self.attributeRunCount <= self.limits.maxAttributeRuns else {
+ return self.fail()
+ }
+ return true
+ }
+
+ @discardableResult
+ func registerPresentationIntentDepth(_ depth: Int) -> Bool {
+ guard depth <= self.limits.maxPresentationIntentDepth else {
+ return self.fail()
+ }
+ return true
+ }
+
+ @discardableResult
+ func registerIntentNode() -> Bool {
+ self.intentNodeCount += 1
+ guard self.intentNodeCount <= self.limits.maxIntentNodes else {
+ return self.fail()
+ }
+ return true
+ }
+
+ @discardableResult
+ func registerBlockDepth(_ depth: Int) -> Bool {
+ guard depth <= self.limits.maxPresentationIntentDepth else {
+ return self.fail()
+ }
+ return true
+ }
+
+ @discardableResult
+ func registerTableColumns(_ count: Int) -> Bool {
+ guard count <= self.limits.maxTableColumns else {
+ return self.fail()
+ }
+ return true
+ }
+
+ @discardableResult
+ func registerTableCells(_ count: Int) -> Bool {
+ self.tableCellCount += count
+ guard self.tableCellCount <= self.limits.maxTableCells else {
+ return self.fail()
+ }
+ return true
+ }
+
+ @discardableResult
+ func registerMediaItem() -> Bool {
+ self.mediaItemCount += 1
+ guard self.mediaItemCount <= self.limits.maxMediaItems else {
+ return self.fail()
+ }
+ return true
+ }
+
+ @discardableResult
+ func registerInlineHTMLStyleDepth(_ depth: Int) -> Bool {
+ guard depth <= self.limits.maxInlineHTMLStyleDepth else {
+ return self.fail()
+ }
+ return true
+ }
+
+ @discardableResult
+ func validateFinalBlocks(_ blocks: [InstantPageBlock]) -> Bool {
+ var count = 0
+ return self.validate(blocks: blocks, depth: 0, count: &count)
+ }
+
+ private func validate(blocks: [InstantPageBlock], depth: Int, count: inout Int) -> Bool {
+ guard self.registerBlockDepth(depth) else {
+ return false
+ }
+ for block in blocks {
+ count += 1
+ guard count <= self.limits.maxEmittedBlocks else {
+ return self.fail()
+ }
+ switch block {
+ case let .list(items, _):
+ for item in items {
+ if !self.validate(listItem: item, depth: depth + 1, count: &count) {
+ return false
+ }
+ }
+ case let .details(_, nestedBlocks, _):
+ if !self.validate(blocks: nestedBlocks, depth: depth + 1, count: &count) {
+ return false
+ }
+ default:
+ break
+ }
+ }
+ return true
+ }
+
+ private func validate(listItem: InstantPageListItem, depth: Int, count: inout Int) -> Bool {
+ guard self.registerBlockDepth(depth) else {
+ return false
+ }
+ if case let .blocks(blocks, _) = listItem {
+ return self.validate(blocks: blocks, depth: depth + 1, count: &count)
+ } else {
+ return true
+ }
+ }
+}
private struct MarkdownPageResult {
let blocks: [InstantPageBlock]
let media: [MediaId: Media]
}
+private enum MarkdownFormulaMode {
+ case inline
+ case block
+}
+
+private struct MarkdownFormulaDescriptor {
+ let placeholder: String
+ let latex: String
+ let mode: MarkdownFormulaMode
+}
+
+private struct MarkdownPreparedSource {
+ let text: String
+ let formulasByPlaceholder: [String: MarkdownFormulaDescriptor]
+}
+
+private enum MarkdownInlineTextSegment {
+ case plain(String)
+ case formula(MarkdownFormulaDescriptor)
+}
+
private enum MarkdownInlineFragment {
case richText(RichText)
+ case formula(MarkdownFormulaDescriptor, RichText)
case image(MarkdownResolvedImage)
}
@@ -44,6 +251,8 @@ private struct MarkdownInlineContent {
switch fragment {
case let .richText(text):
result.append(text)
+ case let .formula(_, text):
+ result.append(text)
case let .image(image):
var text: RichText = .image(id: image.mediaId, dimensions: image.inlineDimensions)
if let linkUrl = image.linkUrl {
@@ -55,6 +264,31 @@ private struct MarkdownInlineContent {
return markdownCompact(result)
}
+
+ var standaloneBlockFormula: MarkdownFormulaDescriptor? {
+ var result: MarkdownFormulaDescriptor?
+
+ for fragment in self.fragments {
+ switch fragment {
+ case let .richText(text):
+ if !markdownIsWhitespaceOnly(text) {
+ return nil
+ }
+ case let .formula(descriptor, _):
+ guard descriptor.mode == .block else {
+ return nil
+ }
+ if result != nil {
+ return nil
+ }
+ result = descriptor
+ case .image:
+ return nil
+ }
+ }
+
+ return result
+ }
var standaloneImage: MarkdownResolvedImage? {
var result: MarkdownResolvedImage?
@@ -65,6 +299,8 @@ private struct MarkdownInlineContent {
if !markdownIsWhitespaceOnly(text) {
return nil
}
+ case .formula:
+ return nil
case let .image(image):
if result != nil {
return nil
@@ -98,20 +334,24 @@ private enum MarkdownTaskListState {
private final class MarkdownConversionContext {
private let context: AccountContext
fileprivate let documentURL: URL
+ fileprivate let formulasByPlaceholder: [String: MarkdownFormulaDescriptor]
+ fileprivate let budget: MarkdownConversionBudget
private var nextRemoteMediaId: Int64 = 0
private var nextLocalMediaId: Int64 = 0
-
+
private(set) var media: [MediaId: Media] = [:]
-
- init(context: AccountContext, documentURL: URL) {
+
+ init(context: AccountContext, documentURL: URL, formulasByPlaceholder: [String: MarkdownFormulaDescriptor], budget: MarkdownConversionBudget) {
self.context = context
self.documentURL = documentURL
+ self.formulasByPlaceholder = formulasByPlaceholder
+ self.budget = budget
}
-
+
func makePageResult(blocks: [InstantPageBlock]) -> MarkdownPageResult {
return MarkdownPageResult(blocks: blocks, media: self.media)
}
-
+
func resolveImage(attributes: [NSAttributedString.Key: Any]) -> MarkdownResolvedImage? {
guard let imageUrl = markdownImageURL(attributes: attributes) else {
return nil
@@ -121,8 +361,11 @@ private final class MarkdownConversionContext {
let caption = markdownImageCaption(markdownAlternateDescription(attributes: attributes))
let linkUrl = markdownLink(attributes: attributes, documentURL: self.documentURL)
- switch markdownResolveImageSource(imageUrl) {
+ switch markdownResolveImageSource(imageUrl, limits: self.budget.limits) {
case let .remote(url):
+ guard self.budget.registerMediaItem() else {
+ return nil
+ }
let mediaId = self.nextMediaId(namespace: Namespaces.Media.CloudImage)
self.media[mediaId] = TelegramMediaImage(
imageId: mediaId,
@@ -146,6 +389,9 @@ private final class MarkdownConversionContext {
linkUrl: linkUrl
)
case let .data(data, dimensions):
+ guard self.budget.registerMediaItem() else {
+ return nil
+ }
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max), size: Int64(data.count), isSecretRelated: false)
self.context.engine.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: data)
@@ -188,6 +434,535 @@ private final class MarkdownConversionContext {
}
}
+private func markdownPassesPreflight(data: Data, limits: MarkdownSafetyLimits) -> Bool {
+ guard data.count <= limits.maxFileSize else {
+ return false
+ }
+ guard let text = markdownDecodedSourceText(data) else {
+ return false
+ }
+ return markdownPassesPreflight(text: text, limits: limits)
+}
+
+private func markdownDecodedSourceText(_ data: Data) -> String? {
+ if let text = String(data: data, encoding: .utf8) {
+ return text
+ }
+ if data.starts(with: [0xff, 0xfe]) {
+ return String(data: data, encoding: .utf16LittleEndian)
+ }
+ if data.starts(with: [0xfe, 0xff]) {
+ return String(data: data, encoding: .utf16BigEndian)
+ }
+ return nil
+}
+
+private func markdownPassesPreflight(text: String, limits: MarkdownSafetyLimits) -> Bool {
+ var activeFence: Character?
+ var htmlTagCount = 0
+ var htmlDepth = 0
+
+ for rawLine in text.split(separator: "\n", omittingEmptySubsequences: false) {
+ var line = String(rawLine)
+ if line.hasSuffix("\r") {
+ line.removeLast()
+ }
+
+ if (line as NSString).length > limits.maxLineLength {
+ return false
+ }
+
+ if let fenceMarker = markdownFenceMarker(in: line) {
+ if activeFence == fenceMarker {
+ activeFence = nil
+ } else {
+ activeFence = fenceMarker
+ }
+ continue
+ }
+
+ if activeFence != nil {
+ continue
+ }
+
+ if markdownBlockquoteDepth(in: line) > limits.maxBlockquoteDepth {
+ return false
+ }
+ if markdownIndentWidth(in: line) > limits.maxListIndent {
+ return false
+ }
+ if !markdownScanRawHTMLTags(in: line, limits: limits, tagCount: &htmlTagCount, depth: &htmlDepth) {
+ return false
+ }
+ }
+
+ return true
+}
+
+private func markdownFenceMarker(in line: String) -> Character? {
+ let trimmed = line.drop(while: { $0 == " " || $0 == "\t" })
+ guard let marker = trimmed.first, marker == "`" || marker == "~" else {
+ return nil
+ }
+ var count = 0
+ var index = trimmed.startIndex
+ while index < trimmed.endIndex, trimmed[index] == marker {
+ count += 1
+ index = trimmed.index(after: index)
+ }
+ return count >= 3 ? marker : nil
+}
+
+private func markdownBlockquoteDepth(in line: String) -> Int {
+ var depth = 0
+ var index = line.startIndex
+
+ while index < line.endIndex {
+ switch line[index] {
+ case " ", "\t":
+ index = line.index(after: index)
+ case ">":
+ depth += 1
+ index = line.index(after: index)
+ if index < line.endIndex, (line[index] == " " || line[index] == "\t") {
+ index = line.index(after: index)
+ }
+ default:
+ return depth
+ }
+ }
+
+ return depth
+}
+
+private func markdownIndentWidth(in line: String) -> Int {
+ var width = 0
+ for character in line {
+ switch character {
+ case " ":
+ width += 1
+ case "\t":
+ width += 4
+ default:
+ return width
+ }
+ }
+ return width
+}
+
+private func markdownScanRawHTMLTags(in line: String, limits: MarkdownSafetyLimits, tagCount: inout Int, depth: inout Int) -> Bool {
+ let nsLine = line as NSString
+ let matches = markdownRawHTMLTagRegex.matches(in: line, range: NSRange(location: 0, length: nsLine.length))
+
+ for match in matches {
+ tagCount += 1
+ guard tagCount <= limits.maxRawHTMLTagCount else {
+ return false
+ }
+
+ let tagText = nsLine.substring(with: match.range)
+ let tagName = nsLine.substring(with: match.range(at: 1)).lowercased()
+ let lowercasedTag = tagText.lowercased()
+
+ if lowercasedTag.hasPrefix("") {
+ depth = max(0, depth - 1)
+ } else if !lowercasedTag.hasSuffix("/>") && !markdownVoidHTMLTags.contains(tagName) {
+ depth += 1
+ guard depth <= limits.maxRawHTMLNestingDepth else {
+ return false
+ }
+ }
+ }
+
+ return true
+}
+
+private func markdownPreparedSource(text: String, limits: MarkdownSafetyLimits) -> MarkdownPreparedSource {
+ let lines = markdownLinesPreservingEndings(text)
+
+ var activeFence: Character?
+ var formulasByPlaceholder: [String: MarkdownFormulaDescriptor] = [:]
+ var acceptedFormulaCount = 0
+ var totalFormulaCharacters = 0
+ var nextPlaceholderIndex = 0
+ var result = ""
+
+ var lineIndex = 0
+ while lineIndex < lines.count {
+ let line = lines[lineIndex]
+ let (content, _) = markdownLineContentAndEnding(line)
+
+ if let fenceMarker = markdownFenceMarker(in: content) {
+ if activeFence == fenceMarker {
+ activeFence = nil
+ } else {
+ activeFence = fenceMarker
+ }
+ result.append(contentsOf: line)
+ lineIndex += 1
+ continue
+ }
+
+ if activeFence != nil {
+ result.append(contentsOf: line)
+ lineIndex += 1
+ continue
+ }
+
+ if let replacement = markdownBlockFormulaReplacement(
+ in: lines,
+ startLineIndex: lineIndex,
+ limits: limits,
+ acceptedFormulaCount: &acceptedFormulaCount,
+ totalFormulaCharacters: &totalFormulaCharacters,
+ nextPlaceholderIndex: &nextPlaceholderIndex
+ ) {
+ result.append(contentsOf: replacement.replacement)
+ formulasByPlaceholder[replacement.descriptor.placeholder] = replacement.descriptor
+ lineIndex = replacement.nextLineIndex
+ continue
+ }
+
+ let processedLine = markdownReplacingInlineFormulas(
+ in: line,
+ limits: limits,
+ acceptedFormulaCount: &acceptedFormulaCount,
+ totalFormulaCharacters: &totalFormulaCharacters,
+ nextPlaceholderIndex: &nextPlaceholderIndex,
+ formulasByPlaceholder: &formulasByPlaceholder
+ )
+ result.append(contentsOf: processedLine)
+ lineIndex += 1
+ }
+
+ return MarkdownPreparedSource(text: result, formulasByPlaceholder: formulasByPlaceholder)
+}
+
+private func markdownLinesPreservingEndings(_ text: String) -> [String] {
+ guard !text.isEmpty else {
+ return []
+ }
+
+ var result: [String] = []
+ var lineStart = text.startIndex
+ var index = text.startIndex
+
+ while index < text.endIndex {
+ if text[index] == "\n" {
+ let nextIndex = text.index(after: index)
+ result.append(String(text[lineStart ..< nextIndex]))
+ lineStart = nextIndex
+ }
+ index = text.index(after: index)
+ }
+
+ if lineStart < text.endIndex {
+ result.append(String(text[lineStart...]))
+ }
+
+ return result
+}
+
+private func markdownLineContentAndEnding(_ line: String) -> (content: String, ending: String) {
+ if line.hasSuffix("\r\n") {
+ return (String(line.dropLast(2)), "\r\n")
+ } else if line.hasSuffix("\n") {
+ return (String(line.dropLast()), "\n")
+ } else {
+ return (line, "")
+ }
+}
+
+private func markdownFormulaPlaceholder(_ index: Int) -> String {
+ return "TGMDMATH\(index)TGMD"
+}
+
+private func markdownAcceptedFormulaDescriptor(
+ latex: String,
+ mode: MarkdownFormulaMode,
+ limits: MarkdownSafetyLimits,
+ acceptedFormulaCount: inout Int,
+ totalFormulaCharacters: inout Int,
+ nextPlaceholderIndex: inout Int
+) -> MarkdownFormulaDescriptor? {
+ let normalizedLatex: String
+ switch mode {
+ case .inline:
+ normalizedLatex = latex
+ case .block:
+ normalizedLatex = latex.trimmingCharacters(in: .whitespacesAndNewlines)
+ }
+
+ guard !normalizedLatex.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
+ return nil
+ }
+
+ let latexLength = (normalizedLatex as NSString).length
+ let maxLength = mode == .inline ? limits.maxInlineFormulaLength : limits.maxBlockFormulaLength
+ guard latexLength <= maxLength else {
+ return nil
+ }
+ guard acceptedFormulaCount < limits.maxFormulas else {
+ return nil
+ }
+ guard totalFormulaCharacters + latexLength <= limits.maxFormulaSourceCharacters else {
+ return nil
+ }
+
+ let placeholder = markdownFormulaPlaceholder(nextPlaceholderIndex)
+ nextPlaceholderIndex += 1
+ acceptedFormulaCount += 1
+ totalFormulaCharacters += latexLength
+ return MarkdownFormulaDescriptor(placeholder: placeholder, latex: normalizedLatex, mode: mode)
+}
+
+private func markdownBlockFormulaReplacement(
+ in lines: [String],
+ startLineIndex: Int,
+ limits: MarkdownSafetyLimits,
+ acceptedFormulaCount: inout Int,
+ totalFormulaCharacters: inout Int,
+ nextPlaceholderIndex: inout Int
+) -> (replacement: String, nextLineIndex: Int, descriptor: MarkdownFormulaDescriptor)? {
+ guard startLineIndex >= 0, startLineIndex < lines.count else {
+ return nil
+ }
+
+ let (content, _) = markdownLineContentAndEnding(lines[startLineIndex])
+ let indentation = String(content.prefix { $0 == " " || $0 == "\t" })
+ let trimmedStart = content.drop(while: { $0 == " " || $0 == "\t" })
+
+ let opener: String
+ let closer: String
+ if trimmedStart.hasPrefix("$$") {
+ opener = "$$"
+ closer = "$$"
+ } else if trimmedStart.hasPrefix("\\[") {
+ opener = "\\["
+ closer = "\\]"
+ } else {
+ return nil
+ }
+
+ let openerContent = String(trimmedStart.dropFirst(opener.count))
+ var latex = ""
+
+ if let closeRange = markdownFirstUnescapedRange(of: closer, in: openerContent, from: openerContent.startIndex) {
+ let trailing = String(openerContent[closeRange.upperBound...])
+ guard trailing.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
+ return nil
+ }
+ latex = String(openerContent[.. String {
+ let (content, ending) = markdownLineContentAndEnding(line)
+ guard !content.isEmpty else {
+ return line
+ }
+
+ var result = ""
+ var index = content.startIndex
+ var activeCodeDelimiterLength: Int?
+
+ while index < content.endIndex {
+ if content[index] == "`" {
+ let delimiterEnd = markdownIndex(afterRepeating: "`", in: content, from: index)
+ let delimiterLength = content.distance(from: index, to: delimiterEnd)
+ result.append(contentsOf: content[index ..< delimiterEnd])
+ if activeCodeDelimiterLength == delimiterLength {
+ activeCodeDelimiterLength = nil
+ } else {
+ activeCodeDelimiterLength = delimiterLength
+ }
+ index = delimiterEnd
+ continue
+ }
+
+ if activeCodeDelimiterLength != nil {
+ result.append(content[index])
+ index = content.index(after: index)
+ continue
+ }
+
+ if content[index] == "\\", !markdownIsEscaped(content, at: index), let nextIndex = markdownIndex(content, offsetBy: 1, from: index), nextIndex < content.endIndex, content[nextIndex] == "(" {
+ let bodyStart = content.index(after: nextIndex)
+ if let closeRange = markdownFirstUnescapedRange(of: "\\)", in: content, from: bodyStart) {
+ let latex = String(content[bodyStart ..< closeRange.lowerBound])
+ if let descriptor = markdownAcceptedFormulaDescriptor(
+ latex: latex,
+ mode: .inline,
+ limits: limits,
+ acceptedFormulaCount: &acceptedFormulaCount,
+ totalFormulaCharacters: &totalFormulaCharacters,
+ nextPlaceholderIndex: &nextPlaceholderIndex
+ ) {
+ result.append(contentsOf: descriptor.placeholder)
+ formulasByPlaceholder[descriptor.placeholder] = descriptor
+ index = closeRange.upperBound
+ continue
+ }
+ }
+ }
+
+ if content[index] == "$", !markdownIsEscaped(content, at: index) {
+ guard let bodyStart = markdownIndex(content, offsetBy: 1, from: index), bodyStart < content.endIndex else {
+ result.append(content[index])
+ index = content.index(after: index)
+ continue
+ }
+ if content[bodyStart] == "$" || content[bodyStart].isWhitespace {
+ result.append(content[index])
+ index = content.index(after: index)
+ continue
+ }
+
+ var searchIndex = bodyStart
+ var matchedRange: Range?
+ while let closeRange = markdownFirstUnescapedRange(of: "$", in: content, from: searchIndex) {
+ let closerIndex = closeRange.lowerBound
+ let previousIndex = content.index(before: closerIndex)
+ if !content[previousIndex].isWhitespace {
+ matchedRange = closeRange
+ break
+ }
+ searchIndex = closeRange.upperBound
+ }
+
+ if let matchedRange {
+ let latex = String(content[bodyStart ..< matchedRange.lowerBound])
+ if let descriptor = markdownAcceptedFormulaDescriptor(
+ latex: latex,
+ mode: .inline,
+ limits: limits,
+ acceptedFormulaCount: &acceptedFormulaCount,
+ totalFormulaCharacters: &totalFormulaCharacters,
+ nextPlaceholderIndex: &nextPlaceholderIndex
+ ) {
+ result.append(contentsOf: descriptor.placeholder)
+ formulasByPlaceholder[descriptor.placeholder] = descriptor
+ index = matchedRange.upperBound
+ continue
+ }
+ }
+ }
+
+ result.append(content[index])
+ index = content.index(after: index)
+ }
+
+ result.append(contentsOf: ending)
+ return result
+}
+
+private func markdownFirstUnescapedRange(of pattern: String, in text: String, from startIndex: String.Index) -> Range? {
+ guard !pattern.isEmpty, startIndex <= text.endIndex else {
+ return nil
+ }
+
+ var searchIndex = startIndex
+ while searchIndex < text.endIndex {
+ guard let range = text.range(of: pattern, range: searchIndex ..< text.endIndex) else {
+ return nil
+ }
+ if !markdownIsEscaped(text, at: range.lowerBound) {
+ return range
+ }
+ searchIndex = range.upperBound
+ }
+ return nil
+}
+
+private func markdownIsEscaped(_ text: String, at index: String.Index) -> Bool {
+ guard index > text.startIndex else {
+ return false
+ }
+
+ var slashCount = 0
+ var currentIndex = text.index(before: index)
+ while true {
+ if text[currentIndex] == "\\" {
+ slashCount += 1
+ } else {
+ break
+ }
+ guard currentIndex > text.startIndex else {
+ break
+ }
+ currentIndex = text.index(before: currentIndex)
+ }
+
+ return slashCount % 2 == 1
+}
+
+private func markdownIndex(_ text: String, offsetBy distance: Int, from index: String.Index) -> String.Index? {
+ guard distance >= 0 else {
+ return nil
+ }
+ return text.index(index, offsetBy: distance, limitedBy: text.endIndex)
+}
+
+private func markdownIndex(afterRepeating character: Character, in text: String, from startIndex: String.Index) -> String.Index {
+ var index = startIndex
+ while index < text.endIndex, text[index] == character {
+ index = text.index(after: index)
+ }
+ return index
+}
+
func markdownWebpage(context: AccountContext, file: FileMediaReference) -> (webPage: TelegramMediaWebpage, fileURL: URL)? {
guard #available(iOS 15.0, *) else {
return nil
@@ -207,10 +982,19 @@ func markdownWebpage(context: AccountContext, file: FileMediaReference) -> (webP
@available(iOS 15.0, *)
private func markdownWebpage(context: AccountContext, file: FileMediaReference, fileURL: URL, data: Data) -> TelegramMediaWebpage? {
+ let limits = markdownSafetyLimits
+ guard markdownPassesPreflight(data: data, limits: limits) else {
+ return nil
+ }
+ guard let sourceText = markdownDecodedSourceText(data) else {
+ return nil
+ }
+ let preparedSource = markdownPreparedSource(text: sourceText, limits: limits)
+
let attributedString: NSAttributedString
do {
attributedString = try NSAttributedString(
- markdown: data,
+ markdown: Data(preparedSource.text.utf8),
options: .init(),
baseURL: fileURL.deletingLastPathComponent()
)
@@ -218,10 +1002,13 @@ private func markdownWebpage(context: AccountContext, file: FileMediaReference,
return nil
}
- let conversionContext = MarkdownConversionContext(context: context, documentURL: fileURL)
- let pageResult = markdownPageResult(from: attributedString, context: conversionContext)
+ let budget = MarkdownConversionBudget(limits: limits)
+ let conversionContext = MarkdownConversionContext(context: context, documentURL: fileURL, formulasByPlaceholder: preparedSource.formulasByPlaceholder, budget: budget)
+ guard let pageResult = markdownPageResult(from: attributedString, context: conversionContext) else {
+ return nil
+ }
let blocks = markdownBlocksWithGeneratedAnchors(pageResult.blocks)
- guard !blocks.isEmpty else {
+ guard !blocks.isEmpty, budget.validateFinalBlocks(blocks) else {
return nil
}
@@ -265,15 +1052,25 @@ private func markdownWebpage(context: AccountContext, file: FileMediaReference,
}
@available(iOS 15.0, *)
-private func markdownPageResult(from attributedString: NSAttributedString, context: MarkdownConversionContext) -> MarkdownPageResult {
+private func markdownPageResult(from attributedString: NSAttributedString, context: MarkdownConversionContext) -> MarkdownPageResult? {
+ guard context.budget.registerAttributedStringLength(attributedString.length) else {
+ return nil
+ }
+
var nodesByIdentity: [Int: MarkdownIntentNode] = [:]
var rootNodes: [MarkdownIntentNode] = []
var rootIdentities: Set = []
-
- attributedString.enumerateAttributes(in: NSRange(location: 0, length: attributedString.length), options: []) { attributes, range, _ in
+ var didAbort = false
+
+ attributedString.enumerateAttributes(in: NSRange(location: 0, length: attributedString.length), options: []) { attributes, range, stop in
guard range.length > 0 else {
return
}
+ guard context.budget.registerAttributeRun() else {
+ didAbort = true
+ stop.pointee = true
+ return
+ }
guard let presentationIntent = attributes[markdownPresentationIntentAttribute] as? PresentationIntent else {
return
}
@@ -281,13 +1078,23 @@ private func markdownPageResult(from attributedString: NSAttributedString, conte
guard !components.isEmpty else {
return
}
-
+ guard context.budget.registerPresentationIntentDepth(components.count) else {
+ didAbort = true
+ stop.pointee = true
+ return
+ }
+
var orderedNodes: [MarkdownIntentNode] = []
for component in components.reversed() {
let node: MarkdownIntentNode
if let current = nodesByIdentity[component.identity] {
node = current
} else {
+ guard context.budget.registerIntentNode() else {
+ didAbort = true
+ stop.pointee = true
+ return
+ }
let created = MarkdownIntentNode(component: component)
nodesByIdentity[component.identity] = created
node = created
@@ -308,37 +1115,69 @@ private func markdownPageResult(from attributedString: NSAttributedString, conte
}
}
- return context.makePageResult(blocks: markdownBlocks(from: rootNodes, context: context))
+ guard !didAbort, !context.budget.isExceeded else {
+ return nil
+ }
+ guard let blocks = markdownBlocks(from: rootNodes, context: context, depth: 0), !context.budget.isExceeded else {
+ return nil
+ }
+ return context.makePageResult(blocks: blocks)
}
-private func markdownBlocks(from nodes: [MarkdownIntentNode], context: MarkdownConversionContext) -> [InstantPageBlock] {
+private func markdownBlocks(from nodes: [MarkdownIntentNode], context: MarkdownConversionContext, depth: Int) -> [InstantPageBlock]? {
+ guard context.budget.registerBlockDepth(depth), !context.budget.isExceeded else {
+ return nil
+ }
+
var result: [InstantPageBlock] = []
for node in nodes {
- result.append(contentsOf: markdownBlocks(from: node, context: context))
+ guard let blocks = markdownBlocks(from: node, context: context, depth: depth + 1) else {
+ return nil
+ }
+ result.append(contentsOf: blocks)
+ if context.budget.isExceeded {
+ return nil
+ }
}
return result
}
-private func markdownBlocks(from node: MarkdownIntentNode, context: MarkdownConversionContext) -> [InstantPageBlock] {
+private func markdownBlocks(from node: MarkdownIntentNode, context: MarkdownConversionContext, depth: Int) -> [InstantPageBlock]? {
+ guard context.budget.registerBlockDepth(depth), !context.budget.isExceeded else {
+ return nil
+ }
+
switch node.kind {
case let .table(alignments):
- let rows = markdownTableRows(from: node.children, alignments: alignments, context: context)
+ guard let rows = markdownTableRows(from: node.children, alignments: alignments, context: context, depth: depth + 1) else {
+ return nil
+ }
guard !rows.isEmpty else {
return []
}
return [.table(title: .empty, rows: rows, bordered: true, striped: false)]
case let .header(level):
- let text = markdownRichText(from: node.attributedText, context: context)
+ guard let text = markdownRichText(from: node.attributedText, context: context) else {
+ return nil
+ }
guard markdownHasDisplayableContent(text) else {
return []
}
- if level <= 1 {
+ switch level {
+ case Int.min ... 1:
+ return [.title(text)]
+ case 2:
return [.header(text)]
- } else {
+ default:
return [.heading(text: text, level: Int32(max(3, min(level, 6))))]
}
case .paragraph:
- let inlineContent = markdownInlineContent(from: node.attributedText, context: context)
+ guard let inlineContent = markdownInlineContent(from: node.attributedText, context: context) else {
+ return nil
+ }
+ if let formula = inlineContent.standaloneBlockFormula {
+ return [.formula(latex: formula.latex)]
+ }
if let image = inlineContent.standaloneImage {
return [
.image(
@@ -355,7 +1194,9 @@ private func markdownBlocks(from node: MarkdownIntentNode, context: MarkdownConv
}
return [.paragraph(text)]
case let .codeBlock(languageHint):
- let text = markdownRichText(from: markdownTrimTrailingCodeBlockNewline(node.attributedText), context: context)
+ guard let text = markdownRichText(from: markdownTrimTrailingCodeBlockNewline(node.attributedText), context: context) else {
+ return nil
+ }
guard markdownHasDisplayableContent(text) else {
return []
}
@@ -365,12 +1206,15 @@ private func markdownBlocks(from node: MarkdownIntentNode, context: MarkdownConv
case .blockQuote:
var result: [InstantPageBlock] = []
for child in node.children {
- for childBlock in markdownBlocks(from: child, context: context) {
+ guard let childBlocks = markdownBlocks(from: child, context: context, depth: depth + 1) else {
+ return nil
+ }
+ for childBlock in childBlocks {
switch childBlock {
case let .paragraph(text):
result.append(.blockQuote(text: text, caption: .empty))
default:
- let plainText = markdownPlainText(from: childBlock)
+ let plainText = markdownPlainText(from: childBlock, depth: depth + 1)
if !plainText.isEmpty {
result.append(.blockQuote(text: .plain(plainText), caption: .empty))
}
@@ -379,29 +1223,39 @@ private func markdownBlocks(from node: MarkdownIntentNode, context: MarkdownConv
}
return result
case .orderedList:
- let items = markdownListItems(from: node.children, ordered: true, context: context)
+ guard let items = markdownListItems(from: node.children, ordered: true, context: context, depth: depth + 1) else {
+ return nil
+ }
guard !items.isEmpty else {
return []
}
return [.list(items: items, ordered: true)]
case .unorderedList:
- let items = markdownListItems(from: node.children, ordered: false, context: context)
+ guard let items = markdownListItems(from: node.children, ordered: false, context: context, depth: depth + 1) else {
+ return nil
+ }
guard !items.isEmpty else {
return []
}
return [.list(items: items, ordered: false)]
case .listItem(_), .tableHeaderRow, .tableRow, .tableCell(_), .unknown:
- return markdownBlocks(from: node.children, context: context)
+ return markdownBlocks(from: node.children, context: context, depth: depth + 1)
}
}
-private func markdownListItems(from nodes: [MarkdownIntentNode], ordered: Bool, context: MarkdownConversionContext) -> [InstantPageListItem] {
+private func markdownListItems(from nodes: [MarkdownIntentNode], ordered: Bool, context: MarkdownConversionContext, depth: Int) -> [InstantPageListItem]? {
+ guard context.budget.registerBlockDepth(depth), !context.budget.isExceeded else {
+ return nil
+ }
+
var result: [InstantPageListItem] = []
for node in nodes {
guard case let .listItem(ordinal) = node.kind else {
continue
}
- var blocks = markdownBlocks(from: node.children, context: context)
+ guard var blocks = markdownBlocks(from: node.children, context: context, depth: depth + 1) else {
+ return nil
+ }
let taskListState = markdownApplyTaskListMarker(to: &blocks)
let number: String?
if let taskListState {
@@ -476,17 +1330,25 @@ private func markdownTaskListMarker(in plainText: String) -> (MarkdownTaskListSt
}
}
-private func markdownTableRows(from nodes: [MarkdownIntentNode], alignments: [TableHorizontalAlignment], context: MarkdownConversionContext) -> [InstantPageTableRow] {
+private func markdownTableRows(from nodes: [MarkdownIntentNode], alignments: [TableHorizontalAlignment], context: MarkdownConversionContext, depth: Int) -> [InstantPageTableRow]? {
+ guard context.budget.registerBlockDepth(depth), !context.budget.isExceeded else {
+ return nil
+ }
+
var result: [InstantPageTableRow] = []
for node in nodes {
switch node.kind {
case .tableHeaderRow:
- let cells = markdownTableCells(from: node.children, alignments: alignments, header: true, context: context)
+ guard let cells = markdownTableCells(from: node.children, alignments: alignments, header: true, context: context, depth: depth + 1) else {
+ return nil
+ }
if !cells.isEmpty {
result.append(InstantPageTableRow(cells: cells))
}
case .tableRow:
- let cells = markdownTableCells(from: node.children, alignments: alignments, header: false, context: context)
+ guard let cells = markdownTableCells(from: node.children, alignments: alignments, header: false, context: context, depth: depth + 1) else {
+ return nil
+ }
if !cells.isEmpty {
result.append(InstantPageTableRow(cells: cells))
}
@@ -494,10 +1356,14 @@ private func markdownTableRows(from nodes: [MarkdownIntentNode], alignments: [Ta
continue
}
}
- return result
+ return context.budget.isExceeded ? nil : result
}
-private func markdownTableCells(from nodes: [MarkdownIntentNode], alignments: [TableHorizontalAlignment], header: Bool, context: MarkdownConversionContext) -> [InstantPageTableCell] {
+private func markdownTableCells(from nodes: [MarkdownIntentNode], alignments: [TableHorizontalAlignment], header: Bool, context: MarkdownConversionContext, depth: Int) -> [InstantPageTableCell]? {
+ guard context.budget.registerBlockDepth(depth), !context.budget.isExceeded else {
+ return nil
+ }
+
let maxColumnIndex = nodes.reduce(-1) { partialResult, node in
if case let .tableCell(column) = node.kind {
return max(partialResult, column)
@@ -509,7 +1375,10 @@ private func markdownTableCells(from nodes: [MarkdownIntentNode], alignments: [T
guard columnCount > 0 else {
return []
}
-
+ guard context.budget.registerTableColumns(columnCount) else {
+ return nil
+ }
+
var result: [InstantPageTableCell] = []
var nextColumn = 0
@@ -517,13 +1386,15 @@ private func markdownTableCells(from nodes: [MarkdownIntentNode], alignments: [T
guard case let .tableCell(column) = node.kind else {
continue
}
-
+
while nextColumn < column {
result.append(markdownEmptyTableCell(header: header, alignment: markdownTableAlignment(at: nextColumn, from: alignments)))
nextColumn += 1
}
-
- let text = markdownRichText(from: node.attributedText, context: context)
+
+ guard let text = markdownRichText(from: node.attributedText, context: context) else {
+ return nil
+ }
result.append(
InstantPageTableCell(
text: text,
@@ -542,6 +1413,9 @@ private func markdownTableCells(from nodes: [MarkdownIntentNode], alignments: [T
nextColumn += 1
}
+ guard context.budget.registerTableCells(result.count) else {
+ return nil
+ }
return result
}
@@ -563,29 +1437,35 @@ private func markdownTableAlignment(at index: Int, from alignments: [TableHorizo
return alignments[index]
}
-private func markdownRichText(from attributedString: NSAttributedString, context: MarkdownConversionContext) -> RichText {
- return markdownInlineContent(from: attributedString, context: context).richText
+private func markdownRichText(from attributedString: NSAttributedString, context: MarkdownConversionContext) -> RichText? {
+ return markdownInlineContent(from: attributedString, context: context)?.richText
}
-private func markdownInlineContent(from attributedString: NSAttributedString, context: MarkdownConversionContext) -> MarkdownInlineContent {
+private func markdownInlineContent(from attributedString: NSAttributedString, context: MarkdownConversionContext) -> MarkdownInlineContent? {
guard attributedString.length > 0, #available(iOS 15.0, *) else {
return MarkdownInlineContent(fragments: [])
}
-
+
var fragments: [MarkdownInlineFragment] = []
var htmlStyles: [MarkdownHTMLInlineStyle] = []
var consumeNextSoftBreak = false
-
- attributedString.enumerateAttributes(in: NSRange(location: 0, length: attributedString.length), options: []) { attributes, range, _ in
+ var didAbort = false
+
+ attributedString.enumerateAttributes(in: NSRange(location: 0, length: attributedString.length), options: []) { attributes, range, stop in
guard range.length > 0 else {
return
}
+ if context.budget.isExceeded {
+ didAbort = true
+ stop.pointee = true
+ return
+ }
let text = attributedString.attributedSubstring(from: range).string
guard !text.isEmpty else {
return
}
-
+
let inlineIntent: InlinePresentationIntent?
if let inlineIntentValue = attributes[markdownInlinePresentationIntentAttribute] as? InlinePresentationIntent {
inlineIntent = inlineIntentValue
@@ -594,11 +1474,16 @@ private func markdownInlineContent(from attributedString: NSAttributedString, co
} else {
inlineIntent = nil
}
-
+
if let inlineIntent, inlineIntent.contains(markdownInlineHTMLInlineIntent), let directive = markdownHTMLDirective(for: text) {
switch directive {
case let .open(style):
htmlStyles.append(style)
+ guard context.budget.registerInlineHTMLStyleDepth(htmlStyles.count) else {
+ didAbort = true
+ stop.pointee = true
+ return
+ }
case let .close(style):
if let index = htmlStyles.lastIndex(of: style) {
htmlStyles.remove(at: index)
@@ -609,7 +1494,7 @@ private func markdownInlineContent(from attributedString: NSAttributedString, co
}
return
}
-
+
if consumeNextSoftBreak {
if let inlineIntent, inlineIntent.contains(markdownSoftBreakInlineIntent), text == " " {
consumeNextSoftBreak = false
@@ -617,42 +1502,107 @@ private func markdownInlineContent(from attributedString: NSAttributedString, co
}
consumeNextSoftBreak = false
}
-
+
if let image = context.resolveImage(attributes: attributes) {
fragments.append(.image(image))
return
}
-
- var fragment: RichText = .plain(text)
- if let inlineIntent {
- if inlineIntent.contains(.stronglyEmphasized) {
- fragment = .bold(fragment)
+
+ let segments = markdownInlineTextSegments(from: text, formulasByPlaceholder: context.formulasByPlaceholder)
+ for segment in segments {
+ let baseText: RichText
+ let descriptor: MarkdownFormulaDescriptor?
+ switch segment {
+ case let .plain(plainText):
+ baseText = .plain(plainText)
+ descriptor = nil
+ case let .formula(formulaDescriptor):
+ baseText = .formula(latex: formulaDescriptor.latex)
+ descriptor = formulaDescriptor
}
- if inlineIntent.contains(.emphasized) {
- fragment = .italic(fragment)
+
+ var fragment = markdownApplyingInlineIntent(inlineIntent, to: baseText)
+ if let url = markdownLink(attributes: attributes, documentURL: context.documentURL) {
+ fragment = .url(text: fragment, url: url, webpageId: nil)
}
- if inlineIntent.contains(.strikethrough) {
- fragment = .strikethrough(fragment)
- }
- if inlineIntent.contains(.code) {
- fragment = .fixed(fragment)
- }
- if inlineIntent.contains(markdownHardBreakInlineIntent) {
- fragment = .plain("\n")
+ fragment = markdownApplyHTMLStyles(htmlStyles, to: fragment)
+
+ if let descriptor {
+ fragments.append(.formula(descriptor, fragment))
+ } else {
+ fragments.append(.richText(fragment))
}
}
-
- if let url = markdownLink(attributes: attributes, documentURL: context.documentURL) {
- fragment = .url(text: fragment, url: url, webpageId: nil)
- }
-
- fragments.append(.richText(markdownApplyHTMLStyles(htmlStyles, to: fragment)))
}
-
+
+ guard !didAbort, !context.budget.isExceeded else {
+ return nil
+ }
return MarkdownInlineContent(fragments: fragments)
}
+private func markdownInlineTextSegments(from text: String, formulasByPlaceholder: [String: MarkdownFormulaDescriptor]) -> [MarkdownInlineTextSegment] {
+ guard !text.isEmpty else {
+ return []
+ }
+
+ let nsText = text as NSString
+ let matches = markdownFormulaPlaceholderRegex.matches(in: text, range: NSRange(location: 0, length: nsText.length))
+ guard !matches.isEmpty else {
+ return [.plain(text)]
+ }
+
+ var result: [MarkdownInlineTextSegment] = []
+ var currentLocation = 0
+
+ for match in matches {
+ if match.range.location > currentLocation {
+ result.append(.plain(nsText.substring(with: NSRange(location: currentLocation, length: match.range.location - currentLocation))))
+ }
+
+ let placeholder = nsText.substring(with: match.range)
+ if let descriptor = formulasByPlaceholder[placeholder] {
+ result.append(.formula(descriptor))
+ } else {
+ result.append(.plain(placeholder))
+ }
+ currentLocation = match.range.location + match.range.length
+ }
+
+ if currentLocation < nsText.length {
+ result.append(.plain(nsText.substring(from: currentLocation)))
+ }
+
+ return result
+}
+
+@available(iOS 15.0, *)
+private func markdownApplyingInlineIntent(_ inlineIntent: InlinePresentationIntent?, to text: RichText) -> RichText {
+ guard let inlineIntent else {
+ return text
+ }
+
+ var result = text
+ if inlineIntent.contains(.stronglyEmphasized) {
+ result = .bold(result)
+ }
+ if inlineIntent.contains(.emphasized) {
+ result = .italic(result)
+ }
+ if inlineIntent.contains(.strikethrough) {
+ result = .strikethrough(result)
+ }
+ if inlineIntent.contains(.code) {
+ result = .fixed(result)
+ }
+ if inlineIntent.contains(markdownHardBreakInlineIntent) {
+ result = .plain("\n")
+ }
+ return result
+}
+
private enum MarkdownHTMLInlineStyle: Equatable {
+ case underline
case `subscript`
case superscript
case marked
@@ -666,6 +1616,10 @@ private enum MarkdownHTMLDirective {
private func markdownHTMLDirective(for text: String) -> MarkdownHTMLDirective? {
switch text.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() {
+ case "":
+ return .open(.underline)
+ case "":
+ return .close(.underline)
case "":
return .open(.subscript)
case "":
@@ -689,6 +1643,8 @@ private func markdownApplyHTMLStyles(_ styles: [MarkdownHTMLInlineStyle], to tex
var result = text
for style in styles {
switch style {
+ case .underline:
+ result = .underline(result)
case .subscript:
result = .subscript(result)
case .superscript:
@@ -720,13 +1676,13 @@ private func markdownImageURL(attributes: [NSAttributedString.Key: Any]) -> Stri
return nil
}
-private func markdownResolveImageSource(_ value: String) -> MarkdownResolvedImageSource {
+private func markdownResolveImageSource(_ value: String, limits: MarkdownSafetyLimits) -> MarkdownResolvedImageSource {
if value.hasPrefix("//") {
return .remote("https:\(value)")
}
if value.lowercased().hasPrefix("data:") {
- return markdownResolveDataImageSource(value)
+ return markdownResolveDataImageSource(value, limits: limits)
}
guard let url = URL(string: value), let scheme = url.scheme?.lowercased() else {
@@ -737,23 +1693,23 @@ private func markdownResolveImageSource(_ value: String) -> MarkdownResolvedImag
case "http", "https":
return .remote(url.absoluteString)
case "data":
- return markdownResolveDataImageSource(url.absoluteString)
+ return markdownResolveDataImageSource(url.absoluteString, limits: limits)
default:
return .unsupported
}
}
-private func markdownResolveDataImageSource(_ value: String) -> MarkdownResolvedImageSource {
+private func markdownResolveDataImageSource(_ value: String, limits: MarkdownSafetyLimits) -> MarkdownResolvedImageSource {
guard value.lowercased().hasPrefix("data:"),
let commaIndex = value.firstIndex(of: ",") else {
return .unsupported
}
-
+
let header = String(value[value.index(value.startIndex, offsetBy: 5) ..< commaIndex])
let payloadStart = value.index(after: commaIndex)
let payload = String(value[payloadStart...])
let isBase64 = header.lowercased().contains(";base64")
-
+
let data: Data?
if isBase64 {
data = Data(base64Encoded: payload, options: [.ignoreUnknownCharacters])
@@ -762,13 +1718,21 @@ private func markdownResolveDataImageSource(_ value: String) -> MarkdownResolved
} else {
data = nil
}
-
- guard let data,
- let image = UIImage(data: data),
+
+ guard let data = data, data.count <= limits.maxDataImageBytes else {
+ return .unsupported
+ }
+
+ guard let image = UIImage(data: data),
let dimensions = markdownImagePixelDimensions(image) else {
return .unsupported
}
-
+
+ let pixelCount = Int64(dimensions.width) * Int64(dimensions.height)
+ guard pixelCount <= Int64(limits.maxDataImagePixelCount) else {
+ return .unsupported
+ }
+
return .data(data, dimensions)
}
@@ -794,7 +1758,7 @@ private func markdownInlineImageDimensions(attributes: [NSAttributedString.Key:
guard let font = attributes[.font] as? UIFont else {
return markdownDefaultInlineImageDimensions
}
-
+
let side = max(markdownDefaultInlineImageDimensions.width, Int32(ceil(font.lineHeight)))
return PixelDimensions(width: side, height: side)
}
@@ -959,6 +1923,13 @@ private func markdownDroppingPrefixLength(_ length: Int, from text: RichText) ->
return dropped == .empty ? .empty : .phone(text: dropped, phone: phone)
case .image:
return text
+ case let .formula(latex):
+ let nsLatex = latex as NSString
+ if nsLatex.length <= length {
+ return .empty
+ } else {
+ return .plain(nsLatex.substring(from: length))
+ }
case let .anchor(inner, name):
let dropped = markdownDroppingPrefixLength(length, from: inner)
return dropped == .empty ? .empty : .anchor(text: dropped, name: name)
@@ -989,6 +1960,8 @@ private func markdownHasDisplayableContent(_ richText: RichText) -> Bool {
return items.contains(where: markdownHasDisplayableContent)
case .image:
return true
+ case let .formula(latex):
+ return !latex.isEmpty
}
}
@@ -1016,10 +1989,16 @@ private func markdownIsWhitespaceOnly(_ richText: RichText) -> Bool {
return items.allSatisfy(markdownIsWhitespaceOnly)
case .image:
return false
+ case let .formula(latex):
+ return latex.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
}
}
-private func markdownPlainText(from block: InstantPageBlock) -> String {
+private func markdownPlainText(from block: InstantPageBlock, depth: Int = 0) -> String {
+ guard depth <= markdownSafetyLimits.maxPresentationIntentDepth else {
+ return ""
+ }
+
switch block {
case let .title(text):
return text.plainText
@@ -1033,6 +2012,8 @@ private func markdownPlainText(from block: InstantPageBlock) -> String {
return text.plainText
case let .heading(text, _):
return text.plainText
+ case let .formula(latex):
+ return latex
case let .paragraph(text):
return text.plainText
case let .preformatted(text, _):
@@ -1084,9 +2065,17 @@ private func markdownNormalizedCodeBlockLanguage(_ language: String?) -> String?
return normalized.isEmpty ? nil : normalized
}
-private func markdownFirstParagraphText(from blocks: [InstantPageBlock]) -> String? {
+private func markdownFirstParagraphText(from blocks: [InstantPageBlock], depth: Int = 0) -> String? {
+ guard depth <= markdownSafetyLimits.maxPresentationIntentDepth else {
+ return nil
+ }
+
for block in blocks {
switch block {
+ case let .formula(latex):
+ if !latex.isEmpty {
+ return latex
+ }
case let .paragraph(text):
if !text.plainText.isEmpty {
return text.plainText
@@ -1099,7 +2088,7 @@ private func markdownFirstParagraphText(from blocks: [InstantPageBlock]) -> Stri
return text.plainText
}
case let .blocks(blocks, _):
- if let text = markdownFirstParagraphText(from: blocks) {
+ if let text = markdownFirstParagraphText(from: blocks, depth: depth + 1) {
return text
}
default:
@@ -1107,7 +2096,7 @@ private func markdownFirstParagraphText(from blocks: [InstantPageBlock]) -> Stri
}
}
case let .details(_, blocks, _):
- if let text = markdownFirstParagraphText(from: blocks) {
+ if let text = markdownFirstParagraphText(from: blocks, depth: depth + 1) {
return text
}
default:
diff --git a/submodules/BrowserUI/Sources/BrowserReadability.swift b/submodules/BrowserUI/Sources/BrowserReadability.swift
index 3f5a45c894..e021ad2d01 100644
--- a/submodules/BrowserUI/Sources/BrowserReadability.swift
+++ b/submodules/BrowserUI/Sources/BrowserReadability.swift
@@ -343,6 +343,8 @@ private func trimStart(_ input: RichText) -> RichText {
}
case .image:
break
+ case .formula:
+ break
}
return text
}
@@ -385,6 +387,8 @@ private func trimEnd(_ input: RichText) -> RichText {
}
case .image:
break
+ case .formula:
+ break
}
return text
}
@@ -428,6 +432,8 @@ private func trim(_ input: RichText) -> RichText {
}
case .image:
break
+ case .formula:
+ break
}
return text
}
@@ -470,6 +476,8 @@ private func addNewLine(_ input: RichText) -> RichText {
}
case .image:
break
+ case let .formula(latex):
+ text = .concat([.formula(latex: latex), .plain("\n")])
}
return text
}
diff --git a/submodules/ChatListUI/Sources/ChatContextMenus.swift b/submodules/ChatListUI/Sources/ChatContextMenus.swift
index ef9229872e..64b01eecac 100644
--- a/submodules/ChatListUI/Sources/ChatContextMenus.swift
+++ b/submodules/ChatListUI/Sources/ChatContextMenus.swift
@@ -331,7 +331,7 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch
}
return filters
}).startStandalone()
- chatListController?.present(UndoOverlayController( presentationData: presentationData, content: .chatAddedToFolder(context: context, chatTitle: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), folderTitle: title.rawAttributedString), elevatedLayout: false, animateInAsReplacement: true, action: { _ in
+ chatListController?.present(UndoOverlayController(presentationData: presentationData, content: .chatAddedToFolder(context: context, chatTitle: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), folderTitle: title.rawAttributedString), elevatedLayout: false, animateInAsReplacement: true, action: { _ in
return false
}), in: .current)
})
@@ -1023,7 +1023,7 @@ public func savedMessagesPeerMenuItems(context: AccountContext, threadId: Int64,
}
private func openCustomMute(context: AccountContext, peerId: EnginePeer.Id, threadId: Int64, baseController: ViewController) {
- let controller = ChatTimerScreen(context: context, updatedPresentationData: nil, style: .default, mode: .mute, currentTime: nil, dismissByTapOutside: true, completion: { [weak baseController] value in
+ let controller = ChatTimerScreen(context: context, updatedPresentationData: nil, style: .default, mode: .mute, currentTime: nil, completion: { [weak baseController] value in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
if value <= 0 {
diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift
index 62a2b18d10..313f684120 100644
--- a/submodules/ChatListUI/Sources/ChatListController.swift
+++ b/submodules/ChatListUI/Sources/ChatListController.swift
@@ -1901,12 +1901,12 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
chatController.canReadHistory.set(false)
source = .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController))
- let contextController = makeContextController(presentationData: strongSelf.presentationData, source: source, items: chatForumTopicMenuItems(context: strongSelf.context, peerId: peer.peerId, threadId: threadId, isPinned: nil, isClosed: nil, chatListController: strongSelf, joined: joined, canSelect: false) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture)
+ let contextController = makeContextController(context: strongSelf.context, presentationData: strongSelf.presentationData, source: source, items: chatForumTopicMenuItems(context: strongSelf.context, peerId: peer.peerId, threadId: threadId, isPinned: nil, isClosed: nil, chatListController: strongSelf, joined: joined, canSelect: false) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture)
strongSelf.presentInGlobalOverlay(contextController)
} else {
let chatListController = ChatListControllerImpl(context: strongSelf.context, location: .forum(peerId: channel.id), controlsHistoryPreload: false, hideNetworkActivityStatus: true, previewing: true, enableDebugActions: false)
chatListController.navigationPresentation = .master
- let contextController = makeContextController(presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatListController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)), items: chatContextMenuItems(context: strongSelf.context, peerId: peer.peerId, promoInfo: promoInfo, source: .chatList(filter: strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.chatListFilter), chatListController: strongSelf, joined: joined) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture)
+ let contextController = makeContextController(context: strongSelf.context, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatListController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)), items: chatContextMenuItems(context: strongSelf.context, peerId: peer.peerId, promoInfo: promoInfo, source: .chatList(filter: strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.chatListFilter), chatListController: strongSelf, joined: joined) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture)
strongSelf.presentInGlobalOverlay(contextController)
}
} else if let peer = peer.peer, peer.id == strongSelf.context.account.peerId, peerData.displayAsTopicList {
@@ -1918,7 +1918,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
source = .controller(ContextControllerContentSourceImpl(controller: peerInfoController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController))
}
- let contextController = makeContextController(presentationData: strongSelf.presentationData, source: source, items: chatContextMenuItems(context: strongSelf.context, peerId: peer.id, promoInfo: promoInfo, source: .chatList(filter: strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.chatListFilter), chatListController: strongSelf, joined: joined) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture)
+ let contextController = makeContextController(context: strongSelf.context, presentationData: strongSelf.presentationData, source: source, items: chatContextMenuItems(context: strongSelf.context, peerId: peer.id, promoInfo: promoInfo, source: .chatList(filter: strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.chatListFilter), chatListController: strongSelf, joined: joined) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture)
strongSelf.presentInGlobalOverlay(contextController)
}
} else {
@@ -1970,7 +1970,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
chatController.canReadHistory.set(false)
source = .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController))
- let contextController = makeContextController(presentationData: strongSelf.presentationData, source: source, items: chatForumTopicMenuItems(context: strongSelf.context, peerId: peer.peerId, threadId: threadId, isPinned: isPinned, isClosed: threadInfo?.isClosed, chatListController: strongSelf, joined: joined, canSelect: true) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture)
+ let contextController = makeContextController(context: strongSelf.context, presentationData: strongSelf.presentationData, source: source, items: chatForumTopicMenuItems(context: strongSelf.context, peerId: peer.peerId, threadId: threadId, isPinned: isPinned, isClosed: threadInfo?.isClosed, chatListController: strongSelf, joined: joined, canSelect: true) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture)
strongSelf.presentInGlobalOverlay(contextController)
}
}
@@ -2005,7 +2005,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
if case let .channel(channel) = peer, channel.isForumOrMonoForum {
let chatListController = ChatListControllerImpl(context: strongSelf.context, location: .forum(peerId: channel.id), controlsHistoryPreload: false, hideNetworkActivityStatus: true, previewing: true, enableDebugActions: false)
chatListController.navigationPresentation = .master
- let contextController = makeContextController(presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatListController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)), items: chatContextMenuItems(context: strongSelf.context, peerId: peer.id, promoInfo: nil, source: .search(source), chatListController: strongSelf, joined: false) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture)
+ let contextController = makeContextController(context: strongSelf.context, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatListController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)), items: chatContextMenuItems(context: strongSelf.context, peerId: peer.id, promoInfo: nil, source: .search(source), chatListController: strongSelf, joined: false) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture)
strongSelf.presentInGlobalOverlay(contextController)
} else {
let contextContentSource: ContextContentSource
diff --git a/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift b/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift
index 05fe3b01f8..b3b24b0048 100644
--- a/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift
+++ b/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift
@@ -225,15 +225,13 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder:
break inner
case let .Audio(isVoice, _, title, performer, _):
if !message.text.isEmpty {
- messageText = "🎤 \(messageText)"
- processed = true
- } else if isVoice {
- if message.text.isEmpty {
- messageText = strings.Message_Audio
- } else {
+ if enableMediaEmoji {
messageText = "🎤 \(messageText)"
}
processed = true
+ } else if isVoice {
+ messageText = strings.Message_Audio
+ processed = true
break inner
} else {
let descriptionString: String
diff --git a/submodules/Display/Source/Font.swift b/submodules/Display/Source/Font.swift
index 9a93be6b1c..4f1a4c0cf8 100644
--- a/submodules/Display/Source/Font.swift
+++ b/submodules/Display/Source/Font.swift
@@ -224,7 +224,7 @@ public struct Font {
} else {
let font: UIFont
switch design {
- case .regular:
+ case .regular, .round:
if traits.contains(.italic) {
if let descriptor = UIFont.systemFont(ofSize: size, weight: weight.weight).fontDescriptor.withSymbolicTraits([.traitItalic]) {
font = UIFont(descriptor: descriptor, size: size)
@@ -254,8 +254,6 @@ public struct Font {
} else {
font = UIFont(name: "Menlo", size: size - 1.0) ?? UIFont.systemFont(ofSize: size)
}
- case .round:
- font = UIFont(name: ".SFCompactRounded-Semibold", size: size) ?? UIFont.systemFont(ofSize: size)
case .camera:
func encodeText(string: String, key: Int16) -> String {
let nsString = string as NSString
diff --git a/submodules/InstantPageUI/BUILD b/submodules/InstantPageUI/BUILD
index 58c18d70ea..91e9d5a2bf 100644
--- a/submodules/InstantPageUI/BUILD
+++ b/submodules/InstantPageUI/BUILD
@@ -29,6 +29,7 @@ swift_library(
"//submodules/UndoUI:UndoUI",
"//submodules/TranslateUI:TranslateUI",
"//submodules/Tuples:Tuples",
+ "//third-party/SwiftMath:SwiftMath",
],
visibility = [
"//visibility:public",
diff --git a/submodules/InstantPageUI/Sources/InstantPageFormulaItem.swift b/submodules/InstantPageUI/Sources/InstantPageFormulaItem.swift
new file mode 100644
index 0000000000..3c6d8d1cbb
--- /dev/null
+++ b/submodules/InstantPageUI/Sources/InstantPageFormulaItem.swift
@@ -0,0 +1,185 @@
+import Foundation
+import UIKit
+import TelegramCore
+import AsyncDisplayKit
+import Display
+import TelegramPresentationData
+import TelegramUIPreferences
+import AccountContext
+import ContextUI
+
+final class InstantPageFormulaNode: ASImageNode, InstantPageNode {
+ let attachment: InstantPageMathAttachment
+
+ init(attachment: InstantPageMathAttachment) {
+ self.attachment = attachment
+ super.init()
+
+ self.isUserInteractionEnabled = false
+ self.displaysAsynchronously = false
+ self.image = attachment.rendered.image
+ }
+
+ func updateIsVisible(_ isVisible: Bool) {
+ }
+
+ func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
+ return nil
+ }
+
+ func updateHiddenMedia(media: InstantPageMedia?) {
+ }
+
+ func update(strings: PresentationStrings, theme: InstantPageTheme) {
+ }
+
+ func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
+ }
+}
+
+final class InstantPageTextFormulaItem: InstantPageItem {
+ var frame: CGRect
+ let attachment: InstantPageMathAttachment
+
+ let wantsNode: Bool = true
+ let separatesTiles: Bool = false
+ let medias: [InstantPageMedia] = []
+
+ init(frame: CGRect, attachment: InstantPageMathAttachment) {
+ self.frame = frame
+ self.attachment = attachment
+ }
+
+ func matchesAnchor(_ anchor: String) -> Bool {
+ return false
+ }
+
+ func drawInTile(context: CGContext) {
+ }
+
+ func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourceLocation: InstantPageSourceLocation, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?, getPreloadedResource: @escaping (String) -> Data?) -> InstantPageNode? {
+ return InstantPageFormulaNode(attachment: self.attachment)
+ }
+
+ func matchesNode(_ node: InstantPageNode) -> Bool {
+ guard let node = node as? InstantPageFormulaNode else {
+ return false
+ }
+ return self.attachment.isEqual(to: node.attachment)
+ }
+
+ func linkSelectionRects(at point: CGPoint) -> [CGRect] {
+ return []
+ }
+
+ func distanceThresholdGroup() -> Int? {
+ return nil
+ }
+
+ func distanceThresholdWithGroupCount(_ count: Int) -> CGFloat {
+ return 0.0
+ }
+}
+
+final class InstantPageFormulaItem: InstantPageItem {
+ var frame: CGRect
+ let attachment: InstantPageMathAttachment
+
+ let wantsNode: Bool = true
+ let separatesTiles: Bool = false
+ let medias: [InstantPageMedia] = []
+
+ init(frame: CGRect, attachment: InstantPageMathAttachment) {
+ self.frame = frame
+ self.attachment = attachment
+ }
+
+ func matchesAnchor(_ anchor: String) -> Bool {
+ return false
+ }
+
+ func drawInTile(context: CGContext) {
+ }
+
+ func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourceLocation: InstantPageSourceLocation, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?, getPreloadedResource: @escaping (String) -> Data?) -> InstantPageNode? {
+ return InstantPageFormulaNode(attachment: self.attachment)
+ }
+
+ func matchesNode(_ node: InstantPageNode) -> Bool {
+ guard let node = node as? InstantPageFormulaNode else {
+ return false
+ }
+ return self.attachment.isEqual(to: node.attachment)
+ }
+
+ func linkSelectionRects(at point: CGPoint) -> [CGRect] {
+ return []
+ }
+
+ func distanceThresholdGroup() -> Int? {
+ return nil
+ }
+
+ func distanceThresholdWithGroupCount(_ count: Int) -> CGFloat {
+ return 0.0
+ }
+}
+
+final class InstantPageScrollableFormulaItem: InstantPageScrollableItem {
+ var frame: CGRect
+ let attachment: InstantPageMathAttachment
+ let totalWidth: CGFloat
+ let horizontalInset: CGFloat
+
+ let medias: [InstantPageMedia] = []
+ let wantsNode: Bool = true
+ let separatesTiles: Bool = false
+ let isRTL: Bool = false
+
+ init(frame: CGRect, attachment: InstantPageMathAttachment, totalWidth: CGFloat, horizontalInset: CGFloat) {
+ self.frame = frame
+ self.attachment = attachment
+ self.totalWidth = totalWidth
+ self.horizontalInset = horizontalInset
+ }
+
+ var contentSize: CGSize {
+ return CGSize(width: self.totalWidth, height: self.frame.height)
+ }
+
+ func matchesAnchor(_ anchor: String) -> Bool {
+ return false
+ }
+
+ func drawInTile(context: CGContext) {
+ }
+
+ func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourceLocation: InstantPageSourceLocation, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?, getPreloadedResource: @escaping (String) -> Data?) -> InstantPageNode? {
+ let node = InstantPageFormulaNode(attachment: self.attachment)
+ node.frame = CGRect(origin: .zero, size: self.attachment.rendered.size)
+ return InstantPageScrollableNode(item: self, additionalNodes: [node])
+ }
+
+ func matchesNode(_ node: InstantPageNode) -> Bool {
+ if let node = node as? InstantPageScrollableNode {
+ return node.item === self
+ }
+ return false
+ }
+
+ func linkSelectionRects(at point: CGPoint) -> [CGRect] {
+ return []
+ }
+
+ func distanceThresholdGroup() -> Int? {
+ return nil
+ }
+
+ func distanceThresholdWithGroupCount(_ count: Int) -> CGFloat {
+ return 0.0
+ }
+
+ func textItemAtLocation(_ location: CGPoint) -> (InstantPageTextItem, CGPoint)? {
+ return nil
+ }
+}
diff --git a/submodules/InstantPageUI/Sources/InstantPageLayout.swift b/submodules/InstantPageUI/Sources/InstantPageLayout.swift
index c5bfaca6b9..937d04c6b4 100644
--- a/submodules/InstantPageUI/Sources/InstantPageLayout.swift
+++ b/submodules/InstantPageUI/Sources/InstantPageLayout.swift
@@ -291,6 +291,37 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
setupStyleStack(styleStack, theme: theme, attributes: theme.headingTextAttributes(level: level, link: false))
let (_, items, contentSize) = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0, offset: CGPoint(x: horizontalInset, y: 0.0), media: media, webpage: webpage)
return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items)
+ case let .formula(latex):
+ let styleStack = InstantPageTextStyleStack()
+ setupStyleStack(styleStack, theme: theme, category: .paragraph, link: false)
+ let attributes = styleStack.textAttributes()
+ let textColor = (attributes[NSAttributedString.Key.foregroundColor] as? UIColor) ?? theme.textCategories.paragraph.color
+ let fontSize = (attributes[NSAttributedString.Key.font] as? UIFont)?.pointSize ?? theme.textCategories.paragraph.font.size
+
+ guard let attachment = instantPageMathAttachment(latex: latex, fontSize: fontSize, textColor: textColor, mode: .block) else {
+ let (_, items, contentSize) = layoutTextItemWithString(attributedStringForRichText(.plain(latex), styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0, offset: CGPoint(x: horizontalInset, y: 0.0), media: media, webpage: webpage)
+ return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items)
+ }
+
+ let availableWidth = boundingWidth - horizontalInset * 2.0
+ if attachment.rendered.size.width > availableWidth {
+ let scrollableItem = InstantPageScrollableFormulaItem(
+ frame: CGRect(origin: .zero, size: CGSize(width: boundingWidth, height: attachment.rendered.size.height)),
+ attachment: attachment,
+ totalWidth: attachment.rendered.size.width,
+ horizontalInset: horizontalInset
+ )
+ return InstantPageLayout(origin: CGPoint(), contentSize: scrollableItem.frame.size, items: [scrollableItem])
+ } else {
+ let item = InstantPageFormulaItem(
+ frame: CGRect(
+ origin: CGPoint(x: horizontalInset + floor((availableWidth - attachment.rendered.size.width) / 2.0), y: 0.0),
+ size: attachment.rendered.size
+ ),
+ attachment: attachment
+ )
+ return InstantPageLayout(origin: CGPoint(), contentSize: CGSize(width: boundingWidth, height: attachment.rendered.size.height), items: [item])
+ }
case let .paragraph(text):
let styleStack = InstantPageTextStyleStack()
setupStyleStack(styleStack, theme: theme, category: .paragraph, link: false)
diff --git a/submodules/InstantPageUI/Sources/InstantPageLayoutSpacings.swift b/submodules/InstantPageUI/Sources/InstantPageLayoutSpacings.swift
index 51cc7d4886..6536f2a200 100644
--- a/submodules/InstantPageUI/Sources/InstantPageLayoutSpacings.swift
+++ b/submodules/InstantPageUI/Sources/InstantPageLayoutSpacings.swift
@@ -27,20 +27,38 @@ func spacingBetweenBlocks(upper: InstantPageBlock?, lower: InstantPageBlock?) ->
return 31.0
case (.preformatted, .paragraph):
return 19.0
+ case (.formula, .paragraph):
+ return 19.0
case (.paragraph, .paragraph):
return 25.0
case (_, .paragraph):
return 20.0
+ case (.title, .formula), (.authorDate, .formula):
+ return 34.0
+ case (.header, .formula), (.subheader, .formula), (.heading, .formula):
+ return 25.0
+ case (.list, .formula):
+ return 31.0
+ case (.preformatted, .formula):
+ return 19.0
+ case (.paragraph, .formula):
+ return 19.0
+ case (_, .formula):
+ return 20.0
case (.title, .list), (.authorDate, .list):
return 34.0
case (.header, .list), (.subheader, .list), (.heading, .list):
return 31.0
case (.preformatted, .list):
return 19.0
+ case (.formula, .list):
+ return 25.0
case (_, .list):
return 25.0
case (.paragraph, .preformatted):
return 19.0
+ case (.formula, .preformatted):
+ return 19.0
case (_, .preformatted):
return 20.0
case (_, .header), (_, .subheader), (_, .heading):
diff --git a/submodules/InstantPageUI/Sources/InstantPageMath.swift b/submodules/InstantPageUI/Sources/InstantPageMath.swift
new file mode 100644
index 0000000000..1c77532019
--- /dev/null
+++ b/submodules/InstantPageUI/Sources/InstantPageMath.swift
@@ -0,0 +1,79 @@
+import Foundation
+import UIKit
+import SwiftMath
+
+enum InstantPageMathMode {
+ case inline
+ case block
+}
+
+struct InstantPageMathRenderResult {
+ let image: UIImage
+ let size: CGSize
+ let width: CGFloat
+ let ascent: CGFloat
+ let descent: CGFloat
+}
+
+final class InstantPageMathAttachment: NSObject {
+ let latex: String
+ let fontSize: CGFloat
+ let textColor: UIColor
+ let mode: InstantPageMathMode
+ let rendered: InstantPageMathRenderResult
+
+ init(latex: String, fontSize: CGFloat, textColor: UIColor, mode: InstantPageMathMode, rendered: InstantPageMathRenderResult) {
+ self.latex = latex
+ self.fontSize = fontSize
+ self.textColor = textColor
+ self.mode = mode
+ self.rendered = rendered
+ }
+
+ func isEqual(to other: InstantPageMathAttachment) -> Bool {
+ return self.latex == other.latex
+ && self.fontSize == other.fontSize
+ && self.mode == other.mode
+ && self.textColor.isEqual(other.textColor)
+ && self.rendered.size == other.rendered.size
+ && self.rendered.ascent == other.rendered.ascent
+ && self.rendered.descent == other.rendered.descent
+ }
+}
+
+func instantPageMathAttachment(latex: String, fontSize: CGFloat, textColor: UIColor, mode: InstantPageMathMode) -> InstantPageMathAttachment? {
+ let effectiveFontSize: CGFloat
+ let multiplier: CGFloat = 1.12
+ switch mode {
+ case .inline:
+ effectiveFontSize = fontSize * multiplier
+ case .block:
+ effectiveFontSize = fontSize * multiplier
+ }
+
+ guard let rendered = instantPageRenderMath(latex: latex, fontSize: effectiveFontSize, textColor: textColor, mode: mode) else {
+ return nil
+ }
+ return InstantPageMathAttachment(latex: latex, fontSize: effectiveFontSize, textColor: textColor, mode: mode, rendered: rendered)
+}
+
+private func instantPageRenderMath(latex: String, fontSize: CGFloat, textColor: UIColor, mode: InstantPageMathMode) -> InstantPageMathRenderResult? {
+ let renderMode: MTMathUILabelMode
+ switch mode {
+ case .inline:
+ renderMode = .text
+ case .block:
+ renderMode = .display
+ }
+
+ guard let rendered = MTMathRenderer.render(latex: latex, fontSize: fontSize, textColor: textColor, mode: renderMode) else {
+ return nil
+ }
+ return InstantPageMathRenderResult(
+ image: rendered.image,
+ size: rendered.size,
+ width: rendered.width,
+ ascent: rendered.ascent,
+ descent: rendered.descent
+ )
+}
diff --git a/submodules/InstantPageUI/Sources/InstantPageTextItem.swift b/submodules/InstantPageUI/Sources/InstantPageTextItem.swift
index 1b4c08197c..a66ff44839 100644
--- a/submodules/InstantPageUI/Sources/InstantPageTextItem.swift
+++ b/submodules/InstantPageUI/Sources/InstantPageTextItem.swift
@@ -26,6 +26,7 @@ public final class InstantPageUrlItem: Equatable {
struct InstantPageTextMarkedItem {
let frame: CGRect
let color: UIColor
+ let range: NSRange
}
struct InstantPageTextStrikethroughItem {
@@ -38,6 +39,12 @@ struct InstantPageTextImageItem {
let id: EngineMedia.Id
}
+struct InstantPageTextFormulaRun {
+ let frame: CGRect
+ let range: NSRange
+ let attachment: InstantPageMathAttachment
+}
+
public struct InstantPageTextAnchorItem {
public let name: String
public let anchorText: NSAttributedString?
@@ -63,16 +70,18 @@ public final class InstantPageTextLine {
let strikethroughItems: [InstantPageTextStrikethroughItem]
let markedItems: [InstantPageTextMarkedItem]
let imageItems: [InstantPageTextImageItem]
+ let formulaItems: [InstantPageTextFormulaRun]
public let anchorItems: [InstantPageTextAnchorItem]
let isRTL: Bool
- init(line: CTLine, range: NSRange, frame: CGRect, strikethroughItems: [InstantPageTextStrikethroughItem], markedItems: [InstantPageTextMarkedItem], imageItems: [InstantPageTextImageItem], anchorItems: [InstantPageTextAnchorItem], isRTL: Bool) {
+ init(line: CTLine, range: NSRange, frame: CGRect, strikethroughItems: [InstantPageTextStrikethroughItem], markedItems: [InstantPageTextMarkedItem], imageItems: [InstantPageTextImageItem], formulaItems: [InstantPageTextFormulaRun], anchorItems: [InstantPageTextAnchorItem], isRTL: Bool) {
self.line = line
self.range = range
self.frame = frame
self.strikethroughItems = strikethroughItems
self.markedItems = markedItems
self.imageItems = imageItems
+ self.formulaItems = formulaItems
self.anchorItems = anchorItems
self.isRTL = isRTL
}
@@ -88,6 +97,75 @@ private func frameForLine(_ line: InstantPageTextLine, boundingWidth: CGFloat, a
return lineFrame
}
+private func expandedFrameForLine(_ line: InstantPageTextLine, boundingWidth: CGFloat, alignment: NSTextAlignment) -> CGRect {
+ var lineFrame = line.frame
+ for imageItem in line.imageItems {
+ if imageItem.frame.minY < lineFrame.minY {
+ let delta = lineFrame.minY - imageItem.frame.minY - 2.0
+ lineFrame = CGRect(x: lineFrame.minX, y: lineFrame.minY - delta, width: lineFrame.width, height: lineFrame.height + delta)
+ }
+ if imageItem.frame.maxY > lineFrame.maxY {
+ let delta = imageItem.frame.maxY - lineFrame.maxY - 2.0
+ lineFrame = CGRect(x: lineFrame.minX, y: lineFrame.minY, width: lineFrame.width, height: lineFrame.height + delta)
+ }
+ }
+ for formulaItem in line.formulaItems {
+ if formulaItem.frame.minY < lineFrame.minY {
+ let delta = lineFrame.minY - formulaItem.frame.minY - 2.0
+ lineFrame = CGRect(x: lineFrame.minX, y: lineFrame.minY - delta, width: lineFrame.width, height: lineFrame.height + delta)
+ }
+ if formulaItem.frame.maxY > lineFrame.maxY {
+ let delta = formulaItem.frame.maxY - lineFrame.maxY - 2.0
+ lineFrame = CGRect(x: lineFrame.minX, y: lineFrame.minY, width: lineFrame.width, height: lineFrame.height + delta)
+ }
+ }
+ lineFrame = lineFrame.insetBy(dx: 0.0, dy: -4.0)
+ if alignment == .center {
+ lineFrame.origin.x = floor((boundingWidth - lineFrame.size.width) / 2.0)
+ } else if alignment == .right || (alignment == .natural && line.isRTL) {
+ lineFrame.origin.x = boundingWidth - lineFrame.size.width
+ }
+ return lineFrame
+}
+
+private func alignedAttachmentFrame(_ frame: CGRect, line: InstantPageTextLine, boundingWidth: CGFloat, alignment: NSTextAlignment) -> CGRect {
+ let lineFrame = frameForLine(line, boundingWidth: boundingWidth, alignment: alignment)
+ return frame.offsetBy(dx: lineFrame.minX - line.frame.minX, dy: 0.0)
+}
+
+private func localAttachmentBoundsForRange(_ range: NSRange, imageItems: [InstantPageTextImageItem], formulaItems: [InstantPageTextFormulaRun]) -> CGRect? {
+ var result: CGRect?
+
+ for imageItem in imageItems {
+ if NSIntersectionRange(range, imageItem.range).length != 0 {
+ if let current = result {
+ result = current.union(imageItem.frame)
+ } else {
+ result = imageItem.frame
+ }
+ }
+ }
+
+ for formulaItem in formulaItems {
+ if NSIntersectionRange(range, formulaItem.range).length != 0 {
+ if let current = result {
+ result = current.union(formulaItem.frame)
+ } else {
+ result = formulaItem.frame
+ }
+ }
+ }
+
+ return result
+}
+
+private func attachmentBoundsForRange(_ range: NSRange, line: InstantPageTextLine, boundingWidth: CGFloat, alignment: NSTextAlignment) -> CGRect? {
+ guard let localBounds = localAttachmentBoundsForRange(range, imageItems: line.imageItems, formulaItems: line.formulaItems) else {
+ return nil
+ }
+ return alignedAttachmentFrame(localBounds, line: line, boundingWidth: boundingWidth, alignment: alignment)
+}
+
public final class InstantPageTextItem: InstantPageItem {
let attributedString: NSAttributedString
public let lines: [InstantPageTextLine]
@@ -197,7 +275,7 @@ public final class InstantPageTextItem: InstantPageItem {
for i in 0 ..< self.lines.count {
let line = self.lines[i]
- let lineFrame = frameForLine(line, boundingWidth: boundsWidth, alignment: self.alignment)
+ let lineFrame = expandedFrameForLine(line, boundingWidth: boundsWidth, alignment: self.alignment)
if lineFrame.insetBy(dx: -5.0, dy: -5.0).contains(transformedPoint) {
var index = CTLineGetStringIndexForPosition(line.line, CGPoint(x: transformedPoint.x - lineFrame.minX, y: transformedPoint.y - lineFrame.minY))
if index == self.attributedString.length {
@@ -238,8 +316,20 @@ public final class InstantPageTextItem: InstantPageItem {
}
let lineFrame = frameForLine(line, boundingWidth: boundsWidth, alignment: self.alignment)
let width = abs(rightOffset - leftOffset)
+
+ var rect: CGRect?
if width > 1.0 {
- rects.append(CGRect(origin: CGPoint(x: lineFrame.minX + (leftOffset < rightOffset ? leftOffset : rightOffset), y: lineFrame.minY), size: CGSize(width: width, height: lineFrame.size.height)))
+ rect = CGRect(origin: CGPoint(x: lineFrame.minX + (leftOffset < rightOffset ? leftOffset : rightOffset), y: lineFrame.minY), size: CGSize(width: width, height: lineFrame.size.height))
+ }
+ if let attachmentBounds = attachmentBoundsForRange(lineRange, line: line, boundingWidth: boundsWidth, alignment: self.alignment) {
+ if rect != nil {
+ rect = rect!.union(attachmentBounds)
+ } else {
+ rect = attachmentBounds
+ }
+ }
+ if let rect, rect.width > 1.0 {
+ rects.append(rect)
}
}
}
@@ -304,25 +394,7 @@ public final class InstantPageTextItem: InstantPageItem {
}
}
- var lineFrame = line.frame
- for imageItem in line.imageItems {
- if imageItem.frame.minY < lineFrame.minY {
- let delta = lineFrame.minY - imageItem.frame.minY - 2.0
- lineFrame = CGRect(x: lineFrame.minX, y: lineFrame.minY - delta, width: lineFrame.width, height: lineFrame.height + delta)
- }
- if imageItem.frame.maxY > lineFrame.maxY {
- let delta = imageItem.frame.maxY - lineFrame.maxY - 2.0
- lineFrame = CGRect(x: lineFrame.minX, y: lineFrame.minY, width: lineFrame.width, height: lineFrame.height + delta)
- }
- }
- lineFrame = lineFrame.insetBy(dx: 0.0, dy: -4.0)
- if self.alignment == .center {
- lineFrame.origin.x = floor((boundsWidth - lineFrame.size.width) / 2.0)
- } else if self.alignment == .right {
- lineFrame.origin.x = boundsWidth - lineFrame.size.width
- } else if self.alignment == .natural && self.rtlLineIndices.contains(i) {
- lineFrame.origin.x = boundsWidth - lineFrame.size.width
- }
+ let lineFrame = expandedFrameForLine(line, boundingWidth: boundsWidth, alignment: self.alignment)
let width = max(0.0, abs(rightOffset - leftOffset))
@@ -368,25 +440,7 @@ public final class InstantPageTextItem: InstantPageItem {
for i in 0 ..< self.lines.count {
let line = self.lines[i]
- var lineFrame = line.frame
- for imageItem in line.imageItems {
- if imageItem.frame.minY < lineFrame.minY {
- let delta = lineFrame.minY - imageItem.frame.minY - 2.0
- lineFrame = CGRect(x: lineFrame.minX, y: lineFrame.minY - delta, width: lineFrame.width, height: lineFrame.height + delta)
- }
- if imageItem.frame.maxY > lineFrame.maxY {
- let delta = imageItem.frame.maxY - lineFrame.maxY - 2.0
- lineFrame = CGRect(x: lineFrame.minX, y: lineFrame.minY, width: lineFrame.width, height: lineFrame.height + delta)
- }
- }
- lineFrame = lineFrame.insetBy(dx: 0.0, dy: -4.0)
- if self.alignment == .center {
- lineFrame.origin.x = floor((boundsWidth - lineFrame.size.width) / 2.0)
- } else if self.alignment == .right {
- lineFrame.origin.x = boundsWidth - lineFrame.size.width
- } else if self.alignment == .natural && self.rtlLineIndices.contains(i) {
- lineFrame.origin.x = boundsWidth - lineFrame.size.width
- }
+ let lineFrame = expandedFrameForLine(line, boundingWidth: boundsWidth, alignment: self.alignment)
if lineFrame.minX < topLeft.x {
topLeft = CGPoint(x: lineFrame.minX, y: topLeft.y)
@@ -633,6 +687,43 @@ func attributedStringForRichText(_ text: RichText, styleStack: InstantPageTextSt
let mutableAttributedString = attributedStringForRichText(.plain(" "), styleStack: styleStack, url: url).mutableCopy() as! NSMutableAttributedString
mutableAttributedString.addAttributes(attrDictionaryDelegate, range: NSMakeRange(0, mutableAttributedString.length))
return mutableAttributedString
+ case let .formula(latex):
+ let attributes = styleStack.textAttributes()
+ let textColor = (attributes[NSAttributedString.Key.foregroundColor] as? UIColor) ?? UIColor.black
+ let fontSize = (attributes[NSAttributedString.Key.font] as? UIFont)?.pointSize ?? 16.0
+ guard let attachment = instantPageMathAttachment(latex: latex, fontSize: fontSize, textColor: textColor, mode: .inline) else {
+ var fallbackAttributes = attributes
+ if let url = url {
+ fallbackAttributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] = url
+ }
+ return NSAttributedString(string: latex, attributes: fallbackAttributes)
+ }
+
+ struct RunStruct {
+ let ascent: CGFloat
+ let descent: CGFloat
+ let width: CGFloat
+ }
+ let extentBuffer = UnsafeMutablePointer.allocate(capacity: 1)
+ extentBuffer.initialize(to: RunStruct(ascent: attachment.rendered.ascent, descent: attachment.rendered.descent, width: attachment.rendered.size.width))
+ var callbacks = CTRunDelegateCallbacks(version: kCTRunDelegateVersion1, dealloc: { _ in
+ }, getAscent: { pointer -> CGFloat in
+ let data = pointer.assumingMemoryBound(to: RunStruct.self)
+ return data.pointee.ascent
+ }, getDescent: { pointer -> CGFloat in
+ let data = pointer.assumingMemoryBound(to: RunStruct.self)
+ return data.pointee.descent
+ }, getWidth: { pointer -> CGFloat in
+ let data = pointer.assumingMemoryBound(to: RunStruct.self)
+ return data.pointee.width
+ })
+ let delegate = CTRunDelegateCreate(&callbacks, extentBuffer)
+ let mutableAttributedString = attributedStringForRichText(.plain(" "), styleStack: styleStack, url: url).mutableCopy() as! NSMutableAttributedString
+ mutableAttributedString.addAttributes([
+ kCTRunDelegateAttributeName as NSAttributedString.Key: delegate as Any,
+ NSAttributedString.Key(rawValue: InstantPageFormulaAttribute): attachment
+ ], range: NSMakeRange(0, mutableAttributedString.length))
+ return mutableAttributedString
case let .anchor(text, name):
var empty = false
var text = text
@@ -655,6 +746,7 @@ func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFlo
var lines: [InstantPageTextLine] = []
var imageItems: [InstantPageTextImageItem] = []
+ var formulaItems: [InstantPageTextFormulaItem] = []
var font = string.attribute(NSAttributedString.Key.font, at: 0, effectiveRange: nil) as? UIFont
if font == nil {
let range = NSMakeRange(0, string.length)
@@ -665,7 +757,8 @@ func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFlo
}
}
let image = string.attribute(NSAttributedString.Key.init(rawValue: InstantPageMediaIdAttribute), at: 0, effectiveRange: nil)
- guard font != nil || image != nil else {
+ let formula = string.attribute(NSAttributedString.Key(rawValue: InstantPageFormulaAttribute), at: 0, effectiveRange: nil)
+ guard font != nil || image != nil || formula != nil else {
return (nil, [], CGSize())
}
@@ -686,7 +779,6 @@ func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFlo
var hasAnchors = false
var maxLineWidth: CGFloat = 0.0
- var maxImageHeight: CGFloat = 0.0
var extraDescent: CGFloat = 0.0
let text = string.string
var indexOffset: CFIndex?
@@ -741,6 +833,8 @@ func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFlo
let hadExtraDescent = extraDescent > 0.0
extraDescent = 0.0
var lineImageItems: [InstantPageTextImageItem] = []
+ var lineFormulaItems: [InstantPageTextFormulaRun] = []
+ var lineMaxAttachmentHeight: CGFloat = 0.0
var isRTL = false
if let glyphRuns = CTLineGetGlyphRuns(line) as? [CTRun], !glyphRuns.isEmpty {
if let run = glyphRuns.first, CTRunGetStatus(run).contains(CTRunStatus.rightToLeft) {
@@ -769,20 +863,49 @@ func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFlo
if !fontLineHeight.isZero {
extraDescent = max(extraDescent, imageFrame.maxY - (workingLineOrigin.y + fontLineHeight + minSpacing))
}
- maxImageHeight = max(maxImageHeight, imageFrame.height)
+ lineMaxAttachmentHeight = max(lineMaxAttachmentHeight, imageFrame.height)
lineImageItems.append(InstantPageTextImageItem(frame: imageFrame, range: range, id: EngineMedia.Id(namespace: Namespaces.Media.CloudFile, id: id)))
+ } else if let attachment = attributes[NSAttributedString.Key(rawValue: InstantPageFormulaAttribute)] as? InstantPageMathAttachment {
+ let xOffset = CTLineGetOffsetForStringIndex(line, range.location, nil)
+ let baselineOffset = (attributes[NSAttributedString.Key.baselineOffset] as? CGFloat) ?? 0.0
+ var formulaFrame = CGRect(
+ origin: CGPoint(
+ x: workingLineOrigin.x + xOffset,
+ y: workingLineOrigin.y + fontLineHeight + baselineOffset - attachment.rendered.ascent
+ ),
+ size: attachment.rendered.size
+ )
+
+ let minSpacing = fontLineSpacing - 4.0
+ let delta = workingLineOrigin.y - minSpacing - formulaFrame.minY - appliedLineOffset
+ if !fontAscent.isZero && delta > 0.0 {
+ workingLineOrigin.y += delta
+ appliedLineOffset += delta
+ formulaFrame.origin = formulaFrame.origin.offsetBy(dx: 0.0, dy: delta)
+ }
+ if !fontLineHeight.isZero {
+ extraDescent = max(extraDescent, formulaFrame.maxY - (workingLineOrigin.y + fontLineHeight + minSpacing))
+ }
+ lineMaxAttachmentHeight = max(lineMaxAttachmentHeight, formulaFrame.height)
+ lineFormulaItems.append(InstantPageTextFormulaRun(frame: formulaFrame, range: range, attachment: attachment))
}
}
}
}
- if substring.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && lineImageItems.count > 0 {
+ if substring.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && (!lineImageItems.isEmpty || !lineFormulaItems.isEmpty) {
extraDescent += max(6.0, fontLineSpacing / 2.0)
}
- if !minimizeWidth && !hadIndexOffset && lineCharacterCount > 1 && lineWidth > currentMaxWidth + 5.0, let imageItem = lineImageItems.last {
- indexOffset = -(lastIndex + lineCharacterCount - imageItem.range.lowerBound)
- continue
+ if !minimizeWidth && !hadIndexOffset && lineCharacterCount > 1 && lineWidth > currentMaxWidth + 5.0 {
+ if let imageItem = lineImageItems.last {
+ indexOffset = -(lastIndex + lineCharacterCount - imageItem.range.lowerBound)
+ continue
+ }
+ if let formulaItem = lineFormulaItems.last {
+ indexOffset = -(lastIndex + lineCharacterCount - formulaItem.range.lowerBound)
+ continue
+ }
}
var strikethroughItems: [InstantPageTextStrikethroughItem] = []
@@ -807,7 +930,7 @@ func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFlo
let lowerX = floor(CTLineGetOffsetForStringIndex(line, range.location, nil))
let upperX = ceil(CTLineGetOffsetForStringIndex(line, range.location + range.length, nil))
let x = lowerX < upperX ? lowerX : upperX
- markedItems.append(InstantPageTextMarkedItem(frame: CGRect(x: workingLineOrigin.x + x, y: workingLineOrigin.y + delta, width: abs(upperX - lowerX), height: lineHeight), color: color))
+ markedItems.append(InstantPageTextMarkedItem(frame: CGRect(x: workingLineOrigin.x + x, y: workingLineOrigin.y + delta, width: abs(upperX - lowerX), height: lineHeight), color: color, range: range))
}
if let item = attributes[NSAttributedString.Key.init(rawValue: InstantPageAnchorAttribute)] as? Dictionary, let name = item["name"] as? String, let empty = item["empty"] as? Bool {
anchorItems.append(InstantPageTextAnchorItem(name: name, anchorText: item["text"] as? NSAttributedString, empty: empty))
@@ -822,11 +945,35 @@ func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFlo
workingLineOrigin.y += fontLineSpacing
}
- let height = !fontLineHeight.isZero ? fontLineHeight : maxImageHeight
- let textLine = InstantPageTextLine(line: line, range: lineRange, frame: CGRect(x: workingLineOrigin.x, y: workingLineOrigin.y, width: lineWidth, height: height), strikethroughItems: strikethroughItems, markedItems: markedItems, imageItems: lineImageItems, anchorItems: anchorItems, isRTL: isRTL)
+ let height = !fontLineHeight.isZero ? max(fontLineHeight, lineMaxAttachmentHeight) : lineMaxAttachmentHeight
+ if !lineFormulaItems.isEmpty {
+ let baselineAdjustment = height - fontLineHeight
+ if !baselineAdjustment.isZero {
+ lineFormulaItems = lineFormulaItems.map { item in
+ InstantPageTextFormulaRun(
+ frame: item.frame.offsetBy(dx: 0.0, dy: baselineAdjustment),
+ range: item.range,
+ attachment: item.attachment
+ )
+ }
+ }
+ }
+ if !markedItems.isEmpty {
+ markedItems = markedItems.map { item in
+ if let attachmentBounds = localAttachmentBoundsForRange(item.range, imageItems: lineImageItems, formulaItems: lineFormulaItems) {
+ return InstantPageTextMarkedItem(frame: attachmentBounds, color: item.color, range: item.range)
+ } else {
+ return item
+ }
+ }
+ }
+ let textLine = InstantPageTextLine(line: line, range: lineRange, frame: CGRect(x: workingLineOrigin.x, y: workingLineOrigin.y, width: lineWidth, height: height), strikethroughItems: strikethroughItems, markedItems: markedItems, imageItems: lineImageItems, formulaItems: lineFormulaItems, anchorItems: anchorItems, isRTL: isRTL)
lines.append(textLine)
imageItems.append(contentsOf: lineImageItems)
+ for formulaItem in lineFormulaItems {
+ formulaItems.append(InstantPageTextFormulaItem(frame: formulaItem.frame, attachment: formulaItem.attachment))
+ }
if lineWidth > maxLineWidth {
maxLineWidth = lineWidth
@@ -853,7 +1000,7 @@ func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFlo
var textWidth = boundingWidth
var requiresScroll = false
- if !imageItems.isEmpty && maxLineWidth > boundingWidth + 10.0 {
+ if (!imageItems.isEmpty || !formulaItems.isEmpty) && maxLineWidth > boundingWidth + 10.0 {
textWidth = maxLineWidth
requiresScroll = true
}
@@ -870,13 +1017,13 @@ func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFlo
var topInset: CGFloat = 0.0
var bottomInset: CGFloat = 0.0
var additionalItems: [InstantPageItem] = []
- if let webpage = webpage {
- let offset = requiresScroll ? CGPoint() : offset
- for line in textItem.lines {
- let lineFrame = frameForLine(line, boundingWidth: boundingWidth, alignment: alignment)
+ let effectiveOffset = requiresScroll ? CGPoint() : offset
+ for line in textItem.lines {
+ let lineFrame = frameForLine(line, boundingWidth: boundingWidth, alignment: alignment)
+ if let webpage = webpage {
for imageItem in line.imageItems {
if let media = media[imageItem.id] {
- let item = InstantPageImageItem(frame: imageItem.frame.offsetBy(dx: lineFrame.minX + offset.x, dy: offset.y), webPage: webpage, media: InstantPageMedia(index: -1, media: media, url: nil, caption: nil, credit: nil), interactive: false, roundCorners: false, fit: false)
+ let item = InstantPageImageItem(frame: imageItem.frame.offsetBy(dx: lineFrame.minX + effectiveOffset.x, dy: effectiveOffset.y), webPage: webpage, media: InstantPageMedia(index: -1, media: media, url: nil, caption: nil, credit: nil), interactive: false, roundCorners: false, fit: false)
additionalItems.append(item)
if item.frame.minY < topInset {
@@ -888,6 +1035,17 @@ func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFlo
}
}
}
+ for formulaItem in line.formulaItems {
+ let item = InstantPageTextFormulaItem(frame: formulaItem.frame.offsetBy(dx: lineFrame.minX + effectiveOffset.x, dy: effectiveOffset.y), attachment: formulaItem.attachment)
+ additionalItems.append(item)
+
+ if item.frame.minY < topInset {
+ topInset = item.frame.minY
+ }
+ if item.frame.maxY > height {
+ bottomInset = max(bottomInset, item.frame.maxY - height)
+ }
+ }
}
if requiresScroll {
diff --git a/submodules/InstantPageUI/Sources/InstantPageTextStyleStack.swift b/submodules/InstantPageUI/Sources/InstantPageTextStyleStack.swift
index 413ff55840..2ab0a10df8 100644
--- a/submodules/InstantPageUI/Sources/InstantPageTextStyleStack.swift
+++ b/submodules/InstantPageUI/Sources/InstantPageTextStyleStack.swift
@@ -28,6 +28,7 @@ let InstantPageMarkerColorAttribute = "MarkerColorAttribute"
let InstantPageMediaIdAttribute = "MediaIdAttribute"
let InstantPageMediaDimensionsAttribute = "MediaDimensionsAttribute"
let InstantPageAnchorAttribute = "AnchorAttribute"
+let InstantPageFormulaAttribute = "FormulaAttribute"
final class InstantPageTextStyleStack {
private var items: [InstantPageTextStyle] = []
diff --git a/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift b/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift
index b4861ca025..8cbb4e2389 100644
--- a/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift
+++ b/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift
@@ -355,7 +355,7 @@ private enum InviteLinksEditEntry: ItemListNodeEntry {
case let .timeHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .timePicker(_, value, enabled):
- return ItemListInviteLinkTimeLimitItem(theme: presentationData.theme, strings: presentationData.strings, value: value, enabled: enabled, sectionId: self.section, updated: { value in
+ return ItemListInviteLinkTimeLimitItem(theme: presentationData.theme, systemStyle: .glass, strings: presentationData.strings, value: value, enabled: enabled, sectionId: self.section, updated: { value in
arguments.updateState({ state in
var updatedState = state
if value != updatedState.time {
@@ -418,7 +418,7 @@ private enum InviteLinksEditEntry: ItemListNodeEntry {
case let .usageHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .usagePicker(_, dateTimeFormat, value, enabled):
- return ItemListInviteLinkUsageLimitItem(theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: dateTimeFormat, value: value, enabled: enabled, sectionId: self.section, updated: { value in
+ return ItemListInviteLinkUsageLimitItem(theme: presentationData.theme, systemStyle: .glass, strings: presentationData.strings, dateTimeFormat: dateTimeFormat, value: value, enabled: enabled, sectionId: self.section, updated: { value in
arguments.dismissInput()
arguments.updateState({ state in
var updatedState = state
diff --git a/submodules/InviteLinksUI/Sources/ItemListInviteLinkDateLimitItem.swift b/submodules/InviteLinksUI/Sources/ItemListInviteLinkDateLimitItem.swift
index 3555521e4e..fe0dd5935e 100644
--- a/submodules/InviteLinksUI/Sources/ItemListInviteLinkDateLimitItem.swift
+++ b/submodules/InviteLinksUI/Sources/ItemListInviteLinkDateLimitItem.swift
@@ -73,14 +73,16 @@ enum InviteLinkTimeLimit: Equatable {
final class ItemListInviteLinkTimeLimitItem: ListViewItem, ItemListItem {
let theme: PresentationTheme
+ let systemStyle: ItemListSystemStyle
let strings: PresentationStrings
let value: InviteLinkTimeLimit
let enabled: Bool
let sectionId: ItemListSectionId
let updated: (InviteLinkTimeLimit) -> Void
- init(theme: PresentationTheme, strings: PresentationStrings, value: InviteLinkTimeLimit, enabled: Bool, sectionId: ItemListSectionId, updated: @escaping (InviteLinkTimeLimit) -> Void) {
+ init(theme: PresentationTheme, systemStyle: ItemListSystemStyle, strings: PresentationStrings, value: InviteLinkTimeLimit, enabled: Bool, sectionId: ItemListSectionId, updated: @escaping (InviteLinkTimeLimit) -> Void) {
self.theme = theme
+ self.systemStyle = systemStyle
self.strings = strings
self.value = value
self.enabled = enabled
@@ -326,7 +328,7 @@ private final class ItemListInviteLinkTimeLimitItemNode: ListViewItemNode {
strongSelf.bottomStripeNode.isHidden = hasCorners
}
- strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
+ strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
diff --git a/submodules/InviteLinksUI/Sources/ItemListInviteLinkUsageLimitItem.swift b/submodules/InviteLinksUI/Sources/ItemListInviteLinkUsageLimitItem.swift
index 3bc5994529..8b297264d9 100644
--- a/submodules/InviteLinksUI/Sources/ItemListInviteLinkUsageLimitItem.swift
+++ b/submodules/InviteLinksUI/Sources/ItemListInviteLinkUsageLimitItem.swift
@@ -87,6 +87,7 @@ enum InviteLinkUsageLimit: Equatable {
final class ItemListInviteLinkUsageLimitItem: ListViewItem, ItemListItem {
let theme: PresentationTheme
+ let systemStyle: ItemListSystemStyle
let strings: PresentationStrings
let dateTimeFormat: PresentationDateTimeFormat
let value: InviteLinkUsageLimit
@@ -94,8 +95,9 @@ final class ItemListInviteLinkUsageLimitItem: ListViewItem, ItemListItem {
let sectionId: ItemListSectionId
let updated: (InviteLinkUsageLimit) -> Void
- init(theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, value: InviteLinkUsageLimit, enabled: Bool, sectionId: ItemListSectionId, updated: @escaping (InviteLinkUsageLimit) -> Void) {
+ init(theme: PresentationTheme, systemStyle: ItemListSystemStyle, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, value: InviteLinkUsageLimit, enabled: Bool, sectionId: ItemListSectionId, updated: @escaping (InviteLinkUsageLimit) -> Void) {
self.theme = theme
+ self.systemStyle = systemStyle
self.strings = strings
self.dateTimeFormat = dateTimeFormat
self.value = value
@@ -336,7 +338,7 @@ private final class ItemListInviteLinkUsageLimitItemNode: ListViewItemNode {
strongSelf.bottomStripeNode.isHidden = hasCorners
}
- strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
+ strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
diff --git a/submodules/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoMessageShadow@2x.png b/submodules/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoMessageShadow@2x.png
deleted file mode 100644
index 7f9680c2be..0000000000
Binary files a/submodules/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoMessageShadow@2x.png and /dev/null differ
diff --git a/submodules/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoMessageShadow@3x.png b/submodules/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoMessageShadow@3x.png
deleted file mode 100644
index de8d7887ac..0000000000
Binary files a/submodules/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoMessageShadow@3x.png and /dev/null differ
diff --git a/submodules/LegacyComponents/Resources/LegacyComponentsResources.bundle/ocr_nn.bin b/submodules/LegacyComponents/Resources/LegacyComponentsResources.bundle/ocr_nn.bin
deleted file mode 100755
index ccab89e554..0000000000
Binary files a/submodules/LegacyComponents/Resources/LegacyComponentsResources.bundle/ocr_nn.bin and /dev/null differ
diff --git a/submodules/LegacyComponents/Sources/TGFont.mm b/submodules/LegacyComponents/Sources/TGFont.mm
index a5d261521f..311787d42e 100644
--- a/submodules/LegacyComponents/Sources/TGFont.mm
+++ b/submodules/LegacyComponents/Sources/TGFont.mm
@@ -116,13 +116,9 @@ UIFont *TGFixedSystemFontOfSize(CGFloat size)
+ (UIFont *)roundedFontOfSize:(CGFloat)size
{
- if (@available(iOSApplicationExtension 13.0, iOS 13.0, *)) {
- UIFontDescriptor *descriptor = [UIFont boldSystemFontOfSize:size].fontDescriptor;
- descriptor = [descriptor fontDescriptorWithDesign:UIFontDescriptorSystemDesignRounded];
- return [UIFont fontWithDescriptor:descriptor size:size];
- } else {
- return [UIFont fontWithName:@".SFCompactRounded-Semibold" size:size];
- }
+ UIFontDescriptor *descriptor = [UIFont boldSystemFontOfSize:size].fontDescriptor;
+ descriptor = [descriptor fontDescriptorWithDesign:UIFontDescriptorSystemDesignRounded];
+ return [UIFont fontWithDescriptor:descriptor size:size];
}
@end
diff --git a/submodules/LegacyComponents/Sources/TGVideoMessageCaptureController.m b/submodules/LegacyComponents/Sources/TGVideoMessageCaptureController.m
index cba0bf530e..4e0f81e1bc 100644
--- a/submodules/LegacyComponents/Sources/TGVideoMessageCaptureController.m
+++ b/submodules/LegacyComponents/Sources/TGVideoMessageCaptureController.m
@@ -330,11 +330,7 @@ typedef enum
_circleWrapperView.alpha = 0.0f;
_circleWrapperView.clipsToBounds = false;
[_wrapperView addSubview:_circleWrapperView];
-
- _shadowView = [[UIImageView alloc] initWithImage:TGComponentsImageNamed(@"VideoMessageShadow")];
- _shadowView.frame = _circleWrapperView.bounds;
- [_circleWrapperView addSubview:_shadowView];
-
+
_circleView = [[UIView alloc] initWithFrame:CGRectInset(_circleWrapperView.bounds, shadowSize, shadowSize)];
_circleView.clipsToBounds = true;
_circleView.layer.cornerRadius = _circleView.frame.size.width / 2.0f;
diff --git a/submodules/LegacyComponents/Sources/ocr.mm b/submodules/LegacyComponents/Sources/ocr.mm
index b81744efe8..6ba13ae008 100755
--- a/submodules/LegacyComponents/Sources/ocr.mm
+++ b/submodules/LegacyComponents/Sources/ocr.mm
@@ -677,6 +677,9 @@ UIImage *normalizeImage(UIImage *image)
NSString *recognizeMRZ(UIImage *input, CGRect *outBoundingRect)
{
+ if (@"".length == 0) {
+ return nil;
+ }
input = normalizeImage(input);
UIImage *binaryImage;
diff --git a/submodules/SettingsUI/Sources/Privacy and Security/GlobalAutoremoveScreen.swift b/submodules/SettingsUI/Sources/Privacy and Security/GlobalAutoremoveScreen.swift
index 7ccb1d9560..66aaae7efe 100644
--- a/submodules/SettingsUI/Sources/Privacy and Security/GlobalAutoremoveScreen.swift
+++ b/submodules/SettingsUI/Sources/Privacy and Security/GlobalAutoremoveScreen.swift
@@ -303,7 +303,7 @@ public func globalAutoremoveScreen(context: AccountContext, initialValue: Int32,
},
openCustomValue: {
let currentValue = stateValue.with({ $0 }).updatedValue
- let controller = ChatTimerScreen(context: context, updatedPresentationData: nil, style: .default, mode: .autoremove, currentTime: currentValue == 0 ? nil : currentValue, dismissByTapOutside: true, completion: { value in
+ let controller = ChatTimerScreen(context: context, updatedPresentationData: nil, style: .default, mode: .autoremove, currentTime: currentValue == 0 ? nil : currentValue, completion: { value in
updateValue(value)
})
presentControllerImpl?(controller, nil)
diff --git a/submodules/TelegramCore/FlatSerialization/Models/InstantPageBlock.fbs b/submodules/TelegramCore/FlatSerialization/Models/InstantPageBlock.fbs
index ec3372afa1..091afbd2e0 100644
--- a/submodules/TelegramCore/FlatSerialization/Models/InstantPageBlock.fbs
+++ b/submodules/TelegramCore/FlatSerialization/Models/InstantPageBlock.fbs
@@ -35,7 +35,8 @@ union InstantPageBlock_Value {
InstantPageBlock_Details,
InstantPageBlock_RelatedArticles,
InstantPageBlock_Map,
- InstantPageBlock_Heading
+ InstantPageBlock_Heading,
+ InstantPageBlock_Formula
}
table InstantPageBlock {
@@ -191,6 +192,10 @@ table InstantPageBlock_Heading {
level:int32 (id: 1);
}
+table InstantPageBlock_Formula {
+ latex:string (id: 0, required);
+}
+
table InstantPageCaption {
text:RichText (id: 0, required);
credit:RichText (id: 1, required);
diff --git a/submodules/TelegramCore/FlatSerialization/Models/RichText.fbs b/submodules/TelegramCore/FlatSerialization/Models/RichText.fbs
index 7a1da32aff..564cd98ce2 100644
--- a/submodules/TelegramCore/FlatSerialization/Models/RichText.fbs
+++ b/submodules/TelegramCore/FlatSerialization/Models/RichText.fbs
@@ -19,7 +19,8 @@ union RichText_Value {
RichText_Marked,
RichText_Phone,
RichText_Image,
- RichText_Anchor
+ RichText_Anchor,
+ RichText_Formula
}
table RichText {
@@ -92,4 +93,8 @@ table RichText_Image {
table RichText_Anchor {
text:RichText (id: 0, required);
name:string (id: 1, required);
-}
\ No newline at end of file
+}
+
+table RichText_Formula {
+ latex:string (id: 0, required);
+}
diff --git a/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift b/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift
index 00176110eb..9d7be75ab7 100644
--- a/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift
+++ b/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift
@@ -988,35 +988,24 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId,
forwardInfo = StoreMessageForwardInfo(authorId: sourceForwardInfo.author?.id, sourceId: sourceForwardInfo.source?.id, sourceMessageId: sourceForwardInfo.sourceMessageId, date: sourceForwardInfo.date, authorSignature: sourceForwardInfo.authorSignature, psaType: nil, flags: [])
} else {
if sourceMessage.id.peerId != account.peerId {
- var hasHiddenForwardMedia = false
- for media in sourceMessage.media {
- if let file = media as? TelegramMediaFile {
- if file.isMusic {
- hasHiddenForwardMedia = true
- }
+ var sourceId: PeerId? = nil
+ var sourceMessageId: MessageId? = nil
+ if case let .channel(peer) = messageMainPeer(EngineMessage(sourceMessage)), case .broadcast = peer.info {
+ sourceId = peer.id
+ sourceMessageId = sourceMessage.id
+ }
+
+ var authorSignature: String?
+ for attribute in sourceMessage.attributes {
+ if let attribute = attribute as? AuthorSignatureMessageAttribute {
+ authorSignature = attribute.signature
+ break
}
}
-
- if !hasHiddenForwardMedia {
- var sourceId: PeerId? = nil
- var sourceMessageId: MessageId? = nil
- if case let .channel(peer) = messageMainPeer(EngineMessage(sourceMessage)), case .broadcast = peer.info {
- sourceId = peer.id
- sourceMessageId = sourceMessage.id
- }
-
- var authorSignature: String?
- for attribute in sourceMessage.attributes {
- if let attribute = attribute as? AuthorSignatureMessageAttribute {
- authorSignature = attribute.signature
- break
- }
- }
-
- let psaType: String? = nil
-
- forwardInfo = StoreMessageForwardInfo(authorId: author.id, sourceId: sourceId, sourceMessageId: sourceMessageId, date: sourceMessage.timestamp, authorSignature: authorSignature, psaType: psaType, flags: [])
- }
+
+ let psaType: String? = nil
+
+ forwardInfo = StoreMessageForwardInfo(authorId: author.id, sourceId: sourceId, sourceMessageId: sourceMessageId, date: sourceMessage.timestamp, authorSignature: authorSignature, psaType: psaType, flags: [])
} else {
forwardInfo = nil
}
diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_InstantPage.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_InstantPage.swift
index 83072ffdf0..0e0574d48b 100644
--- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_InstantPage.swift
+++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_InstantPage.swift
@@ -33,6 +33,7 @@ private enum InstantPageBlockType: Int32 {
case relatedArticles = 26
case map = 27
case heading = 28
+ case formula = 29
}
private func decodeListItems(_ decoder: PostboxDecoder) -> [InstantPageListItem] {
@@ -62,6 +63,7 @@ public indirect enum InstantPageBlock: PostboxCoding, Equatable {
case header(RichText)
case subheader(RichText)
case heading(text: RichText, level: Int32)
+ case formula(latex: String)
case paragraph(RichText)
case preformatted(text: RichText, language: String?)
case footer(RichText)
@@ -101,6 +103,8 @@ public indirect enum InstantPageBlock: PostboxCoding, Equatable {
self = .subheader(decoder.decodeObjectForKey("t", decoder: { RichText(decoder: $0) }) as! RichText)
case InstantPageBlockType.heading.rawValue:
self = .heading(text: decoder.decodeObjectForKey("t", decoder: { RichText(decoder: $0) }) as! RichText, level: decoder.decodeInt32ForKey("l", orElse: 3))
+ case InstantPageBlockType.formula.rawValue:
+ self = .formula(latex: decoder.decodeStringForKey("l", orElse: ""))
case InstantPageBlockType.paragraph.rawValue:
self = .paragraph(decoder.decodeObjectForKey("t", decoder: { RichText(decoder: $0) }) as! RichText)
case InstantPageBlockType.preformatted.rawValue:
@@ -195,6 +199,9 @@ public indirect enum InstantPageBlock: PostboxCoding, Equatable {
encoder.encodeInt32(InstantPageBlockType.heading.rawValue, forKey: "r")
encoder.encodeObject(text, forKey: "t")
encoder.encodeInt32(level, forKey: "l")
+ case let .formula(latex):
+ encoder.encodeInt32(InstantPageBlockType.formula.rawValue, forKey: "r")
+ encoder.encodeString(latex, forKey: "l")
case let .paragraph(text):
encoder.encodeInt32(InstantPageBlockType.paragraph.rawValue, forKey: "r")
encoder.encodeObject(text, forKey: "t")
@@ -396,6 +403,12 @@ public indirect enum InstantPageBlock: PostboxCoding, Equatable {
} else {
return false
}
+ case let .formula(lhsLatex):
+ if case let .formula(rhsLatex) = rhs, lhsLatex == rhsLatex {
+ return true
+ } else {
+ return false
+ }
case let .paragraph(text):
if case .paragraph(text) = rhs {
return true
@@ -572,6 +585,11 @@ public indirect enum InstantPageBlock: PostboxCoding, Equatable {
throw FlatBuffersError.missingRequiredField()
}
self = .heading(text: try RichText(flatBuffersObject: value.text), level: value.level)
+ case .instantpageblockFormula:
+ guard let value = flatBuffersObject.value(type: TelegramCore_InstantPageBlock_Formula.self) else {
+ throw FlatBuffersError.missingRequiredField()
+ }
+ self = .formula(latex: value.latex)
case .instantpageblockParagraph:
guard let value = flatBuffersObject.value(type: TelegramCore_InstantPageBlock_Paragraph.self) else {
throw FlatBuffersError.missingRequiredField()
@@ -732,6 +750,12 @@ public indirect enum InstantPageBlock: PostboxCoding, Equatable {
TelegramCore_InstantPageBlock_Heading.add(text: textOffset, &builder)
TelegramCore_InstantPageBlock_Heading.add(level: level, &builder)
offset = TelegramCore_InstantPageBlock_Heading.endInstantPageBlock_Heading(&builder, start: start)
+ case let .formula(latex):
+ valueType = .instantpageblockFormula
+ let latexOffset = builder.create(string: latex)
+ let start = TelegramCore_InstantPageBlock_Formula.startInstantPageBlock_Formula(&builder)
+ TelegramCore_InstantPageBlock_Formula.add(latex: latexOffset, &builder)
+ offset = TelegramCore_InstantPageBlock_Formula.endInstantPageBlock_Formula(&builder, start: start)
case let .paragraph(text):
valueType = .instantpageblockParagraph
let textOffset = text.encodeToFlatBuffers(builder: &builder)
diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_RichText.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_RichText.swift
index 69311fda00..cce0025996 100644
--- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_RichText.swift
+++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_RichText.swift
@@ -19,6 +19,7 @@ private enum RichTextTypes: Int32 {
case phone = 13
case image = 14
case anchor = 15
+ case formula = 16
}
public indirect enum RichText: PostboxCoding, Equatable {
@@ -38,6 +39,7 @@ public indirect enum RichText: PostboxCoding, Equatable {
case phone(text: RichText, phone: String)
case image(id: MediaId, dimensions: PixelDimensions)
case anchor(text: RichText, name: String)
+ case formula(latex: String)
public init(decoder: PostboxDecoder) {
switch decoder.decodeInt32ForKey("r", orElse: 0) {
@@ -79,6 +81,8 @@ public indirect enum RichText: PostboxCoding, Equatable {
self = .image(id: MediaId(namespace: decoder.decodeInt32ForKey("i.n", orElse: 0), id: decoder.decodeInt64ForKey("i.i", orElse: 0)), dimensions: PixelDimensions(width: decoder.decodeInt32ForKey("sw", orElse: 0), height: decoder.decodeInt32ForKey("sh", orElse: 0)))
case RichTextTypes.anchor.rawValue:
self = .anchor(text: decoder.decodeObjectForKey("t", decoder: { RichText(decoder: $0) }) as! RichText, name: decoder.decodeStringForKey("n", orElse: ""))
+ case RichTextTypes.formula.rawValue:
+ self = .formula(latex: decoder.decodeStringForKey("l", orElse: ""))
default:
self = .empty
}
@@ -147,6 +151,9 @@ public indirect enum RichText: PostboxCoding, Equatable {
encoder.encodeInt32(RichTextTypes.anchor.rawValue, forKey: "r")
encoder.encodeObject(text, forKey: "t")
encoder.encodeString(name, forKey: "n")
+ case let .formula(latex):
+ encoder.encodeInt32(RichTextTypes.formula.rawValue, forKey: "r")
+ encoder.encodeString(latex, forKey: "l")
}
}
@@ -248,6 +255,12 @@ public indirect enum RichText: PostboxCoding, Equatable {
} else {
return false
}
+ case let .formula(lhsLatex):
+ if case let .formula(rhsLatex) = rhs, lhsLatex == rhsLatex {
+ return true
+ } else {
+ return false
+ }
}
}
}
@@ -291,6 +304,8 @@ public extension RichText {
return ""
case let .anchor(text, _):
return text.plainText
+ case let .formula(latex):
+ return latex
}
}
}
@@ -378,6 +393,11 @@ extension RichText {
}
self = .anchor(text: try RichText(flatBuffersObject: value.text),
name: value.name)
+ case .richtextFormula:
+ guard let value = flatBuffersObject.value(type: TelegramCore_RichText_Formula.self) else {
+ throw FlatBuffersError.missingRequiredField()
+ }
+ self = .formula(latex: value.latex)
case .none_:
self = .empty
}
@@ -494,6 +514,12 @@ extension RichText {
TelegramCore_RichText_Anchor.add(text: textOffset, &builder)
TelegramCore_RichText_Anchor.add(name: nameOffset, &builder)
offset = TelegramCore_RichText_Anchor.endRichText_Anchor(&builder, start: start)
+ case let .formula(latex):
+ valueType = .richtextFormula
+ let latexOffset = builder.create(string: latex)
+ let start = TelegramCore_RichText_Formula.startRichText_Formula(&builder)
+ TelegramCore_RichText_Formula.add(latex: latexOffset, &builder)
+ offset = TelegramCore_RichText_Formula.endRichText_Formula(&builder, start: start)
}
return TelegramCore_RichText.createRichText(&builder, valueType: valueType, valueOffset: offset)
diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/ForwardGame.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ForwardGame.swift
index 95e6913650..ef127aa336 100644
--- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/ForwardGame.swift
+++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ForwardGame.swift
@@ -3,6 +3,38 @@ import Postbox
import TelegramApi
import SwiftSignalKit
+public enum SendBotGameError {
+ case generic
+}
+
+func _internal_sendBotGame(account: Account, botPeerId: PeerId, game: String, to peerId: PeerId, threadId: Int64?) -> Signal {
+ return account.postbox.transaction { transaction -> Signal in
+ guard !game.isEmpty, let botPeer = transaction.getPeer(botPeerId), let inputBot = apiInputUser(botPeer), let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) else {
+ return .fail(.generic)
+ }
+
+ var flags: Int32 = 1 << 7
+ var replyTo: Api.InputReplyTo?
+ if let threadId {
+ flags |= 1 << 0
+ let threadMessageId = Int32(clamping: threadId)
+ let replyFlags: Int32 = 1 << 0
+ replyTo = .inputReplyToMessage(.init(flags: replyFlags, replyToMsgId: threadMessageId, topMsgId: threadMessageId, replyToPeerId: nil, quoteText: nil, quoteEntities: nil, quoteOffset: nil, monoforumPeerId: nil, todoItemId: nil, pollOption: nil))
+ }
+
+ return account.network.request(Api.functions.messages.sendMedia(flags: flags, peer: inputPeer, replyTo: replyTo, media: .inputMediaGame(.init(id: .inputGameShortName(.init(botId: inputBot, shortName: game)))), message: "", randomId: Int64.random(in: Int64.min ... Int64.max), replyMarkup: nil, entities: nil, scheduleDate: nil, scheduleRepeatPeriod: nil, sendAs: nil, quickReplyShortcut: nil, effect: nil, allowPaidStars: nil, suggestedPost: nil))
+ |> mapError { _ -> SendBotGameError in
+ return .generic
+ }
+ |> mapToSignal { updates -> Signal in
+ account.stateManager.addUpdates(updates)
+ return .complete()
+ }
+ }
+ |> castError(SendBotGameError.self)
+ |> switchToLatest
+}
+
func _internal_forwardGameWithScore(account: Account, messageId: MessageId, to peerId: PeerId, threadId: Int64?, as sendAsPeerId: PeerId?) -> Signal {
return account.postbox.transaction { transaction -> Signal in
if let _ = transaction.getMessage(messageId), let fromPeer = transaction.getPeer(messageId.peerId), let fromInputPeer = apiInputPeer(fromPeer), let toPeer = transaction.getPeer(peerId), let toInputPeer = apiInputPeer(toPeer) {
diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift
index f79cd4ce72..81d51ae255 100644
--- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift
+++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift
@@ -219,6 +219,10 @@ public extension TelegramEngine {
return _internal_forwardGameWithScore(account: self.account, messageId: messageId, to: peerId, threadId: threadId, as: senderPeerId)
}
+ public func sendBotGame(botPeerId: PeerId, game: String, to peerId: PeerId, threadId: Int64?) -> Signal {
+ return _internal_sendBotGame(account: self.account, botPeerId: botPeerId, game: game, to: peerId, threadId: threadId)
+ }
+
public func requestUpdatePinnedMessage(peerId: PeerId, update: PinnedMessageUpdate) -> Signal {
return _internal_requestUpdatePinnedMessage(account: self.account, peerId: peerId, update: update)
}
diff --git a/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift b/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift
index fad404b7e4..b8bfed14be 100644
--- a/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift
+++ b/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift
@@ -379,9 +379,7 @@ private final class AdminUserActionsContentComponent: Component {
func update(component: AdminUserActionsContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize {
let environment = environment[ViewControllerComponentContainer.Environment.self].value
let sideInset: CGFloat = 16.0
- var contentHeight: CGFloat = 76.0
-
- contentHeight += 15.0
+ var contentHeight: CGFloat = 76.0 + 15.0
let availableOptions = availableAdminUserActionOptionSections(
accountPeerId: component.context.account.peerId,
diff --git a/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/RecentActionsSettingsSheet.swift b/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/RecentActionsSettingsSheet.swift
index 5edc3135aa..a84335e979 100644
--- a/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/RecentActionsSettingsSheet.swift
+++ b/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/RecentActionsSettingsSheet.swift
@@ -1,23 +1,20 @@
import Foundation
import UIKit
import Display
-import AsyncDisplayKit
import ComponentFlow
import SwiftSignalKit
import ViewControllerComponent
import ComponentDisplayAdapters
+import ResizableSheetComponent
import TelegramPresentationData
import AccountContext
import TelegramCore
import MultilineTextComponent
import ButtonComponent
import PresentationDataUtils
-import Markdown
-import UndoUI
import TelegramStringFormatting
import ListSectionComponent
import ListActionItemComponent
-import PlainButtonComponent
import GlassBarButtonComponent
import BundleIconComponent
@@ -163,15 +160,351 @@ private enum ActionType: Hashable {
}
}
-private final class RecentActionsSettingsSheetComponent: Component {
+private struct RecentActionsSettingsSheetState: Equatable {
+ var expandedSections: Set
+ var selectedMembersActions: Set
+ var selectedSettingsActions: Set
+ var selectedMessagesActions: Set
+ var selectedAdmins: Set
+}
+
+private final class RecentActionsSettingsContentComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
-
+
+ let context: AccountContext
+ let peer: EnginePeer
+ let adminPeers: [EnginePeer]
+ let theme: PresentationTheme
+ let sheetState: RecentActionsSettingsSheetState
+ let toggleActionTypeSectionSelection: (ActionTypeSection) -> Void
+ let toggleActionTypeSectionExpansion: (ActionTypeSection) -> Void
+ let toggleActionType: (ActionType) -> Void
+ let toggleAdmin: (EnginePeer) -> Void
+ let toggleAllAdmins: () -> Void
+
+ init(
+ context: AccountContext,
+ peer: EnginePeer,
+ adminPeers: [EnginePeer],
+ theme: PresentationTheme,
+ sheetState: RecentActionsSettingsSheetState,
+ toggleActionTypeSectionSelection: @escaping (ActionTypeSection) -> Void,
+ toggleActionTypeSectionExpansion: @escaping (ActionTypeSection) -> Void,
+ toggleActionType: @escaping (ActionType) -> Void,
+ toggleAdmin: @escaping (EnginePeer) -> Void,
+ toggleAllAdmins: @escaping () -> Void
+ ) {
+ self.context = context
+ self.peer = peer
+ self.adminPeers = adminPeers
+ self.theme = theme
+ self.sheetState = sheetState
+ self.toggleActionTypeSectionSelection = toggleActionTypeSectionSelection
+ self.toggleActionTypeSectionExpansion = toggleActionTypeSectionExpansion
+ self.toggleActionType = toggleActionType
+ self.toggleAdmin = toggleAdmin
+ self.toggleAllAdmins = toggleAllAdmins
+ }
+
+ static func ==(lhs: RecentActionsSettingsContentComponent, rhs: RecentActionsSettingsContentComponent) -> Bool {
+ if lhs.context !== rhs.context {
+ return false
+ }
+ if lhs.peer != rhs.peer {
+ return false
+ }
+ if lhs.adminPeers != rhs.adminPeers {
+ return false
+ }
+ if lhs.theme !== rhs.theme {
+ return false
+ }
+ if lhs.sheetState != rhs.sheetState {
+ return false
+ }
+ return true
+ }
+
+ final class View: UIView {
+ private let optionsSection = ComponentView()
+ private let adminsSection = ComponentView()
+
+ override init(frame: CGRect) {
+ super.init(frame: frame)
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ func update(component: RecentActionsSettingsContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize {
+ let environment = environment[ViewControllerComponentContainer.Environment.self].value
+ let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 })
+ let theme = component.theme
+ let sheetState = component.sheetState
+ let sideInset: CGFloat = 16.0
+
+ var isGroup = true
+ if case let .channel(channel) = component.peer, case .broadcast = channel.info {
+ isGroup = false
+ }
+
+ var contentHeight: CGFloat = 76.0 + 15.0
+
+ let actionTypeSectionItem: (ActionTypeSection) -> AnyComponentWithIdentity = { actionTypeSection in
+ let totalCount: Int
+ let selectedCount: Int
+ let title: String
+ let isExpanded = sheetState.expandedSections.contains(actionTypeSection)
+
+ switch actionTypeSection {
+ case .members:
+ totalCount = MembersActionType.allCases.count
+ selectedCount = sheetState.selectedMembersActions.count
+ title = isGroup ? environment.strings.Channel_AdminLogFilter_Section_MembersGroup : environment.strings.Channel_AdminLogFilter_Section_MembersChannel
+ case .settings:
+ totalCount = SettingsActionType.allCases.count
+ selectedCount = sheetState.selectedSettingsActions.count
+ title = isGroup ? environment.strings.Channel_AdminLogFilter_Section_SettingsGroup : environment.strings.Channel_AdminLogFilter_Section_SettingsChannel
+ case .messages:
+ totalCount = MessagesActionType.allCases.count
+ selectedCount = sheetState.selectedMessagesActions.count
+ title = environment.strings.Channel_AdminLogFilter_Section_Messages
+ }
+
+ let itemTitle: AnyComponent = AnyComponent(HStack([
+ AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent(
+ text: .plain(NSAttributedString(
+ string: title,
+ font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
+ textColor: theme.list.itemPrimaryTextColor
+ )),
+ maximumNumberOfLines: 1
+ ))),
+ AnyComponentWithIdentity(id: 1, component: AnyComponent(MediaSectionExpandIndicatorComponent(
+ theme: theme,
+ title: "\(selectedCount)/\(totalCount)",
+ isExpanded: isExpanded
+ )))
+ ], spacing: 7.0))
+
+ return AnyComponentWithIdentity(id: actionTypeSection, component: AnyComponent(ListActionItemComponent(
+ theme: theme,
+ style: .glass,
+ title: itemTitle,
+ leftIcon: .check(ListActionItemComponent.LeftIcon.Check(
+ isSelected: selectedCount == totalCount,
+ toggle: {
+ component.toggleActionTypeSectionSelection(actionTypeSection)
+ }
+ )),
+ icon: .none,
+ accessory: nil,
+ action: { _ in
+ component.toggleActionTypeSectionExpansion(actionTypeSection)
+ },
+ highlighting: .disabled
+ )))
+ }
+
+ let expandedActionTypeSectionItem: (ActionTypeSection) -> AnyComponentWithIdentity = { actionTypeSection in
+ let sectionId: AnyHashable
+ let selectedActionTypes: Set
+ let actionTypes: [ActionType]
+ switch actionTypeSection {
+ case .members:
+ sectionId = "members-sub"
+ actionTypes = MembersActionType.allCases.map(ActionType.members)
+ selectedActionTypes = Set(sheetState.selectedMembersActions.map(ActionType.members))
+ case .settings:
+ sectionId = "settings-sub"
+ actionTypes = SettingsActionType.allCases.map(ActionType.settings)
+ selectedActionTypes = Set(sheetState.selectedSettingsActions.map(ActionType.settings))
+ case .messages:
+ sectionId = "messages-sub"
+ actionTypes = MessagesActionType.allCases.map(ActionType.messages)
+ selectedActionTypes = Set(sheetState.selectedMessagesActions.map(ActionType.messages))
+ }
+
+ var subItems: [AnyComponentWithIdentity] = []
+ for actionType in actionTypes {
+ let actionItemTitle: String = actionType.title(isGroup: isGroup, strings: environment.strings)
+
+ subItems.append(AnyComponentWithIdentity(id: actionType, component: AnyComponent(ListActionItemComponent(
+ theme: theme,
+ style: .glass,
+ title: AnyComponent(VStack([
+ AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
+ text: .plain(NSAttributedString(
+ string: actionItemTitle,
+ font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
+ textColor: theme.list.itemPrimaryTextColor
+ )),
+ maximumNumberOfLines: 1
+ ))),
+ ], alignment: .left, spacing: 2.0)),
+ leftIcon: .check(ListActionItemComponent.LeftIcon.Check(
+ isSelected: selectedActionTypes.contains(actionType),
+ toggle: {
+ component.toggleActionType(actionType)
+ }
+ )),
+ icon: .none,
+ accessory: .none,
+ action: { _ in
+ component.toggleActionType(actionType)
+ },
+ highlighting: .disabled
+ ))))
+ }
+
+ return AnyComponentWithIdentity(id: sectionId, component: AnyComponent(ListSubSectionComponent(
+ theme: theme,
+ leftInset: 62.0,
+ items: subItems
+ )))
+ }
+
+ var optionsSectionItems: [AnyComponentWithIdentity] = []
+ for actionTypeSection in ActionTypeSection.allCases {
+ optionsSectionItems.append(actionTypeSectionItem(actionTypeSection))
+ if sheetState.expandedSections.contains(actionTypeSection) {
+ optionsSectionItems.append(expandedActionTypeSectionItem(actionTypeSection))
+ }
+ }
+
+ self.optionsSection.parentState = state
+ let optionsSectionSize = self.optionsSection.update(
+ transition: transition,
+ component: AnyComponent(ListSectionComponent(
+ theme: theme,
+ style: .glass,
+ header: AnyComponent(MultilineTextComponent(
+ text: .plain(NSAttributedString(
+ string: environment.strings.Channel_AdminLogFilter_FilterActionsTypeTitle,
+ font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
+ textColor: theme.list.freeTextColor
+ )),
+ maximumNumberOfLines: 0
+ )),
+ footer: nil,
+ items: optionsSectionItems
+ )),
+ environment: {},
+ containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100000.0)
+ )
+ let optionsSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: optionsSectionSize)
+ if let optionsSectionView = self.optionsSection.view {
+ if optionsSectionView.superview == nil {
+ self.addSubview(optionsSectionView)
+ }
+ transition.setFrame(view: optionsSectionView, frame: optionsSectionFrame)
+ }
+ contentHeight += optionsSectionSize.height
+ contentHeight += 24.0
+
+ var peerItems: [AnyComponentWithIdentity] = []
+ for peer in component.adminPeers {
+ peerItems.append(AnyComponentWithIdentity(id: peer.id, component: AnyComponent(AdminUserActionsPeerComponent(
+ context: component.context,
+ theme: theme,
+ strings: environment.strings,
+ baseFontSize: presentationData.listsFontSize.baseDisplaySize,
+ sideInset: 0.0,
+ title: peer.displayTitle(strings: environment.strings, displayOrder: .firstLast),
+ peer: peer,
+ selectionState: .editing(isSelected: sheetState.selectedAdmins.contains(peer.id)),
+ action: { peer in
+ component.toggleAdmin(peer)
+ }
+ ))))
+ }
+
+ var adminsSectionItems: [AnyComponentWithIdentity] = []
+ adminsSectionItems.append(AnyComponentWithIdentity(id: adminsSectionItems.count, component: AnyComponent(ListActionItemComponent(
+ theme: theme,
+ style: .glass,
+ title: AnyComponent(VStack([
+ AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
+ text: .plain(NSAttributedString(
+ string: environment.strings.Channel_AdminLogFilter_ShowAllAdminsActions,
+ font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
+ textColor: theme.list.itemPrimaryTextColor
+ )),
+ maximumNumberOfLines: 1
+ ))),
+ ], alignment: .left, spacing: 2.0)),
+ leftIcon: .check(ListActionItemComponent.LeftIcon.Check(
+ isSelected: sheetState.selectedAdmins.count == component.adminPeers.count,
+ toggle: {
+ component.toggleAllAdmins()
+ }
+ )),
+ icon: .none,
+ accessory: .none,
+ action: { _ in
+ component.toggleAllAdmins()
+ },
+ highlighting: .disabled
+ ))))
+ adminsSectionItems.append(AnyComponentWithIdentity(id: adminsSectionItems.count, component: AnyComponent(ListSubSectionComponent(
+ theme: theme,
+ leftInset: 62.0,
+ items: peerItems
+ ))))
+
+ self.adminsSection.parentState = state
+ let adminsSectionSize = self.adminsSection.update(
+ transition: transition,
+ component: AnyComponent(ListSectionComponent(
+ theme: theme,
+ style: .glass,
+ header: AnyComponent(MultilineTextComponent(
+ text: .plain(NSAttributedString(
+ string: environment.strings.Channel_AdminLogFilter_FilterActionsAdminsTitle,
+ font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
+ textColor: theme.list.freeTextColor
+ )),
+ maximumNumberOfLines: 0
+ )),
+ footer: nil,
+ items: adminsSectionItems
+ )),
+ environment: {},
+ containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100000.0)
+ )
+ let adminsSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: adminsSectionSize)
+ if let adminsSectionView = self.adminsSection.view {
+ if adminsSectionView.superview == nil {
+ self.addSubview(adminsSectionView)
+ }
+ transition.setFrame(view: adminsSectionView, frame: adminsSectionFrame)
+ }
+ contentHeight += adminsSectionSize.height
+ contentHeight += 106.0
+
+ return CGSize(width: availableSize.width, height: contentHeight)
+ }
+ }
+
+ func makeView() -> View {
+ return View(frame: CGRect())
+ }
+
+ func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize {
+ return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
+ }
+}
+
+private final class RecentActionsSettingsResizableSheetComponent: Component {
+ typealias EnvironmentType = ViewControllerComponentContainer.Environment
+
let context: AccountContext
let peer: EnginePeer
let adminPeers: [EnginePeer]
let initialValue: RecentActionsSettingsSheet.Value
let completion: (RecentActionsSettingsSheet.Value) -> Void
-
+
init(
context: AccountContext,
peer: EnginePeer,
@@ -185,8 +518,8 @@ private final class RecentActionsSettingsSheetComponent: Component {
self.initialValue = initialValue
self.completion = completion
}
-
- static func ==(lhs: RecentActionsSettingsSheetComponent, rhs: RecentActionsSettingsSheetComponent) -> Bool {
+
+ static func ==(lhs: RecentActionsSettingsResizableSheetComponent, rhs: RecentActionsSettingsResizableSheetComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
@@ -198,158 +531,29 @@ private final class RecentActionsSettingsSheetComponent: Component {
}
return true
}
-
- private struct ItemLayout: Equatable {
- var containerSize: CGSize
- var containerInset: CGFloat
- var bottomInset: CGFloat
- var topInset: CGFloat
-
- init(containerSize: CGSize, containerInset: CGFloat, bottomInset: CGFloat, topInset: CGFloat) {
- self.containerSize = containerSize
- self.containerInset = containerInset
- self.bottomInset = bottomInset
- self.topInset = topInset
- }
- }
-
- private final class ScrollView: UIScrollView {
- override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
- return super.hitTest(point, with: event)
- }
- }
-
- final class View: UIView, UIScrollViewDelegate {
- private let dimView: UIView
- private let backgroundLayer: SimpleLayer
- private let navigationBarContainer: SparseContainerView
- private let navigationBackgroundView: BlurredBackgroundView
- private let navigationBarSeparator: SimpleLayer
- private let scrollView: ScrollView
- private let scrollContentClippingView: SparseContainerView
- private let scrollContentView: UIView
-
- private let leftButton = ComponentView()
-
- private let title = ComponentView()
- private let actionButton = ComponentView()
-
- private let optionsSection = ComponentView()
- private let adminsSection = ComponentView()
-
- private let bottomOverscrollLimit: CGFloat
-
- private var ignoreScrolling: Bool = false
-
- private var component: RecentActionsSettingsSheetComponent?
+
+ final class View: UIView {
+ private let sheet = ComponentView<(ViewControllerComponentContainer.Environment, ResizableSheetComponentEnvironment)>()
+ private let animateOut = ActionSlot>()
+
+ private var component: RecentActionsSettingsResizableSheetComponent?
private weak var state: EmptyComponentState?
- private var environment: ViewControllerComponentContainer.Environment?
- private var isUpdating: Bool = false
-
- private var itemLayout: ItemLayout?
-
- private var topOffsetDistance: CGFloat?
-
+ private var isDismissing: Bool = false
+
private var expandedSections = Set()
private var selectedMembersActions = Set()
private var selectedSettingsActions = Set()
private var selectedMessagesActions = Set()
private var selectedAdmins = Set()
-
+
override init(frame: CGRect) {
- self.bottomOverscrollLimit = 200.0
-
- self.dimView = UIView()
-
- self.backgroundLayer = SimpleLayer()
- self.backgroundLayer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
- self.backgroundLayer.cornerRadius = 10.0
-
- self.navigationBarContainer = SparseContainerView()
-
- self.navigationBackgroundView = BlurredBackgroundView(color: .clear, enableBlur: true)
- self.navigationBarSeparator = SimpleLayer()
-
- self.scrollView = ScrollView()
-
- self.scrollContentClippingView = SparseContainerView()
- self.scrollContentClippingView.clipsToBounds = true
-
- self.scrollContentView = UIView()
-
super.init(frame: frame)
-
- self.addSubview(self.dimView)
- self.layer.addSublayer(self.backgroundLayer)
-
- self.scrollView.delaysContentTouches = true
- self.scrollView.canCancelContentTouches = true
- self.scrollView.clipsToBounds = false
- if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
- self.scrollView.contentInsetAdjustmentBehavior = .never
- }
- if #available(iOS 13.0, *) {
- self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
- }
- self.scrollView.showsVerticalScrollIndicator = false
- self.scrollView.showsHorizontalScrollIndicator = false
- self.scrollView.alwaysBounceHorizontal = false
- self.scrollView.alwaysBounceVertical = true
- self.scrollView.scrollsToTop = false
- self.scrollView.delegate = self
- self.scrollView.clipsToBounds = true
-
- self.addSubview(self.scrollContentClippingView)
- self.scrollContentClippingView.addSubview(self.scrollView)
-
- self.scrollView.addSubview(self.scrollContentView)
-
- self.addSubview(self.navigationBarContainer)
-
- self.navigationBarContainer.addSubview(self.navigationBackgroundView)
- self.navigationBarContainer.layer.addSublayer(self.navigationBarSeparator)
-
- self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
}
-
+
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
-
- func scrollViewDidScroll(_ scrollView: UIScrollView) {
- if !self.ignoreScrolling {
- self.updateScrolling(transition: .immediate)
- }
- }
-
- func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) {
- }
-
- override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
- if !self.bounds.contains(point) {
- return nil
- }
- if !self.backgroundLayer.frame.contains(point) {
- return self.dimView
- }
-
- if let result = self.navigationBarContainer.hitTest(self.convert(point, to: self.navigationBarContainer), with: event) {
- return result
- }
-
- let result = super.hitTest(point, with: event)
- return result
- }
-
- @objc private func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
- if case .ended = recognizer.state {
- guard let environment = self.environment, let controller = environment.controller() else {
- return
- }
- controller.dismiss()
- }
- }
-
+
private func calculateResult() -> RecentActionsSettingsSheet.Value {
var events: AdminLogEventsFlags = []
var admins: [EnginePeer.Id] = []
@@ -370,598 +574,249 @@ private final class RecentActionsSettingsSheetComponent: Component {
admins: admins
)
}
-
- private func updateScrolling(isFirstTime: Bool = false, transition: ComponentTransition) {
- guard let environment = self.environment, let controller = environment.controller(), let itemLayout = self.itemLayout else {
- return
- }
- var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset
-
- let navigationAlpha: CGFloat = 1.0 - max(0.0, min(1.0, (topOffset + 20.0) / 20.0))
- transition.setAlpha(view: self.navigationBackgroundView, alpha: navigationAlpha)
- transition.setAlpha(layer: self.navigationBarSeparator, alpha: navigationAlpha)
-
- topOffset = max(0.0, topOffset)
- transition.setTransform(layer: self.backgroundLayer, transform: CATransform3DMakeTranslation(0.0, topOffset + itemLayout.containerInset, 0.0))
-
- transition.setPosition(view: self.navigationBarContainer, position: CGPoint(x: 0.0, y: topOffset + itemLayout.containerInset))
-
- let topOffsetDistance: CGFloat = min(200.0, floor(itemLayout.containerSize.height * 0.25))
- self.topOffsetDistance = topOffsetDistance
- var topOffsetFraction = topOffset / topOffsetDistance
- topOffsetFraction = max(0.0, min(1.0, topOffsetFraction))
-
- let modalStyleOverlayTransition: ContainedViewLayoutTransition
- if isFirstTime {
- modalStyleOverlayTransition = .animated(duration: 0.4, curve: .spring)
- } else {
- modalStyleOverlayTransition = transition.containedViewLayoutTransition
- }
-
- let transitionFactor: CGFloat = 1.0 - topOffsetFraction
- if self.isUpdating {
- DispatchQueue.main.async { [weak controller] in
- guard let controller else {
- return
- }
- controller.updateModalStyleOverlayTransitionFactor(transitionFactor, transition: modalStyleOverlayTransition)
- }
- } else {
- controller.updateModalStyleOverlayTransitionFactor(transitionFactor, transition: modalStyleOverlayTransition)
- }
- }
-
- func animateIn() {
- self.dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
- let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY
- self.scrollContentClippingView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
- self.backgroundLayer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
- self.navigationBarContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
- if let actionButtonView = self.actionButton.view {
- actionButtonView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
- }
- }
-
- func animateOut(completion: @escaping () -> Void) {
- let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY
-
- self.dimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
- self.scrollContentClippingView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true, completion: { _ in
- completion()
- })
- self.backgroundLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
- self.navigationBarContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
- if let actionButtonView = self.actionButton.view {
- actionButtonView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
- }
-
- if let environment = self.environment, let controller = environment.controller() {
- controller.updateModalStyleOverlayTransitionFactor(0.0, transition: .animated(duration: 0.3, curve: .easeInOut))
- }
- }
-
- func update(component: RecentActionsSettingsSheetComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize {
- self.isUpdating = true
- defer {
- self.isUpdating = false
- }
-
- let environment = environment[ViewControllerComponentContainer.Environment.self].value
- let themeUpdated = self.environment?.theme !== environment.theme
-
- let resetScrolling = self.scrollView.bounds.width != availableSize.width
-
- let sideInset: CGFloat = 16.0 + environment.safeInsets.left
-
- var isFirstTime = false
+
+ func update(component: RecentActionsSettingsResizableSheetComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize {
+ let environmentValue = environment[ViewControllerComponentContainer.Environment.self].value
+ let controller = environmentValue.controller
+ let theme = environmentValue.theme.withModalBlocksBackground()
+
if self.component == nil {
- isFirstTime = true
self.selectedMembersActions = Set(MembersActionType.actionTypesFromFlags(component.initialValue.events))
self.selectedSettingsActions = Set(SettingsActionType.actionTypesFromFlags(component.initialValue.events))
self.selectedMessagesActions = Set(MessagesActionType.actionTypesFromFlags(component.initialValue.events))
self.selectedAdmins = component.initialValue.admins.flatMap { Set($0) } ?? Set(component.adminPeers.map(\.id))
}
-
- var isGroup = true
- if case let .channel(channel) = component.peer, case .broadcast = channel.info {
- isGroup = false
- }
-
+
self.component = component
self.state = state
- self.environment = environment
-
- if themeUpdated {
- self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
- self.backgroundLayer.backgroundColor = environment.theme.list.blocksBackgroundColor.cgColor
-
- self.navigationBackgroundView.updateColor(color: environment.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate)
- self.navigationBarSeparator.backgroundColor = environment.theme.rootController.navigationBar.separatorColor.cgColor
- }
- let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 })
-
- transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize))
-
- var contentHeight: CGFloat = 0.0
- contentHeight += 54.0
- contentHeight += 16.0
-
- let leftButtonSize = self.leftButton.update(
- transition: transition,
- component: AnyComponent(GlassBarButtonComponent(
- size: CGSize(width: 44.0, height: 44.0),
- backgroundColor: nil,
- isDark: environment.theme.overallDarkAppearance,
- state: .glass,
- component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
- BundleIconComponent(
- name: "Navigation/Close",
- tintColor: environment.theme.chat.inputPanel.panelControlColor
- )
- )),
- action: { [weak self] _ in
- guard let self, let controller = self.environment?.controller() else {
- return
- }
- controller.dismiss()
- }
- )),
- environment: {},
- containerSize: CGSize(width: 44.0, height: 44.0)
- )
- let leftButtonFrame = CGRect(origin: CGPoint(x: 16.0 + environment.safeInsets.left, y: 16.0), size: leftButtonSize)
- if let leftButtonView = self.leftButton.view {
- if leftButtonView.superview == nil {
- self.navigationBarContainer.addSubview(leftButtonView)
- }
- transition.setFrame(view: leftButtonView, frame: leftButtonFrame)
- }
-
- let containerInset: CGFloat = environment.statusBarHeight + 10.0
-
- let clippingY: CGFloat
-
- let actionTypeSectionItem: (ActionTypeSection) -> AnyComponentWithIdentity = { actionTypeSection in
- let sectionId: AnyHashable
- let totalCount: Int
- let selectedCount: Int
- let isExpanded: Bool
- let title: String
-
- sectionId = actionTypeSection
- isExpanded = self.expandedSections.contains(actionTypeSection)
-
- switch actionTypeSection {
- case .members:
- totalCount = MembersActionType.allCases.count
- selectedCount = self.selectedMembersActions.count
- title = isGroup ? environment.strings.Channel_AdminLogFilter_Section_MembersGroup : environment.strings.Channel_AdminLogFilter_Section_MembersChannel
- case .settings:
- totalCount = SettingsActionType.allCases.count
- selectedCount = self.selectedSettingsActions.count
- title = isGroup ? environment.strings.Channel_AdminLogFilter_Section_SettingsGroup : environment.strings.Channel_AdminLogFilter_Section_SettingsChannel
- case .messages:
- totalCount = MessagesActionType.allCases.count
- selectedCount = self.selectedMessagesActions.count
- title = environment.strings.Channel_AdminLogFilter_Section_Messages
- }
-
- let itemTitle: AnyComponent = AnyComponent(HStack([
- AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent(
- text: .plain(NSAttributedString(
- string: title,
- font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
- textColor: environment.theme.list.itemPrimaryTextColor
- )),
- maximumNumberOfLines: 1
- ))),
- AnyComponentWithIdentity(id: 1, component: AnyComponent(MediaSectionExpandIndicatorComponent(
- theme: environment.theme,
- title: "\(selectedCount)/\(totalCount)",
- isExpanded: isExpanded
- )))
- ], spacing: 7.0))
-
- let toggleAction: () -> Void = { [weak self] in
- guard let self else {
- return
- }
-
- switch actionTypeSection {
- case .members:
- if self.selectedMembersActions.isEmpty {
- self.selectedMembersActions = Set(MembersActionType.allCases)
- } else {
- self.selectedMembersActions.removeAll()
- }
- case .settings:
- if self.selectedSettingsActions.isEmpty {
- self.selectedSettingsActions = Set(SettingsActionType.allCases)
- } else {
- self.selectedSettingsActions.removeAll()
- }
- case .messages:
- if self.selectedMessagesActions.isEmpty {
- self.selectedMessagesActions = Set(MessagesActionType.allCases)
- } else {
- self.selectedMessagesActions.removeAll()
- }
- }
-
- self.state?.updated(transition: .spring(duration: 0.35))
- }
-
- return AnyComponentWithIdentity(id: sectionId, component: AnyComponent(ListActionItemComponent(
- theme: environment.theme,
- style: .glass,
- title: itemTitle,
- leftIcon: .check(ListActionItemComponent.LeftIcon.Check(
- isSelected: selectedCount == totalCount,
- toggle: {
- toggleAction()
- }
- )),
- icon: .none,
- accessory: nil,
- action: { [weak self] _ in
- guard let self else {
- return
- }
- if self.expandedSections.contains(actionTypeSection) {
- self.expandedSections.remove(actionTypeSection)
- } else {
- self.expandedSections.insert(actionTypeSection)
- }
-
- self.state?.updated(transition: .spring(duration: 0.35))
- },
- highlighting: .disabled
- )))
- }
-
- let expandedActionTypeSectionItem: (ActionTypeSection) -> AnyComponentWithIdentity = { actionTypeSection in
- let sectionId: AnyHashable
- let selectedActionTypes: Set
- let actionTypes: [ActionType]
- switch actionTypeSection {
- case .members:
- sectionId = "members-sub"
- actionTypes = MembersActionType.allCases.map(ActionType.members)
- selectedActionTypes = Set(self.selectedMembersActions.map(ActionType.members))
- case .settings:
- sectionId = "settings-sub"
- actionTypes = SettingsActionType.allCases.map(ActionType.settings)
- selectedActionTypes = Set(self.selectedSettingsActions.map(ActionType.settings))
- case .messages:
- sectionId = "messages-sub"
- actionTypes = MessagesActionType.allCases.map(ActionType.messages)
- selectedActionTypes = Set(self.selectedMessagesActions.map(ActionType.messages))
- }
-
- var subItems: [AnyComponentWithIdentity] = []
- for actionType in actionTypes {
- let actionItemTitle: String = actionType.title(isGroup: isGroup, strings: environment.strings)
-
- let subItemToggleAction: () -> Void = { [weak self] in
- guard let self else {
- return
- }
-
- switch actionType {
- case let .members(value):
- if self.selectedMembersActions.contains(value) {
- self.selectedMembersActions.remove(value)
- } else {
- self.selectedMembersActions.insert(value)
- }
- case let .settings(value):
- if self.selectedSettingsActions.contains(value) {
- self.selectedSettingsActions.remove(value)
- } else {
- self.selectedSettingsActions.insert(value)
- }
- case let .messages(value):
- if self.selectedMessagesActions.contains(value) {
- self.selectedMessagesActions.remove(value)
- } else {
- self.selectedMessagesActions.insert(value)
- }
- }
-
- self.state?.updated(transition: .spring(duration: 0.35))
- }
-
- subItems.append(AnyComponentWithIdentity(id: actionType, component: AnyComponent(ListActionItemComponent(
- theme: environment.theme,
- style: .glass,
- title: AnyComponent(VStack([
- AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
- text: .plain(NSAttributedString(
- string: actionItemTitle,
- font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
- textColor: environment.theme.list.itemPrimaryTextColor
- )),
- maximumNumberOfLines: 1
- ))),
- ], alignment: .left, spacing: 2.0)),
- leftIcon: .check(ListActionItemComponent.LeftIcon.Check(
- isSelected: selectedActionTypes.contains(actionType),
- toggle: {
- subItemToggleAction()
- }
- )),
- icon: .none,
- accessory: .none,
- action: { _ in
- subItemToggleAction()
- },
- highlighting: .disabled
- ))))
- }
-
- return AnyComponentWithIdentity(id: sectionId, component: AnyComponent(ListSubSectionComponent(
- theme: environment.theme,
- leftInset: 62.0,
- items: subItems
- )))
- }
-
- let titleString: String = environment.strings.Channel_AdminLogFilter_RecentActionsTitle
- let titleSize = self.title.update(
- transition: .immediate,
- component: AnyComponent(MultilineTextComponent(
- text: .plain(NSAttributedString(string: titleString, font: Font.semibold(17.0), textColor: environment.theme.list.itemPrimaryTextColor))
- )),
- environment: {},
- containerSize: CGSize(width: availableSize.width - leftButtonFrame.maxX * 2.0, height: 100.0)
- )
- let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: floor((72.0 - titleSize.height) * 0.5)), size: titleSize)
- if let titleView = title.view {
- if titleView.superview == nil {
- self.navigationBarContainer.addSubview(titleView)
- }
- transition.setFrame(view: titleView, frame: titleFrame)
- }
-
- let navigationBackgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: 54.0))
- transition.setFrame(view: self.navigationBackgroundView, frame: navigationBackgroundFrame)
- self.navigationBackgroundView.update(size: navigationBackgroundFrame.size, cornerRadius: 10.0, maskedCorners: [.layerMinXMinYCorner, .layerMaxXMinYCorner], transition: transition.containedViewLayoutTransition)
- transition.setFrame(layer: self.navigationBarSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: 54.0), size: CGSize(width: availableSize.width, height: UIScreenPixel)))
-
- var optionsSectionItems: [AnyComponentWithIdentity] = []
- for actionTypeSection in ActionTypeSection.allCases {
- optionsSectionItems.append(actionTypeSectionItem(actionTypeSection))
- if self.expandedSections.contains(actionTypeSection) {
- optionsSectionItems.append(expandedActionTypeSectionItem(actionTypeSection))
- }
- }
-
- let optionsSectionTransition = transition
- let optionsSectionSize = self.optionsSection.update(
- transition: optionsSectionTransition,
- component: AnyComponent(ListSectionComponent(
- theme: environment.theme,
- style: .glass,
- header: AnyComponent(MultilineTextComponent(
- text: .plain(NSAttributedString(
- string: environment.strings.Channel_AdminLogFilter_FilterActionsTypeTitle,
- font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
- textColor: environment.theme.list.freeTextColor
- )),
- maximumNumberOfLines: 0
- )),
- footer: nil,
- items: optionsSectionItems
- )),
- environment: {},
- containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100000.0)
- )
- let optionsSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: optionsSectionSize)
- if let optionsSectionView = self.optionsSection.view {
- if optionsSectionView.superview == nil {
- self.scrollContentView.addSubview(optionsSectionView)
- self.optionsSection.parentState = state
- }
- transition.setFrame(view: optionsSectionView, frame: optionsSectionFrame)
- }
- contentHeight += optionsSectionSize.height
- contentHeight += 24.0
-
- var peerItems: [AnyComponentWithIdentity] = []
- for peer in component.adminPeers {
- peerItems.append(AnyComponentWithIdentity(id: peer.id, component: AnyComponent(AdminUserActionsPeerComponent(
- context: component.context,
- theme: environment.theme,
- strings: environment.strings,
- baseFontSize: presentationData.listsFontSize.baseDisplaySize,
- sideInset: 0.0,
- title: peer.displayTitle(strings: environment.strings, displayOrder: .firstLast),
- peer: peer,
- selectionState: .editing(isSelected: self.selectedAdmins.contains(peer.id)),
- action: { [weak self] peer in
- guard let self else {
- return
- }
-
- if self.selectedAdmins.contains(peer.id) {
- self.selectedAdmins.remove(peer.id)
- } else {
- self.selectedAdmins.insert(peer.id)
- }
-
- self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.35, curve: .easeInOut)))
- }
- ))))
- }
-
- var adminsSectionItems: [AnyComponentWithIdentity] = []
- let allAdminsToggleAction: () -> Void = { [weak self] in
- guard let self, let component = self.component else {
+
+ let dismiss: (Bool) -> Void = { [weak self] animated in
+ guard let self, !self.isDismissing else {
return
}
-
- if self.selectedAdmins.isEmpty {
- self.selectedAdmins = Set(component.adminPeers.map(\.id))
- } else {
- self.selectedAdmins.removeAll()
+ self.isDismissing = true
+
+ let performDismiss: () -> Void = {
+ if let controller = controller() as? RecentActionsSettingsSheet {
+ controller.completePendingDismiss()
+ controller.dismiss(animated: false)
+ }
+ }
+
+ if animated {
+ self.animateOut.invoke(Action { _ in
+ performDismiss()
+ })
+ } else {
+ performDismiss()
}
-
- self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.35, curve: .easeInOut)))
}
- adminsSectionItems.append(AnyComponentWithIdentity(id: adminsSectionItems.count, component: AnyComponent(ListActionItemComponent(
- theme: environment.theme,
- style: .glass,
- title: AnyComponent(VStack([
- AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
+
+ let currentState = RecentActionsSettingsSheetState(
+ expandedSections: self.expandedSections,
+ selectedMembersActions: self.selectedMembersActions,
+ selectedSettingsActions: self.selectedSettingsActions,
+ selectedMessagesActions: self.selectedMessagesActions,
+ selectedAdmins: self.selectedAdmins
+ )
+
+ let sheetSize = self.sheet.update(
+ transition: transition,
+ component: AnyComponent(ResizableSheetComponent(
+ content: AnyComponent(RecentActionsSettingsContentComponent(
+ context: component.context,
+ peer: component.peer,
+ adminPeers: component.adminPeers,
+ theme: theme,
+ sheetState: currentState,
+ toggleActionTypeSectionSelection: { [weak self] actionTypeSection in
+ guard let self else {
+ return
+ }
+
+ switch actionTypeSection {
+ case .members:
+ if self.selectedMembersActions.isEmpty {
+ self.selectedMembersActions = Set(MembersActionType.allCases)
+ } else {
+ self.selectedMembersActions.removeAll()
+ }
+ case .settings:
+ if self.selectedSettingsActions.isEmpty {
+ self.selectedSettingsActions = Set(SettingsActionType.allCases)
+ } else {
+ self.selectedSettingsActions.removeAll()
+ }
+ case .messages:
+ if self.selectedMessagesActions.isEmpty {
+ self.selectedMessagesActions = Set(MessagesActionType.allCases)
+ } else {
+ self.selectedMessagesActions.removeAll()
+ }
+ }
+
+ self.state?.updated(transition: .spring(duration: 0.35))
+ },
+ toggleActionTypeSectionExpansion: { [weak self] actionTypeSection in
+ guard let self else {
+ return
+ }
+ if self.expandedSections.contains(actionTypeSection) {
+ self.expandedSections.remove(actionTypeSection)
+ } else {
+ self.expandedSections.insert(actionTypeSection)
+ }
+
+ self.state?.updated(transition: .spring(duration: 0.35))
+ },
+ toggleActionType: { [weak self] actionType in
+ guard let self else {
+ return
+ }
+
+ switch actionType {
+ case let .members(value):
+ if self.selectedMembersActions.contains(value) {
+ self.selectedMembersActions.remove(value)
+ } else {
+ self.selectedMembersActions.insert(value)
+ }
+ case let .settings(value):
+ if self.selectedSettingsActions.contains(value) {
+ self.selectedSettingsActions.remove(value)
+ } else {
+ self.selectedSettingsActions.insert(value)
+ }
+ case let .messages(value):
+ if self.selectedMessagesActions.contains(value) {
+ self.selectedMessagesActions.remove(value)
+ } else {
+ self.selectedMessagesActions.insert(value)
+ }
+ }
+
+ self.state?.updated(transition: .spring(duration: 0.35))
+ },
+ toggleAdmin: { [weak self] peer in
+ guard let self else {
+ return
+ }
+
+ if self.selectedAdmins.contains(peer.id) {
+ self.selectedAdmins.remove(peer.id)
+ } else {
+ self.selectedAdmins.insert(peer.id)
+ }
+
+ self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.35, curve: .easeInOut)))
+ },
+ toggleAllAdmins: { [weak self] in
+ guard let self, let component = self.component else {
+ return
+ }
+
+ if self.selectedAdmins.isEmpty {
+ self.selectedAdmins = Set(component.adminPeers.map(\.id))
+ } else {
+ self.selectedAdmins.removeAll()
+ }
+
+ self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.35, curve: .easeInOut)))
+ }
+ )),
+ titleItem: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
- string: environment.strings.Channel_AdminLogFilter_ShowAllAdminsActions,
- font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
- textColor: environment.theme.list.itemPrimaryTextColor
+ string: environmentValue.strings.Channel_AdminLogFilter_RecentActionsTitle,
+ font: Font.semibold(17.0),
+ textColor: theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
- ))),
- ], alignment: .left, spacing: 2.0)),
- leftIcon: .check(ListActionItemComponent.LeftIcon.Check(
- isSelected: self.selectedAdmins.count == component.adminPeers.count,
- toggle: {
- allAdminsToggleAction()
- }
- )),
- icon: .none,
- accessory: .none,
- action: { _ in
- allAdminsToggleAction()
- },
- highlighting: .disabled
- ))))
- adminsSectionItems.append(AnyComponentWithIdentity(id: adminsSectionItems.count, component: AnyComponent(ListSubSectionComponent(
- theme: environment.theme,
- leftInset: 62.0,
- items: peerItems
- ))))
- let adminsSectionSize = self.adminsSection.update(
- transition: transition,
- component: AnyComponent(ListSectionComponent(
- theme: environment.theme,
- style: .glass,
- header: AnyComponent(MultilineTextComponent(
- text: .plain(NSAttributedString(
- string: environment.strings.Channel_AdminLogFilter_FilterActionsAdminsTitle,
- font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
- textColor: environment.theme.list.freeTextColor
- )),
- maximumNumberOfLines: 0
)),
- footer: nil,
- items: adminsSectionItems
- )),
- environment: {},
- containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100000.0)
- )
- let adminsSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: adminsSectionSize)
- if let adminsSectionView = self.adminsSection.view {
- if adminsSectionView.superview == nil {
- self.scrollContentView.addSubview(adminsSectionView)
- self.adminsSection.parentState = state
- }
- transition.setFrame(view: adminsSectionView, frame: adminsSectionFrame)
- }
- contentHeight += adminsSectionSize.height
-
- contentHeight += 30.0
-
- let bottomInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0)
- let actionButtonSize = self.actionButton.update(
- transition: transition,
- component: AnyComponent(ButtonComponent(
- background: ButtonComponent.Background(
- style: .glass,
- color: environment.theme.list.itemCheckColors.fillColor,
- foreground: environment.theme.list.itemCheckColors.foregroundColor,
- pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
+ leftItem: AnyComponent(
+ GlassBarButtonComponent(
+ size: CGSize(width: 44.0, height: 44.0),
+ backgroundColor: nil,
+ isDark: theme.overallDarkAppearance,
+ state: .glass,
+ component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
+ BundleIconComponent(
+ name: "Navigation/Close",
+ tintColor: theme.chat.inputPanel.panelControlColor
+ )
+ )),
+ action: { _ in
+ dismiss(true)
+ }
+ )
),
- content: AnyComponentWithIdentity(
- id: AnyHashable(0),
- component: AnyComponent(ButtonTextContentComponent(
- text: environment.strings.Channel_AdminLogFilter_ApplyFilter,
- badge: 0,
- textColor: environment.theme.list.itemCheckColors.foregroundColor,
- badgeBackground: environment.theme.list.itemCheckColors.foregroundColor,
- badgeForeground: environment.theme.list.itemCheckColors.fillColor
- ))
- ),
- isEnabled: true,
- displaysProgress: false,
- action: { [weak self] in
- guard let self, let component = self.component else {
- return
+ bottomItem: AnyComponent(ButtonComponent(
+ background: ButtonComponent.Background(
+ style: .glass,
+ color: theme.list.itemCheckColors.fillColor,
+ foreground: theme.list.itemCheckColors.foregroundColor,
+ pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
+ ),
+ content: AnyComponentWithIdentity(
+ id: AnyHashable(0),
+ component: AnyComponent(ButtonTextContentComponent(
+ text: environmentValue.strings.Channel_AdminLogFilter_ApplyFilter,
+ badge: 0,
+ textColor: theme.list.itemCheckColors.foregroundColor,
+ badgeBackground: theme.list.itemCheckColors.foregroundColor,
+ badgeForeground: theme.list.itemCheckColors.fillColor
+ ))
+ ),
+ isEnabled: true,
+ displaysProgress: false,
+ action: { [weak self] in
+ guard let self, let component = self.component else {
+ return
+ }
+ let result = self.calculateResult()
+ dismiss(true)
+ component.completion(result)
}
- self.environment?.controller()?.dismiss()
- component.completion(self.calculateResult())
- }
+ )),
+ backgroundColor: .color(theme.list.modalBlocksBackgroundColor),
+ animateOut: self.animateOut
)),
- environment: {},
- containerSize: CGSize(width: availableSize.width - bottomInsets.left - bottomInsets.right, height: 52.0)
+ environment: {
+ environmentValue
+ ResizableSheetComponentEnvironment(
+ theme: theme,
+ statusBarHeight: environmentValue.statusBarHeight,
+ safeInsets: environmentValue.safeInsets,
+ inputHeight: 0.0,
+ metrics: environmentValue.metrics,
+ deviceMetrics: environmentValue.deviceMetrics,
+ isDisplaying: environmentValue.isVisible,
+ isCentered: environmentValue.metrics.widthClass == .regular,
+ screenSize: availableSize,
+ regularMetricsSize: nil,
+ dismiss: { animated in
+ dismiss(animated)
+ }
+ )
+ },
+ forceUpdate: true,
+ containerSize: availableSize
)
- let bottomPanelHeight = actionButtonSize.height + bottomInsets.bottom
- let actionButtonFrame = CGRect(origin: CGPoint(x: bottomInsets.left, y: availableSize.height - bottomPanelHeight), size: actionButtonSize)
- if let actionButtonView = actionButton.view {
- if actionButtonView.superview == nil {
- self.addSubview(actionButtonView)
+ self.sheet.parentState = state
+ if let sheetView = self.sheet.view {
+ if sheetView.superview == nil {
+ self.addSubview(sheetView)
}
- transition.setFrame(view: actionButtonView, frame: actionButtonFrame)
+ transition.setFrame(view: sheetView, frame: CGRect(origin: .zero, size: sheetSize))
}
-
- contentHeight += bottomPanelHeight
-
- clippingY = actionButtonFrame.minY - 24.0
-
- let topInset: CGFloat = max(0.0, availableSize.height - containerInset - contentHeight)
-
- let scrollContentHeight = max(topInset + contentHeight + containerInset, availableSize.height - containerInset)
-
- self.scrollContentClippingView.layer.cornerRadius = 10.0
-
- self.itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, bottomInset: environment.safeInsets.bottom, topInset: topInset)
-
- transition.setFrame(view: self.scrollContentView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset + containerInset), size: CGSize(width: availableSize.width, height: contentHeight)))
-
- transition.setPosition(layer: self.backgroundLayer, position: CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0))
- transition.setBounds(layer: self.backgroundLayer, bounds: CGRect(origin: CGPoint(), size: availableSize))
-
- let scrollClippingFrame = CGRect(origin: CGPoint(x: sideInset, y: containerInset), size: CGSize(width: availableSize.width - sideInset * 2.0, height: clippingY - containerInset))
- transition.setPosition(view: self.scrollContentClippingView, position: scrollClippingFrame.center)
- transition.setBounds(view: self.scrollContentClippingView, bounds: CGRect(origin: CGPoint(x: scrollClippingFrame.minX, y: scrollClippingFrame.minY), size: scrollClippingFrame.size))
-
- self.ignoreScrolling = true
- let previousBounds = self.scrollView.bounds
- transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height)))
- let contentSize = CGSize(width: availableSize.width, height: scrollContentHeight)
- if contentSize != self.scrollView.contentSize {
- self.scrollView.contentSize = contentSize
- }
- if resetScrolling {
- self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: availableSize)
- } else {
- if !previousBounds.isEmpty, !transition.animation.isImmediate {
- let bounds = self.scrollView.bounds
- if bounds.maxY != previousBounds.maxY {
- let offsetY = previousBounds.maxY - bounds.maxY
- transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: offsetY), to: CGPoint(), additive: true)
- }
- }
- }
- self.ignoreScrolling = false
- self.updateScrolling(isFirstTime: isFirstTime, transition: transition)
-
+
return availableSize
}
}
-
+
func makeView() -> View {
return View(frame: CGRect())
}
-
+
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
@@ -981,11 +836,12 @@ public class RecentActionsSettingsSheet: ViewControllerComponentContainer {
private let context: AccountContext
private var isDismissed: Bool = false
+ private var dismissCompletion: (() -> Void)?
public init(context: AccountContext, peer: EnginePeer, adminPeers: [EnginePeer], initialValue: Value, completion: @escaping (Value) -> Void) {
self.context = context
- super.init(context: context, component: RecentActionsSettingsSheetComponent(context: context, peer: peer, adminPeers: adminPeers, initialValue: initialValue, completion: completion), navigationBarAppearance: .none)
+ super.init(context: context, component: RecentActionsSettingsResizableSheetComponent(context: context, peer: peer, adminPeers: adminPeers, initialValue: initialValue, completion: completion), navigationBarAppearance: .none)
self.statusBar.statusBarStyle = .Ignore
self.navigationPresentation = .flatModal
@@ -1003,22 +859,29 @@ public class RecentActionsSettingsSheet: ViewControllerComponentContainer {
super.viewDidAppear(animated)
self.view.disablesInteractiveModalDismiss = true
-
- if let componentView = self.node.hostView.componentView as? RecentActionsSettingsSheetComponent.View {
- componentView.animateIn()
+ }
+
+ fileprivate func completePendingDismiss() {
+ let dismissCompletion = self.dismissCompletion
+ self.dismissCompletion = nil
+ dismissCompletion?()
+ }
+
+ public func dismissAnimated() {
+ if let view = self.node.hostView.findTaggedView(tag: ResizableSheetComponent.View.Tag()) as? ResizableSheetComponent.View {
+ view.dismissAnimated()
}
}
override public func dismiss(completion: (() -> Void)? = nil) {
if !self.isDismissed {
self.isDismissed = true
+ self.dismissCompletion = completion
- if let componentView = self.node.hostView.componentView as? RecentActionsSettingsSheetComponent.View {
- componentView.animateOut(completion: { [weak self] in
- completion?()
- self?.dismiss(animated: false)
- })
+ if let view = self.node.hostView.findTaggedView(tag: ResizableSheetComponent.View.Tag()) as? ResizableSheetComponent.View {
+ view.dismissAnimated()
} else {
+ self.completePendingDismiss()
self.dismiss(animated: false)
}
}
diff --git a/submodules/TelegramUI/Components/Chat/ChatInputMessageAccessoryPanel/Sources/ChatInputMessageAccessoryPanel.swift b/submodules/TelegramUI/Components/Chat/ChatInputMessageAccessoryPanel/Sources/ChatInputMessageAccessoryPanel.swift
index f3e4c1de58..28980b0af3 100644
--- a/submodules/TelegramUI/Components/Chat/ChatInputMessageAccessoryPanel/Sources/ChatInputMessageAccessoryPanel.swift
+++ b/submodules/TelegramUI/Components/Chat/ChatInputMessageAccessoryPanel/Sources/ChatInputMessageAccessoryPanel.swift
@@ -222,6 +222,7 @@ public final class ChatInputMessageAccessoryPanel: Component {
let contents: Contents
let chatPeerId: EnginePeer.Id?
let action: ((UIView) -> Void)?
+ let longPressAction: ((UIView) -> Void)?
let dismiss: (UIView) -> Void
public init(
@@ -229,12 +230,14 @@ public final class ChatInputMessageAccessoryPanel: Component {
contents: Contents,
chatPeerId: EnginePeer.Id?,
action: ((UIView) -> Void)?,
+ longPressAction: ((UIView) -> Void)? = nil,
dismiss: @escaping (UIView) -> Void
) {
self.context = context
self.contents = contents
self.chatPeerId = chatPeerId
self.action = action
+ self.longPressAction = longPressAction
self.dismiss = dismiss
}
@@ -251,12 +254,16 @@ public final class ChatInputMessageAccessoryPanel: Component {
if (lhs.action == nil) != (rhs.action == nil) {
return false
}
+ if (lhs.longPressAction == nil) != (rhs.longPressAction == nil) {
+ return false
+ }
return true
}
public final class View: UIView, ChatInputAccessoryPanelView {
private let closeButton: HighlightTrackingButton
private let closeButtonIcon: GlassBackgroundView.ContentImageView
+ private let longPressGestureRecognizer: UILongPressGestureRecognizer
private let lineView: UIImageView
private let titleNode: CompositeTextNode
@@ -295,6 +302,7 @@ public final class ChatInputMessageAccessoryPanel: Component {
self.closeButton = HighlightTrackingButton()
self.closeButtonIcon = GlassBackgroundView.ContentImageView()
+ self.longPressGestureRecognizer = UILongPressGestureRecognizer()
self.lineView = UIImageView()
self.titleNode = CompositeTextNode()
@@ -311,6 +319,10 @@ public final class ChatInputMessageAccessoryPanel: Component {
self.closeButton.addTarget(self, action: #selector(self.closeButtonPressed), for: .touchUpInside)
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
+
+ self.longPressGestureRecognizer.addTarget(self, action: #selector(self.longPressGesture(_:)))
+ self.longPressGestureRecognizer.isEnabled = false
+ self.addGestureRecognizer(self.longPressGestureRecognizer)
}
required public init?(coder: NSCoder) {
@@ -330,6 +342,15 @@ public final class ChatInputMessageAccessoryPanel: Component {
}
}
+ @objc private func longPressGesture(_ recognizer: UILongPressGestureRecognizer) {
+ guard let component = self.component else {
+ return
+ }
+ if case .began = recognizer.state {
+ component.longPressAction?(self)
+ }
+ }
+
@objc private func closeButtonPressed() {
guard let component = self.component else {
return
@@ -384,6 +405,7 @@ public final class ChatInputMessageAccessoryPanel: Component {
self.component = component
self.state = state
self.environment = environment
+ self.longPressGestureRecognizer.isEnabled = component.longPressAction != nil
if self.closeButtonIcon.image == nil {
self.closeButtonIcon.image = generateCloseIcon()
diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift
index eb5bfb2f56..e161ea06b6 100644
--- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift
+++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift
@@ -1766,9 +1766,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
if let forwardInfo = item.content.firstMessage.forwardInfo, forwardInfo.source == nil, forwardInfo.author?.id.namespace == Namespaces.Peer.CloudUser {
for media in item.content.firstMessage.media {
if let file = media as? TelegramMediaFile {
- if file.isMusic {
- ignoreForward = true
- } else if file.isInstantVideo {
+ if file.isInstantVideo {
isInstantVideo = true
}
break
diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift
index 994059e499..318593351f 100644
--- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift
+++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift
@@ -772,7 +772,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
displayTranscribe = false
} else if arguments.message.id.peerId.namespace != Namespaces.Peer.SecretChat && !isViewOnceMessage && !arguments.presentationData.isPreview {
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: arguments.context.currentAppConfiguration.with { $0 })
- if arguments.associatedData.isPremium {
+ if arguments.associatedData.isPremium || arguments.associatedData.alwaysDisplayTranscribeButton.providedByGroupBoost {
displayTranscribe = true
} else if premiumConfiguration.audioTransciptionTrialCount > 0 {
if arguments.incoming {
@@ -786,8 +786,6 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
} else if arguments.incoming && isConsumed == false && arguments.associatedData.alwaysDisplayTranscribeButton.displayForNotConsumed {
displayTranscribe = true
}
- } else if arguments.associatedData.alwaysDisplayTranscribeButton.providedByGroupBoost {
- displayTranscribe = true
}
}
@@ -2227,4 +2225,3 @@ public final class FileMessageSelectionNode: ASDisplayNode {
self.checkNode.frame = CGRect(origin: checkOrigin, size: checkSize)
}
}
-
diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift
index 4da9e77586..b6b8530047 100644
--- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift
+++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift
@@ -840,7 +840,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
var displayTranscribe = false
if item.message.id.peerId.namespace != Namespaces.Peer.SecretChat && statusDisplayType == .free && !isViewOnceMessage && !item.presentationData.isPreview {
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: item.context.currentAppConfiguration.with { $0 })
- if item.associatedData.isPremium {
+ if item.associatedData.isPremium || item.associatedData.alwaysDisplayTranscribeButton.providedByGroupBoost {
displayTranscribe = true
} else if premiumConfiguration.audioTransciptionTrialCount > 0 {
if incoming {
@@ -852,8 +852,6 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
} else {
displayTranscribe = false
}
- } else if item.associatedData.alwaysDisplayTranscribeButton.providedByGroupBoost {
- displayTranscribe = true
}
}
diff --git a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift
index 817dfb74ca..d7cd526a60 100644
--- a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift
+++ b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift
@@ -2690,6 +2690,14 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
if !poll.countries.isEmpty, let accountCountry = item.associatedData.accountCountry, !poll.countries.contains(accountCountry) {
isRestricted = true
}
+ if poll.restrictToSubscribers {
+ let period: Int32 = item.context.account.testingEnvironment ? 5 * 60 : 24 * 60 * 60
+ if !item.associatedData.isParticipant {
+ isRestricted = true
+ } else if let invitedOn = item.associatedData.invitedOn, invitedOn + period > Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) {
+ isRestricted = true
+ }
+ }
orderedPollOptions = resolvedOptionOrder(for: item)
diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift
index 9637ba4340..fa8ecc35fb 100644
--- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift
+++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift
@@ -1487,7 +1487,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
}
private func presentAutoremoveSetup() {
- /*let controller = ChatTimerScreen(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: self.peer.id, style: .default, mode: .autoremove, currentTime: currentValue, dismissByTapOutside: true, completion: { [weak self] value in
+ /*let controller = ChatTimerScreen(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: self.peer.id, style: .default, mode: .autoremove, currentTime: currentValue, completion: { [weak self] value in
guard let strongSelf = self else {
return
}
diff --git a/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeScreen.swift b/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeScreen.swift
index a8843ec543..d36aec58d2 100644
--- a/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeScreen.swift
+++ b/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeScreen.swift
@@ -613,12 +613,12 @@ private final class ChatScheduleTimeSheetContentComponent: Component {
component: AnyComponent(ButtonComponent(
background: ButtonComponent.Background(
style: .glass,
- color: environment.theme.list.itemDestructiveColor.withMultipliedAlpha(0.1),
- foreground: environment.theme.list.itemDestructiveColor,
- pressedColor: environment.theme.list.itemDestructiveColor.withMultipliedAlpha(0.8),
+ color: environment.theme.list.itemAccentColor.withMultipliedAlpha(0.1),
+ foreground: environment.theme.list.itemAccentColor,
+ pressedColor: environment.theme.list.itemAccentColor.withMultipliedAlpha(0.8),
),
content: AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent(
- Text(text: strings.Conversation_FormatDate_RemoveDate, font: Font.semibold(17.0), color: environment.theme.list.itemDestructiveColor)
+ Text(text: strings.Conversation_FormatDate_RemoveDate, font: Font.semibold(17.0), color: environment.theme.list.itemAccentColor)
)),
isEnabled: true,
displaysProgress: false,
diff --git a/submodules/TelegramUI/Components/ChatTimerScreen/BUILD b/submodules/TelegramUI/Components/ChatTimerScreen/BUILD
index 222d4862c0..e7b96cf57c 100644
--- a/submodules/TelegramUI/Components/ChatTimerScreen/BUILD
+++ b/submodules/TelegramUI/Components/ChatTimerScreen/BUILD
@@ -11,14 +11,18 @@ swift_library(
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
- "//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/TelegramCore:TelegramCore",
"//submodules/AccountContext:AccountContext",
"//submodules/TelegramPresentationData:TelegramPresentationData",
- "//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode",
"//submodules/PresentationDataUtils:PresentationDataUtils",
"//submodules/TelegramStringFormatting:TelegramStringFormatting",
+ "//submodules/ComponentFlow",
+ "//submodules/Components/ViewControllerComponent",
+ "//submodules/Components/SheetComponent",
+ "//submodules/TelegramUI/Components/ButtonComponent",
+ "//submodules/Components/BundleIconComponent",
+ "//submodules/TelegramUI/Components/GlassBarButtonComponent",
],
visibility = [
"//visibility:public",
diff --git a/submodules/TelegramUI/Components/ChatTimerScreen/Sources/ChatTimerScreen.swift b/submodules/TelegramUI/Components/ChatTimerScreen/Sources/ChatTimerScreen.swift
index f784eb74b2..91ded2f86e 100644
--- a/submodules/TelegramUI/Components/ChatTimerScreen/Sources/ChatTimerScreen.swift
+++ b/submodules/TelegramUI/Components/ChatTimerScreen/Sources/ChatTimerScreen.swift
@@ -1,14 +1,18 @@
import Foundation
import UIKit
import Display
-import AsyncDisplayKit
import TelegramCore
import SwiftSignalKit
import AccountContext
-import SolidRoundedButtonNode
import TelegramPresentationData
import PresentationDataUtils
import TelegramStringFormatting
+import ComponentFlow
+import ViewControllerComponent
+import SheetComponent
+import ButtonComponent
+import BundleIconComponent
+import GlassBarButtonComponent
public enum ChatTimerScreenStyle {
case `default`
@@ -21,101 +25,7 @@ public enum ChatTimerScreenMode {
case mute
}
-public final class ChatTimerScreen: ViewController {
- private var controllerNode: ChatTimerScreenNode {
- return self.displayNode as! ChatTimerScreenNode
- }
-
- private var animatedIn = false
-
- private let context: AccountContext
- private let style: ChatTimerScreenStyle
- private let mode: ChatTimerScreenMode
- private let currentTime: Int32?
- private let dismissByTapOutside: Bool
- private let completion: (Int32) -> Void
-
- private var presentationData: PresentationData
- private var presentationDataDisposable: Disposable?
-
- public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, style: ChatTimerScreenStyle, mode: ChatTimerScreenMode = .sendTimer, currentTime: Int32? = nil, dismissByTapOutside: Bool = true, completion: @escaping (Int32) -> Void) {
- self.context = context
- self.style = style
- self.mode = mode
- self.currentTime = currentTime
- self.dismissByTapOutside = dismissByTapOutside
- self.completion = completion
-
- self.presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
-
- super.init(navigationBarPresentationData: nil)
-
- self.statusBar.statusBarStyle = .Ignore
-
- self.blocksBackgroundWhenInOverlay = true
-
- self.presentationDataDisposable = ((updatedPresentationData?.signal ?? context.sharedContext.presentationData)
- |> deliverOnMainQueue).start(next: { [weak self] presentationData in
- if let strongSelf = self {
- strongSelf.presentationData = presentationData
- strongSelf.controllerNode.updatePresentationData(presentationData)
- }
- })
-
- self.statusBar.statusBarStyle = .Ignore
- }
-
- required public init(coder aDecoder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
-
- deinit {
- self.presentationDataDisposable?.dispose()
- }
-
- override public func loadDisplayNode() {
- self.displayNode = ChatTimerScreenNode(context: self.context, presentationData: presentationData, style: self.style, mode: self.mode, currentTime: self.currentTime, dismissByTapOutside: self.dismissByTapOutside)
- self.controllerNode.completion = { [weak self] time in
- guard let strongSelf = self else {
- return
- }
- strongSelf.completion(time)
- strongSelf.dismiss()
- }
- self.controllerNode.dismiss = { [weak self] in
- self?.presentingViewController?.dismiss(animated: false, completion: nil)
- }
- self.controllerNode.cancel = { [weak self] in
- self?.dismiss()
- }
- }
-
- override public func loadView() {
- super.loadView()
- }
-
- override public func viewDidAppear(_ animated: Bool) {
- super.viewDidAppear(animated)
-
- if !self.animatedIn {
- self.animatedIn = true
- self.controllerNode.animateIn()
- }
- }
-
- override public func dismiss(completion: (() -> Void)? = nil) {
- self.controllerNode.animateOut(completion: completion)
- }
-
- override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
- super.containerLayoutUpdated(layout, transition: transition)
-
- self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
- }
-}
-
private protocol TimerPickerView: UIView {
-
}
private class TimerCustomPickerView: UIPickerView, TimerPickerView {
@@ -128,20 +38,20 @@ private class TimerCustomPickerView: UIPickerView, TimerPickerView {
}
}
}
-
+
override func didAddSubview(_ subview: UIView) {
super.didAddSubview(subview)
-
+
if let selectorColor = self.selectorColor {
if subview.bounds.height <= 1.0 {
subview.backgroundColor = selectorColor
}
}
}
-
+
override func didMoveToWindow() {
super.didMoveToWindow()
-
+
if let selectorColor = self.selectorColor {
for subview in self.subviews {
if subview.bounds.height <= 1.0 {
@@ -162,20 +72,20 @@ private class TimerDatePickerView: UIDatePicker, TimerPickerView {
}
}
}
-
+
override func didAddSubview(_ subview: UIView) {
super.didAddSubview(subview)
-
+
if let selectorColor = self.selectorColor {
if subview.bounds.height <= 1.0 {
subview.backgroundColor = selectorColor
}
}
}
-
+
override func didMoveToWindow() {
super.didMoveToWindow()
-
+
if let selectorColor = self.selectorColor {
for subview in self.subviews {
if subview.bounds.height <= 1.0 {
@@ -192,14 +102,14 @@ private let nondigitsCharacterSet = CharacterSet(charactersIn: "0123456789").inv
private class TimerPickerItemView: UIView {
let valueLabel = UILabel()
let unitLabel = UILabel()
-
+
var textColor: UIColor? = nil {
didSet {
self.valueLabel.textColor = self.textColor
self.unitLabel.textColor = self.textColor
}
}
-
+
var value: (Int32, String)? {
didSet {
if let (value, string) = self.value {
@@ -215,36 +125,36 @@ private class TimerPickerItemView: UIView {
self.unitLabel.text = string.trimmingCharacters(in: digitsCharacterSet)
}
}
-
+
self.setNeedsLayout()
}
}
-
+
override init(frame: CGRect) {
self.valueLabel.backgroundColor = nil
self.valueLabel.isOpaque = false
self.valueLabel.font = Font.regular(24.0)
-
+
self.unitLabel.backgroundColor = nil
self.unitLabel.isOpaque = false
self.unitLabel.font = Font.medium(16.0)
-
+
super.init(frame: frame)
-
+
self.addSubview(self.valueLabel)
self.addSubview(self.unitLabel)
}
-
+
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
-
+
override func layoutSubviews() {
super.layoutSubviews()
-
+
self.valueLabel.sizeToFit()
self.unitLabel.sizeToFit()
-
+
if let (value, _) = self.value, value == viewOnceTimeout {
self.valueLabel.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((self.frame.width - self.valueLabel.frame.size.width) / 2.0), y: floor((self.frame.height - self.valueLabel.frame.height) / 2.0)), size: self.valueLabel.frame.size)
} else {
@@ -265,514 +175,667 @@ private var timerValues: [Int32] = {
return values
}()
-class ChatTimerScreenNode: ViewControllerTracingNode, ASScrollViewDelegate, UIPickerViewDataSource, UIPickerViewDelegate {
- private let context: AccountContext
- private let controllerStyle: ChatTimerScreenStyle
- private var presentationData: PresentationData
- private let dismissByTapOutside: Bool
- private let mode: ChatTimerScreenMode
-
- private let dimNode: ASDisplayNode
- private let wrappingScrollNode: ASScrollNode
- private let contentContainerNode: ASDisplayNode
- private let effectNode: ASDisplayNode
- private let backgroundNode: ASDisplayNode
- private let contentBackgroundNode: ASDisplayNode
- private let titleNode: ASTextNode
- private let textNode: ImmediateTextNode
- private let cancelButton: HighlightableButtonNode
- private let doneButton: SolidRoundedButtonNode
-
- private let disableButton: HighlightableButtonNode
- private let disableButtonTitle: ImmediateTextNode
-
- private var initialTime: Int32?
- private var pickerView: TimerPickerView?
-
- private let autoremoveTimerValues: [Int32]
-
- private var containerLayout: (ContainerViewLayout, CGFloat)?
-
- var completion: ((Int32) -> Void)?
- var dismiss: (() -> Void)?
- var cancel: (() -> Void)?
-
- init(context: AccountContext, presentationData: PresentationData, style: ChatTimerScreenStyle, mode: ChatTimerScreenMode, currentTime: Int32?, dismissByTapOutside: Bool) {
- self.context = context
- self.controllerStyle = style
- self.presentationData = presentationData
- self.dismissByTapOutside = dismissByTapOutside
- self.mode = mode
- self.initialTime = currentTime
-
- self.wrappingScrollNode = ASScrollNode()
- self.wrappingScrollNode.view.alwaysBounceVertical = true
- self.wrappingScrollNode.view.delaysContentTouches = false
- self.wrappingScrollNode.view.canCancelContentTouches = true
-
- self.dimNode = ASDisplayNode()
- self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
-
- self.contentContainerNode = ASDisplayNode()
- self.contentContainerNode.isOpaque = false
+private let autoremoveTimerValues: [Int32] = [
+ 1 * 24 * 60 * 60 as Int32,
+ 2 * 24 * 60 * 60 as Int32,
+ 3 * 24 * 60 * 60 as Int32,
+ 4 * 24 * 60 * 60 as Int32,
+ 5 * 24 * 60 * 60 as Int32,
+ 6 * 24 * 60 * 60 as Int32,
+ 1 * 7 * 24 * 60 * 60 as Int32,
+ 2 * 7 * 24 * 60 * 60 as Int32,
+ 3 * 7 * 24 * 60 * 60 as Int32,
+ 1 * 31 * 24 * 60 * 60 as Int32,
+ 2 * 30 * 24 * 60 * 60 as Int32,
+ 3 * 31 * 24 * 60 * 60 as Int32,
+ 4 * 30 * 24 * 60 * 60 as Int32,
+ 5 * 31 * 24 * 60 * 60 as Int32,
+ 6 * 30 * 24 * 60 * 60 as Int32,
+ 365 * 24 * 60 * 60 as Int32
+]
- self.backgroundNode = ASDisplayNode()
- self.backgroundNode.clipsToBounds = true
- self.backgroundNode.cornerRadius = 16.0
-
- let backgroundColor: UIColor
- let textColor: UIColor
- let accentColor: UIColor
- let blurStyle: UIBlurEffect.Style
- switch style {
- case .default:
- backgroundColor = self.presentationData.theme.actionSheet.itemBackgroundColor
- textColor = self.presentationData.theme.actionSheet.primaryTextColor
- accentColor = self.presentationData.theme.actionSheet.controlAccentColor
- blurStyle = self.presentationData.theme.actionSheet.backgroundType == .light ? .light : .dark
- case .media:
- backgroundColor = UIColor(rgb: 0x1c1c1e)
- textColor = .white
- accentColor = self.presentationData.theme.actionSheet.controlAccentColor
- blurStyle = .dark
+private final class ChatTimerSheetContentComponent: Component {
+ typealias EnvironmentType = ViewControllerComponentContainer.Environment
+
+ let style: ChatTimerScreenStyle
+ let mode: ChatTimerScreenMode
+ let currentTime: Int32?
+ let dismiss: () -> Void
+
+ init(
+ style: ChatTimerScreenStyle,
+ mode: ChatTimerScreenMode,
+ currentTime: Int32?,
+ dismiss: @escaping () -> Void
+ ) {
+ self.style = style
+ self.mode = mode
+ self.currentTime = currentTime
+ self.dismiss = dismiss
+ }
+
+ static func ==(lhs: ChatTimerSheetContentComponent, rhs: ChatTimerSheetContentComponent) -> Bool {
+ if lhs.style != rhs.style {
+ return false
}
-
- self.effectNode = ASDisplayNode(viewBlock: {
- return UIVisualEffectView(effect: UIBlurEffect(style: blurStyle))
- })
-
- self.contentBackgroundNode = ASDisplayNode()
- self.contentBackgroundNode.backgroundColor = backgroundColor
-
- let title: String
- switch self.mode {
- case .sendTimer:
- title = self.presentationData.strings.Conversation_Timer_Title
- case .autoremove:
- title = self.presentationData.strings.Conversation_DeleteTimer_SetupTitle
- case .mute:
- title = self.presentationData.strings.Conversation_Mute_SetupTitle
+ if lhs.mode != rhs.mode {
+ return false
}
- self.titleNode = ASTextNode()
- self.titleNode.attributedText = NSAttributedString(string: title, font: Font.bold(17.0), textColor: textColor)
-
- self.textNode = ImmediateTextNode()
-
- self.cancelButton = HighlightableButtonNode()
- self.cancelButton.setTitle(self.presentationData.strings.Common_Cancel, with: Font.regular(17.0), with: accentColor, for: .normal)
-
- self.doneButton = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: self.presentationData.theme), height: 52.0, cornerRadius: 11.0, isShimmering: false)
- self.doneButton.title = self.presentationData.strings.Conversation_Timer_Send
-
- self.disableButton = HighlightableButtonNode()
- self.disableButtonTitle = ImmediateTextNode()
- self.disableButton.addSubnode(self.disableButtonTitle)
- self.disableButtonTitle.attributedText = NSAttributedString(string: self.presentationData.strings.Conversation_DeleteTimer_Disable, font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemAccentColor)
- self.disableButton.isHidden = true
-
- switch self.mode {
- case .autoremove:
- if self.initialTime != nil {
- self.disableButton.isHidden = false
+ if lhs.currentTime != rhs.currentTime {
+ return false
+ }
+ return true
+ }
+
+ final class View: UIView, UIPickerViewDataSource, UIPickerViewDelegate {
+ private let closeButton = ComponentView()
+ private let title = ComponentView()
+ private let primaryButton = ComponentView()
+ private let secondaryButton = ComponentView()
+
+ private var component: ChatTimerSheetContentComponent?
+ private var environment: EnvironmentType?
+ private weak var state: EmptyComponentState?
+
+ private var pickerView: TimerPickerView?
+ private var isCompleting = false
+
+ override init(frame: CGRect) {
+ super.init(frame: frame)
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ private func selectedValue() -> Int32? {
+ guard let component = self.component, let pickerView = self.pickerView else {
+ return nil
}
- default:
- break
- }
-
- self.autoremoveTimerValues = [
- 1 * 24 * 60 * 60 as Int32,
- 2 * 24 * 60 * 60 as Int32,
- 3 * 24 * 60 * 60 as Int32,
- 4 * 24 * 60 * 60 as Int32,
- 5 * 24 * 60 * 60 as Int32,
- 6 * 24 * 60 * 60 as Int32,
- 1 * 7 * 24 * 60 * 60 as Int32,
- 2 * 7 * 24 * 60 * 60 as Int32,
- 3 * 7 * 24 * 60 * 60 as Int32,
- 1 * 31 * 24 * 60 * 60 as Int32,
- 2 * 30 * 24 * 60 * 60 as Int32,
- 3 * 31 * 24 * 60 * 60 as Int32,
- 4 * 30 * 24 * 60 * 60 as Int32,
- 5 * 31 * 24 * 60 * 60 as Int32,
- 6 * 30 * 24 * 60 * 60 as Int32,
- 365 * 24 * 60 * 60 as Int32
- ]
-
- super.init()
-
- self.backgroundColor = nil
- self.isOpaque = false
-
- self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
- self.addSubnode(self.dimNode)
-
- self.wrappingScrollNode.view.delegate = self.wrappedScrollViewDelegate
- self.addSubnode(self.wrappingScrollNode)
-
- self.wrappingScrollNode.addSubnode(self.backgroundNode)
- self.wrappingScrollNode.addSubnode(self.contentContainerNode)
-
- self.backgroundNode.addSubnode(self.effectNode)
- self.backgroundNode.addSubnode(self.contentBackgroundNode)
- self.contentContainerNode.addSubnode(self.titleNode)
- self.contentContainerNode.addSubnode(self.textNode)
- self.contentContainerNode.addSubnode(self.cancelButton)
- self.contentContainerNode.addSubnode(self.doneButton)
- self.contentContainerNode.addSubnode(self.disableButton)
-
- self.cancelButton.addTarget(self, action: #selector(self.cancelButtonPressed), forControlEvents: .touchUpInside)
- self.doneButton.pressed = { [weak self] in
- if let strongSelf = self, let pickerView = strongSelf.pickerView {
- strongSelf.doneButton.isUserInteractionEnabled = false
- if let pickerView = pickerView as? TimerCustomPickerView {
- switch strongSelf.mode {
- case .sendTimer:
- let row = pickerView.selectedRow(inComponent: 0)
- let value: Int32
- if row == 0 {
- value = viewOnceTimeout
- } else {
- value = timerValues[row - 1]
- }
- strongSelf.completion?(value)
- case .autoremove:
- let timeInterval = strongSelf.autoremoveTimerValues[pickerView.selectedRow(inComponent: 0)]
- strongSelf.completion?(Int32(timeInterval))
- case .mute:
- break
- }
- } else if let pickerView = pickerView as? TimerDatePickerView {
- switch strongSelf.mode {
- case .mute:
- let timeInterval = max(0, Int32(pickerView.date.timeIntervalSince1970) - Int32(Date().timeIntervalSince1970))
- strongSelf.completion?(timeInterval)
- default:
- break
+
+ if let pickerView = pickerView as? TimerCustomPickerView {
+ switch component.mode {
+ case .sendTimer:
+ let row = pickerView.selectedRow(inComponent: 0)
+ if row == 0 {
+ return viewOnceTimeout
+ } else {
+ return timerValues[row - 1]
}
+ case .autoremove:
+ return autoremoveTimerValues[pickerView.selectedRow(inComponent: 0)]
+ case .mute:
+ return nil
}
- }
- }
-
- self.disableButton.addTarget(self, action: #selector(self.disableButtonPressed), forControlEvents: .touchUpInside)
-
- self.setupPickerView(currentTime: currentTime)
- }
-
- @objc private func disableButtonPressed() {
- self.completion?(0)
- }
-
- func setupPickerView(currentTime: Int32? = nil) {
- if let pickerView = self.pickerView {
- pickerView.removeFromSuperview()
- }
-
- switch self.mode {
- case .sendTimer:
- let pickerView = TimerCustomPickerView()
- pickerView.selectorColor = UIColor(rgb: 0xffffff, alpha: 0.18)
- pickerView.dataSource = self
- pickerView.delegate = self
-
- self.contentContainerNode.view.addSubview(pickerView)
- self.pickerView = pickerView
- case .autoremove:
- let pickerView = TimerCustomPickerView()
- pickerView.dataSource = self
- pickerView.delegate = self
-
- pickerView.selectorColor = self.presentationData.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.18)
-
- self.contentContainerNode.view.addSubview(pickerView)
- self.pickerView = pickerView
-
- if let value = self.initialTime {
- var selectedRowIndex = 0
- for i in 0 ..< self.autoremoveTimerValues.count {
- if self.autoremoveTimerValues[i] <= value {
- selectedRowIndex = i
- }
- }
-
- pickerView.selectRow(selectedRowIndex, inComponent: 0, animated: false)
- }
- case .mute:
- let pickerView = TimerDatePickerView()
- pickerView.locale = localeWithStrings(self.presentationData.strings)
- pickerView.datePickerMode = .dateAndTime
- pickerView.minimumDate = Date()
- if #available(iOS 13.4, *) {
- pickerView.preferredDatePickerStyle = .wheels
- }
- pickerView.setValue(self.presentationData.theme.list.itemPrimaryTextColor, forKey: "textColor")
- pickerView.setValue(false, forKey: "highlightsToday")
- pickerView.selectorColor = UIColor(rgb: 0xffffff, alpha: 0.18)
- pickerView.addTarget(self, action: #selector(self.dataPickerChanged), for: .valueChanged)
-
- self.contentContainerNode.view.addSubview(pickerView)
- self.pickerView = pickerView
- }
- }
-
- @objc private func dataPickerChanged() {
- if let (layout, navigationBarHeight) = self.containerLayout {
- self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
- }
- }
-
- func numberOfComponents(in pickerView: UIPickerView) -> Int {
- switch self.mode {
- case .sendTimer:
- return 1
- case .autoremove:
- return 1
- case .mute:
- return 0
- }
- }
-
- func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
- switch self.mode {
- case .sendTimer:
- return timerValues.count + 1
- case .autoremove:
- return self.autoremoveTimerValues.count
- case .mute:
- return 0
- }
- }
-
- func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView {
- switch self.mode {
- case .sendTimer:
- if row == 0 {
- let string = self.presentationData.strings.MediaPicker_Timer_ViewOnce
- if let view = view as? TimerPickerItemView {
- view.value = (viewOnceTimeout, string)
- return view
- }
-
- let view = TimerPickerItemView()
- view.value = (viewOnceTimeout, string)
- view.textColor = .white
- return view
+ } else if let pickerView = pickerView as? TimerDatePickerView {
+ return Int32(pickerView.date.timeIntervalSince1970)
} else {
- let value = timerValues[row - 1]
- let string = timeIntervalString(strings: self.presentationData.strings, value: value)
- if let view = view as? TimerPickerItemView {
- view.value = (value, string)
- return view
- }
-
- let view = TimerPickerItemView()
- view.value = (value, string)
- view.textColor = .white
- return view
+ return nil
}
- case .autoremove:
+ }
+
+ private func pickerTextColor(component: ChatTimerSheetContentComponent, environment: EnvironmentType) -> UIColor {
+ switch component.mode {
+ case .sendTimer:
+ return .white
+ case .autoremove:
+ if case .media = component.style {
+ return .white
+ } else {
+ return environment.theme.list.itemPrimaryTextColor
+ }
+ case .mute:
+ if case .media = component.style {
+ return .white
+ } else {
+ return environment.theme.list.itemPrimaryTextColor
+ }
+ }
+ }
+
+ private func selectAutoremoveValue(_ value: Int32, in pickerView: TimerCustomPickerView) {
+ var selectedRowIndex = 0
+ for i in 0 ..< autoremoveTimerValues.count {
+ if autoremoveTimerValues[i] <= value {
+ selectedRowIndex = i
+ }
+ }
+ pickerView.selectRow(selectedRowIndex, inComponent: 0, animated: false)
+ }
+
+ private func setupPickerView(component: ChatTimerSheetContentComponent, environment: EnvironmentType) {
+ let previousSelectedValue = self.selectedValue()
+ let previousDate = (self.pickerView as? TimerDatePickerView)?.date
+
+ if let pickerView = self.pickerView {
+ pickerView.removeFromSuperview()
+ }
+
+ switch component.mode {
+ case .sendTimer:
+ let pickerView = TimerCustomPickerView()
+ pickerView.selectorColor = UIColor(rgb: 0xffffff, alpha: 0.18)
+ pickerView.dataSource = self
+ pickerView.delegate = self
+ self.addSubview(pickerView)
+ self.pickerView = pickerView
+
+ if let previousSelectedValue {
+ if previousSelectedValue == viewOnceTimeout {
+ pickerView.selectRow(0, inComponent: 0, animated: false)
+ } else if let index = timerValues.firstIndex(of: previousSelectedValue) {
+ pickerView.selectRow(index + 1, inComponent: 0, animated: false)
+ }
+ }
+ case .autoremove:
+ let pickerView = TimerCustomPickerView()
+ pickerView.dataSource = self
+ pickerView.delegate = self
+ pickerView.selectorColor = self.pickerTextColor(component: component, environment: environment).withMultipliedAlpha(0.18)
+ self.addSubview(pickerView)
+ self.pickerView = pickerView
+
+ if let previousSelectedValue {
+ self.selectAutoremoveValue(previousSelectedValue, in: pickerView)
+ } else if let currentTime = component.currentTime {
+ self.selectAutoremoveValue(currentTime, in: pickerView)
+ }
+ case .mute:
+ let pickerView = TimerDatePickerView()
+ pickerView.locale = localeWithStrings(environment.strings)
+ pickerView.datePickerMode = .dateAndTime
+ pickerView.minimumDate = Date()
+ if #available(iOS 13.4, *) {
+ pickerView.preferredDatePickerStyle = .wheels
+ }
+ pickerView.setValue(self.pickerTextColor(component: component, environment: environment), forKey: "textColor")
+ pickerView.setValue(false, forKey: "highlightsToday")
+ pickerView.selectorColor = UIColor(rgb: 0xffffff, alpha: 0.18)
+ pickerView.addTarget(self, action: #selector(self.datePickerChanged), for: .valueChanged)
+ if let previousDate {
+ pickerView.date = max(previousDate, Date())
+ }
+ self.addSubview(pickerView)
+ self.pickerView = pickerView
+ }
+ }
+
+ @objc private func datePickerChanged() {
+ self.state?.updated(transition: .immediate)
+ }
+
+ private func title(strings: PresentationStrings) -> String {
+ guard let component = self.component else {
+ return ""
+ }
+
+ switch component.mode {
+ case .sendTimer:
+ return strings.Conversation_Timer_Title
+ case .autoremove:
+ return strings.Conversation_DeleteTimer_SetupTitle
+ case .mute:
+ return strings.Conversation_Mute_SetupTitle
+ }
+ }
+
+ private func primaryButtonTitle(component: ChatTimerSheetContentComponent, environment: EnvironmentType) -> String {
+ switch component.mode {
+ case .sendTimer:
+ return environment.strings.Conversation_Timer_Send
+ case .autoremove:
+ return environment.strings.Conversation_DeleteTimer_Apply
+ case .mute:
+ if let pickerView = self.pickerView as? TimerDatePickerView {
+ let now = Int32(Date().timeIntervalSince1970)
+ let timeInterval = max(0, Int32(pickerView.date.timeIntervalSince1970) - now)
+
+ if timeInterval > 0 {
+ let timeString = stringForPreciseRelativeTimestamp(strings: environment.strings, relativeTimestamp: Int32(pickerView.date.timeIntervalSince1970), relativeTo: now, dateTimeFormat: environment.dateTimeFormat)
+ return environment.strings.Conversation_Mute_ApplyMuteUntil(timeString).string
+ } else {
+ return environment.strings.Common_Close
+ }
+ } else {
+ return environment.strings.Common_Close
+ }
+ }
+ }
+
+ private func complete(value: Int32) {
+ guard !self.isCompleting else {
+ return
+ }
+ self.isCompleting = true
+
+ if let controller = self.environment?.controller() as? ChatTimerScreen {
+ controller.completion(value)
+ }
+ self.component?.dismiss()
+ }
+
+ private func completeWithPickerValue() {
+ guard let component = self.component, let pickerView = self.pickerView else {
+ return
+ }
+
+ if let pickerView = pickerView as? TimerCustomPickerView {
+ switch component.mode {
+ case .sendTimer:
+ let row = pickerView.selectedRow(inComponent: 0)
+ let value: Int32
+ if row == 0 {
+ value = viewOnceTimeout
+ } else {
+ value = timerValues[row - 1]
+ }
+ self.complete(value: value)
+ case .autoremove:
+ self.complete(value: autoremoveTimerValues[pickerView.selectedRow(inComponent: 0)])
+ case .mute:
+ break
+ }
+ } else if let pickerView = pickerView as? TimerDatePickerView {
+ switch component.mode {
+ case .mute:
+ let timeInterval = max(0, Int32(pickerView.date.timeIntervalSince1970) - Int32(Date().timeIntervalSince1970))
+ self.complete(value: timeInterval)
+ default:
+ break
+ }
+ }
+ }
+
+ func update(component: ChatTimerSheetContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize {
+ let environment = environment[EnvironmentType.self].value
+ let previousComponent = self.component
+ let previousEnvironment = self.environment
+ let themeUpdated: Bool
+ let stringsUpdated: Bool
+ if let previousEnvironment {
+ themeUpdated = previousEnvironment.theme !== environment.theme
+ stringsUpdated = previousEnvironment.strings !== environment.strings
+ } else {
+ themeUpdated = false
+ stringsUpdated = false
+ }
+
+ self.component = component
+ self.environment = environment
+ self.state = state
+
+ if self.pickerView == nil || previousComponent?.mode != component.mode || previousComponent?.style != component.style || themeUpdated || stringsUpdated {
+ self.setupPickerView(component: component, environment: environment)
+ }
+
+ let titleColor: UIColor
+ switch component.style {
+ case .default:
+ titleColor = environment.theme.actionSheet.primaryTextColor
+ case .media:
+ titleColor = .white
+ }
+
+ let barButtonSize = CGSize(width: 44.0, height: 44.0)
+ let closeButtonSize = self.closeButton.update(
+ transition: transition,
+ component: AnyComponent(
+ GlassBarButtonComponent(
+ size: barButtonSize,
+ backgroundColor: nil,
+ isDark: environment.theme.overallDarkAppearance,
+ state: .glass,
+ component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
+ BundleIconComponent(
+ name: "Navigation/Close",
+ tintColor: environment.theme.chat.inputPanel.panelControlColor
+ )
+ )),
+ action: { [weak self] _ in
+ self?.component?.dismiss()
+ }
+ )
+ ),
+ environment: {},
+ containerSize: barButtonSize
+ )
+ if let closeButtonView = self.closeButton.view {
+ if closeButtonView.superview == nil {
+ self.addSubview(closeButtonView)
+ }
+ transition.setFrame(view: closeButtonView, frame: CGRect(origin: CGPoint(x: 16.0, y: 16.0), size: closeButtonSize))
+ }
+
+ let titleSize = self.title.update(
+ transition: transition,
+ component: AnyComponent(
+ Text(text: self.title(strings: environment.strings), font: Font.semibold(17.0), color: titleColor)
+ ),
+ environment: {},
+ containerSize: CGSize(width: availableSize.width - 120.0, height: 44.0)
+ )
+ if let titleView = self.title.view {
+ if titleView.superview == nil {
+ self.addSubview(titleView)
+ }
+ transition.setFrame(view: titleView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - titleSize.width) / 2.0), y: floorToScreenPixels(16.0 + (barButtonSize.height - titleSize.height) / 2.0)), size: titleSize))
+ }
+
+ var contentHeight: CGFloat = 68.0
+
+ let pickerHeight: CGFloat = 216.0
+ if let pickerView = self.pickerView {
+ transition.setFrame(view: pickerView as UIView, frame: CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: availableSize.width, height: pickerHeight)))
+ }
+ contentHeight += pickerHeight
+ contentHeight += 17.0
+
+ let buttonSideInset: CGFloat = 30.0
+ let primaryButtonTitle = self.primaryButtonTitle(component: component, environment: environment)
+ let primaryButtonSize = self.primaryButton.update(
+ transition: transition,
+ component: AnyComponent(ButtonComponent(
+ background: ButtonComponent.Background(
+ style: .glass,
+ color: environment.theme.list.itemCheckColors.fillColor,
+ foreground: environment.theme.list.itemCheckColors.foregroundColor,
+ pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8)
+ ),
+ content: AnyComponentWithIdentity(id: AnyHashable(primaryButtonTitle), component: AnyComponent(
+ Text(text: primaryButtonTitle, font: Font.semibold(17.0), color: environment.theme.list.itemCheckColors.foregroundColor)
+ )),
+ isEnabled: true,
+ displaysProgress: false,
+ action: { [weak self] in
+ self?.completeWithPickerValue()
+ }
+ )),
+ environment: {},
+ containerSize: CGSize(width: availableSize.width - buttonSideInset * 2.0, height: 52.0)
+ )
+ if let primaryButtonView = self.primaryButton.view {
+ if primaryButtonView.superview == nil {
+ self.addSubview(primaryButtonView)
+ }
+ transition.setFrame(view: primaryButtonView, frame: CGRect(origin: CGPoint(x: buttonSideInset, y: contentHeight), size: primaryButtonSize))
+ }
+ contentHeight += primaryButtonSize.height
+
+ if case .autoremove = component.mode, component.currentTime != nil {
+ contentHeight += 8.0
+
+ let secondaryButtonTitle = environment.strings.Conversation_DeleteTimer_Disable
+ let secondaryButtonSize = self.secondaryButton.update(
+ transition: transition,
+ component: AnyComponent(ButtonComponent(
+ background: ButtonComponent.Background(
+ style: .glass,
+ color: environment.theme.list.itemDestructiveColor.withMultipliedAlpha(0.1),
+ foreground: environment.theme.list.itemDestructiveColor,
+ pressedColor: environment.theme.list.itemDestructiveColor.withMultipliedAlpha(0.8)
+ ),
+ content: AnyComponentWithIdentity(id: AnyHashable(secondaryButtonTitle), component: AnyComponent(
+ Text(text: secondaryButtonTitle, font: Font.semibold(17.0), color: environment.theme.list.itemDestructiveColor)
+ )),
+ isEnabled: true,
+ displaysProgress: false,
+ action: { [weak self] in
+ self?.complete(value: 0)
+ }
+ )),
+ environment: {},
+ containerSize: CGSize(width: availableSize.width - buttonSideInset * 2.0, height: 52.0)
+ )
+ if let secondaryButtonView = self.secondaryButton.view {
+ if secondaryButtonView.superview == nil {
+ self.addSubview(secondaryButtonView)
+ }
+ transition.setFrame(view: secondaryButtonView, frame: CGRect(origin: CGPoint(x: buttonSideInset, y: contentHeight), size: secondaryButtonSize))
+ }
+ contentHeight += secondaryButtonSize.height
+ } else if let secondaryButtonView = self.secondaryButton.view, secondaryButtonView.superview != nil {
+ secondaryButtonView.removeFromSuperview()
+ }
+
+ let bottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 5.0 : 15.0
+ contentHeight += bottomInset
+
+ return CGSize(width: availableSize.width, height: contentHeight)
+ }
+
+ func numberOfComponents(in pickerView: UIPickerView) -> Int {
+ return 1
+ }
+
+ func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
+ guard let component = self.component else {
+ return 0
+ }
+
+ switch component.mode {
+ case .sendTimer:
+ return timerValues.count + 1
+ case .autoremove:
+ return autoremoveTimerValues.count
+ case .mute:
+ return 0
+ }
+ }
+
+ func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent componentIndex: Int, reusing view: UIView?) -> UIView {
+ guard let component = self.component, let environment = self.environment else {
+ return UIView()
+ }
+
let itemView: TimerPickerItemView
if let current = view as? TimerPickerItemView {
itemView = current
} else {
itemView = TimerPickerItemView()
- itemView.textColor = self.presentationData.theme.list.itemPrimaryTextColor
}
-
- let value = self.autoremoveTimerValues[row]
-
- let string: String
- string = timeIntervalString(strings: self.presentationData.strings, value: value)
-
- itemView.value = (value, string)
-
- return itemView
- case .mute:
- preconditionFailure()
- }
- }
-
- func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
- self.dataPickerChanged()
- }
-
- func updatePresentationData(_ presentationData: PresentationData) {
- let previousTheme = self.presentationData.theme
- self.presentationData = presentationData
-
- guard case .default = self.controllerStyle else {
- return
- }
-
- if let effectView = self.effectNode.view as? UIVisualEffectView {
- effectView.effect = UIBlurEffect(style: presentationData.theme.actionSheet.backgroundType == .light ? .light : .dark)
- }
-
- self.contentBackgroundNode.backgroundColor = self.presentationData.theme.actionSheet.itemBackgroundColor
- self.titleNode.attributedText = NSAttributedString(string: self.titleNode.attributedText?.string ?? "", font: Font.bold(17.0), textColor: self.presentationData.theme.actionSheet.primaryTextColor)
-
- if previousTheme !== presentationData.theme, let (layout, navigationBarHeight) = self.containerLayout {
- self.setupPickerView()
- self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
- }
-
- self.cancelButton.setTitle(self.presentationData.strings.Common_Cancel, with: Font.regular(17.0), with: self.presentationData.theme.actionSheet.controlAccentColor, for: .normal)
- self.doneButton.updateTheme(SolidRoundedButtonTheme(theme: self.presentationData.theme))
- }
-
- override func didLoad() {
- super.didLoad()
-
- if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
- self.wrappingScrollNode.view.contentInsetAdjustmentBehavior = .never
- }
- }
-
- @objc func cancelButtonPressed() {
- self.cancel?()
- }
-
- @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
- if self.dismissByTapOutside, case .ended = recognizer.state {
- self.cancelButtonPressed()
- }
- }
-
- func animateIn() {
- self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
-
- let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY
- let dimPosition = self.dimNode.layer.position
-
- let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
- let targetBounds = self.bounds
- self.bounds = self.bounds.offsetBy(dx: 0.0, dy: -offset)
- self.dimNode.position = CGPoint(x: dimPosition.x, y: dimPosition.y - offset)
- transition.animateView({
- self.bounds = targetBounds
- self.dimNode.position = dimPosition
- })
- }
-
- func animateOut(completion: (() -> Void)? = nil) {
- var dimCompleted = false
- var offsetCompleted = false
-
- let internalCompletion: () -> Void = { [weak self] in
- if let strongSelf = self, dimCompleted && offsetCompleted {
- strongSelf.dismiss?()
- }
- completion?()
- }
-
- self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in
- dimCompleted = true
- internalCompletion()
- })
-
- let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY
- let dimPosition = self.dimNode.layer.position
- self.dimNode.layer.animatePosition(from: dimPosition, to: CGPoint(x: dimPosition.x, y: dimPosition.y - offset), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
- self.layer.animateBoundsOriginYAdditive(from: 0.0, to: -offset, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
- offsetCompleted = true
- internalCompletion()
- })
- }
-
- override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
- if self.bounds.contains(point) {
- if !self.contentBackgroundNode.bounds.contains(self.convert(point, to: self.contentBackgroundNode)) {
- return self.dimNode.view
- }
- }
- return super.hitTest(point, with: event)
- }
-
- func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
- let contentOffset = scrollView.contentOffset
- let additionalTopHeight = max(0.0, -contentOffset.y)
-
- if additionalTopHeight >= 30.0 {
- self.cancelButtonPressed()
- }
- }
-
- func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
- self.containerLayout = (layout, navigationBarHeight)
-
- var insets = layout.insets(options: [.statusBar, .input])
- let cleanInsets = layout.insets(options: [.statusBar])
- insets.top = max(10.0, insets.top)
-
- var buttonOffset: CGFloat = 0.0
- let bottomInset: CGFloat = 10.0 + cleanInsets.bottom
- let titleHeight: CGFloat = 54.0
- var contentHeight = titleHeight + bottomInset + 52.0 + 17.0
- let pickerHeight: CGFloat = min(216.0, layout.size.height - contentHeight)
-
- if !self.disableButton.isHidden {
- buttonOffset += 52.0
- }
-
- contentHeight = titleHeight + bottomInset + 52.0 + 17.0 + pickerHeight + buttonOffset
-
- let width = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: 0.0)
-
- let sideInset = floor((layout.size.width - width) / 2.0)
- let contentContainerFrame = CGRect(origin: CGPoint(x: sideInset, y: layout.size.height - contentHeight), size: CGSize(width: width, height: contentHeight))
- let contentFrame = contentContainerFrame
-
- var backgroundFrame = CGRect(origin: CGPoint(x: contentFrame.minX, y: contentFrame.minY), size: CGSize(width: contentFrame.width, height: contentFrame.height + 2000.0))
- if backgroundFrame.minY < contentFrame.minY {
- backgroundFrame.origin.y = contentFrame.minY
- }
- transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame)
- transition.updateFrame(node: self.effectNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size))
- transition.updateFrame(node: self.contentBackgroundNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size))
- transition.updateFrame(node: self.wrappingScrollNode, frame: CGRect(origin: CGPoint(), size: layout.size))
- transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size))
-
- let titleSize = self.titleNode.measure(CGSize(width: width, height: titleHeight))
- let titleFrame = CGRect(origin: CGPoint(x: floor((contentFrame.width - titleSize.width) / 2.0), y: 16.0), size: titleSize)
- transition.updateFrame(node: self.titleNode, frame: titleFrame)
-
- let cancelSize = self.cancelButton.measure(CGSize(width: width, height: titleHeight))
- let cancelFrame = CGRect(origin: CGPoint(x: 16.0, y: 16.0), size: cancelSize)
- transition.updateFrame(node: self.cancelButton, frame: cancelFrame)
-
- let buttonInset: CGFloat = 16.0
-
- switch self.mode {
- case .sendTimer:
- break
- case .autoremove:
- self.doneButton.title = self.presentationData.strings.Conversation_DeleteTimer_Apply
- case .mute:
- if let pickerView = self.pickerView as? TimerDatePickerView {
- let timeInterval = max(0, Int32(pickerView.date.timeIntervalSince1970) - Int32(Date().timeIntervalSince1970))
-
- if timeInterval > 0 {
- let timeString = stringForPreciseRelativeTimestamp(strings: self.presentationData.strings, relativeTimestamp: Int32(pickerView.date.timeIntervalSince1970), relativeTo: Int32(Date().timeIntervalSince1970), dateTimeFormat: self.presentationData.dateTimeFormat)
-
- self.doneButton.title = self.presentationData.strings.Conversation_Mute_ApplyMuteUntil(timeString).string
+ itemView.textColor = self.pickerTextColor(component: component, environment: environment)
+
+ switch component.mode {
+ case .sendTimer:
+ if row == 0 {
+ let string = environment.strings.MediaPicker_Timer_ViewOnce
+ itemView.value = (viewOnceTimeout, string)
} else {
- self.doneButton.title = self.presentationData.strings.Common_Close
+ let value = timerValues[row - 1]
+ let string = timeIntervalString(strings: environment.strings, value: value)
+ itemView.value = (value, string)
}
- } else {
- self.doneButton.title = self.presentationData.strings.Common_Close
+ case .autoremove:
+ let value = autoremoveTimerValues[row]
+ let string = timeIntervalString(strings: environment.strings, value: value)
+ itemView.value = (value, string)
+ case .mute:
+ preconditionFailure()
}
+
+ return itemView
+ }
+
+ func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
+ self.state?.updated(transition: .immediate)
+ }
+ }
+
+ func makeView() -> View {
+ return View(frame: CGRect())
+ }
+
+ func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize {
+ return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
+ }
+}
+
+private final class ChatTimerSheetComponent: Component {
+ typealias EnvironmentType = ViewControllerComponentContainer.Environment
+
+ let style: ChatTimerScreenStyle
+ let mode: ChatTimerScreenMode
+ let currentTime: Int32?
+
+ init(
+ style: ChatTimerScreenStyle,
+ mode: ChatTimerScreenMode,
+ currentTime: Int32?
+ ) {
+ self.style = style
+ self.mode = mode
+ self.currentTime = currentTime
+ }
+
+ static func ==(lhs: ChatTimerSheetComponent, rhs: ChatTimerSheetComponent) -> Bool {
+ if lhs.style != rhs.style {
+ return false
+ }
+ if lhs.mode != rhs.mode {
+ return false
+ }
+ if lhs.currentTime != rhs.currentTime {
+ return false
+ }
+ return true
+ }
+
+ final class View: UIView {
+ private let sheet = ComponentView<(ViewControllerComponentContainer.Environment, SheetComponentEnvironment)>()
+ private let sheetAnimateOut = ActionSlot>()
+
+ private var component: ChatTimerSheetComponent?
+ private var environment: EnvironmentType?
+
+ override init(frame: CGRect) {
+ super.init(frame: frame)
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ private func dismiss() {
+ self.sheetAnimateOut.invoke(Action { [weak self] _ in
+ guard let self, let controller = self.environment?.controller() else {
+ return
+ }
+ controller.dismiss(completion: nil)
+ })
+ }
+
+ func update(component: ChatTimerSheetComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize {
+ self.component = component
+
+ let environment = environment[ViewControllerComponentContainer.Environment.self].value
+ self.environment = environment
+
+ let sheetEnvironment = SheetComponentEnvironment(
+ metrics: environment.metrics,
+ deviceMetrics: environment.deviceMetrics,
+ isDisplaying: environment.isVisible,
+ isCentered: environment.metrics.widthClass == .regular,
+ hasInputHeight: !environment.inputHeight.isZero,
+ regularMetricsSize: CGSize(width: 430.0, height: 900.0),
+ dismiss: { [weak self] _ in
+ self?.dismiss()
+ }
+ )
+
+ let backgroundColor: UIColor
+ switch component.style {
+ case .default:
+ backgroundColor = environment.theme.actionSheet.opaqueItemBackgroundColor
+ case .media:
+ backgroundColor = UIColor(rgb: 0x1c1c1e)
+ }
+
+ let _ = self.sheet.update(
+ transition: transition,
+ component: AnyComponent(SheetComponent(
+ content: AnyComponent(ChatTimerSheetContentComponent(
+ style: component.style,
+ mode: component.mode,
+ currentTime: component.currentTime,
+ dismiss: { [weak self] in
+ self?.dismiss()
+ }
+ )),
+ style: .glass,
+ backgroundColor: .color(backgroundColor),
+ followContentSizeChanges: true,
+ animateOut: self.sheetAnimateOut
+ )),
+ environment: {
+ environment
+ sheetEnvironment
+ },
+ containerSize: availableSize
+ )
+ if let sheetView = self.sheet.view {
+ if sheetView.superview == nil {
+ self.addSubview(sheetView)
+ }
+ transition.setFrame(view: sheetView, frame: CGRect(origin: CGPoint(), size: availableSize))
+ }
+
+ return availableSize
+ }
+ }
+
+ func makeView() -> View {
+ return View(frame: CGRect())
+ }
+
+ func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize {
+ return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
+ }
+}
+
+public final class ChatTimerScreen: ViewControllerComponentContainer {
+ fileprivate let completion: (Int32) -> Void
+
+ public init(
+ context: AccountContext,
+ updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil,
+ style: ChatTimerScreenStyle,
+ mode: ChatTimerScreenMode = .sendTimer,
+ currentTime: Int32? = nil,
+ completion: @escaping (Int32) -> Void
+ ) {
+ self.completion = completion
+
+ super.init(
+ context: context,
+ component: ChatTimerSheetComponent(
+ style: style,
+ mode: mode,
+ currentTime: currentTime
+ ),
+ navigationBarAppearance: .none,
+ statusBarStyle: .ignore,
+ theme: style == .media ? .dark : .default,
+ updatedPresentationData: updatedPresentationData
+ )
+
+ self.statusBar.statusBarStyle = .Ignore
+ self.navigationPresentation = .flatModal
+ self.blocksBackgroundWhenInOverlay = true
+ }
+
+ required public init(coder aDecoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override public func viewDidAppear(_ animated: Bool) {
+ super.viewDidAppear(animated)
+
+ self.view.disablesInteractiveModalDismiss = true
+ }
+
+ public func dismissAnimated() {
+ if let view = self.node.hostView.findTaggedView(tag: SheetComponent.View.Tag()) as? SheetComponent.View {
+ view.dismissAnimated()
}
-
- let doneButtonHeight = self.doneButton.updateLayout(width: contentFrame.width - buttonInset * 2.0, transition: transition)
- let doneButtonFrame = CGRect(x: buttonInset, y: contentHeight - doneButtonHeight - insets.bottom - 16.0 - buttonOffset, width: contentFrame.width, height: doneButtonHeight)
- transition.updateFrame(node: self.doneButton, frame: doneButtonFrame)
-
- let disableButtonTitleSize = self.disableButtonTitle.updateLayout(CGSize(width: contentFrame.width, height: doneButtonHeight))
- let disableButtonFrame = CGRect(origin: CGPoint(x: doneButtonFrame.minX, y: doneButtonFrame.maxY), size: CGSize(width: contentFrame.width - buttonInset * 2.0, height: doneButtonHeight))
- transition.updateFrame(node: self.disableButton, frame: disableButtonFrame)
- transition.updateFrame(node: self.disableButtonTitle, frame: CGRect(origin: CGPoint(x: floor((disableButtonFrame.width - disableButtonTitleSize.width) / 2.0), y: floor((disableButtonFrame.height - disableButtonTitleSize.height) / 2.0)), size: disableButtonTitleSize))
-
- self.pickerView?.frame = CGRect(origin: CGPoint(x: 0.0, y: 54.0), size: CGSize(width: contentFrame.width, height: pickerHeight))
-
- transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame)
}
}
diff --git a/submodules/TelegramUI/Components/ComposePollScreen/Sources/ComposePollScreen.swift b/submodules/TelegramUI/Components/ComposePollScreen/Sources/ComposePollScreen.swift
index efac1db824..42855d33c5 100644
--- a/submodules/TelegramUI/Components/ComposePollScreen/Sources/ComposePollScreen.swift
+++ b/submodules/TelegramUI/Components/ComposePollScreen/Sources/ComposePollScreen.swift
@@ -1498,7 +1498,6 @@ final class ComposePollScreenComponent: Component {
pollTextSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListComposePollOptionComponent(
externalState: self.pollTextInputState,
context: component.context,
- style: .glass,
theme: theme,
strings: environment.strings,
resetText: self.resetPollText.flatMap { resetText in
@@ -1534,7 +1533,6 @@ final class ComposePollScreenComponent: Component {
pollTextSectionItems.append(AnyComponentWithIdentity(id: 1, component: AnyComponent(ListComposePollOptionComponent(
externalState: self.pollDescriptionInputState,
context: component.context,
- style: .glass,
theme: theme,
strings: environment.strings,
resetText: nil,
@@ -1650,7 +1648,6 @@ final class ComposePollScreenComponent: Component {
pollOptionsSectionItems.append(AnyComponentWithIdentity(id: pollOption.id, component: AnyComponent(ListComposePollOptionComponent(
externalState: pollOption.textInputState,
context: component.context,
- style: .glass,
theme: theme,
strings: environment.strings,
resetText: pollOption.resetText.flatMap { resetText in
@@ -2499,7 +2496,6 @@ final class ComposePollScreenComponent: Component {
AnyComponentWithIdentity(id: 0, component: AnyComponent(ListComposePollOptionComponent(
externalState: self.quizAnswerTextInputState,
context: component.context,
- style: .glass,
theme: theme,
strings: environment.strings,
resetText: self.resetQuizAnswerText.flatMap { resetText in
diff --git a/submodules/TelegramUI/Components/ComposeTodoScreen/Sources/ComposeTodoScreen.swift b/submodules/TelegramUI/Components/ComposeTodoScreen/Sources/ComposeTodoScreen.swift
index d31acdf3c2..3002238bac 100644
--- a/submodules/TelegramUI/Components/ComposeTodoScreen/Sources/ComposeTodoScreen.swift
+++ b/submodules/TelegramUI/Components/ComposeTodoScreen/Sources/ComposeTodoScreen.swift
@@ -784,7 +784,6 @@ final class ComposeTodoScreenComponent: Component {
todoTextSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListComposePollOptionComponent(
externalState: self.todoTextInputState,
context: component.context,
- style: .glass,
theme: theme,
strings: environment.strings,
isEnabled: canEdit,
@@ -874,7 +873,6 @@ final class ComposeTodoScreenComponent: Component {
todoItemsSectionItems.append(AnyComponentWithIdentity(id: todoItem.id, component: AnyComponent(ListComposePollOptionComponent(
externalState: todoItem.textInputState,
context: component.context,
- style: .glass,
theme: theme,
strings: environment.strings,
isEnabled: isEnabled,
diff --git a/submodules/TelegramUI/Components/Contacts/NewContactScreen/Sources/NewContactScreen.swift b/submodules/TelegramUI/Components/Contacts/NewContactScreen/Sources/NewContactScreen.swift
index 6bf5bc0e66..834c0c56fc 100644
--- a/submodules/TelegramUI/Components/Contacts/NewContactScreen/Sources/NewContactScreen.swift
+++ b/submodules/TelegramUI/Components/Contacts/NewContactScreen/Sources/NewContactScreen.swift
@@ -716,7 +716,6 @@ final class NewContactScreenComponent: Component {
ListComposePollOptionComponent(
externalState: nil,
context: component.context,
- style: .glass,
theme: theme,
strings: strings,
placeholder: NSAttributedString(string: strings.AddContact_NotePlaceholder, font: Font.regular(17.0), textColor: theme.list.itemPlaceholderTextColor),
diff --git a/submodules/TelegramUI/Components/ListComposePollOptionComponent/Sources/ListComposePollOptionComponent.swift b/submodules/TelegramUI/Components/ListComposePollOptionComponent/Sources/ListComposePollOptionComponent.swift
index ab3cced9f4..7a36bed129 100644
--- a/submodules/TelegramUI/Components/ListComposePollOptionComponent/Sources/ListComposePollOptionComponent.swift
+++ b/submodules/TelegramUI/Components/ListComposePollOptionComponent/Sources/ListComposePollOptionComponent.swift
@@ -22,11 +22,6 @@ import EmojiTextAttachmentView
import TextFormat
public final class ListComposePollOptionComponent: Component {
- public enum Style {
- case glass
- case legacy
- }
-
public final class ResetText: Equatable {
public let value: NSAttributedString
@@ -117,7 +112,6 @@ public final class ListComposePollOptionComponent: Component {
public let externalState: TextFieldComponent.ExternalState?
public let context: AccountContext
- public let style: Style
public let theme: PresentationTheme
public let strings: PresentationStrings
public let placeholder: NSAttributedString?
@@ -148,7 +142,6 @@ public final class ListComposePollOptionComponent: Component {
public init(
externalState: TextFieldComponent.ExternalState?,
context: AccountContext,
- style: Style = .legacy,
theme: PresentationTheme,
strings: PresentationStrings,
placeholder: NSAttributedString? = nil,
@@ -178,7 +171,6 @@ public final class ListComposePollOptionComponent: Component {
) {
self.externalState = externalState
self.context = context
- self.style = style
self.theme = theme
self.strings = strings
self.placeholder = placeholder
@@ -214,9 +206,6 @@ public final class ListComposePollOptionComponent: Component {
if lhs.context !== rhs.context {
return false
}
- if lhs.style != rhs.style {
- return false
- }
if lhs.theme !== rhs.theme {
return false
}
@@ -690,10 +679,7 @@ public final class ListComposePollOptionComponent: Component {
self.component = component
self.state = state
- var verticalInset: CGFloat = 12.0
- if case .glass = component.style {
- verticalInset = 16.0
- }
+ let verticalInset: CGFloat = 16.0
var leftInset: CGFloat = 16.0
var rightInset: CGFloat = 16.0
let modeSelectorSize = CGSize(width: 32.0, height: 32.0)
@@ -1372,10 +1358,7 @@ public final class ListComposePollOptionComponent: Component {
return
}
- var verticalInset: CGFloat = 12.0
- if case .glass = component.style {
- verticalInset = 16.0
- }
+ let verticalInset: CGFloat = 16.0
var leftInset: CGFloat = 16.0
let rightInset: CGFloat = 16.0
diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift
index ef4fa49c03..bb01e62bc9 100644
--- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift
+++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift
@@ -3056,7 +3056,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}
func openAutoremove(currentValue: Int32?) {
- let controller = ChatTimerScreen(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, style: .default, mode: .autoremove, currentTime: currentValue, dismissByTapOutside: true, completion: { [weak self] value in
+ let controller = ChatTimerScreen(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, style: .default, mode: .autoremove, currentTime: currentValue, completion: { [weak self] value in
guard let strongSelf = self else {
return
}
@@ -3085,7 +3085,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}
func openCustomMute() {
- let controller = ChatTimerScreen(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, style: .default, mode: .mute, currentTime: nil, dismissByTapOutside: true, completion: { [weak self] value in
+ let controller = ChatTimerScreen(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, style: .default, mode: .mute, currentTime: nil, completion: { [weak self] value in
guard let strongSelf = self, let peer = strongSelf.data?.peer else {
return
}
diff --git a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift
index 72d49bd086..51907ac87e 100644
--- a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift
+++ b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift
@@ -466,13 +466,10 @@ final class PeerSelectionControllerNode: ASDisplayNode {
}
var isDice = false
- var isMusic = false
for media in message.media {
- if let media = media as? TelegramMediaFile, media.isMusic {
- isMusic = true
- } else if media is TelegramMediaDice {
+ if media is TelegramMediaDice {
isDice = true
- } else {
+ } else if (media as? TelegramMediaFile)?.isMusic != true {
if !message.text.isEmpty {
if media is TelegramMediaImage || media is TelegramMediaFile {
hasCaptions = true
@@ -480,7 +477,7 @@ final class PeerSelectionControllerNode: ASDisplayNode {
}
}
}
- if !isDice && !isMusic {
+ if !isDice {
hasOther = true
}
}
diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift
index f7bfd955df..36cce86732 100644
--- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift
+++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift
@@ -2914,12 +2914,12 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) {
})
}
- private func presentTimerPicker(view: StoryItemSetContainerComponent.View, peer: EnginePeer, style: ChatTimerScreenStyle = .default, selectedTime: Int32? = nil, dismissByTapOutside: Bool = true, completion: @escaping (Int32) -> Void) {
+ private func presentTimerPicker(view: StoryItemSetContainerComponent.View, peer: EnginePeer, style: ChatTimerScreenStyle = .default, selectedTime: Int32? = nil, completion: @escaping (Int32) -> Void) {
guard let component = view.component else {
return
}
let theme = component.theme
- let controller = ChatTimerScreen(context: component.context, updatedPresentationData: (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) }), style: style, currentTime: selectedTime, dismissByTapOutside: dismissByTapOutside, completion: { time in
+ let controller = ChatTimerScreen(context: component.context, updatedPresentationData: (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) }), style: style, currentTime: selectedTime, completion: { time in
completion(time)
})
view.endEditing(true)
diff --git a/submodules/TelegramUI/Images.xcassets/Avatar/SampleAvatar1.imageset/Avatar8.pdf b/submodules/TelegramUI/Images.xcassets/Avatar/SampleAvatar1.imageset/Avatar8.pdf
deleted file mode 100644
index 6c97d03a30..0000000000
Binary files a/submodules/TelegramUI/Images.xcassets/Avatar/SampleAvatar1.imageset/Avatar8.pdf and /dev/null differ
diff --git a/submodules/TelegramUI/Images.xcassets/Avatar/SampleAvatar1.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Avatar/SampleAvatar1.imageset/Contents.json
deleted file mode 100644
index 3b6cd02302..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Avatar/SampleAvatar1.imageset/Contents.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "images" : [
- {
- "filename" : "Avatar8.pdf",
- "idiom" : "universal"
- }
- ],
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/submodules/TelegramUI/Images.xcassets/Call/CallCancelButton.imageset/CallCancelIcon@2x.png b/submodules/TelegramUI/Images.xcassets/Call/CallCancelButton.imageset/CallCancelIcon@2x.png
deleted file mode 100644
index 1d51eb1725..0000000000
Binary files a/submodules/TelegramUI/Images.xcassets/Call/CallCancelButton.imageset/CallCancelIcon@2x.png and /dev/null differ
diff --git a/submodules/TelegramUI/Images.xcassets/Call/CallCancelButton.imageset/CallCancelIcon@3x.png b/submodules/TelegramUI/Images.xcassets/Call/CallCancelButton.imageset/CallCancelIcon@3x.png
deleted file mode 100644
index 95e99b4db2..0000000000
Binary files a/submodules/TelegramUI/Images.xcassets/Call/CallCancelButton.imageset/CallCancelIcon@3x.png and /dev/null differ
diff --git a/submodules/TelegramUI/Images.xcassets/Call/CallCancelButton.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Call/CallCancelButton.imageset/Contents.json
deleted file mode 100644
index 313c758607..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Call/CallCancelButton.imageset/Contents.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "images" : [
- {
- "idiom" : "universal",
- "scale" : "1x"
- },
- {
- "idiom" : "universal",
- "filename" : "CallCancelIcon@2x.png",
- "scale" : "2x"
- },
- {
- "idiom" : "universal",
- "filename" : "CallCancelIcon@3x.png",
- "scale" : "3x"
- }
- ],
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
\ No newline at end of file
diff --git a/submodules/TelegramUI/Images.xcassets/Call/CallRouteBluetooth.imageset/CallRouteBluetooth@2x.png b/submodules/TelegramUI/Images.xcassets/Call/CallRouteBluetooth.imageset/CallRouteBluetooth@2x.png
deleted file mode 100644
index 55a0de9e65..0000000000
Binary files a/submodules/TelegramUI/Images.xcassets/Call/CallRouteBluetooth.imageset/CallRouteBluetooth@2x.png and /dev/null differ
diff --git a/submodules/TelegramUI/Images.xcassets/Call/CallRouteBluetooth.imageset/CallRouteBluetooth@3x.png b/submodules/TelegramUI/Images.xcassets/Call/CallRouteBluetooth.imageset/CallRouteBluetooth@3x.png
deleted file mode 100644
index 0f680c2fe6..0000000000
Binary files a/submodules/TelegramUI/Images.xcassets/Call/CallRouteBluetooth.imageset/CallRouteBluetooth@3x.png and /dev/null differ
diff --git a/submodules/TelegramUI/Images.xcassets/Call/CallRouteBluetooth.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Call/CallRouteBluetooth.imageset/Contents.json
deleted file mode 100644
index d221f407ce..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Call/CallRouteBluetooth.imageset/Contents.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "images" : [
- {
- "idiom" : "universal",
- "scale" : "1x"
- },
- {
- "idiom" : "universal",
- "filename" : "CallRouteBluetooth@2x.png",
- "scale" : "2x"
- },
- {
- "idiom" : "universal",
- "filename" : "CallRouteBluetooth@3x.png",
- "scale" : "3x"
- }
- ],
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
\ No newline at end of file
diff --git a/submodules/TelegramUI/Images.xcassets/Call/CallRouteSpeaker.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Call/CallRouteSpeaker.imageset/Contents.json
deleted file mode 100644
index d0d69abee5..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Call/CallRouteSpeaker.imageset/Contents.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "images" : [
- {
- "filename" : "ic_calls_speaker.pdf",
- "idiom" : "universal"
- }
- ],
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/submodules/TelegramUI/Images.xcassets/Call/CallRouteSpeaker.imageset/ic_calls_speaker.pdf b/submodules/TelegramUI/Images.xcassets/Call/CallRouteSpeaker.imageset/ic_calls_speaker.pdf
deleted file mode 100644
index 22af6e39c6..0000000000
Binary files a/submodules/TelegramUI/Images.xcassets/Call/CallRouteSpeaker.imageset/ic_calls_speaker.pdf and /dev/null differ
diff --git a/submodules/TelegramUI/Images.xcassets/Call/LegacyCallRouteSpeaker.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Call/LegacyCallRouteSpeaker.imageset/Contents.json
deleted file mode 100644
index 2ef9a3ee1a..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Call/LegacyCallRouteSpeaker.imageset/Contents.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "images" : [
- {
- "idiom" : "universal",
- "scale" : "1x"
- },
- {
- "filename" : "submodules_TelegramUI_Images.xcassets_Call_CallRouteSpeaker.imageset_CallRouteSpeaker@2x_Before_b542dc3fd7b381de80e554286a94ccd5b8d02154.png",
- "idiom" : "universal",
- "scale" : "2x"
- },
- {
- "filename" : "submodules_TelegramUI_Images.xcassets_Call_CallRouteSpeaker.imageset_CallRouteSpeaker@3x_Before_c8c1c96f16a3977d2a6d1956a0aeb31768c6bb23.png",
- "idiom" : "universal",
- "scale" : "3x"
- }
- ],
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/submodules/TelegramUI/Images.xcassets/Call/LegacyCallRouteSpeaker.imageset/submodules_TelegramUI_Images.xcassets_Call_CallRouteSpeaker.imageset_CallRouteSpeaker@2x_Before_b542dc3fd7b381de80e554286a94ccd5b8d02154.png b/submodules/TelegramUI/Images.xcassets/Call/LegacyCallRouteSpeaker.imageset/submodules_TelegramUI_Images.xcassets_Call_CallRouteSpeaker.imageset_CallRouteSpeaker@2x_Before_b542dc3fd7b381de80e554286a94ccd5b8d02154.png
deleted file mode 100644
index 9b5e566eb4..0000000000
Binary files a/submodules/TelegramUI/Images.xcassets/Call/LegacyCallRouteSpeaker.imageset/submodules_TelegramUI_Images.xcassets_Call_CallRouteSpeaker.imageset_CallRouteSpeaker@2x_Before_b542dc3fd7b381de80e554286a94ccd5b8d02154.png and /dev/null differ
diff --git a/submodules/TelegramUI/Images.xcassets/Call/LegacyCallRouteSpeaker.imageset/submodules_TelegramUI_Images.xcassets_Call_CallRouteSpeaker.imageset_CallRouteSpeaker@3x_Before_c8c1c96f16a3977d2a6d1956a0aeb31768c6bb23.png b/submodules/TelegramUI/Images.xcassets/Call/LegacyCallRouteSpeaker.imageset/submodules_TelegramUI_Images.xcassets_Call_CallRouteSpeaker.imageset_CallRouteSpeaker@3x_Before_c8c1c96f16a3977d2a6d1956a0aeb31768c6bb23.png
deleted file mode 100644
index 0026e6063d..0000000000
Binary files a/submodules/TelegramUI/Images.xcassets/Call/LegacyCallRouteSpeaker.imageset/submodules_TelegramUI_Images.xcassets_Call_CallRouteSpeaker.imageset_CallRouteSpeaker@3x_Before_c8c1c96f16a3977d2a6d1956a0aeb31768c6bb23.png and /dev/null differ
diff --git a/submodules/TelegramUI/Images.xcassets/Call/StatusVolumeOff.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Call/StatusVolumeOff.imageset/Contents.json
deleted file mode 100644
index 95ca49b9d3..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Call/StatusVolumeOff.imageset/Contents.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "images" : [
- {
- "filename" : "ic_voicevolumeoff.pdf",
- "idiom" : "universal"
- }
- ],
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/submodules/TelegramUI/Images.xcassets/Call/StatusVolumeOff.imageset/ic_voicevolumeoff.pdf b/submodules/TelegramUI/Images.xcassets/Call/StatusVolumeOff.imageset/ic_voicevolumeoff.pdf
deleted file mode 100644
index bcc068ce30..0000000000
Binary files a/submodules/TelegramUI/Images.xcassets/Call/StatusVolumeOff.imageset/ic_voicevolumeoff.pdf and /dev/null differ
diff --git a/submodules/TelegramUI/Images.xcassets/Call/SwitchCameraIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Call/SwitchCameraIcon.imageset/Contents.json
deleted file mode 100644
index e6f1b93d3e..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Call/SwitchCameraIcon.imageset/Contents.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "images" : [
- {
- "filename" : "ic_cam_flip.pdf",
- "idiom" : "universal"
- }
- ],
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/submodules/TelegramUI/Images.xcassets/Call/SwitchCameraIcon.imageset/ic_cam_flip.pdf b/submodules/TelegramUI/Images.xcassets/Call/SwitchCameraIcon.imageset/ic_cam_flip.pdf
deleted file mode 100644
index 5084218a29..0000000000
Binary files a/submodules/TelegramUI/Images.xcassets/Call/SwitchCameraIcon.imageset/ic_cam_flip.pdf and /dev/null differ
diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AgeMark.imageset/18on_24.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AgeMark.imageset/18on_24.pdf
deleted file mode 100644
index 356e934807..0000000000
Binary files a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AgeMark.imageset/18on_24.pdf and /dev/null differ
diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AgeMark.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AgeMark.imageset/Contents.json
deleted file mode 100644
index eda08930c8..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AgeMark.imageset/Contents.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "images" : [
- {
- "filename" : "18on_24.pdf",
- "idiom" : "universal"
- }
- ],
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AgeUnmark.imageset/18off_24.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AgeUnmark.imageset/18off_24.pdf
deleted file mode 100644
index 974496474d..0000000000
Binary files a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AgeUnmark.imageset/18off_24.pdf and /dev/null differ
diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AgeUnmark.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AgeUnmark.imageset/Contents.json
deleted file mode 100644
index b48dcd4e15..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AgeUnmark.imageset/Contents.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "images" : [
- {
- "filename" : "18off_24.pdf",
- "idiom" : "universal"
- }
- ],
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/CaptionHide.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/CaptionHide.imageset/Contents.json
deleted file mode 100644
index 1caab208d5..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/CaptionHide.imageset/Contents.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "images" : [
- {
- "filename" : "hidecaption_24.pdf",
- "idiom" : "universal"
- }
- ],
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/CaptionHide.imageset/hidecaption_24.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/CaptionHide.imageset/hidecaption_24.pdf
deleted file mode 100644
index 3d86f95ada..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/CaptionHide.imageset/hidecaption_24.pdf
+++ /dev/null
@@ -1,177 +0,0 @@
-%PDF-1.7
-
-1 0 obj
- << >>
-endobj
-
-2 0 obj
- << /Length 3 0 R >>
-stream
-/DeviceRGB CS
-/DeviceRGB cs
-q
-1.000000 0.000000 -0.000000 1.000000 3.334961 3.205170 cm
-0.000000 0.000000 0.000000 scn
-1.135289 17.265064 m
-0.875590 17.524763 0.454536 17.524763 0.194837 17.265064 c
--0.064862 17.005365 -0.064862 16.584312 0.194837 16.324614 c
-16.194838 0.324612 l
-16.454536 0.064913 16.875591 0.064913 17.135290 0.324612 c
-17.394989 0.584311 17.394989 1.005365 17.135290 1.265064 c
-15.270563 3.129791 l
-15.665111 3.129791 l
-16.032377 3.129791 16.330109 3.427522 16.330109 3.794791 c
-16.330109 4.162061 16.032377 4.459791 15.665111 4.459791 c
-13.940563 4.459791 l
-7.330003 11.070351 l
-7.330003 12.694838 l
-7.330004 12.718859 l
-7.330019 12.978289 7.330032 13.211649 7.314171 13.405788 c
-7.297186 13.613665 7.258753 13.834405 7.148529 14.050732 c
-6.988900 14.364021 6.734188 14.618734 6.420897 14.778363 c
-6.204570 14.888588 5.983830 14.927021 5.775954 14.944005 c
-5.581833 14.959866 5.348500 14.959853 5.089100 14.959841 c
-5.089095 14.959841 l
-5.089057 14.959841 l
-5.089017 14.959841 l
-5.065003 14.959841 l
-3.440514 14.959841 l
-1.135289 17.265064 l
-h
-4.770516 13.629837 m
-6.000003 12.400351 l
-6.000003 12.694838 l
-6.000003 12.985837 5.999486 13.164092 5.988588 13.297483 c
-5.983510 13.359637 5.977127 13.397297 5.971728 13.420219 c
-5.969160 13.431128 5.967000 13.437864 5.965689 13.441540 c
-5.964900 13.443751 5.964303 13.445174 5.963926 13.446011 c
-5.963701 13.446509 5.963554 13.446799 5.963490 13.446924 c
-5.931373 13.509958 5.880125 13.561207 5.817090 13.593325 c
-5.816755 13.593495 5.815236 13.594263 5.811705 13.595523 c
-5.808031 13.596835 5.801293 13.598994 5.790386 13.601562 c
-5.767462 13.606961 5.729803 13.613344 5.667649 13.618422 c
-5.534258 13.629320 5.356003 13.629837 5.065003 13.629837 c
-4.770516 13.629837 l
-h
-0.181478 14.050732 m
-0.267737 14.220026 0.381761 14.372214 0.517790 14.501538 c
-1.459741 13.559587 l
-1.420951 13.529512 1.389032 13.491115 1.366516 13.446924 c
-1.366346 13.446590 1.365578 13.445070 1.364318 13.441540 c
-1.363006 13.437864 1.360847 13.431128 1.358278 13.420219 c
-1.352880 13.397297 1.346497 13.359637 1.341419 13.297483 c
-1.330521 13.164092 1.330003 12.985837 1.330003 12.694838 c
-1.330003 9.894837 l
-1.330003 9.603838 1.330521 9.425583 1.341419 9.292192 c
-1.346497 9.230038 1.352880 9.192378 1.358278 9.169455 c
-1.360847 9.158547 1.363006 9.151810 1.364318 9.148135 c
-1.365578 9.144605 1.366346 9.143085 1.366516 9.142751 c
-1.398634 9.079716 1.449882 9.028467 1.512917 8.996350 c
-1.513251 8.996180 1.514771 8.995412 1.518302 8.994151 c
-1.521976 8.992840 1.528713 8.990681 1.539621 8.988112 c
-1.562544 8.982714 1.600204 8.976331 1.662358 8.971252 c
-1.795749 8.960355 1.974004 8.959837 2.265003 8.959837 c
-5.065003 8.959837 l
-5.356003 8.959837 5.534258 8.960355 5.667649 8.971252 c
-5.729803 8.976331 5.767462 8.982714 5.790386 8.988112 c
-5.801293 8.990681 5.808031 8.992840 5.811705 8.994151 c
-5.815236 8.995412 5.816755 8.996180 5.817090 8.996350 c
-5.861280 9.018866 5.899678 9.050784 5.929753 9.089574 c
-6.871704 8.147623 l
-6.742380 8.011595 6.590191 7.897571 6.420897 7.811312 c
-6.204570 7.701087 5.983830 7.662654 5.775953 7.645670 c
-5.581820 7.629809 5.348469 7.629822 5.089050 7.629836 c
-5.089025 7.629836 l
-5.065003 7.629837 l
-2.265003 7.629837 l
-2.240981 7.629836 l
-2.240957 7.629836 l
-1.981538 7.629822 1.748187 7.629809 1.554053 7.645670 c
-1.346177 7.662654 1.125436 7.701087 0.909109 7.811312 c
-0.595819 7.970941 0.341107 8.225653 0.181478 8.538943 c
-0.071253 8.755270 0.032820 8.976010 0.015836 9.183887 c
--0.000024 9.378011 -0.000013 9.611347 0.000001 9.870750 c
-0.000001 9.870787 l
-0.000001 9.870823 l
-0.000001 9.894837 l
-0.000001 12.694838 l
-0.000001 12.718851 l
-0.000001 12.718889 l
-0.000001 12.718924 l
--0.000013 12.978327 -0.000024 13.211664 0.015836 13.405788 c
-0.032820 13.613665 0.071253 13.834405 0.181478 14.050732 c
-h
-1.665110 4.459791 m
-10.559536 4.459791 l
-11.889536 3.129791 l
-1.665110 3.129791 l
-1.297841 3.129791 1.000110 3.427522 1.000110 3.794791 c
-1.000110 4.162061 1.297841 4.459791 1.665110 4.459791 c
-h
-10.665111 14.459792 m
-10.297841 14.459792 10.000111 14.162062 10.000111 13.794792 c
-10.000111 13.427523 10.297841 13.129791 10.665111 13.129791 c
-15.665111 13.129791 l
-16.032377 13.129791 16.330109 13.427523 16.330109 13.794792 c
-16.330109 14.162062 16.032377 14.459792 15.665111 14.459792 c
-10.665111 14.459792 l
-h
-12.665111 9.459791 m
-12.297841 9.459791 12.000111 9.162061 12.000111 8.794791 c
-12.000111 8.427522 12.297841 8.129791 12.665111 8.129791 c
-15.665111 8.129791 l
-16.032377 8.129791 16.330109 8.427522 16.330109 8.794791 c
-16.330109 9.162061 16.032377 9.459791 15.665111 9.459791 c
-12.665111 9.459791 l
-h
-f*
-n
-Q
-
-endstream
-endobj
-
-3 0 obj
- 4618
-endobj
-
-4 0 obj
- << /Annots []
- /Type /Page
- /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
- /Resources 1 0 R
- /Contents 2 0 R
- /Parent 5 0 R
- >>
-endobj
-
-5 0 obj
- << /Kids [ 4 0 R ]
- /Count 1
- /Type /Pages
- >>
-endobj
-
-6 0 obj
- << /Pages 5 0 R
- /Type /Catalog
- >>
-endobj
-
-xref
-0 7
-0000000000 65535 f
-0000000010 00000 n
-0000000034 00000 n
-0000004708 00000 n
-0000004731 00000 n
-0000004904 00000 n
-0000004978 00000 n
-trailer
-<< /ID [ (some) (id) ]
- /Root 6 0 R
- /Size 7
->>
-startxref
-5037
-%%EOF
\ No newline at end of file
diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/CaptionShow.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/CaptionShow.imageset/Contents.json
deleted file mode 100644
index 8b6179bcaf..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/CaptionShow.imageset/Contents.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "images" : [
- {
- "filename" : "showcaption_24.pdf",
- "idiom" : "universal"
- }
- ],
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/CaptionShow.imageset/showcaption_24.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/CaptionShow.imageset/showcaption_24.pdf
deleted file mode 100644
index 174e22bccd..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/CaptionShow.imageset/showcaption_24.pdf
+++ /dev/null
@@ -1,169 +0,0 @@
-%PDF-1.7
-
-1 0 obj
- << >>
-endobj
-
-2 0 obj
- << /Length 3 0 R >>
-stream
-/DeviceRGB CS
-/DeviceRGB cs
-q
-1.000000 0.000000 -0.000000 1.000000 3.334961 6.334900 cm
-0.000000 0.000000 0.000000 scn
-2.265003 11.830077 m
-2.240989 11.830077 l
-2.240949 11.830077 l
-1.981532 11.830091 1.748184 11.830104 1.554052 11.814242 c
-1.346176 11.797258 1.125435 11.758825 0.909108 11.648602 c
-0.595819 11.488972 0.341106 11.234260 0.181477 10.920970 c
-0.071253 10.704642 0.032820 10.483902 0.015835 10.276026 c
--0.000026 10.081894 -0.000013 9.848544 0.000001 9.589127 c
-0.000001 9.589089 l
-0.000001 9.565076 l
-0.000001 6.765075 l
-0.000001 6.741061 l
-0.000001 6.741024 l
--0.000013 6.481606 -0.000026 6.248257 0.015835 6.054125 c
-0.032820 5.846249 0.071253 5.625508 0.181477 5.409181 c
-0.341106 5.095891 0.595819 4.841178 0.909108 4.681550 c
-1.125435 4.571325 1.346176 4.532892 1.554052 4.515908 c
-1.748192 4.500046 1.981551 4.500060 2.240980 4.500074 c
-2.265002 4.500075 l
-5.065003 4.500075 l
-5.089025 4.500074 l
-5.348454 4.500060 5.581814 4.500046 5.775953 4.515908 c
-5.983829 4.532892 6.204570 4.571325 6.420897 4.681550 c
-6.734187 4.841178 6.988900 5.095891 7.148529 5.409181 c
-7.258753 5.625508 7.297186 5.846249 7.314170 6.054125 c
-7.330032 6.248264 7.330019 6.481624 7.330004 6.741053 c
-7.330003 6.765075 l
-7.330003 9.565075 l
-7.330004 9.589098 l
-7.330019 9.848527 7.330032 10.081886 7.314170 10.276026 c
-7.297186 10.483902 7.258753 10.704642 7.148529 10.920970 c
-6.988900 11.234260 6.734187 11.488972 6.420897 11.648602 c
-6.204570 11.758825 5.983829 11.797258 5.775953 11.814242 c
-5.581822 11.830104 5.348474 11.830091 5.089057 11.830077 c
-5.089017 11.830077 l
-5.065003 11.830077 l
-2.265003 11.830077 l
-h
-1.512916 10.463563 m
-1.513250 10.463733 1.514770 10.464500 1.518301 10.465761 c
-1.521975 10.467072 1.528712 10.469232 1.539620 10.471801 c
-1.562544 10.477199 1.600203 10.483582 1.662357 10.488660 c
-1.795748 10.499558 1.974003 10.500075 2.265003 10.500075 c
-5.065003 10.500075 l
-5.356002 10.500075 5.534257 10.499558 5.667649 10.488660 c
-5.729803 10.483582 5.767462 10.477199 5.790385 10.471801 c
-5.801293 10.469232 5.808030 10.467072 5.811704 10.465761 c
-5.815235 10.464500 5.816755 10.463733 5.817090 10.463563 c
-5.880124 10.431445 5.931373 10.380197 5.963490 10.317163 c
-5.963660 10.316828 5.964428 10.315308 5.965689 10.311777 c
-5.967000 10.308103 5.969159 10.301366 5.971728 10.290458 c
-5.977127 10.267534 5.983509 10.229876 5.988587 10.167721 c
-5.999485 10.034330 6.000003 9.856074 6.000003 9.565075 c
-6.000003 6.765075 l
-6.000003 6.474076 5.999485 6.295821 5.988587 6.162429 c
-5.983509 6.100276 5.977127 6.062616 5.971728 6.039693 c
-5.969159 6.028785 5.967000 6.022048 5.965689 6.018374 c
-5.964428 6.014843 5.963660 6.013323 5.963490 6.012989 c
-5.931373 5.949954 5.880124 5.898705 5.817090 5.866588 c
-5.816755 5.866418 5.815235 5.865650 5.811705 5.864389 c
-5.808030 5.863078 5.801293 5.860919 5.790385 5.858350 c
-5.767462 5.852952 5.729803 5.846569 5.667649 5.841491 c
-5.534257 5.830593 5.356002 5.830075 5.065003 5.830075 c
-2.265002 5.830075 l
-1.974003 5.830075 1.795748 5.830593 1.662357 5.841491 c
-1.600203 5.846569 1.562544 5.852952 1.539620 5.858350 c
-1.528712 5.860919 1.521975 5.863078 1.518301 5.864389 c
-1.514770 5.865650 1.513250 5.866418 1.512916 5.866588 c
-1.449882 5.898705 1.398633 5.949954 1.366516 6.012989 c
-1.366345 6.013323 1.365577 6.014843 1.364317 6.018373 c
-1.363005 6.022048 1.360846 6.028785 1.358277 6.039693 c
-1.352879 6.062616 1.346496 6.100276 1.341418 6.162429 c
-1.330520 6.295821 1.330003 6.474076 1.330003 6.765075 c
-1.330003 9.565076 l
-1.330003 9.856075 1.330520 10.034330 1.341418 10.167721 c
-1.346496 10.229876 1.352879 10.267534 1.358277 10.290458 c
-1.360846 10.301366 1.363005 10.308103 1.364317 10.311777 c
-1.365577 10.315308 1.366345 10.316828 1.366516 10.317163 c
-1.398633 10.380197 1.449882 10.431445 1.512916 10.463563 c
-h
-10.665110 11.330029 m
-10.297840 11.330029 10.000110 11.032299 10.000110 10.665030 c
-10.000110 10.297760 10.297840 10.000030 10.665110 10.000030 c
-15.665110 10.000030 l
-16.032377 10.000030 16.330109 10.297760 16.330109 10.665030 c
-16.330109 11.032299 16.032377 11.330029 15.665110 11.330029 c
-10.665110 11.330029 l
-h
-10.665110 6.330029 m
-10.297840 6.330029 10.000110 6.032299 10.000110 5.665030 c
-10.000110 5.297760 10.297840 5.000030 10.665110 5.000030 c
-15.665110 5.000030 l
-16.032377 5.000030 16.330109 5.297760 16.330109 5.665030 c
-16.330109 6.032299 16.032377 6.330029 15.665110 6.330029 c
-10.665110 6.330029 l
-h
-1.000109 0.665030 m
-1.000109 1.032299 1.297840 1.330029 1.665109 1.330029 c
-15.665110 1.330029 l
-16.032377 1.330029 16.330109 1.032299 16.330109 0.665030 c
-16.330109 0.297760 16.032377 0.000030 15.665110 0.000030 c
-1.665109 0.000030 l
-1.297840 0.000030 1.000109 0.297760 1.000109 0.665030 c
-h
-f*
-n
-Q
-
-endstream
-endobj
-
-3 0 obj
- 4706
-endobj
-
-4 0 obj
- << /Annots []
- /Type /Page
- /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
- /Resources 1 0 R
- /Contents 2 0 R
- /Parent 5 0 R
- >>
-endobj
-
-5 0 obj
- << /Kids [ 4 0 R ]
- /Count 1
- /Type /Pages
- >>
-endobj
-
-6 0 obj
- << /Pages 5 0 R
- /Type /Catalog
- >>
-endobj
-
-xref
-0 7
-0000000000 65535 f
-0000000010 00000 n
-0000000034 00000 n
-0000004796 00000 n
-0000004819 00000 n
-0000004992 00000 n
-0000005066 00000 n
-trailer
-<< /ID [ (some) (id) ]
- /Root 6 0 R
- /Size 7
->>
-startxref
-5125
-%%EOF
\ No newline at end of file
diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/DarkMode.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/DarkMode.imageset/Contents.json
deleted file mode 100644
index 97b7ab0446..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/DarkMode.imageset/Contents.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "images" : [
- {
- "idiom" : "universal",
- "filename" : "ic_lt_darkmode.pdf"
- }
- ],
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
\ No newline at end of file
diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/DarkMode.imageset/ic_lt_darkmode.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/DarkMode.imageset/ic_lt_darkmode.pdf
deleted file mode 100644
index 98a41e4986..0000000000
Binary files a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/DarkMode.imageset/ic_lt_darkmode.pdf and /dev/null differ
diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ImageShrink.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ImageShrink.imageset/Contents.json
deleted file mode 100644
index fb10bc0705..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ImageShrink.imageset/Contents.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "images" : [
- {
- "filename" : "shrink_24.pdf",
- "idiom" : "universal"
- }
- ],
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ImageShrink.imageset/shrink_24.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ImageShrink.imageset/shrink_24.pdf
deleted file mode 100644
index 0b4710ee5d..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ImageShrink.imageset/shrink_24.pdf
+++ /dev/null
@@ -1,169 +0,0 @@
-%PDF-1.7
-
-1 0 obj
- << >>
-endobj
-
-2 0 obj
- << /Length 3 0 R >>
-stream
-/DeviceRGB CS
-/DeviceRGB cs
-q
-1.000000 0.000000 -0.000000 1.000000 3.835083 3.834961 cm
-0.000000 0.000000 0.000000 scn
-5.436178 16.330017 m
-5.465000 16.330017 l
-10.865000 16.330017 l
-10.893822 16.330017 l
-11.709461 16.330023 12.362126 16.330027 12.889546 16.286936 c
-13.430926 16.242702 13.898637 16.149773 14.328876 15.930555 c
-15.018489 15.579180 15.579163 15.018506 15.930539 14.328892 c
-16.149757 13.898653 16.242687 13.430943 16.286919 12.889563 c
-16.330009 12.362154 16.330006 11.709505 16.330000 10.893888 c
-16.330000 10.893824 l
-16.330000 10.865017 l
-16.330000 5.465017 l
-16.330000 5.436212 l
-16.330000 5.436146 l
-16.330006 4.620529 16.330009 3.967880 16.286919 3.440471 c
-16.242687 2.899091 16.149757 2.431380 15.930539 2.001142 c
-15.579163 1.311528 15.018489 0.750854 14.328876 0.399478 c
-13.898637 0.180260 13.430927 0.087330 12.889546 0.043098 c
-12.362138 0.000008 11.709489 0.000011 10.893871 0.000017 c
-10.893806 0.000017 l
-10.865000 0.000017 l
-5.465000 0.000017 l
-5.436194 0.000017 l
-5.436130 0.000017 l
-4.620512 0.000011 3.967863 0.000008 3.440454 0.043098 c
-2.899074 0.087330 2.431364 0.180260 2.001125 0.399478 c
-1.311511 0.750854 0.750837 1.311528 0.399461 2.001142 c
-0.180244 2.431380 0.087314 2.899090 0.043081 3.440471 c
--0.000010 3.967890 -0.000006 4.620555 0.000000 5.436195 c
-0.000000 5.465017 l
-0.000000 10.865017 l
-0.000000 10.893839 l
--0.000006 11.709478 -0.000010 12.362144 0.043081 12.889563 c
-0.087314 13.430943 0.180244 13.898653 0.399461 14.328892 c
-0.750837 15.018506 1.311511 15.579180 2.001125 15.930555 c
-2.431364 16.149773 2.899074 16.242702 3.440454 16.286936 c
-3.967873 16.330027 4.620538 16.330023 5.436178 16.330017 c
-h
-3.548759 14.961352 m
-3.089627 14.923841 2.816429 14.853280 2.604933 14.745517 c
-2.165574 14.521652 1.808364 14.164443 1.584500 13.725084 c
-1.476737 13.513588 1.406177 13.240391 1.368664 12.781259 c
-1.330518 12.314363 1.330000 11.716068 1.330000 10.865017 c
-1.330000 5.465017 l
-1.330000 4.613965 1.330518 4.015671 1.368664 3.548775 c
-1.406177 3.089643 1.476737 2.816445 1.584500 2.604949 c
-1.808364 2.165591 2.165574 1.808381 2.604933 1.584517 c
-2.816429 1.476754 3.089627 1.406194 3.548759 1.368681 c
-4.015654 1.330534 4.613948 1.330017 5.465000 1.330017 c
-10.865000 1.330017 l
-11.716052 1.330017 12.314346 1.330534 12.781242 1.368681 c
-13.240374 1.406194 13.513572 1.476754 13.725068 1.584517 c
-14.164426 1.808381 14.521636 2.165591 14.745501 2.604949 c
-14.853263 2.816445 14.923823 3.089643 14.961336 3.548776 c
-14.999483 4.015671 15.000000 4.613965 15.000000 5.465017 c
-15.000000 10.865017 l
-15.000000 11.716068 14.999483 12.314363 14.961336 12.781259 c
-14.923823 13.240391 14.853263 13.513588 14.745501 13.725084 c
-14.521636 14.164443 14.164426 14.521652 13.725068 14.745517 c
-13.513572 14.853280 13.240374 14.923841 12.781241 14.961352 c
-12.314346 14.999499 11.716052 15.000017 10.865000 15.000017 c
-5.465000 15.000017 l
-4.613948 15.000017 4.015654 14.999499 3.548759 14.961352 c
-h
-7.829878 12.164894 m
-7.829878 12.532164 7.532147 12.829895 7.164878 12.829895 c
-6.797609 12.829895 6.499878 12.532164 6.499878 12.164894 c
-6.499878 10.770347 l
-4.635104 12.635120 l
-4.375406 12.894819 3.954351 12.894819 3.694653 12.635120 c
-3.434954 12.375422 3.434954 11.954367 3.694653 11.694669 c
-5.559426 9.829895 l
-4.164878 9.829895 l
-3.797609 9.829895 3.499878 9.532164 3.499878 9.164894 c
-3.499878 8.797626 3.797609 8.499895 4.164878 8.499895 c
-7.148878 8.499895 l
-7.153147 8.499901 l
-7.157188 8.499939 l
-7.329931 8.497953 7.503299 8.562863 7.635104 8.694669 c
-7.766910 8.826473 7.831820 8.999842 7.829834 9.172585 c
-7.829872 9.176626 l
-7.829878 9.180895 l
-7.829878 12.164894 l
-h
-9.164833 3.499878 m
-8.797565 3.499878 8.499834 3.797609 8.499834 4.164879 c
-8.499834 7.148878 l
-8.499840 7.153147 l
-8.499878 7.157188 l
-8.497892 7.329931 8.562802 7.503300 8.694608 7.635104 c
-8.826412 7.766910 8.999781 7.831820 9.172523 7.829834 c
-9.176565 7.829872 l
-9.180834 7.829878 l
-12.164833 7.829878 l
-12.532103 7.829878 12.829834 7.532147 12.829834 7.164879 c
-12.829834 6.797609 12.532103 6.499878 12.164833 6.499878 c
-10.770286 6.499878 l
-12.635059 4.635104 l
-12.894758 4.375405 12.894758 3.954351 12.635059 3.694653 c
-12.375360 3.434954 11.954307 3.434954 11.694608 3.694653 c
-9.829834 5.559426 l
-9.829834 4.164879 l
-9.829834 3.797609 9.532103 3.499878 9.164833 3.499878 c
-h
-f*
-n
-Q
-
-endstream
-endobj
-
-3 0 obj
- 4327
-endobj
-
-4 0 obj
- << /Annots []
- /Type /Page
- /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
- /Resources 1 0 R
- /Contents 2 0 R
- /Parent 5 0 R
- >>
-endobj
-
-5 0 obj
- << /Kids [ 4 0 R ]
- /Count 1
- /Type /Pages
- >>
-endobj
-
-6 0 obj
- << /Pages 5 0 R
- /Type /Catalog
- >>
-endobj
-
-xref
-0 7
-0000000000 65535 f
-0000000010 00000 n
-0000000034 00000 n
-0000004417 00000 n
-0000004440 00000 n
-0000004613 00000 n
-0000004687 00000 n
-trailer
-<< /ID [ (some) (id) ]
- /Root 6 0 R
- /Size 7
->>
-startxref
-4746
-%%EOF
\ No newline at end of file
diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Mute2h.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Mute2h.imageset/Contents.json
deleted file mode 100644
index 5b28aa91ea..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Mute2h.imageset/Contents.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "images" : [
- {
- "filename" : "ic_mute2hours.pdf",
- "idiom" : "universal"
- }
- ],
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Mute2h.imageset/ic_mute2hours.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Mute2h.imageset/ic_mute2hours.pdf
deleted file mode 100644
index 25545ba34e..0000000000
Binary files a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Mute2h.imageset/ic_mute2hours.pdf and /dev/null differ
diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Unstar.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Unstar.imageset/Contents.json
deleted file mode 100644
index 96ba4365cd..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Unstar.imageset/Contents.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "images" : [
- {
- "filename" : "Size=24px, Type=Crossed Out.pdf",
- "idiom" : "universal"
- }
- ],
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Unstar.imageset/Size=24px, Type=Crossed Out.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Unstar.imageset/Size=24px, Type=Crossed Out.pdf
deleted file mode 100644
index 0b20e7ad20..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Unstar.imageset/Size=24px, Type=Crossed Out.pdf
+++ /dev/null
@@ -1,165 +0,0 @@
-%PDF-1.7
-
-1 0 obj
- << >>
-endobj
-
-2 0 obj
- << /Length 3 0 R >>
-stream
-/DeviceRGB CS
-/DeviceRGB cs
-q
-1.000000 0.000000 -0.000000 1.000000 4.834961 4.361450 cm
-0.000000 0.000000 0.000000 scn
-0.294029 13.569108 m
-0.148725 13.203500 0.079447 12.808213 0.043083 12.363134 c
--0.000009 11.835724 -0.000005 11.183071 0.000001 10.367449 c
-0.000001 10.367412 l
-0.000001 10.338589 l
-0.000001 2.887971 l
-0.000001 2.855927 l
-0.000001 2.855909 l
--0.000013 2.361413 -0.000024 1.944829 0.029606 1.619491 c
-0.058879 1.298060 0.124788 0.933573 0.357819 0.638192 c
-0.669126 0.243593 1.142026 0.010736 1.644602 0.004579 c
-2.020809 -0.000031 2.349897 0.169960 2.622518 0.342736 c
-2.898462 0.517618 3.228661 0.771631 3.620616 1.073153 c
-3.645996 1.092677 l
-6.594922 3.361081 l
-6.992292 3.666750 7.044882 3.691571 7.078142 3.700500 c
-7.135041 3.715775 7.194962 3.715775 7.251861 3.700500 c
-7.285121 3.691571 7.337711 3.666750 7.735081 3.361081 c
-10.684008 1.092677 l
-10.709353 1.073179 l
-11.101323 0.771646 11.431534 0.517624 11.707485 0.342736 c
-11.980106 0.169960 12.309195 -0.000031 12.685401 0.004579 c
-13.008502 0.008537 13.319336 0.106193 13.582854 0.280283 c
-12.355464 1.507673 l
-12.146183 1.646584 11.878798 1.851580 11.494923 2.146868 c
-8.545997 4.415272 l
-8.477973 4.467776 l
-8.477962 4.467785 l
-8.199948 4.682729 7.921870 4.897722 7.596705 4.985017 c
-7.313910 5.060937 7.016093 5.060937 6.733298 4.985017 c
-6.408127 4.897721 6.130046 4.682723 5.852028 4.467772 c
-5.784007 4.415271 l
-2.835081 2.146868 l
-2.410956 1.820618 2.129028 1.604588 1.910557 1.466129 c
-1.735109 1.354937 1.664445 1.337146 1.654627 1.334675 c
-1.654414 1.334620 l
-1.557878 1.337669 1.467334 1.382254 1.406059 1.456912 c
-1.405972 1.457113 l
-1.401946 1.466402 1.372963 1.533260 1.354124 1.740117 c
-1.330665 1.997703 1.330002 2.352881 1.330002 2.887971 c
-1.330002 10.338589 l
-1.330002 11.189641 1.330519 11.787935 1.368666 12.254830 c
-1.375014 12.332534 1.382310 12.404911 1.390571 12.472566 c
-0.294029 13.569108 l
-h
-12.999968 2.744075 m
-12.999996 2.790545 13.000002 2.838489 13.000002 2.887970 c
-13.000002 10.338589 l
-13.000002 11.189640 12.999485 11.787935 12.961338 12.254830 c
-12.923825 12.713963 12.853265 12.987160 12.745502 13.198656 c
-12.521638 13.638015 12.164428 13.995224 11.725070 14.219089 c
-11.513574 14.326852 11.240376 14.397411 10.781243 14.434924 c
-10.314348 14.473071 9.716053 14.473588 8.865002 14.473588 c
-5.465002 14.473588 l
-4.613950 14.473588 4.015655 14.473071 3.548759 14.434924 c
-3.089628 14.397411 2.816430 14.326852 2.604934 14.219089 c
-2.372593 14.100705 2.163225 13.945032 1.984401 13.759640 c
-1.043852 14.700190 l
-1.320836 14.983611 1.643658 15.221989 2.001126 15.404127 c
-2.431365 15.623345 2.899075 15.716275 3.440455 15.760508 c
-3.967874 15.803599 4.620536 15.803595 5.436175 15.803589 c
-5.436180 15.803589 l
-5.465002 15.803589 l
-8.865002 15.803589 l
-8.893824 15.803589 l
-9.709463 15.803595 10.362128 15.803599 10.889548 15.760508 c
-11.430928 15.716275 11.898639 15.623345 12.328877 15.404127 c
-13.018491 15.052752 13.579165 14.492078 13.930541 13.802464 c
-14.149758 13.372225 14.242688 12.904515 14.286921 12.363134 c
-14.330012 11.835714 14.330008 11.183048 14.330002 10.367406 c
-14.330002 10.367395 l
-14.330002 10.338589 l
-14.330002 2.887970 l
-14.330002 2.855918 l
-14.330016 2.361417 14.330028 1.944830 14.300398 1.619491 c
-14.295680 1.567688 14.290010 1.514769 14.282844 1.461199 c
-12.999968 2.744075 l
-h
-f*
-n
-Q
-q
-1.000000 0.000000 -0.000000 1.000000 4.500000 3.540039 cm
-0.000000 0.000000 0.000000 scn
-0.470226 17.930187 m
-0.210527 18.189886 -0.210527 18.189886 -0.470226 17.930187 c
--0.729925 17.670488 -0.729925 17.249434 -0.470226 16.989735 c
-0.470226 17.930187 l
-h
-15.529774 0.989735 m
-15.789473 0.730036 16.210527 0.730036 16.470226 0.989735 c
-16.729925 1.249434 16.729925 1.670488 16.470226 1.930187 c
-15.529774 0.989735 l
-h
--0.470226 16.989735 m
-15.529774 0.989735 l
-16.470226 1.930187 l
-0.470226 17.930187 l
--0.470226 16.989735 l
-h
-f
-n
-Q
-
-endstream
-endobj
-
-3 0 obj
- 3871
-endobj
-
-4 0 obj
- << /Annots []
- /Type /Page
- /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
- /Resources 1 0 R
- /Contents 2 0 R
- /Parent 5 0 R
- >>
-endobj
-
-5 0 obj
- << /Kids [ 4 0 R ]
- /Count 1
- /Type /Pages
- >>
-endobj
-
-6 0 obj
- << /Pages 5 0 R
- /Type /Catalog
- >>
-endobj
-
-xref
-0 7
-0000000000 65535 f
-0000000010 00000 n
-0000000034 00000 n
-0000003961 00000 n
-0000003984 00000 n
-0000004157 00000 n
-0000004231 00000 n
-trailer
-<< /ID [ (some) (id) ]
- /Root 6 0 R
- /Size 7
->>
-startxref
-4290
-%%EOF
\ No newline at end of file
diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/UserCrossed.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/UserCrossed.imageset/Contents.json
deleted file mode 100644
index ab416f72c8..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/UserCrossed.imageset/Contents.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "images" : [
- {
- "filename" : "hideforward_24.pdf",
- "idiom" : "universal"
- }
- ],
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/UserCrossed.imageset/hideforward_24.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/UserCrossed.imageset/hideforward_24.pdf
deleted file mode 100644
index b05af4f661..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/UserCrossed.imageset/hideforward_24.pdf
+++ /dev/null
@@ -1,112 +0,0 @@
-%PDF-1.7
-
-1 0 obj
- << >>
-endobj
-
-2 0 obj
- << /Length 3 0 R >>
-stream
-/DeviceRGB CS
-/DeviceRGB cs
-q
-1.000000 0.000000 -0.000000 1.000000 3.334961 1.270081 cm
-0.000000 0.000000 0.000000 scn
-6.330028 15.229959 m
-6.330028 16.519543 7.375443 17.564959 8.665028 17.564959 c
-9.954613 17.564959 11.000028 16.519543 11.000028 15.229959 c
-11.000028 13.940373 9.954613 12.894958 8.665028 12.894958 c
-7.375443 12.894958 6.330028 13.940373 6.330028 15.229959 c
-h
-8.665028 18.894958 m
-6.640904 18.894958 5.000028 17.254082 5.000028 15.229959 c
-5.000028 13.205835 6.640904 11.564959 8.665028 11.564959 c
-10.689151 11.564959 12.330028 13.205835 12.330028 15.229959 c
-12.330028 17.254082 10.689151 18.894958 8.665028 18.894958 c
-h
-2.847365 5.874736 m
-3.182212 6.620716 3.727596 7.408447 4.622032 8.012360 c
-5.052609 8.303081 5.574740 8.558434 6.209703 8.744807 c
-5.164166 9.790345 l
-4.686711 9.600591 4.259488 9.372349 3.877790 9.114632 c
-2.736064 8.343752 2.047248 7.340033 1.633996 6.419378 c
-1.238339 5.537922 1.434021 4.662781 1.966020 4.035870 c
-2.481754 3.428125 3.295742 3.064959 4.165028 3.064959 c
-11.889552 3.064959 l
-10.559552 4.394958 l
-4.165028 4.394958 l
-3.653601 4.394958 3.222711 4.610523 2.980097 4.896420 c
-2.753749 5.163151 2.677613 5.496559 2.847365 5.874736 c
-h
-14.796587 3.538785 m
-17.135227 1.200146 l
-17.394926 0.940447 17.394926 0.519392 17.135227 0.259693 c
-16.875528 -0.000006 16.454473 -0.000006 16.194775 0.259693 c
-0.194774 16.259693 l
--0.064925 16.519392 -0.064925 16.940447 0.194774 17.200146 c
-0.454473 17.459845 0.875527 17.459845 1.135226 17.200146 c
-7.960464 10.374907 l
-8.188262 10.388149 8.423051 10.394958 8.665028 10.394958 c
-10.754458 10.394958 12.307954 9.887258 13.452266 9.114632 c
-14.593992 8.343751 15.282808 7.340032 15.696060 6.419377 c
-16.091717 5.537921 15.896035 4.662780 15.364036 4.035870 c
-15.201860 3.844761 15.010192 3.677837 14.796587 3.538785 c
-h
-13.815245 4.520127 m
-14.036389 4.610582 14.220131 4.743431 14.349958 4.896420 c
-14.576306 5.163150 14.652442 5.496557 14.482691 5.874735 c
-14.147844 6.620715 13.602460 7.408447 12.708024 8.012359 c
-11.919037 8.545074 10.822649 8.959038 9.287930 9.047441 c
-13.815245 4.520127 l
-h
-f*
-n
-Q
-
-endstream
-endobj
-
-3 0 obj
- 2105
-endobj
-
-4 0 obj
- << /Annots []
- /Type /Page
- /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
- /Resources 1 0 R
- /Contents 2 0 R
- /Parent 5 0 R
- >>
-endobj
-
-5 0 obj
- << /Kids [ 4 0 R ]
- /Count 1
- /Type /Pages
- >>
-endobj
-
-6 0 obj
- << /Pages 5 0 R
- /Type /Catalog
- >>
-endobj
-
-xref
-0 7
-0000000000 65535 f
-0000000010 00000 n
-0000000034 00000 n
-0000002195 00000 n
-0000002218 00000 n
-0000002391 00000 n
-0000002465 00000 n
-trailer
-<< /ID [ (some) (id) ]
- /Root 6 0 R
- /Size 7
->>
-startxref
-2524
-%%EOF
\ No newline at end of file
diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Empty Chat/PremiumRequiredIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Empty Chat/PremiumRequiredIcon.imageset/Contents.json
deleted file mode 100644
index a2cead8511..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Chat/Empty Chat/PremiumRequiredIcon.imageset/Contents.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "images" : [
- {
- "filename" : "premiumlockedchat.pdf",
- "idiom" : "universal"
- }
- ],
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Empty Chat/PremiumRequiredIcon.imageset/premiumlockedchat.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Empty Chat/PremiumRequiredIcon.imageset/premiumlockedchat.pdf
deleted file mode 100644
index 56f0ca020c..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Chat/Empty Chat/PremiumRequiredIcon.imageset/premiumlockedchat.pdf
+++ /dev/null
@@ -1,188 +0,0 @@
-%PDF-1.7
-
-1 0 obj
- << /Type /XObject
- /Length 2 0 R
- /Group << /Type /Group
- /S /Transparency
- >>
- /Subtype /Form
- /Resources << >>
- /BBox [ 0.000000 0.000000 120.000000 120.000000 ]
- >>
-stream
-/DeviceRGB CS
-/DeviceRGB cs
-q
-1.000000 0.000000 -0.000000 1.000000 26.666748 23.947998 cm
-0.000000 0.000000 0.000000 scn
-33.333332 67.118652 m
-51.742825 67.118652 66.666664 53.551525 66.666664 36.815620 c
-66.666664 20.079723 51.742825 6.512592 33.333332 6.512592 c
-30.510870 6.512592 27.770340 6.831497 25.152962 7.431835 c
-24.657856 7.545395 24.095694 7.026123 23.157236 6.159271 c
-22.022297 5.110928 20.337004 3.554222 17.554379 1.993690 c
-13.999221 -0.000084 9.073529 0.172890 8.239902 0.522552 c
-7.440670 0.857796 8.055842 1.524185 9.139198 2.697731 c
-9.888692 3.509621 10.862268 4.564251 11.746614 5.919903 c
-13.909238 9.235085 13.015616 13.181854 12.045396 13.892178 c
-4.458577 19.446659 0.000000 27.270615 0.000000 36.815620 c
-0.000000 53.551525 14.923841 67.118652 33.333332 67.118652 c
-h
-25.627058 49.241592 m
-26.466211 54.000668 31.004473 57.178391 35.763550 56.339237 c
-40.522621 55.500084 43.700348 50.961823 42.861195 46.202747 c
-41.652504 39.347935 l
-42.190289 39.251141 42.644272 39.108437 43.057198 38.898041 c
-44.186165 38.322800 45.104050 37.404915 45.679291 36.275944 c
-46.333252 34.992474 46.333252 33.312317 46.333252 29.952000 c
-46.333252 28.152000 l
-46.333252 24.791687 46.333252 23.111526 45.679291 21.828056 c
-45.104050 20.699085 44.186165 19.781200 43.057198 19.205959 c
-41.773727 18.552002 40.093567 18.552002 36.733253 18.552002 c
-29.933254 18.552002 l
-26.572937 18.552002 24.892780 18.552002 23.609308 19.205959 c
-22.480337 19.781200 21.562452 20.699085 20.987213 21.828056 c
-20.333252 23.111526 20.333252 24.791687 20.333252 28.152000 c
-20.333252 29.952000 l
-20.333252 33.312317 20.333252 34.992474 20.987213 36.275944 c
-21.562452 37.404915 22.480337 38.322800 23.609308 38.898041 c
-24.892780 39.552002 26.572937 39.552002 29.933252 39.552002 c
-36.733253 39.552002 l
-37.045204 39.552002 37.342670 39.552002 37.626686 39.551476 c
-38.921963 46.897339 l
-39.377502 49.480839 37.652451 51.944466 35.068954 52.400005 c
-32.485458 52.855545 30.021830 51.130497 29.566288 48.546997 c
-29.436054 47.808395 l
-29.244247 46.720604 28.206930 45.994267 27.119141 46.186073 c
-26.031353 46.377880 25.305016 47.415199 25.496822 48.502983 c
-25.627058 49.241592 l
-h
-f*
-n
-Q
-
-endstream
-endobj
-
-2 0 obj
- 2182
-endobj
-
-3 0 obj
- << /Type /XObject
- /Length 4 0 R
- /Group << /Type /Group
- /S /Transparency
- >>
- /Subtype /Form
- /Resources << >>
- /BBox [ 0.000000 0.000000 120.000000 120.000000 ]
- >>
-stream
-/DeviceRGB CS
-/DeviceRGB cs
-q
-1.000000 0.000000 -0.000000 1.000000 10.000000 9.000000 cm
-0.000000 0.000000 0.000000 scn
-0.000000 62.666668 m
-0.000000 75.734558 0.000000 82.268509 2.543181 87.259781 c
-4.780226 91.650230 8.349772 95.219772 12.740221 97.456818 c
-17.731495 100.000000 24.265440 100.000000 37.333332 100.000000 c
-62.666668 100.000000 l
-75.734558 100.000000 82.268509 100.000000 87.259781 97.456818 c
-91.650230 95.219772 95.219772 91.650230 97.456818 87.259781 c
-100.000000 82.268509 100.000000 75.734558 100.000000 62.666668 c
-100.000000 37.333332 l
-100.000000 24.265442 100.000000 17.731491 97.456818 12.740219 c
-95.219772 8.349770 91.650230 4.780228 87.259781 2.543182 c
-82.268509 0.000000 75.734558 0.000000 62.666668 0.000000 c
-37.333332 0.000000 l
-24.265440 0.000000 17.731495 0.000000 12.740221 2.543182 c
-8.349772 4.780228 4.780226 8.349770 2.543181 12.740219 c
-0.000000 17.731491 0.000000 24.265442 0.000000 37.333332 c
-0.000000 62.666668 l
-h
-f
-n
-Q
-
-endstream
-endobj
-
-4 0 obj
- 969
-endobj
-
-5 0 obj
- << /XObject << /X1 1 0 R >>
- /ExtGState << /E1 << /SMask << /Type /Mask
- /G 3 0 R
- /S /Alpha
- >>
- /Type /ExtGState
- >> >>
- >>
-endobj
-
-6 0 obj
- << /Length 7 0 R >>
-stream
-/DeviceRGB CS
-/DeviceRGB cs
-q
-/E1 gs
-/X1 Do
-Q
-
-endstream
-endobj
-
-7 0 obj
- 46
-endobj
-
-8 0 obj
- << /Annots []
- /Type /Page
- /MediaBox [ 0.000000 0.000000 120.000000 120.000000 ]
- /Resources 5 0 R
- /Contents 6 0 R
- /Parent 9 0 R
- >>
-endobj
-
-9 0 obj
- << /Kids [ 8 0 R ]
- /Count 1
- /Type /Pages
- >>
-endobj
-
-10 0 obj
- << /Pages 9 0 R
- /Type /Catalog
- >>
-endobj
-
-xref
-0 11
-0000000000 65535 f
-0000000010 00000 n
-0000002442 00000 n
-0000002465 00000 n
-0000003684 00000 n
-0000003706 00000 n
-0000004004 00000 n
-0000004106 00000 n
-0000004127 00000 n
-0000004302 00000 n
-0000004376 00000 n
-trailer
-<< /ID [ (some) (id) ]
- /Root 10 0 R
- /Size 11
->>
-startxref
-4436
-%%EOF
\ No newline at end of file
diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputSearchIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputSearchIcon.imageset/Contents.json
deleted file mode 100644
index ee96d83980..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputSearchIcon.imageset/Contents.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
- "images" : [
- {
- "filename" : "search_24.svg",
- "idiom" : "universal"
- }
- ],
- "info" : {
- "author" : "xcode",
- "version" : 1
- },
- "properties" : {
- "template-rendering-intent" : "template"
- }
-}
diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputSearchIcon.imageset/search_24.svg b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputSearchIcon.imageset/search_24.svg
deleted file mode 100644
index aebf081900..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputSearchIcon.imageset/search_24.svg
+++ /dev/null
@@ -1,3 +0,0 @@
-
diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Text/AccessoryIconReaction.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Input/Text/AccessoryIconReaction.imageset/Contents.json
deleted file mode 100644
index dcbff79c27..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Chat/Input/Text/AccessoryIconReaction.imageset/Contents.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "images" : [
- {
- "filename" : "InputReactionIcon.svg",
- "idiom" : "universal"
- }
- ],
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Text/AccessoryIconReaction.imageset/InputReactionIcon.svg b/submodules/TelegramUI/Images.xcassets/Chat/Input/Text/AccessoryIconReaction.imageset/InputReactionIcon.svg
deleted file mode 100644
index 0e358991b8..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Chat/Input/Text/AccessoryIconReaction.imageset/InputReactionIcon.svg
+++ /dev/null
@@ -1,3 +0,0 @@
-
diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/ExpiredStoryPlaceholder.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Message/ExpiredStoryPlaceholder.imageset/Contents.json
deleted file mode 100644
index 037cefa85b..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Chat/Message/ExpiredStoryPlaceholder.imageset/Contents.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "images" : [
- {
- "filename" : "Slice 1.png",
- "idiom" : "universal"
- }
- ],
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/ExpiredStoryPlaceholder.imageset/Slice 1.png b/submodules/TelegramUI/Images.xcassets/Chat/Message/ExpiredStoryPlaceholder.imageset/Slice 1.png
deleted file mode 100644
index 7546315a2c..0000000000
Binary files a/submodules/TelegramUI/Images.xcassets/Chat/Message/ExpiredStoryPlaceholder.imageset/Slice 1.png and /dev/null differ
diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/ExternalLink.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Message/ExternalLink.imageset/Contents.json
deleted file mode 100644
index 740a85579e..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Chat/Message/ExternalLink.imageset/Contents.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "images" : [
- {
- "filename" : "addlink_16.pdf",
- "idiom" : "universal"
- }
- ],
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/ExternalLink.imageset/addlink_16.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Message/ExternalLink.imageset/addlink_16.pdf
deleted file mode 100644
index 0eb367086f..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Chat/Message/ExternalLink.imageset/addlink_16.pdf
+++ /dev/null
@@ -1,236 +0,0 @@
-%PDF-1.7
-
-1 0 obj
- << >>
-endobj
-
-2 0 obj
- << /Length 3 0 R >>
-stream
-/DeviceRGB CS
-/DeviceRGB cs
-q
-1.000000 0.000000 -0.000000 1.000000 8.500000 7.040154 cm
-0.000000 0.000000 0.000000 scn
--0.470226 1.930072 m
--0.729925 1.670374 -0.729925 1.249319 -0.470226 0.989621 c
--0.210527 0.729922 0.210527 0.729922 0.470226 0.989621 c
--0.470226 1.930072 l
-h
-6.470226 6.989621 m
-6.729925 7.249319 6.729925 7.670374 6.470226 7.930072 c
-6.210527 8.189772 5.789473 8.189772 5.529774 7.930072 c
-6.470226 6.989621 l
-h
-0.470226 0.989621 m
-6.470226 6.989621 l
-5.529774 7.930072 l
--0.470226 1.930072 l
-0.470226 0.989621 l
-h
-f
-n
-Q
-q
-1.000000 0.000000 -0.000000 1.000000 9.500000 8.170006 cm
-0.000000 0.000000 0.000000 scn
-0.000000 6.994994 m
--0.367269 6.994994 -0.665000 6.697264 -0.665000 6.329994 c
--0.665000 5.962725 -0.367269 5.664994 0.000000 5.664994 c
-0.000000 6.994994 l
-h
-5.000000 6.329994 m
-5.665000 6.329994 l
-5.665000 6.697264 5.367270 6.994994 5.000000 6.994994 c
-5.000000 6.329994 l
-h
-4.335000 1.329994 m
-4.335000 0.962725 4.632730 0.664994 5.000000 0.664994 c
-5.367270 0.664994 5.665000 0.962725 5.665000 1.329994 c
-4.335000 1.329994 l
-h
-0.000000 5.664994 m
-5.000000 5.664994 l
-5.000000 6.994994 l
-0.000000 6.994994 l
-0.000000 5.664994 l
-h
-4.335000 6.329994 m
-4.335000 1.329994 l
-5.665000 1.329994 l
-5.665000 6.329994 l
-4.335000 6.329994 l
-h
-f
-n
-Q
-q
-1.000000 0.000000 -0.000000 1.000000 3.000000 1.668968 cm
-0.000000 0.000000 0.000000 scn
-4.000000 10.666032 m
-4.367270 10.666032 4.665000 10.963762 4.665000 11.331032 c
-4.665000 11.698301 4.367270 11.996032 4.000000 11.996032 c
-4.000000 10.666032 l
-h
-10.665000 5.331032 m
-10.665000 5.698301 10.367270 5.996032 10.000000 5.996032 c
-9.632730 5.996032 9.335000 5.698301 9.335000 5.331032 c
-10.665000 5.331032 l
-h
-8.907981 1.549019 m
-9.209885 0.956499 l
-8.907981 1.549019 l
-h
-9.782013 2.423051 m
-10.374533 2.121147 l
-9.782013 2.423051 l
-h
-1.092019 11.113045 m
-0.790115 11.705564 l
-1.092019 11.113045 l
-h
-0.217987 10.239013 m
--0.374532 10.540916 l
-0.217987 10.239013 l
-h
-4.000000 11.996032 m
-3.200000 11.996032 l
-3.200000 10.666032 l
-4.000000 10.666032 l
-4.000000 11.996032 l
-h
--0.665000 8.131032 m
--0.665000 4.531032 l
-0.665000 4.531032 l
-0.665000 8.131032 l
--0.665000 8.131032 l
-h
-3.200000 0.666032 m
-6.800000 0.666032 l
-6.800000 1.996032 l
-3.200000 1.996032 l
-3.200000 0.666032 l
-h
-10.665000 4.531032 m
-10.665000 5.331032 l
-9.335000 5.331032 l
-9.335000 4.531032 l
-10.665000 4.531032 l
-h
-6.800000 0.666032 m
-7.349080 0.666032 7.800883 0.665515 8.167748 0.695489 c
-8.542377 0.726097 8.886601 0.791779 9.209885 0.956499 c
-8.606077 2.141538 l
-8.501536 2.088272 8.351824 2.044960 8.059443 2.021071 c
-7.759301 1.996549 7.371026 1.996032 6.800000 1.996032 c
-6.800000 0.666032 l
-h
-9.335000 4.531032 m
-9.335000 3.960006 9.334483 3.571731 9.309960 3.271588 c
-9.286072 2.979208 9.242760 2.829495 9.189494 2.724955 c
-10.374533 2.121147 l
-10.539253 2.444430 10.604935 2.788655 10.635543 3.163283 c
-10.665517 3.530149 10.665000 3.981952 10.665000 4.531032 c
-9.335000 4.531032 l
-h
-9.209885 0.956499 m
-9.711337 1.212002 10.119030 1.619695 10.374533 2.121147 c
-9.189494 2.724955 l
-9.061502 2.473758 8.857274 2.269529 8.606077 2.141538 c
-9.209885 0.956499 l
-h
--0.665000 4.531032 m
--0.665000 3.981952 -0.665517 3.530149 -0.635543 3.163283 c
--0.604935 2.788655 -0.539253 2.444430 -0.374532 2.121147 c
-0.810506 2.724955 l
-0.757240 2.829495 0.713928 2.979208 0.690040 3.271588 c
-0.665517 3.571731 0.665000 3.960006 0.665000 4.531032 c
--0.665000 4.531032 l
-h
-3.200000 1.996032 m
-2.628974 1.996032 2.240699 1.996549 1.940556 2.021071 c
-1.648176 2.044960 1.498463 2.088272 1.393923 2.141538 c
-0.790115 0.956499 l
-1.113398 0.791779 1.457623 0.726097 1.832252 0.695489 c
-2.199117 0.665515 2.650921 0.666032 3.200000 0.666032 c
-3.200000 1.996032 l
-h
--0.374532 2.121147 m
--0.119030 1.619695 0.288663 1.212002 0.790115 0.956499 c
-1.393923 2.141538 l
-1.142726 2.269529 0.938497 2.473758 0.810506 2.724955 c
--0.374532 2.121147 l
-h
-3.200000 11.996032 m
-2.650921 11.996032 2.199117 11.996549 1.832252 11.966575 c
-1.457623 11.935966 1.113398 11.870285 0.790115 11.705564 c
-1.393923 10.520525 l
-1.498463 10.573792 1.648176 10.617104 1.940556 10.640992 c
-2.240699 10.665515 2.628974 10.666032 3.200000 10.666032 c
-3.200000 11.996032 l
-h
-0.665000 8.131032 m
-0.665000 8.702057 0.665517 9.090332 0.690040 9.390476 c
-0.713928 9.682856 0.757240 9.832568 0.810506 9.937109 c
--0.374532 10.540916 l
--0.539253 10.217633 -0.604935 9.873408 -0.635543 9.498780 c
--0.665517 9.131915 -0.665000 8.680111 -0.665000 8.131032 c
-0.665000 8.131032 l
-h
-0.790115 11.705564 m
-0.288663 11.450062 -0.119030 11.042369 -0.374532 10.540916 c
-0.810506 9.937109 l
-0.938497 10.188306 1.142726 10.392534 1.393923 10.520525 c
-0.790115 11.705564 l
-h
-f
-n
-Q
-
-endstream
-endobj
-
-3 0 obj
- 4659
-endobj
-
-4 0 obj
- << /Annots []
- /Type /Page
- /MediaBox [ 0.000000 0.000000 16.000000 16.000000 ]
- /Resources 1 0 R
- /Contents 2 0 R
- /Parent 5 0 R
- >>
-endobj
-
-5 0 obj
- << /Kids [ 4 0 R ]
- /Count 1
- /Type /Pages
- >>
-endobj
-
-6 0 obj
- << /Pages 5 0 R
- /Type /Catalog
- >>
-endobj
-
-xref
-0 7
-0000000000 65535 f
-0000000010 00000 n
-0000000034 00000 n
-0000004749 00000 n
-0000004772 00000 n
-0000004945 00000 n
-0000005019 00000 n
-trailer
-<< /ID [ (some) (id) ]
- /Root 6 0 R
- /Size 7
->>
-startxref
-5078
-%%EOF
\ No newline at end of file
diff --git a/submodules/TelegramUI/Images.xcassets/Contact List/PeopleNearbyIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Contact List/PeopleNearbyIcon.imageset/Contents.json
deleted file mode 100644
index 77dd2fca4e..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Contact List/PeopleNearbyIcon.imageset/Contents.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "images" : [
- {
- "idiom" : "universal",
- "filename" : "ic_location.pdf"
- }
- ],
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
\ No newline at end of file
diff --git a/submodules/TelegramUI/Images.xcassets/Contact List/PeopleNearbyIcon.imageset/ic_location.pdf b/submodules/TelegramUI/Images.xcassets/Contact List/PeopleNearbyIcon.imageset/ic_location.pdf
deleted file mode 100644
index 45afa9b5f7..0000000000
Binary files a/submodules/TelegramUI/Images.xcassets/Contact List/PeopleNearbyIcon.imageset/ic_location.pdf and /dev/null differ
diff --git a/submodules/TelegramUI/Images.xcassets/Instant View/CloseSmall.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Instant View/CloseSmall.imageset/Contents.json
deleted file mode 100644
index f6826555b5..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Instant View/CloseSmall.imageset/Contents.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "images" : [
- {
- "filename" : "ic_ivclosesmall.pdf",
- "idiom" : "universal"
- }
- ],
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/submodules/TelegramUI/Images.xcassets/Instant View/CloseSmall.imageset/ic_ivclosesmall.pdf b/submodules/TelegramUI/Images.xcassets/Instant View/CloseSmall.imageset/ic_ivclosesmall.pdf
deleted file mode 100644
index 04f8b15429..0000000000
Binary files a/submodules/TelegramUI/Images.xcassets/Instant View/CloseSmall.imageset/ic_ivclosesmall.pdf and /dev/null differ
diff --git a/submodules/TelegramUI/Images.xcassets/Location/PinIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Location/PinIcon.imageset/Contents.json
deleted file mode 100644
index 3b8e5cbe3c..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Location/PinIcon.imageset/Contents.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "images" : [
- {
- "idiom" : "universal",
- "scale" : "1x"
- },
- {
- "idiom" : "universal",
- "filename" : "LocationPinIcon@2x.png",
- "scale" : "2x"
- },
- {
- "idiom" : "universal",
- "filename" : "LocationPinIcon@3x.png",
- "scale" : "3x"
- }
- ],
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
\ No newline at end of file
diff --git a/submodules/TelegramUI/Images.xcassets/Location/PinIcon.imageset/LocationPinIcon@2x.png b/submodules/TelegramUI/Images.xcassets/Location/PinIcon.imageset/LocationPinIcon@2x.png
deleted file mode 100644
index ab6fd29ccb..0000000000
Binary files a/submodules/TelegramUI/Images.xcassets/Location/PinIcon.imageset/LocationPinIcon@2x.png and /dev/null differ
diff --git a/submodules/TelegramUI/Images.xcassets/Location/PinIcon.imageset/LocationPinIcon@3x.png b/submodules/TelegramUI/Images.xcassets/Location/PinIcon.imageset/LocationPinIcon@3x.png
deleted file mode 100644
index a57e0d4252..0000000000
Binary files a/submodules/TelegramUI/Images.xcassets/Location/PinIcon.imageset/LocationPinIcon@3x.png and /dev/null differ
diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQAuto.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQAuto.imageset/Contents.json
deleted file mode 100644
index e2fa1abb90..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQAuto.imageset/Contents.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "images" : [
- {
- "filename" : "videosettingsauto_30.pdf",
- "idiom" : "universal"
- }
- ],
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQAuto.imageset/videosettingsauto_30.pdf b/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQAuto.imageset/videosettingsauto_30.pdf
deleted file mode 100644
index d36b2d7686..0000000000
Binary files a/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQAuto.imageset/videosettingsauto_30.pdf and /dev/null differ
diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQHD.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQHD.imageset/Contents.json
deleted file mode 100644
index 1f7bfc2bb2..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQHD.imageset/Contents.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "images" : [
- {
- "filename" : "videosettingshd_30.pdf",
- "idiom" : "universal"
- }
- ],
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQHD.imageset/videosettingshd_30.pdf b/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQHD.imageset/videosettingshd_30.pdf
deleted file mode 100644
index 735db423a8..0000000000
Binary files a/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQHD.imageset/videosettingshd_30.pdf and /dev/null differ
diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQSD.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQSD.imageset/Contents.json
deleted file mode 100644
index 8f71976b5f..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQSD.imageset/Contents.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "images" : [
- {
- "filename" : "videosettingssd_30.pdf",
- "idiom" : "universal"
- }
- ],
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQSD.imageset/videosettingssd_30.pdf b/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQSD.imageset/videosettingssd_30.pdf
deleted file mode 100644
index c77db5874e..0000000000
Binary files a/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQSD.imageset/videosettingssd_30.pdf and /dev/null differ
diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/CollectibleUsernameInfoTitleIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Peer Info/CollectibleUsernameInfoTitleIcon.imageset/Contents.json
deleted file mode 100644
index 6a062695f3..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Peer Info/CollectibleUsernameInfoTitleIcon.imageset/Contents.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "images" : [
- {
- "filename" : "username.pdf",
- "idiom" : "universal"
- }
- ],
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/CollectibleUsernameInfoTitleIcon.imageset/username.pdf b/submodules/TelegramUI/Images.xcassets/Peer Info/CollectibleUsernameInfoTitleIcon.imageset/username.pdf
deleted file mode 100644
index 548f72b27c..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Peer Info/CollectibleUsernameInfoTitleIcon.imageset/username.pdf
+++ /dev/null
@@ -1,100 +0,0 @@
-%PDF-1.7
-
-1 0 obj
- << >>
-endobj
-
-2 0 obj
- << /Length 3 0 R >>
-stream
-/DeviceRGB CS
-/DeviceRGB cs
-q
-1.000000 0.000000 -0.000000 1.000000 19.254883 19.005127 cm
-0.000000 0.000000 0.000000 scn
-0.000000 25.994989 m
-0.000000 40.351631 11.638357 51.989990 25.995001 51.989990 c
-40.351643 51.989990 51.989998 40.351631 51.989998 25.994989 c
-51.989998 19.994989 l
-51.989998 15.579475 48.410515 11.999992 43.994999 11.999992 c
-40.680130 11.999992 37.836452 14.017384 36.624741 16.891380 c
-34.058002 13.897156 30.248056 11.999989 25.995001 11.999989 c
-18.265776 11.999989 12.000000 18.265766 12.000000 25.994989 c
-12.000000 33.724213 18.265776 39.989990 25.995001 39.989990 c
-29.919403 39.989990 33.466534 38.374702 36.007809 35.772713 c
-36.097725 36.791267 36.953087 37.589989 37.994999 37.589989 c
-39.096806 37.589989 39.989998 36.696800 39.989998 35.594986 c
-39.989998 26.005434 l
-39.990002 25.994989 l
-39.989998 25.984545 l
-39.989998 19.994995 l
-39.989998 17.783092 41.783100 15.989990 43.994999 15.989990 c
-46.206898 15.989990 48.000000 17.783092 48.000000 19.994989 c
-48.000000 25.994989 l
-48.000000 38.148018 38.148026 47.999989 25.995001 47.999989 c
-13.841973 47.999989 3.990000 38.148018 3.990000 25.994989 c
-3.990000 25.793646 3.992699 25.592970 3.998061 25.392994 c
-4.317001 13.518284 14.043282 3.989990 25.995001 3.989990 c
-29.449270 3.989990 32.712318 4.784481 35.616467 6.198845 c
-36.607044 6.681271 37.801151 6.269333 38.283577 5.278755 c
-38.766006 4.288174 38.354069 3.094070 37.363487 2.611641 c
-33.926983 0.938011 30.067709 -0.000008 25.995001 -0.000008 c
-11.638357 -0.000008 0.000000 11.638348 0.000000 25.994989 c
-h
-25.995001 35.999992 m
-31.520609 35.999992 35.999996 31.520597 35.999996 25.994989 c
-35.999996 20.469381 31.520609 15.989994 25.995001 15.989994 c
-20.469393 15.989994 15.990000 20.469381 15.990000 25.994989 c
-15.990000 31.520597 20.469393 35.999992 25.995001 35.999992 c
-h
-f*
-n
-Q
-
-endstream
-endobj
-
-3 0 obj
- 1836
-endobj
-
-4 0 obj
- << /Annots []
- /Type /Page
- /MediaBox [ 0.000000 0.000000 90.000000 90.000000 ]
- /Resources 1 0 R
- /Contents 2 0 R
- /Parent 5 0 R
- >>
-endobj
-
-5 0 obj
- << /Kids [ 4 0 R ]
- /Count 1
- /Type /Pages
- >>
-endobj
-
-6 0 obj
- << /Pages 5 0 R
- /Type /Catalog
- >>
-endobj
-
-xref
-0 7
-0000000000 65535 f
-0000000010 00000 n
-0000000034 00000 n
-0000001926 00000 n
-0000001949 00000 n
-0000002122 00000 n
-0000002196 00000 n
-trailer
-<< /ID [ (some) (id) ]
- /Root 6 0 R
- /Size 7
->>
-startxref
-2255
-%%EOF
\ No newline at end of file
diff --git a/submodules/TelegramUI/Images.xcassets/Premium/BoostReplaceIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/BoostReplaceIcon.imageset/Contents.json
deleted file mode 100644
index dc0972e257..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Premium/BoostReplaceIcon.imageset/Contents.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "images" : [
- {
- "filename" : "replacedboost_30.pdf",
- "idiom" : "universal"
- }
- ],
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/submodules/TelegramUI/Images.xcassets/Premium/BoostReplaceIcon.imageset/replacedboost_30.pdf b/submodules/TelegramUI/Images.xcassets/Premium/BoostReplaceIcon.imageset/replacedboost_30.pdf
deleted file mode 100644
index d6cbcced6a..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Premium/BoostReplaceIcon.imageset/replacedboost_30.pdf
+++ /dev/null
@@ -1,273 +0,0 @@
-%PDF-1.7
-
-1 0 obj
- << >>
-endobj
-
-2 0 obj
- << /Length 3 0 R >>
-stream
-/DeviceRGB CS
-/DeviceRGB cs
-q
-1.000000 0.000000 -0.000000 1.000000 4.877197 15.157974 cm
-0.000000 0.000000 0.000000 scn
-5.552186 8.359201 m
-5.243867 8.359201 5.008486 8.634665 5.056572 8.939211 c
-5.727019 13.185373 l
-5.810456 13.713807 5.119398 13.988482 4.817303 13.546960 c
-0.088512 6.635651 l
--0.139333 6.302646 0.099122 5.850563 0.502614 5.850563 c
-2.581887 5.850563 l
-2.890206 5.850563 3.125587 5.575099 3.077501 5.270554 c
-2.407054 1.024391 l
-2.323617 0.495958 3.014675 0.221282 3.316770 0.662805 c
-8.045561 7.574114 l
-8.273406 7.907119 8.034951 8.359201 7.631459 8.359201 c
-5.552186 8.359201 l
-h
-f*
-n
-Q
-q
-1.000000 0.000000 -0.000000 1.000000 16.877197 1.157974 cm
-0.000000 0.000000 0.000000 scn
-5.552186 8.359201 m
-5.243867 8.359201 5.008486 8.634665 5.056572 8.939211 c
-5.727019 13.185373 l
-5.810456 13.713807 5.119398 13.988482 4.817303 13.546960 c
-0.088512 6.635651 l
--0.139333 6.302646 0.099122 5.850563 0.502614 5.850563 c
-2.581887 5.850563 l
-2.890206 5.850563 3.125587 5.575099 3.077501 5.270554 c
-2.407054 1.024391 l
-2.323617 0.495958 3.014675 0.221282 3.316770 0.662805 c
-8.045561 7.574114 l
-8.273406 7.907119 8.034951 8.359201 7.631459 8.359201 c
-5.552186 8.359201 l
-h
-f*
-n
-Q
-q
-1.000000 0.000000 -0.000000 1.000000 20.000000 15.177917 cm
-0.000000 0.000000 0.000000 scn
-6.586899 4.235184 m
-6.911034 4.559319 6.911034 5.084846 6.586899 5.408981 c
-6.262764 5.733116 5.737236 5.733116 5.413101 5.408981 c
-6.586899 4.235184 l
-h
-3.000000 1.822083 m
-2.413101 1.235184 l
-2.737236 0.911049 3.262764 0.911049 3.586899 1.235184 c
-3.000000 1.822083 l
-h
-0.586899 5.408981 m
-0.262763 5.733116 -0.262763 5.733116 -0.586899 5.408981 c
--0.911034 5.084846 -0.911034 4.559319 -0.586899 4.235184 c
-0.586899 5.408981 l
-h
-5.413101 5.408981 m
-2.413101 2.408981 l
-3.586899 1.235184 l
-6.586899 4.235184 l
-5.413101 5.408981 l
-h
-3.586899 2.408981 m
-0.586899 5.408981 l
--0.586899 4.235184 l
-2.413101 1.235184 l
-3.586899 2.408981 l
-h
-f
-n
-Q
-q
-1.000000 0.000000 -0.000000 1.000000 16.000000 15.339355 cm
-0.000000 0.000000 0.000000 scn
-0.000000 10.490644 m
--0.458396 10.490644 -0.830000 10.119040 -0.830000 9.660645 c
--0.830000 9.202249 -0.458396 8.830645 0.000000 8.830645 c
-0.000000 10.490644 l
-h
-6.170000 1.660645 m
-6.170000 1.202249 6.541604 0.830645 7.000000 0.830645 c
-7.458396 0.830645 7.830000 1.202249 7.830000 1.660645 c
-6.170000 1.660645 l
-h
-6.727516 8.295621 m
-7.467052 8.672433 l
-6.727516 8.295621 l
-h
-0.000000 8.830645 m
-3.000000 8.830645 l
-3.000000 10.490644 l
-0.000000 10.490644 l
-0.000000 8.830645 l
-h
-6.170000 5.660645 m
-6.170000 1.660645 l
-7.830000 1.660645 l
-7.830000 5.660645 l
-6.170000 5.660645 l
-h
-3.000000 8.830645 m
-3.713761 8.830645 4.199165 8.829999 4.574407 8.799340 c
-4.939959 8.769474 5.127283 8.715313 5.258164 8.648625 c
-6.011788 10.127696 l
-5.607891 10.333492 5.177792 10.415573 4.709584 10.453828 c
-4.251065 10.491290 3.686370 10.490644 3.000000 10.490644 c
-3.000000 8.830645 l
-h
-7.830000 5.660645 m
-7.830000 6.347014 7.830646 6.911709 7.793183 7.370228 c
-7.754929 7.838436 7.672848 8.268535 7.467052 8.672433 c
-5.987981 7.918809 l
-6.054668 7.787928 6.108829 7.600603 6.138696 7.235051 c
-6.169354 6.859809 6.170000 6.374406 6.170000 5.660645 c
-7.830000 5.660645 l
-h
-5.258164 8.648625 m
-5.572395 8.488517 5.827872 8.233040 5.987981 7.918809 c
-7.467052 8.672433 l
-7.147793 9.299013 6.638368 9.808438 6.011788 10.127696 c
-5.258164 8.648625 l
-h
-f
-n
-Q
-q
--1.000000 -0.000000 -0.000000 -1.000000 10.000000 14.822083 cm
-0.000000 0.000000 0.000000 scn
-6.586899 4.235184 m
-6.911034 4.559319 6.911034 5.084846 6.586899 5.408981 c
-6.262764 5.733116 5.737236 5.733116 5.413101 5.408981 c
-6.586899 4.235184 l
-h
-3.000000 1.822083 m
-2.413101 1.235184 l
-2.737236 0.911049 3.262764 0.911049 3.586899 1.235184 c
-3.000000 1.822083 l
-h
-0.586899 5.408981 m
-0.262763 5.733116 -0.262763 5.733116 -0.586899 5.408981 c
--0.911034 5.084846 -0.911034 4.559319 -0.586899 4.235184 c
-0.586899 5.408981 l
-h
-5.413101 5.408981 m
-2.413101 2.408981 l
-3.586899 1.235184 l
-6.586899 4.235184 l
-5.413101 5.408981 l
-h
-3.586899 2.408981 m
-0.586899 5.408981 l
--0.586899 4.235184 l
-2.413101 1.235184 l
-3.586899 2.408981 l
-h
-f
-n
-Q
-q
--1.000000 -0.000000 -0.000000 -1.000000 14.000000 13.660645 cm
-0.000000 0.000000 0.000000 scn
-0.000000 9.490644 m
--0.458396 9.490644 -0.830000 9.119040 -0.830000 8.660645 c
--0.830000 8.202249 -0.458396 7.830645 0.000000 7.830645 c
-0.000000 9.490644 l
-h
-6.170000 1.660645 m
-6.170000 1.202248 6.541604 0.830645 7.000000 0.830645 c
-7.458396 0.830645 7.830000 1.202248 7.830000 1.660645 c
-6.170000 1.660645 l
-h
-6.727516 7.295621 m
-7.467052 7.672433 l
-6.727516 7.295621 l
-h
-0.000000 7.830645 m
-3.000000 7.830645 l
-3.000000 9.490644 l
-0.000000 9.490644 l
-0.000000 7.830645 l
-h
-6.170000 4.660645 m
-6.170000 1.660645 l
-7.830000 1.660645 l
-7.830000 4.660645 l
-6.170000 4.660645 l
-h
-3.000000 7.830645 m
-3.713761 7.830645 4.199165 7.829999 4.574407 7.799341 c
-4.939959 7.769474 5.127283 7.715313 5.258164 7.648625 c
-6.011788 9.127696 l
-5.607891 9.333492 5.177792 9.415573 4.709584 9.453828 c
-4.251065 9.491290 3.686370 9.490644 3.000000 9.490644 c
-3.000000 7.830645 l
-h
-7.830000 4.660645 m
-7.830000 5.347014 7.830646 5.911709 7.793183 6.370228 c
-7.754929 6.838436 7.672848 7.268535 7.467052 7.672433 c
-5.987981 6.918809 l
-6.054668 6.787928 6.108829 6.600603 6.138696 6.235051 c
-6.169354 5.859809 6.170000 5.374406 6.170000 4.660645 c
-7.830000 4.660645 l
-h
-5.258164 7.648625 m
-5.572395 7.488517 5.827872 7.233039 5.987981 6.918809 c
-7.467052 7.672433 l
-7.147793 8.299013 6.638368 8.808438 6.011788 9.127696 c
-5.258164 7.648625 l
-h
-f
-n
-Q
-
-endstream
-endobj
-
-3 0 obj
- 5530
-endobj
-
-4 0 obj
- << /Annots []
- /Type /Page
- /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ]
- /Resources 1 0 R
- /Contents 2 0 R
- /Parent 5 0 R
- >>
-endobj
-
-5 0 obj
- << /Kids [ 4 0 R ]
- /Count 1
- /Type /Pages
- >>
-endobj
-
-6 0 obj
- << /Pages 5 0 R
- /Type /Catalog
- >>
-endobj
-
-xref
-0 7
-0000000000 65535 f
-0000000010 00000 n
-0000000034 00000 n
-0000005620 00000 n
-0000005643 00000 n
-0000005816 00000 n
-0000005890 00000 n
-trailer
-<< /ID [ (some) (id) ]
- /Root 6 0 R
- /Size 7
->>
-startxref
-5949
-%%EOF
\ No newline at end of file
diff --git a/submodules/TelegramUI/Images.xcassets/Premium/ForegroundIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/ForegroundIcon.imageset/Contents.json
deleted file mode 100644
index c60ea81e07..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Premium/ForegroundIcon.imageset/Contents.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "images" : [
- {
- "filename" : "addreactions2_32.pdf",
- "idiom" : "universal"
- }
- ],
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/submodules/TelegramUI/Images.xcassets/Premium/ForegroundIcon.imageset/addreactions2_32.pdf b/submodules/TelegramUI/Images.xcassets/Premium/ForegroundIcon.imageset/addreactions2_32.pdf
deleted file mode 100644
index a10889d1e3..0000000000
Binary files a/submodules/TelegramUI/Images.xcassets/Premium/ForegroundIcon.imageset/addreactions2_32.pdf and /dev/null differ
diff --git a/submodules/TelegramUI/Images.xcassets/Premium/GiftCrash.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/GiftCrash.imageset/Contents.json
deleted file mode 100644
index 27e29557d3..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Premium/GiftCrash.imageset/Contents.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "images" : [
- {
- "filename" : "giftcrash.pdf",
- "idiom" : "universal"
- }
- ],
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/submodules/TelegramUI/Images.xcassets/Premium/GiftCrash.imageset/giftcrash.pdf b/submodules/TelegramUI/Images.xcassets/Premium/GiftCrash.imageset/giftcrash.pdf
deleted file mode 100644
index 2bdf30a259..0000000000
Binary files a/submodules/TelegramUI/Images.xcassets/Premium/GiftCrash.imageset/giftcrash.pdf and /dev/null differ
diff --git a/submodules/TelegramUI/Images.xcassets/Premium/PrivacyReadTime.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/PrivacyReadTime.imageset/Contents.json
deleted file mode 100644
index 5898847ff8..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Premium/PrivacyReadTime.imageset/Contents.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "images" : [
- {
- "filename" : "ReadTime.pdf",
- "idiom" : "universal"
- }
- ],
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/submodules/TelegramUI/Images.xcassets/Premium/PrivacyReadTime.imageset/ReadTime.pdf b/submodules/TelegramUI/Images.xcassets/Premium/PrivacyReadTime.imageset/ReadTime.pdf
deleted file mode 100644
index 25f40eeac6..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Premium/PrivacyReadTime.imageset/ReadTime.pdf
+++ /dev/null
@@ -1,277 +0,0 @@
-%PDF-1.7
-
-1 0 obj
- << /Type /XObject
- /Length 2 0 R
- /Group << /Type /Group
- /S /Transparency
- >>
- /Subtype /Form
- /Resources << >>
- /BBox [ 0.000000 0.000000 90.000000 90.000000 ]
- >>
-stream
-/DeviceRGB CS
-/DeviceRGB cs
-q
-0.707107 0.707107 -0.707107 0.707107 67.034660 -18.373894 cm
-0.000000 0.000000 0.000000 scn
-10.453167 41.350018 m
-9.512874 42.238548 8.030319 42.196587 7.141789 41.256294 c
-6.253258 40.316002 6.295220 38.833447 7.235513 37.944916 c
-10.453167 41.350018 l
-h
-0.095610 63.883827 m
--2.239701 64.066391 l
-0.095610 63.883827 l
-h
--2.342436 54.749527 m
--2.342436 53.455833 -1.293692 52.407089 0.000000 52.407089 c
-1.293692 52.407089 2.342436 53.455833 2.342436 54.749527 c
--2.342436 54.749527 l
-h
-7.235513 37.944916 m
-13.076076 32.425873 20.978649 29.037750 29.669981 29.037750 c
-29.669981 33.722622 l
-22.218493 33.722622 15.455904 36.622681 10.453167 41.350018 c
-7.235513 37.944916 l
-h
-29.669981 29.037750 m
-47.645660 29.037750 62.242207 43.525253 62.242207 61.426483 c
-57.557335 61.426483 l
-57.557335 46.139488 45.085209 33.722622 29.669981 33.722622 c
-29.669981 29.037750 l
-h
-62.242207 61.426483 m
-62.242207 79.327713 47.645660 93.815216 29.669981 93.815216 c
-29.669981 89.130341 l
-45.085209 89.130341 57.557335 76.713478 57.557335 61.426483 c
-62.242207 61.426483 l
-h
-29.669981 93.815216 m
-12.540254 93.815216 -0.940433 80.686134 -2.239701 64.066391 c
-2.430921 63.701260 l
-3.546472 77.970970 15.073609 89.130341 29.669981 89.130341 c
-29.669981 93.815216 l
-h
--2.239701 64.066391 m
--2.312374 63.136772 -2.342436 55.500645 -2.342436 54.749527 c
-2.342436 54.749527 l
-2.342436 55.154526 2.350449 57.228703 2.366412 59.305004 c
-2.374389 60.342529 2.384313 61.375381 2.396135 62.197342 c
-2.402054 62.608826 2.408379 62.962509 2.415047 63.235344 c
-2.418387 63.371964 2.421699 63.483559 2.424903 63.569450 c
-2.426500 63.612274 2.427957 63.645615 2.429212 63.670444 c
-2.430558 63.697090 2.431254 63.705528 2.430921 63.701260 c
--2.239701 64.066391 l
-h
-f
-n
-Q
-q
-0.707107 0.707107 -0.707107 0.707107 35.092972 1.912642 cm
-0.000000 0.000000 0.000000 scn
-9.400195 9.052296 m
-16.491901 18.012711 l
-17.065374 18.737297 16.942873 19.789585 16.218287 20.363058 c
-15.921938 20.597603 15.554913 20.724905 15.176980 20.724241 c
-1.113485 20.699522 l
-0.497442 20.698439 -0.001081 20.198158 0.000002 19.582117 c
-0.000426 19.340685 0.079173 19.105904 0.224414 18.913044 c
-7.634510 9.073509 l
-8.005109 8.581406 8.704469 8.482906 9.196571 8.853506 c
-9.272655 8.910804 9.341086 8.977612 9.400195 9.052296 c
-h
-f*
-n
-Q
-q
-1.000000 0.000000 -0.000000 1.000000 37.394165 30.785156 cm
-0.000000 0.000000 0.000000 scn
-25.237368 22.854145 m
-26.030727 23.803909 25.903934 25.216991 24.954170 26.010351 c
-24.004406 26.803711 22.591324 26.676918 21.797964 25.727154 c
-25.237368 22.854145 l
-h
-7.466430 5.075024 m
-9.186129 3.638515 l
-9.186131 3.638519 l
-7.466430 5.075024 l
-h
-7.322968 5.062153 m
-5.886413 3.342493 l
-5.886472 3.342443 l
-7.322968 5.062153 l
-h
-7.306202 5.079990 m
-5.500865 3.752708 l
-5.500870 3.752703 l
-7.306202 5.079990 l
-h
-1.805337 16.344992 m
-1.072299 17.342052 -0.330222 17.556084 -1.327282 16.823048 c
--2.324342 16.090010 -2.538375 14.687489 -1.805337 13.690428 c
-1.805337 16.344992 l
-h
-21.797964 25.727154 m
-5.746728 6.511530 l
-9.186131 3.638519 l
-25.237368 22.854145 l
-21.797964 25.727154 l
-h
-5.746732 6.511532 m
-6.504047 7.418142 7.852895 7.539131 8.759465 6.781860 c
-5.886472 3.342443 l
-6.879384 2.513050 8.356690 2.645563 9.186129 3.638515 c
-5.746732 6.511532 l
-h
-8.759524 6.781811 m
-8.891400 6.671646 9.009609 6.545914 9.111534 6.407280 c
-5.500870 3.752703 l
-5.612496 3.600872 5.741967 3.463160 5.886413 3.342493 c
-8.759524 6.781811 l
-h
-9.111539 6.407272 m
-1.805337 16.344992 l
--1.805337 13.690428 l
-5.500865 3.752708 l
-9.111539 6.407272 l
-h
-f
-n
-Q
-q
-1.000000 0.000000 -0.000000 1.000000 29.174744 29.887207 cm
-0.000000 0.000000 0.000000 scn
-5.315279 3.728403 m
-6.056771 2.737616 7.461063 2.535522 8.451851 3.277015 c
-9.442638 4.018508 9.644732 5.422799 8.903239 6.413588 c
-5.315279 3.728403 l
-h
-1.793980 15.913027 m
-1.052487 16.903814 -0.351804 17.105907 -1.342592 16.364414 c
--2.333380 15.622922 -2.535474 14.218631 -1.793980 13.227842 c
-1.793980 15.913027 l
-h
-8.903239 6.413588 m
-1.793980 15.913027 l
--1.793980 13.227842 l
-5.315279 3.728403 l
-8.903239 6.413588 l
-h
-f
-n
-Q
-
-endstream
-endobj
-
-2 0 obj
- 4078
-endobj
-
-3 0 obj
- << /Type /XObject
- /Length 4 0 R
- /Group << /Type /Group
- /S /Transparency
- >>
- /Subtype /Form
- /Resources << >>
- /BBox [ 0.000000 0.000000 90.000000 90.000000 ]
- >>
-stream
-/DeviceRGB CS
-/DeviceRGB cs
-q
-1.000000 0.000000 -0.000000 1.000000 5.000000 5.000000 cm
-0.000000 0.000000 0.000000 scn
-0.000000 80.000000 m
-80.000000 80.000000 l
-80.000000 0.000000 l
-0.000000 0.000000 l
-0.000000 80.000000 l
-h
-f
-n
-Q
-
-endstream
-endobj
-
-4 0 obj
- 232
-endobj
-
-5 0 obj
- << /XObject << /X1 1 0 R >>
- /ExtGState << /E1 << /SMask << /Type /Mask
- /G 3 0 R
- /S /Alpha
- >>
- /Type /ExtGState
- >> >>
- >>
-endobj
-
-6 0 obj
- << /Length 7 0 R >>
-stream
-/DeviceRGB CS
-/DeviceRGB cs
-q
-/E1 gs
-/X1 Do
-Q
-
-endstream
-endobj
-
-7 0 obj
- 46
-endobj
-
-8 0 obj
- << /Annots []
- /Type /Page
- /MediaBox [ 0.000000 0.000000 90.000000 90.000000 ]
- /Resources 5 0 R
- /Contents 6 0 R
- /Parent 9 0 R
- >>
-endobj
-
-9 0 obj
- << /Kids [ 8 0 R ]
- /Count 1
- /Type /Pages
- >>
-endobj
-
-10 0 obj
- << /Pages 9 0 R
- /Type /Catalog
- >>
-endobj
-
-xref
-0 11
-0000000000 65535 f
-0000000010 00000 n
-0000004336 00000 n
-0000004359 00000 n
-0000004839 00000 n
-0000004861 00000 n
-0000005159 00000 n
-0000005261 00000 n
-0000005282 00000 n
-0000005455 00000 n
-0000005529 00000 n
-trailer
-<< /ID [ (some) (id) ]
- /Root 10 0 R
- /Size 11
->>
-startxref
-5589
-%%EOF
\ No newline at end of file
diff --git a/submodules/TelegramUI/Images.xcassets/Settings/PasscodeClearIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/PasscodeClearIcon.imageset/Contents.json
deleted file mode 100644
index 2c2f33f0da..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Settings/PasscodeClearIcon.imageset/Contents.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "images" : [
- {
- "idiom" : "universal",
- "scale" : "1x"
- },
- {
- "idiom" : "universal",
- "filename" : "clearpasscode@2x.png",
- "scale" : "2x"
- },
- {
- "idiom" : "universal",
- "filename" : "clearpasscode@3x.png",
- "scale" : "3x"
- }
- ],
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
\ No newline at end of file
diff --git a/submodules/TelegramUI/Images.xcassets/Settings/PasscodeClearIcon.imageset/clearpasscode@2x.png b/submodules/TelegramUI/Images.xcassets/Settings/PasscodeClearIcon.imageset/clearpasscode@2x.png
deleted file mode 100644
index 4fd57e8830..0000000000
Binary files a/submodules/TelegramUI/Images.xcassets/Settings/PasscodeClearIcon.imageset/clearpasscode@2x.png and /dev/null differ
diff --git a/submodules/TelegramUI/Images.xcassets/Settings/PasscodeClearIcon.imageset/clearpasscode@3x.png b/submodules/TelegramUI/Images.xcassets/Settings/PasscodeClearIcon.imageset/clearpasscode@3x.png
deleted file mode 100644
index fc6549cc0c..0000000000
Binary files a/submodules/TelegramUI/Images.xcassets/Settings/PasscodeClearIcon.imageset/clearpasscode@3x.png and /dev/null differ
diff --git a/submodules/TelegramUI/Images.xcassets/Settings/PasswordIntroIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/PasswordIntroIcon.imageset/Contents.json
deleted file mode 100644
index 3dd535cbed..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Settings/PasswordIntroIcon.imageset/Contents.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "images" : [
- {
- "idiom" : "universal",
- "scale" : "1x"
- },
- {
- "idiom" : "universal",
- "filename" : "PasswordPlaceholderIcon@2x.png",
- "scale" : "2x"
- },
- {
- "idiom" : "universal",
- "filename" : "PasswordPlaceholderIcon@3x.png",
- "scale" : "3x"
- }
- ],
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
\ No newline at end of file
diff --git a/submodules/TelegramUI/Images.xcassets/Settings/PasswordIntroIcon.imageset/PasswordPlaceholderIcon@2x.png b/submodules/TelegramUI/Images.xcassets/Settings/PasswordIntroIcon.imageset/PasswordPlaceholderIcon@2x.png
deleted file mode 100644
index c9cd47a84c..0000000000
Binary files a/submodules/TelegramUI/Images.xcassets/Settings/PasswordIntroIcon.imageset/PasswordPlaceholderIcon@2x.png and /dev/null differ
diff --git a/submodules/TelegramUI/Images.xcassets/Settings/PasswordIntroIcon.imageset/PasswordPlaceholderIcon@3x.png b/submodules/TelegramUI/Images.xcassets/Settings/PasswordIntroIcon.imageset/PasswordPlaceholderIcon@3x.png
deleted file mode 100644
index 8ce614090f..0000000000
Binary files a/submodules/TelegramUI/Images.xcassets/Settings/PasswordIntroIcon.imageset/PasswordPlaceholderIcon@3x.png and /dev/null differ
diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Power/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/Power/Contents.json
deleted file mode 100644
index 6e965652df..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Settings/Power/Contents.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "info" : {
- "author" : "xcode",
- "version" : 1
- },
- "properties" : {
- "provides-namespace" : true
- }
-}
diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconBackgroundTime.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconBackgroundTime.imageset/Contents.json
deleted file mode 100644
index f42e8f2075..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconBackgroundTime.imageset/Contents.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "images" : [
- {
- "filename" : "PowerIconBackgroundTime.svg",
- "idiom" : "universal"
- }
- ],
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconBackgroundTime.imageset/PowerIconBackgroundTime.svg b/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconBackgroundTime.imageset/PowerIconBackgroundTime.svg
deleted file mode 100644
index d4e4739bc1..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconBackgroundTime.imageset/PowerIconBackgroundTime.svg
+++ /dev/null
@@ -1,11 +0,0 @@
-
diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconEffects.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconEffects.imageset/Contents.json
deleted file mode 100644
index 82b6ec3e70..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconEffects.imageset/Contents.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "images" : [
- {
- "filename" : "PowerIconEffects.svg",
- "idiom" : "universal"
- }
- ],
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconEffects.imageset/PowerIconEffects.svg b/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconEffects.imageset/PowerIconEffects.svg
deleted file mode 100644
index 417e451cca..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconEffects.imageset/PowerIconEffects.svg
+++ /dev/null
@@ -1,15 +0,0 @@
-
diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconEmoji.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconEmoji.imageset/Contents.json
deleted file mode 100644
index fd88972363..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconEmoji.imageset/Contents.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "images" : [
- {
- "filename" : "PowerIconEmoji.svg",
- "idiom" : "universal"
- }
- ],
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconEmoji.imageset/PowerIconEmoji.svg b/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconEmoji.imageset/PowerIconEmoji.svg
deleted file mode 100644
index c8cc8922a1..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconEmoji.imageset/PowerIconEmoji.svg
+++ /dev/null
@@ -1,11 +0,0 @@
-
diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconGif.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconGif.imageset/Contents.json
deleted file mode 100644
index c3d709d9e8..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconGif.imageset/Contents.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "images" : [
- {
- "filename" : "PowerIconGif.svg",
- "idiom" : "universal"
- }
- ],
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconGif.imageset/PowerIconGif.svg b/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconGif.imageset/PowerIconGif.svg
deleted file mode 100644
index adaed14d56..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconGif.imageset/PowerIconGif.svg
+++ /dev/null
@@ -1,11 +0,0 @@
-
diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconMedia.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconMedia.imageset/Contents.json
deleted file mode 100644
index 6e83cc8223..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconMedia.imageset/Contents.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "images" : [
- {
- "filename" : "PowerIconMedia.svg",
- "idiom" : "universal"
- }
- ],
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconMedia.imageset/PowerIconMedia.svg b/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconMedia.imageset/PowerIconMedia.svg
deleted file mode 100644
index 4189aec595..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconMedia.imageset/PowerIconMedia.svg
+++ /dev/null
@@ -1,11 +0,0 @@
-
diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconStickers.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconStickers.imageset/Contents.json
deleted file mode 100644
index 8e14ecc776..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconStickers.imageset/Contents.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "images" : [
- {
- "filename" : "PowerIconStickers.svg",
- "idiom" : "universal"
- }
- ],
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconStickers.imageset/PowerIconStickers.svg b/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconStickers.imageset/PowerIconStickers.svg
deleted file mode 100644
index d825813fc7..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconStickers.imageset/PowerIconStickers.svg
+++ /dev/null
@@ -1,11 +0,0 @@
-
diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconVideo.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconVideo.imageset/Contents.json
deleted file mode 100644
index d47d2a6972..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconVideo.imageset/Contents.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "images" : [
- {
- "filename" : "PowerIconVideo.svg",
- "idiom" : "universal"
- }
- ],
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconVideo.imageset/PowerIconVideo.svg b/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconVideo.imageset/PowerIconVideo.svg
deleted file mode 100644
index 0a3bb1baba..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconVideo.imageset/PowerIconVideo.svg
+++ /dev/null
@@ -1,11 +0,0 @@
-
diff --git a/submodules/TelegramUI/Resources/Animations/anim_hand5.json b/submodules/TelegramUI/Resources/Animations/anim_hand5.json
deleted file mode 100644
index e90dc525a4..0000000000
--- a/submodules/TelegramUI/Resources/Animations/anim_hand5.json
+++ /dev/null
@@ -1 +0,0 @@
-{"v":"5.7.6","fr":60,"ip":0,"op":240,"w":100,"h":100,"nm":"hand_5 BIG 1x","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":3,"nm":"NULL CONTROL","parent":6,"sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":30,"s":[6.742,-39.422,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":36,"s":[5.537,-39.422,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":43,"s":[6.742,-39.422,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":52,"s":[5.537,-39.422,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":68,"s":[6.742,-39.422,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0},"t":87,"s":[5.537,-39.422,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.13,"y":1},"o":{"x":0.174,"y":0},"t":115,"s":[6.742,-39.422,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.34,"y":1},"o":{"x":0.54,"y":0},"t":157,"s":[6.742,-39.422,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.54,"y":0},"t":168,"s":[6.742,-39.422,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0},"t":172,"s":[1.942,-42.622,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0},"t":178,"s":[13.542,-39.422,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0},"t":184,"s":[9.142,-39.422,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.29,"y":1},"o":{"x":0.167,"y":0},"t":190,"s":[9.542,-39.422,0],"to":[0,0,0],"ti":[0,0,0]},{"t":199,"s":[6.742,-39.422,0]}],"ix":2,"l":2},"a":{"a":0,"k":[115,115,0],"ix":1,"l":2},"s":{"a":0,"k":[20,20,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":244,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"head","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.781],"y":[1]},"o":{"x":[0.412],"y":[0]},"t":1,"s":[0]},{"i":{"x":[0.3],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":11,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":30,"s":[21]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":54,"s":[21]},{"i":{"x":[0.13],"y":[1]},"o":{"x":[0.811],"y":[0]},"t":71,"s":[21]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.54],"y":[0]},"t":157,"s":[0.516]},{"i":{"x":[0.34],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":162,"s":[61.167]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.54],"y":[0]},"t":169,"s":[0.516]},{"i":{"x":[0.637],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":177,"s":[-21.833]},{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":183,"s":[0]},{"t":218,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.396,"y":1},"o":{"x":0.683,"y":0},"t":1,"s":[81.289,125.475,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":11,"s":[81.289,176.385,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":30,"s":[115.289,114.475,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.487,"y":0},"t":54,"s":[115.289,148.475,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.7,"y":0},"t":71,"s":[115.289,141.475,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0},"t":97,"s":[133.289,230.475,0],"to":[0,0,0],"ti":[0.667,1.167,0]},{"i":{"x":0.2,"y":0.2},"o":{"x":0.167,"y":0.167},"t":110,"s":[129.289,223.475,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.13,"y":1},"o":{"x":0.181,"y":0},"t":117,"s":[129.289,223.475,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.34,"y":1},"o":{"x":0.54,"y":0},"t":157,"s":[145.289,244.475,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.54,"y":0},"t":169,"s":[153.289,246.475,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0},"t":183,"s":[70.289,86.475,0],"to":[0,0,0],"ti":[0,0,0]},{"t":218,"s":[81.289,125.475,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":1,"s":[500,500,100]},{"i":{"x":[0.425,0.425,0.425],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":5,"s":[485,515,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":11,"s":[500,500,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":16,"s":[520,484,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":30,"s":[500,500,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":54,"s":[500,500,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.7,0.7,0.7],"y":[0,0,0]},"t":71,"s":[500,500,100]},{"i":{"x":[0.13,0.13,0.13],"y":[1,1,1]},"o":{"x":[0.176,0.176,0.176],"y":[0,0,0]},"t":97,"s":[500,500,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.54,0.54,0.54],"y":[0,0,0]},"t":157,"s":[500,500,100]},{"i":{"x":[0.34,0.34,0.34],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":162,"s":[477,515,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.54,0.54,0.54],"y":[0,0,0]},"t":169,"s":[500,500,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":175,"s":[469,533,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":183,"s":[485,515,100]},{"i":{"x":[0.2,0.2,0.2],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":198,"s":[527,484,100]},{"t":218,"s":[500,500,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":0,"s":[{"i":[[-6.341,0],[0,-6.341],[6.341,0],[0,6.341]],"o":[[6.341,0],[0,6.341],[-6.341,0],[0,-6.341]],"v":[[0,-11.482],[11.482,0],[0,11.482],[-11.482,0]],"c":true}]},{"i":{"x":0.13,"y":1},"o":{"x":0.167,"y":0},"t":117,"s":[{"i":[[-6.395,0],[0,-6.395],[6.395,0],[0,6.395]],"o":[[6.395,0],[0,6.395],[-6.395,0],[0,-6.395]],"v":[[0,-11.579],[11.579,0],[0,11.579],[-11.579,0]],"c":true}]},{"i":{"x":0.34,"y":1},"o":{"x":0.54,"y":0},"t":157,"s":[{"i":[[-6.395,0],[0,-6.395],[6.395,0],[0,6.395]],"o":[[6.395,0],[0,6.395],[-6.395,0],[0,-6.395]],"v":[[0,-11.579],[11.579,0],[0,11.579],[-11.579,0]],"c":true}]},{"i":{"x":0.2,"y":1},"o":{"x":0.54,"y":0},"t":169,"s":[{"i":[[-6.395,0],[0,-6.395],[6.395,0],[0,6.395]],"o":[[6.395,0],[0,6.395],[-6.395,0],[0,-6.395]],"v":[[0,-11.579],[11.579,0],[0,11.579],[-11.579,0]],"c":true}]},{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0},"t":183,"s":[{"i":[[-6.395,0],[0,-6.395],[6.395,0],[0,6.395]],"o":[[6.395,0],[0,6.395],[-6.395,0],[0,-6.395]],"v":[[0,-11.579],[11.579,0],[0,11.579],[-11.579,0]],"c":true}]},{"t":218,"s":[{"i":[[-6.395,0],[0,-6.395],[6.395,0],[0,6.395]],"o":[[6.395,0],[0,6.395],[-6.395,0],[0,-6.395]],"v":[[0,-11.579],[11.579,0],[0,11.579],[-11.579,0]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Oval","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":5.5,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":0,"op":244,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":3,"nm":"hands","parent":6,"sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.3],"y":[1]},"o":{"x":[0.5],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":28,"s":[8]},{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.487],"y":[0]},"t":49,"s":[8]},{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.5],"y":[0]},"t":66,"s":[8]},{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.5],"y":[0]},"t":95,"s":[15]},{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.6],"y":[0]},"t":172,"s":[15]},{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":183,"s":[0]},{"t":215,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.538,"y":1},"o":{"x":0.525,"y":0},"t":0,"s":[-2.341,-17.777,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.5,"y":0},"t":10,"s":[-2.341,-12.256,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":0.3},"o":{"x":0.167,"y":0.167},"t":28,"s":[-1.541,-17.777,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.487,"y":0},"t":49,"s":[-1.541,-17.777,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.5,"y":0},"t":66,"s":[-1.541,-17.777,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":0.2},"o":{"x":0.5,"y":0.5},"t":95,"s":[1.459,-10.777,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.6,"y":0},"t":172,"s":[1.459,-10.777,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0},"t":183,"s":[-2.341,-24.177,0],"to":[0,0,0],"ti":[0,0,0]},{"t":215,"s":[-2.341,-17.777,0]}],"ix":2,"l":2},"a":{"a":0,"k":[115,115,0],"ix":1,"l":2},"s":{"a":0,"k":[20,20,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":244,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"hands 3","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[115,115,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[500,500,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":0,"s":[{"i":[[0,0],[-4.026,-6.73],[-10.797,-1.273],[-2.119,-0.01]],"o":[[0.65,4.552],[3.882,6.488],[3.499,0.413],[1.186,0.005]],"v":[[-34.027,-30.664],[-27.902,-11.553],[-6.28,2.761],[4.392,3.022]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.6,"y":0},"t":10,"s":[{"i":[[0,0],[-1.37,-5.996],[-12.799,0.954],[-5.623,-0.366]],"o":[[-2.582,5.43],[1.226,5.005],[4.037,-0.259],[1.184,0.071]],"v":[[-21.167,-23.136],[-27.242,-4.081],[-7.372,3.34],[9.979,3.037]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.5,"y":0},"t":24,"s":[{"i":[[0,0],[-0.633,-3.903],[-5.394,-1.449],[-8.926,0.862]],"o":[[1.012,6.936],[0.998,6.158],[5.589,1.532],[1.18,-0.114]],"v":[[-18.065,-33.976],[-19.359,-12.251],[-7.484,2.834],[8.267,2.627]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0},"t":30,"s":[{"i":[[0,0],[-1.834,-3.522],[-4.71,-2.732],[-8.926,0.862]],"o":[[4.406,7.624],[2.894,5.558],[5.028,2.917],[1.18,-0.114]],"v":[[-29.396,-35.058],[-19.39,-14.698],[-8.387,2.178],[8.267,2.627]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":37,"s":[{"i":[[0,0],[-0.633,-3.903],[-5.394,-1.449],[-8.926,0.862]],"o":[[1.012,6.936],[0.998,6.158],[5.589,1.532],[1.18,-0.114]],"v":[[-18.065,-33.976],[-19.359,-12.251],[-7.484,2.834],[8.267,2.627]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":46,"s":[{"i":[[0,0],[-0.961,-3.853],[-4.984,-1.753],[-8.926,0.862]],"o":[[1.597,6.857],[1.517,6.08],[5.484,1.929],[1.18,-0.114]],"v":[[-23.166,-35.108],[-20.844,-12.632],[-7.906,2.713],[8.267,2.627]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":62,"s":[{"i":[[0,0],[-0.633,-3.903],[-5.394,-1.449],[-8.926,0.862]],"o":[[1.012,6.936],[0.998,6.158],[5.589,1.532],[1.18,-0.114]],"v":[[-18.065,-33.976],[-19.359,-12.251],[-7.484,2.834],[8.371,3.018]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":83,"s":[{"i":[[0,0],[-0.961,-3.853],[-4.906,-1.962],[-7.467,0.538]],"o":[[1.597,6.857],[1.517,6.08],[4.479,1.791],[1.183,-0.085]],"v":[[-23.166,-35.108],[-20.844,-12.632],[-7.906,2.713],[8.484,3.913]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":110,"s":[{"i":[[0,0],[-0.26,-3.59],[-3.384,-0.79],[-7.192,0.386]],"o":[[0.455,6.368],[0.41,5.665],[5.588,1.254],[1.184,-0.062]],"v":[[-19.285,-31.287],[-20.744,-10.988],[-7.332,2.936],[7.831,4.696]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0},"t":159,"s":[{"i":[[0,0],[-0.365,-3.581],[-5.898,-2.25],[-7.859,-0.922]],"o":[[0.643,6.352],[0.576,5.651],[5.898,2.25],[1.177,0.138]],"v":[[-19.77,-28.6],[-20.63,-8.268],[-7.332,2.936],[8.229,6.66]],"c":false}]},{"i":{"x":0.33,"y":1},"o":{"x":0.167,"y":0},"t":170,"s":[{"i":[[0,0],[-0.365,-3.581],[-5.898,-2.25],[-7.859,-0.922]],"o":[[0.643,6.352],[0.576,5.651],[5.898,2.25],[1.177,0.138]],"v":[[-17.58,-28.152],[-18.943,-8.927],[-7.087,3.077],[8.229,6.66]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.6,"y":0},"t":172,"s":[{"i":[[0,0],[-0.365,-3.581],[-5.898,-2.25],[-7.859,-0.922]],"o":[[0.643,6.352],[0.576,5.651],[5.898,2.25],[1.177,0.138]],"v":[[-17.58,-28.152],[-18.943,-8.927],[-7.087,3.077],[8.229,6.66]],"c":false}]},{"i":{"x":0.33,"y":1},"o":{"x":0.167,"y":0.167},"t":177,"s":[{"i":[[0,0],[-0.54,-4.745],[-7.23,-2.167],[-5.398,-0.759]],"o":[[-4.605,4.425],[0.657,5.889],[5.467,1.861],[1.174,0.151]],"v":[[-13.923,-29.232],[-18.91,-12.686],[-7.227,2.305],[7.477,5.457]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.6,"y":0},"t":183,"s":[{"i":[[0,0],[-0.757,-6.182],[-8.875,-2.065],[-4.049,-0.033]],"o":[[1.716,5.76],[0.757,6.182],[5.804,0.802],[1.183,0.01]],"v":[[-20.544,-37.522],[-16.784,-18.145],[-6.731,2.836],[6.768,3.871]],"c":false}]},{"i":{"x":0.22,"y":1},"o":{"x":0.167,"y":0.167},"t":197,"s":[{"i":[[0,0],[-1.231,-6.629],[-10.082,-1.99],[-3.033,-0.134]],"o":[[-2.133,4.972],[1.242,6.689],[4.542,1.029],[1.183,0.052]],"v":[[-21.494,-33.181],[-22.796,-14.58],[-6.487,2.795],[6.539,3.624]],"c":false}]},{"t":230,"s":[{"i":[[0,0],[-4.026,-6.73],[-10.797,-1.273],[-2.119,-0.01]],"o":[[0.65,4.552],[3.882,6.488],[3.499,0.413],[1.186,0.005]],"v":[[-34.027,-30.664],[-27.902,-11.553],[-6.28,2.761],[4.392,3.022]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":5.5,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path-8","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":244,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"hands 2","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[115,115,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[500,500,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":0,"s":[{"i":[[-3.103,0.058],[-2.523,0.098],[-2.257,-5.922],[-0.05,-3.076]],"o":[[2.726,-0.051],[10.848,-0.42],[1.636,4.291],[0,0]],"v":[[3.047,3.014],[11.974,3.008],[30.43,17.71],[32.973,31.114]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":10,"s":[{"i":[[-2.886,-0.073],[-2.347,-0.053],[-2.059,-3.86],[-0.222,-4.947]],"o":[[2.847,0.072],[11.634,0.272],[2.05,3.678],[0,0]],"v":[[2.805,2.87],[11.318,3.002],[28.486,9.829],[31.187,24.214]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":29,"s":[{"i":[[-2.805,0.395],[-2.144,-0.845],[-1.574,-3.736],[-3.615,-5.451]],"o":[[4.23,-0.595],[5.323,2.097],[1.94,4.605],[0,0]],"v":[[2.696,3.339],[12.911,2.962],[23.56,19.096],[31.538,32.046]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.4,"y":0},"t":49,"s":[{"i":[[-2.805,0.396],[-2.168,-0.779],[-1.2,-3.73],[-2.259,-5.223]],"o":[[3.592,-0.507],[6.085,2.162],[1.511,4.741],[0,0]],"v":[[2.591,3.352],[12.843,2.769],[22.316,18.867],[27.587,32.67]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0.167},"t":56,"s":[{"i":[[-2.809,0.363],[-2.164,-0.79],[-1.265,-3.731],[-2.492,-5.262]],"o":[[3.71,-0.472],[5.954,2.15],[1.585,4.718],[0,0]],"v":[[2.587,3.473],[12.901,3.159],[22.53,18.907],[28.267,32.563]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.6,"y":0},"t":71,"s":[{"i":[[-2.826,0.202],[-2.144,-0.845],[-1.574,-3.736],[-3.615,-5.451]],"o":[[4.274,-0.305],[5.323,2.097],[1.94,4.605],[0,0]],"v":[[2.755,3.732],[13.24,3.821],[23.56,19.096],[31.538,32.046]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":77,"s":[{"i":[[-3.178,0.215],[-2.041,-0.845],[-2.499,-1.941],[-4.429,-3.642]],"o":[[4.522,-0.369],[5.066,2.097],[2.711,2.446],[0,0]],"v":[[2.574,4.136],[12.86,4.148],[22.716,18.561],[31.303,23.734]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":83,"s":[{"i":[[-3.695,-0.034],[-1.897,-0.845],[-3.783,0.554],[-5.561,-1.128]],"o":[[4.929,-0.055],[4.71,2.097],[3.783,-0.554],[0,0]],"v":[[2.262,4.258],[12.224,3.929],[21.543,17.816],[30.975,12.182]],"c":false}]},{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0.167},"t":89,"s":[{"i":[[-3.987,-0.175],[-1.816,-0.845],[-2.558,0.538],[-4.267,2.804]],"o":[[4.392,0.151],[4.508,2.097],[4.029,-0.892],[0,0]],"v":[[0.773,4.323],[12.105,4.679],[20.881,17.396],[23.392,2.697]],"c":false}]},{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0},"t":97,"s":[{"i":[[-4.068,-0.214],[-1.793,-0.845],[-2.221,0.534],[-3.91,3.887]],"o":[[3.583,0.188],[4.452,2.097],[4.097,-0.985],[0,0]],"v":[[1.673,4.518],[12.072,4.886],[20.698,17.28],[21.304,0.085]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":110,"s":[{"i":[[-4.068,-0.214],[-1.793,-0.845],[-2.221,0.534],[-1.866,5.72]],"o":[[3.583,0.188],[4.452,2.097],[4.097,-0.985],[0,0]],"v":[[1.647,4.421],[11.286,4.268],[20.698,17.28],[20.124,-0.841]],"c":false}]},{"i":{"x":0.13,"y":1},"o":{"x":0.167,"y":0},"t":117,"s":[{"i":[[-4.068,-0.214],[-1.793,-0.845],[-2.221,0.534],[-1.866,5.72]],"o":[[3.583,0.188],[4.452,2.097],[4.097,-0.985],[0,0]],"v":[[1.647,4.421],[11.286,4.268],[20.698,17.28],[20.124,-0.841]],"c":false}]},{"i":{"x":0.47,"y":1},"o":{"x":0.54,"y":0},"t":159,"s":[{"i":[[-3.882,-1.235],[-1.793,-0.845],[-2.236,0.469],[-6.732,3.4]],"o":[[6.962,2.214],[4.452,2.097],[6.913,-1.449],[0,0]],"v":[[-2.326,4.761],[11.274,7.998],[20.698,17.28],[22.603,3.774]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.53,"y":0},"t":170,"s":[{"i":[[-3.882,-1.235],[-1.793,-0.845],[-3.475,0.534],[-2.888,6.054]],"o":[[6.962,2.214],[4.452,2.097],[6.411,-0.985],[0,0]],"v":[[-2.326,4.761],[12.099,7.984],[24.802,16.49],[25.028,3.271]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":174,"s":[{"i":[[-3.711,-1.045],[-1.877,-0.707],[-3.424,-0.312],[-3.136,3.726]],"o":[[6.283,1.865],[4.802,1.755],[5.881,-0.065],[0,0]],"v":[[-1.494,4.593],[11.879,7.297],[24.69,16.195],[35.961,7.352]],"c":false}]},{"i":{"x":0.33,"y":1},"o":{"x":0.167,"y":0.167},"t":177,"s":[{"i":[[-3.396,-0.694],[-2.033,-0.452],[-3.328,-1.877],[-3.596,-0.579]],"o":[[5.027,1.218],[5.448,1.122],[4.899,1.636],[0,0]],"v":[[0.045,4.282],[11.473,6.026],[24.482,15.649],[36.718,19.295]],"c":false}]},{"i":{"x":0.2,"y":1},"o":{"x":0.6,"y":0},"t":186,"s":[{"i":[[-2.838,-0.073],[-2.309,0],[-3.159,-4.652],[-4.411,-8.209]],"o":[[2.799,0.072],[6.593,0],[3.159,4.652],[0,0]],"v":[[2.773,3.73],[10.753,3.773],[24.114,14.681],[34.584,32.542]],"c":false}]},{"t":222,"s":[{"i":[[-3.103,0.058],[-2.523,0.098],[-2.257,-5.922],[-0.05,-3.076]],"o":[[2.726,-0.051],[10.848,-0.42],[1.636,4.291],[0,0]],"v":[[3.047,3.014],[11.974,3.008],[30.43,17.71],[32.973,31.114]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":5.5,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path-8","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":244,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"body","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.538,"y":1},"o":{"x":0.525,"y":0},"t":0,"s":[262.842,433.748,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.5,"y":0},"t":10,"s":[262.842,450.324,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":28,"s":[262.842,403.748,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":0.3},"o":{"x":0.167,"y":0.167},"t":49,"s":[262.842,433.748,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":0.2},"o":{"x":0.167,"y":0.167},"t":66,"s":[262.842,433.748,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":0.2},"o":{"x":0.6,"y":0.6},"t":172,"s":[262.842,433.748,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":0.2},"o":{"x":0.167,"y":0.167},"t":183,"s":[262.842,433.748,0],"to":[0,0,0],"ti":[0,0,0]},{"t":215,"s":[262.842,433.748,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,14.211,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.5,0.5,0.5],"y":[0,0,0]},"t":0,"s":[500,500,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":3,"s":[515,485,100]},{"i":{"x":[0.566,0.566,0.566],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":5,"s":[485,515,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.484,0.484,0.484],"y":[0,0,0]},"t":10,"s":[489.464,510.536,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":15,"s":[515,485,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":28,"s":[500,500,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":37,"s":[485,515,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":49,"s":[500,500,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":57,"s":[515,485,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":66,"s":[500,500,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":78,"s":[485,515,100]},{"i":{"x":[0.2,0.2,0.2],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":95,"s":[500,500,100]},{"i":{"x":[0.2,0.2,0.2],"y":[1,1,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":172,"s":[500,500,100]},{"i":{"x":[0.2,0.2,0.2],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":183,"s":[500,500,100]},{"t":215,"s":[500,500,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":0,"s":[{"i":[[0,0],[-0.741,-5.903],[-1.922,-0.751],[-2.946,0],[-2.468,1.063],[-0.228,3.004],[0,0]],"o":[[0,0],[0.328,2.608],[2.556,0.999],[2.898,0],[1.959,-0.844],[0.638,-8.388],[0,0]],"v":[[-12.432,-14.575],[-11.977,7.814],[-8.487,12.849],[0.4,13.948],[9.223,12.916],[12.656,7.499],[12.482,-13.225]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.5,"y":0},"t":10,"s":[{"i":[[0,0],[0,0],[-1.991,-0.83],[-3.105,0],[-2.6,1.063],[-0.091,2.131],[0,0]],"o":[[0,0],[0.148,2.059],[2.638,1.099],[3.053,0],[2.064,-0.844],[0,0],[0,0]],"v":[[-14.565,-8.333],[-13.375,7.863],[-9.896,12.562],[0.353,14.211],[10.467,12.616],[13.992,7.749],[14.426,-7.239]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":24,"s":[{"i":[[0.942,-11.03],[0,0],[-1.748,-0.83],[-2.726,0],[-2.283,1.063],[-0.08,2.131],[-1.162,6.43]],"o":[[-0.818,9.587],[0.13,2.059],[2.317,1.099],[2.681,0],[1.812,-0.844],[0,0],[0.041,-0.17]],"v":[[-11.292,-16.725],[-11.425,7.863],[-8.369,12.562],[0.392,14.211],[9.036,12.616],[12.131,7.749],[13.652,-10.268]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0.942,-11.03],[0,0],[-1.748,-0.83],[-2.726,0],[-2.283,1.063],[-0.08,2.131],[-1.162,6.43]],"o":[[-0.818,9.587],[0.13,2.059],[2.317,1.099],[2.681,0],[1.812,-0.844],[0,0],[0.041,-0.17]],"v":[[-11.74,-17.039],[-11.432,7.863],[-8.377,12.562],[0.392,14.211],[9.044,12.616],[12.139,7.749],[13.308,-10.843]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":37,"s":[{"i":[[0.942,-11.03],[0,0],[-1.748,-0.83],[-2.726,0],[-2.283,1.063],[-0.08,2.131],[-1.162,6.43]],"o":[[-0.818,9.587],[0.13,2.059],[2.317,1.099],[2.681,0],[1.812,-0.844],[0,0],[0.041,-0.17]],"v":[[-11.292,-16.725],[-11.425,7.863],[-8.369,12.562],[0.392,14.211],[9.036,12.616],[12.131,7.749],[13.652,-10.268]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0.167},"t":46,"s":[{"i":[[0.942,-11.03],[0,0],[-1.748,-0.83],[-2.726,0],[-2.283,1.063],[-0.08,2.131],[-1.162,6.43]],"o":[[-0.818,9.587],[0.13,2.059],[2.317,1.099],[2.681,0],[1.812,-0.844],[0,0],[0.041,-0.17]],"v":[[-11.74,-17.039],[-11.432,7.863],[-8.377,12.562],[0.392,14.211],[9.044,12.616],[12.139,7.749],[13.308,-10.843]],"c":false}]},{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0},"t":57,"s":[{"i":[[0.942,-11.03],[0,0],[-1.748,-0.83],[-2.726,0],[-2.283,1.063],[-0.08,2.131],[-1.162,6.43]],"o":[[-0.818,9.587],[0.13,2.059],[2.317,1.099],[2.681,0],[1.812,-0.844],[0,0],[0.041,-0.17]],"v":[[-11.292,-16.725],[-11.425,7.863],[-8.369,12.562],[0.392,14.211],[9.036,12.616],[12.131,7.749],[13.652,-10.268]],"c":false}]},{"i":{"x":0.2,"y":1},"o":{"x":0.5,"y":0},"t":66,"s":[{"i":[[0.942,-11.03],[0,0],[-1.748,-0.83],[-2.726,0],[-2.283,1.063],[-0.08,2.131],[-1.162,6.43]],"o":[[-0.818,9.587],[0.13,2.059],[2.317,1.099],[2.681,0],[1.812,-0.844],[0,0],[0.041,-0.17]],"v":[[-11.74,-17.039],[-11.432,7.863],[-8.377,12.562],[0.392,14.211],[9.044,12.616],[12.139,7.749],[13.308,-10.843]],"c":false}]},{"i":{"x":0.2,"y":1},"o":{"x":0.5,"y":0},"t":95,"s":[{"i":[[7.932,-7.722],[-0.296,-1.817],[-2.373,-1.147],[-2.726,0.008],[-2.179,1.263],[-0.336,1.409],[-1.022,3.53]],"o":[[-7.574,7.373],[0.296,1.817],[2.309,1.116],[2.681,-0.008],[2.218,-1.285],[0.336,-1.409],[0.041,-0.17]],"v":[[-11.795,-13.512],[-17.064,5.317],[-13.796,10.808],[-2.427,12.362],[5.214,10.746],[8.296,5.87],[10.454,-3.469]],"c":false}]},{"i":{"x":0.2,"y":1},"o":{"x":0.6,"y":0},"t":172,"s":[{"i":[[7.932,-7.722],[-0.296,-1.817],[-2.373,-1.147],[-2.726,0.008],[-2.179,1.263],[-0.336,1.409],[-1.022,3.53]],"o":[[-7.574,7.373],[0.296,1.817],[2.309,1.116],[2.681,-0.008],[2.218,-1.285],[0.336,-1.409],[0.041,-0.17]],"v":[[-11.795,-13.512],[-17.064,5.317],[-13.796,10.808],[-2.427,12.362],[5.214,10.746],[8.296,5.87],[10.454,-3.469]],"c":false}]},{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0},"t":183,"s":[{"i":[[0,0],[0,0],[-1.638,-0.83],[-2.555,0],[-2.139,1.063],[-0.075,2.131],[0,0]],"o":[[0,0],[0.122,2.059],[2.171,1.099],[2.512,0],[1.698,-0.844],[0,0],[0,0]],"v":[[-11.432,-21.961],[-10.647,7.863],[-7.784,12.562],[0.304,14.211],[8.281,12.616],[11.182,7.749],[11.432,-19.411]],"c":false}]},{"t":215,"s":[{"i":[[0,0],[-0.741,-5.903],[-1.922,-0.751],[-2.946,0],[-2.468,1.063],[-0.228,3.004],[0,0]],"o":[[0,0],[0.328,2.608],[2.556,0.999],[2.898,0],[1.959,-0.844],[0.638,-8.388],[0,0]],"v":[[-12.432,-14.575],[-11.977,7.814],[-8.487,12.849],[0.4,13.948],[9.223,12.916],[12.656,7.499],[12.482,-13.225]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":5.5,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":0,"op":244,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"hand_5","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50.75,47.75,0],"ix":2,"l":2},"a":{"a":0,"k":[256,256,0],"ix":1,"l":2},"s":{"a":0,"k":[11,11,100],"ix":6,"l":2}},"ao":0,"w":512,"h":512,"ip":0,"op":240,"st":0,"bm":0}],"markers":[]}
\ No newline at end of file
diff --git a/submodules/TelegramUI/Resources/Animations/anim_hand6.json b/submodules/TelegramUI/Resources/Animations/anim_hand6.json
deleted file mode 100644
index 1864a8703b..0000000000
--- a/submodules/TelegramUI/Resources/Animations/anim_hand6.json
+++ /dev/null
@@ -1 +0,0 @@
-{"v":"5.7.6","fr":60,"ip":0,"op":240,"w":100,"h":100,"nm":"hand_6 BIG 1x","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":3,"nm":"NULL CONTROL","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[256.989,275.127,0],"ix":2,"l":2},"a":{"a":0,"k":[115,115,0],"ix":1,"l":2},"s":{"a":0,"k":[500,500,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":243,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"head","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.6],"y":[0]},"t":1,"s":[0]},{"i":{"x":[0.3],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":11,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":23.514,"s":[21]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.17],"y":[0]},"t":159,"s":[21]},{"i":{"x":[0.836],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":169,"s":[-18]},{"i":{"x":[0.14],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":181,"s":[0]},{"t":202,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":1,"s":[122.706,17.15,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":11,"s":[122.706,68.999,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.589,"y":1},"o":{"x":0.275,"y":0},"t":23.514,"s":[150.706,5.999,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.206,"y":0},"t":36.148,"s":[150.706,32.05,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.29,"y":1},"o":{"x":0.295,"y":0},"t":55.52,"s":[150.706,27.67,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.71,"y":0.684},"o":{"x":0.75,"y":0},"t":72,"s":[150.706,31.062,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.205,"y":0.943},"o":{"x":0.097,"y":0.337},"t":100,"s":[158.75,127.141,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.205,"y":0.901},"o":{"x":0.328,"y":0.041},"t":123,"s":[140.564,110.895,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.205,"y":0.205},"o":{"x":0.328,"y":0.328},"t":147,"s":[148.731,122.942,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.71,"y":0},"t":159,"s":[148.731,122.942,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.14,"y":1},"o":{"x":0.167,"y":0},"t":181,"s":[122.706,1.199,0],"to":[0,0,0],"ti":[0,0,0]},{"t":202,"s":[122.706,17.15,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":1,"s":[500,500,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":5,"s":[485,515,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":11,"s":[525,475,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":16,"s":[485,515,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":23.514,"s":[500,500,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":36,"s":[500,500,100]},{"i":{"x":[0.605,0.605,0.605],"y":[1,1,1]},"o":{"x":[0.709,0.709,0.709],"y":[0,0,0]},"t":55.52,"s":[500,500,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":72,"s":[500,500,100]},{"i":{"x":[0.605,0.605,0.605],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":84,"s":[485,515,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.195,0.195,0.195],"y":[0,0,0]},"t":100,"s":[531,465,100]},{"i":{"x":[0.666,0.666,0.666],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":111,"s":[508,482,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":123,"s":[531,465,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":147,"s":[531,465,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.71,0.71,0.71],"y":[0,0,0]},"t":159,"s":[531,465,100]},{"i":{"x":[0.2,0.2,0.2],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":169,"s":[485,515,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":181,"s":[500,500,100]},{"i":{"x":[0.14,0.14,0.14],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":190,"s":[515,485,100]},{"t":202,"s":[500,500,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-6.341,0],[0,-6.341],[6.341,0],[0,6.341]],"o":[[6.341,0],[0,6.341],[-6.341,0],[0,-6.341]],"v":[[0,-11.482],[11.482,0],[0,11.482],[-11.482,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Oval","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":5.5,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":0,"op":244,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":3,"nm":"NULL CONTROL","parent":7,"sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-1.541,-17.777,0],"ix":2,"l":2},"a":{"a":0,"k":[115,115,0],"ix":1,"l":2},"s":{"a":0,"k":[20,20,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":244,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":3,"nm":"hands","parent":3,"sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.5],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":23.514,"s":[8]},{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.71],"y":[0]},"t":167,"s":[8]},{"t":187,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":0,"s":[111,115,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":10,"s":[111,155,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0},"t":23.514,"s":[115,115,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0},"t":32.309,"s":[115,121,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0},"t":48.244,"s":[115,115,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":0.5},"o":{"x":0.167,"y":0.167},"t":73.717,"s":[115,121,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":0.5},"o":{"x":0.167,"y":0.167},"t":104,"s":[115,121,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.71,"y":0},"t":167,"s":[115,121,0],"to":[0,0,0],"ti":[0,0,0]},{"t":187,"s":[111,115,0]}],"ix":2,"l":2},"a":{"a":0,"k":[115,115,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":244,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"hands 3","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[115,115,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[500,500,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":0,"s":[{"i":[[0,0],[-4.026,-6.73],[-10.797,-1.273],[-2.119,-0.01]],"o":[[0.65,4.552],[3.882,6.488],[3.499,0.413],[1.186,0.005]],"v":[[-34.027,-30.664],[-27.902,-11.553],[-6.28,2.761],[4.392,3.022]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.6,"y":0},"t":10,"s":[{"i":[[0,0],[-0.71,-5.814],[-13.22,1.67],[-6.666,-0.138]],"o":[[-3.232,5.731],[0.566,4.637],[3.994,-0.505],[1.186,0.024]],"v":[[-18.219,-21.464],[-27.079,-2.224],[-7.644,3.484],[11.485,2.829]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":23.514,"s":[{"i":[[0,0],[-0.961,-3.853],[-4.491,-2.782],[-8.28,0.661]],"o":[[1.597,6.857],[1.517,6.08],[4.565,2.828],[1.182,-0.094]],"v":[[-20.247,-37.336],[-17.926,-14.859],[-7.359,2.636],[10.724,2.534]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0.167},"t":27.281,"s":[{"i":[[0,0],[-0.511,-3.901],[-4.724,-3.504],[-8.291,0.526]],"o":[[2.238,7.884],[0.807,6.156],[4.331,3.14],[1.184,-0.075]],"v":[[-18.139,-37.479],[-16.265,-15.877],[-7.359,2.636],[10.941,2.657]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":32.309,"s":[{"i":[[0,0],[0.125,-3.969],[-5.052,-4.524],[-8.435,1.023]],"o":[[-2.798,7.352],[-0.197,6.263],[4,3.582],[1.177,-0.143]],"v":[[-6.969,-38.346],[-13.919,-17.314],[-7.359,2.636],[9.48,3.009]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0.167},"t":39.988,"s":[{"i":[[0,0],[-0.447,-3.908],[-4.757,-3.608],[-8.213,0.625]],"o":[[-4.119,5.691],[0.705,6.167],[4.297,3.185],[1.182,-0.09]],"v":[[-10.167,-36.731],[-16.027,-16.023],[-7.359,2.636],[11.49,2.929]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":48,"s":[{"i":[[0,0],[-0.961,-3.853],[-4.491,-2.782],[-8.28,0.661]],"o":[[1.597,6.857],[1.517,6.08],[4.565,2.828],[1.182,-0.094]],"v":[[-20.247,-37.336],[-17.926,-14.859],[-7.359,2.636],[10.724,2.534]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":59,"s":[{"i":[[0,0],[-0.745,-3.894],[-5.61,-1.137],[-7.557,0.15]],"o":[[0.04,7.276],[1.176,6.145],[4.685,1.226],[2.785,0.43]],"v":[[-21.221,-29.776],[-21.735,-7.592],[-7.832,2.454],[9.503,3.125]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.7,"y":0},"t":77,"s":[{"i":[[0,0],[-0.534,-3.934],[-6.701,0.467],[-7.857,-1.7]],"o":[[-1.477,7.685],[0.843,6.208],[4.802,-0.335],[4.346,0.94]],"v":[[-22.17,-22.408],[-25.448,-0.509],[-8.293,2.277],[8.34,3.892]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":83,"s":[{"i":[[0,0],[-1.283,-3.822],[-6.716,0.378],[-7.766,-1.789]],"o":[[-1.159,7.502],[1.533,5.663],[4.777,-0.271],[4.347,1.005]],"v":[[-23.938,-21.192],[-25.341,0.043],[-8.232,2.315],[8.497,4.584]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":89,"s":[{"i":[[0,0],[-4.461,-3.345],[-6.782,-0.001],[-7.382,-2.168]],"o":[[0.19,6.726],[4.461,3.345],[4.673,0.001],[4.35,1.277]],"v":[[-31.448,-16.026],[-24.888,2.391],[-7.974,2.476],[8.356,5.481]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":95,"s":[{"i":[[0,0],[-5.006,-0.871],[-6.739,0.618],[-3.595,-1.191]],"o":[[3.138,5.078],[7.808,0.677],[3.051,-0.206],[4.086,1.935]],"v":[[-35.589,-2.901],[-23.582,6.559],[-7.395,2.829],[5.349,5.214]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":96,"s":[{"i":[[0,0],[-5.097,-0.459],[-6.732,0.721],[-1.487,-0.398]],"o":[[3.63,4.803],[8.366,0.233],[2.78,-0.24],[4.048,2.025]],"v":[[-36.279,-0.714],[-23.364,7.254],[-7.298,2.887],[-0.817,3.327]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":98,"s":[{"i":[[0,0],[-5.278,0.366],[-6.718,0.928],[-1.571,-0.06]],"o":[[4.612,4.253],[9.482,-0.657],[2.24,-0.309],[3.971,2.206]],"v":[[-37.66,3.661],[-22.928,8.643],[-7.105,3.005],[-2.464,2.943]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":102,"s":[{"i":[[0,0],[-3.051,3.557],[-6.774,-0.323],[-0.439,-0.145]],"o":[[6.234,-0.011],[3.051,-3.557],[1.059,0.05],[4.488,-0.165]],"v":[[-32.128,21.461],[-18.515,15.867],[-6.941,3.526],[-5.387,3.41]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":104,"s":[{"i":[[0,0],[-1.97,4.904],[-6.781,-0.025],[-0.04,0.149]],"o":[[5.985,-2.499],[1.97,-4.904],[0.507,0.073],[4.271,1.301]],"v":[[-26.349,30.241],[-16.309,19.479],[-6.859,3.786],[-6.722,3.667]],"c":false}]},{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0.167},"t":106,"s":[{"i":[[0,0],[-0.723,4.978],[-6.358,0.441],[-0.086,-0.015]],"o":[[3.449,-4.195],[0.723,-4.978],[0.651,-0.017],[4.461,-0.533]],"v":[[-21.092,35.493],[-14.798,22.142],[-6.816,3.923],[-6.291,3.85]],"c":false}]},{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0},"t":116,"s":[{"i":[[0,0],[0.27,3.962],[-5.659,-0.43],[0,0]],"o":[[-2.38,-4.193],[-0.461,-6.764],[0.813,0.105],[0,0]],"v":[[-6.789,40.46],[-11.955,22.079],[-6.74,4.165],[-6.541,4.123]],"c":false}]},{"i":{"x":0.56,"y":1},"o":{"x":0.167,"y":0},"t":132,"s":[{"i":[[0,0],[0.028,3.971],[-6.776,0.271],[0,0]],"o":[[0.92,-6.575],[-0.044,-6.266],[0.437,0.042],[0,0]],"v":[[-14.554,41.248],[-13.978,22.06],[-6.74,4.165],[-6.04,4.181]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.44,"y":0},"t":149,"s":[{"i":[[0,0],[0.028,3.971],[-6.696,-1.076],[0,0]],"o":[[-1.06,-6.297],[-0.044,-6.266],[0.486,0.034],[0,0]],"v":[[-11.825,41.269],[-13.468,22.09],[-6.74,4.165],[-4.523,4.686]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0.167},"t":163,"s":[{"i":[[0,0],[0.028,3.971],[-6.696,-1.076],[0,0]],"o":[[-1.06,-6.297],[-0.044,-6.266],[0.486,0.034],[0,0]],"v":[[-11.825,41.269],[-13.468,22.09],[-6.74,4.165],[-4.523,4.686]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":167,"s":[{"i":[[0,0],[0.028,3.971],[-6.774,0.312],[0.114,0.062]],"o":[[-1.06,-6.297],[-0.044,-6.266],[0.175,-0.024],[0.128,0.161]],"v":[[-11.825,41.269],[-13.468,22.09],[-6.74,4.165],[-3.2,4.045]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":168,"s":[{"i":[[0,0],[-0.332,3.858],[-6.615,-0.065],[-6.227,-2.487]],"o":[[-1.197,-5.882],[0.411,-5.901],[4.645,0.044],[4.032,1.605]],"v":[[-13.447,39.061],[-15.196,20.338],[-6.713,4.082],[8.845,6.116]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":171,"s":[{"i":[[0,0],[-2.006,3.333],[-5.838,-0.364],[-5.143,-1.039]],"o":[[-1.832,-3.948],[2.53,-4.202],[4.517,0.245],[3.33,0.673]],"v":[[-20.998,28.78],[-23.239,12.181],[-6.64,3.301],[7.921,5.13]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":172,"s":[{"i":[[0,0],[-3.833,2.656],[-5.544,-0.477],[-4.639,-1.02]],"o":[[-0.634,-3.479],[4.095,-3.091],[4.469,0.321],[2.949,0.736]],"v":[[-27.21,21.956],[-23.39,8.31],[-6.628,2.86],[7.594,4.819]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":173,"s":[{"i":[[0,0],[-5.661,1.98],[-5.25,-0.59],[-4.136,-1]],"o":[[0.565,-3.01],[5.66,-1.98],[4.421,0.397],[2.568,0.798]],"v":[[-33.422,15.133],[-23.541,4.439],[-6.54,3.01],[7.266,4.508]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":174,"s":[{"i":[[0,0],[-9.318,0.062],[-4.956,-0.704],[-4.288,-0.672]],"o":[[-0.353,0.511],[6.341,-0.042],[4.372,0.473],[2.252,0.625]],"v":[[-39.783,7.131],[-23.543,1.746],[-6.473,2.566],[6.939,4.148]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":175,"s":[{"i":[[0,0],[-7.373,-0.661],[-5.987,-0.908],[-3.473,-0.494]],"o":[[4.936,-1.112],[5.389,0.674],[4.345,0.516],[2.073,0.526]],"v":[[-41.655,1.633],[-24.332,-0.148],[-6.488,2.688],[6.748,3.903]],"c":false}]},{"i":{"x":0.1,"y":1},"o":{"x":0.167,"y":0.167},"t":177,"s":[{"i":[[0,0],[-3.484,-2.106],[-8.05,-1.318],[-3.2,-0.264]],"o":[[2.562,1.927],[3.484,2.106],[4.29,0.602],[1.716,0.329]],"v":[[-36.386,-11.472],[-25.909,-3.937],[-6.358,2.513],[6.464,3.595]],"c":false}]},{"t":195,"s":[{"i":[[0,0],[-4.026,-6.73],[-10.797,-1.273],[-2.119,-0.01]],"o":[[0.65,4.552],[3.882,6.488],[3.499,0.413],[1.186,0.005]],"v":[[-34.027,-30.664],[-27.902,-11.553],[-6.28,2.761],[4.392,3.022]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":5.5,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path-8","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":244,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"hands 2","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[115,115,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[500,500,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":0,"s":[{"i":[[-3.103,0.058],[-2.523,0.098],[-2.257,-5.922],[-0.05,-3.076]],"o":[[2.726,-0.051],[10.848,-0.42],[1.636,4.291],[0,0]],"v":[[3.047,3.014],[11.974,3.008],[30.43,17.71],[32.973,31.114]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.6,"y":0},"t":10,"s":[{"i":[[-2.832,-0.073],[-2.303,-0.066],[-1.926,-3.085],[-0.239,-5.607]],"o":[[2.794,0.072],[11.894,0.339],[2.109,3.377],[0,0]],"v":[[2.758,2.83],[11.206,2.761],[28.065,7.836],[30.818,22.809]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":23.514,"s":[{"i":[[-2.816,0.314],[-2.144,-0.845],[-1.574,-3.736],[-3.615,-5.451]],"o":[[4.14,-0.462],[5.323,2.097],[1.94,4.605],[0,0]],"v":[[2.456,3.576],[12.827,2.873],[22.174,19.291],[30.152,32.241]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":34,"s":[{"i":[[-2.805,0.396],[-2.168,-0.779],[-1.2,-3.73],[-2.259,-5.223]],"o":[[3.592,-0.507],[6.085,2.162],[1.511,4.741],[0,0]],"v":[[2.725,4.04],[12.869,3.17],[22.316,18.867],[27.587,32.67]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.5,"y":0},"t":48.244,"s":[{"i":[[-2.816,0.314],[-2.144,-0.845],[-1.574,-3.736],[-3.615,-5.451]],"o":[[4.14,-0.462],[5.323,2.097],[1.94,4.605],[0,0]],"v":[[2.456,3.576],[12.827,2.873],[22.174,19.291],[30.152,32.241]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":59,"s":[{"i":[[-2.816,0.149],[-2.272,-0.949],[-1.475,-3.735],[-3.255,-5.39]],"o":[[4.388,-0.192],[5.461,2.271],[1.826,4.641],[0,0]],"v":[[2.315,3.354],[12.923,3.555],[22.3,19.809],[29.56,32.985]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":77,"s":[{"i":[[-2.816,-0.309],[-2.628,-1.238],[-1.2,-3.73],[-2.259,-5.223]],"o":[[5.074,0.557],[5.841,2.753],[1.511,4.741],[0,0]],"v":[[1.925,2.738],[13.189,5.447],[22.65,21.244],[27.921,35.047]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":95,"s":[{"i":[[-0.598,-0.181],[-0.539,-0.561],[-0.196,-2.606],[1.687,-4.266]],"o":[[4.741,1.436],[3.565,3.814],[0.23,3.032],[0,0]],"v":[[5.057,5.135],[15.215,9.739],[20.248,24.608],[18.597,38.923]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0.167},"t":96,"s":[{"i":[[-0.457,-0.364],[-0.52,-0.555],[-0.187,-2.596],[1.723,-4.257]],"o":[[3.097,0.541],[3.544,3.823],[0.218,3.016],[0,0]],"v":[[9.643,6.899],[15.234,9.778],[20.225,24.639],[18.511,38.959]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":98,"s":[{"i":[[-0.369,-0.478],[-0.508,-0.551],[-0.181,-2.589],[1.746,-4.251]],"o":[[0.369,0.484],[3.531,3.83],[0.21,3.006],[0,0]],"v":[[14.421,8.736],[15.245,9.803],[20.211,24.659],[18.457,38.982]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":100,"s":[{"i":[[0,0],[-0.074,-0.046],[-0.361,-3.217],[1.172,-4.139]],"o":[[0,0],[3.532,3.723],[0.33,3.294],[0,0]],"v":[[15.242,9.767],[15.359,9.86],[20.37,24.574],[19.626,38.792]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0.167},"t":102,"s":[{"i":[[-0.412,-0.138],[-0.318,-0.426],[-0.593,-4.024],[0.433,-3.996]],"o":[[0.167,0.033],[3.533,3.586],[0.484,3.664],[0,0]],"v":[[15.365,9.779],[15.505,9.932],[20.575,24.466],[21.13,38.547]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":116,"s":[{"i":[[0,0],[-1.009,-0.802],[-1.352,-6.671],[-1.989,-3.524]],"o":[[0,0],[4.03,3.256],[0.988,4.877],[0,0]],"v":[[12.859,7.391],[15.063,8.718],[21.245,24.11],[26.059,37.745]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0.167},"t":124,"s":[{"i":[[0,0],[-1.097,-1.149],[-0.799,-5.031],[-0.839,-3.94]],"o":[[0,0],[3.609,3.446],[0.71,4.75],[0,0]],"v":[[12.902,6.938],[15.063,8.718],[20.372,24.232],[22.707,38.343]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":137,"s":[{"i":[[0,0],[-0.388,-0.392],[-0.475,-4.068],[-0.163,-4.185]],"o":[[0,0],[3.092,3.858],[0.546,4.676],[0,0]],"v":[[14.961,8.53],[15.063,8.718],[19.859,24.304],[20.739,38.695]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":157,"s":[{"i":[[0,0],[-0.58,-0.466],[-1.352,-6.671],[-0.912,-4.483]],"o":[[0,0],[3.124,3.255],[0.988,4.877],[0,0]],"v":[[14.98,9.235],[15.19,9.407],[20.156,24.263],[23.242,38.545]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0.167},"t":165,"s":[{"i":[[-3.102,-0.816],[-1.57,-1.341],[-1.352,-6.671],[-0.912,-4.483]],"o":[[0.548,0.101],[3.658,3.223],[0.988,4.877],[0,0]],"v":[[12.399,7.597],[15.19,9.407],[20.156,24.263],[23.242,38.545]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":167,"s":[{"i":[[-3.663,-0.964],[-1.749,-1.499],[-1.352,-6.671],[-0.912,-4.483]],"o":[[0.647,0.119],[3.754,3.217],[0.988,4.877],[0,0]],"v":[[8.772,6.017],[15.19,9.407],[20.156,24.263],[23.242,38.545]],"c":false}]},{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0.167},"t":168,"s":[{"i":[[-3.718,-0.617],[-1.769,-1.459],[-1.384,-6.679],[-0.892,-4.426]],"o":[[6.299,1.053],[3.934,3.132],[1.01,4.878],[0,0]],"v":[[1.967,4.432],[15.1,9.259],[20.419,24.094],[23.49,38.317]],"c":false}]},{"t":189,"s":[{"i":[[-3.103,0.058],[-2.523,0.098],[-2.257,-5.922],[-0.05,-3.076]],"o":[[2.726,-0.051],[10.848,-0.42],[1.636,4.291],[0,0]],"v":[[3.047,3.014],[11.974,3.008],[30.43,17.71],[32.973,31.114]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":5.5,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path-8","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":244,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"body","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":0,"s":[116.171,146.724,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":10,"s":[116.171,151.524,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.723,"y":1},"o":{"x":0.395,"y":0},"t":21,"s":[116.171,146.724,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.361,"y":0},"t":32.309,"s":[116.171,147.924,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.723,"y":1},"o":{"x":0.395,"y":0},"t":48.244,"s":[116.171,146.724,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.723,"y":0.723},"o":{"x":0.167,"y":0.167},"t":73.717,"s":[116.171,147.924,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.71,"y":0},"t":167,"s":[116.171,147.924,0],"to":[0,0,0],"ti":[0,0,0]},{"t":187,"s":[116.171,146.724,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,14.211,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.5,0.5,0.5],"y":[0,0,0]},"t":0,"s":[100,100,100]},{"i":{"x":[0.5,0.5,0.5],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":5,"s":[97,103,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.484,0.484,0.484],"y":[0,0,0]},"t":10,"s":[100,100,100]},{"i":{"x":[0.486,0.486,0.486],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":15,"s":[103,97,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.5,0.5,0.5],"y":[0,0,0]},"t":21,"s":[100,100,100]},{"i":{"x":[0.486,0.486,0.486],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":26.025,"s":[98,102,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.5,0.5,0.5],"y":[0,0,0]},"t":32.309,"s":[100,100,100]},{"i":{"x":[0.486,0.486,0.486],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":39.988,"s":[103,97,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.48,0.48,0.48],"y":[0,0,0]},"t":48.244,"s":[100,100,100]},{"i":{"x":[0.471,0.471,0.471],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":59.158,"s":[98,102,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":73.717,"s":[100,100,100]},{"i":{"x":[0.471,0.471,0.471],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":89,"s":[103,97,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":104,"s":[100,100,100]},{"i":{"x":[0.471,0.471,0.471],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":114,"s":[98,102,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":133,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.71,0.71,0.71],"y":[0,0,0]},"t":167,"s":[100,100,100]},{"i":{"x":[0.2,0.2,0.2],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":175,"s":[98,102,100]},{"t":187,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":0,"s":[{"i":[[0,0],[-0.741,-5.903],[-1.922,-0.751],[-2.946,0],[-2.468,1.063],[-0.228,3.004],[0,0]],"o":[[0,0],[0.328,2.608],[2.556,0.999],[2.898,0],[1.959,-0.844],[0.638,-8.388],[0,0]],"v":[[-12.432,-14.575],[-11.977,7.814],[-8.487,12.849],[0.4,13.948],[9.223,12.916],[12.656,7.499],[12.482,-13.225]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":10,"s":[{"i":[[0,0],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[0,0]],"o":[[0,0],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0,0]],"v":[[-14.632,-6.25],[-13.927,7.863],[-10.625,12.562],[0.35,14.211],[11.198,12.616],[14.544,7.749],[14.632,-5.171]],"c":false}]},{"i":{"x":0.833,"y":0.913},"o":{"x":0.5,"y":0},"t":24,"s":[{"i":[[2.01,-10.893],[-0.221,-1.818],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[-2.532,4.395]],"o":[[-1.585,8.587],[0.221,1.818],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0.024,-0.319]],"v":[[-11.288,-18.4],[-12.135,7.863],[-8.833,12.562],[0.35,14.211],[9.406,12.616],[12.752,7.749],[13.837,-10.133]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.087},"t":32,"s":[{"i":[[3.168,-10.612],[-0.338,-1.852],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[-3.171,3.957]],"o":[[-2.669,8.935],[0.338,1.852],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0.078,-0.146]],"v":[[-11.587,-18.638],[-12.157,7.863],[-8.855,12.562],[0.35,14.211],[9.428,12.616],[12.773,7.749],[14.172,-9.711]],"c":false}]},{"i":{"x":0.833,"y":0.954},"o":{"x":0.5,"y":0},"t":48,"s":[{"i":[[2.01,-10.893],[-0.209,-1.898],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[-2.532,4.395]],"o":[[-1.585,8.587],[0.209,1.898],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0.024,-0.319]],"v":[[-11.08,-17.817],[-12.127,7.863],[-8.825,12.562],[0.35,14.211],[9.398,12.616],[12.744,7.749],[13.83,-9.727]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":77,"s":[{"i":[[3.203,-10.603],[-0.305,-1.276],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[-0.275,6.886]],"o":[[-2.702,8.946],[0.305,1.276],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0.08,-0.14]],"v":[[-11.404,-15.827],[-12.123,7.863],[-8.82,12.562],[0.35,14.211],[9.394,12.616],[12.739,7.749],[14.048,-5.48]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":95,"s":[{"i":[[5.646,-9.501],[-0.274,-1.374],[-1.915,-1.173],[-2.813,0],[-2.356,1.063],[-0.75,2.369],[-0.131,5.096]],"o":[[-5.397,9.039],[0.274,1.374],[1.958,1.192],[2.767,0],[1.87,-0.844],[0.743,-2.219],[0.08,-0.14]],"v":[[-10.068,-13.558],[-13.242,7.677],[-9.903,12.562],[-0.168,14.211],[8.516,12.616],[12.267,7.934],[13.587,-2.439]],"c":false}]},{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0.167},"t":98,"s":[{"i":[[5.771,-9.444],[-0.346,-1.217],[-1.916,-1.191],[-2.806,0],[-2.35,1.063],[-0.784,2.382],[-0.124,5.004]],"o":[[-5.535,9.043],[0.346,1.217],[1.93,1.197],[2.76,0],[1.866,-0.844],[0.782,-2.332],[0.08,-0.14]],"v":[[-9.999,-13.441],[-13.299,7.667],[-9.958,12.562],[-0.195,14.211],[8.471,12.616],[12.243,7.944],[13.58,-2.197]],"c":false}]},{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0},"t":104,"s":[{"i":[[5.832,-9.417],[-0.459,-1.398],[-1.917,-1.199],[-2.803,0],[-2.348,1.063],[-0.8,2.388],[-0.12,4.96]],"o":[[-5.602,9.046],[0.459,1.398],[1.917,1.199],[2.757,0],[1.863,-0.844],[0.8,-2.388],[0.08,-0.14]],"v":[[-9.966,-13.385],[-13.327,7.663],[-9.985,12.562],[-0.208,14.211],[8.449,12.616],[12.232,7.949],[13.872,-2.099]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":117,"s":[{"i":[[5.832,-9.417],[-0.392,-1.279],[-1.917,-1.199],[-2.803,0],[-2.348,1.063],[-0.8,2.388],[0.974,5.882]],"o":[[-5.602,9.046],[0.392,1.279],[1.917,1.199],[2.757,0],[1.863,-0.844],[0.8,-2.388],[0.08,-0.14]],"v":[[-8.982,-13.385],[-12.343,7.663],[-9.001,12.562],[-0.208,14.211],[8.058,12.616],[11.841,7.949],[13.276,-4.427]],"c":false}]},{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0.167},"t":124,"s":[{"i":[[5.832,-9.417],[-0.23,-1.373],[-1.917,-1.199],[-2.803,0],[-2.348,1.063],[-0.8,2.388],[0.177,5.21]],"o":[[-5.602,9.046],[0.23,1.373],[1.917,1.199],[2.757,0],[1.863,-0.844],[0.8,-2.388],[0.08,-0.14]],"v":[[-9.699,-13.385],[-13.06,7.663],[-9.718,12.562],[-0.208,14.211],[8.343,12.616],[12.126,7.949],[13.7,-2.8]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":134,"s":[{"i":[[5.832,-9.417],[-0.359,-1.248],[-1.917,-1.199],[-2.803,0],[-2.348,1.063],[-0.8,2.388],[-0.12,4.96]],"o":[[-5.602,9.046],[0.359,1.248],[1.917,1.199],[2.757,0],[1.863,-0.844],[0.8,-2.388],[0.08,-0.14]],"v":[[-9.966,-13.385],[-13.327,7.663],[-9.985,12.562],[-0.208,14.211],[8.449,12.616],[12.232,7.949],[13.652,-2.058]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.71,"y":0},"t":167,"s":[{"i":[[5.832,-9.417],[-0.259,-1.248],[-1.917,-1.199],[-2.803,0],[-2.348,1.063],[-0.8,2.388],[-0.12,4.96]],"o":[[-5.602,9.046],[0.259,1.248],[1.917,1.199],[2.757,0],[1.863,-0.844],[0.8,-2.388],[0.08,-0.14]],"v":[[-9.966,-13.385],[-13.327,7.663],[-9.985,12.562],[-0.208,14.211],[8.449,12.616],[12.232,7.949],[13.852,-2.099]],"c":false}]},{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0.167},"t":171,"s":[{"i":[[4.926,-9.164],[-0.379,-1.503],[-1.916,-1.181],[-2.81,0],[-2.354,1.063],[-0.764,2.375],[-0.114,4.713]],"o":[[-4.766,9.084],[0.379,1.503],[1.946,1.194],[2.764,0],[1.868,-0.844],[0.76,-2.269],[0.076,-0.133]],"v":[[-10.024,-13.626],[-12.944,7.673],[-9.604,12.562],[-0.18,14.211],[8.309,12.616],[12.07,7.939],[13.516,-5.476]],"c":false}]},{"t":187,"s":[{"i":[[0,0],[-0.741,-5.903],[-1.922,-0.751],[-2.946,0],[-2.468,1.063],[-0.228,3.004],[0,0]],"o":[[0,0],[0.328,2.608],[2.556,0.999],[2.898,0],[1.959,-0.844],[0.638,-8.388],[0,0]],"v":[[-12.432,-14.575],[-11.977,7.814],[-8.487,12.849],[0.4,13.948],[9.223,12.916],[12.656,7.499],[12.482,-13.225]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":5.5,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":0,"op":244,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"hand_6","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50.75,47.75,0],"ix":2,"l":2},"a":{"a":0,"k":[256,256,0],"ix":1,"l":2},"s":{"a":0,"k":[11,11,100],"ix":6,"l":2}},"ao":0,"w":512,"h":512,"ip":0,"op":240,"st":0,"bm":0}],"markers":[]}
\ No newline at end of file
diff --git a/submodules/TelegramUI/Resources/Animations/anim_hand7.json b/submodules/TelegramUI/Resources/Animations/anim_hand7.json
deleted file mode 100644
index f8f9d1967d..0000000000
--- a/submodules/TelegramUI/Resources/Animations/anim_hand7.json
+++ /dev/null
@@ -1 +0,0 @@
-{"v":"5.7.6","fr":60,"ip":0,"op":180,"w":100,"h":100,"nm":"hand_7 BIG 1x","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"handle","parent":6,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.6],"y":[0]},"t":10,"s":[174]},{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":16,"s":[147]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.213],"y":[0]},"t":23,"s":[-17]},{"i":{"x":[0.61],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":27,"s":[-35.589]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.5],"y":[0]},"t":32,"s":[27.015]},{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":37,"s":[47.22]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0]},"t":42,"s":[-18]},{"i":{"x":[0.61],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":48,"s":[-35.589]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.5],"y":[0]},"t":54,"s":[27.015]},{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":61,"s":[47.22]},{"i":{"x":[0.785],"y":[-2.394]},"o":{"x":[0.518],"y":[0]},"t":73,"s":[-18]},{"i":{"x":[0.312],"y":[1]},"o":{"x":[0.204],"y":[0.31]},"t":81,"s":[-9.175]},{"t":97,"s":[174]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.6,"y":0},"t":10,"s":[-9.96,1.125,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":16,"s":[-16.661,-20.671,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.5,"y":0},"t":23,"s":[-36.588,-27.368,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":27,"s":[-30.271,-30.836,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.5,"y":0},"t":32,"s":[-11.215,-32.394,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":37,"s":[-16.116,-31.018,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.5,"y":0},"t":42,"s":[-36.588,-27.368,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":48,"s":[-30.271,-30.836,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.5,"y":0},"t":54,"s":[-11.215,-32.394,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":61,"s":[-16.116,-31.018,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.5,"y":0},"t":73,"s":[-36.588,-27.368,0],"to":[0,0,0],"ti":[0,0,0]},{"t":97,"s":[-9.802,-0.869,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-215.5,-116,0],"ix":1,"l":2},"s":{"a":0,"k":[20.019,19.981,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-215.5,-116],[-215.146,-127.132]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":14,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-215.5,-116],[-210.023,-182.103]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-215.5,-116],[-207.5,-198.5]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":86,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-215.5,-116],[-207.5,-198.5]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":87,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-215.5,-116],[-211.874,-179.057]],"c":false}]},{"t":89,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-215.5,-116],[-215.991,-154.088]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":12,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":10,"op":92,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"flag","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":92.982,"ix":10},"p":{"a":0,"k":[-204.72,-162.79,0],"ix":2,"l":2},"a":{"a":0,"k":[-122.092,14.474,0],"ix":1,"l":2},"s":{"a":0,"k":[99.986,100.014,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.6,"y":0},"t":12,"s":[{"i":[[0.317,-0.377],[9.871,0.008],[-5.426,0.66]],"o":[[-6.226,-0.015],[-1.013,-0.074],[4.743,0.541]],"v":[[-5.96,154.637],[-50.486,154.578],[-27.743,151.11]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":14,"s":[{"i":[[0.317,-1.093],[9.871,0.023],[-3.056,2.72]],"o":[[-6.226,-0.044],[-1.013,-0.215],[7.435,1.476]],"v":[[-5.96,154.637],[-28.783,155.672],[-28.323,148.174]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[0.317,-4.427],[9.871,0.094],[-7.661,10.615]],"o":[[-6.226,-0.178],[-1.013,-0.869],[16.085,12.234]],"v":[[-5.96,154.637],[-39.634,154.55],[-66.371,118.451]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":18,"s":[{"i":[[0.317,-7.761],[9.871,0.165],[-21.058,28.062]],"o":[[-6.226,-0.312],[-1.013,-1.524],[1.096,39.632]],"v":[[-5.96,154.637],[-50.486,153.428],[-40.692,84.982]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[0.317,-7.761],[9.871,0.165],[-6.509,17.627]],"o":[[-6.226,-0.312],[-1.013,-1.524],[5.307,24.241]],"v":[[-5.96,154.637],[-50.486,153.428],[-25.785,87.237]],"c":true}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":22,"s":[{"i":[[0.317,-7.761],[9.871,0.165],[-32.021,13.36]],"o":[[-6.226,-0.312],[-1.013,-1.524],[-16.69,26.199]],"v":[[-5.96,154.637],[-50.486,153.428],[15.356,105.805]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":25,"s":[{"i":[[0.317,-7.761],[9.871,0.165],[-5.426,13.583]],"o":[[-6.226,-0.312],[-1.013,-1.524],[4.743,11.147]],"v":[[-5.96,154.637],[-50.486,153.428],[-27.743,82.045]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":27,"s":[{"i":[[0.317,-7.761],[9.871,0.165],[12.378,29.044]],"o":[[-6.226,-0.312],[-1.013,-1.524],[29.83,8.678]],"v":[[-5.96,154.637],[-50.486,153.428],[-51.505,85]],"c":true}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":32,"s":[{"i":[[0.317,7.203],[9.871,-0.153],[-31.325,-39.218]],"o":[[-6.226,0.289],[-1.013,1.414],[-3.169,-41.969]],"v":[[-5.83,154.152],[-51.499,151.85],[-15.606,221.238]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.5,"y":0},"t":34,"s":[{"i":[[0.317,7.203],[9.871,-0.153],[-5.426,-12.606]],"o":[[-6.226,0.289],[-1.013,1.414],[4.743,-10.345]],"v":[[-5.83,154.152],[-51.499,151.85],[-27.874,222.489]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":37,"s":[{"i":[[0.317,7.203],[9.871,-0.153],[11.573,-55.334]],"o":[[-6.226,0.289],[-1.013,1.414],[39.449,-25.407]],"v":[[-5.83,154.152],[-51.499,151.85],[-52.752,224.049]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":42,"s":[{"i":[[0.317,-6.39],[9.871,0.136],[-11.942,3.44]],"o":[[-6.226,-0.257],[-1.013,-1.254],[-1.26,15.905]],"v":[[-5.948,154.592],[-50.579,153.284],[-20.684,99.413]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":45,"s":[{"i":[[0.317,-7.761],[9.871,0.165],[12.378,29.044]],"o":[[-6.226,-0.312],[-1.013,-1.524],[29.83,8.678]],"v":[[-5.96,154.637],[-50.486,153.428],[-51.505,85]],"c":true}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":53,"s":[{"i":[[0.317,7.203],[9.871,-0.153],[-31.325,-39.218]],"o":[[-6.226,0.289],[-1.013,1.414],[-3.169,-41.969]],"v":[[-5.83,154.152],[-51.499,151.85],[-15.606,221.238]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.5,"y":0},"t":55.842,"s":[{"i":[[0.317,7.203],[9.871,-0.153],[-5.426,-12.606]],"o":[[-6.226,0.289],[-1.013,1.414],[4.743,-10.345]],"v":[[-5.83,154.152],[-51.499,151.85],[-27.874,222.489]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":63,"s":[{"i":[[0.317,7.203],[9.871,-0.153],[11.573,-55.334]],"o":[[-6.226,0.289],[-1.013,1.414],[39.449,-25.407]],"v":[[-5.83,154.152],[-51.499,151.85],[-52.752,224.049]],"c":true}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":71,"s":[{"i":[[0.317,-5.893],[9.871,0.126],[-21.71,21.684]],"o":[[-6.226,-0.237],[-1.013,-1.157],[-2.05,24.036]],"v":[[-5.944,154.576],[-50.612,153.231],[-14.362,101.878]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":75,"s":[{"i":[[0.317,-7.761],[9.871,0.165],[-5.426,13.583]],"o":[[-6.226,-0.312],[-1.013,-1.524],[4.743,11.147]],"v":[[-5.96,154.637],[-50.486,153.428],[-27.743,82.045]],"c":true}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":81,"s":[{"i":[[0.317,5.961],[9.871,-0.127],[30.989,-21.541]],"o":[[-6.226,0.24],[-1.013,1.17],[42.685,-3.8]],"v":[[-5.841,154.192],[-51.415,151.981],[-117.05,184.128]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":83,"s":[{"i":[[0.317,7.203],[9.871,-0.153],[-22.34,-40.1]],"o":[[-6.226,0.289],[-1.013,1.414],[1.595,-36.326]],"v":[[-5.83,154.152],[-51.499,151.85],[-28.747,222.978]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0.167},"t":86,"s":[{"i":[[0.139,7.208],[9.871,0.091],[-5.112,-12.737]],"o":[[-6.231,0.135],[-1.048,1.388],[4.997,-10.225]],"v":[[-5.313,153.232],[-48.29,151.152],[-29.044,221.002]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":87,"s":[{"i":[[0.317,7.203],[9.871,-0.153],[-5.426,-12.606]],"o":[[-13.386,-2.417],[-1.013,1.414],[4.743,-10.345]],"v":[[-4.828,156.44],[-48.115,167.327],[-27.874,222.489]],"c":true}]},{"t":89,"s":[{"i":[[0.317,7.203],[9.871,-0.153],[-0.737,-10.604]],"o":[[-6.226,0.289],[-1.013,1.414],[4.743,-10.345]],"v":[[-5.83,154.152],[-15.405,184.967],[-16.93,216.13]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":12,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-113.297,-133.109],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Polystar 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":14,"op":90,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":3,"nm":"NULL CONTROL","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[256.989,275.127,0],"ix":2,"l":2},"a":{"a":0,"k":[115,115,0],"ix":1,"l":2},"s":{"a":0,"k":[500,500,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":182,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"head","parent":8,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.6],"y":[0]},"t":1,"s":[0]},{"i":{"x":[0.3],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":11,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":23,"s":[-13]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.16],"y":[0]},"t":71,"s":[-13]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0]},"t":98.215,"s":[0]},{"i":{"x":[0.09],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":106.715,"s":[7]},{"t":127.357421875,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":1,"s":[0,-37.327,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":11,"s":[0,-26.827,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":23,"s":[-4.4,-39.427,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":36,"s":[-2.401,-30.424,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":50,"s":[-4.4,-39.427,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.315,"y":0},"t":71,"s":[-2.401,-30.424,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.496,"y":1},"o":{"x":0.6,"y":0},"t":98,"s":[5.201,-26.827,0],"to":[0,0,0],"ti":[0,0,0]},{"t":127.357421875,"s":[0,-37.327,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":1,"s":[100,100,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":5,"s":[97,103,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":11,"s":[105,95,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":16,"s":[97,103,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":23,"s":[105,95,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":28,"s":[97,103,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":36,"s":[100,100,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":44,"s":[97,103,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":50,"s":[105,95,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":58,"s":[97,103,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.16,0.16,0.16],"y":[0,0,0]},"t":71,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":98.215,"s":[105,95,100]},{"i":{"x":[0.09,0.09,0.09],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":106.715,"s":[95,105,100]},{"t":127.357421875,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-6.341,0],[0,-6.341],[6.341,0],[0,6.341]],"o":[[6.341,0],[0,6.341],[-6.341,0],[0,-6.341]],"v":[[0,-11.482],[11.482,0],[0,11.482],[-11.482,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Oval","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":5.5,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":0,"op":183,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":3,"nm":"hands","parent":8,"sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.5],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.5],"y":[0]},"t":21,"s":[-10]},{"i":{"x":[0.17],"y":[1]},"o":{"x":[0.5],"y":[0]},"t":70,"s":[-10]},{"t":128.572265625,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":0,"s":[-2.341,-17.777,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":10,"s":[-2.341,-9.777,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":21,"s":[-3.741,-17.977,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":35,"s":[-1.541,-11.777,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":1},"o":{"x":0.5,"y":0},"t":49,"s":[-1.541,-17.777,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.544,"y":0},"t":70,"s":[-1.541,-11.777,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.134,"y":1},"o":{"x":0.5,"y":0},"t":97,"s":[-2.341,-9.777,0],"to":[0,0,0],"ti":[0,0,0]},{"t":128.572265625,"s":[-2.341,-17.777,0]}],"ix":2,"l":2},"a":{"a":0,"k":[115,115,0],"ix":1,"l":2},"s":{"a":0,"k":[20,20,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":183,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"hands 3","parent":5,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[115,115,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[500,500,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":0,"s":[{"i":[[0,0],[-4.026,-6.73],[-10.797,-1.273],[-2.119,-0.01]],"o":[[0.65,4.552],[3.882,6.488],[3.499,0.413],[1.186,0.005]],"v":[[-34.027,-30.664],[-27.902,-11.553],[-6.28,2.761],[4.392,3.022]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.6,"y":0},"t":10,"s":[{"i":[[0,0],[-0.71,-5.814],[-13.22,1.67],[-6.659,-0.445]],"o":[[-4.86,-9.215],[0.566,4.637],[3.994,-0.505],[1.183,0.079]],"v":[[-9.125,0.522],[-27.079,-2.224],[-7.644,3.484],[11.49,2.929]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[0,0],[-1.902,-5.873],[-9.833,-0.14],[-7.372,0.373]],"o":[[-4.328,1.758],[1.817,5.11],[4.766,0.217],[1.182,0.009]],"v":[[-17.041,-20.099],[-26.446,-5.406],[-7.078,3.541],[9.615,2.763]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.5,"y":0},"t":23,"s":[{"i":[[0,0],[-4.205,-5.986],[-9.957,-1.926],[-4.458,0.474]],"o":[[5.753,4.944],[4.232,6.025],[3.773,0.73],[1.179,-0.125]],"v":[[-37.604,-27.175],[-25.224,-11.553],[-5.844,2.761],[7.055,3.069]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":27,"s":[{"i":[[0,0],[-1.728,-7.979],[-9.811,-4.073],[-4.458,0.474]],"o":[[3.018,5.449],[1.024,4.728],[4.574,1.385],[1.179,-0.125]],"v":[[-30.389,-31.532],[-24.556,-12.392],[-6.174,2.474],[7.055,3.069]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.5,"y":0},"t":32,"s":[{"i":[[0,0],[-0.133,-5.267],[-9.077,-3.752],[-4.458,0.474]],"o":[[-4.804,4.722],[0.193,7.66],[5.825,2.408],[1.179,-0.125]],"v":[[-10.804,-33.837],[-18.834,-15.906],[-6.69,2.025],[7.055,3.069]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":37,"s":[{"i":[[0,0],[-0.271,-4.621],[-9.517,-2.839],[-4.458,0.474]],"o":[[-3.394,4.297],[0.242,4.13],[4.799,1.569],[1.179,-0.125]],"v":[[-15.907,-30.468],[-22.029,-13.729],[-6.267,2.393],[7.055,3.069]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.5,"y":0},"t":42,"s":[{"i":[[0,0],[-3.928,-6.171],[-9.957,-1.926],[-4.563,0.793]],"o":[[5.522,5.2],[3.954,6.211],[3.773,0.73],[1.168,-0.203]],"v":[[-36.207,-29.267],[-24.551,-13.099],[-5.703,2.786],[7.055,3.069]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":48,"s":[{"i":[[0,0],[-1.728,-7.979],[-9.811,-4.073],[-4.458,0.474]],"o":[[3.018,5.449],[1.024,4.728],[4.574,1.385],[1.179,-0.125]],"v":[[-30.389,-31.532],[-24.556,-12.392],[-6.174,2.474],[7.055,3.069]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.5,"y":0},"t":54,"s":[{"i":[[0,0],[-0.133,-5.267],[-9.319,-3.103],[-4.568,0.674]],"o":[[-4.804,4.722],[0.193,7.66],[5.735,1.909],[1.173,-0.173]],"v":[[-10.804,-33.837],[-18.834,-15.906],[-6.656,1.832],[7.072,2.972]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":61,"s":[{"i":[[0,0],[-0.271,-4.621],[-9.517,-2.839],[-4.458,0.474]],"o":[[-3.394,4.297],[0.242,4.13],[4.799,1.569],[1.179,-0.125]],"v":[[-15.907,-30.468],[-22.029,-13.729],[-6.267,2.393],[7.055,3.069]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.5,"y":0},"t":73,"s":[{"i":[[0,0],[-4.205,-5.986],[-9.957,-1.926],[-4.458,0.474]],"o":[[5.753,4.944],[4.232,6.025],[3.773,0.73],[1.179,-0.125]],"v":[[-37.604,-27.175],[-25.224,-11.553],[-5.844,2.761],[7.055,3.069]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.6,"y":0},"t":97,"s":[{"i":[[0,0],[-0.71,-5.814],[-13.22,1.67],[-6.659,-0.445]],"o":[[-4.86,-9.215],[0.566,4.637],[3.994,-0.505],[1.183,0.079]],"v":[[-9.125,0.522],[-27.079,-2.224],[-7.644,3.484],[11.49,2.929]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0.167},"t":110,"s":[{"i":[[0,0],[-3.924,-6.477],[-10.094,-0.98],[-3.546,0.075]],"o":[[4.365,3.67],[3.808,6.286],[3.961,0.384],[1.183,-0.028]],"v":[[-37.672,-26.337],[-25.271,-10.97],[-5.942,2.371],[7.258,2.943]],"c":false}]},{"t":131,"s":[{"i":[[0,0],[-4.026,-6.73],[-10.797,-1.273],[-2.119,-0.01]],"o":[[0.65,4.552],[3.882,6.488],[3.499,0.413],[1.186,0.005]],"v":[[-34.027,-30.664],[-27.902,-11.553],[-6.28,2.761],[4.392,3.022]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":5.5,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path-8","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":183,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"hands 2","parent":5,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[115,115,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[500,500,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":0,"s":[{"i":[[-3.103,0.058],[-2.523,0.098],[-2.257,-5.922],[-0.05,-3.076]],"o":[[2.726,-0.051],[10.848,-0.42],[1.636,4.291],[0,0]],"v":[[3.047,3.014],[11.974,3.008],[30.43,17.71],[32.973,31.114]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":10,"s":[{"i":[[-2.832,-0.073],[-2.303,-0.066],[-1.926,-3.085],[-0.239,-5.607]],"o":[[2.794,0.072],[11.894,0.339],[2.109,3.377],[0,0]],"v":[[2.763,2.93],[11.324,2.869],[28.065,7.836],[30.818,22.809]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":22,"s":[{"i":[[-2.833,0.019],[-2.144,-0.845],[-1.574,-3.736],[-3.615,-5.451]],"o":[[4.963,-0.034],[5.323,2.097],[1.94,4.605],[0,0]],"v":[[2.632,3.384],[12.827,2.873],[22.174,19.291],[30.152,32.241]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.7,"y":0},"t":36,"s":[{"i":[[-2.83,-0.135],[-3.536,-1.048],[-1.592,-3.476],[-1.09,-6.235]],"o":[[7.035,0.337],[5.485,1.626],[1.67,3.647],[0,0]],"v":[[1.498,3.488],[12.683,2.542],[23.022,15.874],[26.398,31.323]],"c":false}]},{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":50,"s":[{"i":[[-2.829,0.15],[-2.144,-0.845],[-1.574,-3.736],[-3.615,-5.451]],"o":[[4.712,-0.25],[5.323,2.097],[1.94,4.605],[0,0]],"v":[[2.417,3.447],[12.827,2.873],[22.174,19.291],[30.152,32.241]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.4,"y":0},"t":71,"s":[{"i":[[-2.833,0.023],[-3.532,-0.206],[-1.416,-3.799],[-0.329,-6.321]],"o":[[4.317,-0.036],[4.623,0.27],[1.401,3.758],[0,0]],"v":[[1.983,3.325],[12.861,3.014],[25.932,11.288],[27.784,26.515]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":97,"s":[{"i":[[-2.832,-0.073],[-2.303,-0.066],[-1.926,-3.085],[-0.239,-5.607]],"o":[[2.794,0.072],[11.894,0.339],[2.109,3.377],[0,0]],"v":[[2.763,2.93],[11.324,2.869],[28.065,7.836],[30.818,22.809]],"c":false}]},{"t":124.927734375,"s":[{"i":[[-3.103,0.058],[-2.523,0.098],[-2.257,-5.922],[-0.05,-3.076]],"o":[[2.726,-0.051],[10.848,-0.42],[1.636,4.291],[0,0]],"v":[[3.047,3.014],[11.974,3.008],[30.43,17.71],[32.973,31.114]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":5.5,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path-8","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":183,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"body","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":0,"s":[116.171,146.724,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":10,"s":[116.171,151.524,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":21,"s":[116.171,140.724,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":35,"s":[116.171,146.724,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":1},"o":{"x":0.5,"y":0},"t":49,"s":[116.171,143.924,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.544,"y":0},"t":70,"s":[116.171,146.724,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.134,"y":1},"o":{"x":0.5,"y":0},"t":97,"s":[116.171,151.524,0],"to":[0,0,0],"ti":[0,0,0]},{"t":128.572265625,"s":[116.171,146.724,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,14.211,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.5,0.5,0.5],"y":[0,0,0]},"t":0,"s":[100,100,100]},{"i":{"x":[0.5,0.5,0.5],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":5,"s":[97,103,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.484,0.484,0.484],"y":[0,0,0]},"t":10,"s":[100,100,100]},{"i":{"x":[0.486,0.486,0.486],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":15,"s":[103,97,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.5,0.5,0.5],"y":[0,0,0]},"t":21,"s":[100,100,100]},{"i":{"x":[0.5,0.5,0.5],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":27,"s":[97,103,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.5,0.5,0.5],"y":[0,0,0]},"t":35,"s":[100,100,100]},{"i":{"x":[0.5,0.5,0.5],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":42,"s":[103,97,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.5,0.5,0.5],"y":[0,0,0]},"t":49,"s":[100,100,100]},{"i":{"x":[0.4,0.4,0.4],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":56,"s":[97,103,100]},{"i":{"x":[0.5,0.5,0.5],"y":[1,1,1]},"o":{"x":[0.422,0.422,0.422],"y":[0,0,0]},"t":70,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.484,0.484,0.484],"y":[0,0,0]},"t":97,"s":[100,100,100]},{"i":{"x":[0.17,0.17,0.17],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":109.143,"s":[97,103,100]},{"t":128.572265625,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":0,"s":[{"i":[[0,0],[-0.741,-5.903],[-1.922,-0.751],[-2.946,0],[-2.468,1.063],[-0.228,3.004],[0,0]],"o":[[0,0],[0.328,2.608],[2.556,0.999],[2.898,0],[1.959,-0.844],[0.638,-8.388],[0,0]],"v":[[-12.432,-14.575],[-11.977,7.814],[-8.487,12.849],[0.4,13.948],[9.223,12.916],[12.656,7.499],[12.482,-13.225]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":10,"s":[{"i":[[0,0],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[0,0]],"o":[[0,0],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0,0]],"v":[[-14.032,-4.05],[-13.127,7.863],[-9.825,12.562],[0.35,14.211],[10.398,12.616],[13.744,7.749],[14.232,-7.571]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.5,"y":0},"t":21,"s":[{"i":[[-1.863,-5.271],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[1.3,6.472]],"o":[[1.863,5.271],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[-0.223,-1.111]],"v":[[-14.632,-13.411],[-12.127,7.863],[-8.825,12.562],[0.35,14.211],[9.398,12.616],[12.744,7.749],[11.232,-16.011]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":35,"s":[{"i":[[-1.922,-5.439],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[2.094,6.249]],"o":[[1.922,5.439],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0,-0.323]],"v":[[-15.346,-10.627],[-12.527,7.863],[-9.225,12.562],[0.35,14.211],[9.798,12.616],[13.144,7.749],[11.974,-10.116]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":42,"s":[{"i":[[-1.882,-5.326],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[1.559,6.399]],"o":[[1.882,5.326],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[-0.197,-0.919]],"v":[[-14.481,-12.569],[-12.098,7.863],[-8.796,12.562],[0.35,14.211],[9.369,12.616],[12.715,7.749],[11.484,-13.946]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.5,"y":0},"t":49,"s":[{"i":[[-1.863,-5.271],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[1.3,6.472]],"o":[[1.863,5.271],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[-0.223,-1.111]],"v":[[-14.732,-16.111],[-12.127,7.863],[-8.825,12.562],[0.35,14.211],[9.398,12.616],[12.744,7.749],[11.532,-15.511]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":54,"s":[{"i":[[-1.872,-5.295],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[1.413,6.44]],"o":[[1.872,5.295],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[-0.191,-0.998]],"v":[[-14.729,-16.667],[-12.144,7.863],[-8.842,12.562],[0.35,14.211],[9.415,12.616],[12.761,7.749],[11.555,-14.741]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0.167},"t":57,"s":[{"i":[[-1.882,-5.326],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[1.559,6.399]],"o":[[1.882,5.326],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0,0.274]],"v":[[-14.818,-14.61],[-12.155,7.863],[-8.853,12.562],[0.35,14.211],[9.426,12.616],[12.771,7.749],[11.18,-14.235]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0},"t":70,"s":[{"i":[[-1.922,-5.439],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[2.094,6.249]],"o":[[1.922,5.439],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0,-0.323]],"v":[[-15.324,-9.227],[-12.527,7.863],[-9.225,12.562],[0.35,14.211],[9.798,12.616],[13.144,7.749],[11.974,-10.116]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.5,"y":0},"t":97,"s":[{"i":[[0,0],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[0,0]],"o":[[0,0],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0,0]],"v":[[-14.032,-4.05],[-13.127,7.863],[-9.825,12.562],[0.35,14.211],[10.398,12.616],[13.744,7.749],[14.232,-7.571]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":104,"s":[{"i":[[-0.837,-2.368],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[0.912,2.72]],"o":[[0.837,2.368],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0,-0.141]],"v":[[-13.849,-7.596],[-12.139,7.863],[-8.836,12.562],[0.35,14.211],[9.41,12.616],[12.755,7.749],[12.383,-8.209]],"c":false}]},{"i":{"x":0.17,"y":1},"o":{"x":0.167,"y":0.167},"t":112,"s":[{"i":[[0,-1.426],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[0.292,0.87]],"o":[[0,0.803],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0,-0.045]],"v":[[-13.276,-13.757],[-12.148,7.863],[-8.845,12.562],[0.35,14.211],[9.419,12.616],[12.764,7.749],[12.791,-11.807]],"c":false}]},{"t":128.572265625,"s":[{"i":[[0,0],[-0.741,-5.903],[-1.922,-0.751],[-2.946,0],[-2.468,1.063],[-0.228,3.004],[0,0]],"o":[[0,0],[0.328,2.608],[2.556,0.999],[2.898,0],[1.959,-0.844],[0.638,-8.388],[0,0]],"v":[[-12.432,-14.575],[-11.977,7.814],[-8.487,12.849],[0.4,13.948],[9.223,12.916],[12.656,7.499],[12.482,-13.225]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":5.5,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":0,"op":183,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"hand_7","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50.75,47.75,0],"ix":2,"l":2},"a":{"a":0,"k":[256,256,0],"ix":1,"l":2},"s":{"a":0,"k":[11,11,100],"ix":6,"l":2}},"ao":0,"w":512,"h":512,"ip":0,"op":180,"st":0,"bm":0}],"markers":[]}
\ No newline at end of file
diff --git a/submodules/TelegramUI/Resources/Animations/anim_moretosort_r.json b/submodules/TelegramUI/Resources/Animations/anim_moretosort_r.json
deleted file mode 100644
index c1466c90e7..0000000000
--- a/submodules/TelegramUI/Resources/Animations/anim_moretosort_r.json
+++ /dev/null
@@ -1 +0,0 @@
-{"v":"5.12.1","fr":60,"ip":0,"op":90,"w":512,"h":512,"nm":"more_to_sort_cw","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"1","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":0,"s":[101,150,0],"to":[2.333,0,0],"ti":[-2.333,0,0]},{"t":89,"s":[115,150,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":0,"s":[{"i":[[0,0],[0,-0.823],[-0.823,0],[0,0],[0,0.823],[0.812,0]],"o":[[-0.823,0],[0,0.823],[0,0],[0.812,0],[0,-0.823],[0,0]],"v":[[-0.01,-1.5],[-1.5,0],[-0.01,1.5],[0.01,1.5],[1.5,0],[0.01,-1.5]],"c":true}]},{"t":89,"s":[{"i":[[0,0],[0,-0.367],[-0.367,0],[0,0],[0,0.367],[0.367,0]],"o":[[-0.367,0],[0,0.367],[0,0],[0.367,0],[0,-0.367],[0,0]],"v":[[-6,-0.665],[-6.665,0],[-6,0.665],[6,0.665],[6.665,0],[6,-0.665]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[1000,1000],"ix":3},"r":{"a":0,"k":-90,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector 202 (Stroke)","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":90,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"2","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":0,"s":[150.5,150,0],"to":[0.75,0,0],"ti":[-0.75,0,0]},{"t":89,"s":[155,150,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":0,"s":[{"i":[[0,0],[0,-0.823],[-0.823,0],[0,0],[0,0.823],[0.812,0]],"o":[[-0.823,0],[0,0.823],[0,0],[0.812,0],[0,-0.823],[0,0]],"v":[[-0.01,-1.5],[-1.5,0],[-0.01,1.5],[0.01,1.5],[1.5,0],[0.01,-1.5]],"c":true}]},{"t":89,"s":[{"i":[[0,0],[0,-0.367],[-0.367,0],[0,0],[0,0.367],[0.367,0]],"o":[[-0.367,0],[0,0.367],[0,0],[0.367,0],[0,-0.367],[0,0]],"v":[[-4,-0.665],[-4.665,0],[-4,0.665],[4,0.665],[4.665,0],[4,-0.665]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[1000,1000],"ix":3},"r":{"a":0,"k":-90,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector 202 (Stroke)","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":90,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"3","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":0,"s":[200,150,0],"to":[-0.833,0,0],"ti":[0.833,0,0]},{"t":89,"s":[195,150,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":0,"s":[{"i":[[0,0],[0,-0.823],[-0.823,0],[0,0],[0,0.823],[0.812,0]],"o":[[-0.823,0],[0,0.823],[0,0],[0.812,0],[0,-0.823],[0,0]],"v":[[-0.01,-1.5],[-1.5,0],[-0.01,1.5],[0.01,1.5],[1.5,0],[0.01,-1.5]],"c":true}]},{"t":89,"s":[{"i":[[0,0],[0,-0.367],[-0.367,0],[0,0],[0,0.367],[0.367,0]],"o":[[-0.367,0],[0,0.367],[0,0],[0.367,0],[0,-0.367],[0,0]],"v":[[-2,-0.665],[-2.665,0],[-2,0.665],[2,0.665],[2.665,0],[2,-0.665]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[1000,1000],"ix":3},"r":{"a":0,"k":-90,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector 202 (Stroke)","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":90,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"more and search","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.4],"y":[1]},"o":{"x":[0.6],"y":[0]},"t":0,"s":[0]},{"t":89,"s":[90]}],"ix":10},"p":{"a":0,"k":[256,256,0],"ix":2,"l":2},"a":{"a":0,"k":[150,150,0],"ix":1,"l":2},"s":{"a":0,"k":[170.667,170.667,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":0,"s":[{"i":[[-55.228,0],[0,55.229],[55.229,0],[0,-55.228]],"o":[[55.229,0],[0,-55.228],[-55.228,0],[0,55.229]],"v":[[23.5,123.5],[123.5,23.5],[23.5,-76.5],[-76.5,23.5]],"c":true}]},{"t":102,"s":[{"i":[[-55.228,0],[0,55.229],[55.229,0],[0,-55.228]],"o":[[55.229,0],[0,-55.228],[-55.228,0],[0,55.229]],"v":[[23.5,123.5],[123.5,23.5],[23.5,-76.5],[-76.5,23.5]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":13.33,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[126.5,126.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"c","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-10,"op":92,"st":-10,"ct":1,"bm":0}],"markers":[],"props":{}}
\ No newline at end of file
diff --git a/submodules/TelegramUI/Resources/Animations/anim_notificationsoundprogress.json b/submodules/TelegramUI/Resources/Animations/anim_notificationsoundprogress.json
deleted file mode 100644
index 7c93f3b7b7..0000000000
--- a/submodules/TelegramUI/Resources/Animations/anim_notificationsoundprogress.json
+++ /dev/null
@@ -1 +0,0 @@
-{"v":"4.8.0","meta":{"g":"LottieFiles AE ","a":"","k":"","d":"","tc":""},"fr":60,"ip":0,"op":60,"w":512,"h":512,"nm":"Sound Download","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"NULL ALL","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.2,"y":1},"o":{"x":0.1,"y":0},"t":-1,"s":[256,56,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.7,"y":1},"o":{"x":0.7,"y":0},"t":11,"s":[256,322,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":1},"o":{"x":0.3,"y":0},"t":25,"s":[256,228,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.41,"y":1},"o":{"x":0.3,"y":0},"t":39,"s":[256,288,0],"to":[0,0,0],"ti":[0,0,0]},{"t":59,"s":[256,256,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.4,0.4,0.4],"y":[1,1,1]},"o":{"x":[0.359,0.359,0.115],"y":[0,0,0]},"t":0,"s":[20,60,100]},{"i":{"x":[0.54,0.54,0.738],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":11,"s":[102,70,100]},{"i":{"x":[0.4,0.4,0.4],"y":[1,1,1]},"o":{"x":[0.3,0.3,0.3],"y":[0,0,0]},"t":25,"s":[90,110,100]},{"i":{"x":[0.41,0.41,0.41],"y":[1,1,1]},"o":{"x":[0.3,0.3,0.3],"y":[0,0,0]},"t":39,"s":[105,95,100]},{"t":59,"s":[100,100,100]}],"ix":6}},"ao":0,"ip":1,"op":179,"st":-1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Arrow 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.7,"y":1},"o":{"x":0.7,"y":0},"t":8,"s":[387,21.333,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":1},"o":{"x":0.3,"y":0},"t":22,"s":[389,181.333,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.41,"y":1},"o":{"x":0.3,"y":0},"t":36,"s":[399,111.333,0],"to":[0,0,0],"ti":[0,0,0]},{"t":59,"s":[395,141.333,0]}],"ix":2},"a":{"a":0,"k":[139,-114.667,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[112.991,-95.024],[139.219,-96.402],[166.009,-94.955]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":15,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[112.991,-101.972],[139.219,-36.333],[166.009,-105.232]],"c":false}]},{"i":{"x":0.71,"y":1},"o":{"x":0.167,"y":0.167},"t":22,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[68,-84],[139.173,-36.333],[210,-84]],"c":false}]},{"i":{"x":0.4,"y":1},"o":{"x":0.29,"y":0},"t":36,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[86.5,-84],[139.137,-36.333],[193,-84.5]],"c":false}]},{"t":59,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[70.667,-86.5],[139.167,-36.333],[207.333,-86.5]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":40,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[138.582,-93.931],[139.036,-96.361]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":15,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[138.582,-154],[139.036,-38.319]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":19,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[138.145,-207.143],[139.051,-37.851]],"c":false}]},{"i":{"x":0.71,"y":1},"o":{"x":0.167,"y":0.167},"t":22,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[138.063,-170],[139.063,-37.5]],"c":false}]},{"i":{"x":0.4,"y":1},"o":{"x":0.29,"y":0},"t":36,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[139.063,-193],[139.051,-37.5]],"c":false}]},{"t":59,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[139.063,-193],[139.063,-37.5]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":40,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":12,"op":179,"st":-1,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"R","parent":5,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[88.174,82.369,0],"ix":2},"a":{"a":0,"k":[88.174,82.369,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.6,"y":1},"o":{"x":0.2,"y":0},"t":14,"s":[{"i":[[-0.626,-19.83],[0,-0.716],[39.672,0],[0,30.227],[-39.672,0],[-10.43,-4.012],[0,0],[-5.043,-3.301]],"o":[[0,0],[0,30.227],[-39.672,0],[0,-30.227],[12.077,0],[0,0],[3.48,-0.35],[11.788,7.716]],"v":[[159.476,-150.419],[160.007,103.453],[84.753,158.183],[16.341,103.453],[84.753,48.723],[118.958,55.005],[118.979,-191.031],[139.841,-186.806]],"c":true}]},{"i":{"x":0.517,"y":1},"o":{"x":0.589,"y":0},"t":22,"s":[{"i":[[-4.753,-0.752],[0,-0.716],[39.672,0],[0,30.227],[-39.672,0],[-10.43,-4.012],[0,0],[-5.911,-1.178]],"o":[[0,0],[0,30.227],[-39.672,0],[0,-30.227],[12.077,0],[0,0],[5.313,2.033],[9.821,1.957]],"v":[[159.61,61.441],[160.007,103.453],[84.753,158.183],[16.341,103.453],[84.753,48.723],[118.958,55.005],[119.173,53.964],[136.554,58.5]],"c":true}]},{"i":{"x":0.41,"y":1},"o":{"x":0.433,"y":0},"t":36,"s":[{"i":[[-6.917,3.879],[0,-0.716],[39.672,0],[0,30.227],[-39.672,0],[-10.43,-4.012],[0,0],[-6.028,0]],"o":[[0,0],[0,30.227],[-39.672,0],[0,-30.227],[12.077,0],[0,0],[5.313,2.033],[8.494,0]],"v":[[159.348,-48.29],[160.007,103.453],[84.753,158.183],[16.341,103.453],[84.753,48.723],[118.958,55.005],[118.851,-45.346],[135.959,-42.2]],"c":true}]},{"t":59,"s":[{"i":[[-6.917,3.879],[0,-0.716],[39.672,0],[0,30.227],[-39.672,0],[-10.43,-4.012],[0,0],[-6.028,0]],"o":[[0,0],[0,30.227],[-39.672,0],[0,-30.227],[12.077,0],[0,0],[5.313,2.033],[8.494,0]],"v":[[159.452,6.555],[160.007,103.453],[84.753,158.183],[16.341,103.453],[84.753,48.723],[118.958,55.005],[118.955,9.499],[136.063,12.646]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":181,"st":1,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"L","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-58.912,2.378,0],"ix":2},"a":{"a":0,"k":[-58.912,2.378,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0,"y":0.751},"o":{"x":0.05,"y":0},"t":13,"s":[{"i":[[0,0],[0,0],[0,0],[0.171,-0.721],[0,-0.687],[39.672,0],[0,30.227],[-39.672,0],[-9.268,-3.321],[0,0],[-19.193,4.657]],"o":[[0,0],[0,0],[-0.752,0.152],[0,0],[0,30.227],[-39.672,0],[0,-30.227],[10.92,0],[0,0],[0,-19.896],[0,0]],"v":[[118.79,-191.298],[119.935,-149.636],[-61.03,-115.682],[-62.53,-114.233],[-62.334,137.659],[-134.167,192.389],[-206,137.659],[-134.167,82.929],[-103.627,88.107],[-103.629,-113.786],[-70.799,-155.552]],"c":true}]},{"i":{"x":0.564,"y":1},"o":{"x":0.514,"y":0.128},"t":32,"s":[{"i":[[0,0],[0,0],[0,0],[0.171,-0.721],[0,-0.687],[39.672,0],[0,30.227],[-39.672,0],[-9.268,-3.322],[0,0],[-19.192,4.657]],"o":[[0,0],[0,0],[-0.752,0.152],[0,0],[0,30.227],[-39.672,0],[0,-30.227],[10.92,0],[0,0],[0,-19.896],[0,0]],"v":[[54.288,-183.407],[54.277,-141.529],[-61.03,-115.682],[-62.53,-114.233],[-62.334,137.659],[-134.167,192.389],[-206,137.659],[-134.167,82.929],[-103.627,88.107],[-103.629,-113.786],[-70.799,-155.552]],"c":true}]},{"t":59,"s":[{"i":[[0,0],[0,0],[0,0],[0.171,-0.721],[0,-0.687],[39.672,0],[0,30.227],[-39.672,0],[-9.268,-3.322],[0,0],[-19.193,4.657]],"o":[[0,0],[0,0],[-0.752,0.152],[0,0],[0,30.227],[-39.672,0],[0,-30.227],[10.92,0],[0,0],[0,-19.896],[0,0]],"v":[[88.175,-187.632],[88.165,-145.754],[-61.03,-115.682],[-62.53,-114.233],[-62.334,137.659],[-134.167,192.389],[-206,137.659],[-134.167,82.929],[-103.628,88.107],[-103.629,-113.786],[-70.799,-155.552]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":179,"st":-1,"bm":0}],"markers":[]}
\ No newline at end of file
diff --git a/submodules/TelegramUI/Resources/Animations/anim_voicemute.json b/submodules/TelegramUI/Resources/Animations/anim_voicemute.json
deleted file mode 100644
index a37a9c8609..0000000000
--- a/submodules/TelegramUI/Resources/Animations/anim_voicemute.json
+++ /dev/null
@@ -1 +0,0 @@
-{"v":"5.5.9","fr":60,"ip":0,"op":20,"w":72,"h":72,"nm":"ic_mute","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Path 4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[34.5,37.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-8.5,-8.5],[8.5,8.5]],"c":false},"ix":2},"nm":"Контур 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.33,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Обводка 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"Path 4","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"t":10,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Обрезать контуры 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":20,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Path 5","td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[34.5,37.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-8.5,-8.5],[8.5,8.5]],"c":false},"ix":2},"nm":"Контур 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.109803922474,0.109803922474,0.117647059262,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Обводка 2","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"Path 4","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"t":10,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Обрезать контуры 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":20,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Icon","tt":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[36,37.2,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-0.36,0],[0,-0.36],[3.81,-0.34],[0,0],[0.37,0],[0.04,0.32],[0,0],[0,0],[0,3.9],[-0.36,0],[0,-0.36],[-3.38,0],[0,3.39]],"o":[[0.37,0],[0,3.9],[0,0],[0,0.37],[-0.33,0],[0,0],[0,0],[-3.81,-0.34],[0,-0.36],[0.37,0],[0,3.39],[3.39,0],[0,-0.36]],"v":[[6.796,-1.464],[7.466,-0.804],[0.666,6.636],[0.666,9.196],[-0.004,9.866],[-0.654,9.286],[-0.664,9.196],[-0.664,6.636],[-7.464,-0.804],[-6.804,-1.464],[-6.134,-0.804],[-0.004,5.336],[6.136,-0.804]],"c":true},"ix":2},"nm":"Контур 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[-2.35,0],[-0.09,-2.28],[0,0],[0,0],[2.36,0],[0.1,2.27],[0,0],[0,0]],"o":[[2.3,0],[0,0],[0,0],[0,2.36],[-2.29,0],[0,0],[0,0],[0,-2.35]],"v":[[-0.004,-9.864],[4.256,-5.774],[4.266,-5.604],[4.266,-0.804],[-0.004,3.466],[-4.264,-0.624],[-4.264,-0.804],[-4.264,-5.604]],"c":true},"ix":2},"nm":"Контур 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[1.62,0],[0.09,-1.55],[0,0],[0,0],[-1.63,0],[-0.08,1.55],[0,0],[0,0]],"o":[[-1.57,0],[0,0],[0,0],[0,1.62],[1.57,0],[0,0],[0,0],[0,-1.63]],"v":[[0.004,-8.536],[-2.936,-5.756],[-2.936,-5.596],[-2.936,-0.796],[0.004,2.134],[2.934,-0.646],[2.934,-0.796],[2.934,-5.596]],"c":true},"ix":2},"nm":"Контур 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Объединить контуры 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Заливка 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"Icon","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":20,"st":0,"bm":0}],"markers":[]}
\ No newline at end of file
diff --git a/submodules/TelegramUI/Resources/Animations/device_edge.json b/submodules/TelegramUI/Resources/Animations/device_edge.json
deleted file mode 100644
index 487209ad55..0000000000
--- a/submodules/TelegramUI/Resources/Animations/device_edge.json
+++ /dev/null
@@ -1 +0,0 @@
-{"v":"5.7.4","fr":60,"ip":0,"op":120,"w":30,"h":30,"nm":"edge_30","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Union","parent":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0.003,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[-0.082,0.078],[-0.014,0.019],[0,0.42],[0.001,0.035],[0,0],[0,0],[0,0.009],[0.002,0.03],[0.293,0.391],[0.348,0.176],[0.39,0.003],[0.247,-0.117],[0,0],[0,0],[0.009,-0.004],[0.226,-0.375],[0.005,-0.438],[-3.697,0],[-0.897,0.336],[-0.267,0.139],[-0.069,-0.011],[-0.048,-0.051],[-0.007,-0.069],[0.037,-0.059],[2.152,-0.744],[0,0],[0.253,-0.054],[-0.228,0.072],[2.252,0.966],[1.04,2.219],[-0.014,1.51],[-0.03,0.216],[0.003,-0.209],[-1.868,1.842],[-2.628,0],[-1.655,-3.238],[-0.012,-0.024],[-0.003,-0.006],[0.019,-1.66],[0.417,-0.725],[0.722,-0.422],[0.832,-0.002],[0.001,0],[1.105,0.768],[0,0.204]],"o":[[0.024,-0.023],[0.395,-0.514],[0,-0.029],[0,0],[0,0],[0,-0.009],[-0.001,-0.027],[-0.029,-0.486],[-0.233,-0.313],[-0.348,-0.176],[-0.533,-0.01],[0,0],[0,0],[-0.009,0.004],[-0.384,0.209],[-0.226,0.375],[0,3.263],[0.958,0.002],[0.282,-0.106],[0.061,-0.034],[0.069,0.011],[0.048,0.051],[0.007,0.069],[-1.215,1.926],[0,0],[-0.193,0.061],[0.234,-0.044],[-2.325,0.775],[-2.252,-0.966],[-0.636,-1.37],[0,-0.213],[-0.033,0.203],[0.041,-2.622],[1.871,-1.845],[3.886,0],[0.011,0.022],[0.003,0.006],[0.283,0.552],[-0.002,0.836],[-0.417,0.725],[-0.716,0.425],[0,0],[-0.073,0.003],[-0.236,-0.165],[0,-0.191]],"v":[[1.84,1.691],[1.9,1.628],[2.509,0.073],[2.508,-0.023],[2.508,-0.022],[2.508,-0.021],[2.507,-0.048],[2.504,-0.134],[2.011,-1.479],[1.127,-2.223],[0.004,-2.495],[-1.181,-2.196],[-1.181,-2.196],[-1.183,-2.195],[-1.21,-2.182],[-2.142,-1.29],[-2.493,-0.049],[4.423,5.757],[7.228,5.252],[8.051,4.885],[8.251,4.849],[8.43,4.943],[8.514,5.128],[8.467,5.325],[3.264,9.45],[3.162,9.484],[2.478,9.66],[3.171,9.487],[-3.935,9.19],[-9.045,4.245],[-9.991,-0.13],[-9.945,-0.774],[-10,-0.156],[-7.022,-7.122],[-0.001,-10.001],[9.114,-4.9],[9.149,-4.832],[9.157,-4.815],[10,-1.47],[9.361,0.912],[7.622,2.661],[5.258,3.314],[5.256,3.314],[2.011,2.635],[1.642,2.071]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":40,"s":[{"i":[[-0.082,0.078],[-0.014,0.019],[0,0.42],[0.001,0.035],[0,0],[0,0],[0,0.009],[0.002,0.03],[0.293,0.391],[0.348,0.176],[0.39,0.003],[0.247,-0.117],[0,0],[0,0],[0.009,-0.004],[0.226,-0.375],[0.005,-0.438],[-3.279,0.493],[-0.897,0.336],[-0.267,0.139],[-0.069,-0.011],[-0.048,-0.051],[-0.007,-0.069],[0.037,-0.059],[2.152,-0.744],[0,0],[0.253,-0.054],[-0.228,0.072],[2.252,0.966],[1.04,2.219],[-0.014,1.51],[-0.03,0.216],[0.003,-0.209],[-1.868,1.842],[-2.628,0],[-1.655,-3.238],[-0.012,-0.024],[-0.003,-0.006],[0.019,-1.66],[0.417,-0.725],[0.722,-0.422],[0.832,-0.002],[0.001,0],[1.105,0.768],[0,0.204]],"o":[[0.024,-0.023],[0.395,-0.514],[0,-0.029],[0,0],[0,0],[0,-0.009],[-0.001,-0.027],[-0.029,-0.486],[-0.233,-0.313],[-0.348,-0.176],[-0.533,-0.01],[0,0],[0,0],[-0.009,0.004],[-0.384,0.209],[-0.226,0.375],[0,3.263],[1.152,-0.173],[0.282,-0.106],[0.061,-0.034],[0.069,0.011],[0.048,0.051],[0.007,0.069],[-1.215,1.926],[0,0],[-0.193,0.061],[0.234,-0.044],[-2.325,0.775],[-2.252,-0.966],[-0.636,-1.37],[0,-0.213],[-0.033,0.203],[0.041,-2.622],[1.871,-1.845],[3.886,0],[0.011,0.022],[0.003,0.006],[0.283,0.552],[-0.002,0.836],[-0.417,0.725],[-0.716,0.425],[0,0],[-0.073,0.003],[-0.236,-0.165],[0,-0.191]],"v":[[1.319,2.836],[1.379,2.774],[2.509,0.073],[2.508,-0.023],[2.508,-0.022],[2.508,-0.021],[2.507,-0.048],[2.504,-0.134],[2.011,-1.479],[1.127,-2.223],[0.004,-2.495],[-1.181,-2.196],[-1.181,-2.196],[-1.183,-2.195],[-1.21,-2.182],[-2.142,-1.29],[-2.493,-0.049],[4.423,5.757],[6.189,5.293],[7.114,4.78],[7.313,4.745],[7.493,4.839],[7.577,5.024],[7.529,5.221],[3.264,9.45],[3.162,9.484],[2.478,9.66],[3.171,9.487],[-3.935,9.19],[-9.045,4.245],[-9.991,-0.13],[-9.945,-0.774],[-10,-0.156],[-7.022,-7.122],[-0.001,-10.001],[9.114,-4.9],[9.149,-4.832],[9.157,-4.815],[10,-1.47],[8.84,2.058],[7.102,3.807],[4.737,4.459],[4.735,4.459],[1.49,3.781],[1.121,3.216]],"c":true}]},{"t":50,"s":[{"i":[[-0.082,0.078],[-0.014,0.019],[0,0.42],[0.001,0.035],[0,0],[0,0],[0,0.009],[0.002,0.03],[0.293,0.391],[0.348,0.176],[0.39,0.003],[0.247,-0.117],[0,0],[0,0],[0.009,-0.004],[0.226,-0.375],[0.005,-0.438],[-3.697,0],[-0.897,0.336],[-0.267,0.139],[-0.069,-0.011],[-0.048,-0.051],[-0.007,-0.069],[0.037,-0.059],[2.152,-0.744],[0,0],[0.253,-0.054],[-0.228,0.072],[2.252,0.966],[1.04,2.219],[-0.014,1.51],[-0.03,0.216],[0.003,-0.209],[-1.868,1.842],[-2.628,0],[-1.655,-3.238],[-0.012,-0.024],[-0.003,-0.006],[0.019,-1.66],[0.417,-0.725],[0.722,-0.422],[0.832,-0.002],[0.001,0],[1.105,0.768],[0,0.204]],"o":[[0.024,-0.023],[0.395,-0.514],[0,-0.029],[0,0],[0,0],[0,-0.009],[-0.001,-0.027],[-0.029,-0.486],[-0.233,-0.313],[-0.348,-0.176],[-0.533,-0.01],[0,0],[0,0],[-0.009,0.004],[-0.384,0.209],[-0.226,0.375],[0,3.263],[0.958,0.002],[0.282,-0.106],[0.061,-0.034],[0.069,0.011],[0.048,0.051],[0.007,0.069],[-1.215,1.926],[0,0],[-0.193,0.061],[0.234,-0.044],[-2.325,0.775],[-2.252,-0.966],[-0.636,-1.37],[0,-0.213],[-0.033,0.203],[0.041,-2.622],[1.871,-1.845],[3.886,0],[0.011,0.022],[0.003,0.006],[0.283,0.552],[-0.002,0.836],[-0.417,0.725],[-0.716,0.425],[0,0],[-0.073,0.003],[-0.236,-0.165],[0,-0.191]],"v":[[1.84,1.691],[1.9,1.628],[2.509,0.073],[2.508,-0.023],[2.508,-0.022],[2.508,-0.021],[2.507,-0.048],[2.504,-0.134],[2.011,-1.479],[1.127,-2.223],[0.004,-2.495],[-1.181,-2.196],[-1.181,-2.196],[-1.183,-2.195],[-1.21,-2.182],[-2.142,-1.29],[-2.493,-0.049],[4.423,5.757],[7.228,5.252],[8.051,4.885],[8.251,4.849],[8.43,4.943],[8.514,5.128],[8.467,5.325],[3.264,9.45],[3.162,9.484],[2.478,9.66],[3.171,9.487],[-3.935,9.19],[-9.045,4.245],[-9.991,-0.13],[-9.945,-0.774],[-10,-0.156],[-7.022,-7.122],[-0.001,-10.001],[9.114,-4.9],[9.149,-4.832],[9.157,-4.815],[10,-1.47],[9.361,0.912],[7.622,2.661],[5.258,3.314],[5.256,3.314],[2.011,2.635],[1.642,2.071]],"c":true}]}],"ix":2},"nm":"Контур 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0.005]],"o":[[0,-0.005],[0,0]],"v":[[-10,-0.142],[-10,-0.156]],"c":false},"ix":2},"nm":"Контур 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Объединить контуры 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Заливка 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"Union","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":-60,"bm":0},{"ddd":0,"ind":2,"ty":3,"nm":"Ellipse 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":40,"s":[1090]},{"t":50,"s":[1080]}],"ix":10},"p":{"a":0,"k":[15,15,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[16.667,16.667,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":20,"s":[11.667,11.667,100]},{"t":40,"s":[16.667,16.667,100]}],"ix":6,"l":2}},"ao":0,"ip":0,"op":120,"st":-60,"bm":0}],"markers":[]}
\ No newline at end of file
diff --git a/submodules/TelegramUI/Resources/Fonts/SFCompactRounded-Semibold.otf b/submodules/TelegramUI/Resources/Fonts/SFCompactRounded-Semibold.otf
deleted file mode 100644
index 100e58bff7..0000000000
Binary files a/submodules/TelegramUI/Resources/Fonts/SFCompactRounded-Semibold.otf and /dev/null differ
diff --git a/submodules/TelegramUI/Resources/Fonts/latinmodern-math.otf b/submodules/TelegramUI/Resources/Fonts/latinmodern-math.otf
new file mode 100644
index 0000000000..a7347598ae
Binary files /dev/null and b/submodules/TelegramUI/Resources/Fonts/latinmodern-math.otf differ
diff --git a/submodules/TelegramUI/Resources/Fonts/latinmodern-math.plist b/submodules/TelegramUI/Resources/Fonts/latinmodern-math.plist
new file mode 100755
index 0000000000..c9240d8b76
Binary files /dev/null and b/submodules/TelegramUI/Resources/Fonts/latinmodern-math.plist differ
diff --git a/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift b/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift
index a6ce9ab27f..1d59b973cb 100644
--- a/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift
+++ b/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift
@@ -158,10 +158,8 @@ private func chatForwardOptions(selfController: ChatControllerImpl, sourceView:
}
var isDice = false
- var isMusic = false
for media in message.media {
if let media = media as? TelegramMediaFile, media.isMusic {
- isMusic = true
if !message.text.isEmpty {
hasCaptions = true
}
@@ -175,7 +173,7 @@ private func chatForwardOptions(selfController: ChatControllerImpl, sourceView:
hasPaid = true
}
}
- if !isDice && !isMusic {
+ if !isDice {
hasOther = true
}
}
diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift
index c3fbab0819..5b62ef7ea5 100644
--- a/submodules/TelegramUI/Sources/ChatController.swift
+++ b/submodules/TelegramUI/Sources/ChatController.swift
@@ -10344,11 +10344,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
})
}
- func presentTimerPicker(style: ChatTimerScreenStyle = .default, selectedTime: Int32? = nil, dismissByTapOutside: Bool = true, completion: @escaping (Int32) -> Void) {
+ func presentTimerPicker(style: ChatTimerScreenStyle = .default, selectedTime: Int32? = nil, completion: @escaping (Int32) -> Void) {
guard case .peer = self.chatLocation else {
return
}
- let controller = ChatTimerScreen(context: self.context, updatedPresentationData: self.updatedPresentationData, style: style, currentTime: selectedTime, dismissByTapOutside: dismissByTapOutside, completion: { time in
+ let controller = ChatTimerScreen(context: self.context, updatedPresentationData: self.updatedPresentationData, style: style, currentTime: selectedTime, completion: { time in
completion(time)
})
self.chatDisplayNode.dismissInput()
@@ -10414,7 +10414,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return
}
- let controller = ChatTimerScreen(context: self.context, updatedPresentationData: self.updatedPresentationData, style: .default, mode: .autoremove, currentTime: self.presentationInterfaceState.autoremoveTimeout, dismissByTapOutside: true, completion: { [weak self] value in
+ let controller = ChatTimerScreen(context: self.context, updatedPresentationData: self.updatedPresentationData, style: .default, mode: .autoremove, currentTime: self.presentationInterfaceState.autoremoveTimeout, completion: { [weak self] value in
guard let strongSelf = self else {
return
}
diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift
index a36e3ea1f1..089a3a00d6 100644
--- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift
+++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift
@@ -373,6 +373,8 @@ private func extractAssociatedData(
var automaticMediaDownloadPeerType: MediaAutoDownloadPeerType = .channel
var contactsPeerIds: Set = Set()
var channelDiscussionGroup: ChatMessageItemAssociatedData.ChannelDiscussionGroupStatus = .unknown
+ var isParticipant = false
+ var invitedOn: Int32?
if case let .peer(peerId) = chatLocation {
automaticDownloadPeerId = peerId
@@ -396,11 +398,15 @@ private func extractAssociatedData(
} else if peerId.namespace == Namespaces.Peer.CloudChannel {
for entry in view.additionalData {
if case let .peer(_, value) = entry {
- if let channel = value as? TelegramChannel, case .group = channel.info {
- automaticMediaDownloadPeerType = .group
+ if let channel = value as? TelegramChannel {
+ if case .group = channel.info {
+ automaticMediaDownloadPeerType = .group
+ }
+ isParticipant = channel.participationStatus == .member
}
} else if case let .cachedPeerData(dataPeerId, cachedData) = entry, dataPeerId == peerId {
if let cachedData = cachedData as? CachedChannelData {
+ invitedOn = cachedData.invitedOn
switch cachedData.linkedDiscussionPeerId {
case let .known(value):
channelDiscussionGroup = .known(value)
@@ -422,7 +428,7 @@ private func extractAssociatedData(
automaticDownloadPeerId = message.peerId
}
- return ChatMessageItemAssociatedData(automaticDownloadPeerType: automaticMediaDownloadPeerType, automaticDownloadPeerId: automaticDownloadPeerId, automaticDownloadNetworkType: automaticDownloadNetworkType, preferredStoryHighQuality: preferredStoryHighQuality, isRecentActions: false, subject: subject, contactsPeerIds: contactsPeerIds, channelDiscussionGroup: channelDiscussionGroup, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, currentlyPlayingMessageId: currentlyPlayingMessageId, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, availableMessageEffects: availableMessageEffects, savedMessageTags: savedMessageTags, defaultReaction: defaultReaction, areStarReactionsEnabled: areStarReactionsEnabled, isPremium: isPremium, accountPeer: accountPeer, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, topicAuthorId: topicAuthorId, hasBots: hasBots, translateToLanguage: translateToLanguage, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels, audioTranscriptionTrial: audioTranscriptionTrial, chatThemes: chatThemes, deviceContactsNumbers: deviceContactsNumbers, isInline: isInline, showSensitiveContent: showSensitiveContent, isSuspiciousPeer: isSuspiciousPeer, accountCountry: accountCountry)
+ return ChatMessageItemAssociatedData(automaticDownloadPeerType: automaticMediaDownloadPeerType, automaticDownloadPeerId: automaticDownloadPeerId, automaticDownloadNetworkType: automaticDownloadNetworkType, preferredStoryHighQuality: preferredStoryHighQuality, isRecentActions: false, subject: subject, contactsPeerIds: contactsPeerIds, channelDiscussionGroup: channelDiscussionGroup, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, currentlyPlayingMessageId: currentlyPlayingMessageId, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, availableMessageEffects: availableMessageEffects, savedMessageTags: savedMessageTags, defaultReaction: defaultReaction, areStarReactionsEnabled: areStarReactionsEnabled, isPremium: isPremium, accountPeer: accountPeer, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, topicAuthorId: topicAuthorId, hasBots: hasBots, translateToLanguage: translateToLanguage, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels, audioTranscriptionTrial: audioTranscriptionTrial, chatThemes: chatThemes, deviceContactsNumbers: deviceContactsNumbers, isInline: isInline, showSensitiveContent: showSensitiveContent, isSuspiciousPeer: isSuspiciousPeer, accountCountry: accountCountry, isParticipant: isParticipant, invitedOn: invitedOn)
}
private extension ChatHistoryLocationInput {
diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift
index a40d494a9c..2cdd37cd44 100644
--- a/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift
+++ b/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift
@@ -230,6 +230,9 @@ func textInputAccessoryPanel(
previousTapTimestamp = CFAbsoluteTimeGetCurrent()
interfaceInteraction?.presentReplyOptions(sourceView)
},
+ longPressAction: { _ in
+ interfaceInteraction?.navigateToMessage(replyMessageSubject.messageId, false, true, ChatLoadingMessageSubject.generic)
+ },
dismiss: { _ in
interfaceInteraction?.setupReplyMessage(nil, nil, { _, f in f() })
}
diff --git a/submodules/TelegramUI/Sources/CreateGroupController.swift b/submodules/TelegramUI/Sources/CreateGroupController.swift
index f0ce6b6cc2..14ecb0c14b 100644
--- a/submodules/TelegramUI/Sources/CreateGroupController.swift
+++ b/submodules/TelegramUI/Sources/CreateGroupController.swift
@@ -1158,7 +1158,7 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId]
}, action: { _, f in
f(.default)
- let controller = ChatTimerScreen(context: context, updatedPresentationData: nil, style: .default, mode: .autoremove, currentTime: currentValue == 0 ? nil : currentValue, dismissByTapOutside: true, completion: { value in
+ let controller = ChatTimerScreen(context: context, updatedPresentationData: nil, style: .default, mode: .autoremove, currentTime: currentValue == 0 ? nil : currentValue, completion: { value in
applyValue(value)
})
endEditingImpl?()
diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift
index 49f8231c2a..5d78767dcb 100644
--- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift
+++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift
@@ -307,11 +307,8 @@ func openResolvedUrlImpl(
dismissInput()
navigationController?.pushViewController(controller)
case let .gameStart(botPeerId, game):
- let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyManageable, .excludeDisabled, .excludeRecent, .doNotSearchMessages], hasContactSelector: false, title: presentationData.strings.Bot_AddToChat_Title, selectForumThreads: true))
- controller.peerSelected = { [weak controller] peer, _ in
- let _ = peer.id
- let _ = botPeerId
- let _ = game
+ let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyWriteable, .excludeDisabled, .doNotSearchMessages], hasContactSelector: false, title: presentationData.strings.ShareMenu_SelectChats, selectForumThreads: true))
+ controller.peerSelected = { [weak controller] peer, threadId in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let text: String
if case .user = peer {
@@ -321,6 +318,20 @@ func openResolvedUrlImpl(
}
let alertController = textAlertController(context: context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.RequestPeer_SelectionConfirmationSend, action: {
+ controller?.inProgress = true
+ let _ = (context.engine.messages.sendBotGame(botPeerId: botPeerId, game: game, to: peer.id, threadId: threadId)
+ |> deliverOnMainQueue).startStandalone(error: { _ in
+ controller?.inProgress = false
+ let presentationData = context.sharedContext.currentPresentationData.with { $0 }
+ present(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
+ })
+ if let navigationController {
+ if let threadId {
+ let _ = context.sharedContext.navigateToForumThread(context: context, peerId: peer.id, threadId: threadId, messageId: nil, navigationController: navigationController, activateInput: nil, scrollToEndIfExists: true, keepStack: .always, animated: true).startStandalone()
+ } else {
+ context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), keepStack: .always, useExisting: true, scrollToEndIfExists: true))
+ }
+ }
controller?.dismiss()
}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
})])
diff --git a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift
index 4359fbe72b..bc0e2e3aba 100644
--- a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift
+++ b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift
@@ -368,7 +368,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white)
let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .natural)
self.textNode.attributedText = attributedText
- self.textNode.maximumNumberOfLines = 2
+ self.textNode.maximumNumberOfLines = 5
displayUndo = false
self.originalRemainingSeconds = 5
case let .importedMessage(text):
diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift
index 2da05a1503..a72f287f7a 100644
--- a/submodules/WebUI/Sources/WebAppController.swift
+++ b/submodules/WebUI/Sources/WebAppController.swift
@@ -516,6 +516,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
return
}
#endif*/
+ self.webView?.bindTrustedOrigin(from: url)
self.webView?.load(URLRequest(url: url))
}
@@ -1092,7 +1093,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
guard let controller = self.controller else {
return
}
- guard message.frameInfo.isMainFrame else {
+ guard self.webView?.isTrustedMainFrameMessage(message) == true else {
return
}
guard let body = message.body as? [String: Any] else {
diff --git a/submodules/WebUI/Sources/WebAppWebView.swift b/submodules/WebUI/Sources/WebAppWebView.swift
index a7fb70770c..f722ace6e7 100644
--- a/submodules/WebUI/Sources/WebAppWebView.swift
+++ b/submodules/WebUI/Sources/WebAppWebView.swift
@@ -36,11 +36,27 @@ private class WebViewTouchGestureRecognizer: UITapGestureRecognizer {
}
}
-private let eventProxySource = "var TelegramWebviewProxyProto = function() {}; " +
- "TelegramWebviewProxyProto.prototype.postEvent = function(eventName, eventData) { " +
- "window.webkit.messageHandlers.performAction.postMessage({'eventName': eventName, 'eventData': eventData}); " +
- "}; " +
-"var TelegramWebviewProxy = new TelegramWebviewProxyProto();"
+private func jsStringLiteral(_ value: String) -> String {
+ if let data = try? JSONSerialization.data(withJSONObject: [value], options: []), let string = String(data: data, encoding: .utf8), string.hasPrefix("["), string.hasSuffix("]") {
+ return String(string.dropFirst().dropLast())
+ }
+ return "\"\""
+}
+
+private func eventProxySource(trustedOrigin: String) -> String {
+ return """
+ (function() {
+ if (window.location.origin !== \(jsStringLiteral(trustedOrigin))) {
+ return;
+ }
+ var TelegramWebviewProxyProto = function() {};
+ TelegramWebviewProxyProto.prototype.postEvent = function(eventName, eventData) {
+ window.webkit.messageHandlers.performAction.postMessage({'eventName': eventName, 'eventData': eventData});
+ };
+ window.TelegramWebviewProxy = new TelegramWebviewProxyProto();
+ })();
+ """
+}
private let selectionSource = "var css = '*{-webkit-touch-callout:none;} :not(input):not(textarea):not([\"contenteditable\"=\"true\"]){-webkit-user-select:none;}';"
+ " var head = document.head || document.getElementsByTagName('head')[0];"
@@ -91,6 +107,7 @@ function tgBrowserDisconnectObserver() {
final class WebAppWebView: WKWebView {
var handleScriptMessage: (WKScriptMessage) -> Void = { _ in }
+ private(set) var trustedOrigin: String?
var customInsets: UIEdgeInsets = .zero {
didSet {
@@ -134,8 +151,6 @@ final class WebAppWebView: WKWebView {
let contentController = WKUserContentController()
var handleScriptMessageImpl: ((WKScriptMessage) -> Void)?
- let eventProxyScript = WKUserScript(source: eventProxySource, injectionTime: .atDocumentStart, forMainFrameOnly: true)
- contentController.addUserScript(eventProxyScript)
contentController.add(WeakGameScriptMessageHandler { message in
handleScriptMessageImpl?(message)
}, name: "performAction")
@@ -187,6 +202,36 @@ final class WebAppWebView: WKWebView {
print()
}
+ func bindTrustedOrigin(from url: URL) {
+ guard self.trustedOrigin == nil else {
+ return
+ }
+ guard let origin = normalizedOrigin(url: url) else {
+ return
+ }
+
+ self.trustedOrigin = origin
+
+ let eventProxyScript = WKUserScript(source: eventProxySource(trustedOrigin: origin), injectionTime: .atDocumentStart, forMainFrameOnly: true)
+ self.configuration.userContentController.addUserScript(eventProxyScript)
+ }
+
+ func isTrustedMainFrameMessage(_ message: WKScriptMessage) -> Bool {
+ guard message.frameInfo.isMainFrame else {
+ return false
+ }
+ guard let trustedOrigin = self.trustedOrigin else {
+ return false
+ }
+ guard message.frameInfo.securityOriginString == trustedOrigin else {
+ return false
+ }
+ if let currentOrigin = self.origin, currentOrigin != trustedOrigin {
+ return false
+ }
+ return true
+ }
+
override func didMoveToSuperview() {
super.didMoveToSuperview()
@@ -223,6 +268,9 @@ final class WebAppWebView: WKWebView {
}
func sendEvent(name: String, data: String?) {
+ guard let trustedOrigin = self.trustedOrigin, self.origin == trustedOrigin else {
+ return
+ }
let script = "window.TelegramGameProxy && window.TelegramGameProxy.receiveEvent && window.TelegramGameProxy.receiveEvent(\"\(name)\", \(data ?? "null"))"
self.evaluateJavaScript(script, completionHandler: { _, _ in
})
@@ -279,29 +327,39 @@ final class WebAppWebView: WKWebView {
}
var origin: String? {
- guard let url = self.url, let scheme = url.scheme, let host = url.host else {
+ guard let url = self.url else {
return nil
}
- let port = url.port
- var origin = "\(scheme)://\(host)"
- if let port {
- origin += ":\(port)"
- }
- return origin
+ return normalizedOrigin(url: url)
}
}
extension WKFrameInfo {
var securityOriginString: String {
let securityOrigin = self.securityOrigin
- var origin = ""
- origin.append(securityOrigin.protocol)
- origin.append("://")
- origin.append(securityOrigin.host)
- if securityOrigin.port != 0 {
- origin.append(":")
- origin.append("\(securityOrigin.port)")
- }
- return origin
+ return normalizedOrigin(scheme: securityOrigin.protocol, host: securityOrigin.host, port: securityOrigin.port == 0 ? nil : securityOrigin.port) ?? ""
+ }
+}
+
+private func normalizedOrigin(url: URL) -> String? {
+ return normalizedOrigin(scheme: url.scheme, host: url.host, port: url.port)
+}
+
+private func normalizedOrigin(scheme: String?, host: String?, port: Int?) -> String? {
+ guard let scheme = scheme?.lowercased(), !scheme.isEmpty, let host = host?.lowercased(), !host.isEmpty else {
+ return nil
+ }
+
+ let includePort: Bool
+ if let port {
+ includePort = !(scheme == "http" && port == 80) && !(scheme == "https" && port == 443)
+ } else {
+ includePort = false
+ }
+
+ if includePort, let port {
+ return "\(scheme)://\(host):\(port)"
+ } else {
+ return "\(scheme)://\(host)"
}
}
diff --git a/third-party/SwiftMath/BUILD b/third-party/SwiftMath/BUILD
new file mode 100644
index 0000000000..66634a8c41
--- /dev/null
+++ b/third-party/SwiftMath/BUILD
@@ -0,0 +1,18 @@
+load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
+
+swift_library(
+ name = "SwiftMath",
+ module_name = "SwiftMath",
+ srcs = glob([
+ "Sources/**/*.swift",
+ ]),
+ copts = [
+ "-warnings-as-errors",
+ ],
+ deps = [
+ "//submodules/AppBundle",
+ ],
+ visibility = [
+ "//visibility:public",
+ ],
+)
diff --git a/third-party/SwiftMath/LICENSE b/third-party/SwiftMath/LICENSE
new file mode 100755
index 0000000000..dddb1a7a28
--- /dev/null
+++ b/third-party/SwiftMath/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 Computer Inspirations
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/third-party/SwiftMath/README.md b/third-party/SwiftMath/README.md
new file mode 100644
index 0000000000..883c9038f5
--- /dev/null
+++ b/third-party/SwiftMath/README.md
@@ -0,0 +1,714 @@
+# SwiftMath
+
+`SwiftMath` provides a full Swift implementation of [iosMath](https://travis-ci.org/kostub/iosMath)
+for displaying beautifully rendered math equations in iOS and MacOS applications. It typesets formulae written
+using LaTeX in a `UILabel` equivalent class. It uses the same typesetting rules as LaTeX and
+so the equations are rendered exactly as LaTeX would render them.
+
+Please also check out [SwiftMathDemo](https://github.com/mgriebling/SwiftMathDemo.git) for examples of how to use `SwiftMath`
+from SwiftUI.
+
+`SwiftMath` is similar to [MathJax](https://www.mathjax.org) or
+[KaTeX](https://github.com/Khan/KaTeX) for the web but for native iOS or MacOS
+applications without having to use a `UIWebView` and Javascript. More
+importantly, it is significantly faster than using a `UIWebView`.
+
+`SwiftMath` is a Swift translation of the latest `iosMath` v0.9.5 release but includes bug fixes
+and enhancements like a new \lbar (lambda bar) character and cyrillic alphabet support.
+The original `iosMath` test suites have also been translated to Swift and run without errors.
+Note: Error test conditions are ignored to avoid tagging everything with silly `throw`s.
+Please let me know of any bugs or bug fixes that you find.
+
+`SwiftMath` prepackages everything needed for direct access via the Swift Package Manager.
+
+## Examples
+Here are screenshots of some formulae that were rendered with this library:
+
+```LaTeX
+x = \frac{-b \pm \sqrt{b^2-4ac}}{2a}
+```
+
+
+
+
+```LaTeX
+f(x) = \int\limits_{-\infty}^\infty\!\hat f(\xi)\,e^{2 \pi i \xi x}\,\mathrm{d}\xi
+```
+
+
+
+
+```LaTeX
+\frac{1}{n}\sum_{i=1}^{n}x_i \geq \sqrt[n]{\prod_{i=1}^{n}x_i}
+```
+
+
+
+
+```LaTex
+\frac{1}{\left(\sqrt{\phi \sqrt{5}}-\phi\\right) e^{\frac25 \pi}}
+= 1+\frac{e^{-2\pi}} {1 +\frac{e^{-4\pi}} {1+\frac{e^{-6\pi}} {1+\frac{e^{-8\pi}} {1+\cdots} } } }
+```
+
+
+
+
+More examples are included in [EXAMPLES](EXAMPLES.md)
+
+## Fonts
+Here are previews of the included fonts:
+
+
+
+
+## Requirements
+`SwiftMath` works on iOS 11+ or MacOS 12+. It depends
+on the following Apple frameworks:
+
+* Foundation.framework
+* CoreGraphics.framework
+* QuartzCore.framework
+* CoreText.framework
+
+Additionally for iOS it requires:
+* UIKit.framework
+
+Additionally for MacOS it requires:
+* AppKit.framework
+
+## Installation
+
+### Swift Package
+
+`SwiftMath` is available from [SwiftMath](https://github.com/mgriebling/SwiftMath.git).
+To use it in your code, just add the https://github.com/mgriebling/SwiftMath.git path to
+XCode's package manager.
+
+## Usage
+
+The library provides a class `MTMathUILabel` which is a `UIView` that
+supports rendering math equations. To display an equation simply create
+an `MTMathUILabel` as follows:
+
+```swift
+
+import SwiftMath
+
+let label = MTMathUILabel()
+label.latex = "x = \\frac{-b \\pm \\sqrt{b^2-4ac}}{2a}"
+
+```
+Adding `MTMathUILabel` as a sub-view of your `UIView` will render the
+quadratic formula example shown above.
+
+The following code creates a SwiftUI component called `MathView` encapsulating the MTMathUILabel:
+
+```swift
+import SwiftUI
+import SwiftMath
+
+struct MathView: UIViewRepresentable {
+ var equation: String
+ var font: MathFont = .latinModernFont
+ var textAlignment: MTTextAlignment = .center
+ var fontSize: CGFloat = 30
+ var labelMode: MTMathUILabelMode = .text
+ var insets: MTEdgeInsets = MTEdgeInsets()
+
+ func makeUIView(context: Context) -> MTMathUILabel {
+ let view = MTMathUILabel()
+ view.setContentHuggingPriority(.required, for: .vertical)
+ view.setContentCompressionResistancePriority(.required, for: .vertical)
+ return view
+ }
+
+ func updateUIView(_ view: MTMathUILabel, context: Context) {
+ view.latex = equation
+ let font = MTFontManager().font(withName: font.rawValue, size: fontSize)
+ font?.fallbackFont = UIFont.systemFont(ofSize: fontSize)
+ view.font = font
+ view.textAlignment = textAlignment
+ view.labelMode = labelMode
+ view.textColor = MTColor(Color.primary)
+ view.contentInsets = insets
+ view.invalidateIntrinsicContentSize()
+ }
+
+ func sizeThatFits(_ proposal: ProposedViewSize, uiView: MTMathUILabel, context: Context) -> CGSize? {
+ // Enable line wrapping by passing proposed width to the label
+ if let width = proposal.width, width.isFinite, width > 0 {
+ uiView.preferredMaxLayoutWidth = width
+ let size = uiView.sizeThatFits(CGSize(width: width, height: .greatestFiniteMagnitude))
+ return size
+ }
+ return nil
+ }
+}
+```
+
+For code that works with SwiftUI running natively under MacOS use the following:
+
+```swift
+import SwiftUI
+import SwiftMath
+
+struct MathView: NSViewRepresentable {
+ var equation: String
+ var font: MathFont = .latinModernFont
+ var textAlignment: MTTextAlignment = .center
+ var fontSize: CGFloat = 30
+ var labelMode: MTMathUILabelMode = .text
+ var insets: MTEdgeInsets = MTEdgeInsets()
+
+ func makeNSView(context: Context) -> MTMathUILabel {
+ let view = MTMathUILabel()
+ view.setContentHuggingPriority(.required, for: .vertical)
+ view.setContentCompressionResistancePriority(.required, for: .vertical)
+ return view
+ }
+
+ func updateNSView(_ view: MTMathUILabel, context: Context) {
+ view.latex = equation
+ let font = MTFontManager().font(withName: font.rawValue, size: fontSize)
+ font?.fallbackFont = NSFont.systemFont(ofSize: fontSize)
+ view.font = font
+ view.textAlignment = textAlignment
+ view.labelMode = labelMode
+ view.textColor = MTColor(Color.primary)
+ view.contentInsets = insets
+ view.invalidateIntrinsicContentSize()
+ }
+
+ func sizeThatFits(_ proposal: ProposedViewSize, nsView: MTMathUILabel, context: Context) -> CGSize? {
+ // Enable line wrapping by passing proposed width to the label
+ if let width = proposal.width, width.isFinite, width > 0 {
+ nsView.preferredMaxLayoutWidth = width
+ let size = nsView.sizeThatFits(CGSize(width: width, height: .greatestFiniteMagnitude))
+ return size
+ }
+ return nil
+ }
+}
+```
+
+### Automatic Line Wrapping
+
+`SwiftMath` supports automatic line wrapping (multiline display) for mathematical content. The implementation uses **interatom line breaking** which breaks equations at atom boundaries (between mathematical elements) rather than within them, preserving the semantic structure of the mathematics.
+
+#### Using Line Wrapping with UIKit/AppKit
+
+For direct `MTMathUILabel` usage, set the `preferredMaxLayoutWidth` property:
+
+```swift
+let label = MTMathUILabel()
+label.latex = "\\text{Calculer le discriminant }\\Delta=b^{2}-4ac\\text{ avec }a=1\\text{, }b=-1\\text{, }c=-5"
+label.font = MTFontManager.fontManager.defaultFont
+
+// Enable line wrapping by setting a maximum width
+label.preferredMaxLayoutWidth = 235
+```
+
+You can also use `sizeThatFits` to calculate the size with a width constraint:
+
+```swift
+let constrainedSize = label.sizeThatFits(CGSize(width: 235, height: .greatestFiniteMagnitude))
+```
+
+#### Using Line Wrapping with SwiftUI
+
+The `MathView` examples above include `sizeThatFits()` which automatically enables line wrapping when SwiftUI proposes a width constraint. No additional configuration is needed:
+
+```swift
+VStack(alignment: .leading, spacing: 8) {
+ MathView(
+ equation: "\\text{Calculer le discriminant }\\Delta=b^{2}-4ac\\text{ avec }a=1\\text{, }b=-1\\text{, }c=-5",
+ fontSize: 17,
+ labelMode: .text
+ )
+}
+.frame(maxWidth: 235) // The equation will break across multiple lines
+```
+
+#### Line Wrapping Behavior and Capabilities
+
+SwiftMath implements **two complementary line breaking mechanisms**:
+
+##### 1. Interatom Line Breaking (Primary)
+Breaks equations **between atoms** (mathematical elements) when content exceeds the width constraint. This is the preferred method as it maintains semantic integrity.
+
+##### 2. Universal Line Breaking (Fallback)
+For very long text within single atoms, breaks at Unicode word boundaries using Core Text with number protection (prevents splitting numbers like "3.14").
+
+See `MULTILINE_IMPLEMENTATION_NOTES.md` for implementation details and recent changes.
+
+#### Fully Supported Cases
+
+These atom types work perfectly with interatom line breaking:
+
+**✅ Variables and ordinary text:**
+```swift
+label.latex = "a b c d e f g h i j k l m n o p"
+label.preferredMaxLayoutWidth = 150
+// Breaks between individual variables at natural boundaries
+```
+
+**✅ Binary operators (+, -, ×, ÷):**
+```swift
+label.latex = "a+b+c+d+e+f+g+h"
+label.preferredMaxLayoutWidth = 100
+// Breaks cleanly: "a+b+c+d+"
+// "e+f+g+h"
+```
+
+**✅ Relations (=, <, >, ≤, ≥, etc.):**
+```swift
+label.latex = "a=1, b=2, c=3, d=4, e=5"
+label.preferredMaxLayoutWidth = 120
+// Breaks after commas and operators
+```
+
+**✅ Mixed text and simple math:**
+```swift
+label.latex = "\\text{Calculer }\\Delta=b^{2}-4ac\\text{ avec }a=1\\text{, }b=-1"
+label.preferredMaxLayoutWidth = 200
+// Breaks between text and math atoms naturally
+```
+
+**✅ Punctuation (commas, periods):**
+```swift
+label.latex = "\\text{First, second, third, fourth, fifth}"
+label.preferredMaxLayoutWidth = 150
+// Breaks at commas and spaces
+```
+
+**✅ Brackets and parentheses (simple):**
+```swift
+label.latex = "(a+b)+(c+d)+(e+f)"
+label.preferredMaxLayoutWidth = 120
+// Breaks between parenthesized groups
+```
+
+**✅ Greek letters and symbols:**
+```swift
+label.latex = "\\alpha+\\beta+\\gamma+\\delta+\\epsilon+\\zeta"
+label.preferredMaxLayoutWidth = 150
+// Breaks between Greek letters
+```
+
+**✅ Fractions (NEW!):**
+```swift
+label.latex = "a+\\frac{1}{2}+b+\\frac{3}{4}+c"
+label.preferredMaxLayoutWidth = 150
+// Fractions stay inline if they fit, break to new line only when needed
+// Example: "a + ½ + b" stays on one line if it fits
+```
+
+**✅ Radicals/Square roots (NEW!):**
+```swift
+label.latex = "x+\\sqrt{2}+y+\\sqrt{3}+z"
+label.preferredMaxLayoutWidth = 150
+// Radicals stay inline if they fit, break to new line only when needed
+// Example: "x + √2 + y" stays on one line if it fits
+```
+
+**✅ Mixed fractions and radicals (NEW!):**
+```swift
+label.latex = "a+\\frac{1}{2}+\\sqrt{3}+b"
+label.preferredMaxLayoutWidth = 200
+// Breaks between complex mathematical elements
+```
+
+#### Limited Support Cases
+
+These cases work but with some constraints:
+
+**✅ Large operators (NEW!):**
+```swift
+label.latex = "a + \\sum_{i=1}^{n} x_i + \\int_{0}^{1} f(x)dx + b"
+label.preferredMaxLayoutWidth = 200
+// Large operators stay inline when they fit!
+// Includes height checking for operators with limits
+```
+
+**✅ Delimited expressions (NEW!):**
+```swift
+label.latex = "(a+b) + \\left(\\frac{c}{d}\\right) + e"
+label.preferredMaxLayoutWidth = 200
+// Delimiters stay inline when they fit!
+// Inner content respects width constraints and wraps naturally
+```
+
+**✅ Colored expressions (NEW!):**
+```swift
+label.latex = "a + \\color{red}{b + c + d} + e"
+label.preferredMaxLayoutWidth = 200
+// Colored sections stay inline when they fit!
+// Inner content respects width constraints and wraps properly
+```
+
+**✅ Matrices/tables (NEW!):**
+```swift
+label.latex = "A = \\begin{pmatrix} 1 & 2 \\end{pmatrix} + B"
+label.preferredMaxLayoutWidth = 200
+// Small matrices stay inline when they fit!
+```
+
+**✅ Atoms with superscripts/subscripts (IMPROVED!):**
+```swift
+label.latex = "a^{2}+b^{2}+c^{2}+d^{2}+e^{2}"
+label.preferredMaxLayoutWidth = 150
+// Now works with width-based breaking!
+// Scripted atoms participate in smart line breaking decisions
+```
+
+**✅ Math accents:**
+```swift
+label.latex = "\\hat{x} + \\tilde{y} + \\bar{z} + \\vec{w}"
+label.preferredMaxLayoutWidth = 150
+// Common accents (\hat, \tilde, \bar, \vec) work well
+// Vector arrows (\overrightarrow, etc.) supported with stretching
+```
+
+**⚠️ Very long single text atoms:**
+```swift
+label.latex = "\\text{This is an extremely long piece of text within a single text command}"
+label.preferredMaxLayoutWidth = 200
+// Uses Unicode word boundary breaking with Core Text
+// Protects numbers from being split (e.g., "3.14" stays together)
+```
+**Note**: Breaks within the text atom rather than between atoms, which is expected behavior for very long continuous text.
+
+#### Best Practices
+
+**DO:**
+- Use interatom breaking for simple equations with operators and relations
+- Use for mixed text and math where you want natural breaks
+- Use for long sequences of variables, numbers, and operators
+- Set appropriate `preferredMaxLayoutWidth` based on your layout needs
+
+**DON'T:**
+- Expect natural breaking in expressions with large operators (∑, ∫, etc. - not yet optimized)
+- Expect natural breaking in expressions with \left...\right delimiters (not yet optimized)
+- Use extremely narrow widths (less than ~80pt) which may cause poor breaks
+
+#### Examples
+
+**Excellent use case (discriminant formula):**
+```swift
+label.latex = "\\text{Calculer le discriminant }\\Delta=b^{2}-4ac\\text{ avec }a=1\\text{, }b=-1\\text{, }c=-5"
+label.preferredMaxLayoutWidth = 235
+// ✅ Breaks naturally at good points between atoms
+```
+
+**Good use case (simple arithmetic):**
+```swift
+label.latex = "5+10+15+20+25+30+35+40+45+50"
+label.preferredMaxLayoutWidth = 150
+// ✅ Breaks between operators cleanly
+```
+
+**Excellent use case (fractions inline - NEW!):**
+```swift
+label.latex = "a+\\frac{1}{2}+b+\\frac{3}{4}+c"
+label.preferredMaxLayoutWidth = 200
+// ✅ Fractions stay inline when they fit!
+// Breaks: "a + ½ + b" on line 1, "+ ¾ + c" on line 2
+```
+
+**Excellent use case (radicals inline - NEW!):**
+```swift
+label.latex = "x+\\sqrt{2}+y+\\sqrt{3}+z"
+label.preferredMaxLayoutWidth = 150
+// ✅ Radicals stay inline when they fit!
+// Example: "x + √2 + y" on line 1, "+ √3 + z" on line 2
+```
+
+**Alternative for complex expressions:**
+```swift
+// Instead of trying to break this:
+label.latex = "x = \\frac{-b \\pm \\sqrt{b^2-4ac}}{2a}"
+// Consider it as a single display equation without width constraint
+label.preferredMaxLayoutWidth = 0 // No breaking
+```
+
+#### Technical Details
+
+- **Line spacing**: Dynamic line height based on actual content (tall fractions get more space, regular content stays compact)
+- **Breaking algorithm**: Greedy with look-ahead and break quality scoring - prefers breaking after operators over other positions
+- **Width calculation**: Includes inter-element spacing according to TeX spacing rules
+- **Number protection**: Numbers in patterns like "3.14", "1,000", etc. are kept intact
+- **Supports locales**: English, French, Swiss number formats
+- **Advanced features**: Tokenization infrastructure with phase-based processing (preprocessing → tokenization → line fitting → display generation)
+
+For complete implementation details including recent improvements (dynamic line height, break quality scoring, early exit optimization), see [MULTILINE_IMPLEMENTATION_NOTES.md](MULTILINE_IMPLEMENTATION_NOTES.md).
+
+### Included Features
+This is a list of formula types that the library currently supports:
+
+* Simple algebraic equations
+* Fractions and continued fractions (including `\frac`, `\dfrac`, `\tfrac`, `\cfrac`)
+* Exponents and subscripts
+* Trigonometric formulae (including inverse hyperbolic: `\arcsinh`, `\arccosh`, etc.)
+* Square roots and n-th roots
+* Calculus symbols - limits, derivatives, integrals (including `\iint`, `\iiint`, `\iiiint`)
+* Big operators (e.g. product, sum)
+* Big delimiters (using `\left` and `\right`)
+* Manual delimiter sizing (`\big`, `\Big`, `\bigg`, `\Bigg` and variants)
+* Greek alphabet
+* Bold Greek symbols (`\boldsymbol`)
+* Combinatorics (`\binom`, `\choose` etc.)
+* Geometry symbols (e.g. angle, congruence etc.)
+* Ratios, proportions, percentages
+* Math spacing
+* Overline and underline
+* Math accents (including `\hat`, `\tilde`, `\bar`, `\vec`, `\dot`, `\ddot`, etc.)
+* Vector arrows (`\vec`, `\overrightarrow`, `\overleftarrow`, `\overleftrightarrow` with automatic stretching)
+* Wide accents (`\widehat`, `\widetilde`)
+* Operator names (`\operatorname{name}`)
+* Matrices (including `\smallmatrix` and starred variants like `pmatrix*` with alignment)
+* Multi-line subscripts and limits (`\substack`)
+* Equation alignment
+* Change bold, roman, caligraphic and other font styles (`\bf`, `\text`, etc.)
+* Style commands (`\displaystyle`, `\textstyle`)
+* Custom operators (`\operatorname`, `\operatorname*`)
+* Dirac notation (`\bra`, `\ket`, `\braket`)
+* Most commonly used math symbols
+* Colors for both text and background
+* **Inline and display math mode delimiters** (see below)
+* **Automatic line wrapping** (see Automatic Line Wrapping section)
+
+### LaTeX Math Delimiters
+
+`SwiftMath` now supports all standard LaTeX math delimiters for both inline and display modes. The parser automatically detects and handles these delimiters:
+
+#### Inline Math (Text Style)
+Use these delimiters for inline math within text, which renders more compactly:
+
+```swift
+// Dollar signs (TeX style)
+label.latex = "$E = mc^2$"
+
+// Parentheses (LaTeX style)
+label.latex = "\\(\\sum_{i=1}^{n} x_i\\)"
+
+// Cases environment in inline mode
+label.latex = "\\(\\begin{cases} x + y = 5 \\\\ 2x - y = 1 \\end{cases}\\)"
+```
+
+#### Display Math (Display Style)
+Use these delimiters for standalone equations with larger operators and limits:
+
+```swift
+// Double dollar signs (TeX style)
+label.latex = "$$\\int_{0}^{\\infty} e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}$$"
+
+// Square brackets (LaTeX style)
+label.latex = "\\[\\sum_{k=1}^{n} k^2 = \\frac{n(n+1)(2n+1)}{6}\\]"
+
+// Equation environment
+label.latex = "\\begin{equation} x^2 + y^2 = z^2 \\end{equation}"
+
+// Cases environment in display mode
+label.latex = "\\begin{cases} x + y = 5 \\\\ 2x - y = 1 \\end{cases}"
+```
+
+**Note:** The difference between inline and display modes:
+- **Inline mode** (`$...$` or `\(...\)`) renders compactly, suitable for math within text
+- **Display mode** (`$$...$$`, `\[...\]`, or environments) renders with larger operators and limits positioned above/below
+
+All delimiters are automatically stripped during parsing, and the math mode is set appropriately. No additional configuration is needed!
+
+#### Backward Compatibility
+Equations without explicit delimiters continue to work as before, defaulting to display mode:
+
+```swift
+label.latex = "x = \\frac{-b \\pm \\sqrt{b^2-4ac}}{2a}" // Works as always
+```
+
+#### Programmatic API
+For advanced use cases where you need to parse LaTeX and determine the detected style programmatically, use the `buildWithStyle` method:
+
+```swift
+// Parse LaTeX and get both the math list and detected style
+let (mathList, style) = MTMathListBuilder.buildWithStyle(fromString: "\\[x^2 + y^2 = z^2\\]")
+
+// style will be .display for \[...\] or $$...$$
+// style will be .text for \(...\) or $...$
+
+// Create a display with the detected style
+if let mathList = mathList {
+ let display = MTTypesetter.createLineForMathList(mathList, font: myFont, style: style)
+ // Use the display for rendering
+}
+```
+
+This is particularly useful when building custom renderers or when you need to respect the user's choice of delimiter style.
+
+Note: SwiftMath only supports the commands in LaTeX's math mode. There is
+also no language support for other than west European langugages and some
+Cyrillic characters. There would be two ways to support more languages:
+
+1) Find a math font compatible with `SwiftMath` that contains all the glyphs
+for that language.
+2) Add support to `SwiftMath` for standard Unicode fonts that contain all
+langauge glyphs.
+
+Of these two, the first is much easier. However, if you want a challenge,
+try to tackle the second option.
+
+### Example
+
+The [SwiftMathDemo](https://github.com/mgriebling/SwiftMathDemo) is a SwiftUI version
+of the Objective-C demo included in `iosMath` that uses `SwiftMath` as a Swift package dependency.
+
+### Advanced configuration
+
+`MTMathUILabel` supports some advanced configuration options:
+
+##### Math mode
+
+You can change the mode of the `MTMathUILabel` between Display Mode
+(equivalent to `$$` or `\[` in LaTeX) and Text Mode (equivalent to `$`
+or `\(` in LaTeX). The default style is Display. To switch to Text
+simply:
+
+```swift
+label.labelMode = .text
+```
+
+##### Text Alignment
+The default alignment of the equations is left. This can be changed to
+center or right as follows:
+
+```swift
+label.textAlignment = .center
+```
+
+##### Font size
+The default font-size is 30pt. You can change it as follows:
+
+```swift
+label.fontSize = 25
+```
+##### Font
+The default font is *Latin Modern Math*. This can be changed as:
+
+```swift
+label.font = MTFontManager.fontmanager.termesFont(withSize:20)
+```
+
+This project has 12 fonts bundled with it, but you can use any OTF math
+font. A python script is included that generates the `.plist` files
+required for an `.otf` font to work with `SwiftMath`. If you generate
+(and test) any other fonts please contribute them back to this project for
+others to benefit.
+
+
+
+Note: The `KpMath-Light`, `KpMath-Sans`, `Asana` fonts currently incorrectly
+render very large radicals. It appears that the font files do
+not properly define the offsets required to typeset these glyphs. If
+anyone can fix this, it would be greatly appreciated.
+
+##### Text Color
+The default color of the rendered equation is black. You can change
+it to any other color as follows:
+
+```swift
+label.textColor = .red
+```
+
+It is also possible to set different colors for different parts of the
+equation. Just access the `displayList` field and set the `textColor`
+of the underlying displays of which you want to change the color.
+
+##### Fallback Font for Unicode Text
+By default, math fonts only support a limited set of characters (Latin, Greek, common math symbols).
+To display other Unicode characters like Chinese, Japanese, Korean, emoji, or other scripts in `\text{}`
+commands, you can configure a fallback font:
+
+```swift
+let mathFont = MTFontManager().font(withName: MathFont.latinModernFont.rawValue, size: 30)
+
+// Set a fallback font for unsupported characters (defaults to nil)
+#if os(iOS) || os(visionOS)
+let systemFont = UIFont.systemFont(ofSize: 30)
+mathFont?.fallbackFont = CTFontCreateWithName(systemFont.fontName as CFString, 30, nil)
+#elseif os(macOS)
+let systemFont = NSFont.systemFont(ofSize: 30)
+mathFont?.fallbackFont = CTFontCreateWithName(systemFont.fontName as CFString, 30, nil)
+#endif
+
+label.font = mathFont
+label.latex = "\\text{Hello 世界 🌍}" // English, Chinese, and emoji
+```
+
+When the main math font doesn't contain a glyph for a character, the fallback font will be used automatically.
+This is particularly useful for:
+- Chinese text: `\text{中文}`
+- Japanese text: `\text{日本語}`
+- Korean text: `\text{한국어}`
+- Emoji: `\text{Math is fun! 🎉📐}`
+- Mixed scripts: `\text{Equation: 方程式}`
+
+**Note**: The fallback font only applies to characters within `\text{}` commands, not regular math mode.
+
+##### Custom Commands
+You can define your own commands that are not already predefined. This is
+similar to macros is LaTeX. To define your own command use:
+
+```swift
+MTMathAtomFactory.addLatexSymbol("lcm", value: MTMathAtomFactory.operator(withName: "lcm", limits: false))
+```
+
+This creates an `\lcm` command that can be used in the LaTeX.
+
+##### Content Insets
+The `MTMathUILabel` has `contentInsets` for finer control of placement of the
+equation in relation to the view.
+
+If you need to set it you can do as follows:
+
+```swift
+label.contentInsets = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 20)
+```
+
+##### Error handling
+
+If the LaTeX text given to `MTMathUILabel` is
+invalid or if it contains commands that aren't currently supported then
+an error message will be displayed instead of the label.
+
+This error can be programmatically retrieved as `label.error`. If you
+prefer not to display anything then set:
+
+```swift
+label.displayErrorInline = true
+```
+
+## Future Enhancements
+
+Note this is not a complete implementation of LaTeX math mode. There are
+some pieces that are missing and may be included in future updates:
+
+* `\middle` delimiter for use between `\left` and `\right`
+* Some fine spacing commands (`\:`, `\;`, `\!` - note that `\,` works)
+
+For a complete list of features and their implementation status, see [MISSING_FEATURES.md](MISSING_FEATURES.md).
+
+## License
+
+`SwiftMath` is available under the MIT license. See the [LICENSE](./LICENSE)
+file for more info.
+
+### Fonts
+This distribution contains the following fonts. These fonts are
+licensed as follows:
+* Latin Modern Math:
+ [GUST Font License](GUST-FONT-LICENSE.txt)
+* Tex Gyre Termes:
+ [GUST Font License](GUST-FONT-LICENSE.txt)
+* [XITS Math](https://github.com/khaledhosny/xits-math):
+ [Open Font License](OFL.txt)
+* [KpMath Light/KpMath Sans](http://scripts.sil.org/OFL):
+ [SIL Open Font License](OFL.txt)
diff --git a/third-party/SwiftMath/Sources/MathBundle/MTFontMathTableV2.swift b/third-party/SwiftMath/Sources/MathBundle/MTFontMathTableV2.swift
new file mode 100755
index 0000000000..beb2320aa0
--- /dev/null
+++ b/third-party/SwiftMath/Sources/MathBundle/MTFontMathTableV2.swift
@@ -0,0 +1,194 @@
+//
+// MTFontMathTableV2.swift
+//
+//
+// Created by Peter Tang on 15/9/2023.
+//
+
+import Foundation
+import CoreGraphics
+import CoreText
+
+internal class MTFontMathTableV2: MTFontMathTable {
+ private let mathFont: MathFont
+ private let fontSize: CGFloat
+ private let unitsPerEm: UInt
+ private let mTable: NSDictionary
+ init(mathFont: MathFont, size: CGFloat, unitsPerEm: UInt) {
+ self.mathFont = mathFont
+ self.fontSize = size
+ self.unitsPerEm = unitsPerEm
+ mTable = mathFont.rawMathTable()
+ super.init(withFont: mathFont.mtfont(size: fontSize), mathTable: mTable)
+ super._mathTable = nil
+ // disable all possible access to _mathTable in superclass!
+ }
+ override var _mathTable: NSDictionary? {
+ set { fatalError("\(#function) change to _mathTable \(mathFont.rawValue) not allowed.") }
+ get { mTable }
+ }
+ override var muUnit: CGFloat { fontSize/18 }
+
+ override func fontUnitsToPt(_ fontUnits:Int) -> CGFloat {
+ CGFloat(fontUnits) * fontSize / CGFloat(unitsPerEm)
+ }
+ override func constantFromTable(_ constName: String) -> CGFloat {
+ guard let consts = mTable[kConstants] as? NSDictionary, let val = consts[constName] as? NSNumber else {
+ return .zero
+ }
+ return fontUnitsToPt(val.intValue)
+ }
+ override func percentFromTable(_ percentName: String) -> CGFloat {
+ guard let consts = mTable[kConstants] as? NSDictionary, let val = consts[percentName] as? NSNumber else {
+ return .zero
+ }
+ return CGFloat(val.floatValue) / 100
+ }
+ /** Returns an Array of all the vertical variants of the glyph if any. If
+ there are no variants for the glyph, the array contains the given glyph. */
+ override func getVerticalVariantsForGlyph(_ glyph: CGGlyph) -> [NSNumber?] {
+ guard let variants = mTable[kVertVariants] as? NSDictionary else { return [] }
+ return self.getVariantsForGlyph(glyph, inDictionary: variants)
+ }
+ /** Returns an Array of all the horizontal variants of the glyph if any. If
+ there are no variants for the glyph, the array contains the given glyph. */
+ override func getHorizontalVariantsForGlyph(_ glyph: CGGlyph) -> [NSNumber?] {
+ guard let variants = mTable[kHorizVariants] as? NSDictionary else { return [] }
+ return self.getVariantsForGlyph(glyph, inDictionary:variants)
+ }
+ override func getVariantsForGlyph(_ glyph: CGGlyph, inDictionary variants: NSDictionary) -> [NSNumber?] {
+ let font = mathFont.mtfont(size: fontSize)
+ let glyphName = font.get(nameForGlyph: glyph)
+
+ var glyphArray = [NSNumber]()
+ let variantGlyphs = variants[glyphName] as? NSArray
+
+ guard let variantGlyphs = variantGlyphs, variantGlyphs.count != .zero else {
+ // There are no extra variants, so just add the current glyph to it.
+ let glyph = font.get(glyphWithName: glyphName)
+ glyphArray.append(NSNumber(value:glyph))
+ return glyphArray
+ }
+ for gvn in variantGlyphs {
+ if let glyphVariantName = gvn as? String {
+ let variantGlyph = font.get(glyphWithName: glyphVariantName)
+ glyphArray.append(NSNumber(value:variantGlyph))
+ }
+ }
+ return glyphArray
+ }
+ /** Returns a larger vertical variant of the given glyph if any.
+ If there is no larger version, this returns the current glyph.
+
+ - Parameter glyph: The glyph to find a larger variant for
+ - Parameter forDisplayStyle: If true, selects the largest appropriate variant for display style.
+ If false, selects the next larger variant (incremental sizing).
+ - Returns: A larger glyph variant, or the original glyph if no variants exist
+ */
+ override func getLargerGlyph(_ glyph: CGGlyph, forDisplayStyle: Bool = false) -> CGGlyph {
+ let font = mathFont.mtfont(size: fontSize)
+ let glyphName = font.get(nameForGlyph: glyph)
+
+ guard let variants = mTable[kVertVariants] as? NSDictionary,
+ let variantGlyphs = variants[glyphName] as? NSArray, variantGlyphs.count != .zero else {
+ // There are no extra variants, so just returnt the current glyph.
+ return glyph
+ }
+
+ if forDisplayStyle {
+ // For display style, select a large variant suitable for mathematical display mode
+ // Display integrals should be significantly larger (~2.2em) for visual prominence
+ let count = variantGlyphs.count
+
+ // Strategy: Use the largest variant, but avoid extreme sizes for fonts with many variants
+ let targetIndex: Int
+ if count <= 2 {
+ // Small variant list: use the last one (e.g., integral.size1 at ~2.2em)
+ targetIndex = count - 1
+ } else if count <= 4 {
+ // Medium variant list: use second-to-last to avoid extremes
+ targetIndex = count - 2
+ } else {
+ // Large variant list (like texgyretermes with 6 variants):
+ // Use variant at ~60% position to get appropriate display size (~2.0em)
+ // For 7 variants (0-6), this gives index 4
+ targetIndex = min(count - 2, Int(Double(count) * 0.6))
+ }
+
+ if let glyphVariantName = variantGlyphs[targetIndex] as? String {
+ let variantGlyph = font.get(glyphWithName: glyphVariantName)
+ return variantGlyph
+ }
+ } else {
+ // Text/inline style: use incremental sizing for moderate enlargement
+ // Find the first variant with a different name
+ for gvn in variantGlyphs {
+ if let glyphVariantName = gvn as? String, glyphVariantName != glyphName {
+ let variantGlyph = font.get(glyphWithName: glyphVariantName)
+ return variantGlyph
+ }
+ }
+ }
+
+ // We did not find any variants of this glyph so return it.
+ return glyph
+ }
+ /** Returns the italic correction for the given glyph if any. If there
+ isn't any this returns 0. */
+ override func getItalicCorrection(_ glyph: CGGlyph) -> CGFloat {
+ let font = mathFont.mtfont(size: fontSize)
+ let glyphName = font.get(nameForGlyph: glyph)
+
+ guard let italics = mTable[kItalic] as? NSDictionary, let val = italics[glyphName] as? NSNumber else {
+ return .zero
+ }
+ // if val is nil, this returns 0.
+ return fontUnitsToPt(val.intValue)
+ }
+ override func getTopAccentAdjustment(_ glyph: CGGlyph) -> CGFloat {
+ let font = mathFont.mtfont(size: fontSize)
+ let glyphName = font.get(nameForGlyph: glyph)
+
+ guard let accents = mTable[kAccents] as? NSDictionary, let val = accents[glyphName] as? NSNumber else {
+ // If no top accent is defined then it is the center of the advance width.
+ var glyph = glyph
+ var advances = CGSize.zero
+ CTFontGetAdvancesForGlyphs(font.ctFont, .horizontal, &glyph, &advances, 1)
+ return advances.width/2
+ }
+ return fontUnitsToPt(val.intValue)
+ }
+ override func getVerticalGlyphAssembly(forGlyph glyph: CGGlyph) -> [GlyphPart] {
+ let font = mathFont.mtfont(size: fontSize)
+ let glyphName = font.get(nameForGlyph: glyph)
+
+ guard let assemblyTable = mTable[kVertAssembly] as? NSDictionary,
+ let assemblyInfo = assemblyTable[glyphName] as? NSDictionary,
+ let parts = assemblyInfo[kAssemblyParts] as? NSArray else {
+ // No vertical assembly defined for glyph
+ // parts should always have been defined, but if it isn't return nil
+ return []
+ }
+
+ var rv = [GlyphPart]()
+ for part in parts {
+ guard let partInfo = part as? NSDictionary,
+ let adv = partInfo["advance"] as? NSNumber,
+ let end = partInfo["endConnector"] as? NSNumber,
+ let start = partInfo["startConnector"] as? NSNumber,
+ let ext = partInfo["extender"] as? NSNumber,
+ let glyphName = partInfo["glyph"] as? String else { continue }
+ let fullAdvance = fontUnitsToPt(adv.intValue)
+ let endConnectorLength = fontUnitsToPt(end.intValue)
+ let startConnectorLength = fontUnitsToPt(start.intValue)
+ let isExtender = ext.boolValue
+ let glyph = font.get(glyphWithName: glyphName)
+ let part = GlyphPart(glyph: glyph, fullAdvance: fullAdvance,
+ startConnectorLength: startConnectorLength,
+ endConnectorLength: endConnectorLength,
+ isExtender: isExtender)
+ rv.append(part)
+ }
+ return rv
+ }
+}
diff --git a/third-party/SwiftMath/Sources/MathBundle/MTFontV2.swift b/third-party/SwiftMath/Sources/MathBundle/MTFontV2.swift
new file mode 100755
index 0000000000..14c09a063c
--- /dev/null
+++ b/third-party/SwiftMath/Sources/MathBundle/MTFontV2.swift
@@ -0,0 +1,68 @@
+//
+// MTFontV2.swift
+//
+//
+// Created by Peter Tang on 15/9/2023.
+//
+
+import Foundation
+import CoreGraphics
+import CoreText
+
+extension MathFont {
+ public func mtfont(size: CGFloat) -> MTFontV2 {
+ MTFontV2(font: self, size: size)
+ }
+}
+public final class MTFontV2: MTFont {
+ let font: MathFont
+ let size: CGFloat
+ private let _cgFont: CGFont
+ private let _ctFont: CTFont
+ private let unitsPerEm: UInt
+ private var _mathTab: MTFontMathTableV2?
+ init(font: MathFont = .latinModernFont, size: CGFloat) {
+ self.font = font
+ self.size = size
+ // MathFont cgfont and ctfont are fast & threadsafe, keep a local copy is cheaper than
+ // handling via NSLock
+ self._cgFont = font.cgFont()
+ self._ctFont = font.ctFont(withSize: size)
+ self.unitsPerEm = self._ctFont.unitsPerEm
+ super.init()
+
+ super.defaultCGFont = nil
+ super.ctFont = nil
+ super.mathTable = nil
+ super.rawMathTable = nil
+ }
+ override var defaultCGFont: CGFont! {
+ set { fatalError("\(#function): change to \(font.fontName) not allowed.") }
+ get { _cgFont }
+ }
+ override var ctFont: CTFont! {
+ set { fatalError("\(#function): change to \(font.fontName) not allowed.") }
+ get { _ctFont }
+ }
+ private let mtfontV2LockOnMathTable = NSLock()
+ override var mathTable: MTFontMathTable? {
+ set { fatalError("\(#function): change to \(font.rawValue) not allowed.") }
+ get {
+ guard _mathTab == nil else { return _mathTab }
+ //Note: lazy _mathTab initialization is now threadsafe.
+ mtfontV2LockOnMathTable.lock()
+ defer { mtfontV2LockOnMathTable.unlock() }
+ if _mathTab == nil {
+ _mathTab = MTFontMathTableV2(mathFont: font, size: size, unitsPerEm: unitsPerEm)
+ }
+ return _mathTab
+ }
+ }
+ override var rawMathTable: NSDictionary? {
+ set { fatalError("\(#function): change to \(font.rawValue) not allowed.") }
+ get { fatalError("\(#function): access to \(font.rawValue) not allowed.") }
+ }
+ public override func copy(withSize size: CGFloat) -> MTFont {
+ MTFontV2(font: font, size: size)
+ }
+}
diff --git a/third-party/SwiftMath/Sources/MathBundle/MathFont.swift b/third-party/SwiftMath/Sources/MathBundle/MathFont.swift
new file mode 100644
index 0000000000..650d8f6ee7
--- /dev/null
+++ b/third-party/SwiftMath/Sources/MathBundle/MathFont.swift
@@ -0,0 +1,225 @@
+//
+// MathFont.swift
+//
+//
+// Created by Peter Tang on 10/9/2023.
+//
+
+#if os(iOS) || os(visionOS)
+import UIKit
+#elseif os(macOS)
+import AppKit
+#endif
+
+/// Now available for everyone to use
+public enum MathFont: String, CaseIterable, Identifiable {
+
+ public var id: Self { self } // Makes things simpler for SwiftUI
+
+ case latinModernFont = "latinmodern-math"
+ case kpMathLightFont = "KpMath-Light"
+ case kpMathSansFont = "KpMath-Sans"
+ case xitsFont = "xits-math"
+ case termesFont = "texgyretermes-math"
+ case asanaFont = "Asana-Math"
+ case eulerFont = "Euler-Math"
+ case firaFont = "FiraMath-Regular"
+ case notoSansFont = "NotoSansMath-Regular"
+ case libertinusFont = "LibertinusMath-Regular"
+ case garamondFont = "Garamond-Math"
+ case leteSansFont = "LeteSansMath"
+
+ var fontFamilyName: String {
+ switch self {
+ case .latinModernFont: "Latin Modern Math"
+ case .kpMathLightFont: "KpMath"
+ case .kpMathSansFont: "KpMath"
+ case .xitsFont: "XITS Math"
+ case .termesFont: "TeX Gyre Termes Math"
+ case .asanaFont: "Asana Math"
+ case .eulerFont: "Euler Math"
+ case .firaFont: "Fira Math"
+ case .notoSansFont: "Noto Sans Math"
+ case .libertinusFont: "Libertinus Math"
+ case .garamondFont: "Garamond-Math" // PostScript name is "Garamond-Math", not "Garamond Math"
+ case .leteSansFont: "Lete Sans Math"
+ }
+ }
+
+ var postScriptName: String {
+ switch self {
+ case .latinModernFont: "LatinModernMath-Regular"
+ case .kpMathLightFont: "KpMath-Light"
+ case .kpMathSansFont: "KpMath-Sans"
+ case .xitsFont: "XITSMath"
+ case .termesFont: "TeXGyreTermesMath-Regular"
+ case .asanaFont: "Asana-Math"
+ case .eulerFont: "Euler-Math"
+ case .firaFont: "FiraMath-Regular"
+ case .notoSansFont: "NotoSansMath-Regular"
+ case .libertinusFont: "LibertinusMath-Regular"
+ case .garamondFont: "Garamond-Math"
+ case .leteSansFont: "LeteSansMath"
+ }
+ }
+
+ var fontName: String { self.rawValue }
+
+ public func cgFont() -> CGFont {
+ BundleManager.manager.obtainCGFont(font: self)
+ }
+ public func ctFont(withSize size: CGFloat) -> CTFont {
+ BundleManager.manager.obtainCTFont(font: self, withSize: size)
+ }
+ internal func rawMathTable() -> NSDictionary {
+ BundleManager.manager.obtainRawMathTable(font: self)
+ }
+
+ //Note: Below code are no longer supported, unable to tell if UIFont/NSFont is threadsafe, not used in SwiftMath.
+ // #if os(iOS) || os(visionOS)
+ // public func uiFont(withSize size: CGFloat) -> UIFont? {
+ // UIFont(name: fontName, size: size)
+ // }
+ // #endif
+ // #if os(macOS)
+ // public func nsFont(withSize size: CGFloat) -> NSFont? {
+ // NSFont(name: fontName, size: size)
+ // }
+ // #endif
+}
+internal extension CTFont {
+ /** The size of this font in points. */
+ var fontSize: CGFloat {
+ CTFontGetSize(self)
+ }
+ var unitsPerEm: UInt {
+ return UInt(CTFontGetUnitsPerEm(self))
+ }
+}
+private class BundleManager {
+ //Note: below should be lightweight and without threadsafe problem.
+ static internal let manager = BundleManager()
+
+ private var cgFonts = [MathFont: CGFont]()
+ private var ctFonts = [CTFontSizePair: CTFont]()
+ private var rawMathTables = [MathFont: NSDictionary]()
+
+ private let threadSafeQueue = DispatchQueue(label: "com.smartmath.mathfont.threadsafequeue",
+ qos: .userInitiated,
+ attributes: .concurrent)
+
+ private func registerCGFont(mathFont: MathFont) throws {
+ guard let frameworkBundleURL = Bundle.main.url(forResource: "mathFonts", withExtension: "bundle"),
+ let resourceBundleURL = Bundle(url: frameworkBundleURL)?.path(forResource: mathFont.rawValue, ofType: "otf") else {
+ throw FontError.fontPathNotFound
+ }
+ guard let fontData = NSData(contentsOfFile: resourceBundleURL), let dataProvider = CGDataProvider(data: fontData) else {
+ throw FontError.invalidFontFile
+ }
+ guard let defaultCGFont = CGFont(dataProvider) else {
+ throw FontError.initFontError
+ }
+
+ cgFonts[mathFont] = defaultCGFont
+
+ /// This does not load the complete math font, it only has about half the glyphs of the full math font.
+ /// In particular it does not have the math italic characters which breaks our variable rendering.
+ /// So we first load a CGFont from the file and then convert it to a CTFont.
+ var errorRef: Unmanaged? = nil
+ guard CTFontManagerRegisterGraphicsFont(defaultCGFont, &errorRef) else {
+ throw FontError.registerFailed
+ }
+ let postsript = (defaultCGFont.postScriptName as? String) ?? ""
+ let cgfontName = (defaultCGFont.fullName as? String) ?? ""
+ let threadName = Thread.isMainThread ? "main" : "global"
+ debugPrint("mathFonts bundle resource: \(mathFont.rawValue), font: \(cgfontName), ps: \(postsript) registered on \(threadName).")
+ }
+
+ private func registerMathTable(mathFont: MathFont) throws {
+ guard let frameworkBundleURL = Bundle.main.url(forResource: "mathFonts", withExtension: "bundle"),
+ let mathTablePlist = Bundle(url: frameworkBundleURL)?.url(forResource: mathFont.rawValue, withExtension:"plist") else {
+ throw FontError.fontPathNotFound
+ }
+ guard let rawMathTable = NSDictionary(contentsOf: mathTablePlist),
+ let version = rawMathTable["version"] as? String,
+ version == "1.3" else {
+ throw FontError.invalidMathTable
+ }
+
+ rawMathTables[mathFont] = rawMathTable
+
+ let threadName = Thread.isMainThread ? "main" : "global"
+ debugPrint("mathFonts bundle resource: \(mathFont.rawValue).plist registered on \(threadName).")
+ }
+
+ private func onDemandRegistration(mathFont: MathFont) {
+ guard threadSafeQueue.sync(execute: { cgFonts[mathFont] }) == nil else { return }
+ // Note: resourceLoading is now serialized.
+ threadSafeQueue.sync(flags: .barrier, execute: { [weak self] in
+ if self?.cgFonts[mathFont] == nil {
+ do {
+ try BundleManager.manager.registerCGFont(mathFont: mathFont)
+ try BundleManager.manager.registerMathTable(mathFont: mathFont)
+
+ } catch {
+ fatalError("MTMathFonts:\(#function) ondemand loading failed, mathFont \(mathFont.rawValue), reason \(error)")
+ }
+ }
+ })
+ }
+ fileprivate func obtainCGFont(font: MathFont) -> CGFont {
+ onDemandRegistration(mathFont: font)
+ guard let cgFont = threadSafeQueue.sync(execute: { cgFonts[font] }) else {
+ fatalError("\(#function) unable to locate CGFont \(font.fontName)")
+ }
+ return cgFont
+ }
+
+ fileprivate func obtainCTFont(font: MathFont, withSize size: CGFloat) -> CTFont {
+ onDemandRegistration(mathFont: font)
+ let fontSizePair = CTFontSizePair(font: font, size: size)
+ let ctFont = threadSafeQueue.sync(execute: { ctFonts[fontSizePair] })
+ guard ctFont == nil else { return ctFont! }
+ guard let cgFont = threadSafeQueue.sync(execute: { cgFonts[font] }) else {
+ fatalError("\(#function) unable to locate CGFont \(font.fontName) to create CTFont")
+ }
+ //Note: ctfont creation and caching is now threadsafe.
+ guard threadSafeQueue.sync(execute: { ctFonts[fontSizePair] }) == nil else { return ctFonts[fontSizePair]! }
+ return threadSafeQueue.sync(flags: .barrier, execute: {
+ if let ctfont = ctFonts[fontSizePair] {
+ return ctfont
+ } else {
+ let result = CTFontCreateWithGraphicsFont(cgFont, size, nil, nil)
+ ctFonts[fontSizePair] = result
+ return result
+ }
+ })
+ }
+ fileprivate func obtainRawMathTable(font: MathFont) -> NSDictionary {
+ onDemandRegistration(mathFont: font)
+ guard let mathTable = threadSafeQueue.sync(execute: { rawMathTables[font] } ) else {
+ fatalError("\(#function) unable to locate mathTable: \(font.rawValue).plist")
+ }
+ return mathTable
+ }
+ deinit {
+ ctFonts.removeAll()
+ var errorRef: Unmanaged? = nil
+ cgFonts.values.forEach { cgFont in
+ CTFontManagerUnregisterGraphicsFont(cgFont, &errorRef)
+ }
+ cgFonts.removeAll()
+ }
+ public enum FontError: Error {
+ case invalidFontFile
+ case fontPathNotFound
+ case initFontError
+ case registerFailed
+ case invalidMathTable
+ }
+
+ private struct CTFontSizePair: Hashable {
+ let font: MathFont
+ let size: CGFloat
+ }
+}
diff --git a/third-party/SwiftMath/Sources/MathBundle/MathImage.swift b/third-party/SwiftMath/Sources/MathBundle/MathImage.swift
new file mode 100755
index 0000000000..89a06cb5eb
--- /dev/null
+++ b/third-party/SwiftMath/Sources/MathBundle/MathImage.swift
@@ -0,0 +1,123 @@
+//
+// MathImage.swift
+//
+//
+// Created by Peter Tang on 15/9/2023.
+//
+
+import Foundation
+
+#if os(iOS) || os(visionOS)
+ import UIKit
+#elseif os(macOS)
+ import AppKit
+#endif
+
+public struct MathImage {
+ public var font: MathFont = .latinModernFont
+ public var fontSize: CGFloat
+ public var textColor: MTColor
+
+ public var labelMode: MTMathUILabelMode
+ public var textAlignment: MTTextAlignment
+
+ public var contentInsets: MTEdgeInsets = MTEdgeInsetsZero
+
+ public let latex: String
+
+ private(set) var intrinsicContentSize = CGSize.zero
+
+ public init(latex: String, fontSize: CGFloat, textColor: MTColor, labelMode: MTMathUILabelMode = .display, textAlignment: MTTextAlignment = .center) {
+ self.latex = latex
+ self.fontSize = fontSize
+ self.textColor = textColor
+ self.labelMode = labelMode
+ self.textAlignment = textAlignment
+ }
+}
+extension MathImage {
+ public var currentStyle: MTLineStyle {
+ switch labelMode {
+ case .display: return .display
+ case .text: return .text
+ }
+ }
+ private func intrinsicContentSize(_ displayList: MTMathListDisplay) -> CGSize {
+ CGSize(width: displayList.width + contentInsets.left + contentInsets.right,
+ height: displayList.ascent + displayList.descent + contentInsets.top + contentInsets.bottom)
+ }
+ public struct LayoutInfo {
+ public var ascent: CGFloat = 0
+ public var descent: CGFloat = 0
+
+ public init(ascent: CGFloat, descent: CGFloat) {
+ self.ascent = ascent
+ self.descent = descent
+ }
+ }
+ public mutating func asImage() -> (NSError?, MTImage?, LayoutInfo?) {
+ func layoutImage(size: CGSize, displayList: MTMathListDisplay) {
+ var textX = CGFloat(0)
+ switch self.textAlignment {
+ case .left: textX = contentInsets.left
+ case .center: textX = (size.width - contentInsets.left - contentInsets.right - displayList.width) / 2 + contentInsets.left
+ case .right: textX = size.width - displayList.width - contentInsets.right
+ }
+ let availableHeight = size.height - contentInsets.bottom - contentInsets.top
+
+ // center things vertically
+ var height = displayList.ascent + displayList.descent
+ if height < fontSize/2 {
+ height = fontSize/2 // set height to half the font size
+ }
+ let textY = (availableHeight - height) / 2 + displayList.descent + contentInsets.bottom
+ displayList.position = CGPoint(x: textX, y: textY)
+ }
+ var error: NSError?
+ let mtfont: MTFont? = font.mtfont(size: fontSize)
+
+ guard let mathList = MTMathListBuilder.build(fromString: latex, error: &error), error == nil,
+ let displayList = MTTypesetter.createLineForMathList(mathList, font: mtfont, style: currentStyle) else {
+ return (error, nil, nil)
+ }
+
+ intrinsicContentSize = intrinsicContentSize(displayList)
+ displayList.textColor = textColor
+
+ let size = intrinsicContentSize.regularized
+ layoutImage(size: size, displayList: displayList)
+
+ #if os(iOS) || os(visionOS)
+ let renderer = UIGraphicsImageRenderer(size: size)
+ let image = renderer.image { rendererContext in
+ rendererContext.cgContext.saveGState()
+ rendererContext.cgContext.concatenate(.flippedVertically(size.height))
+ displayList.draw(rendererContext.cgContext)
+ rendererContext.cgContext.restoreGState()
+ }
+ return (nil, image, LayoutInfo(ascent: displayList.ascent, descent: displayList.descent))
+ #endif
+ #if os(macOS)
+ let image = NSImage(size: size, flipped: false) { bounds in
+ guard let context = NSGraphicsContext.current?.cgContext else { return false }
+ context.saveGState()
+ displayList.draw(context)
+ context.restoreGState()
+ return true
+ }
+ return (nil, image, LayoutInfo(ascent: displayList.ascent, descent: displayList.descent))
+ #endif
+ }
+}
+private extension CGAffineTransform {
+ static func flippedVertically(_ height: CGFloat) -> CGAffineTransform {
+ var transform = CGAffineTransform(scaleX: 1, y: -1)
+ transform = transform.translatedBy(x: 0, y: -height)
+ return transform
+ }
+}
+extension CGSize {
+ fileprivate var regularized: CGSize {
+ CGSize(width: ceil(width), height: ceil(height))
+ }
+}
diff --git a/third-party/SwiftMath/Sources/MathRender/MTBezierPath.swift b/third-party/SwiftMath/Sources/MathRender/MTBezierPath.swift
new file mode 100755
index 0000000000..fb5e082a89
--- /dev/null
+++ b/third-party/SwiftMath/Sources/MathRender/MTBezierPath.swift
@@ -0,0 +1,35 @@
+
+//
+// Created by Mike Griebling on 2022-12-31.
+// Translated from an Objective-C implementation by 安志钢.
+//
+// This software may be modified and distributed under the terms of the
+// MIT license. See the LICENSE file for details.
+//
+
+import Foundation
+
+#if os(macOS)
+
+extension MTBezierPath {
+ func addLine(to point: CGPoint) {
+ self.line(to: point)
+ }
+}
+
+extension MTView {
+
+ var backgroundColor:MTColor? {
+ get {
+ MTColor(cgColor: self.layer?.backgroundColor ?? MTColor.clear.cgColor)
+ }
+ set {
+ self.layer?.backgroundColor = MTColor.clear.cgColor
+ self.wantsLayer = true
+ }
+ }
+
+}
+
+#endif
+
diff --git a/third-party/SwiftMath/Sources/MathRender/MTColor.swift b/third-party/SwiftMath/Sources/MathRender/MTColor.swift
new file mode 100755
index 0000000000..88f8a0d24a
--- /dev/null
+++ b/third-party/SwiftMath/Sources/MathRender/MTColor.swift
@@ -0,0 +1,28 @@
+
+//
+// Created by Mike Griebling on 2022-12-31.
+// Translated from an Objective-C implementation by Markus Sähn.
+//
+// This software may be modified and distributed under the terms of the
+// MIT license. See the LICENSE file for details.
+//
+
+import Foundation
+
+extension MTColor {
+
+ public convenience init?(fromHexString hexString:String) {
+ if hexString.isEmpty { return nil }
+ if !hexString.hasPrefix("#") { return nil }
+
+ var rgbValue = UInt64(0)
+ let scanner = Scanner(string: hexString)
+ scanner.charactersToBeSkipped = CharacterSet(charactersIn: "#")
+ scanner.scanHexInt64(&rgbValue)
+ self.init(red: CGFloat((rgbValue & 0xFF0000) >> 16)/255.0,
+ green: CGFloat((rgbValue & 0xFF00) >> 8)/255.0,
+ blue: CGFloat((rgbValue & 0xFF))/255.0,
+ alpha: 1.0)
+ }
+
+}
diff --git a/third-party/SwiftMath/Sources/MathRender/MTConfig.swift b/third-party/SwiftMath/Sources/MathRender/MTConfig.swift
new file mode 100755
index 0000000000..19f339bf69
--- /dev/null
+++ b/third-party/SwiftMath/Sources/MathRender/MTConfig.swift
@@ -0,0 +1,40 @@
+import Foundation
+
+//
+// Created by Mike Griebling on 2022-12-31.
+// Translated from an Objective-C implementation by 安志钢.
+//
+// This software may be modified and distributed under the terms of the
+// MIT license. See the LICENSE file for details.
+//
+
+#if os(iOS) || os(visionOS)
+
+import UIKit
+
+public typealias MTView = UIView
+public typealias MTColor = UIColor
+public typealias MTBezierPath = UIBezierPath
+public typealias MTLabel = UILabel
+public typealias MTEdgeInsets = UIEdgeInsets
+public typealias MTRect = CGRect
+public typealias MTImage = UIImage
+
+let MTEdgeInsetsZero = UIEdgeInsets.zero
+func MTGraphicsGetCurrentContext() -> CGContext? { UIGraphicsGetCurrentContext() }
+
+#else
+
+import AppKit
+
+public typealias MTView = NSView
+public typealias MTColor = NSColor
+public typealias MTBezierPath = NSBezierPath
+public typealias MTEdgeInsets = NSEdgeInsets
+public typealias MTRect = NSRect
+public typealias MTImage = NSImage
+
+let MTEdgeInsetsZero = NSEdgeInsets.init(top: 0, left: 0, bottom: 0, right: 0)
+func MTGraphicsGetCurrentContext() -> CGContext? { NSGraphicsContext.current?.cgContext }
+
+#endif
diff --git a/third-party/SwiftMath/Sources/MathRender/MTFont.swift b/third-party/SwiftMath/Sources/MathRender/MTFont.swift
new file mode 100644
index 0000000000..e1e418d821
--- /dev/null
+++ b/third-party/SwiftMath/Sources/MathRender/MTFont.swift
@@ -0,0 +1,81 @@
+
+import Foundation
+import CoreText
+import AppBundle
+
+//
+// Created by Mike Griebling on 2022-12-31.
+// Translated from an Objective-C implementation by Kostub Deshmukh.
+//
+// This software may be modified and distributed under the terms of the
+// MIT license. See the LICENSE file for details.
+//
+
+public class MTFont {
+
+ var defaultCGFont: CGFont!
+ var ctFont: CTFont!
+ var mathTable: MTFontMathTable?
+ var rawMathTable: NSDictionary?
+
+ /// Fallback font for characters not supported by the main math font.
+ /// Defaults to the system font at the same size. This is particularly useful
+ /// for rendering text in \text{} commands with characters outside the math font's coverage
+ /// (e.g., Chinese, Japanese, Korean, emoji, etc.)
+ public var fallbackFont: CTFont?
+
+ init() {}
+
+ /// `MTFont(fontWithName:)` does not load the complete math font, it only has about half the glyphs of the full math font.
+ /// In particular it does not have the math italic characters which breaks our variable rendering.
+ /// So we first load a CGFont from the file and then convert it to a CTFont.
+ convenience init(fontWithName name: String, size:CGFloat) {
+ self.init()
+ //print("Loading font \(name)")
+ let bundle = getAppBundle()
+ let fontPath = bundle.path(forResource: name, ofType: "otf")
+ let fontDataProvider = CGDataProvider(filename: fontPath!)
+ self.defaultCGFont = CGFont(fontDataProvider!)!
+ //print("Num glyphs: \(self.defaultCGFont.numberOfGlyphs)")
+
+ self.ctFont = CTFontCreateWithGraphicsFont(self.defaultCGFont, size, nil, nil);
+
+ //print("Loading associated .plist")
+ let mathTablePlist = bundle.url(forResource:name, withExtension:"plist")
+ self.rawMathTable = NSDictionary(contentsOf: mathTablePlist!)
+ self.mathTable = MTFontMathTable(withFont:self, mathTable:rawMathTable!)
+ }
+
+ static var fontBundle:Bundle {
+ // Uses bundle for class so that this can be access by the unit tests.
+ Bundle(url: Bundle.main.url(forResource: "mathFonts", withExtension: "bundle")!)!
+ }
+
+ /** Returns a copy of this font but with a different size. */
+ public func copy(withSize size: CGFloat) -> MTFont {
+ let newFont = MTFont()
+ newFont.defaultCGFont = self.defaultCGFont
+ newFont.ctFont = CTFontCreateWithGraphicsFont(self.defaultCGFont, size, nil, nil)
+ newFont.rawMathTable = self.rawMathTable
+ newFont.mathTable = MTFontMathTable(withFont: newFont, mathTable: newFont.rawMathTable!)
+ return newFont
+ }
+
+ func get(nameForGlyph glyph:CGGlyph) -> String {
+ let name = defaultCGFont.name(for: glyph) as? String
+ return name ?? ""
+ }
+
+ func get(glyphWithName name:String) -> CGGlyph {
+ defaultCGFont.getGlyphWithGlyphName(name: name as CFString)
+ }
+
+ /** The size of this font in points. */
+ public var fontSize:CGFloat { CTFontGetSize(self.ctFont) }
+
+ deinit {
+ self.ctFont = nil
+ self.defaultCGFont = nil
+ }
+
+}
diff --git a/third-party/SwiftMath/Sources/MathRender/MTFontManager.swift b/third-party/SwiftMath/Sources/MathRender/MTFontManager.swift
new file mode 100755
index 0000000000..71f42cc477
--- /dev/null
+++ b/third-party/SwiftMath/Sources/MathRender/MTFontManager.swift
@@ -0,0 +1,93 @@
+
+//
+// Created by Mike Griebling on 2022-12-31.
+// Translated from an Objective-C implementation by Kostub Deshmukh.
+//
+// This software may be modified and distributed under the terms of the
+// MIT license. See the LICENSE file for details.
+//
+
+import Foundation
+
+public class MTFontManager {
+
+ static public private(set) var manager: MTFontManager = {
+ MTFontManager()
+ }()
+
+ let kDefaultFontSize = CGFloat(20)
+
+ static var fontManager : MTFontManager {
+ return manager
+ }
+
+ public init() { }
+
+ @RWLocked
+ var nameToFontMap = [String: MTFont]()
+
+ public func font(withName name:String, size:CGFloat) -> MTFont? {
+ var f = self.nameToFontMap[name]
+ if f == nil {
+ f = MTFont(fontWithName: name, size: size)
+ self.nameToFontMap[name] = f
+ }
+
+ if f!.fontSize == size { return f }
+ else { return f!.copy(withSize: size) }
+ }
+
+ public func latinModernFont(withSize size:CGFloat) -> MTFont? {
+ MTFontManager.fontManager.font(withName: "latinmodern-math", size: size)
+ }
+
+ public func kpMathLightFont(withSize size:CGFloat) -> MTFont? {
+ MTFontManager.fontManager.font(withName: "KpMath-Light", size: size)
+ }
+
+ public func kpMathSansFont(withSize size:CGFloat) -> MTFont? {
+ MTFontManager.fontManager.font(withName: "KpMath-Sans", size: size)
+ }
+
+ public func xitsFont(withSize size:CGFloat) -> MTFont? {
+ MTFontManager.fontManager.font(withName: "xits-math", size: size)
+ }
+
+ public func termesFont(withSize size:CGFloat) -> MTFont? {
+ MTFontManager.fontManager.font(withName: "texgyretermes-math", size: size)
+ }
+
+ public func asanaFont(withSize size:CGFloat) -> MTFont? {
+ MTFontManager.fontManager.font(withName: "Asana-Math", size: size)
+ }
+
+ public func eulerFont(withSize size:CGFloat) -> MTFont? {
+ MTFontManager.fontManager.font(withName: "Euler-Math", size: size)
+ }
+
+ public func firaRegularFont(withSize size:CGFloat) -> MTFont? {
+ MTFontManager.fontManager.font(withName: "FiraMath-Regular", size: size)
+ }
+
+ public func notoSansRegularFont(withSize size:CGFloat) -> MTFont? {
+ MTFontManager.fontManager.font(withName: "NotoSansMath-Regular", size: size)
+ }
+
+ public func libertinusRegularFont(withSize size:CGFloat) -> MTFont? {
+ MTFontManager.fontManager.font(withName: "LibertinusMath-Regular", size: size)
+ }
+
+ public func garamondMathFont(withSize size:CGFloat) -> MTFont? {
+ MTFontManager.fontManager.font(withName: "Garamond-Math", size: size)
+ }
+
+ public func leteSansFont(withSize size:CGFloat) -> MTFont? {
+ MTFontManager.fontManager.font(withName: "LeteSansMath", size: size)
+ }
+
+ public var defaultFont: MTFont? {
+ MTFontManager.fontManager.latinModernFont(withSize: kDefaultFontSize)
+ }
+
+
+}
diff --git a/third-party/SwiftMath/Sources/MathRender/MTFontMathTable.swift b/third-party/SwiftMath/Sources/MathRender/MTFontMathTable.swift
new file mode 100755
index 0000000000..35e252b1ad
--- /dev/null
+++ b/third-party/SwiftMath/Sources/MathRender/MTFontMathTable.swift
@@ -0,0 +1,355 @@
+//
+// Created by Mike Griebling on 2022-12-31.
+// Translated from an Objective-C implementation by Kostub Deshmukh.
+//
+// This software may be modified and distributed under the terms of the
+// MIT license. See the LICENSE file for details.
+//
+
+import Foundation
+import CoreText
+
+struct GlyphPart {
+ /// The glyph that represents this part
+ var glyph: CGGlyph!
+
+ /// Full advance width/height for this part, in the direction of the extension in points.
+ var fullAdvance: CGFloat = 0
+
+ /// Advance width/ height of the straight bar connector material at the beginning of the glyph in points.
+ var startConnectorLength: CGFloat = 0
+
+ /// Advance width/ height of the straight bar connector material at the end of the glyph in points.
+ var endConnectorLength: CGFloat = 0
+
+ /// If this part is an extender. If set, the part can be skipped or repeated.
+ var isExtender: Bool = false
+}
+
+/** This class represents the Math table of an open type font.
+
+ The math table is documented here: https://www.microsoft.com/typography/otspec/math.htm
+
+ How the constants in this class affect the display is documented here:
+ http://www.tug.org/TUGboat/tb30-1/tb94vieth.pdf
+
+ Note: We don't parse the math table from the open type font. Rather we parse it
+ in python and convert it to a .plist file which is easily consumed by this class.
+ This approach is preferable to spending an inordinate amount of time figuring out
+ how to parse the returned NSData object using the open type rules.
+
+ Remark: This class is not meant to be used outside of this library.
+ */
+class MTFontMathTable {
+
+ // The font for this math table.
+ public private(set) weak var font:MTFont? // @property (nonatomic, readonly, weak) MTFont* font;
+
+ var _unitsPerEm: UInt
+ var _fontSize: CGFloat
+ var _mathTable: NSDictionary!
+
+ let kConstants = "constants"
+
+ /** MU unit in points */
+ var muUnit:CGFloat { _fontSize/18 }
+
+ func fontUnitsToPt(_ fontUnits:Int) -> CGFloat {
+ CGFloat(fontUnits) * _fontSize / CGFloat(_unitsPerEm)
+ }
+
+ init(withFont font: MTFont?, mathTable:NSDictionary) {
+ assert(font != nil, "font has nil value")
+ assert(font!.ctFont != nil, "font.ctFont has nil value")
+ self.font = font
+ // do domething with font
+ _unitsPerEm = UInt(CTFontGetUnitsPerEm(font!.ctFont))
+ _fontSize = font!.fontSize;
+ _mathTable = mathTable
+ let version = _mathTable["version"] as! String
+ if version != "1.3" {
+ NSException(name: NSExceptionName.internalInconsistencyException, reason: "Invalid version of math table plist: \(version)").raise()
+ }
+ }
+
+ func constantFromTable(_ constName:String) -> CGFloat {
+ let consts = _mathTable[kConstants] as! NSDictionary?
+ let val = consts![constName] as! NSNumber?
+ return fontUnitsToPt(val!.intValue)
+ }
+
+ func percentFromTable(_ percentName:String) -> CGFloat {
+ let consts = _mathTable[kConstants] as! NSDictionary?
+ let val = consts![percentName] as! NSNumber?
+ return CGFloat(val!.floatValue) / 100
+ }
+
+ /// Math Font Metrics from the opentype specification
+ // MARK: - Fractions
+ var fractionNumeratorDisplayStyleShiftUp:CGFloat { constantFromTable("FractionNumeratorDisplayStyleShiftUp") } // \sigma_8 in TeX
+ var fractionNumeratorShiftUp:CGFloat { constantFromTable("FractionNumeratorShiftUp") } // \sigma_9 in TeX
+ var fractionDenominatorDisplayStyleShiftDown:CGFloat { constantFromTable("FractionDenominatorDisplayStyleShiftDown") } // \sigma_11 in TeX
+ var fractionDenominatorShiftDown:CGFloat { constantFromTable("FractionDenominatorShiftDown") } // \sigma_12 in TeX
+ var fractionNumeratorDisplayStyleGapMin:CGFloat { constantFromTable("FractionNumDisplayStyleGapMin") } // 3 * \xi_8 in TeX
+ var fractionNumeratorGapMin:CGFloat { constantFromTable("FractionNumeratorGapMin") } // \xi_8 in TeX
+ var fractionDenominatorDisplayStyleGapMin:CGFloat { constantFromTable("FractionDenomDisplayStyleGapMin") } // 3 * \xi_8 in TeX
+ var fractionDenominatorGapMin:CGFloat { constantFromTable("FractionDenominatorGapMin") } // \xi_8 in TeX
+ var fractionRuleThickness:CGFloat { constantFromTable("FractionRuleThickness") } // \xi_8 in TeX
+ var skewedFractionHorizonalGap:CGFloat { constantFromTable("SkewedFractionHorizontalGap") } // \sigma_20 in TeX
+ var skewedFractionVerticalGap:CGFloat { constantFromTable("SkewedFractionVerticalGap") } // \sigma_21 in TeX
+
+ // MARK: - Non-standard
+ /// FractionDelimiterSize and FractionDelimiterDisplayStyleSize are not constants
+ /// specified in the OpenType Math specification. Rather these are proposed LuaTeX extensions
+ /// for the TeX parameters \sigma_20 (delim1) and \sigma_21 (delim2). Since these do not
+ /// exist in the fonts that we have, we use the same approach as LuaTeX and use the fontSize
+ /// to determine these values. The constants used match the metrics values of the original TeX fonts.
+ /// Note: An alternative approach is to use DelimitedSubFormulaMinHeight for \sigma21 and use a factor
+ /// of 2 to get \sigma 20 as proposed in Vieth paper.
+ /// The XeTeX implementation sets \sigma21 = fontSize and \sigma20 = DelimitedSubFormulaMinHeight which
+ /// will produce smaller delimiters.
+ /// Of all the approaches we've implemented LuaTeX's approach since it mimics LaTeX most accurately.
+ var fractionDelimiterSize: CGFloat { 1.01 * _fontSize }
+
+ /// Modified constant from 2.4 to 2.39 for better visual appearance.
+ var fractionDelimiterDisplayStyleSize: CGFloat { 2.39 * _fontSize }
+
+ // MARK: - Stacks
+ var stackTopDisplayStyleShiftUp:CGFloat { constantFromTable("StackTopDisplayStyleShiftUp") } // \sigma_8 in TeX
+ var stackTopShiftUp:CGFloat { constantFromTable("StackTopShiftUp") } // \sigma_10 in TeX
+ var stackDisplayStyleGapMin:CGFloat { constantFromTable("StackDisplayStyleGapMin") } // 7 \xi_8 in TeX
+ var stackGapMin:CGFloat { constantFromTable("StackGapMin") } // 3 \xi_8 in TeX
+ var stackBottomDisplayStyleShiftDown:CGFloat { constantFromTable("StackBottomDisplayStyleShiftDown") } // \sigma_11 in TeX
+ var stackBottomShiftDown:CGFloat { constantFromTable("StackBottomShiftDown") } // \sigma_12 in TeX
+
+ var stretchStackBottomShiftDown:CGFloat { constantFromTable("StretchStackBottomShiftDown") }
+ var stretchStackGapAboveMin:CGFloat { constantFromTable("StretchStackGapAboveMin") }
+ var stretchStackGapBelowMin:CGFloat { constantFromTable("StretchStackGapBelowMin") }
+ var stretchStackTopShiftUp:CGFloat { constantFromTable("StretchStackTopShiftUp") }
+
+ // MARK: - super/sub scripts
+
+ var superscriptShiftUp:CGFloat { constantFromTable("SuperscriptShiftUp") } // \sigma_13, \sigma_14 in TeX
+ var superscriptShiftUpCramped:CGFloat { constantFromTable("SuperscriptShiftUpCramped") } // \sigma_15 in TeX
+ var subscriptShiftDown:CGFloat { constantFromTable("SubscriptShiftDown") } // \sigma_16, \sigma_17 in TeX
+ var superscriptBaselineDropMax:CGFloat { constantFromTable("SuperscriptBaselineDropMax") } // \sigma_18 in TeX
+ var subscriptBaselineDropMin:CGFloat { constantFromTable("SubscriptBaselineDropMin") } // \sigma_19 in TeX
+ var superscriptBottomMin:CGFloat { constantFromTable("SuperscriptBottomMin") } // 1/4 \sigma_5 in TeX
+ var subscriptTopMax:CGFloat { constantFromTable("SubscriptTopMax") } // 4/5 \sigma_5 in TeX
+ var subSuperscriptGapMin:CGFloat { constantFromTable("SubSuperscriptGapMin") } // 4 \xi_8 in TeX
+ var superscriptBottomMaxWithSubscript:CGFloat { constantFromTable("SuperscriptBottomMaxWithSubscript") } // 4/5 \sigma_5 in TeX
+
+ var spaceAfterScript:CGFloat { constantFromTable("SpaceAfterScript") }
+
+ // MARK: - radicals
+ var radicalExtraAscender:CGFloat { constantFromTable("RadicalExtraAscender") } // \xi_8 in Tex
+ var radicalRuleThickness:CGFloat { constantFromTable("RadicalRuleThickness") } // \xi_8 in Tex
+ var radicalDisplayStyleVerticalGap:CGFloat { constantFromTable("RadicalDisplayStyleVerticalGap") } // \xi_8 + 1/4 \sigma_5 in Tex
+ var radicalVerticalGap:CGFloat { constantFromTable("RadicalVerticalGap") } // 5/4 \xi_8 in Tex
+ var radicalKernBeforeDegree:CGFloat { constantFromTable("RadicalKernBeforeDegree") } // 5 mu in Tex
+ var radicalKernAfterDegree:CGFloat { constantFromTable("RadicalKernAfterDegree") } // -10 mu in Tex
+ var radicalDegreeBottomRaisePercent:CGFloat { percentFromTable("RadicalDegreeBottomRaisePercent") } // 60% in Tex
+
+ // MARK: - Limits
+ var upperLimitBaselineRiseMin:CGFloat { constantFromTable("UpperLimitBaselineRiseMin") } // \xi_11 in TeX
+ var upperLimitGapMin:CGFloat { constantFromTable("UpperLimitGapMin") } // \xi_9 in TeX
+ var lowerLimitGapMin:CGFloat { constantFromTable("LowerLimitGapMin") } // \xi_10 in TeX
+ var lowerLimitBaselineDropMin:CGFloat { constantFromTable("LowerLimitBaselineDropMin") } // \xi_12 in TeX
+ var limitExtraAscenderDescender:CGFloat { 0 } // \xi_13 in TeX, not present in OpenType so we always set it to 0.
+
+ // MARK: - Underline
+ var underbarVerticalGap:CGFloat { constantFromTable("UnderbarVerticalGap") } // 3 \xi_8 in TeX
+ var underbarRuleThickness:CGFloat { constantFromTable("UnderbarRuleThickness") } // \xi_8 in TeX
+ var underbarExtraDescender:CGFloat { constantFromTable("UnderbarExtraDescender") } // \xi_8 in TeX
+
+ // MARK: - Overline
+ var overbarVerticalGap:CGFloat { constantFromTable("OverbarVerticalGap") } // 3 \xi_8 in TeX
+ var overbarRuleThickness:CGFloat { constantFromTable("OverbarRuleThickness") } // \xi_8 in TeX
+ var overbarExtraAscender:CGFloat { constantFromTable("OverbarExtraAscender") } // \xi_8 in TeX
+
+ // MARK: - Constants
+
+ var axisHeight:CGFloat { constantFromTable("AxisHeight") } // \sigma_22 in TeX
+ var scriptScaleDown:CGFloat { percentFromTable("ScriptPercentScaleDown") }
+ var scriptScriptScaleDown:CGFloat { percentFromTable("ScriptScriptPercentScaleDown") }
+ var mathLeading:CGFloat { constantFromTable("MathLeading") }
+ var delimitedSubFormulaMinHeight:CGFloat { constantFromTable("DelimitedSubFormulaMinHeight") }
+
+ // MARK: - Accent
+
+ var accentBaseHeight:CGFloat { constantFromTable("AccentBaseHeight") } // \fontdimen5 in TeX (x-height)
+ var flattenedAccentBaseHeight:CGFloat { constantFromTable("FlattenedAccentBaseHeight") }
+
+ // MARK: - Variants
+
+ let kVertVariants = "v_variants"
+ let kHorizVariants = "h_variants"
+
+ /** Returns an Array of all the vertical variants of the glyph if any. If
+ there are no variants for the glyph, the array contains the given glyph. */
+ func getVerticalVariantsForGlyph( _ glyph:CGGlyph) -> [NSNumber?] {
+ let variants = _mathTable[kVertVariants] as! NSDictionary?
+ return self.getVariantsForGlyph(glyph, inDictionary: variants!)
+ }
+
+ /** Returns an Array of all the horizontal variants of the glyph if any. If
+ there are no variants for the glyph, the array contains the given glyph. */
+ func getHorizontalVariantsForGlyph( _ glyph:CGGlyph) -> [NSNumber?] {
+ let variants = _mathTable[kHorizVariants] as! NSDictionary
+ return self.getVariantsForGlyph(glyph, inDictionary:variants)
+ }
+
+ func getVariantsForGlyph(_ glyph: CGGlyph, inDictionary variants:NSDictionary) -> [NSNumber?] {
+ let glyphName = self.font!.get(nameForGlyph: glyph)
+ let variantGlyphs = variants[glyphName] as! NSArray?
+ var glyphArray = [NSNumber]()
+ if variantGlyphs == nil || variantGlyphs?.count == 0 {
+ // There are no extra variants, so just add the current glyph to it.
+ let glyph = self.font!.get(glyphWithName: glyphName)
+ glyphArray.append(NSNumber(value:glyph))
+ return glyphArray
+ }
+ for gvn in variantGlyphs! {
+ let glyphVariantName = gvn as! String?
+ let variantGlyph = self.font?.get(glyphWithName: glyphVariantName!)
+ glyphArray.append(NSNumber(value:variantGlyph!))
+ }
+ return glyphArray
+ }
+
+ /** Returns a larger vertical variant of the given glyph if any.
+ If there is no larger version, this returns the current glyph.
+
+ - Parameter glyph: The glyph to find a larger variant for
+ - Parameter forDisplayStyle: If true, selects the largest appropriate variant for display style.
+ If false, selects the next larger variant (incremental sizing).
+ - Returns: A larger glyph variant, or the original glyph if no variants exist
+ */
+ func getLargerGlyph(_ glyph:CGGlyph, forDisplayStyle: Bool = false) -> CGGlyph {
+ let variants = _mathTable[kVertVariants] as! NSDictionary?
+ let glyphName = self.font?.get(nameForGlyph: glyph)
+ let variantGlyphs = variants![glyphName!] as! NSArray?
+ if variantGlyphs == nil || variantGlyphs?.count == 0 {
+ // There are no extra variants, so just returnt the current glyph.
+ return glyph
+ }
+
+ if forDisplayStyle {
+ // For display style, select a large variant suitable for mathematical display mode
+ // Display integrals should be significantly larger (~2.2em) for visual prominence
+ let count = variantGlyphs!.count
+
+ // Strategy: Use the largest variant, but avoid extreme sizes for fonts with many variants
+ let targetIndex: Int
+ if count <= 2 {
+ // Small variant list: use the last one (e.g., integral.size1 at ~2.2em)
+ targetIndex = count - 1
+ } else if count <= 4 {
+ // Medium variant list: use second-to-last to avoid extremes
+ targetIndex = count - 2
+ } else {
+ // Large variant list (like texgyretermes with 6 variants):
+ // Use variant at ~60% position to get appropriate display size (~2.0em)
+ // For 7 variants (0-6), this gives index 4
+ targetIndex = min(count - 2, Int(Double(count) * 0.6))
+ }
+
+ if let glyphVariantName = variantGlyphs![targetIndex] as? String {
+ let variantGlyph = self.font?.get(glyphWithName: glyphVariantName)
+ return variantGlyph!
+ }
+ } else {
+ // Text/inline style: use incremental sizing for moderate enlargement
+ // Find the first variant with a different name
+ for gvn in variantGlyphs! {
+ let glyphVariantName = gvn as! String?
+ if glyphVariantName != glyphName {
+ let variantGlyph = self.font?.get(glyphWithName: glyphVariantName!)
+ return variantGlyph!
+ }
+ }
+ }
+
+ // We did not find any variants of this glyph so return it.
+ return glyph;
+ }
+
+ // MARK: - Italic Correction
+
+ let kItalic = "italic"
+
+ /** Returns the italic correction for the given glyph if any. If there
+ isn't any this returns 0. */
+ func getItalicCorrection(_ glyph: CGGlyph) -> CGFloat {
+ let italics = _mathTable[kItalic] as! NSDictionary?
+ let glyphName = self.font?.get(nameForGlyph: glyph)
+ let val = italics![glyphName!] as! NSNumber?
+ // if val is nil, this returns 0.
+ return self.fontUnitsToPt(val?.intValue ?? 0)
+ }
+
+ // MARK: - Accents
+
+ let kAccents = "accents"
+
+ /** Returns the adjustment to the top accent for the given glyph if any.
+ If there isn't any this returns -1. */
+ func getTopAccentAdjustment(_ glyph: CGGlyph) -> CGFloat {
+ var glyph = glyph
+ let accents = _mathTable[kAccents] as! NSDictionary?
+ let glyphName = self.font?.get(nameForGlyph: glyph)
+ let val = accents![glyphName!] as! NSNumber?
+ if let val = val {
+ return self.fontUnitsToPt(val.intValue)
+ } else {
+ // If no top accent is defined then it is the center of the advance width.
+ var advances = CGSize.zero
+ CTFontGetAdvancesForGlyphs(self.font!.ctFont, .horizontal, &glyph, &advances, 1)
+ return advances.width/2
+ }
+ }
+
+ // MARK: - Glyph Construction
+
+ /** Minimum overlap of connecting glyphs during glyph construction */
+ var minConnectorOverlap:CGFloat { constantFromTable("MinConnectorOverlap") }
+
+ let kVertAssembly = "v_assembly"
+ let kAssemblyParts = "parts"
+
+ /** Returns an array of the glyph parts to be used for constructing vertical variants
+ of this glyph. If there is no glyph assembly defined, returns an empty array. */
+ func getVerticalGlyphAssembly(forGlyph glyph:CGGlyph) -> [GlyphPart] {
+ let assemblyTable = _mathTable[kVertAssembly] as! NSDictionary?
+ let glyphName = self.font?.get(nameForGlyph: glyph)
+ let assemblyInfo = assemblyTable![glyphName!] as! NSDictionary?
+ if assemblyInfo == nil {
+ // No vertical assembly defined for glyph
+ return []
+ }
+ let parts = assemblyInfo![kAssemblyParts] as! NSArray?
+ if parts == nil {
+ // parts should always have been defined, but if it isn't return nil
+ return []
+ }
+ var rv = [GlyphPart]()
+ for part in parts! {
+ let partInfo = part as! NSDictionary?
+ var part = GlyphPart()
+ let adv = partInfo!["advance"] as! NSNumber?
+ part.fullAdvance = self.fontUnitsToPt(adv!.intValue)
+ let end = partInfo!["endConnector"] as! NSNumber?
+ part.endConnectorLength = self.fontUnitsToPt(end!.intValue)
+ let start = partInfo!["startConnector"] as! NSNumber?
+ part.startConnectorLength = self.fontUnitsToPt(start!.intValue)
+ let ext = partInfo!["extender"] as! NSNumber?
+ part.isExtender = ext!.boolValue
+ let glyphName = partInfo!["glyph"] as! String?
+ part.glyph = self.font?.get(glyphWithName: glyphName!)
+ rv.append(part)
+ }
+ return rv
+ }
+
+
+}
diff --git a/third-party/SwiftMath/Sources/MathRender/MTLabel.swift b/third-party/SwiftMath/Sources/MathRender/MTLabel.swift
new file mode 100755
index 0000000000..8964a9027b
--- /dev/null
+++ b/third-party/SwiftMath/Sources/MathRender/MTLabel.swift
@@ -0,0 +1,37 @@
+//
+// Created by Mike Griebling on 2022-12-31.
+// Translated from an Objective-C implementation by 安志钢.
+//
+// This software may be modified and distributed under the terms of the
+// MIT license. See the LICENSE file for details.
+//
+
+import Foundation
+import SwiftUI
+
+#if os(macOS)
+
+public class MTLabel : NSTextField {
+
+ init() {
+ super.init(frame: .zero)
+ self.stringValue = ""
+ self.isBezeled = false
+ self.drawsBackground = false
+ self.isEditable = false
+ self.isSelectable = false
+ }
+
+ required init?(coder: NSCoder) {
+ super.init(coder: coder)
+ }
+
+ // MARK: - Customized getter and setter methods for property text.
+ var text:String? {
+ get { super.stringValue }
+ set { super.stringValue = newValue! }
+ }
+
+}
+
+#endif
diff --git a/third-party/SwiftMath/Sources/MathRender/MTMathAtomFactory.swift b/third-party/SwiftMath/Sources/MathRender/MTMathAtomFactory.swift
new file mode 100644
index 0000000000..8d35a93dc3
--- /dev/null
+++ b/third-party/SwiftMath/Sources/MathRender/MTMathAtomFactory.swift
@@ -0,0 +1,1141 @@
+//
+// Created by Mike Griebling on 2022-12-31.
+// Translated from an Objective-C implementation by Kostub Deshmukh.
+//
+// This software may be modified and distributed under the terms of the
+// MIT license. See the LICENSE file for details.
+//
+
+import Foundation
+
+/** A factory to create commonly used MTMathAtoms. */
+public class MTMathAtomFactory {
+
+ public static let aliases = [
+ "lnot" : "neg",
+ "land" : "wedge",
+ "lor" : "vee",
+ "ne" : "neq",
+ "le" : "leq",
+ "ge" : "geq",
+ "lbrace" : "{",
+ "rbrace" : "}",
+ "Vert" : "|",
+ "gets" : "leftarrow",
+ "to" : "rightarrow",
+ "iff" : "Longleftrightarrow",
+ "AA" : "angstrom"
+ ]
+
+ public static let delimiters = [
+ "." : "", // . means no delimiter
+ "(" : "(",
+ ")" : ")",
+ "[" : "[",
+ "]" : "]",
+ "<" : "\u{2329}",
+ ">" : "\u{232A}",
+ "/" : "/",
+ "\\" : "\\",
+ "|" : "|",
+ "lgroup" : "\u{27EE}",
+ "rgroup" : "\u{27EF}",
+ "||" : "\u{2016}",
+ "Vert" : "\u{2016}",
+ "vert" : "|",
+ "uparrow" : "\u{2191}",
+ "downarrow" : "\u{2193}",
+ "updownarrow" : "\u{2195}",
+ "Uparrow" : "\u{21D1}",
+ "Downarrow" : "\u{21D3}",
+ "Updownarrow" : "\u{21D5}",
+ "backslash" : "\\",
+ "rangle" : "\u{232A}",
+ "langle" : "\u{2329}",
+ "rbrace" : "}",
+ "}" : "}",
+ "{" : "{",
+ "lbrace" : "{",
+ "lceil" : "\u{2308}",
+ "rceil" : "\u{2309}",
+ "lfloor" : "\u{230A}",
+ "rfloor" : "\u{230B}",
+ // Corner brackets (amssymb)
+ "ulcorner" : "\u{231C}", // upper left corner
+ "urcorner" : "\u{231D}", // upper right corner
+ "llcorner" : "\u{231E}", // lower left corner
+ "lrcorner" : "\u{231F}", // lower right corner
+ // Double square brackets (strachey brackets)
+ "llbracket" : "\u{27E6}", // left double bracket
+ "rrbracket" : "\u{27E7}", // right double bracket
+ ]
+
+ private static let delimValueLock = NSLock()
+ static var _delimValueToName = [String: String]()
+ public static var delimValueToName: [String: String] {
+ if _delimValueToName.isEmpty {
+ var output = [String: String]()
+ for (key, value) in Self.delimiters {
+ if let existingValue = output[value] {
+ if key.count > existingValue.count {
+ continue
+ } else if key.count == existingValue.count {
+ if key.compare(existingValue) == .orderedDescending {
+ continue
+ }
+ }
+ }
+ output[value] = key
+ }
+ // protect lazily loading table in a multi-thread concurrent environment
+ delimValueLock.lock()
+ defer { delimValueLock.unlock() }
+ if _delimValueToName.isEmpty {
+ _delimValueToName = output
+ }
+ }
+ return _delimValueToName
+ }
+
+ public static let accents = [
+ "grave" : "\u{0300}",
+ "acute" : "\u{0301}",
+ "hat" : "\u{0302}", // In our implementation hat and widehat behave the same.
+ "tilde" : "\u{0303}", // In our implementation tilde and widetilde behave the same.
+ "bar" : "\u{0304}",
+ "breve" : "\u{0306}",
+ "dot" : "\u{0307}",
+ "ddot" : "\u{0308}",
+ "check" : "\u{030C}",
+ "vec" : "\u{20D7}",
+ "widehat" : "\u{0302}",
+ "widetilde" : "\u{0303}",
+ "overleftarrow" : "\u{20D6}", // Combining left arrow above
+ "overrightarrow" : "\u{20D7}", // Combining right arrow above (same as vec)
+ "overleftrightarrow" : "\u{20E1}" // Combining left right arrow above
+ ]
+
+ private static let accentValueLock = NSLock()
+ static var _accentValueToName: [String: String]? = nil
+ public static var accentValueToName: [String: String] {
+ if _accentValueToName == nil {
+ var output = [String: String]()
+
+ for (key, value) in Self.accents {
+ if let existingValue = output[value] {
+ if key.count > existingValue.count {
+ continue
+ } else if key.count == existingValue.count {
+ if key.compare(existingValue) == .orderedDescending {
+ continue
+ }
+ }
+ }
+ output[value] = key
+ }
+ // protect lazily loading table in a multi-thread concurrent environment
+ accentValueLock.lock()
+ defer { accentValueLock.unlock() }
+ if _accentValueToName == nil {
+ _accentValueToName = output
+ }
+ }
+ return _accentValueToName!
+ }
+
+ static var supportedLatexSymbolNames:[String] {
+ let commands = MTMathAtomFactory.supportedLatexSymbols
+ return commands.keys.map { String($0) }
+ }
+
+ static var supportedLatexSymbols: [String: MTMathAtom] = [
+ "square" : MTMathAtomFactory.placeholder(),
+
+ // Greek characters
+ "alpha" : MTMathAtom(type: .variable, value: "\u{03B1}"),
+ "beta" : MTMathAtom(type: .variable, value: "\u{03B2}"),
+ "gamma" : MTMathAtom(type: .variable, value: "\u{03B3}"),
+ "delta" : MTMathAtom(type: .variable, value: "\u{03B4}"),
+ "varepsilon" : MTMathAtom(type: .variable, value: "\u{03B5}"),
+ "zeta" : MTMathAtom(type: .variable, value: "\u{03B6}"),
+ "eta" : MTMathAtom(type: .variable, value: "\u{03B7}"),
+ "theta" : MTMathAtom(type: .variable, value: "\u{03B8}"),
+ "iota" : MTMathAtom(type: .variable, value: "\u{03B9}"),
+ "kappa" : MTMathAtom(type: .variable, value: "\u{03BA}"),
+ "lambda" : MTMathAtom(type: .variable, value: "\u{03BB}"),
+ "mu" : MTMathAtom(type: .variable, value: "\u{03BC}"),
+ "nu" : MTMathAtom(type: .variable, value: "\u{03BD}"),
+ "xi" : MTMathAtom(type: .variable, value: "\u{03BE}"),
+ "omicron" : MTMathAtom(type: .variable, value: "\u{03BF}"),
+ "pi" : MTMathAtom(type: .variable, value: "\u{03C0}"),
+ "rho" : MTMathAtom(type: .variable, value: "\u{03C1}"),
+ "varsigma" : MTMathAtom(type: .variable, value: "\u{03C2}"),
+ "sigma" : MTMathAtom(type: .variable, value: "\u{03C3}"),
+ "tau" : MTMathAtom(type: .variable, value: "\u{03C4}"),
+ "upsilon" : MTMathAtom(type: .variable, value: "\u{03C5}"),
+ "varphi" : MTMathAtom(type: .variable, value: "\u{03C6}"),
+ "chi" : MTMathAtom(type: .variable, value: "\u{03C7}"),
+ "psi" : MTMathAtom(type: .variable, value: "\u{03C8}"),
+ "omega" : MTMathAtom(type: .variable, value: "\u{03C9}"),
+ // We mark the following greek chars as ordinary so that we don't try
+ // to automatically italicize them as we do with variables.
+ // These characters fall outside the rules of italicization that we have defined.
+ "epsilon" : MTMathAtom(type: .ordinary, value: "\u{0001D716}"),
+ "vartheta" : MTMathAtom(type: .ordinary, value: "\u{0001D717}"),
+ "phi" : MTMathAtom(type: .ordinary, value: "\u{0001D719}"),
+ "varrho" : MTMathAtom(type: .ordinary, value: "\u{0001D71A}"),
+ "varpi" : MTMathAtom(type: .ordinary, value: "\u{0001D71B}"),
+ "varkappa" : MTMathAtom(type: .ordinary, value: "\u{03F0}"),
+ // Note: digamma (U+03DD) and Digamma (U+03DC) are not supported by Latin Modern Math font
+
+ // Capital greek characters
+ "Gamma" : MTMathAtom(type: .variable, value: "\u{0393}"),
+ "Delta" : MTMathAtom(type: .variable, value: "\u{0394}"),
+ "Theta" : MTMathAtom(type: .variable, value: "\u{0398}"),
+ "Lambda" : MTMathAtom(type: .variable, value: "\u{039B}"),
+ "Xi" : MTMathAtom(type: .variable, value: "\u{039E}"),
+ "Pi" : MTMathAtom(type: .variable, value: "\u{03A0}"),
+ "Sigma" : MTMathAtom(type: .variable, value: "\u{03A3}"),
+ "Upsilon" : MTMathAtom(type: .variable, value: "\u{03A5}"),
+ "Phi" : MTMathAtom(type: .variable, value: "\u{03A6}"),
+ "Psi" : MTMathAtom(type: .variable, value: "\u{03A8}"),
+ "Omega" : MTMathAtom(type: .variable, value: "\u{03A9}"),
+
+ // Open
+ "lceil" : MTMathAtom(type: .open, value: "\u{2308}"),
+ "lfloor" : MTMathAtom(type: .open, value: "\u{230A}"),
+ "langle" : MTMathAtom(type: .open, value: "\u{27E8}"),
+ "lgroup" : MTMathAtom(type: .open, value: "\u{27EE}"),
+
+ // Close
+ "rceil" : MTMathAtom(type: .close, value: "\u{2309}"),
+ "rfloor" : MTMathAtom(type: .close, value: "\u{230B}"),
+ "rangle" : MTMathAtom(type: .close, value: "\u{27E9}"),
+ "rgroup" : MTMathAtom(type: .close, value: "\u{27EF}"),
+
+ // Arrows
+ "leftarrow" : MTMathAtom(type: .relation, value: "\u{2190}"),
+ "uparrow" : MTMathAtom(type: .relation, value: "\u{2191}"),
+ "rightarrow" : MTMathAtom(type: .relation, value: "\u{2192}"),
+ "downarrow" : MTMathAtom(type: .relation, value: "\u{2193}"),
+ "leftrightarrow" : MTMathAtom(type: .relation, value: "\u{2194}"),
+ "updownarrow" : MTMathAtom(type: .relation, value: "\u{2195}"),
+ "nwarrow" : MTMathAtom(type: .relation, value: "\u{2196}"),
+ "nearrow" : MTMathAtom(type: .relation, value: "\u{2197}"),
+ "searrow" : MTMathAtom(type: .relation, value: "\u{2198}"),
+ "swarrow" : MTMathAtom(type: .relation, value: "\u{2199}"),
+ "mapsto" : MTMathAtom(type: .relation, value: "\u{21A6}"),
+ "Leftarrow" : MTMathAtom(type: .relation, value: "\u{21D0}"),
+ "Uparrow" : MTMathAtom(type: .relation, value: "\u{21D1}"),
+ "Rightarrow" : MTMathAtom(type: .relation, value: "\u{21D2}"),
+ "Downarrow" : MTMathAtom(type: .relation, value: "\u{21D3}"),
+ "Leftrightarrow" : MTMathAtom(type: .relation, value: "\u{21D4}"),
+ "Updownarrow" : MTMathAtom(type: .relation, value: "\u{21D5}"),
+ "longleftarrow" : MTMathAtom(type: .relation, value: "\u{27F5}"),
+ "longrightarrow" : MTMathAtom(type: .relation, value: "\u{27F6}"),
+ "longleftrightarrow" : MTMathAtom(type: .relation, value: "\u{27F7}"),
+ "Longleftarrow" : MTMathAtom(type: .relation, value: "\u{27F8}"),
+ "Longrightarrow" : MTMathAtom(type: .relation, value: "\u{27F9}"),
+ "Longleftrightarrow" : MTMathAtom(type: .relation, value: "\u{27FA}"),
+ "longmapsto" : MTMathAtom(type: .relation, value: "\u{27FC}"),
+ "hookrightarrow" : MTMathAtom(type: .relation, value: "\u{21AA}"),
+ "hookleftarrow" : MTMathAtom(type: .relation, value: "\u{21A9}"),
+
+
+ // Relations
+ "leq" : MTMathAtom(type: .relation, value: UnicodeSymbol.lessEqual),
+ "geq" : MTMathAtom(type: .relation, value: UnicodeSymbol.greaterEqual),
+ "leqslant" : MTMathAtom(type: .relation, value: "\u{2A7D}"),
+ "geqslant" : MTMathAtom(type: .relation, value: "\u{2A7E}"),
+ "neq" : MTMathAtom(type: .relation, value: UnicodeSymbol.notEqual),
+ "in" : MTMathAtom(type: .relation, value: "\u{2208}"),
+ "notin" : MTMathAtom(type: .relation, value: "\u{2209}"),
+ "ni" : MTMathAtom(type: .relation, value: "\u{220B}"),
+ "propto" : MTMathAtom(type: .relation, value: "\u{221D}"),
+ "mid" : MTMathAtom(type: .relation, value: "\u{2223}"),
+ "parallel" : MTMathAtom(type: .relation, value: "\u{2225}"),
+ "sim" : MTMathAtom(type: .relation, value: "\u{223C}"),
+ "simeq" : MTMathAtom(type: .relation, value: "\u{2243}"),
+ "cong" : MTMathAtom(type: .relation, value: "\u{2245}"),
+ "approx" : MTMathAtom(type: .relation, value: "\u{2248}"),
+ "asymp" : MTMathAtom(type: .relation, value: "\u{224D}"),
+ "doteq" : MTMathAtom(type: .relation, value: "\u{2250}"),
+ "equiv" : MTMathAtom(type: .relation, value: "\u{2261}"),
+ "gg" : MTMathAtom(type: .relation, value: "\u{226B}"),
+ "ll" : MTMathAtom(type: .relation, value: "\u{226A}"),
+ "prec" : MTMathAtom(type: .relation, value: "\u{227A}"),
+ "succ" : MTMathAtom(type: .relation, value: "\u{227B}"),
+ "preceq" : MTMathAtom(type: .relation, value: "\u{2AAF}"),
+ "succeq" : MTMathAtom(type: .relation, value: "\u{2AB0}"),
+ "subset" : MTMathAtom(type: .relation, value: "\u{2282}"),
+ "supset" : MTMathAtom(type: .relation, value: "\u{2283}"),
+ "subseteq" : MTMathAtom(type: .relation, value: "\u{2286}"),
+ "supseteq" : MTMathAtom(type: .relation, value: "\u{2287}"),
+ "sqsubset" : MTMathAtom(type: .relation, value: "\u{228F}"),
+ "sqsupset" : MTMathAtom(type: .relation, value: "\u{2290}"),
+ "sqsubseteq" : MTMathAtom(type: .relation, value: "\u{2291}"),
+ "sqsupseteq" : MTMathAtom(type: .relation, value: "\u{2292}"),
+ "models" : MTMathAtom(type: .relation, value: "\u{22A7}"),
+ "vdash" : MTMathAtom(type: .relation, value: "\u{22A2}"),
+ "dashv" : MTMathAtom(type: .relation, value: "\u{22A3}"),
+ "bowtie" : MTMathAtom(type: .relation, value: "\u{22C8}"),
+ "perp" : MTMathAtom(type: .relation, value: "\u{27C2}"),
+ "implies" : MTMathAtom(type: .relation, value: "\u{27F9}"),
+
+ // Negated relations (amssymb)
+ // Inequality negations
+ "nless" : MTMathAtom(type: .relation, value: "\u{226E}"),
+ "ngtr" : MTMathAtom(type: .relation, value: "\u{226F}"),
+ "nleq" : MTMathAtom(type: .relation, value: "\u{2270}"),
+ "ngeq" : MTMathAtom(type: .relation, value: "\u{2271}"),
+ "nleqslant" : MTMathAtom(type: .relation, value: "\u{2A87}"),
+ "ngeqslant" : MTMathAtom(type: .relation, value: "\u{2A88}"),
+ "lneq" : MTMathAtom(type: .relation, value: "\u{2A87}"),
+ "gneq" : MTMathAtom(type: .relation, value: "\u{2A88}"),
+ "lneqq" : MTMathAtom(type: .relation, value: "\u{2268}"),
+ "gneqq" : MTMathAtom(type: .relation, value: "\u{2269}"),
+ "lnsim" : MTMathAtom(type: .relation, value: "\u{22E6}"),
+ "gnsim" : MTMathAtom(type: .relation, value: "\u{22E7}"),
+ "lnapprox" : MTMathAtom(type: .relation, value: "\u{2A89}"),
+ "gnapprox" : MTMathAtom(type: .relation, value: "\u{2A8A}"),
+
+ // Ordering negations
+ "nprec" : MTMathAtom(type: .relation, value: "\u{2280}"),
+ "nsucc" : MTMathAtom(type: .relation, value: "\u{2281}"),
+ "npreceq" : MTMathAtom(type: .relation, value: "\u{22E0}"),
+ "nsucceq" : MTMathAtom(type: .relation, value: "\u{22E1}"),
+ "precneqq" : MTMathAtom(type: .relation, value: "\u{2AB5}"),
+ "succneqq" : MTMathAtom(type: .relation, value: "\u{2AB6}"),
+ "precnsim" : MTMathAtom(type: .relation, value: "\u{22E8}"),
+ "succnsim" : MTMathAtom(type: .relation, value: "\u{22E9}"),
+ "precnapprox" : MTMathAtom(type: .relation, value: "\u{2AB9}"),
+ "succnapprox" : MTMathAtom(type: .relation, value: "\u{2ABA}"),
+
+ // Similarity/congruence negations
+ "nsim" : MTMathAtom(type: .relation, value: "\u{2241}"),
+ "ncong" : MTMathAtom(type: .relation, value: "\u{2247}"),
+ "nmid" : MTMathAtom(type: .relation, value: "\u{2224}"),
+ "nshortmid" : MTMathAtom(type: .relation, value: "\u{2224}"),
+ "nparallel" : MTMathAtom(type: .relation, value: "\u{2226}"),
+ "nshortparallel" : MTMathAtom(type: .relation, value: "\u{2226}"),
+
+ // Set relation negations
+ "nsubseteq" : MTMathAtom(type: .relation, value: "\u{2288}"),
+ "nsupseteq" : MTMathAtom(type: .relation, value: "\u{2289}"),
+ "subsetneq" : MTMathAtom(type: .relation, value: "\u{228A}"),
+ "supsetneq" : MTMathAtom(type: .relation, value: "\u{228B}"),
+ "subsetneqq" : MTMathAtom(type: .relation, value: "\u{2ACB}"),
+ "supsetneqq" : MTMathAtom(type: .relation, value: "\u{2ACC}"),
+ "varsubsetneq" : MTMathAtom(type: .relation, value: "\u{228A}"),
+ "varsupsetneq" : MTMathAtom(type: .relation, value: "\u{228B}"),
+ "varsubsetneqq" : MTMathAtom(type: .relation, value: "\u{2ACB}"),
+ "varsupsetneqq" : MTMathAtom(type: .relation, value: "\u{2ACC}"),
+ "notni" : MTMathAtom(type: .relation, value: "\u{220C}"),
+ "nni" : MTMathAtom(type: .relation, value: "\u{220C}"),
+
+ // Triangle negations
+ "ntriangleleft" : MTMathAtom(type: .relation, value: "\u{22EA}"),
+ "ntriangleright" : MTMathAtom(type: .relation, value: "\u{22EB}"),
+ "ntrianglelefteq" : MTMathAtom(type: .relation, value: "\u{22EC}"),
+ "ntrianglerighteq" : MTMathAtom(type: .relation, value: "\u{22ED}"),
+
+ // Turnstile negations
+ "nvdash" : MTMathAtom(type: .relation, value: "\u{22AC}"),
+ "nvDash" : MTMathAtom(type: .relation, value: "\u{22AD}"),
+ "nVdash" : MTMathAtom(type: .relation, value: "\u{22AE}"),
+ "nVDash" : MTMathAtom(type: .relation, value: "\u{22AF}"),
+
+ // Square subset negations
+ "nsqsubseteq" : MTMathAtom(type: .relation, value: "\u{22E2}"),
+ "nsqsupseteq" : MTMathAtom(type: .relation, value: "\u{22E3}"),
+
+ // operators
+ "times" : MTMathAtomFactory.times(),
+ "div" : MTMathAtomFactory.divide(),
+ "pm" : MTMathAtom(type: .binaryOperator, value: "\u{00B1}"),
+ "dagger" : MTMathAtom(type: .binaryOperator, value: "\u{2020}"),
+ "ddagger" : MTMathAtom(type: .binaryOperator, value: "\u{2021}"),
+ "mp" : MTMathAtom(type: .binaryOperator, value: "\u{2213}"),
+ "setminus" : MTMathAtom(type: .binaryOperator, value: "\u{2216}"),
+ "ast" : MTMathAtom(type: .binaryOperator, value: "\u{2217}"),
+ "circ" : MTMathAtom(type: .binaryOperator, value: "\u{2218}"),
+ "bullet" : MTMathAtom(type: .binaryOperator, value: "\u{2219}"),
+ "wedge" : MTMathAtom(type: .binaryOperator, value: "\u{2227}"),
+ "vee" : MTMathAtom(type: .binaryOperator, value: "\u{2228}"),
+ "cap" : MTMathAtom(type: .binaryOperator, value: "\u{2229}"),
+ "cup" : MTMathAtom(type: .binaryOperator, value: "\u{222A}"),
+ "wr" : MTMathAtom(type: .binaryOperator, value: "\u{2240}"),
+ "uplus" : MTMathAtom(type: .binaryOperator, value: "\u{228E}"),
+ "sqcap" : MTMathAtom(type: .binaryOperator, value: "\u{2293}"),
+ "sqcup" : MTMathAtom(type: .binaryOperator, value: "\u{2294}"),
+ "oplus" : MTMathAtom(type: .binaryOperator, value: "\u{2295}"),
+ "ominus" : MTMathAtom(type: .binaryOperator, value: "\u{2296}"),
+ "otimes" : MTMathAtom(type: .binaryOperator, value: "\u{2297}"),
+ "oslash" : MTMathAtom(type: .binaryOperator, value: "\u{2298}"),
+ "odot" : MTMathAtom(type: .binaryOperator, value: "\u{2299}"),
+ "star" : MTMathAtom(type: .binaryOperator, value: "\u{22C6}"),
+ "cdot" : MTMathAtom(type: .binaryOperator, value: "\u{22C5}"),
+ "diamond" : MTMathAtom(type: .binaryOperator, value: "\u{22C4}"),
+ "amalg" : MTMathAtom(type: .binaryOperator, value: "\u{2A3F}"),
+
+ // Additional binary operators (amssymb)
+ "ltimes" : MTMathAtom(type: .binaryOperator, value: "\u{22C9}"), // left semidirect product
+ "rtimes" : MTMathAtom(type: .binaryOperator, value: "\u{22CA}"), // right semidirect product
+ "circledast" : MTMathAtom(type: .binaryOperator, value: "\u{229B}"),
+ "circledcirc" : MTMathAtom(type: .binaryOperator, value: "\u{229A}"),
+ "circleddash" : MTMathAtom(type: .binaryOperator, value: "\u{229D}"),
+ "boxdot" : MTMathAtom(type: .binaryOperator, value: "\u{22A1}"),
+ "boxminus" : MTMathAtom(type: .binaryOperator, value: "\u{229F}"),
+ "boxplus" : MTMathAtom(type: .binaryOperator, value: "\u{229E}"),
+ "boxtimes" : MTMathAtom(type: .binaryOperator, value: "\u{22A0}"),
+ "divideontimes" : MTMathAtom(type: .binaryOperator, value: "\u{22C7}"),
+ "dotplus" : MTMathAtom(type: .binaryOperator, value: "\u{2214}"),
+ "lhd" : MTMathAtom(type: .binaryOperator, value: "\u{22B2}"), // left normal subgroup
+ "rhd" : MTMathAtom(type: .binaryOperator, value: "\u{22B3}"), // right normal subgroup
+ "unlhd" : MTMathAtom(type: .binaryOperator, value: "\u{22B4}"), // left normal subgroup or equal
+ "unrhd" : MTMathAtom(type: .binaryOperator, value: "\u{22B5}"), // right normal subgroup or equal
+ "intercal" : MTMathAtom(type: .binaryOperator, value: "\u{22BA}"),
+ "barwedge" : MTMathAtom(type: .binaryOperator, value: "\u{22BC}"),
+ "veebar" : MTMathAtom(type: .binaryOperator, value: "\u{22BB}"),
+ "curlywedge" : MTMathAtom(type: .binaryOperator, value: "\u{22CF}"),
+ "curlyvee" : MTMathAtom(type: .binaryOperator, value: "\u{22CE}"),
+ "doublebarwedge" : MTMathAtom(type: .binaryOperator, value: "\u{2A5E}"),
+ "centerdot" : MTMathAtom(type: .binaryOperator, value: "\u{22C5}"), // alias for cdot
+
+ // No limit operators
+ "log" : MTMathAtomFactory.operatorWithName( "log", limits: false),
+ "lg" : MTMathAtomFactory.operatorWithName( "lg", limits: false),
+ "ln" : MTMathAtomFactory.operatorWithName( "ln", limits: false),
+ "sin" : MTMathAtomFactory.operatorWithName( "sin", limits: false),
+ "arcsin" : MTMathAtomFactory.operatorWithName( "arcsin", limits: false),
+ "sinh" : MTMathAtomFactory.operatorWithName( "sinh", limits: false),
+ "cos" : MTMathAtomFactory.operatorWithName( "cos", limits: false),
+ "arccos" : MTMathAtomFactory.operatorWithName( "arccos", limits: false),
+ "cosh" : MTMathAtomFactory.operatorWithName( "cosh", limits: false),
+ "tan" : MTMathAtomFactory.operatorWithName( "tan", limits: false),
+ "arctan" : MTMathAtomFactory.operatorWithName( "arctan", limits: false),
+ "tanh" : MTMathAtomFactory.operatorWithName( "tanh", limits: false),
+ "cot" : MTMathAtomFactory.operatorWithName( "cot", limits: false),
+ "coth" : MTMathAtomFactory.operatorWithName( "coth", limits: false),
+ "sec" : MTMathAtomFactory.operatorWithName( "sec", limits: false),
+ "csc" : MTMathAtomFactory.operatorWithName( "csc", limits: false),
+ // Additional inverse trig functions
+ "arccot" : MTMathAtomFactory.operatorWithName( "arccot", limits: false),
+ "arcsec" : MTMathAtomFactory.operatorWithName( "arcsec", limits: false),
+ "arccsc" : MTMathAtomFactory.operatorWithName( "arccsc", limits: false),
+ // Additional hyperbolic functions
+ "sech" : MTMathAtomFactory.operatorWithName( "sech", limits: false),
+ "csch" : MTMathAtomFactory.operatorWithName( "csch", limits: false),
+ // Inverse hyperbolic functions
+ "arcsinh" : MTMathAtomFactory.operatorWithName( "arcsinh", limits: false),
+ "arccosh" : MTMathAtomFactory.operatorWithName( "arccosh", limits: false),
+ "arctanh" : MTMathAtomFactory.operatorWithName( "arctanh", limits: false),
+ "arccoth" : MTMathAtomFactory.operatorWithName( "arccoth", limits: false),
+ "arcsech" : MTMathAtomFactory.operatorWithName( "arcsech", limits: false),
+ "arccsch" : MTMathAtomFactory.operatorWithName( "arccsch", limits: false),
+ "arg" : MTMathAtomFactory.operatorWithName( "arg", limits: false),
+ "ker" : MTMathAtomFactory.operatorWithName( "ker", limits: false),
+ "dim" : MTMathAtomFactory.operatorWithName( "dim", limits: false),
+ "hom" : MTMathAtomFactory.operatorWithName( "hom", limits: false),
+ "exp" : MTMathAtomFactory.operatorWithName( "exp", limits: false),
+ "deg" : MTMathAtomFactory.operatorWithName( "deg", limits: false),
+ "mod" : MTMathAtomFactory.operatorWithName("mod", limits: false),
+
+ // Limit operators
+ "lim" : MTMathAtomFactory.operatorWithName( "lim", limits: true),
+ "limsup" : MTMathAtomFactory.operatorWithName( "lim sup", limits: true),
+ "liminf" : MTMathAtomFactory.operatorWithName( "lim inf", limits: true),
+ "max" : MTMathAtomFactory.operatorWithName( "max", limits: true),
+ "min" : MTMathAtomFactory.operatorWithName( "min", limits: true),
+ "sup" : MTMathAtomFactory.operatorWithName( "sup", limits: true),
+ "inf" : MTMathAtomFactory.operatorWithName( "inf", limits: true),
+ "det" : MTMathAtomFactory.operatorWithName( "det", limits: true),
+ "Pr" : MTMathAtomFactory.operatorWithName( "Pr", limits: true),
+ "gcd" : MTMathAtomFactory.operatorWithName( "gcd", limits: true),
+
+ // Large operators
+ "prod" : MTMathAtomFactory.operatorWithName( "\u{220F}", limits: true),
+ "coprod" : MTMathAtomFactory.operatorWithName( "\u{2210}", limits: true),
+ "sum" : MTMathAtomFactory.operatorWithName( "\u{2211}", limits: true),
+ "int" : MTMathAtomFactory.operatorWithName( "\u{222B}", limits: false),
+ "iint" : MTMathAtomFactory.operatorWithName( "\u{222C}", limits: false),
+ "iiint" : MTMathAtomFactory.operatorWithName( "\u{222D}", limits: false),
+ "iiiint" : MTMathAtomFactory.operatorWithName( "\u{2A0C}", limits: false),
+ "oint" : MTMathAtomFactory.operatorWithName( "\u{222E}", limits: false),
+ "bigwedge" : MTMathAtomFactory.operatorWithName( "\u{22C0}", limits: true),
+ "bigvee" : MTMathAtomFactory.operatorWithName( "\u{22C1}", limits: true),
+ "bigcap" : MTMathAtomFactory.operatorWithName( "\u{22C2}", limits: true),
+ "bigcup" : MTMathAtomFactory.operatorWithName( "\u{22C3}", limits: true),
+ "bigodot" : MTMathAtomFactory.operatorWithName( "\u{2A00}", limits: true),
+ "bigoplus" : MTMathAtomFactory.operatorWithName( "\u{2A01}", limits: true),
+ "bigotimes" : MTMathAtomFactory.operatorWithName( "\u{2A02}", limits: true),
+ "biguplus" : MTMathAtomFactory.operatorWithName( "\u{2A04}", limits: true),
+ "bigsqcup" : MTMathAtomFactory.operatorWithName( "\u{2A06}", limits: true),
+
+ // Latex command characters
+ "{" : MTMathAtom(type: .open, value: "{"),
+ "}" : MTMathAtom(type: .close, value: "}"),
+ "$" : MTMathAtom(type: .ordinary, value: "$"),
+ "&" : MTMathAtom(type: .ordinary, value: "&"),
+ "#" : MTMathAtom(type: .ordinary, value: "#"),
+ "%" : MTMathAtom(type: .ordinary, value: "%"),
+ "_" : MTMathAtom(type: .ordinary, value: "_"),
+ " " : MTMathAtom(type: .ordinary, value: " "),
+ "backslash" : MTMathAtom(type: .ordinary, value: "\\"),
+
+ // Punctuation
+ // Note: \colon is different from : which is a relation
+ "colon" : MTMathAtom(type: .punctuation, value: ":"),
+ "cdotp" : MTMathAtom(type: .punctuation, value: "\u{00B7}"),
+
+ // Other symbols
+ "degree" : MTMathAtom(type: .ordinary, value: "\u{00B0}"),
+ "neg" : MTMathAtom(type: .ordinary, value: "\u{00AC}"),
+ "angstrom" : MTMathAtom(type: .ordinary, value: "\u{00C5}"),
+ "aa" : MTMathAtom(type: .ordinary, value: "\u{00E5}"), // NEW å
+ "ae" : MTMathAtom(type: .ordinary, value: "\u{00E6}"), // NEW æ
+ "o" : MTMathAtom(type: .ordinary, value: "\u{00F8}"), // NEW ø
+ "oe" : MTMathAtom(type: .ordinary, value: "\u{0153}"), // NEW œ
+ "ss" : MTMathAtom(type: .ordinary, value: "\u{00DF}"), // NEW ß
+ "cc" : MTMathAtom(type: .ordinary, value: "\u{00E7}"), // NEW ç
+ "CC" : MTMathAtom(type: .ordinary, value: "\u{00C7}"), // NEW Ç
+ "O" : MTMathAtom(type: .ordinary, value: "\u{00D8}"), // NEW Ø
+ "AE" : MTMathAtom(type: .ordinary, value: "\u{00C6}"), // NEW Æ
+ "OE" : MTMathAtom(type: .ordinary, value: "\u{0152}"), // NEW Œ
+ "|" : MTMathAtom(type: .ordinary, value: "\u{2016}"),
+ "vert" : MTMathAtom(type: .ordinary, value: "|"),
+ "ldots" : MTMathAtom(type: .ordinary, value: "\u{2026}"),
+ "prime" : MTMathAtom(type: .ordinary, value: "\u{2032}"),
+ "hbar" : MTMathAtom(type: .ordinary, value: "\u{210F}"),
+ "lbar" : MTMathAtom(type: .ordinary, value: "\u{019B}"), // NEW ƛ
+ "Im" : MTMathAtom(type: .ordinary, value: "\u{2111}"),
+ "ell" : MTMathAtom(type: .ordinary, value: "\u{2113}"),
+ "wp" : MTMathAtom(type: .ordinary, value: "\u{2118}"),
+ "Re" : MTMathAtom(type: .ordinary, value: "\u{211C}"),
+ "mho" : MTMathAtom(type: .ordinary, value: "\u{2127}"),
+ "aleph" : MTMathAtom(type: .ordinary, value: "\u{2135}"),
+ "beth" : MTMathAtom(type: .ordinary, value: "\u{2136}"),
+ "gimel" : MTMathAtom(type: .ordinary, value: "\u{2137}"),
+ "daleth" : MTMathAtom(type: .ordinary, value: "\u{2138}"),
+ "forall" : MTMathAtom(type: .ordinary, value: "\u{2200}"),
+ "exists" : MTMathAtom(type: .ordinary, value: "\u{2203}"),
+ "nexists" : MTMathAtom(type: .ordinary, value: "\u{2204}"),
+ "emptyset" : MTMathAtom(type: .ordinary, value: "\u{2205}"),
+ "varnothing" : MTMathAtom(type: .ordinary, value: "\u{2205}"),
+ "nabla" : MTMathAtom(type: .ordinary, value: "\u{2207}"),
+ "infty" : MTMathAtom(type: .ordinary, value: "\u{221E}"),
+ "angle" : MTMathAtom(type: .ordinary, value: "\u{2220}"),
+ "measuredangle" : MTMathAtom(type: .ordinary, value: "\u{2221}"),
+ "top" : MTMathAtom(type: .ordinary, value: "\u{22A4}"),
+ "bot" : MTMathAtom(type: .ordinary, value: "\u{22A5}"),
+ "vdots" : MTMathAtom(type: .ordinary, value: "\u{22EE}"),
+ "cdots" : MTMathAtom(type: .ordinary, value: "\u{22EF}"),
+ "ddots" : MTMathAtom(type: .ordinary, value: "\u{22F1}"),
+ "triangle" : MTMathAtom(type: .ordinary, value: "\u{25B3}"),
+ "Box" : MTMathAtom(type: .ordinary, value: "\u{25A1}"),
+ "imath" : MTMathAtom(type: .ordinary, value: "\u{0001D6A4}"),
+ "jmath" : MTMathAtom(type: .ordinary, value: "\u{0001D6A5}"),
+ "upquote" : MTMathAtom(type: .ordinary, value: "\u{0027}"),
+ "partial" : MTMathAtom(type: .ordinary, value: "\u{0001D715}"),
+
+ // Spacing
+ "," : MTMathSpace(space: 3),
+ ">" : MTMathSpace(space: 4),
+ ";" : MTMathSpace(space: 5),
+ "!" : MTMathSpace(space: -3),
+ "quad" : MTMathSpace(space: 18), // quad = 1em = 18mu
+ "qquad" : MTMathSpace(space: 36), // qquad = 2em
+
+ // Style
+ "displaystyle" : MTMathStyle(style: .display),
+ "textstyle" : MTMathStyle(style: .text),
+ "scriptstyle" : MTMathStyle(style: .script),
+ "scriptscriptstyle" : MTMathStyle(style: .scriptOfScript),
+ ]
+
+ static var supportedAccentedCharacters: [Character: (String, String)] = [
+ // Acute accents
+ "á": ("acute", "a"), "é": ("acute", "e"), "í": ("acute", "i"),
+ "ó": ("acute", "o"), "ú": ("acute", "u"), "ý": ("acute", "y"),
+
+ // Grave accents
+ "à": ("grave", "a"), "è": ("grave", "e"), "ì": ("grave", "i"),
+ "ò": ("grave", "o"), "ù": ("grave", "u"),
+
+ // Circumflex
+ "â": ("hat", "a"), "ê": ("hat", "e"), "î": ("hat", "i"),
+ "ĵ": ("hat", "j"), // j with circumflex (Esperanto)
+ "ô": ("hat", "o"), "û": ("hat", "u"),
+
+ // Umlaut/dieresis
+ "ä": ("ddot", "a"), "ë": ("ddot", "e"), "ï": ("ddot", "i"),
+ "ö": ("ddot", "o"), "ü": ("ddot", "u"), "ÿ": ("ddot", "y"),
+
+ // Tilde
+ "ã": ("tilde", "a"), "ñ": ("tilde", "n"), "õ": ("tilde", "o"),
+
+ // Special characters
+ "ç": ("cc", ""), "ø": ("o", ""), "å": ("aa", ""), "æ": ("ae", ""),
+ "œ": ("oe", ""), "ß": ("ss", ""),
+ "'": ("upquote", ""), // this may be dangerous in math mode
+
+ // Upper case variants
+ "Á": ("acute", "A"), "É": ("acute", "E"), "Í": ("acute", "I"),
+ "Ó": ("acute", "O"), "Ú": ("acute", "U"), "Ý": ("acute", "Y"),
+ "À": ("grave", "A"), "È": ("grave", "E"), "Ì": ("grave", "I"),
+ "Ò": ("grave", "O"), "Ù": ("grave", "U"),
+ "Â": ("hat", "A"), "Ê": ("hat", "E"), "Î": ("hat", "I"),
+ "Ô": ("hat", "O"), "Û": ("hat", "U"),
+ "Ä": ("ddot", "A"), "Ë": ("ddot", "E"), "Ï": ("ddot", "I"),
+ "Ö": ("ddot", "O"), "Ü": ("ddot", "U"),
+ "Ã": ("tilde", "A"), "Ñ": ("tilde", "N"), "Õ": ("tilde", "O"),
+ "Ç": ("CC", ""),
+ "Ø": ("O", ""),
+ "Å": ("AA", ""),
+ "Æ": ("AE", ""),
+ "Œ": ("OE", ""),
+ ]
+
+ private static let textToLatexLock = NSLock()
+ static var _textToLatexSymbolName: [String: String]? = nil
+ public static var textToLatexSymbolName: [String: String] {
+ get {
+ if self._textToLatexSymbolName == nil {
+ var output = [String: String]()
+ for (key, atom) in Self.supportedLatexSymbols {
+ if atom.nucleus.count == 0 {
+ continue
+ }
+ if let existingText = output[atom.nucleus] {
+ // If there are 2 key for the same symbol, choose one deterministically.
+ if key.count > existingText.count {
+ // Keep the shorter command
+ continue
+ } else if key.count == existingText.count {
+ // If the length is the same, keep the alphabetically first
+ if key.compare(existingText) == .orderedDescending {
+ continue
+ }
+ }
+ }
+ output[atom.nucleus] = key
+ }
+ // protect lazily loading table in a multi-thread concurrent environment
+ textToLatexLock.lock()
+ defer { textToLatexLock.unlock() }
+ if self._textToLatexSymbolName == nil {
+ self._textToLatexSymbolName = output
+ }
+ }
+ return self._textToLatexSymbolName!
+ }
+ // make textToLatexSymbolName readonly (allows internal load)
+ // entries can be lazily added with NSLock protection.
+ // set {
+ // self._textToLatexSymbolName = newValue
+ // }
+ }
+
+ // public static let sharedInstance = MTMathAtomFactory()
+
+ static let fontStyles : [String: MTFontStyle] = [
+ "mathnormal" : .defaultStyle,
+ "mathrm": .roman,
+ "textrm": .roman,
+ "rm": .roman,
+ "mathbf": .bold,
+ "bf": .bold,
+ "textbf": .bold,
+ "mathcal": .caligraphic,
+ "cal": .caligraphic,
+ "mathtt": .typewriter,
+ "texttt": .typewriter,
+ "mathit": .italic,
+ "textit": .italic,
+ "mit": .italic,
+ "mathsf": .sansSerif,
+ "textsf": .sansSerif,
+ "mathfrak": .fraktur,
+ "frak": .fraktur,
+ "mathbb": .blackboard,
+ "mathbfit": .boldItalic,
+ "bm": .boldItalic,
+ "boldsymbol": .boldItalic,
+ "text": .roman,
+ // Note: operatorname is handled specially in MTMathListBuilder to create proper operators
+ ]
+
+ public static func fontStyleWithName(_ fontName:String) -> MTFontStyle? {
+ fontStyles[fontName]
+ }
+
+ public static func fontNameForStyle(_ fontStyle:MTFontStyle) -> String {
+ switch fontStyle {
+ case .defaultStyle: return "mathnormal"
+ case .roman: return "mathrm"
+ case .bold: return "mathbf"
+ case .fraktur: return "mathfrak"
+ case .caligraphic: return "mathcal"
+ case .italic: return "mathit"
+ case .sansSerif: return "mathsf"
+ case .blackboard: return "mathbb"
+ case .typewriter: return "mathtt"
+ case .boldItalic: return "bm"
+ }
+ }
+
+ /// Returns an atom for the multiplication sign (i.e., \times or "*")
+ public static func times() -> MTMathAtom {
+ MTMathAtom(type: .binaryOperator, value: UnicodeSymbol.multiplication)
+ }
+
+ /// Returns an atom for the division sign (i.e., \div or "/")
+ public static func divide() -> MTMathAtom {
+ MTMathAtom(type: .binaryOperator, value: UnicodeSymbol.division)
+ }
+
+ /// Returns an atom which is a placeholder square
+ public static func placeholder() -> MTMathAtom {
+ MTMathAtom(type: .placeholder, value: UnicodeSymbol.whiteSquare)
+ }
+
+ /** Returns a fraction with a placeholder for the numerator and denominator */
+ public static func placeholderFraction() -> MTFraction {
+ let frac = MTFraction()
+ frac.numerator = MTMathList()
+ frac.numerator?.add(placeholder())
+ frac.denominator = MTMathList()
+ frac.denominator?.add(placeholder())
+ return frac
+ }
+
+ /** Returns a square root with a placeholder as the radicand. */
+ public static func placeholderSquareRoot() -> MTRadical {
+ let rad = MTRadical()
+ rad.radicand = MTMathList()
+ rad.radicand?.add(placeholder())
+ return rad
+ }
+
+ /** Returns a radical with a placeholder as the radicand. */
+ public static func placeholderRadical() -> MTRadical {
+ let rad = MTRadical()
+ rad.radicand = MTMathList()
+ rad.degree = MTMathList()
+ rad.radicand?.add(placeholder())
+ rad.degree?.add(placeholder())
+ return rad
+ }
+
+ /// Latin Small Letter Dotless I (U+0131) - base character that can be styled
+ private static let dotlessI: Character = "\u{0131}"
+ /// Latin Small Letter Dotless J (U+0237) - base character that can be styled
+ private static let dotlessJ: Character = "\u{0237}"
+
+ public static func atom(fromAccentedCharacter ch: Character) -> MTMathAtom? {
+ if let symbol = supportedAccentedCharacters[ch] {
+ // first handle any special characters
+ if let atom = atom(forLatexSymbol: symbol.0) {
+ return atom
+ }
+
+ if let accent = MTMathAtomFactory.accent(withName: symbol.0) {
+ // The command is an accent
+ let list = MTMathList()
+ let baseChar = Array(symbol.1)[0]
+ // Use dotless variants for 'i' and 'j' to avoid double dots with accents.
+ // We use the base Latin dotless characters (U+0131, U+0237) rather than
+ // the pre-styled mathematical italic versions (U+1D6A4, U+1D6A5) so that
+ // font style (roman, bold, etc.) is properly applied during rendering.
+ if baseChar == "i" {
+ list.add(MTMathAtom(type: .ordinary, value: String(dotlessI)))
+ } else if baseChar == "j" {
+ list.add(MTMathAtom(type: .ordinary, value: String(dotlessJ)))
+ } else {
+ list.add(atom(forCharacter: baseChar))
+ }
+ accent.innerList = list
+ return accent
+ }
+ }
+ return nil
+ }
+
+ // MARK: -
+ /** Gets the atom with the right type for the given character. If an atom
+ cannot be determined for a given character this returns nil.
+ This function follows latex conventions for assigning types to the atoms.
+ The following characters are not supported and will return nil:
+ - Any non-ascii character.
+ - Any control character or spaces (< 0x21)
+ - Latex control chars: $ % # & ~ '
+ - Chars with special meaning in latex: ^ _ { } \
+ All other characters, including those with accents, will have a non-nil atom returned.
+ */
+ public static func atom(forCharacter ch: Character) -> MTMathAtom? {
+ let chStr = String(ch)
+ switch chStr {
+ case "\u{0410}"..."\u{044F}":
+ // Cyrillic alphabet
+ return MTMathAtom(type: .ordinary, value: chStr)
+ case _ where supportedAccentedCharacters.keys.contains(ch):
+ // support for áéíóúýàèìòùâêîôûäëïöüÿãñõçøåæœß'ÁÉÍÓÚÝÀÈÌÒÙÂÊÎÔÛÄËÏÖÜÃÑÕÇØÅÆŒ
+ return atom(fromAccentedCharacter: ch)
+ case _ where ch.utf32Char < 0x0021 || ch.utf32Char > 0x007E:
+ return nil
+ case "$", "%", "#", "&", "~", "\'", "^", "_", "{", "}", "\\":
+ return nil
+ case "(", "[":
+ return MTMathAtom(type: .open, value: chStr)
+ case ")", "]", "!", "?":
+ return MTMathAtom(type: .close, value: chStr)
+ case ",", ";":
+ return MTMathAtom(type: .punctuation, value: chStr)
+ case "=", ">", "<":
+ return MTMathAtom(type: .relation, value: chStr)
+ case ":":
+ // Math colon is ratio. Regular colon is \colon
+ return MTMathAtom(type: .relation, value: "\u{2236}")
+ case "-":
+ return MTMathAtom(type: .binaryOperator, value: "\u{2212}")
+ case "+", "*":
+ return MTMathAtom(type: .binaryOperator, value: chStr)
+ case ".", "0"..."9":
+ return MTMathAtom(type: .number, value: chStr)
+ case "a"..."z", "A"..."Z":
+ return MTMathAtom(type: .variable, value: chStr)
+ case "\"", "/", "@", "`", "|":
+ return MTMathAtom(type: .ordinary, value: chStr)
+ default:
+ assertionFailure("Unknown ASCII character '\(ch)'. Should have been handled earlier.")
+ return nil
+ }
+ }
+
+ /** Returns a `MTMathList` with one atom per character in the given string. This function
+ does not do any LaTeX conversion or interpretation. It simply uses `atom(forCharacter:)` to
+ convert the characters to atoms. Any character that cannot be converted is ignored. */
+ public static func atomList(for string: String) -> MTMathList {
+ let list = MTMathList()
+ for character in string {
+ if let newAtom = atom(forCharacter: character) {
+ list.add(newAtom)
+ }
+ }
+ return list
+ }
+
+ /** Returns an atom with the right type for a given latex symbol (e.g. theta)
+ If the latex symbol is unknown this will return nil. This supports LaTeX aliases as well.
+ */
+ public static func atom(forLatexSymbol name: String) -> MTMathAtom? {
+ var name = name
+ if let canonicalName = aliases[name] {
+ name = canonicalName
+ }
+ if let atom = supportedLatexSymbols[name] {
+ return atom.copy()
+ }
+ return nil
+ }
+
+ /** Finds the name of the LaTeX symbol name for the given atom. This function is a reverse
+ of the above function. If no latex symbol name corresponds to the atom, then this returns `nil`
+ If nucleus of the atom is empty, then this will return `nil`.
+ Note: This is not an exact reverse of the above in the case of aliases. If an LaTeX alias
+ points to a given symbol, then this function will return the original symbol name and not the
+ alias.
+ Note: This function does not convert MathSpaces to latex command names either.
+ */
+ public static func latexSymbolName(for atom: MTMathAtom) -> String? {
+ guard !atom.nucleus.isEmpty else { return nil }
+ return Self.textToLatexSymbolName[atom.nucleus]
+ }
+
+ /** Define a latex symbol for rendering. This function allows defining custom symbols that are
+ not already present in the default set, or override existing symbols with new meaning.
+ e.g. to define a symbol for "lcm" one can call:
+ `MTMathAtomFactory.add(latexSymbol:"lcm", value:MTMathAtomFactory.operatorWithName("lcm", limits: false))` */
+ public static func add(latexSymbol name: String, value: MTMathAtom) {
+ let _ = Self.textToLatexSymbolName
+ // above force textToLatexSymbolName to initialise first, _textToLatexSymbolName also initialized.
+ // protect lazily loading table in a multi-thread concurrent environment
+ textToLatexLock.lock()
+ defer { textToLatexLock.unlock() }
+ supportedLatexSymbols[name] = value
+ Self._textToLatexSymbolName?[value.nucleus] = name
+ }
+
+ /** Returns a large opertor for the given name. If limits is true, limits are set up on
+ the operator and displayed differently. */
+ public static func operatorWithName(_ name: String, limits: Bool) -> MTLargeOperator {
+ MTLargeOperator(value: name, limits: limits)
+ }
+
+ /** Returns an accent with the given name. The name of the accent is the LaTeX name
+ such as `grave`, `hat` etc. If the name is not a recognized accent name, this
+ returns nil. The `innerList` of the returned `MTAccent` is nil.
+ */
+ public static func accent(withName name: String) -> MTAccent? {
+ if let accentValue = accents[name] {
+ let accent = MTAccent(value: accentValue)
+ // Mark stretchy arrow accents (\overleftarrow, \overrightarrow, \overleftrightarrow)
+ // These should stretch to match content width
+ // \vec is NOT stretchy - it should use a small fixed-size arrow
+ let stretchyAccents: Set = ["overleftarrow", "overrightarrow", "overleftrightarrow"]
+ accent.isStretchy = stretchyAccents.contains(name)
+
+ // Mark wide accents (\widehat, \widetilde, \widecheck)
+ // These should stretch horizontally to cover content width
+ // \hat, \tilde, \check are NOT wide - they use fixed-size accents
+ let wideAccents: Set = ["widehat", "widetilde", "widecheck"]
+ accent.isWide = wideAccents.contains(name)
+
+ return accent
+ }
+ return nil
+ }
+
+ /** Returns the accent name for the given accent. This is the reverse of the above
+ function. */
+ public static func accentName(_ accent: MTAccent) -> String? {
+ accentValueToName[accent.nucleus]
+ }
+
+ /** Creates a new boundary atom for the given delimiter name. If the delimiter name
+ is not recognized it returns nil. A delimiter name can be a single character such
+ as '(' or a latex command such as 'uparrow'.
+ @note In order to distinguish between the delimiter '|' and the delimiter '\|' the delimiter '\|'
+ the has been renamed to '||'.
+ */
+ public static func boundary(forDelimiter name: String) -> MTMathAtom? {
+ if let delimValue = Self.delimiters[name] {
+ return MTMathAtom(type: .boundary, value: delimValue)
+ }
+ return nil
+ }
+
+ /** Returns the delimiter name for a boundary atom. This is a reverse of the above function.
+ If the atom is not a boundary atom or if the delimiter value is unknown this returns `nil`.
+ @note This is not an exact reverse of the above function. Some delimiters have two names (e.g.
+ `<` and `langle`) and this function always returns the shorter name.
+ */
+ public static func getDelimiterName(of boundary: MTMathAtom) -> String? {
+ guard boundary.type == .boundary else { return nil }
+ return Self.delimValueToName[boundary.nucleus]
+ }
+
+ /** Returns a fraction with the given numerator and denominator. */
+ public static func fraction(withNumerator num: MTMathList, denominator denom: MTMathList) -> MTFraction {
+ let frac = MTFraction()
+ frac.numerator = num
+ frac.denominator = denom
+ return frac
+ }
+
+ public static func mathListForCharacters(_ chars:String) -> MTMathList? {
+ let list = MTMathList()
+ for ch in chars {
+ if let atom = self.atom(forCharacter: ch) {
+ list.add(atom)
+ }
+ }
+ return list
+ }
+
+ /** Simplification of above function when numerator and denominator are simple strings.
+ This function converts the strings to a `MTFraction`. */
+ public static func fraction(withNumeratorString numStr: String, denominatorString denomStr: String) -> MTFraction {
+ let num = Self.atomList(for: numStr)
+ let denom = Self.atomList(for: denomStr)
+ return Self.fraction(withNumerator: num, denominator: denom)
+ }
+
+
+ static let matrixEnvs = [
+ "matrix": [],
+ "pmatrix": ["(", ")"],
+ "bmatrix": ["[", "]"],
+ "Bmatrix": ["{", "}"],
+ "vmatrix": ["vert", "vert"],
+ "Vmatrix": ["Vert", "Vert"],
+ "smallmatrix": [],
+ // Starred versions with optional alignment
+ "matrix*": [],
+ "pmatrix*": ["(", ")"],
+ "bmatrix*": ["[", "]"],
+ "Bmatrix*": ["{", "}"],
+ "vmatrix*": ["vert", "vert"],
+ "Vmatrix*": ["Vert", "Vert"]
+ ]
+
+ /** Builds a table for a given environment with the given rows. Returns a `MTMathAtom` containing the
+ table and any other atoms necessary for the given environment. Returns nil and sets error
+ if the table could not be built.
+ @param env The environment to use to build the table. If the env is nil, then the default table is built.
+ @note The reason this function returns a `MTMathAtom` and not a `MTMathTable` is because some
+ matrix environments are have builtin delimiters added to the table and hence are returned as inner atoms.
+
+ Column constraints by environment (matching KaTeX behavior):
+ - `aligned`, `eqalign`: Any number of columns (1, 2, 3, 4+) with r-l-r-l alignment pattern
+ - `split`: Maximum 2 columns with r-l alignment
+ - `gather`, `displaylines`: Exactly 1 column, centered
+ - `cases`: 1 or 2 columns, left-aligned
+ - `eqnarray`: Exactly 3 columns with r-c-l alignment
+ */
+ public static func table(withEnvironment env: String?, alignment: MTColumnAlignment? = nil, rows: [[MTMathList]], error:inout NSError?) -> MTMathAtom? {
+ let table = MTMathTable(environment: env)
+
+ for i in 0.. 2 {
+ let message = "split environment can have at most 2 columns"
+ if error == nil {
+ error = NSError(domain: MTParseError, code: MTParseErrors.invalidNumColumns.rawValue, userInfo: [NSLocalizedDescriptionKey:message])
+ }
+ return nil
+ }
+
+ let spacer = MTMathAtom(type: .ordinary, value: "")
+
+ // Add spacer at beginning of odd-indexed columns (1, 3, 5, ...)
+ // This matches KaTeX behavior for binary operator spacing
+ for i in 0.. CGSize {
+ CGSize(width: displayList.width + contentInsets.left + contentInsets.right,
+ height: displayList.ascent + displayList.descent + contentInsets.top + contentInsets.bottom)
+ }
+ public func asImage() -> (NSError?, MTImage?) {
+ func layoutImage(size: CGSize, displayList: MTMathListDisplay) {
+ var textX = CGFloat(0)
+ switch self.textAlignment {
+ case .left: textX = contentInsets.left
+ case .center: textX = (size.width - contentInsets.left - contentInsets.right - displayList.width) / 2 + contentInsets.left
+ case .right: textX = size.width - displayList.width - contentInsets.right
+ }
+ let availableHeight = size.height - contentInsets.bottom - contentInsets.top
+
+ // center things vertically
+ var height = displayList.ascent + displayList.descent
+ if height < fontSize/2 {
+ height = fontSize/2 // set height to half the font size
+ }
+ let textY = (availableHeight - height) / 2 + displayList.descent + contentInsets.bottom
+ displayList.position = CGPoint(x: textX, y: textY)
+ }
+
+ var error: NSError?
+ guard let mathList = MTMathListBuilder.build(fromString: latex, error: &error), error == nil,
+ let displayList = MTTypesetter.createLineForMathList(mathList, font: font, style: currentStyle) else {
+ return (error, nil)
+ }
+
+ intrinsicContentSize = intrinsicContentSize(displayList)
+ displayList.textColor = textColor
+
+ let size = intrinsicContentSize
+ layoutImage(size: size, displayList: displayList)
+
+ #if os(iOS) || os(visionOS)
+ let renderer = UIGraphicsImageRenderer(size: size)
+ let image = renderer.image { rendererContext in
+ rendererContext.cgContext.saveGState()
+ rendererContext.cgContext.concatenate(.flippedVertically(size.height))
+ displayList.draw(rendererContext.cgContext)
+ rendererContext.cgContext.restoreGState()
+ }
+ return (nil, image)
+ #endif
+ #if os(macOS)
+ let image = NSImage(size: size, flipped: false) { bounds in
+ guard let context = NSGraphicsContext.current?.cgContext else { return false }
+ context.saveGState()
+ displayList.draw(context)
+ context.restoreGState()
+ return true
+ }
+ return (nil, image)
+ #endif
+ }
+}
+private extension CGAffineTransform {
+ static func flippedVertically(_ height: CGFloat) -> CGAffineTransform {
+ var transform = CGAffineTransform(scaleX: 1, y: -1)
+ transform = transform.translatedBy(x: 0, y: -height)
+ return transform
+ }
+}
diff --git a/third-party/SwiftMath/Sources/MathRender/MTMathList.swift b/third-party/SwiftMath/Sources/MathRender/MTMathList.swift
new file mode 100644
index 0000000000..9feb02db5c
--- /dev/null
+++ b/third-party/SwiftMath/Sources/MathRender/MTMathList.swift
@@ -0,0 +1,1058 @@
+//
+// Created by Mike Griebling on 2022-12-31.
+// Translated from an Objective-C implementation by Kostub Deshmukh.
+//
+// This software may be modified and distributed under the terms of the
+// MIT license. See the LICENSE file for details.
+//
+
+import Foundation
+
+/**
+ The type of atom in a `MTMathList`.
+
+ The type of the atom determines how it is rendered, and spacing between the atoms.
+ */
+public enum MTMathAtomType: Int, CustomStringConvertible, Comparable {
+ /// A number or text in ordinary format - Ord in TeX
+ case ordinary = 1
+ /// A number - Does not exist in TeX
+ case number
+ /// A variable (i.e. text in italic format) - Does not exist in TeX
+ case variable
+ /// A large operator such as (sin/cos, integral etc.) - Op in TeX
+ case largeOperator
+ /// A binary operator - Bin in TeX
+ case binaryOperator
+ /// A unary operator - Does not exist in TeX.
+ case unaryOperator
+ /// A relation, e.g. = > < etc. - Rel in TeX
+ case relation
+ /// Open brackets - Open in TeX
+ case open
+ /// Close brackets - Close in TeX
+ case close
+ /// A fraction e.g 1/2 - generalized fraction node in TeX
+ case fraction
+ /// A radical operator e.g. sqrt(2)
+ case radical
+ /// Punctuation such as , - Punct in TeX
+ case punctuation
+ /// A placeholder square for future input. Does not exist in TeX
+ case placeholder
+ /// An inner atom, i.e. an embedded math list - Inner in TeX
+ case inner
+ /// An underlined atom - Under in TeX
+ case underline
+ /// An overlined atom - Over in TeX
+ case overline
+ /// An accented atom - Accent in TeX
+ case accent
+
+ // Atoms after this point do not support subscripts or superscripts
+
+ /// A left atom - Left & Right in TeX. We don't need two since we track boundaries separately.
+ case boundary = 101
+
+ // Atoms after this are non-math TeX nodes that are still useful in math mode. They do not have
+ // the usual structure.
+
+ /// Spacing between math atoms. This denotes both glue and kern for TeX. We do not
+ /// distinguish between glue and kern.
+ case space = 201
+
+ /// Denotes style changes during rendering.
+ case style
+ case color
+ case textcolor
+ case colorBox
+
+ // Atoms after this point are not part of TeX and do not have the usual structure.
+
+ /// An table atom. This atom does not exist in TeX. It is equivalent to the TeX command
+ /// halign which is handled outside of the TeX math rendering engine. We bring it into our
+ /// math typesetting to handle matrices and other tables.
+ case table = 1001
+
+ func isNotBinaryOperator() -> Bool {
+ switch self {
+ case .binaryOperator, .relation, .open, .punctuation, .largeOperator: return true
+ default: return false
+ }
+ }
+
+ func isScriptAllowed() -> Bool { self < .boundary }
+
+ // we want string representations to be capitalized
+ public var description: String {
+ switch self {
+ case .ordinary: return "Ordinary"
+ case .number: return "Number"
+ case .variable: return "Variable"
+ case .largeOperator: return "Large Operator"
+ case .binaryOperator: return "Binary Operator"
+ case .unaryOperator: return "Unary Operator"
+ case .relation: return "Relation"
+ case .open: return "Open"
+ case .close: return "Close"
+ case .fraction: return "Fraction"
+ case .radical: return "Radical"
+ case .punctuation: return "Punctuation"
+ case .placeholder: return "Placeholder"
+ case .inner: return "Inner"
+ case .underline: return "Underline"
+ case .overline: return "Overline"
+ case .accent: return "Accent"
+ case .boundary: return "Boundary"
+ case .space: return "Space"
+ case .style: return "Style"
+ case .color: return "Color"
+ case .textcolor: return "TextColor"
+ case .colorBox: return "Colorbox"
+ case .table: return "Table"
+ }
+ }
+
+ // comparable support
+ public static func < (lhs: MTMathAtomType, rhs: MTMathAtomType) -> Bool { lhs.rawValue < rhs.rawValue }
+}
+
+/**
+ The font style of a character.
+
+ The fontstyle of the atom determines what style the character is rendered in. This only applies to atoms
+ of type kMTMathAtomVariable and kMTMathAtomNumber. None of the other atom types change their font style.
+ */
+public enum MTFontStyle:Int {
+ /// The default latex rendering style. i.e. variables are italic and numbers are roman.
+ case defaultStyle = 0,
+ /// Roman font style i.e. \mathrm
+ roman,
+ /// Bold font style i.e. \mathbf
+ bold,
+ /// Caligraphic font style i.e. \mathcal
+ caligraphic,
+ /// Typewriter (monospace) style i.e. \mathtt
+ typewriter,
+ /// Italic style i.e. \mathit
+ italic,
+ /// San-serif font i.e. \mathss
+ sansSerif,
+ /// Fractur font i.e \mathfrak
+ fraktur,
+ /// Blackboard font i.e. \mathbb
+ blackboard,
+ /// Bold italic
+ boldItalic
+}
+
+// MARK: - MTMathAtom
+
+/** A `MTMathAtom` is the basic unit of a math list. Each atom represents a single character
+ or mathematical operator in a list. However certain atoms can represent more complex structures
+ such as fractions and radicals. Each atom has a type which determines how the atom is rendered and
+ a nucleus. The nucleus contains the character(s) that need to be rendered. However the nucleus may
+ be empty for certain types of atoms. An atom has an optional subscript or superscript which represents
+ the subscript or superscript that is to be rendered.
+
+ Certain types of atoms inherit from `MTMathAtom` and may have additional fields.
+ */
+public class MTMathAtom: NSObject {
+ /** The type of the atom. */
+ public var type = MTMathAtomType.ordinary
+ /** An optional subscript. */
+ public var subScript: MTMathList? {
+ didSet {
+ if subScript != nil && !self.isScriptAllowed() {
+ subScript = nil
+ NSException(name: NSExceptionName(rawValue: "Error"), reason: "Subscripts not allowed for atom of type \(self.type)").raise()
+ }
+ }
+ }
+ /** An optional superscript. */
+ public var superScript: MTMathList? {
+ didSet {
+ if superScript != nil && !self.isScriptAllowed() {
+ superScript = nil
+ NSException(name: NSExceptionName(rawValue: "Error"), reason: "Superscripts not allowed for atom of type \(self.type)").raise()
+ }
+ }
+ }
+
+ /** The nucleus of the atom. */
+ public var nucleus: String = ""
+
+ /// The index range in the MTMathList this MTMathAtom tracks. This is used by the finalizing and preprocessing steps
+ /// which fuse MTMathAtoms to track the position of the current MTMathAtom in the original list.
+ public var indexRange = NSRange(location: 0, length: 0) // indexRange in list that this atom tracks:
+
+ /** The font style to be used for the atom. */
+ var fontStyle: MTFontStyle = .defaultStyle
+
+ /// If this atom was formed by fusion of multiple atoms, then this stores the list of atoms that were fused to create this one.
+ /// This is used in the finalizing and preprocessing steps.
+ var fusedAtoms = [MTMathAtom]()
+
+ init(_ atom:MTMathAtom?) {
+ guard let atom = atom else { return }
+ self.type = atom.type
+ self.nucleus = atom.nucleus
+ self.subScript = MTMathList(atom.subScript)
+ self.superScript = MTMathList(atom.superScript)
+ self.indexRange = atom.indexRange
+ self.fontStyle = atom.fontStyle
+ self.fusedAtoms = atom.fusedAtoms
+ }
+
+ override init() { }
+
+ /// Factory function to create an atom with a given type and value.
+ /// - parameter type: The type of the atom to instantiate.
+ /// - parameter value: The value of the atoms nucleus. The value is ignored for fractions and radicals.
+ init(type:MTMathAtomType, value:String) {
+ self.type = type
+ self.nucleus = type == .radical ? "" : value
+ }
+
+ /// Returns a copy of `self`.
+ public func copy() -> MTMathAtom {
+ switch self.type {
+ case .largeOperator:
+ return MTLargeOperator(self as? MTLargeOperator)
+ case .fraction:
+ return MTFraction(self as? MTFraction)
+ case .radical:
+ return MTRadical(self as? MTRadical)
+ case .style:
+ return MTMathStyle(self as? MTMathStyle)
+ case .inner:
+ return MTInner(self as? MTInner)
+ case .underline:
+ return MTUnderLine(self as? MTUnderLine)
+ case .overline:
+ return MTOverLine(self as? MTOverLine)
+ case .accent:
+ return MTAccent(self as? MTAccent)
+ case .space:
+ return MTMathSpace(self as? MTMathSpace)
+ case .color:
+ return MTMathColor(self as? MTMathColor)
+ case .textcolor:
+ return MTMathTextColor(self as? MTMathTextColor)
+ case .colorBox:
+ return MTMathColorbox(self as? MTMathColorbox)
+ case .table:
+ return MTMathTable(self as! MTMathTable)
+ default:
+ return MTMathAtom(self)
+ }
+ }
+
+ public override var description: String {
+ var string = ""
+ string += self.nucleus
+ if self.superScript != nil {
+ string += "^{\(self.superScript!.description)}"
+ }
+ if self.subScript != nil {
+ string += "_{\(self.subScript!.description)}"
+ }
+ return string
+ }
+
+ /// Returns a finalized copy of the atom
+ public var finalized: MTMathAtom {
+ let finalized : MTMathAtom = self.copy()
+ finalized.superScript = finalized.superScript?.finalized
+ finalized.subScript = finalized.subScript?.finalized
+ return finalized
+ }
+
+ public var string:String {
+ var str = self.nucleus
+ if let superScript = self.superScript {
+ str.append("^{\(superScript.string)}")
+ }
+ if let subScript = self.subScript {
+ str.append("_{\(subScript.string)}")
+ }
+ return str
+ }
+
+ // Fuse the given atom with this one by combining their nucleii.
+ func fuse(with atom: MTMathAtom) {
+ assert(self.subScript == nil, "Cannot fuse into an atom which has a subscript: \(self)");
+ assert(self.superScript == nil, "Cannot fuse into an atom which has a superscript: \(self)");
+ assert(atom.type == self.type, "Only atoms of the same type can be fused. \(self), \(atom)");
+ guard self.subScript == nil, self.superScript == nil, self.type == atom.type
+ else { return }
+
+ // Update the fused atoms list
+ if self.fusedAtoms.isEmpty {
+ self.fusedAtoms.append(MTMathAtom(self))
+ }
+ if atom.fusedAtoms.count > 0 {
+ self.fusedAtoms.append(contentsOf: atom.fusedAtoms)
+ } else {
+ self.fusedAtoms.append(atom)
+ }
+
+ // Update nucleus:
+ self.nucleus += atom.nucleus
+
+ // Update range:
+ self.indexRange.length += atom.indexRange.length
+
+ // Update super/subscript:
+ self.superScript = atom.superScript
+ self.subScript = atom.subScript
+ }
+
+ /** Returns true if this atom allows scripts (sub or super). */
+ func isScriptAllowed() -> Bool { self.type.isScriptAllowed() }
+
+ func isNotBinaryOperator() -> Bool { self.type.isNotBinaryOperator() }
+
+}
+
+func isNotBinaryOperator(_ prevNode:MTMathAtom?) -> Bool {
+ guard let prevNode = prevNode else { return true }
+ return prevNode.type.isNotBinaryOperator()
+}
+
+// MARK: - MTFraction
+
+public class MTFraction: MTMathAtom {
+ public var hasRule: Bool = true
+ public var leftDelimiter = ""
+ public var rightDelimiter = ""
+ public var numerator: MTMathList?
+ public var denominator: MTMathList?
+
+ // Continued fraction properties
+ public var isContinuedFraction: Bool = false
+ public var alignment: String = "c" // "l", "r", "c" for left, right, center
+
+ init(_ frac: MTFraction?) {
+ super.init(frac)
+ self.type = .fraction
+ if let frac = frac {
+ self.numerator = MTMathList(frac.numerator)
+ self.denominator = MTMathList(frac.denominator)
+ self.hasRule = frac.hasRule
+ self.leftDelimiter = frac.leftDelimiter
+ self.rightDelimiter = frac.rightDelimiter
+ self.isContinuedFraction = frac.isContinuedFraction
+ self.alignment = frac.alignment
+ }
+ }
+
+ init(hasRule rule:Bool = true) {
+ super.init()
+ self.type = .fraction
+ self.hasRule = rule
+ }
+
+ override public var description: String {
+ var string = self.hasRule ? "\\frac" : "\\atop"
+ if !self.leftDelimiter.isEmpty {
+ string += "[\(self.leftDelimiter)]"
+ }
+ if !self.rightDelimiter.isEmpty {
+ string += "[\(self.rightDelimiter)]"
+ }
+ string += "{\(self.numerator?.description ?? "placeholder")}{\(self.denominator?.description ?? "placeholder")}"
+ if self.superScript != nil {
+ string += "^{\(self.superScript!.description)}"
+ }
+ if self.subScript != nil {
+ string += "_{\(self.subScript!.description)}"
+ }
+ return string
+ }
+
+ override public var finalized: MTMathAtom {
+ let newFrac = super.finalized as! MTFraction
+ newFrac.numerator = newFrac.numerator?.finalized
+ newFrac.denominator = newFrac.denominator?.finalized
+ return newFrac
+ }
+
+}
+
+// MARK: - MTRadical
+/** An atom of type radical (square root). */
+public class MTRadical: MTMathAtom {
+ /// Denotes the term under the square root sign
+ public var radicand: MTMathList?
+
+ /// Denotes the degree of the radical, i.e. the value to the top left of the radical sign
+ /// This can be null if there is no degree.
+ public var degree: MTMathList?
+
+ init(_ rad:MTRadical?) {
+ super.init(rad)
+ self.type = .radical
+ self.radicand = MTMathList(rad?.radicand)
+ self.degree = MTMathList(rad?.degree)
+ self.nucleus = ""
+ }
+
+ override init() {
+ super.init()
+ self.type = .radical
+ self.nucleus = ""
+ }
+
+ override public var description: String {
+ var string = "\\sqrt"
+ if self.degree != nil {
+ string += "[\(self.degree!.description)]"
+ }
+ if self.radicand != nil {
+ string += "{\(self.radicand?.description ?? "placeholder")}"
+ }
+ if self.superScript != nil {
+ string += "^{\(self.superScript!.description)}"
+ }
+ if self.subScript != nil {
+ string += "_{\(self.subScript!.description)}"
+ }
+ return string
+ }
+
+ override public var finalized: MTMathAtom {
+ let newRad = super.finalized as! MTRadical
+ newRad.radicand = newRad.radicand?.finalized
+ newRad.degree = newRad.degree?.finalized
+ return newRad
+ }
+}
+
+// MARK: - MTLargeOperator
+/** A `MTMathAtom` of type `kMTMathAtom.largeOperator`. */
+public class MTLargeOperator: MTMathAtom {
+
+ /** Indicates whether the limits (if present) should be displayed
+ above and below the operator in display mode. If limits is false
+ then the limits (if present) are displayed like a regular subscript/superscript.
+ */
+ public var limits: Bool = false
+
+ init(_ op:MTLargeOperator?) {
+ super.init(op)
+ self.type = .largeOperator
+ self.limits = op!.limits
+ }
+
+ init(value: String, limits: Bool) {
+ super.init(type: .largeOperator, value: value)
+ self.limits = limits
+ }
+}
+
+// MARK: - MTInner
+/** An inner atom. This denotes an atom which contains a math list inside it. An inner atom
+ has optional boundaries. Note: Only one boundary may be present, it is not required to have
+ both. */
+public class MTInner: MTMathAtom {
+ /// The inner math list
+ public var innerList: MTMathList?
+ /// The left boundary atom. This must be a node of type kMTMathAtomBoundary
+ public var leftBoundary: MTMathAtom? {
+ didSet {
+ if let left = leftBoundary, left.type != .boundary {
+ leftBoundary = nil
+ NSException(name: NSExceptionName(rawValue: "Error"), reason: "Left boundary must be of type .boundary").raise()
+ }
+ }
+ }
+ /// The right boundary atom. This must be a node of type kMTMathAtomBoundary
+ public var rightBoundary: MTMathAtom? {
+ didSet {
+ if let right = rightBoundary, right.type != .boundary {
+ rightBoundary = nil
+ NSException(name: NSExceptionName(rawValue: "Error"), reason: "Right boundary must be of type .boundary").raise()
+ }
+ }
+ }
+
+ /// Optional explicit delimiter height (in points). When set, this overrides the automatic
+ /// delimiter sizing based on inner content. Used by \big, \Big, \bigg, \Bigg commands.
+ public var delimiterHeight: CGFloat?
+
+ init(_ inner:MTInner?) {
+ super.init(inner)
+ self.type = .inner
+ self.innerList = MTMathList(inner?.innerList)
+ self.leftBoundary = MTMathAtom(inner?.leftBoundary)
+ self.rightBoundary = MTMathAtom(inner?.rightBoundary)
+ self.delimiterHeight = inner?.delimiterHeight
+ }
+
+ override init() {
+ super.init()
+ self.type = .inner
+ }
+
+ override public var description: String {
+ var string = "\\inner"
+ if self.leftBoundary != nil {
+ string += "[\(self.leftBoundary!.nucleus)]"
+ }
+ string += "{\(self.innerList!.description)}"
+ if self.rightBoundary != nil {
+ string += "[\(self.rightBoundary!.nucleus)]"
+ }
+ if self.superScript != nil {
+ string += "^{\(self.superScript!.description)}"
+ }
+ if self.subScript != nil {
+ string += "_{\(self.subScript!.description)}"
+ }
+ return string
+ }
+
+ override public var finalized: MTMathAtom {
+ let newInner = super.finalized as! MTInner
+ newInner.innerList = newInner.innerList?.finalized
+ return newInner
+ }
+}
+
+// MARK: - MTOverLIne
+/** An atom with a line over the contained math list. */
+public class MTOverLine: MTMathAtom {
+ public var innerList: MTMathList?
+
+ override public var finalized: MTMathAtom {
+ let newOverline = MTOverLine(self)
+ newOverline.innerList = newOverline.innerList?.finalized
+ return newOverline
+ }
+
+ init(_ over: MTOverLine?) {
+ super.init(over)
+ self.type = .overline
+ self.innerList = MTMathList(over!.innerList)
+ }
+
+ override init() {
+ super.init()
+ self.type = .overline
+ }
+}
+
+// MARK: - MTUnderLine
+/** An atom with a line under the contained math list. */
+public class MTUnderLine: MTMathAtom {
+ public var innerList: MTMathList?
+
+ override public var finalized: MTMathAtom {
+ let newUnderline = super.finalized as! MTUnderLine
+ newUnderline.innerList = newUnderline.innerList?.finalized
+ return newUnderline
+ }
+
+ init(_ under: MTUnderLine?) {
+ super.init(under)
+ self.type = .underline
+ self.innerList = MTMathList(under?.innerList)
+ }
+
+ override init() {
+ super.init()
+ self.type = .underline
+ }
+}
+
+// MARK: - MTAccent
+
+public class MTAccent: MTMathAtom {
+ public var innerList: MTMathList?
+ /// Indicates if this accent should use stretchy arrow behavior (for \overrightarrow, etc.)
+ /// vs short accent behavior (for \vec). Only applies to arrow accents.
+ public var isStretchy: Bool = false
+ /// Indicates if this accent should use wide stretching behavior (for \widehat, \widetilde)
+ /// vs regular fixed-size accent behavior (for \hat, \tilde).
+ public var isWide: Bool = false
+
+ override public var finalized: MTMathAtom {
+ let newAccent = super.finalized as! MTAccent
+ newAccent.innerList = newAccent.innerList?.finalized
+ newAccent.isStretchy = self.isStretchy
+ newAccent.isWide = self.isWide
+ return newAccent
+ }
+
+ init(_ accent: MTAccent?) {
+ super.init(accent)
+ self.type = .accent
+ self.innerList = MTMathList(accent?.innerList)
+ self.isStretchy = accent?.isStretchy ?? false
+ self.isWide = accent?.isWide ?? false
+ }
+
+ init(value: String) {
+ super.init()
+ self.type = .accent
+ self.nucleus = value
+ }
+}
+
+// MARK: - MTMathSpace
+/** An atom representing space.
+ Note: None of the usual fields of the `MTMathAtom` apply even though this
+ class inherits from `MTMathAtom`. i.e. it is meaningless to have a value
+ in the nucleus, subscript or superscript fields. */
+public class MTMathSpace: MTMathAtom {
+ /** The amount of space represented by this object in mu units. */
+ public var space: CGFloat = 0
+
+ /// Creates a new `MTMathSpace` with the given spacing.
+ /// - parameter space: The amount of space in mu units.
+ init(_ space: MTMathSpace?) {
+ super.init(space)
+ self.type = .space
+ self.space = space?.space ?? 0
+ }
+
+ init(space:CGFloat) {
+ super.init()
+ self.type = .space
+ self.space = space
+ }
+}
+
+/**
+ Styling of a line of math
+ */
+public enum MTLineStyle:Int, Comparable {
+ /// Display style
+ case display
+ /// Text style (inline)
+ case text
+ /// Script style (for sub/super scripts)
+ case script
+ /// Script script style (for scripts of scripts)
+ case scriptOfScript
+
+ public func inc() -> MTLineStyle {
+ let raw = self.rawValue + 1
+ if let style = MTLineStyle(rawValue: raw) { return style }
+ return .display
+ }
+
+ public var isNotScript:Bool { self < .script }
+ public static func < (lhs: MTLineStyle, rhs: MTLineStyle) -> Bool { lhs.rawValue < rhs.rawValue }
+}
+
+// MARK: - MTMathStyle
+/** An atom representing a style change.
+ Note: None of the usual fields of the `MTMathAtom` apply even though this
+ class inherits from `MTMathAtom`. i.e. it is meaningless to have a value
+ in the nucleus, subscript or superscript fields. */
+public class MTMathStyle: MTMathAtom {
+ public var style: MTLineStyle = .display
+
+ init(_ style:MTMathStyle?) {
+ super.init(style)
+ self.type = .style
+ self.style = style!.style
+ }
+
+ init(style:MTLineStyle) {
+ super.init()
+ self.type = .style
+ self.style = style
+ }
+}
+
+// MARK: - MTMathColor
+/** An atom representing an color element.
+ Note: None of the usual fields of the `MTMathAtom` apply even though this
+ class inherits from `MTMathAtom`. i.e. it is meaningless to have a value
+ in the nucleus, subscript or superscript fields. */
+public class MTMathColor: MTMathAtom {
+ public var colorString:String=""
+ public var innerList:MTMathList?
+
+ init(_ color: MTMathColor?) {
+ super.init(color)
+ self.type = .color
+ self.colorString = color?.colorString ?? ""
+ self.innerList = MTMathList(color?.innerList)
+ }
+
+ override init() {
+ super.init()
+ self.type = .color
+ }
+
+ public override var string: String {
+ "\\color{\(self.colorString)}{\(self.innerList!.string)}"
+ }
+
+ override public var finalized: MTMathAtom {
+ let newColor = super.finalized as! MTMathColor
+ newColor.innerList = newColor.innerList?.finalized
+ return newColor
+ }
+}
+
+// MARK: - MTMathTextColor
+/** An atom representing an textcolor element.
+ Note: None of the usual fields of the `MTMathAtom` apply even though this
+ class inherits from `MTMathAtom`. i.e. it is meaningless to have a value
+ in the nucleus, subscript or superscript fields. */
+public class MTMathTextColor: MTMathAtom {
+ public var colorString:String=""
+ public var innerList:MTMathList?
+
+ init(_ color: MTMathTextColor?) {
+ super.init(color)
+ self.type = .textcolor
+ self.colorString = color?.colorString ?? ""
+ self.innerList = MTMathList(color?.innerList)
+ }
+
+ override init() {
+ super.init()
+ self.type = .textcolor
+ }
+
+ public override var string: String {
+ "\\textcolor{\(self.colorString)}{\(self.innerList!.string)}"
+ }
+
+ override public var finalized: MTMathAtom {
+ let newColor = super.finalized as! MTMathTextColor
+ newColor.innerList = newColor.innerList?.finalized
+ return newColor
+ }
+}
+
+// MARK: - MTMathColorbox
+/** An atom representing an colorbox element.
+ Note: None of the usual fields of the `MTMathAtom` apply even though this
+ class inherits from `MTMathAtom`. i.e. it is meaningless to have a value
+ in the nucleus, subscript or superscript fields. */
+public class MTMathColorbox: MTMathAtom {
+ public var colorString=""
+ public var innerList:MTMathList?
+
+ init(_ cbox: MTMathColorbox?) {
+ super.init(cbox)
+ self.type = .colorBox
+ self.colorString = cbox?.colorString ?? ""
+ self.innerList = MTMathList(cbox?.innerList)
+ }
+
+ override init() {
+ super.init()
+ self.type = .colorBox
+ }
+
+ public override var string: String {
+ "\\colorbox{\(self.colorString)}{\(self.innerList!.string)}"
+ }
+
+ override public var finalized: MTMathAtom {
+ let newColor = super.finalized as! MTMathColorbox
+ newColor.innerList = newColor.innerList?.finalized
+ return newColor
+ }
+}
+
+/**
+ Alignment for a column of MTMathTable
+ */
+public enum MTColumnAlignment {
+ case left
+ case center
+ case right
+}
+
+// MARK: - MTMathTable
+/** An atom representing an table element. This atom is not like other
+ atoms and is not present in TeX. We use it to represent the `\halign` command
+ in TeX with some simplifications. This is used for matrices, equation
+ alignments and other uses of multiline environments.
+
+ The cells in the table are represented as a two dimensional array of
+ `MTMathList` objects. The `MTMathList`s could be empty to denote a missing
+ value in the cell. Additionally an array of alignments indicates how each
+ column will be aligned.
+ */
+public class MTMathTable: MTMathAtom {
+ /// The alignment for each column (left, right, center). The default alignment
+ /// for a column (if not set) is center.
+ public var alignments = [MTColumnAlignment]()
+ /// The cells in the table as a two dimensional array.
+ public var cells = [[MTMathList]]()
+ /// The name of the environment that this table denotes.
+ public var environment = ""
+ /// Spacing between each column in mu units.
+ public var interColumnSpacing: CGFloat = 0
+ /// Additional spacing between rows in jots (one jot is 0.3 times font size).
+ /// If the additional spacing is 0, then normal row spacing is used are used.
+ public var interRowAdditionalSpacing: CGFloat = 0
+
+ override public var finalized: MTMathAtom {
+ let table = super.finalized as! MTMathTable
+ for var row in table.cells {
+ for i in 0.. MTColumnAlignment {
+ if self.alignments.count <= col {
+ return MTColumnAlignment.center
+ } else {
+ return self.alignments[col]
+ }
+ }
+
+ public var numColumns: Int {
+ var numberOfCols = 0
+ for row in self.cells {
+ numberOfCols = max(numberOfCols, row.count)
+ }
+ return numberOfCols
+ }
+
+ public var numRows: Int { self.cells.count }
+}
+
+// MARK: - MTMathList
+
+extension MTMathList {
+ public override var description: String { self.atoms.description }
+ /// converts the MTMathList to a string form. Note: This is not the LaTeX form.
+ public var string: String { self.description }
+}
+
+/** A representation of a list of math objects.
+
+ This list can be constructed directly or built with
+ the help of the MTMathListBuilder. It is not required that the mathematics represented make sense
+ (i.e. this can represent something like "x 2 = +". This list can be used for display using MTLine
+ or can be a list of tokens to be used by a parser after finalizedMathList is called.
+
+ Note: This class is for **advanced** usage only.
+ */
+public class MTMathList : NSObject {
+
+ init?(_ list:MTMathList?) {
+ guard let list = list else { return nil }
+ for atom in list.atoms {
+ self.atoms.append(atom.copy())
+ }
+ }
+
+ /// A list of MathAtoms
+ public var atoms = [MTMathAtom]()
+
+ /// Create a new math list as a final expression and update atoms
+ /// by combining like atoms that occur together and converting unary operators to binary operators.
+ /// This function does not modify the current MTMathList
+ public var finalized: MTMathList {
+ let finalizedList = MTMathList()
+ let zeroRange = NSMakeRange(0, 0)
+
+ var prevNode: MTMathAtom? = nil
+ for atom in self.atoms {
+ let newNode = atom.finalized
+
+ if NSEqualRanges(zeroRange, atom.indexRange) {
+ // CRITICAL FIX: Check if prevNode has a valid range location before using it
+ // If location is NSNotFound, treat as if there's no prevNode
+ // This prevents negative overflow when creating NSMakeRange
+ let index: Int
+ if prevNode == nil || prevNode!.indexRange.location == NSNotFound {
+ index = 0
+ } else {
+ // Additional safety: check for potential overflow
+ let location = prevNode!.indexRange.location
+ let length = prevNode!.indexRange.length
+ // If either value is suspicious (negative or too large), reset to 0
+ if location < 0 || length < 0 || location > Int.max - length {
+ index = 0
+ } else {
+ index = location + length
+ }
+ }
+ newNode.indexRange = NSMakeRange(index, 1)
+ }
+
+ switch newNode.type {
+ case .binaryOperator:
+ if isNotBinaryOperator(prevNode) {
+ newNode.type = .unaryOperator
+ }
+ case .relation, .punctuation, .close:
+ if prevNode != nil && prevNode!.type == .binaryOperator {
+ prevNode!.type = .unaryOperator
+ }
+ case .number:
+ if prevNode != nil && prevNode!.type == .number && prevNode!.subScript == nil && prevNode!.superScript == nil {
+ prevNode!.fuse(with: newNode)
+ continue // skip the current node, we are done here.
+ }
+ default: break
+ }
+ finalizedList.add(newNode)
+ prevNode = newNode
+ }
+ if prevNode != nil && prevNode!.type == .binaryOperator {
+ prevNode!.type = .unaryOperator
+ }
+ return finalizedList
+ }
+
+ public init(atoms: [MTMathAtom]) {
+ self.atoms.append(contentsOf: atoms)
+ }
+
+ public init(atom: MTMathAtom) {
+ self.atoms.append(atom)
+ }
+
+ public override init() { super.init() }
+
+ func NSParamException(_ param:Any?) {
+ if param == nil {
+ NSException(name: NSExceptionName(rawValue: "Error"), reason: "Parameter cannot be nil").raise()
+ }
+ }
+
+ func NSIndexException(_ array:[Any], index: Int) {
+ guard !array.indices.contains(index) else { return }
+ NSException(name: NSExceptionName(rawValue: "Error"), reason: "Index \(index) out of bounds").raise()
+ }
+
+ /// Add an atom to the end of the list.
+ /// - parameter atom: The atom to be inserted. This cannot be `nil` and cannot have the type `kMTMathAtomBoundary`.
+ /// - throws NSException if the atom is of type `kMTMathAtomBoundary`
+ public func add(_ atom: MTMathAtom?) {
+ guard let atom = atom else { return }
+ if self.isAtomAllowed(atom) {
+ self.atoms.append(atom)
+ } else {
+ NSException(name: NSExceptionName(rawValue: "Error"), reason: "Cannot add atom of type \(atom.type.rawValue) into mathlist").raise()
+ }
+ }
+
+ /// Inserts an atom at the given index. If index is already occupied, the objects at index and beyond are
+ /// shifted by adding 1 to their indices to make room. An insert to an `index` greater than the number of atoms
+ /// is ignored. Insertions of nil atoms is ignored.
+ /// - parameter atom: The atom to be inserted. This cannot be `nil` and cannot have the type `kMTMathAtom.boundary`.
+ /// - parameter index: The index where the atom is to be inserted. The index should be less than or equal to the
+ /// number of elements in the math list.
+ /// - throws NSException if the atom is of type kMTMathAtomBoundary
+ public func insert(_ atom: MTMathAtom?, at index: Int) {
+ // NSParamException(atom)
+ guard let atom = atom else { return }
+ guard self.atoms.indices.contains(index) || index == self.atoms.endIndex else { return }
+ // guard self.atoms.endIndex >= index else { NSIndexException(); return }
+ if self.isAtomAllowed(atom) {
+ // NSIndexException(self.atoms, index: index)
+ self.atoms.insert(atom, at: index)
+ } else {
+ NSException(name: NSExceptionName(rawValue: "Error"), reason: "Cannot add atom of type \(atom.type.rawValue) into mathlist").raise()
+ }
+ }
+
+ /// Append the given list to the end of the current list.
+ /// - parameter list: The list to append.
+ public func append(_ list: MTMathList?) {
+ guard let list = list else { return }
+ self.atoms += list.atoms
+ }
+
+ /** Removes the last atom from the math list. If there are no atoms in the list this does nothing. */
+ public func removeLastAtom() {
+ if !self.atoms.isEmpty {
+ self.atoms.removeLast()
+ }
+ }
+
+ /// Removes the atom at the given index.
+ /// - parameter index: The index at which to remove the atom. Must be less than the number of atoms
+ /// in the list.
+ public func removeAtom(at index: Int) {
+ NSIndexException(self.atoms, index:index)
+ self.atoms.remove(at: index)
+ }
+
+ /** Removes all the atoms within the given range. */
+ public func removeAtoms(in range: ClosedRange) {
+ NSIndexException(self.atoms, index: range.lowerBound)
+ NSIndexException(self.atoms, index: range.upperBound)
+ self.atoms.removeSubrange(range)
+ }
+
+ func isAtomAllowed(_ atom: MTMathAtom?) -> Bool { atom?.type != .boundary }
+}
diff --git a/third-party/SwiftMath/Sources/MathRender/MTMathListBuilder.swift b/third-party/SwiftMath/Sources/MathRender/MTMathListBuilder.swift
new file mode 100644
index 0000000000..4083fd90d9
--- /dev/null
+++ b/third-party/SwiftMath/Sources/MathRender/MTMathListBuilder.swift
@@ -0,0 +1,1613 @@
+//
+// Created by Mike Griebling on 2022-12-31.
+// Translated from an Objective-C implementation by Kostub Deshmukh.
+//
+// This software may be modified and distributed under the terms of the
+// MIT license. See the LICENSE file for details.
+//
+
+import Foundation
+
+/** `MTMathListBuilder` is a class for parsing LaTeX into an `MTMathList` that
+ can be rendered and processed mathematically.
+ */
+struct MTEnvProperties {
+ var envName: String?
+ var ended: Bool
+ var numRows: Int
+ var alignment: MTColumnAlignment? // Optional alignment for starred matrix environments
+
+ init(name: String?, alignment: MTColumnAlignment? = nil) {
+ self.envName = name
+ self.numRows = 0
+ self.ended = false
+ self.alignment = alignment
+ }
+}
+
+/**
+ The error encountered when parsing a LaTeX string.
+
+ The `code` in the `NSError` is one of the following indicating why the LaTeX string
+ could not be parsed.
+ */
+enum MTParseErrors:Int {
+ /// The braces { } do not match.
+ case mismatchBraces = 1
+ /// A command in the string is not recognized.
+ case invalidCommand
+ /// An expected character such as ] was not found.
+ case characterNotFound
+ /// The \left or \right command was not followed by a delimiter.
+ case missingDelimiter
+ /// The delimiter following \left or \right was not a valid delimiter.
+ case invalidDelimiter
+ /// There is no \right corresponding to the \left command.
+ case missingRight
+ /// There is no \left corresponding to the \right command.
+ case missingLeft
+ /// The environment given to the \begin command is not recognized
+ case invalidEnv
+ /// A command is used which is only valid inside a \begin,\end environment
+ case missingEnv
+ /// There is no \begin corresponding to the \end command.
+ case missingBegin
+ /// There is no \end corresponding to the \begin command.
+ case missingEnd
+ /// The number of columns do not match the environment
+ case invalidNumColumns
+ /// Internal error, due to a programming mistake.
+ case internalError
+ /// Limit control applied incorrectly
+ case invalidLimits
+}
+
+let MTParseError = "ParseError"
+
+/** `MTMathListBuilder` is a class for parsing LaTeX into an `MTMathList` that
+ can be rendered and processed mathematically.
+ */
+public struct MTMathListBuilder {
+ /// The math mode determines rendering style (inline vs display)
+ enum MathMode {
+ /// Display style - larger operators, limits above/below (e.g., $$...$$, \[...\])
+ case display
+ /// Inline/text style - compact operators, limits to the side (e.g., $...$, \(...\))
+ case inline
+
+ /// Convert MathMode to MTLineStyle for rendering
+ func toLineStyle() -> MTLineStyle {
+ switch self {
+ case .display:
+ return .display
+ case .inline:
+ return .text
+ }
+ }
+ }
+
+ var string: String
+ var currentCharIndex: String.Index
+ var currentInnerAtom: MTInner?
+ var currentEnv: MTEnvProperties?
+ var currentFontStyle:MTFontStyle
+ var spacesAllowed:Bool
+ var mathMode: MathMode = .display
+
+ /** Contains any error that occurred during parsing. */
+ var error:NSError?
+
+ // MARK: - Character-handling routines
+
+ var hasCharacters: Bool { currentCharIndex < string.endIndex }
+
+ // gets the next character and increments the index
+ mutating func getNextCharacter() -> Character {
+ assert(self.hasCharacters, "Retrieving character at index \(self.currentCharIndex) beyond length \(self.string.count)")
+ let ch = string[currentCharIndex]
+ currentCharIndex = string.index(after: currentCharIndex)
+ return ch
+ }
+
+ mutating func unlookCharacter() {
+ assert(currentCharIndex > string.startIndex, "Unlooking when at the first character.")
+ if currentCharIndex > string.startIndex {
+ currentCharIndex = string.index(before: currentCharIndex)
+ }
+ }
+
+ // Peek at next command without consuming it (for \not lookahead)
+ mutating func peekNextCommand() -> String {
+ let savedIndex = currentCharIndex
+ skipSpaces()
+
+ guard hasCharacters else {
+ currentCharIndex = savedIndex
+ return ""
+ }
+
+ let char = getNextCharacter()
+ let command: String
+
+ if char == "\\" {
+ command = readCommand()
+ } else {
+ command = ""
+ }
+
+ // Restore position
+ currentCharIndex = savedIndex
+ return command
+ }
+
+ // Consume the next command (after peeking)
+ mutating func consumeNextCommand() {
+ skipSpaces()
+
+ guard hasCharacters else { return }
+
+ let char = getNextCharacter()
+ if char == "\\" {
+ _ = readCommand()
+ }
+ }
+
+
+
+ mutating func expectCharacter(_ ch: Character) -> Bool {
+ MTAssertNotSpace(ch)
+ self.skipSpaces()
+
+ if self.hasCharacters {
+ let nextChar = self.getNextCharacter()
+ MTAssertNotSpace(nextChar)
+ if nextChar == ch {
+ return true
+ } else {
+ self.unlookCharacter()
+ return false
+ }
+ }
+ return false
+ }
+
+ public static let spaceToCommands: [CGFloat: String] = [
+ 3 : ",",
+ 4 : ">",
+ 5 : ";",
+ (-3) : "!",
+ 18 : "quad",
+ 36 : "qquad",
+ ]
+
+ public static let styleToCommands: [MTLineStyle: String] = [
+ .display: "displaystyle",
+ .text: "textstyle",
+ .script: "scriptstyle",
+ .scriptOfScript: "scriptscriptstyle"
+ ]
+
+ // Comprehensive mapping of \not command combinations to Unicode negated symbols
+ public static let notCombinations: [String: String] = [
+ // Primary targets (user requested)
+ "equiv": "\u{2262}", // ≢ Not equivalent
+ "subset": "\u{2284}", // ⊄ Not subset
+ "in": "\u{2209}", // ∉ Not element of
+
+ // Additional standard negations
+ "sim": "\u{2241}", // ≁ Not similar
+ "approx": "\u{2249}", // ≉ Not approximately equal
+ "cong": "\u{2247}", // ≇ Not congruent
+ "parallel": "\u{2226}", // ∦ Not parallel
+ "subseteq": "\u{2288}", // ⊈ Not subset or equal
+ "supset": "\u{2285}", // ⊅ Not superset
+ "supseteq": "\u{2289}", // ⊉ Not superset or equal
+ "=": "\u{2260}", // ≠ Not equal (alternative to \neq)
+ ]
+
+ /// Delimiter sizing commands with their size multipliers (relative to font size).
+ /// Values based on standard TeX: at 10pt, \big=8.5pt, \Big=11.5pt, \bigg=14.5pt, \Bigg=17.5pt
+ /// These translate to approximately 0.85x, 1.15x, 1.45x, 1.75x of font size.
+ /// We use slightly larger values to ensure visible size differences.
+ public static let delimiterSizeCommands: [String: CGFloat] = [
+ // Basic sizing commands
+ "big": 1.0,
+ "Big": 1.4,
+ "bigg": 1.8,
+ "Bigg": 2.2,
+ // Left variants (same sizes, just semantic distinction in LaTeX)
+ "bigl": 1.0,
+ "Bigl": 1.4,
+ "biggl": 1.8,
+ "Biggl": 2.2,
+ // Right variants
+ "bigr": 1.0,
+ "Bigr": 1.4,
+ "biggr": 1.8,
+ "Biggr": 2.2,
+ // Middle variants (used between delimiters)
+ "bigm": 1.0,
+ "Bigm": 1.4,
+ "biggm": 1.8,
+ "Biggm": 2.2,
+ ]
+
+ init(string: String) {
+ self.error = nil
+ self.string = string
+ self.currentCharIndex = string.startIndex
+ self.currentFontStyle = .defaultStyle
+ self.spacesAllowed = false
+ }
+
+ // MARK: - Delimiter Detection
+
+ /// Detects and strips LaTeX math delimiters from the input string.
+ /// Returns the cleaned content and the detected math mode.
+ /// Supports: $...$ \(...\) $$...$$ \[...\] and environments
+ func detectAndStripDelimiters(from str: String) -> (String, MathMode) {
+ let trimmed = str.trimmingCharacters(in: .whitespacesAndNewlines)
+
+ // Check display delimiters first (more specific patterns)
+
+ // \[...\] - LaTeX display math
+ if trimmed.hasPrefix("\\[") && trimmed.hasSuffix("\\]") && trimmed.count > 4 {
+ let content = String(trimmed.dropFirst(2).dropLast(2))
+ return (content, .display)
+ }
+
+ // $$...$$ - TeX display math (check before single $)
+ if trimmed.hasPrefix("$$") && trimmed.hasSuffix("$$") && trimmed.count > 4 {
+ let content = String(trimmed.dropFirst(2).dropLast(2))
+ return (content, .display)
+ }
+
+ // Check inline delimiters
+
+ // \(...\) - LaTeX inline math
+ if trimmed.hasPrefix("\\(") && trimmed.hasSuffix("\\)") && trimmed.count > 4 {
+ let content = String(trimmed.dropFirst(2).dropLast(2))
+ return (content, .inline)
+ }
+
+ // $...$ - TeX inline math (must check after $$)
+ if trimmed.hasPrefix("$") && trimmed.hasSuffix("$") && trimmed.count > 2 && !trimmed.hasPrefix("$$") {
+ let content = String(trimmed.dropFirst(1).dropLast(1))
+ return (content, .inline)
+ }
+
+ // Check if it's an environment (\begin{...}\end{...})
+ // These are handled by existing logic and are display mode by default
+ if trimmed.hasPrefix("\\begin{") {
+ return (str, .display)
+ }
+
+ // No delimiters found - default to display mode (current behavior for backward compatibility)
+ return (str, .display)
+ }
+
+ // MARK: - MTMathList builder functions
+
+ /// Builds a mathlist from the internal `string`. Returns nil if there is an error.
+ public mutating func build() -> MTMathList? {
+ // Detect and strip delimiters, updating the string and mode
+ let (cleanedString, mode) = detectAndStripDelimiters(from: self.string)
+ self.string = cleanedString
+ self.currentCharIndex = cleanedString.startIndex
+ self.mathMode = mode
+
+ // If inline mode, we could optionally prepend a \textstyle command
+ // to force inline rendering of operators. For now, just track the mode.
+
+ let list = self.buildInternal(false)
+ if self.hasCharacters && error == nil {
+ self.setError(.mismatchBraces, message: "Mismatched braces: \(self.string)")
+ return nil
+ }
+ if error != nil {
+ return nil
+ }
+
+ // Note: For inline mode, we insert \textstyle to match LaTeX behavior.
+ // However, fractionStyle() has been modified to keep fractions at the
+ // same font size in both display and text modes (not one level smaller).
+ // Large operators show limits above/below in text style due to the updated
+ // condition in makeLargeOp() that checks both .display and .text styles.
+ if mode == .inline && list != nil && !list!.atoms.isEmpty {
+ // Prepend \textstyle to force inline rendering
+ let styleAtom = MTMathStyle(style: .text)
+ list!.atoms.insert(styleAtom, at: 0)
+ }
+
+ return list
+ }
+
+ /** Construct a math list from a given string. If there is parse error, returns
+ nil. To retrieve the error use the function `MTMathListBuilder.build(fromString:error:)`.
+ */
+ public static func build(fromString string: String) -> MTMathList? {
+ var builder = MTMathListBuilder(string: string)
+ return builder.build()
+ }
+
+ /** Construct a math list from a given string. If there is an error while
+ constructing the string, this returns nil. The error is returned in the
+ `error` parameter.
+ */
+ public static func build(fromString string: String, error:inout NSError?) -> MTMathList? {
+ var builder = MTMathListBuilder(string: string)
+ let output = builder.build()
+ if builder.error != nil {
+ error = builder.error
+ return nil
+ }
+ return output
+ }
+
+ /** Construct a math list from a given string and return the detected style.
+ This method detects LaTeX delimiters like \[...\], $$...$$, $...$, \(...\)
+ and returns the appropriate rendering style (.display or .text).
+
+ If there is a parse error, returns nil for the MathList.
+
+ - Parameter string: The LaTeX string to parse
+ - Returns: A tuple containing the parsed MathList and the detected MTLineStyle
+ */
+ public static func buildWithStyle(fromString string: String) -> (mathList: MTMathList?, style: MTLineStyle) {
+ var builder = MTMathListBuilder(string: string)
+ let mathList = builder.build()
+ let style = builder.mathMode.toLineStyle()
+ return (mathList, style)
+ }
+
+ /** Construct a math list from a given string and return the detected style.
+ This method detects LaTeX delimiters like \[...\], $$...$$, $...$, \(...\)
+ and returns the appropriate rendering style (.display or .text).
+
+ If there is an error while constructing the string, this returns nil for the MathList.
+ The error is returned in the `error` parameter.
+
+ - Parameters:
+ - string: The LaTeX string to parse
+ - error: An inout parameter that will contain any parse error
+ - Returns: A tuple containing the parsed MathList and the detected MTLineStyle
+ */
+ public static func buildWithStyle(fromString string: String, error: inout NSError?) -> (mathList: MTMathList?, style: MTLineStyle) {
+ var builder = MTMathListBuilder(string: string)
+ let output = builder.build()
+ let style = builder.mathMode.toLineStyle()
+ if builder.error != nil {
+ error = builder.error
+ return (nil, style)
+ }
+ return (output, style)
+ }
+
+ public mutating func buildInternal(_ oneCharOnly: Bool) -> MTMathList? {
+ self.buildInternal(oneCharOnly, stopChar: nil)
+ }
+
+ public mutating func buildInternal(_ oneCharOnly: Bool, stopChar stop: Character?) -> MTMathList? {
+ let list = MTMathList()
+ assert(!(oneCharOnly && stop != nil), "Cannot set both oneCharOnly and stopChar.")
+ var prevAtom: MTMathAtom? = nil
+ while self.hasCharacters {
+ if error != nil { return nil } // If there is an error thus far then bail out.
+
+ var atom: MTMathAtom? = nil
+ let char = self.getNextCharacter()
+
+ if oneCharOnly {
+ if char == "^" || char == "}" || char == "_" || char == "&" {
+ // this is not the character we are looking for.
+ // They are meant for the caller to look at.
+ self.unlookCharacter()
+ return list
+ }
+ }
+ // If there is a stop character, keep scanning 'til we find it
+ if stop != nil && char == stop! {
+ return list
+ }
+
+ if char == "^" {
+ assert(!oneCharOnly, "This should have been handled before")
+ if (prevAtom == nil || prevAtom!.superScript != nil || !prevAtom!.isScriptAllowed()) {
+ // If there is no previous atom, or if it already has a superscript
+ // or if scripts are not allowed for it, then add an empty node.
+ prevAtom = MTMathAtom(type: .ordinary, value: "")
+ list.add(prevAtom!)
+ }
+ // this is a superscript for the previous atom
+ // note: if the next char is the stopChar it will be consumed by the ^ and so it doesn't count as stop
+ prevAtom!.superScript = self.buildInternal(true)
+ continue
+ } else if char == "_" {
+ assert(!oneCharOnly, "This should have been handled before")
+ if (prevAtom == nil || prevAtom!.subScript != nil || !prevAtom!.isScriptAllowed()) {
+ // If there is no previous atom, or if it already has a subcript
+ // or if scripts are not allowed for it, then add an empty node.
+ prevAtom = MTMathAtom(type: .ordinary, value: "")
+ list.add(prevAtom!)
+ }
+ // this is a subscript for the previous atom
+ // note: if the next char is the stopChar it will be consumed by the _ and so it doesn't count as stop
+ prevAtom!.subScript = self.buildInternal(true)
+ continue
+ } else if char == "{" {
+ // this puts us in a recursive routine, and sets oneCharOnly to false and no stop character
+ if let subList = self.buildInternal(false, stopChar: "}") {
+ prevAtom = subList.atoms.last
+ list.append(subList)
+ if oneCharOnly {
+ return list
+ }
+ }
+ continue
+ } else if char == "}" {
+ // \ means a command
+ assert(!oneCharOnly, "This should have been handled before")
+ assert(stop == nil, "This should have been handled before")
+ // Special case: } terminates implicit table (envName == nil) created by \\
+ // This happens when \\ is used inside braces: \substack{a \\ b}
+ if self.currentEnv != nil && self.currentEnv!.envName == nil {
+ // Mark environment as ended, don't consume the }
+ self.currentEnv!.ended = true
+ return list
+ }
+ // We encountered a closing brace when there is no stop set, that means there was no
+ // corresponding opening brace.
+ self.setError(.mismatchBraces, message:"Mismatched braces.")
+ return nil
+ } else if char == "\\" {
+ let command = readCommand()
+ let done = stopCommand(command, list:list, stopChar:stop)
+ if done != nil {
+ return done
+ } else if error != nil {
+ return nil
+ }
+ if self.applyModifier(command, atom:prevAtom) {
+ continue
+ }
+
+ if let fontStyle = MTMathAtomFactory.fontStyleWithName(command) {
+ let oldSpacesAllowed = spacesAllowed
+ // Text has special consideration where it allows spaces without escaping.
+ spacesAllowed = command == "text"
+ let oldFontStyle = currentFontStyle
+ currentFontStyle = fontStyle
+ if let sublist = self.buildInternal(true) {
+ // Restore the font style.
+ currentFontStyle = oldFontStyle
+ spacesAllowed = oldSpacesAllowed
+
+ prevAtom = sublist.atoms.last
+ list.append(sublist)
+ if oneCharOnly {
+ return list
+ }
+ }
+ continue
+ }
+ atom = self.atomForCommand(command)
+ if atom == nil {
+ // this was an unknown command,
+ // we flag an error and return
+ // (note setError will not set the error if there is already one, so we flag internal error
+ // in the odd case that an _error is not set.
+ self.setError(.internalError, message:"Internal error")
+ return nil
+ }
+ } else if char == "&" {
+ // used for column separation in tables
+ assert(!oneCharOnly, "This should have been handled before")
+ if self.currentEnv != nil {
+ return list
+ } else {
+ // Create a new table with the current list and a default env
+ if let table = self.buildTable(env: nil, firstList: list, isRow: false) {
+ return MTMathList(atom: table)
+ } else {
+ return nil
+ }
+ }
+ } else if spacesAllowed && char == " " {
+ // If spaces are allowed then spaces do not need escaping with a \ before being used.
+ atom = MTMathAtomFactory.atom(forLatexSymbol: " ")
+ } else {
+ atom = MTMathAtomFactory.atom(forCharacter: char)
+ if atom == nil {
+ // Not a recognized character in standard math mode
+ // In text mode (spacesAllowed && roman style), accept any Unicode character for fallback font support
+ // This enables Chinese, Japanese, Korean, emoji, etc. in \text{} commands
+ if spacesAllowed && currentFontStyle == .roman {
+ atom = MTMathAtom(type: .ordinary, value: String(char))
+ } else {
+ // In math mode or non-text commands, skip unrecognized characters
+ continue
+ }
+ }
+ }
+
+ assert(atom != nil, "Atom shouldn't be nil")
+ atom?.fontStyle = currentFontStyle
+ // If this is an accent atom (e.g., from an accented character like "é"),
+ // propagate the font style to the inner list atoms that don't already have
+ // an explicit font style. This handles Unicode accented characters which are
+ // converted to accents by atom(fromAccentedCharacter:) without font style context.
+ // We only set font style on atoms with .defaultStyle to avoid overriding
+ // explicit font style commands like \textbf inside accents.
+ if let accent = atom as? MTAccent, let innerList = accent.innerList {
+ for innerAtom in innerList.atoms {
+ if innerAtom.fontStyle == .defaultStyle {
+ innerAtom.fontStyle = currentFontStyle
+ }
+ }
+ }
+ list.add(atom)
+ prevAtom = atom
+
+ if oneCharOnly {
+ return list
+ }
+ }
+ if stop != nil {
+ if stop == "}" {
+ // We did not find a corresponding closing brace.
+ self.setError(.mismatchBraces, message:"Missing closing brace")
+ } else {
+ // we never found our stop character
+ let errorMessage = "Expected character not found: \(stop!)"
+ self.setError(.characterNotFound, message:errorMessage)
+ }
+ }
+ return list
+ }
+
+
+ // MARK: - MTMathList to LaTeX conversion
+
+ /// This converts the MTMathList to LaTeX.
+ public static func mathListToString(_ ml: MTMathList?) -> String {
+ var str = ""
+ var currentfontStyle = MTFontStyle.defaultStyle
+ if let atomList = ml {
+ for atom in atomList.atoms {
+ if currentfontStyle != atom.fontStyle {
+ if currentfontStyle != .defaultStyle {
+ str += "}"
+ }
+ if atom.fontStyle != .defaultStyle {
+ let fontStyleName = MTMathAtomFactory.fontNameForStyle(atom.fontStyle)
+ str += "\\\(fontStyleName){"
+ }
+ currentfontStyle = atom.fontStyle
+ }
+ if atom.type == .fraction {
+ if let frac = atom as? MTFraction {
+ if frac.isContinuedFraction {
+ // Generate \cfrac with optional alignment
+ if frac.alignment != "c" {
+ str += "\\cfrac[\(frac.alignment)]{\(mathListToString(frac.numerator!))}{\(mathListToString(frac.denominator!))}"
+ } else {
+ str += "\\cfrac{\(mathListToString(frac.numerator!))}{\(mathListToString(frac.denominator!))}"
+ }
+ } else if frac.hasRule {
+ str += "\\frac{\(mathListToString(frac.numerator!))}{\(mathListToString(frac.denominator!))}"
+ } else {
+ let command: String
+ if frac.leftDelimiter.isEmpty && frac.rightDelimiter.isEmpty {
+ command = "atop"
+ } else if frac.leftDelimiter == "(" && frac.rightDelimiter == ")" {
+ command = "choose"
+ } else if frac.leftDelimiter == "{" && frac.rightDelimiter == "}" {
+ command = "brace"
+ } else if frac.leftDelimiter == "[" && frac.rightDelimiter == "]" {
+ command = "brack"
+ } else {
+ command = "atopwithdelims\(frac.leftDelimiter)\(frac.rightDelimiter)"
+ }
+ str += "{\(mathListToString(frac.numerator!)) \\\(command) \(mathListToString(frac.denominator!))}"
+ }
+ }
+ } else if atom.type == .radical {
+ str += "\\sqrt"
+ if let rad = atom as? MTRadical {
+ if rad.degree != nil {
+ str += "[\(mathListToString(rad.degree!))]"
+ }
+ str += "{\(mathListToString(rad.radicand!))}"
+ }
+ } else if atom.type == .inner {
+ if let inner = atom as? MTInner {
+ if inner.leftBoundary != nil || inner.rightBoundary != nil {
+ if inner.leftBoundary != nil {
+ str += "\\left\(delimToString(delim: inner.leftBoundary!)) "
+ } else {
+ str += "\\left. "
+ }
+
+ str += mathListToString(inner.innerList!)
+
+ if inner.rightBoundary != nil {
+ str += "\\right\(delimToString(delim: inner.rightBoundary!)) "
+ } else {
+ str += "\\right. "
+ }
+ } else {
+ str += "{\(mathListToString(inner.innerList!))}"
+ }
+ }
+ } else if atom.type == .table {
+ if let table = atom as? MTMathTable {
+ if !table.environment.isEmpty {
+ str += "\\begin{\(table.environment)}"
+ }
+
+ for i in 0..= 1 && cell.atoms[0].type == .style {
+ // remove first atom
+ cell.atoms.removeFirst()
+ }
+ }
+ if table.environment == "eqalign" || table.environment == "aligned" || table.environment == "split" {
+ if j == 1 && cell.atoms.count >= 1 && cell.atoms[0].type == .ordinary && cell.atoms[0].nucleus.count == 0 {
+ // remove empty nucleus added for spacing
+ cell.atoms.removeFirst()
+ }
+ }
+ str += mathListToString(cell)
+ if j < row.count - 1 {
+ str += "&"
+ }
+ }
+ if i < table.numRows - 1 {
+ str += "\\\\ "
+ }
+ }
+ if !table.environment.isEmpty {
+ str += "\\end{\(table.environment)}"
+ }
+ }
+ } else if atom.type == .overline {
+ if let overline = atom as? MTOverLine {
+ str += "\\overline"
+ str += "{\(mathListToString(overline.innerList!))}"
+ }
+ } else if atom.type == .underline {
+ if let underline = atom as? MTUnderLine {
+ str += "\\underline"
+ str += "{\(mathListToString(underline.innerList!))}"
+ }
+ } else if atom.type == .accent {
+ if let accent = atom as? MTAccent {
+ str += "\\\(MTMathAtomFactory.accentName(accent)!){\(mathListToString(accent.innerList!))}"
+ }
+ } else if atom.type == .largeOperator {
+ let op = atom as! MTLargeOperator
+ let command = MTMathAtomFactory.latexSymbolName(for: atom)
+ let originalOp = MTMathAtomFactory.atom(forLatexSymbol: command!) as! MTLargeOperator
+ str += "\\\(command!) "
+ if originalOp.limits != op.limits {
+ if op.limits {
+ str += "\\limits "
+ } else {
+ str += "\\nolimits "
+ }
+ }
+ } else if atom.type == .space {
+ if let space = atom as? MTMathSpace {
+ if let command = Self.spaceToCommands[space.space] {
+ str += "\\\(command) "
+ } else {
+ str += String(format: "\\mkern%.1fmu", space.space)
+ }
+ }
+ } else if atom.type == .style {
+ if let style = atom as? MTMathStyle {
+ if let command = Self.styleToCommands[style.style] {
+ str += "\\\(command) "
+ }
+ }
+ } else if atom.nucleus.isEmpty {
+ str += "{}"
+ } else if atom.nucleus == "\u{2236}" {
+ // math colon
+ str += ":"
+ } else if atom.nucleus == "\u{2212}" {
+ // math minus
+ str += "-"
+ } else {
+ if let command = MTMathAtomFactory.latexSymbolName(for: atom) {
+ str += "\\\(command) "
+ } else {
+ str += "\(atom.nucleus)"
+ }
+ }
+
+ if atom.superScript != nil {
+ str += "^{\(mathListToString(atom.superScript!))}"
+ }
+
+ if atom.subScript != nil {
+ str += "_{\(mathListToString(atom.subScript!))}"
+ }
+ }
+ }
+ if currentfontStyle != .defaultStyle {
+ str += "}"
+ }
+ return str
+ }
+
+ public static func delimToString(delim: MTMathAtom) -> String {
+ if let command = MTMathAtomFactory.getDelimiterName(of: delim) {
+ let singleChars = [ "(", ")", "[", "]", "<", ">", "|", ".", "/"]
+ if singleChars.contains(command) {
+ return command
+ } else if command == "||" {
+ return "\\|"
+ } else {
+ return "\\\(command)"
+ }
+ }
+ return ""
+ }
+
+ mutating func atomForCommand(_ command:String) -> MTMathAtom? {
+ if let atom = MTMathAtomFactory.atom(forLatexSymbol: command) {
+ return atom
+ }
+ if let accent = MTMathAtomFactory.accent(withName: command) {
+ // The command is an accent
+ accent.innerList = self.buildInternal(true)
+ return accent;
+ } else if command == "frac" {
+ // A fraction command has 2 arguments
+ let frac = MTFraction()
+ frac.numerator = self.buildInternal(true)
+ frac.denominator = self.buildInternal(true)
+ return frac;
+ } else if command == "cfrac" {
+ // A continued fraction command with optional alignment and 2 arguments
+ let frac = MTFraction()
+ frac.isContinuedFraction = true
+
+ // Parse optional alignment parameter [l], [r], [c]
+ skipSpaces()
+ if hasCharacters && string[currentCharIndex] == "[" {
+ _ = getNextCharacter() // consume '['
+ if hasCharacters {
+ let alignmentChar = getNextCharacter()
+ if alignmentChar == "l" || alignmentChar == "r" || alignmentChar == "c" {
+ frac.alignment = String(alignmentChar)
+ }
+ }
+ // Consume closing ']'
+ if hasCharacters && string[currentCharIndex] == "]" {
+ _ = getNextCharacter()
+ }
+ }
+
+ frac.numerator = self.buildInternal(true)
+ frac.denominator = self.buildInternal(true)
+ return frac;
+ } else if command == "dfrac" {
+ // Display-style fraction command has 2 arguments
+ let frac = MTFraction()
+ let numerator = self.buildInternal(true)
+ let denominator = self.buildInternal(true)
+
+ // Prepend \displaystyle to force display mode rendering
+ let displayStyle = MTMathStyle(style: .display)
+ numerator?.insert(displayStyle, at: 0)
+ denominator?.insert(displayStyle, at: 0)
+
+ frac.numerator = numerator
+ frac.denominator = denominator
+ return frac;
+ } else if command == "tfrac" {
+ // Text-style fraction command has 2 arguments
+ let frac = MTFraction()
+ let numerator = self.buildInternal(true)
+ let denominator = self.buildInternal(true)
+
+ // Prepend \textstyle to force text mode rendering
+ let textStyle = MTMathStyle(style: .text)
+ numerator?.insert(textStyle, at: 0)
+ denominator?.insert(textStyle, at: 0)
+
+ frac.numerator = numerator
+ frac.denominator = denominator
+ return frac;
+ } else if command == "binom" {
+ // A binom command has 2 arguments
+ let frac = MTFraction(hasRule: false)
+ frac.numerator = self.buildInternal(true)
+ frac.denominator = self.buildInternal(true)
+ frac.leftDelimiter = "(";
+ frac.rightDelimiter = ")";
+ return frac;
+ } else if command == "bra" {
+ // Dirac bra notation: \bra{psi} -> ⟨psi|
+ let inner = MTInner()
+ inner.leftBoundary = MTMathAtomFactory.boundary(forDelimiter: "langle")
+ inner.rightBoundary = MTMathAtomFactory.boundary(forDelimiter: "|")
+ inner.innerList = self.buildInternal(true)
+ return inner
+ } else if command == "ket" {
+ // Dirac ket notation: \ket{psi} -> |psi⟩
+ let inner = MTInner()
+ inner.leftBoundary = MTMathAtomFactory.boundary(forDelimiter: "|")
+ inner.rightBoundary = MTMathAtomFactory.boundary(forDelimiter: "rangle")
+ inner.innerList = self.buildInternal(true)
+ return inner
+ } else if command == "braket" {
+ // Dirac braket notation: \braket{phi}{psi} -> ⟨phi|psi⟩
+ let inner = MTInner()
+ inner.leftBoundary = MTMathAtomFactory.boundary(forDelimiter: "langle")
+ inner.rightBoundary = MTMathAtomFactory.boundary(forDelimiter: "rangle")
+ // Build the inner content: phi | psi
+ let phi = self.buildInternal(true)
+ let psi = self.buildInternal(true)
+ let content = MTMathList()
+ if let phiList = phi {
+ for atom in phiList.atoms {
+ content.add(atom)
+ }
+ }
+ // Add the | separator
+ content.add(MTMathAtom(type: .ordinary, value: "|"))
+ if let psiList = psi {
+ for atom in psiList.atoms {
+ content.add(atom)
+ }
+ }
+ inner.innerList = content
+ return inner
+ } else if command == "operatorname" || command == "operatorname*" {
+ // \operatorname{name} creates a custom operator with proper spacing
+ // \operatorname*{name} creates an operator with limits above/below
+ let hasLimits = command.hasSuffix("*")
+
+ // Parse the operator name
+ let content = self.buildInternal(true)
+
+ // Convert the parsed content to a string
+ var operatorName = ""
+ if let atoms = content?.atoms {
+ for atom in atoms {
+ operatorName += atom.nucleus
+ }
+ }
+
+ if operatorName.isEmpty {
+ let errorMessage = "Missing operator name for \\operatorname"
+ self.setError(.invalidCommand, message: errorMessage)
+ return nil
+ }
+
+ return MTLargeOperator(value: operatorName, limits: hasLimits)
+ } else if command == "sqrt" {
+ // A sqrt command with one argument
+ let rad = MTRadical()
+ guard self.hasCharacters else {
+ rad.radicand = self.buildInternal(true)
+ return rad
+ }
+ let ch = self.getNextCharacter()
+ if (ch == "[") {
+ // special handling for sqrt[degree]{radicand}
+ rad.degree = self.buildInternal(false, stopChar:"]")
+ rad.radicand = self.buildInternal(true)
+ } else {
+ self.unlookCharacter()
+ rad.radicand = self.buildInternal(true)
+ }
+ return rad;
+ } else if command == "left" {
+ // Save the current inner while a new one gets built.
+ let oldInner = currentInnerAtom
+ currentInnerAtom = MTInner()
+ currentInnerAtom!.leftBoundary = self.getBoundaryAtom("left")
+ if currentInnerAtom!.leftBoundary == nil {
+ return nil;
+ }
+ currentInnerAtom!.innerList = self.buildInternal(false)
+ if currentInnerAtom!.rightBoundary == nil {
+ // A right node would have set the right boundary so we must be missing the right node.
+ let errorMessage = "Missing \\right"
+ self.setError(.missingRight, message:errorMessage)
+ return nil
+ }
+ // reinstate the old inner atom.
+ let newInner = currentInnerAtom;
+ currentInnerAtom = oldInner;
+ return newInner;
+ } else if command == "overline" {
+ // The overline command has 1 arguments
+ let over = MTOverLine()
+ over.innerList = self.buildInternal(true)
+ return over
+ } else if command == "underline" {
+ // The underline command has 1 arguments
+ let under = MTUnderLine()
+ under.innerList = self.buildInternal(true)
+ return under
+ } else if command == "substack" {
+ // \substack reads ONE braced argument containing rows separated by \\
+ // Similar to how \frac reads {numerator}{denominator}
+
+ // Read the braced content using standard pattern
+ let content = self.buildInternal(true)
+
+ if content == nil {
+ return nil
+ }
+
+ // The content may already be a table if \\ was encountered
+ // Check if we got a table from the \\ parsing
+ if content!.atoms.count == 1, let tableAtom = content!.atoms.first as? MTMathTable {
+ return tableAtom
+ }
+
+ // Otherwise, single row - wrap in table
+ var rows = [[MTMathList]]()
+ rows.append([content!])
+
+ var error: NSError? = self.error
+ let table = MTMathAtomFactory.table(withEnvironment: nil, rows: rows, error: &error)
+ if table == nil && self.error == nil {
+ self.error = error
+ return nil
+ }
+
+ return table
+ } else if command == "begin" {
+ let env = self.readEnvironment()
+ if env == nil {
+ return nil;
+ }
+ let table = self.buildTable(env: env, firstList:nil, isRow:false)
+ return table
+ } else if command == "color" {
+ // A color command has 2 arguments
+ let mathColor = MTMathColor()
+ let color = self.readColor()
+ if color == nil {
+ return nil;
+ }
+ mathColor.colorString = color!
+ mathColor.innerList = self.buildInternal(true)
+ return mathColor
+ } else if command == "textcolor" {
+ // A textcolor command has 2 arguments
+ let mathColor = MTMathTextColor()
+ let color = self.readColor()
+ if color == nil {
+ return nil;
+ }
+ mathColor.colorString = color!
+ mathColor.innerList = self.buildInternal(true)
+ return mathColor
+ } else if command == "colorbox" {
+ // A color command has 2 arguments
+ let mathColorbox = MTMathColorbox()
+ let color = self.readColor()
+ if color == nil {
+ return nil;
+ }
+ mathColorbox.colorString = color!
+ mathColorbox.innerList = self.buildInternal(true)
+ return mathColorbox
+ } else if command == "pmod" {
+ // A pmod command has 1 argument - creates (mod n)
+ let inner = MTInner()
+ inner.leftBoundary = MTMathAtomFactory.boundary(forDelimiter: "(")
+ inner.rightBoundary = MTMathAtomFactory.boundary(forDelimiter: ")")
+
+ let innerList = MTMathList()
+
+ // Add the "mod" operator (upright text)
+ let modOperator = MTMathAtomFactory.atom(forLatexSymbol: "mod")!
+ innerList.add(modOperator)
+
+ // Add medium space between "mod" and argument (6mu)
+ let space = MTMathSpace(space: 6.0)
+ innerList.add(space)
+
+ // Parse the argument from braces
+ let argument = self.buildInternal(true)
+ if let argList = argument {
+ innerList.append(argList)
+ }
+
+ inner.innerList = innerList
+ return inner
+ } else if command == "not" {
+ // Handle \not command with lookahead for comprehensive negation support
+ let nextCommand = self.peekNextCommand()
+
+ if let negatedUnicode = Self.notCombinations[nextCommand] {
+ self.consumeNextCommand() // Remove base symbol from stream
+ return MTMathAtom(type: .relation, value: negatedUnicode)
+ } else {
+ let errorMessage = "Unsupported \\not\\\(nextCommand) combination"
+ self.setError(.invalidCommand, message: errorMessage)
+ return nil
+ }
+ } else if let sizeMultiplier = Self.delimiterSizeCommands[command] {
+ // Handle \big, \Big, \bigg, \Bigg and their variants
+ let delim = self.readDelimiter()
+ if delim == nil {
+ let errorMessage = "Missing delimiter for \\\(command)"
+ self.setError(.missingDelimiter, message: errorMessage)
+ return nil
+ }
+ let boundary = MTMathAtomFactory.boundary(forDelimiter: delim!)
+ if boundary == nil {
+ let errorMessage = "Invalid delimiter for \\\(command): \(delim!)"
+ self.setError(.invalidDelimiter, message: errorMessage)
+ return nil
+ }
+
+ // Create MTInner with explicit delimiter height
+ let inner = MTInner()
+
+ // Determine if this is a left, right, or middle delimiter based on command suffix
+ let isLeft = command.hasSuffix("l")
+ let isRight = command.hasSuffix("r")
+ // let isMiddle = command.hasSuffix("m") // For future use
+
+ if isLeft {
+ inner.leftBoundary = boundary
+ inner.rightBoundary = MTMathAtomFactory.boundary(forDelimiter: ".")
+ } else if isRight {
+ inner.leftBoundary = MTMathAtomFactory.boundary(forDelimiter: ".")
+ inner.rightBoundary = boundary
+ } else {
+ // For \big, \Big, \bigg, \Bigg and \bigm variants, use the delimiter on both sides
+ // but with empty inner content - it's just a sized delimiter
+ inner.leftBoundary = boundary
+ inner.rightBoundary = MTMathAtomFactory.boundary(forDelimiter: ".")
+ }
+
+ inner.innerList = MTMathList()
+ inner.delimiterHeight = sizeMultiplier // Store multiplier, typesetter will compute actual height
+
+ return inner
+ } else {
+ let errorMessage = "Invalid command \\\(command)"
+ self.setError(.invalidCommand, message:errorMessage)
+ return nil;
+ }
+ }
+
+ mutating func readColor() -> String? {
+ if !self.expectCharacter("{") {
+ // We didn't find an opening brace, so no env found.
+ self.setError(.characterNotFound, message:"Missing {")
+ return nil;
+ }
+
+ // Ignore spaces and nonascii.
+ self.skipSpaces()
+
+ // a string of all upper and lower case characters.
+ var mutable = ""
+ while self.hasCharacters {
+ let ch = self.getNextCharacter()
+ if ch == "#" || (ch >= "A" && ch <= "Z") || (ch >= "a" && ch <= "z") || (ch >= "0" && ch <= "9") {
+ mutable.append(ch) // appendString:[NSString stringWithCharacters:&ch length:1]];
+ } else {
+ // we went too far
+ self.unlookCharacter()
+ break;
+ }
+ }
+
+ if !self.expectCharacter("}") {
+ // We didn't find an closing brace, so invalid format.
+ self.setError(.characterNotFound, message:"Missing }")
+ return nil;
+ }
+ return mutable;
+ }
+
+ mutating func skipSpaces() {
+ while self.hasCharacters {
+ let ch = self.getNextCharacter().utf32Char
+ if ch < 0x21 || ch > 0x7E {
+ // skip non ascii characters and spaces
+ continue;
+ } else {
+ self.unlookCharacter()
+ return;
+ }
+ }
+ }
+
+ static var fractionCommands: [String:[Character]] {
+ [
+ "over": [],
+ "atop" : [],
+ "choose" : [ "(", ")"],
+ "brack" : [ "[", "]"],
+ "brace" : [ "{", "}"]
+ ]
+ }
+
+ mutating func stopCommand(_ command: String, list:MTMathList, stopChar:Character?) -> MTMathList? {
+ if command == "right" {
+ if currentInnerAtom == nil {
+ let errorMessage = "Missing \\left";
+ self.setError(.missingLeft, message:errorMessage)
+ return nil;
+ }
+ currentInnerAtom!.rightBoundary = self.getBoundaryAtom("right")
+ if currentInnerAtom!.rightBoundary == nil {
+ return nil;
+ }
+ // return the list read so far.
+ return list
+ } else if let delims = Self.fractionCommands[command] {
+ var frac:MTFraction! = nil;
+ if command == "over" {
+ frac = MTFraction()
+ } else {
+ frac = MTFraction(hasRule: false)
+ }
+ if delims.count == 2 {
+ frac.leftDelimiter = String(delims[0])
+ frac.rightDelimiter = String(delims[1])
+ }
+ frac.numerator = list;
+ frac.denominator = self.buildInternal(false, stopChar: stopChar)
+ if error != nil {
+ return nil;
+ }
+ let fracList = MTMathList()
+ fracList.add(frac)
+ return fracList
+ } else if command == "\\" || command == "cr" {
+ if currentEnv != nil {
+ // Stop the current list and increment the row count
+ currentEnv!.numRows+=1
+ return list
+ } else {
+ // Create a new table with the current list and a default env
+ if let table = self.buildTable(env: nil, firstList:list, isRow:true) {
+ return MTMathList(atom: table)
+ }
+ }
+ } else if command == "end" {
+ if currentEnv == nil {
+ let errorMessage = "Missing \\begin";
+ self.setError(.missingBegin, message:errorMessage)
+ return nil
+ }
+ let env = self.readEnvironment()
+ if env == nil {
+ return nil
+ }
+ if env! != currentEnv!.envName {
+ let errorMessage = "Begin environment name \(currentEnv!.envName ?? "(none)") does not match end name: \(env!)"
+ self.setError(.invalidEnv, message:errorMessage)
+ return nil
+ }
+ // Finish the current environment.
+ currentEnv!.ended = true
+ return list
+ }
+ return nil
+ }
+
+ // Applies the modifier to the atom. Returns true if modifier applied.
+ mutating func applyModifier(_ modifier:String, atom:MTMathAtom?) -> Bool {
+ if modifier == "limits" {
+ if atom?.type != .largeOperator {
+ let errorMessage = "Limits can only be applied to an operator."
+ self.setError(.invalidLimits, message:errorMessage)
+ } else {
+ let op = atom as! MTLargeOperator
+ op.limits = true
+ }
+ return true
+ } else if modifier == "nolimits" {
+ if atom?.type != .largeOperator {
+ let errorMessage = "No limits can only be applied to an operator."
+ self.setError(.invalidLimits, message:errorMessage)
+ } else {
+ let op = atom as! MTLargeOperator
+ op.limits = false
+ }
+ return true
+ }
+ return false
+ }
+
+ mutating func setError(_ code:MTParseErrors, message:String) {
+ // Only record the first error.
+ if error == nil {
+ error = NSError(domain: MTParseError, code: code.rawValue, userInfo: [ NSLocalizedDescriptionKey : message ])
+ }
+ }
+
+ mutating func atom(forCommand command: String) -> MTMathAtom? {
+ if let atom = MTMathAtomFactory.atom(forLatexSymbol: command) {
+ return atom
+ }
+ if let accent = MTMathAtomFactory.accent(withName: command) {
+ accent.innerList = self.buildInternal(true)
+ return accent
+ } else if command == "frac" {
+ let frac = MTFraction()
+ frac.numerator = self.buildInternal(true)
+ frac.denominator = self.buildInternal(true)
+ return frac
+ } else if command == "cfrac" {
+ let frac = MTFraction()
+ frac.isContinuedFraction = true
+
+ // Parse optional alignment parameter [l], [r], [c]
+ skipSpaces()
+ if hasCharacters && string[currentCharIndex] == "[" {
+ _ = getNextCharacter() // consume '['
+ if hasCharacters {
+ let alignmentChar = getNextCharacter()
+ if alignmentChar == "l" || alignmentChar == "r" || alignmentChar == "c" {
+ frac.alignment = String(alignmentChar)
+ }
+ }
+ // Consume closing ']'
+ if hasCharacters && string[currentCharIndex] == "]" {
+ _ = getNextCharacter()
+ }
+ }
+
+ frac.numerator = self.buildInternal(true)
+ frac.denominator = self.buildInternal(true)
+ return frac
+ } else if command == "dfrac" {
+ // Display-style fraction command has 2 arguments
+ let frac = MTFraction()
+ let numerator = self.buildInternal(true)
+ let denominator = self.buildInternal(true)
+
+ // Prepend \displaystyle to force display mode rendering
+ let displayStyle = MTMathStyle(style: .display)
+ numerator?.insert(displayStyle, at: 0)
+ denominator?.insert(displayStyle, at: 0)
+
+ frac.numerator = numerator
+ frac.denominator = denominator
+ return frac
+ } else if command == "tfrac" {
+ // Text-style fraction command has 2 arguments
+ let frac = MTFraction()
+ let numerator = self.buildInternal(true)
+ let denominator = self.buildInternal(true)
+
+ // Prepend \textstyle to force text mode rendering
+ let textStyle = MTMathStyle(style: .text)
+ numerator?.insert(textStyle, at: 0)
+ denominator?.insert(textStyle, at: 0)
+
+ frac.numerator = numerator
+ frac.denominator = denominator
+ return frac
+ } else if command == "binom" {
+ let frac = MTFraction(hasRule: false)
+ frac.numerator = self.buildInternal(true)
+ frac.denominator = self.buildInternal(true)
+ frac.leftDelimiter = "("
+ frac.rightDelimiter = ")"
+ return frac
+ } else if command == "bra" {
+ // Dirac bra notation: \bra{psi} -> ⟨psi|
+ let inner = MTInner()
+ inner.leftBoundary = MTMathAtomFactory.boundary(forDelimiter: "langle")
+ inner.rightBoundary = MTMathAtomFactory.boundary(forDelimiter: "|")
+ inner.innerList = self.buildInternal(true)
+ return inner
+ } else if command == "ket" {
+ // Dirac ket notation: \ket{psi} -> |psi⟩
+ let inner = MTInner()
+ inner.leftBoundary = MTMathAtomFactory.boundary(forDelimiter: "|")
+ inner.rightBoundary = MTMathAtomFactory.boundary(forDelimiter: "rangle")
+ inner.innerList = self.buildInternal(true)
+ return inner
+ } else if command == "braket" {
+ // Dirac braket notation: \braket{phi}{psi} -> ⟨phi|psi⟩
+ let inner = MTInner()
+ inner.leftBoundary = MTMathAtomFactory.boundary(forDelimiter: "langle")
+ inner.rightBoundary = MTMathAtomFactory.boundary(forDelimiter: "rangle")
+ let phi = self.buildInternal(true)
+ let psi = self.buildInternal(true)
+ let content = MTMathList()
+ if let phiList = phi {
+ for atom in phiList.atoms {
+ content.add(atom)
+ }
+ }
+ content.add(MTMathAtom(type: .ordinary, value: "|"))
+ if let psiList = psi {
+ for atom in psiList.atoms {
+ content.add(atom)
+ }
+ }
+ inner.innerList = content
+ return inner
+ } else if command == "operatorname" || command == "operatorname*" {
+ // \operatorname{name} creates a custom operator with proper spacing
+ // \operatorname*{name} creates an operator with limits above/below
+ let hasLimits = command.hasSuffix("*")
+
+ let content = self.buildInternal(true)
+ var operatorName = ""
+ if let atoms = content?.atoms {
+ for atom in atoms {
+ operatorName += atom.nucleus
+ }
+ }
+ if operatorName.isEmpty {
+ self.setError(.invalidCommand, message: "Missing operator name for \\operatorname")
+ return nil
+ }
+ return MTLargeOperator(value: operatorName, limits: hasLimits)
+ } else if command == "sqrt" {
+ let rad = MTRadical()
+ guard self.hasCharacters else {
+ rad.radicand = self.buildInternal(true)
+ return rad
+ }
+ let char = self.getNextCharacter()
+ if char == "[" {
+ rad.degree = self.buildInternal(false, stopChar: "]")
+ rad.radicand = self.buildInternal(true)
+ } else {
+ self.unlookCharacter()
+ rad.radicand = self.buildInternal(true)
+ }
+ return rad
+ } else if command == "left" {
+ let oldInner = self.currentInnerAtom
+ self.currentInnerAtom = MTInner()
+ self.currentInnerAtom?.leftBoundary = self.getBoundaryAtom("left")
+ if self.currentInnerAtom?.leftBoundary == nil {
+ return nil
+ }
+ self.currentInnerAtom!.innerList = self.buildInternal(false)
+ if self.currentInnerAtom?.rightBoundary == nil {
+ self.setError(.missingRight, message: "Missing \\right")
+ return nil
+ }
+ let newInner = self.currentInnerAtom
+ currentInnerAtom = oldInner
+ return newInner
+ } else if command == "overline" {
+ let over = MTOverLine()
+ over.innerList = self.buildInternal(true)
+
+ return over
+ } else if command == "underline" {
+ let under = MTUnderLine()
+ under.innerList = self.buildInternal(true)
+
+ return under
+ } else if command == "begin" {
+ if let env = self.readEnvironment() {
+ // Check if this is a starred matrix environment and read optional alignment
+ var alignment: MTColumnAlignment? = nil
+ if env.hasSuffix("*") {
+ alignment = self.readOptionalAlignment()
+ if self.error != nil {
+ return nil
+ }
+ }
+
+ let table = self.buildTable(env: env, alignment: alignment, firstList: nil, isRow: false)
+ return table
+ } else {
+ return nil
+ }
+ } else if command == "color" {
+ // A color command has 2 arguments
+ let mathColor = MTMathColor()
+ mathColor.colorString = self.readColor()!
+ mathColor.innerList = self.buildInternal(true)
+ return mathColor
+ } else if command == "colorbox" {
+ // A color command has 2 arguments
+ let mathColorbox = MTMathColorbox()
+ mathColorbox.colorString = self.readColor()!
+ mathColorbox.innerList = self.buildInternal(true)
+ return mathColorbox
+ } else {
+ self.setError(.invalidCommand, message: "Invalid command \\\(command)")
+ return nil
+ }
+ }
+
+ mutating func readEnvironment() -> String? {
+ if !self.expectCharacter("{") {
+ // We didn't find an opening brace, so no env found.
+ self.setError(.characterNotFound, message: "Missing {")
+ return nil
+ }
+
+ self.skipSpaces()
+ let env = self.readString()
+
+ if !self.expectCharacter("}") {
+ // We didn"t find an closing brace, so invalid format.
+ self.setError(.characterNotFound, message: "Missing }")
+ return nil;
+ }
+ return env
+ }
+
+ /// Reads optional alignment parameter for starred matrix environments: [r], [l], or [c]
+ mutating func readOptionalAlignment() -> MTColumnAlignment? {
+ self.skipSpaces()
+
+ // Check if there's an opening bracket
+ guard hasCharacters && string[currentCharIndex] == "[" else {
+ return nil
+ }
+
+ _ = getNextCharacter() // consume '['
+ self.skipSpaces()
+
+ guard hasCharacters else {
+ self.setError(.characterNotFound, message: "Missing alignment specifier after [")
+ return nil
+ }
+
+ let alignChar = getNextCharacter()
+ let alignment: MTColumnAlignment?
+
+ switch alignChar {
+ case "l":
+ alignment = .left
+ case "c":
+ alignment = .center
+ case "r":
+ alignment = .right
+ default:
+ self.setError(.invalidEnv, message: "Invalid alignment specifier: \(alignChar). Must be l, c, or r")
+ return nil
+ }
+
+ self.skipSpaces()
+
+ if !self.expectCharacter("]") {
+ self.setError(.characterNotFound, message: "Missing ] after alignment specifier")
+ return nil
+ }
+
+ return alignment
+ }
+
+ func MTAssertNotSpace(_ ch: Character) {
+ assert(ch >= "\u{21}" && ch <= "\u{7E}", "Expected non-space character \(ch)")
+ }
+
+ mutating func buildTable(env: String?, alignment: MTColumnAlignment? = nil, firstList: MTMathList?, isRow: Bool) -> MTMathAtom? {
+ // Save the current env till an new one gets built.
+ let oldEnv = self.currentEnv
+
+ currentEnv = MTEnvProperties(name: env, alignment: alignment)
+
+ var currentRow = 0
+ var currentCol = 0
+
+ var rows = [[MTMathList]]()
+ rows.append([MTMathList]())
+ if firstList != nil {
+ rows[currentRow].append(firstList!)
+ if isRow {
+ currentEnv!.numRows+=1
+ currentRow+=1
+ rows.append([MTMathList]())
+ } else {
+ currentCol+=1
+ }
+ }
+ while !currentEnv!.ended && self.hasCharacters {
+ let list = self.buildInternal(false)
+ if list == nil {
+ // If there is an error building the list, bail out early.
+ return nil
+ }
+ rows[currentRow].append(list!)
+ currentCol+=1
+ if currentEnv!.numRows > currentRow {
+ currentRow = currentEnv!.numRows
+ rows.append([MTMathList]())
+ currentCol = 0
+ }
+ }
+
+ if !currentEnv!.ended && currentEnv!.envName != nil {
+ self.setError(.missingEnd, message: "Missing \\end")
+ return nil
+ }
+
+ var error:NSError? = self.error
+ let table = MTMathAtomFactory.table(withEnvironment: currentEnv?.envName, alignment: currentEnv?.alignment, rows: rows, error: &error)
+ if table == nil && self.error == nil {
+ self.error = error
+ return nil
+ }
+ self.currentEnv = oldEnv
+ return table
+ }
+
+ mutating func getBoundaryAtom(_ delimiterType: String) -> MTMathAtom? {
+ let delim = self.readDelimiter()
+ if delim == nil {
+ let errorMessage = "Missing delimiter for \\\(delimiterType)"
+ self.setError(.missingDelimiter, message:errorMessage)
+ return nil
+ }
+ let boundary = MTMathAtomFactory.boundary(forDelimiter: delim!)
+ if boundary == nil {
+ let errorMessage = "Invalid delimiter for \(delimiterType): \(delim!)"
+ self.setError(.invalidDelimiter, message:errorMessage)
+ return nil
+ }
+ return boundary
+ }
+
+ mutating func readDelimiter() -> String? {
+ self.skipSpaces()
+ while self.hasCharacters {
+ let char = self.getNextCharacter()
+ MTAssertNotSpace(char)
+ if char == "\\" {
+ let command = self.readCommand()
+ if command == "|" {
+ return "||"
+ }
+ return command
+ } else {
+ return String(char)
+ }
+ }
+ return nil
+ }
+
+ mutating func readCommand() -> String {
+ let singleChars = "{}$#%_| ,>;!\\"
+ if self.hasCharacters {
+ let char = self.getNextCharacter()
+ if let _ = singleChars.firstIndex(of: char) {
+ return String(char)
+ } else {
+ self.unlookCharacter()
+ }
+ }
+ return self.readString()
+ }
+
+ mutating func readString() -> String {
+ // a string of all upper and lower case characters (and asterisks for starred environments)
+ var output = ""
+ while self.hasCharacters {
+ let char = self.getNextCharacter()
+ if char.isLowercase || char.isUppercase || char == "*" {
+ output.append(char)
+ } else {
+ self.unlookCharacter()
+ break
+ }
+ }
+ return output
+ }
+}
diff --git a/third-party/SwiftMath/Sources/MathRender/MTMathListDisplay.swift b/third-party/SwiftMath/Sources/MathRender/MTMathListDisplay.swift
new file mode 100755
index 0000000000..a0fa47c879
--- /dev/null
+++ b/third-party/SwiftMath/Sources/MathRender/MTMathListDisplay.swift
@@ -0,0 +1,881 @@
+//
+// Created by Mike Griebling on 2022-12-31.
+// Translated from an Objective-C implementation by Kostub Deshmukh.
+//
+// This software may be modified and distributed under the terms of the
+// MIT license. See the LICENSE file for details.
+//
+
+import Foundation
+import QuartzCore
+import CoreText
+import SwiftUI
+
+func isIos6Supported() -> Bool {
+ if !MTDisplay.initialized {
+#if os(iOS) || os(visionOS)
+ let reqSysVer = "6.0"
+ let currSysVer = UIDevice.current.systemVersion
+ if currSysVer.compare(reqSysVer, options: .numeric) != .orderedAscending {
+ MTDisplay.supported = true
+ }
+#else
+ MTDisplay.supported = true
+#endif
+ MTDisplay.initialized = true
+ }
+ return MTDisplay.supported
+}
+
+// The Downshift protocol allows an MTDisplay to be shifted down by a given amount.
+protocol DownShift {
+ var shiftDown:CGFloat { set get }
+}
+
+// MARK: - MTDisplay
+
+/// The base class for rendering a math equation.
+public class MTDisplay:NSObject {
+
+ // needed for isIos6Supported() func above
+ static var initialized = false
+ static var supported = false
+
+ /// Draws itself in the given graphics context.
+ public func draw(_ context:CGContext) {
+ if self.localBackgroundColor != nil {
+ context.saveGState()
+ context.setBlendMode(.normal)
+ context.setFillColor(self.localBackgroundColor!.cgColor)
+ context.fill(self.displayBounds())
+ context.restoreGState()
+ }
+ }
+
+ /// Gets the bounding rectangle for the MTDisplay
+ func displayBounds() -> CGRect {
+ CGRectMake(self.position.x, self.position.y - self.descent, self.width, self.ascent + self.descent)
+ }
+
+ /// For debugging. Shows the object in quick look in Xcode.
+#if os(iOS) || os(visionOS)
+ func debugQuickLookObject() -> Any {
+ let size = CGSizeMake(self.width, self.ascent + self.descent);
+ UIGraphicsBeginImageContext(size);
+
+ // get a reference to that context we created
+ let context = UIGraphicsGetCurrentContext()!
+ // translate/flip the graphics context (for transforming from CG* coords to UI* coords
+ context.translateBy(x: 0, y: size.height);
+ context.scaleBy(x: 1.0, y: -1.0);
+ // move the position to (0,0)
+ context.translateBy(x: -self.position.x, y: -self.position.y);
+
+ // Move the line up by self.descent
+ context.translateBy(x: 0, y: self.descent);
+ // Draw self on context
+ self.draw(context)
+
+ // generate a new UIImage from the graphics context we drew onto
+ let img = UIGraphicsGetImageFromCurrentImageContext()
+ return img as Any
+ }
+#endif
+
+ /// The distance from the axis to the top of the display
+ public var ascent:CGFloat = 0
+ /// The distance from the axis to the bottom of the display
+ public var descent:CGFloat = 0
+ /// The width of the display
+ public var width:CGFloat = 0
+ /// Position of the display with respect to the parent view or display.
+ var position = CGPoint.zero
+ /// The range of characters supported by this item
+ public var range:NSRange=NSMakeRange(0, 0)
+ /// Whether the display has a subscript/superscript following it.
+ public var hasScript:Bool = false
+ /// The text color for this display
+ var textColor: MTColor?
+ /// The local color, if the color was mutated local with the color command
+ var localTextColor: MTColor?
+ /// The background color for this display
+ var localBackgroundColor: MTColor?
+
+}
+
+/// Special class to be inherited from that implements the DownShift protocol
+class MTDisplayDS : MTDisplay, DownShift {
+
+ var shiftDown: CGFloat = 0
+
+}
+
+// MARK: - MTCTLineDisplay
+
+/// A rendering of a single CTLine as an MTDisplay
+public class MTCTLineDisplay : MTDisplay {
+
+ /// The CTLine being displayed
+ public var line:CTLine!
+ /// The attributed string used to generate the CTLineRef. Note setting this does not reset the dimensions of
+ /// the display. So set only when
+ var attributedString:NSAttributedString? {
+ didSet {
+ line = CTLineCreateWithAttributedString(attributedString!)
+ }
+ }
+
+ /// An array of MTMathAtoms that this CTLine displays. Used for indexing back into the MTMathList
+ public fileprivate(set) var atoms = [MTMathAtom]()
+
+ init(withString attrString:NSAttributedString?, position:CGPoint, range:NSRange, font:MTFont?, atoms:[MTMathAtom]) {
+ super.init()
+ self.position = position
+ self.attributedString = attrString
+ self.line = CTLineCreateWithAttributedString(attrString!)
+ self.range = range
+ self.atoms = atoms
+ // We can't use typographic bounds here as the ascent and descent returned are for the font and not for the line.
+ // CRITICAL FIX for accented character clipping:
+ // Use the MAXIMUM of typographic width and visual width to account for glyph overhang.
+ // - Typographic width = advance width (how far the cursor moves)
+ // - Visual width = actual glyph extent (CGRectGetMaxX of glyph path bounds)
+ // Some glyphs (especially italic/oblique accented characters) extend beyond their advance width.
+ // Using max() ensures we account for overhang while maintaining proper spacing for normal glyphs.
+ let typographicWidth = CGFloat(CTLineGetTypographicBounds(line, nil, nil, nil))
+ if isIos6Supported() {
+ let bounds = CTLineGetBoundsWithOptions(line, .useGlyphPathBounds)
+ self.ascent = max(0, CGRectGetMaxY(bounds) - 0);
+ self.descent = max(0, 0 - CGRectGetMinY(bounds));
+ // Use the maximum of visual and typographic width to handle both:
+ // 1. Overhanging glyphs (visual > typographic) - prevents clipping
+ // 2. Normal glyphs (typographic >= visual) - maintains correct spacing
+ let visualWidth = CGRectGetMaxX(bounds)
+ self.width = max(typographicWidth, visualWidth);
+ } else {
+ // Our own implementation of the ios6 function to get glyph path bounds.
+ self.computeDimensions(font, typographicWidth: typographicWidth)
+ }
+ }
+
+ override var textColor: MTColor? {
+ set {
+ super.textColor = newValue
+ let attrStr = attributedString!.mutableCopy() as! NSMutableAttributedString
+ let foregroundColor = NSAttributedString.Key(kCTForegroundColorAttributeName as String)
+ attrStr.addAttribute(foregroundColor, value:self.textColor!.cgColor, range:NSMakeRange(0, attrStr.length))
+ self.attributedString = attrStr
+ }
+ get { super.textColor }
+ }
+
+ func computeDimensions(_ font:MTFont?, typographicWidth: CGFloat) {
+ let runs = CTLineGetGlyphRuns(line) as NSArray
+ var maxVisualWidth: CGFloat = 0
+ var currentX: CGFloat = 0
+
+ for obj in runs {
+ let run = obj as! CTRun?
+ let numGlyphs = CTRunGetGlyphCount(run!)
+ var glyphs = [CGGlyph]()
+ glyphs.reserveCapacity(numGlyphs)
+ CTRunGetGlyphs(run!, CFRangeMake(0, numGlyphs), &glyphs);
+ let bounds = CTFontGetBoundingRectsForGlyphs(font!.ctFont, .horizontal, glyphs, nil, numGlyphs);
+ let ascent = max(0, CGRectGetMaxY(bounds) - 0);
+ // Descent is how much the line goes below the origin. However if the line is all above the origin, then descent can't be negative.
+ let descent = max(0, 0 - CGRectGetMinY(bounds));
+ if (ascent > self.ascent) {
+ self.ascent = ascent;
+ }
+ if (descent > self.descent) {
+ self.descent = descent;
+ }
+
+ // Calculate visual width using glyph extent
+ // Get the rightmost edge of this run's glyphs
+ let runVisualWidth = CGRectGetMaxX(bounds)
+ let runRightEdge = currentX + runVisualWidth
+
+ // Get advances to know where next run starts
+ var advances = [CGSize](repeating: CGSize.zero, count: numGlyphs)
+ CTRunGetAdvances(run!, CFRangeMake(0, numGlyphs), &advances)
+ for advance in advances {
+ currentX += advance.width
+ }
+
+ if (runRightEdge > maxVisualWidth) {
+ maxVisualWidth = runRightEdge
+ }
+ }
+
+ // Use maximum of typographic and visual width
+ self.width = max(typographicWidth, maxVisualWidth)
+ }
+
+ override public func draw(_ context: CGContext) {
+ super.draw(context)
+ context.saveGState()
+
+ context.textPosition = self.position
+ CTLineDraw(line, context)
+
+ context.restoreGState()
+ }
+
+}
+
+// MARK: - MTMathListDisplay
+
+/// An MTLine is a rendered form of MTMathList in one line.
+/// It can render itself using the draw method.
+public class MTMathListDisplay : MTDisplay {
+
+ /**
+ The type of position for a line, i.e. subscript/superscript or regular.
+ */
+ public enum LinePosition : Int {
+ /// Regular
+ case regular
+ /// Positioned at a subscript
+ case ssubscript
+ /// Positioned at a superscript
+ case superscript
+ }
+
+ /// Where the line is positioned
+ public var type:LinePosition = .regular
+ /// An array of MTDisplays which are positioned relative to the position of the
+ /// the current display.
+ public fileprivate(set) var subDisplays = [MTDisplay]()
+ /// If a subscript or superscript this denotes the location in the parent MTList. For a
+ /// regular list this is NSNotFound
+ public var index: Int = 0
+
+ init(withDisplays displays:[MTDisplay], range:NSRange) {
+ super.init()
+ self.subDisplays = displays
+ self.position = CGPoint.zero
+ self.type = .regular
+ self.index = NSNotFound
+ self.range = range
+ self.recomputeDimensions()
+ }
+
+ override var textColor: MTColor? {
+ set {
+ super.textColor = newValue
+ for displayAtom in self.subDisplays {
+ if displayAtom.localTextColor == nil {
+ displayAtom.textColor = newValue
+ } else {
+ displayAtom.textColor = displayAtom.localTextColor
+ }
+ }
+ }
+ get { super.textColor }
+ }
+
+ override public func draw(_ context: CGContext) {
+ super.draw(context)
+ context.saveGState()
+
+ // Make the current position the origin as all the positions of the sub atoms are relative to the origin.
+ context.translateBy(x: self.position.x, y: self.position.y)
+ context.textPosition = CGPoint.zero
+
+ // draw each atom separately
+ for displayAtom in self.subDisplays {
+ displayAtom.draw(context)
+ }
+
+ context.restoreGState()
+ }
+
+ func recomputeDimensions() {
+ var max_ascent:CGFloat = 0
+ var max_descent:CGFloat = 0
+ var max_width:CGFloat = 0
+ for atom in self.subDisplays {
+ let ascent = max(0, atom.position.y + atom.ascent);
+ if (ascent > max_ascent) {
+ max_ascent = ascent;
+ }
+
+ let descent = max(0, 0 - (atom.position.y - atom.descent));
+ if (descent > max_descent) {
+ max_descent = descent;
+ }
+ let width = atom.width + atom.position.x;
+ if (width > max_width) {
+ max_width = width;
+ }
+ }
+ self.ascent = max_ascent;
+ self.descent = max_descent;
+ self.width = max_width;
+ }
+
+}
+
+// MARK: - MTFractionDisplay
+
+/// Rendering of an MTFraction as an MTDisplay
+public class MTFractionDisplay : MTDisplay {
+
+ /** A display representing the numerator of the fraction. Its position is relative
+ to the parent and is not treated as a sub-display.
+ */
+ public fileprivate(set) var numerator:MTMathListDisplay?
+ /** A display representing the denominator of the fraction. Its position is relative
+ to the parent is not treated as a sub-display.
+ */
+ public fileprivate(set) var denominator:MTMathListDisplay?
+
+ var numeratorUp:CGFloat=0 { didSet { self.updateNumeratorPosition() } }
+ var denominatorDown:CGFloat=0 { didSet { self.updateDenominatorPosition() } }
+ var linePosition:CGFloat=0
+ var lineThickness:CGFloat=0
+
+ init(withNumerator numerator:MTMathListDisplay?, denominator:MTMathListDisplay?, position:CGPoint, range:NSRange) {
+ super.init()
+ self.numerator = numerator;
+ self.denominator = denominator;
+ self.position = position;
+
+ // CRITICAL FIX: Handle invalid ranges gracefully
+ // When table cells are typeset independently with maxWidth, atoms may have
+ // ranges that are invalid in the cell's context (e.g., (0,0) or other issues)
+ // Instead of crashing with assertion, normalize the range
+ if range.length == 0 {
+ // Create a dummy range with length 1 at the given location
+ self.range = NSMakeRange(range.location, 1)
+ } else {
+ self.range = range;
+ // Still assert for debugging if range length is something unexpected
+ assert(self.range.length == 1, "Fraction range length not 1 - range (\(range.location), \(range.length)")
+ }
+ }
+
+ override public var ascent:CGFloat {
+ set { super.ascent = newValue }
+ get { (numerator?.ascent ?? 0) + self.numeratorUp }
+ }
+
+ override public var descent:CGFloat {
+ set { super.descent = newValue }
+ get { (denominator?.descent ?? 0) + self.denominatorDown }
+ }
+
+ override public var width:CGFloat {
+ set { super.width = newValue }
+ get { max(numerator?.width ?? 0, denominator?.width ?? 0) }
+ }
+
+ func updateDenominatorPosition() {
+ guard denominator != nil else { return }
+ denominator!.position = CGPointMake(self.position.x + (self.width - denominator!.width)/2, self.position.y - self.denominatorDown)
+ }
+
+ func updateNumeratorPosition() {
+ guard numerator != nil else { return }
+ numerator!.position = CGPointMake(self.position.x + (self.width - numerator!.width)/2, self.position.y + self.numeratorUp)
+ }
+
+ override var position: CGPoint {
+ set {
+ super.position = newValue
+ self.updateDenominatorPosition()
+ self.updateNumeratorPosition()
+ }
+ get { super.position }
+ }
+
+ override var textColor: MTColor? {
+ set {
+ super.textColor = newValue
+ numerator?.textColor = newValue
+ denominator?.textColor = newValue
+ }
+ get { super.textColor }
+ }
+
+ override public func draw(_ context:CGContext) {
+ super.draw(context)
+ numerator?.draw(context)
+ denominator?.draw(context)
+
+ context.saveGState()
+
+ self.textColor?.setStroke()
+
+ // draw the horizontal line
+ // Note: line thickness of 0 draws the thinnest possible line - we want no line so check for 0s
+ if self.lineThickness > 0 {
+ let path = MTBezierPath()
+ path.move(to: CGPointMake(self.position.x, self.position.y + self.linePosition))
+ path.addLine(to: CGPointMake(self.position.x + self.width, self.position.y + self.linePosition))
+ path.lineWidth = self.lineThickness
+ path.stroke()
+ }
+
+ context.restoreGState()
+ }
+
+}
+
+// MARK: - MTRadicalDisplay
+
+/// Rendering of an MTRadical as an MTDisplay
+class MTRadicalDisplay : MTDisplay {
+
+ /** A display representing the radicand of the radical. Its position is relative
+ to the parent is not treated as a sub-display.
+ */
+ public fileprivate(set) var radicand:MTMathListDisplay?
+ /** A display representing the degree of the radical. Its position is relative
+ to the parent is not treated as a sub-display.
+ */
+ public fileprivate(set) var degree:MTMathListDisplay?
+
+ override var position: CGPoint {
+ set {
+ super.position = newValue
+ self.updateRadicandPosition()
+ }
+ get { super.position }
+ }
+
+ override var textColor: MTColor? {
+ set {
+ super.textColor = newValue
+ self.radicand?.textColor = newValue
+ self.degree?.textColor = newValue
+ }
+ get { super.textColor }
+ }
+
+ private var _radicalGlyph:MTDisplay?
+ private var _radicalShift:CGFloat=0
+
+ var topKern:CGFloat=0
+ var lineThickness:CGFloat=0
+
+ init(withRadicand radicand:MTMathListDisplay?, glyph:MTDisplay, position:CGPoint, range:NSRange) {
+ super.init()
+ self.radicand = radicand
+ _radicalGlyph = glyph
+ _radicalShift = 0
+
+ self.position = position
+ self.range = range
+ }
+
+ func setDegree(_ degree:MTMathListDisplay?, fontMetrics:MTFontMathTable?) {
+ // sets up the degree of the radical
+ var kernBefore = fontMetrics!.radicalKernBeforeDegree;
+ let kernAfter = fontMetrics!.radicalKernAfterDegree;
+ let raise = fontMetrics!.radicalDegreeBottomRaisePercent * (self.ascent - self.descent);
+
+ // The layout is:
+ // kernBefore, raise, degree, kernAfter, radical
+ self.degree = degree;
+
+ // the radical is now shifted by kernBefore + degree.width + kernAfter
+ _radicalShift = kernBefore + degree!.width + kernAfter;
+ if _radicalShift < 0 {
+ // we can't have the radical shift backwards, so instead we increase the kernBefore such
+ // that _radicalShift will be 0.
+ kernBefore -= _radicalShift;
+ _radicalShift = 0;
+ }
+
+ // Note: position of degree is relative to parent.
+ self.degree!.position = CGPointMake(self.position.x + kernBefore, self.position.y + raise);
+ // Update the width by the _radicalShift
+ self.width = _radicalShift + _radicalGlyph!.width + self.radicand!.width;
+ // update the position of the radicand
+ self.updateRadicandPosition()
+ }
+
+ func updateRadicandPosition() {
+ // The position of the radicand includes the position of the MTRadicalDisplay
+ // This is to make the positioning of the radical consistent with fractions and
+ // have the cursor position finding algorithm work correctly.
+ // move the radicand by the width of the radical sign
+ self.radicand!.position = CGPointMake(self.position.x + _radicalShift + _radicalGlyph!.width, self.position.y);
+ }
+
+ override public func draw(_ context: CGContext) {
+ super.draw(context)
+
+ // draw the radicand & degree at its position
+ self.radicand?.draw(context)
+ self.degree?.draw(context)
+
+ context.saveGState();
+ self.textColor?.setStroke()
+ self.textColor?.setFill()
+
+ // Make the current position the origin as all the positions of the sub atoms are relative to the origin.
+ context.translateBy(x: self.position.x + _radicalShift, y: self.position.y);
+ context.textPosition = CGPoint.zero
+
+ // Draw the glyph.
+ _radicalGlyph?.draw(context)
+
+ // Draw the VBOX
+ // for the kern of, we don't need to draw anything.
+ let heightFromTop = topKern;
+
+ // draw the horizontal line with the given thickness
+ let path = MTBezierPath()
+ let lineStart = CGPointMake(_radicalGlyph!.width, self.ascent - heightFromTop - self.lineThickness / 2); // subtract half the line thickness to center the line
+ let lineEnd = CGPointMake(lineStart.x + self.radicand!.width, lineStart.y);
+ path.move(to: lineStart)
+ path.addLine(to: lineEnd)
+ path.lineWidth = lineThickness
+ path.lineCapStyle = .round
+ path.stroke()
+
+ context.restoreGState();
+ }
+}
+
+// MARK: - MTGlyphDisplay
+
+/// Rendering a glyph as a display
+class MTGlyphDisplay : MTDisplayDS {
+
+ var glyph:CGGlyph!
+ var font:MTFont?
+ /// Horizontal scale factor for stretching glyphs (1.0 = no scaling)
+ var scaleX: CGFloat = 1.0
+
+ init(withGlpyh glyph:CGGlyph, range:NSRange, font:MTFont?) {
+ super.init()
+ self.font = font
+ self.glyph = glyph
+
+ self.position = CGPoint.zero
+ self.range = range
+ }
+
+ override public func draw(_ context: CGContext) {
+ super.draw(context)
+ context.saveGState()
+
+ self.textColor?.setFill()
+
+ // Make the current position the origin as all the positions of the sub atoms are relative to the origin.
+
+ context.translateBy(x: self.position.x, y: self.position.y - self.shiftDown);
+
+ // Apply horizontal scaling if needed (for stretchy arrows)
+ if scaleX != 1.0 {
+ context.scaleBy(x: scaleX, y: 1.0)
+ }
+
+ context.textPosition = CGPoint.zero
+
+ var pos = CGPoint.zero
+ CTFontDrawGlyphs(font!.ctFont, &glyph, &pos, 1, context);
+
+ context.restoreGState();
+ }
+
+ override var ascent:CGFloat {
+ set { super.ascent = newValue }
+ get { super.ascent - self.shiftDown }
+ }
+
+ override var descent:CGFloat {
+ set { super.descent = newValue }
+ get { super.descent + self.shiftDown }
+ }
+}
+
+// MARK: - MTGlyphConstructionDisplay
+
+class MTGlyphConstructionDisplay:MTDisplayDS {
+ var glyphs = [CGGlyph]()
+ var positions = [CGPoint]()
+ var font:MTFont?
+ var numGlyphs:Int=0
+
+ init(withGlyphs glyphs:[NSNumber?], offsets:[NSNumber?], font:MTFont?) {
+ super.init()
+ assert(glyphs.count == offsets.count, "Glyphs and offsets need to match")
+ self.numGlyphs = glyphs.count;
+ self.glyphs = [CGGlyph](repeating: CGGlyph(), count: self.numGlyphs) //malloc(sizeof(CGGlyph) * _numGlyphs);
+ self.positions = [CGPoint](repeating: CGPoint.zero, count: self.numGlyphs) //malloc(sizeof(CGPoint) * _numGlyphs);
+ for i in 0 ..< self.numGlyphs {
+ self.glyphs[i] = glyphs[i]!.uint16Value
+ self.positions[i] = CGPointMake(0, CGFloat(offsets[i]!.floatValue))
+ }
+ self.font = font
+ self.position = CGPoint.zero
+ }
+
+ override public func draw(_ context: CGContext) {
+ super.draw(context)
+ context.saveGState()
+
+ self.textColor?.setFill()
+
+ // Make the current position the origin as all the positions of the sub atoms are relative to the origin.
+ context.translateBy(x: self.position.x, y: self.position.y - self.shiftDown)
+ context.textPosition = CGPoint.zero
+
+ // Draw the glyphs.
+ CTFontDrawGlyphs(font!.ctFont, glyphs, positions, numGlyphs, context)
+
+ context.restoreGState()
+ }
+
+ override var ascent:CGFloat {
+ set { super.ascent = newValue }
+ get { super.ascent - self.shiftDown }
+ }
+
+ override var descent:CGFloat {
+ set { super.descent = newValue }
+ get { super.descent + self.shiftDown }
+ }
+
+}
+
+// MARK: - MTLargeOpLimitsDisplay
+
+/// Rendering a large operator with limits as an MTDisplay
+class MTLargeOpLimitsDisplay : MTDisplay {
+
+ /** A display representing the upper limit of the large operator. Its position is relative
+ to the parent is not treated as a sub-display.
+ */
+ var upperLimit:MTMathListDisplay?
+ /** A display representing the lower limit of the large operator. Its position is relative
+ to the parent is not treated as a sub-display.
+ */
+ var lowerLimit:MTMathListDisplay?
+
+ var limitShift:CGFloat=0
+ var upperLimitGap:CGFloat=0 { didSet { self.updateUpperLimitPosition() } }
+ var lowerLimitGap:CGFloat=0 { didSet { self.updateLowerLimitPosition() } }
+ var extraPadding:CGFloat=0
+
+ var nucleus:MTDisplay?
+
+ init(withNucleus nucleus:MTDisplay?, upperLimit:MTMathListDisplay?, lowerLimit:MTMathListDisplay?, limitShift:CGFloat, extraPadding:CGFloat) {
+ super.init()
+ self.upperLimit = upperLimit;
+ self.lowerLimit = lowerLimit;
+ self.nucleus = nucleus;
+
+ var maxWidth = max(nucleus!.width, upperLimit?.width ?? 0)
+ maxWidth = max(maxWidth, lowerLimit?.width ?? 0)
+
+ self.limitShift = limitShift;
+ self.upperLimitGap = 0;
+ self.lowerLimitGap = 0;
+ self.extraPadding = extraPadding; // corresponds to \xi_13 in TeX
+ self.width = maxWidth;
+ }
+
+ override var ascent:CGFloat {
+ set { super.ascent = newValue }
+ get {
+ if self.upperLimit != nil {
+ return nucleus!.ascent + extraPadding + self.upperLimit!.ascent + upperLimitGap + self.upperLimit!.descent
+ } else {
+ return nucleus!.ascent
+ }
+ }
+ }
+
+ override var descent:CGFloat {
+ set { super.descent = newValue }
+ get {
+ if self.lowerLimit != nil {
+ return nucleus!.descent + extraPadding + lowerLimitGap + self.lowerLimit!.descent + self.lowerLimit!.ascent;
+ } else {
+ return nucleus!.descent;
+ }
+ }
+ }
+
+ override var position: CGPoint {
+ set {
+ super.position = newValue
+ self.updateLowerLimitPosition()
+ self.updateUpperLimitPosition()
+ self.updateNucleusPosition()
+ }
+ get { super.position }
+ }
+
+ func updateLowerLimitPosition() {
+ if self.lowerLimit != nil {
+ // The position of the lower limit includes the position of the MTLargeOpLimitsDisplay
+ // This is to make the positioning of the radical consistent with fractions and radicals
+ // Move the starting point to below the nucleus leaving a gap of _lowerLimitGap and subtract
+ // the ascent to to get the baseline. Also center and shift it to the left by _limitShift.
+ self.lowerLimit!.position = CGPointMake(self.position.x - limitShift + (self.width - lowerLimit!.width)/2,
+ self.position.y - nucleus!.descent - lowerLimitGap - self.lowerLimit!.ascent);
+ }
+ }
+
+ func updateUpperLimitPosition() {
+ if self.upperLimit != nil {
+ // The position of the upper limit includes the position of the MTLargeOpLimitsDisplay
+ // This is to make the positioning of the radical consistent with fractions and radicals
+ // Move the starting point to above the nucleus leaving a gap of _upperLimitGap and add
+ // the descent to to get the baseline. Also center and shift it to the right by _limitShift.
+ self.upperLimit!.position = CGPointMake(self.position.x + limitShift + (self.width - self.upperLimit!.width)/2,
+ self.position.y + nucleus!.ascent + upperLimitGap + self.upperLimit!.descent);
+ }
+ }
+
+ func updateNucleusPosition() {
+ // Center the nucleus
+ nucleus?.position = CGPointMake(self.position.x + (self.width - nucleus!.width)/2, self.position.y);
+ }
+
+ override var textColor: MTColor? {
+ set {
+ super.textColor = newValue
+ self.upperLimit?.textColor = newValue
+ self.lowerLimit?.textColor = newValue
+ nucleus?.textColor = newValue
+ }
+ get { super.textColor }
+ }
+
+ override func draw(_ context:CGContext) {
+ super.draw(context)
+ // Draw the elements.
+ self.upperLimit?.draw(context)
+ self.lowerLimit?.draw(context)
+ nucleus?.draw(context)
+ }
+
+}
+
+// MARK: - MTLineDisplay
+
+/// Rendering of an list with an overline or underline
+class MTLineDisplay : MTDisplay {
+
+ /** A display representing the inner list that is underlined. Its position is relative
+ to the parent is not treated as a sub-display.
+ */
+ var inner:MTMathListDisplay?
+ var lineShiftUp:CGFloat=0
+ var lineThickness:CGFloat=0
+
+ init(withInner inner:MTMathListDisplay?, position:CGPoint, range:NSRange) {
+ super.init()
+ self.inner = inner;
+
+ self.position = position;
+ self.range = range;
+ }
+
+ override var textColor: MTColor? {
+ set {
+ super.textColor = newValue
+ inner?.textColor = newValue
+ }
+ get { super.textColor }
+ }
+
+ override var position: CGPoint {
+ set {
+ super.position = newValue
+ self.updateInnerPosition()
+ }
+ get { super.position }
+ }
+
+ override func draw(_ context:CGContext) {
+ super.draw(context)
+ self.inner?.draw(context)
+
+ context.saveGState();
+
+ self.textColor?.setStroke()
+
+ // draw the horizontal line
+ let path = MTBezierPath()
+ let lineStart = CGPointMake(self.position.x, self.position.y + self.lineShiftUp);
+ let lineEnd = CGPointMake(lineStart.x + self.inner!.width, lineStart.y);
+ path.move(to:lineStart)
+ path.addLine(to: lineEnd)
+ path.lineWidth = self.lineThickness;
+ path.stroke()
+
+ context.restoreGState();
+ }
+
+ func updateInnerPosition() {
+ self.inner?.position = CGPointMake(self.position.x, self.position.y);
+ }
+
+}
+
+// MARK: - MTAccentDisplay
+
+/// Rendering an accent as a display
+class MTAccentDisplay : MTDisplay {
+
+ /** A display representing the inner list that is accented. Its position is relative
+ to the parent is not treated as a sub-display.
+ */
+ var accentee:MTMathListDisplay?
+
+ /** A display representing the accent. Its position is relative to the current display.
+ */
+ var accent:MTGlyphDisplay?
+
+ init(withAccent glyph:MTGlyphDisplay?, accentee:MTMathListDisplay?, range:NSRange) {
+ super.init()
+ self.accent = glyph
+ self.accentee = accentee
+ self.accentee?.position = CGPoint.zero
+ self.range = range
+ }
+
+ override var textColor: MTColor? {
+ set {
+ super.textColor = newValue
+ accentee?.textColor = newValue
+ accent?.textColor = newValue
+ }
+ get { super.textColor }
+ }
+
+ override var position: CGPoint {
+ set {
+ super.position = newValue
+ self.updateAccenteePosition()
+ }
+ get { super.position }
+ }
+
+ func updateAccenteePosition() {
+ self.accentee?.position = CGPointMake(self.position.x, self.position.y);
+ }
+
+ override func draw(_ context:CGContext) {
+ super.draw(context)
+ self.accentee?.draw(context)
+
+ context.saveGState();
+ context.translateBy(x: self.position.x, y: self.position.y);
+ context.textPosition = CGPoint.zero
+
+ self.accent?.draw(context)
+
+ context.restoreGState();
+ }
+
+}
diff --git a/third-party/SwiftMath/Sources/MathRender/MTMathListIndex.swift b/third-party/SwiftMath/Sources/MathRender/MTMathListIndex.swift
new file mode 100755
index 0000000000..34bd6bfbb0
--- /dev/null
+++ b/third-party/SwiftMath/Sources/MathRender/MTMathListIndex.swift
@@ -0,0 +1,197 @@
+//
+// Created by Mike Griebling on 2022-12-31.
+// Translated from an Objective-C implementation by Kostub Deshmukh.
+//
+// This software may be modified and distributed under the terms of the
+// MIT license. See the LICENSE file for details.
+//
+
+import Foundation
+
+/**
+ * An index that points to a particular character in the MTMathList. The index is a LinkedList that represents
+ * a path from the beginning of the MTMathList to reach a particular atom in the list. The next node of the path
+ * is represented by the subIndex. The path terminates when the subIndex is nil.
+ *
+ * If there is a subIndex, the subIndexType denotes what branch the path takes (i.e. superscript, subscript,
+ * numerator, denominator etc.).
+ * e.g in the expression 25^{2/4} the index of the character 4 is represented as:
+ * (1, superscript) -> (0, denominator) -> (0, none)
+ * This can be interpreted as start at index 1 (i.e. the 5) go up to the superscript.
+ * Then look at index 0 (i.e. 2/4) and go to the denominator. Then look up index 0 (i.e. the 4) which this final
+ * index.
+ *
+ * The level of an index is the number of nodes in the LinkedList to get to the final path.
+ */
+public class MTMathListIndex {
+
+ /**
+ The type of the subindex.
+
+ The type of the subindex denotes what branch the path to the atom that this index points to takes.
+ */
+ public enum MTMathListSubIndexType: Int {
+ /// The index denotes the whole atom, subIndex is nil.
+ case none = 0
+ /// The position in the subindex is an index into the nucleus
+ case nucleus
+ /// The subindex indexes into the superscript.
+ case superscript
+ /// The subindex indexes into the subscript
+ case ssubscript
+ /// The subindex indexes into the numerator (only valid for fractions)
+ case numerator
+ /// The subindex indexes into the denominator (only valid for fractions)
+ case denominator
+ /// The subindex indexes into the radicand (only valid for radicals)
+ case radicand
+ /// The subindex indexes into the degree (only valid for radicals)
+ case degree
+ }
+
+ /// The index of the associated atom.
+ var atomIndex: Int
+
+ /// The type of subindex, e.g. superscript, numerator etc.
+ var subIndexType: MTMathListSubIndexType = .none
+
+ /// The index into the sublist.
+ var subIndex: MTMathListIndex?
+
+ var finalIndex: Int {
+ if self.subIndexType == .none {
+ return self.atomIndex
+ } else {
+ return self.subIndex?.finalIndex ?? 0
+ }
+ }
+
+ /// Returns the previous index if present. Returns `nil` if there is no previous index.
+ func prevIndex() -> MTMathListIndex? {
+ if self.subIndexType == .none {
+ if self.atomIndex > 0 {
+ return MTMathListIndex(level0Index: self.atomIndex - 1)
+ }
+ } else {
+ if let prevSubIndex = self.subIndex?.prevIndex() {
+ return MTMathListIndex(at: self.atomIndex, with: prevSubIndex, type: self.subIndexType)
+ }
+ }
+ return nil
+ }
+
+ /// Returns the next index.
+ func nextIndex() -> MTMathListIndex {
+ if self.subIndexType == .none {
+ return MTMathListIndex(level0Index: self.atomIndex + 1)
+ } else if self.subIndexType == .nucleus {
+ return MTMathListIndex(at: self.atomIndex + 1, with: self.subIndex, type: self.subIndexType)
+ } else {
+ return MTMathListIndex(at: self.atomIndex, with: self.subIndex?.nextIndex(), type: self.subIndexType)
+ }
+ }
+
+ /**
+ * Returns true if this index represents the beginning of a line. Note there may be multiple lines in a MTMathList,
+ * e.g. a superscript or a fraction numerator. This returns true if the innermost subindex points to the beginning of a
+ * line.
+ */
+ func isBeginningOfLine() -> Bool { self.finalIndex == 0 }
+
+ func isAtSameLevel(with index: MTMathListIndex?) -> Bool {
+ if self.subIndexType != index?.subIndexType {
+ return false
+ } else if self.subIndexType == .none {
+ // No subindexes, they are at the same level.
+ return true
+ } else if (self.atomIndex != index?.atomIndex) {
+ return false
+ } else {
+ return self.subIndex?.isAtSameLevel(with: index?.subIndex) ?? false
+ }
+ }
+
+ /** Returns the type of the innermost sub index. */
+ func finalSubIndexType() -> MTMathListSubIndexType {
+ if self.subIndex?.subIndex != nil {
+ return self.subIndex!.finalSubIndexType()
+ } else {
+ return self.subIndexType
+ }
+ }
+
+ /** Returns true if any of the subIndexes of this index have the given type. */
+ func hasSubIndex(ofType type: MTMathListSubIndexType) -> Bool {
+ if self.subIndexType == type {
+ return true
+ } else {
+ return self.subIndex?.hasSubIndex(ofType: type) ?? false
+ }
+ }
+
+ func levelUp(with subIndex: MTMathListIndex?, type: MTMathListSubIndexType) -> MTMathListIndex {
+ if self.subIndexType == .none {
+ return MTMathListIndex(at: self.atomIndex, with: subIndex, type: type)
+ }
+
+ return MTMathListIndex(at: self.atomIndex, with: self.subIndex?.levelUp(with: subIndex, type: type), type: self.subIndexType)
+ }
+
+ func levelDown() -> MTMathListIndex? {
+ if self.subIndexType == .none {
+ return nil
+ }
+
+ if let subIndexDown = self.subIndex?.levelDown() {
+ return MTMathListIndex(at: self.atomIndex, with: subIndexDown, type: self.subIndexType)
+ } else {
+ return MTMathListIndex(level0Index: self.atomIndex)
+ }
+ }
+
+ /** Factory function to create a `MTMathListIndex` with no subindexes.
+ @param index The index of the atom that the `MTMathListIndex` points at.
+ */
+ public init(level0Index: Int) {
+ self.atomIndex = level0Index
+ }
+
+ public convenience init(at location: Int, with subIndex: MTMathListIndex?, type: MTMathListSubIndexType) {
+ self.init(level0Index: location)
+ self.subIndexType = type
+ self.subIndex = subIndex
+ }
+}
+
+extension MTMathListIndex: CustomStringConvertible {
+ public var description: String {
+ if self.subIndex != nil {
+ return "[\(self.atomIndex), \(self.subIndexType.rawValue):\(self.subIndex!)]"
+ }
+ return "[\(self.atomIndex)]"
+ }
+}
+
+extension MTMathListIndex: Hashable {
+
+ public func hash(into hasher: inout Hasher) {
+ hasher.combine(self.atomIndex)
+ hasher.combine(self.subIndexType)
+ hasher.combine(self.subIndex)
+ }
+
+}
+
+extension MTMathListIndex: Equatable {
+ public static func ==(lhs: MTMathListIndex, rhs: MTMathListIndex) -> Bool {
+ if lhs.atomIndex != rhs.atomIndex || lhs.subIndexType != rhs.subIndexType {
+ return false
+ }
+
+ if rhs.subIndex != nil {
+ return rhs.subIndex == lhs.subIndex
+ } else {
+ return lhs.subIndex == nil
+ }
+ }
+}
diff --git a/third-party/SwiftMath/Sources/MathRender/MTMathRenderer.swift b/third-party/SwiftMath/Sources/MathRender/MTMathRenderer.swift
new file mode 100644
index 0000000000..8243148734
--- /dev/null
+++ b/third-party/SwiftMath/Sources/MathRender/MTMathRenderer.swift
@@ -0,0 +1,83 @@
+import Foundation
+
+#if os(iOS) || os(visionOS)
+import UIKit
+#endif
+
+#if os(macOS)
+import AppKit
+#endif
+
+public struct MTMathRenderedFormula {
+ public let image: MTImage
+ public let size: CGSize
+ public let width: CGFloat
+ public let ascent: CGFloat
+ public let descent: CGFloat
+
+ public init(image: MTImage, size: CGSize, width: CGFloat, ascent: CGFloat, descent: CGFloat) {
+ self.image = image
+ self.size = size
+ self.width = width
+ self.ascent = ascent
+ self.descent = descent
+ }
+}
+
+public enum MTMathRenderer {
+ public static func render(latex: String, fontSize: CGFloat, textColor: MTColor, mode: MTMathUILabelMode = .display) -> MTMathRenderedFormula? {
+ guard let font = MTFontManager.fontManager.defaultFont?.copy(withSize: fontSize) else {
+ return nil
+ }
+
+ var error: NSError?
+ guard let mathList = MTMathListBuilder.build(fromString: latex, error: &error), error == nil else {
+ return nil
+ }
+
+ let style: MTLineStyle
+ switch mode {
+ case .display:
+ style = .display
+ case .text:
+ style = .text
+ }
+
+ guard let displayList = MTTypesetter.createLineForMathList(mathList, font: font, style: style) else {
+ return nil
+ }
+
+ displayList.textColor = textColor
+
+ let width = max(1.0, ceil(displayList.width))
+ let height = max(1.0, ceil(displayList.ascent + displayList.descent))
+ let size = CGSize(width: width, height: height)
+ displayList.position = CGPoint(x: 0.0, y: displayList.descent)
+
+ #if os(iOS) || os(visionOS)
+ let renderer = UIGraphicsImageRenderer(size: size)
+ let image = renderer.image { rendererContext in
+ rendererContext.cgContext.saveGState()
+ var transform = CGAffineTransform(scaleX: 1.0, y: -1.0)
+ transform = transform.translatedBy(x: 0.0, y: -size.height)
+ rendererContext.cgContext.concatenate(transform)
+ displayList.draw(rendererContext.cgContext)
+ rendererContext.cgContext.restoreGState()
+ }
+ return MTMathRenderedFormula(image: image, size: size, width: displayList.width, ascent: displayList.ascent, descent: displayList.descent)
+ #endif
+
+ #if os(macOS)
+ let image = NSImage(size: size, flipped: false) { _ in
+ guard let context = NSGraphicsContext.current?.cgContext else {
+ return false
+ }
+ context.saveGState()
+ displayList.draw(context)
+ context.restoreGState()
+ return true
+ }
+ return MTMathRenderedFormula(image: image, size: size, width: displayList.width, ascent: displayList.ascent, descent: displayList.descent)
+ #endif
+ }
+}
diff --git a/third-party/SwiftMath/Sources/MathRender/MTMathUILabel.swift b/third-party/SwiftMath/Sources/MathRender/MTMathUILabel.swift
new file mode 100644
index 0000000000..b19aabba33
--- /dev/null
+++ b/third-party/SwiftMath/Sources/MathRender/MTMathUILabel.swift
@@ -0,0 +1,587 @@
+//
+// Created by Mike Griebling on 2022-12-31.
+// Translated from an Objective-C implementation by Kostub Deshmukh.
+//
+// This software may be modified and distributed under the terms of the
+// MIT license. See the LICENSE file for details.
+//
+
+import Foundation
+import CoreText
+
+/**
+ Different display styles supported by the `MTMathUILabel`.
+
+ The only significant difference between the two modes is how fractions
+ and limits on large operators are displayed.
+ */
+public enum MTMathUILabelMode {
+ /// Display mode. Equivalent to $$ in TeX
+ case display
+ /// Text mode. Equivalent to $ in TeX.
+ case text
+}
+
+/**
+ Horizontal text alignment for `MTMathUILabel`.
+ */
+public enum MTTextAlignment : UInt {
+ /// Align left.
+ case left
+ /// Align center.
+ case center
+ /// Align right.
+ case right
+}
+
+/** The main view for rendering math.
+
+ `MTMathLabel` accepts either a string in LaTeX or an `MTMathList` to display. Use
+ `MTMathList` directly only if you are building it programmatically (e.g. using an
+ editor), otherwise using LaTeX is the preferable method.
+
+ The math display is centered vertically in the label. The default horizontal alignment is
+ is left. This can be changed by setting `textAlignment`. The math is default displayed in
+ *Display* mode. This can be changed using `labelMode`.
+
+ When created it uses `[MTFontManager defaultFont]` as its font. This can be changed using
+ the `font` parameter.
+ */
+@IBDesignable
+public class MTMathUILabel : MTView {
+
+ /** The `MTMathList` to render. Setting this will remove any
+ `latex` that has already been set. If `latex` has been set, this will
+ return the parsed `MTMathList` if the `latex` parses successfully. Use this
+ setting if the `MTMathList` has been programmatically constructed, otherwise it
+ is preferred to use `latex`.
+ */
+ public var mathList:MTMathList? {
+ set {
+ _mathList = newValue
+ _error = nil
+ _latex = MTMathListBuilder.mathListToString(newValue)
+ self.invalidateIntrinsicContentSize()
+ self.setNeedsLayout()
+ }
+ get { _mathList }
+ }
+ private var _mathList:MTMathList?
+
+ /** The latex string to be displayed. Setting this will remove any `mathList` that
+ has been set. If latex has not been set, this will return the latex output for the
+ `mathList` that is set.
+ @see error */
+ @IBInspectable
+ public var latex:String {
+ set {
+ _latex = newValue
+ _error = nil
+ var error : NSError? = nil
+ _mathList = MTMathListBuilder.build(fromString: newValue, error: &error)
+ if error != nil {
+ _mathList = nil
+ _error = error
+ self.errorLabel?.text = error!.localizedDescription
+ self.errorLabel?.frame = self.bounds
+ self.errorLabel?.isHidden = !self.displayErrorInline
+ } else {
+ self.errorLabel?.isHidden = true
+ }
+ self.invalidateIntrinsicContentSize()
+ self.setNeedsLayout()
+ }
+ get { _latex }
+ }
+ private var _latex = ""
+
+ /** This contains any error that occurred when parsing the latex. */
+ public var error:NSError? { _error }
+ private var _error:NSError?
+
+ /** If true, if there is an error it displays the error message inline. Default true. */
+ public var displayErrorInline = true
+
+ /** If true, enables detailed debug logging for intrinsicContentSize calculations.
+ This helps diagnose rendering issues like missing row breaks in aligned environments.
+ Default false. */
+ public var debugLogging = false
+
+ /** The MTFont to use for rendering. */
+ public var font:MTFont? {
+ set {
+ guard newValue != nil else { return }
+ _font = newValue
+ self.invalidateIntrinsicContentSize()
+ self.setNeedsLayout()
+ }
+ get { _font }
+ }
+ private var _font:MTFont?
+
+ /** Convenience method to just set the size of the font without changing the fontface. */
+ @IBInspectable
+ public var fontSize:CGFloat {
+ set {
+ _fontSize = newValue
+ let font = font?.copy(withSize: newValue)
+ self.font = font // also forces an update
+ }
+ get { _fontSize }
+ }
+ private var _fontSize:CGFloat=0
+
+ /** This sets the text color of the rendered math formula. The default color is black. */
+ @IBInspectable
+ public var textColor:MTColor? {
+ set {
+ guard newValue != nil else { return }
+ _textColor = newValue
+ self.displayList?.textColor = newValue
+ self.setNeedsDisplay()
+ }
+ get { _textColor }
+ }
+ private var _textColor:MTColor?
+
+ /** The minimum distance from the margin of the view to the rendered math. This value is
+ `UIEdgeInsetsZero` by default. This is useful if you need some padding between the math and
+ the border/background color. sizeThatFits: will have its returned size increased by these insets.
+ */
+ @IBInspectable
+ public var contentInsets:MTEdgeInsets {
+ set {
+ _contentInsets = newValue
+ self.invalidateIntrinsicContentSize()
+ self.setNeedsLayout()
+ }
+ get { _contentInsets }
+ }
+ private var _contentInsets = MTEdgeInsetsZero
+
+ /** The Label mode for the label. The default mode is Display */
+ public var labelMode:MTMathUILabelMode {
+ set {
+ _labelMode = newValue
+ self.invalidateIntrinsicContentSize()
+ self.setNeedsLayout()
+ }
+ get { _labelMode }
+ }
+ private var _labelMode = MTMathUILabelMode.display
+
+ /** Horizontal alignment for the text. The default is align left. */
+ public var textAlignment:MTTextAlignment {
+ set {
+ _textAlignment = newValue
+ self.invalidateIntrinsicContentSize()
+ self.setNeedsLayout()
+ }
+ get { _textAlignment }
+ }
+ private var _textAlignment = MTTextAlignment.left
+
+ /** The internal display of the MTMathUILabel. This is for advanced use only. */
+ public var displayList: MTMathListDisplay? { _displayList }
+ private var _displayList:MTMathListDisplay?
+
+ /** The preferred maximum width (in points) for a multiline label.
+ Set this property to enable line wrapping based on available width. */
+ public var preferredMaxLayoutWidth: CGFloat {
+ set {
+ _preferredMaxLayoutWidth = newValue
+ _displayList = nil // Clear cached display list when width constraint changes
+ self.invalidateIntrinsicContentSize()
+ self.setNeedsLayout()
+ }
+ get { _preferredMaxLayoutWidth }
+ }
+ private var _preferredMaxLayoutWidth: CGFloat = 0
+
+ public var currentStyle:MTLineStyle {
+ switch _labelMode {
+ case .display: return .display
+ case .text: return .text
+ }
+ }
+
+ public var errorLabel: MTLabel?
+
+ public override init(frame: CGRect) {
+ super.init(frame: frame)
+ self.initCommon()
+ }
+
+ public required init?(coder: NSCoder) {
+ super.init(coder: coder)
+ self.initCommon()
+ }
+
+ func initCommon() {
+#if os(macOS)
+ self.layer?.isGeometryFlipped = true
+#else
+ self.layer.isGeometryFlipped = true
+ self.clipsToBounds = true
+#endif
+ _fontSize = 20
+ _contentInsets = MTEdgeInsetsZero
+ _labelMode = .display
+ let font = MTFontManager.fontManager.defaultFont
+ self.font = font
+ _textAlignment = .left
+ _displayList = nil
+ displayErrorInline = true
+ self.backgroundColor = MTColor.clear
+
+ _textColor = MTColor.black
+ let label = MTLabel()
+ self.errorLabel = label
+#if os(macOS)
+ label.layer?.isGeometryFlipped = true
+#else
+ label.layer.isGeometryFlipped = true
+#endif
+ label.isHidden = true
+ label.textColor = MTColor.red
+ self.addSubview(label)
+ }
+
+ override public func draw(_ dirtyRect: MTRect) {
+ super.draw(dirtyRect)
+ if self.mathList == nil { return }
+ if self.font == nil { return }
+
+ // Ensure display list is created before drawing
+ if _displayList == nil {
+ _layoutSubviews()
+ }
+
+ guard let displayList = _displayList else { return }
+
+ // drawing code
+ let context = MTGraphicsGetCurrentContext()!
+ context.saveGState()
+
+ // CRITICAL FIX for clipping: If the displayList is wider than our bounds,
+ // expand the clipping rect to prevent content clipping.
+ // This handles cases where preferredMaxLayoutWidth is a hint but the content
+ // cannot fit within it even with line breaking.
+ let contentWidth = displayList.width + contentInsets.left + contentInsets.right
+ if contentWidth > bounds.size.width {
+ // Content is wider than bounds - expand clip rect
+ let expandedRect = CGRect(
+ x: bounds.origin.x,
+ y: bounds.origin.y,
+ width: contentWidth,
+ height: bounds.size.height
+ )
+ context.clip(to: expandedRect)
+ }
+
+ displayList.draw(context)
+ context.restoreGState()
+ }
+
+ func _layoutSubviews() {
+ guard _mathList != nil && self.font != nil else {
+ _displayList = nil
+ errorLabel?.frame = self.bounds
+ self.setNeedsDisplay()
+ return
+ }
+ // Ensure we have a valid font before attempting to typeset
+ if self.font == nil {
+ // No valid font - try to get default font
+ if let defaultFont = MTFontManager.fontManager.defaultFont {
+ self._font = defaultFont
+ } else {
+ // Cannot typeset without a font, clear display list
+ _displayList = nil
+ errorLabel?.frame = self.bounds
+ self.setNeedsDisplay()
+ return
+ }
+ }
+
+ // Use the effective width for layout
+ let effectiveWidth = _preferredMaxLayoutWidth > 0 ? _preferredMaxLayoutWidth : bounds.size.width
+ var availableWidth = effectiveWidth - contentInsets.left - contentInsets.right
+ // CRITICAL FIX: Ensure availableWidth is never negative
+ // Negative maxWidth passed to MTTypesetter can cause "Negative value is not representable" crashes
+ availableWidth = max(0, availableWidth)
+
+ _displayList = MTTypesetter.createLineForMathList(_mathList, font: self.font, style: currentStyle, maxWidth: availableWidth)
+
+ guard let displayList = _displayList else {
+ // Empty or invalid input - nothing to display
+ return
+ }
+
+ displayList.textColor = textColor
+ var textX = CGFloat(0)
+ switch self.textAlignment {
+ case .left: textX = contentInsets.left
+ case .center: textX = (bounds.size.width - contentInsets.left - contentInsets.right - displayList.width) / 2 + contentInsets.left
+ case .right: textX = bounds.size.width - displayList.width - contentInsets.right
+ }
+ let availableHeight = bounds.size.height - contentInsets.bottom - contentInsets.top
+
+ // center things vertically
+ var height = displayList.ascent + displayList.descent
+ if height < fontSize/2 {
+ height = fontSize/2 // set height to half the font size
+ }
+ let textY = (availableHeight - height) / 2 + displayList.descent + contentInsets.bottom
+
+ displayList.position = CGPointMake(textX, textY)
+ errorLabel?.frame = self.bounds
+ self.setNeedsDisplay()
+ }
+
+ func _sizeThatFits(_ size:CGSize) -> CGSize {
+ // Check if we have empty latex (empty string case)
+ if _latex.isEmpty {
+ // Empty latex - return zero size
+ return CGSize(width: 0, height: 0)
+ }
+
+ guard _mathList != nil else {
+ // No content - return no-intrinsic-size marker
+ return CGSize(width: -1, height: -1)
+ }
+
+ // Ensure we have a valid font before attempting to typeset
+ if self.font == nil {
+ // No valid font - try to get default font
+ if let defaultFont = MTFontManager.fontManager.defaultFont {
+ self._font = defaultFont
+ } else {
+ // Cannot typeset without a font
+ return CGSize(width: -1, height: -1)
+ }
+ }
+
+ // Determine the maximum width to use
+ var maxWidth: CGFloat = 0
+ if _preferredMaxLayoutWidth > 0 {
+ maxWidth = _preferredMaxLayoutWidth - contentInsets.left - contentInsets.right
+ // CRITICAL FIX: Ensure maxWidth is never negative
+ // If contentInsets exceed available width, clamp to 0
+ maxWidth = max(0, maxWidth)
+ } else if size.width > 0 {
+ maxWidth = size.width - contentInsets.left - contentInsets.right
+ // CRITICAL FIX: Ensure maxWidth is never negative
+ maxWidth = max(0, maxWidth)
+ }
+
+ var displayList:MTMathListDisplay? = nil
+ displayList = MTTypesetter.createLineForMathList(_mathList, font: self.font, style: currentStyle, maxWidth: maxWidth)
+
+ guard displayList != nil else {
+ // Failed to create display list
+ return CGSize(width: -1, height: -1)
+ }
+
+ var resultWidth = displayList!.width + contentInsets.left + contentInsets.right
+ var resultHeight = displayList!.ascent + displayList!.descent + contentInsets.top + contentInsets.bottom
+
+ // DEBUG LOGGING for width calculation
+ if debugLogging {
+ print("\n=== MTMathUILabel intrinsicContentSize DEBUG ===")
+ print("LaTeX: \(self.latex)")
+ // Show raw bytes to help debug escaping issues
+ let latexBytes = Array(self.latex.utf8.prefix(100))
+ print("LaTeX bytes (first 100): \(latexBytes)")
+ print("preferredMaxLayoutWidth: \(_preferredMaxLayoutWidth)")
+ print("size constraint: \(size)")
+ print("maxWidth passed to typesetter: \(maxWidth)")
+ print("displayList.width: \(displayList!.width)")
+ print("displayList.ascent: \(displayList!.ascent)")
+ print("displayList.descent: \(displayList!.descent)")
+ print("Number of subDisplays: \(displayList!.subDisplays.count)")
+ // Count lines by unique Y positions
+ let yPositions = Set(displayList!.subDisplays.map { $0.position.y })
+ print("Number of lines (unique Y positions): \(yPositions.count)")
+ print("contentInsets: \(contentInsets)")
+ print("resultWidth (before clamping): \(resultWidth)")
+ print("resultHeight (before clamping): \(resultHeight)")
+
+ // HELPFUL WARNING: Detect common escaping mistake
+ // Check for environments that typically have multiple rows but only 1 row is rendered
+ let multiRowEnvs = ["aligned", "align", "eqnarray", "gather", "split"]
+ let latexLower = self.latex.lowercased()
+ for env in multiRowEnvs {
+ if latexLower.contains("\\begin{\(env)}") {
+ // Check if there's a table with only 1 row
+ if let ml = _mathList {
+ for atom in ml.atoms {
+ if let table = atom as? MTMathTable, table.numRows == 1 {
+ // Check if latex contains single backslash (spacing) where double might be intended
+ // Pattern: \\ followed by non-backslash (like space or letter)
+ if self.latex.contains("\\ ") || self.latex.range(of: #"\\[a-zA-Z]"#, options: .regularExpression) != nil {
+ print("⚠️ WARNING: '\(env)' environment has only 1 row.")
+ print(" If you intended multiple rows, use '\\\\\\\\' (4 backslashes in Swift strings)")
+ print(" or use raw strings: #\"...42\\\\ \\\\dfrac...\"#")
+ print(" Current input has '\\\\ ' which is a SPACING command, not a row break.")
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // CRITICAL FIX: Ensure dimensions are never negative
+ // Negative values cause crashes in NSRange calculations and SwiftUI layout
+ resultWidth = max(0, resultWidth)
+ resultHeight = max(0, resultHeight)
+
+ // CRITICAL FIX for accented character clipping:
+ // The preferredMaxLayoutWidth is a HINT for line breaking, NOT a hard constraint.
+ // If the typesetter cannot fit content within that width (even with line breaking),
+ // we MUST return the actual content width to prevent clipping.
+ //
+ // ONLY clamp in extreme cases to prevent layout explosion (>50% over or >100pt over)
+ if _preferredMaxLayoutWidth > 0 && resultWidth > _preferredMaxLayoutWidth {
+ let overflow = resultWidth - _preferredMaxLayoutWidth
+ let overflowPercent = (overflow / _preferredMaxLayoutWidth) * 100
+
+ if debugLogging {
+ print(" Content exceeds preferredMaxLayoutWidth:")
+ print(" preferredMaxLayoutWidth: \(_preferredMaxLayoutWidth)")
+ print(" resultWidth: \(resultWidth)")
+ print(" overflow: \(overflow) (\(String(format: "%.1f", overflowPercent))%)")
+
+ // Check line breaking
+ let yPositions = Set(displayList!.subDisplays.map { $0.position.y })
+ print(" hasMultipleLines: \(yPositions.count > 1) (yPositions: \(yPositions.count))")
+
+ // Check if any content would be clipped at different widths
+ print(" SubDisplay analysis:")
+ for (i, sub) in displayList!.subDisplays.enumerated() {
+ let rightEdge = sub.position.x + sub.width
+ let clippedAtPreferred = rightEdge > _preferredMaxLayoutWidth
+ let clippedAtResult = rightEdge > resultWidth
+ print(" Sub[\(i)]: rightEdge=\(rightEdge) clippedAt\(_preferredMaxLayoutWidth)=\(clippedAtPreferred) clippedAt\(resultWidth)=\(clippedAtResult)")
+ }
+ }
+
+ // ONLY clamp for truly excessive overflow (>50% or >100pt)
+ // This prevents layout explosion while allowing normal overflow
+ let extremeOverflowThreshold: CGFloat = max(_preferredMaxLayoutWidth * 0.5, 100.0)
+
+ if overflow > extremeOverflowThreshold {
+ // Extreme overflow - clamp to prevent layout issues
+ let clampedWidth = _preferredMaxLayoutWidth + extremeOverflowThreshold
+ if debugLogging {
+ print(" ⚠️ EXTREME OVERFLOW - clamping from \(resultWidth) to \(clampedWidth)")
+ print(" ⚠️ WARNING: This will cause content clipping!")
+ }
+ resultWidth = clampedWidth
+ } else {
+ // Normal overflow - keep actual content width to prevent clipping
+ if debugLogging {
+ print(" ✓ Normal overflow - keeping actual width \(resultWidth) to prevent clipping")
+ }
+ // resultWidth stays as is - NO CLAMPING
+ }
+ } else if _preferredMaxLayoutWidth == 0 && size.width > 0 && resultWidth > size.width {
+ // Similar tolerance for size.width constraint
+ let tolerance = max(size.width * 0.05, 10.0)
+ let maxAllowedWidth = size.width + tolerance
+
+ if debugLogging {
+ print(" Exceeds size.width constraint:")
+ print(" tolerance: \(tolerance)")
+ print(" maxAllowedWidth: \(maxAllowedWidth)")
+ print(" overflow amount: \(resultWidth - size.width)")
+ }
+
+ if resultWidth <= maxAllowedWidth {
+ // Within tolerance - use actual content width
+ // resultWidth stays as is
+ if debugLogging {
+ print(" ✓ Within tolerance - keeping actual width")
+ }
+ } else {
+ if debugLogging {
+ print(" ⚠️ CLAMPING to maxAllowedWidth (may clip content!)")
+ }
+ resultWidth = maxAllowedWidth
+ }
+ }
+
+ if debugLogging {
+ print(" Final resultWidth: \(resultWidth)")
+ print(" Final resultHeight: \(resultHeight)")
+
+ // Check if any display elements would be clipped
+ if let display = displayList {
+ print(" Display subdisplays: \(display.subDisplays.count)")
+ let yPositions = Set(display.subDisplays.map { $0.position.y }).sorted()
+ print(" Unique Y positions (lines): \(yPositions.count) -> \(yPositions)")
+
+ for (i, sub) in display.subDisplays.enumerated() {
+ let rightEdge = sub.position.x + sub.width
+ let clipped = rightEdge > resultWidth
+
+ // Extract text content if this is a CTLineDisplay
+ var textContent = ""
+ if let ctLineDisplay = sub as? MTCTLineDisplay,
+ let attrString = ctLineDisplay.attributedString {
+ textContent = " text=\"\(attrString.string)\""
+ }
+
+ print(" Sub[\(i)]: type=\(type(of: sub)), y=\(sub.position.y), x=\(sub.position.x), width=\(sub.width), rightEdge=\(rightEdge)\(textContent)\(clipped ? " ⚠️ CLIPPED" : "")")
+
+ // Show internal structure for MTMathListDisplay
+ if let mathListDisplay = sub as? MTMathListDisplay, !mathListDisplay.subDisplays.isEmpty {
+ print(" → Contains \(mathListDisplay.subDisplays.count) sub-displays:")
+ for (j, innerSub) in mathListDisplay.subDisplays.enumerated() {
+ var innerTextContent = ""
+ if let innerCTLineDisplay = innerSub as? MTCTLineDisplay,
+ let innerAttrString = innerCTLineDisplay.attributedString {
+ innerTextContent = " text=\"\(innerAttrString.string)\""
+ }
+ print(" [\(j)]: type=\(type(of: innerSub)), y=\(innerSub.position.y), x=\(innerSub.position.x), width=\(innerSub.width)\(innerTextContent)")
+ }
+ }
+
+ if clipped {
+ print(" ⚠️ CLIPPING: rightEdge \(rightEdge) > resultWidth \(resultWidth)")
+ print(" Clipped amount: \(rightEdge - resultWidth)")
+ }
+ }
+ }
+ print("=== END DEBUG ===\n")
+ }
+
+ return CGSize(width: resultWidth, height: resultHeight)
+ }
+
+ #if os(macOS)
+ public func sizeThatFits(_ size: CGSize) -> CGSize {
+ return _sizeThatFits(size)
+ }
+ #else
+ public override func sizeThatFits(_ size: CGSize) -> CGSize {
+ return _sizeThatFits(size)
+ }
+ #endif
+
+#if os(macOS)
+ func setNeedsDisplay() { self.needsDisplay = true }
+ func setNeedsLayout() { self.needsLayout = true }
+ public override var fittingSize: CGSize { _sizeThatFits(CGSizeZero) }
+ public override var intrinsicContentSize: CGSize { _sizeThatFits(CGSizeZero) }
+ override public func layout() {
+ self._layoutSubviews()
+ super.layout()
+ }
+#else
+ public override var intrinsicContentSize: CGSize { _sizeThatFits(CGSizeZero) }
+ override public func layoutSubviews() { _layoutSubviews() }
+#endif
+
+}
diff --git a/third-party/SwiftMath/Sources/MathRender/MTTypesetter+Tokenization.swift b/third-party/SwiftMath/Sources/MathRender/MTTypesetter+Tokenization.swift
new file mode 100644
index 0000000000..3911df6499
--- /dev/null
+++ b/third-party/SwiftMath/Sources/MathRender/MTTypesetter+Tokenization.swift
@@ -0,0 +1,68 @@
+//
+// MTTypesetter+Tokenization.swift
+// SwiftMath
+//
+// Created by Claude Code on 2025-12-16.
+// This software may be modified and distributed under the terms of the
+// MIT license. See the LICENSE file for details.
+//
+
+import Foundation
+import CoreGraphics
+
+extension MTTypesetter {
+
+ /// Create a line for a math list using the new tokenization approach
+ /// This is an alternative to the existing createLineForMathList that uses
+ /// pre-tokenization and greedy line fitting
+ static func createLineForMathListWithTokenization(
+ _ mathList: MTMathList?,
+ font: MTFont?,
+ style: MTLineStyle,
+ cramped: Bool,
+ spaced: Bool,
+ maxWidth: CGFloat
+ ) -> MTMathListDisplay? {
+ guard let mathList = mathList else { return nil }
+ guard let font = font else { return nil }
+ guard !mathList.atoms.isEmpty else {
+ // Return empty display instead of nil (matches KaTeX behavior)
+ return MTMathListDisplay(withDisplays: [], range: NSMakeRange(0, 0))
+ }
+
+ // Phase 0: Preprocess atoms to fuse ordinary characters
+ // This is critical for accents and other structures where multi-character
+ // text like "xyzw" should stay together as a single atom
+ let preprocessedAtoms = MTTypesetter.preprocessMathList(mathList)
+
+ // Phase 1: Tokenize atoms into breakable elements
+ let tokenizer = MTAtomTokenizer(font: font, style: style, cramped: cramped, maxWidth: maxWidth)
+ let elements = tokenizer.tokenize(preprocessedAtoms)
+
+ guard !elements.isEmpty else { return nil }
+
+ // Phase 2: Fit elements into lines
+ let margin = spaced ? font.mathTable?.muUnit ?? 0 : 0
+ let fitter = MTLineFitter(maxWidth: maxWidth, margin: margin)
+ let fittedLines = fitter.fitLines(elements)
+
+ // Phase 3: Generate displays from fitted lines
+ let generator = MTDisplayGenerator(font: font, style: style)
+ let displays = generator.generateDisplays(from: fittedLines, startPosition: CGPoint.zero)
+
+ // Determine range from atoms
+ let range: NSRange
+ if let firstAtom = mathList.atoms.first, let lastAtom = mathList.atoms.last {
+ let start = firstAtom.indexRange.location
+ let end = NSMaxRange(lastAtom.indexRange)
+ range = NSMakeRange(start, end - start)
+ } else {
+ range = NSMakeRange(0, 0)
+ }
+
+ // Create and return the math list display
+ let mathListDisplay = MTMathListDisplay(withDisplays: displays, range: range)
+
+ return mathListDisplay
+ }
+}
diff --git a/third-party/SwiftMath/Sources/MathRender/MTTypesetter.swift b/third-party/SwiftMath/Sources/MathRender/MTTypesetter.swift
new file mode 100644
index 0000000000..72d67a2315
--- /dev/null
+++ b/third-party/SwiftMath/Sources/MathRender/MTTypesetter.swift
@@ -0,0 +1,1927 @@
+//
+// Created by Mike Griebling on 2022-12-31.
+// Translated from an Objective-C implementation by Kostub Deshmukh.
+//
+// This software may be modified and distributed under the terms of the
+// MIT license. See the LICENSE file for details.
+//
+
+import Foundation
+import CoreText
+
+// MARK: - Inter Element Spacing
+
+enum InterElementSpaceType : Int {
+ case invalid = -1
+ case none = 0
+ case thin
+ case nsThin // Thin but not in script mode
+ case nsMedium
+ case nsThick
+}
+
+var interElementSpaceArray = [[InterElementSpaceType]]()
+private let interElementLock = NSLock()
+
+func getInterElementSpaces() -> [[InterElementSpaceType]] {
+ if interElementSpaceArray.isEmpty {
+
+ interElementLock.lock()
+ defer { interElementLock.unlock() }
+ guard interElementSpaceArray.isEmpty else { return interElementSpaceArray }
+
+ interElementSpaceArray =
+ // ordinary operator binary relation open close punct fraction
+ [ [.none, .thin, .nsMedium, .nsThick, .none, .none, .none, .nsThin], // ordinary
+ [.thin, .thin, .invalid, .nsThick, .none, .none, .none, .nsThin], // operator
+ [.nsMedium, .nsMedium, .invalid, .invalid, .nsMedium, .invalid, .invalid, .nsMedium], // binary
+ [.nsThick, .nsThick, .invalid, .none, .nsThick, .none, .none, .nsThick], // relation
+ [.none, .none, .invalid, .none, .none, .none, .none, .none], // open
+ [.none, .thin, .nsMedium, .nsThick, .none, .none, .none, .nsThin], // close
+ [.nsThin, .nsThin, .invalid, .nsThin, .nsThin, .nsThin, .nsThin, .nsThin], // punct
+ [.nsThin, .thin, .nsMedium, .nsThick, .nsThin, .none, .nsThin, .nsThin], // fraction
+ [.nsMedium, .nsThin, .nsMedium, .nsThick, .none, .none, .none, .nsThin]] // radical
+ }
+ return interElementSpaceArray
+}
+
+
+// Get's the index for the given type. If row is true, the index is for the row (i.e. left element) otherwise it is for the column (right element)
+func getInterElementSpaceArrayIndexForType(_ type:MTMathAtomType, row:Bool) -> Int {
+ switch type {
+ case .color, .textcolor, .colorBox, .ordinary, .placeholder: // A placeholder is treated as ordinary
+ return 0
+ case .largeOperator:
+ return 1
+ case .binaryOperator:
+ return 2;
+ case .relation:
+ return 3;
+ case .open:
+ return 4;
+ case .close:
+ return 5;
+ case .punctuation:
+ return 6;
+ case .fraction, // Fraction and inner are treated the same.
+ .inner:
+ return 7;
+ case .radical:
+ if row {
+ // Radicals have inter element spaces only when on the left side.
+ // Note: This is a departure from latex but we don't want \sqrt{4}4 to look weird so we put a space in between.
+ // They have the same spacing as ordinary except with ordinary.
+ return 8;
+ } else {
+ // Treat radical as ordinary on the right side
+ return 0
+ }
+ // Numbers, variables, and unary operators are treated as ordinary
+ case .number, .variable, .unaryOperator:
+ return 0
+ // Decorative types (accent, underline, overline) are treated as ordinary
+ case .accent, .underline, .overline:
+ return 0
+ // Special types that don't typically participate in spacing are treated as ordinary
+ case .boundary, .space, .style, .table:
+ return 0
+ }
+}
+
+// MARK: - Italics
+// mathit
+func getItalicized(_ ch:Character) -> UTF32Char {
+ var unicode = ch.utf32Char
+
+ // Special cases for italics
+ if ch == "h" { return UnicodeSymbol.planksConstant }
+ // Dotless i (U+0131) → Mathematical Italic Small Dotless I (U+1D6A4)
+ if ch == "\u{0131}" { return 0x1D6A4 }
+ // Dotless j (U+0237) → Mathematical Italic Small Dotless J (U+1D6A5)
+ if ch == "\u{0237}" { return 0x1D6A5 }
+
+ if ch.isUpperEnglish {
+ unicode = UnicodeSymbol.capitalItalicStart + (ch.utf32Char - Character("A").utf32Char)
+ } else if ch.isLowerEnglish {
+ unicode = UnicodeSymbol.lowerItalicStart + (ch.utf32Char - Character("a").utf32Char)
+ } else if ch.isCapitalGreek {
+ // Capital Greek characters
+ unicode = UnicodeSymbol.greekCapitalItalicStart + (ch.utf32Char - UnicodeSymbol.capitalGreekStart)
+ } else if ch.isLowerGreek {
+ // Greek characters
+ unicode = UnicodeSymbol.greekLowerItalicStart + (ch.utf32Char - UnicodeSymbol.lowerGreekStart)
+ } else if ch.isGreekSymbol {
+ return UnicodeSymbol.greekSymbolItalicStart + ch.greekSymbolOrder!
+ }
+ // Note there are no italicized numbers in unicode so we don't support italicizing numbers.
+ return unicode
+}
+
+// mathbf
+func getBold(_ ch:Character) -> UTF32Char {
+ var unicode = ch.utf32Char
+ if ch.isUpperEnglish {
+ unicode = UnicodeSymbol.mathCapitalBoldStart + (ch.utf32Char - Character("A").utf32Char)
+ } else if ch.isLowerEnglish {
+ unicode = UnicodeSymbol.mathLowerBoldStart + (ch.utf32Char - Character("a").utf32Char)
+ } else if ch.isCapitalGreek {
+ // Capital Greek characters
+ unicode = UnicodeSymbol.greekCapitalBoldStart + (ch.utf32Char - UnicodeSymbol.capitalGreekStart);
+ } else if ch.isLowerGreek {
+ // Greek characters
+ unicode = UnicodeSymbol.greekLowerBoldStart + (ch.utf32Char - UnicodeSymbol.lowerGreekStart);
+ } else if ch.isGreekSymbol {
+ return UnicodeSymbol.greekSymbolBoldStart + ch.greekSymbolOrder!
+ } else if ch.isNumber {
+ unicode = UnicodeSymbol.numberBoldStart + (ch.utf32Char - Character("0").utf32Char)
+ }
+ return unicode
+}
+
+// mathbfit
+func getBoldItalic(_ ch:Character) -> UTF32Char {
+ var unicode = ch.utf32Char
+ if ch.isUpperEnglish {
+ unicode = UnicodeSymbol.mathCapitalBoldItalicStart + (ch.utf32Char - Character("A").utf32Char)
+ } else if ch.isLowerEnglish {
+ unicode = UnicodeSymbol.mathLowerBoldItalicStart + (ch.utf32Char - Character("a").utf32Char)
+ } else if ch.isCapitalGreek {
+ // Capital Greek characters
+ unicode = UnicodeSymbol.greekCapitalBoldItalicStart + (ch.utf32Char - UnicodeSymbol.capitalGreekStart);
+ } else if ch.isLowerGreek {
+ // Greek characters
+ unicode = UnicodeSymbol.greekLowerBoldItalicStart + (ch.utf32Char - UnicodeSymbol.lowerGreekStart);
+ } else if ch.isGreekSymbol {
+ return UnicodeSymbol.greekSymbolBoldItalicStart + ch.greekSymbolOrder!
+ } else if ch.isNumber {
+ // No bold italic for numbers so we just bold them.
+ unicode = getBold(ch);
+ }
+ return unicode;
+}
+
+// LaTeX default
+func getDefaultStyle(_ ch:Character) -> UTF32Char {
+ if ch.isLowerEnglish || ch.isUpperEnglish || ch.isLowerGreek || ch.isGreekSymbol {
+ return getItalicized(ch);
+ } else if ch == "\u{0131}" || ch == "\u{0237}" {
+ // Dotless i (U+0131) and dotless j (U+0237) should be italicized in default style
+ return getItalicized(ch);
+ } else if ch.isNumber || ch.isCapitalGreek {
+ // In the default style numbers and capital greek is roman
+ return ch.utf32Char
+ } else if ch == "." {
+ // . is treated as a number in our code, but it doesn't change fonts.
+ return ch.utf32Char
+ } else {
+ NSException(name: NSExceptionName("IllegalCharacter"), reason: "Unknown character \(ch) for default style.").raise()
+ }
+ return ch.utf32Char
+}
+
+// mathcal/mathscr (caligraphic or script)
+func getCaligraphic(_ ch:Character) -> UTF32Char {
+ // Caligraphic has lots of exceptions:
+ switch ch {
+ case "B":
+ return 0x212C; // Script B (bernoulli)
+ case "E":
+ return 0x2130; // Script E (emf)
+ case "F":
+ return 0x2131; // Script F (fourier)
+ case "H":
+ return 0x210B; // Script H (hamiltonian)
+ case "I":
+ return 0x2110; // Script I
+ case "L":
+ return 0x2112; // Script L (laplace)
+ case "M":
+ return 0x2133; // Script M (M-matrix)
+ case "R":
+ return 0x211B; // Script R (Riemann integral)
+ case "e":
+ return 0x212F; // Script e (Natural exponent)
+ case "g":
+ return 0x210A; // Script g (real number)
+ case "o":
+ return 0x2134; // Script o (order)
+ default:
+ break;
+ }
+ var unicode:UTF32Char
+ if ch.isUpperEnglish {
+ unicode = UnicodeSymbol.mathCapitalScriptStart + (ch.utf32Char - Character("A").utf32Char)
+ } else if ch.isLowerEnglish {
+ // Latin Modern Math does not have lower case caligraphic characters, so we use
+ // the default style instead of showing a ?
+ unicode = getDefaultStyle(ch)
+ } else {
+ // Caligraphic characters don't exist for greek or numbers, we give them the
+ // default treatment.
+ unicode = getDefaultStyle(ch)
+ }
+ return unicode;
+}
+
+// mathtt (monospace)
+func getTypewriter(_ ch:Character) -> UTF32Char {
+ if ch.isUpperEnglish {
+ return UnicodeSymbol.mathCapitalTTStart + (ch.utf32Char - Character("A").utf32Char)
+ } else if ch.isLowerEnglish {
+ return UnicodeSymbol.mathLowerTTStart + (ch.utf32Char - Character("a").utf32Char)
+ } else if ch.isNumber {
+ return UnicodeSymbol.numberTTStart + (ch.utf32Char - Character("0").utf32Char)
+ }
+ // Monospace characters don't exist for greek, we give them the
+ // default treatment.
+ return getDefaultStyle(ch);
+}
+
+// mathsf
+func getSansSerif(_ ch:Character) -> UTF32Char {
+ if ch.isUpperEnglish {
+ return UnicodeSymbol.mathCapitalSansSerifStart + (ch.utf32Char - Character("A").utf32Char)
+ } else if ch.isLowerEnglish {
+ return UnicodeSymbol.mathLowerSansSerifStart + (ch.utf32Char - Character("a").utf32Char)
+ } else if ch.isNumber {
+ return UnicodeSymbol.numberSansSerifStart + (ch.utf32Char - Character("0").utf32Char)
+ }
+ // Sans-serif characters don't exist for greek, we give them the
+ // default treatment.
+ return getDefaultStyle(ch);
+}
+
+// mathfrak
+func getFraktur(_ ch:Character) -> UTF32Char {
+ // Fraktur has exceptions:
+ switch ch {
+ case "C":
+ return 0x212D; // C Fraktur
+ case "H":
+ return 0x210C; // Hilbert space
+ case "I":
+ return 0x2111; // Imaginary
+ case "R":
+ return 0x211C; // Real
+ case "Z":
+ return 0x2128; // Z Fraktur
+ default:
+ break;
+ }
+ if ch.isUpperEnglish {
+ return UnicodeSymbol.mathCapitalFrakturStart + (ch.utf32Char - Character("A").utf32Char)
+ } else if ch.isLowerEnglish {
+ return UnicodeSymbol.mathLowerFrakturStart + (ch.utf32Char - Character("a").utf32Char)
+ }
+ // Fraktur characters don't exist for greek & numbers, we give them the
+ // default treatment.
+ return getDefaultStyle(ch);
+}
+
+// mathbb (double struck)
+func getBlackboard(_ ch:Character) -> UTF32Char {
+ // Blackboard has lots of exceptions:
+ switch(ch) {
+ case "C":
+ return 0x2102; // Complex numbers
+ case "H":
+ return 0x210D; // Quarternions
+ case "N":
+ return 0x2115; // Natural numbers
+ case "P":
+ return 0x2119; // Primes
+ case "Q":
+ return 0x211A; // Rationals
+ case "R":
+ return 0x211D; // Reals
+ case "Z":
+ return 0x2124; // Integers
+ default:
+ break;
+ }
+ if ch.isUpperEnglish {
+ return UnicodeSymbol.mathCapitalBlackboardStart + (ch.utf32Char - Character("A").utf32Char)
+ } else if ch.isLowerEnglish {
+ return UnicodeSymbol.mathLowerBlackboardStart + (ch.utf32Char - Character("a").utf32Char)
+ } else if ch.isNumber {
+ return UnicodeSymbol.numberBlackboardStart + (ch.utf32Char - Character("0").utf32Char)
+ }
+ // Blackboard characters don't exist for greek, we give them the
+ // default treatment.
+ return getDefaultStyle(ch);
+}
+
+func styleCharacter(_ ch:Character, fontStyle:MTFontStyle) -> UTF32Char {
+ switch fontStyle {
+ case .defaultStyle:
+ return getDefaultStyle(ch);
+ case .roman:
+ return ch.utf32Char
+ case .bold:
+ return getBold(ch);
+ case .italic:
+ return getItalicized(ch);
+ case .boldItalic:
+ return getBoldItalic(ch);
+ case .caligraphic:
+ return getCaligraphic(ch);
+ case .typewriter:
+ return getTypewriter(ch);
+ case .sansSerif:
+ return getSansSerif(ch);
+ case .fraktur:
+ return getFraktur(ch);
+ case .blackboard:
+ return getBlackboard(ch);
+ }
+}
+
+func changeFont(_ str:String, fontStyle:MTFontStyle) -> String {
+ var retval = ""
+ let codes = Array(str)
+ for i in 0.. MTMathListDisplay? {
+ let finalizedList = mathList?.finalized
+ // default is not cramped, no width constraint
+ return self.createLineForMathList(finalizedList, font:font, style:style, cramped:false, maxWidth: 0)
+ }
+
+ static func createLineForMathList(_ mathList:MTMathList?, font:MTFont?, style:MTLineStyle, maxWidth:CGFloat) -> MTMathListDisplay? {
+ let finalizedList = mathList?.finalized
+ // default is not cramped
+ return self.createLineForMathList(finalizedList, font:font, style:style, cramped:false, maxWidth: maxWidth)
+ }
+
+ // Internal
+ static func createLineForMathList(_ mathList:MTMathList?, font:MTFont?, style:MTLineStyle, cramped:Bool) -> MTMathListDisplay? {
+ return self.createLineForMathList(mathList, font:font, style:style, cramped:cramped, spaced:false, maxWidth: 0)
+ }
+
+ // Internal
+ static func createLineForMathList(_ mathList:MTMathList?, font:MTFont?, style:MTLineStyle, cramped:Bool, maxWidth:CGFloat) -> MTMathListDisplay? {
+ return self.createLineForMathList(mathList, font:font, style:style, cramped:cramped, spaced:false, maxWidth: maxWidth)
+ }
+
+ // Internal
+ static func createLineForMathList(_ mathList:MTMathList?, font:MTFont?, style:MTLineStyle, cramped:Bool, spaced:Bool) -> MTMathListDisplay? {
+ return self.createLineForMathList(mathList, font:font, style:style, cramped:cramped, spaced:spaced, maxWidth: 0)
+ }
+
+ // Internal
+ static func createLineForMathList(_ mathList:MTMathList?, font:MTFont?, style:MTLineStyle, cramped:Bool, spaced:Bool, maxWidth:CGFloat) -> MTMathListDisplay? {
+ assert(font != nil)
+
+ // Always use tokenization approach
+ // The tokenization path handles both constrained (maxWidth > 0) and unconstrained (maxWidth = 0) rendering
+ return createLineForMathListWithTokenization(
+ mathList,
+ font: font,
+ style: style,
+ cramped: cramped,
+ spaced: spaced,
+ maxWidth: maxWidth
+ )
+ }
+
+ static var placeholderColor: MTColor { MTColor.blue }
+
+ init(withFont font:MTFont?, style:MTLineStyle, cramped:Bool, spaced:Bool, maxWidth:CGFloat = 0) {
+ self.font = font
+ self.displayAtoms = [MTDisplay]()
+ self.currentPosition = CGPoint.zero
+ self.cramped = cramped
+ self.spaced = spaced
+ self.maxWidth = maxWidth
+ self.style = style
+ }
+
+ static func preprocessMathList(_ ml:MTMathList?) -> [MTMathAtom] {
+ // Note: Some of the preprocessing described by the TeX algorithm is done in the finalize method of MTMathList.
+ // Specifically rules 5 & 6 in Appendix G are handled by finalize.
+ // This function does not do a complete preprocessing as specified by TeX either. It removes any special atom types
+ // that are not included in TeX and applies Rule 14 to merge ordinary characters.
+ var preprocessed = [MTMathAtom]() // arrayWithCapacity:ml.atoms.count)
+ var prevNode:MTMathAtom! = nil
+ preprocessed.reserveCapacity(ml!.atoms.count)
+ for atom in ml!.atoms {
+ if atom.type == .variable || atom.type == .number {
+ // This is not a TeX type node. TeX does this during parsing the input.
+ // switch to using the italic math font
+ // We convert it to ordinary
+ let newFont = changeFont(atom.nucleus, fontStyle: atom.fontStyle) // mathItalicize(atom.nucleus)
+ atom.type = .ordinary
+ atom.nucleus = newFont
+ } else if atom.type == .unaryOperator {
+ // Neither of these are TeX nodes. TeX treats these as Ordinary. So will we.
+ atom.type = .ordinary
+ } else if atom.type == .binaryOperator {
+ // CRITICAL FIX: Convert binary operators to ordinary (unary) in appropriate contexts
+ // According to TeX rules (TeXbook Appendix G, Rule 5), a binary operator is converted
+ // to ordinary when it appears after: Bin, Op, Rel, Open, Punct, or at the beginning
+ // This handles cases like "=-2" where minus should be unary, not binary
+ let shouldConvertToOrdinary: Bool
+ if prevNode == nil {
+ // At the beginning of the list
+ shouldConvertToOrdinary = true
+ } else {
+ switch prevNode.type {
+ case .binaryOperator, .relation, .open, .punctuation, .largeOperator:
+ shouldConvertToOrdinary = true
+ default:
+ shouldConvertToOrdinary = false
+ }
+ }
+
+ if shouldConvertToOrdinary {
+ atom.type = .ordinary
+ }
+ }
+
+ if atom.type == .ordinary {
+ // This is Rule 14 to merge ordinary characters.
+ // combine ordinary atoms together
+ // CRITICAL FIX: Only fuse atoms with the same fontStyle
+ // This prevents fusing roman text (\text{...}) with italic math variables (A, B, etc.)
+ // which would cause incorrect line breaking when the combined string is tokenized
+ if prevNode != nil && prevNode.type == .ordinary && prevNode.subScript == nil && prevNode.superScript == nil && prevNode.fontStyle == atom.fontStyle {
+ prevNode.fuse(with: atom)
+ // skip the current node, we are done here.
+ continue
+ }
+ }
+
+ // TODO: add italic correction here or in second pass?
+ prevNode = atom
+ preprocessed.append(atom)
+ }
+ return preprocessed
+ }
+
+ // returns the size of the font in this style
+ static func getStyleSize(_ style:MTLineStyle, font:MTFont?) -> CGFloat {
+ let original = font!.fontSize
+ let scaled: CGFloat
+ switch style {
+ case .display, .text:
+ scaled = original
+ case .script:
+ scaled = original * font!.mathTable!.scriptScaleDown
+ case .scriptOfScript:
+ scaled = original * font!.mathTable!.scriptScriptScaleDown
+ }
+ // Apply minimum font size threshold to prevent deeply nested exponents
+ // from becoming unreadable (common for expressions like 2^{2^{2^2}})
+ // Minimum of 6pt ensures readability while maintaining proper hierarchy
+ return max(scaled, 6.0)
+ }
+
+ // MARK: - Spacing
+
+ // Returned in units of mu = 1/18 em.
+ func getSpacingInMu(_ type: InterElementSpaceType) -> Int {
+ // let valid = [MTLineStyle.display, .text]
+ switch type {
+ case .invalid: return -1
+ case .none: return 0
+ case .thin: return 3
+ case .nsThin: return style.isNotScript ? 3 : 0;
+ case .nsMedium: return style.isNotScript ? 4 : 0;
+ case .nsThick: return style.isNotScript ? 5 : 0;
+ }
+ }
+
+ func getInterElementSpace(_ left: MTMathAtomType, right:MTMathAtomType) -> CGFloat {
+ let leftIndex = getInterElementSpaceArrayIndexForType(left, row: true)
+ let rightIndex = getInterElementSpaceArrayIndexForType(right, row: false)
+ let spaceArray = getInterElementSpaces()[Int(leftIndex)]
+ let spaceTypeObj = spaceArray[Int(rightIndex)]
+ let spaceType = spaceTypeObj
+ assert(spaceType != .invalid, "Invalid space between \(left) and \(right)")
+
+ let spaceMultipler = self.getSpacingInMu(spaceType)
+ if spaceMultipler > 0 {
+ // 1 em = size of font in pt. space multipler is in multiples mu or 1/18 em
+ return CGFloat(spaceMultipler) * styleFont.mathTable!.muUnit
+ }
+ return 0
+ }
+
+
+ // MARK: - Subscript/Superscript
+
+ func scriptStyle() -> MTLineStyle {
+ switch style {
+ case .display, .text: return .script
+ case .script, .scriptOfScript: return .scriptOfScript
+ }
+ }
+
+ // subscript is always cramped
+ func subscriptCramped() -> Bool { true }
+
+ // superscript is cramped only if the current style is cramped
+ func superScriptCramped() -> Bool { cramped }
+
+ func superScriptShiftUp() -> CGFloat {
+ if cramped {
+ return styleFont.mathTable!.superscriptShiftUpCramped;
+ } else {
+ return styleFont.mathTable!.superscriptShiftUp;
+ }
+ }
+
+ // make scripts for the last atom
+ // index is the index of the element which is getting the sub/super scripts.
+ func makeScripts(_ atom: MTMathAtom?, display:MTDisplay?, index:UInt, delta:CGFloat) {
+ guard let atom = atom else { return }
+ guard atom.subScript != nil || atom.superScript != nil else { return }
+ guard let mathTable = styleFont.mathTable else { return }
+
+ var superScriptShiftUp = 0.0
+ var subscriptShiftDown = 0.0
+
+ display?.hasScript = true
+ if !(display is MTCTLineDisplay), let display = display {
+ // get the font in script style
+ let scriptFontSize = Self.getStyleSize(self.scriptStyle(), font:font)
+ let scriptFont = font.copy(withSize: scriptFontSize)
+ let scriptFontMetrics = scriptFont.mathTable
+
+ // if it is not a simple line then
+ if let scriptFontMetrics = scriptFontMetrics {
+ superScriptShiftUp = display.ascent - scriptFontMetrics.superscriptBaselineDropMax
+ subscriptShiftDown = display.descent + scriptFontMetrics.subscriptBaselineDropMin
+ }
+ }
+
+ if atom.superScript == nil {
+ guard let _subscript = MTTypesetter.createLineForMathList(atom.subScript, font:font, style:self.scriptStyle(), cramped:self.subscriptCramped()) else { return }
+ _subscript.type = .ssubscript
+ _subscript.index = Int(index)
+
+ subscriptShiftDown = fmax(subscriptShiftDown, mathTable.subscriptShiftDown);
+ subscriptShiftDown = fmax(subscriptShiftDown, _subscript.ascent - mathTable.subscriptTopMax);
+ // add the subscript
+ _subscript.position = CGPointMake(currentPosition.x, currentPosition.y - subscriptShiftDown);
+ displayAtoms.append(_subscript)
+ // update the position
+ currentPosition.x += _subscript.width + mathTable.spaceAfterScript;
+ return;
+ }
+
+ guard let superScript = MTTypesetter.createLineForMathList(atom.superScript, font:font, style:self.scriptStyle(), cramped:self.superScriptCramped()) else { return }
+ superScript.type = .superscript
+ superScript.index = Int(index);
+ superScriptShiftUp = fmax(superScriptShiftUp, self.superScriptShiftUp());
+ superScriptShiftUp = fmax(superScriptShiftUp, superScript.descent + mathTable.superscriptBottomMin);
+
+ if atom.subScript == nil {
+ superScript.position = CGPointMake(currentPosition.x, currentPosition.y + superScriptShiftUp);
+ displayAtoms.append(superScript)
+ // update the position
+ currentPosition.x += superScript.width + mathTable.spaceAfterScript;
+ return;
+ }
+ guard let ssubscript = MTTypesetter.createLineForMathList(atom.subScript, font:font, style:self.scriptStyle(), cramped:self.subscriptCramped()) else { return }
+ ssubscript.type = .ssubscript
+ ssubscript.index = Int(index)
+ subscriptShiftDown = fmax(subscriptShiftDown, mathTable.subscriptShiftDown);
+
+ // joint positioning of subscript & superscript
+ let subSuperScriptGap = (superScriptShiftUp - superScript.descent) + (subscriptShiftDown - ssubscript.ascent);
+ if (subSuperScriptGap < mathTable.subSuperscriptGapMin) {
+ // Set the gap to atleast as much
+ subscriptShiftDown += mathTable.subSuperscriptGapMin - subSuperScriptGap;
+ let superscriptBottomDelta = mathTable.superscriptBottomMaxWithSubscript - (superScriptShiftUp - superScript.descent);
+ if (superscriptBottomDelta > 0) {
+ // superscript is lower than the max allowed by the font with a subscript.
+ superScriptShiftUp += superscriptBottomDelta;
+ subscriptShiftDown -= superscriptBottomDelta;
+ }
+ }
+ // The delta is the italic correction above that shift superscript position
+ superScript.position = CGPointMake(currentPosition.x + delta, currentPosition.y + superScriptShiftUp);
+ displayAtoms.append(superScript)
+ ssubscript.position = CGPointMake(currentPosition.x, currentPosition.y - subscriptShiftDown);
+ displayAtoms.append(ssubscript)
+ currentPosition.x += max(superScript.width + delta, ssubscript.width) + mathTable.spaceAfterScript;
+ }
+
+ // MARK: - Helper Functions
+
+ /// Safely converts an NSRange location to UInt, returning 0 if the location is invalid (NSNotFound)
+ /// This prevents "Negative value is not representable" crashes when converting NSNotFound to UInt
+ private func safeUIntFromLocation(_ location: Int) -> UInt {
+ if location == NSNotFound || location < 0 {
+ return 0
+ }
+ return UInt(location)
+ }
+
+ // MARK: - Fractions
+
+ func numeratorShiftUp(_ hasRule:Bool) -> CGFloat {
+ if hasRule {
+ if style == .display {
+ return styleFont.mathTable!.fractionNumeratorDisplayStyleShiftUp
+ } else {
+ return styleFont.mathTable!.fractionNumeratorShiftUp
+ }
+ } else {
+ if style == .display {
+ return styleFont.mathTable!.stackTopDisplayStyleShiftUp
+ } else {
+ return styleFont.mathTable!.stackTopShiftUp
+ }
+ }
+ }
+
+ func numeratorGapMin() -> CGFloat {
+ if style == .display {
+ return styleFont.mathTable!.fractionNumeratorDisplayStyleGapMin;
+ } else {
+ return styleFont.mathTable!.fractionNumeratorGapMin;
+ }
+ }
+
+ func denominatorShiftDown(_ hasRule:Bool) -> CGFloat {
+ if hasRule {
+ if style == .display {
+ return styleFont.mathTable!.fractionDenominatorDisplayStyleShiftDown;
+ } else {
+ return styleFont.mathTable!.fractionDenominatorShiftDown;
+ }
+ } else {
+ if style == .display {
+ return styleFont.mathTable!.stackBottomDisplayStyleShiftDown;
+ } else {
+ return styleFont.mathTable!.stackBottomShiftDown;
+ }
+ }
+ }
+
+ func denominatorGapMin() -> CGFloat {
+ if style == .display {
+ return styleFont.mathTable!.fractionDenominatorDisplayStyleGapMin;
+ } else {
+ return styleFont.mathTable!.fractionDenominatorGapMin;
+ }
+ }
+
+ func stackGapMin() -> CGFloat {
+ if style == .display {
+ return styleFont.mathTable!.stackDisplayStyleGapMin;
+ } else {
+ return styleFont.mathTable!.stackGapMin;
+ }
+ }
+
+ func fractionDelimiterHeight()-> CGFloat {
+ if style == .display {
+ return styleFont.mathTable!.fractionDelimiterDisplayStyleSize;
+ } else {
+ return styleFont.mathTable!.fractionDelimiterSize;
+ }
+ }
+
+ func fractionStyle() -> MTLineStyle {
+ // Keep fractions at the same style level instead of incrementing.
+ // This ensures that fraction numerators/denominators have the same
+ // font size as regular text, preventing them from appearing too small
+ // in inline mode or when nested.
+ return style
+ }
+
+ func makeFraction(_ frac:MTFraction?) -> MTDisplay? {
+ guard let frac = frac else { return nil }
+
+ // lay out the parts of the fraction
+ let numeratorStyle: MTLineStyle
+ let denominatorStyle: MTLineStyle
+
+ if frac.isContinuedFraction {
+ // Continued fractions always use display style
+ numeratorStyle = .display
+ denominatorStyle = .display
+ } else {
+ // Regular fractions use adaptive style
+ let fractionStyle = self.fractionStyle;
+ numeratorStyle = fractionStyle()
+ denominatorStyle = fractionStyle()
+ }
+
+ let numeratorDisplay = MTTypesetter.createLineForMathList(frac.numerator, font:font, style:numeratorStyle, cramped:false)
+ let denominatorDisplay = MTTypesetter.createLineForMathList(frac.denominator, font:font, style:denominatorStyle, cramped:true)
+
+ // Handle empty numerator or denominator by creating empty displays
+ let numDisplay = numeratorDisplay ?? MTMathListDisplay(withDisplays: [], range: NSMakeRange(0, 0))
+ let denomDisplay = denominatorDisplay ?? MTMathListDisplay(withDisplays: [], range: NSMakeRange(0, 0))
+
+ // determine the location of the numerator
+ var numeratorShiftUp = self.numeratorShiftUp(frac.hasRule)
+ var denominatorShiftDown = self.denominatorShiftDown(frac.hasRule)
+ let barLocation = styleFont.mathTable!.axisHeight
+ let barThickness = frac.hasRule ? styleFont.mathTable!.fractionRuleThickness : 0
+
+ if frac.hasRule {
+ // This is the difference between the lowest edge of the numerator and the top edge of the fraction bar
+ let distanceFromNumeratorToBar = (numeratorShiftUp - numDisplay.descent) - (barLocation + barThickness/2);
+ // The distance should at least be displayGap
+ let minNumeratorGap = self.numeratorGapMin;
+ if distanceFromNumeratorToBar < minNumeratorGap() {
+ // This makes the distance between the bottom of the numerator and the top edge of the fraction bar
+ // at least minNumeratorGap.
+ numeratorShiftUp += (minNumeratorGap() - distanceFromNumeratorToBar);
+ }
+
+ // Do the same for the denominator
+ // This is the difference between the top edge of the denominator and the bottom edge of the fraction bar
+ let distanceFromDenominatorToBar = (barLocation - barThickness/2) - (denomDisplay.ascent - denominatorShiftDown);
+ // The distance should at least be denominator gap
+ let minDenominatorGap = self.denominatorGapMin;
+ if distanceFromDenominatorToBar < minDenominatorGap() {
+ // This makes the distance between the top of the denominator and the bottom of the fraction bar to be exactly
+ // minDenominatorGap
+ denominatorShiftDown += (minDenominatorGap() - distanceFromDenominatorToBar);
+ }
+ } else {
+ // This is the distance between the numerator and the denominator
+ let clearance = (numeratorShiftUp - numDisplay.descent) - (denomDisplay.ascent - denominatorShiftDown);
+ // This is the minimum clearance between the numerator and denominator.
+ // For ruleless fractions (like binom, choose, atop), use 1.5x the standard gap
+ // for better visual separation, following TeX's approach for binomial coefficients
+ let minGap = self.stackGapMin() * 1.5
+ if clearance < minGap {
+ numeratorShiftUp += (minGap - clearance)/2;
+ denominatorShiftDown += (minGap - clearance)/2;
+ }
+ }
+
+ let display = MTFractionDisplay(withNumerator: numDisplay, denominator: denomDisplay, position: currentPosition, range: frac.indexRange)
+
+ display.numeratorUp = numeratorShiftUp;
+ display.denominatorDown = denominatorShiftDown;
+ display.lineThickness = barThickness;
+ display.linePosition = barLocation;
+ if frac.leftDelimiter.isEmpty && frac.rightDelimiter.isEmpty {
+ return display
+ } else {
+ return self.addDelimitersToFractionDisplay(display, forFraction: frac)
+ }
+ }
+
+ func addDelimitersToFractionDisplay(_ display: MTFractionDisplay, forFraction frac: MTFraction) -> MTDisplay {
+ assert(!frac.leftDelimiter.isEmpty || !frac.rightDelimiter.isEmpty, "Fraction should have delimiters to call this function")
+
+ var innerElements = [MTDisplay]()
+ let glyphHeight = self.fractionDelimiterHeight
+ var position = CGPoint.zero
+ if !frac.leftDelimiter.isEmpty {
+ if let leftGlyph = self.findGlyphForBoundary(frac.leftDelimiter, withHeight: glyphHeight()) {
+ leftGlyph.position = position
+ position.x += leftGlyph.width
+ innerElements.append(leftGlyph)
+ }
+ }
+
+ display.position = position
+ position.x += display.width
+ innerElements.append(display)
+
+ if !frac.rightDelimiter.isEmpty {
+ if let rightGlyph = self.findGlyphForBoundary(frac.rightDelimiter, withHeight: glyphHeight()) {
+ rightGlyph.position = position
+ position.x += rightGlyph.width
+ innerElements.append(rightGlyph)
+ }
+ }
+ let innerDisplay = MTMathListDisplay(withDisplays: innerElements, range: frac.indexRange)
+ innerDisplay.position = currentPosition
+ return innerDisplay
+ }
+
+ // MARK: - Radicals
+
+ func radicalVerticalGap() -> CGFloat {
+ if style == .display {
+ return styleFont.mathTable!.radicalDisplayStyleVerticalGap
+ } else {
+ return styleFont.mathTable!.radicalVerticalGap
+ }
+ }
+
+ func getRadicalGlyphWithHeight(_ radicalHeight:CGFloat) -> MTDisplayDS? {
+ var glyphAscent=CGFloat(0), glyphDescent=CGFloat(0), glyphWidth=CGFloat(0)
+
+ let radicalGlyph = self.findGlyphForCharacterAtIndex("\u{221A}".startIndex, inString:"\u{221A}")
+ let glyph = self.findGlyph(radicalGlyph, withHeight:radicalHeight, glyphAscent:&glyphAscent, glyphDescent:&glyphDescent, glyphWidth:&glyphWidth)
+
+ var glyphDisplay:MTDisplayDS?
+ if glyphAscent + glyphDescent < radicalHeight {
+ // the glyphs is not as large as required. A glyph needs to be constructed using the extenders.
+ glyphDisplay = self.constructGlyph(radicalGlyph, withHeight:radicalHeight)
+ }
+
+ if glyphDisplay == nil {
+ // No constructed display so use the glyph we got.
+ glyphDisplay = MTGlyphDisplay(withGlpyh: glyph, range: NSMakeRange(NSNotFound, 0), font:styleFont)
+ glyphDisplay!.ascent = glyphAscent;
+ glyphDisplay!.descent = glyphDescent;
+ glyphDisplay!.width = glyphWidth;
+ }
+ return glyphDisplay;
+ }
+
+ func makeRadical(_ radicand:MTMathList?, range:NSRange) -> MTRadicalDisplay? {
+ let innerDisplay = MTTypesetter.createLineForMathList(radicand, font:font, style:style, cramped:true)!
+ var clearance = self.radicalVerticalGap()
+ let radicalRuleThickness = styleFont.mathTable!.radicalRuleThickness
+ let radicalHeight = innerDisplay.ascent + innerDisplay.descent + clearance + radicalRuleThickness
+
+ let glyph = self.getRadicalGlyphWithHeight(radicalHeight)!
+
+ // Note this is a departure from Latex. Latex assumes that glyphAscent == thickness.
+ // Open type math makes no such assumption, and ascent and descent are independent of the thickness.
+ // Latex computes delta as descent - (h(inner) + d(inner) + clearance)
+ // but since we may not have ascent == thickness, we modify the delta calculation slightly.
+ // If the font designer followes Latex conventions, it will be identical.
+ let delta = (glyph.descent + glyph.ascent) - (innerDisplay.ascent + innerDisplay.descent + clearance + radicalRuleThickness)
+ if delta > 0 {
+ clearance += delta/2 // increase the clearance to center the radicand inside the sign.
+ }
+
+ // we need to shift the radical glyph up, to coincide with the baseline of inner.
+ // The new ascent of the radical glyph should be thickness + adjusted clearance + h(inner)
+ let radicalAscent = radicalRuleThickness + clearance + innerDisplay.ascent
+ let shiftUp = radicalAscent - glyph.ascent // Note: if the font designer followed latex conventions, this is the same as glyphAscent == thickness.
+ glyph.shiftDown = -shiftUp
+
+ let radical = MTRadicalDisplay(withRadicand: innerDisplay, glyph: glyph, position: currentPosition, range: range)
+ radical.ascent = radicalAscent + styleFont.mathTable!.radicalExtraAscender
+ radical.topKern = styleFont.mathTable!.radicalExtraAscender
+ radical.lineThickness = radicalRuleThickness
+ // Note: Until we have radical construction from parts, it is possible that glyphAscent+glyphDescent is less
+ // than the requested height of the glyph (i.e. radicalHeight), so in the case the innerDisplay has a larger
+ // descent we use the innerDisplay's descent.
+ radical.descent = max(glyph.ascent + glyph.descent - radicalAscent, innerDisplay.descent)
+ radical.width = glyph.width + innerDisplay.width
+ return radical
+ }
+
+ // MARK: - Glyphs
+
+ func findGlyph(_ glyph:CGGlyph, withHeight height:CGFloat, glyphAscent:inout CGFloat, glyphDescent:inout CGFloat, glyphWidth:inout CGFloat) -> CGGlyph {
+ let variants = styleFont.mathTable!.getVerticalVariantsForGlyph(glyph)
+ let numVariants = variants.count;
+ var glyphs = [CGGlyph]()// numVariants)
+ glyphs.reserveCapacity(numVariants)
+ for i in 0 ..< numVariants {
+ let glyph = variants[i]!.uint16Value
+ glyphs.append(glyph)
+ }
+
+ var bboxes = [CGRect](repeating: CGRect.zero, count: numVariants)
+ var advances = [CGSize](repeating: CGSize.zero, count: numVariants)
+
+ // Get the bounds for these glyphs
+ CTFontGetBoundingRectsForGlyphs(styleFont.ctFont, .horizontal, glyphs, &bboxes, numVariants)
+ CTFontGetAdvancesForGlyphs(styleFont.ctFont, .horizontal, glyphs, &advances, numVariants);
+ var ascent=CGFloat(0), descent=CGFloat(0), width=CGFloat(0)
+ for i in 0..= height) {
+ glyphAscent = ascent;
+ glyphDescent = descent;
+ glyphWidth = width;
+ return glyphs[i]
+ }
+ }
+ glyphAscent = ascent;
+ glyphDescent = descent;
+ glyphWidth = width;
+ return glyphs[numVariants - 1]
+ }
+
+ func constructGlyph(_ glyph:CGGlyph, withHeight glyphHeight:CGFloat) -> MTGlyphConstructionDisplay? {
+ let parts = styleFont.mathTable!.getVerticalGlyphAssembly(forGlyph: glyph)
+ if parts.count == 0 {
+ return nil
+ }
+ var glyphs = [NSNumber](), offsets = [NSNumber]()
+ var height:CGFloat=0
+ self.constructGlyphWithParts(parts, glyphHeight:glyphHeight, glyphs:&glyphs, offsets:&offsets, height:&height)
+ var first = glyphs[0].uint16Value
+ let width = CTFontGetAdvancesForGlyphs(styleFont.ctFont, .horizontal, &first, nil, 1);
+ let display = MTGlyphConstructionDisplay(withGlyphs: glyphs, offsets: offsets, font: styleFont)
+ display.width = width;
+ display.ascent = height;
+ display.descent = 0; // it's upto the rendering to adjust the display up or down.
+ return display;
+ }
+
+ func constructGlyphWithParts(_ parts:[GlyphPart], glyphHeight:CGFloat, glyphs:inout [NSNumber], offsets:inout [NSNumber], height:inout CGFloat) {
+ // Loop forever until the glyph height is valid
+ for numExtenders in 0..= glyphHeight) {
+ // we are done
+ glyphs = glyphsRv;
+ offsets = offsetsRv;
+ height = minHeight;
+ return;
+ } else if (glyphHeight <= maxHeight) {
+ // spread the delta equally between all the connectors
+ let delta = glyphHeight - minHeight;
+ let deltaIncrease = Float(delta) / Float(glyphsRv.count - 1)
+ var lastOffset = CGFloat(0)
+ for i in 0.. CGGlyph {
+ // Get the character at index taking into account UTF-32 characters
+ var chars = Array(str[index].utf16)
+
+ // Get the glyph from the font
+ var glyph = [CGGlyph](repeating: CGGlyph.zero, count: chars.count)
+ let found = CTFontGetGlyphsForCharacters(styleFont.ctFont, &chars, &glyph, chars.count)
+ if !found {
+ // Try fallback font if available
+ if let fallbackFont = styleFont.fallbackFont {
+ let fallbackFound = CTFontGetGlyphsForCharacters(fallbackFont, &chars, &glyph, chars.count)
+ if fallbackFound {
+ return glyph[0]
+ }
+ }
+ // the font did not contain a glyph for our character, so we just return 0 (notdef)
+ return 0
+ }
+ return glyph[0]
+ }
+
+ // MARK: - Large Operators
+
+ func makeLargeOp(_ op:MTLargeOperator!) -> MTDisplay? {
+ // Show limits above/below in display mode
+ // For inline mode, we still center limits below for operators like \lim, but with tighter spacing
+ let limits = op.limits && (style == .display || style == .text)
+ var delta = CGFloat(0)
+ if op.nucleus.count == 1 {
+ var glyph = self.findGlyphForCharacterAtIndex(op.nucleus.startIndex, inString:op.nucleus)
+ if glyph != 0 {
+ // Enlarge large operators to make them visually distinctive
+ if style == .display {
+ // Display style: use large variant for mathematical display mode (~2.2em)
+ glyph = styleFont.mathTable!.getLargerGlyph(glyph, forDisplayStyle: true)
+ } else if style == .text {
+ // Text/inline style: use moderately larger variant to ensure operator is taller than surrounding text
+ glyph = styleFont.mathTable!.getLargerGlyph(glyph, forDisplayStyle: false)
+ }
+ // Script and scriptOfScript styles keep base size (compact rendering)
+ }
+ // This is be the italic correction of the character.
+ delta = styleFont.mathTable!.getItalicCorrection(glyph)
+
+ // vertically center
+ let bbox = CTFontGetBoundingRectsForGlyphs(styleFont.ctFont, .horizontal, &glyph, nil, 1);
+ let width = CTFontGetAdvancesForGlyphs(styleFont.ctFont, .horizontal, &glyph, nil, 1);
+ var ascent=CGFloat(0), descent=CGFloat(0)
+ getBboxDetails(bbox, ascent: &ascent, descent: &descent)
+ let shiftDown = 0.5*(ascent - descent) - styleFont.mathTable!.axisHeight;
+ let glyphDisplay = MTGlyphDisplay(withGlpyh: glyph, range: op.indexRange, font: styleFont)
+ glyphDisplay.ascent = ascent;
+ glyphDisplay.descent = descent;
+ glyphDisplay.width = width;
+ if (op.subScript != nil) && !limits {
+ // Remove italic correction from the width of the glyph if
+ // there is a subscript and limits is not set.
+ glyphDisplay.width -= delta;
+ }
+ glyphDisplay.shiftDown = shiftDown;
+ glyphDisplay.position = currentPosition;
+ return self.addLimitsToDisplay(glyphDisplay, forOperator:op, delta:delta)
+ } else {
+ // Create a regular node
+ let line = NSMutableAttributedString(string: op.nucleus)
+ // add the font
+ line.addAttribute(kCTFontAttributeName as NSAttributedString.Key, value:styleFont.ctFont!, range:NSMakeRange(0, line.length))
+ let displayAtom = MTCTLineDisplay(withString: line, position: currentPosition, range: op.indexRange, font: styleFont, atoms: [op])
+ return self.addLimitsToDisplay(displayAtom, forOperator:op, delta:0)
+ }
+ }
+
+ func addLimitsToDisplay(_ display:MTDisplay?, forOperator op:MTLargeOperator, delta:CGFloat) -> MTDisplay? {
+ // If there is no subscript or superscript, just return the current display
+ if op.subScript == nil && op.superScript == nil {
+ currentPosition.x += display!.width
+ return display;
+ }
+ // Show limits above/below in both display and text (inline) modes
+ if op.limits && (style == .display || style == .text) {
+ // make limits (above/below positioning)
+ var superScript:MTMathListDisplay? = nil, subScript:MTMathListDisplay? = nil
+
+ // Scale font for script style before creating scripts
+ // This matches how MTDisplayPreRenderer.renderScript() handles script sizing
+ let scriptStyle = self.scriptStyle()
+ let scriptFontSize = MTTypesetter.getStyleSize(scriptStyle, font: font)
+ let scriptFont = font.copy(withSize: scriptFontSize)
+
+ if op.superScript != nil {
+ superScript = MTTypesetter.createLineForMathList(op.superScript, font:scriptFont, style:scriptStyle, cramped:self.superScriptCramped())
+ }
+ if op.subScript != nil {
+ subScript = MTTypesetter.createLineForMathList(op.subScript, font:scriptFont, style:scriptStyle, cramped:self.subscriptCramped())
+ }
+ assert((superScript != nil) || (subScript != nil), "At least one of superscript or subscript should have been present.");
+ let opsDisplay = MTLargeOpLimitsDisplay(withNucleus:display, upperLimit:superScript, lowerLimit:subScript, limitShift:delta/2, extraPadding:0)
+
+ // Use standard OpenType MATH metrics for limit spacing
+ if superScript != nil {
+ let upperLimitGap = max(styleFont.mathTable!.upperLimitGapMin, styleFont.mathTable!.upperLimitBaselineRiseMin - superScript!.descent);
+ opsDisplay.upperLimitGap = upperLimitGap;
+ }
+ if subScript != nil {
+ let lowerLimitGap = max(styleFont.mathTable!.lowerLimitGapMin, styleFont.mathTable!.lowerLimitBaselineDropMin - subScript!.ascent);
+ opsDisplay.lowerLimitGap = lowerLimitGap;
+ }
+ opsDisplay.position = currentPosition;
+ opsDisplay.range = op.indexRange;
+ currentPosition.x += opsDisplay.width;
+ return opsDisplay;
+ } else {
+ currentPosition.x += display!.width;
+ self.makeScripts(op, display:display, index:safeUIntFromLocation(op.indexRange.location), delta:delta)
+ return display;
+ }
+ }
+
+ // MARK: - Large delimiters
+
+ // Delimiter shortfall from plain.tex
+ static let kDelimiterFactor = CGFloat(901)
+ static let kDelimiterShortfallPoints = CGFloat(5)
+
+ func makeLeftRight(_ inner: MTInner?, maxWidth: CGFloat = 0) -> MTDisplay? {
+ assert(inner!.leftBoundary != nil || inner!.rightBoundary != nil, "Inner should have a boundary to call this function");
+
+ let glyphHeight: CGFloat
+
+ // Check if we have an explicit delimiter height (from \big, \Big, etc.)
+ if let delimiterMultiplier = inner!.delimiterHeight {
+ // delimiterHeight is a multiplier (e.g., 1.2, 1.8, 2.4, 3.0)
+ // Multiply by font size to get actual height
+ glyphHeight = styleFont.fontSize * delimiterMultiplier
+ } else {
+ // Calculate height based on inner content (for \left...\right)
+ let innerListDisplay = MTTypesetter.createLineForMathList(inner!.innerList, font:font, style:style, cramped:cramped, spaced:true, maxWidth:maxWidth)
+ let axisHeight = styleFont.mathTable!.axisHeight
+ // delta is the max distance from the axis
+ let delta = max(innerListDisplay!.ascent - axisHeight, innerListDisplay!.descent + axisHeight);
+ let d1 = (delta / 500) * MTTypesetter.kDelimiterFactor; // This represents atleast 90% of the formula
+ let d2 = 2 * delta - MTTypesetter.kDelimiterShortfallPoints; // This represents a shortfall of 5pt
+ // The size of the delimiter glyph should cover at least 90% of the formula or
+ // be at most 5pt short.
+ glyphHeight = max(d1, d2);
+ }
+
+ var innerElements = [MTDisplay]()
+ var position = CGPoint.zero
+
+ // Add horizontal padding between delimiters and content
+ // Use 2 mu (about 1/9 em) for breathing room, matching TeX standards
+ let delimiterPadding = styleFont.mathTable!.muUnit * 2
+
+ if inner!.leftBoundary != nil && !inner!.leftBoundary!.nucleus.isEmpty {
+ let leftGlyph = self.findGlyphForBoundary(inner!.leftBoundary!.nucleus, withHeight:glyphHeight)
+ leftGlyph!.position = position
+ position.x += leftGlyph!.width
+ innerElements.append(leftGlyph!)
+ // Add padding after left delimiter
+ position.x += delimiterPadding
+ }
+
+ // Only include inner content if not using explicit delimiter height
+ // (explicit height commands like \big produce standalone delimiters)
+ if inner!.delimiterHeight == nil {
+ let innerListDisplay = MTTypesetter.createLineForMathList(inner!.innerList, font:font, style:style, cramped:cramped, spaced:true, maxWidth:maxWidth)
+ innerListDisplay!.position = position;
+ position.x += innerListDisplay!.width;
+ innerElements.append(innerListDisplay!)
+ }
+
+ if inner!.rightBoundary != nil && !inner!.rightBoundary!.nucleus.isEmpty {
+ // Add padding before right delimiter
+ position.x += delimiterPadding
+ let rightGlyph = self.findGlyphForBoundary(inner!.rightBoundary!.nucleus, withHeight:glyphHeight)
+ rightGlyph!.position = position;
+ position.x += rightGlyph!.width;
+ innerElements.append(rightGlyph!)
+ }
+ let innerDisplay = MTMathListDisplay(withDisplays: innerElements, range: inner!.indexRange)
+ return innerDisplay
+ }
+
+ func findGlyphForBoundary(_ delimiter:String, withHeight glyphHeight:CGFloat) -> MTDisplay? {
+ var glyphAscent=CGFloat(0), glyphDescent=CGFloat(0), glyphWidth=CGFloat(0)
+ let leftGlyph = self.findGlyphForCharacterAtIndex(delimiter.startIndex, inString:delimiter)
+ let glyph = self.findGlyph(leftGlyph, withHeight:glyphHeight, glyphAscent:&glyphAscent, glyphDescent:&glyphDescent, glyphWidth:&glyphWidth)
+
+ var glyphDisplay:MTDisplayDS?
+ if (glyphAscent + glyphDescent < glyphHeight) {
+ // we didn't find a pre-built glyph that is large enough
+ glyphDisplay = self.constructGlyph(leftGlyph, withHeight:glyphHeight)
+ }
+
+ if glyphDisplay == nil {
+ // Create a glyph display
+ glyphDisplay = MTGlyphDisplay(withGlpyh: glyph, range: NSMakeRange(NSNotFound, 0), font:styleFont)
+ glyphDisplay!.ascent = glyphAscent;
+ glyphDisplay!.descent = glyphDescent;
+ glyphDisplay!.width = glyphWidth;
+ }
+ // Center the glyph on the axis
+ let shiftDown = 0.5*(glyphDisplay!.ascent - glyphDisplay!.descent) - styleFont.mathTable!.axisHeight;
+ glyphDisplay!.shiftDown = shiftDown;
+ return glyphDisplay;
+ }
+
+ // MARK: - Underline/Overline
+
+ func makeUnderline(_ under:MTUnderLine?) -> MTDisplay? {
+ let innerListDisplay = MTTypesetter.createLineForMathList(under!.innerList, font:font, style:style, cramped:cramped)
+ let underDisplay = MTLineDisplay(withInner: innerListDisplay, position: currentPosition, range: under!.indexRange)
+ // Move the line down by the vertical gap.
+ underDisplay.lineShiftUp = -(innerListDisplay!.descent + styleFont.mathTable!.underbarVerticalGap);
+ underDisplay.lineThickness = styleFont.mathTable!.underbarRuleThickness;
+ underDisplay.ascent = innerListDisplay!.ascent
+ underDisplay.descent = innerListDisplay!.descent + styleFont.mathTable!.underbarVerticalGap + styleFont.mathTable!.underbarRuleThickness + styleFont.mathTable!.underbarExtraDescender;
+ underDisplay.width = innerListDisplay!.width;
+ return underDisplay;
+ }
+
+ func makeOverline(_ over:MTOverLine?) -> MTDisplay? {
+ let innerListDisplay = MTTypesetter.createLineForMathList(over!.innerList, font:font, style:style, cramped:true)
+ let overDisplay = MTLineDisplay(withInner:innerListDisplay, position:currentPosition, range:over!.indexRange)
+ overDisplay.lineShiftUp = innerListDisplay!.ascent + styleFont.mathTable!.overbarVerticalGap;
+ overDisplay.lineThickness = styleFont.mathTable!.underbarRuleThickness;
+ overDisplay.ascent = innerListDisplay!.ascent + styleFont.mathTable!.overbarVerticalGap + styleFont.mathTable!.overbarRuleThickness + styleFont.mathTable!.overbarExtraAscender;
+ overDisplay.descent = innerListDisplay!.descent;
+ overDisplay.width = innerListDisplay!.width;
+ return overDisplay;
+ }
+
+ // MARK: - Accents
+
+ func isSingleCharAccentee(_ accent:MTAccent?) -> Bool {
+ guard let accent = accent else { return false }
+ if accent.innerList!.atoms.count != 1 {
+ // Not a single char list.
+ return false
+ }
+ let innerAtom = accent.innerList!.atoms[0]
+ if innerAtom.nucleus.count != 1 {
+ // A complex atom, not a simple char.
+ return false
+ }
+ if innerAtom.subScript != nil || innerAtom.superScript != nil {
+ return false
+ }
+ return true
+ }
+
+ // The distance the accent must be moved from the beginning.
+ func getSkew(_ accent: MTAccent?, accenteeWidth width:CGFloat, accentGlyph:CGGlyph) -> CGFloat {
+ guard let accent = accent else { return 0 }
+ if accent.nucleus.isEmpty {
+ // No accent
+ return 0
+ }
+ let accentAdjustment = styleFont.mathTable!.getTopAccentAdjustment(accentGlyph)
+ var accenteeAdjustment = CGFloat(0)
+ if !self.isSingleCharAccentee(accent) {
+ // use the center of the accentee
+ accenteeAdjustment = width/2
+ } else {
+ let innerAtom = accent.innerList!.atoms[0]
+ let accenteeGlyph = self.findGlyphForCharacterAtIndex(innerAtom.nucleus.index(innerAtom.nucleus.endIndex, offsetBy:-1), inString:innerAtom.nucleus)
+ accenteeAdjustment = styleFont.mathTable!.getTopAccentAdjustment(accenteeGlyph)
+ }
+ // The adjustments need to aligned, so skew is just the difference.
+ return (accenteeAdjustment - accentAdjustment)
+ }
+
+ // Find the largest horizontal variant if exists, with width less than max width.
+ func findVariantGlyph(_ glyph:CGGlyph, withMaxWidth maxWidth:CGFloat, maxWidth glyphAscent:inout CGFloat, glyphDescent:inout CGFloat, glyphWidth:inout CGFloat, glyphMinY:inout CGFloat) -> CGGlyph {
+ let variants = styleFont.mathTable!.getHorizontalVariantsForGlyph(glyph)
+ let numVariants = variants.count
+ assert(numVariants > 0, "A glyph is always it's own variant, so number of variants should be > 0");
+ var glyphs = [CGGlyph]() // [numVariants)
+ glyphs.reserveCapacity(numVariants)
+ for i in 0 ..< numVariants {
+ let glyph = variants[i]!.uint16Value
+ glyphs.append(glyph)
+ }
+
+ var curGlyph = glyphs[0] // if no other glyph is found, we'll return the first one.
+ var bboxes = [CGRect](repeating: CGRect.zero, count: numVariants) // [numVariants)
+ var advances = [CGSize](repeating: CGSize.zero, count:numVariants)
+ // Get the bounds for these glyphs
+ CTFontGetBoundingRectsForGlyphs(styleFont.ctFont, .horizontal, &glyphs, &bboxes, numVariants);
+ CTFontGetAdvancesForGlyphs(styleFont.ctFont, .horizontal, &glyphs, &advances, numVariants);
+ for i in 0.. maxWidth) {
+ if (i == 0) {
+ // glyph dimensions are not yet set
+ glyphWidth = advances[i].width;
+ glyphAscent = ascent;
+ glyphDescent = descent;
+ glyphMinY = bounds.minY;
+ }
+ return curGlyph;
+ } else {
+ curGlyph = glyphs[i]
+ glyphWidth = advances[i].width;
+ glyphAscent = ascent;
+ glyphDescent = descent;
+ glyphMinY = bounds.minY;
+ }
+ }
+ // We exhausted all the variants and none was larger than the width, so we return the largest
+ return curGlyph;
+ }
+
+ /// Gets the proper glyph name for arrow accents that have stretchy variants in the font.
+ /// Returns different glyphs based on the LaTeX command used:
+ /// - \vec: use combining character glyph (uni20D7) for small fixed-size arrow
+ /// - \overrightarrow: use non-combining arrow (arrowright) which can be stretched
+ func getArrowAccentGlyphName(_ accent: MTAccent) -> String? {
+ // Check if this is a stretchy arrow accent (set by the factory based on LaTeX command)
+ let useStretchy = accent.isStretchy
+
+ // Map Unicode combining characters to appropriate glyph names
+ switch accent.nucleus {
+ case "\u{20D6}": // Combining left arrow above
+ return useStretchy ? "arrowleft" : "uni20D6"
+ case "\u{20D7}": // Combining right arrow above (\vec or \overrightarrow)
+ return useStretchy ? "arrowright" : "uni20D7"
+ case "\u{20E1}": // Combining left right arrow above
+ return useStretchy ? "arrowboth" : "uni20E1"
+ default:
+ return nil
+ }
+ }
+
+ /// Gets the proper glyph name for wide accents that should stretch to cover content.
+ /// Returns different glyphs based on the LaTeX command used:
+ /// - \hat: use combining character for fixed-size accent
+ /// - \widehat: use non-combining circumflex which can be stretched
+ func getWideAccentGlyphName(_ accent: MTAccent) -> String? {
+ // Only apply to wide accents (set by factory based on LaTeX command)
+ guard accent.isWide else { return nil }
+
+ // Map Unicode combining characters to non-combining glyph names with stretchy variants
+ switch accent.nucleus {
+ case "\u{0302}": // COMBINING CIRCUMFLEX ACCENT (\hat or \widehat)
+ return "circumflex"
+ case "\u{0303}": // COMBINING TILDE (\tilde or \widetilde)
+ return "tilde"
+ case "\u{030C}": // COMBINING CARON (\check or \widecheck)
+ return "caron"
+ default:
+ return nil
+ }
+ }
+
+ /// Counts the approximate character length of the content under a wide accent.
+ /// This is used to select the appropriate glyph variant.
+ func getWideAccentContentLength(_ accent: MTAccent) -> Int {
+ guard let innerList = accent.innerList else { return 0 }
+
+ var charCount = 0
+ for atom in innerList.atoms {
+ switch atom.type {
+ case .variable, .number:
+ // Count actual characters
+ charCount += atom.nucleus.count
+ case .ordinary, .binaryOperator, .relation:
+ // Count as single character
+ charCount += 1
+ case .fraction:
+ // Fractions count as 2 units
+ charCount += 2
+ case .radical:
+ // Radicals count as 2 units
+ charCount += 2
+ case .largeOperator:
+ // Large operators count as 2 units
+ charCount += 2
+ default:
+ // Other types count as 1 unit
+ charCount += 1
+ }
+ }
+ return charCount
+ }
+
+ /// Determines which glyph variant to use for a wide accent based on content length.
+ /// Returns a multiplier for the requested width (1.0, 1.5, 2.0, or 2.5)
+ /// Selects variants based on character count to ensure proper coverage.
+ func getWideAccentVariantMultiplier(_ accent: MTAccent) -> CGFloat {
+ let charCount = getWideAccentContentLength(accent)
+
+ // Map character count to variant width request multiplier
+ // This helps select larger glyph variants from the font's MATH table
+ // 1-2 chars: request 1.0x (smallest variant)
+ // 3-4 chars: request 1.5x (medium variant)
+ // 5-6 chars: request 2.0x (large variant)
+ // 7+ chars: request 2.5x (largest variant)
+ if charCount <= 2 {
+ return 1.0
+ } else if charCount <= 4 {
+ return 1.5
+ } else if charCount <= 6 {
+ return 2.0
+ } else {
+ return 2.5
+ }
+ }
+
+ func makeAccent(_ accent:MTAccent?) -> MTDisplay? {
+ guard let accent = accent else { return nil }
+
+ let accentee = MTTypesetter.createLineForMathList(accent.innerList, font:font, style:style, cramped:true)
+ if accent.nucleus.isEmpty {
+ // no accent!
+ return accentee
+ }
+
+ // If accentee is nil (empty content), create an empty display
+ guard let accentee = accentee else {
+ // Return an empty display for empty content
+ let emptyDisplay = MTMathListDisplay(withDisplays: [], range: accent.indexRange)
+ emptyDisplay.position = currentPosition
+ return emptyDisplay
+ }
+
+ var accentGlyph: CGGlyph
+ let isArrowAccent = getArrowAccentGlyphName(accent) != nil
+ let isWideAccent = getWideAccentGlyphName(accent) != nil
+
+ // Check for special accent types that need non-combining glyphs
+ if let wideGlyphName = getWideAccentGlyphName(accent) {
+ // For wide accents, use non-combining glyphs (e.g., "circumflex", "tilde")
+ // These have horizontal variants that can stretch
+ accentGlyph = styleFont.get(glyphWithName: wideGlyphName)
+ } else if let arrowGlyphName = getArrowAccentGlyphName(accent) {
+ // For arrow accents, use non-combining arrow glyphs (e.g., "arrowright")
+ // These have larger horizontal variants than the combining versions
+ accentGlyph = styleFont.get(glyphWithName: arrowGlyphName)
+ } else {
+ // For regular accents, use Unicode character lookup
+ let end = accent.nucleus.index(before: accent.nucleus.endIndex)
+ accentGlyph = self.findGlyphForCharacterAtIndex(end, inString:accent.nucleus)
+ }
+
+ let accenteeWidth = accentee.width;
+ var glyphAscent=CGFloat(0), glyphDescent=CGFloat(0), glyphWidth=CGFloat(0), glyphMinY=CGFloat(0)
+
+ // Adjust requested width based on accent type:
+ // - Wide accents (\widehat): request width based on content length (variant selection)
+ // - Arrow accents (\overrightarrow): request extra width for stretching
+ // - Regular accents: request exact content width
+ let requestedWidth: CGFloat
+ if isWideAccent {
+ // For wide accents, request width based on content length to select appropriate variant
+ let multiplier = getWideAccentVariantMultiplier(accent)
+ requestedWidth = accenteeWidth * multiplier
+ } else if isArrowAccent {
+ if accent.isStretchy {
+ requestedWidth = accenteeWidth * 1.1 // Request extra width for stretching
+ } else {
+ requestedWidth = 1.0 // Get smallest non-zero variant (typically .h1)
+ }
+ } else {
+ requestedWidth = accenteeWidth
+ }
+
+ accentGlyph = self.findVariantGlyph(accentGlyph, withMaxWidth:requestedWidth, maxWidth:&glyphAscent, glyphDescent:&glyphDescent, glyphWidth:&glyphWidth, glyphMinY:&glyphMinY)
+
+ // For non-stretchy arrow accents (\vec): if we got a zero-width glyph (base combining char),
+ // manually select the first variant which is the proper accent size
+ if isArrowAccent && !accent.isStretchy && glyphWidth == 0 {
+ guard let mathTable = styleFont.mathTable else { return nil }
+ let variants = mathTable.getHorizontalVariantsForGlyph(accentGlyph)
+ if variants.count > 1, let variantNum = variants[1] {
+ // Use the first variant (.h1) which has proper width
+ accentGlyph = CGGlyph(variantNum.uint16Value)
+ var glyph = accentGlyph
+ var advances = CGSize.zero
+ CTFontGetAdvancesForGlyphs(styleFont.ctFont, .horizontal, &glyph, &advances, 1)
+ glyphWidth = advances.width
+ // Recalculate ascent and descent for the variant glyph
+ var boundingRects = CGRect.zero
+ CTFontGetBoundingRectsForGlyphs(styleFont.ctFont, .horizontal, &glyph, &boundingRects, 1)
+ glyphMinY = boundingRects.minY
+ glyphAscent = boundingRects.maxY
+ glyphDescent = -boundingRects.minY
+ }
+ }
+
+ // Special accents (arrows and wide accents) need more vertical space and different positioning
+ let delta: CGFloat
+ let height: CGFloat
+ let skew: CGFloat
+
+ if isWideAccent {
+ // Wide accents (\widehat, \widetilde): use same vertical spacing as stretchy arrows
+ delta = 0 // No compression for wide accents
+ guard let mathTable = styleFont.mathTable else { return nil }
+ let wideAccentSpacing = mathTable.upperLimitGapMin // Same as stretchy arrows
+ // Compensate for internal glyph whitespace (minY > 0)
+ let minYCompensation = max(0, glyphMinY)
+ height = accentee.ascent + wideAccentSpacing - minYCompensation
+
+ // For wide accents: if the largest glyph variant is still smaller than content width,
+ // scale it horizontally to fully cover the content
+ if glyphWidth < accenteeWidth {
+ // Add padding to make accent extend slightly beyond content
+ // Use ~0.1em padding (less than arrows which use ~0.167em)
+ let widePadding = styleFont.fontSize / 10 // Approximately 0.1em
+ let targetWidth = accenteeWidth + widePadding
+
+ let scaleX = targetWidth / glyphWidth
+ let accentGlyphDisplay = MTGlyphDisplay(withGlpyh: accentGlyph, range: accent.indexRange, font: styleFont)
+ accentGlyphDisplay.scaleX = scaleX // Apply horizontal scaling
+ accentGlyphDisplay.ascent = glyphAscent
+ accentGlyphDisplay.descent = glyphDescent
+ accentGlyphDisplay.width = targetWidth // Set width to include padding
+ accentGlyphDisplay.position = CGPointMake(0, height) // Align to left edge
+
+ var finalAccentee = accentee
+ if self.isSingleCharAccentee(accent) && (accent.subScript != nil || accent.superScript != nil) {
+ // Attach the super/subscripts to the accentee instead of the accent.
+ guard let innerList = accent.innerList,
+ !innerList.atoms.isEmpty else { return nil }
+ let innerAtom = innerList.atoms[0]
+ innerAtom.superScript = accent.superScript
+ innerAtom.subScript = accent.subScript
+ accent.superScript = nil
+ accent.subScript = nil
+ if let remadeAccentee = MTTypesetter.createLineForMathList(accent.innerList, font:font, style:style, cramped:cramped) {
+ finalAccentee = remadeAccentee
+ }
+ }
+
+ let display = MTAccentDisplay(withAccent:accentGlyphDisplay, accentee:finalAccentee, range:accent.indexRange)
+ display.width = finalAccentee.width
+ display.descent = finalAccentee.descent
+ let ascent = height + glyphAscent
+ display.ascent = max(finalAccentee.ascent, ascent)
+ display.position = currentPosition
+ return display
+ } else {
+ // Wide accent glyph is wide enough: center it over the content
+ skew = (accenteeWidth - glyphWidth) / 2
+ }
+ } else if isArrowAccent {
+ // Arrow accents spacing depends on whether they're stretchy or not
+ guard let mathTable = styleFont.mathTable else { return nil }
+ if accent.isStretchy {
+ // Stretchy arrows (\overrightarrow): use full ascent + additional spacing
+ delta = 0 // No compression for stretchy arrows
+ let arrowSpacing = mathTable.upperLimitGapMin // Use standard gap
+ // Compensate for internal glyph whitespace (minY > 0)
+ let minYCompensation = max(0, glyphMinY)
+ height = accentee.ascent + arrowSpacing - minYCompensation
+ } else {
+ // Non-stretchy arrows (\vec): use tight spacing like regular accents
+ // This gives a more compact appearance suitable for single-character vectors
+ delta = min(accentee.ascent, mathTable.accentBaseHeight)
+ // Use same formula as regular accents (no minYCompensation adjustment)
+ // This places the arrow properly above the character
+ height = accentee.ascent - delta
+ }
+
+ // For stretchy arrow accents (\overrightarrow): if the largest glyph variant is still smaller than content width,
+ // scale it horizontally to fully cover the content
+ // Add small padding to make arrow tip extend slightly beyond content
+ // For non-stretchy accents (\vec): always center without scaling
+ if accent.isStretchy && glyphWidth < accenteeWidth {
+ // Add padding to make arrow extend beyond content on the tip side
+ // Use approximately 0.15-0.2em extra width
+ let arrowPadding = styleFont.fontSize / 6 // Approximately 0.167em at typical font sizes
+ let targetWidth = accenteeWidth + arrowPadding
+
+ let scaleX = targetWidth / glyphWidth
+ let accentGlyphDisplay = MTGlyphDisplay(withGlpyh: accentGlyph, range: accent.indexRange, font: styleFont)
+ accentGlyphDisplay.scaleX = scaleX // Apply horizontal scaling
+ accentGlyphDisplay.ascent = glyphAscent
+ accentGlyphDisplay.descent = glyphDescent
+ accentGlyphDisplay.width = targetWidth // Set width to include padding
+ accentGlyphDisplay.position = CGPointMake(0, height) // Align to left edge
+
+ var finalAccentee = accentee
+ if self.isSingleCharAccentee(accent) && (accent.subScript != nil || accent.superScript != nil) {
+ // Attach the super/subscripts to the accentee instead of the accent.
+ guard let innerList = accent.innerList,
+ !innerList.atoms.isEmpty else { return nil }
+ let innerAtom = innerList.atoms[0]
+ innerAtom.superScript = accent.superScript
+ innerAtom.subScript = accent.subScript
+ accent.superScript = nil
+ accent.subScript = nil
+ if let remadeAccentee = MTTypesetter.createLineForMathList(accent.innerList, font:font, style:style, cramped:cramped) {
+ finalAccentee = remadeAccentee
+ }
+ }
+
+ let display = MTAccentDisplay(withAccent:accentGlyphDisplay, accentee:finalAccentee, range:accent.indexRange)
+ display.width = finalAccentee.width
+ display.descent = finalAccentee.descent
+ let ascent = height + glyphAscent
+ display.ascent = max(finalAccentee.ascent, ascent)
+ display.position = currentPosition
+ return display
+ } else {
+ // Arrow glyph is wide enough or is non-stretchy (\vec): center it over the content
+ skew = (accenteeWidth - glyphWidth) / 2
+ }
+ } else {
+ // For regular accents: use traditional tight positioning
+ guard let mathTable = styleFont.mathTable else { return nil }
+ delta = min(accentee.ascent, mathTable.accentBaseHeight)
+ skew = self.getSkew(accent, accenteeWidth:accenteeWidth, accentGlyph:accentGlyph)
+ height = accentee.ascent - delta // This is always positive since delta <= height.
+ }
+
+ let accentPosition = CGPointMake(skew, height);
+ let accentGlyphDisplay = MTGlyphDisplay(withGlpyh: accentGlyph, range: accent.indexRange, font: styleFont)
+ accentGlyphDisplay.ascent = glyphAscent;
+ accentGlyphDisplay.descent = glyphDescent;
+ accentGlyphDisplay.width = glyphWidth;
+ accentGlyphDisplay.position = accentPosition;
+
+ var finalAccentee = accentee
+ if self.isSingleCharAccentee(accent) && (accent.subScript != nil || accent.superScript != nil) {
+ // Attach the super/subscripts to the accentee instead of the accent.
+ guard let innerList = accent.innerList,
+ !innerList.atoms.isEmpty else { return nil }
+ let innerAtom = innerList.atoms[0]
+ innerAtom.superScript = accent.superScript;
+ innerAtom.subScript = accent.subScript;
+ accent.superScript = nil;
+ accent.subScript = nil;
+ // Remake the accentee (now with sub/superscripts)
+ // Note: Latex adjusts the heights in case the height of the char is different in non-cramped mode. However this shouldn't be the case since cramping
+ // only affects fractions and superscripts. We skip adjusting the heights.
+ if let remadeAccentee = MTTypesetter.createLineForMathList(accent.innerList, font:font, style:style, cramped:cramped) {
+ finalAccentee = remadeAccentee
+ }
+ }
+
+ let display = MTAccentDisplay(withAccent:accentGlyphDisplay, accentee:finalAccentee, range:accent.indexRange)
+ display.width = finalAccentee.width;
+ display.descent = finalAccentee.descent;
+
+ // Calculate total ascent based on positioning
+ // For arrows: height already includes spacing, so ascent = height + glyphAscent
+ // For regular accents: ascent = accentee.ascent - delta + glyphAscent (existing formula)
+ let ascent = height + glyphAscent;
+ display.ascent = max(finalAccentee.ascent, ascent);
+ display.position = currentPosition;
+
+ return display;
+ }
+
+ /// Determines if an accent can use Unicode composition for inline rendering.
+ /// Unicode combining characters only work correctly for single base characters.
+ /// Multi-character expressions and arrow accents need font-based rendering.
+ func canUseUnicodeComposition(_ accent: MTAccent) -> Bool {
+ // Check if innerList has exactly one simple character
+ guard let innerList = accent.innerList,
+ innerList.atoms.count == 1,
+ let firstAtom = innerList.atoms.first else {
+ return false
+ }
+
+ // Only allow simple variable/number atoms
+ guard firstAtom.type == .variable || firstAtom.type == .number else {
+ return false
+ }
+
+ // Check that the atom doesn't have subscripts/superscripts
+ guard firstAtom.subScript == nil && firstAtom.superScript == nil else {
+ return false
+ }
+
+ // Exclude arrow accents - they need stretching from font glyphs
+ // These Unicode combining characters only apply to single preceding characters
+ let arrowAccents: Set = [
+ "\u{20D6}", // overleftarrow
+ "\u{20D7}", // overrightarrow / vec
+ "\u{20E1}" // overleftrightarrow
+ ]
+
+ if arrowAccents.contains(accent.nucleus) {
+ return false
+ }
+
+ return true
+ }
+
+ // MARK: - Table
+
+ let kBaseLineSkipMultiplier = CGFloat(1.2) // default base line stretch is 12 pt for 10pt font.
+ let kLineSkipMultiplier = CGFloat(0.1) // default is 1pt for 10pt font.
+ let kLineSkipLimitMultiplier = CGFloat(0)
+ let kJotMultiplier = CGFloat(0.3) // A jot is 3pt for a 10pt font.
+
+ func makeTable(_ table:MTMathTable?) -> MTDisplay? {
+ let numColumns = table!.numColumns;
+ if numColumns == 0 || table!.numRows == 0 {
+ // Empty table
+ return MTMathListDisplay(withDisplays: [MTDisplay](), range: table!.indexRange)
+ }
+
+ var columnWidths = [CGFloat](repeating: 0, count: numColumns)
+ let displays = self.typesetCells(table, columnWidths:&columnWidths)
+
+ // Position all the columns in each row
+ var rowDisplays = [MTDisplay]()
+ for row in displays {
+ if let rowDisplay = self.makeRowWithColumns(row, forTable:table, columnWidths:columnWidths) {
+ rowDisplays.append(rowDisplay)
+ }
+ }
+
+ // Position all the rows
+ self.positionRows(rowDisplays, forTable:table)
+ let tableDisplay = MTMathListDisplay(withDisplays: rowDisplays, range: table!.indexRange)
+ tableDisplay.position = currentPosition;
+ return tableDisplay;
+ }
+
+ // Typeset every cell in the table. As a side-effect calculate the max column width of each column.
+ func typesetCells(_ table:MTMathTable?, columnWidths: inout [CGFloat]) -> [[MTDisplay]] {
+ var displays = [[MTDisplay]]()
+ for row in table!.cells {
+ var colDisplays = [MTDisplay]()
+ for i in 0.. MTMathListDisplay? {
+ var columnStart = CGFloat(0)
+ var rowRange = NSMakeRange(NSNotFound, 0);
+ for i in 0.. 0 {
+ if rowRange.location != NSNotFound {
+ rowRange = NSUnionRange(rowRange, col.range);
+ } else {
+ rowRange = col.range;
+ }
+ }
+ // If col.range is invalid or has zero length, skip it - don't update rowRange
+
+ col.position = CGPointMake(cellPos, 0);
+ columnStart += colWidth + table!.interColumnSpacing * styleFont.mathTable!.muUnit;
+ }
+
+ // If no valid ranges were found (all cells had zero-length ranges), use a synthetic range
+ // This represents the row conceptually spanning all its columns
+ if rowRange.location == NSNotFound {
+ rowRange = NSMakeRange(0, cols.count);
+ }
+
+ // Create a display for the row
+ let rowDisplay = MTMathListDisplay(withDisplays: cols, range:rowRange)
+ return rowDisplay
+ }
+
+ func positionRows(_ rows:[MTDisplay], forTable table:MTMathTable?) {
+ // Position the rows
+ // We will first position the rows starting from 0 and then in the second pass center the whole table vertically.
+ var currPos = CGFloat(0)
+ let openup = table!.interRowAdditionalSpacing * kJotMultiplier * styleFont.fontSize;
+ let baselineSkip = openup + kBaseLineSkipMultiplier * styleFont.fontSize;
+ let lineSkip = openup + kLineSkipMultiplier * styleFont.fontSize;
+ let lineSkipLimit = openup + kLineSkipLimitMultiplier * styleFont.fontSize;
+ var prevRowDescent = CGFloat(0)
+ var ascent = CGFloat(0)
+ var first = true
+ for row in rows {
+ if first {
+ row.position = CGPointZero;
+ ascent += row.ascent;
+ first = false;
+ } else {
+ var skip = baselineSkip;
+ if (skip - (prevRowDescent + row.ascent) < lineSkipLimit) {
+ // rows are too close to each other. Space them apart further
+ skip = prevRowDescent + row.ascent + lineSkip;
+ }
+ // We are going down so we decrease the y value.
+ currPos -= skip;
+ row.position = CGPointMake(0, currPos);
+ }
+ prevRowDescent = row.descent;
+ }
+
+ // Vertically center the whole structure around the axis
+ // The descent of the structure is the position of the last row
+ // plus the descent of the last row.
+ let descent = -currPos + prevRowDescent;
+ let shiftDown = 0.5*(ascent - descent) - styleFont.mathTable!.axisHeight;
+
+ for row in rows {
+ row.position = CGPointMake(row.position.x, row.position.y - shiftDown);
+ }
+ }
+}
diff --git a/third-party/SwiftMath/Sources/MathRender/MTUnicode.swift b/third-party/SwiftMath/Sources/MathRender/MTUnicode.swift
new file mode 100755
index 0000000000..a1fc48d41c
--- /dev/null
+++ b/third-party/SwiftMath/Sources/MathRender/MTUnicode.swift
@@ -0,0 +1,89 @@
+//
+// Created by Mike Griebling on 2022-12-31.
+// Translated from an Objective-C implementation by Kostub Deshmukh.
+//
+// This software may be modified and distributed under the terms of the
+// MIT license. See the LICENSE file for details.
+//
+
+import Foundation
+
+public struct UnicodeSymbol {
+ static let multiplication = "\u{00D7}"
+ static let division = "\u{00F7}"
+ static let fractionSlash = "\u{2044}"
+ static let whiteSquare = "\u{25A1}"
+ static let blackSquare = "\u{25A0}"
+ static let lessEqual = "\u{2264}"
+ static let greaterEqual = "\u{2265}"
+ static let notEqual = "\u{2260}"
+ static let squareRoot = "\u{221A}" // \sqrt
+ static let cubeRoot = "\u{221B}"
+ static let infinity = "\u{221E}" // \infty
+ static let angle = "\u{2220}" // \angle
+ static let degree = "\u{00B0}" // \circ
+
+ static let capitalGreekStart = UInt32(0x0391)
+ static let capitalGreekEnd = UInt32(0x03A9)
+ static let lowerGreekStart = UInt32(0x03B1)
+ static let lowerGreekEnd = UInt32(0x03C9)
+ static let planksConstant = UInt32(0x210e)
+ static let lowerItalicStart = UInt32(0x1D44E)
+ static let capitalItalicStart = UInt32(0x1D434)
+ static let greekLowerItalicStart = UInt32(0x1D6FC)
+ static let greekCapitalItalicStart = UInt32(0x1D6E2)
+ static let greekSymbolItalicStart = UInt32(0x1D716)
+
+ static let mathCapitalBoldStart = UInt32(0x1D400)
+ static let mathLowerBoldStart = UInt32(0x1D41A)
+ static let greekCapitalBoldStart = UInt32(0x1D6A8)
+ static let greekLowerBoldStart = UInt32(0x1D6C2)
+ static let greekSymbolBoldStart = UInt32(0x1D6DC)
+ static let numberBoldStart = UInt32(0x1D7CE)
+
+ static let mathCapitalBoldItalicStart = UInt32(0x1D468)
+ static let mathLowerBoldItalicStart = UInt32(0x1D482)
+ static let greekCapitalBoldItalicStart = UInt32(0x1D71C)
+ static let greekLowerBoldItalicStart = UInt32(0x1D736)
+ static let greekSymbolBoldItalicStart = UInt32(0x1D750)
+
+ static let mathCapitalScriptStart = UInt32(0x1D49C)
+ static let mathCapitalTTStart = UInt32(0x1D670)
+ static let mathLowerTTStart = UInt32(0x1D68A)
+ static let numberTTStart = UInt32(0x1D7F6)
+ static let mathCapitalSansSerifStart = UInt32(0x1D5A0)
+ static let mathLowerSansSerifStart = UInt32(0x1D5BA)
+ static let numberSansSerifStart = UInt32(0x1D7E2)
+ static let mathCapitalFrakturStart = UInt32(0x1D504)
+ static let mathLowerFrakturStart = UInt32(0x1D51E)
+ static let mathCapitalBlackboardStart = UInt32(0x1D538)
+ static let mathLowerBlackboardStart = UInt32(0x1D552)
+ static let numberBlackboardStart = UInt32(0x1D7D8)
+}
+
+extension Character {
+
+ var utf32Char: UTF32Char { self.unicodeScalars.map { $0.value }.reduce(0, +) }
+ var isLowerEnglish : Bool { self >= "a" && self <= "z" }
+ var isUpperEnglish : Bool { self >= "A" && self <= "Z" }
+ var isNumber : Bool { self >= "0" && self <= "9" }
+
+ var isLowerGreek : Bool {
+ let uch = self.utf32Char
+ return uch >= UnicodeSymbol.lowerGreekStart && uch <= UnicodeSymbol.lowerGreekEnd
+ }
+
+ var isCapitalGreek : Bool {
+ let uch = self.utf32Char
+ return uch >= UnicodeSymbol.capitalGreekStart && uch <= UnicodeSymbol.capitalGreekEnd
+ }
+
+ var greekSymbolOrder : UInt32? {
+ let greekSymbols : [UTF32Char] = [0x03F5, 0x03D1, 0x03F0, 0x03D5, 0x03F1, 0x03D6]
+ let index = greekSymbols.firstIndex(of: self.utf32Char)
+ if let pos = index { return UInt32(pos) }
+ return nil
+ }
+
+ var isGreekSymbol : Bool { self.greekSymbolOrder != nil }
+}
diff --git a/third-party/SwiftMath/Sources/MathRender/RWLock.swift b/third-party/SwiftMath/Sources/MathRender/RWLock.swift
new file mode 100644
index 0000000000..ccca3976e0
--- /dev/null
+++ b/third-party/SwiftMath/Sources/MathRender/RWLock.swift
@@ -0,0 +1,58 @@
+import Foundation
+
+final class RWLock {
+ init() {
+ pthread_rwlock_init(&lock, nil)
+ }
+
+ deinit {
+ pthread_rwlock_destroy(&lock)
+ }
+
+ func read(_ block: () -> T) -> T {
+ pthread_rwlock_rdlock(&lock)
+ defer { pthread_rwlock_unlock(&lock) }
+ return block()
+ }
+
+ func readWrite(_ block: () -> T) -> T {
+ pthread_rwlock_wrlock(&lock)
+ defer { pthread_rwlock_unlock(&lock) }
+ return block()
+ }
+
+ private var lock = pthread_rwlock_t()
+}
+
+@propertyWrapper
+struct RWLocked {
+ init(wrappedValue: T) {
+ value = wrappedValue
+ }
+
+ var wrappedValue: T {
+ get {
+ lock.read {
+ value
+ }
+ }
+ set {
+ lock.readWrite {
+ value = newValue
+ }
+ }
+ }
+
+ @discardableResult
+ mutating func readWrite(_ block: (inout T) -> Void) -> (oldValue: T, newValue: T) {
+ lock.readWrite {
+ let old = value
+ block(&value)
+ return (old, value)
+ }
+ }
+
+ private var value: T
+ private let lock = RWLock()
+}
+
diff --git a/third-party/SwiftMath/Sources/MathRender/Tokenization/MTAtomTokenizer.swift b/third-party/SwiftMath/Sources/MathRender/Tokenization/MTAtomTokenizer.swift
new file mode 100644
index 0000000000..2a76a661a4
--- /dev/null
+++ b/third-party/SwiftMath/Sources/MathRender/Tokenization/MTAtomTokenizer.swift
@@ -0,0 +1,1145 @@
+//
+// MTAtomTokenizer.swift
+// SwiftMath
+//
+// Created by Claude Code on 2025-12-16.
+// This software may be modified and distributed under the terms of the
+// MIT license. See the LICENSE file for details.
+//
+
+import Foundation
+import CoreGraphics
+
+/// Tokenizes MTMathAtom lists into breakable elements
+class MTAtomTokenizer {
+
+ // MARK: - Properties
+
+ let font: MTFont
+ let style: MTLineStyle
+ let cramped: Bool
+ let maxWidth: CGFloat
+ let widthCalculator: MTElementWidthCalculator
+ let displayRenderer: MTDisplayPreRenderer
+
+ // MARK: - Initialization
+
+ init(font: MTFont, style: MTLineStyle, cramped: Bool = false, maxWidth: CGFloat = 0) {
+ self.font = font
+ self.style = style
+ self.cramped = cramped
+ self.maxWidth = maxWidth
+ self.widthCalculator = MTElementWidthCalculator(font: font, style: style)
+ self.displayRenderer = MTDisplayPreRenderer(font: font, style: style, cramped: cramped)
+ }
+
+ // MARK: - Main Tokenization
+
+ /// Tokenize a list of atoms into breakable elements
+ func tokenize(_ atoms: [MTMathAtom]) -> [MTBreakableElement] {
+ var elements: [MTBreakableElement] = []
+ var index = 0
+ var currentStyle = self.style
+
+ while index < atoms.count {
+ let atom = atoms[index]
+ let prevAtom = index > 0 ? atoms[index - 1] : nil
+
+ // Check for style change atoms
+ if atom.type == .style, let styleAtom = atom as? MTMathStyle {
+ // Update style for subsequent atoms
+ currentStyle = styleAtom.style
+ index += 1
+ continue
+ }
+
+ // Create a tokenizer with the current style for this atom
+ let atomTokenizer: MTAtomTokenizer
+ if currentStyle != self.style {
+ atomTokenizer = MTAtomTokenizer(font: font, style: currentStyle, cramped: cramped, maxWidth: maxWidth)
+ } else {
+ atomTokenizer = self
+ }
+
+ // Handle scripts (subscript/superscript) - these must be grouped with their base
+ if atom.superScript != nil || atom.subScript != nil {
+ let baseElements = atomTokenizer.tokenizeAtomWithScripts(atom, prevAtom: prevAtom, atomIndex: index, allAtoms: atoms)
+ elements.append(contentsOf: baseElements)
+ } else {
+ // Check if this is a multi-character text atom that needs character-level tokenization
+ let isTextAtom = atom.fontStyle == .roman
+ let isMultiChar = atom.nucleus.count > 1
+
+ if isTextAtom && isMultiChar {
+ // Break down multi-character text into individual characters for punctuation rules
+ let charElements = atomTokenizer.tokenizeMultiCharText(atom, prevElements: elements, atomIndex: index, allAtoms: atoms)
+ elements.append(contentsOf: charElements)
+ } else {
+ // Regular atom without scripts
+ if let element = atomTokenizer.tokenizeAtom(atom, prevAtom: prevAtom, atomIndex: index, allAtoms: atoms) {
+ elements.append(element)
+ }
+ }
+ }
+
+ index += 1
+ }
+
+ return elements
+ }
+
+ // MARK: - Atom Tokenization
+
+ /// Tokenize a single atom (without scripts)
+ private func tokenizeAtom(_ atom: MTMathAtom, prevAtom: MTMathAtom?, atomIndex: Int, allAtoms: [MTMathAtom]) -> MTBreakableElement? {
+ switch atom.type {
+ // Simple text and variables
+ case .ordinary, .variable, .number:
+ return tokenizeTextAtom(atom, prevAtom: prevAtom, atomIndex: atomIndex, allAtoms: allAtoms)
+
+ // Operators
+ case .binaryOperator, .relation, .unaryOperator:
+ return tokenizeOperator(atom, prevAtom: prevAtom, atomIndex: atomIndex)
+
+ // Delimiters
+ case .open:
+ return tokenizeOpenDelimiter(atom, prevAtom: prevAtom, atomIndex: atomIndex)
+
+ case .close:
+ return tokenizeCloseDelimiter(atom, prevAtom: prevAtom, atomIndex: atomIndex)
+
+ // Punctuation
+ case .punctuation:
+ return tokenizePunctuation(atom, prevAtom: prevAtom, atomIndex: atomIndex)
+
+ // Complex structures (atomic)
+ case .fraction:
+ return tokenizeFraction(atom as! MTFraction, prevAtom: prevAtom, atomIndex: atomIndex)
+
+ case .radical:
+ return tokenizeRadical(atom as! MTRadical, prevAtom: prevAtom, atomIndex: atomIndex)
+
+ case .largeOperator:
+ return tokenizeLargeOperator(atom as! MTLargeOperator, prevAtom: prevAtom, atomIndex: atomIndex)
+
+ case .accent:
+ return tokenizeAccent(atom as! MTAccent, prevAtom: prevAtom, atomIndex: atomIndex, allAtoms: allAtoms)
+
+ case .underline:
+ return tokenizeUnderline(atom as! MTUnderLine, prevAtom: prevAtom, atomIndex: atomIndex)
+
+ case .overline:
+ return tokenizeOverline(atom as! MTOverLine, prevAtom: prevAtom, atomIndex: atomIndex)
+
+ case .table:
+ return tokenizeTable(atom as! MTMathTable, prevAtom: prevAtom, atomIndex: atomIndex)
+
+ case .inner:
+ return tokenizeInner(atom as! MTInner, prevAtom: prevAtom, atomIndex: atomIndex)
+
+ // Spacing
+ case .space:
+ return tokenizeSpace(atom, prevAtom: prevAtom, atomIndex: atomIndex)
+
+ // Style changes - these don't create elements
+ case .style:
+ return nil
+
+ // Color - extract inner content with color attribute
+ case .color, .colorBox, .textcolor:
+ // For now, treat as ordinary (color will be handled in display generation)
+ return tokenizeTextAtom(atom, prevAtom: prevAtom, atomIndex: atomIndex, allAtoms: allAtoms)
+
+ default:
+ // Treat unknown types as ordinary
+ return tokenizeTextAtom(atom, prevAtom: prevAtom, atomIndex: atomIndex, allAtoms: allAtoms)
+ }
+ }
+
+ // MARK: - Text Atom Tokenization
+
+ private func tokenizeTextAtom(_ atom: MTMathAtom, prevAtom: MTMathAtom?, atomIndex: Int, allAtoms: [MTMathAtom]) -> MTBreakableElement? {
+ let text = atom.nucleus
+ guard !text.isEmpty else { return nil }
+
+ // Calculate width
+ let width = widthCalculator.measureText(text)
+
+ // Calculate ascent/descent (approximate using font metrics)
+ let ascent = font.mathTable?.axisHeight ?? font.fontSize * 0.5
+ let descent = font.fontSize * 0.2
+ let height = ascent + descent
+
+ // Determine break rules using Unicode word boundary detection
+ var isBreakBefore = true
+ var isBreakAfter = true
+ var penaltyBefore = MTBreakPenalty.good
+ var penaltyAfter = MTBreakPenalty.good
+
+ let isTextAtom = atom.fontStyle == .roman
+
+ // Only apply word boundary logic to text atoms (not math variables)
+ if isTextAtom {
+ // First apply punctuation rules for single-character text
+ // This handles cases where punctuation appears in roman text rather than as separate punctuation atoms
+ if text.count == 1, let char = text.first {
+ let (punctBreakBefore, punctBreakAfter, punctPenaltyBefore, punctPenaltyAfter) = punctuationBreakRules(char)
+
+ // Apply punctuation rules
+ isBreakBefore = punctBreakBefore
+ penaltyBefore = punctPenaltyBefore
+ isBreakAfter = punctBreakAfter
+ penaltyAfter = punctPenaltyAfter
+ }
+
+ // Then apply word boundary logic - this ANDs with punctuation rules
+ // Both rules must allow breaking for a break to be permitted
+
+ // Check if we should break BEFORE this atom
+ if let prevAtom = prevAtom {
+ // Handle previous accent atoms (e.g., "é" before "r" in "bactéries")
+ if prevAtom.type == .accent && isTextLetterAtom(prevAtom) {
+ // Previous is a text accent - don't break if current is a letter
+ if text.first?.isLetter == true {
+ isBreakBefore = false
+ penaltyBefore = MTBreakPenalty.never
+ }
+ } else if prevAtom.fontStyle == .roman {
+ let prevText = prevAtom.nucleus
+ if !prevText.isEmpty && !text.isEmpty {
+ // Use Unicode word boundary detection
+ if !hasWordBoundaryBetween(prevText, and: text) {
+ // No word boundary = we're in the middle of a word
+ isBreakBefore = false
+ penaltyBefore = MTBreakPenalty.never
+ }
+ }
+ }
+ }
+
+ // Check if we should break AFTER this atom
+ if atomIndex + 1 < allAtoms.count {
+ let nextAtom = allAtoms[atomIndex + 1]
+ // Handle next accent atoms (e.g., "t" before "é" in "bactéries")
+ if nextAtom.type == .accent && isTextLetterAtom(nextAtom) {
+ // Next is a text accent - don't break if current is a letter
+ if text.last?.isLetter == true {
+ isBreakAfter = false
+ penaltyAfter = MTBreakPenalty.never
+ }
+ } else if nextAtom.fontStyle == .roman {
+ let nextText = nextAtom.nucleus
+ if !text.isEmpty && !nextText.isEmpty {
+ // Use Unicode word boundary detection
+ if !hasWordBoundaryBetween(text, and: nextText) {
+ // No word boundary = next atom is part of same word
+ isBreakAfter = false
+ penaltyAfter = MTBreakPenalty.never
+ }
+ }
+ }
+ }
+ }
+
+ return MTBreakableElement(
+ content: .text(text),
+ width: width,
+ height: height,
+ ascent: ascent,
+ descent: descent,
+ isBreakBefore: isBreakBefore,
+ isBreakAfter: isBreakAfter,
+ penaltyBefore: penaltyBefore,
+ penaltyAfter: penaltyAfter,
+ groupId: nil,
+ parentId: nil,
+ originalAtom: atom,
+ indexRange: atom.indexRange,
+ color: nil,
+ backgroundColor: nil,
+ indivisible: false
+ )
+ }
+
+ /// Tokenize a multi-character text atom into individual character elements
+ /// This enables character-level line breaking with proper punctuation rules
+ private func tokenizeMultiCharText(_ atom: MTMathAtom, prevElements: [MTBreakableElement], atomIndex: Int, allAtoms: [MTMathAtom]) -> [MTBreakableElement] {
+ let text = atom.nucleus
+ guard text.count > 1 else { return [] }
+
+ let debugTokenization = !"".isEmpty // Enable to debug text tokenization
+ if debugTokenization {
+ print("\n=== Tokenizing multi-char text: '\(text)' ===")
+ }
+
+ var charElements: [MTBreakableElement] = []
+ let characters = Array(text)
+
+ for (charIndex, char) in characters.enumerated() {
+ let charString = String(char)
+
+ // Calculate width for this character
+ let width = widthCalculator.measureText(charString)
+
+ // Calculate ascent/descent (approximate using font metrics)
+ let ascent = font.mathTable?.axisHeight ?? font.fontSize * 0.5
+ let descent = font.fontSize * 0.2
+ let height = ascent + descent
+
+ // Determine break rules for this character
+ let (isBreakBefore, isBreakAfter, penaltyBefore, penaltyAfter) = characterBreakRules(
+ char: char,
+ prevChar: charIndex > 0 ? characters[charIndex - 1] : nil,
+ nextChar: charIndex < characters.count - 1 ? characters[charIndex + 1] : nil,
+ isFirstInAtom: charIndex == 0,
+ isLastInAtom: charIndex == characters.count - 1,
+ prevElements: prevElements,
+ nextAtom: atomIndex + 1 < allAtoms.count ? allAtoms[atomIndex + 1] : nil
+ )
+
+ let element = MTBreakableElement(
+ content: .text(charString),
+ width: width,
+ height: height,
+ ascent: ascent,
+ descent: descent,
+ isBreakBefore: isBreakBefore,
+ isBreakAfter: isBreakAfter,
+ penaltyBefore: penaltyBefore,
+ penaltyAfter: penaltyAfter,
+ groupId: nil,
+ parentId: nil,
+ originalAtom: atom,
+ indexRange: atom.indexRange,
+ color: nil,
+ backgroundColor: nil,
+ indivisible: false
+ )
+
+ if debugTokenization {
+ print(" [\(charIndex)] '\(charString)' breakBefore=\(isBreakBefore) breakAfter=\(isBreakAfter) penaltyBefore=\(penaltyBefore) penaltyAfter=\(penaltyAfter) width=\(width)")
+ }
+
+ charElements.append(element)
+ }
+
+ return charElements
+ }
+
+ /// Determine break rules for a character in a multi-character text string
+ private func characterBreakRules(
+ char: Character,
+ prevChar: Character?,
+ nextChar: Character?,
+ isFirstInAtom: Bool,
+ isLastInAtom: Bool,
+ prevElements: [MTBreakableElement],
+ nextAtom: MTMathAtom?
+ ) -> (isBreakBefore: Bool, isBreakAfter: Bool, penaltyBefore: Int, penaltyAfter: Int) {
+
+ // Apply punctuation rules
+ let (punctBreakBefore, punctBreakAfter, punctPenaltyBefore, punctPenaltyAfter) = punctuationBreakRules(char)
+
+ var isBreakBefore = punctBreakBefore
+ var isBreakAfter = punctBreakAfter
+ var penaltyBefore = punctPenaltyBefore
+ var penaltyAfter = punctPenaltyAfter
+
+ // Apply word boundary logic
+ // Don't break in the middle of a word (but CJK characters CAN break between each other)
+ if let prevChar = prevChar {
+ if char.isLetter && prevChar.isLetter {
+ // Check if either character is CJK - CJK allows breaks between characters
+ let isCJKBreak = isCJKCharacter(char) || isCJKCharacter(prevChar)
+
+ if !isCJKBreak {
+ // Both letters in same non-CJK script - middle of word, don't break
+ isBreakBefore = false
+ penaltyBefore = MTBreakPenalty.never
+ }
+ // else: At least one is CJK - allow break (keep punctBreakBefore value)
+ } else if prevChar == "'" || prevChar == "-" {
+ // Apostrophe or hyphen - part of word
+ isBreakBefore = false
+ penaltyBefore = MTBreakPenalty.never
+ }
+ } else if isFirstInAtom {
+ // First character - check against previous element
+ if let lastElement = prevElements.last {
+ switch lastElement.content {
+ case .text(let prevText):
+ if let prevLastChar = prevText.last {
+ if char.isLetter && prevLastChar.isLetter {
+ // Check if either character is CJK
+ let isCJKBreak = isCJKCharacter(char) || isCJKCharacter(prevLastChar)
+
+ if !isCJKBreak {
+ // Both non-CJK letters - don't break
+ isBreakBefore = false
+ penaltyBefore = MTBreakPenalty.never
+ }
+ } else if prevLastChar == "'" || prevLastChar == "-" {
+ isBreakBefore = false
+ penaltyBefore = MTBreakPenalty.never
+ }
+ }
+ case .display:
+ // Check if previous element is a text-mode accent (e.g., "é")
+ // Accents in text mode should not allow breaks after them if current is a letter
+ if lastElement.originalAtom.type == .accent,
+ isTextLetterAtom(lastElement.originalAtom),
+ char.isLetter {
+ isBreakBefore = false
+ penaltyBefore = MTBreakPenalty.never
+ }
+ default:
+ break
+ }
+ }
+ }
+
+ if let nextChar = nextChar {
+ if char.isLetter && nextChar.isLetter {
+ // Check if either character is CJK
+ let isCJKBreak = isCJKCharacter(char) || isCJKCharacter(nextChar)
+
+ if !isCJKBreak {
+ // Both non-CJK letters - middle of word, don't break
+ isBreakAfter = false
+ penaltyAfter = MTBreakPenalty.never
+ }
+ } else if nextChar == "'" || nextChar == "-" {
+ // Before apostrophe or hyphen - part of word
+ isBreakAfter = false
+ penaltyAfter = MTBreakPenalty.never
+ }
+ } else if isLastInAtom {
+ // Last character - check against next atom
+ if let nextAtom = nextAtom {
+ // Handle next accent atoms (e.g., "t" before "é" in "bactéries")
+ if nextAtom.type == .accent && isTextLetterAtom(nextAtom) {
+ if char.isLetter {
+ isBreakAfter = false
+ penaltyAfter = MTBreakPenalty.never
+ }
+ } else if nextAtom.fontStyle == .roman,
+ let nextFirstChar = nextAtom.nucleus.first {
+ if char.isLetter && nextFirstChar.isLetter {
+ // Check if either character is CJK
+ let isCJKBreak = isCJKCharacter(char) || isCJKCharacter(nextFirstChar)
+
+ if !isCJKBreak {
+ // Both non-CJK letters - don't break
+ isBreakAfter = false
+ penaltyAfter = MTBreakPenalty.never
+ }
+ }
+ }
+ }
+ }
+
+ return (isBreakBefore, isBreakAfter, penaltyBefore, penaltyAfter)
+ }
+
+ // MARK: - Word Boundary Detection
+
+ /// Determines if a character is a CJK (Chinese, Japanese, Korean) character
+ /// CJK characters can break between each other even though they are technically "letters"
+ private func isCJKCharacter(_ char: Character) -> Bool {
+ guard let scalar = char.unicodeScalars.first else { return false }
+ let value = scalar.value
+
+ // CJK Unified Ideographs and extensions
+ return (value >= 0x4E00 && value <= 0x9FFF) || // CJK Unified Ideographs (most common Chinese/Japanese kanji)
+ (value >= 0x3400 && value <= 0x4DBF) || // CJK Unified Ideographs Extension A
+ (value >= 0x20000 && value <= 0x2A6DF) || // CJK Unified Ideographs Extension B
+ (value >= 0x3040 && value <= 0x309F) || // Hiragana (Japanese)
+ (value >= 0x30A0 && value <= 0x30FF) || // Katakana (Japanese)
+ (value >= 0xAC00 && value <= 0xD7AF) // Hangul Syllables (Korean)
+ }
+
+ /// Determines if there's a word boundary between two text fragments
+ /// Combines Unicode word segmentation with special handling for contractions and hyphenated words
+ private func hasWordBoundaryBetween(_ text1: String, and text2: String) -> Bool {
+ // RULE 1: Check for apostrophes and hyphens between letters (contractions and hyphenated words)
+ // These should NOT be treated as word boundaries even though Unicode does
+ if let lastChar1 = text1.last, let firstChar2 = text2.first {
+ // Pattern: letter + apostrophe|hyphen + letter → NOT a word boundary
+ if lastChar1.isLetter && (firstChar2 == "'" || firstChar2 == "-") {
+ return false // Don't break before apostrophe/hyphen
+ }
+ if (lastChar1 == "'" || lastChar1 == "-") && firstChar2.isLetter {
+ return false // Don't break after apostrophe/hyphen
+ }
+ }
+
+ // RULE 2: Use Unicode word boundary detection for everything else
+ // This properly handles:
+ // - International text (café, naïve, etc.)
+ // - Various Unicode whitespace characters
+ // - Em-dashes, ellipses, and other Unicode punctuation
+ // - Complex scripts (Thai, Japanese, etc.)
+ let combined = text1 + text2
+ let junctionIndex = text1.endIndex
+
+ var wordBoundaries: Set = []
+ combined.enumerateSubstrings(in: combined.startIndex.. PunctuationClass {
+ let scalar = String(char).unicodeScalars.first?.value ?? 0
+
+ // Latin opening punctuation - never break after
+ if "([{".contains(char) { return .openingPunctuation }
+
+ // Latin closing punctuation and sentence-ending - never break before
+ if ")]}".contains(char) { return .closingPunctuation }
+ if ".,;:!?".contains(char) { return .sentenceEnding }
+
+ // Latin quotation marks - opening quotes
+ // U+0022 " QUOTATION MARK, U+0027 ' APOSTROPHE
+ // U+2018 ' LEFT SINGLE QUOTATION MARK, U+201C " LEFT DOUBLE QUOTATION MARK
+ // U+00AB « LEFT-POINTING DOUBLE ANGLE QUOTATION MARK, U+2039 ‹ SINGLE LEFT-POINTING ANGLE QUOTATION MARK
+ if scalar == 0x0022 || scalar == 0x0027 || // Basic quotes
+ scalar == 0x2018 || scalar == 0x201C || // Curly left quotes
+ scalar == 0x00AB || scalar == 0x2039 { // Guillemets
+ return .openingPunctuation
+ }
+
+ // Latin quotation marks - closing quotes
+ // U+2019 ' RIGHT SINGLE QUOTATION MARK, U+201D " RIGHT DOUBLE QUOTATION MARK
+ // U+00BB » RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK, U+203A › SINGLE RIGHT-POINTING ANGLE QUOTATION MARK
+ if scalar == 0x2019 || scalar == 0x201D || // Curly right quotes
+ scalar == 0x00BB || scalar == 0x203A { // Guillemets
+ return .closingPunctuation
+ }
+
+ // CJK opening brackets (禁則: line-start prohibited)
+ // Japanese/Chinese full-width brackets and corner brackets
+ if "「『(【〔〈《".contains(char) {
+ return .openingPunctuation
+ }
+
+ // CJK closing brackets (禁則: line-end prohibited)
+ if "」』)】〕〉》".contains(char) {
+ return .closingPunctuation
+ }
+
+ // CJK sentence-ending punctuation (禁則: line-end prohibited)
+ // Japanese/Chinese full-width periods, commas, and other punctuation
+ if "。、!?:;".contains(char) {
+ return .sentenceEnding
+ }
+
+ // CJK small kana (禁則: line-end prohibited)
+ // These are smaller versions of hiragana/katakana that must not start a line
+ if "ぁぃぅぇぉっゃゅょゎァィゥェォッャュョヮ".contains(char) {
+ return .cjkSmallKana
+ }
+
+ // CJK iteration marks (禁則: line-end prohibited)
+ if "ゝゞヽヾ々〻".contains(char) {
+ return .cjkSmallKana // Same rules as small kana
+ }
+
+ // CJK prolonged sound mark (禁則: line-end prohibited)
+ if char == "ー" {
+ return .cjkSmallKana // Same rules as small kana
+ }
+
+ return .neutral
+ }
+
+ /// Determine break rules for punctuation based on its classification
+ private func punctuationBreakRules(_ char: Character) -> (isBreakBefore: Bool, isBreakAfter: Bool, penaltyBefore: Int, penaltyAfter: Int) {
+ let classification = classifyPunctuation(char)
+
+ switch classification {
+ case .openingPunctuation:
+ // Opening punctuation: can break before, NEVER after
+ // Examples: ( [ { " ' « 「『
+ return (true, false, MTBreakPenalty.good, MTBreakPenalty.never)
+
+ case .closingPunctuation:
+ // Closing punctuation: NEVER before, can break after
+ // Examples: ) ] } " ' » 」』
+ return (false, true, MTBreakPenalty.never, MTBreakPenalty.good)
+
+ case .sentenceEnding:
+ // Sentence-ending punctuation: NEVER before, good break after
+ // Examples: . , ; : ! ? 。、
+ return (false, true, MTBreakPenalty.never, MTBreakPenalty.best)
+
+ case .cjkSmallKana:
+ // CJK small kana and iteration marks: NEVER before, can break after
+ // Examples: っゃゅょゎ ゝゞ ー
+ return (false, true, MTBreakPenalty.never, MTBreakPenalty.good)
+
+ case .neutral:
+ // Other punctuation: use default rules
+ return (true, true, MTBreakPenalty.good, MTBreakPenalty.good)
+ }
+ }
+
+ // MARK: - Operator Tokenization
+
+ private func tokenizeOperator(_ atom: MTMathAtom, prevAtom: MTMathAtom?, atomIndex: Int) -> MTBreakableElement? {
+ let op = atom.nucleus
+ guard !op.isEmpty else { return nil }
+
+ // Calculate width with operator spacing
+ let width = widthCalculator.measureOperator(op, type: atom.type)
+
+ let ascent = font.fontSize * 0.5
+ let descent = font.fontSize * 0.2
+ let height = ascent + descent
+
+ return MTBreakableElement(
+ content: .operator(op, type: atom.type),
+ width: width,
+ height: height,
+ ascent: ascent,
+ descent: descent,
+ isBreakBefore: true,
+ isBreakAfter: true,
+ penaltyBefore: MTBreakPenalty.best, // Operators are best break points
+ penaltyAfter: MTBreakPenalty.best,
+ groupId: nil,
+ parentId: nil,
+ originalAtom: atom,
+ indexRange: atom.indexRange,
+ color: nil,
+ backgroundColor: nil,
+ indivisible: false
+ )
+ }
+
+ // MARK: - Delimiter Tokenization
+
+ private func tokenizeOpenDelimiter(_ atom: MTMathAtom, prevAtom: MTMathAtom?, atomIndex: Int) -> MTBreakableElement? {
+ let delimiter = atom.nucleus
+ let width = widthCalculator.measureText(delimiter)
+ let ascent = font.fontSize * 0.6
+ let descent = font.fontSize * 0.2
+
+ return MTBreakableElement(
+ content: .text(delimiter),
+ width: width,
+ height: ascent + descent,
+ ascent: ascent,
+ descent: descent,
+ isBreakBefore: true,
+ isBreakAfter: false, // NEVER break after open delimiter
+ penaltyBefore: MTBreakPenalty.acceptable,
+ penaltyAfter: MTBreakPenalty.bad,
+ groupId: nil,
+ parentId: nil,
+ originalAtom: atom,
+ indexRange: atom.indexRange,
+ color: nil,
+ backgroundColor: nil,
+ indivisible: false
+ )
+ }
+
+ private func tokenizeCloseDelimiter(_ atom: MTMathAtom, prevAtom: MTMathAtom?, atomIndex: Int) -> MTBreakableElement? {
+ let delimiter = atom.nucleus
+ let width = widthCalculator.measureText(delimiter)
+ let ascent = font.fontSize * 0.6
+ let descent = font.fontSize * 0.2
+
+ return MTBreakableElement(
+ content: .text(delimiter),
+ width: width,
+ height: ascent + descent,
+ ascent: ascent,
+ descent: descent,
+ isBreakBefore: false, // NEVER break before close delimiter
+ isBreakAfter: true,
+ penaltyBefore: MTBreakPenalty.bad,
+ penaltyAfter: MTBreakPenalty.acceptable,
+ groupId: nil,
+ parentId: nil,
+ originalAtom: atom,
+ indexRange: atom.indexRange,
+ color: nil,
+ backgroundColor: nil,
+ indivisible: false
+ )
+ }
+
+ // MARK: - Punctuation Tokenization
+
+ private func tokenizePunctuation(_ atom: MTMathAtom, prevAtom: MTMathAtom?, atomIndex: Int) -> MTBreakableElement? {
+ let punct = atom.nucleus
+ let width = widthCalculator.measureText(punct)
+ let ascent = font.fontSize * 0.5
+ let descent = font.fontSize * 0.2
+
+ // Apply proper punctuation breaking rules based on character classification
+ // Default rules for multi-character punctuation or empty
+ var isBreakBefore = false
+ var isBreakAfter = true
+ var penaltyBefore = MTBreakPenalty.bad
+ var penaltyAfter = MTBreakPenalty.good
+
+ // For single-character punctuation, use classification rules
+ if punct.count == 1, let char = punct.first {
+ (isBreakBefore, isBreakAfter, penaltyBefore, penaltyAfter) = punctuationBreakRules(char)
+ }
+
+ return MTBreakableElement(
+ content: .text(punct),
+ width: width,
+ height: ascent + descent,
+ ascent: ascent,
+ descent: descent,
+ isBreakBefore: isBreakBefore,
+ isBreakAfter: isBreakAfter,
+ penaltyBefore: penaltyBefore,
+ penaltyAfter: penaltyAfter,
+ groupId: nil,
+ parentId: nil,
+ originalAtom: atom,
+ indexRange: atom.indexRange,
+ color: nil,
+ backgroundColor: nil,
+ indivisible: false
+ )
+ }
+
+ // MARK: - Script Tokenization
+
+ private func tokenizeAtomWithScripts(_ atom: MTMathAtom, prevAtom: MTMathAtom?, atomIndex: Int, allAtoms: [MTMathAtom]) -> [MTBreakableElement] {
+ var elements: [MTBreakableElement] = []
+ let groupId = UUID() // All elements in this group must stay together
+
+ // First, create the base element
+ if let baseElement = tokenizeAtom(atom, prevAtom: prevAtom, atomIndex: atomIndex, allAtoms: allAtoms) {
+ var modifiedBase = baseElement
+ // Modify to be part of group
+ modifiedBase = MTBreakableElement(
+ content: baseElement.content,
+ width: baseElement.width,
+ height: baseElement.height,
+ ascent: baseElement.ascent,
+ descent: baseElement.descent,
+ isBreakBefore: baseElement.isBreakBefore,
+ isBreakAfter: false, // Cannot break after base - must include scripts
+ penaltyBefore: baseElement.penaltyBefore,
+ penaltyAfter: MTBreakPenalty.never,
+ groupId: groupId,
+ parentId: nil,
+ originalAtom: baseElement.originalAtom,
+ indexRange: baseElement.indexRange,
+ color: baseElement.color,
+ backgroundColor: baseElement.backgroundColor,
+ indivisible: baseElement.indivisible
+ )
+ elements.append(modifiedBase)
+ }
+
+ // Add superscript first if present (matches legacy typesetter order)
+ if let superScript = atom.superScript {
+ if let scriptDisplay = displayRenderer.renderScript(superScript, isSuper: true) {
+ let scriptElement = MTBreakableElement(
+ content: .script(scriptDisplay, isSuper: true),
+ width: scriptDisplay.width,
+ height: scriptDisplay.ascent + scriptDisplay.descent,
+ ascent: scriptDisplay.ascent,
+ descent: scriptDisplay.descent,
+ isBreakBefore: false, // Must stay with base
+ isBreakAfter: atom.subScript == nil, // Can break after if last script
+ penaltyBefore: MTBreakPenalty.never,
+ penaltyAfter: atom.subScript == nil ? MTBreakPenalty.good : MTBreakPenalty.never,
+ groupId: groupId,
+ parentId: nil,
+ originalAtom: atom,
+ indexRange: atom.indexRange,
+ color: nil,
+ backgroundColor: nil,
+ indivisible: true
+ )
+ elements.append(scriptElement)
+ }
+ }
+
+ // Add subscript after superscript (matches legacy typesetter order)
+ if let subScript = atom.subScript {
+ if let scriptDisplay = displayRenderer.renderScript(subScript, isSuper: false) {
+ let scriptElement = MTBreakableElement(
+ content: .script(scriptDisplay, isSuper: false),
+ width: scriptDisplay.width,
+ height: scriptDisplay.ascent + scriptDisplay.descent,
+ ascent: scriptDisplay.ascent,
+ descent: scriptDisplay.descent,
+ isBreakBefore: false, // Must stay with base
+ isBreakAfter: true, // Can break after subscript (it's always last)
+ penaltyBefore: MTBreakPenalty.never,
+ penaltyAfter: MTBreakPenalty.good,
+ groupId: groupId,
+ parentId: nil,
+ originalAtom: atom,
+ indexRange: atom.indexRange,
+ color: nil,
+ backgroundColor: nil,
+ indivisible: true
+ )
+ elements.append(scriptElement)
+ }
+ }
+
+ return elements
+ }
+
+ // MARK: - Complex Structure Tokenization
+
+ private func tokenizeFraction(_ fraction: MTFraction, prevAtom: MTMathAtom?, atomIndex: Int) -> MTBreakableElement? {
+ // Create a temporary typesetter to render the fraction
+ let typesetter = MTTypesetter(withFont: font, style: style, cramped: cramped, spaced: false)
+ guard let display = typesetter.makeFraction(fraction) else { return nil }
+
+ return MTBreakableElement(
+ content: .display(display),
+ width: display.width,
+ height: display.ascent + display.descent,
+ ascent: display.ascent,
+ descent: display.descent,
+ isBreakBefore: true,
+ isBreakAfter: true,
+ penaltyBefore: MTBreakPenalty.moderate,
+ penaltyAfter: MTBreakPenalty.moderate,
+ groupId: nil,
+ parentId: nil,
+ originalAtom: fraction,
+ indexRange: fraction.indexRange,
+ color: nil,
+ backgroundColor: nil,
+ indivisible: true // Fractions are atomic
+ )
+ }
+
+ private func tokenizeRadical(_ radical: MTRadical, prevAtom: MTMathAtom?, atomIndex: Int) -> MTBreakableElement? {
+ let typesetter = MTTypesetter(withFont: font, style: style, cramped: cramped, spaced: false)
+ guard let display = typesetter.makeRadical(radical.radicand, range: radical.indexRange) else { return nil }
+
+ // Add degree if present
+ if radical.degree != nil {
+ // Use .script style (71% size) instead of .scriptOfScript (50% size)
+ // This matches TeX standard for radical degrees
+ let degree = MTTypesetter.createLineForMathList(radical.degree, font: font, style: .script)
+ display.setDegree(degree, fontMetrics: font.mathTable)
+ }
+
+ return MTBreakableElement(
+ content: .display(display),
+ width: display.width,
+ height: display.ascent + display.descent,
+ ascent: display.ascent,
+ descent: display.descent,
+ isBreakBefore: true,
+ isBreakAfter: true,
+ penaltyBefore: MTBreakPenalty.good,
+ penaltyAfter: MTBreakPenalty.good,
+ groupId: nil,
+ parentId: nil,
+ originalAtom: radical,
+ indexRange: radical.indexRange,
+ color: nil,
+ backgroundColor: nil,
+ indivisible: true // Radicals are atomic
+ )
+ }
+
+ private func tokenizeLargeOperator(_ op: MTLargeOperator, prevAtom: MTMathAtom?, atomIndex: Int) -> MTBreakableElement? {
+ // CRITICAL DISTINCTION:
+ // - If op.limits=true (e.g., \sum, \prod, \lim in text mode): Scripts go ABOVE/BELOW
+ // → makeLargeOp() creates MTLargeOpLimitsDisplay, which is self-contained
+ // → We should NOT clear scripts, let makeLargeOp() handle everything
+ //
+ // - If op.limits=false (e.g., \int in text mode): Scripts go TO THE SIDE
+ // → makeLargeOp() would create scripts via makeScripts(), causing duplication
+ // → We MUST clear scripts and let tokenizeAtomWithScripts() handle them separately
+
+ let limits = op.limits && (style == .display || style == .text)
+
+ let originalSuperScript = op.superScript
+ let originalSubScript = op.subScript
+
+ // Only clear scripts for side-script operators (limits=false)
+ if !limits && (originalSuperScript != nil || originalSubScript != nil) {
+ op.superScript = nil
+ op.subScript = nil
+ }
+
+ let typesetter = MTTypesetter(withFont: font, style: style, cramped: cramped, spaced: false)
+ guard let operatorDisplay = typesetter.makeLargeOp(op) else {
+ // Restore scripts before returning
+ op.superScript = originalSuperScript
+ op.subScript = originalSubScript
+ return nil
+ }
+
+ // CRITICAL: Handle scripts based on positioning mode
+ if !limits {
+ // Side-script operators (limits=false): Restore scripts for tokenizeAtomWithScripts to handle
+ op.superScript = originalSuperScript
+ op.subScript = originalSubScript
+ } else {
+ // Limit operators (limits=true): Scripts are already rendered in MTLargeOpLimitsDisplay
+ // MUST clear them from atom to prevent tokenizeAtomWithScripts from rendering them again
+ op.superScript = nil
+ op.subScript = nil
+ }
+
+ // CRITICAL: Handle italic correction (delta) for side-script operators
+ // When scripts are present and limits is false, the operator width is reduced by delta
+ // (see MTTypesetter.makeLargeOp line 1046-1050)
+ // Since we cleared scripts for side-script operators, makeLargeOp() didn't apply this reduction
+ var finalWidth = operatorDisplay.width
+
+ if !limits && (originalSubScript != nil) {
+ // Get the italic correction for the operator glyph
+ if let glyphDisplay = operatorDisplay as? MTGlyphDisplay,
+ let mathTable = font.mathTable {
+ let delta = mathTable.getItalicCorrection(glyphDisplay.glyph)
+ finalWidth -= delta
+ }
+ }
+
+ let finalDisplay = operatorDisplay
+
+ return MTBreakableElement(
+ content: .display(finalDisplay),
+ width: finalWidth,
+ height: finalDisplay.ascent + finalDisplay.descent,
+ ascent: finalDisplay.ascent,
+ descent: finalDisplay.descent,
+ isBreakBefore: true,
+ isBreakAfter: true,
+ penaltyBefore: MTBreakPenalty.good,
+ penaltyAfter: MTBreakPenalty.good,
+ groupId: nil,
+ parentId: nil,
+ originalAtom: op,
+ indexRange: op.indexRange,
+ color: nil,
+ backgroundColor: nil,
+ indivisible: true
+ )
+ }
+
+ private func tokenizeAccent(_ accent: MTAccent, prevAtom: MTMathAtom?, atomIndex: Int, allAtoms: [MTMathAtom]) -> MTBreakableElement? {
+ let typesetter = MTTypesetter(withFont: font, style: style, cramped: cramped, spaced: false)
+ guard let display = typesetter.makeAccent(accent) else { return nil }
+
+ // Determine break rules - accents in text mode should respect word boundaries
+ var isBreakBefore = true
+ var isBreakAfter = true
+ var penaltyBefore = MTBreakPenalty.good
+ var penaltyAfter = MTBreakPenalty.good
+
+ // Check if this accent is in a text context (the accented character is roman text)
+ // An accent's innerList contains the base character(s)
+ let isTextAccent = accent.innerList?.atoms.first?.fontStyle == .roman
+
+ if isTextAccent {
+ // Check previous atom - if it's a letter in text mode, don't break before
+ if let prevAtom = prevAtom {
+ let prevIsTextLetter = isTextLetterAtom(prevAtom)
+ if prevIsTextLetter {
+ isBreakBefore = false
+ penaltyBefore = MTBreakPenalty.never
+ }
+ }
+
+ // Check next atom - if it's a letter in text mode, don't break after
+ if atomIndex + 1 < allAtoms.count {
+ let nextAtom = allAtoms[atomIndex + 1]
+ let nextIsTextLetter = isTextLetterAtom(nextAtom)
+ if nextIsTextLetter {
+ isBreakAfter = false
+ penaltyAfter = MTBreakPenalty.never
+ }
+ }
+ }
+
+ return MTBreakableElement(
+ content: .display(display),
+ width: display.width,
+ height: display.ascent + display.descent,
+ ascent: display.ascent,
+ descent: display.descent,
+ isBreakBefore: isBreakBefore,
+ isBreakAfter: isBreakAfter,
+ penaltyBefore: penaltyBefore,
+ penaltyAfter: penaltyAfter,
+ groupId: nil,
+ parentId: nil,
+ originalAtom: accent,
+ indexRange: accent.indexRange,
+ color: nil,
+ backgroundColor: nil,
+ indivisible: true
+ )
+ }
+
+ /// Helper to check if an atom is a letter in text mode (roman style)
+ private func isTextLetterAtom(_ atom: MTMathAtom) -> Bool {
+ // For accent atoms, check their inner content FIRST
+ // (accent atom's nucleus contains the accent mark, not the letter)
+ if let accent = atom as? MTAccent {
+ if let firstInner = accent.innerList?.atoms.first {
+ return isTextLetterAtom(firstInner)
+ }
+ return false
+ }
+ // For regular text atoms with roman style
+ if atom.fontStyle == .roman {
+ // Check if the nucleus contains only letters
+ let nucleus = atom.nucleus
+ if !nucleus.isEmpty {
+ return nucleus.allSatisfy { $0.isLetter }
+ }
+ }
+ return false
+ }
+
+ private func tokenizeUnderline(_ underline: MTUnderLine, prevAtom: MTMathAtom?, atomIndex: Int) -> MTBreakableElement? {
+ let typesetter = MTTypesetter(withFont: font, style: style, cramped: cramped, spaced: false)
+ guard let display = typesetter.makeUnderline(underline) else { return nil }
+
+ return MTBreakableElement(
+ content: .display(display),
+ width: display.width,
+ height: display.ascent + display.descent,
+ ascent: display.ascent,
+ descent: display.descent,
+ isBreakBefore: true,
+ isBreakAfter: true,
+ penaltyBefore: MTBreakPenalty.good,
+ penaltyAfter: MTBreakPenalty.good,
+ groupId: nil,
+ parentId: nil,
+ originalAtom: underline,
+ indexRange: underline.indexRange,
+ color: nil,
+ backgroundColor: nil,
+ indivisible: true
+ )
+ }
+
+ private func tokenizeOverline(_ overline: MTOverLine, prevAtom: MTMathAtom?, atomIndex: Int) -> MTBreakableElement? {
+ let typesetter = MTTypesetter(withFont: font, style: style, cramped: cramped, spaced: false)
+ guard let display = typesetter.makeOverline(overline) else { return nil }
+
+ return MTBreakableElement(
+ content: .display(display),
+ width: display.width,
+ height: display.ascent + display.descent,
+ ascent: display.ascent,
+ descent: display.descent,
+ isBreakBefore: true,
+ isBreakAfter: true,
+ penaltyBefore: MTBreakPenalty.good,
+ penaltyAfter: MTBreakPenalty.good,
+ groupId: nil,
+ parentId: nil,
+ originalAtom: overline,
+ indexRange: overline.indexRange,
+ color: nil,
+ backgroundColor: nil,
+ indivisible: true
+ )
+ }
+
+ private func tokenizeTable(_ table: MTMathTable, prevAtom: MTMathAtom?, atomIndex: Int) -> MTBreakableElement? {
+ let typesetter = MTTypesetter(withFont: font, style: style, cramped: cramped, spaced: false, maxWidth: maxWidth)
+ guard let display = typesetter.makeTable(table) else { return nil }
+
+ return MTBreakableElement(
+ content: .display(display),
+ width: display.width,
+ height: display.ascent + display.descent,
+ ascent: display.ascent,
+ descent: display.descent,
+ isBreakBefore: true,
+ isBreakAfter: true,
+ penaltyBefore: MTBreakPenalty.moderate,
+ penaltyAfter: MTBreakPenalty.moderate,
+ groupId: nil,
+ parentId: nil,
+ originalAtom: table,
+ indexRange: table.indexRange,
+ color: nil,
+ backgroundColor: nil,
+ indivisible: true
+ )
+ }
+
+ private func tokenizeInner(_ inner: MTInner, prevAtom: MTMathAtom?, atomIndex: Int) -> MTBreakableElement? {
+ let typesetter = MTTypesetter(withFont: font, style: style, cramped: cramped, spaced: false)
+ guard let display = typesetter.makeLeftRight(inner) else { return nil }
+
+ return MTBreakableElement(
+ content: .display(display),
+ width: display.width,
+ height: display.ascent + display.descent,
+ ascent: display.ascent,
+ descent: display.descent,
+ isBreakBefore: true,
+ isBreakAfter: true,
+ penaltyBefore: MTBreakPenalty.good,
+ penaltyAfter: MTBreakPenalty.good,
+ groupId: nil,
+ parentId: nil,
+ originalAtom: inner,
+ indexRange: inner.indexRange,
+ color: nil,
+ backgroundColor: nil,
+ indivisible: false
+ )
+ }
+
+ private func tokenizeSpace(_ atom: MTMathAtom, prevAtom: MTMathAtom?, atomIndex: Int) -> MTBreakableElement? {
+ // Space atoms typically don't participate in breaking
+ // They are rendered as-is
+ let width = widthCalculator.measureSpace(atom.type)
+
+ return MTBreakableElement(
+ content: .space(width),
+ width: width,
+ height: 0,
+ ascent: 0,
+ descent: 0,
+ isBreakBefore: false,
+ isBreakAfter: false,
+ penaltyBefore: MTBreakPenalty.never,
+ penaltyAfter: MTBreakPenalty.never,
+ groupId: nil,
+ parentId: nil,
+ originalAtom: atom,
+ indexRange: atom.indexRange,
+ color: nil,
+ backgroundColor: nil,
+ indivisible: true
+ )
+ }
+}
diff --git a/third-party/SwiftMath/Sources/MathRender/Tokenization/MTBreakableElement.swift b/third-party/SwiftMath/Sources/MathRender/Tokenization/MTBreakableElement.swift
new file mode 100644
index 0000000000..163a86871f
--- /dev/null
+++ b/third-party/SwiftMath/Sources/MathRender/Tokenization/MTBreakableElement.swift
@@ -0,0 +1,115 @@
+//
+// MTBreakableElement.swift
+// SwiftMath
+//
+// Created by Claude Code on 2025-12-16.
+// This software may be modified and distributed under the terms of the
+// MIT license. See the LICENSE file for details.
+//
+
+import Foundation
+import CoreGraphics
+
+// MARK: - MTElementContent
+
+/// Represents the content type of a breakable element
+enum MTElementContent {
+ /// Simple text content
+ case text(String)
+ /// Pre-rendered display (fraction, radical, etc.)
+ case display(MTDisplay)
+ /// Math operator with spacing
+ case `operator`(String, type: MTMathAtomType)
+ /// Explicit spacing
+ case space(CGFloat)
+ /// Superscript or subscript display
+ case script(MTDisplay, isSuper: Bool)
+}
+
+// MARK: - MTBreakableElement
+
+/// Represents a breakable element with pre-calculated width and break rules
+struct MTBreakableElement {
+ // MARK: Display properties
+
+ /// The content of this element
+ let content: MTElementContent
+
+ /// Pre-calculated width (cached)
+ let width: CGFloat
+
+ /// Height of the element
+ let height: CGFloat
+
+ /// Distance from baseline to top
+ let ascent: CGFloat
+
+ /// Distance from baseline to bottom
+ let descent: CGFloat
+
+ // MARK: Breaking rules
+
+ /// Can break BEFORE this element?
+ let isBreakBefore: Bool
+
+ /// Can break AFTER this element?
+ let isBreakAfter: Bool
+
+ /// Penalty for breaking before (0=good, 100=bad, 150=never)
+ let penaltyBefore: Int
+
+ /// Penalty for breaking after (0=good, 100=bad, 150=never)
+ let penaltyAfter: Int
+
+ // MARK: Relationship tracking
+
+ /// Elements with same groupId must stay together
+ let groupId: UUID?
+
+ /// Parent element ID (for scripts)
+ let parentId: UUID?
+
+ // MARK: Source tracking
+
+ /// Original atom this element was created from
+ let originalAtom: MTMathAtom
+
+ /// Index range in the original math list
+ let indexRange: NSRange
+
+ // MARK: Optional attributes
+
+ /// Text color for this element
+ let color: MTColor?
+
+ /// Background color for this element
+ let backgroundColor: MTColor?
+
+ // MARK: Atomicity flag
+
+ /// If true, NEVER break this element internally
+ let indivisible: Bool
+}
+
+// MARK: - Penalty Constants
+
+/// Penalty values for line breaking decisions
+enum MTBreakPenalty {
+ /// Best break points (operators, relations)
+ static let best = 0
+
+ /// Good break points (ordinary atoms, after scripts)
+ static let good = 10
+
+ /// Moderate penalty (before fractions, radicals)
+ static let moderate = 15
+
+ /// Acceptable break points
+ static let acceptable = 50
+
+ /// Bad break points (avoid if possible)
+ static let bad = 100
+
+ /// Never break here (grouped elements)
+ static let never = 150
+}
diff --git a/third-party/SwiftMath/Sources/MathRender/Tokenization/MTDisplayGenerator.swift b/third-party/SwiftMath/Sources/MathRender/Tokenization/MTDisplayGenerator.swift
new file mode 100644
index 0000000000..b9faf4f267
--- /dev/null
+++ b/third-party/SwiftMath/Sources/MathRender/Tokenization/MTDisplayGenerator.swift
@@ -0,0 +1,389 @@
+//
+// MTDisplayGenerator.swift
+// SwiftMath
+//
+// Created by Claude Code on 2025-12-16.
+// This software may be modified and distributed under the terms of the
+// MIT license. See the LICENSE file for details.
+//
+
+import Foundation
+import CoreGraphics
+import CoreText
+
+/// Generates MTDisplay objects from fitted lines of breakable elements
+class MTDisplayGenerator {
+
+ // MARK: - Properties
+
+ let font: MTFont
+ let style: MTLineStyle
+ let widthCalculator: MTElementWidthCalculator
+
+ // MARK: - Initialization
+
+ init(font: MTFont, style: MTLineStyle) {
+ self.font = font
+ self.style = style
+ self.widthCalculator = MTElementWidthCalculator(font: font, style: style)
+ }
+
+ // MARK: - Display Generation
+
+ /// Generate displays from fitted lines
+ func generateDisplays(from lines: [[MTBreakableElement]], startPosition: CGPoint) -> [MTDisplay] {
+ var allDisplays: [MTDisplay] = []
+ var currentY = startPosition.y
+
+ // Minimum spacing between lines (20% of font size for breathing room)
+ let minimumLineSpacing = font.fontSize * 0.2
+
+ for (index, line) in lines.enumerated() {
+ let (lineDisplays, currentLineMetrics) = generateLine(line, at: CGPoint(x: startPosition.x, y: currentY))
+ allDisplays.append(contentsOf: lineDisplays)
+
+ // Calculate spacing for next line based on actual content heights
+ if index < lines.count - 1 {
+ let nextLine = lines[index + 1]
+ let nextLineAscent = nextLine.map { $0.ascent }.max() ?? 0
+
+ // Space needed = current line's descent + minimum spacing + next line's ascent
+ let spaceNeeded = currentLineMetrics.descent + minimumLineSpacing + nextLineAscent
+
+ // Ensure minimum spacing of 1.2x font size for readability
+ let minSpacing = font.fontSize * 1.2
+ currentY -= max(spaceNeeded, minSpacing)
+ }
+ }
+
+ return allDisplays
+ }
+
+ /// Line metrics for spacing calculation
+ struct LineMetrics {
+ let ascent: CGFloat
+ let descent: CGFloat
+ var height: CGFloat { ascent + descent }
+ }
+
+ /// Generate displays for a single line
+ private func generateLine(_ elements: [MTBreakableElement], at position: CGPoint) -> ([MTDisplay], LineMetrics) {
+ var displays: [MTDisplay] = []
+ var xOffset: CGFloat = 0
+
+ // Calculate line metrics
+ let lineAscent = elements.map { $0.ascent }.max() ?? 0
+ let lineDescent = elements.map { $0.descent }.max() ?? 0
+
+ // Baseline y position
+ let baseline = position.y
+
+ var i = 0
+ while i < elements.count {
+ let element = elements[i]
+
+ // Check if this is part of a group (base + scripts)
+ if let groupId = element.groupId {
+ // Collect all elements in this group
+ var groupElements: [MTBreakableElement] = []
+ var j = i
+ while j < elements.count && elements[j].groupId == groupId {
+ groupElements.append(elements[j])
+ j += 1
+ }
+
+ // Render the group
+ let groupAdvance = renderGroup(groupElements, at: CGPoint(x: position.x + xOffset, y: baseline), displays: &displays)
+ xOffset += groupAdvance
+ i = j
+ } else {
+ // Regular element (not part of a group)
+
+ // CRITICAL: For operators, spacing should be split evenly before and after
+ // The element.width includes both spacing, but we need to position the operator
+ // with half spacing before it
+ var spacingBefore: CGFloat = 0
+ if case .operator(let op, _) = element.content {
+ // Get the actual text width vs element width to calculate spacing
+ let textWidth = widthCalculator.measureText(op)
+ let totalSpacing = element.width - textWidth
+ spacingBefore = totalSpacing / 2
+ }
+
+ let elementPosition = CGPoint(x: position.x + xOffset + spacingBefore, y: baseline)
+
+ switch element.content {
+ case .text(let text):
+ let display = createTextDisplay(text, at: elementPosition, element: element)
+ displays.append(display)
+
+ case .display(let preRenderedDisplay):
+ // Use pre-rendered display (fraction, radical, etc.)
+ let mutableDisplay = preRenderedDisplay
+ mutableDisplay.position = elementPosition
+ displays.append(mutableDisplay)
+
+ case .operator(let op, _):
+ let display = createTextDisplay(op, at: elementPosition, element: element)
+ displays.append(display)
+
+ case .script:
+ // Standalone script (shouldn't happen, but handle gracefully)
+ break
+
+ case .space:
+ // No display for space, just advance position
+ break
+ }
+
+ xOffset += element.width
+ i += 1
+ }
+ }
+
+ return (displays, LineMetrics(ascent: lineAscent, descent: lineDescent))
+ }
+
+ /// Render a group of elements (base + scripts) and return the horizontal advance
+ private func renderGroup(_ groupElements: [MTBreakableElement], at position: CGPoint, displays: inout [MTDisplay]) -> CGFloat {
+ var baseWidth: CGFloat = 0
+ var superscriptWidth: CGFloat = 0
+ var subscriptWidth: CGFloat = 0
+ var baseXOffset: CGFloat = 0
+
+ // Check if this group has any scripts
+ let hasScripts = groupElements.contains { element in
+ if case .script = element.content {
+ return true
+ }
+ return false
+ }
+
+ // Track the start index of base displays for dimension adjustment
+ let baseDisplayStartIndex = displays.count
+
+ // First pass: render base elements and collect script widths
+ for element in groupElements {
+ switch element.content {
+ case .script:
+ // Skip scripts in first pass
+ break
+ default:
+ // Render base element
+ let basePosition = CGPoint(x: position.x + baseXOffset, y: position.y)
+
+ switch element.content {
+ case .text(let text):
+ let display = createTextDisplay(text, at: basePosition, element: element, hasScript: hasScripts)
+ displays.append(display)
+ case .display(let preRenderedDisplay):
+ let mutableDisplay = preRenderedDisplay
+ mutableDisplay.position = basePosition
+ displays.append(mutableDisplay)
+ case .operator(let op, _):
+ let display = createTextDisplay(op, at: basePosition, element: element, hasScript: hasScripts)
+ displays.append(display)
+ default:
+ break
+ }
+
+ baseWidth += element.width
+ baseXOffset += element.width
+ }
+ }
+
+ // Second pass: collect script information for joint positioning
+ var superscriptDisplay: MTDisplay? = nil
+ var subscriptDisplay: MTDisplay? = nil
+ var hasBothScripts = false
+
+ for element in groupElements {
+ if case .script(let scriptDisplay, let isSuper) = element.content {
+ if isSuper {
+ superscriptDisplay = scriptDisplay
+ } else {
+ subscriptDisplay = scriptDisplay
+ }
+ }
+ }
+
+ hasBothScripts = superscriptDisplay != nil && subscriptDisplay != nil
+
+ // Third pass: render scripts with proper positioning
+ var superScriptShiftUp: CGFloat = 0
+ var subscriptShiftDown: CGFloat = 0
+
+ // Check if base is a glyph (not CTLineDisplay) for special positioning
+ // For glyphs (like large operators), position scripts relative to glyph edges
+ var isGlyphBase = false
+ for disp in displays[baseDisplayStartIndex.. 0 {
+ // Superscript is lower than the max allowed by the font with a subscript
+ superScriptShiftUp += superscriptBottomDelta
+ subscriptShiftDown -= superscriptBottomDelta
+ }
+ }
+ }
+
+ // Calculate italic correction (delta) for superscript positioning
+ // Superscripts are positioned at baseWidth + delta, subscripts at baseWidth
+ var delta: CGFloat = 0
+ if superscriptDisplay != nil {
+ // Get italic correction from the base display if it's a glyph
+ for disp in displays[baseDisplayStartIndex.. baseDisplayStartIndex {
+ // Calculate the full extent of the group including scripts
+ var maxAscent: CGFloat = 0
+ var maxDescent: CGFloat = 0
+
+ for i in baseDisplayStartIndex.. MTDisplay {
+ let attrString = NSMutableAttributedString(string: text)
+ attrString.addAttribute(
+ NSAttributedString.Key(kCTFontAttributeName as String),
+ value: font.ctFont as Any,
+ range: NSMakeRange(0, attrString.length)
+ )
+
+ // If the atom was fused (multiple ordinary chars combined), use fusedAtoms
+ // Otherwise, use the original atom
+ let atoms: [MTMathAtom]
+ if !element.originalAtom.fusedAtoms.isEmpty {
+ atoms = element.originalAtom.fusedAtoms
+ } else {
+ atoms = [element.originalAtom]
+ }
+
+ let display = MTCTLineDisplay(
+ withString: attrString,
+ position: position,
+ range: element.indexRange,
+ font: font,
+ atoms: atoms
+ )
+
+ // Mark if this base element has associated scripts
+ display.hasScript = hasScript
+
+ return display
+ }
+}
diff --git a/third-party/SwiftMath/Sources/MathRender/Tokenization/MTDisplayPreRenderer.swift b/third-party/SwiftMath/Sources/MathRender/Tokenization/MTDisplayPreRenderer.swift
new file mode 100644
index 0000000000..d6af072a60
--- /dev/null
+++ b/third-party/SwiftMath/Sources/MathRender/Tokenization/MTDisplayPreRenderer.swift
@@ -0,0 +1,88 @@
+//
+// MTDisplayPreRenderer.swift
+// SwiftMath
+//
+// Created by Claude Code on 2025-12-16.
+// This software may be modified and distributed under the terms of the
+// MIT license. See the LICENSE file for details.
+//
+
+import Foundation
+import CoreGraphics
+
+/// Pre-renders complex atoms (fractions, radicals, etc.) as MTDisplay objects during tokenization
+class MTDisplayPreRenderer {
+
+ // MARK: - Properties
+
+ let font: MTFont
+ let style: MTLineStyle
+ let cramped: Bool
+
+ // MARK: - Initialization
+
+ init(font: MTFont, style: MTLineStyle, cramped: Bool) {
+ self.font = font
+ self.style = style
+ self.cramped = cramped
+ }
+
+ // MARK: - Script Rendering
+
+ /// Render a script (superscript or subscript) as a display
+ func renderScript(_ mathList: MTMathList, isSuper: Bool) -> MTDisplay? {
+ let scriptStyle = getScriptStyle()
+ let scriptCramped = isSuper ? cramped : true // Subscripts are always cramped
+
+ // Scale the font for the script style
+ let scriptFontSize = MTTypesetter.getStyleSize(scriptStyle, font: font)
+ let scriptFont = font.copy(withSize: scriptFontSize)
+
+ guard let display = MTTypesetter.createLineForMathList(
+ mathList,
+ font: scriptFont,
+ style: scriptStyle,
+ cramped: scriptCramped,
+ spaced: false
+ ) else {
+ return nil
+ }
+
+ // If the result is a MTMathListDisplay with a single subdisplay, unwrap it
+ // This matches the behavior of the legacy typesetter
+ if display.subDisplays.count == 1 {
+ return display.subDisplays[0]
+ }
+
+ return display
+ }
+
+ /// Get the appropriate style for scripts
+ private func getScriptStyle() -> MTLineStyle {
+ switch style {
+ case .display, .text:
+ return .script
+ case .script, .scriptOfScript:
+ return .scriptOfScript
+ }
+ }
+
+ // MARK: - Helper Methods
+
+ /// Pre-render a simple math list without width constraints
+ /// Used for rendering content inside fractions, radicals, etc.
+ func renderMathList(_ mathList: MTMathList?, style renderStyle: MTLineStyle? = nil, cramped renderCramped: Bool? = nil) -> MTDisplay? {
+ guard let mathList = mathList else { return nil }
+
+ let actualStyle = renderStyle ?? style
+ let actualCramped = renderCramped ?? cramped
+
+ return MTTypesetter.createLineForMathList(
+ mathList,
+ font: font,
+ style: actualStyle,
+ cramped: actualCramped,
+ spaced: false
+ )
+ }
+}
diff --git a/third-party/SwiftMath/Sources/MathRender/Tokenization/MTElementWidthCalculator.swift b/third-party/SwiftMath/Sources/MathRender/Tokenization/MTElementWidthCalculator.swift
new file mode 100644
index 0000000000..258eb768be
--- /dev/null
+++ b/third-party/SwiftMath/Sources/MathRender/Tokenization/MTElementWidthCalculator.swift
@@ -0,0 +1,165 @@
+//
+// MTElementWidthCalculator.swift
+// SwiftMath
+//
+// Created by Claude Code on 2025-12-16.
+// This software may be modified and distributed under the terms of the
+// MIT license. See the LICENSE file for details.
+//
+
+import Foundation
+import CoreText
+import CoreGraphics
+
+/// Calculates widths for breakable elements with appropriate spacing
+class MTElementWidthCalculator {
+
+ // MARK: - Properties
+
+ let font: MTFont
+ let style: MTLineStyle
+
+ // MARK: - Initialization
+
+ init(font: MTFont, style: MTLineStyle) {
+ self.font = font
+ self.style = style
+ }
+
+ // MARK: - Text Width Measurement
+
+ /// Measure width of simple text
+ func measureText(_ text: String) -> CGFloat {
+ guard !text.isEmpty else { return 0 }
+
+ let attrString = NSAttributedString(string: text, attributes: [
+ kCTFontAttributeName as NSAttributedString.Key: font.ctFont as Any
+ ])
+ let line = CTLineCreateWithAttributedString(attrString as CFAttributedString)
+ return CGFloat(CTLineGetTypographicBounds(line, nil, nil, nil))
+ }
+
+ // MARK: - Operator Width Measurement
+
+ /// Measure width of operator with appropriate spacing
+ func measureOperator(_ op: String, type: MTMathAtomType) -> CGFloat {
+ let baseWidth = measureText(op)
+ let spacing = getOperatorSpacing(type)
+ return baseWidth + spacing
+ }
+
+ /// Get spacing for an operator (both sides)
+ private func getOperatorSpacing(_ type: MTMathAtomType) -> CGFloat {
+ guard let mathTable = font.mathTable else { return 0 }
+ let muUnit = mathTable.muUnit
+
+ switch type {
+ case .binaryOperator:
+ // Binary operators: 4mu on each side = 8mu total
+ return 2 * muUnit * 4
+
+ case .relation:
+ // Relations: 5mu on each side = 10mu total
+ return 2 * muUnit * 5
+
+ case .largeOperator:
+ // Large operators in inline mode: 1mu on each side
+ if style == .display || style == .text {
+ return 0 // In display mode, handled by MTLargeOpLimitsDisplay
+ }
+ return 2 * muUnit * 1
+
+ default:
+ return 0
+ }
+ }
+
+ // MARK: - Display Width Measurement
+
+ /// Measure width of a pre-rendered display
+ func measureDisplay(_ display: MTDisplay) -> CGFloat {
+ return display.width
+ }
+
+ // MARK: - Space Width Measurement
+
+ /// Get width of explicit spacing command
+ func measureSpace(_ spaceType: MTMathAtomType) -> CGFloat {
+ guard let mathTable = font.mathTable else { return 0 }
+ let muUnit = mathTable.muUnit
+
+ // Note: These are the explicit spacing commands in LaTeX
+ // \, = thin space (3mu)
+ // \: = medium space (4mu)
+ // \; = thick space (5mu)
+ // \quad = 1em
+ // \qquad = 2em
+
+ switch spaceType {
+ case .space:
+ // Default space - context dependent
+ // For now, use thin space
+ return muUnit * 3
+ default:
+ return 0
+ }
+ }
+
+ /// Measure explicit space value
+ func measureExplicitSpace(_ width: CGFloat) -> CGFloat {
+ return width
+ }
+
+ // MARK: - Inter-element Spacing
+
+ /// Get inter-element spacing between two atom types
+ func getInterElementSpacing(left: MTMathAtomType, right: MTMathAtomType) -> CGFloat {
+ let leftIndex = getInterElementSpaceArrayIndexForType(left, row: true)
+ let rightIndex = getInterElementSpaceArrayIndexForType(right, row: false)
+ let spaceArray = getInterElementSpaces()[Int(leftIndex)]
+ let spaceType = spaceArray[Int(rightIndex)]
+
+ guard spaceType != .invalid else {
+ // Should not happen in well-formed math
+ return 0
+ }
+
+ let spaceMultiplier = getSpacingInMu(spaceType)
+ if spaceMultiplier > 0, let mathTable = font.mathTable {
+ return CGFloat(spaceMultiplier) * mathTable.muUnit
+ }
+ return 0
+ }
+
+ /// Get spacing multiplier in mu units
+ private func getSpacingInMu(_ spaceType: InterElementSpaceType) -> Int {
+ switch style {
+ case .display, .text:
+ switch spaceType {
+ case .none, .invalid:
+ return 0
+ case .thin:
+ return 3
+ case .nsThin, .nsMedium, .nsThick:
+ // ns = non-script, same as regular in display/text mode
+ switch spaceType {
+ case .nsThin: return 3
+ case .nsMedium: return 4
+ case .nsThick: return 5
+ default: return 0
+ }
+ }
+
+ case .script, .scriptOfScript:
+ switch spaceType {
+ case .none, .invalid:
+ return 0
+ case .thin:
+ return 3
+ case .nsThin, .nsMedium, .nsThick:
+ // In script mode, ns types don't add space
+ return 0
+ }
+ }
+ }
+}
diff --git a/third-party/SwiftMath/Sources/MathRender/Tokenization/MTLineFitter.swift b/third-party/SwiftMath/Sources/MathRender/Tokenization/MTLineFitter.swift
new file mode 100644
index 0000000000..5afdc36a2a
--- /dev/null
+++ b/third-party/SwiftMath/Sources/MathRender/Tokenization/MTLineFitter.swift
@@ -0,0 +1,266 @@
+//
+// MTLineFitter.swift
+// SwiftMath
+//
+// Created by Claude Code on 2025-12-16.
+// This software may be modified and distributed under the terms of the
+// MIT license. See the LICENSE file for details.
+//
+
+import Foundation
+import CoreGraphics
+
+/// Fits breakable elements into lines respecting width constraints and break rules
+class MTLineFitter {
+
+ // MARK: - Properties
+
+ let maxWidth: CGFloat
+ let margin: CGFloat
+
+ // MARK: - Initialization
+
+ init(maxWidth: CGFloat, margin: CGFloat = 0) {
+ self.maxWidth = maxWidth
+ self.margin = margin
+ }
+
+ // MARK: - Line Fitting
+
+ /// Fit elements into lines using greedy algorithm with backtracking
+ func fitLines(_ elements: [MTBreakableElement]) -> [[MTBreakableElement]] {
+ guard !elements.isEmpty else { return [] }
+ guard maxWidth > 0 else { return [elements] } // No width constraint
+
+ let debugPunctuation = !"".isEmpty // Enable to debug line breaking issues
+
+ if debugPunctuation {
+ print("\n=== MTLineFitter: fitting \(elements.count) elements, maxWidth=\(maxWidth) ===")
+ for (idx, elem) in elements.enumerated() {
+ if case .text(let t) = elem.content {
+ print("[\(idx)] '\(t)' breakBefore=\(elem.isBreakBefore) breakAfter=\(elem.isBreakAfter) width=\(elem.width)")
+ }
+ }
+ }
+
+ var lines: [[MTBreakableElement]] = [[]]
+ var currentWidth: CGFloat = 0
+ var i = 0
+
+ while i < elements.count {
+ let element = elements[i]
+
+ if debugPunctuation, case .text(let t) = element.content {
+ print("\n Processing element[\(i)]: '\(t)' breakBefore=\(element.isBreakBefore)")
+ }
+
+ // Handle grouped elements (base + scripts)
+ if let groupId = element.groupId {
+ let (groupElements, nextIndex) = collectGroup(elements, startIndex: i, groupId: groupId)
+
+ // Calculate group width correctly for scripts
+ // Scripts overlap vertically, so width = max(script widths), not sum
+ let groupWidth = calculateGroupWidth(groupElements)
+
+ // Check if group fits on current line
+ if !lines.last!.isEmpty && currentWidth + groupWidth > maxWidth - margin {
+ // Group doesn't fit - check if first element of group can start a new line
+ if groupElements.first?.isBreakBefore ?? true {
+ // Can start new line
+ lines.append([])
+ currentWidth = 0
+ } else {
+ // Cannot start new line - keep with previous line (allow overflow)
+ // This handles cases like punctuation after base+script groups
+ }
+ }
+
+ // Add entire group to current line
+ lines[lines.count - 1].append(contentsOf: groupElements)
+ currentWidth += groupWidth
+ i = nextIndex
+ continue
+ }
+
+ // Check if element fits on current line
+ if !lines.last!.isEmpty && currentWidth + element.width > maxWidth - margin {
+ if debugPunctuation, case .text = element.content {
+ print(" Doesn't fit (width=\(currentWidth) + \(element.width) > \(maxWidth)), current line has \(lines.last!.count) elements")
+ }
+ // Element doesn't fit - find best break point in current line
+ if let breakIndex = findBestBreak(in: lines[lines.count - 1]) {
+ if debugPunctuation {
+ print(" Found break at index \(breakIndex) out of \(lines.last!.count) elements")
+ if breakIndex < lines.last!.count {
+ if case .text(let t) = lines.last![breakIndex].content {
+ print(" Break at element: '\(t)'")
+ }
+ }
+ }
+ // Found a break point - move elements from breakIndex onward to next line
+ let moveElements = Array(lines[lines.count - 1][breakIndex...])
+ let oldLine = Array(lines[lines.count - 1][.. Adding '\(t)' to new line (part of unbreakable sequence)")
+ }
+ lines[lines.count - 1].append(element)
+ currentWidth += element.width
+ i += 1
+ continue
+ } else {
+ if debugPunctuation, case .text(let t) = element.content {
+ print(" -> Adding '\(t)' to new line (can start line)")
+ }
+ }
+ // Current element can start a line, will be added to new line below
+ } else {
+ // Should not happen if findBestBreak is correct, but handle gracefully
+ // Keep elements on current line (allow overflow)
+ lines[lines.count - 1].append(contentsOf: moveElements)
+ currentWidth += moveElements.reduce(0) { $0 + $1.width }
+ }
+ } else {
+ // No good break point found
+ // Check if current element can start a new line
+ if element.isBreakBefore {
+ // Element can start new line
+ lines.append([])
+ currentWidth = 0
+ } else {
+ // Element cannot start a new line (e.g., closing punctuation)
+ // Keep it on current line even if it causes overflow
+ // This respects punctuation rules over width constraints
+ lines[lines.count - 1].append(element)
+ currentWidth += element.width
+ i += 1
+ continue
+ }
+ }
+ }
+
+ // Add element to current line (may overflow if indivisible and too wide)
+ lines[lines.count - 1].append(element)
+ currentWidth += element.width
+ i += 1
+ }
+
+ let finalLines = lines.filter { !$0.isEmpty }
+
+ if debugPunctuation {
+ print("\n=== Final lines: ===")
+ for (lineIdx, line) in finalLines.enumerated() {
+ print("Line \(lineIdx):")
+ for elem in line {
+ if case .text(let t) = elem.content {
+ print(" '\(t)'", terminator: "")
+ }
+ }
+ print()
+ }
+ }
+
+ return finalLines
+ }
+
+ // MARK: - Helper Methods
+
+ /// Calculate the correct width for a group of elements (e.g., base + scripts)
+ /// Scripts overlap vertically, so the group width is not the sum of all widths
+ private func calculateGroupWidth(_ groupElements: [MTBreakableElement]) -> CGFloat {
+ // For grouped elements (base + scripts), just sum all widths
+ // The display generator will handle the actual positioning and overlap
+ // This is just for line fitting purposes
+ return groupElements.reduce(0) { $0 + $1.width }
+ }
+
+ /// Collect all elements that share the same groupId
+ private func collectGroup(_ elements: [MTBreakableElement], startIndex: Int, groupId: UUID) -> ([MTBreakableElement], Int) {
+ var groupElements: [MTBreakableElement] = []
+ var index = startIndex
+
+ while index < elements.count && elements[index].groupId == groupId {
+ groupElements.append(elements[index])
+ index += 1
+ }
+
+ return (groupElements, index)
+ }
+
+ /// Find the best break point in a line
+ /// Returns the index where the break should occur (elements from this index move to next line)
+ private func findBestBreak(in line: [MTBreakableElement]) -> Int? {
+ var bestIndex: Int? = nil
+ var lowestPenalty = Int.max
+
+ let debugBreak = !"".isEmpty // Enable to debug break point selection
+
+ // Scan from right to left to prefer breaking later in the line
+ // Note: Skip the last element (idx == line.count - 1) because breaking after it
+ // would move 0 elements to the next line, which is pointless
+ for (idx, element) in line.enumerated().reversed() {
+ // Skip the last element - we need to move at least 1 element to the next line
+ if idx >= line.count - 1 {
+ continue
+ }
+
+ // Can we break after this element?
+ let canBreakAfter = element.isBreakAfter
+ let penaltyAfter = element.penaltyAfter
+
+ // Check if next element (which would move to new line) allows breaking before it
+ let canBreakBeforeNext = line[idx + 1].isBreakBefore
+ let penaltyBeforeNext = line[idx + 1].penaltyBefore
+
+ // We can break here only if BOTH:
+ // 1. Current element allows breaking after it
+ // 2. Next element allows breaking before it
+ if canBreakAfter && canBreakBeforeNext {
+ let totalPenalty = max(penaltyAfter, penaltyBeforeNext)
+ if totalPenalty < lowestPenalty {
+ if debugBreak && idx < line.count - 1 {
+ let currText = if case .text(let t) = element.content { t } else { "?" }
+ let nextText = if case .text(let t) = line[idx + 1].content { t } else { "?" }
+ print(" Considering break: '\(currText)' | '\(nextText)' at idx=\(idx), penalty=\(totalPenalty)")
+ }
+ bestIndex = idx + 1
+ lowestPenalty = totalPenalty
+ }
+ }
+ }
+
+ if debugBreak {
+ print(" Best break: index=\(bestIndex ?? -1), penalty=\(lowestPenalty)")
+ }
+
+ // Only return if we found an acceptable break point
+ if let index = bestIndex, lowestPenalty <= MTBreakPenalty.bad {
+ return index
+ }
+
+ return nil
+ }
+
+ /// Check if a line width exceeds the maximum
+ private func exceedsMaxWidth(_ width: CGFloat) -> Bool {
+ return width > maxWidth - margin
+ }
+}