Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Control panel for simulations #443

Open
wants to merge 47 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
1d769c0
Simple button group class
lemniscate8 Aug 2, 2021
be39c0c
Idea for concatenating button groups
lemniscate8 Aug 2, 2021
1a160a4
Toggle button setup for play/pause button
lemniscate8 Aug 2, 2021
ce2d52e
Some setup for the toggle button
lemniscate8 Aug 3, 2021
8992466
Fix switch styling in Bootstrap v5.0
lemniscate8 Aug 3, 2021
22cb37f
Get toggle button working
lemniscate8 Aug 3, 2021
033ac59
Some documentation
lemniscate8 Aug 4, 2021
5db885f
Add styling for imbedded toggle button groups
lemniscate8 Aug 4, 2021
8f649f3
Input works on radio buttons now
lemniscate8 Aug 4, 2021
6ebd496
No need for components as members
lemniscate8 Aug 9, 2021
9f5a3fd
Control panel basics
lemniscate8 Aug 9, 2021
875640b
Allow grouping buttons in ControlPanel
lemniscate8 Aug 10, 2021
7408f8f
No more control panel for now
lemniscate8 Aug 18, 2021
2149bef
Ironing out
lemniscate8 Aug 19, 2021
b9fcedc
Use checker function
lemniscate8 Aug 19, 2021
c2e1aa0
Fix bug with toggle clicks
lemniscate8 Aug 20, 2021
3ae3215
Optimize checker and fix reference issues
lemniscate8 Aug 20, 2021
50ba064
Document control panel
lemniscate8 Aug 20, 2021
b099a3b
Remove step button disabling
lemniscate8 Aug 20, 2021
0d3256e
Add documentation
lemniscate8 Aug 20, 2021
b0be177
Swap rate and unit parameters in constructor
lemniscate8 Aug 25, 2021
5a4cc26
Add unit test for control panel
lemniscate8 Aug 25, 2021
21f8b50
Test for ToggleButtonGroup
lemniscate8 Aug 25, 2021
d0faa5c
Address feedback
lemniscate8 Sep 3, 2021
e4f3f2a
More documentation
lemniscate8 Sep 3, 2021
ca489f3
Simplify some stuff
lemniscate8 Sep 3, 2021
2aac8ac
Remove cout
lemniscate8 Sep 3, 2021
c9abe28
More descriptive template types
lemniscate8 Sep 9, 2021
8ba6455
Explain DivInfo constructor for Div
lemniscate8 Sep 9, 2021
e3db915
Use map to hold checker instances
lemniscate8 Sep 22, 2021
6be2bb2
Name change for clarity
lemniscate8 Sep 27, 2021
892c1d0
Merge branch 'master' into control-panel
mmore500 Sep 29, 2021
0ef6beb
Include string header
mmore500 Sep 29, 2021
7bab656
Include string header
mmore500 Sep 29, 2021
9179f2e
Implement and test DisjointVariant
mmore500 Sep 29, 2021
30e0ed1
Use relative path for in-library includes
mmore500 Sep 29, 2021
9927976
Fix duplicated constructor
mmore500 Sep 29, 2021
30ae1ca
Add missing header
mmore500 Sep 29, 2021
e65067c
Add Assign and Activate methods
mmore500 Sep 29, 2021
b150900
Refactor ControlPanel to use DisjointVariant
mmore500 Sep 29, 2021
359be34
Add DyanmicRefreshChecker
mmore500 Sep 29, 2021
167ce5b
Add zero-overhead static test
mmore500 Sep 29, 2021
4deacf4
Buff DisjointVariant static asserts
mmore500 Sep 29, 2021
6083037
Implement and test ApplyToAll
mmore500 Oct 1, 2021
eae6c72
Mock up describe functions
mmore500 Oct 1, 2021
a656377
Revert "Refactor ControlPanel to use DisjointVariant"
mmore500 Oct 1, 2021
414e349
Minor refactoring on @lemniscate8's approach
mmore500 Oct 1, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions include/emp/prefab/ButtonGroup.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#ifndef EMP_BUTTON_GROUP_HPP
mmore500 marked this conversation as resolved.
Show resolved Hide resolved
#define EMP_BUTTON_GROUP_HPP

