// // ToolbarController.swift // ComposeUI // // Created by Shadowfacts on 3/7/23. // import SwiftUI import Pachyderm import TuskerComponents class ToolbarController: ViewController { static let height: CGFloat = 44 private static let visibilityOptions: [MenuPicker.Option] = Pachyderm.Visibility.allCases.map { vis in .init(value: vis, title: vis.displayName, subtitle: vis.subtitle, image: UIImage(systemName: vis.unfilledImageName), accessibilityLabel: "Visibility: \(vis.displayName)") } unowned let parent: ComposeController @Published var minWidth: CGFloat? @Published var realWidth: CGFloat? init(parent: ComposeController) { self.parent = parent } var view: some View { ToolbarView() } func showEmojiPicker() { guard parent.currentInput?.autocompleteState == nil else { return } parent.shouldEmojiAutocompletionBeginExpanded = true parent.currentInput?.beginAutocompletingEmoji() } func formatAction(_ format: StatusFormat) -> () -> Void { { [weak self] in self?.parent.currentInput?.applyFormat(format) } } struct ToolbarView: View { @EnvironmentObject private var draft: Draft @EnvironmentObject private var controller: ToolbarController @EnvironmentObject private var composeController: ComposeController @ScaledMetric(relativeTo: .body) private var imageSize: CGFloat = 22 @State private var minWidth: CGFloat? @State private var realWidth: CGFloat? var body: some View { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 0) { cwButton MenuPicker(selection: $draft.visibility, options: ToolbarController.visibilityOptions, buttonStyle: .iconOnly) // the button has a bunch of extra space by default, but combined with what we add it's too much .padding(.horizontal, -8) if composeController.mastodonController.instanceFeatures.localOnlyPosts { localOnlyPicker .padding(.horizontal, -8) } if let currentInput = composeController.currentInput, currentInput.toolbarElements.contains(.emojiPicker) { customEmojiButton } if let currentInput = composeController.currentInput, currentInput.toolbarElements.contains(.formattingButtons), composeController.config.contentType != .plain { Spacer() formatButtons } Spacer() } .padding(.horizontal, 16) .frame(minWidth: minWidth) .background(GeometryReader { proxy in Color.clear .preference(key: ToolbarWidthPrefKey.self, value: proxy.size.width) .onPreferenceChange(ToolbarWidthPrefKey.self) { width in realWidth = width } }) } .scrollDisabledIfAvailable(realWidth ?? 0 <= minWidth ?? 0) .frame(height: ToolbarController.height) .frame(maxWidth: .infinity) .background(.regularMaterial, ignoresSafeAreaEdges: .bottom) .overlay(alignment: .top) { Divider() } .background(GeometryReader { proxy in Color.clear .preference(key: ToolbarWidthPrefKey.self, value: proxy.size.width) .onPreferenceChange(ToolbarWidthPrefKey.self) { width in minWidth = width } }) } private var cwButton: some View { Button("CW", action: controller.parent.toggleContentWarning) .accessibilityLabel(draft.contentWarningEnabled ? "Remove content warning" : "Add content warning") .padding(5) .hoverEffect() } private var localOnlyPicker: some View { let domain = composeController.mastodonController.accountInfo!.instanceURL.host! return MenuPicker(selection: $draft.localOnly, options: [ .init(value: true, title: "Local-only", subtitle: "Only \(domain)", image: UIImage(named: "link.broken")), .init(value: false, title: "Federated", image: UIImage(systemName: "link")), ], buttonStyle: .iconOnly) } private var customEmojiButton: some View { Button(action: controller.showEmojiPicker) { Label("Insert custom emoji", systemImage: "face.smiling") } .labelStyle(.iconOnly) .font(.system(size: imageSize)) .padding(5) .hoverEffect() .transition(.opacity.animation(.linear(duration: 0.2))) } private var formatButtons: some View { ForEach(StatusFormat.allCases, id: \.rawValue) { format in Button(action: controller.formatAction(format)) { if let imageName = format.imageName { Image(systemName: imageName) .font(.system(size: imageSize)) } else if let (str, attrs) = format.title { let container = try! AttributeContainer(attrs, including: \.uiKit) Text(AttributedString(str, attributes: container)) } } .accessibilityLabel(format.accessibilityLabel) .padding(5) .hoverEffect() .transition(.opacity.animation(.linear(duration: 0.2))) } } } } private struct ToolbarWidthPrefKey: PreferenceKey { static var defaultValue: CGFloat? = nil static func reduce(value: inout CGFloat?, nextValue: () -> CGFloat?) { value = nextValue() } }