// // ToolbarController.swift // ComposeUI // // Created by Shadowfacts on 3/7/23. // import SwiftUI import Pachyderm import TuskerComponents class ToolbarController: ViewController { static let height: CGFloat = 44 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 #if !os(visionOS) @State private var minWidth: CGFloat? @State private var realWidth: CGFloat? #endif var body: some View { #if os(visionOS) buttons #else ScrollView(.horizontal, showsIndicators: false) { buttons .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, .leading, .trailing]) .overlay(alignment: .top) { Divider() .edgesIgnoringSafeArea([.leading, .trailing]) } .background(GeometryReader { proxy in Color.clear .preference(key: ToolbarWidthPrefKey.self, value: proxy.size.width) .onPreferenceChange(ToolbarWidthPrefKey.self) { width in minWidth = width } }) #endif } @ViewBuilder private var buttons: some View { HStack(spacing: 0) { cwButton MenuPicker(selection: visibilityBinding, options: visibilityOptions, buttonStyle: .iconOnly) #if !targetEnvironment(macCatalyst) && !os(visionOS) // the button has a bunch of extra space by default, but combined with what we add it's too much .padding(.horizontal, -8) #endif .disabled(draft.editedStatusID != nil) .disabled(composeController.mastodonController.instanceFeatures.localOnlyPostsVisibility && draft.localOnly) if composeController.mastodonController.instanceFeatures.localOnlyPosts { localOnlyPicker #if targetEnvironment(macCatalyst) .padding(.leading, 4) #elseif !os(visionOS) .padding(.horizontal, -8) #endif .disabled(draft.editedStatusID != nil) } 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() if #available(iOS 16.0, *), composeController.mastodonController.instanceFeatures.createStatusWithLanguage { LanguagePicker(draftLanguage: $draft.language, hasChangedSelection: $composeController.hasChangedLanguageSelection) } } } 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 visibilityBinding: Binding { // On instances that conflate visibliity and local only, we still show two separate controls but don't allow // changing the visibility when local-only. if draft.localOnly, composeController.mastodonController.instanceFeatures.localOnlyPostsVisibility { return .constant(.public) } else { return $draft.visibility } } private var visibilityOptions: [MenuPicker.Option] { let visibilities: [Pachyderm.Visibility] if !composeController.mastodonController.instanceFeatures.composeDirectStatuses { visibilities = [.public, .unlisted, .private] } else { visibilities = Pachyderm.Visibility.allCases } return visibilities.map { vis in .init(value: vis, title: vis.displayName, subtitle: vis.subtitle, image: UIImage(systemName: vis.unfilledImageName), accessibilityLabel: "Visibility: \(vis.displayName)") } } 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() } }