#include "emp/web/Div.hpp"

namespace emp::prefab {
/**
* Use a ButtonGroup to place buttons of a similar role into the same
* container or to save space by placing buttons without gaps between them.
mmore500 marked this conversation as resolved.
Show resolved Hide resolved
*/
class ButtonGroup : public web::Div {

protected:
/**
* The protected contructor for a ButtonGroup.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mention the README.md or somewhere else where the design pattern is explained

* @param info_ref shared pointer containing presistent state
*/
ButtonGroup(web::internal::DivInfo * info_ref) : web::Div(info_ref) {
SetAttr("class", "btn-group");
}

public:
/**
* Constructor for a ButtonGroup.
* @param in_id HTML ID of ButtonGroup div
*/
ButtonGroup(const std::string & in_id="")
: ButtonGroup(new web::internal::DivInfo(in_id)) { ; }

/**
* A function useful for joining two button groups together into one unit.
* Removes buttons from the ButtonGroup passed in and adds them to this
* button group group.
* @param btn_group a button group
mmore500 marked this conversation as resolved.
Show resolved Hide resolved
*/
ButtonGroup & TakeChildren(ButtonGroup & btn_group) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're destroying it, maybe take as an R-value reference?

*this << btn_group.Children();
btn_group.Clear();
return (*this);
}
};
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// namespace emp::prefab

(here and on other namespaces)


#endif
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// #ifndef EMP_BUTTON_GROUP_HPP
(here and all the other files)

331 changes: 331 additions & 0 deletions include/emp/prefab/ControlPanel.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,331 @@
#ifndef EMP_CONTROL_PANEL_HPP
#define EMP_CONTROL_PANEL_HPP

#include "emp/base/optional.hpp"
#include "emp/base/vector.hpp"

#include "emp/prefab/ButtonGroup.hpp"
#include "emp/prefab/FontAwesomeIcon.hpp"
#include "emp/prefab/ToggleButtonGroup.hpp"

#include "emp/tools/string_utils.hpp"

#include "emp/web/Animate.hpp"
#include "emp/web/Element.hpp"
#include "emp/web/Div.hpp"

