315 lines
9.2 KiB
C++
315 lines
9.2 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 {
|
|
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();
|
|
}
|