// // 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 #include #include #include #include #include #include #include extern Fl_Menu_Item app_menu[]; extern int popup_app_menu(); Fl_Widget *cv1 = 0; Fl_Window *cvwin = 0; // // The canvas interface implements incremental drawing and handles draw events. // It also implements pressure sensitive drawing with a pen or stylus. // And it implements an overlay plane that visualizes pen event data. // class CanvasInterface { Fl_Widget *widget_; bool in_window_; bool first_draw_; Fl_Offscreen offscreen_; Fl_Color color_; enum { NONE, HOVER, DRAW, PEN_HOVER, PEN_DRAW } overlay_; int ov_x_; int ov_y_; public: CanvasInterface(Fl_Widget *w) : widget_(w), in_window_(false), first_draw_(true), offscreen_(0), color_(1), overlay_(NONE), ov_x_(0), ov_y_(0) { } CanvasInterface(Fl_Window *w) : widget_(w), in_window_(true), first_draw_(true), offscreen_(0), color_(1), overlay_(NONE), ov_x_(0), ov_y_(0) { } ~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(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(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=0) : Fl_Widget(x, y, w, h, l), CanvasInterface(this) { } ~CanvasWidget() { } int handle(int event) { // puts(fl_eventname_str(event).c_str()); int ret = cv_handle(event); return ret ? ret : Fl_Widget::handle(event); } void draw() { 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=0) : Fl_Window(x, y, w, h, l), CanvasInterface(this) { } ~CanvasWindow() { } int handle(int event) { int ret = cv_handle(event); return ret ? ret : Fl_Window::handle(event); } void draw() { 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*) { Fl_Window *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 = 0; } } }, { 0 } }; // // Show the menu and run the callback. // int popup_app_menu() { const Fl_Menu_Item *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 Fl_Window *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 CanvasWidget *canvas_widget_0 = new CanvasWidget( 10, 10, 200, 200, "CV0"); // The second canvas is inside a group Fl_Group *cv1_group = new Fl_Group(215, 5, 210, 210); cv1_group->box(FL_FRAME_BOX); CanvasWidget *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. CanvasWindow *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. CanvasWindow *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(); }