diff options
| author | Matthias Melcher <github@matthiasm.com> | 2025-11-17 21:10:01 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-11-17 21:10:01 +0100 |
| commit | fa65cd63214030011d7ac0a18818c644aff05750 (patch) | |
| tree | 8d5b7c129955a258897a14816cda802778c36910 /test | |
| parent | d623ad08a9e7d91e8b0599189bdb57e6ee1dcc94 (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')
| -rw-r--r-- | test/CMakeLists.txt | 1 | ||||
| -rw-r--r-- | test/penpal.cxx | 314 |
2 files changed, 315 insertions, 0 deletions
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 261ff969b..81d0c42fb 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -183,6 +183,7 @@ fl_create_example(navigation navigation.cxx fltk::fltk) fl_create_example(output output.cxx fltk::fltk) fl_create_example(overlay overlay.cxx fltk::fltk) fl_create_example(pack pack.cxx fltk::fltk) +fl_create_example(penpal penpal.cxx fltk::fltk) fl_create_example(pixmap pixmap.cxx fltk::images) fl_create_example(pixmap_browser pixmap_browser.cxx fltk::images) fl_create_example(preferences preferences.fl fltk::fltk) 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(); +} |
