tauri-plugin-system-components 0.1.1

Native system UI components for Tauri 2 — native iOS tab bar over the webview, native controls, and glass window backgrounds on macOS/iOS.
Documentation
//
//  ButtonComponent.swift
//  tauri-plugin-system-components
//

import UIKit

/// UIButton — glass configuration on iOS 26, filled/gray on iOS 15, plain
/// UIButton on iOS 14. Emits `click`.
enum ButtonComponent: ComponentBuilder {
    static func make(_ args: CreateComponentArgs, _ ctx: ComponentContext) -> UIView? {
        let props = args.props
        let id = args.id
        let emit = ctx.emit

        // A circular bitmap (an avatar) renders full-bleed, edge to edge — not
        // as a padded configuration image.
        if (props?.circular ?? false), let b64 = props?.image, let decoded = ImageUtil.decode(b64) {
            let control = UIButton(type: .custom)
            control.clipsToBounds = true
            let side = CGFloat(props?.width ?? props?.height ?? 50)
            control.layer.cornerRadius = side / 2
            control.setImage(decoded, for: .normal)
            control.imageView?.contentMode = .scaleAspectFill
            control.contentHorizontalAlignment = .fill
            control.contentVerticalAlignment = .fill
            control.addAction(
                UIAction { _ in emit(id, "click", nil, nil, nil) }, for: .touchUpInside)
            return control
        }

        // UIButton.Configuration (and the glass styles) are iOS 15 / 26; iOS 14
        // gets a plain UIButton with the same title/icon/tint.
        if #available(iOS 15.0, *) {
            var config: UIButton.Configuration
            // The glass button configurations only exist in the iOS 26 SDK
            // (Xcode 26 / Swift 6.2); compile the filled/gray fallback when
            // building against an older SDK that can't see those symbols.
            #if compiler(>=6.2)
            if #available(iOS 26.0, *) {
                config = (props?.prominent ?? false)
                    ? UIButton.Configuration.prominentGlass()
                    : UIButton.Configuration.glass()
            } else {
                config = (props?.prominent ?? false)
                    ? UIButton.Configuration.filled()
                    : UIButton.Configuration.gray()
            }
            #else
            config = (props?.prominent ?? false)
                ? UIButton.Configuration.filled()
                : UIButton.Configuration.gray()
            #endif
            config.title = props?.label
            if let b64 = props?.image, let decoded = ImageUtil.decode(b64) {
                config.image = ImageUtil.icon(decoded, side: 20, circular: props?.circular ?? false)
                config.imagePadding = 6
            } else if let symbol = props?.sfSymbol {
                config.image = UIImage(systemName: symbol)
                config.imagePadding = 6
            }
            if let tint = props?.tint.flatMap(ColorUtil.from(hex:)) {
                config.baseBackgroundColor = tint
            }
            let control = UIButton(configuration: config)
            control.addAction(
                UIAction { _ in emit(id, "click", nil, nil, nil) }, for: .touchUpInside)
            return control
        } else {
            let control = UIButton(type: .system)
            control.setTitle(props?.label, for: .normal)
            if let b64 = props?.image, let decoded = ImageUtil.decode(b64) {
                control.setImage(
                    ImageUtil.icon(decoded, side: 20, circular: props?.circular ?? false),
                    for: .normal)
            } else if let symbol = props?.sfSymbol {
                control.setImage(UIImage(systemName: symbol), for: .normal)
            }
            if let tint = props?.tint.flatMap(ColorUtil.from(hex:)) {
                control.backgroundColor = tint
            }
            control.addAction(
                UIAction { _ in emit(id, "click", nil, nil, nil) }, for: .touchUpInside)
            return control
        }
    }

    static func update(_ control: UIView, _ props: ComponentPropsArgs) {
        guard let button = control as? UIButton else { return }
        if #available(iOS 15.0, *) {
            var config = button.configuration
            if let label = props.label { config?.title = label }
            if let b64 = props.image, let decoded = ImageUtil.decode(b64) {
                config?.image = ImageUtil.icon(decoded, side: 20, circular: props.circular ?? false)
            }
            button.configuration = config
        } else {
            if let label = props.label { button.setTitle(label, for: .normal) }
            if let b64 = props.image, let decoded = ImageUtil.decode(b64) {
                button.setImage(
                    ImageUtil.icon(decoded, side: 20, circular: props.circular ?? false),
                    for: .normal)
            }
        }
    }
}