// macOS/AppKit F11 demo — scroll fidelity (../../../features/f11-scroll.md).
// AppKit hands scrollWheel: one NSEvent carrying THREE representations of the
// same physical scroll: scrollingDeltaX/Y (the modern, possibly pixel-precise
// deltas), deltaX/deltaY (the legacy line-ish units), and the phase /
// momentumPhase pair that turns a stream of events into trackpad gestures.
// This demo logs every field of every scroll event, keeps a scrollable-ruler
// offset, and prints per-gesture summaries keyed on phase transitions.
//
// What it logs (A[ssh]; locked console — no real wheel/trackpad, so events are
// built with CGEventCreateScrollWheelEvent2 and delivered IN-PROCESS via
// +[NSEvent eventWithCGEvent:] + [window sendEvent:], the f06/f07 route-1
// workhorse; the real-fling choreography is Tier C):
// - per event: scrollingDeltaX/Y, legacy deltaX/deltaY, hasPreciseScrollingDeltas,
// phase + momentumPhase (NSEventPhase bitmask decoded), and
// isDirectionInvertedFromDevice;
// - the unit duality at the CG layer: a line-unit event's synthesized
// kCGScrollWheelEventDeltaAxis1 / FixedPtDeltaAxis1 / PointDeltaAxis1 /
// IsContinuous fields read back (where "one notch = deltaY 1.0 =
// scrollingDeltaY ~10" lives), same for a pixel-unit event;
// - multi-tick and negative line values; a fractional line delta forced via
// CGEventSetDoubleValueField(kCGScrollWheelEventFixedPtDeltaAxis1) — the
// precision-touchpad analog;
// - the phase probe: CGEventSetIntegerValueField(kCGScrollWheelEventScrollPhase /
// ...MomentumPhase) with CGScrollPhase/CGMomentumScrollPhase values — does the
// wrapped NSEvent surface them as NSEventPhase? (the CG->NS phase mapping is
// the finding);
// - a scripted synthetic fling: phase began -> changed... -> ended, then
// momentum began -> changed... -> ended, plus a cancelled variant — each
// transition driving the gesture summaries;
// - a scrollable ruler (offset integrated from scrollingDeltaY) drawn in
// drawRect: (visible pixels are Tier C; the offset is in the log).
//
// Modes: WSI_AUTO_EXIT=1 = bounded scripted run; without it the window stays up
// for a manual (Tier C) wheel/trackpad session. Headless-safe: prints `SKIP:`
// and exits 0 when there is no window server.
module (module) appapp;
import (package) corecore.(module) core.attributeThis module contains UDA's (User Defined Attributes) either used in
the runtime or special UDA's recognized by compiler.
Attribute Name, Linkage, Description
gnuAbiTag, C++,
Declares an ABI tag on a C++ symbol.
mustuse,,
Ensures that values of a struct or union type are not discarded.
optional, Objective-C,
Makes an Objective-C interface method optional.
selector, Objective-C,
Attaches an Objective-C selector to a method.
standalone,,
Marks a shared module constructor as not depending on any
other module constructor being run first.
weak,,
Specifies that a global symbol should be emitted with weak linkage.
Source
core/attribute.d
attribute : selector;
import (package) corecore.(package) core.stdcstdc.(module) core.stdc.stdioD header file for C99 <stdio.h>
pubs.opengroup.org/onlinepubs/009695399/basedefs/stdio.h.html, stdio.h
Source
core/stdc/stdio.d
stdio : (alias) app.printf = int core.stdc.stdio.printf(scope const(char*) format, scope const ...) nothrow @nogcprintf, (alias) app.snprintf = int core.stdc.stdio.snprintf(scope char* s, ulong n, scope const(char*) format, scope const ...) nothrow @nogcsnprintf;
import (package) corecore.(package) core.stdcstdc.(module) core.stdc.stdlibD header file for C99.
pubs.opengroup.org/onlinepubs/009695399/basedefs/stdlib.h.html, stdlib.h
Source
core/stdc/stdlib.d
stdlib : (alias) app.getenv = char* core.stdc.stdlib.getenv(scope const(char*) name) nothrow @nogcgetenv;
import instrument : instr, instrInit;
import objc.autorelease : autoreleasepool_push, autoreleasepool_pop; // objective-d
import objc.rt : SEL;
// --- Geometry ----------------------------------------------------------------------
extern (C) struct (struct) app.NSPointNSPoint
{
double (field) double app.NSPoint.xx, (field) double app.NSPoint.yy;
}
extern (C) struct (struct) app.NSSizeNSSize
{
double (field) double app.NSSize.widthwidth, (field) double app.NSSize.heightheight;
}
extern (C) struct (struct) app.NSRectNSRect
{
(struct) app.NSPointNSPoint (field) app.NSPoint app.NSRect.originorigin;
(struct) app.NSSizeNSSize (field) app.NSSize app.NSRect.sizesize;
}
// --- CoreGraphics C API --------------------------------------------------------------
extern (C) nothrow @nogc
{
uint uint app.CGMainDisplayID() nothrow @nogcCGMainDisplayID();
(struct) app.NSRectNSRect app.NSRect app.CGDisplayBounds(uint display) nothrow @nogcCGDisplayBounds(uint (parameter) uint displaydisplay);
// The non-variadic scroll constructor (up to 3 axes; we use 2).
void* void* app.CGEventCreateScrollWheelEvent2(void* source, uint units, uint wheelCount, int wheel1, int wheel2, int wheel3) nothrow @nogcCGEventCreateScrollWheelEvent2(void* (parameter) void* sourcesource, uint (parameter) uint unitsunits, uint (parameter) uint wheelCountwheelCount,
int (parameter) int wheel1wheel1, int (parameter) int wheel2wheel2, int (parameter) int wheel3wheel3);
void void app.CGEventSetLocation(void* event, app.NSPoint location) nothrow @nogcCGEventSetLocation(void* (parameter) void* eventevent, (struct) app.NSPointNSPoint (parameter) app.NSPoint locationlocation);
void void app.CGEventSetIntegerValueField(void* event, uint field, long value) nothrow @nogcCGEventSetIntegerValueField(void* (parameter) void* eventevent, uint (parameter) uint fieldfield, long (parameter) long valuevalue);
long long app.CGEventGetIntegerValueField(void* event, uint field) nothrow @nogcCGEventGetIntegerValueField(void* (parameter) void* eventevent, uint (parameter) uint fieldfield);
void void app.CGEventSetDoubleValueField(void* event, uint field, double value) nothrow @nogcCGEventSetDoubleValueField(void* (parameter) void* eventevent, uint (parameter) uint fieldfield, double (parameter) double valuevalue);
double double app.CGEventGetDoubleValueField(void* event, uint field) nothrow @nogcCGEventGetDoubleValueField(void* (parameter) void* eventevent, uint (parameter) uint fieldfield);
void void app.CFRelease(void* obj) nothrow @nogcCFRelease(void* (parameter) void* objobj);
void void app.CGContextSetRGBFillColor(void* ctx, double r, double g, double b, double a) nothrow @nogcCGContextSetRGBFillColor(void* (parameter) void* ctxctx, double (parameter) double rr, double (parameter) double gg, double (parameter) double bb, double (parameter) double aa);
void void app.CGContextFillRect(void* ctx, app.NSRect rect) nothrow @nogcCGContextFillRect(void* (parameter) void* ctxctx, (struct) app.NSRectNSRect (parameter) app.NSRect rectrect);
}
enum uint (constant) uint app.kCGScrollEventUnitPixel = 0ukCGScrollEventUnitPixel = 0; // CGScrollEventUnit
enum uint (constant) uint app.kCGScrollEventUnitLine = 1ukCGScrollEventUnitLine = 1;
// CGEventField (CGEventTypes.h), axis 1 = vertical, axis 2 = horizontal.
enum : uint
{
(enum value) app.kCGScrollWheelEventDeltaAxis1 = 11ukCGScrollWheelEventDeltaAxis1 = 11, // integer line delta
(enum value) app.kCGScrollWheelEventDeltaAxis2 = 12ukCGScrollWheelEventDeltaAxis2 = 12,
(enum value) app.kCGScrollWheelEventFixedPtDeltaAxis1 = 93ukCGScrollWheelEventFixedPtDeltaAxis1 = 93, // 16.16 fixed-point line delta
(enum value) app.kCGScrollWheelEventFixedPtDeltaAxis2 = 94ukCGScrollWheelEventFixedPtDeltaAxis2 = 94,
(enum value) app.kCGScrollWheelEventPointDeltaAxis1 = 96ukCGScrollWheelEventPointDeltaAxis1 = 96, // integer pixel delta
(enum value) app.kCGScrollWheelEventPointDeltaAxis2 = 97ukCGScrollWheelEventPointDeltaAxis2 = 97,
(enum value) app.kCGScrollWheelEventIsContinuous = 88ukCGScrollWheelEventIsContinuous = 88, // 1 = pixel-based device (trackpad)
(enum value) app.kCGScrollWheelEventScrollPhase = 99ukCGScrollWheelEventScrollPhase = 99, // CGScrollPhase
(enum value) app.kCGScrollWheelEventMomentumPhase = 123ukCGScrollWheelEventMomentumPhase = 123, // CGMomentumScrollPhase
}
// CGScrollPhase (kCGScrollPhase*: bitmask-style values)...
enum : long
{
(enum value) app.cgPhaseBegan = 1LcgPhaseBegan = 1,
(enum value) app.cgPhaseChanged = 2LcgPhaseChanged = 2,
(enum value) app.cgPhaseEnded = 4LcgPhaseEnded = 4,
(enum value) app.cgPhaseCancelled = 8LcgPhaseCancelled = 8,
(enum value) app.cgPhaseMayBegin = 128LcgPhaseMayBegin = 128,
}
// ...vs CGMomentumScrollPhase (kCGMomentumScrollPhase*: sequential values).
enum : long
{
(enum value) app.cgMomentumNone = 0LcgMomentumNone = 0,
(enum value) app.cgMomentumBegin = 1LcgMomentumBegin = 1,
(enum value) app.cgMomentumContinue = 2LcgMomentumContinue = 2,
(enum value) app.cgMomentumEnd = 3LcgMomentumEnd = 3,
}
// NSEventPhase bitmask (NSEvent.h).
enum : ulong
{
(enum value) app.NSEventPhaseNone = 0LUNSEventPhaseNone = 0,
(enum value) app.NSEventPhaseBegan = 1LUNSEventPhaseBegan = 0x1,
(enum value) app.NSEventPhaseStationary = 2LUNSEventPhaseStationary = 0x2,
(enum value) app.NSEventPhaseChanged = 4LUNSEventPhaseChanged = 0x4,
(enum value) app.NSEventPhaseEnded = 8LUNSEventPhaseEnded = 0x8,
(enum value) app.NSEventPhaseCancelled = 16LUNSEventPhaseCancelled = 0x10,
(enum value) app.NSEventPhaseMayBegin = 32LUNSEventPhaseMayBegin = 0x20,
}
// --- AppKit class declarations --------------------------------------------------------
extern (Objective-C):
extern class (class) app.NSObjectNSObject
{
}
extern class (class) app.NSStringNSString : (class) app.NSObjectNSObject
{
static (class) app.NSStringNSString app.NSString app.NSString.alloc()alloc() @selector("alloc");
(class) app.NSStringNSString app.NSString app.NSString.initWithUTF8String(const(char)* s)initWithUTF8String(const(char)* (parameter) const(char)* ss) @selector("initWithUTF8String:");
}
extern class (class) app.NSEventNSEvent : (class) app.NSObjectNSObject
{
static (class) app.NSEventNSEvent app.NSEvent app.NSEvent.eventWithCGEvent(void* cgEvent)eventWithCGEvent(void* (parameter) void* cgEventcgEvent) @selector("eventWithCGEvent:");
static (class) app.NSEventNSEvent app.NSEvent app.NSEvent.otherEventWithType(long type, app.NSPoint location, ulong modifierFlags, double timestamp, long windowNumber, app.NSObject context, short subtype, long data1, long data2)otherEventWithType(long (parameter) long typetype, (struct) app.NSPointNSPoint (parameter) app.NSPoint locationlocation, ulong (parameter) ulong modifierFlagsmodifierFlags,
double (parameter) double timestamptimestamp, long (parameter) long windowNumberwindowNumber, (class) app.NSObjectNSObject (parameter) app.NSObject contextcontext, short (parameter) short subtypesubtype,
long (parameter) long data1data1, long (parameter) long data2data2)
@selector("otherEventWithType:location:modifierFlags:timestamp:windowNumber:context:subtype:data1:data2:");
long long app.NSEvent.type()type() @selector("type");
(struct) app.NSPointNSPoint app.NSPoint app.NSEvent.locationInWindow()locationInWindow() @selector("locationInWindow");
double double app.NSEvent.deltaX()deltaX() @selector("deltaX");
double double app.NSEvent.deltaY()deltaY() @selector("deltaY");
double double app.NSEvent.scrollingDeltaX()scrollingDeltaX() @selector("scrollingDeltaX");
double double app.NSEvent.scrollingDeltaY()scrollingDeltaY() @selector("scrollingDeltaY");
bool bool app.NSEvent.hasPreciseScrollingDeltas()hasPreciseScrollingDeltas() @selector("hasPreciseScrollingDeltas");
ulong ulong app.NSEvent.phase()phase() @selector("phase");
ulong ulong app.NSEvent.momentumPhase()momentumPhase() @selector("momentumPhase");
bool bool app.NSEvent.isDirectionInvertedFromDevice()isDirectionInvertedFromDevice() @selector("isDirectionInvertedFromDevice");
}
extern class (class) app.NSTimerNSTimer : (class) app.NSObjectNSObject
{
static (class) app.NSTimerNSTimer app.NSTimer.scheduledTimerWithTimeIntervalscheduledTimerWithTimeInterval(double interval, (class) app.NSObjectNSObject target,
SEL sel, (class) app.NSObjectNSObject userInfo, bool repeats)
@selector("scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:");
}
extern class (class) app.NSApplicationNSApplication : (class) app.NSObjectNSObject
{
static (class) app.NSApplicationNSApplication app.NSApplication app.NSApplication.sharedApplication()sharedApplication() @selector("sharedApplication");
void void app.NSApplication.setActivationPolicy(long policy)setActivationPolicy(long (parameter) long policypolicy) @selector("setActivationPolicy:");
void void app.NSApplication.activateIgnoringOtherApps(bool flag)activateIgnoringOtherApps(bool (parameter) bool flagflag) @selector("activateIgnoringOtherApps:");
void void app.NSApplication.run()run() @selector("run");
void void app.NSApplication.stop(app.NSObject sender)stop((class) app.NSObjectNSObject (parameter) app.NSObject sendersender) @selector("stop:");
void void app.NSApplication.postEvent(app.NSEvent event, bool atStart)postEvent((class) app.NSEventNSEvent (parameter) app.NSEvent eventevent, bool (parameter) bool atStartatStart) @selector("postEvent:atStart:");
}
extern class (class) app.NSGraphicsContextNSGraphicsContext : (class) app.NSObjectNSObject
{
static (class) app.NSGraphicsContextNSGraphicsContext app.NSGraphicsContext app.NSGraphicsContext.currentContext()currentContext() @selector("currentContext");
void* void* app.NSGraphicsContext.CGContext()CGContext() @selector("CGContext");
}
extern class (class) app.NSViewNSView : (class) app.NSObjectNSObject
{
(struct) app.NSRectNSRect app.NSRect app.NSView.bounds()bounds() @selector("bounds");
void void app.NSView.setNeedsDisplay(bool flag)setNeedsDisplay(bool (parameter) bool flagflag) @selector("setNeedsDisplay:");
}
extern class (class) app.NSWindowNSWindow : (class) app.NSObjectNSObject
{
static (class) app.NSWindowNSWindow app.NSWindow app.NSWindow.alloc()alloc() @selector("alloc");
(class) app.NSWindowNSWindow app.NSWindow app.NSWindow.initWithContentRect(app.NSRect contentRect, ulong styleMask, ulong backing, bool defer)initWithContentRect((struct) app.NSRectNSRect (parameter) app.NSRect contentRectcontentRect, ulong (parameter) ulong styleMaskstyleMask,
ulong (parameter) ulong backingbacking, bool (parameter) bool deferdefer) @selector("initWithContentRect:styleMask:backing:defer:");
void void app.NSWindow.setTitle(app.NSString title)setTitle((class) app.NSStringNSString (parameter) app.NSString titletitle) @selector("setTitle:");
void void app.NSWindow.setContentView(app.NSObject view)setContentView((class) app.NSObjectNSObject (parameter) app.NSObject viewview) @selector("setContentView:");
void void app.NSWindow.makeKeyAndOrderFront(app.NSObject sender)makeKeyAndOrderFront((class) app.NSObjectNSObject (parameter) app.NSObject sendersender) @selector("makeKeyAndOrderFront:");
bool bool app.NSWindow.makeFirstResponder(app.NSObject responder)makeFirstResponder((class) app.NSObjectNSObject (parameter) app.NSObject responderresponder) @selector("makeFirstResponder:");
void void app.NSWindow.sendEvent(app.NSEvent event)sendEvent((class) app.NSEventNSEvent (parameter) app.NSEvent eventevent) @selector("sendEvent:");
long long app.NSWindow.windowNumber()windowNumber() @selector("windowNumber");
(struct) app.NSRectNSRect app.NSRect app.NSWindow.frame()frame() @selector("frame");
}
// The ruler view: logs every scrollWheel: payload and scrolls a tick ruler.
class (class) app.RulerViewRulerView : (class) app.NSViewNSView
{
static (class) app.RulerViewRulerView app.RulerView app.RulerView.alloc()alloc() @selector("alloc");
(class) app.RulerViewRulerView app.RulerView app.RulerView.initWithFrame(app.NSRect frame)initWithFrame((struct) app.NSRectNSRect (parameter) app.NSRect frameframe) @selector("initWithFrame:");
void void app.RulerView.drawRect(app.NSRect dirtyRect)drawRect((struct) app.NSRectNSRect (parameter) app.NSRect dirtyRectdirtyRect) @selector("drawRect:")
{
// Tier-C visual aid: ruler ticks every 20 pt (major every 100) offset by
// the integrated scroll position, so over/under-scroll is visible.
void* _error_ ctxctx = NSGraphicsContext.NSGraphicsContext.currentContextcurrentContext().NSGraphicsContext.currentContext().CGContextCGContext();
immutable _error_ bb = this.this.boundsbounds();
CGContextSetRGBFillColor(ctx, 0.12, 0.12, 0.14, 1);
CGContextFillRect(ctx, b);
immutable _error_ offoff = g_offsetY % 20.0;
for (double _error_ yy = -off; y < b.size.height; y += 20)
{
immutable _error_ absPosabsPos = y + g_offsetY;
immutable _error_ majormajor = (cast(long)(absPos / 20 + 0.5)) % 5 == 0;
CGContextSetRGBFillColor(ctx, 0.7, 0.7, 0.75, 1);
CGContextFillRect(ctx, NSRect(NSPoint(0, y), NSSize(major ? 60 : 30, 1)));
}
}
void void app.RulerView.scrollWheel(app.NSEvent e)scrollWheel((class) app.NSEventNSEvent (parameter) app.NSEvent ee) @selector("scrollWheel:")
{
onScroll(e);
}
void void app.RulerView.tick(app.NSTimer t)tick((class) app.NSTimerNSTimer (parameter) app.NSTimer tt) @selector("tick:")
{
onStep();
}
}
extern (D): // back to D linkage
enum (constant) int app.NSApplicationActivationPolicyRegular = 0NSApplicationActivationPolicyRegular = 0;
enum : ulong
{
(enum value) app.NSWindowStyleMaskTitled = 1LUNSWindowStyleMaskTitled = 1,
(enum value) app.NSWindowStyleMaskClosable = 2LUNSWindowStyleMaskClosable = 2,
(enum value) app.NSWindowStyleMaskResizable = 8LUNSWindowStyleMaskResizable = 8,
}
enum ulong (constant) ulong app.NSBackingStoreBuffered = 2LUNSBackingStoreBuffered = 2;
enum long (constant) long app.NSEventTypeApplicationDefined = 15LNSEventTypeApplicationDefined = 15;
// --- Demo state -----------------------------------------------------------------------
__gshared (class) app.NSApplicationNSApplication (__gshared global) app.NSApplication app.g_appg_app;
__gshared (class) app.NSWindowNSWindow (__gshared global) app.NSWindow app.g_wing_win;
__gshared (class) app.RulerViewRulerView (__gshared global) app.RulerView app.g_viewg_view;
__gshared bool (__gshared global) bool app.g_autog_auto;
__gshared int (__gshared global) int app.g_stepg_step;
__gshared int (__gshared global) int app.g_arrivalsg_arrivals;
__gshared double (__gshared global) double app.g_offsetYg_offsetY = 0; // ruler position (points)
// Per-gesture accumulators, keyed on phase transitions.
__gshared bool (__gshared global) bool app.g_gestureActiveg_gestureActive;
__gshared int (__gshared global) int app.g_gestureEventsg_gestureEvents;
__gshared double (__gshared global) double app.g_gestureScrollYg_gestureScrollY = 0, (__gshared global) double app.g_gestureLegacyYg_gestureLegacyY = 0;
__gshared bool (__gshared global) bool app.g_momentumActiveg_momentumActive;
__gshared int (__gshared global) int app.g_momentumEventsg_momentumEvents;
__gshared double (__gshared global) double app.g_momentumScrollYg_momentumScrollY = 0;
__gshared int (__gshared global) int app.g_gestureNg_gestureN; // gesture counter
// Decode the NSEventPhase bitmask into a readable token list.
void void app.phaseName(ulong phase, char* buf, ulong cap) nothrow @nogcphaseName(ulong (parameter) ulong phasephase, char* (parameter) char* bufbuf, (alias) object.size_t = ulongsize_t (parameter) ulong capcap) nothrow @nogc
{
if (phase == 0)
{
snprintf(buf, cap, "none");
return;
}
(unresolved type) size_tsize_t _error_ nn = 0;
void void add(const(char)* t) nothrow @nogcadd(const(char)* (parameter) const(char)* tt) nothrow @nogc
{
n += snprintf(buf + n, cap - n, "%s%s", n ? "|".ptr : "".ptr, t);
}
if (phase & NSEventPhaseBegan)
add("began");
if (phase & NSEventPhaseStationary)
add("stationary");
if (phase & NSEventPhaseChanged)
add("changed");
if (phase & NSEventPhaseEnded)
add("ended");
if (phase & NSEventPhaseCancelled)
add("cancelled");
if (phase & NSEventPhaseMayBegin)
add("mayBegin");
}
// --- The scrollWheel: funnel -----------------------------------------------------------
void void app.onScroll(app.NSEvent e)onScroll((class) app.NSEventNSEvent (parameter) app.NSEvent ee)
{
++g_arrivals;
immutable _error_ sdxsdx = e.e.scrollingDeltaXscrollingDeltaX();
immutable _error_ sdysdy = e.e.scrollingDeltaYscrollingDeltaY();
immutable _error_ ldxldx = e.e.deltaXdeltaX();
immutable _error_ ldyldy = e.e.deltaYdeltaY();
immutable _error_ preciseprecise = e.e.hasPreciseScrollingDeltashasPreciseScrollingDeltas();
immutable _error_ phph = e.e.phasephase();
immutable _error_ mphmph = e.e.momentumPhasemomentumPhase();
char[64] _error_ pnpn = void, _error_ mnmn = void;
phaseName(ph, pn.ptr, pn.length);
phaseName(mph, mn.ptr, mn.length);
instr("scroll",
"sdx=%.2f sdy=%.2f legacy_dx=%.3f legacy_dy=%.3f precise=%d phase=0x%llx(%s) momentum=0x%llx(%s) inverted=%d",
sdx, sdy, ldx, ldy, precise ? 1 : 0, ph, pn.ptr, mph, mn.ptr,
e.e.isDirectionInvertedFromDeviceisDirectionInvertedFromDevice() ? 1 : 0);
// Ruler: integrate the modern deltas (positive sdy scrolls content up).
g_offsetY -= sdy;
instr("ruler", "offset_y=%.1f", g_offsetY);
g_view.g_view.setNeedsDisplaysetNeedsDisplay(true);
// Gesture bookkeeping keyed on the phase transitions.
if (ph & NSEventPhaseBegan)
{
g_gestureActive = true;
++g_gestureN;
g_gestureEvents = 0;
g_gestureScrollY = g_gestureLegacyY = 0;
instr("gesture", "n=%d state=begin", g_gestureN);
}
if (g_gestureActive)
{
++g_gestureEvents;
g_gestureScrollY += sdy;
g_gestureLegacyY += ldy;
}
if (ph & (NSEventPhaseEnded | NSEventPhaseCancelled))
{
instr("gesture_summary",
"n=%d kind=drag end=%s events=%d total_sdy=%.2f total_legacy_dy=%.3f",
g_gestureN, (ph & NSEventPhaseCancelled) ? "cancelled".ptr : "ended".ptr,
g_gestureEvents, g_gestureScrollY, g_gestureLegacyY);
g_gestureActive = false;
}
if (mph & NSEventPhaseBegan)
{
g_momentumActive = true;
g_momentumEvents = 0;
g_momentumScrollY = 0;
instr("gesture", "n=%d state=momentum_begin", g_gestureN);
}
if (g_momentumActive)
{
++g_momentumEvents;
g_momentumScrollY += sdy;
}
if (mph & (NSEventPhaseEnded | NSEventPhaseCancelled))
{
instr("gesture_summary", "n=%d kind=momentum events=%d total_sdy=%.2f",
g_gestureN, g_momentumEvents, g_momentumScrollY);
g_momentumActive = false;
}
if (ph == NSEventPhaseNone && mph == NSEventPhaseNone)
instr("gesture_summary", "kind=discrete events=1 sdy=%.2f legacy_dy=%.3f",
sdy, ldy);
}
// --- Synthetic scroll injection ----------------------------------------------------------
// Read back what CG synthesized (or what we forced) at the Quartz layer.
void void app.logCGFields(void* cg, const(char)* label)logCGFields(void* (parameter) void* cgcg, const(char)* (parameter) const(char)* labellabel)
{
instr("cg_fields",
"label=%s delta1=%ld fixedpt1=%.3f point1=%ld continuous=%ld phase=%ld momentum=%ld",
label,
CGEventGetIntegerValueField(cg, kCGScrollWheelEventDeltaAxis1),
CGEventGetDoubleValueField(cg, kCGScrollWheelEventFixedPtDeltaAxis1),
CGEventGetIntegerValueField(cg, kCGScrollWheelEventPointDeltaAxis1),
CGEventGetIntegerValueField(cg, kCGScrollWheelEventIsContinuous),
CGEventGetIntegerValueField(cg, kCGScrollWheelEventScrollPhase),
CGEventGetIntegerValueField(cg, kCGScrollWheelEventMomentumPhase));
}
// Window-content center in CG global coords (top-left-origin), so sendEvent's
// hit test can route the wrapped event to the ruler view.
(struct) app.NSPointNSPoint app.NSPoint app.windowCenterCG()windowCenterCG()
{
immutable _error_ wfwf = g_win.g_win.frameframe(); // AppKit screen coords, y-up
immutable _error_ dbdb = CGDisplayBounds(CGMainDisplayID());
return NSPoint(wf.wf.originorigin.wf.origin.xx + wf.wf.sizesize.wf.size.widthwidth / 2,
db.db.sizesize.db.size.heightheight - (wf.wf.originorigin.wf.origin.yy + wf.wf.sizesize.wf.size.heightheight / 2));
}
// Build, wrap, dispatch. phase/momentum < 0 = leave the field untouched.
void void app.injectScroll(uint units, int wheel1, long phase, long momentum, const(char)* label, double fixedPtOverride = nan)injectScroll(uint (parameter) uint unitsunits, int (parameter) int wheel1wheel1, long (parameter) long phasephase, long (parameter) long momentummomentum,
const(char)* (parameter) const(char)* labellabel, double (parameter) double fixedPtOverridefixedPtOverride = double.(constant) double double.nan = nannan)
{
void* _error_ cgcg = CGEventCreateScrollWheelEvent2(null, units, 2, wheel1, 0, 0);
if (cg is null)
{
instr("inject", "label=%s result=create_failed", label);
return;
}
CGEventSetLocation(cg, windowCenterCG());
if (phase >= 0)
CGEventSetIntegerValueField(cg, kCGScrollWheelEventScrollPhase, phase);
if (momentum >= 0)
CGEventSetIntegerValueField(cg, kCGScrollWheelEventMomentumPhase, momentum);
if (fixedPtOverride == fixedPtOverride) // !isNaN
CGEventSetDoubleValueField(cg, kCGScrollWheelEventFixedPtDeltaAxis1,
fixedPtOverride);
instr("inject", "label=%s units=%s wheel1=%d cg_phase=%ld cg_momentum=%ld",
label, units == kCGScrollEventUnitLine ? "line".ptr : "pixel".ptr, wheel1,
phase, momentum);
logCGFields(cg, label);
(unresolved type) NSEventNSEvent _error_ ee = NSEvent.NSEvent.eventWithCGEventeventWithCGEvent(cg);
if (e is null)
{
instr("wrap", "label=%s result=nil", label);
CFRelease(cg);
return;
}
immutable _error_ beforebefore = g_arrivals;
g_win.g_win.sendEventsendEvent(e);
if (g_arrivals == before)
{
instr("route", "label=%s sendEvent=not_delivered fallback=direct", label);
g_view.g_view.scrollWheelscrollWheel(e);
}
else
instr("route", "label=%s sendEvent=delivered", label);
CFRelease(cg);
}
// --- The scripted run ----------------------------------------------------------------------
void void app.stopApp()stopApp()
{
instr("step", "name=NSApp_stop");
g_app.g_app.stopstop(null);
(unresolved type) NSEventNSEvent _error_ evev = NSEvent.NSEvent.otherEventWithTypeotherEventWithType(NSEventTypeApplicationDefined,
NSPoint(0, 0), 0, 0, 0, null, 0, 0, 0);
g_app.g_app.postEventpostEvent(ev, true); // stop: is only checked after an event dispatch
}
void void app.onStep()onStep()
{
if (!g_auto)
return;
switch (g_step)
{
// -- Unit duality: line-unit (wheel notches) vs pixel-unit (trackpad-like) --
case 0: // one wheel notch up: the deltaY 1.0 / scrollingDeltaY ~10 anchor
injectScroll(kCGScrollEventUnitLine, 1, -1, -1, "line_1_notch");
break;
case 1: // multi-tick
injectScroll(kCGScrollEventUnitLine, 3, -1, -1, "line_3_notches");
break;
case 2: // negative (scroll down)
injectScroll(kCGScrollEventUnitLine, -2, -1, -1, "line_minus2");
break;
case 3: // pixel unit: does the wrapped event turn precise?
injectScroll(kCGScrollEventUnitPixel, 10, -1, -1, "pixel_10");
break;
case 4:
injectScroll(kCGScrollEventUnitPixel, 120, -1, -1, "pixel_120");
break;
case 5: // fractional line delta via the 16.16 fixed-point field
injectScroll(kCGScrollEventUnitLine, 0, -1, -1, "line_fractional_0.4", 0.4);
break;
// -- Phase probe: can a CGEvent carry gesture phases into NSEvent? --
case 6:
injectScroll(kCGScrollEventUnitPixel, 5, cgPhaseBegan, -1, "phase_began_probe");
break;
// -- Full synthetic trackpad fling: drag phases then momentum phases --
case 7:
injectScroll(kCGScrollEventUnitPixel, 30, cgPhaseChanged, -1, "fling_changed_30");
break;
case 8:
injectScroll(kCGScrollEventUnitPixel, 20, cgPhaseChanged, -1, "fling_changed_20");
break;
case 9:
injectScroll(kCGScrollEventUnitPixel, 0, cgPhaseEnded, -1, "fling_ended");
break;
case 10:
injectScroll(kCGScrollEventUnitPixel, 15, -1, cgMomentumBegin, "momentum_begin_15");
break;
case 11:
injectScroll(kCGScrollEventUnitPixel, 8, -1, cgMomentumContinue, "momentum_continue_8");
break;
case 12:
injectScroll(kCGScrollEventUnitPixel, 3, -1, cgMomentumContinue, "momentum_continue_3");
break;
case 13:
injectScroll(kCGScrollEventUnitPixel, 0, -1, cgMomentumEnd, "momentum_end");
break;
// -- What does a cancelled gesture look like? --
case 14:
injectScroll(kCGScrollEventUnitPixel, 4, cgPhaseBegan, -1, "cancel_began");
break;
case 15:
injectScroll(kCGScrollEventUnitPixel, 0, cgPhaseCancelled, -1, "cancel_cancelled");
break;
case 16:
instr("totals", "arrivals=%d gestures=%d final_offset_y=%.1f",
g_arrivals, g_gestureN, g_offsetY);
break;
case 17:
stopApp();
break;
default:
break;
}
++g_step;
}
int int D main()main()
{
g_auto = getenv("WSI_AUTO_EXIT") !is null;
instrInit("APPKIT_F11");
instr("init_start", "auto_exit=%d", g_auto ? 1 : 0);
instr("step", "name=CGMainDisplayID");
if (CGMainDisplayID() == 0)
{
printf("SKIP: no macOS window server (CGMainDisplayID == 0)\n");
return 0;
}
auto _error_ poolpool = autoreleasepool_push();
scope (exit)
autoreleasepool_pop(pool);
g_app = NSApplication.NSApplication.sharedApplicationsharedApplication();
g_app.g_app.setActivationPolicysetActivationPolicy(NSApplicationActivationPolicyRegular);
immutable _error_ contentRectcontentRect = NSRect(NSPoint(120, 120), NSSize(480, 320));
g_win = NSWindow.NSWindow.allocalloc().NSWindow.alloc().initWithContentRectinitWithContentRect(contentRect,
NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskResizable,
NSBackingStoreBuffered, false);
g_win.g_win.setTitlesetTitle(NSString.NSString.allocalloc().NSString.alloc().initWithUTF8StringinitWithUTF8String("wsi-f11-scroll"));
g_view = RulerView.RulerView.allocalloc().RulerView.alloc().initWithFrameinitWithFrame(NSRect(NSPoint(0, 0), contentRect.contentRect.sizesize));
g_win.g_win.setContentViewsetContentView(g_view);
g_win.g_win.makeKeyAndOrderFrontmakeKeyAndOrderFront(null);
g_app.g_app.activateIgnoringOtherAppsactivateIgnoringOtherApps(true);
g_win.g_win.makeFirstRespondermakeFirstResponder(g_view);
instr("window_created", "win_num=%ld", g_win.g_win.windowNumberwindowNumber());
instr("step", "name=script_timer_start interval_ms=150");
NSTimer.NSTimer.scheduledTimerWithTimeIntervalscheduledTimerWithTimeInterval(0.15, g_view, SEL.register("tick:"), null, true);
instr("step", "name=NSApp_run");
g_app.g_app.runrun();
instr("loop_exit", "steps=%d arrivals=%d", g_step, g_arrivals);
return 0;
}