Add macOS pen/stylus/tablet driver.

This commit is contained in:
Matthias Melcher 2025-11-16 01:43:19 +01:00
parent 1fc0676270
commit c49a3a7380
4 changed files with 473 additions and 93 deletions

View File

@ -84,6 +84,11 @@ public:
Fl_Widget_Tracker& operator=(Fl_Widget_Tracker&&) = delete;
~Fl_Widget_Tracker();
/**
Clear the widget pointer.
*/
void clear() { wp_ = nullptr; }
/**
Returns a pointer to the watched widget.
\return nullptr if the widget was deleted.
@ -94,13 +99,13 @@ public:
Check if the widget was deleted since the tracker was created.
\return 1 if the watched widget has been deleted, otherwise 0
*/
int deleted() {return wp_ == 0;}
int deleted() {return wp_ == nullptr;}
/**
Check if the widget exists and was not deleted since the tracker was created.
\return 1 if the watched widget exists, otherwise 0
*/
int exists() { return wp_ != 0; }
int exists() { return wp_ != nullptr; }
};

View File

@ -84,12 +84,45 @@ enum class Trait : uint32_t {
PROXIMITY = 0x0400,
};
/**
\brief Bitwise OR operator for Trait enum.
\param lhs Left-hand side trait flags
\param rhs Right-hand side trait flags
\return Combined trait flags
*/
inline constexpr Trait operator|(Trait lhs, Trait rhs) {
return static_cast<Trait>(static_cast<uint32_t>(lhs) | static_cast<uint32_t>(rhs));
}
/**
\brief Bitwise AND operator for Trait enum.
\param lhs Left-hand side trait flags
\param rhs Right-hand side trait flags
\return Intersection of trait flags
*/
inline constexpr Trait operator&(Trait lhs, Trait rhs) {
return static_cast<Trait>(static_cast<uint32_t>(lhs) & static_cast<uint32_t>(rhs));
}
/**
\brief Bitwise OR assignment operator for Trait enum.
\param lhs Left-hand side trait flags (modified in place)
\param rhs Right-hand side trait flags
\return Reference to modified lhs
*/
inline Trait& operator|=(Trait& lhs, Trait rhs) {
lhs = lhs | rhs;
return lhs;
}
/**
\brief Bitfield of pen state flags.
\see event_state(), event_trigger()
*/
enum class State : uint32_t {
/// No button pressed
NONE = 0x0000,
/// The tip hovers over the surface but does not touch it
TIP_HOVERS = 0x0001,
/// The tip touches the surface
@ -106,8 +139,42 @@ enum class State : uint32_t {
BUTTON2 = 0x0400,
/// Barrel button 3 is pressed
BUTTON3 = 0x0800,
/// Mask for all buttons, tip, and eraser down
ANY_DOWN = BUTTON0 | BUTTON1 | BUTTON2 | BUTTON3 | TIP_DOWN | ERASER_DOWN,
};
/*
\brief Bitwise OR operator for State enum.
\param lhs Left-hand side state flags
\param rhs Right-hand side state flags
\return Combined state flags
*/
inline constexpr State operator|(State lhs, State rhs) {
return static_cast<State>(static_cast<uint32_t>(lhs) | static_cast<uint32_t>(rhs));
}
/*
\brief Bitwise AND operator for State enum.
\param lhs Left-hand side state flags
\param rhs Right-hand side state flags
\return Intersection of state flags
*/
inline constexpr State operator&(State lhs, State rhs) {
return static_cast<State>(static_cast<uint32_t>(lhs) & static_cast<uint32_t>(rhs));
}
/*
\brief Bitwise OR assignment operator for State enum.
\param lhs Left-hand side state flags (modified in place)
\param rhs Right-hand side state flags
\return Reference to modified lhs
*/
inline State& operator|=(State& lhs, State rhs) {
lhs = lhs | rhs;
return lhs;
}
/**
\brief List of pen events.
These events extend the standard Fl_Event enumeration.
@ -315,6 +382,14 @@ FL_EXPORT extern double event_proximity();
*/
FL_EXPORT extern State event_state();
/**
* \brief Return true if the corresponding bit is set in the event state.
* \return true if any bit is set
*/
inline bool event_state(State bits) {
return ((event_state() & bits) != State::NONE);
}
/**
* \brief Returns the state change that triggered the event.
* \return a state with one bit set for the action that triggered this event
@ -323,67 +398,6 @@ FL_EXPORT extern State event_trigger();
/** @} */ // group fl_pen_events
/**
\brief Bitwise OR operator for Trait enum.
\param lhs Left-hand side trait flags
\param rhs Right-hand side trait flags
\return Combined trait flags
*/
inline constexpr Trait operator|(Trait lhs, Trait rhs) {
return static_cast<Trait>(static_cast<uint32_t>(lhs) | static_cast<uint32_t>(rhs));
}
/**
\brief Bitwise AND operator for Trait enum.
\param lhs Left-hand side trait flags
\param rhs Right-hand side trait flags
\return Intersection of trait flags
*/
inline constexpr Trait operator&(Trait lhs, Trait rhs) {
return static_cast<Trait>(static_cast<uint32_t>(lhs) & static_cast<uint32_t>(rhs));
}
/**
\brief Bitwise OR assignment operator for Trait enum.
\param lhs Left-hand side trait flags (modified in place)
\param rhs Right-hand side trait flags
\return Reference to modified lhs
*/
inline Trait& operator|=(Trait& lhs, Trait rhs) {
lhs = lhs | rhs;
return lhs;
}
/*
\brief Bitwise OR operator for State enum.
\param lhs Left-hand side state flags
\param rhs Right-hand side state flags
\return Combined state flags
*/
inline constexpr State operator|(State lhs, State rhs) {
return static_cast<State>(static_cast<uint32_t>(lhs) | static_cast<uint32_t>(rhs));
}
/*
\brief Bitwise AND operator for State enum.
\param lhs Left-hand side state flags
\param rhs Right-hand side state flags
\return Intersection of state flags
*/
inline constexpr State operator&(State lhs, State rhs) {
return static_cast<State>(static_cast<uint32_t>(lhs) & static_cast<uint32_t>(rhs));
}
/*
\brief Bitwise OR assignment operator for State enum.
\param lhs Left-hand side state flags (modified in place)
\param rhs Right-hand side state flags
\return Reference to modified lhs
*/
inline State& operator|=(State& lhs, State rhs) {
lhs = lhs | rhs;
return lhs;
}
} // namespace Pen

