chromium/ios/chrome/browser/ui/popup_menu/overflow_menu/overflow_menu_action_row.swift

// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import SwiftUI
import ios_chrome_common_ui_colors_swift

/// Custom toggle style for Overflow Menu Action rows, consisting of a circle
/// border when the toggle is off and a circle with checkmark when the toggle
/// is on.
struct OverflowMenuActionToggleStyle: ToggleStyle {
  static let onStyle = AnyShapeStyle(.tint)
  static let offStyle = AnyShapeStyle(Color.grey500)

  @ViewBuilder
  func makeBody(configuration: Configuration) -> some View {
    Button {
      configuration.isOn.toggle()
    } label: {
      Label {
        configuration.label
      } icon: {
        Image(systemName: configuration.isOn ? "checkmark.circle.fill" : "circle")
          .foregroundStyle(configuration.isOn ? Self.onStyle : Self.offStyle)
          .imageScale(.large)
      }
    }
    .overflowMenuActionToggleCompat()
  }
}

extension View {
  /// For whatever reason, in iOS 15, the button is not toggleable unless this
  /// `buttonStyle` is set. In iOS 16+, the `buttonStyle` is not necessary.
  fileprivate func overflowMenuActionToggleCompat() -> some View {
    if #available(iOS 16.0, *) {
      return self
    }
    return self.buttonStyle(.borderless)
  }
}

/// A view that displays an action in the overflow menu.
struct OverflowMenuActionRow: View {
  /// Remove some of the default padding on the row, as it is too large by
  /// default.
  private static let rowEndPadding: CGFloat = -4

  /// Add extra padding between the row content and move handle in edit mode.
  private static let editRowEndPadding: CGFloat = 8

  /// The size of the "N" IPH icon.
  private static let newLabelIconWidth: CGFloat = 15

  // The duration that the view's highlight should persist.
  private static let highlightDuration: DispatchTimeInterval = .seconds(2)

  /// The action for this row.
  @ObservedObject var action: OverflowMenuAction

  weak var metricsHandler: PopupMenuMetricsHandler?

  @Environment(\.editMode) var editMode

  private var isEditing: Bool {
    return editMode?.wrappedValue.isEditing ?? false
  }

  var body: some View {
    button
      .listRowBackground(background)
      .onChange(of: action.highlighted) { _ in
        guard action.automaticallyUnhighlight else {
          return
        }
        DispatchQueue.main.asyncAfter(deadline: .now() + Self.highlightDuration) {
          action.highlighted = false
        }
      }
      .onAppear {
        if action.highlighted && action.automaticallyUnhighlight {
          DispatchQueue.main.asyncAfter(deadline: .now() + Self.highlightDuration) {
            action.highlighted = false
          }
        }
      }
      .accessibilityIdentifier(action.accessibilityIdentifier)
      .disabled(!action.enabled || action.enterpriseDisabled)
      .if(!isEditing) { view in
        view.contextMenu {
          ForEach(action.longPressItems) { item in
            Section {
              Button {
                item.handler()
              } label: {
                Label(item.title, systemImage: item.symbolName)
              }
            }
          }
        }
      }
      .if(!action.useButtonStyling) { view in
        view.accentColor(.textPrimary)
      }
      .listRowSeparatorTint(.overflowMenuSeparator)
  }

  @ViewBuilder
  private var rowContent: some View {
    if isEditing {
      HStack {
        Toggle(isOn: $action.shown.animation()) {
          Text(action.name)
        }
        .toggleStyle(OverflowMenuActionToggleStyle())
        .labelStyle(.iconOnly)
        .tint(.chromeBlue)
        .accessibilityRemoveTraits(.isSelected)
        rowIcon
        centerTextView
        Spacer()
      }
      .padding([.trailing], Self.editRowEndPadding)
      .accessibilityElement(children: .combine)
    } else {
      HStack {
        // If there is no icon, the text should be centered.
        if rowIcon == nil {
          Spacer()
        }
        centerTextView
        if action.displayNewLabelIcon {
          newLabelIconView
        }
        Spacer()
        if let rowIcon = rowIcon {
          rowIcon
        }
      }
      .padding([.trailing], Self.rowEndPadding)
    }
  }

  /// The row's middle text content
  @ViewBuilder
  private var centerTextView: some View {
    VStack(alignment: .leading, spacing: 4) {
      name
      subtitle
    }
  }

  // The button view, which is replaced by just a plain view when this is in
  // edit mode.
  @ViewBuilder
  var button: some View {
    if isEditing {
      rowContent
    } else {
      Button(
        action: {
          metricsHandler?.popupMenuTookAction()
          metricsHandler?.popupMenuUserSelectedAction()
          action.handler()
        },
        label: {
          rowContent
            .contentShape(Rectangle())
        }
      )
      .if(action.useButtonStyling) { view in
        view.buttonStyle(.borderless)
      }
    }
  }

  private var name: some View {
    Text(action.name).lineLimit(2)
  }

  @ViewBuilder
  private var subtitle: some View {
    if let subtitle = action.subtitle {
      Text(subtitle).lineLimit(1).font(.caption).foregroundColor(.textTertiary)
    }
  }

  private var rowIcon: OverflowMenuRowIcon? {
    action.symbolName.flatMap { symbolName in
      OverflowMenuRowIcon(
        symbolName: symbolName, systemSymbol: action.systemSymbol,
        monochromeSymbol: action.monochromeSymbol)
    }
  }

  /// The background color for this row.
  var background: some View {
    let color =
      action.highlighted
      ? Color("destination_highlight_color") : Color(.secondarySystemGroupedBackground)
    // `.listRowBackground cannot be animated, so apply the animation to the color directly.
    return color.animation(.default)
  }

  // The "N" IPH icon view.
  private var newLabelIconView: some View {
    Image(systemName: "seal.fill")
      .resizable()
      .foregroundColor(.blue600)
      .frame(
        width: OverflowMenuActionRow.newLabelIconWidth,
        height: OverflowMenuActionRow.newLabelIconWidth
      )
      .overlay {
        if let newLabelString = L10nUtils.stringWithFixup(
          messageId: IDS_IOS_NEW_LABEL_FEATURE_BADGE)
        {
          Text(newLabelString)
            .font(.system(size: 10, weight: .bold, design: .rounded))
            .scaledToFit()
            .foregroundColor(.primaryBackground)
        }
      }
      .accessibilityIdentifier("overflowRowIPHBadgeIdentifier")
  }
}