Skip to content
Open
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 @@ -10,6 +10,7 @@ import SwiftCrossUI
struct GreetingGeneratorApp: App {
@State var name = ""
@State var greetings: [String] = []
@State var isGreetingSelectable = false

var body: some Scene {
WindowGroup("Greeting Generator") {
Expand All @@ -26,9 +27,11 @@ struct GreetingGeneratorApp: App {
}
}

Toggle("Selectable Greeting", active: $isGreetingSelectable)
if let latest = greetings.last {
Text(latest)
.padding(.top, 5)
.textSelectionEnabled(isGreetingSelectable)

if greetings.count > 1 {
Text("History:")
Expand Down
1 change: 1 addition & 0 deletions Sources/AppKitBackend/AppKitBackend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,7 @@ public final class AppKitBackend: AppBackend {
) {
let field = textView as! NSTextField
field.attributedStringValue = Self.attributedString(for: content, in: environment)
field.isSelectable = environment.isTextSelectionEnabled
}

public func createButton() -> Widget {
Expand Down
2 changes: 1 addition & 1 deletion Sources/GtkBackend/GtkBackend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@
return 0
#else
if window.showMenuBar {
// TODO: Don't hardcode this (if possible), because some Gtk

Check warning on line 159 in Sources/GtkBackend/GtkBackend.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Todo Violation: TODOs should be resolved (Don't hardcode this (if possib...) (todo)
// themes may affect the height of the menu bar.
25
} else {
Expand All @@ -172,7 +172,7 @@
}

public func isWindowProgrammaticallyResizable(_ window: Window) -> Bool {
// TODO: Detect whether window is fullscreen

Check warning on line 175 in Sources/GtkBackend/GtkBackend.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Todo Violation: TODOs should be resolved (Detect whether window is fulls...) (todo)
return true
}

Expand Down Expand Up @@ -376,14 +376,14 @@
}

public func setRootEnvironmentChangeHandler(to action: @escaping () -> Void) {
// TODO: React to theme changes

Check warning on line 379 in Sources/GtkBackend/GtkBackend.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Todo Violation: TODOs should be resolved (React to theme changes) (todo)
}

public func computeWindowEnvironment(
window: Window,
rootEnvironment: EnvironmentValues
) -> EnvironmentValues {
// TODO: Record window scale factor in here

Check warning on line 386 in Sources/GtkBackend/GtkBackend.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Todo Violation: TODOs should be resolved (Record window scale factor in ...) (todo)
rootEnvironment
}

Expand All @@ -391,7 +391,7 @@
of window: Window,
to action: @escaping () -> Void
) {
// TODO: Notify when window scale factor changes

Check warning on line 394 in Sources/GtkBackend/GtkBackend.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Todo Violation: TODOs should be resolved (Notify when window scale facto...) (todo)
}

public func setIncomingURLHandler(to action: @escaping (URL) -> Void) {
Expand Down Expand Up @@ -606,7 +606,7 @@
case .trailing:
Justification.right
}

textView.selectable = environment.isTextSelectionEnabled
textView.css.clear()
textView.css.set(properties: Self.cssProperties(for: environment))
}
Expand All @@ -633,7 +633,7 @@
return imageView
}

public func updateImageView(

Check warning on line 636 in Sources/GtkBackend/GtkBackend.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Function Parameter Count Violation: Function should have 5 parameters or less: it currently has 8 (function_parameter_count)
_ imageView: Widget,
rgbaData: [UInt8],
width: Int,
Expand Down Expand Up @@ -673,7 +673,7 @@

// private let tables = Tables()

// TODO: Implement tables

Check warning on line 676 in Sources/GtkBackend/GtkBackend.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Todo Violation: TODOs should be resolved (Implement tables) (todo)
// public func createTable(rows: Int, columns: Int) -> Widget {
// let widget = Grid()

Expand Down Expand Up @@ -831,7 +831,7 @@
return scale
}

public func updateSlider(

Check warning on line 834 in Sources/GtkBackend/GtkBackend.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Function Parameter Count Violation: Function should have 5 parameters or less: it currently has 6 (function_parameter_count)
_ slider: Widget,
minimum: Double,
maximum: Double,
Expand Down Expand Up @@ -1166,7 +1166,7 @@
}
},
window: window ?? windows[0]
) { result in

Check warning on line 1169 in Sources/GtkBackend/GtkBackend.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Multiple Closures with Trailing Closure Violation: Trailing closure syntax should not be used when passing more than one closure argument (multiple_closures_with_trailing_closure)
switch result {
case .success(let urls):
handleResult(.success(urls[0]))
Expand Down Expand Up @@ -1199,7 +1199,7 @@
configure(chooser)

chooser.registerSignals()
chooser.response = { (_: NativeDialog, response: Int) -> Void in

Check warning on line 1202 in Sources/GtkBackend/GtkBackend.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Redundant Void Return Violation: Returning Void in a function declaration is redundant (redundant_void_return)
// Release our intentional retain cycle which ironically only exists
// because of this line. The retain cycle keeps the file chooser
// around long enough for the user to respond (it gets released
Expand Down
3 changes: 3 additions & 0 deletions Sources/SwiftCrossUI/Environment/EnvironmentValues.swift
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ public struct EnvironmentValues {
/// The style of toggle to use.
public var toggleStyle: ToggleStyle

/// Wether a Text should be selectable. Set by ``View/textSelectionEnabled(_:)``.
public var isTextSelectionEnabled: Bool = false

// Backing storage for extensible subscript
private var extraValues: [ObjectIdentifier: Any]

Expand Down
12 changes: 12 additions & 0 deletions Sources/SwiftCrossUI/Views/Modifiers/TextSelectionModifier.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
extension View {
/// Set SwiftUI.Text selectability
public func textSelectionEnabled(_ isEnabled: Bool = true) -> some View {
EnvironmentModifier(
self,
modification: { environment in
var newEnvironment = environment
newEnvironment.isTextSelectionEnabled = isEnabled
return newEnvironment
})
}
}
67 changes: 65 additions & 2 deletions Sources/UIKitBackend/UIKitBackend+Passive.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
}

public func createTextView() -> Widget {
let widget = WrapperWidget<UILabel>()
let widget = WrapperWidget<OptionallySelectableLabel>()
widget.child.numberOfLines = 0
Copy link
Contributor

Choose a reason for hiding this comment

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

Does the subclass do something that makes it unnecessary? With plain UILabel, forgetting this means multiline text doesn't work, and I don't see anywhere in the subclass that sets this property.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you, I read the description of this property wrong, leading to me thinking 0 was the default value.

return widget
}
Expand All @@ -46,12 +46,13 @@
content: String,
environment: EnvironmentValues
) {
let wrapper = textView as! WrapperWidget<UILabel>
let wrapper = textView as! WrapperWidget<OptionallySelectableLabel>
wrapper.child.overrideUserInterfaceStyle = environment.colorScheme.userInterfaceStyle
wrapper.child.attributedText = UIKitBackend.attributedString(
text: content,
environment: environment
)
wrapper.child.isSelectable = environment.isTextSelectionEnabled
}

public func size(
Expand Down Expand Up @@ -104,3 +105,65 @@
wrapper.child.image = .init(ciImage: ciImage)
}
}

// Inspired by https://medium.com/kinandcartacreated/making-uilabel-accessible-5f3d5c342df4
// Thank you to Sam Dods for the base idea
final class OptionallySelectableLabel: UILabel {
var isSelectable: Bool = false

override init(frame: CGRect) {
super.init(frame: frame)
setupTextSelection()
}

required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupTextSelection()
}

override var canBecomeFirstResponder: Bool {
isSelectable
}

private func setupTextSelection() {
let longPress = UILongPressGestureRecognizer(target: self, action: #selector(didLongPress))
addGestureRecognizer(longPress)
isUserInteractionEnabled = true
}

@objc private func didLongPress(_ gesture: UILongPressGestureRecognizer) {
guard isSelectable, gesture.state == .began, let text = self.attributedText?.string,
!text.isEmpty
else {
return
}
window?.endEditing(true)
guard becomeFirstResponder() else { return }

let menu = UIMenuController.shared

Check warning on line 143 in Sources/UIKitBackend/UIKitBackend+Passive.swift

View workflow job for this annotation

GitHub Actions / uikit (Vision)

'UIMenuController' was deprecated in visionOS 1.0: UIMenuController is deprecated. Use UIEditMenuInteraction instead.
if !menu.isMenuVisible {
menu.showMenu(from: self, rect: textRect())
}
}

override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
return action == #selector(copy(_:))
}

private func textRect() -> CGRect {
let inset: CGFloat = -4
return textRect(forBounds: bounds, limitedToNumberOfLines: numberOfLines).insetBy(
dx: inset, dy: inset)
}

private func cancelSelection() {
let menu = UIMenuController.shared

Check warning on line 160 in Sources/UIKitBackend/UIKitBackend+Passive.swift

View workflow job for this annotation

GitHub Actions / uikit (Vision)

'UIMenuController' was deprecated in visionOS 1.0: UIMenuController is deprecated. Use UIEditMenuInteraction instead.
menu.hideMenu(from: self)
}

@objc override func copy(_ sender: Any?) {
cancelSelection()
let board = UIPasteboard.general
board.string = text
}
}
1 change: 1 addition & 0 deletions Sources/WinUIBackend/WinUIBackend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,7 @@ public final class WinUIBackend: AppBackend {
) {
let block = textView as! TextBlock
block.text = content
block.isTextSelectionEnabled = environment.isTextSelectionEnabled
missing("font design handling (monospace vs normal)")
environment.apply(to: block)
}
Expand Down
Loading