View File

@ -1,5 +1,5 @@
//
// Definition of default Pen/Tablet event driver.
// Definition of macOS Cocoa Pen/Tablet event driver.
//
// Copyright 2025 by Bill Spitzak and others.
//
@ -19,16 +19,103 @@
#include <FL/platform.H>
#include <FL/core/pen_events.H>
#include <FL/Fl.H>
#include <FL/Fl_Window.H>
#include <FL/Fl_Tooltip.H>
#include <FL/Fl_Widget_Tracker.H>
#include "../../Fl_Screen_Driver.H"
#import <Cocoa/Cocoa.h>
class Fl_Widget;
#include <map>
#include <memory>
extern Fl_Window *fl_xmousewin;
/*
Widgets and windows must subscribe to pen events. This is to reduce the amount
of events sent into the widget hierarchy.
Usually there is a pretty small number of subscribers, so looping through the
subscriber list should not be an issue.
All subscribers track their widget. If a widget is deleted while subscribed,
including during event handling, the driver will remove the subscription.
There is no need to explicitly unsubscribe.
*/
class Subscriber : public Fl_Widget_Tracker {
public:
Subscriber(Fl_Widget *w) : Fl_Widget_Tracker(w) { }
};
/*
Manage a list of subscribers.
*/
class SubscriberList : public std::map<Fl_Widget*, std::shared_ptr<Subscriber>> {
public:
SubscriberList() = default;
/* Remove subscribers that have a nullptr as a widget */
void cleanup() {
for (auto it = begin(); it != end(); ) {
if (!it->second->widget()) {
it = erase(it);
} else {
++it;
}
}
}
/* Add a new subscriber, or return an existing one. */
std::shared_ptr<Subscriber> add(Fl_Widget *w) {
cleanup();
auto it = find(w);
if (it == end()) {
auto sub = std::make_shared<Subscriber>(w);
insert(std::make_pair(w, sub));
return sub;
} else {
return it->second;
}
}
/* Remove a subscriber form the list. */
void remove(Fl_Widget *w) {
auto it = find(w);
if (it != end()) {
it->second->clear();
erase(it);
}
}
};
static SubscriberList subscriber_list_;
static std::shared_ptr<Subscriber> pushed_;
static std::shared_ptr<Subscriber> below_pen_;
static NSPointingDeviceType device_type_ { NSPointingDeviceTypePen };
struct EventData {
double x { 0.0 };
double y { 0.0 };
double rx { 0.0 };
double ry { 0.0 };
double tilt_x { 0.0 };
double tilt_y { 0.0 };
double pressure { 1.0 };
double barrel_pressure { 0.0 };
double twist { 0.0 };
int device_id { 0 };
Fl::Pen::State state { (Fl::Pen::State)0 };
Fl::Pen::State trigger { (Fl::Pen::State)0 };
};
// Temporary storage of event data for the driver;
static struct EventData ev;
namespace Fl {
namespace Pen {
// double e_pressure_;
// The event data that is made available to the user during event handling
struct EventData e;
} // namespace Pen
@ -37,42 +124,310 @@ namespace Pen {
using namespace Fl::Pen;
bool fl_cocoa_tablet_handler(NSEvent*, Fl_Window*) { return false; }
Trait Fl::Pen::driver_traits() { return static_cast<Trait>(0); }
// TODO: implement this
Trait Fl::Pen::driver_traits() {
return Trait::DRIVER_AVAILABLE /* | and more! */ ;
}
Trait Fl::Pen::pen_traits(Fl_Window *window) { return static_cast<Trait>(0); }
// TODO: implement this
Trait Fl::Pen::pen_traits(Fl_Window *window) {
return Trait::DRIVER_AVAILABLE /* | and more as we discover them! */ ;
}
void Fl::Pen::subscribe(Fl_Widget* widget) { }
void Fl::Pen::subscribe(Fl_Widget* widget) {
subscriber_list_.add(widget);
}
void Fl::Pen::unsubscribe(Fl_Widget* widget) { }
void Fl::Pen::unsubscribe(Fl_Widget* widget) {
subscriber_list_.remove(widget);
}
void Fl::Pen::grab(Fl_Widget* widget) { }
// TODO: implement this - do we really need this interface?
void Fl::Pen::grab(Fl_Widget* widget) {
}
void Fl::Pen::release() { }
void Fl::Pen::release() { grab(nullptr); }
double Fl::Pen::event_x() { return 0.0; }
double Fl::Pen::event_x() { return e.x; }
double Fl::Pen::event_y() { return 0.0; }
double Fl::Pen::event_y() { return e.y; }
double Fl::Pen::event_x_root() { return 0.0; }
double Fl::Pen::event_x_root() { return e.rx; }
double Fl::Pen::event_y_root() { return 0.0; }
double Fl::Pen::event_y_root() { return e.ry; }
int Fl::Pen::event_id() { return 0; }
int Fl::Pen::event_id() { return e.device_id; }
double Fl::Pen::event_pressure() { return 1.0; }
double Fl::Pen::event_pressure() { return e.pressure; }
double Fl::Pen::event_barrel_pressure() { return 0.0; }
double Fl::Pen::event_barrel_pressure() { return e.barrel_pressure; }
double Fl::Pen::event_tilt_x() { return 0.0; }
double Fl::Pen::event_tilt_x() { return e.tilt_x; }
double Fl::Pen::event_tilt_y() { return 0.0; }
double Fl::Pen::event_tilt_y() { return e.tilt_y; }
double Fl::Pen::event_twist() { return 0.0; }
double Fl::Pen::event_twist() { return e.twist; }
// Not supported in AppKit NSEvent
double Fl::Pen::event_proximity() { return 0.0; }
State Fl::Pen::event_state();
State Fl::Pen::event_state() { return e.state; }
State Fl::Pen::event_trigger();
State Fl::Pen::event_trigger() { return e.trigger; }
/**
Copy the event state.
*/
static void copy_state() {
Fl::Pen::State tr = (Fl::Pen::State)((uint32_t)Fl::Pen::e.state ^ (uint32_t)ev.state);
Fl::Pen::e = ev;
Fl::Pen::e.trigger = tr;
Fl::e_x = (int)ev.x;
Fl::e_y = (int)ev.y;
Fl::e_x_root = (int)ev.rx;
Fl::e_y_root = (int)ev.ry;
}
/**
Offset coordinates for subwindows and subsubwindows.
*/
static void offset_subwindow_event(Fl_Widget *w, double &x, double &y) {
Fl_Widget *p = w, *q;
while (p) {
q = p->parent();
if (p->as_window() && q) {
x -= p->x();
y -= p->y();
}
p = q;
};
}
/*
Check if coordinates are within the widget box.
Coordinates are in top_window space. We iterate up the hierarchy to ensure
that we handle subwindows correctly.
*/
static bool event_inside(Fl_Widget *w, double x, double y) {
offset_subwindow_event(w, x, y);
if (w->as_window()) {
return ((x >= 0) && (y >= 0) && (x < w->w()) && (y < w->h()));
} else {
return ((x >= w->x()) && (y >= w->y()) && (x < w->x() + w->w()) && (y < w->y() + w->h()));
}
}
/*
Find the widget under the pen event.
Search the subscriber list for widgets that are inside the same top window,
are visible, and are within the give coordinates. Subwindow aware.
*/
static Fl_Widget *find_below_pen(Fl_Window *win, double x, double y) {
for (auto &sub: subscriber_list_) {
Fl_Widget *candidate = sub.second->widget();
if (candidate && (candidate->top_window() == win)) {
if (candidate->visible() && event_inside(candidate, x, y)) {
return candidate;
}
}
}
return nullptr;
}
/*
Send the current event and event data to a widget.
Note: we will get the wrong coordinates if the widget is not a child of
the current event window (LEAVE events between windows).
*/
static int pen_send(Fl_Widget *w, int event, State trigger, bool &copied) {
// Copy most event data only once
if (!copied) {
copy_state();
copied = true;
}
// Copy the top_window coordinates again as they may change when w changes
e.x = ev.x;
e.y = ev.y;
offset_subwindow_event(w, e.x, e.y);
Fl::e_x = e.x;
Fl::e_y = e.y;
// Send the event.
e.trigger = trigger;
return w->handle(event);
}
/*
Convert the NSEvent button number to Fl::Pen::State,
*/
static State button_to_trigger(NSInteger button, bool down)
{
switch (button) {
case 0:
if ( (ev.state & (State::ERASER_DOWN | State::ERASER_HOVERS)) != State::NONE ) {
return down ? State::ERASER_DOWN : State::ERASER_HOVERS;
} else {
return down ? State::TIP_DOWN : State::TIP_HOVERS;
}
case 1: return State::BUTTON0;
case 2: return State::BUTTON1;
case 3: return State::BUTTON2;
case 4: return State::BUTTON3;
default: return State::NONE;
}
}
/*
Handle events coming from Cocoa.
TODO: clickCount: store in Fl::event_clicks()
capabilityMask is useless, because it is vendor defined
TODO: enteringProximity: TabletProximityType and Subtype
*/
bool fl_cocoa_tablet_handler(NSEvent *event, Fl_Window *eventWindow)
{
// Quick access to the main type.
auto type = [event type];
// There seems nothing useful here. Ignore for now.
if ((type == NSEventTypeMouseEntered) || (type == NSEventTypeMouseExited)) {
return false;
}
// Sort out tablet-only events and mouse plus tablet events.
bool is_mouse = ((type != NSEventTypeTabletPoint) && (type != NSEventTypeTabletProximity));
// Set the subtype if one is available. Only NSEventSubtypeTabletPoint and
// NSEventSubtypeTabletProximity matter in this context
NSEventSubtype subtype = is_mouse ? [event subtype] : NSEventSubtypeMouseEvent;
// Is this a change in proximity event?
bool is_proximity = ((type == NSEventTypeTabletProximity) || (subtype == NSEventSubtypeTabletProximity));
// Is this a pen pointer event?
bool is_point = ((type == NSEventTypeTabletPoint) || (subtype == NSEventSubtypeTabletPoint));
// Check if any of the pen down, move, drag, or up events was triggered.
bool is_down = ((type == NSEventTypeLeftMouseDown) || (type == NSEventTypeRightMouseDown) || (type == NSEventTypeOtherMouseDown));
bool is_up = ((type == NSEventTypeLeftMouseUp) || (type == NSEventTypeRightMouseUp) || (type == NSEventTypeOtherMouseUp));
bool is_drag = ((type == NSEventTypeLeftMouseDragged) || (type == NSEventTypeRightMouseDragged) || (type == NSEventTypeOtherMouseDragged));
bool is_motion = is_drag || (type == NSEventTypeMouseMoved);
// Find out if we can get the pen position
bool has_position = (eventWindow != nullptr) && (is_up || is_down || is_motion || is_proximity || is_point);
// Event has extended pen data set:
if (has_position) {
// Get the position data.
auto pt = [event locationInWindow];
double s = Fl::screen_driver()->scale(0);
ev.x = pt.x/s;
ev.y = eventWindow->h() - pt.y/s;
// TODO: verify actual values: root coordinates may be used for popup windows
ev.rx = ev.x*s + eventWindow->x();
ev.ry = ev.y*s + eventWindow->y();
if (is_proximity) {
// untested: use pointingDeviceID or pointingDeviceSerialNumber instead?
ev.device_id = (int)[event vendorID];
device_type_ = [event pointingDeviceType];
} else {
// Get the pressure data.
ev.pressure = [event pressure];
ev.barrel_pressure = [event tangentialPressure];
// Get the tilt
auto tilt = [event tilt];
ev.tilt_x = -tilt.x;
ev.tilt_y = tilt.y;
// Other stuff
ev.twist = [event rotation]; // TODO: untested
}
if (device_type_ == NSPointingDeviceTypeEraser) {
if ([event buttonMask] & 1)
ev.state = State::ERASER_DOWN;
else
ev.state = State::ERASER_HOVERS;
} else {
if ([event buttonMask] & 1)
ev.state = State::TIP_DOWN;
else
ev.state = State::TIP_HOVERS;
}
if ([event buttonMask] & 0x0002) ev.state |= State::BUTTON0;
if ([event buttonMask] & 0x0004) ev.state |= State::BUTTON1;
if ([event buttonMask] & 0x0008) ev.state |= State::BUTTON2;
if ([event buttonMask] & 0x0010) ev.state |= State::BUTTON3;
// printf("0x%08x\n", [event buttonMask]);
}
Fl_Widget *receiver = nullptr;
bool pushed = false;
bool event_data_copied = false;
if (has_position) {
fl_xmousewin = eventWindow;
if (pushed_ && pushed_->widget() && (Fl::pushed() == pushed_->widget())) {
receiver = pushed_->widget();
// TODO: check Fl::grab(), clear dnd_flag?
pushed = true;
} else {
// TODO: check Fl::modal() and Fl::grab()
auto bpen = below_pen_ ? below_pen_->widget() : nullptr;
auto bmouse = Fl::belowmouse();
auto bpen_old = bmouse && (bmouse == bpen) ? bpen : nullptr;
auto bpen_now = find_below_pen(eventWindow, ev.x, ev.y);
if (bpen_now != bpen_old) {
if (bpen_old) {
pen_send(bpen_old, Fl::Pen::LEAVE, State::NONE, event_data_copied);
}
below_pen_ = nullptr;
if (bpen_now) {
if (pen_send(bpen_now, Fl::Pen::ENTER, State::NONE, event_data_copied)) {
below_pen_ = subscriber_list_[bpen_now];
Fl::belowmouse(bpen_now);
}
}
}
receiver = below_pen_ ? below_pen_->widget() : nullptr;
if (!receiver)
return 0;
}
} else {
// TODO: handle non-position events (proximity)
}
if (!receiver)
return 0;
int ret = 0;
if (is_down) {
if (!pushed) {
pushed_ = subscriber_list_[receiver];
Fl::pushed(receiver);
}
State trigger = button_to_trigger([event buttonNumber], true);
if ([event buttonNumber] == 0)
ret = pen_send(receiver, Fl::Pen::TOUCH, trigger, event_data_copied);
else
ret = pen_send(receiver, Fl::Pen::BUTTON_PUSH, trigger, event_data_copied);
} else if (is_up) {
if ( (ev.state & State::ANY_DOWN) == State::NONE ) {
Fl::pushed(nullptr);
pushed_ = nullptr;
}
State trigger = button_to_trigger([event buttonNumber], true);
if ([event buttonNumber] == 0)
ret = pen_send(receiver, Fl::Pen::LIFT, trigger, event_data_copied);
else
ret = pen_send(receiver, Fl::Pen::BUTTON_RELEASE, trigger, event_data_copied);
} else if (is_motion) {
if (pushed) {
ret = pen_send(receiver, Fl::Pen::DRAW, State::NONE, event_data_copied);
} else {
ret = pen_send(receiver, Fl::Pen::HOVER, State::NONE, event_data_copied);
}
}
// Always return 1 because at this point, we capture pen events and don't
// want mouse events anymore!
return 1;
}

View File

@ -1,7 +1,7 @@
//
// Hello, World! program for the Fast Light Tool Kit (FLTK).
// Penpal pen/stylus/tablet test program for the Fast Light Tool Kit (FLTK).
//
// Copyright 1998-2021 by Bill Spitzak and others.
// Copyright 2025 by Bill Spitzak and others.
//
// This library is free software. Distribution and use rights are outlined in
// the file "COPYING" which should have been included with this file. If this
@ -54,8 +54,10 @@ public:
void cv_pen_paint();
};
int CanvasInterface::cv_handle(int event) {
switch (event) {
int CanvasInterface::cv_handle(int event)
{
switch (event)
{
// Event handling for pen events:
case Fl::Pen::ENTER:
color_++;
@ -72,11 +74,12 @@ int CanvasInterface::cv_handle(int event) {
overlay_ = PEN_DRAW;
ov_x_ = Fl::event_x();
ov_y_ = Fl::event_y();
cv_paint();
cv_pen_paint();
widget_->redraw();
return 1;
case Fl::Pen::LIFT: return 1;
case Fl::Pen::LEAVE: overlay_ = NONE; widget_->redraw(); return 1;
// Event handling for mouse events:
case FL_ENTER:
color_++;
@ -133,7 +136,9 @@ void CanvasInterface::cv_draw() {
/* fall through */
case DRAW:
fl_arc(ov_x_-r, ov_y_-r, 2*r, 2*r, 0, 360);
break;
fl_arc(ov_x_-r/2-40*Fl::Pen::event_tilt_x(),
ov_y_-r/2-40*Fl::Pen::event_tilt_y(), r, r, 0, 360);
printf("%d\n", Fl::Pen::event_id());
break;
}
}
@ -152,8 +157,9 @@ void CanvasInterface::cv_pen_paint() {
return;
int r = static_cast<int>(32.0 * Fl::Pen::event_pressure());
int dx = in_window_ ? 0 : widget_->x(), dy = in_window_ ? 0 : widget_->y();
Fl_Color cc = Fl::Pen::event_state(Fl::Pen::State::ERASER_DOWN) ? FL_WHITE : color_;
fl_begin_offscreen(offscreen_);
fl_draw_circle(Fl::event_x()-dx-r, Fl::event_y()-dy-r, 2*r, color_);
fl_draw_circle(Fl::event_x()-dx-r, Fl::event_y()-dy-r, 2*r, cc);
fl_end_offscreen();
}
@ -198,7 +204,7 @@ int main(int argc, char **argv) {
window->end();
auto cv_window = new CanvasWindow(100, 380, 200, 200);
auto cv_window = new CanvasWindow(100, 380, 200, 200, "CV Window");
Fl::Pen::subscribe(canvas_widget_0);
Fl::Pen::subscribe(canvas_widget_1);