Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,28 @@ final class AccessoryActionButton: UIButton {
/// The configurable item this button triggers.
let item: ResolvedToolbarItem

/// Whether this modifier is double-tap *sticky-locked* (vs. single-tap armed).
///
/// A sticky-locked modifier stays applied to every keystroke until the user
/// taps it off, whereas an armed modifier is consumed by the next key. On
/// iOS 26 both states share the same prominent-glass blue fill, so the lock
/// needs its own visual cue: a white capsule border drawn on the button's
/// layer, *over* the glass, mirroring the 2pt white stroke the pre-26 flat
/// style already used for the locked state. The border is drawn at the layer
/// level (not via `UIButton.Configuration.background.strokeColor`) so it
/// composites on top of Liquid Glass regardless of how the glass material
/// renders its own background, and adds zero intrinsic width so it does not
/// fight the bar's min-width sizing.
var isStickyLocked = false {
didSet {
guard oldValue != isStickyLocked else { return }
updateStickyLockBorder()
}
}

/// Width of the sticky-lock capsule border, matching the pre-26 flat stroke.
private static let stickyLockBorderWidth: CGFloat = 2

/// Creates a button bound to a resolved toolbar item.
/// - Parameter item: The built-in or custom action the button represents.
init(item: ResolvedToolbarItem) {
Expand All @@ -24,4 +46,28 @@ final class AccessoryActionButton: UIButton {
required init?(coder: NSCoder) {
fatalError("init(coder:) is not supported")
}

override func layoutSubviews() {
super.layoutSubviews()
// Keep the lock border a true capsule that hugs the glass pill as the
// button's bounds settle (height is fixed, but the corner radius is
// derived here so the border tracks any future sizing change).
updateStickyLockBorder()
}

/// Sync the layer-level white capsule border to ``isStickyLocked``.
///
/// Always clears the border when not locked, so a button that transitions
/// locked → armed → resting never keeps a stale border.
private func updateStickyLockBorder() {
if isStickyLocked {
layer.cornerRadius = bounds.height / 2
layer.cornerCurve = .continuous
layer.borderColor = UIColor.white.cgColor
layer.borderWidth = Self.stickyLockBorderWidth
} else {
layer.borderWidth = 0
layer.borderColor = nil
}
Comment on lines +68 to +71

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Stale cornerRadius and cornerCurve after unlocking — the else branch clears the border properties but never resets layer.cornerRadius or layer.cornerCurve. Once a button transitions from locked → armed → resting, the root CALayer permanently retains a capsule cornerRadius. If UIKit's glass configuration ever sets masksToBounds on the root layer (or if a future UIKit update does), the stale radius would clip the glass content to a capsule shape for the button's lifetime.

Suggested change
} else {
layer.borderWidth = 0
layer.borderColor = nil
}
} else {
layer.borderWidth = 0
layer.borderColor = nil
layer.cornerRadius = 0
layer.cornerCurve = .circular
}

Rule Used: Flag Swift fixes that patch symptoms while leaving... (source)

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,20 @@ final class TerminalInputTextView: UITextView {
}
config.contentInsets = Self.accessoryButtonContentInsets
button.configuration = config
if let actionButton = button as? AccessoryActionButton {
// On iOS 26 the armed and sticky states share the same
// prominent-glass blue fill, so the double-tap *lock* is
// distinguished by a white capsule border drawn over the glass (see
// ``AccessoryActionButton/isStickyLocked``). On earlier OSes the
// flat style already renders the locked white stroke through the
// background configuration, so the layer border stays off to avoid
// a doubled stroke.
if #available(iOS 26.0, *) {
actionButton.isStickyLocked = sticky
} else {
actionButton.isStickyLocked = false
}
}
}

private static func accessoryButtonConfiguration(armed: Bool, sticky: Bool) -> UIButton.Configuration {
Expand Down
Loading