namespace emp::prefab {

namespace internal {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't indent the internal namespace

using checker_func_t = std::function<bool(const web::Animate &)>;
/**
* Shared pointer held by instances of ControlPanel class representing
* the same conceptual ControlPanel DOM object.
* Contains state that should persist while ControlPanel DOM object
* persists.
*/
class ControlPanelInfo : public web::internal::DivInfo {

// The unit for rate of refresh
std::string refresh_unit;

// A list of refresh rates with current value associated with current unit
std::map<std::string, int> refresh_rates{
{"MILLISECONDS", 100}, {"FRAMES", 5}
};

// Map units to cool refresh checkers
const std::map<std::string, const checker_func_t> refresh_checkers{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unordered_map

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

explain what they take as a param and what they return

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm... it looks like you're only allowing one refresh checker at a time?

What I would do is make two functor classes as public members of this class

ControlPanel::MillisecondRefresher

ControlPanel::FrameRefresher

Both functor classes would take a value as their constructor

instead of storing refresh_rates, refrhesh_units, and refresh_checkers

I would just store a std::function refresh_checker

{ "MILLISECONDS",
[elapsed_milliseconds = 0, this]
(const web::Animate & anim) mutable {
int rate = this->refresh_rates[this->refresh_unit];
elapsed_milliseconds += anim.GetStepTime();
if (elapsed_milliseconds > rate) {
elapsed_milliseconds -= rate;
if (elapsed_milliseconds > rate) elapsed_milliseconds = 0;
return true;
// If this is weird check out explanation of pattern in ReadoutPanel
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would just copy/paste those comments here

}
return false;
}},
{ "FRAMES",
[this](const web::Animate & anim) {
return anim.GetFrameCount() % this->refresh_rates[this->refresh_unit];
}
}
};

// The current redraw checker function
checker_func_t do_redraw;

// A list of widget that should be redrawn when do_redraw return true
emp::vector<web::Widget> refresh_list;

// A function to run every frame (as fast as possible)
std::function<void()> simulation;

public:
/**
* Construct a shared pointer to manage ControlPanel state.
*
* @param in_id HTML ID of ConfigPanel div
*/
ControlPanelInfo(const std::string & in_id="")
: DivInfo(in_id),
refresh_unit("MILLISECONDS"),
do_redraw(refresh_checkers.at(refresh_unit)),
simulation([](){ ; }) { ; }

/**
* Get a reference to the redraw checker function
* @return a redraw checker (void(const Animate &) function)
*/
const checker_func_t & GetRedrawChecker() const {
return do_redraw;
}

/**
* Set the simulation for this control panel
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Explain that by simulation you mean a void callback that can be used to advance the simulation one frame

* @param sim the function to be run every frame (as fast as possible)
*/
void SetSimulation(const std::function<void()> & sim) {
simulation = sim;
}

/**
* Get the simulation for this control panel
* @return the function to be run every frame (as fast as possible)
*/
const std::function<void()> & GetSimulation() const {
return simulation;
}

/**
* Set the refresh rate units for this control panel
* @param unit either "MILLISECONDS" or "FRAMES"
*/
void SetUnit(const std::string & unit) {
refresh_unit = unit;
do_redraw = refresh_checkers.at(refresh_unit);
}

/**
* Set the refresh rate for this control panel
* @param rate the number of milliseconds or frames between refreshes
*/
void SetRate(const int & rate) {
refresh_rates[refresh_unit] = rate;
}

/**
* Get the refresh list for this control panel
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be private? Who needs to use this outside of the class?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ControlPanel class needs access.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yup makes sense!

* @return a list of Widgets that will be refreshed every update period
*/
emp::vector<web::Widget> & GetRefreshList() {
return refresh_list;
}
};
}
/**
* Use the ConfigPanel class to add a play/pause toggle button and a step
* button to your application. You can add a simulation to be run, web
* components to be redrawn, and more Buttons or ButtonGroups to add more
* functionality to the control panel.
*/
class ControlPanel : public web::Div {

/**
* Get shared info pointer, cast to ControlPanel-specific type.
* @return cast pointer
*/
internal::ControlPanelInfo * Info() {
return dynamic_cast<internal::ControlPanelInfo *>(info);
}

/**
* Get shared info pointer, cast to const ControlPanel-specific type.
* @return cast pointer
*/
const internal::ControlPanelInfo * Info() const {
return dynamic_cast<internal::ControlPanelInfo *>(info);
}

ToggleButtonGroup toggle_run;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

briefly explain what these members are here for

Div button_line;
web::Button step;

protected:
/**
* The protected contructor for a Control panel that sets up the state
* and event handlers
* @param refresh_mode units of "MILLISECONDS" or "FRAMES"
* @param refresh_rate the number of milliseconds or frames between refreshes
* @param in_info info object associated with this component
*/
ControlPanel(
const int & refresh_rate,
const std::string & refresh_unit,
web::internal::DivInfo * in_info
) : web::Div(in_info),
toggle_run{
FontAwesomeIcon{"fa-play"}, FontAwesomeIcon{"fa-pause"},
"success", "warning",
true, false,
emp::to_string(GetID(), "_main_toggle")
},
button_line(ButtonGroup{emp::to_string(GetID(), "_main")}),
step{
[](){ ; },
"<span class=\"fa fa-step-forward\" aria-hidden=\"true\"></span>",
emp::to_string(GetID(), "_main_step")
}
{
AddAttr(
"class", "btn-toolbar",
"class", "space_groups",
"role", "toolbar",
"aria-label", "Toolbar with simulation controls"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good accessibility move

);
SetRefreshRate(refresh_rate, refresh_unit);
step.AddAttr(
"class", "btn",
"class", "btn-success"
);

static_cast<Div>(*this) << button_line;
button_line << toggle_run;
button_line << step;

AddAnimation(GetID(),
[&run_sim=GetSimulation(),
&refresh_list=Info()->GetRefreshList(),
&do_redraw=Info()->GetRedrawChecker()]
(const web::Animate & anim) mutable {
// Run the simulation function every frame
run_sim();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would name this step_sim, or update_sim

// Redraw widgets according to a rule
if(do_redraw(anim)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rename do_redraw should_redraw

I might also move auto& wid : refresh_list wid.Redraw to a helper method (probably private?) called do_redraw

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or DoRedraw

for (auto & wid : refresh_list) {
wid.Redraw();
}
}
}
);

toggle_run.SetCallback(
[&anim=Animate(GetID()), step=web::Button(step)]
(bool is_active) mutable {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const bool

if (is_active) {
anim.Start();
} else {
anim.Stop();
}
}
);

step.SetCallback([&anim=Animate(GetID())]() {
anim.Step();
});
}

public:
/**
* Contructor for a Control panel.
* @param refresh_rate the number of milliseconds or frames between refreshes
* @param refresh_mode units of "MILLISECONDS" or "FRAMES"
* @param in_id HTML ID of control panel div
*/
ControlPanel(
const int & refresh_rate,
const std::string & refresh_unit,
const std::string & in_id="")
: ControlPanel(
refresh_rate,
refresh_unit,
new internal::ControlPanelInfo(in_id)
) { ; }

/**
* Set the simulation for this control panel
* @param sim the function to be run every frame (as fast as possible)
*/
ControlPanel & SetSimulation(const std::function<void()> & sim) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SetSimulationUpdateCallback

Info()->SetSimulation(sim);
return *this;
}

/**
* Get the simulation for this control panel
* @return the function to be run every frame (as fast as possible)
*/
const std::function<void()> & GetSimulation() const {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do outside users need to access this? delete or make private?

return Info()->GetSimulation();
}

/**
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have this take a std::function and note that they can write their own or use ControlPanel::MillisecondRefresher or ControlPanel::FrameRefresher

* Set the refresh rate units for this control panel.
* @param unit either "MILLISECONDS" or "FRAMES"
* @note rates are independent for "MILLISECONDS" and "FRAMES"
*/
ControlPanel & SetRefreshUnit(const std::string & units) {
Info()->SetUnit(units);
return *this;
}

/**
* Set the refresh rate for this control panel for the current unit.
* @param rate period in frames or milliseconds
* @note rates are independent for "MILLISECONDS" and "FRAMES".
*/
void SetRefreshRate(const int & rate) {
Info()->SetRate(rate);
}

/**
* Set the refresh rate for this control panel.
* @param rate the number of milliseconds or frames between refreshes
* @param unit either "MILLISECONDS" or "FRAMES"
* @note rates are independent for "MILLISECONDS" and "FRAMES"
*/
void SetRefreshRate( const int & rate, const std::string & units) {
Info()->SetUnit(units);
Info()->SetRate(rate);
}

/**
* Adds a Widget to a list of widgets redrawn at the specified refresh rate.
* @param area a widget
*/
void AddToRefreshList(Widget & area) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a definite use case for public access to this? We can make it private and then always make it public later

Info()->GetRefreshList().push_back(area);
}

