diff --git a/include/emp/datastructs/DisjointVariant.hpp b/include/emp/datastructs/DisjointVariant.hpp new file mode 100644 index 0000000000..2fd3fb908c --- /dev/null +++ b/include/emp/datastructs/DisjointVariant.hpp @@ -0,0 +1,93 @@ +/** + * @note This file is part of Empirical, https://github.com/devosoft/Empirical + * @copyright Copyright (C) Michigan State University, MIT Software license; see doc/LICENSE.md + * @date 2021 + * + * @file DisjointVariant.hpp + * @brief A container similar to std::variant, where only one of a set of + * types can be active, but state is maintained for inactive types (they are + * not destructed or overwritten). + */ + + +#ifndef EMP_DISJOINT_VARIANT_HPP + +#include +#include +#include +#include + +#include "../polyfill/type_identity.hpp" + +namespace emp { + +template +class DisjointVariant { + + // Holds state for each element. + std::tuple disjoint_data; + + /// Tracks which type is active. + std::variant...> active_typeid; + + public: + + /// Forwarding constructor. + template + DisjointVariant(Args&&... args) + : disjoint_data(std::forward(args)...) + {} + + /// Switch which data element is active. + template + void Activate() { + using wrapped_active_type_t = std::type_identity; + active_typeid.template emplace(); + } + + /// Assign to data element. + template + void AssignToElement(T&& val) { + std::get(disjoint_data) = std::forward(val); + } + + /// Assign data element and set that element as active. + template + void AssignAndActivate(T&& val) { + AssignToElement( std::forward(val) ); + Activate(); + } + + /// Wraps std::visit to execute visitor on active data element. + template + decltype(auto) Visit(Visitor&& visitor) { + return std::visit( + [this, &visitor]( const auto& typeid_ ){ + using wrapped_active_type_t = std::decay_t; + using active_type_t = typename wrapped_active_type_t::type; + auto& active_data = std::get( disjoint_data ); + return std::forward(visitor)(active_data); + }, + active_typeid + ); + } + + + /// Wraps std::apply to execute function on each data element. + template + void ApplyToAll(UnaryFunction&& f) { + // adapted from https://stackoverflow.com/a/54053084 + std::apply( + [&f](auto&&... args){ + (( f(args) ), ...); + }, + disjoint_data + ); + } + + +}; + +} // namespace emp + +#endif diff --git a/include/emp/polyfill/type_identity.hpp b/include/emp/polyfill/type_identity.hpp new file mode 100644 index 0000000000..221cc0b08b --- /dev/null +++ b/include/emp/polyfill/type_identity.hpp @@ -0,0 +1,23 @@ +#ifndef POLYFILL_TYPE_IDENTITY_H +#define POLYFILL_TYPE_IDENTITY_H + +#if __cplusplus <= 201703L + +// TODO: C++20 || cpp20 +namespace std { + + // adapted from https://en.cppreference.com/w/cpp/types/type_identity + template< class T > + struct type_identity { + using type = T; + }; + +} + +#else // #if __cplusplus <= 201703L + +#include + +#endif // #if __cplusplus <= 201703L + +#endif // #ifndef POLYFILL_TYPE_IDENTITY_H diff --git a/include/emp/prefab/ButtonGroup.hpp b/include/emp/prefab/ButtonGroup.hpp new file mode 100644 index 0000000000..cae4f751b4 --- /dev/null +++ b/include/emp/prefab/ButtonGroup.hpp @@ -0,0 +1,61 @@ +/** + * @note This file is part of Empirical, https://github.com/devosoft/Empirical + * @copyright Copyright (C) Michigan State University, MIT Software license; see doc/LICENSE.md + * @date 2021 + * + * @file ButtonGroup.hpp + * @brief ButtonGroups add styling to compactly display a group of buttons and + * provides methods useful for moving buttons between groups. + */ + +#ifndef EMP_BUTTON_GROUP_HPP +#define EMP_BUTTON_GROUP_HPP + +#include "emp/web/Div.hpp" + +namespace emp::prefab { + /** + * A ButtonGroup is a container with styling specifically to display buttons. + * It also provides methods for moving buttons from one group into another + * allowing the user to combine groups. + * + * 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. + */ + class ButtonGroup : public web::Div { + + protected: + /** + * A protected contructor for a ButtonGroup for internal use only. See the + * prefab/README.md for more information on this design pattern. + * + * @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 appends them in order + * to this button group group. + * + * @param btn_group a button group + */ + ButtonGroup & TakeChildren(ButtonGroup && btn_group) { + *this << btn_group.Children(); + btn_group.Clear(); + return (*this); + } + }; +} // namespace emp::prefab + +#endif // #ifndef EMP_BUTTON_GROUP_HPP diff --git a/include/emp/prefab/ControlPanel.hpp b/include/emp/prefab/ControlPanel.hpp new file mode 100644 index 0000000000..3a9663f79c --- /dev/null +++ b/include/emp/prefab/ControlPanel.hpp @@ -0,0 +1,389 @@ +/** + * @note This file is part of Empirical, https://github.com/devosoft/Empirical + * @copyright Copyright (C) Michigan State University, MIT Software license; see doc/LICENSE.md + * @date 2021 + * + * @file ControlPanel.hpp + * @brief A ControlPanel manages a container for buttons and an Animate instance + * that can run a callback function every frame and redraw a list of Widgets + * based on frames or milliseconds elapsed and a specified rate. + * + * @todo The default control panel should eventually contain a select element + * to choose the rate unit and a numeric input for the rate. The two rates + * should be independent so that changing the unit to e.g. FRAMES will change + * the input to the FRAMES rate. + */ + +#ifndef EMP_CONTROL_PANEL_HPP +#define EMP_CONTROL_PANEL_HPP + +#include +#include +#include +#include +#include + +#include "../base/optional.hpp" +#include "../base/vector.hpp" +#include "../prefab/ButtonGroup.hpp" +#include "../prefab/FontAwesomeIcon.hpp" +#include "../prefab/ToggleButtonGroup.hpp" +#include "../tools/string_utils.hpp" +#include "../web/Animate.hpp" +#include "../web/Element.hpp" +#include "../web/Div.hpp" + +namespace emp::prefab { + +class ControlPanel; + +namespace internal { + + /** + * 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 { + friend ControlPanel; + /** + * A RefreshChecker is a functor that accepts an Animate reference + * and returns a boolean indicating whether components should be redrawn + * based on an internal "rate" for redrawing. + */ + class RefreshChecker { + protected: + int rate = 0; + public: + virtual ~RefreshChecker() {} + virtual bool ShouldRedraw(const web::Animate &) = 0; + void SetRefreshRate(const int r) { rate = r; } + int GetRefreshRate() const { return rate; } + }; + + class MillisecondRefreshChecker: public RefreshChecker { + public: + int elapsed_milliseconds = 0; + bool ShouldRedraw(const web::Animate & anim) override { + elapsed_milliseconds += anim.GetStepTime(); + if (elapsed_milliseconds > rate) { + elapsed_milliseconds -= rate; + if (elapsed_milliseconds > rate) elapsed_milliseconds = 0; + return true; + } + return false; + } + }; + + class FrameRefreshChecker: public RefreshChecker { + public: + bool ShouldRedraw(const web::Animate & anim) override { + return anim.GetFrameCount() % rate; + } + }; + + // ControlPanelInfo holds contains two specific instances of a + // RefreshChecker for milliseconds and frames to keep independent rates + // for both. + std::unordered_map> refresh_checkers{ + { "MILLISECONDS", std::make_shared() }, + { "FRAMES", std::make_shared() } + }; + + // The current redraw checker function + std::weak_ptr cur_checker; + + // A list of widget that should be redrawn when do_redraw return true + emp::vector refresh_list; + + // A void callback function to run every frame (as fast as possible) + std::function step_callback; + + /** + * 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), + cur_checker(refresh_checkers.at("MILLISECONDS")), + step_callback([](){ ; }) { ; } + + /** + * Get current refresh rate. + */ + int GetRefreshRate() const { return cur_checker.lock()->GetRefreshRate(); } + + /** + * Set the active refresh checker's refresh rate. + */ + void SetRefreshRate(const int rate) { + cur_checker.lock()->SetRefreshRate(rate); + } + + /** + * Adds a new refresh checker to the list of those available. + * @param name a name for the checker + * @param checker an instance of a functor inheriting from emp::prefab::internal::RefreshChecker + */ + template + void AddRefreshChecker(const std::string & name, SPECIALIZED_REFRESH_CHECKER && checker) { + refresh_checkers[name] = std::make_shared( + std::forward(checker) + ); + } + + /** + * Calls a void callback function to advance the state of some simulation or process. + */ + void CallStepCallback() const { step_callback(); } + + /** + * Refresh all elements on the refresh list for this control panel. + */ + void TryRefreshWidgets(const web::Animate & anim) { + if (cur_checker.lock()->ShouldRedraw(anim)) { + for (auto& widget : refresh_list) widget.Redraw(); + } + } + + /** + * Set the redraw checker function to the one specified. + */ + void SetRefreshChecker(const std::string & checker_name) { + cur_checker = refresh_checkers.at(checker_name); + } + + /** + * Sets a void callback function that will be called every frame (as often as possible). + * @param step the void callback function to advance the state of some simulation or process + * by one update every call. + */ + void SetStepCallback(const std::function & step) { + step_callback = step; + } + + /** + * Adds a Widget to a list of widgets redrawn at the specified refresh rate. + * @param area a widget + */ + void AddToRefreshList(const web::Widget & area) { + refresh_list.push_back(area); + } + + }; +} // namspace internal + + /** + * 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 { + public: + using RefreshChecker = internal::ControlPanelInfo::RefreshChecker; + + private: + /** + * Get shared info pointer, cast to ControlPanel-specific type. + * @return cast pointer + */ + internal::ControlPanelInfo * Info() { + return dynamic_cast(info); + } + + /** + * Get shared info pointer, cast to const ControlPanel-specific type. + * @return cast pointer + */ + const internal::ControlPanelInfo * Info() const { + return dynamic_cast(info); + } + + ToggleButtonGroup toggle_run; // The main toggle to stop/start animation + ButtonGroup button_line; // Current button group to which buttons are streamed + web::Button step; // Button to step forward animation + + protected: + /** + * The protected contructor for a Control panel that sets up the state + * and event handlers. For internal use only. See the prefab/README.md for + * more information on this design pattern. + * + * @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", + emp::to_string(GetID(), "_main_toggle") + }, + button_line(ButtonGroup{emp::to_string(GetID(), "_main")}), + step{ + [](){ ; }, + "", + emp::to_string(GetID(), "_main_step") + } + { + AddAttr( + "class", "btn-toolbar", + "class", "space_groups", + "role", "toolbar", + "aria-label", "Toolbar with simulation controls" + ); + SetRefreshRate(refresh_rate, refresh_unit); + step.AddAttr( + "class", "btn", + "class", "btn-success" + ); + + static_cast
(*this) << button_line; + button_line << toggle_run; + button_line << step; + + AddAnimation(GetID(), + [info=Info()](const web::Animate & anim) mutable { + // Run the simulation function every frame + info->CallStepCallback(); + // Redraw widgets according to a rule + info->TryRefreshWidgets(anim); + } + ); + + toggle_run.SetCallback( + [&anim=Animate(GetID()), step=web::Button(step)] + (const bool & is_active) mutable { + 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) + ) { ; } + + /** + * Sets a void callback function that will be called every frame (as often as possible). + * @param step the void callback function to advance the state of some simulation or process + * by one update every call. + */ + ControlPanel & SetStepCallback(const std::function & step) { + Info()->SetStepCallback(step); + return *this; + } + + /** + * Adds a new refresh checker to the list of those available. + * @param name a name for the checker + * @param checker an instance of a functor inheriting from + * emp::prefab::ControlPanel::RefreshChecker + */ + template + void AddRefreshMode(const std::string & name, SPECIALIZED_REFRESH_CHECKER && checker) { + Info()->AddRefreshChecker(name, std::forward(checker)); + } + + /** + * Set the refresh rate units for this control panel. + * @param unit either "MILLISECONDS" or "FRAMES" + * @note rates are independent for "MILLISECONDS" and "FRAMES" so changing + * units may also change the rate. + */ + ControlPanel & SetRefreshUnit(const std::string & units) { + Info()->SetRefreshChecker(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". + */ + ControlPanel & SetRefreshRate(const int & rate) { + Info()->SetRefreshRate(rate); + return *this; + } + + /** + * 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". + */ + ControlPanel & SetRefreshRate(const int & rate, const std::string & units) { + SetRefreshUnit(units); + SetRefreshRate(rate); + return *this; + } + + /** + * Gets the current refresh rate for the control panel. + * @return the current refresh rate + */ + int GetRefreshRate() const { return Info()->GetRefreshRate(); } + + /** + * Adds a Widget to a list of widgets redrawn at the specified refresh rate. + * @param area a widget + */ + void AddToRefreshList(const Widget & area) { + Info()->AddToRefreshList(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. + * 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 + 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::type, web::Button>::value || + std::is_same::type, ToggleButtonGroup>::value) { + button_line << std::forward(in_val); + } else if constexpr(std::is_same::type, ButtonGroup>::value) { + button_line = std::forward(in_val); + static_cast
(*this) << button_line; + } else { + static_cast
(*this) << std::forward(in_val); + } + return (*this); + } + }; +} // namespace emp::prefab + +#endif // #ifndef EMP_CONTROL_PANEL_HPP diff --git a/include/emp/prefab/DefaultPrefabStyles.less b/include/emp/prefab/DefaultPrefabStyles.less index a926efd061..039f802d16 100644 --- a/include/emp/prefab/DefaultPrefabStyles.less +++ b/include/emp/prefab/DefaultPrefabStyles.less @@ -23,7 +23,8 @@ @BootstrapDeviceLarge: 992px; @BootstrapDeviceExtraLarge: 1200px; -/* Glyphicon Toggle */ +// ---------------- Glyphicon Toggle ---------------- + .collapse_toggle[aria-expanded=true] .fa-angle-double-down { display: none; } @@ -49,7 +50,7 @@ float: right; } -/* Card */ +// ------------------ Card ------------------ .card{ margin-bottom: 10px; } @@ -69,7 +70,7 @@ text-align: left; } -/* Comment Box */ +// ---------------- Comment Box ---------------- .commentbox_triangle { width: 0; height: 0; @@ -90,7 +91,7 @@ display: none; } -/* Loading modal */ +// ---------------- Loading Modal ----------------- .bd-example-modal-lg .modal-dialog { display: table; position: relative; @@ -103,7 +104,8 @@ border: none; } -/* Config Panel */ +// ---------------- Config Panel ---------------- + .config_main { display: flex; flex-flow: column wrap; @@ -125,7 +127,7 @@ justify-content: flex-end; } -/* Mobile adjustments */ +// ---------------- Mobile Adjustments ---------------- // TODO: it would be much better to avoid media break points, // but need to solve the auto-flow dense/min-width issue (see .value_view). @@ -138,7 +140,7 @@ } } -/* Value Box, Control and Display */ +// ----------- Value Box, Control and Display ----------- .value_box { display: grid; grid-template-columns: auto 1fr; @@ -188,8 +190,7 @@ } } -// A value description should span from the first column -// to the last (entire row) +// A value description should span from the first column to the last (whole row) .value_description { grid-column: 1 / -1; } @@ -198,3 +199,41 @@ .excluded { display: none; } + +// -------------- ToolBar -------------- + +.btn-toolbar.space_groups > :not(:last-child) { + margin-right: .5rem; +} + +// -------------- ToggleButtonGroup -------------- + +// Overrides nested toggle button group styles when using "hide_active" to turn +// cassette style buttons into play/pause style and "grayout" to add additional +// visual cues + +div:not(.btn-group) > div.btn-group.btn-group-toggle.hide_inactive > .btn { + border-radius: .25rem; +} + +.btn-group > .hide_inactive.btn-group-toggle:first-child > .btn { + border-top-left-radius: .25rem; + border-bottom-left-radius: .25rem; +} + +.btn-group > .hide_inactive.btn-group-toggle:last-child > .btn { + border-top-right-radius: .25rem; + border-bottom-right-radius: .25rem; +} + +.btn-group > .hide_inactive.btn-group-toggle > .btn { + margin-left: unset; +} + +.btn-group-toggle.hide_inactive > label.active { + display: none; +} + +.btn-group-toggle.grayout > label:not(.active):not(:hover) { + filter: grayscale(1); +} diff --git a/include/emp/prefab/ToggleButtonGroup.hpp b/include/emp/prefab/ToggleButtonGroup.hpp new file mode 100644 index 0000000000..58ccb9e173 --- /dev/null +++ b/include/emp/prefab/ToggleButtonGroup.hpp @@ -0,0 +1,263 @@ +/** + * @note This file is part of Empirical, https://github.com/devosoft/Empirical + * @copyright Copyright (C) Michigan State University, MIT Software license; see doc/LICENSE.md + * @date 2021 + * + * @file ToggleButtonGroup.hpp + * @brief ToggleButtonGroups maintain two button elements intended to represent two + * mutually exclusive states. + */ + +#ifndef EMP_TOGGLE_BUTTON_GROUP_HPP +#define EMP_TOGGLE_BUTTON_GROUP_HPP + +#include +#include +#include + +#include "emp/tools/string_utils.hpp" +#include "emp/web/Div.hpp" +#include "emp/web/Element.hpp" +#include "emp/web/Input.hpp" +#include "emp/prefab/ButtonGroup.hpp" + +namespace emp::prefab { +namespace internal { + using on_toggle_t = std::function; + /** + * Shared pointer held by instances of ToggleButtonGroup class representing + * the same conceptual ToggleButtonGroup DOM object. + * Contains state that should persist while ToggleButtonGroup DOM object + * persists. + */ + class ToggleButtonGroupInfo : public web::internal::DivInfo { + on_toggle_t callback; // A callback to be called when the component changes states + bool is_active; // Whether the toggle is in the activated or deactivated state + + public: + ToggleButtonGroupInfo(const std::string & in_id) + : web::internal::DivInfo(in_id), callback([](bool){ ; }), is_active(false) { ; } + + /** + * Set the function to be called when the component toggles + * @param cb a callback function that accepts a boolean indicating + * whether the toggle is active or inactive + */ + void UpdateCallback(const on_toggle_t & cb) { + callback = cb; + } + + /** + * Get the function to be called when the component toggles from activated + * to deactivated. + * @return a callback function + */ + const on_toggle_t & GetCallback() const { + return callback; + } + + /** + * Determines whether the toggle is in the active state + * @return boolean + */ + bool IsActive() const { + return is_active; + } + + /** + * Sets this toggle to activated. + */ + void SetActive() { + is_active = true; + } + + /** + * Sets this toggle to deactivated. + */ + void SetInactive() { + is_active = false; + } + }; +} // namespace internal + + /** + * Use a ToggleButtonGroup to create a control with two labeled, visually + * distinct states. Choose whether the button should display cassette-style + * with two separate buttons or as a single button that flip-flops state. + * + * State can be accessed procedurally via IsActive() or in an event driven + * manner by setting a callback via SetCallback(). + */ + class ToggleButtonGroup : public ButtonGroup { + using on_toggle_t = internal::on_toggle_t; + + /** + * Get shared info pointer, cast to ToggleButton-specific type. + * + * @return cast pointer + */ + internal::ToggleButtonGroupInfo * Info() { + return dynamic_cast(info); + } + + /** + * Get shared info pointer, cast to const ToggleButton-specific type. + * + * @return cast pointer + */ + const internal::ToggleButtonGroupInfo * Info() const { + return dynamic_cast(info); + } + + protected: + /** + * Protected constructor for a ToggleButton group. For internal use only. + * See the prefab/README.md for more information on this design pattern. + * + * @param activate_indicator a string, FontAwesomeIcon or other component + * indicating that the first button actives this toggle + * @param deactivate_indicator a string, FontAwesomeIcon or other component + * indicating that the second button deactivates this toggle + * @param activate_style a bootstrap style (primary, secondary, etc) for + * the first button + * @param deactivate_style a bootstrap style (primary, secondary, etc) for + * the second button + * @param info_ref shared pointer containing presistent state + */ + template + ToggleButtonGroup( + LABEL1_TYPE && activate_indicator, + LABEL2_TYPE && deactivate_indicator, + const std::string & activate_style, + const std::string & deactivate_style, + web::internal::DivInfo * info_ref + ) : ButtonGroup(info_ref) + { + AddAttr( + "class", "btn-group-toggle", "data-toggle", "buttons" + ); + + web::Element activate_label("label", emp::to_string(GetID(), "_activate")); + web::Element deactivate_label("label", emp::to_string(GetID(), "_deactivate")); + *this << activate_label; + *this << deactivate_label; + + auto & on_toggle = Info()->GetCallback(); + + activate_label.AddAttr( + "class", "btn", + "class", emp::to_string("btn-outline-", activate_style) + ); + activate_label.OnClick([tog=*this, &handle_toggle=on_toggle]() mutable { + tog.SetActive(); + handle_toggle(true); + }); + // OnClick used due to a strange bug(?) in Javascript in which the input + // radios only fire their onchange function once. Probably due to some + // Bootstrap/jQuery weirdness. This is a last minute work around ¯\_(ツ)_/¯. + + web::Input activate_radio( + [](std::string){ ; }, + "radio", "", emp::to_string(GetID(), "_activate_radio"), + false, false + ); + activate_label << activate_radio; + activate_label << std::forward(activate_indicator); + + deactivate_label.AddAttr( + "class", "active", + "class", "btn", + "class", emp::to_string("btn-outline-", deactivate_style) + ); + deactivate_label.OnClick([tog=*this, &handle_toggle=on_toggle]() mutable { + tog.SetInactive(); + handle_toggle(false); + }); + + web::Input deactivate_radio( + [](std::string){ ; }, + "radio", "", emp::to_string(GetID(), "_deactivate_radio"), + false, true + ); + deactivate_label << deactivate_radio; + deactivate_label << std::forward(deactivate_indicator); + } + + public: + /** + * @param activate_indicator a string, FontAwesomeIcon or other component + * indicating that the first button activates this toggle + * @param deactivate_indicator a string, FontAwesomeIcon or other component + * indicating that the second button deactivates this toggle + * @param activate_style a bootstrap style (primary, secondary, etc) for + * the first button + * @param deactivate_style a bootstrap style (primary, secondary, etc) for + * the second button + * @param in_id HTML ID of ToggleButtonGroup div + */ + template + ToggleButtonGroup( + LABEL1_TYPE && activate_indicator, + LABEL2_TYPE && deactivate_indicator, + const std::string & activate_style="success", + const std::string & deactivate_style="warning", + const std::string & in_id="" + ) : ToggleButtonGroup( + std::forward(activate_indicator), + std::forward(deactivate_indicator), + activate_style, deactivate_style, + new internal::ToggleButtonGroupInfo(in_id) + ) { ; } + + /** + * Determines whether the toggle is activated or deactivated. + * @return boolean + */ + bool IsActive() const { + return Info()->IsActive(); + } + + /** + * Sets the state of the toggle to active. + */ + void SetActive() { + Info()->SetActive(); + } + + /** + * Sets the state of the toggle to inactive. + */ + void SetInactive() { + Info()->SetInactive(); + } + + /** + * Set the function to be called when the component toggles + * @param cb a void callback function that accepts a boolean indicating + * whether the toggle is active or inactive + */ + ToggleButtonGroup & SetCallback(const on_toggle_t & cb) { + Info()->UpdateCallback(cb); + return (*this); + } + + /** + * Change styling from cassette style (buttons side-by-side) to single + * button style so that button will swap between the two indicators. + */ + ToggleButtonGroup & Compress() { + AddAttr("class", "hide_inactive"); + return (*this); + } + + /** + * Add a grayscale filter to further emphasize the current state of the toggle. + */ + ToggleButtonGroup & Grayout() { + AddAttr("class", "grayout"); + return (*this); + } + }; +} // namespace emp::prefab + +#endif // #ifndef EMP_TOGGLE_BUTTON_GROUP_HPP diff --git a/include/emp/prefab/ToggleSwitch.hpp b/include/emp/prefab/ToggleSwitch.hpp index 31f964b4dc..9afede9ef5 100644 --- a/include/emp/prefab/ToggleSwitch.hpp +++ b/include/emp/prefab/ToggleSwitch.hpp @@ -38,14 +38,14 @@ namespace prefab { if (label != "") { label_element << label; } - checkbox.SetAttr("class", "custom-control-input"); - this->SetAttr("class", "custom-control custom-switch"); + checkbox.SetAttr("class", "custom-control-input form-check-input"); + this->SetAttr("class", "custom-control custom-switch form-check form-switch"); this->SetCSS( "clear", "none", "display", "inline" ); label_element.SetAttr( - "class", "custom-control-label", + "class", "custom-control-label form-check-label", "for", checkbox.GetID() ); } diff --git a/include/emp/web/Div.hpp b/include/emp/web/Div.hpp index 64ec8d1a82..ea0f87541b 100644 --- a/include/emp/web/Div.hpp +++ b/include/emp/web/Div.hpp @@ -35,6 +35,11 @@ #include "Widget.hpp" #include "init.hpp" +namespace emp::prefab { + class ButtonGroup; + class ControlPanel; +} + namespace emp { namespace web { @@ -52,6 +57,8 @@ namespace web { class TableInfo; class DivInfo : public internal::WidgetInfo { friend Element; friend Div; friend TableInfo; + friend prefab::ButtonGroup; + friend prefab::ControlPanel; protected: double scroll_top; ///< Where should div scroll to? (0.0 to 1.0) emp::vector m_children; ///< Widgets contained in this one. @@ -305,6 +312,10 @@ namespace web { // Get a properly cast version of info. internal::DivInfo * Info() { return (internal::DivInfo *) info; } const internal::DivInfo * Info() const { return (internal::DivInfo *) info; } + + // A constructor using a DivInfo pointer allows us to pass down derived + // class pointers when we need to extend the functionality of a component + // see the prefab/README.md for more about this design pattern Div(internal::DivInfo * in_info) : WidgetFacet(in_info) { ; } public: diff --git a/include/emp/web/Input.hpp b/include/emp/web/Input.hpp index 60a4755102..1c9be5c4e7 100644 --- a/include/emp/web/Input.hpp +++ b/include/emp/web/Input.hpp @@ -261,7 +261,7 @@ namespace web { Info()->callback_id = JSWrap( std::function( [b_info](std::string new_val){b_info->DoChange(new_val);} ) ); Info()->onchange_info = emp::to_string("emp.Callback(", Info()->callback_id, ", ['checkbox', 'radio'].includes(this.type) ? (this.checked ? '1' : '0') : this.value);"); // Allows user to set the checkbox to start out on/checked - if (in_type.compare("checkbox") == 0 && is_checked){ + if ((in_type.compare("checkbox") == 0 || in_type.compare("radio") == 0) && is_checked) { this->SetAttr("checked", "true"); } } diff --git a/tests/datastructs/DisjointVariant.cpp b/tests/datastructs/DisjointVariant.cpp new file mode 100644 index 0000000000..35d2ecb7ae --- /dev/null +++ b/tests/datastructs/DisjointVariant.cpp @@ -0,0 +1,50 @@ +#define CATCH_CONFIG_MAIN + +#include "third-party/Catch/single_include/catch2/catch.hpp" + +#include "emp/datastructs/DisjointVariant.hpp" + +#include +#include +#include +#include + +TEST_CASE("Test DisjointVariant", "[datastructs]") { + + emp::DisjointVariant disjoint_variant{10, 8.2}; + + std::stringstream ss; + disjoint_variant.ApplyToAll( [&ss](auto&& v){ ss << v << ' '; } ); + REQUIRE( ss.str() == "10 8.2 " ); + + disjoint_variant.Visit( [](auto&& v){ REQUIRE( v == 10 ); } ); + + // write a new value into disjoint_variant's int slot + disjoint_variant.Visit( [](auto&& v){ v = 42; } ); + disjoint_variant.Visit( [](auto&& v){ REQUIRE( v == 42 ); } ); + + disjoint_variant.Activate(); + disjoint_variant.Visit( [](auto&& v){ REQUIRE( v == 8.2 ); } ); + + disjoint_variant.Activate(); + disjoint_variant.Visit( [](auto&& v){ REQUIRE( v == 42 ); } ); + + // ensure that disjoint variant is zero overhead, + // that there's only one copy of a big type in there + static_assert( + sizeof(emp::DisjointVariant>) + < 2 * sizeof(std::array) + ); + // that overhead scales 1:1 with data + static_assert( + sizeof(emp::DisjointVariant) + - sizeof(emp::DisjointVariant) + == sizeof(std::tuple) + - sizeof(std::tuple) + ); + // that overhead is <= uint64_t + static_assert( + sizeof(emp::DisjointVariant) <= sizeof(uint64_t) + 1 + ); + +} diff --git a/tests/datastructs/Makefile b/tests/datastructs/Makefile index 8ade0e9885..859bfd9f2e 100644 --- a/tests/datastructs/Makefile +++ b/tests/datastructs/Makefile @@ -1,4 +1,4 @@ -TEST_NAMES = BloomFilter Bool Cache DynamicString Graph graph_utils hash_utils IndexMap map_utils QueueCache ra_set reference_vector set_utils SmallFifoMap SmallVector StringMap TimeQueue tuple_struct tuple_utils TypeMap UnorderedIndexMap valsort_map vector_utils +TEST_NAMES = BloomFilter Bool Cache DisjointVariant DynamicString Graph graph_utils hash_utils IndexMap map_utils QueueCache ra_set reference_vector set_utils SmallFifoMap SmallVector StringMap TimeQueue tuple_struct tuple_utils TypeMap UnorderedIndexMap valsort_map vector_utils # -O3 -Wl,--stack,8388608 -ftrack-macro-expansion=0 FLAGS = -std=c++17 -g -pthread -Wall -Wno-unused-function -Wno-unused-private-field -I../../include/ -I../../ -I../../third-party/cereal/include/ diff --git a/tests/web/ControlPanel.cpp b/tests/web/ControlPanel.cpp new file mode 100644 index 0000000000..4a7d686b4a --- /dev/null +++ b/tests/web/ControlPanel.cpp @@ -0,0 +1,154 @@ +// This file is part of Empirical, https://github.com/devosoft/Empirical +// Copyright (C) Michigan State University, 2021. +// Released under the MIT Software license; see doc/LICENSE + +#include "emp/web/_MochaTestRunner.hpp" +#include "emp/web/Document.hpp" +#include "emp/web/Div.hpp" +#include "emp/prefab/ButtonGroup.hpp" +#include "emp/prefab/ToggleButtonGroup.hpp" +#include "emp/prefab/ControlPanel.hpp" + +// Tests the integration between the Control Panel, Buttons, Button Groups, +// and ToggleButtonGroups +struct Test_Control_Panel : emp::web::BaseTest { +/* + * Creates the following control panel structure + * +------------------+------+-----+-------------------+ +---+---+---+ +--+ + * | +------+-------+ | | | +------+--------+ | | | | | | | + * | | Play | Pause | | Step | C | | Auto | Manual | | | D | A | B | | | + * | +------+-------+ | | | +------+--------+ | | | | | | | + * +------------------+------+-----+-------------------+ +---+---+---+ +--+ + * ToggleButtonGroup (default) ToggleButtonGroup (added) + */ + Test_Control_Panel() + : BaseTest({"emp_test_container"}) { + + emp::prefab::ControlPanel cp{5, "FRAMES", "ctrl"}; + emp::web::Div sim_area{"sim_area"}; + cp.AddToRefreshList(sim_area); + cp.SetRefreshRate(500, "MILLISECONDS"); + + emp::prefab::ButtonGroup husk{"husk"}; + husk << emp::web::Button{[](){;}, "A", "a_button"}; + husk << emp::web::Button{[](){;}, "B", "b_button"}; + + cp << emp::web::Button([](){;}, "C", "c_button"); + emp::prefab::ToggleButtonGroup toggle{ + "Auto", "Manual", + "primary", "secondary", + "mode_toggle" + }; + cp << toggle; + toggle.SetCallback([](bool active) { + if (active) { + std::cout << "Auto!" << std::endl; + } else { + std::cout << "Manual!" << std::endl; + } + }); + emp::prefab::ButtonGroup real{"real"}; + real << emp::web::Button([](){;}, "D", "d_button"); + real.TakeChildren(std::forward(husk)); + cp << real; + cp << husk; + + Doc("emp_test_container") << sim_area; + Doc("emp_test_container") << cp; + } + + void Describe() override { + EM_ASM({ + describe("Control Panel HTML layout", function() { + const cp = document.getElementById('ctrl'); + it("should have three children (3 main button groups)", function() { + chai.assert.equal(cp.childElementCount, 3); + }); + + describe("first button group (#ctrl_main)", function() { + const bg1 = document.getElementById('ctrl_main'); + it("should exist", function() { + chai.assert.isNotNull(bg1); + }); + it("should have control panel (#ctrl) as parent", function() { + chai.assert.equal(bg1.parentElement.getAttribute("id"), "ctrl"); + }); + describe("group's children", function() { + // has 2 toggle button group bookending two buttons + it("has children elements: div, 2 buttons, div", function() { + const nodeNames = Array.from(bg1.children).map(child => child.nodeName); + chai.assert.deepEqual(nodeNames, ["DIV", "BUTTON", "BUTTON", "DIV"]); + }); + it("has main toggle button group (#ctrl_main_toggle)", function() { + const main_toggle = document.getElementById('ctrl_main_toggle'); + chai.assert.equal(main_toggle.parentElement.getAttribute("id"), "ctrl_main"); + }); + it("has step button (#ctrl_main_step)", function() { + const step = document.getElementById('ctrl_main_step'); + chai.assert.equal(step.parentElement.getAttribute("id"), "ctrl_main"); + }); + it("has C button (#c_button)", function() { + const c_button = bg1.children[2]; + chai.assert.equal(c_button.getAttribute("id"), "c_button"); + }); + it("has auto/manual toggle", function() { + const mode_toggle = bg1.children[3]; + chai.assert.equal(mode_toggle.getAttribute("id"), "mode_toggle"); + }); + }); + }); + + describe("second button group (#real)", function() { + const bg2 = document.getElementById('real'); + it("should exist", function() { + chai.assert.isNotNull(bg2); + }); + it("should have control panel (#ctrl) as parent", function() { + chai.assert.equal(bg2.parentElement.getAttribute("id"), "ctrl"); + }); + it("should have 3 children (due to TakeChildren)", function() { + chai.assert.equal(bg2.childElementCount, 3); + }); + describe("group's children", function() { + it("has only buttons", function() { + const nodeNames = Array.from(bg2.children).map(child => child.nodeName); + chai.assert.deepEqual(nodeNames, ["BUTTON", "BUTTON","BUTTON"]); + }); + it("has first child D (#d_button)", function() { + const d_button = bg2.children[0]; + chai.assert.equal(d_button.getAttribute("id"), "d_button"); + }); + it("has second child A (#a_button)", function() { + const a_button = bg2.children[1]; + chai.assert.equal(a_button.getAttribute("id"), "a_button"); + }); + it("has third child B (#b_button)", function() { + const b_button = bg2.children[2]; + chai.assert.equal(b_button.getAttribute("id"), "b_button"); + }); + }); + }); + + describe("third button group (#husk)", function() { + const bg3 = document.getElementById('husk'); + it("should exist", function() { + chai.assert.isNotNull(bg3); + }); + it("should have control panel (#ctrl) as parent", function() { + chai.assert.equal(bg3.parentElement.getAttribute("id"), "ctrl"); + }); + it("should have no children (due to TakeChildren)", function() { + chai.assert.equal(bg3.childElementCount, 0); + }); + }); + }); + }); + } +}; + +emp::web::MochaTestRunner test_runner; +int main() { + test_runner.Initialize({"emp_test_container"}); + test_runner.AddTest("Test emp::prefab::ControlPanel HTML Layout"); + test_runner.Run(); +} diff --git a/tests/web/Makefile b/tests/web/Makefile index 9c8d4db1d7..c549170470 100644 --- a/tests/web/Makefile +++ b/tests/web/Makefile @@ -1,6 +1,6 @@ SHELL := /bin/bash -TEST_NAMES = ConfigPanel Collapse LoadingModal Card CommentBox Modal ToggleSwitch CodeBlock LoadingIcon FontAwesomeIcon ClickCounterDemo ClickCollapseDemo Element TextFeed js_utils JSWrap Widget visualizations ValueBox ReadoutPanel +TEST_NAMES = ConfigPanel Collapse LoadingModal Card CommentBox Modal ToggleSwitch CodeBlock LoadingIcon FontAwesomeIcon ClickCounterDemo ClickCollapseDemo Element TextFeed js_utils JSWrap Widget visualizations ValueBox ReadoutPanel ControlPanel ToggleButtonGroup # Currently a couple of the tests won't compile to native so this is a separate list for now. Eventually we should fix # that and just have one list diff --git a/tests/web/ToggleButtonGroup.cpp b/tests/web/ToggleButtonGroup.cpp new file mode 100644 index 0000000000..261b32af1c --- /dev/null +++ b/tests/web/ToggleButtonGroup.cpp @@ -0,0 +1,77 @@ +// This file is part of Empirical, https://github.com/devosoft/Empirical +// Copyright (C) Michigan State University, 2021. +// Released under the MIT Software license; see doc/LICENSE + +#include "emp/web/_MochaTestRunner.hpp" +#include "emp/web/Document.hpp" +#include "emp/prefab/ToggleButtonGroup.hpp" +#include "emp/prefab/FontAwesomeIcon.hpp" + +struct Test_Toggle_Button_Group : emp::web::BaseTest { + Test_Toggle_Button_Group() + : BaseTest({"emp_test_container"}) { + emp::prefab::ToggleButtonGroup icon_and_string( + emp::prefab::FontAwesomeIcon{"fa-play"}, "Pause", + "primary", "secondary", "icon_and_string" + ); + Doc("emp_test_container") << icon_and_string; + } + + void Describe() override { + EM_ASM({ + describe("ToggleButtonGroup HTML Layout", function() { + const toggle = document.getElementById("icon_and_string"); + it("should exists", function() { + chai.assert.isNotNull(toggle); + }); + it("should have parent #emp_test_container", function() { + chai.assert.equal(toggle.parentElement.getAttribute("id"), "emp_test_container"); + }); + it("should have 2 children", function() { + chai.assert.equal(toggle.childElementCount, 2); + }); + describe("first label (activate)", function() { + const label1 = toggle.children[0]; + it("should have ID #icon_and_string_activate", function() { + chai.assert.equal(label1.getAttribute("id"), "icon_and_string_activate"); + }); + it("should have two children", function() { + chai.assert.equal(label1.childElementCount, 2); + }); + const activate_radio = label1.children[0]; + it('should have first child be a radio input', function() { + chai.assert.equal(activate_radio.nodeName, "INPUT"); + }); + const activate_indicator = label1.children[1]; + it('should have second child be a span', function() { + chai.assert.equal(activate_indicator.nodeName, "SPAN"); + }); + }); + describe("second label (deactivate)", function() { + const label2 = toggle.children[1]; + it("should have ID #icon_and_string_deactivate", function() { + chai.assert.equal(label2.getAttribute("id"), "icon_and_string_deactivate"); + }); + it("should have two children", function() { + chai.assert.equal(label2.childElementCount, 2); + }); + const deactivate_radio = label2.children[0]; + it('should have first child be a radio input', function() { + chai.assert.equal(deactivate_radio.nodeName, "INPUT"); + }); + const deactivate_indicator = label2.children[1]; + it('should have second child be a span', function() { + chai.assert.equal(deactivate_indicator.nodeName, "SPAN"); + }); + }); + }); + }); + } +}; + +emp::web::MochaTestRunner test_runner; +int main() { + test_runner.Initialize({"emp_test_container"}); + test_runner.AddTest("Test emp::prefab::ToggleButtonGroup HTML Layout"); + test_runner.Run(); +}