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: #"]*?>"#) +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("") && !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} +``` + +![Quadratic Formula](img/quadratic-light.png#gh-light-mode-only) +![Quadratic Formula](img/quadratic-dark.png#gh-dark-mode-only) + +```LaTeX +f(x) = \int\limits_{-\infty}^\infty\!\hat f(\xi)\,e^{2 \pi i \xi x}\,\mathrm{d}\xi +``` + +![Calculus](img/calculus-light.png#gh-light-mode-only) +![Calculus](img/calculus-dark.png#gh-dark-mode-only) + +```LaTeX +\frac{1}{n}\sum_{i=1}^{n}x_i \geq \sqrt[n]{\prod_{i=1}^{n}x_i} +``` + +![AM-GM](img/amgm-light.png#gh-light-mode-only) +![AM-GM](img/amgm-dark.png#gh-dark-mode-only) + +```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} } } } +``` + +![Ramanujan Identity](img/ramanujan-light.png#gh-light-mode-only) +![Ramanujan Identity](img/ramanujan-dark.png#gh-dark-mode-only) + +More examples are included in [EXAMPLES](EXAMPLES.md) + +## Fonts +Here are previews of the included fonts: + +![](img/FontsPreview.png#gh-dark-mode-only) +![](img/FontsPreviewLight.png#gh-light-mode-only) + +## 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 + } +}