Skip to content
Merged
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
72 changes: 68 additions & 4 deletions Sources/AppKitBackend/AppKitBackend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1095,7 +1095,11 @@ public final class AppKitBackend: AppBackend {
}
}

public func createTapGestureTarget(wrapping child: Widget) -> Widget {
public func createTapGestureTarget(wrapping child: Widget, gesture _: TapGesture) -> Widget {
if child.subviews.count >= 2 && child.subviews[1] is NSCustomTapGestureTarget {
return child
}

let container = NSView()

container.addSubview(child)
Expand All @@ -1122,19 +1126,79 @@ public final class AppKitBackend: AppBackend {

public func updateTapGestureTarget(
_ container: Widget,
gesture: TapGesture,
action: @escaping () -> Void
) {
let tapGestureTarget = container.subviews[1] as! NSCustomTapGestureTarget
tapGestureTarget.leftClickHandler = action
switch gesture.kind {
case .primary:
tapGestureTarget.leftClickHandler = action
case .secondary:
tapGestureTarget.rightClickHandler = action
case .longPress:
tapGestureTarget.longPressHandler = action
}
}
}

final class NSCustomTapGestureTarget: NSView {
var leftClickHandler: (() -> Void)?
var leftClickHandler: (() -> Void)? {
didSet {
if leftClickHandler != nil && leftClickRecognizer == nil {
let gestureRecognizer = NSClickGestureRecognizer(
target: self, action: #selector(leftClick))
addGestureRecognizer(gestureRecognizer)
leftClickRecognizer = gestureRecognizer
}
}
}
var rightClickHandler: (() -> Void)? {
didSet {
if rightClickHandler != nil && rightClickRecognizer == nil {
let gestureRecognizer = NSClickGestureRecognizer(
target: self, action: #selector(rightClick))
gestureRecognizer.buttonMask = 1 << 1
addGestureRecognizer(gestureRecognizer)
rightClickRecognizer = gestureRecognizer
}
}
}
var longPressHandler: (() -> Void)? {
didSet {
if longPressHandler != nil && longPressRecognizer == nil {
let gestureRecognizer = NSPressGestureRecognizer(
target: self, action: #selector(longPress))
// Both GTK and UIKit default to half a second for long presses
gestureRecognizer.minimumPressDuration = 0.5
addGestureRecognizer(gestureRecognizer)
longPressRecognizer = gestureRecognizer
}
}
}

private var leftClickRecognizer: NSClickGestureRecognizer?
private var rightClickRecognizer: NSClickGestureRecognizer?
private var longPressRecognizer: NSPressGestureRecognizer?

override func mouseDown(with event: NSEvent) {
@objc
func leftClick() {
leftClickHandler?()
}

@objc
func rightClick() {
rightClickHandler?()
}

@objc
func longPress(sender: NSPressGestureRecognizer) {
// GTK emits the event once as soon as the gesture is recognized.
// AppKit emits it twice, once when it's recognized and once when you release the mouse button.
// For consistency, ignore the second event.
if sender.state != .ended {
longPressHandler?()
}
}
}

final class NSCustomMenuItem: NSMenuItem {
Expand Down
72 changes: 72 additions & 0 deletions Sources/Gtk/Generated/GestureLongPress.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import CGtk

/// `GtkGestureLongPress` is a `GtkGesture` for long presses.
///
/// This gesture is also known as “Press and Hold”.
///
/// When the timeout is exceeded, the gesture is triggering the
/// [[email protected]::pressed] signal.
///
/// If the touchpoint is lifted before the timeout passes, or if
/// it drifts too far of the initial press point, the
/// [[email protected]::cancelled] signal will be emitted.
///
/// How long the timeout is before the ::pressed signal gets emitted is
/// determined by the [[email protected]:gtk-long-press-time] setting.
/// It can be modified by the [[email protected]:delay-factor]
/// property.
public class GestureLongPress: GestureSingle {
/// Returns a newly created `GtkGesture` that recognizes long presses.
public convenience init() {
self.init(
gtk_gesture_long_press_new()
)
}

public override func registerSignals() {
super.registerSignals()

addSignal(name: "cancelled") { [weak self] () in
guard let self = self else { return }
self.cancelled?(self)
}

let handler1:
@convention(c) (UnsafeMutableRawPointer, Double, Double, UnsafeMutableRawPointer) ->
Void =
{ _, value1, value2, data in
SignalBox2<Double, Double>.run(data, value1, value2)
}

addSignal(name: "pressed", handler: gCallback(handler1)) {
[weak self] (param0: Double, param1: Double) in
guard let self = self else { return }
self.pressed?(self, param0, param1)
}

let handler2:
@convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void =
{ _, value1, data in
SignalBox1<OpaquePointer>.run(data, value1)
}

addSignal(name: "notify::delay-factor", handler: gCallback(handler2)) {
[weak self] (param0: OpaquePointer) in
guard let self = self else { return }
self.notifyDelayFactor?(self, param0)
}
}

/// Factor by which to modify the default timeout.
@GObjectProperty(named: "delay-factor") public var delayFactor: Double

/// Emitted whenever a press moved too far, or was released
/// before [[email protected]::pressed] happened.
public var cancelled: ((GestureLongPress) -> Void)?

/// Emitted whenever a press goes unmoved/unreleased longer than
/// what the GTK defaults tell.
public var pressed: ((GestureLongPress, Double, Double) -> Void)?

public var notifyDelayFactor: ((GestureLongPress, OpaquePointer) -> Void)?
}
9 changes: 8 additions & 1 deletion Sources/Gtk3Backend/Gtk3Backend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -934,7 +934,10 @@ public final class Gtk3Backend: AppBackend {
gtk_native_dialog_show(chooser.gobjectPointer.cast())
}

public func createTapGestureTarget(wrapping child: Widget) -> Widget {
public func createTapGestureTarget(wrapping child: Widget, gesture: TapGesture) -> Widget {
if gesture != .primary {
fatalError("Unsupported gesture type \(gesture)")
}
let eventBox = Gtk3.EventBox()
eventBox.setChild(to: child)
eventBox.aboveChild = true
Expand All @@ -943,8 +946,12 @@ public final class Gtk3Backend: AppBackend {

public func updateTapGestureTarget(
_ tapGestureTarget: Widget,
gesture: TapGesture,
action: @escaping () -> Void
) {
if gesture != .primary {
fatalError("Unsupported gesture type \(gesture)")
}
tapGestureTarget.onButtonPress = { _, buttonEvent in
let eventType = buttonEvent.type
guard
Expand Down
55 changes: 46 additions & 9 deletions Sources/GtkBackend/GtkBackend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -954,22 +954,59 @@ public final class GtkBackend: AppBackend {
gtk_native_dialog_show(chooser.gobjectPointer.cast())
}

public func createTapGestureTarget(wrapping child: Widget) -> Widget {
let gesture = Gtk.GestureClick()
child.addEventController(gesture)
public func createTapGestureTarget(wrapping child: Widget, gesture: TapGesture) -> Widget {
var gtkGesture: GestureSingle
switch gesture.kind {
case .primary:
gtkGesture = GestureClick()
case .secondary:
gtkGesture = GestureClick()
gtk_gesture_single_set_button(gtkGesture.opaquePointer, guint(GDK_BUTTON_SECONDARY))
case .longPress:
gtkGesture = GestureLongPress()
}
child.addEventController(gtkGesture)
return child
}

public func updateTapGestureTarget(
_ tapGestureTarget: Widget,
gesture: TapGesture,
action: @escaping () -> Void
) {
let gesture = tapGestureTarget.eventControllers[0] as! Gtk.GestureClick
gesture.pressed = { _, nPress, _, _ in
guard nPress == 1 else {
return
}
action()
switch gesture.kind {
case .primary:
let gesture =
tapGestureTarget.eventControllers.first {
$0 is GestureClick
&& gtk_gesture_single_get_button($0.opaquePointer) == GDK_BUTTON_PRIMARY
} as! GestureClick
gesture.pressed = { _, nPress, _, _ in
guard nPress == 1 else {
return
}
action()
}
case .secondary:
let gesture =
tapGestureTarget.eventControllers.first {
$0 is GestureClick
&& gtk_gesture_single_get_button($0.opaquePointer)
== GDK_BUTTON_SECONDARY
} as! GestureClick
gesture.pressed = { _, nPress, _, _ in
guard nPress == 1 else {
return
}
action()
}
case .longPress:
let gesture =
tapGestureTarget.eventControllers.lazy.compactMap { $0 as? GestureLongPress }
.first!
gesture.pressed = { _, _, _ in
action()
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/GtkCodeGen/GtkCodeGen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@
let allowListedClasses = [
"Button", "Entry", "Label", "TextView", "Range", "Scale", "Image", "Switch", "Spinner",
"ProgressBar", "FileChooserNative", "NativeDialog", "GestureClick", "GestureSingle",
"Gesture", "EventController",
"Gesture", "EventController", "GestureLongPress",
]
let gtk3AllowListedClasses = ["MenuShell", "EventBox"]
let gtk4AllowListedClasses = ["Picture", "DropDown", "Popover"]
Expand Down Expand Up @@ -283,7 +283,7 @@
}

static func docComment(_ doc: String?) -> String {
// TODO: Parse comment format to replace image includes, links, and similar

Check warning on line 286 in Sources/GtkCodeGen/GtkCodeGen.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Todo Violation: TODOs should be resolved (Parse comment format to replac...) (todo)
doc?
.split(separator: "\n", omittingEmptySubsequences: false)
.map { $0.trimmingCharacters(in: .whitespaces) }
Expand Down Expand Up @@ -338,7 +338,7 @@
properties.append(decl)
}

// TODO: Refactor so that notify::property signal handlers aren't just hacked into the

Check warning on line 341 in Sources/GtkCodeGen/GtkCodeGen.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Todo Violation: TODOs should be resolved (Refactor so that notify::prope...) (todo)
// signal handler generation code so jankily. Ideally we should decouple the signal generation
// code from the GIR types a bit more so that we can synthesize signals without having to
// create fake GIR entries.
Expand Down Expand Up @@ -500,7 +500,7 @@
exprs.append(
DeclSyntax(
"""
let handler\(raw: signalIndex): @convention(c) (UnsafeMutableRawPointer, \(raw: typeParameters), UnsafeMutableRawPointer) -> Void =

Check warning on line 503 in Sources/GtkCodeGen/GtkCodeGen.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Line Length Violation: Line should be 140 characters or less; currently it has 155 characters (line_length)
{ _, \(raw: arguments), data in
SignalBox\(raw: parameterCount)<\(raw: typeParameters)>.run(data, \(raw: arguments))
}
Expand Down Expand Up @@ -594,7 +594,7 @@
fatalError("'\(classLike.name)' is missing method matching '\(getterFunction)'")
}

// TODO: Handle this conversion more cleanly

Check warning on line 597 in Sources/GtkCodeGen/GtkCodeGen.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Todo Violation: TODOs should be resolved (Handle this conversion more cl...) (todo)
if type.hasPrefix("Gtk") {
type = String(type.dropFirst(3))
}
Expand All @@ -604,7 +604,7 @@
&& type != "OpaquePointer"
{
print("Skipping \(property.name) with type \(type)")
// TODO: Handle more types

Check warning on line 607 in Sources/GtkCodeGen/GtkCodeGen.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Todo Violation: TODOs should be resolved (Handle more types) (todo)
return nil
}

Expand All @@ -612,14 +612,14 @@
type += "?"
}

// TODO: Figure out why DropDown.selected doesn't work as a UInt (Gtk complains that

Check warning on line 615 in Sources/GtkCodeGen/GtkCodeGen.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Todo Violation: TODOs should be resolved (Figure out why DropDown.select...) (todo)
// the property doesn't hold a UInt, implying that the docs are wrong??)
if classLike.name == "DropDown" && property.name == "selected" {
type = "Int"
}

guard !type.contains(".") else {
// TODO: Handle namespaced types

Check warning on line 622 in Sources/GtkCodeGen/GtkCodeGen.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Todo Violation: TODOs should be resolved (Handle namespaced types) (todo)
return nil
}

Expand Down Expand Up @@ -699,7 +699,7 @@
parameters[i].name = convertCIdentifier(parameter.name)
}

// TODO: Fix for `gtk_scale_new_with_range`

Check warning on line 702 in Sources/GtkCodeGen/GtkCodeGen.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Todo Violation: TODOs should be resolved (Fix for `gtk_scale_new_with_ra...) (todo)
// Add a label to the first parameter name based on the constructor name if possible (to
// avoid ambiguity between certain initializers). E.g. `gtk_button_new_with_label` and
// `gtk_button_new_with_mnemonic` both call their first parameter `label` which would be
Expand Down Expand Up @@ -733,7 +733,7 @@
let name = convertCIdentifier(parameter.name)
var argument = name

// TODO: Handle nested pointer arrays more generally

Check warning on line 736 in Sources/GtkCodeGen/GtkCodeGen.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Todo Violation: TODOs should be resolved (Handle nested pointer arrays m...) (todo)
if parameter.array?.type.cType == "char*" {
argument = """
\(argument)
Expand Down
6 changes: 4 additions & 2 deletions Sources/SwiftCrossUI/Backend/AppBackend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -508,11 +508,12 @@ public protocol AppBackend {
/// Wraps a view in a container that can receive tap gestures. Some
/// backends may not have to wrap the child, in which case they may
/// just return the child as is.
func createTapGestureTarget(wrapping child: Widget) -> Widget
func createTapGestureTarget(wrapping child: Widget, gesture: TapGesture) -> Widget
/// Update the tap gesture target with a new action. Replaces the old
/// action.
func updateTapGestureTarget(
_ tapGestureTarget: Widget,
gesture: TapGesture,
action: @escaping () -> Void
)
}
Expand Down Expand Up @@ -813,11 +814,12 @@ extension AppBackend {
todo()
}

public func createTapGestureTarget(wrapping child: Widget) -> Widget {
public func createTapGestureTarget(wrapping child: Widget, gesture: TapGesture) -> Widget {
todo()
}
public func updateTapGestureTarget(
_ clickTarget: Widget,
gesture: TapGesture,
action: @escaping () -> Void
) {
todo()
Expand Down
34 changes: 28 additions & 6 deletions Sources/SwiftCrossUI/Modifiers/OnTapGestureModifier.swift
Original file line number Diff line number Diff line change
@@ -1,14 +1,35 @@
public struct TapGesture: Sendable, Hashable {
package var kind: TapGestureKind

/// The idiomatic "primary" interaction for the device, such as a left-click with the mouse
/// or normal tap on a touch screen.
public static let primary = TapGesture(kind: .primary)
/// The idiomatic "secondary" interaction for the device, such as a right-click with the
/// mouse or long press on a touch screen.
public static let secondary = TapGesture(kind: .secondary)
/// A long press of the same interaction type as ``primary``. May be equivalent to
/// ``secondary`` on some backends, particularly on mobile devices.
public static let longPress = TapGesture(kind: .longPress)

package enum TapGestureKind {
case primary, secondary, longPress
}
}

extension View {
/// Adds an action to perform when the user taps or clicks this view.
///
/// Any tappable elements within the view will no longer be tappable.
public func onTapGesture(perform action: @escaping () -> Void) -> some View {
OnTapGestureModifier(body: TupleView1(self), action: action)
/// Any tappable elements within the view will no longer be tappable with the same gesture
/// type.
public func onTapGesture(gesture: TapGesture = .primary, perform action: @escaping () -> Void)
-> some View
{
OnTapGestureModifier(body: TupleView1(self), gesture: gesture, action: action)
}

/// Adds an action to run when this view is clicked. Any clickable elements
/// within the view will no longer be clickable.
@available(*, deprecated, renamed: "onTapGesture(perform:)")
@available(*, deprecated, renamed: "onTapGesture(gesture:perform:)")
public func onClick(perform action: @escaping () -> Void) -> some View {
onTapGesture(perform: action)
}
Expand All @@ -18,6 +39,7 @@ struct OnTapGestureModifier<Content: View>: TypeSafeView {
typealias Children = TupleView1<Content>.Children

var body: TupleView1<Content>
var gesture: TapGesture
var action: () -> Void

func children<Backend: AppBackend>(
Expand All @@ -36,7 +58,7 @@ struct OnTapGestureModifier<Content: View>: TypeSafeView {
_ children: Children,
backend: Backend
) -> Backend.Widget {
backend.createTapGestureTarget(wrapping: children.child0.widget.into())
backend.createTapGestureTarget(wrapping: children.child0.widget.into(), gesture: gesture)
}

func update<Backend: AppBackend>(
Expand All @@ -55,7 +77,7 @@ struct OnTapGestureModifier<Content: View>: TypeSafeView {
)
if !dryRun {
backend.setSize(of: widget, to: childResult.size.size)
backend.updateTapGestureTarget(widget, action: action)
backend.updateTapGestureTarget(widget, gesture: gesture, action: action)
}
return childResult
}
Expand Down
Loading
Loading