|
|
|
|
@ -150,12 +150,10 @@ private class ContextMenuActionsView: UIView, UIGestureRecognizerDelegate, UIScr
|
|
|
|
|
|
|
|
|
|
private class ContextMenuActionRow: UIView {
|
|
|
|
|
let attributes: ContextMenuAction.Attributes
|
|
|
|
|
let hostEffect: UIBlurEffect
|
|
|
|
|
let forceDarkTheme: Bool
|
|
|
|
|
let visualEffectView: UIVisualEffectView
|
|
|
|
|
let titleLabel: UILabel
|
|
|
|
|
let iconView: UIImageView
|
|
|
|
|
let separatorView: UIView
|
|
|
|
|
var separatorView: UIView?
|
|
|
|
|
var highlightedView: UIView?
|
|
|
|
|
var isHighlighted: Bool {
|
|
|
|
|
didSet {
|
|
|
|
|
@ -163,7 +161,16 @@ private class ContextMenuActionsView: UIView, UIGestureRecognizerDelegate, UIScr
|
|
|
|
|
if isHighlighted {
|
|
|
|
|
if highlightedView == nil {
|
|
|
|
|
let view = UIView()
|
|
|
|
|
view.frame = bounds
|
|
|
|
|
|
|
|
|
|
if #available(iOS 26, *), FeatureFlags.iOS26SDKIsAvailable {
|
|
|
|
|
#if compiler(>=6.2)
|
|
|
|
|
view.frame = bounds.insetBy(dx: 12, dy: 4)
|
|
|
|
|
view.cornerConfiguration = .capsule()
|
|
|
|
|
#endif
|
|
|
|
|
} else {
|
|
|
|
|
view.frame = bounds
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
view.backgroundColor = forceDarkTheme || Theme.isDarkThemeEnabled
|
|
|
|
|
? UIColor(rgbHex: 0x787880).withAlphaComponent(0.32)
|
|
|
|
|
: UIColor(rgbHex: 0x787880).withAlphaComponent(0.16)
|
|
|
|
|
@ -182,16 +189,25 @@ private class ContextMenuActionsView: UIView, UIGestureRecognizerDelegate, UIScr
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var maxWidth: CGFloat = 250
|
|
|
|
|
let margin: CGFloat = 16
|
|
|
|
|
let margin: CGFloat = if #available(iOS 26, *), FeatureFlags.iOS26SDKIsAvailable {
|
|
|
|
|
24
|
|
|
|
|
} else {
|
|
|
|
|
16
|
|
|
|
|
}
|
|
|
|
|
let iconSpacing: CGFloat = 12
|
|
|
|
|
let verticalPadding: CGFloat = 23
|
|
|
|
|
let iconSize: CGFloat = 20
|
|
|
|
|
let iconSize: CGFloat = if #available(iOS 26, *), FeatureFlags.iOS26SDKIsAvailable {
|
|
|
|
|
24
|
|
|
|
|
} else {
|
|
|
|
|
20
|
|
|
|
|
}
|
|
|
|
|
let titleMaxLines = 2
|
|
|
|
|
|
|
|
|
|
public init(
|
|
|
|
|
title: String,
|
|
|
|
|
icon: UIImage?,
|
|
|
|
|
attributes: ContextMenuAction.Attributes,
|
|
|
|
|
hostBlurEffect: UIBlurEffect,
|
|
|
|
|
hostBlurEffect: UIBlurEffect?,
|
|
|
|
|
forceDarkTheme: Bool
|
|
|
|
|
) {
|
|
|
|
|
titleLabel = UILabel(frame: CGRect.zero)
|
|
|
|
|
@ -200,7 +216,6 @@ private class ContextMenuActionsView: UIView, UIGestureRecognizerDelegate, UIScr
|
|
|
|
|
titleLabel.numberOfLines = titleMaxLines
|
|
|
|
|
|
|
|
|
|
self.attributes = attributes
|
|
|
|
|
hostEffect = hostBlurEffect
|
|
|
|
|
self.forceDarkTheme = forceDarkTheme
|
|
|
|
|
|
|
|
|
|
/// when made a child of a UIVisualEffectView, UILabel text color is overridden, but a vibrancy effect is added.
|
|
|
|
|
@ -222,26 +237,34 @@ private class ContextMenuActionsView: UIView, UIGestureRecognizerDelegate, UIScr
|
|
|
|
|
iconView.contentMode = .scaleAspectFit
|
|
|
|
|
iconView.tintColor = titleLabel.textColor
|
|
|
|
|
|
|
|
|
|
separatorView = UIView()
|
|
|
|
|
separatorView.backgroundColor = forceDarkTheme || Theme.isDarkThemeEnabled
|
|
|
|
|
? UIColor(rgbHex: 0x545458).withAlphaComponent(0.6)
|
|
|
|
|
: UIColor(rgbHex: 0x3c3c43).withAlphaComponent(0.3)
|
|
|
|
|
separatorView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
|
|
|
isHighlighted = false
|
|
|
|
|
|
|
|
|
|
visualEffectView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: hostBlurEffect, style: .label))
|
|
|
|
|
super.init(frame: .zero)
|
|
|
|
|
|
|
|
|
|
addSubview(visualEffectView)
|
|
|
|
|
if makeLabelSubviewOfVisualEffectsView {
|
|
|
|
|
visualEffectView.contentView.addSubview(titleLabel)
|
|
|
|
|
if let hostBlurEffect {
|
|
|
|
|
let visualEffectView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: hostBlurEffect, style: .label))
|
|
|
|
|
addSubview(visualEffectView)
|
|
|
|
|
if makeLabelSubviewOfVisualEffectsView {
|
|
|
|
|
visualEffectView.contentView.addSubview(titleLabel)
|
|
|
|
|
} else {
|
|
|
|
|
addSubview(titleLabel)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let separatorView = UIView()
|
|
|
|
|
separatorView.backgroundColor = forceDarkTheme || Theme.isDarkThemeEnabled
|
|
|
|
|
? UIColor(rgbHex: 0x545458).withAlphaComponent(0.6)
|
|
|
|
|
: UIColor(rgbHex: 0x3c3c43).withAlphaComponent(0.3)
|
|
|
|
|
separatorView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
|
|
|
self.separatorView = separatorView
|
|
|
|
|
|
|
|
|
|
visualEffectView.contentView.addSubview(iconView)
|
|
|
|
|
visualEffectView.contentView.addSubview(separatorView)
|
|
|
|
|
|
|
|
|
|
visualEffectView.autoPinEdgesToSuperviewEdges()
|
|
|
|
|
} else {
|
|
|
|
|
addSubview(titleLabel)
|
|
|
|
|
addSubview(iconView)
|
|
|
|
|
}
|
|
|
|
|
visualEffectView.contentView.addSubview(iconView)
|
|
|
|
|
visualEffectView.contentView.addSubview(separatorView)
|
|
|
|
|
|
|
|
|
|
visualEffectView.autoPinEdgesToSuperviewEdges()
|
|
|
|
|
|
|
|
|
|
isAccessibilityElement = true
|
|
|
|
|
accessibilityLabel = titleLabel.text
|
|
|
|
|
@ -263,7 +286,7 @@ private class ContextMenuActionsView: UIView, UIGestureRecognizerDelegate, UIScr
|
|
|
|
|
titleFrame.y = ceil((bounds.height - titleFrame.height) / 2)
|
|
|
|
|
iconViewFrame.y = max(0, (bounds.height - iconViewFrame.height) / 2)
|
|
|
|
|
|
|
|
|
|
let titleWidth = bounds.width - iconViewFrame.width - 3 * margin
|
|
|
|
|
let titleWidth = bounds.width - iconViewFrame.width - (2 * margin) - iconSpacing
|
|
|
|
|
if titleWidth < titleFrame.width {
|
|
|
|
|
// Give it more height for a second line.
|
|
|
|
|
let originalHeight = titleLabel.textRect(forBounds: CGRect.infinite, limitedToNumberOfLines: 1).height
|
|
|
|
|
@ -280,27 +303,35 @@ private class ContextMenuActionsView: UIView, UIGestureRecognizerDelegate, UIScr
|
|
|
|
|
}
|
|
|
|
|
titleFrame.width = titleWidth
|
|
|
|
|
|
|
|
|
|
if !isRTL {
|
|
|
|
|
let iconIsToTheRightOfText = if #available(iOS 26, *), FeatureFlags.iOS26SDKIsAvailable {
|
|
|
|
|
isRTL
|
|
|
|
|
} else {
|
|
|
|
|
!isRTL
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if iconIsToTheRightOfText {
|
|
|
|
|
titleFrame.x = margin
|
|
|
|
|
iconViewFrame.x = titleFrame.maxX + margin
|
|
|
|
|
iconViewFrame.x = titleFrame.maxX + iconSpacing
|
|
|
|
|
} else {
|
|
|
|
|
iconViewFrame.x = margin
|
|
|
|
|
titleFrame.x = iconViewFrame.maxX + margin
|
|
|
|
|
titleFrame.x = iconViewFrame.maxX + iconSpacing
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
titleLabel.frame = titleFrame
|
|
|
|
|
iconView.frame = iconViewFrame
|
|
|
|
|
|
|
|
|
|
var separatorFrame = bounds
|
|
|
|
|
separatorFrame.height = 1.0 / UIScreen.main.scale
|
|
|
|
|
separatorFrame.y = bounds.maxY - separatorFrame.height
|
|
|
|
|
separatorView.frame = separatorFrame
|
|
|
|
|
if let separatorView {
|
|
|
|
|
var separatorFrame = bounds
|
|
|
|
|
separatorFrame.height = 1.0 / UIScreen.main.scale
|
|
|
|
|
separatorFrame.y = bounds.maxY - separatorFrame.height
|
|
|
|
|
separatorView.frame = separatorFrame
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override func sizeThatFits(
|
|
|
|
|
_ size: CGSize
|
|
|
|
|
) -> CGSize {
|
|
|
|
|
let height = ceil(titleLabel.sizeThatFits(CGSize(width: maxWidth - 3 * margin - iconSize, height: 0)).height) + verticalPadding
|
|
|
|
|
let height = ceil(titleLabel.sizeThatFits(CGSize(width: maxWidth - (2 * margin) - iconSpacing - iconSize, height: 0)).height) + verticalPadding
|
|
|
|
|
return CGSize(width: maxWidth, height: height)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@ -326,7 +357,17 @@ private class ContextMenuActionsView: UIView, UIGestureRecognizerDelegate, UIScr
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let cornerRadius: CGFloat = 12
|
|
|
|
|
let cornerRadius: CGFloat = if #available(iOS 26, *), FeatureFlags.iOS26SDKIsAvailable {
|
|
|
|
|
33
|
|
|
|
|
} else {
|
|
|
|
|
12
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let vMargin: CGFloat = if #available(iOS 26, *), FeatureFlags.iOS26SDKIsAvailable {
|
|
|
|
|
10
|
|
|
|
|
} else {
|
|
|
|
|
0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public init(
|
|
|
|
|
menu: ContextMenu,
|
|
|
|
|
@ -336,15 +377,36 @@ private class ContextMenuActionsView: UIView, UIGestureRecognizerDelegate, UIScr
|
|
|
|
|
self.forceDarkTheme = forceDarkTheme
|
|
|
|
|
|
|
|
|
|
scrollView = UIScrollView(frame: CGRect.zero)
|
|
|
|
|
let effect: UIBlurEffect = .init(style: .systemThinMaterial)
|
|
|
|
|
backdropView = UIVisualEffectView(effect: effect)
|
|
|
|
|
backdropView.backgroundColor = Theme.isDarkThemeEnabled || forceDarkTheme
|
|
|
|
|
? .ows_blackAlpha80
|
|
|
|
|
: .ows_whiteAlpha40
|
|
|
|
|
scrollView.verticalScrollIndicatorInsets = .init(hMargin: 0, vMargin: cornerRadius)
|
|
|
|
|
|
|
|
|
|
let blurEffect: UIBlurEffect?
|
|
|
|
|
if #available(iOS 26, *), FeatureFlags.iOS26SDKIsAvailable {
|
|
|
|
|
#if compiler(>=6.2)
|
|
|
|
|
let effect = UIGlassEffect(style: .clear)
|
|
|
|
|
effect.isInteractive = true
|
|
|
|
|
backdropView = UIVisualEffectView(effect: effect)
|
|
|
|
|
#else
|
|
|
|
|
// will never execute because of the FeatureFlags.iOS26SDKIsAvailable check above, but needed for the Xcode 16 compiler to not get mad. Remove when CI is all Xcode 26
|
|
|
|
|
backdropView = UIVisualEffectView(effect: UIBlurEffect(style: .systemThinMaterial))
|
|
|
|
|
#endif
|
|
|
|
|
blurEffect = nil
|
|
|
|
|
} else {
|
|
|
|
|
blurEffect = UIBlurEffect(style: .systemThinMaterial)
|
|
|
|
|
backdropView = UIVisualEffectView(effect: blurEffect)
|
|
|
|
|
backdropView.backgroundColor = Theme.isDarkThemeEnabled || forceDarkTheme
|
|
|
|
|
? .ows_blackAlpha80
|
|
|
|
|
: .ows_whiteAlpha40
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var actionViews: [ContextMenuActionRow] = []
|
|
|
|
|
for action in menu.children {
|
|
|
|
|
let actionView = ContextMenuActionRow(title: action.title, icon: action.image, attributes: action.attributes, hostBlurEffect: effect, forceDarkTheme: forceDarkTheme)
|
|
|
|
|
let actionView = ContextMenuActionRow(
|
|
|
|
|
title: action.title,
|
|
|
|
|
icon: action.image,
|
|
|
|
|
attributes: action.attributes,
|
|
|
|
|
hostBlurEffect: blurEffect,
|
|
|
|
|
forceDarkTheme: forceDarkTheme
|
|
|
|
|
)
|
|
|
|
|
actionViews.append(actionView)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -365,11 +427,14 @@ private class ContextMenuActionsView: UIView, UIGestureRecognizerDelegate, UIScr
|
|
|
|
|
addGestureRecognizer(highlightHoverGestureRecognizer)
|
|
|
|
|
self.highlightHoverGestureRecognizer = highlightHoverGestureRecognizer
|
|
|
|
|
|
|
|
|
|
layer.cornerRadius = cornerRadius
|
|
|
|
|
layer.shadowRadius = 64
|
|
|
|
|
layer.shadowOffset = CGSize(width: 0, height: 32)
|
|
|
|
|
layer.shadowColor = UIColor.ows_black.cgColor
|
|
|
|
|
layer.shadowOpacity = 0.2
|
|
|
|
|
if #available(iOS 26, *), FeatureFlags.iOS26SDKIsAvailable {
|
|
|
|
|
} else {
|
|
|
|
|
layer.cornerRadius = cornerRadius
|
|
|
|
|
layer.shadowRadius = 64
|
|
|
|
|
layer.shadowOffset = CGSize(width: 0, height: 32)
|
|
|
|
|
layer.shadowColor = UIColor.ows_black.cgColor
|
|
|
|
|
layer.shadowOpacity = 0.2
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
backdropView.layer.cornerRadius = cornerRadius
|
|
|
|
|
backdropView.layer.masksToBounds = true
|
|
|
|
|
@ -382,7 +447,7 @@ private class ContextMenuActionsView: UIView, UIGestureRecognizerDelegate, UIScr
|
|
|
|
|
|
|
|
|
|
scrollView.delegate = self
|
|
|
|
|
|
|
|
|
|
actionViews.last?.separatorView.isHidden = true
|
|
|
|
|
actionViews.last?.separatorView?.isHidden = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
required init?(coder: NSCoder) {
|
|
|
|
|
@ -396,7 +461,7 @@ private class ContextMenuActionsView: UIView, UIGestureRecognizerDelegate, UIScr
|
|
|
|
|
|
|
|
|
|
backdropView.frame = bounds
|
|
|
|
|
scrollView.frame = bounds
|
|
|
|
|
var yOffset: CGFloat = 0
|
|
|
|
|
var yOffset: CGFloat = vMargin
|
|
|
|
|
var maxY: CGFloat = 0
|
|
|
|
|
var width: CGFloat = 0.0
|
|
|
|
|
for actionView in actionViews {
|
|
|
|
|
@ -406,6 +471,7 @@ private class ContextMenuActionsView: UIView, UIGestureRecognizerDelegate, UIScr
|
|
|
|
|
yOffset += size.height
|
|
|
|
|
maxY = max(maxY, actionView.frame.maxY)
|
|
|
|
|
}
|
|
|
|
|
maxY += vMargin
|
|
|
|
|
|
|
|
|
|
scrollView.contentSize = CGSize(width: width, height: maxY)
|
|
|
|
|
}
|
|
|
|
|
@ -414,7 +480,7 @@ private class ContextMenuActionsView: UIView, UIGestureRecognizerDelegate, UIScr
|
|
|
|
|
_ size: CGSize
|
|
|
|
|
) -> CGSize {
|
|
|
|
|
// every entry may have a different height
|
|
|
|
|
var height = 0.0
|
|
|
|
|
var height = vMargin * 2
|
|
|
|
|
var width = 0.0
|
|
|
|
|
for actionView in actionViews {
|
|
|
|
|
let size = actionView.sizeThatFits(size)
|
|
|
|
|
|