/**
* Stream operator to add a component to the control panel.
*
* Some special behavior: Buttons and ToggleButtonGroups will be added
* to the last ButtonGroup added to keep related components together.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this need to be a ButtonGroup immediately preceding?

Copy link
Collaborator Author

@lemniscate8 lemniscate8 Sep 2, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's already a button group to hold the start/stop toggle and the step button so any buttons streamed get added there until a new button group gets streamed in.

* If you want to start a new group, just stream in a new ButtonGroup.
* @param in_val a component to be added to the control panel
*/
template <typename IN_TYPE>
ControlPanel & operator<<(IN_TYPE && in_val) {
// Took soooo long to figure out but if in_val is a r-value ref
// IN_TYPE is just the TYPE. If it's l-value then it's TYPE &.
// std::decay and forward help handle both.
if constexpr(std::is_same<typename std::decay<IN_TYPE>::type, web::Button>::value ||
std::is_same<typename std::decay<IN_TYPE>::type, ToggleButtonGroup>::value) {
button_line << std::forward<IN_TYPE>(in_val);
} else if constexpr(std::is_same<typename std::decay<IN_TYPE>::type, ButtonGroup>::value) {
button_line = std::forward<ButtonGroup>(in_val);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

std::forward<IN_TYPE> ? (unless you have a good reason to deviate from that idiom)

static_cast<Div>(*this) << button_line;
} else {
static_cast<Div>(*this) << std::forward<IN_TYPE>(in_val);
}
return (*this);
}
};
}

#endif
Loading