#include "timeline.h" #include #include #include #include #include "log.h" #include "toast.h" #include "vector_.h" using namespace anm2ed::resource; using namespace anm2ed::types; using namespace anm2ed::util; using namespace glm; namespace anm2ed::imgui { constexpr auto COLOR_HIDDEN_MULTIPLIER = vec4(0.5f, 0.5f, 0.5f, 1.000f); constexpr auto FRAME_BORDER_COLOR_DARK = ImVec4(1.0f, 1.0f, 1.0f, 0.15f); constexpr auto FRAME_BORDER_COLOR_LIGHT = ImVec4(0.0f, 0.0f, 0.0f, 0.25f); constexpr auto FRAME_BORDER_COLOR_REFERENCED_DARK = ImVec4(1.0f, 1.0f, 1.0f, 0.50f); constexpr auto FRAME_BORDER_COLOR_REFERENCED_LIGHT = ImVec4(0.0f, 0.0f, 0.0f, 0.60f); constexpr auto FRAME_BORDER_THICKNESS = 1.0f; constexpr auto FRAME_BORDER_THICKNESS_REFERENCED = 2.0f; constexpr auto FRAME_MULTIPLE_OVERLAY_COLOR_DARK = ImVec4(1.0f, 1.0f, 1.0f, 0.05f); constexpr auto FRAME_MULTIPLE_OVERLAY_COLOR_LIGHT = ImVec4(0.0f, 0.0f, 0.0f, 0.1f); constexpr auto FRAME_ROUNDING = 3.0f; constexpr auto ICON_TINT_DEFAULT_DARK = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); constexpr auto ICON_TINT_DEFAULT_LIGHT = ImVec4(0.0f, 0.0f, 0.0f, 1.0f); constexpr auto ITEM_TEXT_COLOR_DARK = to_imvec4(color::WHITE); constexpr auto ITEM_TEXT_COLOR_LIGHT = ImVec4(0.0f, 0.0f, 0.0f, 1.0f); constexpr auto PLAYHEAD_ICON_TINT_LIGHT = ImVec4(0.3922f, 0.7843f, 0.5882f, 1.0f); constexpr auto PLAYHEAD_LINE_COLOR_DARK = to_imvec4(color::WHITE); constexpr auto PLAYHEAD_LINE_COLOR_LIGHT = ImVec4(0.1961f, 0.5882f, 0.3922f, 1.0f); constexpr auto PLAYHEAD_LINE_THICKNESS = 4.0f; constexpr auto TEXT_MULTIPLE_COLOR_DARK = to_imvec4(color::WHITE); constexpr auto TEXT_MULTIPLE_COLOR_LIGHT = ImVec4(0.0f, 0.0f, 0.0f, 1.0f); constexpr auto TIMELINE_BACKGROUND_COLOR_LIGHT = ImVec4(0.5490f, 0.5490f, 0.5882f, 1.0f); constexpr auto TIMELINE_PLAYHEAD_RECT_COLOR_DARK = ImVec4(0.60f, 0.45f, 0.30f, 1.0f); constexpr auto TIMELINE_PLAYHEAD_RECT_COLOR_LIGHT = ImVec4(0.8353f, 0.8353f, 0.7294f, 1.0f); constexpr auto TIMELINE_TICK_COLOR_LIGHT = ImVec4(0.0f, 0.0f, 0.0f, 1.0f); constexpr auto TIMELINE_CHILD_BG_COLOR_LIGHT = ImVec4(0.5490f, 0.5490f, 0.5882f, 1.0f); constexpr auto TIMELINE_TEXT_COLOR_LIGHT = ImVec4(0.0f, 0.0f, 0.0f, 1.0f); constexpr glm::vec4 FRAME_COLOR_LIGHT_BASE[] = {{0.80f, 0.80f, 0.80f, 1.0f}, {0.5216f, 0.7333f, 1.0f, 1.0f}, {1.0f, 0.9961f, 0.5882f, 1.0f}, {0.6157f, 1.0f, 0.5882f, 1.0f}, {1.0f, 0.5882f, 0.8314f, 1.0f}}; constexpr glm::vec4 FRAME_COLOR_LIGHT_ACTIVE[] = {{0.74f, 0.74f, 0.74f, 1.0f}, {0.0980f, 0.3765f, 0.6431f, 1.0f}, {1.0f, 0.5255f, 0.3333f, 1.0f}, {0.3686f, 0.5765f, 0.2353f, 1.0f}, {0.6118f, 0.2039f, 0.2745f, 1.0f}}; constexpr glm::vec4 FRAME_COLOR_LIGHT_HOVERED[] = {{0.84f, 0.84f, 0.84f, 1.0f}, {0.0752f, 0.2887f, 0.4931f, 1.0f}, {0.85f, 0.4467f, 0.2833f, 1.0f}, {0.2727f, 0.4265f, 0.1741f, 1.0f}, {0.4618f, 0.1539f, 0.2072f, 1.0f}}; constexpr glm::vec4 ITEM_COLOR_LIGHT_BASE[] = {{0.3059f, 0.3255f, 0.5412f, 1.0f}, {0.3333f, 0.5725f, 0.8392f, 1.0f}, {1.0f, 0.5412f, 0.3412f, 1.0f}, {0.5255f, 0.8471f, 0.4588f, 1.0f}, {0.7961f, 0.3882f, 0.5412f, 1.0f}}; constexpr glm::vec4 ITEM_COLOR_LIGHT_ACTIVE[] = {{0.3459f, 0.3655f, 0.5812f, 1.0f}, {0.3733f, 0.6125f, 0.8792f, 1.0f}, {0.9106f, 0.4949f, 0.2753f, 1.0f}, {0.5655f, 0.8871f, 0.4988f, 1.0f}, {0.8361f, 0.4282f, 0.5812f, 1.0f}}; constexpr glm::vec4 ITEM_COLOR_LIGHT_SELECTED[] = {{0.74f, 0.74f, 0.74f, 1.0f}, {0.2039f, 0.4549f, 0.7176f, 1.0f}, {0.8745f, 0.4392f, 0.2275f, 1.0f}, {0.3765f, 0.6784f, 0.2980f, 1.0f}, {0.6353f, 0.2235f, 0.3647f, 1.0f}}; constexpr auto FRAME_MULTIPLE = 5; constexpr auto FRAME_DRAG_PAYLOAD_ID = "Frame Drag Drop"; constexpr auto FRAME_TOOLTIP_HOVER_DELAY = 0.75f; // Extra delay for frame info tooltip. #define ITEM_FRAME_CHILD_HEIGHT ImGui::GetTextLineHeightWithSpacing() + (ImGui::GetStyle().WindowPadding.y * 1.5) #define ITEM_CHILD_WIDTH ImGui::GetTextLineHeightWithSpacing() * 12.5 void Timeline::update(Manager& manager, Settings& settings, Resources& resources, Clipboard& clipboard) { auto& document = *manager.get(); auto& anm2 = document.anm2; auto& playback = document.playback; auto& reference = document.reference; auto& frames = document.frames; auto animation = document.animation_get(); style = ImGui::GetStyle(); auto isLightTheme = settings.theme == theme::LIGHT; bool isTextPushed = false; if (isLightTheme) { ImGui::PushStyleColor(ImGuiCol_Text, TIMELINE_TEXT_COLOR_LIGHT); isTextPushed = true; } auto type_index = [](anm2::Type type) { return std::clamp((int)type, 0, (int)anm2::TRIGGER); }; auto type_color_base_vec = [&](anm2::Type type) { return isLightTheme ? FRAME_COLOR_LIGHT_BASE[type_index(type)] : anm2::TYPE_COLOR[type]; }; auto type_color_active_vec = [&](anm2::Type type) { if (isLightTheme) return FRAME_COLOR_LIGHT_ACTIVE[type_index(type)]; return anm2::TYPE_COLOR_ACTIVE[type]; }; auto type_color_hovered_vec = [&](anm2::Type type) { if (isLightTheme) return FRAME_COLOR_LIGHT_HOVERED[type_index(type)]; return anm2::TYPE_COLOR_HOVERED[type]; }; auto item_color_vec = [&](anm2::Type type) { if (!isLightTheme) return anm2::TYPE_COLOR[type]; return ITEM_COLOR_LIGHT_BASE[type_index(type)]; }; auto item_color_active_vec = [&](anm2::Type type) { if (!isLightTheme) return anm2::TYPE_COLOR_ACTIVE[type]; return ITEM_COLOR_LIGHT_ACTIVE[type_index(type)]; }; auto iconTintDefault = isLightTheme ? ICON_TINT_DEFAULT_LIGHT : ICON_TINT_DEFAULT_DARK; auto itemIconTint = isLightTheme ? ICON_TINT_DEFAULT_LIGHT : iconTintDefault; auto frameBorderColor = isLightTheme ? FRAME_BORDER_COLOR_LIGHT : FRAME_BORDER_COLOR_DARK; auto frameBorderColorReferenced = isLightTheme ? FRAME_BORDER_COLOR_REFERENCED_LIGHT : FRAME_BORDER_COLOR_REFERENCED_DARK; auto frameMultipleOverlayColor = isLightTheme ? FRAME_MULTIPLE_OVERLAY_COLOR_LIGHT : FRAME_MULTIPLE_OVERLAY_COLOR_DARK; auto textMultipleColor = isLightTheme ? TEXT_MULTIPLE_COLOR_LIGHT : TEXT_MULTIPLE_COLOR_DARK; auto playheadLineColor = isLightTheme ? PLAYHEAD_LINE_COLOR_LIGHT : PLAYHEAD_LINE_COLOR_DARK; auto playheadIconTint = isLightTheme ? PLAYHEAD_ICON_TINT_LIGHT : iconTintDefault; auto timelineBackgroundColor = isLightTheme ? TIMELINE_BACKGROUND_COLOR_LIGHT : ImGui::GetStyleColorVec4(ImGuiCol_Header); auto timelinePlayheadRectColor = isLightTheme ? TIMELINE_PLAYHEAD_RECT_COLOR_LIGHT : TIMELINE_PLAYHEAD_RECT_COLOR_DARK; auto timelineTickColor = isLightTheme ? TIMELINE_TICK_COLOR_LIGHT : frameBorderColor; auto itemTextColor = isLightTheme ? ITEM_TEXT_COLOR_LIGHT : ITEM_TEXT_COLOR_DARK; auto overlay_icon = [&](GLuint textureId, ImVec4 tint, bool isForced = false) { if (!isForced && !isLightTheme) return; auto min = ImGui::GetItemRectMin(); auto max = ImGui::GetItemRectMax(); ImGui::GetWindowDrawList()->AddImage((ImTextureID)(intptr_t)textureId, min, max, ImVec2(0, 0), ImVec2(1, 1), ImGui::GetColorU32(tint)); }; auto frames_selection_set_reference = [&]() { frames.selection.clear(); if (reference.frameIndex >= 0) frames.selection.insert(reference.frameIndex); frameSelectionSnapshot.assign(frames.selection.begin(), frames.selection.end()); frameSelectionSnapshotReference = reference; frameSelectionLocked.clear(); isFrameSelectionLocked = false; frameFocusIndex = reference.frameIndex; frameFocusRequested = reference.frameIndex >= 0; }; auto frame_insert = [&](anm2::Item* item) { if (!item) return; auto behavior = [&, item]() { if (reference.itemType == anm2::TRIGGER) { for (auto& trigger : animation->triggers.frames) if (document.frameTime == trigger.atFrame) return; anm2::Frame addFrame{}; addFrame.atFrame = document.frameTime; item->frames.push_back(addFrame); item->frames_sort_by_at_frame(); reference.frameIndex = item->frame_index_from_at_frame_get(addFrame.atFrame); } else { auto frame = document.frame_get(); if (frame) { auto addFrame = *frame; item->frames.insert(item->frames.begin() + reference.frameIndex, addFrame); reference.frameIndex++; } else if (!item->frames.empty()) { auto addFrame = item->frames.back(); item->frames.emplace_back(addFrame); reference.frameIndex = (int)(item->frames.size()) - 1; } else { item->frames.emplace_back(anm2::Frame()); reference.frameIndex = 0; } } frames_selection_set_reference(); }; DOCUMENT_EDIT(document, localize.get(EDIT_INSERT_FRAME), Document::FRAMES, behavior()); }; auto frames_delete = [&]() { if (auto item = animation->item_get(reference.itemType, reference.itemID); item) { for (auto it = frames.selection.rbegin(); it != frames.selection.rend(); ++it) { auto i = *it; item->frames.erase(item->frames.begin() + i); } if (item->frames.empty()) reference.frameIndex = -1; else reference.frameIndex = glm::clamp(--reference.frameIndex, 0, (int)item->frames.size() - 1); frames_selection_set_reference(); } }; auto frames_delete_action = [&]() { if (!document.frame_get()) return; DOCUMENT_EDIT(document, localize.get(EDIT_DELETE_FRAMES), Document::FRAMES, frames_delete()); }; auto frames_bake = [&]() { auto behavior = [&]() { if (auto item = document.item_get()) for (auto i : frames.selection | std::views::reverse) item->frames_bake(i, settings.bakeInterval, settings.bakeIsRoundScale, settings.bakeIsRoundRotation); frames.clear(); }; DOCUMENT_EDIT(document, localize.get(EDIT_BAKE_FRAMES), Document::FRAMES, behavior()); }; auto frame_split = [&]() { auto behavior = [&]() { if (reference.itemType == anm2::TRIGGER) return; auto item = document.item_get(); auto frame = document.frame_get(); if (!item || !frame) return; auto originalDuration = frame->duration; if (originalDuration <= 1) return; auto frameStartTime = item->frame_time_from_index_get(reference.frameIndex); int frameStart = (int)std::round(frameStartTime); int playheadTime = (int)std::floor(playback.time); int firstDuration = playheadTime - frameStart + 1; if (firstDuration <= 0 || firstDuration >= originalDuration) return; int secondDuration = originalDuration - firstDuration; anm2::Frame splitFrame = *frame; splitFrame.duration = secondDuration; auto nextFrame = vector::in_bounds(item->frames, reference.frameIndex + 1) ? &item->frames[reference.frameIndex + 1] : nullptr; if (frame->isInterpolated && nextFrame) { float interpolation = (float)firstDuration / (float)originalDuration; splitFrame.rotation = glm::mix(frame->rotation, nextFrame->rotation, interpolation); splitFrame.position = glm::mix(frame->position, nextFrame->position, interpolation); splitFrame.scale = glm::mix(frame->scale, nextFrame->scale, interpolation); splitFrame.colorOffset = glm::mix(frame->colorOffset, nextFrame->colorOffset, interpolation); splitFrame.tint = glm::mix(frame->tint, nextFrame->tint, interpolation); } frame->duration = firstDuration; item->frames.insert(item->frames.begin() + reference.frameIndex + 1, splitFrame); frames_selection_set_reference(); }; DOCUMENT_EDIT(document, localize.get(EDIT_SPLIT_FRAME), Document::FRAMES, behavior()); }; auto frames_selection_reset = [&]() { frames.clear(); frameSelectionSnapshot.clear(); frameSelectionLocked.clear(); isFrameSelectionLocked = false; frameFocusRequested = false; frameFocusIndex = -1; frameSelectionSnapshotReference = reference; }; auto reference_clear = [&]() { reference = {reference.animationIndex}; frames_selection_reset(); }; auto reference_set_item = [&](anm2::Type type, int id) { reference = {reference.animationIndex, type, id}; frames_selection_reset(); }; auto item_remove = [&]() { auto behavior = [&]() { if (!animation) return; if (reference.itemType == anm2::LAYER || reference.itemType == anm2::NULL_) animation->item_remove(reference.itemType, reference.itemID); reference_clear(); }; DOCUMENT_EDIT(document, localize.get(EDIT_REMOVE_ITEMS), Document::ITEMS, behavior()); }; auto fit_animation_length = [&]() { if (!animation) return; auto behavior = [&]() { animation->fit_length(); }; DOCUMENT_EDIT(document, localize.get(EDIT_FIT_ANIMATION_LENGTH), Document::ANIMATIONS, behavior()); }; auto context_menu = [&]() { ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing); auto copy = [&]() { if (frames.selection.empty()) return; if (auto item = animation->item_get(reference.itemType, reference.itemID); item) { std::string clipboardString{}; for (auto& i : frames.selection) { if (!vector::in_bounds(item->frames, i)) break; clipboardString += item->frames[i].to_string(reference.itemType); } clipboard.set(clipboardString); } }; auto cut = [&]() { copy(); DOCUMENT_EDIT(document, localize.get(EDIT_CUT_FRAMES), Document::FRAMES, frames_delete()); }; auto paste = [&]() { if (clipboard.is_empty()) return; auto behavior = [&]() { if (auto item = animation->item_get(reference.itemType, reference.itemID)) { document.snapshot(localize.get(EDIT_PASTE_FRAMES)); std::set indices{}; std::string errorString{}; int insertIndex = (int)item->frames.size(); if (!frames.selection.empty()) insertIndex = std::min((int)item->frames.size(), *frames.selection.rbegin() + 1); else if (reference.frameIndex >= 0 && reference.frameIndex < (int)item->frames.size()) insertIndex = reference.frameIndex + 1; auto start = reference.itemType == anm2::TRIGGER ? hoveredTime : insertIndex; if (item->frames_deserialize(clipboard.get(), reference.itemType, start, indices, &errorString)) { frames.selection.clear(); for (auto i : indices) frames.selection.insert(i); reference.frameIndex = *indices.begin(); document.change(Document::FRAMES); } else { toasts.push(std::format("{} {}", localize.get(TOAST_DESERIALIZE_FRAMES_FAILED), errorString)); logger.error( std::format("{} {}", localize.get(TOAST_DESERIALIZE_FRAMES_FAILED, anm2ed::ENGLISH), errorString)); } } else { toasts.push(localize.get(TOAST_DESERIALIZE_FRAMES_NO_SELECTION)); logger.warning(localize.get(TOAST_DESERIALIZE_FRAMES_NO_SELECTION, anm2ed::ENGLISH)); } }; DOCUMENT_EDIT(document, localize.get(EDIT_PASTE_FRAMES), Document::FRAMES, behavior()); }; if (shortcut(manager.chords[SHORTCUT_CUT], shortcut::FOCUSED)) cut(); if (shortcut(manager.chords[SHORTCUT_COPY], shortcut::FOCUSED)) copy(); if (shortcut(manager.chords[SHORTCUT_PASTE], shortcut::FOCUSED)) paste(); if (shortcut(manager.chords[SHORTCUT_SPLIT], shortcut::FOCUSED)) frame_split(); if (shortcut(manager.chords[SHORTCUT_BAKE], shortcut::FOCUSED)) frames_bake(); if (shortcut(manager.chords[SHORTCUT_FIT], shortcut::FOCUSED)) fit_animation_length(); if (ImGui::BeginPopupContextWindow("##Context Menu", ImGuiPopupFlags_MouseButtonRight)) { auto item = animation ? animation->item_get(reference.itemType, reference.itemID) : nullptr; if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_UNDO), settings.shortcutUndo.c_str(), false, document.is_able_to_undo())) document.undo(); if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_REDO), settings.shortcutRedo.c_str(), false, document.is_able_to_redo())) document.redo(); ImGui::Separator(); auto label = playback.isPlaying ? localize.get(LABEL_PAUSE) : localize.get(LABEL_PLAY); if (ImGui::MenuItem(label, settings.shortcutPlayPause.c_str())) playback.toggle(); if (ImGui::MenuItem(localize.get(LABEL_INSERT), settings.shortcutInsertFrame.c_str(), false, item)) frame_insert(item); if (ImGui::MenuItem(localize.get(LABEL_DELETE), settings.shortcutRemove.c_str(), false, document.frame_get())) frames_delete_action(); if (ImGui::MenuItem(localize.get(LABEL_BAKE), settings.shortcutBake.c_str(), false, !frames.selection.empty())) frames_bake(); if (ImGui::MenuItem(localize.get(LABEL_FIT_ANIMATION_LENGTH), settings.shortcutFit.c_str(), false, animation && animation->frameNum != animation->length())) fit_animation_length(); if (ImGui::MenuItem(localize.get(LABEL_SPLIT), settings.shortcutSplit.c_str(), false, frames.selection.size() == 1)) frame_split(); ImGui::Separator(); if (ImGui::MenuItem(localize.get(BASIC_CUT), settings.shortcutCut.c_str(), false, !frames.selection.empty())) cut(); if (ImGui::MenuItem(localize.get(BASIC_COPY), settings.shortcutCopy.c_str(), false, !frames.selection.empty())) copy(); if (ImGui::MenuItem(localize.get(BASIC_PASTE), settings.shortcutPaste.c_str(), false, !clipboard.is_empty())) paste(); ImGui::EndPopup(); } ImGui::PopStyleVar(2); }; auto item_base_properties_open = [&](anm2::Type type, int id) { switch (type) { case anm2::LAYER: manager.layer_properties_open(id); break; case anm2::NULL_: manager.null_properties_open(id); default: break; }; }; auto item_properties_reset = [&]() { addItemName.clear(); addItemSpritesheetID = {}; addItemID = -1; }; auto unused_items_get = [&](anm2::Type type) { if (!animation) return std::set{}; if (type == anm2::LAYER) return anm2.layers_unused(*animation); if (type == anm2::NULL_) return anm2.nulls_unused(*animation); return std::set{}; }; auto item_context_menu = [&]() { ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing); auto& type = reference.itemType; auto& id = reference.itemID; auto item = document.item_get(); if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenOverlappedByWindow) && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) ImGui::OpenPopup("##Items Context Menu"); if (ImGui::BeginPopup("##Items Context Menu")) { if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_UNDO), settings.shortcutUndo.c_str(), false, document.is_able_to_undo())) document.undo(); if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_REDO), settings.shortcutRedo.c_str(), false, document.is_able_to_redo())) document.redo(); ImGui::Separator(); if (ImGui::MenuItem(localize.get(BASIC_PROPERTIES), nullptr, false, item && (type == anm2::LAYER || type == anm2::NULL_))) item_base_properties_open(type, id); if (ImGui::MenuItem(localize.get(BASIC_ADD), settings.shortcutAdd.c_str(), false, animation)) { item_properties_reset(); propertiesPopup.open(); } if (ImGui::MenuItem(localize.get(BASIC_REMOVE), settings.shortcutRemove.c_str(), false, item)) item_remove(); ImGui::EndPopup(); } ImGui::PopStyleVar(2); }; auto item_child = [&](anm2::Type type, int id, int index) { ImGui::PushID(index); auto item = animation ? animation->item_get(type, id) : nullptr; auto isVisible = item ? item->isVisible : false; auto& isOnlyShowLayers = settings.timelineIsOnlyShowLayers; if (isOnlyShowLayers && type != anm2::LAYER) isVisible = false; auto isReferenced = reference.itemType == type && reference.itemID == id; auto label = type == anm2::LAYER ? std::vformat(localize.get(FORMAT_LAYER), std::make_format_args(id, anm2.content.layers[id].name, anm2.content.layers[id].spritesheetID)) : type == anm2::NULL_ ? std::vformat(localize.get(FORMAT_NULL), std::make_format_args(id, anm2.content.nulls[id].name)) : localize.get(anm2::TYPE_STRINGS[type]); auto icon = anm2::TYPE_ICONS[type]; auto iconTintCurrent = isLightTheme && type == anm2::NONE ? ImVec4(1.0f, 1.0f, 1.0f, 1.0f) : itemIconTint; auto baseColorVec = item_color_vec(type); auto activeColorVec = item_color_active_vec(type); auto colorVec = baseColorVec; if (isReferenced && type != anm2::NONE) { if (isLightTheme) colorVec = ITEM_COLOR_LIGHT_SELECTED[type_index(type)]; else colorVec = activeColorVec; } auto color = to_imvec4(colorVec); color = !isVisible ? to_imvec4(colorVec * COLOR_HIDDEN_MULTIPLIER) : color; ImGui::PushStyleColor(ImGuiCol_ChildBg, color); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding); auto itemSize = ImVec2(ImGui::GetContentRegionAvail().x, ITEM_FRAME_CHILD_HEIGHT); if (ImGui::BeginChild(label.c_str(), itemSize, ImGuiChildFlags_Borders, ImGuiWindowFlags_NoScrollWithMouse)) { auto cursorPos = ImGui::GetCursorPos(); if (type != anm2::NONE) { ImGui::SetCursorPos(to_imvec2(to_vec2(cursorPos) - to_vec2(style.ItemSpacing))); ImGui::SetNextItemAllowOverlap(); ImGui::PushStyleColor(ImGuiCol_Header, ImVec4()); ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4()); ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4()); ImGui::SetNextItemStorageID(id); if (ImGui::Selectable("##Item Button", isReferenced, ImGuiSelectableFlags_SelectOnClick, itemSize)) { if (type == anm2::LAYER) document.spritesheet.reference = anm2.content.layers[id].spritesheetID; reference_set_item(type, id); } ImGui::PopStyleColor(3); if (ImGui::IsItemHovered()) { if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) item_base_properties_open(type, id); auto& imguiStyle = ImGui::GetStyle(); auto previousTooltipFlags = imguiStyle.HoverFlagsForTooltipMouse; auto previousTooltipDelay = imguiStyle.HoverDelayNormal; imguiStyle.HoverFlagsForTooltipMouse = ImGuiHoveredFlags_Stationary | ImGuiHoveredFlags_DelayNormal | ImGuiHoveredFlags_AllowWhenDisabled | ImGuiHoveredFlags_NoSharedDelay; imguiStyle.HoverDelayNormal = FRAME_TOOLTIP_HOVER_DELAY; bool showItemTooltip = ImGui::BeginItemTooltip(); imguiStyle.HoverFlagsForTooltipMouse = previousTooltipFlags; imguiStyle.HoverDelayNormal = previousTooltipDelay; if (showItemTooltip) { auto yesNoLabel = [&](bool value) { return value ? localize.get(BASIC_YES) : localize.get(BASIC_NO); }; auto visibleLabel = yesNoLabel(isVisible); auto framesCount = item ? (int)item->frames.size() : 0; switch (type) { case anm2::ROOT: { ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE); ImGui::TextUnformatted(localize.get(BASIC_ROOT)); ImGui::PopFont(); ImGui::TextUnformatted( std::vformat(localize.get(FORMAT_VISIBLE), std::make_format_args(visibleLabel)).c_str()); auto transformLabel = yesNoLabel(settings.previewIsRootTransform); ImGui::TextUnformatted( std::vformat(localize.get(FORMAT_TRANSFORM), std::make_format_args(transformLabel)).c_str()); ImGui::TextUnformatted( std::vformat(localize.get(FORMAT_FRAMES_COUNT), std::make_format_args(framesCount)).c_str()); break; } case anm2::LAYER: { auto& layer = anm2.content.layers[id]; ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE); ImGui::TextUnformatted(layer.name.c_str()); ImGui::PopFont(); ImGui::TextUnformatted(std::vformat(localize.get(FORMAT_ID), std::make_format_args(id)).c_str()); ImGui::TextUnformatted( std::vformat(localize.get(FORMAT_SPRITESHEET_ID), std::make_format_args(layer.spritesheetID)) .c_str()); ImGui::TextUnformatted( std::vformat(localize.get(FORMAT_VISIBLE), std::make_format_args(visibleLabel)).c_str()); ImGui::TextUnformatted( std::vformat(localize.get(FORMAT_FRAMES_COUNT), std::make_format_args(framesCount)).c_str()); break; } case anm2::NULL_: { auto& nullInfo = anm2.content.nulls[id]; ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE); ImGui::TextUnformatted(nullInfo.name.c_str()); ImGui::PopFont(); auto rectLabel = yesNoLabel(nullInfo.isShowRect); ImGui::TextUnformatted(std::vformat(localize.get(FORMAT_ID), std::make_format_args(id)).c_str()); ImGui::TextUnformatted( std::vformat(localize.get(FORMAT_RECT), std::make_format_args(rectLabel)).c_str()); ImGui::TextUnformatted( std::vformat(localize.get(FORMAT_VISIBLE), std::make_format_args(visibleLabel)).c_str()); ImGui::TextUnformatted( std::vformat(localize.get(FORMAT_FRAMES_COUNT), std::make_format_args(framesCount)).c_str()); break; } case anm2::TRIGGER: { ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE); ImGui::TextUnformatted(localize.get(BASIC_TRIGGERS)); ImGui::PopFont(); ImGui::TextUnformatted( std::vformat(localize.get(FORMAT_VISIBLE), std::make_format_args(visibleLabel)).c_str()); ImGui::TextUnformatted( std::vformat(localize.get(FORMAT_TRIGGERS_COUNT), std::make_format_args(framesCount)).c_str()); break; } default: break; } ImGui::EndTooltip(); } } if (type == anm2::LAYER) { if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_SourceNoPreviewTooltip)) { ImGui::SetDragDropPayload("Layer Animation Drag Drop", &id, sizeof(int)); ImGui::EndDragDropSource(); } if (ImGui::BeginDragDropTarget()) { if (auto payload = ImGui::AcceptDragDropPayload("Layer Animation Drag Drop")) { auto droppedID = *(int*)payload->Data; auto layer_order_move = [&]() { int source = vector::find_index(animation->layerOrder, droppedID); int destination = vector::find_index(animation->layerOrder, id); if (source != -1 && destination != -1) vector::move_index(animation->layerOrder, source, destination); }; DOCUMENT_EDIT(document, localize.get(EDIT_MOVE_LAYER_ANIMATION), Document::ITEMS, layer_order_move()); } ImGui::EndDragDropTarget(); } } ImGui::SetCursorPos(cursorPos); ImGui::Image(resources.icons[icon].id, icon_size_get()); overlay_icon(resources.icons[icon].id, iconTintCurrent); ImGui::SameLine(); if (isReferenced) ImGui::PushFont(resources.fonts[font::ITALICS].get(), font::SIZE); ImGui::PushStyleColor(ImGuiCol_Text, itemTextColor); ImGui::TextUnformatted(label.c_str()); ImGui::PopStyleColor(); if (isReferenced) ImGui::PopFont(); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4()); ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4()); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4()); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2()); ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); ImGui::SetCursorPos( ImVec2(itemSize.x - ImGui::GetTextLineHeightWithSpacing() - ImGui::GetStyle().ItemSpacing.x, (itemSize.y - ImGui::GetTextLineHeightWithSpacing()) / 2)); int visibleIcon = item->isVisible ? icon::VISIBLE : icon::INVISIBLE; if (ImGui::ImageButton("##Visible Toggle", resources.icons[visibleIcon].id, icon_size_get())) DOCUMENT_EDIT(document, localize.get(EDIT_TOGGLE_ITEM_VISIBILITY), Document::FRAMES, item->isVisible = !item->isVisible); overlay_icon(resources.icons[visibleIcon].id, iconTintCurrent); ImGui::SetItemTooltip("%s", isVisible ? localize.get(TOOLTIP_ITEM_VISIBILITY_SHOWN) : localize.get(TOOLTIP_ITEM_VISIBILITY_HIDDEN)); if (type == anm2::NULL_) { auto& null = anm2.content.nulls.at(id); auto& isShowRect = null.isShowRect; auto rectIcon = isShowRect ? icon::SHOW_RECT : icon::HIDE_RECT; ImGui::SetCursorPos( ImVec2(itemSize.x - (ImGui::GetTextLineHeightWithSpacing() * 2) - ImGui::GetStyle().ItemSpacing.x, (itemSize.y - ImGui::GetTextLineHeightWithSpacing()) / 2)); if (ImGui::ImageButton("##Rect Toggle", resources.icons[rectIcon].id, icon_size_get())) DOCUMENT_EDIT(document, localize.get(EDIT_TOGGLE_NULL_RECT), Document::FRAMES, null.isShowRect = !null.isShowRect); overlay_icon(resources.icons[rectIcon].id, iconTintCurrent); ImGui::SetItemTooltip("%s", isShowRect ? localize.get(TOOLTIP_NULL_RECT_SHOWN) : localize.get(TOOLTIP_NULL_RECT_HIDDEN)); } ImGui::PopStyleVar(2); ImGui::PopStyleColor(3); } else { auto cursorPos = ImGui::GetCursorPos(); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4()); ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4()); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4()); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2()); ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); ImGui::SetCursorPos( ImVec2(itemSize.x - ImGui::GetTextLineHeightWithSpacing() - ImGui::GetStyle().ItemSpacing.x, (itemSize.y - ImGui::GetTextLineHeightWithSpacing()) / 2)); auto& isShowUnused = settings.timelineIsShowUnused; auto unusedIcon = isShowUnused ? icon::SHOW_UNUSED : icon::HIDE_UNUSED; if (ImGui::ImageButton("##Unused Toggle", resources.icons[unusedIcon].id, icon_size_get())) isShowUnused = !isShowUnused; overlay_icon(resources.icons[unusedIcon].id, iconTintCurrent); ImGui::SetItemTooltip("%s", isShowUnused ? localize.get(TOOLTIP_UNUSED_ITEMS_SHOWN) : localize.get(TOOLTIP_UNUSED_ITEMS_HIDDEN)); auto& showLayersOnly = settings.timelineIsOnlyShowLayers; auto layersIcon = showLayersOnly ? icon::SHOW_LAYERS : icon::HIDE_LAYERS; ImGui::SetCursorPos( ImVec2(itemSize.x - (ImGui::GetTextLineHeightWithSpacing() * 2) - ImGui::GetStyle().ItemSpacing.x, (itemSize.y - ImGui::GetTextLineHeightWithSpacing()) / 2)); if (ImGui::ImageButton("##Layers Toggle", resources.icons[layersIcon].id, icon_size_get())) showLayersOnly = !showLayersOnly; overlay_icon(resources.icons[layersIcon].id, iconTintCurrent); ImGui::SetItemTooltip("%s", showLayersOnly ? localize.get(TOOLTIP_ONLY_LAYERS_VISIBLE) : localize.get(TOOLTIP_ALL_ITEMS_VISIBLE)); ImGui::PopStyleVar(2); ImGui::PopStyleColor(3); ImGui::SetCursorPos(cursorPos); ImGui::BeginDisabled(); ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); ImGui::Text("(?)"); ImGui::PopStyleColor(); auto tooltipShortcuts = std::vformat( localize.get(TOOLTIP_TIMELINE_SHORTCUTS), std::make_format_args(settings.shortcutMovePlayheadBack, settings.shortcutMovePlayheadForward, settings.shortcutShortenFrame, settings.shortcutExtendFrame, settings.shortcutPreviousFrame, settings.shortcutNextFrame)); ImGui::SetItemTooltip("%s", tooltipShortcuts.c_str()); ImGui::EndDisabled(); } } ImGui::EndChild(); ImGui::PopStyleColor(); ImGui::PopStyleVar(2); ImGui::PopID(); }; auto items_child = [&]() { auto itemsChildSize = ImVec2(ITEM_CHILD_WIDTH, ImGui::GetContentRegionAvail().y); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2()); bool isItemsChildOpen = ImGui::BeginChild("##Items Child", itemsChildSize, ImGuiChildFlags_Borders); ImGui::PopStyleVar(); if (isItemsChildOpen) { auto itemsListChildSize = ImVec2(ImGui::GetContentRegionAvail().x, ImGui::GetContentRegionAvail().y - ImGui::GetTextLineHeightWithSpacing() - ImGui::GetStyle().ItemSpacing.y * 2); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2()); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2()); if (ImGui::BeginChild("##Items List Child", itemsListChildSize, ImGuiChildFlags_Borders, ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoScrollbar)) { ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2()); ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarSize, 0.0f); if (ImGui::BeginTable("##Item Table", 1, ImGuiTableFlags_Borders | ImGuiTableFlags_ScrollY)) { ImGui::GetCurrentWindow()->Flags |= ImGuiWindowFlags_NoScrollWithMouse; ImGui::SetScrollY(scroll.y); ImGui::TableSetupScrollFreeze(0, 1); ImGui::TableSetupColumn("##Items"); auto item_child_row = [&](anm2::Type type, int id = -1, int index = 0) { ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); item_child(type, id, index); }; item_child_row(anm2::NONE); int index{}; if (animation) { item_child_row(anm2::ROOT, -1, index++); for (auto& id : animation->layerOrder | std::views::reverse) { if (!settings.timelineIsShowUnused && animation->layerAnimations[id].frames.empty()) continue; item_child_row(anm2::LAYER, id, index++); } for (auto& [id, nullAnimation] : animation->nullAnimations) { if (!settings.timelineIsShowUnused && nullAnimation.frames.empty()) continue; item_child_row(anm2::NULL_, id, index++); } item_child_row(anm2::TRIGGER); } if (isHorizontalScroll && ImGui::GetCurrentWindow()->ScrollbarY) { ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); ImGui::Dummy(ImVec2(0, style.ScrollbarSize)); } ImGui::EndTable(); } ImGui::PopStyleVar(2); item_context_menu(); } ImGui::PopStyleVar(2); ImGui::EndChild(); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding); ImGui::SetCursorPos(ImVec2(ImGui::GetCursorPosX() + style.WindowPadding.x, ImGui::GetCursorPosY())); auto widgetSize = widget_size_with_row_get(2, ImGui::GetContentRegionAvail().x - style.WindowPadding.x); ImGui::BeginDisabled(!animation); { shortcut(manager.chords[SHORTCUT_ADD]); if (ImGui::Button(localize.get(BASIC_ADD), widgetSize)) { item_properties_reset(); propertiesPopup.open(); } set_item_tooltip_shortcut(localize.get(TOOLTIP_ADD_ITEM), settings.shortcutAdd); ImGui::SameLine(); ImGui::BeginDisabled(!document.item_get()); shortcut(manager.chords[SHORTCUT_REMOVE]); if (ImGui::Button(localize.get(BASIC_REMOVE), widgetSize)) item_remove(); set_item_tooltip_shortcut(localize.get(TOOLTIP_REMOVE_ITEMS), settings.shortcutRemove); ImGui::EndDisabled(); } ImGui::EndDisabled(); ImGui::PopStyleVar(); } ImGui::EndChild(); }; auto frame_child = [&](anm2::Type type, int id, int& index, float width) { auto item = animation ? animation->item_get(type, id) : nullptr; auto isVisible = item ? item->isVisible : false; auto& isOnlyShowLayers = settings.timelineIsOnlyShowLayers; if (isOnlyShowLayers && type != anm2::LAYER) isVisible = false; auto colorVec = type_color_base_vec(type); auto colorActiveVec = type_color_active_vec(type); auto colorHoveredVec = type_color_hovered_vec(type); auto color = to_imvec4(colorVec); auto colorActive = to_imvec4(colorActiveVec); auto colorHovered = to_imvec4(colorHoveredVec); auto colorHidden = to_imvec4(colorVec * COLOR_HIDDEN_MULTIPLIER); auto colorActiveHidden = to_imvec4(colorActiveVec * COLOR_HIDDEN_MULTIPLIER); auto colorHoveredHidden = to_imvec4(colorHoveredVec * COLOR_HIDDEN_MULTIPLIER); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding); auto childSize = ImVec2(width, ITEM_FRAME_CHILD_HEIGHT); ImGui::PopStyleVar(2); ImGui::PushID(index); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2()); bool isDefaultChild = type == anm2::NONE; if (isLightTheme && isDefaultChild) ImGui::PushStyleColor(ImGuiCol_ChildBg, TIMELINE_CHILD_BG_COLOR_LIGHT); if (ImGui::BeginChild("##Frames Child", childSize, ImGuiChildFlags_Borders)) { if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) && ImGui::Shortcut(ImGuiKey_Escape, ImGuiInputFlags_RouteFocused)) { if (!frames.selection.empty()) { reference.frameIndex = -1; frames_selection_set_reference(); } else if (reference.itemType != anm2::NONE || reference.itemID != -1) reference_clear(); } auto drawList = ImGui::GetWindowDrawList(); auto clipMax = drawList->GetClipRectMax(); auto length = animation ? animation->frameNum : 0; auto frameSize = ImVec2(ImGui::GetTextLineHeight(), ImGui::GetContentRegionAvail().y); auto framesSize = ImVec2(frameSize.x * length, frameSize.y); auto cursorPos = ImGui::GetCursorPos(); auto cursorScreenPos = ImGui::GetCursorScreenPos(); auto border = glm::max(0.5f, ImGui::GetStyle().FrameBorderSize * 0.5f); auto borderLineLength = frameSize.y / 5; auto frameMin = std::max(0, (int)std::floor(scroll.x / frameSize.x) - 1); auto frameMax = std::min(anm2::FRAME_NUM_MAX, (int)std::ceil((scroll.x + clipMax.x) / frameSize.x) + 1); pickerLineDrawList = drawList; if (type == anm2::NONE) { if (length > 0) { if (isLightTheme) { auto totalMax = ImVec2(cursorScreenPos.x + framesSize.x, cursorScreenPos.y + framesSize.y); drawList->AddRectFilled(cursorScreenPos, totalMax, ImGui::GetColorU32(timelineBackgroundColor)); float animationWidth = std::min(framesSize.x, frameSize.x * (float)length); drawList->AddRectFilled(cursorScreenPos, ImVec2(cursorScreenPos.x + animationWidth, cursorScreenPos.y + framesSize.y), ImGui::GetColorU32(timelinePlayheadRectColor)); } else { drawList->AddRectFilled(cursorScreenPos, ImVec2(cursorScreenPos.x + framesSize.x, cursorScreenPos.y + framesSize.y), ImGui::GetColorU32(ImGui::GetStyleColorVec4(ImGuiCol_Header))); } } for (int i = frameMin; i < frameMax; i++) { auto frameScreenPos = ImVec2(cursorScreenPos.x + frameSize.x * (float)i, cursorScreenPos.y); drawList->AddRect(frameScreenPos, ImVec2(frameScreenPos.x + border, frameScreenPos.y + borderLineLength), ImGui::GetColorU32(timelineTickColor), 0, 0, 0.5f); drawList->AddRect(ImVec2(frameScreenPos.x, frameScreenPos.y + frameSize.y - borderLineLength), ImVec2(frameScreenPos.x + border, frameScreenPos.y + frameSize.y), ImGui::GetColorU32(timelineTickColor), 0, 0, 0.5); if (i % FRAME_MULTIPLE == 0) { auto string = std::to_string(i); auto textSize = ImGui::CalcTextSize(string.c_str()); auto textPos = ImVec2(frameScreenPos.x + (frameSize.x - textSize.x) / 2, frameScreenPos.y + (frameSize.y - textSize.y) / 2); drawList->AddRectFilled(frameScreenPos, ImVec2(frameScreenPos.x + frameSize.x, frameScreenPos.y + frameSize.y), ImGui::GetColorU32(frameMultipleOverlayColor)); drawList->AddText(textPos, ImGui::GetColorU32(textMultipleColor), string.c_str()); } } if (ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem) && ImGui::IsMouseDown(0)) isDragging = true; auto childPos = ImGui::GetWindowPos(); auto mousePos = ImGui::GetIO().MousePos; auto localMousePos = ImVec2(mousePos.x - childPos.x, mousePos.y - childPos.y); hoveredTime = floorf(localMousePos.x / frameSize.x); if (isDragging) { playback.time = hoveredTime; playback.clamp(settings.playbackIsClamp ? length : anm2::FRAME_NUM_MAX); document.frameTime = playback.time; } playback.clamp(settings.playbackIsClamp ? length : anm2::FRAME_NUM_MAX); if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) isDragging = false; if (length > 0) { ImGui::SetCursorPos(ImVec2(cursorPos.x + frameSize.x * floorf(playback.time), cursorPos.y)); ImGui::Image(resources.icons[icon::PLAYHEAD].id, frameSize); overlay_icon(resources.icons[icon::PLAYHEAD].id, playheadIconTint, true); } } else if (animation) { float frameTime{}; if (ImGui::IsWindowHovered() && (ImGui::IsMouseReleased(ImGuiMouseButton_Left) || ImGui::IsMouseReleased(ImGuiMouseButton_Right)) && !ImGui::IsAnyItemHovered()) reference_set_item(type, id); for (int i = frameMin; i < frameMax; i++) { auto frameScreenPos = ImVec2(cursorScreenPos.x + frameSize.x * (float)i, cursorScreenPos.y); auto frameRectMax = ImVec2(frameScreenPos.x + frameSize.x, frameScreenPos.y + frameSize.y); drawList->AddRect(frameScreenPos, frameRectMax, ImGui::GetColorU32(frameBorderColor)); if (i % FRAME_MULTIPLE == 0) drawList->AddRectFilled(frameScreenPos, frameRectMax, ImGui::GetColorU32(frameMultipleOverlayColor)); } if (type != anm2::TRIGGER) frames.selection.start(item->frames.size(), ImGuiMultiSelectFlags_ClearOnEscape); for (auto [i, frame] : std::views::enumerate(item->frames)) { ImGui::PushID((int)i); auto frameReference = anm2::Reference{reference.animationIndex, type, id, (int)i}; auto isFrameVisible = isVisible && frame.isVisible; auto isReferenced = reference == frameReference; auto isSelected = (frames.selection.contains((int)i) && reference.itemType == type && reference.itemID == id); if (type == anm2::TRIGGER) frameTime = frame.atFrame; auto buttonSize = type == anm2::TRIGGER ? frameSize : to_imvec2(vec2(frameSize.x * frame.duration, frameSize.y)); auto frameStart = type == anm2::TRIGGER ? frame.atFrame : frameTime; auto frameEnd = type == anm2::TRIGGER ? frameStart + 1.0f : frameStart + frame.duration; if (frameEnd <= (float)frameMin || frameStart >= (float)frameMax) { if (type != anm2::TRIGGER) frameTime += frame.duration; ImGui::PopID(); continue; } auto buttonPos = ImVec2(cursorPos.x + (frameTime * frameSize.x), cursorPos.y); if (frameFocusRequested && frameFocusIndex == (int)i && reference == frameReference) { ImGui::SetKeyboardFocusHere(); frameFocusRequested = false; } ImGui::SetCursorPos(buttonPos); auto buttonScreenPos = ImGui::GetCursorScreenPos(); auto fillColor = isSelected ? (isFrameVisible ? colorActive : colorActiveHidden) : (isFrameVisible ? color : colorHidden); drawList->AddRectFilled(buttonScreenPos, ImVec2(buttonScreenPos.x + buttonSize.x, buttonScreenPos.y + buttonSize.y), ImGui::GetColorU32(fillColor), FRAME_ROUNDING); ImGui::PushStyleColor(ImGuiCol_Header, isFrameVisible ? colorActive : colorActiveHidden); ImGui::PushStyleColor(ImGuiCol_HeaderActive, isFrameVisible ? colorActive : colorActiveHidden); ImGui::PushStyleColor(ImGuiCol_HeaderHovered, isFrameVisible ? colorHovered : colorHoveredHidden); ImGui::PushStyleColor(ImGuiCol_NavCursor, isFrameVisible ? colorHovered : colorHoveredHidden); ImGui::SetNextItemAllowOverlap(); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, FRAME_ROUNDING); ImGui::SetNextItemSelectionUserData((int)i); bool isDifferentItem = reference.itemType != type || reference.itemID != id; if (ImGui::Selectable("##Frame Button", isSelected, ImGuiSelectableFlags_None, buttonSize)) { if (type == anm2::LAYER) document.spritesheet.reference = anm2.content.layers[id].spritesheetID; if (type != anm2::TRIGGER) { if (ImGui::IsKeyDown(ImGuiMod_Alt)) DOCUMENT_EDIT(document, localize.get(EDIT_FRAME_INTERPOLATION), Document::FRAMES, frame.isInterpolated = !frame.isInterpolated); document.frameTime = frameTime; } reference = frameReference; isReferenced = true; if (isDifferentItem) frames_selection_set_reference(); } ImGui::PopStyleVar(); ImGui::PopStyleColor(4); if (ImGui::IsItemHovered() && ImGui::IsMouseDown(ImGuiMouseButton_Left)) { if (ImGui::IsMouseDown(ImGuiMouseButton_Left)) { if (type == anm2::TRIGGER || ImGui::IsKeyDown(ImGuiMod_Ctrl)) { draggedFrame = &frame; draggedFrameType = type; draggedFrameIndex = (int)i; draggedFrameStart = hoveredTime; if (type != anm2::TRIGGER) draggedFrameStartDuration = draggedFrame->duration; } } } if (type != anm2::TRIGGER) { if (!draggedFrame && ImGui::BeginDragDropSource(ImGuiDragDropFlags_SourceNoPreviewTooltip)) { frameDragDrop = {}; frameDragDrop.type = type; frameDragDrop.itemID = id; frameDragDrop.animationIndex = reference.animationIndex; auto append_valid_indices = [&](const auto& container) { for (auto idx : container) if (idx >= 0 && idx < (int)item->frames.size()) frameDragDrop.selection.push_back(idx); }; if (isReferenced) append_valid_indices(frames.selection); auto contains_index = [&](const std::vector& container, int index) { return std::find(container.begin(), container.end(), index) != container.end(); }; if ((!contains_index(frameDragDrop.selection, (int)i) || frameDragDrop.selection.size() <= 1) && frameSelectionSnapshotReference.animationIndex == reference.animationIndex && frameSelectionSnapshotReference.itemType == type && frameSelectionSnapshotReference.itemID == id && contains_index(frameSelectionSnapshot, (int)i)) { frameDragDrop.selection = frameSelectionSnapshot; frames.selection.clear(); for (int idx : frameSelectionSnapshot) if (idx >= 0 && idx < (int)item->frames.size()) frames.selection.insert(idx); frameSelectionLocked = frameDragDrop.selection; isFrameSelectionLocked = true; } if (frameDragDrop.selection.empty()) frameDragDrop.selection.push_back((int)i); std::sort(frameDragDrop.selection.begin(), frameDragDrop.selection.end()); frameDragDrop.selection.erase( std::unique(frameDragDrop.selection.begin(), frameDragDrop.selection.end()), frameDragDrop.selection.end()); ImGui::SetDragDropPayload(FRAME_DRAG_PAYLOAD_ID, &frameDragDrop, sizeof(FrameDragDrop)); ImGui::EndDragDropSource(); } if (!draggedFrame && ImGui::BeginDragDropTarget()) { if (auto payload = ImGui::AcceptDragDropPayload(FRAME_DRAG_PAYLOAD_ID)) { auto source = static_cast(payload->Data); auto sameAnimation = source && source->animationIndex == reference.animationIndex; auto sourceItem = sameAnimation && animation ? animation->item_get(source->type, source->itemID) : nullptr; auto targetItem = animation ? animation->item_get(type, id) : nullptr; auto time_from_index = [&](anm2::Item* target, int index) { if (!target || target->frames.empty()) return 0.0f; index = std::clamp(index, 0, (int)target->frames.size()); float timeAccum = 0.0f; for (int n = 0; n < index && n < (int)target->frames.size(); ++n) timeAccum += target->frames[n].duration; return timeAccum; }; if (source && sourceItem && targetItem && source->type != anm2::TRIGGER && type != anm2::TRIGGER) { std::vector indices = source->selection; if (indices.empty()) indices.push_back((int)i); std::sort(indices.begin(), indices.end()); indices.erase(std::unique(indices.begin(), indices.end()), indices.end()); int insertPosResult = -1; int insertedCount = 0; DOCUMENT_EDIT(document, localize.get(EDIT_MOVE_FRAMES), Document::FRAMES, { std::vector movedFrames; movedFrames.reserve(indices.size()); for (int i : indices) if (i >= 0 && i < (int)sourceItem->frames.size()) movedFrames.push_back(std::move(sourceItem->frames[i])); for (auto it = indices.rbegin(); it != indices.rend(); ++it) if (*it >= 0 && *it < (int)sourceItem->frames.size()) sourceItem->frames.erase(sourceItem->frames.begin() + *it); const int dropIndex = (int)i; int desired = std::clamp(dropIndex + 1, 0, (int)targetItem->frames.size()); if (sourceItem == targetItem) { if (dropIndex < indices.front()) desired = dropIndex; else if (dropIndex > indices.back()) desired = dropIndex + 1; else desired = indices.front(); int removedBefore = 0; for (int i : indices) if (i < desired) ++removedBefore; desired -= removedBefore; } desired = std::clamp(desired, 0, (int)targetItem->frames.size()); insertPosResult = desired; insertedCount = (int)movedFrames.size(); targetItem->frames.insert(targetItem->frames.begin() + insertPosResult, std::make_move_iterator(movedFrames.begin()), std::make_move_iterator(movedFrames.end())); }); if (insertedCount > 0) { frames.selection.clear(); for (int offset = 0; offset < insertedCount; ++offset) frames.selection.insert(insertPosResult + offset); reference = {reference.animationIndex, type, id, insertPosResult}; document.frameTime = time_from_index(targetItem, reference.frameIndex); if (type == anm2::LAYER) { document.spritesheet.reference = anm2.content.layers[id].spritesheetID; } } } } ImGui::EndDragDropTarget(); } } auto rectMin = ImGui::GetItemRectMin(); auto rectMax = ImGui::GetItemRectMax(); auto borderColor = isReferenced ? frameBorderColorReferenced : frameBorderColor; auto borderThickness = isReferenced ? FRAME_BORDER_THICKNESS_REFERENCED : FRAME_BORDER_THICKNESS; drawList->AddRect(rectMin, rectMax, ImGui::GetColorU32(borderColor), FRAME_ROUNDING, 0, borderThickness); auto icon = type == anm2::TRIGGER ? icon::TRIGGER : frame.isInterpolated ? icon::INTERPOLATED : icon::UNINTERPOLATED; auto iconPos = ImVec2(cursorPos.x + (frameTime * frameSize.x), cursorPos.y + (frameSize.y / 2) - (icon_size_get().y / 2)); ImGui::SetCursorPos(iconPos); ImGui::Image(resources.icons[icon].id, icon_size_get()); overlay_icon(resources.icons[icon].id, iconTintDefault); if (type != anm2::TRIGGER) frameTime += frame.duration; ImGui::PopID(); } if (type != anm2::TRIGGER) frames.selection.finish(); if (isFrameSelectionLocked) { frames.selection.clear(); for (int idx : frameSelectionLocked) frames.selection.insert(idx); isFrameSelectionLocked = false; frameSelectionLocked.clear(); } if (reference.itemType == type && reference.itemID == id) { frameSelectionSnapshot.assign(frames.selection.begin(), frames.selection.end()); frameSelectionSnapshotReference = reference; } } } if (draggedFrame) { ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); if (!isDraggedFrameSnapshot && hoveredTime != draggedFrameStart) { isDraggedFrameSnapshot = true; document.snapshot(draggedFrameType == anm2::TRIGGER ? localize.get(EDIT_TRIGGER_AT_FRAME) : localize.get(EDIT_FRAME_DURATION)); } if (draggedFrameType == anm2::TRIGGER) { draggedFrame->atFrame = glm::clamp(hoveredTime, 0, settings.playbackIsClamp ? animation->frameNum - 1 : anm2::FRAME_NUM_MAX - 1); for (auto [i, trigger] : std::views::enumerate(animation->triggers.frames)) { if ((int)i == draggedFrameIndex) continue; if (trigger.atFrame == draggedFrame->atFrame) draggedFrame->atFrame--; } } else { draggedFrame->duration = glm::clamp(draggedFrameStartDuration + (hoveredTime - draggedFrameStart), anm2::FRAME_DURATION_MIN, anm2::FRAME_DURATION_MAX); } if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) { document.change(Document::FRAMES); draggedFrame = nullptr; draggedFrameType = anm2::NONE; draggedFrameIndex = -1; draggedFrameStart = -1; draggedFrameStartDuration = -1; isDraggedFrameSnapshot = false; if (type == anm2::TRIGGER) item->frames_sort_by_at_frame(); } } context_menu(); ImGui::EndChild(); if (isLightTheme && isDefaultChild) ImGui::PopStyleColor(); ImGui::PopStyleVar(); index++; ImGui::PopID(); }; auto frames_child = [&]() { auto cursorPos = ImGui::GetCursorPos(); ImGui::SetCursorPos(ImVec2(cursorPos.x + ITEM_CHILD_WIDTH, cursorPos.y)); auto framesChildSize = ImVec2(ImGui::GetContentRegionAvail().x, ImGui::GetContentRegionAvail().y); if (ImGui::BeginChild("##Frames Child", framesChildSize, ImGuiChildFlags_Borders)) { auto viewListChildSize = ImVec2(ImGui::GetContentRegionAvail().x, ImGui::GetContentRegionAvail().y - ImGui::GetTextLineHeightWithSpacing() - style.ItemSpacing.y * 2); auto childWidth = anm2.animations.length() * ImGui::GetTextLineHeight(); if (animation && animation->frameNum > anm2.animations.length()) childWidth = animation->frameNum * ImGui::GetTextLineHeight(); else if (ImGui::GetContentRegionAvail().x > childWidth) childWidth = ImGui::GetContentRegionAvail().x; ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2()); if (ImGui::BeginChild("##Frames List Child", viewListChildSize, true, ImGuiWindowFlags_HorizontalScrollbar)) { auto cursorScreenPos = ImGui::GetCursorScreenPos(); ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2()); if (ImGui::BeginTable("##Frames List Table", 1, ImGuiTableFlags_Borders | ImGuiTableFlags_ScrollX | ImGuiTableFlags_ScrollY)) { ImGuiWindow* window = ImGui::GetCurrentWindow(); window->Flags |= ImGuiWindowFlags_NoScrollWithMouse; scroll.x = ImGui::GetScrollX(); scroll.y = ImGui::GetScrollY(); isHorizontalScroll = window->ScrollbarX; if (isWindowHovered) { auto& io = ImGui::GetIO(); auto lineHeight = ImGui::GetTextLineHeightWithSpacing() * 2; scroll.x -= io.MouseWheelH * lineHeight; scroll.y -= io.MouseWheel * lineHeight; } ImGui::SetScrollX(scroll.x); ImGui::SetScrollY(scroll.y); int index{}; ImGui::TableSetupScrollFreeze(0, 1); ImGui::TableSetupColumn("##Frames"); auto frames_child_row = [&](anm2::Type type, int id = -1) { ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); frame_child(type, id, index, childWidth); }; frames_child_row(anm2::NONE); if (animation) { ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 2.0f); frames_child_row(anm2::ROOT); for (auto& id : animation->layerOrder | std::views::reverse) { if (auto item = animation->item_get(anm2::LAYER, id); item) if (!settings.timelineIsShowUnused && item->frames.empty()) continue; frames_child_row(anm2::LAYER, id); } for (const auto& entry : animation->nullAnimations) { auto id = entry.first; if (auto item = animation->item_get(anm2::NULL_, id); item) if (!settings.timelineIsShowUnused && item->frames.empty()) continue; frames_child_row(anm2::NULL_, id); } frames_child_row(anm2::TRIGGER); ImGui::PopStyleVar(); } ImGui::EndTable(); } ImDrawList* windowDrawList = ImGui::GetWindowDrawList(); auto frameSize = ImVec2(ImGui::GetTextLineHeight(), ImGui::GetTextLineHeightWithSpacing() + (ImGui::GetStyle().WindowPadding.y * 2)); auto playheadIndex = std::floor(playback.time); auto lineCenterX = cursorScreenPos.x + frameSize.x * (playheadIndex + 0.6f) - scroll.x; float lineOffsetY = frameSize.y; auto linePos = ImVec2(lineCenterX - (PLAYHEAD_LINE_THICKNESS * 0.5f), cursorScreenPos.y + frameSize.y + lineOffsetY); auto lineSize = ImVec2((PLAYHEAD_LINE_THICKNESS / 2.0f), viewListChildSize.y - frameSize.y - lineOffsetY - (isHorizontalScroll ? ImGui::GetStyle().ScrollbarSize : 0.0f)); auto rectMin = windowDrawList->GetClipRectMin(); auto rectMax = windowDrawList->GetClipRectMax(); if (pickerLineDrawList) { pickerLineDrawList->PushClipRect(rectMin, rectMax); pickerLineDrawList->AddRectFilled(linePos, ImVec2(linePos.x + lineSize.x, linePos.y + lineSize.y), ImGui::GetColorU32(playheadLineColor)); pickerLineDrawList->PopClipRect(); } ImGui::PopStyleVar(); } ImGui::EndChild(); ImGui::PopStyleVar(); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding); ImGui::SetCursorPos( ImVec2(ImGui::GetStyle().WindowPadding.x, ImGui::GetCursorPos().y + ImGui::GetStyle().ItemSpacing.y)); auto widgetSize = widget_size_with_row_get(10); ImGui::BeginDisabled(!animation); { auto label = playback.isPlaying ? localize.get(LABEL_PAUSE) : localize.get(LABEL_PLAY); auto tooltip = playback.isPlaying ? localize.get(TOOLTIP_PAUSE_ANIMATION) : localize.get(TOOLTIP_PLAY_ANIMATION); shortcut(manager.chords[SHORTCUT_PLAY_PAUSE]); if (ImGui::Button(label, widgetSize)) playback.toggle(); set_item_tooltip_shortcut(tooltip, settings.shortcutPlayPause); ImGui::SameLine(); auto item = animation->item_get(reference.itemType, reference.itemID); ImGui::BeginDisabled(!item); { shortcut(manager.chords[SHORTCUT_INSERT_FRAME]); if (ImGui::Button(localize.get(LABEL_INSERT), widgetSize)) frame_insert(item); set_item_tooltip_shortcut(localize.get(TOOLTIP_INSERT_FRAME), settings.shortcutInsertFrame); ImGui::SameLine(); ImGui::BeginDisabled(!document.frame_get()); { shortcut(manager.chords[SHORTCUT_REMOVE]); if (ImGui::Button(localize.get(LABEL_DELETE), widgetSize)) frames_delete_action(); set_item_tooltip_shortcut(localize.get(TOOLTIP_DELETE_FRAMES), settings.shortcutRemove); ImGui::SameLine(); if (ImGui::Button(localize.get(LABEL_BAKE), widgetSize)) bakePopup.open(); set_item_tooltip_shortcut(localize.get(TOOLTIP_BAKE_FRAMES), settings.shortcutBake); } ImGui::EndDisabled(); } ImGui::EndDisabled(); ImGui::SameLine(); ImGui::BeginDisabled(!animation || animation->frameNum == animation->length()); shortcut(manager.chords[SHORTCUT_FIT]); if (ImGui::Button(localize.get(LABEL_FIT_ANIMATION_LENGTH), widgetSize)) fit_animation_length(); set_item_tooltip_shortcut(localize.get(TOOLTIP_FIT_ANIMATION_LENGTH), settings.shortcutFit); ImGui::EndDisabled(); ImGui::SameLine(); auto frameNum = animation ? animation->frameNum : dummy_value(); ImGui::SetNextItemWidth(widgetSize.x); if (input_int_range(localize.get(LABEL_ANIMATION_LENGTH), frameNum, anm2::FRAME_NUM_MIN, anm2::FRAME_NUM_MAX, STEP, STEP_FAST, !animation ? ImGuiInputTextFlags_DisplayEmptyRefVal : 0)) DOCUMENT_EDIT(document, localize.get(EDIT_ANIMATION_LENGTH), Document::ANIMATIONS, animation->frameNum = frameNum); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_ANIMATION_LENGTH)); ImGui::SameLine(); auto isLoop = animation ? animation->isLoop : dummy_value(); ImGui::SetNextItemWidth(widgetSize.x); if (ImGui::Checkbox(localize.get(LABEL_LOOP), &isLoop)) DOCUMENT_EDIT(document, localize.get(EDIT_LOOP), Document::ANIMATIONS, animation->isLoop = isLoop); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_LOOP_ANIMATION)); } ImGui::EndDisabled(); ImGui::SameLine(); auto fps = anm2.info.fps; ImGui::SetNextItemWidth(widgetSize.x); if (input_int_range(localize.get(LABEL_FPS), fps, anm2::FPS_MIN, anm2::FPS_MAX)) DOCUMENT_EDIT(document, localize.get(EDIT_FPS), Document::ANIMATIONS, anm2.info.fps = fps); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_FPS)); ImGui::SameLine(); auto createdBy = anm2.info.createdBy; ImGui::SetNextItemWidth(widgetSize.x); if (input_text_string(localize.get(LABEL_AUTHOR), &createdBy)) DOCUMENT_EDIT(document, localize.get(EDIT_AUTHOR), Document::ANIMATIONS, anm2.info.createdBy = createdBy); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_AUTHOR)); ImGui::SameLine(); ImGui::SetNextItemWidth(widgetSize.x); ImGui::Checkbox(localize.get(LABEL_SOUND), &settings.timelineIsSound); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_SOUND)); ImGui::PopStyleVar(); } ImGui::EndChild(); ImGui::SetCursorPos(cursorPos); }; ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2()); if (ImGui::Begin(localize.get(LABEL_TIMELINE_WINDOW), &settings.windowIsTimeline)) { isWindowHovered = ImGui::IsWindowHovered(ImGuiHoveredFlags_RootAndChildWindows); frames_child(); items_child(); } ImGui::PopStyleVar(); ImGui::End(); propertiesPopup.trigger(); if (ImGui::BeginPopupModal(propertiesPopup.label(), &propertiesPopup.isOpen, ImGuiWindowFlags_NoResize)) { auto item_properties_close = [&]() { item_properties_reset(); propertiesPopup.close(); }; auto& type = settings.timelineAddItemType; auto& destination = settings.timelineAddItemDestination; auto& source = settings.timelineAddItemSource; auto footerSize = footer_size_get(); auto optionsSize = child_size_get(11); auto itemsSize = ImVec2(0, ImGui::GetContentRegionAvail().y - (optionsSize.y + footerSize.y + ImGui::GetStyle().ItemSpacing.y * 4)); if (ImGui::BeginChild("##Options", optionsSize, ImGuiChildFlags_Borders)) { ImGui::SeparatorText(localize.get(LABEL_TYPE)); auto size = ImVec2(ImGui::GetContentRegionAvail().x * 0.5f, ImGui::GetFrameHeightWithSpacing()); if (ImGui::BeginChild("##Type 1", size)) { ImGui::RadioButton(localize.get(LABEL_LAYER), &type, anm2::LAYER); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_LAYER_TYPE)); } ImGui::EndChild(); ImGui::SameLine(); if (ImGui::BeginChild("##Type 2", size)) { ImGui::RadioButton(localize.get(LABEL_NULL), &type, anm2::NULL_); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_NULL_TYPE)); } ImGui::EndChild(); ImGui::SeparatorText(localize.get(LABEL_SOURCE)); if (ImGui::BeginChild("##Source 1", size)) { ImGui::RadioButton(localize.get(LABEL_NEW), &source, source::NEW); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_NEW_ITEM)); } ImGui::EndChild(); ImGui::SameLine(); if (ImGui::BeginChild("##Source 2", size)) { auto isUnusedItems = animation && !unused_items_get((anm2::Type)type).empty(); ImGui::BeginDisabled(!isUnusedItems); ImGui::RadioButton(localize.get(LABEL_EXISTING), &source, source::EXISTING); ImGui::EndDisabled(); ImGui::SetItemTooltip("%s", isUnusedItems ? localize.get(TOOLTIP_USE_EXISTING_ITEM) : localize.get(TOOLTIP_NO_UNUSED_ITEMS)); } ImGui::EndChild(); ImGui::SeparatorText(localize.get(LABEL_DESTINATION)); if (ImGui::BeginChild("##Destination 1", size)) { ImGui::RadioButton(localize.get(LABEL_ALL_ANIMATIONS), &destination, destination::ALL); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_ITEM_ALL_ANIMATIONS)); } ImGui::EndChild(); ImGui::SameLine(); if (ImGui::BeginChild("##Destination 2", size)) { ImGui::RadioButton(localize.get(LABEL_THIS_ANIMATION), &destination, destination::THIS); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_ITEM_THIS_ANIMATION)); } ImGui::EndChild(); ImGui::SeparatorText(localize.get(LABEL_OPTIONS)); ImGui::BeginDisabled(source == source::EXISTING); { input_text_string(localize.get(BASIC_NAME), &addItemName); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_ITEM_NAME)); if (type == anm2::LAYER) { ImGui::Combo(localize.get(LABEL_SPRITESHEET), &addItemSpritesheetID, document.spritesheet.labels.data(), (int)document.spritesheet.labels.size()); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_LAYER_SPRITESHEET)); } else if (type == anm2::NULL_) { ImGui::Checkbox(localize.get(LABEL_RECT), &addItemIsShowRect); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_NULL_RECT)); } } ImGui::EndDisabled(); } ImGui::EndChild(); if (ImGui::BeginChild("##Items", itemsSize, ImGuiChildFlags_Borders)) { if (animation && source == source::EXISTING) { auto unusedItems = unused_items_get((anm2::Type)type); if (addItemID != -1 && !unusedItems.contains(addItemID)) addItemID = -1; for (auto id : unusedItems) { auto isSelected = addItemID == id; ImGui::PushID(id); if (type == anm2::LAYER) { auto& layer = anm2.content.layers[id]; auto label = std::vformat(localize.get(FORMAT_LAYER), std::make_format_args(id, layer.name, layer.spritesheetID)); if (ImGui::Selectable(label.c_str(), isSelected)) { addItemID = id; addItemSpritesheetID = layer.spritesheetID; } } else if (type == anm2::NULL_) { auto& null = anm2.content.nulls[id]; auto label = std::vformat(localize.get(FORMAT_NULL), std::make_format_args(id, null.name)); if (ImGui::Selectable(label.c_str(), isSelected)) { addItemID = id; addItemIsShowRect = null.isShowRect; } } ImGui::PopID(); } } } ImGui::EndChild(); auto widgetSize = widget_size_with_row_get(2); ImGui::BeginDisabled(source == source::EXISTING && addItemID == -1); if (ImGui::Button(localize.get(BASIC_ADD), widgetSize)) { anm2::Reference addReference{}; document.snapshot(localize.get(EDIT_ADD_ITEM)); if (type == anm2::LAYER) addReference = anm2.layer_animation_add({reference.animationIndex, anm2::LAYER, addItemID}, addItemName, addItemSpritesheetID, (destination::Type)destination); else if (type == anm2::NULL_) addReference = anm2.null_animation_add({reference.animationIndex, anm2::NULL_, addItemID}, addItemName, addItemIsShowRect, (destination::Type)destination); document.change(Document::ITEMS); reference_set_item(addReference.itemType, addReference.itemID); item_properties_close(); } ImGui::EndDisabled(); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_ADD_ITEM)); ImGui::SameLine(); if (ImGui::Button(localize.get(BASIC_CANCEL), widgetSize)) item_properties_close(); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_CANCEL_ADD_ITEM)); ImGui::EndPopup(); } bakePopup.trigger(); if (ImGui::BeginPopupModal(bakePopup.label(), &bakePopup.isOpen, ImGuiWindowFlags_NoResize)) { auto& interval = settings.bakeInterval; auto& isRoundRotation = settings.bakeIsRoundRotation; auto& isRoundScale = settings.bakeIsRoundScale; auto frame = document.frame_get(); input_int_range(localize.get(LABEL_INTERVAL), interval, anm2::FRAME_DURATION_MIN, frame ? frame->duration : anm2::FRAME_DURATION_MIN); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_INTERVAL)); ImGui::Checkbox(localize.get(LABEL_ROUND_ROTATION), &isRoundRotation); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_ROUND_ROTATION)); ImGui::Checkbox(localize.get(LABEL_ROUND_SCALE), &isRoundScale); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_ROUND_SCALE)); auto widgetSize = widget_size_with_row_get(2); if (ImGui::Button(localize.get(LABEL_BAKE), widgetSize)) { frames_bake(); bakePopup.close(); } ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_BAKE_FRAMES_OPTIONS)); ImGui::SameLine(); if (ImGui::Button(localize.get(BASIC_CANCEL), widgetSize)) bakePopup.close(); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_CANCEL_BAKE_FRAMES)); ImGui::EndPopup(); } if (animation) { if (shortcut(manager.chords[SHORTCUT_PLAY_PAUSE], shortcut::GLOBAL)) playback.toggle(); if (shortcut(manager.chords[SHORTCUT_MOVE_PLAYHEAD_BACK], shortcut::GLOBAL)) { playback.decrement(settings.playbackIsClamp ? animation->frameNum : anm2::FRAME_NUM_MAX); document.frameTime = playback.time; } if (shortcut(manager.chords[SHORTCUT_MOVE_PLAYHEAD_FORWARD], shortcut::GLOBAL)) { playback.increment(settings.playbackIsClamp ? animation->frameNum : anm2::FRAME_NUM_MAX); document.frameTime = playback.time; } static bool isShortenChordHeld = false; auto isShortenFrame = shortcut(manager.chords[SHORTCUT_SHORTEN_FRAME], shortcut::GLOBAL); if (isShortenFrame && !isShortenChordHeld) document.snapshot(localize.get(EDIT_SHORTEN_FRAME)); if (isShortenFrame) { if (auto frame = document.frame_get()) { frame->shorten(); document.change(Document::FRAMES); } } isShortenChordHeld = isShortenFrame; static bool isExtendChordHeld = false; auto isExtendFrame = shortcut(manager.chords[SHORTCUT_EXTEND_FRAME], shortcut::GLOBAL); if (isExtendFrame && !isExtendChordHeld) document.snapshot(localize.get(EDIT_EXTEND_FRAME)); if (isExtendFrame) { if (auto frame = document.frame_get()) { frame->extend(); document.change(Document::FRAMES); } } isExtendChordHeld = isExtendFrame; if (shortcut(manager.chords[SHORTCUT_PREVIOUS_FRAME], shortcut::GLOBAL)) { if (auto item = document.item_get(); !item->frames.empty()) { reference.frameIndex = glm::clamp(--reference.frameIndex, 0, (int)item->frames.size() - 1); frames_selection_set_reference(); document.frameTime = item->frame_time_from_index_get(reference.frameIndex); } } if (shortcut(manager.chords[SHORTCUT_NEXT_FRAME], shortcut::GLOBAL)) { if (auto item = document.item_get(); !item->frames.empty()) { reference.frameIndex = glm::clamp(++reference.frameIndex, 0, (int)item->frames.size() - 1); frames_selection_set_reference(); document.frameTime = item->frame_time_from_index_get(reference.frameIndex); } } } if (isTextPushed) ImGui::PopStyleColor(); } }