Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/SignalUI/Wallpapers/Wallpaper+SignalUI.swift
1 views
//
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import Foundation
import PureLayout
public import SignalServiceKit

extension Wallpaper {

    public static func viewBuilder(for thread: TSThread? = nil, tx: DBReadTransaction) -> WallpaperViewBuilder? {
        AssertIsOnMainThread()

        let wallpaperStore = DependenciesBridge.shared.wallpaperStore
        guard
            let resolvedWallpaper = wallpaperStore.fetchWallpaperForRendering(
                for: thread?.uniqueId,
                tx: tx,
            )
        else {
            return nil
        }

        return viewBuilder(
            for: resolvedWallpaper,
            customPhoto: {
                WallpaperStore.fetchResolvedValue(
                    for: thread,
                    fetchBlock: {
                        if let thread = $0 {
                            DependenciesBridge.shared.wallpaperImageStore.loadWallpaperImage(for: thread, tx: tx)
                        } else {
                            DependenciesBridge.shared.wallpaperImageStore.loadGlobalThreadWallpaper(tx: tx)
                        }
                    },
                )
            },
            shouldDimInDarkTheme: wallpaperStore.fetchDimInDarkModeForRendering(for: thread?.uniqueId, tx: tx),
        )
    }

    public static func viewBuilder(
        for wallpaper: Wallpaper,
        customPhoto: () -> UIImage?,
        shouldDimInDarkTheme: Bool,
    ) -> WallpaperViewBuilder? {
        AssertIsOnMainThread()

        if case .photo = wallpaper, let customPhoto = customPhoto() {
            return .customPhoto(customPhoto, shouldDimInDarkMode: shouldDimInDarkTheme)
        } else if let colorOrGradientSetting = wallpaper.asColorOrGradientSetting {
            return .colorOrGradient(colorOrGradientSetting, shouldDimInDarkMode: shouldDimInDarkTheme)
        } else {
            owsFailDebug("Couldn't create wallpaper view builder.")
            return nil
        }
    }
}

// MARK: -

public enum WallpaperViewBuilder {
    case colorOrGradient(ColorOrGradientSetting, shouldDimInDarkMode: Bool)
    case customPhoto(UIImage, shouldDimInDarkMode: Bool)

    public func build() -> WallpaperView {
        switch self {
        case .customPhoto(let customPhoto, let shouldDimInDarkMode):
            return WallpaperView(mode: .imageView(customPhoto), shouldDimInDarkTheme: shouldDimInDarkMode)
        case .colorOrGradient(let colorOrGradientSetting, let shouldDimInDarkMode):
            return WallpaperView(
                mode: .colorView(ColorOrGradientSwatchView(
                    setting: colorOrGradientSetting,
                    shapeMode: .rectangle,
                    themeMode: shouldDimInDarkMode ? .auto : .alwaysLight,
                )),
                shouldDimInDarkTheme: shouldDimInDarkMode,
            )
        }
    }
}

// MARK: -

public class WallpaperView {
    fileprivate enum Mode {
        case colorView(UIView)
        case imageView(UIImage)
    }

    public private(set) var contentView: UIView?

    public private(set) var dimmingView: UIView?

    public private(set) var blurProvider: WallpaperBlurProvider?

    private let mode: Mode

    fileprivate init(mode: Mode, shouldDimInDarkTheme: Bool) {
        self.mode = mode

        configure(shouldDimInDarkTheme: shouldDimInDarkTheme)
    }

    public func asPreviewView() -> UIView {
        let previewView = UIView.container()
        if let contentView {
            previewView.addSubview(contentView)
            contentView.autoPinEdgesToSuperviewEdges()
        }
        if let dimmingView {
            previewView.addSubview(dimmingView)
            dimmingView.autoPinEdgesToSuperviewEdges()
        }
        return previewView
    }

    private func configure(shouldDimInDarkTheme: Bool) {
        let contentView: UIView = {
            switch mode {
            case .colorView(let colorView):
                return colorView
            case .imageView(let image):
                let imageView = UIImageView(image: image)
                imageView.contentMode = .scaleAspectFill
                imageView.clipsToBounds = true

                // TODO: Bake dimming into the image.
                let shouldDim = Theme.isDarkThemeEnabled && shouldDimInDarkTheme
                if shouldDim {
                    let dimmingView = UIView()
                    dimmingView.backgroundColor = .ows_blackAlpha20
                    self.dimmingView = dimmingView
                }

                return imageView
            }
        }()
        self.contentView = contentView
        self.blurProvider = WallpaperBlurProviderImpl(contentView: contentView, shouldDimInDarkTheme: shouldDimInDarkTheme)
    }
}

