WinterGram/submodules/MediaPickerUI/Sources/MediaPickerPhotoToolbarView.swift
Ilya Laktyushin 622f9d970a Various fixes
2026-05-31 17:35:32 +02:00

1187 lines
47 KiB
Swift

import Foundation
import UIKit
import Display
import LegacyComponents
import AccountContext
import TelegramPresentationData
import ComponentFlow
import GlassBackgroundComponent
import GlassBarButtonComponent
import PlainButtonComponent
import BundleIconComponent
private let toolbarButtonSide: CGFloat = 44.0
private let toolbarSideButtonSide: CGFloat = 44.0
private let centerButtonSpacing: CGFloat = 10.0
private let toolbarTabOrder: [TGPhotoEditorTab] = [
.cropTab,
.stickerTab,
.paintTab,
.eraserTab,
.textTab,
.toolsTab,
.rotateTab,
.qualityTab,
.timerTab,
.mirrorTab,
.aspectRatioTab,
.tintTab,
.blurTab,
.curvesTab
]
private let dontHighlightOnSelectionTabs = Set<UInt>([
TGPhotoEditorTab.rotateTab.rawValue,
TGPhotoEditorTab.stickerTab.rawValue,
TGPhotoEditorTab.textTab.rawValue,
TGPhotoEditorTab.qualityTab.rawValue,
TGPhotoEditorTab.timerTab.rawValue,
TGPhotoEditorTab.mirrorTab.rawValue,
TGPhotoEditorTab.aspectRatioTab.rawValue
])
private final class MediaPickerPhotoToolbarImageCache {
private var images: [String: UIImage] = [:]
func qualityIcon(isPhoto: Bool, highQuality: Bool, preset: Int, color: UIColor) -> UIImage? {
let key = "quality-\(isPhoto)-\(highQuality)-\(preset)-\(self.colorKey(color))"
if let image = self.images[key] {
return image
}
let image = generateQualityIcon(isPhoto: isPhoto, highQuality: highQuality, preset: preset, color: color)
self.images[key] = image
return image
}
func timerIcon(value: Int, color: UIColor) -> UIImage? {
let key = "timer-\(value)-\(self.colorKey(color))"
if let image = self.images[key] {
return image
}
let image = generateTimerIcon(value: value, color: color)
self.images[key] = image
return image
}
private func colorKey(_ color: UIColor) -> String {
var red: CGFloat = 0.0
var green: CGFloat = 0.0
var blue: CGFloat = 0.0
var alpha: CGFloat = 0.0
if color.getRed(&red, green: &green, blue: &blue, alpha: &alpha) {
return "\(Int(red * 255.0))-\(Int(green * 255.0))-\(Int(blue * 255.0))-\(Int(alpha * 255.0))"
}
var white: CGFloat = 0.0
if color.getWhite(&white, alpha: &alpha) {
return "\(Int(white * 255.0))-\(Int(alpha * 255.0))"
}
return color.description
}
}
private func generateQualityIcon(isPhoto: Bool, highQuality: Bool, preset: Int, color: UIColor) -> UIImage? {
let label: String
if isPhoto {
label = highQuality ? "HD" : "SD"
} else {
switch preset {
case 1:
label = "240"
case 2:
label = "360"
case 3:
label = "480"
case 4:
label = "720"
case 5:
label = "HD"
default:
label = "480"
}
}
let size = CGSize(width: isPhoto ? 24.0 : 28.0, height: 24.0)
let lineWidth = 2.0 - UIScreenPixel
let rect = CGRect(origin: .zero, size: size).insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0)
UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
guard let context = UIGraphicsGetCurrentContext() else {
UIGraphicsEndImageContext()
return nil
}
context.setStrokeColor(color.cgColor)
context.setLineWidth(lineWidth)
context.addPath(UIBezierPath(roundedRect: rect, cornerRadius: 7.0).cgPath)
context.strokePath()
let font = Font.with(size: 11.0, design: .round, weight: .bold)
let attributes: [NSAttributedString.Key: Any] = [
.font: font,
.foregroundColor: color,
.kern: -0.7
]
let textSize = label.size(withAttributes: attributes)
label.draw(
in: CGRect(
x: floorToScreenPixels((size.width - textSize.width) / 2.0) + UIScreenPixel,
y: 5.0,
width: textSize.width,
height: textSize.height
),
withAttributes: attributes
)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
private func generateTimerIcon(value: Int, color: UIColor) -> UIImage? {
let size = CGSize(width: 24.0, height: 24.0)
let lineWidth = 2.0 - UIScreenPixel
UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
guard let context = UIGraphicsGetCurrentContext() else {
UIGraphicsEndImageContext()
return nil
}
context.setStrokeColor(color.cgColor)
context.setLineWidth(lineWidth)
context.setLineCap(.round)
context.setLineJoin(.round)
let bodyRect = CGRect(x: 4.0, y: 5.0, width: 16.0, height: 16.0).insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0)
context.strokeEllipse(in: bodyRect)
context.move(to: CGPoint(x: 9.0, y: 2.5))
context.addLine(to: CGPoint(x: 15.0, y: 2.5))
context.strokePath()
context.move(to: CGPoint(x: 12.0, y: 5.0))
context.addLine(to: CGPoint(x: 12.0, y: 8.0))
context.strokePath()
if value == 0 {
context.move(to: CGPoint(x: 12.0, y: 13.0))
context.addLine(to: CGPoint(x: 12.0, y: 9.0))
context.move(to: CGPoint(x: 12.0, y: 13.0))
context.addLine(to: CGPoint(x: 15.0, y: 13.0))
context.strokePath()
} else {
let label = "\(value)"
let font = Font.with(size: 10.0, design: .round, weight: .semibold)
let attributes: [NSAttributedString.Key: Any] = [
.font: font,
.foregroundColor: color
]
let textSize = label.size(withAttributes: attributes)
label.draw(
in: CGRect(
x: floorToScreenPixels((size.width - textSize.width) / 2.0),
y: 9.0,
width: textSize.width,
height: textSize.height
),
withAttributes: attributes
)
}
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
private final class MediaPickerPhotoToolbarComponent: Component {
let context: AccountContext
let solidBackground: Bool
let backButtonType: TGPhotoEditorBackButton
let doneButtonType: TGPhotoEditorDoneButton
let currentTabs: TGPhotoEditorTab
let activeTab: TGPhotoEditorTab
let highlightedTabs: TGPhotoEditorTab
let disabledTabs: TGPhotoEditorTab
let qualityIsPhoto: Bool
let qualityHighQuality: Bool
let qualityPreset: Int
let timerValue: Int
let hasSendStarsButton: Bool
let sendPaidMessageStars: Int64
let editButtonsHidden: Bool
let editButtonsEnabled: Bool
let centerButtonsHidden: Bool
let allButtonsHidden: Bool
let cancelDoneButtonsHidden: Bool
let doneButtonEnabled: Bool
let interfaceOrientation: UIInterfaceOrientation
let bottomInset: CGFloat
let infoString: String?
let cancelPressed: (() -> Void)?
let donePressed: (() -> Void)?
let doneLongPressed: ((Any?) -> Void)?
let tabPressed: ((TGPhotoEditorTab) -> Void)?
init(
context: AccountContext,
solidBackground: Bool,
backButtonType: TGPhotoEditorBackButton,
doneButtonType: TGPhotoEditorDoneButton,
currentTabs: TGPhotoEditorTab,
activeTab: TGPhotoEditorTab,
highlightedTabs: TGPhotoEditorTab,
disabledTabs: TGPhotoEditorTab,
qualityIsPhoto: Bool,
qualityHighQuality: Bool,
qualityPreset: Int,
timerValue: Int,
hasSendStarsButton: Bool,
sendPaidMessageStars: Int64,
editButtonsHidden: Bool,
editButtonsEnabled: Bool,
centerButtonsHidden: Bool,
allButtonsHidden: Bool,
cancelDoneButtonsHidden: Bool,
doneButtonEnabled: Bool,
interfaceOrientation: UIInterfaceOrientation,
bottomInset: CGFloat,
infoString: String?,
cancelPressed: (() -> Void)?,
donePressed: (() -> Void)?,
doneLongPressed: ((Any?) -> Void)?,
tabPressed: ((TGPhotoEditorTab) -> Void)?
) {
self.context = context
self.solidBackground = solidBackground
self.backButtonType = backButtonType
self.doneButtonType = doneButtonType
self.currentTabs = currentTabs
self.activeTab = activeTab
self.highlightedTabs = highlightedTabs
self.disabledTabs = disabledTabs
self.qualityIsPhoto = qualityIsPhoto
self.qualityHighQuality = qualityHighQuality
self.qualityPreset = qualityPreset
self.timerValue = timerValue
self.hasSendStarsButton = hasSendStarsButton
self.sendPaidMessageStars = sendPaidMessageStars
self.editButtonsHidden = editButtonsHidden
self.editButtonsEnabled = editButtonsEnabled
self.centerButtonsHidden = centerButtonsHidden
self.allButtonsHidden = allButtonsHidden
self.cancelDoneButtonsHidden = cancelDoneButtonsHidden
self.doneButtonEnabled = doneButtonEnabled
self.interfaceOrientation = interfaceOrientation
self.bottomInset = bottomInset
self.infoString = infoString
self.cancelPressed = cancelPressed
self.donePressed = donePressed
self.doneLongPressed = doneLongPressed
self.tabPressed = tabPressed
}
static func ==(lhs: MediaPickerPhotoToolbarComponent, rhs: MediaPickerPhotoToolbarComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.solidBackground != rhs.solidBackground {
return false
}
if lhs.backButtonType != rhs.backButtonType {
return false
}
if lhs.doneButtonType != rhs.doneButtonType {
return false
}
if lhs.currentTabs != rhs.currentTabs {
return false
}
if lhs.activeTab != rhs.activeTab {
return false
}
if lhs.highlightedTabs != rhs.highlightedTabs {
return false
}
if lhs.disabledTabs != rhs.disabledTabs {
return false
}
if lhs.qualityIsPhoto != rhs.qualityIsPhoto {
return false
}
if lhs.qualityHighQuality != rhs.qualityHighQuality {
return false
}
if lhs.qualityPreset != rhs.qualityPreset {
return false
}
if lhs.timerValue != rhs.timerValue {
return false
}
if lhs.hasSendStarsButton != rhs.hasSendStarsButton {
return false
}
if lhs.sendPaidMessageStars != rhs.sendPaidMessageStars {
return false
}
if lhs.editButtonsHidden != rhs.editButtonsHidden {
return false
}
if lhs.editButtonsEnabled != rhs.editButtonsEnabled {
return false
}
if lhs.centerButtonsHidden != rhs.centerButtonsHidden {
return false
}
if lhs.allButtonsHidden != rhs.allButtonsHidden {
return false
}
if lhs.cancelDoneButtonsHidden != rhs.cancelDoneButtonsHidden {
return false
}
if lhs.doneButtonEnabled != rhs.doneButtonEnabled {
return false
}
if lhs.interfaceOrientation != rhs.interfaceOrientation {
return false
}
if lhs.bottomInset != rhs.bottomInset {
return false
}
if lhs.infoString != rhs.infoString {
return false
}
return true
}
final class View: UIView {
fileprivate let cancelButton = ComponentView<Empty>()
fileprivate let doneButton = ComponentView<Empty>()
private let doneButtonContextView = ContextControllerSourceView()
private let buttonsBackgroundView = GlassBackgroundView()
private let selectionView = UIView()
private let infoLabel = UILabel()
private let imageCache = MediaPickerPhotoToolbarImageCache()
private var centerButtonViews: [UInt: ComponentView<Empty>] = [:]
private var component: MediaPickerPhotoToolbarComponent?
private var cancelPressed: (() -> Void)?
private var donePressed: (() -> Void)?
private var doneLongPressed: ((Any?) -> Void)?
private var tabPressed: ((TGPhotoEditorTab) -> Void)?
override init(frame: CGRect) {
super.init(frame: frame)
self.clipsToBounds = false
self.addSubview(self.buttonsBackgroundView)
self.selectionView.backgroundColor = UIColor(rgb: 0xffffff, alpha: 0.15)
self.selectionView.isUserInteractionEnabled = false
self.selectionView.alpha = 0.0
self.infoLabel.backgroundColor = .clear
self.infoLabel.textAlignment = .center
self.infoLabel.textColor = .white
self.infoLabel.font = Font.regular(13.0)
self.infoLabel.isUserInteractionEnabled = false
self.addSubview(self.infoLabel)
self.doneButtonContextView.beginDelay = 0.4
self.doneButtonContextView.activated = { [weak self] gesture, _ in
guard let self else {
gesture.cancel()
return
}
self.doneLongPressed?(self.doneButtonContextView)
}
self.doneButtonContextView.shouldBegin = { [weak self] _ in
guard let self, let component = self.component else {
return false
}
return self.doneLongPressed != nil && component.doneButtonEnabled && !component.cancelDoneButtonsHidden && !component.allButtonsHidden
}
}
required init?(coder: NSCoder) {
preconditionFailure()
}
fileprivate var cancelButtonFrame: CGRect {
return self.cancelButton.view?.frame ?? .zero
}
fileprivate var doneButtonFrame: CGRect {
return self.doneButtonContextView.frame
}
fileprivate var doneButtonSourceView: UIView {
return self.doneButtonContextView
}
fileprivate func viewForTab(_ tab: TGPhotoEditorTab) -> UIView? {
return self.centerButtonViews[tab.rawValue]?.view
}
func update(component: MediaPickerPhotoToolbarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
self.cancelPressed = component.cancelPressed
self.donePressed = component.donePressed
self.doneLongPressed = component.doneLongPressed
self.tabPressed = component.tabPressed
//let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
let accentColor = UIColor(rgb: 0xffd300) //presentationData.theme.list.itemAccentColor
let selectedIconColor = UIColor.white //(rgb: 0xffd300)
self.backgroundColor = .clear //component.solidBackground ? UIColor(rgb: 0x000000, alpha: 0.55) : .clear
let cancelSize = self.cancelButton.update(
transition: transition,
component: AnyComponent(
GlassBarButtonComponent(
size: CGSize(width: toolbarButtonSide, height: toolbarButtonSide),
backgroundColor: nil,
isDark: true,
state: .glass,
isEnabled: !component.cancelDoneButtonsHidden && !component.allButtonsHidden,
isVisible: true,
component: AnyComponentWithIdentity(
id: "close",
component: AnyComponent(
BundleIconComponent(
name: component.backButtonType == TGPhotoEditorBackButtonBack ? "Navigation/Back" : "Navigation/Close",
tintColor: .white
)
)
),
action: { [weak self] _ in
self?.cancelPressed?()
}
)
),
environment: {},
containerSize: CGSize(width: toolbarButtonSide, height: toolbarButtonSide)
)
let doneIconName: String
let doneBackgroundColor: UIColor? = UIColor(rgb: 0x0088ff)
var doneWidth: CGFloat = toolbarButtonSide
switch component.doneButtonType {
case TGPhotoEditorDoneButtonSend:
doneIconName = "Media Editor/Send"
doneWidth = 46.0
case TGPhotoEditorDoneButtonSchedule:
doneIconName = "Chat/Input/ScheduleIcon"
doneWidth = 46.0
case TGPhotoEditorDoneButtonDone:
doneIconName = "Navigation/Done"
default:
doneIconName = "Navigation/Done"
}
let doneSize = self.doneButton.update(
transition: transition,
component: AnyComponent(
GlassBarButtonComponent(
size: CGSize(width: doneWidth, height: toolbarButtonSide),
backgroundColor: doneBackgroundColor,
isDark: true,
state: doneBackgroundColor != nil ? .tintedGlass : .glass,
isEnabled: component.doneButtonEnabled && !component.cancelDoneButtonsHidden && !component.allButtonsHidden,
isVisible: true,
component: AnyComponentWithIdentity(
id: "done",
component: AnyComponent(
BundleIconComponent(
name: doneIconName,
tintColor: .white
)
)
),
action: { [weak self] _ in
self?.donePressed?()
}
)
),
environment: {},
containerSize: CGSize(width: doneWidth, height: toolbarButtonSide)
)
// let doneContent: AnyComponentWithIdentity<Empty>
// let doneFixedSize: CGSize?
// let doneAvailableSize: CGSize
// if component.hasSendStarsButton {
// let text = "\u{2b50}\u{fe0f} \(component.sendPaidMessageStars)"
// doneContent = AnyComponentWithIdentity(
// id: "stars-\(component.sendPaidMessageStars)",
// component: AnyComponent(Text(text: text, font: Font.with(size: 17.0, design: .round, weight: .semibold, traits: .monospacedNumbers), color: .white))
// )
// doneFixedSize = nil
// doneAvailableSize = CGSize(width: 180.0, height: toolbarSideButtonSide)
// } else {
// doneContent = self.sideButtonContent(iconName: self.doneIconName(component.doneButtonType), color: .white, id: "done-\(component.doneButtonType.rawValue)")
// doneFixedSize = CGSize(width: toolbarSideButtonSide, height: toolbarSideButtonSide)
// doneAvailableSize = CGSize(width: toolbarSideButtonSide, height: toolbarSideButtonSide)
// }
// let doneSize = self.doneButton.update(
// content: doneContent,
// fixedSize: doneFixedSize,
// availableSize: doneAvailableSize,
// isEnabled: component.doneButtonEnabled && !component.cancelDoneButtonsHidden && !component.allButtonsHidden,
// transition: transition
// )
let sideAlpha = component.allButtonsHidden ? 0.0 : 1.0
let sideFrames = self.sideButtonFrames(availableSize: availableSize, cancelSize: cancelSize, doneSize: doneSize, component: component)
if let cancelButtonView = self.cancelButton.view {
if cancelButtonView.superview == nil {
self.addSubview(cancelButtonView)
}
transition.setAlpha(view: cancelButtonView, alpha: sideAlpha)
transition.setFrame(view: cancelButtonView, frame: sideFrames.cancel)
}
if let doneButtonView = self.doneButton.view {
if self.doneButtonContextView.superview == nil {
self.addSubview(self.doneButtonContextView)
}
if doneButtonView.superview !== self.doneButtonContextView {
self.doneButtonContextView.addSubview(doneButtonView)
}
self.doneButtonContextView.targetViewForActivationProgress = doneButtonView
self.doneButtonContextView.isGestureEnabled = component.doneLongPressed != nil && component.doneButtonEnabled && !component.cancelDoneButtonsHidden && !component.allButtonsHidden
transition.setAlpha(view: self.doneButtonContextView, alpha: sideAlpha * (component.doneButtonEnabled ? 1.0 : 0.2))
transition.setFrame(view: self.doneButtonContextView, frame: sideFrames.done)
transition.setFrame(view: doneButtonView, frame: CGRect(origin: .zero, size: sideFrames.done.size))
}
self.updateCenterButtons(
component: component,
availableSize: availableSize,
doneSize: doneSize,
selectedIconColor: selectedIconColor,
accentColor: accentColor,
transition: transition
)
self.updateInfoLabel(component: component, availableSize: availableSize, transition: transition)
return availableSize
}
private func doneIconName(_ type: TGPhotoEditorDoneButton) -> String {
switch type.rawValue {
case 1:
return "Editor/Commit"
case 2:
return "Editor/Commit"
case 3:
return "PhotoPickerSendIcon"
default:
return "PhotoPickerSendIcon"
}
}
private func sideButtonFrames(availableSize: CGSize, cancelSize: CGSize, doneSize: CGSize, component: MediaPickerPhotoToolbarComponent) -> (cancel: CGRect, done: CGRect) {
let sideInset: CGFloat = 26.0
if availableSize.width > availableSize.height {
let cancelFrame = CGRect(origin: CGPoint(x: sideInset, y: 0.0), size: cancelSize)
let doneFrame: CGRect
if component.hasSendStarsButton {
doneFrame = CGRect(x: availableSize.width - doneSize.width - 2.0, y: 0.0, width: doneSize.width, height: doneSize.height)
} else {
doneFrame = CGRect(x: availableSize.width - doneSize.width - sideInset, y: 0.0, width: doneSize.width, height: doneSize.height)
}
return (cancelFrame, doneFrame)
} else {
let offset: CGFloat = component.interfaceOrientation == .landscapeLeft ? availableSize.width - toolbarSideButtonSide : 0.0
let cancelFrame = CGRect(x: offset, y: availableSize.height - toolbarSideButtonSide, width: toolbarSideButtonSide, height: toolbarSideButtonSide)
let doneFrame = CGRect(x: offset, y: 0.0, width: toolbarSideButtonSide, height: toolbarSideButtonSide)
return (cancelFrame, doneFrame)
}
}
private func updateCenterButtons(component: MediaPickerPhotoToolbarComponent, availableSize: CGSize, doneSize: CGSize, selectedIconColor: UIColor, accentColor: UIColor, transition: ComponentTransition) {
let tabs = toolbarTabOrder.filter { component.currentTabs.contains($0) }
let visibleRawValues = Set(tabs.map { $0.rawValue })
for (rawValue, buttonView) in Array(self.centerButtonViews) {
if !visibleRawValues.contains(rawValue) {
if let view = buttonView.view {
transition.setAlpha(view: view, alpha: 0.0, completion: { _ in
view.removeFromSuperview()
})
}
self.centerButtonViews.removeValue(forKey: rawValue)
}
}
guard !tabs.isEmpty else {
transition.setAlpha(view: self.buttonsBackgroundView, alpha: 0.0)
transition.setAlpha(view: self.selectionView, alpha: 0.0)
self.buttonsBackgroundView.isUserInteractionEnabled = false
return
}
let buttonFrames = self.centerButtonFrames(tabs: tabs, availableSize: availableSize, doneSize: doneSize, component: component)
var backgroundFrame = CGRect.null
for tab in tabs {
if let frame = buttonFrames[tab.rawValue] {
backgroundFrame = backgroundFrame.union(frame)
}
}
if backgroundFrame.isNull {
transition.setAlpha(view: self.buttonsBackgroundView, alpha: 0.0)
transition.setAlpha(view: self.selectionView, alpha: 0.0)
self.buttonsBackgroundView.isUserInteractionEnabled = false
return
}
backgroundFrame = CGRect(
x: floorToScreenPixels(backgroundFrame.minX),
y: floorToScreenPixels(backgroundFrame.minY),
width: ceil(backgroundFrame.width),
height: ceil(backgroundFrame.height)
)
let centerHidden = component.allButtonsHidden || component.centerButtonsHidden || component.editButtonsHidden
let centerAlpha: CGFloat = centerHidden ? 0.0 : (component.editButtonsEnabled ? 1.0 : 0.2)
self.buttonsBackgroundView.isUserInteractionEnabled = !centerHidden && component.editButtonsEnabled
transition.setAlpha(view: self.buttonsBackgroundView, alpha: centerAlpha)
transition.setFrame(view: self.buttonsBackgroundView, frame: backgroundFrame)
let minSide = min(backgroundFrame.width, backgroundFrame.height)
self.buttonsBackgroundView.update(size: backgroundFrame.size, cornerRadius: minSide * 0.5, isDark: true, tintColor: .init(kind: .panel), isInteractive: true, isVisible: !centerHidden, transition: transition)
if self.selectionView.superview == nil {
self.buttonsBackgroundView.contentView.insertSubview(self.selectionView, at: 0)
} else {
self.buttonsBackgroundView.contentView.sendSubviewToBack(self.selectionView)
}
var selectionFrame: CGRect?
for tab in tabs {
guard let buttonFrame = buttonFrames[tab.rawValue] else {
continue
}
let rawValue = tab.rawValue
let buttonView: ComponentView<Empty>
if let current = self.centerButtonViews[rawValue] {
buttonView = current
} else {
buttonView = ComponentView<Empty>()
self.centerButtonViews[rawValue] = buttonView
}
let isDisabled = component.disabledTabs.contains(tab)
let isHighlighted = component.highlightedTabs.contains(tab)
let selectedVisual = component.activeTab == tab && !dontHighlightOnSelectionTabs.contains(rawValue)
let iconColor: UIColor
if selectedVisual {
iconColor = selectedIconColor
} else if isHighlighted && !isDisabled {
iconColor = accentColor
} else {
iconColor = .white
}
let content: AnyComponent<Empty>
switch tab {
case .qualityTab:
content = AnyComponent(
Image(
image: self.imageCache.qualityIcon(
isPhoto: component.qualityIsPhoto,
highQuality: component.qualityHighQuality,
preset: component.qualityPreset,
color: iconColor
),
size: CGSize(width: 28.0, height: 22.0),
contentMode: .center
)
)
case .timerTab:
content = AnyComponent(
Image(
image: self.imageCache.timerIcon(value: component.timerValue, color: iconColor),
size: CGSize(width: 24.0, height: 24.0),
contentMode: .center
)
)
default:
content = AnyComponent(
BundleIconComponent(
name: self.iconName(tab),
tintColor: iconColor,
maxSize: CGSize(width: 28.0, height: 28.0)
)
)
}
let buttonSize = buttonView.update(
transition: transition,
component: AnyComponent(
PlainButtonComponent(
content: content,
minSize: CGSize(width: toolbarButtonSide, height: toolbarButtonSide),
action: { [weak self] in
self?.tabPressed?(tab)
},
isEnabled: component.editButtonsEnabled && !isDisabled,
animateAlpha: true,
animateScale: false,
animateContents: false
)
),
environment: {},
containerSize: CGSize(width: toolbarButtonSide, height: toolbarButtonSide)
)
if let view = buttonView.view {
if view.superview == nil {
self.buttonsBackgroundView.contentView.addSubview(view)
}
let localFrame = CGRect(
x: buttonFrame.minX - backgroundFrame.minX,
y: buttonFrame.minY - backgroundFrame.minY,
width: buttonSize.width,
height: buttonSize.height
)
if selectedVisual {
let selectionSide = max(0.0, min(backgroundFrame.width, backgroundFrame.height) - 6.0)
selectionFrame = CGRect(
x: floorToScreenPixels(localFrame.midX - selectionSide / 2.0),
y: floorToScreenPixels(localFrame.midY - selectionSide / 2.0),
width: selectionSide,
height: selectionSide
)
}
transition.setFrame(view: view, frame: localFrame)
transition.setAlpha(view: view, alpha: isDisabled ? 0.2 : 1.0)
}
}
if let selectionFrame = selectionFrame {
self.selectionView.layer.cornerRadius = selectionFrame.width * 0.5
transition.setFrame(view: self.selectionView, frame: selectionFrame)
transition.setAlpha(view: self.selectionView, alpha: centerHidden ? 0.0 : 1.0)
} else {
transition.setAlpha(view: self.selectionView, alpha: 0.0)
}
}
private func iconName(_ tab: TGPhotoEditorTab) -> String {
switch tab {
case .cropTab:
return "Media Editor/Crop"
case .toolsTab:
return "Media Editor/Adjustments"
case .rotateTab:
return "Editor/Rotate"
case .paintTab:
return "Media Editor/Pencil"
case .stickerTab:
return "Media Editor/AddSticker"
case .textTab:
return "Media Editor/AddText"
case .eraserTab:
return "Editor/Eraser"
case .mirrorTab:
return "Media Editor/Mirror"
case .aspectRatioTab:
return "Editor/AspectRatio"
case .tintTab:
return "Media Editor/Tint"
case .blurTab:
return "Media Editor/Blur"
case .curvesTab:
return "Media Editor/Curves"
default:
return "Media Editor/Crop"
}
}
private func centerButtonFrames(tabs: [TGPhotoEditorTab], availableSize: CGSize, doneSize: CGSize, component: MediaPickerPhotoToolbarComponent) -> [UInt: CGRect] {
var result: [UInt: CGRect] = [:]
let count = tabs.count
guard count != 0 else {
return result
}
let totalLength = CGFloat(count) * toolbarButtonSide + CGFloat(count - 1) * centerButtonSpacing
let step = toolbarButtonSide + centerButtonSpacing
if availableSize.width > availableSize.height {
let leftEdge = toolbarSideButtonSide
let rightEdge: CGFloat = component.hasSendStarsButton ? doneSize.width + 2.0 : toolbarSideButtonSide
let availableWidth = availableSize.width - leftEdge - rightEdge
let startX = floorToScreenPixels(leftEdge + (availableWidth - totalLength) / 2.0)
for i in 0 ..< count {
result[tabs[i].rawValue] = CGRect(
x: startX + CGFloat(i) * step,
y: 0.0,
width: toolbarButtonSide,
height: toolbarButtonSide
)
}
} else {
let x: CGFloat
if component.interfaceOrientation == .landscapeLeft {
x = availableSize.width - toolbarButtonSide - 8.0
} else {
x = 8.0
}
let topInset = toolbarSideButtonSide
let bottomInset = toolbarSideButtonSide
let availableHeight = availableSize.height - topInset - bottomInset
let startY = floorToScreenPixels(topInset + (availableHeight - totalLength) / 2.0)
for i in 0 ..< count {
result[tabs[i].rawValue] = CGRect(
x: x,
y: startY + CGFloat(i) * step,
width: toolbarButtonSide,
height: toolbarButtonSide
)
}
}
return result
}
private func updateInfoLabel(component: MediaPickerPhotoToolbarComponent, availableSize: CGSize, transition: ComponentTransition) {
self.infoLabel.text = component.infoString
self.infoLabel.isHidden = component.infoString == nil
guard component.infoString != nil else {
return
}
if availableSize.width > availableSize.height {
self.infoLabel.transform = .identity
transition.setFrame(view: self.infoLabel, frame: CGRect(x: toolbarSideButtonSide + 10.0, y: 0.0, width: availableSize.width - (toolbarSideButtonSide + 10.0) * 2.0, height: toolbarSideButtonSide))
} else {
let bounds = CGRect(x: 0.0, y: 0.0, width: availableSize.height - (toolbarSideButtonSide + 10.0) * 2.0, height: availableSize.width)
self.infoLabel.bounds = bounds
self.infoLabel.center = CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0)
if component.interfaceOrientation == .landscapeLeft {
self.infoLabel.transform = CGAffineTransform(rotationAngle: .pi / 2.0)
} else if component.interfaceOrientation == .landscapeRight {
self.infoLabel.transform = CGAffineTransform(rotationAngle: -.pi / 2.0)
} else {
self.infoLabel.transform = .identity
}
}
}
}
func makeView() -> View {
return View(frame: .zero)
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
public func makeMediaPickerPhotoToolbarView(context: AccountContext, backButton: TGPhotoEditorBackButton, doneButton: TGPhotoEditorDoneButton, solidBackground: Bool, hasSendStarsButton: Bool) -> (UIView & TGPhotoToolbarViewProtocol)? {
return MediaPickerPhotoToolbarView(context: context, backButton: backButton, doneButton: doneButton, solidBackground: solidBackground, hasSendStarsButton: hasSendStarsButton)
}
final class MediaPickerPhotoToolbarView: UIView, TGPhotoToolbarViewProtocol {
private let context: AccountContext
private let solidBackground: Bool
private let hasSendStarsButton: Bool
private let rootView = ComponentView<Empty>()
private var transitionedOut = false
var cancelPressed: (() -> Void)? {
didSet {
self.update(transition: .immediate)
}
}
var donePressed: (() -> Void)? {
didSet {
self.update(transition: .immediate)
}
}
var doneLongPressed: ((Any?) -> Void)? {
didSet {
self.update(transition: .immediate)
}
}
var tabPressed: ((TGPhotoEditorTab) -> Void)? {
didSet {
self.update(transition: .immediate)
}
}
var interfaceOrientation: UIInterfaceOrientation = .portrait {
didSet {
self.update(transition: .immediate)
}
}
var bottomInset: CGFloat = 0.0
var backButtonType: TGPhotoEditorBackButton {
didSet {
self.update(transition: .immediate)
}
}
var doneButtonType: TGPhotoEditorDoneButton {
didSet {
self.update(transition: .immediate)
}
}
var sendPaidMessageStars: Int64 = 0 {
didSet {
self.update(transition: ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut)))
}
}
private(set) var currentTabs: TGPhotoEditorTab = []
private var activeTab: TGPhotoEditorTab = []
private var highlightedTabs: TGPhotoEditorTab = []
private var disabledTabs: TGPhotoEditorTab = []
private var editButtonsHidden = false
private var editButtonsEnabled = true
private var centerButtonsHidden = false
private var allButtonsHidden = false
private var cancelDoneButtonsHidden = false
private var doneButtonEnabled = true
private var qualityIsPhoto = false
private var qualityHighQuality = false
private var qualityPreset = 3
private var timerValue = 0
private var infoString: String?
var doneButton: UIView {
return (self.rootView.view as? MediaPickerPhotoToolbarComponent.View)?.doneButtonSourceView ?? UIView()
}
var cancelButtonFrame: CGRect {
return (self.rootView.view as? MediaPickerPhotoToolbarComponent.View)?.cancelButtonFrame ?? .zero
}
var doneButtonFrame: CGRect {
return (self.rootView.view as? MediaPickerPhotoToolbarComponent.View)?.doneButtonFrame ?? .zero
}
init(context: AccountContext, backButton: TGPhotoEditorBackButton, doneButton: TGPhotoEditorDoneButton, solidBackground: Bool, hasSendStarsButton: Bool) {
self.context = context
self.backButtonType = backButton
self.doneButtonType = doneButton
self.solidBackground = solidBackground
self.hasSendStarsButton = hasSendStarsButton
super.init(frame: .zero)
self.clipsToBounds = false
self.backgroundColor = .clear
self.update(transition: .immediate)
}
required init?(coder: NSCoder) {
preconditionFailure()
}
override func layoutSubviews() {
super.layoutSubviews()
self.update(transition: .immediate)
self.updateRootFrame(transition: .immediate)
}
func transitionIn(animated: Bool) {
self.transitionIn(animated: animated, transparent: false)
}
func transitionIn(animated: Bool, transparent: Bool) {
self.transitionedOut = false
self.isHidden = false
if !transparent {
self.backgroundColor = UIColor(rgb: 0x000000)
}
let transition = animated ? ComponentTransition(animation: .curve(duration: 0.3, curve: .easeInOut)) : .immediate
self.updateRootFrame(transition: transition)
if !transparent {
self.backgroundColor = .clear
}
}
func transitionOut(animated: Bool) {
self.transitionOut(animated: animated, transparent: false, hideOnCompletion: false)
}
func transitionOut(animated: Bool, transparent: Bool, hideOnCompletion: Bool) {
self.transitionedOut = true
if !transparent {
self.backgroundColor = UIColor(rgb: 0x000000)
}
let duration: Double = animated ? 0.3 : 0.0
let transition = animated ? ComponentTransition(animation: .curve(duration: duration, curve: .easeInOut)) : .immediate
self.updateRootFrame(transition: transition)
if hideOnCompletion {
DispatchQueue.main.asyncAfter(deadline: .now() + duration) { [weak self] in
self?.isHidden = true
}
}
}
func setDoneButtonEnabled(_ enabled: Bool, animated: Bool) {
self.doneButtonEnabled = enabled
self.update(transition: self.transition(animated: animated))
}
func setEditButtonsEnabled(_ enabled: Bool, animated: Bool) {
self.editButtonsEnabled = enabled
self.update(transition: self.transition(animated: animated))
}
func setEditButtonsHidden(_ hidden: Bool, animated: Bool) {
self.editButtonsHidden = hidden
self.update(transition: self.transition(animated: animated))
}
func setEditButtonsHighlighted(_ buttons: TGPhotoEditorTab) {
self.highlightedTabs = buttons
self.update(transition: .immediate)
}
func setEditButtonsDisabled(_ buttons: TGPhotoEditorTab) {
self.disabledTabs = buttons
self.update(transition: .immediate)
}
func setCenterButtonsHidden(_ hidden: Bool, animated: Bool) {
self.centerButtonsHidden = hidden
self.update(transition: self.transition(animated: animated))
}
func setAllButtonsHidden(_ hidden: Bool, animated: Bool) {
self.allButtonsHidden = hidden
self.update(transition: self.transition(animated: animated))
}
func setCancelDoneButtonsHidden(_ hidden: Bool, animated: Bool) {
self.cancelDoneButtonsHidden = hidden
self.update(transition: self.transition(animated: animated))
}
func setToolbarTabs(_ tabs: TGPhotoEditorTab, animated: Bool) {
self.currentTabs = tabs
self.update(transition: self.transition(animated: animated))
}
func setActiveTab(_ tab: TGPhotoEditorTab) {
self.activeTab = tab
self.update(transition: .spring(duration: 0.4))
}
func setQualityButtonIsPhoto(_ isPhoto: Bool, highQuality: Bool, videoPreset: Int) {
self.qualityIsPhoto = isPhoto
self.qualityHighQuality = highQuality
self.qualityPreset = videoPreset
self.update(transition: .immediate)
}
func setTimerButtonValue(_ value: Int) {
self.timerValue = value
self.update(transition: .immediate)
}
func setInfoString(_ string: String?) {
self.infoString = string
self.update(transition: .immediate)
}
@objc(viewForTab:)
func view(for tab: TGPhotoEditorTab) -> UIView? {
return (self.rootView.view as? MediaPickerPhotoToolbarComponent.View)?.viewForTab(tab)
}
private func transition(animated: Bool) -> ComponentTransition {
if animated {
return ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut))
} else {
return .immediate
}
}
private func update(transition: ComponentTransition) {
let size = self.bounds.size
let _ = self.rootView.update(
transition: transition,
component: AnyComponent(
MediaPickerPhotoToolbarComponent(
context: self.context,
solidBackground: self.solidBackground,
backButtonType: self.backButtonType,
doneButtonType: self.doneButtonType,
currentTabs: self.currentTabs,
activeTab: self.activeTab,
highlightedTabs: self.highlightedTabs,
disabledTabs: self.disabledTabs,
qualityIsPhoto: self.qualityIsPhoto,
qualityHighQuality: self.qualityHighQuality,
qualityPreset: self.qualityPreset,
timerValue: self.timerValue,
hasSendStarsButton: self.hasSendStarsButton,
sendPaidMessageStars: self.sendPaidMessageStars,
editButtonsHidden: self.editButtonsHidden,
editButtonsEnabled: self.editButtonsEnabled,
centerButtonsHidden: self.centerButtonsHidden,
allButtonsHidden: self.allButtonsHidden,
cancelDoneButtonsHidden: self.cancelDoneButtonsHidden,
doneButtonEnabled: self.doneButtonEnabled,
interfaceOrientation: self.interfaceOrientation,
bottomInset: self.bottomInset,
infoString: self.infoString,
cancelPressed: self.cancelPressed,
donePressed: self.donePressed,
doneLongPressed: self.doneLongPressed,
tabPressed: self.tabPressed
)
),
environment: {},
forceUpdate: true,
containerSize: size
)
if let view = self.rootView.view {
if view.superview == nil {
self.addSubview(view)
}
self.updateRootFrame(transition: transition)
}
}
private func updateRootFrame(transition: ComponentTransition) {
guard let view = self.rootView.view else {
return
}
var frame = CGRect(origin: .zero, size: self.bounds.size)
if self.transitionedOut {
if self.bounds.width > self.bounds.height {
frame.origin.y = self.bounds.height
} else if self.interfaceOrientation == .landscapeLeft {
frame.origin.x = -self.bounds.width
} else {
frame.origin.x = self.bounds.width
}
}
transition.setFrame(view: view, frame: frame)
}
}