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

import SignalServiceKit
import UIKit

///
/// An object that describes shape of a bubble in chat.
///
/// This structure is designed to work in conjunction with `CVColorOrGradientView` and `CVWallpaperBlurView`.
///
public struct BubbleConfiguration {

    /// Bubble's corner rounding configuration.
    public let corners: Corners

    /// Bubble's stroke configuration.
    ///
    /// This property can be `nil` for no stroke.
    public let stroke: Stroke?

    /// - Parameter corners: Bubble's corner rouding configuration.
    /// - Parameter stroke: Bubble's stroke configuration. Pass `nil` for no stroke.
    public init(corners: Corners, stroke: Stroke? = nil) {
        self.stroke = stroke
        self.corners = corners
    }

    // MARK: - Corners

    ///
    /// An object that contains configuration of chat bubble corner rounding..
    ///
    public struct Corners {

        fileprivate enum Style {
            /// Same radius for all corners.
            case uniform(radius: CGFloat)
            /// One radius for corners in `sharpCorners`, other radius for the rest.
            case segmented(sharpCorners: UIRectCorner, sharpCornerRadius: CGFloat, wideCornerRadius: CGFloat)
            /// Dynamic corner radius dependent on view's size.
            case capsule(maxRadius: CGFloat)
        }

        fileprivate let style: Style

        /// Creates a configuration where all corners have the same radius.
        public static func uniform(_ radius: CGFloat) -> Corners {
            Corners(style: .uniform(radius: radius))
        }

        /// Creates a configuration where some corners have one (sharp) corner radius
        /// and the rest have another (wide) corner radius.
        ///
        /// - Parameter sharpCorners: Set of corners that should have `sharpCornerRadius`.
        /// - Parameter sharpCornerRadius: Radius for corners specified in `sharpCorners`.
        /// - Parameter wideCornerRadius: Radius to use in corners that are not in `sharpCorners`.
        ///
        /// This method will check parameter value and will fall back to `uniform()` if needed.
        public static func segmented(
            sharpCorners: OWSDirectionalRectCorner,
            sharpCornerRadius: CGFloat,
            wideCornerRadius: CGFloat,
        ) -> Corners {
            if sharpCornerRadius == wideCornerRadius {
                return .uniform(sharpCornerRadius)
            }
            if sharpCorners.isEmpty {
                return .uniform(wideCornerRadius)
            }
            if sharpCorners == [.allCorners] {
                return .uniform(sharpCornerRadius)
            }
            return Corners(style: .segmented(
                sharpCorners: UIView.uiRectCorner(forOWSDirectionalRectCorner: sharpCorners),
                sharpCornerRadius: sharpCornerRadius,
                wideCornerRadius: wideCornerRadius,
            ))
        }

        /// Creates a configuration where corner radius is calculated dynamically based on view's dimensions.
        ///
        /// - Parameter maxRadius: Upper limit for corner radius. Pass `0` for no limit.
        public static func capsule(maxRadius: CGFloat = 18) -> Corners {
            Corners(style: .capsule(maxRadius: maxRadius))
        }

        /// Does a quick check if corner configuration has uniform corners and returns corner radius if it does.
        ///
        /// - Returns Corner radius if corners are uniform, otherwise returns `nil`.
        ///
        /// It more performant to set `CALayer.cornerRadius` instead of doing a mask layer.
        /// This method is design to help with that.
        public func uniformCornerRadius(for rect: CGRect) -> CGFloat? {
            if case .segmented = style {
                return nil
            }
            return radius(for: .topLeft, in: rect)
        }

        /// - Returns Radius for a specific corner for a given view rectangle.
        public func radius(for corner: UIRectCorner, in rect: CGRect) -> CGFloat {
            switch style {
            case .uniform(let radius):
                return min(radius, rect.size.smallerAxis / 2)

            case .segmented(let sharpCorners, let sharpCornerRadius, let wideCornerRadius):
                return sharpCorners.contains(corner) ? sharpCornerRadius : wideCornerRadius

            case .capsule(let maxRadius):
                let radius = rect.size.smallerAxis / 2
                return maxRadius > 0 ? min(maxRadius, radius) : radius
            }
        }
    }

    // MARK: - Stroke

    ///
    /// An object that contains description of chat bubble's outline (stroke).
    ///
    public struct Stroke {

        /// Stroke's color.
        public let color: UIColor

        /// Stroke width.
        ///
        /// Note that center of the stroke line lies on the edge of the bubble view.
        /// Therefore half of the width provided will be drawn inside of the view and another half - outside.
        public let width: CGFloat

        public init(color: UIColor, width: CGFloat) {
            self.color = color
            self.width = width
        }
    }

    // MARK: UIBezierPath conversions

    /// - Returns `UIBezierPath` describing bubble shape.
    ///
    /// Designed to allow callers to configure masking layers that match bubble shape..
    public func bubblePath(for rect: CGRect) -> UIBezierPath {
        switch corners.style {
        case .uniform:
            let cornerRadius = corners.radius(for: .topLeft, in: rect)
            return UIBezierPath(cgPath: CGPath(
                roundedRect: rect,
                cornerWidth: cornerRadius,
                cornerHeight: cornerRadius,
                transform: nil,
            ))

        case .segmented(let sharpCorners, let sharpCornerRadius, let wideCornerRadius):
            return UIBezierPath.roundedRect(
                rect,
                sharpCorners: sharpCorners,
                sharpCornerRadius: sharpCornerRadius,
                wideCornerRadius: wideCornerRadius,
            )

        case .capsule:
            let cornerRadius = corners.radius(for: .topLeft, in: rect)
            return UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius)
        }
    }

    /// - Returns `UIBezierPath` containing stroke path for the provided `UIRect`.
    /// Will return `nil` if `BubbleConfiguration` doesn't have stroke specified.
    ///
    /// Designed to work with `CAShapeLayer` to add stroke to chat bubbles.
    public func strokePath(for rect: CGRect) -> UIBezierPath? {
        guard stroke != nil else { return nil }

        return bubblePath(for: rect)
    }
}