// MARK: -

private struct WallpaperBlurToken: Equatable {
    let contentSize: CGSize
    let shouldDimInDarkTheme: Bool
    let isDarkThemeEnabled: Bool
}

// MARK: -

public class WallpaperBlurState: NSObject {
    public let image: UIImage
    public let referenceView: UIView
    fileprivate let token: WallpaperBlurToken

    private static let idCounter = AtomicUInt(0, lock: .sharedGlobal)
    public let id: UInt = WallpaperBlurState.idCounter.increment()

    fileprivate init(
        image: UIImage,
        referenceView: UIView,
        token: WallpaperBlurToken,
    ) {
        self.image = image
        self.referenceView = referenceView
        self.token = token
    }
}

// MARK: -

public protocol WallpaperBlurProvider: AnyObject {
    var wallpaperBlurState: WallpaperBlurState? { get }
}

// MARK: -

public class WallpaperBlurProviderImpl: NSObject, WallpaperBlurProvider {

    private let contentView: UIView

    private let shouldDimInDarkTheme: Bool

    private var cachedState: WallpaperBlurState?

    init(contentView: UIView, shouldDimInDarkTheme: Bool) {
        self.contentView = contentView
        self.shouldDimInDarkTheme = shouldDimInDarkTheme
    }

    @available(swift, obsoleted: 1.0)
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    public var wallpaperBlurState: WallpaperBlurState? {
        AssertIsOnMainThread()

        // De-bounce.
        let bounds = contentView.bounds
        let isDarkThemeEnabled = Theme.isDarkThemeEnabled
        let newToken = WallpaperBlurToken(
            contentSize: bounds.size,
            shouldDimInDarkTheme: shouldDimInDarkTheme,
            isDarkThemeEnabled: isDarkThemeEnabled,
        )
        if let cachedState, cachedState.token == newToken {
            return cachedState
        }

        self.cachedState = nil

        guard bounds.size.isNonEmpty else { return nil }

        let blurRadius: CGFloat = 20
        let colorOverlays: [(UIColor, UIImage.CompositingMode)]
        // Replicate UIBlurEffect.Style.systemThinMaterialLight and .systemThinMaterialDark.
        if isDarkThemeEnabled {
            let mainOverlayAlpha = shouldDimInDarkTheme ? 0.8 : 0.9
            colorOverlays = [
                (UIColor(white: 0, alpha: mainOverlayAlpha), .sourceAtop),
                (UIColor(white: 0.5, alpha: 0.04), .darken),
            ]
        } else {
            colorOverlays = [
                (UIColor(white: 1, alpha: 0.4), .sourceAtop),
                (UIColor(white: 1, alpha: 0.16), .lighten),
            ]
        }

        do {
            let contentImage = contentView.renderAsImage(opaque: true, scale: 1)
            let blurredImage = try contentImage.withGaussianBlur(
                radius: blurRadius,
                colorOverlays: colorOverlays,
                vibrancy: 0.2,
                exposureAdjustment: 0.4,
            )
            let state = WallpaperBlurState(
                image: blurredImage,
                referenceView: contentView,
                token: newToken,
            )
            self.cachedState = state
            return state
        } catch {
            owsFailDebug("Error: \(error).")
            return nil
        }
    }
}

// MARK: -

extension CACornerMask {
    var asUIRectCorner: UIRectCorner {
        var corners = UIRectCorner()
        if self.contains(.layerMinXMinYCorner) {
            corners.formUnion(.topLeft)
        }
        if self.contains(.layerMaxXMinYCorner) {
            corners.formUnion(.topRight)
        }
        if self.contains(.layerMinXMaxYCorner) {
            corners.formUnion(.bottomLeft)
        }
        if self.contains(.layerMaxXMaxYCorner) {
            corners.formUnion(.bottomRight)
        }
        return corners
    }
}