From 3a6c85ef587e63537fa4a607b3fdd8af828dd406 Mon Sep 17 00:00:00 2001 From: Filip Sykala - NTB T15p Date: Fri, 30 Jun 2023 20:30:51 +0200 Subject: [PATCH] Load white/gray icons by icon manager --- src/slic3r/GUI/Gizmos/GLGizmoSVG.cpp | 227 ++++++++++++++++++--------- src/slic3r/GUI/Gizmos/GLGizmoSVG.hpp | 5 + src/slic3r/GUI/IconManager.cpp | 191 +++++++++++++++++++++- src/slic3r/GUI/IconManager.hpp | 9 +- 4 files changed, 349 insertions(+), 83 deletions(-) diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSVG.cpp b/src/slic3r/GUI/Gizmos/GLGizmoSVG.cpp index dc10d38e7c..f1a62a90cb 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSVG.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSVG.cpp @@ -112,6 +112,20 @@ std::string volume_name(const EmbossShape& shape); /// Params CreateVolumeParams create_input(GLCanvas3D &canvas, RaycastManager &raycaster, ModelVolumeType volume_type); +enum class IconType : unsigned { + reset_value, + reset_value_hover, + lock, + lock_hover, + unlock, + unlock_hover, + // automatic calc of icon's count + _count +}; + +const IconManager::Icon &get_icon(const IconManager::Icons &icons, IconType type) { + return *icons[static_cast(type)]; } + // This configs holds GUI layout size given by translated texts. // etc. When language changes, GUI is recreated and this class constructed again, // so the change takes effect. (info by GLGizmoFdmSupports.hpp) @@ -121,6 +135,9 @@ struct GuiCfg double screen_scale; float main_toolbar_height; + // Define bigger size(width or height) + unsigned texture_max_size_px = 64; + // Zero means it is calculated in init function ImVec2 minimal_window_size = ImVec2(0, 0); @@ -337,6 +354,55 @@ void GLGizmoSVG::on_unregister_raycasters_for_picking(){ m_rotate_gizmo.unregister_raycasters_for_picking(); } +namespace{ +IconManager::Icons init_icons(IconManager &mng, const GuiCfg &cfg) +{ + mng.release(); + + ImVec2 size(cfg.icon_width, cfg.icon_width); + // icon order has to match the enum IconType + IconManager::InitTypes init_types{ + {"undo.svg", size, IconManager::RasterType::white_only_data}, // undo + {"undo.svg", size, IconManager::RasterType::color}, // undo_hovered + {"lock_closed.svg", size, IconManager::RasterType::white_only_data}, // lock, + {"lock_closed_f.svg",size, IconManager::RasterType::white_only_data}, // lock_hovered, + {"lock_open.svg", size, IconManager::RasterType::white_only_data}, // unlock, + {"lock_open_f.svg", size, IconManager::RasterType::white_only_data} // unlock_hovered + }; + + assert(init_types.size() == static_cast(IconType::_count)); + std::string path = resources_dir() + "/icons/"; + for (IconManager::InitType &init_type : init_types) + init_type.filepath = path + init_type.filepath; + + return mng.init(init_types); + + //IconManager::VIcons vicons = mng.init(init_types); + // + //// flatten icons + //IconManager::Icons icons; + //icons.reserve(vicons.size()); + //for (IconManager::Icons &i : vicons) + // icons.push_back(i.front()); + //return icons; +} + +bool reset_button(const IconManager::Icons &icons) +{ + float reset_offset = ImGui::GetStyle().FramePadding.x; + ImGui::SameLine(reset_offset); + + // from GLGizmoCut + //std::string label_id = "neco"; + //std::string btn_label; + //btn_label += ImGui::RevertButton; + //return ImGui::Button((btn_label + "##" + label_id).c_str()); + + return clickable(get_icon(icons, IconType::reset_value), get_icon(icons, IconType::reset_value_hover)); +} + +} // namespace + void GLGizmoSVG::on_render_input_window(float x, float y, float bottom_limit) { set_volume_by_selection(); @@ -358,6 +424,8 @@ void GLGizmoSVG::on_render_input_window(float x, float y, float bottom_limit) // set position near toolbar m_set_window_offset = ImVec2(-1.f, -1.f); + + m_icons = init_icons(m_icon_manager, *m_gui_cfg); // need regeneration when change resolution(move between monitors) } const ImVec2 &min_window_size = m_gui_cfg->minimal_window_size; @@ -399,8 +467,8 @@ void GLGizmoSVG::on_render_input_window(float x, float y, float bottom_limit) } ImGui::End(); - if (!is_opened) - close(); + //if (!is_opened) + // close(); } void GLGizmoSVG::on_set_state() @@ -455,7 +523,8 @@ void GLGizmoSVG::on_dragging(const UpdateData &data) { m_rotate_gizmo.dragging(d #include "slic3r/GUI/BitmapCache.hpp" #include "nanosvg/nanosvgrast.h" namespace{ -bool init_texture(Texture &texture, const ModelVolume &mv) { +bool init_texture(Texture &texture, const ModelVolume &mv, unsigned max_size_px) +{ if (!mv.emboss_shape.has_value()) return false; @@ -464,7 +533,6 @@ bool init_texture(Texture &texture, const ModelVolume &mv) { if (filepath.empty()) return false; - unsigned max_size_px = 256; // inspired by: // GLTexture::load_from_svg_file(filepath, false, false, false, max_size_px); NSVGimage *image = BitmapCache::nsvgParseFromFileWithReplace(filepath.c_str(), "px", 96.0f, {}); @@ -563,7 +631,7 @@ void GLGizmoSVG::set_volume_by_selection() // Calculate current angle of up vector m_angle = calc_up(gl_volume->world_matrix(), Slic3r::GUI::up_limit); m_distance = calc_distance(*gl_volume, m_raycast_manager, m_parent); - init_texture(m_texture, *m_volume); + // calculate scale for height and depth inside of scaled object instance calculate_scale(); } @@ -644,15 +712,9 @@ void GLGizmoSVG::draw_window() ImGui::Text("Not valid state please report reproduction steps on github"); return; } + draw_preview(); - if (m_volume->emboss_shape.has_value()) - ImGui::Text("SVG file path is %s", m_volume->emboss_shape->svg_file_path.c_str()); - - if (m_texture.id != 0) { - ImTextureID id = (void *) static_cast(m_texture.id); - ImVec2 s(m_texture.width, m_texture.height); - ImGui::Image(id, s); - } + ImGui::Separator(); ImGui::Indent(m_gui_cfg->icon_width); draw_depth(); @@ -661,14 +723,7 @@ void GLGizmoSVG::draw_window() draw_distance(); draw_rotation(); - ImGui::Unindent(m_gui_cfg->icon_width); - - if (ImGui::Button("change file")) { - m_volume_shape.shapes_with_ids = select_shape().shapes_with_ids; - init_texture(m_texture, *m_volume); - // TODO: use setted scale - process(); - } + ImGui::Unindent(m_gui_cfg->icon_width); if (!m_volume->is_the_only_one_part()) { ImGui::Separator(); @@ -676,6 +731,40 @@ void GLGizmoSVG::draw_window() } } +void GLGizmoSVG::draw_preview(){ + + if (m_volume->emboss_shape.has_value()) + ImGui::Text("SVG file path is %s", m_volume->emboss_shape->svg_file_path.c_str()); + + if (m_texture.id == 0) + init_texture(m_texture, *m_volume, m_gui_cfg->texture_max_size_px); + + if (m_texture.id != 0) { + ImTextureID id = (void *) static_cast(m_texture.id); + ImVec2 s(m_texture.width, m_texture.height); + ImGui::Image(id, s); + } + + ImGui::SameLine(); + if (ImGui::Button("change file")) { + m_volume_shape.shapes_with_ids = select_shape().shapes_with_ids; + init_texture(m_texture, *m_volume, m_gui_cfg->texture_max_size_px); + process(); + } + + // Re-Load button + bool can_reload = !m_volume_shape.svg_file_path.empty(); + if (can_reload) { + ImGui::SameLine(); + if (clickable(get_icon(m_icons, IconType::reset_value), get_icon(m_icons, IconType::reset_value_hover))) { + m_volume_shape.shapes_with_ids = select_shape(m_volume_shape.svg_file_path).shapes_with_ids; + init_texture(m_texture, *m_volume, m_gui_cfg->texture_max_size_px); + process(); + } else if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", _u8L("Re-load SVG file from disk.").c_str()); + } +} + void GLGizmoSVG::draw_depth() { ImGuiWrapper::text(m_gui_cfg->translations.depth); @@ -746,16 +835,13 @@ void GLGizmoSVG::draw_size() ImGui::SetTooltip("%s", _u8L("Height of SVG.").c_str()); bool can_reset = !is_approx(m_volume_shape.scale, DEFAULT_SCALE); - m_imgui->disabled_begin(!can_reset); - ScopeGuard sc([imgui = m_imgui]() { imgui->disabled_end(); }); - - float reset_offset = ImGui::GetStyle().FramePadding.x; - ImGui::SameLine(reset_offset); - if (ImGui::Button("R##size_reset")) { - m_volume_shape.scale = DEFAULT_SCALE; - process(); - } else if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", _u8L("Reset scale to loaded one from the SVG").c_str()); + if (can_reset) { + if (reset_button(m_icons)) { + m_volume_shape.scale = DEFAULT_SCALE; + process(); + } else if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", _u8L("Reset scale to loaded one from the SVG").c_str()); + } } void GLGizmoSVG::draw_use_surface() @@ -810,16 +896,14 @@ void GLGizmoSVG::draw_distance() is_moved = true; } - m_imgui->disabled_begin(!m_distance.has_value() && allowe_surface_distance); - ScopeGuard sg2([imgui = m_imgui]() { imgui->disabled_end(); }); - - float reset_offset = ImGui::GetStyle().FramePadding.x; - ImGui::SameLine(reset_offset); - if (ImGui::Button("R##distance_reset")){ - m_distance.reset(); - is_moved = true; - } else if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", _u8L("Reset distance to zero value").c_str()); + bool can_reset = m_distance.has_value(); + if (can_reset) { + if (reset_button(m_icons)) { + m_distance.reset(); + is_moved = true; + } else if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", _u8L("Reset distance to zero value").c_str()); + } if (is_moved) do_local_z_move(m_parent, m_distance.value_or(.0f) - prev_distance); @@ -837,7 +921,7 @@ void GLGizmoSVG::draw_rotation() // minus create clock-wise roation from CCW float angle = m_angle.value_or(0.f); float angle_deg = static_cast(-angle * 180 / M_PI); - if (m_imgui->slider_float("##angle", &angle_deg, limits.angle.min, limits.angle.max, u8"%.2f DEG", 1.f, false, _L("Rotate text Clock-wise."))){ + if (m_imgui->slider_float("##angle", &angle_deg, limits.angle.min, limits.angle.max, u8"%.2f °", 1.f, false, _L("Rotate text Clock-wise."))){ // convert back to radians and CCW double angle_rad = -angle_deg * M_PI / 180.0; Geometry::to_range_pi_pi(angle_rad); @@ -854,45 +938,32 @@ void GLGizmoSVG::draw_rotation() process(); } - if (!m_volume->is_the_only_one_part()) { - // Keep up - lock button icon - ImGui::SameLine(m_gui_cfg->lock_offset); - ImGui::Checkbox("##Lock_up_vector", &m_keep_up); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", (m_keep_up ? - _u8L("Unlock the rotation when moving volume along the object's surface.") : - _u8L("Lock the rotation when moving volume along the object's surface.")) - .c_str()); + // Reset button + if (m_angle.has_value()) { + if (reset_button(m_icons)) { + do_local_z_rotate(m_parent, -(*m_angle)); + m_angle.reset(); + + // recalculate for surface cut + if (m_volume->emboss_shape->projection.use_surface) + process(); + } else if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", _u8L("Reset rotation to zero value").c_str()); } - // Reset button - m_imgui->disabled_begin(!m_angle.has_value()); - ScopeGuard sg([imgui = m_imgui]() { imgui->disabled_end(); }); - - float reset_offset = ImGui::GetStyle().FramePadding.x; - ImGui::SameLine(reset_offset); - if (ImGui::Button("R##angle_reset")) { - do_local_z_rotate(m_parent, -(*m_angle)); - m_angle.reset(); - - // recalculate for surface cut - if (m_volume->emboss_shape->projection.use_surface) - process(); - } else if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", _u8L("Reset rotation to zero value").c_str()); - // Keep up - lock button icon - //ImGui::SameLine(m_gui_cfg->lock_offset); - //const IconManager::Icon &icon = get_icon(m_icons, m_keep_up ? IconType::lock : IconType::unlock, IconState::activable); - //const IconManager::Icon &icon_hover = get_icon(m_icons, m_keep_up ? IconType::lock_bold : IconType::unlock_bold, IconState::activable); - //const IconManager::Icon &icon_disable = get_icon(m_icons, m_keep_up ? IconType::lock : IconType::unlock, IconState::disabled); - //if (button(icon, icon_hover, icon_disable)) - // m_keep_up = !m_keep_up; - //if (ImGui::IsItemHovered()) - // ImGui::SetTooltip("%s", (m_keep_up? - // _u8L("Unlock the text's rotation when moving text along the object's surface."): - // _u8L("Lock the text's rotation when moving text along the object's surface.") - // ).c_str()); + if (!m_volume->is_the_only_one_part()) { + ImGui::SameLine(m_gui_cfg->lock_offset); + const IconManager::Icon &icon = get_icon(m_icons,m_keep_up ? IconType::lock : IconType::unlock); + const IconManager::Icon &icon_hover = get_icon(m_icons, m_keep_up ? IconType::lock_hover : IconType::unlock_hover); + if (button(icon, icon_hover, icon)) + m_keep_up = !m_keep_up; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", (m_keep_up? + _u8L("Free angle when dragging above the object's surface."): + _u8L("Keep same rotation angle when dragging above the object's surface.") + ).c_str()); + } } void GLGizmoSVG::draw_model_type() diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSVG.hpp b/src/slic3r/GUI/Gizmos/GLGizmoSVG.hpp index 4f50934cb3..f59dcab292 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSVG.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSVG.hpp @@ -8,6 +8,7 @@ #include "slic3r/GUI/SurfaceDrag.hpp" #include "slic3r/GUI/GLTexture.hpp" #include "slic3r/Utils/RaycastManager.hpp" +#include "slic3r/GUI/IconManager.hpp" #include #include @@ -115,6 +116,7 @@ private: bool process(); void close(); void draw_window(); + void draw_preview(); void draw_depth(); void draw_size(); void draw_use_surface(); @@ -172,6 +174,9 @@ private: Texture m_texture; + IconManager m_icon_manager; + IconManager::Icons m_icons; + // only temporary solution static const std::string M_ICON_FILENAME; }; diff --git a/src/slic3r/GUI/IconManager.cpp b/src/slic3r/GUI/IconManager.cpp index 45c76887c3..e4937111ee 100644 --- a/src/slic3r/GUI/IconManager.cpp +++ b/src/slic3r/GUI/IconManager.cpp @@ -1,6 +1,15 @@ #include "IconManager.hpp" #include #include +#include "nanosvg/nanosvg.h" +#include "nanosvg/nanosvgrast.h" +#include "libslic3r/Utils.hpp" // ScopeGuard + +#include "3DScene.hpp" // glsafe +#include "GL/glew.h" + +#define STB_RECT_PACK_IMPLEMENTATION +#include "imgui/imstb_rectpack.h" // distribute rectangles using namespace Slic3r::GUI; @@ -14,12 +23,188 @@ static void draw_transparent_icon(const IconManager::Icon &icon); // only help f IconManager::~IconManager() { priv::clear(m_icons); // release opengl texture is made in ~GLTexture() + + if (m_id != 0) + glsafe(::glDeleteTextures(1, &m_id)); } -std::vector IconManager::init(const InitTypes &input) +namespace { +NSVGimage *parse_file(const char * filepath) { + FILE *fp = boost::nowide::fopen(filepath, "rb"); + assert(fp != nullptr); + if (fp == nullptr) + return nullptr; + + Slic3r::ScopeGuard sg([fp]() { fclose(fp); }); + + fseek(fp, 0, SEEK_END); + size_t size = ftell(fp); + fseek(fp, 0, SEEK_SET); + + // Note: +1 is for null termination + auto data_ptr = std::make_unique(size+1); + data_ptr[size] = '\0'; // Must be null terminated. + + size_t readed_size = fread(data_ptr.get(), 1, size, fp); + assert(readed_size == size); + if (readed_size != size) + return nullptr; + + return nsvgParse(data_ptr.get(), "px", 96.0f); +} + +void subdata(unsigned char *data, size_t data_stride, const std::vector &data2, size_t data2_row) { + assert(data_stride >= data2_row); + for (size_t data2_offset = 0, data_offset = 0; + data2_offset < data2.size(); + data2_offset += data2_row, data_offset += data_stride) + ::memcpy((void *)(data + data_offset), (const void *)(data2.data() + data2_offset), data2_row); +} +} + +IconManager::Icons IconManager::init(const InitTypes &input) { - BOOST_LOG_TRIVIAL(error) << "Not implemented yet"; - return {}; + if (input.empty()) + return {}; + + // TODO: remove in future + if (m_id != 0) { + glsafe(::glDeleteTextures(1, &m_id)); + m_id = 0; + } + + int total_surface = 0; + for (const InitType &i : input) + total_surface += i.size.x * i.size.y; + const int surface_sqrt = (int)sqrt((float)total_surface) + 1; + + // Start packing + // Pack our extra data rectangles first, so it will be on the upper-left corner of our texture (UV will have small values). + const int TEX_HEIGHT_MAX = 1024 * 32; + int width = (surface_sqrt >= 4096 * 0.7f) ? 4096 : (surface_sqrt >= 2048 * 0.7f) ? 2048 : (surface_sqrt >= 1024 * 0.7f) ? 1024 : 512; + + int num_nodes = width; + std::vector nodes(num_nodes); + stbrp_context context; + stbrp_init_target(&context, width, TEX_HEIGHT_MAX, nodes.data(), num_nodes); + + ImVector pack_rects; + pack_rects.resize(input.size()); + memset(pack_rects.Data, 0, (size_t) pack_rects.size_in_bytes()); + for (int i = 0; i < input.size(); i++) { + const ImVec2 &size = input[i].size; + assert(size.x > 1); + assert(size.y > 1); + pack_rects[i].w = size.x; + pack_rects[i].h = size.y; + } + int pack_rects_res = stbrp_pack_rects(&context, &pack_rects[0], pack_rects.Size); + assert(pack_rects_res == 1); + if (pack_rects_res != 1) + return {}; + + ImVec2 tex_size(width, width); + for (const stbrp_rect &rect : pack_rects) { + float x = rect.x + rect.w; + float y = rect.y + rect.h; + if(x > tex_size.x) tex_size.x = x; + if(y > tex_size.y) tex_size.y = y; + } + + Icons result(input.size()); + for (int i = 0; i < pack_rects.Size; i++) { + const stbrp_rect &rect = pack_rects[i]; + assert(rect.was_packed); + if (!rect.was_packed) + return {}; + + ImVec2 tl(rect.x / tex_size.x, rect.y / tex_size.y); + ImVec2 br((rect.x + rect.w) / tex_size.x, (rect.y + rect.h) / tex_size.y); + + assert(input[i].size.x == rect.w); + assert(input[i].size.y == rect.h); + Icon icon = {input[i].size, tl, br}; + result[i] = std::make_shared(std::move(icon)); + } + + NSVGrasterizer *rast = nsvgCreateRasterizer(); + assert(rast != nullptr); + if (rast == nullptr) + return {}; + ScopeGuard sg_rast([rast]() { ::nsvgDeleteRasterizer(rast); }); + + int channels = 4; + int n_pixels = tex_size.x * tex_size.y; + // store data for whole texture + std::vector data(n_pixels * channels, {0}); + + // initialize original index locations + std::vector idx(input.size()); + std::iota(idx.begin(), idx.end(), 0); + + // Group same filename by sort inputs + // sort indexes based on comparing values in input + std::sort(idx.begin(), idx.end(), [&input](size_t i1, size_t i2) { return input[i1].filepath < input[i2].filepath; }); + for (size_t j: idx) { + const InitType &i = input[j]; + if (i.filepath.empty()) + continue; // no file path only reservation of space for texture + + if (!boost::filesystem::exists(i.filepath)) + continue; + if (!boost::algorithm::iends_with(i.filepath, ".svg")) + continue; + + NSVGimage *image = parse_file(i.filepath.c_str()); + assert(image != nullptr); + if (image == nullptr) + return {}; + + ScopeGuard sg_image([image]() { ::nsvgDelete(image); }); + + float svg_scale = i.size.y / image->height; + // scale should be same in both directions + assert(is_approx(svg_scale, i.size.y / image->width)); + + const stbrp_rect &rect = pack_rects[j]; + int n_pixels = rect.w * rect.h; + std::vector icon_data(n_pixels * channels, {0}); + ::nsvgRasterize(rast, image, 0, 0, svg_scale, icon_data.data(), i.size.x, i.size.y, i.size.x * channels); + + // makes white or gray only data in icon + if (i.type == RasterType::white_only_data || + i.type == RasterType::gray_only_data) { + unsigned char value = (i.type == RasterType::white_only_data) ? 255 : 127; + for (size_t k = 0; k < icon_data.size(); k += channels) + if (icon_data[k] != 0 || icon_data[k + 1] != 0 || icon_data[k + 2] != 0) { + icon_data[k] = value; + icon_data[k + 1] = value; + icon_data[k + 2] = value; + } + } + + int start_offset = (rect.y*tex_size.x + rect.x) * channels; + int data_stride = tex_size.x * channels; + subdata(data.data() + start_offset, data_stride, icon_data, rect.w * channels); + } + + if (m_id != 0) + glsafe(::glDeleteTextures(1, &m_id)); + + glsafe(::glPixelStorei(GL_UNPACK_ALIGNMENT, 1)); + glsafe(::glGenTextures(1, &m_id)); + glsafe(::glBindTexture(GL_TEXTURE_2D, (GLuint) m_id)); + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)); + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0)); + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)); + glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, (GLsizei) tex_size.x, (GLsizei) tex_size.y, 0, GL_RGBA, GL_UNSIGNED_BYTE, (const void*) data.data())); + + // bind no texture + glsafe(::glBindTexture(GL_TEXTURE_2D, 0)); + + for (const auto &i : result) + i->tex_id = m_id; + return result; } std::vector IconManager::init(const std::vector &file_paths, const ImVec2 &size, RasterType type) diff --git a/src/slic3r/GUI/IconManager.hpp b/src/slic3r/GUI/IconManager.hpp index aa7afda800..7814b2280f 100644 --- a/src/slic3r/GUI/IconManager.hpp +++ b/src/slic3r/GUI/IconManager.hpp @@ -70,10 +70,10 @@ public: /// Initialize raster texture on GPU with given images /// NOTE: Have to be called after OpenGL initialization /// - /// Define files and its + /// Define files and its size with rasterization /// Rasterized icons stored on GPU, /// Same size and order as input, each item of vector is set of texture in order by RasterType - VIcons init(const InitTypes &input); + Icons init(const InitTypes &input); /// /// Initialize multiple icons with same settings for size and type @@ -96,6 +96,11 @@ public: private: // keep data stored on GPU GLTexture m_icons_texture; + + unsigned int m_id{ 0 }; + int m_width{ 0 }; + int m_height{ 0 }; + Icons m_icons; };