Improved, yet compatible, widget callback system using macros (#729)

* adds FL/fl_callback.macros.H
* adds FL_FUNCTION_CALLBACK_n(widget, function, [type, data])
* adds FL_METHOD_CALLBACK_n(widget, class, instance, method, [type, data])
* adds FL_INLINE_CALLBACK_n(widget, [type, name, data], callback_body)
* adds `examples/callback`
* full documentation
This commit is contained in:
Matthias Melcher 2023-08-15 11:36:58 +02:00 committed by GitHub
parent e6440ca0a8
commit 10d9010ed9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 969 additions and 47 deletions

View File

@ -121,6 +121,8 @@ Changes in FLTK 1.4.0 Released: ??? ?? 2022
- Added Fl_Surface_Device::push_current(new_surface) and
Fl_Surface_Device::pop_current() to set/unset the current surface
receiving graphics commands.
- New macros for easy function and method callbacks with multiple
type safe arguments (see FL_METHOD_CALLBACK_1 etc.) .
New Configuration Options (ABI Version)

View File

@ -70,6 +70,24 @@ struct FL_EXPORT Fl_Label {
};
/** A class prototype that allows for additional data in callbacks.
Users can extend this class and pass it to widget callbacks. Widgets can
take ownership of the callback data, deleting the data when the widget
itself is deleted.
The destructor of this class is virtual, allowing for additional code to
deallocate resources when the user data is deleted.
\see FL_FUNCTION_CALLBACK, FL_METHOD_CALLBACK, FL_INLINE_CALLBACK
\see Fl_Widget::callback(Fl_Callback*, Fl_Callback_User_Data*, bool)
\see Fl_Widget::user_data(Fl_Callback_User_Data*, bool)
*/
class Fl_Callback_User_Data {
public:
virtual ~Fl_Callback_User_Data() { }
};
/** Fl_Widget is the base class for all widgets in FLTK.
@ -152,7 +170,7 @@ protected:
CLIP_CHILDREN = 1<<11, ///< all drawing within this widget will be clipped (Fl_Group)
MENU_WINDOW = 1<<12, ///< a temporary popup window, dismissed by clicking outside (Fl_Window)
TOOLTIP_WINDOW = 1<<13, ///< a temporary popup, transparent to events, and dismissed easily (Fl_Window)
MODAL = 1<<14, ///< a window blocking input to all other winows (Fl_Window)
MODAL = 1<<14, ///< a window blocking input to all other windows (Fl_Window)
NO_OVERLAY = 1<<15, ///< window not using a hardware overlay plane (Fl_Menu_Window)
GROUP_RELATIVE = 1<<16, ///< Reserved, not implemented. DO NOT USE.
COPIED_TOOLTIP = 1<<17, ///< the widget tooltip is internally copied, its destruction is handled by the widget
@ -161,6 +179,7 @@ protected:
NEEDS_KEYBOARD = 1<<20, ///< set on touch screen devices if a widget needs a keyboard when it gets the focus. Reserved, not yet used in 1.4.0. \see Fl_Widget::needs_keyboard()
IMAGE_BOUND = 1<<21, ///< binding the image to the widget will transfer ownership, so that the widget will delete the image when it is no longer needed
DEIMAGE_BOUND = 1<<22, ///< bind the inactive image to the widget, so the widget deletes the image when it is no longer needed
AUTO_DELETE_USER_DATA = 1<<23, ///< automatically call `delete` on the user_data pointer when destroying this widget; if set, user_data must point to a class derived from the class Fl_Callback_User_Data
// Note to devs: add new FLTK core flags above this line (up to 1<<28).
@ -667,12 +686,27 @@ public:
*/
Fl_Callback_p callback() const {return callback_;}
/** Sets the current callback function for the widget.
/** Sets the current callback function and data for the widget.
Each widget has a single callback.
\param[in] cb new callback
\param[in] p user data
*/
void callback(Fl_Callback* cb, void* p) {callback_ = cb; user_data_ = p;}
void callback(Fl_Callback* cb, void* p) {
callback_ = cb;
user_data(p);
}
/** Sets the current callback function and managed user data for the widget.
Setting auto_free will transfer ownership of the callback user data to the
widget. Deleting the widget will then also delete the user data.
\param[in] cb new callback
\param[in] p user data
\param[in] auto_free if set, the widget will free user data when destroyed
*/
void callback(Fl_Callback* cb, Fl_Callback_User_Data* p, bool auto_free) {
callback_ = cb;
user_data(p, auto_free);
}
/** Sets the current callback function for the widget.
Each widget has a single callback.
@ -695,7 +729,7 @@ public:
*/
void callback(Fl_Callback1* cb, long p = 0) {
callback_ = (Fl_Callback*)(fl_intptr_t)(cb);
user_data_ = (void*)(fl_intptr_t)p;
user_data((void*)(fl_intptr_t)p);
}
/** Gets the user data for this widget.
@ -704,11 +738,11 @@ public:
*/
void* user_data() const {return user_data_;}
/** Sets the user data for this widget.
Sets the new user data (void *) argument that is passed to the callback function.
\param[in] v new user data
*/
void user_data(void* v) {user_data_ = v;}
/** \brief Sets the user data for this widget. */
void user_data(void* v);
/** \brief Sets the user data for this widget. */
void user_data(Fl_Callback_User_Data* v, bool auto_free);
/** Gets the current user data (long) argument that is passed to the callback function.
@ -727,7 +761,7 @@ public:
\see argument()
*/
void argument(long v) {user_data_ = (void*)(fl_intptr_t)v;}
void argument(long v) {user_data((void*)(fl_intptr_t)v);}
/** Returns the conditions under which the callback is called.

607
FL/fl_callback_macros.H Normal file
View File

@ -0,0 +1,607 @@
/*
* Macros for easy callbacks for the Fast Light Tool Kit (FLTK).
*
* Copyright 2023 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
*/
#ifndef _FL_FL_CALLBACK_MACROS_H_
#define _FL_FL_CALLBACK_MACROS_H_
#include <stdlib.h>
/**
\file fl_callback_macros.H
This file provides macros for easy function and method callbacks
with multiple type safe arguments.
*/
#ifdef FL_DOXYGEN
/**
\brief Declare a C function callback with custom parameters.
You can declare a plain C function callback or a static method callback with
custom parameters using this macro. It simplifies the process of calling
arbitrary functions with up to five custom parameters. The macro generates
code that ensures type safety and expands FLTK's standard callbacks, which
are limited to a single `void*` or `long` argument.
To use the macro, you provide the widget that will handle the callback as the
first argument. The second argument can be either a regular function or a
static method in any class.
Following these arguments, you can include up to five pairs, where each
pair consists of a type and a value. For example,
`int, 3` specifies an integer parameter with a value of 3. If you need to
pass two arguments, you can use two pairs, like this:
`int, 3, int, 4`. The last digit of the macro name must be the same as
the number of pairs (0..5)
Whenever the code generated by the macro is called, the custom parameters are
duplicated and marked for automatic deallocation using `delete` when the
callback widget is destroyed.
\code{.cpp}
#include <FL/fl_callback_macros.H>
...
Fl_Button *btn1 = new Fl_Button(10, 10, 100, 20, "Beep");
FL_FUNCTION_CALLBACK(btn1, fl_beep);
...
Fl_Button *btn2 = new Fl_Button(10, 40, 100, 20, "Hello");
FL_FUNCTION_CALLBACK(btn2,
fl_message,
const char *, text, "Hello\n%d %d %d %d",
int, a, 1, int, b, 2, int, c, 3, int, d, 4
);
\endcode
You can find a small demonstration program showcasing the usage of
`FL_*_CALLBACK_*` in the `examples/callbacks.cxx` file.
\param WIDGET the widget that will call the callback
\param FUNC a C/C++ function or a static class method
\param TYPE0 VALUE0 TYPE1 VALUE1 TYPE2 VALUE2 a list of zero to five type/value pairs, all separated by commas
\see FL_METHOD_CALLBACK_1, FL_INLINE_CALLBACK_2
*/
#define FL_FUNCTION_CALLBACK_3(WIDGET, FUNC, TYPE0, VALUE0, TYPE1, VALUE1, TYPE2, VALUE2)
/**
\brief Declare a non-static class method callback with custom parameters.
You can declare a callback for a non-static class method with custom parameters
using this macro. It provides a convenient way to call arbitrary methods in
any class, overcoming FLTK's limitation of passing only a single `void*` or
`long` argument. Furthermore, it ensures type safety.
The first argument of the macro specifies the widget that will handle the
callback. The second argument indicates the class type to be called. The
third argument must be a pointer to an instance of that class. The
fourth argument is the name of the method within the class. That method must be
public and should not be static.
Following these arguments, you can include up to five pairs, where each
pair consists of a type and a value. For example,
`int, 3` specifies an integer parameter with a value of 3. If you need to
pass two arguments, you can use two pairs, like this:
`int, 3, int, 4`. The last digit of the macro name must be the same as
the number of pairs (0..5)
Whenever the code generated by the macro is called, the custom parameters are
duplicated and marked for automatic deallocation using `delete` when the
callback widget is destroyed.
\code{.cpp}
#include <FL/fl_callback_macros.H>
...
Fl_Button *btn = new Fl_Button(10, 10, 100, 20, "Test");
FL_METHOD_CALLBACK_1(btn, Fl_Button, btn, color, Fl_Color, FL_GREEN);
\endcode
You can find a small demonstration program showcasing the usage of
`FL_*_CALLBACK_*` in the `examples/callbacks.cxx` file.
\param WIDGET the widget that will call the callback
\param CLASS the class type
\param SELF a pointer to an instance of the class
\param METH a C++ class method that must be public and not static
\param TYPE0 VALUE0 a list of zero to five type/value pairs, all separated by commas
\see FL_FUNCTION_CALLBACK_3, FL_INLINE_CALLBACK_2
*/
#define FL_METHOD_CALLBACK_1(WIDGET, CLASS, SELF, METH, TYPE0, VALUE0)
/**
\brief Creates code to declare a callback function in line with instantiating a widget.
You can use this macro to create a function as a callback, allowing you to
define the callback function right where the widget and callback are declared,
similar to a Lambda function.
The first argument of the macro specifies the widget that will handle the
callback. Next, you can include up to five triplets, where each triplet
consists of a type, a parameter name, and a value. For example, `int, x, 3`
specifies an integer parameter with a value of 3. If you need to pass two
arguments, you can use two triplets, such as `int, x, 3, int, y, 4`. The last
digit of the macro name must be the same as the number of triplets (0..5).
The last argument is the actual function body itself.
The function body is limited to a syntax that the macro preprocessor can
handle. It should include the leading '{' and trailing '}' and may contain
local variable declarations, use global variables and functions, and use also
the variables listed and initialized in the argument triples of the macro.
Very large function bodies should be avoided because they may exceed the
admissible size of a macro argument.
Whenever the code generated by the macro is called, the custom parameters are
duplicated and marked for automatic deallocation using `delete` when the
callback widget is destroyed.
\code{.cpp}
#include <FL/fl_callback_macros.H>
...
Fl_Button *btn = new Fl_Button(10, 10, 100, 20, "Test");
FL_INLINE_CALLBACK_1(btn,
Fl_String, name, btn->label(),
{
fl_message("Greetings from the %s button", name);
}
);
\endcode
You can find a small demonstration program showcasing the usage of
`FL_*_CALLBACK_*` in the `examples/callbacks.cxx` file.
\param WIDGET the widget that will call the callback
\param TYPE0 the type of the first parameter in the function call
\param NAME0 an arbitrary variable name that can be used as a parameter in the function body
\param VALUE0 a constant value or a variable; the value of the variable is copied when the callback is created
\param TYPE1 NAME1 VALUE1 as above; there are six macros that support 0 to 5 parameters
\param LAMBDA_FUNCTION_BODY the function body within the limits of the C macro preprocessor
\see FL_METHOD_CALLBACK_1, FL_FUNCTION_CALLBACK_3
*/
#define FL_INLINE_CALLBACK_2(WIDGET, TYPE0, NAME0, VALUE0, TYPE1, VALUE1, NAME1, LAMBDA_FUNCTION_BODY)
#else // FL_DOXYGEN
/*
These two macros make it possible to call macros with names that are created
by concatenating the name in x and (in this context) the number in y.
*/
#define _FL_CBD_CONCAT_IMPL(x, y) x##y
#define _FL_CBD_CONCAT(x, y) _FL_CBD_CONCAT_IMPL(x, y)
/*
Create a unique name for the derived class based on the current source code
line number.
*/
#define _FL_CBD_CLASS_NAME _FL_CBD_CONCAT(Fl_Callback_User_Data_,__LINE__)
/*
These macros create boilerplate code for callbacks to functions and
static class methods with up to five arguments.
This macro invocation for example
```
FL_FUNCTION_CALLBACK_2( func_cb_btn_2, hello_2_args_cb,
const char *, text, "FLTK",
int, number, 2 );
```
will generate the following code:
```
do {
class Fl_Callback_User_Data_92 : public Fl_Callback_User_Data {
public:
const char * p0_;
int p1_;
static void cb(Fl_Widget *w, void *user_data) {
Fl_Callback_User_Data_92 *d = (Fl_Callback_User_Data_92*)user_data;
hello_2_args_cb(d->p0_, d->p1_);
};
Fl_Callback_User_Data_92(const char * p0, int p1)
: p0_(p0),
p1_(p1)
{ }
};
func_cb_btn_2->callback(Fl_Callback_User_Data_92::cb,
new Fl_Callback_User_Data_92("FLTK", 2),
true);
} while(0)
```
Clicking the Fl_Button `func_cb_btn_2` will call `hello_2_args_cb("FLTK", 2)`.
Deleting the button will also delete the data that was created in our
boilerplate code.
*/
#define FL_FUNCTION_CALLBACK_5(WIDGET, FUNC, TYPE0, VALUE0, TYPE1, VALUE1, TYPE2, VALUE2, TYPE3, VALUE3, TYPE4, VALUE4) \
do { \
class _FL_CBD_CLASS_NAME : public Fl_Callback_User_Data { \
public: \
TYPE0 p0_; TYPE1 p1_; TYPE2 p2_; TYPE3 p3_; TYPE4 p4_; \
static void cb(Fl_Widget *w, void *user_data) { \
_FL_CBD_CLASS_NAME *d = (_FL_CBD_CLASS_NAME*)user_data; \
FUNC(d->p0_, d->p1_, d->p2_, d->p3_, d->p4_); \
}; \
_FL_CBD_CLASS_NAME(TYPE0 p0, TYPE1 p1, TYPE2 p2, TYPE3 p3, TYPE4 p4) \
: p0_(p0), p1_(p1), p2_(p2), p3_(p3), p4_(p4) { }; \
}; \
WIDGET->callback(_FL_CBD_CLASS_NAME::cb, new _FL_CBD_CLASS_NAME(VALUE0, VALUE1, VALUE2, VALUE3, VALUE4), true); \
} while(0)
#define FL_FUNCTION_CALLBACK_4(WIDGET, FUNC, TYPE0, VALUE0, TYPE1, VALUE1, TYPE2, VALUE2, TYPE3, VALUE3) \
do { \
class _FL_CBD_CLASS_NAME : public Fl_Callback_User_Data { \
public: \
TYPE0 p0_; TYPE1 p1_; TYPE2 p2_; TYPE3 p3_; \
static void cb(Fl_Widget *w, void *user_data) { \
_FL_CBD_CLASS_NAME *d = (_FL_CBD_CLASS_NAME*)user_data; \
FUNC(d->p0_, d->p1_, d->p2_, d->p3_); \
}; \
_FL_CBD_CLASS_NAME(TYPE0 p0, TYPE1 p1, TYPE2 p2, TYPE3 p3) \
: p0_(p0), p1_(p1), p2_(p2), p3_(p3) { }; \
}; \
WIDGET->callback(_FL_CBD_CLASS_NAME::cb, new _FL_CBD_CLASS_NAME(VALUE0, VALUE1, VALUE2, VALUE3), true); \
} while(0)
#define FL_FUNCTION_CALLBACK_3(WIDGET, FUNC, TYPE0, VALUE0, TYPE1, VALUE1, TYPE2, VALUE2) \
do { \
class _FL_CBD_CLASS_NAME : public Fl_Callback_User_Data { \
public: \
TYPE0 p0_; TYPE1 p1_; TYPE2 p2_; \
static void cb(Fl_Widget *w, void *user_data) { \
_FL_CBD_CLASS_NAME *d = (_FL_CBD_CLASS_NAME*)user_data; \
FUNC(d->p0_, d->p1_, d->p2_); \
}; \
_FL_CBD_CLASS_NAME(TYPE0 p0, TYPE1 p1, TYPE2 p2) \
: p0_(p0), p1_(p1), p2_(p2) { }; \
}; \
WIDGET->callback(_FL_CBD_CLASS_NAME::cb, new _FL_CBD_CLASS_NAME(VALUE0, VALUE1, VALUE2), true); \
} while(0)
#define FL_FUNCTION_CALLBACK_2(WIDGET, FUNC, TYPE0, VALUE0, TYPE1, VALUE1) \
do { \
class _FL_CBD_CLASS_NAME : public Fl_Callback_User_Data { \
public: \
TYPE0 p0_; TYPE1 p1_; \
static void cb(Fl_Widget *w, void *user_data) { \
_FL_CBD_CLASS_NAME *d = (_FL_CBD_CLASS_NAME*)user_data; \
FUNC(d->p0_, d->p1_); \
}; \
_FL_CBD_CLASS_NAME(TYPE0 p0, TYPE1 p1) \
: p0_(p0), p1_(p1) { }; \
}; \
WIDGET->callback(_FL_CBD_CLASS_NAME::cb, new _FL_CBD_CLASS_NAME(VALUE0, VALUE1), true); \
} while(0)
#define FL_FUNCTION_CALLBACK_1(WIDGET, FUNC, TYPE0, VALUE0) \
do { \
class _FL_CBD_CLASS_NAME : public Fl_Callback_User_Data { \
public: \
TYPE0 p0_; \
static void cb(Fl_Widget *w, void *user_data) { \
_FL_CBD_CLASS_NAME *d = (_FL_CBD_CLASS_NAME*)user_data; \
FUNC(d->p0_); \
}; \
_FL_CBD_CLASS_NAME(TYPE0 p0) \
: p0_(p0) { }; \
}; \
WIDGET->callback(_FL_CBD_CLASS_NAME::cb, new _FL_CBD_CLASS_NAME(VALUE0), true); \
} while(0)
#define FL_FUNCTION_CALLBACK_0(WIDGET, FUNC) \
do { \
class _FL_CBD_CLASS_NAME : public Fl_Callback_User_Data { \
public: \
static void cb(Fl_Widget *w, void *user_data) { \
FUNC(); \
}; \
_FL_CBD_CLASS_NAME() { }; \
}; \
WIDGET->callback(_FL_CBD_CLASS_NAME::cb, new _FL_CBD_CLASS_NAME(), true); \
} while(0)
/*
These macros create boilerplate code for callbacks to class methods
with up to five arguments.
This macro invocation for example
```
FL_METHOD_CALLBACK_4(btn,
MyWindow, win, resize,
int, test_x+10,
int, test_y+10,
int, 320,
int, 400);
```
will generate the following code:
```
do {
class Fl_Callback_User_Data_73 : public Fl_Callback_User_Data {
public:
int p0_;
int p1_;
int p2_;
int p3_;
MyWindow *self_;
static void cb(Fl_Widget *w, void *user_data) {
Fl_Callback_User_Data_73 *d = (Fl_Callback_User_Data_73*)user_data;
d->self_->resize(d->p0_, d->p1_, d->p2_, d->p3_);
};
Fl_Callback_User_Data_73(MyWindow *self, int p0, int p1, int p2, int p3)
: self_(self), p0_(p0), p1_(p1), p2_(p2), p3_(p3) { }
};
btn->callback(Fl_Callback_User_Data_73::cb,
new Fl_Callback_User_Data_73(win, test_x+10, test_y+10, 320, 400),
true);
} while(0);
```
Clicking the Fl_Button `btn` will call
`win->resize(test_x+10, test_y+10, 320, 400);`.
Deleting the button will also delete the data that was created in our
boilerplate code.
*/
#define FL_METHOD_CALLBACK_5(WIDGET, CLASS, SELF, METHOD, TYPE0, VALUE0, TYPE1, VALUE1, TYPE2, VALUE2, TYPE3, VALUE3, TYPE4, VALUE4) \
do { \
class _FL_CBD_CLASS_NAME : public Fl_Callback_User_Data { \
public: \
CLASS *self_; \
TYPE0 p0_; TYPE1 p1_; TYPE2 p2_; TYPE3 p3_; TYPE4 p4_; \
static void cb(Fl_Widget *w, void *user_data) { \
_FL_CBD_CLASS_NAME *d = (_FL_CBD_CLASS_NAME*)user_data; \
d->self_->METHOD(d->p0_, d->p1_, d->p2_, d->p3_, d->p4_); \
}; \
_FL_CBD_CLASS_NAME(CLASS *self, TYPE0 p0, TYPE1 p1, TYPE2 p2, TYPE3 p3, TYPE4 p4) \
: self_(self), p0_(p0), p1_(p1), p2_(p2), p3_(p3), p4_(p4) { }; \
}; \
WIDGET->callback(_FL_CBD_CLASS_NAME::cb, new _FL_CBD_CLASS_NAME(SELF, VALUE0, VALUE1, VALUE2, VALUE3, VALUE4), true); \
} while(0)
#define FL_METHOD_CALLBACK_4(WIDGET, CLASS, SELF, METHOD, TYPE0, VALUE0, TYPE1, VALUE1, TYPE2, VALUE2, TYPE3, VALUE3) \
do { \
class _FL_CBD_CLASS_NAME : public Fl_Callback_User_Data { \
public: \
CLASS *self_; \
TYPE0 p0_; TYPE1 p1_; TYPE2 p2_; TYPE3 p3_; \
static void cb(Fl_Widget *w, void *user_data) { \
_FL_CBD_CLASS_NAME *d = (_FL_CBD_CLASS_NAME*)user_data; \
d->self_->METHOD(d->p0_, d->p1_, d->p2_, d->p3_); \
}; \
_FL_CBD_CLASS_NAME(CLASS *self, TYPE0 p0, TYPE1 p1, TYPE2 p2, TYPE3 p3) \
: self_(self), p0_(p0), p1_(p1), p2_(p2), p3_(p3) { }; \
}; \
WIDGET->callback(_FL_CBD_CLASS_NAME::cb, new _FL_CBD_CLASS_NAME(SELF, VALUE0, VALUE1, VALUE2, VALUE3), true); \
} while(0)
#define FL_METHOD_CALLBACK_3(WIDGET, CLASS, SELF, METHOD, TYPE0, VALUE0, TYPE1, VALUE1, TYPE2, VALUE2) \
do { \
class _FL_CBD_CLASS_NAME : public Fl_Callback_User_Data { \
public: \
CLASS *self_; \
TYPE0 p0_; TYPE1 p1_; TYPE2 p2_; \
static void cb(Fl_Widget *w, void *user_data) { \
_FL_CBD_CLASS_NAME *d = (_FL_CBD_CLASS_NAME*)user_data; \
d->self_->METHOD(d->p0_, d->p1_, d->p2_); \
}; \
_FL_CBD_CLASS_NAME(CLASS *self, TYPE0 p0, TYPE1 p1, TYPE2 p2) \
: self_(self), p0_(p0), p1_(p1), p2_(p2) { }; \
}; \
WIDGET->callback(_FL_CBD_CLASS_NAME::cb, new _FL_CBD_CLASS_NAME(SELF, VALUE0, VALUE1, VALUE2), true); \
} while(0)
#define FL_METHOD_CALLBACK_2(WIDGET, CLASS, SELF, METHOD, TYPE0, VALUE0, TYPE1, VALUE1) \
do { \
class _FL_CBD_CLASS_NAME : public Fl_Callback_User_Data { \
public: \
CLASS *self_; \
TYPE0 p0_; TYPE1 p1_; \
static void cb(Fl_Widget *w, void *user_data) { \
_FL_CBD_CLASS_NAME *d = (_FL_CBD_CLASS_NAME*)user_data; \
d->self_->METHOD(d->p0_, d->p1_); \
}; \
_FL_CBD_CLASS_NAME(CLASS *self, TYPE0 p0, TYPE1 p1) \
: self_(self), p0_(p0), p1_(p1) { }; \
}; \
WIDGET->callback(_FL_CBD_CLASS_NAME::cb, new _FL_CBD_CLASS_NAME(SELF, VALUE0, VALUE1), true); \
} while(0)
#define FL_METHOD_CALLBACK_1(WIDGET, CLASS, SELF, METHOD, TYPE0, VALUE0) \
do { \
class _FL_CBD_CLASS_NAME : public Fl_Callback_User_Data { \
public: \
CLASS *self_; \
TYPE0 p0_; \
static void cb(Fl_Widget *w, void *user_data) { \
_FL_CBD_CLASS_NAME *d = (_FL_CBD_CLASS_NAME*)user_data; \
d->self_->METHOD(d->p0_); \
}; \
_FL_CBD_CLASS_NAME(CLASS *self, TYPE0 p0) \
: self_(self), p0_(p0) { }; \
}; \
WIDGET->callback(_FL_CBD_CLASS_NAME::cb, new _FL_CBD_CLASS_NAME(SELF, VALUE0), true); \
} while(0)
#define FL_METHOD_CALLBACK_0(WIDGET, CLASS, SELF, METHOD) \
do { \
class _FL_CBD_CLASS_NAME : public Fl_Callback_User_Data { \
public: \
CLASS *self_; \
static void cb(Fl_Widget *w, void *user_data) { \
_FL_CBD_CLASS_NAME *d = (_FL_CBD_CLASS_NAME*)user_data; \
d->self_->METHOD(); \
}; \
_FL_CBD_CLASS_NAME(CLASS *self) \
: self_(self) { }; \
}; \
WIDGET->callback(_FL_CBD_CLASS_NAME::cb, new _FL_CBD_CLASS_NAME(SELF), true); \
} while(0)
/*
These macros create boilerplate code for callback functions inlined into
the widget creation code (similar to lambda functions in C++11 and up)
with up to five arguments.
This macro invocation for example
```
FL_INLINE_CALLBACK_2( // callback has two parameters
btn, // attach callback to this button
const char *, text, "FLTK", // first parameter (type, name, value)
int, number, 2, // second parameter
{ // function body
fl_message("We received the message %s with %d!", text, number);
}
);
```
will generate the following code:
```
do {
class Fl_Callback_User_Data_133 : public Fl_Callback_User_Data {
public:
const char * p0_; // store first parameter here
int p1_; // store second parameter here
// lambda style function
static void fn(const char * text, int number ) {
fl_message("We received the message %s with %d!", text, number);
};
// FLTK style callback
static void cb(Fl_Widget *w, void *user_data) {
Fl_Callback_User_Data_133 *d = (Fl_Callback_User_Data_133*)user_data;
fn(d->p0_, d->p1_);
};
// class constructor
Fl_Callback_User_Data_133(const char * p0, int p1)
: p0_(p0), // copy parameter 0
p1_(p1) // copy parameter 1
{ } // constructor body
};
// connect our class to the widget callback
btn->callback(Fl_Callback_User_Data_133::cb,
new Fl_Callback_User_Data_133("FLTK", 2),
true);
} while(0); // user code adds semicolon
```
Clicking the Fl_Button `btn` will call
`fl_message("We received the message %s with %d!", "FLTK", 2);`.
Deleting the button will also delete the data that was created in our
boilerplate code.
*/
#define FL_INLINE_CALLBACK_5(WIDGET, TYPE0, NAME0, VALUE0, TYPE1, NAME1, VALUE1, TYPE2, NAME2, VALUE2, TYPE3, NAME3, VALUE3, TYPE4, NAME4, VALUE4, LAMBDA) \
do { \
class _FL_CBD_CLASS_NAME : public Fl_Callback_User_Data { \
public: \
TYPE0 p0_; TYPE1 p1_; TYPE2 p2_; TYPE3 p3_; TYPE4 p4_; \
static void fn(TYPE0 NAME0, TYPE1 NAME1, TYPE2 NAME2, TYPE3 NAME3, TYPE4 NAME4) \
LAMBDA; \
static void cb(Fl_Widget *w, void *user_data) { \
_FL_CBD_CLASS_NAME *d = (_FL_CBD_CLASS_NAME*)user_data; \
_FL_CBD_CLASS_NAME::fn(d->p0_, d->p1_, d->p2_, d->p3_, d->p4_); \
}; \
_FL_CBD_CLASS_NAME(TYPE0 p0, TYPE1 p1, TYPE2 p2, TYPE3 p3, TYPE4 p4) \
: p0_(p0), p1_(p1), p2_(p2), p3_(p3), p4_(p4) { }; \
}; \
WIDGET->callback(_FL_CBD_CLASS_NAME::cb, new _FL_CBD_CLASS_NAME(VALUE0, VALUE1, VALUE2, VALUE3, VALUE4), true); \
} while(0)
#define FL_INLINE_CALLBACK_4(WIDGET, TYPE0, NAME0, VALUE0, TYPE1, NAME1, VALUE1, TYPE2, NAME2, VALUE2, TYPE3, NAME3, VALUE3, LAMBDA) \
do { \
class _FL_CBD_CLASS_NAME : public Fl_Callback_User_Data { \
public: \
TYPE0 p0_; TYPE1 p1_; TYPE2 p2_; TYPE3 p3_; \
static void fn(TYPE0 NAME0, TYPE1 NAME1, TYPE2 NAME2, TYPE3 NAME3) \
LAMBDA; \
static void cb(Fl_Widget *w, void *user_data) { \
_FL_CBD_CLASS_NAME *d = (_FL_CBD_CLASS_NAME*)user_data; \
_FL_CBD_CLASS_NAME::fn(d->p0_, d->p1_, d->p2_, d->p3_); \
}; \
_FL_CBD_CLASS_NAME(TYPE0 p0, TYPE1 p1, TYPE2 p2, TYPE3 p3) \
: p0_(p0), p1_(p1), p2_(p2), p3_(p3) { }; \
}; \
WIDGET->callback(_FL_CBD_CLASS_NAME::cb, new _FL_CBD_CLASS_NAME(VALUE0, VALUE1, VALUE2, VALUE3), true); \
} while(0)
#define FL_INLINE_CALLBACK_3(WIDGET, TYPE0, NAME0, VALUE0, TYPE1, NAME1, VALUE1, TYPE2, NAME2, VALUE2, LAMBDA) \
do { \
class _FL_CBD_CLASS_NAME : public Fl_Callback_User_Data { \
public: \
TYPE0 p0_; TYPE1 p1_; TYPE2 p2_; \
static void fn(TYPE0 NAME0, TYPE1 NAME1, TYPE2 NAME2) \
LAMBDA; \
static void cb(Fl_Widget *w, void *user_data) { \
_FL_CBD_CLASS_NAME *d = (_FL_CBD_CLASS_NAME*)user_data; \
_FL_CBD_CLASS_NAME::fn(d->p0_, d->p1_, d->p2_); \
}; \
_FL_CBD_CLASS_NAME(TYPE0 p0, TYPE1 p1, TYPE2 p2) \
: p0_(p0), p1_(p1), p2_(p2) { }; \
}; \
WIDGET->callback(_FL_CBD_CLASS_NAME::cb, new _FL_CBD_CLASS_NAME(VALUE0, VALUE1, VALUE2), true); \
} while(0)
#define FL_INLINE_CALLBACK_2(WIDGET, TYPE0, NAME0, VALUE0, TYPE1, NAME1, VALUE1, LAMBDA) \
do { \
class _FL_CBD_CLASS_NAME : public Fl_Callback_User_Data { \
public: \
TYPE0 p0_; TYPE1 p1_; \
static void fn(TYPE0 NAME0, TYPE1 NAME1) \
LAMBDA; \
static void cb(Fl_Widget *w, void *user_data) { \
_FL_CBD_CLASS_NAME *d = (_FL_CBD_CLASS_NAME*)user_data; \
_FL_CBD_CLASS_NAME::fn(d->p0_, d->p1_); \
}; \
_FL_CBD_CLASS_NAME(TYPE0 p0, TYPE1 p1) \
: p0_(p0), p1_(p1) { }; \
}; \
WIDGET->callback(_FL_CBD_CLASS_NAME::cb, new _FL_CBD_CLASS_NAME(VALUE0, VALUE1), true); \
} while(0)
#define FL_INLINE_CALLBACK_1(WIDGET, TYPE0, NAME0, VALUE0, LAMBDA) \
do { \
class _FL_CBD_CLASS_NAME : public Fl_Callback_User_Data { \
public: \
TYPE0 p0_; \
static void fn(TYPE0 NAME0) \
LAMBDA; \
static void cb(Fl_Widget *w, void *user_data) { \
_FL_CBD_CLASS_NAME *d = (_FL_CBD_CLASS_NAME*)user_data; \
_FL_CBD_CLASS_NAME::fn(d->p0_); \
}; \
_FL_CBD_CLASS_NAME(TYPE0 p0) \
: p0_(p0) { }; \
}; \
WIDGET->callback(_FL_CBD_CLASS_NAME::cb, new _FL_CBD_CLASS_NAME(VALUE0), true); \
} while(0)
#define FL_INLINE_CALLBACK_0(WIDGET, LAMBDA) \
do { \
class _FL_CBD_CLASS_NAME : public Fl_Callback_User_Data { \
public: \
static void fn() \
LAMBDA; \
static void cb(Fl_Widget *w, void *user_data) { \
_FL_CBD_CLASS_NAME::fn(); \
}; \
_FL_CBD_CLASS_NAME() { }; \
}; \
WIDGET->callback(_FL_CBD_CLASS_NAME::cb, new _FL_CBD_CLASS_NAME(), true); \
} while(0)
#endif // FL_DOXYGEN
#endif /* !_FL_FL_CALLBACK_MACROS_H_ */

View File

@ -550,6 +550,72 @@ int xyz_data;
button->callback(xyz_callback, &xyz_data);
\endcode
\note You cannot delete a widget inside a callback, as the
widget may still be accessed by FLTK after your callback
is completed. Instead, use the Fl::delete_widget()
method to mark your widget for deletion when it is safe
to do so.
Many programmers new to FLTK or C++ try to use a
non-static class method instead of a static class method
or function for their callback. Since callbacks are done
outside a C++ class, the `this` pointer is not
initialized for class methods.
To work around this problem, define a static method
in your class that accepts a pointer to the class, and
then have the static method call the class method(s) as
needed. The data pointer you provide to the
\p callback() method of the widget can be a
pointer to the instance of your class.
\code
class Foo {
void my_callback(Fl_Widget *w);
static void my_static_callback(Fl_Widget *w, void *f) { ((Foo *)f)->my_callback(w); }
...
}
...
w->callback(my_static_callback, (void *)this);
\endcode
In an effort to make callbacks easier, more flexible, and type safe, FLTK
provides three groups of macros that generate the code needed to call class
methods directly with up to five custom parameters.
- `FL_FUNCTION_CALLBACK_#(WIDGET, FUNCTION, ...)` creates code for callbacks to
functions and static class methods with up to five arguments. The `#` must
be replaced by the number of callback arguments.
- `FL_METHOD_CALLBACK_#(WIDGET, CLASS, SELF, METH, ...)` creates code for
callbacks to arbitrary public class methods
- `FL_INLINE_CALLBACK_#(WIDGET, ..., FUNCTION_BODY)` creates code for callback
functions that are very close to (almost in the same line) the widget
creation code, similar to lambda function in C++11. The last argument of
this macro is the callback code.
The syntax is a bit unconventional, but the resulting code is flexible and
needs no additional maintenance. It is also C++98 compatible. For example:
\code
#include <FL/fl_callback_macros.H>
...
Fl_String *str = new Fl_String("FLTK");
Fl_Button *btn = new Fl_Button(10, 10, 100, 100);
FL_METHOD_CALLBACK_2(btn, Fl_String, str, insert, int, 2, const char*, "...");
...
Fl_Button *inline_cb_btn_2 = new Fl_Button(390, 60, 180, 25, "2 args");
FL_INLINE_CALLBACK_2( inline_cb_btn_2,
const char *, text, "FLTK", int, number, 2,
{
fl_message("We received the message %s with %d!", text, number);
}
);
\endcode
\see Fl_Widget::callback(Fl_Callback*, void*), FL_FUNCTION_CALLBACK_3, FL_METHOD_CALLBACK_1, FL_INLINE_CALLBACK_2
\section common_when When and Reason
Normally callbacks are performed only when the value of the
widget changes. You can change this using the Fl_Widget::when()
method:
@ -564,45 +630,13 @@ button->when(FL_WHEN_ENTER_KEY_ALWAYS);
button->when(FL_WHEN_CHANGED | FL_WHEN_NOT_CHANGED);
\endcode
<CENTER><TABLE WIDTH="80%" BORDER="1" CELLPADDING="5" CELLSPACING="0" BGCOLOR="#cccccc">
<TR>
<TD><B>Note:</B>
Within the callback, you can query why the callback was called using
Fl::callback_reason(). For example, setting
You cannot delete a widget inside a callback, as the
widget may still be accessed by FLTK after your callback
is completed. Instead, use the Fl::delete_widget()
method to mark your widget for deletion when it is safe
to do so.
\code myInput->when(FL_WHEN_RELEASE|FL_WHEN_CHANGED) \endcode
<B>Hint:</B>
Many programmers new to FLTK or C++ try to use a
non-static class method instead of a static class method
or function for their callback. Since callbacks are done
outside a C++ class, the <tt>this</tt> pointer is not
initialized for class methods.
To work around this problem, define a static method
in your class that accepts a pointer to the class, and
then have the static method call the class method(s) as
needed. The data pointer you provide to the
\p callback() method of the widget can be a
pointer to the instance of your class.
\code
class Foo {
void my_callback(Fl_Widget *w);
static void my_static_callback(Fl_Widget *w, void *f) { ((Foo *)f)->my_callback(w); }
...
}
...
w->callback(my_static_callback, (void *)this);
\endcode
</TD>
</TR>
</TABLE></CENTER>
for a text input field may return \ref FL_REASON_LOST_FOCUS or
\ref FL_REASON_CHANGED as a callback reason.
\section common_shortcuts Shortcuts

View File

@ -32,6 +32,7 @@ file (MAKE_DIRECTORY ${EXECUTABLE_OUTPUT_PATH})
set (SIMPLE_SOURCES
chart-simple
callbacks
browser-simple
draggable-group
howto-add_fd-and-popen

View File

@ -28,6 +28,7 @@ ALL = animgifimage$(EXEEXT) \
animgifimage-resize$(EXEEXT) \
browser-simple$(EXEEXT) \
cairo-draw-x$(EXEEXT) \
callbacks$(EXEEXT) \
chart-simple$(EXEEXT) \
draggable-group$(EXEEXT) \
howto-add_fd-and-popen$(EXEEXT) \

152
examples/callbacks.cxx Normal file
View File

@ -0,0 +1,152 @@
//
// Callback macros example program for the Fast Light Tool Kit (FLTK).
//
// Copyright 2023 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
//
#include <stdio.h>
#include <FL/Fl.H>
#include <FL/Fl_Window.H>
#include <FL/Fl_Box.H>
#include <FL/Fl_Button.H>
#include <FL/Fl_String.H>
#include <FL/fl_ask.H>
#include <FL/fl_callback_macros.H>
Fl_Window *window = NULL;
/*
Here is a list of callback functions that can take custom parameters and are
not limited to FLTK's built-in `void*` or `long` user_data.
*/
void hello_0_args_cb() {
fl_message("Hello with 0 arguments");
}
void hello_2_args_cb(Fl_String &text, int number) {
fl_message("Hello with 2 arguments,\n\"%s\" and '%d'", text.c_str(), number);
}
void hello_4_args_cb(int a1, int a2, int a3, int a4) {
fl_message("Hello with 4 arguments:\n%d %d %d %d", a1, a2, a3, a4);
}
/*
We create our own little class here that uses method callbacks.
*/
class MyButton : public Fl_Button {
// id will be set in the constructor
int id_;
public:
// create a simple push button
MyButton(int x, int y, int w, int h, const char *label, int id)
: Fl_Button(x, y, w, h, label), id_(id)
{ }
// public non-static callback method
void hello(int a, int b, int c) {
// it's not a static method, so we have full access to all members, i.e. 'id_'
fl_message("MyButton has the id %d\nand was called with the custom parameters\n%d, %d, and %d.", id_, a, b, c);
}
};
/*
Whenever the code created by the macro is called, custom parameters are
duplicated. This ensures that each widget created dynamically with the same
function has its own separate set of user data at runtime. Consequently,
multiple widgets can be created dynamically, and each widget will have its
own unique set of parameters.
*/
void make_button(Fl_Window *win, int set) {
int y_lut[] = { 60, 90 };
const char *label_lut[] = { "id 2 (5, 6, 7)", "id 3 (6, 7, 8)" };
MyButton *btn = new MyButton(200, y_lut[set], 180, 25, label_lut[set], set+2);
FL_METHOD_CALLBACK_3(btn, MyButton, btn, hello, int, set+5, int, set+6, int, set+7);
}
int main(int argc, char ** argv) {
window = new Fl_Window(580, 120);
/* -- testing function callbacks with multiple arguments
These buttons demo the use of the CALLBACK macro to call standard C style
functions with up to five custom parameters.
*/
new Fl_Box(10, 5, 180, 25, "Function Callbacks:");
Fl_Button *func_cb_btn_0 = new Fl_Button(10, 30, 180, 25, "0 args");
FL_FUNCTION_CALLBACK_0(func_cb_btn_0, hello_0_args_cb);
Fl_Button *func_cb_btn_2 = new Fl_Button(10, 60, 180, 25, "2 args");
FL_FUNCTION_CALLBACK_2(func_cb_btn_2, hello_2_args_cb, Fl_String, "FLTK", int, 2);
Fl_Button *func_cb_btn_4 = new Fl_Button(10, 90, 180, 25, "4 args");
FL_FUNCTION_CALLBACK_4(func_cb_btn_4, hello_4_args_cb, int, 1, int, 2, int, 3, int, 4);
/* -- testing non-static method callbacks with multiple arguments
The following buttons call non-static class methods with custom parameters.
Check the class above to see how this is implemented.
*/
new Fl_Box(200, 5, 180, 25, "Method Callbacks:");
MyButton *meth_cb_btn_0 = new MyButton(200, 30, 180, 25, "id 1 (1, 2, 3)", 1);
// 1: the macro needs a pointer to the button first
// 2: we can call a method in any class, but here we call ourselves
// 3: call a method in our own class, so we need to set 'meth_cb_btn_0' again
// Note: we could just as well call a method in a different class.
// 4: this is the method that we want to call; it must be "public"
// 5: add zero to five parameter triplets, note the comma placement
FL_METHOD_CALLBACK_3(meth_cb_btn_0, MyButton, meth_cb_btn_0, hello, int, 1, int, 2, int, 3);
// call the same FL_METHOD_CALLBACK macro multiple times to ensure we get
// individual parameter sets
make_button(window, 0);
make_button(window, 1);
/* -- testing inline callback functions
Adding a simple Lambda style functionality to FLTK without actually using
lambdas and staying C++99 compatible.
*/
new Fl_Box(390, 5, 180, 25, "Inline Callbacks:");
Fl_Button *inline_cb_btn_0 = new Fl_Button(390, 30, 180, 25, "0 args");
FL_INLINE_CALLBACK_0(inline_cb_btn_0,
{ fl_message("Inline callback with 0 args."); }
);
Fl_Button *inline_cb_btn_2 = new Fl_Button(390, 60, 180, 25, "2 args");
FL_INLINE_CALLBACK_2(inline_cb_btn_2,
const char *, text, "FLTK", int, number, 2,
{ fl_message("We received the message %s with %d!", text, number); }
);
Fl_Button *inline_cb_btn_4 = new Fl_Button(390, 90, 180, 25, "4 args");
FL_INLINE_CALLBACK_4(inline_cb_btn_4,
int, x, window->x(),
int, y, window->y(),
int, w, window->w(),
int, h, window->h(),
{ fl_message("The main window was at\nx:%d, y:%d, w:%d, h:%d\n"
"when the callback was created\n"
"and is now at x:%d, y:%d", x, y, w, h,
window->x(), window->y());
}
);
window->end();
window->show(argc,argv);
return Fl::run();
}

View File

@ -178,6 +178,8 @@ Fl_Widget::~Fl_Widget() {
fl_throw_focus(this);
// remove stale entries from default callback queue (Fl::readqueue())
if (callback_ == default_callback) cleanup_readqueue(this);
if ( (flags_ & AUTO_DELETE_USER_DATA) && user_data_)
delete (Fl_Callback_User_Data*)user_data_;
}
/**
@ -399,3 +401,28 @@ void Fl_Widget::do_callback(Fl_Widget *widget, void *arg, Fl_Callback_Reason rea
if (callback_ != default_callback)
clear_changed();
}
/*
\brief Sets the user data for this widget.
Sets the new user data (void *) argument that is passed to the callback function.
\param[in] v new user data
*/
void Fl_Widget::user_data(void* v) {
if ((flags_ & AUTO_DELETE_USER_DATA) && user_data_)
delete (Fl_Callback_User_Data*)user_data_;
clear_flag(AUTO_DELETE_USER_DATA);
user_data_ = v;
}
/*
\brief Sets the user data for this widget.
Sets the new user data (void *) argument that is passed to the callback function.
\param[in] v new user data
\param[in] auto_free if set, the widget will free user data when destroyed; defaults to false
*/
void Fl_Widget::user_data(Fl_Callback_User_Data* v, bool auto_free) {
user_data((void*)v);
if (auto_free)
set_flag(AUTO_DELETE_USER_DATA);
}

View File

@ -17,8 +17,10 @@
#include "unittests.h"
#include <FL/Fl_Group.H>
#include <FL/Fl_Button.H>
#include <FL/Fl_Simple_Terminal.H>
#include <FL/Fl_String.H>
#include <FL/fl_callback_macros.H>
/* Test Fl_String constructor and assignment. */
TEST(Fl_String, Assignment) {
@ -216,6 +218,68 @@ TEST(Fl_Preferences, Strings) {
return true;
}
bool cb1a_ok = false, cb1b_ok = false, cb1c_ok = false;
int cb1_alloc = 0;
class MyString : public Fl_String {
public:
MyString() : Fl_String() { cb1_alloc++; }
MyString(const MyString &str) : Fl_String(str) { cb1_alloc++; }
MyString(const char *t) : Fl_String(t) { cb1_alloc++; }
~MyString() { cb1_alloc--; }
};
void cb1(MyString a, int b) {
cb1a_ok = true;
if (strcmp(a.c_str(),"FLTK")==0) cb1b_ok = true;
if (b==4) cb1c_ok = true;
}
/* Test callback macros. */
TEST(Fl_Callback_Macros, FL_FUNCTION_CALLBACK) {
Fl_Group::current(NULL);
Fl_Button *btn = new Fl_Button(10, 10, 100, 100);
FL_FUNCTION_CALLBACK_2(btn, cb1, MyString, "FLTK", int, 4);
do { class Fl_Callback_User_Data_240 : public Fl_Callback_User_Data {
public: MyString a_; int b_;
static void cb(Fl_Widget *w, void *user_data) {
Fl_Callback_User_Data_240 *cbdata = (Fl_Callback_User_Data_240*)user_data; (void)cbdata; cb1(cbdata->a_, cbdata->b_); }; Fl_Callback_User_Data_240(MyString a, int b) : a_(a), b_(b) { } }; btn->callback(Fl_Callback_User_Data_240::cb, new Fl_Callback_User_Data_240("FLTK", 4), true); } while(0);
btn->do_callback();
delete btn;
EXPECT_TRUE(cb1a_ok); // callback called
EXPECT_TRUE(cb1b_ok); // string stored correctly
EXPECT_TRUE(cb1c_ok); // integer stored correctly
EXPECT_TRUE(cb1_alloc==0); // string destroyed correctly (allocated as often as deallocated)
return true;
}
TEST(Fl_Callback_Macros, FL_METHOD_CALLBACK) {
Fl_Group::current(NULL);
Fl_String *str = new Fl_String("FLTK");
Fl_Button *btn = new Fl_Button(10, 10, 100, 100);
FL_METHOD_CALLBACK_2(btn, Fl_String, str, insert, int, 2, const char*, "XX");
btn->do_callback();
EXPECT_STREQ(str->c_str(), "FLXXTK");
delete btn;
delete str;
return true;
}
int cb3a = 0, cb3b = 0;
TEST(Fl_Callback_Macros, FL_INLINE_CALLBACK) {
Fl_Group::current(NULL);
Fl_Button *btn = new Fl_Button(10, 10, 100, 100);
FL_INLINE_CALLBACK_2(btn,
int, a, 42, int, b, 16,
{ cb3a = a; cb3b = b; }
);
btn->do_callback();
EXPECT_EQ(cb3a, 42);
EXPECT_EQ(cb3b, 16);
delete btn;
return true;
}
//
//------- test aspects of the FLTK core library ----------
//