/* * InfiniPaint * Copyright (C) 2025-2026 Yousef Khadadeh * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "Toolbar.hpp" #include "CustomEvents.hpp" #include "DrawingProgram/Tools/DrawingProgramToolBase.hpp" #include "FileHelpers.hpp" #include "GUIStuff/ElementHelpers/CheckBoxHelpers.hpp" #include "GUIStuff/ElementHelpers/LayoutHelpers.hpp" #include "GUIStuff/ElementHelpers/PopupHelpers.hpp" #include "GUIStuff/ElementHelpers/RadioButtonHelpers.hpp" #include "GUIStuff/Elements/ManyElementScrollArea.hpp" #include "Helpers/ConvertVec.hpp" #include "Helpers/FileDownloader.hpp" #include "Helpers/MathExtras.hpp" #include "Helpers/Networking/NetLibrary.hpp" #include "Helpers/NetworkingObjects/NetObjGenericSerializedClass.hpp" #include "MainProgram.hpp" #include "InputManager.hpp" #include "ResourceDisplay/ImageResourceDisplay.hpp" #include "RichText/TextStyleModifier.hpp" #include "VersionConstants.hpp" #include "World.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "GUIStuff/GUIManager.hpp" #include "GUIStuff/ElementHelpers/TextLabelHelpers.hpp" #include "GUIStuff/ElementHelpers/ButtonHelpers.hpp" #include "GUIStuff/ElementHelpers/PopupHelpers.hpp" #include "GUIStuff/ElementHelpers/TextBoxHelpers.hpp" #include "GUIStuff/ElementHelpers/ColorPickerHelpers.hpp" #include "GUIStuff/ElementHelpers/RadioButtonHelpers.hpp" #include "GUIStuff/ElementHelpers/NumberSliderHelpers.hpp" #include "GUIStuff/Elements/ScrollArea.hpp" #include "GUIStuff/Elements/LayoutElement.hpp" #include "GUIStuff/Elements/DropDown.hpp" #include "GUIStuff/Elements/SVGIcon.hpp" #include "GUIStuff/Elements/MovableTabList.hpp" #include "GUIStuff/Elements/TextParagraph.hpp" #include "Screens/DesktopDrawingProgramScreen.hpp" #define UPDATE_DOWNLOAD_URL "https://infinipaint.com/download.html" #define UPDATE_NOTIFICATION_URL "https://infinipaint.com/updateNotificationVersion.txt" #ifdef __EMSCRIPTEN__ #include #endif using namespace GUIStuff; using namespace ElementHelpers; Toolbar::Toolbar(MainProgram& initMain, DesktopDrawingProgramScreen& initDrawScreen): main(initMain), drawScreen(initDrawScreen) {} void Toolbar::open_file_selector(const std::string& filePickerName, const std::vector& extensionFilters, Screen::OpenFileSelectorCallback postSelectionFunc, const std::string& fileName, bool isSaving) { drawScreen.open_file_selector(filePickerName, extensionFilters, postSelectionFunc, fileName, isSaving); } void Toolbar::open_file_selector_non_native(const std::string& filePickerName, const std::vector& extensionFilters, Screen::OpenFileSelectorCallback postSelectionFunc, const std::string& fileName, bool isSaving) { filePicker.isOpen = true; filePicker.extensionFiltersComplete = extensionFilters; filePicker.extensionFilters.clear(); for(auto& [name, exList] : extensionFilters) filePicker.extensionFilters.emplace_back(exList); filePicker.extensionSelected = extensionFilters.size() - 1; filePicker.filePickerWindowName = filePickerName; filePicker.postSelectionFunc = postSelectionFunc; filePicker.fileName = ""; filePicker.isSaving = isSaving; filePicker.entriesScrollArea = nullptr; file_picker_gui_refresh_entries(); } void Toolbar::color_button_left(const char* id, Vector4f* color, const ColorSelectorButtonData& colorSelectorData) { auto& gui = main.g.gui; color_button(gui, id, color, { .isSelected = colorLeft == color, .onClickButton = [&, colorSelectorData, color] (SelectableButton* b) { if(colorSelectorData.onSelectorButtonClick) colorSelectorData.onSelectorButtonClick(); color_selector_left(b, color, { .onChange = colorSelectorData.onChange, .onSelect = colorSelectorData.onSelect, .onDeselect = colorSelectorData.onDeselect, }); } }); } void Toolbar::color_button_right(const char* id, Vector4f* color, const ColorSelectorButtonData& colorSelectorData) { auto& gui = main.g.gui; color_button(gui, id, color, { .isSelected = colorRight == color, .onClickButton = [&, colorSelectorData, color] (SelectableButton* b) { if(colorSelectorData.onSelectorButtonClick) colorSelectorData.onSelectorButtonClick(); color_selector_right(b, color, { .onChange = colorSelectorData.onChange, .onSelect = colorSelectorData.onSelect, .onDeselect = colorSelectorData.onDeselect, }); } }); } void Toolbar::color_selector_left(Element* button, Vector4f* color, const ColorSelectorData& colorSelectorData) { if(colorLeft != color) { colorLeft = color; colorLeftData = colorSelectorData; colorLeftButton = button; } else colorLeft = nullptr; main.g.gui.set_to_layout(); } void Toolbar::color_selector_right(Element* button, Vector4f* color, const ColorSelectorData& colorSelectorData) { if(colorRight != color) { colorRight = color; colorRightData = colorSelectorData; colorRightButton = button; } else colorRight = nullptr; main.g.gui.set_to_layout(); } void Toolbar::update() { std::erase_if(main.logMessages, [&](auto& logM) { logM.time.update_time_since(); if(logM.time > UserLogMessage::FADE_START_TIME) { main.g.gui.set_to_layout(); return logM.time >= UserLogMessage::DISPLAY_TIME; } return false; }); if(!chatboxOpen) { for(auto& chatMessage : main.world->chatMessages) { bool wasShown = chatMessage.time < ChatMessage::DISPLAY_TIME; chatMessage.time.update_time_since(); bool isShown = chatMessage.time < ChatMessage::DISPLAY_TIME; bool isFading = chatMessage.time >= ChatMessage::FADE_START_TIME; if((isFading && isShown) || (wasShown && !isShown)) main.g.gui.set_to_layout(); } } } void Toolbar::open_chatbox() { if(!chatboxOpen) { chatMessageInput.clear(); chatboxOpen = true; main.g.gui.set_to_layout(); } } void Toolbar::close_chatbox() { if(chatboxOpen) { chatboxOpen = false; main.g.gui.set_to_layout(); } } void Toolbar::layout_run() { auto& gui = main.g.gui; auto& io = gui.io; #ifndef __EMSCRIPTEN__ update_notification_check(); #endif if(drawGui) { CLAY_AUTO_ID({ .layout = { .sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_GROW(0)}, .padding = CLAY_PADDING_ALL(io.theme->padding1), .childGap = io.theme->childGap1, .childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_TOP}, .layoutDirection = CLAY_TOP_TO_BOTTOM } }) { top_toolbar(); if(!main.world->clientStillConnecting) drawing_program_gui(); if(!closePopupData.worldsToClose.empty()) close_popup_gui(); if(main.conf.viewWebVersionWelcome) web_version_welcome(); #ifndef __EMSCRIPTEN__ else if(updateCheckerData.showGui) update_notification_gui(); #endif else if(filePicker.isOpen) file_picker_gui(); else if(optionsMenuOpen) options_menu(); else if(main.world->clientStillConnecting) center_message("Connecting to server message", "Connecting to server..."); else if(playerMenuOpen) player_list(); else if(!main.world->drawProg.layerMan.is_a_layer_being_edited()) center_message("Select layer to edit message", "Select a layer to edit"); } if(!main.world->clientStillConnecting) chat_box(); } else { if(!closePopupData.worldsToClose.empty()) // Should still show close popup if gui is disabled close_popup_gui(); } if(!main.world->clientStillConnecting) main.world->drawProg.right_click_popup_gui(*this); } bool Toolbar::app_close_requested() { for(auto& w : main.worlds) { if(w->should_ask_before_closing()) add_world_to_close_popup_data(w); } closePopupData.closeAppWhenDone = true; main.g.gui.set_to_layout(); return closePopupData.worldsToClose.empty(); } void Toolbar::add_world_to_close_popup_data(const std::shared_ptr& w) { auto it = std::find_if(closePopupData.worldsToClose.begin(), closePopupData.worldsToClose.end(), [&](auto& wStruct) { return wStruct.w.lock() == w; }); if(it == closePopupData.worldsToClose.end()) closePopupData.worldsToClose.emplace_back(w, true); main.g.gui.set_to_layout(); } void Toolbar::close_popup_gui() { auto& gui = main.g.gui; auto& io = gui.io; std::erase_if(closePopupData.worldsToClose, [](auto& wPair) { return wPair.w.expired(); }); center_obstructing_window_gui("Close program popup GUI", CLAY_SIZING_FIT(0), CLAY_SIZING_FIT(0, 600), [&] { text_label(gui, "Files may contain unsaved changes"); gui.clipping_element("close file popup gui scroll area", ScrollArea::Options{ .scrollVertical = true, .clipVertical = true, .scrollbarY = ScrollArea::ScrollbarType::NORMAL, .innerContent = [&](const ScrollArea::InnerContentParameters&) { CLAY_AUTO_ID({ .layout = { .childGap = io.theme->childGap1, .childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_TOP}, .layoutDirection = CLAY_TOP_TO_BOTTOM } }) { size_t i = 0; for(auto& [w, setToSave] : closePopupData.worldsToClose) { auto wLock = w.lock(); CLAY_AUTO_ID({ .layout = { .sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIT(0) }, .padding = CLAY_PADDING_ALL(io.theme->padding1), .childGap = io.theme->childGap1, .childAlignment = { .x = CLAY_ALIGN_X_LEFT, .y = CLAY_ALIGN_Y_CENTER}, .layoutDirection = CLAY_LEFT_TO_RIGHT }, .backgroundColor = convert_vec4(io.theme->backColor2), .cornerRadius = CLAY_CORNER_RADIUS(io.theme->windowCorners1), }) { gui.new_id(i, [&] { checkbox_boolean(gui, "set to save checkbox", &setToSave); CLAY_AUTO_ID({ .layout = { .sizing = {.width = CLAY_SIZING_FIT(0), .height = CLAY_SIZING_FIT(0) }, .childGap = 0, .childAlignment = { .x = CLAY_ALIGN_X_LEFT, .y = CLAY_ALIGN_Y_CENTER}, .layoutDirection = CLAY_TOP_TO_BOTTOM } }) { text_label(gui, wLock->name); text_label_light(gui, wLock->filePath.empty() ? "Autosave in " + main.documentsPath.string() : wLock->filePath.string()); } }); } ++i; } } } }); text_button(gui, "Save", "Save", { .wide = true, .onClick = [&] { for(auto& [w, setToSave] : closePopupData.worldsToClose) { auto wLock = w.lock(); if(setToSave) { if(wLock->filePath.empty()) wLock->autosave_to_directory(main.documentsPath); else wLock->save_to_file(wLock->filePath); } main.set_tab_to_close(wLock.get()); } closePopupData.worldsToClose.clear(); if(closePopupData.closeAppWhenDone) main.setToQuit = true; } }); text_button(gui, "Discard All", "Discard All", { .wide = true, .onClick = [&] { for(auto& [w, setToSave] : closePopupData.worldsToClose) { auto wLock = w.lock(); if(wLock) main.set_tab_to_close(wLock.get()); } closePopupData.worldsToClose.clear(); if(closePopupData.closeAppWhenDone) main.setToQuit = true; } }); text_button(gui, "Cancel", "Cancel", { .wide = true, .onClick = [&] { closePopupData.worldsToClose.clear(); closePopupData.closeAppWhenDone = false; } }); }); } void Toolbar::save_func() { if(main.world->filePath == std::filesystem::path()) save_as_func(); else main.world->save_to_file(main.world->filePath); } void Toolbar::save_as_func() { #ifdef __EMSCRIPTEN__ optionsMenuOpen = true; optionsMenuType = SET_DOWNLOAD_NAME; #else open_file_selector("Save", {{"InfiniPaint Canvas", World::FILE_EXTENSION}}, [w = make_weak_ptr(main.world)](const std::filesystem::path& p, const auto& e) { auto world = w.lock(); if(world) world->save_to_file(p); }, "", true); #endif } void Toolbar::paint_popup(Vector2f popupPos) { using namespace GUIStuff; auto& gui = main.g.gui; std::shared_ptr newRotationAngle = std::make_shared(main.world->drawData.cam.c.rotation); gui.set_z_index(-1, [&] { paint_circle_popup_menu(gui, "paint circle popup", popupPos, { .rotationAngle = newRotationAngle.get(), .selectedColor = main.world->drawProg.get_foreground_color_ptr(), .palette = main.conf.palettes[paletteData.selectedPalette].colors, .onRotate = [&, newRotationAngle] { main.world->drawData.cam.c.rotate_about(main.world->drawData.cam.c.from_space(main.window.size.cast() * 0.5f), *newRotationAngle - main.world->drawData.cam.c.rotation); *newRotationAngle = main.world->drawData.cam.c.rotation; gui.set_to_layout(); }, .onPaletteClick = [&] { gui.set_to_layout(); }, .mouseButton = [&](const InputManager::MouseButtonCallbackArgs& button, bool mouseHovering) { if(!mouseHovering && button.down && button.button != InputManager::MouseButton::RIGHT) main.world->drawProg.clear_right_click_popup(); } }); }); } void Toolbar::top_toolbar() { auto& gui = main.g.gui; auto& io = gui.io; gui.element("top menu bar", [&] (LayoutElement*, const Clay_ElementId& lId) { CLAY(lId, { .layout = { .sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIT(0) }, .padding = CLAY_PADDING_ALL(static_cast(io.theme->padding1 / 2)), .childGap = static_cast(io.theme->childGap1 / 2), .childAlignment = { .x = CLAY_ALIGN_X_LEFT, .y = CLAY_ALIGN_Y_CENTER}, .layoutDirection = CLAY_LEFT_TO_RIGHT }, .backgroundColor = convert_vec4(io.theme->backColor1), .cornerRadius = CLAY_CORNER_RADIUS(io.theme->windowCorners1) }) { global_log(); auto icon_button_top_toolbar = [&](const char* id, const std::string& svgPath, bool isSelected, const std::function& onClick) { return svg_icon_button(gui, id, svgPath, { .drawType = SelectableButton::DrawType::TRANSPARENT_ALL, .isSelected = isSelected, .onClick = onClick }); }; Element* mainMenuButton = icon_button_top_toolbar("Main Menu Button", "data/icons/menu.svg", menuPopUpOpen, [&] { menuPopUpOpen = !menuPopUpOpen; }); std::vector tabNames; for(size_t i = 0; i < main.worlds.size(); i++) { auto& w = main.worlds[i]; bool shouldAddStarNextToName = w->should_ask_before_closing() && !w->netServer && !w->netClient; tabNames.emplace_back(w->netObjMan.is_connected() ? "data/icons/network.svg" : "", w->name + (shouldAddStarNextToName ? "*" : "")); } gui.element("file tab list", MovableTabListData{ .tabNames = tabNames, .selectedTab = main.worldIndex, .changeSelectedTab = [&] (size_t i) { main.switch_to_tab(i); }, .closeTab = [&] (size_t i) { if(main.worlds[i]->should_ask_before_closing()) add_world_to_close_popup_data(main.worlds[i]); else main.set_tab_to_close(main.worlds[i].get()); } }); if(!main.world->clientStillConnecting) { if(main.world->netObjMan.is_connected()) { icon_button_top_toolbar("Player List Toggle Button", "data/icons/list.svg", playerMenuOpen, [&] { playerMenuOpen = !playerMenuOpen; }); } icon_button_top_toolbar("Menu Undo Button", "data/icons/undo.svg", false, [&] { main.world->undo_with_checks(); }); icon_button_top_toolbar("Menu Redo Button", "data/icons/redo.svg", false, [&] { main.world->redo_with_checks(); }); Element* gridMenuButton = icon_button_top_toolbar("Grids Button", "data/icons/grid.svg", gridMenuPopupOpen, [&] { if(gridMenuPopupOpen) stop_displaying_grid_menu(); else gridMenuPopupOpen = true; }); Element* layerMenuButton = icon_button_top_toolbar("Layer Menu Button", "data/icons/layer.svg", layerMenuPopupOpen, [&] { if(layerMenuPopupOpen) stop_displaying_layer_menu(); else layerMenuPopupOpen = true; }); Element* bookmarkMenuButton = icon_button_top_toolbar("Bookmark Menu Button", "data/icons/bookmark.svg", bookmarkMenuPopupOpen, [&] { if(bookmarkMenuPopupOpen) stop_displaying_bookmark_menu(); else bookmarkMenuPopupOpen = true; }); if(gridMenuPopupOpen) grid_menu(gridMenuButton); if(bookmarkMenuPopupOpen) bookmark_menu(bookmarkMenuButton); if(layerMenuPopupOpen) layer_menu(layerMenuButton); } if(menuPopUpOpen) { gui.set_z_index(5, [&] { gui.element("main menu popup", [&] (LayoutElement*, const Clay_ElementId& id) { CLAY(id, { .layout = { .sizing = {.width = CLAY_SIZING_FIT(100), .height = CLAY_SIZING_FIT(0) }, .padding = CLAY_PADDING_ALL(io.theme->padding1), .childGap = 1, .childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_TOP}, .layoutDirection = CLAY_TOP_TO_BOTTOM }, .backgroundColor = convert_vec4(io.theme->backColor1), .cornerRadius = CLAY_CORNER_RADIUS(io.theme->windowCorners1), .floating = {.offset = {.x = 0, .y = static_cast(io.theme->padding1)}, .zIndex = gui.get_z_index(), .attachPoints = {.element = CLAY_ATTACH_POINT_LEFT_TOP, .parent = CLAY_ATTACH_POINT_LEFT_BOTTOM}, .attachTo = CLAY_ATTACH_TO_PARENT} }) { auto menu_popup_text_button = [&](const char* id, const char* str, const std::function& onClick) { text_button(gui, id, str, { .drawType = SelectableButton::DrawType::TRANSPARENT_ALL, .wide = true, .centered = false, .onClick = [&, onClick]{ onClick(); menuPopUpOpen = false; } }); }; menu_popup_text_button("new file local", "New File", [&] { CustomEvents::emit_event({ .isClient = false }); }); menu_popup_text_button("open file", "Open", [&] { open_world_file(false, "", ""); }); if(!main.world->clientStillConnecting) { menu_popup_text_button("save file", "Save", [&] { save_func(); }); menu_popup_text_button("save as file", "Save As", [&] { save_as_func(); }); menu_popup_text_button("screenshot", "Screenshot", [&] { main.world->drawProg.switch_to_tool(DrawingProgramToolType::SCREENSHOT); }); menu_popup_text_button("add image or file to canvas", "Add Image/File to Canvas", [&] { #ifdef __EMSCRIPTEN__ emscripten_browser_file::upload("*", [](std::string const& fileName, std::string const& mimeType, std::string_view buffer, void* callbackData) { if(!buffer.empty()) { CustomEvents::emit_event({ .type = CustomEvents::AddFileToCanvasEvent::Type::BUFFER, .name = fileName, .buffer = std::string(buffer) }); } }, &main); #else open_file_selector("Open File", {{"Any File", "*"}}, [&](const std::filesystem::path& p, const auto& e) { CustomEvents::emit_event({ .type = CustomEvents::AddFileToCanvasEvent::Type::PATH, .filePath = p, .pos = main.window.size.cast() / 2.0f }); }); #endif }); if(main.world->netObjMan.is_connected()) { menu_popup_text_button("lobby info", "Lobby Info", [&] { optionsMenuOpen = true; optionsMenuType = LOBBY_INFO_MENU; }); } menu_popup_text_button("start hosting", "Host", [&] { serverLocalID = NetLibrary::get_random_server_local_id(); serverToConnectTo = NetLibrary::get_global_id() + serverLocalID; optionsMenuOpen = true; optionsMenuType = HOST_MENU; }); menu_popup_text_button("canvas specific settings", "Canvas Settings", [&] { optionsMenuOpen = true; optionsMenuType = CANVAS_SETTINGS_MENU; }); } menu_popup_text_button("start connecting", "Connect", [&] { serverToConnectTo.clear(); optionsMenuOpen = true; optionsMenuType = CONNECT_MENU; }); menu_popup_text_button("open options", "Settings", [&] { optionsMenuOpen = true; optionsMenuType = GENERAL_SETTINGS_MENU; }); menu_popup_text_button("about menu button", "About", [&] { optionsMenuOpen = true; optionsMenuType = ABOUT_MENU; }); #ifndef __EMSCRIPTEN__ menu_popup_text_button("quit button", "Quit", [&] { if(main.app_close_requested()) main.setToQuit = true; }); #endif } }, LayoutElement::Callbacks { .onClick = [&, mainMenuButton](LayoutElement* l, const InputManager::MouseButtonCallbackArgs& button) { if(!l->mouseHovering && button.down && !mainMenuButton->mouseHovering) { menuPopUpOpen = false; gui.set_to_layout(); } } }); }); } } }); } void Toolbar::web_version_welcome() { auto& gui = main.g.gui; auto& io = gui.io; CLAY_AUTO_ID({ .layout = { .sizing = {.width = CLAY_SIZING_FIXED(700), .height = CLAY_SIZING_FIT(0) }, .padding = CLAY_PADDING_ALL(io.theme->padding1), .childGap = io.theme->childGap1, .childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_TOP}, .layoutDirection = CLAY_TOP_TO_BOTTOM }, .backgroundColor = convert_vec4(io.theme->backColor1), .cornerRadius = CLAY_CORNER_RADIUS(io.theme->windowCorners1), .floating = {.attachPoints = {.element = CLAY_ATTACH_POINT_CENTER_CENTER, .parent = CLAY_ATTACH_POINT_CENTER_CENTER}, .attachTo = CLAY_ATTACH_TO_PARENT} }) { gui.new_id("web version welcome gui", [&] { text_label_centered(gui, "Welcome to the web version of InfiniPaint!"); text_label(gui, R"(This version contains more known issues than the native version of the app. This includes: - Rare crashes - If this browser tab is unfocused, or the window is minimized, any InfiniPaint tabs connected online (whether host or client) will be disconnected - 4GB memory limit. Might be a problem if you're uploading many files/images - Not multithreaded - Can't access local fonts If you like this app, consider downloading the native version for your system)"); text_button(gui, "got it", "Got It", { .wide = true, .onClick = [&] { main.conf.viewWebVersionWelcome = false; } }); }); } } void Toolbar::center_message(const char* id, const std::string& m) { auto& gui = main.g.gui; auto& io = gui.io; gui.set_z_index(gui.get_z_index() + 1, [&] { gui.element(id, [&] (LayoutElement*, const Clay_ElementId& lId) { CLAY(lId, { .layout = { .sizing = {.width = CLAY_SIZING_FIT(0), .height = CLAY_SIZING_FIT(0) }, .padding = CLAY_PADDING_ALL(io.theme->padding1), .childGap = io.theme->childGap1, .childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_TOP}, .layoutDirection = CLAY_TOP_TO_BOTTOM }, .backgroundColor = convert_vec4(io.theme->backColor1), .cornerRadius = CLAY_CORNER_RADIUS(io.theme->windowCorners1), .floating = { .zIndex = gui.get_z_index(), .attachPoints = {.element = CLAY_ATTACH_POINT_CENTER_CENTER, .parent = CLAY_ATTACH_POINT_CENTER_CENTER}, .attachTo = CLAY_ATTACH_TO_PARENT, } }) { text_label(gui, m); } }); }); } #ifndef __EMSCRIPTEN__ void Toolbar::update_notification_check() { if(!updateCheckerData.updateCheckDone) { if(main.conf.checkForUpdates) { if(!updateCheckerData.versionFile) updateCheckerData.versionFile = FileDownloader::download_data_from_url(UPDATE_NOTIFICATION_URL); else { switch(updateCheckerData.versionFile->status) { case FileDownloader::DownloadData::Status::IN_PROGRESS: break; case FileDownloader::DownloadData::Status::SUCCESS: { updateCheckerData.updateCheckDone = true; std::optional newVersion = version_str_to_version_numbers(updateCheckerData.versionFile->str); std::optional currentVersion = VersionConstants::CURRENT_VERSION_NUMBER; if(newVersion.has_value() && currentVersion.has_value()) { VersionNumber& newV = newVersion.value(); VersionNumber& currentV = currentVersion.value(); updateCheckerData.newVersionStr = version_numbers_to_version_str(newV); Logger::get().log(Logger::LogType::INFO, "Latest online version is v" + updateCheckerData.newVersionStr); if(newV > currentV) { updateCheckerData.showGui = true; main.g.gui.set_to_layout(); } else if(newV == currentV) Logger::get().log(Logger::LogType::INFO, "Current version is up to date"); else Logger::get().log(Logger::LogType::INFO, "Local version has larger version number than the latest online one"); } else Logger::get().log(Logger::LogType::INFO, "Update notification file couldn't be converted to version numbers"); updateCheckerData.versionFile = nullptr; break; } case FileDownloader::DownloadData::Status::FAILURE: Logger::get().log(Logger::LogType::INFO, "Failed to check for updates"); updateCheckerData.updateCheckDone = true; updateCheckerData.versionFile = nullptr; break; } } } else updateCheckerData.updateCheckDone = true; } } void Toolbar::update_notification_gui() { auto& gui = main.g.gui; center_obstructing_window_gui("Update notifications GUI", CLAY_SIZING_FIXED(700), CLAY_SIZING_FIT(0), [&] { gui.new_id("update notification gui", [&] { text_label_centered(gui, "Update v" + updateCheckerData.newVersionStr + " available!"); text_button(gui, "download", "Open download page in web browser", { .wide = true, .onClick = [&]{ SDL_OpenURL(UPDATE_DOWNLOAD_URL); updateCheckerData.showGui = false; } }); text_button(gui, "ignore forever", "Ignore and don't notify again (can be changed in settings)", { .wide = true, .onClick = [&]{ main.conf.checkForUpdates = false; updateCheckerData.showGui = false; } }); text_button(gui, "ignore for now", "Ignore for now", { .wide = true, .onClick = [&]{ updateCheckerData.showGui = false; } }); }); }); } #endif void Toolbar::grid_menu(Element* gridMenuButton) { auto& gui = main.g.gui; auto& io = gui.io; if(main.world->gridMan.grids) { gui.set_z_index(gui.get_z_index() + 1, [&] { gui.element("grid menu", [&] (LayoutElement*, const Clay_ElementId& lId) { CLAY(lId, { .layout = { .sizing = {.width = CLAY_SIZING_FIT(300), .height = CLAY_SIZING_FIT(0, 600) }, .padding = CLAY_PADDING_ALL(io.theme->padding1), .childGap = io.theme->childGap1, .childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_TOP}, .layoutDirection = CLAY_TOP_TO_BOTTOM }, .backgroundColor = convert_vec4(io.theme->backColor1), .cornerRadius = CLAY_CORNER_RADIUS(io.theme->windowCorners1), .floating = {.offset = {.x = 0, .y = static_cast(io.theme->padding1)}, .zIndex = gui.get_z_index(), .attachPoints = {.element = CLAY_ATTACH_POINT_RIGHT_TOP, .parent = CLAY_ATTACH_POINT_RIGHT_BOTTOM}, .attachTo = CLAY_ATTACH_TO_PARENT} }) { text_label_centered(gui, "Grids"); main.world->gridMan.setup_list_gui([&]{stop_displaying_grid_menu();}); } }, LayoutElement::Callbacks { .onClick = [&, gridMenuButton] (LayoutElement* l, const InputManager::MouseButtonCallbackArgs& button) { if(!l->mouseHovering && !l->childMouseHovering && !gridMenuButton->mouseHovering && button.down) stop_displaying_grid_menu(); } }); }); } } void Toolbar::stop_displaying_grid_menu() { gridMenuPopupOpen = false; main.world->gridMan.refresh_gui_data(); main.g.gui.set_to_layout(); } void Toolbar::stop_displaying_bookmark_menu() { main.world->bMan.refresh_gui_data(); bookmarkMenuPopupOpen = false; main.g.gui.set_to_layout(); } void Toolbar::stop_displaying_layer_menu() { main.world->drawProg.layerMan.listGUI.refresh_gui_data(); layerMenuPopupOpen = false; main.g.gui.set_to_layout(); } void Toolbar::bookmark_menu(Element* bookmarkMenuButton) { auto& gui = main.g.gui; auto& io = gui.io; gui.set_z_index(gui.get_z_index() + 1, [&] { gui.element("bookmark menu", [&] (LayoutElement*, const Clay_ElementId& lId) { CLAY(lId, { .layout = { .sizing = {.width = CLAY_SIZING_FIT(300), .height = CLAY_SIZING_FIT(0, 600) }, .padding = CLAY_PADDING_ALL(io.theme->padding1), .childGap = io.theme->childGap1, .childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_TOP}, .layoutDirection = CLAY_TOP_TO_BOTTOM }, .backgroundColor = convert_vec4(io.theme->backColor1), .cornerRadius = CLAY_CORNER_RADIUS(io.theme->windowCorners1), .floating = {.offset = {.x = 0, .y = static_cast(io.theme->padding1)}, .zIndex = gui.get_z_index(), .attachPoints = {.element = CLAY_ATTACH_POINT_RIGHT_TOP, .parent = CLAY_ATTACH_POINT_RIGHT_BOTTOM}, .attachTo = CLAY_ATTACH_TO_PARENT} }) { text_label_centered(gui, "Bookmarks"); main.world->bMan.setup_list_gui(); } }, LayoutElement::Callbacks { .onClick = [&, bookmarkMenuButton] (LayoutElement* l, const InputManager::MouseButtonCallbackArgs& button) { if(!l->mouseHovering && !l->childMouseHovering && !bookmarkMenuButton->mouseHovering && button.down) stop_displaying_bookmark_menu(); } }); }); } void Toolbar::layer_menu(Element* layerMenuButton) { auto& gui = main.g.gui; auto& io = gui.io; gui.set_z_index(gui.get_z_index() + 1, [&] { gui.element("layer menu", [&] (LayoutElement*, const Clay_ElementId& lId) { CLAY(lId, { .layout = { .sizing = {.width = CLAY_SIZING_FIT(300), .height = CLAY_SIZING_FIT(0, 600) }, .padding = CLAY_PADDING_ALL(io.theme->padding1), .childGap = io.theme->childGap1, .childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_TOP}, .layoutDirection = CLAY_TOP_TO_BOTTOM }, .backgroundColor = convert_vec4(io.theme->backColor1), .cornerRadius = CLAY_CORNER_RADIUS(io.theme->windowCorners1), .floating = {.offset = {.x = 0, .y = static_cast(io.theme->padding1)}, .zIndex = gui.get_z_index(), .attachPoints = {.element = CLAY_ATTACH_POINT_RIGHT_TOP, .parent = CLAY_ATTACH_POINT_RIGHT_BOTTOM}, .attachTo = CLAY_ATTACH_TO_PARENT} }) { text_label_centered(gui, "Layers"); main.world->drawProg.layerMan.listGUI.setup_list_gui(); } }, LayoutElement::Callbacks { .onClick = [&, layerMenuButton] (LayoutElement* l, const InputManager::MouseButtonCallbackArgs& button) { if(!l->mouseHovering && !l->childMouseHovering && !layerMenuButton->mouseHovering && button.down) stop_displaying_layer_menu(); } }); }); } RichText::TextData Toolbar::build_paragraph_from_chat_message(const ChatMessage& message, float alpha) { auto& gui = main.g.gui; auto& io = gui.io; RichText::TextData toRet; auto& par = toRet.paragraphs.emplace_back(); RichText::PositionedTextStyleMod& positionedModInit = toRet.tStyleMods.emplace_back(); positionedModInit.pos = {0, 0}; positionedModInit.mods[RichText::TextStyleModifier::ModifierType::WEIGHT] = std::make_shared(SkFontStyle::Weight::kBold_Weight); positionedModInit.mods[RichText::TextStyleModifier::ModifierType::COLOR] = std::make_shared(convert_vec4(color_mul_alpha(message.type == ChatMessage::JOIN ? io.theme->warningColor : io.theme->frontColor1, alpha))); if(message.type == ChatMessage::JOIN) par.text += message.name + " "; else par.text += "[" + message.name + "] "; RichText::PositionedTextStyleMod& positionedModMessage = toRet.tStyleMods.emplace_back(); positionedModMessage.pos = {0, par.text.size()}; positionedModMessage.mods[RichText::TextStyleModifier::ModifierType::WEIGHT] = std::make_shared(SkFontStyle::Weight::kNormal_Weight); par.text += message.message; return toRet; } void Toolbar::chat_box() { auto& gui = main.g.gui; auto& io = gui.io; constexpr float CHATBOX_WIDTH = 700; if(main.world->netObjMan.is_connected()) { gui.element("Infinipaint chat box open button", [&](LayoutElement*, const Clay_ElementId& lId) { CLAY(lId, { .layout = { .layoutDirection = CLAY_LEFT_TO_RIGHT }, .floating = {.offset = {static_cast(io.theme->padding1), -static_cast(io.theme->padding1)}, .zIndex = gui.get_z_index(), .attachPoints = {.element = CLAY_ATTACH_POINT_LEFT_BOTTOM, .parent = CLAY_ATTACH_POINT_LEFT_BOTTOM}, .attachTo = CLAY_ATTACH_TO_PARENT} }) { svg_icon_button(gui, "Chat open button", "data/icons/chat.svg", { .onClick = [&] { chatboxOpen = !chatboxOpen; chatMessageInput.clear(); } }); } }); } gui.element("Infinipaint chat box", [&](LayoutElement*, const Clay_ElementId& lId) { CLAY(lId, { .layout = { .sizing = {.width = CLAY_SIZING_FIXED(CHATBOX_WIDTH), .height = CLAY_SIZING_FIT(0) }, .childGap = 0, .childAlignment = { .x = CLAY_ALIGN_X_LEFT, .y = CLAY_ALIGN_Y_BOTTOM}, .layoutDirection = CLAY_TOP_TO_BOTTOM }, .floating = {.offset = {60 + static_cast(io.theme->padding1), -static_cast(io.theme->padding1)}, .zIndex = gui.get_z_index(), .attachPoints = {.element = CLAY_ATTACH_POINT_LEFT_BOTTOM, .parent = CLAY_ATTACH_POINT_LEFT_BOTTOM}, .attachTo = CLAY_ATTACH_TO_PARENT} }) { if(chatboxOpen) { CLAY_AUTO_ID({ .layout = { .sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIT(0) }, .padding = CLAY_PADDING_ALL(0), .childGap = 0, .childAlignment = { .x = CLAY_ALIGN_X_LEFT, .y = CLAY_ALIGN_Y_BOTTOM}, .layoutDirection = CLAY_TOP_TO_BOTTOM }, .backgroundColor = convert_vec4(io.theme->backColor1) }) { int id = 0; for(auto& chatMessage : main.world->chatMessages | std::views::reverse) { gui.new_id(id++, [&] { gui.element("text", TextParagraph::Data{ .text = build_paragraph_from_chat_message(chatMessage, 1.0f), .maxGrowX = CHATBOX_WIDTH, .ellipsis = false }); }); } } left_to_right_line_layout(gui, [&] { input_text(gui, "message input", &chatMessageInput, { .onEnter = [&] { if(!chatMessageInput.empty()) main.world->send_chat_message(chatMessageInput); chatboxOpen = false; }, .onDeselect = [&] { chatboxOpen = false; gui.set_to_layout(); } })->select(); text_button(gui, "send button", "Send", { .instantResponse = true, .onClick = [&] { if(!chatMessageInput.empty()) main.world->send_chat_message(chatMessageInput); chatboxOpen = false; } }); }); } else { gui.new_id("Message popups", [&] { int id = 0; for(auto& chatMessage : main.world->chatMessages | std::views::reverse) { chatMessage.time.update_time_since(); if(chatMessage.time < ChatMessage::DISPLAY_TIME) { gui.new_id(id++, [&] { float a = 1.0f - lerp_time(chatMessage.time, ChatMessage::DISPLAY_TIME, ChatMessage::FADE_START_TIME); gui.element("Chat message", [&](LayoutElement*, const Clay_ElementId& lId) { CLAY(lId, { .layout = { .sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIT(0) }, .padding = CLAY_PADDING_ALL(0), .childGap = 0, .childAlignment = { .x = CLAY_ALIGN_X_LEFT, .y = CLAY_ALIGN_Y_TOP}, .layoutDirection = CLAY_TOP_TO_BOTTOM }, .backgroundColor = convert_vec4(color_mul_alpha(io.theme->backColor1, a)), }) { gui.element("message", TextParagraph::Data{ .text = build_paragraph_from_chat_message(chatMessage, a), .maxGrowX = CHATBOX_WIDTH, .ellipsis = false }); } }); }); } } }); } } }); } void Toolbar::global_log() { auto& gui = main.g.gui; auto& io = gui.io; gui.new_id("Global log popup list", [&] { CLAY_AUTO_ID({ .layout = { .sizing = {.width = CLAY_SIZING_FIXED(300), .height = CLAY_SIZING_FIT(0) }, .childGap = io.theme->childGap1, .childAlignment = { .x = CLAY_ALIGN_X_RIGHT, .y = CLAY_ALIGN_Y_TOP}, .layoutDirection = CLAY_TOP_TO_BOTTOM }, .floating = {.offset = {0, 10}, .attachPoints = {.element = CLAY_ATTACH_POINT_RIGHT_TOP, .parent = CLAY_ATTACH_POINT_RIGHT_BOTTOM}, .attachTo = CLAY_ATTACH_TO_PARENT} }) { for(size_t i = 0; i < main.logMessages.size(); i++) { auto& logM = main.logMessages[i]; logM.time.update_time_since(); if(logM.time < UserLogMessage::DISPLAY_TIME) { if(logM.whereToDisplay == UserLogMessage::DISPLAY_PHONE_ONLY) continue; float a = 1.0f - lerp_time(logM.time, UserLogMessage::DISPLAY_TIME, UserLogMessage::FADE_START_TIME); gui.new_id(i, [&] { gui.element("Global log message", [&] (LayoutElement*, const Clay_ElementId& lId) { CLAY(lId, { .layout = { .sizing = {.width = CLAY_SIZING_FIT(300), .height = CLAY_SIZING_FIT(0) }, .padding = CLAY_PADDING_ALL(io.theme->padding1), .childGap = 0, .childAlignment = { .x = CLAY_ALIGN_X_LEFT, .y = CLAY_ALIGN_Y_TOP}, .layoutDirection = CLAY_TOP_TO_BOTTOM }, .backgroundColor = convert_vec4(color_mul_alpha(io.theme->backColor1, a)), .cornerRadius = CLAY_CORNER_RADIUS(io.theme->windowCorners1) }) { SkColor4f c{0, 0, 0, 0}; switch(logM.color) { case UserLogMessage::COLOR_NORMAL: c = io.theme->frontColor1; break; case UserLogMessage::COLOR_ERROR: c = io.theme->errorColor; break; } text_label_color(gui, logM.text, color_mul_alpha(c, a)); } }); }); } else break; } } }); } void Toolbar::drawing_program_gui() { auto& gui = main.g.gui; auto& io = gui.io; CLAY_AUTO_ID({ .layout = { .sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_GROW(0) }, .childGap = io.theme->childGap1, .childAlignment = { .x = CLAY_ALIGN_X_LEFT, .y = CLAY_ALIGN_Y_CENTER}, .layoutDirection = CLAY_LEFT_TO_RIGHT }, }) { main.world->drawProg.toolbar_gui(*this); colorLeft = main.world->drawProg.color_picker_color(colorLeft); if(colorLeft) color_picker_window("Drawing program gui color picker left", &colorLeft, colorLeftButton, colorLeftData); CLAY_AUTO_ID({ .layout = { .sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_GROW(0)} } }) {} colorRight = main.world->drawProg.color_picker_color(colorRight); if(colorRight) color_picker_window("Drawing program gui color picker right", &colorRight, colorRightButton, colorRightData); main.world->drawProg.tool_options_gui(*this); } } void Toolbar::color_picker_window(const char* id, Vector4f** color, GUIStuff::Element* b, const ColorSelectorData& colorSelectorData) { auto& gui = main.g.gui; CLAY_AUTO_ID({ .layout = { .padding = {.top = 40, .bottom = 40} } }) { main.g.gui.element(id, [&](LayoutElement*, const Clay_ElementId& lId) { CLAY(lId, { .layout = { .sizing = {.width = CLAY_SIZING_FIT(300), .height = CLAY_SIZING_FIT(0)}, .padding = CLAY_PADDING_ALL(gui.io.theme->padding1), .childGap = gui.io.theme->childGap1, .childAlignment = { .x = CLAY_ALIGN_X_LEFT, .y = CLAY_ALIGN_Y_TOP}, .layoutDirection = CLAY_TOP_TO_BOTTOM }, .backgroundColor = convert_vec4(gui.io.theme->backColor1), .cornerRadius = CLAY_CORNER_RADIUS(gui.io.theme->windowCorners1) }) { color_picker_items(gui, "colorpicker", *color, { .onEdit = colorSelectorData.onChange, .onSelect = colorSelectorData.onSelect, .onDeselect = colorSelectorData.onDeselect, }); color_palette("colorpickerpalette", *color, [colorSelectorData] { if(colorSelectorData.onSelect) colorSelectorData.onSelect(); if(colorSelectorData.onChange) colorSelectorData.onChange(); if(colorSelectorData.onDeselect) colorSelectorData.onDeselect(); }); } }, LayoutElement::Callbacks{ .onClick = [&, b, color](LayoutElement* l, const InputManager::MouseButtonCallbackArgs& button) { if(!l->mouseHovering && !l->childMouseHovering && !b->mouseHovering && button.down) { *color = nullptr; main.g.gui.set_to_layout(); } } }); } } void Toolbar::color_palette(const char* id, Vector4f* color, const std::function& onChange) { auto& gui = main.g.gui; auto& io = gui.io; gui.new_id(id, [&] { auto& palette = main.conf.palettes[paletteData.selectedPalette].colors; gui.clipping_element("color palette scroll area", ScrollArea::Options{ .scrollVertical = true, .clipVertical = true, .scrollbarY = ScrollArea::ScrollbarType::NORMAL, .innerContent = [&](const ScrollArea::InnerContentParameters&) { CLAY_AUTO_ID({ .layout = { .sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_GROW(0)}, .childGap = io.theme->childGap1, .layoutDirection = CLAY_TOP_TO_BOTTOM } }) { size_t i = 0; size_t nextID = 0; while(i < palette.size()) { CLAY_AUTO_ID({ .layout = { .sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIXED(BIG_BUTTON_SIZE)}, .childGap = io.theme->childGap1, .childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER}, .layoutDirection = CLAY_LEFT_TO_RIGHT } }) { while(i < palette.size()) { CLAY_AUTO_ID({ .layout = { .sizing = {.width = CLAY_SIZING_FIXED(BIG_BUTTON_SIZE), .height = CLAY_SIZING_FIXED(BIG_BUTTON_SIZE)} } }) { auto newC = std::make_shared(palette[i].x(), palette[i].y(), palette[i].z()); gui.new_id(nextID++, [&] { color_button(gui, "c", newC.get(), { .isSelected = newC->x() == color->x() && newC->y() == color->y() && newC->z() == color->z(), .hasAlpha = false, .onClick = [newC, color, onChange] { // We want to keep the old color's alpha color->x() = newC->x(); color->y() = newC->y(); color->z() = newC->z(); if(onChange) onChange(); } }); }); } i++; if(i % 6 == 0) break; } } } } } }); if(paletteData.selectedPalette != 0) { CLAY_AUTO_ID({ .layout = { .sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIT(0)}, .padding = {.top = 3, .bottom = 3}, .childGap = io.theme->childGap1, .childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER}, .layoutDirection = CLAY_LEFT_TO_RIGHT } }) { svg_icon_button(gui, "addcolor", "data/icons/plus.svg", { .onClick = [&, color] { std::erase(palette, Vector3f{color->x(), color->y(), color->z()}); palette.emplace_back(color->x(), color->y(), color->z()); } }); svg_icon_button(gui, "deletecolor", "data/icons/close.svg", { .onClick = [&, color] { std::erase(palette, Vector3f{color->x(), color->y(), color->z()}); } }); } } CLAY_AUTO_ID({ .layout = { .sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIT(0)}, .padding = {.top = 3, .bottom = 3}, .childGap = io.theme->childGap1, .childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER}, .layoutDirection = CLAY_LEFT_TO_RIGHT } }) { std::vector paletteNames; for(auto& p : main.conf.palettes) paletteNames.emplace_back(p.name); gui.element>("paletteselector", &paletteData.selectedPalette, paletteNames, DropdownOptions{ .onClick = [&] { gui.set_to_layout(); } }); svg_icon_button(gui, "paletteadd", "data/icons/plus.svg", { .size = 25.0f, .onClick = [&] { paletteData.addingPalette = !paletteData.addingPalette; } }); svg_icon_button(gui, "paletteremove", "data/icons/close.svg", { .size = 25.0f, .onClick = [&] { main.conf.palettes.erase(main.conf.palettes.begin() + paletteData.selectedPalette); paletteData.selectedPalette = 0; } }); } if(paletteData.addingPalette) { input_text_field(gui, "paletteinputname", "Name", &paletteData.newPaletteStr); text_button_wide("addpalettebutton", "Create", [&] { if(!paletteData.newPaletteStr.empty()) { main.conf.palettes.emplace_back(); main.conf.palettes.back().name = paletteData.newPaletteStr; paletteData.selectedPalette = main.conf.palettes.size() - 1; paletteData.addingPalette = false; } }); } }); } void Toolbar::player_list() { auto& gui = main.g.gui; center_obstructing_window_gui("player client list", CLAY_SIZING_FIT(500), CLAY_SIZING_FIT(0), [&] { gui.new_id("client list", [&] { text_label_centered(gui, "Player List"); if(!main.world->clientStillConnecting) { left_to_right_line_layout(gui, [&]() { CLAY_AUTO_ID({ .layout = { .sizing = {.width = CLAY_SIZING_FIXED(20), .height = CLAY_SIZING_FIXED(20)} }, .backgroundColor = convert_vec4(SkColor4f{main.world->ownClientData->get_cursor_color().x(), main.world->ownClientData->get_cursor_color().y(), main.world->ownClientData->get_cursor_color().z(), 1.0f}), .cornerRadius = CLAY_CORNER_RADIUS(3) }) {} text_label(gui, main.world->ownClientData->get_display_name()); }); size_t num = 0; for(auto& client : main.world->clients->get_data()) { if(client != main.world->ownClientData) { gui.new_id(num++, [&] { left_to_right_line_layout(gui, [&]() { CLAY_AUTO_ID({ .layout = { .sizing = {.width = CLAY_SIZING_FIXED(20), .height = CLAY_SIZING_FIXED(20)} }, .backgroundColor = convert_vec4(SkColor4f{client->get_cursor_color().x(), client->get_cursor_color().y(), client->get_cursor_color().z(), 1.0f}), .cornerRadius = CLAY_CORNER_RADIUS(3) }) {} text_label(gui, client->get_display_name()); CLAY_AUTO_ID({.layout = {.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_GROW(0)}}}) {} text_button(gui, "teleport button", "Jump To", { .onClick = [&] { main.world->drawData.cam.smooth_move_to(*main.world, client->get_cam_coords(), client->get_window_size()); }}); }); }); } } } text_button(gui, "close list", "Done", { .wide = true, .onClick = [&] { playerMenuOpen = false; }}); }); }); } void Toolbar::open_world_file(bool isClient, const std::string& netSource, const std::string& serverLocalID2) { #ifdef __EMSCRIPTEN__ static struct UploadData { bool iC; std::string nS; std::string sLID; MainProgram* main; } uploadData; uploadData.iC = isClient; uploadData.nS = netSource; uploadData.sLID = serverLocalID2; uploadData.main = &main; emscripten_browser_file::upload("." + World::FILE_EXTENSION, [](std::string const& fileName, std::string const& mimeType, std::string_view buffer, void* callbackData) { if(!buffer.empty()) { UploadData* uD = (UploadData*)callbackData; CustomEvents::emit_event({ .isClient = uD->iC, .filePathSource = std::filesystem::path(fileName), .netSource = uD->nS, .serverLocalID = uD->sLID, .fileDataBuffer = buffer }); } }, &uploadData); #else open_file_selector("Open", {{"InfiniPaint Canvas", World::FILE_EXTENSION}, {"Any File", "*"}}, [&, isClient = isClient, netSource = netSource, serverLocalID2 = serverLocalID2](const std::filesystem::path& p, const auto& e) { CustomEvents::emit_event({ .isClient = isClient, .filePathSource = p, .netSource = netSource, .serverLocalID = serverLocalID2 }); }); #endif } void Toolbar::center_obstructing_window_gui(const char* id, Clay_SizingAxis x, Clay_SizingAxis y, const std::function& innerContent) { auto& gui = main.g.gui; auto& io = gui.io; gui.set_z_index(100, [&] { gui.element(id, [&] (LayoutElement*, const Clay_ElementId& id) { CLAY(id, { .layout = { .sizing = {.width = x, .height = y }, .padding = CLAY_PADDING_ALL(io.theme->padding1), .childGap = io.theme->childGap1, .childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_TOP}, .layoutDirection = CLAY_TOP_TO_BOTTOM }, .backgroundColor = convert_vec4(io.theme->backColor1), .cornerRadius = CLAY_CORNER_RADIUS(io.theme->windowCorners1), .floating = {.zIndex = gui.get_z_index(), .attachPoints = {.element = CLAY_ATTACH_POINT_CENTER_CENTER, .parent = CLAY_ATTACH_POINT_CENTER_CENTER}, .attachTo = CLAY_ATTACH_TO_PARENT} }) { innerContent(); } }); }); } void Toolbar::text_button_wide(const char* id, const char* str, const std::function& onClick) { auto& gui = main.g.gui; text_button(gui, id, str, { .wide = true, .onClick = onClick }); } void Toolbar::options_menu() { auto& gui = main.g.gui; switch(optionsMenuType) { case HOST_MENU: { center_obstructing_window_gui("host menu", CLAY_SIZING_FIT(650), CLAY_SIZING_FIT(0), [&] { input_text_field(gui, "lobby", "Lobby", &serverToConnectTo, { .immutable = true }); left_to_right_line_layout(gui, [&]() { text_button_wide("copy lobby address", "Copy Lobby Address", [&] { main.input.set_clipboard_str(serverToConnectTo); }); text_button_wide("host file", "Host", [&] { main.world->start_hosting(serverToConnectTo, serverLocalID); optionsMenuOpen = false; }); text_button_wide("cancel", "Cancel", [&] { optionsMenuOpen = false; }); }); }); break; } case CONNECT_MENU: { center_obstructing_window_gui("connect menu", CLAY_SIZING_FIT(650), CLAY_SIZING_FIT(0), [&] { input_text_field(gui, "lobby", "Lobby", &serverToConnectTo); left_to_right_line_layout(gui, [&]() { text_button_wide("connect", "Connect", [&] { if(serverToConnectTo.length() != (NetLibrary::LOCALID_LEN + NetLibrary::GLOBALID_LEN)) Logger::get().log(Logger::LogType::DESKTOP_USERINFO, "Connect issue: Incorrect address length"); else if(serverToConnectTo.substr(0, NetLibrary::GLOBALID_LEN) == NetLibrary::get_global_id()) Logger::get().log(Logger::LogType::DESKTOP_USERINFO, "Connect issue: Can't connect to your own address"); else { CustomEvents::emit_event({ .isClient = true, .netSource = serverToConnectTo }); optionsMenuOpen = false; } }); text_button_wide("cancel", "Cancel", [&] { optionsMenuOpen = false; }); }); }); break; } case GENERAL_SETTINGS_MENU: { center_obstructing_window_gui("gsettings", CLAY_SIZING_FIXED(600), CLAY_SIZING_FIXED(500), [&] { general_settings_inner_gui(); }); break; } case LOBBY_INFO_MENU: { center_obstructing_window_gui("lobby info menu", CLAY_SIZING_FIT(650), CLAY_SIZING_FIT(0), [&] { input_text_field(gui, "lobby", "Lobby", &main.world->netSource); left_to_right_line_layout(gui, [&]() { text_button_wide("copy lobby address", "Copy Lobby Address", [&] { main.input.set_clipboard_str(main.world->netSource); }); text_button_wide("done", "Done", [&] { optionsMenuOpen = false; }); }); }); break; } case CANVAS_SETTINGS_MENU: { center_obstructing_window_gui("canvas settings menu", CLAY_SIZING_FIT(500), CLAY_SIZING_FIT(0), [&] { auto newColorToSet = std::make_shared(main.world->canvasTheme.get_back_color()); color_picker_button_field(gui, "canvasColor", "Canvas Color", newColorToSet.get(), { .hasAlpha = false, .onEdit = [&, newColorToSet] { main.world->canvasTheme.set_back_color(convert_vec3(*newColorToSet)); } }); text_button_wide("done", "Done", [&] { optionsMenuOpen = false; }); }); break; } case SET_DOWNLOAD_NAME: { center_obstructing_window_gui("set download name menu", CLAY_SIZING_FIT(500), CLAY_SIZING_FIT(0), [&] { input_text_field(gui, "file name", "File Name", &downloadNameSet); left_to_right_line_layout(gui, [&]() { text_button_wide("download save button", "Save", [&] { main.world->save_to_file(downloadNameSet); optionsMenuOpen = false; }); text_button_wide("cancel", "Cancel", [&] { optionsMenuOpen = false; }); }); }); break; } case ABOUT_MENU: { center_obstructing_window_gui("about menu", CLAY_SIZING_FIXED(650), CLAY_SIZING_FIXED(500), [&] { about_menu_inner_gui(); }); break; } } } void Toolbar::general_settings_inner_gui() { auto& gui = main.g.gui; auto& io = gui.io; CLAY_AUTO_ID({ .layout = { .sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_GROW(0)}, .padding = CLAY_PADDING_ALL(io.theme->padding1), .childGap = io.theme->childGap1, .childAlignment = { .x = CLAY_ALIGN_X_LEFT, .y = CLAY_ALIGN_Y_TOP}, .layoutDirection = CLAY_LEFT_TO_RIGHT } }) { CLAY_AUTO_ID({ .layout = { .sizing = {.width = CLAY_SIZING_FIT(150), .height = CLAY_SIZING_GROW(0) }, .padding = CLAY_PADDING_ALL(io.theme->padding1), .childGap = 2, .childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_TOP}, .layoutDirection = CLAY_TOP_TO_BOTTOM } }) { auto category_button = [&](const char* id, const char* str, GeneralSettingsOptions opt) { text_button(gui, id, str, { .drawType = SelectableButton::DrawType::TRANSPARENT_ALL, .isSelected = generalSettingsOptions == opt, .wide = true, .centered = false, .onClick = [&, opt] { main.g.load_theme(main.conf.configPath, main.conf.themeCurrentlyLoaded); themeData.selectedThemeIndex = std::nullopt; generalSettingsOptions = opt; main.keybindWaiting = std::nullopt; } }); }; category_button("Generalbutton", "General", GSETTINGS_GENERAL); category_button("Tabletbutton", "Tablet", GSETTINGS_TABLET); category_button("Themebutton", "Theme", GSETTINGS_THEME); category_button("Keybindsbutton", "Keybinds", GSETTINGS_KEYBINDS); category_button("Debugbutton", "Debug", GSETTINGS_DEBUG); } CLAY_AUTO_ID({ .layout = { .sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_GROW(0) }, .childAlignment = { .x = CLAY_ALIGN_X_LEFT, .y = CLAY_ALIGN_Y_TOP}, .layoutDirection = CLAY_TOP_TO_BOTTOM } }) { auto general_scroll_area = [&](const char* id, const std::function& innerContent) { gui.clipping_element("general settings scroll area", ScrollArea::Options{ .scrollVertical = true, .clipVertical = true, .scrollbarY = ScrollArea::ScrollbarType::NORMAL, .innerContent = [&, innerContent](const ScrollArea::InnerContentParameters&) { CLAY_AUTO_ID({ .layout = { .sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_GROW(0) }, .padding = CLAY_PADDING_ALL(io.theme->padding1), .childGap = io.theme->childGap1, .childAlignment = { .x = CLAY_ALIGN_X_LEFT, .y = CLAY_ALIGN_Y_TOP}, .layoutDirection = CLAY_TOP_TO_BOTTOM } }) { innerContent(); } } }); }; switch(generalSettingsOptions) { case GSETTINGS_GENERAL: { general_scroll_area("general settings", [&] { input_text_field(gui, "display name input", "Display name", &main.conf.displayName, { .onEdit = [&] { main.update_display_names(); } }); color_picker_button_field(gui, "defaultCanvasBackgroundColor", "Default canvas background color", &main.conf.defaultCanvasBackgroundColor, { .hasAlpha = false }); #ifndef __EMSCRIPTEN__ checkbox_boolean_field(gui, "native file pick", "Use native file picker", &main.conf.useNativeFilePicker); checkbox_boolean_field(gui, "update notifications enable", "Check for updates on startup", &main.conf.checkForUpdates); #endif slider_scalar_field(gui, "drag zoom slider", "Drag zoom speed", &main.conf.dragZoomSpeed, 0.0, 1.0, {.decimalPrecision = 3}); slider_scalar_field(gui, "scroll zoom slider", "Scroll zoom speed", &main.conf.scrollZoomSpeed, 0.0, 1.0, {.decimalPrecision = 3}); checkbox_boolean_field(gui, "flip zoom tool direction", "Flip zoom tool direction", &main.conf.flipZoomToolDirection); checkbox_boolean_field(gui, "make all tools share same size", "Make all tools share size", &main.toolConfig.globalConf.useGlobalRelativeWidth); #ifndef __EMSCRIPTEN__ checkbox_boolean_field(gui, "disable graphics driver workarounds", "Disable graphics driver workarounds (enabling or disabling this might fix some graphical glitches, requires restart)", &main.conf.disableGraphicsDriverWorkarounds); #endif input_scalar_field(gui, "jump transition time", "Jump transition time", &main.conf.jumpTransitionTime, 0.01f, 1000.0f, {.decimalPrecision = 2}); input_scalar_field(gui, "Max GUI Scale", "Max GUI Scale", &main.conf.guiScale, 0.5f, 5.0f, { .decimalPrecision = 1, .onEdit = [&] { main.g.window_update(); } }); text_label(gui, "Anti-aliasing:"); radio_button_selector(gui, "Antialiasing selector", &main.conf.antialiasing, { {"None", GlobalConfig::AntiAliasing::NONE}, {"Skia", GlobalConfig::AntiAliasing::SKIA}, {"Dynamic MSAA", GlobalConfig::AntiAliasing::DYNAMIC_MSAA} }, [&] { main.refresh_draw_surfaces(); }); text_label(gui, "VSync:"); radio_button_selector(gui, "VSync selector", &main.conf.vsyncValue, { {"On", 1}, {"Off", 0}, {"Adaptive", -1} }, [&] { main.set_vsync_value(main.conf.vsyncValue); }); #ifndef __EMSCRIPTEN__ checkbox_boolean_field(gui, "apply display scale", "Apply display scale", &main.conf.applyDisplayScale); #endif }); break; } case GSETTINGS_TABLET: { general_scroll_area("tablet settings", [&] { checkbox_boolean_field(gui, "pen pressure width", "Pen pressure affects brush size", &main.conf.tabletOptions.pressureAffectsBrushWidth); input_scalar_field(gui, "middle click", "Middle click pen button", &main.conf.tabletOptions.middleClickButton, 1, 255); input_scalar_field(gui, "right click", "Right click pen button", &main.conf.tabletOptions.rightClickButton, 1, 255); slider_scalar_field(gui, "tablet brush minimum size", "Brush relative minimum size", &main.conf.tabletOptions.brushMinimumSize, 0.0f, 1.0f, {.decimalPrecision = 3}); slider_scalar_field(gui, "tablet brush pressure smoothing factor", "Brush pressure smoothing factor", &main.conf.tabletOptions.brushPressureSmoothingFactor, 0.0f, 1.0f, {.decimalPrecision = 3}); checkbox_boolean_field(gui, "tablet zoom with button method", "Zoom when pen touching tablet and pen button assigned to middle click is held", &main.conf.tabletOptions.zoomWhilePenDownAndButtonHeld); #ifdef _WIN32 checkbox_boolean_field(gui, "mouse ignore when pen proximity", "Ignore mouse movement when pen in proximity", &main.conf.tabletOptions.ignoreMouseMovementWhenPenInProximity); #endif checkbox_boolean_field(gui, "disable touch when pen in proximity", "Disable touch when pen in proximity", &main.conf.tabletOptions.disableTouchWhenPenInProximity); }); break; } case GSETTINGS_THEME: { general_scroll_area("theme", [&] { if(!themeData.selectedThemeIndex) reload_theme_list(); CLAY_AUTO_ID({.layout = { .sizing = {.width = CLAY_SIZING_GROW(0)}, .childGap = io.theme->childGap1, .childAlignment = {.x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER}, .layoutDirection = CLAY_LEFT_TO_RIGHT, } }) { text_label(gui, "Theme: "); gui.element>("dropdownSelectThemes", &themeData.selectedThemeIndex.value(), themeData.themeDirList, DropdownOptions{ .onClick = [&]() { main.conf.themeCurrentlyLoaded = themeData.themeDirList[themeData.selectedThemeIndex.value()]; reload_theme_list(); } }); } left_to_right_line_layout(gui, [&]() { if(themeData.selectedThemeIndex != 0) { text_button_wide("savethemebutton", "Save", [&] { main.conf.themeCurrentlyLoaded = themeData.themeDirList[themeData.selectedThemeIndex.value()]; main.g.save_theme(main.conf.configPath, main.conf.themeCurrentlyLoaded); reload_theme_list(); }); } text_button_wide("saveasthemebutton", "Save As", [&]() { themeData.openedSaveAsMenu = !themeData.openedSaveAsMenu; }); text_button_wide("reloadthemebutton", "Reload", [&] { main.conf.themeCurrentlyLoaded = themeData.themeDirList[themeData.selectedThemeIndex.value()]; reload_theme_list(); }); if(themeData.selectedThemeIndex != 0) { text_button_wide("deletethemebutton", "Delete", [&] { try { std::filesystem::remove(main.conf.configPath / "themes" / (themeData.themeDirList[themeData.selectedThemeIndex.value()] + ".json")); } catch(...) { } main.conf.themeCurrentlyLoaded = "Default"; reload_theme_list(); }); } }); if(themeData.openedSaveAsMenu) { input_text_field(gui, "Theme name:", "Theme name: ", &main.conf.themeCurrentlyLoaded); left_to_right_line_layout(gui, [&]() { text_button_wide("saveasdone", "Done", [&] { main.g.save_theme(main.conf.configPath, main.conf.themeCurrentlyLoaded); reload_theme_list(); }); text_button_wide("saveascancel", "Cancel", [&] { themeData.openedSaveAsMenu = false; }); }); } text_label(gui, "Edit theme:"); text_label_light(gui, "Note: Changes only remain if theme is saved"); auto theme_color_field = [&](const char* id, const char* name, SkColor4f* c) { color_picker_button_field(gui, id, name, c, {}); }; theme_color_field("fillColor2", "Fill Color 2", &io.theme->fillColor2); theme_color_field("backColor1", "Back Color 1", &io.theme->backColor1); theme_color_field("backColor2", "Back Color 2", &io.theme->backColor2); theme_color_field("frontColor1", "Front Color 1", &io.theme->frontColor1); theme_color_field("frontColor2", "Front Color 2", &io.theme->frontColor2); theme_color_field("warningColor", "Warning Color", &io.theme->warningColor); theme_color_field("errorColor", "Error Color", &io.theme->errorColor); //gui.slider_scalar_field("hoverExpandTime", "Hover Expand Time", &io.theme->hoverExpandTime, 0.001f, 1.0f); input_scalar_field(gui, "childGap1", "Gap between child elements", &io.theme->childGap1, 0, 30); input_scalar_field(gui, "padding1", "Window padding", &io.theme->padding1, 0, 30); slider_scalar_field(gui, "windowCorners1", "Window corner radius", &io.theme->windowCorners1, 0, 30); }); break; } case GSETTINGS_KEYBINDS: { general_scroll_area("keybind entries", [&] { for(unsigned i = 0; i < InputManager::KEY_ASSIGNABLE_COUNT; i++) { gui.new_id(i, [&] { CLAY_AUTO_ID({ .layout = { .sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIT(0) }, .padding = CLAY_PADDING_ALL(0), .childGap = io.theme->childGap1, .childAlignment = { .x = CLAY_ALIGN_X_LEFT, .y = CLAY_ALIGN_Y_CENTER}, .layoutDirection = CLAY_LEFT_TO_RIGHT } }) { text_label(gui, std::string(nlohmann::json(static_cast(i)))); auto f = std::find_if(main.input.keyAssignments.begin(), main.input.keyAssignments.end(), [&](auto& p) { return p.second == i; }); std::string assignedKeystrokeStr = f != main.input.keyAssignments.end() ? main.input.key_assignment_to_str(f->first) : ""; text_button(gui, "keybind button", assignedKeystrokeStr, { .isSelected = main.keybindWaiting.has_value() && main.keybindWaiting.value() == i, .wide = true, .onClick = [&, i] { main.keybindWaiting = i; } }); } }); } }); break; } case GSETTINGS_DEBUG: { general_scroll_area("debug settings menu", [&] { checkbox_boolean_field(gui, "use mobile UI", "Use mobile UI (requires restart)", &main.conf.mobileUI); input_scalars_field(gui, "jump transition easing", "Jump easing", &main.conf.jumpTransitionEasing, 4, -10.0f, 10.0f, { .decimalPrecision = 2 }); input_scalar_field(gui, "image load max threads", "Maximum image loading threads", &ImageResourceDisplay::IMAGE_LOAD_THREAD_COUNT_MAX, 1, 10000); text_label_light(gui, "Cache related settings"); input_scalar_field(gui, "cache node resolution", "Cache node resolution", &DrawingProgramCache::CACHE_NODE_RESOLUTION, 256, 8192); input_scalar_field(gui, "max cache nodes", "Maximum cached nodes", &DrawingProgramCache::MAXIMUM_DRAW_CACHE_SURFACES, 2, 10000); size_t cacheVRAMConsumptionInMB = ( DrawingProgramCache::MAXIMUM_DRAW_CACHE_SURFACES // Number of surfaces * DrawingProgramCache::CACHE_NODE_RESOLUTION * DrawingProgramCache::CACHE_NODE_RESOLUTION // Number of pixels per cache surface * 4) // 4 Channels per pixel (RGBA) / (1024 * 1024); // Bytes -> Megabytes conversion text_label_light(gui, "Cache max VRAM consumption (MB): " + std::to_string(cacheVRAMConsumptionInMB)); input_scalar_field(gui, "max components in node", "Maximum components in single node", &DrawingProgramCache::MAXIMUM_COMPONENTS_IN_SINGLE_NODE, 2, 10000); input_scalar_field(gui, "components to force cache rebuild", "Number of components to force cache rebuild", &DrawingProgramCache::MINIMUM_COMPONENTS_TO_START_REBUILD, 1, 1000000); input_scalar_field(gui, "maximum frame time to force cache rebuild", "Maximum frame time to force cache rebuild (ms)", &DrawingProgramCache::MILLISECOND_FRAME_TIME_TO_FORCE_CACHE_REFRESH, 1, 1000000); input_scalar_field(gui, "minimum time to force cache rebuild", "Minimum time to check cache rebuild (ms)", &DrawingProgramCache::MILLISECOND_MINIMUM_TIME_TO_CHECK_FORCE_REFRESH, 1, 1000000); }); break; } } text_button_wide("done menu", "Done", [&] { main.save_config(); main.g.load_theme(main.conf.configPath, main.conf.themeCurrentlyLoaded); themeData.selectedThemeIndex = std::nullopt; main.keybindWaiting = std::nullopt; optionsMenuOpen = false; }); } } } void Toolbar::about_menu_inner_gui() { auto& gui = main.g.gui; auto& io = gui.io; left_to_right_line_layout(gui, [&]() { gui.new_id("Menu Selector", [&] { CLAY_AUTO_ID({ .layout = { .sizing = {.width = CLAY_SIZING_FIT(200), .height = CLAY_SIZING_FIT(0) }, .padding = CLAY_PADDING_ALL(io.theme->padding1), .childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_TOP}, .layoutDirection = CLAY_TOP_TO_BOTTOM } }) { gui.clipping_element("About Menu Selector Scroll Area", ScrollArea::Options{ .scrollVertical = true, .clipVertical = true, .scrollbarY = ScrollArea::ScrollbarType::NORMAL, .innerContent = [&](const ScrollArea::InnerContentParameters&) { CLAY_AUTO_ID({ .layout = { .sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIT(0)} } }) { text_button(gui, "infinipaintnoticebutton", "InfiniPaint", { .drawType = SelectableButton::DrawType::TRANSPARENT_ALL, .isSelected = selectedLicense == -1, .wide = true, .centered = false, .onClick = [&] { selectedLicense = -1; } }); } text_label_light_centered(gui, "Third Party Components"); for(int i = 0; i < static_cast(main.conf.thirdPartyLicenses.size()); i++) { CLAY_AUTO_ID({ .layout = { .sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIT(0) }, .padding = CLAY_PADDING_ALL(1) } }) { gui.new_id(i, [&] { text_button(gui, "noticebutton", main.conf.thirdPartyLicenses[i].first, { .drawType = SelectableButton::DrawType::TRANSPARENT_ALL, .isSelected = selectedLicense == i, .wide = true, .centered = false, .onClick = [&, i] { selectedLicense = i; } }); }); } } } }); } }); gui.clipping_element("About Menu Text Scroll Area", ScrollArea::Options{ .scrollVertical = true, .clipVertical = true, .scrollbarY = ScrollArea::ScrollbarType::NORMAL, .innerContent = [&](const ScrollArea::InnerContentParameters&) { text_label_size(gui, (selectedLicense == -1) ? main.conf.ownLicenseText : main.conf.thirdPartyLicenses[selectedLicense].second, 0.8f); } }); }); text_button_wide("done", "Done", [&] { optionsMenuOpen = false; }); } void Toolbar::reload_theme_list() { themeData.selectedThemeIndex = 0; themeData.themeDirList.clear(); themeData.themeDirList.emplace_back("Default"); std::filesystem::path themeDir = main.conf.configPath / "themes"; if(std::filesystem::exists(themeDir) && std::filesystem::is_directory(themeDir)) { for(auto& theme : std::filesystem::recursive_directory_iterator(themeDir)) { std::string name = theme.path().stem().string(); if(name != "Default") { themeData.themeDirList.emplace_back(name); if(name == main.conf.themeCurrentlyLoaded) themeData.selectedThemeIndex = themeData.themeDirList.size() - 1; } } } if(!main.g.load_theme(main.conf.configPath, main.conf.themeCurrentlyLoaded)) themeData.selectedThemeIndex = 0; themeData.openedSaveAsMenu = false; } void Toolbar::file_picker_gui_refresh_entries() { auto& gui = main.g.gui; filePicker.entries.clear(); for(;;) { try { for(const std::filesystem::path& entry : std::filesystem::directory_iterator(main.conf.currentSearchPath)) filePicker.entries.emplace_back(entry); break; } catch(const std::exception& e) { Logger::get().log(Logger::LogType::INFO, e.what()); if(main.conf.currentSearchPath == main.homePath) // The home path must exist. If we get errors on the home path, we have a real problem throw e; main.conf.currentSearchPath = main.homePath; } } std::vector extensionList = split_string_by_token(filePicker.extensionFilters[filePicker.extensionSelected], ";"); for(std::string& s : extensionList) { std::transform(s.begin(), s.end(), s.begin(), ::tolower); s.insert(0, "."); } std::erase_if(filePicker.entries, [&](const std::filesystem::path& a) { if(extensionList.empty()) return false; if(extensionList[0] == ".*") return false; std::string fExtension; if(a.has_extension()) { fExtension = a.extension().string(); std::transform(fExtension.begin(), fExtension.end(), fExtension.begin(), ::tolower); } return std::filesystem::is_regular_file(a) && (fExtension.empty() || !std::ranges::contains(extensionList, fExtension)); }); std::sort(filePicker.entries.begin(), filePicker.entries.end(), [&](const std::filesystem::path& a, const std::filesystem::path& b) { bool aDir = std::filesystem::is_directory(a); bool bDir = std::filesystem::is_directory(b); const std::string& aStr = a.string(); const std::string& bStr = b.string(); if(aDir && !bDir) return true; if(!aDir && bDir) return false; return std::lexicographical_compare(aStr.begin(), aStr.end(), bStr.begin(), bStr.end()); }); if(filePicker.entriesScrollArea) filePicker.entriesScrollArea->reset_scroll(); gui.set_to_layout(); } void Toolbar::file_picker_gui_done() { if(!filePicker.fileName.empty()) { std::filesystem::path pathToRet = main.conf.currentSearchPath / filePicker.fileName; if(filePicker.isSaving) pathToRet = force_extension_on_path(pathToRet, filePicker.extensionFiltersComplete[filePicker.extensionSelected].extensions); filePicker.postSelectionFunc(pathToRet, filePicker.extensionFiltersComplete[filePicker.extensionSelected]); } filePicker.entriesScrollArea = nullptr; filePicker.isOpen = false; } void Toolbar::file_picker_gui() { auto& gui = main.g.gui; auto& io = gui.io; center_obstructing_window_gui("file picker gui window", CLAY_SIZING_FIXED(700), CLAY_SIZING_FIXED(500), [&] { text_label_centered(gui, filePicker.filePickerWindowName); left_to_right_line_layout(gui, [&]() { svg_icon_button(gui, "file picker back button", "data/icons/backarrow.svg", { .onClick = [&] { main.conf.currentSearchPath = main.conf.currentSearchPath.parent_path(); file_picker_gui_refresh_entries(); } }); input_path_field(gui, "file picker path", "Path", &main.conf.currentSearchPath, { .fileTypeRestriction = std::filesystem::file_type::directory, .onEdit = [&] { file_picker_gui_refresh_entries(); } }); }); CLAY_AUTO_ID({ .layout = { .sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_GROW(0)}, .childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_TOP}, .layoutDirection = CLAY_TOP_TO_BOTTOM }, .backgroundColor = convert_vec4(io.theme->backColor2) }) { constexpr float entryHeight = 25.0f; filePicker.entriesScrollArea = gui.element("file picker entries", ManyElementScrollArea::Options{ .entryHeight = entryHeight, .entryCount = filePicker.entries.size(), .clipHorizontal = false, .elementContent = [&] (size_t i) { const std::filesystem::path& entry = filePicker.entries[i]; bool selectedEntry = filePicker.currentSelectedPath == entry; gui.element("elem", [&] (LayoutElement*, const Clay_ElementId& lId) { CLAY(lId, { .layout = { .sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIXED(entryHeight)}, .childGap = 1, .childAlignment = { .x = CLAY_ALIGN_X_LEFT, .y = CLAY_ALIGN_Y_CENTER}, .layoutDirection = CLAY_LEFT_TO_RIGHT }, .backgroundColor = selectedEntry ? convert_vec4(io.theme->backColor1) : convert_vec4(io.theme->backColor2) }) { CLAY_AUTO_ID({ .layout = { .sizing = {.width = CLAY_SIZING_FIXED(20), .height = CLAY_SIZING_FIXED(20)} }, }) { if(std::filesystem::is_directory(entry)) gui.element("folder icon", "data/icons/folder.svg", selectedEntry); else gui.element("file icon", "data/icons/file.svg", selectedEntry); } text_label(gui, entry.filename().string()); } }, LayoutElement::Callbacks { .onClick = [&, selectedEntry, entry](LayoutElement* l, const InputManager::MouseButtonCallbackArgs& button) { if(l->mouseHovering && button.button == InputManager::MouseButton::LEFT && button.down) { gui.set_post_callback_func([&, button, selectedEntry, entry] { if(selectedEntry && button.clicks >= 2) { if(std::filesystem::is_directory(entry)) { main.conf.currentSearchPath = entry; file_picker_gui_refresh_entries(); } else if(std::filesystem::is_regular_file(entry)) file_picker_gui_done(); } else { filePicker.currentSelectedPath = entry; if(std::filesystem::is_regular_file(entry)) filePicker.fileName = entry.filename().string(); } }); gui.set_to_layout(); } } }); } })->scrollArea; } left_to_right_line_layout(gui, [&]() { input_text(gui, "filepicker filename", &filePicker.fileName); gui.element>("filepicker select type", &filePicker.extensionSelected, filePicker.extensionFilters, DropdownOptions{ .onClick = [&] { file_picker_gui_refresh_entries(); } }); }); left_to_right_line_layout(gui, [&]() { text_button_wide("filepicker done", "Done", [&] { file_picker_gui_done(); }); text_button_wide("filepicker cancel", "Cancel", [&] { filePicker.isOpen = false; }); }); }); }