#include "park_editor_internal.h" #include namespace dingosdk::overlay { using namespace park_editor_detail; namespace { using theme::keycap; // A tile like the menu's tabs: blue with black text when selected. bool mode_tile(ImDrawList *draw, ImFont *font, const char *label, bool on, float k, unsigned seed) { const float text_size = 14 * k; const auto extent = font->CalcTextSizeA(text_size, FLT_MAX, 0, label); const ImVec2 size{extent.x + 28 * k, ImGui::GetFrameHeight()}; const auto at = ImGui::GetCursorScreenPos(); ImGui::PushID(label); const bool pressed = ImGui::InvisibleButton("##mode", size); const bool hovered = ImGui::IsItemHovered(); ImGui::PopID(); skate_theme::rough_rect(draw, at, {at.x + size.x, at.y + size.y}, on ? blue : hovered ? skate_theme::tile_light : skate_theme::tile_grey, seed, k); draw->AddText(font, text_size, {at.x + 14 * k, at.y + (size.y - extent.y) * .5f}, on ? skate_theme::black : paper, label); return pressed; } void leave(ParkEditorUI &ui, const CallbacksV3 &callbacks, bool &visible, bool play) { if (ui.exit_pending || !callbacks.queue_debug) return; std::array result{}; if (callbacks.queue_debug(callbacks.user, {DebugAction::set_park_editor, false}, result.data(), result.size())) { ui.exit_pending = true; ui.exit_at = ImGui::GetTime(); visible = !play; ui.flying = false; ui.dragging = -1; ui.placing.clear(); } ui.feedback = result.data(); ui.feedback_until = ImGui::GetTime() + 5; } } // namespace void set_park_surface_queue(ParkSurfaceQueue queue) noexcept { surface_queue.store(queue); } void set_park_preview_queue(ParkPreviewQueue queue) noexcept { preview_queue.store(queue); } void set_park_selection_queue(ParkSelectionQueue queue) noexcept { selection_queue.store(queue); } void set_park_paste_queue(ParkPasteQueue queue) noexcept { paste_queue.store(queue); } bool draw_park_editor(ParkEditorUI &ui, const Model &model, const CallbacksV3 &callbacks, bool console_open, bool exit_requested, bool &visible) { const auto &state = model.editor; auto &io = ImGui::GetIO(); // Esc cancels a drag or placement in progress; with nothing in progress it // closes the editor and returns to the game. An open dialog closes itself. bool escape_closes = false; if (!console_open && !io.WantTextInput && ImGui::IsKeyPressed(ImGuiKey_Escape, false) && !ImGui::IsPopupOpen(nullptr, ImGuiPopupFlags_AnyPopupId | ImGuiPopupFlags_AnyPopupLevel)) { const bool in_progress = ui.dragging >= 0 || !ui.placing.empty() || ui.preview_token || ui.surface_confirm || ui.inspector_dirty; if (in_progress) { cancel_drag(ui, model); ui.placing.clear(); ui.dragging = -1; ui.inspector_dirty = false; ui.draft_revision = 0; ui.surface_confirm = false; } else escape_closes = true; } if (ui.generation != state.generation) { ui.generation = state.generation; ui.selected = 0; ui.selection.clear(); ui.paste_expected.clear(); ui.paste_before.clear(); ui.preview_token = 0; ui.pick_confirm = ui.asset_drag = false; ui.dragging = -1; ui.placing.clear(); ui.pending = false; ui.inspector_dirty = false; ui.initialized = false; ui.surface_confirm = false; ui.surface_request = {}; } if (!ui.initialized) { ui.plane_height = model.debug.skater_position_valid ? model.debug.skater_position[1] : model.debug.camera_position[1] - 2; ui.initialized = true; } if (ui.exit_pending && ImGui::GetTime() > ui.exit_at + 3) { ui.exit_pending = false; ui.feedback = model.debug.status; ui.feedback_until = ImGui::GetTime() + 5; } if (ui.pending && !state.busy && (state.revision != ui.queued_revision || state.status != ui.queued_status || ImGui::GetTime() > ui.queued_at + 12)) ui.pending = false; if (!ui.pending && !state.busy && !ui.paste_expected.empty()) select_pasted(ui, state); if (ui.preview_token && (state.failed || (!state.busy && state.revision > ui.preview_revision))) { ui.preview_token = 0; ui.dragging = -1; ui.placing.clear(); ui.free_release = ui.surface_confirm = false; refresh_selection(ui, state); } if ((ui.selected || !ui.selection.empty()) && !ui.inspector_dirty && ui.dragging < 0 && !ui.pending && ui.draft_revision != state.revision) refresh_selection(ui, state); if (exit_requested || escape_closes) { if (state.busy || ui.pending || ui.preview_token) { ui.feedback = "Wait for the current edit before closing the editor."; ui.feedback_until = ImGui::GetTime() + 5; } else leave(ui, callbacks, visible, escape_closes); // Esc goes straight back to the game } namespace skate = dingosdk::skate_theme; const float k = std::clamp(ui.ui_scale, .5f, 3.0f); const auto P = [k](float value) { return value * k; }; const auto restore_font_scale = io.FontGlobalScale; io.FontGlobalScale = k; const auto *main = ImGui::GetMainViewport(); const auto origin = main->Pos, size = main->Size; const float left = std::clamp(size.x * .25f, P(250), P(400)), right = std::clamp(size.x * .23f, P(250), P(330)), top = P(104), bottom = P(40), gap = P(10); ImGui::SetNextWindowPos(origin); ImGui::SetNextWindowSize(size); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, {0, 0}); ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0); ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 0); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, {P(9), P(6)}); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, {P(8), P(6)}); ImGui::PushStyleVar(ImGuiStyleVar_PopupRounding, 0); ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarRounding, 0); ImGui::PushStyleVar(ImGuiStyleVar_GrabRounding, 0); const int colours = skate::push_widget_colours(); const auto flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoBringToFrontOnFocus; ImGui::Begin("ReSkate Park Editor", nullptr, flags); auto *frame = ImGui::GetWindowDrawList(); ImFont *title_font = font_or(ui.title_font), *bold_font = font_or(ui.bold_font); // ---- header band: brushed title, map, and the Test / Exit actions frame->AddRectFilled(origin, {origin.x + size.x, origin.y + top}, IM_COL32(10, 10, 11, 245)); { const int start = frame->VtxBuffer.Size; const ImVec2 at{origin.x + P(20), origin.y + P(8)}; frame->AddText(title_font, P(40), {at.x + P(2), at.y + P(3)}, IM_COL32(0, 0, 0, 160), "PARK EDITOR"); frame->AddText(title_font, P(40), at, paper, "PARK EDITOR"); const auto extent = title_font->CalcTextSizeA(P(40), FLT_MAX, 0, "PARK EDITOR"); skate::rotate_since(frame, start, -3.0f, {at.x + extent.x * .5f, at.y + extent.y * .5f}); frame->AddText(ImGui::GetFont(), P(14), {at.x + extent.x + P(18), at.y + P(20)}, muted, state.map.empty() ? "Build your own park." : state.map.c_str()); } const bool busy = state.busy || state.failed || ui.pending || ui.preview_token || ui.exit_pending || !state.available; ImGui::PushFont(bold_font); { const ImVec2 test{P(130), P(34)}, exit{P(96), P(34)}; ImGui::SetCursorPos({size.x - P(20) - exit.x - P(8) - test.x, P(14)}); ImGui::BeginDisabled(state.busy || ui.pending || ui.preview_token); skate::push_primary_button(); if (ImGui::Button("TEST PARK", test)) leave(ui, callbacks, visible, true); skate::pop_primary_button(); ImGui::SameLine(0, P(8)); if (ImGui::Button("EXIT", exit)) leave(ui, callbacks, visible, false); ImGui::EndDisabled(); } // ---- tool row: file, history, transform mode ImGui::SetCursorPos({P(20), P(62)}); ImGui::BeginDisabled(busy); const auto project = std::find_if(state.park_mods.begin(), state.park_mods.end(), [&](const auto &mod) { return mod.folder == state.project; }); const bool has_project = project != state.park_mods.end(); const auto project_label = "PROJECT: " + (has_project ? (project->title.empty() ? project->folder : project->title) : std::string("UNSAVED")) + "##project"; if (ImGui::Button(project_label.c_str())) { ui.project_selected = has_project ? project->folder : std::string(); ui.project_open = true; } if (ImGui::IsItemHovered()) ImGui::SetTooltip("Create, open or save a park mod."); ImGui::SameLine(); if (ImGui::Button("SAVE")) { if (has_project) send(ui, model, callbacks, "mod-save", project->folder); else { ui.project_selected.clear(); ui.project_open = true; } } ImGui::SameLine(0, P(20)); ImGui::BeginDisabled(!state.can_undo); if (ImGui::Button("UNDO")) send(ui, model, callbacks, "undo"); ImGui::EndDisabled(); ImGui::SameLine(); ImGui::BeginDisabled(!state.can_redo); if (ImGui::Button("REDO")) send(ui, model, callbacks, "redo"); ImGui::EndDisabled(); ImGui::SameLine(0, P(20)); const std::array, 3> modes{ {{"MOVE T", ParkTransformMode::move}, {"ROTATE R", ParkTransformMode::rotate}, {"SCALE Y", ParkTransformMode::scale}}}; for (std::size_t i = 0; i < modes.size(); ++i) { if (i) ImGui::SameLine(0, P(6)); if (mode_tile(frame, bold_font, modes[i].first, ui.mode == modes[i].second, k, static_cast(41 + i))) { ui.mode = modes[i].second; ui.dragging = -1; } } ImGui::PopFont(); ImGui::SameLine(0, P(14)); if (ui.selection.size() > 1) { ImGui::AlignTextToFramePadding(); ImGui::TextDisabled("World axes"); } else ImGui::Checkbox("Local axes", &ui.local_axes); ImGui::EndDisabled(); // ---- dialogs ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, {P(20), P(18)}); ImGui::PushStyleVar(ImGuiStyleVar_PopupBorderSize, 0); ImGui::PushStyleColor(ImGuiCol_PopupBg, IM_COL32(18, 18, 19, 252)); ImGui::PushStyleColor(ImGuiCol_ModalWindowDimBg, IM_COL32(4, 6, 9, 150)); if (ui.project_open) { ImGui::OpenPopup("Park project"); ui.project_open = false; } ImGui::SetNextWindowSize({std::min(P(860), size.x - P(40)), std::min(P(580), size.y - P(40))}); ImGui::SetNextWindowPos({origin.x + size.x * .5f, origin.y + size.y * .5f}, ImGuiCond_Always, {.5f, .5f}); if (ImGui::BeginPopupModal("Park project", nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove)) { if (ImGui::IsKeyPressed(ImGuiKey_Escape, false) && !io.WantTextInput) ImGui::CloseCurrentPopup(); auto *draw = ImGui::GetWindowDrawList(); const auto at = ImGui::GetWindowPos(); draw->AddRectFilled(at, {at.x + ImGui::GetWindowWidth(), at.y + P(3)}, blue); ImGui::PushFont(font_or(ui.heading_font)); ImGui::TextUnformatted("PARK PROJECT"); ImGui::PopFont(); ImGui::PushTextWrapPos(0); ImGui::TextDisabled("Parks are saved as mods in the Mods folder, so you can share them. Others install them " "with the launcher and load them from Build > Park mods."); ImGui::PopTextWrapPos(); ImGui::Spacing(); const auto *selected_mod = [&]() -> const EditorParkMod * { for (const auto &mod : state.park_mods) if (mod.folder == ui.project_selected) return &mod; return nullptr; }(); const bool creating = !selected_mod; // Fill the form from the selection once, then leave it to the user. const std::string fields_for = creating ? std::string("\n") : ui.project_selected; if (ui.project_fields_for != fields_for) { ui.project_fields_for = fields_for; const auto copy = [](auto &buffer, const std::string &text) { buffer.fill(0); std::copy_n(text.begin(), std::min(text.size(), buffer.size() - 1), buffer.begin()); }; copy(ui.mod_title, creating ? std::string() : selected_mod->title); copy(ui.mod_author, creating ? std::string() : selected_mod->author); copy(ui.mod_version, creating ? std::string("1.0") : selected_mod->version); copy(ui.mod_description, creating ? std::string() : selected_mod->description); } const float footer = ImGui::GetFrameHeightWithSpacing() + P(14); const float body = ImGui::GetContentRegionAvail().y - footer; const float list_width = P(300); // left: projects { const auto list_at = ImGui::GetCursorScreenPos(); skate::rough_rect(draw, list_at, {list_at.x + list_width, list_at.y + body}, skate::tile, 61, k); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, {P(10), P(10)}); ImGui::BeginChild("##projects", {list_width, body}, ImGuiChildFlags_AlwaysUseWindowPadding); ImGui::PushFont(bold_font); if (ImGui::Selectable("+ NEW PARK MOD", creating)) ui.project_selected.clear(); ImGui::PopFont(); ImGui::Spacing(); ImGui::TextDisabled("PARK MODS"); if (state.park_mods.empty()) ImGui::TextDisabled("None yet."); for (const auto &mod : state.park_mods) { ImGui::PushID(mod.folder.c_str()); const bool current = mod.folder == state.project; const auto label = (current ? "> " : "") + (mod.title.empty() ? mod.folder : mod.title); if (ImGui::Selectable(label.c_str(), ui.project_selected == mod.folder)) ui.project_selected = mod.folder; std::string detail = mod.author.empty() ? std::string() : "by " + mod.author + " / "; detail += mod.has_map ? "park for " + state.map : "no park for " + state.map + " yet"; if (!mod.enabled) detail += " / disabled"; ImGui::TextDisabled(" %s", detail.c_str()); ImGui::PopID(); } if (!state.legacy_parks.empty()) { ImGui::Spacing(); ImGui::TextDisabled("SAVED BEFORE PARK MODS"); for (const auto &name : state.legacy_parks) { ImGui::PushID(name.c_str()); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(name.c_str()); ImGui::SameLine(ImGui::GetContentRegionMax().x - P(76)); ImGui::BeginDisabled(busy); if (ImGui::Button("Convert", {P(76), 0})) send(ui, model, callbacks, "mod-convert", name); ImGui::EndDisabled(); if (ImGui::IsItemHovered()) ImGui::SetTooltip("Copy this park into a new park mod. The old file is kept."); ImGui::PopID(); } } ImGui::EndChild(); ImGui::PopStyleVar(); } // right: details and actions ImGui::SameLine(0, P(16)); ImGui::BeginChild("##project-details", {0, body}); ImGui::PushFont(font_or(ui.heading_font)); ImGui::TextUnformatted(creating ? "NEW PARK MOD" : (selected_mod->title.empty() ? selected_mod->folder : selected_mod->title).c_str()); ImGui::PopFont(); if (creating) ImGui::TextDisabled("Saves the current layout on %s into a new mod.", state.map.c_str()); else { std::string maps; for (const auto &map : selected_mod->maps) maps += (maps.empty() ? "" : ", ") + map; ImGui::TextDisabled("Mods\\%s / parks: %s", selected_mod->folder.c_str(), maps.empty() ? "none" : maps.c_str()); } ImGui::Spacing(); const auto field = [&](const char *label, auto &buffer, const char *hint) { ImGui::TextUnformatted(label); ImGui::SetNextItemWidth(-1); ImGui::InputTextWithHint((std::string("##") + label).c_str(), hint, buffer.data(), buffer.size()); }; field("Name", ui.mod_title, "My skatepark"); field("Author", ui.mod_author, "Your name"); field("Version", ui.mod_version, "1.0"); ImGui::TextUnformatted("Description"); ImGui::InputTextMultiline("##description", ui.mod_description.data(), ui.mod_description.size(), {-1, P(90)}); ImGui::Spacing(); std::string description = ui.mod_description.data(); std::replace(description.begin(), description.end(), '\n', ' '); const std::vector texts{ui.mod_title.data(), ui.mod_author.data(), ui.mod_version.data(), description}; const bool named = ui.mod_title[0] != 0; ImGui::BeginDisabled(busy); ImGui::PushFont(bold_font); if (creating) { ImGui::BeginDisabled(!named); skate::push_primary_button(); if (ImGui::Button("CREATE AND SAVE LAYOUT", {-1, P(38)}) && send(ui, model, callbacks, "mod-create", {}, texts)) ImGui::CloseCurrentPopup(); skate::pop_primary_button(); ImGui::EndDisabled(); } else { const bool can_open = selected_mod->has_map && state.can_load_parks; ImGui::BeginDisabled(!can_open); skate::push_primary_button(); if (ImGui::Button("OPEN IN EDITOR", {-1, P(38)}) && send(ui, model, callbacks, "mod-open", selected_mod->folder)) ImGui::CloseCurrentPopup(); skate::pop_primary_button(); ImGui::EndDisabled(); if (!can_open && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("%s", !state.can_load_parks ? "In multiplayer only the host can load park mods." : "This mod has no park for this map yet."); const float half = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * .5f; if (ImGui::Button("SAVE LAYOUT HERE", {half, 0}) && send(ui, model, callbacks, "mod-save", selected_mod->folder)) ImGui::CloseCurrentPopup(); if (ImGui::IsItemHovered()) ImGui::SetTooltip(selected_mod->has_map ? "Replace this mod's park for %s with the current layout." : "Add the current layout to this mod as its park for %s.", state.map.c_str()); ImGui::SameLine(); ImGui::BeginDisabled(!named); if (ImGui::Button("SAVE DETAILS", {-1, 0})) send(ui, model, callbacks, "mod-details", selected_mod->folder, texts); ImGui::EndDisabled(); } ImGui::PopFont(); ImGui::EndDisabled(); ImGui::EndChild(); // footer ImGui::Spacing(); ImGui::BeginDisabled(busy); if (ImGui::Button("Clear layout")) send(ui, model, callbacks, "new"); if (ImGui::IsItemHovered()) ImGui::SetTooltip("Remove every object and start an unsaved layout. Undo brings it back."); ImGui::SameLine(); if (ImGui::Button("Refresh")) send(ui, model, callbacks, "mods-refresh"); ImGui::EndDisabled(); ImGui::SameLine(ImGui::GetContentRegionMax().x - P(120)); if (ImGui::Button("Close", {P(120), 0})) ImGui::CloseCurrentPopup(); ImGui::EndPopup(); } ImGui::PopStyleColor(2); ImGui::PopStyleVar(2); // ---- side panels on rough-cut tiles const float content_height = std::max(P(100), size.y - top - bottom - gap * 2); const auto panel = [&](const char *id, float x, float width, unsigned seed, auto &&body) { const ImVec2 at{origin.x + x, origin.y + top + gap}; skate::rough_rect(frame, at, {at.x + width, at.y + content_height}, IM_COL32(20, 20, 21, 242), seed, k); ImGui::SetCursorPos({x, top + gap}); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, {P(14), P(12)}); ImGui::BeginChild(id, {width, content_height}, ImGuiChildFlags_AlwaysUseWindowPadding); body(); ImGui::EndChild(); ImGui::PopStyleVar(); }; panel("library-panel", gap, left - gap, 52, [&] { library(ui, model); }); panel("inspector-panel", size.x - right, right - gap, 53, [&] { inspector(ui, model, callbacks); }); ImGui::SetCursorPos({left, top}); ImGui::BeginChild("viewport-panel", {std::max(20.0f, size.x - left - right), size.y - top - bottom}); const bool blocked = console_open || ui.exit_pending || ImGui::IsPopupOpen(nullptr, ImGuiPopupFlags_AnyPopupId | ImGuiPopupFlags_AnyPopupLevel) || io.AppFocusLost; viewport(ui, model, origin, size, blocked); const auto highlight = blocked || ui.exit_pending ? std::vector{} : ui.selection; if (highlight != ui.highlighted || ImGui::GetTime() >= ui.selection_heartbeat) { if (const auto queue = selection_queue.load(); queue && queue({state.generation, highlight})) { ui.highlighted = highlight; ui.selection_heartbeat = ImGui::GetTime() + .25; } } ImGui::EndChild(); // ---- status bar: the latest message and keycap hints { const ImVec2 at{origin.x, origin.y + size.y - bottom}; frame->AddRectFilled(at, {at.x + size.x, at.y + bottom}, IM_COL32(10, 10, 11, 245)); frame->AddRectFilled(at, {at.x + P(4), at.y + bottom}, blue); const std::string status = ui.pending ? "Waiting for the game to apply the edit..." : !ui.feedback.empty() && ImGui::GetTime() < ui.feedback_until ? ui.feedback : state.status; const auto text = status.empty() ? std::string("Current layout autosaves. Save a named park to keep a reusable copy.") : status; // Hints on the right; the message is clipped to the space left of them. float hints = 0; const std::array, 4> keys{ {{"RMB", "Fly"}, {"WASD", "Move"}, {"CTRL Z", "Undo"}, {"ESC", "Close"}}}; std::array widths{}; for (std::size_t i = 0; i < keys.size(); ++i) { widths[i] = bold_font->CalcTextSizeA(P(12), FLT_MAX, 0, keys[i].first).x + bold_font->CalcTextSizeA(P(14), FLT_MAX, 0, keys[i].second).x + P(20); hints += widths[i] + P(18); } const bool show_hints = size.x - hints > P(360); float x = at.x + size.x - P(16) - (show_hints ? hints - P(18) : 0); if (show_hints) for (std::size_t i = 0; i < keys.size(); ++i) { keycap(frame, bold_font, k, {x, at.y + (bottom - P(20)) * .5f}, keys[i].first, keys[i].second); x += widths[i] + P(18); } const float message_right = at.x + size.x - P(16) - (show_hints ? hints + P(8) : 0); frame->PushClipRect({at.x + P(16), at.y}, {message_right, at.y + bottom}, true); frame->AddText(ImGui::GetFont(), P(15), {at.x + P(18), at.y + (bottom - P(15)) * .5f}, status.empty() ? muted : paper, text.c_str()); frame->PopClipRect(); ImGui::SetCursorPos({0, size.y - bottom}); ImGui::InvisibleButton("##status", {std::max(1.0f, message_right - origin.x), bottom}); if (!status.empty() && ImGui::IsItemHovered()) ImGui::SetTooltip("%s", status.c_str()); } if (!console_open && !io.WantTextInput && !ImGui::IsAnyItemActive() && !blocked) { if (!busy) { if (!io.KeyCtrl && !io.KeyAlt && !io.KeySuper) { if (ImGui::IsKeyPressed(ImGuiKey_T, false)) ui.mode = ParkTransformMode::move; if (ImGui::IsKeyPressed(ImGuiKey_R, false)) ui.mode = ParkTransformMode::rotate; if (ImGui::IsKeyPressed(ImGuiKey_Y, false)) ui.mode = ParkTransformMode::scale; } if (io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_C, false)) copy_selection(ui, state); if (io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_V, false)) paste_objects(ui, state); if (io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_S, false)) { if (!state.project.empty()) send(ui, model, callbacks, "mod-save", state.project); else { ui.project_selected.clear(); ui.project_open = true; } } if (io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_Z)) send(ui, model, callbacks, io.KeyShift ? "redo" : "undo"); if (io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_Y)) send(ui, model, callbacks, "redo"); if (ui.selected && ui.selection.size() <= 1 && ImGui::IsKeyPressed(ImGuiKey_Delete)) send(ui, model, callbacks, "delete"); } } ImGui::End(); ImGui::PopStyleColor(colours); ImGui::PopStyleVar(9); io.FontGlobalScale = restore_font_scale; return ui.flying && !blocked && !io.WantTextInput && ui.dragging < 0; } } // namespace dingosdk::overlay