infinipaint/src/Toolbar.cpp

1995 lines
99 KiB
C++
Raw Normal View History

/*
* 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 <https://www.gnu.org/licenses/>.
*/
2025-07-06 20:43:53 +03:00
#include "Toolbar.hpp"
2026-04-05 20:26:03 +03:00
#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"
2025-07-06 20:43:53 +03:00
#include "Helpers/ConvertVec.hpp"
2025-10-01 20:08:01 +03:00
#include "Helpers/FileDownloader.hpp"
#include "Helpers/MathExtras.hpp"
2025-07-06 20:43:53 +03:00
#include "Helpers/Networking/NetLibrary.hpp"
2025-12-10 13:06:45 +03:00
#include "Helpers/NetworkingObjects/NetObjGenericSerializedClass.hpp"
2025-07-06 20:43:53 +03:00
#include "MainProgram.hpp"
#include "InputManager.hpp"
#include "ResourceDisplay/ImageResourceDisplay.hpp"
2026-04-09 16:30:28 +03:00
#include "RichText/TextStyleModifier.hpp"
2025-11-16 16:39:12 +03:00
#include "VersionConstants.hpp"
2025-07-06 20:43:53 +03:00
#include "World.hpp"
#include <SDL3/SDL_dialog.h>
2026-02-24 17:21:41 +03:00
#include <SDL3/SDL_filesystem.h>
2025-11-20 12:38:28 +03:00
#include <SDL3/SDL_render.h>
2025-07-06 20:43:53 +03:00
#include <algorithm>
#include <filesystem>
#include <optional>
#include <Helpers/Logger.hpp>
#include <Helpers/StringHelpers.hpp>
2025-10-01 20:08:01 +03:00
#include <Helpers/VersionNumber.hpp>
#include <modules/skparagraph/src/ParagraphBuilderImpl.h>
#include <modules/skparagraph/include/ParagraphStyle.h>
#include <modules/skparagraph/include/FontCollection.h>
#include <modules/skparagraph/include/TextStyle.h>
2025-10-16 09:38:58 +03:00
#include <include/core/SkFontStyle.h>
#include <modules/skunicode/include/SkUnicode_icu.h>
#include <modules/svg/include/SkSVGNode.h>
#include <include/core/SkStream.h>
#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"
2026-04-05 20:26:03 +03:00
#include "GUIStuff/Elements/MovableTabList.hpp"
2026-04-09 16:30:28 +03:00
#include "GUIStuff/Elements/TextParagraph.hpp"
#include "Screens/DesktopDrawingProgramScreen.hpp"
2025-10-01 20:08:01 +03:00
#define UPDATE_DOWNLOAD_URL "https://infinipaint.com/download.html"
#define UPDATE_NOTIFICATION_URL "https://infinipaint.com/updateNotificationVersion.txt"
2025-07-06 20:43:53 +03:00
#ifdef __EMSCRIPTEN__
#include <EmscriptenHelpers/emscripten_browser_file.h>
#endif
using namespace GUIStuff;
using namespace ElementHelpers;
Toolbar::Toolbar(MainProgram& initMain, DesktopDrawingProgramScreen& initDrawScreen):
main(initMain),
drawScreen(initDrawScreen)
2026-04-12 12:36:00 +03:00
{}
2025-07-06 20:43:53 +03:00
void Toolbar::open_file_selector(const std::string& filePickerName, const std::vector<Screen::ExtensionFilter>& extensionFilters, Screen::OpenFileSelectorCallback postSelectionFunc, const std::string& fileName, bool isSaving) {
drawScreen.open_file_selector(filePickerName, extensionFilters, postSelectionFunc, fileName, isSaving);
2025-07-06 20:43:53 +03:00
}
void Toolbar::open_file_selector_non_native(const std::string& filePickerName, const std::vector<Screen::ExtensionFilter>& 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();
2025-07-06 20:43:53 +03:00
}
void Toolbar::color_button_left(const char* id, Vector4f* color, const ColorSelectorButtonData& colorSelectorData) {
2026-04-02 18:23:36 +03:00
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,
});
2026-04-02 18:23:36 +03:00
}
});
}
void Toolbar::color_button_right(const char* id, Vector4f* color, const ColorSelectorButtonData& colorSelectorData) {
2026-04-02 18:23:36 +03:00
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,
});
2026-04-02 18:23:36 +03:00
}
});
}
void Toolbar::color_selector_left(Element* button, Vector4f* color, const ColorSelectorData& colorSelectorData) {
2026-04-02 18:23:36 +03:00
if(colorLeft != color) {
colorLeft = color;
colorLeftData = colorSelectorData;
colorLeftButton = button;
2026-04-02 18:23:36 +03:00
}
else
colorLeft = nullptr;
2026-04-02 18:23:36 +03:00
main.g.gui.set_to_layout();
2025-07-06 20:43:53 +03:00
}
void Toolbar::color_selector_right(Element* button, Vector4f* color, const ColorSelectorData& colorSelectorData) {
2026-04-02 18:23:36 +03:00
if(colorRight != color) {
colorRight = color;
colorRightData = colorSelectorData;
colorRightButton = button;
2026-04-02 18:23:36 +03:00
}
else
colorRight = nullptr;
2026-04-02 18:23:36 +03:00
main.g.gui.set_to_layout();
2025-07-06 20:43:53 +03:00
}
2026-04-05 22:01:30 +03:00
void Toolbar::update() {
std::erase_if(main.logMessages, [&](auto& logM) {
logM.time.update_time_since();
2026-04-12 12:36:00 +03:00
if(logM.time > UserLogMessage::FADE_START_TIME) {
2026-04-05 22:01:30 +03:00
main.g.gui.set_to_layout();
2026-04-12 12:36:00 +03:00
return logM.time >= UserLogMessage::DISPLAY_TIME;
2026-04-05 22:01:30 +03:00
}
return false;
});
2026-04-09 16:30:28 +03:00
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();
}
2026-04-05 22:01:30 +03:00
}
void Toolbar::layout_run() {
auto& gui = main.g.gui;
auto& io = gui.io;
2025-10-01 20:08:01 +03:00
#ifndef __EMSCRIPTEN__
update_notification_check();
#endif
2026-04-12 12:36:00 +03:00
if(drawGui) {
CLAY_AUTO_ID({
2025-07-06 20:43:53 +03:00
.layout = {
.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_GROW(0)},
.padding = CLAY_PADDING_ALL(io.theme->padding1),
.childGap = io.theme->childGap1,
2025-07-06 20:43:53 +03:00
.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();
2026-01-18 13:39:49 +03:00
if(!closePopupData.worldsToClose.empty())
close_popup_gui();
if(main.conf.viewWebVersionWelcome)
2025-07-06 20:43:53 +03:00
web_version_welcome();
2025-10-01 20:08:01 +03:00
#ifndef __EMSCRIPTEN__
else if(updateCheckerData.showGui)
update_notification_gui();
#endif
2025-07-06 20:43:53 +03:00
else if(filePicker.isOpen)
file_picker_gui();
else if(optionsMenuOpen)
options_menu();
else if(main.world->clientStillConnecting)
2026-05-13 14:31:35 +03:00
center_message("Connecting to server message", "Connecting to server...");
2025-07-06 20:43:53 +03:00
else if(playerMenuOpen)
player_list();
else if(!main.world->drawProg.layerMan.is_a_layer_being_edited())
2026-05-13 14:31:35 +03:00
center_message("Select layer to edit message", "Select a layer to edit");
2025-07-06 20:43:53 +03:00
}
if(!main.world->clientStillConnecting)
chat_box();
2025-07-06 20:43:53 +03:00
}
else {
if(!closePopupData.worldsToClose.empty()) // Should still show close popup if gui is disabled
close_popup_gui();
}
2025-07-06 20:43:53 +03:00
2026-04-05 15:02:26 +03:00
if(!main.world->clientStillConnecting)
2026-04-12 12:36:00 +03:00
main.world->drawProg.right_click_popup_gui(*this);
2025-07-06 20:43:53 +03:00
}
2026-01-18 13:39:49 +03:00
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;
2026-04-07 20:33:48 +03:00
main.g.gui.set_to_layout();
2026-01-18 13:39:49 +03:00
return closePopupData.worldsToClose.empty();
}
void Toolbar::add_world_to_close_popup_data(const std::shared_ptr<World>& 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);
2026-04-05 20:26:03 +03:00
main.g.gui.set_to_layout();
2026-01-18 13:39:49 +03:00
}
void Toolbar::close_popup_gui() {
auto& gui = main.g.gui;
auto& io = gui.io;
2026-01-18 13:39:49 +03:00
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<ScrollArea>("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
2026-01-18 13:39:49 +03:00
}
}) {
size_t i = 0;
2026-03-30 18:51:14 +03:00
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<Clay_Color>(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());
}
});
2026-03-30 18:51:14 +03:00
}
++i;
}
2026-03-30 18:51:14 +03:00
}
}
});
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);
}
2026-04-05 20:26:03 +03:00
main.set_tab_to_close(wLock.get());
2026-03-30 18:51:14 +03:00
}
closePopupData.worldsToClose.clear();
if(closePopupData.closeAppWhenDone)
main.setToQuit = true;
}
});
text_button(gui, "Discard All", "Discard All", {
.wide = true,
.onClick = [&] {
2026-04-05 20:26:03 +03:00
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;
}
});
});
2026-01-18 13:39:49 +03:00
}
2025-07-06 20:43:53 +03:00
void Toolbar::save_func() {
if(main.world->filePath == std::filesystem::path())
save_as_func();
2025-07-28 18:21:09 +03:00
else
2026-01-18 13:39:49 +03:00
main.world->save_to_file(main.world->filePath);
2025-07-06 20:43:53 +03:00
}
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) {
2025-07-06 20:43:53 +03:00
auto world = w.lock();
if(world)
world->save_to_file(p);
}, "", true);
#endif
}
2025-12-01 17:33:14 +03:00
void Toolbar::paint_popup(Vector2f popupPos) {
2026-04-02 12:46:16 +03:00
using namespace GUIStuff;
auto& gui = main.g.gui;
2026-04-02 12:46:16 +03:00
std::shared_ptr<double> newRotationAngle = std::make_shared<double>(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(),
2026-04-12 12:36:00 +03:00
.palette = main.conf.palettes[paletteData.selectedPalette].colors,
2026-04-02 12:46:16 +03:00
.onRotate = [&, newRotationAngle] {
main.world->drawData.cam.c.rotate_about(main.world->drawData.cam.c.from_space(main.window.size.cast<float>() * 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();
2026-04-02 12:46:16 +03:00
}
});
2025-12-01 17:33:14 +03:00
});
2025-07-06 20:43:53 +03:00
}
void Toolbar::top_toolbar() {
auto& gui = main.g.gui;
auto& io = gui.io;
gui.element<LayoutElement>("top menu bar", [&] (LayoutElement*, const Clay_ElementId& lId) {
2026-03-31 20:52:46 +03:00
CLAY(lId, {
.layout = {
.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIT(0) },
.padding = CLAY_PADDING_ALL(static_cast<uint16_t>(io.theme->padding1 / 2)),
.childGap = static_cast<uint16_t>(io.theme->childGap1 / 2),
.childAlignment = { .x = CLAY_ALIGN_X_LEFT, .y = CLAY_ALIGN_Y_CENTER},
.layoutDirection = CLAY_LEFT_TO_RIGHT
},
.backgroundColor = convert_vec4<Clay_Color>(io.theme->backColor1),
.cornerRadius = CLAY_CORNER_RADIUS(io.theme->windowCorners1)
}) {
2026-03-30 18:51:14 +03:00
global_log();
2026-03-30 18:51:14 +03:00
auto icon_button_top_toolbar = [&](const char* id, const std::string& svgPath, bool isSelected, const std::function<void()>& onClick) {
return svg_icon_button(gui, id, svgPath, {
2026-03-30 19:41:18 +03:00
.drawType = SelectableButton::DrawType::TRANSPARENT_ALL,
2026-03-30 18:51:14 +03:00
.isSelected = isSelected,
.onClick = onClick
});
2026-03-30 18:51:14 +03:00
};
Element* mainMenuButton = icon_button_top_toolbar("Main Menu Button", "data/icons/menu.svg", menuPopUpOpen, [&] {
menuPopUpOpen = !menuPopUpOpen;
});
2026-03-30 19:41:18 +03:00
2026-04-05 20:26:03 +03:00
std::vector<MovableTabListData::IconNamePair> 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 ? "*" : ""));
}
2026-03-30 19:41:18 +03:00
2026-04-05 20:26:03 +03:00
gui.element<MovableTabList>("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());
2026-03-30 19:41:18 +03:00
}
2026-04-05 20:26:03 +03:00
});
2026-03-30 18:51:14 +03:00
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;
});
}
2026-03-30 18:51:14 +03:00
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();
});
2026-05-13 14:31:35 +03:00
Element* gridMenuButton = icon_button_top_toolbar("Grids Button", "data/icons/grid.svg", gridMenuPopupOpen, [&] {
if(gridMenuPopupOpen)
2026-03-30 18:51:14 +03:00
stop_displaying_grid_menu();
else
2026-05-13 14:31:35 +03:00
gridMenuPopupOpen = true;
2026-03-30 18:51:14 +03:00
});
Element* layerMenuButton = icon_button_top_toolbar("Layer Menu Button", "data/icons/layer.svg", layerMenuPopupOpen, [&] {
if(layerMenuPopupOpen)
stop_displaying_layer_menu();
else
2026-03-30 18:51:14 +03:00
layerMenuPopupOpen = true;
});
2026-04-06 20:16:25 +03:00
Element* bookmarkMenuButton = icon_button_top_toolbar("Bookmark Menu Button", "data/icons/bookmark.svg", bookmarkMenuPopupOpen, [&] {
if(bookmarkMenuPopupOpen)
stop_displaying_bookmark_menu();
else
2026-03-30 18:51:14 +03:00
bookmarkMenuPopupOpen = true;
});
2025-07-06 20:43:53 +03:00
2026-05-13 14:31:35 +03:00
if(gridMenuPopupOpen)
grid_menu(gridMenuButton);
2026-03-30 18:51:14 +03:00
if(bookmarkMenuPopupOpen)
2026-04-06 20:16:25 +03:00
bookmark_menu(bookmarkMenuButton);
2026-03-30 18:51:14 +03:00
if(layerMenuPopupOpen)
layer_menu(layerMenuButton);
2026-03-30 18:51:14 +03:00
}
if(menuPopUpOpen) {
2026-04-02 18:23:36 +03:00
gui.set_z_index(5, [&] {
gui.element<LayoutElement>("main menu popup", [&] (LayoutElement*, const Clay_ElementId& id) {
2026-04-02 18:23:36 +03:00
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<Clay_Color>(io.theme->backColor1),
.cornerRadius = CLAY_CORNER_RADIUS(io.theme->windowCorners1),
.floating = {.offset = {.x = 0, .y = static_cast<float>(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<void()>& 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<CustomEvents::OpenInfiniPaintFileEvent>({
2026-04-02 18:23:36 +03:00
.isClient = false
2026-04-05 20:26:03 +03:00
});
2026-03-30 18:51:14 +03:00
});
2026-04-02 18:23:36 +03:00
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<CustomEvents::AddFileToCanvasEvent>({
.type = CustomEvents::AddFileToCanvasEvent::Type::BUFFER,
.name = fileName,
.buffer = std::string(buffer)
});
2026-04-02 18:23:36 +03:00
}
}, &main);
2026-04-02 18:23:36 +03:00
#else
open_file_selector("Open File", {{"Any File", "*"}}, [&](const std::filesystem::path& p, const auto& e) {
CustomEvents::emit_event<CustomEvents::AddFileToCanvasEvent>({
.type = CustomEvents::AddFileToCanvasEvent::Type::PATH,
.filePath = p,
.pos = main.window.size.cast<float>() / 2.0f
});
2026-04-02 18:23:36 +03:00
});
#endif
});
if(main.world->netObjMan.is_connected()) {
menu_popup_text_button("lobby info", "Lobby Info", [&] {
optionsMenuOpen = true;
optionsMenuType = LOBBY_INFO_MENU;
2026-03-30 18:51:14 +03:00
});
2026-04-02 18:23:36 +03:00
}
menu_popup_text_button("start hosting", "Host", [&] {
serverLocalID = NetLibrary::get_random_server_local_id();
serverToConnectTo = NetLibrary::get_global_id() + serverLocalID;
2026-03-30 18:51:14 +03:00
optionsMenuOpen = true;
2026-04-02 18:23:36 +03:00
optionsMenuType = HOST_MENU;
});
menu_popup_text_button("canvas specific settings", "Canvas Settings", [&] {
optionsMenuOpen = true;
optionsMenuType = CANVAS_SETTINGS_MENU;
});
2026-03-30 18:51:14 +03:00
}
2026-04-02 18:23:36 +03:00
menu_popup_text_button("start connecting", "Connect", [&] {
serverToConnectTo.clear();
optionsMenuOpen = true;
2026-04-02 18:23:36 +03:00
optionsMenuType = CONNECT_MENU;
2026-03-30 18:51:14 +03:00
});
2026-04-02 18:23:36 +03:00
menu_popup_text_button("open options", "Settings", [&] {
2026-03-30 18:51:14 +03:00
optionsMenuOpen = true;
2026-04-02 18:23:36 +03:00
optionsMenuType = GENERAL_SETTINGS_MENU;
});
2026-04-02 18:23:36 +03:00
menu_popup_text_button("about menu button", "About", [&] {
optionsMenuOpen = true;
optionsMenuType = ABOUT_MENU;
2026-03-30 18:51:14 +03:00
});
2026-04-02 18:23:36 +03:00
#ifndef __EMSCRIPTEN__
menu_popup_text_button("quit button", "Quit", [&] {
if(main.app_close_requested())
main.setToQuit = true;
});
#endif
2026-03-30 18:51:14 +03:00
}
2026-04-02 18:23:36 +03:00
}, LayoutElement::Callbacks {
.onClick = [&, mainMenuButton](LayoutElement* l, const InputManager::MouseButtonCallbackArgs& button) {
if(!l->mouseHovering && button.down && !mainMenuButton->mouseHovering) {
2026-04-02 18:23:36 +03:00
menuPopUpOpen = false;
gui.set_to_layout();
}
}
});
2026-03-30 18:51:14 +03:00
});
}
2026-03-31 20:52:46 +03:00
}
});
2025-07-06 20:43:53 +03:00
}
void Toolbar::web_version_welcome() {
auto& gui = main.g.gui;
auto& io = gui.io;
CLAY_AUTO_ID({
2025-07-06 20:43:53 +03:00
.layout = {
.sizing = {.width = CLAY_SIZING_FIXED(700), .height = CLAY_SIZING_FIT(0) },
.padding = CLAY_PADDING_ALL(io.theme->padding1),
.childGap = io.theme->childGap1,
2025-07-06 20:43:53 +03:00
.childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_TOP},
.layoutDirection = CLAY_TOP_TO_BOTTOM
},
.backgroundColor = convert_vec4<Clay_Color>(io.theme->backColor1),
.cornerRadius = CLAY_CORNER_RADIUS(io.theme->windowCorners1),
2025-11-01 20:47:34 +03:00
.floating = {.attachPoints = {.element = CLAY_ATTACH_POINT_CENTER_CENTER, .parent = CLAY_ATTACH_POINT_CENTER_CENTER}, .attachTo = CLAY_ATTACH_TO_PARENT}
2025-07-06 20:43:53 +03:00
}) {
2026-03-30 18:51:14 +03:00
gui.new_id("web version welcome gui", [&] {
text_label_centered(gui, "Welcome to the web version of InfiniPaint!");
text_label(gui,
2025-07-06 20:43:53 +03:00
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
2025-07-06 20:43:53 +03:00
If you like this app, consider downloading the native version for your system)");
2026-03-30 18:51:14 +03:00
text_button(gui, "got it", "Got It", {
.wide = true,
.onClick = [&] {
main.conf.viewWebVersionWelcome = false;
2026-03-30 18:51:14 +03:00
}
});
});
2025-07-06 20:43:53 +03:00
}
}
2026-05-13 14:31:35 +03:00
void Toolbar::center_message(const char* id, const std::string& m) {
auto& gui = main.g.gui;
auto& io = gui.io;
2026-05-13 14:31:35 +03:00
gui.set_z_index(gui.get_z_index() + 1, [&] {
gui.element<LayoutElement>(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<Clay_Color>(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);
}
});
});
}
2025-10-01 20:08:01 +03:00
#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<VersionNumber> newVersion = version_str_to_version_numbers(updateCheckerData.versionFile->str);
2025-11-16 16:39:12 +03:00
std::optional<VersionNumber> 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("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("INFO", "Current version is up to date");
else
Logger::get().log("INFO", "Local version has larger version number than the latest online one");
}
2025-10-01 20:08:01 +03:00
else
Logger::get().log("INFO", "Update notification file couldn't be converted to version numbers");
updateCheckerData.versionFile = nullptr;
break;
2025-10-01 20:08:01 +03:00
}
case FileDownloader::DownloadData::Status::FAILURE:
Logger::get().log("INFO", "Failed to check for updates");
updateCheckerData.updateCheckDone = true;
updateCheckerData.versionFile = nullptr;
break;
2025-10-01 20:08:01 +03:00
}
}
}
else
updateCheckerData.updateCheckDone = true;
2025-10-01 20:08:01 +03:00
}
}
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), [&] {
2026-03-30 18:51:14 +03:00
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;
2026-03-30 18:51:14 +03:00
updateCheckerData.showGui = false;
}
});
text_button(gui, "ignore for now", "Ignore for now", {
.wide = true,
.onClick = [&]{
updateCheckerData.showGui = false;
}
});
});
});
2025-10-01 20:08:01 +03:00
}
#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<LayoutElement>("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<Clay_Color>(io.theme->backColor1),
.cornerRadius = CLAY_CORNER_RADIUS(io.theme->windowCorners1),
2026-04-06 20:16:25 +03:00
.floating = {.offset = {.x = 0, .y = static_cast<float>(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");
2026-05-13 14:31:35 +03:00
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();
}
});
2026-03-30 18:51:14 +03:00
});
2025-09-06 19:06:30 +03:00
}
}
2025-09-12 09:54:49 +03:00
void Toolbar::stop_displaying_grid_menu() {
2026-05-13 14:31:35 +03:00
gridMenuPopupOpen = false;
main.world->gridMan.refresh_gui_data();
main.g.gui.set_to_layout();
2025-09-12 09:54:49 +03:00
}
2026-04-06 20:16:25 +03:00
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();
}
2026-04-06 20:16:25 +03:00
void Toolbar::bookmark_menu(Element* bookmarkMenuButton) {
auto& gui = main.g.gui;
auto& io = gui.io;
2026-04-06 20:16:25 +03:00
gui.set_z_index(gui.get_z_index() + 1, [&] {
gui.element<LayoutElement>("bookmark menu", [&] (LayoutElement*, const Clay_ElementId& lId) {
2026-04-06 20:16:25 +03:00
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<Clay_Color>(io.theme->backColor1),
.cornerRadius = CLAY_CORNER_RADIUS(io.theme->windowCorners1),
.floating = {.offset = {.x = 0, .y = static_cast<float>(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();
2026-04-06 20:16:25 +03:00
}
}, LayoutElement::Callbacks {
.onClick = [&, bookmarkMenuButton] (LayoutElement* l, const InputManager::MouseButtonCallbackArgs& button) {
2026-04-06 20:16:25 +03:00
if(!l->mouseHovering && !l->childMouseHovering && !bookmarkMenuButton->mouseHovering && button.down)
stop_displaying_bookmark_menu();
}
});
2026-03-30 18:51:14 +03:00
});
2025-07-06 20:43:53 +03:00
}
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<LayoutElement>("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<Clay_Color>(io.theme->backColor1),
.cornerRadius = CLAY_CORNER_RADIUS(io.theme->windowCorners1),
.floating = {.offset = {.x = 0, .y = static_cast<float>(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();
}
});
2026-03-30 18:51:14 +03:00
});
2025-12-25 17:18:28 +03:00
}
2026-04-09 16:30:28 +03:00
RichText::TextData Toolbar::build_paragraph_from_chat_message(const ChatMessage& message, float alpha) {
auto& gui = main.g.gui;
auto& io = gui.io;
2026-04-09 16:30:28 +03:00
RichText::TextData toRet;
auto& par = toRet.paragraphs.emplace_back();
2025-10-16 09:38:58 +03:00
2026-04-09 16:30:28 +03:00
RichText::PositionedTextStyleMod& positionedModInit = toRet.tStyleMods.emplace_back();
positionedModInit.pos = {0, 0};
positionedModInit.mods[RichText::TextStyleModifier::ModifierType::WEIGHT] = std::make_shared<RichText::WeightTextStyleModifier>(SkFontStyle::Weight::kBold_Weight);
positionedModInit.mods[RichText::TextStyleModifier::ModifierType::COLOR] = std::make_shared<RichText::ColorTextStyleModifier>(convert_vec4<Vector4f>(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<RichText::WeightTextStyleModifier>(SkFontStyle::Weight::kNormal_Weight);
par.text += message.message;
2025-10-16 09:38:58 +03:00
2026-04-09 16:30:28 +03:00
return toRet;
2025-10-16 09:38:58 +03:00
}
2025-07-06 20:43:53 +03:00
void Toolbar::chat_box() {
2026-04-09 16:30:28 +03:00
auto& gui = main.g.gui;
auto& io = gui.io;
constexpr float CHATBOX_WIDTH = 700;
if(main.world->netObjMan.is_connected()) {
gui.element<LayoutElement>("Infinipaint chat box open button", [&](LayoutElement*, const Clay_ElementId& lId) {
CLAY(lId, {
.layout = {
.layoutDirection = CLAY_LEFT_TO_RIGHT
},
.floating = {.offset = {static_cast<float>(io.theme->padding1), -static_cast<float>(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<LayoutElement>("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<float>(io.theme->padding1), -static_cast<float>(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<Clay_Color>(io.theme->backColor1)
}) {
int id = 0;
for(auto& chatMessage : main.world->chatMessages | std::views::reverse) {
gui.new_id(id++, [&] {
gui.element<TextParagraph>("text", TextParagraph::Data{
.text = build_paragraph_from_chat_message(chatMessage, 1.0f),
.maxGrowX = CHATBOX_WIDTH,
.ellipsis = false
2026-04-09 16:30:28 +03:00
});
});
}
}
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<float>(chatMessage.time, ChatMessage::DISPLAY_TIME, ChatMessage::FADE_START_TIME);
gui.element<LayoutElement>("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<Clay_Color>(color_mul_alpha(io.theme->backColor1, a)),
}) {
gui.element<TextParagraph>("message", TextParagraph::Data{
.text = build_paragraph_from_chat_message(chatMessage, a),
.maxGrowX = CHATBOX_WIDTH,
.ellipsis = false
2026-04-09 16:30:28 +03:00
});
}
});
});
}
}
});
}
}
});
2025-07-06 20:43:53 +03:00
}
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();
2026-04-12 12:36:00 +03:00
if(logM.time < UserLogMessage::DISPLAY_TIME) {
float a = 1.0f - lerp_time<float>(logM.time, UserLogMessage::DISPLAY_TIME, UserLogMessage::FADE_START_TIME);
gui.new_id(i, [&] {
gui.element<LayoutElement>("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<Clay_Color>(color_mul_alpha(io.theme->backColor1, a)),
.cornerRadius = CLAY_CORNER_RADIUS(io.theme->windowCorners1)
}) {
SkColor4f c{0, 0, 0, 0};
switch(logM.color) {
2026-04-12 12:36:00 +03:00
case UserLogMessage::COLOR_NORMAL:
c = io.theme->frontColor1;
break;
2026-04-12 12:36:00 +03:00
case UserLogMessage::COLOR_ERROR:
c = io.theme->errorColor;
break;
}
text_label_color(gui, logM.text, color_mul_alpha(c, a));
2026-04-05 22:01:30 +03:00
}
});
2026-04-05 22:01:30 +03:00
});
}
else
break;
2026-04-05 22:01:30 +03:00
}
}
});
2025-07-06 20:43:53 +03:00
}
void Toolbar::drawing_program_gui() {
auto& gui = main.g.gui;
auto& io = gui.io;
CLAY_AUTO_ID({
2025-07-06 20:43:53 +03:00
.layout = {
.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_GROW(0) },
.childGap = io.theme->childGap1,
2025-07-06 20:43:53 +03:00
.childAlignment = { .x = CLAY_ALIGN_X_LEFT, .y = CLAY_ALIGN_Y_CENTER},
.layoutDirection = CLAY_LEFT_TO_RIGHT
},
}) {
2026-04-12 12:36:00 +03:00
main.world->drawProg.toolbar_gui(*this);
2026-04-02 18:23:36 +03:00
2026-05-12 15:19:43 +03:00
colorLeft = main.world->drawProg.color_picker_color(colorLeft);
2026-04-02 18:23:36 +03:00
if(colorLeft)
color_picker_window("Drawing program gui color picker left", &colorLeft, colorLeftButton, colorLeftData);
CLAY_AUTO_ID({
2025-07-06 20:43:53 +03:00
.layout = {
.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_GROW(0)}
}
}) {}
2026-05-12 15:19:43 +03:00
colorRight = main.world->drawProg.color_picker_color(colorRight);
2026-04-02 18:23:36 +03:00
if(colorRight)
color_picker_window("Drawing program gui color picker right", &colorRight, colorRightButton, colorRightData);
2026-04-02 18:23:36 +03:00
2026-04-12 12:36:00 +03:00
main.world->drawProg.tool_options_gui(*this);
2025-07-06 20:43:53 +03:00
}
}
void Toolbar::color_picker_window(const char* id, Vector4f** color, GUIStuff::Element* b, const ColorSelectorData& colorSelectorData) {
2026-04-02 18:23:36 +03:00
auto& gui = main.g.gui;
CLAY_AUTO_ID({
.layout = {
.padding = {.top = 40, .bottom = 40}
}
}) {
main.g.gui.element<LayoutElement>(id, [&](LayoutElement*, const Clay_ElementId& lId) {
2026-04-02 18:23:36 +03:00
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,
2026-05-03 11:36:32 +03:00
.childAlignment = { .x = CLAY_ALIGN_X_LEFT, .y = CLAY_ALIGN_Y_TOP},
2026-04-02 18:23:36 +03:00
.layoutDirection = CLAY_TOP_TO_BOTTOM
},
.backgroundColor = convert_vec4<Clay_Color>(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();
2026-04-02 18:23:36 +03:00
});
}
}, LayoutElement::Callbacks{
.onClick = [&, b, color](LayoutElement* l, const InputManager::MouseButtonCallbackArgs& button) {
if(!l->mouseHovering && !l->childMouseHovering && !b->mouseHovering && button.down) {
*color = nullptr;
2026-04-02 18:23:36 +03:00
main.g.gui.set_to_layout();
}
}
});
}
}
2026-04-03 09:52:45 +03:00
void Toolbar::color_palette(const char* id, Vector4f* color, const std::function<void()>& onChange) {
auto& gui = main.g.gui;
auto& io = gui.io;
2026-03-30 18:51:14 +03:00
gui.new_id(id, [&] {
2026-04-12 12:36:00 +03:00
auto& palette = main.conf.palettes[paletteData.selectedPalette].colors;
gui.clipping_element<ScrollArea>("color palette scroll area", ScrollArea::Options{
2026-04-03 09:52:45 +03:00
.scrollVertical = true,
.clipVertical = true,
.scrollbarY = ScrollArea::ScrollbarType::NORMAL,
2026-04-03 09:52:45 +03:00
.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<Vector3f>(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;
}
}
}
}
}
});
2026-04-03 09:52:45 +03:00
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()});
}
});
}
}
2026-04-03 09:52:45 +03:00
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<std::string> paletteNames;
2026-04-12 12:36:00 +03:00
for(auto& p : main.conf.palettes)
2026-04-03 09:52:45 +03:00
paletteNames.emplace_back(p.name);
gui.element<DropDown<size_t>>("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 = [&] {
2026-04-12 12:36:00 +03:00
main.conf.palettes.erase(main.conf.palettes.begin() + paletteData.selectedPalette);
2026-04-03 09:52:45 +03:00
paletteData.selectedPalette = 0;
}
});
}
if(paletteData.addingPalette) {
input_text_field(gui, "paletteinputname", "Name", &paletteData.newPaletteStr);
text_button_wide("addpalettebutton", "Create", [&] {
if(!paletteData.newPaletteStr.empty()) {
2026-04-12 12:36:00 +03:00
main.conf.palettes.emplace_back();
main.conf.palettes.back().name = paletteData.newPaletteStr;
paletteData.selectedPalette = main.conf.palettes.size() - 1;
2026-04-03 09:52:45 +03:00
paletteData.addingPalette = false;
}
});
}
2026-03-30 18:51:14 +03:00
});
}
2025-07-06 20:43:53 +03:00
void Toolbar::player_list() {
auto& gui = main.g.gui;
2026-04-05 20:26:03 +03:00
center_obstructing_window_gui("player client list", CLAY_SIZING_FIT(500), CLAY_SIZING_FIT(0), [&] {
2026-03-30 18:51:14 +03:00
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<Clay_Color>(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<Clay_Color>(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());
}});
});
});
}
}
}
2026-03-30 18:51:14 +03:00
text_button(gui, "close list", "Done", { .wide = true, .onClick = [&] {
playerMenuOpen = false;
}});
});
2026-04-05 20:26:03 +03:00
});
2025-07-06 20:43:53 +03:00
}
2026-01-01 22:46:02 +03:00
void Toolbar::open_world_file(bool isClient, const std::string& netSource, const std::string& serverLocalID2) {
2025-07-06 20:43:53 +03:00
#ifdef __EMSCRIPTEN__
static struct UploadData {
2026-01-01 22:46:02 +03:00
bool iC;
2025-07-06 20:43:53 +03:00
std::string nS;
std::string sLID;
MainProgram* main;
} uploadData;
2026-01-01 22:46:02 +03:00
uploadData.iC = isClient;
2025-07-06 20:43:53 +03:00
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;
2026-04-12 17:31:32 +03:00
CustomEvents::emit_event<CustomEvents::OpenInfiniPaintFileEvent>({
2026-01-01 22:46:02 +03:00
.isClient = uD->iC,
.filePathSource = std::filesystem::path(fileName),
2025-07-06 20:43:53 +03:00
.netSource = uD->nS,
.serverLocalID = uD->sLID,
.fileDataBuffer = buffer
2026-04-12 17:31:32 +03:00
});
2025-07-06 20:43:53 +03:00
}
}, &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<CustomEvents::OpenInfiniPaintFileEvent>({
2026-01-01 22:46:02 +03:00
.isClient = isClient,
.filePathSource = p,
2025-07-06 20:43:53 +03:00
.netSource = netSource,
.serverLocalID = serverLocalID2
});
});
#endif
}
void Toolbar::center_obstructing_window_gui(const char* id, Clay_SizingAxis x, Clay_SizingAxis y, const std::function<void()>& innerContent) {
auto& gui = main.g.gui;
auto& io = gui.io;
2026-04-02 18:23:36 +03:00
gui.set_z_index(100, [&] {
gui.element<LayoutElement>(id, [&] (LayoutElement*, const Clay_ElementId& id) {
2026-04-02 18:23:36 +03:00
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<Clay_Color>(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<void()>& onClick) {
auto& gui = main.g.gui;
text_button(gui, id, str, {
.wide = true,
.onClick = onClick
});
}
2025-07-06 20:43:53 +03:00
void Toolbar::options_menu() {
auto& gui = main.g.gui;
2025-07-06 20:43:53 +03:00
switch(optionsMenuType) {
case HOST_MENU: {
center_obstructing_window_gui("host menu", CLAY_SIZING_FIT(650), CLAY_SIZING_FIT(0), [&] {
2026-05-13 19:56:14 +03:00
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;
}
2025-07-06 20:43:53 +03:00
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("USERINFO", "Connect issue: Incorrect address length");
else if(serverToConnectTo.substr(0, NetLibrary::GLOBALID_LEN) == NetLibrary::get_global_id())
Logger::get().log("USERINFO", "Connect issue: Can't connect to your own address");
else {
CustomEvents::emit_event<CustomEvents::OpenInfiniPaintFileEvent>({
.isClient = true,
.netSource = serverToConnectTo
2026-04-05 20:26:03 +03:00
});
2025-07-06 20:43:53 +03:00
optionsMenuOpen = false;
}
});
text_button_wide("cancel", "Cancel", [&] {
optionsMenuOpen = false;
2025-07-06 20:43:53 +03:00
});
});
});
2025-07-06 20:43:53 +03:00
break;
}
case GENERAL_SETTINGS_MENU: {
center_obstructing_window_gui("gsettings", CLAY_SIZING_FIXED(600), CLAY_SIZING_FIXED(500), [&] {
general_settings_inner_gui();
});
2025-07-06 20:43:53 +03:00
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);
2025-07-06 20:43:53 +03:00
});
text_button_wide("done", "Done", [&] {
optionsMenuOpen = false;
});
});
});
2025-07-06 20:43:53 +03:00
break;
}
2025-08-01 10:58:45 +03:00
case CANVAS_SETTINGS_MENU: {
center_obstructing_window_gui("canvas settings menu", CLAY_SIZING_FIT(500), CLAY_SIZING_FIT(0), [&] {
auto newColorToSet = std::make_shared<SkColor4f>(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<Vector3f>(*newColorToSet));
}
});
text_button_wide("done", "Done", [&] {
2025-08-01 10:58:45 +03:00
optionsMenuOpen = false;
});
});
2025-08-01 10:58:45 +03:00
break;
}
2025-07-06 20:43:53 +03:00
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", [&] {
2025-07-28 18:21:09 +03:00
main.world->save_to_file(downloadNameSet);
2025-07-06 20:43:53 +03:00
optionsMenuOpen = false;
});
text_button_wide("cancel", "Cancel", [&] {
2025-07-06 20:43:53 +03:00
optionsMenuOpen = false;
});
2025-07-06 20:43:53 +03:00
});
});
2025-07-06 20:43:53 +03:00
break;
}
case ABOUT_MENU: {
2026-03-31 21:21:49 +03:00
center_obstructing_window_gui("about menu", CLAY_SIZING_FIXED(650), CLAY_SIZING_FIXED(500), [&] {
about_menu_inner_gui();
});
2025-07-06 20:43:53 +03:00
break;
}
}
}
void Toolbar::general_settings_inner_gui() {
auto& gui = main.g.gui;
auto& io = gui.io;
CLAY_AUTO_ID({
2025-07-06 20:43:53 +03:00
.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
}
2025-07-06 20:43:53 +03:00
}) {
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,
2026-03-31 20:52:46 +03:00
.centered = false,
.onClick = [&, opt] {
2026-04-12 12:36:00 +03:00
main.g.load_theme(main.conf.configPath, main.conf.themeCurrentlyLoaded);
themeData.selectedThemeIndex = std::nullopt;
generalSettingsOptions = opt;
2026-04-02 19:18:39 +03:00
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
}
}) {
2026-04-01 10:18:38 +03:00
auto general_scroll_area = [&](const char* id, const std::function<void()>& innerContent) {
gui.clipping_element<ScrollArea>("general settings scroll area", ScrollArea::Options{
2026-04-01 10:18:38 +03:00
.scrollVertical = true,
.clipVertical = true,
.scrollbarY = ScrollArea::ScrollbarType::NORMAL,
2026-04-01 10:18:38 +03:00
.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();
}
2026-04-01 10:18:38 +03:00
}
});
};
switch(generalSettingsOptions) {
case GSETTINGS_GENERAL: {
general_scroll_area("general settings", [&] {
2026-05-14 11:53:36 +03:00
input_text_field(gui, "display name input", "Display name", &main.conf.displayName, {
.onEdit = [&] {
main.update_display_names();
}
});
2026-04-01 10:18:38 +03:00
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(); }
});
2026-04-01 10:18:38 +03:00
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);
});
2026-04-01 10:18:38 +03:00
#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<uint8_t>(gui, "middle click", "Middle click pen button", &main.conf.tabletOptions.middleClickButton, 1, 255);
input_scalar_field<uint8_t>(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});
2026-04-01 10:18:38 +03:00
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
2026-04-11 11:17:26 +03:00
checkbox_boolean_field(gui, "mouse ignore when pen proximity", "Ignore mouse movement when pen in proximity", &main.conf.tabletOptions.ignoreMouseMovementWhenPenInProximity);
2026-04-01 10:18:38 +03:00
#endif
2026-05-15 09:51:25 +03:00
checkbox_boolean_field(gui, "disable touch when pen in proximity", "Disable touch when pen in proximity", &main.conf.tabletOptions.disableTouchWhenPenInProximity);
2026-04-01 10:18:38 +03:00
});
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,
2026-04-01 10:18:38 +03:00
}
}) {
text_label(gui, "Theme: ");
gui.element<DropDown<size_t>>("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()];
2026-04-12 12:36:00 +03:00
main.g.save_theme(main.conf.configPath, main.conf.themeCurrentlyLoaded);
2026-04-01 10:18:38 +03:00
reload_theme_list();
2026-03-30 18:51:14 +03:00
});
}
2026-04-01 10:18:38 +03:00
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", [&] {
2026-04-12 12:36:00 +03:00
try { std::filesystem::remove(main.conf.configPath / "themes" / (themeData.themeDirList[themeData.selectedThemeIndex.value()] + ".json")); } catch(...) { }
2026-04-01 10:18:38 +03:00
main.conf.themeCurrentlyLoaded = "Default";
reload_theme_list();
2026-03-30 18:51:14 +03:00
});
}
2026-04-01 10:18:38 +03:00
});
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", [&] {
2026-04-12 12:36:00 +03:00
main.g.save_theme(main.conf.configPath, main.conf.themeCurrentlyLoaded);
2026-04-01 10:18:38 +03:00
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<SkColor4f>(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<uint16_t>(gui, "childGap1", "Gap between child elements", &io.theme->childGap1, 0, 30);
input_scalar_field<uint16_t>(gui, "padding1", "Window padding", &io.theme->padding1, 0, 30);
slider_scalar_field<float>(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
2026-03-30 18:51:14 +03:00
}
2026-04-01 10:18:38 +03:00
}) {
text_label(gui, std::string(nlohmann::json(static_cast<InputManager::KeyCodeEnum>(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, {
2026-04-02 19:18:39 +03:00
.isSelected = main.keybindWaiting.has_value() && main.keybindWaiting.value() == i,
2026-04-01 10:18:38 +03:00
.wide = true,
2026-04-02 19:18:39 +03:00
.onClick = [&, i] {
main.keybindWaiting = i;
2026-03-30 18:51:14 +03:00
}
});
}
2026-04-01 10:18:38 +03:00
});
}
2026-04-01 10:18:38 +03:00
});
break;
2025-07-06 20:43:53 +03:00
}
2026-04-01 10:18:38 +03:00
case GSETTINGS_DEBUG: {
general_scroll_area("debug settings menu", [&] {
2026-05-14 20:21:45 +03:00
checkbox_boolean_field(gui, "use mobile UI", "Use mobile UI (requires restart)", &main.conf.mobileUI);
2026-04-01 10:18:38 +03:00
input_scalars_field(gui, "jump transition easing", "Jump easing", &main.conf.jumpTransitionEasing, 4, -10.0f, 10.0f, { .decimalPrecision = 2 });
input_scalar_field<int>(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<size_t>(gui, "cache node resolution", "Cache node resolution", &DrawingProgramCache::CACHE_NODE_RESOLUTION, 256, 8192);
input_scalar_field<size_t>(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<size_t>(gui, "max components in node", "Maximum components in single node", &DrawingProgramCache::MAXIMUM_COMPONENTS_IN_SINGLE_NODE, 2, 10000);
input_scalar_field<size_t>(gui, "components to force cache rebuild", "Number of components to force cache rebuild", &DrawingProgramCache::MINIMUM_COMPONENTS_TO_START_REBUILD, 1, 1000000);
input_scalar_field<size_t>(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<size_t>(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();
2026-04-12 12:36:00 +03:00
main.g.load_theme(main.conf.configPath, main.conf.themeCurrentlyLoaded);
themeData.selectedThemeIndex = std::nullopt;
2026-04-02 19:18:39 +03:00
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, [&]() {
2026-03-30 18:51:14 +03:00
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),
2026-03-30 18:51:14 +03:00
.childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_TOP},
.layoutDirection = CLAY_TOP_TO_BOTTOM
}
}) {
gui.clipping_element<ScrollArea>("About Menu Selector Scroll Area", ScrollArea::Options{
2026-03-31 21:21:49 +03:00
.scrollVertical = true,
.clipVertical = true,
.scrollbarY = ScrollArea::ScrollbarType::NORMAL,
2026-03-30 18:51:14 +03:00
.innerContent = [&](const ScrollArea::InnerContentParameters&) {
CLAY_AUTO_ID({
2025-07-06 20:43:53 +03:00
.layout = {
2026-03-30 18:51:14 +03:00
.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIT(0)}
2025-07-06 20:43:53 +03:00
}
}) {
2026-03-30 18:51:14 +03:00
text_button(gui, "infinipaintnoticebutton", "InfiniPaint", {
.drawType = SelectableButton::DrawType::TRANSPARENT_ALL,
2026-03-30 18:51:14 +03:00
.isSelected = selectedLicense == -1,
.wide = true,
2026-03-31 21:21:49 +03:00
.centered = false,
2026-03-30 18:51:14 +03:00
.onClick = [&] { selectedLicense = -1; }
});
2025-07-06 20:43:53 +03:00
}
2026-03-30 18:51:14 +03:00
text_label_light_centered(gui, "Third Party Components");
2026-04-12 12:36:00 +03:00
for(int i = 0; i < static_cast<int>(main.conf.thirdPartyLicenses.size()); i++) {
2026-03-30 18:51:14 +03:00
CLAY_AUTO_ID({
.layout = {
.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIT(0) },
.padding = CLAY_PADDING_ALL(1)
}
}) {
gui.new_id(i, [&] {
2026-04-12 12:36:00 +03:00
text_button(gui, "noticebutton", main.conf.thirdPartyLicenses[i].first, {
2026-03-30 18:51:14 +03:00
.drawType = SelectableButton::DrawType::TRANSPARENT_ALL,
.isSelected = selectedLicense == i,
.wide = true,
2026-03-31 21:21:49 +03:00
.centered = false,
2026-03-30 18:51:14 +03:00
.onClick = [&, i] { selectedLicense = i; }
});
});
}
}
2025-07-06 20:43:53 +03:00
}
2026-03-30 18:51:14 +03:00
});
}
});
gui.clipping_element<ScrollArea>("About Menu Text Scroll Area", ScrollArea::Options{
.scrollVertical = true,
.clipVertical = true,
.scrollbarY = ScrollArea::ScrollbarType::NORMAL,
.innerContent = [&](const ScrollArea::InnerContentParameters&) {
2026-04-12 12:36:00 +03:00
text_label_size(gui, (selectedLicense == -1) ? main.conf.ownLicenseText : main.conf.thirdPartyLicenses[selectedLicense].second, 0.8f);
}
});
});
text_button_wide("done", "Done", [&] {
optionsMenuOpen = false;
});
2025-07-06 20:43:53 +03:00
}
void Toolbar::reload_theme_list() {
themeData.selectedThemeIndex = 0;
themeData.themeDirList.clear();
themeData.themeDirList.emplace_back("Default");
2026-04-12 12:36:00 +03:00
std::filesystem::path themeDir = main.conf.configPath / "themes";
2025-07-06 20:43:53 +03:00
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)
2025-07-06 20:43:53 +03:00
themeData.selectedThemeIndex = themeData.themeDirList.size() - 1;
}
}
}
2026-04-12 12:36:00 +03:00
if(!main.g.load_theme(main.conf.configPath, main.conf.themeCurrentlyLoaded))
2025-07-06 20:43:53 +03:00
themeData.selectedThemeIndex = 0;
themeData.openedSaveAsMenu = false;
}
void Toolbar::file_picker_gui_refresh_entries() {
auto& gui = main.g.gui;
filePicker.entries.clear();
for(;;) {
try {
2026-04-12 12:36:00 +03:00
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("INFO", e.what());
2026-04-12 12:36:00 +03:00
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;
2026-04-12 12:36:00 +03:00
main.conf.currentSearchPath = main.homePath;
}
}
std::vector<std::string> 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());
});
2026-04-01 22:28:48 +03:00
if(filePicker.entriesScrollArea)
filePicker.entriesScrollArea->reset_scroll();
gui.set_to_layout();
}
void Toolbar::file_picker_gui_done() {
if(!filePicker.fileName.empty()) {
2026-04-12 12:36:00 +03:00
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]);
}
2026-04-01 22:28:48 +03:00
filePicker.entriesScrollArea = nullptr;
filePicker.isOpen = false;
}
2025-07-06 20:43:53 +03:00
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 = [&] {
2026-04-12 12:36:00 +03:00
main.conf.currentSearchPath = main.conf.currentSearchPath.parent_path();
file_picker_gui_refresh_entries();
}
});
2026-04-12 12:36:00 +03:00
input_path_field(gui, "file picker path", "Path", &main.conf.currentSearchPath, {
2026-04-09 16:30:28 +03:00
.fileTypeRestriction = std::filesystem::file_type::directory,
.onEdit = [&] {
file_picker_gui_refresh_entries();
}
});
2025-07-06 20:43:53 +03:00
});
CLAY_AUTO_ID({
2025-07-06 20:43:53 +03:00
.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<Clay_Color>(io.theme->backColor2)
2025-07-06 20:43:53 +03:00
}) {
constexpr float entryHeight = 25.0f;
filePicker.entriesScrollArea = gui.element<ManyElementScrollArea>("file picker entries", ManyElementScrollArea::Options{
.entryHeight = entryHeight,
.entryCount = filePicker.entries.size(),
2026-03-31 21:21:49 +03:00
.clipHorizontal = false,
.elementContent = [&] (size_t i) {
const std::filesystem::path& entry = filePicker.entries[i];
bool selectedEntry = filePicker.currentSelectedPath == entry;
gui.element<LayoutElement>("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<Clay_Color>(io.theme->backColor1) : convert_vec4<Clay_Color>(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<SVGIcon>("folder icon", "data/icons/folder.svg", selectedEntry);
else
gui.element<SVGIcon>("file icon", "data/icons/file.svg", selectedEntry);
2025-07-06 20:43:53 +03:00
}
text_label(gui, entry.filename().string());
2025-07-06 20:43:53 +03:00
}
}, 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)) {
2026-04-12 12:36:00 +03:00
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();
}
2025-07-06 20:43:53 +03:00
}
});
2025-07-06 20:43:53 +03:00
}
})->scrollArea;
2025-07-06 20:43:53 +03:00
}
left_to_right_line_layout(gui, [&]() {
input_text(gui, "filepicker filename", &filePicker.fileName);
gui.element<DropDown<size_t>>("filepicker select type", &filePicker.extensionSelected, filePicker.extensionFilters, DropdownOptions{
.onClick = [&] { file_picker_gui_refresh_entries(); }
});
2025-07-06 20:43:53 +03:00
});
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; });
2025-07-06 20:43:53 +03:00
});
});
2026-03-04 12:15:24 +03:00
}