1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
|
//
// 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 = 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<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=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();
}
|