fltk/test/penpal.cxx
Matthias Melcher 70527b3bf0 Improve access to C++ symbols.
For events, fonts, and callback reasons
2025-11-29 21:01:53 +01:00

316 lines
9.3 KiB
C++

//
// 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 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_ { 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 {
// puts(fl_eventname_str(event).c_str());
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();
}