summaryrefslogtreecommitdiff
path: root/test/penpal.cxx
diff options
context:
space:
mode:
authorMatthias Melcher <github@matthiasm.com>2025-11-17 21:10:01 +0100
committerGitHub <noreply@github.com>2025-11-17 21:10:01 +0100
commitfa65cd63214030011d7ac0a18818c644aff05750 (patch)
tree8d5b7c129955a258897a14816cda802778c36910 /test/penpal.cxx
parentd623ad08a9e7d91e8b0599189bdb57e6ee1dcc94 (diff)
Add pen/stylus/tablet API and driver for macOS (#1326)
* define the pen/tablet support API * add pen event handler stub as a fallback * add pen device test "penpal". * Add macOS pen/stylus/tablet driver. * Add Oxygen documentation.
Diffstat (limited to 'test/penpal.cxx')
-rw-r--r--test/penpal.cxx314
1 files changed, 314 insertions, 0 deletions
diff --git a/test/penpal.cxx b/test/penpal.cxx
new file mode 100644
index 000000000..4ace5cbe9
--- /dev/null
+++ b/test/penpal.cxx
@@ -0,0 +1,314 @@
+//
+// Penpal pen/stylus/tablet test program for the Fast Light Tool Kit (FLTK).
+//
+// 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
+// file is missing or damaged, see the license at:
+//
+// https://www.fltk.org/COPYING.php
+//
+// Please see the following page on how to report bugs and issues:
+//
+// https://www.fltk.org/bugs.php
+//
+
+// The Penpal test app is here to test pen/stylus/tablet event distribution
+// in the Fl::Pen driver. Our main window has three canvases for drawing.
+// The first canvas is a child of the main window. The second canvas is
+// inside a group. The third canvas is a subwindow inside the main window.
+// A second application window is itself yet another canvas.
+
+// We can test if the events are delivered to the right receiver, if the
+// mouse and pen offsets are correct. The pen implementation also reacts
+// to pen pressure and angles. If handle() returns 1 when receiving
+// Fl::Pen::ENTER, the event handler should not send any mouse events until
+// Fl::Pen::LEAVE.
+
+#include <FL/Fl.H>
+#include <FL/Fl_Window.H>
+#include <FL/Fl_Box.H>
+#include <FL/Fl_Menu_Item.H>
+#include <FL/platform.H>
+#include <FL/fl_draw.H>
+#include <FL/fl_message.H>
+#include <FL/names.h>
+
+extern Fl_Menu_Item app_menu[];
+extern int popup_app_menu();
+Fl_Widget *cv1 { nullptr };
+Fl_Window *cvwin { nullptr };
+
+//
+// The canvas interface implements incremental drawing and handles draw events.
+// It also implement pressure sensitive drawing with a pen or stylus.
+// And it implements an overlay plane that visualizes pen event data.
+//
+class CanvasInterface {
+ Fl_Widget *widget_ { nullptr };
+ bool in_window_ { false };
+ bool first_draw_ { true };
+ Fl_Offscreen offscreen_ { 0 };
+ Fl_Color color_ { 1 };
+ enum { NONE, HOVER, DRAW, PEN_HOVER, PEN_DRAW } overlay_ { NONE };
+ int ov_x_ { 0 };
+ int ov_y_ { 0 };
+public:
+ CanvasInterface(Fl_Widget *w) : widget_(w) { }
+ CanvasInterface(Fl_Window *w) : widget_(w), in_window_(true) { }
+ ~CanvasInterface() {
+ if (offscreen_) fl_delete_offscreen(offscreen_);
+ }
+ int cv_handle(int event);
+ void cv_draw();
+ void cv_paint();
+ void cv_pen_paint();
+};
+
+
+//
+// Handle mouse and pen events.
+//
+int CanvasInterface::cv_handle(int event)
+{
+ switch (event)
+ {
+ // Event handling for pen events:
+ case Fl::Pen::ENTER: // Return 1 to receive all pen events and suppress mouse events
+ // Pen entered the widget area.
+ color_++;
+ if (color_ > 6) color_ = 1;
+ /* fall through */
+ case Fl::Pen::HOVER:
+ // Pen move over the surface without touching it.
+ overlay_ = PEN_HOVER;
+ ov_x_ = Fl::event_x();
+ ov_y_ = Fl::event_y();
+ widget_->redraw();
+ return 1;
+ case Fl::Pen::TOUCH:
+ // Pen tip or eraser just touched the surface.
+ if (Fl::event_state(FL_CTRL) || Fl::Pen::event_state(Fl::Pen::State::BUTTON0))
+ return popup_app_menu();
+ /* fall through */
+ case Fl::Pen::DRAW:
+ // Pen is dragged over the surface, or hovers with a button pressed.
+ overlay_ = PEN_DRAW;
+ ov_x_ = Fl::event_x();
+ ov_y_ = Fl::event_y();
+ cv_pen_paint();
+ widget_->redraw();
+ return 1;
+ case Fl::Pen::LIFT:
+ // Pen was just lifted from the surface and is now hovering
+ return 1;
+ case Fl::Pen::LEAVE:
+ // The pen left the drawing area.
+ overlay_ = NONE;
+ widget_->redraw();
+ return 1;
+
+ // Event handling for mouse events:
+ case FL_ENTER:
+ color_++;
+ if (color_ > 6) color_ = 1;
+ /* fall through */
+ case FL_MOVE:
+ overlay_ = HOVER;
+ ov_x_ = Fl::event_x();
+ ov_y_ = Fl::event_y();
+ widget_->redraw();
+ return 1;
+ case FL_PUSH:
+ if (Fl::event_state(FL_CTRL) || Fl::event_button() == FL_RIGHT_MOUSE)
+ return popup_app_menu();
+ /* fall through */
+ case FL_DRAG:
+ overlay_ = DRAW;
+ ov_x_ = Fl::event_x();
+ ov_y_ = Fl::event_y();
+ cv_paint();
+ widget_->redraw();
+ return 1;
+ case FL_RELEASE:
+ return 1;
+ case FL_LEAVE:
+ overlay_ = NONE;
+ widget_->redraw();
+ return 1;
+ }
+ return 0;
+}
+
+//
+// Canvas drawing copies the offscreen bitmap and then draws the overlays.
+//
+void CanvasInterface::cv_draw()
+{
+ if (first_draw_) {
+ first_draw_ = false;
+ offscreen_ = fl_create_offscreen(widget_->w(), widget_->h());
+ fl_begin_offscreen(offscreen_);
+ fl_color(FL_WHITE);
+ fl_rectf(0, 0, widget_->w(), widget_->h());
+ fl_end_offscreen();
+ }
+ int dx = in_window_ ? 0 : widget_->x(), dy = in_window_ ? 0 : widget_->y();
+ fl_copy_offscreen(dx, dy, widget_->w(), widget_->h(), offscreen_, 0, 0);
+
+ // Preset values for overlay
+ int r = 10;
+ if (overlay_ == PEN_DRAW)
+ r = static_cast<int>(32.0 * Fl::Pen::event_pressure());
+ fl_color(FL_BLACK);
+ switch (overlay_) {
+ case NONE: break;
+ case PEN_HOVER:
+ fl_color(FL_RED);
+ /* fall through */
+ case HOVER:
+ fl_xyline(ov_x_-10, ov_y_, ov_x_+10);
+ fl_yxline(ov_x_, ov_y_-10, ov_y_+10);
+ break;
+ case PEN_DRAW:
+ fl_color(FL_RED);
+ /* fall through */
+ case DRAW:
+ fl_arc(ov_x_-r, ov_y_-r, 2*r, 2*r, 0, 360);
+ 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);
+ break;
+ }
+}
+
+//
+// Paint a circle with mouse events.
+//
+void CanvasInterface::cv_paint() {
+ if (!offscreen_)
+ return;
+ int dx = in_window_ ? 0 : widget_->x(), dy = in_window_ ? 0 : widget_->y();
+ fl_begin_offscreen(offscreen_);
+ fl_draw_circle(Fl::event_x()-dx-12, Fl::event_y()-dy-12, 24, color_);
+ fl_end_offscreen();
+}
+
+//
+// Paint a circle with pen events. If the eraser is touching the surface,
+// draw a white circle.
+//
+void CanvasInterface::cv_pen_paint() {
+ if (!offscreen_)
+ return;
+ int r = static_cast<int>(32.0 * (Fl::Pen::event_pressure()*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, cc);
+ fl_end_offscreen();
+}
+
+
+//
+// A drawing canvas, based on a minimal widget.
+//
+class CanvasWidget : public Fl_Widget, CanvasInterface {
+public:
+ CanvasWidget(int x, int y, int w, int h, const char *l=nullptr)
+ : Fl_Widget(x, y, w, h, l), CanvasInterface(this) { }
+ ~CanvasWidget() override { }
+ int handle(int event) override {
+ auto ret = cv_handle(event);
+ return ret ? ret : Fl_Widget::handle(event);
+ }
+ void draw() override { return cv_draw(); }
+};
+
+//
+// A drawing canvas based on a window. Can be used as a standalone window
+// and also as a subwindow inside another window.
+//
+class CanvasWindow : public Fl_Window, CanvasInterface {
+public:
+ CanvasWindow(int x, int y, int w, int h, const char *l=nullptr)
+ : Fl_Window(x, y, w, h, l), CanvasInterface(this) { }
+ ~CanvasWindow() override { }
+ int handle(int event) override {
+ auto ret = cv_handle(event);
+ return ret ? ret : Fl_Window::handle(event);
+ }
+ void draw() override { return cv_draw(); }
+};
+
+// A popup menu with a few test tasks.
+Fl_Menu_Item app_menu[] = {
+ { "with modal window", 0, [](Fl_Widget*, void*) {
+ fl_message("None of the canvas areas should receive\n"
+ "pen events while this window is open.");
+ } },
+ { "with non-modal window", 0, [](Fl_Widget*, void*) {
+ auto w = new Fl_Window(400, 32, "Toolbox");
+ w->set_non_modal();
+ w->show();
+ } },
+ { "unsubscribe middle canvas", 0, [](Fl_Widget*, void*) {
+ if (cv1) Fl::Pen::unsubscribe(cv1);
+ } },
+ { "resubscribe middle canvas", 0, [](Fl_Widget*, void*) {
+ if (cv1) Fl::Pen::subscribe(cv1);
+ } },
+ { "delete middle canvas", 0, [](Fl_Widget*, void*) {
+ if (cv1) { cv1->top_window()->redraw(); delete cv1; cv1 = nullptr; }
+ } },
+ { nullptr }
+};
+
+//
+// Show the menu and run the callback.
+//
+int popup_app_menu() {
+ auto mi = app_menu->popup(Fl::event_x(), Fl::event_y(), "Tests");
+ if (mi) mi->do_callback((Fl_Widget*)mi);
+ return 1;
+}
+
+//
+// Main app entry point
+//
+int main(int argc, char **argv)
+{
+ // Create our main app window
+ auto window = new Fl_Window(100, 100, 640, 220, "FLTK Pen/Stylus/Tablet test, Ctrl-Tap for menu");
+
+ // One testing canvas is just a regular child widget of the window
+ auto canvas_widget_0 = new CanvasWidget( 10, 10, 200, 200, "CV0");
+
+ // The second canvas is inside a group
+ auto cv1_group = new Fl_Group(215, 5, 210, 210);
+ cv1_group->box(FL_FRAME_BOX);
+ auto canvas_widget_1 = cv1 = new CanvasWidget(220, 10, 200, 200, "CV1");
+ cv1_group->end();
+
+ // The third canvas is a window inside a window, so we can verify
+ // that pen coordinates are calculated correctly.
+ auto canvas_widget_2 = new CanvasWindow(430, 10, 200, 200, "CV2");
+ canvas_widget_2->end();
+
+ window->end();
+
+ // A fourth canvas is a top level window by itself.
+ auto cv_window = cvwin = new CanvasWindow(100, 380, 200, 200, "Canvas Window");
+
+ // All canvases subscribe to pen events.
+ Fl::Pen::subscribe(canvas_widget_0);
+ Fl::Pen::subscribe(canvas_widget_1);
+ Fl::Pen::subscribe(canvas_widget_2);
+ Fl::Pen::subscribe(cv_window);
+
+ window->show(argc, argv);
+ canvas_widget_2->show();
+ cv_window->show();
+
+ return Fl::run();
+}