Files
letro-ios/ElementX/Sources/Screens/Authentication/StartScreen/View/AuthenticationStartLogo.swift
Doug 67162381a8 Fix the AuthenticationStartScreen with large Dynamic Type sizes. (#5191)
* Allow AuthenticationStartLogo to be scaled and fix dark mode when not on a gradient.

* Fix the layout of AuthenticationStartScreen with large Dynamic Type sizes.
2026-03-12 14:18:19 +00:00

193 lines
6.9 KiB
Swift

//
// Copyright 2025 Element Creations Ltd.
// Copyright 2023-2025 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
import SwiftUI
/// The app's logo styled to fit on various launch pages.
struct AuthenticationStartLogo: View {
/// Set to specify a custom size for the Logo, otherwise the default size of 158pt will be used.
var size: CGFloat?
/// Set to `true` to skip the brand chrome.
let hideBrandChrome: Bool
/// Set to `true` when using on top of `Asset.Images.launchBackground`.
let isOnGradient: Bool
private let appLogoImage = Image(asset: Asset.Images.appLogo)
struct SizeMetrics {
let scale: CGFloat
let imageSize: CGFloat
}
private var sizeMetrics: SizeMetrics? {
size.map { customSize in
let scale = customSize / 158
return SizeMetrics(scale: scale,
imageSize: hideBrandChrome ? customSize : 110 * scale)
}
}
var body: some View {
if let sizeMetrics {
appLogoImage
.resizable()
.frame(width: sizeMetrics.imageSize, height: sizeMetrics.imageSize)
.modifier(AuthenticationBrandLogoModifier(scale: sizeMetrics.scale,
hideBrandChrome: hideBrandChrome,
isOnGradient: isOnGradient))
} else {
appLogoImage
.modifier(AuthenticationBrandLogoModifier(scale: 1,
hideBrandChrome: hideBrandChrome,
isOnGradient: isOnGradient))
}
}
}
/// Applies the brand chrome styling (rounded card with shadows and border) to any image,
/// as seen on the authentication start screen.
private struct AuthenticationBrandLogoModifier: ViewModifier {
@Environment(\.colorScheme) private var colorScheme
/// Scale factor relative to the original 158pt design.
let scale: CGFloat
/// Set to `true` to skip the brand chrome.
let hideBrandChrome: Bool
/// Set to `true` when using on top of `Asset.Images.launchBackground`.
let isOnGradient: Bool
private let outerShapeShadowColor = Color(red: 0.11, green: 0.11, blue: 0.13)
private var isLight: Bool {
colorScheme == .light
}
/// Extra padding needed to avoid cropping the shadows.
private var extra: CGFloat {
64 * scale
}
/// The shape that the logo is composed on top of.
private var outerShape: RoundedRectangle {
RoundedRectangle(cornerRadius: 44 * scale)
}
func body(content: Content) -> some View {
if hideBrandChrome {
content
} else {
styledContent(content)
}
}
private func styledContent(_ content: Content) -> some View {
content
.background {
Circle()
.inset(by: 1 * scale)
.shadow(color: .black.opacity(!isLight && isOnGradient ? 0.3 : 0.4),
radius: 12.57143 * scale,
y: 6.28571 * scale)
Circle()
.inset(by: 1 * scale)
.shadow(color: .black.opacity(0.5),
radius: 12.57143 * scale,
y: 6.28571 * scale)
.blendMode(.overlay)
}
.padding(24 * scale)
.background {
Color.white
.opacity(isLight ? 0.23 : isOnGradient ? 0.05 : 0.13)
}
.clipShape(outerShape)
.overlay {
outerShape
.inset(by: 0.25 * scale)
.stroke(.white.opacity(isLight ? 1 : isOnGradient ? 0.9 : 0.25), lineWidth: 0.5 * scale)
.blendMode(isLight ? .normal : .overlay)
}
.padding(extra)
.background {
ZStack {
if !isLight, isOnGradient {
outerShape
.inset(by: 1 * scale)
.padding(extra)
.shadow(color: .black.opacity(0.5),
radius: 32.91666 * scale,
y: 1.05333 * scale)
} else {
outerShape
.inset(by: 1 * scale)
.padding(extra)
.shadow(color: outerShapeShadowColor.opacity(isLight ? 0.23 : 0.08),
radius: 16 * scale,
y: 8 * scale)
outerShape
.inset(by: 1 * scale)
.padding(extra)
.shadow(color: outerShapeShadowColor.opacity(0.5),
radius: 16 * scale,
y: 8 * scale)
.blendMode(.overlay)
}
}
.mask {
outerShape
.inset(by: -extra / 2)
.stroke(lineWidth: extra)
.padding(extra)
}
}
.padding(-extra)
.accessibilityHidden(true)
}
}
#Preview {
VStack(spacing: 0) {
HStack(spacing: 0) {
AuthenticationStartLogo(hideBrandChrome: false, isOnGradient: false)
.padding()
AuthenticationStartLogo(hideBrandChrome: false, isOnGradient: true)
.padding()
.background {
AuthenticationStartScreenBackgroundImage().offset(y: 70)
}
.clipped()
}
.background(.compound.bgCanvasDefault)
HStack(spacing: 0) {
AuthenticationStartLogo(hideBrandChrome: false, isOnGradient: false)
.padding()
AuthenticationStartLogo(hideBrandChrome: false, isOnGradient: true)
.padding()
.background {
AuthenticationStartScreenBackgroundImage().offset(y: 70)
}
.clipped()
}
.background(.compound.bgCanvasDefault)
.colorScheme(.dark)
HStack(spacing: 0) {
AuthenticationStartLogo(size: 54, hideBrandChrome: false, isOnGradient: false)
.padding()
.background(.compound.bgCanvasDefault)
AuthenticationStartLogo(size: 54, hideBrandChrome: false, isOnGradient: false)
.padding()
.background(.compound.bgCanvasDefault)
.colorScheme(.dark)
}
.padding(.top)
}
}